TT Lab
はじめる
学ぶ 学習パス コース

リーダーが二人いた

リーダーを選ぶルールを自分で書く

TT Labで続きを見る

目標

3つのプロセスが互いに票を交換して、リーダー1つを選ぶ過程を自分で実装します。 終わると、任期(term)、投票、過半数、そしてタイムアウトをなぜランダムにするのかを、手で体験した状態になります。

なぜ重要なのか

合意アルゴリズムが難しい理由は、数式ではなく境界です。任期が同じときと大きいとき、 まだ投票していないときとすでにしたとき、票が半分のときと半分を超えたとき。この境界を 1つずれて引くと、システムは普段は問題なく動いているのに、ネットワークが不安定な日に、リーダーを2つ 作ってしまいます。そのため、ここでは配線(HTTPサーバー・タイマー・再送)を用意してあり、判断する 関数だけをあなたが書きます。判断がそのままアルゴリズムで、残りは配管です。

最後のステップは、Raftの論文がランダムなタイムアウトを導入した理由を、実測で見せてくれます。 3つのノードのタイマーを同じに合わせておくと、3つが同時に候補になり、それぞれ自分に1票ずつ与えて、 誰も過半数に届かないまま、任期だけが際限なく上がります。

ステップ

  1. /root/raft/cluster.jsonに、3つのノードのidとポートを書きます。
  2. /opt/lab/raft/node.pyを/root/raft/node.pyにコピーして、1つを起動し、/statusを確認します。
  3. /root/raft/raftrules.pyに、election_timeout_ms(node_id, fixed_ms)を書きます。
  4. 同じファイルに、on_timeout(state)を追加します。
  5. 同じファイルに、on_request_vote(state, req)を追加します。
  6. 同じファイルに、on_append_entries(state, req)を追加します。
  7. 同じファイルに、has_majority(votes, total)を追加して、3つのノードを起動し、リーダーが選ばれるのを確認します。
  8. 3つのノードを--timeout-ms 900で起動して、結果を/root/raft/split-vote.jsonに書きます。

参考

3つのノードの配置表

/root/raft/cluster.jsonに、id 1、2、3と、ポート5001、5002、5003を書いてください。

ノードはコンテナではなく、ポートだけが異なるプロセスです。後で3回起動するときに使う配置表を、先に書いておきます。nodesというリストの下に、idとportを持つオブジェクトを3つ置いてください。

配線を所定の場所に置いて起動してみる

/opt/lab/raft/node.pyを/root/raft/node.pyにコピーして、1つを起動し、/statusが応答するかを確認してください。

HTTPサーバーとタイマーのような配線は、すでに作ってあります。あなたが埋めるのは「判断」だけです。python3 /root/raft/node.py --id 1 --port 5001 --peers ''で起動して、別のウィンドウでcurl 127.0.0.1:5001/statusを呼び出してください。ルールファイルがまだないので、何の判断もせずにじっとしているのが正常です。

選挙のタイムアウト

/root/raft/raftrules.pyに、election_timeout_ms(node_id, fixed_ms)を書いてください。fixed_msが0でなければその値をそのまま、0なら800から1500までのランダムな値を返します。

random.randint(a, b)は、両端を含みます。なぜランダムでなければならないのかは、最後のステップで自分の目で見ることになります。fixed_msをそのまま返す道を、必ず残しておいてください。その道がなければ、最後のステップの実験が成り立ちません。

タイムアウトになったら候補になる

raftrules.pyに、on_timeout(state)を追加してください。任期を1上げて、stateをcandidateに、voted_forを自分のidに変えた辞書を返します。

stateは、{'id', 'term', 'state', 'voted_for', 'leader', 'log', 'commit'}を持つ辞書です。変更したいキーだけを入れて返せば、配線が反映します。候補が自分に投票することが、Raftの最初の1票です。これを忘れると、誰も過半数に届きません。

1つの任期に1票

raftrules.pyに、on_request_vote(state, req)を追加してください。term、voted_for、state、grantedを含む辞書を返します。

3つのことを順に見てください。(1)reqの任期が自分の任期より大きければ、自分の任期を上げて退き、今回の任期の投票記録を消します。(2)任期が自分のものより小さければ拒否します。このとき、自分の任期を下げてはいけません。(3)同じ任期では、まだ誰にも与えていないか、同じ候補に与えたときだけ与えます。同じ候補の再送を拒否すると、パケットを1回失っただけで選挙が止まります。

ハートビートを受け取る側

raftrules.pyに、on_append_entries(state, req)を追加してください。term、state、leader、voted_for、okを含む辞書を返します。

リーダーは、話すことがなくても話しかけ続けます。それが「私はまだ生きている」という唯一の合図だからです。reqの任期が自分の任期より小さければ、okをFalseにして返し、自分の任期はそのままにします。それ以外の場合は、フォロワーに戻ってリーダーを記録し、okをTrueにします。候補でいるときに、同じ任期のリーダーを見たら、退かなければなりません。

過半数を数えてリーダーになる

raftrules.pyに、has_majority(votes, total)を追加した後、3つのノードを5001、5002、5003で起動して、リーダーが1つ選ばれることを確認してください。

過半数は、半分「以上」ではなく、半分「超過」です。4ノードで2票は過半数ではありません。整数の割り算で書くと、この境界で間違えやすいので、両辺に2を掛けて比較してください。3つのノードを起動した後、curl 127.0.0.1:5001/statusを3つのポートに対して実行して、stateとtermとleaderを一緒に見てください。

タイマーが同じなら、誰も勝てない

3つのノードをすべて--timeout-ms 900で起動して、9秒後の状態を見て、/root/raft/split-vote.jsonに、fixed_timeout_ms、leader、max_term、statesを書いてください。

--timeout-msを指定すると、配線が3つのノードの目覚める時刻を壁時計に合わせます。タイマーが本当に同じときに何が起きるかを見るための仕組みです。3つが同じ瞬間に候補になると、それぞれ自分に1票ずつ与えて、誰も過半数に届かないまま、任期だけが上がります。leaderはnullと書きます。