#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()
метод. Хитрость заключается в формировании очереди запросов, чтобы не было ожидания в другом потоке.