毎週書く予定だったのにすっかり間があいてしまった。細かいtipsはQiitaに書いたりしてるので、こっちに書くのはある程度まとまった話にしたいと思う。

ここ最近開発・運用していたサービスが落ち着いたので、一度感想を書いておこうと思う。一人でインフラの設計、開発、運用までやるのは初めてだったのでいろいろ勉強になった。 terraformconsulを半年くらい使ってるので、感想を書いておく。今回はconsul

サービスの要件

サービスはAWS上で動かす。 いわゆる普通のWebサービスと違うのは、Websocketサーバのようなコネクションを持つサーバがあり、単純にELBで負荷分散できない部分がいくつかあること。イメージとしては、ニコニコ生放送みたいな、動画のストリームを受けて配信するサーバ(サーバAとする)と、リアルタイムにコメントをブロードキャストするサーバ(サーバBとする)、のようなものに近い。

サーバAは配信するクライアントとコネクションをはるタイプのサーバだけど、サーバ側のCPU負荷が高くサーバ1台で5-10本くらいしか捌けない。

サーバBは、チャットルームのようなものが存在して、部屋に入っている人同士がWebsocketサーバ経由で大量のメッセージを送り合う。そのため、同じ部屋に入ったユーザは同じサーバに割り当てる必要があり、部屋ごとにユーザ数やメッセージ数が異なるので均等に負荷を割り振ることが難しい。

サーバA、Bともに負荷が上がると新たに増やす必要があるし、全てのコネクションが切れるまでサーバを落とすことはできない。

あとは、普通のAPIサーバやWebのフロントサーバがあるけど、それはELBがあれば問題ない感じ。

Consulを使う

ユーザが新たに配信を始める際、どのサーバA・Bに接続させたらよいか判断するためには、それぞれのサーバのリストと負荷状況を知る必要がある。また、サーバ台数を増やしたり減らしたりすることがあるので、そのサーバに接続してもいいのかどうかも知る必要がある。 そこで、サーバの管理にconsulを利用することにした。

consulとは

consulのまとまった日本語の説明を探してみたけど、いい感じのが見つからなかった。hashicorpのサイトを見るのが分かりやすい。

上記ページにあるように、サービスディスカバリとヘルスチェックができてKeyValueStoreの機能も持ってる、クラスタ管理のための仕組み。HTTPのAPIやDNSのインタフェースもある。マルチデータセンタの機能もあるけど今回は使ってない。

Consulをどう使ったか

以下では、consulをどのように使ったか、説明する。

クラスタ参加

まず、サービスをデプロイする時、consulのサーバを数台立てる。次に、各サーバを立てるのだが、その際に先に立ち上げたconsulサーバのアドレスを教えることで、各サーバはconsulのクラスタにjoinできる。これで、各サーバはconsulのクライアントとなる。

各サーバには、1つ以上の"サービス"を持つ。例えば、api-serverとか。1つのサーバが複数のサービスを持ってもいい。例えば、admin-serverlog-serverとか。 このサービスがconsulに登録される。サービスには、name, id, address, tagsが含まれる。idはユニークなid。これらがconsulに管理される。

参考 - Service Definition - Consul by HashiCorp

この情報はHTTP API経由で取得できる。

ヘルスチェックを設定する

各サービスには、それぞれヘルスチェックが登録できる。 たとえば、CPU使用率とか、httpのレスポンスが一定時間以内に返るか、とか。 その値によって、passing, warning, failingといったstatusになる。

参考 - Check Definition - Consul by HashiCorp

これで、それぞれのサービスが動作してるかある程度監視することができる。 ヘルスチェックの値もHTTP API経由で取得できる。

例えばサービスのnameがapi-serverで、ヘルスチェックがpassingなサーバのリスト、などを取得できる。

DNSを利用する

クラスタ内では、どのサーバがどのIPアドレスを持っているか各サーバは知らないが、websocketサーバからapi-serverにHTTPリクエストを投げたい。api-serverは台数も変わるしアドレスも変わるので、各サーバが把握することはできない。そこでconsulを利用するのだが、どのapi-serverでもいいから処理を投げたい、という場合にはDNSインターフェースを利用するのが便利。

dnsmasqとconsulのDNSを設定しておけば、consulクラスタ内で api-server.service.consulというURLが利用できる。なお、このとき返ってくるアドレスは、ヘルスチェックに通っているサービスのアドレスだけである。
もしすべてのapi-serverがバグっててヘルスチェックに通ってなければ、1台も返らず名前解決に失敗することになる。

$ dig api-server.service.consul
(中略)
;; QUESTION SECTION:
;api-server.service.consul.     IN      A

;; ANSWER SECTION:
api-server.service.consul. 0    IN      A       10.0.5.123  
api-server.service.consul. 0    IN      A       10.0.17.123  

参考 - DNS Interface - Consul by HashiCorp

これで、websocketサーバはこのapi-server.service.consulをいう変わらないURLだけ把握しておけばよくなる。

Consulを使った、サーバの負荷分散

ここまではconsulが提供する機能をそのまま使えばできる。 今回それに加えてやりたかったのは、api-serverに新たなリクエストが来た時に、どのwebsocketサーバが一番余裕があるかを判断し、ユーザに割り当てること。そこで、goで簡単なサーバを書いた。

まず、websocketサーバの負荷はある程度その接続数に応じて増えるので、そのときの接続数を判定の基準とすることにする。もちろん、CPU使用率などを利用してもいい。 そして、websocketサーバに対する接続数をconsulのヘルスチェック機能を使ってconsulに登録する。接続数が閾値を越えたらヘルスチェックでwarningにできるので、その閾値を負荷テストを行って設定しておく。

次に、goで書いたサーバはapi-serverからのリクエストが来たら、consulのHTTP APIを叩いて、ヘルスチェックに通ったwebsocketサーバのリストを取得し、それぞれのヘルスチェック時の接続数を元に、一番接続数が少ないサーバをapi-serverに返す。

これで、負荷が少ないサーバを優先的に返すことが可能になる。 この設計のメリットとしては、api-serverからのリクエストのたびに各websocketサーバに問い合わせる必要がなく、各websocketサーバの最後のヘルスチェックの値が利用できるので、比較的早く答えを返すことができる。一方でヘルスチェックのintervalが大きいと、最新の値と乖離した値を返してしまう可能性があるので、check_update_intervalを適切に設定する必要がある。 また、もちろん接続数以外の原因でCPU負荷やその他の負荷が高まった場合や、websocketサーバが何らかの問題が起きている場合にも、各ヘルスチェックでwarningになればリストから外せるので、問題ない。

参考 - Configuration - Consul by HashiCorp

なお、consulのAPIを利用するgoのサーバは、consul/api を利用すると簡単に書くことができる。

クラスタから取り除く

今まで説明した、websocketサーバを更新する、あるいはサーバを減らすために停止する、というときには、新たな接続が来るのを止めて、全てのコネクションが切れるのを待つ必要がある。

上記の仕組みを利用していれば、consulクラスタから一度外してしまえば新たな接続が来なくなるので、その上で接続数が0になるまで待てばよい。

一時的に外したい場合には consul maintコマンド、完全に外すときには consul leaveコマンドを使って、consulのクラスタから外すことができる。

参考

まとめ

若干忘れているので、あまり正確でない部分があるかも。 もっといい使い方があれば教えて欲しいです。

まとめると、consulを使うと複数台のサーバの管理がとても簡単にできるようになります。セットアップが簡単で、欲しい機能がちゃんと用意されていて、かなりよくできていると思う。ここには書かなかったけど、KVSの機能も一部使ってる。 半年使って、一時期raft関係のエラーがかなり出ていて不安になったくらいで、特に問題は起きてなかった。ただ、consulサーバが死んだり、consul自体のバージョンを上げようと思うと結構大変かもしれない。