redis 连接一直是 ESTABLISHED 的问题排查

昨天想删一台机器,发现上面还有 redis 连接:

# netstat -nat |grep ESTABLISHED |grep 6379
tcp 0 0 10.0.27.92:6379 10.0.27.157:24044 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.96.27:28975 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.47:58511 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44571 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.29.49:48137 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.46:8854 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42271 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42269 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:17776 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.91:17823 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.79:59200 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:46296 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.98:31277 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.118:40458 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44548 ESTABLISHED

这些连接一直都在,更奇怪的是,10.0.70.67 这个 IP 已经 ping 不通了,连接却不会断,正常情况下 tcp keepalive 会隔段时间检查一次,发现不通之后会发送 reset 。

 

为了看看到底有没有发送 tcp keepalive ack 包,抓个包看看,命令是 tcpdump -i em2 port 6379,下面是抓了一夜的包:redis.cpap

用 wireshark 分析,发现基本都是 keepalive ack 的包,取 10.0.96.27 这个IP,截个图看看:

4888351E-2D1D-4E6F-8062-F2DA259CF9C7

可以看到,10.0.96.27 主动 发送 ACK(SEQ: M、ACK: N)给 redis,redis 回复 ACK(SEQ: N、ACK:M+1),且 Len 都是0。

这能够解释大部分 IP 一直在 ESTABLISHED,因为一直有 tcp keepalive,但是 10.0.70.67 解释不通了,而且上面根本没抓到 10.0.70.67 的包,这只有一种可能: redis 不主动发送 keepalive。

 

找了下文档,发现 redis 确实默认关闭 tcp keepalive,所以对于已经建立的连接,不会发送 tcp keepalive ack 来确认对方存活,而如果对方突然死机或者关电源导致对方不主动关闭连接,那么 redis 就一直认为对方是活的,就不会去关闭连接了。

redis 提供了配置文件来更改这一默认行为:

# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Take the connection alive from the point of view of network
# equipment in the middle.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 60 seconds.
tcp-keepalive 0

 

事实上,如果 redis 对 idle 有时间限制,我遇到的情况也不会存在,但是 redis 也确实默认对 idle 的连接不加时间限制, 只是提供了 timeout 参数来更改。

 

解决 HTTP 请求被 LVS reset 的问题

最近两天遇到了 HTTP 请求被 LVS reset 的问题,LVS 默认有三个超时参数,如下:

# ipvsadm -L –timeout
Timeout (tcp tcpfin udp): 90 3 300

第一个就是 TCP 的空闲连接时间,这里超过 90 s 连接没数据会被 LVS reset 掉,在 HTTP Client 端可以看到 Connection reset by peer。我的某个服务 HTTP 请求时间很长,至少有 5 min,所以每次都会被 reset。

 

一开始我想到的解决办法是在 HTTP Client (HTTP 请求使用 requests)指定 so_keepidle参数小于 90 s。

因为 requests 使用 urllib3,而 urllib3 使用标准库 http.client(Python 3),Python 2.x 版本使用 httplib(它们会调用 socket.create_connection),所以我们可以封装一下来实现修改 so_keepidle:

import httplib
orig_connect = httplib.HTTPConnection.connect
def monkey_connect(self):
    orig_connect(self)
    self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 100)
httplib.HTTPConnection.connect = monkey_connect
import requests

但是抓包发现 HTTP Client 端根本不发送 TCP keepalive 的包来保活,很是奇怪(可能是这段代码有问题,引用的还是老的模块,没继续debug,mark)。

 

后来发现可以在 Nginx 来实现 so_keepidle(LVS 后面是 Nginx),而且 Nginx 支持 so_keepidle 的配置,在 linsten 后面指定,格式如下:

222111h4s4ipokimf3zppr

比如:listen 80 so_keepalive=60::;

实测有效,终于解决了因为 HTTP 请求时间太长被 LVS reset 的问题,而且对域名生效,不影响其他域名(如果修改 LVS TCP idle timeout,对所有域名都有影响)。

 

参考:

http://stackoverflow.com/questions/15148225/is-it-possible-to-override-the-default-socket-options-in-requests

https://linux.cn/article-3766-1.html

http://nginx.org/en/docs/http/ngx_http_core_module.html

 

关于 Nginx upstream keepalive 的说明

模块是 HttpUpstreamModule,配置的一个例子:

[shell]upstream http_backend {
server 127.0.0.1:8080;

keepalive 16;
}
server {

location /http/ {
proxy_pass http://http_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";

}
}[/shell]

 

有几点要说明:

1. 默认情况下 Nginx 访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 新开一个端口和后端建立连接,请求结束连接回收。如果像上面的配置一样设置了长连接,Nginx 会接受客户端的请求,处理完成之后 Nginx 会「继续保持和后端的长连接」,如果并发请求超过了 keepalive 指定的最大连接数,Nginx 会启动新的连接 来转发请求,新连接在请求完毕后关闭,而且新建立的连接是长连接,这可能会造成额外的问题,最后再说。

2. keepalive 指定的 数值 是 Nginx 每个 worker  连接后端的最大长连接数,而不是整个 Nginx 的。 而且这里的后端指的是「所有的后端」,而不是每一个后端(已验证)。

3. 在官网上  可以看到 还有一个 single 参数,说实话我没懂什么意思,但是它已经被废弃了,看这里

4. HttpUpstreamModule 模块里面还有一共指令:least_conn,解释如下:

Specifies that a group should use a load balancing method where a request is passed to the server with the least number of active connections, taking into account weights of servers. If there are several such servers, they are tried in turn using a weighted round-robin balancing method.

翻译:

指定服务器组的负载均衡方法,根据其权重值,将请求发送到「活跃连接数最少」的那台服务器。 如果这样的服务器有多台,那就采取有权重的轮转法进行尝试。

使用这个指令的时候后端服务器的连接数好像会变的非常少,很奇怪,我还没亲自测过,待确认。

 

最后再说一下第 1 点,先说一个现象,我发现当 keepalive 设置小的时候,比如1,那么并发请求上去之后 Nginx 会出现大量的 TIME_WAIT,而如果把 keepalive 关掉(proxy_http_version 1.1 和 proxy_set_header Connection “” 指令也去掉),那么 TIME_WAIT 就会出现在后端服务器了,后端用的是 tornado,相信 jetty 和 tomcat 也是一样。

这个现象是怎么产生的呢,做一个测试。

命令:ab -n 100 -c 5 http://pxe1.hy01/

pxe1.hy01 (Nginx,上面配置了长连接,ip: 10.0.31.84) ,它会转到 pxe0.hy01 的 8888 端口(tornado,ip: 10.0.11.12)

测试的时候在 pxe1.hy01 执行抓包命令:

# tcpdump -i em2 -A host 10.0.11.12 -n

看到:

00:22:53.252039 IP 10.0.31.84.53915 > 10.0.11.12.ddi-tcp-1: Flags [P.], seq 81:161, ack 275, win 123, length 80
@.@.].
..T
…..”…p%8|..P..{>…GET / HTTP/1.1
Host: http_backend
User-Agent: ApacheBench/2.3
Accept: */*

但是如果把 pxe1.hy01 的长连接设置都去掉的话,抓包如下:

00:23:58.111974 IP 10.0.31.84.54051 > 10.0.11.12.ddi-tcp-1: Flags [P.], seq 1:100, ack 1, win 115, length 99
E…..@.@.Z=
..T
….#”…O…SUP..s>…GET / HTTP/1.0
Host: http_backend
Connection: close
User-Agent: ApacheBench/2.3
Accept: */*

那么上面出现的现象就好解释了,是这样:

Nginx 和后端的长连接不够用时 Nginx 会新建连接来处理新的请求,而我们的配置已经配置死了 HTTP1.1,建立连接后,后端认为是「长连接」而不会主动关闭连接(一般有个空闲超时),关闭连接由 Nginx 来做了,所以 Nginx 会出现大量的 TIME_WAIT。

而默认情况下,Nginx 用 HTTP1.0 请求后端,后端处理完成后就主动关闭连接,所以 TIME_WAIT 在后端。

那么现在有新的问题了,如果开启了长连接,而长连接又大量不够用,此时 Nginx 存在的 TIME_WAIT 可能会大量占用端口,导致端口用尽,如果用尽,后果很严重。

所以,「慎用 Nginx 的 长连接」。