事件回顾:评论创建失败 + 推送通知错误
这次事件回顾比以往任何一次都更难分享,因为尽管问题很严重,但它只影响了我们用户群中很小一部分人。然而,我们从中吸取了一些极其宝贵的教训,我认为也应该让其他人有机会从我们的错误中吸取教训。
语境
7月8日星期四,我们合并了一个大型PR,该PR更新了我们的代码,开始使用新的用户设置来替代已弃用的用户字段。将这些字段移至用户设置的目的是为了简化用户模型,并使这些类型的设置更易于针对每个论坛进行配置。
问题
7月9日中午前后,我们收到一份错误报告,指出前端的评论创建功能似乎出现了故障。团队成员随即开始调查此问题。通过Honeycomb工具,我们确认问题始于用户设置PR的部署,因此该PR被列为主要嫌疑对象。
我立即深入研究了 PR,并很快找到了出错的代码行。
我们错误地user_ids从用于收集用户 ID 以发送移动推送通知的过滤规则中移除了范围。这导致我们尝试获取所有已开启移动通知用户的通知设置和 ID,而这经常超时。这些超时通常发生在评论创建之后,因此大多数情况下评论仍然存在,但我们始终无法向前端返回成功响应。这导致前端卡顿,给用户造成了糟糕的体验。
使固定
为了解决这个问题,我们迅速恢复了权限范围并进行了部署。错误和超时问题立即消失,评论创建功能恢复正常。
此时,我们得出结论,可能发送了一些额外的通知,但大部分请求都已超时,所以我们认为目前情况良好。我检查了应用程序中是否存在某种推送通知模型,可能存储了未发送的通知,但没有找到,因此我们认为问题已经解决。
更多问题
7月9日中午12点左右(美国东部时间):很遗憾,上述假设并不正确。就在午夜前不久,一位团队成员报告说她的手机收到了一些随机通知。此时,我们的移动工程师立即展开调查,发现Redis中积压了大量推送通知。
7 月 9 日凌晨 2 点左右(美国东部时间):移动工程师在我们的 #emergency Slack 频道中发布了有关此问题的帖子。
7月9日早上7点左右(美国东部时间):值班工程师醒来后看到Slack上的#emergency消息,立即加入调查,尝试解决问题。不久之后,我也加入了进来。正是在这时,我了解了我们推送通知系统的工作原理。我们使用RPush与各种推送通知服务进行通信。RPush将推送通知数据存储在Redis中,以实现快速的插入和删除。我们很快发现,尽管解决方案前一天才部署,但Redis中仍然有大量推送通知排队等待发送。
修复方案 2
意识到所有这些记录都存储在 Redis 中后,我们采取了以下步骤来解决这个问题:
- 从 Heroku 移除 iOS 证书,以防止发送任何通知。移除此证书后,我们可以确保不再发送错误通知,从而为修复 Redis 中的数据争取时间。
- 为了安全起见,我们从 Sidekiq 中删除了所有任务
PushNotifications::DeliveryWorker,以免频繁地向 Redis 请求键值,导致键值收集速度变慢。- 同时,我们遍历了 Redis 中的所有键,并收集了所有 rpush 键。这些键包括所有已发送和未发送的键。由于有 120 万个 rpush 键,因此这个过程大约耗时 30 分钟。我们使用了以下脚本:
redis = Rails.cache.redis
start = 0
total = 0
key_collection = []
index, keys = redis.scan(start);
while index != "0"
start = index
total += keys.count
keys.each do |key|
key_collection << key if key.include?('rpush:notifications')
end
index, keys = redis.scan(start)
end
- 收集到钥匙后,我们仔细核对了所有钥匙是否是我们想要的,然后使用以下代码分批删除了所有钥匙,每次删除 10 把:
key_collection.each_slice(10){|s| puts redis.del(s)}
- 密钥丢失后,我们再次核对了数量,然后将 iOS 证书重新添加到 Heroku。
- Heroku 重启后,我们进行了测试,以确认通知已恢复发送,并且我们在 Redis 中再次正确记录了已送达的通知。
影响
评论创建
7月8日用户设置PR部署后,前端评论创建流程中断了30个小时。但是,评论创建量保持稳定。
irb(main):001:0> Comment.where(created_at: 36.hours.ago..Time.now).count
=> 762
irb(main):002:0> Comment.where(created_at: 72.hours.ago..36.hours.ago).count
=> 857
irb(main):003:0> Comment.where(created_at: 108.hours.ago..72.hours.ago).count
=> 852
irb(main):004:0> Comment.where(created_at: 144.hours.ago..108.hours.ago).count
=> 756
irb(main):005:0> Comment.where(created_at: 180.hours.ago..144.hours.ago).count
=> 587
总共有 934 条评论受到前端界面故障的影响。
推送通知
鉴于 DEV 的移动端发展尚处于早期阶段,目前只有 0.2% 的用户注册了能够接收推送通知的设备。这意味着只有 0.2% 的用户受到了此次事件的影响。我们很幸运能够及时发现并解决这些问题,并将此次事件的影响控制在很小的范围内。
由于受影响的用户数量较少,我们决定主动通过电子邮件联系所有这些用户,表示歉意并解释他们为什么可能会收到错误的通知。
学习
大型公关活动
大型 PR 自然会带来更多风险。引发这一系列事件的 bug 源于一个大型 PR 中一个很小的改动,而这个改动被多人忽略了。首先,该行代码的差异分析并没有提供太多帮助来识别导致问题的改动。此外,由于代码行数众多,很容易被遗漏。将 PR 拆分是避免这种情况的一种方法。
然而,有时需要进行大规模、广泛的更改,在这种情况下,您必须依靠您的测试套件。
缺少测试
出问题的功能完全未经测试。如果我们事先对这个功能进行了充分的测试,我认为很有可能就能发现这个漏洞。推送热修复补丁后,我立即添加了一条测试,以确保此类问题不再发生。
技术特征教育
修复漏洞后,我们这些参与解决问题的人对推送通知系统的工作原理都不太了解。由于周五已经很晚了,我们只是粗略地浏览了一下代码,就觉得应该没问题了。结果证明这个假设是错误的,因为我们周六才了解到,推送通知与应用通知不同,它是通过 Redis 存储和排队的。我认为,在推出这些大型功能时,我们比以往任何时候都更需要互相分享和学习相关知识。
值得庆幸的是,我们确实有一些很棒的推送通知文档,但在事件发生期间却没人去查阅。有没有办法让这些文档更容易被找到?事件发生时,我们都埋头于代码,或许我们应该在代码中添加指向文档的 URL 链接?
更有意识地
周五晚上我本可以很轻松地联系我们的移动团队,再次确认推送通知功能是否一切正常。然而,周五晚上我又一次因为忙于工作而放弃了质疑和核实,转而接受了自己的假设。
预防此类事件的一个简单方法是使用检查清单。检查清单(在航空领域应用广泛,因为它已被证明是预防事故的关键)可以轻松确保您不会遗漏任何事项,并使您的决策更加深思熟虑。我们的内部 Gitbook 中有用于处理 Heroku 事件的优秀检查清单。但是,我们目前还没有通用的事件值班检查清单,我们计划添加一个。
事件响应
在深入探讨细节之前,我想指出,这是我们一段时间以来遇到的第一起大型事件。事件发生频率低固然是好事!但这同时也意味着我们的事件响应机制有些生疏,现有的事件处理流程也略显过时,难以应对这种情况。接下来,我们将仔细审视所有这些方面,确保它们能够根据我们应用程序和团队的当前状况,保持其有效性和实用性。
评论创建流程故障报告流程
最初的评论创建问题是由一位开发用户通过 GitHub Issues 报告的,大约 11 小时后才被我们的内部工程团队发现。GitHub Issues 对我们来说并非紧急报告渠道,因为它们会在工作时间内处理。我们可以改进这一流程的一个方法是进一步强调,紧急支持问题需要通过电子邮件发送至 [电子邮件地址] yo@forem.com。我们的客户成功团队始终密切关注这些渠道,可以更快地对问题进行分类并上报给相关团队。
除了手动报告之外,理想情况下,我们应该通过监控程序自动发现这个问题,并在检测到 HTTP 请求错误增加时发出警报。
今后,我们计划设置监控系统,以便在出现这些变化时第一时间收到警报,而不是依赖用户报告问题。如果监控系统到位,我们本可以在几个小时内发现问题,而不是花费一天多的时间。
推送通知故障报告流程
评论创建功能失效的问题一出现,我们就立即做出了响应,但后续的推送通知问题却没有及时解决。我们的工程师在发现通知出现问题后,正确地在 #emergency 频道发出了 ping 请求,但他并没有将问题上报,也没有通过 PagerDuty 通知值班开发人员。
我们可以简化这个流程的一个方法是,设置 #emergency Slack 频道,使其在发布消息时自动通知 PageDuty。另一个方法是加强团队对预期事件响应流程的培训,以解决我们事件响应经验不足的问题。这可能需要定期开展培训,以防止随着系统的发展,流程和知识过时。
技术补救
我们在尝试解决这个问题时遇到的一个延迟是,Rpush 键与我们的 Rails 缓存存储在同一个 Redis 实例中。由于我们的 Rails 缓存非常庞大,包含超过 400 万个键,遍历所有键来查找 Rpush 键既繁琐又耗时。此外,该缓存的访问量也非常大,导致我们在尝试对其执行较繁重的操作时遇到了超时问题。
我们可以通过为 Rpush 使用单独的 Redis 实例,或者将这些键放在与 Rails 缓存键不同的独立数据库中来缓解这种摩擦。展望未来,随着我们移动平台的不断发展,我认为我们应该做出上述更改之一,以确保能够轻松访问这些键和记录。这也能确保随着推送通知的扩展,我们不必担心会影响其他系统,例如我们的核心 Rails 应用。
谢谢大家🤗
很多人牺牲了自己的私人时间,在周五和周六齐心协力解决这些问题。我衷心感谢Forem团队的出色表现以及他们对这款软件的奉献精神。在此过程中,我们对系统有了更深入的了解,确保这次事件不会白费。我希望其他人也能从中吸取经验教训。
文章来源:https://dev.to/devteam/incident-retro-failing-comment-creation-erroneous-push-notifications-55dj


