Skip to content

Zuul 简介

Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用,Zuul 组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:

  • 动态路由:动态将请求路由到不同后端集群
  • 压力测试:逐渐增加指向集群的流量,以了解性能
  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
  • 静态响应处理:边缘位置进行响应,避免转发到内部集群
  • 身份认证和安全:识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud 对 Zuul 进行了整合和增强。

Zuul 网关服务器搭建准备

创建 Zuul 组件的示例工程 spring-cloud-sample-zuul,复用之前 spring-cloud-sample-feign 工程的代码,删除一些无用的内容

创建zuul服务引入依赖

创建 zuu l网关工程 shop-server-zuul,并添加 zuul 组件的依赖

xml
<!-- spring cloud netflix zuul 组件核心依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

开启zuul网关功能

创建启动类 ZuulServerApplication,在启动类上标识@EnableZuulProxy注解开启zuul网关功能

java
@SpringBootApplication
@EnableZuulProxy // 开启zuul网关功能
public class ZuulServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}

项目配置

创建工程的配置文件application.yml,并添加基础配置

yml
server:
  port: 8080 # 项目端口
spring:
  application:
    name: shop-server-zuul # 服务名称

Zuul 的路由转发

路由转发是指根据请求 URL,将请求分配到对应的处理程序。在微服务体系中,Zuul 负责接收所有的请求。根据不同的 URL 匹配规则,将不同的请求转发到不同的微服务处理。

基础配置

修改工程的application.yml配置文件,配置路由规则

yml
# zuul 路由配置
zuul:
  routes:
    # 商品微服务
    shop-service-product: # 路由名称,名称任意,保持所有路由名称唯一即可
      path: /product-service/** # 配置映射路径,当前示例即:localhost:8080/product-service/xxxx/xx
      url: http://127.0.0.1:9001 # 配置请求映射路径时转发到对应的实际微服务url地址,可以是实际ip或者域名
      sensitiveHeaders: # 默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务

配置项说明:

  • shop-service-product:配置路由id,可以随意取名,只要保持配置中所有路由id唯一即可
  • path:配置映射路径,上面示例代表所有前缀为/product-service的请求,都会转发到http://127.0.0.1:9001
  • url:映射路径对应的实际url地址

Zuul 路由配置后,启动服务,在浏览器中输入http://localhost:8080/product-service/product/1,即可访问到商品微服务

通过注册中心方式的路由配置

对于一个 URL 请求,最终会确认一个服务实例进行处理。在实际项目众多微服务情况下,如果对每个服务实例手动指定一个唯一访问地址,然后根据 URL 去手动实现请求匹配,显然不可能的。

Zuul 支持与 Eureka 整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,此配置方式不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改 Zuul 的路由配置

  1. 修改网关服务,添加注册中心 Eureka 客户端的依赖
xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. 开启Eureka客户端发现功能。在启动类中标识@EnableDiscoveryClient注:从Spring Cloud Edgware版本开始,开启服务发现的注解可以省略不写。只需加上相关依赖与相应配置,即可注册服务
  2. application.yml中添加 Eureka 配置,获取服务信息
yml
# Eureka 客户端配置
eureka:
  instance:
    prefer-ip-address: true # 将当前服务的ip地址注册到Eureka服务中
    instance-id: ${spring.cloud.client.ip-address}:${server.port}  # 指定实例id
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/ # Eureka server 地址,多个eureka server之间用,隔开
      registry-fetch-interval-seconds: 5 # 配置获取服务列表的周期为5s,默认值是30s
  1. 修改zuul的映射配置,通过服务名称获取。引入Eureka客户端后,可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能
yml
zuul:
  routes:
    # 商品微服务
    shop-service-product: # 路由名称,名称任意,保持所有路由名称唯一即可
      path: /product-service/** # 配置映射路径,当前示例即:localhost:8080/product-service/xxxx/xx
      serviceId: shop-service-product # 配置请求转发相应的服务名称,网关会从eureka中获取该服务名称下的服务实例的地址
      sensitiveHeaders: # 默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务

配置项说明:

  • serviceId:指定请求需要转发的微服务实例名称(服务注册到eureka的名称)

依次启动Eureka,商品微服务,API网关,在浏览器上通过访问 http://localhost:8080/product-service/product/1 查看最终效果。

路由配置的简化方式

传统面向服务的路由配置规则是:

  • zuul.routes.<route>.path=/xxx/**:来指定映射路径。<route>是自定义的路由名
  • zuul.routes.<route>.serviceId=/product-service:来指定服务名

有一种特殊的情况:如果当<route>路由名称和服务名定义一样的时候,Zuul提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>。上面示例的配置简化后如下:

yml
zuul:
  routes:
    # 当路由的名称和服务名称一样的时候,zuul路由配置可简化成 zuul.routes.<serviceId>=<path>
    shop-service-product: /product-service/**

默认的路由规则

Zuul不单单提供的路由的简化配置,zuul还有默认的路由规则。默认的路由规则是:一切服务的映射路径就是服务的名称

例如:服务的名称为shop-service-order,则默认的映射路径就是shop-service-order/**。所以示例工程不需要配置order服务,zuul也可以成功转发请求

通过注册中心方式配置zuul路由的架构图

Zuul 的过滤器

Zuul 组件包含了两个核心功能:对请求的路由和过滤

  • 路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础
  • 过滤器功能负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础

其实路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以过滤器可以说是 Zuul 实现 API 网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。

ZuulFilter 简介

Zuul 中的过滤器跟 Servlet 使用的javax.servlet.Filter不一样,javax.servlet.Filter只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有4种类型,且每种类型都有对应的使用场景。

  1. PRE:此过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  2. ROUTING:此过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
  3. POST:此过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。
  4. ERROR:在其他阶段发生错误时执行该过滤器。

过滤器的生命周期

正常流程

  • 请求到达首先会经过 pre 类型过滤器,而后到达 routing 类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达 post 过滤器。而后返回响应

异常流程

  • 整个过程中,pre 或者 routing 过滤器出现异常,都会直接进入 error 过滤器,再 error 处理完毕后,会将请求交给 POST 过滤器,最后返回给用户。
  • 如果是 error 过滤器自己出现异常,最终也会进入 POST 过滤器,而后返回。
  • 如果是 POST 过滤器出现异常,会跳转到 error 过滤器,但是与 pre 和 routing 不同的时,请求不会再到达 POST 过滤器了

不同过滤器的场景

  • 请求鉴权:一般放在 pre 类型,如果发现没有访问权限,直接就拦截了
  • 异常处理:一般会在 error 类型和 post 类型过滤器中结合来处理
  • 服务调用时长统计:pre 和 post 结合使用。

zuul 内置过滤器

自定义 zuul 过滤器

基础用法

Zuul 提供了自定义过滤器的功能,只需要编写一个类去实现 zuul 提供的 ZuulFilter 接口即可。

Zuul 也提供了过滤器的顶级抽象类 ZuulFilter,一般自定义 zuul 过滤器都继承该抽象父类,其中定义的4个最重要的方法如下:

java
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {

    boolean shouldFilter(); // 继承于IZuulFilter接口

    Object run() throws ZuulException; // 继承于IZuulFilter接口

    abstract public String filterType();

    abstract public int filterOrder();
}
  • shouldFilter():返回一个 Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false 不执行
  • run():过滤器的具体业务逻辑
  • filterType():返回字符串,代表过滤器的类型。包含以下4种:
    • pre:请求在被路由之前执行
    • routing:在路由请求时调用
    • post:在routingerrror过滤器之后调用
    • error:处理请求时发生错误调用
  • filterOrder():通过返回int类型的值来定义过滤器的执行顺序,数字越小优先级越高

自定义过滤器示例

示例需求:自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求头中有access-token参数,则认为请求有效,放行。

shop-server-zuul工程中创建LoginFilter类,实现ZuulFilter接口,编写请求被路由前的业务逻辑

java
package com.moon.zuul.filter;

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 org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * zuul自定义过滤器示例 - 请求登陆校验过滤
 */
@Component // 自定义zuul过滤器,需要使用@Component标识该bean给spring容器管理
// 自定义zuul过滤器需要继承抽象父类ZuulFilter
public class LoginFilter extends ZuulFilter {
    /**
     * 设置过滤器类型
     * pre:请求在被路由之前执行
     * routing:在路由请求时调用
     * post:在routing和errror过滤器之后调用
     * error:处理请求时发生错误调用
     *
     * @return 过滤器的类型字符串
     */
    @Override
    public String filterType() {
        // 登录校验,设置为请求路由之前拦截
        return "pre";
    }

    /**
     * 指定过滤器的执行顺序,返回值越小越被优先执行
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 过滤器是否需要执行
     *
     * @return true: 执行此过滤器,false: 不执行
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的业务逻辑方法
     * <p>
     * 此示例完成身份认证:
     * 1.所有的请求需要携带一个参数: access-token
     * 2.获取request请求头"access-token"
     * 3.判断token是否为空
     * 4.1 token==null : 身份验证失败
     * 4.2 token!=null : 执行后续操作
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 在zuul网关中,通过RequestContext的上下文对象,可以获取HttpServletRequest和HttpServletResponse对象
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        HttpServletResponse response = context.getResponse();

        // 获取请求头中access-token字段
        String token = request.getHeader("access-token");

        if (StringUtils.isEmpty(token)) {
            // 如果请求头不包含Authorization,则拒绝访问
            context.setSendZuulResponse(false);
            // 设置响应状态码
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            // 设置JSON格式的响应信息
            context.setResponseBody("{\"code\": \"-1\", \"retMsg\": \"此操作需要登陆系统!\"}");
            // 设置contentType为json类型
            response.setContentType("application/json;charset=UTF-8");
            // 还可以进行其他的操作
        }
        // 校验通过,返回继续向后执行。(也可以考虑把用户信息放入上下文)
        return null;
    }
}

上面示例的RequestContext是zuul用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequestHttpServletResponse都存储在RequestContext中。RequestContext扩展了ConcurrentHashMap,所以,任何数据都可以存储在上下文中

启动服务进行测试:

服务网关Zuul的核心源码解析

Zuul 网关处理请求的流程

在 Zuul 中,整个请求的过程是:

  1. 首先将请求给ZuulServlet处理,ZuulServlet中有一个ZuulRunner对象,该对象中初始化了RequestContext,作为存储整个请求的一些数据,并被所有的ZuulFilter共享。
  2. ZuulRunner中还有FilterProcessorFilterProcessor作为执行所有的ZuulFilter的管理器。FilterProcessorFilterLoader中获取ZuulFilter,而ZuulFilter是被FilterFileManager所加载,并支持 groovy 热加载,采用了轮询的方式热加载。
  3. 有了这些 filter 之后,ZuulServlet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。
  4. 执行完这些过滤器,最终将请求的结果返回给客户端

zuul 的初始化

Spring Cloud 对 Zuul 的封装使得发布一个ZuulServer,根据自动装载原则可以在 spring-cloud-netflix-zuul-2.1.0.RELEASE.jar 下找到 spring.factories 文件

项目启动时会加载ZuulServerAutoConfigurationZuulProxyAutoConfiguration类,这两个类是Zuul服务端的自动配置类。其中ZuulProxyAutoConfiguration继承了ZuulServerAutoConfiguration

java
@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
		RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
		RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
		HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
    ....
}

查看ZuulServerAutoConfiguration源码,里面创建了几个zuul服务的核心类

java
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
    ....
    @Bean
	@Primary
	public CompositeRouteLocator primaryRouteLocator(
			Collection<RouteLocator> routeLocators) {
		return new CompositeRouteLocator(routeLocators);
	}

	@Bean
	@ConditionalOnMissingBean(SimpleRouteLocator.class)
	public SimpleRouteLocator simpleRouteLocator() {
		return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
				this.zuulProperties);
	}

	@Bean
	public ZuulController zuulController() {
		return new ZuulController();
	}
	....
	@Configuration
	protected static class ZuulFilterConfiguration {

		@Autowired
		private Map<String, ZuulFilter> filters;

		@Bean
		public ZuulFilterInitializer zuulFilterInitializer(
				CounterFactory counterFactory, TracerFactory tracerFactory) {
			FilterLoader filterLoader = FilterLoader.getInstance();
			FilterRegistry filterRegistry = FilterRegistry.instance();
			return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
		}
	}
	....
}
  • CompositeRouteLocator:组合路由定位器,看入参就知道应该是会保存好多个RouteLocator,构造过程中其实仅包括一个DiscoveryClientRouteLocator实例
  • SimpleRouteLocator:默认的路由定位器,主要负责维护配置文件中的路由配置。
  • ZuulController:Zuul 创建的一个 Controller,用于将请求交由ZuulServlet处理。
  • ZuulHandlerMapping:此类会添加到SpringMVC的HandlerMapping链中,只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程。
  • ZuulFilterInitializer:在内部类ZuulFilterConfiguration中,注册ZuulFilterInitializer实例,通过FilterLoader加载应用中所有的过滤器并将过滤器注册到FilterRegistry

以下就是ZuulFilterInitializer是如何将过滤器加载到应用中的源码

java
public class ZuulFilterInitializer {

	private static final Log log = LogFactory.getLog(ZuulFilterInitializer.class);

	private final Map<String, ZuulFilter> filters;
	private final CounterFactory counterFactory;
	private final TracerFactory tracerFactory;
	private final FilterLoader filterLoader;
	private final FilterRegistry filterRegistry;

	public ZuulFilterInitializer(Map<String, ZuulFilter> filters,
								 CounterFactory counterFactory,
								 TracerFactory tracerFactory,
								 FilterLoader filterLoader,
								 FilterRegistry filterRegistry) {
		this.filters = filters;
		this.counterFactory = counterFactory;
		this.tracerFactory = tracerFactory;
		this.filterLoader = filterLoader;
		this.filterRegistry = filterRegistry;
	}

	@PostConstruct
	public void contextInitialized() {
		log.info("Starting filter initializer");

		TracerFactory.initialize(tracerFactory);
		CounterFactory.initialize(counterFactory);

		for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
			filterRegistry.put(entry.getKey(), entry.getValue());
		}
	}

	@PreDestroy
	public void contextDestroyed() {
		log.info("Stopping filter initializer");
		for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
			filterRegistry.remove(entry.getKey());
		}
		clearLoaderCache();

		TracerFactory.initialize(null);
		CounterFactory.initialize(null);
	}

	private void clearLoaderCache() {
		Field field = ReflectionUtils.findField(FilterLoader.class, "hashFiltersByType");
		ReflectionUtils.makeAccessible(field);
		@SuppressWarnings("rawtypes")
		Map cache = (Map) ReflectionUtils.getField(field, filterLoader);
		cache.clear();
	}
}

zuul 处理请求的转发

在 Zuul 的ZuulServerAutoConfiguration自动配置类中,创建了ZuulHandlerMapping实例,此类为 Spring MVC 中HandlerMapping的拓展实现,会自动的添加到HandlerMapping链中。

java
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

	private final RouteLocator routeLocator;
	private final ZuulController zuul;
	private ErrorController errorController;
	private PathMatcher pathMatcher = new AntPathMatcher();
	private volatile boolean dirty = true;

	public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
		this.routeLocator = routeLocator;
		this.zuul = zuul;
		setOrder(-200);
	}
    // ....省略
	private void registerHandlers() {
		Collection<Route> routes = this.routeLocator.getRoutes();
		if (routes.isEmpty()) {
			this.logger.warn("No routes found from RouteLocator");
		}
		else {
			for (Route route : routes) {
				registerHandler(route.getFullPath(), this.zuul);
			}
		}
	}
}

ZuulHandlerMapping其主要目的就是把所有请求路径导入到ZuulController中。另外的功能是当觉察RouteLocator路由表变更,则更新自己dirty状态,重新注册所有RouteZuulController

java
public class ZuulController extends ServletWrappingController {

	public ZuulController() {
	    // 在这里已经设置了ZuulServlet
		setServletClass(ZuulServlet.class);
		setServletName("zuul");
		setSupportedMethods((String[]) null); // Allow all
	}

	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
		try {
			// We don't care about the other features of the base class, just want to handle the request
			// 在这里面会调用ZuulServlet的service方法
			return super.handleRequestInternal(request, response);
		}
		finally {
			RequestContext.getCurrentContext().unset();
		}
	}
}

ZuulController 中的 handleRequest 方法,会调用已经注册的 ZuulServlet 完成业务请求

java
public class ZuulServlet extends HttpServlet {
    // ....省略
    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }
    // ....省略
}

zuul 内置的过滤器

Zuul 默认注入的过滤器可以在 spring-cloud-netflix-core.jar 中找到

Zuul网关的替换方案

Zuul网关存在的问题

  • 性能问题

Zuul 1.x 版本本质上就是一个同步 Servlet,采用多线程阻塞模型进行请求转发。即每来一个请求,Servlet 容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。而 Servlet 容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求

  • 不支持任何长连接,如WebSocket

替换方案 - Zuul2.x版本

Zuul2.x 版本是对 zuul 的升级版,但并没有被 Spring Cloud 收录

替换方案 - Spring Cloud Gateway

Spring Cloud Gateway 是 Spring Cloud 单独推出的微服务网关组件,比 Zuul 1.x 系列的性能和功能整体要好

Spring Cloud Gateway 详细的说明与使用详见《Spring Cloud Gateway 服务网关》笔记

I know where dry desert ends, green grass grows · MooN