Table of contents
1 可靠訊息傳遞問題
喺 distributed systems 既世界裡面,好多時候都會用到 messaging services(message brokers)黎達到 asynchronous 既 microservice communication。
不過,只要有任何 architecture 上既改變,都會衍生一啲新既困難,就好似每加多一樣 infrastructure 或者 middleware 入我地既 architecture,我地既 architecture 裡面就會有多一樣野可以出錯或者需要我地持續 maintain。而當我地用左 messaging services(brokers),就會出現一啲難題,例如如果 producer microservice 喺 update 完佢既 database 之後因為某啲原因而唔能夠成功發送訊息(或者話係個 broker 接收失敗),又或者 consumer microservice 收到訊息之後既處理過程出錯,咁點算呢?我地既 data 會唔會爛左呢?我地希望透過 messaging design patterns 去解決傳遞訊息既時候可能會遇到既突發情況,從而達至可靠既訊息傳遞。
2 訊息傳遞既唔同情況
訊息傳遞可以有幾多種唔同既結果:
- 成功——訊息完全正常,帶有正常既格式、正確既數據,而且 producer、messaging broker、consumer 全部都運作正常。
- 失敗——訊息完全正常,帶有正常既格式、正確既數據,但係 producer、messaging broker、consumer 全部或者部分出錯。
- 失敗——因為訊息本身有問題(稱之為 poison message),例如係帶有 consumer microservice 冇辦法理解既格式或者錯誤既數據。
3 訊息傳遞保證
喺講 design patterns 之前,我地要先思考下訊息傳遞可以有幾多種保證(message delivery guarantees)。
訊息傳遞保證 | 描述 | 解釋 |
---|
Exactly once | 每個訊息一定會被傳遞一次。 | 最理想當然係咁,但當我地考慮到任何野喺任何時候都有低機率會出錯,咁就知道其實實際上係好難(甚至冇可能)實現到呢個保證。 |
At least once | 每個訊息至少會被傳遞一次,亦有可能會被傳遞多過一次。 | 呢個保證係可以接受既次選,但係接收左訊息之後,我地既處理過程一定要係 idempotent,即係同一段 code 執行多過一次既結果都係一樣(主要係 database 裡面既 data)。當遇到有問題既訊息,呢個保證會令到個 MQ 長期塞住。 |
At most once | 每個訊息最多會被傳遞一次,亦有可能冇被傳遞。 | 呢個純粹係 best effort 盡做,所以係一個唔可靠既保證。 |
4 Outbox pattern
Outbox pattern 可以做到 at least once 既訊息傳遞保證,確保最終一定可以發送到訊息。
通常 producer microservices 都唔會只係發送訊息,佢地通常都會先做一輪 database 讀寫,最後先會發送訊息去通知 downstream microservices。因為呢兩個步驟唔係 atomic,如果 database 讀寫成功,但係發送訊息失敗,咁就好可能會令業務出現問題。
其中一個可能性係 messaging service 出現問題(但 network 冇問題),令到呢一刻唔能夠發送訊息,就算有 retry 都未必可以喺短期內成功發送到訊息去 messaging service 度。
因為係 at least once 既訊息傳遞,我地唔可以因為重複處理相同既訊息而令到數據出錯,例如業務上重複扣除客戶戶口既錢。咁所以:
- Consumer microservices 要識得 deduplicate 訊息;
- 或者 consumer microservices 喺接收到訊息之後既處理過程一定要係 idempotent。
註:
- 就算 messaging service 識得 deduplicate 都冇用,因為 consumer microservices 可能會喺向 messaging service 發送確認既時候出錯,咁就會令到 consumer microservices 接收多過一次相同既訊息。
4.1 實現方式
我地需要用到一個 RDBMS database table 去保存需要發送既訊息,並且利用 RDBMS databases 既 ACID 特性,令到 database 讀寫、發送訊息呢兩個步驟變到 atomic。
以下係具體既流程:
- Producer microservice 收到 request。
- Producer microservice 開始一個 database transaction。
- Producer microservice 讀寫 database 去更新一啲業務數據。
- Producer microservice 將需要發送既訊息保存喺 outbox table。
- Producer microservice 完成個 database transaction。
另外亦都需要有一個定期重複執行既 schedule,producer microservice 要定期發送所有未發送既訊息:
- Producer microservice 查詢 outbox table 裡面未發送既 records。
- Producer microservice 發送一個或多個訊息到 messaging service。
- Producer microservice 等 messaging service 確認所有訊息成功發送(publisher acknowledgements/confirms)。
- Producer microservice 開始一個 database transaction。
- Producer microservice soft delete 或者 hard delete outbox table record(s)。
- Producer microservice 完成個 database transaction。
註:
- 視乎情況,發送訊息既時候有可能需要注意 message ordering。
- 如果有任何出錯,一定要 throw exception。
- 如果發送訊息既時候出現 exception,都唔一定需要影響到處理其他 surrogate keys 既 records。
- 如果利用 batch publishing 既方式發送訊息,而唔係逐個訊息咁發送,咁個 messaging service 一係要保證到 atomicity(全部成功或者全部失敗)。
- 如果我地對 message ordering 有要求,而個 messaging service 唔保證 atomicity,咁 consumer microservice 處理既訊息次序就有可能會亂曬。
4.2 所有有可能出錯既情況
我地需要考慮到任何步驟都有出錯既機會,我地注重既係對數據一致性既影響。
註:
- 「出錯」可以係 runtime exception,亦可以係成個 microservice instance 或者 messaging service 重新啟動或者死機。
- 一旦出錯,就會跳過所有後續既步驟。
出錯情況 | 對數據一致性既影響 |
---|
Producer microservice 收到 request 之前出錯。 | ✅ 如果只有一個 instance,咁好可能會影響業務運作,但就對數據一致性冇任何影響。 |
Producer microservice 收到 request 之後出錯。 | ✅ 個 request 固然會失敗,但係因為未寫入過任何數據,所以對數據一致性冇任何影響。 |
Producer microservice 開始一個 database transaction 之後出錯。 | ✅ 個 request 固然會失敗,而 database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Producer microservice 讀寫 database 去更新一啲業務數據之後出錯。 | ✅ 個 request 固然會失敗,但係 database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Producer microservice 將需要發送既訊息保存喺 outbox table 之後出錯。 | ✅ 個 request 固然會失敗,但係 database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Producer microservice 完成個 database transaction 之後出錯。 | ✅ 因為已經完成 database transaction,所有數據已經保存妥當,所以對數據一致性冇影響。但因為個 request 失敗左,upstream microservice 可能會 retry 個 request,只要呢個 producer microservice 既處理過程係 idempotent,咁就唔成問題。 |
至於定期發送訊息既 schedule:
出錯情況 | 對數據一致性既影響 |
---|
Producer microservice 查詢 outbox table 裡面未發送既 records 之前出錯。 | ✅ 因為乜都未做過,所以對數據一致性冇影響。 |
Producer microservice 查詢 outbox table 裡面未發送既 records 之後出錯。 | ✅ 因為乜都未做過,所以對數據一致性冇影響。 |
Producer microservice 發送單一訊息既時候出錯,導致訊息發送唔到。 | ✅ 因為冇發送到訊息,所以對數據一致性冇影響。 |
Producer microservice 發送單一訊息到 messaging service 既時候出錯,導致訊息發送左,但係收唔到 messaging service 既確認。 | ✅ 因為有可能已經成功發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice 發送單一訊息到 messaging service 之後出錯,導致訊息發送左,而又收到 messaging service 既確認,但係之後出錯。 | ✅ 因為已經發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice batch 發送多個訊息到 messaging service 之間出錯。 | ✅ 用得 batch publishing,我地就期望個 messaging service 有 atomicity guarantee,如果失敗既話就會係個 batch 裡面全部訊息發送失敗,個 messaging service 唔會保存呢個 batch 裡面既任何一個訊息。因為等同於冇發送到訊息,所以對數據一致性冇影響。 |
Producer microservice batch 發送多個訊息到 messaging service 既時候出錯,導致訊息發送左,但係收唔到 messaging service 既確認。 | ✅ 因為有可能已經成功發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice batch 發送多個訊息到 messaging service 之後出錯,導致訊息發送左,而又收到 messaging service 既確認,但係之後出錯。 | ✅ 因為已經發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice 開始一個 database transaction 之後出錯。 | ✅ 因為已經發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice soft delete 或者 hard delete outbox table record(s) 之後出錯。 | ✅ 因為已經發送左訊息,但係冇更新到 outbox table,咁之後一定會再次發送相同既訊息,如果個 messaging service 支援 deduplication 又或者個 consumer microservice 識得 deduplicate 或者係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Producer microservice 完成個 database transaction 之後出錯。 | ✅ 因為所有訊息已經成功發送,而且已經完成 database transaction,所有數據已經保存妥當,所以對數據一致性冇影響。 |
5 Inbox pattern
Inbox pattern 雖然有啲似 outbox pattern,但佢就冇 outbox pattern 既必要性。
Outbox pattern 之所以重要,係因為:
- 唔用 outbox pattern 而發送訊息失敗既話,咁可能就咩都冇,需要發送既訊息從此就會漏左;
- 用左 outbox pattern 就可以保證我地想發送既訊息最終都一定會發送到。
如果冇 inbox pattern,都唔係唔得既,只係 consumer microservices 就要喺成功完成所有業務邏輯之後先至可以向 messaging service 確認訊息接收成功(acknowledgement),而確認既過程都有可能會出錯,咁即係無論 producer microservices 發送訊息係 at least once,或者 consumer microservices 接收訊息係 at least once,我地既 consumer microservices 都一定要有 idempotent 既處理過程。
5.1 實現方式
我地需要用到一個 RDBMS database table 去 mark 低未完成以及已經完成處理既訊息,並且利用 RDBMS databases 既 ACID 特性,令到完成處理訊息、database 讀寫呢兩個步驟變到 atomic。
以下係具體既流程:
- Consumer microservice 訂閱 messaging service。
- Consumer microservice 接收到新訊息。
- Consumer microservice 開始一個 database transaction。
- Consumer microservice 將需要處理既訊息保存喺 inbox table。
- Consumer microservice 完成個 database transaction。
- Consumer microservice 向 messaging service 確認訊息接收(consumer acknowledgements)。
另外亦都需要有一個定期重複執行既 schedule,consumer microservice 要定期處理所有接收左既訊息:
- Consumer microservice 查詢 inbox table 未處理既 records。
- Consumer microservice 開始一個 database transaction。
- Consumer microservice soft delete 或者 hard delete inbox table record(s)。
- Consumer microservice 讀寫 database 去更新一啲業務數據。
- Consumer microservice 完成個 database transaction。
註:
- 如果想利用個 inbox table 幫我地 deduplicate 重複接收既相同訊息,咁我地處理既時候就唔可以 hard delete inbox table records,因為成個 record 刪左就會冇辦法知道邊啲係已經接收過。
5.2 所有有可能出錯既情況
我地需要考慮到任何步驟都有出錯既機會,我地注重既係對數據一致性既影響。
註:
- 「出錯」可以係 runtime exception,亦可以係成個 microservice instance 或者 messaging service 重新啟動或者死機。
- 一旦出錯,就會跳過所有後續既步驟。
出錯情況 | 對數據一致性既影響 |
---|
Consumer microservice 訂閱 messaging service 之前出錯。 | ✅ 如果只有一個 instance,咁好可能會影響業務運作,但就對數據一致性冇任何影響。 |
Consumer microservice 訂閱 messaging service 之後出錯。 | ✅ 如果只有一個 instance,咁好可能會影響業務運作,但就對數據一致性冇任何影響。 |
Consumer microservice 接收到新訊息之後出錯。 | ✅ 因為已經接收左訊息,但係冇更新到 inbox table,咁之後一定會再次接收相同既訊息,如果個 consumer microservice 係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Consumer microservice 開始一個 database transaction 之後出錯。 | ✅ 因為 database 會自動 rollback transaction,咁之後一定會再次接收相同既訊息,如果個 consumer microservice 係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Consumer microservice 將需要處理既訊息保存喺 inbox table 之後出錯。 | ✅ 因為已經接收左訊息,而 database 會自動 rollback transaction,咁之後一定會再次接收相同既訊息,如果個 consumer microservice 係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Consumer microservice 完成個 database transaction 之後出錯,冇向 messaging service 確認訊息接收。 | ✅ 因為已經完成 database transaction,但係冇向 messaging service 確認到訊息接收,咁之後一定會再次接收相同既訊息,如果個 consumer microservice 係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Consumer microservice 向 messaging service 確認訊息接收既時候出錯。 | ✅ 因為已經完成 database transaction,但係有可能冇確認到訊息接收,咁之後可能會再次接收相同既訊息,如果個 inbox table 識得 deduplicate 或者 consumer microservice 係 idempotent 既處理過程就冇問題,所以對數據一致性冇影響。 |
Consumer microservice 向 messaging service 確認訊息接收之後出錯。 | ✅ 因為已經完成 database transaction,所有數據已經保存妥當,而且已經確認接收,所以之後唔會接收重複既訊息,亦對數據一致性冇影響。 |
至於定期處理訊息既 schedule:
出錯情況 | 對數據一致性既影響 |
---|
Consumer microservice 查詢 inbox table 未處理既 records 之前出錯。 | ✅ 因為乜都未做過,所以對數據一致性冇影響。 |
Consumer microservice 查詢 inbox table 未處理既 records 之後出錯。 | ✅ 因為乜都未做過,所以對數據一致性冇影響。 |
Consumer microservice 開始一個 database transaction 之後出錯。 | ✅ Database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Consumer microservice soft delete 或者 hard delete inbox table record(s) 之後出錯。 | ✅ Database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Consumer microservice 讀寫 database 去更新一啲業務數據之後出錯。 | ✅ Database 會自動 rollback transaction,最終係冇任何數據上既改動,亦對數據一致性冇影響。 |
Consumer microservice 完成個 database transaction 之後出錯。 | ✅ 因為已經完成 database transaction,所有數據已經保存妥當,所以對數據一致性冇影響。 |
6 考慮事項
- 我地既 microservices 既處理過程需要做到 idempotent。如果做唔到,數據就有機會因為各種 retry 機制而終有一日出錯。
- 考慮到 consumer microservice 可能會重複收到相同既訊息,如果佢既處理過程冇辦法做到 idempotent,咁就要考慮從個 inbox table 入手。
- Upstream microservice 發送畀 producer microservice 既 HTTP request 既 header 可以加入一個隨機既 UUID。而次選既方法係 producer microservice 根據 HTTP request body 某啲數據去計算一個 hash,但係會有機會誤判。
- Producer microservice 可以用呢個 value 作為訊息既 ID。
- Consumer microservice 既 inbox table 要有一個
message_id
既 column,然後整一個 unique index 落呢個 column 度。
- Consumer microservice 收到訊息之後,會將佢放落 inbox table,如果已經存在相同訊息 ID 既 record,就會出現 unique index violation 既 exception,但係 consumer microservice 需要 ignore 呢個 exception,照樣向 messaging service 確認已經成功接收訊息,否則下次又會重新收到相同既訊息。
- Consumer microservice 成功處理完訊息之後,只可以 soft delete inbox table 既 records,因為 hard delete 既話就會失去 unique index 幫我地 deduplicate 訊息既作用。
- 如果用左 outbox pattern 以及 inbox pattern,仲有冇需要用 messaging service 收發訊息黎取代 HTTP requests?
- 我地的確可以用返 HTTP requests,因為有 database 既 inbox table 裝住未處理既數據,咁就一樣可以做到 asynchronous processing。
- 不過,HTTP requests 只能做到單一 consumer instance;相反,messaging service 可以畀我地做到 consumer groups,同一個 message 被多個 consumers 處理。
- 另外,如果我地想用 horizontal autoscaling 去維持多個 consumer microservice instances 去達到 high availability,而如果我地一定要維持返 message ordering,咁用 HTTP requests 既話就冇辦法指定邊一個 consumer microservice instance,咁就冇辦法確保到 requests 一定係順序完成處理;相反,如果我地用 RabbitMQ 既話可以用 single active consumer,或者 Kafka 既話用 partition,確保只會有一個 consumer microservice instance 接收到訊息,咁訊息接收次序就會同訊息發送次序一樣。
7 參考資料
- Microservices 101: Transactional Outbox and Inbox
-
Very often, the recipient service can cope without the inbox. If the task doesn’t take long to finish or completes a predictable amount of time, it can just ack the message after processing. Otherwise, it might be worthwhile to spend some effort to implement the pattern.
- Outbox, Inbox patterns and delivery guarantees explained
- AWS Prescriptive Guidance - Transactional outbox pattern
- Handling Failure Successfully in RabbitMQ
- Microservice architecture [Outbox pattern]
- Every Programmer Should Know #1: Idempotency
Simply, it is possible to make a POST request idempotent by including a unique identifier in the request body or header, which can be used to identify and prevent duplicate requests.
Many approaches can be used to determine whether a request is a copy of an earlier request. For example, it may be possible to derive a synthetic token based on the parameters in the request. You can derive a hash of existing parameters and assume that any request with the same parameters from the same caller is a duplicate. On the surface, this seems to simplify both the customer experience and service implementation. Any request that looks exactly like a previous request is considered a duplicate request. However, this approach is unlikely to work in all situations. For example, let's say you order a meal, and when your next-door neighbor orders the same meal, are these requests repeated or are they two different requests? Or after you place an order, your friend calls and says he's hungry, and when you re-create the same order a short time later, will we treat them as renewed requests? Is this scenario very similar to the client retrying the service because of the network latency we just mentioned? It’s possible that the caller actually wants two identical meals.
The generally preferred approach is to include a unique caller-supplied client request identifier in the API contract. Requests from the same caller with the same customer request identifier can be considered duplicate requests and handled accordingly. A unique caller-supplied client request identifier for idempotent operations satisfies this need.