Bài 2 đã đo được giới hạn cứng của một server: knee ở khoảng 4 kết nối đồng thời, và với handler chiếm CPU thì throughput đứng yên ở 200 req/s ngay từ một kết nối. Dù code hoàn hảo, một tiến trình vẫn là một tiến trình — và một điểm chết.

Bài này đặt nginx lên phía trước, nhân app thành ba replica, rồi đo lại. Kết quả có hai điều phản trực giác đến mức mình phải chạy lại nhiều lần để chắc: một thuật toán phân phối cho biểu đồ hoàn toàn cân bằng nhưng p95 tệ hơn 340 lần, và graceful shutdown — thứ mọi tài liệu đều khuyên — hoá ra không tạo khác biệt nào đo được trong trường hợp phổ biến nhất.

ℹ️ Cấu hình máy đo cho mọi số liệu trong bài này
Apple M1 Max, 10 lõi, 32 GB RAM, macOS 26.5.2, Docker 29.6.2. Container native arm64 (Node v22.23.2). Mỗi app replica giới hạn 1 CPU / 256 MB, nginx 1.27-alpine, bộ đo tải 2 CPU. Mọi thí nghiệm đều được lặp lại ít nhất 3 lần và bài ghi cả khoảng biến động, không lấy một lần chạy làm kết luận.

3.1 Scale dọc và scale ngang

Scale dọc (scale up) là cho máy khoẻ hơn: thêm CPU, thêm RAM. Scale ngang (scale out) là thêm máy. Hai hướng này không tương đương, và lựa chọn sai hướng là một trong những sai lầm đắt nhất.

Scale dọc thắng ở sự đơn giản: không phải nghĩ về phân tán, không mất tính nhất quán, không thêm hop mạng. Nhưng nó có ba giới hạn cứng:

  • Trần vật lý. Không tồn tại máy mạnh vô hạn.
  • Giá tăng phi tuyến. Máy gấp đôi hiệu năng thường đắt hơn gấp đôi, nhất là ở phân khúc cao cấp.
  • Vẫn là một điểm chết. Đây là giới hạn nghiêm trọng nhất và không thể mua để khắc phục — máy 128 lõi chết thì hệ thống vẫn chết.

Scale ngang gỡ được cả ba, nhưng có một điều kiện tiên quyết không thương lượng: ứng dụng phải stateless. Cụ thể là không giữ trong RAM tiến trình bất cứ thứ gì mà request sau cần đọc lại — session, cache cục bộ, bộ đếm, kết quả tính dở.

🕳️ Cạm bẫy: scale ngang một app còn giữ state trong RAM
Đây là loại bug khó chịu nhất trong nghề, vì nó không làm hệ thống sập — nó chỉ sai thỉnh thoảng. Người dùng đăng nhập ở replica 1, request tiếp theo rơi vào replica 2 và bị coi là chưa đăng nhập. Với 3 replica thì xác suất "đúng" là 1/3, nên lỗi xảy ra khoảng 2/3 số lần — nhưng vì bạn thử trên máy dev một replica nên nó không bao giờ tái tạo được.

Dấu hiệu nhận biết: bug biến mất khi scale về 1 replica. Nếu thấy triệu chứng đó, đừng đi tìm race condition — hãy đi tìm state cục bộ.

3.2 L4 và L7: load balancer "thấy" được gì

Khác biệt cốt lõi giữa hai tầng không phải tốc độ mà là lượng thông tin mà load balancer đọc được — và cái giá phải trả để đọc nó.

L4 vs L7 — cùng một request, hai mức "nhìn thấy" L4 — tầng vận chuyển (TCP) ĐỌC ĐƯỢC: IP nguồn / đích · cổng KHÔNG ĐỌC ĐƯỢC: đường dẫn URL · header · cookie → rẻ, nhanh, không giải mã TLS L7 — tầng ứng dụng (HTTP) ĐỌC ĐƯỢC: tất cả của L4, cộng thêm: URL · header · cookie · method → định tuyến theo path, A/B test, TLS termination → phải giải mã TLS: tốn CPU, thành tầng phải scale riêng Cùng một gói tin, phần nào nằm trong "tầm nhìn" của mỗi loại: TCP header 10.0.0.5:443 → app:3000 HTTP payload (bị mã hoá nếu là HTTPS) GET /api/orders/42 · Host: shop.vn · Cookie: sid=abc · Authorization: Bearer … L4 chỉ thấy đến đây L7 thấy hết — nhưng phải giải mã TLS mới thấy được Lab của series này dùng nginx ở chế độ L7, vì ta cần định tuyến theo path và ghi log biết request đi tới replica nào.
Không có lựa chọn "tốt hơn" tuyệt đối: L4 cho thông lượng thô, L7 cho khả năng ra quyết định.
🕳️ Cạm bẫy: chọn L7 cho mọi thứ rồi ngạc nhiên vì LB thành bottleneck
L7 phải giải mã TLS để đọc được HTTP. Với lưu lượng lớn, chi phí đó biến chính load balancer thành thành phần cần scale — và nó lại là thành phần duy nhất mọi request phải đi qua. Nếu bạn không cần định tuyến theo nội dung (ví dụ chỉ chia tải cho một pool đồng nhất), L4 vừa rẻ hơn vừa bớt một điểm phải lo.

3.3 Thuật toán phân phối — và phép đo làm mình phải chạy lại nhiều lần

Bốn thuật toán thường gặp nhất:

Thuật toán Cách chọn replica Điểm yếu
Round Robin Lần lượt theo vòng Không biết replica nào đang bận — chia đều số lượng, không chia đều tải
Least Connections Replica có ít kết nối đang mở nhất Cần theo dõi trạng thái; "ít kết nối" chưa chắc là "ít tải"
Random Chọn ngẫu nhiên Phương sai lớn, dễ dồn cục ngẫu nhiên
Random of two choices Lấy ngẫu nhiên 2, chọn cái ít việc hơn Gần như không có — rẻ như random mà chất lượng gần least-conn

Khi mọi request tốn như nhau và mọi replica khoẻ như nhau, cả bốn cho kết quả gần giống nhau. Sự khác biệt chỉ lộ ra khi có lệch — và trong thực tế thì luôn có lệch: một replica vừa bị GC pause, một replica đang chạy trên máy chủ ồn ào hơn, một replica vừa restart nên cache còn lạnh.

Thí nghiệm: một replica bị suy giảm

Đây là kịch bản tất định và rất thật: ba replica, trong đó một replica bị chiếm event loop (mình bơm tải /slow-sync?ms=30 trực tiếp vào app3, không qua load balancer). Rồi đo /fast qua load balancer. Lặp 3 lần cho mỗi thuật toán:

Thuật toán Throughput p50 p95 p99
Round Robin 147 req/s 0,50 ms 119,64 ms 149,70 ms
215 req/s 0,43 ms 119,42 ms 119,79 ms
145 req/s 0,41 ms 120,91 ms 149,49 ms
Least Connections 6.511 req/s 0,23 ms 0,35 ms 2,64 ms
5.892 req/s 0,23 ms 0,35 ms 59,11 ms
8.883 req/s 0,23 ms 0,35 ms 0,64 ms

Đọc bảng này chậm lại một chút, vì nó chứa ba điều đáng giá:

  • Throughput chênh khoảng 40 lần (~170 req/s so với ~7.100 req/s). Cùng phần cứng, cùng mã nguồn, khác đúng một dòng cấu hình.
  • p95 chênh khoảng 340 lần (~120 ms so với 0,35 ms). Với round robin, cứ khoảng một phần ba request bị đưa vào replica đang bị chặn và phải chờ tới lượt.
  • Nhưng p50 gần như y hệt (0,4–0,5 ms so với 0,23 ms). Đây là phần nguy hiểm: nếu bảng điều khiển của bạn hiển thị latency trung vị, thảm hoạ này hoàn toàn vô hình.
Cùng một replica bị suy giảm — hai cách phản ứng Round Robin — chia đều SỐ LƯỢNG app1 11 request · khoẻ · 0,4 ms app2 10 request · khoẻ · 0,4 ms app3 9 request · ĐANG BỊ CHẶN · ~120 ms Biểu đồ "số request mỗi node" trông cân bằng hoàn hảo (11 / 10 / 9) — không có gì báo động. Nhưng 1/3 người dùng phải chờ ~120 ms. Kết quả: 147 req/s, p95 = 119,6 ms. Least Connections — chia theo VIỆC ĐANG LÀM app1 nhận phần lớn tải · 0,23 ms app2 nhận phần lớn tải · 0,23 ms app3 bị TRÁNH vì đang giữ nhiều kết nối Biểu đồ trông LỆCH — nhưng kết quả: 6.511 req/s, p95 = 0,35 ms. Lệch mới là đúng.
Số request mỗi node là số đếm thật qua load balancer khi app3 đang bị chặn.
🕳️ Cạm bẫy: dùng "số request mỗi node" để đánh giá cân bằng tải
Mình đã đo đúng điều này: với round robin và app3 đang bị chặn hoàn toàn, phân phối vẫn là 11 / 10 / 9 trên 30 request — cân bằng gần như hoàn hảo. Một bảng điều khiển hiển thị "requests per node" sẽ hiện ba cột bằng nhau, xanh hết, trong khi throughput đã sụp 40 lần.

Round robin luôn chia đều số lượng — đó là định nghĩa của nó. Vậy nên chỉ số đó không mang thông tin gì về cân bằng tải. Hãy nhìn utilizationlatency theo replica. Và ngược lại: khi least-conn hoạt động đúng, biểu đồ số lượng sẽ trông lệch — đừng "sửa" nó.

Cài đặt bốn thuật toán

Chỉ vài dòng mỗi thuật toán. Đây là mã thật đang chạy trong bộ mô phỏng của series (sysdesign-sim-engine.js), nên bạn có thể đối chiếu hành vi mô phỏng với lab:

four_lb_algorithms.js
// `alive` = list of replicas still alive. Each replica has: busy (in-flight count), queueDepth.
function pickReplica(alive, policy, rng) {
  if (alive.length === 0) return null;      // whole tier is dead => must drop, cannot hang
  if (alive.length === 1) return alive[0];

  switch (policy) {
    case 'round-robin': {
      const r = alive[this.rrCursor % alive.length];
      this.rrCursor++;
      return r;                              // does NOT read replica state at all
    }

    case 'least-connections': {
      let best = alive[0];
      for (const r of alive) {
        if (r.busy + r.queueDepth < best.busy + best.queueDepth) best = r;
      }
      return best;                           // reads state => avoids the congested node
    }

    case 'random':
      return alive[Math.floor(rng.next() * alive.length)];

    case 'random-two-choices': {
      // "Power of two choices": sample 2 replicas at random, keep the less busy one.
      // Cheap like random, but balance quality is close to least-connections.
      const a = alive[Math.floor(rng.next() * alive.length)];
      const b = alive[Math.floor(rng.next() * alive.length)];
      return a.busy + a.queueDepth <= b.busy + b.queueDepth ? a : b;
    }
  }
}

Bốn nhánh switch ở trên là toàn bộ khác biệt giữa các thuật toán: hai nhánh cuối (least-connections, random-two-choices) đọc trạng thái replica trước khi chọn, hai nhánh đầu (round-robin, random) thì không hề biết replica nào đang bận. Trong nginx thì chỉ là một dòng trong khối upstream (khối khai báo nhóm server backend mà nginx phân phối tải tới):

nginx/lb.conf
upstream app_pool {
    # nginx defaults to round robin (nothing to declare for it).
    # Uncomment the line below to switch to least connections, then RE-MEASURE:
    least_conn;

    server app1:3000 max_fails=2 fail_timeout=5s;
    server app2:3000 max_fails=2 fail_timeout=5s;
    server app3:3000 max_fails=2 fail_timeout=5s;

    # Persistent connections to upstream: without this, nginx opens a new TCP
    # connection for EVERY request, and most of the latency you measure will
    # just be the internal TCP handshake cost.
    keepalive 64;
}
🕳️ Cạm bẫy trong lúc làm lab: nginx -s reload đọc file bị hụt byte
Lỗi này xảy ra thật khi mình đổi thuật toán để đo. Sửa file cấu hình tại chỗ (bằng sed -i) trong khi file đang được bind-mount vào container làm nginx đọc phải trạng thái nửa vời:

nginx: [emerg] pread() returned only 2830 bytes instead of 2832

Nguy hiểm ở chỗ reload thất bại nhưng nginx vẫn chạy với cấu hình cũ. Mình đã đo được một bộ số "sau khi đổi thuật toán" mà thực chất vẫn là round robin — và phải bỏ đi.

Cách đúng: ghi file kiểu nguyên tử (ghi ra file tạm rồi mv) hoặc restart container, và luôn xác nhận cấu hình đang có hiệu lực thay vì tin rằng nó đã đổi:
docker compose exec lb grep -c '^ *least_conn;' /etc/nginx/conf.d/default.conf

3.4 Health check, failover và graceful shutdown

Load balancer chỉ hữu ích nếu nó biết replica nào còn sống. Có hai cách biết, và nginx bản mã nguồn mở chỉ có một:

  • Chủ động (active): load balancer định kỳ gọi /health. Phát hiện sớm, rút node ra trước khi có request nào bị ảnh hưởng. Đây là tính năng của nginx Plus, không có trong bản mã nguồn mở.
  • Thụ động (passive): đếm lỗi thực tế. max_fails=2 fail_timeout=5s nghĩa là sau 2 lần thất bại trong 5 giây thì tạm loại node đó. Hệ quả quan trọng: vài request đầu tiên sau khi node chết vẫn phải thất bại — đó là cái giá của việc phát hiện.

Nhưng biết replica nào chết mới là nửa đầu câu chuyện. Nửa sau là graceful shutdown: khi chính replica đó chủ động dừng (deploy, scale down, lệnh vận hành), nó không nên thoát ngay lập tức. Trình tự đúng: tự đánh dấu unhealthy để load balancer thôi gửi request mới tới, rồi drain — đợi các request đang xử lý dở hoàn tất — rồi mới thoát tiến trình. Tín hiệu Unix kích hoạt bước này thường là SIGTERM (yêu cầu dừng êm ái, tiến trình còn cơ hội tự dọn dẹp trước khi thoát), khác với SIGKILL (ngắt ngay lập tức, không có cơ hội dọn dẹp). docker compose stop gửi SIGTERM trước, và chỉ gửi SIGKILL nếu tiến trình không tự thoát sau một khoảng thời gian chờ.

Thí nghiệm: dừng một replica trong lúc đang có tải

Chạy tải /fast qua load balancer với 8 kết nối, đến giây thứ 5 thì docker compose stop app3. Mình kỳ vọng thấy một loạt lỗi 502. Kết quả thực tế khác hẳn:

Chỉ số Có graceful shutdown Không graceful shutdown
Lỗi mà client nhìn thấy 0 0
Mã trạng thái toàn bộ 200 toàn bộ 200
p99 0,92 ms 0,90 ms
Latency lớn nhất 2.011 ms 2.011 ms
Số request nginx phải thử lại upstream khác 27 31
Số request mất ≥ 1 giây 29 27

Hai phát hiện, và cả hai đều đi ngược điều mình dự đoán trước khi đo.

Thứ nhất: không có lỗi nào. Nguyên nhân nằm trong chính cấu hình proxy_next_upstream — nginx tự thử lại request ở upstream khác. Log của nginx cho thấy rõ từng trường hợp:

log nginx — request được thử lại
172.21.0.6 "GET /fast HTTP/1.1" status=200 \
  to=172.21.0.2:3000, 172.21.0.4:3000 \
  ut=2.001, 0.000  rt=2.001  tries=504, 200

# How to read this line:
#   to=   two upstreams => already retried
#   ut=   first upstream took 2.001s (proxy_connect_timeout expired), second took ~0s
#   tries=504, 200  => first attempt timed out, second attempt succeeded
#   status=200      => CLIENT NEVER SAW AN ERROR, only extra latency

Đây là tin tốt và tin xấu cùng lúc. Tin tốt: retry cứu được tính sẵn sàng. Tin xấu: nó biến mất mát tính sẵn sàng thành mất mát độ trễ, và độ lớn của mất mát đó bằng đúng proxy_connect_timeout mà bạn đặt — ở đây là 2 giây. Nếu bạn chỉ theo dõi tỉ lệ lỗi, bạn sẽ kết luận "deploy hoàn toàn êm" trong khi ~30 người dùng vừa phải chờ 2 giây.

Thứ hai: graceful shutdown không tạo khác biệt nào đo được. 27 so với 31 request bị thử lại là nằm trong nhiễu. Điều này thoạt nghe như phản bác mọi tài liệu vận hành, nhưng lý do rất đơn giản: /fast hoàn thành trong 0,2 ms, nên tại thời điểm nhận SIGTERM hầu như không có request nào đang bay để mà drain. Log của app xác nhận: "đánh dấu unhealthy, chờ 0 request đang xử lý..."

Vậy graceful shutdown có ích khi nào? Đo lại với request dài

Lặp lại đúng thí nghiệm nhưng đổi sang /slow-async?ms=500 — request đủ dài để luôn có vài request đang bay khi node bị dừng:

Chỉ số (request 500 ms) Có graceful shutdown Không graceful shutdown
Lỗi client nhìn thấy 0 0
Latency lớn nhất 860,78 ms 2.514,83 ms
Log của app khi nhận SIGTERM "chờ 5 request đang xử lý..." → drain xong → thoát "thoát ngay, request đang xử lý bị cắt"

Giờ khác biệt hiện ra rõ: 861 ms so với 2.515 ms — đuôi latency tệ hơn khoảng 2,9 lần. Năm request đang bay bị cắt giữa đường; nginx phải thử lại chúng ở một upstream khác, và phần lớn chi phí đó là 2 giây chờ hết proxy_connect_timeout trước khi thử lại, cộng thêm thời gian làm lại phần việc dở dang.

Dừng một replica đang phục vụ request 500 ms Có graceful shutdown — max 861 ms SIGTERM 5 request đang bay được CHẠY XONG rồi mới thoát Trình tự: đánh dấu unhealthy → chờ drain → thoát. Không request nào bị cắt, không phải làm lại. Không graceful shutdown — max 2.515 ms SIGTERM → thoát ngay bị CẮT giữa đường nginx chờ hết proxy_connect_timeout = 2 s làm lại 500 ms Phần lớn chi phí là 2 s chờ proxy_connect_timeout rồi làm lại phần việc dở dang — khớp với latency lớn nhất đo được: 2.515 ms. Client vẫn nhận 200 — nên nếu chỉ theo dõi tỉ lệ lỗi thì bạn không thấy gì cả.
Cả hai mốc latency lớn nhất đều là số đo thật từ lab.
🔬 Đào sâu: graceful shutdown một mình là chưa đủ
App trong lab làm đúng ba bước: đánh dấu unhealthy → chờ drain → thoát. Nhưng nginh bản mã nguồn mở không hỏi /health bao giờ, nên việc "đánh dấu unhealthy" chẳng ai nghe thấy. Đó chính là lý do vẫn còn ~30 request phải trả giá 2 giây.

Trình tự deploy đúng phải là: rút node khỏi load balancer trước (qua API của LB, hoặc chờ đủ một chu kỳ health check chủ động), rồi mới gửi SIGTERM, rồi drain, rồi thoát. Thiếu bước đầu thì graceful shutdown chỉ giải quyết được nửa vấn đề.

Đây cũng là lý do timeout là một trong những tham số quan trọng nhất của hệ phân tán — Bài 17 sẽ quay lại đúng con số 2 giây này.
🕳️ Cạm bẫy: health check chỉ kiểm tra cổng TCP
Nhiều cấu hình chỉ kiểm tra "cổng 3000 có mở không". Một tiến trình Node bị chặn event loop vẫn mở cổng, vẫn hoàn tất bắt tay TCP — nên vẫn được coi là khoẻ, đúng lúc nó hoàn toàn không phục vụ được ai. Đó chính là app3 trong thí nghiệm ở mục 3.3: sống theo mọi tiêu chí TCP, nhưng p95 là 120 ms.

/health phải thực sự thực thi được một chút logic, và tốt hơn nữa là kiểm tra cả dependency thiết yếu. Nhưng đừng thái quá: nếu /health trả lỗi chỉ vì database chậm, cả pool sẽ bị rút ra cùng lúc và bạn tự gây ra sự cố toàn phần.

3.5 Sticky session và cái giá của state cục bộ

Khi app giữ session trong RAM, cách sửa nhanh nhất là bắt mỗi người dùng luôn về đúng một replica — sticky session. Trong nginx chỉ cần một dòng ip_hash;. Nó chạy được, và đó chính là vấn đề.

Hệ quả Vì sao xảy ra
Mất cân bằng tải Người dùng không sinh ra lượng tải như nhau; một replica có thể trúng toàn người dùng nặng
Mất session khi replica chết State chỉ nằm trong RAM của replica đó — chết là mất, người dùng bị đăng xuất
Chặn autoscale Thêm replica không giúp gì cho người dùng đã bị ghim; bớt replica là mất session
Deploy nào cũng gây gián đoạn Mỗi lần restart replica là toàn bộ người dùng ghim vào nó bị ảnh hưởng
Sai lệch với NAT ip_hash ghim theo IP, nên cả một công ty sau NAT bị dồn vào một replica

Lời giải đúng là đưa session ra store dùng chung — Redis, hoặc token có chữ ký để không cần lưu gì cả. Khi đó app thật sự stateless và mọi vấn đề trên tan biến. Bài 5 sẽ dựng đúng tầng đó.

🕳️ Cạm bẫy: dùng sticky session để che lỗi thiết kế
Sticky session là công cụ hợp lệ trong vài tình huống hẹp (ví dụ WebSocket có state kết nối, hoặc cache cục bộ nóng mà mất thì chỉ chậm chứ không sai). Nhưng khi nó được bật để tránh phải sửa state cục bộ, hệ thống sẽ chạy được ngay hôm nay và tự khoá khả năng scale về sau. Cái giá không xuất hiện lúc bật — nó xuất hiện sáu tháng sau, khi bạn cần autoscale và phát hiện mình không thể.

Tự làm lại các phép đo

terminal
# 1) Bring up nginx + 3 replicas
cd blog/sysdesign/sysdesign-lab
docker compose --profile lb up -d
sleep 10                      # IMPORTANT: let all 3 replicas finish warming up

# 2) Check the distribution (round robin => counts should be roughly equal)
for i in $(seq 1 30); do curl -s http://localhost:8080/whoami; echo; done \
  | grep -o '"instance":"app[0-9]"' | sort | uniq -c

# 3) DEGRADE exactly one replica: hit app3 directly, bypassing the LB
docker compose run --rm -d --name hog loadgen \
  loadgen.js --url "http://app3:3000/slow-sync?ms=30" -c 3 -d 25 -w 0

# 4) Measure /fast through the LB while app3 is blocked
docker compose run --rm loadgen \
  loadgen.js --url http://lb:8080/fast -c 6 -d 10 -w 3

# 5) Switch to least_conn, then RE-MEASURE. Write the file atomically, not with sed -i:
sed 's/^    # least_conn;/    least_conn;/' nginx/lb.conf > /tmp/lb.new \
  && mv /tmp/lb.new nginx/lb.conf
docker compose restart lb
# CONFIRM the config actually took effect before trusting the next measurement:
docker compose exec lb grep -c '^ *least_conn;' /etc/nginx/conf.d/default.conf

# 6) Count requests nginx retried, and requests that took >= 1 second
docker compose logs --no-log-prefix lb | grep -c 'to=[^ ]*,'
docker compose logs --no-log-prefix lb | grep -oE 'rt=[0-9.]+' \
  | awk -F= '$2>=1{n++} END{print n+0}'

Ba câu hỏi nên tự trả lời sau khi chạy: phân phối số lượng có cân bằng không (nên là có), p95 chênh bao nhiêu giữa hai thuật toán, và p50 có phản ánh được vấn đề không (nên là không).

Đối chiếu với mô phỏng

Traffic Lab dưới đây cho phép đổi thuật toán phân phối và giết replica ngay trên trang. Chọn kiến trúc "2. LB + nhiều app server", đặt tải đủ cao, rồi bấm "Giết 1 app server" để xem hàng đợi dồn sang các replica còn lại và utilization nhảy lên.

Ba replica rồi — nhưng chúng đang giành nhau cái gì?

Ta đã gỡ được điểm chết duy nhất và nhân throughput lên. Nhưng để ý một điều trong toàn bộ bài này: cả ba replica đều đọc cùng một nguồn dữ liệu. Scale tầng stateless là phần dễ; nó chỉ đẩy điểm nghẽn xuống tầng sau.

Bài 4 sẽ mở rộng nginx từ load balancer thành API gateway — định tuyến theo path, TLS termination, và một cạm bẫy bảo mật cụ thể: vì sao không bao giờ được tin header X-Forwarded-For do client gửi lên.

📖 Tài liệu tham khảo

Tải mã nguồn thực hành

Cấu hình nginx L7 dùng trong bài (có log to=$upstream_addr để tự đếm phân phối) và lõi mô phỏng chứa cài đặt bốn thuật toán phân phối:

Tải về lb.conf Tải về sysdesign-sim-engine.js

Bài viết liên quan trong series

Bài 2: Dựng Lab & Đo Giới Hạn Một Server Bài 4: Reverse Proxy & API Gateway Quay lại Lộ trình Thiết Kế Hệ Thống

Bình luận