serfの基本的な動作を試してみた

#serf

serf1を知るためにアレコレと試した作業記録のようなもの。

@glidenote先生によるserf-muninの設定やブログ2を眺めたり(同じ部署で大変お世話になっている)、公式ドキュメントを眺めたりしながら、Vagrant上でserf clusteringを試した。

何番煎じかも分からないほど後発だが、あくまで備忘録である。

試験環境

  • CentOS 6.5 x86_6434
  • Serf 0.6.3

以下の簡易な図のように、one.devtwo.devという2個のVMを用意し、serf clusterとする。

+---------+   serf cluster   +---------+
| one.dev | ---------------- | two.dev |
+---------+                  +---------+

Vagrantfile

導入環境を満たすVagrantfileを用意する。 VMは2つ、それぞれにSerf 0.6.3をインストールする。

$script = <<SCRIPT
[ -f /usr/local/src/serf.zip ] || wget -q https://dl.bintray.com/mitchellh/serf/0.6.3_linux_amd64.zip -O /usr/local/src/serf.zip
[ -f /usr/local/bin/serf ]     || unzip /usr/local/src/serf.zip -d /usr/local/bin

mkdir -p /etc/serf/conf.d
cat<<EOS >/etc/serf/conf.d/config.json
{
  "interface": "eth1",
  "discover": "serftest",
  "encrypt_key": "TywlF+RXm2mHDIUqwCl8/w==",
  "enable_syslog": true
}
EOS
SCRIPT

Vagrant.configure("2") do |config|
  config.vm.box = "hfm4/centos6"
  config.vm.provision :shell, inline: $script

  config.vm.define :one do |c|
    c.vm.hostname = "one.dev"
    c.vm.network :private_network, ip: "192.168.128.2"
  end

  config.vm.define :two do |c|
    c.vm.hostname = "two.dev"
    c.vm.network :private_network, ip: "192.168.128.3"
  end
end

vagrant up --provisionと実行すれば、serfとその設定ファイルが入ったVMが立ち上がる。

/etc/serf/conf.d/config.json

Vagrantfileのprovisionによって、serf用設定ファイルが用意される。 config.jsonがそれで、今回はシンプルな設定にすることにした。

{
  "interface": "eth1",
  "discover": "serftest",
  "encrypt_key": "TywlF+RXm2mHDIUqwCl8/w==",
  "enable_syslog": true
}

"interface": "eth1", "discover": "serftest"を用いて、private networkによるクラスタリングを行う。 encrypt_keyは無くてもいいが、一応付けた。 また、enable_syslogを有効化し、ログを/var/log/messagesに記録する。

単純なクラスタリング

まずはクラスタリング自体が出来るかどうかを確認するため、iptablesによる制限は一時的に止めた。

sudo service iptables stop

まずはそれぞれのVMからserfを起動する。 起動時に-config-dir=/etc/serf/conf.dを指定し、該当ディレクトリ以下の設定ファイルを読み込ませる。

one.devでserfを起動する

ログの出力はデフォルト-log-level=infoのままにした。

[vagrant@one ~]$ serf agent -config-dir=/etc/serf/conf.d
==> Using interface 'eth1' address '192.168.128.2'
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'one.dev'
         Bind addr: '192.168.128.2:7946'
          RPC addr: '127.0.0.1:7373'
         Encrypted: true
          Snapshot: false
           Profile: lan
      mDNS cluster: serftest

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

    2014/07/21 23:58:32 [INFO] agent: Serf agent starting
    2014/07/21 23:58:32 [INFO] serf: EventMemberJoin: one.dev 192.168.128.2
    2014/07/21 23:58:32 [INFO] agent: joining: [192.168.128.2:7946] replay: false
    2014/07/21 23:58:32 [INFO] agent: joined: 1 nodes
    2014/07/21 23:58:32 [INFO] agent.mdns: Joined 1 hosts
    2014/07/21 23:58:33 [INFO] agent: Received event: member-join

one.dev自身がメンバとしてクラスタに参加し、member-joinイベントが発行されていることが分かる。

two.devでserfを起動する

続いてtwo.dev側のserfを起動した。

[vagrant@two ~]$ serf agent -config-dir=/etc/serf/conf.d
==> Using interface 'eth1' address '192.168.128.3'
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'two.dev'
         Bind addr: '192.168.128.3:7946'
          RPC addr: '127.0.0.1:7373'
         Encrypted: true
          Snapshot: false
           Profile: lan
      mDNS cluster: serftest

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

    2014/07/21 23:58:43 [INFO] agent: Serf agent starting
    2014/07/21 23:58:43 [INFO] serf: EventMemberJoin: two.dev 192.168.128.3
    2014/07/21 23:58:43 [INFO] agent: joining: [192.168.128.3:7946 192.168.128.2:7946] replay: false
    2014/07/21 23:58:43 [INFO] serf: EventMemberJoin: one.dev 192.168.128.2
    2014/07/21 23:58:43 [INFO] agent: joined: 2 nodes
    2014/07/21 23:58:43 [INFO] agent.mdns: Joined 2 hosts
    2014/07/21 23:58:44 [INFO] agent: Received event: member-join

クラスタリングを確認する

two.dev側のログ

さて、two.dev側のログの、以下の5行を見てみる。 one.devと同じクラスタに参加、ノード数が2に加算され、member-joinイベントが発行されたことが分かる。

    2014/07/21 23:58:43 [INFO] agent: joining: [192.168.128.3:7946 192.168.128.2:7946] replay: false
    2014/07/21 23:58:43 [INFO] serf: EventMemberJoin: one.dev 192.168.128.2
    2014/07/21 23:58:43 [INFO] agent: joined: 2 nodes
    2014/07/21 23:58:43 [INFO] agent.mdns: Joined 2 hosts
    2014/07/21 23:58:44 [INFO] agent: Received event: member-join

one.dev側のログ

two.dev側でserfを起動したタイミングで、one.dev側にもログが出力されていた。 two.devがクラスタに参加した(クラスタに追加された)通知をmember-joinイベントと共に受け取っている。

    2014/07/21 23:58:43 [INFO] serf: EventMemberJoin: two.dev 192.168.128.3
    2014/07/21 23:58:44 [INFO] agent: Received event: member-join
    2014/07/21 23:59:32 [INFO] agent: joining: [192.168.128.3:7946] replay: false
    2014/07/21 23:59:32 [INFO] agent: joined: 1 nodes
    2014/07/21 23:59:32 [INFO] agent.mdns: Joined 1 hosts

メンバの確認

serf memberコマンドを実行すると、どのホスト、IPが参加しているかが分かる。

[vagrant@one ~]$ serf members
one.dev  192.168.128.2:7946  alive
two.dev  192.168.128.3:7946  alive

ここまでで、単純なクラスタリングの確認を行った。

iptables

iptablesを起動しているとクラスタリングに失敗することがある。
結論を先回りすると、以下のようにprivate network側をまるごと許可すれば動く。

sudo iptables -I INPUT -i eth1 -j ACCEPT

TCP7946番ポート

バインドアドレスは<ETH1 IP ADDR>:7946で、TCPの7946番ポートを開ける必要がある(ただし、ポート番号も変更可能。) しかし、7946番ポートを解放しても、クラスタへの参加は可能になるものの、実はまだ充分ではない。

UDP, あるいはGossip Protocol

/var/log/messagesに次のようなログが残っている。

Jul 22 00:11:40 localhost serf[3202]: memberlist: Responding to push/pull sync with: 192.168.128.3:46428
Jul 22 00:12:10 localhost serf[3202]: memberlist: Responding to push/pull sync with: 192.168.128.3:46429
Jul 22 00:12:40 localhost serf[3202]: memberlist: Responding to push/pull sync with: 192.168.128.3:46430

詳細はまだ調べきれていないのだが、これはGossip Protocol5の挙動の1つで、UDPで通信をしている。 自動クラスタリングを行うためにはUDP側の解放も必要らしい(らしい、と書いたのは、繰り返しになるが、私自身がGossip Protocolを理解できていないことに起因する。)

粗い調査で申し訳ないが、Gossip Protocolを活用するためには、やはりeth1側を解放しておくのが楽そうだと感じた。

Event Handler

イベントハンドラの動作を確認するため、今までのserf agentコマンドに-event-handler=dateを足した。 dateコマンドによって、ログに時刻が出力されることを期待している。

serf agent -config-dir=/etc/serf/conf.d/config.json -event-handler=date

イベントハンドラの実行タイミングを知るために、以下のような操作を順番に行った。

1. twoがoneへmember-join    | one | <-- member-join  --- | two |
2. oneがtwoへmember-leave   | one | --- member-leave --> | two |
3. oneがtwoへmember-join    | one | --- member-join  --> | two |
4. twoがoneへmember-leave   | one | <-- member-leave --- | two |

操作に対する/var/log/messagesは以下のようになった。

# 1 .'member-join'が発行されて、`date`の結果が出力されている。
Jul 20 03:40:11 localhost serf[22050]: serf: EventMemberJoin: one.dev 192.168.128.2
Jul 20 03:40:12 localhost serf[22050]: agent: Event 'member-join' script output: Sun Jul 20 03:40:12 JST 2014

# 2. 'member-leave'が発行されて、`date`の結果が出力されている。
Jul 20 03:40:16 localhost serf[22050]: serf: EventMemberLeave: one.dev 192.168.128.2
Jul 20 03:40:17 localhost serf[22050]: agent: Event 'member-leave' script output: Sun Jul 20 03:40:17 JST 2014

# 3. 'member-join'が発行されて、`date`の結果が出力されている。
Jul 20 03:40:19 localhost serf[22050]: serf: EventMemberJoin: one.dev 192.168.128.2
Jul 20 03:40:20 localhost serf[22050]: agent: Event 'member-join' script output: Sun Jul 20 03:40:20 JST 2014

# 4. 自分から離れたため、event-handlerは実行されず。
Jul 20 03:40:23 localhost serf[22050]: serf: EventMemberLeave: two.dev 192.168.128.3

自らが(Ctrl-Cかkillシグナル等を受けて)明示的に離脱する以外はイベントハンドラが発行されていた。

イベントの種類は他にもあるが、突然死でも無い限り、自分が離脱する以外の操作は全てイベントハンドラが発行されるのではないだろうか(全て検証するのは骨が折れるのでやってない。)

Specifying Event Handlers6を見る限りは、イベントハンドラに渡せるイベントの種類は自ら設定できるので、特定のイベントにだけ対応するハンドラを設定することは出来る。

Tagsと環境変数

serfはイベントハンドラ発行時に環境変数をセットする7。 そこで、以下のようなイベントハンドラを設定し、どのような環境変数が得られるのかを見てみた。

serf agent -config-dir=/etc/serf/conf.d/config.json -event-handler='env | grep SERF'

例えば、member-joinmember-leaveイベントが発行されると、以下のような結果が得られた。

Jul 20 04:05:12 localhost serf[22169]: serf: EventMemberJoin: one.dev 192.168.128.2
Jul 20 04:05:13 localhost serf[22169]: agent: Event 'member-join' script output: SERF_SELF_NAME=two.dev#012SERF_EVENT=member-join#012SERF_SELF_ROLE=
Jul 20 04:05:17 localhost serf[22169]: serf: EventMemberLeave: one.dev 192.168.128.2
Jul 20 04:05:18 localhost serf[22169]: agent: Event 'member-leave' script output: SERF_SELF_NAME=two.dev#012SERF_EVENT=member-leave#012SERF_SELF_ROLE=

SERF_SELF_NAMESERF_EVENTはそのままなので省略する。

SERF_SELF_ROLE=は何も指定していないので空白だ。 ただし、Configuration8では-roleがdeprecatedになっており、この環境変数の将来は怪しい。

2014/07/27追記:
Membership Event Dataを読む限り、roleそのものをdeprecatedにしたわけではなく、tagsから指定することへの切り替えを推奨しているだけらしい。 つまり、SERF_SELF_ROLEは生きるのかもしれない。

tag role=test

次に、-tag role=testというオプションを付与して、先ほどと同じようにイベントを発行してみた。

serf agent -config-dir=/etc/serf/conf.d/config.json -tag role=test -event-handler='env | grep SERF'

結果は以下のようになった。

Jul 20 04:27:58 localhost serf[22230]: agent: Event 'member-join' script output: SERF_SELF_NAME=two.dev#012SERF_EVENT=member-join#012SERF_TAG_ROLE=test#012SERF_SELF_ROLE=test

SERF_TAG_ROLE=testSERF_SELF_ROLE=testから分かるように、どうやらtagでroleを設定すると両方に値が入ってしまうらしい。

tag munin-group=manage

また別パターンとして、munin-groupというタグを作って試してみたら、面白い結果になった。

Jul 20 04:32:06 localhost serf[22244]: agent: Event 'member-join' script output: SERF_SELF_NAME=two.dev#012SERF_TAG_MUNIN_GROUP=manage#012SERF_EVENT=member-join#012SERF_SELF_ROLE=

どうもmunin-groupというtagはSERF_TAG_MUNIN_GROUPと解釈されるらしい。 ハイフンをアンダースコアに変換しているようだ。 そもそも、変数の命名規則からしてハイフンを使えないので当然の結果とも取れる。

しかし、この挙動はハマりそうなので、TAG名には英数文字とアンダースコアだけを使うのがいいだろう。

1つのタグに複数の値を入れるには

config.jsonを修正して、以下のように、roleにArrayを入れるとどうなるかを見てみた。

{
  "interface": "eth1",
  "discover": "serftest",
  "encrypt_key": "tm+rXm2hdIyWLfUqwCl8/w==",
  "tags": {
    "role": [ "storage", "mogfs" ]
  },
  "enable_syslog": true
}

結果は以下のとおりで、エラー文から、tags[role]はstring型でないといけないことが分かった。

[vagrant@one ~]$ serf agent -config-dir=/etc/serf/conf.d/config.json -event-handler='env | grep SERF'
==> Error decoding '/etc/serf/conf.d/config.json': 1 error(s) decoding:

* 'tags[role]' expected type 'string', got unconvertible type '[]interface {}'

よって、1つのタグに複数の値を付与したい(マルチロールなサーバ等)場合は、以下のようなカンマ区切り等のローカルルールを取り決め、プログラム側でsplitするしか無さそうだ。

{ "role": "storage,mogfs" }

ちなみに上記の場合、SERF_TAG_ROLE=storage,mogfsという値が得られる。

終わりに

serfの挙動を知るために、VagrantでVMを用意し、クラスタリングを試してみた。 まだイベントハンドラの扱いが充分ではないが、serf自身の基本的な動作はそれなりに分かってきた。

後、Gossip Protocolについての理解も不十分なので、その辺りの勉強もしていきたい。