Title: STUFF GOES BAD: ERLANG IN ANGER
AUTHOR: Fred Hebert and Heroku
Link: https://s3.amazonaws.com/erlang-in-anger/text.v1.0.1.pdf
License: a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Title: STUFF GOES BAD: ERLANG IN ANGER
AUTHOR: Fred Hebert and Heroku
Link: https://s3.amazonaws.com/erlang-in-anger/text.v1.0.1.pdf
License: a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Erlangは他のプログラミング言語に比べてエラー処理のアプローチが特別です。 共通的にはプログラム、プログラム環境、方法論でエラーをさけるようにします。 もしランタイムでエラーが起きたときは何が必要か考えます。
環境にデプロイしたあとはもしエラーがあれば新しいバージョンをデプロイする必要もあるでしょう。
一方でErlangは開発、運用、ハードウェアといったいかなるレイヤーでの問題がおきうるように 設計されています。それはシステム上のいかなるエラーをも取り除けるということです。 もしあなたが全てのエラーを事前にさけるのではなく、エラーを取り扱えるようになれるであれば、 それは予期していない振る舞いについてそういったアプローチがとれるということになります。
これはクラッシュさせようという考え方からきています。 なぜなら全てのエラーを事前に防ぐというのは非常にコストが高いです。また、エラーについては プログラマーが対処方法を知っている必要があり、それ以外はプロセスを終わらせるかVMをそのままにしておくことになります。
ほとんどのバグは処理を再起動させていい状態に戻せます。 Erlangは人間の免疫システムと等価な環境を提供します。他の言語はどうやって体の中に病原菌を持ち込まないかを意識します。 どちらの形式も非常に重要です。ほとんどの環境にさまざまな健康状態を保つための機能を提供します。 他の環境はランタイム時にエラーチェックを行いますが、それ以降は免疫システムのようなものは提供しません。 システムが最初何か悪い状態になったとき崩壊しないようにErlang/OTPは医者になることができます。 あなたはシステムの中に入り、本番環境でどのようなことが起きているか注意深く見ることができ、修正を試みることもできます。 Erlangは患者がそこにいることを必要とせず、活動も中断しないようにしながらテストを拡大して様々な診断を行うことができます。
この本はErlangを利用して戦時中にどうやって治療するかアドバイスできるようにします。 Erlangを利用したデバッグの方法や失敗がどこからきているか理解するための方法について記載します。
これは初心者向けの本ではありません。チュートリアルやトレーニングセッションではなく、実際に本番環境にある 診断やデバッグ環境に関するものです。ガイドラインを終えて、そこから先踏み込んだ世界に入るための暗黙的なフェーズです。 この本を読む人はErlangとOTPに精通している人かとおもいます。 Erlang/OTPについては私が理解しているようにお話ししますが困惑した場合は読者の人は自分で調べることができると思っています。 既にソフトウェアのデバッグ方法について理解してる人はこの本を読む必要はないかとおもいます。
この本は大きく2つの章に分かれています。 1つ目の大きな話のテーマはどのようにアプリケーションを書くかです。 コードベースを1章で説明して、2章で一般的なノウハウを説明します。3章についてはシステムデザインについて説明します。 2つ目の大きな話のテーマは診断についてです。4章でどのようにノードにつなげるかについてはなします。 5章では基本的な診断方法についてはなします。6章でクラッシュダンプについて話して7章はメモリリークについてはなします。 8章はCPUについてはなします。 最終章ではreconを利用して本番環境でシステムダウンする前にどのようにシステムコールを追跡するかについてはなします。
どの章にも理解を助けるための質問かハンズオンを用意しています。
”ソースを読むこと”がよくいわれる厄介な方法ですがErlangプログラマにとってはそれはよく行うべきことになります。 ライブラリのドキュメントも完全はななかったり、古かったり完璧ではありません。 ErlangプログラマはどちらかというとLisperに似ていて自分の問題を解決するのが最優先で、 テストやその他事情を解決するという傾向があまりありません。 あなたはコードを読んでなにをしているのかを理解し、自分でコードを修正して自分のシステムで利用できるようにする必要があります。 他のどの言語もあなたが設計していない限り似たような問題は存在するでしょう。 Erlangには大きく分けて3つのタイプがあります。一つはErlangのベースコード、もう一つはOTPアプリケーション、 そして最後はOTPそのものになります。本章ではそれらをどう読み込んでいくか見ていきます。
もしErlangのコードを書くとしたら、ほとんどは自分で書くことになるかとおもいます。 特定の標準方法に準拠しているのはまれなので、ほとんどは自分で中身を見ていくことになります。 README.mdを見るとアプリケーションのポイントは著者へのアクセス方法が書いていたりします。 幸運にも、初心者や素晴らしいErlangプログラマによってプロジェクトはできているため、 滅多に自分で1から書く必要はなくなっています。一般的にはrebarというツールを利用してOTPアプリケーションを記述します。
OTPアプリケーションの構成はシンプルです。一般的には以下のようなディレクトリ構成を持ちます。
doc/
ebin/
src/
test/
LICENSE.txt
README.md
rebar.config
微妙に違いますが、構成はほとんど同じです。 それぞれのOTPアプリケーションはebin/{AppFile}.appもしくはsrc/{AppFile}.app.srcがそれぞれ含まれています。 2つのファイルは以下のように動作します。
{application, useragent, [
{description, "Identify browsers & OSes from useragent strings"},
{vsn, "0.1.2"},
{registered, []},
{applications, [kernel, stdlib]},
{modules, [useragent]}
]}.
{application, dispcount, [
{description, "A dispatching library for resources and task "
"limiting based on shared counters"},
{vsn, "1.0.0"},
{applications, [kernel, stdlib]},
{registered, []},
{mod, {dispcount, []}},
{modules, [dispcount, dispcount_serv, dispcount_sup,
dispcount_supersup, dispcount_watcher, watchers_sup]}
]}.
最初のケースはライブラリアプリケーションで、2つ目のケースは普通のアプリケーションです。
ライブラリアプリケーションは普通appname_somethingをappnameというモジュールで構成されます。 これは一般的にインタフェースモジュールなのでどうやってアクセスできるか見るのに適しています。 ソースを読むとどうやって動作するか記述されています。もしgen_serverやgen_fsm等があれば、 そこからどのようにsupervisorが動作するかわかります。 もしbehaivorが記載されていなければあなたの手で利用可能なステートレスなライブラリです。 この例では自分でエクスポートして利用可能になるので早い理解が可能になるでしょう。
普通のアプリケーションでは以下に二つポイントがあります。
最初のファイルにはどのようなライブラリをもっているかについて記述し、 次のファイルにはトッププロセスがどのような振る舞いをするのか記述します。 たまに最初のファイルで二つの役割を両方記述するときもあります。
もしあなたがアプリケーションにライブラリを追加するだけの簡単な変更であれば、 appnameを編集するだけです。もしアプリケーションを修正するのであれば、 appname_appを編集する必要があります。
アプリケーションはトップレベルでsupervisorを起動し、pidを返却します。 supervisorには今後起動するchild processについても特徴を記述しておきます。 ツリー構造上でより高い位置にあるプロセスは基本的に生存確立が高くなります。 どのプロセスが重要かしていすることも可能です。supervisor内でプロセスを起動すると 既に開いているプロセスに依存して配置されます。 さらに依存関係を持ったプロセスはグループ化されなんにかエラーが起きたときは 一緒に落ちてくれます。これは慎重な選択でおかしくなった状態を復元するより 再起動したほうが懸命な判断だと考えられます。 supervisorの再起動戦略はsupervisorが持つプロセス間に依存します。
この構造はOTPアプリケーションでsupervisorがプロセスの扱いを意味します。 これらを監視するプロセスは以下のような役割を持つことができます。
これらのモジュールは同様の構成をとります。エクスポートされた公開関数やコールバック関数、 プライベート関数が利用できます。 これらのsupervisorの関係と種類をベースにして他のモジュールを利用したり、いろいろな実装が 記述されています。
全てのアプリケーションは依存関係をもっており、それらの依存関係はそれぞれ依存関係をもっています。 OTPアプリケーションは基本的にそれらの間で状態を持ちませんが、それらは開発者がapp fileに記述するという 正しいマナーによって保たれています。図1.1にOTPアプリケーションの構造を理解するのに役立つものをのせています。 このようなヒエラルキーを使ってどこに何がおかれているのか理解することができます。これに似たスクリプトとして、 reconを利用してescript script/app_deps.erlを活用します。似たような構成はobserveアプリケーションがありますが それぞれsupervisor構成をとっています。 これらを利用することでコードの理解を助けることになります。
OTPのリリースはそこらにあるOTPアプリケーションより理解するのは簡単です。 OTPリリースはリリース可能なOTPアプリケーションがパッケージ化されたもので、どのアプリでも application:start/2を呼ぶことで立ち上げとシャットダウンを可能にします。 前のOTPのバージョンでリリースしているアプリケーションも基本的には適用可能です。 通常これに設定ツールであるsystoolsやreltoolを利用してパッケージングを行います。 これらを理解するために、私はhttp://learnyousomeerlang.com/release-is-the-wordを読むのをおすすめします。 もし可能なら2014年の頭にリリースされたrelxを利用できると簡単です。
ほとんどのErlangの本はどのようにしてOTPアプリケーションを構築するかについて書かれています。 しかしいくつかの本はErlangコミュニティの話などがはいっています。それらは本質ではありません。 この章では簡単にErlangに特化した簡単なツアーを行うようにします。 OTPアプリケーションはErlangの人が記述するほとんどのオープンソースになっています。 事実、たくさんの人がOTPのリリースを必要としています。もしあなたが書いている何かが誰かのモジュールを 利用しているのであればそれはおそらくOTPアプリケーションでしょう。 あなたが構築したアプリケーションをでプロイするのであればきっとそれはOTPリリースを利用していると思います。
メインのビルドツールとしてrebarとerlang.mkをサポートしています。 フォーマットはErlangスクリプトでそれ自身や標準ライブラリをサポートしています。erlang.mkは 冗長ですが非常に高速にコンパイルされます。 この章ではrebarを前提とした解説をします。rebarがerlang.mkをサポートしていることについてもお話します。
OTPアプリケーションの構成はOTPリリースとは少し違います。 OTPアプリケーションは頂点に一つのsupervisorがいてその下にたくさんの依存関係が存在します。 OTPリリースは複数のOTPアプリケーションがあってそれぞれが依存していない可能性があります。
OTPアプリケーションの基本構成は前章でお話ししていたものと大体同じです。
1 doc/
2 deps/
3 ebin/
4 src/
5 test/
6 LICENSE.txt
7 README.md
8 rebar.config
新しいのはdeps/ですがこれはrebarによって自動的に生成されるものです。 これはErlangには標準的なパッケージ管理ツールがないためです。 人々は代わりにrebarを育ててプロジェクトを管理するようになりました。これはコンフリクトはなくなりましたが それぞれで依存性をダウンロードする必要があります。 これがrebar.configの例です。
1 {deps,
2 [{application_name, "1.0.*",
3 {git, "git://github.com/user/myapp.git", {branch,"master"}}},
4 {application_name, "2.0.1",
5 {git, "git://github.com/user/hisapp.git", {tag,"2.0.1"}}},
6 {application_name, "",
7 {git, "https://bitbucket.org/user/herapp.git", "7cd0aef4cd65"}},
8 {application_name, "my regex",
9 {hg, "https://bitbucket.org/user/theirapp.hg" {branch, "stable"}}}]}.
アプリケーションはgitから再帰的に取得されます。かれらは取得後コンパイルオプションによってコンパイルされます。 そのディレクトリの中でOTPアプリケーションの開発を行います。 それらをコンパイルするために、rebar get-deps compileを呼び出すとダウンロードしてアプリごとコンパイルします。 アプリケーションを世界に公開する際、依存性無しで配信することができます。 これによってあなたがしたように同じアプリケーションを構築できるし複数回出荷する必要もありません。 ビルドシステムは重複している部分を理解し、必要な処理を一回だけ呼び出すようにします。
リリースの場合、構成が若干かわります。リリースはアプリケーションの集合のため、 それが反映されます。トップレベルにアプリを配置しない代わりに、appsとdepsに ネストしたアプリを配置します。apps以下にアプリケーションのコードを配置して、 deps以下に依存関係を記述します。
apps/
doc/
deps/
LICENSE.txt
README.md
rebar.config
リリースに必要なものを生成します。SystoolとReltoolはこれらをカバーしており、ユーザを楽にさせます。 さらに最近はrelxを利用するのが一般的になっています。 relxを利用すると以下のようなファイルが生成されます。
1 {paths, ["apps", "deps"]}.
2 {include_erts, false}. % will use currently installed Erlang
3 {default_release, demo, "1.0.0"}.
4
5 {release, {demo, "1.0.0"},
6 [members,
7 feedstore,
8 ...
9 recon]}.
relxを呼ぶとリリース用のパッケージを_rel以下に配置します。 もしrebarが好きならrebar.configに以下のように追記しておくとより便利になります。
{post_hooks,[{compile, "./relx"}]}.
こうしておくとrebar comileするとその際にrelxもよばれるようになります。
複雑なシステムではほとんどの失敗とエラーはその状態でのみおきるもので、リトライするのが いい方法だといわれています。Jim Grayの論文ではMean Times Between Failuresによると上記のバグは supervisorのリスタートをしないより4倍優れているとでています。 大事なのはErlangのsupervisorsとその子プロセスの起動は同期的ということです。 各OTPプロセスが起動から関連プロセスを守る可能性があるということになります。 プロセスが死んだ場合、あまりに頻繁に失敗するまでリトライされるようになります。 非常にミスが起こりやすい場所があります。supervisorがプロセスのクラッシュ後に再起動する前に 待ち時間はありません。もしネットワークベースのアプリケーションが初期化フェーズ中にリモート先のサービスが 落ちた場合、そのアプリケーションは大量に無駄な再起動を行うでしょう。そして、そのシステムはシャットダウンします。 多くのErlang開発者がsupervisorが冷却期間を持った方がいいのではないかと主張しています。 私は単純な保証に関する理由でそれを強く反対しています。
プロセスを再起動することはよく知られた状態に戻すためです。そこから物事はリトライされます。 先ほどの例のように初期化が安定していない場合、supervisorのもつメリットは小さいです。 初期化プロセスは何があっても安定すべきです。子プロセスたちは事前に確認したうえで起動することで しすてむが健全な状態で起動することができます。 もしあなたがそういった構成を手供しない場合try...catch節でループするだけでメリットをほとんど 享受できないでしょう。supervisorのプロセスは初期化のなかでベストエフォートではなく、 完全に保証すべきです。これは例えばあなたがデータベースやサービスに接続するクライアントを記述する場合、 問題がないことを保証できるまでは初期化の中で接続を確立してはいけないということです。
例えばあなたがErlangシステムを立ち上げる前にローカルのデータベースを立ち上げているので、 初期化ののなかで接続を確立できたとすると、再起動すると正常に動作します。 前提条件の保証ができない場合、ノードはクラッシュします。それはシステム全体の検証としては失敗です。
もしデータベースがリモートにある場合接続に失敗する場合を考えなければいけません。 それは分散システムが落ちているということになります。この状態であなたが保証できるのは 接続リクエストを送ることだけですが、{error, not_connected}が出るだけです。 データベースへの再接続はシステムの安定性に影響を与えることなく、あなたが最適だと信じる冷却期間を通してから 行うことになります。最適化等を行う初期化フェーズでは試せますが、プロセスの場合、何かしらの理由で接続が切れて その後再接続できるようにしなれければないらないためです。
もし外部接続で障害を想定する場合、あなたのシステム自体で保証を行うようにしないでください。 私たちは現実世界を取り扱うため、依存する外部システムは常にオプションであるべきです。
もちろんライブラリやプロセスが呼び出したクライアントがデーベースなしで動くことを期待していない場合、 エラーになります。ビジネス上のルールやクライアントに対してできる/できないを設定することと 対処することは同じ問題でも全く違う方針の問題です。 例えばクライアントはシステム全体に影響しないようなエラーを無視できるようにする運用を検討すべきです。
初期化とsupervisorsのアプローチで違う点はクライアントを呼びだす側はクライアント自身ではないので、 どの程度失敗を許容できるのか決めることができます。 これはフォルトトレラントをでデザインするのに非常に重要です。supervisorsが再起動する場合は、 安定した状態ではないといけません。
以下にプロセスの状態をの一部で接続を保証するコードを記述します。
1 init(Args) ->
2 Opts = parse_args(Args),
3 {ok, Port} = connect(Opts),
4 {ok, #state{sock=Port, opts=Opts}}.
5
6 [...]
7
8 handle_info(reconnect, S = #state{sock=undefined, opts=Opts}) ->
9 %% try reconnecting in a loop
10 case connect(Opts) of
11 {ok, New} -> {noreply, S#state{sock=New}};
12 _ -> self() ! reconnect, {noreply, S}
13 end;
かわりに書き直したコードが以下になります。
1 init(Args) ->
2 Opts = parse_args(Args),
3 %% you could try connecting here anyway, for a best
4 %% effort thing, but be ready to not have a connection.
5 self() ! reconnect,
6 {ok, #state{sock=undefined, opts=Opts}}.
7
8 [...]
9
10 handle_info(reconnect, S = #state{sock=undefined, opts=Opts}) ->
11 %% try reconnecting in a loop
12 case connect(Opts) of
13 {ok, New} -> {noreply, S#state{sock=New}};
14 _ -> self() ! reconnect, {noreply, S}
15 end;
書き直したことによって、初期化の中でいくつかの保証を行うようにしました。 これで接続ができることを確認してから接続マネージャを利用可能にするという動きになります。
システムに対して2つのアプローチで検討を行いました。 設定ファイルのようなファイルシステムへのアクセス、ログでUDPポートを開くようなローカルリソースへのアクセス、 ネットワークやディスクからの復元等supervisorは要件としてどの程度同期的に待つのが問題ないのか設定することに なるでしょう。(基本的には起動から10分かかるのはまれですが、GBレベルの同期などは問題ないはずです) 一方で、コードがローカルにないデータベースや外部サービスに依存する場合は、通常の操作中におきる可能性があり、 それは今でも後でも代わりがないので、supervisorを部分的に起動する方針にするといいでしょう。 あなたは同様に処理すべきですがシステム内での制約は減らした方がいいでしょう。
失敗が連続することがノードが死んだという考えるのは正しくありません。一度システムがOTPアプリケーションとして 稼働したらその後はそれ自身がノードが生存可能かどうか判断すべきです。 それぞれのOTPアプリケーションは3つの方法で立ち上げることができます。permanent, transient, temporaryで これらはapplication:start/2で呼び出すこともできるしあなたのリリース設定ファイルに記述することも可能です。
もちろんOTP配下にあるアプリケーションを再起動することも可能です。
昔は、私が一番遭遇していたエラーの原因はメモリ不足によるものでした。 さらにいうと、それはメッセージキューによるものでした。 これに対する扱い方はたくさんありますが、どう対応するかについては、 うごいているシステムを理解する必要があります。 物事を単純化していくと、ほとんどのプロジェクトはバスルームのシンクにみえてきます。 ユーザとデータの入力は蛇口からきています。Erlangシステムはシンクとパイプで、 出力は下水道システムと考えることができます。Erlangのノードが何かしらの理由で あふれてしまった場合、何が原因か考えることがきわめて重要です。 誰かが大量に水をシンクにいれていませんか?下水道システムは正常にうごいていますか?
小さすぎるパイプを設計しましたか? キューが壊れるようにするのはそんなに難しいことではありません。クラッシュダンプでこの情報を見つけることができます。 しかし、なぜ壊れたかを知るのは少しトリッキーです。プロセスの役割や実行時の情報をもとに、 水が急激に流れてきたのか、プロセスがブロックされていないかなど考えることができます。 一番難しい部分はこれをどのようにして直すかです。 シンクが多くの廃棄物でつまった場合、シンク自体を大きくしようとします。 シンクの排水が小さすぎるようであればそれを最適化します。それからパイプ自体が狭くないか確認して そうであれば最適化します。下水道がそれ以上対応できなくなるようになるまでは負荷はシステムの中で 対応することになります。ここでのポイントは、シンクやバスルームの追加を全体の負荷を見て決めることです。
そしてバスルームのレベルでそれ以上改善できない点もあります。ログを送りすぎていたり、 データベースの一貫性で待つ必要があったり、組織の中で十分な知識が持てていないケースも考えられます。 ポイントを探すことによって、本当のボトルネックポイントを探すことができ、それまでの最適化も必要だったかとは 思いますが、幾分無駄になってしまっていることもわかります。私たちはより賢明になる必要があります。 よりシステムを軽くするために情報をよく設計する必要があります。 さらに負荷が高すぎるとシステムの制約をいれてそこを廃棄するかサービスの質を下げるか難しい判断が必要になります。 これらのメカニズムに関する二つの戦略が"back-pressure"と"load-shedding"になります。
この章ではErlangシステムが共通的に負荷がこえてしまう原因について見ていきます。
数少ない原因ではありますが、どのような設計を仕様とも負荷があふれてしまうケースがあります。 それらはスケールアップやシステムが成長したときや、想定より困難な問題が発生した場合に見られます。
皮肉なことですが、エラーログは最も壊れやすいものの一つです。Erlangをインストールしたときのデフォルトでは、 エラーログのネットワークもしくはディスク上に書き出すことはエラーの発生より多くの時間がかかります。 これはユーザが生成したログは大規模なプロセスのクラッシュに特に当てはまります。 前者の場合、エラーログは継続的に入っているメッセージを期待していません。こういったケースは例外的で 基本的に多くのトラフィックを期待していません。後者の場合、プロセス全体の状態がログにコピーされます。 これだけでメモリを逼迫するメッセージを受け取り、それがOutOfMemoryになりえない場合、 追加の処理に時間がかかる可能性があります。執筆時点での最善の解決策はlagerというライブラリを利用することです。 lagerは全ての問題を解決する訳ではありませんが、しきい値をこえたOTPのエラーログメッセージを切り捨て、 動的にユーザメッセージの同期と非同期方式で必要に応じて切り替えます。 これはユーザが送信してきた大量のメッセージを捌くようなことはできませんが、そういった場合は プログラマが制御しているものです。
ロックとブロック命令はプロセス間で継続的にタスクをやり取りしている中であるプロセスが予想以上に長く 時間がかかっているときによく問題になります。よくある例の一つとして、TCPソケットからメッセージを受け取るのを 待っている場面をよく見ます。このタイプのブロック命令は、メッセージキューにメッセージが積み上がります。 特に悪かった例はlhttpcというforkライブラリにかかれていたプール管理ライブラリです。 これはテストはほとんど通っていましたし接続タイムアウトも10ミリセカンドをこえないように設計していました。 しかし最初の数週間完璧に稼働したあと、あるリモートサーバがダウンしたことが原因でクライアントが落ちました。 この原因は10ミリセカンド処理にかかるとしていたものが突然全ての接続を諦めなければならなくなったからでした。 秒間5ミリセカンド程度のリクエストで9000メッセージ/秒処理していたものが上記が原因で18,000メッセージ/秒の負荷がかかり 手に負えなくなりました。 私たちが考えた解決策は呼び出し側の接続作業を残したまま管理者がその作業を完了させたと強制的に認識させることでした。 ブロック操作はライブラリによって全てのユーザが利用可能になったため、管理者がやるべきことがなくなり、 今はより多くのリクエストを受け入れることができます。 メッセージを中央で受けとる必要がなくなったため、時間のかかるタスクは極力外でやるようになりました。 プロセスを増やすことによって負荷が予測できる(ブロック操作やバッファリング操作)というのはいいアイデアです。 本質的に同時に行う必要がないものをプロセスを増やして複雑性を生み出すまえに、本当にそれが必要か確認することも重要です。 別のオプションとしてタスクを非同期のものに変換するということです。もしそれが許されるのであれば、 長時間ジョブを開始し、それを一意に識別することで元々の要求を処理します。リクエストを利用可能になったとき、 サーバに前のトークンと一緒になって戻されます。サーバはメッセージを受信し、トークンとマッチングをかけ、 ブロックタイムなしで要求を返すことができます。 このオプションはたくさんのプロセスを利用する点とコールバック地獄になる点はすこしデメリットもありますが、 より効率的にリソースを活用することができます。
OTPアプリケーションを開発していると想定していないメッセージがくることはまれです。 なぜならOTPアプリケーションはhandle_info/2でハンドリングされているので、予期しないメッセージは あまり蓄積されないでしょう。しかしながらOTP準拠システムが全ての動作を実装しているとは限りません。 もしモニタリングツールがあるのであれば、どのようにメモリが増えていくのか、キューのサイズを見て 落ちそうなプロセスを見つけることができます。ここで必要とされるメッセージをハンドリングすることで 問題を修正することができます。
入力制限で最も簡単なのはErlangシステム上のメッセージキューの制限を行うことです。 それは最もシンプルで最適かもしれませんがユーザの動きを遅くしていることを意味します。 一方でユーザには悪い経験を与えることになります。データ入力を制限する一般的な方法は 同期制御不能なプロセス呼び出しを行うことです。次のリクエストに移る前にレスポンスを要求することにより、 対象部分が遅延することを防ぎます。このアプローチの難しい部分はボトルネックとなっているキューが、 システムの一番先端ではないため、深く探索しようとするとその近辺にある全てのキューを最適化してから 探す必要があります。ボトルネックは一般的にデータベースや、ディスクIO,ネットワーク上のサービスなどがあげられます。 これは結局同期部分をバックエンドに一旦処理を依頼して終わるまでユーザに"減速してください"と伝えることができます。 開発者はよくこのパターンをユーザごとのAPI制限等に利用しています。これはQoSの保証や公平なリソースの供給が可能になります。
この方式でトリッキーのなのは同期呼び出しで負荷をあたえる部分をバックエンドに追いやることですが、 その同期作業がどの程度時間がかかるものなのかタイムアウトを設定しなければなりません。 この問題を表現する一番の方法はシステムの先端で最初の時間がはじまってから、その本質の指令が潜ることです。 これはタイマーが計画した時間待たなければ行けないということを意味します。 一番簡単なのは待ち時間を無制限にすることです。Pat Hellandはこれに対して面白い回答をしています。
いくつかのアプリケーション開発者は議論してタイムアウトなしで問題ないというかもしれません。
私はタイムアウトを30年にしないか提案しました。そうすると次々と合理的なレスポンスが生成されます。
なぜ30年が愚かで無制限が合理的なのでしょう?私は未だに無制限待つメッセージングアプリケーションに
出会ったことがありません。
結局これはケースバイケースだとおもいます。しかし多くのケースではフロー制御に違うメカニズムを利用するように 設計すべきでしょう。
バックエンドにまかせるシンプルな方式としてブロックしてほしいリソース、例えば早くできないものや ビジネスやユーザにクリティカルなものを特定するというものがあります。それらのリソースをロックする 際にリクエストやそれらを利用する権利があるか確認を行います。確認するものとして、CPU,メモリ、負荷、 並列度、レスポンス時間などがあれられます。SafetyValveアプリケーションはこういったニーズにそったフレームワークです。 さらにシステムや障害に関して何かユースケースで何かしたいケースに対しても、利用可能な回路があります。 例えば、breaky, fuse, circuirt_breakerなどがあります。 ETSを利用したアドホックなシステムといった他のツールも利用可能です。重要な部分はシステムの先端でブロックしてしまうときに プロセスがデータにアクセスする権利があるか確認するのですが、コードの本質的なボトルネックは権利があるかどうか 確認する部分になります。この方式で進んでいくことのメリットはタイマーや同期レイヤーの抽象化を複雑にしなくていい点です。 あなたはボトルネックを守り、エッジや制御点、全てにおいて読みやすいものが出来上がります。
バンクエンドのトリッキーな部分についてここで説明します。同期呼び出しを通して暗黙的にバックエンドにまかせた処理が 完了したとき、負荷が実際にどこにかかているか知る唯一の方法はシステムが遅くなっていく場面です。悲しいことに、 弱いハードウェア、弱いネットワーク、関連のない場面での負荷、低速のクライアントなど様々なことが考えられます。 システムがバックエンドに処理を適用する際にそれ自身の応答性を計測することは熱を持った人を診断するのと同義です。 そして何か間違っていないかを表示します。
診察のための権限を確認すると、あなたに明確なレポートを送れるようインタフェースを定義します。 システム全体として負荷が高い場合や、指令のなかでどこかの制限にひっかかっている場合を知ることができる。 システムを設計する際に行うべき選択があります。ユーザはユーザごとに制限をもっていますか?それとも システム全体として制限をもっていますか? システム全体として制限をもつかノードごとに制限を持つかどちらにしても実装は簡単です。しかし、どちらかに倒すと 不公平になるかもしれません。90%のリクエストをしめているユーザがいたとして、そのユーザは他の大多数のユーザのせいで プラットフォームが利用できないかもしれません。 ユーザごとに制限を持つと、公平ですしプレミアムユーザで制限をなくすという対応も可能です。これは単純にいいことですが、 より多くのユーザに利用してもらうためにはより効率的なシステム全体での制御を検討する必要があります。 100ユーザではじめて一分間で100リクエストを制限とした場合最大で一分間に10,000回リクエストがくることになります。 20ユーザ同様の条件で追加するとたちまちクラッシュしてしまうかもしれません。 ユーザが増えてくるとそれだけエラーが起こりえる可能性が増えてきます。ビジネス的に許容できるトレードオフを考慮する ポイントは重要ですが、ユーザはシステム全体が一定時間落ちているだけより、一日中落ちていることに不満を持つ 傾向があります。
Erlangシステムの外側で遅くなって、それがスケールアップできないときあなたはデータを捨てるかクラッシュさせる必要があります。 それは悲しい現実ですがそれ以外の対応方法は難しいです。プログラマ、ソフトウェアエンジニア、コンピュータサイエンティストは 使っていないデータを削ぎ落とし、使いやすいように保ちます。諦めずに最適化を続けて正常な状態にします。 しかしながら、もしデータが出て行くより入ってくる方が早い場合、 Erlangシステムがそれを十分に対応できるか判断すべきポイントになります。それはafterというコンポーネントで処理します。 もしあなたがデータが入ってくる制限を持っていない場合クラッシュしないようにデータを廃棄する必要があります。
ランダムにデータを捨てることが最も簡単でシンプルに保つことができます。 トリックとしては例えば0から1の間の数字は捨てると行ったしきい値によるものです。
-module(drop).
-export([random/1]).
random(Rate) ->
maybe_seed(),
random:uniform() =< Rate.
maybe_seed() ->
case get(random_seed) of
undefined -> random:seed(erlang:now());
{X,X,X} -> random:seed(erlang:now());
_ -> ok
end.
もしメッセージの95%を保持したいのであれば、認証はcase drop:random(0.95) of true -> send(); false -> drop() endを 呼びます。(もしくはdrop:random(0.95) andalso send())これは特にメッセージの削除に何も意味を持たせない場合です。 maybee_seed関数はプロセスの中でseedが検証された値かくらだないものになっていないか検証しています。 また、複数回now関数が呼ばれるのをさける目的でも利用されています。 このメソッドから1つわかったことがあります。ランダムドロップは理想的にはキューレベルではなくProducerレベルで 実行されるべきです。キューの負荷をさけるためには最初の場所からデータを送らない方法です。 なぜならErlangには閉ざされたメールボックスはないので保証された受信プロセスのみ捨てるようにします。 このプロセスは乱暴に動作しているもので、大量のメッセージを取り除くようにして、正常に動作させるようつとめます。
一方で、Producerレベルでの廃棄は全てのプロセス間で共有できる必要があります。 これは面白い場所で最適化が行われます。ETSのテーブルやapplication/set_envを利用してしきい値を設定するのです。 これによって負荷をベースに廃棄すべきメッセージをコントロールでき、設定データについてはapplication:get_env/2を利用します。 上記の技術を応用するとConsumerレベルではなくメッセージの優先順位で廃棄すべき割合を設定できます。
キューバッファはランダム廃棄ではない方法でメッセージをコントロールする方法としていい方法です。 特にコンンスタントにはいってくる線形ストリーミングデータではなく入力データの負荷が気になるときに有効です。 プロセスがキューの形を持ったメールボックスであるにも関わらず、あなたは全てのメッセージを受信することができます。 キューバッファは安全に動作するため、2つのプロセスを必要とします。
これを動作させるために、バッファプロセスはメールボックスからデータを取り除いて、キューにデータをおきます。 いつでもサーバを動作できるようになっており、バッファプロセスにいくつのメッセージがきているか確認できます。 バッファプロセスはキューから取り出してサーバにわたし、データを蓄積します。 キューにデータがきて新しいメッセージを受け取れるようになればあなたはデータを取り出して新しいとこにおくことができます。 必要であれば古いメッセージは削除もできるし残しておくこともできます。 ここでリングバッファのように安定した受信数のメッセージを維持し、かつ負荷に対する耐性も必要です。 PO Boxが代表的なライブラリです。
スタックバッファはキューバッファを制御するのに便利ですが、遅延があまりない要件が重要であることを意識してください。 バッファをスタックとして利用する場合、2つのプロセスが必要で、一つはキューバッファともう一つは キューバッファをリストとして扱うためのプロセスです。 スタックバッファが低レイテンシで特に喜ばれるのはバッファフロートに関連するものです。 もしキューに入ったメッセージの中でいくつか遅延が発生すると、キューにある全てのメッセージが数ミリ秒遅延します。 結局、メッセージの鮮度は悪くなり廃棄の対象になります。一方でスタックにすることで新しいものを時間通りサーバに送りながら 制限された個数の要素のみ所持することが可能になります。 あなたはスタックを確認しながらQoSに応じてスタックを管理することができます。PO Boxもそれににた実装になっています。 スタックバッファの欠点は必ずしも送られてきた順番に処理する必要がないということです。 それぞれが独立のタスクとなっている場合はいいのですが、連続したものが一つのタスクとしてなっている場合、 無駄なタスクが残ってしまう可能性があります。
もしあなたが古すぎるイベントの前に古いイベントを対応しないといけない場合、そこから複雑な構成になり、 毎回スタックを確認しないといけなくなるので、コンスタントにスタックを削除するのが非常に非効率になります。 面白いアプローチは複数のスタックをもったバケットを用意してスタック間を時間で管理するという方法です。 もしQoSに満たないスタックがあれば時間を参照してスタック全体を削除すればいいのです。 これは一部のメリットに対して多くのデメリットがあるように思うかもしれませんが、とにかくメッセージを削除するときに 低レイテンシで行いたい場合は非常に好ましい方法になります。
定期的な負荷を扱うための新しい解決策が必要になるかもしれません。キューとバッファーは時々起こる負荷に対して 相性がいいです。そしてこれらは入力に対して追いつけるようなときに信頼性を持って処理ができます。 こういった場合たくさんのメッセージが一つのプロセスに集中すると問題が発生します。 この場合2つのいいアプローチがあります。
ETSテーブルはプロセスより多数のリクエストを捌くことができますが、ETSテーブルがサポートしている動作は ごく基本的なもののみです。一貫したカウンタから追加、削除、単純な読み込みは動作可能です。 ETSテーブルは2つのアプローチを必要とします。 一般的にはなされるのは最初の条件は普通のプロセスとしてうまく動作することです。 N個のプロセスをあげて、それらに名前をつけて、どれかを選んでメッセージを投げることができるようにします。 負荷を想定してイベントを分散させてランダムにプロセスを選択し、かつ信頼性が必要とされます。 状態を持たないコミュニケーションが必要なのであれば、作業はほぼ同じ方法で共有され、失敗に対して鈍感になります。 経験的に私は動的にatomを作成することを避けていて、ETSテーブルのread_concurrencyをtrueにセットすることでworkerに 共有する方式を好んでいます。これは動くことももちろんですが後でいくつかの更新を行う際に便利だからです。 似たような方式としてlhttpcライブラリを利用してドメインを基本としたロードバランス構成を採ることもできます。
二つ目の条件はロックとカウンターを使いますが、基本的な構成は残して、実際にメッセージを送る前に ETSテーブルを更新できるようにしておくことです。全てのクライアントが共有するのに制限があるのはよく知られていますが プロセスを作成するリクエストはこの制限をクリアするようにします。 このアプローチはメッセージキューを避けるためにdispcountが利用されています。そしてこれはリクエストが拒否されても 待たなくていいように低レイテンシを保証した上で実現することができます。 その後ライブラリ側ですぐ諦めるのか別のワーカーで試すのか実装します。
ここまでのほとんどの解決策はメッセージの量をベースにして考えてきましたが、 メッセージサイズや、複雑性、もしくはあなtが指標としたい何かをベースに考えることもできます。 キューかスタックバッファを利用したときは、エントリの数を数えるのではなく、メッセージサイズや 負荷をチェックする必要があるかもしれません。 私が経験上考えるのはメッセージの詳細を考慮せずに捨てるのではなく、 各アプリケーションが受け入れるかどうか判断する独自のポイントを持っています。 もちろんそこにはデータをあなたに送って"fire-and-forget"に従った方法で対応することも可能です (システム全体を非同期パイプラインの一部だと考える)が、その方法だとなぜデータが捨てられたのか エンドユーザ向けの説明が難しくなります。もしあなたが廃棄するメッセージを集めて、"Y個のメッセージが破棄されました。 理由はXです"とするのであれば、ユーザの理解を得やすくなります。 この場合Herokuのlogprex機能を採用することになるかとおもいます。これはルーティングに関するシステムで L10 errorsを吐き出すことができます。これを利用することでシステムの一部が全てのメッセージを扱うことができないことを ユーザに伝えることができます。
最後に負荷があるときに受け入れいれるかどうかについてはシステムを利用するユーザに依存する傾向があります。 新しい技術を開発するより要件は簡単で少しの変更で可能な場合が多いですが、 ときどきそれだけでは対応できないこともあります。
実行中のサーバと対話する場合、基本的に次のいずれかの方法でおこなわれます。 一つはscreenやtmuxを利用してバックグラウンドで起動しておきシェルを利用可能な状態にしておくことです。 もう一つは関数や包括的な設定ファイルを再読み込みさせる方法です。 動的なセッションで対話する場合REPL形式で動作していれば一般的にはアクセス可能です。 プログラムや設定ファイルで管理している場合、慎重に検討する必要があり、何をしたいのか はっきりしさせてからアクセスする必要があります。後者の方法は全てのシステムで利用可能なはずですので 今回はこれが何を与えるのかについてはスキップします。 Erlangの場合REPLよりもう少し"動的"に近い方式をとります。基本的にErlangVMはREPLを必要としておらず、 バイトコードを実行して動作されるので、シェルは必要ありません。 しかしErlangがどのように並列にマルチプロセスで動作するのか、そして分散のためにどのようなサポートを しているか知るために任意のErlangプロセスに対してREPL形式でアクセス可能です。 Erlangでは一つのscreenに一つのshellという形式ではなく、一度に複数のErlangVM上に存在する 複数の接続にアクセスすることができます。 基本的に同時に二つの接続にアクセスする場合はcookieをベースにアクセスしますが、それが含まれていない ケースでもアクセスすることは可能です。 その場合ノードの名前をもとにそれらのノードに事前にアクセスできることを 確認しておく必要があります。(prior measure)
ジョブ制御モード(JCLモード)はErlangシェル上で^Gを押すことで切り替えられます。 メニューからリモートシェルにアクセスするためのオプションが選べます。
([email protected])1>
User switch command
--> h
c [nn] - connect to job
i [nn] - interrupt job
k [nn] - kill job
j - list all jobs
s [shell] - start local shell
r [node [shell]] - start remote shell
q - quit erlang
? | h - this message
--> r ’[email protected]’
--> c
Eshell Vx.x.x (abort with ^G)
([email protected])1>
上記コマンドを発行すると、行が編集されてリモートシェルとして機能するようになります。 全ての出力はリモート先の出力がローカル上に表示されます。 シェルを抜けてJCLモードにもどるには^Gを押します。ローカルシェルから抜けるためには ^G qをタイプします。
([email protected])1>
User switch command
--> q
自動的にクラスタ全体に接続しないようにhiddenモードで接続した方がいいかもしれません。
このメカニズムはJCLモードに似ていますが、用法が違います。 全体のJCLモードにアクセスするためには以下のようにアクセスします。
erl -name [email protected] -remsh [email protected]
これは短縮版です。
erl -sname local@domain -remsh remote@domain
その他のErlangのオプションも検証されます。 JCLモードで動いているものとメカニズムは同じですが、最初からリモートシェルに アクセスできているところが違います。^Gが安全に抜けるいい方法です。
Erlang/OTPはSSH実装が同胞されていてそれはサーバにもクライアントにもなることができます。 そのデモアプリケーションはErlangのリモートシェルで動作しています。 これを動作させるために、SSHのキーを用意しておく必要があるので、テスト用に以下のように設定しておきます。
$ mkdir /tmp/ssh
$ ssh-keygen -t rsa -f /tmp/ssh/ssh_host_rsa_key
$ ssh-keygen -t rsa1 -f /tmp/ssh/ssh_host_key
$ ssh-keygen -t dsa -f /tmp/ssh/ssh_host_dsa_key
$ erl
1> application:ensure_all_started(ssh).
{ok,[crypto,asn1,public_key,ssh]}
2> ssh:daemon(8989, [{system_dir, "/tmp/ssh"},
2> {user_dir, "/home/ferd/.ssh"}]).
{ok,<0.52.0>}
私はここでいくつかのオプションを設定しています。system_dirはhostファイルがどこにあるのか指定しています。 user_dirで設定ファイルの場所を指定しています。特別なパスワードや公開キーの指定もオプションで設定可能です。 SSHデーモンを介して接続する場合以下のように接続します。
$ ssh -p 8989 [email protected]
Eshell Vx.x.x (abort with ^G)
1>
こうすることで現在のハードにErlangをインストールすることなくアクセスができます。 接続を切るときはSSHの接続を切るだけで大丈夫です。q()やinit:stop()関数を実行すると リモート先が停止してしまうので気をつけてください。 もし接続で困った場合、-oLogLevel=DEBUGをつけて実行するとSSH接続デバッグの出力が表示されます。
あまり知られていない方法として明示的に分散を指定してないErlangノードに対して名前付けされたパイプを 通して接続することができます。これはrun_erlという名前付けパイプをラップしたコマンドでアクセスできます。
$ run_erl /tmp/erl_pipe /tmp/log_dir "erl"
最初の引数が名前付けパイプを利用するファイルです。二つ目の引数がログをはくディレクトリです。 ノードに接続するためにはto_erlを利用します。
$ to_erl /tmp/erl_pipe
Attaching to /tmp/erl_pipe (^D to exit)
1>
これでシェルが接続されます。`抜けるときは^Dをタイプします。
ErlangVMを本番環境で利用することの一つのセールスポイントは透過的に実行時の中身、 デバッグ、プロファイル、分析ができることです。 プログラムを利用してアクセスできることのメリットはそれらを利用したツールが簡単に作れるので、 タスクや監視ツールを作るのが簡単だということです。必要なときにVMの情報を閲覧することができるのです。
経験的なアプローチでは成長していくシステムを健康的な状態に保つためには全ての視点で監視できることが重要です。 それが普通かそう出ないかというアドバイスは一般的にはありません。あなたは通常の状態を知るためにある時間帯の たくさんのデータが欲しいでしょう。何日かたって必要な情報を集めることができたら、それをOFFにして対策をたてることができます。 この章では普通のOTPアプリケーションにどのようにアクセスできるのを見せたいとおもいます。 しかし、全ての特徴が一カ所にあるわけではないですが、本番システムで自身で簡単にアクセスできます。 それらは便利なツールというよりはブロックを構築するというほうが近いかもしれません。 基本的なツールはreconライブラリにまとめられていてテキストのハイライトや便利なオペレーションがまとまっています。
大きい意味でVMの中身をみようとすると、コードが走っているかどうかは関係なく、統計情報を集めるのが役立ちます。 もちろんあなたの狙いが長い期間の情報取得だとすると、一週間をこえる小時間のウィンドウが検出できないといって 問題があがってきます。メモリやプロセスリーク含めこれらの長い期間へのいい対応としては、まずデータを取得して 日単位や週単位の活動のなかでどういったことが発生するか数ヶ月確信できるまで見極めることです。 こういったケースでErlangの計測ツールは便利です。以下にオプションがあります。
これらをあなたの目的にそって必要な情報を取得することがいいとおもいます。
VM上のメモリを調べる場合はerlang:memory()を利用します。
1> erlang:memory().
[{total,13772400},
{processes,4390232},
{processes_used,4390112},
{system,9382168},
{atom,194289},
{atom_used,173419},
{binary,979264},
{code,4026603},
{ets,305920}]
これにはいくつかの説明が必要です、 単位は全てバイトで返し、割り当てられているメモリを返します。 これは後で見てみますがOSが割り当てるものより少し小さい値になります。 totalフィールドはプロセスとシステムが利用しているメモリの合計になります。 processesはErlangのプロセスが利用しているメモリを、systemはシステムで利用しているメモリになります。 残りはETSテーブルとVMで利用しているメモリの一覧で他の隠れフィールドについては説明しません。 もしあなたがVM全体のメモリ量を知りたいのであれば、本当の最大値ではulimitで取得できる値ですが、 正確にはVMの中で取得することは難しいです。 もししりたい場合はtopかhtopを利用して取得することができます。幸運にもrecon_alloc:memory/1で取得可能です。 引数は以下になります。
これらの追加オプションも指定可能です、そして7章で利用するメモリリークについても必要になるとおもいます。
不幸にもErlang開発者にとってCPUをプロファイルするのは難しいです。理由は以下の通りです。
これらの要素により、実際にうごいているCPUを知ることは難しいです。Erlangが共通的に利用する CPUもありますが実際にたくさんの実作業がはいるとCPUはさらに利用することになります。 最も正確にデータを知れるのがスケジューラのウォールタイムです。それはオプションで実行できる 統計でノード上で手で実行するとそこから定期間隔で取得します。 これはErlangプロセスが実際に動作していた時間を示します。 NIFs, BIFs, garbage collectionなどが対象になります。 これはCPU利用率というよりかはスケジューラ利用率の値というほうが正しいです。 Erlang/OTPリファレンスマニュアルで基本的な使いかたは書いてありますがここではreconを使って取得してみます。
1> recon:scheduler_usage(1000).
[{1,0.9919596133421669},
{2,0.9369579039389054},
{3,1.9294092120138725e-5},
{4,1.2087551402238991e-5}]
recon:scheduler_usage(N)はNミリ秒間隔でデータを取得し、各スケジューラの利用時間を出力します。 このケースではおおきく二つのスケジューラがあって一つは93%程度、もう一つは1%以下しか使っていません。 ただ、htopを利用すると各コアの利用率は以下のように見えます。
1 [||||||||||||||||||||||||| 70.4%]
2 [||||||| 20.6%]
3 [|||||||||||||||||||||||||||||100.0%]
4 [|||||||||||||||| 40.2%]
この結果はErlangが比較的暇なときにとった結果ですが、OSからみると忙しく見えます。 他にも面白い振る舞いがみれていて1点目のポイントではOSが報告してきた内容より Erlangが報告した内容のほうが高くなっています。OSリソースを待っているスケジューラたちは 彼らがこれ以上仕事ができないものだと判断しています。もしOSがCPUを利用しないタスクをしている場合 Erlangスケジューラは仕事ができないものだと判断して高い利用率が表示されます。 これらの把握は容量計画時には重要でCPUが指し示すものよりErlangのものを信用した方がよいでしょう。
全体でみるとVMの中でどの程度タスクをこなしているかみることができます。一般的にErlangでいいといわれているのは 本当に同時に動いているプロセスを使うことです。例えばWebサーバでいうと1リクエストもしくは1接続で1プロセス使いますし ステートフルの場合1ユーザあたり1プロセス追加されることになるでしょう。そしてノード上で何本のプロセスを 使っているのか統計を取得することができます。ほとんどのツールは5.1でお話しましたがマニュアルで利用するときは 以下のように取得できます。
1> length(processes()).
56535
この値を取得することでプロセスリーク等の情報に役立てることができます。
ポートもプロセスと同様に考慮すべきです。 ポートは外の世界と接続するためのデータタイプで、TCPソケット、UDPソケット、SCTPソケット、 ファイル記述子などがあげられます。プロセス同様ポートをカウントするメソッドがあります。(length(erlang:ports())) しかしこのメソッドでは全てのタイプのポートをマージしてしまうのですが、 reconを利用してそれらをソートしてみせることが可能です。
1> recon:port_types().
[{"tcp_inet",21480},
{"efile",2},
{"udp_inet",2},
{"0/1",1},
{"2/2",1},
{"inet_gethost 4 ",1}]
このリストは各ポートについてカウントされており、データタイプも持っています。 タイプの名前はErlang VMが持っているもの自身を文字列にしたものです。 "_inet"がつくものは一般的にTCP,UDP,SCTPといったプロトコルを示します。 "etype"ファイルは一般的にファイルを示すもので、"0/1"と"2/2"は標準IOと標準エラーIOを表現しています。 ほとんどの他のタイプはport programやport driverなどプログラム内で利用されるドライバ名を示します。 これらを追跡することでシステムの負荷状況やシステム評価漏れ等を確認することができます。
あなたが問題を持って大きなログ等を見るときはいつでも、まずは目的にそってその周辺から興味を持ち始めるでしょう。 奇妙な状態のプロセスがありますか?だとしたらそれは追跡が必要になります! 追跡することは入出力の確認等で大変便利ですがそこにいきつく前に、もっと掘り下げることが必要です。 メモリリーク以外(メモリリーク自体は少し特別な技術が必要でそれは7章で説明します)はほとんどが プロセスとポート(ファイルとソケット)に関連します。
Erlangシステムにおいてプロセスは非常に重要です。なぜならプロセスは中心にいてそこから始まり、 そこから始まることの多くを知っています。幸運にもVMは多くの情報を利用可能で、いくつかは安全に使うことができますが、 いくつかは本番環境で利用するのにおすすめしないものもあります。 (なぜならかれらは大量のデータをコピーしてメモリーを消費し、それを表示しようとするとノードがこらせれてしまうかもしれません)
全ての値はprocess_info(Pid, key) もしくは process_info(Pid, [keys])を呼ぶことで取得できます。 以下にいくつかの共通で利用するキーを示します。
Meta:
dictionary: プロセス辞書にある全てを返します。一般的にギガバイトレベルのデータを入れるべきではないので安全です。
group_leader: IOが走っているプロセスの親プロセス(format/1-3の出力)
registered_name: もしプロセスが名前を持っていたらそれを返します
status: スケジューラから見たプロセスの状態。次の値を持っています。
exiting: プロセスは完了しているがクリアされていない状態
waiting: receive ... end で待っている状態
running: 実行中
runnable: 始められる状態だがスケジュールが他のプロセスを実行している状態
garbage_collecting: GC
suspended: BIFもしくはポートバッファが一杯になっていることによって中断されている状態。
このプロセスはポートが忙しい状態じゃなくなったときに再実行されます。
Signals:
links: 全てのリンクしているプロセスとソケットやファイルディスクリプタの一覧を返します。
基本的には安全ですが数千の情報をもった大きなスーパーバイザには注意が必要です。
monitored_by: 現在のプロセスを監視しているプロセス一覧を返します(erlang:monitor/2と同じ)
monitors: monitored_byの反対の種類です。あるプロセスに監視されているプロセス一丸を返します。
trap_exit: もしプロセスがトラップしている場合はtrueをそれ以外はfalseを返します
Location:
current_function: 実行中の関数情報をタプルで表現します。{Mod, Fun, Arity}
current_location: モジュール内のどこにいるかタプルで表現します。 {Mod, Fun, Arity, [{File, Filename}, {line, Num]}
current_stacktrace: 現在のスタックとレースを返します
initial_call: プロセスがspawnされたときの関数情報を{Mod, Fun, Arity}で返します。どのプロセスにspawnされたか識別する際に便利です。
Memory used:
binary: バイナリへの参照とそのデータサイズを返します。もしプロセスが大量にそれらを解放した場合安全に利用できないかもしれません。
garbage collection: プロセス内のGCされた情報を返します。内容は"subject to change"として文書化されます。フルsweep GCなどのオプションを通して何回GCされたのかとヒープサイズについて返します。
heap_size: Erlangプロセスにはoldとnewヒープがありますが、これはGCによって振り分けられます。これは一番新しい世代のヒープをスタックサイズ含めて返却しします。値はwordsの中に入っています。
memory: プロセスで利用しているメモリ(コールスタック、ヒープ、VMで利用されているメモリ)をバイトレベルで返却します
message_queue_len: プロセスのメールボックスの中にあるメッセージ数を返します
message: プロセスの中にあるメッセージ全てを返します。これはまれにメールボックス内をロックしてしまうので本番環境で利用するのはおすすめできないときがあります。常にまずはmessage_queue_lenでサイズを確認しえtください。
total_heap_size: heap_sizeと似ていますがこれにはold領域も含まれています。
Work:
reductions: Erlang VMはスケジューリングの実装をポータブルにすることで任意のタスク単位でスケジューリングできるようにしています。(時間ベースにするとたくさんのOSでErlang VMは効果的に動きません)高いreductionsであればプロセスがたくさん仕事をしていることになります。
幸運にもこれらは安全に利用可能でrecon:info/1を利用することでヘルプを活用できます。
1> recon:info("<0.12.0>").
[{meta,[{registered_name,rex},
{dictionary,[{’$ancestors’,[kernel_sup,<0.10.0>]},
{’$initial_call’,{rpc,init,1}}]},
{group_leader,<0.9.0>},
{status,waiting}]},
{signals,[{links,[<0.11.0>]},
{monitors,[]},
{monitored_by,[]},
{trap_exit,true}]},
{location,[{initial_call,{proc_lib,init_p,5}},
{current_stacktrace,[{gen_server,loop,6,
[{file,"gen_server.erl"},{line,358}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,239}]}]}]},
{memory_used,[{memory,2808},
{message_queue_len,0},
{heap_size,233},
{total_heap_size,233},
{garbage_collection,[{min_bin_vheap_size,46422},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,0}]}]},
{work,[{reductions,35}]}]
recon:info/1はこれを拠点にして動けるように最初の引数にpidのようなものを指定してそれを扱います。 それはpidに関する文字列("<0,12,0>")、名前をatomで指定({global, Atom})、もしくはサードパーティの指定方式(gproc: {via, gproc, Name})や タプルでの指定({0, 12, 0})も可能です。プロセスはデバックしたいノードのローカルであることが必要です。 もしあなたがカテゴリ情報だけほしいのであれば直接以下のように参照できます。
2> recon:info(self(), work).
{work,[{reductions,11035}]}
もしくは同じ答えを出すのにprocess_info/2も利用可能です。
3> recon:info(self(), [memory, status]).
[{memory,10600},{status,running}]
後者の場合は不確実な情報を取得していることになります。 全てのデータを利用して私たちはシステムをデバックすることができます。理解しようとするには 書くプロセスのデータを対象のプロセスのデータを見ることになるでしょう。
高いメモリ利用率を見た場合、例としてプロセス一覧と高いメモリ利用率N個をみたいとします。 そのときはrecon:proc_count(Attribute, N)を利用すると以下の結果を得ることができます。
4> recon:proc_count(memory, 3).
[{<0.26.0>,831448,
[{current_function,{group,server_loop,3}},
{initial_call,{group,server,3}}]},
{<0.25.0>,372440,
[user,
{current_function,{group,server_loop,3}},
{initial_call,{group,server,3}}]},
{<0.20.0>,372312,
[code_server,
{current_function,{code_server,loop,1}},
{initial_call,{erlang,apply,2}}]}]
いくつかの属性が表示されますが、長い時間動作しているプロセスを把握できるため、とても便利です。 しかしながら問題もあってほとんどのプロセスが短命の場合、状況を確認するのに十分な時間がありません。 (例えばたった今忙しい状態にしているコードやプロセスを把握したいときなど)
こういったケースのために、Reconはrecon:proc_window(Attribute, Num, Milliseconds)を用意しています。 これはwindowのスナップショットを見るのに便利です。例えば以下のようなタイムラインがあったとします。
--w---- [Sample1] ---x-------------y----- [Sample2] ---z--->
この関数は二つのサンプルをミリセコンド秒で定義するものとします。 これらのサンプルはwとx, yとz, xとyの間のどこかで生存するでしょう。しかし不完全のため、あまり役にはたちません。 もしあなたのプロセスがxからyの間に実行されたとすると、あなたはこれより少ない時間にサンプリングしたことを 確信できるのでwからzの間を評価すればいいことになります。これをしないで結果を見ると、 データを蓄積するのに10倍時間がかかる長時間プロセスが一つではなかったときそれらは巨大な消費者になってしまいます。 この関数は以下のような結果を得ることができます。
5> recon:proc_window(reductions, 3, 500).
[{<0.46.0>,51728,
[{current_function,{queue,in,2}},
{initial_call,{erlang,apply,2}}]},
{<0.49.0>,5728,
[{current_function,{dict,new,0}},
{initial_call,{erlang,apply,2}}]},
{<0.43.0>,650,
[{current_function,{timer,sleep,1}},
{initial_call,{erlang,apply,2}}]}]
これら二つの関数は問題のあるプロセスの特徴を見つけることができます。
もしプロセスに関する質問がOTPプロセスの場合(本番環境にあるプロセスは大体OTPだとおもう)それらを 監視するツールがさらにあります。一般的にはsysモジュールを利用して中身を見ることになります。 ドキュメントを読めばなぜ便利かわかります。それはOTPに対して以下のような特徴を持っています。
プロセス実行の中断または再開する機能も提供します。 これらの詳細までは立ち入りませんがそれらがあることは覚えておくべきです。