本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。期待加入IOT時代最具戰鬥力的團隊。QQ郵箱地址:1120746959@qq.com,如有任何學術交流,可隨時聯絡。
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就可能在分發事件時阻塞(或延時),而成為瓶頸的問題。
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 {
//異常處理 略
}
}
}複製程式碼
-
詳情原始碼剖析請參考如下部落格,講解非常詳細。
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執行緒池的存在,通過交付-獲取的方式而不是阻塞等待的方式,讓整個訊息處理實現完全的非同步化,各個角色各司其職,模組之間無耦合,執行緒之間或者相互競爭任務,或者被上層安排處理部分任務,整個效率非常高,結構也相當清晰
-
本文參考了大量技術部落格,加上個人的理解,通過走讀原始碼完成這篇學習筆記,辛苦成文,實屬不易,各自珍惜。
-
秦凱新 於深圳