あるクラスタ内に複数ノードがあり、そのうち高々 1 ノードのみがサービスを提供する仕組みを作りたいと思いました。 これは etcd を使うとかんたんに実装できるんじゃないかとおもい試してみました。
etcd には compare and swap の API があり、キーの前の値や無い時のみにキーを更新できます。 それを使ってノード間で leader election して、leader になった後にサービスを起動する仕組みを考えました。 他の場所でもよくみる方法だったので、これを使えば実現できるんじゃないかと思い実装してみました。
- https://github.com/coreos/etcd/issues/225
- https://www.sandtable.com/etcd3-leader-election-using-python/
試しに次のようなシェルスクリプトを書いてみました。 結果から言うと、これは正しく動きません。
#!/bin/sh
ID=$(uuidgen)
LEADER_KEY="localhost:2379/v2/keys/master"
TTL=5
INTERVAL=1
i_am_leader() {
echo "I am leader";
while true; do
body=$(curl -sSL -XPUT "${LEADER_KEY}?prevValue=${ID}&ttl=${TTL}&value=${ID}")
[ $? != 0 ] && return
errorCode=$(echo $body | jq -r .errorCode)
if [ $errorCode != 'null' ]; then
echo "something fail"
exit 1
fi
# do something
sleep $INTERVAL
done
}
try_to_be_leader() {
echo "wait for leader dying"
while true; do
body=$(curl -sSL -XPUT "${LEADER_KEY}?prevExist=false&ttl=${TTL}&value=${ID}")
[ $? != 0 ] && return
errorCode=$(echo $body | jq -r .errorCode)
if [ $errorCode = 'null' ]; then
i_am_leader
fi
sleep $INTERVAL
done
}
try_to_be_leader
etcd を使った leader election では、TTL を使って leader を表す key を作り、クライアントは TTL 未満でそのノードを上書きし続けます。 しかしこの方法には問題があり、あるノードが leader を獲得してから処理を始めるまでに(do something までに)ノードが TTL の時間以上停止してしまうと etcd から key が消えます。 その間に他のノードが leader になってしまい、結果として複数ノードが同時に do something の部分を実行します。
TTL ベースで lock を取ると上記のことは避けられないようで、別の箇所で回避する必要があります。 Kubernetes や Kafka では楽観的ロックを使って複数からのデータ更新を防いでます。 うーん分散システムは難しい。