Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added notes/pics/feign-chain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
176 changes: 164 additions & 12 deletions notes/微服务.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
* [微服务](#微服务)
* [一、从单体到微服务](#一从单体到微服务)
* [服务拆分](#服务拆分)
* [二、组件](#二组件)
* [二、Spring Cloud](#二spring-cloud)
* [组件](#组件)
* [接口与实现](#接口与实现)
* [版本](#版本)
* [三、注册中心](#三注册中心)
* [四、服务调用](#四服务调用)
* [OpenFeign](#openfeign)
* [负载均衡策略](#负载均衡策略)
* [实例下线后多久能感知](#实例下线后多久能感知)
* [注册中心宕机](#注册中心宕机)
* [五、网关](#五网关)
Expand Down Expand Up @@ -46,24 +50,116 @@

拆分已有的单体时通常逐步进行:先拆变化快、压力大的模块,新功能直接写在新服务中,网关把对应的请求转发给新服务,旧功能再一点点迁出。数据库也是先理清每个模块用到哪些表,消除跨模块的 JOIN 和事务,最后再分库。

## 二、组件
## 二、Spring Cloud

服务拆分之后,要解决服务在哪里、怎么调用、配置怎么管理等问题。Spring Cloud 为这些问题定义了统一的接口,Spring Cloud Alibaba 等项目提供具体的实现:
服务拆分之后,每个服务都要解决同样的问题:去哪里找其它服务的实例、选择哪个实例、配置放在哪里、下游故障时怎么办。Spring Cloud 是一组基于 Spring Boot 的项目,把这些问题抽象成统一的接口和配置方式,具体的实现由不同的项目提供。

| 要解决的问题 | 组件 | 其它常见选择 |
| --- | --- | --- |
| 服务的实例在哪里 | 注册中心:Nacos | Eureka、ZooKeeper、Consul |
| 配置怎么集中管理和动态修改 | 配置中心:Nacos | Apollo |
| 怎么调用其它服务 | OpenFeign + Spring Cloud LoadBalancer | Dubbo、gRPC |
| 外部请求从哪里进入 | 网关:Spring Cloud Gateway | Kong |
| 下游故障时怎么保护自己 | Sentinel | Resilience4j |
| 跨服务的事务 | Seata | 本地消息表、事务消息 |
| 一个请求经过了哪些服务 | 链路追踪:Micrometer Tracing | SkyWalking、Zipkin |
Spring Cloud 与 Spring Boot 的关系:Spring Boot 用来快速构建单个服务,Spring Cloud 处理多个服务之间的协作。每个 Spring Cloud 组件都是一个 starter,依靠 Spring Boot 的 [自动配置](Spring.md#四spring-boot-自动配置) 生效。例如引入 spring-cloud-starter-alibaba-nacos-discovery 后,它的 jar 包中列出了 NacosDiscoveryAutoConfiguration、NacosServiceRegistryAutoConfiguration 等 10 个自动配置类,应用启动时就会自动注册到 Nacos,不需要写任何代码。

### 组件

| 要解决的问题 | Spring Cloud 中的项目 | Spring Cloud Alibaba | Netflix 的早期组件 |
| --- | --- | --- | --- |
| 服务的实例在哪里 | DiscoveryClient 接口 | Nacos | Eureka |
| 选择哪个实例 | Spring Cloud LoadBalancer | Nacos 的权重负载均衡 | Ribbon |
| 怎么调用其它服务 | OpenFeign | — | — |
| 配置的集中管理和动态修改 | Spring Cloud Config | Nacos | Archaius |
| 外部请求从哪里进入 | Spring Cloud Gateway | — | Zuul |
| 下游故障时怎么保护自己 | Spring Cloud Circuit Breaker(Resilience4j) | Sentinel | Hystrix |
| 跨服务的事务 | — | Seata | — |
| 一个请求经过了哪些服务 | Micrometer Tracing | — | — |

早期的 Spring Cloud 项目大多使用 Netflix 的组件。其中 Ribbon、Hystrix、Zuul 1、Archaius 1 在 2018 年进入维护模式,在 2020.0 版本中被移除,分别由 Spring Cloud LoadBalancer、Resilience4j、Spring Cloud Gateway 和 Spring Boot 的外部配置加 Spring Cloud Config 取代;Eureka 不在其中,仍在维护。链路追踪原来的 Spring Cloud Sleuth 不支持 Spring Boot 3,核心部分已经移到了 Micrometer Tracing。

国内常用的组合是 Spring Cloud Alibaba:Nacos 同时作为注册中心和配置中心,Sentinel 负责限流和熔断,Seata 负责分布式事务,服务调用和网关仍然使用 Spring Cloud 自己的 OpenFeign 和 Gateway。

Spring Cloud 与 Dubbo 的区别:Spring Cloud 是覆盖服务发现、配置、网关、熔断的一整套组件,服务之间默认通过 HTTP 调用;Dubbo 从 RPC 框架发展而来,重点是高性能的服务调用和服务治理。两者并不互斥,Dubbo 也可以使用 Nacos 作为注册中心。

下面的例子都在本机实际运行:网关、订单服务各一个实例,库存服务两个实例,订单服务通过 OpenFeign 调用库存服务扣减库存。

<div align="center"> <img src="pics/microservice-overview.png" width="740px"/> </div><br>

### 接口与实现

Spring Cloud Commons 定义了服务发现、负载均衡等接口,例如 DiscoveryClient 表示“按服务名查询实例”。业务代码只依赖这个接口:

```java
@RestController
class InstanceController {
private final DiscoveryClient discoveryClient; // Spring Cloud Commons 中的接口

InstanceController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}

@GetMapping("/instances")
List<String> instances() {
return discoveryClient.getInstances("inventory-service").stream()
.map(i -> i.getHost() + ":" + i.getPort())
.toList();
}
}
```

引入了 Nacos 的 starter 时,注入的 DiscoveryClient 是 CompositeDiscoveryClient,它依次询问 NacosDiscoveryClient 和 Spring Cloud 自带的 SimpleDiscoveryClient,/instances 返回 ["127.0.0.1:8081","127.0.0.1:8082"]。

接着关闭 Nacos 的服务发现,在配置文件中写死实例的地址:

```yaml
spring:
cloud:
nacos:
discovery:
enabled: false
discovery:
client:
simple:
instances:
inventory-service:
- uri: http://127.0.0.1:8081
```

代码不用修改,/instances 返回 ["127.0.0.1:8081"],OpenFeign 的调用也全部落到了 8081。换成 Consul、Eureka 等注册中心也是同样的道理:替换依赖和配置,业务代码不变。

### 版本

Spring Cloud 以发布列车(release train)的方式发版,版本号由年份和序号组成,每个版本还有一个代号,例如 2023.0 的代号是 Leyton。每个版本只兼容特定的 Spring Boot 版本:

| Spring Cloud | Spring Boot |
| --- | --- |
| 2025.0(Northfields) | 3.5 |
| 2024.0(Moorgate) | 3.4 |
| 2023.0(Leyton) | 3.2、3.3 |

Spring Cloud Alibaba 的版本号前缀与 Spring Cloud 一致,例如 2023.0.3.4 对应 Spring Cloud 2023.0。为了避免组件之间的版本不匹配,通常在父工程中导入 Spring Cloud 和 Spring Cloud Alibaba 的 BOM,子模块引入依赖时不再写版本号。本文的例子使用的组合如下,版本的对应关系以官方最新的说明为准:

```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.3.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```

## 三、注册中心

注册中心的基本原理见 [服务发现](分布式.md#服务发现)。库存服务启动后,Nacos 中记录的实例信息如下(省略了部分字段):
Expand All @@ -80,6 +176,8 @@ weight 用于按权重分配流量;metadata 可以存放版本号等自定义

Nacos 2.x 的客户端与服务端之间保持一个 gRPC 长连接,端口为主端口加 1000(默认 9848)。临时实例依附于这个连接,连接断开,服务端就删除这个实例。持久化实例保存在服务端,客户端断开后不会被删除,只会被标记为不健康,通常由运维通过控制台或者接口维护。

Nacos 与 Eureka 的区别:Eureka 的客户端每 30 秒发送一次心跳,服务端 90 秒没有收到心跳才删除实例,客户端也是每 30 秒拉取一次实例列表的变化;Nacos 2.x 依靠长连接发现客户端断开,并把实例的变化推送给订阅者,本文实测进程退出后 0.6 秒就删除了实例。另外 Eureka 只是注册中心,Nacos 同时也是配置中心。

Nacos 与 ZooKeeper 的区别:Nacos 的临时实例使用自研的 Distro 协议,每个节点都保存全部数据,网络分区时仍然可以返回实例列表,属于 AP;ZooKeeper 是 CP 的,只有多数节点可用时才能提供服务。服务发现可以容忍短暂的数据不一致,却不能接受注册中心不可用,所以更适合 AP。

## 四、服务调用
Expand All @@ -106,6 +204,16 @@ Map<String, Object> result = inventoryClient.deduct(10086, 2);
2. Spring Cloud LoadBalancer 从 inventory-service 的实例列表中选出一个实例,默认轮询,连续下单 6 次依次落在 8082、8081、8082、8081、8082、8081;
3. 把请求发给选中的实例,再把响应的 JSON 转换为返回值。

@FeignClient 注解的接口没有实现类,却能直接注入使用,过程如下:

- 启动时,@EnableFeignClients 导入 FeignClientsRegistrar,它扫描所有带 @FeignClient 的接口,为每个接口注册一个 FeignClientFactoryBean 类型的 Bean 定义;
- 创建 Bean 时,FeignClientFactoryBean 把服务名拼成 http://inventory-service,用 JDK 动态代理生成接口的实现,也就是上面的 $Proxy88;
- 调用时,代理把方法和参数转换成 HTTP 请求,交给 FeignBlockingLoadBalancerClient,它通过 Spring Cloud LoadBalancer 选出一个实例,把 URL 中的服务名换成实例的地址后再发送。

<div align="center"> <img src="pics/feign-chain.png" width="740px"/> </div><br>

下文实例下线实验中 35 秒的延迟,就来自图中 CachingServiceInstanceListSupplier 的缓存。

原理与 [RPC 的调用过程](分布式.md#调用过程) 相同,只是 OpenFeign 使用 HTTP + JSON,而 Dubbo 默认使用 TCP 上的二进制协议。Spring Cloud 项目常用 OpenFeign,因为它的注解与 Spring MVC 一致,被调用的就是普通的 HTTP 接口;调用量大、对延迟敏感的内部服务可以选择 Dubbo 或者 gRPC,原因见 [RPC 与 HTTP](分布式.md#rpc-与-http)。

服务之间不通过 Nginx 转发,是因为 Nginx 是集中式的代理:每次调用都要多经过一跳,Nginx 本身也要做高可用,实例变化时还要更新它的配置。客户端负载均衡由调用方直接从注册中心获取实例列表并选择实例,没有中间这一跳。
Expand All @@ -123,6 +231,41 @@ spring:
read-timeout: 2000
```

### 负载均衡策略

Spring Cloud LoadBalancer 默认使用 RoundRobinLoadBalancer 轮询,还提供 RandomLoadBalancer 随机选择。早期的 Ribbon 通过 IRule 接口配置策略,已经在 2020.0 版本中移除。

轮询不考虑机器之间的差异。例如 8081 所在的机器配置更高,启动时用 spring.cloud.nacos.discovery.weight 把它在 Nacos 中的权重设为 3,8082 保持 1,再各调用 400 次:

| 负载均衡 | 8081(权重 3) | 8082(权重 1) |
| --- | --- | --- |
| 默认的 RoundRobinLoadBalancer | 200 | 200 |
| RandomLoadBalancer | 203 | 197 |
| NacosLoadBalancer | 303 | 97 |

默认的轮询和随机都不读取 Nacos 中的权重。把 spring.cloud.loadbalancer.nacos.enabled 设为 true 后,Spring Cloud Alibaba 用 NacosLoadBalancer 替换默认的实现,它优先选择与调用方在同一个集群(cluster-name)中的实例,再按权重随机选择。

为某个服务指定其它策略时,用 @LoadBalancerClient 引用一个配置类:

```java
@Configuration
@LoadBalancerClient(name = "inventory-service", configuration = RandomConfiguration.class)
class LoadBalancerConfig {
}

// 不加 @Configuration,或者放在组件扫描的范围之外,这样只对 inventory-service 生效
public class RandomConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory factory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
```

需要自定义规则时,例如只选择元数据中 version 为 v2 的实例,可以参照 RandomLoadBalancer 实现 ReactorServiceInstanceLoadBalancer 接口,再用同样的方式注册。

### 实例下线后多久能感知

注册中心删除实例之后,调用方并不会立刻停止调用它。下面让订单服务每 0.2 秒调用一次库存服务,再用三种方式停掉 8082:
Expand Down Expand Up @@ -219,6 +362,8 @@ class OrderController {

在 Nacos 中把 order.max-items 从 5 改为 8,306 ms 后订单服务就读到了新值,不需要重启。配置变化后 Nacos 主动通知客户端,而不是等客户端定时来查询;@RefreshScope 的 Bean 是一个代理,配置刷新时缓存的实例被丢弃,下次调用时用新的配置重新创建。

Spring Cloud 2020.0 起默认不再加载 bootstrap.yml,配置中心的配置改为通过上面的 spring.config.import 导入。

客户端还会在本地保存配置的快照(~/nacos/config 目录下)。Nacos 不可用时,实测新启动的订单服务从快照中读到了 max-items: 8。

## 七、服务保护与分布式事务
Expand Down Expand Up @@ -264,6 +409,13 @@ traceparent 是 W3C Trace Context 规定的格式,四段依次是版本、trac

## 参考资料

- [Spring Cloud](https://spring.io/projects/spring-cloud)
- [Spring Cloud Greenwich.RC1 Available Now](https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now)
- [Spring Cloud 2020.0 Release Notes](https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2020.0-Release-Notes)
- [Spring Cloud Sleuth](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/)
- [Spring Cloud Circuit Breaker](https://docs.spring.io/spring-cloud-circuitbreaker/reference/)
- [Spring Cloud Alibaba 版本说明](https://sca.aliyun.com/en/docs/2023/overview/version-explain/)
- [Understanding Eureka Client Server Communication](https://github.com/Netflix/eureka/wiki/Understanding-eureka-client-server-communication)
- [Nacos: Instance Lifecycle](https://nacos.io/en/docs/latest/manual/user/naming/instance-lifecycle/)
- [Nacos 2.0 兼容性文档](https://nacos.io/docs/latest/upgrading/200-compatibility/)
- [Nacos 的 Distro 一致性协议](https://nacos.io/en/news/article-nacos-distro-mechanism/)
Expand Down
Loading
Loading