Skip to content

Commit 9fe9316

Browse files
authored
非同期設計ガイドライン: ブラ主アップ (#255)
1 parent a2037e0 commit 9fe9316

File tree

2 files changed

+109
-17
lines changed

2 files changed

+109
-17
lines changed

documents/forAsync/async_guidelines.md

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,43 @@ graph LR
167167
- 非同期から非同期を呼び出す基準としては、**前段と後続でイベント処理の粒度が変わるケース**は許容する
168168
- **呼び出し元より粒度が「大きく」なるケース(集約)**
169169
- **(例)ECサイト:** 注文(同期)→ 在庫引当(非同期1)→ **倉庫内のピッキング指示(非同期2)**
170-
- **理由:** 引当の都度ピッキング指示を出すより、一定数近隣のロケーションからのピッキングが溜まった時点でまとめて指示をする方が効率的なため
170+
- **理由:** 引当の都度ピッキング指示を出すより、一定数近隣のロケーションからのピッキングが溜まった時点でまとめて指示をする方が効率的なため。ただし、この場合は別プロセスでポーリングさせピッキングが溜まった時点での指示を第一に考える
171171
- **呼び出し元より粒度が「細かく」なるケース(分散)**
172172
- **(例)月次決済:** 締め処理(同期)→ 請求額計算(非同期1)→ **個々のユーザ決済実行(非同期2)**
173173
- **理由:** 複数のユーザーに対する決済処理を「非同期1」の中で順次ループ処理すると、1件の失敗や遅延が全体の完了を遅らせるリスクがあるため、個別に分割して実行する
174174

175+
判断フローの例を示す。
176+
177+
```mermaid
178+
graph LR
179+
Start([コンシューマー処理中]) --> Purpose{さらに非同期処理を<br>呼び出す目的は?}
180+
181+
Purpose -- "データ整合性<br>(外部API連携など)" --> RollbackCheck{外部SaaS失敗時に<br>自DBもロールバック可か?}
182+
RollbackCheck -- Yes --> RetryAll>単一トランザクションで<br>全体リトライ]
183+
RollbackCheck -- No --> FinalChain
184+
185+
Purpose -- "性能向上<br>(並列化)" --> ProgParallel{言語機能による<br>並列化は現実的か?}
186+
ProgParallel -- Yes --> ProgThread>プログラム内で並列化]
187+
ProgParallel -- No --> GranularityCheck
188+
189+
Purpose -- "リカバリ単位の分割<br>(ロングトランザクション)" --> GranularityCheck{元のタスク粒度を<br>細かく分割できるか?}
190+
191+
GranularityCheck -- Yes --> SplitTask>プロデューサー側での<br>分割を検討]
192+
GranularityCheck -- No --> FinalChain
193+
194+
Purpose -- 流量制御 --> FlowCheck1{コンシューマーの<br>同時実行数の制限可能か?}
195+
FlowCheck1 -- Yes --> Avoid1>設定値での制御を優先する]
196+
197+
FlowCheck1 -- No --> PollingCheck{ポーリング等<br>別の非同期方式は<br>採用可能か?}
198+
PollingCheck -- Yes --> AdoptPolling>ポーリング等の検討]
199+
200+
PollingCheck -- No --> FlowCheck2{Sleep等ロジックでの<br>回避が現実的か?}
201+
FlowCheck2 -- Yes --> Avoid2>ロジックでの制御を検討]
202+
FlowCheck2 -- No --> FinalChain
203+
204+
FinalChain>非同期の連鎖を検討]
205+
```
206+
175207
# バッチから非同期呼び出し
176208

177209
非同期処理の呼び出しは、ユーザーの操作イベント経由だけではなく、バッチ処理から呼びだされる場合もありえる。[バッチ設計ガイドライン](https://future-architect.github.io/arch-guidelines/documents/forBatch/batch_guidelines.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%82%BF%E3%82%B9%E3%82%AF) を参照すること。
@@ -320,7 +352,7 @@ SQSを利用する場合の重複排除(Exactly once)の仕組みには以
320352
推奨は以下の通り。
321353

322354
- 原則、(1)を採用する。FIFOキュー利用によるクラウド利用費用の増加は多くのケースで誤差レベルであると考えられるため
323-
- スループットが重視されるケース(数千~数万TPS)や5分間という制限を超えた重複排除が必要なケースは(2)を採用する
355+
- スループットが重視されるケース(数千~数万TPS)[SQS FIFOキューの重複排除ウィンドウである5分間](https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html)を超える必要があるケースは(2)を採用する
324356

325357
# リトライ
326358

@@ -340,7 +372,13 @@ SQSを利用する場合の重複排除(Exactly once)の仕組みには以
340372

341373
メッセージロックのタイムアウトとは、キューにおいてコンシューマーがメッセージを取得し処理している間、そのメッセージを他のコンシューマーから見えなくし重複処理を防ぐための時間制限のことである。Amazon SQSでは、このメッセージロックのタイムアウトを「Visibility Timeout:可視性タイムアウト」と呼ぶ。
342374

343-
タイムアウト値の設定についてのトレードオフは以下の通り。
375+
設定の主体:
376+
377+
- 本設定は、キューの定義またはコンシューマー側で行う。プロデューサーの指定は不可
378+
- キュー定義: キュー作成時にデフォルト値として指定できる
379+
- コンシューマー側: メッセージ受信時に動的にタイムアウト値を指定できる。処理中にAPI(`ChangeMessageVisibility`)を呼び出して延長もできる
380+
381+
タイムアウト設定のトレードオフ:
344382

345383
- **短すぎる場合:** メッセージの処理中にタイムアウトを迎えた場合、他のコンシューマーが同じメッセージを取得して処理を開始してしまうため、処理が重複して実行されてしまう
346384
- **長すぎる場合:** メッセージの処理中に、一時的なエラー(ネットワークエラーなど)が発生した場合などは、コンシューマーは処理を中断し、メッセージをキューに戻すことでリトライを試みることが一般的である。タイムアウト値が長いと、この再試行が許可されるまでの時間が長くなるため、リカバリまでの時間が長くなり、メッセージが滞留することで全体のスループットが低下する
@@ -355,7 +393,61 @@ SQSを利用する場合の重複排除(Exactly once)の仕組みには以
355393

356394
# メッセージ送信の信頼性
357395

358-
非同期処理の課題の1つとして、「DBの更新」と「後続メッセージの送信」という2操作の整合性をどのように保つかがある。
396+
プロデューサー側で自領域のDB更新とキューへのメッセージ送信の整合性をどのように保つかが課題となる。例えば、キューの送信には成功したが自領域のDBのコミットに失敗してしまうことが考えられる。この場合、コンシューマーは起動するが処理対象のデータが存在しないため不整合となる。これはファントムメッセージと呼ばれる。
397+
398+
```mermaid
399+
---
400+
title: 【失敗例】ファントムメッセージ
401+
---
402+
%%{init: {'sequence': {'mirrorActors': false}}}%%
403+
sequenceDiagram
404+
autonumber
405+
participant App as プロデューサー
406+
participant DB as DB
407+
participant Queue as キュー
408+
participant Worker as コンシューマー
409+
410+
App->>DB: 業務処理
411+
App->>Queue: Send Message
412+
Queue-->>App: OK (Ack)
413+
414+
rect rgb(255, 230, 230)
415+
App->>DB: COMMIT
416+
DB--xApp: ❌ エラー / タイムアウト
417+
end
418+
419+
420+
Queue->>Worker: メッセージ受信
421+
activate Worker
422+
Worker->>DB: データ参照 (SELECT)
423+
DB-->>Worker: 0件 (Not Found)
424+
Note right of Worker: ❌ データ不整合<br>メッセージは届いたのにデータがない
425+
deactivate Worker
426+
```
427+
428+
これは、単純に順序を逆転しても解決しない。むしろ、一部処理が成功したと見せかけて後続のコンシューマーが起動しないというメッセージロストが発生する分、事態は悪化しているとも言える。
429+
430+
```mermaid
431+
---
432+
title: 【失敗例】メッセージロスト
433+
---
434+
%%{init: {'sequence': {'mirrorActors': false}}}%%
435+
sequenceDiagram
436+
autonumber
437+
participant App as プロデューサー
438+
participant DB as DB
439+
participant Queue as キュー
440+
participant Worker as コンシューマー
441+
442+
App->>DB: 業務処理
443+
App->>DB: COMMIT
444+
DB-->>App: OK
445+
446+
App->>Queue: Send Message
447+
Queue--xApp: ❌ ネットワークエラー / 障害
448+
449+
Note over Worker: ❌️後続処理が起動しない
450+
```
359451

360452
代表的な対応案にトランザクションアウトボックスパターンがある。
361453

@@ -364,20 +456,20 @@ SQSを利用する場合の重複排除(Exactly once)の仕組みには以
364456
1. 各コンシューマーは自身の担う業務ロジック処理と、処理完了を示すステータス更新を1トランザクションで実施する。
365457
2. 後続コンシューマーへのメッセージ連携は、別のトランザクションで行う。
366458

367-
これにより、1→2の順序性の担保と、2単体でのリトライが可能となる。
459+
これにより、1→2の順序性の担保と、2単体でのリトライが可能となる。
368460
:::
369461

370-
トランザクションアウトボックスパターンと、1トランザクション内ですべてを行う直接ディスパッチ処理方式を下表で比較する
462+
ファントムメッセージが発生する直接ディスパッチ方式と、トランザクションアウトボックスパターンのポーリング版、CDC版を下表で比較する
371463

372-
| \# | (1)直接ディスパッチ | (2)アウトボックス (ポーリング・リレー) | (3)アウトボックス (CDC・リレー) |
373-
| :----------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
374-
|| ![アプリ上でDBコミットとキューへメッセージ送信](images/direct_dispatch.drawio.png) | ![別プロセスがDBポーリングしてキューにメッセージ送信](images/outbox_polling.drawio.png) | ![CDC経由でLambdaを起動しキューへメッセージ送信](images/outbox_eventdriven.drawio.png) |
375-
| 処理概要 | メッセージ送信 → DBコミット | DBコミット → (別プロセス) → ポーリング → メッセージ送信 | DBコミット → (CDC) → イベント → メッセージ送信 |
376-
| 信頼性 | |||
377-
| クラッシュ時の主なリスク |メッセージは送信されたが、DB更新がロールバックされるファントムメッセージの懸念 | ✅ リレーの送信失敗時は、次回のポーリングで自動リトライされる | ✅ リレーのリトライCDCがトリガーしたLambda等のリトライ機構で処理される |
378-
| 実装コスト | ✅ 低 | ⚠️中 ポーリングバッチの実装/運用が必要 | ❌ 高 CDCパイプライン(Debezium/DMS等)の構築/運用が必要 |
379-
| レイテンシ | ✅ 低 | ⚠️ 中~高(ポーリング間隔に依存) | ✅ 低 |
380-
| DB負荷 | ✅ 低 | ⚠️ 中(定期的なポーリングスキャンが発生) | ✅ 低(トランザクションログベース) |
464+
| \# | (1)直接ディスパッチ | (2)アウトボックス (ポーリング・リレー) | (3)アウトボックス (CDC・リレー) |
465+
| :----------------------- | :------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
466+
|| ![DBとキューへ両方アクセス](images/direct_dispatch.drawio.png) | ![別プロセスがDBポーリングしてキューにメッセージ送信](images/outbox_polling.drawio.png) | ![CDC経由でLambdaを起動しキューへメッセージ送信](images/outbox_eventdriven.drawio.png) |
467+
| 処理概要 | メッセージ送信 → DBコミット | DBコミット → (別プロセス) → ポーリング → メッセージ送信 | DBコミット → (CDC) → イベント → メッセージ送信 |
468+
| 信頼性 ||||
469+
| クラッシュ時の主なリスク |ファントムメッセージの懸念 | ✅ リレーの送信失敗時は、次回のポーリングで自動リトライされる | ✅ リレーのリトライCDCがトリガーしたLambda等のリトライ機構で処理される |
470+
| 実装コスト | ✅ 低 | ⚠️中 ポーリングバッチの実装/運用が必要 | ❌ 高 CDCパイプライン(Debezium/DMS等)の構築/運用が必要 |
471+
| レイテンシ | ✅ 低 | ⚠️ 中~高(ポーリング間隔に依存) | ✅ 低 |
472+
| DB負荷 | ✅ 低 | ⚠️ 中(定期的なポーリングスキャンが発生) | ✅ 低(トランザクションログベース) |
381473

382474
推奨は以下の通り。
383475

@@ -579,9 +671,9 @@ flowchart LR
579671

580672
```mermaid
581673
graph LR
582-
User2(fa:fa-user ユーザー/システム) -- "1.リクエスト" --> API2[Producer]
674+
User2(fa:fa-user ユーザー/システム) -- "1.リクエスト" --> API2[プロデューサー]
583675
API2 -- "2.メッセージ送信" --> Q([キュー]) & DB[(Status DB)]
584-
Q -- "3.処理実行" --> C[Consumer]
676+
Q -- "3.処理実行" --> C[コンシューマー]
585677
C -- "4.DBに書込" --> DB
586678
API2 -. "5.ポーリング" .-> DB
587679
API2 -- "6.処理結果 (同期)" --> User2
5.14 KB
Loading

0 commit comments

Comments
 (0)