まえがき
この記事は英語の原文を日本語に翻訳したものです。原文: https://blog.stenmans.org/theBeamBook/#_preface
翻訳元: happi/theBeamBook 7998e22e78417dbe20e5136b9aee862a1ecaa404(コミット 7998e22)
ErlangやElixirは、これまでに作られた中でも屈指の堅牢な仮想マシンの上で動いています。 本書は、その仕組みを概念のレベルだけでなく、実際に何が起きているかというレベルまで踏み込んで説明します。
本書はスタイルガイドではありません。 構文やベストプラクティスを扱う本でもありません。 GenServerの構造化やスーパービジョンツリーの議論を教える本でもありません。
本書が扱うのは、BEAMの内部です。 プロセスのスケジューリング。 メモリの割り当て。 並行性の扱い。 負荷がかかったときのトレードオフ。 システムが本番で稼働していて、バグがテストでは表面化しないときにこそ重要になってくる事柄です。
終盤ではトレーシング、プロファイリング、パフォーマンスチューニングも扱います。 ただし本当の狙いは、実行時にコードが何をしているのか、そしてなぜそうなるのかを理解するための道具を渡すことです。
システムの挙動を理解すれば、当て推量をやめられます。
本書について
本書は次のような人のためのものです。
- Erlangが実際にどう動いているかを理解したい
- パフォーマンスや信頼性のためにBEAMのインストールをチューニングしたい
- VM内部のクラッシュや奇妙な挙動をデバッグしたい
- 本番環境でシステムを最大限に活用したい
- あるいは、自分だけのランタイムをゼロから作ってみたい
パフォーマンスのテクニックだけが目当てなら、後の章まで読み飛ばしてもかまいません。 ただし、そうしたツールの裏側で何が起きているのか、システムが実際にどう動いているのかを理解したいなら、最初から読み進めてください。
理解が深いほど、判断の質も上がります。
本書での表記規則
BEAM VMは、小さな組み込み機器から、テラバイト級のメモリを持つ大規模なマルチコアサーバまで、幅広いシステム上で動作します。 BEAMアプリケーションのパフォーマンスを効果的に最適化するには、VMがメモリなどのリソースをどう利用しているかを理解する必要があります。
本書では、メモリ構造やデータレイアウトを明快に説明するために図を多用します。 メモリレイアウトやスタック操作は表現の仕方に幅があるため、図についてここで採用した表記規則を示しておきます。
メモリ図
- アドレスは図の上方向に向かって増加します。
- 低位アドレスが下、高位アドレスが上に位置します。
- スタックは通常、高位アドレス側から下方向に伸び、新しい要素は低位アドレス側に積まれます。
C構造体の図
- C構造体は、先頭フィールドが実際には低位アドレスにあるにもかかわらず、図では先頭フィールドを上に配置します。
- そのため、一般的なメモリ領域の図とは上下が逆転して見えます。
構造体とメモリ領域が同じ図に混在する場合、アドレスの表現が鏡写しになる点に注意してください。 この表記規則を理解しておけば、本書を通して図を見るときの混乱を防げます。
本書を読むのにErlangプログラマである必要はありませんが、Erlangとは何かについての基本的な理解は必要です。 次の節でErlangの背景を説明します。
Erlang
本節では、本書の残りを理解するうえで欠かせない、Erlangの基本的な概念をいくつか見ていきます。
Erlangは、生みの親の一人であるJoe Armstrongによって、並行性指向言語(concurrency oriented language)と呼ばれてきました。 並行性はErlangの中心にある概念であり、Erlangシステムがどう動くかを理解するには、Erlangの並行性モデルを理解する必要があります。
まず、並行性(concurrency)と並列性(parallelism)を区別しておきます。 本書における並行性とは、互いに独立して実行できる2つ以上のプロセスがあるという概念を指し、一方を実行してからもう一方を実行する、実行を交互に切り替える、あるいは実際に並列実行する、といういずれの形でも実現できます。 一方、並列実行とは、複数の物理的な実行ユニットを使って、プロセスが実際に同じ時刻に実行されることを指します。 並列性はさまざまなレベルで実現でき、1つのコア内の実行パイプラインにある複数の実行ユニット、1つのCPU内の複数のコア、1台のマシン内の複数のCPU、あるいは複数台の、場合によっては別々の場所にあるマシンを通じて実現されます。
Erlangは、並行性を実現するためにプロセスを使います。 概念的には、ErlangのプロセスはほとんどのOSプロセスに似ています。 それぞれが独立したメモリ空間を持ち(そのためスレッドではありません)、並列に実行され、シグナルを通じて通信できます。 実際には大きな違いがあり、ErlangのプロセスはほとんどのOSプロセスよりはるかに軽量です。 他の並行プログラミング言語の多くは、Erlangのプロセスに相当するものをエージェントと呼びます。
Erlangは、Erlang仮想マシンであるBEAM上でプロセスの実行を交互に切り替えることで並行性を実現します。 マルチコアのプロセッサでは、BEAMはコアごとに1つのスケジューラを走らせ、スケジューラごとに1つのErlangプロセスを実行することで、真の並列性を実現します。 Erlangシステムの設計者は、システムを複数台のコンピュータに分散させることで、さらなる並列性を実現できます。
典型的なErlangシステム(Erlangで構築されたサーバやサービス)は、マイクロサービスやライブラリのような、独立してバージョン管理される単位である複数のアプリケーションから構成されます。
各アプリケーションは複数のErlangモジュールからなり、モジュールはコードをカプセル化する基本単位です。
各モジュールは別々のファイルに収められ、ロード可能なコンパイル済みコードには.beam拡張子が、コンパイル対象のソースコードには.erl拡張子が付きます。
各モジュールには複数の関数が含まれます。 その一部はエクスポートされて他のモジュールから呼び出せますが、残りは内部専用です。 関数は複数の引数を取り、値を返します。 関数の本体は式(expression)から構成されます。 Erlangは関数型言語であり文(statement)を持たず、結果を生成する式だけを持ちます。 Erlangコード例に、Erlangの式と関数の例を示します。
Erlangコード例
%% いくつかのErlangの式:
true.
1+1.
if (X > Y) -> X; true -> Y end.
%% Erlangの関数:
max(X, Y) ->
if (X > Y) -> X;
true -> Y
end.
Erlangには、VMによって実装された組み込み関数(BIFs、built-in functions)が数多くあります。
これは効率上の理由による場合もあり、たとえばlists:append()はErlang自身でも十分に実装できる関数です。
あるいは、Erlang自身では実装が難しい、または不可能な低レベルの機能を提供するためでもあります。
list_to_atom()はその一例です。
エンドユーザーは、Native Implemented Functions(NIF)インターフェースを使うことで、C言語で実装した独自の関数を提供できます(NIFsを参照してください)。
謝辞
まず、EricssonのOTPチーム全体に感謝します。 Erlangとそのランタイムシステムをメンテナンスしてくれたこと、そして私のあらゆる質問に辛抱強く答えてくれたことに対してです。 特にKenneth Lundin、Björn Gustavsson、Lukas Larsson、Rickard Green、Raimo Niskanenに感謝します。
また、本書に大きく貢献してくれたYoshihiro Tanaka、Roberto Aloi、Dmytro Lytovchenkoにも感謝します。 そして本書の執筆をスポンサーしてくれたHappiHackingとTubiTVにも感謝します。