Python后端中间件专题23两个 API 实例只能有一个入口——Nginx 反代与可信请求头假设api-a和api-b都在工作客户端究竟该记住哪个地址若把两个容器端口都公开客户端就要承担故障发现、切换和安全头处理。TicketFlow 只把 Nginx 的127.0.0.1:8080暴露给部署主机两个 API 名称留在 Compose 网络中。统一入口首先是一条边界其次才是分流器。本课目标与前置读完应能解释upstream与proxy_pass、验证两个不同上游均被使用、在停掉一台 API 后经同一个入口继续读取已创建的租户工单并指出转发头从哪里开始可信。前置是 02 的/live与/health区别、07 的写请求幂等键、22 的搜索故障边界只读静态配置不需要 Docker真实切换必须在获准的远端隔离 Compose 项目运行。上一课练习答案答案 EX-22-01 [code]在已安装开发依赖的project/中保存为ex22_search_failure.py。这里的 ES 是MockTransport不是可用性演练importasyncioimporthttpxfromticketflow.search.http_adapterimportElasticsearchHTTP,SearchUnavailablefromticketflow.search.queryimportSearchFiltersasyncdefmain():asyncwithhttpx.AsyncClient(base_urlhttp://es.test:9200,transporthttpx.MockTransport(lambdarequest:httpx.Response(503)),)asclient:searchElasticsearchHTTP(client,aliasticketflow-tickets)try:awaitsearch.search(tenant_idtenant-a,queryvpn,filtersSearchFilters(),size10)exceptSearchUnavailableaserror:assertstr(error)search service unavailableprint(explicit unavailable)else:raiseAssertionError(503 must not become an empty successful result)asyncio.run(main())运行命令PYTHONPATHsrc python ex22_search_failure.pyPowerShell$env:PYTHONPATHsrc; python ex22_search_failure.py。预期输出explicit unavailable这只证明适配器把 ES 503 转为有类型的不可用错误并没有向真实 ES 发请求。答案 EX-22-02 [prose]PostgreSQL 仍正常时按 ID 读取详情以数据库事实为准既有工单读取可继续创建/修改依照 SQL 事务与 Outbox 提交不应等待 ES 索引可用。搜索端点则显式返回服务不可用项目 API 将SearchUnavailable映射到 HTTP 503而不是200 {hits:[]}后者会让用户误以为“确实没有单”掩盖尚未索引或服务故障。写入成功不意味着搜索立即可见。Nginx 的双上游只处理 API 实例故障两个实例依赖同一个坏掉的 ES切换 API 并不能修复搜索。从图读入口的故障域图中两条上游线不是两份业务数据两台 API 仍共享 PostgreSQL 事实源。upstream ticketflow_api使用least_conn偏向当前活动连接较少的实例不是严格一下一下轮询。max_fails1 fail_timeout5s是 Nginx 被动失败判断不是主动探活proxy_next_upstream_tries 2给单次请求最多两个上游尝试。健康检查必须结合/health的依赖状态而不是只看 Nginx 的静态/live。请求到达代理时客户端可能伪造X-Forwarded-For: 10.0.0.1或X-Request-ID: administrator。配置使用$remote_addr覆盖前者用 Nginx 生成的$request_id覆盖后者还以$scheme/$host/$server_port重建协议、主机与端口。API 只能在直达 API 端口不可被不可信客户端访问、且代理是明确可信跳的前提下使用这些头如果上游还有负载均衡器$remote_addr指向那一跳必须额外配置可信代理来源和 real-IP 规则不能无条件拼接用户传来的 XFF。Host由$host得到也不能被当作用户权限证明。实际怎么验而不是看见 200 就算切换正式本地检查点从课程根运行独立安装 lesson-23 并执行 baseline 与 09–23 的全部历史合同python tools/verify_executable_revisions.py--lesson 23--python python真实故障切换另需在获授权的隔离远端项目运行python scripts/run_remote_selector.py --checkpoint 23 --selector tests/e2e/test_nginx_failover.py::test_public_entry_reads_existing_ticket_after_one_api_replica_stops。该测试才会观察双 upstream、停api-a后读取既有事实并恢复实例本地静态合同不能替代这一证据。单次 200 不证明双实例分流静态/live200 不证明 API 可用。本地未设TICKETFLOW_REMOTE_TESTS1时此 selector 为SKIPremote-service-required不是故障切换 PASS。静态nginx.conf还不能证明线上 TLS、真实请求头覆盖或复原动作成功。对非幂等写请求代理重试可能在确认不确定窗口重复送达07 的Idempotency-Key和 SQL 唯一事实约束仍不可少。分诊卡同一个入口不同的坏法观察下一步不应下的结论/live200/health503分别看 api-a/b 日志和 PostgreSQL 健康、迁移Nginx 活着不等于应用就绪新日志只有一个upstream地址确认第二实例是否启动、连接数与日志时间窗least_conn不保证短样本交替不应据少量请求断言配置失效停 api-a 后仍 502/504查 api-b 进程、网络/DNS、重试次数及超时恢复故障注入双实例本身不保证共同依赖可用应用日志出现客户端伪造的 ID/IP确认只能通过入口访问、检查代理覆写头及实际加载配置不要把任意X-Forwarded-*视为认证本课练习练习 EX-23-01 [code]在project/写一个只读配置审计脚本读取docker/nginx/nginx.conf断言api-a:8000、api-b:8000、proxy_next_upstream_tries 2存在且X-Forwarded-For、X-Request-ID分别被$remote_addr、$request_id覆盖。打印四项检查结果。说明这为什么仍不等于实测双实例故障切换。练习 EX-23-02 [prose]某次写请求在 api-a 执行 SQL commit 后代理没收到响应并重试 api-b。分析何时可能产生重复、由哪一层收敛区分 Nginx/live与 API/health各能证明什么说明停掉唯一一台坏实例时的排障顺序。收束与下一站入口统一了地址和受信头的来源但没有替业务证明成功也没有为请求大小和等待时间定界。24 将用 413、504、TLS 配置把边缘保护与 API 业务规则拆开。完整核心模块Nginx 双实例入口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_fails1 fail_timeout5s; server api-b:8000 max_fails1 fail_timeout5s; keepalive 32; } server { listen 80; server_name _; 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_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_pass http://ticketflow_api; } } }