Steven
Steven1 分钟阅读

我们的 App 对自己发起 DDoS 的那一天

一堆积压的待上传任务,在启动时被一次性全放出去,把每一个 GeekBye 客户端都变成了对我们自己服务器的一次小型拒绝服务攻击。那次修复 — 以及它逼着我们搭出来的那条连接存活性阶梯 — 是 v2 教给我们最有用的东西之一。

可靠性
网络
工程
GeekBye 发布
我们的 App 对自己发起 DDoS 的那一天

每一个搞分布式系统的工程师,迟早都会撞见惊群(thundering herd):一大群客户端在同一瞬间做同一件事,而它们背后的那个共享资源被压垮了。通常那是别人家的客户端。在 GeekBye v2.0.1 里,那是我们自己的,而攻击者就是我们自己的 App。

一个笔记助手怎么攻击自己

GeekBye 可以把录音上传到你的 Google Drive。如果一段录音没法立刻上传 — 你当时离线、App 关了、Drive 抽了一下风 — 它就进一个积压队列,留着以后重试。合情合理。

失败出在那个"以后"什么时候发生。在下一次启动时,App 会试图一次性把整个积压全排空。一个攒了一周待上传录音的用户,在打开 App 的那一瞬间,就变成了一阵同时发出的上传请求爆发。再乘上每一个在早上启动的用户,我们的后端就被要求在同样那几秒里去认证、去查限流、去处理一堵流量的墙 — 而这些客户端,技术上说,全都规规矩矩。

我们的后端做了对的事,把这股洪流限了流。而二阶伤害就是从那儿开始的。一个限流响应是一个 HTTP 429,而 429,你要是不小心,看起来跟别的失败很像:

  • 启动时的拉取用户资料吃了一个 429,App 把它当成了认证失败 — 把用户从一个完全有效的会话里短暂地登出了。
  • 转写连接吃了一个笼统的 429,弹出了一个音频额度已用尽的升级提示 — 告诉付费用户他们撞上了一个根本没撞上的配额。

于是一个根因 — 一个没排节奏的积压 — 生出了三个看得见的症状:服务器吃紧、假登出、假升级推销。这就是自我 DoS 的经典形状:负载本身是坏的,但用户真正感受到的,是对这份负载症状的误读

修复,分两层

止住踩踏。 上传积压现在排上了节奏 — 请求带退避地摊开,任何一个 429 之后都跟一段冷却,好让客户端从一台吃紧的服务器旁退开,而不是更用力地压上去。一场惊群(thundering herd)变成了一列有序的队。

止住误读。 启动期间一次瞬时的 4295xx 不再把你登出 — 客户端把"服务器只是短暂地忙"和"你的会话失效了"区分开。而转写路径上一个笼统的限流 429 不再假扮成一个音频配额错误;只有一个货真价实的配额响应才弹升级提示。真正记住的教训是:错误码不是错误的含义。 429 的意思是"慢一点",不是"把你登出",也不是"给我们付钱" — 而客户端必须知道这个区别。

随之而来的那条存活性阶梯

给这股踩踏排上节奏,暴露出了一个更安静的问题:当一条连接真的在负载下坏掉时,我们多快才注意到?比我们想要的慢。于是 v2.0.4 为实时转写搭出了一条连接存活性阶梯:

  • 心跳 — 转写套接字按固定间隔被 ping,好让一条悄没声息死掉的连接在几秒内被发现,而不是等下一块音频失败。
  • 网络恢复时的重连触发 — 当操作系统报告网络回来了,App 主动把连接重新建起来,而不是等着自己撞上这个发现。
  • 控制层 ping/pong 存活性 — 一次应用级的往返,不只确认"套接字开着",还确认"另一端真的在回应"。
  • 一个由服务持有的重连闸门 — 由一个地方来决定要不要重连,而不是 App 的好几个部分抢着去做、互相踩脚。

这些都不光鲜。而正是这一切,让一次 v2 的会话感觉就像它只是……一直连着。

本周它又发生了 — 在下一层

这才是让这件事不止是一个战地故事的部分。这同一类失败在几天前又冒了出来,就在我们下面一层。一个客户端在不到 400 毫秒里打出了 21 次转写会话启动 — 撞上了我们语音供应商那条账户级 15 个并发请求的上限。一场对共享基础设施的踩踏,和那个上传积压一模一样,只不过瞄的是一个依赖,而不是我们自己的后端。

形状一模一样,修法也一模一样:那个踩踏的客户端需要一个 single-flight 守卫,好让它没法为一个意图打出二十次启动;那个共享资源需要一个按用户的上限,好让一个客户端没法把整个池子吃光。我们以前就造过这东西的客户端版本 — 那个上传节奏器就是这个模式。现在我们把它用到会话启动上。惊群(thundering herd)不是一个你修一次就完的 bug;它是一个形状,你学会在客户端遇上一个共享上限的每一个地方都把它认出来。

三点带走的东西

  1. 你自己的客户端,是一场你没排期的压力测试。 任何在启动时批量做、在启动时重试、在唤醒时重连的东西,都是一场等着用户攒够的惊群(thundering herd)。趁你还没攒够用户,先给它排上节奏。
  2. 把码和含义分开。 429 是这世上被误读得最多的状态。"慢一点"不是"登出",也不是"给我们付钱"。把每一种失败路由到它真正的含义上去。
  3. 存活性是一条阶梯,不是一个标志位。 "连接还活着吗?"在不同的层上有好几个诚实的答案 — 套接字开着、字节在流动、另一端在回应、网络在。一个稳健的 App 会不止查一个。

这就是 GeekBye v2 那份平稳底下、不光鲜的机器。关于它撑起来的那些可靠性功能,见为什么你的 AI 笔记助手会在糟糕的 Wi-Fi 上停下以及当防火墙封掉 WebSocket 时的实时转写(v2.0.8)。这一系列里相邻的发布,见为什么你的 AI 笔记助手会在会议中途停止录音(v2.0.9)。