如何使用fortio实现性能测试
fortio介绍
Dapr 采用 fortio 作为性能测试工具。
Fortio 相关内容已经转移到单独的笔记中:Learning Fortio
tester 应用
tester 镜像的生成方法
tester 应用的镜像生成由三个镜像组成:
构建 tester go app 二进制文件的镜像
FROM golang:1.17 as build_env
ARG GOARCH_ARG=amd64
WORKDIR /app
COPY app.go go.mod ./
RUN go get -d -v && GOOS=linux GOARCH=$GOARCH_ARG go build -o tester .
这个 dockerfile 会将 dapr 仓库下 tests/apps/perf/tester
目录中的 app.go 和 go.mod 文件复制到镜像中,然后执行 go get 和 go build 命令将 go 代码打包为名为 tester 的二进制可执行文件。
最终的产出物是 /app/tester
这个二进制可执行文件。
构建 fortio 二进制文件的镜像
FROM golang:1.17 as fortio_build_env
ARG GOARCH_ARG=amd64
WORKDIR /fortio
ADD "https://api.github.com/repos/fortio/fortio/branches/master" skipcache
RUN git clone https://github.com/fortio/fortio.git
RUN cd fortio && git checkout v1.16.1 && GOOS=linux GOARCH=$GOARCH_ARG go build
这个镜像是构建 fortio v1.16.1 的代码。
最终的产出物是 /fortio/fortio/fortio
这个二进制可执行文件。
构建 buster-slim 的镜像
FROM debian:buster-slim
#RUN apt update
#RUN apt install wget -y
WORKDIR /
COPY --from=build_env /app/tester /
COPY --from=fortio_build_env /fortio/fortio/fortio /usr/local/bin
CMD ["/tester"]
这个就只是复制前两个镜像的产出物了,将 /app/tester
复制到根目录,将 fortio 复制到
/usr/local/bin
目录。
tester 应用的工作原理和实现代码
tests/apps/perf/tester/app.go
的核心代码如下:
main 函数
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/test", testHandler)
log.Fatal(http.ListenAndServe(":3001", nil))
}
main 函数启动 http server,监听 3001 端口,然后注册了两个路径和对应的 handler。
简单探活 handler
这个 handler 超级简单,什么都不做,只是返回 http 200 。
func main() {
http.HandleFunc("/", handler)
......
}
func handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
测试handler
testHandler 执行性能测试:
func main() {
http.HandleFunc("/test", testHandler)
......
}
func testHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("test execution request received")
// 步骤1: 从请求中读取测试相关的配置参数,这些参数是从 test case 中发出的
var testParams TestParameters
b, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error reading request body: %s", err)))
return
}
// 步骤2: 解析读取的测试相关的配置参数
err = json.Unmarshal(b, &testParams)
if err != nil {
w.WriteHeader(400)
w.Write([]byte(fmt.Sprintf("error parsing test params: %s", err)))
return
}
// 步骤3: 开始执行性能测试
fmt.Println("executing test")
results, err := runTest(testParams)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error encountered while running test: %s", err)))
return
}
// 步骤4: 返回性能测试的结果
fmt.Println("test finished")
w.Header().Add("Content-Type", "application/json")
w.Write(results)
}
真正的性能测试
真正的性能测试是通过 exec.Command 来执行命令行,通过调用 fortio 工具来进行的,也即是说,前面的 tester 应用除了用来启动 daprd 外,tester 自身只是配合走完性能测试的流程,真正的性能测试是由 fortio 进行。
// runTest accepts a set of test parameters, runs Fortio with the configured setting and returns
// the test results in json format.
func runTest(params TestParameters) ([]byte, error) {
var args []string
// 步骤1: 根据请求参数构建不同的 fortio 执行参数
if len(params.Payload) > 0 {
args = []string{
"load", "-json", "result.json", "-content-type", "application/json", "-qps", fmt.Sprint(params.QPS), "-c", fmt.Sprint(params.ClientConnections),
"-t", params.TestDuration, "-payload", params.Payload,
}
} else {
args = []string{
"load", "-json", "result.json", "-qps", fmt.Sprint(params.QPS), "-c", fmt.Sprint(params.ClientConnections),
"-t", params.TestDuration, "-payload-size", fmt.Sprint(params.PayloadSizeKB),
}
}
if params.StdClient {
args = append(args, "-stdclient")
}
args = append(args, params.TargetEndpoint)
fmt.Printf("running test with params: %s", args)
// 步骤2: 调用 fortio 执行性能测试
cmd := exec.Command("fortio", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return nil, err
}
// 步骤3: 返回性能测试执行结果
return os.ReadFile("result.json")
}
无负载的性能测试
对于 payload 大小为 0 的情况,执行的是如下的 fortio 命令:
fortio load -json result.json -qps ${QPS} -c ${ClientConnections} -t ${TestDuration} -payload-size ${PayloadSizeKB} ${TargetEndpoint}
疑问:payload 都为零了,为啥了还要设置 -payload-size ?
翻了一下相关的日志,找到对应的日志内容为:
fmt.Printf("running test with params: %s", args)
running test with params: [load -json result.json -qps 1 -c 1 -t 1m -payload-size 0 http://testapp:3000/test]
带负载的性能测试
对于 payload 大小不为 0 的情况,执行的是如下的 fortio 命令:
fortio load -json result.json -content-type application/json -qps ${QPS} -c ${ClientConnections} -t ${TestDuration} -payload ${Payload} ${TargetEndpoint}
全流程参数传递
在 perf test 的测试过程中,参数传递比较复杂(或者说比较绕),期间涉及到 perf test case 如何
title parameters transfer in load test
hide footbox
skinparam style strictuml
actor test_case as "Test Case"
participant tester as "Tester App"
box "Fortio" #LightBlue
participant fortio_load as "fortioLoad"
participant fgrpc
participant dapr as "daprResults"
end box
box "daprd"
participant grpc_api as "gRPC API"
end box
test_case -> tester : HTTP Post
note left: TestParameters
tester -> fortio_load : exec
note left: fortio load ... -grpc -dapr k1=v1,k2=v2
fortio_load -> fgrpc : RunGRPCTest()
note left: -grpc -dapr k1=v1,k2=v2
fgrpc -> dapr : RunTest()
note left: -dapr k1=v1,k2=v2
dapr -> grpc_api
dapr <-- grpc_api
fgrpc <-- dapr
fortio_load <-- fgrpc
tester <-- fortio_load
test_case <-- tester
步骤1:perf test case 获取测试参数
perf test case 在启动时,会从环境变量中获取测试参数
- DAPR_PERF_QPS: 默认1
- DAPR_PERF_CONNECTIONS: 默认1
- DAPR_TEST_DURATION: 默认 “1m”,即1分钟
- DAPR_PAYLOAD_SIZE: 默认0
- DAPR_PAYLOAD: 默认为空(字符串"")
步骤2:perf test case打包测试参数发送给tester app
perf test case 会发送一个 HTTP 请求到 tester app,其内容如下:
-
URL: http://testerAppURL/test
-
Method: POST
-
Body: 将 TestParameters 结构体系列化为 json
TestParameters 结构体的字段如下所示:
type TestParameters struct {
QPS int `json:"qps"`
ClientConnections int `json:"clientConnections"`
TargetEndpoint string `json:"targetEndpoint"`
TestDuration string `json:"testDuration"`
PayloadSizeKB int `json:"payloadSizeKB"`
Payload string `json:"payload"`
StdClient bool `json:"stdClient"`
}
在支持 gRPC + dapr 时,由于没有合适的参数可以使用,因此增加了 grpc 和 dapr 两个字段:
type TestParameters struct {
......
Grpc bool `json:"grpc"`
Dapr string `json:"dapr"`
}
步骤3:tester app解析测试参数,传递给fortio
在 tester app (tests/apps/perf/tester/app.go
) 的 testHandler() 方法中,会对传入http body进行读取和json解析,然后将 TestParameters 转为 fortio 的参数:
func testHandler(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
err = json.Unmarshal(b, &testParams)
results, err := runTest(testParams)
......
}
func runTest(params TestParameters) ([]byte, error) {
args := buildFortioArgs(params)
......
}
TestParameters 字段和 fortio 参数的对应关系:
语义 | 环境变量 | TestParameters 字段 | fortio 参数 |
---|---|---|---|
load | |||
-json result.json |
|||
-content-type application/json |
|||
QPS | DAPR_PERF_QPS | QPS | -qps |
客户端连接数 | DAPR_PERF_CONNECTIONS | ClientConnections | -c |
测试时长 | DAPR_TEST_DURATION | TestDuration | -t |
负载 | DAPR_PAYLOAD | Payload | -payload |
负载大小 | DAPR_PAYLOAD_SIZE | PayloadSizeKB | -payload-size |
StdClient | -stdclient |
||
是否是grpc测试 | Grpc | -grpc |
|
Dapr测试参数 | Dapr | -dapr |
步骤4:fortio解析 dapr flag,在发起的dapr 请求中使用
在 fortio 的执行中, load
子命令会有 fortioLoad
方法负责,检查发现有 -grpc
flag,则会转到 fgrpc.RunGRPCTest()
方法。
fortio 的 grpc 支持默认只有自带的 ping 和标准的 health,为了支持 dapr ,我们扩展了 fgrpc.RunGRPCTest()
方法。
-dapr
flag 传递的参数会被透传到 dapr 的扩展代码中, 然后解析为下面的结构:
type DaprRequestParameters struct {
capability string
target string
method string
appId string
store string
extensions map[string]string
}
这些参数将在后面 fortio 扩展代码中进行 dapr 调用时被使用到。