docker swarmとraft
ここ数年のraftの寵児っぷりはすごい。2014年に発表されてから、四半世紀に渡って合意形成アルゴリズム界隈の長だったPaxosに取って代わろうとしている。すでに主要言語ではライブラリの実装が出揃っていて、各所で採用されつつある。
もともとRaftは、理解しやすく実用的なアルゴリズムになるようにデザインされている。背景としてPaxosのアルゴリズムが難解で、加えて細かい部分のスケッチがないため実装難易度が高かったということがあって、それがRaftのモチベーションになっている。Paxosについては学生時代にLeslie Lamportの論文を読んだことがあるけど、証明まで確認するのは結構大変だった。暇のなせる業だと思う。ただ個人的には文書のクセも理解を妨げてきて辛さ倍増だった。
- https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf
- https://lamport.azurewebsites.net/pubs/paxos-simple.pdf
というのでRaftの論文を読んでみたのだけど、Paxosに比べたら格段に簡便化されていた。
ちなみに"Raft"は上の画像にあるように「Replicated And Falt Tolerant」の頭文字だそう。
raftの実装としてはetcd/raft, hashicorp/raftあたりが有名所で、dockerでもetcd/raftがswarmクラスタのManagerノードのFTに使われている。以下は、そのログとスナップショットの動きを観察したメモ。
環境
6台のVMを用意して、01-03をManager、04,05をWorkerとして投下。06はあとでManagerとして追加する。
% for i in {1..6}; do docker-machine create --driver=virtualbox swarm-node$(printf "%02d" $i); done % docker-machine ls --filter="name=swarm*" NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS swarm-node01 - virtualbox Running tcp://192.168.99.107:2376 v17.12.0-ce swarm-node02 - virtualbox Running tcp://192.168.99.108:2376 v17.12.0-ce swarm-node03 - virtualbox Running tcp://192.168.99.109:2376 v17.12.0-ce swarm-node04 - virtualbox Running tcp://192.168.99.110:2376 v17.12.0-ce swarm-node05 - virtualbox Running tcp://192.168.99.111:2376 v17.12.0-ce swarm-node06 - virtualbox Running tcp://192.168.99.112:2376 v17.12.0-ce % docker $(docker-machine config swarm-node01) swarm init --advertise-addr $(docker-machine ip swarm-node01):2377 --listen-addr $(docker-machine ip swarm-node01):2377 % for i in {2..3}; do docker $(docker-machine config swarm-node$(printf "%02d" $i)) swarm join --token=$(docker $(docker-machine config swarm-node01) swarm join-token --quiet manager) $(docker-machine ip swarm-node01):2377; done % for i in {4,5}; do docker $(docker-machine config swarm-node$(printf "%02d" $i)) swarm join --token=$(docker $(docker-machine config swarm-node01) swarm join-token --quiet worker) $(docker-machine ip swarm-node01):2377; done % docker $(docker-machine config swarm-node01) node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS nl4uzxx1xgehafrr1uf9p9fi6 * swarm-node01 Ready Active Leader w9u2hh77f64j08wlk489g89d6 swarm-node02 Ready Active Reachable kng5nc8sswjxsa7qla2mv6nz9 swarm-node03 Ready Active Reachable hc46vfiv597dn2ejnh5yor6oe swarm-node04 Ready Active be166pbo1m79zo9olzn5wv7uw swarm-node05 Ready Active
最右カラムの「MANAGER STATUS」でraftのサーバ状態が確認できる。Leaderはレプリケーションの調停役で、最初に追加したswarm-node01がLeaderになっている。ReachableはraftでいうところのFollowerで、Leaderからのレプリケーション要求を受け付ける。raftのレプリケーションは、Leaderが全Followerに要求を出してクォーラムのACKでコミットという仕組みなので、常にLeader -> Followerの方向で行われる。raftのサーバ状態にはもう一つ、投票を開始したFollowerが遷移するCandidateがあるが、これもReachableに入る。
パラメータ
raft関連のパラメータは以下の4つ。
% docker $(docker-machine config swarm-node01) info | grep -i raft: -A 5 Raft: Snapshot Interval: 10000 Number of Old Snapshots to Retain: 0 Heartbeat Tick: 1 Election Tick: 3 Dispatcher:
リーダ選出のトリガは、LeaderからFollowerに一定間隔で送られるHBの途絶で、タイムアウト値はFollowerがそれぞれセットする。HBの送信間隔 (Heartbeat Tick) はデフォルトだと1チック (秒)でタイムアウト (の基準値) (Election Tick) は3チック (秒) になっている。etcdの実装ではタイムアウト値は[T, 2T-1]のランダム値で、サーバ状態の遷移時にセットされる。
それからraftではクラスタ内の状態変更をログに記録していく。ただ、それが肥大化するのでcompactionのために、定期的にスナップショットを取ってそれ以前のログを削除している。デフォルトではそのインターバル (Snapshot Interval) は10000エントリで、過去分のスナップショットは保持しない (Number of Old Snapshot to Retain) 。
ログとスナップショット
raftのログは以下のディレクトリにある。
- /var/lib/docker/swarm/raft/wal-v3-encrypted
ログは暗号化されているので、swarmkit/swarm-rafttoolで復号化して確認する。
% docker-machine ssh swarm-node01 "sudo rm -rf /tmp/swarmdir && sudo cp -r /var/lib/docker/swarm/ /tmp/swarmdir && sudo chmod -R 0755 /tmp/swarmdir" && docker-machine scp -r swarm-node01:/tmp/swarmdir ./swarm-node01_01 % ~/.go/bin/swarm-rafttool dump-wal -d ./swarm-node01_01 > ./swarm-node01_01.wal % grep -i entry -A 5 swarm-node01_01.wal | head -n 30 Entry Index=1, Term=1, Type=EntryConfChange: Conf change type: ConfChangeAddNode Node ID: 6be9955b56e20ca9 Entry Index=2, Term=2, Type=EntryNormal: Entry Index=3, Term=2, Type=EntryNormal: id: 106274915582209 action: < action: STORE_ACTION_CREATE cluster: < id: "2g1lok9e3428l4r9ue9g25irx" -- Entry Index=4, Term=2, Type=EntryNormal: id: 106274915582210 action: < action: STORE_ACTION_UPDATE node: < id: "nl4uzxx1xgehafrr1uf9p9fi6" -- Entry Index=5, Term=2, Type=EntryNormal: id: 106274915582211 action: < action: STORE_ACTION_UPDATE cluster: < id: "2g1lok9e3428l4r9ue9g25irx" -- Entry Index=6, Term=2, Type=EntryNormal: id: 106274915582212 action: <
それっぽい。各エントリのヘッダにraftのログ・インデックスとターム番号が記録されていて、swarmクラスタ内の状態変更が記録されている。タームというのはリーダの選出を区切りとするraftクラスタの動作期間のこと。眺めてみると、この時点ではサーバの追加に伴うコマンドが発行されていた。このログがサーバ (Manager) 間でレプリケーションされる。
% for i in {1..3}; do docker-machine ssh swarm-node$(printf "%02d" $i) "sudo rm -rf /tmp/swarmdir && sudo cp -r /var/lib/docker/swarm/ /tmp/swarmdir && sudo chmod -R 0755 /tmp/swarmdir" && docker-machine scp -r swarm-node$(printf "%02d" $i):/tmp/swarmdir ./swarm-node$(printf "%02d" $i)_02; done % for i in {1..3}; do ~/.go/bin/swarm-rafttool dump-wal -d ./swarm-node$(printf "%02d" $i)_02 > ./swarm-node$(printf "%02d" $i)_02.wal; done % md5 ./swarm-node*_02.wal MD5 (./swarm-node01_02.wal) = 672a8f26525b288897d0f90aabb37f63 MD5 (./swarm-node02_02.wal) = 672a8f26525b288897d0f90aabb37f63 MD5 (./swarm-node03_02.wal) = 672a8f26525b288897d0f90aabb37f63
スナップショットは、以下のディレクトリにある。
- /var/lib/docker/swarm/raft/snap-v3-encrypted
スナップショットのインターバルがデフォルトの10000では、遊んでいる分にはなかなか到達しないので10回に下げる。
% docker-machine ssh swarm-node01 sudo ls -l /var/lib/docker/swarm/raft/snap-v3-encrypted % docker $(docker-machine config swarm-node01) swarm update --snapshot-interval 10
この時点で一回スナップショットが作成されて、ログもローテートされる。
% docker-machine ssh swarm-node01 sudo ls -l /var/lib/docker/swarm/raft/snap-v3-encrypted total 16 -rw-r--r-- 1 root root 13702 Dec 30 15:37 0000000000000002-0000000000000022.snap % docker-machine ssh swarm-node01 "sudo rm -rf /tmp/swarmdir && sudo cp -r /var/lib/docker/swarm/ /tmp/swarmdir && sudo chmod -R 0755 /tmp/swarmdir" && docker-machine scp -r swarm-node01:/tmp/swarmdir ./swarm-node01_03 % ~/.go/bin/swarm-rafttool dump-wal -d ./swarm-node01_03 > ./swarm-node01_03.wal % wc -l swarm-node01_03.wal 0 swarm-node01_03.wal
試しにサービスをデプロイしてログを進めると、指定したインターバルで更新される。
% docker $(docker-machine config swarm-node01) service create --name nginx nginx % docker $(docker-machine config swarm-node01) service update --publish-add=8080:80 nginx % docker-machine ssh swarm-node01 sudo ls -l /var/lib/docker/swarm/raft/snap-v3-encrypted total 16 -rw-r--r-- 1 root root 15033 Dec 30 15:39 0000000000000002-000000000000002c.snap % docker-machine ssh swarm-node01 sudo ls -l /var/lib/docker/swarm/raft/wal-v3-encrypted total 125000 -rw------- 1 root root 64000000 Dec 30 15:33 0.tmp -rw------- 1 root root 64000000 Dec 30 15:39 0000000000000000-0000000000000000.wal % docker-machine ssh swarm-node01 "sudo rm -rf /tmp/swarmdir && sudo cp -r /var/lib/docker/swarm/ /tmp/swarmdir && sudo chmod -R 0755 /tmp/swarmdir" && docker-machine scp -r swarm-node01:/tmp/swarmdir ./swarm-node01_04 % ~/.go/bin/swarm-rafttool dump-wal -d ./swarm-node01_04 > ./swarm-node01_04.wal % wc -l swarm-node01_*.wal 2781 swarm-node01_01.wal 2781 swarm-node01_02.wal 0 swarm-node01_03.wal 1358 swarm-node01_04.wal 6920 total % grep -i entry swarm-node01_02.wal | tail -n 1 Entry Index=33, Term=2, Type=EntryNormal: % grep -i entry swarm-node01_04.wal | head -n 1 Entry Index=45, Term=2, Type=EntryNormal:
スナップショットもログと同じツールでダンプすることができて、クラスタで管理しているオブジェクトの状態が記録されている。
% ~/.go/bin/swarm-rafttool dump-snapshot --state-dir ./swarm-node01_04 | grep -i "^[a-z]" Active members: Removed members: Objects: nodes: < nodes: < nodes: < nodes: < nodes: < services: < networks: < networks: < networks: < tasks: < tasks: < clusters: <
スナップショットは基本的にサーバごとに独立して取得されるが、レプリケーションが著しく遅延 (?) しているFollowerや新規に追加されたFollowerには、Leaderからログとスナップショットが送信される。試しにManagerノードを新しく追加してみる。
% docker $(docker-machine config swarm-node01) swarm update --snapshot-interval 50 % docker $(docker-machine config swarm-node06) swarm join --token=$(docker $(docker-machine config swarm-node01) swarm join-token --quiet manager) $(docker-machine ip swarm-node01):2377 % docker-machine ssh swarm-node01 sudo ls -l /var/lib/docker/swarm/raft/snap-v3-encrypted total 16 -rw-r--r-- 1 root root 15482 Dec 30 15:42 0000000000000002-0000000000000036.snap % docker-machine ssh swarm-node06 sudo ls -l /var/lib/docker/swarm/raft/snap-v3-encrypted total 20 -rw-r--r-- 1 root root 18099 Dec 30 15:42 0000000000000002-000000000000003c.snap
# 最初にインターバルを変更しているのはノードの追加でスナップショットが更新されないようにするためで、現在のログ・インデックスより大きいとスナップショットの転送は不要と判断されてしまうため50にしている。
スナップショットのサイズに差があるが、Leaderのスナップショット + ログ分が追加したFollowerのスナップショットに入っている。
% for i in {1,6}; do docker-machine ssh swarm-node$(printf "%02d" $i) "sudo rm -rf /tmp/swarmdir && sudo cp -r /var/lib/docker/swarm/ /tmp/swarmdir && sudo chmod -R 0755 /tmp/swarmdir" && docker-machine scp -r swarm-node$(printf "%02d" $i):/tmp/swarmdir ./swarm-node$(printf "%02d" $i)_05; done % for i in {1,6}; do ~/.go/bin/swarm-rafttool dump-wal -d ./swarm-node$(printf "%02d" $i)_05 > ./swarm-node$(printf "%02d" $i)_05.wal; done % for i in {1,6}; do ~/.go/bin/swarm-rafttool dump-snapshot -d ./swarm-node$(printf "%02d" $i)_05 > ./swarm-node$(printf "%02d" $i)_05.snap; done % wc -l swarm-node06_05.wal 0 swarm-node06_05.wal % grep -i index swarm-node06_05.snap index: 32 index: 6 index: 27 index: 6 index: 22 index: 6 index: 9 index: 6 index: 58 index: 6 index: 16 index: 6 index: 52 index: 34 index: 6 index: 7 index: 7 index: 51 index: 34 index: 6 index: 48 index: 53 % grep -i entry swarm-node01_05.wal Entry Index=55, Term=2, Type=EntryNormal: Entry Index=56, Term=2, Type=EntryNormal: Entry Index=57, Term=2, Type=EntryNormal: Entry Index=58, Term=2, Type=EntryNormal: Entry Index=59, Term=2, Type=EntryNormal: Entry Index=60, Term=2, Type=EntryConfChange: % grep -i index swarm-node01_05.snap index: 32 index: 6 index: 27 index: 6 index: 22 index: 6 index: 9 index: 6 index: 16 index: 6 index: 52 index: 34 index: 6 index: 7 index: 7 index: 51 index: 34 index: 6 index: 48 index: 53
という感じである程度は観察できるけど、まともにログを吐いてくれないしメッセージをダンプしようにもprotobufなので、あとは実装を見た方が楽。
おまけ
swarmkit/swarm-rafttoolでは、特定のオブジェクトをログとスナップショットからダンプすることもできる。ただ、用途は不明。
% ~/.go/bin/swarm-rafttool dump-object network -d swarm-node01_01 | head -n 10 id: "61sbtvw4rucebjjzgxolkju8u" meta: < version: < index: 6 > created_at: < seconds: 1514648016 nanos: 774799363 > updated_at: < % ~/.go/bin/swarm-rafttool dump-object network --name ingress -d swarm-node01_01 | head -n 10 id: "61sbtvw4rucebjjzgxolkju8u" meta: < version: < index: 6 > created_at: < seconds: 1514648016 nanos: 774799363 > updated_at: <
その他のtypeは以下が指定できる。