500 GB を R2 へ:巨大なアップロードが止まったとき、そしてまた始めたときに起きること
- 巨大ファイル:S3 マルチパートでストリーミングアップロード。メモリに丸ごと載せることはありません。500 GB なら 52 MiB × 9,847 パート、同時 5 パート。Cloudflare R2 で最後まで実測済み。
- 中断したら:何も失われません。送信済みパートはサーバーに残り、ディスク上のジャーナルが一つずつ記録します。再開は——アプリを終了した後でも——同じファイルであることを確認し、それらのパートを読み直さずに引き継ぎ、欠けている分だけを送ります。
- 孤児パート:未完了アップロードはバケット内に表示され、削除できます。削除するまで課金されます。RocketBucket が自動で中止することはありません。別のクライアントのものかもしれないからです。
- メモリ:再開を含む 500 GB の全行程でピーク約 900 MB〜1 GB。全パートがすでにサーバーにある再開はピーク 49.1 MB——何も読まないからです。
RocketBucket で 500 GB のファイルを Cloudflare R2 にアップロードし、途中で止め、もう一度始めました。完了しました——536,870,912,000 バイト、9,847 パート。最初のパートから組み立てられたオブジェクトまで、実行中ずっとメモリを見ていましたが、およそ 900 MB から 1 GB を超えることはありませんでした。その下で何が起きているのか、そしてこの文が真になる前に何を直さなければならなかったのか、という話です。
S3 にとって 500 GB とはどう見えるか
S3 と、その API を話すすべてのサービスは、大きなファイルをひとつのリクエストでは受け取りません。受け取るのはパートです。最大 10,000 個、各パートは 5 MiB から 5 GiB、そして最後にそれらをひとつのオブジェクトに貼り合わせる呼び出しが一回。RocketBucket のデフォルトのパートは 8 MiB ですが、500 GB を 8 MiB で割ると 64,000 パートになるので、アップロード開始前に逆向きに計算します。500 GB ÷ 10,000、MiB 単位に切り上げて 52 MiB。この決定は永久です。最初のパートがサーバーに載ったあとでパートサイズは変えられず、再開はファイルをまったく同じ位置で切らなければ何も揃いません。
同時に飛んでいるのは 5 パート。ファイルはストリームで読まれ、丸ごと読み込まれることはないので、転送に必要なメモリはおよそ 5 パート+バッファひとつ——約 300 MB——に、ネットワークライブラリ自身のコピーが乗ったものです。私たちが測った 900 MB から 1 GB はその範囲です。
アップロード中に書かれるものが二つあり、どちらも止まる瞬間のためだけに存在します。オブジェクトは作成時にランダムなコミットトークンをメタデータに刻まれます。そしてディスク上の小さなジャーナルにパートごとに一行——番号、サイズ、そのバイト列の私たち自身の SHA-256——が、サーバーがそのパートを受け入れたあとにだけ書かれます。サーバーの ETag を信じずに自分でハッシュするのは、SSE-KMS の下では ETag がパートの MD5 ではなく、あとで比較する相手がなくなるからです。
止まったときに起きること
アップロードが止まる理由は二つ。接続が切れるか、あなたが停止を押すか。二つ目には告白が付いています。今週まで、大きな一つのファイルの途中で停止を押しても何も起きませんでした。キャンセルフラグはファイルとファイルの間でしか確認されず、9,847 パートのファイルはひとつのファイルです。修正済みです——キャンセルはいまパートのアップロードの中まで届き、飛んでいるパートのうちに効きます——そして見つかった経緯はこの話の大半と同じで、実際に 10 GB の再開を走らせてボタンを押したことでした。
どちらの場合も同じルールが適用されます。すべてを残す。ジャーナルはディスクに書き出され、サーバー上のマルチパートアップロードは中止されません。届いた 6,000 パートはアップロード ID の下に留まり、待ちます。待っている間は課金されます——S3 互換のストアはどれも未完了アップロードのパートに課金し、通常のリストには見えません——だから RocketBucket はバケットの未完了アップロードを表示し、削除できるようにしています。しかし自分から中止することは決してありません。自分が作ったものではないかもしれず、別のクライアントのものかもしれないからです。
また始めたときに起きること
同じファイルを同じ場所にドラッグします。RocketBucket はその 1 バイトも読む前に、そのキーの未完了アップロードをサーバーに列挙させ、ジャーナルと照合します。ハッシュもディスク読みもなし——何かを尋ねる前にやれるほど安いのです。それから尋ねます。これを再開するか、やり直すか(選んだアップロードだけを中止)、キャンセルするか。
再開を選ぶと、最初の確認は「これはまだ同じファイルか」。同じサイズ、同じ更新日時、同じ inode——だからまったく同じサイズの別ファイルに置き換えられたものはここで弾かれ、ジャーナルは信用されません。次にサーバーのパート一覧がジャーナルと一致しなければなりません。記録したすべてのパートが、記録した ETag とサイズで存在すること。そのときはじめて、それらのパートは採用されます。
採用されたパートは二度と読まれません。RocketBucket はそれらを飛び越え、欠けているパートだけをバイト範囲で読み、それぞれをハッシュしてジャーナルに書き、アップロードします。すべてのパートがすでにサーバーにある再開は、ディスクから 0 バイトを読み、0 回のアップロードを送ります。10,000 パートでまさにそれを断言するテストがあります。
最初の版はそうではなく、そしてその最初の版が、500 GB が何を意味するかを私たちに教えました。それはスキップの前に各パートを検証するため、ファイルを 0 バイト目から読み、すでにアップロード済みの 9,800 パートも含めてすべてのパートをハッシュしていました。一度も止まらないループの中で。メモリは 14.5 GB まで登り、macOS がそれを殺しました。最初に疑ったのはジャーナルでした——パートごとにファイル全体を書き直し、育ち続ける文書を一万回書き直していた——ので 64 パートごとにまとめました。それは本物のバグで正しい修正でしたが、再開はやはり 14.5 GB で死にました。本当の原因はハッシュのループにありました。16 進エンコードに String(format:) を使っていて、1 バイトごとに Objective-C の文字列をひとつ確保し、残されたおよそ 127,000 個の一時オブジェクトを誰も回収しなかったのです。欠けているパートだけを読むようにしたことで、その問いは消えました。
どう終わるか
最後のパートが上がると、RocketBucket はパート 1 から 9,847 がすべて揃い、サイズの合計がファイルに一致することを確認して、Complete リクエストを送ります。小さなファイルならそこで終わりです。R2 上の 500 GB では、奇妙な 30 秒の始まりです。サーバーはリクエストを受け付け、それから 9,847 片をひとつのオブジェクトに組み立てるのに数十秒かけます。その間、オブジェクトはまだ存在せず、マルチパートアップロードはもう消えています。
これを知っているのは、最初の 500 GB の完了がアップロードの完了は不確かです。オブジェクトを手動で確認してくださいと報告したからです——そしてオブジェクトはそこにあり、完全で、私たちのトークンが付いていました。Complete が曖昧なときに走る照合は「オブジェクトはあるか、私のトークンを持ち、私のサイズか」と 4 秒間隔で 3 回尋ね、毎回「アップロードは消え、オブジェクトは不在」を見て、諦めていました。いまは 6 回——2、4、8、15、15、15 秒——尋ね、消えたアップロードを答えではなく問いとして扱います。成功の定義は動いていません。オブジェクトが応答し、私たちのトークンを持ち、期待どおりのサイズであること。トークンかサイズが違えば、やはり「確信がない」と言います。その代わりは、誰か他人のものかもしれないオブジェクトを名乗ることだからです。
レビューは出荷前の再開経路にさらに二つを見つけました。範囲読みが要求したすべてのバイトを返したか確認していなかったこと——サーバーが喜んで受け入れる切り詰められたオブジェクトを完了させ得た——と、速い経路が Complete を直接呼び、上の照合を飛ばしていたこと。どちらも修正済みです。
その一回
500 GB のアップロードを始める。止める。終了する。RocketBucket を開き、ファイルをもう一度ドラッグし、再開を選ぶ。サーバー上のパートを採用し、残りをアップロードし、完了し、組み立てを待ち、オブジェクトを確認する。536,870,912,000 バイト、9,847 パート、コミットトークンあり、残されたマルチパートアップロードなし。実行全体でのピークメモリは、およそ 900 MB から 1 GB。
これらすべての設計は 5 回レビューされ、正しかった。壊れたのはそのどこにもないものでした。アロケータ、育つジャーナル、間違ったループで確認されていた停止ボタン、そして九千片を貼り合わせるのに 30 秒を要するサーバー。これらはレビューでは見つかりません。500 GB をアップロードし、止め、また始めることで見つかるのです。
RocketBucket は S3、R2、B2、MinIO のためのデュアルペイン・ファイル転送アプリで、Mac App Store で公開中です。rocketbucket.app