Internal Engineering Document

So sánh System Design Guideline
với hạ tầng hiện tại & Kế hoạch chuyển đổi

Đối chiếu từng khuyến nghị trong System Design Guideline — Finance API Platform (Draft v1, 03/07/2026) với hạ tầng kioskgaming đang chạy trên Hetzner (khảo sát trực tiếp 5 server + Hetzner Console, 04/07), và lộ trình chuyển đổi. Kiến trúc đích sau chuyển đổi: xem Hệ Thống Đích · hiện trạng chi tiết: Deep Dive hạ tầng · Sơ đồ hệ thống.

Ngày lập: 03/07 · cập nhật 04/07/2026 Nguồn đối chiếu: repo nginx + khảo sát 5 server Hạ tầng: 5 server Hetzner · Ashburn us-east Trạng thái: Đề xuất — chưa triển khai
01

Tóm tắt điều hành

Guideline đề xuất 4 thay đổi chính. Đối chiếu với hạ tầng thật cho kết quả như sau.

Khuyến nghị chính của guidelineHiện trạngKết luận
Nginx thứ 2 + Cloudflare LB (xóa SPOF)Chưa cóƯu tiên #1. Thực tế nghiêm trọng hơn guideline mô tả — xem mục 04.
Idempotency key qua RedisChưa cóViệc của code backend (kioskgaming_backend), làm song song giai đoạn 1.
PgBouncer transaction modeChưa cóCần audit connection ngay (giai đoạn 2), triển khai giai đoạn 3.
Đo đạc trước khi thêm computeĐo 1 lầnSnapshot docker stats trên V2 (04/07): CPU <1%, ~90–100MB/instance — không nghẽn compute. Còn thiếu số liệu giờ cao điểm.
PM2 cluster trong compute layerĐã cóĐã làm ở DDoS Resilience Phase 1 — guideline viết như chưa có.
Postgres read replica "nếu cần"Đã cópostgres-slave trên V4, streaming không lag (xác nhận 04/07) — chỉ thiếu định tuyến read.
(Ngoài guideline) Network firewall khớp với tầng applicationLệchPhát hiện 04/07: rule :80 Hetzner Firewall chỉ whitelist 5/15 dải Cloudflare mà nginx tin — sửa ngay được trên Console, xem Giai đoạn 0.
Tinh thần chung

Đồng thuận với guideline: đây là bổ sung, không đập đi xây lại. Phần lớn nền tảng (Cloudflare edge, least_conn, health check upstream, Redis/Postgres replication, PM2 cluster) đã đúng hướng. Việc còn thiếu tập trung ở 3 chỗ: SPOF nginx, idempotency, và kiểm soát connection tới Postgres.

02

Hạ tầng hiện tại (thực tế)

Chi tiết đầy đủ trong Ha-Tang-Deep-Dive.html.

ServerPlanIP nội bộVai trò
V1-KioskCCX23 · 4 vCPU/16GB10.0.0.5nginx-lb :80 (gateway duy nhất toàn hệ) + API prod :3001–3004 + API dev :4001
V2-KioskCCX13 · 2 vCPU/8GB10.0.0.4API prod :3001–3004 + API dev :4001 — đo thực tế: ~90–100MB/instance, CPU thấp
V3-KioskCCX13 · 2 vCPU/8GB10.0.0.2Redis master :6379 + Postgres master :5432
V4-KioskCCX13 · 2 vCPU/8GB10.0.0.37 container: Redis slave + Redis dev + Postgres slave :5433 + RabbitMQ + RedisInsight + OmniDB + Dozzle
kiosk-gamify-engineCCX23 · 4 vCPU/16GB10.0.0.6Gamify dev/pro (5 instance) + OpenSearch + Dashboards + Fluent Bit

Luồng request: client → HTTPS → Cloudflare (Flexible mode) → HTTP :80 → nginx-lb trên V1 → upstream backend least_conn 8 instance (V1 + V2). Postgres và Redis replicate sang V4, online không lag; failover hiện là thủ công. 6 site frontend (client/shop/agent/super-agent/admin/cashier) hosting trên Cloudflare Pages — ngoài phạm vi 5 server này.

Phát hiện 04/07

① Hetzner Firewall firewall-ssh (duy nhất trong project, áp cho 5/5 server): rule :80 chỉ whitelist 5/15 dải Cloudflare — traffic thật qua PoP thuộc 10 dải thiếu bị DROP trước khi tới nginx; :22 mở toàn Internet; các port data (6379/5432/9200…) default-deny ✓. ② Mọi container Dozzle (4/4 server) báo unhealthy — lỗi healthcheck của image, không phải sự cố. ③ V2 và V3 trùng hostname ubuntu-8gb-ash-1 — nhận diện server phải bằng IP.

03

Bảng so sánh từng khuyến nghị

Từng điểm trong guideline, đối chiếu với cấu hình thật.

Guideline đề xuấtHiện trạng thực tế (nguồn kiểm chứng)Trạng thái
Cloudflare ở edge chặn DDoS L3/4 Mọi domain *.kioskservice.club sau Cloudflare; 15 dải set_real_ip_from + CF-Connecting-IP trong mọi conf (nginx/conf.d/*.conf) Đã đạt
2 Nginx độc lập + Cloudflare LB 1 nginx-lb duy nhất trên V1. Nặng hơn guideline mô tả: V1 gánh cả gateway lẫn 4/8 instance API prod — V1 chết là mất gateway nửa app cùng lúc SPOF
least_conn + max_fails=3 + fail_timeout Đã có đủ trong upstream backend (conf.d/default.conf) Đã đạt
keepalive tới upstream Đang bị comment — # keepalive 32; (default.conf:15) Tắt
Rate limit limit_req/limit_conn tầng nginx Không tồn tại trong bất kỳ file conf nào (đã grep toàn bộ conf.d/ + nginx.conf). Rate limit hiện chỉ có ở tầng backend (DDoS Phase 1) Chưa có
Timeout ngắn: connect 3s / read 8s Đang 60s / 60s (default.conf:61-63) Quá dài
PM2 instances = số vCPU, không hơn PM2 cluster đã chạy (DDoS Phase 1). V2 là CCX13 2 vCPU chạy 4 instance :3001–3004 — vượt số core. Snapshot 04/07: CPU <1%, RAM ~100MB/instance ⇒ không nghẽn, nhưng 4 instance trên 2 core vẫn là thừa (chỉ tốn context-switch, không thêm throughput) Đo 1 lần
(Ngoài guideline) Firewall tầng network khớp danh sách trust tầng nginx Lệch (Console, 04/07): rule :80 chỉ có 5/15 dải Cloudflare trong khi set_real_ip_from tin 15 dải; rule :443 lại đủ 15/15 dù nginx không listen 443; :22 mở Any IPv4+IPv6 Lệch
PgBouncer transaction pooling Chưa có. max_connections=100 (postgres-master.yml:31); 8 instance prod + 2 dev + gamify cùng trỏ 1 master — pool 10/instance là đã chạm trần Chưa có
Read replica "cân nhắc khi cần" Đã có sẵn: postgres-slave streaming replication từ 10.0.0.2 (postgres-slave.yml) — guideline không biết. Chỉ thiếu định tuyến read từ app Đã có
Redis: cache + idempotency key Redis master-slave đã chạy, đang dùng cache + rate limit backend. Idempotency key chưa có — là việc của code kioskgaming_backend, không thuộc repo nginx Một nửa
Không dùng Kubernetes ở quy mô này Hiện không dùng K8s — đồng thuận giữ nguyên Đồng ý
04

Điểm guideline bỏ sót

Guideline viết cho mô hình tổng quát "Cloudflare → 1 Nginx → app → DB" nên thiếu vài thực tế của hệ thống này.

SPOF thứ hai: Server 3 (10.0.0.2)

Redis master và Postgres master cùng nằm trên một server CCX13. Slave của cả hai đã có nhưng failover là thủ công — không có Sentinel (Redis) hay Patroni/repmgr (Postgres). Sau khi xóa SPOF nginx, đây là điểm chết kế tiếp của hệ thống: V3 chết là mất toàn bộ write path dù app và gateway vẫn sống.

V1 kiêm nhiệm quá nhiều — chọn V4 cho Nginx #2

Guideline coi Nginx là một node riêng. Thực tế V1 vừa là gateway toàn hệ, vừa chạy 4 instance API prod + API dev — sự cố tài nguyên tầng app trên V1 kéo sập luôn gateway. Nginx #2 đặt trên V4 dù V4 đang cõng 7 container (Redis slave 1.5GB limit + Postgres slave shared_buffers 1GB + RabbitMQ 1GB + 4 tool) trên 8GB RAM — vì nginx rất nhẹ (vài chục MB) và failure-domain của V4 tốt hơn: mất V4 = mất gateway #2 + replica (đường write vẫn sống); đặt trên V2 thì mất V2 = mất gateway #2 + nửa API prod cùng lúc — lặp lại đúng cấu trúc lỗi của V1. Điều kiện kèm theo: audit RAM V4 trước khi cài (bước 1A).

Tầng firewall network — guideline hoàn toàn không nhắc

Guideline chỉ bàn từ Cloudflare xuống nginx, bỏ qua lớp firewall của nhà cung cấp hạ tầng đứng trước nginx. Thực tế lớp này đang lệch với tầng application: rule :80 thiếu 10/15 dải Cloudflare (traffic thật có thể bị DROP tùy PoP — lỗi ngắt quãng khó truy), :22 mở toàn Internet. Đây là 2 việc sửa trên Console trong vài phút, không đụng server — đưa vào Giai đoạn 0 bên dưới, làm trước mọi thứ khác.

Con số rate limit của guideline không áp thẳng được

Guideline khuyến nghị rate=5r/s cho "finance API gọi thưa". Kioskgaming là nền tảng gaming — client gọi dày hơn nhiều (polling số dư, game callback, session). Áp thẳng 5r/s sẽ chặn nhầm user thật. Cần bắt đầu lỏng (≈20r/s burst 40), quan sát log rồi siết dần. Tương tự với timeout 3s/8s — các endpoint report/game callback có thể chậm hơn.

Trùng phạm vi với kế hoạch DDoS Resilience

Rate limit + hardening tầng nginx trùng với Edge L0–L1 của DDoS-Resilience-Technical-Plan (đang pending). Nên gộp làm một lần sửa conf.d/, tránh hai kế hoạch đụng cùng file.

05

Kế hoạch chuyển đổi — Giai đoạn 0 & 1

Giai đoạn 0: sửa lệch firewall phát hiện 04/07 — chỉ thao tác Console, vài phút, làm ngay. Giai đoạn 1: xóa SPOF nginx + Idempotency — rủi ro business cao nhất.

0A

Bổ sung 10 dải Cloudflare còn thiếu vào rule :80

Console → Firewalls → firewall-ssh → rule TCP :80 → thêm: 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17 — sau đó rule :80 khớp đúng 15 dải của set_real_ip_from. Áp dụng tức thì cho cả 5 server, không cần restart gì. Đây là việc đang âm thầm chặn traffic thật, ưu tiên trước mọi thứ.

0B

Giới hạn nguồn SSH :22

Rule :22 đang mở Any IPv4+IPv6. Nếu có IP admin/VPN cố định → giới hạn về dải đó. Nếu chưa có → tối thiểu xác nhận PasswordAuthentication no trong sshd_config cả 5 server + cân nhắc fail2ban; ghi lại quyết định để không quên.

1A

Dựng nginx-lb #2 trên Server 4 (V4-Kiosk, 10.0.0.3)

Chọn V4 vì failure-domain: mất V4 = mất gateway #2 + replica, đường write còn sống; đặt trên V2 thì mất V2 = mất gateway #2 + nửa API prod — lặp lại lỗi cấu trúc của V1. V4 đang chạy 7 container nhưng nginx chỉ tốn vài chục MB nên vẫn vừa, với điều kiện kiểm tra trước.

  • Tiền đề: free -h + docker stats --no-stream trên V4 — xác nhận còn ≥500MB RAM trống ổn định trước khi cài.
  • Copy nginx.yml + thư mục nginx/ (conf.d, htpasswd) sang V4 → docker compose -f nginx.yml up -d. Config dùng IP nội bộ 10.0.0.x nên chạy nguyên xi, không sửa gì. Lưu ý network_mode: host: nginx nghe :80 trên host — V4 chưa có gì chiếm :80 nên không xung đột.
  • Firewall: firewall-ssh áp cho cả 5 server nên :80 trên V4 đã mở sẵn cho dải Cloudflare — không cần thao tác thêm (đã gộp phần sửa 15/15 vào bước 0A).
  • Kiểm chứng: curl -H "Host: api.kioskservice.club" http://178.156.211.230/ từ IP được phép (hoặc từ chính V4: curl -H "Host: api.kioskservice.club" http://localhost/).
1B

Cloudflare trỏ về cả 2 origin

Hai phương án, chọn theo ngân sách:

  • Cloudflare Load Balancing (add-on trả phí ~$5/tháng + phí origin): health check chủ động, tự loại origin chết khỏi vòng quay trong ~15–60s. Khuyến nghị.
  • 2 A record proxied (miễn phí): Cloudflare phân phối nhưng không health-check chủ động — failover chậm hơn, phụ thuộc retry của Cloudflare. Chấp nhận được nếu chưa duyệt chi phí.
1C

Script đồng bộ cấu hình 2 nginx

Từ đây mọi thay đổi conf.d/ phải sync cả 2 server. Thêm sync-nginx.sh vào repo: rsync conf → nginx -t từng con → reload từng con → dừng ngay nếu nginx -t fail. Cấu hình lệch nhau giữa 2 gateway là lỗi vận hành khó truy nhất về sau.

1D

Idempotency Key qua Redis (việc của kioskgaming_backend)

Không thuộc repo nginx — tách task riêng cho backend. Middleware đọc header Idempotency-Key, dùng SET key value NX EX ttl trên Redis master cho các endpoint deposit / withdraw / transfer. Lưu ý: phải lưu cả kết quả response để trả lại khi trùng key (SETNX minh họa trong guideline mới chỉ chặn xử lý đôi, chưa trả kết quả cũ). TTL 60–300s phủ hết chu kỳ retry của client.

06

Kế hoạch chuyển đổi — Giai đoạn 2

Đo đạc & audit — không thay đổi compute trước khi có số liệu, đúng nguyên tắc 05 của guideline.

2A

Đo CPU/RAM giờ cao điểm trên V1, V2

docker stats / pm2 monit. Nghi vấn cần trả lời: V2 (2 vCPU) chạy 4 instance :3001–3004 — nếu CPU không phải bottleneck, giảm còn 2 instance để giải phóng RAM và bớt context-switching.

2B

Audit connection Postgres

Trên master: SELECT count(*), usename, application_name FROM pg_stat_activity GROUP BY 2,3;. Tính tổng = (8 instance prod + 2 dev + gamify) × pool size mỗi instance, so với trần max_connections=100 trừ buffer 10–20 cho backup/migration/monitoring. Con số này quyết định giai đoạn 3 làm gấp hay từ từ.

2C

Siết cấu hình nginx (gộp với Edge L0–L1 của plan DDoS)

  • Bật lại keepalive 32 trong upstream backend (bỏ comment default.conf:15, thêm proxy_http_version 1.1 + proxy_set_header Connection "").
  • Thêm limit_req_zone ≈20r/s burst 40 (không dùng 5r/s của guideline — xem mục 04), key theo $binary_remote_addr sau real_ip Cloudflare. Quan sát log 1–2 tuần rồi siết dần.
  • Hạ timeout 60s → 15s connect / 30s read trước; chỉ xuống thấp hơn sau khi xác nhận không có endpoint hợp lệ nào chậm hơn.
  • Quy trình áp dụng: sửa local → sync 2 server → docker exec nginx-lb nginx -tnginx -s reload từng con.
07

Kế hoạch chuyển đổi — Giai đoạn 3

PgBouncer + các việc để sau, triển khai theo số liệu giai đoạn 2.

3A

PgBouncer trên Server 3, cạnh Postgres master

Thêm service vào postgres-master.yml (hoặc yml riêng), pool_mode=transaction, max_client_conn=500, default_pool_size=20. App đổi connection từ :5432 → :6432 — đổi dần từng instance, có thể rollback từng con.

3A′

⚠️ Điều kiện tiên quyết: kiểm tra prepared statements

Transaction pooling không hỗ trợ prepared statement mức session. Trước khi chuyển: kiểm tra ORM/driver của kioskgaming_backend (TypeORM / node-postgres) — hoặc bật max_prepared_statements (PgBouncer ≥ 1.21), hoặc tắt prepared statements phía driver. Bỏ qua bước này sẽ gây lỗi ngẫu nhiên khó truy trên production.

3B

Định tuyến read-only sang postgres-slave sẵn có

Report, lịch sử giao dịch, dashboard đọc từ slave. Chỉ làm khi số liệu 2B cho thấy master chịu tải đọc lớn — replica đã chạy sẵn nên chi phí chỉ là sửa connection string phía app.

3C

Failover cho Server 3 (ngoài phạm vi guideline)

Tối thiểu: văn bản hóa quy trình failover thủ công Redis/Postgres (promote slave, đổi IP trong env backend, thứ tự thao tác). Nâng cao: Redis Sentinel; Postgres cân nhắc repmgr/Patroni khi đội vận hành sẵn sàng.

08

Việc không làm & hành động ngay

Giới hạn phạm vi rõ ràng để kế hoạch không phình.

Không làm (và lý do)

Có thể bắt đầu ngay, không đụng production

Nguyên tắc an toàn khi thực thi

Mọi thay đổi conf soạn ở repo local trước; áp dụng trên server luôn theo trình tự docker exec nginx-lb nginx -tdocker exec nginx-lb nginx -s reload (không restart). Thay đổi ảnh hưởng data service (Postgres/Redis) chỉ đưa lệnh để người vận hành tự chạy, kèm cảnh báo tác động.