給你一台4路E7-4820V2(32核心64線程),512G內存的服務器,你該如何編程才能支持百萬長連接?
最直接的想法是采用BIO的模式,為每個連接新建一個線程,在一一對應的線程中直接處理連接上的數據請求。
但在Java中,新建線程的開銷非常昂貴(默認情況下每個線程會占據1M多的內存,百萬連接就是1T內存,這顯然是不可接受的)
優化點的想法是使用Java NIO,用一個線程來處理所有客戶端的請求。
但是根據我之前的測試,單個線程最多同時處理5w/s的echo message,此時單個core已經跑滿,如果再接着加大負載會導致請求堆積。
進一步的優化是將線程分離,使用一個線程作為acceptor,一堆線程作為worker
acceptor監聽服務端口的accept事件,如果有accept事件被觸發,說明有客戶端連接進來,acceptor獲取連接(Channel)並將其分派給某個worker,worker監聽這個Channel的read事件,一旦Channel可讀,worker就會做出相應的處理。
也就是說將連接均分到各個worker,減輕壓力,也可以讓多個core被利用起來,使單機處理百萬長連接成為可能。
這就是所謂Reactor模型了,也是Netty所采用的線程模型。(還有更進一步的主從多線程模型,用於處理認證較為耗時的情況,這里不做介紹)
借用一下Doug Lea老爺子的示例圖:
用這個思想分析一下Netty的示例代碼:
public void go(int port) { EventLoopGroup bossGroup = new NioEventLoopGroup(1);//acceptor線程 EventLoopGroup workerGroup = new NioEventLoopGroup();//worker線程組 try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { //ehco to client ctx.write(msg); ctx.flush(); } }); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } catch (Exception e) { } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }
可以猜出所謂的bossGroup就是Reactor模型中的acceptor,負責處理客戶端產生的TCP連接請求,workerGroup則是worker,真正負責IO讀寫操作。具體實現我們后續再做分析。