目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

Zuul

诞生背景

 微服务架构将后端拆解成许多个单独的应用:看似清晰的服务拆分,实则杂乱无章,完成一个业务逻辑,需要到不同主机和不同端口上面调取接口。于是一个面向服务治理、服务编排的㢟出现了——微服务网关。
网关示意图

 Zuul 是从设备和网站到后端应用程序所有请求的前门,为内部服务提供可配置的对外 URL 到服务的映射关系,基于 JVM 的后端路由器,其具备以下功能:

  • 统一接入:智能路由、AB 测试、灰度测试、负载均衡、容灾处理、日志埋点
  • 流量监控:限流处理、服务降级
  • 安全防护:鉴权处理、监控、机器网络隔离

 虽然 Zuul2.x 采用 Netty 有较大的性能提升,单改动较大,考虑到稳定性 Spring Cloud Finchley 继续沿用 Netflix Zuul 1.x 版本,另外由于 Spring Cloud Gateway 已经孵化成功,相较于 Zuul 在功能以及性能上都有明显提升,Pivotal 公司正在走一条“去 Netflix 化”的路线。

主流开源网关概览

主流开源网关对比

入门案例

Maven 依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

添加配置

#Zuul对外端口号
server:
  port: 9000

#服务的名称
spring:
  application:
    name: api-gateway
  cloud:
    client:
      ipAddress: 192.168.31.230

#指定注册中心地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true #调试Eureka管控台实例的显示格式
    instance-id: ${spring.cloud.client.ipAddress}:${server.port}:${spring.application.name}

启动类添加注解@EnableZuulProxy

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

访问 Eureka 管控台 http://localhost:8761/
服务发现网关

#Zuul 内部实现细节
Zuul内部实现细节

Zuul 原理

Zuul流程

  1. 前置过滤器,在这个过程中可以自定义一些过滤器扩扩展功能(日志埋点、限流、鉴权等…)。
  2. 智能路由,将请求路由到对应的服务。
  3. 如果出错进入错误过滤器。
  4. 最终都会经过后置过滤器进行最后的处理,然后响应给前端请求。

过滤器执行顺序

 过滤器的 order 值越小,越优先执行。
 通过 RequestContext 对象共享上下文内容。

网关默认访问规则

改写请求,通过网关统一接入

http://192.168.31.230:8781/api/v1/order/save?product_id=4&user_id=2
http://192.168.31.230:9000/order-service/api/v1/order/save?product_id=4&user_id=2    #次请求经过网关

http://192.168.31.230:8771/api/v1/product/list
http://192.168.31.230:9000/product-service/api/v1/product/list    #次请求经过网关

自定义路由规则

访问路径

http://192.168.31.230:9000/apigateway/product/api/v1/product/list
http://192.168.31.230:9000/apigateway/orderapi/v1/order/save?product_id=4&user_id=2

#自定义路由映射
zuul:
  routes:
 #解决路由映射重复覆盖问题
    order-service: /apigateway/order/**
    product-service: /apigateway/product/**
  #统一入口为上面的配置,其他入口忽略
  ignored-patterns: /*-service/**
  #忽略整个服务,对外提供接口
  #gnored-services: product-service

处理 http 请求头传递问题

 默认 "Cookie"、"Set-Cookie"、"Authorization" 这三个请求头会被 Zuul 过滤掉,并不会传递到后端,可以通过相关配置解决此问题:

#自定义路由映射
zuul:
  routes:
  #解决http请求头经过Zuul时不传递的问题
  sensitive-headers:

自定义 Zuul 过滤器实现登录鉴权

新建类并实现 ZuulFilter,然后实现方法,最后在加上@Component 注解

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

@Component
public class LoginFilter extends ZuulFilter {
    /**
     * 设置顾虑器类型:前置过滤器
     * @return
     */
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    /**
     * 过滤器执行优先级:数字越小,越优先执行。
     * @return
     */
    @Override
    public int filterOrder() {
        return 4;
    }
    /**
     * 过滤器是否生效
     * @return
     */
    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        System.out.println(request.getRequestURI());    // /api-gateway/order/api/v1/order/save
        System.out.println(request.getRequestURL());    // http://192.168.31.230:9000/api-gateway/order/api/v1/order/save
	//ACL  访问控制列表,一般都是定期从redis中拉取过来动态填写。
	//针对指定的服务URI进行拦截,而不是对所有URI进行拦截,这里可以用if进行判断。
        if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
            return true;
        }
	//if(){ }   ......
        return false;
    }

    /**
     * 业务逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        //JWT  查看是否有token
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //获取token
        String token = request.getHeader("token");
        if(StringUtils.isBlank(token)){//如果从head中获取为空,就从参数中获取
            token = request.getParameter("token");
        }

        //登录校验逻辑,根据公司情况自定义
        if(StringUtils.isBlank(token)) { //如果为token为空则返回错误响应
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
        }
        return null;
    }
}

不带 token 参数的请求 http://192.168.31.230:9000/apigateway/order/api/v1/order/save?user_id=2&product_id=2
401
带 token 参数的请求
http://192.168.31.230:9000/apigateway/order/api/v1/order/save?user_id=2&product_id=2&token=asdasd
带token参数的请求

限流

 构建一个自我修复型系统一直是各大企业进行架构设计的难点所在,在 Hystrix 中,我们可以通过熔断器来实现,通过触发某个阈值对异常流量进行降级处理。除此之外,也可以对异常流量进行提前的预防措施,比如:异步队列的削峰、流量排队、限流、分流等…

限流算法

  • 漏桶(Leaky Bucket)
     请求数据涌来,在漏桶的作用下流量被整形,不能满足要求的部分请求被削减掉(收集到一个队列中,做流量排队),漏铜算法能够强制限定流量速率。
    漏桶算法
  • 令牌桶(Token Bucket)
     桶里面存放令牌,而令牌又是以一个恒定的速率被加入桶内,可以积压,可以溢出。当数据涌来,量化请求用于获取令牌,如果获取到令牌则放行,同时桶内丢掉这个令牌,如果不能去得到令牌,请求则被丢弃。由于令牌桶内可以存在一定数量的令牌,那么就可能存在一定程度的流量突发,这也是决定漏桶算法与令牌桶算法使用与不同应用场景的主要原因。
    令牌桶算法

谷歌 guava 框架
基于令牌桶算法
Guava
在网关中编写过滤器:集群中需要结合 Redis 进行限流,令牌可以存放在公共缓存中供集群中所有节点使用。

import com.google.common.util.concurrent.RateLimiter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * 订单限流
 */
@Component
public class OrderRateLimiterFilter extends ZuulFilter {
    
 
    //每秒产生1000个令牌
 ; 令牌的具体数量需要根据业务进行测试而定。
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return -4;
    }
    
    @Override
    public boolean shouldFilter() {
        
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //只对订单接口限流
        if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
            return true;
        }
        
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        if(!RATE_LIMITER.tryAcquire()){ //如果没有获得令牌,则返回错误状态码
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
        }
        return null;
    }
}

网关的限流与高可用

  • 通常情况下限流需要反响代理 Nginx 结合使用
  • Zuul 的高可用比较简单,启动多个节点即可。
  • Nginx 的高可用可以通过 nginx+lvs+keepalive 实现。


作者:Soulboy