![]()
Discord的工程师最近公开了一组数据:他们的语音和聊天系统每天要处理数十亿条消息,高峰期超过2000万用户同时在线。在这个体量下,他们完成了一个看起来不可能的任务——给Elixir的Actor模型装上分布式追踪,而且性能零损耗。
这相当于给一条高速公路上的每辆车都装上GPS,但不让车速掉哪怕1公里/小时。
为什么Actor模型天生"黑盒"
微服务架构的追踪相对直观。一个HTTP请求从网关进来,带着Header里的Trace ID在各个服务间流转,像快递单号一样可查。但Elixir的Actor模型完全不同——进程之间通过消息传递通信,消息本身没有任何"信封"可以贴追踪标签。
Discord的基础设施负责人Yuliy Pisetsky在博客中解释:「OpenTelemetry的标准实现能很好地追踪单个服务内部,但进程边界是断点。一个消息从用户A的进程发到频道进程,再广播到用户B、C、D的进程,这段链路在监控大屏上是完全不可见的。」
这种不可见性在排查问题时致命。想象一下:用户报告"消息延迟",工程师只能看到某个服务响应慢了,但不知道延迟发生在哪段进程跳转——是频道路由?还是消息序列化?或者是下游推送队列?
![]()
Discord从2021年就开始尝试解决这个问题。他们试过在消息体里手动注入追踪上下文,但Elixir的消息格式由业务代码定义,强行加字段意味着修改数千个进程的实现。也试过全局进程字典,但Actor模型的隔离性设计让这种"共享状态"方案格格不入。
Transport库:把追踪做成"基础设施层"
最终的解法是把问题下沉一层。Discord写了一个叫Transport的库,包裹住Elixir原生的消息发送接口。
具体机制是:每次进程间通信时,Transport自动把当前的Trace上下文编码进一个独立的元数据字段,随消息一起传递。接收方进程通过Transport接收消息时,自动解码并恢复上下文。对业务代码来说,这完全是透明的——他们仍然调用普通的send/receive,只是底层换成了Transport的包装版本。
Pisetsky提到一个关键设计决策:「我们必须支持两种模式。有些团队直接用裸消息传递,有些用GenServer(Elixir的通用服务器抽象)。Transport要同时覆盖这两种场景,否则 adoption(采用率)会很惨。」
更棘手的是部署。Discord的生产环境有数万个节点,任何需要停服升级的追踪方案都会被否决。Transport的实现利用了Elixir的代码热加载能力——新版本的进程可以和老版本进程互发消息,元数据字段的添加不会破坏旧代码的解析逻辑。
![]()
性能数据验证了这套方案的可行性。在内部基准测试中,Transport的上下文传递开销低于0.3%,在Discord的实际生产负载中完全淹没在噪声里。作为对比,他们早期试过的进程字典方案在某些场景下有5-8%的CPU开销。
从"能跑"到"可观测"的代价
这套系统上线后,Discord的故障排查时间从平均45分钟降到8分钟。一个具体案例:去年某次消息延迟报警,工程师通过追踪链路发现,问题出在特定类型的富媒体消息在序列化时触发了意外的二进制拷贝——这个瓶颈在之前的监控中完全不可见。
但Pisetsky也坦承了 trade-off(权衡)。Transport目前只覆盖进程间通信,对于NIF(原生接口函数,即调用C/Rust代码的边界)和外部数据库查询,仍然需要额外的手动埋点。此外,全量追踪产生的数据量巨大,Discord采用了动态采样策略——正常流量只记录1%的链路,异常场景自动提升到100%。
这套方案已经开源。GitHub仓库的README里有一句备注:「我们设计这个库时假设你有类似的规模问题。如果你的日活用户不到百万,直接用OpenTelemetry的Elixir SDK可能更简单。」
Discord的Transport库现在每天处理超过500亿条带追踪上下文的消息。Pisetsky在博客结尾写道:「最满意的反馈来自一位值班工程师——他说现在看到报警时,终于知道该叫醒哪个团队的同事了。」
你的基础设施里,有多少"黑盒"进程还在靠猜来排查问题?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.