1. 前言
在传统的开发中,由于提供服务的地址是相对静态的,所以我们只需要找到对应服务的开发人员,然后了解到对应的服务接口地址就可以了。
而在微服务架构开发过程中,如果我们需要调用一个RESTFul风格的API接口,我们就需要知道对应的服务在网络的什么位置,也就是说需要知道服务方的IP和端口号。在微服务架构或分布式环境下,这个问题很难得到解决,所以服务注册与发现技术不可或缺,这也是程序员进阶之路必须要掌握的核心技术之一。
2. 架构演进
先来看一个问题,假如现在我们要做一个商城项目,作为架构师,应该怎样设计系统的架构?你心里肯定在想:这还不容易直接照搬淘宝的架构不就行了。但在现实的创业环境中一个项目可能是九死一生,如果一开始投入巨大的人力和财力,一旦项目失败损失就很大。
作为一位有经验的架构师需要结合公司财力、人力投入预算等现状,选择最适合眼下的架构才是王道。大型网站都是从小型网站发展而来,架构也是一样。
任何一个大型网站的架构都不是从一开始就一层不变的,而是随着用户量和数据量的不断增加不断迭代演进的结果。
在架构不断迭代演进的过程中我们会遇到很多问题,技术发展的本质就是不断发现问题再解决问题,解决问题又发现问题。
2.1 单体架构
在系统建立之初可能不会有特别多的用户,将所有的业务打成一个应用包放在tomcat容器中运行,与数据库共用一台服务器,这种架构一般称之为单体架构。
在初期这种架构的效率非常高,根据用户的反馈可以快速迭代上线。但是随着用户量增加,一台服务的内存和CPU吃紧,很容易造成瓶颈,新的问题来了怎么解决呢?
2.2 应用与数据分离
随着用户请求量增加,一台服务器的内存和CPU持续飙升,用户请求响应时间变慢。这时候可以考虑将应用与数据库拆开,各自使用一台服务器。
突然有一天扫地阿姨不小心碰了电线,其中一台服务器掉电了,用户所有的请求都报错,随之而来的是一系列投诉电话。
2.3 集群部署
单实例很容易造成单点问题,比如遇到服务器故障或者服务能力瓶颈,那怎么办?聪明的你肯定想到了,用集群呀。
集群部署是指将应用部署在多个服务器或者虚机上,用户通过服务均衡随机访问其中的一个实例,从而使多个实例的流量均衡,如果一个实例出现故障可以将其下线,其他实例不受影响仍然可以对外提供服务。
随着用户数量快速增加,老板决定增加投入扩大团队规模。开发团队壮大后效率并没有得到显著的提高,以前小团队可以一周迭代上线一次,现在至少需要两到三周时间。
业务逻辑越来越复杂,代码间耦合很严重,修改一行代码可能引入几个线上问题。架构师意识到需要进行架构重构。
2.4 微服务架构
当单体架构演进到一定阶段后开发测试的复杂性都会成本增加,团队规模的扩大也会使得各自工作耦合性更严重,牵一发而动全身就是这种场景。
单体架构遇到瓶颈了,微服务架构就横空出世了。微服务就是将之前的单体服务按照业务维度进行拆分,拆分粒度可大可小,拆分时机可以分节奏进行。最佳实践是先将一些独立的功能从单体中剥离出来抽成一个或多个微服务,这样可以保障业务的连续性和稳定性。
如上图将一个商用应用拆分为六个独立微服务。六个微服务可以使用Docker容器化进行多实例部署。
架构演化到这里遇到了一个难题,如果要查询用户所有的订单,用户服务可能会依赖订单服务,用户服务如何与订单服务交互呢?订单服务有多个实例该访问哪一个?
通常有几种解决办法:
(1)服务地址硬编码
服务的地址写死在数据库或者配置文件,通过访问DNS域名进行寻址路由。
服务B的地址硬编码在数据库或者配置文件中,服务A首先需要拿到服务B的地址,然后通过DNS服务器解析获取其中一实例的真实地址,最后可以向服务B发起请求。
如果遇到大促活动需要对服务实例扩容,大促完需要对服务实例进行下线,运维人员要做大量的手工操作,非常容易误操作。
(2)服务动态注册与发现
服务地址硬编码还有一个非常致命的问题,如果一台实例挂了,运维人员可能不能及时感知到,导致一部分用户的请求会异常。
引入服务注册与发现组件可以很好解决上面遇到的问题,避免过多的人工操作。
2.5 架构演进总结
在单体架构中一个应用程序就是一个服务包,包内的模块通过函数方法相互调用,模型足够简单,根本没有服务注册和发现一说。
在微服务架构中会将一个应用程序拆分为多个微服务,微服务会部署在不同的服务器、不同的容器、甚至多数据中心,服务实例在网络的位置还可能是由网络动态分配的。除此之外,由于服务的扩展、故障、服务升级等操作,都会导致网络地址的变化,这个时候客户端调用就没有办法精确的知道具体的服务的IP和端口了。这个时候,服务注册和发现就成为了一个不可或缺的组件。
3. 服务注册与发现基本原理
服务注册与发现是分为注册和发现两个关键的步骤。
服务注册:服务进程在注册中心注册自己的元数据信息。通常包括主机和端口号,有时还有身份验证信息,协议,版本号,以及运行环境的信息。
服务发现:客户端服务进程向注册中心发起查询,来获取服务的信息。服务发现的一个重要作用就是提供给客户端一个可用的服务列表。
3.1 服务注册
服务注册中心是整个服务发现最为重要的一个组件,它存储了整个网络中所有实例的网络位置数据。从这个角度来讲,服务注册中心需要始终保持两点,1)是高可用,2)数据一定要是最新的。
当然实际的工作中,客户端可以从缓存中获取到服务端网络位置,但是这个信息总会有不可用的时候,到那个时候客户端就无法知道服务的位置了。
服务注册有两种形式:自主注册和代理注册(第三方注册)。
3.1.1 自主注册
自主注册是服务自己要负责注册与注销的工作。当服务启动后注册线程向注册中心注册,当服务下线时注销自己。
这种方式的缺点是注册注销逻辑与服务的业务逻辑耦合在一起,如果服务使用不同语言开发,那需要适配多套服务注册逻辑。
3.1.2 代理注册(第三方注册)
代理注册由一个单独的代理服务负责注册与注销。当服务提供者启动后以某种方式通知代理服务,然后代理服务负责向注册中心发起注册工作。
代理服务还会通过轮询的方式来跟踪服务实例的变化情况,如果发现新的服务的时候就会将其实时添加到服务注册中心;
这种方式的好处就是实现了服务实例和服务注册中心的解耦,不需要专门对接服务实例语言,但是如果要实现高可用的模式的话,就需要引入一个高可用的组件(代理服务)来保障注册中心的高可用,增加了成本。
3.2 服务发现
服务发现也分为客户端发现和代理发现。
3.2.1 客户端发现
客户端发现是指客户端负责向注册中心查询可用服务地址,获取到所有的可用实例地址列表后客户端根据负载均衡算法选择一个实例发起请求调用。
这种方式非常直接,实现起来相对比较简单,除了与服务注册中心的联动,没有其他的联动操作,客户端可以控制负载均衡算法。
但是缺点也很明显,获取实例地址、负载均衡等逻辑与服务的业务逻辑强耦合在一起,如果服务发现或者负载平衡有变化,那么所有的服务都要修改重新上线。另外,还需要为客户端的每一种编程语言都需要实现服务发现逻辑。
3.2.2 代理发现
代理发现是指新增一个路由服务负责服务发现获取可用的实例列表,服务消费者如果需要调用服务A的一个实例可以直接将请求发往路由服务,路由服务根据配置好的负载均衡算法从可用的实例列表中选择一个实例将请求转发过去即可,如果发现实例不可用,路由服务还可以自行重试,服务消费者完全不用感知。
这种方式的优点就是将服务发现的功能从客户端抽离出来,进行了解耦合,客户端只需要向路由服务发出请求就可以了,剩下的就交给路由服务,这样就不需要为每个客户端都实现一个发现逻辑了,除此之外,路由服务还可以实现负载均衡功能。
这样实现的缺点是除了引入一个负载均衡的环境之外,还需要引入一个高可用的系统组件,并且管理相对比较复杂。
3.3 为什么redis不行?
假如我们根据key-value的存储方式,去存储服务的信息,请求时去redis获取服务位置可以吗?
答案是否定的,因为作为一个服务发现服务,不仅要保存服务的网络位置(ip+port),而且还需要实现几点:
1)需要有服务监听的功能,隔一段时间去监听服务是否正常,即健康检查;
2)需要有负载均衡的功能,如果有多个节点提供服务,特别是高并发的时候,要知道把请求落到哪个节点上,才是相对最合理的;
3)如果几百个服务,我们不可能一个个的去修改服务的配置,而且本身作为微服务,每个服务之间应该都是独立的,一个服务的改动不应影响到其他服务;
所以说一个服务发现的系统,至少应该具备服务发现、健康检查、负载均衡、全局发布的功能;
4. 心跳机制
如果服务有多个实例,其中一个实例出现宕机,注册中心是可以实时感知到,并且将该实例信息从列表中移出,也称为摘机。
如何实现摘机?业界比较常用的方式是通过心跳检测的方式实现,心跳检测有主动和被动两种方式。
4.1 被动检测
被动检测是指服务主动向注册中心发送心跳消息,时间间隔可自定义,比如配置5秒发送一次,注册中心如果在三个周期内比如说15秒内没有收到实例的心跳消息,就会将该实例从列表中移除。
心跳机制-被动检测
上图中服务A的实例2已经宕机不能主动给注册中心发送心跳消息,15秒之后注册就会将实例2移除掉。
4.2 主动检测
主动检测是注册中心主动发起,每隔几秒中会给所有列表中的服务实例发送心跳检测消息,如果多个周期内未发送成功或未收到回复就会主动移除该实例。
心跳机制-主动检测
5. 业界常用的服务注册与发现组件对比
了解服务注册与发现的基本原理后,如果要在项目中使用服务注册与发现组件,当面对众多的开源组件时该如何进行技术选型?
在互联网公司里,有研发实力的大公司一般会选择自研或者基于开源组件进行二次开发,但是对于中小型公司来说直接选用一款开源软件会是一个不错的选择。
常用的注册与发现组件有Eureka,zookeeper,consul,etcd等,由于Eureka在2018年已经宣布放弃维护,这里就不再推荐使用了。
业界开源组件
下面结合各个维度对比一下各组件。
组件 | 优点 | 缺点 | 接口类型 | 一致性算法 |
---|---|---|---|---|
zookeeper | 1.功能强大,不仅仅只是服务发现; 2.提供watcher机制可以实时获取服务提供者的状态; 3.广泛使用,dubbo等微服务框架已支持; | 1.没有健康检查; 2.需要在服务中引入sdk,集成复杂度高; 3.不支持多数据中心; | sdk | Paxos |
consul | 1.开箱即用,方便集成; 2.带健康检查; 3.支持多数据中心; 4.提供web管理界面; | 不能实时获取服务变换通知 | restful/dns | Raft |
etcd | 1.开箱即用,方便集成; 2.可配置性强 | 1.没有健康检查; 2.需配合三方工具完成服务发现功能; 3.不支持多数据中心; | restful | Raft |
从整体上看consul的功能更加完备和均衡。接下来以consul为例详细介绍一下。
6. Consul
6.1 简单认识一下Consul
Consul是HashiCorp公司推出的开源工具,使用Go语言开发,具有开箱即可部署的特点。Consul是分布式的、高可用的、 可横向扩展的用于实现分布式系统的服务发现与配置组件。
6.2 Consul有哪些优势?
1)服务注册发现:Consul提供了通过DNS或者restful接口的方式来注册服务和发现服务。可根据实际情况自行选择。
2)健康检查:Consul的Client可以提供任意数量的健康检查,既可以与给定的服务相关联,也可以与本地节点相关联。
3)多数据中心:Consul支持多数据中心,这意味着用户不需要担心Consul自身的高可用性问题以及多数据中心带来的扩展接入等问题。
6.3 Consul的架构图
Consul架构
Consul 实现多数据中心依赖于gossip protocol协议。这样做的目的:
1)不需要使用服务器的地址来配置客户端;服务发现是自动完成的。
2)健康检查故障的工作不是放在服务器上,而是分布式的。
6.4 Consul的使用场景
Consul的应用场景包括服务注册发现、服务隔离、服务配置等。
服务注册发现场景中consul作为注册中心,服务地址被注册到consul中以后,可以使用consul提供的dns、http接口查询,consul支持health check。
服务隔离场景中consul支持以服务为单位设置访问策略,能同时支持经典的平台和新兴的平台,支持tls证书分发,service-to-service加密。
服务配置场景中consul提供key-value数据存储功能,并且能将变动迅速地通知出去,借助Consul可以实现配置共享,需要读取配置的服务可以从Consul中读取到准确的配置信息。
- END -
本文参考自《13张图彻底搞懂分布式系统服务注册与发现原理》