один grpc-сервер как для унарного, так и для bidi-потока с netty

#java #netty #grpc #http2 #grpc-java

#java #netty #grpc #http2 #grpc-java

Вопрос:

У меня есть требование реализовать grpc-сервер на java, который способен обрабатывать как унарную, так и двунаправленную потоковую передачу grpc.Служба, использующая grpc bidi-streaming, может отправлять большое количество сообщений в секунду.(возможно, 2000 сообщений в секунду или более) У меня есть две реализации, и я немного запутался, какая из них больше всего подходит для моих требований.

1. Используйте один и тот же сервер как для однонаправленного, так и для двунаправленного grpc.

При использовании этого подхода, поскольку и унарный grpc, и поток bidi используют один и тот же порт, один основной поток будет выделен как для унарного, так и для bidi-потока. Поэтому я не уверен, насколько хорошо он работает в случае, если потоки bidi получают большое количество сообщений в секунду. (Я имею в виду, будет ли основной поток занят для потоков 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 унарного, а другой для grpc bidi-потокового.

Здесь ранее упомянутой проблемы нет, поскольку мы выделяем 2 основных потока по одному для каждого унарного потока grpc и потока bidi. Но для служб я использую исполнитель, который использует java ThreadPoolExecutor, и мой вопрос в том, должен ли я использовать 2 пула потоков для двух служб, которые используют потоковую передачу grpc unary и 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);

    // 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));
    }
 

Комментарии:

1. Рассматривали ли вы возможность написания каких-либо стресс-тестов для обеих реализаций?

2. Да, думал об этом, но не нашел времени для этого. Проведем некоторое тестирование производительности и посмотрим.

3. @drfloob Но я думаю, это больше похоже на обычный вариант использования. Итак, должен быть стандартный способ сделать это правильно?

Ответ №1:

Используйте один сервер.

Основной поток используется только для принятия () новых подключений. Он не используется для фактической обработки. Это выполняется циклами рабочих событий. Каждое соединение назначается одному циклу событий, и один цикл событий может обслуживать несколько подключений.

В одном потоке Netty может обрабатывать 100 тыс. сообщений в секунду. Но это на самом деле медленно. Поиск границ сообщений обрабатывается другим потоком, отличным от потока доставки сообщений, и обмен данными между этими двумя потоками увеличивает задержку. Эта добавленная задержка замедляет работу. С requests(5) помощью хитрости, позволяющей избежать задержек, один поток Netty может обрабатывать 1250 тыс. сообщений в секунду. (Эти показатели производительности будут варьироваться в зависимости от машины, на которой вы их запускаете, но они явно намного выше, чем вам нужно.) См https://github.com/grpc/grpc-java/issues/6696 где обсуждается проблема задержки.

Но, допустим, на мгновение вам нужна более высокая производительность или вы хотите отделить унарный трафик от потокового трафика. В этом случае мы бы рекомендовали использовать два разных канала. Каждый канал будет использовать свои собственные соединения и (возможно) отдельные циклы рабочих событий.

Только если вы очень обеспокоены задержкой, вам следует разделить два типа трафика на отдельные серверы. (Таким образом, у вас также есть тесты, показывающие, насколько это помогает.) И да, используя отдельные серверы и каналы со своими собственными workerEventLoopGroup() s (в том числе и на канале! Каналы по умолчанию используют общую группу циклов событий) с ограниченным количеством потоков, поэтому каждый может иметь свое собственное процессорное ядро для обработки. Но я бы ожидал, что это будет редкая ситуация; вы быстро приближаетесь к тому моменту, когда захотите разделить двоичный файл сервера на две части, чтобы избежать GC и аналогичного взаимодействия между службами.

Комментарии:

1. большое спасибо, и это имеет смысл. Кстати, что это за трюк с запросами (5)? Погуглил, и ничего, относящегося к этому.

2. Я думаю, что это ClientCall.request() вызов e, g. github.com/grpc/grpc-java/blob/master/benchmarks/src/jmh/java /…

3. В выпуске 6696 говорится об этом, но показан только хак, чтобы увидеть поведение. Хитрость заключается в том, чтобы просто вызвать «serverCall.request (5)» один раз для каждого RPC. Это можно сделать в перехватчике или привести StreamObserver к ServerCallStreamObserver тому, для которого есть идентичный request() метод. Хитрость заключается в формировании очереди запросов, чтобы не было ожидания в другом потоке.