SpringCloud微服务实战(十一) - Zuul及其实现原理

195 阅读9分钟

1 简介

1.1 网关服务的意义

点对点

服务之间直接调用,每个微服务都开放Rest API,并调用其他微服务的接口

API网关(应用最广泛的方式)

当服务数量增多,需要一个角色充当 request 请求的统一入口,即服务网关。 业务接口通过API网关暴露,是所有客户端接口的唯一入口。 微服务之间的通信也通过API网关。

1.2 网关要素

  • 稳定性,高可用
  • 安全性
  • 性能、并发性
  • 扩展性

网关是具体核心业务服务的看门神,相比于具体实现业务的系统服务,它是一个边缘服务,主要提供动态路由,监控,弹性,安全性等功能。 下面我们从单体应用到多体应用的演化过程来讲解网关的演化历程。

一般业务系统发展历程都是基本相似的,从单体应用到多应用,从本地调用到远程调用。

单体应用

由于只需一个应用,所有业务模块的功能都打包为了一个 War 包进行部署,这样可以减少机器资源和部署的繁琐。

单体应用中,网关模块和应用部署到同一JVM进程,当外部移动设备或者web站点访问单体应用的功能时,请求是先被应用的网关模块拦截,网关模块对请求进行鉴权、限流等动作后在把具体的请求转发到当前应用对应的模块处理。

多体应用

随着业务发展,网站流量越来越大,在单体应用中简单的通过加机器的方式可以带来的承受流量冲击的能力也越来越低,这时候就会考虑根据业务将单体应用拆成若干功能独立应用,单体应用拆为多个应用后,由于不同的应用开发对应的功能,所以多应用开发之间可以独立开发而不用去理解对方的业务,另外不同的应用模块只承受对应业务流量的压力,不会对其他应用模块造成影响,这时多体的分布式系统就出现了,如下

多体应用中业务模块A和B单独启个应用,每个应用里有自己的网关模块。 如果业务模块太多,每个应用都有自己的网关模块,复用性不好,考虑把网关模块提出来,单独作为一个应用做服务路由,如下

如上图当移动设备发起请求时候是具体发送到网关应用的,经过鉴权后请求会被转发到具体的后端服务应用上,对应前端移动设备来说他们不在乎也不知道后端服务器应用是一个还是多个,他们只能感知到网关应用的存在。

1.3 常用网关方案

  • Nginx + Lua
  • Tyk
  • Kong
  • Spring Cloud Zuul

1.4 Zuul 的特点

Zuul 网关是具体核心业务服务的看门神,相比具体实现业务的系统服务来说它是一个边缘服务,主要提供动态路由,监控,弹性,安全性等功能。在分布式的微服务系统中,系统被拆为了多套系统,通过zuul网关来对用户的请求进行路由,转发到具体的后台服务系统中。

  • 路由+过滤器= Zuul 核心是一系列的过滤器

1.5 四种过滤器

在zuul中过滤器分为四种:

  1. PRE Filters(前置过滤器) 当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤器,日志过滤器,还有路由选择过滤器
  2. ROUTING Filters (路由过滤器) 一般通过Apache HttpClient 或者 Netflix Ribbon把请求具体转发到后端服务器
  3. POST Filters(后置过滤器) 当把请求路由到具体后端服务器后执行的过滤器;场景有添加标准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等
  4. ERROR Filters(错误过滤器) 当上面任何一个类型过滤器执行出错时候执行该过滤器

1.6 架构图

Zuul网关核心是一系列过滤器,可以对请求或响应结果做过滤。 Zuul 提供一个框架可支持动态加载,编译,运行这些过滤器,这些过滤器通过责任链方式顺序处理请求或者响应结果。过滤器之间不会直接通信,但可通过责任链传递的RequestContext参数共享一些信息。

虽然Zuul 支持任何可以在jvm上跑的语言,但是目前zuul的过滤器只能使用Groovy脚本来编写。编写好的过滤器脚本一般放在zuul服务器的固定目录,zuul服务器会开启一个线程定时去轮询被修改或者新增的过滤器,然后动态进行编译,加载到内存,然后等后续有请求进来,新增或者修改后的过滤器就会生效了。

1.7 请求生命周期

Zuul接收到请求后

  1. 首先会由前置过滤器处理
  2. 然后在由路由过滤器具体把请求转发到后端应用
  3. 然后在执行后置过滤器把执行结果写回请求方
  4. 当上面任何一个类型过滤器执行出错时候执行错误过滤器

1.8 核心处理流程 - ZuulServlet类

在Zuul1.0中最核心的是ZuulServlet类,该类是个servlet,用来对匹配条件的请求执行核心的 pre, routing, post过滤器。

  • 该类核心时序图

上图可知当请求过来后,先后执行了FilterProcessor管理的三种过滤器:

public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
            ...
            try {
            // 1 前置过滤器
                preRoute();
            } catch (ZuulException e) {
                // 1.1 错误过滤器
                error(e);
                // 1.2 后置过滤器
                postRoute();
                return;
            }
            try {
                // 2 路由过滤器
                route();
            } catch (ZuulException e) {
                // 2.1 错误过滤器
                error(e);
                // 2.2 后置过滤器
                postRoute();
                return;
            }
            try {
                // 3 后置过滤器
                postRoute();
            } catch (ZuulException e) {
                // 3.1 错误过滤器
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
        }
    }

如果在三种过滤器执行过程中发生了错误,会执行error(e),该方法执行错误过滤器,注意如果在pre、route过滤器执行过程中出现错误,在执行错误过滤器后还需再执行后置过滤器。

FilterProcessor#runFilters

执行具体过滤器:

public Object runFilters(String sType) throws Throwable {
        ...
        boolean bResult = false;
        // 2 获取sType类型的过滤器
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                // 2.1具体执行过滤器
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

代码2是具体获取不同类型的过滤器 代码2.1是具体执行过滤器 我们看下代码2看FilterLoader类的getFiltersByType方法如何获取不同类型的过滤器:

FilterLoader#getFiltersByType

 public List<ZuulFilter> getFiltersByType(String filterType) {

        // 3 分类缓存是否存在
        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        list = new ArrayList<ZuulFilter>();
        // 4 获取所有过滤器
        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                list.add(filter);
            }
        }
        Collections.sort(list); // sort by priority
        // 5 保存到分类缓存
        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }
    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
  • 代码3首先看分类缓存里是否有该类型过滤器,有直接返回
  • 否则执行代码4获取所有注册过滤器,然后从中过滤出当前需要类别
  • 然后缓存到分类过滤器

看到这里想必大家知道FilterRegistry类是存放所有过滤器的类,FilterRegistry里面 : private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();存放所有注册的过滤器,那么这些过滤器什么时候放入的那?这个我们后面讲解。

这里我们剖析了如何获取具体类型的过滤器的,下面回到代码2.1看如何执行过滤器的:

 public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        final String metricPrefix = "zuul.filter-";
        long execTime = 0;
        String filterName = "";
        try {
            ...
            // 5 执行过滤器
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;

            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    
                    break;
                default:
                    break;
            }
            
            if (t != null) throw t;

            usageNotifier.notify(filter, s);
            return o;

        } catch (Throwable e) {
          ...
        }
    }

1.9 Zuul 2.0 服务架构新特性

zuul2.0使用netty server作为网关监听服务器监听客户端发来的请求,然后把请求转发到前置过滤器(inbound filters)进行处理,处理完毕后在把请求使用netty client代理到具体的后端服务器进行处理,处理完毕后在把结果交给后者过滤器(outbound filters)进行处理,然后把处理结果通过nettyServer写回客户端。

在zuul1.0时候客户端发起的请求后需要同步等待zuul网关返回,zuul网关这边对每个请求会分派一个线程来进行处理,这会导致并发请求数量有限。 而zuul2.0使用netty作为异步通讯,可以大大加大并发请求量。

2 实践

新建
注意端口

  • 嵌入式Zuul反向代理 Spring Cloud已经创建了一个嵌入式Zuul代理,以简化UI应用程序想要代理对一个或多个后端服务的调用的非常常见的用例的开发。此功能对于用户界面对其所需的后端服务进行代理是有用的,避免了对所有后端独立管理CORS和验证问题的需求。

要启用它,使用@EnableZuulProxy注释Spring Boot主类,并将本地调用转发到相应的服务

按照惯例,ID为“用Users”的服务将接收来自位于/users的代理(具有前缀stripped)的请求。 代理使用Ribbon来定位要通过发现转发的实例,并且所有请求都以hystrix命令执行,所以故障将显示在Hystrix指标中,一旦电路打开,代理将不会尝试联系服务。
添加注解
Zuul启动器不包括发现客户端,因此对于基于服务ID的路由,还需要在类路径中提供其中一个路由(例如Eureka)
欲获取 Product 服务的 list 接口
先将 Product 服务启动