微服务网关zuul限流、熔断、路由规则详解


简介

最近在研究灰度方案。 灰度主要涉及两个方面,一个是网关,一个是配置。 这次主要研究了网关及其原理,我们现在使用项目每天都有很多流量进入。 每一个流量基本都要经过网关。所以弄清楚网关也可以帮我们更好的理解微服务请求的转发,对于后面研究灰度也有所帮助。我们项目中使用的zuul网关,所以也是以zuul网关为研究对象进行研究。主要从以下几个方面分析:

  • 1:网关的由来
  • 2:网关的基本功能和使用场景
  • 3:网关的的架构分析和生命周期
  • 4:网关标准的过滤器类型
  • 5:网关的应用
    • 5.1:zuul网关项目搭建(spring boot +nacos)
    • 5.2:zuul网关配置特殊路由规则
    • 5.3:zuul的全局拦截功能
    • 5.4:在zuul网关中实现限流
    • 5.5:在zuul网关中实现熔断器
  • 6:网关原理和源码分析(由于内容较多, 放在下次分享)
    研究网关, 首先需要知道网关是干什么的?他是怎么演变过来的?之前没有,后来为什么就有了呢?能解决哪些问题?具体怎么使用?按照这个顺序来分析研究

    一. 网关的由来

    我们最开始的服务是单体服务, 所有的功能业务都是在一台服务器上. 可以通过过滤器来实现安全校验, 如权限校验等.

单体时代,整个微服务的架构也比较简单. 后来随着业务的复杂度越来越高, 我们切换到了微服务时代
微服务网关演进.png
进入微服务时代, 有如下特点:

1:原来单体服务被拆分为n个小的微服务
2:这些微服务有一些公共的跨横切面的功能逻辑, 比如:安全,路由,日志等等。 如果每个微服务都来处理这些功能, 微服务就会很复杂, 微服务不但要处理业务逻辑,还要处理安全,路由,日志等逻辑, 开发的负担就会比较重。 所以 ,我们把这些公共的服务抽取出来, 放在网关服务上, 这就是在微服务上为什么会产生网关。
3:在单体时代, 我们的客户端主要是浏览器, 在微服务时代, 客户端可能是浏览器,可能是手机, 可能是我们对外提供的API。如果让这些客户端单独对接微服务, 也是很复杂的。 如果让他们都对接网关服务,外面的各种平台只需要对接一个入口, 通过网关接入就可以了,不需要知道很多内部细节。

二. 网关的基本功能和使用场景

2.1 网关的主要功能

网关的主要功能.png
如上图所示, 网关的主要功能有: 单点入口, 路由转发, 熔断限流, 日志监控, 安全认证等等

  • 单点入口: 有了网关以后, 客户端只需要看到一个入口, 不需要看到内部复杂的细节
  • 路由转发: 客户端请求来了, 对应到网关只有一个入口, 那么具体请求的是哪一个服务呢?网关要做一个路由转发
  • 限流熔断: 在微服务时代, 微服务有很多, 每一个微服务都可能会出错或者产生延迟, 如果没有好的限流熔断机制, 很容器造成客户端被阻塞, 或者产生严重的后果,如雪崩效应. 网关要做一个限流熔断,保护后台服务的这样的一个功能.
  • 日志监控: 所有的请求经过网关, 我们都可以写日志,对他进行监控. 整个服务的健康状态, 有没有人利用网关做坏事情等. 我们还要时刻掌握网关的性能状况. 可以在网关上增加监控来做到
  • 安全认证: 所有的请求都通过网关进入到微服务, 网关就像是一道门, 一个请求进来, 我们要在门口检查一下, 他是不是安全的. 是否是经过认证的.

    2.2 网关的应用场景

    1.红绿部署
    红绿部署.png
    绿色集群是老系统, 红色集群是新系统. 当新功能上线以后, 通过网关切流量, 先切部分流量到红色集群,也就是新系统. 做金丝雀测试. 测试没问题, 在逐渐切换到红色集群,直至所有的流量全部切换到红色集群. 期间, 有任何错误,
    可以直接切换回老系统
    2.健康检查和屏蔽怀节点
    健康检查屏蔽坏节点.png
    网关可以对请求进行度量分析, 监控. 如果某一个后台的微服务节点出错了, 或者不响应了.可自动摘除无用服务器, 屏蔽坏的节点. 这样用户的体验就更好.
    3.调试路由
    调试路由.png
    有的时候生成环境除了一些问题, 我们需要用到调试路由. 比如, 测试人员在header中增加一些参数. 或者特殊的信息放在里面. 请求过来以后, 网关会把这部分流量分配到调试集群. 这样就可以做线上的调试, 来发现一些问题.
    4.金色雀测试
    金丝雀测试
    我们可以利用网关实现金丝雀测试. 新的应用上线之前, 分为baseLine集群和canary集群.通过网关, 调拨少量的流量到canary金丝雀, 如果流量没有问题, 说明整个应用是健康的. 再全量的切换到新的版本. 如有问题,随时切换回去.
    5.粘性金丝雀
    粘性金丝雀.png
    粘性金丝雀测试也是金丝雀测试. 如果不带粘性, 那么流量过来到网关, 网关是会随机分配给baseLine和金丝雀集群的. 如果是粘性, 网关有黏住你的能力, 如果某一次你走的是金丝雀, 那么网关会记住你的信息,如ip,
    下次你来了, 还让你走金丝雀集群. 他就不会随便跳了. 这在有些情况下是必须的, 不能让ip跳来跳去, 不然可能导致功能或结果不一致, 黏住就是一致的了。

    三. 网关的的架构分析和生命周期

    3.1 zuul网关的核心流程

    zuul网关的核心流程.png
    如上图, spring cloud zuul网关的核心架构

这个运行时模块本身是一个http servlet, 请求过来以后, 首先交给zuul servlet, zuul servlet再将请求交给zuul filter runner. zuul filter runner是整个网关最核心的组件.

请求到达以后,会依次经过前置路油过滤器, 路由过滤器, 和后置路由过滤器. 经过过滤以后, 请求会以response的形式响应给客户端. zuul网关最核心的部门就是过滤链, 依次运行过滤器. 有一个对象很重要,
就是Request Context, 当请求在zuul网关过滤链中流转的时候, 他们需要共享一些过滤信息. 这些信息就保存在RequestContext中的。 RequestContext是线程内过滤器共享的。
比如前置过滤器会设置一些信息,给路由过滤器去读取. 信息的交换是通过Request Context, 就像是过滤器之间可以共享的存储, 而且是线程安全的。 每个请求有一个局部的RequestContext.

3.2 请求处理的生命周期

请求处理的生命周期.png

  • 请求过来了, 首先会进入一系列的前置过滤器pre filter.
  • 前置过滤器处理完了, 进入routing filter路由过滤器, routing filter路由过滤器是真正的向后台服务发起请求, 接收响应的过滤器
  • 经过routing filter路由过滤器, 最后会传递给post filter 后置过滤器,进行一些后续的处理, 这时候已经拿到响应了, 然后在返回给客户端.
  • 在这三个过滤器过滤的过程中,任何一个环节发生错误, 都会进入error filter. 有error filter进行统一的错误处理. error filter错误过滤器会发送给post filter, 也是以响应的方式发回给客户端.

这是一个请求, 在网关处理的生命周期.

四. 网关标准的过滤器类型

如上描述, 有下面这4中类型

1. pre:前置过滤器

在请求被路由到原服务器之前, 要执行的过滤器

认证 : 认证安全, 是否符合条件, 认证为安全的才能放过
选路由: 当前这个请求来了, 应该调用后面的哪个微服务呢? A还是B
请求日志: 请求日志, 日志来了, 写日志, 对其进行监控

2. routing: 路由过滤器

处理将请求发送到源服务器的过滤器

3. post: 后置过滤器

在响应从源服务器返回时要被执行的过滤器

对响应增加http请求头: 要增加调试的header日志
收集统计和度量: 这次请求, 它的性能如何, 有没有出错? 可以搜集一些信息
将响应以流的方式返回客户端

4. error: 错误过滤器

上述三种过滤器中任何过滤器出现错误都要执行的过滤器

五. 网关的应用

5.1. zuul网关项目搭建

zuul网关项目.png

5.1.1 引入项目依赖

搭建一个全新的项目

https://start.spring.io/ 创建一个项目, 项目名称zuul-test-gateway

引入的依赖: web, zuul, nacos-config, nacos-discovery

5.1.2 新建bootstrap.yml配置文件,配置nacos
spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yml
        namespace: 482c42bd-fba1-4147-a700-5b678d7c0747
        group: ZUUL_TEST
5.1.3 修改配置文件application.yml
server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: 482c42bd-fba1-4147-a700-5b678d7c0747
  redis:
    host: localhost
    port: 6379
5.1.4 在启动类引入相关注解

1.开启zuul网关的注解:@EnableZuulProxy
2.nacos注册发现注解: @EnableDiscoveryClient
3.web框架注解: @RestController
4.微服务间调用注解feign: @EnableFeignClients
5.动态更新配置注解: @RefreshScope

package com.xzg.www.gateway;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
@EnableZuulProxy // 开启zuul网关功能
@RefreshScope
public class ZuulTestGatewayApplication {
    @Value("${config}")
    private String config;

    public static void main(String[] args) {
        SpringApplication.run(ZuulTestGatewayApplication.class, args);
    }
    @GetMapping("config")
    public String getconfig(){
        return this.config;
    }
}
5.1.5 启动nacos服务端

启动nacos, 我这里是单机模式启动, 进入到nacos的目录

cd /users/nacos/nacos/bin
./startup.sh -m standalone
5.1.6 启动gateway网关项目

上面的配置文件中已经包含了nacos配置和服务发现的配置内容

5.1.7 在nacos中查看网关项目

1:网关的配置文件, 如下图所示
nacos网关的配置文件.png
2.网关项目启动后, 查看服务列表
nacos网关实例列表.png

5.1.8 创建另一个服务,用户服务—-zuul-test-user

方法同上
zuul-test-user-project.png
在控制台启动2台user服务。 查看naco中服务注册信息
zuul-test-user-nacos-console.png
至此项目搭建就完成了!!!

5.1.9 下面来感受一下网关

现在有两个服务, 一个是网关服务,端口是8080; 一个是user服务, 端口分别是8089和8088.

我们通过网关服务区请求user, 看看可不可以
通过zuul请求user服务.png
我在postman中输入的是网关的端口localhost:8080, 然后紧跟着服务名+path, 可以正确跳转到user服务上.

其实,我们在配置文件中, 没有做任何配置.

网关是网关服务, 用户是用户服务, 他们都可已单独存在, 单独工作. 但是通过网关的地址却能够跳转到user服务上. 这是zuul自动为我们在服务发现上发现相应的集群中的其他服务.

5.2. 配置特殊的路由规则

zuul: 
  routes:
    user-router: # 随便写, 是一个唯一的, 代表一个微服务的路由机制
      service-id: user # 该路由机制针对的是哪个微服务
      path: /user1/**

这表示, 所有连接 http://localhost:8080/user1/** 的连接都会分发到服务名为user的服务上

比如:我们请求路径写成 http://localhost:8080/user1/config, 那么也会正确的请求的user服务

5.3. zuul的全局拦截

zuul是前端访问的唯一入口, 我们可以在zuul实现一个token的拦截验证

1.定义一个TokenFilter过滤器,这个过滤器extends ZuulFilter

package com.xzg.www.gateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 实现token 拦截验证
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 怎么判断用户的token?
        // RequestContext 请求的上下文, 包含所有的请求参数, 他默认和线程绑定
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();

        // 从请求头中拿出token
        String token = request.getHeader("token");

        if (!StringUtils.hasText(token)) {
            // token 为null
            currentContext.setResponseBody("token is null");
            currentContext.setResponseStatusCode(401);
            // 是否发送路由响应
            currentContext.setSendZuulResponse(false);
            return null;
        }

        if (!"123456".equals(token)) {
            currentContext.setResponseBody("token is error");
            currentContext.setResponseStatusCode(401);
            currentContext.setSendZuulResponse(false);
            return null;
        }

        currentContext.setSendZuulResponse(true);
        return null;
    }
}

这个过滤器extends自ZuulFilter, 后面的案例多了, 我们就发现, 其实zuul网关的本质就是拦截器, zuul的各种功能,也是通过拦截器来实现的

filterType() : 拦截器的类型是前置拦截器.

filterOrder(): 执行顺序是第一个执行.

shouldFilter(): 过滤器执行的条件, 这里是所有的连接都需要过这个拦截器, 所以直接设置为true

run(): 拦截器的核心逻辑. 这里的拦截器逻辑很简单, 就是判断header中是否有一个叫做token的属性, 且其值为123456

2.启动服务, 查看拦截器效果
当有header, 不启用的时候, 会被拦截, 提示token is null, 并且跳过后面的拦截器, 直接返回

zuul-token-invalid.png
当有token ,但是token的值不是123456的时候, 会说token错误
zuul-token-error.png
只有当token符合我们的预期的时候, 才可以放行
zuul-token-success.png

5.4 在zuul里面实现限流

1. 添加zuul的限流组件

git地址:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit

进入git查看zuul-ratelimit的使用方法, 上面是源码, 下面是使用方法

2. 添加限流组件–引入依赖
<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>2.4.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.4.1</version>
</dependency>

我们引入限流组件, 同时引入redis. 用来记录当前用户登录的次数

3. 在配置文件中增加redis配置.

在本地先启动redis, 端口6379

然后增加redis配置
redis信息配置.png

4. 增加限流配置

根据文档, 限流配置有两种,

第一种: 通用限流配置: 对所有的微服务都生效
第二种:特定服务限流配置: 针对某一个微服务设置的限流
a. 通用限流配置:
zuul限流配置
如上限流的含义是: 对所有微服务都生效的限流策略是: 对某个微服务在60s内,请求次数限制是10次. 或者刷新窗口时间隔实现的限制是1秒. 也就是1秒刷新一次窗口.

配置中的具体含义如上注释

配置好以后, 启动微服务, 在postman中查看 是否起到限流作用
限流测试.png
当次数超过10次时候, 就会给出异常提示, too many requests.

b.特定微服务的限流限制
一定时间内限流测试.png
设置单个微服务的限流策略, 如上所示, 具体含义: 60s内, 限制次数为10次, 窗口刷新间隔时间是1秒.

效果如下, 当每分钟请求次数超过5次的时候, 爆出连接次数过多
定期内限流postman测试.png

5. 限流的机制

限流器的本质是filter

在限流器通过filter来记录用户的访问次数, 当次数达到一定值, 直接让filter拦截

6. 限流里面redis的作用

使用redis的原因, 当zuul是一个集群的时候,要在多个redis共享访问次数

5.5 在zuul里面实现熔断器

熔断, 其含义是当路由失败的时候, 执行熔断器.

zuul是自带熔断机制的. 不需要引入任何额外的依赖

我们需要的是, 实现熔断器中的FallbackProvider接口. 定义自己的熔断机制

@Component
public class ZuulFallbackProvider implements FallbackProvider {

    /**
     * 要对哪个微服务实现熔断
     * @return
     */
    @Override
    public String getRoute() {
        return "user"; // 这里写的是服务的id, *表示任何服务
    }

    /**
     * 在熔断时, 用户执行怎样的响应数据
     * @return 当熔断被触发以后, 如何响应内容
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("熔断器被触发:" + route);
        System.out.println("熔断的原因:" + cause);
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.BAD_REQUEST;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 401;
            }

            @Override
            public String getStatusText() throws IOException {
                return "服务异常!";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                byte[] body = "server is error, get in the fallback".getBytes();
                return new ByteArrayInputStream(body);
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.add("reason", "server is error, get in fallback!");
                return headers;
            }
        };
    }
}

getRoute(): 定义对哪个服务启用熔断策略

fallbackResponse(String route, Throwable cause): 当熔断时, 执行怎么样的操作

这里的熔断机制比较简单, 如果熔断了, 那么就打印日志, 并输出server is error, get in fallback!

比如: 我的user微服务挂了, 通过网关请求, 就会进得到如下信息提示.
zuul-fallback-test.png
以上就是网关在项目中通常使用的场景. 我们可以根据实际需求增加实现.


文章作者: 小张哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小张哥 !
评论
 上一篇
springcloud结合nacos实现灰度发版方案 springcloud结合nacos实现灰度发版方案
最近和组内伙伴一起分享讨论了灰度方案,先将内容整理出来, 具体如下: 灰度方案设计及demo实现 架构设计 组件设计及原理 关键代码实现 项目资料一. 架构设计原理 1:微服务系统在启动时将自己注册到服务注册中心,同时对外发布 Htt
下一篇 
golang从内核到epoll golang从内核到epoll
引子:在之前的文章里 golang netpoll的实现与分析 讲了一些,对于golang netpoll的实现,但是,数据是怎么通过硬件到达golang的这块不是太明确,今天就主要分析下这一块。 linux的网络的基本实现在 TCP/
  目录