docker swarmとraft

f:id:shkh:20171231031951p:plain

ここ数年のraftの寵児っぷりはすごい。2014年に発表されてから、四半世紀に渡って合意形成アルゴリズム界隈の長だったPaxosに取って代わろうとしている。すでに主要言語ではライブラリの実装が出揃っていて、各所で採用されつつある。

もともとRaftは、理解しやすく実用的なアルゴリズムになるようにデザインされている。背景としてPaxosのアルゴリズムが難解で、加えて細かい部分のスケッチがないため実装難易度が高かったということがあって、それがRaftのモチベーションになっている。Paxosについては学生時代にLeslie Lamportの論文を読んだことがあるけど、証明まで確認するのは結構大変だった。暇のなせる業だと思う。ただ個人的には文書のクセも理解を妨げてきて辛さ倍増だった。

というので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は以下が指定できる。