kiến thức [Event Tặng Title] Microservice Pattern

Akka biết từ rất lâu, chắc phải từ 2012 2013 gì đó (hồi mà cty còn gọi là Typesafe, chính là Lightbend sau này) mà chưa bao giờ có cơ hội xài. Có bác nào dùng rồi thông não nó dùng cho usecase nào không nhỉ :amazed:
 
Netflix làm lâu rồi, trước khi có kubernetes

Akka code khó bỏ mẹ

Cái spring cloud dùng netflix ấy, để deploy ở máy công ty thôi. Maintain khó vãi loàn ra, lên cloud thì dùng service của cloud tốt hơn nhiều chứ

Nhưng việc thím bảo nó outdate và ko thực tế là ko đúng!

Thực tế ng ta vẫn xài. Vấn đề là thím xài cloud thì thím bỏ qua thôi, vì cloud nó có hết cho thím rồi chăng?
Còn nếu thím bảo kỹ thuật cũ thì mời thím chia sẻ kỹ thuật nào để xử lý tình huống này. Mình nói về kỹ thuật nhé.

https://medium.com/@ch.gerkens/circuit-breaker-solution-for-aws-lambda-functions-5264cb59031f

https://aws.amazon.com/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/

Cloud AWS cho thím luôn.

Còn nếu thím bảo là chỉ happy case thì... :go:

https://github.com/gunnargrosch/circuitbreaker-lambda
1636556761888.png
 
Last edited:
Như trong bài mình có đề cập. Heath check qua heartbeat message là 1 giải pháp. Nó thường xác định là server có hoạt động hay ko. Còn để check nội dung trả về thì fake transaction nhé.

Ngoài ra, monitoring/alerting các tham số như CPU/Ram/Disk cũng là một việc quan trọng cần làm.

Bản chất cái heartbeat message cũng rất phức tạp. Ví dụ bạn gửi 100 cái heartbeat message, có thể dựa vào thời gian gửi và nhận request để tính toán và xác định trạng thái của server.
Việc đặt timeout chỉ là 1 giải pháp khá là "naive" thôi.

Có một thuật toán cho cái này, healhcheck liên tục có thể làm stress hệ thống. Thuật toán mới đại khai là dựa trên thơi gian đợi khi gọi lần check kế tiếp, vị dụ gưi health check 1k lần tính trung vị, độ lệch chuẩn gì đó, rồi đoán xem thằng server có vấn đề hay ko. Để tý cf về tớ gửi tên.:D

Sent using vozFApp
 
Có một thuật toán cho cái này, healhcheck liên tục có thể làm stress hệ thống. Thuật toán mới đại khai là dựa trên thơi gian đợi khi gọi lần check kế tiếp, vị dụ gưi health check 1k lần tính trung vị, độ lệch chuẩn gì đó, rồi đoán xem thằng server có vấn đề hay ko. Để tý cf về tớ gửi tên.:D

Sent using vozFApp

Cái đấy để xác định trạng thái của server. Akka cũng đang xài thuật toán đó :) (nếu cái mình nghĩ giống cái thím nói, nhưng ko phải trung vị)
Nhưng nếu muốn check nội dung trả về thì vẫn phải fake trấnction.
Còn sau khi check xong thì vẫn là CB xử lý đóng hay mở thôi.
 
Last edited:
zFNuZTA.png
lý thuyết rất hay nhưng còn thiếu nhiều thứ lắm. Còn khi triển khai thực tế thì tôi vote các anh dùng FW nhé. Ai xài node thì tui xin khuyến nghị thằng này moleculer
 
Cái đấy để xác định trạng thái của server. Akka cũng đang xài thuật toán đó :) (nếu cái mình nghĩ giống cái thím nói, nhưng ko phải trung vị)
Nhưng nếu muốn check nội dung trả về thì vẫn phải fake trấnction.
Còn sau khi check xong thì vẫn là CB xử lý đóng hay mở thôi.
Thuật toán tớ nói là cái này :D
Phi Accrual Failure Detection

Background#

In distributed systems, accurately detecting failures is a hard problem to solve, as we cannot say with 100% surety if a system is genuinely down or is just very slow in responding due to heavy load, network congestion, etc. Conventional failure detection mechanisms like Heartbeating outputs a boolean value telling us if the system is alive or not; there is no middle ground. Heartbeating uses a fixed timeout, and if there is no heartbeat from a server, the system, after the timeout assumes that the server has crashed. Here, the value of the timeout is critical. If we keep the timeout short, the system will detect failures quickly but with many false positives due to slow machines or faulty network. On the other hand, if we keep the timeout long, the false positives will be reduced, but the system will not perform efficiently for being slow in detecting failures.

Definition#

Use adaptive failure detection algorithm as described by Phi Accrual Failure Detector. Accrual means accumulation or the act of accumulating over time. This algorithm uses historical heartbeat information to make the threshold adaptive. Instead of telling if the server is alive or not, a generic Accrual Failure Detector outputs the suspicion level about a server. A higher suspicion level means there are higher chances that the server is down.

Solution#

With Phi Accrual Failure Detector, if a node does not respond, its suspicion level is increased and could be declared dead later. As a node’s suspicion level increases, the system can gradually stop sending new requests to it. Phi Accrual Failure Detector makes a distributed system efficient as it takes into account fluctuations in the network environment and other intermittent server issues before declaring a system completely dead.

Examples#

Cassandra uses the Phi Accrual Failure Detector algorithm to determine the state of the nodes in the cluster.
 
Hóng SAGA. Đọc thì cũng hiểu tương đối mà không có ví dụ nên chưa rõ implement như nào. Search toàn ra hẳn framework, mấy language ko có làm em hơi confused :sweat:

via theNEXTvoz for iPhone
 
Lý thuyết có vẻ cũ và ko thực tế, mấy cái cb với timeout kia có tác dụng gì. Stateless thì ko nói

Bên khách của tôi thì spring boot app deploy trên kubenetes của aws, nhiều service chia theo nhiều namespace

Mỗi service 1 db riêng. Giao tiếp qua activemq, hoặc rest api, chủ yếu activemq

Front end là angular deploy trên cloud front

Tích hợp okta, và đủ thứ của aws

//micro services mà ko có khả năng deploy trên cloud thì vất
bên mình cũng triển khai khá giống như này, khác cái là dùng kafka chứ ko phải activeMQ. Cái này là event driven architecture rồi, 1 dạng giao tiếp asynchronous . Circuit breaker là cho giao tiếp synchronous thôi.
 
Hóng SAGA. Đọc thì cũng hiểu tương đối mà không có ví dụ nên chưa rõ implement như nào. Search toàn ra hẳn framework, mấy language ko có làm em hơi confused :sweat:

via theNEXTvoz for iPhone
Em cũng giống bác. Đi pv thì chém gió saga các thứ nhưng chỉ là lý thuyết xuông chứ chưa được làm thực tế bao giờ
 
Lý thuyết Saga thì đầy rồi ko bàn thêm nữa, còn nếu bạn nào muốn thìm hiểu Saga một cách thực dụng - "Saga in practice" thì nên tìm hiểu workflow, stateful function trước. Đó là những cách người ta impl Saga trong thực tế :big_smile:
 
SAGA có lẽ là một trong những pattern phổ biến nhất trong mảng distributed transaction. SAGA dc giới thiệu lần đầuu vào năm 1987 (Hector Garcia-Molina and Kenneth Salem, “Sagas,” ACM Sigmod Record 16, no. 3 (1987): 249–59.)

Một saga sẽ có nhiều transaction nhỏ, mỗi transaction thực thi tại một local microservice. Khi transaction đầu tiên bắt đầu thực thi, nếu thành công sẽ trigger transaction kế tiếp, và cứ tiếp tục như thế cho tới cuối cùng. Nếu trong quá trình đó có 1 transaction thất bại, nó sẽ trigger một hành động "compensation" (đền bù) cho các transaction trước đó. Có 2 cách để cài đặt là "pattern-events-based choreography" và "orchestration thông qua một service điều phối."

The are two main ways to implement the pattern—events-based choreography and orchestration via a coordinator service.

ACID
Chúng ta đều đã nghe qua và biết tới ACID khi nhắc tới DB transaction, nên mình sẽ ko nói thêm về 4 yếu tố này. Không phải tất cả các DB đều support ACID.
Nếu chỉ có 1 một "single microservice" và một single db, thì mọi chuyện đơn giản hơn. Giả sử ta có 1 transaction update data trong 2 table của single db, nếu 1 lệnh update bị fail thì lệnh update thứ 2 sẽ không được thực hiện.

Tuy nhiên sẽ phức tạp hơn nếu ta có 2 microservice, 2 database, và ta có 2 transaction cần udpate data của 2 table nằm ở 2 db khác nhau. Nếu 1 update ở một table bị fail thì update ở table kia sẽ không được thực hiện.

Ví dụ như trong hình sau: (mình lấy hình từ sách cho đỡ tốn công).
1641401817157.png

Ta có 2 microservice, và thực hiện thay đổi lên 2 table Customer và PendingEnrollment. 2 transaction trong hình sẽ thực hiện độc lập với nhau và có thể bị fail mà ko phụ thuộc vào transaction còn lại.
Giả sử thứ tự thực hiện như sau:

Thực thi transaction A
Nếu thành công -> thực thi transaction B
Nhưng nếu B fail thì sao?
Lưu ý là 2 transaction này thực hiện độc lập với nhau trên 2 DB khác nhau!

Việc thiếu tính atomicity này sẽ dẫn tới rất nhiều vấn đề, đặc biệt là khi migrate các hệ thống phụ thuộc vào yếu tố này.

Chúng ta sẽ thử bắt đầu với việc xem xét thuật toán 2-phase commit (2PC) để thực hiện các distributed transaction.

2PC
Thường được sử dụng để thực hiện các giao dịch thay đổi trong 1 hệ thống phân tán, nhiều quy trình riêng biệt sẽ được cập nhật như là một phần của hoạt động tổng thể. Tuy nhiên 2PC có thể ko giải quyết dc vấn đề và mang tới nhiều rắc rối hơn.

2PC gồm 2 phần : Voting phase commit phase.
- Voting phase: Một central coordination (trung tâm giao tiếp) sẽ liên lạc với các worker tham gia vào transaction, và yêu cầu xác nhận thông tin có thực hiện thay đổi hay không.
Ví dụ trong hình sau:
1641401868087.png

Chúng ta thấy có 2 yêu cầu thực hiện thay đổi trên 2 table, nếu cả 2 request này đều dc cho phép thì sẽ thực hiện phase tiếp theo. Nếu không, có 1 worker không cho phép thực hiện request, thì toàn bộ quá trình sẽ bị chặn lại. Ta có thể thấy worker không thực hiện thay đổi ngay lập tức mà đảm bảo là có thể thực hiện thay đổi đó trong tương lai. Vì thế để tránh các can thiệp vào row đó (ví dụ có 1 thao tác bên ngoài xóa đi row đó hay update lại data chẳng hạn) thì worker phải thực hiện "lock" lại các row trên 2 table. Do đó, sau quá trình thì release lock là việc cần làm.

- Commit phase:
Tại bước này, các thay đổi sẽ được thực hiện, sau đó release lock. Một điều cần lưu ý là các commit này sẽ không được thực hiện cùng 1 thời điểm, do message sẽ được gửi đi từ Coordinate vào thời gian khác nhau. Có nghĩa là có thể ta sẽ thấy thay đổi trên worker A, nhưng trên worker B thì chưa. Khoảng latency giữa Coordinate và các bên khác càng lớn, các worker thực hiện càng trễ, thì ta sẽ thấy data không nhất quán (inconsistency).
--> mất đi tính chất isolation.

Ngoài ra vấn đề về lock cũng là một vấn đề khó khăn khác.
Giả sử có 1 host bị tạch do bất kỳ 1 lý do nào đó (mất điện chẳng hạn). Worker đã không phản hồi ở bước commit. Vậy ta cần làm gì? Fix tay?
Việc có càng nhiều bên tham gia, thay vì 2 worker mà là nhiều worker hơn sẽ càng khiến cho latency lớn hơn, đặc biệt là khi cần lock nhiều dữ liệu. Do đó 2PC thường chỉ thích hợp với các thao tác cần ít thời gian, vì thao tác của bạn càng nhiều thì thời gian bạn lock lại resource càng lớn.

Vì các lý do đó ta nên tránh sử dụng distributed transacion như 2PC để giải quyết bài toán này.

Một giải pháp khác đó chính là không chia nhỏ data ra. Nếu bạn có những thông tin gắn kết với nhau, và bạn vẫn muốn đảm bảo tính atomic + consistency, ACID transaction, thì tốt nhất nên để chúng trong 1 DB và được quản lý bởi 1 single service.
Nhưng nếu bạn vẫn muốn phải tách nó ra để quản lý bởi các microservices, thì đây là lúc cần tới... SAGA.

(Đối với Distributed Transaction các bạn có thể tham khảo thêm Spanner, 1 NewSQL với khả năng scale nhưng vẫn đảm bảo ACID, tất nhiên cũng sẽ có những trade-off đi kèm, mình sẽ giới thiệu ở các bài sau).

SAGA

Không như 2PC, Saga dc thiết kế để tránh việc locking resource trong 1 thời gian dài, "how to handle operations known as long lived transactions (LLTs)".
Một single DB cũng có thể gặp tình huống LLT khi lock 1 phần lớn (hoặc toàn bộ) db trong 1 thời gian dài -> lock các hoạt động khác liên quan.
Ý tưởng chính của tác giả là chia nhỏ transaction ra thành 1 chuỗi các transaction có thể được handle một cách độc lập. Các sub-transaction này sẽ nhỏ hơn, thực thi trong thời gian ngắn hơn, và chỉ tác động lên 1 phần dữ liệu -> qua đó giảm đi tác động của việc lock lên data trong 1 thời gian dài.

Saga ban đầu được coi là cơ chế giúp các LLT hoạt động trên 1 DB duy nhất, thì mô hình này cũng có thể áp dụng để điều phối hoạt động trên microservices. Chúng ta sẽ chia một "single business operation" thành một tập hợp các lệnh sẽ thực hiện lên các service.

Lưu ý: Saga ko cung cấp tính atomicity như các DB thông thường. Saga cung cấp cho ta đủ thông tin để suy luận về các trạng thái của dữ liệu, qua đó handle bài toán này.

Mấu chốt của bài toán là làm sao handle failure của 1 transaction độc lập. Hình dung luồng hoạt động là Worker A -> thực hiện transacion A lên DB A, sau đó nếu thành công tới Worker B -> thực hiện transaction B lên DB B. Cứ như thế tới lúc kết thúc, vậy nếu có 1 transaction ở giữa bị fail thì sao? Làm sao để recovery?
bản thân paper gốc của Saga mô tả 2 cách: backward recovery và forward recovery.

  • Backward recovery: reverting failure và rollback các transactiont trước đó. Để làm dc việc này, ta cần phải định nghĩa các thao tác để undo các transaction đã commit trước đó.
  • Forward recovery: Ta bắt đầu từ điểm thất bại và thực hiện retry -> hệ thống cần đủ thông tin để cho phép thực hiện retry.

Tùy thuộc vào business mà bạn có thể chọn 1 trong 2 hay cả 2 phương pháp trên.
(Một lưu ý khác là saga cho phép ta recover từ business fail chứ ko phải technical fail, saga hoạt động dựa trên giả định là ta đang tương tác với các reliable component.)

Saga rollback

Với 1 ACID transaction, giả sử ta gặp 1 problem, ta có thể trigger rollback trước khi thực hiện commit. Sau khi rollback mọi thứ trở về như cũ. Với saga chúng ta có nhiều transaction tham gia, một trong số chúng đã được commit trước khi ta quyết định rollback lại toàn bộ thao tác. Vậy câu hỏi đặt ra là, làm sao ta có thể rollback transaction khi mà nó đã commit? Các transaction này lại có thể dc quản lý bởi các service khác nhau, các DB khác nhau.

Lúc này bạn cần implement một "compensating transaction" (giao dịch bù), một hoạt động hoàn tác transaction trước đó. Để khôi phục quy trình, ta cần kích hoạt (trigger) các giao dịch bù cho từng transaction đã commit.

Ví dụ với quy trình:
Worker A -> Transaction A -> DB A (Commited) ==> Compen transaction
Worker B -> Transaction B -> DB B (Commited) ==> Compen transaction
Worker C -> Transaction C -> DB C (Commited) ==> Compen transaction
Worker D -> Transaction D -> DB D (Failed)

Tuy nhiên ko thể thao tác nào cũng thực hiện được (ví dụ gửi email cho khách, ta ko thể unsend email!)
Nhưng ta có thể gửi 1 email thứ 2 để báo với khách hàng thông tin.
Vì ta ko thể revert 1 cách hoàn toàn nên ta gọi đây là semantic rollbacks.

Một cách khác để giảm rollback, ta có thể thực hiện reorder workflow.

Ta move lại thứ tự như sau
Worker A -> Transaction A -> DB A (Commited) ->
Worker C -> Transaction C -> DB C (Commited) -> if fail, roll back the whole saga
Worker D -> Transaction D -> DB D (Commited) ->
Worker B -> Transaction B -> DB B (Commited)

Việc này giúp ta ko phải lo về rollback, nếu gặp sự cố ở Worker B hay D. Bằng việc đẩy các transaction có khả năng fail về phía trước (Worker C), ta tránh dc việc phải trigger compensating transaction.

Mixing fail-backward and fail-forward situations

Ta có thể kết hợp nhiều phương án với nhau. Ví dụ với 1 hệ thống thương mại điện tử, sau khi người dùng thanh toán và đi tới khâu cuối cùng là vận chuyển. Vì một lý do nào đó việc vận chuyển bị thất bại (social distancing), ta có thể thử lại vào hôm sau. Nếu vẫn ko dc ta có thể yêu cầu sự can thiệp của con người.


Implementing Sagas

Rồi bây giờ sẽ tới phần quan trọng là implement saga. Như nói ngay từ đầu bài, chúng ta có 2 phương án cơ bản: orchestration và choreography.

Orchestrated sagas

Với phương án này, ta sử dụng 1 trung tâm điều phối, gọi là orchestrator. Trung tâm này sẽ định nghĩa thứ tự thực thi cũng như trigger các giao dịch bù. Nó sẽ điều phối, quản lý làm những việc gì, vào lúc nào một cách rõ ràng.
Ví dụ:
1641401945389.png


Trong hình trên bạn sẽ thấy trung tâm điều phối Order Processor đóng vai trò orchestrator, giao tiếp với các component khác. Nó biết các service khác làm gì và khi nào gọi tới các service đó. Nếu một lệnh call thất bại, nó sẽ quyết định phải làm gì tiếp theo. Một cách tổng quát, mô hình này sẽ cần một lượng lớn giao tiếp theo mô hình request-response giữa các service. Order Processor gửi request tới một service và chờ đợi 1 response để biết là request đã được thực hiện thành công và nhận về kết quả.

Về mặt bản chất, đây là phương án tiếp cận couple. Order Processor cần phải biết về các service liên quan dẫn tới domain coupling. (Đã giải thích ở các post trước). Domain coupling ko phải là điều gì quá xấu, nhưng ta nên giữ nó ở mức nhỏ nhất có thể. Trong tình huống này Order Processor cần phải beiest và quản lý rất nhiều thứ -> coupling là ko thể tránh khỏi.
Một vấn đề khác là các logic xử lý sẽ bị đưa vào orchestrator nhiều hơn. Khi điều này xả ray, các service sẽ ít đi các behavior của chính nó, chỉ nhận các lệnh từ orchestrator.
(If logic has a place where it can be centralized, it will become centralized!)

Để tránh việc tập trung quá nhiều luồng điều phối này, cần đảm bảo bạn có các service khác nhau đóng vai trò orchestrator cho các luồng khác nhau.

Choreographed sagas

Ví dụ:
1641401967442.png


Các microservice sẽ react với các event nhận được. Các event sẽ được phát broadcast trong toàn hệ thống và được nhận bởi các service quan tâm tới kiểu event đó (pub - sub). Bạn không gửi event tới 1 microservice, mà phát nó đi trong hệ thống, các microservice đã đăng ký nhận event sẽ nhận được nó và xử lý tiếp theo.
Ví dụ serice WareHouse khi nhận được event "Order Place", nó sẽ xử lý và kích hoạt 1 event khác tiếp theo. Nếu không thể xử lý (hết hàng chẳng hạn), nó sẽ raise lên 1 event khác để chặn lại việc đặt hàng.

Điều này cũng cho phép các service xử lý song song với nhau, ví dụ sau khi service Payment đã thực hiện phần thanh toán xong và bắn event Payment Taken, các service Warehouse và Loyalty có thể thực hiện cùng lúc.
Quá trình này cần một message broker để quản lý phần broadcast và delivery event 1 cách hiệu quả, nhiều service có thể cùng react với 1 event (subscriber cùng topic).

Với phương án này, các service sẽ không cần phải biết về các service khác hoạt động như thế nào. Chúng chỉ cần biết là chúng sẽ nhận event gì và xử lý tiếp theo như thế nào, qua đó giảm bớt domain coupling.

Một điểm quan trọng là cần 1 unique id cho saga (correlation id) và đặt nó vào trong các event. Qua đó giúp các service kiểm soát và xử lý logic đúng với từng event nhận được.

Mixing styles

Bạn có thể kết hợp cả 2 phương án lại với nhau.
Để làm được việc này, bạn cần hiểu rõ về các trạng thái, các hoạt động nào sẽ xảy ra như là 1 phần của saga để việc recovery từ failure dễ xử lý hơn.

Tham khảo:
- Sách: Building Microservice 2nd
 
SAGA có lẽ là một trong những pattern phổ biến nhất trong mảng distributed transaction. SAGA dc giới thiệu lần đầuu vào năm 1987 (Hector Garcia-Molina and Kenneth Salem, “Sagas,” ACM Sigmod Record 16, no. 3 (1987): 249–59.)

Một saga sẽ có nhiều transaction nhỏ, mỗi transaction thực thi tại một local microservice. Khi transaction đầu tiên bắt đầu thực thi, nếu thành công sẽ trigger transaction kế tiếp, và cứ tiếp tục như thế cho tới cuối cùng. Nếu trong quá trình đó có 1 transaction thất bại, nó sẽ trigger một hành động "compensation" (đền bù) cho các transaction trước đó. Có 2 cách để cài đặt là "pattern-events-based choreography" và "orchestration thông qua một service điều phối."

The are two main ways to implement the pattern—events-based choreography and orchestration via a coordinator service.

ACID
Chúng ta đều đã nghe qua và biết tới ACID khi nhắc tới DB transaction, nên mình sẽ ko nói thêm về 4 yếu tố này. Không phải tất cả các DB đều support ACID.
Nếu chỉ có 1 một "single microservice" và một single db, thì mọi chuyện đơn giản hơn. Giả sử ta có 1 transaction update data trong 2 table của single db, nếu 1 lệnh update bị fail thì lệnh update thứ 2 sẽ không được thực hiện.

Tuy nhiên sẽ phức tạp hơn nếu ta có 2 microservice, 2 database, và ta có 2 transaction cần udpate data của 2 table nằm ở 2 db khác nhau. Nếu 1 update ở một table bị fail thì update ở table kia sẽ không được thực hiện.

Ví dụ như trong hình sau: (mình lấy hình từ sách cho đỡ tốn công).
View attachment 960857
Ta có 2 microservice, và thực hiện thay đổi lên 2 table Customer và PendingEnrollment. 2 transaction trong hình sẽ thực hiện độc lập với nhau và có thể bị fail mà ko phụ thuộc vào transaction còn lại.
Giả sử thứ tự thực hiện như sau:

Thực thi transaction A
Nếu thành công -> thực thi transaction B
Nhưng nếu B fail thì sao?
Lưu ý là 2 transaction này thực hiện độc lập với nhau trên 2 DB khác nhau!

Việc thiếu tính atomicity này sẽ dẫn tới rất nhiều vấn đề, đặc biệt là khi migrate các hệ thống phụ thuộc vào yếu tố này.

Chúng ta sẽ thử bắt đầu với việc xem xét thuật toán 2-phase commit (2PC) để thực hiện các distributed transaction.

2PC
Thường được sử dụng để thực hiện các giao dịch thay đổi trong 1 hệ thống phân tán, nhiều quy trình riêng biệt sẽ được cập nhật như là một phần của hoạt động tổng thể. Tuy nhiên 2PC có thể ko giải quyết dc vấn đề và mang tới nhiều rắc rối hơn.

2PC gồm 2 phần : Voting phase commit phase.
- Voting phase: Một central coordination (trung tâm giao tiếp) sẽ liên lạc với các worker tham gia vào transaction, và yêu cầu xác nhận thông tin có thực hiện thay đổi hay không.
Ví dụ trong hình sau:
View attachment 960859
Chúng ta thấy có 2 yêu cầu thực hiện thay đổi trên 2 table, nếu cả 2 request này đều dc cho phép thì sẽ thực hiện phase tiếp theo. Nếu không, có 1 worker không cho phép thực hiện request, thì toàn bộ quá trình sẽ bị chặn lại. Ta có thể thấy worker không thực hiện thay đổi ngay lập tức mà đảm bảo là có thể thực hiện thay đổi đó trong tương lai. Vì thế để tránh các can thiệp vào row đó (ví dụ có 1 thao tác bên ngoài xóa đi row đó hay update lại data chẳng hạn) thì worker phải thực hiện "lock" lại các row trên 2 table. Do đó, sau quá trình thì release lock là việc cần làm.

- Commit phase:
Tại bước này, các thay đổi sẽ được thực hiện, sau đó release lock. Một điều cần lưu ý là các commit này sẽ không được thực hiện cùng 1 thời điểm, do message sẽ được gửi đi từ Coordinate vào thời gian khác nhau. Có nghĩa là có thể ta sẽ thấy thay đổi trên worker A, nhưng trên worker B thì chưa. Khoảng latency giữa Coordinate và các bên khác càng lớn, các worker thực hiện càng trễ, thì ta sẽ thấy data không nhất quán (inconsistency).
--> mất đi tính chất isolation.

Ngoài ra vấn đề về lock cũng là một vấn đề khó khăn khác.
Giả sử có 1 host bị tạch do bất kỳ 1 lý do nào đó (mất điện chẳng hạn). Worker đã không phản hồi ở bước commit. Vậy ta cần làm gì? Fix tay?
Việc có càng nhiều bên tham gia, thay vì 2 worker mà là nhiều worker hơn sẽ càng khiến cho latency lớn hơn, đặc biệt là khi cần lock nhiều dữ liệu. Do đó 2PC thường chỉ thích hợp với các thao tác cần ít thời gian, vì thao tác của bạn càng nhiều thì thời gian bạn lock lại resource càng lớn.

Vì các lý do đó ta nên tránh sử dụng distributed transacion như 2PC để giải quyết bài toán này.

Một giải pháp khác đó chính là không chia nhỏ data ra. Nếu bạn có những thông tin gắn kết với nhau, và bạn vẫn muốn đảm bảo tính atomic + consistency, ACID transaction, thì tốt nhất nên để chúng trong 1 DB và được quản lý bởi 1 single service.
Nhưng nếu bạn vẫn muốn phải tách nó ra để quản lý bởi các microservices, thì đây là lúc cần tới... SAGA.

(Đối với Distributed Transaction các bạn có thể tham khảo thêm Spanner, 1 NewSQL với khả năng scale nhưng vẫn đảm bảo ACID, tất nhiên cũng sẽ có những trade-off đi kèm, mình sẽ giới thiệu ở các bài sau).

SAGA

Không như 2PC, Saga dc thiết kế để tránh việc locking resource trong 1 thời gian dài, "how to handle operations known as long lived transactions (LLTs)".
Một single DB cũng có thể gặp tình huống LLT khi lock 1 phần lớn (hoặc toàn bộ) db trong 1 thời gian dài -> lock các hoạt động khác liên quan.
Ý tưởng chính của tác giả là chia nhỏ transaction ra thành 1 chuỗi các transaction có thể được handle một cách độc lập. Các sub-transaction này sẽ nhỏ hơn, thực thi trong thời gian ngắn hơn, và chỉ tác động lên 1 phần dữ liệu -> qua đó giảm đi tác động của việc lock lên data trong 1 thời gian dài.

Saga ban đầu được coi là cơ chế giúp các LLT hoạt động trên 1 DB duy nhất, thì mô hình này cũng có thể áp dụng để điều phối hoạt động trên microservices. Chúng ta sẽ chia một "single business operation" thành một tập hợp các lệnh sẽ thực hiện lên các service.

Lưu ý: Saga ko cung cấp tính atomicity như các DB thông thường. Saga cung cấp cho ta đủ thông tin để suy luận về các trạng thái của dữ liệu, qua đó handle bài toán này.

Mấu chốt của bài toán là làm sao handle failure của 1 transaction độc lập. Hình dung luồng hoạt động là Worker A -> thực hiện transacion A lên DB A, sau đó nếu thành công tới Worker B -> thực hiện transaction B lên DB B. Cứ như thế tới lúc kết thúc, vậy nếu có 1 transaction ở giữa bị fail thì sao? Làm sao để recovery?
bản thân paper gốc của Saga mô tả 2 cách: backward recovery và forward recovery.

  • Backward recovery: reverting failure và rollback các transactiont trước đó. Để làm dc việc này, ta cần phải định nghĩa các thao tác để undo các transaction đã commit trước đó.
  • Forward recovery: Ta bắt đầu từ điểm thất bại và thực hiện retry -> hệ thống cần đủ thông tin để cho phép thực hiện retry.

Tùy thuộc vào business mà bạn có thể chọn 1 trong 2 hay cả 2 phương pháp trên.
(Một lưu ý khác là saga cho phép ta recover từ business fail chứ ko phải technical fail, saga hoạt động dựa trên giả định là ta đang tương tác với các reliable component.)

Saga rollback

Với 1 ACID transaction, giả sử ta gặp 1 problem, ta có thể trigger rollback trước khi thực hiện commit. Sau khi rollback mọi thứ trở về như cũ. Với saga chúng ta có nhiều transaction tham gia, một trong số chúng đã được commit trước khi ta quyết định rollback lại toàn bộ thao tác. Vậy câu hỏi đặt ra là, làm sao ta có thể rollback transaction khi mà nó đã commit? Các transaction này lại có thể dc quản lý bởi các service khác nhau, các DB khác nhau.

Lúc này bạn cần implement một "compensating transaction" (giao dịch bù), một hoạt động hoàn tác transaction trước đó. Để khôi phục quy trình, ta cần kích hoạt (trigger) các giao dịch bù cho từng transaction đã commit.

Ví dụ với quy trình:
Worker A -> Transaction A -> DB A (Commited) ==> Compen transaction
Worker B -> Transaction B -> DB B (Commited) ==> Compen transaction
Worker C -> Transaction C -> DB C (Commited) ==> Compen transaction
Worker D -> Transaction D -> DB D (Failed)

Tuy nhiên ko thể thao tác nào cũng thực hiện được (ví dụ gửi email cho khách, ta ko thể unsend email!)
Nhưng ta có thể gửi 1 email thứ 2 để báo với khách hàng thông tin.
Vì ta ko thể revert 1 cách hoàn toàn nên ta gọi đây là semantic rollbacks.

Một cách khác để giảm rollback, ta có thể thực hiện reorder workflow.

Ta move lại thứ tự như sau
Worker A -> Transaction A -> DB A (Commited) ->
Worker C -> Transaction C -> DB C (Commited) -> if fail, roll back the whole saga
Worker D -> Transaction D -> DB D (Commited) ->
Worker B -> Transaction B -> DB B (Commited)

Việc này giúp ta ko phải lo về rollback, nếu gặp sự cố ở Worker B hay D. Bằng việc đẩy các transaction có khả năng fail về phía trước (Worker C), ta tránh dc việc phải trigger compensating transaction.

Mixing fail-backward and fail-forward situations

Ta có thể kết hợp nhiều phương án với nhau. Ví dụ với 1 hệ thống thương mại điện tử, sau khi người dùng thanh toán và đi tới khâu cuối cùng là vận chuyển. Vì một lý do nào đó việc vận chuyển bị thất bại (social distancing), ta có thể thử lại vào hôm sau. Nếu vẫn ko dc ta có thể yêu cầu sự can thiệp của con người.


Implementing Sagas

Rồi bây giờ sẽ tới phần quan trọng là implement saga. Như nói ngay từ đầu bài, chúng ta có 2 phương án cơ bản: orchestration và choreography.

Orchestrated sagas

Với phương án này, ta sử dụng 1 trung tâm điều phối, gọi là orchestrator. Trung tâm này sẽ định nghĩa thứ tự thực thi cũng như trigger các giao dịch bù. Nó sẽ điều phối, quản lý làm những việc gì, vào lúc nào một cách rõ ràng.
Ví dụ:
View attachment 960860

Trong hình trên bạn sẽ thấy trung tâm điều phối Order Processor đóng vai trò orchestrator, giao tiếp với các component khác. Nó biết các service khác làm gì và khi nào gọi tới các service đó. Nếu một lệnh call thất bại, nó sẽ quyết định phải làm gì tiếp theo. Một cách tổng quát, mô hình này sẽ cần một lượng lớn giao tiếp theo mô hình request-response giữa các service. Order Processor gửi request tới một service và chờ đợi 1 response để biết là request đã được thực hiện thành công và nhận về kết quả.

Về mặt bản chất, đây là phương án tiếp cận couple. Order Processor cần phải biết về các service liên quan dẫn tới domain coupling. (Đã giải thích ở các post trước). Domain coupling ko phải là điều gì quá xấu, nhưng ta nên giữ nó ở mức nhỏ nhất có thể. Trong tình huống này Order Processor cần phải beiest và quản lý rất nhiều thứ -> coupling là ko thể tránh khỏi.
Một vấn đề khác là các logic xử lý sẽ bị đưa vào orchestrator nhiều hơn. Khi điều này xả ray, các service sẽ ít đi các behavior của chính nó, chỉ nhận các lệnh từ orchestrator.
(If logic has a place where it can be centralized, it will become centralized!)

Để tránh việc tập trung quá nhiều luồng điều phối này, cần đảm bảo bạn có các service khác nhau đóng vai trò orchestrator cho các luồng khác nhau.

Choreographed sagas

Ví dụ:
View attachment 960861

Các microservice sẽ react với các event nhận được. Các event sẽ được phát broadcast trong toàn hệ thống và được nhận bởi các service quan tâm tới kiểu event đó (pub - sub). Bạn không gửi event tới 1 microservice, mà phát nó đi trong hệ thống, các microservice đã đăng ký nhận event sẽ nhận được nó và xử lý tiếp theo.
Ví dụ serice WareHouse khi nhận được event "Order Place", nó sẽ xử lý và kích hoạt 1 event khác tiếp theo. Nếu không thể xử lý (hết hàng chẳng hạn), nó sẽ raise lên 1 event khác để chặn lại việc đặt hàng.

Điều này cũng cho phép các service xử lý song song với nhau, ví dụ sau khi service Payment đã thực hiện phần thanh toán xong và bắn event Payment Taken, các service Warehouse và Loyalty có thể thực hiện cùng lúc.
Quá trình này cần một message broker để quản lý phần broadcast và delivery event 1 cách hiệu quả, nhiều service có thể cùng react với 1 event (subscriber cùng topic).

Với phương án này, các service sẽ không cần phải biết về các service khác hoạt động như thế nào. Chúng chỉ cần biết là chúng sẽ nhận event gì và xử lý tiếp theo như thế nào, qua đó giảm bớt domain coupling.

Một điểm quan trọng là cần 1 unique id cho saga (correlation id) và đặt nó vào trong các event. Qua đó giúp các service kiểm soát và xử lý logic đúng với từng event nhận được.

Mixing styles

Bạn có thể kết hợp cả 2 phương án lại với nhau.
Để làm được việc này, bạn cần hiểu rõ về các trạng thái, các hoạt động nào sẽ xảy ra như là 1 phần của saga để việc recovery từ failure dễ xử lý hơn.

Tham khảo:
- Sách: Building Microservice 2nd

Cái phương pháp Oches hình như Ochestrator vẫn dùng event message để truyền command message cho các service con dc nhỉ? Do mình từng xem 1 số source thì có đề cập và từng gặp trong cả code base từng làm
https://microservices.io/patterns/data/saga.html
 
Back
Top