プログラミング
パックファイルを再作成すればオブジェクトストレージ上でGitを実行できる
You can run Git on object storage if you re-make packfiles (tigrisdata.com)
要約
オブジェクトストレージ上でGitを効率的に実行するためには、従来のパックファイル形式を再設計する必要があることが示されています。本記事では、Gitのオブジェクト構造とパックファイルの課題を解説し、オブジェクトストレージネイティブな新しいパックファイル形式の提案とその利点について説明しています。
全文翻訳
最近、多くの企業が何らかのGit製品をリリースしようとしているように見えます。なぜでしょうか。
いずれにせよ、私はオブジェクトストレージをバックエンドとしたGitサーバーをオープンソースプロジェクトとして構築しています。
最初は単純に思えました。
Gitはファイルシステムのように見えるので、オブジェクトストレージの上にファイルシステムのような翻訳レイヤーを使用して、Gitにオブジェクトストレージを扱わせようと考えました。
このモデルは…まあ、そこそこ機能しましたが、実用的なサイズのレポジトリではうまく機能しなかったので、別のアプローチが必要でした。
Gitはすべてをオブジェクトに格納します。では、それらをTigrisのオブジェクトとして格納するのはどうでしょうか?
Gitのパックファイルと、それが私の(認めざるを得ない、かなりひどい)ファイルシステムシムとどのように交差していたかが、遅延の主な原因であることが判明しました。
最終的には、コラム型ストアを備えた独自のパックファイル形式を考案する必要がありました。これはオブジェクトストレージネイティブなものです。
これは、私のパフォーマンス分析、メトリクス注釈、そしてk8sクラスターが棒を振るよりも多くのテキサス風分散システム作業の成果です。
このアプローチは、本番環境サイズのレポジトリで驚くほどうまく機能したので、当面はこの新しいパックファイル形式を採用することにしました。
クライアント側の変更なしに、Gitオブジェクトをオブジェクトストレージオブジェクトのように感じさせるための、最も邪魔にならない変更のようです。
Gitとは何か?
みじめなオブジェクトの山です!
コミットを行うと、Gitは変更を.git(多くのバックティックを書かなくて済むように「dotgit」と呼びます)フォルダ内のオブジェクトとして格納します。
Gitを2つのものとして想像してください。オブジェクトの海と、個々のオブジェクトへの名前付き参照です。
各オブジェクトは、コンテンツアドレス指定され、圧縮されたファイルです。
小さなGitレポジトリの例を以下に示します。
$ mkdir ~/tmp/gitexample
$ git init && git branch -m main
$ echo "Hello, blog!" >> hello.txt
$ git add .
$ git commit -sm "chore: initial commit"
これにより、ディスク上に次のようなオブジェクトが生成されます。
FIG 01
オブジェクトの海、そしてそこへのいくつかの名前
.git/objects/
refs/heads/main
├── 1c/7a26a901..ec7966 ─────▶ commit 1c7a26a
│ ├── 8e/67afbb2e..857bd3 ─────▶ tree 8e67afb
│ hello.txt
└── 9c/c9867337..09fe26 ─────▶ blob 9cc9867 "Hello, blog!"
ファイル名はファイル内のバイトのsha1なので、同じコンテンツは常に、どこでも、まったく同じオブジェクトです。
オブジェクトの内容を読み取るには、圧縮されているため、かなり邪悪に見えるPythonのワンライナーを使用して、おいしい中身をすくい取る必要があります。
$ file .git/objects/**/* | grep -v directory
.git/objects/1c/7a26a901724b4ce766655ac387413fb9ec7966: zlib compressed data
.git/objects/8e/67afbb2ee6bdcbb79061dfdfb93febce857bd3: zlib compressed data
.git/objects/9c/c9867337c2ebae85ba2350f901e0bcc209fe26: zlib compressed data
$ python3 -c "import sys, zlib; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))" < .git/objects/9c/c9867337c2ebae85ba2350f901e0bcc209fe26
blob 13Hello, blog!
ご覧のとおり、オブジェクトは単なる生のファイルです。
LinuxカーネルのGitレポジトリを見て、任意のコミットを抽出してみましょう。
すべてが単なる億万個の生のオブジェクトファイルであるべきですよね?
ディスク上でIDを探すだけで、単一のコミットを簡単に見つけられるはずですよね?
現実はそれほど単純であればいいのですが。
$ cd ~/Code/linux.git/
$ tree objects
objects
├── info
└── pack
├── pack-45986f41063f286029742ec12e2c2882b88c5786.idx
├── pack-45986f41063f286029742ec12e2c2882b88c5786.pack
└── pack-45986f41063f286029742ec12e2c2882b88c5786.rev3
2 directories, 3 files
ええ、ご想像のとおり、すべてを個別のファイルに入れるだけでは、Linuxカーネルのような規模には対応できません。
おそらく、フラクタルなnode_modulesフォルダの時代に誰もが陥ったように、inodeの制限に遭遇するでしょう。
余談
Nodeを十分に長く使っていて、それを覚えている方は、ぜひ大腸内視鏡検査を受けてください。
大腸がんは、多くの人が長期間見過ごしている現実的な懸念であり、あまりにも多くの命を奪っています。
Gitはこの問題を、オブジェクトをパックファイル、つまりそれらをすべて1つのファイルに圧縮したバンドルに入れることで回避しています。
私のobjgitのチェックアウトにおけるパックファイルの効率性の例を以下に示します。
$ git count-objects -v
count: 756
size: 3500
in-pack: 448
packs: 1
size-pack: 321
prune-packable: 0
garbage: 0
size-garbage: 0
もしGitに生のオブジェクトをパックファイルに入れることを「強制」する必要がある場合は、git gcを実行できます。
$ git gc
[omitted for brevity]
$ git count-objects -v
count: 0
size: 0
in-pack: 1203
packs: 2
size-pack: 848
prune-packable: 0
garbage: 0
size-garbage: 0
余談
私がオブジェクトストレージの上にGitを実装していることの美しい点の1つは、オブジェクトデータがオブジェクトの海であり、FoundationDBに格納されたその海のポイントへの名前付き参照であるプラットフォームを使用していることです。
これは、その美しさをどのように説明すればよいか分からない、神聖な再帰の一種です。
上にあるものが下にもあるように。
Yo dawg, オブジェクトが好きだって聞いたぜ
Linuxカーネルのコピーのオブジェクト数を以下に示します。
xe@zohar:~/Code/linux.git$ git count-objects -v
count: 0
size: 0
in-pack: 11827138
packs: 1
size-pack: 3876775
prune-packable: 0
garbage: 0
size-garbage: 0
これは1100万個のオブジェクトであり、GetObject呼び出しあたり10ミリ秒という非常に寛大な仮定では、すべてを取得するには1時間以上かかります。
真実は、ディスク上の個別のファイルとして1100万個のオブジェクトがあるわけではなく、それらは1つの大きなハッピーな3.4Giパックファイルにバンドルされているということです。
通常のGitリポジトリは、分割するのが理にかなっているにつれて、それらを蓄積する傾向があります。
私のTigrisブログのローカルコピーには、4つのパックファイルと約290個の生のオブジェクトがあります。
それで、あなたは「ああ、Gitにはパックファイルがあるのに、なぜこの記事の残りは存在するのだ?」と考えているかもしれません。
さて、分散システムでは多くのことがそうであるように、それは複雑です。
パックファイルは、ローカルストレージやmmapを念頭に置いて設計されているため、扱いにくいです。
Gitはパックファイルをディスクに絶えず書き込み、それらを再読み込みします。
その場合、ファイルシステムの読み取りは最大でも10ナノ秒(ファイルシステムキャッシュが非常に役立ちます)ですが、ネットワークラウンドトリップを行うと最低でも10ミリ秒かかります。
現実がどのように機能するかにより、少なくとも100万倍遅くなります。
注
/usr/bin/gitが行うことの中で、オブジェクトストレージへの統合を困難にしていることの1つは、ファイルに書き込んでからすぐにそのファイルから読み取ってハッシュを計算するというUnix的なイディオムです。
オブジェクトストレージでは、PutObject呼び出しが完了していないものをGetObjectすることはできません。
以前はディスクに書き込んでからその方法で行うことで回避していましたが、実際にはその経験はあまり良くありませんでした。
パックファイルいじり
各パックファイルには、その内容を記述するインデックスがあります。
前の簡単なGitレポジトリから作成されたパックファイルのインデックスのビューを以下に示します。
uppost:
$ git gc # オブジェクトをパックファイルに強制的に格納
$ git verify-pack -v .git/objects/pack/pack-3971f5085c23c38be00e517ed0c64ca7df19b746.idx
1c7a26a901724b4ce766655ac387413fb9ec7966 commit 526 366
129cc9867337c2ebae85ba2350f901e0bcc209fe26 blob 13 22
3788e67afbb2ee6bdcbb79061dfdfb93febce857bd3 tree 37 48
400 non delta: 3 objects.
.git/objects/pack/pack-3971f5085c23c38be00e517ed0c64ca7df19b746.pack: ok
コミットは、ファイル「hello.txt」がブロブを指しているツリーを指し、ブロブ、それがすべてです。
Gitはこれらのバイナリインデックスを使用して、どこを探すべきか、パックファイル内のどこまでシークする必要があるかを知り、どこに行けばそれらを取得できるかを知ります。
FIG 02
1100万個のオブジェクト、1つのパックファイル、1つのインデックス
$ git count-objects -v .git/objects/pack/
count: 0
└── pack-45986f41..c5786.pack 3.7 GiB
in-pack: 11827138
packs: 1
eleven million loose files would be
size-pack: 3876775
eleven million inodes.
so: one file.
.idx .pack
┌───────────────────────┐ ┌──────────────────────────────────────┐
│ 0002ff4c.. 0x0000c │ │███ ██ ████ █ ███████ ██ █ ████ ██████│
│ 0031ab90.. 0x0a13f │ └──────────────────────────────────────┘
│ 1c7a26a9.. 0x1f3a4 │
│ ... │
└───────────────────────┘
each row's colour is the run of bytes its offset points at, so a read is a seek to an offset inside one very big file
ALL/03
3つの行、3つのオフセット、3つのバイトの実行
▶ play
素晴らしいのは、これがすべてファイルシステム内にある場合に非常にうまく機能することです。
Gitはパックファイルをmmapするため、