摘要:生产环境有些服务对耗时非常敏感,所以一般会设置较低的 socket_timeout。在晚上流量高峰期时,就容易出现偶尔的丢包导致的超时异常,我们可以添加适当的重试逻辑来缓解。

问题描述

生产环境偶尔出现 Redis 访问超时或 requests 请求超时异常。

Traceback (most recent call last):
  File "./local/lib/python2.7/site-packages/nameko/containers.py", line 476, in _run_worker
    result = method(*worker_ctx.args, **worker_ctx.kwargs)
  File "./service/xxx.py", line 237, in check_limit
    already, _ = pipe.execute()
  File "./local/lib/python2.7/site-packages/pyzipkin/patcher/redis.py", line 88, in _patched_execute
    return __org_execute__(self, *args, **kwargs)
  File "./local/lib/python2.7/site-packages/redis/client.py", line 2879, in execute
    return execute(conn, stack, raise_on_error)
  File "./local/lib/python2.7/site-packages/redis/client.py", line 2816, in _execute_pipeline
    self.parse_response(connection, args[0], **options))
  File "./local/lib/python2.7/site-packages/redis/client.py", line 2838, in parse_response
    self, connection, command_name, **options)
  File "./local/lib/python2.7/site-packages/redis/client.py", line 680, in parse_response
    response = connection.read_response()
  File "./local/lib/python2.7/site-packages/redis/connection.py", line 624, in read_response
    response = self._parser.read_response()
  File "./local/lib/python2.7/site-packages/redis/connection.py", line 284, in read_response
    response = self._buffer.readline()
  File "./local/lib/python2.7/site-packages/redis/connection.py", line 216, in readline
    self._read_from_socket()
  File "./local/lib/python2.7/site-packages/redis/connection.py", line 187, in _read_from_socket
    raise TimeoutError("Timeout reading from socket")
TimeoutError: Timeout reading from socket

问题原因

经过定位发现线上网络偶尔会出现丢包(参考如何排查Linux服务器网络丢包)的情况,由于 TCP 出现丢包时,协议层会以指数退避(exponential backoff)方式进行重传:

首次重传超时时间为1.5s(Linux 实现实际首次大概是1.1s),然后是3s、6s、12s、24s、48s和多个64s。

所以当我们业务层设置 socket_timeout 小于1.0s时,就很容易出现超时异常。

解决办法

将超时时间设置为1.5s以上(至少允许一次重传)。如果业务无法接受较大的超时时间,可以在设置较小的超时时间(比如300ms),然后在业务层重试。

当然如果是内网环境经常出现丢包,是不正常的,需要找运维人员排查解决。

业务层重试的常见方法

Python requests 自动重试

Python requests 库可以通过注册 adapter 来设置重试策略,比如重试次数、出现何种状态码时重试等:

import requests

session = requests.Session()
adapter = requests.adapters.HTTPAdapter(max_retries=1, status_forcelist=[500, 503])  # 自动重试1次
session.mount('http://', adapter)
session.mount('https://', adapter)

def get_url(url, timeout=1.5):
    return session.get(url, timeout=timeout)  # 使用session还可以利用连接池,避免频繁创建连接

requests 自动重试有几点需要注意(详见源码):

  • 只会对 GET/PUT/DELETE/HEAD/TRACE/OPTIONS 幂等形式的请求进行重试,可以通过method_whitelist=['GET', 'POST']来修改
  • 只会对 DNS 解析错误、链接错误、链接超时、读取超时、协议错误、30x跳转、status_forcelist 对应的 Status 错误进行重试
  • 如果重试一直出现 TimeoutError,则接口耗时最大可以去到(max_retries + 1) * timeout
  • 重试后还失败时会导致返回的错误为 MaxRetryError,而不是确切的异常

Redis 自动重试

通过添加 retry_on_timeout=True,然 Redis client 任何操作遇到 socket timeout 时都会和 ConnectionError 一样重试一次。

import redis

rd = redis.StrictRedis('127.0.0.1', socket_timeout=1.5, retry_on_timeout=True)
rd.get('test')  # 如果遇到Timeout,内部会自动重试一次

Gateway(Nginx)自动重试

对于 HTTP 接口,可以在 Nginx 配置重试策略,当 GET/PUT/DELETE/HEAD/TRACE/OPTIONS 幂等形式的请求遇到 upstream 返回 error、timeout 或500/503错误时,Nginx 自动重试下一个 upstream详见 Nginx 配置文档

location / {
    ...
    proxy_connect_timeout 5s;
    proxy_read_timeout 5s;
    proxy_send_timeout 5s;
    proxy_next_upstream error timeout http_500 http_503;
    proxy_next_upstream_timeout 5s;  # 如果耗时已经超过5s,则不进行重试
    proxy_next_upstream_tries 2;     # 原始请求1次 + 重试1次
}
  • proxy_connect_timeout time;

    与后端/上游服务器建立连接的超时时间,默认为60s,此时间不超过75s。

  • proxy_read_timeout time;

    设置从后端/上游服务器读取响应的超时时间,默认为60s,此超时时间指的是两次成功读操作间隔时间, 而不是读取整个响应体的超时时间,如果在此超时时间内上游服务器没有发送任何响应,则 Nginx 关闭此连接。

  • proxy_send_timeout time;

    设置往后端/上游服务器发送请求的超时时间,默认为60s,此超时时间指的是两次成功写操作间隔时间, 而不是发送整个请求的超时时间,如果在此超时时间内上游服务器没有接收任何响应,则 Nginx 关闭此连接。

对于内网高并发服务,请根据需要调整这几个参数,比如内网服务 TP999 为1s,可以将连接超时设置为100~500毫秒,而读超时可以为1.5~3秒左右。

失败重试机制设置:

  • proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off ...;

    配置什么情况下需要请求下一台上游服务器进行重试。默认为“error timeout”。

    • error 表示与上游服务器建立连接、写请求或者读响应头出错。
    • timeout 表示与上游服务器建立连接、写请求或者读响应头超时。
    • invalid_header 表示上游服务器返回空的或错误的响应头。
    • http_XXX·= 表示上游服务器返回特定的状态码。
    • non_idempotent 表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器 (即默认幂等方法 GET、HEAD、PUT、DELETE、OPTIONS、TRACE 才可以重试)。
    • off 表示禁用重试。

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

  • proxy_next_upstream_tries number;

    设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。

  • proxy_next_upstream_timeout time;

    设置重试最大超时时间,默认0表示不限制。即在 proxy_next_upstream_timeout 时间内允许 proxy_next_upstream_tries 次重试。 如果超过了其中一个设置,则 Nginx 也会结束重试并返回客户端响应(可能是错误码)。

如下配置表示当 errortimeout 时重试 upstream 中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次, 则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx 结束重试并返回客户端响应。

proxy_next_upstream error timeout;
proxy_next_upstream_timeout 6s;
proxy_next_upstream_tries 2;

本文来自《如何应对网络偶尔丢包导致的超时异常》- 熊清亮的博客。

转载请注明原文链接:https://seealso.cn/debug/timeout-caused-by-occasional-network-packet-loss