本文最后更新于 2022年3月11日 下午
Spring Cloud Alibaba 微服务相关概念 为什么要使用微服务/分布式架构 架构演变过程:
单体架构在业务规模不大的情况下,开发维护容易。随着企业的发展,业务规模与日递增,单体应用变得愈发臃肿。由于单体应用将各种业务模块聚合在一起,并且部署在一个进程内,所以通常我们对其中一个业务模块的修改也必须将整个应用重新打包上线。为了解决单体应用变得庞大脯肿之后产生的难以维护的问题,出现了微服务,分布式等架构。
优点:
服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
微服务之间采用Restful等轻量级http协议相互调用
缺点:
Spring Cloud Alibaba相关概念 Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
组件:
nacos:一个更易于构建云原生应用的动态服务发现 、配置 管理和服务 管理平台。
sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务 的稳定性。
seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务 解决方案。
Spring Cloud的不同实现方式:
Spring Cloud Alibaba 配置 先使用Spring Initializr创建一个项目,对应版本参照WIKI:
本项目Spring Cloud Alibaba采用2.2.7.RELEASE版本。
父POM文件配置为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <modules > <module > Order</module > <module > Stock</module > <module > Order_OpenFeign</module > <module > Config</module > <module > Order-Sentinel</module > <module > Order_OpenFeign_Sentinel</module > <module > 03-alibaba-order-seata</module > <module > 04-alibaba-stock-seata</module > </modules > <groupId > com.example</groupId > <artifactId > SpringCloudAlibaba</artifactId > <version > 0.0.1-SNAPSHOT</version > <packaging > pom</packaging > <properties > <java.version > 1.8</java.version > <spring.cloud.alibaba.version > 2.2.7.RELEASE</spring.cloud.alibaba.version > <spring.boot.version > 2.3.12.RELEASE</spring.boot.version > <spring.cloud.version > Hoxton.SR12</spring.cloud.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies > <dependencyManagement > <dependencies > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-dependencies</artifactId > <version > ${spring.cloud.alibaba.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > ${spring.boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > ${spring.cloud.version}</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
Nacos 搭建及使用 Nacos相关概念 Nacos运行及服务调用流程
更加详细的流程图:
其中,server端都要定时发心跳包,使注册注册中心即使发现服务以及检测服务当前状态。
一些注册中心的对比:
项目服务的一些代码编写 在项目中创建Order,Stock两个服务,其中Order调用Stock接口。
在不使用Feign调用接口时,用的是restTemplate,其中一些代码:
OrderApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootApplication public class OrderApplication { public static void main (String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate (RestTemplateBuilder builder) { RestTemplate restTemplate = builder.build(); return restTemplate; } }
OrderController:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("/order") public class OrderController { @Resource RestTemplate restTemplate; @GetMapping("/add") public String add () { String msg = restTemplate.getForObject("http://stock-service/stock/deduct" , String.class); return "下单成功" + msg; } }
StockController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping("/stock") public class StockController { @Value("${server.port}") private String port; @GetMapping("/deduct") public String deduct () { System.out.println("库存扣除" ); return "库存扣除,当前端口:" + port; } @GetMapping("/error") public String errorTest () { int a = 1 / 0 ; System.out.println("库存扣除" ); return "库存扣除,当前端口:" + port; } @GetMapping("/get/{id}") public String buy (@PathVariable Integer id) { return "商品购买,id = " + id; } }
Nacos搭建及配置 Server端搭建 步骤参照:https://nacos.io/zh-cn/docs/quick-start-docker.html
首先需要在修改nacos-docker /example /.env文件
改为上面对应的版本
单机模式:
standalone-mysql-5.7.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 version: "2" services: nacos: image: nacos/nacos-server:${NACOS_VERSION} container_name: nacos-standalone-mysql env_file: - ../env/nacos-standlone-mysql.env volumes: - ./standalone-logs/:/home/nacos/logs - ./init.d/custom.properties:/home/nacos/init.d/custom.properties ports: - "8848:8848" - "9848:9848" - "9555:9555" depends_on: - mysql restart: on-failure mysql: container_name: mysql image: nacos/nacos-mysql:5.7 env_file: - ../env/mysql.env volumes: - ./mysql:/var/lib/mysql ports: - "3306:3306"
提前在当前目录建立好volumes对应的目录和文件。
直接docker-compose -f example/standalone-mysql-5.7.yaml up -d
运行容器
访问http://ip:8848/nacos/ 正常。
集群模式:
cluster-hostname.yaml ,修改一些配置(JVM内存设置,防止打不开):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 version: "3" services: nacos1: hostname: nacos1 container_name: nacos1 image: nacos/nacos-server:${NACOS_VERSION} volumes: - ./cluster-logs/nacos1:/home/nacos/logs - ./init.d/custom.properties:/home/nacos/init.d/custom.properties ports: - "8848:8848" - "9848:9848" - "9555:9555" env_file: - ../env/nacos-hostname.env environment: - JVM_XMS=512m - JVM_XMX=512m - JVM_XMN=128m restart: always depends_on: - mysql nacos2: hostname: nacos2 image: nacos/nacos-server:${NACOS_VERSION} container_name: nacos2 volumes: - ./cluster-logs/nacos2:/home/nacos/logs - ./init.d/custom.properties:/home/nacos/init.d/custom.properties ports: - "8849:8848" - "9849:9848" env_file: - ../env/nacos-hostname.env environment: - JVM_XMS=512m - JVM_XMX=512m - JVM_XMN=128m restart: always depends_on: - mysql nacos3: hostname: nacos3 image: nacos/nacos-server:${NACOS_VERSION} container_name: nacos3 volumes: - ./cluster-logs/nacos3:/home/nacos/logs - ./init.d/custom.properties:/home/nacos/init.d/custom.properties ports: - "8850:8848" - "9850:9848" env_file: - ../env/nacos-hostname.env environment: - JVM_XMS=512m - JVM_XMX=512m - JVM_XMN=128m restart: always depends_on: - mysql mysql: container_name: mysql image: nacos/nacos-mysql:5.7 env_file: - ../env/mysql.env volumes: - ./mysql:/var/lib/mysql ports: - "3306:3306"
还是建立好文件和文件夹,启动容器,docker ps
看容器均正常运行,打开网站,均能正常访问。
配置nginx:
nginx_docker.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 version: "3" services: nginx: restart: always image: nginx:latest container_name: nginx ports: - "8855:80" - "8856:443" - "9080:9080" volumes: - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf - ./data/nginx/:/usr/share/nginx/html/ - ./log/nginx/:/var/log/nginx/
nginx.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 worker_processes 1 ;events { worker_connections 1024 ; }stream { upstream nacosgrpcc { server 192.168.2.125:9848 ; server 192.168.2.125:9849 ; server 192.168.2.125:9850 ; } server { listen 9080 ; proxy_pass nacosgrpcc; } }http { include mime.types; default_type application/octet-stream; client_max_body_size 100m ; sendfile on ; keepalive_timeout 65 ; upstream nacos { server 192.168.2.125:8848 weight=10 ; server 192.168.2.125:8849 weight=10 ; server 192.168.2.125:8850 weight=10 ; } server{ listen 80 ; server_name 192.168.2.125 ; location / { proxy_pass http://nacos/; } } }
配置完网页可以正常访问,但是程序并不能启动,连上注册中心,待解决。
可选方案:https://gitee.com/little_wear/docker-compose-nacos-nginx 待验证。
客户端配置 1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency >
加上web和nacos discovery依赖即可。
application.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 server: port: 8010 spring: application: name: order-service cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public
一启动两个服务,在注册中心中可以看到对应的服务:
负载均衡 创建两个Stock服务,使用Edit Configuration:
改好端口就行,
之前程序中使用了RestTemplate,@LoadBalanced,消费端默认使用轮询机制。
其中,Spring Cloud使用的Ribben负载均衡为客户端负载均衡,配置均在客户端中,获取注册中心服务段相关信息,由客户端选择提供服务的服务端。
而nginx负载均衡为服务段负载均衡,客户端先发送请求,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;需要集中式的配置。即在服务器端进行负载均衡算法的分配。
Ribbon负载均衡相关算法:
使用Ribbon的随机负载均衡策略 ,每个服务访问完全随机:
配置类RandRule:
1 2 3 4 5 6 7 8 @Configuration public class RandRule { @Bean public IRule iRule () { return new RandomRule (); } }
启动类上要加上Ribbon注解:
1 2 3 4 @SpringBootApplication @RibbonClients(value = { @RibbonClient(name = "stock-service", configuration = RandRule.class) })
使用NacosRule(基于权重的负载均衡策略) :
application.yml中添加:
1 2 3 4 stock-service: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
不需要@RibbonClients注解,直接在Nacos中配置:
其中权重范围为0-1,越小访问概率越低。
使用自定义负载均衡策略:
application.yml中添加:
1 2 3 4 5 6 7 8 9 10 stock-service: ribbon: NFLoadBalancerRuleClassName: com.example.ribbon.rule.CustomRule ribbon: eager-load: enabled: true clients: stock-service
启动类中不用其他注解
自定义类(实现随机负载均衡策略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class CustomRule extends AbstractLoadBalancerRule { @Override public void initWithNiwsConfig (IClientConfig iClientConfig) { } @Override public Server choose (Object o) { ILoadBalancer loadBalancer = this .getLoadBalancer(); List<Server> servers = loadBalancer.getReachableServers(); int random = ThreadLocalRandom.current().nextInt(servers.size()); Server server = servers.get(random); return server; } }
使用Spring Cloud自带负载均衡LoadBalancer(轮询):
需要在POM中排除Ribbon:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > 不使用ribbon负载均衡 <exclusions > <exclusion > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-ribbon</artifactId > </exclusion > </exclusions > </dependency >
加上loadbalancer:
1 2 3 4 5 6 使用Spring Cloud负载均衡<dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-loadbalancer</artifactId > <version > 3.1.1</version > </dependency >
只需要在配置文件中添加:
1 2 3 loadbalancer: ribbon: enabled: false
Feign的配置和使用 关于一些接口调用的方法:
Feign是Netflix开发声明式,模板化的HTTP客户端,可以优雅的调用HTTP API。Feign支持多种注解,也有自带注解。
SpringCloudOpenFeign对Feign进行了增强,使其支持Spring MVC注解,还整合了Ribbon和Nacos,使Feign的使用更加方便。
Feign可以做到使用HTTP请求远程服务时就像调用本地方法一样的体验,开发者完全感知不到这是否是远程方法或HTTP请求。
Feign使用 引入依赖:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
启动类需要加入注解@EnableFeignClients:
1 2 3 4 5 6 7 8 @SpringBootApplication @EnableFeignClients public class OrderApplication { public static void main (String[] args) { SpringApplication.run(OrderApplication.class, args); } }
新建服务类,接口:
1 2 3 4 5 6 @FeignClient(name = "stock-service", path = "/stock") public interface StockFeignService { @RequestMapping("/deduct") String deduct () ; }
完全模仿接口的形式,Controller:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("/order") public class OrderController { @Resource StockFeignService service; @GetMapping("/add") public String add () { return "下单成功" + service.deduct(); } }
Feign日志配置使用
先创建一个配置类:
1 2 3 4 5 6 7 8 public class FeignConfig { @Bean public Logger.Level feignLoggerLevel () { return Logger.Level.FULL; } }
@Configuration加上就是全局配置 ,对所有接口都生效
不加@Configuration,则为部分配置,需要在接口中这样写:
1 2 3 4 5 6 7 @FeignClient(name = "stock-service", path = "/stock", configuration = FeignConfig.class) public interface StockFeignService { @RequestMapping("/deduct") String deduct () ; }
需要加上configuration参数,配置完后不显示日志,是因为spirngboot 默认info 级别,大于debug,需要配置:
1 2 3 4 logging: level: com.example.order.feign: debug
配置文件配置日志:
1 2 3 4 5 6 feign: client: config: stock-service: loggerLevel: BASIC
其他配置 契约配置:
1 2 3 4 5 6 feign: client: config: stock-service: loggerLevel: BASIC contract: feign.Contract.Default
SpringCloud在Feign的基础上做了扩展,使用SpringMVC的注解来完成Feign的功能。原生Feign是不支持SpringMVC注解的,如果你想在SpringCloud中使用原生注解的方式来定义客户端也是可以的,通过契约来改变这个配置,SpringCloud中模式是使用SpringMVC Contract。
超时时间配置:
1 2 3 4 5 6 7 8 feign: client: config: stock-service: loggerLevel: BASIC connectTimeout: 5000 readTimeout: 3000
拦截器配置:
在客户端消费者调用提供者服务时进行拦截。
1 2 3 4 5 6 7 8 9 10 11 public class CustomFeignInterceptor implements RequestInterceptor { Logger logger = LoggerFactory.getLogger(this .getClass()); @Override public void apply (RequestTemplate template) { template.header("xxx" , "xxx" ); logger.info("Feign Interceptor!!!!!!!!!!!!!!!!!" ); } }
全局配置(针对所有接口调用):
配置类中加上@Configuration,并且注入到Spring中:
1 2 3 4 @Bean public CustomFeignInterceptor customFeignInterceptor () { return new CustomFeignInterceptor (); }
局部配置(推荐):
直接在配置中声明:
1 2 3 4 5 6 7 8 9 10 feign: client: config: stock-service: loggerLevel: BASIC connectTimeout: 5000 readTimeout: 3000 requestInterceptors[0]: com.example.order.intercptor.CustomFeignInterceptor
Nacos Config 配置中心 Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。
主要结构图:
方便维护,统一管理,安全,时效
几个配置中心的对比:
使用配置中心 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency >
必须使用 bootstrap.properties 配置文件来配置Nacos Server 地址
bootstrap.yml:
1 2 3 4 5 6 7 8 spring: application: name: com.example.user cloud: nacos: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos
其中name必须和Data Id相同,否则将读取不到配置。
启动类读取配置:
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootApplication public class ConfigApplication { public static void main (String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(ConfigApplication.class, args); String userName = applicationContext.getEnvironment().getProperty("user.name" ); String userAge = applicationContext.getEnvironment().getProperty("user.age" ); String userConfig = applicationContext.getEnvironment().getProperty("user.config" ); System.out.println("UserName: " + userName); System.out.println("UserAge: " + userAge); System.out.println("Config: " + userConfig); } }
nacos-config的原理是判断文件是否修改,修改了的文件md5值会发生变化,md5发生变化就会去拉取文件。
即先在配置文件中配置profiles:
application.yml:
1 2 3 4 5 6 server: port: 8020 spring: profiles: active: dev
然后配置中心新建配置:
Nacos Config也支持Namespace和Group管理配置文件,
直接在bootstrap.yml指定相对应的dev的id,group名即可
1 2 3 4 5 6 7 8 9 10 11 12 13 spring: application: name: com.example.user cloud: nacos: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos config: file-extension: yaml namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d group: example
如何使用的自定义Data Id:
方法1,shared-configs:
在bootstrap.yml配置shared-configs即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 spring: application: name: com.example.user cloud: nacos: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos config: file-extension: yaml namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d group: example shared-configs: - data-id: com.example.common.properties refresh: true - data-id: com.example.common02.properties refresh: true
配置中心这样写:
方法2,extension-configs:
两种方法都差不多,配置bootstrap.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 spring: application: name: com.example.user cloud: nacos: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos config: file-extension: yaml namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d group: example shared-configs: - data-id: com.example.common.properties refresh: true - data-id: com.example.common02.properties refresh: true extension-configs: - data-id: com.example.common.properties refresh: true
关于这几种方法的优先级: 优先级 profile(dev,prod..) > 默认配置文件 > extension-configs > shared-configs
如何在Controller中使用配置 和直接写在application.yml中的配置使用一样,使用@Value
注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("/config") @RefreshScope public class ConfigController { @Value("${user.name}") private String userName; @GetMapping("/show") public String showConfig () { return "UserName: " + userName; } }
但是配置变化无法感知,需要加上@RefreshScope
注解,自动感知。
Sentinel的配置和使用 关于Sentinel 服务雪崩:
当一个不太重要的积分服务Down了,依赖积分服务的商品服务得不到相应,大量请求堆积,导致一系列的服务Down。
出现异常的几种形式:
如何解决服务雪崩:
限流
即控制QPS(Queries Per Second),当QPS超过了设定的阈值,则进行相应的处理。
超时机制
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题。
隔离
原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。
隔离前:
服务熔断
Sentinel的使用 下载sentinel-dashboard,配置启动项:
加入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-core</artifactId > <version > 1.8.1</version > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-annotation-aspectj</artifactId > <version > 1.8.1</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-transport-simple-http</artifactId > <version > 1.8.1</version > </dependency > </dependencies >
使用代码编写流控规则:
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 private static final String RESOURCE_NAME = "Hello" ;private static final String USER_RESOURCE_NAME = "Hello" ;private static final String DEGRADE_RESOURCE_NAME = "degrade" ;@GetMapping("/goods") public String getGoods () { Entry entry = null ; try { entry = SphU.entry(RESOURCE_NAME); String str = "Hello Sentinel" ; log.info("字符串: " + str); return str; } catch (BlockException e1) { log.info("block!!!!!!!!" ); return "当前QPS过高,被流控" ; } catch (Exception ex) { Tracer.traceEntry(ex, entry); } finally { if (entry != null ) { entry.exit(); } } return null ; }@PostConstruct private static void initFlowRules () { List<FlowRule> rules = new ArrayList <>(); FlowRule rule = new FlowRule (); rule.setResource(RESOURCE_NAME); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(1 ); rules.add(rule); FlowRule rule2 = new FlowRule (); rule2.setResource(USER_RESOURCE_NAME); rule2.setGrade(RuleConstant.FLOW_GRADE_QPS); rule2.setCount(1 ); rules.add(rule2); FlowRuleManager.loadRules(rules); }
可以看到,侵入性强,不方便。
推荐使用Spring的面向切片 编程:
启动类需要这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootApplication public class SentinelDemoApplication { public static void main (String[] args) { SpringApplication.run(SentinelDemoApplication.class, args); } @Bean public SentinelResourceAspect sentinelResourceAspect () { return new SentinelResourceAspect (); } }
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @GetMapping("/user") @SentinelResource(value = USER_RESOURCE_NAME, blockHandler = "blockHandlerForGetUser", fallback = "fallbackHandler") public User getUser (String id) { return new User ("Azusa" ); }public User blockHandlerForGetUser (String id, BlockException ex) { ex.printStackTrace(); return new User ("当前QPS过高,被流控!" ); }public User fallbackHandler (String id, Throwable e) { e.printStackTrace(); return new User ("ERROR" ); }
加上@SentinelResource注解,写blockHandler和falllback方法。
基于QPS与线程数流控对比:
熔断降级规则编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @GetMapping("/de") @SentinelResource(value = DEGRADE_RESOURCE_NAME, entryType = EntryType.IN, blockHandler = "blockTest") public User degradeTest (String id) { throw new RuntimeException ("异常" ); }public User blockTest (String id, BlockException ex) { ex.printStackTrace(); return new User ("触发熔断" ); }@PostConstruct public void initDegradeRule () { List<DegradeRule> degradeRules = new ArrayList <>(); DegradeRule degradeRule = new DegradeRule (); degradeRule.setResource(DEGRADE_RESOURCE_NAME); degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); degradeRule.setCount(2 ); degradeRule.setMinRequestAmount(2 ); degradeRule.setStatIntervalMs(60 *1000 ); degradeRule.setTimeWindow(10 ); degradeRules.add(degradeRule); DegradeRuleManager.loadRules(degradeRules); }
使用Sentinel控制台进行流控 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency > </dependencies >
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server: port: 8010 spring: application: name: order-sentinel cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public sentinel: transport: dashboard: 127.0 .0 .1 :8869
控制类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @RestController @RequestMapping("/order") public class OrderController { @Resource TestService testService; @GetMapping("/add") public String add () { return "下单成功" ; } @GetMapping("/flow") public String flow () { return "流控测试" ; } public String block (BlockException e) { return "QPS过高" ; } @GetMapping("/test1") public String test1 () { return testService.getUser(); } @GetMapping("/test2") public String test2 () { return testService.getUser(); } }
之后,访问任意一个接口,sentinel控制台都会有相应接口:
可以直接在控制台添加流控规则,不需要写代码:
降级规则也同样可以设置:
当然,默认返回的流控信息不是很方便,同样可以使用@SentinelResource注解设置blockHandler方法。
关联流控模式:
如果访问超过了2次/order/add接口,/order/get查询订单将会被限流。
链路流控模式:
配置文件需要启用链路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: application: name: order-sentinel cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public sentinel: transport: dashboard: 127.0 .0 .1 :8869 web-context-unify: false
需要在ServiceImpl类中实现:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class TestServiceImpl implements TestService { @Override @SentinelResource(value = "getUser",blockHandler = "blockHandler") public String getUser () { return "返回用户" ; } public String blockHandler (BlockException e) { return "被流控!!" ; } }
不然默认返回500的错误界面。
频繁访问/order/test2,被流控,但是访问/order/test1不会被流控。
统一异常处理 统一返回结果类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Result <T> { private Integer code; private String msg; private T data; public Result (Integer code, String msg, T data) { this .code = code; this .msg = msg; this .data = data; } public Result (Integer code, String msg) { this .code = code; this .msg = msg; } public Integer getCode () { return code; } public void setCode (Integer code) { this .code = code; } public String getMsg () { return msg; } public void setMsg (String msg) { this .msg = msg; } public T getData () { return data; } public void setData (T data) { this .data = data; } public static Result error (Integer code, String msg) { return new Result (code, msg); } }
统一异常类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Component public class MyBlockExceptionHandler implements BlockExceptionHandler { Logger log = LoggerFactory.getLogger(this .getClass()); @Override public void handle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { log.info("BlockExceptionHandler, 触发中断: " + e.getRule()); Result r = null ; if (e instanceof FlowException) { r = Result.error(100 , "接口限流" ); } else if (e instanceof DegradeException) { r = Result.error(101 , "服务降级" ); } else if (e instanceof ParamFlowException) { r = Result.error(102 , "热点参数限流" ); } else if (e instanceof SystemBlockException) { r = Result.error(103 , "触发系统保护规则" ); } else if (e instanceof AuthorityException) { r = Result.error(104 , "授权规则不通过" ); } httpServletResponse.setStatus(500 ); httpServletResponse.setCharacterEncoding("utf-8" ); httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); new ObjectMapper ().writeValue(httpServletResponse.getWriter(), r); } }
访问接口,如果限流就会同意返回JSON:
流控效果 直接失败:
超过设定值直接返回流控信息。
Warm Up:
Jmeter压测工具使用:
300个线程,在10s内启动完毕,
新建HTTP请求,访问接口,
可以在结果树中看到访问成功的线程越来越多,逐渐到达阈值,
sentinel中也类似可以看到:
排队等待:
目的是解决脉冲流量。
效果如下:
熔断降级策略设置 注意熔断的半开状态,如果熔断并且过了熔断时长后就进入了半开状态,接下来的第一个请求如果达不到设置的值,直接熔断。
慢调用比例:
异常比例:
统计10s内,最少出现100个请求,且出现异常的比例大于0.5(50个),则进入熔断。
异常数:
同理,只不过把比例换成了实际的数值。
Sentinel 整合OpenFeign 依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency > </dependencies >
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 server: port: 8010 spring: application: name: order-sentinel-feign cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public sentinel: transport: dashboard: 127.0 .0 .1 :8869 feign: sentinel: enabled: true
FeignService:
1 2 3 4 5 6 7 8 9 10 11 12 13 @FeignClient(name = "stock-service", path = "/stock", fallback = StockFeignServiceFallback.class) public interface StockFeignService { @RequestMapping("/deduct") String deduct () ; @RequestMapping("/error") String errorTest () ; @RequestMapping("/get/{id}") String buy (@PathVariable("id") Integer id) ; }
FeignServiceFallback类,用来降级时显示信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class StockFeignServiceFallback implements StockFeignService { @Override public String deduct () { return "降级!!!!" ; } @Override public String errorTest () { return "降级!!!!" ; } @Override public String buy (Integer id) { return "降级!!!!!" ; } }
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController @RequestMapping("/order") public class OrderController { @Resource StockFeignService service; @GetMapping("/add") public String add () { return "下单成功" + service.errorTest(); } @GetMapping("/buy/{id}") @SentinelResource(value = "getById", blockHandler = "hotBlockHandler") public String buyGoods (@PathVariable("id") Integer id) { return "购买成功," + service.buy(id); } public String hotBlockHandler (@PathVariable("id") Integer id, BlockException e) { return "HotBlock" ; } }
添加限流熔断的方法和上面一模一样。
热点限流:
如果大部分是热点参数,那阈值主要针对热点参数进行流控,其他就针对普通参数进行流控。
如果大部分是普通流量,那主要就对普通流量进行设置。
当id为1时,访问超过2次就会进入限流,其他参数都是正常的。
系统保护规则
相较于上面对单个服务的流控,系统保护则是对整个机器访问的保护。
具体参数设置非常简单,超过阈值就无法访问,适用于order-service的所有接口。
使用Nacos Config 规则持久化 依赖需要添加:
1 2 3 4 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency >
在Nacos中添加配置:
配置文件添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: application: name: order-sentinel cloud: sentinel: transport: dashboard: 127.0 .0 .1 :8869 web-context-unify: false datasource: flow-rule: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos dataId: order-sentinel-flow-rule rule-type: flow
访问一次接口可以在sentinel中看到配置好的规则。
Seata配置和使用 事务概念:
本地事务:一般是单体架构,只有一个数据库。
分布式事务:
一般是处理多个数据库,并且有不同的操作,这时使用@Transactional注解没有作用,需要使用分布式事务解决方案Seata。
常见的事务解决方案都基于2PC这一概念:
即一次Prepare,等待确认,另一次Commit,等待确认。
几种分布式事务解决方案的结构:
MQ消息队列:
提交预备消息。
如果成功发出确认消息,此时会真正投递消息。
如果确认信息发送失败,MQ有回查机制一定的次数(15次)。如果回查失败,就会删除确认消息,就不会进行真正消息的投递了。
当确认消息已经确认,那么MQ会告知消费者真正去扣减库存,如果MQ没有通知成功,MQ也有重试的机制。
如果MQ最终没有重试成功的通知到消费者,此时就需要人工干预了。
Seata的配置
第一阶段:
第二阶段:
第一阶段遇到异常回滚:
Seata Server 单机部署:
下载Releases:https://github.com/seata/seata/releases
配置Seata 高可用模式(DB):
修改conf/file.conf为DB模式:
新建数据库,并导入sql脚本https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql
配置Seata服务为Nacos模式:
修改conf/registry.conf文件:
修改script/config-center/config.txt
1 2 3 4 5 6 7 8 9 service.vgroupMapping.my_test_tx_group =default store.mode =db store.db.datasource =druid store.db.dbType =mysql store.db.driverClassName =com.mysql.jdbc.Driver store.db.url =jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true store.db.user =root store.db.password =root
使用Python脚本导入配置:
此时启动seata:使用seata/bin/seata-server.bat -p 8866
Seata Client配置 一共两个服务:
Order服务,调用Stock
Stock服务,提供接口给Order
Order的一个接口/order/add,调用时在seata_order数据库中新增一条记录,并调用Stock服务,Stock服务为更新stock_seata数据库的表中减少库存数量
准备工作:
新建两个数据库,并建好表:
undo_log就是用来存储事务中间过程的一些信息,当事务开始时,也就是第一阶段,向undo_log中插入一条记录,当收到seata对commit确认后,删除undo_log中的记录,当收到rollback时,使用undolog做交易补偿,然后删除undolog。
依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <dependencies > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-seata</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > </dependency > </dependencies >
配置文件,order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 spring: datasource: username: root password: root url: jdbc:mysql://127.0.0.1:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC& driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource schema: classpath:sql/schema.sql initialization-mode: NEVER application: name: alibaba-order-seata cloud: nacos: discovery: server-addr: 192.168 .2 .125 :8848 username: nacos password: nacos alibaba: seata: tx-service-group: my_test_tx_group mybatis: mapper-locations: classpath:com/tulingxueyuan/order/mapper/*Mapper.xml typeAliasesPackage: com.tulingxueyuan.order.pojo configuration: mapUnderscoreToCamelCase: true server: port: 8070 seata: registry: type: nacos nacos: server-addr: 192.168 .2 .125 :8848 application: seata-server username: nacos password: nacos group: SEATA_GROUP config: type: nacos nacos: server-addr: 192.168 .2 .125 :8848 application: seata-server username: nacos password: nacos group: SEATA_GROUP
注意坑点:
必须要配置tx-service-group: my_test_tx_group
,否则会一直报错:
no available service ‘null‘ found, please make sure registry config correct
OrderService类中这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service public class OrderServiceImpl implements OrderService { @Autowired OrderMapper orderMapper; @Autowired StockService stockService; @Override @GlobalTransactional public Order create (Order order) { orderMapper.insert(order); stockService.reduct(order.getProductId()); return order; } }
没有异常,先运行一遍,可以看到数据库中正常执行了语句,库存也减少了:
再人为制造一个异常,在执行一遍,可以看到网页出现500异常:
order-server:
stock-service:
可以看到更新完直接回滚,
Seata中也有相应的信息:
数据库中库存没有减少:
GateWay 网关 API网关:
所谓的API网关,就是系统的统一入口 ,它封装了应用哦程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,如,认证,鉴权,监控,路由转发等。
总结:网关的作用可以帮我们维护一些公共的功能,节省重复造轮子的事情,结合nacos注册中心后,还可以帮我们维护服务的地址,进行统一的路由。就像小区里的门卫。
整个结构就如下图:
Spring Cloud GateWay 相关概念:
Spring Cloud GateWay 功能特性:
基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建
动态路由:能够匹配任何请求属性
支持路径重写
集成 Spring Cloud 服务发现功能(Nacos、Eruka)
可集成流控降级功能( Sentinel. Hystrix)
可以对路由指定易于编写的Predicate(断言)和Filter(过滤器);
核心概念:
路由(route) 路由是网关中最基础的部分,路由信息包括-个ID、一个目的URI、一组断言工厂、一组ilter组成。 如果断言为真,则说明请求的URL和配的路由匹配。
断言(predicates) Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange.断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。
过滤器(Filter) SpringCloud Gateway中的ilter分为Gateway Fller和Global Fiter。Flter可以对请求和响应进行处理。
Spring Cloud GateWay 使用 简单使用转发功能:
新建网关模块,设置依赖:
1 2 3 4 5 6 <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-gateway</artifactId > </dependency > </dependencies >
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 8088 spring: application: name: api-gateway cloud: gateway: routes: - id: order_route uri: http://localhost:8010 predicates: - Path=/order-serv/** filters: - StripPrefix=1
访问8088端口,自动转发到8010,成功。
GateWay整合Nacos: 上面的基础上添加Nacos依赖:
1 2 3 4 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency >
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 8088 spring: application: name: api-gateway cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order-serv/** filters: - StripPrefix=1
一种简便方法配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: port: 8088 spring: application: name: api-gateway cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public gateway: discovery: locator: enabled: true
这样无需配置断言,过滤器之类的,直接以服务名作为断言路径。
断言工厂
尝试:
1 2 3 4 5 6 7 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** - After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]
正常访问。
设置时间晚一点,出现404。Before,Between同理。
加上验证请求头的断言:
1 2 3 4 5 6 7 8 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** - After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai] - Header=X-Request-Id,\d+
加上请求头正常访问,去掉请求头404。
同样,可以加入请求类型,指定请求参数的断言,操作都一样。
自定义断言工厂:
自定义路由断言工厂需要继承AbstractRoutePredicateFactory类,重写apply方法的逻辑。在apply方法中可以通过exchange getRequest()拿倒ServerHttpRequest对象,从而可以获取到请求的参数、请求方式、请求头等信息。
要求:
必须spring组件 bean
类必须加kRoutePredicateFactory作为结尾
必须继承AbstractRoutePredicateFactory
必须声明静态内部类声明属性来接收配置文件中对应的断言的信息
需要结合shortcutFieldOrder进行绑定
通过apply进行逻辑判断true就是匹配成功false匹配失败
模仿自带的断言工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Component public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory <CheckAuthRoutePredicateFactory.Config> { public CheckAuthRoutePredicateFactory () { super (CheckAuthRoutePredicateFactory.Config.class); } @Override public List<String> shortcutFieldOrder () { return Arrays.asList("name" ); } public Predicate<ServerWebExchange> apply (CheckAuthRoutePredicateFactory.Config config) { return new GatewayPredicate () { public boolean test (ServerWebExchange exchange) { if (config.getName().equals("Azusa" )){ return true ; } return false ; } }; } @Validated public static class Config { private String name; public String getName () { return name; } public void setName (String name) { this .name = name; } } }
配置文件中加上断言:
1 2 3 4 5 6 7 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** - CheckAuth=Azusa
正常访问,修改value为其他的值,404。
过滤器工厂 自带的过滤器:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#token-relay-gatewayfilter-factory
尝试以下添加请求头的过滤器:
1 2 3 4 5 6 7 8 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** filters: - AddRequestHeader=X-Request-clor, red
在Order中添加接口来查看请求头是否设置好了:
1 2 3 4 5 @RequestMapping("/header") public String header (@RequestHeader("X-Request-color") String color) { System.out.println("header信息接口" ); return color; }
访问header:
尝试添加前缀路径的过滤器:
order-server的设置context-path:
1 2 3 4 server: port: 8010 servlet: context-path: /mall
order-server必须加上路径才能访问。
在GateWay中配置PrefixPath:
1 2 3 4 5 6 7 8 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** filters: - PrefixPath=/mall
不需要加上前缀也可以访问。
尝试使用重定向过滤器:
1 2 3 4 5 6 7 8 9 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** filters: - PrefixPath=/mall - RedirectTo=302, https://www.baidu.com/
直接重定向到百度。
自定义过滤器工厂:
和自定义断言工厂一样模仿:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.example.filters;import org.apache.commons.lang.StringUtils;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.Arrays;import java.util.List;@Component public class CheckAuthGatewayFilterFactory extends AbstractGatewayFilterFactory <CheckAuthGatewayFilterFactory.Config> { public CheckAuthGatewayFilterFactory () { super (CheckAuthGatewayFilterFactory.Config.class); } public List<String> shortcutFieldOrder () { return Arrays.asList("value" ); } public GatewayFilter apply (CheckAuthGatewayFilterFactory.Config config) { return new GatewayFilter () { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { String name = exchange.getRequest().getQueryParams().getFirst("name" ); if (StringUtils.isNotBlank(name)){ if (config.getValue().equals(name)){ return chain.filter(exchange); }else { exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); return exchange.getResponse().setComplete(); } } return chain.filter(exchange); } }; } public static class Config { String value; public String getValue () { return value; } public void setValue (String value) { this .value = value; } public Config () { } } }
配置文件:
1 2 3 4 5 6 7 8 9 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** filters: - PrefixPath=/mall - CheckAuth=Azusa
成功验证,当参数与配置中不相同时,404错误。
全局过滤器
局部过滤器和全局过滤器区别: 局部:局部针对某个路由,需要在路由中进行配置 全局:针对所有路由请求,一旦定义就会投入使用
写一个类,实现GlobalFilter
1 2 3 4 5 6 7 8 9 @Slf4j @Component public class LogFilter implements GlobalFilter { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { log.info(exchange.getRequest().getPath().value()); return chain.filter(exchange); } }
使用Reactor Netty访问日志:
设置VM:-Dreactor.netty.http.server.accessLogEnabled=true
比上面更加详细
跨域相关问题 前端页面不在同一个域中发起请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <script src ="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js" > </script > </head > <body > <button onclick ="testCros()" > 发送信息</button > <script > function testCros ( ){ $.get ("http://localhost:8088/order/add" ,function (res ){ alert (res) }) } </script > </body > </html >
有跨域错误。
添加GateWay的允许跨域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** filters: - PrefixPath=/mall - CheckAuth=Azusa globalcors: cors-configurations: '[/**]' : allowedOrigins: "*" allowedMethods: - GET - POST
显示结果正常。
使用配置类的方法允许跨域:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter () { CorsConfiguration config = new CorsConfiguration (); config.addAllowedMethod("*" ); config.addAllowedOrigin("*" ); config.addAllowedHeader("*" ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (new PathPatternParser ()); source.registerCorsConfiguration("/**" ,config); return new CorsWebFilter (source); } }
也可以正常请求。
GateWay整合Sentinel 加入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependencies > <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 > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-alibaba-sentinel-gateway</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency > </dependencies >
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server: port: 8088 spring: application: name: api-gateway cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** sentinel: transport: dashboard: 127.0 .0 .1 :8869
打开Sentinel,随便访问一个接口,例如http://localhost:8088/order/add
流控测试:
正常。
详细功能测试:
间隔,就是Queries Per 间隔
Burst size:指的是QPS阈值可以再允许访问的次数,比如QPS阈值为2,设置了Brust size,则表示访问4次不会流控,第五次才流控。
针对请求属性:
相当于断言工厂,只有设置的这些匹配了才会限流,
测试成功。其他配置和之前sentinel一模一样。
API分组流控:
即把不同接口统一管理,进行流控。
访问http://localhost:8088/order/add 和 http://localhost:8088/order/flow 都限流,访问其他接口不限流。
降级规则设置:
和之前sentinel一样
自定义异常处理(代码):
写一个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration public class GatewayConfig { @PostConstruct public void init () { BlockRequestHandler blockRequestHandler = new BlockRequestHandler () { @Override public Mono<ServerResponse> handleRequest (ServerWebExchange serverWebExchange, Throwable throwable) { System.out.print(throwable); HashMap<String,String> map =new HashMap <>(); map.put("code" , HttpStatus.TOO_MANY_REQUESTS.toString()); map.put("message" ,"限流了" ); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
自定义异常处理(配置文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 spring: application: name: api-gateway cloud: nacos: server-addr: 192.168 .2 .125 :8848 discovery: username: nacos password: nacos namespace: public gateway: routes: - id: order_route uri: lb://order-service predicates: - Path=/order/** sentinel: transport: dashboard: 127.0 .0 .1 :8869 scg: fallback: mode: response response-body: '"code":"429 TOO_MANY_REQUESTS","message":"限流了..."}'
达到同样的效果。
GateWay 高可用集群 使用nginx反向代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 upstream gateway { server localhost:5000 ; server localhost:5001 ; }server { listen 80 ; server_name localhost; location / { proxy_pass http://gateway; } }
SkyWalking
因为复杂的调用关系,我们很难通过查看日志的方式去定位问题,我们需要捋清调用的链路,每个结点的访问详细信息等等。
SkyWalking 配置 主要框架结构:
SkyWalking 部署:
下载:https://skywalking.apache.org/downloads/
修改webapp端口:webapp/webapp.yml
最新的SkyWalking还需要单独下载java-agent,
接入微服务 接入单个微服务:
只需要配置启动项:
1 2 3 -javaagent:D:\Java\skywalking-agent\skywalking-agent.jar -DSW_AGENT_NAME=api-service -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800
分别配置java-agent路径,要显示的服务名,以及skywalking的ip:端口
接入多个微服务:
直接修改启动配置就可以了。
效果:
SkyWalking 持久化 为啥持久化:
内存占用越来越多,需要经常重启
SkyWalking配置文件修改:
config/application.yml
启动SkyWalking,各种数据就保存在数据库中。
SkyWalking 自定义链路追踪 添加依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.skywalking</groupId > <artifactId > apm-toolkit-trace</artifactId > <version > 8.9.0</version > </dependency >
在service中添加注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Trace @Tag(key="getAll",value="returnObj") public List<OrderTbl> all () throws InterruptedException { TimeUnit.SECONDS.sleep(2 ); return orderMapper.selectList(null ); } @Trace @Tags({ @Tag(key="getAll",value = "returnedObj"), @Tag(key="getAll",value="arg[0]") }) public OrderTbl get (Integer id) { return orderMapper.selectById(id); }
controller:
1 2 3 4 5 6 7 8 @RequestMapping("/get/{id}") public OrderTbl get (@PathVariable Integer id) { return orderService.get(id); } @RequestMapping("/all") public List<OrderTbl> getAll () throws InterruptedException { return orderService.all(); }
访问接口,可以在skywalking中看到返回值和参数。
性能剖析 一开始是没有数据的
需要新建任务:
日志 首先加入依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.skywalking</groupId > <artifactId > apm-toolkit-logback-1.x</artifactId > <version > 8.9.0</version > </dependency >
新建logback-spring.xml日志配置文件在resources目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <include resource ="org/springframework/boot/logging/logback/defaults.xml" /> <appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.core.encoder.LayoutWrappingEncoder" > <layout class ="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout" > <Pattern > -%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} [%tid] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}</Pattern > </layout > </encoder > </appender > <appender name ="grpc-log" class ="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender" > <encoder class ="ch.qos.logback.core.encoder.LayoutWrappingEncoder" > <layout class ="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout" > <Pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern > </layout > </encoder > </appender > <root level ="info" > <appender-ref ref ="CONSOLE" /> <appender-ref ref ="grpc-log" /> </root > </configuration >
其中,配置了输出到控制台的日志和输出到网页上的日志
请求接口:
点击右边的追踪ID可以直接跳到追踪所显示的页面:
如果SkyWalking部署在远程,需要修改Agent的配置:
config\agent.config:
1 2 3 4 plugin.toolkit.log.grpc.reporter.server_host =${SW_GRPC_LOG_SERVER_HOST:127.0.0.1} plugin.toolkit.log.grpc.reporter.server_port =${SW_GRPC_LOG_SERVER_PORT:11800} plugin.toolkit.log.grpc.reporter.max_message_size =${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760} plugin.toolkit.log.grpc.reporter.upstream_timeout =${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30}
告警
webhooks:
出现报警自动调用这个接口,这个接口可以接受这些参数,用Server酱之类的提醒:
config/alarm-settings.yml:
1 2 3 4 5 webhooks: - http://127.0.0.1:8848/notify/
编写webhook接口:
按照这个类来写:
https://github.com/apache/skywalking/blob/8.9.1/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Setter @Getter @ToString public class AlarmMessage { private int scopeId; private String scope; private String name; private String id0; private String id1; private String ruleName; private String alarmMessage; private List<Tag> tags; private long startTime; private transient int period; private transient boolean onlyAsCondition; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Setter @EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor public class Tag { private String key; private String value; @Override public String toString () { return key + "=" + value; } public static class Util { public static List<String> toStringList (List<Tag> list) { if (CollectionUtils.isEmpty(list)) { return Collections.emptyList(); } return list.stream().map(Tag::toString).collect(Collectors.toList()); } }
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/alarm") public class SwAlarmController { @RequestMapping("/receive") public void receive (@RequestBody List<AlarmMessage> alarmList) { System.out.println("alarm receive" ); log.info("告警邮件信息已经发送。。。" ); StringBuffer stringBuffer = new StringBuffer (); for (AlarmMessage alarmMessage : alarmList) { stringBuffer.append(alarmMessage); log.info(alarmMessage.toString()); System.out.println(alarmMessage); } } }
出现告警信息:
SkyWalking高可用
接下来,配置skywalking:
config/application.yml
改为nacos,再配置nacos相关ip等
然后修改webapp的配置:
webapp/webapp.yml:
添加服务器IP即可;
最后,在启动配置中用逗号分隔不同的服务器: