给你一台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读写操作。具体实现我们后续再做分析。