Spring Cloud Alibaba 为分布式应用开发提供一站式解决方案。它包含开发分布式应用程序所需的所有组件,使您可以轻松地使用 Spring Cloud 微服务框架开发应用程序。
Spring Cloud 是一系列框架的有序集合,这些框架为我们提供了分布式系统构建工具。
我的理解是 Spring Cloud 是 Spring Boot 应用的集群。
而一系列 Spring Boot 应用在不同的场景的、运用就造就了很多 SpringCloud 组件,这些包括:服务注册、服务治理、配置中心、网关、限流熔断器、服务调用、负载均衡、消息总线等。
组件 | 名称 |
---|---|
服务注册与发现 | Alibaba Nacos、Netflix Eureka、Apache Zookper |
分布式配置中心 | Alibaba Nacos、Spring Cloud Config |
网关 | Spring Cloud Gateway、Netflix Zull |
限流熔断器 | Alibaba Sentinel、Netflix Hystrix、 Resilience4j |
服务调用 | RestTemplate、Open Feign、Dubbo Spring Cloud |
负载均衡 | Spring Cloud LoadBalancer、Netflix Ribbon |
消息总线 | Spring Cloud Bus |
… | … |
版本适配的 官方参考地址
Spring Cloud Alibaba Version | Spring Cloud Version | Spring Boot Version |
---|---|---|
2.2.6.RELEASE | Hoxton.SR9 | 2.3.2.RELEASE |
Spring Cloud Alibaba 与各组件版本关系
Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | Seata Version |
---|---|---|---|
2.2.6.RELEASE | 1.8.1 | 1.4.2 | 1.3.0 |
Nacos 是一个易于使用的平台,旨在进行动态服务发现、配置和服务管理。它可以帮助您轻松构建云原生应用程序和微服务平台。
startup.cmd -m standalone
E:\workspace\nacos-server\nacos-cluster
│
├─nacos-8848
│ ├─bin
│ ├─conf
│ │ application.properties
│ │ cluster.conf.example
│ │ nacos-mysql.sql
│ ├─status
│ └─target
│
├─nacos-8849
│ ├─bin
│ ├─conf
│ │ application.properties
│ │ cluster.conf.example
│ │ nacos-mysql.sql
│ ├─status
│ └─target
│
└─nacos-8850
├─bin
├─conf
│ application.properties
│ cluster.conf.example
│ nacos-mysql.sql
├─status
└─target
创建数据库,我这里取名为 nacos_config
,并执行解压目录中的 conf/nacos-mysql.sql
。这里是为 Nacos 集群配置共同的数据源,如果不配置的话,其默认的 derby 数据库是不能确保集群中所有 Nacos 节点数据一致的。
将三个 Nacos 节点的 conf/application.properties
配置其端口号和数据源即可,这里以解压的8848这一副本为例:
#*************** Spring Boot Related Configurations ***************#
### Default web context path:
server.servlet.contextPath=/nacos
### Default web server port:
server.port=8848
#*************** Network Related Configurations ***************#
### If prefer hostname over ip for Nacos server addresses in cluster.conf:
# nacos.inetutils.prefer-hostname-over-ip=false
### Specify local server's IP:
# nacos.inetutils.ip-address=
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
# spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=nacos
db.password.0=nacos
conf/cluster.conf.example
更名为 conf/cluster.conf
,均如下:192.168.18.154:8848
192.168.18.154:8849
192.168.18.154:8850
这里需要注意下,本地实验的时候不要写 127.0.0.1
也不要写 localhost
,写本机的真实 IP 即可,如果不知道ip,可以使用 ipconfig
查询一下,否则后面 Nacos 客户端连接的时候会抛出如下异常:
com.alibaba.nacos.api.exception.NacosException: failed to req API:/nacos/v1/ns/instance after all servers([localhost:8848, localhost:8849, localhost:8850]) tried: ErrCode:400, ErrMsg:<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Sat Jan 13 20:05:40 CST 2024</div><div>There was an unexpected error (type=Bad Request, status=400).</div><div>receive invalid redirect request from peer 127.0.0.1</div></body></html>
at com.alibaba.nacos.client.naming.net.NamingProxy.reqApi(NamingProxy.java:556) ~[nacos-client-1.4.2.jar:na]
at com.alibaba.nacos.client.naming.net.NamingProxy.reqApi(NamingProxy.java:498) ~[nacos-client-1.4.2.jar:na]
......
......
其错误描述是:receive invalid redirect request from peer 127.0.0.1,如下图:
.\nacos-8848\bin\startup.cmd -m cluster
.\nacos-8849\bin\startup.cmd -m cluster
.\nacos-8850\bin\startup.cmd -m cluster
脚本的默认模式为集群,所以可以不需要 -m cluster
参数
http://localhost:8848/nacos
、http://localhost:8849/nacos
、http://localhost:8850/nacos
。或者可以配置一个 Nginx 作为 Nacos 前端进行代理。访问账号和密码默认都是 nacos,登录后界面如下:我在本机为了方便启动集群,自己写了个批处理文件,以便于开发过程中,直接可以一键启动,就省去了很多的麻烦,欢迎大家给予意见哦。源码如下:
@ECHO off
SETLOCAL enabledelayedexpansion
SET CLUSTER_CONF=%cd%\nacos-cluster\cluster.conf
SET CLUSTER_DIR=%cd%\nacos-cluster\
SET LOCAL_IP=.
IF "%1%"=="cluster" (
ECHO cluster mode
REM 获取本机 IP 地址
FOR /f "skip=1 tokens=2 delims=:" %%i IN ('ipconfig ^|find /i "ipv4"') DO (
REM 去除 IP 前面的空格
FOR /f "delims= " %%a IN ("%%i") DO (
SET LOCAL_IP=%%a
)
)
ECHO ip=!LOCAL_IP!
IF "!LOCAL_IP!"=="" (
REM 未能获取到 IP,换个方法再试一次
FOR /f "tokens=16" %%i IN ('ipconfig ^|find /i "ipv4"') DO SET LOCAL_IP=%%i
)
REM 删除集群配置文件
IF EXIST !CLUSTER_CONF! DEL /F !CLUSTER_CONF!
REM /d 用于查询目录, 去掉 /d 查询的是文件
for /d %%s IN (!CLUSTER_DIR!\*) DO (
REM 删除 data 目录和 logs 目录
IF EXIST %%s\data rmdir /s/q %%s\data
IF EXIST %%s\logs rmdir /s/q %%s\logs
REM 获取到目录名称后面的端口号
for /f "tokens=2 delims=-" %%p IN ("%%~ns") DO (
ECHO !LOCAL_IP!:%%p>>!CLUSTER_CONF!
)
)
REM 延迟 1s 执行
CHOICE /t 2 /d y /n >NUL
for /d %%s IN (!CLUSTER_DIR!\*) DO (
REM 复制集群配置文件,如果存在,则覆盖
COPY /Y !CLUSTER_CONF! %%s\conf\cluster.conf
REM 启动 Nacos
START %%s\bin\startup.cmd
)
REM 删除集群配置文件
IF EXIST !CLUSTER_CONF! DEL /F !CLUSTER_CONF!
REM 重启 nginx
%cd%\..\nginx-1.22.0\nginx-restart.cmd
) ELSE (
ECHO standalone mode
CALL %cd%\nacos\bin\startup.cmd -m standalone
)
这个批处理程序接受一个入参,当参数为 cluster
的时候,会启动集群,否则启动单机的 Nacos。在源码中的 else 部分就是单机启动 Nacos ,比较好理解,主要是集群启动的时候,我遇到很多问题。主要还是配置集群文件 cluster.conf
的 IP和端口的问题,因为我是笔记本电脑,WiFi连接路由器的 IP 是自动分配的,难道我每次启动集群都要修改一次这个集群配置文件吗?不,我不愿意!所以,我的思路是:
nacos-8848
、nacos-8849
和 nacos-8850
就是想遍历的时候根据名称来获取后面的数字作为端口,结合前面获取到的 IP 地址,然后组合成新的 cluster.conf
文件cluster.conf
文件,分别复制到三个节点的 conf
目录下。我在复制之前使用 CHOICE
指令停滞了 2s,原因是我发现前面的双层循环好像有点延迟,导致后面复制的时候会提示找不到文件,不过后来我又发现,即便删除这个延迟 2s 的代码也能用,所以我就没删除这行。cluster.conf
文件后重启 Nginx。当然,这一步不是必须的,可以直接把这一行删除,我只是想用 Nginx 作为 Nacos 集群的前端。nginx-restart.cmd
也是一个自己写的用于重启 Nginx 的脚本。cluster.conf
文件的时候会把每个 Nacos 节点的 data 和 logs 目录删除,这一步其实也不是必须的。因为我之前遇到前文提到的那个 receive invalid redirect request from peer 127.0.0.1 问题,查了好多博客都说是要将这两个目录删除,然后重启,其实没啥用,因为是我的 IP 不应该携程本地环回地址,要不然我为啥需要那么费力在脚本中获取本机的 IP 呢。因为 Nacos 默认采用的是 derby 数据库,删除 data 目录可以将他们的元数据删除,在启动后重新连接 MySQL,会同步这一数据的,所以,我还是保留了删除这俩目录的代码。这个批处理程序我命名为 nacos-server.cmd
,它和上文中的三个 Nacos 节点的位置结构如下所示:
E:\workspace\nacos-server
│
│ nacos-server-1.4.2.tar.gz ---------> 从官网下载的 Nacos 服务端程序
│ nacos-server.cmd ---------> 运行 Nacos 的批处理程序
│
├─nacos ---------> 用于 Nacos 单机运行
│ ├─bin
│ ├─conf
│ └─target
│
└─nacos-cluster ---------> 用于 Nacos 集群运行
├─nacos-8848
│ ├─bin
│ ├─conf
│ ├─status
│ └─target
│
├─nacos-8849
│ ├─bin
│ ├─conf
│ ├─status
│ └─target
│
└─nacos-8850
├─bin
├─conf
├─status
└─target
为了更直观解释目录结构,可以看下图:
最后给这个批处理程序配置到系统的环境变量里,后面只需要按下 win + r,直接输入 nacos-server cluster
就可启动 Nacos 集群,输入 nacos-server
就可启动 Nacos 单机。
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<spring-boot.version>2.3.2.RELEASEspring-boot.version>
<spring-cloud.version>Hoxton.SR9spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASEspring-cloud-alibaba.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
bootstrap.yml
文件或 bootstrap.properties
,因为该文件加载顺序最早:加载顺序bootstrap.yml > application.yml > application-dev(prod).yml
server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.18.154:8848,192.168.18.154:8849,192.168.18.154:8850
service: order-service
127.0.0.1
和 localhost
都能最后连接上 Nacos!@SpringBootApplication
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}
}
在开发过程中,尤其是业务领域,常常会遇到配置需要更新的情况,但是单点服务在修改配置后,需要重启应用才能生效,引入配置中心就能无痛更新配置,效率更高!
Nacos 作为配置中心,具有:
如何使用 Nacos 配置中心呢?
order-service-configuration
意味着该配置文件我将会为订单服务所用。我们可以将一个 Data ID 视作是一个配置文件,命名时也可以有后缀名,例如order-service-configuration.yaml
,它隶属某个组(Group),而每个组又隶属于一个命名空间,他们三个的关系是依次被包含的关系。默认情况下,Nacos 仅一个命名空间 public,新建的配置 Data ID 也是默认隶属于 DEFAULT_GROUP 组
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
我这里依然是在 bootstrap.yml
文件中配置的,就是在之前的基础上增加了 spring.application.name=order-service-configuration
,值得注意的是这里的名称跟前面的 Data Id 的名称是保持一致。
server:
port: 8081
spring:
application:
name: order-service-configuration
cloud:
nacos:
discovery:
server-addr: 192.168.18.154:8848,192.168.18.154:8849,192.168.18.154:8850
service: order-service
@Value
来获取 Nacos 配置中心的配置的值@RefreshScope
@RestController
@RequestMapping("order")
public class OrderController {
@Value("${spring.application.name}")
private String name;
@Value("${nickname}")
private String nickname;
@Value("${service.name}")
private String serviceName;
@Value("${service.duration}")
private Long serviceDuration;
@GetMapping("")
public String info() {
String info = ""
+ name + "";
info += ""
;
info += "nickname=" + nickname + "";
info += "service.name=" + serviceName + "";
info += "service.duration=" + serviceDuration + "";
info += "";
return info;
}
}
访问 http://localhost:8081/order
可在页面中看到其配置值已经成功获取。需要注意以下几点:
application.yml
中配置了 service.name
和 service.duration
的值,读取的结果依然是 Nacos 配置中心。spring.application.name
则读取本地结果,也就是 application.yml 中的配置值@RefreshScope
注解。bootstrap.properties
来进行配置,附上其他拓展配置(非必需)# nacos地址
spring.cloud.nacos.config.server-addr=192.168.18.154:8848
# 1、在配置中心配置的 Data Id 最好跟 ${spring.application.name} 保持一致
spring.application.name=order-service-configuration
# 2、指定文件后缀名称
# 加载文件为:${application.name}.${file-extension}
# 例如 order-service-configuration.yaml
spring.cloud.nacos.config.file-extension=yaml
# 3、profile: 指定环境 文件名:${application.name}-${profile}.${file-extension}
# 例如 order-service-configuration-prod.yaml
spring.profiles.active=prod
# 4、nacos自己提供的环境隔离,也就是配置命名空间,可以在Nacos 管理后台创建命名空间后,将其ID粘贴到这里
spring.cloud.nacos.config.namespace=9d72b6c1-a31b-4528-9cf0-02d8a26399dd
# 5、 自定义 group 配置,这里也可以设置为数据库配置组,中间件配置组,但是一般不用,使用默认值DEFAULT_GROUP
spring.cloud.nacos.config.group=DEFAULT_GROUP
#6、自定义Data Id的配置 共享配置(sharedConfigs)0
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml
# 可以不配置,使用默认
spring.cloud.nacos.config.shared-configs[0].group=DEFAULT_GROUP
# 这里需要设置为true,动态可以刷新,默认为false
spring.cloud.nacos.config.shared-configs[0].refresh=true
# 7、扩展配置(extensionConfigs)
# 支持一个应用有多个DataId配置,例如 mybatis.yaml datasource.yaml
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yaml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
配置文件生效的顺序依次是:
${application.name}-${profile}.${file- extension}
${application.name}.${file-extension}
${application.name}
拿本例来说,其优先级依次为 order-service-configuration-prod.yaml
> order-service-configuration.yaml
> order-service-configuration
传统的方式前端发送请求会到我们的的 Nginx 上去,Nginx 作为反向代理,然后路由给后端的服务器,由于负载均衡算法是 Nginx 提供的,而 Nginx 是部署到服务器端的,所以这种方式又被称为服务器端负载均衡。
现在有三个实例,内容中心可以通过discoveryClient 获取到用户中心的实例信息,如果我们再订单中心写一个负载均衡 的规则计算请求那个实例,交给restTemplate进行请求,这样也可以实现负载均衡,这个算法里面,负载均衡是有订单中心提供的,而订单中心相对于用户中心是一个客户端,所以这种方式又称为客户端负负载均衡。
@RestController
@RequestMapping("load-balancer")
public class LoadBalancerController {
@Resource
private DiscoveryClient client;
/**
* 手动使用 DiscoveryClient 来实现负载均衡,策略是:随机选择
*/
@RequestMapping("test-discovery-client")
public String testDiscoveryClient() {
// 获取库存服务的所有实例集合
List<ServiceInstance> instances = client.getInstances("stock-service");
List<String> strings = instances.stream().map(ins -> ins.getUri().toString()).collect(Collectors.toList());
// 从集合中随机挑选一个实例地址
String url = strings.get(ThreadLocalRandom.current().nextInt(strings.size()));
url += "/stock/out";
// 调用实例所提供的接口地址,并返回结果
RestTemplate template = new RestTemplate();
String result = template.getForObject(url, String.class);
return "利用 DiscoveryClient 手动随机选取进行服务调用,调用目标:" + url + ",调用出库接口的结果为:" + result;
}
}
前文提到,我在 pom 中引入了 spring-cloud-starter-alibaba-nacos-discovery
,该组件中内置了 spring-cloud-netflix-ribbon
,所以不需要单独引入依赖。所以,实际运用时,直接引入即可。
@LoadBalanced
注解,开启 Ribbon 负载均衡@Configuration
public class RibbonConfiguration {
@Bean
@LoadBalanced
public RestTemplate template() {
return new RestTemplate();
}
}
@RestController
@RequestMapping("load-balancer")
public class LoadBalancerController {
@Resource
private RestTemplate template;
/**
* 使用 Ribbon 来实现负载均衡,默认策略是:ZoneAvoidanceRule
*/
@RequestMapping("test-ribbon")
public String testRibbon() {
// 调用地址
String url = "http://stock-service/stock/out";
// 调用实例所提供的接口地址,并返回结果
String result = template.getForObject(url, String.class);
return "利用 Ribbon 实现负载均衡进行服务调用,调用目标:" + url + ",调用出库接口的结果为:" + result;
}
}
接口 | 作用 | 默认值 |
---|---|---|
IClientConfig | 读取配置 | DefaultclientConfigImpl |
IRule | 负载均衡规则,选择实例 | ZoneAvoidanceRule |
IPing | 筛选掉ping不通的实例 | 默认采用DummyPing实现, 实际上它并不会检查实例是否可用,而是始终返回true, 即默认认为所有服务实例都是可用的. |
ServerList |
交给Ribbon的实例列表 | Ribbon: ConfigurationBasedServerList Spring Cloud Alibaba: NacosServerList |
ServerListFilter | 过滤掉不符合条件的实例 | ZonePreferenceServerListFilter |
ILoadBalancer | Ribbon的入口 | ZoneAwareLoadBalancer |
ServerListUpdater | 更新交给Ribbon的List的策略 | PollingServerListUpdater |
规则名称 | 特点 |
---|---|
RandomRule | 随机选择一个Server |
RetryRule | 对选定的负责均衡策略机上充值机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server |
RoundRobinRule | 轮询选择,轮询index,选择index对应位置Server |
WeightedResponseTimeRule | 根据相应时间加权,相应时间越长,权重越小,被选中的可能性越低 |
ZoneAvoidanceRule | (默认规则)该策略能够在多区域环境下选出最佳区域的实例进行访问。在没有Zone的环境下,类似于轮询(RoundRobinRule) |
比如在调用库存服务的时候希望采用随机规则,调用用户服务的时候采用轮询规则,可以按照如下方式进行配置:
stock-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
Ribbon 默认采用懒加载的方式初始化,为了使得首次调用服务的时候避免因为初始化过程而占用时间,可以使用饿加载,如下所示:
ribbon:
eager-load:
enabled: true
clients: user-service,stock-service
需要注意的是:如果调用的服务名称在 ribbon.eager-load.clients
中未声明,依然会在首次调用的时候进行初始化。
先自定义类,在类中声明轮询规则的 Bean
/**
* 轮询选择
*/
public class RibbonConfigurationForRoundRobinRule {
@Bean
public IRule rule() { return new RoundRobinRule(); }
}
先自定义类,在类中声明随机规则的 Bean
/**
* 随机选择
*/
public class RibbonConfigurationForRandomRule {
@Bean
public IRule rule() { return new RandomRule(); }
}
在配置类上使用 @RibbonClients
注解对多个服务进行配置规则,也可以使用 @RibbonClient
注解对单个服务进行配置规则,未配置的服务在调用的时候默认采用 。ZoneAvoidanceRule
@Configuration
@RibbonClients({
@RibbonClient(name = "stock-service", configuration = RibbonConfigurationForRandomRule.class),
@RibbonClient(name = "user-service", configuration = RibbonConfigurationForRoundRobinRule.class)
})
//@RibbonClient(name = "user-service", configuration = RibbonConfigurationForRandomRule.class)
public class RibbonConfiguration {
@Bean
@LoadBalanced
public RestTemplate template() {
return new RestTemplate();
}
}
如果 @RibbonClients
中未配置 name 的值,则针对的所有的服务采用相同的规则,即:
@Configuration
@RibbonClients({
// 调用所有服务均采用随机规则
@RibbonClient(configuration = RibbonConfigurationForRandomRule.class)
})
public class RibbonConfiguration {
@Bean
@LoadBalanced
public RestTemplate template() {
return new RestTemplate();
}
}
Feign 是 Netflix 开源的声明式 HTTP 客户端
Fegin 和 OpenFeign 的区别:
HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 Http 协 议的客户端编程工具包,并且它支持 HTTP 协议最新版本和建议。HttpClient 相比传统 JDK 自带的 URLConnection,提升了易用性和灵活性,使客户端发送 HTTP 请求变得容易,提高了开发的效率。
一个处理网络请求的开源项目,是安卓端最火的轻量级框架,由 Square 公司贡献,用于替代 HttpUrlConnection 和 Apache HttpClient。OkHttp 拥有简洁的 API、高效的性能,并支持多种协议 (HTTP/2 和 SPDY)。
HttpURLConnection 是 Java 的标准类,它继承自 URLConnection,可用于向指定网站发送 GET 请求、 POST 请求。HttpURLConnection 使用比较复杂,不像 HttpClient 那样容易使用。
RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 HTTP 服务的方法,能够大大提高客户端的编写效率。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@EnableFeignClients
注解启用 Feign 客户端@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class);
}
}
@FeignClient(name = "user-service")
public interface UserService {
@RequestMapping("/user/profile/{username}")
String profile(@PathVariable("username") String username);
}
@FeignClient("stock-service")
public interface StockService {
@GetMapping("/stock/out")
String out();
}
@RestController
@RequestMapping("feign-client")
public class FeignClientController {
@Resource
private StockService stockService;
@Resource
private UserService userService;
/**
* 使用 Feign 来实现调用服务
*/
@RequestMapping("test-ribbon")
public String testRibbon() {
// 调用实例所提供的接口地址,并返回结果
String result = stockService.out();
String resp = "利用 feign 进行服务调用,调用出库接口的结果为:" + result;
result = userService.profile("nano");
return resp += "
调用目标:调用查询用户信息的结果为:" + result;
}
}
这里以追加某个请求头信息为例,可以为应用中所有的 Feign 接口在调用服务的时候带有特定的值,如下所示:
@Component
public class FeignClientRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", RandomStringUtils.randomAlphanumeric(10));
}
}
这样一来,目标服务可以在控制器类中使用 @RequestHeader
注解获取由 Feign 拦截器统一追加的请求头信息了。
Spring Cloud Gateway 是Spring Cloud官方推出的第二代网关框架,定位于取代 Netflix Zuul。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的有功能。
路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组谓 词工厂、一组Filter组成。如果谓词为真,则说明请求的URL和配置的路由匹配。
即java.util.function.Predicate , Spring Cloud Gateway使用Predicate实现路由的匹配条件。
SpringCloud Gateway中 的filter分为Gateway FilIer和Global Filter。Filter可以对请求和响应进行处理。
路由就是转发规则,谓词就是是否走这个路径的条件,过滤器可以为路由添加业务逻辑,修改请求以及响应。
因为 Spring Cloud Gateway 它不能在传统的 Servlet 容器中工作,也不能构建成 War 包。所以,如果父工程如果引入了 Servlet 容器,则需要将其排除
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
server:
port: 9090
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848,localhost:8849,localhost:8850
gateway:
# 是否开启网关
enabled: true
discovery:
locator:
# 默认值是 false,如果设为true开启通过微服务创建路由的功能,
# 即可以通过微服务名访问服务。但不建议打开,因为这样暴露了服务名称
enabled: true
需要注意的是:
spring.cloud.gateway.discovery.locator.enabled
默认是 false,为了不暴露服务的名称,一般我们不这样配置。
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
spring-cloud-gateway 提供了很多的谓词来配置路由,最为关键的配置项莫过于 routes
集合中的 predicates
了。详情参考参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
predicate 可作名词译为谓词,也可作动词译为断言。不少人将它翻译为断言,我个人认为它应该作名词译为谓词更合适,因为说明文档和配置文件中它都以复数形式出现,而且通过源码可以得知,predicates 接受收的是一个 List
类型的 RouteDefinition
集合。
刚开始接触的时候,这个是很容易弄错的谓词,尤其是之前了解过 zuul 的朋友,这个 path 跟 zuul 的还有点区别。比如在订单服务中有个控制器是这样的:
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping("info")
public String info() { return "订单服务"; }
}
这个控制器在订单服务中的访问地址是:http://localhost:8081/test/info
,那在网关中为了使用 Path 来匹配上这个可以怎么配置呢?
server:
port: 9090
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848,localhost:8849,localhost:8850
gateway:
enabled: true
routes:
- id: PathRoutePredicateFactory
uri: lb://order-service
predicates:
- Path=/test/**
这样配置很容易让人误以为: Path
配置的 /test/**
是将所有以 /test
开头的全部转发到订单服务且以 /test
为前缀。想着会将 /test
和控制器中的 /test/info
拼凑起来得到的 /test/test/info
才是最终访问的地址,其实正确的访问地址是: http://localhost:9090/test/info
,是不是有点意想不到。
需要注意的是,Path 配置的路径是带匹配路径转发请求,会匹配订单服务中所有以 /test
开头的请求路径。
现在换个匹配词,如果我们将这里谓词的 Path 修改为 /v2/**
,就会匹配订单服务中所有以 /v2
开头的请求路径,就拿刚刚的测试控制器为例,它在订单服务中单独使用的时候,就是 /test/info
,那也就意味着无法访问到订单服务的这个测试控制器了。为了能匹配上,我们就得修改控制器代码如下:
@RestController
@RequestMapping(path = {"test", "v2/test"})
public class TestController {
@RequestMapping("info")
public String info() {
return "订单服务";
}
}
这样就能使得 http://localhost:9090/v2/test/info
和 http://localhost:8081/test/info
都能访问通了,当然,出来个 http://localhost:8081/v2/test/info
也能用。但这样做代价太大了,这需要改动订单服务中所有的控制器的请求地址,全部增加一个 /v2
前缀。不,我不愿意!
不改动控制器代码的情况,怎么配置能达到预期的效果呢?修改配置如下:
server:
port: 9090
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848,localhost:8849,localhost:8850
gateway:
enabled: true
routes:
- id: PathRoutePredicateFactory
uri: lb://order-service
predicates:
- Path=/v2/**
filters:
- StripPrefix=1
StripPrefix 配置项的目的就是截掉匹配模式不转发,也就是把 /v2
截掉不转发了。这样不仅能使得 http://localhost:9090/v2/test/info
和 http://localhost:8081/test/info
都能访问通了,而且控制器的代码也不需要挨个增加前缀。
如果所有有需求是,所有发往用户服务的请求头上都必须有 X-Request-Id
,且值必须是数字,可以使用如下配置:
spring:
gateway:
routes:
- id: HeaderRoutePredicateFactory
uri: lb://user-service
predicates:
- Header=X-Request-Id, \d+
例如用户服务有可用路径是 http://localhost:8083/user/profile
那么就可以在 postman 中做测试为 http://localhost:9090/user/profile
设置 X-Request-Id 请求头,请求同样能正常返回结果,没有该请求头则返回 404。
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于 服务容错 的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护 等多个维度来保护服务的稳定性。
微服务架构是一种分布式的架构,体系中的服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况。此时产生并发,就会出现多条线程阻塞等待,进而导致服务瘫痪。这种由于服务与服务之间的依赖性,会使得单点故障会传播,进而会对整个微服务系统造成灾难性的严重后果,这就是 服务雪崩效应。
发生这种事故的原因很多,最常见原因: 程序Bug,大流量请求,硬件故障,缓存击穿等等。为了防止雪崩的扩散,我们就要提升服务的容错性,常见的容错思路有隔离、超时、限流、熔断、降级这几种。
物理隔离:在不同用户的所在地区,我们可以把某个服务(比如 user-service)的实例分为好几个组,比如上海组,北京组,这样就进行了物理隔离。用户在北京调用该服务的时候,即便是出现阻塞或异常,也不会影响上海的朋友。
队列隔离:在某个实例的程序内部,我们可以为每个服务的调用都会创建一个队列,这样进行了队列隔离。当然也可以设置一个单独的线程池进行线程池隔离。
在上游服务调用下游服务的时候,上游服务设置一个最大响应时间,如果超过这个时间,下游未作出反应,上游服务就断开请求,释放掉线程。
限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题,对于一些出问题的服务可以限制流量访问。可以考虑的一些限流算法:计数器固定窗口算法、计数器滑动窗口算法、漏桶算法、令牌桶算法等。
当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。感觉是不是跟超时有点像,其实,超时也是熔断的一种措施。
服务熔断一般有三种状态:
这三种状态就像保险丝一样,熔断关闭就是保险丝还好好的,半熔断就是保险丝烧红了,还在坚持用,熔断开启就是保险丝已经烧断了。
所谓降级就是我们调用的服务异常超时等原因不能正常返回的情况下,我们返回一个缺省的值。由于降级经常和熔断一起使用,所以就会有熔断降级的说法。
Hystrix :Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止 级联失败,从而提升系统的可用性与容错性。
Resilience4J :Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推 荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel :Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
下面是三个组件在各方面的对比:
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于慢调用比例、异常比例、异常数 | 基于异常比例 | 基于异常比例、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持近十种动态数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
单机限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
集群流控 | 支持 | 不支持 | 不支持 |
流量整形 | 支持预热模式与匀速排队控制效果 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
热点识别/防护 | 支持 | 不支持 | 不支持 |
多语言支持 | Java/Go/C++ | Java | Java |
Service Mesh 支持 | 支持 Envoy/Istio | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、实时监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
Sentinel 分为两个部分:
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败(或者其他处理方式),避免影响到其它的资源而导致级联故障。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
<version>1.8.1version>
dependency>
@RestController
@RequestMapping("sentinel/resource")
public class SentinelResourceController {
public static final String PROTECTED_RESOURCE_NAME = "Hello Sentinel!";
/**
* 获取受保护的资源名称
*/
@GetMapping("name")
public ResponseEntity<String> getProtectedResourceName() {
Entry entry = null;
// 务必保证 finally 会被执行
try {
// 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K)。
// 超出几千请作为参数传入而不要直接作为资源名
entry = SphU.entry(PROTECTED_RESOURCE_NAME);
// 被保护的业务逻辑
String resp = "手动设置 Sentinel 保护的资源名称已被获取:";
return ResponseEntity.ok(resp + PROTECTED_RESOURCE_NAME);
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("被限流");
} catch (Exception ex) {
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
} finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
}
return ResponseEntity.notFound().build();
}
/**
* 定义流控规则
*/
@PostConstruct
private void init() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置受保护的资源
rule.setResource(PROTECTED_RESOURCE_NAME);
// 设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值,即 QPS 超过 1 就限流
rule.setCount(1);
rules.add(rule);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}
因为设置限流规则是 QPS 不能超过 1,所以当1s 内访问 http://localhost:8083/sentinel/resource/name
频次过高时就会触发限流结果。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-annotation-aspectjartifactId>
<version>1.8.1version>
dependency>
需要注意的是,sentinel-annotation-aspectj 已经包含了 sentinel-core,不需要重复引入
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
@Slf4j
public class SentinelException {
public static ResponseEntity<String> fallback(Throwable e) {
log.info("出现业务异常");
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return ResponseEntity.status(status).body("出现业务异常");
}
public static ResponseEntity<String> block(BlockException e) {
log.info("出现限流");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("被限流");
}
}
@RestController
@RequestMapping("sentinel/annotation/resource")
public class SentinelAnnotationResourceController {
public static final String PROTECTED_RESOURCE_NAME = "基于注解的 Sentinel 使用!";
/**
* 获取受保护的资源名称
*/
@GetMapping("name")
@SentinelResource(value = PROTECTED_RESOURCE_NAME,
fallback = "fallback", fallbackClass = SentinelException.class,
blockHandler = "block", blockHandlerClass = SentinelException.class
)
public ResponseEntity<String> getProtectedResourceName() {
String resp = "手动设置 Sentinel 保护的资源名称已被获取:";
return ResponseEntity.ok(resp + PROTECTED_RESOURCE_NAME);
}
/**
* 定义流控规则
*/
@PostConstruct
private void init() {
System.out.println("设置资源");
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置受保护的资源
rule.setResource(PROTECTED_RESOURCE_NAME);
// 设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值,即 QPS 超过 1 就限流
rule.setCount(1);
rules.add(rule);
// 加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}
需要注意的是,前一个控制器中也有设置限流规则,所以需要将前一个控制器中的 @PostConstruct
注释掉,否则可能后面的不会生效,可以考虑把这两端代码合并到一个方法中,然后将两个资源都加载到流控规则管理器中。
因为设置限流规则是 QPS 不能超过 1,所以当1s 内访问 http://localhost:8083/sentinel/annotation/resource/name
频次过高时就会触发限流结果。
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
下载官方 Sentinel 包:https://github.com/alibaba/Sentinel/releases/download/1.8.1/sentinel-dashboard-1.8.1.jar
直接使用命令执行该 jar 文件
java -Dserver.port=7070 -Dcsp.sentinel.dashboard.server=localhost:7070 -Dproject.name=Dashboard -jar sentinel-dashboard-1.8.1.jar
因为刚刚我们使用了 SentinelResourceAspect
作为 Sentinel 的流控管理,此时需要将其移除,以避免冲突。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
这里的端口号跟第二步启动 Sentinel 的时候设置的端口保持一致
spring:
cloud:
sentinel:
transport:
dashboard: localhost:7070
启动服务,然后请求服务中的一个地址,我这里是请求库存服务的出库地址 http://localhost:8082/stock/out,可以看到自己输出的结果
访问 Sentinel 控制台 http://localhost:7070,登录账号和密码默认都是 sentinel,进来后可以看到左侧会出现跟库存服务名称一样的菜单,点击簇点链路可以看到如下图所示的界面
接下来就可以在这里设置流控了,点击该页面的流控 按钮,为 /stock/out
设置流控。我这里为 QPS 设置为 1,也就意味着 1s 内超频访问就会被限流。
然后模拟请求 /stock/out
被限流后页面会显示 Blocked by Sentinel (flow limiting)。访问越快,被拒绝的就越多。然后回到Sentinel控制台 实时监控 和 簇点链路界面,会很直观的看到他的通过数和拒绝数。