べき等性とexactly-once配信の幻想
重複メッセージで二重課金や残高ずれを起こさないために。at-least-once配信の上に、べき等キーと重複排除窓で「実効的exactly-once」を組む原理と、真のexactly-onceが原理的に不可能な理由がわかります。
- 1.ネットワークでは送信側のackロストと受信側の処理失敗が区別できないため、配信は至多一度(メッセージ欠落あり)か至少一度(重複あり)のどちらかしか選べない。
- 2.実務のexactly-onceは、at-least-once配信+受信側のべき等な処理(べき等キーと重複排除)で「効果が一度だけ」を作る合成物であり、配信そのものが一度になるわけではない。
- 3.重複排除は無限の記憶を持てないため有限のdedup窓で運用され、遅延した重複が窓を越えると二重処理が漏れる。窓は再送タイムアウトの上限から逆算して設計する。
二将軍問題:なぜ「一度だけ届けた」と確信できないのか
メッセージング設計の出発点は、ひとつの不可能性です。送信側がメッセージを送り、受信側が ack を返す。ところがネットワーク上では、ack が失われた状態と、メッセージそのものが届かなかった状態を、送信側からは区別できません。送信側のタイムアウトが発火したとき、起きうるのは次の二つです。
ケースA: メッセージが届かず処理されていない(再送が必要)
ケースB: メッセージは届いて処理され、ack だけが帰り道で消えた(再送は重複になる)
送信側は A か B かを知る術がありません。ここで二択を迫られます。安全側に倒して再送すれば、B のときに重複が生じる(at-least-once/至少一度)。重複を嫌って再送しなければ、A のときにメッセージが永久に欠落する(at-most-once/至多一度)。この「ackの応答自体もまた失われうる」という入れ子構造が、有限回のやり取りでは合意に至れないことを示す二将軍問題です。
最後に送ったメッセージが相手に届いたかを、送信側は確認できません。確認の ack を要求すれば、今度はその ack が届いたかを相手が知りたくなり、確認の確認が無限に続きます。だからどこかで「届いたと仮定して」打ち切るしかなく、その仮定が外れたときに欠落か重複が生まれます。コンセンサスの不可能性(/devops/flp-impossibility/)と同じ根を持つ問題です。
つまり配信の保証として原理的に選べるのは at-most-once か at-least-once のどちらかであり、exactly-once(過不足なく一度だけ配信される)は配信レイヤ単独では到達できません。
at-least-once を土台に選ぶ
欠落と重複のどちらが許せるかは用途で決まります。多くの業務系(決済、注文、在庫)ではメッセージの欠落が致命傷です。注文が消える・引き落としが行われない事態は、重複よりはるかに重い。そこで土台には at-least-once を採り、「重複は出るが欠落はしない」状態を確保します。
at-least-once の代償が重複です。再送(/devops/retry-backoff-jitter/)が走るたび、ケースB の取りこぼし ack が二重処理を生みます。さらにブローカ側のリバランスやコンシューマのクラッシュ後の再配信でも重複は発生します。重複は例外ではなく、at-least-once では定常的に起こる前提として扱わねばなりません。
ここで戦略が定まります。配信を一度にするのは諦め、受信側で「何度届いても効果が一度きり」になるよう処理を設計する。配信の重複を、処理のべき等性で吸収するわけです。
べき等性:効果が一度きりになる処理を作る
べき等(idempotent) とは、同じ操作を何度適用しても結果(システムの状態)が変わらない性質です。数学では f(f(x)) = f(x)、HTTP では PUT や DELETE が該当します。重複が前提の世界では、処理がべき等であれば重複が無害化されます。
問題は、現実の多くの操作が本質的に非べき等なことです。「残高を100引く」「在庫を1減らす」「メールを送る」は、二度実行すれば二度効きます。これらをべき等にする鍵が べき等キー(idempotency key) です。
# べき等キーによる重複排除の骨格
key = リクエスト固有のID(クライアントが生成、再送でも不変)
処理(key, payload):
if 既に key を処理済み:
return 保存しておいた前回の結果 # 副作用を再実行しない
結果 = 本来の処理を実行(payload)
key と結果を原子的に記録
return 結果
肝は二点です。第一に、キーはクライアントが生成し、再送のたびに同じ値であること。サーバー側で採番すると再送ごとに別キーになり重複排除が成立しません。第二に、「処理の実行」と「キーの記録」が原子的であること。ここが割れると、処理だけ済んでキー記録前にクラッシュした場合に、再送で二重実行します。
「キーの存在を確認 → なければ実行」を別々の操作で書くと、二つの再送がほぼ同時に来たとき両方が「未処理」と判定し、二重実行します(TOCTOU)。INSERT ... ON CONFLICT DO NOTHING でキー行の挿入が成功したスレッドだけが処理する、あるいは一意制約で二人目を弾く、といった**原子的な「初回判定」**が必須です。重複排除は本質的に並行制御の問題です。
重複排除の状態はどこに置くか:dedup窓という現実
べき等キーで重複を弾くには、処理済みキーの集合を覚えておく必要があります。理想は全キーを永久に保持することですが、それは無限のストレージを要求します。実運用では、処理済みキーを有限の 重複排除窓(dedup window) に保持します。
窓は時間(例:過去24時間)かサイズ(直近N件)で区切ります。窓の外に出たキーは忘れられます。ここに穴が開きます。再送が窓より遅れて到着すると、そのキーは「未知」と判定され二重処理されるのです。
t=0 : key=K を処理、記録
t=24h : K が窓から退避(忘却)
t=25h : 大幅に遅延した再送の K が到着
→ 記録に無いので「初回」と誤判定 → 二重処理が漏れる
したがって窓の長さは、再送が起こりうる最大遅延より長くなければなりません。設計手順は逆算です。送信側の再送タイムアウトの上限(リトライ予算が尽きるまでの総時間、メッセージの TTL)を見積もり、それを十分に上回る窓を取ります。窓を短くすればストレージは軽くなりますが、遅延重複の取りこぼしリスクが上がる——このトレードオフが dedup 設計の中心です。
| 項目 | 短いdedup窓 | 長いdedup窓 |
|---|---|---|
| ストレージ | 軽い(キー集合が小さい) | 重い(保持量が増え続ける) |
| 遅延重複の検出 | 弱い(窓越えで二重処理が漏れる) | 強い(古い再送も弾ける) |
| 適する状況 | 再送遅延が短く有界な経路 | 長時間の再配信・障害復旧がある経路 |
| 主な失敗様式 | 窓を越えた重複の取りこぼし | 状態肥大・退避処理のコスト増 |
実装ではブローカ組み込みの重複排除(Kafka のプロデューサ冪等性は producer ID とシーケンス番号でブローカ側の再送重複を窓内で除去)や、アプリ側のキー表(Redis の TTL 付きセット、RDB の一意制約テーブル)が使われます。いずれも有限窓である点は変わりません。
「実効的exactly-once」の正体と、真のexactly-onceが不可能な理由
ここまでを束ねると、実務で語られる exactly-once の実体が見えます。
実効的 exactly-once = at-least-once 配信(欠落しない)
+ 受信側のべき等処理(重複が効果に表れない)
配信は依然として複数回起こりえます。一度になっているのは観測される効果(状態変化や副作用)であって、メッセージの到着回数ではありません。だからこれは「effectively-once(実効的に一度)」と呼ぶのが正確で、配信そのものが一度になる真の exactly-once 配信は存在しません。理由を三層で整理します。
- 配信レイヤ単独では二将軍問題で原理的に不可能:ack のロストが区別できない以上、配信は at-most か at-least のどちらかにしかならない。
- べき等性で「効果」を一度にできても、外部副作用が冪等とは限らない:DB 更新は冪等キーで一度にできても、「外部 API への課金」「物理的なメール送信」が相手側でべき等でなければ、効果の一度性は相手のべき等性に依存する。境界を越えるたびに保証が継ぎ目で漏れる。
- 重複排除は有限窓でしか運用できない:無限の記憶がない以上、窓を越えた重複は必ず存在しうる。確率を下げられても、ゼロにはできない。
「うちは exactly-once です」という主張は、ほぼ常に「at-least-once 配信+べき等な消費」の言い換えです。レビューで確認すべきは三点——(1) べき等キーをクライアントが採番し再送で不変か、(2) 「初回判定とキー記録」が原子的か(TOCTOU を塞いでいるか)、(3) dedup窓が再送の最大遅延を上回るか。さらに「処理の確定(コミット)」と「ackの送信」の順序も問われます。先にコミットしてから ack すれば at-least-once、先に ack してからコミットすると at-most-once に化けます。
ストリーム処理基盤(Flink 等)の exactly-once も、入力オフセット・処理状態・出力をチェックポイントで原子的に確定し、障害時にその一貫したスナップショットへ巻き戻す仕組みであって、二将軍問題を破っているわけではありません。境界の外(最終的な書き込み先)でべき等性かトランザクションが効いて初めて、端から端までの効果一度性が成立します。これは状態一貫性の議論(/devops/consistency-models/)と地続きで、どこに一貫性の境界を引くかという設計判断に帰着します。
まとめ
- ackのロストと欠落が区別できない(二将軍問題)ため、配信は at-most-once(欠落あり)か at-least-once(重複あり) の二択。真の exactly-once 配信は原理的に存在しない。
- 業務系は欠落が致命的なので at-least-once を土台にし、定常的に出る重複を受信側のべき等処理で無害化する。
- べき等の鍵は クライアント採番のべき等キー と、初回判定とキー記録の原子性(TOCTOU を塞ぐ並行制御)。
- 重複排除は 有限のdedup窓でしか運用できず、再送の最大遅延を上回る窓長を逆算する。窓を越えた遅延重複は取りこぼれる。
- 実務の exactly-once は「at-least-once+べき等消費」が作る 実効的(effectively-once) な効果の一度性であり、配信が一度になるわけではない。
DevOps/インフラ Article
べき等性とexactly-once配信の幻想を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
べき等性
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5
導入後に効く点
実務のexactly-onceは、at-least-once配信+受信側のべき等な処理(べき等キーと重複排除)で「効果が一度だけ」を作る合成物であり、配信そのものが一度になるわけではない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「べき等性 / exactly-once」に近いか確認する。
- 強みである「ネットワークでは送信側のackロストと受信側の処理失敗が区別できないため、配信は至多一度(メッセージ欠落あり)か至少一度(重複あり)のどちらかしか選べない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。