服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现
。
服务注册:服务实例将自身服务信息注册到注册中心。
服务发现:服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务
服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。
在微服务架构中,通常存在多个服务之间远程调用的需求。木IQ铵主流远程调用技术有基于http的RESTful接口以及基于TCP的RPC协议
比较项 | RESTful | RPC |
---|---|---|
通讯协议 | HTTP | 一般使用TCP |
性能 | 略低 | 较高 |
灵活度 | 高 | 低 |
应用 | 微服务架构 | SOA架构 |
随着微服务的不断增多,不同的微服务—般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
针对这些问题,AP网关顺势而生。API网关直面意思是将所有API调用统接入到API网关层,由网关层统接入和输出,是整个微服务的入口
。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。
有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建
在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布
在了几千台服务器,横跨多个不同的数据中心。因此,就需要对—次请求涉及的多个服链路进行日志记录、性能
监控,即链路追踪。
Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球
首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发
者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。
Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
Spring Cloud Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组
件,方便开发者通过 Spring Cloud编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,只需要添加一些注解和少量配置,就可以将 Spring Cloud应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
springcloud-alibaba——父工程
shop-common——公共模块【实体类、工具类】
shop-user——用户微服务【端口:807X】
shop-product——商品微服务【端口:808×】
shop-order——订单微服务【端口:809x】
修改父工程的pom文件
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.zrlgroupId>
<artifactId>springcloud-alibabaartifactId>
<packaging>warpackaging>
<version>1.0-SNAPSHOTversion>
<repositories>
<repository>
<id>alimavenid>
<name>aliyun mavenname>
<url>http://maven.aliyun.com/nexus/content/groups/public/url>
repository>
repositories>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.3.RELEASEversion>
parent>
<properties> <java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF- 8project.reporting.outputEncoding>
<spring-cloud.version>Greenwich.RELEASEspring-cloud.version>
<spring-cloud-alibaba.version>2.1.0.RELEASEspring-cloud-alibaba.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
project>
版本对应关系
创建的maven项目继承父工程,添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
dependencies>
创建实体类
//用户
@Entity(name = "shop_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer uid;//主键
private String username;//用户名
private String password;//密码
private String telephone;//手机号
}
//商品
@Entity(name = "shop_product")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
}
//订单
@Entity(name = "shop_order")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;//订单id
private Integer uid;//用户id
private String username;//用户名
private Integer pid;//商品id
private String pname;//商品名称
private Double pprice;//商品单价
private Integer number;//购买数量
}
创建maven项目,添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.zrlgroupId>
<artifactId>shop-commonartifactId>
dependency>
dependencies>
创建UserApplication.java启动文件
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
在resources文件夹中创建application.yml
配置文件
服务治理是微服务架构中最核心最基本的模块,用于实现各个微服务的自动化注册与发现。
通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用,一般包含如下几个功能:
服务发现:
服务配置:
服务健康检测
Zookeeper:zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
Eureka:Eureka是Springcloud Netflflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源
Consul:Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
Nacos:Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。
Nacos就是微服务架构中服务注册中心以及统一配置中心,用来替换原来的(eureka,consul)以及config组件,可以这样认为nacos=eureka+config
https://nacos.io/zh-cn/index.html
下载nacos:
https://github.com/alibaba/nacos/releases
目录介绍:
- bin 启动nacos服务的脚本目录
- conf nacos的配置文件目录
- target nacos的启动依赖存放目录
- data nacos启动成功后保存数据的目录
启动服务:
- linux/unix/mac启动
打开终端进入nacos的bin目录执行如下命令
./startup.sh -m standalone
- windows启动
在cmd中
执行 startup.cmd -m standalone 或者双击startup.cmd运行文件。
访问nacos的web服务管理界面:
- http://localhost:8848/nacos/
- 用户名 和 密码都是nacos
引入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
配置注册地址:
server.port=8789 #指定当前服务端口
spring.application.name=nacosclient #指定服务名称
spring.cloud.nacos.server-addr=localhost:8848 #指定nacos服务地址
spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr} #指定注册中心地址
management.endpoints.web.exposure.include=* #暴露所有web端点
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
主类加上注解(新版本可不加):
@EnableDiscoveryClient
@SpringBootApplication
public class ShopProductApplication {
public static void main(String[] args) {
SpringApplication.run(ShopProductApplication.class, args);
}
}
查看服务列表:
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/order/prod/{pid}")
public Object order(@PathVariable("pid") Integer pid) {
//根据服务名调用服务
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance serviceInstance = instances.get(0);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
//http://169.254.138.135:8081
URI uri = serviceInstance.getUri();
//调用商品微服务,查询商品信息
Product product =
restTemplate.getForObject(uri+"/product/" + pid, Product.class);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
}
先配置一下idea,使其启动将同一个微服务启动两次
查看nacos服务列表,发现实例数为2了,酷酷的
使用random随机选择:
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
int index = new Random().nextInt(instances.size());
ServiceInstance serviceInstance = instances.get(index);
Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡
第一步:在RestTemplate(ShopOrderApplication类中)的生成方法上添加@LoadBalanced
注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
第二步:修改服务调用的方法,不用再通过nacos获取实例了,通过服务名就OK
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
//调用商品微服务,查询商品信息
Product product =
restTemplate.getForObject("http://service-product/product/" + pid, Product.class);
//todu
}
Ribbon默认使用轮询(RoundRobinRule)的负载均衡,常用的还有随机、最小并发等策略,支持的负载均衡策略如下:
策略名 | 策略描述 | 实现说明 |
---|---|---|
BestAvailableRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(activeconnections 超过配置的阈值) | 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就是检查status里记录的各个server的运行状态 |
WeightedResponseTimeRule | 根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。 | 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成statas时,使用roubine策略选择server。 |
RetryRule | 对选定的负载均衡策略机上重试机制。 | 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server |
RoundRobinRule | 轮询方式轮询选择server | 轮询index,选择index对应位置的server |
RandomRule | 随机选择一个server | 在index上随机,选择index对应位置的server |
ZoneAvoidanceRule | 复合判断server所在区域的性能和server的可用性选择server | 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。 |
在服务调用者上添加yml配置:
service-product: # 服务提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果
在服务调用者上引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
在主类添加注解
@EnableFeignClients//开启Fegin的客户端
@SpringBootApplication
public class ShopOrderApplication {}
编写调用接口
@FeignClient("service-product")//声明调用的提供者的name
public interface ProductService {
//指定调用提供者的哪个方法
// @FeignClient+@GetMapping 就是一个完整的请求路径 http://service-product/product/{pid}
@GetMapping("/product/{pid}")
Product findByPid(@PathVariable("pid") Integer pid);
}
修改controller的调用方式
@Autowired
private ProductService productService;
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
//通过fegin调用商品微服务
Product product = productService.findByPid(pid);
//todu
}
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
常见的容错思路有隔离、超时、限流、熔断、降级这几种
将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案
Hystrix:Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J:Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel:Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
放在服务调用者这里
添加依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
添加配置:
spring:
cloud:
sentinel:
transport:
port: 9999 # 和控制台交流的端口,随意指定一个未使用的端口
dashboard: localhost:8080 # 指定控制台的地址
启动Sentinel控制台:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
sentinel是懒加载,只有调用过微服务之后才会出现在sentinel中
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流控模式规则配置:
直接:接口达到限流条件时,开启限流
关联:被关联的资源达到限制时,该资源自身也限流,类似于让步的操作
链路:当从某个接口过来的资源达到限流条件时,开启限流,针对来源是针对微服务的,这个是更加细粒度的限流
使用链路限流的时候,在需要限流的方法上加上
@SentinelResource("message")
注解,value是资源名称。SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛,否则链路限流不生效
流控效果规则配置:
平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口(以 s 为单位)之内,就会对这个方法进行服务降级。
异常比例:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0,1.0]。
异常数 :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。
热点参数流控规则是一种更细粒度的流控规则,它允许将规则具体到参数上。还是对方法添加注解,然后通过参数的索引进行限流,只能设置单秒访问量
在规则的编辑界面有参数例外项,可以对某些参数单独放行
根据来源进行限流,流控应用需要自己写,步骤如下:
写一个类,定义区分来源,本质作用是通过request域获取到来源标识,然后交给流控应用位置进行匹配
@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request) {
//为了简单,在参数里传递一下
String serviceName = request.getParameter("serviceName");
//如果拿不到就报错
return serviceName;
}
}
然后在授权规则中的流控应用写serviceName进行限流
前面的规则都是基于资源的,这个是应用维度的
public class ExceptionHandlerPage implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
//BlockException 异常接口,包含Sentinel的五个异常
// FlowException限流异常、DegradeException降级异常、ParamFlowException参数限流异常
// AuthorityException授权异常 、 SystemBlockException 系统负载异常
httpServletResponse.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "接口被降级了...");
}
httpServletResponse.getWriter().write(JSON.toJSONString(data));
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
class ResponseData {
private int code;
private String message;
}
}
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能
通过@SentinelResource来指定出现异常时的处理策略。
例如定义限流和降级后的处理方法:
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource( value = "message",
blockHandler = "blockHandler",//指定发生BlockException时进入的方法
fallback = "fallback"//指定发生Throwable时进入的方法,接收所有异常
)
public String message(String name) {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}return "message";
}
//BlockException时进入的方法
public String blockHandler(String name, BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
//Throwable时进入的方法
public String fallback(String name, Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource( value = "message",
blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class,
blockHandler = "blockHandler",
fallbackClass = OrderServiceImpl3FallbackClass.class,
fallback = "fallback" )
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
}
}
@Slf4j
public class OrderServiceImpl3BlockHandlerClass {
//注意这里必须使用static修饰方法
public static String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
}
@Slf4j public class OrderServiceImpl3FallbackClass {
//注意这里必须使用static修饰方法
public static String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
emmm代码有点多,就是写个配置类,再创建个文件夹就好,就不写在笔记里了
引入sentinel依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
配置fefin对sentinel的支持
feign:
sentinel:
enabled: true
创建容错类
//容错类要求必须实现被容错的接口,并为每个方法实现容错方案,fegin调用出现异常就会进入这个类
@Component
@Slf4j
public class ProductServiceFallBack implements ProductService {
@Override public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-1);
return product;
}
}
为被容器的接口指定容错类
//value用于指定调用nacos下哪个微服务
//fallback用于指定容错类
@FeignClient(value = "service-product", fallback = ProductServiceFallBack.class)
public interface ProductService {
@RequestMapping("/product/{pid}")//指定请求的URI部分
Product findByPid(@PathVariable Integer pid);
}
如果想在容错类中拿到具体的错误也是可以的,指定fallbackFactory参数就好,注意fallback和fallbackFactory只能使用其中一种方式
@FeignClient( value = "service-product",
fallbackFactory = ProductServiceFallBackFactory.class)
public interface ProductService {
@RequestMapping("/product/{pid}")//指定请求的URI部分
Product findByPid(@PathVariable Integer pid);
}
@Component
public class ProductServiceFallBackFactory implements FallbackFactory<ProductService> {
@Override public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product findByPid(Integer pid) {
throwable.printStackTrace();
Product product = new Product();
product.setPid(-1);
return product;
}
};
}
}
创建Spring Boot项目,引入gateway依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
添加配置信息
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由数组r路由就是指当请求满足什么样的条件的时候转发到哪个微服务上
- id: product_route # 当前路由发的标识,要求唯一,默认是UUID
uri: http://localhost:8081 # 请求最终要被转发到的地址
order: 1 # 路由的优先级,数字越小代表路由的优先级越高
predicates: # 断言(条件判断,返回值是boolean转发请求要满足的条件)
- Path=/product-serv/** # 当请求路径满足path指定的规则时,此路由信息才会正常转发
filters: # 过滤器(在请求传递过程中对请求做一些手脚)
- StripPrefix=1 # 在请求转发之前去掉一层路径
发起的请求是http://localhost:7000/product-serv/product/findall,如果不写StripPrefix则转发的是http://localhost:8081/product-serv/product/findall,写了就成了http://localhost:8081/product/findall
启动项目去访问网关地址
引入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
主类添加注解:
@EnableDiscoveryClient
修改配置文件:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 将gateway注册到nacos
gateway:
discovery:
locator:
enabled: true # 让gateway从nacos中获取服务信息
routes:
- id: product_route
uri: lb://service-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
启动项目去访问网关地址
去掉关于路由的配置:
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 将gateway注册到nacos
gateway:
discovery:
locator:
enabled: true # 让gateway从nacos中获取服务信息
访问发现,只要按照网关地址/微服务名/接口
的格式去访问,就可以得到成功响应
基于Datetime的断言工厂
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
基于远程地址(ip段)的断言工厂
-RemoteAddr=192.168.1.1/24
基于Cookie的断言工厂
-Cookie=chocolate, 正则表达式
基于Header的断言工厂
-Header=X-Request-Id, \d+
基于Host的断言工厂
-Host=**.testhost.org
基于Method请求方法的断言工厂
-Method=GET
基于Path请求路径的断言工厂
-Path=/foo/{segment}
基于Query请求参数的断言工厂
-Query=baz, ba.
基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重],然后对于同一个组内的路由按照权重转发
-Weight=group3, 1
server:
port: 7000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 将gateway注册到nacos
gateway:
discovery:
locator:
enabled: true # 让gateway从nacos中获取服务信息
routes:
- id: product_route
uri: lb://service-product
order: 1
predicates:
- Path=/product-serv/**
- Before=2019-11-28T00:00:00.000+08:00 #限制请求时间在2019-11-28之前
- Method=POST #限制请求方式为POST
filters:
- StripPrefix=1
场景: 假设我们的应用仅仅让age在(min,max)之间的人来访问
在配置文件的predicates中,添加一个Age的断言配置:
- Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
自定义一个断言工厂, 实现断言方法
//这是一个自定义的路由断言工厂类,要求有两个
//1、名字必须是 配置+RoutePredicateFactory
//2、必须继承AbstractRoutePredicateFactory<配置类>
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
//构造函数
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//读取配置文件的中参数值 给他赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这个位置的顺序必须跟配置文件中的值的顺序对应
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1、接收前台传入的age参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
//2、先判断是否为空
if (StringUtils.isNotEmpty(ageStr)) {
//3、如果不为空,再进行路由逻辑判断
int age = Integer.parseInt(ageStr);
if (age < config.getMaxAge() && age > config.getMinAge()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
//配置类,用于接收配置文件中的对应参数
@Data
@NoArgsConstructor
public static class Config {
private int minAge;//18
private int maxAge;//60
}
}
测试:
http://localhost:7000/product-serv/product/1?age=30
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#gatewayfilter-factories
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
在配置文件中,添加一个Log的过滤器配置
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=true, false
自定义一个过滤器工厂,实现方法
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//构造函数
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
//过滤器逻辑
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog已经开启了....");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog已经开启了....");
}
return chain.filter(exchange);
}
};
}
//配置类 接收配置参数
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
还有另外一种方式,实现Gateway接口和Orderd接口,什么注解都不加,然后实现里面的方法,最后通过定义配置类的形式进行配置
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由,通常情况下,Spring Cloud的内置局部过滤器已经够我们使用了,我们开发更多的是全局过滤器,统一鉴权啥的
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (!StringUtils.equals(token, "admin")) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高,特殊的是-1大于所有的顺序
@Override
public int getOrder() {
return 0;
}
}
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
域名相同,端口不同:localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
导入依赖:
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
dependency>
编写配置类:
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route") //资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
//自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-serv/product/api1 开头的请求
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("product_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/product-serv/product/api2/demo1 完成的url路径匹配
add(new ApiPathPredicateItem().setPattern("/product-serv/product/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
RequestRateLimter,内部要使用redis,https://www.bilibili.com/video/BV1Pv41187Er?p=11
https://www.bilibili.com/video/BV1Pv41187Er?p=13
所有微服务都添加依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
调用某个服务的接口,控制台打印:
其中5399d5cb061971bd
是TraceId, 5399d5cb061971bd
是SpanId,依次调用有一个全局的TraceId,将调用链路串起来,false
表示是否将最终结果输出到第三方平台。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以将日志聚合,并进行可视化展示和全文检索
下载ZipKin服务端的jar包:
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec
通过命令行,输入下面的命令启动ZipKin Server
java -jar zipkin-server-2.12.9-exec.jar
通过浏览器访问 http://localhost:9411访问
每个微服务导入依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
每个微服务添加配置:
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
访问微服务,并观察zipkin的ui界面
MySQL持久化就创建一下表,然后启动的时候添加一些配置
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql -- MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root - -MYSQL_PASS=root
elasticsearch持久化就先启动elasticsearch,然后启动的时候添加一些配置
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ES- HOST=localhost:9200
目前存在的问题:
配置中心的思路是:
搭建nacos服务端环境:
启动bin目录下的startup即可
引入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
添加配置:
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: localhost:8848 # nacos的服务端地址
namespace: public
discovery:
server-addr: localhost:8848
profiles:
active: dev # 开发环境
配置文件优先级:bootstrap.properties > bootstrap.yml > application.properties > application.yml
在nacos添加统一配置:
Data ID格式:服务名+环境+文件格式
在统一配置中添加如下内容:
config:
appName: product
方式一:自行获取
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@RequestMapping("/test-config1")
public String testConfig1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
}
方式二:注解形式(推荐)
@RestController
@RefreshScope//动态刷新的注解
public class NacosConfigController {
@Value("${config.appName}")
private String appName;
@RequestMapping("/test-config2")
public String testConfig2() {
return appName;
}
}