Serf 虎の巻
サービスディスカバリーとオーケストレーション用のツールであるSerfについてまとめた.基本的には公式のHPのGetting Startの抄訳.Vagrantで試験環境を立てて実際に触りつつSerfを使い始められるようにした.
目次
Serfとは
Serfはサービスディスカバリーやオーケストレーション,障害検出のためのツール.Vagrantの開発者であるMitchell Hashimoto氏により開発が進められている.SerfはImmutable Infrastructureの文脈で登場してきたツールであり,Immutableなシステムアーキテクチャー,デプロイを実現する上で必須のツールである.
Immutable Infrastructureを簡単に説明すると,上書き的にサーバーを更新するのではなく,デプロイの度に1からにサーバ,イメージを構築してしまおうという考え方.現段階では,ChefやPuppet,AnsibleのようなConfiguration toolでソフトウェア,サービスの設定を行いイメージを作成し,テストが完了した段階でロードバランサを切り替えるというワークフローが提唱されている(Blue Green Deployment).もしくは,Dockerなどのコンテナベースであれば,そのポータビリティにより,ローカルでコンテナをつくって,それをそのままプロダクションデプロイする方法も考えられる.
このとき問題になるのは,ロードバランサへの追加や,Memcacheのクラスタ,MySQLのslave/masterなどの動的に変わるような設定.もちろんChefやPuppetがこれらの設定まで受け持つことは可能であるが,Immutableなデプロイを実現する上では複雑性が増す.
これを解決するのがSerf.ChefやPuppetで不変なサーバ,イメージが完成したあとに,それらのサーバ,イメージ間の紐付けやクラスタリングを行う.
Serfができること
Serfは,大きく以下の3つのことを行うことができる.
- クラスタリング: Serfはクラスタを形成し,クラスタへメンバーの参加,離脱といったイベントを検出して,メンバーそれぞれにあらかじめ設定したスクリプトを実行させることができる.例えば,SerfはロードバランサのためのWebサーバのリストをもち,ノードの増減の度にロードバランサにそれを通知することができる.
- 障害の検出と回復: Serfはクラスタのメンバーが障害で落ちた場合にそれを検出し,残りのメンバーにそれを通知することができる.また,障害によりダウンしたメンバーを再びクラスタに参加させるように働く.
- イベントの伝搬: Serfはメンバーの参加,離脱といったイベント以外にオリジナルのカスタムイベントをメンバーに伝搬させることができる.これらは,デプロイやConfigurationのトリガーなどに使うことができる.
Serfの利用例
具体的なSerfの利用例には以下のようなものがある.
詳細は,公式のUse Casesを参照.
Gossip Protocolとは
Serfはクラスタのメンバーへのイベントの伝搬にGossip Protocolを用いている.Gossip Protocolは“SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol”を基にしており,SWIMのイベントの伝搬速度とカバレッジに改良を加えている.
SWIM Protocolの概要
Serfは新しいクラスタの形成,既存のクラスタへ参入,のどちらかで起動する.新しいクラスタが形成されると,そこには新しいノードが参入してくることが期待される.既存のクラスタに参入するには,既存クラスタのメンバーのIPアドレスが必要になる.新しいメンバーはTCPで既存クラスタのメンバーと状態が同期され,Gossiping(噂,情報のやりとり)が始まる.
GossipingはUDPで通信される.これにより,ネットワークの容量はノードの数に比例して一定になる.Gossipingよりも頻度は低いが,定期的にTCPによるランダムなノード間で完全な状態同期が行われる.
障害検出は,定期的にランダムなノードをチェックすることにより行われる.もし一定期間あるノードから反応がない場合は,直接そのノードに対してチェックが行われる.ネットワーク上問題でノードからの反応が得られていない可能性を考慮して,この直接のチェックは複数のノードから行われる.ランダムなチェックおよび,直接のチェックでも反応がない場合,そのノードは,suspiciousと認定される.suspiciousであってもそのノードはクラスタの一員として扱われる.それでも反応が慣れれば,そのノードは落ちたと認定され,それは他のノードにGossipされる.
GossipのSWIMからの改良点
Gossip ProtocolのSWIMからの変更点は大きく以下の3点
- SerfはTCPで全状態の同期を行うが,SWIMは変更をGossipすることしかしない.最終的には,どちらも一貫性を持つが,Serfは状態の収束が速い.
- SerfはGossipingのみを行うレイヤーと障害検出を行うプロトコルを分離しているが,SWIMは,障害検出にGossipingが上乗りしている.Serfは上乗りもしている.これによりSerfはより速いGossipingを可能にする.
- Serfは落ちたノードを一定期間保持するため,全状態の同期の際にそれも伝搬される.SWIMはTCPによる状態の同期を行わない.これにより障害からの復帰が速くなる.
試験環境の準備
serf/demo/vagrant-clusterのVagrantfileを改良して,Serfがプレインストールされたノードが3つ立ち上がった試験環境を作る.ノードのIPはそれぞれ”172.20.20.10”,”172.20.20.11”,”172.20.20.12”とし,同一ネットワーク上に存在する.Vagrantfileは以下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
立ち上げる.
1 2 3 4 5 6 7 |
|
クラスタの形成
シンプルなクラスタを形成してみる.
エージェントの起動
まず,n1
で最初のエージェント(agent1
)を起動する.同一ネットワーク上で発見されるように,bindアドレスにはprivate networkのIPを指定する.
1 2 3 4 5 6 7 8 9 10 11 |
|
次に,別のウィンドウを立ち上げてn2
で新たなエージェント(agent2
)を起動する.
1 2 3 4 5 6 7 8 9 10 11 |
|
この時点で,2つのホストで2つのserfエージェントが起動してる.しかし,2つのエージェントは互いについては何も知らない.それぞれが自分自身のクラスタを形成している.serf member
を実行するとそれを確認できる.
1 2 |
|
1 2 |
|
クラスタへのJoin
クラスタにjoinしてみる.agent2
をagent1
にjoinさせる.
1 2 |
|
それぞれのエージェントのログをみると,メンバーのjoin情報(EventMemberJoin
)を互いに受け取っていることが確認できる.
1 2 |
|
1 2 3 4 |
|
それぞれのエージェントでserf member
を実行すると,それぞれのエージェントが互いのことを認識していることが確認できる.
1 2 3 |
|
1 2 3 |
|
さらに別のウィンドウを立ち上げてn3
で新たなエージェント(agent3
)を起動し,同時にagent1
とagent2
で形成するクラスタにjoinする.起動と同時にクラスタにjoinするには,-join
オプションを使う.ここでは,agent2
のbindアドレスを指定してjoinしてみる.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
それぞれのエージェントのログをみると,エージェントのjoinの情報がやり取りされているのがわかる.agent1
とagent2
はagent3
のjoin情報を,新しくクラスタにjoinしたばかりのagent3
はagent1
とagent2
のjoin情報を受け取っている.
1 2 |
|
1 2 |
|
1 2 3 4 |
|
それぞれのエージェントでserf memberを実行すると,agent3
が新たなメンバーとして追加されていることが確認できる.
1 2 3 4 |
|
1 2 3 4 |
|
1 2 3 4 |
|
クラスタからの離脱
クラスタから抜けてみる.エージェントを停止するだけ.停止方法は,エージェントの起動画面でCtrl-C
(interrupt signalを送る)するか,エージェントのプロセスをkill(terminated)するだけ.
二つの停止方法で,serfの挙動は異なる.
- 正常終了.
Ctrl-C
(interrupt)による停止.Serfは他のクラスタのメンバーにその停止エージェントのleftを通知し,以後そのノードに対して通信はしない. - 異常終了.プロセスをkill(terminated)して停止.クラスタの他のメンバーはそのノードがfailedしたと検知する.そしてSerfは再びそのノードに接続しよう通信を継続する.
正常終了
まず,agent3
をCtrl-C
で停止してみる.
1 2 3 4 5 6 |
|
他のメンバーに対して,クラスタのleaveを通知してからエージェントを停止している.また,残ったエージェントのログをみると,メンバーのleave情報(EventMemberLeave
)を受け取っていることが確認できる.
1 2 |
|
1 2 |
|
それぞれのエージェントでserf memberを実行すると,agent3
がleftしたことが確認できる.
1 2 3 4 |
|
1 2 3 4 |
|
異常終了
次に,agent2
をプロセスのkillで停止してみる.
1 2 3 4 |
|
agent3
の場合とは異なり,leave通知なしで停止している.残ったagent1
のログをみると,メンバーのfailedを検知し,再接続しようとしているのが確認できる.
1 2 3 4 5 6 |
|
serf members
を実行すると,agent2
がfailedとなっていることが確認できる.
1 2 3 4 |
|
Serfはfailedノードを再接続しようとし続けるので,再びn2
でagent2
を起動すると,joinすることなく自動でクラスタにjoinされる.
1 2 3 |
|
イベントハンドラ
Serfのエージェントの起動方法,クラスタへの参入/離脱方法はわかった.Serfが強力なのは,メンバーのjoinやその他のイベントに反応できるところ.特定のイベントに対して,オリジナルのスクリプトを実行することができる.
以下のような,単純なrubyスクリプトによるイベントハンドラ(handler.rb
)を作る.
1 2 3 4 5 6 7 8 |
|
このイベントハンドラは,単純にSERF_EVENT
という環境変数に格納されたイベント名を出力する.Serfのイベントのデータは常に標準入力からくる,のでSTDIN
によりこれを取得する.
イベントハンドラの登録
では,実際にこのイベントハンドラを動かしてみる.エージェントを起動する際に,-event-handler
で上記のスクリプトを指定するだけ.イベントハンドラの出力はDEBUGモードの時に出力されるので,ログレベルをDEBUGにしておく.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
ログの最終行をみると,イベントに対して,スクリプトを実行しているのがわかる.今回の
SERF_EVENT
はmember-join
で,それを出力している.
イベントハンドラの種類
Serfが発行するイベントは以下.
member-join
メンバーのjoinmember-leave
メンバーの離脱(Ctrl+c
による離脱,正常終了の場合)member-failed
メンバーのダウン,Failed(異常終了の場合)member-update
メンバーのアップデートmember-reap
メンバーの解除(failedメンバーへの再接続のタイムアウト)user
カスタムイベントの発行query
カスタムクエリの発行
環境変数
イベントハンドラが実行されると,Serfは以下のような環境変数を設定する.
SERF_EVENT
発生したイベント名SERF_SELF_NAME
イベントを発行したノード名SERF_SELF_ROLE
イベントを発行したノードのrole名SERF_TAG_${TAG}
エージェントが持つタグ名SERF_USER_EVENT
カスタムイベント名SERF_USER_LTIME
カスタムイベントのLamportTime
SERF_QUERY_NAME
カスタムクエリ名SERF_QUERY_LTIME
カスタムクエリのLamportTime
LamportTime
はLamport timestampsを参照.
特定のイベントに対するイベントハンドラの登録
特定のイベントに対して,イベントハンドラを登録することもできる.
member-leave
のときのみ,handler.rb
を実行したい場合は,
1
|
|
memver-join
とmember-leave
のときのみ,handler.rb
を実行した場合は,
1
|
|
カスタムイベント
joinやleave等の標準のイベントに加えて,ユーザ独自のイベントをクラスタ内に伝搬させることもできる.このイベントには,基になるノードもないし,反応も期待しない.また,全てのノードに伝搬したか保証できない.カスタムイベントは,デプロイのトリガー,クラスタの再起動などに使われる.
カスタムイベントの発行
あらかじめエージェントを起動しておく.例として,二つのホスト(n1
とn2
)でagent1
とagent2
を起動し,クラスタを形成する.
1 2 |
|
1 2 |
|
1 2 3 |
|
カスタムイベントを発行するには,serf event
コマンドを実行する.hello
というイベントを発行する.
1 2 |
|
それぞれのエージェントのログをみると,hello
イベントを受け取っていることがわかる
1 2 |
|
1 2 |
|
カスタムイベントに対するイベントハンドラ
標準のイベントと同様に,イベントハンドラはこのカスタムイベントに反応することができる.
すべてのカスタムイベントに対して,handler.rb
を実行したい場合,
1
|
|
特定のカスタムイベントに対するイベントハンドラは,user:イベント名
で登録する.例えば,上のhello
カスタムイベントに対して,handler.rb
を実行したい場合,
1
|
|
イベントペイロード
イベント名を伝搬するだけではなく,イベント名に紐づく任意のデータ(ペイロード)を同時に伝搬させることができる.
例えば,name
というイベント名で,deeeet
を伝搬させるには以下のようにする.
1
|
|
データは,標準入力として入力されるので,イベントハンドラ内で利用できる.
SerfのゴシッププロトコルはUDPを使っているので,理論的には,最大積載量は1KB未満であり,Serfはさらにそれを制限している.
カスタムクエリ
カスタムイベントは,イベントを伝搬させるだけだが,カスタムクエリは各ノードにレスポンスを要求する.カスタムクエリは,イベントよりも柔軟で,伝搬させるべきノードをフィルタリングして,さらに好きなレスポンスを返させることができる.カスタムクエリは,ノードの情報種集などに利用される.
カスタムクエリの発行
あらかじめエージェントを起動しておく.例として,二つのホスト(n1
とn2
)でagent1
とagent2
を起動し,クラスタを形成する.
1 2 |
|
1 2 |
|
1 2 3 |
|
カスタムクエリを発行するには,serf query
コマンドを実行する.uptime
というクエリを発行する.
1 2 3 4 5 6 |
|
カスタムイベントとはことなり,各ノードからレスポンスが返ってきている.それぞれのエージェントのログをみると,uptime
クエリを受け取っていることがわかる.
1 2 |
|
1 2 |
|
カスタムクエリに対するイベントハンドラ
カスタムクエリが強力なのは,イベントハンドラの出力結果をレスポンスとして返させることができること.
特定のクエリに対するイベントハンドラは,query:クエリ名
で登録する.例えば,上のuptime
カスタムクエリに対して,uptime
を実行したい場合は,以下のようにする.
1
|
|
この状態で,uptime
クエリを実行すると,
1 2 3 4 5 6 |
|
agent1
からuptime
の実行結果が返ってきているのがわかる.
クエリペイロード
クエリ名を伝搬するだけではなく,クエリ名に紐づく任意のデータ(ペイロード)を同時に伝搬させることができる.
例えば,nameというクエリ名で,deeeetを伝搬させるには以下のようにする.
1
|
|
データは,標準入力として入力されるので,イベントハンドラ内で利用できる.
SerfのゴシッププロトコルはUDPを使っているので,理論的には,最大積載量は1KB未満であり,Serfはさらにそれを制限している.
伝搬させるノードの制限
クエリを伝搬させるべきノードを制限することができる.例えばagent1
のみに伝搬させたい場合は,
1
|
|
カスタムクエリの応用例
イベントハンドラにシェルを指定し,クエリペイロードを用いると,任意のコマンドを発行し,その結果を受け取ることができる.
1
|
|
1
|
|
“Serf という Orchestration ツール #immutableinfra”を参考.
コマンド一覧
v0.5.0現在で利用可能なコマンド一覧.
メンバーシップ関連
serf agent
エージェントを起動するserf join
クラスタに参入するserf leave
クラスタから離脱するserf force-leave
メンバーを離脱させるserf memers
クラスタのメンバーを確認する
カスタムメッセージ関連
serf event
カスタムイベントを配信するserf query
カスタムクエリを配信する
デバッグ関連
serf monitor
起動しているエージェントの接続して,そのログを確認するserf reachability
ネットワークの接続確認をする
その他
serf keygen
暗号通信を行うための暗号キーを生成するserf tag
クラスタのメンバーのタグを変更する