diff --git a/notes/pics/feign-chain.png b/notes/pics/feign-chain.png
new file mode 100644
index 00000000..23969450
Binary files /dev/null and b/notes/pics/feign-chain.png differ
diff --git a/notes/微服务.md b/notes/微服务.md
index a26d10c9..48af9cae 100644
--- a/notes/微服务.md
+++ b/notes/微服务.md
@@ -3,10 +3,14 @@
* [微服务](#微服务)
* [一、从单体到微服务](#一从单体到微服务)
* [服务拆分](#服务拆分)
- * [二、组件](#二组件)
+ * [二、Spring Cloud](#二spring-cloud)
+ * [组件](#组件)
+ * [接口与实现](#接口与实现)
+ * [版本](#版本)
* [三、注册中心](#三注册中心)
* [四、服务调用](#四服务调用)
* [OpenFeign](#openfeign)
+ * [负载均衡策略](#负载均衡策略)
* [实例下线后多久能感知](#实例下线后多久能感知)
* [注册中心宕机](#注册中心宕机)
* [五、网关](#五网关)
@@ -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 调用库存服务扣减库存。
+### 接口与实现
+
+Spring Cloud Commons 定义了服务发现、负载均衡等接口,例如 DiscoveryClient 表示“按服务名查询实例”。业务代码只依赖这个接口:
+
+```java
+@RestController
+class InstanceController {
+ private final DiscoveryClient discoveryClient; // Spring Cloud Commons 中的接口
+
+ InstanceController(DiscoveryClient discoveryClient) {
+ this.discoveryClient = discoveryClient;
+ }
+
+ @GetMapping("/instances")
+ List 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
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ 2023.0.3
+ pom
+ import
+
+
+ com.alibaba.cloud
+ spring-cloud-alibaba-dependencies
+ 2023.0.3.4
+ pom
+ import
+
+
+
+```
+
## 三、注册中心
注册中心的基本原理见 [服务发现](分布式.md#服务发现)。库存服务启动后,Nacos 中记录的实例信息如下(省略了部分字段):
@@ -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。
## 四、服务调用
@@ -106,6 +204,16 @@ Map 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 中的服务名换成实例的地址后再发送。
+
+
+
+下文实例下线实验中 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 本身也要做高可用,实例变化时还要更新它的配置。客户端负载均衡由调用方直接从注册中心获取实例列表并选择实例,没有中间这一跳。
@@ -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 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:
@@ -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。
## 七、服务保护与分布式事务
@@ -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/)
diff --git a/tools/diagram/src/feign-chain.svg b/tools/diagram/src/feign-chain.svg
new file mode 100644
index 00000000..04c2bfb5
--- /dev/null
+++ b/tools/diagram/src/feign-chain.svg
@@ -0,0 +1,103 @@
+