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 |