哪些错误会被 nginx 换到下一个上游重试?5xx 算不算?POST 这样的非幂等请求重试了会怎样?换几次由谁决定?这篇用 10 种上游故障 × 6 种配置的矩阵,加订单重复创建的实验量出来。
A 的故障 | 默认 (error timeout) | + http_500 | + http_502 http_503 http_504 | + invalid_header | + http_404 | off ------------------------------------------+--------------------+----------------+------------------------------+------------------+----------------+--------------- 连接被拒 | B(重试了) | B(重试了) | B(重试了) | B(重试了) | B(重试了) | 502 读响应超时(沉默 1.8 s) | B(重试了) | B(重试了) | B(重试了) | B(重试了) | B(重试了) | 504 连接被重置(收到请求后 RST) | B(重试了) | B(重试了) | B(重试了) | B(重试了) | B(重试了) | 502 状态行就不是 HTTP(整行垃圾) | BadStatusLine | BadStatusLine | BadStatusLine | BadStatusLine | BadStatusLine | BadStatusLine 状态行正常,但有一行响应头没有冒号 | 502 | 502 | 502 | B(重试了) | 502 | 502 HTTP 500 | 500 | B(重试了) | 500 | 500 | 500 | 500 HTTP 502 | 502 | 502 | B(重试了) | 502 | 502 | 502 HTTP 503 | 503 | 503 | B(重试了) | 503 | 503 | 503 HTTP 504 | 504 | 504 | B(重试了) | 504 | 504 | 504 HTTP 404 | 404 | 404 | 404 | 404 | B(重试了) | 404 响应头发完后只给 2 个字节的响应体就断开(Content-Length 100) | IncompleteRead | IncompleteRead | IncompleteRead | IncompleteRead | IncompleteRead | IncompleteRead 默认 error timeout 只处理“连接/读写出错”和“超时”:HTTP 5xx 的应答是上游认真回的,nginx 默认原样转给 client;要重试 5xx 必须显式加 http_5xx;invalid_header(状态行正常、但某行响应头不合法)在默认下不重试,client 收到 nginx 的 502;如果连状态行都不是 HTTP,nginx 按 HTTP/0.9 的简单响应处理,把那一行原样当作响应体转给 client,client 的 HTTP 库抛 BadStatusLine,换上游的 invalid_header 也不会触发;off 关掉所有重试;响应头已经发给 client 之后(最后一行)无论怎么配都不能换上游
默认只有连接错误和超时;5xx 要显式写 http_5xx;响应体发了一半无论怎么配都不能换上游。
配置 | client 看到的状态码 | 订单被 A 记下 | 订单被 B 记下 | 同一个订单被 A 和 B 都记下(重复创建) | 共有多少个订单被记下过
--------------------------------------------------+--------------------+----------+----------+------------------------+------------
默认 | {504: 50, 200: 50} | 50 | 50 | 0 | 100
proxy_next_upstream error timeout non_idempotent; | {200: 100} | 50 | 100 | 50 | 100
默认配置下,client 约一半的请求(落在 A 的)拿到 504,但这些订单其实已经被 A 记下了(client 以为失败,实际成功);non_idempotent 让 nginx 在超时后把 POST 重发给 B,client 拿到 200,代价是同一个订单在 A 和 B 都被创建一次。解决办法是让 POST 带幂等键,而不是靠网关的重试策略POST 超时后默认不重试,client 看到失败但订单已创建;允许重试则会重复创建。要靠幂等键。
配置 | client 看到的状态码 | 上游收到的总次数 | 平均每个请求 | A1 / A2 / A3 各收到
--------+---------------+----------+--------+-----------------
默认 | {503: 30} | 90 | 3.0 | [30, 30, 30]
tries 2 | {503: 30} | 60 | 2.0 | [15, 30, 15]
tries 1 | {503: 30} | 30 | 1.0 | [10, 10, 10] 默认试遍所有上游,会把一个失败的请求放大;tries 封顶。