BEAMアプリケーションのデバッグ

この記事は英語の原文を日本語に翻訳したものです。原文: https://blog.stenmans.org/theBeamBook/#CH-Debugging

翻訳元: happi/theBeamBook 7998e22e78417dbe20e5136b9aee862a1ecaa404(コミット 7998e22

はじめに

本章では、稼働中のサービスを止めることなくバグを見つけて修正するためのさまざまな手法を扱います。コードのテストとデバッグに役立つテスト技法、ツール、フレームワークを取り上げます。また、デッドロック、メッセージのあふれ、メモリの問題といった、バグのよくある原因にも触れ、それらを特定し解決するための指針を示します。

デバッグとは、ソフトウェアから誤り、いわゆる「バグ」を特定し取り除く作業です。Erlangにはデバッガのようなステップ実行式のデバッグツールもありますが、もっとも効果を発揮するデバッグ手法の多くは、Erlangのトレーシング機構に頼っています。この機構についてはトレーシングの章で詳しく取り上げます。本章では、DTraceとSystemTapによるシステムレベルのトレーシングにも触れます。

本章ではさらに、「クラッシュダンプ」という概念も扱います。これは、メモリ不足やエミュレータの上限到達といった回復不能なエラーが発生したときに、Erlangランタイムシステムが生成する人間が読めるテキストファイルです。クラッシュダンプはErlangノードの事後分析に欠かせないものであり、その読み方と理解の仕方を学びます。

これらに加えて、本章ではEUnitやCommon Testといった、コードの信頼性と堅牢性を保証するうえで欠かせないさまざまなテスト手法についても論じます。テストにおけるモック化の重要性と、そのベストプラクティスについても検討します。

「let it crash」の原則と、それをシステムの中で効果的に実践する方法を身につけます。例外の仕組みとスーパバイザツリーの設計についても理解を深めます。

本章を読み終える頃には、システムとその個々のコンポーネントを体系的にテストする知識が身につきます。よくある間違いや問題を見分けられるようになり、デバッグの思想のようなものも自然と身についているかもしれません。

デバッグの思想

デバッグはソフトウェア開発に欠かせない作業ですが、Erlangでは、この言語のフォールトトレラントな設計ゆえに独特のアプローチを取ります。失敗を防ぐことだけに注力するのではなく、Erlangはエラーを検知し、診断し、そこから効果的に回復するリアクティブなデバッグの思想を促します。Erlangにおけるデバッグでは、体系的な進め方を活用し、本番環境での障害を分析し、失敗から学んでコードの品質を継続的に高めていくことが求められます。

デバッグの体系的な進め方

構造化されたアプローチを取ることで、問題の特定と解決にかかる時間と労力を大きく減らせます。Erlangにおけるデバッグは、観察、切り分け、検証という手順を踏む、体系立った進め方に従います。

1. 問題を再現する

バグを修正する前に、まずその問題を安定して再現する必要があります。Erlangシステムで問題を再現するための技法には、次のようなものがあります。

  • 詳細なロギングlagerlogger)を有効にしてシステムを動かす。
  • dbgreconのようなトレーシングツールを使って、関数呼び出しやメッセージのやり取りを捕捉する。
  • 制御されたテスト環境で障害シナリオをシミュレートする

例: モジュール内の関数呼び出しを調べるためにトレーシングを有効にします。

dbg:tracer().
dbg:p(all, c).
dbg:tpl(my_module, my_function, []). % my_functionへのすべての呼び出しをトレースする

問題が断続的にしか発生しない場合は、prop_erCommon Testといったツールで負荷テストを行うと、競合状態を突き止める助けになります。

2. 問題箇所を切り分ける

問題を再現できたら、次は特定のモジュール、プロセス、関数へと問題を切り分ける段階です。

次のようにしてプロセスのメッセージキューを確認します。

process_info(Pid, messages).

メッセージキューが長い場合、性能上のボトルネックを示している可能性があります。

ETSテーブルとメモリ使用量を調べます。

ets:info(my_table, size).
erlang:memory().

問題に関係するプロセスだけに絞り込むため、選択的なトレーシングを使います。

dbg:p(self(), [m]).  % 現在のプロセスだけをトレースする

問題箇所を切り分けることで、デバッグの対象範囲を絞り込み、余計な調査に時間を取られずに済みます。

3. ログとクラッシュダンプを分析する

ログとクラッシュダンプは、システム障害について貴重な情報を与えてくれます。Erlangノードがクラッシュすると、次のような詳細を含むerl_crash.dumpファイルが生成されます。

  • クラッシュの原因(メモリ枯渇、無限ループ、デッドロックなど)
  • 障害発生時点での各プロセスの状態
  • クラッシュしたプロセスのコールスタック

例: クラッシュダンプのメモリ使用量セクションを確認します。

=memory
total: 2147483648
processes: 1807483648
ets: 107374182
binary: 32212254
code: 5242880

プロセスのメモリ使用量が異常に高い場合、メモリリークを示している可能性があります。

リアルタイムでのデバッグにはcrashdump_viewerを使います。

crashdump_viewer:start().

4. デバッグツールを活用する

Erlangには、システムの挙動を分析するための強力なランタイムデバッグツールが用意されています。

  • Observer GUIobserver:start()): 対話的なプロセス監視。
  • dbgrecon: 低レベルのトレーシングと調査。
  • SystemTapDTrace: 高度なデバッグ向けのカーネルレベルのプロファイリング。

適切なツールを使うことで、不要なコード変更を避け、デバッグを素早く進められます。

5. 修正を検証し回帰テストを書く

バグを特定して修正したら、それが再発しないことを確認します。

  • Common TestEUnit回帰テストを書く
  • エッジケースを検証するためにプロパティベースのテストPropErQuickCheck)を実行する。
  • 本番環境にデプロイする前にステージング環境でテストする

失敗から学びコード品質を高める

どんなバグも、コードベースを改善し将来の問題を防ぐ機会になります。Erlangの回復力と自己修復という思想は、開発者が失敗にどう向き合い、システムをどう洗練させていくかにも及びます。

1. ポストモーテムを実施する

重大なバグを修正したあとは、なぜそれが起きたのかどうすれば防げたのかを分析します。ポストモーテム分析では、次の問いに答える必要があります。

  • 問題の根本原因は何だったか。
  • システムにどのような影響を与えたか。
  • 同種のバグをどうすれば防げるか。

プロセスが予期しないメッセージによってクラッシュした場合は、メッセージのフィルタリングを堅牢にしておきます。

handle_info(_Unexpected, State) ->
    {noreply, State}.

2. ロギングとオブザーバビリティを改善する

多くの問題は、ロギングと監視が不十分なために起こります。システムのオブザーバビリティを改善するには、次のようなことが含まれます。

ログレベルを伴う構造化ロギングlagerlogger)を使います。

logger:log(info, "User logged in: ~p", [UserId]).

リアルタイムの監視を実装します。

recon:bin_leak(10). % メモリリークの可能性を検出する。

ロギングを改善しておくと、異常が大きな障害へとエスカレートする前に検知できます。

3. コードの可読性と保守性を高める

よく構造化されたコードはデバッグしやすくなります。Erlangのベストプラクティスに従うことで保守性が高まります。

  • わかりやすい関数名を使うdo_it/1ではなくhandle_request/1のように)。
  • OTPの設計原則gen_serversupervisor)に従う。
  • デバッグをしやすくするためにモジュール化されたコードを書く。

例: 複雑にネストしたcase文の代わりに次のように書きます。

case Result of
    {ok, Data} -> process(Data);
    {error, _} -> handle_error()
end.

明快さのためにパターンマッチを使います。

process_request({ok, Data}) -> process(Data);
process_request({error, _}) -> handle_error().

4. フェイルファストの仕組みを実装する

ErlangのLet It Crashという思想は、エラーが起きたときに不正な状態を伝播させるのではなく、プロセスをすぐに失敗させるべきだという考え方です。

例: ガード節でフェイルファストの挙動を強制します。

handle_request({ok, Data}) when is_list(Data) ->
    process(Data);
handle_request(_) ->
    exit(bad_request).

フェイルファストの仕組みは、静かな失敗を防ぎ、デバッグをしやすくします。

5. オープンソースのErlangシステムから学ぶ

本番運用されている多くのErlangアプリケーションはオープンソースです。それらのデバッグの実践を学ぶことは、貴重な知見をもたらします。

  • RabbitMQ: 構造化ロギングと監視ツールを使っています。
  • MongooseIM: 広範なトレーシングを実装しています。
  • Riak: 分散型の障害回復技法を採用しています。

これらのプロジェクトを調べることは、デバッグの技量を高めシステム設計への理解を深めることにつながります。

バグのよくある原因

ソフトウェアシステムでは、安定性や性能に影響する決まった種類の障害が繰り返し現れがちです。Erlangはフォールトトレランスを念頭に設計されていますが、それでも特定の種類のバグは頻繁に現れます。本節では、デッドロック、メールボックスのオーバーフロー、メモリの問題をはじめ、Erlangアプリケーションでよく見られる問題の原因を取り上げます。これらの問題を理解し、その診断と解決の方法を学ぶことは、より信頼性が高く効率的なErlangプログラムを書く助けになります。

デッドロック

デッドロックは、二つ以上のプロセスが互いにリソースの解放を待ち合い、どちらも先へ進めなくなる状態です。これは、Erlangの軽量プロセスで構築されたシステムを含め、並行システムでよく起こる問題です。

Erlangにおけるデッドロックは、主に次の原因で発生します。

  • 循環的な依存関係: 二つのプロセスが、互いに相手が保持するリソースを待つ状態。
  • ロックの誤用: gen_servergen_fsmを使う際、メッセージ処理の順序を誤るとデッドロックにつながります。
  • gen_server内でのブロッキング呼び出し: handle_call/3コールバックの中でgen_server:call/2を呼び出すと、プロセスが無期限にブロックされることがあります。

デッドロックを特定するには次のようにします。

  • プロセスの調査: observer:start().process_info(Pid, status).を使って、止まっているプロセスがないか確認します。
  • dbgによるトレーシング: 関数呼び出しのトレーシングを有効にし、プロセスがどこで無期限に待ち続けているかを調べます。
  • メッセージキューの分析: プロセスが決して届かないメッセージを待っている場合、process_info(Pid, messages).でそのメールボックスを確認します。

ブロッキング操作にはタイムアウトを設定します。

gen_server:call(Server, Request, Timeout).

適切なタイムアウトを設定しておけば、無期限のブロックを防げます。

ブロックを避けるには、非同期呼び出し(gen_server:cast/2)や監視メッセージ(erlang:monitor/2)を使います。

循環的な依存関係を防ぐため、すべてのロックをプロセス間で一貫した順序で取得するようにします。

プロセスの状態を監視し、デッドロックしたプロセスを強制的に再起動する定期チェックを実装します。

メールボックスのオーバーフロー

Erlangのメッセージパッシングモデルでは、プロセスはメールボックスを介して非同期にメッセージを受け取れます。しかし、プロセスが処理できる速度を上回ってメッセージが蓄積すると、メールボックスは際限なく膨らみ、メモリ消費の増大やクラッシュにつながります。

メッセージのオーバーフローには、よくある原因と兆候があります。

  • メッセージ処理の遅さ: リクエストの処理に時間がかかりすぎるgen_serverでは、未処理のメッセージが積み上がります。
  • メッセージの過剰な生成: バックプレッシャーを確認せずに頻繁にメッセージを送るプロセス。
  • システムメッセージの未処理: gen_server:handle_info/2のようなシステムメッセージの処理漏れ。

兆候としては次のようなものがあります。

  • メモリ使用量の増加(process_info(Pid, memory).
  • プロセスのメッセージキューが長くなる(process_info(Pid, message_queue_len).
  • アイドル状態に見えるが実は過負荷になっている、応答しないプロセス

メールボックスのオーバーフローを防ぎ解消する

メッセージキューの長さを監視します

process_info(Pid, message_queue_len).

キューがしきい値を超えて伸びたときにアラートを発する監視ツールを使います。

送信側のレート制限

  • 追加のメッセージを送る前に明示的な確認応答を求めるなど、バックプレッシャーの仕組みを使います。
  • フロー制御を実装します。メッセージを無条件に送るのではなく、送信側が受信側の負荷を確認できるようにします。

選択的受信を適切に使う

次のようなパターンは避けます。

receive {specific_message, Data} -> process(Data) end.

このパターンは他の保留中のメッセージを無視してしまうため、メールボックスが際限なく膨らむ原因になります。ただし、rpc型の送受信でRefトリックを使う場合はこの限りではありません。詳しくはRefトリックを参照してください。

重い計算処理をオフロードする:

  • メインのプロセスループの中で行うのではなく、負荷の高い処理はワーカープロセスにオフロードします。
  • 処理を終えたあとに非同期でメッセージへ応答するには**gen_server:reply/2**を使います。

メモリの問題

Erlangのメモリモデルは、プロセスごとのヒープ、ガベージコレクション、バイナリアロケータに基づいています。効率を重視して設計されていますが、メモリの使い方を誤ると性能低下につながります。

Erlangにおけるメモリリークは、主に次のようなことに起因します。

  • 状態を蓄積し続ける長命なプロセス: ETSテーブル、大きなリスト、未処理のメッセージなど。
  • 無制限に膨らむメッセージキュー: メッセージを受け取るだけで消費しないプロセス。
  • バイナリデータの蓄積: 大きなバイナリはメモリの断片化を引き起こすことがあります。

メモリリークの検出方法

個々のプロセスのメモリ使用量を確認します。

process_info(Pid, memory).

observer:start().を使い、「Processes」タブで過剰にメモリを消費しているプロセスを確認します。

次のコマンドでメモリ割り当てのトレーシングを有効にします。

recon_alloc:memory(ets).

バイナリのメモリ使用量を管理する

大きなバイナリは、参照カウントの仕組みによってプロセスヒープとは別に管理されます。次のような場合に問題が生じます。

  • プロセスが必要以上に長くバイナリへの参照を保持し続ける場合。
  • ガベージコレクションの遅延により、使われなくなった大きなバイナリが残り続ける場合。

解決策:

大きなバイナリを小さなチャンクに変換します

binary:split(BigBinary, <<"\n">>).

ガベージコレクションを強制的に実行します

erlang:garbage_collect(Pid).

これにより、プロセスがもう参照していないバイナリが使っていたメモリを回収できます。バイナリをもう使っていないのに参照だけを保持し続けているプロセスをリレーする場合、これは重要な意味を持ちます。バイナリは参照カウント方式で管理され、プロセスをまたいで存在し続けることを覚えておいてください。

バイナリのメモリ割り当てを監視します

erlang:memory(binary).

Erlangシステムのメモリ使用量を最適化する

Erlangには、ヒープの割り当て挙動を制御するいくつかのシステムフラグが用意されています。

min_heap_size(プロセスヒープの最小サイズ)

  • 新しく生成されたプロセスの初期ヒープサイズを定めます。
  • 大量のデータを扱うことが見込まれるプロセスで、ヒープの拡張が頻繁に起きるのを避けるのに役立ちます。
  • デフォルトは通常233ワードですが、急速に大きくなるプロセスでは、これを少し(例えば256512に)増やすことで性能が改善することがあります。

使用例: 次のようにして、特定のプロセスに対してこの設定を行えます。

spawn_opt(fun() -> my_function() end, [{min_heap_size, 512}]).

あるいは次のようにしてグローバルに適用できます。

erl +hms 512

これにより、すべての新しいプロセスが少なくとも512ワードのヒープを持って開始するようになり、頻繁なヒープ拡張の必要性が減ります。

min_bin_vheap_size(バイナリ用仮想ヒープの最小サイズ)

  • 参照カウントされるバイナリ(64バイトを超えるバイナリ)の仮想ヒープサイズを制御します。
  • 大きなバイナリデータを扱うプロセスのメモリ割り当てを最適化するのに役立ちます。
  • デフォルトは46422ですが、バイナリを多用するワークロードでは、512以上に調整するとよいでしょう。
spawn_opt(fun() -> handle_large_binaries() end, [{min_bin_vheap_size, 100000}]).

これにより、プロセスが十分なバイナリヒープ領域を持って開始し、頻繁な再割り当てを防げます。

フルスイープガベージコレクションのしきい値(fullsweep_after)を最適化します。

ETSを効率的に使います。

  • メモリの肥大化を避けるため、使われなくなったエントリを定期的に片付けます。
  • 必要がない限り、**bagordered_setよりset**テーブルを選びます。

長命で共有される大きな項をプロセス間で渡さないよう注意します。大きな項をプロセス間で送る代わりに参照を使います(例えば、大きなデータはETSやデータベースに保存し、参照だけを送ります)。

Let It Crash原則

Erlangの**「Let It Crash」**原則は、フォールトトレラントでレジリエントなシステムを設計するうえでの基本的な思想です。あらゆるエラーに対処する防御的なコードを書く代わりに、Erlangの開発者は失敗を受け入れ、スーパバイザツリーにクラッシュの検知と回復を委ねます。このアプローチによってコードが単純になり、保守性が高まり、予期しないエラーに直面してもシステムが堅牢であり続けられます。

概要と根拠

従来のプログラミングでは、エラー処理のためにおびただしいtry-catch文と、失敗を見越した防御的なコードを書くことが一般的でした。しかしこのアプローチは複雑さを持ち込み、保守しづらいコードベースにつながりがちです。Erlangは、失敗は起こるものだと受け入れ、網羅的なエラー防止よりも自動的な回復に力点を置くという、異なるアプローチを取ります。

「Let It Crash」の根拠は次のとおりです。

  • 障害の分離: Erlangの各プロセスは独立して動作するため、一つのプロセスのクラッシュが他に影響しません。
  • 自動的な回復: スーパバイザがプロセスを監視し、失敗したときに再起動します。
  • コードの単純化: 開発者は防御的なコードを減らし、エラー処理よりビジネスロジックに集中できます。
  • 障害の封じ込め: プロセスを制御された形でクラッシュさせて再起動することで、エラーの拡散を防ぎます。

この思想により、Erlangのシステムは、障害が避けられない分散環境において特に高いレジリエンスを発揮します。

Erlangにおける例外

Erlangには例外処理のための組み込みの仕組みがありますが、あらゆるエラーをローカルで回復することに注力するのではなく、スーパビジョンを通じたプロセスの終了と再起動を推奨しています。

例外の種類

Erlangには三種類の例外があります。

  • エラーerror:Reason): ゼロ除算や未定義関数の呼び出しといった重大な不具合によって発生します。
  • スローthrow:Reason): 非局所的なリターンや制御されたexitのために使われます。
  • exitexit:Reason): プロセスが予期せず、あるいは意図的に終了したときに発生します。

例外処理の例

防御的なプログラミングはクラッシュを避けようとしますが、必要であればErlangでも例外を明示的に処理できます。

try 1 / 0 of
    Result -> io:format("Result: ~p~n", [Result])
catch
    error:badarith -> io:format("Cannot divide by zero!~n")
end.

これは、その場での処理が必要な場合に役立ちますが、Erlangにおけるほとんどの失敗はクラッシュに任せ、スーパバイザに処理させます。

プロセスの終了と監視

プロセスがクラッシュすると、リンクされたプロセスにexitシグナルが送られます。必要であれば、これらのexitを監視したりトラップしたりできます。

spawn_monitor(fun() -> exit(died) end).

これにより、別のプロセスが障害を検知し、それに応じて対応できるようになります。

スーパバイザツリーによるシステム設計

すべての関数の中でエラーを処理するのではなく、Erlangアプリケーションはスーパバイザツリー、つまりスーパバイザがワーカープロセスを監視し、失敗したときに再起動する階層構造に頼ります。

スーパバイザツリーの構造

スーパバイザツリーは次の要素からなります。

  • スーパバイザ: ワーカープロセスや他のスーパバイザを管理する特別なプロセス。
  • ワーカー: 実際の計算を行うプロセス。クラッシュした場合、どう再起動するかはスーパバイザが決めます。
-module(my_supervisor).
-behaviour(supervisor).

-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link(?MODULE, []).

init([]) ->
    {ok, {{one_for_one, 3, 10},
          [{worker1, {my_worker, start_link, []}, permanent, 5000, worker, [my_worker]}]}}.

このスーパバイザは、my_workerがクラッシュした場合に自動的に再起動されることを保証します。

スーパービジョン戦略

スーパバイザは、いくつかの異なる再起動戦略に従えます。

  • one_for_one: クラッシュしたプロセスだけを再起動します(もっとも一般的)。
  • one_for_all: 一つが失敗したら、すべての子プロセスを再起動します。
  • rest_for_one: 失敗したプロセスと、それより後に起動されたすべてのプロセスを再起動します。
  • simple_one_for_one: 同種のワーカープロセスを動的に生成する場合に使います。

スーパバイザツリーを使う利点

  • 自動的な障害回復: ワーカーがクラッシュしても、手作業を介さずに再起動されます。
  • スケーラビリティ: スーパバイザは何千ものプロセスを効率的に管理できます。
  • 関心の分離: ビジネスロジックはワーカーに留まり、障害回復は別で扱われます。

デバッグツールと技法

Erlangアプリケーションで予期しない挙動に対処するには、デバッグが欠かせません。Erlangのエコシステムには、いくつものツールが存在します。

Erlangデバッガ(dbg

dbgモジュールは、稼働中のシステムを性能への影響を最小限に抑えながらデバッグするための、強力なトレーシング機能を提供します。

dbgを使い始める

dbgツールを開始するには次のようにします。

1> dbg:tracer().
{ok,<0.85.0>}

これにより、デバッグ情報を収集するトレーサープロセスが設定されます。出力先には異なるバックエンドを選べます。

  • dbg:tracer(console). : シェルに出力する
  • dbg:tracer(port, file:open("trace.log", [write])). : ファイルに書き出す

トレーシングを有効にしたら、プロセスや関数にトレーサーを結びつけられます。

すべての関数呼び出しをトレースします。

dbg:p(all, c). % すべてのプロセスにおけるすべての関数呼び出しをトレースする

特定の関数をトレースします。

dbg:tpl(my_module, my_function, []). % my_function/0への呼び出しをトレースする

条件付きトレースを設定します。

関数の引数が一致したときだけトレースします。

dbg:tpl(my_module, my_function, [{'_', [], [{message, "Function called"}]}]).

コード実行を一行ずつ追うにはブレークポイントが役立ちます。グラフィカルデバッガを起動します。

debugger:start().

続いて、モジュールにブレークポイントを設定します。

int:break(my_module, my_function, Arity).

関数がトレースされると、呼び出しと戻り値が記録されます。

トレース出力の例:

(<0.85.0>) call my_module:my_function(42)
(<0.85.0>) returned from my_function -> "Result: 42"

これにより、実行を通して値がどう変化していくかを追跡できます。

次世代デバッガEDB

Erlang Debugger(EDB)は、Erlangアプリケーション向けの現代的で機能豊富なデバッガです。ブレークポイントの設定、変数の調査、コード実行のステップ実行のための言語サーバーインタフェースを提供します。詳しくはhttps://github.com/WhatsApp/edbを参照してください。

OTP 28より前のバージョンでEDBを使うには、EDBサポート付きでErlangをソースからビルドする必要があります。EDBサポート付きでErlangをソースからビルドする手順は次のとおりです。

git clone https://github.com/WhatsApp/edb.git
git submodule update --init
pushd otp
./configure --prefix $(pwd)/../otp-bin
make -j$(nproc)
make -j$(nproc) install
popd
rebar3 escriptize

これでEDBを次のように起動できます。

_build/default/bin/edb dap

このコマンドはEDBを起動し、DAPを通じて開発環境とやり取りできるようにし、しっかりとしたデバッグ体験を提供します。

現状と安定性

執筆時点で、EDBは登場したばかりで急速に進化しているツールです。統合と使いやすさは有望ではあるものの、特にIDEのセットアップ、ノードへの接続、実行環境の互換性まわりでは、まだ難しい場面があります。安定性は、OTPのバージョンや開発ツールの選択によって大きく変わることがあります。

Erlangのクラッシュダンプ

クラッシュダンプは、Erlangシステムの障害を診断するための情報を提供します。クラッシュ時点でのシステムの状態、メモリ使用量、プロセス情報、コールスタックについての詳細を含んでいます。これらのファイルの読み方を理解しておくと、デバッグを大きく速め、将来のクラッシュを防ぐことにつながります。

クラッシュダンプの読み方

クラッシュダンプ(erl_crash.dump)は、異常終了時点でのErlangランタイムシステム(ERTS)のスナップショットです。次のものが含まれます。

  • システムバージョンとランタイムパラメータ
  • メモリ使用量の統計
  • ロード済みモジュール
  • プロセスの状態とコールスタック
  • ポートとドライバの情報

クラッシュダンプを分析することで、システムがなぜクラッシュしたのか、メモリ枯渇、無限ループ、デッドロック、その他の障害のどれが原因なのかを突き止められます。

クラッシュダンプの形式については、公式ドキュメントHow to Interpret the Erlang Crash Dumpsに詳しい説明があります。ここでは基本だけを扱います。

デフォルトでは、クラッシュダンプはErlangシステムを起動した作業ディレクトリに保存されます。ファイル名は通常次のとおりです。

erl_crash.dump

環境変数を設定することで場所を変更できます。

export ERL_CRASH_DUMP=/var/log/erl_crash.dump

あるいは実行時に次のようにもできます。

erlang:system_flag(crash_dump, "/var/log/erl_crash.dump").

クラッシュダンプの基本構造

クラッシュダンプは複数のセクションからなります。以下は一部を省略した例です。

=erl_crash_dump:0.5
Sun Feb 18 13:45:52 2025
Slogan: eheap_alloc: Cannot allocate 1048576 bytes of memory (of type "heap").
System version: Erlang/OTP 26 [erts-13.1] [source] [64-bit]
Compiled: Fri Jan 26 14:10:07 2025
Taints: none
Atoms: 18423
Processes: 482
Memory: 2147483648
=memory
total: 2147483648
processes: 1807483648
ets: 107374182
binary: 32212254
code: 5242880

このダンプは、メモリ割り当ての失敗(Cannot allocate 1048576 bytes of memory)によってシステムがクラッシュしたことを示しています。

クラッシュダンプの主要セクション

  1. Slogan

    クラッシュの理由を示します。よくあるsloganには次のようなものがあります。

    • eheap_alloc: Cannot allocate X bytes of memory(メモリ枯渇)
    • Init terminating in do_boot ()(起動スクリプトのエラーの可能性)
    • Could not start kernel pid(設定の引数が誤っている可能性)
  2. システム情報

    ランタイムに関する詳細を含みます。

    • System version: Erlang/OTPのバージョンとビルドの詳細
    • Compiled: システムがビルドされた日時
    • Taints: 外部のネイティブコード(NIF)が動いているかどうか
  3. メモリ使用量

    メモリの内訳を表示します。

    • Total: メモリ使用量の合計
    • Processes: プロセスが使用しているメモリ(値が大きい場合はメモリリークの可能性)
    • ETS: Erlang Term Storageの使用量(制御なく増え続ける場合は問題になり得ます)
    • Binary: バイナリに割り当てられたメモリ(リークの原因になり得ます)
    • Code: ロード済みコードのメモリ使用量
  4. プロセスリスト

    稼働中のプロセスに関する詳細を提供します。このセクションは次のような問題の特定に欠かせません。

    • 過剰にメモリを消費しているプロセス(Stack+Heapのサイズ)
    • 無限ループに陥っているプロセス(Reductionsの値が異常に高い)
    • メッセージキューの過負荷(Messagesフィールドが際限なく増えている)
  5. ポートとドライバ

    開いているポートとドライバの一覧です。外部システムとのやり取り(ファイル、ソケット、データベース)がクラッシュの原因として疑われる場合に役立ちます。

  6. ロード済みモジュール

    動的にロードされたコード(code:load_file/1経由など)がクラッシュの原因かどうかを判断するのに役立ちます。

クラッシュダンプの分析

Erlangには、クラッシュダンプを解析するための組み込みツールcrashdump_viewerが用意されています。

起動するには次のようにします。

crashdump_viewer:start().

これにより、クラッシュダンプを調べるためのグラフィカルなインタフェースが提供されます。

クラッシュダンプが生成されない原因を調べる

システムがクラッシュしても、erl_crash.dumpファイルが生成されないことがあります。その理由と対処法を見ていきます。

クラッシュダンプが無効になっている場合

Erlangでは次のようにしてクラッシュダンプの有効・無効を切り替えられます。

erlang:system_flag(dump_on_exit, true).

有効になっていることを確認します。

ERL_CRASH_DUMP=/var/log/erl_crash.dump

またはsys.config経由で次のようにします。

[{kernel, [{error_logger, {file, "/var/log/erl_crash.dump"}}]}].

権限不足の場合

Erlangを実行しているプロセスが、目的のダンプディレクトリへの書き込み権限を持っていることを確認します。

sudo chmod 777 /var/log/erl_crash.dump

所有権を確認します。

ls -l /var/log/erl_crash.dump

必要であれば所有者を変更します。

sudo chown erlang_user /var/log/erl_crash.dump

ダンプ書き込み前にクラッシュする場合

ダンプを書き込む前にシステムがメモリ不足に陥る場合は、メモリを予約しておく必要があるかもしれません。

erlang:system_flag(reserved_memory, 1000000).

あるいはスワップ領域を増やします。

システム全体の制限

LinuxやmacOSのシステム上限が、ダンプの生成を妨げていることがあります。確認します。

ulimit -a

core file size0になっている場合は、有効にします。

ulimit -c unlimited

macOSでは次のようにします。

sudo launchctl limit core unlimited

NIF内でのクラッシュ

Native Implemented Function(NIF)がクラッシュした場合、Erlangはそれをうまく処理できないことがあります。そのような場合、gdbのようなデバッガの下でBEAMエミュレータを実行すると、クラッシュ時点でのシステムの状態を調べる助けになります。

ランタイムシステムのデバッグ

Erlangランタイムシステム(ERTS)の内部で起きている問題を理解し診断するのは、その複雑さゆえに難しい作業です。しかし、GNU Debugger(GDB)のようなツールを活用することで、この作業を大きく助けられます。本節では、環境の準備やGDBマクロの活用によってデバッグの流れをスムーズにする方法も含め、GDBを使ってBEAMをデバッグする方法の概要を示します。

GDBの利用

GDBは、コンパイル済みプログラムの実行を機械語レベルでデバッグするための強力なツールです。BEAMに適用すると、GDBを使って、実行中またはクラッシュ後のErlang仮想マシンの状態を調べられます。

BEAMに対してGDBを効果的に使うには、Erlangランタイムシステムをデバッグシンボル付きでコンパイルしておくと便利です。この方法でコンパイルすると、デバッグセッション中に詳細な情報が得られます。

デバッグ情報付きのErlangをコンパイルして実行する手順については、ERTSのビルドの章を参照してください。

ビルドが終わったら、cerl起動スクリプトを使って、ビルドディレクトリからデバッグ版のBEAMを実行できます。

bin/cerl -debug

GDBでErlangを実行するもっとも簡単な方法は、cerl-rgdbフラグを使うことです。その際、デバッグ版も選択するようにします。

bin/cerl -rgdb -debug

GDBのプロンプトに入ったら、ブレークポイントなどを設定し、準備ができたら次のようにしてBEAMの実行を開始します。

(gdb) run

クラッシュした実行のOSコアダンプがある場合(Erlangのクラッシュダンプとは別物です。Erlangのクラッシュダンプを参照してください)、代わりに-rcoreフラグを付けてcerlを実行し、GDBを起動できます。

bin/cerl -rcore <core file>
Emacsエディタに慣れている場合は、cerl-gdb-coreフラグ(先頭にrを付けない)を使うと、デバッグセッション用のIDEとして動作するEmacsインスタンスを起動できます。(先にEMACS=emacsclientを設定しておけば、M-x server-startを実行済みの既存のEmacsの中で動かすこともできます。)詳しくはEmacs GDBのドキュメントを参照してください。

すでに動いているBEAMにGDBをアタッチしたい場合は、そのOSプロセスIDを調べる必要があります。例えば、別のシェルウィンドウで次のように入力します。

pgrep -l beam

これは次のような結果を表示するはずです。

3140019 beam.debug.smp

そして、実際に使われている実行ファイルのパスとプロセスIDを指定して、次のようにGDBを起動できます。

gdb bin/x86_64-unknown-linux-gnu/beam.debug.smp 3140019

bin/cerlbin/erlのパスをGDBに指定することはできない点に注意してください。これらは単に、BEAM実行ファイルのために適切な環境変数を設定するシェルスクリプトにすぎません。)

自分が所有しているプロセスであっても、OSがデフォルトでアタッチを制限している場合があります。これを再設定する方法は本書の範囲外です。

GDBマクロの利用

GDBマクロは、繰り返し行う作業を自動化し、複雑なコマンドのショートカットを提供することで、デバッグセッションの効率を高めます。ErlangランタイムにはEmulator Toolbox for Pathologists(ETP)と呼ばれる、あらかじめ定義されたGDBマクロ一式が含まれており、内部のBEAM構造、プロセスの状態、メモリ割り当て、スケジューリング情報といったBEAMのさまざまな側面を調べるのに役立ちます。

ETPマクロはerts/etc/unix/etp-commandsにあります。前節で説明したcerlスクリプト経由でGDBセッションを起動すると、これらは自動的に読み込まれます。すると、etp-process-infoのようなマクロを使って、特定のErlangプロセスに関する詳細な情報を取得できるようになります。

etp-process-info <process_pointer>

<process_pointer>は、調べたいプロセス制御ブロック(PCB)への実際のポインタに置き換えてください。これらのマクロを使うと、BEAMの内部構造から意味のあるデータを取り出す作業が簡単になります。

GDBを使ったBEAMのデバッグとこれらのマクロの活用について包括的に知りたい場合は、Debugging the BEAMDebug emulatorのドキュメントを参照してください。これらの資料には、Erlangランタイムシステム内の問題を効果的に診断し解決するための、詳しい手順と例が載っています。

SystemTapとDTrace

SystemTapとDTraceは、アプリケーションのコードを変更することなく、リアルタイムでシステムの挙動を分析・監視できる強力な動的トレーシングフレームワークです。これらのツールは、性能上のボトルネックの調査、問題のデバッグ、低レベルでのシステムの動作の理解に特に役立ちます。両者は似た目的を持ちますが、対象とするOSは異なります。SystemTapは主にLinuxで使われ、DTraceは主にSolaris、macOS、BSD系で使われます。

これらのツールをErlangと組み合わせて使うと、BEAM仮想マシンの挙動、プロセスのスケジューリング、ガベージコレクション、プロセス間通信について深い知見が得られます。

SystemTapとDTraceの概要

SystemTapとDTraceは、動的に生成したプローブを、稼働中のカーネルやユーザー空間のアプリケーションに挿入することで動作します。これらのプローブはリアルタイムのデータを捕捉し、開発者はアプリケーションを止めたり変更したりすることなく、プログラムの実行を調査・分析できます。

  • SystemTap: Linux向けに開発されたSystemTapは、スクリプトを使ってカーネルイベント、ユーザー空間のプログラム、ランタイムの挙動を監視できます。プロファイリング、障害検知、システムの内部調査によく使われます。
  • DTrace: もともとSun MicrosystemsがSolaris向けに開発したDTraceは、堅牢なスクリプト言語を備えた同様のトレーシング機能を提供します。macOS、FreeBSD、SmartOSで広く使われています。

どちらのツールも、関数の実行時間の計測、システムコールのトレース、メモリ使用量の調査、性能の最適化や複雑なアプリケーションのデバッグに欠かせないイベントベースのデータの捕捉を可能にします。

ErlangでSystemTapとDTraceを使う

ErlangでSystemTapやDTraceを使うには、BEAMランタイムシステムで必要なトレーシングサポートを有効にする必要があります。これにより、関数呼び出し、メッセージパッシング、ガベージコレクション、スケジューリングのイベントを監視するためのプローブを仮想マシンに挿入できるようになります。

ErlangでSystemTapを使う

SystemTapのスクリプトは、BEAMエミュレータに埋め込まれたユーザー空間のマーカーに依存します。これらのマーカーによって、SystemTapは内部のさまざまなイベントにフックできます。ErlangでSystemTapを使うには次のようにします。

SystemTapがインストールされていることを確認します(Ubuntu、Fedora、CentOSといったLinuxディストリビューションの場合)。

sudo apt-get install systemtap systemtap-sdt-dev

あるいは

sudo dnf install systemtap systemtap-devel

ErlangのSystemTapプローブを有効にします。BEAM VMはSystemTapに対応していますが、--enable-systemtap付きでコンパイルする必要があります。

./configure --enable-systemtap
make

利用可能なプローブを一覧表示します。BEAMランタイムでどのプローブが利用できるか確認するには次のようにします。

stap -L 'process("*beam.smp").mark("*")'

SystemTapのスクリプトを書きます。次の例は、BEAM VM内の関数呼び出しをトレースします。

probe process("beam.smp").mark("function_entry") {
    printf("Function call in BEAM: %s\n", user_string($arg1))
}

スクリプトを実行します。トレーシングを開始するにはスクリプトを実行します。

sudo stap my_script.stp

これにより、開発者は関数呼び出しを観察し、ボトルネックを検出し、性能の問題をリアルタイムでデバッグできます。

ErlangでDTraceを使う

DTraceはBEAMランタイムに直接組み込まれ、システムの動作を深く可視化します。関数呼び出し、メモリ割り当て、ガベージコレクション、プロセス間通信のトレースができます。

DTraceはSolarisでもっともよく機能します。SystemTapに同梱されたLinux版もありますが、Solaris版ほど強力ではありません。

macOSにはDTraceがあらかじめインストールされています。Ubuntuでは次のようにインストールできます。

sudo apt-get install systemtap-sdt-dev

BEAM VMには組み込みのDTraceサポートがあります。必要であれば、DTraceサポート付きでErlangを再ビルドします。

./configure --with-dtrace
make

簡単なDTraceスクリプトを書きます。次のスクリプトはErlangの関数呼び出しをトレースします。

syscall::write:entry
/execname == "beam.smp"/ {
    printf("Erlang process writing output\n");
}

スクリプトを実行します。トレーシングを開始するにはDTraceを実行します。

sudo dtrace -s my_script.d

これにより、BEAM仮想マシンの内部の挙動を、非侵襲的な形でリアルタイムに監視できます。