やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirでシンプルなロードバランサーを実装してみた

先日、Twitterを見ていると面白そうな記事が紹介されていました。
少し古い記事ですが、シンプルなロードバランサーをRustで実装してみたとのこと。凄いですね...。

qiita.com

ロードバランサーと聞くと何やら難しそうなイメージがありますが、記事内で実装されているのは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にて公開しています。

github.com

サーバーの状態管理には構造体と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

最後に

以上、シンプルではありながらロードバランサーを実装することができました。
実際に求められるロードバランサーはキャッシュ、静的ファイル配信...と実装する必要のある機能がたくさんあるでしょうが、その一部だけでも自分で実装できると何だか嬉しくなるものです。
改めて小さくスタートして成功体験を積むことの重要さを感じました。みなさんもぜひ、シンプルなロードバランサーを実装してみてください。

qiita.com

少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。