Agent 平台

一个人,能维护到多大规模?

从 270 到 1987 日活,每一次规模跃升前都先撞了一次墙。这条增长曲线记录的不是预设设计,而是每次故障都被完整归档后逐步演进出来的可行性边界。

从 2026-05-22 的 270 个用户到 07 下旬的峰值 1987 日活,这个平台的增长不是线性的。中间隔着四次明确的容量撞墙,每一次都留下了可复查的根因记录。这篇文章讲的是,什么条件下一个人能维护一个到达四位数日活规模的多租户平台。

成长曲线与撞墙的四个节点

架构的设计前提是"1 用户 = 1 容器"。这个决策确定了,用户数与容器数就是同一个口径。没有"日活 X 万、同时在线 Y 万"这种双指标的混淆。

实际的规模序列是这样的:

  • 270 个 service(05-22):刚完成 ffmpeg 升级后的测量
  • 372 RUNNING(05-24):滚动升级前的容器计数
  • 379(05-25):五个 P0 patch 部署后稳定
  • 574 用户(06-03):迁移到 K8s 时的基线,539 个 RUNNING
  • 1987(07 下旬):最近测得的峰值

跨度是两个月,增速从 Swarm 期间的两周内 270→379(40% 增长)、到迁 K8s 后约八周 574→1987(约 3.5 倍)。这个加速度不是"计划好的伸缩",而是每一次解决了瓶颈后,下一个瓶颈暴露出来。

四堵撞过的墙

第一堵:容器网络 IP 池(05-22,FA-012)

用户报告"AI 容器里没 ffmpeg"。这本来是个 Dockerfile 一行 apt 的事。但打算上线这个修改时,意外发现 gwbridge(Docker Swarm 默认的容器网络)IP 地址段是 /24,总共 253 个 IP,已经被 270 个用户的容器占满。13 个用户容器长期处于启动失败的循环中。

排查逻辑是这样的:一行 Dockerfile 改动不至于触发灰度风险,但灰度一个用户时,Swarm 需要给新容器分配 gwbridge 的 IP。如果池子满了,IP 分配失败,新容器启动失败,再加上老容器还没完全释放(endpoint 还在占位),就陷入了"申请 → 失败 → retry"的循环。这种症状看起来随机,用户看到的是"我的容器启不来",系统看到的是"又一个容器 cycling"。直到看 docker info 的输出,才发现 gwbridge 已经 100% 满用。

根本原因在于一个不易察觉的配置陷阱:Docker Daemon 的配置文件里改了 default-address-pools,但这个配置只在 swarm init 那一刻消费一次。已经创建的网络不会动。之前改过这个配置的人可能不知道这个行为,改了等于没改。

修复不能简单地改配置重启。我试过在 staging 环境用 dry-run 验证,写脚本探测不冲突的子网段,然后在 production 的 canary 验证阶段发现"释放的 IP 立刻被其它 cycling 任务抢走"这样的负反馈。所以流程变成:先 scale 0 所有失联的服务(停止它们 cycling,释放的位置不会被抢),然后手动删除旧的 gwbridge、用新 subnet 重建。最终把容量从 253 扩到了 4094,增长了 16 倍。13 个失联用户全部恢复。

这次的启示是配置陷阱往往比代码 bug 更隐蔽。因为配置改了看不出效果,维护者会觉得没改上去、会反复尝试,但每次尝试的假设都错了。

第二堵:单容器资源限制与内核参数(05-25,FA-013)

五天后,部署了五个 P0 patch:B1 容器内存 burst factor 调整(硬限制改为内存 × 4)、B2 mcp register 的假失败救活逻辑、B3 ulimit nofile 扩大到 65536、B4 mcp cooldown 清理、B6 provision 接口幂等性。这一次的滚动升级从 05-25 凌晨 0:41 一直跑到 07:34,实际耗时 6 小时 42 分钟,原本预估只要 3 小时。

但更关键的数据是这个:升级前,平台里有一个重灾户容器一天被 OOM kill 了 247 次。这不是偶发故障——是每一天都在重复。升级完成一小时后,这个数字变成了 0。

这次的根因是九个缺陷的叠加。B1 的根因是硬限制设得太低(原来 93 个用户只有 500MB 硬限,远低于实际需要),B2 是 mcp register exit code 的错误判断(非零 exit 自动当做失败,但有时是因为配置覆盖了),B3 是文件描述符不足导致新连接打开失败。单个缺陷可能不致命,但聚在一起就是 OOM 风暴。更严重的是,一个用户的 OOM 不只影响那个用户——每次 OOM 都会触发内核的 swap 操作,拖累整机的 I/O,让 cpa-api 主进程的事件循环卡顿 18 秒。前端看到的是全站变慢,实际根因可能是某个用户容器在反复 OOM。

部署前做了充分的 staging 验证和 production canary,分阶段升级了重灾户、然后全量升级、最后等完全收敛。期间遇到的问题是单容器的 docker service update 实际耗时约 60 秒(含 Swarm scheduler 延迟),并发调度起来比预期慢 2 倍。

这次的启示是:当多个独立缺陷同时叠加时,表象看起来是单一故障(OOM 风暴),但根因分散在配置、参数、逻辑判断的不同层。修复必须从全景取证开始,先理清每个环节的缺陷,再按优先级有序修复。一次部署才能彻底收敛,局部修复反而会留下隐患。

第三堵:编排层与自愈能力(06-03,迁 K8s)

到 06-03,用户已经涨到 574 个。继续打补丁的成本已经高于"一次性迁移编排平台"。Swarm 的问题不只是容量——还有调度自愈的缺失。任何故障都依赖人工介入,一个人无法 24 小时在线。决策很简单:从 Docker Swarm 迁到 Kubernetes。

这次迁移的复杂性不在于技术实现本身(用 COS 中转数据、转换 sub2api 密钥、把 124G 数据分批导入),而在于理解新平台引入的新故障域。K8s 有更细的控制粒度和自动调度,但同时也暴露了之前 Swarm 隐藏的问题。比如共享存储(NFS/SFS)的客户端卡死,在 Swarm 时期可能因为容器分散在不同节点而被掩盖;但在 K8s 这样的细粒度编排下,如果一个节点的 NFS 客户端出问题,节点上的所有 pod 都会受影响。所以迁移不是终点,而是暴露新问题的起点。

第四堵:节点级存储与可观测性(06-05,FA-015)

迁移两天后,某个节点上的 NFS 客户端在某个时刻 hang 住了。Kubernetes 不知道发生了什么,kubelet 仍然报告节点 Ready(因为 kubelet 本身没卡),但这个节点上 52 个实例的网关全部 DOWN 或 HUNG。这 52 个实例对应的是本该分散部署的用户容器,因为某种原因堆在了同一个节点上。

故障的症状可以分成几类。单个实例网关的 DOWN(容器无法启动、schema 非法)、HUNG(反复重启导致 adapter 504)、节点级的卡死(整节点上的容器创建/删除都阻塞)、数据库与实际状态割裂(DB 里是 ERROR、但 pod 健康了)。每种症状对应不同的根因,需要不同的检测和自愈手段。

最严重的是,这个故障没有任何告警。平台的监控指标全部正常,绿灯一片。直到用户反馈"连不上面板",才被发现。这已经是故障发生几小时以后的事。故障本身是可逆的——节点冻死就人工 cordon、删除卡住的 pod、让其它节点重新调度。但不可见的故障比故障本身更致命

这次事故直接导出了四层兜底的设计:L0 从配置和资源限制层面降低故障触发(比如 NFS 改 soft 参数而不是 hard,这样网络抖动时容器报错退出而不是整个节点冻);L1 加检测让故障可见(每 60 秒检测一遍"节点上是否有 pod 卡 ContainerCreating、网关探测失败率多少");L2 对低风险故障自动自愈(网关单次 DOWN 就重建 pod、DB 对账失败就修复状态);L3 对高风险动作告警优先(节点冻死先推送告警、等人工确认再 drain)。

三件真正决定可行性的事

1. 文档即基础设施

这个平台现在有 200+ 份设计文档与 80 份故障档案。这些不是为了"好看"存在的。

一个人无法对整个系统保持完整的心智模型。200+ 文档是唯一能让一个人记住系统全貌的方式。每当遇到新故障,能快速检索以前遇过的类似问题。每当需要做架构决策,能回溯当初为什么这样设计、放弃过哪些选项。

同时,这些文档也是 AI 能有效介入的前提。我可以把这些故障档案喂给模型,让它帮助排查新问题、验证修复方案、甚至生成监控规则。但前提是要把故障记录得清楚。空洞的"修好了"没有任何价值。

2. 故障必须被归档

同一个症状反复出现,每次修复可能只治了表象的一个侧面,真正的收敛需要理解全部的根因。这只有在每一次都写清档案的情况下才可能。

如果没有归档制度,第二次遇到类似症状时,维护者根本不知道第一次修复做过什么、为什么还没有彻底解决。"又来了"和"这个问题还有遗留"的反应完全不同。前者是被动应对、逐次救火,后者是主动追踪、系统解决。

实际上,平台里有不少故障走过了四到五次的修复周期。每一次修复时,回看之前的档案,能快速理清"这一层已经改过,那一层还没触及"。这种"有案可查、有据可循"的状态,把修复从赌博变成了可重复的流程——每一次问题复发时,不是从零开始排查,而是从已知的检查点继续。

3. 自愈优先于告警,告警优先于人工

在 FA-015 之前,平台大量依赖人工值守。任何问题都需要运维看到日志、理解现象、手动操作。一个人无法 24 小时在线。

FA-015 的教训直接导出了四层兜底的设计:

  • L0 预防:从配置和资源限制的层面降低故障触发的概率。
  • L1 检测:让不可见的故障变成可见——节点卡死、实例网关异常、数据库与实际状态割裂,全部要有独立的检测逻辑。
  • L2 自愈:对于低风险的故障(单实例网关重启、DB 状态对账),直接自动修复。高风险的动作(节点 drain)先告警、等人工确认。
  • L3 告警:自愈失败时、检测到新的异常时,推送给人。

这样的设计下,一个人维护平台的上限大幅抬高。不是因为个人能力变强了,而是系统能自动处理大多数故障,只把人类的决策能力用在最关键的地方。

诚实的边界在哪

但这个设计也有明显的天花板:

真正撑不住的(需要六小时以上连续操作、需要跨时区响应、需要多人交叉验证):

  • 数据库或存储层的重大故障。恢复涉及数据一致性检查,无法完全自动化。
  • 涉及业务逻辑的错误。修复需要理解用户意图,不只是系统恢复。
  • 密钥泄露或安全事件。需要立刻通知客户、协调应急处置、事后全面审计。
  • 多个独立故障同时发生、相互放大的情况。需要多个人在不同维度分别操作。

如果重来一次,优先级这样排

  1. 最先做的是 L1 检测——让故障可见。这是一切自动化的前提。宁可产生虚报,也不能漏掉真实故障。
  2. 其次是 L0 预防——从配置、资源限制、网络参数这些基础设施层降低故障率。这些改动成本低、收益高。
  3. 然后才是 L2 自愈——只对低风险的故障做自动恢复。对于高风险操作,即使多花一个人工确认的时间,也要确保不会进一步破坏系统。
  4. 最后是文档和监控。这些不是"最后的事情",而是贯穿全过程的——每个改动都要同步更新文档、每个故障都要写进档案。

那些回避的成本很高。曾经因为 NFS 挂载的 hard 参数导致节点冻死,这个参数的改动只需要改一行配置文件、然后滚动重启一次实例。但因为这个调整一直没做,就承受了几小时的无声故障。反过来说,那些看起来"小"的改动——改配置参数、改资源限制、加一个监控规则——才是最划算的投资。

星野的头像

星野 XINGYE

一个人维护 AI 平台的工程师。这里记录 63 篇复盘:18 份故障档案、OTA、架构演进与工作流。