DragonflyのCDN機能を使って効率的なイメージダウンロードを実現する
Dragonflyという、Alibabaが中心となって開発されているイメージ・ファイルのP2Pベース分散配布ツールがあります。今回はDragonflyのCDN機能について調査・検証しました。なお、検証に使ったDragonflyのバージョンはv1.0.0です。
Dragonflyとは何か
Dragonflyが何か、一言で述べるとP2Pベースのイメージやファイルの配布ツールと言えます。大規模な環境において、ファイル転送の効率をあげ、ネットワーク帯域の利用効率を最大化することを目的としています。What Is Dragonfly? によるとアリババでは月に20億回Dragonflyが呼び出され、3.4PBのデータ転送が行われているとのこと。Dragonfly自体は汎用的に作られているが、わかりやすい利用事例としてはコンテナイメージの配布が挙げられます。例えば、Kubernetesのノード台数が増えれば増えるほど、それぞれのノードでイメージのpullが行われ、帯域や効率へ影響が出ます。イメージのサイズが大きくなると、イメージpullのパフォーマンスはさらに悪化する可能性もあります。Dragonflyを使うと単純なDocker利用より57倍のスループットを実現し、レジストリの帯域を最大99.5%節約できると書かれていました。
なぜ必要か
Dragonflyがどのような機能を持つのか、What Is Dragonfly? から紹介します。アリババの内部ではさらに多くの機能があるそうです。
- P2Pベースのファイル配布
- ファイル転送にP2Pの技術を使うことで、各ピアが持つ帯域リソースを最大限活用する。また、IDC間の帯域を節約できる。
- あらゆる種類のコンテナ技術に対して、非侵襲的にサポートする
- Dragonflyは様々なイメージ配布のためのコンテナ技術をシームレスにサポートする
- ホストレベルの速度制限
- wgetやcurlのようなダウンロードが持つタスクレベルの速度制限に加えて、Dragonflyはホスト全体の速度制限を提供する
- パッシブCDN
- CDNの機構を備えており、リモートからのダウンロードの繰り返しを回避できる
- 強力な整合性
- Dragonflyはユーザーがmd5のようなチェックサムを提供しなくても全てのダウンロードファイルに整合性があることを確認できる
- ディスクの保護と効率的なIO
- ディスク容量の事前チェックや同期の遅延、最適な順序でのファイルブロック書き込み、net-read/disk-writeの分離など
- 高いパフォーマンス
- SuperNode(Dragonflyのコンポーネントの1つ)は完全に閉制御されていて、データベースや分散キャッシュを必要とせずに高いパフォーマンスでリクエストを処理する
- 例外の自動隔離
- Dragonflyは自動的に例外ノード(peerもしくはSuperNode)を隔離し、ダウンロードの安定性を改善する
- ファイルソースへのプレッシャーがない
- ソースからダウンロードするSuperNodeはごくわずかで良い
- 標準的なHTTPヘッダのサポート
- HTTPヘッダを介した認証情報の送信をサポート
- Registry Authの効果的な同時実行制御
- RegistryAuthサービスへのプレッシャーを軽減する
- シンプルで簡単に使える
- 必要な設定はほとんどない
なるほど、イメージは掴めてきました。では実際に触って行ってみましょう。今回はCDN機能について主に試します。
一般的なファイルダウンロードの場合
適当なKubernetesクラスターを用意して、Dragonflyをインストールします。helm chartがあるのでこれを使うと比較的楽です。
NAME READY STATUS RESTARTS AGE
d7y-dragonfly-dfclient-2q7jw 1/1 Running 0 3d
d7y-dragonfly-dfclient-4w77g 1/1 Running 0 3d
d7y-dragonfly-dfclient-6hhw4 1/1 Running 0 3d
d7y-dragonfly-dfclient-7xtw6 1/1 Running 0 3d
d7y-dragonfly-dfclient-vpzgb 1/1 Running 0 3d
d7y-dragonfly-dfclient-xxnj5 1/1 Running 0 3d
d7y-dragonfly-supernode-6b96dfc674-wwrls 1/1 Running 0 3d
適当なPodに入って、dfgetコマンドでファイルダウンロードしてCDNが動作しているのかを確かめてみます。
$ k exec -it d7y-dragonfly-dfclient-vpzgb bash
bash-4.4#
bash-4.4#
bash-4.4# time /opt/dragonfly/df-client/dfget --url https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz --output ./kubernetes-server-linux-amd64.tar.gz --node d7y-drago
nfly-supernode --verbose
--2020-04-20 03:31:06-- https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz
dfget version:1.0.0
workspace:/root/.small-dragonfly
sign:29-1587353466.555
client:10.100.0.156 connected to node:d7y-dragonfly-supernode:8002
start download by dragonfly...
download SUCCESS cost:42.585s length:363231097 reason:0
real 0m42.754s
user 0m3.072s
sys 0m6.552s
bash-4.4#
bash-4.4#
bash-4.4# rm ./kubernetes-server-linux-amd64.tar.gz
bash-4.4# time /opt/dragonfly/df-client/dfget --url https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz --output ./kubernetes-server-linux-amd64.tar.gz --node d7y-dragonfly-supernode --verbose
--2020-04-20 03:31:56-- https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz
dfget version:1.0.0
workspace:/root/.small-dragonfly
sign:45-1587353516.929
client:10.100.0.156 connected to node:d7y-dragonfly-supernode:8002
start download by dragonfly...
download SUCCESS cost:19.322s length:363231097 reason:0
real 0m19.405s
user 0m1.976s
sys 0m1.000s
1回目と2回目でダウンロードにかかった時間がかなり違うことが確認できました。dfgetコマンドで手軽に扱えるのは便利です。
アーキテクチャとしてはこのようになっています。supernodeとdfgetの2コンポーネントが使われて、dfgetを使うとsupernode経由でファイルをダウンロードします。また、dfgetはP2Pクライアント(ピア)として動き、ピア間でのブロック転送も行われます。supernodeはCDNとして働き、各ピア間のブロック転送のスケジューリングなどを行います。
コンテナイメージpullの場合
次に、コンテナイメージのpullについて試します。なお、コンテナランタイムとしてはdockerではなくcontainerdを使っています。
dfdaemonの設定はこのような感じ。dfdaemonはデフォルトでindex.docker.ioをレジストリミラーとして扱うようになっています。また、proxiesでURLにblobs/sha256.*
が含まれている場合はdfgetを使ってリクエストする、という設定にしました。
dfget_flags:
- --verbose
- -f
- Expires&OSSAccessKeyId&Signature
proxies:
- regx: blobs/sha256.*
use_https: true
node:
- 10.233.60.241:8002
containerdにregistry mirrorの設定をします。docker.ioのミラーエンドポイントとしてdfdaemonを指定します。
[plugins.cri.registry]
[plugins.cri.registry.mirrors]
[plugins.cri.registry.mirrors."docker.io"]
endpoint = ["http://127.0.0.1:65001"]
これで準備はOK。イメージをpullしてみます。初回と2回目で約7.3秒の差があ離ました。centosイメージなのでそもそもそんなに大きくないのですが、大きなイメージを扱うときには役に立つであろうことがわかります。
$ time sudo crictl pull centos
Image is up to date for sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee
real 0m23.898s
user 0m0.028s
sys 0m0.020s
$ sudo crictl rmi centos
Deleted: docker.io/library/centos:latest
$ time sudo crictl pull centos
Image is up to date for sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee
real 0m16.698s
user 0m0.032s
sys 0m0.024s
アーキテクチャはこのようになっていて、supernodeとdfgetに加えて、dfdaemonが使われます。dfdaemonはdockerのようなコンテナランタイムからのpushやpullリクエストをインターセプトし、必要に応じてdfgetで処理します。
プライベートレジストリからのイメージをpullする
前節ではDockerHubからのイメージpullをDragonfly経由で行いました。しかし、場合によってはプライベートレジストリからのイメージpullをDragonfly経由で行いたいのではないでしょうか(僕がやりたい)。これを行う方法は2つあります。一つ目は、dfdaemonをHTTP Proxyとして扱う方法。もう一つは、dfdaemonをregistry mirrorとして扱う方法です。
HTTP Proxyとして扱う
Use Dfdaemon as HTTP Proxy for Docker Daemon に方法が記載されています。Dockerはregistry-mirrorsフラグを使ったプライベートレジストリの設定に対応していないため、この方法を使う必要があります。しかしdfdaemonをdockerで起動していると、docker再起動時にdfdaemonが止まり、dockerがdfdaemonに繋がらず起動しませんでした。systemdなどを使って起動すれば問題ないかと思います。
registry mirrorとして扱う
dfdaemonをHTTP Proxyとして使うのは若干扱いにくいと感じたため、別の策として検証しました。containerdはプライベートレジストリをregistry mirrorとして設定できるため、HTTP Proxyの設定がなくても動作するはずです。
dfdaemonの設定はこのようにします。dfdaemon自身がレジストリミラーとして参照するURLを変更します。
registry_mirror:
remote: https://your.private.registry
dfget_flags:
- --verbose
- -f
- Expires&OSSAccessKeyId&Signature
proxies:
- regx: blobs/sha256.*
use_https: true
verbose: true
node:
- 10.233.60.241:8002
containerdのレジストリ設定はこのようにします。containerd、レジストリの認証情報もtomlにまとめて設定できて便利です。
[plugins.cri.registry]
[plugins.cri.registry.auths]
[plugins.cri.registry.auths."http://127.0.0.1:65001"]
username = "username"
password = "password"
auth = "XXXXXXXXXXXXXXXXXXXXXXXX"
[plugins.cri.registry.mirrors]
[plugins.cri.registry.mirrors."your.private.registry"]
endpoint = ["http://127.0.0.1:65001"]
これで、イメージのダウンロード時にdfdaemonはdfgetを用いてsupernodeへリクエストします。1回目と2回目でpullの時間が変わるかどうかみてみましょう。
$ time sudo crictl pull your.private.registry/image:0.0.2
Image is up to date for sha256:4358591c9919667bf6a8baca78c2e85f1ffe82700ca81e2550a63c532e49c142
real 1m30.891s
user 0m0.040s
sys 0m0.036s
$
$ sudo crictl rmi your.private.registry/image:0.0.2
Deleted: your.private.registry/image:0.0.2
$ time sudo crictl pull your.private.registry/image:0.0.2
Image is up to date for sha256:4358591c9919667bf6a8baca78c2e85f1ffe82700ca81e2550a63c532e49c142
real 1m31.800s
user 0m0.040s
sys 0m0.024s
あれ、ほとんど変わりませんね… Dragonflyは帯域制御などの機能もあるので、その辺りの設定を変えてみました。今回変えたのは以下。
- dfdaemon
- ratelimit (dfgetのlocallimitにセットされる。ダウンロード時のネットワーク帯域の上限を指定)
- supernode
- systemReservedBandwidth
設定を変更したところ、初回のpullは1m58s、2回目のpullは59sと変化が感じられました(初回は遅くなってるじゃんという)。しかしcontainerd単体でpullすると40sくらいで終わるので、もう少し調べたいところです。構成要素が複数あり、その設定がどこに影響するのかを把握するのが難しいなと感じました。特にdfdaemonは設定と内部の構造体が分かれているので、追いかけるのが大変です。
まとめ
DragonflyのCDN機能について、一般的なファイルダウンロードとコンテナイメージPullについて調査と検証を行いました。大規模なクラスターでイメージのPullが帯域を圧迫していたり、巨大なイメージのPullに時間がかかって困っているような場合は試してみるといいかもしれません。目玉であろうP2P機能については今回は試さなかったので、今後試していきたいですね。また、複数のレジストリについてDragonflyでミラーすることができるのかについても検証しようと考えています(やりたい)。
オマケ:元々はJavaで書かれていたらしい
DragonflyはGoで書かれています。しかし、ドキュメントを読んでいると、Java関係の記述が割と出てくる。なぜかなと思ったら、元々はJavaで書かれていたのがGoで書き直されたようです。0.4.0で完全にGoになったとのことです。普段よく読み書きしているのがGoなので、同じ感覚で扱えるのはありがたいです。