zookeeper能够同步同步各节点的znode数据,client可以使用getChildren,getData,exists方法在znode tree路径上设置watch,当watch路径上发生节点create、delete、update的时候,会通知到client。client可以得到通知后,再获取数据,执行业务逻辑操作。
但是因为没有消息接收后的确认机制,这个通知机制是不可靠的,也就是说znode的修改者并不知道是否所有的client都被通知到了,或者说client也不知道自己是否错过了哪些通知消息。这种现象可能由网络原因引起,也可能是client刚触发了watch事件,还没有来得及重新设置watch,下个事件就发生了(zookeeper只提供了一次性watch)。
在笔者的使用场景中,这是有问题的。在我们分布式配置管理的场景中,有3份配置副本,管理节点本地数据库中有一份,zookeeper中保存了一份,使用配置的软件引擎进程中保存了一份。当下发配置时,是需要全部软件引擎都生效最新的配置,而如果有某个引擎错过了通知,那么就会漏掉某个配置,导致问题,而到底谁没有成功生效,这些节点都不知道,甚至错过通知的节点自身也不知道。
因此我提出一种设计,至少让错过通知的节点自身知道错过了消息,并采取主动同步配置的方式,来补救。这样能够保证,配置下发后,至少一段时间后所有软件引擎的都使用了最新的配置。
这利用了zookeeper自身的几个特性:
1)zookeeper维护一个全局的操作id,zxid,每一个create,delete,update操作都会使该id加1。
2)zookeeper为每个路径(znode)节点都保存了它的修改版本dataversion,和最新一次修改zxid——mzxid。
3)zookeeper保证每个client连接的session中,看到的通知顺序与这些事件发生的先后顺序是严格一致的。
我们来假设要在/group/policy下增加、删除、修改配置,每个配置有1个节点,配置数量可以很多、而且不固定。client要知道增加了什么配置,修改了什么,删除了什么配置,因此设定了watch。其中A复杂修改这些配置,N1,N2,...Nm这些节点监听通知,并更新软件引擎的变量,使其生效配置。
首先A在/group/policy下做create、delete、update操作后,都要set 一次 /group/policy节点。这会导致/group/policy的dataversion加1。并且可以知道有几次操作,dataversion就增加几。
伪代码如下:
doSomeOperion();
setData("/group/policy","")
Ni节点要对/group/policy下节点发生修改的事件进行watch,还要对/group/policy节点自身的修改进行watch。因此如果Ni没有错过通知的话,它将一次触发两个通知:1)配置变化通知;2)/group/policy数据更新通知。
Ni要在本地保存三个变量current_dataversion,current_zxid
在Ni client初始化时:
current_dataversion=/group/policy的dataversion;
current_zxid=/group/policy的mzxid;
然后在watch到配置发生变化的回调函数中:
doSometing(); //生效具体配置
current_dataversion += 1; //期待/group/policy的下一个dataversion增加1
在watch到/group/policy的数据发生变化后回调函数中:
if current_dataversion == /group/policy的dataversion: //意味着没有漏掉消息
current_zxid = /group/policy的mzxid
elif next_dataversion < /group/policy的dataversion: //一位置有配置变化的消息没有收到
遍历/group/policy子节点
if /group/policy/znodei的mzxid > current_zxid:
使用znodei中的配置。
删除已经不存在znode的配置项;
同步完成;
next_dataversion = /group/policy的dataversion
current_zxid = /group/policy的mzxid
这样就可以保证client能够发现自己错过了消息,并发现哪些znode的修改被自己错过了。那么至少在下一次发生修改配置后,client能够完全与当前配置一致。
我们可以写一个场景验证下:
初始时/group/policy下为空,/group/policy的stat为(mzxid=2,dataversion=0)
current_dataversion=0; current_zxid=2 1)create /group/policy/n1(mzxid=3,dataversion=0) 收到通知 current_dataversion+=1 (等于1) 2)set /group/policy (mzxid=4,dataversion=1) 收到通知 curren_dataversion==/group/policy.dataversion,没有漏掉通知 current_zxid=/group/policy.mzxid (等于4) 情形一) 3.1) create /group/policy/n2 (mzxid=5,dataversion=0) 没有收到通知 current_dataversion不变(等于1) 4.1)set /group/policy (mzxid=6,dataversion=2) 收到通知 current_data < /group/policy.dataversion,得知漏掉了通知,并且知道漏掉1个 同步mzxid大于current_zxid(值为4)的节点(即n2节点)配置; 删除已经不存在znode的配置; current_data = /group/policy.dataversion (等于2) current_zxid = /group/policy.zxid (等于5) 情形二) 3.2)create /group/policy/n2(mzxid=5,dataversion=0) 收到通知 current_dataversion+=1 (等于2) 4.2)set /group/policy (mzxid=6,dataversion=2) 没有收到通知 current_zxid(=4)不变。漏掉该消息是没有关系的,再次收到该消息时,会更新current_zxid 情形三) 3.3)create /group/policy/n2(mzxid=5,dataversion=0) 没收到通知 current_dataversion不变(等于1) 4.3)set /group/policy (mzxid=6,dataversion=2) 没有收到通知 current_zxid(=4)不变。 5)create /group/policy/n3(mzxid=7,dataversion=0) 收到通知 current_dataversion+=1 (等于2) 6)set /group/policy (mzxid=8,dataversion=3) 收到通知 current_data < /group/policy.dataversion 得知漏掉了通知 同步mzxid大于current_zxid(值为4)的节点(即n2,n3节点)配置; 删除已经不存在znode的配置; current_data = /group/policy.dataversion (等于3) current_zxid = /group/policy.zxid (等于8)
通过这种方式,可以让client端知道自己错过了通知,至少在下次收到/group/policy节点更新通知时,能够重新同步配置。因此可以保证client之间迟早会变得同步。
更进一步,可以额外再增加时钟来触发对/group/policy节点的检查。这样就可以保证一个时钟间隔之后,client肯定是同步的。