• 性能优化:Netty连接参数优化


    参考资料:

    《Netty优化》

    相关文章:

    《Netty:入门(1)》

    《Netty:入门(2)》

    《Netty:粘包与半包的处理》

    《性能优化:TCP连接优化之三次握手》

            写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。

    前言

            在此前的文章中我们介绍了Netty这一网络编程框架,既然是网络编程,那就必然与网络连接有非常密切的联系。而Netty为了能更好的使用网络连接,提供了一些参数来对网络连接进行设置。

            在客户端,可以使用Bootstrap.option()函数来配置参数,配置参数作用于SocketChannel。

            在服务器端,可以使用ServerBootstrap来配置参数,但是对于不同的 Channel 需要选择不同的方法。通过 option 来配置 ServerSocketChannel 上的参数,而childOption则是用来配置 SocketChannel上的参数。

    目录

    前言

    一、客户端连接超时设置

            1、参数介绍

            2、源码分析

    二、TCP连接参数

            1、连接队列大小设置

            1.1、半、全连接队列

            1.2、backlog 

            2、关闭缓冲算法

            3、调整滑动窗口大小(不建议使用)

    三、buf控制

            1、buf类型控制

            参数控制

            源码分析

            2、接收缓冲区大小控制


    一、客户端连接超时设置

            1、参数介绍

            Netty为SocketChannal(客户端连接)提供了一个参数CONNECT_TIMEOUT_MILLIS,用于在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常。

    1. public class TestParam {
    2. public static void main(String[] args) {
    3. // SocketChannel 5s内未建立连接就抛出异常
    4. new Bootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
    5. // ServerSocketChannel 5s内未建立连接就抛出异常
    6. new ServerBootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000);
    7. // SocketChannel 5s内未建立连接就抛出异常
    8. new ServerBootstrap().childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
    9. }
    10. }

            2、源码分析

            首先,我们根据连接超时的报错定位到异常代码

             于是,我们定位到AbstractNioChannel.AbstractNioUnsafe.connect方法中。

            我们从schedule方法可以看出这是一个定时任务,其中的内容为一个定义了具体任务内容的Runable对象与超时时间connectTimeoutMillis。

            另外我们还看到了Promise,于是可得知正是由此对象实现的和主线程之间的交互。

    1. public final void connect(
    2. final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    3. ...
    4. // Schedule connect timeout.
    5. // 设置超时时间,通过option方法传入的CONNECT_TIMEOUT_MILLIS参数进行设置
    6. int connectTimeoutMillis = config().getConnectTimeoutMillis();
    7. // 如果超时时间大于0
    8. if (connectTimeoutMillis > 0) {
    9. // 创建一个定时任务,延时connectTimeoutMillis(设置的超时时间时间)后执行
    10. // schedule(Runnable command, long delay, TimeUnit unit)
    11. connectTimeoutFuture = eventLoop().schedule(new Runnable() {
    12. @Override
    13. public void run() {
    14. // 判断是否建立连接,Promise进行NIO线程与主线程之间的通信
    15. // 如果超时,则通过tryFailure方法将异常放入Promise中
    16. // 在主线程中抛出
    17. ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
    18. ConnectTimeoutException cause = new ConnectTimeoutException("connection timed out: " + remoteAddress);
    19. if (connectPromise != null && connectPromise.tryFailure(cause)) {
    20. close(voidPromise());
    21. }
    22. }
    23. }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
    24. }
    25. ...
    26. }

             总结一下该超时功能就是,通过 Eventloop 的 schedule 方法和 Promise:

    • schedule 设置了一个定时任务,延迟connectTimeoutMillis秒后执行该方法。
    • 如果指定时间内没有建立连接,则会执行其中的任务,任务负责创建 ConnectTimeoutException 异常,并将异常通过 Pormise 传给主线程并抛出。

    二、TCP连接参数

            1、连接队列大小设置

            下文的内容会涉及到TCP协议的相关内容,因此这里再给不太了解的朋友介绍下TCP的知识点。

            1.1、半、全连接队列

            这一部分如果有兴趣的朋友可以看我的这篇文章(《性能优化:TCP连接优化之三次握手》),完整的介绍了三次握手的过程,不过本文主要是介绍Netty提供的优化参数,因此这里只介绍相应涉及的内容。

            第一次握手时,因为客户端与服务器之间的连接还未完全建立,Linux内核就会建立一个半连接队列来维护未完成的握手信息,当半连接队列溢出后,服务端就无法再建立新的连接。(若系统开启tcp_syncookies参数则不会丢弃

            当完成三次握手以后,内核会把连接从半连接队列移除,然后创建新的完全的连接(即全连接队列),并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。(全连接队列长度受/proc/sys/net/core/somaxconn参数影响,默认为 128,最终长度为min(backlog, somaxconn),这个backlog就是应用系统传入的参数。)

            1.2、backlog 

            在Netty中,SO_BACKLOG主要用于设置全连接队列的大小。当处理Accept的速率小于连接建立的速率时,全连接队列中堆积的连接数大于SO_BACKLOG设置的值是,便会抛出异常。

            可以使用如下方式进行设置

    1. // 设置全连接队列,大小为2
    2. new ServerBootstrap().option(ChannelOption.SO_BACKLOG, 2);

            backlog参数在NioSocketChannel.doBind方法被使用

    1. @Override
    2. protected void doBind(SocketAddress localAddress) throws Exception {
    3. if (PlatformDependent.javaVersion() >= 7) {
    4. javaChannel().bind(localAddress, config.getBacklog());
    5. } else {
    6. javaChannel().socket().bind(localAddress, config.getBacklog());
    7. }
    8. }

            其中backlog被保存在了DefaultServerSocketChannelConfig配置类中

    private volatile int backlog = NetUtil.SOMAXCONN;

             具体的生效步骤如下:

    1. SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction() {
    2. @Override
    3. public Integer run() {
    4. // 根据操作系统选择默认somaxconn的大小,Linux默认128
    5. int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
    6. File file = new File("/proc/sys/net/core/somaxconn");
    7. BufferedReader in = null;
    8. try {
    9. // 如果配置文件/proc/sys/net/core/somaxconn存在
    10. // 会读取配置文件中的值,并将backlog的值设置为配置文件中指定的
    11. if (file.exists()) {
    12. in = new BufferedReader(new FileReader(file));
    13. // 将somaxconn设置为Linux配置文件中设置的值
    14. somaxconn = Integer.parseInt(in.readLine());
    15. if (logger.isDebugEnabled()) {
    16. logger.debug("{}: {}", file, somaxconn);
    17. }
    18. } else {
    19. ...
    20. }
    21. ...
    22. }
    23. // 返回backlog的值
    24. return somaxconn;
    25. }
    26. }

            2、关闭缓冲算法

            在介绍Nginx的优化与Netty的粘包问题时(《性能优化:Nginx配置优化》《Netty:粘包与半包的处理》),聊到过Nagle算法,它在一定的时间段,将小数据包暂存,将这些小数据包集合起来,整合为一个数据包发送,在下一个时间段又是如此。这改善了网络传输的效率。

            但是,TCP提供的这一优化方案并非总是带来好处,它的一项坏影响便是可能导致数据的发送存在一定的延时。

            由于该功能是默认开启的,因此如果想关闭的话可以给SocketChannal将TCP_NODELAY这个参数设置为true。

            3、调整滑动窗口大小(不建议使用)

            TCP协议使用滑动窗口来动态的协调发送方与接收方的处理速率 

             Netty提供了两个参数来指定接收方与发送方的滑动窗口大小:

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

             不过由于现在的操作系统可以自动的对其进行调整,我们自己再来调整反而有点弄巧成拙,因此不建议对滑动窗口的大小进行调整。

    三、buf控制

            1、buf类型控制

            参数控制

            当我们使用ctx.alloc()获取buf时,Netty默认分配的buf为池化的直接内存,如果有特殊需求的话可以进行调整。

    1. // 选择ALLOCATOR参数,设置SocketChannel中分配的ByteBuf类型
    2. // 第二个参数需要传入一个ByteBufAllocator,用于指定生成的 ByteBuf 的类型
    3. new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator());
    4. // true表示使用直接内存
    5. //new PooledByteBufAllocator(true);
    6. // false表示使用堆内存
    7. //new PooledByteBufAllocator(false);
    8. // ture表示使用直接内存
    9. //new UnpooledByteBufAllocator(true);
    10. // false表示使用堆内存
    11. //new UnpooledByteBufAllocator(false);

            源码分析

            通过默认配置项我们定位到了ByteBufUtil.java类中

    1. // DefaultChannelConfig.java
    2. private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
    3. // ByteBufAllocator.java
    4. ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;

            在ByteBufUtil.java类中,我们看到可以通过启动参数配置默认的buf类型,如果未配置,则再根据是否为安卓系统判断是否使用池化方案。

    1. // 定义默认类型
    2. static final ByteBufAllocator DEFAULT_ALLOCATOR;
    3. static {
    4. // 从启动参数中获取是否池化的配置类型,如未配置则使用默认值
    5. String allocType = SystemPropertyUtil.get(
    6. "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
    7. allocType = allocType.toLowerCase(Locale.US).trim();
    8. ByteBufAllocator alloc;
    9. if ("unpooled".equals(allocType)) {
    10. alloc = UnpooledByteBufAllocator.DEFAULT;
    11. logger.debug("-Dio.netty.allocator.type: {}", allocType);
    12. } else if ("pooled".equals(allocType)) {
    13. alloc = PooledByteBufAllocator.DEFAULT;
    14. logger.debug("-Dio.netty.allocator.type: {}", allocType);
    15. } else {
    16. alloc = PooledByteBufAllocator.DEFAULT;
    17. logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
    18. }
    19. DEFAULT_ALLOCATOR = alloc;
    20. THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
    21. logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
    22. MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
    23. logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
    24. }

            不过,我们看到无论是池化还是非池化依然继续读取了参数,我们接着往下回溯到PlatformDependent.java中,发现依然是采用启动参数来配置是否使用直接内存。

    1. // UnpooledByteBufAllocator.java
    2. public static final UnpooledByteBufAllocator DEFAULT =
    3. new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
    4. // PlatformDependent.java
    5. public static boolean directBufferPreferred() {
    6. return DIRECT_BUFFER_PREFERRED;
    7. }
    8. // PlatformDependent.java
    9. static{
    10. // 其余代码
    11. DIRECT_BUFFER_PREFERRED = CLEANER != NOOP
    12. && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
    13. if (logger.isDebugEnabled()) {
    14. logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED);
    15. }
    16. // 其余代码
    17. }

            2、接收缓冲区大小控制

            对于接受过来的参数所开辟的空间,类型依旧根据上文我们提到的虚拟机参数io.netty.allocator.type=pooled|unpooled来配置!分配空间位置默认是直接内存,这是固定的不能够更改。(netty认为使用直接内存效率更高)。

            使用RCVBUF_ALLOCATOR参数可以控制netty的接收缓冲区大小。就是在readchannel事件中读到的ByteBuf。

            RCVBUF_ALLOCATOR负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,但具体池化还是非池化由 allocator 决定。

    1. //设置指定的接收ByteBuf大小为100字节
    2. new ServerBootstrap().childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(100))

             接收区buf分配的源码如下

    1. final ChannelPipeline pipeline = pipeline();
    2. // byteBuf的分配器,负责管理池化还是非池化
    3. final ByteBufAllocator allocator = config.getAllocator();
    4. // 其余代码
    5. try {
    6. do {
    7. // RecvByteBufAllocator的内部类,真正决定byte的大小和direct,创建buf的方法
    8. byteBuf = allocHandle.allocate(allocator);
    9. pipeline.fireChannelRead(byteBuf);
    10. byteBuf = null;
    11. }
    12. }

             allocHandle的初始化则追溯到AdaptiveRecvByteBufAllocator.java类中,可以看到buf的大小默认为DEFAULT_INITIAL(1024),然后动态的调整接收缓冲区的大小,如果数据太多,就逐步扩大,直到DEFAULT_MAXIMUM(65535),反之则会逐步减小,直到DEFAULT_MINIMUM(64)

    1. // DefaultChannelConfig.java
    2. public DefaultChannelConfig(Channel channel) {
    3. this(channel, new AdaptiveRecvByteBufAllocator());
    4. }
    5. // AdaptiveRecvByteBufAllocator.java
    6. static final int DEFAULT_MINIMUM = 64;
    7. static final int DEFAULT_INITIAL = 1024;
    8. static final int DEFAULT_MAXIMUM = 65536;
    9. public AdaptiveRecvByteBufAllocator() {
    10. this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
    11. }
    12. // 动态调整
    13. public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
    14. checkPositive(minimum, "minimum");
    15. if (initial < minimum) {
    16. throw new IllegalArgumentException("initial: " + initial);
    17. }
    18. if (maximum < initial) {
    19. throw new IllegalArgumentException("maximum: " + maximum);
    20. }
    21. int minIndex = getSizeTableIndex(minimum);
    22. if (SIZE_TABLE[minIndex] < minimum) {
    23. this.minIndex = minIndex + 1;
    24. } else {
    25. this.minIndex = minIndex;
    26. }
    27. int maxIndex = getSizeTableIndex(maximum);
    28. if (SIZE_TABLE[maxIndex] > maximum) {
    29. this.maxIndex = maxIndex - 1;
    30. } else {
    31. this.maxIndex = maxIndex;
    32. }
    33. this.initial = initial;
    34. }

  • 相关阅读:
    Zookeeper入门
    node版本管理工具推荐
    【IVI】VehicleService启动
    学习Opencv(蝴蝶书/C++)相关——1. 前言 和 第1章.概述
    基于TRE文章的非线性模型化线性方法
    MySQL—优化数据库
    前端ES6相关的面试题
    Argo Rollouts结合Service进行Blue-Green部署
    Pytorch中Tensor类型转换
    什么是 CSRF 、原理及其解决方式
  • 原文地址:https://blog.csdn.net/wzngzaixiaomantou/article/details/128054579