ElixirのFailoverとTakeover

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

アプリケーションに何らかの障害が発生した場合に、アプリケーションを別のノードで立ち上げ直す(Failover)と、復旧した時にアプリケーションを元のノードに切り戻す(Takeover)機能があれば信頼性が向上します。
今回はFailoverとTakeoverをElixirのアプリケーションで試してみます。

概要

簡単にFailoverとTakeoverの説明をします。
以下はノードA、 ノードB、 ノードCでクラスタを組んでいる場合のFailoverとTakeoverの例です。

通常時はノードAで起動
failover-1

ノードAで障害が発生した場合にノードBでアプリケーションが立ち上がります。これがFailoverです。 failover-2

ノードBでも障害が発生した場合、今度はノードCで起動します。 failover-3

そして、ノードAが復旧すると別のノードで起動していたアプリケーションがノードAで起動し直します。これがTakeoverです。
failover-4

クラスタの構築

通常時にアプリケーションを稼働させるマスターノードと障害時にアプリケーションを切り替えるスレーブノードを設定します。
今回はサーバ2台での構成で試します。サーバAではマスターノード、サーバBではスレーブノードの2ノードが待機系として起動します。

ノード毎に設定ファイルを用意します。
以下はmasterノードの設定ファイルです。
config/master.config

[{kernel,
  [{distributed, [{wikipedy, 5000, ['master@198.51.100.0', {'slave-a@203.0.113.0', 'slave-b@203.0.113.0'}]}]},
   {sync_nodes_mandatory, ['slave-a@203.0.113.0', 'slave-b@203.0.113.0']},
   {sync_nodes_timeout, 30000}
  ]}].

{distributed, [{wikipedy, 5000, ['master@198.51.100.0', {'slave-a@203.0.113.0', 'slave-b@203.0.113.0'}]}]} この行は以下の内容を指定しています。

  • アプリケーション名
  • ノードがダウンしたと判定するまでのミリ秒
  • クラスタを構築するノードをそれぞれ指定しています

(クラスタを構築するノードの指定では優先度も指定しています。この設定値では、優先度が一番高いノードがmaster@198.51.100.0でその次に slave-a@203.0.113.0 もしくは slave-b@203.0.113.0 が続きます)

sync_nodes_mandatoryとsync_nodes_timeoutはそれぞれ以下の意味です。

sync_nodes_mandatory ここに指定したノードが起動するまでアプリケーションの起動を待ちます
sync_nodes_timeout ここで指定したタイムアウト値まで指定したノードが起動してこないとアプリケーションの起動を諦めてクラッシュします

同様にslaveノードの設定ファイルも用意します。2ノードそれぞれのファイルが必要です。
config/slave-a.config

[{kernel,
  [{distributed, [{wikipedy, 5000, ['master@198.51.100.0', {'slave-a@203.0.113.0', 'slave-b@203.0.113.0'}]}]},
   {sync_nodes_mandatory, ['master@198.51.100.0', 'slave-b@203.0.113.0']},
   {sync_nodes_timeout, 30000}
  ]}].

config/slave-b.config

[{kernel,
  [{distributed, [{wikipedy, 5000, ['master@198.51.100.0', {'slave-a@203.0.113.0', 'slave-b@203.0.113.0'}]}]},
   {sync_nodes_mandatory, ['master@198.51.100.0', 'slave-a@203.0.113.0']},
   {sync_nodes_timeout, 30000}
  ]}].

アプリケーションの起動タイプ

アプリケーション起動時に渡されるtypeの値が通常起動時、フェイルオーバー時、テイクオーバー時で異なります。

引数 内容
:normal 通常の起動に渡される
{:faliover, node} アプリケーションがフェイルオーバーした時に渡される

パターンマッチで引数を判定しそれぞれの場合の特有の処理を入れることができます。
ここではわかりやすくログ出力をそれぞれのパターン毎に入れます。

lib/wikipedy.ex

defmodule Wikipedy do
  use Application
  require Logger

  def start(type, _args) do
    import Supervisor.Spec
    children = [
      worker(Wikipedy.Server, [])
    ]

    case type do
      :normal ->
        Logger.info """

        ##################################################
        Application is started on #{node}
        ##################################################
        """

      {:takeover, old_node} ->
        Logger.info """

        ###################################################
        #{node} is taking over #{old_node}
        ###################################################
        """

      {:failover, old_node} ->
        Logger.info """

        #########################################################
        #{old_node} is failing over to #{node}
        #########################################################
        """
    end

    opts = [strategy: :one_for_one, name: {:global, Wikipedy.Supervisor}]
    Supervisor.start_link(children, opts)
  end
end

あとはアプリケーションロジックの実装です。
なんでも良いのですが、ノードの切り替えが分かりやすいように一定間隔で標準出力するプログラムにします。
Wikipediaの「本日の出来事」を取得してその中の1件をランダムに取得して表示します。

lib/wikipedy/server.ex

defmodule Wikipedy.Server do
  use GenServer

  # API
  def start_link do
    GenServer.start_link(__MODULE__, [], [name: {:global, __MODULE__}])
  end

  def init([]) do
    :random.seed(:os.timestamp)
    my_loop
    {:ok, []}
  end

  def my_loop do
    # 1.5秒に1回、「本日の出来事」からランダムな1剣を取得して標準出力する関数を呼び出します
    Process.send_after(self, :today_topic, 1_500)
  end

  def handle_info(:today_topic, topics) do
    topics =
      case Enum.empty?(topics) do
        true ->
          case Wikipedy.TopicFetcher.topics do
            {:ok, list} -> list
            {:error, reason} ->
              IO.puts(reason)
              topics

          end
        false -> topics
      end

    IO.puts(Enum.random(topics))
    my_loop
    {:noreply, topics }
  end
end

Wikipediaからスクレイピングで「本日の出来事」を取得するプログラムです。

lib/wikipedy/topic_fetcher.ex

defmodule Wikipedy.TopicFetcher do
  def topics do
    {{_,m,d}, _} = :calendar.local_time
    case HTTPoison.get(URI.encode("https://ja.wikipedia.org/wiki/#{m}月#{d}日")) do
      {:ok, %HTTPoison.Response{body: body}} ->
        topic_elms = String.split(body, "</h2>") |> Enum.at(2)

        topics =
          Floki.find(topic_elms, "mw-content-text, ul")
          |> Enum.at(0)
          |> Floki.text
          |> String.split("。")

        {:ok, topics}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

今回使用するライブラリをmix.exsの依存ライブラリに追加します。

mix.exs

defp deps do
  [
    {:httpoison, "~> 0.9.0"},
    {:floki, "~> 0.10.0"},
    {:distillery, ">= 0.8.0", warn_missing: false},
  ]
end

distillery は、ビルドで使用するために追加しています。

ビルド

今回はEC2上で試します。セキュリティグループの設定が必要になりますので、以下のページを参考に設定してください。
Elixirのノード接続とメッセージ通信

それではアプリケーションのビルドを行います。
まず初期化コマンドを実行して設定ファイルを作成します。

$ mix release.init

次のコマンドでビルドします。

$ mix release

_build/prod/rel/アプリケーション名/release/0.1.0にtarファイルが出来ます。
このファイルをサーバのアプリケーション実行ディレクトリに解凍してください。

VMパラメータ設定ファイル

解凍したアプリケーション配下のreleases/0.1.0ディレクトリにvm.argsファイルがあります。
このファイルにアプリケーション実行時のパラメータを指定します。

vm.args

## 実行するノード名を指定します
-name master@198.51.100.0

## ここに指定するクッキーの値は全ノードで共通の値にします
-setcookie secret

## クラスタ構築するために作成した設定ファイルのパスを指定します。それぞれのノード毎にファイルを指定します
-config /home/ec2-user/deploy/master.config

## AWSのセキュリティグループに設定したポート番号を指定します
-kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155

同じようにslave-a、slave-bのvm.argsにも設定します

実行

それでは、EC2上で実行してみます。
masterノードとslaveノードがそれぞれ別サーバで動きます。slaveノードは二つ起動します。
REPLを起動してみましょう(3ノード起動します)。

$ bin/wikipedy console

うまくFailover, Takeover出来ました。
処理は以下の流れで進みます。  

順番 内容
1 最初、masterノードでアプリケーションが起動します
2 masterノードのREPLを強制終了すると、一つ目のslaveノード上でアプリケーションがが起動します(Failover)
3 slaveノードのREPLも強制終了すると、二つ目のslaveノード上でアプリケーションが起動します(Failover)
4 masterノードのREPLを再度起動すると、アプリケーションがmasterノードで起動し直します(Takeover)

Failover、Takeoverの機能を実現するためのミドルウェアはすでに多くあると思いますが、それが言語のランタイムに含まれているのはすごいですね。
興味がある方は是非試してみてください。

参考情報

The little Elixir & OTP Guidebook
プログラミングElixir
すごいErlangゆかいに学ぼう!