1. SpringCloud Alibaba简介
1.1 为什么会出现SpringCloud Alibaba
Spring Cloud Netflix项目进入到维护模式
什么是维护模式?=> 将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能(我们将修复block级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request)。
进入维护模式意味着?=> SpringCloud Netflix将不再开发新的组件。新组件功能将以其他替代品代替的方法实现。
1.2 SpringCloud Alibaba有什么
官网 诞生于2018.10.31,Spring Clooud Alibaba正式入驻Spring Cloud官方孵化器,并在Maven中央库发布了第一个版本。
能干什么?
功能 | 具体说明 |
---|---|
服务限流与降级 | 默认支持Servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控。 |
服务注册与发现 | 适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持。 |
分布式配置管理 | 支持分布式系统中的外部化配置,配置更改时自动刷新。 |
消息驱动能力 | 基于SpringCloud Stream 为微服务应用构建消息驱动能力。 |
阿里云对象储存 | 阿里云提供的海量,安全,底成本,高可靠的云存储服务。支持在任何时间,任何地点储存和访问任意类型的数据。 |
分布式任务的调度 | 提供秒级,精准,高可靠,高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。 |
使用SpringCloud Alibaba需要在POM文件中引入其依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. Nacos作为服务注册中心
官方文档
2.1 Nacos简介
“Nacos”,前四个字母分别问Naming和Configuration的前两个字母,最后的s为Service,它是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,说白了就是 注册中心 + 配置中心 的组合,等价于SpringCloud之前的 Eureka + Config + Bus,所以Nacos可以替代Eureka做服务中心,可以替代Config做配置中心。
2.2 Nacos安装与运行
在Nacos官网下载地址中下载Nacos,在安装Nacos前需要本地Java8和Maven的环境已经准备好,然后下载其压缩包:
解压该压缩文件后直接运行在bin目录下的startup.cmd启动Nacos:
然后访问 http://localhost:8848/nacos/ 进入Nacos,其默认的账号密码都是nacos,登录成功后即可看到如下界面:
在之前学习服务注册中心的时候讲过CAP理论,即
C | A | P |
---|---|---|
Consistency | Available | Partition tolerance |
强一致性 | 可用性 | 分区容错性 |
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性、可用性、分区容错性。
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。在分布式架构中,P永远要求被保证,所以当前的分布式架构只有AP和CP两种。Nacos属于AP模型,再次对比这些服务注册中心:
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本历史问题) |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP(事实上也可以支持CP) | 支持 | 高 |
据说Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。
2.3 Nacos作为服务注册中心
基于Nacos的服务提供者
新建Module:cloudalibaba-provider-payment9002作为服务提供方微服务。当然为了实现Nacos的负载均衡,使nacos-payment-provider服务集群化,我们同事也仿照9002搭建一个9003微服务。
然后在工程的父POM中引入依赖(Spring Cloud Alibaba简介中引入的依赖)的前提下,在该模块的POM引入依赖Nacos服务注册发现的依赖:
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
然后添加其配置文件application.yml:
server:
port: 9002
spring:
application:
name: nacos-payment-provider # 微服务名称
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*' # 监控端点全部打开
编写其主启动类并在主启动类上添加 @EnableDiscoveryClient
注解,使9002微服务能够被注册中心发现:
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9002.class);
}
}
然后编写一个简单的业务类:
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "Nacos服务注册,端口:" + serverPort + ";id:" + id;
}
}
启动9002、9003微服务模块,在Nacos的服务列表中我们可以看到这两个微服务已经入驻服务注册中心:
点开nacos-payment-provider服务的详情页面,可以看到服务的实例详情:
基于Nacos的服务消费者
新建Module:cloudalibaba-consumer-nacos-order83作为服务消费方微服务,在该模块的POM中同样引入令Nacos服务注册中心发现自己的依赖,配置其配置文件:
server:
port: 83
spring:
application:
name: nacos-order-cosumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 消费者将要去访问的微服务名称(注册成功进Nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
然后编写服务消费方的主启动类,Nacos本身就具有负载均衡功能,因为引入Nacos依赖的同时,Nacos内部集成了Ribbon,如图所示:
而我们在学习Ribbon时知道,用了Ribbon就需要使用 RestTemplate
,所有我们编写配置类,向Spring容器中注入 RestTemplate
:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
注意 注入 RestTemplate
时一定要添加 @LoadBalanced
注解,否则不会开启负载均衡,在服务提供方集群的情况下,由于没有开启负载均衡,消费方会无法选择具体调用哪个微服务实例,也就是服务提供方实例有很多个,而消费方服务不知道要调用哪个具体的服务提供方实例,不加该注解会产生 UnknownHostException
的异常。
然后编写其业务类:
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
//直接读取配置文件中的值,减少代码冗余
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL
+ "/payment/nacos/" + id, String.class);
}
}
启动83服务消费方,在Nacos服务注册中心中我们可以看到入驻了两个服务提供方实例和一个服务消费方实例。
测试
多次访问 http://localhost:83/consumer/payment/nacos/1 ,我们发现服务消费方微服务83可以完成对服务提供方微服务9002/9003的轮询负载均衡调用,也就是 Nacos内部就整合了Ribbon,实现了负载均衡,默认负载均衡算法采用轮询。
2.4 各种服务注册中心的对比
事实上,Nacos可以在AP模式和CP模式中进行切换,也就是说Nacos不仅仅支持AP(可用性和分区容错性),它同样支持CP(一致性和分区容错性)。当 采取入驻到Nacos服务注册中心的微服务对自己的健康状态进行上报时,也就是对入驻到注册中心的微服务进行非持久化的保存,一旦客户端上报不健康信息,就将不健康的实例摘除掉,这类似于Eureka(当然Eureka可以开启自我保护模式);当 采取由Nacos服务注册中心自己探测入驻到中心的微服务是否健康时,也就是对入驻到注册中心的微服务进行持久化保存,即使服务注册中心发现微服务已经不健康了,也不会删除到微服务,这类似于Consul。如下图(CoreDNS也是一种服务注册中心):
Nacos与其他服务注册中心特性的对比:
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性协议 | CP/AP | AP | CP | / | CP |
健康检查 | TCP/HTTP/MySQL/Client Beat(客户端心跳) | Clent Beat | TCP/HTTP/gRPC/cmd | / | Client Beat |
负载均衡 | 权重/DSL/元数据/CMDB | Ribbon | Fabio | RR | / |
雪崩保护 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
自动注销实例 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
访问协议 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
监听支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
多数据中心 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
跨注册中心 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
K8s集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
从理论知识中我们知道Nacos服务注册中心可以在AP和CP模式中进行切换,C是所有节点在同一时间看到的数据是一致的,而A是所有的请求都会受到响应(A可以近似理解为高可用)。
一般来说,如果不需要存储服务级别的信息且服务实例是通过Nacos-Client注册的,并且服务能保持心跳上报,那么就可以选择AP模式,当前主流的微服务框架如SpringCloud和Dubbo,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此 AP模式下只支持注册临时实例。
而如果需要在服务级别编辑或存储配置信息,那么CP是必须的,K8s服务和DNS服务使用于CP模式,CP模式下则支持注册持久化实例,此时以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
如果需要让Nacos服务从AP模式切换到CP模式的话,只需要向服务注册中心发送POST请求即可:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
3. Nacos作为服务配置中心
在用SpringCloud Config结合SpringCloud Bus时,我们可以把配置信息托管到远端如GitHub上,而现在我们应用了Nacos后,可以直接在Nacos上托管配置信息。
3.1 基础配置
新建Module:cloudalibaba-config-nacos-client3377作为服务配置中心微服务,在其POM文件中引入必要的依赖(如Nachos服务注册中心的依赖)为,引入配置中心的依赖:
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
Nacos同SpringCloud Config一样,在项目初始化时,要保证先从配置中心(直接以Nacos作为配置中心)进行配置拉取,拉取配置后,才能保证项目的正常启动,而我们知道SpringBoot配置文件的加载顺序是存在优先级的,bootstrap的优先级要高于application,所以我们创建两个配置文件 bootstrap.yml
(从配置中心拉取配置)和 application.yml
(写自己的配置),我们配置 bootstrap.yml
以使3377服务从Nacos上拉取配置信息:
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
#Nacos服务注册中心地址
server-addr: localhost:8848
config:
#Nacos作为配置中心地址
server-addr: localhost:8848
# 指定yaml格式的配置,也就是说从Nacos上读yaml格式的配置文件
file-extension: yaml
# 配置匹配规则
# ${spring.application.name}-${spring.profile.active}.${file-extension}
然后用 application.yml
定制自己的配置信息,将环境定义为开发环境:
spring:
profiles:
active: dev # 表示开发环境
编写其主启动类后,然后编写其业务类,在业务来上添加SpringCloud的原生注解@RefreshScope
以使服务可以支持从Nacos配置中心动态刷新配置信息:
@RestController
@RefreshScope //支持Nacos的动态刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
在上面的业务层中我们从配置中心读取了配置信息
而我们在Nacos配置中心中添加配置信息,在Nacos配置中心中添加配置文件要遵循一定的匹配规则。 ↓
Nacos中的 dataId
的组成格式及与SpringBoot配置文件中的匹配规则,更详细的信息可以参考Nacos官方文档。
在Nacos SpringCloud中,dataId
的完整格式为:
${prefix}-${spring.profile.active}.${file-extension}
prefix
默认为spring.application.name
(服务名)的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置。spring.profile.active
即为当前环境对应的 profile(上面我们在application.yml
中配置的属性),详情可以参考 Spring Boot文档。 注意:当spring.profile.active
为空时,对应的连接符-
也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
, 建议 不要使spring.profile.active
为空。file-exetension
为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension
来配置。目前只支持properties
和yaml
类型。
由于prefix
默认为 spring.application.name
(服务名)的值,所以 dataId
的完整格式可以替换为:
${spring.application.name}-${spring.profile.active}.${file-extension}
结合我们在 boostrap.yml
配置文件和 application.yml
配置文件的配置,我们可以得到在当前实例中的 dataId
应该为:
nacos-config-client-dev.yaml
根据上述公式得到 dataId
应为的值后,我们就可以在Nacos配置中心的配置列表中新建配置文件。在新建配置文件时,Data ID
中填入我们得到的 dataId
,组名先选择默认的即可,配置格式选择我们在 file-extension
中设置的YAML格式,然后编写配置文件,编写后点击 发布即可:
注意,在Nacos中的
dataId
中的后缀名必须用yaml
而不能用yml
。
添加配置文件后再点开Nacos配置中心的配置列表,就能发现已经存在刚才创建的配置文件:
然后我们启动3377微服务,访问 http://localhost:3377/config/info 查看能否访问都Nacos配置中心的配置信息:
Nacos配置中心直接就 支持动态刷新,再更改了Nacos配置中心的配置信息后,再通过3377微服务访问,就可以得到更新后的配置信息。目前Nacos已经实现了Eureka + Config + Bus的功能,但是Nacos之所以如此优秀是因为这些框架有的功能它都有,而这些框架没有的功能它还有!下面看Nacos的一些高级功能。
3.2 分类配置
问题:多环境多项目管理
在多环境多项目管理时,也就是实际开发中,通常一个系统会准备不同的开发环境,如dev开发环境、test测试环境、prod生产环境,如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境等,那对这些微服务配置该如何管理呢?=> 在Nacos配置中心中就可以进行分类配置,一个配置文件的具体所属由 namespace
+ Group
+ dataId
所构成:
为什么如此设计?
这种设计就类似于Java中的包名和类名,最外层的 namespace
是可以用于区分部署环境的,主要用来实现隔离,比如有三个环境:开发、测试、生产,那就可以创建三个 namespace
。
Group
和 dataId
逻辑上区分两个目标对象。Group可以把不同的微服务划分到同一个分组里面去。Service就是微服务,一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFALUT,Cluster是对指定微服务的一个虚拟划分。
举例:比如说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称HZ,给广州机房的Service微服务起一个集群名字GZ,还可以尽量让同一个机房的微服务互相调用,以提升性能。
最后就是Instance
,就是微服务的实例。
默认情况下 namespace
的值为 public
, Group
的值为 DEFAULT_GROUP
,默认Cluster是DEFAULT。
下面对这三个属性进行详细展开。
dataId
方案
指定 spring.profile.active
配置属性和配置文件的 dataId
来使不同环境下读取不同的配置,也就是说我们现在用默认的命名空间 namespace
(即 public
),默认的分组 Group
(即 DEFAULT_GROUP
),然后在Nacos配置中心中建立两个 dataId
分别为 dev
环境和test
环境。此时在同一命名空间同一组中就有了两个应用于开发、测试不同环境的配置文件:
然后我们通过修改微服务自己定制配置信息 application.yml
配置文件中的spring.profile.active
属性即可进行多环境下的配置文件的读取,配置什么就加载什么,在之前我们的3377微服务获取的是配置中心中 nacos-config-client-dev.yaml
配置文件的配置信息,现在我们将 application.yml
修改为:
spring:
profiles:
# active: dev # 表示开发环境
active: test # 表示开发环境
然后重启3377微服务,再次进行测试发现访问到的配置信息为测试环境的配置信息:
Group
方案
不仅通过 dataId
可以实现环境分区,应用 Group
也可以实现环境分区,首先我们在Nacos配置中心新建两个相同 dataId
,不同 Group
的配置文件,一个用于开发环境,一个用于测试环境:
由于这两个配置文件的 dataId
均为 nacos-config-client-info.yaml
,根据之前解释的 dataId
的命名规则,我们应该将3377微服务的自定义配置文件 application.yml
中的 spring.profile.active
属性更改为 info
:
spring:
profiles:
active: info
在确认了读取的配置文件的 dataId
后,我们在其 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.group
属性,设置为相应的组名即可,比如我们现在设置为测试环境组:
spring:
cloud:
nacos:
config:
group: TEST_GROUP
重启3377微服务,再次访问可以发现获取到的配置信息即为TEST_GROUP
组的配置信息。
namespace
方案
根据namespace同样可以进行环境区分。
新建两个命名空间
服务列表查看
在 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.namespace
属性,设置为相应的命名空间流水号即可,比如我们现在设置为开发环境命名空间:
spring:
cloud:
nacos:
config:
namespace: 5e4fbf07-10f3-4216-86c5-e40391667310
在dev命名空间下新建不同group的配置文件
重启3377微服务,再次访问可以发现获取到的配置信息即为 dev
命名空间下TEST_GROUP
组的配置信息。
4. Nacos集群和持久化配置
4.1 官网说明
官网
官网集群部署架构图
开源的时候推荐用户把所有服务列表放到一个vip下面,然后挂到一个域名下面
http://ip1:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。
http://SLB:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),直连SLB即可,下面挂server真实ip,可读性不好。
http://nacos.com:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),可读性好,而且换ip方便,推荐模式
VIP:即虚拟IP,即Nginx
上图翻译一下就是
4.2 Nacos持久化配置解释
默认Nacos使用嵌入式数据库实现数据的存储,所以,如果启动多个默认配置下的Nacos节点,数据储存是存在一致性问题的。Nacos默认自带的嵌入式数据库为derby。
为了解决这个问题,Nacos采用了 集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
单机模式支持MySQL,在0.7版本之前,在单机模式时Nacos使用嵌入式数据库实现数据的储存,不方便观察数据的基本情况。0.7版本增加了支持MySQL数据源能力,具体的操作步骤如下。
derby到mysql切换配置步骤:
- 安装数据库,版本要求:5.6.6+
- nacos-server-1.1.4\nacos\conf目录下找到sql脚本(nacos-mysql.sql),初始化MySQL数据库nacos_config,数据库初始化文件:nacos-mysql.sql
- nacos-server-1.1.4\nacos\conf目录下找到application.properties,修改conf/application.properties文件,增加支持MySQL数据源配置(目前只支持MySQL),添加MySQL数据源的url,用户名和密码。
# 如果是Mysql5.+
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://mpolaris.top:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
db.user=root
db.password=123456
# 如果是Mysql8.+
spring.datasource.platform=mysql
jdbc.DriverClassName=com.mysql.cj.jdbc.Driver
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user= root
db.password= 123456
启动Nacos,可以看到是个全新的空记录界面,以前是记录进derby。
4.4 Linux版Nacos+MySQL生产环境配置
需要一个Nginx,三个Nacos注册中心,一个Mysql
Nacos下载Linux版,下载地址,nacos-server-1.1.4.tar.gz,解压后安装
集群配置步骤
Linux服务器上MySQL数据库配置(按照4.3配置)
application.properties配置(按照4.3配置)
Linux服务器上Nacos的集群配置cluster.conf
- 梳理出三台Nacos机器的不同服务端口号(3333,4444,5555)
- 复制出cluster.conf
cd /opt/nacos/conf
cp cluster.conf.example cluster.conf
- 修改内容
注意:这个IP不能写127.0.0.1,必须是Linux命令hostname -i 能够识别的IP
# it is ip
# example
# 这里我填的是自己的私网ip
172.20.xxx.xxx:3333
172.20.xxx.xxx:4444
172.20.xxx.xxx:5555
编辑Nacos的启动脚本startup.sh(先备份),使它能够接受不同的启动端口
- /mynacos/nacos/bin 目录下有startup.sh
- 怎样改?
# 平时单机版的启动,都是./startup.sh即可
# 但是集群启动,我们希望可以类似其他软件的shell命令,传递不同的端口号启动不同的nacos实例。
# 例如:命令 ./startup.sh -p 3333 表示启动端口号为3333的nacos服务器实例
vim ./startup.sh
# 修改第57行开始增加p
export SERVER="nacos-server"
export MODE="cluster"
export FUNCTION_MODE="all"
while getopts ":m:f:s:p:" opt
do
case $opt in
m)
MODE=$OPTARG;;
f)
FUNCTION_MODE=$OPTARG;;
s)
SERVER=$OPTARG;;
p)
PORT=$OPTARG;;
?)
echo "Unknown parameter"
exit 1;;
esac
done
# 修改第134行,增加 - Dserver.port=${PORT}
nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1
- 执行方式
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
#注意:提示服务器内存不够,可以把nacos启动脚本里-Xms和-Xmx jvm内存调整小一点
# 同时通过window浏览器访问:
http://192.168.42.82:3333/nacos/#/login
http://192.168.42.82:4444/nacos/#/login
http://192.168.42.82:5555/nacos/#/login
Nginx的配置,由它作为负载均衡器
- 修改Nginx的配置文件Nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream cluster {
server 127.0.0.1:3333;
server 127.0.0.1:4444;
server 127.0.0.1:5555;
}
server {
listen 1111;
server_name localhost;
location / {
# root html;
# index index.html index.htm;
proxy_pass http://cluster;
}
}
}
- 按照指定配置文件启动Nginx
cd /usr/local/nginx/sbin
./nginx -c /opt/nginx/conf/nginx.conf
开始测试
- 测试通过Nginx访问Nacos:http//192.168.42.82:1111/nacos/#/login,成功访问!
- 新建一个配置测试数据库,Linux服务器的MySQL也插入一条记录!
微服务测试
微服务springalibaba-provider-payment9002启动注册进nacos集群
- yml配置文件
server:
port: 9002
spring:
application:
name: nacos-payment-provider # 微服务名称
cloud:
nacos:
discovery:
# server-addr: localhost:8848 # 配置Nacos地址
# 换成Nginx的1111端口,做集群
server-addr: 192.168.42.1:1111
management:
endpoints:
web:
exposure:
include: '*' # 监控端点全部打开
- 结果:成功启动!
高可用总结
5. Sentinel熔断与限流
5.1 Sentiel简介
官网
主要特性
与Hystrix对比
- Hystrix需要我们自己手工搭建监控平台,没有一套Wbe界面可以给我们进行更加细粒度化的配置,流控,速率控制,服务熔断,服务降级。
- Sentinel单独一个组件,可以独立出来。直接界面化的细粒度统一配置。
Sentiel可以解决的问题
- 服务雪崩
- 服务降级
- 服务熔断
- 服务限流
- …
5.2 安装Sentiel控制台
Sentinel分为两部分:
- 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对Dubbo / Spring Cloud等框架也有较好的支持。
- 控制台(Dashboard)基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器.
下载
运行命令
# 前提:Java8环境,8080端口不被占用
java -jar sentinel-dashboard-1.7.0.jar
访问Sentinel管理界面
- http://localhost:8080
- 登陆账号密码均为sentinel
5.3 初始化演示工程
启动Nacos8848,http://localhost:8848/nacos/#/login
新建Moudle:cloudalibaba-sentinel-service8401
pom.xml
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--其他基础包-->
yml配置文件
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 # 配置Sentinel dashboard地址
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
业务类FlowLimitController
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "==> testA";
}
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "==> testB");
return "==> testB";
}
}
启动Sentinel8080,启动微服务8401
启动8401微服务后台查看sentinel控制,啥也没有?=> Sentinel采用懒加载,执行两次访问http:localhost8401/testA,http:localhost8401/testB,效果如下:
5.4 流控规则
名词说明:
- 资源名:唯一名字,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量):当调用该API的QPS达到阈值的时候,进行限流
- 线程数:当调用该API的线程数达到阈值的时候,进行限流
- 是否集群:我们这里不需要集群
- 流控模式:
- 直接:API达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
流控模式
直接(默认)
- 配置:表示1秒钟内查询1次就是OK,若超过次数1,就直接快速失败,报默认错误
-
测试:快速点击访问 http://localhost:8401/testA => Blocked by Sentinel(flow limiting)
-
思考:直接调用了默认报错信息,技术方面是没问题的,但是 是否因该有我们自己的后续处理呢?(类似有一个fallback的兜底方法),后面会讲到如何配置
关联
- 是什么:当关联的资源达到阈值时,就限流自己。当与A关联的资源B达到阈值后,就限流自己。例如B惹事A挂了
- 配置A:当关联资源/testB的QPS阈值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名
- postman模拟并发密集访问testB
- 访问B成功
- postman里新建多线程集合组(Collection)
- 将访问地址添加进新线程组(Collection)
- RUN => 大批量线程高并发访问B,导致A失效了
- 运行后发现testA挂了(点击访问A =>Blocked by Sentinel(flow limiting) )
链路
- 多个请求调用了同一个微服务,相同道理,这里就不演示了
流控效果
快速失败
- 直接失败,抛出异常(Blocked by Sentinel(flow limiting))
- 源码:com.alibaba.csp.sentinel.slots.block.controller.DefaultController
Warm Up(预热)
- 说明:公式 => 阈值除以coldFactor冷加载因子(默认值为3),经过预热时长后才会达到阈值
- 源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
- Warm Up
- Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统提升到高水位可能瞬间把系统压垮。通过“冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 – Warm Up文档,具体的例子可见 WarmUpFlowDemo。
- 通常冷处理的过程系统允许通过的QPS曲线如下图所示:
- WarmUp配置
- 多次点击http://localhost:8401/testB,刚开始不行,后续慢慢OK
- 应用场景如:秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增加到设置的阈值。
排队等待
- 匀速排队,阈值必须设置为QPS,否则无效
- 源码:com.ailibaba.csp.sentinel.slots.block.controller.RateLimiterController
- 匀速排队:
- 匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 – 匀速排队模式,具体的例子可以参见 PaceFlowDemo
- 该方式的作用如下图所示:这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
- 配置:/test每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒
5.5 降级规则
官网说明
名词说明:
RT(平均响应时间,秒级)
- 平均响应时间,超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级,窗口期过后关闭断路器
- RT最大4900 ms(更大的需要通过
-Dcsp.sentinel.statistic.max.rt=XXXX
才能生效)
异常比例(秒级)
- QPS >= 5 且 比例(秒级统计)超过阈值时,触发降级,窗口期过后关闭断路器
注意:Sentinel的断路器是没有半开状态的
半开状态:半开的状态,系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用,具体可以参考Hystrix。
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradException )。
降级策略演示
RT(平均响应时间(DEGRADE_GRADE_RT
))
- 是什么:当 1s 内持续进入 5 个请求,且对应时刻的平均响应时间(秒级)均超过阈值(
count
,以ms为单位),那么在接下来的时间窗口(DegradeRule
中的timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动的熔断(抛出DegradeException
)。注意 Sentinel 莫仍统计的 RT 上限是 4900 ms,超出次阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx
来配置。
-
测试
- 代码
@GetMapping("/testD")
public String testD() {
//暂停几秒线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 测试RT");
return "==> testD";
}
- 配置
- jmeter压测:直接 Blocked by Sentinel (flow limiting)
- 结论:① 默认请求是 1 秒 5个,而我们能jmeter压测设置的 1 秒 10个。② 代码中设置的平均响应时间为 1 秒,而我们设置的RT为 2 毫秒。两个条件都不满足则直接断路器开启,微服务不可用了,服务熔断降级。
异常比例(DEGRADE_GRADE_EXCEPTION_RATIO)
- 是什么:当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(
DegradeRule
中的count
)之后,资源进入降级状态,即在接下来的时间窗口(DegradeRule
中的timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动的返回。异常比率的阈值范围是[0.0,1.0]
,代表 0% – 100%。
-
测试
- 代码
@GetMapping("/testD")
public String testD() {
log.info("testD 测试RT");
int age = 10 / 0;
return "==> testD";
}
- 配置
- jmeter压测:直接 Blocked by Sentinel (flow limiting)
异常数(DEGRADE_GRADE_EXCEPTION_COUNT):
- 是什么:当资源近 1 分钟的异常数目超过阈值之后会进行熔断降级。注意由于统计 时间窗口时分钟级别的,若
timeWindow
小于 60s,则结束熔断状态后仍可能再进入熔断状态。即 时间窗口一定要 >= 60 秒。
-
测试
- 代码:同异常比例
- 配置
- jemter压测:直接 Blocked by Sentinel (flow limiting)
5.6 热点key限流
官网说明
源码
com.alibaba.csp.sentinel.slots.block.BlockException
名词说明
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看作是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
回顾 – 兜底方法
兜底方法分为 系统默认 和 客户自定义 两种,之前限流出现问题后,都是用sentinel 系统默认的提示:Blocked by Sentinel (flow limiting)。
我们能不能自定义?类似hystrix,某个方法出问题了,就找对应的兜底降级方法!
=> 在sentinel中通过 @SentinelResource
注解能够实现 (类似于hystrix的HystrixCommand
)。
/**
* value: 唯一标识,对应热点规则资源名
* blockHandler:违背了热点规则后指向的兜底方法,如果不配置,异常就会直接打到前台去(不友好)
*/
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler="deal_testHotKey")
public String testHotKey(@RequestParam(value="p1",required=false) String p1,
@RequestParam(value="p2",required=false) String p2){
return "==> testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException e) {
return "==> deal_testHotKey";
}
配置
方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理。且用了我们自定义的兜底方法。
测试
# QPS超过每秒1次会限流
http://localhost:8401/testHotKey?p1=0
# QPS超过每秒1次会限流
http://localhost:8401/testHotKey?p1=0&p2=1
# 无限制,不会限流
http://localhost:8401/testHotKey?p2=1
参数例外项
上述案例演示了第一个参数p1,当QPS超过每秒1次立刻就会被限流。
特殊情况:我们期望 p1 参数当它是某个特殊值时,它的限流和平时不一样,例如当 p1 的值等于 5 时,它的阈值可以达到 200。怎样实现呢?=> 配置参数例外项。
前提条件:热点参数必须是 基本类型 或者 String
其他注意点
@SentineResource 处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底方法处理。假如程序代码出错如 int age = 10/0,这个是Java运行时爆出的运行时异常RunTimeException,@SentineResource 不管。
总结:@SentineResource 主管配置出错,运行出错该走异常还是会走异常!
5.7 系统规则
官网说明
各项配置说明
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load,CPU使用率,平均RT,入口QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是 应用整体维度的,而不是资源维度的,并且 仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务器 或Dubbo 服务端接受的请求,都属于入口流量。
系统规则支持以下的模式:
Load 自适应
(仅对 Linux/Unix-like 机器生效):系统的 load1作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR阶段)。系统容量由系统的maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。CPU usage
(1.5.0+ 版本):当系统CPU 使用率超过阈值即触发系统保护(取值范围 0.0 ~ 1.0),比较灵敏。平均RT
:当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。并发线程数
:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。入口QPS
:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
配置全局QPS为例:
系统规则一般少用,使用危险,一竹竿打死一船人!
5.8 @SentinelResource
按资源名称限流 + 后续处理
启动nacos,sentinel
修改8401pom.xml,将公共模块也引入
<!-- 公共模块 -->
<dependency>
<groupId>com.polaris</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
新增业务类RateLimitController,便于演示
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK",
new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444,
exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
配置流控规则:表示1秒钟内查询次数大于1,就跑到我们自定义的限流处进行限流
测试:出现了我们自定义的限流兜底方案!
发现问题:此时如果关闭服务8401会怎样?=> Sentinel控制台流控规则消失了?临时存储?
按照Url地址限流 + 后续处理
通过访问URL来限流,会返回Sentinel自带默认的限流处理信息
业务类增加
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200,
"按url限流测试OK", new Payment(2020L, "serial002"));
}
配置流控规则
测试:会返回Seninel自带的限流处理结果:Blocked by Sentinel (flow limiting)
上述兜底方案面临的问题
系统默认的方案,没有体现我们自己的业务需求
依照现有条件,我们自定义的处理方法又和业务代码耦合在一起,不直观
每个业务方法都添加一个兜底方法,会导致代码膨胀加剧
全局统一的处理方法没有体现
客户自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流处理逻辑
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444,
"按客戶自定义,global handlerException => 1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444,
"按客戶自定义,global handlerException => 2");
}
}
RateLimitController增加方法
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义",
new Payment(2020L, "serial003"));
}
测试:出现了我们自定义的限流兜底方案!
@SentinelResource的其他属性
属性 | 作用 | 是否必须 |
---|---|---|
value | 资源名称 | 是 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT | 否 |
blockHandler | 处理BlockException的函数名称。函数要求: 1. 必须是 public 2.返回类型与原方法一致 3. 参数类型需要和原方法相匹配,并在最后加 BlockException 类型的参数。4. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。 |
否 |
blockHanderClass | 存放blockHandler的类。对应的处理函数必须static修饰,否则无法解析,其他要求:同blockHandler。 | 否 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:1. 返回类型与原方法一致 2. 参数类型需要和原方法相匹配,Sentinel 1.6开始,也可在方法最后加 Throwable 类型的参数。3.默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法。 |
否 |
fallbackClass (1.6) | 存放fallback的类。对应的处理函数必须static修饰,否则无法解析,其他要求:同fallback。 | 否 |
defaultFallback | 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:\1. 返回类型与原方法一致 \2. 方法参数列表为空,或者有一个 Throwable 类型的参数。\3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。 |
否 |
exceptionTolgnore (1.6) | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。 | 否 |
exceptionsToTrace | 需要trace的异常 | Throwable |
注意:
1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出
BlockException
时只会进入blockHandler
处理逻辑。若未配置blockHandler
、fallback
和defaultFallback
,则被限流降级时会将BlockException
直接抛出。从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用
Tracer.trace(ex)
来记录业务异常。Sentinel 1.4.0 以前的版本需要自行调用Tracer.trace(ex)
来记录业务异常。
5.9 服务熔断功能
sentinel整合Ribbon + openFeign + fallback
启动nacos和sentinel
新建服务提供者9004/9005
- pom重要依赖
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- yml
server:
port: 9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,
"from mysql,serverPort: " + serverPort, payment);
return result;
}
}
新建服务消费者84
- pom重要依赖
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
- 主启动
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
- 配置类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- service
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444,
"服务降级返回,=> PaymentFallbackService",
new Payment(id, "errorSerial"));
}
}
- controller
- fallback管运行异常
- blockHandler管配置违规
- 若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 只会进入 blockHandler处理逻辑
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
//① 没有配置
// @SentinelResource(value = "fallback")
//② fallback只负责业务异常
// @SentinelResource(value = "fallback",fallback = "handlerFallback")
//③ blockHandler只负责sentinel控制台配置违规
// @SentinelResource(value = "fallback",blockHandler = "blockHandler")
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback",
blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL
+ "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException(
"IllegalArgumentException,非法参数异常!");
} else if (result.getData() == null) {
throw new NullPointerException(
"NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 "
+ e.getMessage(), payment);
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445,
"blockHandler-sentinel限流,无此流水: blockException "
+ blockException.getMessage(), payment);
}
//==> OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
}
测试
- 测试轮询:http://localhost:84/consumer/fallback/1
- 测试@SentinelResource
- 没有任何配置:给客户error页面,不友好
- 添加fallback,blockHandler,exceptionsToIgnore:① 忽略了非法参数异常,出现非法参数异常仍是系统错误页面,② 其他情况都有了兜底方案
5.10 规则持久化
是什么
一旦我们重启应用,Sentinel规则消失,生产环境需要将配置规则进行持久化
怎么做
将限流规则持久进Nacos保存,只要刷新8401某个rest地址,Sentinel控制台的流控规则就能看的到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效。
步骤演示
现象:我们先在Sentinel配置一条流控规则,当重启8401后,该流控规则就会失效消失了!
修改8401
- pom
<!--SpringCloud ailibaba sentinel-datasource-nacos 做持久化用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- yml (添加Nacos数据源配置)
添加Nacos业务规则配置(规则即可以保存在Sentinel中,也可以保存在Nacos中等,只要是一个持久化媒介就行)
[
{
"resource":"/rateLimit/byUrl", //资源名称
"limitApp":"default", //来源应用
"grade":1, //阈值类型,0表示线程数,1表示QPS
"count":1, //单机阈值
"strategy":0, //流控模式,0表示直接,1表示关联,2表示链路
"controlBehavior":0, //流控效果,0表示快速失败,1表示Warm Up,2表示排队等候
"clusterMode":false //是否集群
}
]
启动8401 刷新Sentinel发现业务规则有了
停止8401,再看sentinel,发现停机后流控规则消失了!
重启8401,再看sentinel,多次调用接口,多次刷新sentinel后,发现流控规则重新出现了,持久化验证成功!
6. Seata处理分布式事务
6.1 分布式事务问题
分布式之前
单机库存没有这个问题
从1:1 => 1:N => N:N,开始出现分布式事务问题
分布式之后
单机应用被拆分成微服务应用,原来的三个模块被拆分成 三个独立的应用,分别使用 三个独立的数据源 ,业务操作需要调用 三个服务来完成。
此时 每个服务内部的数据一致性由 本地 事务来保证,但是 全局 的数据一致性问题没法保证。
一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
6.2 Seata简介
官网
下载地址
是什么
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
作用
一个典型的分布式事务过程:ID + 三组件模型
- Transaction ID(XID):全局唯一的事务ID
- 三组件概念
- Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
- Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
- Resource Manage(RM):控制分支(本地)事务,负责分支注册,状态汇报,并接受事务协调的指令,驱动分支(本地)事务的提交和回滚
处理过程(我们可以用这种方式帮助理解:RM-学生,TM-班主任,TC-授课老师)
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
怎样用
本地 @Transational
全局 @GlobalTransational
- seata的分布式交易解决方案
6.3 Seata-Server安装
下载版本
修改conf目录下的file.conf配置文件
-
先备份原始file.conf文件
-
主要修改:自定义事务组名称 + 事务日志存储模式为db + 数据库连接
- service模块 / store模块
mysql5.7数据库新建库seata,建表db_store.sql在seata-server-0.9.0\seata\conf目录里面
修改seata-server-0.9.0\seata\conf目录下的registry.conf目录下的registry.conf配置文件
目的是指明注册中心为nacos,及修改nacos连接信息。
先启动Nacos,端口号为8848
再启动seata-server:bin目录下seata-server.bat
6.4 订单/库存/账户业务数据库准备
分布式事务业务说明
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
创建业务数据库
- 建库sql
create database seata_order;
create database seata_storage;
create database seata_account;
- 按照上诉3库分别建立对应业务表
# seata_order库下新建t_order表
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11, 0) DEFAULT NULL COMMENT '金额',
`status` int(1) DEFAULT NULL COMMENT '订单状态: 0:创建中 1:已完结',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;
# seata_storage库下新建t_storage表
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
# seata_account库下新建t_account表
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10, 0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10, 0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10, 0) DEFAULT NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
- 按照上述3库分别建立对应的回滚日志表(三个库都需要建立各自独立的回滚日志表)
# 该sql在seata\conf目录下的db_undo_log.sql中
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
6.5 订单/库存/账户业务微服务准备
业务需求
下订单 => 减库存 => 扣余额 => 改(订单)状态
新建订单Order-Module seata-order-service2001
pom
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://mpolaris.top:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 1234321
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
domain
//CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
//order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //订单状态:0:创建中;1:已完结
}
Dao
@Mapper
public interface OrderDao{
//1 新建订单
void create(Order order);
//2 修改订单状态,从零改为1
void update(@Param("userId") Long userId,
@Param("status") Integer status);
}
service
public interface OrderService{
void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService{
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
*/
@Override
@GlobalTransactional(name="fsp-create-order",rollbackFor=Exception.class)
public void create(Order order)
{
log.info("==> 开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("==> 订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("==> 订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("==> 订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("==> 订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("==> 修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("==> 修改订单状态结束");
log.info("==> 下订单结束了,O(∩_∩)O哈哈~");
}
}
controller
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}
}
config
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(
DataSourceProxy dataSourceProxy)throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean =
new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(
new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@MapperScan({"com.polaris.springcloud.dao"})
public class MyBatisConfig {
}
主启动
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动创建
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
mapper
<mapper namespace="com.polaris.springcloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.polaris.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
insert into t_order (id,user_id,product_id,count,money,status)
values (null,#{userId},#{productId},#{count},#{money},0);
</insert>
<update id="update">
update t_order set status = 1
where user_id=#{userId} and status = #{status};
</update>
</mapper>
将seata-server/conf目录下的file.conf和registry.conf拷贝到项目模块下的resource中(seata-server/conf下的是总控)
新建库存Storage-Module seata-storage-service2002
新建账户Account-Module seata-account-service2003
参考订单Module,源码参考个人GitHub
6.6 测试
数据库初始情况
正常下单
- http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
- 数据库情况正常
超时异常,没加@GlobalTransactional
- AccountServiceImpl设置超时
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("==> account-service中扣减账户余额开始");
//模拟超时异常,全局事务回滚
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
LOGGER.info("==> account-service中扣减账户余额结束");
}
- 数据库情况:① 加进来了但是订单状态为0,表示未支付 ②库存减少了 ③ 钱也被扣了
- 故障情况:
- 当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从0改为1
- 而且由于feign的重试机制,账户余额还有可能被多次扣减
超时异常,添加了@GlobalTransactional
- AccountServiceImpl设置超时
- 三个数据库都未增加数据,即全局事务回滚了
6.7 Seata原理浅析
再次理解TC/TM/RM三个组件 – 分布式事务的执行流程:
- TM开启分布式事务(TM向TC注册全局事务记录)
- 按业务场景,编排数据库,服务等事务内资源(RM向TC汇报资源准备状态)
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
- TC汇报事务信息,决定分布式事务是否提交还是回滚
- TC通知所有RM提交/回滚资源,事务二阶段结束
AT模式如何做到对业务的无侵入:
- AT模式
- 一阶段加载
- 二阶段提交
- 二阶段回滚