分析一下我司ZooKeeper客户端封装思路

  为了提高系统的可用性,我司现在不少项目已经采用ZooKeeper进行服务发布和服务发现了。
  之前已经对这块内容预学习了很久,虽说ZooKeeper的C API使用起来很简单,核心接口不超过十个左右的函数,但是多线程的库中接口涉及到大量的函数回调,要想在C++中做到傻瓜式的使用还是有点麻烦的,因此同往常一样C++中也有大量针对ZooKeeper Client封装的轮子。当前我们软件研运部使用客户端库的是我们老大传说花了一周心血封装完成的,已经被大量使用并接受了考验,这边文章是对其封装设计思路的总结,所有Idea及版权归原作者所有,抱歉源码无法分享。

  对于服务提供者来说,最为常见的情况就是把服务自身创建注册为一个临时节点,当自己挂掉之后该节点自动消失,监听服务目录的服务调用者感知变化并作出反应。这比较的直观的设计方式,基本ZooKeeper的教科书都会这么举例子,但是也带来一个问题:因为临时节点不支持创建子节点,所以这个服务提供者的其他配置信息就必须丢到node data域里面了,而如果配置信息比较多的时候就需要使用json或者乱七八糟的编码方式,这同时也意味着更新一个配置的话需要将数据全部取出来解码,修改某些字段后再编码打包,最后再将这一坨东西写回去。如果是程序自动协助完成还好,但是要在zkCli.sh的方式临时手动更新的话,这种困难可想而知。
  因此,我们不将服务提供者实现为一个临时节点,而将其创建为一个持久节点,然后在其下面建立属性子节点方便配置和更新,下面就是一个服务提供者所需要考虑的常见属性信息,当然还可以按照需求扩充,下面这些节点容我描述过来。
eink-pdf
  图中的service_name族的节点表示某个具体的服务,服务消费者可以Watch这个节点,而以host:port命令的子节点代表一个个实际的服务提供者,在服务提供者启动的时候会尝试创建或者更新这个永久节点,这个服务提供者节点的子节点包括:
  active:是一个临时节点,只有在其存在的时候才表示服务提供者是活着可用的,服务挂掉或者和ZooKeeper会话断开后会自动消失;
  priority:服务提供者的优先级,值高的表示优先级大,在消费者获取服务的时候会有限选择他,这尤其在准备形式部署情况很有用;
  weight:服务提供者的权重值,可以给多个服务者设置合适的权重,以实现合理的负载均衡,并且可以根据服务提供者的质量进行动态调整优化;
  enable:可以手动关闭某个节点的访问,而不用将其杀死或者剔除ZooKeeper。
  使用过程中,当服务提供者将自己注册完成后,就算是功成圆满了,但是服务调用者对于服务提供者的选择可能是千差万别的:比如主备模式、权重模式、轮训模式等,要作为一个通用的客户端库就必须考虑好各种各样的应用场景,上面的priority和weight恰好可以满足这些常见的消费场景了。不过需要说明的是,这里的属性值都是通过我们手动配置的,服务调用者Watch节点后会感知这种变化,从而加载到本地中去;同时服务调用者可以根据调用结果做一些流控优化,增加或者降低某些服务提供者的权重甚至优先级,不过他们修改的数据都是本地的副本,而不应该修改全局的配置,因为每个调用者都是从自己的视角来评价服务的,造成这样的因素可能是调用者本身的问题,而不应该将自己的感受强加给其他的调用者,否则集群可能会很不稳固。
  服务调用者读取和Watch服务目录得到所有的服务提供者,同时也获得各个服务提供者的priority、weight等参数,而在真正消费的时候会根据优先级、权重等因素作用下获得一个可用节点,进而对其发起调用。这些数据都是缓存在本地的,所以性能不会是问题,同时服务提供者的任何更新都会触发Watch的回调被执行,进而感知更新;如果事件丢失或者获取失败,调用者还可以使用本地Legacy的旧数据继续维持运转。恩,这里如果加个定时器定期去获取更新节点信息是不是更好点?

  其实,大公司有其繁荣之道,小公司也有其存亡之理。以现在公司的体量,这种方式应该是很够用了,目前主要的缺陷是服务挂掉后active节点虽然会自动消失,但是整个服务提供者的节点还存在着,对于洁癖人来说有点不能忍受……

本文完!