Consulクラスタ内でファイルを分散配布する tuggle を書いた

github.com

これはなに?

HTTPを使って、ファイルを Consul クラスタ内で分散配布する daemon です。Go で書かれています。読みかたは「たぐる」です。

開発動機とユースケース

拙作の Stretcher というデプロイツールがあります。嬉しいことに、自分が勤務しているカヤックだけではなく、他社さんでも使われているようです。 先日、某社の Stretcher をお使いのかたに話を伺う機会があり、デプロイアーカイブの取得時のネットワーク負荷が問題になっているということを聞きました。

カヤックではサーバは基本 AWS にあり、S3 からデプロイアーカイブ(200〜300MB程度)を取得しています。S3はちょっと意味が分からないぐらい堅牢で、stretcherの -random-delay 5 (開始を平均2.5秒、最大5秒ずらす) を設定した状態で100台程度から一斉に200MBを取得しに行っても、平然と各ホストに対して 60〜80MB/sec ぐらいの転送速度でファイルを配ってきますし、エラーになることもほぼありません。(毎日10回deployしていて S3 からの取得が失敗するのは数ヶ月に一度とか)

ところがAWS外部から取得する場合、AWSまでの回線の問題もありますし、delayを大きく取っても結構な確率で失敗するので困っている、という話でした。

ということで、Consul クラスタ内部でファイルを分散配布し、HTTP で取得できる仕組みを作ってみました。

使い方

README に書いてあるとおりですが、HTTP の PUT, GET, HEAD, DELETE に対応しています。

Consul クラスタ内で各 node に tuggle を起動します。(デフォルトではTCP 8900番ポートを listen します)

[node{1,2,3}]$ mkdir /tmp/tuggle
[node{1,2,3}]$ tuggle -data-dir /tmp/tuggle

どこか1台の node で PUT することでファイルを登録できます。

[node1]$ curl -XPUT -H"Content-Type: application/gzip" --data-binary @test.gz localhost:8900/test.gz

登録されたファイルは GET / することで一覧を参照できます。

[node1]$ curl -s localhost:8900 | jq .
[
  {
    "id": "6524a9d7b3bde0f3543f1ead0ae8604f",
    "name": "test.gz",
    "content_type": "application/gzip",
    "size": 8764510,
    "created_at": "2017-02-13T07:46:18.809409122Z"
  }
]

その後、他の node で動いている tuggle から GET することで、ファイルを取得できます。

[node2]$ curl localhost:8900/test.gz > test.gz
[node2]$ ls -l test.gz
-rw-rw-r-- 1 foo bar 8764510 Feb 13 07:55 test.gz

ファイルの上書きは未対応です。同一ファイル名へは一旦 DELETE で消してから PUT しなおしてください。どこの node からでも DELETE 可能です。

[node1]$ curl -X DELETE localhost:8900/test.gz

動作原理

tuggle の動作原理を簡単に説明します。

tuggle は PUT を受け付けると、以下の動作をします。

  • ファイルを data-dir に保存
  • Consul KV にファイルに対応するメタデータを記録
  • Consul Service に name=tuggle tag=ファイル名のMD5 として自分の node を登録
  • (URL引数 sync がついている場合、Consul Event を発行して他の node の tuggle に取得を促す)

つまり、どの node がどのファイルを保持しているのかを Consul Service のタグで管理しています。

tuggleはGETを受け付けると、Consul KVのメタデータを参照した上で

  • 自分の data-dir に存在していればそれを返します
  • 自分が持っていない場合、Consul Serviceを name=tuggle tag=ファイル名のMD5 で検索し、ファイルを持っているnodeにHTTPで取得しに行きます
    • 取得が終わったら data-dir に保存し、自分自身を Service に登録した上でクライアントにファイルを返します

という動作をします。これによって、自分が持っていないファイルはその時点で持っている node を検索して取得し、その後自分自身も配布 node として動作することになります。

DELETEでは、受け付けた node は以下の動作をします。

  • Consul KV からメタデータを削除
  • Consul Eventを発行して、クラスタ内のtuggleにローカルファイルを削除するように通知

Stretcher と組み合わせて使う場合はデプロイアーカイブを tuggle に PUT してから、manifest で src: http://localhost:8900/foo.tar.gz などと指定する想定です。

実装上の工夫

最初にファイルを持っているのは 1 node なので、デプロイ時のように一斉に各 node で取得しようとすると、結局最初の1台に負荷が集中してしまいます。 そこで tuggle は Consul のセマフォの仕組みを利用して、1 nodeに対する取得の並列数を 3 に制限しています。

セマフォが取得できなかった node は、しばらく待ったあとに Consul Service の検索からリトライします。その時点で取得が終わった node は配布 node となっているため、増えた配布 node の中からランダムに選択することで負荷を分散します。

また、Consul 0.6 以降では Consul Agent 間でネットワーク的な距離を測定しています。Serviceを検索するときに near=_agent というクエリをすることで、自分と近い node を優先的に選択することができます。

これによって、AWSの AZ (Availiability Zone)間のようにネットワーク間に距離がある場合でも、自分と近いところから取得することで効率を上げることができます。

また、取得によって帯域を使い尽くさないように -fetch-rate というオプションで、取得に使用する帯域を制限することができます。これは拙作の shapeio というライブラリで実現しています。

https://github.com/fujiwara/shapeio

実際どんな感じ?

ファイルがどのように node 間で伝達していったのをかを Consul KV に保管しておいて、graphviz の dot 形式で出力する機能があります。

$ curl "http://localhost:8900/test.gz?graph" > graph.dot
$ curl "http://localhost:8900/test.gz?graph&format=svg" > graph.svg

node に graphviz (dot コマンド) がインストールされていれば、format=(svg|png) などと指定することで変換後の画像を取得することもできます。

実際に 135MB のファイルを AWS 上で c4.large インスタンス 50台、multi-az 構成で配布 (sync引数付き) してみたグラフは以下のようになりました。

f:id:sfujiwara:20170213180457p:plain

全 node にファイルが配布完了するまでに約18秒でした。c4.large インスタンスではおおよそ 70MB/sec 程度のネットワーク速度しか出ないため、1台に対して全台が取得すると100秒近くかかるはずのところが分散配布によって高速化しているのが分かります。

(手動で) AZ ごとに色を付けてみると以下のように、いいかんじに AZ 内を優先してファイルが流れていってるようです。最初に同 AZ のが集中しているのは、ネットワーク的に近いため Consul Event の伝達が早いものから先にセマフォを取得したためかと思われます。この辺は工夫の余地がありそうです。

f:id:sfujiwara:20170213180333p:plain

制限

  • Consul Service のタグでファイルを管理している都合上、大量のファイルは扱わない方がよさそうです
    • 試しに1000ファイルを登録してみても特に問題は出ませんでしたが、小さいファイルではConsul APIを叩くオーバーヘッドがあるため、普通にHTTPで配るよりもだいぶ遅くなります
  • ファイルにディレクトリ階層は付けられません
    • 面倒だっただけなのでそのうち直すかも

ということで、まだ動き始めたばかりで本番投入もしていませんが、興味のあるかたはお試しいただけると幸いです。

リポジトリの docker ディレクトリ以下で make up とすると、docker-compose で 8 node のクラスタが起動するので、試すのはこちらがお手軽です。