微服务初探
Chapter 0. 基本概念
- 微服务是一种软件架构风格。专注于单一职责的小型业务为基础,组成复杂大型应用。
- 需要解决的问题:服务拆分、远程调用(RPC)、服务治理(可用性与调度)、请求路由、身份认证、配置管 理、分布式事务(一致性问题)、异步通信……
- 优点和特征:粒度小、单服务开发便捷,团队自治,服务自治,系统耦合性低;
- 缺点:跨模块开发难度大,运维成本高;
对比而言,单体架构:
- 优点:架构简单、部署成本低(适用于开发功能相对简单、规模较小的项目);
- 缺点:团队协作成本高,系统发布效率低、系统可用性差(软件可靠性差);
- 对应框架:Spring Cloud,全球范围广泛使用的微服务框架;
- 服务注册发现组件:Eureka、Nacos、Consul……
- 服务远程调用(RPC);OpenFeign、Dubbo……
- 服务链路监控:Zipkin、Sleuth……
- 统一配置管理:Spring Cloud Config、Nacos……
- 统一网关路由:Spring Cloud Gateway、Zuul……
- 流控、降级、保护:Hystix、Sentinel……
Chapter 1. 基于实践学习:将单体架构拆分
1.1 基本思路
- 拆分时机:
- 创业项目:先使用单体架构,快速开发、试错;规模扩大后再拆分;
- 确定的大型项目:资金充足,目标明确,直接选择微服务架构;
- 拆分原则:
- 高内聚:每个微服务的职责尽量单一,包含的业务相互关联度高、完整度高;
- 低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖;
- 拆分方式:
- 纵向拆分:按业务模块拆分(用户认证管理、订单管理……);
- 横向拆分:抽取公共服务,提高复用性(用户登录、订单管理中有公共服务,例如风控、日志服务);
具体如何拆分何种模式?
独立 Project:一个单体架构的项目拆分后,将每个 service 放在一个独立 Project 内;
优点:相当解耦,关联度更低;
缺点:对于小型项目而言仓库管理臃肿费力;
适用:大型项目,每个 service 的业务逻辑相当复杂;
Maven 聚合:一个单体项目拆分后,所有 service 都存放在一个 project 中,这个 project 专门用于管理依赖,每个 service 都是 project 的一个 module;
适用:中小型项目,敏捷开发;
1.2 远程调用 RPC:以 Spring RestTemplate
为例
拆分的问题:经常会遇到一个服务依赖另一个服务的情况。这可以直接通过服务间远程调用(RPC)来完成。
远程调用 RPC 更像是一种软件协议,能让一个程序和本地执行流程一样,在远端执行一段代码。
在软件工程原理的 23 个设计模式中,更像是利用了 Proxy 设计模式。调用者无需关心远程信息交互的具体细节,只需按照接口像调用本地服务一样即可完成目标。
在 Spring 中,一种轻量级的远程调用的方法是,使用接口:RestTemplate
,调用它就相当于在 Java 代码中向 RESTful API 发送网络请求并且获取回复。
1 | public <T> ResponseEntity<T> exchange( |
规避类型擦除:
new ParameterizedTypeReference<T>() {}
;
但是有一些不可避免的问题:
- 若代码中写死
url
,则 负载均衡、可用性轮换等等策略全部失效; - 若业务逻辑中的远程调用的部分较多,则
RestTemplate
对象散落各处不方便维护; - ……
于是我们需要更严格、复杂的架构将服务间的远程调用管理起来。一种思路就是 “发布-订阅者模式”;
1.3 微服务的注册与发现机制
1.3.1 发布-订阅模式(架构)
发布-订阅者模式(pub-sub pattern);
The Pub/Sub (Publisher/Subscriber) model is a messaging pattern used in software architecture to facilitate asynchronous communication between different components or systems. In this model, publishers produce messages that are then consumed by subscribers.
发布-订阅模型,作为一种消息传递模式,用在一些软件架构中,来实现不同组件、系统间的异步通信。
subscribers:消息的获取方,publishers:消息生产方;
topics:一种订阅频道 / 分类(channels / categories);
发布者可以按照消息的业务含义为消息打上话题标签(并且无需知道订阅者的特性),而订阅者可以按照 topics 来订阅、获取部分感兴趣的消息(并且无需知道发布者的特性);
Topics help categorize messages and enable targeted message delivery to interested subscribers.
message brokers:一种中间件/中间人(intermediaries),负责在 publishers 和 subscribers 间使用合适的策略传递消息(调度队列、延时……);
- message brokers 可以按 topics 维护一些消息队列;
- 确保消息发送给正确的订阅者,并且提供额外的特性:消息持久化、可扩展性、可靠性;
message:可以是任何形式的数据信息(text, JSON, binary, etc);
subscription:订阅代表了 subscriber 和某个 topics 建立的一个关联。它定义了:
- 订阅者订阅了什么 topic 的消息;
- 订阅设置,例如是否事务、是否有订阅内容保证(at-most-once / at-least-once)等等;
发布-订阅模型的理论过程:
- 发布者创建并发送消息给 pub/sub system,并且根据消息内容类型放入指定 topic 或者说 channel 中;
- 订阅者向 pub/sub system 表达需要订阅某个/些 topic 的意愿,有信息就会收到;
message brokers 根据收到发布者的 topic 对收到的消息分类,再根据所有 topics 的订阅情况 forward 给所有订阅了这个 topic 的 subscribers;
以上过程全部异步,发布者不需要关心订阅者的状态即可发布;订阅者无需关心发布者的状态即可接受消息。
发布-订阅模型的优势:
- decoupling & scalability:将消息生产方和消息接受方解耦,不仅无需关心对方状态和交互细节,而且 scalable,便捷地增减 publishers 和 subscribers 的数量而无需影响现有组件;
- asynchronous communication:异步通信能力;
- event-driven architecture:发布者和订阅者互不耦合,但发布者的事件行为可以影响订阅者;
- dynamic subscription:允许运行时动态更换订阅,去耦合,全面的灵活性;
发布-订阅模型的适用场景:
- 消息队列系统;
- 需要构建 scalable web app 的时候,尤其是在线文档、实时更新的场景;
- 微服务架构间的远程通信;
- ……
发布-订阅模型不应该使用的场景:
- 对交互时延有极强需求的场景,例如游戏;
- 低复杂度的交互场景,例如系统只有两个组件间的交互,贸然引入会增大不必要的复杂度;
- ……
回到微服务远程调用的主题上。为了确保服务远程调用的灵活性、可用性,我们可以借鉴 发布-订阅者模式,通过注册、发现、订阅的流程,动态调度服务的访问方式。
这样既可以有效地、统一地管理远程访问,提升可维护性,又可以便捷地进行调度,充分利用服务资源。
1.3.2 注册中心
在微服务架构中,规避微服务间直接远程调用缺陷的一种方式就是引入注册中心机制,借鉴发布-订阅模式,引入注册中心后的主要步骤如下:
- 服务发布者向注册中心注册服务信息(提供何种服务,即 topic,还有地址在哪里);
- 服务订阅者向注册中心订阅感兴趣的服务。此时注册中心可以将当前可用的发布者信息告诉订阅者;
- 订阅者(或者注册中心)可以进行负载均衡,选择一个发布者向其请求服务(远程调用)。
由于我们利用了发布-订阅模式,所以即便是已经获取服务列表的订阅者,也能从注册中心实时获取当前发布者的可用情况。例如:
假设订阅者从注册中心获取了 3 个可能的服务发布者,但是一段时间后其中一个服务提供方 A 宕机。
这个时候 A 不再通过注册中心的 health check(heart beat),注册中心认为 A 不再有效,于是向所有订阅了 A 服务所在 topic 的所有订阅者推送变更。
这就保证了订阅者订阅列表的有效性。创建了新的服务实例也是如此!实现了 scalable service,随意增减服务实例数量、负载均衡。
在代码方面,我们知道在 Spring Cloud 中,注册中心有很多实现,例如 Alibaba 的 Nacos,Netflix 的 Eureka。我们就以其中的 Nacos 为例。我们只需要将注册中心部署在固定 IP 的服务器上即可。
配置好 Nacos,在注册服务客户端(也就是提供服务方)引入 nacos 注册发现服务,还需要对 Spring 进行配置,指定 registry server 的地址和需要的服务名。最后启动这个服务实例即可完成注册!
而在服务调用端,需要在项目中引入 Nacos Client,它实现了 Spring Cloud 的 DiscoveryClient
接口。
我们直接注入 DiscoverClient
,使用如下方法:
DiscoverClient.getInstances(String serviceName) -> List<ServiceInstance>
,serviceName
是服务提供方在 Nacos 中注册的服务名;- 而我们可以通过
ServiceInstance
获取uri / host / port
信息,手动写负载均衡或其他处理工作。
注:Nacos 需要导入依赖
spring-cloud-starter-alibaba-nacos-discovery
,并且进行配置:
1
2
3 spring:
cloud:
nacos.server-addr: ...
1.3.3 更优雅的远程调用:OpenFeign
初步认识 OpenFeign
但是这里有个问题,我们通过 DiscoverClient
获取可用服务列表,然后再处理一系列可能的异常,然后还要手写 RestTemplate
进行远程调用,最后才能访问服务!
如此繁琐的远程调用,我们应该进行封装!好在同样有框架能够更轻松地帮助我们完成远程调用:OpenFeign;
注:我们还要对负载均衡算法进行封装。使用 Spring cloud load balancer 就能解决问题。
引入 OpenFeign 的方法如下:
引入 OpenFeign 依赖;
在 Springboot Application 启动类注解
@EnableFeignClients
启用 OpenFeign 的特性;定义接口,用于指定要远程调用的服务名、远程调用服务名的 endpoint。举个例子:
1
2
3
4
5
6
7
8/* 需要向注册中心申请的服务名 */
public interface ItemClient {
/* 告诉 OpenFeign 需要连接远程服务的 endpoint 相当于指定 RestTemplate 的 uri 和 HttpMethod */
/* 告诉 OpenFeign 的传入参数、请求体信息,以及服务返回 JSON 对应的类型,相当于指定 RestTemplate 的 RequestEntity,responseType,uriVariables */
List<ItemDto> queryItemByIds(; Collection<Long> ids)
}
这样相当于实现了 proxy 设计模式(这个 interface 对应 Spring 生成的实例就是 proxy),在调用远程服务就像调用本地服务一样简单!
OpenFeign 连接池优化
OpenFeign 的底层实现非常类似我们手动使用 DiscoverClient
,并且考虑负载均衡、服务异常的情况。
感兴趣的同学可以步进调试观察其底层行为。
值得注意的是,OpenFeign 底层默认的远程调用的方式是利用 HttpURLConnection
类(位于 FeignBlockingLoadBalancerClient
的 delegate
成员变量),而这个 Java 内置类比较底层,不支持连接池,不像 apache 的 HttpClient
和 OkHttp
,可能造成资源利用率较低。
好在我们可以为 OpenFeign 的底层请求更换具有连接池的连接类,这样可以减小创建和销毁连接的开销,有助于提升程序性能。只需导入依赖相应依赖(例如 feign-okhttp
),并且在 application.yaml/properties
中配置开启即可(例如 feign.okhttp.enabled = true
);
注意,是在 OpenFeign 调用方模块设定才有效!
OpenFeign 最佳实践
到这里仍然没有结束,我们还需要知道 OpenFeign 框架的最佳实践。
上面介绍引入 OpenFeign 的方法实际上是有严重问题的。我们发现,当单体架构的程序被拆成微服务后,有可能多个服务依赖同一个微服务,难不成所有用到的微服务都要定义一遍像 ItemClient
一样的接口?
我们知道,代码重复是大忌,这不仅降低了可维护性(增添维护人员心智负担)、可扩展性(修改繁琐,不对修改开发),还降低了代码可读性和正确性。
正确使用 OpenFeign 的方案有几种:
直接交给服务提供方编写 Feign Client,将服务提供方拆成 3 个模块:
DTO
中包含向服务调用方返回的数据类型;api
中包含FeignClient
接口以供调用;blz
中包含原先的代码业务逻辑;
最后其他服务只需引用该服务为依赖即可。
优点:代码逻辑更合理,易于维护,耦合度更低;
缺点:项目结构更复杂、编写麻烦;
创建一个不属于任何一个微服务模块的、同级的
api
module,专门管理FeignClient
,主要包含:client
:所有微服务想要向外暴露的FeignClient
;config
:所有微服务的配置类;dto
:所有微服务想要返回的数据类型;
优点:项目结构清晰,无需改动原先微服务;
缺点:代码耦合度增加,每个微服务模块都需要引入该模块为依赖(而前一种方法只需引入需要用的模块就行),所有微服务都需要和
api
一起开发;但是如果以这种方式创建
FeignClient
,没法完成依赖注入(因为 Spring Application 没法自主扫描本包以外的 bean),就需要手动指定接口类型,并将其纳入 IoC Container 中。两种方法:1
2
3
4
5
6/* 在 SpringApplication 启动类启用 FeignClient 时显示指定 bean 类型 */
/* 方法一:为 Spring 指定 bean 扫描的包名(精确到子包) */
/* 方法二:利用反射手动指定 bean 对应接口的 Class 类型 */
OpenFeign 日志管理
OpenFeign 框架默认情况下仅在 FeignClient
所在包配置的日志级别为 DEBUG
时才会输出日志,并且自身的日志级别是 NONE
(不输出),故需要我们手动配置。
注:OpenFeign 自身的日志级别有 4 种:
NONE / BASIC / HEADERS / FULL
;
定义 OpenFeign 的日志级别需要完成两件事:
定义配置类,例如:
1
2
3
4
5
6
7/* 没有 @Configuration / @Service 等注解,该 @Bean 不会被纳入 IoC Container */
public class FeignConfig {
public Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
}将配置类配置给指定 / 全局的
FeignClient
:1
2
3
4
5/* 指定 FeignClient */
/* 全局 FeignClient 默认 */
1.4 微服务网关
在大致拆好微服务后,有个问题就随之出现:前端应该如何访问后端服务?难道前端还需要动态获取各个服务的地址?
肯定不能这样,我们的期望是前后端的解耦,就是说单体架构和微服务架构下,前端是无需改变的,只需要向固定的地址发送不同请求就能得到对应的响应,这就需要一个中间层来完成这个任务。
这个能将不同服务转发给某个符合条件的微服务的中间层就是网关。网关不仅能完成前面的 forward 的功能,还能配合注册中心进行负载均衡。
功能:
- 请求路由(路径针对什么微服务?);
- 转发(帮忙将 HTTP 请求 forward 给某个动态地址的实例);
- 身份校验(检查请求的
Authorization
是否合法);
这样,我们就不需要在每个微服务中进行身份校验等繁琐工作了。更安全的是,后端微服务甚至不需要向外暴露端口了,只需暴露网关,大大增强安全性。
此外,引入网关后,后端实现了封装和解耦,在前端看来这和单体架构别无二致。
1.4.1 微服务网关框架:Spring Cloud Gateway
Spring Cloud Gateway | Netflix Zuul |
---|---|
基于 WebFlux 响应式编程 |
基于 Servlet 阻塞式编程 |
无需调优即有很好性能 | 需要调优才有接近Spring Cloud Gateway 的性能 |
正常维护 | 更新较慢 |
基于上述特点,我们以 Spring Cloud Gateway 为例。
它的使用很简单:导入依赖、编写启动类、编写配置。
Spring Cloud Gateway 配置示例
1 | spring: |
众所周知,application.yaml
中的配置内容相当于 XML Bean,都在向 Spring 框架的类型中填写初始化属性罢了。这里 spring.cloud.gateway.routes
对应的是 Collection<RouteDefinition>
类型。
其中,predicates
属性可取以下值:
Name | Description | Example |
---|---|---|
After | 是某个时间点后的请求 | After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | Method=GET,POST |
Path | 请求路径必须符合指定规则 | Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | Query=name, Jack 或者 Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 | Weight=group1, 2 |
X-Forwarded Remote Addr | 基于请求的来源IP做判断 | XForwardedRemoteAddr=192.168.1.1/24 |
filters
可取以下值:
Name | Description | Example | |
---|---|---|---|
AddRequestHeader | 给当前请求添加一个请求头 | AddrequestHeader=headerName,headerValue | |
RemoveRequestHeader | 移除请求中的一个请求头 | RemoveRequestHeader=headerName | |
AddResponseHeader | 给响应结果中添加一个响应头 | AddResponseHeader=headerName,headerValue | |
RemoveResponseHeader | 从响应结果中移除有一个响应头 | RemoveResponseHeader=headerName | |
RewritePath | 请求路径重写(ant path 语法) | RewritePath=/red/?(?\<segment\ | .*), /\\{segment} |
StripPrefix | 去除请求路径中的N段前缀 | StripPrefix=1,则路径/a/b转发时只保留/b | |
…… | …… | …… |
一共有 33 种,详见官网。
值得注意的是,filter 是基于 router 生效的、作用于 router 的。Spring Cloud 中的 filter 分为两种:
- GlobalFilter:全局过滤器,作用于所有路由,在声明该过滤器后无需激活即可生效;
- GatewayFilter:路由过滤器,共 33 种,作用于指定路由,默认不生效。上面的在配置文件中写的
filters
就是这个过滤器!
Spring Cloud Gateway 过滤机制 以及 Spring Security 对比
这里只介绍了网关 forward 的配置,那么如果想对网关做更进一步的配置(例如身份验证),应该怎么操作?
首先需要了解 Spring Cloud Gateway 的底层机制。
这里的思路几乎和 Spring Security 的过滤器链一模一样(责任链模式)。
因此我们想加入身份验证功能,就需要在 Filter 的 PRE
部分定义检查逻辑。如果符合条件,则允许通过 NettyRoutingFilter
进行转发;否则抛出异常立即拒绝请求。
现在做身份验证的思路就非常清楚了:自定义一个 Filter 类,最好像 Spring Security 的 OncePerRequestFilter
一样每次请求仅通过一次,插入到 NettyRoutingFilter
之前,就能完成任务。
但是有几点和之前的微服务架构截然不同!!!
考虑第一点:网关如何向微服务传递当前登录用户的信息?
注意到现在网关、各个微服务都是独立的服务了,和单体架构不同,我们不能通过保存在类似于 SecurityContext
这样的单线程上下文(ThreadLocal
)中,把网关中检查的用户信息传给 forward 的目标服务了。
回忆一下,网关向微服务 forward 实际上已经是一次新的 HTTP 请求了,而且我们之前在 Gateway 的 filters 参数中看到,gateway 可以配置额外添加请求头信息。因此不难想到,我们可以通过在网关的自定义 filter 中加入关于用户信息的请求头就能解决这个问题!
但再考虑第二个问题:既然网关和微服务间通过传递请求头来完成用户信息传递,那微服务之间相互调用也很频繁,它们默认还是原来的请求方式(Nacos + OpenFeign
),没有请求头,难道要更改原来的代码,每个微服务请求 Client Proxy 时还要主动记录和传递用户信息?
是的!不过幸好 OpenFeign
有另一套方法帮我们加上这个请求头,所以不必担心。
下面我们来认识一下如何自定义 filter,并且完成身份认证的功能。
自定义 Global Filter
1 | public interface GlobalFilter { |
还需要注意的是:
- global filter 需要继承于
Ordered
接口; - 并且重写
int getOrder()
方法,这样能为 filter 安排插入顺序。注意:返回的整型越小优先级越高,并且NettyRoutingFilter
的 order 是最大整型,因此任意一个不是INT_MAX
的 order 都会让 filter 排在它的前面; - 最后需要使用
@Component
纳入 Spring IoC Container 管理; - 最后还要选一个配置类,使用
@Bean
提供一个GlobalFilter
实例!!
自定义 Gateway Filter
这就是我们自定义写在 filters
配置文件中的 filter。本部分为进阶功能,一般使用不到。
我们需要继承于 AbstractGatewayFilterFactory<Object>
:
1 |
|
上面的 Config
类型可以换成任何的自定义类型,以完成参数配置需求。如果不需要任何参数,直接使用 Object
类型,后面 3 个函数也就没有必要了。
这样前缀 PrintAny
(去除 GatewayFilterFactory
)就是配置名,我们就能自定义 filters 参数了:
1 | spring: |
此外,对于 GatewayFilter
如果想指定顺序,请使用 OrderedGatewayFilter
包装:
1 |
|
总结一下,GatewayFilter
的定义使用了抽象工厂模式,满足了多样化定制需求。
Spring Cloud Gateway 传递用户信息
现在我们来解决之前提到的两个问题:
- 网关如何向微服务传递当前登录用户的信息?
- 微服务之间相互调用如何使用
OpenFeign
传递用户信息?
对第一个问题,我们采用如下思路:
这样我们微服务中的业务在本微服务内就沿用之前的 context 方案,无需更改,只需要修改进入微服务的拦截器即可;
先看如何更改 GlobalFilter
的请求内容:
1 | /* ServerWebExchange.mutate() 方法可以返回已初始化的对象的 builder 以供修改 */ |
对于新建拦截器,我们不必在每个微服务中都写一遍,只需在共同依赖的模块中写入即可。
1 | public class UserInfoInterceptor implements HandlerInterceptor { |
记得把 Interceptor 配置到 Spring 中,在每次请求前进行:
1 |
|
但是还需要注意,这里的 Bean 现在没法被其他用到的模块扫描到,我们需要在这个模块的 resources/META_INF/spring.factories
中,配置:
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
这样使得在该项目中, Spring 默认会将这个类加入 IoC Container。
注意:在 Spring 3.x 以后,已经全面取消
spring.factories
中的org.springframework.boot.autoconfigure.EnableAutoConfiguration
键的作用了。在 Spring 3.x 的项目中,上面的配置是无效的,Spring 是不会扫描并且自动装配你指定的类的。
你需要在
META-INT/spring/
目录下新建一个文件 ``
但是需要注意,这里采用的 WebMvcConfigurer
只能在微服务中生效,不能在 Gateway 中生效(因为 Gateway 是 WebFlux
非阻塞式接口,不能引入 Spring MVC 的接口),所以我们需要条件装配:
1 | /* 在 MvcConfig 前面的加入注解 */ |
对于第二个问题,我们只需要对 OpenFeign
的请求进行定义:让每次 OpenFeign
触发微服务间调用时,都带上一个自定义的请求头,就像网关传给微服务一样。这里使用 OpenFeign
给的接口:
1 | public interface RequestInterceptor { |
这同样要定义在所有微服务中都依赖的模块中(因为是配置,所以用 bean 注入):
1 | /* 在某个配置 Feign 的类中 */ |
当然,这个配置类需要显式配置给 OpenFeign
(就像之前配置 Feign 日志的全局配置一样):
1 |
Chapter 2. 微服务理论
2.1 微服务雪崩
在微服务相互调用中,服务提供者出现故障或阻塞。并且:
- 服务调用者没有做好异常处理,导致自身故障;
- 或者访问连接一直保持 / 请求速度大于处理速率,致使请求不断堆积在 tomcat 中导致资源耗尽;
最终,调用链中的所有服务级联失败,导致整个集群故障。
解决微服务雪崩的思路主要如下:
- 尝试避免出现故障 / 阻塞;
- 保证代码的健壮性;
- 保证网络畅通;
- 能应对较高的并发请求;
- 微服务保护:保护服务提供方;
- 局部出现故障 / 阻塞后,及时做好预备方案(积极有效的错误处理);
- 微服务保护:保护服务调用方;
2.2 微服务保护
为了应对微服务雪崩,我们有许多解决方案。其中,微服务保护是在业务逻辑代码层面以外的一种重要方案。
微服务保护有以下一些思路:
请求限流:保护服务提供方。限制访问微服务的请求的并发量,避免服务因流量激增出现故障(应对访问模式:spike 型);
线程隔离(舱壁模式):保护服务消费方。通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散;
快速失败 和 服务熔断:
- 快速失败:给业务编写一个调用失败时的处理的逻辑,称为
fallback
。当调用出现故障(比如无线程可用)时,按照失败处理逻辑执行业务并返回,而不是直接抛出异常; - 由断路器统计请求的异常比例或慢调用比例,如果超出阈值,则认为某个微服务业务所对应的所有实例都不可用,熔断该业务,则拦截该接口的请求。熔断期间,所有请求均
fallback
为快速失败逻辑;
- 快速失败:给业务编写一个调用失败时的处理的逻辑,称为
以上微服务保护的策略可以使用 Sentinel
/ Hystrix
框架完成。
Metrics or Feature | Sentinel |
Hystrix |
---|---|---|
Belong to | Spring Cloud Alibaba | Spring Cloud Netflix |
Thread Isolation | 信号量隔离 | 线程池隔离/信号量隔离 |
Fuse Policy | 基于慢调用比例或异常比例 | 基于异常比率 |
Traffic Limiting | 基于 QPS,支持流量整形 | 支持 |
Fallback | 支持 | 支持 |
Configuration Method | 基于控制台,重启后失效 | 基于注解或配置文件,永久生效 |
想了解微服务保护框架具体如何使用,请参见官网样例或官方文档。
2.3 微服务分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务的事务就是一个分支事务。整个业务称为全局事务。
除了微服务雪崩的问题外,微服务设计中还存在一个重难点:如何保证微服务数据 ACID 的性质(如何正确进行分布式事务)。
以一个商品订单服务为例:
如果最终库存服务失败,那么虽然订单服务可能可以识别到错误并且回滚,但是购物车服务与库存服务间没有关系,极有可能不会回滚,造成数据的不一致性。这就是没有保证分布式事务一致性。
那么我们应该如何保证微服务流程的一致性?
这个时候需要引入一个分布式事务的协调组件,让各个子事务(分支事务)感知到彼此的事务状态,根据总体的事务状态进行判断,协调全局事务的提交或回滚,这样就能保证事务状态和数据的一致性。
这个分布式事务的协调组件被称为:事务协调者(Transaction Coordinator,TC);
除了事务协调者,还需要有一个组件,用于定义单个全局事务的范围(从哪个子事务开始,到哪个子事务结束)。这个组件就被称为:事务管理器(Transaction Manager,TM);
有了 TC 和 TM,就能准确地定义一个全局事务;
为了进一步从业务逻辑中解耦,我们额外增添一个组件用于说明某个子事务的事务状态。它的作用是,向 TC 注册子事务,并且报告子事务的事务状态。这就被称为:资源管理器(Resource Manager,RM);