之前总结过Zookeeper的各种设计优点,但是这个系统的缺陷与优点同样突出,本文就是结合自己的使用经验,业界给出的评价对ZK的缺点进行的归纳,一方面归纳使用表现上的不足,另一方面根据个人经验总结出系统本身功能设计时的就存在的缺陷。同时也思考了相应对策与改进的办法,算是本人对ZK设计的完整的思考总结吧。最后还关注了下etcd这个后起之秀的设计,看看它是否已经弥补了ZK的不足,能否担当后继者。
文章目录
1.实际使用暴露的不足
尽管ZK已经在工业界大量使用,但实际使用ZK时就会发现自己仍然面临种种问题,这里总结一下实际使用中系统暴露出的种种不足。
API使用复杂
使用ZK最直观的感觉就是客户端API使用起来特别麻烦,因为ZK的实际逻辑模型是文件存储,其他逻辑都需要结合使用临时节点和回调机制来实现,API接口与实际需求之间差距非常大,说白了就是ZK官方提供的API太过底层,而且表现力不足。9ZK服务只能通过客户端API访问,导致兼容性与表现各异。比如下面这样一个细节很容易坑人:
ZK不能确保任何客户端能够获取(即Read Request)到一样的数据,需要客户端自己同步:方法是客户端在获取数据之前调用org.apache.zookeeper.Async(Callback.VoidCallback, java.lang.Object) 完成sync操作。
异常状态判断复杂
比逻辑更复杂的是开发者总是需要自己同时处理ZK通信中的异常状态,比如需要正确处理CONNECTION LOSS(连接断开) 和 SESSION EXPIRED(Session 过期)两类连接异常。一个典型的难题就是Session的超时机制的问题,正如官方文档描述的:
The client will stay in disconnected state until the TCP connection is re-established with the cluster, at which point the watcher of the expired session will receive the “session expired” notification.
这些异常状态处理甚至需要考虑包括业务方系统load变高,或者发生长时间GC,导致ZK重连甚至Session过期的问题。4
回调次数限制
ZK中所有Watch回调通知都是一次性的。同一个ZK客户端对某一个Znode节点注册相同的Watch,也只会收到一次通知。Znode节点数据的版本变化会触发NodeDataChanged回调,这导致需要重复注册,而如果节点数据的更新频率很高的话,客户端肯定就无法收到所有回调通知了。8
实际客户端逻辑复杂
因为ZK提供的是非常底层的API,所以要实现一个基础需求,开发者用户自己需要依靠这些底层API在客户端实现并维护一个异常复杂的逻辑,甚至官方自己都一度不能提供正确的逻辑。比如实现分布式锁时,需要开发者自己在客户端处理异步请求锁的时间判断与仲裁,很容易出错。因此实现了多种ZK常用功能的开源项目Curator已经取代了官方成为ZK事实标准上的客户端了。这个开源项目总算让开发者使用ZK时稍微轻松一些,能够专注到自己的业务逻辑上面,但是Curator方案仍然有问题无法避免,实际正如下面这个例子所概况的那样3:
Curator 这类客户端的复杂性使得支持多语言环境较难,怎样保证两个语言的 recipe 的是行为一致的?基本没有办法通过自动化测试来保证正确性,只能不停地线上踩坑一个个排查。
Zab协议与全系统的有效性
实际应用中ZK系统整体可靠性也不一定有保证,比如在跨机房部署方面,由于ZK集群只能有一个Leader,因此一旦机房之间连接出现故障,Leader就只能照顾一个机房,其他机房运行的业务模块由于没有Leader也都只能停掉。于是所有流量集中到有Leader的那个机房,很容易造成系统crash。2
同时ZK对于网络隔离极度敏感,导致系统对于网络的任何风吹草动都会做出激烈反应。这是Zab协议本身的设计,一旦出现网络隔离,ZK就要发起选举流程。悲催的是Zab协议的选举过程比较慢,期间zookeeper由于没有Leader也不能提供服务,造成本来半秒一秒的网络隔离造成的不可用时间被放大为选举不可用时间。
整体性能有限
ZK本身的性能比较有限。典型的ZK的TPS大概是一万多,单个Znode节点平均连接数是6K,40多万Znode节点数据,设置平均30万Watch,极限只能达到40多万TPS,服务不能超过200个客户端。吞吐数据看起来似乎还可以,但是时延就没那么乐观了,基本稳定在毫秒级。1
服务缺乏可扩展性
虽然ZK中Znode节点数量并不影响读写性能(因为实际目录索引使用的是hashtable结构)但Watch设置多了也会对ZK性能有较大影响1。而ZK系统中不存在服务能力水平扩展的方法,增加机器只能增强ZK集群可靠性而已,即无法在线实时扩展一个已经运行中的ZK集群的服务能力。
存储缺乏可扩展性
ZK中Znode节点存储极限数目理论实际受限于所在机器的内存,一个Znode节点下的子节点数量则受限于系统定义的1M的数据极限,而且节点数据大小也会对ZK性能有较大影响。ZK系统设计中也是不存在存储能力的水平扩展方法的,因此ZK并不能当做分布式存储看待。
2. 系统设计上的缺陷
实际使用上的问题暴露出的是ZK系统设计层面上的缺陷,总结下可以归纳为:
- API事务能力不足,不支持客户端发起事务性的多步骤操作
- 服务器无仲裁能力,ZK服务器端不能做基本的判断逻辑,必须都在客户端进行
- 回调机制受限,Znode上面watch仅支持触发一次回调,不支持定时过期失效
- 可扩展性不足,ZK集群不支持在线动态添加机器或替换机器
- Zab协议,协议在选举和发送环节都有优化空间
3. 设想更好的设计
针对上述的问题,个人设想了一些功能设计,也许可以解决一些问题。当然个人水平有限,也欢迎大家共同探讨。
- 客户端提供更丰富有效的API。比如支持机器动态添加移除,支持CAS事务类的多步骤请求,多watch设置等
- 服务端增加逻辑处理能力,支持服务端脚本,类似像redis支持luaScript一样
- 增加更多Znode节点类型,比如定时过期的节点、支持多次触发回调的节点
- 增加服务横向扩展能力,支持多机器作为整体一个节点服务且支持在线机器动态添加移除。
- JVM是个双刃剑,统一了底层平台的同时又引入GC的麻烦
4. 继任者etcd
尽管ZK问题多多,但是一直保持着统治地位,谁想etcd居然在ZK的领域里能够迅速崛起,目前颇有平分天下的架势。微服务相关的项目都纷纷抛弃ZK转投etcd,可见其确实有可取之处,也可以看出一个项目的易用的重要性有时候甚至高于稳定性和功能,最后鹿死谁手尚未可知。etcd与ZK的不同之处与优势总结如下6:
- 使用Raft协议取代Zab协议,实现相比ZK更简单,不过跨机房问题仍然存在
- 使用go语言实现,性能对比ZK提升明显,仍然存在GC问题
- 客户端使用restful API,客户端支持类型更多,易用性加强很多
- 支持过期节点与续约机制,支持批量事务和多watch设置
- 支持运行时机器变动
5. 参考资料
- zookeeper节点数与watch的性能测试
- 对Zookeeper的一些分析
- ZooKeeper真的low吗?上千节点场景配置服务讨论
- Zookeeper常见问题整理
- ZAB问题总结
- 剖析etcd
- etcd:从应用场景到实现原理的全方位解读
- 阿里巴巴为什么不用 ZooKeeper 做服务发现?
- 采用zookeeper的EPHEMERAL节点机制实现服务集群的陷阱
本文是全系列中第1 / 3篇:分布式系统设计
- Zookeeper系统设计的缺陷
- 分布式系统的基础算法知识
- Zookeeper系统设计的优点
抢沙发
还没有评论,你可以来抢沙发