先日教えてもらったのですが、Amazon EFSというめちゃくちゃ便利に見えるサービスがあります。
- 複数のEC2インスタンスで共有できるストレージ
- 事前に容量を決める必要がない(使ったら使った分だけ増えていく)
要するにNFSで、EBSと違って複数のEC2インスタンスから共有できるのが特に便利に見えます。具体的に言うと、SideCIでgit clone
してきたリポジトリを保存して共有するのに最適に見えます。(見えました。)
SideCIでは、Gitリポジトリの操作を抽象化したサーバの開発を現在進めていて、
git clone
して欲しいリビジョンをgit archive
git diff
して変更された行を特定
などの操作をWeb API経由で実行できるようになりたいと考えています。(リファクタリングの話なのでサービスの強化には、すぐには繋がらないのですが……)このとき問題になるのが、GitHubからcloneしてきたリポジトリをどこに保存するかということです。
Webサーバそれぞれのローカルストレージに保存しても良いのですが、
- 複数台のWebサーバでリポジトリを共有できるとcloneが減ってより高速に実行できる
- できるだけローカルのリポジトリには消えて欲しくないのでデプロイが面倒になる
といった問題があります。EFSにリポジトリを保存するようにすれば、これらの問題が全部解決できるのでは!?NFSということでネットワーク越しにアクセスするのですから、当然PCI Expressなどで接続されたストレージにアクセスするよりは遅くなるはずですが、そもそもEBSにしても物理的にはネットワーク越しにアクセスしているはずですから、そんなに酷いことにはならないでしょう。
などと考えながら試してみたところ、6倍以上もEFSが遅かったので、問題は一切解決しなかったというご報告です。
- Amazon EFS - https://aws.amazon.com/jp/efs/
実行結果
us-eastリージョンのt1.microインスタンスで試しました。AMIはamzn-ami-2016.03.i-amazon-ecs-optimized-4ce33fd9-63ff-4f35-8d3a-939b641f1931-ami-3d55272a.3 (ami-03562b14)
というので、Dockerの中でData Volumeとして読み書きしています。
まずは普通のEBS上のディレクトリで。
$ time git clone https://github.com/rails/rails.git Cloning into 'rails'... remote: Counting objects: 556404, done. remote: Compressing objects: 100% (74/74), done. remote: Total 556404 (delta 30), reused 9 (delta 9), pack-reused 556321 Receiving objects: 100% (556404/556404), 134.66 MiB | 14.66 MiB/s, done. Resolving deltas: 100% (411204/411204), done. real 0m23.724s user 0m18.688s sys 0m2.396s
次にEFSでマウントしたディレクトリで。
$ time git clone https://github.com/rails/rails.git Cloning into 'rails'... remote: Counting objects: 556404, done. remote: Compressing objects: 100% (74/74), done. remote: Total 556404 (delta 30), reused 9 (delta 9), pack-reused 556321 Receiving objects: 100% (556404/556404), 134.66 MiB | 11.08 MiB/s, done. Resolving deltas: 100% (411204/411204), done. Checking out files: 100% (3180/3180), done. real 2m34.889s user 0m18.956s sys 0m3.852s
EBSでは24秒でgit clone
が終わりましたが、EFSでは2分35秒かかりました。time
の出力を見ると、real
は大きく増えていますがuser
とsys
はほとんど変更がないので、IO待ちで遅くなっていることがわかります。
しかしEFSはこんなに遅くて大丈夫なのでしょうか。もう少しディスクの読み書きの速度に注目して、テストしてみましょう。簡単にdd
でテストしてみます。
EBSの場合。
$ dd if=/dev/zero of=/ebs/test ibs=1M obs=1M count=1024 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB) copied, 17.5108 s, 61.3 MB/s
EFSの場合。
$ dd if=/dev/zero of=/efs/test ibs=1M obs=1M count=1024 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB) copied, 18.4784 s, 58.1 MB/s
あれっ、速いぞ……EBSより少し遅いけど、でも速い。なんでGitだけこんなに遅いんでしょう?
なんでGitだけこんなに遅いのか
Gitのリポジトリ操作を実装するlibgit2というライブラリがありますが、ここにgit_odb_backend
という型があったりします。バックエンドというのはGitリポジトリのオブジェクトを保管する場所のことで、git_odb_backend
のメンバに適当な関数ポインタを設定することで、Gitリポジトリをファイルシステムだけではなくていろんなところに保存できるように作られています。MySQLとかMemcachedとか。
- libgit2 - https://libgit2.github.com
- libgit2-backends - https://github.com/libgit2/libgit2-backends
git_odb_backend
の定義を見てみましょう。
/** * An instance for a custom backend */ struct git_odb_backend { unsigned int version; git_odb *odb; /* read and read_prefix each return to libgit2 a buffer which * will be freed later. The buffer should be allocated using * the function git_odb_backend_malloc to ensure that it can * be safely freed later. */ int (* read)( void **, size_t *, git_otype *, git_odb_backend *, const git_oid *); /** 中略 **/ /** * Write an object into the backend. The id of the object has * already been calculated and is passed in. */ int (* write)( git_odb_backend *, const git_oid *, const void *, size_t, git_otype); /** 以下略 **/ };
色々ありますが大胆に削って、read
とwrite
だけで。これらの関数の型を見るとオブジェクトのIDを表しているgit_oid
の数を渡す引数がないので、「えっ、こいつら一個ずつ読み書きしてるんじゃ……」ということに気づきます。それは遅いだろ……
Webアプリケーションをバリバリ開発されている皆さんがよくご存知のN+1クエリという問題があります。N個のレコードを取ってくるときにN回SELECT
すると遅いけど、一回のSELECT
でN個取ってくると速い、というやつです。Gitのオブジェクトにも同じことが言えます。つまり、一個ずつディスクから読むと、まとめて読むより遅い。普通に接続されたディスクならそれでも十分に速く動作しますが、NFSでネットワーク越しにいちいちファイルを読み書きするとあからさまに遅かった、ということなのでしょう。
というと多分少し語弊があって、Gitでは高速に動作するよう工夫があるようです。実装があるファイルを眺めると、長々とコメントが書いてあります。(流し読みして、頑張ってるんだなーと思いました。)
- https://github.com/libgit2/libgit2/blob/89c332e41b12a72d89de40d63bc568c56a2c336a/src/odb_pack.c ここら辺の話です
(多分カスタムバックエンドのサンプル的な扱いになっている)MySQLバックエンドなどはわかりやすい感じで、毎回一個ずつ
SELECT
してくるいかにも遅そうな実装になっています。
残った問題
GitHub.comとかはどうなっているんだろう?
ところで、世界で一番Gitリポジトリを持っている組織の一つであろう、GitHubさんはどうやって実装しているんでしょう。「GFSを使っていた」などという声もありますが、現在は頑張って実装しているようです。リポジトリをローカルのストレージに保存するサーバがたくさんあるようです。
- What filesystem does GitHub use for its repositories and why? - Quora 2010年末に「昔はGFSだった」と言っている。
- Introducing DGit (DGit is now called Spokes) - GitHub Engineering 2016年4月の記事なので、多分これが最新。
Introducing DGitを読むと、
Git is optimized to be fast when accessing fast disks, so the DGit file servers store repositories on fast, local SSDs. (Gitはディスクに高速にアクセスできるときに速く動作するように最適化されているので、DGitもファイルを高速なローカルSSDに保存している。)
などと書いてありますね。
EBSなんでこんなに速いの?
EFSが遅い理由は(間違っているかもしれませんが)納得しましたが、EBSがこんなに速い理由がむしろ気になります。EBSは一個のEC2インスタンスからしかアクセスされないので、積極的にキャッシュなどできるのかなーなどと考えています。
結局SideCIはどうするの?
ひとまずは、速いインスタンスを少数用意して、それぞれEBSにリポジトリを保管することにしました。GitHub.comと違ってSideCIにあるリポジトリはただのキャッシュなので、気軽に消すことができます。消してしまうとgit clone
し直さないといけないので、少し遅くなりますが、EC2とGitHub.comの通信が意外と早かったので許容範囲内ではないかという結論になりました。
それで耐えられないくらい遅くなるようなら、仕方がない。我々のDGitを作ります。