Harbor

Ứng dụng chat với AI

KhóStreaming SSEGPU inferenceBatchingQuota theo góiGiống ChatGPT, Claude, Gemini

Hiểu đề

Ứng dụng chat với AI là gì? Ứng dụng cho người dùng trò chuyện với một mô hình ngôn ngữ lớn (LLM): gõ câu hỏi, câu trả lời hiện dần từng chữ, lịch sử hội thoại được lưu để quay lại hỏi tiếp. Khác với chat thường, phần đắt nhất là GPU sinh token, không phải database.

Yêu cầu chức năng

  1. Người dùng có thể gửi tin nhắn và thấy câu trả lời hiện dần từng chữ (streaming)
  2. Người dùng có thể dừng câu trả lời đang sinh hoặc yêu cầu sinh lại
  3. Người dùng có thể xem danh sách hội thoại, mở lại một hội thoại cũ và hỏi tiếp với đúng ngữ cảnh
  4. Người dùng gói Free và Plus có hạn mức khác nhau và được báo rõ khi hết hạn mức

Yêu cầu phi chức năng

  • Token đầu tiên hiện ra p95 < 1,5 s khi hệ thống chưa quá tải
  • Tốc độ sinh ≥ 20 token/s cho mỗi câu trả lời đang stream
  • Mất mạng giữa chừng không mất câu trả lời: nối lại stream được trong 10 phút
  • API sẵn sàng 99,9%; khi GPU quá tải thì ưu tiên gói trả phí, gói Free nhận 429 kèm Retry-After
  • Mở lại hội thoại cũ p95 < 300 ms, lịch sử không mất

Ngoài phạm vi: Huấn luyện và fine-tune mô hình; Tải file, phân tích ảnh, sinh ảnh, giọng nói; Plugin, công cụ gọi web, agent nhiều bước; Kiểm duyệt nội dung chi tiết (chỉ nhắc là một bước trong luồng).

Thực thể và API

User
Người dùng: id, gói (FREE/PLUS/TEAM), múi giờ để reset hạn mức theo ngày.
Conversation
Hội thoại: id, user_id, tiêu đề tự sinh, model, updated_at, bản tóm tắt phần cũ (summary).
Message
Một tin trong hội thoại: conversation_id, seq tăng dần, role (user/assistant), nội dung, số token, parent_id để hỗ trợ sinh lại.
Generation
Một lần sinh câu trả lời: message_id, model, trạng thái QUEUED/STREAMING/DONE/CANCELLED/FAILED, token vào/ra, thời gian token đầu.
UsageLedger
Sổ dùng token theo user và ngày: token vào, token ra, số tin; nguồn để tính hạn mức và chi phí.
MethodEndpointGhi chú
POST/v1/conversationsTạo hội thoại mới, trả conversationId.
GET/v1/conversations?cursor=Danh sách hội thoại theo updated_at giảm dần, phân trang bằng cursor.
GET/v1/conversations/{id}/messages?before={seq}Lịch sử tin nhắn, tải dần từ mới về cũ.
POST/v1/conversations/{id}/messagesGửi tin: body {content, model, clientMessageId}. Response là text/event-stream, mỗi event có id tăng dần. Trả 429 nếu hết hạn mức.
GET/v1/generations/{id}/streamNối lại stream đang chạy, header Last-Event-ID để chỉ nhận phần còn thiếu.
POST/v1/generations/{id}/cancelDừng sinh. GPU nhả slot ngay, phần đã sinh vẫn được lưu.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người dùng có thể gửi tin nhắn và thấy câu trả lời hiện dần

Chat API không giữ trạng thái: nó kiểm tra hạn mức, ghép ngữ cảnh, đẩy yêu cầu vào bộ điều phối GPU rồi đọc token từ một stream trung gian để gửi xuống client bằng SSE. Vì token nằm ở stream trung gian theo generation_id, instance nào cũng phục vụ được việc nối lại.

  1. 1Client gửi POST /messages, Chat API lưu tin của user và tạo Generation QUEUED
  2. 2Chat API trừ trước hạn mức ước lượng (max_tokens) trong Redis, hết hạn mức thì trả 429
  3. 3Chat API ghép ngữ cảnh (system prompt + tóm tắt + các tin gần nhất) và gửi tới bộ điều phối
  4. 4Bộ điều phối xếp yêu cầu vào hàng đợi theo gói, GPU sinh token theo continuous batching
  5. 5GPU worker ghi từng cụm token vào Redis Stream gen:{id}; Chat API đọc và đẩy xuống client qua SSE
  6. 6Xong thì lưu tin assistant, ghi số token thật vào UsageLedger và hoàn lại phần hạn mức trừ dư

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Motivation" (hàng đợi hấp thụ đột biến, priority queue) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Rate Limiter Algorithms", mục "Token bucket algorithm" ; Redis in Action ↗ · ch.6 · §6.5 Pull messaging (tr. 140–146) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

2) Người dùng có thể dừng hoặc sinh lại câu trả lời

Dừng là việc quan trọng về chi phí: mỗi giây GPU tiếp tục sinh cho người đã bỏ đi là tiền mất. Lệnh dừng đi qua cờ trong Redis mà GPU worker kiểm tra giữa các bước sinh.

  1. 1Client gọi POST /generations/{id}/cancel hoặc đóng tab (server phát hiện SSE bị ngắt quá 30 s)
  2. 2Chat API đặt cờ cancel:{id} trong Redis
  3. 3GPU worker thấy cờ ở bước sinh kế tiếp, bỏ yêu cầu khỏi batch, giải phóng KV cache
  4. 4Phần đã sinh được lưu với trạng thái CANCELLED; sinh lại tạo Message mới cùng parent_id

Nguồn: Redis in Action ↗ · ch.3 · §3.7 Other commands › "Expiring keys" (tr. 61–62) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

3) Người dùng có thể mở lại hội thoại cũ và hỏi tiếp

Lịch sử đọc theo conversation_id và seq nên chọn store khóa theo hội thoại. Khi hỏi tiếp, không gửi toàn bộ lịch sử cho mô hình mà chỉ gửi phần vừa đủ trong giới hạn ngữ cảnh.

  1. 1Client gọi GET /conversations để lấy danh sách, GET /messages?before= để tải lịch sử
  2. 2Store tin nhắn đọc theo partition conversation_id, sắp theo seq
  3. 3Khi user hỏi tiếp, Chat API lấy summary + N tin gần nhất cho đến khi chạm ngân sách token
  4. 4Worker nền cập nhật summary khi phần lịch sử ngoài cửa sổ vượt ngưỡng

Nguồn: Grokking Modern System Design · ch.10 Key-value Store ↗ · bài "Design of a Key-value Store", mục "Data type" và "API design" ; Grokking Modern System Design · ch.9 Databases ↗ · bài "Data Partitioning", mục "Hash-based sharding" ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU
Tải đỉnh
≈ 6 req/s, ≈ 80 stream đồng thời
Chi phí ước lượng
≈ 4.000–6.000 USD/tháng (ước lượng, gần hết là tiền token)

Sản phẩm mới, gọi API mô hình của nhà cung cấp và trả tiền theo token. Chat API trên Cloud Run stream SSE, một Postgres lưu user, hội thoại, tin nhắn và hạn mức.

Người dùngNgười dùngDịch vụChat API (SSE)Bên ngoàiAPI LLMDatabasePostgreSQLBên ngoàiAuthBên ngoàiStripeHTTPS + SSEstream tokenlịch sử, hạn mứcxác thựcđổi gói

Ước lượng

  • Tin nhắn: 10.000 DAU × 10 tin / 86.400 ≈ 1,2 req/s, đỉnh ×5 ≈ 6 req/s
  • Giả định mỗi lượt: 2.000 token vào, 400 token ra, sinh 30 token/s → 400 / 30 ≈ 13 s; stream đồng thời ≈ 6 × 13 ≈ 80
  • Giá API giả định 0,5 USD/1M token vào, 1,5 USD/1M token ra: 2.000 × 0,5 / 10^6 + 400 × 1,5 / 10^6 = 0,001 + 0,0006 = 0,0016 USD/lượt
  • Chi phí token: 100.000 lượt/ngày × 30 × 0,0016 ≈ 4.800 USD/tháng
  • Nếu tự host: 6 × 600 token tương đương = 3.600 /s → ceil(3.600 / 1.050) = 4 GPU × 730 h × 2,5 USD ≈ 7.300 USD/tháng và rảnh phần lớn thời gian
  • Lưu trữ: 100.000 lượt × 2,5 KB ≈ 250 MB/ngày ≈ 91 GB/năm

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · bài "Examples of Resource Estimation", mục "Number of servers required", "Storage requirements" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (500 req/s mỗi vCPU, chạy 60% CPU, Redis ×1,3) ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
Chat APINode.js (NestJS), Cloud Run, bật streaming response22 vCPU · 4 GB RAM6 req/s và 80 stream chỉ cần 1 instance, chạy 2 để dự phòng; timeout request nâng lên 5 phút.
Mô hìnhAPI LLM của nhà cung cấp (OpenAI, Anthropic, Google...)1Trả theo token, stream sẵn4 GPU tự host đắt hơn và phần lớn thời gian ngồi không; API không cần vận hành.
DatabasePostgreSQL managed12 vCPU · 8 GB · 200 GB SSD91 GB/năm, index (conversation_id, seq); bảng usage đếm hạn mức theo ngày.
Đăng nhậpAuth0 / Firebase Auth / Clerk1Dịch vụ bên ngoàiKhông tự làm phần đăng nhập ở giai đoạn đầu.
Thanh toán góiStripe1Dịch vụ bên ngoài, webhookWebhook cập nhật gói của user trong Postgres.
Thay đổi so với mức trước
  • Điểm xuất phát: một Chat API stream thẳng từ API của nhà cung cấp mô hình
  • Mọi dữ liệu nằm trong một Postgres, hạn mức đếm bằng bảng usage theo ngày
  • Chưa có GPU riêng, chi phí biến đổi theo số token
Nút thắt tiếp theo

Hóa đơn token tăng tuyến tính theo người dùng và nhà cung cấp giới hạn số token mỗi phút của tài khoản. Lên 1 triệu DAU thì vừa đắt vừa bị giới hạn, cần cân nhắc tự host và hạn mức chính xác theo token.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU1 triệu DAU50 triệu DAU
Chi phí≈ 4.000–6.000 USD/tháng (ước lượng, gần hết là tiền token)≈ 230.000–380.000 USD/tháng (ước lượng, GPU chiếm trên 90%, giá giả định 2,5 USD/GPU-giờ)≈ 5–9 triệu USD/tháng (ước lượng, giả định 2 USD/GPU-giờ thuê dài hạn; GPU chiếm trên 90%)
Chat API
Node.js (NestJS), Cloud Run, bật streaming response
2 × 2 vCPU · 4 GB RAM
Node.js, container autoscale
2–4 (autoscale) × 2 vCPU · 4 GB RAM
Node.js trên Kubernetes, 3 region
76 pod (HPA theo số kết nối) × 2 vCPU · 4 GB mỗi pod
Mô hình
API LLM của nhà cung cấp (OpenAI, Anthropic, Google...)
1 × Trả theo token, stream sẵn
Chưa cầnChưa cần
Database
PostgreSQL managed
1 × 2 vCPU · 8 GB · 200 GB SSD
PostgreSQL managed (HA)
1 primary + 1 replica × 4 vCPU · 16 GB · 200 GB SSD
PostgreSQL HA, đọc qua replica
1 primary + 2 replica × 16 vCPU · 64 GB · 1 TB SSD
Đăng nhập
Auth0 / Firebase Auth / Clerk
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
Thanh toán gói
Stripe
1 × Dịch vụ bên ngoài, webhook
Chưa cầnChưa cần
Load balancerChưa cần
ALB / Cloud Load Balancing
1 × Idle timeout 300 s cho SSE
Chưa cần
Bộ điều phốiChưa cần
Service nhỏ (Go/Node) giữ hàng đợi Plus/Free
3 × 2 vCPU · 4 GB RAM
Chưa cần
Cụm GPUChưa cần
vLLM, mô hình open-weight ~70B, continuous batching
70–200 GPU (9–25 node 8×H100) × 8 × H100 80 GB mỗi node
vLLM/TensorRT-LLM, pool lớn + pool nhỏ, prefix cache
≈ 3.000 + 1.050 GPU (≈ 507 node) × 8 × H100 80 GB mỗi node
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 8 GB
Chưa cần
Tin nhắnChưa cần
DynamoDB, PK conversation_id, SK seq
1 bảng × On-demand
ScyllaDB / Cassandra, PK conversation_id, CK seq
Cluster 30+ node, RF 3 × 16 vCPU · 64 GB · NVMe mỗi node
Global LB + WAFChưa cầnChưa cần
Cloudflare / Global Load Balancer
1 × Định tuyến theo region gần nhất, chặn bot
Router + điều phốiChưa cầnChưa cần
Service phân loại câu hỏi + hàng đợi ưu tiên
12 pod × 4 vCPU · 8 GB mỗi pod
Redis ClusterChưa cầnChưa cần
ElastiCache Redis Cluster mỗi region
16 shard × (1 primary + 1 replica) × 4 vCPU · 16 GB mỗi node
Kafka + workerChưa cầnChưa cần
MSK / Confluent, worker Node.js
6 broker, 20 pod worker × Topic usage, conversation-updated
Vector DBChưa cầnChưa cần
pgvector shard / Qdrant / OpenSearch k-NN
Cluster theo region × Index theo user_id

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Stream token tới client thế nào khi API là stateless và nhiều instance?

Cách chưa tốt
Chờ sinh xong rồi trả một lần

API gọi mô hình, đợi 10-20 s cho đủ câu trả lời rồi trả JSON. Dễ làm nhưng người dùng nhìn màn hình trống rất lâu, và load balancer hay proxy có thể cắt request vì timeout.

Đánh đổi: Đơn giản nhất nhưng trải nghiệm kém, không đạt yêu cầu token đầu < 1,5 s.

Nguồn: Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản ; Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · bài "Availability" (độ trễ và tính sẵn sàng là yêu cầu phi chức năng)

Cách tốt
SSE nối thẳng từ API tới GPU worker

API mở stream gRPC tới worker và chuyển tiếp từng token xuống client qua SSE. SSE là HTTP thường, một chiều, đi qua proxy dễ, trình duyệt có sẵn EventSource. Chú ý backpressure: client chậm thì API phải đệm có giới hạn chứ không đọc vô hạn từ worker.

Đánh đổi: Kết nối bị gắn vào một instance API: instance chết hoặc client đổi mạng là mất phần đang sinh, phải sinh lại và tốn GPU lần nữa.

Nguồn: Grokking Modern System Design · ch.3 Abstractions ↗ · bài "Network Abstractions: Remote Procedure Calls" (RPC từ API tới worker) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

Cách tốt nhất
GPU worker ghi vào stream trung gian theo generation_id, API chỉ đọc và chuyển tiếp

Worker ghi cụm token (khoảng 250 ms một lần) vào Redis Stream gen:{id} với TTL 10 phút, mỗi entry có id tăng dần. Instance API nào cũng XREAD từ stream đó và gửi xuống client với id của SSE event. Mất kết nối thì client gọi /stream kèm Last-Event-ID và nhận đúng phần thiếu, GPU không phải sinh lại. Gom token theo cụm giúp giảm số lần ghi Redis và số gói tin xuống client.

Đánh đổi: Thêm một bước trung gian (vài ms trễ) và Redis phải chịu tải ghi tỉ lệ với số stream đồng thời; ở quy mô lớn cần Redis Cluster.

Nguồn: Redis in Action ↗ · ch.6 · §6.5 Pull messaging (list/pub-sub làm kênh nhận) ; Redis in Action ↗ · ch.3 · §3.6 Publish/subscribe (tr. 54–57) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

2) Cần bao nhiêu GPU, và xếp hàng, gom batch yêu cầu thế nào cho hiệu quả?

Cách chưa tốt
Mỗi GPU xử lý một yêu cầu tại một thời điểm

Mỗi bước sinh token phải đọc toàn bộ trọng số mô hình từ bộ nhớ GPU, nên chạy một yêu cầu thì GPU chủ yếu chờ bộ nhớ. Giả định một H100 chỉ ra khoảng 30-40 token/s cho một luồng, tức là phải có hàng nghìn GPU cho vài trăm req/s.

Đánh đổi: Dễ hiểu nhưng lãng phí gần hết năng lực GPU, chi phí mỗi request cao gấp hàng chục lần.

Nguồn: Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · bài "Put Back-of-the-envelope Numbers in Perspective", mục "Requests estimation" (công thức số server từ RPS)

Cách tốt
Static batching: gom N yêu cầu rồi chạy cùng lúc

Đợi đủ 16-32 yêu cầu (hoặc hết 50 ms) rồi chạy chung một batch, một lần đọc trọng số phục vụ nhiều luồng. Thông lượng tăng nhiều lần.

Đánh đổi: Câu trả lời ngắn phải đợi câu dài nhất trong batch xong mới nhường chỗ, GPU lại bị bỏ trống và độ trễ token đầu tăng.

⚠ Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

Cách tốt nhất
Continuous batching + hàng đợi ưu tiên theo gói + admission control theo KV cache

Dùng server như vLLM: sau mỗi bước sinh, yêu cầu xong rời batch và yêu cầu mới vào ngay, KV cache cấp phát theo trang để không phí bộ nhớ. Giả định để tính: một H100 chạy mô hình ~70B ra ≈ 1.500 token tương đương/s tổng, với 1 token tương đương = 1 token ra hoặc 10 token vào (prefill rẻ hơn nhiều). Số GPU = tải đỉnh / (1.500 × 0,7). Phía trước là bộ điều phối có hàng đợi riêng cho Plus và Free, chỉ đưa yêu cầu vào GPU khi còn chỗ KV cache, đo độ dài hàng đợi để autoscale và cắt tải gói Free trước.

Đánh đổi: Một GPU mới cần vài phút để tải trọng số nên autoscale chậm; phải giữ sẵn dung lượng nền và chấp nhận trả 429 cho gói Free trong những phút đỉnh.

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Motivation" (Priority queue: mỗi mức ưu tiên một queue, Rate limiting hấp thụ đột biến) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · bài "Design Considerations", mục "Execution cap" và "Prioritization" ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế

3) Lưu hội thoại và lấy ngữ cảnh cho câu hỏi tiếp theo thế nào?

Cách chưa tốt
Gửi toàn bộ lịch sử cho mô hình mỗi lượt

Mỗi tin mới lại gửi cả hội thoại. Hội thoại 50 lượt có thể dài 30.000 token, vừa chậm token đầu vừa tốn tiền, và sẽ vượt giới hạn ngữ cảnh của mô hình.

Đánh đổi: Chi phí mỗi lượt tăng theo độ dài hội thoại, tổng chi phí một hội thoại tăng theo bình phương số lượt.

Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "System Design: The Rate Limiter", mục "Why do we need a rate limiter?" (không nêu gì về cửa sổ ngữ cảnh) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

Cách tốt
Cửa sổ trượt: chỉ gửi N token gần nhất, tin nhắn lưu theo conversation_id

Lưu Message với khóa (conversation_id, seq), đọc ngược từ mới về cũ đến khi đủ ngân sách, ví dụ 4.000 token. Ở mức nhỏ dùng Postgres với index (conversation_id, seq); khi dữ liệu lên hàng TB thì chuyển sang DynamoDB hoặc Cassandra với partition key là conversation_id.

Đánh đổi: Rẻ và dự đoán được chi phí, nhưng mô hình quên hẳn những gì nói ở đầu hội thoại.

Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · bài "Data Partitioning", mục "Key-range based sharding" ; Microservices Patterns ↗ (.zip) · ch.7 · §7.4 Implementing a CQRS view with AWS DynamoDB, mục "Data modeling and query design with DynamoDB" (tr. 244–249) ; Grokking Modern System Design · ch.9 ↗ · bài "Types of Databases", mục "Key-value database"

Cách tốt nhất
Cửa sổ gần nhất + tóm tắt lăn + truy hồi bằng embedding + prefix cache

Ngữ cảnh = system prompt + summary của phần cũ + vài đoạn cũ liên quan nhất (tìm bằng vector của câu hỏi) + các tin gần nhất. Worker nền cập nhật summary và embedding sau mỗi vài lượt, không nằm trên đường trả lời. Giữ phần đầu prompt ổn định để GPU tái dùng KV cache của prefix, giảm thời gian prefill. Tin nhắn cũ hơn 90 ngày có thể chuyển sang storage rẻ và nạp lại khi mở.

Đánh đổi: Nhiều thành phần hơn (worker tóm tắt, vector DB) và tóm tắt có thể làm mất chi tiết; cần đo chất lượng câu trả lời chứ không chỉ đo độ trễ.

Nguồn: Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · bài "System Design: The Distributed Messaging Queue", mục "Messaging queue use cases" (Recommender systems: xử lý tốn thời gian đưa ra ngoài đường trả lời bằng queue) ; Chưa có nguồn trong thư viện: vector database không có sách; xem bài /hoc/vector-database của khoá (cũng đánh dấu chưa có nguồn) ; Chưa có nguồn trong thư viện: SSE, continuous batching, vLLM, KV cache, embedding và tóm tắt hội thoại không có trong sách của thư viện; chỉ nêu khái niệm cơ bản

4) Giới hạn theo gói và kiểm soát chi phí mỗi request thế nào?

Cách chưa tốt
Đếm số request mỗi phút như API thường

Giới hạn 20 request/phút cho mọi người. Nhưng một request 'tóm tắt tài liệu này' có thể tốn gấp 50 lần request 'xin chào', nên đếm request không phản ánh chi phí GPU.

Đánh đổi: Dễ làm nhưng vài người dùng nặng có thể ngốn phần lớn GPU mà vẫn trong hạn mức.

Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "System Design: The Rate Limiter", mục "Why do we need a rate limiter?" ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Requirements of a Rate Limiter's Design", mục "Types of throttling"

Cách tốt
Token bucket theo token trên Redis, hạn mức khác nhau theo gói

Mỗi user có bucket tính theo token tương đương mỗi phút và hạn mức theo ngày, giá trị tùy gói (ví dụ Free 50 tin/ngày, Plus cao hơn nhiều). Dùng script Lua trên Redis để kiểm tra và trừ nguyên tử. Trả header còn lại bao nhiêu để client hiển thị.

Đánh đổi: Chưa biết trước câu trả lời dài bao nhiêu nên chỉ trừ được sau khi xong; nhiều request song song có thể vượt hạn mức.

Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Rate Limiter Algorithms", mục "Token bucket algorithm" (tham số, ưu nhược điểm) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Design of a Rate Limiter", mục "Race condition" (kiểm tra và trừ phải nguyên tử) ; Redis in Action ↗ · ch.11 · §11.2 Rewriting locks and semaphores with Lua (tr. 255–260) ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Design of a Rate Limiter", mục "High-level design" (luật theo loại)

Cách tốt nhất
Đặt trước rồi quyết toán, cộng định tuyến mô hình theo chi phí

Trước khi sinh, trừ trước token vào + max_tokens; xong thì trả lại phần dư theo số token thật và ghi UsageLedger qua sự kiện. Mỗi request có chi phí tính được: (token tương đương / thông lượng GPU) × giá GPU-giây. Với giả định ở bài này một lượt 600 token tương đương tốn ≈ 0,57 GPU-giây ≈ 0,0004 USD. Để giảm chi phí: định tuyến câu hỏi dễ và gói Free sang mô hình nhỏ, cache prefix, giới hạn max_tokens theo gói, và dừng sinh ngay khi client bỏ đi.

Đánh đổi: Phải có bộ phân loại để định tuyến, chất lượng có thể giảm cho câu hỏi bị đánh giá sai là dễ; sổ quyết toán cần idempotent để không trừ hai lần.

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages (sổ quyết toán idempotent, tr. 95–97) ; Chưa có nguồn trong thư viện: thư viện không có sách về LLM/GPU; thông lượng GPU, giá GPU-giờ, giá token API, token/s và kích thước KV cache đều là giả định của khoá học, cần đo thực tế

Trong phỏng vấn

Nên nói
  • “Tài nguyên đắt nhất là GPU, nên tôi tính kích thước theo token mỗi giây, không theo request mỗi giây.”
  • “Tôi stream bằng SSE; token đi qua Redis Stream theo generation_id nên API stateless và client nối lại được.”
  • “Trên GPU tôi dùng continuous batching, phía trước có hàng đợi ưu tiên theo gói để cắt tải Free trước.”
  • “Hạn mức tính theo token: trừ trước theo max_tokens, quyết toán theo số token thật khi sinh xong.”
Bẫy hay gặp
  • Tính số máy theo req/s như API thường và quên rằng mỗi request chiếm GPU hơn 10 giây.
  • Gửi toàn bộ lịch sử hội thoại cho mô hình mỗi lượt.
  • Để load balancer cắt SSE ở timeout mặc định 60 s.
  • Không dừng sinh khi client bỏ đi, GPU vẫn chạy cho người không còn đọc.

Đọc thêm

Cập nhật realtime
Đẩy thay đổi tới client trong vài trăm mili giây: chọn giữa polling, long polling, SSE, WebSocket, rồi fan-out qua nhiều gateway, xử lý reconnect và backpressure.
Tác vụ chạy lâu
Cách xử lý việc mất vài giây tới vài giờ mà không giữ request: nhận job, trả 202 kèm job id, worker xử lý, báo kết quả qua polling, webhook hoặc SSE. Kèm hàng đợi, retry có backoff và jitter, DLQ, idempotency, heartbeat, lập lịch, workflow engine và cách tính số worker.
Redis
Redis là kho key-value trong RAM với nhiều cấu trúc dữ liệu: dùng làm cache, bộ đếm, leaderboard, hàng đợi, GEO và pub/sub. Bài này đi qua mô hình dữ liệu, persistence, replication, Cluster, Lua và cách tính RAM.
Cassandra
Cassandra là database wide-column phân tán, ghi rất nhanh nhờ LSM tree, mở rộng gần tuyến tính bằng ring và consistent hashing. Bài này giải thích partition key, clustering column, cách mô hình hoá theo câu truy vấn, tunable consistency và những cái bẫy như tombstone.
Vector database
Vector database lưu embedding và tìm các vector gần nhất trong vài mili giây. Bài này đi qua độ đo tương đồng, index HNSW và IVF, pgvector so với Qdrant, Milvus, Pinecone, bài toán lọc metadata, RAG và cách tính RAM.
API Gateway
API gateway là cửa vào duy nhất cho client: định tuyến, xác thực, rate limit, gộp response và kết thúc TLS. Bài này phân biệt gateway với load balancer và service mesh, giải thích pattern BFF, so sánh Kong, Envoy, AWS API Gateway, Nginx và cách tính overhead, sizing.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.