Synapse と Serf でサービスディスカバリ

先週の Immutable Infrastructure Conference #1 に参加して意識が高まったので、@mirakui さんの発表で言及されていた、Synapse と Serf の組み合わせを試してみました。

Synapse とは

Synapse は Airbnb が開発しているサービスディスカバリ用のツールです。簡単に言うと「バックエンドサーバを監視して、HAProxy の設定を書き換えてくれるツール」です。

詳しくは: SmartStack: Service Discovery in the Cloud – Airbnb Engineering

Watcher

Synapse にはバックエンドサーバの増減を監視するレイヤーとして、Watcher が用意されています。
(https://github.com/airbnb/synapse/tree/master/lib/synapse/service_watcher)

現時点では、

  • DnsWatcher (DNS に問い合わせる)
  • DockerWatcher (Docker のコンテナ一覧を取得する)
  • EC2Watcher (EC2 のインスタンス一覧を取得する / 未実装)
  • ZookeeperWatcher (Zookeeper に問い合わせる)

があります。

Airbnb 社では Nerve と ZookeeperWatcher を組み合わせて利用しているようです。

Serf と Synapse

と、ここまでくると、Serf でクラスタリングして、Synapse と連携させたくなってきます。 既存の watcher で Serf と連携するのは難しいので、watcher を実装しました。

Serf にはメンバが追加されたり、削除されたりした際に、event handler を実行する仕組みがあるのでこれを使います。 event handler ではテキストファイルにクラスタのメンバリストを出力しておき、Synapse の watcher 側では inotify などの通知をトリガーに更新します。

Synapse with Serf

Serf の RPC は?

Serf は RPC で操作できるので、watcher で RPC を叩くのがよさそうなのですが、その場合、定期的にメンバリストを取得するしかない(はず)です。 今回はイベントハンドラを利用するために、ファイルベースで実装しました。

FileWatcher

ということで、ファイルを監視する watcher を実装して、プルリクをおくりました。

https://github.com/airbnb/synapse/pull/53

まだ、マージされていないのですが、利用例を紹介します。 (マージされてからブログを書こうかと思っていたのですが、他のプルリクを見る限り、なかなかマージされなさそうな雰囲気なので、先に書いてしまうことにしました)

Synapse + FileWatcher + Serf

Synapse をチェックアウトします。

1
2
$ git clone [email protected]:airbnb/synapse.git
$ cd synapse

本家にはまだ FileWatcher が入っていないのと、(おそらく)バグがあるので、ここでは私のフォークをもってきます。

1
2
3
4
5
$ git remote add ryotarai [email protected]:ryotarai/synapse.git
$ git fetch --all
$ git checkout -b file-watcher-sample
$ git merge ryotarai/file-watcher
$ git merge ryotarai/shared_frontend-is-optional

Vagrant で 3 台 VM を立ち上げます。

Vagrantfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vagrant.configure("2") do |config|
  # ubuntu precise
  config.vm.box = "precise64"

  config.vm.define :proxy do |c|
    c.vm.network :private_network, ip: '192.168.55.55'
  end
  config.vm.define :web1 do |c|
    c.vm.network :private_network, ip: '192.168.55.56'
  end
  config.vm.define :web2 do |c|
    c.vm.network :private_network, ip: '192.168.55.57'
  end
end
  • proxy * 1: HAProxy, Synapse, Serf を動かします
  • web * 2: nginx, Serf を動かします

Web サーバのセットアップ

Web サーバには nginx をいれて適当に静的ファイルをおいておきます。

1
2
3
4
5
6
$ vagrant ssh web1
(web1) $ sudo apt-get install nginx
(web1) $ sudo /etc/init.d/nginx start
(web1) $ sudo sh -c "echo 'web1' > /usr/share/nginx/www/index.html"
(web1) $ curl http://localhost
web1
1
2
3
4
5
6
$ vagrant ssh web2
(web2) $ sudo apt-get install nginx
(web2) $ sudo /etc/init.d/nginx start
(web2) $ sudo sh -c "echo 'web2' > /usr/share/nginx/www/index.html"
(web2) $ curl http://localhost
web2

Serf を入れます。

1
2
3
(web1, web2) $ cd ~
(web1, web2) $ wget https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip
(web1, web2) $ unzip 0.5.0_linux_amd64.zip

Proxy サーバのセットアップ

rbenv で Ruby 2.1.1 をいれます。

1
(proxy) $ curl https://gist.githubusercontent.com/ryotarai/c2e6df6185fc262e0986/raw/install_rbenv_debian.sh | bash

HAProxy, Serf を入れます。

1
2
3
(proxy) $ sudo apt-get install haproxy
(proxy) $ sudo sed -i -e 's/ENABLED=0/ENABLED=1/' /etc/default/haproxy
(proxy) $ sudo /etc/init.d/haproxy start
1
2
3
(proxy) $ cd ~
(proxy) $ wget https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip
(proxy) $ unzip 0.5.0_linux_amd64.zip

Serf の join-member, leave-member イベントを受けて、サーバ一覧を更新するスクリプトをおいておきます。 以下の内容を~/serf-event-handlerとして保存します。

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
35
36
37
38
39
#!/usr/bin/env ruby
require 'fileutils'

# proxy 以外のサーバの場合実行しない
exit 0 unless ENV['SERF_SELF_ROLE'] == 'proxy'

def memberships
  $stdin.each_line.map do |line|
    name, address, role, tags = line.split(' ')
    tags = Hash[tags.split(',').map {|kv| kv.split('=') }]
    {name: name, address: address, role: role, tags: tags}
  end
end

server_list_file = File.expand_path(ENV['SERVER_LIST'])

webservers = memberships.select do |membership|
  membership[:role] == 'web'
end

case ENV['SERF_EVENT']
when 'member-join'
  open(server_list_file, 'a') do |f|
    webservers.each do |webserver|
      f.puts("#{webserver[:address]} #{webserver[:tags]['port']}")
    end
  end
when 'member-leave'
  server_list_content = File.read(server_list_file)
  webservers.each do |webserver|
    escaped_address = Regexp.escape(webserver[:address])
    server_list_content.sub!(/^#{escaped_address} .+$\n/, '')
  end
  new_server_list_file = '/tmp/new_server_list'
  open(new_server_list_file, 'w') do |f|
    f.write(server_list_content)
  end
  FileUtils.mv(new_server_list_file, server_list_file)
end

実行可能にしておきます。

1
(proxy) $ chmod +x ~/serf-event-handler

Synapse 用の 設定ファイルを置いておきます。 以下の内容を~/synapse.conf.jsonとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "services": {
        "web": {
            "default_servers": [],
            "discovery": {
                "method": "file",
                "path": "/home/vagrant/server.list"
            },
            "haproxy": {
                "port": 80,
            }
        }
    },
    "haproxy": {
        "reload_command": "/etc/init.d/haproxy reload",
        "config_file_path": "/etc/haproxy/haproxy.cfg",
        "global": [],
        "defaults": [],
        "do_writes": true,
        "do_reloads": true
    }
}

synapse を実行しておきます。

1
2
3
4
(proxy) $ touch ~/server.list
(proxy) $ cd /vagrant
(proxy) $ bundle install
(proxy) $ sudo bundle exec synapse -c ~/synapse.conf.json

Serf エージェントを実行しておきます。

1
(proxy) $ SERVER_LIST=$HOME/server.list ./serf agent -node=proxy -bind=192.168.55.55 -tag role=proxy -event-handler=$HOME/serf-event-handler

Web サーバを Proxy のバックエンドに追加する

準備ができたので、実際に Web サーバをバックエンドに追加します。

web1 で Serf エージェントを起動して、クラスタに参加させます。 ここで指定したタグはイベントハンドラに渡され、ポート番号として使われます。

1
(web1) $ ./serf agent -node=web1 -bind=192.168.55.56 -join=192.168.55.55 -tag role=web -tag port=80

proxy で状態を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(proxy) $ cat server.list
192.168.55.56 80
(proxy) $ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:13:01 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.56:80 192.168.55.56:80
(proxy) $ curl http://localhost
web1
(proxy) $ curl http://localhost
web1

web1 がバックエンドに追加されていることがわかります。

続いて、web2 をクラスタに追加します。

1
(web2) $ ./serf agent -node=web2 -bind=192.168.55.57 -join=192.168.55.55 -tag role=web -tag port=80

proxy をみてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(proxy) $ cat server.list
192.168.55.56 80
192.168.55.57 80
(proxy) $ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:15:39 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.56:80 192.168.55.56:80
  server 192.168.55.57:80 192.168.55.57:80
(proxy) $ curl http://localhost
web1
(proxy) $ curl http://localhost
web2
(proxy) $ curl http://localhost
web1

web2 がバックエンドとして追加されました。

ここで web1 の Serf エージェントを終了して、クラスタから除きます。

1
2
(web1) $ ./serf agent ...
(Ctrl + C)

proxy を確認すると、web1 がバックエンドから除かれていることがわかります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat server.list
192.168.55.57 80
$ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:18:37 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.57:80 192.168.55.57:80
(proxy) $ curl http://localhost
web2
(proxy) $ curl http://localhost
web2

まとめ

Serf + Synapse を使うことで、Web サーバを立ち上げて Serf を立ち上げるだけで、プロキシ側を触ることなくサービスに投入することができます。また、Web サーバを落とすだけで、バックエンドから自動的に除かれます。 Disposable Infrastructure と相性がよさそうですね。

実際、Synapse を使わなくても、Serf のイベントハンドラを使うことで HAProxy の設定ファイルを動的に更新することは可能なので、Synapse を使うメリットがあるかは賛否がわかれるところかもしれません。