在线 Debug II:生产级技艺
上一章你学会了「看」,这一章学「治」——以及在几千 QPS 的活体上动刀时,怎么不把病人捅出第二个窟窿。三门手艺:限速追踪(看清高频调用而不刷崩节点)、热加载(运行中换代码,状态不丢、连接不断),和分布式(多台 BEAM 组网后,查体半径扩到整个集群)。外加两件现代化装备:Livebook 和 observer_cli。
recon:生产环境的标准查体包
上一章自测题让你用 Process.list 笨办法找邮箱最长的进程——能用,但在几十万进程的节点上自己遍历,又慢又占资源。生产上大家用 recon:Erlang 圈公认的生产诊断库(作者 Fred Hebert,《Erlang in Anger》的作者),专为「在出事的节点上安全操作」设计。加一行依赖 {:recon, "~> 2.5"},查体包常备三板斧:
iex> :recon.proc_count(:message_queue_len, 3) # 邮箱堆积 TOP 3
[
{#PID<0.412.0>, 18243, [OrderCounter, ...]},
{#PID<0.397.0>, 12, [...]},
{#PID<0.201.0>, 3, [...]}
]
iex> :recon.proc_count(:memory, 3) # 内存大户 TOP 3
iex> :recon.proc_window(:reductions, 3, 1000) # 这 1 秒内谁烧 CPU 最凶
注意 proc_count 与 proc_window 的区别:前者看累计值(谁历史上吃得多),后者看时间窗增量(此刻谁在发疯)——排查「突然变慢」用后者。第一条命令的输出顺便把进程的注册名带出来了,18243 条积压 + OrderCounter 的名字,案情基本就明了了。
限速追踪:看高频调用,但别刷崩节点
上一章警告过 :sys.trace 不限速。BEAM 底层的追踪设施(:dbg/:trace)极其强大——能对任意模块、任意函数、按参数模式下钩子,不用改一行代码——但同样不限速:对一个每秒被调一万次的函数开追踪,输出风暴能把节点 IO 打死。这就是「电锯」:能力越界的工具,生产上需要带保险的版本。recon_trace 的保险是强制的消息配额:
iex> :recon_trace.calls({OrderCounter, :handle_cast, :return_trace}, 10)
2 # 布了 2 个追踪点
# 接下来 10 条命中会实时打印:谁调的、参数原文、返回值
13:02:11.482 <0.412.0> OrderCounter.handle_cast({:order, 8812}, %{total: 8811, ...})
13:02:11.483 <0.412.0> OrderCounter.handle_cast/2 --> {:noreply, %{total: 8812, ...}}
...
Recon tracer rate limit tripped. # 配额用完,自动全部拆除
读懂这个交互的设计:「给我看接下来 10 次这个函数的调用,连参数带返回值,然后自动收工」——哪怕你看完忘了关、哪怕 SSH 断了,追踪都不会留在节点上。第三个参数还能写成 {50, 1000}(每秒最多 50 条)做持续观察。对比一下别的技术栈:看生产函数的实参,通常意味着「加日志 → 发版 → 等复现 → 删日志 → 再发版」,一个来回一天;这里是 30 秒,零发布。
内存问题另有一把专用尺::recon_alloc。BEAM 的内存由自带的分配器体系管理,「top 显示占 8G 但进程加起来才 3G」这类悬案,答案通常在分配器碎片里——:recon_alloc.memory(:usage) 一行给出真实利用率。粗查则用内置的 :erlang.memory():总量、进程、ETS、二进制、原子各占多少,一张表分账。
追踪工具至此凑齐三把,按场景选:
| 工具 | 粒度 | 限速 | 用在哪 |
|---|---|---|---|
:sys.trace | 单个 OTP 进程的消息/状态 | 无 | 开发期、低流量进程 |
:recon_trace | 任意函数调用,按 M/F/A 下钩 | 强制配额 | 生产,默认选它 |
:dbg / :trace | 底层全能(消息/调用/GC/调度) | 无 | 实验室里解疑难,或给工具当引擎 |
热加载:换代码,不换进程
先看机制再谈边界——机制是虚拟机的正经特性,不是黑魔法。BEAM 允许每个模块同时存在两个版本:current(新)和 old(旧)。加载新版本时,正在执行旧代码的进程继续跑完手头的函数;此后所有带模块名的调用(Module.fun(),包括 GenServer 每次回调)自动进入新版本。旧版本被 :code.purge 淘汰时,还赖在旧代码里的进程会被杀掉(然后监督树接手,你已经知道剧情了)。iex 里体验:
iex> r Counter # 重新编译并加载 Counter 模块
{:reloaded, [Counter]}
iex> :sys.get_state(Counter)
8812 # 同一个进程,状态原封没动,代码已是新版
补丁上线后怎么确认生效?本卷的工具串成一条验证链::sys.get_state 确认状态无损 → :recon_trace.calls 抓两条新调用、看返回值已是修复后的行为 → proc_count(:message_queue_len, 3) 确认没有新堆积。观测、治疗、再观测——查体工具在治疗前后各用一遍,才算一次完整的处置。
r/1(重编译已加载模块)、c/1(编译指定文件)是 iex 的内置热载命令。进程不重启、PID 不变、状态保留、连接不断——这就是「运行中换代码」的字面意思。如果新版本的状态结构变了(比如 state 从整数变成 Map),GenServer 的 code_change/3 回调负责把旧状态迁移成新形状,由 :sys.suspend → change_code → resume 的标准三步驱动。
「Erlang 可以热更新整个系统」的传说,严格说指 release 级的 relup/appup 机制:全应用、带状态迁移脚本、可回滚。它真实存在,电信设备真在用,但互联网公司极少用——编写迁移脚本的复杂度高,而滚动重启(有监督树和多节点,重启本来就无感)便宜得多。行业现状:日常部署用滚动重启;热加载留给两个场景——①线上急救:严重 bug,发版流程要 40 分钟,热补丁 40 秒,先止血再正式发版;②有状态长连接:几十万 WebSocket 挂着,重启即全体掉线重连,热补单个模块能免掉一次「重连风暴」。定位准确的说法是:热加载不是日常部署手段,是止血钳——但当你需要止血钳时,别的技术栈根本没有。
热补丁的第一纪律:打完必须回填。节点里跑的是 v2,git 里躺的是 v1——下次正常发版,v1 会把你的补丁悄悄冲掉,幽灵 bug 复活且无人记得为什么。流程必须是:热补止血 → 同一份修复立刻走正式提交/发布 → 确认新版本落地。另外多节点集群,每台都要补,漏一台就是「行为不一致」疑难杂症的温床。
动手:热更新手术台
左边是带 bug 的 v1 代码(一次 :tick 加了 2),右边两个病人:热更新组和「重启式部署」对照组。点「热加载 v2」,看热更组的 count 曲线放缓但不归零、pid 不变、uptime 不断;再给对照组来一次重启部署,对比它失去了什么。
分布式:查体半径扩到整个集群
上一章的 remsh 已经偷用了分布式(它就是起了个节点连过去)。把它转正:任意两个 BEAM 节点,cookie 一致即可直接组网,组网后跨节点发消息和本地发消息是同一个 API:
iex> Node.connect(:node_b@host2) true iex> Node.list() [:node_b@host2, :node_c@host3] iex> :erpc.call(:node_b@host2, Counter, :value, []) 41023 # 在 b 节点上执行,结果传回来
上一章的 :observer 在这里顺势升级:GUI 顶部的 Nodes 菜单可以切换被观测的节点——本地开一个 observer,下拉选中生产节点,看的就是它的监督树和进程排行。做诊断跳板时还有个礼貌细节:--hidden 参数让你的诊断节点以「隐身」方式入网,不进对方的 Node.list()、不触发全网状拓扑,查完即走不留痕。
没有消息中间件、没有 RPC 框架、没有序列化协议选型——send(pid, msg) 里的 pid 本来就编码了节点信息,消息跨机器和跨进程长得一模一样。Phoenix 的多节点广播、分布式注册表(pg/global),都直接铺在这层地基上。对本卷的意义:你的 remsh 连进任何一台,就能以它为跳板查体全集群——:erpc.call 把 recon 三板斧撒到每个节点,一台终端监护整个机房。(生产集群的节点自动发现用 libcluster 库,K8s/DNS/gossip 策略齐全。)
现代化装备:Livebook 与 observer_cli
最后两件装备,把查体体验拉到 2026 年的水准。Livebook 是 Elixir 官方的交互式笔记本(像 Jupyter),关键是它可以 attach 到任何运行中的节点:连上生产,你的每一步诊断——命令、输出、图表、结论——自动沉淀成一份可重放的文档。事故复盘时,那份 notebook 就是完整的查体记录;下次同类故障,打开重跑即可。「诊断过程本身可版本化、可交接」,这是 remsh 裸奔时代之后的一大步。
observer_cli(基于 recon)则是 :observer 的终端版:生产机器通常没有 GUI,SSH 进去 :observer_cli.start(),htop 风格的全节点监护台就在字符界面里刷新——进程排行、内存曲线、网络 IO,方向键翻页。裸 SSH 环境的最后一块拼图。
再留一份书单坐标:本章的技艺系统化的成文出处,是 Fred Hebert 的 《Erlang in Anger》——副标题「愤怒中的 Erlang」,专讲「系统正在着火时怎么办」,官网免费,薄薄一百页,是 BEAM 圈的值班圣经。读完本书 VOL.IV 再读它,处处会心。
- 给你的练手项目加上 recon,跑一遍三板斧;故意 cast 风暴制造堆积,用
proc_count(:message_queue_len, 3)抓现行。 - 真机复刻热补:iex -S mix 跑 Counter,改源码里的步长,
r Counter,验证 pid 与状态未变而行为已变。 - 开两个 --sname 节点,Node.connect 组网,用 :erpc.call 跨节点取 Counter 状态。
- 装 Livebook(livebook.dev),attach 到本地节点,把第 1 题的诊断过程做成一份 notebook。
- 复述:热加载的两个正当场景是什么?第一纪律是什么?
至此,传说完整兑现:看得见(11)、治得了(12)、崩不倒(09)。最后一章,把这门语言的生态版图和你的下一步铺开——包括那个「只有 BEAM 做得优雅」的 LiveView,和 1.20 的类型系统。