Flink運行時之基於Netty的網絡通信上

概述

本文以及接下來的幾篇文章將介紹Flink運行時TaskManager間進行數據交換的核心部分——基於Netty通信框架遠程請求ResultSubpartition。作爲系列文章的第一篇,先列出一些需要了解的基礎對象。

NettyConnectionManager

Netty連接管理器(NettyConnectionManager)是連接管理器接口(ConnectionManager)針對基於Netty的遠程連接管理的實現者。它是TaskManager中負責網絡通信的網絡環境對象(NetworkEnvironment)的核心部件之一。

一個TaskManager中可能同時運行着很多任務實例,有時某些任務需要消費某遠程任務所生產的結果分區,有時某些任務可能會生產結果分區供其他任務消費。所以對一個TaskManager來說,其職責並非單一的,它既可能充當客戶端的角色也可能充當服務端角色。因此,一個NettyConnectionManager會同時管理着一個Netty客戶端(NettyClient)和一個Netty服務器(NettyServer)實例。當然除此之外還有一個Netty緩衝池(NettyBufferPool)以及一個分區請求客戶端工廠(PartitionRequestClientFactory,用於創建分區請求客戶端PartitionRequestClient),這些對象都在NettyConnectionManager構造器中被初始化。

每個PartitionRequestClientFactory實例都依賴一個NettyClient。也就是說所有PartitionRequestClient底層都共用一個NettyClient。

Netty客戶端和服務器對象的啓動和停止都是由NettyConnectionManager統一控制的。NettyConnectionManager啓動的時機是當TaskManager跟JobManager關聯上之後調用NetworkEnvironment的associateWithTaskManagerAndJobManager方法時。而當TaskManager跟JobManager解除關聯時停止。

NettyBufferPool

NettyClient和NettyServer在實例化Netty通信的核心對象時都需要配置各自的“字節緩衝分配器”用於爲Netty讀寫數據分配內存單元。Netty自身提供了一個池化的字節緩衝分配器(PooledByteBufAllocator),但Flink又在此基礎上進行了包裝並提供了Netty緩衝池(NettyBufferPool)。此舉的目的是嚴格控制所創建的分配器(Arena)的個數,轉而依賴TaskManager的相關配置指定。

什麼是Arena?當指定PooledByteBufAllocator來執行ByteBuf分配時,最終的內存分配工作被委託給類PoolArena。由於Netty通常用於高併發系統,所以各個線程進行內存分配時競爭不可避免,這可能會極大的影響內存分配的效率,爲了緩解高併發時的線程競爭,Netty允許使用者創建多個分配器(Arena)來分離鎖,提高內存分配效率。

NettyBufferPool在構造器內部以固定的參數實例化PooledByteBufAllocator並作爲自己的內部分配器。具體做了哪些限制呢?首先,PooledByteBufAllocator本身既支持堆內存分配也支持堆外內存分配,NettyBufferPool將其限定爲只在堆外內存上進行分配。其次, 顯式指定了pageSize大小爲8192,maxOrder值爲11。這兩個參數是什麼意思呢?Netty中的內存池包含頁(page)和塊(chunk)兩種分配單位,通過PooledByteBufAllocator構造器可以設置頁大小(也即pageSize參數),該參數在PooledByteBufAllocator中的默認值爲8192,而參數maxOder則用於計算塊的大小。

計算公式爲:chunkSize = pageSize << maxOrder;因此這裏塊大小爲16MB。

另外,NettyBufferPool通過反射還拿到了PooledByteBufAllocator中的PoolArena分配器對象集合,但此舉更多的是出於調試目的。並且顯式關閉了對堆內存相關的操作方法。

NettyClient

NettyClient的主要職責是初始化Netty客戶端的核心對象,並根據NettyProtocol配置用於客戶端事件處理的ChannelPipeline。

NettyClient並不用於發起遠程結果子分區請求,該工作將由PartitionRequestClient完成。

一個Netty引導客戶端的創建步驟如下:

  • 創建Bootstrap對象用來引導啓動客戶端:
bootstrap = new Bootstrap();
  • 創建NioEventLoopGroup或EpollEventLoopGroup對象並設置到Bootstrap中,EventLoopGroup可以理解爲是一個線程池,用來處理連接、接收數據、發送數據:
switch (config.getTransportType()) {
    case NIO:
        initNioBootstrap();
        break;

    case EPOLL:
        initEpollBootstrap();
        break;

    case AUTO:
        if (Epoll.isAvailable()) {
            initEpollBootstrap();
            LOG.info("Transport type 'auto': using EPOLL.");
        }
        else {
            initNioBootstrap();
            LOG.info("Transport type 'auto': using NIO.");
        }
}

Netty自版本4.0.16開始,對於Linux系統提供原生的套接字通信傳輸支持(也即,epoll機制,藉助於JNI調用),這種傳輸機制擁有更高的性能。

  • 進行一系列配置,並設置ChannelHandler用來處理邏輯:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel channel) throws Exception {
        channel.pipeline().addLast(protocol.getClientChannelHandlers());
    }
});

注意以上設置的是基於NettyPotocol獲得的一個ChannelHandler數組組成的管道。

  • 調用Bootstrap.connect()來連接服務器:
return bootstrap.connect(serverSocketAddress);

以上就是一個Netty客戶端從初始化到跟服務器建立連接的大致過程。但這裏需要注意的是,一個TaskManager根本上只會存在一個NettyClient對象(對應的也只有一個Bootstrap實例)。但一個TaskManager中的子任務實例很有可能會跟多個不同的遠程TaskManager通信,所以同一個Bootstrap實例可能會跟多個目標服務器建立連接,所以它是複用的,這一點不存在問題因爲無論跟哪個目標服務器通信,Bootstrap的配置都是不變的。至於不同的RemoteChannel如何跟某個連接建立對應關係,這一點由PartitionRequestClientFactory來保證。

Netty自版本4.0.16開始,對於Linux系統提供原生的套接字通信傳輸支持(也即,epoll機制,藉助於JNI調用),這種傳輸機制擁有更高的性能。

NettyServer

跟NettyClient一樣,NettyServer也會初始化Netty服務端的核心對象,除此之外它會啓動對特定端口的偵聽並準備接收客戶端發起的請求。下面是NettyServer的初始化與啓動步驟:

  • 創建ServerBootstrap實例來引導綁定和啓動服務器:
bootstrap = new ServerBootstrap();
  • 根據配置創建NioEventLoopGroup或EpollEventLoopGroup對象來處理事件,如接收新連接、接收數據、寫數據等等:
switch (config.getTransportType()) {
    case NIO:
        initNioBootstrap();
        break;

    case EPOLL:
        initEpollBootstrap();
        break;

    case AUTO:
        if (Epoll.isAvailable()) {
            initEpollBootstrap();
            LOG.info("Transport type 'auto': using EPOLL.");
        }
        else {
            initNioBootstrap();
            LOG.info("Transport type 'auto': using NIO.");
        }
}
  • 指定InetSocketAddress,服務器監聽此端口:
bootstrap.localAddress(config.getServerAddress(), config.getServerPort());
  • 進行各種參數配置,設置childHandler執行所有的連接請求:
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel channel) throws Exception {
        channel.pipeline().addLast(protocol.getServerChannelHandlers());
    }
});

注意以上設置的是基於NettyPotocol獲得的一個ChannelHandler數組組成的管道。

  • 都設置完畢了,最後調用ServerBootstrap.bind()方法來綁定服務器:
bindFuture = bootstrap.bind().syncUninterruptibly();

微信掃碼關注公衆號:Apache_Flink

apache_flink_weichat


QQ掃碼關注QQ羣:Apache Flink學習交流羣(123414680)

qrcode_for_apache_flink_qq_group

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章