GRPC:在Java / Scala中制作高吞吐量客户端


9

我有一项可以以很高的速率传输邮件的服务。

目前,它由akka-tcp提供服务,每分钟发送350万条消息。我决定尝试一下grpc。不幸的是,它导致吞吐量大大降低:每分钟约50万条消息甚至更低。

您能推荐如何优化它吗?

我的设定

硬件:32核,24Gb堆。

grpc版本:1.25.0

消息格式和端点

消息基本上是一个二进制blob。客户端将100K-1M和更多消息流传输到同一请求中(异步),服务器不响应任何内容,客户端使用无操作观察者

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

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

message DummyResponse {
}

问题:与akka实施相比,消息速率低。我观察到CPU使用率较低,因此我怀疑grpc调用实际上在内部阻塞,尽管它另有说明。onNext()确实不会立即返回调用,但表上也有GC。

我试图产生更多的发件人来缓解此问题,但并没有太大的改进。

我的发现 Grpc在序列化每个消息时实际上分配了一个8KB的字节缓冲区。请参阅堆栈跟踪:

java.lang.Thread.State:在com.google.common.io.ByteStreams.copy(ByteStreams.java:com.google.common.io.ByteStreams.createBuffer(ByteStreams.java:58)处处于阻塞状态(在对象监视器上): 105)在io.grpc.internal.MessageFramer.writeToOutputStream(MessageFramer.java:274)在io.grpc.internal.MessageFramer.writeKnownLengthUncompressed(MessageFramer.java:230)在io.grpc.internal.MessageFramer.writeUncompressed(MessageFramer.java :168)位于io.grpc.internal.MessageFramer.writePayload(MessageFramer.java:141)位于io.grpc.internal.AbstractStream.writeMessage(AbstractStream.java:53)位于io.grpc.internal.ForwardingClientStream.writeMessage(ForwardingClientStream。 java:37)位于io.grpc.internal.DelayedStream.writeMessage(DelayedStream.java:252)位于io.grpc.internal。io.grpc.internal.ClientCallImpl.sendMessage(ClientCallImpl.java:457)的io.grpc.ForwardingClientCall.sendMessage(ForwardingClientCall.java:37)的io.grpc.internal.ClientCallImpl.sendMessageInternal(ClientCallImpl.java:473)io.grpc.ForwardingClientCall.sendMessage的(ForwardingClientCall.java:37)位于io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext(ClientCalls.java:346)

对于构建高吞吐量grpc客户的最佳做法的任何帮助,均表示赞赏。


您在使用Protobuf吗?仅当MethodDescriptor.Marshaller.stream()返回的InputStream不实现Drainable时,才应采用此代码路径。Protobuf Marshaller确实支持Drainable。如果您使用的是Protobuf,则ClientInterceptor是否可以更改MethodDescriptor?
埃里克·安德森

@EricAnderson谢谢您的回复。我使用gradle(com.google.protobuf:protoc:3.10.1,io.grpc:protoc-gen-grpc-java:1.25.0)和gradle尝试了标准protobuf scalapb。可能此堆栈跟踪确实是从scalapb生成的代码。我删除了与scalapb相关的所有内容,但对性能没有太大帮助。
simpadjo

@EricAnderson我解决了我的问题。请您作为grpc的开发人员。我的答案有意义吗?
simpadjo,

Answers:


4

我通过在ManagedChannel每个目标位置创建多个实例来解决此问题。尽管有文章说a ManagedChannel本身可以产生足够的连接,所以一个实例就足够了,在我的情况下并不是这样。

性能与akka-tcp实现相当。


1
每个后端的ManagedChannel(具有内置的LB策略)使用的连接数不超过一个。因此,如果您的后端吞吐量高而后端很少,则可以使与所有后端的连接饱和。在这些情况下,使用多个渠道可以提高性能。
埃里克·安德森

@EricAnderson谢谢。以我
为例,

后端越少,带宽越高,就越可能需要多个通道。因此,“单个后端”将使其更有可能使用更多渠道是有帮助的。
埃里克·安德森

0

有趣的问题。使用协议栈对计算机网络软件包进行编码,并且这些协议是在前一个协议的规范之上构建的。因此,由于要在基础协议之上添加额外的编码/解码步骤,因此协议的性能(吞吐量)受构建协议的性能的限制。

例如gRPC建立在之上HTTP 1.1/2,它是应用层上的协议,或L7如此,它的性能受HTTP。现在,HTTP它本身是基于TCP,位于传输层或之上的,L4因此我们可以推断出gRPC吞吐量不能大于该TCP层中服务的等效代码。

换句话说:如果您的服务器能够处理原始数据TCP包,那么如何增加新的复杂度(gRPC)层将如何提高性能?


正是由于这个原因,我使用了流式传输方法:我为建立http连接付费一次,并使用它发送约300M消息。它在后台使用websockets,我希望其开销相对较低。
simpadjo,

因为gRPC您还要为建立连接付费一次,但是您增加了解析protobuf的额外负担。无论如何,很难在没有太多信息的情况下进行猜测,但是我敢打赌,一般来说,由于要在管道中添加额外的编码/解码步骤,因此gRPC实现的速度将比等效的Web套接字慢。
Batato

Akka也增加了一些开销。无论如何,x5减速看起来太多了。
simpadjo

我认为您可能会发现这很有趣:github.com/REASY/akka-http-vs-akka-grpc,在他的情况下(我认为这延伸到您的情况),瓶颈可能是由于protobuf中的高内存使用率(de )序列化,进而触发对垃圾收集器的更多调用。
Batato

谢谢,有趣,尽管我已经解决了我的问题
simpadjo

0

我对Akka TCP的出色表现印象深刻:D

我们的经验略有不同。我们正在使用Akka Cluster处理更小的实例。对于Akka远程处理,我们使用Artery从Akka TCP更改为UDP,并实现了更高的速率+更低的响应时间和更稳定的响应时间。Artery中甚至有一个配置可帮助在CPU消耗和冷启动响应时间之间取得平衡。

我的建议是使用一些基于UDP的框架,该框架也为您解决传输可靠性问题(例如,Artery UDP),而只是使用Protobuf进行序列化,而不是使用完整的gRPC。HTTP / 2传输通道并不是真正用于高吞吐量,低响应时间的目的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.