# spring-cloud-study **Repository Path**: chmingx/spring-cloud-study ## Basic Information - **Project Name**: spring-cloud-study - **Description**: SpringCloud学习笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-07-07 - **Last Updated**: 2022-11-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringCloud [TOC] ## 1 概述 分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶 - java8 - maven - git - Nginx - rabbitMQ - SpringBoot2.0 组成 - 服务注册与发现 eureka, zookeeper, nacos - 服务调用 netflix oss ribbon, LoadBalancer - 服务熔断 hystrix - 负载均衡 feign, openfeign - 服务降级 hystrix, Sentinel - 服务消息队列 MQ - 配置中心管理 config, nacos - 服务网关 zuul, gateway - 服务监控 hystrix - 全链路追踪 - 自动化构建部署 - 服务定时任务调度操作 - 服务总线 bus, nacos - 服务分布式配置 spring cloud config, nacos - 服务开发 spring boot https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/ https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/ - spring boot 2.2.2 - spring cloud H版 maven中 - dependencyManagement: 用来提供一种管理依赖版本号的方式,通常,在一个组织或项目中的最顶层的父POM中看到dependencyManagement元素, - 子项目添加依赖时可以不用指定版本号,而使用父POM中指定的版本号 - dependencyManagement只是声明依赖,并不实际引入,因此子项目还是需要显示声明所需的依赖 - 如果不在子项目中声明依赖,是不会从父项目中继承下来的,只有在子项目中写明了依赖,并且没有指明具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom - 如果子项目指定了具体版本,则使用子项目的版本 - dependencies: docker启动mysql ```shell $ docker run -d -p 3306:3306 -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql/conf/mysql.cnf:/etc/mysql/mysql.cnf -e MYSQL_ROOT_PASSWORD=123456 --name mysql-service docker.io/mysql ``` ## 2 微服务架构编码构建 建项目,指定包和版本-->建模块 1. 建数据库 2. 实体类 3. dao 4. 映射文件xml 5. service 6. controller 开启热部署 - 添加依赖 ```xml org.springframework.boot spring-boot-devtools runtime true ``` - 父工程中添加maven插件 ```xml org.springframework.boot spring-boot-maven-plugin true true ``` - idea设置compiler中ADBC四项全部✔ - 注册 - `ctrl+shift+alt+/`打开registry - 勾选: - `compiler.automake.allow.when.app.ruunning` - `actionSystem.assertFocusAccessFromEdt` - 重启idea 微服务间的调用使用RestTemplate::green_apple: RestTemplate提供了多种便捷访问远程Http服务的方法 是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集 :gift: cloud01, 微服务间的调用 工程重构: - 提取公共部分为模块 - maven install clean模块到maven仓库 - 其他模块引入公共模块的jar包 :gift: cloud02, 单机版 ## 3 服务注册 ### 3.1Eureka #### 3.1.1 服务治理 在传统的rpc调用框架中,管理每个服务与服务直接依赖关系比较复杂,所以需要使用服务治理,管理服务与服务之间的依赖关系,可以实现服务调用,负载均衡,容错等,实现服务发现与注册 Eureka采用了C/S的设计架构 Eureka Server: 各个为服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到 EurekaClient:通过注册中心进行访问,是一个Java客户端,与EurekaServer交互,客户端同时也具备一个内置的,使用轮询负载均衡算法的负载均衡器,在应用启动后,将会向EurekaServer发送心跳,。如果EurekaServer没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除 #### 3.1.2 eureka-server - 依赖 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` - 配置 ```yaml server: port: 7001 eureka: instance: hostname: localhost # eureka服务端的实例名称 client: register-with-eureka: false # 不向注册中心注册自己 fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务 service-url: # 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaServer // 标注是eureka服务 public class EurekaMain { public static void main(String[] args) { SpringApplication.run(EurekaMain.class, args); } } ``` #### 3.1.3 Eureka-client - 依赖 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` - 配置 ```yaml eureka: client: register-with-eureka: true # 将自己注册进eureka server fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡 service-url: defaultZone: http://localhost:7001/eureka ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaClient public class PaymentMain { public static void main(String[] args) { SpringApplication.run(PaymentMain.class, args); } } ``` #### 3.1.4 集群 - 服务注册: 将服务信息注册进注册中心 - 服务发现:从注册中心上获取服务信息 - 实质:存key服务名,取value调用地址 :question: 微服务RPC远程服务调用最核心的是什么? 高可用 :gift: cloud03, 集群版 搭建eureka集群,实现负载均衡和故障容错 --> 互相注册,相互守望 :hammer_and_wrench: - 新建eureka-server模块,参考之前的 - 修改pom - 修改映射配置 /etc/hosts ```shell # hosts 127.0.0.1 eureka7001.com 127.0.0.1 eureka7002.com ``` - 修改yaml ```shell server: port: 7001 eureka: instance: hostname: eureka7001.com # eureka服务端的实例名称 client: register-with-eureka: false # 不向注册中心注册自己 fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务 service-url: # 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址 defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ ``` ```shell server: port: 7002 eureka: instance: hostname: eureka7002.com # eureka服务端的实例名称 client: register-with-eureka: false # 不向注册中心注册自己 fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务 service-url: # 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/ ``` - 主启动,每个eureka-server都由自己的主启动,都启动服务 - 将其他微服务注册进eureka-server集群 ```yaml eureka: client: register-with-eureka: true # 将自己注册进eureka server fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡 service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ ``` :bomb: _在同一台服务器上,以不同的端口来搭建集群,ip 或者 主机名相同时,无法形成副本。所以将其中一台迁移到了另外的服务器上了_ :books: 将支付微服务也调整为集群 - 参考之前的payment新建模块payment2 - 修改pom - 修改yaml - 主启动类 - 业务类 - 修改8001/8002的controller (实现负载均衡) :question: 此时由两个微服务payment1和payment2,如何实现负载均衡呢? ![](images/eureka_info.png) :key: 可以看到现在eureka集群有两台,而PAYMENT-SERVICE同名服务有两台机器,此时在order-service代码访问payment-service的ip和端口是写死的,不满足负载均衡。 1. 修改成通过服务名去访问payment-service. ```java public static final String PAYMENT_URL = "http://PAYMENT-SERVICE"; ``` 2. 使用 `@LocalBalanced` 赋予`RestTemplate`负载均衡的能力 ```java @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` :pushpin: 这个就是下文的ribbon的负载均衡功能 ![eureka](images/eureka.png) Actuator集群信息完善 ```yaml # client config eureka: instance: instance-id: payment8002 # 配置主机名称 prefer-ip-address: true # 访问路径可以显示ip地址 ``` Eureka注册的微服务发现 ```java // payment controller @RestController public class PaymentController { @Autowired private PaymentService paymentService; @Value("${server.port}") private String serverPort; @Resource private DiscoveryClient discoveryClient; // 用于暴露微服务信息 ... @GetMapping("/payment/discovery") public Object discovery() { List services = discoveryClient.getServices(); for (String service : services) { System.out.println(service); } List instances = discoveryClient.getInstances("PAYMENT-SERVICE"); for (ServiceInstance instance : instances) { System.out.println(instance.getInstanceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri()); } return discoveryClient; } } ``` ```java @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient // 主启动类添加服务发现注解 public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } } ``` Eureka的自我保护机制 看到 EMERGENCY!... 就说明eureka进入保护模式。 什么自我保护机制:就是指,某时刻某一个微服务不可用了,eureka不会立刻清理,依旧会对该微服务信息进行保存。如果eurekaserver在一定事件内没有收到某个微服务实例的心跳,eurekaserver就会注销该实例,默认90秒。但是当网络分区故障发生,比如卡顿、延时等,微服务与eurekaserver之间无法正常通行,以上行为可能变得非常危险,因为服务本身是健康的,此时不应该注销这个微服务。eureka通过自我保护模式来解决这个问题。eureka宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例,好死不如赖活着! 如何关闭自我保护? - 修改eureka server配置 ```yaml eureka: instance: hostname: eureka7001.com # eureka服务端的实例名称 client: register-with-eureka: false # 不向注册中心注册自己 fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务 service-url: # 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址 defaultZone: http://eureka7002.com:7002/eureka/ server: enable-self-preservation: false # 关闭自我保护 ``` - 修改eureka client配置 ```yaml eureka: client: register-with-eureka: true # 将自己注册进eureka server fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡 service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka instance: instance-id: payment8001 # 配置实例名称 prefer-ip-address: true # 访问路径可以显示ip地址 lease-renewal-interval-in-seconds: 1 # client向server发送心跳的事件间隔,单位为秒,默认30 lease-expiration-duration-in-seconds: 2 # eureka server在收到最后一次心跳后等待的时间上限,单位为秒, 默认90。超时将删除微服务 ``` ### 3.2 Zookeeper zookeeper是一个分布式协调工具,可以实现注册中心功能。 ![zookeeper](images/zookeeper.png) 关闭zookeeper防火墙后启动zookeeper服务器 ```shell $ systemctl stop firewalld ``` - pom ```xml org.springframework.cloud spring-cloud-starter-zookeeper-discovery ``` - yaml ```yaml server: port: 8004 spring: application: name: payment-service # 注册到zookeeper注册中心的名称 cloud: zookeeper: connect-string: 192.168.80.130:2181 # connect-string: 192.168.80.130:2181,192.168.80.131:2181 #集群 ``` - 主启动+controller+service等 (略) ```java @SpringBootApplication @EnableDiscoveryClient // 该注解用于向使用consul或者zookeeper作为注册中心时注册服务 public class PaymentMain8004 { public static void main(String[] args) { SpringApplication.run(PaymentMain8004.class, args); } } ``` ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/zk") public String paymentzk() { return "springcloud with zookeeper" + serverPort + UUID.randomUUID().toString(); } } ``` - zookeeper包版本冲突处理 ```xml org.springframework.cloud spring-cloud-starter-zookeeper-discovery org.apache.zookeeper zookeeper org.apache.zookeeper zookeeper 3.4.9 slf4j-log4j12 org.slf4j ``` - zookeeper服务中查看 ```shell $ zkCli.sh ls /services # 查看有哪些微服务 ls /services/payment # 获取节点列表 get /services/payment-service/7aa84ad6-5ec5-4309-a3d2-63f10e4af278 # 获取节点信息 ``` ```json // 节点信息 { "name": "payment-service", "id": "7aa84ad6-5ec5-4309-a3d2-63f10e4af278", "address": "192.168.190.1", "port": 8004, "sslPort": null, "payload": { "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", "id": "application-1", "name": "payment-service", "metadata": {} }, "registrationTimeUTC": 1627912537184, "serviceType": "DYNAMIC", "uriSpec": { "parts": [ { "value": "scheme", "variable": true }, { "value": "://", "variable": false }, { "value": "address", "variable": true }, { "value": ":", "variable": false }, { "value": "port", "variable": true } ] } } ``` - 访问 ```shell $ curl http://localhost:8004/payment/zk ``` :thinking: 在zookeeper上注册的服务节点时临时的还是持久的?临时的 :package: payment8004 + orderzk80 ### 3.3 Consul 略 :package: cloud-provider-consul-payment8006 + cloud-consumer-consul-order80 ### 3.4 Nacos ### 3.5 总结 | 组件 | 语言 | CAP | 服务监控检查 | 对外暴露接口 | Spring Cloud集成 | | --------- | ---- | ---- | ------------ | ------------ | ---------------- | | Eureka | Java | AP | 可配置支持 | HTTP | 已集成 | | Consul | Go | CP | 支持 | HTTP/DNS | 已集成 | | Zookeeper | Java | CP | 支持 | 客户端 | 已集成 | | Nacos | | AP | 支持 | | | :bulb: 分布式环境中老生常谈的一个东西 - **CAP** ``` C - consistency 强一致性 A - availability 可用性 P - partition tolerance 分区容错性 ``` CAP理论的核心是: 一个分布式系统不可能同时满足一致性,可用性和分区容错性这三个需求。CAP理论关注粒度是数据,而不是整体系统设计的策略 **CA - 单点集群,满足一致性,可用性,通常在可拓展性上不太强大 CP - 满足一致性,分区容错性的系统,通常性能不是特别的高 AP - 满足可用性,分区容错性,通过对数据一致性要求低一些。** 所以,分布式系统考虑到集群的拓展,只能选择CP 或者 AP 。 ![](images/cap.jpg) **Zookeeper 保证CP** 但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点回重新进行leader选举,问题在于选举leader的时间太长,30~120s且选举期间整个zk集群都是不可用的。这就导致在选举期间注册服务瘫痪,在云部署的环境下,因网络问题使得zk集群失去master节点很大概率会发生的事情。虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用时不能容忍的 **Eureka 保证AP** 可以容忍数据的不一致 ## 4 负载均衡服务调用 ### 4.1 Ribbon #### 4.1.1 负载均衡 Spring Cloud Ribbon是一套客户端实现负载均衡的工具,主要提供客户端软件负载均衡和服务调用。 在配置文件中列出LoadBalancer后面所有的机器,Ribbon会自动的帮助你基于吗某种规则(如简单轮询,随机连接等)去连接这些机器。 一句话:负载均衡 + RestTemplate **与Nginx的区别** - Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。 - Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表后缓存到JVM本地,从而在本地实现RPC远程服务调用。 ```xml org.springframework.cloud spring-cloud-starter-netflix-ribbon ``` `spring-cloud-stater-netflix-eureka-client` 自带 `spring-cloud-starter-netflix-ribbon` #### 4.1.2 Ribbon自带的负载均衡规则: - 轮询 :ballot_box_with_check: - 随机 - 先轮询,轮询失败就重试 - 对轮询的扩展,响应速度越快权重越大 - 跳过故障服务,先择一个并发量最小的服务 - 跳过故障实例,选择冰消较小的实例 - 复合判断server所在区域的性能和server的可用性选择服务器 如何替换默认规则呢:question: 1. 新建package `package com.chmingx.myrule;` 2. 配置 ```java @Configuration public class MyselfRule { @Bean public IRule myRule() { return new RandomRule(); // 随机负载均衡规则 } } ``` 3. 主启动类上添加`@RibbonClient` ```java @SpringBootApplication @EnableEurekaClient @RibbonClient(name = "PAYMENT-SERVICE", configuration = MyselfRule.class) public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } } ``` :package: orderribbon80 :candy: 还有其他方法也可以实现替换,可以查询文档 :scroll: #### 4.1.3 轮询算法 :mag: 详解ribbon的轮询算法 __原理__: rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标。 每次服务器重启后rest接口计数从1开始 $$ rest接口第几次请求数 \% 服务器集群总数量 = 实际调用服务器位置下标 $$ :writing_hand: 自己手写轮询算法 1. 不再使用ribbon自带的轮询算法,取消 `@LoadBalanced` 注释 ```java @Configuration public class ApplicationContexstConfig { @Bean // @LoadBalanced // 使RestTemplate具有负载均衡的能力 public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` 2. 定义接口和实现类,用自旋锁实现多并发下获取第几次请求,$$Rest接口第几次请求数 \% 服务器集群总数量 = 实际调用服务器位置下标$$​​, 找到下标,进而找到服务器 ```java public interface LoadBalancer { ServiceInstance instance(List serviceInstances); } ``` ```java public class MyLB implements LoadBalancer { private AtomicInteger atomicInteger = new AtomicInteger(0); public final int getAndIncrement() { int current; int next; do { current = this.atomicInteger.get(); // Integer.MAX_VALUE 为 2147483647 最大的整型 next = current >= 2147483647 ? 0 : current + 1; } while (!this.atomicInteger.compareAndSet(current, next)); // 此处用到自旋锁, 处理并发下,是第几次访问 System.out.println("*****第几次访问,次数next: " + next); return next; } // rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 @Override public ServiceInstance instance(List serviceInstances) { int index = getAndIncrement() % serviceInstances.size(); return serviceInstances.get(index); } } ``` 3. 测试(可以跳过) ```java // 在服务端添加测试接口 @GetMapping("/payment/lb") // 用于自定义轮询负载均衡,获取lb节点 public String getPaymentLB() { return serverPort; } ``` 4. 将手写的负载均衡算法添加进controller并发送请求进行测试 ```java @RestController public class OrderController { private static final String PAYMENT_URL = "http://PAYMENT-SERVICE"; @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancer loadBalancer; // 注入自定义的负载均衡器 @Autowired private DiscoveryClient discoveryClient; // 使用自定义的负载均衡算法 @GetMapping("/consumer/payment/lb") public String getPaymentLB() { List instanceList = discoveryClient.getInstances("PAYMENT-SERVICE"); if (instanceList == null || instanceList.size() <= 0) { return null; } ServiceInstance serviceInstance = loadBalancer.instance(instanceList); // 使用自定义的负载均衡器 URI uri = serviceInstance.getUri(); return restTemplate.getForObject(uri + "/payment/lb", String.class); } } ``` :package: orderribbon80 ### 4.2 OpenFeign #### 4.2.1 概念 Feign是一个声明式的Web服务端,让编写Web服务客户端变得非常容易。只需创建一个接口,并在接口上添加注解即可。 前面已经使用Ribbon + RestTemplate,利用Rest Template对Http请求进行封装处理,形成一套模板化的调用方法。但是在实际开发中,由于服务依赖调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign实现下,我们只需要一个接口并使用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。 Feign也集成了Ribbon,并且通过轮询实现了客户端的负载均衡. ![](images/ribbon.png) ![](images/openfeign.png) Feign :vs: OpenFeign 1. Feign是Spring Cloud组件中一个轻量级Restful的Http服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign使用的方式是:使用Feign的注解定义接口,,调用这个接口,就可以调用服务注册中心的服务。 2. OpenFeign是Spring Cloud在Feign基础上支持了SpringMVC的注解。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方法产生实现类,实现类中做负载均衡并调用其他服务 #### 4.2.2 OpenFeign实现调用 :question: 如何实现OpenFeign的调用呢? 1. 新建模块 :package: orderfeign80 2. pom ```xml org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client com.chmingx common 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` 3. yaml ```yaml server: port: 80 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka spring: application: name: order-service # 设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: ReadTimeout: 5000 # 建立连接所用时间,适用于网络状况正常状态下,两端连接所用时间 ConnectTimeout: 5000 # 指的是建立连接后从服务器读取到可用资源所用时间 logging: level: # feign日志:以什么级别监控哪个接口 com.chmingx.springcloud.service.PaymentFeignService: debug ``` 4. 主启动类激活Feign ```java @SpringBootApplication @EnableFeignClients // 激活Feign public class OrderFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } } ``` 5. 消费者定义service接口调用服务提供者controller中的接口 ```java /** * 表面上是一个接口,而添加了@FeignClient注解后,动态代理生成了一个controller, * 所以当别的controller调用这个接口的时候,本质上是那个controller调用这个接口生成的动态代理controller */ @Component @FeignClient(value = "PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping("/payment/{id}") CommonResult getPaymentById(@PathVariable("id") Long id); } ``` 6. 在controller中调用FeignClient接口 ```java @RestController public class OrderFeignController { @Autowired private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } } ``` #### 4.2.3 OpenFeign超时控制 :alarm_clock: OpenFeign超时控制 OpenFeign默认等待 ==__1sec__==,超时则报错. 超时测试 ```java // 超时测试 @GetMapping("/payment/feign/timeout") public String paymentFeignTimeout() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return serverPort; } ``` ```java @Component @FeignClient(value = "PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping("/payment/{id}") CommonResult getPaymentById(@PathVariable("id") Long id); @GetMapping("/payment/feign/timeout") public String paymentFeignTimeout(); } ``` ```java @RestController public class OrderFeignController { @Autowired private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } // 超时测试 @GetMapping("/payment/feign/timeout") public String paymentFeignTimeout() { // openfiegn 默认等待1秒 return paymentFeignService.paymentFeignTimeout(); } } ``` :writing_hand: 修改配置 ```yaml # 设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: ReadTimeout: 5000 # 建立连接所用时间,适用于网络状况正常状态下,两端连接所用时间 ConnectTimeout: 5000 # 指的是建立连接后从服务器读取到可用资源所用时间 ``` #### 4.2.3 OpenFeign日志 :scroll: OpenFeign日志级别 - NONE - BASIC - HEADERS - FULL ```java @Configuration public class FeignConfig { // 配置open feign日志级别 @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ``` ```yaml logging: level: # feign日志:以什么级别监控哪个接口 com.chmingx.springcloud.service.PaymentFeignService: debug ``` ## 5 服务降级\熔断\限流 复杂的分布式体系结构中的应用程序有数是个依赖,每个依赖关系在某些时候不可避免地失败。 服务雪崩: 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务C又调用其他微服务,这就是所谓的“扇出”, 如果扇出的链路上某个微服务响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的雪崩效应。 ### 5.1 Hystrix #### 5.1.1 概念理解 Hystix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,比卖你级联故障,以提高分布式系统的弹性。 断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似保险丝熔断),向调用方返回一个符合预期的,可处理的备选响应(Fallback),而不是长时间等待或者抛出调用方无法处理的异常,这样保证了服务调用方的线程不会被长时间、不必要地占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。 __服务降级__:假设服务系统不可用了,需要提供一个兜底的解决方法,即可处理的备选响应Fallback - 运行异常 - 超时 - 服务熔断出发服务降级 - 线程池/信号量打满也会触发服务降级 __服务熔断__:类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示 __服务限流__:秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行 #### 5.1.2 hystrix 服务的构建 :package: cloud-provider-hystrix-payment8001 :package: cloud-consumer-feign-hystrix-order80 - pom - yaml - 主启动类 - 业务类 #### 5.1.3 模拟压测 jmeter进行压测 高并发下,8001同一层次的其他接口服务被困死,因为tomcat线程池里面的工作线程已经被抢占完毕,此时再调用,客户端响应缓慢,甚至出现超时错误。因为有上述故障或不佳的表现,才有我们的降级、容错、限流等技术诞生 解决要求: - 超时-- 不再等待,必须要有服务降级 - 出错,宕机或程序错误-- 出错要有兜底,必须要有服务降级 - 客户端自己故障或等待时间小于服务提供所需时间,自己处理降级 #### 5.1.4 服务降级 ```java @HystrixCommand ``` ##### 5.1.4.1 从服务侧解决 __设置自身调用超时时间的峰值,峰值内可以正常运行;冲过来需要有兜底的处理方法,作为服务fallback__ - 业务类启用 ```java @Service public class PaymentService { // 正常访问 public String paymentInfo_OK(Integer id) { return "线程池: " + Thread.currentThread().getName() + "\t" + "paymentInfo_OK, id: " + id + "\t" + "OK!!!"; } /** * 该方法超时之后交给paymentInfo_TimeoutHandler进行处理 */ @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000") // 这个线程的超时时间是1秒, 也可以再yaml中配置 }) public String paymentInfo_Timeout(Integer id) { int timeNumber = 3; // int age = 10 / 0; // 刻意制造的异样,也可以由fallback方法处理 try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池: " + Thread.currentThread().getName() + "\t" + "paymentInfo_Timeout, id: " + id + "\t" + "OK!!!" + "\t" + "耗时: " + timeNumber; } // paymentInfo_Timeout 发生超时错误时的兜底方法, 注意服务降级的方法需要与原方法保持一致 public String paymentInfo_TimeoutHandler(Integer id) { return "线程池: " + Thread.currentThread().getName() + "\t" + "系统繁忙或运行错误,请稍后再试" + "\t" + "Timeout!!!" + "\t" + "超时啦!!!"; } } ``` ```yaml # 改变默认超时时间: hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 3000 ``` - 在主启动类激活 ```java @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker // 激活服务降级 public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } } ``` ##### 5.1.4.2 从客户侧解决,服务降级一般放在客户端 IDEA的热部署对Java代码修改敏感,但是对@HystrixCommand内属性的修改不敏感,此时建议重启微服务 - 开启客户端支持服务降级 ```yaml feign: hystrix: enabled: true ``` - 主启动 ```java @SpringBootApplication @EnableFeignClients @EnableHystrix // 开启Hystrix public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class, args); } } ``` - controller配置fallback ```java @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeoutFallback", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500") }) public String paymentInfo_Timeout(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_Timeout(id); log.info(result); return result; } public String paymentTimeoutFallback(@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙,请稍后再试,或者自己运行出错,请检查自己"; } ``` ##### 5.1.4.3 全局配置Fallback - 方法1: 使用 `@DefaultProperties(defaultFallback = "")` , 专门配置了fallback的就调用fallback,没有就调用全局的fallback ```java @RestController @Slf4j @DefaultProperties(defaultFallback = "paymentGlobalFallback") // 配置全局的fallback public class OrderHystrixController { @Autowired private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/global/{id}") @HystrixCommand // 服务报错后,使用降级兜底的方法 public String paymentInfo_Global(@PathVariable("id") Integer id) { int age = 10 / 0; String result = paymentHystrixService.paymentInfo_Timeout(id); log.info(result); return result; } public String paymentGlobalFallback() { return "Global异常处理信息,对方支付系统繁忙,请稍后再试,或者自己运行出错,请检查自己"; } } ``` - 方法2: 利用 `@FeignClient` 再次优化代码,只需要为Feign客户端定义的接口interface添加一个服务降级处理实现类即可解耦代码。常用来处理宕机问题 ```java @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class) public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id); } ``` ```java // 兜底类实现接口 @Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "---PaymentFallbackService fall back paymentInfo_OK, o(-_-)o"; } @Override public String paymentInfo_Timeout(Integer id) { return "---PaymentFallbackService fall back paymentInfo_Timeout, o(-_-)o"; } } ``` #### 5.1.5 服务熔断 熔断机制:熔断机制时应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或响应时间太长时,会进行微服务降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。 在SpringCloud里,熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省5秒内20次调用失败,就会启动熔断机制。熔断机制注解是`@HystrixCommand` ```java @Service public class PaymentService { // ---------- 服务熔断 --------------- @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期, 经过多久后恢复一次尝试 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") // 失败率达到多少后跳闸, 这个是概率,百分数 }) public String paymentCircuitBreaker(@PathVariable("id") Integer id) { if (id < 0) { throw new RuntimeException("****** id 不能为负数"); } String serialNumber = IdUtil.simpleUUID(); // UUID.randomUUID().toString(); return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber; } public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) { return "id 不能为负数,请稍后再试, T_T, id: " + id; } } ``` 三要素: - 快照时间窗口:断路器是否需要打开统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为10秒 - 请求总阈值:在快照时间窗内,必须满足请求总阈值,才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开 - 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30此调用,如果在这30次调用中,由15次发生了错误,也就超过50%的错误百分比,在默认设定50%阈值情况下,这时候会将断路器打开,发生服务熔断 当服务熔断发生后,将不会再调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误,并将降级逻辑切换为主逻辑,减少响应延迟效果。 :question: 服务如何恢复呢? 当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑时临时成为主逻辑,当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将会闭合,主逻辑恢复,如果请求依然有问题,则断路器继续进入打开,状态,休眠时间长重新计时 #### 5.1.6 服务限流 略 #### 5.1.7 Hystrix 工作流程 #### 5.1.8 Hystrix Dashboard Hystrix Dashboard 准实时的调用监控 :package: cloud-consumer-hystrix-dashboard9001 - pom ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard ``` - yaml ```yaml server: port: 9001 ``` - main ```java @SpringBootApplication @EnableHystrixDashboard public class HystrixDashboardMain9001 { public static void main(String[] args) { SpringApplication.run(HystrixDashboardMain9001.class, args); } } ``` - 被监控的微服务需要引入 `spring-boot-starter-actuator` - 访问 http://localhost:9001/hystrix - 配置要监控的微服务 ```java @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker // public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } /** * 此配置时为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 * * ServletRegistrationBean因为SpringBoot的默认路径不是 /hystrix.stream * 只要在自己的项目里配置上下面的servlet就可以了 * @return */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } ``` - 将要监控的微服务地址输入hystrix-dashboard,查看监控 ## 6 服务网关 ### 6.1 GateWay 概述 GateWay: 是在Spring生态系统之上构建的API网关服务,基于Spring5, Spring Boot2,Project Reactor等。旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如熔断、限流、重试。 GateWay是基于异步非阻塞模型上进行开发的,性能方面不用太担心。 ```mermaid graph LR 外部请求 --> 负载均衡Nginx --> 网关 --> 微服务 ``` GateWay特性: - 基于Spring5, Spring Boot2,Project Reactor进行构建 - 动态路由:能够匹配任何请求属性 - 可以对路由指定Predicate(断言)和Filter(过滤器) - 集成Hystrix的断路器功能 - 集成SpringCloud服务发现功能 - 易于编写Predicate(断言)和Filter(过滤器) - 请求限流功能 - 支持路径重写 :question: Java web中Servlet生命周期? servlet由servlet container进行生命周期管理,container启动时构造servlet对象并调用servlet init()进行初始化。contianer运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service(),container关闭时,调用servlet destroy()销毁servlet。 这是一个阻塞的网络I/O servlet3.1以后出现非阻塞异步I/O __概念__: - 路由: 是构建网关的基本模块,它由ID,目标时URI,一系列断言和过滤器组成,如果断言为True,则匹配该路由 - 断言Predicate:参考Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头和请求参数),如果请求与断言匹配则进行路由 - 过滤:Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或之后对请求进行修改 web请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制,predicate就是我们匹配条件;而Filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了 __Gateway的流程__: 客户端向SpringCloud GateWay发出请求,然后再Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器可以再发送代理请求前或后执行逻辑。比如参数校验,权限验证,流量监控,日志输出,协议转换,或修改响应内容,响应头等。 ### 6.2 GateWay项目构建 :package: cloud-gateway-gateway9527 - pom ```xml org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` - yaml ```yaml server: port: 9527 spring: application: name: cloud-gateway eureka: instance: hostname: cloud-gateway-service # 服务提供者provider注册进eureka服务列表内 client: service-url: register-with-eureka: true fetch-registry: true defaultZone: http://eureka7001.com:7001/eureka ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaClient public class GateWayMain9527 { public static void main(String[] args) { SpringApplication.run(GateWayMain9527.class, args); } } ``` - 如何配置网关呢?比如不想暴露payment8001的8001端口,希望在8001外面套一层9527 ```yaml spring: cloud: gateway: routes: - id: payment_routh # payment_route # 路由的id,没有规则,但要求唯一,建议配合服务名 uri: http://localhost:8001 # 匹配后提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_routh2 # payment_route3 uri: http://localhost:8001 predicates: - Path=/payment/lb/** ``` - 测试 ```shell # 配置前 $ curl http://localhost:8001/payment/get/1 # 配置后, 可以掩藏真实网关 $ curl http://localhost:9527/payment/get/1 ``` ### 6.3 配置路由的两种方式 - yaml配置, 见上节 :point_up: - 代码中注入RouteLocator的Bean ```java @Configuration public class GateWayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) { RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); routes.route("customer_route_locator", r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build(); return routes.build(); } } ``` ### 6.4通过微服务名动态配置路由 ![gateway实现负载均衡](images/gateway.png) 默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径,创建动态路由进行转发,从而实现动态路由的功能 ```yaml # 动态路由,通过微服务名 spring: application: name: cloud-gateway cloud: gateway: discovery: locator: enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由 routes: - id: payment_routh # 路由id,要求唯一 uri: lb://cloud-provider-payment # 匹配后提供服务的路由地址, 需要注意的是uri的协议为lb,表示启用负载均衡 predicates: - Path=/payment/get/** # 断言,路径相匹配进行路由 - id: payment_routh2 uri: lb://cloud-provider-payment predicates: - Path=/payment/lb/** ``` ### 6.5 常见Predicates - after ```yaml spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: # 这个时间后才能起效 - After=2017-01-20T17:42:47.789-07:00[America/Denver] ``` - before - between - cookie ```yaml spring: cloud: gateway: routes: - id: cookie_route uri: https://example.org predicates: - Cookie=chocolate, ch.p # cookie名字,正则 ``` ```shell # 该命令相当于发get请求,且没带cookie curl http://localhost:9527/payment/lb # 带cookie的 curl http://localhost:9527/payment/lb --cookie "chocolate=chip" ``` - header ```yaml spring: cloud: gateway: routes: - id: header_route uri: https://example.org predicates: - Header=X-Request-Id, \d+ ``` ```shell # 带指定请求头的参数的CURL命令 curl http://localhost:9527/payment/lb -H "X-Request-Id:123" ``` ### 6.6 Gateway的Filter 路由过滤器可用于修改进入HTTP的请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway中内置了多种路由过滤器,他们都由GatewayFilter的工厂类来生产 生命周期: - pre - post 种类: - GatewayFilter - GlobalFilter 使用方法可查看官网, 举个例子 :pear: ```yaml - id: payment_routh2 uri: lb://cloud-provider-payment predicates: - Path=/payment/lb/** - Method=GET,POST filters: - AddRequestParameter=X-Request-Id,1024 # 过滤器工厂会在匹配的请求头上加一对请求头,名称为X-Request-Id, 值为1024 ``` #### 6.6.1 自定义过滤器 ```java /** * 自定义全局过滤器 */ @Component @Slf4j public class MyLogGatewayFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("**** come in MyLogGatewayFilter: " + new Date()); String uname = exchange.getRequest().getQueryParams().getFirst("uname"); if (uname == null) { log.info("****** 用户名为Null,非法用户,禁止访问 ****"); exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; // 返回过滤器优先级,数值越小,优先级越高 } } ``` 测试 ```shell $ curl http://localhost:9527/payment/lb?uname=zs ``` ## 7 服务配置 ### 7.1 概述 SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的松油环境提供一个中心化的外部配置中心 - 服务端:分布式配置中心,是一个独立的微服务应用,用来连接配置服务器,并为客户端提供获取配置信息,加密,解密信息等访问接口 - 客户端:则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样有主语对环境配置进行版本该你了,并且可以通过git客户端工具来方便管理和访问配置内容 ![](images/springcloudconfig.png) ### 7.2 配置总控 :package: cloud-config-center-3344 - 创建git仓库 `git@gitee.com:chmingx/springcloud-config.git` - pom ```xml org.springframework.cloud spring-cloud-config-server org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools org.projectlombok lombok true ``` - yaml ```yaml server: port: 3344 spring: application: name: cloud-config-center # 著恶策进eureka服务器的微服务名 cloud: config: server: git: # uri: git@gitee.com:chmingx/springcloud-config.git # gitee上面git仓库的名称, 报错是因为ssh版本太高, uri: https://gitee.com/chmingx/springcloud-config.git # 搜索目录 search-paths: - springcloud-config force-pull: true username: chmingx password: 1024.chm # 读取分支 label: master # 服务注册到eureka地址 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka ``` - 主启动类 ```java @SpringBootApplication @EnableConfigServer public class ConfigCenterMain3344 { public static void main(String[] args) { SpringApplication.run(ConfigCenterMain3344.class, args); } } ``` - 测试 ```shell $ curl http://config-3344.com:3344/master/config-dev.yml $ curl http://config-3344.com:3344/config-dev.yml $ curl http://config-3344.com:3344/config-dev.yml/master ``` 注意 :boom: ```shell 如果使用ssh报错是因为, openssh版本太高了 ssh-keygen -m PEM -t rsa 重新生成旧格式的key,变可解决 -m 参数指定密钥的格式,PEM(也就是RSA格式)是之前使用的旧格式 ``` ### 7.3 配置客户端搭建 - application.yaml 是用户级的资源配置项 - bootstrap.yaml 是系统级的,优先级更高 :white_check_mark: Spring Cloud会创建一个 Bootstrap Context, 作为Spring应用的 Application Context的父上下文,初始化的时候, Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment Bootstrap属性有更高优先级,默认情况下,他们不会被本地配置覆盖。Bootstrap Context和ApplicationContext有着不同的约定。 bootstrap.yaml的优先级比application.yaml高,先加载 :package: cloud-config-client-3355 - pom ```xml org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` - bootstrap.yaml ```yaml server: port: 3355 spring: application: name: config-client cloud: # Config 客户端配置 config: label: master # 分支 name: config # 配置文件名 profile: dev # 读取后缀名称 上述3个综合: master分支上config-dev.yml的配置文件被读取, http://config-3344.com:3344/master/conf9g-dev.yml uri: http://config-3344.com:3344 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaClient public class ConfigClientMain3355 { public static void main(String[] args) { SpringApplication.run(ConfigClientMain3355.class, args); } } ``` - 添加controller,以RESTful风格访问配置文件 ```java @RestController public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/configInfo") public String getConfigInfo() { return configInfo; } } ``` - 测试 ```shell $ curl http://localhost:3355/configInfo ``` :question: 如果有人修改了配置文件,会发现cloud-config-center上及时更新了,但是cloud-config-client需要重启微服务后才能刷新,那么如何实现动态刷新呢? ### 7.4 动态刷新配置 - pom中添加actuactor ```xml org.springframework.boot spring-boot-starter-actuator ``` - 修改yaml,暴露监控端点 ```yaml # bootstrap.yaml # 暴露监控端点 management: endpoints: web: exposure: include: "*" # 暴露所有的监控信息,可以配置,比如只暴露info, health等 ``` - controller添加 @RefreshScope ```java @RestController @// 需要热加载的bean需要加上@RefreshScope public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/configInfo") public String getConfigInfo() { return configInfo; } } ``` - 测试: gitee修改:ballot_box_with_check: --> 查看cloud-config-center-3344已修改:white_check_mark: --> 查看cloud-config-client-3355 未修改:x: - 运维人员修改gitee仓库中的配置文件后,需要发送POST请求到cloud-config-client-3355 ```shell $ curl -X POST "http://localhost:3355/actuator/refresh" ``` - 测试: gitee修改:ballot_box_with_check: --> 查看cloud-config-center-3344已修改:white_check_mark: --> 查看cloud-config-client-3355 已修改:white_check_mark: :question: 这种手动刷新的方法比较麻烦,如果微服务数量很大,如果有些要刷新,有些不需要,也比较麻烦,所以引入消息总线帮忙处理 ## 8 服务总线 ### 8.1 概述 __消息总线__:在微服务交媾的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统所有微服务实例都连接上来。由于该主题产生的消息会被所有实例监听和消费,所以称为消息总线。 SpringCloudBus是用来将分布式系统的节点与轻量级消息系统连接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。能管理和传播分布式系统间的消息,就像一个分布式执行器,可以用于广播状态更改,事件推送等 Spring Cloud Bus + Spring Cloud Config 可以实现配置的动态自动刷新 支持: RabbitMQ + Kafka 原理: Config Client实例会监听一个MQ中同一个topic(默认是SpringCloudBus),当一个微服务刷新数据的时候,他会把这个消息放到Topic中,这样其他监听同一个Topic的服务就能得到通知,然后去更新自身配置 ### 8.2 SpringCloud Bus动态刷新全局广播 ```shell # docker运行rabbitmq $ docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management ``` :ballot_box_with_check: 利用消息总线触发一个服务端ConfigServer的/bus/refresh, 而刷新所有客户端的配置 #### 8.2.1 配置中心服务端添加消息总线支持 :package: cloud-config-center-3344 - pom添加消息总线RabbitMQ支持 ```xml org.springframework.cloud spring-cloud-starter-bus-amqp ``` - 修改yaml ```yaml spring: # rabbitmq 相关配置 rabbitmq: host: 192.168.80.130 port: 5672 username: guest password: guest # 暴露bus刷新配置的端点 management: endpoints: web: exposure: include: 'bus-refresh' # 要用单引号 ``` bus-refresh是actuator的刷新操作 #### 8.2.2 配置客户端添加消息总线支持 :package: cloud-config-client-3355 :package: cloud-config-client-3366 - pom ```xml org.springframework.cloud spring-cloud-starter-bus-amqp ``` - yaml ```yaml spring: # 配置rabbitmq rabbitmq: host: 192.168.80.130 port: 5672 username: guest password: guest # 暴露监控端点 management: endpoints: web: exposure: include: "*" ``` #### 8.2.3 测试 - 修改gitee上配置文件 - 发送POST请求 ```shell curl -X POST "http://localhost:3344/actuator/bus-refresh" ``` - gitee :ballot_box_with_check: --> config-center:white_check_mark: -> config-client:white_check_mark: ### 8.3 SpringCloud Bus动态刷新定点通知 可以指定某个具体实例生效,而不是全部 公式: `http://localhost:3344/actuator/bus-refresh/{destination}` ```shell # 只通知3355,不通知3366 curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355" ``` ## 9 消息驱动 ### 9.1 Spring Cloud Stream概述 SpringCloud Stream作为中间层, 屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型 https://spring.io/projects/spring-cloud-stream#overview 应用程序通过input或者output来与SpringCloud Stream中binder对象交互,而binder负责与消息中间件交互,所以,只需要搞清楚如何与SpringCloud Stream交互就可以方便使用消息驱动的方式 目前支持: RabbitMQ / Kafka ![](images/stream.png) SpringCloud Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange, 在Kafka中就是Topic - binder:连接中间件,屏蔽差异 - channel: 通过,是队列queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过channel对队列进行配置 - source/sink: 从stream发布消息就是输出,接受消息就是输入 ### 9.2 消息驱动生产者 :package: cloud-stream-rabbit-provider8801 - pom ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` - yaml ```yaml server: port: 8801 spring: application: name: cloud-stream-provider rabbitmq: host: 192.168.80.130 port: 5672 username: guest password: guest cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息 defaultRabbit: # 表示定义的名称, 用于binding整合 type: rabbit # 消息组件类型 # environment: # 设置rabbitmq的相关的环境配置 # spring: # rabbitmq: # host: 192.168.80.130 # port: 5672 # username: guest # password: guest bindings: # 服务整合处理 output: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain" binder: defaultRabbit # 设置要绑定的消息服务的具体设置 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka instance: lease-renewal-interval-in-seconds: 2 # 设置心跳事件间隔2 lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔 instance-id: send-8801.com # 在信息列表时显示主机名 prefer-ip-address: true # 访问的路径变为IP地址 ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaClient public class StreamMQMain8801 { public static void main(String[] args) { SpringApplication.run(StreamMQMain8801.class, args); } } ``` - 访问RabbitMQ的接口 ```java public interface IMessageProvider { public String send(); } ``` - 实现接口 ```java /** * 该service是与rabbitmq交互,无需@Service注解 */ @EnableBinding(Source.class) // 定义消息的推送管道 public class MessageProviderImpl implements IMessageProvider { @Autowired private MessageChannel output; // 消息发送管道, 与配置文件中output相呼应 @Override public String send() { String serial = UUID.randomUUID().toString(); output.send(MessageBuilder.withPayload(serial).build()); System.out.println("*****serial: " + serial); return serial; } } ``` - controller ```java @RestController public class SendMessageController { @Autowired private IMessageProvider messageProvider; @GetMapping(value = "/sendMessage") public String sendMessage() { return messageProvider.send(); } } ``` - test ```shell curl http://localhost:8801/sendMessage ``` ### 9.3 消息驱动消费者 :package: cloud-stream-rabbitmq-consumer8802 :package: cloud-stream-rabbitmq-consumer8803 - pom ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` - yaml ```yaml server: port: 8802 spring: application: name: cloud-stream-consumer rabbitmq: host: 192.168.80.130 port: 5672 username: guest password: guest cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息 defaultRabbit: # 表示定义的名称, 用于binding整合 type: rabbit # 消息组件类型 # environment: # 设置rabbitmq的相关的环境配置 # spring: # rabbitmq: # host: 192.168.80.130 # port: 5672 # username: guest # password: guest bindings: # 服务整合处理 input: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain" binder: defaultRabbit # 设置要绑定的消息服务的具体设置 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka instance: lease-renewal-interval-in-seconds: 2 # 设置心跳事件间隔2 lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔 instance-id: receive-8802.com # 在信息列表时显示主机名 prefer-ip-address: true # 访问的路径变为IP地址 ``` - 主启动类 ```java @SpringBootApplication @EnableEurekaClient public class StreamMQMain8802 { public static void main(String[] args) { SpringApplication.run(StreamMQMain8802.class, args); } } ``` - 业务类 ```java @Component @EnableBinding(Sink.class) public class ReceiveMessageListenerController { @Value("${server.port}") private String serverPort; @StreamListener(Sink.INPUT) public void input(Message message) { System.out.println("consumer 1, ----> 接受到: " + message.getPayload() + "\t" + "Port: " + serverPort); } } ``` ### 9.4 分组 :question: 重复消费问题 比如一个订单同时被两个服务收到,那么就会造成数据错误,我们得避免这种情况。可以用Stream的消息分组来解决。 __在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次,而不同组可以重复消费__ 修改yaml,为消费者分组 ```yaml bindings: # 服务整合处理 input: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain" binder: defaultRabbit # 设置要绑定的消息服务的具体设置 group: chmingxA # 分组 ``` :question: 去掉8802分组,保留8803分组,并停掉8802和8803,8801发送消息后,重启8802,8802不会受到消息,而重启8803后会收到消息 这是因为,exchange数据发送到队列中,由于02重启没有设置分组,会重新创建队列并监听,而03还是监听原来队列。 __所以分组可以实现消息持久化,防止数据丢失__ ## 10 Sleuth分布式请求链路式跟踪 sleuth 监控 + zipkin 呈现 监控微服务的调用 - Trace 类似于树结构的Span集合,表示一条调用链路,存在唯一标识 - span 表示调用链路来源,通俗的理解span就是一次请求信息 :package: cloud-consumer-order80 :package: cloud-provider-payment8001 均添加 - pom ```xml org.springframework.cloud spring-cloud-starter-zipkin ``` - yaml ```yaml spring: application: name: cloud-provider-payment # 微服务链路监控相关配置 zipkin: base-url: http://localhost:9411 sleuth: sampler: probability: 1 # 采样率介于0~1,1表示全部采集 ``` ## 11 Nacos ### 11.1 概述 一个更易于构建云原生应用的动态服务发现、配置管理和服务平台。 Nacos: dynamic naming and configuration service Nacos=注册中心+配置中心 Nacos=Eureka+Config+Bus ```shell $ docker ps -a|grep Exited|awk '{print $1}' # 查看所有没有运行的容器 $ docker rm `docker ps -a|grep Exited|awk '{print $1}'` # 删除所有停止了的容器 $ docker rm $(sudo docker ps -a -q) $ docker pull nacos/nacos-server $ docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server $ firewall-cmd --zone=public --query-port=8848/tcp # 查询端口是否打开 $ firewall-cmd --zone=public --add-port=8848/tcp --permanent # 永久开发8848端口 ``` :boom: 虚拟机中记得关闭防火墙 ### 11.2 注册中心 #### 11.2.1 注册服务生产者 :gift: cloud04 :package: cloudalibaba-provider-payment9001 :package: cloudalibaba-provider-payment9002 - pom ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` - yaml ```yaml server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 # 配置nacos地址 # 打开监控端点 management: endpoints: web: exposure: include: '*' ``` - 主启动类 ```java @SpringBootApplication @EnableDiscoveryClient public class PaymentMain9001 { public static void main(String[] args) { SpringApplication.run(PaymentMain9001.class, args); } } ``` - 业务类 ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/{id}") public String getPaymentById(@PathVariable("id") Integer id) { return "nacos registry, serverPort: " + serverPort + "\t id" + id; } } ``` - 查看 http://localhost:8848/nacos #### 11.2.2 注册服务消费者 :package: cloudalibaba-consumer-order83 #### 11.2.3 总结 各个注册中心对比 C是所有节点在同一事件看到的数据是一致的;A是所有的请求都会收到响应 如果不需要存储服务级别信息且服务实例时通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring Cloud和Dubbo都适用于AP模式,AP模式为了服务的可能性而减肉一致性,因此AP模式下只支持注册临时实例 如果需要服务级别便捷或存储配置信息,那么CP时必须,K8S服务和DNS服务则使用与CP模式,CP模式下则支持注册持久实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误 Nacos支持CP和AP ```shell # 切换命令 curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP' ``` ### 11.3 配置中心 #### 11.3.1 配置中心项目构建 - pom ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` - bootstrap.yaml ```yaml # nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} # nacos-config-client-dev.yaml # nacos-config-client-test.yaml ----> config.info ``` - application.yaml ```yaml spring: profiles: active: dev # 表示开发环境 #active: test # 表示测试环境 #active: info ``` - 主启动类 ```java @SpringBootApplication @EnableDiscoveryClient public class NacosConfigClientMain3377 { public static void main(String[] args) { SpringApplication.run(NacosConfigClientMain3377.class, args); } } ``` - 业务类 ```java @RestController @RefreshScope //支持Nacos的动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } } ``` - 在nacos上添加配置 - 测试 在 Nacos Spring Cloud中,dataId的完整格式如下: ```yaml #${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} # nacos-config-client-dev.yaml ``` - prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.config.prefix来配置 - spring.profile.active即为当前环境对应的 profile,详情可以参考 Spring Boot文档。注意:当spring.profile.active为空时,对应的连接符 - 也将不存在,datald 的拼接格式变成`${prefix}.$​​{file-extension}` - file-exetension为配置内容的数据格式,可以通过配置项spring .cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型 - 通过Spring Cloud 原生注解@RefreshScope实现配置自动更新 #### 11.3.2 分组配置 ![](images/nacos-config.png) namespace + group + dataid - namespace区分部署环境,默认的命名空间是public。比如有三个环境:开发、测试、生产,我们可以创建三个namespace,不同的namespace之间是隔离的 - group默认是DEFAULT_GROUP,可以把不同的微服务划分到一个分组里 - Service就是微服务:一个Service可以包含多个Cluster (集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。 - 最后是Instance,就是微服务的实例 ### 11.4 持久化 - nacos/conf/nacos-mysql.sql, 创建数据库表 `nacos_config` - 运行镜像 ```shell docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server docker exec -it bash vim conf/application.properties # 修改对应的mysql参数 ``` - 修改application.properties中对应的mysql参数 ```properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=true&serverTimezone=Asia/Shanghai db.user=root db.password=123456 ``` - 重启nacos ### 11.5 nacos集群 :triangular_flag_on_post: ![](images/nacos-cluster.png) 默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,__Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储__。 Nacos支持三种部署模式 - 单机模式-用于测试和单机试用。 - 集群模式-用于生产环境,确保高可用。 - 多集群模式-用于多数据中心场景。 Nginx + 3 Nacos + mysql 1. 在mysql中用conf/nacos-mysql.sql建表 2. 修改nacos/conf/application.properties 3. 修改nacos/conf/cluster.conf ```shell 192.168.80.130:8847 192.168.80.130:8848 192.168.80.130:8849 ``` 4. 修改nacos/bin/start.sh, 传入参数`-p `, 就以port启动nacos服务 5. 启动三个nacos ```shell $ ./start.sh -p 8847 $ ./start.sh -p 8848 $ ./start.sh -p 8849 ``` 6. nginx配置修改 7. 启动nginx 8. 测试,可以查看和持久化存储配置文件 将为服务注册进nacos集群 ```yaml # application.yaml server: port: 9002 spring: application: name: nacos-payment-provider c1oud: nacos: discovery: #配置Nacos地址 #server-addr: Localhost:8848 #换成nginx的1111端口,做集群 server-addr: 192.168.111.144:1111 management: endpoints: web: exposure: inc1ude: '*' ``` ## 12 Sentinel Sentinel是分布式系统的流量防卫兵 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 Sentinel 具有以下特征: - __丰富的应用场景__:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 - __完备的实时监控__:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 - __广泛的开源生态__:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。 - __完善的 SPI 扩展点__:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 - 服务雪崩 - 服务降级 - 服务熔断 - 服务限流 运行sentinel ```shell $ java -Dserver.port=6666 -jar sentinel-dashboard-1.7.2.jar ``` ### 12.1 sentinel监控项目 :package: cloudalibaba-sentinel-service8401 - pom ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.csp sentinel-datasource-nacos com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test cn.hutool hutool-all 5.7.5 org.projectlombok lombok true ``` - yaml ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard地址 port: 8719 # 8719是sentinel所在的后台要和sentinel前台的dashboard交互要用的端口, 如果8719被占用,会自动往上加1,直到找到未被占用端口 management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true # 激活Sentinel对Feign的支持 ``` - 主启动 ```java @SpringBootApplication @EnableDiscoveryClient public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } } ``` - 业务类 ```java @RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------ testA"; } @GetMapping("/testB") public String testB() { return "------ testB"; } } ``` - 测试: nacos-->sentinel-->cloudalibaba-sentinel-service8401 :boom: sentinel 是懒加载的 ### 12.2 流量控制 概念: - 资源名:唯一名称,默认请求路径。 - 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。 - 阈值类型/单机阈值: - QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。 - 线程数:当调用该API的线程数达到阈值的时候,进行限流。 - 是否集群:不需要集群。 - 流控模式: - 直接:API达到限流条件时,直接限流。 - 关联:当关联的资源达到阈值时,就限流自己。 - 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。 - 流控效果: - 快速失败:直接失败,抛异常。 - Warm up:根据Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。 - 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效 __Warm Up__:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮 应用场景 如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值 __匀速排队__方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。 这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。 ### 12.3 熔断降级 熔断降级概述 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。 现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。 流控是保护服务不挂掉 熔断是服务已经有问题了防止雪崩 - RT(平均响应时间,秒级) - 平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级。 - 窗口期过后关闭断路器。 - RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)。 - 异常比列(秒级) > QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 。 > 异常数(分钟级) - 异常数(分钟统计) > 超过阈值时,触发降级;时间窗口结束后,关闭降级 > > 异常数是按照分钟统计的,时间窗口一定要大于等于60秒 ### 12.4 热点key 何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如: - 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 - 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。 **前提条件** - 热点参数的注意点,参数必须是基本类型或者String @SentinelResource - 处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理; RuntimeException int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管 ### 12.5 系统规则 Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 - Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。 - 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。 - 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。 - 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。 ### 12.6 `@SentinelResource` ```java @RestController public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource", blockHandler = "handleException") public String byResource() { return "200, 按资源名称限流测试, OK"; } public String handleExeception(BlockException blockException) { return "444, 按资源名称限流测试, Fail"; } @GetMapping("/rateLimit/byUrl") @SentinelResource("byUrl") public String byUrl() { return "200, 按url限流测试, OK"; } } ``` :open_book: 升级代码 创建自定义的限流处理逻辑类 ```java /** * 自定义限流处理类,提供处理限流兜底方法 */ public class CustomerBlockHandler { public static String handlerException(BlockException blockException) { return "444, 自定义blockhandler处理限流 ------- 1"; } public static String handlerException2(BlockException blockException) { return "444, 自定义blockhandler处理限流 ------- 2"; } } ``` 配置使用自定义限流处理逻辑 ```java @RestController public class RateLimitController { // 使用自定义的用户处理类 @GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") public String customerBlockHandler() { return "200, 自定义限流处理, OK"; } } ``` @SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性: - value:资源名称,必需项(不能为空) - entryType:entry 类型,可选项(默认为 EntryType.OUT) - blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 - fallback /fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求: - 返回值类型必须与原函数返回值类型一致; - 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 - defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求: - 返回值类型必须与原函数返回值类型一致; - 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。 - defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 - exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 ### 12.7 整合ribbon和openfeign :package: cloudalibaba-provider-payment9003 :package: cloudalibaba-provider-payment9004 :package: cloudalibaba-consumer-order84 - pom ```xml org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true ``` - yaml ```yaml server: port: 84 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) service-url: nacos-user-service: http://nacos-payment-provider # 激活Sentinel对Feign的支持 feign: sentinel: enabled: true ``` - 主启动 ```java @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients // 开启feign的支持 public class OrderNacosMain84 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain84.class, args); } } ``` - 业务类 ```java @Component @FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class) public interface PaymentService { @GetMapping("/payment/{id}") public String getPaymentById(@PathVariable("id") Integer id); } ``` ```java @Component public class PaymentFallbackService implements PaymentService{ @Override public String getPaymentById(Integer id) { return "openfeign, 服务降级, 444"; } } ``` - controller ```java @RestController public class CircleBreakerController { @Value("${service-url.nacos-user-service}") private String serviceUrl; @Autowired private RestTemplate restTemplate; @GetMapping("/consumer/payment/{id}") @SentinelResource(value = "consumer", blockHandler = "blockHandler", // 负责处理Sentinel控制台配置违规 fallback = "fallbackHandler", // 负责处理java内部异常 exceptionsToIgnore = {IllegalArgumentException.class} // 指定忽略的异常 ) public String getPaymentById(@PathVariable("id") Integer id) { if (id == 0) { // exceptionsToIgnore属性有IllegalArgumentException.class, //所以IllegalArgumentException不会跳入指定的兜底程序 throw new IllegalArgumentException("非法参数异常"); } else if (id == 1) { throw new NullPointerException("空指针异常"); } return restTemplate.getForObject(serviceUrl + "/payment/" + id, String.class, id); } public String blockHandler(Integer id, BlockException blockException) { return "blockHandler-sentinel限流,无此流水: blockException" + blockException.getMessage(); } public String fallbackHandler(Integer id, Throwable throwable) { return "兜底异常handlerFallback,exception内容 " + throwable.getMessage(); } // ------ openfeign ----- @Autowired private PaymentService paymentService; @GetMapping("/consumer/openfeign/payment/{id}") public String getPaymentThroughFeign(@PathVariable("id") Integer id) { return paymentService.getPaymentById(id); } } ``` ### 12.8 持久化 将流量控制规则写进nacos中, :package: cloudalibaba-sentinel-service8401 - pom ```xml com.alibaba.csp sentinel-datasource-nacos ``` - yaml ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard地址 port: 8719 # 8719是sentinel所在的后台要和sentinel前台的dashboard交互要用的端口, 如果8719被占用,会自动往上加1,直到找到未被占用端口 # 将sentinel的监控规则持久化保存到nacos中 datasource: ds1: nacos: server-addr: localhost8848 dataId: cloudalibaba-sentinel-service # 与spring.application.name一样 groupId: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true # 激活Sentinel对Feign的支持 ``` - nacos中添加配置内容 ```json [{ "resource": "/rateLimit/byUrl", // 资源名称 "IimitApp": "default", // 来源应用 "grade": 1, // 阈值类型,0表示线程数,1表示QPS "count": 1, // 单机阈值 "strategy": 0, // 流控模式 0表示直接, 1表示关联, 2表示链路 "controlBehavior": 0, // 流控效果 0表示快速失败, 1表示Warm up, 2表示排队等待 "clusterMode": false // 是否为集群 }] ``` ### 12.9 总结 ## 13 Seata 处理分布式事务 ### 13.1 Seata概述 :question: 每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证,一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。 Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 分布式事务处理过程的一ID+三组件模型: - Transaction ID XID 全局唯一的事务ID - 三组件概念 - TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。 - TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。 - RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 处理过程: 1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID; 2. XID在微服务调用链路的上下文中传播; 3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖; 4. TM向TC发起针对XID的全局提交或回滚决议; 5. TC调度XID下管辖的全部分支事务完成提交或回滚请求。 ### 13.2 Seata安装与配置 略 ### 13.3 分布式项目构建 :package: seata-order-service2001 :package: seata-storage-service2002 :package: seata-account-service2003 - 业务表和回滚日志表构建 ```sql CREATE DATABASE seata_order; CREATE DATABASE seata_storage; CREATE DATABASE seata_account; CREATE TABLE seata_order.t_order ( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `count` INT(11) DEFAULT NULL COMMENT '数量', `money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额', `status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结', ) ENGINE=INNODB AUTO_INCREMENT=` DEFAULT CHARSET=utf8; SELECT * FROM t_order; CREATE TABLE seata_storage.t_storage ( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `total` INT(11) DEFAULT NULL COMMENT '总库存', `used` INT(11) DEFAULT NULL COMMENT '已用库存', `residue` INT(11) DEFAULT NULL COMMENT '剩余库存' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0','100'); SELECT * FROM t_storage; CREATE TABLE seata_account.t_account( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id', `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度', `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额', I `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000'); SELECT * FROM t_account; ``` seata/conf/db_undo_log.sql :scroll:, 每个库都要建 ```sql -- the table to store seata xid data -- 0.7.0+ add context -- you must to init this sql for you business databese. the seata server not need it. -- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库) -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log drop table `undo_log`; CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ``` ```java @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) // //rollbackFor = Exception.class表示对任意异常都进行回滚 ```