Erlang跟踪调试指南
编程最重要的一点就是学会调试。顺序执行的时候调试难度就非常大,并发型系统的调试就更不用说了。而Erlang整体生态就是一个并发型的生态,其调试难度非常大。但是这其中还是有一些技巧的,当我们学会这些技巧,很多事情就好做多了。
原文地址: Guide to Tracing in Erlang
在过去的5年里面,我花费了很多时间去调试Elixir和Erlang的应用,因此我决定将这些经验分享出来。我从2017年的4月就开始写篇博文(发表在2021年8月)但是一直没有什么灵感去完成它,直到一个月前观看了Jeffery Utter’s Debugging Live Systems on the BEAM的视频,我决定把博文完成。如果你想更快的了解Erlang的跟踪技术,我非常建议你去看视频,而不是阅读本博文。
在调试Erlang程序时,首先就是要理解因为Erlang的并发特性,调试的手法和一般程序会大相径庭。虽然Erlang有传统的调试器,它可以设置断点,暂停进程,但是如果系统中有进程正在等到我们暂停的进程的消息,这会导致那个进程崩溃。另外一点,跟踪可以在任何情况下使用,并且能和传统调试器一样提供丰富的调试信息。同时跟踪也比断点调试灵活,因为它不会干扰进程的执行,并且能同时获得多个进程的信息。
Erlang的传统调试方案
有些时候,只需要设置几个断点,就可以找出问题所在。因此Erlang带有一个调试器模块,这样可以让我们设置断点并暂停执行,检查上下文和变量。并且这个调试器带有GUI,这可以让我们非常容易的控制调试过程
在这篇博客中,我会使用非常简单的arithmetic
模块作为例子。如果需要调试某个模块,需要在编译这个模块时候添加debug_info
标志。使用erlc +debug_info
可以编译出带有调试信息的模块。好了,我先执行下erlc +debug_info arithmetic.erl
。编译结束后,我们在Erlang的shell中执行debugger:start().
:
1 2 3 |
Erlang/OTP 18 [erts-7.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Eshell V7.2 (abort with ^G) 1> debugger:start(). |
接着我们就可以看到漂亮的调试器的GUI了
在GUI下我们可以点选_Module > Interpret…然后选择我们要调试的文件。那些没有使用debug_info
编译的模块会变成灰色的。当我们成功家在文件后,我们就能在”Module“这个菜单项下看到这个模块。然后我们在子选项点击View_。就会出现查看代码的窗口。
在任何你想添加断点的地方双击下,并回到刚才启动调试器的Erlang的shell中执行相应代码进行调试。这次我想调试arithmetic:multiply/2
,所以我执行了
1 |
2> arithmetic:multiply(1,2). |
此次执行会被阻塞,调试器的主窗口中会显示中断的状态。双击列表将显示带有调试控制器的调试器,其中会显示源代码,以及变量值的。这样我们就可以检查变量的值,并通过单击“继续”按钮转到下一个断点。
这个调试器具备所有其它调试器该有的功能,更多的功能信息可以查阅文档debugging page in the Erlang documentation.。
在Erlang中,像这样进行调试是有一定的限制的。首先,模块必须使用debug_info
来编译(erlc +debug_info
)。其次,代码在调试阶段会在断点处阻塞。在很多语言中,这并不是一个问题,因为它们是顺序的,程序会在用户点击调试器后继续。但是Erlang的问题是,如果应用中的一个进程正在等待被调试的进程的消息。但是这个进程并不知道被调试进程已经发生阻塞了,该进程会在等待被调试进程的消息的过程中超时,还可能会造成崩溃。超时,京城会导致1个或这2个进程都被杀死或者崩溃。跟踪是不会阻塞执行的,因此它非常适合调试多进程参与的复杂场景。
跟踪调试入门
当谈到Erlang的程序跟踪技术时,很容易不知如何入手。事实上,这里有多种方法可以完成跟踪调试,我们可以使用Erlang自带的工具,也可以使用许多成熟的开源库。我们先来介绍下Erlang最基本的跟踪函数,那些在erlang
模块中定义的函数。然后会介绍dbg
跟踪器和一些第三方开源库。
erlang:trace/3 和 erlang:trace_pattern/2–3 函数
erlang:trace/3
是个会被其它所有的跟踪工具使用到的函数。我们先粗浅的了解这个函数的功能。这个函数有3个参数,但是第3个参数有很多选项,同时第1个参数也有很多可能的选择。
1 |
erlang:trace(PidPortSpec, OnOffBoolean, FlagList) -> integer() |
第1个参数可以是PID,Port的PID或者通过原子去指定特殊类型的进程和Port去跟踪
1 2 3 4 5 6 |
PidPortSpec = pid() | port() | all | processes | ports | existing | existing_processes | existing_ports | new | new_processes | new_ports |
文档介绍了所有的参数的含义,但是我们只先关注以下这几个
all
跟踪所有的进程和Portprocesses
只跟踪进程new
只跟踪新建的进程和Portexisting
只跟踪当前已经存在的进程和Port
只跟踪进程 所有的跟踪操作都需要选择进程来进行信息收集。如果我们想跟踪一个可以被任何进程调用的函数,我们就需要跟踪所有的进程来收集信息。
图4: Visual cheatsheet of the possible PidPortSpec values
第2个参数就是简单的boolean值,用来打开或者关闭跟踪。设置为true
我们就打开了跟踪,设置为false
时我们就关闭了跟踪。第3个参数是一堆标志开关,用来控制我们是如何进行跟踪的:
1 2 3 4 5 6 7 8 9 |
all | send | 'receive' | procs | ports | call | arity | return_to | silent | running | exiting | running_procs | running_ports | garbage_collection | timestamp | cpu_timestamp | monotonic_timestamp | strict_monotonic_timestamp | set_on_spawn | set_on_first_spawn | set_on_link | set_on_first_link | {tracer, pid() | port()} | {tracer, module(), term()} |
选项非常多,并且这些选项可以进行组合。文档trace/3详细的解释了每个选项的含义。但是我们依然只关注比较重要的几个:
all
组合了除了tracer
和cpu_timestamp
外所有的跟踪选项。这是一个非常好的默认选项。send
,'receive'
跟踪发送和接收的消息。garbage_collection
跟踪垃圾回收情况set_on_spawn
和set_on_link
让已经被跟踪的进程创建出的进程或者link的进程也被跟踪。{tracer, Pid}
指定一个进程去接收跟踪时收到的消息。这是个非常有用的选项,当我们不想把跟踪信息全发送的shell进程而是发送给特定的接收进程时,就非常有用。{tracer, Module, State}
指定一个跟踪函数,用来处理跟踪得到的消息,可以参考erl_tracer编写跟踪模块。
所有我们想记录的事件必须是通过这些_跟踪选项_来指定。
erlang:trace/3
是可以单独使用的,但是如果我们想跟踪函数,我们就需要使用erlang:trace_pattern/2-3
来告诉Erlang那些函数是我们想要跟踪的。erlang:trace/3
用来指定那些进程或者Port会被跟踪,而erlang:trace_pattern/2-3
是用来指定那些函数是我们要跟踪的。跟踪事件只会在被跟踪的进程调用被跟踪的函数时才会发生。这个函数的第1个参数时我们常说的MFA元组(模块,函数,参数个数),这个元组所指定的函数将会被跟踪。我们可以参考以下几个例子:
1 2 3 4 5 6 7 8 |
% 模块下所有名为Function的函数 {Module,Function,'_'} % 模块下所有函数 {Module,'_','_'} % 所有模块下的全部函数 {'_', '_', '_'} |
第2个参数可以时true
或者false
用来开启或者关闭跟踪或者是符合match spec规范的模式跟踪。我们稍后再讨论match spec。第3个参数和模式跟踪有关系,这里我们只关注最重要的两个:
global
跟踪所有通过模块名显示调用函数的情况,例如arithmetic:multiply(3, 4)
local
跟踪所有调用该函数的场景,包括不使用模块名的情况,例如multiply(3, 4)
trace_pattern/3里面详细解释了每个参数的含义,我们在此就不多做赘述了。
erlang:trace/3 和 erlang:trace_pattern/3实用演示
让我们为arithmetic
模块中的一个函数设置跟踪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
% 跟踪所有的进程call操作 1> erlang:trace(all, true, [call]). 40 % 跟踪arithmetic模块中所有的函数 2> erlang:trace_pattern({arithmetic, '_', '_'}, true, [local]). 6 % 启动一个新的进程来执行函数 3> spawn(fun() -> arithmetic:multiply(4, 3) end). <0.126.0> % 关闭跟踪 > erlang:trace(all, false, [call]). 40 % 因为我们没指定跟踪事件接收进程,这些消息都被发送到了shell中 > flush(). Shell got {trace,<0.126.0>,call,{arithmetic,multiply,[4,4]}} Shell got {trace,<0.126.0>,call,{arithmetic,'-multiply/2-fun-0-',[1,0,4]}} Shell got {trace,<0.126.0>,call,{arithmetic,'-multiply/2-fun-0-',[2,4,4]}} Shell got {trace,<0.126.0>,call,{arithmetic,'-multiply/2-fun-0-',[3,8,4]}} |
我们可以看到,shell进程接收到了每次调用arithmetic
模块函数时的消息。因为我们在模式追踪中使用了local
选项,因此我们甚至可以看到lists:foldl/3
中匿名函数调用的跟踪事件。我们也发现了,我们并没有收到其它模块的函数调用的跟踪事件。我们并没有看到lists:foldl/3
或者lists:seq/2
的调用跟踪事件,因为我们在模式跟踪中指定了,我们只关注arithmetic
模块。当我们在shell中执行flush()
函数时,我们会看到shell打印出Shell got
和我们收到的消息。不同的跟踪事件会产生少许不同格式的消息,但是所有的跟踪事件消息,都是以trace
开头的元组。因为我们只是跟踪了函数调用,所以我们看到的所有消息都是相同的格式
1 |
{trace, PidOfInvocation, call, {Module, FunctionCalled, ArgumentsInvokedWith}} |
erlang:trace/3
的文档中列出了所有跟踪消息的模式,在此也不做赘述了。
erlang:trace/3和erlang:trace_pattern/3的另一个例子
为了再演示一些erlang:trace/3
的功能,我们将使用稍微复杂一点的例子。取代前面我们使用的arithmetic
模块,这里我们对这个模块稍加变化,让该模块启动一个进程来进行基本的数学运算。为了简单起见,新的模块依然非常精简,但是我也不打算在这里展示其中的代码,但是你可以下载arithmetic_server
这个模块。
由于我们现在要跟踪一个Erlang的server,让我们跟踪下进程发送和接收到的消息,以及该模块的函数调用情况。同时让我们创建一个独立的tracer进程,来处理跟踪事件的消息,从而方便我们对这些消息做一些额外的操作。下面是一个简单的跟踪模块,将跟踪事件的消息用更方便阅读的方式打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
-module(my_tracer). -export([start/0]). start() -> spawn(fun loop/0). loop() -> receive % 打印更友好的消息格式 {trace, Pid, send, Msg, To} -> % 处理send io:format("Process ~p sent message ~p to process ~p~n", [Pid, Msg, To]); {trace, Pid, 'receive', Msg} -> % 处理receive io:format("Process ~p received message ~p~n", [Pid, Msg]); {trace, Pid, return_from, {Module, Function, Arity}, ReturnValue} -> % 处理返回值 io:format("Function ~p:~p/~p returned ~p in process ~p~n", [Module, Function, Arity, ReturnValue, Pid]); {trace, Pid, call, {Module, Function, Arguments}} -> % 处理调用 io:format("Function ~p:~p invoked with arguments ~p in process ~p~n", [Module, Function, Arguments, Pid]); Msg -> io:format("Received unexpected message ~p~n", [Msg]) end. |
我们可以看到这个跟踪模块,希望能接收到消息发送和接收的跟踪事件,同时它也能处理return_from
这个跟踪事件。在前面的例子中,我们只跟踪了函数的调用阶段,所以我们可以看到函数的调用参数,但是我们无法看到函数的返回值。这次我们让跟踪器跟中函数的返回值,但是想要完成这件事情,就必须使用match spec做参数来设置erlang:trace_pattern/3
。我并不打算在本篇博文中介绍match spec的细节,只要我们知道match spec是可以作为模式跟踪的参数就可以了。可以让跟踪器捕获函数返回值且最精简的match spec是[{'_', [], [{return_trace}]}]
。我们将使用这个参数来配置跟踪器来捕获返回值。
好了,让我们对我们的arithmetic server进行跟踪吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
% 启动跟踪模块 1> Pid = my_tracer:start(). % 启动算数服务器,此时我们还没有启动跟踪器 2> ServerPid = arithmetic_server:start(). % 开始进行跟踪 3> erlang:trace(ServerPid, true, [call, send, 'receive', {tracer, Pid}]). % 设置模式跟踪 4> erlang:trace_pattern({arithmetic_server, '_', '_'}, [{'_', [], [{return_trace}]}], [local]). % 执行算数服务器中的函数 5> arithmetic_server:multiply(ServerPid, 4, 3). Process <0.89.0> received message {<0.85.0>,{multiply,4,3}} Function arithmetic_server:multiply invoked with arguments [4,3] in process <0.89.0> 12 Function arithmetic_server:'-multiply/2-fun-0-' invoked with arguments [1,0,4] in process <0.89.0> Function arithmetic_server:'-multiply/2-fun-0-'/3 returned 4 in process <0.89.0> Function arithmetic_server:'-multiply/2-fun-0-' invoked with arguments [2,4,4] in process <0.89.0> Function arithmetic_server:'-multiply/2-fun-0-'/3 returned 8 in process <0.89.0> Function arithmetic_server:'-multiply/2-fun-0-' invoked with arguments [3,8,4] in process <0.89.0> Function arithmetic_server:'-multiply/2-fun-0-'/3 returned 12 in process <0.89.0> Function arithmetic_server:multiply/2 returned 12 in process <0.89.0> Process <0.89.0> sent message 12 to process <0.85.0> Function arithmetic_server:loop invoked with arguments [] in process <0.89.0> % 关闭跟踪 6> erlang:trace(all, false, [call, send, 'receive']). |
追踪事件的消息被发送到我们所指定的进程上而不是将这些消息发送给调用erlang:trace/3
函数的进程,我们所制定的进程将接收到的消息直接打印出来。我们可以看到,当函数调用返回时,我们的跟踪模块会打印出返回值。这样我们就可以明确的之后,每次函数调用接收到参数以及相对应的返回值。在这个例子中,我们也会看到lists:foldl/3
中的匿名函数也被追踪了,这可以让我们明确的看到它的参数和返回值。
erlang:trace/3的不足
erlang:trace/3
和erlang:trace_pattern/3
是非常强大函数,它们可以追踪函数调用,垃圾回收,消息传递和其他的一些Erlang VM内部的东西。这些函数几乎无所不能。但是erlang:trace/3
也有一些显而易见的缺点:
- API拥有太多且复杂的选项就如我们所展示的这些例子一样,简单的函数跟踪任务至少需要两次函数调用,每个函数都需要3个参数。这对一个简单的跟踪任务真的是有点复杂。
- 接口使用并不直观。除了非常复杂之外,这两个接口使用非常不直观。在前面的例子中,我们就可以清晰的感受到:首先是关闭追中。我们调用
erlang:trace/3
时第3个参数需要使用和打开追踪时是相同的,否则的化,我们是无法正常的关闭跟踪功能。第二点,跟踪函数返回值只能使用match spec和erlang:trace_pattern/3
而不是直接在erlang:trace/3
中设置选项。 - 无法在生产环境上安全使用。如果我们在跟踪太多的东西了,很容易让当前节点崩溃。同时跟踪可能会引起死锁,因为被跟踪的函数被跟踪模块使用了。我曾经在开发环境中犯过很多简单的错误,并导致节点崩溃。
dbg
dbg将是我在本篇博文中唯一介绍的Erlang自带的跟踪模块。dbg是一个字符界面的跟踪器,通常它只是将采集到的数据发送到shell中。它是runtime_tools
应用的一部分,如果我们想在应用中使用dbg,就需要确保我们已经包含了runtime_tools
。
dbg就像erlang:trace/3
一样灵活,但是它的接口却非常不同。dbg并没有像trace那样只提供了两个函数和大量的选项,而是选择提供了大量的函数,每个函数只需要非常少的参数。
使用dbg进行跟踪调试
让我们用dbg来替代前面例子中的erlang:trace/3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
% 启动跟踪器 1> dbg:tracer(). {ok,<0.87.0>} % 跟踪所有进程的调用操作 2> dbg:p(all, [c]). {ok,[{matched,nonode@nohost,42}]} % 跟踪arithmetic模块中所有的函数 3> dbg:tpl({arithmetic, '_', '_'}, []). {ok,[{matched,nonode@nohost,6}]} % 执行代码 4> arithmetic:multiply(4, 3). (<0.85.0>) call arithmetic:multiply(4,3) (<0.85.0>) call arithmetic:'-multiply/2-fun-0-'(1,0,4) (<0.85.0>) call arithmetic:'-multiply/2-fun-0-'(2,4,4) (<0.85.0>) call arithmetic:'-multiply/2-fun-0-'(3,8,4) 12 % 关闭跟踪 5> dbg:stop_clear(). ok |
我们可以看到这和erlang:trace/3
非长相似,但是只是使用了完全不同的函数调用,其中最大的区别就是我们需要通过dbg:tracer/0
先启动dbg的跟踪进程。
dbg:p(all, [c])
函数调用等同于erlang:trace(all, true, [call])
,并且dbg也会调用相同的函数。
dbg:tpl({arithmetic, '', ''}, [])
函数调用等同于erlang:trace_pattern({arithmetic, '', ''}, true, [local])
。都是让跟踪器去跟踪arithmetic
模块下的所有函数调用。
另外我们可以注意到dbg打印的日志更容易阅读,这和使用erlang:trace/3
时产生的跟踪事件是不同的,这些日至并不是消息,而是被格式化过的文本。如果我们在shell上进行调试,这非常方便。
另外一个使用dbg的例子
这次让我们像前面使用erlang:trace/3
来跟踪消息,函数调用和函数返回值。我们依然使用前面的例子,只是将其中erlang:trace/3
和erlang:trace_pattern/3
的函数替换成dbg中对应的函数。最大却别,就是这次函数比较简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
1> dbg:tracer(). {ok,<0.87.0>} % 启动server,但还没有开始跟踪 2> ServerPid = arithmetic_server:start(). <0.91.0> % 跟踪函数调用,消息 3> dbg:p(ServerPid, [m, c]). {ok,[{matched,nonode@nohost,1}]} % 跟踪arithmetic_server模块下的所有函数调用的返回值 4> dbg:tpl({arithmetic_server, '_', '_'}, [{'_',[],[{return_trace}]}]). {ok,[{matched,nonode@nohost,12}]} % 执行被跟踪进程中的代码 5> arithmetic_server:multiply(ServerPid, 4, 3). (<0.90.0>) << {<0.85.0>,{multiply,4,3}} (<0.90.0>) call arithmetic_server:multiply(4,3) 12 (<0.90.0>) call arithmetic_server:'-multiply/2-fun-0-'(1,0,4) (<0.90.0>) returned from arithmetic_server:'-multiply/2-fun-0-'/3 -> 4 6> (<0.90.0>) call arithmetic_server:'-multiply/2-fun-0-'(2,4,4) (<0.90.0>) returned from arithmetic_server:'-multiply/2-fun-0-'/3 -> 8 (<0.90.0>) call arithmetic_server:'-multiply/2-fun-0-'(3,8,4) (<0.90.0>) returned from arithmetic_server:'-multiply/2-fun-0-'/3 -> 12 (<0.90.0>) returned from arithmetic_server:multiply/2 -> 12 (<0.90.0>) <0.85.0> ! 12 (<0.90.0>) call arithmetic_server:loop() % 关闭跟踪 5> dbg:stop_clear(). |
好了我们可以看到,基本上是相同的,我们依然需要使用match spec的{return_trace}
去让dbg跟踪函数返回值,这和erlang:trace_pattern/3
完全一样。
dbg的不足
dbg
是个强大的跟踪调试库,提供了和erlang:trace/3
以及erlang:trace_pattern/3
等价且易于使用的接口,但是这依然很难使用。短函数名很容易混淆,并且难于记住。它依然继承了erlang:trace/3
的两个短板。
- 接口使用并不直观。函数的名字很难记住,跟踪函数调用的返回值依然需要使用match spec的{return_trace}。
- 无法在生产环境上安全使用。和
erlang:trace/3
一样,如果我们在跟踪太多的东西了,很容易让当前节点崩溃。
Erlang中其它跟踪调试模块
这里依然还有好几个Erlang自身携带的可以用于跟踪调试的模块。我在这篇博文中并不打算详细介绍,也许将来的博文会介绍他们。他们分别是
- et 事件跟踪。用于记录任意事件。可以使用记录的事件生成序列。
- ttb 跟踪构建工具。用于在分布式系统上创建跟踪。依赖
dbg
并且提供了很多简介的方法创建跟踪,并在分布式系统上进行跟踪。 - dyntrace 这是个捐赠模块,它提供了使用类似
dtrace
这种外部工具来进行动态跟踪的接口。 - seq_trace 用于跟踪进程间消息传输的跟踪模块。
- erl_tracer 用与实现自己跟踪器的behavior。
跟踪调试库
这里还有很多开源第三方的调试和跟踪调试库。这里介绍几个比较知名的库。
- recon_trace 一个可以在生产环境中安全跟踪单一Erlang节点函数调用的库
- redbug 一个和Erlang跟踪函数交互的工具。它具有一些内建的安全特性,如只使用跟踪函数功能的安全子集,当数据量过大的时候自动退出,并且可以在跟踪一段时间或接收了一定数量的跟踪事件后自动退出。
- 一个使用Erlang跟踪BIF且具有界面的Erlang,Elixir和LFE调试器。
在这三个当中,本篇博文只会选出Recon库中的recon_trace
作介绍。
Recon
Recon是Fred Hebert大神开发的一个开源库,其中包含很多模块用来对Erlang系统的生产环境进行诊断。其中的一个模块就是recon_trace,它的文档很好的概括了它的功能。
recon_trace可以在生产环境中安全跟踪单一Erlang节点上的函数调用,其中功能包括:
- 比dbg或者trace BIFs更易使用的接口
- 防呆设计(例如不小心跟踪了整个节点上所有的调用)
- 使用的跟踪事件计数或者限速进行安全保护
- 更好的日志格式
recon_trace
的接口要比erlang:trace/3
和dbg更易于使用。比dbg默认的跟踪结果输出更易阅读。recon_trace
可以在生产环境的节点上使用,但是只能在单一节点上跟踪函数调用。recon_trace
会在调用达到一定的数量后自动停止,因此任何一个跟踪器,我们在启动它的时候,都要设置它的跟踪调用上限。这样防止我们因为接收了太多的跟踪事件而让Erlang节点过载。
因为recon并非是Erlang自身携带的库,因此我们需要将它作为应用的依赖。如果我们打算将recon作为生产环境调试问题的工具,我们就需要在我们发布到生产版本时,把recon加入到依赖列表中。否则当我们在生产环境中遇到了问题,但是我们并没有recon库来帮助我们来调试问题。
如果我们使用rebar3,只需要在rebar.config
文件中加入recon
1 |
{deps, [recon]}. |
然后运行rebar3 get-deps
来下载recon。
使用recon进行跟踪调试
因为recon_trace
只支持跟踪函数,我们无法像erlang:trace/3
和dbg那样跟踪arithmetic_server
消息的发送和接收。但是我们依然可以使用recon_trace
来完成函数跟踪,并且它非常简单。我们全部所需要作的事情,就是以MFA元组(第3个元素要么是函数的参数个数,要么是一个match spec),最大接收跟踪事件的数量,一个可选参数列表为参数调用recon_trace:calls/2,3
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
% 我们将跟踪所有进程随后的20个函数调用 1> recon_trace:calls({arithmetic, '_', fun(_) -> return_trace() end}, 20, [{scope, local}]). 6 % 执行代码 2> arithmetic:multiply(4, 3). 20:32:49.814072 <0.146.0> arithmetic:multiply(4, 3) 12 20:32:49.825579 <0.146.0> arithmetic:'-multiply/2-fun-0-'(1, 0, 4) 20:32:49.825802 <0.146.0> arithmetic:'-multiply/2-fun-0-'/3 --> 4 20:32:49.826031 <0.146.0> arithmetic:'-multiply/2-fun-0-'(2, 4, 4) 20:32:49.826208 <0.146.0> arithmetic:'-multiply/2-fun-0-'/3 --> 8 20:32:49.826342 <0.146.0> arithmetic:'-multiply/2-fun-0-'(3, 8, 4) 20:32:49.826465 <0.146.0> arithmetic:'-multiply/2-fun-0-'/3 --> 12 20:32:49.826658 <0.146.0> arithmetic:multiply/2 --> 12 % 停止跟踪,很多时候我们无需这么作 3> recon_trace:clear(). |
总结
Erlang自身提供了非常底层的API和多种工具来调试程序代码。我们已经看到了Erlang中是可以使用断点调试这种传统调试方式,但是因为Erlang的并法特性导致它的应用场景非常有限。我们也看到了erlang:trace/3
和erlang:trace_pattern/2,3
是如何让我们在Erlang虚拟机上选定特定进程并跟踪几乎所有的事件。我们也尝试了dbg库,它提供了和地层API截然不同的接口来完成跟踪调试。最后我们使用了recon_trace
,这种可以在生产环境中安全跟踪函数调用的跟踪库。
这里所有的案例只展示出Erlang虚拟机底层跟踪API的一小部分能力。Erlang的虚拟机的跟踪功能非常强大,为我们提供了检查运行中系统并找出问题的一切工具。
如果你发现这篇博文对你很有用,你可以前去阅读Fred Hebert大神的免费电子书 >。 它涵盖了很多生产环境中调试的技巧,尤其是对没有使用Erlang经验的人非常有价值。同时也欢迎订阅我的博客,我正考虑编写更多关于Erlang和Elixir调试及跟踪的文章。
参考
文中已经提及的连接,就不再次提及
- Erlang in Anger
- Erlang presentation on dbg
- Blog post on using Erlyberly to do tracing on Elixir nodes
- Short screencast showing how to use dbg to trace ExUnit unit tests in Elixir
- More conference talk covering debugging techniques for Elixir
- Blog post on tracing remote nodes
译注
写程序难,写正确的程序更难。学习编程虽然算法很重要,但是在实际开发中很难保证一次性把程序写的完全正确。因此学会调试就显得非常重要。同时由于Erlang的并发特性,让调试变得很困难,这让很多人对Erlang望而却步(语法也是一个阻碍)。
我是在使用Erlang很久之后才学会了Erlang的调试和跟踪调试,但是不能像本篇博文的播主那样讲解的非常清楚。因此当我发现本篇博文后,就第一时间进行了翻译。
当然在调试Erlang程序时,除了调试和跟踪外,还有经久不衰的日志大法,很多并发和分布式系统中都使用了大量的日志来进行除错,因此日志也是一个不错的选择(但是会耗费性能)。不过我们可以认为跟踪调试,是日志调试的另一种形式,发现问题,按需开启跟踪并记录,从而解决问题。