为什么 gRPC 比发送数组的 HTTP API 慢得多

J.F*_*.F. 10 rest protocol-buffers grpc grpc-node

我正在 Node.JS 中实现的服务之间进行负载测试,同一台机器上的两个服务都通过localhost.

有 REST 和 gRPC 客户端和服务器文件。主要目标是证明 gRPC 比 HTTP 调用更快,因为使用 HTTP/2、使用比编码/解码 JSON 更高效的协议缓冲区......

但在我的测试中(发送整数数组)gRPC 慢得多。

对于这两种实现来说,代码都非常简单,我有一个辅助类来生成大小为(以 MB 为单位)的对象:0.125、0.25、0.5、1、2、5、20。REST 和 gRPC 服务器使用此辅助类,因此对象发送是一样的。

有效负载中发送的对象如下所示:

{
  message: "Hello world",
  array: []
}
Run Code Online (Sandbox Code Playgroud)

数组中填充数字,直到达到所需的大小。

我的 .proto 是这样的:

syntax = "proto3";

service ExampleService {
    rpc GetExample (Size) returns (Example) {}
}

message Size {
    int32 size = 1;
}

message Example {
   string message = 1;
   repeated int32 array = 2;
}
Run Code Online (Sandbox Code Playgroud)

另外,我运行的应用程序仅测量一次调用,不创建循环并查找平均值,也不用回调处理测量时间。所以我运行该应用程序 10 次并计算平均值。

休息服务器:

app.get('/:size',(req,res) => {
    const size = req.params.size
    res.status(200).send(objects[size])
})
Run Code Online (Sandbox Code Playgroud)

休息客户端:

const start = performance.now()
const response = await axios.get(`http://localhost:8080/${size}`)
const end = performance.now()
Run Code Online (Sandbox Code Playgroud)

gRPC 服务器:

getExample:(call, callback) => {
    callback(null, objects.objects[call.request.size])
}
Run Code Online (Sandbox Code Playgroud)

和 gRPC 客户端:

const start = performance.now()
client.getExample({ size: size }, (error, response) => {
    const end = performance.now()
})
Run Code Online (Sandbox Code Playgroud)

为了更有效地做事,我尝试过:

  1. 像这样压缩数据:
let server = new grpc.Server({
    'grpc.default_compression_level': 3, // (1->Low -- 3->High)
});
Run Code Online (Sandbox Code Playgroud)

我知道我可以用来streaming获取数据并迭代数组,但我想证明这两种方法中的“相同的调用”。

而且差别这么大。

我看到的另一件事是,使用 REST 方式的时间更加“线性”,时间之间的差异很小,但使用 gRPC 一次调用发送 2MB 可能需要 220 毫秒,而下一个调用可能需要 500 毫秒。

这是最终的比较,您可以看到差异相当大。

数据:

大小(MB) 休息(毫秒) gRPC(毫秒)
0,125 37.98976998329162 35.5489800453186
0,25 40.03781998157501 46.077759981155396
0,5 51.35283002853394 59.37109994888306
1 63.4725800037384 166.7616500457128
2 95.76031665007274 394.2442199707031
5 261.9365399837494 804.1371199131012
20 713.1867599964141 5492.330539941788

在此输入图像描述

但我想...也许数组字段无法以有效的方式解码,也许是整数,这对于 JSON 来说并不重...我不知道,所以我将尝试发送一个绳子,一根非常巨大的绳子。

所以我的原型文件现在看起来像这样:

syntax = "proto3";

service ExampleService {
    rpc GetExample (Size) returns (Example) {}
}

message Size {
    int32 size = 1;
}

message Example {
   string message = 1;
   string array = 2;
}
Run Code Online (Sandbox Code Playgroud)

现在发送的对象是这样的:

{
  message: "Hello world",
  array: "text to reach the desired MB"
}
Run Code Online (Sandbox Code Playgroud)

结果差异很大,现在 gRPC 效率更高了。

数据:

大小(MB) 休息(毫秒) gRPC(毫秒)
0,125 30.672580003738403 25.028959941864013
0,25 33.568540048599246 25.366739988327026
0,5 37.19938006401062 27.539460039138795
1 46.4020166794459 28.798949996630352
2 57.50188330809275 35.45066670576731
5 107.39933327833812 48.90079998970032
20 313.4138665994008 136.4138500293096

在此输入图像描述

问题是:那么,为什么发送整数数组不如发送字符串那么有效?protobuf 是对数组进行编码/解码的方式吗?是不是高效发送repeated值?与语言(JS)有关吗?

Dan*_*Dan 6

gRPC(实际上是 protobufs)在您的示例中无法很好地扩展的原因是,字段的每个条目repeated都会导致 protobuf 需要解码单独的字段,并且存在与此相关的开销。您可以在此处的文档中查看有关重复字段编码的更多详细信息。您正在使用proto3,因此至少您不需要指定该[packed=true]选项,尽管如果您使用 ,这会有所帮助proto2

\n

string切换到or字段的速度如此之快的原因bytes是,该字段只有一个恒定的解码成本,它不会随着该字段中编码的数据量而扩展(尽管不确定 JS,这可能需要创建数据的副本,但显然这仍然比实际解析数据快得多)。只需确保您的协议定义了字段中数据的格式/字节顺序:-)

\n

在更高层次上回答你的问题,在单个 API 调用中发送多个兆字节通常并不是一个令人惊奇的想法 - 它会长时间占用服务器和客户端上的线程,这迫使你使用多线程或异步代码来获得合理的性能。(诚​​然,这可能不是什么问题,因为您习惯在 Node 上编写异步内容,但服务器上仍然只有这么多 CPU 需要消耗。)

\n

根据您实际尝试执行的操作,常见模式可能是将数据写入共享存储系统(S3 等)中的文件并将文件名传递给其他服务,然后该服务可以在以下情况下下载它:它实际上是需要的。

\n