警惕!四层、七层负载乱用,Java lettuce连接池故障一例
01
写在之前
负载均衡
负载均衡建立在现有网络结构之上,它提供一种廉价、有效、透明的方法,扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。(百科)
四层负载 | 七层负载 | |
概念 | IP+Port实现的负载均衡、不涉及具体协议、工作在TCP层、只转发你的流量,不关注流量内容是什么,只转发即可 | 七层是应用层、带协议的才能称为七层,它是基于应用层协议的信息进行转发,七层负载均衡也称为“内容交换”,主要是通过报文中具有真正意义的应用层内容,再加实现负载均衡的设备设置的服务器选择方式 ,决定最终选择哪台上游内容服务器;最常见的是http协议, |
实现方式 | 使用三层的IP与四层的端口,来决定哪些流量做负载均衡,对需要做负载的流量进行NAT处理或直连路由或ip隧道的方式 ,把流量转发至上游服务器、并记录这个TCP或者UDP流量是由哪台上游服务器处理,后续这个连接的所有流量都同样转发到同一台服务器处理; | 七层是在四层基础之上存在的,它在在四层的基础上结合应用层协议的特性进行负载,比如一个http服务,除了根据三层的IP+四层的端口来辨别哪些流量需要处理,还能基于七层的URI、浏览器类别、Cookie、语言等信息来决定负载均衡; |
工具选择 | LVS、Nginx、HAProxy、F5等 | Nginx、HAProxy、Squid、F5等 |
利弊 | 简单、直接转发,不需要太多复杂配置,安全性相对7层差一些; | 更智能化(例如根据用户请求内容区分图片服务器或者对文本进行压缩等),对客户端请求和服务器端的响应可以做修改(header信息、超时配置等),提高应用系统的灵活性;安全方面可以定制一些策略如waf、还有SYN Flood攻击,四层负载会直接到上游服务器,而七层的这些SYN攻击,到负载均衡就截止,不会影响到上游服务器,提高一些安全性; |
工作场景中,根据公司业务需要自行选择使用哪种负载均衡即可,一般情况下http应用使用七层负载较多,其它七层应用协议如mysql协议、redis协议等都不建议使用七层负载均衡,因为七层均衡设备或软件很少会实现这种专有协议的负载算法。
五层协议模型
目的是熟悉协议工作在哪一层,数据承载形式是什么,redis协议是7层应用层协议,7层负载均衡设备是否就能实现所有七层协议呢?一般情况下不能,这里的redis是应用层协议,属于专用私有协议,但7层负载均衡如果实现了redis协议,就可以做7层负载,但如果没有实现也能类似HTTP一样用7层负载,强烈不建议这样的用法,因为会出现各种奇葩问题,建议使用4层负载就好了。
五层模型 | 常见协议 | 数据表现形式 |
应用层 | HTTP、FTP、Telnet、SNMP、DNS、SMTP、TFTP以及mysql、redis、grpc专有协议等都属于应用层协议 | 消息(message) |
传输层 | 有连接安全的TCP,无连接不安全UDP | TCP的叫做Segment(数据段)UDP的叫做数据报(Datagram) |
网络层 | ICMP、IP、BGP、IGMP、RIP、OSPF等协议 | 数据包(Packet) |
数据链路层 | ARP、RARP、PPP、MTU等协议 | 数据帧(Frame) |
物理机 | EEE802,IEEE802.11等协议 | 二进制位 |
除了上面的数据承载形式外,还有一种叫数据单元(data unit)的数据,常用的数据单元有服务数据单元(SDU)、协议数据单元(PDU);SDU是在同一机器上的两层之间传送信息,而PDU是发送机器上每层的信息发送到接收机器上的相应层(同等层间交流用的)。
F5 基础知识
F5是硬件负载均衡器,分LTM和GTM,配置中均有几个重要参数:Node(节点)、Pool(资源池)、和Virtual Server(虚拟服务器)。
在配置F5的过程中Virtual Server是核心,通过配置它,可以关联到Pool、Node、并且为VS分配一个IP,这里简单说下配置VS时的类型有哪些,这些也是F5的负载均衡类型,VS的Type有Performance L4、Standard VS、Forwarding IP 和 Fast Http,这里重点介绍下4层与7层负载均衡,其它两种不做多过介绍。
Performance L4类型的Virtual Server,是我们常说的四层负载均衡,也是一般企业都会选择的类型,因为它对应用系统的影响小、转发快、不改变TCP中的任何参数、直接转发。
Forwarding IP类型的 Virtual Server 它主要用在内外网路由功能上面,如果要使用路由功能,需要单独开启,这里不过多介绍。
02
问题排查
问题解决后图示
问题描述
最近升级 codis proxy 时,发现升级完成后,Java 程序无法连接 Codis 的现象,线上大面积报警,没有办法只能重启下Java程序,服务正常;访问方式是Java 程序访问F5 VS IP,然后由 VS IP 负载到 Codis Proxy,这个问题不解决,影响后续 Codis Proxy升级。
信息收集
1. 网络反馈 F5 VS 配置未变更过;
2. Java程序也未发版,使用 lettuce连接池通过F5 VS 去访问 Codis 服务 ;
3. 升级 codis proxy 即 codis proxy重启过;
4. RD 同学反馈连接 codis异常,建议重启试下;
5. 重启后正常,把出问题的 Java 程序全部重启,业务恢复;
6. 其实这个时间回滚 codis proxy 代码也不能解决这个问题,必须 重启Java 程序(一般是谁升级了,就回滚谁,百分之八九十可以解决,很可惜此时回滚codis proxy 代码是无法解决)。
怀疑猜想
1. codis proxy 新代码导致,但中间件同学反馈,Java 程序重启之后正常,说明codis proxy 升级后的程序没有问题;
2. Java 使用的 lettuce 客户端套件问题;
3. Java 程序异常,日志显示如下:
[2020-02-28 10:21:52.180] [http-nio-8080-exec-5] ERROR [b9c9e148e24f478aba6d75cf93a21f0d] [] [] [] c.j.fsinnerapi.app.provider.common.exception.handler.GlobalExceptionHandler - occur error:
org.springframework.data.redis.connection.PoolException: Returned connection io.lettuce.core.StatefulRedisConnectionImpl@4afe9497 was either previously returned or does not belong to this connection provider
大意是先前链接已经返回,或者是不属于此连接提供者,说明先前的链接已经失效了,抛出异常了。
肯定是codis proxy 升级重启过程中导致问题,但问题原因究竟是什么呢?不可能因为升级codis proxy,把所有应用全部重启一次吧,陷入僵局,现在抓包分析吧。
抓包分析
java程序服务器端
三次握手是成功的,在Push数据时,被reset掉了,这里的reset是谁发出来的呢,根据TTL可以判定是F5。
F5与Java程序端
F5与Java程序之间的数据包,与Java程序端的一致,但这里的TTL是255,自身发出的RST。
F5到Codis proxy数据包与codis-proxy自己的数据包,均没有tcp.port == 6701的数据包,根据TTL的判定,应该是七层RESET,原因是如果七层代理有设计RESET功能,并且在无法连接后端程序的时候,发送RESET应用层响应包给客户端,那就认为是七层RESET,如果没有,而只是机械转发后端TCP层的包给客户端,就是四层RESET。从这里发现这个F5的 VS 竟然是七层负载均衡;
排查过程异常的曲折,当时抓包的时候,在重启完proxy后,立即停止了抓包,就出现类似上面的数据包;继续实验,让Java程序直接连 Codis proxy时,重启Codis proxy,Java程序丢几个包后,很快就能重新获取连接,继续访问codis并操作,RD 认为是F5的问题,F5同学目前也不确认问题原因,从这里可以得出Java 程序通过4层直连codis proxy,codis proxy重启时,Java程序丢几个包后,即可直接继续访问,无需重启;
从上面两个方面可以发现,F5 VS使用的七层负载,直连相当对4层,直接让F5同学修改VS配置,修改成4层转发,Codis Proxy 重启,Java 程序丢几个包后正常,到此问题找到了一个解决办法。
到底是什么原因造成的这种现象呢?Codis Proxy程序不升级,F5 VS 使用的七层负载(使用七层负载,暂不说是否合理),Java 程序能正常访问,为什么Codis Proxy 重启后,Java 程序收到一些RESET后,再连F5 VS 就不成功了呢?RD反馈是RESET,这句话看着没有什么太大的问题,其实里面隐藏着一个问题,RESET是什么时候发的,怎么判定 Java程序收到 Reset(Codis Proxy重启过程中)后又重新发起过连接呢?通过再次抓包分析(重启后多抓一段时间的数据包),确实论证了一个问题点,Codis Proxy重启正常后,Java 程序没有再发起连接请求,不重试了,Java 客户端放弃“治疗”,出现上面的 Java 异常错误。到这里基本上可以判断,应该是 Java 客户端使用 lettuce 连接池问题。
入手点是 lettuce 连接池源码,思路是七层负载时,VS IP:9100一直是通的,即使Pool池中所有NodeIP:Port都down了,也不影响IP:9100,客户端依然可以连接,四层负载(直连时)VS IP:9100是随Codis Proxy 端口存在而存在,重启时Java 客户端可以感知到IP:9100的存在与否。
问题本质原因
应用系统基于SpringBoot2.x版本开发,并配合使用Spring官方推荐的Lettuce作为Redis连接池套件。
Lettuce开发者认为,Redis单线程效率较高,而在项目实际开发过程中,仅阻塞或事务性操作对Redis的连接时长要求较高,所以Lettuce连接管理单元在默认情况下仅开启一个本地链接供多个LettuceConnector使用,也就是说,在默认情况下,即使你配置了连接池,实际上使用的也仅仅是一个物理连接,且默认情况下Lettuce不会校验该链接的可靠性(是否被关闭、是否可以ping通),所以链接永不释放。
Lettuce通过两种链接方式进行访问redis服务
独享链接 | 共享链接 |
多线程分别持有独立的连接(每个线程单独建立Socket) | 多线程共享同一个链接 |
每个操作都会开启和关闭对应的连接 | 连接永不关闭,且默认情况下不进行可靠性校验 |
默认情况下,阻塞性、事务性操作会强制开启一个独占链接 | 默认情况下,非阻塞性、非事务性操作都只能使用共享连接进行操作 |
private boolean validateConnection = false;
private boolean shareNativeConnection = true;
由于使用了默认参数配置即共享链接,它只开启一个NativeConnection 实际上仅开启了一个连接。
七层负载时F5 VS IP:9100是通的,Codis Proxy重启时,不影响F5 VS IP:9100的连通性,Java程序无法感知到Codis Proxy重启了,它一直认为服务是正常的,但Codis Proxy 重启导致LettuceConnectionFactory.SharedConnection(共享链接模式下)的validateConnection方法在校验时失败,就会重复调用connectionProvider.release(connection),即出现上面的异常日志,并没有重试再去连接,而一直异常状态;
四层负载时F5 VS IP:9100 随Codis Proxy重启,而Java的Lettuce 可以感知到,连接被关闭;
参数 | 四层负载 | 七层负载 |
validateConnection = false; shareNativeConnection = true;(默认) |
共享链接方式正常访问 | 共享链接方式不可以正常访问,报异常错误 |
validateConnection = false; shareNativeConnection = false; |
独享链接方式正常访问 | 独享链接方式正常访问 |
各位RD同学可读下源码分析一下,这里给一个网上分析链接:https://segmentfault.com/a/1190000016417906
03
总结
此问题有两种解决方案,一是七层负载修改成四层(这里由于是F5,支持这种应用层协议使用Standard 类型可以使用,Nginx无法使用七层代理这种非标准应用层协议,4层stream 可以),另一种修改Java使用lettuce连接池参数,禁止lettuce作者推荐的共享式,使用独立链接方式,连接池默认5个连接,使用完成,再重新创建。(由于我是运维,lettuce 源码读的不是精,如有错误,可以指点出来,在这里谢谢你)
郑重声明下非标准的应用层协议,一律推荐使用4层负载,不能使用七层负载,类似MySQL、Redis协议等,原因:Redis是应用层协议,但不能使用7层负载,一是因为负载转换器不可能实现这种私有的协议,二是因为这样一来会造成很多意想不到的错误,并且大部分七层反向代理服务器(软件)不支持这种操作,只有像F5这种硬件设备才支持。
附件 F5 四层负载配置图示
附件 F5 七层负载配置图示
您的关注是写作的动力
往期故障分享
往期K8S分享