RSS

移动基准测试

随着 gRPC 成为一个更好更快的 RPC 框架,我们一直被问到:“gRPC 到底快了多少?” 我们已经有了全面的服务器端基准测试,但没有移动端基准测试。对客户端进行基准测试与对服务器进行基准测试有些不同。我们更关注延迟和请求大小等方面,而不那么关注每秒查询数(QPS)和并发线程数等。因此,我们构建了一个 Android 应用程序,以量化这些因素并提供确凿的数据支持。

具体来说,我们要对客户端 Protobuf 与 JSON 的序列化/反序列化以及 gRPC 与 RESTful HTTP JSON 服务进行基准测试。对于序列化基准测试,我们希望测量消息的大小以及序列化和反序列化的速度。对于 RPC 基准测试,我们希望测量端到端请求的延迟和数据包大小。

Protobuf vs. JSON

为了对 Protobuf 和 JSON 进行基准测试,我们对随机生成的 Protobuf 消息重复进行序列化和反序列化,这些消息可以在这里查看。这些 Protobuf 消息的大小和复杂性差异很大,从只有几个字节到超过 100kb 不等。我们还创建了相应的 JSON 等效物并进行了基准测试。对于 Protobuf 消息,我们主要有三种序列化和反序列化方法:直接使用字节数组、Protobuf 自己的输入输出流实现 CodedOutputStream/CodedInputStream,以及 Java 的 ByteArrayOutputStreamByteArrayInputStream。对于 JSON,我们使用了 org.jsonJSONObject。它只有一种序列化和反序列化方法,分别是 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 和 gzipped JSON 上运行。

我们发现,无论 Protobuf 使用何种序列化/反序列化方法,其序列化速度始终比 JSON 快约 3 倍。对于反序列化,JSON 在小消息(<1kb)时实际上稍快,约为 1.5 倍,但对于大消息(>15kb)时 Protobuf 快 2 倍。对于 gzipped JSON,无论大小如何,Protobuf 的序列化速度都远超 5 倍。对于反序列化,两者在小消息时速度相近,但 Protobuf 在大消息时快约 3 倍。更深入的测试结果和复现方法可在README 文件中查看。

gRPC vs. HTTP JSON

为了对 RPC 调用进行基准测试,我们希望测量端到端延迟和带宽。为此,我们与服务器进行 60 秒的乒乓式传输,每次使用相同的消息,并测量延迟和消息大小。消息包含一些供服务器读取的字段和一个字节有效负载。我们将 gRPC 的一元调用与一个简单的 RESTful HTTP JSON 服务进行比较。gRPC 基准测试会创建一个通道,并启动一个一元调用,该调用在收到响应后会重复进行,直到 60 秒过去。响应中包含一个带有相同有效负载的 Protobuf 消息。

同样地,对于 HTTP JSON 基准测试,它会向服务器发送一个包含等效 JSON 对象的 POST 请求,服务器则返回一个带有相同有效负载的 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);
        }
    }
});

使用了 HttpUrlConnectionOkHttp 库

只有 gRPC 的一元调用与 HTTP 进行了基准测试,因为流式调用比一元调用快 2 倍以上。此外,HTTP 没有等效的流式功能,流式是 HTTP/2 特有的功能。

结果

在延迟方面,gRPC 在 95 百分位之前快 5 到 10 倍,端到端请求的平均延迟约为 2 毫秒。在带宽方面,gRPC 对于小型请求(100-1000 字节有效负载)快约 3 倍,对于大型请求(10kb-100kb 有效负载)始终快 2 倍。要复现这些结果或进行更深入的探索,请查看我们的仓库