SF(すごくふつう)なブログ

記録・メモ・自己啓発・他

SerfをAWSで使った

serfとは

serfdom.io

クラスタ管理用のツールです。クラスタ全体にメッセージ送信を行ったり、クエリーを実行することで各ノードの色々な情報を収集したりすることが出来ます。

serfの持っている機能としては、

  • クラスター形成機能
  • ノードに対するイベント通知機能
  • ノードに対するクエリー実行機能(クエリーを実行すると、各ノードにイベントが通知され、なおかつそのイベントの実行結果を収集することができます)

といったものがあります。

今回やりたかったこと

あるアプリケーションの動作に問題が発生していました。

  • 問題の動作

    • アプリケーションがメモリリークを起こしており、long runningさせると一定時間経過後(350時間経過後)ランダムにOutOfMemoryを起こしてアプリケーションが終了してしまう
  • 期待する動作

    • (A) アプリケーションがOutOfMemoryを起こす前に再起動させ、メモリリークをリフレッシュする
    • (B) アプリケーションは複数のサーバーで動いていて、それぞれのアプリケーションは排他的に再起動する

期待する動作Aは何かしらのプロセス監視ツールで実現できますが、Bはよくあるツール(例えばcron)でやろうとすると非常に手間がかかります。今回は上記のBをシンプルに実現するため、また技術的な興味からserfを使うことにしました。

serfを導入した後のシステム構成図

システム構成はシンプルで、各ノード(アプリケーションが動いているサーバー)に下記のツールがインストールされており、それらのノードがserfの機能によってクラスターを形成しています。

f:id:lycaon_mk2:20150928204455p:plain

ノードの動きの流れ

まずクラスターを構成する部分ですが、serfにはmDNSという機能があり、それが使える環境であればserfを立ち上げるだけでserf自身がクラスターをmDNS経由で探し出し、クラスターに参加します。ただAWS(と多くのIaaS環境)ではネットワークの制約によりmDNS機能を使うことができません。そのためAWSAPIから既存のノードのIPアドレスを取得しそれら全てに対して参加申請を行うことでmDNSが使えない状況を克服しています。 クラスターに無事参加すると全体の処理が動き出します。ノード毎に動いているプログラムの関係は次のようになっています。

f:id:lycaon_mk2:20150928204940p:plain

アプリケーションの生死を実際にコントロールするmonitプログラム、serfのクエリーを実際に処理するinfratoolプログラムが動いています。

アプリケーションがリスタートする[*]かどうかを判定する処理

[*] この動作を以降 `切腹` もしくは `seppuku`と呼びます。

今回の事例ではあくまでシンプルな協調動作になるようにしています。本当にきっちりやるのであれば何らかのロックをクラスター上で取得できる仕組みを用意し、切腹するノードはそのロックを取得するような仕組みにするのが最も安全だと思います。 シンプルな協調動作(クラスター内でのロックを使用せず、個々のノードがそれぞれ切腹するかどうかを判断する)を実装するために2つのクエリーを使用することにしました。 クエリー1: appuptime クエリー2: chash クエリー1のappuptimeはアプリケーションのuptimeを取得するクエリー、クエリー2のchashは、そのノードから見えているノードから取得したuptimemd5ハッシュ値を取得するクエリーです。 ハッシュ値の計算は、各ノードからappuptimeクエリーで取得したfloat値を元にホスト名のソートを行い、そのソート後のホスト名を順に連結し最後にmd5ハッシュ値を計算します。これによって各ノードから見えているノードグループにおいて、そのappuptimeの順序が同じであればソートされたホスト名の配列も全てのノードグループにおいて同じ順序になり、結果としてハッシュ値も同じという結果が得られます。

発生した問題

Query実行中に同じノードでQueryを実行した際の挙動がおかしい

serfがクエリーを実行している最中に別のクエリーを走らせるとpast deadlineというエラーが吐かれ、結果が返ってこないという現象に遭遇しました。詳細は後述しますが、イベントの実行がGoのチャンネルにキューの形で管理されており、1つのイベントが実行完了しないと次のイベントが実行されません。これが原因で、あるクエリーが後に実行されるクエリーに依存している状態(反依存)だとクエリー実行がロックされ、結果としてタイムアウトしてしまいます。 そのため今回の構成では反依存が必要なクエリーは別々に実行し、依存されているクエリーの結果をキャッシュさせるという方法でこの挙動を回避しました。

下記のコードからserfのイベントをディスパッチは1個のゴルーチンで実行されているので、一つのイベントの実行が完了しないと他のイベントの実行が開始されないことがわかります。

// github.com/hashicorp/serf/command/agent/agent.go#L238
// eventLoop listens to events from Serf and fans out to event handlers
func (a *Agent) eventLoop() {
    serfShutdownCh := a.serf.ShutdownCh()
    for {
        select {
        case e := <-a.eventCh:
            a.logger.Printf("[INFO] agent: Received event: %s", e.String())
            a.eventHandlersLock.Lock()
            handlers := a.eventHandlerList

実際に動かした結果

本番環境で動かした結果のメモリ使用量が次のようになりました。 メモリ使用量が抑えられており、当初の目的が達成されたことがわかります。

enabled disabled
memory usage ratio @ 210 hours running 28.4% 48.7%

結論

serfによる複数ノード間の協調動作は上手くいったと思っています。serfはとても簡単に分散型のノード管理を実現できるプロダクトなので、今後も色々なところで使いたいと思います。

余談

システムのログをSlackに流すようにしたのですが、非常に便利です。ただしログが無制限に保存できるStandardプラン以上の契約は必須です。

f:id:lycaon_mk2:20150928200314p:plain

当ブログのコンテンツの引用は自由です。