文章首发于公众号:松花皮蛋的黑板报
作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解
一、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高级、微服务架构
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。