SpringCloud学习笔记

简介

1、软件架构

1.1、单体软件架构

单体架构在小企业比较常见,典型代表就是一个应用、一个数据库、一个web容器就可以跑起来。

在单体架构中,技术选型非常灵活,优先满足快速上线的要求,也便于快速跟进市场。

  • 在企业发展的初期,为了保证快速上线,采用此种方案较为简单灵活。

  • 传统企业中垂直度较高,访问压力较小的业务。在这种模式下对技术要求较低,方便各层次开发人员接手,也能满足客户需求。

1.2、垂直软件架构(分布式架构)

当企业的业务慢慢的增多,随之也需要应对日常访问中更大的流量,这时候就会对原有的业务进行拆分。其实就是把整个系统拆分成多个业务,把每个业务当成一个子系统。

同时还可以把每个业务子系统都部署到多台服务器上来均衡这个子系统的业务访问量,每台服务器承担一部分,每台服务器上的系统是一样的。这是子系统的水平扩展。(集群)

  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上。(垂直)

  • 集群:同一个业务,部署在多个服务器上。(水平)

1.3、SOA软件架构

如果企业的业务进一步增多,垂直子系统会变的越来越多,系统和系统之间的调用关系呈指数上升的趋势。在这种情况下,企业就应该考虑 SOA架构了。

SOA(Service-Oriented Architecture)代表面向服务(比如部署在 ESB总线上由服务进行管理)的软件架构,将应用程序根据不同的职责划分为不同的模块,不同的模块之间通过特定的协议和接口进行交互。这样使整个系统切分成很多单个组件服务来完成请求。

SOA架构中,会把项目的整个业务系统分解为多个组件,让每个组件都独立提供可复用的服务能力,通过服务的组合和编排来实现上层的业务流程。

从单体架构到服务化架构(SOA),应用数量都在不断的增加,抽离出来的通用功能就成了基础组件(可提供服务),和业务相关的功能就成为业务系统。业务系统可调用其他基础组件提供的服务来完成指定的业务功能。

1.4、微服务软件架构

微服务架构中的每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

微服务会引入较多的单一服务,这就需要涉及到服务调用、服务编排、问题排查、持续集成、自动化测试、配置管理、负载均衡、服务降级等一系列问题的解决和配合方案的整合。

1.4.1、微服务和SOA的区别

微服务其实就是对 SOA 更细致的划分。

2000年 SOA就被提出,各个系统对外提供粗粒度的服务,以供外部系统访问,所有的服务都集中在一个 ESB(Enterprise Service Bus,即企业服务总线) 上,然而这种集成方式开发代价大、通信效率低,且有单点故障的风险,实际上在企业中并没有得到大规模应用。

SOA 中往往是一个完整的子系统,且其中所要完成实现的功能也较多,耦合较大。而微服务中的每个服务只做一件事情,更加单一、直接。

1.4.2、微服务的特点

  • 微服务可独立运行在自己的进程里

  • 每个微服务一般只完成某个特定的功能

  • 多个独立运行的微服务共同构建起了整个系统

  • 微服务之间的通信使用轻量级的通信机制,例如 REST-API或者RPC

  • 微服务是软件架构,和具体实现的编程语言无关

1.5、什么是 RPC

1.5.1、简介

RPC(Remote Procedure Call),是指远程过程调用,是一种进程间通信方式,他是一种技术的思想。

它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式地编码这个远程调用的细节。

RPC 的核心模块就是:通讯、序列化。

2、什么是 SpringCloud

springcloud 是一系列框架的有序集合,将好的服务框架组合起来,并通过 springboot 对其整合的一系列技术进行再封装,屏蔽掉了原本复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

父项目可以既有 dependencies大标签也有 dependencyManagement大标签。

Netflix 的技术:Eureka(2.x版本不开源,1.x版本不更新)、Ribbon(停止更新,已经废除)、Hystrix(停止更新,已经废除)、Zuul(停止更新,已经废除)、

SpringCloud 官方的技术:SpringCloud Loadbalancer、Gateway、

Spring Cloud

Alibaba 的技术:Nacos、Sentinel、dubbo

Spring Cloud Alibaba 参考文档

2.1、微服务中的 CAP原则

CAP原则又称 CAP定理,指的是在一个分布式系统中,存在 Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),三者不可同时保证,最多只能保证其中的两者。

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻都是同样的值(所有的节点无论何时访问都能拿到最新的值)

  • 可用性(A):系统中非故障节点收到的每个请求都必须得到响应(比如我们之前使用的服务降级和熔断,其实就是一种维持可用性的措施,虽然服务返回的是没有什么意义的数据,但是不至于用户的请求会被服务器忽略)

  • 分区容错性(P):一个分布式系统里面,节点之间组成的网络本来应该是连通的,然而可能因为一些故障(比如网络丢包等,这是很难避免的),使得有些节点之间不连通了,整个网络就分成了几块区域,数据就散布在了这些不连通的区域中(这样就可能出现某些被分区节点存放的数据访问失败,我们需要来容忍这些不可靠的情况)

2.1.1、三选二

总的来说,数据存放的节点数越多,分区容忍性就越高,但是要复制更新的次数就越多,一致性就越难保证。同时为了保证一致性,更新所有节点数据所需要的时间就越长,那么可用性就会降低。

  • AC:可用性 + 一致性 要同时保证可用性和一致性,代表着某个节点数据更新之后,需要立即将结果通知给其他节点,并且要尽可能的快,这样才能及时响应保证可用性,这就对网络的稳定性要求非常高,但是实际情况下,网络很容易出现丢包等情况,并不是一个可靠的传输,如果需要避免这种问题,就只能将节点全部放在一起,但是这显然违背了分布式系统的概念,所以对于我们的分布式系统来说,很难接受。

  • CP:一致性 + 分区容错性 为了保证一致性,那么就得将某个节点的最新数据发送给其他节点,并且需要等到所有节点都得到数据才能进行响应,同时有了分区容错性,那么代表我们可以容忍网络的不可靠问题,所以就算网络出现卡顿,那么也必须等待所有节点完成数据同步,才能进行响应,因此就会导致服务在一段时间内完全失效,所以可用性是无法得到保证的。

  • AP:可用性 + 分区容错性 既然 CP 可能会导致一段时间内服务得不到任何响应,那么要保证可用性,就只能放弃节点之间数据的高度统一,也就是说可以在数据不统一的情况下,进行响应,因此就无法保证一致性了。

虽然这样会导致拿不到最新的数据,但是只要数据同步操作在后台继续运行,一定能够在某一时刻完成所有节点数据的同步,那么就能实现最终一致性,所以 AP 实际上是最能接受的一种方案。

比如我们实现的 Eureka集群(P),它使用的就是 AP方案,Eureka 各个节点都是平等的,少数节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka客户端在向某个 Eureka服务端注册时如果发现连接失败,则会自动切换至其他节点。只要有一台Eureka服务器正常运行,那么就能保证服务可用(A),只不过查询到的信息可能不是最新的(C)。

2.2、SpringCloudAlibaba

目前 Spring Cloud Alibaba 提供了如下功能:

  • 服务限流降级:支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Dubbo 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。

  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。

  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。

  • Rpc服务:扩展 Spring Cloud 客户端 RestTemplate 和 OpenFeign,支持调用 Dubbo RPC 服务

  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。

  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。

  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。

  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

3、注册中心

3.1、注册中心简介

常见的注册中心:Eureka(闭源)、Consul(go语言实现)、Zookeeper(服务节点)、nacos(SpringCloudAlibaba 的开源项目)

注册中心,也可以称为服务中心,管理各种服务功能包括服务的注册、发现、熔断、负载、降级等。虽然服务拆分完成,但是没有一个比较合理的管理机制,如果单纯只是这样编写,在部署和维护起来,肯定是很麻烦的。并且相互间的调用是强关联的,如果端口修改会相当麻烦。

springcloud 目前支持 Eureka、Consul、Zookeeper、Nacos 作为微服务架构中的服务中心

解耦,当项目B 的端口发生改变时, 项目B 会修改服务中心中的端口,并且项目A 不关心项目B 的端口,只关心项目B 在服务中心中的服务名称。

3.2、Eureka

记录在独立笔记中。

3.3、Nacos

记录在独立笔记中。

3.4、Consul(未进行整理)

2.2.3、创建 Consul服务端

由 go语言编写实现, Consul有较好的图形化界面。

Eureka是通过代码来创建一个服务中心,而 Consul是通过这个软件来创建服务中心。

Consul 需要额外下载使用,因为是go 语言写的,所以不能通过引入依赖的方式来使用。

解压缩后看到.exe 的界面,在路径中输入cmd ,进入命令界面后输入 consul agent -dev 这样来启动,启动后通过浏览器访问8500 端口就能访问到。

也可以使用Windows 的批处理方式脚本,新建一个txt,然后把它改名成: xxx.bat,Linux中叫 .sh,然后编辑它输入 consul agent -dev

2.2.3.1、pom.xml

  • consul 相关依赖(即客户端,consul 的服务发现者):spring-cloud-starter-consul-discovery

  • web 支持:spring-boot-starter-web

  • actuator 支持:spring-boot-starter-actuator

2.2.3.2、application.yml

server:
  port: 9001
spring:
  application:
    name: consul-producer  # 自定义名称
  cloud:
    consul:
      host: localhost  # 默认localhost
      port: 8500  # 默认8500
      discovery:
        # 注册到服务中的名字
        service-name: producer
        # 对应主机节点使用IP地址来表示
        prefer-ip-address:true

2.2.3.2、DemoApplication

该注解 // @EnableDiscoveryClient 不用写。

3.3.2、客户端注册到 Consul的服务中心

由于 Consul的服务中心是开启软件,所以此时只有一个项目——demo03-springcloud-consul-producer项目。

2.3.2.1、pom.xml

<modelVersion>4.0.0</modelVersion>
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.3.0.RELEASE</version>
  <relativePath /> <!-- lookup parent from repository -->
</parent>
​
<groupId>com.briup</groupId>
<artifactId>demo03-springcloud-consul-producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo03-springcloud-consul-producer</name>
<description>Demo project for Spring Boot</description>
​
<properties>
  <java.version>1.8</java.version>
  <skipTests>true</skipTests>
  <spring-cloud.version>Hoxton.SR5</spring-cloud.version>
</properties>
​
<dependencies>
  <!-- consul相关依赖 -->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  </dependency>
  <!-- 需要web支持 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- consul的健康检查,需要actuator支持 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
</dependencies>
​
<dependencyManagement>
  <dependencies>
    <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>

2.3.2.2、application.yml

server:
  port: 9001
spring:
  application:
    name: consul-producer
  cloud:
    consul:
      # Consul服务器的地址和端口号,默认值就是localhost和8500
      host: localhost
      port: 8500
      discovery:
        # 注册到服务中的名字
        service-name: producer
        # 对应主机节点使用IP来表示
        prefer-ip-address: true

2.3.2.3、DemoApplication

一样。

2.3.2.4、HelloController

一样。

4、远程服务调用

4.1、服务调用简介

springcloud 中常用的两种微服务调用方式,一种是 Ribbon+RestTemplate,另一种是 Feign。

4.2、使用 RestTemplate远程调用

4.2.1、RestTemplate简介和简单使用

它是 spring框架中封装好的一个工具类,可以用于在应用中调用 rest服务,它简化了与 http服务的通信方式,统一了 RESTful的标准,封装了 http链接,我们只需要传入url及返回值类型即可。

  1. 将 RestTemplate对象放到 Spring容器里面去(@Bean),return new RestTemplate();

  2. 使用的时候,自动注入一个 RestTemplate对象,然后通过该对象 .方法 去进行远程访问 通过 SpringWeb内置的轻量级的 Http访问方式——RestTemplate,根据资源地址去远程访问资源。

跨域拦截是要有 ajax请求才会拦截,即前端发送的请求,所以 RestTemplate发送的请求不会被拦截。

使用 RestTemplate的缺点是 完整的 url写死了,耦合较高。

RestTemplate template = new RestTemplate();
User user = template.getForObject("http://localhost:8001/user/" + id, User.class);
​

4.3、Ribbon

记录在独立笔记中。

4.4、Feign(推荐方式)

记录在独立笔记中。

5、负载均衡

5.1、SpringCloud 的负载均衡简介

在 2020年前的 SpringCloud版本是采用 Ribbon 作为负载均衡实现,但是 2020年的版本之后 SpringCloud 把 Ribbon 移除了,进而用自己编写的 LoadBalancer 替代。使用的时候还是继续使用 @LoadBalanced注解。

5.1.1、负载均衡LB(Load Balancer)

Ribbon 属于进程内LB,它只是一个类库,集成在消费方进程,消费方通过它获取到服务提供方的地址。

5.1.1.1、两种方式
  • 集中式LB 在服务的消费方和提供方之间使用独立的 LB设施(可以是硬件,如 F5, 也可以是软件,如 nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方。

  • 进程内LB 将 LB逻辑集成到消费方,消费方从服务中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。

5.1.1.2、LB策略
  • 简单轮询负载均衡(RoundRobin)(默认) 以轮询的方式依次将请求调度不同的服务器,即每次调度执行 i = (i + 1) mod n,并选出第 i台服务器。

即有两台服务器拥有相同 url的资源,当一个消费方反复去访问此资源的时候,会依次访问两台服务器的资源。

  • 随机负载均衡(Random) 随机选择状态为UP的服务主机节点。

  • 加权响应时间负载均衡(WeightedResponseTime) 根据相应时间分配一个 weight,相应时间越长,weight越小,被选中的可能性越低。

  • 区域感知轮询负载均衡(ZoneAvoidanceRule) 复合判断 server 所在区域的性能和 server 的可用性从而来选择 server。

5.1.2、自定义负载均衡策略

LoadBalancer默认提供了两种负载均衡策略:

  • (默认) RoundRobinLoadBalancer:轮询分配策略

  • RandomLoadBalancer:随机分配策略

5.1.2.1、写一个 config配置类

不需要添加 @Configuration注解。

public class LoadBalancerConfig {
  // 将官方提供的 RandomLoadBalancer 注册为 Bean
  @Bean
  public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
  }
}
5.1.2.2、修改 RestTemplate配置类
@Configuration
// 指定为 userservice服务,只要是调用此服务都会使用我们指定的策略。
// spring.application.name 中指定的服务名称
@LoadBalancerClient(value="userservice",configuration=LoadBalancerConfig.class)
public class RestTemplateConfig {
  @Bean
  @LoadBalanced
  RestTemplate restTemplate(){
    return new RestTemplate();
  }
}

5.2、@LoadBalanced注解原理

在添加 @LoadBalanced注解之后,会启用拦截器对我们发起的服务调用请求进行拦截(注意这里是针对我们发起的服务调用请求进行的拦截),叫做 LoadBalancerInterceptor,它实现了 ClientHttpRequestInterceptor接口

5.2.1、LoadBalancerInterceptor类

实现了 intercept方法。

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
  ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}
​
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
  private LoadBalancerClient loadBalancer;
  private LoadBalancerRequestFactory requestFactory;
  // 两个构造方法
  
  public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    // 服务调用请求的地址 URI,如 http://user-service/user
    URI originalUri = request.getURI();
    // 得到服务名称,如 use-service
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    // 调用 execute方法,可以看到两个实现,一个是 cloud 的,另一个是 ribbon 的
    return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
  }
}

5.2.2、BlockingLoadBalancerClient类

5.2.1.1、execute方法
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  // ......
  
  // choose方法,根据负载均衡策略选择一个 eureka服务端
  ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);
  if (serviceInstance == null) {
    supportedLifecycleProcessors.forEach((lifecycle) -> {
        lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));
    });
    // 如果没有发现任何服务端的实例就抛异常
    throw new IllegalStateException("No instances available for " + serviceId);
  } else {
    // 成功选择并获取到非空的 eureka服务器实例之后,就正常通过发起 HTTP请求获取信息
    // 执行 execute方法
    return this.execute(serviceId, serviceInstance, lbRequest);
  }
}
​
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
  DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
  Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);
  Request lbRequest = request instanceof Request ? (Request)request : new DefaultRequest();
  supportedLifecycleProcessors.forEach((lifecycle) -> {
    lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance));
  });
  try {
    T response = request.apply(serviceInstance);
    Object clientResponse = this.getClientResponse(response);
    supportedLifecycleProcessors.forEach((lifecycle) -> {
        lifecycle.onComplete(new CompletionContext(Status.SUCCESS, lbRequest, defaultResponse, clientResponse));
    });
    return response;
  } catch (IOException var9) {
    supportedLifecycleProcessors.forEach((lifecycle) -> {
        lifecycle.onComplete(new CompletionContext(Status.FAILED, var9, lbRequest, defaultResponse));
    });
    throw var9;
  } catch (Exception var10) {
    supportedLifecycleProcessors.forEach((lifecycle) -> {
        lifecycle.onComplete(new CompletionContext(Status.FAILED, var10, lbRequest, defaultResponse));
    });
    ReflectionUtils.rethrowRuntimeException(var10);
    return null;
  }
}
5.2.1.2、choose方法
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
  // ReactiveLoadBalancer接口,即负载均衡策略,它有很多实现类:RoundRobinLoadBalancer轮询
  // 得到策略对应的 loadBalancer
  ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);
  if (loadBalancer == null) {
    return null;
  } else {
    // 调用对应的 choose方法,因为默认的是轮询策略,所以去看轮询的 choose方法
    Response<ServiceInstance> loadBalancerResponse = (Response)Mono.from(loadBalancer.choose(request)).block();
    return loadBalancerResponse == null ? null : (ServiceInstance)loadBalancerResponse.getServer();
  }
}

5.2.3、RoundRobinLoadBalancer类

5.2.3.1、choose方法
public Mono<Response<ServiceInstance>> choose(Request request) {
  ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
  // 获取 eureka服务端实例并遍历,然后调用 processInstanceResponse方法
  return supplier.get(request).next().map((serviceInstances) -> {
      return this.processInstanceResponse(supplier, serviceInstances);
  });
}
5.2.3.2、processInstanceResponse方法
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
  Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
  if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
    // 调用 selectedServiceInstance方法选择一个 eureka服务端
    ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
  }
  // 选择完一个服务端实例后方法返回,回到 execute方法
  return serviceInstanceResponse;
}

6、服务降级和服务熔断

熔断机制的原理,如同电力过载的保护器,它是保证服务高可用的最后一道防线,即使服务不可用了,也可以快速返回一个固定的默认值。

微服务的服务链路中,某微服务出现故障是不可避免的,因为因素太多了,所以我们只能预防,防止故障带来的影响变大。

6.1、雪崩效应

微服务调用链中,某个服务发生故障,进而引起系统整体不可用,这就是所谓的”雪崩效应”。

为解决雪崩效应,出现了断路器模型,和实现了断路器模式的组件:Hystrix、Sentinel、Resilience4j等。就相当于保险丝。

当对特定的不可用的服务的调用达到一个阀值(例如 Hystrix 是 5秒 20次)的时候,断路器将会被打开,会快速返回一个固定的值。即断路器会强迫其以后的多个调用,进行快速的失败并返回一个提前配置好的结果,而不是再继续去访问出故障远程服务器。

熔断器也可以诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

6.2、Hystrix(已过时)

记录在独立笔记中。

6.3、Sentinel

记录在独立笔记中。

7、路由网关

在 springcloud 中,支持使用 zuul 和 gateway 作为微服务中的网关服务组件。

在微服务架构中,通常需要几个基础的组件,其中包括服务注册与发现、服务消费、负载均衡、断路器、智能路由、配置管理等。如果没用 API网关作为统一出口,那么就会暴露接口,可能会被注入攻击,非常地不安全。

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个 API网关,根据用户请求的 url,将其路由到相应的服务端点。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙可以进行微服务访问的权限控制,以及将请求均衡分发给后台服务端。

常见的模型就是最外层是 nginx集群,然后是 gateway集群,然后是微服务。

7.1、gateway

SpringCloud Gateway是基于 WebFlux框架实现的,而 WebFlux框架底层则使用了高

性能的 Reactor模式通信框架 Netty。不仅提供统一的路由方式,并且基于 Filter链的方式提供了服务网关的各种功能。

7.1.1、简单使用 gateway

新建一个子项目模块,分布式项目中职责划分明确。

7.1.1.1、pom.xml

注意不能添加 web依赖,因为 gateway 需要内置的 spring-webflux 支持,相互不兼容的。

springboot 的 web依赖内置了 tomcat,并且使用的是 springmvc 响应的。

并且需要引入某个注册中心的依赖,因为服务网关也是要将自己注册到注册中心的。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
7.1.1.2、application.yml

uri 中前面加 lb:// 就能实现负载均衡,predicates 就是路由断言,符合条件的路由会被代理。

server:
  # 服务端口,在 80 方便与前端做对接,当然也可以使用别的端口
  port: 80
spring:
  application:
    # 服务名
    name: service-gateway
  cloud:
    nacos:
      discovery:
        # nacos服务地址,将自己注册到注册中心
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 使用服务发现路由
          enabled: true
      routes:
          # 设置路由id,就是服务名称 serviceId
        - id: service-hospital
          # 设置路由的 uri,lb 就是 LoadBalance 的意思
          uri: lb://service-hospital
          # 设置路由断言,代理 servicerId为 auth-service 的 /auth/路径
          predicates:
            - Path=/*/hospital/**
        - id: service-cmn
          uri: lb://service-cmn
          predicates:
            - Path=/*/cmn/**
7.1.1.3、代码配置取代 yml配置文件
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
  // 代码方式配置路由规则
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("api-test", r -> r.path("/api/test/**")
      .filters(f->f.stripPrefix(2))
      .uri("lb://producer"))
      .build();
  }
}

7.1.2、Predicate断言

断言工厂官网:Spring Cloud Gateway

断言的意义就是,满足什么条件,才会进行反向代理。

Predicate(断言、谓词)即:断定型接口,接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断数据是否有变化等操作。

RoutePredicateFactory接口,就是具体的 Predicates工厂类需要去实现的指定接口,该工厂可以生产出相应的 Predicates接口的实例,来进行具体条件的验证判断,从而进行路由规则的匹配。

谓词Predicate工厂功能
AfterAfterRoutePredicateFactory请求时间满足在配置时间之后
BeforeBeforeRoutePredicateFactory请求时间满足在配置时间之前
BetweenBetweenRoutePredicateFactory请求时间满足在配置时间之间
CookieCookieRoutePredicateFactory请求匹配Cookie正则匹配指定值
HeaderHeaderRoutePredicateFactory请求匹配Header正则匹配指定值
HostHostRoutePredicateFactory请求Host匹配指定值
MethodMethodRoutePredicateFactory请求Method匹配指定方式
PathPathRoutePredicateFactory请求路径正则匹配指定值
QueryQueryRoutePredicateFactory请求查询参数正则匹配指定值
RemoteAddrRemoteAddrRoutePredicateFactory请求远程地址匹配指定值
WeightWeightRoutePredicateFactory根据指定权重进行匹配
7.1.2.1、请求参数匹配

QueryRoutePredicateFactory 支持传入俩个参数,一个是属性名一个为属性值,属性值可以是正则表达式,也可以只指定属性名,不指定属性值。

spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: gateway
  cloud:
    gateway:
      # 配置路由(通常由 一个ID、一个URI、一组predicate、一组filter 而组成)
      routes:
      - id: test1
        uri: lb://producer
        predicates:
          - Query=name,briup

即请求中的查询参数包含 name=briup 的时候才会做反向代理, 也可以写成 Query=name 表示只要包含 name 参数,就转换给 producer服务。

7.1.2.2、请求路径匹配

PathRoutePredicateFactory 支持传入一个参数,用来配置指定的路径,该参数是一个 List,可以英文逗号隔开表示多种不同的路径。

spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: gateway
  cloud:
    gateway:
      # 配置路由(通常由 一个ID、一个URI、一组predicate、一组filter 而组成)
      routes:
      - id: test2
        uri: lb://producer
        predicates:
          - Path=/hello,/cookie

只允许 /hello 和 /cookie 做反向代理。

7.1.2.2、header匹配

HeaderRoutePredicateFactory 支持传入俩 个参数,一个 header 中属性名称和一个属性值,属性值可以不写,同时属性值可以使用正则表达式。

表示请求的 header 中必须包含 token字段,并且值必须由字母、数字、下划线组成,且只能是5个字符。

predicates:
  - Header=token,\w{5}
7.1.2.4、cookie匹配

CookieRoutePredicateFactory支持传入俩 个参数,一个 是cookie的name,另一个是对应的value值,value可以不写,同时value可以使用正则表达式。

如果请求中,没有携带cookie或者cookie中name不等于briup,那么访问将会报错。

predicates:
  - Cookie=name,briup
7.1.2.5、Host匹配

HostRoutePredicateFactory 支持传入一个参数,用来配置指定的 Host,该参数是一个 List,可以英文逗号隔开表示多种不同的路径。同时还支持 ant风格的通配。

predicates:
  - Host=**.briup.com,**.baidu.com
7.1.2.6、ip地址匹配

RemoteAddrRoutePredicateFactory 支持传入一个参数,用来配置指定的请求方的 ip地址,该参数是一个 List,可以英文逗号隔开表示多个 ip地址。同时也可以选择同时指定子网掩码。

表示如果客户端的ip在指定的ip列表中,就把请求转发给producer服务。

predicates:
  - RemoteAddr=127.0.0.1,192.168.0.110/24
7.1.2.6、请求方式匹配

MethodRoutePredicateFactory 支持传入一个参数,用来配置指定的请求方式(GET POST等),该参数是一个 List,可以英文逗号隔开表示支持多种请求方式。

predicates:
  - Method=GET,POST,DELETE
7.1.2.7、组合匹配
predicates:
  - Path=/briup/**
  - Method=GET
  - Query=name
  - Header=token,\d{5}
  - Cookie=flag,1
  - Host=**.briup.com
  - RemoteAddr=127.0.0.1
filters:
  - StripPrefix=1
7.1.2.8、时间匹配

BeforeRoutePredicateFactory,AfterRoutePredicateFactory,BetweenRoutePredicateFactory 分别可以判断当前请求时间是否在指定时间之前、之后、之间。

Before、After条件需要传一个 UTC时间格式的时间参数,Between条件需要传俩个 UTC时间格式的时间参数。

协调世界时,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称 UTC。

协调世界时是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。

中国在东八区,时间与 UTC 的时差均为 +8,也就是 UTC+8。

UTC + 时区差=本地时间

JDK8 中,专门引入一个新的时间类型 java.time.ZonedDateTime ,专门用来表示这个 UTC时间。

predicates:
  - Before=2020-06-17T10:20:05.727+08:00[Asia/Shanghai]
  - After=2020-06-16T10:20:05.727+08:00[Asia/Shanghai]
  - Between=2020-06-16T10:20:05.727+08:00[Asia/Shanghai],2020-06-17T10:20:05.727+08:00[Asia/Shanghai]
7.1.2.9、访问权重匹配

WeightRoutePredicateFactory,可以根据配置的访问权重进行匹配转发。

不是谁权重高就一定使用谁进行路由,而是权重越高,概率越大。

当前请求的参数是 name=tom 的时候,test10_1 和 test10_2 这俩个路由都匹配,将来满足要求的请求,大致上 10个请求中会有 1个给转发给 producer1服务,9个请求转发给 producer2服务

spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: gateway
  cloud:
    gateway:
      # 配置路由(通常由 一个ID、一个URI、一组predicate、一组filter 而组成)
      routes:
      - id: test10_1
        uri: lb://producer1
        predicates:
          - Query=name,tom
          - Weight=mygroup,1
      - id: test10_2
        uri: lb://producer2
        predicates:
          - Query=name,tom
          - Weight=mygroup,9

7.1.3、Filter路由过滤器

路由过滤器工厂官网:Spring Cloud Gateway

由过滤器支持以某种方式修改传入的 HTTP请求或传出的 HTTP响应,路由过滤器的范围是某一个路由,跟之前的断言一样。利用过滤器,可以实现增加请求头、增加请求参数 、增加响应头和断增加路器等功能。

Spring Cloud Gateway 中的 Filter 的生命周期只有两个:“pre” 和“post”。

  • PRE:在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  • POST:在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等 Spring Cloud Gateway 中的 Filter,从作用范围上还可以分为俩种:GatewayFilter 与 GlobalFilter。

  • GatewayFilter:应用到单个路由或者一组路由上

  • GlobalFilter:全局过滤器,不需要在配置文件中配置,应用到所有的路由上

7.1.3.1、AddRequestHeader添加请求头

访问对应的路由时,会自动添加请求头到请求中。

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: book-service
          uri: lb://bookservice
          predicates:
            - Path=/book/**
          # 添加过滤器
          filters:
            - AddRequestHeader=TestHeader, HelloWorld!
7.1.3.2、全局过滤器

内置的全局过滤器可以满足大多数的需求,如果业务中有相应的功能要求,也可以自定义全局过滤器。

自定义全局过滤器只需要 implements GlobalFilter,Ordered。

过滤器的执行顺序若不指定返回的 Order值,那么是配置类的全局过滤器先执行,配置文件中的过滤器(Order值为 1)后执行。

例如自定义一个全局过滤器,验证请求中是否携带 token,没有就返回 401

@Configuration
public class GatewayConfig {
  public static class TokenGlobalFilter implements GlobalFilter,Ordered {
    private Logger logger=LoggerFactory.getLogger(TokenGlobalFilter.class);
    
    //order值越小,优先级越高
    @Override
    public int getOrder() {
      return -1000;
    }
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      String token=exchange.getRequest().getQueryParams().getFirst("token");
      //如果请求中没有携带token参数,那么直接返回401
      //也可以使用 StringUtils.hasText(token),不用写这么多
      if (token == null || token.isEmpty()) {
        logger.info( "token 不能为空" );
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        // 不再向下传递,返回响应
        return exchange.getResponse().setComplete();
      }
      // 将 ServerWebExchange 向过滤链的下一级传递
      return chain.filter(exchange);
    }
  }
  
  @Bean
  public TokenGlobalFilter tokenGlobalFilter(){
    return new TokenGlobalFilter();
  }
}
7.1.3.3、Ratelimiter限流

RequestRateLimiterGatewayFilterFactory,限流。nginx 也可以做限流,漏桶算法。

缓存、降级和限流是解决高并发系统中,解决常见问题的重要手段。

  • 缓存,可以提升系统访问速度。

  • 降级,可以在服务出现问题时,暂时屏蔽该服务,并快速给出默认响应,问题解决后恢复服务访问。

  • 限流,可以对一个时间窗口内的请求进行限速来保护系统 一般常见的限流有:限制总并发数、限制瞬时并发数、限制时间窗口内的平均速率、限制远程接口的调用速率、限制MQ的消费速率。也可以是根据网络连接数、网络流量、CPU 或内存负载等来限流。

  • 令牌桶算法:

  1. 假设存在一个桶,用来存放固定数量的令牌(例如设置最多 200个)

  2. 该算法还可以用一定的速率往桶中存放令牌,并且是持续不断的往桶里存放令牌(例如每秒存放 5个进去)

  3. 桶中令牌数达到上限的时候,就丢弃令牌(保证最多 200个)

  4. 每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择就等待或者拒绝访问

  5. 算法初始化完成后,桶中已经存在了最大数量的令牌(例如 200个)

  6. 桶中没有令牌的时候,请求等待,最后以一定的速率进行处理请求(和每秒存的令牌数量一致)

  7. 假如网关中设置的令牌桶最大 100,每秒添加令牌的速率是 5,所以 1秒内可以处理的最大请求为 105个

  • burstCapacity,令牌桶总容量,

  • replenishRate,令牌桶每秒填充平均速率,

  • key-resolver,用于限流的键的解析器的 Bean 对象的名字,使用 SpEL 表达式根据 #{@beanName} 从 Spring 容器中获取 Bean 对象。

server:
  port: 9999
spring:
  cloud:
    gateway:
      # 配置路由(通常由 一个ID、一个URI、一组predicate、一组filter 而组成)
      routes:
      - id: filter1
        uri: lb://producer
        predicates:
          - Method=GET
        filters:
          - name: RequestRateLimiter
            args:
              key-resolver: '#{@myKeyResolver}'
              redis-rate-limiter.replenishRate: 5
              redis-rate-limiter.burstCapacity: 100

这里是使用了静态内部类,作为 KeyResolver接口的实现类。

@Configuration
public class GatewayConfig {
  public static class MyKeyResolver implements KeyResolver{
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
      //获取请求用户ip作为限流key
      return
        Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
        //获取请求地址的uri作为限流key
        // return Mono.just(exchange.getRequest().getURI().getPath());
        //获取请求中携带的用户id作为限流key
        // return
        Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    }
  }
  @Bean
  public MyKeyResolver myKeyResolver() {
    return new MyKeyResolver();
  }
}

2.6.1、zuul(已过时)(未进行整理)

服务提供方提供资源,我们可以通过 url直接访问到,但是现在希望该资源对外隐藏,只对外暴露网关的 url。

2.6.1.1、消费方实现

我们这里网关在消费方集成了,但是网关是可以独立运行的。

eureka-server、eureka-producer不变,改变 eureka-consumer,我们作为用户去访问消费方的网关暴露地址。

2.6.1.1.1、pom.xml

springboot、springcloud、eureka-client、web支持、netflix-zuul依赖支持

2.6.1.1.2、application.yml

api-test是一个路由规则的名字,随便起就可以,但原则上需要有一定的意义,

当用户访问:http://127.0.0.1:9999/api/test/hello?name=zs,会把请求转发给 producer服务的:http://127.0.0.1:9001/hello?name=zs 或者 http://127.0.0.1:9002/hello?name=zs(因为 producer的 hello服务有两个,网关就会做一个负载均衡)

网关会对路由默认做一个负载均衡。

server:
  port: 9999
spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: zuul
eureka:
  instance:
    # 设置当前微服务在服务中心中的节点名称为 ip:port
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      # 服务中心的地址
      defaultZone: http://localhost:8001/eureka/
zuul:
  # 配置路由规则
  routes:
    # 指定一个路由规则的名字,可以配置多个
    api-test:
      # 当用户访问这个模式的路径的时候,就把请求转发给producer服务地址
      path: /api/test/**
      service-id: producer
2.6.1.1.3、DemoApplication

@EnableDiscoveryClient开启服务发现功能,@EnableZuulProxy开启网关功能

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
  SpringApplication.run(DemoApplication.class, args);}}

2.6.1.2、默认路由规则(*)

如果没在配置文件配置路由规则,zuul其实也会进行自动的默认配置。

zuul会自动代理所有注册到 Eureka Server的微服务,并且默认的路由规则如为:http://zuul-server-ip:port/serviceId/**

即:访问 http://127.0.0.1:9999/producer/hello?name=zs,会把请求转发给 producer服务的:http://127.0.0.1:9001/hello?name=zs 或者 http://127.0.0.1:9002/hello?name=zs

如果有一些微服务并不希望对外暴露,那么可以指定不自动生成路由规则:

zuul:
  ignored-services:
  - producer
  - other-server

2.6.1.3、请求头部过滤

zuul内部的过滤器对 请求/响应 进行路由的时候,会把一些默认设置好的敏感的头信息给过滤掉,最终导致请求中通过头信息来携带的数据,无法传递给最终调用的微服务。

比如 cookie就是敏感信息,当需要使用到 cookie的时候,可能 cookie就会被过滤掉。

zuul默认过滤掉的头信息在 org.springframework.cloud.netflix.zuul.filters.ZuulProperties中进行的默认设置。

新增了 sensitive-headers属性的配置,意思是设置需要忽略的敏感头字段,但是这里设置为

空,表示没有需要忽略的头字段,即所有的头字段全都保留下来

zuul:
  # 配置路由规则
  routes:
    # 指定一个路由规则的名字,可以配置多个
    api-test:
      # 当用户访问这个模式的路径的时候,就把请求转发给producer服务地址
      path: /api/test/**
      service-id: producer
  ignored-services:
  # 配置指定的微服务,以便对其忽略默认的路由规则
  - other-server
  
  # 配置敏感头字段列表为空,这样就不会默认忽略cookie等字段了
  sensitive-headers:

2.6.1.4、请求的重定向

客户端访问 API网关,然后把请求转发给指定的微服务,如果这时候需要进行重定向操作,默认情况下,会暴露出目标微服务的实际地址。

原因是因为zuul在把请求进行路由的时候,没有正确的处理好HTTP请求头信息中的Host属性导致的,所以需要修改 yml中 zuul的配置

zuul:
  # 配置路由规则
  routes:
    # 指定一个路由规则的名字,可以配置多个
    api-test:
      # 当用户访问这个模式的路径的时候,就把请求转发给producer服务地址
      path: /api/test/**
      service-id: producer
  ignored-services:
  # 配置指定的微服务,以便对其忽略默认的路由规则
  - other-server
  
  # 配置敏感头字段列表为空,这样就不会默认忽略cookie等字段了
  sensitive-headers:
  
  # 在转发请求的时候,请求头host字段中,添加网关的地址信息
  add-host-header: true

即第一次访问 127.0.0.1:9999/api/test/redirect的时候,这个 controller做了一个重定向,然后这个重定向地址未经过反向代理,当你开启请求头 host字段中添加网关地址信息后,就会把这个重定向的地址也做一个反向代理,

所以没开启的时候的重定向地址是 response.sendRedirect("/cookie"); 就可访问到指定地址,但是会暴露地址,

但是开启后,因为又做了一次反向代理,所以重定向地址需要加上路由规则才能访问成功,

即 response.sendRedirect("/api/test/cookie");

2.6.1.5、本地映射跳转

在配置的zuul项目中,也可以编写自己的Controller,所以zuul不仅可以把请求转发给具体的微服务,它同样可以把请求交给自己的Controller来处理。更改 yml中 zuul的配置

zuul:
  # 配置路由规则
  routes:
    # 指定一个路由规则的名字,可以配置多个
    api-test:
      # 当用户访问这个模式的路径的时候,就把请求转发给producer服务地址
      path: /api/test/**
      service-id: producer
    # 本地跳转映射
    local:
      path: /api/local/**
      url: forward:/
  ignored-services:
  # 配置指定的微服务,以便对其忽略默认的路由规则
  - other-server
  
  # 配置敏感头字段列表为空,这样就不会默认忽略cookie等字段了
  sensitive-headers:
  
  # 在转发请求的时候,请求头host字段中,添加网关的地址信息
  add-host-header: true

2.6.1.6、网关的过滤器(*)

过滤器(Filter)是 zuul的核心,用来实现对服务的控制。

Filter的生命周期有4个,分别是:“PRE”(前置)、“ROUTING”(路由中)、“POST”(后置)、“ERROR”(报错)。

Servlet的 Filter只有一个:doFilter,拦截器 Interceptor 有三个:pre、post、渲染页面后

zuul的大部分功能都是通过Filter来实现的,这些不同类型的Filter分别对应于生命周期的一个阶段:

  1. PRE-Filter 这种Filter在请求被路由之前调用。它可以实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  2. ROUTE-Filter 这种Filter将请求路由到微服务。它可以构建、设置发送给微服务的请求。

  3. POST-Filter 这种Filter在路由到微服务以后执行。它可以为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

  4. ERROR-Filter 这种Filter在其他阶段发生错误时执行。它可以在发送错误之后添加一些操作,例如日志记录等。

zuul里面默认实现的核心Filter(order值在 FilterConstants类中,值越小,优先级越高):

类型顺序过滤器作用
pre-3ServletDetectionFilter标记处理servlet类型
pre-2Servlet30WrapperFilter包装 request请求
pre-1FormBodyWrapperFilter包装请求体
route1DebugFilter标记调试点
route5PreDecorationFilter处理请求上下文
route10RibbonRoutingFilter请求转发 ServiceID
route100SendHostRoutingFilter请求转发 URL
route500SendForwardFilter请求转发 forward
post0SendErrorFilter处理有错误的响应
post1000SendResponseFilter处理正常的响应

2.6.1.7、自定义过滤器

在zuul中,自定义过滤器,需要实现指定接口,实现俩个抽象方法,也可以继承指的父类型,重写四个抽象方法。

@Component
public class TokenFilter extends ZuulFilter{
  // 是否可以执行该过滤器,true表示可以
  @Override
  public boolean shouldFilter() {
    return true;
  }
  
  // 过滤器的类型 pre route post error
  @Override
  public String filterType() {
    return FilterConstants.PRE_TYPE;
  }
​
  // filter执行顺序,数字越小,优先级越高
  @Override
  public int filterOrder() {
    return 0;
  }
  
  // 过滤时候执行的方法
  // 当前实现中可以忽略返回值
  @Override
  public Object run() throws ZuulException {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();
    String token = request.getParameter("token");
    
    ctx.getResponse().setCharacterEncoding("UTF-8");
    ctx.getResponse().setContentType("text/html;charset=utf-8");
    if (StringUtils.isNotBlank(token)) {
      ctx.setSendZuulResponse(true); //表示通过,可以对该请求进行路由
      ctx.setResponseStatusCode(200);
    } else {
      ctx.setSendZuulResponse(false); //表示不通过,不对其进行路由
      ctx.setResponseStatusCode(400);
      ctx.setResponseBody("token 不能为空");
    }
    return null;
  }
}

2.6.1.8、网关路由熔断

一般用不上配置网关的熔断,做一个降级,指定一个 fallback方法,需要实现zuul里面指定的FallbackProvider接口,并实现其中的抽象方法。

@Component
public class ProducerFallback implements FallbackProvider{
  //指定要针对的是哪一个微服务(serviceID)。可以使用*通配
  @Override
  public String getRoute() {
    return "producer";
  }
  
  //指定的微服务出现问题时,返回什么默认值
  //需要返回ClientHttpResponse类型对象,可以使用内部类或Lamda表达式完成
  @Override
  public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    return new ClientHttpResponse() {
      @Override
      public InputStream getBody() throws IOException {
        String res = "当前服务不可用,请稍后再试";
        return new ByteArrayInputStream(res.getBytes());
      }
      @Override
      public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.TEXT_PLAIN);
        return headers;
      }
      @Override
      public HttpStatus getStatusCode() throws IOException {
        return HttpStatus.OK;
      }
      @Override
      public int getRawStatusCode() throws IOException {
        return HttpStatus.OK.value();
      }
      @Override
      public String getStatusText() throws IOException {
        return HttpStatus.OK.getReasonPhrase();
      }
      @Override
      public void close() {
      }
    };
  }
}

2.6.1.9、配置请求重试

因为网络或者其它原因,服务可能会暂时的不可用,这时,通常会希望可以再次对服务进行重试调用。即网关做一个自动重试。

需要结合spring-retry 组件一起来用。

2.6.1.9.1、pom.xml

spring-cloud-starter-netflix-eureka-client、spring-boot-starter-web、spring-cloud-starter-netflix-zuul、spring-retry

2.6.1.9.2、application.yml

新增了属性 zuul.retryable=true,开启 zuul的全局重试功能,也可以在指定路由规则中局部开启;

新增了 ribbon的相关配置,因为 zuul中也是要通过 Ribbon(负载均衡客户端)去调用其他服务的。

ribbon的这些属性配置后,会有警告,显示未知的属性,这是 STS中没有识别的原因,相关属性可以在 com.netflix.client.config.CommonClientConfigKey 中找到。

server:
  port: 9999
spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: zuul-pro
eureka:
  instance:
    # 设置当前微服务在服务中心中的节点名称为 ip:port
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
  service-url:
    # 服务中心的地址
    defaultZone: http://localhost:8001/eureka/
zuul:
  # 配置路由规则
  routes:
    # 指定一个路由规则的名字,可以配置多个
    api-test:
      # 当用户访问这个模式的路径的时候,就把请求转发给producer服务地址
      path: /api/test/**
      service-id: producer
    # 本地跳转映射
    local:
      path: /api/local/**
      url: forward:/
  ignored-services:
  # 配置指定的微服务,以便对其忽略默认的路由规则
  - other-server
  
  # 配置敏感头信息字段列表为空,这样就不会默认忽略cookie等字段了
  sensitive-headers:
  
  # 在转发请求的时候,请求头host字段中,添加网关的地址信息
  add-host-header: true
  
  # 开启retry功能,默认值是false
  retryable: true
  
# 可配置的属性在com.netflix.client.config.CommonClientConfigKey中记录着
ribbon:
    # 同一台实例最大重试次数,不包括首次调用
    MaxAutoRetries: 2
    # 重试负载均衡其他的实例最大重试次数,不包括首次server
    MaxAutoRetriesNextServer: 0
    # 请求连接的超时时间
    ConnectTimeout: 2000
    # 请求处理的超时时间
    ReadTimeout: 3000
2.6.1.9.3、DemoApplication
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);}}

2.6.1.10、高可用的网关

为了保证Zuul的高可用性,在具体实施的过程中,可以同时启动多个API网关实例进行负载,并且在 API网关的前方,使用Nginx(软件)或者F5(硬件)进行负载转发以达到高可用性。

即:nginx集群 -> 网关集群 -> 很多微服务,服务中心,相互调用

8、配置中心

在没有配置中心之前,各个微服务项目中的重要配置都由自己管理,项目启动的时候读取项目中配置信息即可,但是不易于修改和共享。

有了配置中心(config server)之后,每一个微服务项目都可以相当于一个客户端(config client),可以从配置中心获取自己项目中需要的配置信息。

在 spring-cloud-config中,分两个角色,一是 config server,二是 config client。配置信息可以存放在远程仓库(GIT或者SVN)或者本地文件中,然后由配置中心(config server)统一的去读取,最后每一个微服务项目(config client)通过配置中心(config server)获得自己需要的配置信息。

支持 git(github,gitee)、redis、JDBC 等等。

8.1、git 仓库

8.1.1、配置中心服务端

  • {配置文件名称}-{环境}.yml

  • {配置文件名称}-{环境}.properties 在远程仓库中,配置文件的配置文件名称可以任意,因为 bootstrap、application 这些是对于项目本地的配置文件而言的。

别的项目访问的时候就通过 http://localhost:配置中心的端口/配置文件名称/环境/Git分支,或者通过 http://localhost:配置中心的端口/Git分支/配置文件名称-环境.yml

8.1.1.1、pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
​
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
8.1.1.2、application.yml
  • 如果是本地的 git仓库,那么 uri: file://${user.home}/Desktop/config-repo。

  • 如果是本地的磁盘,那么 config 下的所有都去掉,只需要加上 config.native.search-locations: D:/config-repo

server:
  port: 8700
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          # 仓库的地址
          uri: https://github.com/briup-wood-github/spring-cloud-config
          # 指定分支,如果不指定就使用默认的分支,默认就是 master
          default-label: master
          search-paths:
            # 指定仓库中的目录
            - config-repo
          # 如果仓库为公开仓库,可以不填写用户名和密码,否则必须正确填写
          # username: CHWQiuBai
          # password: 123456
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka, http://localhost:8002/eureka
  instance:
    # 设置当前微服务在服务中心中的节点名称为 ip:port
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
8.1.1.3、启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
  public static void main(String[] args) {
    SpringApplication.run(ConfigApplication.class, args);
  }
}

8.1.2、配置中心客户端

删除本地的 application.yml(也可以不删除,那么最后本地配置和远端配置都会被加载),然后添加 bootstrap.yml,因为 bootstrap.yml 可以先于 application.yml 被加载。

8.1.2.1、pom.xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
​
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
​
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
8.1.2.2、bootstrap.yml
spring:
  application:
    # 注册到服务中心后,当前微服务的名字(serviceID)
    name: config-client
  cloud:
    config:
      discovery:
        # 通过配置中心的服务名称来访问
        enabled: true
        # 指定配置中心的服务名称
        service-id: config-server
      # 配置中心服务器的地址,如果已经使用 discovery 发现了,就不需要指定了
      # uri: http://localhost:8700
      # 假设远端的配置文件的名称为 wood-dev.yml
      # 配置文件名称,就相当于 application 这一部分的名字
      name: wood
      # 环境
      profile: dev
      # 分支
      label: master
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka, http://localhost:8002/eureka
  instance:
    # 设置当前微服务在服务中心中的节点名称为 ip:port
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
8.  2p3efer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      # 服务中心的地址
      defaultZone: http://localhost:8001/eureka/

####

2.7.1.2.4、D
  e
mo8licat4on
@EnableDiscoveryClient
@SpringBootApplication
public class DemoAp plication {
  public static void main(String[] args) {
    
  SingApplication.run(DemoAlication.class, args);}}
2.1.2.5、HelloControlle@RestC:onfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);}}

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值