Monolith và Microservices
Xem thửMột cuộc họp có thể tốn hai quý
Trong đợt khuyến mãi, trang checkout của gdev-shop chậm hẳn. Trong cuộc họp, một kỹ sư đề xuất: "Chúng ta đang là monolith nên mới chậm. Hãy tách thành microservices." Cả phòng gật gù. Trước khi bạn gật theo, hãy xem số liệu đo được lúc đó:
| Chỉ số trong lúc chậm | Giá trị |
|---|---|
| CPU của các instance ứng dụng | khoảng 25% |
| CPU của PostgreSQL | khoảng 96% |
| Truy vấn chậm nhất | 1,8 giây, quét toàn bảng orders |
| Số instance ứng dụng | 3 |
Hãy dự đoán trước khi đọc tiếp: nếu tách order-service ra riêng thì checkout có nhanh hơn không?
Phần bằng chứng nói rằng ứng dụng gần như đang rảnh, còn database đang nghẹt vì một truy vấn thiếu index. Tách service không làm truy vấn nhanh hơn. Nó chỉ thêm một lần gọi qua mạng, thêm một thứ để triển khai, và quả bóng vẫn nằm ở database. Bottleneck thật là một câu truy vấn, và cách chữa là thêm index chứ không phải đổi kiến trúc. Bài học này dạy cách tránh những quyết định đắt tiền như vậy: so sánh theo động lực thực tế, không theo mốt.
Không có lựa chọn tuyệt đối
Monolith và microservices là hai điểm trên một dải, và giữa chúng còn một lựa chọn quan trọng:
- Monolith: một đơn vị triển khai, có thể có hoặc không có ranh giới module rõ ràng.
- Modular monolith: vẫn là một đơn vị triển khai, nhưng bên trong chia thành các module có ranh giới chặt: module chỉ giao tiếp với nhau qua giao diện công khai, không đọc bảng dữ liệu của nhau tùy tiện.
- Microservices: nhiều đơn vị triển khai độc lập, giao tiếp qua mạng.
Modular monolith thường là điểm cân bằng tốt: giữ được sự đơn giản vận hành và giao dịch cục bộ, đồng thời chuẩn bị sẵn ranh giới để về sau tách module thành service khi cần. Phần lớn lợi ích về cấu trúc mà người ta tìm ở microservices thực ra đạt được bằng kỷ luật module, không cần phân tán.
So sánh theo tiêu chí
| Tiêu chí | Monolith (kể cả modular) | Microservices |
|---|---|---|
| Triển khai | Một artifact, một quy trình | Nhiều artifact, mỗi service một pipeline |
| Giao dịch | ACID cục bộ, đơn giản | Saga, outbox, nhất quán sau một khoảng thời gian |
| Mở rộng | Theo cả khối | Theo từng service |
| Cô lập lỗi | Lỗi nặng ảnh hưởng toàn tiến trình | Khoanh vùng được nếu có timeout và dự phòng |
| Gỡ lỗi | Một stack trace, một nơi đọc log | Cần tracing và log tập trung |
| Độ trễ nội bộ | Lời gọi hàm, rất thấp | Lời gọi mạng, cao hơn và có thể lỗi |
| Chi phí nền tảng | Thấp | Cao: CI/CD, giám sát, gateway, quản lý cấu hình |
| Tổ chức nhóm | Hợp với một nhóm hoặc vài nhóm nhỏ | Hợp với nhiều nhóm sở hữu từng service |
| Tốc độ ban đầu | Nhanh | Chậm hơn do phải dựng nền tảng |
Quyết định theo động lực
Đang tải sơ đồ…
Đang tải Animation…
Đang tải Animation…
Cách đọc sơ đồ
Điểm mới của sơ đồ này so với cách nghĩ thông thường là nó bắt đầu bằng đo lường chứ không bắt đầu bằng chọn kiến trúc:
- Đo bottleneck thật (như bảng số liệu đầu bài).
- Hỏi: nỗi đau này có thật sự là vấn đề kiến trúc không? Truy vấn chậm, thiếu cache, thiếu index, N+1 query là vấn đề bên trong hệ thống hiện tại. Sửa ở đó rẻ hơn nhiều.
- Chỉ khi nỗi đau thật sự mang tính kiến trúc (nhóm giẫm chân nhau khi phát hành, một module cần mở rộng khác hẳn, cần cô lập lỗi), mới hỏi tiếp: ranh giới đã ổn định và nền tảng vận hành đã sẵn sàng chưa?
- Chưa sẵn sàng thì giữ modular monolith và quay lại câu hỏi khi áp lực tăng. Sẵn sàng thì tách service đầu tiên, thường là phần có ranh giới rõ nhất và áp lực lớn nhất, không phải tách tất cả cùng lúc.
Quyết định sai tạo ra bottleneck về thời gian kỹ sư: nhiều tháng dựng nền tảng phân tán cho một vấn đề mà một câu CREATE INDEX đã giải quyết.
Tín hiệu thật cho thấy nên tách
- Hai nhóm thường xuyên chờ nhau hoặc xung đột khi phát hành vì dùng chung một artifact.
- Một module có hồ sơ tải rất khác (ví dụ checkout chịu tải gấp 10 lần phần còn lại) và việc nhân bản cả khối tốn kém.
- Một phần cần cô lập lỗi hoặc tuân thủ riêng (ví dụ xử lý thanh toán cần phạm vi bảo mật hẹp).
- Một phần cần công nghệ khác hẳn mà nhóm chấp nhận được chi phí vận hành thêm.
Ngược lại, những lý do không đủ để tách: "ai cũng làm vậy", "để CV đẹp", "để phỏng vấn nói được".
Khi lỗi xảy ra
Microservices thiếu giám sát và tracing biến sự cố thành trò đoán mò: request đi qua sáu service và bạn không biết service nào chậm. Monolith ít gặp tình huống này vì mọi thứ nằm trong một stack trace. Điều này nghĩa là cái giá của microservices phải được trả trước (xây giám sát) chứ không phải sau khi có sự cố.
Hiểu lầm phổ biến
- "Microservices nhanh hơn monolith." Mỗi lời gọi giữa service là lời gọi mạng, chậm hơn lời gọi hàm. Microservices có thể mở rộng tốt hơn ở một số tình huống, nhưng không nhanh hơn theo mặc định.
- "Phải chọn một trong hai." Modular monolith và việc tách từng service khi cần là con đường phổ biến.
- "Tách service sẽ tự sửa code rối." Code rối trong monolith khi tách thành service sẽ thành code rối được nối bằng mạng.
Must Know, Good to Know, Advanced
- Must Know: so sánh theo tiêu chí thực tế; modular monolith là lựa chọn hợp lệ; bắt đầu từ đo lường bottleneck.
- Good to Know: các tín hiệu thật để tách service.
- Advanced: chiến lược tách dần (ví dụ bao vây module cũ bằng service mới, thường gọi là strangler). Chỉ cần biết rằng có cách tách từng bước thay vì viết lại toàn bộ.
Tự kiểm tra
- Với bảng số liệu đầu bài, bạn sẽ đề xuất gì thay cho việc tách service? Số liệu nào khiến bạn tin vào đề xuất đó?
- Nhóm 5 người, ba tháng đầu, sản phẩm chưa có người dùng: kiến trúc nào và vì sao?
- Kể hai tín hiệu thật và hai lý do không đủ để tách service.
Đáp án gợi ý
- Đề xuất: thêm index cho truy vấn quét toàn bảng
orders, rồi đo lại. Số liệu thuyết phục: CPU ứng dụng chỉ khoảng 25% (ứng dụng rảnh) trong khi CPU PostgreSQL khoảng 96% và truy vấn chậm nhất 1,8 giây quét toàn bảng. Nghẽn ở database, tách service không làm truy vấn nhanh hơn mà còn thêm một lần gọi mạng. - Chọn modular monolith: một đơn vị triển khai, chia module có ranh giới rõ. Nhóm 5 người chưa có người dùng cần tốc độ phát triển và sự đơn giản, chưa đủ bằng chứng để trả chi phí nền tảng của microservices. Ranh giới module chuẩn bị sẵn cho việc tách về sau.
- Hai tín hiệu thật: nhiều nhóm giẫm chân nhau khi phát hành; một module cần mở rộng hoặc cô lập lỗi hoàn toàn khác phần còn lại. Hai lý do không đủ: "monolith nghe cũ" hoặc xu hướng công nghệ; hoặc "đội khác dùng microservices". Lý do tách phải là áp lực đã đo được, không phải cảm tính.
Nếu chỉ nhớ 3 điều
- Chọn kiến trúc sau khi đo bottleneck thật: nhiều vấn đề chậm là truy vấn hay cache, không phải kiến trúc.
- Modular monolith giữ được cấu trúc tốt mà không phải trả chi phí phân tán.
- Chỉ tách service khi có áp lực thật về tổ chức, tải hoặc cô lập lỗi, và nền tảng vận hành đã sẵn sàng.