文章来源:http://www.liangsonghua.me
作者介绍:京东资深工程师 - 梁松华,长期关注稳定性保障、敏捷开发、微服务架构
一、Zuul 简介
Zuul 相当于是第三方调用和服务提供方之间的防护门,其中最大的亮点就是可动态发布过滤器
二、Zuul 可以为我们提供什么
1、权限控制
2、预警和监控
3、红绿部署、(粘性) 金丝雀部署,流量调度支持
4、流量复制转发,方便分支测试、埋点测试、压力测试
5、跨区域高可用,异常感知
6、防爬防攻击
7、负载均衡、健康检查和屏蔽坏节点
8、静态资源处理
9、重试容错服务
三、Zuul 网关架构
可以看到其架构主要分为发布模块、控制管理加载模块、运行时模块、线程安全的请求上下文模块。在 Spring Cloud 中,Zuul 每个后端都称为一个 Route, 为了避免资源抢占,整合了 Hystrix 进行隔离和限流,基于线程的隔离机制,另外一种机制是信号量,后面文章会提到。Zuul 默认使用 ThreadLocal
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
@Override
protected RequestContext initialValue() {
try {return contextClass.newInstance();
} catch (Throwable e) {e.printStackTrace();
throw new RuntimeException(e);
}
}
};
请求处理生命周期,”pre”filters(认证、路由、请求日记)->”routing filters”(将请求发送到后端)->”post”filters(增加 HTTP 头、收集统计和度量、客户端响应)
四、过滤器
一些概念
1、类型 Type, 定义被运行的阶段,也就是 preroutingposterror 阶段
2、顺序 Execution Order, 定义同类型链执行中顺序
3、条件 Criteria, 定义过滤器执行的前提条件
4、动作 Action,定义过滤器执行的业务
下面是一个 DEMO
class DebugFilter extends ZuulFilter {static final DynamicBooleanProperty routingDebug = DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, true)
static final DynamicStringProperty debugParameter = DynamicPropertyFactory.getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "d")
@Override
String filterType() {return 'pre'}
@Override
int filterOrder() {return 1}
boolean shouldFilter() {if ("true".equals(RequestContext.getCurrentContext().getRequest().getParameter(debugParameter.get()))) {return true}
return routingDebug.get();}
Object run() {RequestContext ctx = RequestContext.getCurrentContext()
ctx.setDebugRouting(true)
ctx.setDebugRequest(true)
ctx.setChunkedRequestBody()
return null;
}
五、代码剖析
在 Servlet API 中有一个 ServletContextListener 接口,它能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用的生命周期。接口中定义了两个方法
/**
* 当 Servlet 容器启动 Web 应用时调用该方法。在调用完该方法之后,容器再对 Filter 初始化,* 并且对那些在 Web 应用启动时就需要被初始化的 Servlet 进行初始化。*/
contextInitialized(ServletContextEvent sce)
/**
* 当 Servlet 容器终止 Web 应用时调用该方法。在调用该方法之前,容器会先销毁所有的 Servlet 和 Filter 过滤器。*/
contextDestroyed(ServletContextEvent sce)
在 Zuul 网关中
public class InitializeServletListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent arg0) {
try {
// 实例化
initZuul();} catch (Exception e) {LOGGER.error("Error while initializing zuul gateway.", e);
throw new RuntimeException(e);
}
}
private void initZuul() throws Exception {
// 文件管理
FilterFileManager.init(5, preFiltersPath, postFiltersPath, routeFiltersPath, errorFiltersPath);
// 从 DB 中加载 Filter
startZuulFilterPoller();}
}
在 initZuul 中,FilterFileManager 主要是做文件管理,起一个 poll Thread,定期把 FilterDirectory 中 file 放到 FilterLoader 中,在 FilterLoad 中会进行编译再放到 filterRegistry 中。而 startZuulFilterPoller 主要是判断 DB 中有是否变化或者新增的 Filer, 然后写到 FilterDirectory 中
public boolean putFilter(File file) throws Exception {Class clazz = COMPILER.compile(file);
if (!Modifier.isAbstract(clazz.getModifiers())) {
// 通过反射创建对象,可以对此类一无所知
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
filterClassLastModified.put(sName, file.lastModified());
// 二次 hash 检查
List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
if (list != null) {hashFiltersByType.remove(filter.filterType()); //rebuild this list
}
}
}
过滤器对应 DB 的字段如下 filter_id,revision,create_time,is_active,is_canary,filter_code,filter_type,filter_name,disable_property_name,filter_order,application_name
我们再回到主流程看 ZuulServlet, 每当一个客户请求一个 HttpServlet 对象, 该对象的 service()方法就要被调用, 而且传递给这个方法一个”请求”(ServletRequest)对象和一个”响应”(ServletResponse)对象作为参数
public class ZuulServlet extends HttpServlet {private ZuulRunner zuulRunner = new ZuulRunner();
@Override
public void service(javax.servlet.ServletRequest req, javax.servlet.ServletResponse res) throws javax.servlet.ServletException, java.io.IOException {
try {init((HttpServletRequest) req, (HttpServletResponse) res);
RequestContext.getCurrentContext().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) { } finally {RequestContext.getCurrentContext().unset();}
}
运行时主要从 filterRegistry 根据 type 取出过滤器依次执行
六、Zuul2.x 版本解读
Zuul2.x 的核心功能特性
服务器协议
HTTP/2——完整的入站(inbound)HTTP/ 2 连接服务器支持
双向 TLS(Mutual TLS)——支持在更安全的场景下运行
弹性特性
自适应重试——Netflix 用于增强弹性和可用性的核心重试逻辑
源并发保护——可配置的并发限制,避免源过载,隔离 Zuul 背后的各个源
运营特性
请求 Passport——跟踪每个请求的所有生命周期事件,这对调试异步请求非常有用
状态分类——请求成功和失败的可能状态枚举,比 HTTP 状态码更精细
请求尝试——跟踪每个代理的尝试和状态,对调试重试和路由特别有用
实际上 Zuul2.x 是将 ZuulFilter 变换成 Netty Handler, 在 Netty 中,一系列的 Handler 会聚合在一起并使用 Pipline 执行,拿 Netty 的 Sample 来说明下
//EventLoopGroup 线程组,包含一组 NIO 线程
//bossGroup\workerGroup, 一个用于连接管理,另外一个进行 SocketChannel 的网络读写
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(10240, 0, 2, 0, 2))
.addLast(new StringDecoder(UTF_8))
.addLast(new LengthFieldPrepender(2))
.addLast(new StringEncoder(UTF_8))
.addLast(new ServerHandler());
}
}).childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture future = bootstrap.bind(18080).sync();
在 Zuul2.x 中默认注册了这些 Handler
@Override
protected void initChannel(Channel ch) throws Exception
{
// Configure our pipeline of ChannelHandlerS.
ChannelPipeline pipeline = ch.pipeline();
storeChannel(ch);
addTimeoutHandlers(pipeline);
addPassportHandler(pipeline);
addTcpRelatedHandlers(pipeline);
addHttp1Handlers(pipeline);
addHttpRelatedHandlers(pipeline);
addZuulHandlers(pipeline);
}
我们在上面的 pipeline 中注册了一个 ServerHandler, 这个 handler 就是用来处理 Client 端实际发送的数据的
public class ServerHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelRead0(ChannelHandlerContext ctx, String message) throws Exception {System.out.println("from client:" + message);
JSONObject json = JSONObject.fromObject(message);
String source = json.getString("source");
String md5 = DigestUtils.md5Hex(source);
json.put("md5Hex",md5);
ctx.writeAndFlush(json.toString());//write bytes to socket,and flush(clear) the buffer cache.
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();
ctx.close();}
}
Zuul2.x 相比 1.x 最大的变化就是异步化,最大的功臣莫过于 Netty, 上面涉及到的很重要的就是 ChannelPipleline 和 ChannelFuture
ChannelPipleline 实际上是一个双向链表,提供了 addBeforeaddAfteraddFirstaddLastremove 等方法,链表操作会影响 Handler 的调用关系。ChannelFuture 是为了解决如何获取异步结果的问题而声音设计的接口,有未完成和完成这两种状态,不过通过 CannelFuture 的 get()方法获取结果可能导致线程长时间被阻塞,一般使用非阻塞的 GenericFutureListener
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {ChannelFuture future = ctx.channel().close();
future.addListener(new ChannelFutureListener() {public void operationComplete(ChannelFuture future) {}});
}
点击查阅关于 NIO 和 BIO 的深度解析,Netty 相关资料感兴趣的朋友可以网上了解
BLOG 连接:www.liangsonghua.me
作者介绍:京东资深工程师 - 梁松华,长期关注稳定性保障、敏捷开发、JAVA 高级、微服务架构