这是我领导于2016-12-8写的问题调查报告。好文共欣赏。
一. 问题和现象简述:
从11月22日起到12月1日,Thread日志中频繁出现Jgroup相关的报警(warning 级别)
- 11.24日上午,11.30日下午分别出现两次172.16.2.113/92服务器向172.16.2.106服务器传送大量数据的问题,以至于把网络占满,系统无法对外提供服务
- 从11月25日开始,Thread模块频繁出现内存溢出问题
- 从11月29日开始,Thread模块频繁遇到启动hang住问题
- 在12月1日晚发现Linux Kenel网络Socket缓冲区设置很小(Jgroup默认5M,Linux OS默认131k),当晚修改了三台机器Kenel参数
- 从12.1日晚至今,上述问题都没有再出现,JGroup集群表现非常稳定,再没有报任何异常。
二. 初步调查
服务器分布情况
- 106在一个机柜A,机柜上有交换机A - 113和92在一个机柜B,机柜上有交换机B - A和B通过第三个机柜上的交换机C连通
Jgroup版本
- 3.6.4.Final(发布于2015.06)
- 最新稳定版:3.6.11.Final(发布于2016.09)
日志分析
查询近6个月日志,从11.18日开始(11.18日只有几条),出现JGroup相关的warning信息,主要有几类:
- server-172-16-2-113-63346: creating the client socket to /172.16.2.106:25436 failed: Connection refused(连不上端口)
- JGRP000011: server-172-16-2-113-23585: dropped message 110 from non-member server-172-16-2-92-10055(收到了一个不在集群中的节点发过来的消息,丢弃)
- server-172-16-2-92-10055: not member of view [server-172-16-2-106-7543|17]; discarding it(收到了不在集群中的节点发过来的配置同步信息,丢弃)
- server-172-16-2-92-10055: failed to create view from delta-view;(无法同步配置信息)
- server-172-16-2-106-37680: JOIN(server-172-16-2-106-37680) sent to server-172-16-2-113-24325 timed out (after 2000 ms)(申请加群超时) 这些日志基本表明JGroup集群节点之间组网遇到了问题,也就是说正常情况下,在生产环境有17个Thread模块节点,这些节点应该组成一个JGroup消息群,但是现在有些节点不在群里了。
JGroup实时内存分析
11月26-30期间多次查看JVM内存,通过分析JGroup JavaBean内存数据,可以看出17个节点组成的JGroup集群是非常不稳定的:
- 大部分时候组不成一个群,有时组成3-4个群,有时两三个
- 由于JGroup有自动侦听、广播消息、恢复功能,所以有时能自动恢复(几个群自动合并成一个群),但绝大多数时候无法恢复,甚至由两个群分裂为更多群,有的一个群中只有一个节点。
- 每次观测,106上的10节点总是能组成一个群,而113/92上的节点有时能加进来,有时则不能。
初步结论
通过以上调查,可以确认 Thread节点间的JGroup集群处在一个非常不稳定的状态,并且组群状态看起来和网络数据传输有关系。
三. JGroup工作机制研究
-
JGroup支持多种网络协议来传输数据(TCP/UDP/TUNNEL等),Thread应用中配置的是UDP协议
-
UDP协议是不可靠数据传输方式,因此JGroup做了额外的工作来保证数据传输的可靠性
- NAKACK2是JGroup中负责接收和发送数据的模块,发送的消息会被按顺序编号
- NAKACK2模块用“被动确认”的机制来重发数据,比如S向R发送消息(序号为0-100),当R收到1-10,接着收到20-30的数据时,会先缓存消息20-30,并向S申请重发序号11-19的消息,直到收到后才会把消息11-30一起向上传给应用层
- STABLE模块的作用是定时在集群中广播本节点接收到的消息摘要,包括确认收到的消息最大编号,在上述例子中,1-10号消息是确认收到的,最大编号是10.
- 为了保证消息能够被重发,在Sender节点中,NAKACK2模块会缓存已经发出去的消息,直到它接收到Receiver节点广播出来的消息摘要,把小于对方确认收到的消息编号的消息删除。在上面例子中,即把1-10号消息从发送队列缓存中删除。
-
Jgroup集群中的各节点是怎么发现并组群的?
- JGroup支持很多方法来互相发现:PING(不是网络命令PING)、TCPPING、FILEPING、JDBCPING等等。
- Thread项目中使用的默认PING方式
- PING方法是用IP组播来发送一个“发现请求”(底层使用UDP协议),然后等待回应。收到请求的每个节点都会把当前的集群信息发回去,然后新节点再发起“申请加入”的请求。
-
JGroup集群中如何检测节点离开或存活?
- JGroup有很多检测节点存活的方法:FD,FD_ALL, FD_SOCKET等。Thread中同时用了FD_ALL和 FD_SOCKET.
- FD_ALL方式:
- i. 集群中每个节点定时广播自己的状态_心跳消息(每8秒);
- ii. 每个节点收到节点N的心跳消息的节点,更新自己保存的节点N的最新心跳时间
- iii. 每个节点定时(每2秒)检查自己的保存的其他节点N的最新心跳时间,如果超时(40秒),则广播N节点可疑死亡的消息
- iv. 集群Coordinator节点发消息给节点N,确实是否存活,如果否,则发起重建集群的消息,将N从集群中移除。
- FD_SOCKET方式:
- i. 在集群节点间互相建立TCP链接,组成环状链接,相互检测存活(N1—>N2—>N3—>N1)
- ii. 当N2连接N3的 TCP Socket断开时,N2广播N3的可疑消息
- iii. N1收到消息后会发消息给N3确认
- iv. 一旦N1收不到确认回复,则会发起消息重建集群,将N3移除集群。
- v. 这种方式探测节点出问题的速度非常快,通常在毫秒级完成。
- vi. 弊端是当服务器Hang住或者路由器崩溃时,Socket连接可能不会被关闭,导致探测失败。
-
JGroup怎么做流量控制,即当接收端处理速度慢于发送端时,怎么避免发送端一直发送导致数据丢失或溢出?
- JGroup FC模块使用了类似点数的机制,即初始时发送端S和接收端R都有默认的点数(2M),
- 当S向R发送消息时,S和R端的点数都会减少(发送1k数据,点数就减少1k),直到当点数少于一定比例(0.25, 2M*0.25=0.5M)时,R端会再向S端发送消息补充点数(0.5M,S端点数变为1M)。
- 如果R端处理的很慢,一直不向S发送补充点数消息,S端点数会变为0,这时S端就会停止发送数据。所以消息的发送和接收端实际上构成了一种流量协商的机制。
- 由于流量控制,集群通过组播方式传递消息时,传递消息的速度被那个处理最慢的节点所决定。
四. 问题分析和解释
通过调查,我们发现当前环境配置JGroup节点之间消息丢失的可能性极高:
1) 根据https://access.redhat.com/documentation/en-US/JBoss_Enterprise_Web_Platform/5/html/Administration_And_Configuration_Guide/jgroups-perf-udpbuffer.html, https://developer.jboss.org/wiki/PerfTuning,https://segmentfault.com/a/1190000000473365, 当OS Socket缓冲区较小、网络延迟较大、发送方数据传输较多时,就会发生数据包丢失现象。
2) 在我们系统中,Jgroup默认组播发送和接收Buffer 5M,而实际OS层默认131k,差异巨大,所以丢数据的风险很高。 JGroup节点之间丢消息,基本可以认定是集群不稳定、启动Hang住、内存溢出的根源。
JGroup集群不稳定问题分析
- 当集群节点之间发送的“Discover”消息丢失时,就会导致节点发现不了集群或者加不进集群。
- 当集群节点之间探测存活的心跳消息丢失时,就会导致集群节点时进时出,集群中的节点数量在动态变化。
应用启动Hang住的问题分析
- 通过日志发现,每次Hang住时的日志停止在:
GMS: address=server-172-16-2-113-34057, cluster=EH_CACHE, physical address=172.16.2.113:51465 这个日志标明JGroup 节点正在广播自己的消息,等待集群中节点回应
- 当有其他节点重启时,Hang住的节点常常立刻就继续并启动了。
- 在消息丢包严重、集群很不稳定的情况下,很有可能导致新节点加群出问题。
应用内存溢出问题分析
- 分析内存溢出时JVM Dump文件,看出在Jgroup NAKACK2 模块,占用了近2G内存
- 内存溢出时的日志标明,问题发生在NAKACK2 模块试图重发消息时:
- NAKACK2 模块在JGroup中负责发送和接收数据,如上文所述,NAKACK2会缓存发送数据,只有接收到其他节点发送的STABLE消息摘要,确认消息已被收到时才会清除,所以当消息传递不稳定时,缓存数据迟迟得不到清除,越积越多,就会导致内存溢出。
- 在网络传输丢数据的情况下,发送端S发送1M数据,接收端R只收到500K,由于流量控制机制, R返还给S的点数只有500K,久而久之就会导致S端点数降为0得不到补充,无法继续发送消息,这也可能导致S端数据缓存过多而内存溢出。
网络流量异常问题分析:
- 具体现象:
- 11.24日上午出现(10:40-11:10左右),主要症状是92/113服务器流量出,106服务器流量入,瞬时流量最高接近1G/s。持续半小时后自动恢复正常。
- 11.30日下午出现(14:55-15:30左右),症状同上,由于通知机房在15:30左右重启了113/92,所以不确定是自动恢复正常还是由于重启。
- 11.30日下午故障阶段,106服务器是数据接收方,怀疑是Thread模块问题,当把所有Thread进程都Kill(15:15)了之后,仍然继续大量接收数据(直到15:30)
- 11.30日下午故障阶段,为了避免113往106发流量,争取106能用,紧急在106上把113/92加到防火墙黑名单,但没有作用,仍然观测到大量数据流入。
- 在网络流量异常时段,106服务器上Thread Tomcat服务占用内存/CPU 均正常。
- 可以肯定的是,由于网络传输丢包,需要重传数据,肯定带来了额外的不必要的网络流量,增加了网络负载。
- 但是高达1G/s的流量,是否都是JGroup产生的,则存在疑问:
- Jgroup本身有流量控制,如果S向R发送数据,R没收到,则S点数很快用完不能再发;如果R收到了,则不会要求重发,而正常的EHCache数据同步根本达不到1G/s的量。
- 在106上Thread程序被Kill以后,还有15分钟流量异常,但按照JGroup原理,集群中节点会互相探测心跳,收不到回复就会重新组群,不会再向没有节点的机器上发数据。
- 106上面有10个节点,1G/s 平均到每个节点的流量是100M/s,10秒钟就是1个G,但是问题时段观测106上各Thread应用CPU/内存/日志均无明显异常。
- 网络调查:
- 网络上极少类似报告,只查到一个类似:i. https://developer.jboss.org/thread/201544 ii. JGroup开发者针对问题的建议是:检查网络是否有回路,版本太老需要升级 b) 现有版本,JGroup Bug库中并没有相关问题报告
结论
- 可以确定数据传输丢包会引起JGroup重传数据,增加数据传输量,但由于其流量控制机制,不应该占满带宽。
- 不排除JGroup有未知Bug引起问题,但是没有明显证据。JGroup框架自2003年发布至今,一直在稳定更新,被应用于近20个世界知名项目,包括JBoss集群、Autodesk、Jetty 会话复制、Tomcat会话复制等,其质量应该是较为可靠的。
- 如果网络基础设置有问题(以前出现过网口虚接的问题),在广播报文明显增多的情况下,也可能会触发“网络风暴”: i. http://www.360doc.com/content/11/0330/20/5179015_105969937.shtml ii. http://lbzxy.blog.51cto.com/497155/124316 iii. http://www.programgo.com/article/34214135910/
- 本着对问题负责的态度,我们会把JGroup升级到最新稳定版本
五. 开发团队反思
这次解决问题、调查问题花了很长时间,反映了工作上有需要优化的地方:
-
开发团队成员普遍对JGroup技术不熟悉,缺乏技术储备。我们平时对底层技术缺乏重视,绝大部分时间用于开发应用功能,对用到的技术、工具和框架仅限于使用,对其原理缺乏了解,遇到疑难杂症时就得临时抱佛脚。对策:
开发团队要投入更多资源,对所用到的技术、工具、框架有深入了解和掌握,做到知其然并知其所以然。包括但不限于:学习、演练、调试,要掌握原理和了解各种配置参数。
-
开发团队对生产环境不熟悉、缺乏权限,导致一开始调查时无从下手。对策:
开发团队和运维沟通,获取更多权限;全面了解目前生产环境的部署情况。
参考资料:
JGroup技术手册:http://www.jgroups.org/manual/index.html JGroup官方系列培训资料:https://github.com/belaban/workshop/blob/master/slides/toc.adoc JGroup官方入门教程:http://www.jgroups.org/tutorial/index.html JGroup源码 Google查询