diff --git a/notes/pics/feign-chain.png b/notes/pics/feign-chain.png new file mode 100644 index 000000000..239694507 Binary files /dev/null and b/notes/pics/feign-chain.png differ diff --git "a/notes/\345\276\256\346\234\215\345\212\241.md" "b/notes/\345\276\256\346\234\215\345\212\241.md" index a26d10c9c..48af9caee 100644 --- "a/notes/\345\276\256\346\234\215\345\212\241.md" +++ "b/notes/\345\276\256\346\234\215\345\212\241.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 000000000..04c2bfb55 --- /dev/null +++ b/tools/diagram/src/feign-chain.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + inventoryClient.deduct(10086, 2) 经过的对象 + + + + + + inventoryClient.deduct(10086, 2) + + 代理 $Proxy88(启动时由 + FeignClientFactoryBean 创建) + + + POST http://inventory-service/… + 此时还是服务名 + + FeignBlockingLoadBalancerClient + 交给负载均衡 + + RoundRobinLoadBalancer 轮询选择 + + + + + + + + + + + + + 8081 ✓ + 8082 + + reconstructURI:把服务名换成实例地址 + + + POST http://127.0.0.1:8081/… + + + + + + + + + + + 库存服务 8081 + + + Nacos 注册中心 + + 127.0.0.1:8081 + + 127.0.0.1:8082 + NacosDiscoveryClient + + + DiscoveryClient 接口 + CompositeDiscoveryClient + + + + + 缓存实例列表 35 秒 + CachingServiceInstanceListSupplier + + 实例列表 + 左列是请求的变化,右列是实例列表的来源;类名都是运行时实际打印出来的。 +