Saturday, November 22, 2014

docker用の超絶シンプルDNS

fig使って遊んでいたら、どうしても欲しくなり、Goでプログラム書きたかったこともあり、作ってみた。

実際にはこんな回りくどいことしなくても、既存のDNS使った、マシなソリューションがあると思う。参考

まあ200行くらいで済んだし、これはこれでいいかなー。

go-docker-dns

Github

原理は死ぬほど単純で、dockerのリモートAPIをひたすら見守り続けて、コンテナの作成イベントを捉えたら、そのホスト名とIPの組を登録。逆にコンテナ停止のイベントを捉えたら、その情報を削除する。これとは別のDNSのスレッドが動いていて(ほとんど流用だけどっ)、dockerコンテナからの要求に応じて、上で登録したホスト名-IPの組を見つけたら、そのIPを回答している。

逆引きは実装していない。結構簡単に実装できそうだけど、現状でも用途には応えているから作る理由もない…。

もともとfigを使って、普段使っているPC上で比較的大きなシステム(WebAPI x MQ x KVS x RDB x LB)のはりぼてみたいなのを作り、その動作を検証するといったことをやりたかった。

この時、DNSがいないとクラスタを組む類のサービス(典型的には、今回変なDNSを実装するきっかけになったRabbitMQがスケール時にまともに動かない。

RabbitMQの場合:masterノードとslaveノードを用意し、masterノードに対して複数のslaveを登録する仕組みを今回作ったが、fig scale mqslave=3としたときに、docker --linkの仕組みでmasterノードのIPアドレスは解決できるが、他のslaveノードのIPアドレスはDNSがいないと解決のしようがない。

他の人のアプローチは、もっとslaveノードが欲しかったら、

fig.yml

rabbit:
    image: bijukunjummen/rabbitmq-server
    hostname: rabbit_1
    ports:
        - "5672:5672"
        - "15672:15672"
mqnode1:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode_1
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true
    links:
        - rabbit
mqnode2:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode_2
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true
    links:
        - rabbit
        - mqnode1
...

こんな感じに増やしていけばいいじゃない!って言ってるけど、流石に「ス、スケールとは一体」…って言いたくなる。fig scale mqnode=3ってやったら1 master x 3 slaves の構成でクラスタが構築できてほしい。

コンテナ用のDNSサーバーを立てることで、わざわざfig.ymllinksを記載しなくても、scale時に名前解決してくれるようになる。

ここでもう一つ面倒な話があって、(RabbitMQでは)コンテナのホスト名がクラスタを組むときに大事になってくる。具体的にいうとfigは、scaleして増やすコンテナのホスト名をfig.ymlで記述されたものを使う。fig scale mqnode=3とやると、mqnodeというホスト名を持つコンテナが3つできるorz じゃあ、どやって名前解決してるの?っていうと、これとは別に環境変数envdocker exec <コンテナID> echo envで覗ける)と/etc/hostsに、linkのところで指定された、関係のあるコンテナのIPと外側向けホスト名みたいのを列挙している。(この辺はdocker側の実装。)

この状況だと、RabbitMQのmasterノードはslaveノードをクラスタに参加させる時に、「お前、slave_1っていう名前で参加しようとしてきたのに、実際にはslaveじゃん。」みたいな事を指摘して参加が上手くいかない。頑張って参加するときの名前とホスト名を一致させても、今度は2つめ以降のslaveが参加するときにホスト名が被る。つまり、1ノードだけが運よく参加できて、他は今参加しているノードが落ちるまで参加できない…\(^o^)/ いや、ある意味、可用性あるけど、そうじゃないいいいいいい。

fig改悪?版

という訳でfig本体にもしぶしぶ手を入れた。具体的には上に書いたようにホスト名をscaleを考慮した形で生成する。

以下のようなfig.ymlがあったとする。

mqnode:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true

fig up -d mqnodeとやると、コンテナが1つだけ立ち上がる。ホスト名はmqnodeではなく、mqnode_1。続いてfig scale mqnode=3。これで新しいコンテナが2つ立ち上がる。ホスト名はそれぞれmqnode_2, mqnode_3。自作したDNSサーバーは、コンテナが作成されたイベントを検知すると、このホスト名とIPの組をレコードとして登録する。IPアドレスが10.10.0.1から始まってたとすると、こういうレコード。

mqnode_1 10.10.0.1
mqnode_2 10.10.0.2
mqnode_3 10.10.0.3

RabbitMQのクラスタ構築時の動きが

  1. slaveノードが起動する
  2. slaveノードがmasterノードに自身の存在を通知
  3. masterノードが参加してきたslaveノードに他のslaveの存在を教える
  4. slaveノードが教えてもらった他のslaveノードにアクセスを試みる

ってな感じになってるから、fig側の修正とDNSサーバの合わせ技で、この振る舞いが正しく機能するようになった。

オマケ

開発中は、必要に応じてコンテナを頻繁に作り直すことになると思うんだけど、fig up serviceの現在の実装がとても気に入らない。

fig up -d service
fig scale service=3

こうすると、ホスト名、およびコンテナ名がservice_1, service_2, service_3となり、これはいい。でも、この後、もう一度

fig up -d service

ってやると、service_1, service_2, service_3が削除され、service_3, service_4, service_5が新たに立ち上がる。なんで番号が増えんのよorz

別途立ち上げていたnginxコンテナにロードバランスの設定を

upstream webapp {
        server service_1;
        server service_2;
        server service_3;
    }

こんな感じに書いてるものだから、コンテナを作り直す度にnginxの設定を書き直さなきゃならなくなる。流石に気が滅入ってくるorz ので、fig up -d serviceした時に既存のサービスが立ち上がっているなら、そのホスト名で作り直すように変更。better_recreateって名前のブランチで、ここまでに書いたfig側の変更を全て反映している。

まとめ

  • Goの練習も兼ねてdocker用の超絶シンプルなDNSを作成
  • figを使ってRabbitMQクラスタを自動構築できるようにした
  • figのコードをちょっと変えてrecreate時にホスト名を引き継げるようにした

ここまでやって、メモリ4G程度のゲストマシン(CoreOS)上に計18のサービス(Rails App 2種類, KVS, RDB, Node.js App, Nginx)が協調動作する環境を構築できた。全部動いていると、だいたい3.8Gくらいメモリを消費。

なんやかんや問題も多いわけだけど、figみたいのを使うと、これまで鯖を何台も用意、provisonerなんかを使って構築していた比較的大きいシステムを、(ハリボテだけど)各開発者が自分のマシン上に持てるようになる、これで開発効率上がるといいな、とか思ったり思わなかったり。ここまでできると、本番環境へのデプロイを同じ設定ファイル使いまわしで実現したくなるね。

No comments:

Post a Comment