本文与姚悦合著

本文汇集了Twitter自使用当前事故追踪JIRA系统(2012年)至2022年间,至少部分归因于缓存的严重事故(SEV-0SEV-1,即最高严重级别分类)信息,另包含一起2012年前的额外事故。不计额外事故,事故追踪器中至少部分归因于缓存的事故包括6起SEV-0和6起SEV-1,另有38起严重程度较低的事故,本文不予讨论。

我们记录这些内容有几个原因。首先,科技公司内部发生的事件历史知识流失率相当高,我们认为保留部分内容是有益的。其次,从特定角度审视事故与可靠性,将所有信息集中呈现,有时能让某些模式变得非常清晰。

关于知识流失,当我们看到关于某科技公司事件的病毒式Twitter帖子或其他爆款故事时,深入调查后会发现,最广为流传的版本通常严重失实,原因往往很平常。其一,夸张离谱的故事更容易病毒式传播,因此这些版本更易被记住。其二,存在一个由前总监/副总裁组成的“小作坊”,他们热衷于讲述自我吹嘘的丰功伟绩,委婉地说,这些故事常常扭曲事实(尽管个人贡献者也可能如此,但我们所见传播最广的虚假故事多来自管理层人士)。这两种情况都体现了某种故事领域的格雷欣法则,即错误故事往往战胜正确故事。

即便真心试图了解真相,知识也会迅速流失。在我们进行的这项及其他事故分析项目中,过去几年的文档和工单链接通常有效(90%以上概率),但更早的链接存活率较低,到2012年时几乎降至0%。有时,人们会将信息藏匿于加密文档、邮件等中,但这些内容常链接到现已完全失效的资源,要弄清真相需与多人交谈,而由于人类记忆的特性,你会得到需要拼凑的不一致叙述1

从特定角度审视问题时,广泛审视失败案例并分类整理所有故障固然有用,但深入剖析特定类型的故障同样重要。例如,丽贝卡·艾萨克斯与丹·卢曾对推特(2018至2020年)的故障转移测试进行内部非公开分析,发现了多项促使运营变革的要素。从某种意义上说,该分析并未提供新信息——所有结论均源自已有文档——但将分散信息整合后,那些逐年逐案审视时难以察觉的规律便清晰浮现。

这份文档不会对推特产生任何变革影响,因为针对缓存故障的规律性分析及应对措施早已完成。不过,将这些案例集中呈现,或许仍能为推特以外的从业者提供参考。

至于为何聚焦缓存故障(而非其他系统故障),缓存系统在重大事故中频繁“现身”。正如姚在推特内部“战争故事”分享会上的评论(以运营视角描述推特的“黑暗时代”):

迄今为止的每起事故都至少提及缓存。事实上,在很长一段时间里,缓存很可能是导致网站宕机的头号元凶。

入职头六个月,每次重启缓存服务器,按今天的标准都算得上SEV-0级事故。运气好的时候,重启一台缓存后,网站外部请求成功率(SR)勉强能维持在95%……

此外,推特绝大多数缓存系统基于(某个分支的)memcached2——该方案在其他平台广泛使用,因此相关经验比讨论完全定制的推特系统更具普适性。

更广义而言,缓存系统因其简洁性,成为展现分布式系统常见故障模式的绝佳案例。从概念上看,缓存服务器本质上是“高吞吐低延迟RPC服务器”与“数据管理库(管理内存/磁盘及键值索引)“的结合体。对于内存缓存而言,数据管理端理应轻松超越RPC端性能(朴素的内存键值库单核可达百万级QPS,而未采用用户态网络、批处理或流水线技术的朴素RPC服务器,性能甚至难达其十分之一)。正因RPC栈之外的部分足够简单,缓存可近似视为纯RPC工作负载的缩影——这类负载在重度服务化架构中往往至关重要。

当规模和性能成为关键考量时,缓存通常会采用分片集群,这使其面临分布式系统的约束和陷阱(但与强一致性分布式数据库等其他工作负载相比,由于更注重性能,对同步问题的关注度较低)。此外,由于分布式系统的本质,缓存使用者会暴露在这些故障模式中,并可能因某些分布式系统故障的级联效应而变得脆弱,甚至被卷入故障中。

缓存的故障模式之所以值得关注,还在于当缓存承担了大部分请求或数据服务时,缓存的中断甚至性能下降都可能轻易引发全面瘫痪——因为为缓存性能而设计的架构,其底层数据库存储性能通常不足以(也不应该)支撑整个网站的持续运行。

与其他工作负载相比,缓存对底层堆栈(如内核、固件、硬件等)的性能异常更为敏感。这是因为缓存通常具有高吞吐量和低延迟的服务等级目标(SLO)(缓存存在的意义就是快),并且(除非使用用户态网络等技术)大量时间消耗在内核层(以Twitter memcached运行常规内核网络为例,约80%的时间在内核)。此外,由于缓存服务器通常运行少量线程,它们相对容易被共享相同底层资源(CPU、内存、磁盘等)的其他工作负载“饿死”。高吞吐量和低延迟的SLO会加剧正反馈循环,导致经典的分布式系统故障模式——“死亡螺旋”。

在分析以下事件时,我们会发现大多数故障并非源于缓存逻辑本身的错误,而是某种异常触发了未充分缓解的正反馈循环,最终演变为失控的反馈循环。

因此,阅读以下事件时,建议重点关注缓存如何与堆栈上层调用缓存的组件、以及堆栈下层与缓存交互的组件进行互动。另一个值得注意的现象是:许多重大事故源于对先前事故的修复不彻底,或工程师认为严重的运维问题未被优先处理。这些也是Rebecca Isaacs和Dan Luu在分析故障转移测试失败原因时发现的常见主题。

2011年8月(SEV-0级事故)

在数月时间里,用户发起的重大变更(如用户名、显示名称和密码)有相当比例会被回滚。此风险在此后两年间仍持续存在。

背景

当时Rails应用采用单线程工作进程,由单个主进程管理健康检查、重新部署等操作。若某个工作进程卡顿超过30秒,主进程会将其终止并重启。

团队当时直接在裸金属上运行,没有使用像Mesos或Kubernetes这样的集群管理器。团队完全拥有硬件所有权,并负责内核升级等操作。

决定键值分配到哪个分片的算法涉及哈希运算。如果某个节点离线,原本哈希到该节点的键值会被重新哈希到其他节点。每个工作节点都有一个客户端,该客户端独立做出路由决策,以确定要访问哪个缓存分片——这意味着每个工作节点独立判断哪些缓存节点在线以及键值应存储的位置。如果某个客户端认为某台主机不再“健康”,该主机就会被标记为“剔除”。

事件经过

11月8日,一名用户将其用户名从[旧用户名]更改为[新用户名]。一周后,该用户名又恢复为[旧用户名]。

从11月8日到12月初,客服人员提交了数十个此类工单。由于Twitter缺乏相应的监控工具来定位问题根源,前两周的调查主要是在Rails应用中添加指标,以理解问题来源。每次修改都需要与部署团队协调,耗时至少两小时。当Rails应用完成充分监控后,所有线索都指向缓存是问题根源。为了真正确定缓存是否为罪魁祸首,又花了一到两周时间完成全套修改,包括添加缓存不一致性、缓存异常路径和主机剔除等指标。

在添加监控后,一位工程师于12月初在JIRA工单中写下如下评论:

我今天启用了代码,以便在将缓存中的用户数据写回数据库时,观察缓存用户与数据库用户不同步的程度。这个比例大约为0.2%……检查了Twitter上150个热门用户,查看他们存在于多少个缓存中(理论上最多应为一个)。大多数用户至少存在于两个缓存中,有些甚至多达六个。

第一个修复方案是避免将过期数据写回数据库。但这并未解决同一数据在不同缓存分片中存在多个副本的问题。第二个修复方案旨在减少键出现在多个位置的次数,即在驱逐主机前进行多次重试。其思路是:若主机确实永久宕机,系统会触发告警,但当时并未触发死主机告警,因此导致主机被驱逐的错误应是暂时性的——只要客户端持续重试,就能在“预期位置”找到键。随后,为防止因主机频繁出现暂时性错误导致键值“抖动”,被驱逐主机的驱逐时长被延长。

该变更先在单个缓存上测试,随后推广至其他缓存。但立即全量部署导致网站宕机:驱逐仍会发生,而更长的驱逐时间使后端压力骤增。当时后端使用MySQL,其配置在高负载下可能任意延长请求响应时间。这导致工作进程返回结果耗时过长,主进程被迫终止工作进程——当此情况大规模发生时,因可用工作进程不足,网站彻底瘫痪。

回滚第二个修复后,用户仍会看到过期数据:虽然过期数据不再写回数据库,但缓存更新可能发生在某个位置的键上,而客户端可能在另一位置读取到该键的过期缓存副本。另一项缓解措施是将用户数据缓存从高利用率集群迁移至低利用率集群。

进一步调试发现,重试机制可解决因“随机”尾部延迟导致的驱逐,但仍有大量驱逐源于非随机因素。通过监控指标观察到,有时存在高丢包率,且与入包速率相关而非带宽占用。检查高丢包率时段的主机状态发现,CPU0有65%-70%的时间用于处理软中断,表明丢包很可能源于CPU0无法跟上数据包到达速率。

解决方案是设置中断亲和性,将入包处理分散到所有物理核心。部署该修复后,用户数据迁移至的新集群仍出现丢包和缓存不一致现象,但旧集群未再出现此问题。

此时已是12月下旬。查看其他集群时,发现部分集群也存在丢包现象。进一步观察发现,某些特定机器每20小时40分钟就会发生一次丢包。所有出现此问题的机器均采用特定硬件SKU并搭载特定BIOS版本(最新版本;同SKU但使用较早BIOS版本的机器运行正常)。经查明,搭载此BIOS版本的主机会触发BMC每20小时40分钟执行一次极其耗时的健康检查,期间会中断内核运行,导致无法处理任何数据包,从而引发丢包。

后来发现,内核团队约六个月前已注意到此问题,并试图推送一项内核配置变更来修复(通过增大数据包环形缓冲区大小,防止缓冲区溢出时瞬态问题导致丢包)。尽管该工单标记为已解决,但修复方案因不明原因从未广泛部署。

采取的快速缓解措施是错开主机重启时间,避免整个集群在同一时间出现协调性丢包。

由于BMC版本需与BIOS版本匹配且BMC无法回退,因此无法通过回滚BIOS来修复。为同步升级BMC和BIOS,HWENG团队不得不紧急进行测试/认证,以最快速度完成,随后部署了BIOS修复,丢包问题随之消失。

整个过程总计耗时约两个月。

然而这并非彻底修复——主机驱逐行为仍未改变,任何导致部分客户端(而非全部)驱逐缓存分片的随机问题仍会引发不一致。修复此问题需要更改缓存架构,无法快速完成(耗时约两年)。

缓解措施/修复方案

  • 增加可见性
  • 设置IRQ亲和性以避免CPU0过载
  • 修复导致主机周期性丢包的固件问题
  • 将缓存架构改为能容忍分区而不产生不一致的架构

经验教训

  • 需要可见性
  • 需要底层系统知识来运维缓存
  • 进行隔离变更(导致问题复杂化的一个因素是:在推送IRQ亲和性修复的同时迁移到新集群,这令人困惑地同时修复了一个丢包问题又引入了另一个问题)。

2012年7月(SEV-1级)

约10%的用户在约10小时内无法看到个性化趋势,显示为空白的趋势框。

部署Rails应用更新后,趋势缓存停止返回结果。此问题仅影响非个性化趋势(因其直接由Rails提供),个性化趋势由独立服务提供。

两小时后,确认问题源于刷新趋势缓存的守护进程出现段错误,而段错误是由于内存耗尽导致的。根本原因是部署的变更向趋势对象添加了一个Thrift字段,导致趋势缓存刷新守护进程的内存使用量超出限制。

虽然存在针对趋势缓存守护进程失败的告警,但该告警仅检查守护进程是否成功启动运行,而非是否成功完成运行。

缓解措施/修复方案:

  • 增加ulimit限制
  • 将告警条件从作业启动改为作业成功
  • 在全局仪表板中添加全局404错误率指标

经验教训

  • 告警应以作业成功为判断标准,而非作业启动

2012年7月(SEV-0)

这是Twitter外部知名度较高的事件之一,因为此次事故导致公开错误页面显示,且无图片或CSS样式:

Twitter目前因<% = reason %>宕机

我们预计将在<% = deadline %>恢复

网站受到严重影响约四小时。

由于该时期的记录极不完整(相关JIRA工单注明“本次事故已进行深度事后分析和审查。关闭事故工单。”),但关于事故的书面文档大多已丢失,因此信息较为零散。

此次事故的触发因素是两排机架断电。对缓存的影响方面,48台主机断电,一小时后恢复供电时重启。其中37台主机的缓存未能恢复,因为脚本预期存在的目录未挂载到这些主机上。“手动”修复这些主机的布局耗时30分钟,随后缓存恢复正常。

该目录对于运行缓存服务器并非必需(至少按当时Twitter的运行方式如此)。但存在一个启动时检查该目录是否存在的脚本,而该目录一个月前已从布局设置脚本中移除,但检查脚本未同步更新。

另一个延长调试时间的原因是主机重启时/proc未正确挂载。虽然这不是根本问题,但异常情况耗费了一些时间才确认其与事故无关,属于需单独处理的非紧急问题。

如果网站其他部分运行正常,上述缓存问题本不会导致如此严重的事故。但多个问题叠加,最终造成持续较长时间的全面网站瘫痪。

其他问题包括:

  • 本应在5秒超时的慢请求未能超时。相反,它们会持续30秒,直到处理该慢请求的整个工作进程被杀死并重启
    • 本应触发5秒超时的代码确实被执行了,但它没有使用正确的时间戳来计算持续时间,因此未能触发超时
  • 用户数据服务恢复耗时很长
    • 故障期间的日志记录消耗了大量资源,并导致极高的GC压力
  • 大量非缓存主机在重启后无法恢复,问题包括卡在fsck或陷入PXE启动循环
  • 尽管网站和错误信息是静态的,但故障页面使用了Ruby通配符,导致模板消息显示给用户
    • 这是因为Twitter最近从使用Rails应用作为前端迁移到了C++前端;错误相关的资源被直接复制过来,仍然包含ERB模板
  • CSS未能加载,因为网站CSS本应加载的部分已经宕机
  • 前端过载,并且在健康检查发现分片不健康时未能正确重启

缓存缓解/修复措施:

  • 修复配置布局的软件,以避免未来出现此问题
  • 审计现有主机,修复任何当时受影响的主机上的问题
  • 确保内核升级时挂载/proc
  • 创建更新/升级配置布局的软件的流程,以降低引入未来错误的概率
  • 确保缓存主机(以及其他主机)更均匀地分布在故障域中

其他缓解/修复措施(非常不完整):

  • 在可观测性机器上设置磁盘/RAID健康与维护
  • 将损坏/不健康的主机发送给SiteOps进行修复
  • 从故障页面中移除Ruby通配符
  • 将CSS打包到故障页面中,以便当其他服务宕机但故障页面正常时,网站CSS仍能工作
  • 在前端添加负载削减功能,在过载时丢弃流量
  • 将用户数据服务的日志记录库更换为成本更低的库,以防止错误率高时GC压力导致服务崩溃
  • 修复5秒超时,使其查看正确的头部
  • 在堆栈的不同层级添加独立的超时机制,当请求完全无法推进时也应触发
  • 修改前端健康检查和重启逻辑,强制杀死节点,而不是尝试优雅关闭
  • 确保在任何给定时间,一个节点上只运行一个版本的健康检查脚本

经验教训:

  • 需要主动测试故障模式,包括会导致主机重启或超时的故障模式
  • 需要制定机架多样性要求,这样丢失几个机架不会对少数服务造成不成比例的影响

2013年1月(SEV-0)

网站宕机3小时30分钟

负载增加(据我所知,这是当天的正常情况,并非异常负载峰值)导致缓存的尾部延迟上升。缓存尾部延迟上升的原因是新的缓存主机未设置IRQ亲和性,从而导致队列长度增加,进而引发延迟升高。

缓存延迟增加,加上推文服务使用缓存的设计,导致使用缓存的该服务分片进入GC死亡螺旋(延迟增加 → 未完成请求增多 → GC压力增大 → 分片负载上升 → 延迟进一步增加),进而导致剩余分片的负载增加。

当时,推文服务缓存和用户数据缓存被部署在同一台机器上,每台机器包含1个推文服务缓存分片和2个用户数据缓存分片。推文服务缓存新增主机时未出现问题。随后,用户数据缓存在一个晚上逐步新增主机,初期也未出现异常。但当早间流量高峰到来时(早间是流量高峰,因为此时接近亚洲和美国的峰值使用时间——亚洲国家通常在非“朝九晚五”工作时间达到使用高峰,而美国则在工作时间达到峰值),触发了IRQ亲和性问题。推文服务受IRQ亲和性问题的影响远大于用户数据服务。

缓解措施/修复方案

  • 根据2011年8月的事件,需要为缓存主机设置IRQ亲和性
    • 将此设为机器默认配置,而非让缓存主机单独进行一次性修改
  • 修改推文服务设置
    • 减少最大连接数
    • 增加超时时间
    • 未进行GC配置更改,因为当时GC统计信息未作为指标导出,且GC日志记录的信息不足以判断不良GC设置是否为影响因素
  • 修改所有使用缓存的服务设置
    • 将连接限制调整为稳态值的约2倍

2013年9月(SEV-1)

某个数据中心的整体站点成功率降至92%。用户受影响约15分钟。

时间线服务无法访问其使用的其中一个缓存的大约75%。缓存团队对该缓存进行了服务器集变更,但时间线服务未使用推荐的机制来消费缓存服务器集路径,因此不“知道”哪些服务器是缓存服务器。

缓解措施/修复方案

  • 让时间线服务使用推荐的机制来查找服务器集路径
  • 审计所有消费服务器集路径的代码,确保没有服务使用非推荐机制处理服务器集路径

2014年1月(SEV-0)

某个数据中心的站点宕机,导致请求被路由到该数据中心的用户受影响20分钟。

该推文服务开始向缓存发送高负载请求。当时最近的一项更改移除了可向缓存建立的连接数上限。当时,当缓存连接数达到约16万时,它们将无法接受新连接。这导致监控服务无法连接到缓存分片,进而导致监控服务重启缓存分片,最终引发服务中断。

在故障发生前的几个月里,共有五个工单描述了导致故障的各种因素。

其中一个工单是对一次较轻微事件的后续跟进——该事件由不良C状态配置与SMI共同引发。工单中指出,缓存连接数达到约16万时便停止接受新连接。一名工程师对此问题进行了详细调试,查明了原因,并提出了若干可能的缓解方案。

其中一个因素是:尤其在缓存高负载时,即使内核已建立TCP连接,缓存也可能尚未accept该连接。

客户端并不“知道”连接实际上并未真正对缓存开放,因此会发送请求并等待响应。如果Finagle“认为”需要更高并发度,它可能会建立多个连接。150毫秒后,请求将超时。如果缓存端的队列较长,此时缓存很可能尚未开始处理该请求。

超时后,Finagle会重试并再次建立新连接,导致每次重试都使缓存分片过载加剧。

在客户端,每个此类请求都会产生大量内存分配,造成严重的GC压力。

当时,配置允许在标记节点不可用30秒前最多发送5次请求,连接并行度为16,且每个客户端尝试连接3台服务器。当所有这些数值乘以分片数量后,推文服务便能在缓存停止接受连接前达到其处理极限。

在缓存端,存在一个调度线程和N个工作线程。调度线程负责调用listenaccept,然后将任务放入工作线程的队列。默认情况下,积压队列长度为1024。当因文件描述符限制导致accept失败时,调度线程会将listen的积压队列设为0,并忽略所有发往监听文件描述符的事件。当某个连接关闭释放文件描述符后,积压队列会恢复正常,连接重新被接受。

在此次重大故障之前,已观察到当连接数“过高”后,连接开始被拒绝。经过一段时间,被拒绝连接产生的背压会使缓存得以恢复。

该问题的另一个因素是:在某款硬件SKU上,当系统在高缓存负载下耗尽32kB页面时会发生OOM,这进一步增加了未OOM缓存的负载。该问题由Twitter的一名内核工程师修复。

提交 96c7a2ff21501691587e1ae969b83cbec8b78e08
作者: Eric W. Biederman <ebiederm@xmission.com>
日期: 2014年2月10日 14:25:41 -0800

fs/file.c:fdtable: 避免 alloc_fdmem 触发 OOM

最近,由于每秒连接数激增,3台独立服务器上的 memcached 在 accept 时触发了 OOM killer。触发 OOM killer 时,zone 1 中 36GB 内存还有 4GB 空闲。问题在于 alloc_fdtable 分配了一个 order 3 的页(32KiB)来存储位图,而内存碎片化严重,导致最大可用页只有 8KiB。

我认为 PAGE_ALLOC_COSTLY_ORDER 不会失败这一逻辑相当可疑,但我确实同意 order 3 分配成功的可能性很高。

在内存充足的情况下,order > 0 的分配仍然可能失败,这始终存在病理情况。根据鸽巢原理,很容易证明:需要超过 50% 的页面空闲且多出 1 页,才能保证 order 1(8KiB)分配成功;需要超过 75% 的页面空闲且多出 1 页,才能保证 order 2(16KiB)分配成功;需要超过 87.5% 的页面空闲且多出 1 页,才能保证 order 3 分配成功。

像 memcached 这样频繁处理小请求和小响应的服务器,会不断搅动内存,这通常会导致大页面可用的概率降低。

因此,我们不应给外部应用程序提供实际可行的途径来杀死 Linux 服务器应用,并在 alloc_fdmem 的 kmalloc 中指定 __GFP_NORETRY。除非我误读了代码,否则当代码到达 __alloc_pages_slowpath 中的 should_alloc_retry(此时 __GFP_NORETRY 变得有意义)时,我们已经尝试了所有合理的分配页面的方法,剩下的唯一选择就是等待。因此,即使没有触发 OOM killer 的风险,不等待并立即回退到 vmalloc 似乎是合理的做法。

Signed-off-by: "Eric W. Biederman" <ebiederm@xmission.com>
    Cc: Eric Dumazet <eric.dumazet@gmail.com>
    Acked-by: David Rientjes <rientjes@google.com>
    Cc: Cong Wang <cwang@twopensource.com>
    Cc: <stable@vger.kernel.org>
    Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
    Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

这也是为什么像 Twitter 这样规模的公司从拥有内核团队中获得价值的另一个例子。

另一个工单指出了为缓存主机设置标准化参数(如 IRQ 亲和性、C 状态、睿频加速、NIC 绑定和固件版本)的重要性,这是对另一个工单的跟进。该工单指出,推文服务有时在某些主机上出现延迟升高,最终确定是由于内核升级后,与固件版本之间的某些交互导致 SMIs 增加,影响了一种硬件 SKU 类型。

缓存缓解措施/修复

  • 将积压从 1024 减少到 128,以便在调度器过载时更快地施加背压
  • 降低文件描述符限制,避免某些分片耗尽内存
  • 在缓存中使用固定哈希表大小,避免哈希表迁移期间大量分配以及内存/CPU 负载
  • 在低延迟 memcache 主机上使用 CPU 亲和性

采用这些缓解措施的测试表明,即使不修复客户端以防止其“试图”压垮缓存,这些措施也能防止缓存在类似事件条件下崩溃。

推文服务缓解措施/修复

  • 更改超时、重试和并发客户端连接设置,避免缓存过载

经验教训

  • 一致的硬件设置很重要
  • 在施加背压前允许高队列深度可能很危险
  • 客户端在设置重试策略时应“计算清楚”,避免使用当 100% 响应失败且施加最大背压时可能完全压垮缓存服务器的重试策略

2014 年 3 月(SEV-0)

Ellen 的一条推文在奥斯卡颁奖典礼期间被频繁转发,导致搜索服务宕机约 25 分钟,并引发网站故障,使许多用户无法正常使用网站。

此次事件涉及多个环节。从缓存角度来看,这是另一个因客户端行为不当导致缓存过载的案例。

这与我们之前分析的 2014 年 1 月事件类似,但不同之处在于,针对该事件实施的缓存端缓解措施并不足够,因为“攻击性”客户端选择了比 2014 年 1 月事件中推文服务更激进的参数。此外,此时部分缓存已在共享 Mesos 上的容器化环境中运行,使其容易受到节流死亡螺旋的影响。

解决这一直接问题的主要措施是向 Finagle memcached 客户端添加流水线功能,使大多数客户端仅需 1 或 2 个连接即可获得足够的吞吐量,从而降低客户端持续冲击缓存直至其崩溃的概率。

对于其他服务,跨多个服务实施了近 50 项修复。修复的主要主题包括:

  • 在适当位置添加背压
    • 避免在施加背压时重试
  • 确保数据(大部分)流向同一数据中心,避免昂贵且缓慢的跨数据中心流量
  • 创建适当的线程池,防止关键工作被饿死
  • 为热门项目添加进程内缓存
  • 在高负载下为查询返回不完整结果 / 如果结果不完整,不拒绝请求
  • 创建指南,说明如何配置缓存客户端以避免对缓存进行 DDoS 攻击

2016 年 1 月(SEV-0)

SMAP,这个前日本男子偶像团体后来成为广受欢迎的成人J-Pop组合,同时也是日本收视率常居榜首的综艺节目主持人,曾召开记者会虚假否认即将解散的传闻。这导致一个数据中心发生故障,约20分钟内路由到该数据中心的用户受到影响,直到该数据中心被切换下线。受影响数据中心的服务约六小时后才完全恢复。

该数据中心的一个推文服务出现负载激增,导致39个缓存分片主机因内存溢出(OOM)而终止进程。由于故障节点过多,集群管理器未自动将这些死节点从服务器集中移除(若仅有少量节点故障,系统会自动移除;但若故障节点过多,为避免自动操作可能加剧灾难性故障——因为从缓存服务器集中移除节点可能导致持久化存储流量激增——此类变更不会自动化处理)。当缓存值班人员手动清理死节点后,本应重启这些节点的服务却未能执行,原因是某次Puppet变更意外删除了该服务重启节点所需的缓存相关配置。待有问题的Puppet提交回滚后,缓存分片虽重新上线,但初始恢复速度过慢,随后又过快恢复,导致推文服务成功率恢复耗时较长。

缓存分片主机发生OOM的根本原因是内核套接字缓冲区内存分配过多。

初步解决方案是将主机TCP缓冲区大小限制为4GB,但该方案未通过压力测试。经排查,运行时间较长(2年)的主机存在内存碎片化问题,因此缓解措施是提高主机重启频率以清理碎片。

缓解措施/修复方案

  • 将主机重启频率从两年一次提升至更短周期
  • 为缓存服务器添加Puppet告警机制,以检测破坏性Puppet变更
  • 优化集群管理器对大规模变更的处理能力(因先前较小规模事故已启动相关改进)

2016年2月(SEV-1级事故)

这是上述2016年1月SEV-0事故中提及的压力测试失败事件。该事件导致网站成功率在数分钟内轻度下降,直至压力测试终止。

2016年7月(SEV-1级事故)

计划中的用户数据缓存从专用主机迁移至Mesos的操作,导致一个数据中心服务严重降级,另一个数据中心出现轻度降级。部分现有用户受到影响,所有新用户注册功能基本中断约半小时。

集群管理器以最快速度向服务器集新增了115个缓存实例,导致缓存命中率下降。缓存集群管理器本应每20分钟添加1个分片,但配置变更意外修改了最小缓存集群大小,这“迫使”集群管理器以最快速度添加节点。

一次性添加如此多节点导致用户数据缓存命中率从正常的99.8%骤降至84%。为防止情况恶化,运维人员终止了集群管理器以阻止其继续向服务器集添加节点,随后将集群管理器重新部署至先前状态以恢复原有配置,用户数据缓存命中率随即回升。

在缓存命中率下降期间,后端数据库遭遇流量激增,引发长时间GC停顿。这导致未命中缓存的用户数据服务请求在查询后端数据库时成功率降至0%。

尽管已设置速率限制以防止后端数据库过载,但阈值过高未能触发。为恢复后端数据库,运维人员执行了滚动重启并部署了严格速率限制。由于上述原因导致一个数据中心被故障转移,严格速率限制在另一数据中心被触发——因为一个数据中心的故障转移导致另一数据中心流量激增。这导致用户数据服务成功率轻微下降,因为请求被严格速率限制拒绝,这也是本次事件影响未受原始缓存故障波及的数据中心的原因。

缓解措施/修复方案

  • 新增部署钩子,在操作人员向缓存集群添加或移除大量节点时发出警告
  • 在运行手册中补充关于部署、集群创建、扩容、缩容等操作的详细信息
  • 为所有“0级”(关键)缓存部署添加检查清单

2018年4月(SEV-0级事件)

一次计划中的测试数据中心故障转移导致部分站点中断约1小时。故障转移开始1分钟后即发现成功率下降。测试立即回滚,但站点完全恢复耗时近一小时。

初始站点性能下降源于用户数据服务错误率上升,其根本原因是缓存热键。系统本设有缓存热键机制,通过采样1%的事件(采用采样方式以降低开销,其逻辑是若某个键为热键,即便采样也应能被发现)并将采样键存入FIFO队列,配合哈希映射统计各键在队列中的出现次数。

尽管此方法在之前的高负载事件中有效,但在某些情况下(当值较大时)效果并不理想(但并非事件根本原因),因为1%的采样率导致缓存无法在存在大值(因此成本较高)时足够快地“识别”热键。最初的热键检测逻辑是为推文服务缓存设计的,其中最大的键约为5KB。随后该逻辑被用于其他缓存,而这些缓存的键可能大得多。用户数据缓存最初并未考虑热键设计,因为在设计热键提升机制时,用户数据缓存尚未出现热键问题——当时本应成为最热键的数据项已通过进程内缓存提供服务。

大键问题因使用FNV1-32进行键哈希而加剧,该算法忽略了最低有效字节。导致问题的数据集在最后一个字节中包含大量差异,因此使用FNV1-32导致所有大值键被存储在少量缓存分片上。早在2014年就有人建议至少迁移FNV1-32,并已在工具库中添加了更现代的哈希函数,但部分缓存所有者选择不迁移。

由于热键提升逻辑未触发,热缓存分片的流量使这些分片的NIC带宽饱和——这些分片使用1Gb NIC承载热键(Twitter硬件通常异构,除非有人确保集群仅具有特定特性;尽管许多缓存主机配备10Gb NIC,但仍有大量主机使用1Gb NIC)。

修复/缓解措施:

  • 调整用户数据缓存的热键检测参数
  • 将相关缓存集群的所有硬件升级为配备10Gb NIC的主机
  • 将部分缓存从FNV切换为murmur3

2018年6月(SEV-1级事件)

在测试数据中心故障切换期间,某些操作的成功率骤降至约50%,直至约四分钟后测试故障切换被中止。

从缓存角度来看,问题在于推文服务缓存分片处理的流量远低于预期(约为基于非代表性负载测试结果的50%),导致推文服务缓存配置不足。导致负载测试设置不切实际的因素包括:

此外,缓存性能下降的一个原因是:每分钟会运行一次基于容器的性能计数器采集,持续十秒。由于采集的计数器数量远超硬件计数器数量,内核需要执行昂贵的操作来切换计数器,因此该过程相当昂贵。

性能下降在采集性能计数器的时间窗口内显著增加了延迟,导致缓存分片在达到容器节流限制前无法完成工作,延迟恶化到推文服务请求超时的程度。根据配置,若单个缓存节点连续失败12次,推文服务客户端会将该节点标记为死亡30秒,并停止向其发送请求。由于客户端独立决定标记节点死亡,该节点在30秒内无任何流量。这导致请求率上升,超出后端数据库的请求配额,请求被数据库拒绝,进一步增加了推文服务的失败率。

缓解措施/修复方案

  • 将推文服务客户端到缓存的连接数从4减少到2,降低了延迟。
    • 如之前事故所述,引入流水线后,缓存仅需1个客户端连接即可高效运行。但部分工程师担心1个连接可能不够(此前连接数远高于此),因此“以防万一”选择了4个连接。然而,在标准Linux内核网络下,更多连接会增加尾部延迟,反而降低了性能。
  • 增加缓存节点数量,以减轻单个缓存分片的负载。
  • 改进缓存热键提升算法。
    • 此问题并非本次事故特有,但一位工程师分析发现,一年前引入的热键提升算法因多种问题,导致某个缓存集群的命中率约为0.3%。改用更优算法后,缓存命中率和性能显著提升。
  • 修改缓存资格认证流程,使用于确定容量(节点数量)的缓存性能更准确地反映实际缓存性能。
  • 对多路复用性能计数器采集的成本进行详细分析。

感谢 Reforge - 工程项目Flatirons Development 通过以主要赞助商级别赞助我帮助本文得以发布。

此外,感谢 Michael Leinartas、Tao L.、Michael Motherwell、Jonathan Riechhold、Stephan Zuercher、Justin Blank、Jamie Brandon、John Hergenroeder 和 Ben Kuhn 提供的评论/修正/讨论。

附录:Pelikan 缓存

Pelikan 的创建是为了解决我们在大规模运维 memcached 和 Redis 时遇到的问题。这份文档 解释了 Pelikan 的部分动机。其模块化/易于修改的特性使我们能够发现新颖的缓存创新,例如一种新的淘汰算法,解决了我们在现有淘汰算法中遇到的问题

就本文讨论的内容而言,Pelikan 在生产环境测试中表现出比我们现有缓存更可预测的性能、更好的中位数性能以及更好的尾部性能,这意味着我们能够以更低的成本获得更高的可靠性和更大的容量。


  1. 知识快速衰减并非 Twitter 独有。事实上,在我作为全职员工工作过的所有公司中,我认为 Twitter 在知识保存方面做得最好。我曾工作过的芯片公司 Centaur 基本上不相信书面文档(除了全面的错误报告),因此许多类型的知识很快就丢失了。微软的情况几乎同样糟糕,因为默认情况下文档被锁定且遵循严格的“按需知密”原则,所以基本上除了少数拥有极广泛权限的人之外,没人能翻阅旧文档来理解事情的来龙去脉。

谷歌早期很像现在的 Twitter,但随着公司发展以及对法律诉讼的担忧加剧——尤其是在多次尴尬事件后,高管们公开表示有意采取不道德和非法行为——事情变得像微软一样更加封闭。

[返回] 2. 此外,也有使用 Redis 分支的情况,但其平均性能明显较差,且尾部性能相对于平均性能更差。此外,由于其设计直接导致大规模运维负担更高,这限制了我们对它的使用。[返回]