微服务系列(三):服务注册与发现

目录

1 微服务的注册与发现

微服务注册与发现类似于生活中的"电话通讯录"的概念,它记录了通讯录服务和电话的映射关系。在分布式架构中,服务会注册进去,当服务需要调用其它服务时,就这里找到服务的地址,进行调用。

步骤如下:

1、你先要把"好友某某"记录在通讯录中。

2、拨打电话的时候通过通讯录中找到"好友某某",并拨通回电话。

3、当好友某某电话号码更新的时候,需要通知到你,并修改通讯录服务中的号码。

从这个过程中我们看到了一些特点:

1、把 "好友某某" 的电话号码写入通讯录中,统一在通讯录中维护,后续号码变更也是更新到通讯录中,这个过程就是服务注册的过程。

2、后续我们通过"好友某某"就可以定位到通讯录中的电话号码,并拨通电话,这个过程理解为服务发现的过程。

而我们微服务架构中的服务注册与发现结构如下图所示:

image_6_1.png

图片中是一个典型的微服务架构,这个结构中主要涉及到三大角色:

provider - 服务提供者

consumer - 服务消费者

register center - 注册中心

它们之间的关系大致如下:

1、每个微服务在启动时,将自己的网络地址等信息(微服务的ServiceName、IP、Port、MetaData等)注册到注册中心,注册中心存储这些数据。

2、服务消费者从注册中心查询服务提供者的地址,并通过该地址调用服务提供者的接口。

3、各个微服务与注册中心使用一定机制(例如心跳)通信。依据租约、连接状态、主动探测及保护策略判断是否标记不健康或剔除实例。

优点如下:

1、解耦:服务消费者跟服务提供者解耦,地址变更可由发现机制传播,但接口契约仍需兼容

2、扩展:服务消费者和服务提供者增加和删除新的服务,无需手工维护全部地址,但存在传播延迟和容量变化

3、中介者设计模式:用一个中介对象来封装一系列的对象交互,这是一种多对多关系的中介者模式。

从功能上拆开主要有三块:服务注册、服务发现,和注册中心。我们一个一个来看。

1.1 服务注册

如图中,为Register注册中心注册一个服务信息,会将服务的信息:ServiceName、IP、Port以及服务实例MetaData元数据信息写入到注册中心。当服务发生变化的时候,也可以更新到注册中心。

image_6_2.png

服务提供者(服务实例) 的服务注册模型是一种简单、容易理解、流行的服务注册模型,其在多种技术生态中都有所体现:

1、在K8S生态中,通过 K8S Service服务信息,和Pod的 endpoint(用来记录service对应的pod的访问地址)来进行注册。

2、在Spring Cloud生态中,应用名 对应 服务Service,实例 IP + Port 对应 Instance实例。比较典型的就是A服务,后面对应有多个实例做负载均衡。

3、在其他的注册组件中,比如 Eureka、Consul,服务模型也都是 服务 -> 服务实例。

可以认为服务实例是一个真正的实体的载体,服务是对这些相同能力或者相同功能服务实例的一个抽象。

image_6_3.png

1.2 服务发现

服务发现实际就是我们查询已经注册好的服务提供者,比如 p->p.queryService(serviceName),通过服务名称查询某个服务是否存在,如果存在,

返回它的所有实例信息,即一组包含ip 、 port 、metadata元数据信息的endpoints信息。

这一组endpoints信息一般会被缓存在本地,如果注册中心挂掉,当缓存地址仍然有效时可以维持部分调用;它降低了运行期依赖,不保证冷启动或所有故障下都可用。对于单个 Service 后面有多个 Instance的情况(如上图),做 load balance。

服务发现的方式一般有两种:

1、拉取的方式:服务消费方(Consumer)主动向注册中心发起服务查询的请求。

2、推送的方式:服务订阅/通知变更(下发):服务消费方(Consumer)主动向注册中心订阅某个服务,当注册中心中该服务信息发生变更时,注册中心主动通知消费者。

1.3 注册中心

注册中心提供的基本能力包括:提供服务注册、服务发现 以及 健康检查。

服务注册跟服务发现上面已经详细介绍了, 健康检查指的是指注册中心能够感知到微服务实例的健康状况,便于上游微服务实例及时发现下游微服务实例的健康状况。采取必备的访问措施,如避免访问不健康的实例。

主要的检查方式包括:

1、服务Provider 进行 TTL 健康汇报(Time To Live,微服务Provider定期向注册中心汇报健康状态)。

2、注册中心主动检查服务Provider接口。

综合我们前面的内容,可以总结下注册中心有如下几种能力:

1、高可用

这个主要体现在两个方面。一个方面是,注册中心本身作为基础设施层,具备高可用;第二种是就是前面我们说到的去中心化,极端情况下的故障,已有有效缓存可能维持调用,新服务发现与地址更新仍会受影响

2、可视化操作

常用的注册中心,类似 Eureka、Consul 都有比较丰富的管理界面,对配置、服务注册、服务发现进行可视化管理。

3、高效运维

注册中心的文档丰富,对运维的支持比较好,并且对于服务的注册是动态感知获取的,方便动态扩容。

4、权限控制

数据是具有敏感性,无论是服务信息注册或服务是调用,需要具备权限控制能力,避免侵入或越权请求

5、服务注册推、拉能力

这个前面说过了,微服务应用程序(服务的Consumer),能够快速感知到服务实例的变化情况,使用拉取或者注册中心下发的方式进行处理。

image_6_4.png

2 按故障行为与生态选择注册中心

注册中心提供的是服务地址及状态视图,调用是否成功还取决于网络、实例状态和客户端策略。下面比较能力与边界,不按厂商地域排列优先级。

2.1 Nacos:同时提供注册发现和配置管理

Nacos 的 Naming 与 Config 是不同子系统。以本文示例采用的 2.x 代际为例,临时实例同步使用 Distro,持久实例涉及 Raft/JRaft;不能把配置数据也笼统描述成同一条 Raft 复制链路。配置持久化与同步还取决于版本和部署方式。

Nacos 2.x Java 客户端通常通过 gRPC 长连接维护临时实例,不能继续按 1.x HTTP 心跳模型解释全部行为。服务端主动探测、临时实例连接存活和应用真正可处理业务也不是同一概念。Nacos 架构说明

它与 Spring Cloud Alibaba、Dubbo 等生态有适配,但命名空间、分组、服务模型和客户端版本需要对齐。配置管理减少了组件种类,也增加了同一平台承担的职责,应分别配置访问权限和恢复策略。

2.2 Consul:目录、健康检查和跨数据中心

Consul 的客户端代理、服务端与数据中心关系

在经典 Agent 部署中,Client Agent 参与本地成员管理并将相应请求交给 Server。Server 通过 Raft 维护目录等状态;LAN/WAN Gossip 用于成员和故障信息传播,不是用来保证跨数据中心业务数据原子提交。

Raft 的提交需要投票成员多数派。例如 3 个投票节点可容忍 1 个故障,5 个可容忍 2 个;增加到 4 个并不比 3 个多容忍一个故障。奇数规模通常更经济,但并不保证不会发生选票分裂。选择 3 或 5 个节点需要结合故障域、时延和恢复目标。Consul 共识机制

Raft 投票节点数量与多数派的关系

Consul 的读取有不同一致性模式,不能因写入使用 Raft 就认为所有查询都读到最新状态。健康检查支持 HTTP、TCP、TTL 等形式;能建立 TCP 连接并不代表业务依赖正常。

2.3 etcd 与 Kubernetes:不要混淆存储和服务发现接口

etcd 是基于 Raft 的键值存储,提供事务、Lease 和 Watch,可作为构建注册系统的基础。其默认 KV 读取可提供线性一致语义,显式的 serializable 读取可以返回较旧数据;Watch 也不等同于线性一致读取。etcd v3.5 API 保证

在 Kubernetes 中,典型链路是:

  1. 应用声明 Service,控制器根据 Selector、Pod 等信息维护 EndpointSlice
  2. 这些 API 资源由 API Server 持久化到 etcd
  3. kube-proxy、CoreDNS 等组件通过 Kubernetes API 观察相关资源,更新转发规则或 DNS 响应
  4. 应用访问 Service 名称或地址,通常无需自行连接控制面的 etcd

Service、EndpointSlice 与 DNS 已经满足需求时,不必为了“云原生”再让业务应用直连 etcd。Kubernetes Service

2.4 ZooKeeper:以会话和节点构建协调

ZooKeeper 节点组织与服务注册模型

持久节点保留到显式删除;临时节点与会话绑定,会话过期或关闭后删除。实例突然失联时不会立刻消失,需要等待会话判定,因此消费者还要处理地址失效窗口。

临时节点与服务实例会话的关系

ZooKeeper 使用 ZAB 进行有序复制,不能简单写成“Paxos 算法”。成功的写入具有相应原子性和顺序保证,但不同客户端读取可能暂时看到不同进度,不能概括为所有节点时刻相同。普通 Watch 通常是一次性的;持久 Watch 属于不同 API,客户端需按实际使用方式重新订阅或恢复。ZooKeeper 3.9.3 编程指南

ZooKeeper 的 Leader 与副本组织

Dubbo 等客户端在这些协调原语上构建注册发现能力。已有部署是否迁移,应依据容量、维护和功能缺口,不以“新项目只能用另一产品”作为结论。

Dubbo Provider、Consumer 与 ZooKeeper 的协作

2.5 Eureka:续约、复制和客户端缓存

Eureka 通过注册、续约、实例列表拉取和节点间复制维护服务视图。自我保护可在大量续约异常时减缓实例剔除,但也意味着消费者可能继续看到失效实例。

“Eureka 已停止维护”的说法不准确:早期 Eureka 2 研发项目的状态不能代表现在的 Netflix/eureka 仓库;官方仍有版本发布。评估时应检查所用版本、Spring Cloud 适配和安全更新。Eureka 官方发布记录

它没有提供与所有其他产品相同的配置管理或推送机制,这属于能力差异,不能自动推导出已有系统必须迁移。

2.6 将选型落到验收场景

检查维度 应确认的问题
冷启动 注册中心不可达时,新 Consumer 能否启动,允许什么行为
地址滞后 实例下线到调用方不再选择它,最长经历哪些环节
分区 哪些注册、查询可以继续,返回的是何时的视图
健康检查 检查进程、网络还是业务就绪;假阳性如何处理
权限 谁能注册、订阅或修改元数据,是否与业务调用权限分开
生态 客户端、BOM、服务模型和负载均衡器是否兼容
迁移 双注册、分批切换、旧地址排空与回退如何验证

CAP 可帮助理解分区下的约束,但不能替代上述操作级别的检查。一个产品内不同数据类型、读取模式也可能有不同语义。

3 Spring Cloud 框架下实战

Spring Cloud是微服务架构的一站式解决方案,我们平时构建微服务的过程中需要做的如配置管理、服务发现、负载均衡、断路器、智能路由、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作。Spring Cloud 为我们提供了一套简易的编程模型,使我们能在 Spring Boot 的基础上轻松地实现微服务项目的构建。

Spring Cloud 为服务治理做了一层抽象,为不同注册中心提供统一抽象,但迁移仍需核对服务名、元数据、健康状态、缓存和负载均衡语义。下面以当前主流的 Nacos 和 Consul 为例,演示 Spring Cloud 下的服务注册与发现实战。

3.1 Spring Cloud Nacos 实战

Nacos 作为注册中心接入 Spring Cloud 非常简便,通过 Spring Cloud Alibaba 提供的 spring-cloud-starter-alibaba-nacos-discovery 即可完成集成。

3.1.1 启动 Nacos Server

以下配置固定为历史教学基线:JDK 17、Spring Boot 3.2.4、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.0,对应 Nacos 客户端 2.3.2。它不是当前生产版本推荐;升级时整体核对兼容矩阵,不要混用“最新”组件。演示可使用隔离本机的 Nacos 2.3.2 Server,在其 bin 目录启动:

# 单机模式启动(开发环境)
sh startup.sh -m standalone

该 2.x 基线的控制台入口为 http://localhost:8848/nacos。认证行为取决于配置,不依赖默认口令;仅在隔离的本机环境演示,实际部署按对应版本配置认证。客户端还需能够访问 Nacos 的 gRPC 端口,不能只转发 HTTP 端口。

3.1.2 创建服务提供者

创建 Spring Boot 项目,提供者的 pom.xml 如下。Java 代码省略 package 与 import,各 public 类分别放入同一应用扫描包下的文件:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
    <relativePath/>
  </parent>
  <groupId>example</groupId>
  <artifactId>nacos-provider</artifactId>
  <version>1.0.0</version>
  <properties><java.version>17</java.version></properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2023.0.1</version>
        <type>pom</type><scope>import</scope>
      </dependency>
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2023.0.1.0</version>
        <type>pom</type><scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>
  <build><plugins><plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin></plugins></build>
</project>

application.yml 配置:

server:
  port: 8001
spring:
  application:
    name: nacos-provider          # 服务名称,注册到 Nacos 后其他服务通过此名称发现
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848   # Nacos Server 地址
        # 默认 public 命名空间省略 namespace,其他命名空间填 ID 而非显示名

主类无需额外注解(Spring Cloud Alibaba 会自动启用服务注册):

@SpringBootApplication
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}

提供一个简单的接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(@RequestParam("name") String name) {
        return "Hello, " + name + " from Nacos Provider!";
    }
}

启动后,在 Nacos 控制台的"服务列表"中可以看到 nacos-provider 已注册。

3.1.3 创建服务消费者

创建另一个 Spring Boot 项目作为消费者,沿用相同 BOM,引入 nacos-discovery、spring-boot-starter-web 和 spring-cloud-starter-loadbalancer。后者提供 @LoadBalanced 所需的客户端负载均衡实现。application.yml:

server:
  port: 8002
spring:
  application:
    name: nacos-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

通过 @LoadBalanced + RestTemplate 实现基于服务名的调用:

@SpringBootApplication
public class ConsumerApplication {
    @Bean
    @LoadBalanced   // 开启客户端负载均衡
    public RestTemplate restTemplate() {
        var factory = new org.springframework.http.client.SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(1000);
        factory.setReadTimeout(2000);
        return new RestTemplate(factory);
    }

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
@RestController
public class ConsumerController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/call")
    public String call() {
        // 直接使用服务名 nacos-provider 替代 IP:Port
        return restTemplate.getForObject(
            "http://nacos-provider/hello?name=Consumer", String.class);
    }
}

在两个应用和注册中心正常运行时,访问 http://localhost:8002/call,预期返回 Hello, Consumer from Nacos Provider!。整个过程中,消费者不需要知道提供者的 IP 和端口——通过 Nacos 注册中心自动发现。

3.1.4 Nacos 的额外能力

相比其他注册中心,Nacos 在 Spring Cloud 生态中还提供以下开箱即用的能力:

能力 说明
配置管理 spring-cloud-starter-alibaba-nacos-config,无需额外部署 Spring Cloud Config
命名空间隔离 通过 namespace 实现多环境(dev/staging/prod)隔离
服务分组 通过 group 实现同一命名空间内的逻辑分组
权重路由 客户端采用支持 Nacos 权重的选择策略时,可按实例权重调配;普通轮询不会自动读取并应用该权重
健康检查 按客户端版本和实例类型使用连接存活、心跳或服务端探测

3.2 Spring Cloud Consul 实战

Consul 用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更具"一站式"特征,内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 之类的)。

而Spring Cloud Consul ,是将其作为一个整体,在微服务架构中为我们的基础设施提供服务发现和服务配置的工具。

3.2.1 Consul 的优势

1、使用 Raft 算法来保证一致性, 比复杂的 Paxos 算法更直接。

2、支持多数据中心联邦,但不意味着所有目录与业务状态构成一个跨地域强一致集群;跨中心发现、容灾和网络策略需分别设计。

3、支持健康检查。

4、支持 HTTP 和 DNS 接口;etcd v3 的主要 API 基于 gRPC,不能描述成只支持 HTTP。

5、官方提供 web 管理界面, etcd 无此功能。

3.2.2 安装Consul注册中心

下面保留 Consul 1.10.4 的历史终端输出与截图,不作为当前安装版本或本次复现结果。开发模式仅用于本机实验;版本更新需核对相应兼容性。

1、官方下载64版本 :https://www.consul.io/downloads.html

2、解压后复制到目录 /usr/local/bin 下

3、启动终端,先看下啥版本的

liyifei@MacPro ~ % consul --version
Consul v1.10.4
Revision 7bbad6fe
Protocol spoken by default, understands to (agent will automatically use protocol >when speaking to compatible agents)

4、执行安装命令,可以看到他的 Client Addr 的端口为8500。所以访问 8500端口站点,http://127.0.0.1:8500/ui/dc1/services

  liyifei@MacPro ~ % consul agent -dev
  ==> Starting Consul agent...
             Version: '1.10.4'
             Node ID: '6db154b4-62ff-e67d-e745-1a7270fa1ce8'
           Node name: 'B000000147796DS'
          Datacenter: 'dc1' (Segment: '<all>')
              Server: true (Bootstrap: false)
         Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
        Cluster Addr: 127.0.0.(LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false

image_7_4.png

我们可以看到,现在没有客户端注册上来,只有一个自身的实例。

3.2.3 创建服务提供者

由于Spring Cloud Consul项目的实现,我们可以轻松的将基于Spring Boot的微服务应用注册到Consul上,并通过此实现微服务架构中的服务治理。

我们在micro-service-center下新建一个cloud项目consul-client,该项目pom文件添加如下:

下列片段放入与上文相同 Spring Boot / Spring Cloud 基线的 pom.xml,移除 Nacos discovery 依赖,保留 Web starter:

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
</dependencies>

然后修改一下application.yml的配置信息,将consul配置写入,注释应该很清楚了,如下:

spring:
  application:
    name: consul-producer # 当前服务的名称
  cloud:
    consul: # 以下为Consul注册中心的地址,如果安装的不是这个host和port,这边可以调整
      host: localhost
      port: 8500
server:
  port: 8501 # 当前服务的端口

在本文 Spring Cloud 基线中,相应 starter 自动配置可启用注册,@EnableDiscoveryClient 通常无需显式添加。注册成功仍取决于 Consul 连通性、实例配置和健康检查。

大家看到这个做法跟Eureka一样,因为Spring Cloud对服务治理做的一层抽象,所以可以屏蔽Eureka和Consul服务治理的实现细节,

统一抽象减少调用代码改动,但不能省略注册地址、健康检查、认证和失败行为的迁移验证。

@SpringBootApplication
public class ConsulClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsulClientApplication.class, args);
    }
}

默认 HTTP 健康检查访问 /actuator/health,应确认 Consul Agent 能访问它且获得健康响应;有认证或自定义路径时需要相应配置。Spring Cloud Consul 文档

修改完成之后,我们就可以把这个服务提供者启动了,然后再去注册中心查看服务的注册情况,就可以看到被注册进来的Provider(consul-producer):

image_7_5.png

3.2.4 生态对接 Spring Cloud

image_6_14.png

Consul作为注册中心,集成在Spring Cloud生态。可以看出,跟Eureka对接到Spring Cloud 生态的过程很像。

但是这边的健康检查更丰富,可以有多种不同的的Check方式:

  • Script check(Script+ Interval)
  • 基于HTTP请求
  • 基于tcp请求
  • 基于grpc请求

4 怎样验证接入成功

注册列表中出现名字只是第一步。应继续验证提供者健康状态、消费者实际调用、启动第二个实例后的选择行为,以及停止一个实例后地址更新和在途请求的结果。注册中心暂时不可达时,分别检查已有消费者和新启动消费者,不能把缓存命中当成注册中心仍然可用。

5 总结

无论何种注册中心技术,本质上都是为了解决微服务中的如下问题:

解耦服务之间相互依赖的细节

我们知道服务之间的远程调用必须要知道对方的IP、端口信息。我们可以在调用方直接配置被调用方的IP、端口,这种调用方直接依赖IP、端口的方式存在明显的问题,如被调用的IP、端口变化后,调用方法也要同步修改。

通过服务发现,将服务之间IP与端口的依赖转化为服务名的依赖,服务名可以根据具微服务业务来做标识,因此,屏蔽、解耦服务之间的依赖细节是服务发现与注册解决的第一个问题。

对微服务进行动态管理

在微服务架构中,服务众多,服务之间的相互依赖也错综复杂,无论是服务主动停止,意外挂掉,还是因为流量增加对服务实现进行扩容,这些服务数据或状态上的动态变化,都需要尽快的通知到被调用方,被调用方才采取相应的措施。因此,对于服务注册与发现要实时管理者服务的数据与状态,包括服务的注册上线、服务主动下线,异常服务的剔除。

评论