那些年,我们一起踩过的ZooKeeper的坑

背景介绍

因为项目中使用到了ZooKeeper作为服务发现,对这个比较通用的ZooKeeper项目进行了半年多的调研和使用。

ZooKeeper的基本介绍已经有很多资料,所以这篇文章不做介绍了。可能是因为ZooKeeper是使用Java开发的,很多项目都是以Java项目为背景来讲解ZooKeeper的注意事项,因为Java有成熟的API,CuratorZkClient,尤其是Curator,简直是ZooKeeper开发必备神器,可惜我们的项目是C++的,而C++写的好用的API几乎没有,于是只能自己动手造轮子,写了一个C++的ZooKeeper API,具体可以移步《Zookeeper C语言API封装和注意事项》

这篇文章主要讲一下开发和运营过程中,因为不熟悉ZooKeeper的机制,在开发和运维阶段容易踩入的坑。

开发中的坑

Watcher机制

ZooKeeper的Watcher,实际上分为节点子节点两个类型,再加上注册Watcher的时候提供的路径Context回调函数地址,这四个变量的值确定了唯一一个Watcher。

而ZooKeeper还提供一种全局Watcher的注册方式,使用全局Watcher则相当于Context和回调函数地址都使用初始化时传入的那个全局Context和回调函数地址,是一种特殊情况。

ZooKeeper的Watcher另一个特性是,只触发一次,所以要尽量避免漏消息的情况发生,如果Watcher触发后还需要继续监听,则需要重新注册一次。
Watcher的信息量比较大,因此不可能将所有的信息都传给服务端,实际上服务端只会保存一个标志位,比如这个路径存在节点或者子节点的Watcher,如果有触发,那么就会通知客户端,并且删除标志位。
客户端的处理也是类似的,只是额外存储了Context和回调函数地址,Watcher触发时,调用回调函数,并且删除Watcher记录。

客户端的回调会在同一个线程中串行执行,因此,回调里不能有太耗时的操作,否则会影响后面所有回调的触发,包括异步接口的回调,也是一样,不能有太耗时的操作。

因此,在使用过程中可能会踩入下面的坑:

  • Watcher触发后没重新注册,不会触发第二次。
  • 使用同样的参数,同样的方式注册多次,只会触发一次。
  • Exist和Get使用相同的参数重复注册,实际上只会触发一次,Exist和Get的区别只是Exist在节点不存在的时候能够注册Watcher,在节点创建的时候触发节点CREATE事件,而Get在节点不存在的时候无法注册Watcher。
  • 使用Exist、Get和GetChildren使用全局回调注册同一个节点,如果节点删除,只会触发一次Watcher。
  • 如果使用Client ID重连已存在的Session,所有的Watcher会失效,需要重新注册。

以上只触发一次的Watcher的情况在实际项目开发中都需要对本地的数据进行特殊处理,避免出现丢Watcher的现象发生。

Multi机制

ZooKeeper提供批量操作接口,但是它并不是事务操作,也不是原子操作,只是简单的顺序操作,而且最关键的一点是,如果批量操作中有一个操作未成功(返回值非ZOK),则后面的所有的操作都不会执行,在结果中均返回-2,整个批量请求的返回值为首个失败的值,并且前面执行成功的操作不会回退

返回值例子如下图所示,前面的Set和Create操作成功,但是因为Exists操作失败,后面的3个操作均不执行,返回-2。

因此,如果使用批量操作,如果正常情况下不是全成功的话,需要了解一下可能出现的这个烂摊子。
当然,也可以根据这个策略,正确使用,比如在我封装的API中,递归创建节点,就是用的批量操作递归检测目录是否存在。
比如要创建节点”/a/b/c/d”,那么就发一个批量请求:
Exists “/a”
Exists “/a/b”
Exists “/a/b/c”
Exists “/a/b/c/d”
如果成功,则表示节点已经存在;如果失败,则找到第一个失败的节点,比如“/a/b/c”,表示这个节点和后面的节点都不存在,那么再发送一个批量请求,将不存在的节点全部创建好:
Create “/a/b/c”
Create “/a/b/c/d”

其他的坑

除了上面的必须要了解的坑,如果想尝试封装API,可能还会遇到临时节点,重连,解除重注册Watcher、ACL的控制,使用Client ID恢复Session等一系列的坑,详细可以参考《Zookeeper C语言API封装和注意事项》

运维中的坑

磁盘满进程自动退出

由于ZooKeeper设计的时候就是以保障集群可用性作为目标,因此单节点的可用性并不是特别关心,所以一旦单节点出现异常情况,比如不同步或者磁盘满了,那么单台ZooKeeper会采取比较激进的处理方式:

  • 如果某个节点与Leader不同步了,根据ZAB协议,它将直接被踢出集群
  • 如果某个节点磁盘满了,那么进程会自动关闭
  • 如果数据异常了,那么它将无法启动

这样做的好处是处理简单,并且逻辑统一,而且偶然的单节点事故并不会对集群的可用性造成影响,其他的运维操作交给运维管理员根据各自的情况处理。
所以在运维过程中,需要对节点的可用性做监控,比如发送四字命令”ruok”进行进程存活判断。如果进程不正常了,需要自动拉起或者告警,如果无法拉起或者反复出现死机的情况,则需要人为干预修复。
因此,ZooKeeper的运维工作是必不可少的,毕竟ZooKeeper在项目中一般都处于关键路径,集群挂了的话,整个项目服务都可能瘫痪。设计中也可以“留一手”,比如在服务发现中缓存ZooKeeper的配置,即便ZooKeeper挂了,还能够使用缓存中的数据。

进程日志默认不滚动

ZooKeeper使用log4j库进行程序日志输出,默认情况是打印到控制台,使用自带的脚本启动会将打印信息重定向到zookeeper.out文件中,并且要命的是,这个日志文件不会进行滚动,于是等待你的可能是一个数十G的zookeeper.out文件。
因此,需要对ZooKeeper的日志配置部分进行修改:

 

事务日志和快照文件默认不清除

ZooKeeper设计者为了避免数据丢失,对事务日志和快照文件采取了一种比较保守的策略,即默认情况下永不删除。但是提供了删除不必要的日志的工具和配置,所以在ZooKeeper服务启动前,也需要做这项工作,否则日志文件会将磁盘撑满,然后ZooKeeper进程退出。

这里简单介绍一下ZooKeeper的事务日志文件和快照文件,事务日志文件和快照文件类似于Redis的AOF和RDB,详细资料可以参考ZooKeeper Administrator’s Guide-Data File Management

  • 事务日志文件
    每次写入事务都会写到事务日志文件中,保存在配置文件中的dataLogDir中,如果不存在,则保存在dataDir中,文件名为”log.[起始事务zxid]”。
    因为它对写操作影响很大,所以官方强烈建议将其放到一个单独的物理磁盘中,避免对写入性能造成影响,如果只是作为服务发现,基本没什么事务日志的话,就可以不用这样操作了。
    这个文件默认情况是64M一个,在配置文件中可通过preAllocSize参数配置,单位为KB。如果写满了或者重启或者生成了新的快照文件都会重新生成一份,旧的不会删除。因此时间长了,可能会产生上百G的日志文件。
  • 快照文件
    快照文件保存了一个时期所有的数据,快照加上事务日志文件,就可以恢复快照后任意zxid的数据版本,它保存在dataDir中,文件名为”snapshot.[起始事务zxid]”。
    快照的生成默认是每达到一定次数的事务就会重新生成一份,这个数值的配置项为snapCount,生成快照所需事务为snapCount/2到snapCount中随机选择一个值,避免集群中所有的节点同时开始生成快照。默认为10万,表示5到5W次事务会生成一次快照。

还好ZooKeeper提供了很方便的清除过期数据文件的方法,一般是设定保留N份快照文件以及这些快照文件恢复所需的日志文件。
清除过期日志文件目前有两种方法:
1、定期调用bin/zkCleanup.sh,用法为:

参数count表示保存的快照数量,必须>=3。

2、3.4版本后可以在配置中配置清理参数:
autopurge.snapRetainCount:表示保存的快照数量,必须>=3
autopurge.purgeInterval:自动清理的间隔时间(小时),必须>=1,为0表示不进行自动清理。
配置好之后,重启ZooKeeper,会进行第一次的数据清理工作,之后每隔autopurge.purgeInterval小时会进行一次数据的清理。

参考资料

anyShare分享到:

原文地址:http://godmoon.wicp.net/blog/index.php/post_421.html,转载请注明出处

Moon发表于2016年11月30日
打赏作者

您的支持将鼓励我们继续创作!

[微信] 扫描二维码打赏

[支付宝] 扫描二维码打赏

发布者

sytzz

学会用简单的语言将复杂的问题说清楚。

发表评论

电子邮件地址不会被公开。