Neon 如何系统性提升可靠性:从事故复盘到控制平面加固
Neon: How we systematically improved our reliability
Neon 分享了其从初创实践转向企业级可靠性的一系列改进,起因是今年一周内发生两起由外部依赖故障引发、拖垮控制平面且无法自愈的事故。改进包括采用 Databricks 的复盘标准,围绕影响范围、检测与缓解时间线及 5 Whys 分析定位根因,并针对依赖故障加固控制平面。

Neon 是一套完整的云后端原语
让我们从一个简单的问题开始:一个数据库、一个进程、一台主机。这是一个我们熟知的可靠性问题。我们知道如何连接、检查、重启和读取日志。
现在,把这一切复制粘贴数百万次。进程会崩溃。机器会宕机。网络会抖动。在百万数据库的规模下,这些故障是家常便饭。而 Lakebase 每天都在应对这些故障。
Lakebase Postgres 在两大平台(Neon 和 Databricks)和三大云(AWS、Azure 和 GCP)上运行着非常庞大的集群。我们覆盖 30 多个区域。我们还使用 cell——隔离部署,让我们能够水平扩展并缩小区域内的爆炸半径。一个区域最多可以有 15 个 cell。这使得环境数量超过 70 个。
最初只是一个数据库问题,现在变成了一个全球运营问题。
在 Lakebase Postgres 中,有一组服务负责管理整个集群。我们称之为控制平面。它们负责预配和配置数据库、监控健康状态、在需要时恢复数据库,并管理高可用性、分支创建、缩容到零以及其他功能。控制平面故障可能会阻止数据库的创建、唤醒和恢复。当这些路径失败时,受影响的客户可能无法连接。控制平面本身必须在集群规模下保持对故障的韧性。

今年早些时候,我们在一周内发生了两起事故。两起都始于一个外部依赖故障;该故障触发了一个自我强化的循环,导致控制平面瘫痪,并且无法自行恢复。
在那两起事故之后,我们加倍投入可靠性。本文总结了自那时以来我们一直在推行的可靠性工作,以在集群、产品和客户群不断增长的同时保持高可用性。
这项工作永远不会真正结束,新的改进会持续发布。但我们对目前的结果感到满意:得益于这些实践,我们的稳定性已经有所提升。
第一步:撰写有效的复盘报告
一份好的复盘报告会重建事件经过、找出根本原因,并将其转化为修复措施和经验教训。Databricks 有一套出色的复盘标准实践,Neon 将其采纳为从初创规模实践向企业级可靠性过渡的举措之一。
我们的复盘报告现在围绕以下两个总体原则来组织:
- 有效的复盘报告存在的目的是减少未来对客户的伤害。它应该作为一种降低风险的工具,而不仅仅是事件记录。
- 未参与响应的评审者应该能够理解客户经历了什么,通过系统追溯这一结果,核查证据,并判断所提议的行动是否恰当且完整。

我们首先写的是事故的影响。这一部分是整个复盘报告的基础,它分析
- 谁——受影响的客户、账户、工作区或项目,以及分母和百分比
- 什么——受影响的端点、API、区域、cell,以及控制平面和数据平面操作,还有哪些未受影响
- 多严重——错误率、相对于基线的延迟、失败的操作
- 持续多久——开始、结束、持续时间,以及影响是连续的还是间歇的
- 有多确定——测量来源、方法、已知盲点,以及每个数字是精确值还是估计值
把数字搞对很重要,原因有三:
- 比例性。如果你记录“客户遇到了延迟”,这可能意味着非常不同的事情。一个非关键方法上 10% 的延迟增加,以及所有控制平面和数据平面操作上 1000 倍的增加,都可以被描述为“延迟”,但这两个事件需要非常不同的补救措施。
- 未知的影响也是信息。不要制造精确性。如果系统无法给出计数,就如实说明,解释原因,在证据允许的范围内给出边界,并将这一缺口视为可观测性行动项。
- 内部客户也算数。值班工程师、支持专家和产品经理也会受到事件影响。用浪费的时间和意外打断来衡量这一点,尤其是夜间呼叫。
有效使用 AI
我们衡量检测和缓解所花费的时间
接下来,我们看看我们是如何响应的。我们记录三个时间线:
- 检测时间:从客户首次受到影响,到有人首次做出反应的时间
- 缓解时间:从检测到客户损害实际停止的那一刻。我们将其拆分为更小的步骤(联系到合适的工程师、弄清问题、决定做什么、执行、等待其生效),这样我们就能看到时间花在了哪里。
- 恢复时间:我们从客户侧确认恢复

对于每一个时间跨度,我们都会问自己:要将其缩短一半需要什么?答案应该是具体的——例如,我们可能需要一个新的信号、告警、运行手册、权限或自动化。
我们使用 5 Whys 框架,从影响开始:“为什么 [数量] 个客户在 [时长] 内无法使用 [功能]?”每个答案都会成为下一个问题,直到我们找到系统中可以修复的具体问题。每个答案都必须指向证据,并指出缺失或未起作用的保障措施。像“人为错误”或“糟糕的部署”这样的标签不是答案,因为它们没有解释为什么系统会让问题到达客户那里。
事件很少只有一个原因。在复杂系统中,通常会有几件事同时出错。因此,Whys 往往会分支成一棵树:
- 为什么失败
- 为什么影响了这么多客户
- 为什么持续了这么久
- 以及为什么花了这么长时间才检测到并阻止
当证据无法确定哪种解释正确时,我们将这些选项标记为假设。
“我们不知道”只在两种情况下可以接受:
- 证据不存在——例如因为没有保留日志。我们说明缺少什么,并添加一个行动项,以便下次有数据。
- 完整答案需要的工作量太大。只有当我们能证明我们的修复覆盖了最糟糕的现实结果时,我们才会停止。

依赖故障也是我们的问题
我们发现的每个问题最终都会成为一个行动项、一个无需采取行动的明确理由,或一个由负责人接受的已知风险。好的行动项是:
- 具体且可测试:不是“改进监控”,而是添加哪个告警、何时触发、呼叫谁,以及我们如何证明它有效
- 足够小以便完成:紧急修复需要几天,大多数事项需要几周,最多几个月
- 值得做:我们根据客户影响和问题再次发生的可能性来确定优先级,并跳过那些成本高但收益小的项目
- 从文本链接:每个问题都指向其被提及处的行动项,例如“[AI10:检查所有告警是否都链接到运行手册]。”如果我们无法链接,就说明发现了缺口
执行摘要出现在文档最前面,但我们最后才写它,等其余分析支撑起其中的数字之后。它应当能独立成篇:发生了什么、多少客户受影响以及持续多久、为什么发生、我们如何阻止、以及还剩什么风险。
草稿准备好后,要经过两次评审。第一次是团队评审,最好包括一位未参与该事件的人。第二次是由资深评审人进行的独立评审,他会检查整份文档。
事后复盘之后,就是修复
本文开头两起事件的事后复盘,在三个月内带来了 30 多项后续修复。它们分为三个领域:控制平面如何使用自己的数据库、控制平面代码中的缺陷和默认值,以及系统设计中只在事件负载下才暴露出来的弱点。
控制平面将自己的状态保存在一个 Postgres 数据库中。当该数据库出现问题时,整个客户集群的配置、健康检查和恢复都会变慢或失败。
我们修复了若干查询计划回归问题,并添加了防护措施,防止它们再次出现。我们还开始阻止长时间运行的事务,这样卡住的查询就不会拖住控制平面。最后,我们调整了几个 Postgres 参数,包括关闭并行查询执行,它曾导致部分计划和运行时问题。
控制平面缺陷和默认值
接下来,我们检查了控制平面代码和运行时设置。我们在以下方面发现了问题:
- 我们使用的一个开源依赖项
- 队列处理
- TCP 超时设置
- 连接池默认值
其中大多数并不在我们编写的代码中。它们是我们继承来的默认值和行为。一个被广泛使用的库仍可能带有不适合管理数百万数据库的控制平面的默认值,因此我们会针对自己的工作负载审查并设置这些值。
最后一组修复是架构性的,这些问题在事件本身期间暴露得最清楚:
- 我们的背压机制——在系统过载时减缓传入工作——没有按预期工作
- 在负载下,我们遇到了饥饿和公平性问题:一些工作持续取得进展,而另一些工作则在等待
这 30 多项修复解决了这两起事件中出错的地方。但事后复盘产生的修复来得太晚——等我们写出来时,客户已经受到了影响。
于是我们退后一步,追问可靠性故障最初来自哪里,以便在下一次事件之前就抓住它们。
我们发现它们来自三个地方:
- 环境:基础设施、网络、依赖项、硬件。我们依赖的任何东西最终都会失败。我们的两起事件都始于此,由外部依赖项故障引发。
- 我们做出的变更:代码、配置、迁移、运维。
- 客户端工作负载:流量、使用模式、无效输入、重试。即使是可预测的模式,例如每小时 cron 峰值,在规模扩大后也可能变得危险。
知道了故障来自哪里,我们围绕两个理念构建了一套系统:
- 每个变更都会获得风险评分
- 评分越高,变更在设计、构建、发布和运维过程中必须通过的可靠性检查就越多

我们这样为每个变更评分:
风险 = 概率 × 影响
概率反映变更的规模有多大、有多新,以及它触及多少依赖项。影响反映它可能破坏哪些客户工作流、可能影响多少客户,以及恢复需要多长时间。
- 低风险:对内部仪表板的变更
- 高风险:影响数据库创建的控制平面迁移
评分决定一项变更必须通过多少项可靠性检查。高风险变更受到更严格的审查,低风险变更则保持快速。
每项变更都已经经过相同的四个阶段:设计、构建、发布和运营。我们在每个阶段都加入了可靠性检查,并按变更的风险评分进行缩放,这样在变更到达客户之前,所有三个故障来源都会被审视。
在我们编写大量代码之前,我们会问这项变更可能如何失败:
- 我们构建的一个 AI 技能会从可靠性角度审查设计
- 我们给这项变更打出风险评分
- 对于高风险变更,我们会进行事前验尸:我们想象该功能已经引发了事故,然后推演出这可能是如何发生的,以及如何预防
在编写代码时,我们确保可能的故障得到测试,并且我们能够撤销这项变更:
- 我们从可靠性角度审查代码
- 我们运行负载和故障测试
- 我们检查回滚是否有效
在推出过程中,我们观察整个机群,如果变更行为异常就停止:
- 我们分阶段逐步放量
- 功能负责人在推出期间观察 SLO
- 高风险变更要经过运营就绪审查
一旦变更上线,收到告警的团队必须能够运行它:
- 功能负责人向值班团队交接他们需要知道的内容
- 我们进行值班演练
- 团队审查每一次可靠性下降,哪怕是最轻微的
我们描述的流程已经在预防事故,我们正在通过更多分析和更多自动化来扩展它,包括
- 失效模式与影响分析(FMEA):我们选取一个组件,想象它发生故障,追踪对客户的影响,并在它发生之前修复薄弱环节。
- 故障树分析(FTA):我们从客户会看到的故障出发,反向推导出可能导致它的每一个组件。
- 自动化压力与故障测试:我们让负载、限制和恢复测试自行运行,这样它们就不依赖于有人记得去运行它们。
你把数据和正常运行时间托付给我们。我们认真对待这一点。可靠性工作没有终点线:我们会不断改进,并一路分享我们学到的东西。
来源:Hacker News · neon.com