背景
产线nacos集群硬件比较差,cpu只有2核,出现了一次cpu满,导致服务调用失败,复盘后,要进行硬件升级,升到4核,先停1台,观察,刚开始没有影响,以为稳了,没有想到过了3分钟,业务监控指标就不行了,服务出现调用失败,报No provider 异常,立即重新拉起来,因为下线一台,就导致1分多钟的线上故障,一身冷汗啊!!!
nacos作为服务注册中心,不是AP吗,怎么停一台就有问题。不合理啊。
为啥刚停没有问题,等了3分钟才出现问题,为啥啊
带着这两个问题和错误日志,花了几天的时间排查,总算是把问题弄清楚了,也顺便把nacos的同步机制整明白了。
日志现象
重新拉起来后,就立即开始了故障分析,因为没有其他异常,就从错误日志开始排查,我们集群三个节点,发现另外两台在停掉一台3分钟后,都出现大批量的链接超时,删除了链接对应的client,从而触发了推送,导致客户端没有了节点可用。
客户端空保护
dubbo consumr在路由时,如果对打tag的机器做路由不能给正常的机器使用,即使只剩下一台打tag的机器,也不能用,反而对tag的流量可用用非tag的机器,即做了降级,是不是很难理解,不为正常的流量做降级,反而为灰度的这点流量做了兜底,个人觉得这个设计很坑,因为我们线上都会有一台灰度的机器,nacos推送的时候,最后存活的是这个灰度的机器,consumer 端就game over了,即出现no provider错误。
同步原理
经过日志分析,最终定位是同步延迟导致的,但为啥同步会延迟了,同步的机制和原理又是怎么样的,你一定又这些疑问,下面我们就来说明下同步的原理,只有搞明白了同步的原理,才能理解为啥在下线一台后,同步就会延迟。
集群同步架构
nacos 注册中心是最终一致性,2.x的版本和客户端是通过grpc长链接来实现的,所以集群集群的每个节点会把注册到自己节点的client,同步给集群其他几台节点,具体的架构图如下:
-
client :就是provdier和nacos 建立链接后 nacos维护的一个记录,用来标记一个客户端,分两种:
- native client:即直接链接在本节点的provider,也该节点是client的责任节点。
- 非native client,即是集群其他节点同步过来的,也就是非责任节点,
Verify 任务
同步给其他节点,怎么知道该client是否活的呢,因为链接又不是和它建立的,为了解决这个问题,nacos 会为所有要同步的节点,生成一个verify 任务,去更新下同步过去的client的活跃时间,来让集群其他节点,认为是活的,不要给删了,nacos会有一个定时执行的超时检查任务,如果在3分钟内这个client 没有更新活跃时间,就认为该同步过来的节点超时了,要干掉了Delete 任务
正常情况下,通过verify 任务来续约,如果nacos发现这个client链接端了,异常了,比如pod重启了,nacos 就会通过delete task 来告诉其他节点,该client已经挂了,不需要维护了。
超时机制
上面介绍verify 任务时,说了超时检查,为了更好的理解,再提供一张图:
同步原理
通过上面同步,verify,delete 任务,nacos让provider在集群里保持一致,做到了服务注册无论链接到那个nacos节点,consumer 链接谁,都能消费到全量的数据,就是这个同步来保证的。
上面只是一个概括同步机制,那同步是怎么实现的呢,写再多,不如一张图好理解,真正实现同步的原理在下面:
知识点:
- 集群又几个节点,就同步几次,每次都生成一个同步任务。
- 当前同步是添加到任务集合ConcurrentMap就结束,异步化。
- 有一个线程专门负责从ConcurrentMap取任务,因为任务支持延迟,就是在这里判断的,超过延迟时间,就删除,同时生成一个新的提交到同步线程引擎队列。
- 同步引擎线程池,线程数和cpu核数保持一致,每个线程匹配一个大小为2的15次方的队列,执行线程就不断的从这个队列取任务来发给目前节点。
- 网络发送是通过grpc异步发送。
- 失败处理机制,因为不能丢任务,所以失败了是要把这个任务重新加回到队列。
根因分析
通过这个图,特地标记红色的地方,就是我们这次停了一台机器导致服务调用失败的根因,我们用的是2.0.3的版本,我们集群3台机器,所以停了一台时,这台机器上的链接全部断开,客户端会重连,这时就产生了很多的同步任务,要同步,好了这么多任务要同步,但是一个任务要同步给两台机器,也就是两个任务,同步给这个故障的节点由于在前面没有判断,到最后执行同步的线程检查的时候,肯定是失败的,就等100ms,还重试3次,也就是往这台故障的节点同步一个任务,这个同步线程要阻塞300ms,才能去处理下一个任务,这个地方的代码贴下,更直观点:
所以在停掉一台节点的情况,一个任务要延迟300ms,哪超过600个任务要同步时,这地600以后的任务同步延迟就在3分钟以上,就超过了前面我们说的同步client的超时时间,就触发了删除操作,push了错误的数据给客户端。
所以如果你是2.0.3的版本,平时重启时一定要快,如果要长时间下线的,一定要升级到新的版本,否则给你的大礼就是故障了。
解决方案
上面的问题, 是因为同步延迟导致超时,被删除,触发推送,但是服务本身是没有任何问题的,而且除了这个场景外,还会有其他的问题导致错误推送,比如nacos 节点cpu100%,fugc等,网络问题等,都会触发该问题,所以我们需要一个完整的解决方案,确保nacos 无论出现了啥问提,都不能影响服务的调用,服务都是正常的,你nacos 有问题,为啥影响服务调用。
所以我们从两个方便入手,一个是增加一个反向检查,确保服务正常时,不再乱推,还有一个就时推拉空保护。两个措施结合,能把nacos稳住。
反向检查
在超时任务触发时,如果超时了,就对client做一次健康检查,通过类似tcp telnet机制,因为我们有服务的ip和端口,通过建立链接来判断是否成功,现在服务注册的都有这个机制,比如consul都支持,不过这里需要注意的事,检查通过了,需要做一些脏数据清理。
推拉空保护
客户端从nacos获取数据,就两个地方,一个是客户端主动拉,一个是nacos推送,所以在这两个地方都要加一个保护,防止推送空的实例,我们是定义一个空的发展,计算实例数小于该阀值时,就不推送或者饭后空的集合。