如何应对网络偶尔丢包导致的超时异常
/ / 阅读数:2855摘要 :生产环境有些服务对耗时非常敏感,所以一般会设置较低的 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 也会结束重试并返回客户端响应(可能是错误码)。
如下配置表示当error
或timeout
时重试 upstream 中的下一台上游服务器,如果重试的总时间超出了 6s 或者重试了 1 次,
则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx 结束重试并返回客户端响应。
proxy_next_upstream error timeout; proxy_next_upstream_timeout 6s; proxy_next_upstream_tries 2; |