单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
单体架构的优缺点如下:
优点: 架构简单、部署成本低
缺点: 耦合度高(维护困难、升级困难)
分布式架构
分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
分布式架构的优缺点:
优点: 降低服务耦合、有利于服务升级和拓展
缺点: 服务调用关系错综复杂
微服务的架构特征
单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
自治:团队独立、技术独立、数据独立,独立部署和交付
面向服务:服务提供统一标准的接口,与语言和技术无关
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
SpringCloud
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
服务拆分原则
不同微服务,不要重复开发相同业务
微服务数据独立,不要访问其它微服务的数据库
微服务可以将自己的业务暴露为接口,供其它微服务调用
实现远程调用
1,user-service项目
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
2,注入RestTemplate对象
springboot的主程序类
MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//创建RestTemplate对象并注入spring容器
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3,order-service服务中,跨服务的远程调用
order-service模块的OrderService
/**
* 实现了跨服务的远程调用,其实就是发送了一次http请求
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2利用RestTemplate发起http请求,查询用户
String url = "http://localhost:8081/user/"+order.getUserId();
//发送http请求,实现远程调用
//get请求,设置返回值json封装成User对象
User user = restTemplate.getForObject(url, User.class);
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
4,测试
http://localhost:8080/order/101
{
"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,
"userId":1,
"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
}
提供者与消费者
服务提供者:
一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:
一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。
如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?
对于A调用B的业务而言:A是服务消费者,B是服务提供者
对于B调用C的业务而言:B是服务消费者,C是服务提供者
因此,服务B既可以是服务提供者,也可以是服务消费者。
Eureka注册中心
问题1:order-service如何得知user-service实例地址?
获取地址信息的流程如下:
- user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
- eureka-server保存服务名称到服务实例地址列表的映射关系
- order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取
问题2:order-service如何从多个user-service实例中选择具体的实例?
- order-service从实例列表中利用负载均衡算法选中一个实例地址
- 向该实例地址发起远程调用
问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
- user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
- 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
- order-service拉取服务时,就能将故障实例排除了
> 注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端
1,搭建eureka-server
1.1,创建eureka-server服务
1.1.1,引入eureka依赖
<dependencies>
<!--eureka服务端,无需指定版本号,因为父工程中springboot已经指定-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
1.1.2,编写启动类
@EnableEurekaServer//自动装配开关
@SpringBootApplication
public class EurekeApplication {
public static void main(String[] args) {
SpringApplication.run(EurekeApplication.class,args);
}
}
1.1.3,编写配置文件
application.yml
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver # eureka的服务名称
eureka:
client:
service-url: # eureka的地址信息 eureka会将自己也注册到eureka服务中
defaultZone: http://127.0.0.1:10086/eureka
1.1.4,启动服务
启动微服务,然后在浏览器访问:http://127.0.0.1:10086
2,服务注册
2.1,引入依赖
在user-service,order-service的pom文件中,引入下面的eureka-client依赖:
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.2,配置文件application.yml
在user-service,order-service中,修改application.yml文件,添加服务名称、eureka地址:
2.2.1,order-service的application.yml
server:
port: 8080
spring:
datasource:省略
application:
name: orderservice # order服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
mybatis:省略
logging:省略
2.2.2,user-service的application.yml
server:
port: 8081
spring:
datasource:省略
application:
name: userservice # user服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
mybatis:省略
logging:省略
2.3,启动两个user-service实例
2.4,查看eureka-server管理页面
3,服务发现
从eureka-server拉取user-service的信息,实现服务发现
3.1,负载均衡
RestTemplate这个Bean添加一个@LoadBalanced注解:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@LoadBalanced //加入负载均衡,order去调取user或者user2
@Bean //创建RestTemplate对象并注入spring容器
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3.2,修改userservice服务名称
/**
* 实现了跨服务的远程调用,其实就是发送了一次http请求
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2利用RestTemplate发起http请求,查询用户
//userservice是user-service application.yml配置的映射名
//userservice替代localhost:8081
String url = "http://userservice/user/"+order.getUserId();
//发送http请求,实现远程调用
//get请求,设置返回值json封装成User对象
User user = restTemplate.getForObject(url, User.class);
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
3.3,测试
http://localhost:8080/order/101
{
"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,
"userId":1,
"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
}
Ribbon负载均衡
基本流程如下:
- 拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求
负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同规则的含义如下:默认的实现就是ZoneAvoidanceRule,是一种轮询方案
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
修改负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
方式1:代码方式(作用于order-service中调用别的任何微服务)
在order-service中的OrderApplication类中,定义一个新的IRule:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
//加入负载均衡,order去调取user或者user2
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public IRule randomRule(){
return new RandomRule();
}
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
方式2:配置文件方式(只作用于userservice)
在order-service的application.yml文件中,添加新的配置也可以修改规则:
server:
port: 8080
spring:
datasource:省略
application:
name: orderservice # order服务名称
eureka:
client:
service-url: # eureka的地址信息 eureka会将自己也注册到eureka服务中
defaultZone: http://127.0.0.1:10086/eureka
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
mybatis:省略
logging:省略
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
server:
port: 8080
spring:
datasource:省略
application:
name: orderservice # order服务名称
eureka:
client:
service-url: # eureka的地址信息 eureka会将自己也注册到eureka服务中
defaultZone: http://127.0.0.1:10086/eureka
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
- userservice
mybatis:省略
logging:省略
Nacos注册中心
国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos Windows安装
将nacos-server-1.4.1.zip解压到任意非中文目录下
在安装目录,shift右击,打开"Windows PowerShell",执行".\startup.cmd -m standalone"启动nacos
访问 http://192.168.0.106:8848/nacos/index.html
输入用户名nacos,密码nacos登录
服务注册到nacos
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:依赖不同、服务地址不同
注释掉eureka的依赖。
1,引入依赖
在cloud-demo父工程的pom文件中的<dependencyManagement>
中引入SpringCloudAlibaba的依赖:
<!--nacos的管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
然后在user-service和order-service中的pom文件中引入nacos-discovery依赖,注释掉eureka依赖:
<!--eureka客户端依赖-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2,配置nacos地址
在user-service和order-service的application.yml中添加nacos地址:
注意:需要注释掉eureka的地址
server:
port: 8081
spring:
datasource:省略
application:
name: userservice # user服务名称
cloud:
nacos:
server-addr: localhost:8848
#eureka:
# client:
# service-url: # eureka的地址信息 eureka会将自己也注册到eureka服务中
# defaultZone: http://127.0.0.1:10086/eureka
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
- userservice
mybatis:省略
logging:省略
3,重启微服务
重启微服务后,登录nacos管理页面,可以看到微服务信息:
http://192.168.0.106:8848/nacos/index.html
nacos服务分级存储模型
Nacos就将同一机房内的实例 划分为一个集群。
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群
给user-service配置集群
1,修改user-service的application.yml文件,添加集群配置:
server:
port: 8081
spring:
datasource:省略
application:
name: userservice # user服务名称
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: bj XA # 集群名称,自定义XA表示西安集群 BJ代表北京集群
mybatis:省略
logging:省略
2,重启3个user-service实例后,我们可以在nacos控制台看到下面结果:
user-service服务
相关集群详情
同集群优先的负载均衡NacosRule
默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule
的实现,可以优先从同集群中随机挑选实例。
1,给order-service配置集群信息并且修改负载均衡规则
修改order-service的application.yml文件,添加集群配置、修改负载均衡规则:
server:
port: 8080
spring:
datasource:省略
application:
name: orderservice # order服务名称
cloud:
nacos:
server-addr: localhost:8848
# 1,给order-service配置集群信息
discovery:
cluster-name: xa # 集群名称,自定义XA表示西安集群 BJ代表北京集群
userservice: # 配置负载均衡规则
ribbon:
# 2,修改负载均衡规则
NFLoadBalancerRuleClassName:
com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
- userservice
mybatis:省略
logging:省略
2,order service访问时就会优先访问本集群的user service。
但是如果本集群user service异常,也会访问别的集群,但是会报警告,
A cross-cluster call occurs
权重配置
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
注意:如果权重修改为0,则该实例永远不会被访问
环境隔离
Nacos提供了namespace来实现环境隔离功能。
nacos中可以有多个namespace
namespace下可以有group、service等
不同namespace之间相互隔离,例如不同namespace的服务互相不可见
创建namespace
默认情况下,所有service、data、group都在同一个namespace,名为public:
我们可以点击页面新增按钮,添加一个namespace:
然后,填写表单:
就能在页面看到一个新的namespace:
给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现。
例如,修改order-service的application.yml文件:
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: XA # 集群名称,自定义XA表示西安集群 BJ代表北京集群
namespace: 6269166d-9ecb-4cc9-bd97-739fdf6afd7b # 命名空间,填ID
重启order-service后,访问控制台,可以看到下面的结果:
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:
java.lang.IllegalStateException: No instances available for userservice
Nacos与Eureka的区别
Nacos的服务实例分为两种类型:
临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring:
datasource:省略
application:
name: orderservice # order服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: XA # 集群名称,自定义XA表示西安集群 BJ代表北京集群
namespace: 6269166d-9ecb-4cc9-bd97-739fdf6afd7b # 命名空间,填ID
ephemeral: false # 是否是临时实例
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
Nacos与eureka的共同点
都支持服务注册和服务拉取
都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别
Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
临时实例心跳不正常会被剔除,非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos配置管理
Nacos除了可以做注册中心,同样可以做配置管理来使用。
Nacos统一配置管理
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
在nacos中添加配置文件
如何在nacos中管理配置呢?
然后在弹出的表单中,填写配置信息:
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
userservice-dev.yaml
pattern:
dateformat: yyyy-MM-dd HH:mm:ss
从微服务拉取配置
微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
1,添加nacos配置
nacos配置管理-->配置列表-->新建userservice-dev.yaml
pattern:
dateformat: yyyy-MM-dd HH:mm:ss
2,引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
3,添加bootstrap.yaml
然后,在user-service中添加一个bootstrap.yaml文件,内容如下:
spring:
application:
name: userservice # 服务名称
profiles:
active: dev # 开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置。
本例中,就是去读取userservice-dev.yaml
:
4,读取nacos配置
在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
5,测试
http://localhost:8081/user/now
显示如下:
2024-05-06 17:55:21
nacos配置热更新
修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
要实现配置热更新,可以使用两种方式:
方式一:在@Value注入的变量,所在类上添加注解@RefreshScope
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
方式二:使用@ConfigurationProperties注解代替@Value注解
在user-service服务中,添加一个类,读取patterrn.dateformat属性:
@ConfigurationProperties注解使用
配置类
@Data
@Component
//@ConfigurationPropertie将application.yml配置的值注入到bean上,必须将对象注入IOC容器中才有配置绑定的功能
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
Controller类
@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
@Autowired
private PatternProperties properties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
}
nacos配置共享
userservice.yaml不包含环境,可以被多个环境dev、prod等共享。
1,添加一个环境共享配置
在nacos中添加一个userservice.yaml文件:
2,在user-service中读取共享配置
在user-service服务中,修改PatternProperties类,读取新添加的属性:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
//@ConfigurationPropertie将application.yml配置的值注入到bean上,必须将对象注入IOC容器中才有配置绑定的功能
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
private String envSharedValue;
}
在user-service服务中,修改UserController,添加一个方法:
@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
@Autowired
private PatternProperties properties;
@GetMapping("prop")
public PatternProperties properties(){
return properties;
}
}
3,使用不同的profile
不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。
http://localhost:8081/user/prop
{
"dateformat": "yyyy-MM-dd HH:mm:ss",
"envSharedValue": "多环境共享"
}
配置共享的优先级
nacos配置 > 本地配置
nacos各环境配置 > nacos不包含环境配置
Nacos集群搭建
其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。我们计划的集群结构:
三个nacos节点的地址:
节点 | ip | port |
---|---|---|
nacos1 | 192.168.150.1 | 8845 |
nacos2 | 192.168.150.1 | 8846 |
nacos3 | 192.168.150.1 | 8847 |
搭建集群
搭建集群的基本步骤:
1,搭建数据库,初始化数据库表结构,
首先新建一个数据库,命名为nacos,而后导入下面的SQL:
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL ,
`gmt_modified` datetime NOT NULL ,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
2,下载nacos安装包
3,配置nacos,
进入nacos的conf目录,然后修改application.properties文件,添加数据库配置
application.properties
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
修改配置文件cluster.conf.example,重命名为cluster.conf
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847
4,启动nacos集群
将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
然后分别修改三个文件夹中的application.properties,
nacos1中修改:server.port=8845
nacos2中修改:server.port=8846
nacos3中修改:server.port=8847
然后分别启动三个nacos节点:
shift+右击,打开Windows powershell,运行startup.cmd命令
5,nginx反向代理
修改conf/nginx.conf文件,配置如下:
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 81;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
而后在浏览器访问:http://localhost:81/nacos即可。
userservice代码中application.yml文件配置如下:
spring:
cloud:
nacos:
server-addr: localhost:81 # Nacos地址
启动userservice1,userservice2就可以在nacos控制台看到服务列表里有启动的userservice了
遇到问题:无法向配置列表中添加配置,本地安装的nacos集群是1.x版本,也不是网上说的需要增加个字段的问题
Feign远程调用
以前利用RestTemplate发起远程调用的代码:
存在下面的问题:
代码可读性差,编程体验不统一
参数复杂URL难以维护
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2利用RestTemplate发起http请求,查询用户
//服务地址"userservice"是映射的Instances currently registered with Eureka上的application名字
String url = "http://userservice/user/"+order.getUserId();
//发送http请求,实现远程调用
//get请求,设置返回值json封装成User对象
User user = restTemplate.getForObject(url, User.class);
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
Feign替代RestTemplate
1,引入依赖
我们在order-service服务的pom文件中引入feign的依赖:
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2,添加@EnableFeignClients注解
在order-service的启动类添加注解开启Feign的功能:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients//自动装配开关
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3,编写Feign的客户端
在order-service中新建一个接口,内容如下:
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
4,使用FeignClient中定义的方法代替RestTemplate
修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:
//实现了跨服务的远程调用,其实就是发送了一次http请求
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2使用feign远程调用
User user = userClient.findById(order.getUserId());
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
Feign自定义配置如日志级别
Feign可以支持很多的自定义配置,如下表所示:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
以日志为例来演示如何自定义配置
配置文件方式
基于配置文件修改feign的日志级别可以针对单个服务:
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
而日志的级别分为四种:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Java代码方式
先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
@MapperScan("cn.benjamin.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
如果是局部生效,则把它放到对应的@FeignClient这个注解中:
//局部有效
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
使用连接池优化Feign
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
底层客户端 | 连接池 | 优化手段 |
---|---|---|
URLConnection | 默认实现,不支持连接池 | |
Apache HttpClient | 支持连接池 | 提高Feign的性能主要手段就是使用连接池代替默认的URLConnection |
OKHttp | 支持连接池 | 提高Feign的性能主要手段就是使用连接池代替默认的URLConnection |
这里我们用Apache的HttpClient来演示。
1,引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入feign HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2,配置连接池
在order-service的application.yml中添加配置,日志级别尽量用basic
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 单个路径的最大连接数
最佳实践
Feign的客户端与服务提供者的controller代码非常相似:
继承方式
一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都集成改接口
抽取方式
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
1,抽取 首先创建一个module,命名为feign-api
在feign-api中然后引入feign的starter依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
@FeignClient(value = "userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
@Data
public class User {
private Long id;
private String username;
private String address;
}
2,在order-service中使用feign-api
首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。
在order-service的pom文件中中引入feign-api的依赖:
注意:如果使用的是idea ide的多模块话,需要模块之间的依赖。需要把parent工程,也就是package是pom的那个工程先install一下
<dependency>
<groupId>cn.test.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包
import cn.test.feign.clients.UserClient;
import cn.test.feign.config.DefaultFeignConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@MapperScan("cn.test.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
import cn.test.feign.clients.UserClient;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
3,解决扫描包问题
因为@SpringBootApplication只会扫描到本项目下的包,com.api.clients.UserClient是引入的第三方jar,需要再指定扫描
方式一:
指定Feign应该扫描的包:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
//basePackages指定feign client所在包
@EnableFeignClients(basePackages = "com.api.clients",defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
方式二:
指定需要加载的Client接口:推荐方式二
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
//clients = UserClient.class指定加载哪个客户端
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
Gateway服务网关
为什么需要网关?
Gateway网关是我们服务的守门神,所有微服务的统一入口。
网关的核心功能特性:
核心功能 | remark |
---|---|
权限控制 | 网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。 |
路由和负载均衡 | 一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。 |
限流 | 当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。 |
gateway快速入门
1,新建SpringBoot工程gateway,引入nacos服务发现和gateway依赖
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2,编写启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
3,配置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
1. 路由id:路由的唯一标示
2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
3. 路由断言(predicates):判断路由的规则,
4. 路由过滤器(filters):对请求或响应做处理
创建application.yml文件,内容如下:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
4,启动网关服务进行测试
重启网关,访问http://localhost:10010/user/1时,符合/user/**
规则,请求转发到http://userservice/user/1
predicates断言工厂
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
比如Before和Path
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] # 时间要求在2031年之前
网关过滤器
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
路由过滤器的种类
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
局部请求头过滤器
以AddRequestHeader 为例,添加局部过滤器
只需要修改gateway服务的application.yml文件,添加路由过滤即可:
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, you are freaking awesome! # 添加请求头
全局默认过滤器DefaultFilter
如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, you are freaking awesome!
全局过滤器GlobalFilter
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
自定义全局过滤器例子
1,在gateway中定义一个过滤器
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
@Order(-1)//顺序注解,说明filter执行顺序。数字越大越往后
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1,获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
//2,获取参数中的authorization参数
String authorization = queryParams.getFirst("authorization");
//3,判断参数值是否等于admin
if("admin".equals(authorization)){
//4,放行
return chain.filter(exchange);
}
//5,拦截
//5.1,设置状态码为401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//5.2,拦截请求
return exchange.getResponse().setComplete();
}
}
2,重启gateway模块,访问
2.1,携带请求参数,放行
http://localhost:10010/user/1?authorization=admin
{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
2.2,未携带请求参数,拦截请求
http://localhost:10010/user/1
HTTP ERROR 401
过滤器Filter执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
排序的规则是什么呢?
1,每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
2,GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
3,路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
4,当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
跨域问题
什么是跨域问题?
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org(一级域名不同)
和 www.jd.com 和 miaosha.jd.com(三级域名不同)
域名相同,端口不同:localhost:8080和localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
# filters: # 路由的过滤器
# - AddRequestHeader=Truth,good good!!!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
default-filters:
- AddRequestHeader=Truth,you is freaking awesome!
# 跨域问题解决配置
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
或者添加以下配置类
@SpringBootConfiguration
public class WebGlobalConfig {
@Bean
public CorsFilter corsFilter() {
//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域
config.addAllowedOriginPattern("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//放行哪些请求方式
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //放行全部请求
//是否发送Cookie
config.setAllowCredentials(true);
//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}
微服务保护
雪崩问题
微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。
那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败,雪崩就发生了:
解决雪崩问题的常见方式有四种
流量控制是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。
超时处理、舱壁模式、熔断降级是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。
1,超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
2,舱壁模式:舱壁模式来源于船舱的设计,船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
3,熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。断路器会统计访问某个服务的请求数量,异常比例
当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断
4,流量控制:限制业务访问的QPS(每秒处理的请求数量),避免服务因流量的突增而故障。
安装Sentinel
1,将jar包sentinel-dashboard-1.8.1.jar放到任意非中文目录,执行命令
java -jar sentinel-dashboard-1.8.1.jar
如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
| **配置项** | **默认值** | **说明** |
| -------------------------------- | ---------- | ---------- |
| server.port | 8080 | 服务端口 |
| sentinel.dashboard.auth.username | sentinel | 默认用户名 |
| sentinel.dashboard.auth.password | sentinel | 默认密码 |
例如,修改端口:
java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar
2,访问http://localhost:8080页面,就可以看到sentinel的控制台了:
需要输入账号和密码,默认都是:sentinel
微服务整合Sentinel
在order-service中整合sentinel,并连接sentinel的控制台,步骤如下:
1,引入sentinel依赖
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2,修改application.yaml文件,添加下面内容:
server:
port: 8088
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制台地址
3,访问order-service的任意端点
打开浏览器,访问http://localhost:8088/order/101,这样才能触发sentinel的监控。然后再访问sentinel的控制台查看监控。
流量控制
簇点链路
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
例如,我们刚才访问的order-service中的OrderController中的端点:/order/{orderId}
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
流控:流量控制
降级:降级熔断
热点:热点参数限流,是限流的一种
授权:请求的权限控制
流控模式
在添加限流规则时,点击高级选项,可以选择三种流控模式:
流控模式 | remark |
---|---|
直接模式 | 统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式 |
关联模式 | 统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流 |
链路模式 | 统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流 |
1,流控模式–直接模式
1.1,点击资源/order/{orderId}后面的流控按钮,就可以弹出表单。
1.2,在sentinel控制台添加限流规则
1.3,利用jmeter测试
选中流控入门,QPS<5
右键运行:
20个用户,2秒内运行完,QPS是10,超过了5
1.4,查看Sentinel 控制台,可以看到,成功的请求每次只有5个
2,关联模式
关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
两个有竞争关系的资源
一个优先级较高,一个优先级较低
配置规则:
在OrderController新建两个端点:/order/query和/order/update,无需实现业务.配置流控规则,当/order/ update资源被访问的QPS超过5时,对/order/query请求限流
2.1,定义/order/query端点,模拟订单查询。定义/order/update端点,模拟订单更新
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("{orderId}")
public Order queryContextOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
@GetMapping("/query")
public String queryOrder(){
return "查询订单成功";
}
@GetMapping("/update")
public String updateOrder(){
return "修改订单成功";
}
}
2.2,重启服务,查看sentinel控制台的簇点链路
2.3,配置流控规则
对哪个端点限流,就点击哪个端点后面的按钮。我们是对订单查询/order/query限流,因此点击它后面的按钮:
在表单中填写流控规则:
2.4,在Jmeter测试
可以看到1000个用户,100秒,因此QPS为10,超过了我们设定的阈值:5
2.5,但限流的目标是/order/query,我们在浏览器访问,可以发现
3,链路模式
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
3.1,在OrderService中添加一个queryGoods方法,不用实现业务
@SentinelResource("goods")
public void queryGoods(){
//查询商品数据库
}
3.2,在OrderController中,改造/order/query端点,调用OrderService中的queryGoods方法
@GetMapping("/query")
public String queryOrder() {
// 查询商品
orderService.queryGoods();
// 查询订单
System.out.println("查询订单");
return "查询订单成功";
}
3.3,在OrderController中添加一个/order/save的端点,调用OrderService的queryGoods方法
@GetMapping("/save")
public String saveOrder() {
// 查询商品
orderService.queryGoods();
// 查询订单
System.err.println("新增订单");
return "新增订单成功";
}
3.4,给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPS必须小于2
只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息:
3.5,关闭context整合
server:
port: 8088
spring:
datasource:省略
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制台地址
web-context-unify: false # 关闭context整合
datasource:
flow:
nacos:
server-addr: localhost:8848 # nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow # 还可以是:degrade、authority、param-flow
3.6,Jmeter测试
一个http请求是访问/order/save,结果完全不受影响:
另一个是访问/order/query:
流控效果
在流控的高级选项中,还有一个流控效果选项:
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
快速失败 | warm up 预热模式 | 排队等待 |
---|---|---|
达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式 | 对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值 | 让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长 |
1,warm up 预热模式
阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
例如设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
1.1,配置流控规则:
1.2,Jmeter测试
1.3,Sentinel控制台查看实时监控
2,排队等待
排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。
那什么叫做预期等待时长呢?
比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:
第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms
第12个请求的预期等待时长 = 200 * (12-1) = 2200ms
如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s
2.1,添加流控规则
2.2,Jmeter测试
sentinel查看实时监控的QPS曲线
QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。
当队列满了以后,才会有部分请求失败:
热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为5.
而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:
给/order/{orderId}这个资源添加热点参数限流,规则如下:
默认的热点参数规则是每1秒请求量不超过2
给102这个参数设置例外:每1秒请求量不超过4
给103这个参数设置例外:每1秒请求量不超过10
注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源
1,标记资源
给order-service中的OrderController中的/order/{orderId}资源添加注解
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@SentinelResource("hot")
@GetMapping("{orderId}")
public Order queryContextOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
}
2,热点参数限流规则
点击左侧菜单中热点规则菜单
点击新增,填写表单
3,Jmeter测试
4,运行结果
101资源每秒只能成功2个,102资源每秒成功4个,103资源每秒5次请求全部成功
FeignClient整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
注意:feign注入报循环依赖问题,一般是<spring-cloud.version>Hoxton.SR8</spring-cloud.version>版本问题
1,修改配置,开启sentinel功能
修改OrderService的application.yml文件,开启Feign的Sentinel功能:
feign:
httpclient:
enabled: true # 支持HttpClient的开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
sentinel:
enabled: true # 开启feign对sentinel的支持
2,编写失败降级逻辑
feing-api项目中定义类,实现FallbackFactory
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
//失败降级逻辑
log.error("查询用户异常", throwable);
return new User();
}
};
}
}
3,在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean
public class DefaultFeignConfiguration {
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
4,在feing-api项目中的UserClient接口中使用UserClientFallbackFactory
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
5,重启后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路
线程隔离(舱壁模式)
线程隔离有两种方式实现:
Hystix | Sentinel |
---|---|
Hystix默认是基于线程池实现的线程隔离 | Sentinel是基于信号量(计数器)实现的线程隔离 |
每一个被隔离的业务都要创建一个独立的线程池 | 不用创建线程池 |
支持主动超时,支持异步调用 | 不支持主动超时,不支持异步调用 |
线程的额外开销比较大,性能一般,但是隔离性更强 | 轻量级,无额外开销,性能较好,但是隔离性一般 |
sentinel的线程隔离(信号量隔离)
案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试
1,配置隔离规则
选择feign接口后面的流控按钮:
填写表单:
2,Jmeter测试
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
熔断状态 | |
---|---|
closed关闭状态 | 断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态 |
open打开状态 | 服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态 |
half-open半开状态 | 放行一次请求,根据执行结果来判断接下来的操作。请求成功:则切换到closed状态;请求失败:则切换到open状态 |
熔断策略
慢调用、异常比例、异常数
慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
例如:
1,设置慢调用规则
解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
2,设置慢调用
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
if (id == 1) {
// 休眠,触发熔断
Thread.sleep(500);
}
System.out.println("truth:" + truth);
return userService.queryById(id);
}
}
3,测试
在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:
触发了熔断,请求时长缩短至5ms,快速失败了,直接走降级逻辑,返回的null
在浏览器访问:http://localhost:8088/order/102,也被熔断了,当超过5s再次访问时,请求恢复正常。
异常比例、异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
异常比例设置:
统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
异常数设置:
统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。
设置异常请求
修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例/异常数的熔断:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
if (id == 1) {
// 休眠,触发熔断
Thread.sleep(500);
} else if (id == 2) {
throw new RuntimeException("故意出错,触发熔断");
}
System.out.println("truth:" + truth);
return userService.queryById(id);
}
}
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问
其实就是给允许访问的资源加上特定头信息,在RequestOriginParser实现类中进行判断,如果符合授权规则就允许访问
比如:
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)
1,例如order-service服务中,我们定义一个RequestOriginParser的实现类:
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
2,给网关添加请求头
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。
修改gateway服务中的application.yml,添加一个defaultFilter:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
3,配置授权规则
4.1,直接访问order-service服务
4.2,通过网关访问order-service服务
自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
1,自定义异常处理
在order-service定义一个自定义异常处理类:
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
2,重启测试,在不同场景下,会返回不同的异常消息.
限流:
localhost:8088/order/103
{
"msg": 请求被限流了,
"status": 429
}
授权拦截时:
localhost:8088/order/103
{
"msg": 没有权限访问,
"status": 401
}
Sentinel 规则持久化
1,引入依赖 在order-service中引入sentinel监听nacos的依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2,配置nacos地址
在order-service中的application.yml文件配置nacos地址及监听的配置信息:
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制台地址
web-context-unify: false # 关闭context整合
datasource:
flow:
nacos:
server-addr: localhost:8848 # nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow # 还可以是:degrade、authority、param-flow
3,修改sentinel-dashboard源码
4,启动修改后的sentinel-dashboard
java -jar sentinel-dashboard.jar
5,添加持久化后的sentinel配置
将流控规则添加到“流控规则-NACOS”
6,查看nacos控制台
7,重启order service后,sentinel配置依然在