Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Cloud教程 - API网关服务-Zuul #15

Open
TFdream opened this issue Jul 20, 2020 · 0 comments
Open

Spring Cloud教程 - API网关服务-Zuul #15

TFdream opened this issue Jul 20, 2020 · 0 comments

Comments

@TFdream
Copy link
Owner

TFdream commented Jul 20, 2020

Zuul

Zuul 也是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器。提供了路由、监控、弹性、安全等服务。Zuul 能够与 Eureka、Ribbon、Hystrix 等组件配合使用。

image

Zuul 于 2012 年开源,目前在 GitHub 上有超过 8000 多颗星的关注,经过 Netflix 在生产环境中长期的使用和改进,Zuul 的稳定性非常好。

你现在看到的是 Zuul 在 Netflix 内部使用的一张架构图,从图中可以看出,最上层的客户端通过AWS 的负载均衡器把请求路由到 Zuul 网关上,然后 Zuul 网关负责把请求路由到后端具体的服务上。

过滤器

过滤器是 Zuul 中最核心的内容,过滤器可以对请求或响应结果进行处理,Zuul 还支持动态加载、编译、运行这些过滤器,过滤器的使用方式是采取责任链的方式进行处理,过滤器之间通过 RequestContext 来传递上下文,通过过滤器可以扩展很多高级功能。Zuul 中的过滤器总共有 4 种类型,每种类型都有对应的使用场景。

  • pre 过滤器:可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。
  • route 过滤器:在路由请求时被调用。适用于灰度发布的场景,在将要路由的时候可以做一些自定义的逻辑。
  • post 过滤器:在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于添加响应头,记录响应日志等应用场景。
  • error 过滤器:处理请求发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。

请求生命周期

当一个请求进来时,会先进入 pre 过滤器,在 pre 过滤器执行完后,接着就到了 routing 过滤器中,开始路由到具体的服务中,路由完成后,接着就到了 post 过滤器中,然后将请求结果返回给客户端。如果在这个过程中出现异常,则会进入 error 过滤器中,这就是请求在整个 Zuul 中的生命周期。

image

对应的源码在 com.netflix.zuul.http.ZuulServlet 中,我们可以打开 ZuulServlet 的源码,service 方法中就是执行过滤器的逻辑,首先是 preRoute 方法,也就是执行 pre 过滤器,如果异常了就会执行 error 过滤器和 post 过滤器,接着就是 routing 过滤器,这就是整个过滤器执行流程对应的源码部分。


    @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();
        }
    }

具体可以在 ZuulProxyAutoConfiguration -> ZuulServerAutoConfiguration 中自动注入 zuulServlet。

	@Bean
	@ConditionalOnMissingBean(name = "zuulServlet")
	@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false",
			matchIfMissing = true)
	public ServletRegistrationBean zuulServlet() {
		ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(
				new ZuulServlet(), this.zuulProperties.getServletPattern());
		// The whole point of exposing this servlet is to provide a route that doesn't
		// buffer requests.
		servlet.addInitParameter("buffer-requests", "false");
		return servlet;
	}

Zuul 的使用

我们快速体验下 Zuul 的路由功能,首先在 pom 中增加 spring-cloud-starter-netflix-zuul 的依赖,加完依赖之后,在启动类上增加 @EnableZuulProxy 注解。

跨域配置

关闭 zuul 全局路由转发

Zuul 中会默认为 Eureka 中所有的服务都进行路由转发,这种方式确实很方便,相当于我们不需要配置路由规则就可以直接使用默认的服务名称加 API 的 URI 方式去访问,不好的点在于 Eureka 中的服务是全量的,我们的某个网关对外提供服务,并不需要将所有的 API 都暴露给外部,但是默认的映射会让外部可以访问到,所以我们需要将这个默认的路由转发关闭,通过配置 zuul.ignored-services=* 关闭。

关闭后 Zuul 就只会根据我们配置的路由规则去转发对应的请求了,这样就避免了不想暴露的服务也被外部调用。

再细一点可能会遇到在某个服务中,有的 API 想暴露,有的不想暴露,这种需求有两种方式,一种是增加一个聚合层,通过聚合层来暴露对应的 API,对于多个 API 需要聚合的场景,使用聚合层是非常合理的,但是对于简单的 API,并不需要聚合数据,再加聚合层的话无疑是多了一次转发,影响了性能。

在 Zuul 中有个配置可以忽略指定的 URI 地址,可以通过配置 zuul.ignoredPatterns 来忽略你不想暴露的 API。

动态过滤器

Zuul 支持过滤器动态修改加载功能,Filter 需要使用 Groovy 编写才可以被动态加载。动态加载的实现原理是定期扫描存放 Groovy Filter 文件的目录,如果发现有新 Groovy Filter 文件或者 Groovy Filter 源码有改动,那么就会对 Groovy 文件进行编译加载。

final String scriptRoot = System.getProperty("zuul.filter.root");
try {
    FilterFileManager.setFilenameFilter(new GroovyFileFilter());
    FilterFileManager.init(5,
            scriptRoot + "/pre",
            scriptRoot + "/route",
            scriptRoot + "/post");
} catch (Exception e) {
    throw new RuntimeException(e);
}

要实现动态过滤器,首先需要在项目中增加 Groovy 的依赖,然后在项目启动后设置 Groovy 的动态加载任务,这样就会定时的动态加载指定目录的 Groovy 文件了。

然后编写一个简单的 Groovy Filter,在 run 中输出一句话即可,然后访问下网关的接口,可以看到这个动态的过滤器生效了。有了动态过滤器的功能,我们就可以在不用停止服务的情况下,去支持需求的变化。

Zuul 控制路由实例选择

前面在讲到网关的必要性时,提到了基于网关去做灰度发布,去做压力测试等高级扩展功能,在后面的课时中我会单独介绍使用目前已经开源的组件来实现灰度发布的功能,在这里就不做过多的介绍,只是想让大家了解下如果要实现这些扩展能力,我们需要做哪些工作?最重要的是要了解核心原理,当你了解了核心原理后,也就相当于有了深厚的内力,怎么表现出来就只是表面上的招式而已。

我们来看灰度发布,首先我们需要知道当前请求的目的地是什么。也就是当前请求是正常请求还是一个灰度请求,如果是灰度请求,那么这个请求想要访问的版本是什么?或者想要访问指定的哪个服务实例等。

然后我们需要根据这个请求带来的信息,从 Eureka 中选择一个符合要求的实例信息给 Zuul 进行转发,总体需求就是这两点,那么我们该用什么技术呢?

Zuul 中也是集成了 Ribbon 来做负载均衡的,Ribbon 中又提供了自定义算法策略来让我们控制服务实例的选择,技术方案很明显我们需要自定义 Ribbon 的算法策略来实现这个需求。

这边需要注意的是获取 request 只能在信号量隔离下使用,线程隔离下 ThreadLocal 无法使用,会触发空指针异常。解决方案大家可以参考我的这篇文章,在 Hystrix 课时中也讲到过这个问题。除了文章中介绍的解决方案,还有其他的方案也可以实现这个需求,这个在后面专门讲灰度发布的时候给大家分析如何跨线程池传递数据到 Hystrix 中。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant