はじめに
こんにちは、RetailAIXの@long10langです。さて、advent calendar何書こう?というわけで、あれこれ考えた結果、Raftについて書こうと思い立ちまして、それなら単にRaftの説明をしたところでいまさらしょうがないので、raというErlang実装をご紹介しようと思います。
目次
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 それぞれで依存性の注入を書く というタイトルで発表します!ぜひ、お楽しみください〜