LoginSignup
21
4

More than 1 year has passed since last update.

RaftのErlang実装ことはじめ

Last updated at Posted at 2022-11-30

はじめに

こんにちは、RetailAIXの@long10langです。さて、advent calendar何書こう?というわけで、あれこれ考えた結果、Raftについて書こうと思い立ちまして、それなら単にRaftの説明をしたところでいまさらしょうがないので、raというErlang実装をご紹介しようと思います。

目次

  1. Raftについて
  2. raの導入
  3. raの使い方
  4. まとめ
  5. 参考文献

Raftについて

とはいえ、まずRaftについても少しご紹介しておきましょう。Raftとは、コンセンサスアルゴリズムのひとつで、コンセンサスアルゴリズムというのは、平たくいえば「複数の処理が走っていたとしてもデータの整合性をちゃんと合わせるための方法」のことです。こちらのアニメーションによる説明がとてもわかりやすいです。

詳しい説明は抜きにして、なぜいくつもあるRaft実装のうち、Erlang実装を紹介するのかというと、やはりOTPが落ちにくいという点に魅力があります。Raftがいくらデータの整合性を保ったとしても実装自体が頻繁に落ちてしまっては、結局データ欠損を免れません。そこで、どのくらい実用性があるかどうかはさておき、可能性を探るべく調査をしてみましたので、その辺りをご紹介できればというのが、こちらの記事の発端です。

raの導入

ではまず、しのごのと言わずraを利用できるようにしていきましょう。環境はWSL2で行なっていきます。

$ git clone https://github.com/rabbitmq/ra.git

環境構築に必要なやつらをインストールします。この辺りはすでに入っている場合は不要です。

$ sudo apt install libssl-dev automake autoconf libncurses5-dev gcc

次に、kerlを入れてやります。kerlはマジ便利ツールですので、ぜひ入れてください。

$ curl -O https://raw.githubusercontent.com/kerl/kerl/master/kerl
#実行権限もつけましょう
$ chmod a+x kerl

これで、kerlが利用できるようになったか確認します。バージョンのリストが表示されたらOKです。

$ ./kerl list releases

それでは、Erlang/OTPをインストールしていきましょう。今日(2022.12.1)時点での最新バージョンは、25.1.2ですので、そちらをインストールしたいと思います。まずはビルドします。大体、終わるまでに2、3分かかります。

$ ./kerl build 25.1.2
...
#終わるとこういった文言が表示されます。
Erlang/OTP 25.1.2 (25.1.2) has been successfully built

Erlangがちゃんとビルドされたかどうか確認します。

$ ./kerl list builds
25.1.2,25.1.2

はい、ちゃんとビルドされていますね。では、こちらをインストールしたいと思います。

$ ./kerl install 25.1.2 ~/kerl/25.1.2
Installing Erlang/OTP 25.1.2 (25.1.2) in /home/user/kerl/25.1.2...
You can activate this installation running the following command:
. /home/user/kerl/25.1.2/activate.fish
Later on, you can leave the installation typing:
kerl_deactivate

ぼくの場合は、shellにfishを使っているので、こんな感じの指示が表示されます。その通りに実施した後、ちゃんとインストールされたか確認してみましょう。

./kerl list installations
25.1.2 /home/user/kerl/25.1.2

出ました。これでようやくErlangのインストールが終了です。ちょっと遊んでみましょう。erlとコマンドを打つとインタラクティブシェルが起動します。

$ erl
Erlang/OTP 25 [erts-13.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]

Eshell V13.1.2  (abort with ^G)
1>

ここで、ちょこっと演算などやってみます。「.」をお忘れなく。

1> 5+7.
12
2> Num = 4+12.
16
3> Num + 8.
24
4> q().
ok
5>

ここで、せっかくなのでElixirも導入したいと思います。raを使う上でErlangよりもElixirの方が色々と使い勝手が良いからです。kerlと同様、Elixirのバージョン管理ツールKiexも入れましょう。

curl -sSL https://raw.githubusercontent.com/taylor/kiex/master/install | bash -s

こちらも、ちゃんとインストールされていれば、以下のコマンドでバージョン一覧が表示されるはずです。

$ kiex list known
...
    1.13.4
    1.14.0
    1.14.0-rc.0
    1.14.0-rc.1
    1.14.1
    1.14.2

Elixirの方は、今日(2022.12.1)の最新版が1.14.2なので、こちらをインストールしましょう。

kiex install 1.14.2
...
rm -f man/elixir.1
rm -f man/elixir.1.bak
rm -f man/iex.1
rm -f man/iex.1.bak
make[2]: Leaving directory '/home/long/.kiex/builds/elixir-git'
make[1]: Leaving directory '/home/long/.kiex/builds/elixir-git'
Installed Elixir version 1.14.2
Load with:
           kiex use 1.14.2
or load the elixir environment file with:
   source $HOME/.kiex/elixirs/.elixir-1.14.2.env.fish

はい、インストールされました。これまた指示通り、使えるように設定しましょう。そして、以下のコマンドを打って問題なく表示されたらElixirのインストールもOKです。

$ elixir --version
Erlang/OTP 25 [erts-13.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]

Elixir 1.14.2 (compiled with Erlang/OTP 25)

Elixirもちょこっと演算など。

$ iex

Erlang/OTP 25 [erts-13.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]

Interactive Elixir (1.14.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 1+1
2
iex(2)> 3-1
2
iex(3)>

ではここで、rebar3をインストールします。

$ git clone https://github.com/erlang/rebar3.git
$ cd rebar3
$ ./bootstrap
$ ./rebar3 local install

ここでもまた、シェルにパスを追加して使えるようにしましょう。そして、rebar3が使えるようになったことを以下のコマンドで確認します。

rebar3 local upgrade

はい、これでようやくra導入の準備が整いました。それでは、本題のraを使ったことはじめと行きたいと思います。

raの使い方

ではまず、raをgit cloneしてきます。

git clone https://github.com/rabbitmq/ra.git
cd ra

次に、各clusterを起動していきます。tmuxなどを使って別ペインで確認すると便利です。

# cluster1を起動する
$ rebar3 shell --name ra1@127.0.0.1

# cluster2を起動する
$ rebar3 shell --name ra2@127.0.0.1

# cluster3を起動する
$ rebar3 shell --name ra3@127.0.0.1

clusterを起動した後に、raアプリケーションをそれぞれ起動します。

# cluster1でraアプリケーションを起動する
ra:start().
% => ok

# cluster2でraアプリケーションを起動する
ra:start().
% => ok

# cluster3でraアプリケーションを起動する
ra:start().
% => ok

起動が全て確認できたら、cluster2のペインを開き、まず手始めにcluster2のメンバを追加してみます。

ClusterName = dyn_members,
Machine = {simple, fun erlang:'+'/2, 0},

{ok, _, _} =  ra:start_cluster(default, ClusterName, Machine, [{dyn_members, 'ra2@127.0.0.1'}]).
% => ok

次に、cluster1に向けてメンバを追加します。同じくcluster2のペインからメンバを追加してください。

{ok, _, _} = ra:add_member({dyn_members, 'ra2@127.0.0.1'}, {dyn_members, 'ra1@127.0.0.1'}),
ok = ra:start_server(default, ClusterName, {dyn_members, 'ra1@127.0.0.1'}, Machine, [{dyn_members, 'ra2@127.0.0.1'}]).
% => ok

そして最後に、cluster3に向けてメンバを追加します。

{ok, _, _} = ra:add_member({dyn_members, 'ra2@127.0.0.1'}, {dyn_members, 'ra3@127.0.0.1'}),
ok = ra:start_server(default, ClusterName, {dyn_members, 'ra3@127.0.0.1'}, Machine, [{dyn_members, 'ra2@127.0.0.1'}]).
% => ok

はい、これで追加したメンバたちを各clusterから確認すると、ちゃんと表示されることを確認してみましょう。

# cluster1,cluster2,cluster3それぞれから追加したメンバが確認できます。
(ra1@127.0.0.1)5> ra:members({dyn_members, node()}).
{ok,[{dyn_members,'ra1@127.0.0.1'},
     {dyn_members,'ra2@127.0.0.1'},
     {dyn_members,'ra3@127.0.0.1'}],
    {dyn_members,'ra2@127.0.0.1'}}

はい、できてますね。

ついでに、せっかくなんでElixirでもclusterを立ち上げてErlangのクラスタとメンバを共有してみようと思います。Elixirでは、まずraを導入するにあたって、以下のMix.exsファイルを用意する必要があります。このファイルを作ったらraのディレクトリの中にとりあえず置いておきましょう。

defmodule KV do
  use Mix.Project

  def project do
    [
      app: :kv,
      version: "0.1.0",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      {:ra, git: "https://github.com/rabbitmq/ra.git", tag: "v2.4.0"}
    ]
  end
end

それでは、以下のiexコマンドでcluster4を起動してみます。普通に起動してもraを読み込めないので、先ほどの定義を-S mixオプションで読み込む必要があります。

$ iex --name ra4@127.0.0.1 -S mix

そして、cluster起動後にraアプリケーションを起動します。

# start cluster
:ra.start()

再び、cluster2のペインを開きElixirで立ち上げたra4clusterのメンバを追加します。

{ok, _, _} = ra:add_member({dyn_members, 'ra2@127.0.0.1'}, {dyn_members, 'ra4@127.0.0.1'}),
ok = ra:start_server(default, ClusterName, {dyn_members, 'ra4@127.0.0.1'}, Machine, [{dyn_members, 'ra2@127.0.0.1'}]).
% => ok

その後、cluster4ペインに戻りメンバを確認します。

# Check the members from any node
iex(ra4@127.0.0.1)3> :ra.members({:dyn_members, node()})
{:ok,
 [
   dyn_members: :"ra1@127.0.0.1",
   dyn_members: :"ra2@127.0.0.1",
   dyn_members: :"ra3@127.0.0.1",
   dyn_members: :"ra4@127.0.0.1"
 ], {:dyn_members, :"ra3@127.0.0.1"}}

おお、ちゃんと追加されてますね!

まとめ

いかがでしたでしょうか?raは直感的で使いやすいですが、APIはあまりまだ多くない印象でした。Elixirで記述できるところが嬉しいですね。スナップショットの復元など実装が捗りそうです。
ただ、RabbitMQチームは、RabbitMQ3.8以降で、Quorum QueuesというRaft実装を採用していて、raとの棲み分けってどうなんだろう?(あるいは、raはQuorum Queuesのラッパー?)というのが気になりました。

さて明日は、サーバサイドエンジニアの@kanto-mizoさんが、Ruby と Go それぞれで依存性の注入を書く というタイトルで発表します!ぜひ、お楽しみください〜

参考文献

21
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
4