マルチパートアップロードとETagの算数
一言でいうと
マルチパートのETagは、内容のmd5ではありません。各パートのmd5のバイナリ値をつなげてもう一度md5し、-파트수(プレースホルダーはパート数です)を付けた値です。
なぜ必要なのか
5GBのファイルを1回のPUTでアップロードしていて、90%の地点で接続が切れると、最初からやり直しです。そして、1本のTCP接続では、帯域幅を使い切れません。
マルチパートアップロードが、この2つを解決します。アップロードを開始してUploadIdを受け取り、ファイルをパートに分けて並列でアップロードし、最後にパートの一覧で完了を通知します。途中で切れても、そのパートだけをやり直せばよいのです。
どう動くのか
ルールに数字があります。パートは最大10,000個で、最後のパートを除く各パートは、最小5MiBである必要があります。パートの最大サイズは5GiBです。この3つを掛け合わせると、単一オブジェクトの最大サイズが出てきます。5GiB x 10,000 = 約5TBです。
パートサイズの選択は、トレードオフです。小さいとリクエスト数が増え(リクエストごとに課金があるサービスではコスト)、大きいと再送のコストが大きくなります。実務では、8–64MiBの間がよく使われます。ファイルサイズを10,000で割った値よりは大きい必要があります。
ETagの計算が興味深いところです。単一パートなら、内容のmd5そのままです。マルチパートなら、次のように計算します。
1. 각 파트의 md5 를 이진(16바이트)으로 구한다
2. 그것들을 순서대로 이어 붙인다 (파트 5개면 80바이트)
3. 그 80바이트의 md5 를 구한다
4. 뒤에 "-5" 를 붙인다
そのため、ETagにハイフンがあれば、マルチパートでアップロードされたオブジェクトで、ローカルファイルのmd5とは絶対に一致しません。「ETagで完全性を検証したのに合わない」という問い合わせの大半が、これです。検証するには、同じパートサイズで分けて、同じ計算をする必要があります。
パートサイズを決める計算
パートサイズは、勘で決めません。2つの制約が、上限と下限を決めてくれます。
하한 = 파일 크기 ÷ 10,000 (파트 수 제한)
하한 = 최소 5 MiB (마지막 파트 제외)
상한 = 5 GiB
100GB 파일이면 → 100GB ÷ 10,000 = 10MB 이상이어야 한다
→ 16MiB 로 잡으면 6,400 파트. 여유가 있다
そのあと、再送のコストを見ます。ネットワークが不安定で、平均2%のパートが失敗するなら、パートが大きいほど、1回の失敗で捨てるバイトが大きくなります。逆に、パートが小さいと、リクエスト数が増えて、オーバーヘッドとリクエスト課金が増えます。
| ファイルサイズ | 推奨パート | パート数 | 備考 |
|---|---|---|---|
| 100MBまで | マルチパート不要 | 1 | 単一PUTが簡単です |
| 100MB–1GB | 8–16 MiB | 12–64 | 並列4–8で十分です |
| 1GB–50GB | 32–64 MiB | 30–800 | 並列8–16 |
| 50GB以上 | 64–128 MiB | 400–800 | パート数の上限を先に確認 |
並列度は、帯域幅とメモリで決めます。パートサイズ × 並列度の分だけメモリに載るので、64MiB × 16 = 1GBです。コンテナのメモリ上限を超えると、OOMで死にます。
チェックサムで完全性を確認する正しい方法
ETagで完全性を確認しようとする試みは、マルチパートで崩れます。代わりに、S3が提供する追加のチェックサムを使います。アップロードするときにアルゴリズムを指定すると、S3がパートごとに検証し、オブジェクト全体のチェックサムも保管します。
s3.put_object(Bucket=b, Key=k, Body=data, ChecksumAlgorithm="SHA256")
r = s3.head_object(Bucket=b, Key=k, ChecksumMode="ENABLED")
print(r.get("ChecksumSHA256"))
マルチパートでは、この値も合成方式ですが(パートのチェックサムをつなげて、もう一度ハッシュ)、S3がパートを受け取るたびに検証するので、転送中の破損はその場で見つかります。ETagを手で再計算するより、はるかに安全です。
CRC32Cは計算が速く、大容量に有利で、SHA256は遅いですが、暗号学的に強力です。転送エラーの検出が目的なら、CRC32Cで十分です。
中断されたアップロードを見つけて整理する
# 이 버킷에 떠도는 미완료 업로드
aws s3api list-multipart-uploads --bucket my-bucket
# 하나 지우기 — 올라간 파트가 모두 사라진다
aws s3api abort-multipart-upload --bucket my-bucket --key big.tar --upload-id <UploadId>
手動で探すのは忘れます。ライフサイクルルールで自動化するのが正解です(AbortIncompleteMultipartUpload、7日)。このルール1つが、バケットを作るときに、標準で入れておくべき設定です。
現場での姿
中断されたマルチパートアップロードは、静かにお金を食います。完了していないアップロードのパートは、ストレージ容量を占めますが、一覧にはオブジェクトとして見えません。クライアントが死んだり、デプロイ中に切れたりしたアップロードが数か月溜まると、請求書に理由のない数百GBが現れます。AWSでよくあるコスト漏れの項目です。
対応は2つです。アプリケーションが、失敗時に明示的に中止(abort)すること、そしてバケットのライフサイクルルールにAbortIncompleteMultipartUploadを7日ほどでかけておくことです。後者を標準としてかけておくのが安全です。
次のラボですること
24MiBのファイルを5MiBのパートに分けて、自分でマルチパートアップロードします。UploadIdを受け取り、パートをアップロードし、完了し、そのあとマルチパートETagを手で計算して、サーバーの値と合わせてみます。最後に、アップロードを開始だけして中止するところまでやってみます。