12.5 服务级可观测性
一次请求跨过 HTTP、应用 pool、PgBouncer、PostgreSQL transaction 与 outbox。每层都有自己的 identity:
request/trace ID one logical request or business flow
idempotency key one replayable business command
application_name one low-cardinality workload class
query identity one normalized SQL shape
transaction/backend one execution attempt
outbox event key one publishable fact
release identity one application/database artifact把它们全塞进一个字符串会失去可聚合性;一个都不关联又无法从用户症状走到数据库证据。
12.5.1 请求、事务、查询指纹与 application_name
Identity 分层
本章的关联关系:
request:
X-Request-ID=trace-order-001
database session class:
application_name=pg36-ch12-api
durable facts:
sales_order.trace_id=trace-order-001
outbox.trace_id=trace-order-001
outbox.event_key=order:order-001:placed
business replay:
request_key=order-001
fingerprint=sha256(canonical command fields)trace_id 告诉你一次调用从哪里来;request_key 告诉数据库它是否与先前命令是同一个业务意图。两者不能互换:
- retry 可以有新 trace,但保留同 idempotency key;
- 同一 trace 可能跨多个内部调用;
- trace 通常有保留期,idempotency ledger 是业务状态;
- trace 不应承担 uniqueness/authorization。
application_name 保持低基数
本章所有业务 backend:
application_name=pg36-ch12-api不要每请求改成:
pg36-api/tenant-123/user-456/trace-abcdef...否则:
- metrics cardinality 爆炸;
pg_stat_activity分组失去意义;- PgBouncer tracking/reset 行为更复杂;
- 日志与 dashboard 成本上升;
- 可能泄露 tenant/user。
合理粒度:
pg36-api-rw
pg36-worker-outbox
pg36-migration
pg36-read-report必要时附固定 version/channel,但先评估 cardinality。per-request identity 放结构化 log/trace,不放 session label。
Query identity 不等于 raw SQL + values
观测 query 应优先关联:
- PostgreSQL
query_id/pg_stat_statements.queryid; - normalized query text;
- driver operation name;
- route/query bundle mapping;
- application version。
不要把 password、token、PII、完整 JSON 或 payment data 作为 metric label。参数对诊断重要时:
- 保存经过批准的 bounded class,例如
sku_class=known/missing; - 对值 hash/pseudonymize;
- 只在短期受控 evidence 中保留;
- 遵循日志保留与访问政策。
第 8 章已说明 query text 与 parameter identity 缺一不可;本章增加了 API route 与 trace,但没有取消隐私边界。
Transaction attempt 与 logical request
trace-retry-001 的服务请求只返回一次,数据库执行了两个 transaction attempt:
attempt 1 → 40001 rollback
attempt 2 → commit order 1200002metrics 同时需要:
request total=1
transaction retry total=1
SQLSTATE 40001 total=1
committed order total=1如果只看 HTTP 201,会漏掉 serialization pressure;如果把每次 attempt 都算一个请求,会夸大业务流量。
结构化日志
正常请求:
{
"msg": "request",
"route": "orders.create",
"status": 201,
"duration_ms": 3.004,
"trace_id": "trace-order-001"
}数据库 timeout:
{
"msg": "request_error",
"error_code": "database_timeout",
"status": 504,
"retryable": true,
"trace_id": "trace-timeout-001",
"sqlstate": "57014",
"constraint": ""
}日志不包含:
PG36_DATABASE_URL
password
raw request body
full SQL arguments
stack trace in client response不是所有错误都要打 stack;预期 domain conflict 应按可聚合 code 记录,真正未知 defect 才需要更深内部 cause。
12.5.2 延迟、错误、连接等待和数据库等待
一个慢请求至少有四段
[ T_{request} = T_{app}
- T_{pool}
- T_{db}
- T_{response} ]
T_db 还可拆:
parse/plan
execution CPU/IO
lock wait
client read/write wait
commit/WAL仅看总延迟无法决定加 index、加 connection 还是减并发。
应用 pool wait
pgxpool 暴露:
AcquireCount
AcquireDuration
EmptyAcquireCount
EmptyAcquireWaitTime
CanceledAcquireCount
AcquiredConns
IdleConns
TotalConns
MaxConns本章转换成:
pg36_pool_acquire_total
pg36_pool_acquire_seconds_total
pg36_pool_empty_acquire_total
pg36_pool_canceled_acquire_total
pg36_pool_connections{state=...}CanceledAcquireCount 上升表示请求在拿到 DB connection 前就耗尽 context。此时 PostgreSQL pg_stat_activity 看不到对应 query;从数据库侧“没有慢 SQL”并不能证明数据库路径无关,连接预算或 pool queue 可能已经挡在前面。
PgBouncer wait
通过 Pigsty primary service 时还有 PgBouncer 队列。需要结合:
- PgBouncer client active/waiting;
- server active/idle;
- pool size/reserve;
- average/max wait;
- connection errors;
- user/database pool identity;
- HAProxy/service health。
app pool 不等待、PostgreSQL backend 也不多,仍可能是 PgBouncer client 在等 server slot。第 22 章会用 SHOW POOLS / SHOW STATS 和 Pigsty dashboard 深化。
PostgreSQL wait
取得 backend 后,观察:
SELECT
pid,
application_name,
state,
wait_event_type,
wait_event,
query_id,
xact_start,
query_start
FROM pg_catalog.pg_stat_activity
WHERE application_name = 'pg36-ch12-api';典型解释:
| state/wait | 方向 |
|---|---|
| active + Lock | blocking graph |
| active + IO | plan/buffers/storage |
| active + Client* | application consume/send |
| idle in transaction | leaked transaction/locks/vacuum impact |
| no backend + app pool wait | app-side admission |
| PgBouncer waiting + few DB slots | pooler/server budget |
不要把 state=active 等同于“在用 CPU”;wait event 才说明当前等待类型。
Error 指标保留 SQLSTATE 与领域 code
本章同时记录:
HTTP route/status class
domain error code in logs
SQLSTATE counter
transaction retries
idempotent replays一次运行:
pg36_db_errors_total{sqlstate="40001"}=1
pg36_db_errors_total{sqlstate="57014"}=1
pg36_transaction_retries_total=1
pg36_idempotent_replays_total=2SQLSTATE cardinality 是 bounded vocabulary;constraint name 通常也相对 bounded,但 table/tenant/raw error message 不适合无审查直接做 label。
Rate、error、duration 与 saturation 同看
最小服务面板:
traffic:
requests/s by route
errors:
status class + domain code + SQLSTATE
duration:
p50/p95/p99 request
DB/query duration
pool acquire duration
saturation:
app pool acquired/max/wait/cancel
PgBouncer wait/server slots
PostgreSQL active/wait/CPU/IO平均值会隐藏 tail。pool wait 若只占 1% 请求,平均可能很小,p99 已经超时。
12.5.3 从一次请求追到数据库证据
一条可执行调查路径
用户报告下单超时,先固定:
UTC window
route=orders.create
trace_id
idempotency key if authorized
application version
service endpoint/pool mode然后沿层次走:
1. application log
status/domain code/duration/trace
2. pool metrics
acquired/max/acquire wait/canceled
3. PgBouncer/Pigsty
client wait/server slots/service target
4. PostgreSQL
application_name/query_id/wait/blocker/SQLSTATE
5. durable business evidence
idempotency ledger/order/outbox
6. client outcome
response received, timed out, or unknown最后一步不是“SQL 后来成功了吗”,而是:
logical command committed?
what response is persisted?
is replay safe?
is an outbox event pending?实测 trace 关系
本章 observer 独立于 app 查询:
order 1200001:
sales_order.trace_id=trace-order-001
payment 1200001:
payment.trace_id=trace-payment-001
outbox:
order:order-001:placed
→ trace-order-001
order:order-retry:placed
→ trace-retry-001
payment:pay-001:captured
→ trace-payment-001
pg_stat_activity:
distinct application_name=[pg36-ch12-api]这让 operator 能从 request 走到 committed fact,又保持 database session label 可聚合。
诊断一次 pool exhaustion
实验时间线:
t0:
holder-1 acquires connection
holder-2 acquires connection
database active sleepers=2
t1:
/health/live → 200
t2:
/health/ready waits its 150 ms budget
→ 503 pool_unavailable
t3:
business GET waits 100 ms request budget
→ 503 pool_unavailable
t4:
holders complete 200
acquired returns 0
readiness → 200证据组合:
app pool max=2
canceled acquisition=2
PostgreSQL had exactly two sleepers
no business SQL for rejected GET
liveness unaffected
recovery without process restart这排除了“PostgreSQL query 本身超时”,并证明 admission queue 按预算失败。
诊断一次 statement timeout
trace=trace-timeout-001
fault=statement-timeout
SET LOCAL statement_timeout=50ms
pg_sleep(200ms)
SQLSTATE=57014
HTTP=504 database_timeout
state snapshot before == after若只看 504,会与 gateway timeout 混淆;若只看 57014,又会与 client cancel 混淆。timeline + source timeout + state comparison 才是完整证据。
Unknown commit 的调查
客户端 timeout 后不要先删 key 再重试。先用相同 idempotency key 查询/重放:
ledger has completed response
→ return it; command committed
ledger absent
→ safe to attempt command
ledger exists but incomplete
→ protocol defect/manual repair path本章 schema constraint 允许 transaction 内暂时 response=NULL,但完整 transaction 失败会 rollback;正常 committed state 只能是 key+order/payment+response 完整。verify 把 incomplete committed row 当失败。
本节检查表
- trace、idempotency、application、query、event identity 分层;
-
application_name是低基数 workload class; - request retry 与 transaction attempt 分开计数;
- log 使用结构化 code,不泄露 secret/raw payload;
- request、pool、PgBouncer、DB 延迟可分别观察;
- pool canceled acquire 有独立 metric;
-
pg_stat_activity同时看 state 与 wait event; - SQLSTATE 与 domain code 都保留;
- tail latency 与 saturation 同窗;
- trace 能关联到 durable order/payment/outbox;
- unknown commit 通过 ledger 判断,不凭网络结果猜;
- 故障结论同时包含反证与 committed state。
参考资料
- PostgreSQL 18:Monitoring Database Activity
- PostgreSQL 18:pg_stat_activity
- PostgreSQL 18:Error Codes
- pgx v5.10.0:pgxpool Stat
- Pigsty:PostgreSQL Dashboards
- Pigsty:PgBouncer Administration
上一节:会话状态与连接池陷阱 · 返回本章目录 · 下一节:部署与接入 pg36_shop ·
查看全书目录 · 查看索引中心