netty(十九)Netty優化 - option中的參數優化 一、什麼是option? 二、常用參數

經過前面的學習,我們已經學會了Netty的使用。本章節開始我們要進行一些細節方面的學習,使其能更好的運用在我們以後的工作當中。

一、什麼是option?

前面學習了Netty的服務端,和客戶端,知道了創建服務要分別使用ServerBootStrap和BootStrap,不知道有沒有關注到其中有一個方法叫做Option的?

我們分別看下ServerBootStrap和BootStrap的option:
1)ServerBootStrap

如上圖所示,有兩種option,一個是自己的option(ServerSocketChannel),一個childOption(ScoketChannel的option)。

2)BootStrap
只有一個option方法。

無論是上述哪兩種,參數都是ChannelOption<T>,而這個ChannelOption Netty已經幫我們準備好了,可以直接使用。

下面我們會針對幾種重要的配置講解一下。

二、常用參數

2.1 CONNECT_TIMEOUT_MILLIS

ScoketChannel的參數。

用在客戶端建立連接時,如果超過指定的時間仍未連接,則拋出timeout異常。

    public static void main(String[] args) {

        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,500);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }

只啓動客戶端,拋出如下異常:

Exception in thread "main" io.netty.channel.ConnectTimeoutException: connection timed out: /127.0.0.1:8080
    at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe$1.run(AbstractNioChannel.java:261)
    at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:98)
    at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:170)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)

若果該參數設置過長,且服務端確實沒啓動,則會拋出java層面的異常,拒絕連接:

Exception in thread "main" io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: /127.0.0.1:8080
Caused by: java.net.ConnectException: Connection refused: no further information
    at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
    at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
    at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:330)
    at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:334)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:702)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)

2.2 SO_TIMEOUT

這個參數適用於阻塞IO,比如阻塞IO當中的read,accept等方法,修飾阻塞的,如果不想一直阻塞,可以通過改參數設置超時時間。

不要與CONNECT_TIMEOUT_MILLIS弄混了。

2.3 SO_BACKLOG

ServerSocketChannal 參數。

在瞭解這個參數之前,要先了解下TCP的三次握手,sync_queue(半連接隊列)和accept_queue(全連接隊列)。

其中半連接隊列是在首次握手時,將請求放入半連接隊列,當三次握手全部成功後,將請求從半連接隊列放入全連接隊列。

下圖展示netty和三次握手的關係:

  • 第一次握手,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。

在上面的過程中,提到的sync_queue和accept_queue是我們本篇文章需要關注的重點。

在linux2.2之前,backlog包括了兩個隊列的大小。在之後的版本當中,由如下兩個參數來控制:

  • sync queue - 半連接隊列

    • 大小通過 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 啓用的情況下,邏輯上沒有最大值限制,這個設置便被忽略
  • accept queue - 全連接隊列

    • 其大小通過 /proc/sys/net/core/somaxconn 指定,在使用 listen 函數時,內核會根據傳入的 backlog 參數與系統參數比較,取二者的較小值。
    • 如果 accpet queue 隊列滿了,server 將發送一個拒絕連接的錯誤信息到 client。

下面迴歸正題,在netty當中,通過ChannelOption.SO_BACKLOG設置大小,如下所示:

public class Server {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            //此處爲了模擬,設置爲2
            serverBootstrap.option(ChannelOption.SO_BACKLOG,2);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

如上代碼所示,設置了一個backlog爲2的值。然後我們需要啓動至少三個客戶端看結果。

通過前面的三次握手的圖,可以知道,只有當服務端處理不過來時,纔會使用全連接隊列,並將其佔滿,否則會直接走accept()方法,導致我們看不到測試結果。

所以我們這裏不做測試了。

我們看下這個backlog的默認值在nio當中是多少:

在NIO當中backlog在ServerSocketChannel當中的bind方法被調用,所以我們從這裏跟蹤進去找到bind方法:

    public final ServerSocketChannel bind(SocketAddress local)
        throws IOException
    {
        return bind(local, 0);
    }

查看bind被哪些地方調用,NioServerSocketChannel:

    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
    @Override
    protected void doBind(SocketAddress localAddress) throws Exception {
        if (PlatformDependent.javaVersion() >= 7) {
            javaChannel().bind(localAddress, config.getBacklog());
        } else {
            javaChannel().socket().bind(localAddress, config.getBacklog());
        }
    }

跟蹤config.getBacklog():

private final ServerSocketChannelConfig config;

這個config是接口,直接看它的實現DefaultServerSocketChannelConfig:

private volatile int backlog = NetUtil.SOMAXCONN;

找SOMAXCONN:

public static final int SOMAXCONN;

找到SOMAXCONN賦值的位置,默認是windows200,Linux或mac默認128,如果有前面我們提到的文件/proc/sys/net/core/somaxconn,則走此配置中的文件:

SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction<Integer>() {
            @Override
            public Integer run() {
                // Determine the default somaxconn (server socket backlog) value of the platform.
                // The known defaults:
                // - Windows NT Server 4.0+: 200
                // - Linux and Mac OS X: 128
                int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
                File file = new File("/proc/sys/net/core/somaxconn");
                BufferedReader in = null;
                try {
                    // file.exists() may throw a SecurityException if a SecurityManager is used, so execute it in the
                    // try / catch block.
                    // See https://github.com/netty/netty/issues/4936
                    if (file.exists()) {
                        in = new BufferedReader(new FileReader(file));
                        somaxconn = Integer.parseInt(in.readLine());
                        if (logger.isDebugEnabled()) {
                            logger.debug("{}: {}", file, somaxconn);
                        }
                    } else {
                        // Try to get from sysctl
                        Integer tmp = null;
                        if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
                            tmp = sysctlGetInt("kern.ipc.somaxconn");
                            if (tmp == null) {
                                tmp = sysctlGetInt("kern.ipc.soacceptqueue");
                                if (tmp != null) {
                                    somaxconn = tmp;
                                }
                            } else {
                                somaxconn = tmp;
                            }
                        }

                        if (tmp == null) {
                            logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
                                         somaxconn);
                        }
                    }
                } catch (Exception e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
                                file, somaxconn, e);
                    }
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (Exception e) {
                            // Ignored.
                        }
                    }
                }
                return somaxconn;
            }
        });

2.4 ulimit

屬於操作系統參數。

使用ulimit -n 可以查看當前的最大打開文件數。使用ulimit -a 可以查看當前系統的所有限制值。

linux默認1024,當服務器負載較大時,會發生too many open files的錯誤,所以我們爲了提供併發量,需要手動將其調整。

使用如下命令可以將其調整,但是是臨時性的,可以考慮將其放在啓動腳本當中:

ulimit -n 4096

2.5 TCP_NODELAY

屬於 SocketChannal 參數。

在前面的文章當中,我們提到過TCP的nagle算法,我們使用netty時,它的默認開始的。

nagle算法會使我們某些較小的數據包造成延遲,因爲爲了提升效率,nagle會等到收集到一定數據後進行發送,這樣可能造成我們消息的延遲。

可以通過如下方式設置,開啓無延遲的配置:

serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);

2.6 SO_SNDBUF & SO_RCVBUF

SO_SNDBUF 屬於 SocketChannal 參數
SO_RCVBUF 既可用於 SocketChannal 參數,也可以用於 ServerSocketChannal 參數

這兩個參數不建議我們手動進行設置,因爲操作系統會根據當前佔用,進行自動的調整。

2.7 ALLOCATOR

屬於 SocketChannal 參數。

ByteBuf的分配器。

serverBootstrap.childOption(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT);

這個參數只有一個DEFAULT可以使用。

這個參數與ch.alloc().buffer()命令有關,關係着我們分配的buf是池化還是非池化,是直接內存還是堆內存。

我們從上面的Default跟蹤進去:

ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;

繼續跟蹤DEFAULT_ALLOCATOR:

static final ByteBufAllocator DEFAULT_ALLOCATOR;

找到對其賦值的位置,發現瞭如下的靜態代碼塊,此處就是設置buf是pooled還是unpooled,通過環境變量:"io.netty.allocator.type" 指定,我們可以在啓動項目時指定-Dio.netty.allocator.type=unpooled設置成非池化。從源碼可以看到,安卓是unpooled,其他事pooled。

 static {
        MAX_BYTES_PER_CHAR_UTF8 = (int)CharsetUtil.encoder(CharsetUtil.UTF_8).maxBytesPerChar();
        String allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();
        Object alloc;
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }

        DEFAULT_ALLOCATOR = (ByteBufAllocator)alloc;
        THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
        logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
        MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16384);
        logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
        FIND_NON_ASCII = new ByteProcessor() {
            public boolean process(byte value) {
                return value >= 0;
            }
        };
    }

如上代碼中的UnpooledByteBufAllocator.DEFAULT和PooledByteBufAllocator.DEFAULT就指定了我們使用的直接內存還是堆內存,跟蹤其中的UnpooledByteBufAllocator.DEFAULT看一下:

public static final UnpooledByteBufAllocator DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());

跟蹤PlatformDependent.directBufferPreferred():

private static final boolean DIRECT_BUFFER_PREFERRED;

找DIRECT_BUFFER_PREFERRED賦值的位置:

DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);

重點關注上述代碼後半段!SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);,我們可用通過-Dio.netty.noPreferDirect=true環境變量指定我們使用堆內存。

2.8 RCVBUF_ALLOCATOR

屬於 SocketChannal 參數。

控制 netty 接收緩衝區大小。

這個RCVBUF_ALLOCATOR不要與前面的ALLOCATOR弄混。

負責入站數據的分配,決定入站緩衝區的大小(並可動態調整),統一採用 direct 直接內存,具體池化還是非池化由 allocator 決定。

通俗的講在handler內部分配的byteBuf可以是直接內存,也可以是堆內存,但是經過網絡io的內存,netty會強制爲直接內存。

我們啓動一個客戶端和服務端去驗證一下上述的描述:
服務端:

public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            System.out.println(msg);
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

客戶端:

public static void main(String[] args) {

        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            ByteBuf byteBuf = ctx.alloc().buffer();
                            byteBuf.writeBytes("hello world!".getBytes());
                            System.out.println(byteBuf);
                            ctx.writeAndFlush(byteBuf);
                            super.channelActive(ctx);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080);
            //阻塞等待連接
            channelFuture.sync();
            //阻塞等待釋放連接
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("server error:" + e);
        } finally {
            // 釋放EventLoopGroup
            worker.shutdownGracefully();
        }
    }

我們重點關注服務端接收到的Object msg,它應該是一個Bytebuf對象,那麼他是什麼類型的?如何生成的?
我們通過打斷點的方式,找到線程調用的堆棧,發現第一個channel的位置:

點擊進去發現如下代碼,並且就是我們得到的ByteBuf:

重點關注這部分有註釋代碼:

 ChannelPipeline pipeline = AbstractNioByteChannel.this.pipeline();
                // 上一小節提到的allocator,負責ByteBuf是池化還是非池化
                ByteBufAllocator allocator = config.getAllocator();
                //此處Handle是RecvByteBufAllocator內部類
                Handle allocHandle = this.recvBufAllocHandle();
                allocHandle.reset(config);
                ByteBuf byteBuf = null;
                boolean close = false;

                try {
                    do {
                        //allocate方法創建byteBuf
                        byteBuf = allocHandle.allocate(allocator);

下面重點關注這個allocate方法,這裏會分配一個ioBuffer,即直接內存buffer:

        public ByteBuf allocate(ByteBufAllocator alloc) {
            return alloc.ioBuffer(this.guess());
        }

如上的guess()會根據數據量大小,動態分配buffer的大小,範圍如下,自適應的AdaptiveRecvByteBufAllocator,最小不會小於64,最大不會超過65536:

public AdaptiveRecvByteBufAllocator() {
        this(64, 1024, 65536);
    }

關於主要參數就介紹這麼多,有用的話幫忙點個贊吧~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章