单机百万连接调优
实现单机的百万连接,瓶颈有以下几点:
- 如何模拟百万连接
- 突破局部文件句柄的限制
- 突破全局文件句柄的限制
在linux系统里面,单个进程打开的句柄数是非常有限的,一条TCP连接就对应一个文件句柄,而对于我们应用程序来说,一个服务端默认建立的连接数是有限制的。
如何模拟百万连接
如上图所示,当服务端开启一个端口,客户端去连接,除去固定的端口,最多只能实现单机6W的连接,实现单机百万连接,最简单的方法,就是启动十几个客户端,然后去连接同一个端口,但是比较麻烦的。
在服务端启动800~8100,而客户端依旧使用1025-65535范围内可用的端口号,让同一个端口号,可以连接Server的不同端口。这样的话,6W的端口可以连接Server的100个端口,累加起来就能实现近600W左右的连接,TCP是以一个四元组概念,以原IP、原端口号、目的IP、目的端口号来确定的,所以TCP连接可以如此设计。
突破局部文件句柄的限制
- ulimit -n
- /etc/security/limits.conf
首先在终端输入ulimit -n,查看一个进程能够打开的最大文件数,一条TCP连接,对应Linux系统里面是一个文件,所以服务端最大连接数会受限于这个数字,然后,在/etc/security/limits.conf文件中配置如下两行:
- hard nofile 1000000
- soft nofile 1000000
soft和hard为两种限制方式,其中soft表示警告的限制,hard表示真正限制,nofile表示打开的最大文件数。
突破全局文件句柄的限制
- cat /proc/sys/fs/file-max
- etc/sysctl.conf
cat /proc/sys/fs/file-max查看我所有进程能够打开的最大文件数是多少,TCP连接,每一个连接代表一个文件,局部的不能大过全局的限制,然后进入etc/sysctl.conf,在该配置文件中添加fs.file-max = 1000000,file-max表示全局文件句柄数的限制,这里我设置为100W。然后,我通过简单DEMO进行模仿连接,最终结果大约在94W左右。
实例代码
Client端
public class Client {
private static final String SERVER_HOST = "192.168.1.42";
public static void main(String[] args) {
new Client().start(BEGIN_PORT, N_PORT);
}
public void start(final int beginPort, int nPort) {
System.out.println("client starting....");
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
final Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_REUSEADDR, true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
}
});
int index = 0;
int port;
while (!Thread.interrupted()) {
port = beginPort + index;
try {
ChannelFuture channelFuture = bootstrap.connect(SERVER_HOST, port);
channelFuture.addListener((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
System.out.println("connect failed, exit!");
System.exit(0);
}
});
channelFuture.get();
} catch (Exception e) {
}
if (++index == nPort) {
index = 0;
}
}
}
}
Server端
public final class Server {
public static void main(String[] args) {
new Server().start(BEGIN_PORT, N_PORT);
}
public void start(int beginPort, int nPort) {
System.out.println("server starting....");
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
bootstrap.childHandler(new ConnectionCountHandler());
for (int i = 0; i < nPort; i++) {
int port = beginPort + i;
bootstrap.bind(port).addListener((ChannelFutureListener) future -> {
System.out.println("bind success in port: " + port);
});
}
System.out.println("server started!");
}
}
其中,BEGIN_PORT 和 N_PORT分别为8000和100。
Handler
@Sharable
public class ConnectionCountHandler extends ChannelInboundHandlerAdapter {
private AtomicInteger nConnection = new AtomicInteger();
public ConnectionCountHandler() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
System.out.println("connections: " + nConnection.get());
}, 0, 2, TimeUnit.SECONDS);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
nConnection.incrementAndGet();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
nConnection.decrementAndGet();
}
}
上述简单DEMO,用于利用端口进行的模拟百万连接,然后各位看官按照,我后面介绍的突破局部文件句柄和全局文件句柄,则能基本实现百万连接,具体连接数最终受限于你个人所用的电脑硬件配置。
Netty应用级别性能调优
在一般Netty项目中,如果在channelHandler里面做一些业务复杂操作,比如数据库或者网络操作,通常情况下,请求比较快,在百分之十或者百分之一左右,这操作是非常耗时的,这时候,需要把这操作放在单独的线程池去处理,调整线程数的大小应该是我们首先想到的方法,但线程数的大小最终也将会存在一个上限。
接下来,我简单概述下Netty应用级别的调优方式:
- 第一种方式,在handler里面,自己创建线程池,在执行具体代码的时候,只需要针对特定代码到线程池去处理,而其他操作仍可以在netty提供的线程池里完成。
- 另一种方式,在添加handler的时候,直接指定一个线程池,而不需要在handler里面指定一个线程池,对业务代码是无侵入的,但方法里的每行操作都是在单独的线程池里面,假若在该线程池里的某个方法内做内存分配,则就只在该业务线程池里进行分配,无法做到内存的共享。