Netty - 如何提高RT

309 阅读6分钟

1. Netty的线程

我们知道 Netty服务端的写法 .

NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup work = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors()*2);

**根据线程一般都是设置为CPU核数的两倍. **

实例化俩线程,然后交给ServerBootstrap . 那么为啥叫work线程了. 其实他是NIO模型. boot线程就是一直在轮询事件,当连接来了, 交给work处理. work线程是真正来用来处理当前连接的客户端的所以操作.

那么如果 10W个连接. 每个连接又有好多个请求. 那么这16个线程能处理吗, 万一遇见那种也别久的请求呢 ,难以处理呢.

首先我们先来试试这些操作行不行. 我相信下面这段代码大家并不陌生. 这个是我们的服务器端 .

public class DemoServer {

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1,new NamedThreadFactory("boss"));
        NioEventLoopGroup work = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2, new NamedThreadFactory("worker"));

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss, work).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // 字符串编码器.
                        pipeline.addLast(new StringEncoder());
                        // 每行行解码器.
                        pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
                        // 字符串解码器.
                        pipeline.addLast(new StringDecoder());
                        // 自己的Handler
                        pipeline.addLast(MyChannelDuplexHandler.INSTANCE);
                    }
                });
        ChannelFuture future = null;
        try {
            future = bootstrap.bind(10086).sync();
            System.out.println("bind success : localhost:10086");
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }

    @ChannelHandler.Sharable
    private static class MyChannelDuplexHandler extends ChannelDuplexHandler {
        // 写日志信息的. 
        final CharSink sink;
        MyChannelDuplexHandler() {
            sink = Files.asCharSink(new File("D:\\MyDesktop\\template\\log.txt"), Charset.forName("utf-8"), FileWriteMode.APPEND);
        }
        static ChannelDuplexHandler INSTANCE = new MyChannelDuplexHandler();

        @Override

        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
               func(ctx, msg);
        }

        // 主要处理逻辑. 
        private void func(ChannelHandlerContext ctx, Object msg) throws IOException {
            String message = (String) msg;
            long start = Long.parseLong(message.trim());
            System.out.printf("thread : %s ,addr : %s , spend : %dms\n", Thread.currentThread().getName(), ctx.channel().remoteAddress().toString(), System.currentTimeMillis() - start);
            String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm:ss SSS\n"));
            ctx.writeAndFlush(format);
            sink.write((System.currentTimeMillis() - start) + "\n");
        }


        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }

        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            //no ting
        }
    }
}

压测代码. Golang写的

// @date : 2020/2/2 21:16
// @author : <a href='mailto:fanhaodong516@qq.com'>Anthony</a>

/**
	todo
 */
package main

import (
	"fmt"
	"log"
	"net"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup

	var start = time.Now().UnixNano() / 1e6

	for x := 0; x < 50; x++ {
		wg.Add(1)
		go func() {
			conn(&wg)
		}()
	}
	wg.Wait()
	var end = time.Now().UnixNano() / 1e6
	fmt.Printf("耗时 : %d\n", end-start)
}

func conn(wg *sync.WaitGroup) {
	conn, e := net.Dial("tcp", "localhost:10086")
	if e != nil {
		log.Fatal(e)
	}
	var start = time.Now().UnixNano() / 1e6
	defer func() {
		wg.Done()
		conn.Close()
		var end = time.Now().UnixNano() / 1e6
		fmt.Printf("耗时 : %d\n", end-start)
	}()


	for x := 0; x < 500; x++ {
		fmt.Fprintf(conn, "%s \n", "hello world")
	}
}

scala 计算均值 .

object Count {
	def main(args: Array[String]): Unit = {

		val count: Int =Source.fromFile("D:\\MyDesktop\\template\\log.txt").getLines().count((_: String) => true)

		val sum: Int = Source.fromFile("D:\\MyDesktop\\template\\log.txt").getLines().map(_.toInt).sum

		printf("总请求数 : %d  , 总耗时 :  %dms , 平均响应时间 : %dms\n", count, sum, sum / count)
	}
}

测试 50个客户端, 没人发送500次请求. 也就是处理 2.5w个请求响应和 50个客户端

输出 15875 , 说明处理很严重, 存在严重的处理不了的现象. 说明 1W请求存在 无法处理的问题.

总请求数 : 15875  , 总耗时 :  9610317ms , 平均响应时间 : 605ms

为什么呢. 我们先模拟一下. 5个客户端发送请求的问题. 看输出

thread : worker-thread-1 ,addr : /127.0.0.1:6235 , spend : 2ms
thread : worker-thread-2 ,addr : /127.0.0.1:6236 , spend : 3ms
thread : worker-thread-8 ,addr : /127.0.0.1:6233 , spend : 3ms
thread : worker-thread-1 ,addr : /127.0.0.1:6235 , spend : 4ms
thread : worker-thread-2 ,addr : /127.0.0.1:6236 , spend : 4ms
thread : worker-thread-3 ,addr : /127.0.0.1:6237 , spend : 4ms
thread : worker-thread-3 ,addr : /127.0.0.1:6237 , spend : 5ms
thread : worker-thread-7 ,addr : /127.0.0.1:6234 , spend : 6ms
thread : worker-thread-8 ,addr : /127.0.0.1:6233 , spend : 6ms
thread : worker-thread-7 ,addr : /127.0.0.1:6234 , spend : 6ms

我们发现所有的客户端与线程都是一一对应的关系 , 也就是说一个客户端会分配死了, 就是那个线程. 那就是worker线程. 那这个就是为啥慢的原因了. 他的每一个 客户端只绑定一个线程 ,这个线程还是死的.

解决方式 一

根据 , Netty 分配请求的原则. 我们可以知道.

ChannelPipeline addLast(EventExecutorGroup group, ChannelHandler... handlers);

我们可以在 pipeline中添加. 一个 EventExecutorGroup . 来处理改Handler的请求. 所以我们就创建一个全局的NioEventLoopGroup , 来放入进去. 我们继续测试.

public static void main(String[] args) {

    NioEventLoopGroup boss = new NioEventLoopGroup(1,new NamedThreadFactory("boss"));
    NioEventLoopGroup work = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2, new NamedThreadFactory("worker"));

    // 专门用来处理Handler处理器的. 
    NioEventLoopGroup handler = new NioEventLoopGroup(50, new NamedThreadFactory("handler"));
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(boss, work).channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    // 字符串编码器.
                    pipeline.addLast(new StringEncoder());
                    // 每行行解码器.
                    pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
                    // 字符串解码器.
                    pipeline.addLast(new StringDecoder());
                    // 自己的Handler
                    pipeline.addLast(handler,MyChannelDuplexHandler.INSTANCE);
                }
            });
    ChannelFuture future = null;
    try {
        future = bootstrap.bind(10086).sync();
	    System.out.println("bind success : localhost:10086");
        future.channel().closeFuture().sync();
    } catch (Exception e) {
        System.out.println(e.getMessage());
    } finally {
        boss.shutdownGracefully();
        work.shutdownGracefully();
    }
}

我们先模拟一下. 50客户端 2.5请求. 我们的handler 有 50个线程处理.

再次请求 . 总请求数为 2.5w , 接收响应并且都处理了, 但是平均响应时间也很长. 处理很不恰当.

我们统计三组数据.

总请求数 : 25000  , 总耗时 :  45855712ms , 平均响应时间 : 1834ms
总请求数 : 25000  , 总耗时 :  48299098ms , 平均响应时间 : 1931ms
总请求数 : 25000  , 总耗时 :  45203872ms , 平均响应时间 : 1808ms

大约响应处理时间大约在 2s左右. 所以很不理想.

通常来说对于客户端连接多 . 请求少的情况下 . 可以选择这种. 但是当客户端少. 连接多这就容易出问题.

所以我们采用线程池.

解决方式2

我们在 MyChannelDuplexHandler 中添加 一个线程池来处理 .

@ChannelHandler.Sharable
private static class MyChannelDuplexHandler extends ChannelDuplexHandler {
    final CharSink sink;
    MyChannelDuplexHandler() {
        sink = Files.asCharSink(new File("D:\\MyDesktop\\template\\log.txt"), Charset.forName("utf-8"), FileWriteMode.APPEND);
    }
    // 添加一个线程池 . 来处理全部的处理逻辑.  
    static final ExecutorService service = Executors.newFixedThreadPool(50);

    static ChannelDuplexHandler INSTANCE = new MyChannelDuplexHandler();

    @Override

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        service.execute(() -> {
            try {
                this.func(ctx, msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    private void func(ChannelHandlerContext ctx, Object msg) throws IOException {
        String message = (String) msg;
        long start = Long.parseLong(message.trim());
        // System.out.printf("thread : %s ,addr : %s , spend : %dms\n", Thread.currentThread().getName(), ctx.channel().remoteAddress().toString(), System.currentTimeMillis() - start);
        String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm:ss SSS\n"));
        ctx.writeAndFlush(format);
        sink.write((System.currentTimeMillis() - start) + "\n");
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //no ting
    }
}

我们再次压测.

总请求数 : 25000  , 总耗时 :  40851493ms , 平均响应时间 : 1634ms

再次修改 , 修改线程数. 为 100

总请求数 : 25000  , 总耗时 :  41833614ms , 平均响应时间 : 1673ms

其实不管你多少个线程数. 首先就是线程需要初始化. 大量的线程切换. 会消耗很长的时间, 所以看如何做处理.

Dubbo 的做法

他的 ThreadPoll 是一个SPI 接口

org.apache.dubbo.remoting.transport.dispatcher.WrappedChannelHandler#WrappedChannelHandler

executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);

一共有这几种 , 默认使用的是 fixed 也就是org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool

fixed=org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool
cached=org.apache.dubbo.common.threadpool.support.cached.CachedThreadPool
limited=org.apache.dubbo.common.threadpool.support.limited.LimitedThreadPool
eager=org.apache.dubbo.common.threadpool.support.eager.EagerThreadPool

然后看看fix的实现

@Override
public Executor getExecutor(URL url) {
    String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
    int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
    int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
    return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
            queues == 0 ? new SynchronousQueue<Runnable>() :
                    (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                            : new LinkedBlockingQueue<Runnable>(queues)),
            new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}

如果队列大小为 0 , 那么就是一个 SynchronousQueue , 否则为LinkedBlockingQueue , 对于超出的策略, 他使用的是自定义策略. 自定义策略需要继承 ThreadPoolExecutor.AbortPolicy 类.

说到底 都是 ThreadPoolExecutor , 只是他的细节做得好罢了.

默认是 thread 大小为 10个. 他会在你启动 ServiceConfig 的时候创建好那个url, 默认是 10个. 如果threads为空的话就是 200个. queues为 0 , 需要配置可以加 queues=1000 , 如果queues=-1 ,则是无界的.

反正基本就是这么一个情况 , 一般是在业务方法中添加线程池 , 来处理的, 因为这个线程池可以很好的处理 . 不要使用Netty的.