コンテンツにスキップ

Amazon S3で分散ロックを実現するsets3lock

チェック

  • [ ] 本文を確認した
  • [ ] 概要を確認した
  • [ ] タグを確認した
  • [ ] inbox/ 直下へ移行した

概要

Amazon S3 の条件付き書き込みと条件付き削除を使い、DynamoDB なしで分散ロックを実現する sets3lock の紹介記事。 ロック用 S3 オブジェクトが存在することを「ロック獲得中」とみなし、PutObjectIf-None-Match: * で獲得、DeleteObjectIf-Match: <etag> で自分のロックだけを解放する。 Terraform の S3 backend が DynamoDB なしの state lock に寄っている流れと同じく、S3 単体で排他制御できるケースが増えたことが背景。

本文

記事の背景は、Amazon S3 が条件付き書き込みと条件付き削除をサポートしたこと。 以前は、S3 を使った処理で多重実行を防ぎたい場合、DynamoDB を併用してロックを持つことが多かった。 しかし S3 側に条件付き操作が入ったことで、単純な排他制御なら S3 だけで実現できる場面が増えた。

DynamoDB が必要だったケース

筆者は以前、AWS Lambda と S3 を使って yum レポジトリを作った。 この仕組みでは、yum レポジトリのメタデータ更新が同時に走ると、片方の更新が上書きされるロストアップデートが起きる可能性があった。 そのため、当時は DynamoDB を使って排他的ロックを実装していた。

同じような文脈として Terraform の S3 backend も挙げられている。 Terraform は state の多重更新を防ぐためにロックが必要で、以前は DynamoDB table を併用する構成が一般的だった。 Terraform v1.10 以降では、S3 側の条件付き書き込みを使うことで、DynamoDB なしの state locking が可能になっている。

つまり、S3 にオブジェクトを置く処理と排他制御だけが必要な場合、DynamoDB を増やさずに済む可能性がある。

sets3lock の位置づけ

sets3lock は、setlock の S3 版。 setlock は指定したロックを取ってからコマンドを実行する wrapper 的なプログラムで、同じ系統として Redis 版や DynamoDB 版も作られてきた。

sets3lock では、ロック対象がローカルファイルではなく S3 オブジェクトになる。 使い方は次のような形。

sets3lock s3://bucket/key ./task.sh

この場合、s3://bucket/key をロックオブジェクトとして使い、ロックを取れたプロセスだけが task.sh を実行する。 ロックは S3 にあるため、同じサーバー内だけでなく、複数サーバーや Lambda など分散した実行環境の間でも共有できる。

Go ライブラリとしての利用

コマンドだけでなく Go ライブラリとしても使える。 インターフェースは DynamoDB 版の setddblock に合わせている。

利用イメージは次の通り。

package main

import (
    "context"
    "log"

    "github.com/shogo82148/sets3lock"
)

func main() {
    ctx := context.Background()

    locker, err := sets3lock.New(ctx, "s3://bucket/key")
    if err != nil {
        log.Fatal(err)
    }

    if _, err := locker.LockWithErr(ctx); err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err := locker.UnlockWithErr(ctx); err != nil {
            log.Printf("unlock failed: %v", err)
        }
    }()

    // ここに排他的に実行したい処理を書く。
}

記事では、現時点では heartbeat をしないため、タイムアウト時間の設計が必要かもしれないと補足されている。 長時間処理でプロセスが死んだ場合に、どのタイミングでロックを古いものとして扱うかは、分散ロックでは重要な論点になる。

ロック獲得の仕組み

sets3lock の考え方は単純で、ロック用オブジェクトが S3 に存在すればロック中、存在しなければロック未取得とみなす。

ロック獲得では PutObject を条件付きで実行する。

aws s3api put-object \
  --bucket bucket \
  --key key \
  --if-none-match '*' \
  --body lock.json

重要なのは --if-none-match '*'。 これは「対象オブジェクトが存在しない場合だけ書き込む」という条件になる。 すでに他のクライアントが同じ key にロックオブジェクトを作っていれば、書き込みは失敗する。 失敗した側は、一定時間待ってからリトライする。

分散ロックで難しい「同時に複数プロセスがロックを取ってしまわないこと」は、この条件付き書き込みに任せる。

ロック解放の仕組み

ロック解放では DeleteObject を条件付きで実行する。 ロック獲得時の PutObject で返ってきた ETag を使い、その ETag と一致するオブジェクトだけを削除する。

aws s3api delete-object \
  --bucket bucket \
  --key key \
  --if-match 'etag'

--if-match を付けることで、「自分が作ったロックオブジェクト」を削除することを保証する。 もしロックの期限切れや別の処理により、同じ key に別のロックオブジェクトが作られていた場合、ETag が違うため削除は失敗する。

この点が重要。 単に key だけで削除すると、古いロック保持者が復帰したときに、別のクライアントが取得した新しいロックを消してしまう危険がある。 ETag 条件付き削除は、その事故を避けるための最低限の安全策になる。

yum レポジトリへの組み込み

記事の本来の目的は、自作 yum レポジトリのメタデータ更新処理から DynamoDB 依存を外すこと。 sets3lock ができたことで、メタデータ更新処理を LockWithErrUnlockWithErr で囲むだけでよくなった。

つまり、変更の本質は「DynamoDB lock table を見る」から「S3 lock object を条件付き操作する」へ移したこと。 S3 をすでにメタデータ置き場として使っている構成では、追加リソースが減る。

使える場面と注意点

S3 だけで完結する lock は、次のような場面に向く。

  • S3 上のメタデータを更新する処理の多重実行防止。
  • Lambda など分散実行環境で、同じ task を同時実行したくない場合。
  • DynamoDB table を増やすほどではない簡単な排他制御。
  • Terraform backend のように、S3 と排他制御が密接な処理。

一方で、強い lease、heartbeat、フェイルオーバー、ロック所有者の監視、高頻度な lock/unlock が必要な場合は、専用の分散ロック実装や DynamoDB、RDB、Redis などの方が適する可能性がある。 S3 は KVS として使えるが、ロックマネージャではない。 どの程度の安全性と復旧性が必要かを見て使う。

要点

  • S3 の条件付き書き込みと条件付き削除により、S3 単体で排他制御できる場面が増えた。
  • sets3locksetlock の S3 版で、S3 オブジェクトをロックとして使う。
  • ロック獲得は PutObject + If-None-Match: *
  • ロック解放は DeleteObject + If-Match: <etag>
  • If-Match により、自分が作ったロックオブジェクトだけを削除できる。
  • Go ライブラリとしても使え、既存処理を LockWithErr / UnlockWithErr で囲める。
  • heartbeat は少なくとも現時点ではないため、長時間処理や異常終了時の扱いは設計が必要。

タグ

aws #s3 #distributed-lock #go #concurrency #terraform