移动端基准测试
随着 gRPC 成为一个更好更快的 RPC 框架,我们不断被问到这样一个问题:“gRPC 到底快了多少?” 我们已经有了全面的服务器端基准测试,但我们没有移动端基准测试。基准测试客户端与基准测试服务器略有不同。我们更关心诸如延迟和请求大小之类的事情,而不太关心诸如每秒查询次数 (QPS) 和并发线程数之类的事情。因此,我们构建了一个 Android 应用程序,以便量化这些因素并提供支持它们的可靠数据。
我们想要具体测试的是客户端 protobuf 与 JSON 序列化/反序列化以及 gRPC 与 RESTful HTTP JSON 服务。对于序列化基准测试,我们想测量消息的大小以及序列化和反序列化的速度。对于 RPC 基准测试,我们想测量端到端请求的延迟和数据包大小。
Protobuf vs. JSON
为了测试 protobuf 和 JSON,我们在随机生成的 proto 上一遍又一遍地运行序列化和反序列化,可以在 这里 看到。这些 proto 的大小和复杂性差异很大,从几个字节到超过 100kb。创建了 JSON 等效项,然后也进行了基准测试。对于 protobuf 消息,我们有三种主要的序列化和反序列化方法:简单地使用字节数组,CodedOutputStream
/CodedInputStream
(这是 protobuf 自己的输入和输出流实现)以及 Java 的 ByteArrayOutputStream
和 ByteArrayInputStream
。对于 JSON,我们使用了 org.json
的 JSONObject
。这只有一种序列化和反序列化方法,分别为 toString()
和 new JSONObject()
。
为了使基准测试尽可能准确,我们将要测试的代码包装在一个接口中,然后简单地循环它以进行设定的迭代次数。这样,我们就忽略了任何检查系统时间所花费的时间。
interface Action {
void execute();
}
// Sample benchmark of multiplication
Action a = new Action() {
@Override
public void execute() {
int x = 1000 * 123456;
}
}
for (int i = 0; i < 100; ++i) {
a.execute();
}
在运行基准测试之前,我们运行了一个预热,以便清除 JVM 的任何不规律行为,然后计算在设定时间(在 protobuf vs. JSON 的情况下为 10 秒)内运行所需的迭代次数。为此,我们从 1 次迭代开始,测量该运行所花费的时间,并将其与最小采样时间(在我们的例子中为 2 秒)进行比较。如果迭代次数花费的时间足够长,我们通过进行一些数学运算来估计运行 10 秒所需的迭代次数。否则,我们将迭代次数乘以 2 并重复。
// This can be found in ProtobufBenchmarker.java benchmark()
int iterations = 1;
// Time action simply reports the time it takes to run a certain action for that number of iterations
long elapsed = timeAction(action, iterations);
while (elapsed < MIN_SAMPLE_TIME_MS) {
iterations *= 2;
elapsed = timeAction(action, iterations);
}
// Estimate number of iterations to run for 10 seconds
iterations = (int) ((TARGET_TIME_MS / (double) elapsed) * iterations);
结果
基准测试在 protobuf、JSON 和 gzip 压缩的 JSON 上运行。
我们发现,无论使用哪种 protobuf 的序列化/反序列化方法,其序列化速度始终比 JSON 快约 3 倍。 对于反序列化,JSON 在处理小消息(<1kb)时实际上略快,约为 1.5 倍,但对于较大的消息(>15kb),protobuf 快 2 倍。 对于经过 gzip 压缩的 JSON,protobuf 在序列化方面的速度远远快 5 倍以上,无论消息大小。 对于反序列化,两者在处理小消息时的速度相差不大,但 protobuf 在处理较大消息时的速度快约 3 倍。 可以在 README 中更深入地探索和复现这些结果。
gRPC vs. HTTP JSON
为了对 RPC 调用进行基准测试,我们需要测量端到端延迟和带宽。 为此,我们与服务器进行 60 秒的乒乓测试,每次使用相同的消息,并测量延迟和消息大小。 消息包含一些供服务器读取的字段,以及一个字节有效负载。 我们比较了 gRPC 的一元调用与简单的 RESTful HTTP JSON 服务。 gRPC 基准测试创建一个通道,并启动一个一元调用,该调用在收到响应时重复执行,直到 60 秒过去。 响应包含一个带有相同有效负载的 proto。
类似地,对于 HTTP JSON 基准测试,它向服务器发送一个 POST 请求,其中包含一个等效的 JSON 对象,服务器发回一个包含相同有效负载的 JSON 对象。
// This can be found in AsyncClient.java doUnaryCalls()
// Make stub to send unary call
final BenchmarkServiceStub stub = BenchmarkServiceGrpc.newStub(channel);
stub.unaryCall(request, new StreamObserver<SimpleResponse>() {
long lastCall = System.nanoTime();
// Do nothing on next
@Override
public void onNext(SimpleResponse value) {
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
System.err.println("Encountered an error in unaryCall. Status is " + status);
t.printStackTrace();
future.cancel(true);
}
// Repeat if time isn't reached
@Override
public void onCompleted() {
long now = System.nanoTime();
// Record the latencies in microseconds
histogram.recordValue((now - lastCall) / 1000);
lastCall = now;
Context prevCtx = Context.ROOT.attach();
try {
if (endTime > now) {
stub.unaryCall(request, this);
} else {
future.done();
}
} finally {
Context.current().detach(prevCtx);
}
}
});
使用了 HttpUrlConnection
和 OkHttp 库。
仅对 gRPC 的一元调用与 HTTP 进行了基准测试,因为流式调用比一元调用快 2 倍以上。 此外,HTTP 没有等效的流式传输,流式传输是 HTTP/2 的特定功能。
结果
在延迟方面,gRPC 在高达 95% 的百分位数时快 5 倍-10 倍,端到端请求的平均延迟约为 2 毫秒。 在带宽方面,对于小型请求(100-1000 字节有效负载),gRPC 快约 3 倍,对于大型请求(10kb-100kb 有效负载),则始终快 2 倍。 要复现这些结果或进行更深入的探索,请查看我们的 存储库。