其他

服务注册与服务发现简介

前言


什么是服务注册、服务发现

假如这个产品已经在线上运行,有一天运营想搞一场促销活动,那么我们相对应的【产品服务】可能就要新开启三个微服务实例来支撑这场促销活动。而与此同时,作为苦逼程序员的你就只有手动去 API gateway 中添加新增的这三个微服务实例的 ip 与port ,一个真正在线的微服务系统可能有成百上千微服务,难道也要一个一个去手动添加吗?有没有让系统自动去实现这些操作的方法呢?答案当然是有的。且看

如图所示,当我们新添加一个微服务实例的时候,微服务就会将自己的 ip 与 port 发送到注册中心,在注册中心里面记录起来。当 API gateway 需要访问某些微服务的时候,就会去注册中心取到相应的 ip 与 port。从而实现自动化操作。

以下是一个比较完整的服务注册与服务发现的流程:

微服务独立部署、具有清晰的边界,服务之前通过远程调用构建复杂的业务功能。

服务注册与发现主要解决了如下两个问题:

  • 1、屏蔽、解耦服务之间相互依赖的细节。服务之间的远程调用必须要知道IP、端口信息。调用方直接配置被调用方的IP、端口,这种调用直接依赖IP、端口的方式存在依赖,如被调用的IP、端口变化后,调用方也要同步修改。通过服务发现,将服务的IP、端口转化成服务名来调用。
  • 2、对微服务进行动态配置。微服务架构中,服务众多、服务之间的相互依赖错综复杂,无论是服务主动停止、意外挂掉,还是因为流量增加对服务实现扩容,这些服务或状态上的动态变化,都需要尽快的通知到被调用方,被调用方采取相应的策略。对于服务注册与发现要实时管理者服务的数据和状态,包括服务的注册上线、服务主动下线,异常服务的踢出。

服务发现将服务IP、端口等细节通过一个服务名抽象给调用者,并动态管理者各个微服务的状态检测、状态更新,服务上线,下线等,这些都是微服务治理的基础,包括,负载均衡,链路跟踪。

服务注册的两种方式:

  • 客户端注册

客户端注册即为:将服务注册与服务注销的逻辑写进代码里面,当一个微服务启动的时候,将信息写入注册中心,当一个微服务下线的时候,注销相应的信息。且,需要不同的编程语言实现相同的一套逻辑。对业务代码有一定的入侵。期间,注册中心与各个微服务之间还需要保持心跳。

  • 第三方注册

第三方注册由一个独立的服务 Registrar 负责注册与注销。当服务启动后以某种方式通知Registrar,然后Registrar负责向注册中心发起注册工作。同时注册中心要维护与服务之间的心跳,当服务不可用时,向注册中心注销服务。

服务发现的两种方式

  • 客户端发现
    客户端负责向注册中心获取相应的 ip 与 port ,多种语言需要实现同一套逻辑,有点冗余的感觉。
  • 服务端发现
    由 API gateway 实现服务发现的功能,这样一套语言便可轻松维护服务发现的功能。

对于服务发现和注册,一般有两种实现模式:服务器模式、客户端模式。

服务器端模式:通过一个中间服务器,来屏蔽被调用服务的复杂性和变动性。当有新的服务加入或老服务剔除时,只需要修改中间服务器上的配置即可,此模式的显著特点是:引入独立的中间代理服务器来屏蔽真实服务的具体细节。

该模式的优点是:配置集中在独立的中间服务器端完成,对代码没有任何入侵,也不存在跨平台跨语言的问题。因为,所有请求都需要穿透中间服务器,缺点也很明显:中间服务器会成为一个单点,对性能也会有所影响。

客户端模式(进程内):在进程内直接调用服务,也叫做进程内负载,由于不需要穿透中间服务器,性能损耗比较少。但是,需要在客户端中维护注册信息,负载均衡算法等,有一定的代码入侵性,对跨平台、跨语言不是很友好。

服务发现的两种模式各有优缺点,也适用于不同的场景,对于大型应用一般会有多层负载,外层用服务器端负载均衡,内部用客户端负载均衡。

什么玩意儿?


在传统的系统部署中,服务运行在一个固定的已知的 IP 和端口上,如果一个服务需要调用另外一个服务,可以通过地址直接调用。但是,在微服务架构下,服务实例的启动和销毁是很频繁的,服务地址在动态的变化,而且,由于自动扩展,失败和更新,服务实例的配置也经常变化,所以,无法通过硬编码服务地址的方法来访问该服务。

因此,需要设置专门的服务来对实时变化的服务状态进行同步。

目前微服务的服务发现机制主要包含三个角色:服务提供者、服务消费者和服务注册表

  • 服务提供者(Service Provider):服务启动时将服务信息注册到服务注册表,服务退出时将服务注册表的服务信息删除掉
  • 服务消费者(Service Consumer):从服务注册表获取服务提供者的最新网络位置等服务信息,维护与服务提供者之间的通信。
  • 服务注册表(Service Registry):联系服务提供者和服务消费者的桥梁,维护服务提供者的最新网络位置等服务信息

服务发现机制的关键部分是服务注册表(Service Registry)。服务注册表提供管理和查询服务注册信息的API。当服务提供者的实例发生变更时(新增/删除服务),服务注册表需要通知服务消费者同步最新的服务实例地址列表。目前大多数的微服务框架使用Netflix Eureka、Etcd、Consul或Apache Zookeeper等作为服务注册表。

服务注册(Services Registration)


自注册模式

服务实例本身负责从服务注册表中注册和注销服务,必要时发送心跳防止注册过期。

优点就是简单,不需要其它组件的参与;缺点在服务实例与服务注册表相对应,必须要为服务中用到的每种编程语言和框架实现服务注册的代码

第三方注册模式

服务实例由另一个系统组件(Registrar)负责注册。Registrar通过轮询部署环境或订阅事件去跟踪运行中的实例的变化,并且向服务注册表注册或者注销服务。

优点是解耦了服务和服务注册表,不需要为每个语言和框架都实现服务注册逻辑。服务实例注册由一个专用的服务集中实现。

缺点是除了被内置到部署环境中,它本身也是一个高可用的系统组件,需要被启动和管理

服务发现(Services Discovery)


客户端发现模式

客户端负责确定服务提供者的可用实例地址列表和负载均衡策略。客户端访问服务注册表,定时同步目标服务的实例地址列表,然后基于负载均衡算法选择目标服务的一个可用实例地址发送请求。

当服务客户端调用目标服务时,它会查询服务注册表以获取服务实例地址列表。为了提高性能,客户端缓存服务实例地址列表。然后,服务客户端使用负载均衡算法(如循环或随机)来选择服务实例发送请求。优点:

  • 通常是服务客户端查询目标服务的实例地址列表之后,执行负载均衡算法选择可用的目标服务。优点是服务客户端可以灵活、智能地制定负载均衡策略,包括轮询、加权轮询、一致性哈希等策略。
  • 可以实现点对点的网状通讯,即去中心化的通讯。可以有效避开单点造成的性能瓶颈和可靠性下降等问题。
  • 服务客户端通常以SDK的方式直接引入到项目,这种方式语言的整合程度最佳,程序执行性能最佳,程序错误排查更加容易。

缺点:

  • 服务客户端与服务注册表耦合。需要为服务客户端使用的每种编程语言和框架实现客户端服务发现逻辑。
  • 服务客户端通常以SDK方式使用服务发现功能。这种侵入式方案存在于应用程序的所有客户端,如果客户端服务发现功能需要进行更新,要求所有的应用程序重新编译,部署服务。微服务的规模越大,服务更新越困难,这在一定程度上违背了微服务架构提倡的技术独立性。

服务端发现模式

使用服务端发现模式,服务客户端通过路由器(或者负载均衡器)访问目标服务。路由器负责查询服务注册表,获取目标服务实例的地址列表转发请求。

优点:

  • 部署平台提供服务发现功能,负责处理服务发现的所有方面。因此,无论使用任何语言,所有的服务提供者和消费者都可以轻松地使用服务发现机制。
  • 服务发现功能对于服务客户端而言是透明的,因此,服务发现功能的相关更新对于服务客户端是无感知的。

缺点:

  • 部署平台的服务发现功能仅支持发现使用该平台部署的服务。例如,基于Kubernetes 的服务发现仅适应于在Kubernetes上部署运行的服务。
  • 服务的架构增加了一次转发,延迟时间会增加。整个系统增加了一个故障点,系统的运维难度增加。最关键的是负责转发请求的路由器或者负载均衡器可能变成性能的瓶颈。
  • 微服务的一个目标是故障隔离,将整个系统切割为多个服务共同运行,如果某服务无法正常运行,只会影响到整个系统的相关部分功能,其它功能能够正常运行,即去中心化。然而,服务端发现模式实际上是集中式的做法,如果路由器或者负载均衡器无法提供服务,那么将导致整个系统瘫痪。

服务注册表(Services Registry)


服务注册表是一个可用的服务实例的数据库。服务注册表提供了一个管理API和一个查询API。服务实例的注册和注销通过管理API实现,查询API用来寻找可用的服务实例。服务注册表是一个分布式的kv数据库,因此,存在CAP问题。根据CAP原则:分布式系统不能同时支持 C(一致性)、A(可用性)、P(分区容错性)需要根据自己的实际业务需求选择CAP的其中两个。

常见注册中心


Eureka

Eureka Server 集群当中的每个节点都是通过 Replicate(即复制)来同步数据,没有主节点和从节点之分,所有节点都是平等而且数据都保持一致。因为结点之间是通过异步方式进行同步数据,不保证强一致性,保证可用性,所以是 AP。

注册与发现的工作流程

  • 假设 Eureka Server 已经启动,Eureka Client(服务提供者)启动时把服务注册到 Eureka Server;
  • Eureka Client(服务提供者)每 30 秒(默认可配置)向 Eureka Sever 发 http 请求(即心跳),即服务续约;
  • Eureka Server90 秒没有收到向 Eureka Client(服务提供者)的心跳请求,则统计 15 分钟内是否存在 85% 的 Eureka Client(服务提供者)没有发心跳,如果是则进行自我保护状态(比如网络不稳定),如果不是则剔除该 Eureka Client(服务提供者)实例;
  • Eureka Client(服务消费者)定时调用 Eureka Server 接口获取服务列表更新本地缓存;
  • Eureka Client(服务消费者)远程调用服务时,先从本地缓存找,如果找到则直接发起服务调用,如果没有则到 Eureka Server 获取服务列表缓存到本地后再发起服务调用;
  • Eureka Client(服务提供者)应用关闭时会发 HTTP 请求到 Eureka Server,服务端接受请求后把该实例剔除。

Zookeeper

Zookeeper 支持 CP,当集群中如果有节点宕机则需要选举 leader(FastLeaderElection),选举过程需要 30 至 120 秒,选举过程时集群不可用,牺牲时间来保证数据一致性。

注册与发现的工作流程

  • 假设 ZK 已经启动,服务提供者启动时把服务注册到 ZK 注册中心;
  • ZK 注册中心和服务提供者之间建立一个 Socket 长连接,ZK 注册中心定时向每个服务提供者发数据包,如果服务提供者没响应,则剔除该服务提供者实例,把更新后的服务列表发送给所有服务消费者(即通知);
  • 服务消费者启动时到 ZK 注册中心获取一份服务列表缓存到本地供以后使用;
  • 服务消费者远程调用服务时,先从本地缓存找,如果找到则直接发起服务调用,如果没有则到 ZK 注册中心获取服务列表缓存到本地后再发起服务调用;
  • 当其中一个服务提供者宕机或正常关闭时,ZK 注册中心会把该节点剔除,并通知所有服务消费者更新本地缓存;
  • 当这个服务提供者正常启动后,ZK 注册中心也能感知到,并通知所有服务消费者更新本地缓存。