JGroups 2.8 源码阅读
2019
年为处理某故障,对 JGroups
做了粗略的代码解读。目前尚有系统仍在使用 JGroups
,故找出文档,分享在此。
EhCache 底层调度 JGroups 代码分析
1. JGroups 协议栈初始化与启动时序图
启动EhCache的JGroupManager
new NotificationBus(String, String) |
至此,启动完毕
服务端
sequenceDiagram
participant App
participant NotificationBus
participant JChannel
participant ProtocolStack
participant Configurator
participant Protocol as Protocol Layer
App->>NotificationBus: new NotificationBus()
NotificationBus->>JChannel: new JChannel()
JChannel->>ProtocolStack: setup()
ProtocolStack->>Configurator: createProtocols()
Configurator->>Protocol: createLayer()
Protocol->>Protocol: setPropertiesInternal()
Protocol->>Protocol: setProperties() // 各协议配置自身
ProtocolStack->>Configurator: initProtocolStack()
Configurator->>Protocol: init() // 各协议初始化自身
客户端
sequenceDiagram
participant App
participant NotificationBus
participant JChannel
participant ProtocolStack
participant Configurator
participant Protocol as Protocol Layer
App->>NotificationBus: new NotificationBus()
NotificationBus->>JChannel: connect()
JChannel->>ProtocolStack: startStack()
ProtocolStack->>Configurator: startProtocolStack()
Configurator->>Protocol: start() // 各协议启动自身
JChannel->>ProtocolStack: downcall(Event.CONNECT)
ProtocolStack->>Protocol: down(Event) // 事件向下传递
2. TCP 协议初始化与启动时序图
TCP协议
setProperties() |
setProperties
sequenceDiagram
participant TCP
TCP->>TCP: createInitialHosts()
init
sequenceDiagram
participant TCP
participant BasicTCP
participant TP
TCP->>BasicTCP: BasicTCP.init()
TCP->>TP: TP.init() // 创建线程工厂
start
sequenceDiagram
participant TCP
participant ConnectionTable
participant Thread as Thread Pool
TCP->>ConnectionTable: getConnectionTable()
ConnectionTable->>ConnectionTable: init()
ConnectionTable->>ConnectionTable: createServerSocket() // 绑定端口
TCP->>ConnectionTable: start()
ConnectionTable->>Thread: start() // ConnectionTable.run()
ConnectionTable->>Thread: start() // Reaper.run()
3. TCPPING 协议发现成员时序图
TCPPING协议
init() |
init
sequenceDiagram
participant TCPPING
TCPPING->>TCPPING: timer // 创建定时器
down(Event)
sequenceDiagram
participant TCPPING
participant Discovery
participant Timer as Timer Thread
TCPPING->>Discovery: findInitialMembers()
Discovery->>Timer: PingSenderTask.start()
Discovery->>Timer: Responses.get(timeout)
loop 等待响应
Timer->>Timer: response
end
Note over Timer: 等待条件: 1.足够节点<br/>2.num_initial_srv_members<br/>3.协调者响应<br/>4.超时
Discovery->>Timer: PingSenderTask.stop()
4. FD 与 VERIFY_SUSPECT 协议协作时序图
FD协议,每次view变化时,会触发监视器,超过两个节点,就通过FD向邻居发送心跳包,没有收到心跳包,会向上触发事件Event.SUSPECT
VERIFY_SUSPECT协议,这个协议需要在FD之上,GMS之下
当调用BasicTCP.sendToSingleMember(Address, byte[], int, int)出现异常时,会向上触发事件Event.SUSPECT
响应Event.SUSPECT |
up(Event)
sequenceDiagram
participant FD as FD Protocol
participant VS as VERIFY_SUSPECT
participant Timer as Timer Thread
participant LowerLayer as Lower Protocol
par 定期检测
FD->>FD: 定期发送心跳
and 异常检测
FD->>FD: 检测到心跳丢失
FD->>VS: up(Event.SUSPECT)
end
VS->>VS: verifySuspect(Address)
VS->>Timer: startTimer() // 等待回应
VS->>LowerLayer: down(Event) // 发送ARE_YOU_DEAD
alt 收到回应
Timer-->>VS: 收到回应
VS->>VS: 取消怀疑
else 超时未回应
Timer-->>VS: 超时
VS->>UpperLayer: up(Event.SUSPECT) // 确认怀疑
end
5. GMS 协议处理连接时序图
GMS协议
new GMS() |
sequenceDiagram
participant GMS as GMS Protocol
participant ClientImpl as ClientGmsImpl
participant LowerLayer as Lower Protocol
GMS->>GMS: new GMS()
GMS->>GMS: initState()
GMS->>GMS: becomeClient()
GMS->>ClientImpl: init()
GMS->>LowerLayer: down(Event.CONNECT)
GMS->>ClientImpl: join(Address)
ClientImpl->>ClientImpl: findInitialMembers()
ClientImpl->>LowerLayer: down(Event.FIND_INITIAL_MBRS)
alt 找到初始成员
ClientImpl->>ClientImpl: determineCoord()
ClientImpl->>ClientImpl: sendJoinMessage()
else 未找到初始成员
ClientImpl->>ClientImpl: becomeSingletonMember()
ClientImpl->>GMS: up(Event.BECOME_SERVER)
end
## EhCache 底层调度 JGroups 的配置解读
connect=TCP(start_port=7800;end_port=7800;bind_addr=192.168.20.118)://通讯底层协议 |
配置建议:
- 在所有需要的节点上,均将
TCP
的start_port
和end_port
设为一个值,且每个节点端口唯一。用以保证不会端口跳号。 - 在
TCPPING
的initial_hosts
中,将所有节点均进行静态配置。只有静态配置完整了,MERGE2
才能正常工作。 TCPPING
的超时timeout
,在查找初始节点时被请求数num_ping_requests
进行了等分,用以执行多次。默认num_ping_requests
是2。将timeout
值设置得稍大点,能确保节点的找到。- 关闭
FD
和GMS
的shun
选项。在采用了MERGE2
协议后,不必配置,用merge
代替shun
。 NAKACK
的gc_lag
需要细心设置,根据包的大小合理估计。默认值是20
。- 为防止节点因迟缓而被踢出,可以适当延长
FD
协议中timeout
值。 - 许多操作是通过
timer
进行处理的。默认打开3个timer
。需要通过-Djgroups.timer.num_threads
来设置。
某故障分析
重启节点后为什么无法加入到原集群组中
根据连接
158:7801
的日志,说明其未单独建立集群,正在向当前集群协调者申请加入。
根据代码、配置及日志,分析如下:- 前次启动
158
时,由于某些特殊原因,比如7800
处于time_wait
状态,导致158
占用了7801
端口 158
和159
在出问题前,是一个完整集群,158:7801
是作为协调者的158
出问题后,FD
协议侦测出了158:7801
有问题,但由于FD
协议被设置到了VERIFY_SUSPECT
协议上层,没法交给VERIFY_SUSPECT
协议去确认嫌疑。159
中协调者仍然保持为158:7801
,未将自己升级为协调者。- 在
158
重启后,向159
要来了集群信息,得到158:7801
是协调者这一信息。于是向158:7801
申请加入。 - 由于
158:7801
实际已消亡,所以,无法加入。158:7800
一直处于待加入集群态。
- 前次启动
为什么说
158
单独建立了集群?如问题一答复,根据连接
158:7801
的日志,说明其未单独建立集群,正在向当前集群协调者申请加入。jgroups
集群成员丢失的判断依据- 基于协议
FD
/VERIFY_SUSPECT
。FD
协议需要在VERIFY_SUSPECT
协议下层。 FD
发送心跳包are-you-alive
,检查邻居存活情况。一旦侦测到失败,就向上发出Event.SUSPECT
。VERIFY_SUSPECT
处理FD
检查到心跳失败时向上发出的Event.SUSPECT
。会向该节点发出are-you-dead
,如果在超时时间内没返回,确认怀疑。进一步向上发出Event.SUSPECT
。- 但某系统配置有误,将
FD
协议放到了VERIFY_SUSPECT
的上层,导致异常侦测存在问题。
- 基于协议
某故障分析2
问题现象
某系统报日志大量滚动刷新输出,经确认,为 jgroups
日志输出
问题分析
前置知识背景
jgroups
是基于TCPPING
进行的集群发现,TCPPING
是基于TCP
的PING
协议,用于发现集群中的节点,并返回节点的IP
地址和port
,用于后续的TCP
连接。TCPPING
协议需配置initial_hosts
参数,用于指定集群中节点的IP
地址和port
,用于后续的TCP
连接。- 配置在
initial_hosts
参数中的节点,被认为是集群中的leader
节点,或者说初始节点。 - 如果只有一个节点,那么这个节点就是初始节点,否则,第一个节点作为初始节点,后续节点作为后续节点。
本次问题分析
- 使用了
name_masked_framework
框架,对jgroups
进行了封装。 - 在
name_masked_framework
的配置里,bindHosts
中配置的节点信息将被传递到jgroups
的TCPPING
协议中,作为initial_hosts
信息。 - 根据现场得到信息,
xxx_app
和yyy_app
的节点都配置了bindHosts
参数,但bindHosts
参数中只配置了一个节点,即本机:192.168.10.111:12335
。 - 这造成了一个严重问题,即在
xxx_app
和yyy_app
中,jgroups
集群中,只有第一个启动的进程,才会被认定为leader
(初始节点),另一个只能作为后续节点。 - 本次,通过分析,可发现
xxx_app
进程为初始节点,yyy_app
为后续节点。
推演问题过程
xxx_app
进程停机,此时集群中只剩下一个进程,即yyy_app
进程。该进程在集群中是后续节点,不具备维持jgroups
集群能力。xxx_app
进程重启,此时xxx_app
进程重新监听12335
端口成为初始节点,构建jgroups
集群。yyy_app
进程通过TCPPING
探测到12335
端口,尝试加入到jgroups
集群中。但由于xxx_app
进程启动过程中未接受到任何组网包,不认为yyy_app
进程是自己集群内成员,拒绝其发送的包。
复现
已通过最小代码集构建测试包,复现此问题。
配置解决方案
将 yyy_app
版本的配置与 xxx_app
版本的配置分离,分别配置,不公用配置即可
两种改法,1 隔离改法
# xxx_app |
两种改法,2 全部配置为初始节点
# xxx_app |