在线 Debug I:活体检查
兑现承诺的一章。前十章造好了一个活的系统:百万进程、各自心跳、崩了自愈。现在回答那个把你带进这本书的问题——系统跑着的时候,怎么把手伸进去?本章全是可以立刻上手的硬货:三行命令连进一个活节点,掏出任意进程此刻的内部状态,再给它挂上实时追踪。读完你会明白,为什么用过的人都说这个体验「回不去了」。
先把梯子摆好:Debug 工具的四级台阶
「Debug」在 Elixir 里不是一个工具,是一道梯度,按侵入性和场景从轻到重:
| 台阶 | 工具 | 场景 |
|---|---|---|
| ① 打印 | IO.inspect(x, label: "...") / tap | 开发期,看一眼值(第 4 章) |
| ② 透视 | dbg() | 开发期,整条管道逐段曝光(第 4 章) |
| ③ 断点 | IEx.pry / break! | 开发期,在 iex 里冻结现场单步看 |
| ④ 活体检查 | 本章与下章的全部内容 | 运行中的系统,包括生产 |
①②在第 4 章交过了(label: 选项让 inspect 输出带名字,多点打印不串),③一句话补齐:代码里插一行 require IEx; IEx.pry,用 iex -S mix 跑到那里,执行会暂停,当前作用域的变量随你查——类似打断点,但断在 REPL 里。前三级都是「开发期武器」,和别的语言大同小异。真正的分水岭是第④级:别的语言到③为止,BEAM 从④才开始发力。
公理:一切都是进程,所以一切可查
活体检查的全部魔法建立在两条你已经学过的事实上:①系统里任何活动都是进程(第 7 章);②虚拟机像操作系统一样管理着它们(第 1 章)。所以「查看系统」=「遍历进程 + 读进程体征」,而这两个都是内置函数:
iex(1)> length(Process.list())
87
iex(2)> pid = Process.whereis(Counter)
#PID<0.121.0>
iex(3)> Process.info(pid, [:message_queue_len, :memory, :reductions, :current_function])
[
message_queue_len: 0,
memory: 2688,
reductions: 1403,
current_function: {:gen_server, :loop, 7}
]
iex 还备了一发全景弹:runtime_info(),一条命令打出节点的系统总览——虚拟机版本、调度器数、总进程数、内存分账、原子表水位,相当于一份「入院总检报告」,查体前先扫一眼定基调。
逐项读这份体征单:message_queue_len 是第 7 章 Demo 里那个水位数字的真身,BEAM 系统健康度的头号指标——它在涨,说明这个进程消费不过来了;memory 单位字节,2.6 KB,第 7 章的数字得到验证;reductions 是累计运算量,谁烧得快谁是 CPU 大户;current_function 显示它此刻停在 gen_server 的 loop 里等消息——连「它卡在哪个函数」都直接可问。这就是查体的基本功:找到进程,读它的体征。
:sys.get_state:掏出正在跳动的状态
体征之上,是内脏。第 8 章埋的谜底现在揭开:所有 OTP 标准件(GenServer/Supervisor/Agent)都实现了 :sys 系统协议——系统消息和业务消息走同一个邮箱,由框架代答,你的回调毫不知情。于是——
iex(1)> :sys.get_state(Counter) 2 iex(2)> Counter.incr() :ok iex(3)> :sys.get_state(Counter) 3
请停下来体会一下这两行的分量:没有加日志、没有埋点、没有重启、没有 debug 编译标记,一个正在服务的进程,此刻抱着的完整状态,应声交出。状态是嵌套 Map 也照样整个掏出来,配合 IO.inspect(pretty: true) 直接读。姊妹函数 :sys.replace_state/2 甚至能当场换掉状态——急救时手动把坏数据修正回去,系统不停机。(强调:replace 是急救刀,不是日常餐具。)
再给进程挂上实时心电::sys.trace(Counter, true),之后它收的每条消息、每次状态变更都实时打印,false 关闭。同族还有 :sys.statistics(pid, true)(开始记账:处理了多少消息、多少次 reduction,查完 get_statistics 取账本)和一口气全掏的 :sys.get_status(pid)(状态 + 父进程 + 运行模式一张总表)。开发环境随便开;生产环境 trace 不限速,高流量进程会刷屏拖垮节点——生产用下一章的限速版(recon_trace)。
③号台阶也有个进阶形态值得记下:用 iex --dbg pry -S mix 启动时,代码里的 dbg() 不再只是打印,而是升级成断点——执行停在那里,进入 pry 会话,变量任查,输入 continue 放行。同一个 dbg 宏,开发期两副面孔。
:observer:图形化的整机监护仪
iex(1)> :observer.start() :ok # 弹出 GUI 窗口
一行命令,弹出整个节点的可视化监护台:System 页是内存/调度器/IO 总览曲线;Applications 页是第 9 章预告的时刻——每个应用的监督树画成一张活图,节点是进程、连线是 link,右键任意节点可以看状态、发消息、甚至杀掉它(试试,看监督者瞬间拉起新的);Processes 页是全进程排行榜,按 reductions 排序抓 CPU 大户,按 message_queue_len 排序抓堆积瓶颈。第一次打开 observer 看到自己的监督树在眼前呼吸,是每个 BEAM 工程师的成人礼。
重头戏:连进另一个活着的节点
以上都在自己的 iex 里玩。现在复刻生产场景:系统在那边跑着,我从这边连进去。这是第 1 章你为之而来的那张王牌,过程比传说朴素得多——开两个终端就够:
$ iex --sname prod
iex(prod@MacBook)1> {:ok, _} = Counter.start_link(0)
iex(prod@MacBook)2> Counter.incr()
# 它就这么一直跑着,别管它了
$ iex --sname doctor --remsh prod Interactive Elixir (1.20.0) - remote shell attached iex(prod@MacBook)1> self() #PID<0.135.0> iex(prod@MacBook)1> :sys.get_state(Counter) 1 iex(prod@MacBook)2> :observer.start() :ok # GUI 在你这边弹出,看的却是 prod 节点
--sname 给节点起短名(同机互连;跨机器用 --name 带完整主机名),--remsh prod 的意思是:「起一个新节点,但 shell 直接寄生到 prod 节点里」。看终端 B 的提示符——iex(prod@MacBook),你敲的每一行代码都在对方的虚拟机里求值:whereis 找的是它的进程,get_state 掏的是它的状态,observer 观测的是它的心跳。生产环境的操作与此完全同构,只是节点名换成 app@10.0.3.7,并多一道门禁(见警示)。用 release 打包部署的应用连命令都备好了:bin/my_app remote,一回车就是这个 remsh。
三条安全须知。①互连凭证是 cookie(~/.erlang.cookie 或 --cookie 参数):两个节点 cookie 一致才许连接,生产 cookie 是机密,别进 git。②分布式端口(epmd 4369 及动态端口)绝不能暴露公网——有 cookie 就有全部权力(能 get_state 就能 :os.cmd)。标准做法:端口只对内网/localhost 开放,工程师 SSH 跳板进去再 remsh。③remsh 里你是「在生产神经系统里挥手术刀」:查看类操作(info/get_state/observer)随便用,改变类操作(replace_state、杀进程、热载)按你们的变更流程走。
类比一下才知道这有多奢侈。Android 侧想看 App 运行时状态:接 adb、上 Flipper/Stetho、预埋调试面板,还只能看你提前想到要暴露的东西;线上用户的问题,靠日志回捞和远程配置碰运气。而 BEAM 的答案是:任何进程、任何状态、任何时刻,shell 一连全都在——不需要预先想到,因为观测接口是运行时的公共设施,不是应用层的自建工程。这就是「可观测性内置于虚拟机」和「可观测性是个第三方 SDK」的差距。
动手:远程查体台
压轴模拟器:左边是一个「活着的生产节点」——OrderCounter 在真实地累加订单、邮箱在真实地进出消息(你看不到内部,那正是问题所在);右边是你的远程 shell。建议的查体流程:①Node.self() 确认身份;②whereis 找到 pid;③get_state 掏两次状态,观察 total 在涨;④把左边流量滑块拉满,连查几次 message_queue_len,亲眼看堆积;⑤开 :sys.trace 围观消息流,记得关掉。每一条命令在真机上原样可用。
查体口袋卡
本章命令按「问题 → 处方」整理成一张口袋卡,值班时照抄:
| 你想知道 | 处方 |
|---|---|
| 这个名字对应哪个进程 | Process.whereis(Name) |
| 它现在抱着什么状态 | :sys.get_state(Name) |
| 它堵了吗、多重 | Process.info(pid, :message_queue_len) |
| 它在吃多少内存/CPU | Process.info(pid, [:memory, :reductions]) |
| 它此刻卡在哪个函数 | Process.info(pid, :current_function) |
| 它邮箱里都压着什么信 | Process.info(pid, :messages)(短邮箱可看,长的别全打) |
| 它每条消息的进出实况 | :sys.trace(pid, true)(记得关;生产用 recon_trace) |
| 整个节点的全景 | :observer.start() |
- 真机复刻双终端实验:prod 节点跑第 8 章的 Counter,doctor 节点 remsh 进去 get_state。这是本书最重要的一次动手,别跳过。
- 在 remsh 里用
Process.list() |> Enum.map(&Process.info(&1, :message_queue_len))找出邮箱最长的进程(下一章有现成函数,先笨办法体会一次)。 - observer 的 Applications 页里找到你的 Counter,右键 kill 它,看监督树把它拉起来——第 9 章 + 第 11 章一次连线。
- 思考:为什么说「OTP 标准件写的系统天生可观测」?:sys 协议在其中扮演什么角色?
你已经会「看」了。下一章升级成「治」:生产级的限速追踪、运行中换代码的热加载,以及多节点分布式——在线 Debug 的完全体。