一、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線程
//bossGroupworkerGroup,一個用于連接管理,另外一個進行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相關資料感興趣的朋友可以網上了解。






