同じ請求が2回届いた
冪等性とは、同じ操作を何回行っても、結果が1回行ったときと同じになる性質です。分散システムでは、メッセージの送り直し(再試行)が日常的に起きるので、受け取る側にこの性質が欠かせません。
お客様が3000円を払ったのに、決済のメッセージが2回届いて、3000円が2回引き落とされた。これを防ぐ設計を学びます。
このレッスンのゴール
メモ
再試行されても二重に処理しない、メッセージの受け取り方を設計できます。
キーワード
| ことば | いみ |
|---|---|
| 再試行 | 返事が来ないとき、同じ操作をもう一度行うこと |
| 少なくとも1回 | 1回は必ず届くが、重複するかもしれない配送 |
| 最大1回 | 重複はしないが、失われるかもしれない配送 |
| 冪等性 | 何回行っても、結果が1回と同じになる性質 |
| メッセージID | メッセージを見分けるための、決まった名前 |
| コミット | どこまで読んだかを、確かな形で記録すること |
学生が入力するセルは、このプレビューでは空白です。
返事が来ないから、送り直す
送る側は、メッセージを送ったあと、受け取った合図を待ちます。合図が来ないとき、メッセージが届かなかったのか、合図だけが届かなかったのかは、分かりません。
分からないときの安全な選択は、再試行です。送り直せば、メッセージは失われません。ただし、本当は届いていたなら、受け取る側には同じメッセージが2件並びます。
1パーティションのトピックを作る
curl -s -X POST -H "Content-Type: application/json" -d '{"name":"receipts","partitions":1,"replicationFactor":1,"configs":{}}' http://kafka-ui:8080/api/clusters/lab/topics -o /dev/null -w "%{http_code}\n"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
決済のメッセージr1001を送る
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"r1001","value":"paid3000","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/receipts/messages -o /dev/null -w "%{http_code}\n"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
合図が来なかったつもりで、送り直す
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"r1001","value":"paid3000","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/receipts/messages -o /dev/null -w "%{http_code}\n"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ログに何件並んだか見る
curl -s http://kafka-ui:8080/api/clusters/lab/topics/receipts | grep -o '"partition":[0-9]*\|"offsetMax":[0-9]*'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
配送の約束は3つある
| 約束 | 失われるか | 重複するか | 使いどころ |
|---|---|---|---|
| 最大1回 | 失われ得る | しない | 温度の記録など、1件抜けてもよいもの |
| 少なくとも1回 | 失われない | し得る | 決済など、抜けては困るもの |
| ちょうど1回 | 失われない | しない | 少なくとも1回と冪等な処理で作る |
学生が入力するセルは、このプレビューでは空白です。
IDを覚えれば、重複は怖くない
重複を防ぐ鍵は、冪等性です。各メッセージにメッセージIDをつけ、処理したIDを覚えておきます。同じIDが来たら、飛ばします。
送る側は「少なくとも1回」届けて、受け取る側が重複を捨てる。この組み合わせで、結果は「ちょうど1回」と同じになります。
同じIDを飛ばして合計する
# 同じIDのメッセージは、2回目から飛ばします。IDは、処理したものを覚えておく目印です。
messages = [["r1001", 3000], ["r1001", 3000], ["r1002", 1200]]
done = []
total = 0
for message in messages:
if message[0] in done:
print(message[0], "は処理済みなので、飛ばします")
else:
done.append(message[0])
total = total + message[1]
print("合計:", total, "円")
# 2つ目の "r1001" を "r1003" に変えると、合計はいくらになるでしょうか。
ログを最初から読み直す
curl -s -N "http://kafka-ui:8080/api/clusters/lab/topics/receipts/messages/v2?seekType=BEGINNING&partitions=0&limit=10" | grep -o '"partition":[0-9]*,"offset":[0-9]*\|"value":"[a-z0-9]*"'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
オフセットの記録は、処理のあとにする
コンシューマーは、どこまで読んだかをコミット(確かな形で記録)します。処理のあとにコミットすれば、途中で止まっても、次はコミットの位置から読み直せます。
ただし、処理が終わってからコミットするまでの間に止まると、同じメッセージをもう一度読みます。これが「少なくとも1回」の重複の正体です。
よくある間違い
注意
「ちょうど1回」の配送は、ネットワークだけで実現できると考えがちです。送り直しがある限り、重複は起こります。受け取る側が重複を捨てて、はじめて、結果が1回になります。