Spring Cloud 入门

微服务基础组件、服务治理与主流生态演进

Posted by Ekko on August 16, 2020

这篇笔记围绕 Spring Cloud 在微服务体系中的基础职责展开,主线仍然是注册中心、服务调用、负载均衡、容错、网关、配置中心与消息总线,同时补充当前主流生态中的常见演进方向,用一篇笔记把核心组件之间的关系串起来。

文中同时保留 EurekaRibbonHystrixZuul 等经典 Netflix 组件,也补充 NacosGatewayOpenFeignSentinel 等较新的组合,阅读时需要把“经典方案的设计思想”和“当前项目里的主流选型”区分开来看。

参考资料:

官方项目:Spring CloudSpring Cloud GatewaySpring Cloud OpenFeignSpring Cloud ConfigSpring Cloud Bus

生态文档:Spring Cloud NetflixNacosSentinel

理论与补充资料:CAP理论中的P到底是个什么意思分布式系统的CAP理论JavaGuide - Spring Cloud

[TOC]


CAP 理论

CAP 是分布式系统里最常被提到的基础理论之一,它讨论的不是“系统平时好不好用”,而是:

当网络分区发生时,一个分布式系统无法同时满足强一致性(C)和可用性(A)。

先看三个概念:

  • C(Consistency,一致性): 所有节点对外表现得像“只有一份最新数据”。一次写入成功后,之后的每次读取都应该读到这次写入后的最新值,否则就返回错误或拒绝服务
  • A(Availability,可用性): 每个没有故障的节点,都必须在有限时间内对请求给出明确响应,不能一直等待,更不能直接不处理
  • P(Partition tolerance,分区容错性): 节点之间即使因为网络故障而无法通信,系统仍然要继续工作

什么是“强一致性”

很多人第一次看 CAP 时都会困惑:到底是什么内容要保持一致?

这里的一致性,指的是分布式系统中多副本数据的一致性,或者说,客户端从不同节点读到的结果是否一致

比如同一份业务数据被保存在多个节点中:

  • 用户账户是否已经注册
  • 商品库存还剩多少
  • 订单状态是否已经支付
  • 服务注册中心里某个实例是否还在线

如果系统满足 CAP 里的 C,那么客户端无论访问哪个节点,看到的都应该是同一份“最新结果”。

它的具体表现通常是:

  • 用户刚在节点 A 完成注册,马上去节点 B 查询,也必须能查到这个账户
  • 库存在节点 A 被扣减后,其他节点不能继续把旧库存返回给客户端
  • 一个实例已经下线,任意节点对外提供的注册信息都不应继续把它当作可用实例

换句话说,强一致性强调的是“读到的一定是最新写入结果”
如果做不到这一点,而是允许一段时间内不同节点返回不同结果,那就不是 CAP 语境下的强一致性。

下面有三个节点(它们组成一个集群),此时三个节点都能够相互通信:

三节点通信.png

由于分布式系统依赖网络通信,而网络本身就可能出现延迟、丢包、机房隔离等问题,所以节点之间一旦无法互相通信,整个集群就会被切成几个彼此不连通的区域。

这就是网络分区(Partition)。

三节点通信失败.png

现在假设网络分区已经发生,此时有一个请求过来,想要注册一个账户:

网络分区后接受请求.png

这时节点一和节点三无法通信,系统就必须做选择。

选择 A:优先保证可用性

如果系统决定:请求来了先处理,先给用户成功响应,那么用户的注册请求可以在当前能够通信的那一侧完成。

这样做的好处是:

  • 用户不会因为网络分区而立刻看到报错
  • 服务仍然能对外提供读写能力

但问题也很明显:

  • 节点一和节点三暂时无法同步数据
  • 用户此时访问不同节点,可能看到不同结果
  • 某些节点已经知道“这个账户注册成功了”,某些节点还不知道

这种情况就是:选择了 A,牺牲了 C。

也就是说,系统继续对外提供服务,但短时间内不能保证所有节点都看到同一份最新数据。

选择 C:优先保证强一致性

如果系统决定:只要数据还不能在所有关键节点之间达成一致,就暂时不接这个请求,那么它就会拒绝本次注册,或者让请求等待到网络恢复后再处理。

这样做的结果是:

  • 一旦请求成功,所有节点看到的一定是统一的最新结果
  • 不会出现“某些节点注册成功、某些节点还不知道”的情况

但代价是:

  • 分区期间部分请求可能失败
  • 用户会感知到服务不可用,或者响应明显变慢

这种情况就是:选择了 C,牺牲了 A。

为什么 P 几乎是必选项

在实际分布式系统中,网络故障不是“会不会发生”的问题,而是“什么时候发生”的问题。

所以,P 往往不是主观选择,而是客观约束。只要系统是分布式部署的,就必须接受网络可能被分区这个事实。

因此,CAP 更准确的理解应该是:

在出现网络分区时,系统必须在 C 和 A 之间做取舍。

注意,这并不是说:

  • 选择了 AP,系统就完全不要一致性
  • 选择了 CP,系统就完全不可用

更准确地说是:

  • AP 系统: 在分区期间优先响应请求,通常只能保证最终一致性
  • CP 系统: 在分区期间优先保证数据正确,允许部分请求失败或超时

一致性的常见层次

CAP 里的 C 指的是强一致性。而在工程实践中,一致性通常还有不同层次:

  • 强一致性: 写入一旦成功,之后任意节点上的读取都必须返回最新值
  • 弱一致性: 读取不保证拿到最新值
  • 最终一致性(Eventual Consistency): 允许短时间不一致,但在没有新的更新后,数据最终会达到一致

很多互联网系统之所以能做到更高可用,本质上就是接受了“短时间内不一致”,用最终一致性来换取更好的服务连续性。

什么是“可用性”

可用性不是泛泛地指“系统差不多能用”,而是一个更严格的概念:

对于每一个发到非故障节点的请求,系统都必须在有限时间内返回响应。

这里的“响应”有两个重点:

  • 不能无限等待
  • 不能因为要等其他分区同步完成,就一直卡住

它的具体表现通常是:

  • 服务还能够正常接收请求
  • 用户能够在可接受时间内拿到结果
  • 即使部分节点或链路异常,系统也尽量不整体中断

所以在工程里,人们常常用可用率来衡量服务质量,比如 99.9%99.99%。本质上都是在描述:系统有多少时间可以持续提供服务。

可用性停机时间.png

一句话总结

CAP 不是在说“三个里永远只能选两个”,而是在说:

当网络分区发生时,分布式系统无法同时做到“所有节点马上看到同一份最新数据”和“所有请求都持续成功返回”。

因此,CAP 理论描述的核心矛盾是:

在容忍网络分区的前提下,强一致性和高可用性不能同时被绝对满足。


Spring Cloud 是什么

SpringCloud架构.png

Spring Cloud 不是单独的某一个中间件,而是一组围绕微服务基础设施的组件集合。
如果说 Spring Boot 更关注单个应用如何快速开发和自动配置,那么 Spring Cloud 更关注的是多个服务拆开之后,如何完成注册、发现、调用、治理、监控与协同

它本身并不是“重新发明一套分布式系统”,而是把不同领域里已经成熟的能力按照 Spring 体系的方式做了统一抽象和集成。于是,服务发现、配置管理、网关、链路追踪、消息驱动这些原本分散的问题,可以在同一套编程模型里组织起来。

从历史上看,很多人第一次接触 Spring Cloud,往往接触到的是 Spring Cloud Netflix 这一代组合,例如 EurekaRibbonHystrixFeignZuul
而从当前项目实践看,更常见的则是 NacosOpenFeignSpring Cloud GatewaySentinelMicrometer Tracing 等组合。因此,学习 Spring Cloud 时,既要理解经典组件背后的设计思想,也要知道这些能力在新生态中的演进方向。

Spring Cloud 主要解决什么问题

能力域 经典方案 当前常见方案 解决的问题
服务注册与发现 Eureka Nacos、Consul 服务实例动态上下线后,调用方如何找到可用实例
服务调用 Feign + Ribbon OpenFeign + Spring Cloud LoadBalancer 服务之间如何以统一方式发起远程调用
服务保护 Hystrix Sentinel、Resilience4j 下游故障、超时、雪崩时如何隔离、熔断、降级
网关 Zuul Spring Cloud Gateway 统一入口、统一路由、统一鉴权与限流
配置管理 Spring Cloud Config Nacos Config、Spring Cloud Config 配置如何集中管理、动态刷新
可观测性 Sleuth + Zipkin Micrometer Tracing、OpenTelemetry 跨服务请求如何关联日志和定位瓶颈
消息驱动 Spring Cloud Stream、Bus Spring Cloud Stream、Bus 异步解耦、事件广播、配置刷新如何落地
graph LR
    A[客户端] --> B[网关 Gateway/Zuul]
    B --> C[业务服务]
    C --> D[注册中心 Eureka/Nacos]
    C --> E[配置中心 Config/Nacos]
    C --> F[服务保护 Hystrix/Sentinel]
    C --> G[链路追踪 Sleuth/Zipkin]
    C --> H[消息系统 Stream/Bus]

本文相关的常见项目

项目名称 项目职能
Spring Cloud Netflix 整合 Eureka、Ribbon、Hystrix、Zuul 等经典组件,是大量早期教程和老项目的基础
Spring Cloud Config 提供集中式配置管理,支持远程配置拉取、加解密与刷新机制
Spring Cloud Bus 把配置变更等事件广播到整个服务集群
Spring Cloud OpenFeign 提供声明式 HTTP 客户端,降低服务间调用代码量
Spring Cloud Gateway 提供统一入口、路由、过滤、限流等网关能力
Spring Cloud Stream 对 Kafka、RabbitMQ 等消息中间件做统一抽象
Spring Cloud Sleuth 为分布式链路追踪提供埋点与上下文传播能力
Spring Cloud Consul / Zookeeper 提供其他注册中心与配置管理实现

组件演进补充

把 Spring Cloud 放到今天的项目实践里看,它并不是只有一套固定组合,而是经历过明显的组件演进。

很多早期教程里常见的是这一套:

  • Eureka:注册中心
  • Ribbon:客户端负载均衡
  • Hystrix:熔断降级
  • Feign:声明式调用
  • Zuul:网关

而在现在更常见的项目里,大家更容易看到的是:

  • Nacos:注册中心 + 配置中心
  • Spring Cloud Gateway:网关
  • OpenFeign:声明式服务调用
  • SentinelResilience4j:限流、熔断、降级
  • Sleuth/ZipkinMicrometer Tracing:链路追踪
  • Spring Cloud Stream:消息驱动微服务

也就是说,这篇笔记前半部分讲到的是 Spring Cloud Netflix 时代的经典组件,而后面补充的内容,会更偏向 现在项目里更常见的 Spring Cloud / Spring Cloud Alibaba 组合

两套东西不是互相否定,而是代表了不同阶段的主流方案。


Eureka 服务注册中心

分布式拆分子服务后,子系统之间的通讯必然成为需要考虑的问题

子系统与子系统之间不再位于同一个进程中,服务调用自然变成远程调用。最直接的做法是把目标服务的 IP 和端口写死在配置里,例如通过 HttpClientRestTemplate 直接访问某个地址。

但只要服务实例一旦扩容、缩容、迁移或重启,静态地址就会迅速变成维护负担。典型问题往往有两类:

功能实现一:A 服务需要调用 B 服务

在 A 服务的代码里面调用 B 服务,显式通过 IP 地址调用:http://123.123.123.123:8888/java3y/3

功能实现二:A 服务调用 B 服务,B 服务调用 C 服务,C 服务调用 D 服务

在 A 服务的代码里面调用 B 服务,显式通过 IP 地址调用:http://123.123.123.123:8888/java3y/3,(同样地)B->C,C->D

… …

一旦 B 服务的 IP 地址变化,所有依赖它的服务都需要修改配置;服务数量越多,维护这些静态地址的成本就越高。

因此,服务注册与服务发现就成了微服务体系最基础的一层。它解决的问题不是“如何调用 HTTP”,而是“如何在服务实例动态变化时,仍然稳定地找到可用目标”。

在早期 Spring Cloud Netflix 体系里,最典型的注册中心实现就是 Eureka

创建一个E服务,将 A、B、C、D 四个服务的信息都注册到 E 服务上,E 服务维护这些已经注册进来的信息

Eureka服务注册中心.png

A、B、C、D 四个服务都可以从 Eureka 获取注册表。这样一来,服务之间不再依赖具体的 IP 地址,而是通过服务名完成调用。

  • 拿到注册清单后,根据服务名选择具体实例
  • 服务实例的 IP 可以变化,但服务名通常保持稳定

提供注册表服务的节点称为 Eureka Server,注册到注册中心并消费注册表的节点统称为 Eureka Client

Eureka 注册中心的三种角色:

  1. Eureka Server: 通过Register、Get、Renew 等接口提供服务的注册和发现

  2. Application Service (Service Provider) 服务提供方把自身的服务实例注册到Eureka Server 中

  3. Application Client (Service Consumer) 服务调用方通过 Eureka Server 获取服务列表,消费服务

Eureka Server 的典型配置:

1
2
3
4
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

这里的含义是:注册中心本身不需要把自己注册到其他注册中心,也不需要再去拉取注册表。

Eureka Client 可以是服务提供者,也可以是服务消费者,很多服务同时承担这两种角色。

需要区分两个配置项:

  • register-with-eureka:是否把当前实例注册到 Eureka
  • fetch-registry:是否从 Eureka 拉取服务注册表

因此,一个纯消费端即使不注册自己,也依然可以通过拉取注册表来发现其他服务。

1
2
3
4
5
6
eureka:
  client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

Eureka 的治理机制:

服务提供者:

  • 服务注册: 启动的时候会通过发送 REST 请求的方式将自己注册到 Eureka Server上,同时带上了自身服务的一些元数据信息
  • 服务续约: 注册完成后,服务提供者会通过心跳持续向 Eureka Server 续约
  • 服务下线: 当服务实例正常关闭时,会主动发送下线请求,通知注册中心移除自己

服务消费者:

  • 获取服务: 启动时从注册中心拉取服务清单
  • 服务调用: 根据服务名和实例元数据选择目标节点,通常优先选择同 Zone 的实例

Eureka Server(服务注册中心):

  • 失效剔除: 默认按周期扫描注册表,把长时间未续约的实例剔除出去
  • 自我保护: 当短时间内心跳失败比例异常升高时,Eureka 会暂时放宽摘除策略,避免因为网络抖动把大量健康实例误删

Eureka服务治理.png


Eureka、ZooKeeper、Nacos 对比

前面讲完 CAP 之后,再看注册中心就会顺很多。

因为注册中心本质上也要面对同一个问题:

  • 网络分区发生时,还要不要继续返回服务列表
  • 返回的服务列表,是不是必须保证每个节点都完全一致
  • 如果一致性和可用性冲突,应该优先保哪一个

这也是为什么大家总喜欢把 EurekaZooKeeperNacos 放在一起比较。

先说结论

如果只从“作为服务注册中心”这个角度看,可以先记住下面这句话:

  • Eureka: 更偏 AP,优先保证服务发现可用
  • ZooKeeper: 更偏 CP,优先保证注册数据一致
  • Nacos: 更灵活,既能做注册中心,也能做配置中心;在服务发现上可以根据实例类型呈现不同取向

不过要注意,这里的“偏 AP / 偏 CP”是从典型使用方式和设计取向来理解的,不是简单粗暴地给整个产品贴一个绝对标签。

为什么 Eureka 更偏 AP

Eureka 的设计目标之一,就是让服务发现尽量不要中断

它采用的是 Peer to Peer 的对等复制思路,没有像 ZooKeeper 那样强依赖一个需要完成选举的中心 Leader。多个 Eureka Server 之间互相同步注册信息,每个节点都可以对外提供服务。

它的具体表现是:

  • 某个 Eureka Server 宕机后,客户端可以切换到其他节点
  • 服务实例的注册信息会在各个 Eureka 节点之间复制
  • 当短时间内心跳丢失过多时,Eureka 会进入自我保护模式

这里的关键点在于:
Eureka 在异常情况下更倾向于“先别轻易把实例删掉,先保证还能返回服务列表”。

这意味着什么?

  • 好处是:注册中心不容易因为网络抖动就把整个服务发现能力搞挂
  • 代价是:返回的数据可能不是绝对最新的

比如某个服务实例实际上已经不可用了,但在短时间内,Eureka 仍可能把它保留在注册表里。这会导致客户端拿到旧数据,但至少还能继续发起调用,而不是整个注册中心直接不可用。

所以说,Eureka 更偏 AP,不是因为它完全不要一致性,而是因为它在分区或异常场景下,更愿意用“短时间可能不一致”来换“服务还能继续被发现”。

为什么 ZooKeeper 更偏 CP

ZooKeeper 本质上是一个分布式协调组件,它最擅长的事情其实是:

  • 维护一致的元数据
  • 做分布式协调
  • 做选主、锁、配置同步

它的核心设计强调的是:

  • 数据有序
  • 写请求按严格顺序提交
  • 集群需要基于多数派机制达成一致

因此,当 ZooKeeper 集群出现网络分区、Leader 选举、或者可用节点数不足半数时,系统会优先保证一致性,而不是优先保证每个请求都成功。

这在服务发现里的直观表现就是:

  • 如果 ZooKeeper 正在选主,客户端可能暂时拿不到最新服务列表
  • 如果集群中没有形成多数派,请求可能失败
  • 它宁可暂时不提供服务,也不愿意返回一个可能有问题的结果

所以说,ZooKeeper 更偏 CP,不是因为它完全不可用,而是因为一旦一致性无法保证,它会优先收紧对外能力。

这套思路非常适合:

  • 分布式锁
  • 领导者选举
  • 配置一致性要求高的协调场景

但如果只是拿它来做服务注册中心,就会出现一个现实问题:

对于服务发现来说,客户端往往更在乎“先找到一个还能调用的实例”,而不是“注册表在每一秒都绝对一致”。

也正因为如此,作为纯服务注册中心时,ZooKeeper 的使用体验通常不如 Eureka 那么“抗抖动”。

为什么说 Nacos 更灵活

Nacos 和前两者最大的不同,不只是“它也能做注册中心”,而是:

它同时把服务发现、配置管理、服务管理整合到了一个平台里。

从 Spring Cloud 生态的实际使用习惯来看,Nacos 经常既承担:

  • 注册中心 的角色
  • 配置中心 的角色

这让它在微服务项目里非常常见。

如果只看服务发现这一块,Nacos 并不是简单地永远偏 AP 或永远偏 CP,而是要结合实例类型来看:

  • 临时实例(ephemeral,默认更常见): 更偏 AP,依赖客户端心跳,适合动态上下线的微服务实例
  • 持久实例(persistent): 更偏 CP,适合对实例信息持久化和一致性要求更高的场景

这也是 Nacos 更灵活的原因之一:
它没有像“Eureka 就是这样、ZooKeeper 就是那样”那么单一,而是给了不同场景不同选择。

另外还要补一句:

当 Nacos 被当作配置中心使用时,关注点又和服务发现不一样。
配置数据通常比“某个实例此刻是否在线”更强调持久化和一致性,所以它作为配置中心时,思路会比纯服务发现场景更偏向“配置不能乱、配置不能丢”。

作为注册中心,三者怎么理解

如果把它们都放回“服务注册中心”这个具体场景里,可以概括为:

Eureka

  • 目标更偏向“服务发现别中断”
  • 更能接受短时间注册信息不一致
  • 更适合把“可用性”放在前面的服务发现体系

ZooKeeper

  • 目标更偏向“元数据必须一致”
  • 对 Leader、半数机制、一致性要求更敏感
  • 更适合协调类场景,不是天然最优的服务注册中心选择

Nacos

  • 既能做注册中心,又能做配置中心
  • 在服务注册场景下能力更完整,生态里也更常见
  • 能根据实例类型在可用性和一致性之间做不同权衡

一个更实用的理解方式

选型时更关键的问题并不是“谁最好”,而是“当前场景更在意什么”。

如果更在意的是:

  • 注册中心不要轻易不可用
  • 即使偶尔拿到旧实例,也比完全拿不到服务列表更能接受

那么思路会更接近 Eureka

如果更在意的是:

  • 元数据必须严格一致
  • 宁可短时间失败,也不能接受不一致结果

那么思路会更接近 ZooKeeper

如果希望:

  • 注册中心和配置中心尽量统一
  • Spring Cloud Alibaba 生态集成更自然
  • 后续还想要命名空间、分组、动态配置这些能力

那么很多项目会更倾向于 Nacos

简单总结

  • Eureka 更偏 AP: 因为它在异常场景下优先保证服务发现还能继续,允许短时间注册信息不是最新
  • ZooKeeper 更偏 CP: 因为它优先保证数据一致,必要时宁可暂时不提供服务
  • Nacos 更灵活: 既是注册中心,也是配置中心;在服务发现上能根据不同实例类型体现不同取向

从今天的 Spring Cloud 实战来看:

  • 老的 Spring Cloud Netflix 项目里常见 Eureka
  • 偏协调组件、分布式治理场景里常见 ZooKeeper
  • 现在国内 Spring Cloud / Spring Cloud Alibaba 项目里,Nacos 往往是更常见的选择

Ribbon 客户端负载均衡

通过 Eureka 这类注册中心,调用方已经可以拿到某个服务的多个实例列表。接下来的问题就变成:同一个服务有多个实例时,当前请求应该落到哪一台机器上。

在早期 Spring Cloud Netflix 体系中,最常见的选择就是 Ribbon。它工作的方式不是在服务端放一个统一调度器,而是在调用方本地基于服务列表做负载均衡决策。

可以先看一个通过服务名调用的 RestTemplate 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 传统方式直接写死 IP,不利于扩容和迁移
//private static final String REST_URL_PREFIX = "http://localhost:8001";

// 服务实例名
private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";

/**
 * postForObject(url, request, responseType)
 * 分别表示:请求地址、请求体、响应类型。
*/
@Autowired
private RestTemplate restTemplate;

@RequestMapping(value = "/consumer/dept/add")
public boolean add(Dept dept) {
    return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
}

服务提供者通常不会只部署一个实例,而是会做成集群,以便提升吞吐和可用性。

服务提供者集群.png

当一个服务有多个实例时,请求如何在这些实例之间分配,就是负载均衡问题。

Spring Cloud 既可以配合 Nginx 这类服务端负载均衡器,也可以在客户端使用 Ribbon 做本地负载均衡。

负载均衡又区分了两种类型:

客户端负载均衡(Ribbon)

  • 服务实例清单保存在客户端
  • 调用方从注册中心拉取服务列表,再在本地根据算法选择目标实例

服务端负载均衡(Nginx)

  • 服务实例清单由代理层维护
  • 请求先进入代理,再由代理转发到后端实例

客户端服务端负载均衡.png


Ribbon 细节:

Ribbon 默认提供轮询等负载均衡策略,也支持自定义规则。

1
2
3
4
5
6
7
8
9
@Configuration
public class MySelfRule {
    @Bean
    public IRule myRule() {
        // return new RoundRobinRule(); // 轮询
        // return new RandomRule();     // 随机
        return new RandomRule_ZY();     // 自定义规则
    }
}

自定义策略时,通常是继承 AbstractLoadBalancerRule 并重写 choose(ILoadBalancer lb, Object key)

需要注意两点:

  • Ribbon 只是客户端负载均衡组件,本身不等同于整个系统的 CAP 取舍
  • 客户端重试虽然能提升成功率,但必须结合接口幂等性,否则可能放大重复调用风险

另外,在较新的 Spring Cloud 版本里,Ribbon 已经逐步退出主流,新项目更常见的是 Spring Cloud LoadBalancer


Nginx 和 Ribbon 的对比

同样是“负载均衡”,NginxRibbon 的职责位置并不一样。

Nignx负载均衡.png

Ribbon负载均衡.png

维度 Nginx Ribbon / LoadBalancer
负载均衡位置 服务端代理层 客户端本地
服务列表维护位置 代理层维护后端地址 调用方从注册中心拉取
流量入口 所有请求先经过代理 请求在本地选好实例后直达目标
典型用途 对外统一入口、反向代理、静态资源、TLS 终止 服务间调用时按实例做本地负载均衡
与注册中心关系 可以无关,也可以联动 通常直接依赖注册中心提供的实例列表

Hystrix 服务容错保护

调用多个远程服务时,当某个服务出现延迟:

SpringCloud调用延迟.png

在高并发的情况下,由于单个服务的延迟,可能导致所有的请求都处于延迟状态,甚至在几秒钟就使服务处于负载饱和的状态,资源耗尽,直到不可用,最终导致这个分布式系统都不可用,这就是“雪崩”

SpringCloud延迟雪崩.png

针对上述问题, Spring Cloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能

  • Fallback(失败快速返回): 当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延
  • 资源/依赖隔离(线程池隔离): 它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务

Hystrix 中常被提到的一组默认熔断参数包括:

  • 请求量阈值 20
  • 休眠时间窗 5s
  • 错误百分比阈值 50%

  • 每当 20 个请求中,有 50% 失败时,熔断器就会打开,此时再调用此服务,将会直接返回失败,不再调远程服务
  • 直到 5s 钟之后,重新检测该触发条件,判断是否把熔断器关闭,或者继续打开

熔断:

是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过断路器直接切断这条调用链。可以使用 @HystrixCommand 为方法增加超时、熔断和降级能力。

1
2
3
4
5
6
@HystrixCommand(
    commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public List<Xxx> getXxxx() {
    // ...省略代码逻辑
}

降级

降级的目标是在下游异常时提供一个可控的兜底结果,而不是把错误直接放大到整个系统。

例如热点接口因为访问量激增导致后端压力过大时,可以降级返回“系统繁忙,请稍后再试”之类的占位结果,而不是继续把线程和连接耗尽。

1
2
3
4
5
6
7
8
9
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/news")
public String getNews(@PathVariable("id") int id) {
    // 调用新闻服务,省略具体远程调用代码
}

public String getHystrixNews(int id) {
    return "当前访问量较高,新闻详情暂时不可用";
}

需要补充一点:Hystrix 现在更多承担“理解熔断、隔离、降级模型”的学习价值。在较新的项目里,更常见的替代组合是 SentinelResilience4j


Feign / OpenFeign 声明式服务调用

在服务之间频繁发生 HTTP 调用时,如果每次都手写 RestTemplateHttpClient,代码会迅速变得重复而分散。

Feign 的价值就在于把“远程调用”抽象成接口声明。早期 Spring Cloud 文档常写 Feign,当前更常见的名称则是 OpenFeign,核心思想并没有变化。

它本质上是一个声明式 HTTP 客户端:调用方只定义接口、请求路径和参数绑定规则,底层由框架生成实际调用逻辑。

Feign 服务绑定

1
2
3
4
5
6
7
8
9
10
11
@FeignClient(name = "MICROSERVICECLOUD-DEPT", fallback = DeptClientServiceFallback.class)
public interface DeptClientService {
    @GetMapping("/dept/get/{id}")
    Dept get(@PathVariable("id") Long id);

    @GetMapping("/dept/list")
    List<Dept> list();

    @PostMapping("/dept/add")
    boolean add(@RequestBody Dept dept);
}

Feign 中使用降级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class DeptClientServiceFallback implements DeptClientService {
    @Override
    public Dept get(Long id) {
        return new Dept()
                .setDeptno(id)
                .setDname("id=" + id + " 的部门信息暂时不可用,已触发服务降级")
                .setDb_source("no this database in MySQL");
    }

    @Override
    public List<Dept> list() {
        return Collections.emptyList();
    }

    @Override
    public boolean add(Dept dept) {
        return false;
    }
}

调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class DeptController_Consumer {
    @Autowired
    private DeptClientService deptClientService;

    @GetMapping("/consumer/dept/get/{id}")
    public Dept get(@PathVariable("id") Long id) {
        return deptClientService.get(id);
    }

    @GetMapping("/consumer/dept/list")
    public List<Dept> list() {
        return deptClientService.list();
    }

    @PostMapping("/consumer/dept/add")
    public boolean add(@RequestBody Dept dept) {
        return deptClientService.add(dept);
    }
}

Zuul 网关服务

如果没有网关,外部流量通常会直接打到各个微服务:

没有网关的微服务.png

这种架构至少会暴露两个问题:

  • 路由维护成本高: 外层代理需要感知所有服务实例的地址变化
  • 横切逻辑重复: 鉴权、签名、限流、日志等逻辑容易在每个服务中重复实现

nginx维护每个服务实例地址.png

每个服务都有自己的 IP 地址,Nginx 想要正确请求转发到服务上,就必须维护着每个服务实例的地址

更麻烦的是,服务实例地址会变化,服务边界也可能持续调整。

例如购物车和订单都需要登录校验,如果没有统一入口,这些校验逻辑就很容易在多个服务里重复出现。

API 网关 的作用,就是把这些对外统一能力前移到系统入口层。在 Spring Cloud Netflix 体系中,对应的经典实现就是 Spring Cloud Zuul

Zuul 主要解决的是两类问题:

  • 与 Eureka 配合后,Zuul 可以根据服务名做动态路由,不再由外层静态维护每个实例地址
  • 所有请求先经过网关,因此鉴权、过滤、限流、灰度等逻辑可以统一放在入口层处理

在经典组合里,Zuul 还可以与 RibbonHystrix 协同工作,因此它并不只是“转发请求”,而是具备网关治理能力的一层基础设施。


Zuul 过滤功能

过滤器是 Zuul 最核心的扩展点之一。因为所有流量都会经过网关,所以限流、灰度发布、权限控制、日志采集这类逻辑都可以通过过滤器接入。

Zuul过滤器.png

过滤器类型:Pre、Route、Post、Error

  • Pre:请求转发之前执行
  • Route:负责路由转发过程
  • Post:响应返回之前执行
  • Error:异常处理阶段执行

下面用一个最基础的日志示例说明过滤器结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Component
public class PreRequestFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.set("startTime", System.currentTimeMillis());
        return null;
    }
}

@Slf4j
@Component
public class AccessLogFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        Long startTime = (Long) context.get("startTime");
        String uri = request.getRequestURI();
        long duration = System.currentTimeMillis() - startTime;
        log.info("uri: {}, duration: {}ms", uri, duration);
        return null;
    }
}

Zuul 令牌桶限流

令牌桶只是网关限流里的典型做法之一,这里用一个最常见的示例说明思路。

Zuul令牌桶.png

令牌桶限流:

系统会以固定速率向桶中放入令牌,请求到来时先尝试获取令牌;获取失败则拒绝,获取成功则放行。

在 Zuul 中,这类逻辑通常放在前置过滤器里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
    // 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return -5;
    }

    @Override
    public Object run() throws ZuulException {
        log.info("放行");
        return null;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        if(!RATE_LIMITER.tryAcquire()) {
            log.warn("访问量超载");
            // 指定当前请求未通过过滤
            context.setSendZuulResponse(false);
            // 向客户端返回响应码429,请求数量过多
            context.setResponseStatusCode(429);
            return false;
        }
        return true;
    }
}

同一类过滤器机制还可以扩展到权限校验、灰度发布、审计日志等场景。

Zuul 自身也可能成为单点,因此生产环境通常会把网关做成集群,再由外层负载均衡器统一接入。


Zuul 的路由功能

如果系统里已经有多个 Consumer 和 Provider,再加入 Zuul 后,入口结构通常会变成“客户端 -> 网关 -> 内部服务”。

加zuul网关的架构.png

Zuul 注册到 Eureka 后,可以根据注册表中的服务元数据做动态路由映射。例如原本直接调用 localhost:8001/studentInfo/update,可以改为通过网关统一入口访问。

Zuul 最基本的配置:

1
2
3
4
5
6
server:
  port: 9000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9997/eureka

然后在启动类上加入 @EnableZuulProxy 即可开启网关代理能力。

Zuul 统一前缀:

1
2
zuul:
  prefix: /zuul

此时访问路径会变成 localhost:9000/zuul/consumer1/studentInfo/update

路由策略配置:

如果直接把服务名暴露在外部路径上,虽然方便,但并不总是合适。更常见的做法是自定义外部路由路径。

1
2
3
4
zuul:
  routes:
    consumer1: /FrancisQ1/**
    consumer2: /FrancisQ2/**

此时可以通过 localhost:9000/zuul/FrancisQ1/studentInfo/update 访问对应服务。

服务名屏蔽:

如果希望外部完全看不到真实服务名,还需要显式屏蔽服务名路由。

1
2
zuul:
  ignore-services: "*"

路径屏蔽:

Zuul 还支持按 URI 模式屏蔽路径,用于限制某些内部接口直接暴露出去。

1
2
3
zuul:
  ignore-patterns:
    - /**/auto/**

这样,匹配 auto 路径规则的请求就不会再被转发。

** 代表匹配多级任意路径 *代表匹配一级任意路径

敏感请求头屏蔽:

默认情况下,像 CookieSet-Cookie 这类敏感请求头会被 Zuul 屏蔽;如果业务需要,也可以显式调整这部分转发策略。


Zuul 小结

Zuul 和 Feign 都能和 Ribbon、Hystrix 协同工作,但二者处在完全不同的层级。

Zuul网关.png

组件 所处位置 主要职责
Zuul 系统入口层 统一接入、路由转发、鉴权、过滤、限流
Feign / OpenFeign 服务内部调用层 以声明式接口方式发起服务间 HTTP 调用
Ribbon / LoadBalancer 调用方本地 在多个服务实例之间选择一个目标节点

Spring Cloud Config 分布式配置中心

随着业务的扩展,服务会越来越多。每个服务都有自己的配置文件,既然是配置文件,配置的东西难免会有些改动。比如每个服务中写的数据库配置,配置文件中的密码需要更换,那就得三个都要重新更改

Config配置中心.png

在分布式系统中,某一个基础服务信息变更,都很可能会引起一系列的更新和重启

Spring Cloud Config 提供了一套集中式配置管理方案。它由两部分组成:

  • Config Server: 负责从 Git、SVN、本地文件系统等配置仓库读取配置并对外暴露
  • Config Client: 启动时从远端拉取配置,并据此初始化自身应用

  • 更直观地说,就是把配置文件放到统一位置管理,例如 Git 仓库
  • 服务启动时不再只依赖本地 application.yml,而是可以先到配置中心取配置

Config配置中心结构.png

Spring Cloud Config 的默认仓库实现通常使用 Git,也支持 SVN 或本地 native 模式。

  • 支持配置内容的加密与解密
  • 如果希望配置修改后尽量少重启服务,通常会配合 Spring Cloud Bus 做动态刷新
  • 在传统 Spring Cloud 配置模型里,bootstrap.yml 早于 application.yml 加载,常用于配置配置中心地址等“启动早期参数”
  • 在较新的 Spring Boot / Spring Cloud 版本中,外部配置加载方式已经逐步演进,实际项目里还会看到 spring.config.import 等新写法

Spring Cloud Bus 事件总线

如果直接修改远程配置仓库中的配置文件,已经启动的应用并不会自动感知这些变化。

在简单场景里,可以通过 Webhook 触发刷新流程;但在生产环境中,更常见的方式是通过 Spring Cloud Bus + 消息中间件 把刷新事件广播到整个集群。

Bus 的重点不在于“拉配置”,而在于“传播事件”。

Spring Cloud Bus 用于将服务和服务实例与分布式消息系统连接起来,在集群中传播状态变化事件,例如配置刷新事件。

sequenceDiagram
    participant Admin as 运维操作
    participant Repo as 配置仓库
    participant Bus as Spring Cloud Bus
    participant A as 服务实例A
    participant B as 服务实例B

    Admin->>Repo: 提交配置变更
    Admin->>Bus: 触发 busrefresh
    Bus-->>A: 广播刷新事件
    Bus-->>B: 广播刷新事件
    A->>Repo: 重新拉取配置
    B->>Repo: 重新拉取配置

因此,Bus 更适合被理解为框架层的事件广播机制。配置刷新只是其中最典型的一个应用。

如果某个 Bean 需要在配置变化后重新注入属性,常见做法是在相关 Bean 上使用 @RefreshScope

bus事件总线动态修改配置.png


Spring Cloud Gateway 网关服务

前面已经讲过 Zuul,但如果是现在的 Spring Cloud 项目,实际更常见的网关组件往往是 Spring Cloud Gateway

可以把它理解成:
Spring Cloud 生态里更现代的一代网关实现。

它主要解决的问题和 Zuul 一样,依然是:

  • 统一入口
  • 统一路由
  • 统一鉴权
  • 统一限流
  • 统一日志与监控

但 Spring Cloud Gateway 的设计更贴近现在的 Spring 技术栈,它基于 Spring WebFlux 和响应式编程模型实现,更适合承担高并发场景下的网关职责。

Gateway 的核心概念

Spring Cloud Gateway 的核心可以拆成三部分:

  • Route(路由): 请求最终要转发到哪里
  • Predicate(断言): 什么样的请求命中这条路由
  • Filter(过滤器): 在请求和响应的前后做什么处理

如果用一句话概括,就是:

先用 Predicate 判断“这个请求该不该走这条路”,再用 Filter 决定“转发前后还要做哪些操作”。

Gateway 能做什么

它在工程里的常见用途包括:

  • 根据路径、请求头、方法、参数进行路由匹配
  • 给请求统一加鉴权、签名、日志、灰度标记
  • 统一做限流、熔断、重试、降级
  • 配合注册中心实现基于服务名的动态路由
  • 对外隐藏内部微服务真实地址

一个最常见的路由例子

1
2
3
4
5
6
7
8
9
10
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1

这段配置表示:

  • 当请求路径匹配 /api/order/**
  • Gateway 就把请求转发给 order-service
  • lb:// 表示通过注册中心拿到服务实例,并结合负载均衡进行转发
  • StripPrefix=1 表示转发前把前缀 /api 去掉

Gateway 和 Zuul 的关系

两者关系可以概括为:

  • Zuul: Spring Cloud Netflix 时代常见的网关方案
  • Gateway: 现在 Spring Cloud 生态里更常见的网关方案

如果是老项目,依然可能看到 Zuul;
如果是新项目,很多时候会优先考虑 Gateway。

一句话总结

Gateway 本质上就是微服务系统的统一入口,它把原本分散在各个服务里的通用逻辑,前移到了网关层统一处理。


Sentinel 流量治理与熔断降级

前面讲 Hystrix 时,重点是服务熔断和降级。
在当前很多 Spring Cloud Alibaba 项目中,更常见的流量治理组件往往是 Sentinel

Sentinel 是阿里开源的流量治理组件,在 Spring Cloud Alibaba 生态里使用非常普遍。

Sentinel 解决什么问题

微服务系统一旦遇到下面这些情况,就很容易出问题:

  • 突发流量过大
  • 某个下游服务响应变慢
  • 某个热点接口被打爆
  • 整体系统负载过高

这些问题如果没有保护机制,就会逐步演变成:

  • 接口超时
  • 线程池打满
  • 服务雪崩
  • 整个调用链被拖垮

Sentinel 的核心价值,就是:

从“流量”这个入口出发,对系统做保护。

Sentinel 常见能力

它最常见的能力有:

  • 流量控制: 限制某个接口或资源的 QPS、线程数
  • 熔断降级: 当异常比例、响应时间达到阈值时,自动触发保护
  • 热点参数限流: 比如某个热点商品 ID、热点用户 ID 的请求特别多时单独限流
  • 系统保护: 根据系统负载、CPU、入口流量等维度做整体保护
  • 实时监控: 配合 Sentinel Dashboard 观察资源调用情况和规则命中情况

怎么理解“资源”和“规则”

学习 Sentinel 时,有两个词一定要搞清楚:

  • 资源: 需要被保护的对象,例如一个接口、一个方法、一次远程调用
  • 规则: 对这个资源施加的保护条件,例如 QPS 超过 100 就触发限流

也就是说,Sentinel 的思路是:

先标记资源,再为资源配置规则。

一个简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SentinelResource(
    value = "createOrder",
    blockHandler = "handleBlock",
    fallback = "handleFallback"
)
public String createOrder(Long productId) {
    return "下单成功";
}

public String handleBlock(Long productId, com.alibaba.csp.sentinel.slots.block.BlockException ex) {
    return "请求过多,请稍后再试";
}

public String handleFallback(Long productId, Throwable ex) {
    return "系统繁忙,请稍后重试";
}

这段代码里:

  • createOrder 是被保护的资源
  • blockHandler 用来处理被限流、被熔断这类 Sentinel 规则触发的场景
  • fallback 用来处理业务执行过程中抛出的异常

Sentinel 和 Hystrix 怎么理解

学习时可以这样对应:

  • Hystrix: 更偏“服务容错”这个角度
  • Sentinel: 更偏“流量治理 + 服务保护”这个角度

Sentinel 也支持熔断降级,但它比 Hystrix 更强调:

  • 流量控制
  • 热点防护
  • 系统自适应保护
  • 控制台实时观察与动态规则配置

因此在很多 Spring Cloud Alibaba 项目里,经常会看到这样的组合:

  • OpenFeign 负责远程调用
  • Gateway 负责统一入口
  • Sentinel 负责限流、熔断、降级和系统保护

一句话总结

Hystrix 更像“故障发生后的保护器”,而 Sentinel 更像“在流量入口就提前做治理和保护的守门员”。


Spring Cloud Sleuth / Zipkin 链路追踪

微服务拆分后,调用链会变长。一次请求可能会经过:

用户请求 -> 网关 -> 订单服务 -> 库存服务 -> 支付服务 -> 消息队列 -> 通知服务

此时最典型的问题包括:

  • 某次请求到底经过了哪些服务
  • 哪一跳最慢
  • 错误是在哪个服务里发生的
  • 日志应该怎么串起来看

这时就需要 分布式链路追踪

什么是 Trace 和 Span

链路追踪里最核心的两个概念是:

  • Trace: 一次完整请求的全链路
  • Span: 链路中的一个具体操作单元

可以把它理解成:

  • 一次用户下单是一个 Trace
  • 订单服务查库存、调用支付、发送消息,这些每一步都是一个 Span

Sleuth 和 Zipkin 分别干什么

  • Spring Cloud Sleuth: 负责在应用内部自动埋点,并把 traceIdspanId 传递下去
  • Zipkin: 负责收集、存储、展示这些调用链数据

所以它们通常是配合使用的:

  • Sleuth 负责“产生链路数据”
  • Zipkin 负责“查看链路数据”

它最直观的价值是什么

引入链路追踪后,直接得到的能力包括:

  • 一个请求在多个服务之间都有统一的 traceId
  • 日志可以按同一个请求串起来看
  • 可以看到每一跳的耗时
  • 可以定位性能瓶颈和异常节点

例如日志里看到:

1
2
3
[order-service,traceId=8f3a...,spanId=1ab2...]
[inventory-service,traceId=8f3a...,spanId=8cd1...]
[payment-service,traceId=8f3a...,spanId=9ef0...]

就可以判断这几条日志属于同一次请求。

需要注意的一点

在较新的 Spring 生态里,链路追踪能力已经逐步向 Micrometer Tracing / OpenTelemetry 方向演进。
不过在学习 Spring Cloud 微服务体系时,Sleuth + Zipkin 依然是理解“链路追踪到底在做什么”的非常经典的一套组合。

一句话总结

链路追踪的本质,就是把一次跨多个服务的请求重新串成一条完整可观察的调用链。


Spring Cloud Stream 消息驱动微服务

前面讲的很多内容,都是同步调用视角下的微服务协作。
但在真实系统里,不少场景更适合用异步消息来解耦。

比如:

  • 下单成功后通知库存扣减
  • 支付成功后发送短信或邮件
  • 大促场景下做削峰填谷
  • 业务事件广播给多个下游系统

这时就会用到消息中间件,比如 KafkaRabbitMQ。而 Spring Cloud Stream 的作用,就是:

把应用和底层消息中间件之间做一层抽象。

为什么需要 Stream

如果直接操作 Kafka、RabbitMQ 的原生 API,开发时往往要关心很多细节:

  • 连接和客户端配置
  • 消费者组
  • Topic / Exchange / Queue
  • 消息序列化和反序列化
  • 重试、分区、失败处理

Spring Cloud Stream 通过 Binder(绑定器)抽象,把这些差异屏蔽在业务代码之外。

可以把它概括为:

  • 业务代码只关心“我发什么消息、我收什么消息”
  • Binder 负责把这些消息对接到具体的 Kafka 或 RabbitMQ

Stream 的核心概念

最重要的几个概念是:

  • Binder: 对接具体消息中间件的适配层
  • Binding: 应用和消息中间件之间的绑定关系
  • Producer: 生产消息的一方
  • Consumer: 消费消息的一方

一个函数式风格的简单例子

1
2
3
4
@Bean
public Consumer<String> orderPaidConsumer() {
    return message -> System.out.println("收到支付成功消息:" + message);
}

如果配合配置:

1
2
3
4
5
6
7
8
spring:
  cloud:
    function:
      definition: orderPaidConsumer
    stream:
      bindings:
        orderPaidConsumer-in-0:
          destination: order-paid-topic

那么应用就会订阅 order-paid-topic,并把消息交给 orderPaidConsumer 处理。

Stream 的价值

它在工程上的好处主要是:

  • 让业务代码和具体 MQ 实现解耦
  • 更容易在不同中间件之间切换
  • 更适合事件驱动、异步削峰、最终一致性的场景

Bus 和 Stream 的关系

很多人会把 Spring Cloud BusSpring Cloud Stream 混在一起,其实它们关注点不一样:

  • Spring Cloud Stream: 面向业务消息流
  • Spring Cloud Bus: 面向服务之间的事件广播,比如配置刷新

可以理解成:

  • Stream 更像“业务层消息编程模型”
  • Bus 更像“框架层事件传播机制”

一句话总结

Spring Cloud Stream 不是一个消息队列本身,而是一层把业务代码和消息中间件解耦的抽象。


Spring Cloud 全家桶常见组合

放到实际项目里,常见组合大致可以分成两类。

经典 Netflix 组合

  • Eureka:注册中心
  • Ribbon:客户端负载均衡
  • Feign:服务调用
  • Hystrix:熔断降级
  • Zuul:网关
  • Config + Bus:配置中心与动态刷新

这套组合非常适合理解 Spring Cloud 微服务的基本思想,也是很多老项目的真实现状。

现在更常见的组合

  • Nacos:注册中心 + 配置中心
  • Gateway:统一网关入口
  • OpenFeign:声明式服务调用
  • Sentinel:流量治理、限流、熔断、降级
  • Sleuth/ZipkinMicrometer Tracing:链路追踪
  • Stream:消息驱动微服务

如果再往前走一步,很多项目还会继续引入:

  • Seata:分布式事务
  • RocketMQ / Kafka:异步解耦与削峰
  • Kubernetes:容器编排与服务治理

按职责记忆

整套 Spring Cloud 全家桶可以按职责拆成几个维度来看:

  • 服务发现: Eureka、Nacos
  • 服务调用: Ribbon、Feign、OpenFeign
  • 网关层: Zuul、Gateway
  • 服务保护: Hystrix、Sentinel、Resilience4j
  • 配置管理: Config、Nacos、Bus
  • 可观测性: Sleuth、Zipkin、Micrometer Tracing
  • 消息驱动: Stream

这样记,会比死背组件名字更容易建立整体认知。