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の利用例には以下のようなものがある.

  • Webサーバのロードバランサへの登録,解除
  • RedisやMemcachedのクラスタリング
  • DNSの更新
  • デプロイのトリガー(カスタムイベント
  • シンプルなサービス監視(カスタムクエリ

詳細は,公式の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は以下.

$script = <<SCRIPT

echo Installing depedencies...
sudo apt-get install -y unzip

echo Fetching Serf...
cd /tmp/
wget https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip -O serf.zip

echo Installing Serf...
unzip serf.zip
sudo chmod +x serf
sudo mv serf /usr/bin/serf

SCRIPT

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise64"
    config.vm.box_url = "http://files.vagrantup.com/precise64.box"

  config.vm.provision "shell", inline: $script

  config.vm.define "n1" do |n1|
      n1.vm.network "private_network", ip: "172.20.20.10"
  end

  config.vm.define "n2" do |n2|
      n2.vm.network "private_network", ip: "172.20.20.11"
  end

  config.vm.define "n3" do |n3|
      n3.vm.network "private_network", ip: "172.20.20.12"
  end
end  

立ち上げる.

$ vagrant up
$ vagrant status
Current machine states:

n1                        running (virtualbox)
n2                        running (virtualbox)
n3                        running (virtualbox)

目次へ

クラスタの形成

シンプルなクラスタを形成してみる.

エージェントの起動

まず,n1で最初のエージェント(agent1)を起動する.同一ネットワーク上で発見されるように,bindアドレスにはprivate networkのIPを指定する.

$ vagrant ssh n1
vagrant@n1:$ serf agent -node=agent1 -bind=172.20.20.10
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'agent1'
         Bind addr: '172.20.20.10:7946'
         RPC addr: '127.0.0.1:7373'
         Encrypted: false
         Snapshot: false
         Profile: lan

次に,別のウィンドウを立ち上げてn2で新たなエージェント(agent2)を起動する.

$ vagrant ssh n2
vagrant@n2:$ serf agent -node=agent2 -bind=172.20.20.11
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'agent2'
         Bind addr: '172.20.20.11:7946'
         RPC addr: '127.0.0.1:7373'
         Encrypted: false
         Snapshot: false
         Profile: lan

この時点で,2つのホストで2つのserfエージェントが起動してる.しかし,2つのエージェントは互いについては何も知らない.それぞれが自分自身のクラスタを形成している.serf memberを実行するとそれを確認できる.

vagrant@n1:$ serf members
agent1  172.20.20.10:7946  alive
vagrant@n2:$ serf members
agent2  172.20.20.11:7946  alive

クラスタへのJoin

クラスタにjoinしてみる.agent2agent1にjoinさせる.

vagrant@n2$ serf join 172.20.20.10
Successfully joined cluster by contacting 1 nodes.

それぞれのエージェントのログをみると,メンバーのjoin情報(EventMemberJoin)を互いに受け取っていることが確認できる.

# agent1
2014/03/23 13:45:18 [INFO] serf: EventMemberJoin: agent2 172.20.20.11
# agent2
2014/03/23 13:45:18 [INFO] agent: joining: [172.20.20.10] replay: false
2014/03/23 13:45:18 [INFO] serf: EventMemberJoin: agent1 172.20.20.10
2014/03/23 13:45:18 [INFO] agent: joined: 1 Err: <nil>

それぞれのエージェントでserf memberを実行すると,それぞれのエージェントが互いのことを認識していることが確認できる.

vagrant@n1:$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive
vagrant@n2:$ serf members
agent2  172.20.20.11:7946  alive
agent1  172.20.20.10:7946  alive

さらに別のウィンドウを立ち上げてn3で新たなエージェント(agent3)を起動し,同時にagent1agent2で形成するクラスタにjoinする.起動と同時にクラスタにjoinするには,-joinオプションを使う.ここでは,agent2のbindアドレスを指定してjoinしてみる.

$ vagrant ssh n3
vagrant@precise64:~$ serf agent -node=agent3 -bind=172.20.20.12 -join=172.20.20.11
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'agent3'
         Bind addr: '172.20.20.12:7946'
         RPC addr: '127.0.0.1:7373'
         Encrypted: false
         Snapshot: false
         Profile: lan
==> Joining cluster...(replay: false)
Join completed. Synced with 1 initial agents

それぞれのエージェントのログをみると,エージェントのjoinの情報がやり取りされているのがわかる.agent1agent2agent3のjoin情報を,新しくクラスタにjoinしたばかりのagent3agent1agent2のjoin情報を受け取っている.

# agent1
2014/03/23 14:15:09 [INFO] serf: EventMemberJoin: agent3 172.20.20.12
# agent2
2014/03/23 14:15:08 [INFO] serf: EventMemberJoin: agent3 172.20.20.12
# agent3
2014/03/23 14:15:08 [INFO] agent: joining: [172.20.20.11] replay: false
2014/03/23 14:15:08 [INFO] serf: EventMemberJoin: agent1 172.20.20.10
2014/03/23 14:15:08 [INFO] serf: EventMemberJoin: agent2 172.20.20.11

それぞれのエージェントでserf memberを実行すると,agent3が新たなメンバーとして追加されていることが確認できる.

vagrant@n1:$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive
agent3  172.20.20.12:7946  alive
vagrant@n2:$ serf members
agent2  172.20.20.11:7946  alive
agent1  172.20.20.10:7946  alive
agent3  172.20.20.12:7946  alive
vagrant@n3:$ serf members
agent3  172.20.20.12:7946  alive
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive

目次へ

クラスタからの離脱

クラスタから抜けてみる.エージェントを停止するだけ.停止方法は,エージェントの起動画面でCtrl-C(interrupt signalを送る)するか,エージェントのプロセスをkill(terminated)するだけ.

二つの停止方法で,serfの挙動は異なる.

  • 正常終了.Ctrl-C(interrupt)による停止.Serfは他のクラスタのメンバーにその停止エージェントの_left_を通知し,以後そのノードに対して通信はしない.
  • 異常終了.プロセスをkill(terminated)して停止.クラスタの他のメンバーはそのノードが_failed_したと検知する.そしてSerfは再びそのノードに接続しよう通信を継続する.

正常終了

まず,agent3Ctrl-Cで停止してみる.

==> Caught signal: interrupt
==> Gracefully shutting down agent...
    2014/03/23 14:42:07 [INFO] agent: requesting graceful leave from Serf
    2014/03/23 14:42:08 [INFO] serf: EventMemberLeave: agent3 172.20.20.12
    2014/03/23 14:42:08 [INFO] agent: requesting serf shutdown
    2014/03/23 14:42:08 [INFO] agent: shutdown complete

他のメンバーに対して,クラスタのleaveを通知してからエージェントを停止している.また,残ったエージェントのログをみると,メンバーのleave情報(EventMemberLeave)を受け取っていることが確認できる.

# agent1
2014/03/23 14:42:08 [INFO] serf: EventMemberLeave: agent3 172.20.20.12
# agent2
2014/03/23 14:42:08 [INFO] serf: EventMemberLeave: agent3 172.20.20.12

それぞれのエージェントでserf memberを実行すると,agent3が_left_したことが確認できる.

vagrant@n1:$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive
agent3  172.20.20.12:7946  left
vagrant@n2:$ serf members
agent2  172.20.20.11:7946  alive
agent1  172.20.20.10:7946  alive
agent3  172.20.20.12:7946  left

異常終了

次に,agent2をプロセスのkillで停止してみる.

==> Caught signal: terminated
    2014/03/23 14:58:11 [INFO] agent: requesting serf shutdown
    2014/03/23 14:58:11 [WARN] Shutdown without a Leave
    2014/03/23 14:58:11 [INFO] agent: shutdown complete

agent3の場合とは異なり,leave通知なしで停止している.残ったagent1のログをみると,メンバーの_failed_を検知し,再接続しようとしているのが確認できる.

# agent1
2014/03/23 14:58:19 [INFO] serf: EventMemberFailed: agent2 172.20.20.11
2014/03/23 14:58:20 [INFO] agent: Received event: member-failed
2014/03/23 14:58:36 [INFO] serf: attempting reconnect to agent2 172.20.20.11
2014/03/23 14:59:06 [INFO] serf: attempting reconnect to agent2 172.20.20.11
2014/03/23 14:59:36 [INFO] serf: attempting reconnect to agent2 172.20.20.11

serf membersを実行すると,agent2が_failed_となっていることが確認できる.

vagrant@n1:$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  failed
agent3  172.20.20.12:7946  left

Serfは_failed_ノードを再接続しようとし続けるので,再びn2agent2を起動すると,joinすることなく自動でクラスタにjoinされる.

vagrant@n2:$ serf agent -node=agent2 -bind=172.20.20.11
...
2014/03/23 15:05:36 [INFO] serf: EventMemberJoin: agent1 172.20.20.10

目次へ

イベントハンドラ

Serfのエージェントの起動方法,クラスタへの参入/離脱方法はわかった.Serfが強力なのは,メンバーのjoinやその他のイベントに反応できるところ.特定のイベントに対して,オリジナルのスクリプトを実行することができる.

以下のような,単純なrubyスクリプトによるイベントハンドラ(handler.rb)を作る.

#handler.rb

puts
puts "New event: #{ENV["SERF_EVENT"]}. "

while str = STDIN.gets
    puts str
end

このイベントハンドラは,単純にSERF_EVENTという環境変数に格納されたイベント名を出力する.Serfのイベントのデータは常に標準入力からくる,のでSTDINによりこれを取得する.

イベントハンドラの登録

では,実際にこのイベントハンドラを動かしてみる.エージェントを起動する際に,-event-handlerで上記のスクリプトを指定するだけ.イベントハンドラの出力はDEBUGモードの時に出力されるので,ログレベルをDEBUGにしておく.

$ serf agent -node=agent1 -log-level=debug -event-handler='ruby handler.rb'
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'agent1'
         Bind addr: '0.0.0.0:7946'
         RPC addr: '127.0.0.1:7373'
         Encrypted: false
         Snapshot: false
         Profile: lan

==> Log data will now stream in as it occurs:

    2014/03/25 16:43:00 [INFO] agent: Serf agent starting
    2014/03/25 16:43:00 [INFO] serf: EventMemberJoin: agent1 10.0.2.15
    2014/03/25 16:43:01 [INFO] agent: Received event: member-join
    2014/03/25 16:43:01 [DEBUG] agent: Event 'member-join' script output:
    New event: member-join.
    agent1  10.0.2.15

ログの最終行をみると,イベントに対して,スクリプトを実行しているのがわかる.今回の SERF_EVENTmember-joinで,それを出力している.

イベントハンドラの種類

Serfが発行するイベントは以下.

  • member-join メンバーのjoin
  • member-leave メンバーの離脱(Ctrl+cによる離脱,正常終了の場合)
  • member-failed メンバーのダウン,Failed(異常終了の場合)
  • member-update メンバーのアップデート
  • member-reap メンバーの解除(_failed_メンバーへの再接続のタイムアウト)
  • user カスタムイベントの発行
  • query カスタムクエリの発行

環境変数

イベントハンドラが実行されると,Serfは以下のような環境変数を設定する.

LamportTimeLamport timestampsを参照.

特定のイベントに対するイベントハンドラの登録

特定のイベントに対して,イベントハンドラを登録することもできる.

member-leaveのときのみ,handler.rbを実行したい場合は,

$ serf agent -node=agent1 -log-level=debug -event-handler member-leave='ruby handler.rb'

memver-joinmember-leaveのときのみ,handler.rbを実行した場合は,

$ serf agent -node=agent1 -log-level=debug -event-handler member-join,member-leave='ruby handler.rb'

目次へ

カスタムイベント

joinやleave等の標準のイベントに加えて,ユーザ独自のイベントをクラスタ内に伝搬させることもできる.このイベントには,基になるノードもないし,反応も期待しない.また,全てのノードに伝搬したか保証できない.カスタムイベントは,デプロイのトリガー,クラスタの再起動などに使われる.

カスタムイベントの発行

あらかじめエージェントを起動しておく.例として,二つのホスト(n1n2)でagent1agent2を起動し,クラスタを形成する.

$ vagrant ssh n1
vagrant@n1:$ serf agent -node=agent1 -bind=172.20.20.10
$ vagrant ssh n2
vagrant@n2:$ serf agent -node=agent2 -bind=172.20.20.11 -join=172.20.20.10
$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive

カスタムイベントを発行するには,serf eventコマンドを実行する.helloというイベントを発行する.

$ vagrant ssh n2
vagrant@n2:$ vagrant event hello

それぞれのエージェントのログをみると,helloイベントを受け取っていることがわかる

#agent1
2014/03/26 14:10:38 [INFO] agent: Received event: user-event: hello
#agent2
2014/03/26 14:10:38 [INFO] agent: Received event: user-event: hello

カスタムイベントに対するイベントハンドラ

標準のイベントと同様に,イベントハンドラはこのカスタムイベントに反応することができる.

すべてのカスタムイベントに対して,handler.rbを実行したい場合,

$ serf agent -log-level=debug -event-handler user="ruby handler.rb"

特定のカスタムイベントに対するイベントハンドラは,user:イベント名で登録する.例えば,上のhelloカスタムイベントに対して,handler.rbを実行したい場合,

$ serf agent -log-level=debug -event-handler user:hello="ruby handler.rb"

イベントペイロード

イベント名を伝搬するだけではなく,イベント名に紐づく任意のデータ(ペイロード)を同時に伝搬させることができる.

例えば,nameというイベント名で,deeeetを伝搬させるには以下のようにする.

$ serf event name deeeet

データは,標準入力として入力されるので,イベントハンドラ内で利用できる.

SerfのゴシッププロトコルはUDPを使っているので,理論的には,最大積載量は1KB未満であり,Serfはさらにそれを制限している.

目次へ

カスタムクエリ

カスタムイベントは,イベントを伝搬させるだけだが,カスタムクエリは各ノードにレスポンスを要求する.カスタムクエリは,イベントよりも柔軟で,伝搬させるべきノードをフィルタリングして,さらに好きなレスポンスを返させることができる.カスタムクエリは,ノードの情報種集などに利用される.

カスタムクエリの発行

あらかじめエージェントを起動しておく.例として,二つのホスト(n1n2)でagent1agent2を起動し,クラスタを形成する.

$ vagrant ssh n1
vagrant@n1:$ serf agent -node=agent1 -bind=172.20.20.10
$ vagrant ssh n2
vagrant@n2:$ serf agent -node=agent2 -bind=172.20.20.11 -join=172.20.20.10
$ serf members
agent1  172.20.20.10:7946  alive
agent2  172.20.20.11:7946  alive

カスタムクエリを発行するには,serf queryコマンドを実行する.uptimeというクエリを発行する.

$ serf query uptime
Query 'uptime' dispatched
Ack from 'agent2'
Ack from 'agent1'
Total Acks: 2
Total Responses: 0

カスタムイベントとはことなり,各ノードからレスポンスが返ってきている.それぞれのエージェントのログをみると,uptimeクエリを受け取っていることがわかる.

# agent1
2014/03/26 15:18:32 [INFO] agent: Received event: query: uptime
# agent2
2014/03/26 15:18:32 [INFO] agent: Received event: query: uptime

カスタムクエリに対するイベントハンドラ

カスタムクエリが強力なのは,イベントハンドラの出力結果をレスポンスとして返させることができること.

特定のクエリに対するイベントハンドラは,query:クエリ名で登録する.例えば,上のuptimeカスタムクエリに対して,uptimeを実行したい場合は,以下のようにする.

$ serf agent -node=agent1 -bind=172.20.20.10 -event-handler query:uptime=uptime

この状態で,uptimeクエリを実行すると,

$ serf query uptime
Query 'uptime' dispatched
Ack from 'agent1'
Response from 'agent1':  15:29:29 up 2 days,  6:27,  2 users,  load average: 0.13, 0.25, 0.30
Total Acks: 1
Total Responses: 1

agent1からuptimeの実行結果が返ってきているのがわかる.

クエリペイロード

クエリ名を伝搬するだけではなく,クエリ名に紐づく任意のデータ(ペイロード)を同時に伝搬させることができる.

例えば,nameというクエリ名で,deeeetを伝搬させるには以下のようにする.

$ serf query name deeeet

データは,標準入力として入力されるので,イベントハンドラ内で利用できる.

SerfのゴシッププロトコルはUDPを使っているので,理論的には,最大積載量は1KB未満であり,Serfはさらにそれを制限している.

伝搬させるノードの制限

クエリを伝搬させるべきノードを制限することができる.例えばagent1のみに伝搬させたい場合は,

$ serf query -node agent1 uptime

カスタムクエリの応用例

イベントハンドラにシェルを指定し,クエリペイロードを用いると,任意のコマンドを発行し,その結果を受け取ることができる.

$ serf agent -node=agent1 -bind=172.20.20.10 -event-handler query:sh='/bin/bash'
$ serf query sh 'service nginx reload'

“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 クラスタのメンバーのタグを変更する

目次へ

参考