场景:
开发反馈测试环境A应用通过httpclient连接B应用,偶发java.net.socketException: connection reset报错。
理解 Connection reset:
在我看来 Connection reset分为2种情况:
第一种Connection reset by peer:
服务器返回了RST时,如果此时客户端正在往Socket套接字的输入流中写数据则会提示"Connection reset by peer"。
第二种java.net.socketException: connection reset:
服务器返回了RST时,如果此时客户端正在从Socket套接字的输出流中读数据则会提示Connection reset";
那么不管那种情况 其实都是服务器返回了RST标志位,但是客户端还在进行输入输出数据,
通过以上描述,猜测Connection reset的原因是客户端连接保活机制超时回收连接,但是服务端还在使用该连接池导致报错。
排查过程:
首先肯定是自己去模拟错误,但是这个模拟错误代价有点大,毕竟是偶发现象。连续访问几十次都没模拟出问题。之后我只能放弃挣扎去看两个应用的相关日志 看能不能找到一点猫腻,比如在这里我会根据traceid跟踪整条链路,但是几乎没什么收获。之后配合开发将连接地址改成直连,这里说一下我这环境是k8环境。 然后直连模拟了大概一天。并没有出现connection reset。
那么问题肯定是出在连接svc地址的时候出现了connection reset问题。
目前该环境用的模式是ipvs,那么众所周知的是ipvs是有超时时间的,如果超时的话,连接将被回收。那么我们可以看下这个环境的配置
所以说,7200是两个小时,明显大于15分钟。本来预期的2小时的tcp链接,15分钟就被kill掉了。
为了验证这个问题,多次等待15分钟访问该接口,终于功夫不负有心人,15分钟必现,那么就不存在偶然的说法了。
之后围绕这个问题开始了一系列的操作。
为了达到 ipvs timeout > docker 中 net.ipv4.tcp_keep_alive
解决这个问题:
1.增大 ipvs timeout
ipvsadm --set 7500 120 300
2.缩小net.ipv4.tcp_keep_alive = 600
在主机上修改/etc/sysctl.conf 将 tcp_keep_alive 改为600 并且同时需要修改k8s pod的tcp_keep_alive 并且kubelet需要增加--allowed-unsafe-sysctls=net.*
securityContext:
sysctls:
- name: net.ipv4.tcp_keepalive_probes
value: "10"
- name: net.ipv4.tcp_keepalive_intvl
value: "30"
- name: net.ipv4.tcp_keepalive_time
value: "600"
这边解释一下原理,k8s同一个集群的两个pod直接通过svc cluster ip 访问,通过cluster ip 访问的微服务,流量会经过kube-proxy,kube-proxy底层走的是ipvs,ipvs timeout 默认超时时间15分钟。过了15分钟,ipvs就会销毁tcp链接。
在容器A中,通过netstat -altpn --timers 命令检查tcp链接,发现之前的tcp通道没有关闭,10.215.10.117 是podip, 10.222.222.187:80 是服务B的service_ip:port
在容器A中,用ss -nt 命令和 ipvsadm命令查看到,在请求结束后,服务A容器内该链接未断开,而主机上ipvs通道已经超时销毁。如下图 当保活时间到0的时候宿主机的连接就会消失。
很奇怪的是以上配置结束之后 并没有解决问题。后续通过linux的 watch机制 反复查看宿主机已经客户端的连接池的状态,发现当客户端建立连接的时候 连接池的状态一直处于off状态(没有时间计时)初步怀疑是客户端建立连接的时候没有打开保活机制导致。
如果开启保活连接的话 会处于keepalive状态如图:
当为 keepalive 时,表明 TCP 协议栈开启了 keep alive timers,后面括号中的值分别是 timer 所剩时长/重传次数/keepalive probe 次数。当为 off 时,表明没有开启 TCP keepalive timer。当然,正如前面所说,这个 timer 是否开启,和 HTTP keep alive 没有必然联系。
宿主机:
httpClient pod:
所以按照httpclient 返回的timer status状态(off)来看并不是keepalived导致的,那么设置keepalived 的系统参数是没有用的。
所以最终解决方案就是:
第一种(4层tcp keepalived): 连接方式增加保活机制 必须搭配net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_probes net.ipv4.tcp_keepalive_intvl 几个配置
SocketConfig socketConfig = SocketConfig.custom().setSoKeepAlive(true).setSoTimeout(3600000).build(); //We need to set socket keep alive
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(3600000).build();
CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).
setDefaultSocketConfig(socketConfig).build();
HttpPost post = new HttpPost(url.toString());
HttpResponse response = httpClient.execute(post);
第二种(7层keepalived,目前我使用的方法):修改客户端的keepalive的策略keepAliveStrategy,当连接超时之后不再复用老连接。
源代码:
修改后的连接方式: