🤖
AI审核中

GitHub可用性改造背后的故障隔离真相

Java 16分钟 109浏览 0评论

很多系统出现性能问题或线上故障后,团队的第一反应往往是:

拆微服务、上 Kubernetes、增加机器、部署多机房。

这些手段当然有价值,但它们并不会自动带来高可用。

一个系统即使拆成几十个微服务,只要所有服务仍然共用同一个数据库、Redis、消息队列或连接池,那么其中一个模块产生异常流量,依然可能拖垮整个系统。

2026 年,GitHub 持续公开其可用性改造进展。它所做的事情看起来像是“拆分单体、迁移云平台”,但背后真正的主线并不是增加服务数量,而是:

持续移除共享故障点,把故障限制在更小的范围内。

截至 2026 年 7 月,GitHub 已经让超过一半的单体应用读取流量运行在 Azure Central US,开始将认证数据迁出最老的共享数据库,并通过独立用户服务、仓库服务和 Pull Request 服务,减少核心业务对共享基础设施的依赖。(The GitHub Blog)

这套思路对普通开发团队同样重要。

真正需要拆分的,往往不是代码仓库,而是故障域

一、一次缓存失效,为什么能拖垮半个平台

2026 年 3 月 3 日,GitHub 曾发生一次影响范围很大的故障。

在部署一项降低缓存写入压力的改动时,一个缺陷导致大量用户缓存同时过期。缓存失效后,系统需要重新计算数据并写回缓存,突然增加的负载进一步引发数据库复制延迟,最终影响 GitHub 网站、API、Git 操作和 Copilot 等多个产品。

故障高峰期间,GitHub 网站请求失败率约为 40%,API 请求失败率约为 43%。(The GitHub Blog)

整个故障链路大致如下:

flowchart LR
    A[缓存逻辑变更] --> B[大量缓存同时失效]
    B --> C[请求集中回源]
    C --> D[重新计算并写入缓存]
    D --> E[数据库负载上升]
    E --> F[复制延迟增加]
    F --> G[依赖服务超时]
    G --> H[客户端与服务重试]
    H --> E

表面上看,这是一个缓存失效问题。

但真正导致事故扩大的原因,是多个重要业务共享同一条数据链路:

  • 用户配置依赖它;
  • 权限判断依赖它;
  • API 请求依赖它;
  • 其他服务又依赖这些基础能力。

因此,一个局部缓存问题最终演变成了跨业务的级联故障。

GitHub 后续不仅修复了代码,还为该缓存机制增加紧急开关、强化监控,并将其迁移到独立主机,让未来类似问题只影响真正依赖它的功能。(The GitHub Blog)

这揭示了一个很重要的工程原则:

故障无法完全避免,但可以通过架构控制故障的传播范围。

二、微服务数量不等于故障隔离

很多系统在架构图上已经是微服务,运行时却仍然是一个“分布式单体”。

例如,一个系统拆分为用户服务、订单服务、商品服务和消息服务,但它们仍然存在以下关系:

flowchart LR
    G[API Gateway] --> U[用户服务]
    G --> O[订单服务]
    G --> P[商品服务]
    G --> N[通知服务]

    U --> DB[(共享数据库)]
    O --> DB
    P --> DB
    N --> DB

    U --> R[(共享 Redis)]
    O --> R
    P --> R
    N --> R

    U --> MQ[(共享消息队列)]
    O --> MQ
    P --> MQ
    N --> MQ

此时虽然代码已经分开,但故障仍然可以沿着共享资源快速传播。

表面上的拆分 实际仍然存在的问题
不同服务进程 共用数据库连接上限
不同代码仓库 共用 Redis 内存与连接
独立部署 共用消息队列和消费者线程
不同业务模块 共用线程池或任务队列
不同容器 运行在同一个故障节点
多个服务接口 仍然依赖同一张核心表

真正的隔离至少包含三个层次。

第一层是代码边界,让不同业务模块可以独立开发。

第二层是资源边界,让不同业务不会争抢同一组连接、线程、缓存或计算资源。

第三层是故障边界,确保一个业务出现异常时,其他业务仍然能够继续运行。

AWS 对 Cell-Based Architecture 的定义强调,Cell 应当形成相对独立的故障边界,使代码缺陷、异常请求或资源过载被限制在单个单元内部。Azure 的 Bulkhead Pattern 同样强调把应用资源划分为相互隔离的资源池,避免局部故障扩散到整个系统。(AWS 文档)

所以,判断一个系统是否真正完成拆分,不能只看服务数量,还要问:

某个服务失控后,究竟能消耗多少不属于它的资源?

三、GitHub 真正拆掉的是共享故障点

GitHub 的改造并不是简单地把 Rails 单体切成多个服务,而是围绕几个高风险共享点逐步建立隔离。

1. 将认证和权限从共享数据库中迁出

认证和权限判断通常位于所有请求的入口。

如果每一次请求都需要查询同一个共享数据库,那么流量增长不仅会增加业务查询,还会同步放大认证查询。

这意味着,即使文章、仓库或 Pull Request 本身没有问题,只要认证链路发生拥塞,所有业务都会受到影响。

GitHub 在 2026 年陆续将用户、认证和授权能力拆分为独立域,并推进无状态认证令牌,减少每次请求对数据库的依赖。到 7 月,其独立用户服务在高峰期能够卸载超过每秒一百万次数据库查询,一项主要授权查询也已有约 80% 迁移到隔离服务路径。(The GitHub Blog)

这不是普通意义上的“拆一个用户服务”。

它真正拆掉的是:

  • 共享数据库压力;
  • 认证与普通业务之间的资源竞争;
  • 用户数据故障向其他业务传播的路径。

2. 让仓库和 Pull Request 使用独立基础设施

仓库读取和 Pull Request 是 GitHub 最核心的用户路径之一。

GitHub 将仓库内容流量迁移到独立基础设施,同时逐步让 Pull Request 服务承接原本由单体处理的读取请求。

截至 2026 年 7 月,仓库内容流量已经完全从 Central US 的专用基础设施提供;独立 Pull Request 服务在登录用户读取场景中,已达到与单体应用 99.87% 的结果一致性,为后续流量切换提供基础。(The GitHub Blog)

这里值得注意的是,GitHub 并没有直接把全部流量切换过去。

它先验证结果一致性,再逐步扩大流量。

这比“新服务上线后直接替换旧服务”安全得多。

3. 降低对单个机房和区域的依赖

如果应用拥有多个微服务,却只能在一个数据中心完整运行,那么这个数据中心仍然是巨大的共享故障点。

GitHub 将更多单体读取流量、Git 流量和仓库副本迁移到 Azure Central US,使系统能够从独立容量中处理更多请求。

2026 年 7 月 28 日,其单体读取流量在 Azure Central US 的占比最高达到 52.75%;Git 流量达到约 47%,同时约 29% 的仓库已在 Central US 拥有第二份副本。(The GitHub Blog)

最终形成的架构,不再是所有功能围绕同一组共享资源运行,而是不同核心域拥有相对独立的服务和资源路径:

flowchart LR
    U[用户请求] --> G[Gateway]

    G --> A[认证与权限域]
    G --> R[仓库内容域]
    G --> P[Pull Request 域]
    G --> N[非核心业务域]

    A --> ADB[(认证数据库)]
    A --> AC[(认证缓存)]

    R --> RDB[(仓库数据与副本)]
    R --> RC[(仓库缓存)]

    P --> PDB[(PR 数据库)]
    P --> PC[(PR 缓存)]

    N --> SDB[(通用业务数据库)]
    N --> Q[异步任务队列]

这张图的重点不在于服务变多,而在于:

每条核心路径都有自己的资源边界。

四、可靠性改造的五个核心原则

1. 先识别核心用户路径,再讨论服务拆分

很多监控系统关注的是:

  • CPU 使用率;
  • JVM 内存;
  • 数据库连接数;
  • 接口平均响应时间;
  • 服务是否存活。

这些指标当然重要,但它们不能直接说明用户是否完成了关键操作。

例如,所有服务实例都处于健康状态,并不代表用户一定能够登录、打开文章、提交订单或创建 Pull Request。

GitHub 正在从单纯观察基础设施状态,转向关注 Pull Request 等重要用户工作流的健康度。(The GitHub Blog)

普通团队也可以先定义自己的核心路径。

以一个内容平台为例,可以划分为:

优先级 用户路径 故障时的处理目标
P0 打开文章、登录、读取正文 必须尽量保持可用
P1 发表评论、搜索文章 可以短暂降级
P2 AI 摘要、邮件通知、IP 查询 可以排队或暂停
P3 浏览量统计、推荐刷新 可以延迟执行

只有明确核心路径,团队才知道哪些资源必须优先保护。

2. 优先隔离资源,而不是急着拆代码

对于中小型系统,直接拆数据库和服务可能成本很高。

但很多隔离措施并不要求立刻完成微服务化。

即使仍然运行在一个 Spring Boot 应用中,也可以先进行:

  • 核心接口和后台任务使用不同线程池;
  • AI 调用和文章读取使用不同并发额度;
  • 定时任务不能无限占用数据库连接;
  • 非核心任务使用有界队列;
  • 不同业务设置独立超时;
  • 高风险查询设置单独连接池或并发限制。

例如,AI 摘要生成不应该与文章详情接口共用一个无限队列。

可以为 AI 任务配置独立的有界线程池:

@Bean("aiTaskExecutor")
public ThreadPoolTaskExecutor aiTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(4);
    executor.setMaxPoolSize(8);
    executor.setQueueCapacity(100);
    executor.setKeepAliveSeconds(60);
    executor.setThreadNamePrefix("ai-task-");

    // 队列满时直接拒绝,避免继续堆积拖垮整个应用
    executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.AbortPolicy()
    );

    executor.initialize();
    return executor;
}

当 AI 任务量超过系统处理能力时,最合理的结果不一定是继续排队。

系统可以返回“任务繁忙,请稍后重试”,但文章阅读、登录和评论仍应继续正常工作。

这就是资源级的舱壁隔离。

3. 在系统崩溃之前主动丢弃负载

很多系统面对压力时,会不断扩容、重试和排队,直到所有资源耗尽。

更可靠的做法,是在接近容量边界时主动拒绝一部分低优先级请求。

Google SRE 将这种机制称为 Load Shedding,即在服务进入过载状态前丢弃部分负载,避免内存耗尽、健康检查失败和响应延迟持续恶化。(Google SRE)

GitHub 在 2026 年 6 月已经让客户端侧数据库负载丢弃机制处理约 5% 的真实生产流量,用于验证系统能否在数据库承压时,提前丢弃低优先级查询,防止压力继续扩散。(The GitHub Blog)

请求通常可以分为三类:

类型 示例 过载时策略
必须执行 登录校验、支付确认、核心读取 保留资源优先执行
可以延迟 邮件发送、数据统计、索引更新 放入有界队列
可以丢弃 推荐刷新、非核心埋点、预加载 直接拒绝或跳过

真正危险的不是请求失败,而是所有请求一起等待,最终拖垮整个系统。

4. 所有迁移都必须能够逐步放量和快速回退

架构改造最容易犯的错误,是开发完成后一次性切换。

GitHub 在 2026 年 5 月的一次流量提升过程中发现稳定性问题,因此主动暂停了向 Azure 扩大流量的计划。恢复放量后,每一次流量提升都必须先通过稳定性检查。(The GitHub Blog)

类似思路也出现在 GitHub 的 MySQL 8.0 升级中。

其升级过程并不是直接替换所有数据库,而是逐个副本、逐个数据中心放量,保留旧版本副本作为回退路径,并在完整业务周期内验证稳定性。(The GitHub Blog)

普通系统可以采用类似阶段:

flowchart LR
    A[影子流量验证] --> B[1% 流量]
    B --> C[5% 流量]
    C --> D[20% 流量]
    D --> E[50% 流量]
    E --> F[100% 流量]

    B -.异常.-> R[立即回退]
    C -.异常.-> R
    D -.异常.-> R
    E -.异常.-> R

每次放量前,都应设置明确的稳定性门槛,例如:

  • P95 和 P99 延迟没有明显上升;
  • 数据库连接使用率低于安全水位;
  • 新旧系统结果一致率达到要求;
  • 错误率没有超过基线;
  • 队列积压能够在限定时间内恢复;
  • 回退过程已经真实演练。

灰度发布并不只是把流量比例从 10% 调到 20%。

真正的灰度发布,是每一步都能证明系统仍然安全。

5. 高可用不等于所有功能永远正常

一个成熟系统发生故障时,不一定要保持所有功能完整。

它更应该保证核心流程继续可用。

例如:

  • 推荐服务异常时,返回热门文章;
  • AI 摘要异常时,直接展示正文;
  • 邮件服务异常时,将任务暂存;
  • 评论统计异常时,暂时不显示数量;
  • 搜索服务异常时,保留分类和归档入口;
  • 数据库压力过高时,暂停后台统计任务。

因此,可以重新理解高可用:

高可用不是任何功能都不失败,而是非核心功能失败时,核心业务仍然能够运行。

五、中小团队应该怎样开始改造

系统可靠性改造不需要从多机房、服务网格或 Cell 架构开始。

更现实的方式,是分阶段建立故障边界。

第一阶段:画出真实依赖关系

不要只画服务之间的调用关系,还要画出:

  • 使用了哪个数据库;
  • 使用了哪个连接池;
  • 依赖哪个 Redis;
  • 进入哪个消息队列;
  • 使用哪个线程池;
  • 调用了哪些第三方接口;
  • 哪些功能运行在同一台机器。

最终需要回答:

某个依赖发生故障时,会影响哪些用户路径?

第二阶段:识别最危险的共享资源

优先寻找以下问题:

  • 所有业务是否共用一个数据库连接池;
  • 所有缓存是否放在同一个 Redis 实例;
  • 定时任务是否可能占满业务线程;
  • 第三方接口是否没有超时;
  • 消息消费者是否能够无限重试;
  • 任务队列是否没有长度限制;
  • 非核心请求是否可以无限占用资源。

这类问题通常比“是否拆微服务”更加紧迫。

第三阶段:先建立软隔离

软隔离不需要马上迁移数据库。

可以先完成:

  • 独立线程池;
  • 独立队列;
  • 并发信号量;
  • 请求超时;
  • 熔断;
  • 限流;
  • 连接配额;
  • 任务优先级;
  • 紧急功能开关。

这些措施能够在较低成本下显著缩小故障影响。

第四阶段:拆分一个高风险业务域

不要同时拆十个服务。

可以先选择一个同时满足以下条件的业务:

  • 请求量较大;
  • 业务边界清晰;
  • 经常影响主数据库;
  • 可以独立降级;
  • 数据迁移范围可控。

为它建立独立服务、缓存或数据存储,再通过流量复制、结果比对和灰度放量逐步迁移。

第五阶段:主动进行过载测试

普通压力测试往往只关注系统最高能承受多少 QPS。

可靠性测试还应该关注:

  • Redis 延迟突然升高会怎样;
  • 数据库连接减少一半会怎样;
  • 消息队列停止消费会怎样;
  • 第三方接口超时 30 秒会怎样;
  • 某个缓存键集中失效会怎样;
  • 客户端同时重试会怎样;
  • 回滚时历史任务是否会重复执行。

一个系统在正常情况下跑得很快,只能说明它性能不错。

只有在部分依赖失效时仍能保护核心路径,才能说明它真正具备韧性。

六、什么时候真的应该拆微服务

拆分一个服务之前,可以先回答六个问题:

  1. 它能否拥有独立的数据边界?
  2. 它能否拥有独立的资源配额?
  3. 它能否独立发布和回退?
  4. 它发生故障时,其他业务能否继续运行?
  5. 它是否拥有独立的容量和健康指标?
  6. 拆分后是否真正减少了共享故障点?

如果答案大多是否定的,那么拆分可能只是把方法调用变成了网络调用。

系统增加了部署、监控、链路追踪和数据一致性成本,却没有获得真正的故障隔离能力。

反过来,即使一个系统仍然是单体,只要不同业务拥有独立线程池、有界队列、资源配额、超时、降级策略和清晰的故障开关,它也可能比很多“伪微服务系统”更加可靠。

七、真正的架构升级,是学会控制故障

优秀架构并不假设数据库永远稳定、缓存永远命中、网络永远畅通、代码永远没有缺陷。

它默认故障一定会发生,并提前回答三个问题:

  • 故障会从哪里开始?
  • 故障最多可以影响多大范围?
  • 系统如何在故障期间保住核心功能?

GitHub 2026 年的可用性改造给出的答案非常明确:

不是为了拆分而拆分,也不是为了追求更复杂的技术栈。

而是逐步移除共享数据库、共享容量、共享服务路径和单区域依赖,让认证、仓库、Pull Request 等关键能力拥有更加独立的运行边界。

所以,在下一次准备拆微服务之前,真正应该先问的不是:

“这个模块能不能单独部署?”

而是:

“这个模块失控之后,能不能只让自己失败?”

这才是高可用架构真正的起点。

0 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论