先日、Twitterを見ていると面白そうな記事が紹介されていました。
少し古い記事ですが、シンプルなロードバランサーをRustで実装してみたとのこと。凄いですね...。
ロードバランサーと聞くと何やら難しそうなイメージがありますが、記事内で実装されているのは2種類のヘルスチェックを行うという非常にシンプルな仕様となっていました。
「これなら自分でも作れるかも...」と恐縮ながら、感じたので作ってみました。
記事ではRustが採用されているのですが、状態管理の排他制御にMutexを使用していますが、排他制御をあまり考えたくないのでElixirを採用しました。
また書き慣れている言語なので、サクッと実装するにピッタリです。
ヘルスチェックについて
記事内で紹介されている2種類のヘルスチェックについて簡単に紹介しておきます。
- passive health check
- active health check
passive health check
5秒に一度、分散先として登録されている全サーバーに対して、定期的なヘルスチェックを行います。
実際に呼び出されるのはGET: /api/v1/health
のような200ステータスをただ返すだけのAPIでしょうか。
分散先として、リクエストを受け付けられない処理できない状況では500系のステータスもしくは、サーバーからの応答がないと思われます。
active health check
定期的なpassive health checkとは異なり、サーバーにリクエストを送信する際に行われるヘルスチェックです。
リクエストの分配は、ラウンドロビンと呼ばれる均等にリクエストを分配するシンプルな方式を採用しました。
サーバーA,B,Cと順番にリクエスト先を変えるだけなので非常にシンプルです。
できたもの
実装したものはGithubにて公開しています。
サーバーの状態管理には構造体とGenServerを使用しました。
Elixirを選定した理由にも書いたように、ElixirはActorモデルに依存しておりMutexを使った排他制御を考える必要がありません。プロセス同士のメッセージパッシングによって状態を安全に管理することが可能であり、便利なライブラリ(eg: GenServer)が標準搭載されています。
defmodule ServerState do use GenServer @enforce_keys [:host, :is_active] defstruct [:host, :is_active] def init(_state) do # サーバーの一覧と次に呼び出すサーバーのindexを保持 servers = [ %ServerState{ host: "http://localhost:3000", is_active: true }, %ServerState{ host: "http://localhost:3001", is_active: true }, %ServerState{ host: "http://localhost:3002", is_active: true }, ] { :ok, { servers, 0 } } end end
ヘルスチェックについて
GenServerに対応するメッセージを送信すると、それぞれのヘルスチェックに対応するライブラリを実行するようにしました。 今回は書いていませんが、単体テストの書きやすさを確保するためにGenServerはライブラリの呼び出しを行い、戻り値をただ記録するだけという実装を意識しました。
def handle_call({ :active_health_check, endpoint, header, body }, _from, state) do { resp, next } = HealthCheck.Active.exec(state, endpoint, header, body) { :reply, resp, next } end def handle_cast(:passive_health_check, { servers, current } ) do next = HealthCheck.Passive.exec(servers) { :noreply, { next, current } } end
Self-Made-LoadBalancer/lib/health_check at master · okabe-yuya/Self-Made-LoadBalancer · GitHub
プロセスについて
メインプロセス(LoadBalancer)とは別にpassive health checkのみを行う専用のプロセスを用意しました。
passive health checkを行うプロセスはメインプロセスの立ち上げ時に、合わせて立ち上げるようにしました。
その後、5秒に一度、passive health checkを再帰を利用して行い続けます。
defmodule LoadBalancer do def launch do { :ok, s_pid } = GenServer.start_link(ServerState, nil, name: ServerState) spawn(fn -> passive_health_check() end) s_pid end def passive_health_check do :timer.sleep(5000) IO.puts(":::Health check...") GenServer.cast(ServerState, :passive_health_check) passive_health_check() end end
最後に
以上、シンプルではありながらロードバランサーを実装することができました。
実際に求められるロードバランサーはキャッシュ、静的ファイル配信...と実装する必要のある機能がたくさんあるでしょうが、その一部だけでも自分で実装できると何だか嬉しくなるものです。
改めて小さくスタートして成功体験を積むことの重要さを感じました。みなさんもぜひ、シンプルなロードバランサーを実装してみてください。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。