一、测试目的

很多中间件都用netty做通讯,如zookeeper\dubbo\rocketMq等等。
我们自己线上也在用netty,但我们目前连接数还不算很大,峰值连接也仅在1万左右。
今天我们来测试一下netty在单机百万连接情况下服务器cpu、内存、网络等使用情况。

二、服务器配置以及设置

1.测试机器配置

两台4核16G8M带宽的华为云服务器。
服务器:华为云,centos 8.2,4核16G,部署运行netty-server.jar
客户端:华为云,centos 8.2,4核16G,部署运行netty-client.jar
jdk:1.8
netty:4.1.67.Final
spring-boot:2.3.9.RELEASE

2.查看linux一个进程能够打开的最大文件数连接

ulimit -n
默认如下:
image.png

可以通过 ulimit -n 1000000进行临时修改,
注意这个数值不宜过高,我在测试过程中设置2000000时会连接不上系统了,
这个值建议可以设置为1000000进行测试。
也通过vi /etc/security/limits.conf
image.png
修改为1000000永久配置,修改如下:
image.png

3.查看全局文件句柄限制

cat /proc/sys/fs/file-max
默认如下:8.2默认配置就很高了,可以不修改
image.png
ps:centos8.2的默认配置就已经很高了,这里就不改了。

如果你机器默认值过低的话要进行以下修改
echo 1000000> /proc/sys/fs/file-max这个在重启后会失效,
下面这个操作就会永久生效:
vi /etc/sysctl.conf
我们在文件末尾加上
fs.file-max=1000000
image.png
注意,以上配置服务器和客户端都要修改才行,修改完后重启服务器和客户端。
image.png
重启后分别查看服务器和客户端的配置是否生效了:
image.png

三、编写服务端代码

1.服务端口绑定

  • 在开始端口绑定之前先普及一下很多人对tcp端口的几种常见误解:
    1. 服务器上一个端口仅会建立一个连接,服务器上最大的连接数量范围是0-65535。

linux内核对tcp连接的识别是通过四元组(源ip,源port,目标ip,目标port)来区分的。只要四元组中任意一个不同就被认为是完全不同的连接了。
所以服务器上同一个端口上只要目标ip,目标port(即客户端ip、客户端端口)不同就会被认为是完全不同的连接了,
所以服务器上同一个端口上是能建立无数个tcp连接的,只要设置系统能打开的文件数量足够大即可,而能打开的文件数量由内存来决定,
所以理论上只要内存足够大,那么服务器上仅一个端口上的连接数量就可以是无数个了,就更没服务器上最大的连接数量范围是0-65535这么一说了。

  1. 服务器上在接受到客户端的连接后会占用一个新的端口跟客户端保持连接

服务器上不会为客户端的连接开一个新的端口进行通讯的,比如web服务的80端口上,所有的客户端连接都是用的80。
根据四元组可知,只要目标ip、目标port任意一个不相同,那么80端口上可以接受无数连接的。
同理你自己程序所绑定监听的端口同样也仅是在你所绑定的端口上做通讯的,同样不会为客户端连接新开一个服务端口去进行通讯的。

  1. 客户端connect建立成功后端口会占用不可重用或者客户端能产生的最大连接数量范围是0-65535。

在客户端的同一个端口上,只要源ip、源port不相同(即服务器ip、服务器端口),那么客户端上同一个端口上的这些连接就会被认为是不相同的连接了,
所以客户端同一个端口上同样是可以连接无数个不同的服务器的,同样最终的上限还是由机器内存本身来决定。

  • 下面正式开始本测试的测试方法:

启动一个服务端绑定8000端口。
当我们服务器端在接受到一个客户端的连接之后,服务器端此时并不会新占用一个服务端口来保持连接,而是依然会用8000端口跟客户端进行保持通讯。
当客户端在成功连接到服务器后会用一个随机的客户端端口来跟服务器保持连接,如果此时我们仅用一台客户端去连接服务器的一个8000端口的话,根据四元组可知这一台客户端此时最多只能有65536个连接。
如果按这样的话我们大概需要16台客户端去连接8000端口的服务器才能测试得了100万的连接,这样操作上就很麻烦了,而且我们实际上也没有这么多的客户端去做测试。
所以为了能在只有一台服务器、一台客户端的情况下去做百万连接测试,我们可以根据上面所说的四元组对服务器端、客户端做一些巧妙的设计即可实现,具体方法如下:

  1. 一台服务端从8000端口开始监听往后的100个端口,也就是用一台服务器提供100个监听端口给客户端,按100*65536,理论上一台客户端能产生650万个连接了。
  2. 一台客户端从8000端口开始往后进行连接。根据四元组可知客户端此时同一个端口可同时连接多个同ip但不同端口的服务,比如客户端的62102端口上可同时连接服务端的8000~8100端口。

    1. 此时客户端便可用while一直不停的去连接服务器上的那100个不同的端口直到客户端产生超过100万连接后抛出异常。<br />具体的实现代码如下:

    2.服务端代码实现

    pom.xml

    1. <dependencies>
    2. <dependency>
    3. <groupId>io.netty</groupId>
    4. <artifactId>netty-all</artifactId>
    5. <version>4.1.67.Final</version>
    6. </dependency>
    7. </dependencies>
    8. <!--打包jar-->
    9. <build>
    10. <finalName>netty-server</finalName>
    11. <plugins>
    12. <!--spring-boot-maven-plugin-->
    13. <plugin>
    14. <groupId>org.springframework.boot</groupId>
    15. <artifactId>spring-boot-maven-plugin</artifactId>
    16. <version>2.3.9.RELEASE</version>
    17. <!--解决打包出来的jar文件中没有主清单属性问题-->
    18. <executions>
    19. <execution>
    20. <goals>
    21. <goal>repackage</goal>
    22. </goals>
    23. </execution>
    24. </executions>
    25. </plugin>
    26. <!--指定maven-compiler版本和<source>编译版本-->
    27. <plugin>
    28. <groupId>org.apache.maven.plugins</groupId>
    29. <artifactId>maven-compiler-plugin</artifactId>
    30. <version>3.8.1</version>
    31. <configuration>
    32. <source>1.8</source>
    33. <target>1.8</target>
    34. </configuration>
    35. </plugin>
    36. </plugins>
    37. </build>

    服务端 ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel;

public final class Server { final static int BEGIN_PORT = 8000; final static int N_PORT = 100; 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!”); } }

  1. 简单的连接数统计Hanlder
  2. ```java
  3. import io.netty.channel.ChannelHandler;
  4. import io.netty.channel.ChannelHandlerContext;
  5. import io.netty.channel.ChannelInboundHandlerAdapter;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.TimeUnit;
  8. import java.util.concurrent.atomic.AtomicInteger;
  9. @ChannelHandler.Sharable
  10. public class ConnectionCountHandler extends ChannelInboundHandlerAdapter {
  11. private AtomicInteger nConnection = new AtomicInteger();
  12. public ConnectionCountHandler() {
  13. //间隔2秒打印当前服务端的连接总数量
  14. Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
  15. System.out.println("connections: " + nConnection.get());
  16. }, 0, 2, TimeUnit.SECONDS);
  17. }
  18. @Override
  19. public void channelActive(ChannelHandlerContext ctx) {
  20. //当有新的连接进来触发计算器自增1
  21. nConnection.incrementAndGet();
  22. }
  23. @Override
  24. public void channelInactive(ChannelHandlerContext ctx) {
  25. //当有连接断开了触发计算器自减1
  26. nConnection.decrementAndGet();
  27. }
  28. }

3.编写启动脚本

我机器是16G的内存,机器上啥都没安装,16G能分15个G给JVM,所以选择了G1。
如果你的机器能配置的JVM内存小于8G的话,那建议用CMS。

  1. #!/bin/bash
  2. JAVA_OPTS="
  3. -server
  4. -Xmx13G
  5. -Xms13G
  6. -Xmn10G
  7. -XX:MetaspaceSize=256M
  8. -XX:+UseG1GC
  9. -XX:MaxGCPauseMillis=50
  10. -XX:G1HeapRegionSize=16M"
  11. java $JAVA_OPTS -jar netty-server.jar

我这里做了3个G预留,给JVM配置了13个G的堆内存和新生代10个G
服务器启动脚本:sh start.sh,程序启动打印启动成功结果,等待客户端连接。
然后再启动客户端脚本:sh start.sh。
image.png

四、编写客户端代码

1.客户端代码实现

  1. import io.netty.bootstrap.Bootstrap;
  2. import io.netty.channel.*;
  3. import io.netty.channel.nio.NioEventLoopGroup;
  4. import io.netty.channel.socket.SocketChannel;
  5. import io.netty.channel.socket.nio.NioSocketChannel;
  6. public class Client {
  7. private static final String SERVER_HOST = "192.168.0.94";
  8. final static int BEGIN_PORT = 8000;
  9. final static int N_PORT = 100;
  10. public static void main(String[] args) {
  11. new Client().start(BEGIN_PORT, N_PORT);
  12. }
  13. public void start(final int beginPort, int nPort) {
  14. System.out.println("client starting....");
  15. EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
  16. final Bootstrap bootstrap = new Bootstrap();
  17. bootstrap.group(eventLoopGroup);
  18. bootstrap.channel(NioSocketChannel.class);
  19. bootstrap.option(ChannelOption.SO_REUSEADDR, true);
  20. bootstrap.handler(new ChannelInitializer<SocketChannel>() {
  21. @Override
  22. protected void initChannel(SocketChannel ch) {
  23. }
  24. });
  25. int index = 0;
  26. int port;
  27. while (!Thread.interrupted()) {
  28. port = beginPort + index;
  29. try {
  30. ChannelFuture channelFuture = bootstrap.connect(SERVER_HOST, port);
  31. channelFuture.addListener((ChannelFutureListener) future -> {
  32. if (!future.isSuccess()) {
  33. System.out.println("connect failed, exit!");
  34. }else{
  35. System.out.println("serverAddress:"+ channelFuture.channel().remoteAddress().toString() +",localAddress:"+channelFuture.channel().localAddress().toString());
  36. }
  37. });
  38. channelFuture.get();
  39. } catch (Exception e) {
  40. }
  41. if (++index == nPort) {
  42. index = 0;
  43. }
  44. }
  45. }
  46. }

五、测试结果

一、100万连接测试结果

1、0个连接时系统CPU和内存情况

image.png

2、10万连接时系统CPU和内存情况

image.png

3、50万连接时系统CPU和内存情况

image.png

4、80万连接时系统CPU和内存情况

image.png
image.png

5、100万连接时系统CPU和内存情况

程序实际上没有达到100万连接,在达到99万多接近100万连接的时候程序就抛出打开文件数过多的异常了
image.png
下图是我在华为云服务器控制面板看到的网络瞬间的峰值情况
image.png

从下图可以看出当接近百万连接时,cpu的使用率还是比较低的,而内存也仅是使用了5.78个G(包含了系统)。
image.png
堆使用情况与GC日志情况,一次GC都没产生。
image.png

二、测试总结

1.测试结果有点出乎意料的,没想到在普通的4核心16G机器便能轻松达到百万级别的长连接。
整体来看netty百万连接对cpu没太大的要求,比较的吃内存,毕竟打开了上百万个文件。
2.当然,上面也仅是做了连接数的测试,线上真实的情况下还要考虑数据传输带来的网络压力。