Skip to content

微服务之服务发现

约 3321 字大约 11 分钟

微服务服务发现Spring CloudEureka

2025-09-17

为什么要使用服务发现功能?

当调用REST API 或Thrift API的服务时,我们在构建请求时通常需要知道服务实例的IP和端口。在传统应用中,服务实例的地址信息相对固定,可以从配置文件中读取。而这些地址也只是只会偶尔更新。

但在现代应用程序中,往往是基于云的微服务架构,此时获取服务实例的IP和端口便是一个需要解决的难题。如下图所示:

上图中,服务实例实例的IP是动态分配。同时,还面临着服务的增减、故障以及升级等变化。这对于客户端程序来说,就需要使用更精确的服务发现机制。

ba5a567ff62c2cac326fdea417dce8b1

目前,服务发现模式主要有两种:客户端发现模式服务端发现模式。先来看一下客户端发现模式。

客户端发现模式

使用客户端发现模式时,客户端负责判断服务实例的可用性和请求的负载均衡。服务实例存储在注册表中,也就是说注册表是服务实例的数据库。客户端通过查询服务注册表,获得服务实例列表,然后使用负载均衡算法从中选择一个,然后发起请求。

下图为这种模式的架构图:

5ddf859baff127561150fc3baaada243

此种模式下,当服务实例启动时,会将自己的地址信息注册到服务注册表,当服务停止时从服务注册表中移除。这期间,通常使用心跳机制来定刷新服务实例的注册。

Netflix Eureka就是一个服务注册表组件,它提供了基于REST API的服务实例注册和查询功能。Netflix Ribbon是一个IPC客户端,可配合Eureka实现对服务实例请求的负载均衡。

客户端发现模式的优点是相对简单,除服务注册表外,不需要其他部分做改动。同时,由于客户端知道所有的可用实例,可以做出更明智的、基于特定应用场景的负载均衡决策,比如使用一致性哈希算法。这种模式的缺点是将客户端和服务注册表功能耦合在了一起,必须为每种编程语言和框架的客户端实现服务发现逻辑。

缺点可能听起来晦涩难懂,现在让我为你举一个例子说明。

缺点解释

缺点主要是 耦合性和实现复杂度高

  1. 客户端必须直接跟注册中心打交道
    • 每个客户端都要写一套「获取服务列表 + 选择实例」的逻辑。
    • 如果你有 Java、Python、Go 三种语言的客户端,都要分别实现一套。
  2. 更新和维护麻烦
    • 如果注册中心协议改了(比如从 HTTP 改成 gRPC),所有客户端代码都得改。
    • 如果要增加新特性(比如加权路由、蓝绿发布),也要修改所有客户端。
  3. 一致性风险
    • 每个客户端实现的负载均衡策略可能不一致。
    • 有些客户端可能缓存了过时的实例信息,导致调用失败。

举个例子

假设有一个电商系统:

  • 服务 A:订单服务
  • 服务 B:库存服务
  • 注册中心:Nacos

采用 客户端发现模式

  1. Java客户端调用库存服务
    • Java里用 Ribbon(Spring Cloud 组件)来从 Nacos 获取 B 的实例列表,然后自己做负载均衡。
  2. Python客户端调用库存服务
    • Python里要单独实现一套逻辑,比如调用 Nacos API 拉取实例,再写个轮询负载均衡算法。
  3. Go客户端调用库存服务
    • Go语言也得再写一套发现逻辑。

问题出现了

某天运维同学在 Nacos 里 新增了一个权重路由策略(例如:让「库存服务的实例1」只处理 20% 请求,「实例2」处理 80% 请求)。

  • Java 客户端更新了 SDK,支持权重路由。
  • 但 Python、Go 客户端没有更新,仍然用轮询。
  • 结果就出现:
    • Java调用分布正常(20/80)。
    • Python和Go调用分布混乱(50/50)。
    • 库存实例1压力太大,最终崩溃。

这就是所谓「客户端和注册中心耦合,导致不同语言实现不一致」的问题。

服务器端发现模式

另外一种服务发现模式就是服务器发现模式。下图中展示了该模式的结构:

2216c79f489747400ae4e83910b59411

客户端通过负载均衡器向服务发起请求,负载均衡器查询服务注册表,并将请求路由到可用的服务实例。与客户端发现相比,服务实例是通过服务注册表进行注册和注销的。

AWS的ELB(Elastic Load Balancer)就是服务器端发现路由器的示例。ELB通常用于负载均衡来自外网的流量,但你也可以使用ELB来负载均衡私有云(VPV)内部的流量。客户端使用DNS名称,通过ELB发送请求(Http或TCP),ELB在已注册的弹性计算云(EC2)实例或EC2容器服务(ECS)的容器之间进行负载均衡。这种实现并没有单独的服务注册表,而是将EC2实例和ECS容器注册到ELB自身上。

Http服务器和负载均衡器(比如,Nginx plus和Nginx)也可以用作服务器端发现的负载均衡器。比如,使用Consul模板动态配置Nginx反向代理。Consul可以从存储在Consul服务注册表中的配置数据中定时重新生成任意配置文件。每当文件改变时,可以运行一个任意shell命令。比如,Consul模板生成一个nginx.conf文件,用于配置反向代理,然后执行命令告诉Nginx去重新加载配置。

某些部署环境(例如Kubernetes和Marathon)会在集群中的每个主机上运行一个代理。这个代理扮演服务器端发现负载平衡器的角色。客户端向服务发出请求时,会通过代理进行路由,透明地将请求转发到集群中某个服务实例。

服务器端发现模式最大的优点是,服务发现的实现细节从客户端抽离出来了,客户端只用发送请求到负载均衡器即可。这样就无需为每种编程语言和框架的客户端实现服务发现逻辑。而且,某些部署环境已经免费提供了该功能。当然,这种模式也有一些缺点,如果部署环境未提供负载均衡器,你还需要搭建和管理一个额外的高可用系统组件。

两者区别


服务端发现模式简介

  • 核心思想:客户端只知道 服务名,不知道具体实例;服务端(通常是网关或负载均衡器) 负责从注册中心获取实例列表并做负载均衡。
  • 客户端实现简单:不需要实现服务发现和负载均衡逻辑。
  • 负载均衡集中管理:策略统一、更新方便。

区别于客户端发现模式

特性客户端发现服务端发现
谁做负载均衡客户端服务端(网关/代理)
客户端复杂度高,每种语言都要实现发现逻辑低,客户端只关心服务名
灵活性高,可以做自定义策略(比如一致性哈希)中,策略由网关统一控制
多语言支持每种语言都要实现各语言客户端都一样,统一调用网关即可
更新策略成本

举个例子

场景

  • 服务 A:订单服务
  • 服务 B:库存服务
  • 注册中心:Nacos

1️⃣ 客户端发现模式

  • Java 客户端调用库存服务
    1. Java客户端向 Nacos 拉取库存服务实例列表
    2. 自己选择一个实例做负载均衡(轮询/随机/一致性哈希)
  • Python客户端调用库存服务
    • 也得实现一套拉取实例和负载均衡逻辑

问题:每种语言都要实现服务发现逻辑,策略可能不一致。


2️⃣ 服务端发现模式

  • Java/Python客户端
    • 只发请求给网关或负载均衡器:http://gateway/stock-service/api/...
  • 网关/负载均衡器
    1. 查询 Nacos 获取库存服务的实例列表
    2. 根据统一策略(轮询/权重)选择实例
    3. 转发请求到对应实例

好处

  • 客户端完全不用管实例信息。
  • 更新负载均衡策略只需修改网关配置,所有客户端立即生效。
  • 各语言客户端一致,运维更方便。

简单理解:

  • 客户端发现:客户端像“自己去菜市场挑菜”,自己选哪家服务实例。
  • 服务端发现:客户端像“告诉餐厅我想吃什么”,餐厅(网关)帮你挑菜。

服务注册表

服务注册表是服务发现的关键,它是一个包含服务实例地址信息的数据库。服务注册表需要具有高可用性和实时更新性。客户端可以缓存从注册表获得的服务实例地址信息。但这些信息会过时,因此,服务注册表也需要是集群模式,且集群之间还需要通过协议维持一致性。

Netflix Eureka是一个服务注册表组件,它提供了基于REST API形式的服务实例注册和查询功能。一个服务实例可以通过POST请求将自己注册到注册表中;可以通过PUT请求,每隔30秒刷新它的注册信息;可以通过Http的DELETE请求或超时机制来删除实例的注册信息;可以使用Http的GET请求来检索注册的服务实例。

常见的服务注册表组件有:etcd、Consul、Apache Zookeeper、Nacos等。

服务注册的选项

服务实例必须通过注册表进行注册或注销,通常有几种不同方式来处理注册和注销。一种是服务实例自己注册,即自我注册模式;另一种是基于其他系统组件来管理服务实例的注册,即第三方注册模式。先来看一下自我注册模式。

自我注册模式

当使用自我注册模式时,服务实例负责在服务注册表中进行自身的注册和注销。如果需要,服务实例还需要发送心跳请求以避免因超时而被注销。下图展示了这种模式的结构图:

b2ab52eeb6e960e8df86d98ae4f7ccfc

Netflix OSS Eureka客户端就是这种模式的示例,Eureka客户端负责处理服务实例所有的注册和注销事项。在Spring Cloud项目中,实现了包括服务发现的各种模式,基于此可以很轻松的实现自动注册服务实例到Eureka。你只需在Java配置类上使用@EnableEurekaClient注解即可。

自我注册模式的优点是使用起来非常简单,不需要任何其他系统组件。缺点是服务实例与服务注册表紧密耦合,需要在每种编程语言和框架中实现注册功能。

另外一种方式可以让服务和注册表解耦的方式就是第三方注册模式。

第三方注册模式

当使用第三方注册模式时,服务实例不再负责将自己注册到服务注册表。这一功能由第三方组件作为服务注册商来处理。服务注册商通过轮询或订阅事件来跟踪实例的变化。当发现新的可用服务实例时,会将服务实例注册到服务注册表中。同时,也会注销已经停止的服务实例。

下图展示了这种模式的结构:

012a6ad37208ec5f3851a2b010648d5d

开源项目Registrator便是一个示例,它可以基于Docker容器自动注册和注销服务实例。Registrator支持多种注册表,包括etcd和Consul。

NetflixOSS Prana项目是另外一个示例,它主要用于非JVM语言编写的服务,是与服务实例并行的Sidecar应用程序。Prana基于Netflix Eureka注册和注销服务实例。

第三方注册模式的优点是服务与服务注册表分离,无需每种编程语言和框架的客户端实现服务注册逻辑,而是在专用服务内以集中方式处理服务实例注册。这种模式缺点是,除非部署环境提供内置服务,否则还需要额外搭建和管理一个高度可用的系统组件。