Bài 3 đặt nginx lên trước ba replica và dùng nó cho đúng một việc: chia tải. Nhưng thành phần đứng ở vị trí đó — nơi mọi request đều phải đi qua — có thể làm nhiều hơn thế rất nhiều: kết thúc TLS, định tuyến theo đường dẫn, phục vụ file tĩnh, chặn request thiếu token, giới hạn tần suất. Bài này giữ nguyên nginx nhưng giao cho nó những việc đó, rồi đo xem mỗi việc đáng giá bao nhiêu.
Hai phép đo trong bài đi ngược lại điều mình dự đoán khi viết đề cương. Thứ nhất: câu "cho gateway phục vụ file tĩnh để app đỡ việc" hoá ra khiến gateway chậm hơn 2,6 lần so với việc chuyển tiếp request về app — cho tới khi thêm đúng một chỉ thị cấu hình. Thứ hai: "TLS tốn khoảng 10%" là con số mình đo được và cũng là con số hay được trích dẫn, nhưng nó chỉ đúng trong một điều kiện rất cụ thể; bỏ điều kiện đó ra thì chi phí thành gần 16 lần.
4.1 Ba cái tên cho cùng một vị trí
"Reverse proxy", "load balancer", "API gateway" thường được nói như ba sản phẩm khác nhau. Thực tế chúng là ba mức trách nhiệm gán cho cùng một vị trí trong hệ thống: chặng đứng giữa client và các service. Cùng một tiến trình nginx trong lab của series này lần lượt đóng cả ba vai.
Phân biệt thêm một cặp dễ lẫn: forward proxy và reverse proxy. Forward proxy đứng cạnh client và đại diện cho client (ví dụ proxy của công ty đi ra Internet); reverse proxy đứng cạnh server và đại diện cho server. Cùng công nghệ, ngược hướng, và bên nào cấu hình nó cũng khác nhau.
Lab của bài này
Lab dùng chung của series được mở thêm profile gw. Điểm quan trọng trong thiết kế lab: cổng
HTTP (8081) và cổng HTTPS (8443) include cùng một file route, nên chênh lệch đo được giữa hai
cổng chỉ đến từ TLS, không lẫn khác biệt cấu hình:
cd blog/sysdesign/sysdesign-lab
# 1. Generate a SELF-SIGNED cert for the gateway (lab only, never use it anywhere else)
mkdir -p nginx/certs
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout nginx/certs/lab.key -out nginx/certs/lab.crt \
-subj "/CN=localhost"
# 2. Start the gateway + 3 app replicas
docker compose --profile gw up -d
# 3. Check that both ports are alive
curl -s http://localhost:8081/gw-health # -> gateway ok
curl -sk https://localhost:8443/gw-health # -> gateway ok (-k: skip cert verification)
# 4. Gateway strips the /api/ prefix before forwarding
curl -s http://localhost:8081/api/whoami
# -> {"instance":"app1", ...} the app has NO idea the /api/ prefix even exists
Nếu cả hai lệnh curl ở bước 3 đều trả về gateway ok, gateway đang chạy đúng và
sẵn sàng cho phần đo tiếp theo. Bước 4 xác nhận điều quan trọng nhất của bài: gateway và app nói hai đường
dẫn khác nhau — client gọi /api/whoami, app chỉ thấy /whoami.
4.2 TLS termination và sự thật về X-Forwarded-For
TLS termination nghĩa là gateway giải mã HTTPS rồi nói HTTP thuần với các service phía sau. Lý do làm vậy rất thực dụng: chứng chỉ chỉ nằm ở một chỗ (gia hạn một lần thay vì N lần), service không cần biết gì về TLS, và gateway đọc được nội dung nên mới định tuyến theo path được.
TLS tốn bao nhiêu?
Cùng route /api/fast, 8 kết nối đồng thời, 8 giây đo, ba lần lặp — chỉ khác cổng vào là HTTP
hay HTTPS:
| Cổng | Throughput (req/s) | p50 | p99 |
|---|---|---|---|
| 8081 — HTTP | 22.950 / 22.799 / 21.939 | 0,33–0,35 ms | 0,70–0,76 ms |
| 8443 — HTTPS | 20.494 / 20.881 / 19.828 | 0,36 ms | 0,88–0,96 ms |
| Chênh lệch | thấp hơn ~9–10% | +0,02 ms | +0,18 ms |
Khoảng 10% throughput, và độ trễ tăng thêm nhỏ hơn nhiều so với thời gian một truy vấn database tầm thường nhất. Kết luận có vẻ rõ ràng: TLS rẻ, bật đi. Nhưng con số này che một điều kiện.
Con số 10% chỉ đúng khi có keep-alive
Bộ đo tải của lab mặc định dùng kết nối bền (keep-alive): mở 8 kết nối rồi gửi hàng trăm nghìn request qua chúng. Cái đắt của TLS là bắt tay — trao đổi khoá bất đối xứng, một hoặc hai vòng mạng — còn phần mã hoá đối xứng sau đó thì rất rẻ. Với keep-alive, chi phí bắt tay được chia cho hàng chục nghìn request nên gần như tan biến.
Bộ đo tải có cờ --no-keepalive để buộc mỗi request mở một kết nối mới. Cùng route, cùng số
kết nối đồng thời:
| Cấu hình | Throughput (req/s) | p50 | p99 |
|---|---|---|---|
| HTTP + keep-alive | ~22.600 | 0,34 ms | 0,73 ms |
| HTTPS + keep-alive | ~20.400 | 0,36 ms | 0,91 ms |
| HTTP, mỗi request một kết nối | 7.006 / 7.375 | 1,07–1,11 ms | 1,66–1,88 ms |
| HTTPS, mỗi request một kết nối | 1.290 / 1.231 | 5,96–6,21 ms | 12,97–13,39 ms |
Bỏ keep-alive đi, HTTP mất 3 lần throughput vì phải bắt tay TCP mỗi lần. Nhưng HTTPS mất gần 16 lần so với chính nó khi có keep-alive (20.400 req/s xuống còn khoảng 1.260 req/s), và p50 tăng từ 0,36 ms lên hơn 6 ms — gấp 17 lần. Đó là hình dạng thật của chi phí TLS: nó không nằm ở mỗi request, nó nằm ở mỗi kết nối.
Ba việc đáng làm, theo thứ tự hiệu quả: (1) bật
ssl_session_cache ở gateway để kết nối lại
dùng được phiên cũ — lab đã bật, nghĩa là con số gần 16 lần ở trên vẫn còn là lạc quan; (2) bật
HTTP/2 để nhiều request đi chung một kết nối — lab cũng đã bật (http2 on; ở cổng 8443, xem
nginx/gw.conf), nên chi phí đo được ở trên đã tính luôn cả lợi ích này; (3) kiểm tra client
của bạn có thật sự dùng connection pool hay không — đây là chỗ thường sai nhất và cũng dễ sửa nhất trong
code của chính bạn.
Sau khi giải mã, app mất thông tin gì?
Kết nối TCP mà app nhìn thấy là kết nối từ gateway, không phải từ client. Nên
socket.remoteAddress của app luôn là IP của gateway, và app cũng không còn biết client vào
bằng HTTP hay HTTPS. Thông tin đó phải được chuyển tiếp bằng header — và đây là chỗ sinh ra một lỗ hổng
rất phổ biến.
Lab có endpoint /client-ip tồn tại chỉ để chứng minh chuyện này. Chạy thật, gửi kèm một
header bịa:
curl -s -H 'X-Forwarded-For: 1.2.3.4' http://localhost:8081/api/client-ip
# {"instance":"app1",
# "remoteAddress":"::ffff:172.21.0.5", ← the GATEWAY's IP, not the client's
# "xForwardedForRaw":"1.2.3.4, 192.168.65.1", ← spoofed value + real IP
# "trustProxyHops":0,
# "trustedClientIp":"::ffff:172.21.0.5",
# "naiveClientIp":"1.2.3.4"} ← WRONG: took spoofed value verbatim
# ---- Fix 1: gateway OVERWRITES the header (route /safe-api/) ----
curl -s -H 'X-Forwarded-For: 1.2.3.4' http://localhost:8081/safe-api/client-ip
# "xForwardedForRaw":"192.168.65.1" ← spoofed value wiped out
# "naiveClientIp":"192.168.65.1" ← correct no matter how you read it
# ---- Fix 2: app counts from the right ----
# Set TRUST_PROXY_HOPS='1' for all three apps in docker-compose.yml (default is '0'),
# then RECREATE the containers - `restart` does not reload environment variables:
# docker compose --profile gw up -d --force-recreate app1 app2 app3
curl -s -H 'X-Forwarded-For: 1.2.3.4' http://localhost:8081/api/client-ip
# "xForwardedForRaw":"1.2.3.4, 192.168.65.1",
# "trustProxyHops":1,
# "trustedClientIp":"192.168.65.1" ← CORRECT
# "naiveClientIp":"1.2.3.4" ← still wrong, kept for comparison
Cấu hình nginx tạo ra hai hành vi đó chỉ khác nhau một dòng:
location /api/ {
proxy_pass http://api_pool/;
# APPEND: keep what the client sent, add the real IP at the end.
# Use this when there is ANOTHER trusted proxy in front of the gateway (a CDN,
# say) and the app needs the whole chain. The app MUST read from the right.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /safe-api/ {
proxy_pass http://api_pool/;
# OVERWRITE: wipe out everything the client sent. Use this when the gateway is
# the FIRST hop touching the Internet. This is the safest default.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
trust proxy. Đặt nó thành "true" thường có nghĩa là "tin
toàn bộ chuỗi", tức là quay về đúng cách đọc sai. Giá trị đúng luôn là một
con số cụ thể: số chặng proxy bạn thật sự vận hành. Nếu bạn không biết con số đó thì
chưa nên bật công tắc — hãy để gateway ghi đè header.Và đây không phải lỗi hiển thị:
X-Forwarded-Proto bị bịa có thể làm app tưởng request đã mã
hoá nên bỏ qua redirect sang HTTPS; XFF bị bịa vô hiệu hoá mọi rate limit theo IP.
4.3 Định tuyến theo đường dẫn — và một phép đo đi ngược dự đoán
Việc dễ thấy nhất của gateway: xem đường dẫn rồi quyết định gửi đi đâu.
/api/orders/* về service đơn hàng, /api/users/* về service người dùng,
/static/* thì gateway tự trả. Client thấy một domain duy nhất và không biết phía sau có bao
nhiêu service.
proxy_pass http://api_pool/; — có dấu / ở cuối — sẽ
cắt tiền tố /api/ trước khi chuyển tiếp, nên app nhận /fast.proxy_pass http://api_pool; — không có dấu / — giữ nguyên, app nhận
/api/fast và trả 404.Một ký tự, hai hành vi khác nhau, và thông báo lỗi duy nhất bạn nhận được là 404 từ app. Cách kiểm tra nhanh: gọi
/api/whoami; nếu ra 404 thì xem lại dấu gạch chéo trước khi nghi ngờ bất cứ điều
gì khác.
"Cho gateway phục vụ file tĩnh" — đo trước khi tin
Lời khuyên kinh điển: để gateway tự trả file tĩnh, mỗi file nó trả là một request app không phải xử lý.
nginx đọc file nhanh hơn Node rất nhiều, nên chuyện này nghe như không cần kiểm tra. Mình đo thử, 16 kết
nối, 8 giây, ba lần lặp, so /static/hello.json (nginx tự trả) với
/api/fast (chuyển tiếp về app):
| Route | Throughput (req/s) | p50 | p99 |
|---|---|---|---|
/static/ — nginx tự trả, cấu hình mặc định |
8.623 / 9.260 / 8.842 | 1,53–1,63 ms | 3,78–4,51 ms |
/api/fast — chuyển tiếp về Node |
22.768 / 22.680 / 22.720 | 0,58–0,59 ms | 1,38–1,41 ms |
File tĩnh chậm hơn 2,6 lần so với đi qua proxy tới một tiến trình Node. Điều này ngược hoàn toàn với dự đoán, nên nó là một trong hai khả năng: hoặc lời khuyên sai, hoặc mình đang đo một thứ khác với thứ mình nghĩ.
Giả thuyết: thư mục static được bind-mount từ máy host vào container. Trên Docker Desktop
(macOS/Windows), bind-mount đi qua ranh giới máy ảo, nên mỗi lời gọi hệ thống lên file đó đắt hơn hẳn. Và
nginx mặc định mở lại file mỗi request: stat(), open(),
read(). Nếu giả thuyết đúng thì việc cache metadata và file descriptor phải xoá được gần hết
khoảng cách. Thêm bốn dòng:
location /static/ {
alias /usr/share/nginx/static/;
expires 1h;
# Cache metadata + file descriptor instead of stat()+open()+read() every request.
open_file_cache max=1000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 1;
open_file_cache_errors on;
}
Cấu hình /static/ |
Throughput (req/s) | p50 | p99 |
|---|---|---|---|
| Mặc định | 8.623 / 9.260 / 8.842 | 1,53–1,63 ms | 3,78–4,51 ms |
Có open_file_cache
|
21.580 / 21.809 / 21.343 | 0,67–0,69 ms | 1,45–1,50 ms |
| Mốc so sánh: proxy về Node | ~22.720 | 0,59 ms | 1,40 ms |
Nhanh lên 2,4 lần, p50 giảm từ 1,6 ms xuống 0,68 ms. Giả thuyết đúng: nút cổ chai không
phải nginx mà là hệ thống file phía dưới nó. Sau khi sửa, file tĩnh gần bằng route proxy — vẫn thấp hơn
chút, và với file lớn hơn static/hello.json (94 byte) của lab thì lợi thế sẽ nghiêng hẳn về
nginx: chi phí cố định của mỗi request (bắt tay TCP nội bộ, event loop) chiếm phần lớn hơn khi file nhỏ,
còn nginx đọc file lớn thẳng từ cache mà không tốn CPU cho JavaScript.
Đáng chú ý hơn: nếu mình chỉ đo lần đầu rồi kết luận "nginx phục vụ tĩnh chậm", kết luận đó vừa sai vừa nghe rất thuyết phục vì có số liệu kèm theo. Số đo ngược dự đoán không phải kết luận — nó là câu hỏi. Việc tiếp theo luôn là đặt một giả thuyết cụ thể rồi tìm cách làm nó sai.
Đánh version API
Khi API phải thay đổi theo cách không tương thích ngược, gateway là nơi tự nhiên để hai phiên bản cùng tồn tại. Ba cách thường gặp:
| Cách | Ví dụ | Được | Mất |
|---|---|---|---|
| Trong đường dẫn | /v1/orders, /v2/orders |
Thấy ngay trong log và trong URL; gateway định tuyến bằng đúng cơ chế đã có | Version lẫn vào tài nguyên; cache và bookmark bị chia đôi |
| Bằng header | Accept: application/vnd.api.v2+json |
URL giữ nguyên ý nghĩa "tài nguyên"; đúng tinh thần HTTP | Không thấy trong URL nên khó debug; dễ quên khi gọi bằng curl |
| Bằng query | /orders?version=2 |
Dễ thử nhanh | Dễ bị bỏ sót; nhiều tầng cache bỏ qua query khi tạo khoá |
Trong thực tế, version trong đường dẫn thắng ở phần lớn hệ thống vì lý do rất thực dụng: khi có sự cố lúc 2 giờ sáng, bạn đọc được version ngay trên dòng log mà không phải mở thêm gì.
Gateway cũng là chỗ để canary: đẩy một phần nhỏ lưu lượng sang phiên bản mới trước khi
chuyển hết. nginx làm được bằng split_clients, và điểm quan trọng là nó băm theo một khoá ổn
định để một client luôn thấy cùng một phiên bản:
# Hash on IP (or better: on the user id in a cookie) => ONE client always sees
# the SAME version. Hashing on $request_id instead makes the same person bounce
# between v1 and v2 across requests — a bug that's very hard to reproduce.
split_clients "${remote_addr}canary" $api_backend {
5% api_v2;
* api_v1;
}
location /api/ {
proxy_pass http://$api_backend/;
}
5% ở đây là điểm khởi đầu thận trọng, không phải con số cố định — tăng dần (5% → 25% → 100%) khi tỉ lệ lỗi
và độ trễ của api_v2 đo riêng vẫn ổn, và có đường lùi ngay lập tức về 0% nếu không.
4.4 Việc xuyên suốt: cái gì nên đặt ở gateway
Có một nhóm việc mà mọi service đều cần: xác thực, giới hạn tần suất, ghi log, nén, CORS. Nếu mỗi service tự làm, bạn có N bản cài đặt và N cơ hội để một bản làm sai. Gateway là nơi làm một lần. Nhưng không phải việc gì cũng nên chuyển lên đó.
| Việc | Đặt ở đâu | Lý do |
|---|---|---|
| Kết thúc TLS, nén, ghi log truy cập | Gateway | Thuần kỹ thuật, không cần biết nghiệp vụ, giống nhau với mọi service |
| Kiểm tra token còn hợp lệ (chữ ký, hạn dùng) | Gateway | Chặn được request rác trước khi nó tiêu tài nguyên service |
| Người này có quyền sửa đơn hàng #42 không | Service | Cần dữ liệu nghiệp vụ mà chỉ service mới có. Gateway biết chuyện này là gateway đã phình. |
| Giới hạn tần suất theo IP hoặc theo API key | Gateway | Cần một chỗ đếm chung; đếm riêng ở từng replica thì hạn mức thật là N lần hạn mức khai báo |
| Giới hạn theo hạn mức nghiệp vụ (gói cước, số dư) | Service | Là quy tắc nghiệp vụ, thay đổi theo sản phẩm chứ không theo hạ tầng |
| Kiểm tra dữ liệu đầu vào hợp lệ | Cả hai | Gateway chặn thô (kích thước, kiểu nội dung); service kiểm tra đúng nghĩa và không bao giờ được tin rằng gateway đã kiểm |
Ranh giới nằm ở một câu hỏi: việc này có cần biết nghiệp vụ không? Không cần thì gateway. Cần thì service. Áp dụng câu hỏi đó cho từng việc là cách duy nhất mình biết để gateway không dần biến thành nơi chứa logic.
Gộp nhiều service: độ trễ bằng nhánh chậm nhất
Một màn hình thường cần dữ liệu từ nhiều service. Nếu client tự gọi từng cái, nó phải chịu toàn bộ độ trễ mạng của mình — với mạng di động thì đắt. Cho gateway (hoặc một service BFF — backend for frontend) gọi hộ trong mạng nội bộ rồi trả về một lần là kiểu thiết kế phổ biến. Điều then chốt là gọi song song:
Endpoint /aggregate của lab gọi thật ba nhánh qua HTTP nên chi phí socket và event loop đều
là chi phí thật, không phải setTimeout giả:
curl -sk "https://localhost:8443/api/aggregate?branches=50,120,200&mode=parallel"
# "maxBranchMs":200, "sumBranchMs":370, "totalMs":208.36
curl -sk "https://localhost:8443/api/aggregate?branches=50,120,200&mode=sequential"
# "maxBranchMs":200, "sumBranchMs":370, "totalMs":386.00
# Same three branches. Parallel: ~MAX. Sequential: ~SUM.
Cách thoát phổ biến: tách BFF thành service riêng đứng sau gateway. Gateway giữ đúng phần kỹ thuật (TLS, route, rate limit, xác thực token); BFF giữ phần biết nghiệp vụ và do nhóm làm frontend sở hữu. Hai việc khác nhau về nhịp thay đổi thì nên ở hai chỗ khác nhau.
Về giới hạn tần suất, nginx làm sẵn — nhưng cần hiểu nó cho phép bùng tới mức nào, vì đó là chỗ hay gây bất ngờ:
# Shared bucket, keyed by the real IP (do NOT use raw XFF - see section 4.2).
# 10r/s is the REFILL rate, not "10 requests per second, max".
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
location /api/ {
# burst=20: let up to 20 requests queue up, then drain them at 10r/s.
# nodelay: serve those 20 queued requests IMMEDIATELY instead of pacing them.
# => with nodelay, a client can burst 20 requests in a fraction of a second.
# Without nodelay it is smoother, but the client sees the delay stretched out.
limit_req zone=perip burst=20 nodelay;
limit_req_status 429; # default is 503 — 429 is more meaningful
proxy_pass http://api_pool/;
}
4.5 Gateway là điểm chết duy nhất — và ngân sách timeout
Mọi thứ trong bài này đều dồn về một chỗ, và chỗ đó nằm trên đường đi của 100% lưu lượng. Gateway chết là cả hệ thống chết, dù mọi service phía sau vẫn khoẻ mạnh. Nghịch lý: thành phần được thêm vào để tăng độ tin cậy lại là thành phần nguy hiểm nhất.
| Rủi ro | Cách giảm | Điều còn lại phải chấp nhận |
|---|---|---|
| Tiến trình gateway chết | Nhiều bản gateway sau một địa chỉ IP nổi (keepalived) hoặc DNS nhiều bản ghi | Cơ chế chuyển đổi đó lại thành thứ phải tin cậy; DNS có cache nên chuyển không tức thì |
| Cả vùng đặt gateway mất | Gateway ở nhiều vùng, định tuyến bằng anycast (một địa chỉ IP trỏ tới nhiều vị trí vật lý gần nhất) hoặc DNS theo vùng | Chi phí và độ phức tạp tăng đáng kể; cần dữ liệu cũng sẵn ở nhiều vùng |
| Cấu hình sai làm sập toàn bộ | nginx -t trước khi nạp lại; triển khai từng bản một, có bản để quay lại |
Vẫn cần người kiểm tra; xem cạm bẫy ngay dưới |
| Gateway thành nút cổ chai | Đo riêng CPU của gateway; giữ việc ở đó thuần kỹ thuật; scale ngang | Rate limit cần đếm chung nên phải có kho đếm chia sẻ (Redis) — lại thêm một phụ thuộc |
sed -i trên volume bind-mount rồi gọi nginx -s reload,
nginx báo pread() returned only 2830 bytes instead of 2832: nó đọc file trong khi file đang
bị viết dở. Reload thất bại nhưng nginx vẫn chạy bình thường với cấu hình cũ, nên nhìn từ ngoài
không có gì bất thường — và cả một loạt số đo sau đó vô nghĩa, phải bỏ đi.Hai quy tắc rút ra: (1) ghi cấu hình theo kiểu nguyên tử — viết ra file tạm rồi
mv đè lên, vì mv trong cùng hệ thống file là một thao tác duy nhất; (2)
luôn kiểm tra cấu hình đang có hiệu lực sau khi nạp lại, đừng tin là reload đã thành
công. Một reload thất bại im lặng còn tệ hơn một reload làm sập, vì nó không cho bạn biết gì.
Ngân sách timeout
Mỗi chặng trên đường đi có timeout riêng, và chúng phải giảm dần khi đi vào trong. Nếu chặng trong chờ lâu hơn chặng ngoài, phần chờ vượt quá là công việc chắc chắn bị bỏ: client đã đi từ lâu mà service vẫn còn chiếm kết nối và CPU để tính một kết quả không ai nhận.
| Chặng | Ngân sách ví dụ | Vì sao nhỏ hơn chặng ngoài |
|---|---|---|
| Client (app di động) | 10 s | Trần trên: quá mức này người dùng đã rời đi |
| Gateway → service | 8 s | Chừa 2 s để gateway còn kịp trả lỗi tử tế thay vì để client tự hết giờ |
| Service → database | 3 s | Chừa chỗ cho thử lại và cho cả các nhánh gọi khác trong cùng request |
| Từng nhánh khi gộp dữ liệu | 2 s | Một nhánh chậm không được quyền kéo cả màn hình xuống |
proxy_next_upstream để gateway tự thử replica khác — rất hiệu quả, và nó chính
là lý do lần đo đó không thấy một lỗi 502 nào. Nhưng nếu client cũng thử lại 3 lần, và gateway thử 3
replica, và service cũng thử lại database 3 lần, thì một sự cố nhỏ tạo ra
27 lần công việc đúng lúc hệ thống yếu nhất. Đây là cách một sự cố nhỏ trở thành sự cố
toàn diện.Nguyên tắc: chỉ thử lại ở một tầng, kèm giới hạn tổng số lần thử và có khoảng nghỉ tăng dần cộng nhiễu ngẫu nhiên. Nếu bạn không biết chắc tầng nào đang thử lại thì hệ thống đang có một hệ số nhân mà bạn chưa nhìn thấy.
Tóm lại
Reverse proxy, load balancer và API gateway là ba mức trách nhiệm ở cùng một vị trí, và mỗi mức thêm quyền năng bằng cách thêm trạng thái phải quản lý. TLS termination rẻ khi có kết nối bền và đắt gấp nhiều lần khi không — chi phí nằm ở mỗi kết nối, không phải mỗi request. X-Forwarded-For phải đọc từ bên phải, hoặc tốt hơn là để gateway ghi đè. Việc gì không cần biết nghiệp vụ thì đặt ở gateway, việc cần biết nghiệp vụ thì để service. Và mọi thứ dồn về gateway nghĩa là ngân sách timeout cùng chính sách thử lại phải được thiết kế có chủ ý, không để mặc định.
Phép đo file tĩnh trong bài này để lại bài học rộng hơn cả kiến trúc: một con số ngược dự đoán không phải kết luận, nó là câu hỏi. Nếu mình dừng ở lần đo đầu, bài này đã khẳng định "nginx phục vụ file tĩnh chậm hơn Node" — sai, nhưng có số liệu kèm theo nên nghe rất thuyết phục.
Bài 5 đi vào tầng ngay sau gateway: caching. Lab đã có Redis chờ ở profile
cache, và câu hỏi trung tâm là cái mà mọi tầng cache đều phải trả lời — dữ liệu cũ tới mức
nào thì vẫn dùng được, và ai chịu trách nhiệm xoá nó.
📖 Tài liệu tham khảo
- nginx — Module ngx_http_proxy (proxy_pass, proxy_set_header, TLS termination)
- MDN — Header X-Forwarded-For (cú pháp, và vì sao không được tin phần bên trái)
- nginx — Module ngx_http_core, chỉ thị open_file_cache (chỉ thị xoá khoảng cách 2,6 lần trong bài)
- nginx — Module ngx_http_limit_req (rate limiting kiểu leaky bucket, burst, nodelay)
- nginx — Module ngx_http_split_clients (cơ chế canary theo tỉ lệ phần trăm)
Tải mã nguồn thực hành
Hai file cấu hình nginx của gateway trong lab: server block cho hai cổng HTTP/HTTPS, và các
location dùng chung (TLS termination, X-Forwarded-For, file tĩnh có
open_file_cache):
Bình luận