性能测试
> 文档中心 > INFINI Gateway > 入门指南 > 性能测试

性能测试 #

推荐使用 Elasticsearch 专属压测工具 Loadgen 来对网关进行性能压测。

Loadgen 的特点:

  • 性能强劲
  • 轻量级无依赖
  • 支持模板化参数随机
  • 支持高并发
  • 支持压测端均衡流量控制
  • 支持服务端返回值校验

下载地址: http://release.infinilabs.com/loadgen/

Loadgen #

Loadgen 使用非常简单,下载解压之后会得到两个文件,一个可执行程序和一个配置文件 loadgen.yml,配置文件样例如下:

env:
  ES_USERNAME: elastic
  ES_PASSWORD: elastic
runner:
  # total_rounds: 1
  no_warm: false
  log_requests: false
  assert_invalid: false
  assert_error: false
variables:
  - name: ip
    type: file
    path: test/ip.txt
  - name: user
    type: file
    path: test/user.txt
  - name: id
    type: sequence
  - name: uuid
    type: uuid
  - name: now_local
    type: now_local
  - name: now_utc
    type: now_utc
  - name: now_unix
    type: now_unix
requests:
  - request:
      method: GET
      basic_auth:
        username: $[[env.ES_USERNAME]]
        password: $[[env.ES_PASSWORD]]
      url: http://localhost:8000/medcl/_search
      body: '{  "query": {"match": {    "name": "$[[user]]"  }}}'

运行模式设置 #

默认配置下,loadgen会以性能测试模式运行,在指定时间(-d)内重复执行requests里的所有请求。如果只需要检查一次测试结果,可以通过runner.total_rounds来设置requests的执行次数。

HTTP 响应头处理 #

默认配置下,loadgen会自动格式化 HTTP 的响应头(user-agent: xxx -> User-Agent: xxx),如果需要精确判断服务器返回的响应头,可以通过runner.disable_header_names_normalizing来禁用这个行为。

变量的使用 #

上面的配置中,variables 用来定义变量参数,根据 name 来设置变量标识,在构造请求的使用 $[[变量名]] 即可访问该变量的值,变量目前支持的类型有:

类型说明变量参数
file文件型外部变量参数path: 数据文件路径
data: 数据列表,会被附加到path文件内容后读取
list自定义枚举变量参数data: 字符数组类型的枚举数据列表
sequence32 位自增数字类型的变量from: 初始值
to: 最大值
sequence6464 位自增数字类型的变量from: 初始值
to: 最大值
range数字范围类型的变量,支持参数 fromto 来限制范围from: 初始值
to: 最大值
random_array生成一个随机数组,数据元素来自variable_key指定的变量variable_key: 数据源变量
size: 输出数组的长度
square_bracket: true/false,输出值是否需要[]
string_bracket: 字符串,输出元素前后会附加指定的字符串
uuidUUID 字符类型的变量
now_local当前时间、本地时区
now_utc当前时间、UTC 时区。输出格式:2006-01-02 15:04:05.999999999 -0700 MST
now_utc_lite当前时间、UTC 时区。输出格式:2006-01-02T15:04:05.000
now_unix当前时间、Unix 时间戳
now_with_format当前时间,支持自定义 format 参数来格式化时间字符串,如:2006-01-02T15:04:05-0700format: 输出的时间格式 ( 示例)

变量使用示例 #

file 类型变量参数加载自外部文本文件,每行一个变量参数,访问该变量时每次随机取其中一个,变量里面的定义格式举例如下:

➜  loadgen git:(master) ✗ cat test/user.txt
medcl
elastic

附生成固定长度的随机字符串,如 1024 个字符每行:

LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt

环境变量 #

loadgen支持自动读取环境变量,环境变量可以在运行loadgen时通过命令行传入,也可以在loadgen.yml里指定默认的环境变量值,loadgen运行时会使用命令行传入的环境变量覆盖loadgen.yml里的默认值。

配置的环境变量可以通过$[[env.环境变量]]来使用:

# 配置环境变量默认值
env:
  ES_USERNAME: elastic
  ES_PASSWORD: elastic
  ES_ENDPOINT: http://localhost:8000
requests:
  - request:
      method: GET
      basic_auth:
        username: $[[env.ES_USERNAME]] # 使用运行时变量
        password: $[[env.ES_PASSWORD]] # 使用运行时变量
      url: $[[env.ES_ENDPOINT]]/medcl/_search # 使用运行时变量
      body: '{  "query": {"match": {    "name": "$[[user]]"  }}}'

请求的定义 #

配置节点 requests 用来设置 Loadgen 将依次执行的请求,支持固定参数的请求,也可支持模板变量参数化构造请求,以下是一个普通的查询请求:

requests:
  - request:
      method: GET
      basic_auth:
        username: elastic
        password: pass
      url: http://localhost:8000/medcl/_search?q=name:$[[user]]

上面的查询对 medcl 索引进行了查询,并对 name 字段执行一个查询,每次请求的值来自随机变量 user

模拟批量写入 #

使用 Loadgen 来模拟 bulk 批量写入也非常简单,在请求体里面配置一条索引操作,然后使用 body_repeat_times 参数来随机参数化复制若干条请求即可完成一批请求的准备,如下:

  - request:
      method: POST
      basic_auth:
        username: test
        password: testtest
      url: http://localhost:8000/_bulk
      body_repeat_times: 1000
      body: |
        { "index" : { "_index" : "medcl-y4","_type":"doc", "_id" : "$[[uuid]]" } }
        { "id" : "$[[id]]","field1" : "$[[user]]","ip" : "$[[ip]]","now_local" : "$[[now_local]]","now_unix" : "$[[now_unix]]" }

返回值判断 #

每个requests配置可以通过assert来设置是否需要检查返回值。assert功能支持 INFINI Gateway 的大部分 条件判断功能

requests:
  - request:
      method: GET
      basic_auth:
        username: elastic
        password: pass
      url: http://localhost:8000/medcl/_search?q=name:$[[user]]
    assert:
      equals:
        _ctx.response.status: 201

请求返回值可以通过_ctx获取,_ctx目前包含以下信息:

参数说明
_ctx.response.statusHTTP 返回状态码
_ctx.response.headerHTTP 返回响应头
_ctx.response.bodyHTTP 返回响应体
_ctx.response.body_json如果 HTTP 返回响应体是一个有效的 JSON 字符串,可以通过body_json来访问 JSON 内容字段
_ctx.elapsed当前请求发送到返回消耗的时间(毫秒)

如果请求失败(请求地址无法访问等),loadgen无法获取 HTTP 请求返回值,loadgen会在输出日志里记录Number of Errors。如果配置了runner.assert_error且存在请求失败的请求,loadgen会返回exit(2)错误码。

如果返回值不符合判断条件,loadgen会停止执行当前轮次后续请求,并在输出日志里记录Number of Invalid。如果配置了runner.assert_invalid且存在判断失败的请求,loadgen会返回exit(1)错误码。

动态变量注册 #

每个requests配置可以通过register来动态添加运行时参数,一个常见的使用场景是根据前序请求的返回值来动态设置后序请求的参数。

这个示例调用$[[env.ES_ENDPOINT]]/test接口获取索引的 uuid,并注册到index_id变量。后续的请求定义可以通过$[[index_id]]来获取这个值。

requests:
  - request:
      method: GET
      url: $[[env.ES_ENDPOINT]]/test
    assert:
      equals:
        _ctx.response.status: 200
    register:
      - index_id: _ctx.response.body_json.test.settings.index.uuid

执行压测 #

执行 Loadgen 程序即可执行压测,如下:

➜  loadgen git:(master) ✗ ./bin/loadgen -d 30 -c 100 -compress
   __   ___  _      ___  ___   __    __
  / /  /___\/_\    /   \/ _ \ /__\/\ \ \
 / /  //  ///_\\  / /\ / /_\//_\ /  \/ /
/ /__/ \_//  _  \/ /_// /_\\//__/ /\  /
\____|___/\_/ \_/___,'\____/\__/\_\ \/

[LOADGEN] A http load generator and testing suit.
[LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files
[07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0
[07-19 16:15:00] [INF] [loader.go:312] warmup started
[07-19 16:15:00] [INF] [app.go:306] loadgen now started.
[07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search
[07-19 16:15:00] [INF] [loader.go:317] status: 200,<nil>,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
[07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl
[07-19 16:15:00] [INF] [loader.go:317] status: 200,<nil>,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
[07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk
[07-19 16:15:01] [INF] [loader.go:317] status: 200,<nil>,{"took":120,"errors":false,"items":[{"index":{"_index":"medcl-y4","_type":"doc","_id":"c3qj9123r0okahraiej0","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":5735852,"_primary_term":3,"status":201}}]}
[07-19 16:15:01] [INF] [loader.go:325] warmup finished

5253 requests in 32.756483336s, 524.61KB sent, 2.49MB received

[Loadgen Client Metrics]
Requests/sec:		175.10
Request Traffic/sec:	17.49KB
Total Transfer/sec:	102.34KB
Avg Req Time:		5.711022ms
Fastest Request:	440.448µs
Slowest Request:	3.624302658s
Number of Errors:	0
Number of Invalid:	0
Status 200:		5253

[Estimated Server Metrics]
Requests/sec:		160.37
Transfer/sec:		93.73KB
Avg Req Time:		623.576686ms

Loadgen 在正式压测之前会将所有的请求执行一次来进行预热,如果出现错误会提示是否继续,预热的请求结果也会输出到终端,执行完成之后会输出执行的摘要信息。可以通过设置runner.no_warm来跳过这个检查阶段。

因为 Loadgen 最后的结果是所有请求全部执行完成之后的累计统计,可能存在不准的问题,建议通过打开 Kibana 的监控仪表板来实时查看 Elasticsearch 的各项运行指标。

命令行参数 #

Loadgen 会循环执行配置文件里面定义的请求,默认 Loadgen 只会运行 5s 就自动退出了,如果希望延长运行时间或者加大并发可以通过启动的时候设置参数来控制,通过查看帮助命令如下:

➜  loadgen git:(master) ✗ ./bin/loadgen --help
Usage of ./bin/loadgen:
  -c int
    	Number of concurrent threads (default 1)
  -compress
    	Compress requests with gzip
  -config string
    	the location of config file, default: loadgen.yml (default "loadgen.yml")
  -d int
    	Duration of tests in seconds (default 5)
  -debug
    	run in debug mode, loadgen will quit with panic error
  -l int
    	Limit total requests (default -1)
  -log string
    	the log level,options:trace,debug,info,warn,error (default "info")
  -r int
    	Max requests per second (fixed QPS) (default -1)
  -v	version

限制客户端压力 #

使用 Loadgen 并设置命令行参数 -r 可以限制客户端发送的每秒请求数,从而评估固定压力下 Elasticsearch 的响应时间和负载情况,如下:

➜  loadgen git:(master) ✗ ./bin/loadgen -d 30 -c 100 -r 100

注意,在大量并发下,此客户端吞吐限制可能不完全准确。

限制请求的总条数 #

通过设置参数 -l 可以控制客户端发送的请求总数,从而制造固定的文档,修改配置如下:

requests:
  - request:
      method: POST
      basic_auth:
        username: test
        password: testtest
      url: http://localhost:8000/medcl-test/doc2/_bulk
      body_repeat_times: 1
      body: |
        { "index" : { "_index" : "medcl-test", "_id" : "$[[uuid]]" } }
        { "id" : "$[[id]]","field1" : "$[[user]]","ip" : "$[[ip]]" }

每次请求只有一个文档,然后执行 loadgen

./bin/loadgen -config loadgen-gw.yml -d 600 -c 100 -l 50000

执行完成之后,Elasticsearch 的索引 medcl-test 将增加 50000 条记录。

使用自增 ID 来确保文档的顺序性 #

如果希望生成的文档编号自增有规律,方便进行对比,可以使用 sequence 类型的自增 ID 来作为主键,内容也不要用随机数,如下:

requests:
  - request:
      method: POST
      basic_auth:
        username: test
        password: testtest
      url: http://localhost:8000/medcl-test/doc2/_bulk
      body_repeat_times: 1
      body: |
        { "index" : { "_index" : "medcl-test", "_id" : "$[[id]]" } }
        { "id" : "$[[id]]" }

上下文复用变量 #

在一个请求中,我们可能希望有相同的参数出现,比如 routing 参数用来控制分片的路由,同时我们又希望该参数也保存在文档的 JSON 里面, 可以使用 runtime_variables 来设置请求级别的变量,或者 runtime_body_line_variables 定义请求体级别的变量,如果请求体复制 N 份,每份的参数是不同的,举例如下:

variables:
  - name: id
    type: sequence
  - name: uuid
    type: uuid
  - name: now_local
    type: now_local
  - name: now_utc
    type: now_utc
  - name: now_unix
    type: now_unix
  - name: suffix
    type: range
    from: 10
    to: 15
requests:
  - request:
      method: POST
      runtime_variables:
        batch_no: id
      runtime_body_line_variables:
        routing_no: uuid
      basic_auth:
        username: ingest
        password: password
      #url: http://localhost:8000/_search?q=$[[id]]
      url: http://192.168.3.188:9206/_bulk
      body_repeat_times: 10
      body: |
        { "create" : { "_index" : "test-$[[suffix]]","_type":"doc", "_id" : "$[[uuid]]" , "routing" : "$[[routing_no]]" } }
        { "id" : "$[[uuid]]","routing_no" : "$[[routing_no]]","batch_number" : "$[[batch_no]]", "random_no" : "$[[suffix]]","ip" : "$[[ip]]","now_local" : "$[[now_local]]","now_unix" : "$[[now_unix]]" }

我们定义了 batch_no 变量来代表一批文档里面的相同批次号,同时又定义了 routing_no 变量来代表每个文档级别的 routing 值。

自定义 Header #

requests:
  - request:
      method: GET
      url: http://localhost:8000/test/_search
      headers:
        - Agent: "Loadgen-1"
      disable_header_names_normalizing: false

默认配置下,loadgen会自动格式化配置里的 HTTP 的请求头(user-agent: xxx -> User-Agent: xxx),如果需要精确设置 HTTP 请求头,可以通过设置disable_header_names_normalizing: true来禁用这个行为。

运行测试套件 #

loadrun 是一个用来批量运行loadgen测试用例的工具,不需要重复编写测试用例,通过切换套件配置来快速测试不同的环境配置。

下载地址: http://release.infinilabs.com/loadrun/

下载解压之后会得到两个文件,一个可执行程序loadrun和一个配置文件 loadrun.yml,配置文件样例如下:

env:
  LR_LOADGEN_CMD: ./bin/loadgen # The path to the executable of loadgen tool.
  LR_TEST_DIR: ./testing # The path to the test cases.
  # If you want to start gateway dynamically and automatically:
  LR_GATEWAY_CMD: ./bin/gateway # The path to the executable of INFINI Gateway
  LR_GATEWAY_HOST: 0.0.0.0:18000 # The binding host of the INFINI Gateway
  LR_GATEWAY_API_HOST: 0.0.0.0:19000 # The binding host of the INFINI Gateway API server
  # Set up other envrionments for the gateway and loadgen
  LR_ELASTICSEARCH_ENDPOINT: http://localhost:19201
  CUSTOM_ENV: myenv
tests:
  # The relative path of test cases under `LR_TEST_DIR`
  #
  # - gateway.yml: (Optional) the configuration to start the INFINI Gateway dynamically.
  # - loadgen.yml: the configuration to run the loadgen tool.
  #
  # The environments set in `env` section will be passed to the INFINI Gateway and loadgen.
  - path: cases/gateway/echo/echo_with_context

环境变量配置 #

loadrun通过环境变量来动态配置 INFINI Gateway 和loadgen,环境变量在env里指定。以下环境变量是必选的:

变量名说明
LR_TEST_DIR测试用例所在目录
LR_LOADGEN_CMDloadgen工具可执行文件的路径

如果你需要loadrun根据配置动态启动 INFINI Gateway,需要设置以下环境变量:

变量名说明
LR_GATEWAY_CMDINFINI Gateway 可执行文件的路径
LR_GATEWAY_HOSTINFINI Gateway 绑定的主机名:端口
LR_GATEWAY_API_HOSTINFINI Gateway API 绑定的主机名:端口

测试用例配置 #

测试用例在tests里配置,每个路径(path)指向一个测试用例的目录,每个测试用例需要配置一份gateway.yml(可选)和loadgen.yml。配置文件可以使用env下配置的环境变量($[[env.ENV_KEY]])。

gateway.yml参考配置:

path.data: data
path.logs: log

entry:
  - name: my_es_entry
    enabled: true
    router: my_router
    max_concurrency: 200000
    network:
      binding: $[[env.LR_GATEWAY_HOST]]

flow:
  - name: hello_world
    filter:
      - echo:
          message: 'hello world'
router:
  - name: my_router
    default_flow: hello_world

loadgen.yml参考配置:

runner:
  total_rounds: 1
  no_warm: true
  log_requests: true
  assert_invalid: true
  assert_error: true
requests:
  - request:
      method: GET
      url: http://$[[env.LR_GATEWAY_HOST]]/
    assert:
      and:
        - equals:
            _ctx.response.status: 200
        - equals:
            _ctx.response.body: 'hello world'

测试套件运行 #

配置好测试loadrun.yml后,可以通过以下命令运行loadrun

loadrun -config ./loadrun.yml

loadrun会运行配置指定的所有测试用例,并输出测试结果:

   __   ___  _      ___  ___   __    __
  / /  /___\/_\    /   \/ _ \ /__\/\ \ \
 / /  //  ///_\\  / /\ / /_\//_\ /  \/ /
/ /__/ \_//  _  \/ /_// /_\\//__/ /\  /
\____|___/\_/ \_/___,'\____/\__/\_\ \/

[LOAD-RUNNER] A testing suite runner
[LOAD-RUNNER] 1.0.0_SNAPSHOT#001, 2023-02-21 02:49:57, 2023-12-31 10:10:10, b427e3657a1336b4839a7eff59f79f8e334f3934
[02-21 10:50:05] [INF] [app.go:192] initializing loadrun
[02-21 10:50:05] [INF] [app.go:193] using config: /Users/kassian/Workspace/infini/src/infini.sh/testing/suites/dev.yml
[02-21 10:50:05] [INF] [instance.go:78] workspace: /Users/kassian/Workspace/infini/src/infini.sh/testing/data/loadrun/nodes/cfpihf15k34iqhpd4d00
[02-21 10:50:05] [INF] [app.go:399] loadrun is up and running now.
[2023-02-21 10:50:05][TEST][SUCCESS] [setup/loadgen/cases/dummy] duration: 105(ms)

1 requests in 68.373875ms, 0.00bytes sent, 0.00bytes received

[Loadgen Client Metrics]
Requests/sec:		0.20
Request Traffic/sec:	0.00bytes
Total Transfer/sec:	0.00bytes
Avg Req Time:		5s
Fastest Request:	68.373875ms
Slowest Request:	68.373875ms
Number of Errors:	0
Number of Invalid:	0
Status 200:		1

[Estimated Server Metrics]
Requests/sec:		14.63
Transfer/sec:		0.00bytes
Avg Req Time:		68.373875ms


[2023-02-21 10:50:06][TEST][FAILED] [setup/gateway/cases/echo/echo_with_context/] duration: 1274(ms)
#0 request, GET http://$[[env.LR_GATEWAY_HOST]]/any/, assertion failed, skiping subsequent requests
1 requests in 1.255678s, 0.00bytes sent, 0.00bytes received

[Loadgen Client Metrics]
Requests/sec:		0.20
Request Traffic/sec:	0.00bytes
Total Transfer/sec:	0.00bytes
Avg Req Time:		5s
Fastest Request:	1.255678s
Slowest Request:	1.255678s
Number of Errors:	1
Number of Invalid:	1
Status 0:		1

[Estimated Server Metrics]
Requests/sec:		0.80
Transfer/sec:		0.00bytes
Avg Req Time:		1.255678s