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 从这些文件及其依赖项加载所有定义。
import { Client } from 'k6/net/grpc';
const client = new Client();
client.load(['definitions'], 'hello.proto');
或者,您可以使用 gRPC 反射协议动态加载定义。要启用反射,您可以将 reflect: true
选项传递给 Client.connect()
。然后 k6 从服务器及其依赖项加载所有定义。
仅当服务器已实现反射支持时,此选项才可用。
import { Client } from 'k6/net/grpc';
const client = new Client();
client.connect('127.0.0.1:10000', { reflect: true });
一元 gRPC 请求
一元调用与常规 HTTP 请求的工作方式相同。向服务器发送一个请求,服务器回复一个响应。
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 流
在服务器流模式下,客户端向服务器发送一个请求,服务器回复多个响应。
以下示例演示了服务器流。
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 流
客户端流模式与服务器流模式相反。客户端向服务器发送多个请求,服务器回复一个响应。
以下示例演示了客户端流。
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
事件处理程序。
处理程序接收 一个错误对象。
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 定义如下
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);
}
传递消息时,您应该使用字符串或整数,而不是对象。因此,您将收到一个已经封送的类型。
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 定义如下
syntax = "proto3";
package testing;
service Service {
rpc Test(Foo) returns (Foo) {}
}
message Foo {
oneof Bar {
string code = 1;
uint32 id = 2;
}
}
在这种情况下,您应该传递一个包含 code
或 id
字段的对象。
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 });