Netty突破单机百万级连接

实现单机的百万连接,瓶颈有以下几点:

  • 如何模拟百万连接
  • 突破局部文件句柄的限制
  • 突破全局文件句柄的限制

在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里面指定一个线程池,对业务代码是无侵入的,但方法里的每行操作都是在单独的线程池里面,假若在该线程池里的某个方法内做内存分配,则就只在该业务线程池里进行分配,无法做到内存的共享。


评论区
Rick ©2018