请求到网关(GateWay)–>到负载均衡(Ribbon)–>进入服务实例,通过Nacos获得调用实例路径,通过Feign发送请求调用–>通过MQ进行异步消费–>使用Nacos实现配置–>注册Sentinel实现对服务的限流,隔离,熔断降级
服务网关
GateWay
基于Spring5提供的WebFlux,响应式编程,性能更优
功能:
- 身份认证和权限校验
- 服务路由、负载均衡
- 请求限流,基于redis实现令牌桶算法
- 代码实现
<!-- 网关相关配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
server:
port: 10010 #端口号
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #网关路由配置
- id: user-service #路由ID, 自定义,唯一
url: lb://userservice #路由地址,lb就是负载均衡,后边是服务名
predicates: #路由断言,判断请求是否符合规则
- Path=/user/** #按路径匹配 /user/开头请求
- id: order-service
url: lb://orderservice #路由地址,lb就是负载均衡,后边是服务名
predicates: #路由断言,判断请求是否符合规则
- Path=/order/** #按路径匹配 /order/开头请求
filters: # 过滤器
- AddRequestHeader=Truth,Itcast is freaking aowsome! #添加请求头
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader=Truth,Itcast is freaking aowsome! #添加请求头
路由配置参数
- id
- url
- predicates断言
4.filter过滤器
- default-filters默认过滤器,通过spring配置文件实现
- GlobalFilter自定义过滤器,可手动配置添加处理逻辑
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// @Order(-1) //设置全局过滤器执行顺序
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取参数中的 authorization 参数
String auth = params.getFirst("authorization");
// 3.判断参数值是否等于 admin
if ("admin".equals(auth)) {
// 4.是,放行
return chain.filter(exchange);
}
// 5.否,拦截
// 5.1.设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2.拦截请求
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}
- 过滤器执行顺序
跨域问题
spring:
application:
name: gateway #服务名称
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]': #拦截的请求
allowedOrigins: #允许跨域的请求
- "http://localhost:8080"
allowedMethods: #运行跨域的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #允许请求中携带的头信息
allowedCredentials: true #是否允许携带cookie
maxAge: 36000 #跨域检测的有效期,单位s
Zuul
基于Servlet实现,阻塞式编程
待完善
负载均衡
Ribbon
1、服务发起请求,RibbonLoadBanlancerClient拦截请求
2、解析请求获取请求中的服务Id,将服务Id发送到DynamicServerListLoadBalancer
3、DynamicServerListLoadBalancer将拉取eureka的服务列表
4、将返回的服务列表发送到IRule中进行负载均衡
5、选择某个服务后,返回到RibbonLoadBanlancerClient中修改URL并发起请求
Ribbon负载策略
内置负载均衡类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务。它是Ribbon默认的负载均衡规则 |
AvailablilityFilteringRule | 对以下两种服务进行忽略1、在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何的增长。2、并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发并发连接数的上线,可以有客户端的…ActiveConnectionsLimit属性进行配置 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器 |
RandomRule | 随机选择一个可用的服务器 |
RetryRule | 重试机制的选择逻辑 |
修改Ribbon负载均衡策略的方式
- 代码方式:在Order-service中的OrderApplication类中,定义一个新的IRule
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate对象并注入Spring
* 容器
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 作用范围是全局的,即全部微服务
@Bean
public IRule randomRule() {
return new RandomRule();
}
- 配置文件方式:在Order-service的application.yml文件中,添加新的配置也可以修改规则
userservice:
ribbon: # 作用范围是某个微服务
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
Ribbon饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低一次访问的耗时,通过下面配置开启饥饿加载
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: orderservice #指定饥饿加载的名称
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients:
- orderservice #指定饥饿加载的名称 列表
- userservice
Nacos
待完善
注册中心
Nacos
Nacos注册中心流程
服务注册到Nacos
- 添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 服务中配置Nacos地址
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
Nacos服务分级存储
配置服务集群
spring:
cloud:
nacos:
server-addr: localhost:8848 #nacos服务端地址
discovery:
cluster-name: HZ #配置集群名称,也就是机房的位置
ephemeral: false #设置为非临时实例 (临时实例主动向Nacos发送心跳,服务挂掉Nacos删除服务、非临时实例Nacos向服务发送心跳,服务挂掉不会被删除)
配置负载均衡策略
Nacos默认策略为轮询
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则
Nacos按权重负载可在页面中设置权重值
Nacos环境隔离
- 在控制台创建namespace
- yml中添加创建的namespace
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: HZ
namespace: c0ae74d4-276e-449d-9bd4-055fef48bf86 #命名空间id
服务不能跨namespace访问其他服务
Nacos注册表结构
/**
* Map(namespace, Map(group::serviceName, Service))
*/
private final Map<String, Map<String, Service>> serviceMap = new concurrentHashMap<>();
private Map<String, Cluster> clusterMap = new HashMap<>();
private Set<Instance> persistenInstances = new HashSet<>();
private Set<Instance> ephemeralInstances = new HashSet<>();
首先最外层是一个Map,结构为:Map<String, Map<String, Service>>:
key:是namespace_id,起到环境隔离的作用。namespace下可以有多个group
value:又是一个Map<String, Service>,代表分组及组内的服务。一个组内可以有多个服务
key:代表group分组,不过作为key时格式是group_name:service_name
value:分组下的某一个服务,例如userservice,用户服务。类型为Service,内部也包含一个Map<String,Cluster>,一个服务下可以有多个集群
key:集群名称
value:Cluster类型,包含集群的具体信息。一个集群中可能包含多个实例,也就是具体的节点信息,其中包含一个Set,就是该集群下的实例的集合
Instance:实例信息,包含实例的IP、Port、健康状态、权重等等信息
每一个服务去注册到Nacos时,就会把信息组织并存入这个Map中。
Nacos如何支撑数十万服务注册压力
- 集群化
- 内部接收到的注册请求不会立即处理,而是将服务注册任务放到一个阻塞队列后立即响应客户端,然后利用线程池读取阻塞队列中的任务,异步完成实例更新。
Nacos如何避免并发读写冲突
- (写冲突解决)通过加锁sync和.newSingleScheduledExecutorService单利实现串行化执行
- (读冲突解决)更新实例时会采用CopyOnWrite,先将旧实例A拷贝一份A1,然后对A1进行更新操作A2,完成后会用更新后的A2实例覆盖原有的A实例,这样更新过程中不会影响别的请求去读操作
Eureka
心跳监视30s,超过60s?90s?会被任务服务挂掉删除服务
<!--Eureka服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
#配置eureka服务端口
server:
port: 1012
#由于eureka本身也是一个微服务所以
#需要配置服务名称以及服务地址信息
#可用于eureka集群服务
#配置eureka服务名称
spring:
application:
name: eurekaserver
#配置eureka的地址信息
eureka:
client:
service-url:
defaultZone: http://localhost:1012/eureka
<!--注册Eureka服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#配置注册到eureka服务名称
application:
name: userservice
#配置注册到eureka的地址信息
eureka:
client:
service-url:
defaultZone: http://localhost:1012/eureka
@EnableEurekaServer //表名该服务是Eureka服务注册中心
@EnableEurekaClient //Eureka客户端注解
服务调用Feign
Feign是一个声明式的httpf服务端
Feign的使用
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 开启自动装配功能
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
- 编写feign客户端接口声明
// 服务名称
@FeignClient("userservice")
public interface UserClient {
// 请求类型:GET
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
- 在service中调用feign接口
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
// 使用feign进行远程调用
User user = userClient.findById(order.getUserId());
order.setUser(user);
// 4.返回
return order;
}
}
feign集成了ribbon自动实现了负载均衡
feign的自定义配置
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如请求解析json字符串转为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign.Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试机制 |
自定义配置修改方式
- 配置文件实现方式
- 全局生效
feign:
client:
config:
default:
loggerLevel: FULL #feign的日志级别
- 局部生效
feign:
client:
config:
userservice: # 服务名称
loggerLevel: FULL #日志级别
- java代码实现方式
- 声明一个bean
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
}
- 全局配置,则把它放到@EnableFeignClients这个注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
- 如果是局部配置,则把他放到@FeignClient这个注解中
@FeignClient(value="userservice",configuration=FeignClientConfiguration.class)
配置中心
Nacos
统一配置管理
- 在nacos控制台中的配置列表进行添加配置
nacos发布的配置如下:
- 服务读取nacos发布的配置文件
- 引入nacos的配置管理客户端依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在userservice中的resource目录添加一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml
spring:
application:
name: userservice #服务名称
profiles:
active: dev #开发环境
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml #文件后缀名
# application.name、profiles.active和cloud.nacos.config.file-extension
# 组成了nacos的DataID,注意application.yml有重复的配置需要删除掉
- 在代码中读取配置
@RestController
@RequestMapping("order")
public class OrderController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/query")
public String query() {
return dateformat;
}
}
Nacos配置热更新
- 在@Value注入的变量所在的类上添加注解@RefreshScope
@RestController
@RequestMapping("order")
@RefreshScope
public class OrderController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/query")
public String query() {
return dateformat;
}
}
- 使用@ConfigurationProperties注解
- 创建一个配置类
@Data
@Component
@ConfigurationProperties(prefix = "pattern") //配置的前缀
public class PatternProperties {
/** 前缀和变量名拼接成nacos发布的配置文件的配置属性 */
private String dateformat;
}
- 在业务中使用
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private PatternProperties properties;
@GetMapping("/query")
public String query() {
return properties.getDateformat();
}
}
Nacos多环境配置共享
微服务启动时会从nacos读取多个配置文件
[spring.application.name]-[spring.profiles.active].yaml,例如userservice-dev.yaml
[spring.application.name].yaml,例如userservice.yaml
无论profiles如何改变,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件
- 在@ConfigurationProperties注解的配置类中添加共享属性
@Data
@Component
@ConfigurationProperties(prefix = "pattern") //配置的前缀
public class PatternProperties {
/** 前缀和变量名拼接成nacos发布的配置文件的配置属性 */
private String dateformat;
private String envSharedValue;
}
- 在Nacos的控制台中创建一个userservice.yaml的配置文件
多环境配置共享优先级
[spring.application.name]-[spring.profiles.active].yaml >[spring.application.name].yaml >
本地的yaml文件
消息队列
RabbitMQ
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: localhost # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: guest # 用户名
password: guest # 密码
listener:
simple:
default-requeue-rejected: false
acknowledge-mode: manual # 确认模式为手动确认-需要在代码中手动ACK
retry:
enabled: true # 开启消费者出现异常情况下,进行重试消费,默认false
max-attempts: 5 # 最大重试次数,默认为3
initial-interval: 3000 # 重试间隔时间,默认1000(单位毫秒)
# RabbitMQ可通过重试+死信队列的方式实现重新消费消息
# 当消息达到重试最大值再被拒绝后会被放到死信队列或存入DB,人工介入操作
# RabbitMQ如不设置重试参数,默认会不断重新消费该消息,可能会造成阻塞
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
String queueName = "simple.queue";
String message = "发送消息";
rabbitTemplate.convertAndSend(queueName, message);
}
//接收端代码
@Component
public class SpringRabbitListener {
// queues里使用的是队列的名称
@RabbitListener(queues = "simple.queue")
public void listenerSimpleQueue(String message) {
System.out.println("接收到的消息 : " + message);
}
}
注意:SpringAMQP一旦消费了消息就会销毁
WorkQueue
Work Queue即工作队列,可以提高消息的处理速度,避免消息堆积。由于一个消息生产者,一个消息消费者,一个消息队列。由于消息生产者比消息消费者生产的速度大于消费的速度,就会导致消息队列堆积,当消息队列堆积满后就导致消息丢失。
//绑定多个监听者
@RabbitListener(queues = "simple.queue")
public void listenerWorkQueue1(String message) throws InterruptedException {
System.out.println("work1接收到的消息 : " + message);
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenerWorkQueue2(String message) throws InterruptedException {
System.err.println("work2接收到的消息 : " + message);
Thread.sleep(200);
}
//发送多条消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void sendMessage() {
String queueName = "simple.queue";
String message = "发送消息 -- ";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
}
}
由于RabbitTemplate默认会采用轮询策略来分发给不同的消费者,所以会导致work1和work2都获取到了25条消息。但现在由于消费者的消费能力不一致,所以希望消费能力强的消费者多消费来避免消息发送出现的拥堵情况
# 通过application.yml文件配置如下来达到要求
spring:
rabbitmq:
host: localhost # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: guest # 用户名
password: guest # 密码
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完才能获取下一条消息
发布订阅模式
Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue,其中fount Exchange的名称为itcast.fanout
//配置类
@Configuration
public class FanoutConfig {
/**
* 说明交换机
* @return
*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcase.fanout");
}
/**
* 声明队列
* @return
*/
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
/**
* 绑定交换机和队列
* @param fanoutQueue1
* @param fanoutExchange
* @return
*/
@Bean
public Binding bindingFanoutExchange1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Binding bindingFanoutExchange2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
//监听者
@RabbitListener(queues = "fanout.queue1")
public void listenerFanoutQueue1(String message) {
System.out.println("fanout queue1接收到的消息 : " + message);
}
@RabbitListener(queues = "fanout.queue2")
public void listenerFanoutQueue2(String message) {
System.out.println("fanout queue2接收到的消息 : " + message);
}
Direct Exchange
Direct Exchange会根据规则路由发送到指定的Queue。
每一个Queue都与Exchange设置BindingKey,发布者发送消息时,指定消息的RoutingKey,Exchange将消息路由到BindingKey与消息的RoutingKey一致的队列
//监听者
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"), // 队列名称
exchange = @Exchange(name = "itcase.direct", type = ExchangeTypes.DIRECT), // 交换机的名称以及类型
key = {"red", "blue"}
))
public void listenDirectQueue1(String message) {
System.out.println("queue1 接收到的 " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"), // 队列名称
exchange = @Exchange(name = "itcase.direct", type = ExchangeTypes.DIRECT), // 交换机的名称以及类型
key = {"red", "yellow"}
))
public void listenDirectQueue2(String message) {
System.out.println("queue2 接收到的 " + message);
}
}
Topic Exchange
Topic Exchange与Direct Exchange类似,区别在于routingKey必须是多个单词的类别,并以.分割
Queue与Exchange指定BindingKey时可以使用通配符
#代指0个或多个单词
*代指一个单词
//监听者
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcase.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void ListenTopicQueue1(String message) {
System.out.println("queue1 接收到的 " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcase.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void ListenTopicQueue2(String message) {
System.out.println("queue2 接收到的 " + message);
}
}
链路追踪
Sentinel(信号量隔离)
限流方式
固定窗口计数器
将时间划分为多个窗口,在每个窗口周期限制能处理请求数量,超出丢弃
滑动窗口计数器
窗口更小,且不再固定窗口时间,通过(当前请求时间 - 窗口时间跨度)得出窗口起始时间并以当前请求时间为终止时间确定窗口大小
令牌桶
请求来获取到令牌才能被服务器响应,多余的请求等待或被丢弃
漏桶算法(本质是排队等待)
请求来了放到漏桶里,慢慢放请求出去,如果漏桶满了,多余请求丢弃
雪崩问题
在微服务中,一个微服务往往依赖于多个其它微服务。如果服务提供者I发生了故障,当前的应用的部分业务因为依赖于服务I,因此也会被阻塞。依赖服务I的业务请求被阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞,服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况,就是雪崩。
解决雪崩问题的常见方式
- 超时处理: 设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。(释放速度没有请求速度快,终有一天会阻塞)
舱壁模式: 限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。(服务c挂了之后还一直访问,会造成资源浪费)
熔断降级: 由熔断器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切需求。 令牌桶
流量控制: 限制业务访问的QPS(每秒钟请求的数量),避免服务因流量的突增而故障 。漏桶
如何避免因瞬间高并发流量而导致服务故障?流量控制;如何避免因服务故障引起的雪崩问题?超时处理,舱壁模式,熔断降级
微服务整合Sentinel
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
server:
port: 8088
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
流量控制
簇点链路
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点就是调用链路中的一个资源。
流控、熔断等都是针对簇点链路中的资源来设置的,因此可以点击对应资源后面的按钮来设置规则:
- 流控:流量控制
- 降级:降级熔断
- 热点:热点参数限流,是限流的一种
- 授权:请求的权限控制
流控模式
- 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流(A触发阈值对B限流)
- 使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
- 语法说明: 当/write资源访问量触发阈值时,就会对/read资源限流,避免影响/write资源。
- 关联模式需满足关系:
- 两个有竞争关系的资源
- 一个优先级高,一个优先级低
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流(对请求来源做判断和限流)
例如有两条请求链路,/test1->/common,/test2->/common,只希望统计从/test2进入到/common的请求,配置如下:
流控效果
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
假如设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10
- warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能大于指定时长
- 当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。
排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。
使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:平滑的QPS曲线,对于服务器来说是更友好的。
热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
例如,访问/goods/{id}的请求中,id参数值会有变化,热点参数限流会根据参数值分别统计QPS,统计结果:
当id=1的请求触发阈值被限流时,id值不为1的请求不受影响。
隔离和降级
限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级。
不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。
FeignClient整合Sentinel
- 修改OrderService的application.yml文件,开启Feign的Sentinel功能:Sentinel会自动监护Feign客户端,把它变成链路中的一个资源
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
- 给FeignClient编写失败后的降级逻辑(业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑)
- FallbackClass,无法对远程调用的异常做处理
- FallbackFactory,可以对远程调用的异常做处理,我们选择这种
- 在feing-api项目中定义类,实现FallbackFactory:
@Slf4j
//指定给哪个feign客户端编写
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
//创建UserClient接口实现类,实现其中的方法,编写失败降级的处理逻辑
return new UserClient() {
@Override
public User findById(Long id) {
//记录异常信息
log.error("查询用户异常", throwable);
//根据业务需求返回默认的数据,这里是空用户
return new User();
}
};
}
}
在feing-api项目中的DefaultFeignConfiguration类中注入UserClientFallbackFactory
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class,fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
重启后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:
线程隔离(舱壁模式)
线程隔离: 调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。
程隔离有两种方式实现:
- 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果(基于计数器模式,简单,开销小)
- 优点:支持主动超时(在远程调用请求的独立线程,通过线程池可以终止),支持异步调用
- 缺点:线程的额外开销比较大
- 场景:低扇出(依赖的服务少)
- 信号量隔离(Sentinel默认采用):不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求(基于线程池模式,有额外开销,但隔离控制更强)
- 优点:轻量集,无额外开销
- 缺点:不支持主动超时,不支持异步调用
- 场景:高频调用,高扇出
熔断降级
熔断降级: 在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
解读: RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
案例: 给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5、
异常比例/异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
解读: 统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
- 资源名:就是受保护的资源,例如/order/{orderId}
- 流控应用:是来源者的名单
- 如果是勾选白名单,则名单中的来源被许可访问。
- 如果是勾选黑名单,则名单中的来源被禁止访问。
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。因此,需要自定义这个接口的实现,让不同的请求,返回不同的origin。
- 默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。因此,需要自定义这个接口的实现,让不同的请求,返回不同的origin。
@Component
public class HeaderOriginParse implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
获取请求origin的方式是从reques-header中获取origin值,所以必须让所有从gateway路由到微服务的请求都带上origin头。需要通过AddRequestHeaderGatewayFilter来实现。 修改gateway服务中的application.yml,添加一个defaultFilter:
`
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway #添加名为origin的请求头,值为gateway
routes:
# ...略`
配置授权规则
自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
三个参数:
- HttpServletRequest request:request对象
- HttpServletResponse response:response对象
- BlockException e:被sentinel拦截时抛出的异常
BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
在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 + "}");
}
}
Sentinel与Hystrix对比
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系,热点数量的限流 | 基于 QPS,支持基于调用关系,热点数量的限流 |
流量整形 | 流量整形 支持慢启动、匀速排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
Docker容器化部署
背景
将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包,每个应用放到一个隔离容器去运行,避免相互干扰。同时docker将用户程序与所需调用的系统函数库一起打包,这样docker运行到不同操作系统时,直接基于打包的函数库,借助于操作系统的Linux内核来运行
镜像
docker将应用程序及其所需的依赖、函数库、环境、配置的文件打包在一起,称为镜像。镜像都是只读的
容器
镜像中的应用程序运行后形成的进程就是容器,只是docker会给容器做隔离,对外不可见
Docker Hub
DockerHub是一个docker镜像的托管平台。这样的平台称为docker registry,在国内也有类似于DockerHub的公开服务,比如阿里云镜像库等
Docker架构
Docker是一个CS架构的程序,有两个部分组成
- 服务端:docker守护线程,负责处理docker指令、管理镜像、容器等
- 客户端:通过命令或者RestAPI向Docker服务端发送指令。可以在本地或者远程向服务器发送指令
Docker安装
由于企业服务器使用的都是linux操作系统作为服务器,为此本次安装都是以CentOS为主
Docker命令
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken #安装yum,\代表指令太长一行写不完回车继续
yum install -y docker-ce #安装docker
#镜像命令
docker pull #从服务器拉取镜像
docker images #查看docker镜像
docker push #推送镜像到服务器
docker save #保存镜像为一个压缩包
docker build #构建docker镜像
docker --help #查看docker命令帮助文档
docker rmi #删除镜像
docker build -t javaweb:1.0 . # -t表示镜像的版本号,点号表示dockerfile所在的目录
docker run --name docker-web -p 8090:8090 -d javaweb:1.0 #使用docker命令运行自定义镜像
#容器命令
docker pause #docker从运行到暂停
docker unpause #docker从暂停到运行
docker stop #docker从运行到停止
docker start #docker从停止到运行
docker ps #查看所有运行的容器及状态
docker logs #查看docker日志
docker exec #进入docker容器内部
docker rm #删除指定容器,不仅回收docker容器的内存以及进程杀死,并且把硬盘的文件删除 注意docker rmi 是删除镜像
#数据卷命令
docker volume [COMMAND] #docker volume命令是数据卷操作,根据命令后跟随的command来指定下一步操作
#create 创建一个volume
#inspect 显示一个或者多个volume的信息
#prune 删除未使用的volume
#rm 删除一个或多个指定volume
什么是DockerFile
DockerFile就是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer
指令 | 说明 | 示例 |
---|---|---|
FROM | 指定基础镜像 | FROM centos:8 |
ENV | 设置环境变量,可在后面指令使用 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql-5.7.rpm /tmp |
RUN | 执行Linux的shell命令,一般是安装命令 | RUN yum install gcc |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTRYPOINT | 镜像中应用的启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar |
dockerfile文件示例
#########1.0版本##############
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
#########2.0版本##############
# 基于java:8-alpine构建镜像
FROM java:8-alpine
# 拷贝java项目的包
COPY ./docker-demo.jar /tmp/app.jar
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
#########DockerCompose版本##############
version: "3.2"
services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql" #$PWD是linux命令
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"
分布式事务解决方案
- 2PC
- 3PC
- TCC(补偿事务)
- 消息队列实现的最终一致性
- 三方框架–>seata