zookeeper Watch丢通知故障的定位

2020-03-24 17:37:51 浏览数 (1)

在下面的描述中,ZK指的是zookeeper,Watch丢通知故障简称为丢消息,因个人水平的原因,文章中定位出的原因,未必是真实的原因,仅供参考。

背景介绍

在我深度参与的一个计算平台项目中,团队第一次使用ZK作为配置中心,ZK的功能:(1)存储和固化配置;(2)在配置发生更新的时候,通知多个工作节点拉取新的配置。这两项是ZK作为分布式服务框架最常用的功能之一。项目使用github上开源的go-zookeeper库来实现ZK的操作,库地址为:github.com/samuel/go-zookeeper/zk 。

工作节点任务的升级,依赖于ZK通过Watch消息通知给客户端代理,以下简称agent,agent是由我开发维护的模块。ZK一共3个节点,按照IP最后的数字,分别命名为144、227、229。故事发生在agent和三台ZK服务器之间。

系统简化结构系统简化结构

故障现象

用户在客户端执行一些配置更新后,经常反馈计算节点的配置没有更新成功,还在跑着旧版本。登录计算节点查看日志,可以发现在用户执行更新后的几分钟内,Agent没有进入任何通知消息的回调处理。故障的紧急恢复采用的方法是重启agent,重启后会全量拉取新的配置。另外注意到,重启后一段时间内(在几个小时到几天不等),可以正常收到ZK的消息。

定位过程

首先简单介绍代码。在zk.Connect连接上conf.ZkHost中的某一台ZK节点后,在go-zookeeper的sendLoop中会按照指定的时间间隔,由agent主动发起ping操作并等待应答。没有收到或者收到错误的应答之后,连接将被关闭,并且在一个for循环中主动去尝试conf.ZkHost中的其他节点,如果迅速恢复,使用的sessionid不发生变化。

代码语言:go复制
//连接ZK服务器,注册回调函数
if agent.zkConn, _, err = zk.Connect(conf.ZkHost, time.Second, 
                           zk.WithEventCallback(ZkCallback), 
                           zk.WithMaxBufferSize(10*1024*1024), 
                           zk.WithMaxConnBufferSize(10*1024*1024)); 
                           err != nil {
   Logger.LogError("connect %v err, %s", conf.ZkHost, err)  
}
//ZK回调函数
func ZkCallback(ev zk.Event) {
	go agent.HandleEvent(ev)
}
//ZK消息处理函数
func (m *McAgent) HandleEvent(ev zk.Event) {	
	switch ev.Type {
	case zk.EventNodeChildrenChanged:
		//
	case zk.EventNodeDataChanged:
		//
	case zk.EventNodeDeleted:
		//	
	}
}

根据代码,结合go-zookeeper实现,发现了第一个问题:没有处理ZK的状态变化消息。在agent与ZK节点之间由于网络偶发故障或延时,导致Agent ping ZK节点失败的时候,ZK会连接其他ZK节点,这时候之前通过GetW方式注册的Watch事件会全部丢失。导致这个Agent再也收不到原先监控的ZK节点变化的消息。

针对这个故障,考虑到在网络故障的短暂时间内存在丢消息的可能,因此解决方案比较直接:

代码语言:javascript复制
func (m *McAgent) HandleEvent(ev zk.Event) {
   
   switch ev.Type {
   case zk.EventNodeChildrenChanged:
      //
   case zk.EventNodeDataChanged:
      //
   case zk.EventNodeDeleted:
     //
   case zk.EventSession:
      if ev.State == zk.StateExpired {
        //继续使用本地缓存的数据,可能是脏的
        //打告警
      } else if ev.State == zk.StateConnected {
        //从ZK全量拉取新数据,重新注册Watch
      }
   }
}

作为一个严谨求实的程序员,本着对自己代码高度负责任的态度,我使用iptables模拟网络断开重连的情况,能及时的给出告警,并且可以自动重连,重连之后还能实时收到ZK服务器发出的事件通知。

代码语言:shell复制
iptables -A OUTPUT -p tcp --dport 2181 -j DROP
iptables -D OUTPUT -p tcp --dport 2181 -j DROP

更新上线,平静了好几天,没人找我说怎么任务又更新失败了。完美。

在我以为这事终于消停之后,又偶尔有用户在群里怼我,说有更新失败的情况,但是我没有收到任何告警信息。感觉颜面扫地,年底可以one星走人了。不过离年底还有点时间,先再找找原因。

从故障Agent的日志看,没有任何异常,也没有任何ZK连接变化相关的日志信息。去ZK节点上捞取日志,通过一系列检索过程,发现了故障场景的共性。

故障相关的ZK节点日志故障相关的ZK节点日志

日志不太直观,下面的图形更直观一些。简单的说,就是Agent所连接的ZK服务器,在静默的情况下,由一台(144)迅速迁移到了另一台(227),使用相同的sessionid重建与新服务器的连接。由于客户端注册的Watch在ZK服务器上是以本地存储的方式记录,并没有同步给其他机器。因此,在连接静默迁移到新的服务器之后,Watch又丢掉了。所谓静默迁移,就是agent端没有收到连接变化相关的任何回调消息。这可能是go-zookeeper的bug,但是时间精力原因,没有继续深入下去了。

故障过程描述故障过程描述

解决方案

从ZK相关的文档可以看出,ZK消息并不保证一定送达,在网络短暂故障的重连期间,仍然可能存在消息丢失的情况,所以在ZK服务器压力不大并且数据不大的情况下,彻底的解决方案是废弃Watch机制不用,周期性全量拉取ZK数据。

使用定期全量拉取数据之后,类似问题再也没有出现过。

优化空间

在ZK节点数量比较多、节点数据比较大的情况下,大量重新拉去数据会给ZK服务器和网络带来压力。准备执行的优化是设置一个节点,存储数据版本信息,在数据版本未发生变化的时候,不执行重新拉取的操作。

还有一个方向是去调试go-zookeeper代码,比较简单的方式是在其中起连接ZK IP的监控代码,在调用Next函数的时候,使用setState方式发送一下连接变化的消息。但是由于通过网络传输的消息存在丢失的可能,这仍然不是最终的解决方法。

再次强调,相关结论存在模糊和不清楚的地方,不要轻信。有更好方法的,请留言告知。

0 人点赞