[System Design] Giải ngố Eventual Consistency (Nhất quán cuối): Nghệ thuật "Hứa thật nhiều, làm... từ từ" trong Microservices
Chào anh em, lại là mình đây.
Ở bài viết trước về cái luồng thiết kế hệ thống Flash Sale ngàn đô, mình có nhắc đến một từ khóa ở khúc cuối: Eventual Consistency (Nhất quán cuối). Thấy nhiều anh em comment hỏi vụ này quá, nên hôm nay mình quyết định lên luôn một bài mổ xẻ cái concept "ảo ma" nhưng lại là xương sống của toàn bộ các hệ thống lớn hiện nay (từ Shopee, Facebook cho đến Netflix).\
Nếu anh em mới học database, chắc chắn trong đầu luôn bị ám ảnh bởi tính ACID của SQL, đặc biệt là chữ C - Consistency (Tính nhất quán). Nhưng khi bước ra đời thực, ôm mấy hệ thống vài chục ngàn Request/giây (RPS), anh em sẽ nhận ra: Cố chấp giữ cái "Nhất quán" tuyệt đối chỉ khiến server của anh em chầu ông bà sớm.
Vậy Eventual Consistency là cái quái gì? Cùng bóc tách nhé.
- Nỗi ám ảnh mang tên "Strong Consistency" (Nhất quán mạnh)
Hãy nói về tư duy truyền thống trước. Giả sử anh em code app ngân hàng. User A chuyển 100k cho User B. Anh em mở một cái Transaction (Giao dịch):
- Trừ 100k ở tài khoản A.
- Cộng 100k vào tài khoản B.
Commit.
Nếu bước 2 lỗi, anh em Rollback lại bước 1. Tiền của A nguyên vẹn. Đây gọi là Strong Consistency. Mọi dữ liệu phải khớp nhau ở mọi thời điểm, trên mọi máy chủ. Cực kỳ an toàn!
Nhưng cái giá phải trả là gì? Là Tốc Độ và Sự Chờ Đợi. Khi một transaction đang chạy, các bảng/dòng dữ liệu liên quan sẽ bị "Khóa" (Lock). Nếu có 1 triệu người cùng thực hiện giao dịch, chúng nó phải xếp hàng đợi nhau. Hệ thống sẽ chậm như rùa bò, thậm chí sập luôn vì nghẽn cổ chai (bottleneck) ở Database.
2. Sự "Lươn lẹo" của Eventual Consistency (Nhất quán cuối)
Để giải quyết bài toán scale (mở rộng), các pháp sư Big Tech mới ngồi lại với nhau (đẻ ra cái định lý CAP) và nhận ra: "Thực ra trong đa số trường hợp, người dùng không cần dữ liệu phải chính xác ngay-lập-tức từng mili-giây. Chậm một tí cũng chả ai chết".
Và thế là Eventual Consistency ra đời. Nó có nghĩa là: "Nhất quán cuối cùng". Hệ thống không hứa sẽ đồng bộ dữ liệu ngay lập tức, nhưng nó hứa là cuối cùng (eventually) - có thể là vài trăm mili-giây, hoặc vài giây sau - toàn bộ dữ liệu trên các node/service sẽ giống hệt nhau.
Ví dụ thực tế đập thẳng vào mặt anh em hàng ngày:
- Nút Like của Facebook: Anh em thả tim ảnh crush. Màn hình của anh em hiện tim đỏ chót ngay lập tức +1 like (để thao túng tâm lý anh em là app chạy rất mượt). Nhưng thằng bạn ngồi cạnh mở điện thoại nó lên, bài viết đó vẫn chưa tăng like nào. Tầm 2-3 giây sau nó mới nhảy số. Có ai kiện Facebook vì 3 giây lag đó không? Không!
- View Youtube: Đã bao giờ anh em thấy cái video có 301 lượt xem nhưng lại có tới 50.000 lượt like chưa? Youtube không rảnh để cộng View một cách Strong Consistency (vì 1 tỷ người xem cùng lúc thì vỡ mồm DB). Nó ghi nhận tạm trên cache, rồi từ từ đồng bộ xuống DB tổng. Cuối cùng (eventually) thì số view sẽ chuẩn.
3. Eventual Consistency vận hành thế nào trong Microservices?
Trong kiến trúc Microservices, Database thường bị xé lẻ ra (Service nào ôm DB của service nấy). Không thể dùng Transaction của SQL để khóa chéo 2 cái DB nằm ở 2 server khác nhau được.
Lúc này, người ta dùng Message Queue (Kafka, RabbitMQ) làm con thoi để thực hiện Eventual Consistency.
Lấy lại ví dụ Đặt hàng Flash Sale:
- Order Service nhận request đặt hàng -> Tạo đơn trạng thái "Pending" trong DB của nó -> Bắn một event ORDER_CREATED vào Kafka. Trả kết quả ngay cho user: "Đặt thành công, chờ xử lý nha". (User vui vẻ đi khoe bạn bè).
- Inventory Service (Kho) rảnh rỗi nhặt event đó từ Kafka ra -> Trừ số lượng kho.
- Payment Service nhận event -> Trừ tiền ví điện tử.
- Notification Service nhận event -> Gửi email báo thành công.
Ở giây thứ 1, dữ liệu không hề "Nhất quán". Đơn hàng đã tạo nhưng kho chưa trừ, tiền chưa mất. Nhưng đến giây thứ 3, khi các Service đọc xong Queue và xử lý xong, toàn bộ hệ thống đạt trạng thái đồng nhất. Nếu Service Kho đang bị sập? Không sao, event vẫn nằm trong Kafka chờ đó, bao giờ kho reboot lên thì nó đọc tiếp. Không một request nào bị rớt!
4. Mặt trái của sự "Từ từ"
Dùng cái này sướng về mặt Performance, nhưng lại cực kỳ khổ cho đội code vì phải xử lý UX (trải nghiệm người dùng) và các ca "vỡ kế hoạch":
- Vấn đề UX (Đọc dữ liệu bị cũ - Stale Read): User vừa đổi tên avatar xong, F5 lại trang vẫn thấy cái mặt cũ thù lù (do cache chưa kịp đồng bộ).
- Cách giải quyết: Front-end phải fake data (Optimistic UI - tự đổi ảnh trên giao diện trước), hoặc lúc user vừa update thì ép hệ thống đọc trực tiếp từ DB Master thay vì DB Slave.
- Vấn đề "Quay xe" (Compensating Transaction): Ở cái luồng đặt hàng bên trên, lỡ Order tạo rồi, Kho trừ rồi, nhưng sang bước Payment thì thẻ ngân hàng của khách bị từ chối (hết tiền) thì sao? Không thể dùng lệnh Rollback của SQL được nữa. Anh em phải code thêm một cái Event "Bù trừ" (ví dụ PAYMENT_FAILED) ném ngược lại vào Kafka, để thằng Kho nghe thấy thì tự động cộng lại +1 sản phẩm, thằng Order nghe thấy thì đổi trạng thái đơn thành "Canceled". (Anh em có thể tìm hiểu thêm keyword Saga Pattern để giải quyết vụ này nhé).
Chốt hạ
Lựa chọn giữa Strong Consistency và Eventual Consistency là bài toán kinh điển của sự Đánh đổi (Trade-off).
- Nếu anh em làm Core Banking (sổ cái kế toán, chuyển tiền), bắt buộc phải xài Strong Consistency. Chậm cũng phải chờ, lệch 1 đồng là đi tù.
- Nhưng nếu anh em làm Mạng xã hội, E-commerce, Đặt xe... thì Eventual Consistency là "chân lý". Hãy để hệ thống thở, hãy để Message Queue làm việc của nó, và chấp nhận sự sai lệch trong vài giây để đổi lấy khả năng gánh hàng triệu user cùng lúc.
Anh em đọc xong đã thấy cái đầu "nảy số" hơn khi cấu trúc app chưa? Đi phỏng vấn mà chém được vụ xài Message Queue để làm Eventual Consistency thì các sếp chỉ có gật gù rót nước mời vào team thôi. Vote mạnh cho bài để mình lên tiếp các pattern dị hợm khác nhé. Peace!
All Rights Reserved