题记与说明
| 元信息 | |
|---|---|
| 标题 | How Discord Scaled Elixir to 5,000,000 Concurrent Users |
| 作者 | Stanislav Vishnevskiy(Discord CTO & 联合创始人) |
| 来源 URL | https://discord.com/blog/how-discord-scaled-elixir-to-5-000-000-concurrent-users |
| 发表日期 | 2017-07-06 |
| 存档日期 | 2026-11-04 |
| 标签 | Elixir、并发架构、系统设计、性能优化、工程取舍、Agent 系统 |
这篇讲什么
2017 年,Discord 冲到近 500 万并发用户、每秒数百万事件,CTO 亲述 Elixir/Erlang 技术栈在膨胀路上撞到的三个瓶颈及解法:消息扇出、热点共享数据、并发过载保护。主线只有一句话:性能问题不靠加机器解决,靠拆解成本结构、找到真瓶颈、再选或造合适的原语。
核心观点
1. 消息扇出 → 自研 Manifold。 单次 send/2 因 Erlang 去调度要 30~70μs,大 guild(如 /r/Overwatch,3 万在线)一次发布要 0.9~2.1s,guild 进程消息队列积压到要手动关功能。Manifold 把发送工作按远端节点分组、每节点内按核数一致哈希分片给子 worker,并行了 CPU 成本还顺带降低节点间流量,同时保住客户端依赖的事件线性一致性——send/2 的即插即用替代品。
2. 热点数据 → 共享只读堆。 一致性哈希环查找原走单进程 request/reply(约 12μs/次),session 节点重启光查询就要 30 秒;搬进 ETS 后仍有 17.5s,因为环很大、进出 ETS 的拷贝才是大头。最终借 mochiglobal 思路把环编译成常量函数、放进 VM 的只读共享堆,单次查找降到 0.3μs,总耗时压到 750ms,代价是重建模块最多 1 秒——环很少变,值得付。
3. 并发保护 → ETS 信号量。 修好热路径反而撤掉了天然背压:500 万 session 进程踩踏 10 个注册进程,超时重试无限堆积,队列膨胀到 OOM 整个 VM,级联宕机。他们明确拒绝熔断器(不想要"超时爆发后一段时间什么都不试"的状态),改用 :ets.update_counter/4 的原子条件自增做信号量,让注定失败的调用提前放弃。效果:之后一次下游崩溃中,session 服务"纹丝不动",几分钟内自愈。
最值得记住的 3 个数据/金句
- "单次 send/2 30~70μs,大 guild 一次发布 900ms~2.1s" —— 热路径上的常数乘上规模就是事故。
- "环查找 17.5s → 750ms" —— 先 benchmark 再动手,最优解常藏在"语言不让你共享内存"这个假设的破绽里。
- 拒绝熔断器的理由:"我们不想让超时爆发导致一段时间内完全不做尝试" —— 背压应该是"提前放弃",不是"间歇性拒绝"。
对我的启发
- Agent 系统:多 Agent 并发编排会原样复现本文问题——共享注册表被踩踏、队列堆积、级联雪崩。信号量 + 提前退出(而非熔断器)可以直接搬进 Agent 的工具调用限流与子 Agent 并发控制。
- 写作:这是"架构决策记录落地为真实取舍"的活案例——每个决策都带测量数字、被否掉的备选方案(分片、熔断器、ETS)和明码标价的代价,技术写作就该这么写。
- 个人知识管理:Discord 把一次踩坑沉淀成三个开源库(Manifold、FastGlobal、Semaphore)加一篇博客,一次经验、多重输出,是知识复用的范本。
适合谁读
后端/分布式系统工程师、Erlang/Elixir 使用者、设计 Agent 并发编排需要限流背压思路的人,以及想把架构决策写成好文章的每个工程师。