kafka叢集Broker端基於Reactor模式請求處理流程深入剖析-kafka商業環境實戰

秦凱新的技術社群發表於2018-12-02

本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。期待加入IOT時代最具戰鬥力的團隊。QQ郵箱地址:1120746959@qq.com,如有任何學術交流,可隨時聯絡。

kafka叢集Broker端基於Reactor模式請求處理流程深入剖析-kafka商業環境實戰

1 Reactor單執行緒案例程式碼熱熱身

  • 如下是單執行緒的JAVA NIO程式設計模型。

  • 首先服務端建立ServerSocketChannel物件,並註冊到Select上OP_ACCEPT事件,然後ServerSocketChannel負責監聽指定埠上的連線請求。

  • 客戶端一旦連線上ServerSocketChannel,就會觸發Acceptor來處理OP_ACCEPT事件,併為來自客戶端的連線建立Socket Channel,並設定為非阻塞模式,並在其Selector上註冊OP_READ或者OP_WRITE,最終實現客戶端與服務端的連線建立和資料通道打通。

  • 當客戶端向建立的SocketChannel傳送請求時,服務端的Selector就會監聽到OP_READ事件,並觸發相應的處理邏輯。當服務端向客戶端寫資料時,會觸發服務端Selector的OP_WRITE事件,從而執行響應的處理邏輯。

  • 這裡有一個明顯的問題,就是所有時間的處理邏輯都是在Acceptor單執行緒完成的,在併發連線數較小,資料量較小的場景下,是沒有問題的,但是……

  • Selector 允許一個單一的執行緒來操作多個 Channel. 如果我們的應用程式中使用了多個 Channel, 那麼使用 Selector 很方便的實現這樣的目的, 但是因為在一個執行緒中使用了多個 Channel, 因此也會造成了每個 Channel 傳輸效率的降低.

  • 優化點在於:通道連線|讀取或寫入|業務處理均採用單執行緒來處理。通過執行緒池或者MessageQueue共享佇列,進一步優化了高併發的處理要求,這樣就解決了同一時間出現大量I/O事件時,單獨的Select就可能在分發事件時阻塞(或延時),而成為瓶頸的問題。

    kafka叢集Broker端基於Reactor模式請求處理流程深入剖析-kafka商業環境實戰
      public class NioEchoServer { 
    private static final int BUF_SIZE = 256;
    private static final int TIMEOUT = 3000;
    public static void main(String args[]) throws Exception {
    // 開啟服務端 Socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 開啟 Selector Selector selector = Selector.open();
    // 服務端 Socket 監聽8080埠, 並配置為非阻塞模式 serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    serverSocketChannel.configureBlocking(false);
    // 將 channel 註冊到 selector 中. // 通常我們都是先註冊一個 OP_ACCEPT 事件, 然後在 OP_ACCEPT 到來時, 再將這個 Channel 的 OP_READ // 註冊到 Selector 中. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
    // 通過呼叫 select 方法, 阻塞地等待 channel I/O 可操作 if (selector.select(TIMEOUT) == 0) {
    System.out.print(".");
    continue;

    } // 獲取 I/O 操作就緒的 SelectionKey, 通過 SelectionKey 可以知道哪些 Channel 的哪類 I/O 操作已經就緒. Iterator<
    SelectionKey>
    keyIterator = selector.selectedKeys().iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    // 當獲取一個 SelectionKey 後, 就要將它刪除, 表示我們已經對這個 IO 事件進行了處理. keyIterator.remove();
    if (key.isAcceptable()) {
    // 當 OP_ACCEPT 事件到來時, 我們就有從 ServerSocketChannel 中獲取一個 SocketChannel, // 代表客戶端的連線 // 注意, 在 OP_ACCEPT 事件中, 從 key.channel() 返回的 Channel 是 ServerSocketChannel. // 而在 OP_WRITE 和 OP_READ 中, 從 key.channel() 返回的是 SocketChannel. SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
    clientChannel.configureBlocking(false);
    //在 OP_ACCEPT 到來時, 再將這個 Channel 的 OP_READ 註冊到 Selector 中. // 注意, 這裡我們如果沒有設定 OP_READ 的話, 即 interest set 仍然是 OP_CONNECT 的話, 那麼 select 方法會一直直接返回. clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE));

    } if (key.isReadable()) {
    SocketChannel clientChannel = (SocketChannel) key.channel();
    ByteBuffer buf = (ByteBuffer) key.attachment();
    long bytesRead = clientChannel.read(buf);
    if (bytesRead == -1) {
    clientChannel.close();

    } else if (bytesRead >
    0) {
    key.interestOps(OP_READ | SelectionKey.OP_WRITE);
    System.out.println("Get data length: " + bytesRead);

    }
    } if (key.isValid() &
    &
    key.isWritable()) {
    ByteBuffer buf = (ByteBuffer) key.attachment();
    buf.flip();
    SocketChannel clientChannel = (SocketChannel) key.channel();
    clientChannel.write(buf);
    if (!buf.hasRemaining()) {
    key.interestOps(OP_READ);

    } buf.compact();

    }
    }
    }
    }複製程式碼

}

2 Kafka Reactor模式設計思路

  • SelectionKey.OP_READ:Socket 讀事件,以從遠端傳送過來了相應資料

  • SelectionKey.OP_WRITE:Socket寫事件,即向遠端傳送資料

  • SelectionKey.OP_CONNECT:Socket連線事件,用來客戶端同遠端Server建立連線的時候註冊到Selector,當連線建立以後,即對應的SocketChannel已經準備好了,使用者可以從對應的key上取出SocketChannel.

  • SelectionKey.OP_ACCEPT:Socket連線接受事件,用來伺服器端通過ServerSocketChannel繫結了對某個埠的監聽,然後會讓其SocketChannel對應的socket註冊到服務端的Selector上,並關注該OP_ACCEPT事件。

  • Kafka的網路層入口類是SocketServer。我們知道,kafka.Kafka是Kafka Broker的入口類,kafka.Kafka.main()是Kafka Server的main()方法,即Kafka Broker的啟動入口。我們跟蹤程式碼,即沿著方法呼叫棧kafka.Kafka.main() ->
    KafkaServerStartable() ->
    KafkaServer().startup可以從main()方法入口一直跟蹤到SocketServer即網路層物件的建立,這意味著Kafka Server啟動的時候會初始化並啟動SocketServer。

  • Acceptor的構造方法中,首先通過openServerSocket()開啟自己負責的EndPoint的Socket,即開啟埠並啟動監聽。然後,Acceptor會負責構造自己管理的一個或者多個Processor物件。其實,每一個Processor都是一個獨立執行緒。

       private[kafka] class Acceptor(val endPoint: EndPoint,                                    val sendBufferSize: Int,                                    val recvBufferSize: Int,                                    brokerId: Int,                                    processors: Array[Processor],                                    connectionQuotas: ConnectionQuotas) extends AbstractServerThread(connectionQuotas) with KafkaMetricsGroup { 
    private val nioSelector = NSelector.open() val serverChannel = openServerSocket(endPoint.host, endPoint.port)//建立一個ServerSocketChannel,監聽endPoint.host, endPoint.port套接字 //Acceptor被構造的時候就會啟動所有的processor執行緒 this.synchronized {
    //每個processor建立一個單獨執行緒 processors.foreach {
    processor =>
    Utils.newThread("kafka-network-thread-%d-%s-%d".format(brokerId, endPoint.protocolType.toString, processor.id), processor, false).start()
    }
    }複製程式碼
  • Acceptor執行緒的run()方法,是不斷監聽對應ServerChannel上的連線請求,如果有新的連線請求,就選擇出一個Processor,用來處理這個請求,將這個新連線交付給Processor是在方法Acceptor.accept()

     def accept(key: SelectionKey, processor: Processor) { 
    val serverSocketChannel = key.channel().asInstanceOf[ServerSocketChannel]//取出channel val socketChannel = serverSocketChannel.accept()//建立socketChannel,專門負責與這個客戶端的連線 try {
    //socketChannel引數設定 processor.accept(socketChannel)//將SocketChannel交給process進行處理
    } catch {
    //異常處理
    }
    } //Processor.accept(): /** * Queue up a new connection for reading */ def accept(socketChannel: SocketChannel) {
    newConnections.add(socketChannel) wakeup()
    }複製程式碼
  • 每一個Processor都維護了一個單獨的KSelector物件,這個KSelector只負責這個Processor上所有channel的監聽。這樣最大程度上保證了不同Processor執行緒之間的完全並行和業務隔離,儘管,在非同步IO情況下,一個Selector負責成百上千個socketChannel的狀態監控也不會帶來效率問題。

       override def run() { 
    startupComplete()//表示初始化流程已經結束,通過這個CountDownLatch代表初始化已經結束,這個Processor已經開始正常執行了 while (isRunning) {
    try {
    // setup any new connections that have been queued up configureNewConnections()//為已經接受的請求註冊OR_READ事件 // register any new responses for writing processNewResponses()//處理響應佇列,這個響應佇列是Handler執行緒處理以後的結果,會交付給RequestChannel.responseQueue.同時呼叫unmute,開始接受請求 poll() //呼叫KSelector.poll(),進行真正的資料讀寫 processCompletedReceives()//呼叫mute,停止接受新的請求 processCompletedSends() processDisconnected()
    } catch {
    //異常處理 略
    } debug("Closing selector - processor " + id) swallowError(closeAll()) shutdownComplete()
    }複製程式碼
  • KSelector.register()方法,開始對遠端客戶端或者其它伺服器的讀請求(OP_READ)進行繫結和處理。KSelect.register()方法,會將服務端的SocketChannel註冊到伺服器端的nioSelector,並關注SelectionKey.OP_READ,即,如果發生讀請求,可以取出對應的Channel進行處理。這裡的Channel也是Kafka經過封裝以後的KafkaChannel物件

      public void register(String id, SocketChannel socketChannel) throws ClosedChannelException { 
    SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP_READ);
    //如果是SocketServer建立的這個物件並且是純文字,則channelBuilder是@Code PlainTextChannelBuilder KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize);
    //構造一個KafkaChannel key.attach(channel);
    //將KafkaChannel物件attach到這個registration,以後可以通過呼叫SelectionKey.attachment()獲得這個物件 this.channels.put(id, channel);
    //記錄這個Channel
    }複製程式碼
  • Processor.processCompletedReceives()通過遍歷completedReceives,對於每一個已經完成接收的資料,對資料進行解析和封裝,交付給RequestChannel,RequestChannel會交付給具體的業務處理層進行處理。

    * 將completedReceived中的物件進行封裝,交付給requestQueue.completRequets */private def processCompletedReceives() { 
    selector.completedReceives.asScala.foreach {
    receive =>
    //每一個receive是一個NetworkReceivedui'xiagn try {
    //receive.source代表了這個請求的傳送者的身份,KSelector儲存了channel另一端的身份和對應的SocketChannel之間的對應關係 val channel = selector.channel(receive.source) val session = RequestChannel.Session(new KafkaPrincipal(KafkaPrincipal.USER_TYPE, channel.principal.getName), channel.socketAddress) val req = RequestChannel.Request(processor = id, connectionId = receive.source, session = session, buffer = receive.payload, startTimeMs = time.milliseconds, securityProtocol = protocol) requestChannel.sendRequest(req)//將請求通過RequestChannel.requestQueue交付給Handler selector.mute(receive.source)//不再接受Read請求,傳送響應之前,不可以再接收任何請求
    } catch {
    //異常處理 略
    }
    }
    }複製程式碼
kafka叢集Broker端基於Reactor模式請求處理流程深入剖析-kafka商業環境實戰
  • 詳情原始碼剖析請參考如下部落格,講解非常詳細。

      https://blog.csdn.net/zhanyuanlin/article/details/76556578  https://blog.csdn.net/zhanyuanlin/article/details/76906583複製程式碼
  • RequestChannel 負責訊息從網路層轉接到業務層,以及將業務層的處理結果交付給網路層進而返回給客戶端。每一個SocketServer只有一個RequestChannel物件,在SocketServer中構造。RequestChannel構造方法中初始化了requestQueue,用來存放網路層接收到的請求,這些請求即將交付給業務層進行處理。同時,初始化了responseQueues,為每一個Processor建立了一個response佇列,用來存放這個Processor的一個或者多個Response,這些response即將交付給網路層返回給客戶端。

      //建立RequestChannel,有totalProcessorThreads個responseQueue佇列,    val requestChannel = new RequestChannel(totalProcessorThreads, maxQueuedRequests)  class RequestChannel(val numProcessors: Int, val queueSize: Int) extends KafkaMetricsGroup { 
    private var responseListeners: List[(Int) =>
    Unit] = Nil //request存放了所有Processor接收到的遠端請求,負責把requestQueue中的請求交付給具體業務邏輯進行處理 private val requestQueue = new ArrayBlockingQueue[RequestChannel.Request](queueSize) //responseQueues存放了所有Processor的帶出來的response,即每一個Processor都有一個response queue private val responseQueues = new Array[BlockingQueue[RequestChannel.Response]](numProcessors) for(i <
    - 0 until numProcessors) //初始化responseQueues responseQueues(i) = new LinkedBlockingQueue[RequestChannel.Response]() //一些metrics用來監控request和response的數量,程式碼略
    }複製程式碼
  • KafkaApis是Kafka的API介面層,可以理解為一個工具類,職責就是解析請求然後獲取請求型別,根據請求型別將請求交付給對應的業務層

      class KafkaRequestHandlerPool(val brokerId: Int,                            val requestChannel: RequestChannel,                            val apis: KafkaApis,                            numThreads: Int) extends Logging with KafkaMetricsGroup { 
    /* a meter to track the average free capacity of the request handlers */ private val aggregateIdleMeter = newMeter("RequestHandlerAvgIdlePercent", "percent", TimeUnit.NANOSECONDS) this.logIdent = "[Kafka Request Handler on Broker " + brokerId + "], " val threads = new Array[Thread](numThreads) //初始化由KafkaRequestHandler執行緒構成的執行緒陣列 val runnables = new Array[KafkaRequestHandler](numThreads) for(i <
    - 0 until numThreads) {
    runnables(i) = new KafkaRequestHandler(i, brokerId, aggregateIdleMeter, numThreads, requestChannel, apis) threads(i) = Utils.daemonThread("kafka-request-handler-" + i, runnables(i)) threads(i).start()
    }複製程式碼
  • KafkaRequestHandler.run()方法,就是不斷從requestQueue中取出請求,呼叫API層業務處理邏輯進行處理

       def run() { 
    while(true) {
    try {
    var req : RequestChannel.Request = null while (req == null) {
    //略 req = requestChannel.receiveRequest(300)//從RequestChannel.requestQueue中取出請求 //略 apis.handle(req)//呼叫KafkaApi.handle(),將請求交付給業務
    } catch {
    }
    }
    }複製程式碼

3 引數調優設定

  • numProcessorThreads:通過num.network.threads進行配置,單個Acceptor所管理的Processor物件的數量。
  • maxQueuedRequests:通過queued.max.requests進行配置,請求佇列所允許的最大的未響應請求的數量,用來給ConnectionQuotas進行請求限額控制,避免Kafka Server產生過大的網路負載;
  • totalProcessorThreads:計算方式為numProcessorThreads * endpoints.size,即單臺機器總的Processor的數量;
  • maxConnectionsPerIp:配置項為max.connections.per.ip,單個IP上的最大連線數,用來給ConnectionQuotas控制連線數;
  • num.io.threads:表示KafkaRequestHander實際從佇列中獲取請求進行執行的執行緒數,預設是8個。

4 總結

  • 通過Acceptor、Processor、RequestChannel、KafkaRequestHandler以及KafkaApis多個角色的解析,完成了整個Kafka的訊息流通閉環,即從客戶端建立連線、傳送請求給Kafka Server的Acceptor進行處理,進一步交由Processor、Kafka Server將請求交付給KafkaRequestHandler具體業務進行處理、業務將處理結果返回給網路層、網路層將結果通過NIO返回給客戶端。

  • 由於多Processor執行緒、以及KafkaRequestHandlerPoll執行緒池的存在,通過交付-獲取的方式而不是阻塞等待的方式,讓整個訊息處理實現完全的非同步化,各個角色各司其職,模組之間無耦合,執行緒之間或者相互競爭任務,或者被上層安排處理部分任務,整個效率非常高,結構也相當清晰

  • 本文參考了大量技術部落格,加上個人的理解,通過走讀原始碼完成這篇學習筆記,辛苦成文,實屬不易,各自珍惜。

  • 秦凱新 於深圳

來源:https://juejin.im/post/5c038fd6e51d45100653e146

相關文章