GRPC: сделать высокопроизводительный клиент на Java / Scala

9

У меня есть сервис, который передает сообщения с довольно высокой скоростью.

В настоящее время он обслуживается akka-tcp и составляет 3,5 миллиона сообщений в минуту. Я решил попробовать grpc. К сожалению, это привело к гораздо меньшей пропускной способности: ~ 500 тыс. Сообщений в минуту и ​​даже меньше.

Не могли бы вы порекомендовать, как его оптимизировать?

Моя настройка

Аппаратное обеспечение : 32 ядра, куча 24 Гб.

версия grpc: 1.25.0

Формат сообщения и конечная точка

Сообщение в основном двоичный двоичный объект. Клиент направляет 100K - 1M и более сообщений в один и тот же запрос (асинхронно), сервер не отвечает ни на что, клиент использует неоперативный наблюдатель

service MyService {
    rpc send (stream MyMessage) returns (stream DummyResponse);
}

message MyMessage {
    int64 someField = 1;
    bytes payload = 2;  //not huge
}

message DummyResponse {
}

Проблемы: Скорость передачи сообщений низкая по сравнению с реализацией akka. Я наблюдаю низкую загрузку процессора, поэтому я подозреваю, что вызов grpc фактически блокируется внутри, несмотря на то, что он говорит об обратном. призваниеonNext() действительно не сразу возвращается, но на столе также есть GC.

Я пытался породить больше отправителей, чтобы смягчить эту проблему, но не получил большого улучшения.

Мои выводы Grpc фактически выделяет 8-килобайтный буфер байтов для каждого сообщения при его сериализации. Смотрите трассировку стека:

java.lang.Thread.State: BLOCKED (на мониторе объекта) на com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) на com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) в io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) в io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) в io.grpc.internal.javaFramerFramer.wmer.Fmer : 168) в io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) в io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) в io.grpc.internal.ForwardingClientStream.writeMessageStreaming. Forward. Java: 37) в io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) в io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) по адресу io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) по адресу io.grpc.ForwardingClientCall.sendMessage (ForwardingClioCall.Force.IcCent.dll) (ForwardingClientCall.java:37) в io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Любая помощь с лучшими практиками по созданию высокопроизводительных клиентов GRPC приветствуется.

simpadjo
источник
Вы используете Protobuf? Этот путь к коду следует использовать только в том случае, если InputStream, возвращаемый MethodDescriptor.Marshaller.stream (), не реализует Drainable. Protobuf Marshaller поддерживает Drainable. Если вы используете Protobuf, возможно ли, что ClientInterceptor меняет MethodDescriptor?
Эрик Андерсон
@EricAnderson спасибо за ваш ответ. Я попробовал стандартный protobuf с Gradle (com.google.protobuf: protoc: 3.10.1, io.grpc: protoc-gen-grpc-java: 1.25.0), а также scalapb. Вероятно, эта трассировка стека действительно была от сгенерированного скальпом кода. Я удалил все, что связано с scalapb, но это не сильно повлияло на производительность.
Simpadjo
@EricAnderson Я решил свою проблему. Пингует вас как разработчика grpc. Имеет ли смысл мой ответ?
Simpadjo

Ответы:

4

Я решил проблему, создав несколько ManagedChannelэкземпляров для каждого пункта назначения. Несмотря на статьи говорят, чтоManagedChannel может порождать достаточно соединений, поэтому достаточно одного экземпляра, но в моем случае это не так

Производительность находится в паритете с реализацией akka-tcp.

simpadjo
источник
1
ManagedChannel (со встроенными политиками LB) не использует более одного соединения на серверную часть. Таким образом, если у вас высокая пропускная способность с небольшим количеством бэкэндов, можно насыщать соединения со всеми бэкэндами. Использование нескольких каналов может повысить производительность в этих случаях.
Эрик Андерсон
@EricAnderson спасибо. В моем случае помогло
создание
Чем меньше бэкендов и чем выше пропускная способность, тем больше вероятность, что вам понадобится несколько каналов. Так что «единый бэкэнд» сделает более вероятным использование большего количества каналов.
Эрик Андерсон
0

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

Например gRPC, построен поверх HTTP 1.1/2, который является протоколом на уровне приложений , или L7, и, как таковая, его производительность ограничена производительностью HTTP. Теперь HTTPон строится поверх TCP, на транспортном уровне , или L4, таким образом, мы можем сделать вывод, что gRPCпропускная способность не может быть больше, чем эквивалентный код, обслуживаемый на TCPуровне.

Другими словами: если ваш сервер способен обрабатывать необработанные TCPпакеты, как добавление новых уровней сложности ( gRPC) повысит производительность?

Batato
источник
Именно по этой причине я использую потоковый подход: я плачу один раз за установление http-соединения и отправляю ~ 300M сообщений, используя его. Это использует веб-сокеты под капотом, который я ожидаю иметь относительно низкие накладные расходы.
Simpadjo
Для gRPCвас также заплатить один раз для установления соединения, но вы добавили дополнительное бремя разбора Protobuf. В любом случае, трудно догадываться без лишней информации, но я бы поспорил, что в целом, поскольку вы добавляете дополнительные шаги кодирования / декодирования в свой конвейер, gRPCреализация будет медленнее, чем аналогичная веб-сокетная.
Batato
Акка также добавляет некоторые накладные расходы. Во всяком случае, x5 замедление выглядит слишком много.
Simpadjo
Я думаю, что вы можете найти это интересным: github.com/REASY/akka-http-vs-akka-grpc , в его случае (и я думаю, что это распространяется на ваш), узкое место может быть связано с большим использованием памяти в protobuf (de ) сериализация, которая, в свою очередь, вызывает больше обращений к сборщику мусора.
Batato
спасибо, интересно, несмотря на то, что я уже решил свою проблему
simpadjo
0

Я очень впечатлен тем, как хорошо Akka TCP выступил здесь: D

Наш опыт был немного другим. Мы работали над гораздо меньшими экземплярами, используя кластер Akka. Для удаленного взаимодействия Akka мы изменили TCP / Akka на UDP, используя Artery, и получили гораздо более высокую скорость + более низкое и более стабильное время отклика. В Artery есть даже конфиг, помогающий сбалансировать потребление ресурсов процессора и время отклика при холодном запуске.

Мое предложение состоит в том, чтобы использовать некоторую платформу на основе UDP, которая также заботится о надежности передачи (например, это Artery UDP), и просто сериализовать, используя Protobuf, вместо использования полной gRPC плоти. Канал передачи HTTP / 2 не предназначен для целей с высокой пропускной способностью и низким временем отклика.

Ван Сиань
источник