62.7 秒:从「请手动删 pod」到宣布 Resolved——那条 Resolved 里自己写着「有人仍然是坏的」

42 阅读17分钟

上一篇收尾在「一致性度量的是判据的方差」。那一条的判据还是机器——规则写得再死,它至少还是个函数。 这一篇更靠前一点:判据连函数都不是,是一个人在某一刻按下的。

GitHub 自己留了一条 62.7 秒的时间线,全在同一个事故页面上,一条匿名可开:

2026-08-07T02:03:41.768Z  monitoring   让用户手动 kubectl 删 ARC pod;写明部分事件永远补不回来
2026-08-07T02:04:44.460Z  resolved     整条事故判为已解决

62.692 秒。 上一条还在告诉用户「你得自己去删,有些东西永远回不来」,下一条就变绿了。

而那第二条变绿的正文里,逐字写着:

Some Actions Runner Controller (ARC) runners remained stuck after the incident …… causing some to remain offline until they were manually recovered.

宣布「已恢复」的那段话本身,就是「还有人没恢复」的证据。 这不需要用户来作证,是厂商自己写下的。

⚠️ 口径先钉住:本篇的每一条厂商材料,都是我从 githubstatus.com 的公开 API 和 Atlassian 公开文档里 逐条读原文取出来的;HN 那 418 条评论是全量拉下来本地统计,不是抽样。 凡是我没有取到正文的东西,本文一个字都不写(AWS、Cloudflare 的复盘我上一轮没拿到,所以本文不提它们)。

一、先把那条事故读完

qcvjkzcs7j74,名字叫 Incident with Actions,impact: critical,波及 Actions 和 Pages。 全程 10 小时 41 分 55 秒(15:22:49.029Z → 02:04:44.460Z),24 条 update。

先看它自己承认的影响面,这一段是整篇最硬的:

At peak, 71% of workflow runs experienced infrastructure failures and 75% of the remaining workflow runs were delayed by more than 5 minutes.

七成的流水线在峰值时刻直接失败。 这是一个相当严重的事故,没有人需要被科普它有多严重。

然后是全篇真正要说的那件事。在同一段复盘里,GitHub 写下了三个时间:

17:00 UTC      These services recovered at 17:00 after expanding capacity…
00:14 UTC      事故影响窗口的终点("between 15:05 UTC and 00:14 UTC on August 7")
02:04:44 UTC   这条事故被判为 Resolved 的时刻

三个时间,同一份复盘,互相之间差了 7 小时 14 分和 1 小时 50 分 44 秒。

按理说,「服务在 17:00 恢复了」这句话一出来,后面就该是「所以 00:14 收尾、然后变绿」。 实际发生的是另一条路径:17:00 之后,状态页继续红了 9 小时 4 分 44 秒; 这 9 个小时里的最后一条 update(02:03:41)还在教用户手动删 pod; 然后 62.7 秒之后,它变绿了,并且顺手承认了「有 runner 一直离线,要手动恢复」。

判定既不跟指标走,也不跟用户体感走。它跟「有人腾出手来写这条 update」走。

指标说好了,用户说没好,人说好了。三个信号,三条独立的时间线, 没有任何一个会去检查另外两个。 这就是「Resolved」的实质——它不是这三条线里任何一条的输出, 它是第四条线:某个人在某个时刻,决定该相信前三条里的哪一条。

二、为什么厂商宁可让人来判

这件事不是 GitHub 一家的怪癖,是整个行业的地基。

Statuspage 的官方文档里有两段,我是从页面的 articleBody 字段里读的原文,不是搜索摘要:

Some people confuse Statuspage for a monitoring tool, but that's not really what we do. Statuspage doesn't ping your servers or endpoints to check the status……

A fully automated status page means you have computers talking to people. That's risky as it can result in a poor user experience by way of false negatives, false positives, lack of context, and flapping notifications. As a best practice, we never recommend fully automating your status page.

把这两段合起来读,逻辑是闭合的:

  1. 状态页不探测,它只负责说;
  2. 全自动会有 false negative(机器看不出已经好了),

官方觉得宁可错报也不漏报;

  1. 所以选择让人来判断。

这个选择在工程上是对的。 问题在于它有一个没被写出来的代价:

一旦判定交给人,「已恢复」就从一次测量,变成了一次声明。

声明可以不来自任何测量。它可以来自「我看到主指标绿了」,也可以来自「我不想再让这个页挂着」, 也可以来自「我确认过了」。这三种在状态页上长得一模一样:都是一行 Resolved。 外部读者拿不到它到底是哪一种。

三、同一套系统里,四种不同的失效形态

我按 id 逐条核了四个事故(不是按标题认,见第六节)。它们不是同一个故事重复四遍, 是四种不同的错法:

序号 / incident / 形态 / 关键那一行

  • A —— 2dpbcq5j165n —— 依赖没好,自己好了 —— 「capacity 已恢复」,同时「还在跟 Azure 处理底层条件」——6 分钟后判 Resolved
  • B —— dqn46wtvbdzv —— 服务好了,你的好不了 —— 「Service connectivity has recovered, but the lost state cannot be restored」
  • C —— 1xqvzyv99skw —— 指标压根没覆盖你 —— 「we failed to process approximately 128,000 deployments」+「this incident was not fully captured by our availability metrics」
  • D —— 3q1yb5m7ltvb —— 全都好了,9 分钟之后又开新事故 —— 宣布「all affected services recovered by 22:49 UTC」→ 23:47:45 新开 2kxnqcxcrr7f

逐个说清楚,因为这四种是能互相顶替的:

A 是依赖没解决。 2026-10-01,17:50:54 那条写得很平静:

Service capacity has recovered, and we are continuing to monitor while working with Azure on the underlying condition.

17:56:54,This incident has been resolved.——距 359.9 秒,整整 6 分钟。 「上游 Azure 的问题还在处理中」和「已解决」这两件事,被同一双手在 6 分钟内写进了同一个字段。 没有任何一步把「上游还没好」这个已知事实纳入判定条件。

B 是服务好了但你的状态好不了,而且这一条最狠:它标题里就有 deployment gate approvals—— 门禁。门禁的输入(你那条 workflow 的执行状态)已经不可逆地丢了:

an isolated infrastructure failure caused GitHub Actions to lose execution state…… Affected runs may remain stuck, fail deployment approvals…… Service connectivity has recovered, but the lost state cannot be restored by retrying an approval.

更值得注意的是它的时间形状:这条事故的 created_at 和 resolved_at 都等于 2026-10-01T02:00:00.000Z,全部只有 1 条 update,而那 1 条是在 11:34 才发出去的 resolved。

事故发生在凌晨 2 点,而状态页上关于它的那句话,是在事故结束 9 个半小时之后才出现的, 一出现就是「已解决」。 中间那 9 个半小时里,用户能查到的信息是零。

C 是官方承认自己的指标没覆盖到。 这是最难得的一条,因为厂商通常不会说这个:

Customer impact occurred from 11:34 to 15:32 UTC. During this period, we failed to process approximately 128,000 deployments. We've confirmed this incident was not fully captured by our availability metrics. In the coming days, we'll update how GitHub Pages availability is measured so incidents like this are accurately reflected going forward.

12.8 万次部署没处理掉,而可用性指标是绿的——这句话是 GitHub 自己写的。 一个系统可以同时做到「指标绿」和「12.8 万次部署丢了」,只要它测的东西和你用的东西不是同一个。

D 是判定说完又反悔。 10 月 5 日那条复盘结尾写得很清楚:

GitHub Actions and Hosted Runners recovered by 21:54 UTC, and all affected services recovered by 22:49 UTC.

然后 23:47:45——58 分钟之后——GitHub 自己新开了一条事故 2kxnqcxcrr7f, 关于的是组织与企业版的账单和许可页面,和 22:49 那句「全部服务已恢复」指向的是同一片区域。

判定边界不是被某条规则划出来的。它是被手划出来的,手还会划错,然后自己再划一条新的。

四、状态页变绿之后

到这一步为止都还是厂商在自证。自证有说服力,但它是单边的—— 一家公司自己说自己哪里做得不够,总有人会想「那是不是话术」。 所以我去看了用户侧。

qcvjkzcs7j74 那天在 HN 上被讨论爆了:submission 49198302,509 分 / 418 条评论, story URL 直接指的就是这条事故。我把全树 418 条拉下来落盘,按 resolved_at 切:

时点 / 条数

  • 宣布 Resolved 之前 —— 273
  • 宣布 Resolved 之后 —— 145(34.7%)

超过三分之一的讨论发生在状态页已经变绿之后。这本身不说明什么—— 事故一结束人们总要来吐槽。所以要看的是绿了之后还在说什么:

距 Resolved / 用户 / 原文

  • +2.37h —— switchbak —— 「my build is still completely borked …… and we're self hosting runners」
  • +3.96h —— rurban —— 「All tests failed, for the whole day. For 20m, ok, I can tolerate that.」
  • +5.00h —— steelaz —— 「only failed GHA invocations are counted towards an SLA breach」
  • +11.70h —— Kinrany —— 「were Webhooks also down, not notifying about commit pushes?」
  • +11.74h —— tom1337 —— 「Theres not much information given besides "Webhook deliveries may be delayed."」
  • +11.91h —— vsilent —— 「I was waiting entire day for gh actions to work」

这里必须先把话说死:用户说自己还坏着,不等于系统当时还坏着。 switchbak 说「still borked」不能证明 GitHub 还在故障,只证明他本人没有恢复。 所以本文的承重项始终是 GitHub 自己那份 Resolved 正文,用户评论只作交叉印证—— 方向一致就算印证,方向不一致也不会被我写成证据。

但有两处是实打实对上了的。

第一处,switchbak 是自托管 runner。 官方 Resolved 正文承认卡住的是 ARC runner,官方给的解法也是让用户自己动手。 厂商和用户在同一件事上对上了:都需要有人手动去恢复。 用户在事故后 2.37 小时还在骂的, 正是官方复盘里写的那批 runner。

第二处,steelaz 把这件事推到了合同层,这可能是整篇最有用的一条。

This incident made me look at the SLAs we have with GitHub, since our company is on the Enterprise plan. Interestingly, it seems that only failed GHA invocations are counted towards an SLA breach.

他们公司当时为了止损,让工程团队停用 GHA。结果是——停用之后跑不起来的调用, 根本不算 SLA 违约,因为违约只统计「失败的那部分调用」。

于是同一场事故长出了三套互不校验的账:

口径 / 它对这场事故的结论

  • 状态页 —— Resolved,一切正常
  • SLA 账单 —— 主动停用不算失败,所以接近零违约
  • 用户体感 —— 「整整一天,所有测试都挂了」

这三份账没有一份是错的。它们只是各自在数不同的东西,而且从不对账。 真正吃亏的是那个需要同时看着这三份账做决定的运维——他在状态页上看到绿灯, 在账单上看到零成本,在用户群里看到一整天全红。没有任何一个数字会告诉他另外两个。

tom1337 那条则是另一种失效,信号存在,但没回答你以为它回答的问题:

I am not sure if all Webhooks were affected or only those regarding actions. Theres not much information given besides "Webhook deliveries may be delayed."

事故页面从头到尾挂着一个 Webhooks 组件,状态写得规规矩矩。 但「可能延迟」这句话没有回答任何一个他真正想知道的问题:到底受没受影响? 「有组件被列为受影响」这件事,和「你该知道自己有没有事」之间,还差着一整个判断过程。 这个判断过程,在这一行里被省略了,而它被省略掉的部分,就是这次事故的全部内容。

五、有人已经不信了,而且他们动手了

有一条我原本当是段子,查完发现是正经项目。

https://mrshu.github.io/github-statuses/(作者 Marek Šuppa,开源在 github.com/mrshu/github-statuses),About 段原文:

GitHub stopped updating its status page with aggregate uptime numbers some time ago — if you use it regularly, you might have a feeling why. This is the missing version.

We rebuild platform-wide and per-service uptime from archived status updates, derive minute-level downtime windows, and map incidents to services whenever the source data allows.

Nerd notes: the pipeline reconstructs history by replaying the Atom feed snapshots committed to git via Flat Data. It rebuilds each incident timeline and computes uptime using merged downtime windows rather than day-level buckets.

这件事的分量不在于「有个民间项目」,在于它做的事:

有人把官方状态页的历史快照抓下来重放,重算出了官方不再公布的分钟级可用率。

也就是说,「状态页说的不算数」这个判断,不是我下的,也不是用户抱怨出来的—— 是有人用一套抓取、合并、跨事故去重的工程量做出来的。 而且它做的事恰好就是第四节那个漏洞的反面:把 day 级的桶换成 minute 级的窗口, 把 N 条互不对账的时间线合并去重。

同一个 HN 树里还有另一个同类项目(damrnelson.github.io/github-historical-uptime/), 有人贴了 90 天 93.91% 的可用率。

当一个基础设施的数据只能靠民间爬虫重建才能拿到完整版时,缺的已经不是功能,是信任的凭据。

六、这一轮我自己犯的错,和它为什么重要

留个记录,因为这一条恰好就是本文的论点。

我第一轮整理这批材料时,是按事故标题去认的。GitHub 那几天有好几条都叫 Incident with Actions / Incident with Pages,我于是把两条并成了一条算, 得出「Resolved 之前只有 4 条评论,之后 414 条」这个惊人对比。

那条结论整个作废。两条事故的 id 分别是:

id / name / resolved_at

  • qcvjkzcs7j74 —— Incident with Actions —— 2026-08-07T02:04:44Z
  • 1xqvzyv99skw —— Incident with Pages - Deployment Lag —— 2026-08-06T16:22:59Z

它们是同一天的并列事故,而 HN 那个 submission 挂的是前者。 按 id 重新核之后,数字变成 273 / 145。方向没变,但值全变了。

同名事故必须按 id 钉死,不按标题认。 因为这篇讲的就是「字段名和组件名可以一样, 底下是两条完全不同的事故」——我犯的正是本文批评的那个错。

同一轮里还有两处,一并交代:

  1. 留档里曾给案例 B 标了「deliver_notifications: false(没推送给任何人)」。

我今天重新拉了那个事故的原始 JSON,这个字段根本不存在。 已删除。 案例 B 现在只保留能被原文支撑的两点:执行状态丢失不可恢复, 以及事故 02:00 发生、唯一的 update 11:34 才发。

  1. 留档里把 qcvjkzcs7j74 的 update 条数写成 22,实测是 24。不影响任何结论,但数字按实改。

七、那看到一个 Resolved,能怎么读

不写「别信状态页」——那是废话,而且对本文没有增量。有几条是能落地的:

一、去看 resolved 前最后一条 update 的时间戳,和 resolved_at 差多少。 本文那个案例是 62.7 秒。这个差值就是「宣布恢复」和「最后一次确认状况」之间的时间。 它越短,你越应该怀疑那次确认覆盖了真实影响面。

二、去看复盘正文里有没有「但是」。 qcvjkzcs7j74 的 Resolved 正文里有「remained stuck」「manually recovered」「cannot be restored」。 厂商把话说全了,只是没人读那一段。 状态页的 status 字段是个枚举值, 正文才是真相,而正文默认折叠在最后一条 update 里。

三、把「组件被列为受影响」和「你该知道有没有事」分开。 tom1337 的问题是 Webhook 到底受没受影响。组件名不是答案, 「可能延迟」也不是答案,中间的判断过程没人替你做。

四、如果你的 SLA 和状态页不是同一个口径,去查清你的 SLA 是哪个口径。 steelaz 的教训是:止损动作本身会改变你的违约计数。 他让工程团队在事故期间停用 GHA,因为在 SLA 口径里「停用」不算「失败」。 一个把公司从赔偿里救出来的操作,在用户那边是整整一天全红。

五、如果你要一个可用的可用率数字,不要指望状态页。 它要么不提供(GitHub 已经停更汇总数字),要么只到 day 级, 要么如案例 C 所承认的,压根没覆盖到你用的那条路径。 需要的话,自己做分钟级窗口——这件事已经有人做完开源了。

附:本文引用

一手(公开可匿名复核,本轮我亲自逐条读过原文)

结论 / 出处

  • 最后两条 update 相距 62.692 秒 —— api/v2/incidents/qcvjkzcs7j74.json,incident_updates 按 created_at 升序取末两条
  • Resolved 正文自认 ARC runner 仍卡死、需手动恢复 —— 同上,2026-08-07T02:04:44.460Z 那条 body 原文
  • 峰值 71% workflow runs 失败、其余 75% 延迟超 5 分钟 —— 同上,同一条 body
  • 同一份复盘里的三个时间:17:00 / 00:14 / 02:04:44 —— 同上
  • 事故窗口 10h41m55s,24 条 update,impact: critical —— 同上,created_at / resolved_at / incident_updates 长度
  • 用户自救入口指向社区帖 —— 同上,正文所链 github.com/orgs/community/discussions/204152(HTTP 200,本轮验过)
  • 案例 A:依赖未解决,359.9 秒后判 resolved —— api/v2/incidents/2dpbcq5j165n.json,17:50:54.351Z → 17:56:54.260Z
  • 案例 B:执行状态丢失不可恢复 —— api/v2/incidents/dqn46wtvbdzv.json,唯一 update 2026-10-01T11:34:16.913Z
  • 案例 B:事故 02:00 发生,update 11:34 才发 —— 同上,created_at == resolved_at == 2026-10-01T02:00:00.000Z
  • 案例 C:128,000 deployments 未处理 + 指标未覆盖 —— api/v2/incidents/1xqvzyv99skw.json 的 resolved body
  • 案例 D:22:49 宣布全恢复,23:47:45 新开 2kxnqcxcrr7f —— 3q1yb5m7ltvb.json resolved body + 2kxnqcxcrr7f.json
  • 状态页不探测;全自动会 false negative,故不推荐全自动 —— Atlassian Statuspage 文档 articleBody 字段原文
  • HN 418 条 / 509 分 / resolved 前 273 后 145(34.7%) —— hn.algolia.com/api/v1/items/49198302(必须 https),全树本地落盘后统计
  • 六条用户原话 —— HN 49205894 49206414 49206880 49210323 49210357 49210528
  • 官方停更汇总可用率,民间用 Atom 快照重算分钟级窗口 —— mrshu.github.io/github-statuses About 段

本篇不成立或刻意不写的结论(免得被过度引用)

  1. 不主张「状态页在撒谎」。本文的论点更窄也更硬:**Resolved 是一个被人工赋值的状态字段,

它和监控指标、SLA 账单、用户体感是三套互不校验的账。** 厂商在这些案例里说的全是真话, 只是三份真话的时间线没有一条去校验另外两条。

  1. 不主张「用户自述可作为事故证据」。用户自述只证明该用户没有恢复。

本文的承重项始终是厂商自己的 Resolved 正文。

  1. 「三套账」只有一条厂商侧硬证据(案例 A 的依赖未解决、案例 C 的指标未覆盖),

SLA 那一套来自企业用户陈述,我没有取得 GitHub 官方 SLA 文本,不写成事实。

  1. 本文不含 AWS / Cloudflare 的任何结论——我上一轮没取到它们复盘的正文,

一个字都没引用。

  1. isgithubdown.net 是恶搞站(随机字符串生成器,响应池写死

「The status page says all systems operational. So, yes.」)。我上一轮误当旁证,已弃用, 本文不出现。

  1. 七个时间点全部来自 2026 年 8–10 月的四个事故,样本极小。

本文的结论是机制层面的(人工赋值字段 + 三条独立时间线),不是统计层面的一般性结论。

  1. 统计口径提醒:本文所有时间戳我都按 created_at 升序取,

而 incident_updates 在 API 里是倒序的——照抄数组顺序会得到相反的结论。