Mục lục
- Mục Tiêu Bài Học
- Cài
oha— Rust-Native Load Test Tool - Benchmark Baseline
GET /products - Pool Metrics API:
size(),num_idle(),num_active() - Implement
/health/dbDeep Health Check /metricsPrometheus Format Endpoint- Stress Test Pool Saturation + Alert Threshold
- Verify End-To-End + Wire Routes
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Cài và dùng
ohatool (Rust-native, faster than wrk) đo throughput. - Benchmark Shop API
GET /productsbaseline + variant pool config 5 mức. - Hiểu API
pool.size(),pool.num_idle(),pool.num_active(). - Expose metrics endpoint Prometheus exposition format text.
- Implement
/health/readydeep check (kiểm pool + DB ping). - Pattern alert pool gần full (capacity utilization > 80%).
- Hiểu lựa chọn
metricscrate vs Prometheus exposition format manual.
Cài oha — Rust-Native Load Test Tool
oha (viết tắt oh-my-load-application) là HTTP load testing tool viết bằng Rust, mở source 2020 bởi Hatoo trên GitHub. So với wrk (C, 2012) và hey (Go, 2016), oha ergonomic hơn nhờ TUI real-time + JSON output cho CI parse.
Cài đặt 2 cách phổ biến:
cargo install oha
# hoặc trên macOS
brew install oha
Verify phiên bản:
oha --version
# oha 1.4.x
Tính năng chính khiến oha phù hợp Rust project:
- TUI real-time hiển thị latency histogram, RPS, status code distribution suốt quá trình test.
- HTTP/2 + HTTP/1.1 support — test endpoint axum sẵn HTTP/2 nếu enable.
- JSON output qua flag
-jcho CI parse:oha -j ... > report.jsonrồijqextract field. - HTTP method, header, body custom flag
-m POST,-H 'X-Foo: bar',-d '{"x":1}'đầy đủ.
Test khởi động đầu tiên ngay sau khi Shop API chạy:
oha -n 1000 -c 10 http://localhost:3000/health/live
# -n 1000: tổng request
# -c 10: concurrent client
# -z 30s: duration mode (thay -n)
Output sample của oha in ra terminal:
Summary:
Success rate: 100.00%
Total: 2.4123 secs
Requests/sec: 4145.2
Status code distribution:
[200] 1000 responses
Latency distribution:
10% in 0.002 secs
50% in 0.003 secs
90% in 0.005 secs
99% in 0.009 secs
Đây là baseline cho endpoint không chạm DB — bước kế tiếp benchmark endpoint thật GET /products qua sqlx pool.
Benchmark Baseline GET /products
Chuẩn bị data: insert 100 product vào DB cho list endpoint trả nhiều row:
for i in {1..100}; do
curl -X POST http://localhost:3000/api/v1/products \
-H 'Content-Type: application/json' \
-d "{\"name\":\"Product $i\",\"slug\":\"product-$i\",\"price\":\"$((i * 10000)).00\",\"stock\":$i}"
done
Baseline test 30 giây với 50 concurrent:
oha -z 30s -c 50 'http://localhost:3000/api/v1/products?page=1&per_page=20'
Kết quả mẫu trên máy laptop tham khảo (không chạy thực, dùng để minh họa pattern phân tích):
Summary:
Success rate: 100.00%
Total: 30.0123 secs
Slowest: 0.082 secs
Fastest: 0.003 secs
Average: 0.012 secs
Requests/sec: 4180.5
Latency distribution:
50% in 0.011 secs
90% in 0.018 secs
99% in 0.034 secs
Bài tập variant: chạy lại với 4 mức POOL_MAX_CONN khác nhau và ghi lại RPS + p99:
POOL_MAX_CONN=5 cargo run -p shop-api # cùng oha test 30s
POOL_MAX_CONN=20 cargo run -p shop-api # default
POOL_MAX_CONN=100 cargo run -p shop-api
POOL_MAX_CONN=200 cargo run -p shop-api
Bảng so sánh kết quả tham khảo (laptop 8 core + Postgres local default):
Config | RPS | p99 | Nhận xét
----------------------------+--------+-------+------------------------
POOL_MAX_CONN=5 | 1200 | 95ms | pool saturate, queue chờ
POOL_MAX_CONN=20 (default) | 4180 | 34ms | sweet spot
POOL_MAX_CONN=100 | 4250 | 35ms | diminishing return
POOL_MAX_CONN=200 | 3900 | 45ms | overhead worse
Kết luận từ data thực nghiệm: sweet spot quanh 20 max cho cấu hình dev/staging. Pool quá nhỏ (5) bị queue, pool quá lớn (200) thêm overhead Postgres process per connection + context switch + lock contention nội bộ pg_locks — RPS không tăng còn p99 tệ hơn. Quy luật chung từ PgBadger + benchmark community: pool size = (CPU cores × 2) + effective spindles cho workload OLTP.
Lock decision Shop API: 20 max là default OK cho dev/staging, production tune theo CPU cores DB server (vd RDS db.r6g.xlarge 4 core → 4 × 2 = 8-10 max per replica, scale ngang qua replica + PgBouncer chi tiết G18).
Pool Metrics API: size(), num_idle(), num_active()
sqlx PgPool expose 2 method public về metrics, method thứ 3 tính bằng phép trừ:
use shop_api::state::AppState;
use sqlx::PgPool;
fn snapshot(state: &AppState) {
let pool: &PgPool = &state.db;
let size = pool.size(); // tổng connection trong pool (idle + active)
let num_idle = pool.num_idle(); // connection idle chờ acquire (usize)
let num_active = size - num_idle as u32; // connection đang query
}
Field meaning chi tiết:
size: số connection vật lý hiện có trong pool (lazy grow theo demand, không tớimax_connectionsngay từ đầu).num_idle: connection sẵn sàng acquire, đang chờ trong queue.num_active=size - num_idle: connection đang được handler giữ để chạy query.
Theo dõi 3 con số theo thời gian phát hiện bottleneck:
num_activeluôn xấp xỉmax_connections→ pool exhausted, cần tăng pool hoặc tối ưu query.num_idleluôn cao (vd 18/20 idle suốt) → over-provisioned, có thể giảm pool tiết kiệm RAM.sizenhỏ hơnmaxnhiều → traffic chưa đủ để lazy grow, pool đang ở giai đoạn warm.
Pattern lock Shop API cho bài này:
- Expose 3 con số qua endpoint
/metricstheo Prometheus format. - Endpoint
/health/readytrả 503 khinum_active / max_connections > 0.95để load balancer take pod out of rotation. - Alert Prometheus
PoolHighUtilizationkhinum_active / max > 0.8kéo dài 2 phút (capacity warning).
Implement /health/db Deep Health Check
Refactor file crates/shop-api/src/routes/health.rs đã có ở B5 (placeholder chỉ trả OK text) thành 2 handler đầy đủ:
// File: crates/shop-api/src/routes/health.rs
use axum::{extract::State, http::StatusCode, response::Json};
use serde_json::{json, Value};
use crate::state::AppState;
pub async fn liveness() -> StatusCode {
StatusCode::OK
}
pub async fn readiness(State(state): State<AppState>) -> (StatusCode, Json<Value>) {
// Ping DB nhanh
let db_ok = sqlx::query("SELECT 1")
.execute(&state.db)
.await
.is_ok();
// Pool stats
let pool_size = state.db.size();
let pool_idle = state.db.num_idle() as u32;
let pool_active = pool_size - pool_idle;
let pool_max = state.config.pool_max_connections;
let utilization = pool_active as f64 / pool_max as f64;
let status = if db_ok && utilization < 0.95 {
StatusCode::OK
} else {
// !db_ok hoặc pool gần exhausted >= 95%
StatusCode::SERVICE_UNAVAILABLE
};
let body = json!({
"status": if status.is_success() { "ok" } else { "degraded" },
"checks": {
"database": {
"ok": db_ok,
"pool_size": pool_size,
"pool_idle": pool_idle,
"pool_active": pool_active,
"pool_max": pool_max,
"utilization": format!("{:.2}", utilization),
}
}
});
(status, Json(body))
}
pub fn routes() -> axum::Router<AppState> {
use axum::routing::get;
axum::Router::new()
.route("/health/live", get(liveness))
.route("/health/ready", get(readiness))
}
Liveness vs Readiness theo Kubernetes pattern:
/health/live: process còn alive (chỉ trả 200). K8s liveness probe fail → restart pod. Endpoint này không chạm DB, không touch pool — chỉ trả OK nếu tokio runtime còn chạy được async function./health/ready: ready accept traffic (DB OK + pool còn slot). K8s readiness probe fail → take pod out of rotation (load balancer ngừng route traffic về pod), pod vẫn chạy không bị restart. Đây là cách elegant xử lý overload tạm thời.
Acquire timeout dành cho health: phải nhỏ — pool config Shop API set 5s default (B56 lock), nhưng probe K8s default check 10s timeout 1s, nếu DB chết hoặc pool exhausted phải fail nhanh để LB đưa pod ra khỏi rotation trong cùng probe interval.
Test thủ công:
curl http://localhost:3000/health/live
# 200 OK (body rỗng)
curl http://localhost:3000/health/ready | jq
# {
# "status": "ok",
# "checks": {
# "database": {
# "ok": true,
# "pool_size": 5,
# "pool_idle": 4,
# "pool_active": 1,
# "pool_max": 20,
# "utilization": "0.05"
# }
# }
# }
Khi DB dừng (vd docker stop shop-postgres), readiness trả ngay 503 + "status":"degraded" + "database.ok":false — LB rút pod khỏi rotation, mọi instance còn DB tiếp tục phục vụ traffic.
/metrics Prometheus Format Endpoint
Prometheus exposition format (RFC text format 0.0.4) là text/plain UTF-8 với mỗi metric gồm 3 dòng:
# HELP shop_db_pool_size Total connections in pool
# TYPE shop_db_pool_size gauge
shop_db_pool_size 5
# HELP shop_db_pool_idle Idle connections in pool
# TYPE shop_db_pool_idle gauge
shop_db_pool_idle 4
# HELP shop_db_pool_active Active connections in pool
# TYPE shop_db_pool_active gauge
shop_db_pool_active 1
# HELP shop_db_pool_max Maximum connections allowed
# TYPE shop_db_pool_max gauge
shop_db_pool_max 20
Tạo handler mới crates/shop-api/src/routes/metrics.rs:
// File: crates/shop-api/src/routes/metrics.rs
use axum::{extract::State, http::header, response::Response};
use crate::state::AppState;
pub async fn metrics(State(state): State<AppState>) -> Response {
let size = state.db.size();
let idle = state.db.num_idle();
let active = size - idle as u32;
let max = state.config.pool_max_connections;
let utilization = active as f64 / max as f64;
let body = format!(
"# HELP shop_db_pool_size Total connections in pool\n\
# TYPE shop_db_pool_size gauge\n\
shop_db_pool_size {size}\n\
# HELP shop_db_pool_idle Idle connections in pool\n\
# TYPE shop_db_pool_idle gauge\n\
shop_db_pool_idle {idle}\n\
# HELP shop_db_pool_active Active connections in pool\n\
# TYPE shop_db_pool_active gauge\n\
shop_db_pool_active {active}\n\
# HELP shop_db_pool_max Maximum connections allowed\n\
# TYPE shop_db_pool_max gauge\n\
shop_db_pool_max {max}\n\
# HELP shop_db_pool_utilization Pool utilization ratio (0..1)\n\
# TYPE shop_db_pool_utilization gauge\n\
shop_db_pool_utilization {utilization:.4}\n"
);
Response::builder()
.header(header::CONTENT_TYPE, "text/plain; version=0.0.4; charset=utf-8")
.body(body.into())
.unwrap()
}
Wire route trong crates/shop-api/src/router.rs:
// File: crates/shop-api/src/router.rs (đoạn build_router)
use axum::{routing::get, Router};
use crate::{routes, state::AppState};
pub fn build_router(state: AppState) -> Router {
let api_v1 = Router::new()
.merge(routes::products::routes());
Router::new()
.route("/", get(routes::root))
.merge(routes::health::routes()) // /health/live + /health/ready
.route("/metrics", get(routes::metrics::metrics)) // /metrics
.nest("/api/v1", api_v1)
// ... compression + decompression + request_id middleware giữ nguyên B50
.with_state(state)
}
Verify:
curl http://localhost:3000/metrics
Response sample:
# HELP shop_db_pool_size Total connections in pool
# TYPE shop_db_pool_size gauge
shop_db_pool_size 5
# HELP shop_db_pool_idle Idle connections in pool
# TYPE shop_db_pool_idle gauge
shop_db_pool_idle 4
# HELP shop_db_pool_active Active connections in pool
# TYPE shop_db_pool_active gauge
shop_db_pool_active 1
# HELP shop_db_pool_max Maximum connections allowed
# TYPE shop_db_pool_max gauge
shop_db_pool_max 20
# HELP shop_db_pool_utilization Pool utilization ratio (0..1)
# TYPE shop_db_pool_utilization gauge
shop_db_pool_utilization 0.0500
Pattern lock Shop API: /metrics endpoint manual format text — KHÔNG cần metrics crate cho basic case. Group 15 (B141-B150) sẽ refactor sang metrics crate + prometheus-client khi cần histogram/counter chi tiết (request latency, status code distribution, query duration). Hiện tại 5 gauge đủ giám sát pool capacity.
Stress Test Pool Saturation + Alert Threshold
Scenario kiểm chứng pool exhaustion + alert behavior — chạy server với pool nhỏ rồi stress:
# Terminal 1 — set pool nhỏ để dễ saturate
POOL_MAX_CONN=5 cargo run -p shop-api
# Terminal 2 — stress test concurrent > pool size
oha -z 30s -c 50 'http://localhost:3000/api/v1/products'
# Terminal 3 — theo dõi metrics song song
watch -n 1 'curl -s http://localhost:3000/metrics | grep pool'
Terminal 3 thấy giá trị thay đổi real-time:
shop_db_pool_size 5
shop_db_pool_idle 0
shop_db_pool_active 5
shop_db_pool_max 5
shop_db_pool_utilization 1.0000
Khi pool saturate, request mới chờ tới acquire_timeout 5s (B56 lock) rồi nhận 503 ServiceUnavailable qua mapping PoolTimedOut → AppError::ServiceUnavailable (B55 lock). Output oha thể hiện rate 503 tăng song song.
Alert rule Prometheus 2 mức cho production (deep dive Group 15):
groups:
- name: shop_api_pool
rules:
- alert: PoolHighUtilization
expr: shop_db_pool_utilization > 0.8
for: 2m
labels:
severity: warning
annotations:
summary: "Shop API pool > 80% utilization"
description: "Pool đang dùng {{ $value | humanizePercentage }}, cần scale lên."
- alert: PoolExhausted
expr: shop_db_pool_utilization >= 1.0
for: 30s
labels:
severity: critical
annotations:
summary: "Shop API pool exhausted — scale up needed"
description: "Pool đầy, request mới sẽ 503 sau acquire_timeout."
3 action khi alert fire theo runbook đề xuất:
- Check Postgres
max_connectionscòn slot không:psql -c 'SHOW max_connections; SELECT count(*) FROM pg_stat_activity;'. - Tăng
POOL_MAX_CONNqua env hoặc add replica (rolling update K8s), không restart full fleet. - Audit query slow gây giữ connection lâu —
pg_stat_statements+ EXPLAIN ANALYZE; B59 sẽ deep dive dynamic query timeout per endpoint.
Pattern phổ biến: alert PoolHighUtilization 80% chạy 2 phút để loại bỏ false positive spike ngắn, alert PoolExhausted 100% chạy 30 giây vì critical phải react nhanh.
Verify End-To-End + Wire Routes
Cập nhật crates/shop-api/src/routes/mod.rs để khai báo 2 module mới:
// File: crates/shop-api/src/routes/mod.rs
pub mod demo_async;
pub mod demo_error;
pub mod health; // refactor B57 — 2 handler liveness + readiness
pub mod metrics; // NEW B57 — Prometheus exposition format
pub mod products;
pub mod version;
Router cuối cùng đảm bảo 3 endpoint mới có trong stack:
// File: crates/shop-api/src/router.rs
use axum::{routing::get, Router};
use crate::{routes, state::AppState};
pub fn build_router(state: AppState) -> Router {
let api_v1 = Router::new()
.merge(routes::products::routes());
Router::new()
.route("/", get(routes::root::root))
.route("/version", get(routes::version::version))
.merge(routes::health::routes())
.route("/metrics", get(routes::metrics::metrics))
.nest("/api/v1", api_v1)
// middleware stack giữ nguyên B50
// (compression > decompression > request_id > enrich_error)
.with_state(state)
}
Full benchmark workflow đề xuất ghi vào scripts/bench.sh để tái lập sau:
#!/usr/bin/env bash
# scripts/bench.sh
set -euo pipefail
# Start server (background)
cargo run -p shop-api &
SERVER_PID=$!
trap "kill $SERVER_PID" EXIT
# Wait warm-up
sleep 3
# Baseline JSON output
oha -z 30s -c 50 -j 'http://localhost:3000/api/v1/products' > baseline.json
# Parse output
jq '.summary.requestsPerSec' baseline.json
jq '.latencyPercentiles."99"' baseline.json
# Snapshot metrics
curl -s http://localhost:3000/metrics | grep pool > pool-snapshot.txt
Suggested commit: B57: /health/live + /health/ready + /metrics Prometheus + oha benchmark.
Tổng Kết
ohaRust-native load test tool — TUI real-time + HTTP/2 + JSON output cho CI parse.- Pool metrics API:
pool.size(),pool.num_idle(),pool.num_active() = size - num_idle. - Liveness vs Readiness pattern Kubernetes:
/health/liveprocess alive (200 luôn) — restart pod khi fail./health/readyaccept traffic (DB OK + pool < 95% utilization) — take pod out of rotation khi fail.
/metricsPrometheus format manual text — đủ cho basic, scale lênmetricscrate ở G15.- 5 metrics Shop API:
shop_db_pool_size,shop_db_pool_idle,shop_db_pool_active,shop_db_pool_max,shop_db_pool_utilization. - Alert threshold: 80% warning (for 2m), 100% critical (for 30s).
- 3 action khi alert fire: check Postgres
max_connections, tăngPOOL_MAX_CONNhoặc add replica, audit slow query. - Stress test workflow:
oha+watch metrics+ parse JSON output. - Pool tune sweet spot: 20 default OK cho dev/staging; tune theo CPU cores DB server (rule of thumb cores × 2 + spindles).
- Diminishing return pool size > sweet spot: thêm overhead Postgres process per connection + context switch, RPS không tăng còn p99 tệ hơn.
- File path lock:
crates/shop-api/src/routes/{health.rs, metrics.rs}(mới + refactor B5). - Foundation cho B58 (sqlx offline cache + CI), Group 15 OpenTelemetry tracing deep.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
ohaso vớiwrk/heykhác gì? Tại sao chọnohacho Rust project?- Liveness vs Readiness probe — Kubernetes xử lý khác nhau ra sao? Use case mỗi loại.
- Pool metrics 3 con số chính (
size/idle/active) — quan hệ toán học ra sao? - Alert threshold 80% warning vs 100% critical — tại sao 2 mức? Action mỗi mức là gì?
- Pool size sweet spot tune theo gì? Tại sao tăng quá cao lại làm RPS giảm?
Đáp án
ohavswrk/hey: cả 3 đều là HTTP load test tool nhưng khác ngôn ngữ + ergonomic.wrk(C, 2012) hiệu năng cao nhất nhưng cần Lua script cho complex scenario, chỉ output text plain cuối session khó parse CI, không TUI;hey(Go, 2016) đơn giản dễ dùng nhưng cũ — không HTTP/2, không TUI, output text only.oha(Rust, 2020, Hatoo) ergonomic hơn nhờ 4 điểm: (a) TUI real-time hiển thị latency histogram + RPS + status code distribution suốt quá trình test thay vì chỉ summary cuối, giúp phát hiện sớm degradation; (b) HTTP/2 + HTTP/1.1 đầy đủ — axum 0.8 hỗ trợ HTTP/2 native qua hyper, test endpoint thật mới hợp; (c) JSON output qua flag-jcho CI parse vớijqextract field.summary.requestsPerSec,.latencyPercentiles."99", threshold guard trong GitHub Actions YAML; (d) flag UX hiện đại —-z 30sduration mode tự nhiên,-cconcurrent,-ntotal request,-Hheader,-dbody — không phải nhớ syntax Lua như wrk. Tại sao chọnohacho Rust project: (1) cài quacargo installcùng toolchain dev — không phải brew/apt riêng, CI build container đã có cargo cài thêm 30s; (2) cùng async ecosystem tokio nội bộ, scaling concurrent client tới hàng nghìn không tốn nhiều thread như wrk pthread; (3) maintenance active 2026 (wrk last commit 2019, hey last commit 2021); (4) ecosystem Rust — dev đã quen Rust khi đọc source debug nếu cần. Lock Shop API:ohaMANDATORY cho local benchmark + CI threshold check.- Liveness vs Readiness probe Kubernetes: 2 loại probe khác nhau hoàn toàn về action khi fail, không thay thế lẫn nhau. Liveness probe trả lời câu hỏi "process còn alive không?" — endpoint chỉ check tokio runtime còn chạy được async function (vd
/health/livetrảStatusCode::OKđơn giản, không touch DB không touch pool không acquire resource). K8s action khi liveness fail:kill container+restart pod(saufailureThresholdconsecutive fail, default 3). Use case: deadlock thread, infinite loop, OOM gần kill — process còn nhưng không respond, restart pod refresh state. Cảnh báo: KHÔNG check DB trong liveness vì DB down → mọi pod fail liveness → mọi pod restart cùng lúc → cascade failure. Readiness probe trả lời câu hỏi "pod sẵn sàng nhận traffic không?" — endpoint deep check dependency (DB, Redis, pool capacity). K8s action khi readiness fail: take pod out of Service endpoints (load balancer ngừng route traffic về pod), pod vẫn chạy không bị restart, không bị thay thế. Sau khi readiness pass lại, pod tự động được đưa vào rotation. Use case: (1) startup pod chưa migrate xong DB → readiness fail trong vài giây đầu, LB không gửi traffic; (2) pool exhausted utilization > 95% → fail tạm thời để LB chuyển traffic sang pod khác còn slot; (3) DB ping fail tạm thời → take pod out rồi khi DB up readiness pass lại tự động put back in rotation. Endpoint Shop API:/health/livechỉ trả 200 không chạm DB không acquire connection (cách ly khỏi DB outage),/health/readyping DBSELECT 1+ utilization < 0.95 (deep check). Config K8s YAML pattern: livenessProbe period 30s timeout 1s threshold 3, readinessProbe period 5s timeout 1s threshold 1 (react nhanh hơn liveness vì action nhẹ hơn — out of rotation reversible, restart pod expensive). - Pool metrics quan hệ toán học: sqlx
PgPoolexpose 2 method public về count connection + method thứ 3 tính bằng phép trừ.pool.size() -> u32trả tổng connection vật lý hiện có trong pool, bao gồm cả idle (đang chờ) + active (đang query); pool lazy grow theo demand không tạo đủmax_connectionsngay từ đầu — startup chỉ cómin_connectionswarm trước.pool.num_idle() -> usizetrả số connection idle, sẵn sàng cấp cho handler tiếp theo gọipool.acquire().num_active = size - num_idle as u32phép trừ tính connection đang được handler giữ chạy query — sqlx KHÔNG expose methodnum_active()trực tiếp, phải tính. Quan hệ:size = num_idle + num_activeluôn đúng +size <= max_connectionsluôn đúng. Pattern monitor: (a)num_active > max × 0.8kéo dài → pool gần exhausted, capacity warning, scale up; (b)num_active = maxliên tục → pool exhausted, request mới 503 sau acquire_timeout; (c)num_idle > max × 0.9liên tục → over-provisioned, có thể giảm pool tiết kiệm RAM Postgres; (d)size < max × 0.5+ idle = full → traffic thấp, pool chưa cần grow, OK. Lưu ý:num_idle()trảusizetrong sqlx 0.8 (không phải u32) vì internal dùngVecDeque::len()— castas u32khi phép tính trừ. Convention naming Prometheus tag:shop_db_pool_*consistent với env varPOOL_*lock B56. Theo dõi qua time series Prometheus thấy pattern traffic peak/off-peak, kích thước pool đúng sai khi capacity planning. - Alert threshold 80% warning vs 100% critical: 2 mức + 2 thời gian khác nhau (for 2m vs for 30s) thiết kế theo nguyên tắc "warn early + escalate fast khi critical". Mức 80% warning (
shop_db_pool_utilization > 0.8for 2 phút): chưa cấp bách nhưng dấu hiệu sớm pool sắp đầy — phải scale lên trước khi tới 100%. Yêu cầu 2 phút liên tục để loại bỏ false positive — spike traffic 30 giây (cron job đồng loạt, bot crawl burst, cache miss đồng thời) không đủ điều kiện báo động, traffic sustained mới fire. Action mức 80%: (1) log notification non-paging vào Slack #alerts channel cho engineer trực thấy; (2) trigger autoscaler nếu có (K8s HPA scale up replica + 1 instance); (3) review query slow trong 5 phút gần đây qua pg_stat_statements. Mức 100% critical (shop_db_pool_utilization >= 1.0for 30 giây): pool đã full, request mới 503 sau acquire_timeout 5s — user-facing error đang xảy ra. Yêu cầu chỉ 30 giây vì critical impact user, không chờ lâu được. Action mức 100%: (1) trigger PagerDuty/Opsgenie page on-call engineer cấp tốc, không vào Slack vì có thể engineer không thấy ngoài giờ; (2) immediate scale up qua kubectl scale hoặc tăngPOOL_MAX_CONNqua ConfigMap roll out; (3) check Postgresmax_connectionscòn slot không (SHOW max_connections; SELECT count(*) FROM pg_stat_activity;) — nếu Postgres đã full thì scale pool app không giúp, phải scale Postgres (vertical lên instance lớn hơn hoặc PgBouncer transaction mode); (4) post-mortem sau incident với root cause analysis. Pattern industry: alert level = warning + critical (2 mức), không 3+ mức vì rối engineer (more is less). Threshold 80% là sweet spot industry-wide — Datadog/Grafana template default, đủ buffer ~20% scale up trước khi user impact. - Pool size sweet spot tune theo gì + tại sao tăng quá cao RPS giảm: pool size sweet spot tune theo 2 yếu tố chính: (a) CPU cores DB server — Postgres process per connection chứ không phải thread, mỗi connection chạy 1 backend process Linux độc lập; nếu pool > cores thì OS phải context switch giữa các process, overhead tăng phi tuyến, throughput không tăng còn latency tệ; (b) workload characteristic — OLTP (short query, high concurrency, point lookup) khác OLAP (long query, low concurrency, aggregation). Rule of thumb industry:
pool_size = (CPU cores × 2) + effective_spindlescho OLTP — vd DB server 4 core + SSD (effective_spindles ~1) → pool ~9, làm tròn lên 10; PostgreSQL official wiki khuyến nghị tương tự (HikariCP analytics chứng minh số liệu này). Tại sao tăng quá cao RPS giảm — 3 nguyên nhân: (1) Postgres process overhead — mỗi connection 1 backend process ~10MB RAM + slot pg_stat_activity + entry pg_locks; 200 connection × 10MB = 2GB RAM Postgres dành cho idle connection, RAM thiếu cho buffer cache → query phải hit disk → slow; (2) context switch OS — kernel scheduler phải switch giữa nhiều process, mỗi switch ~5-10μs (TLB flush + cache miss), 200 process active đồng thời trên 4 core → mỗi process chỉ được time-slice ngắn, overhead schedule lớn hơn work thực; (3) lock contention nội bộ Postgres —pg_lockstable contention tăng phi tuyến với số connection, mỗi lock acquire phải scan list khác → bottleneck SpinLock kernel-level. Kết quả benchmark thực nghiệm community + bài này: pool 20 → 4180 RPS p99 34ms, pool 100 → 4250 RPS p99 35ms (gain ~2% không đáng kể), pool 200 → 3900 RPS p99 45ms (degrade ~7%). Diminishing return chứng minh: tăng pool > sweet spot KHÔNG có lợi, chỉ tốn RAM Postgres + giảm performance. Cách scale đúng: dùng PgBouncer transaction mode gom 1000 client connection app → ~20 server connection Postgres (lock B56), scale ngang qua replica + read replica với load balancer route read-only queries.
Bài Tiếp Theo
Bài 58: sqlx Offline Cache + CI Workflow — deep .sqlx/ offline cache + cargo sqlx prepare command + GitHub Actions workflow với SQLX_OFFLINE=true + Docker compose Postgres testcontainer integration test, áp Shop API CI/CD pipeline.
