菜单
开源

gRPC

概述

gRPC 是一个轻量级的开源 RPC 框架。它最初由 Google 开发,1.0 版本于 2016 年 8 月发布。此后,它受到了广泛关注并被广泛采用。

JSON 以人类可读的文本形式传输,而 gRPC 是二进制的。二进制格式使得数据传输更快、更紧凑。在我们所看到的基准测试中,gRPC 比其更传统的基于 JSON 的对应方 REST 快得多。gRPC 使用的消息和服务在 .proto 文件中描述,这些文件包含 Protocol Buffers (protobuf) 的定义。

使用 k6 对 gRPC 服务进行负载测试

从 k6 v0.49.0 开始,k6 支持一元 gRPC 请求和流式传输,作为 k6/net/grpc 核心模块的一部分。

gRPC 定义

在与 gRPC 服务交互之前,k6 需要了解消息和服务的定义。

一种方法是显式使用 Client.load() 方法并从本地文件系统加载客户端定义。该方法接受导入路径列表和 .proto 文件列表。然后 k6 从这些文件及其依赖项加载所有定义。

JavaScript
import { Client } from 'k6/net/grpc';

const client = new Client();
client.load(['definitions'], 'hello.proto');

或者,您可以使用 gRPC 反射协议动态加载定义。要启用反射,您可以将 reflect: true 选项传递给 Client.connect()。然后 k6 从服务器及其依赖项加载所有定义。

仅当服务器已实现反射支持时,此选项才可用。

JavaScript
import { Client } from 'k6/net/grpc';

const client = new Client();
client.connect('127.0.0.1:10000', { reflect: true });

一元 gRPC 请求

一元调用与常规 HTTP 请求的工作方式相同。向服务器发送一个请求,服务器回复一个响应。

JavaScript
import { Client, StatusOK } from 'k6/net/grpc';
import { check, sleep } from 'k6';

const client = new Client();
client.load(['definitions'], 'hello.proto');

export default () => {
  client.connect('127.0.0.1:10000', {});

  const data = { greeting: 'Bert' };
  const response = client.invoke('hello.HelloService/SayHello', data);

  check(response, {
    'status is OK': (r) => r && r.status === StatusOK,
  });

  console.log(JSON.stringify(response.message));

  client.close();
  sleep(1);
};

服务器 gRPC 流

在服务器流模式下,客户端向服务器发送一个请求,服务器回复多个响应。

以下示例演示了服务器流。

JavaScript
import { Client, Stream } from 'k6/net/grpc';
import { sleep } from 'k6';

const COORD_FACTOR = 1e7;

const client = new Client();

export default () => {
  if (__ITER == 0) {
    client.connect('127.0.0.1:10000', { plaintext: true, reflect: true });
  }

  const stream = new Stream(client, 'main.FeatureExplorer/ListFeatures', null);

  stream.on('data', function (feature) {
    console.log(
      `Found feature called "${feature.name}" at ${feature.location.latitude / COORD_FACTOR}, ${
        feature.location.longitude / COORD_FACTOR
      }`
    );
  });

  stream.on('end', function () {
    // The server has finished sending
    client.close();
    console.log('All done');
  });

  // send a message to the server
  stream.write({
    lo: {
      latitude: 400000000,
      longitude: -750000000,
    },
    hi: {
      latitude: 420000000,
      longitude: -730000000,
    },
  });

  sleep(0.5);
};

在示例脚本中,k6 连接到 gRPC 服务器,创建一个流,并向服务器发送包含纬度和经度坐标的消息。当服务器返回数据时,它会记录特征名称及其位置。当服务器完成发送数据后,它会关闭客户端连接并记录完成消息。

客户端 gRPC 流

客户端流模式与服务器流模式相反。客户端向服务器发送多个请求,服务器回复一个响应。

以下示例演示了客户端流。

JavaScript
import { Client, Stream } from 'k6/net/grpc';
import { sleep } from 'k6';

const COORD_FACTOR = 1e7;
const client = new Client();

// a sample points collection
const points = [
  {
    location: { latitude: 407838351, longitude: -746143763 },
    name: 'Patriots Path, Mendham, NJ 07945, USA',
  },
  {
    location: { latitude: 408122808, longitude: -743999179 },
    name: '101 New Jersey 10, Whippany, NJ 07981, USA',
  },
  {
    location: { latitude: 413628156, longitude: -749015468 },
    name: 'U.S. 6, Shohola, PA 18458, USA',
  },
  {
    location: { latitude: 419999544, longitude: -740371136 },
    name: '5 Conners Road, Kingston, NY 12401, USA',
  },
  {
    location: { latitude: 414008389, longitude: -743951297 },
    name: 'Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA',
  },
];

export default () => {
  if (__ITER == 0) {
    client.connect('127.0.0.1:10000', { plaintext: true, reflect: true });
  }

  const stream = new Stream(client, 'main.RouteGuide/RecordRoute');

  stream.on('data', (stats) => {
    console.log(`Finished trip with ${stats.pointCount} points`);
    console.log(`Passed ${stats.featureCount} features`);
    console.log(`Travelled ${stats.distance} meters`);
    console.log(`It took ${stats.elapsedTime} seconds`);
  });

  stream.on('end', () => {
    client.close();
    console.log('All done');
  });

  // send 3 random points
  for (let i = 0; i < 3; i++) {
    const point = points[Math.floor(Math.random() * points.length)];
    pointSender(stream, point);
  }

  // close the client stream
  stream.end();
};

const pointSender = (stream, point) => {
  console.log(
    `Visiting point ${point.name} ${point.location.latitude / COORD_FACTOR}, ${
      point.location.longitude / COORD_FACTOR
    }`
  );

  // send the location to the server
  stream.write(point.location);

  sleep(0.5);
};

在示例脚本中,k6 与 gRPC 服务器建立连接,创建一个流,并发送三个随机点。服务器会回复行程统计信息,这些信息会记录到控制台。代码还处理流的结束,关闭客户端并记录完成消息。

双向 gRPC 流

在双向流模式下,客户端和服务器都可以发送多条消息。

从 API 角度看,它结合了客户端流和服务器流模式,因此代码类似于上面的示例。

流式错误处理

要捕获流式传输期间发生的错误,您可以使用 error 事件处理程序。

处理程序接收 一个错误对象

JavaScript
import { Client, Stream } from 'k6/net/grpc';

const client = new Client();
const stream = new Stream(client, 'main.RouteGuide/RecordRoute');

stream.on('error', function (e) {
  // An error has occurred and the stream has been closed.
  console.log('Error: ' + JSON.stringify(e));
});

Protocol Buffers JSON 映射

重要的是要注意 k6 如何处理请求和消息。首先,它尝试以 JSON 格式封送请求/消息。然后,k6 使用 protojson 包编码或解码 Protobuf 消息。

在此过程中有一个限制是,您作为请求/消息传递的对象必须是可序列化的。这意味着像 Map 这样的结构不起作用。

使用 protojson 的好处是支持规范的 JSON 编码。Protocol Buffers 文档描述了这种映射。

示例

例如,如果您导入 "google/protobuf/wrappers.proto" 并且您的 proto 定义如下

proto
syntax = "proto3";

package testing;

import "google/protobuf/wrappers.proto";

service Service {
   rpc SayHey(google.protobuf.StringValue) returns (google.protobuf.StringValue);
   rpc DoubleInteger(google.protobuf.Int64Value) returns (google.protobuf.Int64Value);
}

传递消息时,您应该使用字符串或整数,而不是对象。因此,您将收到一个已经封送的类型。

JavaScript
import { Client } from 'k6/net/grpc';
const client = new Client();

// an example of passing a string
const respString = client.invoke('testing.Service/SayHey', 'John');
if (respString.message !== 'hey John') {
  throw new Error("expected to get 'hey John', but got a " + respString.message);
}

// an example of passing an integer
const respInt = client.invoke('testing.Service/DoubleInteger', '3');
if (respInt.message !== '6') {
  throw new Error("expected to get '6', but got a " + respInt.message);
}

另一个例子是使用 oneof。假设您有一个 proto 定义如下

proto
syntax = "proto3";

package testing;

service Service {
   rpc Test(Foo)  returns (Foo) {}
}

message Foo {
   oneof Bar {
     string code = 1;
     uint32 id = 2;
   }
}

在这种情况下,您应该传递一个包含 codeid 字段的对象。

JavaScript
import { Client } from 'k6/net/grpc';
const client = new Client();

// calling RPC with filled code field
const respWithCode = client.invoke('testing.Service/Test', { code: 'abc-123' });

// calling RPC with filled id field
const respWithID = client.invoke('testing.Service/Test', { id: 123 });