SideCI TechBlog

SideCIを作っているアクトキャットのエンジニアによる技術ブログです。


GitリポジトリをAmazon EFSにcloneしたら遅かった話

この記事で説明したGitの話には、私の誤解が含まれています。コメントまで見てください。(EFSにリポジトリを置くと遅いのは本当。)

先日教えてもらったのですが、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が遅かったので、問題は一切解決しなかったというご報告です。

実行結果

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は大きく増えていますがusersysはほとんど変更がないので、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とか。

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);

    /** 以下略 **/
};

色々ありますが大胆に削って、readwriteだけで。これらの関数の型を見るとオブジェクトのIDを表しているgit_oidの数を渡す引数がないので、「えっ、こいつら一個ずつ読み書きしてるんじゃ……」ということに気づきます。それは遅いだろ……

Webアプリケーションをバリバリ開発されている皆さんがよくご存知のN+1クエリという問題があります。N個のレコードを取ってくるときにN回SELECTすると遅いけど、一回のSELECTでN個取ってくると速い、というやつです。Gitのオブジェクトにも同じことが言えます。つまり、一個ずつディスクから読むと、まとめて読むより遅い。普通に接続されたディスクならそれでも十分に速く動作しますが、NFSでネットワーク越しにいちいちファイルを読み書きするとあからさまに遅かった、ということなのでしょう。

というと多分少し語弊があって、Gitでは高速に動作するよう工夫があるようです。実装があるファイルを眺めると、長々とコメントが書いてあります。(流し読みして、頑張ってるんだなーと思いました。)

(多分カスタムバックエンドのサンプル的な扱いになっている)MySQLバックエンドなどはわかりやすい感じで、毎回一個ずつSELECTしてくるいかにも遅そうな実装になっています。

残った問題

GitHub.comとかはどうなっているんだろう?

ところで、世界で一番Gitリポジトリを持っている組織の一つであろう、GitHubさんはどうやって実装しているんでしょう。「GFSを使っていた」などという声もありますが、現在は頑張って実装しているようです。リポジトリをローカルのストレージに保存するサーバがたくさんあるようです。

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を作ります。