打开网易新闻 查看精彩图片

4月6日周一,Bluesky约50%用户经历了断断续续8小时的服务中断。这是该平台史上最严重的一次事故。

系统工程师Jim在事后报告中写道:「这是我入职以来见过最糟糕的宕机,完全不可接受。」但真正的麻烦,其实从前一个周末就开始了。

周六的警报:被误判的"网络问题"

周六的警报:被误判的"网络问题"

4月4日周六,监控系统触发告警。Jim第一反应是"传输层问题"——毕竟他们有完善的网络监控,看起来一切正常。

但日志里藏着线索。数据平面(AppView的后端服务)频繁报错:

「failed to set post cache item」——绑定地址已被占用

错误指向TCP端口耗尽。Bluesky的数据平面重度依赖Memcached(一种高性能缓存系统)来分担主数据库Scylla的压力。如果端口耗尽,缓存失效,请求直接砸向数据库,雪崩开始。

问题在于,当时的监控体系有个盲区。Jim后来承认:「我们假设每个请求都很轻量、很快完成。」但这个假设,在上周部署的一项新服务面前彻底失效。

新服务的"温柔一刀":每秒3次请求,每次2万个URI

新服务的"温柔一刀":每秒3次请求,每次2万个URI

上周上线的内部服务,看起来人畜无害。调用频率极低——每秒不到3次。但某些请求会一次性批量查询15,000到20,000条帖子URI(统一资源标识符)。

正常业务场景?1到50条URI per请求。

数据平面的代码用Go语言编写,每个RPC处理器都有并发限制(errgroup.SetLimit),防止资源被单个请求吃光。这是基础设施的标准防护。

唯独GetPostRecords这个端点没有。

代码里本该有一行:group.SetLimit(50)。它不存在。于是15,000个URI进来,系统瞬间启动15,000个goroutine(Go语言的轻量级线程),向Memcached狂建连接。

连接池上限是1,000。超额连接用完即抛,堆积在TCP的TIME_WAIT状态。65535个可用端口,耗尽只是时间问题。

周一的全面崩溃:端口枯竭的连锁反应

周六的"小波动"只是预演。周一流量高峰到来,问题被放大到平台级。

图表显示,用户请求量在8小时内出现多次断崖式下跌——绿色和黄色曲线不重要,那些深坑才是真实的用户掉线。约半数用户无法正常加载时间线、发帖或互动。

事后复盘的关键发现:一个漏写的参数,藏在整个系统最繁忙的端点之一。GetPostRecords负责批量获取帖子记录,是 feed 渲染的核心路径。它每天处理数十亿次查询,却唯独缺少并发保护。

Jim的描述很直白:「我们 slammed the daylights out of memcached」——把Memcached揍得够呛。

修复很简单:加上那行SetLimit。但定位问题花了整整两天,因为监控没准备好应对"单个请求内部爆炸"的场景。

一个参数背后的工程债务

一个参数背后的工程债务

Bluesky的技术栈选型相当激进:自研AT Protocol(认证传输协议)、联邦架构、Go语言全链路。这种架构下,单个服务的边界模糊,调用链复杂。

GetPostRecords的设计初衷是高效——批量查询减少往返。但"高效"和"安全"的边界,被一个新服务的异常用法击穿。

更值得玩味的是时间线。新服务上周部署,周六首次触发告警,周一全面爆发。中间有48小时窗口,但监控的假设让团队走了弯路。

Jim在报告末尾放出了招聘链接。这场事故成了技术品牌的另类广告:来帮我们修这种级别的坑。

分布式系统的恐怖故事往往如此——不是某个组件彻底坏掉,而是两个"正常工作"的东西以意想不到的方式共振。一个每秒3次的低频服务,和一个缺失的并发限制,联手制造了平台史上最长的中断。

如果Bluesky当时给这个端点设置了默认的并发上限,或者新服务的开发者注意到批量请求的潜在风险,这8小时本可以避免。但工程世界里没有如果,只有事后才能看清的依赖关系图。

Jim的报告没有给出具体的改进时间表,只提到"observability improvements are underway"。读者不妨想想:你负责的系统里,有没有哪个端点也缺了一行SetLimit?