前传04 | Netty性能调优

前传04 | Netty性能调优

  • 一、操作系统参数调试
    • 01 文件描述符
      • 1)最大文件句柄数
      • 2)单进程打开的最大句柄数 ulimit -n
      • 3)并发接入的TCP连接数
    • 02 TCP/IP相关参数
    • 03 多网卡队列和软中断
  • 二、Netty性能调优
    • 01 工作线程池优化
    • 02 心跳优化
    • 03 接收和发送缓冲区调优
    • 04 合理使用内存池
      • 05 防止I/O线程被意外阻塞
      • 06 I/O线程和业务线程分离
      • 07 针对端侧并发连接数的流控
  • 三、Netty参数调优
    • 01 SocketChannal 参数
      • 1)CONNECT_TIMEOUT_MILLIS
      • 2)TCP_NODELAY
      • 3)SO_SNDBUF & SO_RCVBUF
      • 4)ALLOCATOR
      • 5)RCVBUF_ALLOCATOR
    • 02 ServerSocketChannal 参数
      • 1)SO_BACKLOG

一、操作系统参数调试

01 文件描述符

1)最大文件句柄数

首先查看系统最大文件句柄数,执行命令#cat/proc/sys/fs/file-max,查看最大句柄数是否满足需要,如果不满足,通过#vim/etc/sysctl.conf命令插入如下配置:
‘fs.file-max = 1000000’
配置完成后,执行#sysctl-p命令,让配置修改立即生效。

2)单进程打开的最大句柄数 ulimit -n

设置完系统最大文件句柄数,对单进程打开的最大句柄数进行设置。通过 ulimit-a命令查看当前设置的值是否满足要求:
前传04 | Netty性能调优_第1张图片

3)并发接入的TCP连接数

当并发接入的TCP连接数超过上限时,就会提示“too many open files”,所有新的客户端接入将失败。通过#vi/etc/security/limits.conf命令添加如下配置参数:

在这里插入图片描述

02 TCP/IP相关参数

通过#vi/etc/sysctl.conf命令对上述网络参数进行优化,具体修改如下(大约可以接入50万个连接,可以根据业务需要调整参数),修改完成后,通过执行#sysctl-p命令使配置立即生效。
前传04 | Netty性能调优_第2张图片

(1)net.ipv4.tcp_rmem:为每个TCP连接分配的读缓冲区内存大小。第一个值是socket接收缓冲区分配的最小字节数。第二个值是默认值,缓冲区在系统负载不高的情况下可以增长到该值。第三个值是接收缓冲区分配的最大字节数。
(2)net.ipv4.tcp_wmem:为每个TCP连接分配的写缓冲区内存大小。第一个值是socket发送缓冲区分配的最小字节数。第二个值是默认值,缓冲区在系统负载不高的情况下可以增长到该值。第三个值是发送缓冲区分配的最大字节数。
(3)net.ipv4.tcp_mem:内核分配给TCP连接的内存,单位是page(1个page通常为4096字节,可以通过#getconf PAGESIZE命令查看),包括最小、默认和最大三个配置项。
(4)net.ipv4.tcp_keepalive_time:最近一次数据包发送与第一次keep alive探测消息发送的时间间隔,用于确认TCP连接是否有效。
(5)tcp_keepalive_intvl:在未获得探测消息响应时,发送探测消息的时间间隔。
(6)tcp_keepalive_probes:判断TCP连接失效连续发送的探测消息个数,达到之后判定连接失效。
(7)net.ipv4.tcp_tw_reuse:是否允许将TIME_WAIT Socket重新用于新的TCP连接,默认为0,表示关闭。
(8)net.ipv4.tcp_tw_recycle:是否开启TCP连接中TIME_WAIT Socket的快速回收功能,默认为0,表示关闭。
(9)net.ipv4.tcp_fin_timeout:套接字自身关闭时保持在FIN_WAIT_2状态的时间,默认为60。

03 多网卡队列和软中断

多队列网卡需要网卡硬件支持,首先判断当前系统是否支持多队列网卡,通过命令“lspci-vvv”或者“ethtool-l 网卡interface名”查看网卡驱动型号,根据网卡驱动官方说明确认当前系统是否支持多队列网卡(是否支持多队列网卡与网卡硬件、操作系统版本等有关)。有些网卡驱动默认开启了多队列网卡,有些则没有,由于不同的网卡驱动、云服务商提供的开启命令不同,因此需要根据实际情况处理,此处不再详细列举开启方式。

对于不支持多队列网卡的系统,如果内核版本支持RPS(kernel 2.6.35及以上版本),开启RPS后可以实现软中断,提升网络的吞吐量。RPS根据数据包的源地址、目的地址及目的和源端口,算出一个hash值,然后根据这个hash值选择软中断运行的CPU,从上层来看,也就是说将每个连接和CPU绑定,并通过这个hash值,在多个CPU上均衡软中断,提升网络并行处理性能,它实际提供了一种通过软件模拟多队列网卡的功能。

二、Netty性能调优

01 工作线程池优化

对于I/O工作线程池的优化,可以先采用系统默认值(即 CPU内核数×2)进行性能测试,在性能测试过程中采集I/O线程的CPU占用大小,看是否存在瓶颈,具体策略如下。
(1)通过执行 ps-ef|grep java 找到服务端进程pid。
(2)执行top-Hp pid 查询该进程下所有线程的运行情况,通过“shift+p”对CPU占用大小做排序,获取线程的pid及对应的CPU占用大小。
(3)使用printf‘%x\n’ pid将pid转换成16进制格式。
(4)通过jstack-f pid命令获取线程堆栈,或者通过jvisualvm工具打印线程堆栈,找到I/O work工作线程,查看它们的CPU占用大小及线程堆栈,如图11-3所示。
前传04 | Netty性能调优_第3张图片

如果连续采集几次进行对比,发现线程堆栈都停留在 SelectorImpl.lockAndDoSelect处,则说明I/O 线程比较空闲,无须对工作线程数做调整。

如果发现I/O线程的热点停留在读或者写操作,或者停留在ChannelHandler的执行处,则可以通过适当调大NioEventLoop线程的个数来提升网络的读写性能。调整方式有两种。
(1)接口API指定:在创建NioEventLoopGroup实例时指定线程数。
(2)系统参数指定:通过-Dio.netty.eventLoopThreads来指定NioEventLoopGroup线程池.

02 心跳优化

不同协议的心跳检测机制存在差异,归纳起来主要分为两类。
(1)Ping-Pong型心跳:由通信一方定时发送Ping消息,对方接到Ping消息立即返回Pong应答消息给对方,属于“请求-响应型”心跳。
(2)Ping-Ping型心跳:不区分心跳请求和应答,由通信双方按照约定定时向对方发送心跳Ping消息,它属于双向心跳。

心跳检测策略如下:
(1)连续N次心跳检测都没有收到对方的Pong应答消息或者Ping请求消息,则认为链路已经发生逻辑失效,这被称为心跳超时。
(2)在读取和发送心跳消息的时候如果直接发生了I/O异常,说明链路已经失效,这被称为心跳失败。无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常。

03 接收和发送缓冲区调优

在一些场景下,单个链路的消息收发量并不大,针对此类场景,可以通过调小 TCP的接收和发送缓冲区来降低单个 TCP 连接的资源占用率,例如将收发缓冲区设置为8KB,相关代码如下:
前传04 | Netty性能调优_第4张图片

04 合理使用内存池

Netty 内存池从实现上可以分为两类:堆外直接内存和堆内存。由于 ByteBuf 主要用于网络I/O读写,因此采用堆外直接内存会减少一次从用户堆内存到内核态的字节数组拷贝,所以性能更高。由于DirectByteBuf的创建成本比较高,因此如果使用DirectByteBuf,则需要配合内存池使用,否则性价比可能还不如HeapByteBuf。
Netty 默认的 I/O 读写操作采用的都是内存池的堆外直接内存模式,如果用户需要额外使用 ByteBuf,建议也采用内存池方式;如果不涉及网络 I/O 操作(只是纯粹的内存操作),可以使用堆内存池,这样内存的创建效率会更高一些。
前传04 | Netty性能调优_第5张图片

05 防止I/O线程被意外阻塞

以最常用的log4j(1.2.X版本)为例,尽管它支持异步写日志(AsyncAppender),但是当日志队列满时,它会同步阻塞业务线程(采用等待非丢弃方式时),直到日志队列有空闲位置可用,相关代码如下:
前传04 | Netty性能调优_第6张图片

06 I/O线程和业务线程分离

对于I/O线程,由于互相之间不存在锁竞争,可以创建一个大的 NioEventLoopGroup 线程组,所有 Channel 都共享同一个线程池。对于后端的业务线程池,则建议创建多个小的业务线程池,线程池可以与 I/O线程绑定,这样既减少了锁竞争,又提升了后端的处理性能。
前传04 | Netty性能调优_第7张图片

07 针对端侧并发连接数的流控

在 Netty中,可以非常方便地实现流控功能:新增一个 FlowControlChannelHandler,添加到ChannelPipeline靠前的位置,继承channelActive()方法,创建TCP链路后,执行流控逻辑,如果达到流控阈值,则拒绝该连接,调用ChannelHandlerContext的close()方法关闭连接。
TLS/SSL的连接数的流控相对复杂一些,可以在TLS/SSL握手成功后,监听握手成功的事件,执行流控逻辑。握手成功后发送SslHandshakeCompletionEvent事件,代码示例如下(SslHandler类):
前传04 | Netty性能调优_第8张图片

FlowControlChannelHandler继承userEventTriggered()方法,拦截TLS/SSL握手成功事件,执行流控逻辑,示例代码如下:
前传04 | Netty性能调优_第9张图片

三、Netty参数调优

01 SocketChannal 参数

1)CONNECT_TIMEOUT_MILLIS

  • 属于 SocketChannal 参数

  • 用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常

  • SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间

@Slf4j
public class TestConnectionTimeout {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300)
                    .channel(NioSocketChannel.class)
                    .handler(new LoggingHandler());
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080);
            future.sync().channel().closeFuture().sync(); // 断点1
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("timeout");
        } finally {
            group.shutdownGracefully();
        }
    }
}

另外源码部分 io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe#connect

@Override
public final void connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    // ...
    // Schedule connect timeout.
    int connectTimeoutMillis = config().getConnectTimeoutMillis();
    if (connectTimeoutMillis > 0) {
        connectTimeoutFuture = eventLoop().schedule(new Runnable() {
            @Override
            public void run() {                
                ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                ConnectTimeoutException cause =
                    new ConnectTimeoutException("connection timed out: " + remoteAddress); // 断点2
                if (connectPromise != null && connectPromise.tryFailure(cause)) {
                    close(voidPromise());
                }
            }
        }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
    }
	// ...
}

2)TCP_NODELAY

属于 SocketChannal 参数

3)SO_SNDBUF & SO_RCVBUF

SO_SNDBUF 属于 SocketChannal 参数
SO_RCVBUF 既可用于 SocketChannal 参数,也可以用于 ServerSocketChannal 参数(建议设置到 ServerSocketChannal 上)

4)ALLOCATOR

属于 SocketChannal 参数
用来分配 ByteBuf, ctx.alloc()

5)RCVBUF_ALLOCATOR

属于 SocketChannal 参数
控制 netty 接收缓冲区大小
负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定

02 ServerSocketChannal 参数

1)SO_BACKLOG

netty 中可以通过 option(ChannelOption.SO_BACKLOG, 值) 来设置大小,可以通过下面源码查看默认大小

public class DefaultServerSocketChannelConfig extends DefaultChannelConfig
                                              implements ServerSocketChannelConfig {

    private volatile int backlog = NetUtil.SOMAXCONN;
    // ...
}

sequenceDiagram

participant c as client
participant s as server
participant sq as syns queue
participant aq as accept queue

s ->> s : bind()
s ->> s : listen()
c ->> c : connect()
c ->> s : 1. SYN
Note left of c : SYN_SEND
s ->> sq : put
Note right of s : SYN_RCVD
s ->> c : 2. SYN + ACK
Note left of c : ESTABLISHED
c ->> s : 3. ACK
sq ->> aq : put
Note right of s : ESTABLISHED
aq -->> s : 
s ->> s : accept()

第一次握手,client 发送 SYN 到 server,状态修改为 SYN_SEND,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 队列
第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED,并发送 ACK 给 server
第三次握手,server 收到 ACK,状态改变为 ESTABLISHED,将该请求从 sync queue 放入 accept queue

其中

  • 在 linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制

  • sync queue - 半连接队列

    • 大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 启用的情况下,逻辑上没有最大值限制,这个设置便被忽略
  • accept queue - 全连接队列

    • 其大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值
    • 如果 accpet queue 队列满了,server 将发送一个拒绝连接的错误信息到 client

调试关键断点为:io.netty.channel.nio.NioEventLoop#processSelectedKey
oio 中更容易说明,不用 debug 模式

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888, 2);
        Socket accept = ss.accept();
        System.out.println(accept);
        System.in.read();
    }
}

客户端启动 4 个

public class Client {
    public static void main(String[] args) throws IOException {
        try {
            Socket s = new Socket();
            System.out.println(new Date()+" connecting...");
            s.connect(new InetSocketAddress("localhost", 8888),1000);
            System.out.println(new Date()+" connected...");
            s.getOutputStream().write(1);
            System.in.read();
        } catch (IOException e) {
            System.out.println(new Date()+" connecting timeout...");
            e.printStackTrace();
        }
    }
}

第 1,2,3 个客户端都打印,但除了第一个处于 accpet 外,其它两个都处于 accept queue 中

Tue Apr 21 20:30:28 CST 2020 connecting...
Tue Apr 21 20:30:28 CST 2020 connected...

第 4 个客户端连接时

Tue Apr 21 20:53:58 CST 2020 connecting...
Tue Apr 21 20:53:59 CST 2020 connecting timeout...
java.net.SocketTimeoutException: connect timed out

你可能感兴趣的:(netty4,java,nio)