Vẽ sơ đồ High Level Design
Xem thửMột sơ đồ nhiều người hiểu khác nhau
Bạn trình bày kiến trúc gdev.link cho nhóm. Sơ đồ có 14 hộp, mũi tên chằng chịt hai chiều, chỗ ghi "Service", chỗ ghi "Event Bus". Người A nghĩ đó là đường đọc, người B nghĩ đó là đường ghi, người C hỏi "mũi tên này là gọi đồng bộ hay gửi tin nhắn?". Mười phút sau cả nhóm đang tranh luận về cái sơ đồ chứ không phải về hệ thống.
Một sơ đồ HLD là công cụ giao tiếp. Tiêu chuẩn để đánh giá nó rất đơn giản: người xem phải lần theo được một request cụ thể từ đầu đến cuối mà không phải đoán.
Nguyên tắc vẽ tốt
- Mỗi sơ đồ một mục tiêu. Vẽ riêng đường đọc (read path), đường ghi (write path) và đường bất đồng bộ (async path). Một sơ đồ chứa tất cả mũi tên sẽ không ai đọc nổi.
- Đặt tên cụ thể.
gdev-link-api,links-db, thay vì "Service 1" và "Database". - Mũi tên phải có nghĩa. Ghi rõ đó là gọi đồng bộ, gửi tin bất đồng bộ hay sao chép dữ liệu. Mũi tên hai chiều không nhãn là mơ hồ.
- Ghi chú NFR cạnh đường quan trọng. Ví dụ "P99 dưới 50 ms" cạnh đường redirect.
- Đánh dấu điểm lỗi duy nhất (single point of failure) nếu còn: thành phần mà hỏng là cả hệ thống hỏng.
Chọn loại sơ đồ theo câu hỏi
| Loại sơ đồ | Trả lời câu hỏi | Khi dùng |
|---|---|---|
| Sơ đồ kiến trúc (flowchart) | Hệ thống gồm những khối nào, nối với nhau ra sao? | Đầu buổi thiết kế, phỏng vấn mở |
| Sơ đồ tuần tự (sequence) | Ai gọi ai, theo thứ tự nào, khi nào đọc cache hay database? | Mô tả một request, cache hit hoặc miss |
| Sơ đồ trạng thái (state) | Một đối tượng chuyển qua các trạng thái nào? | Vòng đời đơn hàng, circuit breaker |
| Sơ đồ thực thể (ER) | Dữ liệu liên quan với nhau thế nào? | Thiết kế database |
Quy tắc ghi nhớ: sơ đồ kiến trúc cho biết "có gì", sơ đồ tuần tự cho biết "xảy ra như thế nào". Người mới thường chỉ vẽ loại đầu và bỏ sót loại sau, trong khi chính thứ tự xảy ra mới lộ ra các vấn đề như cache miss hay lỗi giữa chừng.
Ví dụ: tách đường đọc và đường ghi cho gdev.link
Bài toán: người dùng bấm vào gdev.link/aZ3k9, hệ thống phải chuyển đến link gốc thật nhanh. Người khác tạo link mới bằng cách gửi link dài. Hai việc này có tải và yêu cầu rất khác nhau (đọc nhiều hơn ghi rất nhiều), nên vẽ riêng.
Đường đọc: vòng đời của một request redirect
Cách đọc hình kiến trúc. Có hai đường tách biệt. Đường đọc (chuyển hướng) đi vào Redirect service, hỏi Redis trước (cache hit là đường nhanh nhất) và chỉ khi trượt mới đọc database rồi nạp lại cache. Đường ghi (tạo link) đi vào Create service, ghi database trước, và lấy mã ngắn từ bộ cấp khoảng số để hai instance không tạo trùng mã. Tách hai đường vì tải đọc lớn hơn tải ghi cả trăm lần, nên đường đọc cần cache và mở rộng riêng.
Đang tải sơ đồ…
Đang tải Animation…
Cách đọc sơ đồ
Sơ đồ tuần tự đọc từ trên xuống dưới, mỗi mũi tên là một thông điệp theo thời gian:
- Client gửi
GET /aZ3k9đếngdev-link API. Đây là toàn bộ dữ liệu đầu vào: mã link ngắn. - API hỏi Redis trước, vì Redis nằm trong bộ nhớ nên nhanh hơn database nhiều lần.
- Hai nhánh xảy ra sau đó:
- Cache hit: Redis có sẵn link gốc, trả về ngay. Database không bị chạm đến.
- Cache miss: Redis không có (trả về
nil). API mới truy vấn database, lấy link gốc, rồi ghi lại vào Redis kèm TTL (thời gian sống) để các lần sau không phải hỏi database nữa.
- Cuối cùng API trả về mã
302 Redirectkèm link gốc, trình duyệt tự chuyển đến đó.
Bottleneck: một link cực kỳ phổ biến (ví dụ link được chia sẻ ở một chương trình truyền hình) làm hàng nghìn request cùng chạm một key Redis. Hướng xử lý là thêm bộ nhớ đệm cục bộ trong API hoặc đặt cache ở CDN.
Rủi ro khi lỗi: nếu Redis vừa khởi động lại thì gần như mọi request đều là cache miss cùng lúc, đẩy một lượng truy vấn lớn xuống database. Hiện tượng này (cache stampede) sẽ được học kỹ ở chương Caching. Ở mức nhập môn, bạn chỉ cần nhớ rằng database phải đủ sức chịu tải ngay cả khi cache trống.
Đường ghi: tạo link ngắn
Đang tải sơ đồ…
Đang tải Animation…
Cách đọc sơ đồ
Đường ghi ngắn hơn và đơn giản hơn:
- Client gửi link dài. API sinh mã ngắn rồi ghi vào database trước. Database là nguồn dữ liệu gốc, nên thao tác này phải thành công thì mới tiếp tục.
- Sau khi database báo OK, API có thể ghi luôn vào Redis (gọi là làm nóng cache) để lần bấm đầu tiên không bị cache miss. Bước này là tùy chọn.
- API trả về
201kèm link ngắn.
Thứ tự "database trước, cache sau" có chủ đích: nếu ghi cache trước mà ghi database thất bại thì cache chứa dữ liệu không có thật. Nhưng cũng có kẽ hở: ghi database xong mà ghi cache lỗi thì lần đọc đầu tiên chỉ là cache miss, chậm hơn chứ không sai. Đó là kiểu lỗi ta chấp nhận được.
Trade-off: làm nóng cache khi ghi giảm số lần miss đầu tiên nhưng mỗi lần ghi tốn thêm một thao tác. Với dịch vụ có hàng triệu link ít khi được bấm, làm nóng cache cho tất cả có thể lãng phí bộ nhớ.
Checklist trước khi chốt sơ đồ
- Đã có đủ các tác nhân và ranh giới tin cậy (phần công khai, phần nội bộ) chưa?
- Đã chỉ rõ đâu là đồng bộ, đâu là bất đồng bộ chưa?
- Mỗi loại dữ liệu (metadata, file lớn) có nơi lưu phù hợp chưa?
- Nếu tải tăng 10 lần, điểm nào nghẽn đầu tiên?
- Nếu mất cache, mất một server, mất database chính thì chuyện gì xảy ra?
Ví dụ áp dụng cho gdev.link:
- Tác nhân và ranh giới tin cậy: người dùng và hệ thống khách ở phía công khai, chỉ đi vào qua Load Balancer; Create service, Redirect service, Redis và database nằm ở phía nội bộ.
- Đồng bộ và bất đồng bộ: chuyển hướng và tạo link đều đồng bộ vì người dùng chờ kết quả; sơ đồ này chưa có việc nền nào nên chưa cần hàng đợi.
- Nơi lưu dữ liệu: metadata của link (mã ngắn, URL gốc) trong database, bản sao đọc nhanh trong Redis; không có file lớn nên chưa cần object storage.
- Tải tăng 10 lần: đường đọc nghẽn đầu tiên ở database nếu hit ratio của Redis giảm, nên theo dõi hit ratio và chuẩn bị bản sao đọc.
- Mất cache, mất một server, mất database chính: mất cache thì đọc xuống database chậm hơn; mất một server thì Load Balancer loại nó; mất database chính thì chuyển sang bản dự phòng, và dữ liệu link chỉ còn nếu đã được sao chép.
Những lỗi thường gặp khi vẽ
- Mũi tên hai chiều không ghi hướng dữ liệu, người xem không biết ai gọi ai.
- Vẽ "Event Bus" nhưng không nói ai gửi, ai nhận.
- Quên xác thực và giới hạn tốc độ ở lớp biên.
- Nhồi cả đường đọc, đường ghi và đường nền vào một hình.
- Chỉ vẽ trường hợp thành công. Sơ đồ tuần tự nên có ít nhất một nhánh lỗi hoặc cache miss.
Must Know, Good to Know, Advanced
- Must Know: vẽ đúng loại sơ đồ cho đúng câu hỏi; tách đường đọc và đường ghi; mũi tên có nhãn và có hướng.
- Good to Know: ghi chú NFR cạnh đường quan trọng; đánh dấu điểm lỗi duy nhất.
- Advanced: các chuẩn vẽ như C4 model. Khóa cơ bản chỉ cần biết tồn tại.
Tự kiểm tra
- Vì sao đường đọc của gdev.link nên có hai nhánh (hit và miss) trong cùng một sơ đồ tuần tự?
- Nếu đổi thứ tự ở đường ghi thành "ghi Redis trước, database sau", lỗi nào có thể xảy ra?
- Hãy vẽ sơ đồ tuần tự cho thao tác "người dùng xem lịch sử đơn hàng" của gdev-shop.
Đáp án gợi ý
- Vì đường đọc có hai kết cục khác nhau: cache hit trả lời nhanh từ Redis, cache miss phải xuống database rồi nạp lại cache. Nếu chỉ vẽ nhánh thành công thì người đọc không thấy chi phí và rủi ro của nhánh miss, vốn quyết định độ trễ đuôi và tải lên database. Một sơ đồ chứa cả hai nhánh cho thấy hệ thống thật cư xử thế nào.
- Nếu ghi Redis trước và database sau, có thể xảy ra cache chứa dữ liệu chưa được lưu bền: nếu bước ghi database thất bại hoặc máy chết giữa chừng, người dùng đọc thấy link mà database không có, và link biến mất khi cache hết hạn hoặc bị xóa. Thứ tự an toàn là ghi nơi bền (database) trước rồi mới cập nhật cache.
- Sơ đồ tuần tự gồm các bên: Client, API/App, Cache, Database. Client gửi yêu cầu xem lịch sử, App kiểm tra quyền, hỏi Cache theo khóa của người dùng; nếu hit thì trả luôn, nếu miss thì truy vấn Database lấy danh sách đơn có phân trang, nạp vào Cache với TTL rồi trả kết quả. Thêm một nhánh lỗi: Database chậm hoặc timeout thì App trả lỗi rõ ràng thay vì treo.
Nếu chỉ nhớ 3 điều
- Một sơ đồ tốt cho phép lần theo một request cụ thể mà không phải đoán.
- Flowchart cho biết có gì, sequence diagram cho biết xảy ra theo thứ tự nào. Hãy tách đường đọc, đường ghi và đường nền.
- Luôn vẽ thêm nhánh không hoàn hảo: cache miss, lỗi, hoặc thành phần chết.