微服務架構之網關層Zuul剖析

clipboard.png

文章來源:http://www.liangsonghua.me
做者介紹:京東資深工程師-梁鬆華,長期關注穩定性保障、敏捷開發、微服務架構java

1、Zuul簡介
Zuul至關因而第三方調用和服務提供方之間的防禦門,其中最大的亮點就是可動態發佈過濾器json

2、Zuul能夠爲咱們提供什麼
一、權限控制
二、預警和監控
三、紅綠部署、(粘性)金絲雀部署,流量調度支持
四、流量複製轉發,方便分支測試、埋點測試、壓力測試
五、跨區域高可用,異常感知
六、防爬防攻擊
七、負載均衡、健康檢查和屏蔽壞節點
八、靜態資源處理
九、重試容錯服務
3、Zuul網關架構bootstrap

clipboard.png

能夠看到其架構主要分爲發佈模塊、控制管理加載模塊、運行時模塊、線程安全的請求上下文模塊。在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);
      }
   }
};

clipboard.png

請求處理生命週期,」pre」 filters(認證、路由、請求日記)->」routing filters」(將請求發送到後端)->」post」 filters(增長HTTP頭、收集統計和度量、客戶端響應)安全

4、過濾器
一些概念服務器

一、類型Type,定義被運行的階段,也就是preroutingposterror階段
二、順序Execution Order,定義同類型鏈執行中順序
三、條件Criteria,定義過濾器執行的前提條件
四、動做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;
    }

5、代碼剖析
在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中app

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取出過濾器依次執行

6、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高級、微服務架構

clipboard.png

相關文章
相關標籤/搜索