背景
Haunt是有赞内部使用的服务发现系统,下面会详细介绍一下该系统的设计与思考。
首先,我们设想一下,我们提供的RESTful API或者其他API的服务,为了完成一次服务请求,服务调用方需要知道服务实例的网络位置(IP地址和端口)。传统应用都运行在物理硬件上,服务实例的网络位置都是相对固定的。比较常见的做法是,服务调用方可以从一个经常变更的配置文件中读取网络位置。而对于一个现代的,基于云端微服务的应用来说,这却是一个很麻烦的问题。如下图所示:
PaaS平台中的应用一般都有多个实例,实例故障重启透明化与负载均衡都与服务发现密切相关。通过服务发现机制,可以透明的对多个实例进行访问,并实现负载均衡。而且应用的某个实例随时都可能故障重启,这时就需要动态配置服务调用方的路由信息。服务发现就可以解决这个动态配置的问题,Haunt(Youzan服务发现系统)也应运而生。
服务发现
Haunt使用的服务发现模式是客户端发现模式。使用客户端发现模式时,客户端决定相应服务实例的网络位置,并且对请求实现负载均衡。客户端查询服务注册中心,后者是一个可用服务实例的数据库;然后使用负载均衡算法从中选择一个实例,并发出请求。这种模式相对直接,除了服务注册外,其它部分无需变动。此外,由于客户端知晓可用的服务实例,能针对特定应用实现智能负载均衡。当然缺点也比较明显,客户端与服务注册中心绑定,要针对服务端用到的每个编程语言和框架,实现客户端的服务发现逻辑;而且对于服务治理不是非常友好。架构如下图:
服务注册
服务实例必须向注册中心注册服务,这样服务调用方才能从注册中心拉到服务的实例列表。实现服务注册的方式有两种:自注册模式和第三方注册模式。
自注册模式是服务实例自己负责在服务注册表中注册与注销。另外,如果需要的话,一个服务实例也要发送心跳到注册中心来保证注册信息不会过时。自注册模式的优点是相对简单,无需其他系统组件。然而,他的主要缺点是把服务实例与服务注册中心耦合,必须在每个编程语言和框架内实现注册代码。
Haunt使用的是第三方注册模式,服务实例不需要直接跟注册中心进行交互。服务注册器(Haunt Agent)负责向注册中心注册和注销此服务,并对服务进行健康检查。架构如下图:
第三方注册模式的优点主要是服务与注册中心解耦,无需为每个编程语言和框架实现服务与注册中心的逻辑(注册,注销,维持心跳等)。相反,服务实例通过一个专有服务(Haunt)以中心化的方式进行管理。不足之处在于,除非该服务内置于部署环境,否则需要配置和管理一个高可用的系统组件。Haunt Agent单独内置于部署环境的理由也是如此,因为不想引入一个高可用的系统组件来保证服务注册器的数据一致性,从而增加系统的复杂度。
Haunt Agent运行在集群的每一个节点上,每一个Haunt Agent拥有分布式健康检测机制,这个健康检测机制可以应用到任意规模的集群,而不仅仅是作用于特定的服务器组。同时,Haunt也支持在本地进行多种健康检测。比如,可以检测web服务器是否正在返回200状态码,内存利用率是否达到临界点,是否有足够的数据存储盘等。最后,Haunt可以配置健康检测的间隔时间,对于一些容错率要求比较高的服务,可以配置1秒检测,这样在服务故障的情况下,基本可以秒级收敛。
注册中心
注册中心是服务发现的核心,是包含服务实例数据(例如ip地址,端口等)的数据库。所以注册中心需要一个高可用的分布式键/值存储,例如Etcd,Zookeeper,Consul等。
在注册中心的选型上,Haunt选择了Etcd。可能很多人会有疑问,为什么不用Zookeeper?不可否认,Zookeeper是最早被用来做服务发现的开源项目之一,主要优势是拥有成熟、健壮以及丰富的特性。Zookeeper普遍的应用场景,按照个人感觉应用的频率从高到低排序主要是这些:可靠存储(配置管理,名字服务等),集群管理,选主服务,服务注册管理,分布式锁,负载均衡等。
然而,Zookeeper的缺点也很明显。首先,Zookeeper会引入大量的依赖,而运维人员普遍希望机器集群尽可能地简单,维护起来也不易出错;还有,Zookeeper的部署、维护和使用都很复杂,管理员需要掌握一系列知识和技能,一方面Zookeeper是功能丰富,但从另一个角度分析,丰富的特性反而将其从优势转变为累赘。因此,在我看来,Etcd是在服务发现上可能是更好的选择。
当然不仅仅是以上几个方面,经过详细的调研,下面我会具体来分析下Etcd相比Zookeeper所具有的特点:
- Etcd更加稳定可靠,它的唯一目标就是把一致性kv做到极致,更注重稳定性和扩展性。
- 在服务发现的实现上,Etcd使用的是节点租约(Lease),并且支持Group(多key);Zookeeper使用临时节点,临时节点的问题后面会提到。
- Etcd支持稳定的watch,而不是Zookeeper一样简单的one time trigger watch。因为在未来微服务的环境下,通过调度系统的调度一个服务随时可能会下线,也可能应对临时访问压力增加新的服务节点。而很多调度系统是需要得到完整节点历史记录的,etcd可以存储上十万个历史变更。
- Etcd支持mvcc,因为有协同系统需要无锁操作。
- Etcd支持更大的数据规模,支持存储百万到千万级别的key。
- 相比Zookeeper,Etcd性能更好。在一个由3台8核节点组成的的云服务器上,etcd可以做到每秒数万次的写操作和十万次读操作。
关于性能,下面两张图是高并发下,Etcd与Zookeeper耗时与吞吐量的对比:
注:上图来自https://github.com/coreos/dbtester
红色线是Etcd,由上面两个图可以看出,在高并发下,Etcd相比Zookeeper,耗时和吞吐量相对而言更加稳定,性能更好。
再来说说Zookeeper的临时节点,临时节点其实就是Zookeeper里的K/V条目,当客户端跟Zookeeper连接断开的时候,这些条目会被删除,它只是一个非常原始的活跃度检测。也就是说,在客户端跟Zookeeper之间网络分区或者网络不稳定的情况下,这些条目也有可能被删除,导致所有客户端都删除该服务实例,虽然可能此时该服务实例是健康的。因此Zookeeper的临时节点存在固有的扩展性问题,并且会增加客户端的复杂性。与ZooKeeper服务器端连接时,客户端必须保持活跃,并且去做持续性连接。此外,ZooKeeper还需要胖客户端,会暴露系统的复杂性给客户端,而胖客户端是很难编写的,并且胖客户端会经常导致调试质询。而Etcd采用的是租约(Lease)机制,通过TTL可以有效的规避网络分区的问题。
未来展望
我心中相对完整的微服务架构可能如下图。