Add 微服务.md

新增微服务笔记,包括服务拆分、注册中心、OpenFeign 与实例下线的感知、网关、配置中心、服务保护与分布式事务、链路追踪和灰度发布,并配图。
This commit is contained in:
CyC2018
2026-10-26 03:43:23 +08:00
parent c7d176e037
commit 145a775aad
5 changed files with 504 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

+278
View File
@@ -0,0 +1,278 @@
# 微服务
<!-- GFM-TOC -->
* [微服务](#微服务)
* [一、从单体到微服务](#一从单体到微服务)
* [服务拆分](#服务拆分)
* [二、组件](#二组件)
* [三、注册中心](#三注册中心)
* [四、服务调用](#四服务调用)
* [OpenFeign](#openfeign)
* [实例下线后多久能感知](#实例下线后多久能感知)
* [注册中心宕机](#注册中心宕机)
* [五、网关](#五网关)
* [六、配置中心](#六配置中心)
* [七、服务保护与分布式事务](#七服务保护与分布式事务)
* [八、链路追踪](#八链路追踪)
* [九、灰度发布](#九灰度发布)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 一、从单体到微服务
一个商城最初通常是单体应用:用户、商品、库存、订单、支付的代码在同一个工程中,部署成一个进程,共用一个数据库。微服务指的是把系统按业务拆成多个独立部署的服务,每个服务有自己的数据库,服务之间通过网络调用。例如下单时,订单服务要通过网络调用库存服务扣减库存,而不是调用同一个进程中的方法。
拆分带来的好处:
- 独立部署:修改库存服务只需重新发布库存服务;
- 独立扩容:大促时只给订单服务和库存服务增加实例;
- 故障隔离:支付服务出问题时,用户仍然可以浏览商品;
- 团队独立:每个团队负责几个服务,可以各自开发和发布。
代价同样明显:
- 方法调用变成了网络调用,可能超时和失败,需要处理超时、重试和熔断;
- 一个数据库事务变成了跨服务的分布式事务;
- 一个请求经过多个服务,排查问题需要链路追踪;
- 服务和实例的数量增加,部署和监控的成本更高。
因此团队规模小、业务还在快速变化时,单体应用加上清晰的模块划分往往更合适;当各个模块的发布互相阻塞,或者某些模块需要单独扩容时,再考虑拆分。
### 服务拆分
拆分的依据是业务边界,而不是技术上的分层。判断方法是看数据:经常被一起修改、需要在同一个事务中保证一致的数据,应该放在同一个服务中。例如订单和订单明细放在订单服务,库存放在库存服务。
拆分之后每个服务只能访问自己的数据库,订单服务不能再用 JOIN 查询用户表中的用户名。常见的做法有两种:调用用户服务的接口查询;或者在下单时把用户名、商品名称和价格作为快照保存到订单表中,商品之后改名、改价,订单中记录的仍然是下单时的信息。
拆分已有的单体时通常逐步进行:先拆变化快、压力大的模块,新功能直接写在新服务中,网关把对应的请求转发给新服务,旧功能再一点点迁出。数据库也是先理清每个模块用到哪些表,消除跨模块的 JOIN 和事务,最后再分库。
## 二、组件
服务拆分之后,要解决服务在哪里、怎么调用、配置怎么管理等问题。Spring Cloud 为这些问题定义了统一的接口,Spring Cloud Alibaba 等项目提供具体的实现:
| 要解决的问题 | 组件 | 其它常见选择 |
| --- | --- | --- |
| 服务的实例在哪里 | 注册中心:Nacos | Eureka、ZooKeeper、Consul |
| 配置怎么集中管理和动态修改 | 配置中心:Nacos | Apollo |
| 怎么调用其它服务 | OpenFeign + Spring Cloud LoadBalancer | Dubbo、gRPC |
| 外部请求从哪里进入 | 网关:Spring Cloud Gateway | Kong |
| 下游故障时怎么保护自己 | Sentinel | Resilience4j |
| 跨服务的事务 | Seata | 本地消息表、事务消息 |
| 一个请求经过了哪些服务 | 链路追踪:Micrometer Tracing | SkyWalking、Zipkin |
下面的例子都在本机实际运行:网关、订单服务各一个实例,库存服务两个实例,订单服务通过 OpenFeign 调用库存服务扣减库存。
<div align="center"> <img src="pics/microservice-overview.png" width="740px"/> </div><br>
## 三、注册中心
注册中心的基本原理见 [服务发现](分布式.md#服务发现)。库存服务启动后,Nacos 中记录的实例信息如下(省略了部分字段):
```json
{
"ip": "127.0.0.1", "port": 8081,
"weight": 1.0, "healthy": true, "enabled": true, "ephemeral": true,
"metadata": {"preserved.register.source": "SPRING_CLOUD"}
}
```
weight 用于按权重分配流量;metadata 可以存放版本号等自定义信息,灰度发布时会用到;ephemeral 表示临时实例,Spring Cloud 应用默认注册为临时实例。
Nacos 2.x 的客户端与服务端之间保持一个 gRPC 长连接,端口为主端口加 1000(默认 9848)。临时实例依附于这个连接,连接断开,服务端就删除这个实例。持久化实例保存在服务端,客户端断开后不会被删除,只会被标记为不健康,通常由运维通过控制台或者接口维护。
Nacos 与 ZooKeeper 的区别:Nacos 的临时实例使用自研的 Distro 协议,每个节点都保存全部数据,网络分区时仍然可以返回实例列表,属于 AP;ZooKeeper 是 CP 的,只有多数节点可用时才能提供服务。服务发现可以容忍短暂的数据不一致,却不能接受注册中心不可用,所以更适合 AP。
## 四、服务调用
### OpenFeign
订单服务调用库存服务时,只需声明一个接口:
```java
@FeignClient(name = "inventory-service")
interface InventoryClient {
@PostMapping("/inventory/deduct")
Map<String, Object> deduct(@RequestParam("skuId") long skuId, @RequestParam("count") int count);
}
```
```java
Map<String, Object> result = inventoryClient.deduct(10086, 2);
```
注入的 inventoryClient 是 JDK 动态代理生成的对象,运行时打印它的类名为 $Proxy88。调用 deduct() 时:
1. 代理根据注解拼出 HTTP 请求 POST /inventory/deduct?skuId=10086&count=2;
2. Spring Cloud LoadBalancer 从 inventory-service 的实例列表中选出一个实例,默认轮询,连续下单 6 次依次落在 8082、8081、8082、8081、8082、8081;
3. 把请求发给选中的实例,再把响应的 JSON 转换为返回值。
原理与 [RPC 的调用过程](分布式.md#调用过程) 相同,只是 OpenFeign 使用 HTTP + JSON,而 Dubbo 默认使用 TCP 上的二进制协议。Spring Cloud 项目常用 OpenFeign,因为它的注解与 Spring MVC 一致,被调用的就是普通的 HTTP 接口;调用量大、对延迟敏感的内部服务可以选择 Dubbo 或者 gRPC,原因见 [RPC 与 HTTP](分布式.md#rpc-与-http)。
服务之间不通过 Nginx 转发,是因为 Nginx 是集中式的代理:每次调用都要多经过一跳,Nginx 本身也要做高可用,实例变化时还要更新它的配置。客户端负载均衡由调用方直接从注册中心获取实例列表并选择实例,没有中间这一跳。
需要注意的是,OpenFeign 默认的连接超时为 10 秒、读取超时为 60 秒。下游实例卡住时,实测订单服务要等 60 秒才失败,期间一直占用一个线程,所以要按接口的实际耗时设置超时:
```yaml
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 500
read-timeout: 2000
```
### 实例下线后多久能感知
注册中心删除实例之后,调用方并不会立刻停止调用它。下面让订单服务每 0.2 秒调用一次库存服务,再用三种方式停掉 8082:
<div align="center"> <img src="pics/instance-down.png" width="740px"/> </div><br>
| 停止方式 | Nacos 删除实例 | 默认配置 | 关闭负载均衡缓存,读取超时 2 秒 |
| --- | --- | --- | --- |
| kill -9,进程退出 | 0.6 s | 35 s 内约一半调用失败,共 83 次 | 失败 1 次 |
| 正常关闭 | 0.6 s | 33 s 内失败 77 次 | 失败 1 次 |
| kill -STOP,进程卡住 | 23 s | 发往它的调用要等 60 s 才失败 | 每次 2 s 后失败,约 23 s 后不再调用 |
原因有两个:
- 负载均衡缓存:Spring Cloud LoadBalancer 默认把实例列表缓存 35 秒,Nacos 已经删除了实例,调用方在缓存过期前仍然会选中它。Nacos 客户端会接收服务端推送的实例变化并保存在本地,所以可以关闭这层缓存(spring.cloud.loadbalancer.cache.enabled 设为 false),Spring Cloud 的文档也建议在服务发现客户端已有缓存时关闭,避免重复缓存;
- 连接没有断开:进程退出或者正常关闭时连接随之断开,Nacos 立刻删除实例;进程卡住、机器宕机或者网络中断时连接并没有断开,Nacos 要等健康检查超时才能发现,本机实测为 23 秒。这段时间只能靠调用方的超时和熔断尽快放弃这个实例。
### 注册中心宕机
停掉 Nacos 后:
- 已经运行的订单服务仍然可以调用库存服务,因为实例列表已经缓存在本地;
- 新启动的订单服务默认无法启动:spring.cloud.nacos.discovery.fail-fast 默认为 true,注册失败时直接报错退出。把它设为 false 后可以启动,配置也能从本地快照读取,但调用库存服务时报错 No servers available for service: inventory-service,因为新进程从没有拿到过实例列表。
所以注册中心宕机不会立刻影响线上的调用,但扩容和发布都无法进行,生产环境中 Nacos 要以集群方式部署。
## 五、网关
外部请求不应该直接访问各个服务:客户端要知道每个服务的地址,每个服务也都要各自做鉴权、限流。网关是所有外部请求的统一入口,负责:
- 路由:按路径把请求转发给对应的服务,例如把 /api/orders/** 转发给订单服务;
- 鉴权:校验令牌,拒绝未登录的请求;
- 限流、日志、跨域等每个服务都需要的处理。
Spring Cloud Gateway 的路由配置:
```yaml
spring:
cloud:
gateway:
routes:
- id: order
uri: lb://order-service # 从注册中心获取 order-service 的实例并负载均衡
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1 # 去掉 /api,转发为 /orders/**
```
鉴权通常放在网关:网关校验令牌(例如 [JWT](HTTP.md#11-jwt)),取出用户 ID 放进请求头,下游服务直接使用这个请求头,不再重复校验:
```java
@Bean
GlobalFilter auth() {
return (exchange, chain) -> {
String userId = verify(exchange.getRequest().getHeaders().getFirst("Authorization"));
if (userId == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange.mutate()
.request(r -> r.headers(h -> h.set("X-User-Id", userId))).build());
};
}
```
实测不带令牌或者令牌错误时,网关返回 401;带上正确的令牌时,订单服务收到 X-User-Id: 1024。需要注意两点:
- 用 set 而不是 add:客户端自己带上 X-User-Id: 1 时会被覆盖,实测订单服务收到的仍然是 1024;
- 下游服务信任网关传来的请求头,所以这些服务本身不能对外暴露,只能通过网关访问。
网关与 Nginx 并不冲突:Nginx 通常在最外层负责 TLS 和多个网关实例之间的负载均衡,网关在它后面按服务路由,并处理鉴权这类与业务相关的逻辑。
## 六、配置中心
每个服务有多个实例,如果配置写在各自的配置文件中,修改一个参数就要改所有实例并重启。配置中心集中保存配置,修改后推送给运行中的实例。
订单服务从 Nacos 读取名为 order-service.yaml 的配置:
```yaml
spring:
config:
import: optional:nacos:order-service.yaml
```
```java
@RestController
@RefreshScope
class OrderController {
@Value("${order.max-items:10}")
int maxItems;
}
```
在 Nacos 中把 order.max-items 从 5 改为 8,306 ms 后订单服务就读到了新值,不需要重启。配置变化后 Nacos 主动通知客户端,而不是等客户端定时来查询;@RefreshScope 的 Bean 是一个代理,配置刷新时缓存的实例被丢弃,下次调用时用新的配置重新创建。
客户端还会在本地保存配置的快照(~/nacos/config 目录下)。Nacos 不可用时,实测新启动的订单服务从快照中读到了 max-items: 8。
## 七、服务保护与分布式事务
服务之间调用越多,一个服务变慢就越容易拖垮调用它的服务:上面卡住的库存实例让每次调用等待 60 秒,订单服务的线程会被逐渐占满。限流、降级和熔断的原理见 [限流、降级与熔断](系统设计基础.md#限流降级与熔断),Spring Cloud Alibaba 中常用 Sentinel 实现:把接口或者一段代码定义为资源,再为资源配置规则。熔断规则可以按慢调用比例、异常比例或者异常数触发,熔断时长过后进入半开状态,放行一个请求试探,成功就结束熔断,失败就继续熔断。
跨服务的数据一致性见 [分布式事务](分布式.md#二分布式事务)。Seata 的 AT 模式对业务代码的改动最少,以下单时订单服务创建订单、库存服务扣减库存为例:
1. 第一阶段:每个服务执行自己的 SQL,并在同一个本地事务中写入 undo_log,记录被修改的行修改前后的值,然后提交本地事务。提交前要先获得这些行的全局锁,防止其它全局事务同时修改它们;
2. 全局事务成功时,第二阶段异步删除 undo_log;
3. 全局事务失败时,第二阶段按 undo_log 中修改前的值生成反向的 SQL,例如库存服务把扣掉的库存加回去。
## 八、链路追踪
一个下单请求依次经过网关、订单服务和库存服务,出错时需要知道是哪一步出了错、每一步耗时多少。链路追踪为每个请求分配一个 traceId,调用下游时通过请求头传递,各个服务打印日志和上报耗时的时候都带上它。
在上面的服务中加入 Micrometer Tracing 后,同一次下单在订单服务和库存服务中打印的日志如下,方括号中依次是 traceId 和 spanId:
```text
[order-service] [6ade5947250103822ff3f13a5baab74b-2763eeac981ce4a4] 创建订单 userId=1024
[inventory-service] [6ade5947250103822ff3f13a5baab74b-92a95370047818fb] 扣减库存 skuId=10086
```
库存服务收到的请求头为:
```text
traceparent: 00-6ade5947250103822ff3f13a5baab74b-64990a2774676e11-01
```
traceparent 是 W3C Trace Context 规定的格式,四段依次是版本、traceId、调用方这次请求的 spanId(这里是订单服务发起 HTTP 调用的 span)和采样标记。每个服务处理请求时生成自己的 spanId,并记下父 spanId,这些 span 上报到 Zipkin、SkyWalking 等系统后,就能按 traceId 还原出完整的调用树和每一段的耗时。用 traceId 在日志系统中搜索,也能找到这个请求在所有服务中的日志。
## 九、灰度发布
新版本上线时,常用的发布方式:
- 滚动发布:逐个替换旧的实例,占用的资源少,但一段时间内新旧版本同时运行;
- 蓝绿发布:准备一整套新环境,验证后把流量一次切过去,回滚时切回即可,但需要双倍的资源;
- 灰度发布(金丝雀发布):先让一小部分流量进入新版本,观察错误率和耗时,没有问题再逐步扩大比例。
灰度发布可以借助注册中心和网关实现:新版本的实例注册时带上元数据 version=v2,或者注册为单独的服务名,网关按比例(Spring Cloud Gateway 的 Weight 断言)或者按用户、请求头把请求转发给新版本。发现问题时把新版本的比例调回 0 即可回滚。
需要注意的是,新旧版本会同时访问同一个数据库,表结构的修改必须同时兼容两个版本。例如先只增加新列,等旧版本全部下线后,再删除不再使用的旧列。
## 参考资料
- [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/)
- [Nacos Java SDK](https://nacos.io/docs/latest/manual/user/java-sdk/usage/)
- [ZooKeeper: Overview](https://zookeeper.apache.org/doc/current/zookeeperOver.html)
- [Spring Cloud Commons: Spring Cloud LoadBalancer](https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html)
- [Spring Cloud Commons: Refresh Scope](https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/application-context-services.html)
- [Spring Cloud OpenFeign](https://docs.spring.io/spring-cloud-openfeign/reference/)
- [Spring Cloud Gateway: Route Predicate Factories](https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webflux/request-predicates-factories.html)
- [Sentinel 熔断降级](https://sentinelguard.io/zh-cn/docs/circuit-breaking.html)
- [Seata AT 模式](https://seata.apache.org/docs/dev/mode/at-mode/)
- [W3C Trace Context](https://www.w3.org/TR/trace-context/)
+153
View File
@@ -0,0 +1,153 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="336" viewBox="0 0 740 336">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="370" y="28" class="title">停掉库存实例 8082 后,订单服务调用它的情况</text>
<line x1="250.0" y1="56" x2="250.0" y2="270" stroke="#EEE"/>
<text x="250" y="286" class="c sub">0 s</text>
<line x1="367.5" y1="56" x2="367.5" y2="270" stroke="#EEE"/>
<text x="367.5" y="286" class="c sub">10 s</text>
<line x1="485.0" y1="56" x2="485.0" y2="270" stroke="#EEE"/>
<text x="485" y="286" class="c sub">20 s</text>
<line x1="602.5" y1="56" x2="602.5" y2="270" stroke="#EEE"/>
<text x="602.5" y="286" class="c sub">30 s</text>
<line x1="720.0" y1="56" x2="720.0" y2="270" stroke="#EEE"/>
<text x="720" y="286" class="c sub">40 s</text>
<text x="20" y="86" class="b">kill -9,默认配置</text>
<rect x="250" y="74" width="470" height="18" rx="3" fill="#F5F5F5"/>
<rect x="250.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="255.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="260.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="265.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="271.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="276.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="281.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="287.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="292.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="297.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="302.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="308.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="313.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="318.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="324.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="329.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="334.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="339.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="345.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="350.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="355.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="361.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="366.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="371.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="376.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="382.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="387.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="392.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="398.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="403.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="408.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="413.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="419.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="424.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="429.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="435.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="440.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="445.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="450.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="456.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="461.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="466.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="472.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="477.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="482.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="487.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="493.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="498.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="503.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="509.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="514.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="519.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="525.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="530.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="535.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="540.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="546.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="551.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="556.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="562.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="567.2" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="572.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="577.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="583.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="588.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="593.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="599.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="604.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="609.5" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="614.8" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="620.1" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="625.4" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="630.7" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="636.0" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="641.3" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="646.6" y="74" width="2.4" height="18" fill="#C0605A"/>
<rect x="651.9" y="74" width="2.4" height="18" fill="#C0605A"/>
<text x="260" y="108" class="sub">35 s 内约一半调用失败,共 83 次</text>
<text x="20" y="134">kill -9,关闭负载均衡缓存</text>
<rect x="250" y="122" width="470" height="18" rx="3" fill="#F5F5F5"/>
<rect x="252.3" y="122" width="3" height="18" fill="#C0605A"/>
<text x="260" y="156" class="sub">只失败 1 次</text>
<text x="20" y="182" class="b">进程卡住,默认配置</text>
<rect x="250" y="170" width="470" height="18" rx="3" fill="#F5F5F5"/>
<rect x="252.3" y="175" width="467.6" height="8" rx="2" fill="#E8B4B0"/>
<text x="260" y="204" class="sub">发往它的调用要等 60 s 才失败</text>
<text x="20" y="230">进程卡住,读取超时 2 s</text>
<rect x="250" y="218" width="470" height="18" rx="3" fill="#F5F5F5"/>
<rect x="250.0" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="275.9" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="301.7" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="327.6" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="353.4" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="379.2" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="405.1" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="430.9" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="456.8" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="482.6" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<rect x="508.5" y="218" width="23.5" height="18" fill="#E8B4B0"/>
<text x="260" y="252" class="sub">每次 2 s 后失败,约 23 s 后不再调用</text>
<line x1="257.1" y1="58" x2="257.1" y2="270" stroke="#9188D8" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="261.05" y="50" class="sub" fill="#7A70C8">Nacos 删除实例 0.6 s</text>
<line x1="520.2" y1="58" x2="520.2" y2="270" stroke="#9188D8" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="524.25" y="50" class="sub" fill="#7A70C8">进程卡住时 Nacos 23 s 后才删除</text>
<line x1="661.2" y1="58" x2="661.2" y2="270" stroke="#9188D8" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="657.25" y="304" class="sub e" fill="#7A70C8">负载均衡缓存 35 s 过期</text>
<text x="20" y="322" class="sub">订单服务每 0.2 s 调用一次库存服务,请求在 8081 和 8082 之间轮询;红色为失败的调用</text>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

@@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="400" viewBox="0 0 740 400">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="370" y="28" class="title">一次下单请求经过的组件</text>
<rect x="22" y="172" width="90" height="56" rx="6" class="shadow"/>
<rect x="20" y="170" width="90" height="56" rx="6" fill="#DDD7F6"/>
<text x="65" y="196" class="b c">App</text>
<text x="65" y="211" class="sub c">带上令牌</text>
<rect x="162" y="160" width="150" height="80" rx="6" class="shadow"/>
<rect x="160" y="158" width="150" height="80" rx="6" fill="#B1DDF0"/>
<text x="235" y="182" class="c b">网关 :8080</text>
<text x="235" y="202" class="c sub">校验令牌,加上 X-User-Id</text>
<text x="235" y="220" class="c sub">/api/orders/** → 订单服务</text>
<rect x="362" y="160" width="150" height="80" rx="6" class="shadow"/>
<rect x="360" y="158" width="150" height="80" rx="6" fill="#B1DDF0"/>
<text x="435" y="182" class="c b">订单服务 :8090</text>
<text x="435" y="202" class="c sub">OpenFeign 调用库存服务</text>
<text x="435" y="220" class="c sub">读取 order.max-items</text>
<rect x="582" y="132" width="140" height="50" rx="6" class="shadow"/>
<rect x="580" y="130" width="140" height="50" rx="6" fill="#B0E3E6"/>
<text x="650" y="159.5" class="b c">库存服务 :8081</text>
<rect x="582" y="212" width="140" height="50" rx="6" class="shadow"/>
<rect x="580" y="210" width="140" height="50" rx="6" fill="#B0E3E6"/>
<text x="650" y="239.5" class="b c">库存服务 :8082</text>
<line x1="113" y1="198" x2="156" y2="198" class="arrow "/>
<line x1="313" y1="198" x2="356" y2="198" class="arrow "/>
<line x1="513" y1="190" x2="576" y2="158" class="arrow "/>
<line x1="513" y1="206" x2="576" y2="232" class="arrow "/>
<text x="545" y="200" class="c sub">轮询</text>
<rect x="262" y="52" width="360" height="60" rx="6" class="shadow"/>
<rect x="260" y="50" width="360" height="60" rx="6" fill="#E6E6E6"/>
<text x="440" y="72" class="c b">Nacos :8848</text>
<text x="440" y="92" class="c sub">注册中心:服务名 → 实例列表;配置中心:order-service.yaml</text>
<path d="M235,154 C235,130 300,118 330,113" class="arrow dash"/>
<text x="180" y="128" class="sub">查询实例</text>
<path d="M435,114 L435,154" class="arrow dash"/>
<text x="441" y="138" class="sub">实例列表、配置推送</text>
<path d="M650,126 C650,100 640,90 624,86" class="arrow dash"/>
<text x="656" y="108" class="sub">注册</text>
<line x1="160" y1="300" x2="720" y2="300" stroke="#9188D8" stroke-width="2" stroke-dasharray="6 4"/>
<text x="160" y="292" class="sub">traceId 6ade5947…74b 随请求头 traceparent 传递,各服务的日志中都带上它</text>
<text x="160" y="324" class="sub">订单服务调用库存服务时加上超时和熔断(Sentinel),跨服务的事务用 Seata 或消息保证一致</text>
<text x="20" y="380" class="sub">本机实际运行:Nacos 2.5.4,Spring Boot 3.3,Spring Cloud 2023.0,Spring Cloud Alibaba 2023.0.3.4</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB