Nền tảng Thiết kế Hệ thống¶
Thiết kế hệ thống là cách bạn xây dựng phần mềm vận hành ổn định ở quy mô lớn. File này bao quát kiến trúc client-server, các giao thức mạng, DNS, proxy, cân bằng tải, bộ nhớ đệm, cơ sở dữ liệu, hàng đợi thông điệp, mô hình nhất quán và các mẫu chống chịu lỗi.
-
Mọi hệ thống ML đang chạy thực tế đều là một hệ thống phân tán. Một công cụ gợi ý không chỉ là một mô hình — nó là một máy chủ API, một feature store (kho đặc trưng), một registry mô hình, một tầng bộ nhớ đệm, một hàng đợi thông điệp và một stack giám sát, tất cả giao tiếp qua mạng. Hiểu thiết kế hệ thống là thứ tách biệt "tôi đã huấn luyện một mô hình" khỏi "tôi đã xây dựng một sản phẩm".
-
Các buổi phỏng vấn thiết kế hệ thống ở các công ty công nghệ lớn (Google, Meta, Amazon, OpenAI) kiểm tra xem bạn có thể thiết kế những hệ thống này hay không. Chương này cung cấp cho bạn các khối xây dựng (file này), hạ tầng đám mây (file 02), các mẫu mở rộng quy mô (file 03), thiết kế riêng cho ML (file 04) và các ví dụ thực hành (file 05).
Kiến trúc Client-Server¶
-
Mẫu cơ bản: một client gửi yêu cầu, một server xử lý và trả về phản hồi. Trình duyệt của bạn (client) gửi một yêu cầu HTTP tới google.com (server), server trả về HTML.
-
Mô hình request-response: đồng bộ. Client chờ phản hồi. Đơn giản nhưng tạo ra điểm thắt nút: client rảnh rỗi trong khi chờ, và server phải xử lý xong yêu cầu trước khi sang việc khác.
-
Server phi trạng thái (stateless): server không nhớ các yêu cầu trước. Mỗi yêu cầu chứa đủ thông tin để xử lý. Điều này giúp mở rộng dễ dàng: bất kỳ server nào cũng xử lý được bất kỳ yêu cầu nào, nên bạn có thể thêm nhiều server đằng sau một load balancer.
-
Server có trạng thái (stateful): server duy trì trạng thái giữa các yêu cầu (ví dụ phiên người dùng). Khó mở rộng hơn vì các yêu cầu từ cùng một người dùng phải tới cùng một server (session affinity). Các hệ thống hiện đại tránh trạng thái phía server bằng cách lưu nó vào cơ sở dữ liệu hoặc bộ nhớ đệm (Redis).
Các giao thức mạng¶
-
Chúng ta đã đề cập đến mạng ở chương 13 (các lớp TCP/IP, socket). Ở đây ta tập trung vào các giao thức tầng ứng dụng dùng trong thiết kế hệ thống:
-
HTTP/HTTPS: giao thức của web và hầu hết các API. Các phương thức yêu cầu: GET (đọc), POST (tạo/dự đoán), PUT (cập nhật), DELETE (xóa). HTTPS thêm mã hóa TLS (bảo mật chương 13). Các REST API (chương 15 file 03) được xây dựng trên HTTP.
-
WebSockets: kết nối hai chiều liên tục giữa client và server. Khác với HTTP (yêu cầu → phản hồi → đóng kết nối), WebSocket giữ kết nối mở để truyền dữ liệu thời gian thực. Dùng cho: phát token LLM (gửi token ngay khi được sinh ra), dashboard trực tiếp, ứng dụng chat.
-
gRPC: framework RPC của Google. Dùng Protocol Buffers (tuần tự hóa nhị phân, nhỏ và nhanh gấp ~10 lần JSON) qua HTTP/2. Hỗ trợ streaming (phía server, phía client, hai chiều). Dùng cho giao tiếp nội bộ service-to-service khi hiệu năng quan trọng. Triton Inference Server (chương 15) và TensorFlow Serving dùng gRPC.
-
Protocol Buffers: định nghĩa schema thông điệp trong các file
.proto:
message PredictRequest {
repeated float features = 1;
string model_version = 2;
}
message PredictResponse {
float prediction = 1;
float confidence = 2;
}
service ModelService {
rpc Predict(PredictRequest) returns (PredictResponse);
}
- Schema được biên dịch thành code client và server bằng bất kỳ ngôn ngữ nào (Python, C++, Go, Java). An toàn kiểu, tương thích ngược và hiệu năng có được miễn phí.
DNS¶
-
DNS (Hệ thống Tên miền) dịch các tên dễ đọc thành địa chỉ IP (chương 13). Với thiết kế hệ thống, DNS còn cung cấp:
-
Cân bằng tải qua DNS: trả về các địa chỉ IP khác nhau cho cùng một tên miền, phân phối lưu lượng qua nhiều server. Đơn giản nhưng thô (kết quả DNS được cache từ vài phút đến vài giờ, nên lưu lượng không tái cân bằng nhanh).
-
Định tuyến theo địa lý: trả về IP của trung tâm dữ liệu gần nhất dựa trên vị trí client. Người dùng ở Tokyo nhận trung tâm dữ liệu Nhật; người dùng ở London nhận trung tâm châu Âu.
-
Failover: nếu một server ngã xuống, DNS ngừng trả về IP của nó. Các client mới tới các server khỏe. Nhưng các mục DNS đã cache nghĩa là một số client tiếp tục gõ server chết trong vài phút (vấn đề TTL).
Proxy¶
-
Một proxy là trung gian giữa client và server:
-
Reverse proxy (đứng trước các server): client kết nối tới proxy, proxy chuyển tiếp yêu cầu tới các server backend. Client không biết server nào đã xử lý yêu cầu. Nginx và HAProxy là các reverse proxy tiêu chuẩn. Chúng cung cấp: cân bằng tải (phân phối yêu cầu), SSL termination (giải mã HTTPS tại proxy, gửi HTTP thuần tới backend), bộ nhớ đệm, giới hạn tốc độ và nén.
-
API gateway: một reverse proxy chuyên biệt cho API. Xử lý xác thực, giới hạn tốc độ, định tuyến yêu cầu (các đường dẫn khác → các service khác) và phiên bản hóa API. Kong, AWS API Gateway và Envoy là các lựa chọn phổ biến.
-
Cho ML serving: một API gateway đứng trước các model server của bạn. Nó xác thực API key, giới hạn tốc độ người dùng gói miễn phí, định tuyến
/v1/predicttới model server A và/v2/predicttới model server B, và thu thập metric sử dụng.
Cân bằng tải (Load Balancing)¶
- Khi bạn có nhiều server, một load balancer phân phối các yêu cầu đến đến chúng.
-
Các thuật toán:
- Round robin: gửi yêu cầu tới các server theo thứ tự (1, 2, 3, 1, 2, 3...). Đơn giản, công bằng, nhưng không tính đến tải server.
- Least connections: gửi tới server có ít kết nối hoạt động nhất. Tốt hơn cho các yêu cầu có thời gian xử lý biến thiên (một số yêu cầu LLM sinh 10 token, một số sinh 1000).
- Weighted round robin: các server có dung lượng lớn hơn nhận nhiều yêu cầu hơn. Server có 80 GB bộ nhớ GPU xử lý gấp 2x yêu cầu của server có 40 GB.
- Consistent hashing: băm khóa yêu cầu tới một server cụ thể. Cùng khóa luôn tới cùng server. Hữu ích cho: bộ nhớ đệm (các yêu cầu của cùng một người dùng gõ cùng một cache), session affinity, và prefix caching (chương 17: các yêu cầu có cùng system prompt đi tới server có KV-cache cho prompt đó).
-
Cân bằng tải L4 vs L7:
- L4 (tầng giao vận): định tuyến dựa trên IP và cổng. Nhanh nhưng không thể soi xét nội dung yêu cầu.
- L7 (tầng ứng dụng): định tuyến dựa trên đường dẫn HTTP, header hoặc nội dung body. Có thể định tuyến
/api/chattới chat server và/api/embedtới embedding server. Chậm hơn nhưng linh hoạt hơn.
Bộ nhớ đệm (Caching)¶
- Caching lưu dữ liệu truy cập thường xuyên vào một tầng lưu trữ nhanh (RAM) để tránh tính toán lại hoặc truy xuất lại.
-
Các mẫu cache:
- Cache-aside (lazy loading): ứng dụng kiểm tra cache trước. Khi miss, nó lấy từ database, lưu vào cache và trả về. Mẫu phổ biến nhất.
- Write-through: mọi ghi đi tới cả cache và database đồng thời. Đảm bảo cache luôn cập nhật nhưng làm chậm ghi.
- Write-back: ghi chỉ tới cache; cache bflush bất đồng bộ xuống database. Ghi nhanh nhất nhưng rủi ro mất dữ liệu nếu cache sập trước khi flush.
-
Chính sách đẩy ra (eviction policies) (khi cache đầy):
- LRU (Least Recently Used — ít dùng gần đây nhất): đẩy ra mục không được truy cập lâu nhất. Chính sách phổ biến nhất.
- LFU (Least Frequently Used — ít truy cập nhất): đẩy ra mục ít được truy cập nhất. Tốt hơn khi một số mục luôn phổ biến.
- TTL (Time To Live): các mục hết hạn sau một thời lượng cố định. Dùng cho dữ liệu trở nên cũ (dự đoán mô hình cache 5 phút, giá trị đặc trưng cache 1 giờ).
-
CDN (Mạng phân phối nội dung): một cache phân tán toàn cầu cho nội dung tĩnh (hình ảnh, JavaScript, CSS). Các server tại 100+ vị trí trên thế giới phục vụ nội dung cache từ vị trí gần người dùng nhất. Với ML: trọng số mô hình có thể được cache trên CDN để tải xuống nhanh.
-
Redis: cache/cơ sở dữ liệu trong bộ nhớ tiêu chuẩn. Hỗ trợ chuỗi, danh sách, tập hợp, tập hợp có thứ tự, bảng băm và luồng. Độ trễ dưới mili-giây. Dùng cho: cache dự đoán mô hình, lưu trữ dữ liệu phiên, giới hạn tốc độ (đếm yêu cầu mỗi người dùng mỗi phút) và phục vụ đặc trưng thời gian thực.
-
Cho ML serving: cache các dự đoán cho đầu vào lặp lại. Nếu nhiều người dùng hỏi "Thủ đô của Pháp là gì?", tính câu trả lời một lần và phục vụ kết quả cache. Tỷ lệ cache hit 20-40% là phổ biến cho khối lượng công việc chatbot, giảm tương ứng chi phí GPU.
Cơ sở dữ liệu¶
SQL (Quan hệ)¶
-
Cơ sở dữ liệu SQL (PostgreSQL, MySQL) lưu dữ liệu trong các bảng với hàng và cột. Các quan hệ giữa bảng được biểu diễn qua khóa ngoại. Truy vấn dùng SQL. Đảm bảo ACID:
- Atomicity (tính nguyên tử): một giao dịch hoặc hoàn thành hoàn toàn hoặc rollback hoàn toàn. Không có cập nhật một phần.
- Consistency (tính nhất quán): cơ sở dữ liệu chuyển từ một trạng thái hợp lệ sang trạng thái hợp lệ khác. Các ràng buộc (khóa duy nhất, khóa ngoại) luôn được thỏa mãn.
- Isolation (tính cô lập): các giao dịch đồng thời không can thiệp vào nhau.
- Durability (tính bền vững): dữ liệu đã commit tồn tại qua các vụ sập (được ghi xuống đĩa trước khi xác nhận).
-
Cơ sở dữ liệu SQL xuất sắc ở: dữ liệu có cấu trúc với quan hệ, truy vấn phức tạp (join, aggregation), yêu cầu nhất quán nghiêm ngặt và toàn vẹn dữ liệu.
NoSQL¶
-
Cơ sở dữ liệu NoSQL đánh đổi một số đảm bảo ACID lấy khả năng mở rộng và linh hoạt:
- Key-value stores (Redis, DynamoDB): mô hình đơn giản nhất. Tra cứu nhanh theo khóa. Dùng cho cache, lưu trữ phiên và feature store.
- Document stores (MongoDB, Firestore): lưu các tài liệu giống JSON. Schema linh hoạt (mỗi tài liệu có thể có các trường khác nhau). Dùng cho hồ sơ người dùng, danh mục sản phẩm và cấu hình.
- Column-family stores (Cassandra, HBase): tối ưu cho khối lượng ghi nặng và dữ liệu chuỗi thời gian. Dùng cho ghi log sự kiện, metric và phân tích.
- Graph databases (Neo4j): lưu các nút và cạnh. Tối ưu cho truy vấn duyệt. Dùng cho mạng xã hội, đồ thị tri thức và hệ thống gợi ý.
- Vector databases (Pinecone, Milvus, Weaviate, FAISS): lưu các embedding nhiều chiều và hỗ trợ tìm kiếm láng giềng gần xấp xỉ (ANN). Thiết yếu cho tìm kiếm ngữ nghĩa, RAG (tạo sinh có truy hồi tăng cường) và hệ thống gợi ý.
Định lý CAP¶
-
Trong một cơ sở dữ liệu phân tán, bạn có thể có tối đa hai trong ba thuộc tính:
- Consistency (nhất quán): mọi đọc trả về ghi gần đây nhất.
- Availability (sẵn sàng): mọi yêu cầu đều nhận được phản hồi (kể cả khi một số nút sập).
- Partition tolerance (chịu phân vùng): hệ thống tiếp tục hoạt động bất chấp các phân vùng mạng (các nút không thể giao tiếp).
-
Vì phân vùng mạng là không thể tránh khỏi trong hệ thống phân tán, sự lựa chọn thực sự là CP (nhất quán nhưng có thể không sẵn sàng trong phân vùng — ví dụ PostgreSQL) so với AP (sẵn sàng nhưng có thể trả về dữ liệu cũ trong phân vùng — ví dụ Cassandra, DynamoDB).
-
Cho ML: feature store thường chọn AP (một giá trị đặc trưng hơi cũ tốt hơn là không có dự đoán). Registry mô hình chọn CP (phục vụ sai phiên bản mô hình là thảm họa).
Sharding¶
-
Sharding chia một cơ sở dữ liệu qua nhiều máy. Mỗi shard giữ một tập hợp con của dữ liệu.
-
Hash sharding: băm khóa để xác định shard.
shard = hash(user_id) % num_shards. Phân phối đều nhưng làm cho truy vấn theo khoảng không thể. -
Range sharding: mỗi shard giữ một khoảng khóa (người dùng A-G trên shard 1, H-N trên shard 2). Cho phép truy vấn theo khoảng nhưng có thể tạo điểm nóng (nếu nhiều người dùng có tên bắt đầu bằng "S").
-
Vấn đề resharding: thêm một shard làm mất hiệu lực ánh xạ băm. Consistent hashing tối thiểu hóa sự di chuyển dữ liệu: chỉ ~1/n khóa cần di chuyển khi thêm shard thứ n.
Chỉ mục cơ sở dữ liệu (Database Indexing)¶
-
Một index là một cấu trúc dữ liệu tăng tốc truy vấn với chi phí thêm bộ nhớ và ghi chậm hơn. Không có index, một truy vấn quét mọi hàng (O(n)). Có index, nó tìm mục tiêu trong O(log n).
-
B-tree index (mặc định): một cây cân bằng (chương 13, chương 14) nơi mỗi nút chứa nhiều khóa và con trỏ. B-tree thân thiện với cache (các nút rộng vừa dòng cache) và hỗ trợ truy vấn theo khoảng (
WHERE age BETWEEN 20 AND 30). Hầu hết cơ sở dữ liệu SQL dùng B-tree. -
Hash index: ánh xạ khóa tới vị trí hàng dùng hàm băm. Tra cứu \(O(1)\) nhưng không hỗ trợ truy vấn theo khoảng. Dùng cho tra cứu khớp chính xác (
WHERE id = 12345). -
Composite index: một index trên nhiều cột.
CREATE INDEX ON users(country, city)tăng tốc các truy vấn lọc theo country, hoặc country + city, NHƯNG KHÔNG theo city một mình (cột ngoài cùng bên trái phải có trong truy vấn). -
Sự đánh đổi: mọi index đều tăng tốc đọc nhưng làm chậm ghi (index phải được cập nhật trên mọi insert/update/delete) và dùng bộ nhớ (~10-30% kích thước bảng mỗi index). Đừng index mọi thứ — hãy index các cột bạn truy vấn thường xuyên.
-
Cho hệ thống ML: cơ sở dữ liệu trực tuyến của feature store cần index trên khóa thực thể (user_id, item_id) để tra cứu đặc trưng nhanh. Cơ sở dữ liệu theo dõi thí nghiệm cần index trên (experiment_id, metric_name) cho truy vấn dashboard.
Thiết kế API¶
-
Các hệ thống giao tiếp qua API. Thiết kế API tốt làm hệ thống có thể dùng được, tiến hóa được và dễ debug:
-
Quy ước REST: dùng danh từ cho tài nguyên (
/users,/models), phương thức HTTP cho hành động (GET = đọc, POST = tạo, PUT = cập nhật, DELETE = xóa), và mã trạng thái cho kết quả (200 = OK, 201 = đã tạo, 400 = yêu cầu sai, 404 = không tìm thấy, 429 = bị giới hạn tốc độ, 500 = lỗi server). -
Phân trang (Pagination): với các endpoint trả về danh sách, không bao giờ trả về tất cả kết quả cùng lúc. Dùng phân trang dựa trên con trỏ (
GET /items?cursor=abc&limit=50) hoặc dựa trên offset (GET /items?offset=100&limit=50). Dựa trên con trỏ hiệu quả hơn cho tập dữ liệu lớn (dựa trên offset yêu cầu bỏ qua các hàng). -
Phiên bản hóa (Versioning): thêm tiền tố phiên bản vào đường dẫn API (
/v1/predict,/v2/predict). Điều này cho phép bạn tiến hóa API mà không phá vỡ các client hiện có. Các client chuyển sang v2 theo tốc độ của riêng họ; v1 bị deprecated nhưng không bị xóa cho đến khi lưu lượng giảm. -
Phản hồi lỗi: trả về các lỗi có cấu trúc với đủ thông tin để debug:
{
"error": {
"code": "INVALID_INPUT",
"message": "Đặc trưng 'user_age' phải là số nguyên dương",
"details": {"field": "user_age", "value": -5}
}
}
Hàng đợi thông điệp (Message Queues)¶
-
Message queues tách rời các producer (service sinh công việc) khỏi các consumer (service xử lý nó). Producer gửi một thông điệp tới hàng đợi; consumer kéo nó khi sẵn sàng.
-
Tại sao hàng đợi quan trọng: không có hàng đợi, nếu consumer chậm hoặc sập, producer bị chặn. Có hàng đợi, producer bắn và quên; hàng đợi đệm các thông điệp cho đến khi consumer sẵn sàng.
-
Apache Kafka: một hàng đợi thông điệp phân tán, bền bỉ, thông lượng cao. Các thông điệp được lưu trong topics, mỗi topic được phân vùng qua nhiều broker. Consumer đọc từ các phân vùng, theo dõi vị trí của chúng (offset). Kafka đảm bảo thứ tự trong một phân vùng và có thể phát lại thông điệp (log bền bỉ).
-
Pub/sub: các publisher gửi thông điệp tới một topic; tất cả subscriber của topic đó nhận một bản sao. Dùng cho kiến trúc hướng sự kiện: "một mô hình mới đã được triển khai" kích hoạt service giám sát, service A/B testing và service logging đồng thời.
-
Cho ML: một yêu cầu dự đoán đến qua HTTP, được đặt trong hàng đợi Kafka, được xử lý bởi một GPU worker, và kết quả được trả về qua callback hoặc WebSocket. Hàng đợi đệm các burst lưu lượng và đảm bảo không yêu cầu nào bị mất nếu một GPU worker sập.
Mô hình nhất quán (Consistency Models)¶
-
Trong một hệ thống phân tán, các nút khác nhau có thể có các góc nhìn khác nhau về dữ liệu. Các mô hình nhất quán định nghĩa những đảm bảo hệ thống cung cấp:
-
Strong consistency (nhất quán mạnh): sau một ghi, tất cả các đọc tiếp theo (từ bất kỳ nút nào) thấy giá trị mới. Dễ suy luận nhưng chậm (yêu cầu phối hợp giữa các nút).
-
Eventual consistency (nhất quán cuối cùng): sau một ghi, các đọc có thể thấy dữ liệu cũ trong một thời gian, nhưng cuối cùng sẽ thấy giá trị mới. Nhanh (không phối hợp) nhưng yêu cầu ứng dụng xử lý các đọc cũ.
-
Causal consistency (nhất quán nhân quả): nếu thao tác A đi trước B theo nhân quả (ví dụ "ghi X rồi đọc X"), hệ thống đảm bảo B thấy kết quả của A. Nhưng các thao tác không liên quan có thể được thấy theo bất kỳ thứ tự nào.
-
Read-your-writes (đọc ghi của chính mình): một người dùng luôn thấy các ghi của chính họ ngay lập tức, ngay cả khi người dùng khác thấy dữ liệu cũ. Mức nhất quán tối thiểu hầu hết ứng dụng cần.
Các mẫu chống chịu lỗi (Resilience Patterns)¶
-
Giới hạn tốc độ (Rate limiting): giới hạn số yêu cầu mỗi người dùng mỗi cửa sổ thời gian. Bảo vệ chống lạm dụng và đảm bảo truy cập công bằng. Được triển khai với token bucket hoặc sliding window counter trong Redis.
-
Cầu chì (Circuit breaker): nếu một service hạ nguồn bắt đầu thất bại (tỷ lệ lỗi vượt ngưỡng), circuit breaker "mở" và ngừng gửi yêu cầu tới nó (trả về ngay một phản hồi dự phòng). Sau một timeout, nó "nửa mở" và gửi một yêu cầu thử nghiệm. Nếu thử nghiệm thành công, nó đóng (hoạt động bình thường). Điều này ngăn chặn lỗi lan truyền: nếu feature store sập, model server trả về dự đoán không cần đặc trưng thay vì timeout trên mọi yêu cầu.
-
Backpressure (áp lực ngược): khi một hệ thống quá tải, nó báo hiệu upstream chậm lại. Thay vì chấp nhận yêu cầu và thất bại, nó từ chối sớm các yêu cầu dư thừa (với mã trạng thái 429 hoặc 503). Client retry với exponential backoff.
-
Retry với exponential backoff (thử lại với lùi theo cấp số nhân): nếu một yêu cầu thất bại, chờ 1 giây và thử lại. Nếu lại thất bại, chờ 2 giây. Rồi 4, 8, v.v. Thêm jitter (độ trễ ngẫu nhiên) để ngăn tất cả client retry đồng thời (vấn đề bầy đàn — thundering herd).
-
Idempotency (tính bất biến): một thao tác là idempotent nếu thực hiện hai lần có cùng hiệu quả như một lần.
PUT /user/123 {"name": "Alice"}là idempotent (đặt tên thành Alice hai lần vẫn ổn).POST /paymentsthì không (trả tiền hai lần là tệ). Làm cho các thao tác idempotent đảm bảo retry an toàn.