理系学生日記

おまえはいつまで学生気分なのか

Service Discoveryができるconsulっていうものは果たしてどういうものなのか

MicroServices、コンテナオーケストレーションなどが花盛りですが、そこに本格的に踏み出すにはまだ若干の時間が必要なように思います。 とはいえ、サーバの動的な構成変更への対応はいくらでも出てくるので、サービスディスカバリと、サーバのステータスをトリガにしたアクションには興味がありました。

そういうわけで、まずは Consul by HashiCorp の機能を試してみたいと思います。

まずは Consul とは何か

Consul は、以下の機能を持ったツールです。

  • サービスディスカバリ
  • ヘルスチェック
  • K/V ストア

「ツール」と書いたように、実装として取り込む「ライブラリ」ではなく、独立して動作する「バイナリ」として提供されます。

アーキテクチャ

アーキテクチャとしてはクライアント・サーバ構成になっていて、クライアント、サーバともに複数台から構成することで可用性を上げる考えになっています。 クライアント、サーバをまとめて、Consul Agent と呼ばれます。

基本的には、データセンタ(ここでの「データセンタ」は、一般的な意味な DC ではなく、低遅延、高帯域のネットワークで繋がれた環境として定義されます) 内でクライアントとサーバがクラスタとして動作し、 このデータセンタ内に閉じて、メンバシップ情報やサービスステータスといった情報共有を行います。

一方、データセンタ間は個々のデータサーバ間の互いのサーバ同士が、互いの情報の共有を行うという多層構造になっています。

公式のガイドでいうと、以下の図が分かりやすいかと思います。

f:id:kiririmode:20180615135725p:plain
Consul Architecture | Consul by HashiCorpより引用

上記で LAN/WAN GOSSIP と書かれているところは SWIM をベースとした通信を、サーバ間あるいはクライアント間で行われる GOSSIP については、それぞれ以下のエントリを参照ください。

個々の Consul Agent は、HTTP API と DNS Interface を持っていて、

  • ノードの情報
  • サービスの情報

なんかが、これらのインタフェースを利用することで参照できます。 DNS Interface を持っているっていうことは、アプリケーションからはホスト名をちょっと変えるだけで、わりと transparent に Service Discovery を利用できるってことですね。

クラスタを構成する

そこそこの台数の consul agent でクラスタを形成したかったので、Docker Composeを使うことにしました。

Dockerfile

consul に関しては、公式が alpine ベースのイメージを提供してくれています。

いくつかコマンドを使いたかったので、以下のような Dockerfile を用意しました。 上記の consul の image をベースにして、apache2 をインストールしています。 httpd とコマンドを叩けば apache が起動するようになっています。

FROM consul:1.1.0

ENV HTTPD_DIR /usr/local/apache2
ENV HTTPD_PID_DIR /run/apache2

# We install apache2 with some utility commands, but it is to be run manually.
RUN set -ex \
    && apk update \
    && apk add --no-cache --virtual .deps \
           curl \
           bind-tools \
           apache2 \
    && adduser -D -S -G www-data www-data \
    && mkdir -p "$HTTPD_DIR" "$HTTPD_PID_DIR" \
    && chown www-data:www-data "$HTTPD_DIR" "$HTTPD_PID_DIR"

docker-compose.yaml

docker-compose.yaml には、サーバとクライアントをそれぞれサービスとして用意します。 これにより、サーバ、クライアントとも、docker-compose の scale オプションで任意の台数を指定できるようになります。

version: '3'
services:
  server: &agent
    build:
      context: .
      dockerfile: Dockerfile
    image: kiririmode/consul:latest
    command: "agent -server -ui -bootstrap-expect ${SERVER_NUM} -retry-join consul_server_1 -client 0.0.0.0 -recursor=8.8.8.8"
    ports:
      - "8500-8549:8500"
      - "10000-10049:80"
  client:
    <<: *agent
    command: "agent -ui -retry-join consul_server_1 -client 0.0.0.0 -recursor=8.8.8.8"
    ports:
      - "8550-8599:8500"
      - "10050-10099:80"

クラスタをあげる

サーバ台数は、Raft を使用している関係と、パフォーマンスの見地から、3 台か 5 台が推奨されています。 ここではまず、1 つのデータセンタ内にサーバ 3 台、クライアント 5 台からなるクラスタを構築してみます。

$ export SERVER_NUM=3
$ docker-compose -p consul up --scale server=3 --scale client=5

まずはメンバーを確認します。 以下のように、意図したとおり、サーバ 3 台、クライアント 5 台のクラスタが構成されていることが分かります。

$ docker-compose -p consul exec --index=1 server consul members
Node          Address          Status  Type    Build  Protocol  DC   Segment
37087184553e  172.18.0.4:8301  alive   server  1.1.0  2         dc1  <all>
6a56ea9fa974  172.18.0.3:8301  alive   server  1.1.0  2         dc1  <all>
df2dc341f480  172.18.0.2:8301  alive   server  1.1.0  2         dc1  <all>
4804c51b4d9f  172.18.0.9:8301  alive   client  1.1.0  2         dc1  <default>
712f685e59ca  172.18.0.6:8301  alive   client  1.1.0  2         dc1  <default>
9c182e4347bc  172.18.0.8:8301  alive   client  1.1.0  2         dc1  <default>
d455dbe41191  172.18.0.5:8301  alive   client  1.1.0  2         dc1  <default>
fbd8cbc2cea2  172.18.0.7:8301  alive   client  1.1.0  2         dc1  <default>

Raft のステータスはどうでしょうか。Raft のステータスを確認するためには、consul operator raft <sub-command> を使います。 以下のように、Node 0a35 がリーダとして選出されていることがわかります。

$ docker-compose -p consul exec --index=1 server consul operator raft list-peers
Node          ID                                    Address          State     Voter  RaftProtocol
6a56ea9fa974  0a35ddd5-f851-e1dc-804e-1aad15a165b8  172.18.0.3:8300  leader    true   3
df2dc341f480  bc14f2e3-988f-5177-973d-78ab02ada1ed  172.18.0.2:8300  follower  true   3
37087184553e  e7636e01-6fa2-0610-a5d9-8b7da9a107b2  172.18.0.4:8300  follower  true   3

では、DNS Interface、HTTP API それぞれでメンバの状態を見てみましょう。

DNS Interface

consul は DNS サーバがバイナリに埋め込まれており、デフォルトでは 8600 番ポートで起動します。 名前空間は consul で終わるようになっています。 例えば、consul サーバを名前解決する場合は、consul.service.consul に問合せを行います。

結果としては、以下のように 3 台の server が名前解決できています。

$ docker-compose -p consul exec --index=1 server \
  dig +noall +answer @127.0.0.1 -p 8600 consul.service.consul
consul.service.consul.  0       IN      A       172.18.0.3
consul.service.consul.  0       IN      A       172.18.0.2
consul.service.consul.  0       IN      A       172.18.0.4

HTTP API

HTTP サーバについてもバイナリに埋め込まれており、参照系・更新系といった種々の機能が利用できます (そういう意味で、DNS Interface より高機能です)。

以下は、メンバの情報を参照する API ですが、このように 8 台の情報が返ってくることが分かります。

$ docker-compose -p consul exec --index=1 server \
  curl http://127.0.0.1:8500/v1/catalog/nodes \
  | jq -r '.[].Address'
172.18.0.4
172.18.0.9
172.18.0.3
172.18.0.6
172.18.0.8
172.18.0.5
172.18.0.2
172.18.0.7

ノードの死に対する耐性

consul において、ノードのステータスは通常状態の他に、left と failed があります。 行儀よくクラスタを離脱した場合は left、いきなりの故障が発生した場合は failed に遷移します。

ここでは、3 つ目のサーバに SIGKILL を発行してみます。

$ docker kill -s KILL consul_server_3
consul_server_3

こうすると、クライアント 5 はもはや周囲のノードからの ping に応答しなくなり、SWIM の中で failed と判定されます。実際、consul members の結果は以下のとおりで、1 台のクライアントが failed の ステータスになっていることがわかります。

$ docker-compose -p consul exec --index=1 server \
  consul members
Node          Address          Status  Type    Build  Protocol  DC   Segment
37087184553e  172.18.0.4:8301  alive   server  1.1.0  2         dc1  <all>
6a56ea9fa974  172.18.0.3:8301  alive   server  1.1.0  2         dc1  <all>
df2dc341f480  172.18.0.2:8301  failed  server  1.1.0  2         dc1  <all>
4804c51b4d9f  172.18.0.9:8301  alive   client  1.1.0  2         dc1  <default>
712f685e59ca  172.18.0.6:8301  alive   client  1.1.0  2         dc1  <default>
9c182e4347bc  172.18.0.8:8301  alive   client  1.1.0  2         dc1  <default>
d455dbe41191  172.18.0.5:8301  alive   client  1.1.0  2         dc1  <default>
fbd8cbc2cea2  172.18.0.7:8301  alive   client  1.1.0  2         dc1  <default>

DNS Interface で問い合わせてみても、1 台減っていることが確認できます。

$ docker-compose -p consul exec --index=1 server \
  dig +noall +answer @127.0.0.1 -p 8600 consul.service.consul
consul.service.consul.  0       IN      A       172.18.0.3
consul.service.consul.  0       IN      A       172.18.0.4

Service Discovery

なんといっても、consul のユースケースの 1 つは Service Discovery です。 上記の consul.service.consul への DNS クエリもその 1 つではあったのですが、ここでは動的に Service の設定を追加してみます。

Docker ホストに、以下のような web.json を用意します。 これは、"apache" というサービスに関する設定として定義していて、10 秒毎に http://localhost に GET でヘルスチェックを実施します。

{
    "Name": "apache",
    "Port": 80,
    "Check": {
        "Name": "http on port 80",
        "HTTP": "http://localhost",
        "Method": "GET",
        "Interval": "10s",
        "Timeout": "1s"
    }
}

これを任意の consul エージェントに HTTP で PUT します。ここでは、1 番目のクライアントに PUT してみましょう。

# 1 番目のクライアントの 8500 番ポートにマッピングされているポート
$ docker-compose -p consul port --index=1 client 8500
0.0.0.0:8571

# Docker ホストからサービス設定を投入
$ curl -XPUT [email protected] http://localhost:8571/v1/agent/service/register

この段階では、1 番目のクライアントで httpd が立ち上がっていないので、"apache" というサービスをディスカバリしようとしても 存在しません。

# 1 番目のサーバ上で DNS での Service Discovery
$ docker-compose -p consul exec --index=1 server \
  dig +noall +answer @127.0.0.1 -p 8600 apache.service.consul
# 何も返却されません

では、httpd を立ち上げてみた後の結果が以下になります。 このとおり、きちんと apache サービスが Discovery できることがわかります。

# 1 番目のクライアント上で httpd を立ち上げる
$ docker-compose -p consul exec --index=1 client httpd

# 1 番目のサーバ上で httpd の Service Discovery
$ docker-compose -p consul exec --index=1 server \
  dig +noall +answer @127.0.0.1 -p 8600 apache.service.consul
apache.service.consul.  0       IN      A       172.18.0.8

もちろん、この設定を他のノードにも投入すれば、Service Discovery の結果の数が増えていきます。

今回は http でヘルスチェックを行いましたが、他にも TCP や、任意のスクリプトを用いてのヘルスチェックも実現できます。

K/V Store

consul は Key Value Store の機能も持っています。 ここでは、3 台目のクライアントで Key-Value を投入し、それを 2 台目のサーバで確認してみます。

まずは投入。key は kiririmode、value は dislikes onion としてます。

$ curl -XPUT -d "dislikes onion" http://localhost:$(docker-compose -p consul port --index=3 client 8500 | cut -d: -f2)/v1/kv/kiririmode
true

これをサーバで確認してみます。

$ curl http://localhost:$(docker-compose -p consul port --index=2 server 8500 | cut -d: -f2)/v1/kv/kiririmode\?pretty
[
    {
        "LockIndex": 0,
        "Key": "kiririmode",
        "Flags": 0,
        "Value": "ZGlzbGlrZXMgb25pb24=",
        "CreateIndex": 153,
        "ModifyIndex": 153
    }
]

このように、value は base64 でエンコードされています。デコードすると、たしかに別サーバで同じ値が取得できることがわかります。

$ curl -s http://localhost:$(docker-compose -p consul port --index=2 server 8500 | cut -d: -f2)/v1/kv/kiririmode \
  | jq -r '.[0].Value' \
  | base64 -d
dislikes onion

最後に

今回は ローカルホスト上に複数個の Docker Container として浮かべる形でクラスタを構成しました。 これは単にその方が手軽に検証できるからです。

consul を Container から利用する場合、

  • Docker Host に対して 1 つの Container とする
  • また、bridge ではなくて Host Networking (--net host) を使用する

というのが本来なのでご注意ください。

詳細は、consul をご参照ください。