冗長化したConsulサーバクラスタでの問題

Consulはサーバを-bootstrap-expectオプションで3以上(あるいは、設定ファイルでbootstrap_expectの値を3以上)を指定して3台以上起動することによってサーバクラスタとして冗長化することができ、半数以上が停止するまでサーバとして利用可能な状態のままで居続けることができる。
つまり、-bootstrap-expectが3なら1台までサーバが落ちても問題ない。

逆に言えば半数以上のサーバが落ちてしまえば利用できなくなる。1
もちろんそれは仕様通りなのだが、(少なくとも私の環境では)その後再度落ちたサーバを普通に起動してもLeaderの再選定が行われず全員Followerになってしまうため利用可能な状態になってくれない。
これはバグだと思われる。
https://github.com/hashicorp/consul/issues/993

復帰方法

そのような場合に復帰させる方法を試行錯誤して見つけ出したので紹介する。

  • サーバのconsulを全部一旦落とす。
  • -bootstrap-expectを1にしてサーバのconsulを全部起動する。
  • そうするとLeaderが1台、Followerが1台、エラーが1台、という状態になる。
    • 各サーバ上でconsul exec - <<< 'consul info | grep Leader'を実行して「Failed to create session: Unexpected response code: 500 (No cluster leader)」と返ってくればエラー、自分が「finished with exit code 0」ならばLeader、「finished with exit code 1」ならばFollower。
  • まずエラーの1台のconsulを落とし、-bootstrap-expectを3にして起動し直す。続けてFollowerを落として-bootstrap-expectを3にして起動。最後にLeaderを落として-bootstrap-expectを3にして起動。
  • 各クライアントのconsulを再起動。

これをAnsibleで

この復帰方法をAnsibleのplaybookにしてみた。
前提として私の環境は、

consul_members.sh
#!/bin/bash

hosts=$(consul members | sed '1d')
echo "{'members':["
<<< "$hosts" awk '{ print "47" $1 ".node.consul47" }' | paste -s -d,
echo "],'server':["
<<< "$hosts" awk '$4=="server" { print "47" $1 ".node.consul47" }' | paste -s -d,
echo "],'alive':["
<<< "$hosts" awk '$3=="alive" { print "47" $1 ".node.consul47" }' | paste -s -d,
echo "],'_meta':{'hostvars':{"
<<< "$hosts" awk '{ sub(":[0-9]+$","",$2); print "47" $1 ".node.consul47:{47ansible_host47:47" $2 "47}" }' | paste -s -d,
echo "}}}"

このシェルスクリプトの実行結果の例:

{'members':[
'c1.node.consul','s1.node.consul','s2.node.consul','s3.node.consul'
],'server':[
's1.node.consul','s2.node.consul','s3.node.consul'
],'alive':[
'c1.node.consul','s1.node.consul','s2.node.consul','s3.node.consul'
],'_meta':{'hostvars':{
'c1.node.consul':{'ansible_host':'10.18.1.10'},'s1.node.consul':{'ansible_host':'10.18.0.10'},'s2.node.consul':{'ansible_host':'10.18.0.11'},'s3.node.consul':{'ansible_host':'10.18.0.12'}
}}}

playbookは以下の通り。

consul_restart.yml
- hosts: server
  tasks:
  - service: name=consul state=stopped
  - lineinfile: dest=/etc/consul.d/agent.json regexp='^( *"bootstrap_expect":)' line='1 1' backrefs=yes
  - service: name=consul state=started
  - lineinfile: dest=/etc/consul.d/agent.json regexp='^( *"bootstrap_expect":)' line='1 3' backrefs=yes
  - block:
    - shell: consul exec - <<< 'consul info | grep Leader'
      register: result
      retries: 3
      delay: 3
      until: result.rc != 1
      failed_when: result.rc == 1
    rescue:
    - service: name=consul state=restarted
  - service: name=consul state=restarted
    when: "'{{ inventory_hostname_short }}: finished with exit code 1' in result.stdout"
  - service: name=consul state=restarted
    when: "'{{ inventory_hostname_short }}: finished with exit code 0' in result.stdout"

- hosts: "!server"
  tasks:
  - service: name=consul state=restarted

Consulクライアント上でplaybookを実行。
dynamic inventoryのシェルスクリプトには実行権限を付けておく。
Consulサーバ上ではないのはサーバが正常になった時点でconsul membersからクライアントが見えないから。でもサーバもクライアントも全滅してしまっている状況からの復帰だとクライアント上だとconsul membersで自分しか見えないのでサーバ上で実行してクライアントは何か別の方法で再起動しないとならないが。

# chmod +x consul_members.sh
# ansible-playbook -i consul_members.sh consul_restart.yml


  1. 正確には、落ちたのがFollowerだけでLeaderが生き残り続けていけば半数以上が落ちたとしても利用可能な状態のまま居続けることができる。 

TOP