GRPC: criar cliente de alto rendimento em Java / Scala

9

Eu tenho um serviço que transfere mensagens a uma taxa bastante alta.

Atualmente, é atendido pelo akka-tcp e gera 3,5 milhões de mensagens por minuto. Decidi experimentar o grpc. Infelizmente, resultou em uma taxa de transferência muito menor: ~ 500 mil mensagens por minuto e até menos.

Você poderia recomendar como otimizá-lo?

Minha configuração

Hardware : 32 núcleos, pilha de 24 GB.

versão grpc: 1.25.0

Formato da mensagem e ponto final

A mensagem é basicamente um blob binário. O cliente transmite de 100K a 1M e mais mensagens na mesma solicitação (de forma assíncrona), o servidor não responde com nada, o cliente usa um observador não operacional

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

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

message DummyResponse {
}

Problemas: a taxa de mensagens é baixa em comparação com a implementação do akka. Observo baixo uso da CPU e, portanto, suspeito que a chamada grpc esteja realmente bloqueando internamente, apesar de dizer o contrário. Ligar de onNext()fato não retorna imediatamente, mas também há GC na mesa.

Tentei gerar mais remetentes para mitigar esse problema, mas não obtive muitas melhorias.

Minhas descobertas O Grpc realmente aloca um buffer de 8 KB em cada mensagem quando o serializa. Veja o stacktrace:

java.lang.Thread.State: BLOCKED (no monitor de objeto) em com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) em com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) em io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) em io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) em io.grpc.internal.MessageFramer.writeUncom : 168) em io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) em io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) em io.grpc.internal.ForwardingClientStream.writeMessage (ForwardingClientStream. java: 37) em io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) em io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) em io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) em io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.java:c.): (ForwardingClientCall.java:37) em io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Qualquer ajuda com as melhores práticas na criação de clientes grpc de alto rendimento é apreciada.

simpadjo
fonte
Você está usando o Protobuf? Esse caminho de código deve ser utilizado apenas se o InputStream retornado por MethodDescriptor.Marshaller.stream () não implementar Drainable. O Protobuf Marshaller suporta Drenável. Se você estiver usando o Protobuf, é possível que um ClientInterceptor esteja alterando o MethodDescriptor?
Eric Anderson
@EricAnderson, obrigado por sua resposta. Tentei o protobuf padrão com gradle (com.google.protobuf: protoc: 3.10.1, io.grpc: protoc-gen-grpc-java: 1.25.0) e também scalapb. Provavelmente, esse rastreamento de pilha foi realmente do código gerado pelo scalapb. Eu removi tudo relacionado ao scalapb, mas não ajudou muito no desempenho.
Simpadjo #
@EricAnderson Resolvi meu problema. Pingando você como desenvolvedor do grpc. Minha resposta faz sentido?
Simpadjo 13/11/19

Respostas:

4

Resolvi o problema criando várias ManagedChannelinstâncias por destino. Apesar dos artigos dizerem que umManagedChannel pode gerar conexões suficientes por si só, uma instância é suficiente, não era verdade no meu caso.

O desempenho está em paridade com a implementação do akka-tcp.

simpadjo
fonte
11
O ManagedChannel (com políticas LB internas) não usa mais de uma conexão por back-end. Portanto, se você possui alto desempenho com poucos recursos, é possível saturar as conexões com todos os recursos. O uso de vários canais pode aumentar o desempenho nesses casos.
Eric Anderson
@EricAnderson thanks. No meu caso desova vários canais, mesmo para um nó de back-end único ajudou
simpadjo
Quanto menos back-end e maior a largura de banda, maior a probabilidade de você precisar de vários canais. Portanto, o "back-end único" tornaria mais provável que mais canais sejam úteis.
Eric Anderson
0

Pergunta interessante. Os pacotes de rede de computadores são codificados usando uma pilha de protocolos , e esses protocolos são criados com base nas especificações do anterior. Portanto, o desempenho (taxa de transferência) de um protocolo é limitado pelo desempenho do usado para construí-lo, uma vez que você está adicionando etapas extras de codificação / decodificação sobre o subjacente.

Por exemplo, gRPCé construído em cima de HTTP 1.1/2, que é um protocolo na camada Aplicativo ou L7, e, como tal, seu desempenho é limitado pelo desempenho de HTTP. Agora, HTTPele próprio é construído em cima de TCP, que está na camada Transporte , ou L4, portanto, podemos deduzir que a gRPCtaxa de transferência não pode ser maior que um código equivalente exibido noTCP camada.

Em outras palavras: se o servidor é capaz de lidar com TCPpacotes brutos , como adicionar novas camadas de complexidade ( gRPC) melhoraria o desempenho?

Batato
fonte
Por esse motivo, eu uso a abordagem de streaming: pago uma vez pelo estabelecimento de uma conexão http e envio ~ 300 milhões de mensagens usando-a. Ele usa websockets sob o capô, o que espero ter uma sobrecarga relativamente baixa.
Simpadjo 9/11/19
Para gRPCvocê também paga uma vez para estabelecer uma conexão, mas você adicionou a carga extra de análise protobuf. De qualquer forma, é difícil fazer suposições sem muita informação, mas eu apostaria que, em geral, como você está adicionando etapas extras de codificação / decodificação ao seu pipeline, a gRPCimplementação seria mais lenta que a do soquete da Web equivalente.
Batato
Akka acrescenta algumas despesas gerais também. Enfim, a desaceleração x5 parece demais.
Simpadjo 10/11/19
Acho que você pode achar isso interessante: github.com/REASY/akka-http-vs-akka-grpc , no caso dele (e acho que isso se estende ao seu), o gargalo pode ser devido ao alto uso de memória no protobuf (de ) serialização, que, por sua vez, aciona mais chamadas para o coletor de lixo.
Batato
graças, interessante, apesar de eu já resolveu o meu problema
simpadjo
0

Estou bastante impressionado com o desempenho do Akka TCP aqui: D

Nossa experiência foi um pouco diferente. Estávamos trabalhando em instâncias muito menores usando o Akka Cluster. Para o Akka remoting, mudamos de Akka TCP para UDP usando o Artéria e alcançamos uma taxa muito mais alta + tempo de resposta mais baixo e mais estável. Existe até uma configuração no Artéria que ajuda a equilibrar entre o consumo da CPU e o tempo de resposta desde o início a frio.

Minha sugestão é usar alguma estrutura baseada em UDP que também cuide da confiabilidade da transmissão para você (por exemplo, a UDP da artéria) e apenas serialize usando o Protobuf, em vez de usar o gRPC completo. O canal de transmissão HTTP / 2 não é realmente para fins de alto rendimento e baixo tempo de resposta.

Wang Xian
fonte