TT Lab
はじめる
学ぶ 学習パス コース

オブジェクトストレージとS3

delete markerと非現行バージョン

TT Labで続きを見る

一言でいうと

バージョニングを有効にすると、削除は消すことではなく、「消したという印(delete marker)」をもう1つ載せることになります。元のデータはそのまま残り、料金も出続けます。

なぜ必要なのか

S3は長いあいだ、結果整合性でした。オブジェクトを上書きしてすぐ読むと、古い値が返ることがあり、このために多くのアプリケーションがリトライロジックを入れました。2020年12月から、AWS S3は強い読み取り-書き込みの整合性を提供しています。PUT直後のGETが、新しい値を見ます。

ただし、この保証の範囲を正確に知っておく必要があります。

操作 現在の保証 注意
PUTのあとのGET(新しいキー) 強い整合性 —
PUTのあとのGET(上書き) 強い整合性 —
DELETEのあとのGET 強い整合性 バージョニングを有効にすると、404のあとにデータが残っています
LIST 強い整合性 ほかの実装は、たいてい、ここで弱いです
マルチリージョンレプリケーション 非同期 レプリカは、いつでも遅れることがあります

「S3は今は強い整合性」という文を、すべてのS3互換ストレージにそのまま当てはめてはいけません。MinIOは強い整合性ですが、Ceph RGWは設定によって違い、SeaweedFSは、filerの構成によって、一覧の取得が遅れることがあります。このラボのストレージも同じです。自分が書くコードがどんな保証の上に立っているかは、ドキュメントではなく実測で確認します。

条件付きリクエストで競合を防ぐ

強い整合性があっても、2つのプロセスが同じキーに同時に書く問題は残ります。あとに書いたものが勝つので、前の更新が黙って消えます。S3は、これを条件付きヘッダーで防ぎます。

# 없을 때만 만든다 — 있으면 412 Precondition Failed
s3.put_object(Bucket=b, Key=k, Body=data, IfNoneMatch="*")

# 내가 읽은 그 버전일 때만 덮어쓴다(낙관적 잠금)
head = s3.head_object(Bucket=b, Key=k)
s3.put_object(Bucket=b, Key=k, Body=new, IfMatch=head["ETag"])

IfMatchが412を返したら、その間に誰かが変更したということです。もう一度読み直してマージし、リトライします。このパターンがないと、「同時に保存を押したら、1人分だけが残る」事故が起きます。

バージョニングは別の問題を解く

バージョニングが解くのは、整合性ではなく、誤って上書きしたり削除したりしたものを元に戻す問題です。

バージョニングを有効にすると、同じキーに書くたびに、新しいバージョンIDができます。最新のものが現行バージョンで、残りは非現行バージョンです。古いバージョンは、バージョンIDでいつでも読めます。

削除が興味深いところです。バージョンIDなしでDELETEすると、実際には削除されず、delete markerという特殊なバージョンが、一番上に載ります。そのため、通常のGETは404を受け取りますが、データはその下にそのまま残っています。

키 report.csv 의 버전 스택 (위가 최신)

  delete marker   (v4)  ← 지금 GET 하면 404
  ────────────────────
  2026-09-05 판   (v3)  ← v4 를 지우면 이것이 다시 현행이 된다
  2026-09-04 판   (v2)
  2026-09-03 판   (v1)

delete markerを削除すると、すぐ下のバージョンが、再び現行になります。これが、ミスからの復旧の仕組みです。本当に削除するには、バージョンIDを明示してDELETEする必要があり、この操作は元に戻せません。

もう1つ。バージョニングは、有効にはできても、無効にはできません。Suspendedの状態にしかできず、すでに溜まったバージョンはそのまま残ります。Suspendedの状態で書いたオブジェクトは、バージョンIDがnullの特殊なバージョンになり、次にまた書くと、それを上書きします。有効にする前に、ライフサイクルルールを先に決める理由です。

現場での姿

バージョニングのコストは、よく見落とされます。ログファイルを毎日同じキーに上書きするコードにバージョニングを有効にすると、1年後に、そのキー1つに365個のバージョンが溜まっています。一覧ではオブジェクト1つに見えるのに、料金は365倍です。そして、「全部消したのに料金がそのまま」という状況も、delete markerのせいです。

隠れたコストがもう1つあります。失敗したマルチパートアップロードです。5GBのファイルをアップロードしていて、80%で切れると、すでにアップロードされたパートが、どこにも見えないまま課金されます。一覧にも出ず、バージョン一覧にも出ません。list-multipart-uploadsでだけ見えます。

そのため、バージョニングを有効にするときは、必ずライフサイクルルールを一緒にかけます。3つのルールが、基本セットです。

{"Rules": [
  {"ID": "expire-noncurrent", "Status": "Enabled", "Filter": {},
   "NoncurrentVersionExpiration": {"NoncurrentDays": 30}},
  {"ID": "clean-delete-markers", "Status": "Enabled", "Filter": {},
   "Expiration": {"ExpiredObjectDeleteMarker": true}},
  {"ID": "abort-incomplete-mpu", "Status": "Enabled", "Filter": {},
   "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}}
]}

ルールは1日1回、非同期で動くので、すぐには反映されません。適用の翌日から容量が減り始めれば、正常です。

元に戻せない削除を、そもそも防ぐ必要があるデータには、Object Lockを使います。GOVERNANCEモードは、特別な権限があれば解除でき、COMPLIANCEモードは、ルートアカウントでも期間内は削除できません。監査対象のログには、後者を使います。

次のラボですること

バージョニングを有効にして、同じキーに3回書いてバージョンを溜め、古いバージョンを読み、削除後のdelete markerを確認し、それを除去して復旧し、非現行バージョンが占める容量を計算したあと、ライフサイクルルールを適用します。最後に、条件付きPUTが412を返すことを、自分で確認します。