服务网关和 Zuul
0 简介
0.1 为什么需要网关服务
当服务数量增多,需要一个角色充当 request 请求的统一入口,即服务网关
0.2 网关的要素
网关是具体核心业务服务的看门神,相比具体实现业务的系统服务来说它是一个边缘服务,主要提供动态路由,监控,弹性,安全性等功能,下面我们从单体应用到多体应用的演化过程来讲解网关的演化历程。
一般业务系统发展历程都是基本相似的,从单体应用到多应用,从本地调用到远程调用。对应单体应用架构模式,由于只需一个应用,所有业务模块的功能都打包为了一个 War 包进行部署,这样可以减少机器资源和部署的繁琐。
在单体应用中,网关模块是和应用部署到同一个jvm进程里面的,当外部移动设备或者web站点访问单体应用的功能时候,请求是先被应用的网关模块拦截的,网关模块对请求进行鉴权、限流等动作后在把具体的请求转发到当前应用对应的模块进行处理。
随着业务的发展,网站的流量会越来越大,在单体应用中简单的通过加机器的方式可以带来的承受流量冲击的能力也越来越低,这时候就会考虑根据业务将单体应用拆成若干个功能独立的应用,单体应用拆为多个应用后,由于不同的应用开发对应的功能,所以多应用开发之间可以独立开发而不用去理解对方的业务,另外不同的应用模块只承受对应业务流量的压力,不会对其他应用模块造成影响,这时候多体的分布式系统就出现了,如下
如上图在多体应用中业务模块A和B单独起了个应用,每个应用里面有自己的网关模块,如果业务模块多了,那么每个应用都有自己的网关模块,这样复用性不好,所以可以考虑把网关模块提起出来,单独作为一个应用来做服务路由,如下
如上图当移动设备发起请求时候是具体发送到网关应用的,经过鉴权后请求会被转发到具体的后端服务应用上,对应前端移动设备来说他们不在乎也不知道后端服务器应用是一个还是多个,他们只能感知到网关应用的存在。
0.3 常用的网关方案
0.4 Zuul 的特点
Zuul 网关是具体核心业务服务的看门神,相比具体实现业务的系统服务来说它是一个边缘服务,主要提供动态路由,监控,弹性,安全性等功能。在分布式的微服务系统中,系统被拆为了多套系统,通过zuul网关来对用户的请求进行路由,转发到具体的后台服务系统中。
0.5 四种过滤器 API
在zuul中过滤器分为四种:
PRE Filters(前置过滤器) :当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤器,日志过滤器,还有路由选择过滤器
ROUTING Filters (路由过滤器):该过滤器作用是把请求具体转发到后端服务器上,一般是通过Apache HttpClient 或者 Netflix Ribbon把请求发送到具体的后端服务器上
POST Filters(后置过滤器):当把请求路由到具体后端服务器后执行的过滤器;场景有添加标准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等。
ERROR Filters(错误过滤器):当上面任何一个类型过滤器执行出错时候执行该过滤器
0.6 架构图
Zuul网关的核心是一系列的过滤器,这些过滤器可以对请求或者响应结果做一系列过滤,Zuul 提供了一个框架可以支持动态加载,编译,运行这些过滤器,这些过滤器是使用责任链方式顺序对请求或者响应结果进行处理的,这些过滤器直接不会直接进行通信,但是通过责任链传递的RequestContext参数可以共享一些东西。
虽然Zuul 支持任何可以在jvm上跑的语言,但是目前zuul的过滤器只能使用Groovy脚本来编写。编写好的过滤器脚本一般放在zuul服务器的固定目录,zuul服务器会开启一个线程定时去轮询被修改或者新增的过滤器,然后动态进行编译,加载到内存,然后等后续有请求进来,新增或者修改后的过滤器就会生效了。
0.7 请求生命周期
当Zuul接受到请求后,首先会由前置过滤器进行处理,然后在由路由过滤器具体把请求转发到后端应用,然后在执行后置过滤器把执行结果写会到请求方,当上面任何一个类型过滤器执行出错时候执行该过滤器。
0.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方法如何获取不同类型的过滤器:
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) {
...
}
}
0.9 Zuul 2.0 服务架构新特性
- 新特性
Netty作为高性能异步网络通讯框架,在dubbo,rocketmq,sofa等知名开源框架中都有使用,如下图zuul2.0使用netty server作为网关监听服务器监听客户端发来的请求,然后把请求转发到前置过滤器(inbound filters)进行处理,处理完毕后在把请求使用netty client代理到具体的后端服务器进行处理,处理完毕后在把结果交给后者过滤器(outbound filters)进行处理,然后把处理结果通过nettyServer写回客户端。
...
总: 在zuul1.0时候客户端发起的请求后需要同步等待zuul网关返回,zuul网关这边对每个请求会分派一个线程来进行处理,这会导致并发请求数量有限。而zuul2.0使用netty作为异步通讯,可以大大加大并发请求量。
1 实践
新建注意端口
- 嵌入式Zuul反向代理
Spring Cloud已经创建了一个嵌入式Zuul代理,以简化UI应用程序想要代理对一个或多个后端服务的调用的非常常见的用例的开发。此功能对于用户界面对其所需的后端服务进行代理是有用的,避免了对所有后端独立管理CORS和验证问题的需求。
要启用它,使用@EnableZuulProxy
注释Spring Boot主类,并将本地调用转发到相应的服务
按照惯例,ID为“用Users”的服务将接收来自位于
/users
的代理(具有前缀stripped)的请求。代理使用Ribbon来定位要通过发现转发的实例,并且所有请求都以hystrix命令执行,所以故障将显示在Hystrix指标中,一旦电路打开,代理将不会尝试联系服务。
添加注解
Zuul启动器不包括发现客户端,因此对于基于服务ID的路由,还需要在类路径中提供其中一个路由(例如Eureka)
欲获取 Product 服务的 list 接口
先将 Product 服务启动