跳至内容

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 1200002

metrics 同时需要:

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 + Lockblocking graph
active + IOplan/buffers/storage
active + Client*application consume/send
idle in transactionleaked transaction/locks/vacuum impact
no backend + app pool waitapp-side admission
PgBouncer waiting + few DB slotspooler/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=2

SQLSTATE 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。

参考资料


上一节:会话状态与连接池陷阱 · 返回本章目录 · 下一节:部署与接入 pg36_shop · 查看全书目录 · 查看索引中心

最后更新于