一个 grpc 服务器,用于 netty 的一元流和 bidi 流

one grpc server for both unary and bidi stream with netty

我需要在 java 中实现一个 grpc 服务器,它能够处理 grpc 一元流和双向流。使用的服务是可能的grpc bidi-streaming 每秒发送大量消息。(可能每秒 2000 条消息或更多)我有两种实现方式,有点困惑哪一种最适合我的要求。

1.为 grpc 一元和 grpc 双向使用相同的服务器。

使用此方法时,由于 grpc 一元流和 bidi 流都使用相同的端口,因此将为一元流和 bidi 流分配一个 boss 线程。所以我不确定在 bidi 流每秒接收大量消息的情况下它的性能如何。 (我的意思是 boss 线程是否会忙于 bidi 流而变得不可用)

final EventLoopGroup bossGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());
    final EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
    int blockingQueueLength = 1000;
    final BlockingQueue blockingQueue = new LinkedBlockingQueue(blockingQueueLength);
    final Executor executor = new ThreadPoolExecutor(400, 500, 30, TimeUnit.SECONDS, blockingQueue);
    Server server = NettyServerBuilder.forPort(PORT).maxConcurrentCallsPerConnection(50)
            .keepAliveTime(60, TimeUnit.SECONDS).bossEventLoopGroup(bossGroup)
            .workerEventLoopGroup(workerGroup).addService(new ExtAuthService()).addService(new RateLimitService())
            .channelType(NioServerSocketChannel.class)
            .executor(executor).build();
            
    try{
        server.start();
        erver.awaitTermination();
    }catch(Exception e){
        Logger.Error("Execption", new Error(e));
    }

2。使用两台服务器,一台用于 grpc unary,一台用于 grpc bidi streaming。

这里不存在前面提到的问题,因为我们为每个 grpc 一元和 bidi 流分配了 2 个 boss 线程。但是对于我正在使用使用 java ThreadPoolExecutor 的执行程序的服务,我的问题是 我应该为使用 grpc 一元和 bidi 流的两个服务使用 2 个线程池吗?

final EventLoopGroup bossGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors());
    final EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
    int blockingQueueLength = 1000;
    final BlockingQueue blockingQueue = new LinkedBlockingQueue(blockingQueueLength);
    final Executor executor = new ThreadPoolExecutor(400, 500, 30, TimeUnit.SECONDS, blockingQueue);

    // I have used here the same executor for both servers. 
    Server server1 = NettyServerBuilder.forPort(PORT_1).maxConcurrentCallsPerConnection(50)
            .keepAliveTime(60, TimeUnit.SECONDS).bossEventLoopGroup(bossGroup)
            .workerEventLoopGroup(workerGroup).addService(new ExtAuthService())
            .channelType(NioServerSocketChannel.class)
            .executor(executor).build();

    Server server2 = NettyServerBuilder.forPort(PORT_2).maxConcurrentCallsPerConnection(50)
            .keepAliveTime(60, TimeUnit.SECONDS).bossEventLoopGroup(bossGroup)
            .workerEventLoopGroup(workerGroup).addService(new RateLimitService())
            .channelType(NioServerSocketChannel.class)
            .executor(executor).build();
            
    try{
        server1.start();
        server2.start();
        server1.awaitTermination();
        server2.awaitTermination();
    }catch(Exception e){
        Logger.Error("Execption", new Error(e));
    }

使用单个服务器。

boss 线程只用于accept()ing 新连接。它不用于实际处理。这是由工作事件循环完成的。每个连接都分配给一个事件循环,一个事件循环可以服务多个连接。

在单个流上,Netty 每秒可以处理 100k 条消息。但这实际上很慢。查找消息边界由与传递消息不同的线程处理,这两个线程之间的通信增加了延迟。增加的延迟会减慢速度。使用避免延迟的 requests(5) 技巧,单个 Netty 流每秒可以处理 1250k 条消息。 (这些性能数字会根据您 运行 使用它们的机器而有所不同,但它们显然比您需要的要高得多。)请参阅 https://github.com/grpc/grpc-java/issues/6696 讨论延迟问题的地方。

但假设您需要更高的性能,或者想要将一元流量与流式流量分开。在这种情况下,我们建议使用两个不同的 频道 。每个通道都将使用自己的连接和(可能)独立的工作事件循环。

只有当您非常担心延迟时,您才应该费心将两种类型的流量拆分到不同的服务器中。 (这样你也有基准显示它有多大帮助。)是的,使用单独的服务器和频道与他们自己的 workerEventLoopGroup()s (在频道上也是如此!频道默认使用共享事件循环组),与线程数量有限,因此每个线程都可以有自己的处理器内核进行处理。但我希望这是一种罕见的情况;您很快就要将服务器二进制文件一分为二,以避免 GC 和服务之间类似的相互作用。