Python后端中间件专题24:代理超时不是业务超时——body、连接、TLS 与边缘限流
用户上传一个 1 MiB 多一字节的请求,和 API 已接收请求却长时间不返回,是两种事故。一个应尽早由边缘拒绝;另一个涉及代理等待、上游尝试与业务执行的不确定性。把二者都说成“后端超时”,会使值班人员在错误组件上调参数。
一张状态码分诊表的前置
本课要能按client_max_body_size、client_body_timeout、proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout找到拒绝发生在哪一段,并能说明 TLS 终止、边缘粗粒度限流和 06 Redis 业务预算的不同责任。前置为 23 的 Nginx 单入口、06 的保护型写入限流、07 的幂等重试;真实 413/504 和证书实验只在获准的远端环境进行。
上一课练习答案
答案 EX-23-01 [code]
保存ex23_entry_audit.py于project/;这是配置静态审计,不向服务发包:
frompathlibimportPathimportre config=Path("docker/nginx/nginx.conf").read_text(encoding="utf-8")checks={"two_upstreams":all(serverinconfigforserverin("server api-a:8000","server api-b:8000")),"two_tries":"proxy_next_upstream_tries 2;"inconfig,"trusted_ip":bool(re.search(r"proxy_set_header\s+X-Forwarded-For\s+\$remote_addr;",config)),"generated_id":bool(re.search(r"proxy_set_header\s+X-Request-ID\s+\$request_id;",config)),}assertall(checks.values()),checksforname,passedinchecks.items():print(f"{name}={passed}")运行命令:python ex23_entry_audit.py。
预期输出:
two_upstreams=True two_tries=True trusted_ip=True generated_id=True静态审计不能证明实际 DNS 解析、两个实例均能接单或停机后的重试路径,后者留给 23 的远端 e2e selector。
答案 EX-23-02 [prose]
api-a 已提交,但响应丢失,Nginx 可能把请求重送到 api-b;若客户端也重试,次数更多。HTTP 代理无法判断第一次 SQL commit 是否成功,不能许诺写请求只执行一次。客户端维持相同Idempotency-Key和请求指纹,07 的 SQL 唯一约束才使同键同体重试拿到同一工单;同键异体应冲突。/live是代理自己生成的固定 200,仅证明 Nginx 在接请求;/health转到 API 并检查 PostgreSQL,可观察应用就绪。先核对第二实例和数据库健康,再查 Nginx upstream 错误与重试,再恢复故障实例,最后用业务 ID/幂等键核对有无重复事实,不能先清空数据库或盲目重发。
图上的三只计时器
client_max_body_size 1m限制 Nginx 接受的 body;超过阈值返回 413,不是 FastAPI 的字段验证 422。client_body_timeout 10s管客户端传 body 时相邻读取的等待,不能把它当成整个请求的固定 10 秒预算。proxy_connect_timeout 2s管建立到上游的连接;proxy_send_timeout 30s管把请求送到上游时相邻写操作;proxy_read_timeout 30s管从上游相邻读操作,不是业务执行的端到端 SLA。send_timeout 30s管边缘向慢客户端发送响应。超时与proxy_next_upstream_tries 2叠加,所以两个被暂停的上游可能约 60 秒才最终返回 504;准确耗时必须实测,不能用配置推定业务在 30 秒时已回滚。
413、502、504、429 各指向哪里
| 状态 | 优先看 | 对业务的含义 |
|---|---|---|
| 413 | Nginx body 大小与Content-Length/流式上传 | 通常未进入 API,缩小请求;不要放宽所有端点绕过保护 |
| 502 | 上游连接失败、协议/提前断开、网关错误 | 检查 API 进程和 upstream error,不等于 SQL 一定失败 |
| 504 | 建连/发送/读取超时或重试耗尽 | 请求可能已执行,应以幂等键和 SQL 事实核对 |
| 429 | 限流器明确拒绝 | 本课 Nginx 配置没有limit_req,TicketFlow 的 06 Redis 业务预算另行提供拒绝语义 |
边缘 IP 限流适合吸收明显突发流量,业务 Redis 预算按租户/端点保证原子计数与故障策略;只依赖$binary_remote_addr的边缘规则还会误伤 NAT 后用户。课程的快照未配置 Nginxlimit_req_zone/limit_req,因此不能把 429 当作本课已有边缘验收。若要加,应在http定义有界 zone、在路由启用并另做真实并发/可信 IP 测试,不能靠文本中出现一个示意指令便算完成。
TLS 终止的实际边界
主nginx.conf只监听 80。项目另有compose.tls.yml挂载nginx-tls.conf与证书目录,监听 443、声明 TLSv1.2/1.3,并把X-Forwarded-Proto https传给 API。静态合同可在project/跑python -m pytest tests/unit/test_edge_acceptance_config.py::test_tls_overlay_exposes_loopback_https_and_mounts_certificates -q。获准远端的隔离project/、已启动独立 Compose 项目中,可用仅一天有效、含 IP SAN 的临时测试证书演练;这不是公网证书部署步骤:
test-n"$COMPOSE_PROJECT_NAME"||{echo'set isolated COMPOSE_PROJECT_NAME'>&2;exit1;}exportTICKETFLOW_TLS_DIR="$(mktemp-d)"openssl req-x509-newkeyrsa:2048-noenc-days1\-keyout"$TICKETFLOW_TLS_DIR/ticketflow.key"\-out"$TICKETFLOW_TLS_DIR/ticketflow.crt"\-subj'/CN=localhost'-addext'subjectAltName=DNS:localhost,IP:127.0.0.1'chmod600"$TICKETFLOW_TLS_DIR/ticketflow.key"dockercompose-fcompose.yml-fcompose.tls.yml-p"$COMPOSE_PROJECT_NAME"config>/dev/nulldockercompose-fcompose.yml-fcompose.tls.yml-p"$COMPOSE_PROJECT_NAME"up-dnginxdockercompose-fcompose.yml-fcompose.tls.yml-p"$COMPOSE_PROJECT_NAME"execnginx nginx-tcurl--fail-with-body--cacert"$TICKETFLOW_TLS_DIR/ticketflow.crt"https://127.0.0.1:8443/health预期nginx -t报配置校验成功,curl成功校验证书并返回 API/healthJSON;实际结果取决于证书工具、隔离项目和服务健康,本次未运行。若 TLS 端口被覆写,URL 需改为NGINX_TLS_PORT的实际值。测试后仅处理这次mktemp给出的精确目录,别清理其他项目的证书。overlay 是否保留 HTTP 端口、证书轮换和公网暴露策略另需审核;不要将curl -k的握手成功写成证书信任验证通过。
证据、恢复与选择器
正式本地选择器:python tools/verify_executable_revisions.py --lesson 24 --python python,会独立安装本课 revision 并运行 baseline 与 09–24 全部历史合同。真实 413/504 另需在获授权隔离远端运行python scripts/run_remote_selector.py --checkpoint 24 --selector tests/e2e/test_nginx_limits.py::test_nginx_rejects_oversized_body_and_distinguishes_upstream_timeout;本地不启动 Docker,未运行远端仍为 PENDING。
若 413 没来,查实际加载配置与入口端口,不要先断言 FastAPI 拒绝;若 504 很快出现,区分连接拒绝的 502/快速重试与read_timeout;若超时后工单存在,以原幂等键查询,不能因 504 直接再建一张单。TLS 失败则先确认挂载文件和服务端证书 SAN、有效期、信任链,而不是关闭验证。
本课练习
练习 EX-24-01 [code]
写project/ex24_limit_audit.py:静态读取配置,断言 body 上限确为1m、连接超时2s、读取超时30s,计算边界字节数,打印limit=1048576 over=1048577。说明为什么这个脚本不证明 Nginx 返回过 413。
练习 EX-24-02 [prose]
按同一次事故里的 413、504、429 分别指出哪个组件应给出证据;解释为何代理 504 之后不能断定数据库回滚,TLS overlay 的静态检查又不能证明浏览器的证书信任。给出不要在日志里记录请求 body 的理由。
结论和预告
边缘限制约束传输,却不能告诉我们哪个工单、哪个事件最终失败。25 将让 API、Outbox relay 与 Worker 的事件使用同一批相关标识,并在写日志之前脱敏。
完整核心模块:边缘大小与超时界限
worker_processes auto; pid /tmp/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; server_tokens off; access_log /dev/stdout; error_log /dev/stderr warn; upstream ticketflow_api { least_conn; server api-a:8000 max_fails=1 fail_timeout=5s; server api-b:8000 max_fails=1 fail_timeout=5s; keepalive 32; } server { listen 80; server_name _; client_max_body_size 1m; client_body_timeout 10s; send_timeout 30s; location = /live { access_log off; add_header Content-Type text/plain; return 200 'nginx alive\n'; } location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; # Do not relay client-supplied forwarding or request identity headers. proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Request-ID $request_id; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 2s; proxy_send_timeout 30s; proxy_read_timeout 30s; proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_pass http://ticketflow_api; } } }