Gleamマスター:BEAM VM上で静的型付けを活用した耐障害性分散システムの構築

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

BEAM VMは数十年にわたって、ほぼ完璧な稼働率で本番システムを動かし続けています。Erlangは50人のエンジニアチームで9億人のユーザーを支えるWhatsAppを実現しました。ElixirはBEAMをRuby開発者の世代に広めました。どちらも動的型付け言語です——バグの一部は実行時、つまり本番環境でユーザーが影響を受けてから初めて表面化します。

Gleamはその問題を解決します。静的型付けの関数型言語で、BEAMバイトコード(およびJavaScript)にコンパイルされ、軽量プロセス、耐障害性監視ツリー、アクターベースの並行処理を、コンパイル時にエラーを検出する型システムのもとで実現します。

Erlang、Go、Elixirのスタックで分散サービスをリリースしてきた中で、あるパターンが繰り返し現れます:実行時の型エラーが深夜3時にアラートを鳴らすバグになるのです。滅多に実行されないコードパスに潜み、テストをすり抜け、実際の本番負荷の下でのみ現れます。Gleamはその問題を直接攻略します——静的型付けを後付けではなく、言語の核心に組み込んでいます。

アプローチ比較——Gleam、Elixir、Erlangを並べて見る

分散システムの障害の多くは2つのことに起因します:コードに埋め込まれた誤った前提と、クラッシュ時に回復しないプロセスです。言語の選択がこの両方にどれだけ対処できるかを左右します。

Erlang——信頼性のゴールドスタンダード、しかし親しみにくい

ErlangはBEAMの元祖言語です。通信インフラ、メッセージキュー、RabbitMQやCouchDBといったデータベースを動かしています。そのプロセスモデルは類を見ません。しかし構文はほとんどの開発者にとって異質で、型システムはオプションかつ不完全(Dialyzerは助けになりますが、強制はしません)、学習曲線は急峻です。

Elixir——美しい構文、しかし実行時型エラー

ElixirはRubyライクな構文、優れたエコシステム(Phoenix、Ecto、LiveView)、活発なコミュニティでErlangの使いやすさの問題を解決しました。しかし動的型付けです。String.upcase(42)と書いても、そのコードパスが本番で実行されるまで間違いに気づけません——もしかすると金曜の深夜2時かもしれません。

Gleam——BEAM上の静的型付け

GleamはMLにインスパイアされた構文、ヒンドリー・ミルナー型システム(HaskellやRustの型推論に似ています)、ErlangおよびElixirコードとの完全な相互運用性を持ちます。コンパイル時の保証とBEAMの実行時特性を両立します。トレードオフではなく——両方を同時に実現します。

3つの言語でエラーハンドリングがどのように見えるか比較してみましょう:

% Erlang — コンパイラによる呼び出し元での強制なし
divide(A, B) ->
    case B of
        0 -> {error, division_by_zero};
        _ -> {ok, A div B}
    end.
# Elixir — 同様の形、しかし依然として動的型付け
def divide(a, b) do
  case b do
    0 -> {:error, :division_by_zero}
    _ -> {:ok, div(a, b)}
  end
end
// Gleam — コンパイラがOkとErrorの両方の処理を強制する
pub fn divide(a: Int, b: Int) -> Result(Int, String) {
  case b {
    0 -> Error("ゼロ除算")
    _ -> Ok(a / b)
  }
}

Gleamでdivide()を呼び出してErrorケースを無視してみてください。コードはコンパイルされません。警告ではなく——本番に1バイトも届く前のハードストップです。

Gleamの長所と短所

Gleamが正しくやっていること

  • コンパイル時の網羅性チェック:パターンマッチはすべてのケースをカバーする必要があります。ブランチを見落とすとコンパイラがデプロイ前に教えてくれます。
  • Erlangエコシステムとの完全な相互運用性:FFI経由でErlangやElixirのあらゆるライブラリをGleamから呼び出せます。RabbitMQクライアント、データベースドライバ、Phoenix——すべてアクセス可能です。ゼロから始める必要はありません。
  • JavaScriptへのコンパイル:GleamはJavaScriptにもコンパイルできるため、1つのコードベースでバックエンドとフロントエンドのビジネスロジックを共有できます。
  • 読みやすいコンパイラエラー:エラーメッセージはIDEで解析するためだけでなく、人間が理解できるように書かれています。何が間違っているかを教え、修正方法を提案してくれることも多いです。
  • ラベル付き引数の強制:複数パラメータの関数呼び出しでは呼び出し元にラベルが必要で、余分なコメントなしにコードを自己文書化します。

Gleamにまだ改善の余地があるところ

  • 小さいが成長中のエコシステム:Elixirの成熟したパッケージインデックスと比べると、Gleamのライブラリ選択肢は限られています。一部の機能ではErlangへのFFIが必要になることを覚悟してください。
  • OTPの抽象化は新しいgleam_otpライブラリはGenServerとSupervisorをラップしていますが、使い勝手はElixirほど洗練されていません。
  • マクロなし:Gleamは意図的にメタプログラミングを避けています。コードベースがElixirマクロに大きく依存している場合、その考え方を再構成する必要があります。
  • ツールはまだ成熟中VS Code拡張機能とZedのプラグインは存在しますが、エコシステムはGoやRustのツールほど洗練されていません。

推奨セットアップ

最初にErlang(OTP 25以上推奨)をインストールし、次にGleam本体をインストールします。

# macOS(Homebrewを使用)
brew install erlang gleam

# Ubuntu/Debian — 最初にErlangをインストール、次にGleamバイナリ
apt install erlang
# GitHubリリースページからGleamバイナリをダウンロード
# またはバージョン管理にasdfを使用:
asdf plugin add gleam
asdf install gleam latest
asdf global gleam latest

# 両方が動作していることを確認
gleam --version
erl --version

新しいプロジェクトを作成し、OTP依存関係を追加します:

gleam new fault_tolerant_worker
cd fault_tolerant_worker

gleam.tomlを編集してOTPパッケージを追加します:

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
gleam_otp = ">= 0.10.0 and < 1.0.0"
gleam_erlang = ">= 0.25.0 and < 1.0.0"
gleam deps download

実装ガイド——耐障害性アクターをゼロから作る

BEAMにおける耐障害性の基本単位はアクター——独立した状態とメッセージ受信箱を持つ軽量プロセスです。Gleamでのアクター構築がどのようなものか見てみましょう。

アクターの定義

各アクターにはメッセージ型(代数的データ型)、状態、ハンドラ関数が必要です。型システムがすべてのメッセージバリアントが処理されることを保証します:

// src/counter.gleam
import gleam/erlang/process
import gleam/otp/actor

pub type Message {
  Increment(amount: Int)
  Reset
  GetCount(reply_to: process.Subject(Int))
}

pub fn start() -> Result(process.Subject(Message), actor.StartError) {
  actor.start(0, handle_message)
}

fn handle_message(
  message: Message,
  count: Int,
) -> actor.Next(Message, Int) {
  case message {
    Increment(amount) -> actor.continue(count + amount)
    Reset -> actor.continue(0)
    GetCount(client) -> {
      process.send(client, count)
      actor.continue(count)
    }
  }
}

Messageに新しいバリアントを追加してhandle_messageでの処理を忘れてみてください。コンパイラはビルドを拒否します。これが網羅性チェックです——コードが実行される前に将来の本番インシデントを防ぎます。

アクターの使用

// src/fault_tolerant_worker.gleam
import gleam/erlang/process
import gleam/io
import gleam/int
import counter

pub fn main() {
  let assert Ok(counter_pid) = counter.start()

  // 送りっぱなしメッセージ
  process.send(counter_pid, counter.Increment(amount: 5))
  process.send(counter_pid, counter.Increment(amount: 3))

  // 同期呼び出し——返信サブジェクトを送信し、レスポンスを待つ
  let reply_subject = process.new_subject()
  process.send(counter_pid, counter.GetCount(reply_to: reply_subject))
  let count = process.receive(reply_subject, 1000)

  case count {
    Ok(n) -> io.println("カウント: " <> int.to_string(n))
    Error(_) -> io.println("カウントの待機タイムアウト")
  }
}
gleam run
# カウント: 8

自動回復のためのスーパーバイザーの追加

スーパービジョンはBEAMシステムがその名声を得るところです。カウンターアクターがクラッシュした場合——バグ、予期しないメッセージ、リソース障害によって——スーパーバイザーが新しい状態で自動的に再起動します:

// src/app_supervisor.gleam
import gleam/otp/supervisor
import counter

pub fn start() {
  supervisor.start(fn(children) {
    children
    |> supervisor.add(
      supervisor.worker(fn(_) { counter.start() })
    )
  })
}

これがBEAMシステムをこれほど回復力のあるものにする「let it crash(クラッシュさせろ)」哲学です。あらゆる関数に散在する防御的エラーハンドリングを省略してください。代わりに自己修復プロセス階層を構築します。アクターが予期しない状態に陥り、クリーンにクラッシュし、スーパーバイザーが気づき、新しいインスタンスが起動します——多くの場合、誰かがアラートを受け取る前に。

Erlangライブラリの直接呼び出し

GleamがErlangライブラリのネイティブラッパーを持っていない場合、FFIは1行で済みます:

// Erlangの:timerモジュールに直接バインド
@external(erlang, "timer", "sleep")
pub fn sleep(milliseconds: Int) -> Nil

// これで通常のGleam関数と同様に使用可能
pub fn delayed_reset(counter_pid) {
  sleep(5000)
  process.send(counter_pid, counter.Reset)
}

Gleamはあなたの次のプロジェクトに適した選択肢か?

Gleam 1.0は2024年3月にリリースされました。コア言語仕様は安定しており固定されています。高い並行性、障害分離、ホットコードリロードを求めるグリーンフィールド分散システムで——静的型安全性も加えて——Gleamは今日の実際のプロダクション選択肢です。

チームがすでにElixirを使用していて動的型付けで問題が発生していないなら、切り替える緊急の理由はありません。しかし新しい分散システムをゼロから始める場合、またはランタイム型エラーで過去に痛い目にあったことがある場合、Gleamは他のいかなる言語も占めていないポジションにいます。静的型付け。プロセス分離。監視ツリー。すべてが一つのパッケージに。

Rustは型安全性を提供しますが、BEAMの軽量プロセスモデルは提供しません。Goは堅実な並行処理を提供しますが、耐障害性監視ツリーは提供しません。Haskellは強力な型システムを提供しますが、数十年の分散システムインフラは提供しません。Gleamはこの3つすべてを提供します——そして一度クラッシュ後に自動的に自己修復するシステムを運用したなら、他の選択肢は妥協に感じられ始めます。

Share: