【微服务/分布式】Spring Cloud学习笔记

371 阅读24分钟

本文记录笔者学习Spring Cloud脚印,仅供参考

www.bilibili.com/video/BV1f9…

零、预备知识

0.微服务

0.0.简介

  • 将一个大的应用,拆分成多个小的模块,每个模块都有自己的功能和职责,每个模块可以 进行交互,这就是微服务
  • 微服务架构的系统是个分布式系统,按业务领域划分为独立的服务单元,有自动化运维、容错、快速演进的特点,它能够解决传统单体架构系统的痛点,同时也能满足越来越复杂的业务需求。
  • 简而言之,微服务架构的风格,就是将单一程序开发成一个微服务, 每个微服务运行在自己的进程中,并使用轻量级通信机制,通常是 HTTP RESTFUL API 。这些服务围绕业务能力来划分构建的,并通 过完全自动化部署机制来独立部署这些服务可以使用不同的编程语 言,以及不同数据存储技术,以保证最低限度的集中式管理

0.1.特点

  • 按业务(功能)划分为一个独立运行的程序,即服务单元。
  • 服务之间通过 HTTP 协议相互通信。 http 是一个万能的协议 (web 应用都支持的模式)
  • 自动化部署。
  • 可以用不同的编程语言。
  • 可以用不同的存储技术。
  • 服务集中化管理。
  • 微服务是一个分布式系统。

0.2.优缺点

优点:

  • 将一个复杂的业务分解成若干小的业务,每个业务拆分成一个服务,服务的边界明确, 将复杂的问题简单化。
  • 微服务系统的微服务单元,具 有很强的横向扩展能力
  • 服务与服务之问通过 HTTP 网络通信协议来通信,单个微服务内部高度祸合,服务与服 务之间完全独立,无调合
  • 如果是一个单体的应用,由于业务的复杂性、代码的祸合性,以及可能存在的历史问题
  • 微服务的每个服务单元都是独立部署的,即独立运行在某个进程里。微服务的修改和部 署对其他服务没有影响
  • 微服务在 CAP 理论中采用的是 AP 架构,即具有高可用和分区容错的特点。高可用主 要体现在系统 7 x 24 小时不间断的服务,它要求系统有大量的服务器集群,从而提高了系统 的负载能力。另外,分区容错也使得系统更加健壮。

缺点:

  • 微服务的复杂度
  • 分布式事务问题
  • 服务的划分(按照功能划分 还是按照组件来划分呢) 分工
  • 服务的部署(不用自动化部署 自动化部署)

1.分布式架构

1.0.简介

  • 分布式系统是集群部署的,由很多计算机相互协作共同构成,它能够处理海量的用户请求。当 分布式系统对外提供服务时,用户是毫不知情的,还以为是一台服务器在提供服务。分布式系 统的复杂任务通过计算机之间的相互协作来完成,当然简单的任务也可以在一台计算机上完 成。分布式系统通过网络协议来通信,所以分布式系统在空间上没有任何限制,即分布式服务 器可以部署不同的机房和不同的地区。微服务架构是分布式架构
  • 另外,分布式系统的应用都是集群化部署,会给数据一致性带来困难。

2.服务雪崩

  • 在分布式系统中,服务之间 相互依赖,如果一个服务出现了故障或者是网络延迟,在高并发的情况下,会导致线程阻塞, 在很短的时间内该服务的线程资源会消耗殆尽,最终使得该服务不可用。由于服务的相互依赖, 可能会导致整个系统的不可用,这就是“雪崩效应”。

3.熔断机制

  • 为了防止“雪崩效应”事件的发生,分布式系统采用了熔断机制。
  • 例如在微 服务系统中,有 a、 b、 c、 d、 e、 如果此时服务 b 出现故障或者网络延迟,服务 b 会出现大量的线程阻塞,有可能在很短的时 间内线程资源就被消耗完了,导致服务 b 的不可用。如果服务 b 为较 底层的服务,会影响 到其他服务,导致其他服务会一直等待服务 b 的处理。如果服务 b 迟迟不处理,大量的网络 请求不仅仅堆积在服务 b,而且会堆积到依赖于服务 b 的其他服务。而因服务 b 出现故障影 响的服务,也会影响到依赖于因服务 b 出现故障影响的服务的其他服务,从而由 b 开始,影 响到整个系统,导致整个系统的不可用。
  • 为了解决这一难题,微服务架构引入了熔断机制。当服务 b 出现故障,请求失败次数超过设 定的阀值之后,服务 b 就会开启熔断器,之后服务 b 不进行任何的业务逻辑操作,执行快速失败,直接返回请求失败的信息。其他依赖于 b 的服务就不会因为得不到响应而线程阻塞, 这时除了服务 b 和依赖于服务 b 的部分功能不可用外,其他功能正常。

4.Spring Cloud

SpringCloud 就是微服务理念的一种具体落地实现方式,帮助微服务架构提供了必备的功能。

4.0.简介

  • Spring Cloud 作为 Java 语言的微服务框架,它依赖于 Spring Boot,有快速开发、持续 交付和容易部署等特点。 Spring Cloud 的组件非常多,涉及微服务的方方面面
  • Spring Cloud 在开发部署上继承了 Spring Boot 的一些优点,提高其在开发和部署上的效 率。 Spring Cloud 的首要目标就是通过提供一系列开发组件和框架,帮助开发者迅速搭建 一个分布式的微服务系统。 Spring Cloud 是通过包装其他技术框架来实现的,例如包装开 源的 Netflix oss 组件,实现了一套通过基于注解、 Java 配置和基于模版开发的微服务框 架。 Spring Cloud 提供了开发分布式微服务系统的一些常用组件,例如服务注册和发现、 配置中心、熔断器、远程调用,智能路由、微代理、控制总线、全局锁、分布式会话等

4.1.SpringCloud 版本对应关系【开发重点】

4.2. SpringCloud 常用组件表 (管家)

  • 服务的注册和发现。(eureka,nacos,consul)
  • 服务的负载均衡。(ribbon,dubbo)
  • 服务的相互调用。(openFeign,dubbo)
  • 服务的容错。(hystrix,sentinel)
  • 服务网关。(gateway,zuul)
  • 服务配置的统一管理。(config-server,nacos,apollo)
  • 服务消息总线。(bus)
  • 服务安全组件。(security,Oauth2.0)
  • 服务监控。(admin) (jvm)
  • 链路追踪。(sleuth+zipkin)

5.整合步骤【重点】

  • 倒入依赖pom
  • 配置文件
  • 注解
  • 编码

一、Eureka

注册发现中心

0.概念

0.什么是 CAP 原则(面试)

在分布式 微服务里面 CAP 定理 问:为什么 zookeeper 不适合做注册中心?

CAP 原则又称 CAP 定理,指的是在一个分布式系统中,

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容错性(Partition tolerance)(这个特性是不可避免的

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

1.分布式特征

image.png

  • C : 数据的一致性 (A,B,C 里面的数据是一致的) Zk 注重数据的一致性。 Eureka 不是很注重数据的一致性!
  • A: 服务的可用性(若 zk 集群里面的 master 挂了怎么办)Paxos(多数派) 在 zk 里面,若主机挂了,则 zk 集群整体不对外提供服务了,需要选一个新的出来(120s 左右)才能继续对外提供服务! Eureka 注重服务的可用性,当 Eureka 集群只有一台活着,它就能对外提供服务
  • P:分区的容错性(在集群里面的机器,因为网络原因,机房的原因,可能导致数据不会里面 同步),它在分布式必须需要实现的特性!

Zookeeper 注重数据的一致性,CP zk(注册中心,配置文件中心,协调中心) Eureka 注重服务的可用性 AP eureka (注册中心)

2.Spring Cloud 其他注册中心

Consul、Nacos、Eureka

1.实现

0.server

pom.xml: 
<modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
​
​
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
​
    <groupId>com.heng</groupId>
    <artifactId>eureka-client-b</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-client-b</name>
    <description>Demo project for Spring Boot</description>
​
yml:
    server:
         port: 8761
    spring:
        application:
            name: eureka-server
加上注解:
    @EnableEurekaServer 
​

1.client

pom.xml:
<modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
​
​
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
​
    <groupId>com.heng.eurekaclientb</groupId>
    <artifactId>eureka-client-b</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-client-b</name>
    <description>Demo project for Spring Boot</description>
​
yml:
    server:
      port: 8082
​
    spring:
      application:
        name: eureka-client-b
​
    eureka:
      client: # 客户端的相关配置
        service-url: # 指定注册的地址
          defaultZone: http://localhost:8761/eureka
​
注解:
    @EnableEurekaClient

2.集群

image.png

server:
	port: 8761 #不需要修改 defaultZone 了,修改端口起三个服务 
spring: 
	application: 
		name: eureka-server #服务名称
 eureka:
 	client: 
 		fetch-registry: true #是否拉取服务列表
         register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务) 
         service-url: 
         	defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/,http://peer3:8763/eureka/ server: eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
     instance: lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间 不能收到客户端心跳,则剔除此实例并且禁止流量

3 .Eureka 概念的理解

  • 注册:当项目启动时(eureka 的客户端),就会向 eureka-server 发送自己的元数据(原始数据) (运行的 ip,端口 port,健康的状态监控等,因为使用的是 http/ResuFul 请求风格), eureka-server 会在自己内部保留这些元数据(内存中)。(有一个服务列表)(restful 风 格,以 http 动词的请求方式,完成对 url 资源的操作)。
  • 续约:项目启动成功了,除了向 eureka-server 注册自己成功,还会定时的向 eureka-server 汇 报自己,心跳,表示自己还活着。(修改一个时间)。
  • 下线(主动下线) :当项目关闭时,会给 eureka-server 报告,说明自己要下机了
  • 剔除(被动下线,主动剔除) :项目超过了指定时间没有向 eureka-server 汇报自己,那么 eureka-server 就会认为此节点死掉了,会把它剔除掉,也不会放流量和请求到此节点了。

4.服务发现

image.png

eureka 客户端会把服务列表缓存到本地 为了提高性能

但是有脏读问题,当你启动一个新的应用的时候 不会被老的应用快速发现

@RestController
public class DiscoveryController {
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("test")
    public String doDiscovery(String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        ServiceInstance instance = instances.get(0);
        String ip = instance.getHost();
        int port = instance.getPort();
        System.out.println(ip + ":" + port);
        return instance.toString();
    }
}

5.RestTemplate

RestTemplate是一个执行HTTP请求的同步阻塞式工具类,它仅仅只是在 HTTP 客户端库(例如 JDK HttpURLConnectionApache HttpComponentsokHttp 等)基础上,封装了更加简单易用的模板方法 API,方便程序员利用已提供的模板方法发起网络请求和处理,能很大程度上提升我们的开发效率

    @Test
    void testGet() {
        RestTemplate template = new RestTemplate();
        String url = "http://localhost:8080/testRestTemplate/doGet?name=huashui";
        String res = template.getForObject(url, String.class);
        System.out.println(res);
    }
​
    @Test
    void testPost(){
        RestTemplate template = new RestTemplate();
        String url = "http://localhost:8080/testRestTemplate/doPost";
        User user = new User("huahsui", 12);
        String res = template.postForObject(url,user,String.class);
​
        System.out.println(res);
    }

二、Ribbon

0.概念

  • Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求 自动转换成客户端负载均衡的服务调用
  • 主要功能是提供客户端负载均衡算法和 服务调用
  • 构建的微服务系统中, Ribbon 作为服务消费者的负载均衡器,有两种使 用方式,一种是和 RestTemplate 相结合,另一种是和 OpenFeign 相结合。

1.负载均衡

  • 负载均衡,英文名称为 Load Balance(LB)http:// lb://(负载均衡协议) ,其含义 就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如 Web 服务器、 企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务

image.png

  • Ribbon 里面有没有服务列表?

Ribbon 只做负载均衡和远程调用

  • 服务列表从哪来?

从 eureka 来 Ribbon 有一个核心接口 ILoadBalance(承上(eureka)启下(Rule))

  • 我们发现在负载均衡之前,服务列表已经有数据了

0.负载均衡的实现和几种算法【重点】

负载均衡算法:随机 轮训 权重 iphash (响应时间最短算法,区域内亲和(轮训)算法)

1.RoundRobinRule--轮询 请求次数 % 机器数量

2.RandomRule--随机

3.AvailabilityFilteringRule --会先过滤掉由于多次访问故障处于断路器跳闸状态的服 务,还有并发的连接数量超过阈值的服务,然后对于剩余的服务列表按照轮询的策略进行访问

4.WeightedResponseTimeRule--根据平均响应时间计算所有服务的权重,响应时间越快服 务权重越大被选中的概率越大。刚启动时如果同统计信息不足,则使用轮询的策略,等统计信 息足够会切换到自身规则

5.RetryRule-- 先按照轮询的策略获取服务,如果获取服务失败则在指定的时间内会进行重 试,获取可用的服务

6.BestAvailableRule --会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后 选择一个并发量小的服务

7.ZoneAvoidanceRule -- 默认规则,复合判断 Server 所在区域的性能和 Server 的可用 行选择服务器。 Ribbon 默认使用

image.png

2.Ribbon 快速入门

image.png

consumer 和 provider-1 和 provider-2 都是 eureka-client 注意这三个依赖是 eureka-client 注意 provider-1 和 provider-2 的 spring.application.name=provider 注意启动类的注解和配置文件的端口以及服务名称

ribbon: #全局的设置 
	eager-load:
    	enabled: false # ribbon 一启动不会主动去拉取服务列表,当实际使用时才 去拉取 是否立即加载 
    http:
    	client:
        	enabled: false # 在 ribbon 最后要发起 Http 的调用调用,我们认为是 RestTemplate 完成的,其实最后是 HttpURLConnection 来完成的,这里面设置为 true , 可以把 HttpUrlConnection->HttpClient 
    okhttp: 
    	enabled: false #HttpUrlConnection 来完成的,这里面设置为 true ,可以 把 HttpUrlConnection->OkHttpClient(也是发 http 请求的,它在移动端的开发用的多) 
    	provider: #提供者的服务名称,那么访问该服务的时候就会按照自定义的负载均衡算法 
    		ribbon:
            	NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule 
#修改默认负载均衡算法,几种算法的全限定类名 
# NFLoadBalancerClassName: #loadBalance 策略 
# NFLoadBalancerPingClassName: #ping 机制策略 
# NIWSServerListClassName: #服务列表策略
# NIWSServerListFilterClassName: #服务列表过滤策略 ZonePreferenceServerListFilter 默认是优先过滤非一个区的服务列表

3.总结

  • Ribbon 是客户端实现负载均衡的远程调用组件,用法简单

  • Ribbon 源码核心:

    • ILoadBalancer 接口:起到承上启下的作用
    • 承上:从 eureka 拉取服务列表
    • 启下:使用 IRule 算法实现客户端调用的负载均衡

至此,就可以使用 eureka+ribbon 做分布式项目了

三、openFeign

Feign 是声明性(注解)Web 服务客户端。它使编写 Web 服务客户端更加容易。要使用 Feign, 请创建一个接口并对其进行注解。它具有可插入注解支持,包括 Feign 注解和 JAX-RS 注解。 Feign 还支持可插拔编码器和解码器。Spring Cloud 添加了对 Spring MVC 注解的支持,并 支持使用 HttpMessageConverters,Spring Web 中默认使用的注解。Spring Cloud 集成 了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载平衡 的 http 客户端

0.快速入门

image.png

1.yml

user-service.yml
    server:
        port: 8081
    spring:
        application:
            name: user-service
    eureka:
        client:
            service-url:
                defaultZone: http://localhost:8761/eureka
        instance:
            hostname: localhost
            prefer-ip-address: true
            instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
    # feign只是帮你封装了远程调用的功能  底层还是ribbon 所以我们需要去修改ribbon的时间
    ribbon:
        ReadTimeout: 3000 # 给3s超时时间
        ConnectTimeout: 3000 # 链接服务的超时时间
order-service.yml
    server:
        port: 8080
    spring:
        application:
            name: order-service
    eureka:
        client:
            service-url:
                defaultZone: http://localhost:8761/eureka
        instance:
            hostname: localhost
            prefer-ip-address: true
            instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

2.order-service 修改启动类增加一个访问接口

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @FeignClient(value = "order-service")
 * value 就是提供者的应用名称
 */
@FeignClient(value = "order-service")
public interface UserOrderFeign {

    @GetMapping("doOrder")
    public String doOrder();
}

3.调用过程

consumer-user-service ---> /userDoOrder ---> 通过 feign 调用 /doOrder ---> provider-order-service 下单成功

1.调用参数处理(开发重点)

  • Feign 传参确保消费者和提供者的参数列表一致 包括返回值 方法签名要一致
  • 通过 URL 传参数,GET 请求,参数列表使用@PathVariable(“”)
  • 如果是 GET 请求,每个基本参数必须加@RequestParam(“”)
  • 如果是 POST 请求,而且是对象集合等参数,必须加@Requestbody 或者@RequestParam

注意:时间日期参数问题

使用 feign 远程调用时,传递 Date 类型,接收方的时间会相差 14 个小时,是因为时区造成 的

处理方案:

    1. 使用字符串传递参数,接收方转换成时间类型(推荐使用)不要单独传递时间

  1. 使用 JDK8 的 LocalDate(日期) 或 LocalDateTime(日期和时间,接收方只有秒,没有毫 秒)
  2. 自定义转换方法

2.日志功能

日志级别

  • NONE 默认的,不显示日志
  • BASE 仅记录请求方法,URL ,响应状态码及执行时间
  • HEADERS 在 BASE 之上增加了请求和响应头的信息
  • FULL 在 HEADERS 之上增加了请求和响应的正文及无数据
@Bean
    public Logger.Level level(){
        return Logger.Level.FULL;
    }
logging:
    level:
        com/.../feign/UserOrderFeign: debug # 我需要打印这个接口下面的日志

3.总结

OpenFeign 主要基于接口和注解实现了远程调用

源码总结:面试

  1. OpenFeign 用过吗?它是如何运作的?

    • 在主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了 @FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用 ribbon 做了负载均衡和远程调用
  2. 如何创建的代理对象?

    • 当 项 目 在 启 动 时 , 先 扫 描 , 然 后 拿 到 标 记 了 @FeignClient 注 解 的 接 口 信 息 , 由 ReflectiveFeign 类的 newInstance 方法创建了代理对象 JDK 代理
  3. OpenFeign 到底是用什么做的远程调用? 使用的是 HttpURLConnection (java.net)

  4. OpenFeign 怎么和 ribbon 整合的? 在代理对象执行调用的时候

四、Hystrix

0.服务雪崩

image.png

0.修改调用的超时时长(不推荐)

  • 将服务间的调用超时时长改小,这样就可以让线程及时回收,保证服务可用
  • 优点:非常简单,也可以有效的解决服务雪崩
  • 缺点:不够灵活,有的服务需要更长的时间去处理(写库,整理数据)

1.设置拦截器

image.png

1.快速入门

熔断器,也叫断路器!(正常情况下 断路器是关的 只有出了问题才打开)用来保护微服务不 雪崩的方法。能够阻止分布式系统中出现 联动故障

设计:

customer-service:用户service,调用租车服务【web,eureka-client,openFeign,Hystrix】

rent-car-service:租车service【web,eureka-client】

重点:

<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> 
</dependency>
0.创建 CustomerRentHystrix 实现 OrderServiceFeign(代替方案)
@Component
public class CustomerRentHystrix implements CustomerRentFeign {
    @Override
    public String rent() {
​
        return "我是熔断逻辑";
    }
}
​
1.修改 OrderServiceFeign 增加一个 fallback
@FeignClient(value = "rent-car-service", fallback = CustomerRentHystrix.class)
public interface CustomerRentFeign {
    @GetMapping("rent")
    public String rent();
}

配置文件开启熔断:

feign:
    hystrix:
        enabled: true  # 在cloud的F版以前 是默认开启的 但是因为后来有了其他的熔断组件

2.参数

#隔离方式 两种隔离方式 
# thread 线程池 按照 group(10 个线程)划分服务提供者,用户请求的线程 和做远程的线程不一样 
    # 好处 当 B 服务调用失败了 或者请求 B 服务的量太大了 不会对 C 服务造成影响 用户访问比较大的情 况下使用比较好 异步的方式 
    # 缺点 线程间切换开销大,对机器性能影响 
    # 应用场景 调用第三方服务 并发量大的情况下 
# SEMAPHORE 信号量隔离 每次请进来 有一个原子计数器 做请求次数的++ 当请求完成以后 -- 
    # 好处 对 cpu 开销小 
    # 缺点 并发请求不易太多 当请求过多 就会拒绝请求 做一个保护机制 
    # 场景 使用内部调用 ,并发小的情况下 
# 源码入门 HystrixCommand AbstractCommand HystrixThreadPool

五、Sleuth

链路追踪就是:追踪微服务的调用路径

在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个请求都会开成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引导起整个请求最后的失败。(不建议微服务中链路调用超过 3次)

image.png

0.分布式链路调用的监控

sleuth+zipkin(zipkin 就是一个可视化的监控控制台)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
spring:
    zipkin:
        base-url: http://localhost:9411
sleuth:
    sampler:
        probability: 1 #配置采样率 默认的采样比例为: 0.1,即 10%,所设
置的值介于 0  1 之间,1 则表示全部采集
        rate: 10

1.Admin

用于监控服务运行详细状态的服务

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
management:
    endpoints:
        web:
            exposure:
                include: '*'  # 暴露所有的监控端点 # 如果一个服务需要被监控 那么就要讲自身的一些情况(一些信息接口)暴露出去

六、gateway

0.什么是网关

网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁

  • 没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的 ip:port;
  • 有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的 ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可实现 token 拦截,权限验证,限流等操作

image.png

1.gateway这里

SpringCloud Gateway 是基于 webFlux 框架实现的,而 webFlux 框架底层则使用了高性能的 Reactor 模式通信框架的 Netty。

总结:Gateway 的核心逻辑也就是 路由转发 + 执行过滤器链

image.png

三大核心概念

  • Route(路由)(重点 和 eureka 结合做动态路由
  • Predicate(断言)(就是一个返回 bool 的表达式)
  • Filter(过滤) (重点)

2.Nginx 和 Gateway 的区别

  • Nginx 在做路由,负载均衡,限流之前,都有修改 nginx.conf 的配置文件,把需要负载均衡,路由,限流的规则加在里面。Eg:使用 nginx 做 tomcat 的负载均衡。【操作系统级别的负载均衡】
  • gateway 不同,gateway 自动的负载均衡和路由,gateway 和 eureka 高度集成,实现自动的路由,和 Ribbon 结合,实现了负载均衡(lb),gateway 也能轻易的实现限流和权限验证。【项目级别的负载均衡】
  • Nginx(c)比 gateway(java)的性能高一点

image.png

3.快速入门

image.png

@SpringBootApplication
@EnableEurekaClient //网关也是 eureka 的客户端
public class Gateway80Application {
    public static void main(String[] args) {
    	SpringApplication.run(Gateway80Application.class, args);
    }
}
server:
	port: 80
spring:
	application:
		name: gateway-80
cloud:
	gateway:
		enabled: true #开启网关,默认是开启的
	routes: #设置路由,注意是数组,可以设置多个,按照 id 做隔离
		- id: user-service-router #路由 id,没有要求,保持唯一即可
		  uri: http://localhost:8081 #设置真正的服务 ip:port
		  predicates: #断言匹配
			- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
		- id: provider-service-router
		  uri: http://localhost:8082
		  predicates:
			- Path=/info/** #如果匹配到第一个路由,则第二个就不会走了,注意这不是负载均衡
#eureka 的配置
eureka:
	instance:
		instance-id: ${spring.application.name}:${server.port}
		prefer-ip-address: true
	client:
	service-url:
		defaultZone: http://localhost:8761/eureka/

集群

image.png

server:
	port: 80
spring:
	application:
		name: gateway-80
cloud:
	gateway:
		enabled: true #开启网关,默认是开启的
		
	discovery:
		locator:
			enabled: true
			lower-case-service-id: true
	routes: #设置路由,注意是数组,可以设置多个,按照 id 做隔离
		- id: user-service-router #路由 id,没有要求,保持唯一即可
		  uri: http://192.168.226.1:8081 #设置真正的服务 ip:port
		  predicates: #断言匹配
			- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
		- id: provider-service-router
		  uri: http://192.168.226.1:8082
		  predicates:
			- Path=/info/** #如果匹配到第一个路由,则第二个就不会走了,注意这不是负载均衡
#eureka 的配置
eureka:
	instance:
		instance-id: ${spring.application.name}:${server.port}
		prefer-ip-address: true
	client:
		service-url:
			defaultZone: http://localhost:8761/eureka/

两种路由配置方式

0.代码

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes
            .route("path_rote_guonei", r -> r.path("/guonei").uri("http://news.baidu.com/guonei"))
            .route("path_rote_guoji", r -> r.path("/guoji").uri("http://news.baidu.com/guoji"))
            .route("path_rote_tech", r -> r.path("/tech").uri("http://news.baidu.com/tech"))
            .route("path_rote_lady", r -> r.path("/lady").uri("http://news.baidu.com/lady"))
            .build();
        return routes.build();
	}
}

1.使用 yml 方式(重点)

image.png

4.动态路由,负载均衡

  • Gateway 会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能需要注意的是 uri 的协议为 lb(load Balance),表示启用 Gateway 的负载均衡功能。
  • lb://serviceName 是 spring cloud gateway 在微服务中自动为我们创建的负载均衡 uri

5.自定义过滤器【重点】

全局过滤器的优点的初始化时默认挂到所有路由上,我们可以使用它来完成 IP 过滤,限流等功能

@Component
public class MyGlobalFilter implements GlobalFilter , Ordered {
    /**
     * 这个就是过滤的方法
     * 过滤器链模式
     * 责任链模式
     * 网关里面有使用  mybatis的 二级缓存有变种责任链模式
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 针对请求的过滤  拿到请求  header  url 参数 ....
        ServerHttpRequest request = exchange.getRequest();
​
        String path = request.getURI().getPath();
        System.out.println(path);
        HttpHeaders headers = request.getHeaders();
        System.out.println(headers);
        String methodName = request.getMethod().name();
        System.out.println(methodName);
​
        //--------------------------------------------------------------------------
​
        ServerHttpResponse response = exchange.getResponse();
        // 用了微服务 肯定是前后端分离的 前后端分离 一般前后通过 json
        // {"code":200,"msg":"ok"}
        // 设置编码 响应头里面置
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        // 组装业务返回值
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", HttpStatus.UNAUTHORIZED.value());
        map.put("msg","你未授权");
        ObjectMapper objectMapper = new ObjectMapper();
        // 把一个map转成一个字节
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        // 通过buffer工厂将字节数组包装成 一个数据包
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
​
        //-----------------------------------------------------------------------------
​
        return chain.filter(exchange);//放行
    }
​
    /**
     * 排序过滤器,指定执行的先后
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
​

6.IP 认证拦截

@Component
public class IPGlobalFilter implements Ordered, GlobalFilter {
​
    private static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "192.158.10.32");
​
    /**
     * 网关里面 过滤器
     * ip拦截
     * 请求都有一个源头
     * 电话 144  027  010
     * 请求------->gateway------->service
     * 黑名单 black_list
     * 白名单 white_list
     * 根据数量
     * 像具体的业务服务 一般黑名单
     * 一般像数据库 用白名单
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getHeaders().getHost().getHostString();
        if (!BLACK_LIST.contains(ip)) {
            // 放行
            return chain.filter(exchange);
        }
​
        ServerHttpResponse response = exchange.getResponse();
        // 用了微服务 肯定是前后端分离的 前后端分离 一般前后通过 json
        // {"code":200,"msg":"ok"}
        // 设置编码 响应头里面置
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        // 组装业务返回值
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", 600);
        map.put("msg","你是黑名单!");
        ObjectMapper objectMapper = new ObjectMapper();
        // 把一个map转成一个字节
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        // 通过buffer工厂将字节数组包装成 一个数据包
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }
​
    @Override
    public int getOrder() {
        return -10;
    }
}
​

7.token全局校验

image.png

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
​
    /**
     * 指定好放行的路径
     */
    public static final List<String> ALLOW_URL = Arrays.asList("/login-service/doLogin", "/myUrl", "/doLogin");
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    /**
     * 前提是? 和前端约定好 一般放在请求头里面 一般key   Authorization   value bearer token
     * 1.拿到请求url
     * 2.判断放行
     * 3.拿到请求头
     * 4.拿到token
     * 5.校验
     * 6.放行/拦截
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        if (ALLOW_URL.contains(path)) {
            return chain.filter(exchange);
        }
        HttpHeaders headers = request.getHeaders();
        List<String> authorizations = headers.get("Authorization");
        if (!CollectionUtils.isEmpty(authorizations)) {
            String token = authorizations.get(0);
            if (StringUtils.hasText(token)) {
                String realToken = token.replaceFirst("bearer ", "");
                if (StringUtils.hasText(realToken) && redisTemplate.hasKey(realToken)) {
                    return chain.filter(exchange);
                }
            }
        }
​
        ServerHttpResponse response = exchange.getResponse();
        // 设置编码 响应头里面置
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        // 组装业务返回值
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", HttpStatus.UNAUTHORIZED.value());
        map.put("msg","你未授权");
        ObjectMapper objectMapper = new ObjectMapper();
        // 把一个map转成一个字节
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        // 通过buffer工厂将字节数组包装成 一个数据包
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }
​
    @Override
    public int getOrder() {
        return -5;
    }
}

8.结合redis实现限流

通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:

  • IP 限流(5s 内同一个 ip 访问超过 3 次,则限制不让访问,过一段时间才可继续访问)
  • 请求量限流(只要在一段时间内(窗口期),请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个 api(url),一个服务)

Spring Cloud Gateway 已经内置了一个 RequestRateLimiterGatewayFilterFactory,我们可以直接使用。 目前 RequestRateLimiterGatewayFilterFactory 的实现依赖于 Redis,所以我们还要引入spring-boot-starter-data-redis-reactive。

本次限流模型

image.png

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

修改配置文件

server:
    port: 80
spring:
    application:
        name: gateway-80
    cloud:
        gateway:
            enabled: true
            routes:
                - id: user-service
                uri: lb://consumer-user-service
                predicates:
                - Path=/info/**
            filters:
            - name: RequestRateLimiter
            args:
                key-resolver: '#{@hostAddrKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 3
redis: #redis 的配置
    host: 192.168.226.128
    port: 6379
    database: 0
eureka:
    instance:
        instance-id: ${spring.application.name}:${server.port}
        prefer-ip-address: true
    client:
    service-url:
        defaultZone: http://localhost:8761/eureka/

9.跨域配置

globalcors:
    corsConfigurations:
        '[/**]':
            allowCredentials: true  # 可以携带cookie
            allowedHeaders: '*'x
            allowedMethods: '*'
            allowedOrigins: '*'
@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.addAllowedOrigin("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", configuration);
        return new CorsWebFilter(source);
    }

10.总结

  • 你们网关用的什么 ? Gateway zuul

  • 你们网关里面写什么代码?

    跨域,路由(动态路由,负载均衡),ip 黑名单拦截,Token 的校验,对请求进行过滤(请求参数校验), 对响应做处理(状态码,响应头) 熔断, 限流. 微服务的网关,可以很好地将具体的服务和浏览器隔离开,只暴露网关的地址给到浏览器 在微服务网关中,可以很好的实现校验认证,负载均衡(lb),黑名单拦截,限流等。

待续...