LobiにおけるConsul活用事例

Lobiチームの長田です。 今回はConsulのLobiにおける活用事例を紹介します。

Consulとは

https://www.consul.io/

HashiCorp社が公開している、複数ホストを管理するために必要な機能が盛り込まれたツールです。

公式サイトには以下のように書かれています(意訳):

  • Service Discovery
    サービスの登録とDNSまたはHTTPインターフェイスを用いたサービスの特定。 SaaSのような外部サービスも登録することができる。
  • Health Checking
    オペレーターがクラスタ内で問題があったことを素早く知ることができる。 Service DiscoveryはHealth Checkingと連動し、 問題のあるホストがサービス特定の対象になることはない。
  • Key/Value Storage
    動的な設定・フラグなどをを保持するための柔軟なkey/valueストア。 シンプルなHTTP APIでクラスタ内のどこからでもアクセスすることができる。
  • Multi-Datacenter
    複雑な設定を行うこと無く複数のデータセンターをまたいで利用することができる。

最後のMulti-Datacenterについてはよほど大規模なクラスタを組まなければ利用することはないかもしれませんが、 それ以外の機能については数台のホストで構成されるクラスタを管理する際にも便利に利用できます。

各種情報を保持するconsul-serverと、その情報にアクセスするconsul-agentから成り、 実装にはGo言語が使用されています。

それではLobi内での具体的な活用事例を紹介していきましょう。

Lobi内での活用事例

chefによるconsul serviceの定義 (service)

Lobiでは各ホストのプロビジョニングにchefを使用しています。

consul serviceの定義はConsulの設定ディレクトリにchefでjsonファイルを配置することで行なっています。


node[:consul][:services].each do |sv|
  file "/etc/consul.d/#{sv['name']}.json" do
    owner "root"
    group "root"
    mode  0644
    content( { "service" => sv }.to_json )
    notifies :reload, "service[consul]"
  end
end

chefのroleやnodeごとに定義したservice(node[:consul][:services])の他に、 chefのroleや、


file "/etc/consul.d/role.json" do
  owner "root"
  group "root"
  mode  0644
  content( { "service" => { "name" => "role", "tags" => node[:roles] } }.to_json )
  notifies :reload, "service[consul]"
end

daemontoolsで起動しているサービス、


file "/etc/consul.d/daemontools.json" do
  owner "root"
  group "root"
  mode  0644
  content( { "service" => { "name" => "daemontools", "tags" => node[:daemontools_services] } }.to_json )
  notifies :reload, "service[consul]"
end

例えば、各種ログを集約するlog-aggregatorというchef roleが適用されているホストの場合、 log-aggregator.role.service.consul.という名前を解決する際の対象ホストとして登録され、 各ホストからはこの名前を指定することでいずれかのログ集約ホストにログを送信することができます。

他にも、

  • メール送信サーバー(postfix)
  • 内部用proxy(squid)

についても同様にconsul serviceに登録することで送信先の名前解決を行なっています。

各アプリケーションのセカンダリ参照 (service, dns)

Service Discoveryの機能を使い、クラスタ内における名前解決を行なっています。 dnsmasqを経由することで通常のDNSと同じように利用することができます。

Lobi内ではid発番にはkatsubushi、 iOSへのプッシュ通知送信にはgunfishを使用しています。 これらのアプリケーションはクラスタ内の各ホストで起動しており、 利用側はローカルホストを参照することでアプリケーションに接続します。

Consulにサービスとしてこれらのアプリケーションを登録することで、 ローカルホスト以外をセカンダリとして利用することができます。


# PerlモジュールのCache::Memcached::Fastを使用してkatsubushiに接続する場合
# ※katsubushiはmemcachedプロトコルを使って接続を受け付ける
my $client = [
    # 正常時はlocalhostで起動しているkatsubushiに接続
    Cache::Memcached::Fast->new({ servers => ["localhost"] }),
    Cache::Memcached::Fast->new({ servers => ["katsubushi.service.consul"] }),
];

# localhost -> katsubushi.service.consul の順でid取得を試みる
for my $client (@$client) {
    my $id = $client->get("id");
    return $id if $id;
}

デプロイイベントの通知 (event, watch)

Lobiではデプロイ操作をstretcherを用いて行なっていますが、 この起動をconsul event経由で行なっています。


deploy server --> consul event --> target hosts

daemontools serviceのrunスクリプトは以下のような物を使用しています。


# manifestのURI($uri_for_manifest)がeventの値として送られる
consul event -name deploy -node $target_nodes_regex $uri_for_manifest

daemontools serviceとしてconsul watchを実行しeventを受け取ります。 runスクリプトは以下のようになっています。


#!/bin/sh

set -e
exec 2>&1

umask 002

# consul watchでconsul eventの発火を待ち受け
# eventを受け取るとstretcherコマンドが実行される
exec setuidgid lobi consul watch -type event -name deploy stretcher

stretcherによるデプロイについて、詳しくは以下の記事を参照下さい。

ファイル更新もchef実行もstretcherで!Lobiをデプロイするときにやっていること | tech.kayac.com - KAYAC engineers' blog

多重実行防止 (lock)

Lobiではスムーズな開発をサポートするため、 また一般ユーザー向けのイベントを開催するためなど、 様々な用途でchat botを動作させています。

botアプリケーションの冗長化については、 複数のホストで同一のbotアプリケーションを動作させることで行なっています。 HTTPリクエストを受け付けて動作するようなものであれば ロードバランサーがひとつのホストだけに振り分けるので問題ないのですが、 たとえばWebSocketでイベントを受け取る場合、 (イベント送信側の実装にもよりますが)複数のbotアプリケーションが同時にイベントを受け取ることになってしまいます。 この問題はbotアプリケーション間で連携することでも解決できますが、 小規模なbotアプリケーションの場合あまり複雑な仕組みは入れたくないものです。

そこでconsul lockを使って、 クラスタ内にひとつのbotアプリケーションしか起動しないようにしました。 このbotアプリケーションの起動コマンドは以下のようになっています。


exec consul lock bot/some-bot-app "path/to/bot.sh"

第1引数のsome-bot-appはロックのキー、 第2引数のpath/to/bot.shロック取得成功時に実行するコマンドです。 同一のキーでロックが取得されている場合は第2引数に指定されたコマンドを実行せず、 ロックが取得できるまで待つようになっています。 ロックは第2引数のコマンドが終了すると同時に解放されます。

(botアプリケーション自体の不具合で再起動が必要なのであればdaemontoolsなどで起動すれば事足りるのですが、 それが起動しているホストごと不調になった場合にそなえてこのような仕組みが必要になります)

もちろんconsul lockを使った冗長化で対応できるのは、 botアプリケーションが捌くイベントがひとつのアプリケーションで対応できる範囲に収まる場合だけです。 より大規模なbotアプリケーションを冗長化する際には別の解決方法が必要になるでしょう。

再起動時の自動組み込み防止 (maint)

Lobiではアプリケーションからデータベースのslaveに接続する際に HAProxyが設定されたweightに基づいてどのslaveに接続するべきかを決定します。 接続先として適当かどうかはMHAから各データベースslaveに対してヘルスチェックを実行することで判断しています。

以下はhaproxy.cfgの一部です。 ここでは、

  • 3台のデータベースslaveについて
  • ヘルスチェックを各ホストに対して行い
  • weightの偏りなく接続を振り分ける

よう設定しています。


listen  mysql-slave-main
        bind            0.0.0.0:3307
        mode            tcp
        option          httpchk
        balance         roundrobin
        server          db-slave01  db-slave01:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3
        server          db-slave02  db-slave02:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3
        server          db-slave03  db-slave03:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3

各ホストのヘルスチェック応答用のアプリケーションはdaemontoolsで実行され、 ホストの起動時に自動的に起動します。 つまり、起動と同時にデータベースslaveへの接続が手配されるわけですが、 起動直後はメモリ上にデータが載っておらず、disk readが発生する確率が非常に高くなっています。 この状態で他のホストと同じweightで接続を振り分けるのは危険です。

再起動前にconsul maintを使って ホストをメンテナンス状態にしておくことでこの問題を解決しています。


$ consul maint -enable "データベース設定変更"

ヘルスチェック応答用アプリケーションでconsul maintの状態を確認し、 有効であればアプリケーション自体が起動しないようにしています。 consul maintの設定状況はconsul serverが保持しているため、 対象ホストの再起動等でこの情報が失われることはありません。


# consul がメンテ状態の場合は起動しない
maint=$(consul maint)
if [[ $maint != "" ]]; then
    echo "$maint"
    sleep 10
    exit 1
fi

# 以下ヘルスチェック応答用アプリケーション起動処理
...

HAProxyの設定で対象のデータベースslaveについてweight=0にしてしまえば同様の結果は得られるのですが、 そのためにはHAProxyが稼働している全てのホストで設定変更が必要になります。 consul maintを有効にしてヘルスチェックが失敗するようにすれば 対象のデータベースについての変更のみでクラスタ内のすべての関連ホストからの接続を排除することができるのです。

また、consul maintを有効にしておけばconsulが提供するserviceの名前解決の対象からもはずれます。 HAProxyなどの仕組みを使わない場合はこれによりそのホストへの接続等を制御することができます。

Lobiにおけるデータベースの冗長化については以下を参照下さい。

LobiのDB masterがダウンすると何が起こるのか

スパムIPリストの更新 (consul-template)

Lobiはチャットを主軸としたサービスです。 特定のメンバーのみが閲覧できるプライベートなチャットグループの他に、 誰でも参加・閲覧できる公開チャットグループがあります。

公開チャットグループはLobiのアカウントさえあれば誰でも書き込みができるという性質上、 一定の割合でスパム投稿や荒らし投稿が発生します。 これについていくつか対策を講じているのですが、そのひとつとしてIPアドレス単位のアクセス拒否を行なっています。

とある場所から拒否するべきIPアドレスのリストを取得しnginxのdeny対象として設定しているのですが、 この設定の反映にconsul-templateを使用しています。

consul-templateはconsul kvの変更をトリガーに、

  • 対象ファイルの更新(nginx設定ファイルの更新)
  • コマンドの実行(nginx reload)

を行うことができます。


script(run by crontab) ---> consul kv ---> consul-template ---> update nginx conf, exec nginx reload

consul kvの変更はcrontabに登録したスクリプトで定期的に行うようにしています。

以前は同様のことをchefのdata bagとtemplateを使って行なっていました。 chef実行の前処理として拒否対象IPアドレスをdata bagに保存し、 chef実行時にdata bagの内容を元にtemplateからnginx設定ファイルを更新、nginxをreloadする、という流れです。

この方法の問題点として、

  • chef実行は頻繁に行われるわけではないので拒否対象のIPアドレスリストが最新に保てない
  • 拒否対象IPアドレスリストは頻繁に変更されるためchefのレポートが肥大化する

といったものがありましたが、consul-templateを用いることで解決しています。

ダッシュボード (members, kvs)

クラスタ内の各種ステータスを一覧する場所として Consul KV Dashboardを使用しています。

Consul KV DashboardはConsulをバックグラウンドとして使用しているダッシュボードアプリケーションです。 Lobiでは、

  • 各ホストのデプロイログ
  • 各ホストのserverspecの実行ログ
  • 定期実行処理の実行ログ

などをこのダッシュボードで一覧できるようにしています。

各ログはconsul kvsに保存され、 ノードリストはconsul membersを元に表示されるため、 たとえばホストをシャットダウンした場合は自動的にダッシュボードからは非表示になります。

dashboard.png

コマンドの発行 (exec)

consul execを使用すると クラスタ内のホストに対してコマンドを発行することができます。


$ consul exec -node "hostname regexp" "command"

-nodeパラメータで対象ホストをホスト名で絞り込むことができます。 Lobiではこれを利用してデプロイ時のdaemontools管理のアプリケーションの再起動を行なっています。


$ consul exec -node "lobi-app-" "sudo svc -h /service/app"

注意点として、コマンドの実行はconsul userとして各ホストで実行されるため、 sudoするが必要な場合はsudoersでconsul userにそのコマンドのパスワード無し実行を許可する必要があります。


consul  ALL=NOPASSWD: /command/svc

用途が限定的なコマンドであればNOPASSWDに設定しても問題はないでしょう。 汎用的なコマンドを設定する場合は本当にその必要があるのかを充分に検討して下さい。

ssh先の選択 (members)

OSの設定変更やミドルウェアのインストール・アップデートはchefで行なっているため、 通常であれば各ホストに直接sshログインすることはありません。 しかし、不具合発生時の調査など、sshログインが必要になる場合は発生します。

Lobiでは常時数十台以上のホストが稼働しており、なおかつAuto Scalingしているため、 sshログイン先のIPアドレスをて入力するのは現実的ではありません。

そこで.bashrcに以下のような関数を定義し、sshログインの手間を簡略化しています。


function lobi_ssh() {
    host="$(consul members | grep alive | cut -d' ' -f1 | sort | peco)"
    if [[ -n $host ]]; then
        ssh "$host"
    fi
}

pecoはリスト内のインクリメンタルな検索機能を提供する汎用的なツールです。 consul membersで取得したホストリストから稼働中のホストを抽出しpecoに渡し、 インクリメンタルに検索してsshコマンドを実行する、という関数になっています。

members.png

いちいちホスト一覧を参照してIPアドレスを調べる手間もなく、大変便利です。

参考資料

おわり

Consulの様々な活用方法を紹介しましたが、これらすべてを利用する必要はありません。 その必要な機能を必要な部分で採用し、クラスタの管理を簡略化していきましょう。

次回はLobiのstreaming APIについて紹介します。

カヤックではツールを使ってインフラ管理を楽にしたいエンジニアも募集しています!