向服务注入 gRPC 故障
此示例展示了如何使用 ServiceDisruptor 测试注入到服务处理的 gRPC 请求中的故障效果。
完整的源代码位于本文档末尾。下一部分将详细介绍代码。
本示例使用 grpcpbin,这是一个简单的请求/响应应用程序,提供用于测试不同 gRPC 请求的端点。
测试要求将 grpcpbin
部署在集群中的 grpcbin
命名空间中,并通过外部 IP 暴露。该 IP 地址应位于环境变量 GRPC_HOST
中。
有关 Kubernetes manifests 以及如何在集群中部署它的说明,请参阅本文档末尾的测试设置部分。要了解如何获取外部 IP 地址,请参阅暴露您的应用程序。
初始化
初始化代码导入测试所需的外部依赖项。从 xk6-disruptor
扩展导入的 ServiceDisruptor 类提供了向服务注入故障的功能。k6/net/grpc 模块提供了执行 gRPC 请求的功能。check 函数用于验证请求结果。
import { ServiceDisruptor } from 'k6/x/disruptor';
import grpc from 'k6/net/grpc';
import { check } from 'k6';
我们还创建了一个 grpc 客户端,并加载了 HelloService 服务的 protobuf 定义。
const client = new grpc.Client();
client.load(['pb'], 'hello.proto');
测试负载
测试负载由 default
函数生成,该函数使用从环境变量 GRPC_HOST
获取的 IP 和端口连接到 grpcbin
服务,并调用 hello.HelloService
服务的 SayHello
方法。最后,检查响应的状态码。当注入故障时,此检查应失败。
export default function () {
client.connect(__ENV.GRPC_HOST, {
plaintext: true,
timeout: '1s',
});
const data = { greeting: 'Bert' };
const response = client.invoke('hello.HelloService/SayHello', data, {
timeout: '1s',
});
client.close();
check(response, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
});
}
故障注入
disrupt
函数为 grpcbin
命名空间中的 grpcbin
服务创建一个 ServiceDisruptor
。
通过调用 ServiceDisruptor.injectGrpcFaults 方法注入 gRPC 故障,使用的故障定义会在每个请求中引入 300ms
的延迟,并在 10%
的请求中引入状态码为 13
(“Internal error”)的错误。
export function disrupt() {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
const fault = {
averageDelay: '300ms',
statusCode: grpc.StatusInternal,
errorRate: 0.1,
port: 9000,
};
const disruptor = new ServiceDisruptor('grpcbin', 'grpcbin');
disruptor.injectGrpcFaults(fault, '30s');
}
请注意上面 injectFaults
函数中的以下代码片段
export function disrupt() {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
//...
}
如果测试执行时传入环境变量 SKIP_FAULTS
的值为“1”,此代码将使函数返回而不注入故障,如故障注入部分所述。另请注意参数 --env GRPC_HOST
,它传递用于访问 grpcbin
应用程序的外部 IP。
场景
此测试定义了要执行的两个场景。load
场景调用 default
函数,对 grpcpbin
应用程序施加 30s
的测试负载。disrupt
场景调用 disrupt
函数,向目标应用程序的 gRPC 请求注入故障。
export const options = {
scenarios: {
load: {
executor: 'constant-arrival-rate',
rate: 100,
preAllocatedVUs: 10,
maxVUs: 100,
exec: 'default',
startTime: '0s',
duration: '30s',
},
disrupt: {
executor: 'shared-iterations',
iterations: 1,
vus: 1,
exec: 'disrupt',
startTime: '0s',
},
},
};
注意
disrupt
场景使用shared-iterations
执行器,一次迭代和一个VU
。此设置确保disrupt
函数仅执行一次。并发多次执行此函数可能会导致不可预测的结果。
执行
注意
本节中的命令假定
xk6-disruptor
二进制文件位于您当前的目录中。此位置可能因安装过程和平台而异。有关如何在您的环境中安装它,请参阅安装。
基线执行
我们将首先在不引入故障的情况下执行测试,使用以下命令获取基线
xk6-disruptor --env SKIP_FAULTS=1 --env GRPC_HOST=$GRPC_HOST run grpc-faults.js
xk6-disruptor --env SKIP_FAULTS=1 --env "GRPC_HOST=$Env:GRPC_HOST" run grpc-faults.js
注意参数 --env SKIP_FAULT=1
,它会使 disrupt
函数返回而不注入任何故障,如故障注入部分所述。另请注意参数 --env GRPC_HOST
,它传递用于访问 grpcbin
应用程序的外部 IP。
您应该会看到类似于以下的输出(使用 Expand
按钮查看所有输出)。
故障注入
我们重复执行并注入故障。注意我们已移除 --env SKIP_FAULTS=1
参数。
xk6-disruptor --env GRPC_HOST=$GRPC_HOST run grpc-faults.js
xk6-disruptor --env "GRPC_HOST=$Env:GRPC_HOST" run grpc-faults.js
比较
让我们仔细看看每个场景中请求的结果。我们可以观察到,在 base
场景中,请求持续时间的 95 百分位值为 2.09ms
,并且 100%
的请求通过检查。而 faults
场景的 96 百分位值为 303.57ms
,只有 89%
的请求通过检查(或者换句话说,11%
的请求失败),这与故障定义密切匹配。
执行 | P95 请求持续时间 | 通过的检查数 |
---|---|---|
基线 | 2.09ms | 100% |
故障注入 | 303.57ms | 89% |
源代码
import { ServiceDisruptor } from 'k6/x/disruptor';
import grpc from 'k6/net/grpc';
import { check } from 'k6';
const client = new grpc.Client();
client.load(['pb'], 'hello.proto');
export default function () {
client.connect(__ENV.GRPC_HOST, {
plaintext: true,
timeout: '1s',
});
const data = { greeting: 'Bert' };
const response = client.invoke('hello.HelloService/SayHello', data, {
timeout: '1s',
});
client.close();
check(response, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
});
}
export function disrupt() {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
const disruptor = new ServiceDisruptor('grpcbin', 'grpcbin');
// inject errors in requests
const fault = {
averageDelay: '300ms',
statusCode: grpc.StatusInternal,
errorRate: 0.1,
port: 9000,
};
disruptor.injectGrpcFaults(fault, '30s');
}
export const options = {
scenarios: {
load: {
executor: 'constant-arrival-rate',
rate: 100,
preAllocatedVUs: 10,
maxVUs: 100,
exec: 'default',
startTime: '0s',
duration: '30s',
},
disrupt: {
executor: 'shared-iterations',
iterations: 1,
vus: 1,
exec: 'disrupt',
startTime: '0s',
},
},
};
syntax = "proto2";
package hello;
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
}
message HelloRequest {
optional string greeting = 1;
}
message HelloResponse {
required string reply = 1;
}
测试设置
测试需要部署 grpcbin
应用程序。该应用程序还必须能够通过环境变量 GRPC_HOST
中的外部 IP 访问。
以下manifests 定义了部署应用程序并将其暴露为 LoadBalancer 服务所需的资源。
您可以使用以下命令部署应用程序
# Create Namespace
kubectl apply -f namespace.yaml
namespace/grpcbin created
# Deploy Pod
kubectl apply -f pod.yaml
pod/grpcbin created
# Expose Pod as a Service
kubectl apply -f service.yaml
service/grpcbin created
您必须设置环境变量 GRPC_HOST
,其中包含从测试脚本访问 grpcbin
服务所需的外部 IP 地址和端口。
要了解如何获取外部 IP 地址,请参阅暴露您的应用程序。
Manifests
apiVersion: 'v1'
kind: Namespace
metadata:
name: grpcbin
kind: 'Pod'
apiVersion: 'v1'
metadata:
name: grpcbin
namespace: grpcbin
labels:
app: grpcbin
spec:
containers:
- name: grpcbin
image: moul/grpcbin
ports:
- name: http
containerPort: 9000
apiVersion: 'v1'
kind: 'Service'
metadata:
name: grpcbin
namespace: grpcbin
spec:
selector:
app: grpcbin
type: 'NodePort'
ports:
- port: 9000
targetPort: 9000