注册中心zookeeper被重启,线上微服务全部掉线,怎么回事?!
最近由于一次过错的运维操作,导致线上注册中心zk被重启。而zk重启后发现一切线上微服务开始不断掉线,造成了持续30分钟的P0毛病。
整体排查过程深入学习了 zookeeper的session机制,以及在这种反常状况下,RPC结构应该怎么处理。
好了,一起来回顾下这次线上毛病吧,最佳实践总结放在终究,千万不要错过。
1、现象描述
某天晚上19:43分左右,误操作将线上zk集群下线(stop),一共7台节点,下线了6台,导致zk停止工作。
在发现节点下掉后,于19:51分左右将一切zk节点进行重启(start),期间服务正常运行,没有收到批量业务调用的报错和客诉。
直到19:56分,开始收到大面积调用失利的警报和客诉,咱们测验着依赖自研RPC结构与zk间重连后的「主动康复」机制,期望能够在短时刻内批量康复。
可是很不幸,过了接近8分钟,没有任何大面积康复的痕迹。结合zk znode节点数上升十分缓慢的状况,所以咱们采取了应急措施,将一切微服务的pod原地重启,执行重启后效果显著,大面积服务在短时刻内逐渐康复。
2、开始剖析
咱们自研的RPC结构选用典型的 注册中心+provider+consumer 的形式,通过zk暂时节点的办法做服务的注册发现,如下图所示。
结合毛病期间产生的现象,咱们开始剖析:
- **阶段1:**zk集群停服(stop)期间,业务能够正常调用。原因是consumer无法访问zk,暂时失去服务发现才能,所以在这个期间只要服务没有重启,就不会改写本地的服务发现provider缓存列表provider-list,调用无反常。
- **阶段2:**zk集群启动结束后,服务间马上出现调用问题。原因是consumer连接上zk后,马上进行服务发现操作,然而provider服务这时还没从头注册到zk,读取到的是空地址列表,造成了业务的批量报错。
- **阶段3:**zk康复后续一段时刻,provider服务依然没「主动重连」到zk,导致consumer持续报错。在一切服务全量重启后,provider服务从头注册成功,consumer康复。
这儿存在一个问题:
- 为什么zk集群康复后,provider客户端「主动重连」注册中心的机制没有生效?导致consumer被推送了空地址列表后,没有再收到从头的provider注册节点信息了。
3、深入排查
3.1 问题复现
依据大量测验和真实体现,咱们找到了稳定复现本次问题的办法:
zk session过期包含 「服务端过期」 和 「客户端过期」,在「客户端过期」状况下康复zk集群,会导致「暂时节点」丢掉,且无法主动康复的状况。
3.2 剖析
1)在集群重启康复后,RPC结构客户端马上就与zk集群取得重连,将保存在本地内存待注册的providers节点 + 待订阅的consumers节点 进行重建。
2)可是zk集群此刻依据snapshot康复的「暂时节点」(包含provider和consumer) 都还在,因而重建操作回来NodeExist反常,重建失利了。(问题1:为什么没有重试?)
3)在集群重启康复40s后,将过期Session相关的 暂时节点全都移除了。(问题2:为什么要移除?)
4)consumer监听到 节点移除 的空列表,清空了本地provider列表。毛病产生了。
基于这个剖析,咱们需求进一步环绕2个问题进行源码的定位:
- 问题1:zk集群康复后,前40s,为什么RPC结构的客户端在创立暂时节点失利后没有重试?
- 问题2:zk集群康复后,40s后,为什么zk会删去之前一切现已康复的暂时节点?
3.3 问题1:为什么暂时节点创立失利没有重试?
通过源码剖析,咱们看到,RPC结构客户端与服务端取得重连后,会将内存里老的暂时节点进行从头创立。
这段逻辑看来没有什么问题,doRegister成功之后才会将该节点从失利列表中移除,否则将持续守时去重试创立。
持续往下走,要害点来了:
这儿咱们能够看到,在创立暂时节点时,吞掉了服务端回来的NodeExistsException,使整个外层的doRegister和doSubscribe(订阅)办法在这种状况下都被认为是从头创立成功,所以只创立了一次。
正如上面剖析的,其实正常状况下,这儿对NodeExistsException不做处理是没有问题的,便是节点现已存在不必再添加了,也不需求再重试了,可是随同服务端后续踢出老sessionId一起删去了相关暂时节点,就引起了毛病。
3.4 问题2:zk为什么删去现已康复的暂时节点
3.4.1 从zk的session机制说起
众所周知,zk session管理在客户端、服务端都有完成,并且两者通过心跳进行交互。
在发送心跳包时,客户端会带着自己的sessionId,服务端收到请求,检查sessionId承认存活后再发送回来结果给客户端。
假如客户端发送了一个服务端并不知道的sessionId,那么服务端会生成一个新的sessionId颁布给客户端,客户端收到后本地进行sessionid的改写。
3.4.2 zk客户端(curator)session过期机制
当客户端(curator)本地sessionTimeout超不时,会进行本地zk对象的重建(reset),咱们从源码能够看到默许将本地的sessionId重置为0了。
zk服务端后续收到这个为“0”sessionId,认为是一个未知的session需求创立,接着就为客户端创立了一个新的sessionId。
3.4.3 服务端(zookeeper)session过期处理机制
服务端(zookeeper) sessionTimeout的管理,是在zk会话管理器中看到一个线程使命,不断判断管理的session是否有超时(获取下一个过期时刻点nextExpirationTime现已超时的会话),并进行会话的整理。
咱们持续往下走,要害点来了,在整理session的过程中,除了将sessionId从本地expiryMap中清在外,还进行了暂时节点的整理:
原来zkserver端是将sessionId和它所创立的暂时节点进行了绑定。随同着服务端sessionId的过期,绑定的一切暂时节点也会随之删去。
因而,zk集群康复后40s,zk服务端session超时,删去了过期session的一切相关暂时节点。
4、 毛病根本原因总结
1)zk集群康复的第一时刻,对zk的snapshot文件进行了读取并初始化zk数据,取到了老session,进行了create session的操作,完成了一次老session的续约(重置40s)。
集群康复要害入口-从头加载snapshot:
反序列化最近的snapshot文件,并读取session康复到本地内存:
进行session康复(创立)操作,默许session timeout 40s:
2)而此刻客户端session早现已过期,带着空sessionid 0x0进行重连,获得新sessionId。可是此刻RPC结构在暂时节点注册失利后吞掉了服务端回来的NodeExistsException,被认为是从头创立成功,所以只创立了一次。
3)zk集群康复后通过40s终究由于服务端session过期,将过期sessionId和及其绑定的暂时节点进行了铲除。
4)consumer监听到 节点移除 的空列表,清空了本地provider列表。毛病产生了。
5、解决计划
通过上面的源码剖析和解答,解决计划有两种:
计划1:客户端(curator)设置session过期时刻更长或许不过期,那么集群康复后的前40s,客户端带着原本的sessionid跟服务端做一次请求,就主动续约了,不再过期。
计划2:客户端session过期后,带着空sessionid 0x0进行重连的时分,对NodeExsitException做处理,进行 删去-重添加 操作,保证重连成功。
所以咱们调研了一下业界运用zk的开源微服务结构是否支撑自愈,以及怎么完成的:
dubbo选用了计划2。
注释也写的十分清楚:
“ZNode途径现已存在,由于咱们只会在会话过期时测验从头创立节点,所以这种重复可能是由zk服务器的删去推迟引起的,这意味着旧的过期会话可能依然保存着这个ZNode,而服务器只是没有时刻进行删去。在这种状况下,咱们能够测验删去并再次创立。”
看来dubbo确实后续也考虑到这个边界场景,防止踩坑。
所以终究咱们的解决计划也是学习dubbo fix的逻辑,进行节点的替换:先deletePath再createPath,这么做的原因是将zk服务端内存维护的过期sessionId替换新的sessionId,避免后续zk整理老sessionId时将一切绑定的节点删去。
6、最佳实践
回顾整个毛病,咱们其实还疏忽了一点最佳实践。
除了优化对反常的捕获处理外,RPC结构对注册中心的空地址推送也应该做特殊判断,用业界的专业名词来说,便是「推空维护」。
所谓「推空维护」,便是在服务发现监听获取空节点列表时,维持本地服务发现列表缓存,而不是清空处理。
这样能够完全避免类似问题。
都看到终究了,原创不易,点个重视,点个赞吧~
知识碎片从头整理,构建Java知识图谱:github.com/saigu/JavaK…(历史文章查阅十分方便)