"Không có thiết kế nào đúng — chỉ có thiết kế phù hợp với yêu cầu và quy mô hiện tại. System design không phải là nhớ thật nhiều công nghệ, mà là biết đánh đổi cái gì để lấy cái gì."

Đây là bài nhập môn thiết kế hệ thống (System Design) — thứ tưởng mông lung nhưng thật ra xoay quanh vài viên gạch nền tảng lặp đi lặp lại. Bài này đi qua bảy viên gạch đó: mở rộng (scaling), cân bằng tải, bộ nhớ đệm (caching), CDN, cơ sở dữ liệu (SQL/NoSQL, nhân bản, phân mảnh), định lý CAP, và hàng đợi thông điệp. Hiểu chúng, bạn ghép được phần lớn hệ thống thực tế.

Tư tưởng chung

Phần 0 — System design là nghệ thuật đánh đổi

Không có đáp án đúng tuyệt đối, chỉ có đánh đổi phù hợp.

Mọi quyết định thiết kế đều là một sự đánh đổi giữa các đại lượng đối nghịch: độ trễ (latency) và thông lượng (throughput), tính nhất quán (consistency) và tính sẵn sàng (availability), chi phí và độ phức tạp. Thêm cache thì nhanh hơn nhưng phải lo dữ liệu cũ; phân mảnh DB thì mở rộng được nhưng truy vấn chéo mảnh trở nên khó.

Vì vậy nguyên tắc số một: bắt đầu từ yêu cầu, không phải từ công nghệ. Đừng vội "dùng Kafka, dùng Redis" — hãy hỏi hệ thống cần gì trước đã.

Chân lý nền tảng

Không có "kiến trúc tốt nhất", chỉ có kiến trúc tốt nhất cho yêu cầu và quy mô này. Cùng một bài toán, quy mô 1.000 người dùng và 100 triệu người dùng đòi hỏi hai thiết kế hoàn toàn khác nhau.

Bước khởi đầu

Phần 1 — Yêu cầu & ước lượng nhanh

Chốt yêu cầu, rồi ước lượng con số trước khi vẽ kiến trúc.

Trước khi vẽ bất cứ hộp nào, hãy tách hai loại yêu cầu:

  • Chức năng (functional): hệ thống làm gì — đăng bài, gửi tin nhắn, rút gọn URL...
  • Phi chức năng (non-functional): hệ thống chạy tốt cỡ nào — bao nhiêu người dùng, độ trễ mục tiêu, mức sẵn sàng (99,9%?), tỉ lệ đọc/ghi.

Sau đó ước lượng nhanh (back-of-envelope) để biết mình đang thiết kế cho quy mô nào:

# Ước lượng cho một mạng xã hội 100 triệu người dùng
DAU         = 100_000_000 * 0.20        # 20% hoạt động mỗi ngày = 20 triệu
req_per_day = DAU * 20                  # mỗi người ~20 request/ngày = 400 triệu
QPS         = req_per_day / 86_400      # ~4.600 request/giây (trung bình)
peak_QPS    = QPS * 3                   # giờ cao điểm gấp ~3 = ~14.000 QPS
Con số nên thuộc

Một ngày ≈ 86.400 giây ≈ 10⁵ giây. Nhờ đó "1 triệu request/ngày" ≈ 12 QPS trong đầu. Đa số hệ thống đọc nhiều hơn ghi rất nhiều (tỉ lệ 10:1, 100:1) — đây là lý do caching và replica đọc quan trọng đến vậy.

Phần A · Mở rộng & tốc độ

Scaling

A.1 — Scale dọc vs scale ngang

Nâng cấp một máy, hay thêm nhiều máy?

Khi hệ thống quá tải, có hai hướng lớn:

  • Scale dọc (vertical / scale up): nhét thêm CPU, RAM, ổ đĩa vào cùng một máy. Đơn giản, không phải đổi code — nhưng có trần cứng và máy đó là điểm hỏng duy nhất.
  • Scale ngang (horizontal / scale out): thêm nhiều máy chạy song song. Mở rộng gần như vô hạn và chịu lỗi tốt — nhưng cần cân bằng tải và dịch vụ phải không lưu trạng thái (stateless).
Tiêu chí Scale dọc (up) Scale ngang (out)
Độ phức tạp Thấp (không đổi code) Cao (LB, stateless, đồng bộ)
Giới hạn Trần phần cứng Gần như vô hạn
Chịu lỗi Kém (1 điểm hỏng) Tốt (dư thừa)
Chi phí Tăng vọt khi lên cao Máy phổ thông, tuyến tính
Mẹo nhớ

Scale dọc tới khi không thể, rồi scale ngang. Máy đơn dễ vận hành, nên nâng cấp máy là bước đầu hợp lý; nhưng để phục vụ triệu người và không bao giờ sập, sớm muộn cũng phải scale ngang.

Scaling

A.2 — Load balancer (cân bằng tải)

Người gác cổng chia đều lưu lượng cho nhiều máy chủ.

Load balancer đứng trước một cụm máy chủ và phân phối request đến chúng, để không máy nào quá tải. Nó cũng khám sức khỏe (health check): máy nào chết thì tự loại khỏi vòng quay → xóa điểm hỏng duy nhất.

Vài thuật toán phân phối phổ biến: round-robin (lần lượt), least connections (ưu tiên máy ít kết nối nhất), IP hash (cùng người dùng luôn về một máy). Người ta phân biệt L4 (cân bằng theo TCP/IP, nhanh) và L7 (theo nội dung HTTP, thông minh hơn — định tuyến theo đường dẫn, cookie...).

Tốc độ

A.3 — Caching (bộ nhớ đệm)

Giữ dữ liệu nóng ở nơi truy cập nhanh để khỏi tính/đọc lại.

Cache lưu dữ liệu hay dùng ở bộ nhớ nhanh (thường là RAM — Redis, Memcached) để tránh đọc lại DB chậm hay tính toán lại. Đây là cách rẻ và hiệu quả nhất để giảm độ trễ và gánh nặng cho DB.

Mẫu phổ biến nhất là cache-aside (lazy loading):

def get_user(user_id):
    data = cache.get(user_id)          # 1. thử cache trước
    if data is not None:
        return data                    # cache hit → trả ngay
    data = db.query(user_id)           # 2. miss → đọc DB
    cache.set(user_id, data, ttl=300)  # 3. ghi lại cache (hết hạn sau 5 phút)
    return data

Khi cache đầy, phải trục xuất (eviction) theo chính sách LRU (bỏ cái lâu chưa dùng), LFU (bỏ cái ít dùng), hoặc TTL (hết hạn theo thời gian). Có thể cache ở nhiều tầng: trình duyệt, CDN, tầng ứng dụng, và ngay trong DB.

Khó nhất là làm mới

"Có hai việc khó trong khoa học máy tính: đặt tên, và vô hiệu hóa cache (cache invalidation)." Cache nhanh, nhưng nếu dữ liệu gốc đổi mà cache không đổi, người dùng thấy dữ liệu cũ. Luôn có chiến lược làm mới (TTL ngắn, xóa cache khi ghi) và đề phòng cache stampede (cache hết hạn đồng loạt, cả triệu request đổ dồn xuống DB cùng lúc).

Tốc độ

A.4 — CDN (mạng phân phối nội dung)

Bộ nhớ đệm đặt gần người dùng về mặt địa lý.

CDN (Content Delivery Network) là mạng máy chủ đặt rải khắp thế giới, lưu bản sao nội dung tĩnh (ảnh, JS, CSS, video) ở "biên (edge)" gần người dùng. Người ở Việt Nam tải ảnh từ máy chủ tại Việt Nam thay vì bay nửa vòng Trái Đất sang Mỹ → độ trễ giảm mạnh, đồng thời giảm tải cho máy chủ gốc.

Có hai kiểu: pull (CDN tự lấy nội dung từ gốc lần đầu có người yêu cầu, rồi cache lại) và push (bạn chủ động đẩy nội dung lên CDN). Đa số web dùng pull cho tiện.

Khi nào dùng

Bất cứ thứ gì tĩnh và dùng chung cho nhiều người — ảnh, video, file tải, asset front-end — đều nên đặt sau CDN. Nội dung động, cá nhân hóa thì không hợp (nhưng vẫn có thể cache ngắn ở biên).

Phần B · Dữ liệu

Cơ sở dữ liệu

B.1 — SQL vs NoSQL

Quan hệ chặt chẽ, hay linh hoạt & dễ mở rộng?

SQL (quan hệ): dữ liệu ở bảng có lược đồ (schema) cố định, hỗ trợ giao dịch ACID, join, nhất quán mạnh. Tuyệt cho dữ liệu có cấu trúc và cần đúng tuyệt đối (tài chính, đơn hàng). Ví dụ: PostgreSQL, MySQL.

NoSQL: lược đồ linh hoạt, mở rộng ngang dễ, đổi lại thường chỉ nhất quán cuối (BASE). Nhiều loại: khóa-giá trị (Redis), tài liệu (MongoDB), cột rộng (Cassandra), đồ thị (Neo4j). Hợp cho dữ liệu lớn, ít cấu trúc, ghi nhiều.

Tiêu chí SQL (quan hệ) NoSQL
Lược đồ Cố định, chặt Linh hoạt
Nhất quán Mạnh (ACID) Thường là cuối (BASE)
Mở rộng Chủ yếu scale dọc Scale ngang tự nhiên
Hợp với Giao dịch, quan hệ phức tạp Dữ liệu lớn, đọc/ghi đơn giản, nhanh
Mẹo chọn

Chọn theo mẫu truy cập, không theo trào lưu. Cần giao dịch chặt và quan hệ phức tạp → SQL. Cần mở rộng khủng với truy vấn đơn giản theo khóa → NoSQL. Nhiều hệ thống thật dùng cả hai (polyglot persistence).

Cơ sở dữ liệu

B.2 — Replication (nhân bản)

Nhiều bản sao dữ liệu để sẵn sàng & đọc khỏe hơn.

Nhân bản là giữ nhiều bản sao dữ liệu trên nhiều máy. Lợi ích kép: nếu một máy chết vẫn còn máy khác (tính sẵn sàng), và có thể chia tải đọc ra nhiều bản sao.

Mô hình phổ biến là leader–follower (chính–phụ): mọi ghi đi vào leader, rồi lan sang các follower; đọc có thể lấy từ bất kỳ follower nào. Vì lan truyền cần thời gian, follower có thể trễ hơn leader một chút — gọi là độ trễ nhân bản (replication lag), dẫn tới đọc thấy dữ liệu hơi cũ (nhất quán cuối).

Cơ sở dữ liệu

B.3 — Sharding (phân mảnh)

Chia dữ liệu ra nhiều máy khi một máy không chứa nổi.

Khi dữ liệu (hoặc lượng ghi) vượt sức một máy, ta phân mảnh (sharding): chẻ dữ liệu thành nhiều phần đặt trên nhiều máy, theo một khóa mảnh (shard key). Ví dụ chia người dùng theo hash(user_id) % N, hoặc theo dải chữ cái tên.

Đây là cách duy nhất để mở rộng phần ghi và dung lượng vượt một máy — nhưng kèm cái giá: truy vấn chéo mảnh trở nên khó, dễ sinh điểm nóng (hotspot) nếu khóa lệch, và tái phân mảnh (resharding) rất đau. Kỹ thuật consistent hashing (băm nhất quán) giúp thêm/bớt máy mà xáo trộn ít dữ liệu nhất.

Chọn khóa mảnh cẩn thận

Khóa mảnh sai là ác mộng. Chọn khóa phân tán đều để không máy nào thành điểm nóng, và cố gắng để các truy vấn hay dùng chỉ chạm một mảnh. Đổi khóa mảnh sau này gần như phải di chuyển toàn bộ dữ liệu.

Cơ sở dữ liệu

B.4 — Định lý CAP

Khi mạng phân mảnh, chọn nhất quán hay sẵn sàng?

Định lý CAP nói: khi có phân vùng mạng (Partition) — các nhóm máy tạm mất liên lạc với nhau — một hệ phân tán chỉ có thể giữ một trong hai: Nhất quán (Consistency) hoặc Sẵn sàng (Availability), không thể cả hai.

  • CP (chọn nhất quán): khi phân vùng, từ chối phục vụ chỗ không chắc đúng, thà báo lỗi còn hơn trả dữ liệu sai. Hợp với ngân hàng, thanh toán.
  • AP (chọn sẵn sàng): vẫn phục vụ, chấp nhận dữ liệu có thể hơi cũ/khác nhau tạm thời rồi hội tụ sau. Hợp với bảng tin mạng xã hội, đếm lượt thích.
Tình huống Ưu tiên Vì sao
Số dư ngân hàng CP (nhất quán) Sai số tiền là thảm họa
Feed mạng xã hội AP (sẵn sàng) Thấy bài trễ vài giây không sao
Giỏ hàng Tùy (thường AP) Ưu tiên luôn thêm được, hòa giải sau
Thực tế

Phân vùng mạng là chuyện chắc chắn xảy ra, nên thật ra bạn luôn phải chọn trước C hay A. Mở rộng PACELC còn nhắc: cả khi không phân vùng, vẫn có đánh đổi giữa độ trễ (Latency) và nhất quán. Không có bữa trưa miễn phí.

Phần C · Giao tiếp

Bất đồng bộ

C.1 — Message queue & xử lý bất đồng bộ

Tách rời việc nặng ra khỏi luồng trả lời người dùng.

Hàng đợi thông điệp (message queue) — Kafka, RabbitMQ, SQS — đặt giữa bên gửi (producer) và bên xử lý (consumer). Thay vì làm việc nặng ngay trong request (gửi email, xử lý ảnh, xuất báo cáo), ứng dụng chỉ đẩy một thông điệp vào hàng đợi rồi trả lời người dùng ngay; các worker nền sẽ lấy ra xử lý sau.

Bốn lợi ích lớn: tách rời (producer và consumer không cần biết nhau), đệm tải (đỉnh lưu lượng dồn vào hàng đợi thay vì đánh sập DB), thử lại (xử lý lỗi thì để lại hàng đợi làm lại), và mở rộng (thêm worker khi hàng đợi dài).

Mẹo nhớ

Việc gì không cần trả lời ngay thì làm bất đồng bộ. Người dùng bấm "đăng bài" chỉ cần biết đã nhận; việc tạo thumbnail, bắn thông báo, cập nhật feed bạn bè... đẩy hết ra hàng đợi. Request trả về nhanh, hệ thống chịu tải mượt hơn.

Phần chung

Ghép lại

D — Kiến trúc một web điển hình

Ráp bảy viên gạch thành một sơ đồ hoàn chỉnh.

Ghép các mảnh trên lại, một web quy mô lớn thường trông như sau — theo dấu một request từ trình duyệt tới tận DB:

   Người dùng
       │
       ▼
   [ DNS ] ──▶ [ CDN ]  ◀── ảnh, JS, CSS, video (nội dung tĩnh)
       │
       ▼  (nội dung động)
   [ Load Balancer ]   health check · round-robin
       │
   ┌───┼───────────┐
   ▼   ▼           ▼
 [App1][App2] ... [AppN]   máy chủ STATELESS, scale ngang
   └───┬───────────┘
       │  đọc nóng          việc nặng, chạy nền
       ▼                    │
   [ Cache: Redis ]         ▼
       │              [ Message Queue ] ──▶ [ Workers ]
       ▼
   [ Database ]  Primary (ghi) + Replicas (đọc), đã sharding

Luồng: người dùng → DNS trỏ tới điểm gần nhất → nội dung tĩnh trả thẳng từ CDN; nội dung động qua load balancer tới một máy chủ stateless; máy chủ thử cache trước, miss thì đọc DB (replica cho đọc, primary cho ghi); việc nặng được đẩy vào hàng đợi cho worker xử lý nền.

Bức tranh lớn

Gần như mọi hệ thống web lớn đều là biến thể của sơ đồ này. Nắm sơ đồ này là bạn có "khung xương" để bắt đầu bất kỳ bài System Design nào, rồi tùy yêu cầu mà thêm/bớt từng khối.

Cạm bẫy

E — ☠️ Cạm bẫy thường gặp

Những lỗi khiến thiết kế đẹp trên giấy nhưng sập trong thực tế.

💀 Bẫy #1 — Tối ưu quá sớm / over-engineering. Chưa có người dùng đã bày microservices, Kafka, đa vùng. Bắt đầu đơn giản, mở rộng khi thật sự cần.

💀 Bẫy #2 — Điểm hỏng duy nhất (SPOF). Một load balancer, một DB, một cache — chết là sập cả hệ thống. Luôn có dự phòng cho thành phần trọng yếu.

💀 Bẫy #3 — Quên tầng dữ liệu. DB thường là nút thắt cổ chai đầu tiên. Web server thêm là xong, nhưng scale DB (replica, shard) mới là phần khó.

💀 Bẫy #4 — Máy chủ có trạng thái. Lưu session trên máy làm hỏng scale ngang (người dùng phải luôn về đúng máy cũ). Đẩy trạng thái ra ngoài (Redis/DB).

💀 Bẫy #5 — Không có chiến lược cache. Cache bừa dẫn tới dữ liệu cũ khó lường; hoặc cache stampede làm sập DB khi hết hạn đồng loạt.

💀 Bẫy #6 — Bỏ qua giám sát. Không log, không metric, không cảnh báo → không biết hệ thống đang khỏe hay ốm cho tới khi người dùng la làng.

💀 Bẫy #7 — Thiết kế cho sai quy mô. Không ước lượng con số nên hoặc phí tài nguyên, hoặc vỡ trận khi tải tăng. Luôn tính back-of-envelope trước.

Tra cứu

F — 📋 Bảng tra cứu thần tốc

Vấn đề → công cụ, và các con số nên thuộc.

Vấn đề Công cụ
Quá nhiều lưu lượng cho 1 máy Scale ngang + load balancer
Đọc chậm / DB quá tải đọc Cache + read replica
Người dùng ở xa, tải tĩnh chậm CDN
Dữ liệu/ghi vượt một máy Sharding
Việc nặng làm chậm request Message queue + worker nền
Cần đúng tuyệt đối (tiền) SQL + giao dịch ACID (nghiêng CP)

Vài con số độ trễ nên thuộc (bậc độ lớn):

   Đọc RAM              ~100 nano-giây
   Đọc SSD (ngẫu nhiên) ~100 micro-giây   (chậm hơn RAM ~1.000 lần)
   Round-trip trong DC  ~0,5 mili-giây
   Đọc đĩa HDD          ~10 mili-giây     (chậm hơn RAM ~100.000 lần)
   Mạng xuyên lục địa   ~150 mili-giây

Bốn câu hỏi khi gặp bài System Design: (1) Quy mô bao nhiêu (người dùng, QPS, dung lượng)? (2) Đọc nhiều hay ghi nhiều? (3) Cần nhất quán mạnh hay chấp nhận cuối? (4) Đâu là nút thắt cổ chai và làm sao mở rộng nó?

Lời kết

🎓 Tâm pháp System Design

Năm câu gói trọn tư duy thiết kế hệ thống cơ bản.

  1. Bắt đầu từ yêu cầu & con số, không từ công nghệ. Ước lượng QPS/dung lượng trước khi vẽ hộp.
  2. Scale dọc tới khi hết, rồi scale ngang với load balancer + máy chủ stateless.
  3. Đọc nhiều hơn ghi → cache + read replica. Đây là đòn bẩy hiệu năng lớn nhất và rẻ nhất.
  4. Vượt một máy → sharding; cần bền → replication. Và nhớ CAP: khi phân vùng, chọn C hay A.
  5. Việc gì không cần đồng bộ → đẩy ra hàng đợi. Request nhanh, hệ thống chịu đỉnh tải mượt.

System design không phải phép màu — nó là vài viên gạch nền tảng ghép lại một cách có chủ đích, luôn kèm câu hỏi "đánh đổi cái gì". Nắm chắc bảy viên gạch trong bài, tự hỏi bốn câu định hình mỗi lần thiết kế, và bạn sẽ ráp được phần lớn hệ thống thực tế. 🏗️

Một câu để nhớ

Mọi kỹ thuật ở đây đều phục vụ một mục tiêu: loại bỏ nút thắt cổ chai và điểm hỏng duy nhất, trong ngân sách cho phép. Thiết kế tốt không phải cái phức tạp nhất, mà là cái đơn giản nhất vẫn đáp ứng được yêu cầu.