,{"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}}]}
283 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished
284 |
285 | 209 requests finished in 10.031365126s, 0.00bytes sent, 32.86KB received
286 |
287 | [Loadgen Client Metrics]
288 | Requests/sec: 20.82
289 | Request Traffic/sec: 0.00bytes
290 | Total Transfer/sec: 3.27KB
291 | Fastest Request: 1ms
292 | Slowest Request: 182.437792ms
293 | Status 302: 209
294 |
295 | [Latency Metrics]
296 | 209 samples of 209 events
297 | Cumulative: 10.031365126s
298 | HMean: 46.31664ms
299 | Avg.: 47.996962ms
300 | p50: 45.712292ms
301 | p75: 51.6065ms
302 | p95: 53.05475ms
303 | p99: 118.162416ms
304 | p999: 182.437792ms
305 | Long 5%: 87.678145ms
306 | Short 5%: 39.11217ms
307 | Max: 182.437792ms
308 | Min: 38.257791ms
309 | Range: 144.180001ms
310 | StdDev: 14.407579ms
311 | Rate/sec.: 20.82
312 |
313 | [Latency Distribution]
314 | 38.257ms - 52.675ms ------------------------------
315 | 52.675ms - 67.093ms --
316 | 67.093ms - 81.511ms -
317 | 81.511ms - 95.929ms -
318 | 95.929ms - 110.347ms -
319 | 110.347ms - 124.765ms -
320 |
321 |
322 | [Estimated Server Metrics]
323 | Requests/sec: 20.83
324 | Avg Req Time: 47.996962ms
325 | Transfer/sec: 3.28KB
326 | ```
327 |
328 | Loadgen executes all requests once to warm up before the formal benchmark test. If an error occurs, a prompt is displayed, asking you whether to continue.
329 | The warm-up request results are also output to the terminal. After execution, an execution summary is output.
330 | You can set `runner.no_warm: true` to skip the warm-up stage.
331 |
332 | > The final results of Loadgen are the cumulative statistics after all requests are executed, and they may be inaccurate. You are advised to start the Kibana dashboard to check all operating indicators of Elasticsearch in real time.
333 |
334 | ### CLI Parameters
335 |
336 | Loadgen cyclically executes requests defined in the configuration file. By default, Loadgen runs for `5s` and then automatically exits. If you want to prolong the running time or increase the concurrency, you can set the tool's startup parameters. The help commands are as follows:
337 |
338 | ```
339 | ➜ loadgen git:(master) ✗ ./bin/loadgen --help
340 | Usage of ./bin/loadgen:
341 | -c int
342 | Number of concurrent threads (default 1)
343 | -compress
344 | Compress requests with gzip
345 | -config string
346 | the location of config file, default: loadgen.yml (default "loadgen.yml")
347 | -d int
348 | Duration of tests in seconds (default 5)
349 | -debug
350 | run in debug mode, loadgen will quit with panic error
351 | -l int
352 | Limit total requests (default -1)
353 | -log string
354 | the log level,options:trace,debug,info,warn,error (default "info")
355 | -r int
356 | Max requests per second (fixed QPS) (default -1)
357 | -v version
358 | ```
359 |
360 | ### Limiting the Client Workload
361 |
362 | You can use Loadgen and set the CLI parameter `-r` to restrict the number of requests that can be sent by the client per second, so as to evaluate the response time and load of Elasticsearch under fixed pressure. See the following example.
363 |
364 | ```
365 | ➜ loadgen git:(master) ✗ ./bin/loadgen -d 30 -c 100 -r 100
366 | ```
367 |
368 | > Note: The client throughput limit may not be accurate enough in the case of massive concurrencies.
369 |
370 | ### Limiting the Total Number of Requests
371 |
372 | You can set the `-l` parameter to control the total number of requests that can be sent by the client, so as to generate a fixed number of documents. Modify the configuration as follows:
373 |
374 | ```
375 | requests:
376 | - request:
377 | method: POST
378 | basic_auth:
379 | username: test
380 | password: testtest
381 | url: http://localhost:8000/medcl-test/doc2/_bulk
382 | body_repeat_times: 1
383 | body: |
384 | { "index" : { "_index" : "medcl-test", "_id" : "$[[uuid]]" } }
385 | { "id" : "$[[id]]","field1" : "$[[user]]","ip" : "$[[ip]]" }
386 | ```
387 |
388 | Configured parameters use the content of only one document for each request. Then, the system executes Loadgen.
389 |
390 | ```
391 | ./bin/loadgen -config loadgen-gw.yml -d 600 -c 100 -l 50000
392 | ```
393 |
394 | After execution, `50000` records are added for the Elasticsearch index `medcl-test`.
395 |
396 | ### Using Auto Incremental IDs to Ensure the Document Sequence
397 |
398 | If the IDs of generated documents need to increase regularly to facilitate comparison, you can use the auto incremental IDs of the `sequence` type as the primary key and avoid using random numbers in the content. See the following example.
399 |
400 | ```
401 | requests:
402 | - request:
403 | method: POST
404 | basic_auth:
405 | username: test
406 | password: testtest
407 | url: http://localhost:8000/medcl-test/doc2/_bulk
408 | body_repeat_times: 1
409 | body: |
410 | { "index" : { "_index" : "medcl-test", "_id" : "$[[id]]" } }
411 | { "id" : "$[[id]]" }
412 | ```
413 |
414 | ### Reuse variables in Request Context
415 |
416 | In a request, we might want use the same variable value, such as the `routing` parameter to control the shard destination, also store the field in the JSON document.
417 | You can use `runtime_variables` to set request-level variables, or `runtime_body_line_variables` to define request-body-level variables.
418 | If the request body set `body_repeat_times`, each line will be different, as shown in the following example:
419 |
420 | ```
421 | variables:
422 | - name: id
423 | type: sequence
424 | - name: uuid
425 | type: uuid
426 | - name: now_local
427 | type: now_local
428 | - name: now_utc
429 | type: now_utc
430 | - name: now_unix
431 | type: now_unix
432 | - name: suffix
433 | type: range
434 | from: 10
435 | to: 15
436 | requests:
437 | - request:
438 | method: POST
439 | runtime_variables:
440 | batch_no: id
441 | runtime_body_line_variables:
442 | routing_no: uuid
443 | basic_auth:
444 | username: ingest
445 | password: password
446 | #url: http://localhost:8000/_search?q=$[[id]]
447 | url: http://192.168.3.188:9206/_bulk
448 | body_repeat_times: 10
449 | body: |
450 | { "create" : { "_index" : "test-$[[suffix]]","_type":"doc", "_id" : "$[[uuid]]" , "routing" : "$[[routing_no]]" } }
451 | { "id" : "$[[uuid]]","routing_no" : "$[[routing_no]]","batch_number" : "$[[batch_no]]", "random_no" : "$[[suffix]]","ip" : "$[[ip]]","now_local" : "$[[now_local]]","now_unix" : "$[[now_unix]]" }
452 | ```
453 |
454 | We defined the `batch_no` variable to represent the same batch number in a batch of documents, and the `routing_no` variable to represent the routing value at each document level.
455 |
456 | ### Customize Header
457 |
458 | ```
459 | requests:
460 | - request:
461 | method: GET
462 | url: http://localhost:8000/test/_search
463 | headers:
464 | - Agent: "Loadgen-1"
465 | disable_header_names_normalizing: false
466 | ```
467 |
468 | By default, `loadgen` will canonilize the HTTP header keys before sending the request (`user-agent: xxx` -> `User-Agent: xxx`), if you need to set the header keys exactly as is, set `disable_header_names_normalizing: true`.
469 |
470 | ### Work with DSL
471 |
472 | Loadgen also support simply the requests called DSL,for example, prepare a dsl file for loadgen, save as `bulk.dsl`:
473 |
474 | ```
475 | POST /_bulk
476 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}}
477 | {"id": "$[[id]]", "routing": "$[[routing_no]]", "batch": "$[[batch_no]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
478 | ```
479 | And specify the dsl file with parameter `run`:
480 |
481 | ```
482 | $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run bulk.dsl
483 | ```
484 |
485 | Now you should ready to rock~
--------------------------------------------------------------------------------
/api-testing-example.dsl:
--------------------------------------------------------------------------------
1 | # // How to use this example?
2 | # // $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run api-testing-example.dsl
3 |
4 | # runner: {
5 | # total_rounds: 1,
6 | # no_warm: true,
7 | # assert_invalid: true,
8 | # continue_on_assert_invalid: true,
9 | # }
10 |
11 | DELETE /$[[env.INDEX_NAME]]
12 |
13 | PUT /$[[env.INDEX_NAME]]
14 | # 200
15 | # {"acknowledged":true,"shards_acknowledged":true,"index":"medcl123"}
16 |
17 | POST /_bulk
18 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}}
19 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
20 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}}
21 | {"id": "$[[id]]", "field1": "$[[list]]", "some_other_fields": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
22 | # 200
23 | # {"errors":false,}
24 |
25 | GET /$[[env.INDEX_NAME]]/_refresh
26 | # 200
27 | # {"_shards":{"total":2,"successful":1,"failed":0}}
28 |
29 | GET /$[[env.INDEX_NAME]]/_count
30 | # 200
31 | # {"count":2}
32 |
33 | GET /$[[env.INDEX_NAME]]/_search
34 | # 200
35 |
--------------------------------------------------------------------------------
/bulk-testing-example.dsl:
--------------------------------------------------------------------------------
1 | # // How to use this example?
2 | # // $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run bulk.dsl
3 |
4 | # runner: {
5 | # total_rounds: 100000,
6 | # no_warm: true,
7 | # assert_invalid: true,
8 | # continue_on_assert_invalid: true,
9 | # }
10 |
11 |
12 | POST /_bulk
13 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}}
14 | {"id": "$[[id]]", "routing": "$[[routing_no]]", "batch": "$[[batch_no]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
15 | # request: {
16 | # runtime_variables: {batch_no: "uuid"},
17 | # runtime_body_line_variables: {routing_no: "uuid"},
18 | # body_repeat_times: 1000,
19 | # basic_auth: {
20 | # username: "$[[env.ES_USERNAME]]",
21 | # password: "$[[env.ES_PASSWORD]]",
22 | # },
23 | # }
--------------------------------------------------------------------------------
/config/generated.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | const LastCommitLog = "N/A"
4 |
5 | const BuildDate = "N/A"
6 |
7 | const EOLDate = "N/A"
8 |
9 | const Version = "0.0.1-SNAPSHOT"
10 |
11 | const BuildNumber = "001"
12 |
--------------------------------------------------------------------------------
/dict/ip.txt:
--------------------------------------------------------------------------------
1 | 192.168.0.1
2 | 192.168.0.2
3 | 192.168.0.3
4 | 192.168.0.4
5 |
--------------------------------------------------------------------------------
/dict/type.txt:
--------------------------------------------------------------------------------
1 | doc
2 | _doc
3 |
--------------------------------------------------------------------------------
/dict/user.txt:
--------------------------------------------------------------------------------
1 | medcl
2 | elastic
3 | "google" abc
4 | \"abcd"
5 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /public/
2 | /resources/
3 | /themes/
4 | /config.bak
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | SHELL=/bin/bash
2 |
3 | # Basic info
4 | PRODUCT?= $(shell basename "$(shell cd .. && pwd)")
5 | BRANCH?= main
6 | VERSION?= $(shell [[ "$(BRANCH)" == "main" ]] && echo "main" || echo "$(BRANCH)")
7 | CURRENT_VERSION?= $(VERSION)
8 | VERSIONS?= "main"
9 | OUTPUT?= "/tmp/docs"
10 | THEME_FOLDER?= "themes/book"
11 | THEME_REPO?= "https://github.com/infinilabs/docs-theme.git"
12 | THEME_BRANCH?= "main"
13 |
14 | .PHONY: docs-build
15 |
16 | default: docs-build
17 |
18 | docs-init:
19 | @if [ ! -d $(THEME_FOLDER) ]; then echo "theme does not exist";(git clone -b $(THEME_BRANCH) $(THEME_REPO) $(THEME_FOLDER) ) fi
20 |
21 | docs-env:
22 | @echo "Debugging Variables:"
23 | @echo "PRODUCT: $(PRODUCT)"
24 | @echo "BRANCH: $(BRANCH)"
25 | @echo "VERSION: $(VERSION)"
26 | @echo "CURRENT_VERSION: $(CURRENT_VERSION)"
27 | @echo "VERSIONS: $(VERSIONS)"
28 | @echo "OUTPUT: $(OUTPUT)"
29 |
30 | docs-config: docs-init
31 | cp config.yaml config.bak
32 | # Detect OS and apply the appropriate sed command
33 | @if [ "$$(uname)" = "Darwin" ]; then \
34 | echo "Running on macOS"; \
35 | sed -i '' "s/BRANCH/$(VERSION)/g" config.yaml; \
36 | else \
37 | echo "Running on Linux"; \
38 | sed -i 's/BRANCH/$(VERSION)/g' config.yaml; \
39 | fi
40 |
41 | docs-build: docs-config
42 | hugo --minify --theme book --destination="$(OUTPUT)/$(PRODUCT)/$(VERSION)" \
43 | --baseURL="/$(PRODUCT)/$(VERSION)"
44 | @$(MAKE) docs-restore-generated-file
45 |
46 | docs-serve: docs-config
47 | hugo serve
48 | @$(MAKE) docs-restore-generated-file
49 |
50 | docs-place-redirect:
51 | echo " REDIRECT TO THE LATEST_VERSION.
" > $(OUTPUT)/$(PRODUCT)/index.html
52 |
53 | docs-restore-generated-file:
54 | mv config.bak config.yaml
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/config.yaml:
--------------------------------------------------------------------------------
1 | # VERSIONS=latest,v1.0 hugo --minify --baseURL="/product/v1.0/" -d public/product/v1.0
2 |
3 | title: INFINI Loadgen
4 | theme: book
5 |
6 | # Book configuration
7 | disablePathToLower: true
8 | enableGitInfo: false
9 |
10 | outputs:
11 | home:
12 | - HTML
13 | - RSS
14 | - JSON
15 |
16 | # Needed for mermaid/katex shortcodes
17 | markup:
18 | goldmark:
19 | renderer:
20 | unsafe: true
21 | tableOfContents:
22 | startLevel: 1
23 |
24 | # Multi-lingual mode config
25 | # There are different options to translate files
26 | # See https://gohugo.io/content-management/multilingual/#translation-by-filename
27 | # And https://gohugo.io/content-management/multilingual/#translation-by-content-directory
28 | defaultContentLanguage: en
29 | languages:
30 | en:
31 | languageName: English
32 | contentDir: content.en
33 | weight: 3
34 | zh:
35 | languageName: 简体中文
36 | contentDir: content.zh
37 | weight: 4
38 |
39 |
40 | menu:
41 | before: []
42 | after:
43 | - name: "Github"
44 | url: "https://github.com/infinilabs/loadgen"
45 | weight: 10
46 |
47 | params:
48 | # (Optional, default light) Sets color theme: light, dark or auto.
49 | # Theme 'auto' switches between dark and light modes based on browser/os preferences
50 | BookTheme: "auto"
51 |
52 | # (Optional, default true) Controls table of contents visibility on right side of pages.
53 | # Start and end levels can be controlled with markup.tableOfContents setting.
54 | # You can also specify this parameter per page in front matter.
55 | BookToC: true
56 |
57 | # (Optional, default none) Set the path to a logo for the book. If the logo is
58 | # /static/logo.png then the path would be logo.png
59 | BookLogo: img/logo
60 |
61 | # (Optional, default none) Set leaf bundle to render as side menu
62 | # When not specified file structure and weights will be used
63 | # BookMenuBundle: /menu
64 |
65 | # (Optional, default docs) Specify root page to render child pages as menu.
66 | # Page is resoled by .GetPage function: https://gohugo.io/functions/getpage/
67 | # For backward compatibility you can set '*' to render all sections to menu. Acts same as '/'
68 | BookSection: docs
69 |
70 | # Set source repository location.
71 | # Used for 'Last Modified' and 'Edit this page' links.
72 | BookRepo: https://github.com/infinilabs/loadgen
73 |
74 | # Enable "Edit this page" links for 'doc' page type.
75 | # Disabled by default. Uncomment to enable. Requires 'BookRepo' param.
76 | # Edit path must point to root directory of repo.
77 | BookEditPath: edit/BRANCH/docs
78 |
79 | # Configure the date format used on the pages
80 | # - In git information
81 | # - In blog posts
82 | BookDateFormat: "January 2, 2006"
83 |
84 | # (Optional, default true) Enables search function with flexsearch,
85 | # Index is built on fly, therefore it might slowdown your website.
86 | # Configuration for indexing can be adjusted in i18n folder per language.
87 | BookSearch: false
88 |
89 | # (Optional, default true) Enables comments template on pages
90 | # By default partals/docs/comments.html includes Disqus template
91 | # See https://gohugo.io/content-management/comments/#configure-disqus
92 | # Can be overwritten by same param in page frontmatter
93 | BookComments: false
94 |
95 | # /!\ This is an experimental feature, might be removed or changed at any time
96 | # (Optional, experimental, default false) Enables portable links and link checks in markdown pages.
97 | # Portable links meant to work with text editors and let you write markdown without {{< relref >}} shortcode
98 | # Theme will print warning if page referenced in markdown does not exists.
99 | BookPortableLinks: true
100 |
101 | # /!\ This is an experimental feature, might be removed or changed at any time
102 | # (Optional, experimental, default false) Enables service worker that caches visited pages and resources for offline use.
103 | BookServiceWorker: false
104 |
--------------------------------------------------------------------------------
/docs/content.en/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: INFINI Loadgen
3 | type: docs
4 | bookCollapseSection: true
5 | weight: 3
6 | ---
7 |
8 | # INFINI Loadgen
9 |
10 | ## Introduction
11 |
12 | INFINI Loadgen is a lightweight performance testing tool specifically designed for Easysearch, Elasticsearch, and OpenSearch.
13 |
14 | ## Features
15 |
16 | - Robust performance
17 | - Lightweight and dependency-free
18 | - Random selection of template-based parameters
19 | - High concurrency
20 | - Balanced traffic control at the benchmark end
21 | - Validate server responses.
22 |
23 | {{< button relref="../docs/getting-started/install/" >}}Getting Started Now{{< /button >}}
24 |
25 | ## Community
26 |
27 | Fell free to join the Discord server to discuss anything around this project:
28 |
29 | [Discord Server](https://discord.gg/4tKTMkkvVX)
30 |
31 | ## Who Is Using?
32 |
33 | If you are using INFINI Loadgen and feel it pretty good, please [let us know](https://discord.gg/4tKTMkkvVX). Thank you for your support.
34 |
--------------------------------------------------------------------------------
/docs/content.en/docs/getting-started/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 10
3 | title: Getting Started
4 | bookCollapseSection: true
5 | ---
6 |
--------------------------------------------------------------------------------
/docs/content.en/docs/getting-started/benchmark.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 50
3 | title: "Benchmark Testing"
4 | ---
5 |
6 | # Benchmark Testing
7 |
8 | INFINI Loadgen is a lightweight performance testing tool specifically designed for Easysearch, Elasticsearch, and OpenSearch.
9 |
10 | Features of Loadgen:
11 |
12 | - Robust performance
13 | - Lightweight and dependency-free
14 | - Supports template-based parameter randomization
15 | - Supports high concurrency
16 | - Supports balanced traffic control at the benchmark end
17 | - Supports server response validation
18 |
19 | > Download link:
20 |
21 | ## Loadgen
22 |
23 | Loadgen is easy to use. After the tool is downloaded and decompressed, you will get three files: an executable program, a configuration file `loadgen.yml`, and a test file `loadgen.dsl`. The configuration file example is as follows:
24 |
25 | ```yaml
26 | env:
27 | ES_USERNAME: elastic
28 | ES_PASSWORD: elastic
29 | ES_ENDPOINT: http://localhost:8000
30 | ```
31 |
32 | The test file example is as follows:
33 |
34 | ```text
35 | # runner: {
36 | # // total_rounds: 1
37 | # no_warm: false,
38 | # // Whether to log all requests
39 | # log_requests: false,
40 | # // Whether to log all requests with the specified response status
41 | # log_status_codes: [0, 500],
42 | # assert_invalid: false,
43 | # assert_error: false,
44 | # },
45 | # variables: [
46 | # {
47 | # name: "ip",
48 | # type: "file",
49 | # path: "dict/ip.txt",
50 | # // Replace special characters in the value
51 | # replace: {
52 | # '"': '\\"',
53 | # '\\': '\\\\',
54 | # },
55 | # },
56 | # {
57 | # name: "id",
58 | # type: "sequence",
59 | # },
60 | # {
61 | # name: "id64",
62 | # type: "sequence64",
63 | # },
64 | # {
65 | # name: "uuid",
66 | # type: "uuid",
67 | # },
68 | # {
69 | # name: "now_local",
70 | # type: "now_local",
71 | # },
72 | # {
73 | # name: "now_utc",
74 | # type: "now_utc",
75 | # },
76 | # {
77 | # name: "now_utc_lite",
78 | # type: "now_utc_lite",
79 | # },
80 | # {
81 | # name: "now_unix",
82 | # type: "now_unix",
83 | # },
84 | # {
85 | # name: "now_with_format",
86 | # type: "now_with_format",
87 | # // https://programming.guide/go/format-parse-string-time-date-example.html
88 | # format: "2006-01-02T15:04:05-0700",
89 | # },
90 | # {
91 | # name: "suffix",
92 | # type: "range",
93 | # from: 10,
94 | # to: 1000,
95 | # },
96 | # {
97 | # name: "bool",
98 | # type: "range",
99 | # from: 0,
100 | # to: 1,
101 | # },
102 | # {
103 | # name: "list",
104 | # type: "list",
105 | # data: ["medcl", "abc", "efg", "xyz"],
106 | # },
107 | # {
108 | # name: "id_list",
109 | # type: "random_array",
110 | # variable_type: "number", // number/string
111 | # variable_key: "suffix", // variable key to get array items
112 | # square_bracket: false,
113 | # size: 10, // how many items for array
114 | # },
115 | # {
116 | # name: "str_list",
117 | # type: "random_array",
118 | # variable_type: "number", // number/string
119 | # variable_key: "suffix", // variable key to get array items
120 | # square_bracket: true,
121 | # size: 10, // how many items for array
122 | # replace: {
123 | # // Use ' instead of " for string quotes
124 | # '"': "'",
125 | # // Use {} instead of [] as array brackets
126 | # "[": "{",
127 | # "]": "}",
128 | # },
129 | # },
130 | # ],
131 |
132 | POST $[[env.ES_ENDPOINT]]/medcl/_search
133 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } }
134 | # request: {
135 | # runtime_variables: {batch_no: "uuid"},
136 | # runtime_body_line_variables: {routing_no: "uuid"},
137 | # basic_auth: {
138 | # username: "$[[env.ES_USERNAME]]",
139 | # password: "$[[env.ES_PASSWORD]]",
140 | # },
141 | # },
142 | ```
143 |
144 | ### Running Mode Settings
145 |
146 | By default, Loadgen runs in performance testing mode, repeating all requests in `requests` for the specified duration (`-d`). If you only need to check the test results once, you can set the number of executions of `requests` by `runner.total_rounds`.
147 |
148 | ### HTTP Header Handling
149 |
150 | By default, Loadgen will automatically format the HTTP response headers (`user-agent: xxx` -> `User-Agent: xxx`). If you need to precisely determine the response headers returned by the server, you can disable this behavior by setting `runner.disable_header_names_normalizing`.
151 |
152 | ## Usage of Variables
153 |
154 | In the above configuration, `variables` is used to define variable parameters, identified by `name`. In a constructed request, `$[[Variable name]]` can be used to access the value of the variable. The currently supported variable types are:
155 |
156 | | Type | Description | Parameters |
157 | | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158 | | `file` | Load variables from file | `path`: the path of the data files
`data`: a list of values, will get appended to the end of the data specified by `path` file |
159 | | `list` | Defined variables inline | use `data` to define a string array |
160 | | `sequence` | 32-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values |
161 | | `sequence64` | 64-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values |
162 | | `range` | Variable of the range numbers, support parameters `from` and `to` to define the range | `from`: the minimum of the values
`to`: the maximum of the values |
163 | | `random_array` | Generate a random array, data elements come from the variable specified by `variable_key` | `variable_key`: data source variable
`size`: length of the output array
`square_bracket`: `true/false`, whether the output value needs `[` and `]`
`string_bracket`: string, the specified string will be attached before and after the output element |
164 | | `uuid` | UUID string type variable | |
165 | | `now_local` | Current time, local time zone | |
166 | | `now_utc` | Current time, UTC time zone. Output format: `2006-01-02 15:04:05.999999999 -0700 MST` | |
167 | | `now_utc_lite` | Current time, UTC time zone. Output format: `2006-01-02T15:04:05.000` | |
168 | | `now_unix` | Current time, Unix timestamp | |
169 | | `now_with_format` | Current time, supports custom `format` parameter to format the time string, such as: `2006-01-02T15:04:05-0700` | `format`: output time format ([example](https://www.geeksforgeeks.org/time-formatting-in-golang/)) |
170 |
171 | ### Variable Usage Example
172 |
173 | Variable parameters of the `file` type are loaded from an external text file. One variable parameter occupies one line. When one variable of the file type is accessed, one variable value is taken randomly. An example of the variable format is as follows:
174 |
175 | ```text
176 | # test/user.txt
177 | medcl
178 | elastic
179 | ```
180 |
181 | Tips about how to generate a random string of fixed length, such as 1024 per line:
182 |
183 | ```bash
184 | LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt
185 | ```
186 |
187 | ### Environment Variables
188 |
189 | Loadgen supports loading and using environment variables. You can specify the default values in the `loadgen.dsl` configuration. Loadgen will overwrite the variables at runtime if they are also specified by the command-line environment.
190 |
191 | The environment variables can be accessed by `$[[env.ENV_KEY]]`:
192 |
193 | ```text
194 | #// Configure default values for environment variables
195 | # env: {
196 | # ES_USERNAME: "elastic",
197 | # ES_PASSWORD: "elastic",
198 | # ES_ENDPOINT: "http://localhost:8000",
199 | # },
200 |
201 | #// Use runtime variables
202 | GET $[[env.ES_ENDPOINT]]/medcl/_search
203 | {"query": {"match": {"name": "$[[user]]"}}}
204 | # request: {
205 | # // Use runtime variables
206 | # basic_auth: {
207 | # username: "$[[env.ES_USERNAME]]",
208 | # password: "$[[env.ES_PASSWORD]]",
209 | # },
210 | # },
211 | ```
212 |
213 | ## Request Definition
214 |
215 | The `requests` node is used to set requests to be executed by Loadgen in sequence. Loadgen supports fixed-parameter requests and requests constructed using template-based variable parameters. The following is an example of a common query request:
216 |
217 | ```text
218 | GET http://localhost:8000/medcl/_search?q=name:$[[user]]
219 | # request: {
220 | # username: elastic,
221 | # password: pass,
222 | # },
223 | ```
224 |
225 | In the above query, Loadgen conducts queries based on the `medcl` index and executes one query based on the `name` field. The value of each request is from the random variable `user`.
226 |
227 | ### Simulating Bulk Ingestion
228 |
229 | It is very easy to use Loadgen to simulate bulk ingestion. Configure one index operation in the request body and then use the `body_repeat_times` parameter to randomly replicate several parameterized requests to complete the preparation of a batch of requests. See the following example.
230 |
231 | ```text
232 | POST http://localhost:8000/_bulk
233 | {"index": {"_index": "medcl-y4", "_type": "doc", "_id": "$[[uuid]]"}}
234 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
235 | # request: {
236 | # basic_auth: {
237 | # username: "test",
238 | # password: "testtest",
239 | # },
240 | # body_repeat_times: 1000,
241 | # },
242 | ```
243 |
244 | ### Response Assertions
245 |
246 | You can use the `assert` configuration to check the response values. `assert` now supports most of all the [condition checkers](https://docs.infinilabs.com/gateway/main/docs/references/flow/#condition-type) of INFINI Gateway.
247 |
248 | ```text
249 | GET http://localhost:8000/medcl/_search?q=name:$[[user]]
250 | # request: {
251 | # basic_auth: {
252 | # username: "test",
253 | # password: "testtest",
254 | # },
255 | # },
256 | # assert: {
257 | # _ctx.response.status: 201,
258 | # },
259 | ```
260 |
261 | The
262 |
263 | response value can be accessed from the `_ctx` value, currently it contains these values:
264 |
265 | | Parameter | Description |
266 | | ------------------------- | ----------------------------------------------------------------------------------------------- |
267 | | `_ctx.response.status` | HTTP response status code |
268 | | `_ctx.response.header` | HTTP response headers |
269 | | `_ctx.response.body` | HTTP response body text |
270 | | `_ctx.response.body_json` | If the HTTP response body is a valid JSON string, you can access the JSON fields by `body_json` |
271 | | `_ctx.elapsed` | The time elapsed since request sent to the server (milliseconds) |
272 |
273 | If the request failed (e.g. the host is not reachable), Loadgen will record it under `Number of Errors` as part of the testing output. If you configured `runner.assert_error: true`, Loadgen will exit as `exit(2)` when there're any requests failed.
274 |
275 | If the assertion failed, Loadgen will record it under `Number of Invalid` as part of the testing output and skip the subsequent requests in this round. If you configured `runner.assert_invalid: true`, Loadgen will exit as `exit(1)` when there're any assertions failed.
276 |
277 | ### Dynamic Variable Registration
278 |
279 | Each request can use `register` to dynamically set the variables based on the response value, a common usage is to update the parameters of the later requests based on the previous responses.
280 |
281 | In the below example, we're registering the response value `_ctx.response.body_json.test.settings.index.uuid` of the `$[[env.ES_ENDPOINT]]/test` to the `index_id` variable, then we can access it by `$[[index_id]]`.
282 |
283 | ```text
284 | GET $[[env.ES_ENDPOINT]]/test
285 | # register: [
286 | # {index_id: "_ctx.response.body_json.test.settings.index.uuid"},
287 | # ],
288 | # assert: (200, {}),
289 | ```
290 |
291 | ## Running the Benchmark
292 |
293 | Run the Loadgen program to perform the benchmark test as follows:
294 |
295 | ```text
296 | $ loadgen -d 30 -c 100 -compress -run
297 |
298 | loadgen.dsl
299 |
300 |
301 | __ ___ _ ___ ___ __ __
302 | / / /___\/_\ / \/ _ \ /__\/\ \ \
303 | / / // ///_\\ / /\ / /_\//_\ / \/ /
304 | / /__/ \_// _ \/ /_// /_\\//__/ /\ /
305 | \____|___/\_/ \_/___,'\____/\__/\_\ \/
306 |
307 | [LOADGEN] A http load generator and testing suit.
308 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files
309 | [07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0
310 | [07-19 16:15:00] [INF] [loader.go:312] warmup started
311 | [07-19 16:15:00] [INF] [app.go:306] loadgen now started.
312 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search
313 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
314 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl
315 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
316 | [07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk
317 | [07-19 16:15:01] [INF] [loader.go:317] status: 200,,{"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}}]}
318 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished
319 |
320 | 5253 requests in 32.756483336s, 524.61KB sent, 2.49MB received
321 |
322 | [Loadgen Client Metrics]
323 | Requests/sec: 175.10
324 | Request Traffic/sec: 17.49KB
325 | Total Transfer/sec: 102.34KB
326 | Avg Req Time: 5.711022ms
327 | Fastest Request: 440.448µs
328 | Slowest Request: 3.624302658s
329 | Number of Errors: 0
330 | Number of Invalid: 0
331 | Status 200: 5253
332 |
333 | [Estimated Server Metrics]
334 | Requests/sec: 160.37
335 | Transfer/sec: 93.73KB
336 | Avg Req Time: 623.576686ms
337 | ```
338 |
339 | Before the formal benchmark, Loadgen will execute all requests once for warm-up. If an error occurs, it will prompt whether to continue. The warm-up request results will also be output to the terminal. After execution, a summary of the execution will be output. You can skip this check phase by setting `runner.no_warm`.
340 |
341 | > Since the final result of Loadgen is the cumulative statistics after all requests are completed, there may be inaccuracies. It is recommended to monitor Elasticsearch's various operating indicators in real-time through the Kibana monitoring dashboard.
342 |
343 | ### Command Line Parameters
344 |
345 | Loadgen will loop through the requests defined in the configuration file. By default, Loadgen will only run for `5s` and then automatically exit. If you want to extend the runtime or increase concurrency, you can control it by setting parameters at startup. Check the help command as follows:
346 |
347 | ```text
348 | $ loadgen -help
349 | Usage of loadgen:
350 | -c int
351 | Number of concurrent threads (default 1)
352 | -compress
353 | Compress requests with gzip
354 | -config string
355 | the location of config file (default "loadgen.yml")
356 | -cpu int
357 | the number of CPUs to use (default -1)
358 | -d int
359 | Duration of tests in seconds (default 5)
360 | -debug
361 | run in debug mode, loadgen will quit on panic immediately with full stack trace
362 | -dial-timeout int
363 | Connection dial timeout in seconds, default 3s (default 3)
364 | -gateway-log string
365 | Log level of Gateway (default "debug")
366 | -l int
367 | Limit total requests (default -1)
368 | -log string
369 | the log level, options: trace,debug,info,warn,error,off
370 | -mem int
371 | the max size of Memory to use, soft limit in megabyte (default -1)
372 | -plugin value
373 | load additional plugins
374 | -r int
375 | Max requests per second (fixed QPS) (default -1)
376 | -read-timeout int
377 | Connection read timeout in seconds, default 0s (use -timeout)
378 | -run string
379 | DSL config to run tests (default "loadgen.dsl")
380 | -service string
381 | service management, options: install,uninstall,start,stop
382 | -timeout int
383 | Request timeout in seconds, default 60s (default 60)
384 | -v version
385 | -write-timeout int
386 | Connection write timeout in seconds, default 0s (use -timeout)
387 | ```
388 |
389 | ### Limiting Client Workload
390 |
391 | Using Loadgen and setting the command line parameter `-r` can limit the number of requests sent by the client per second, thereby evaluating the response time and load of Elasticsearch under fixed pressure, as follows:
392 |
393 | ```bash
394 | loadgen -d 30 -c 100 -r 100
395 | ```
396 |
397 | > Note: The client throughput limit may not be accurate enough in the case of massive concurrencies.
398 |
399 | ### Limiting the Total Number of Requests
400 |
401 | By setting the parameter `-l`, you can control the total number of requests sent by the client to generate fixed documents. Modify the configuration as follows:
402 |
403 | ```text
404 | #// loadgen-gw.dsl
405 | POST http://localhost:8000/medcl-test/doc2/_bulk
406 | {"index": {"_index": "medcl-test", "_id": "$[[uuid]]"}}
407 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]"}
408 | # request: {
409 | # basic_auth: {
410 | # username: "test",
411 | # password: "testtest",
412 | # },
413 | # body_repeat_times: 1,
414 | # },
415 | ```
416 |
417 | Each request contains only one document, then execute Loadgen
418 |
419 | ```bash
420 | loadgen -run loadgen-gw.dsl -d 600 -c 100 -l 50000
421 | ```
422 |
423 | After execution, the Elasticsearch index `medcl-test` will have `50000` more records.
424 |
425 | ### Using Auto Incremental IDs to Ensure the Document Sequence
426 |
427 | If you want the generated document IDs to increase regularly for easy comparison, you can use the `sequence` type auto incremental ID as the primary key and avoid using random numbers in the content, as follows:
428 |
429 | ```text
430 | POST http://localhost:8000/medcl-test/doc2/_bulk
431 | {"index": {"_index": "medcl-test", "_id": "$[[id]]"}}
432 | {"id": "$[[id]]"}
433 | # request: {
434 | # basic_auth: {
435 | # username: "test",
436 | # password: "testtest",
437 | # },
438 | # body_repeat_times: 1,
439 | # },
440 | ```
441 |
442 | ### Reuse Variables in Request Context
443 |
444 | In a request, we might want to use the same variable value, such as the `routing` parameter to control the shard destination, also store the field in the JSON document. You can use `runtime_variables` to set request-level variables, or `runtime_body_line_variables` to define request-body-level variables. If the request body is replicated N times, each line will be different, as shown in the following example:
445 |
446 | ```text
447 | # variables: [
448 | # {name: "id", type: "sequence"},
449 | # {name: "uuid", type: "uuid"},
450 | # {name: "now_local", type: "now_local"},
451 | # {name: "now_utc", type: "now_utc"},
452 | # {name: "now_unix", type: "now_unix"},
453 | # {name: "suffix", type: "range", from: 10, to 15},
454 | # ],
455 |
456 | POST http://192.168.3.188:9206/_bulk
457 | {"create": {"_index": "test-$[[suffix]]", "_type": "doc", "_id": "$[[uuid]]", "routing": "$[[routing_no]]"}}
458 | {"id": "$[[uuid]]", "routing_no": "$[[routing_no]]", "batch_number": "$[[batch_no]]", "random_no": "$[[suffix]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
459 | # request: {
460 | # runtime_variables: {
461 | # batch_no: "id",
462 | # },
463 | # runtime_body_line_variables: {
464 | # routing_no: "uuid",
465 | # },
466 | # basic_auth: {
467 | # username: "ingest",
468 | # password: "password",
469 | # },
470 | # body_repeat_times: 10,
471 | # },
472 | ```
473 |
474 | We defined the `batch_no` variable to represent the same batch number in a batch of documents, and the `routing_no` variable to represent the routing value at each document level.
475 |
476 | ### Customize Header
477 |
478 | ```text
479 | GET http://localhost:8000/test/_search
480 | # request: {
481 | # headers: [
482 | # {Agent: "Loadgen-1"},
483 | # ],
484 | # disable_header_names_normalizing: false,
485 | # },
486 | ```
487 |
488 | By default, Loadgen will canonilize the HTTP header keys in the configuration (`user-agent: xxx` -> `User-Agent: xxx`). If you need to set the HTTP header keys exactly, you can disable this behavior by setting `disable_header_names_normalizing: true`.
489 |
490 | ## Running Test Suites
491 |
492 | Loadgen supports running test cases in batches without writing test cases repeatedly. You can quickly test different environment configurations by switching suite configurations:
493 |
494 | ```yaml
495 | # loadgen.yml
496 | env:
497 | # Set up environments to run test suite
498 | LR_TEST_DIR: ./testing # The path to the test cases.
499 | # If you want to start gateway dynamically and automatically:
500 | LR_GATEWAY_CMD: ./bin/gateway # The path to the executable of INFINI Gateway
501 | LR_GATEWAY_HOST: 0.0.0.0:18000 # The binding host of the INFINI Gateway
502 | LR_GATEWAY_API_HOST: 0.0.0.0:19000 # The binding host of the INFINI Gateway API server
503 | # Set up other environments for the gateway and loadgen
504 | LR_ELASTICSEARCH_ENDPOINT: http://localhost:19201
505 | CUSTOM_ENV: myenv
506 | tests:
507 | # The relative path of test cases under `LR_TEST_DIR`
508 | #
509 | # - gateway.yml: (Optional) the configuration to start the INFINI Gateway dynamically.
510 | # - loadgen.dsl: the configuration to run the loadgen tool.
511 | #
512 | # The environments set in `env` section will be passed to the INFINI Gateway and loadgen.
513 | - path: cases/gateway/echo/echo_with_context
514 | ```
515 |
516 | ### Environment Variables Configuration
517 |
518 | Loadgen dynamically configures INFINI Gateway through environment variables specified in `env`. The following environment variables are required:
519 |
520 | | Variable Name | Description |
521 | | ------------- | ----------------------- |
522 | | `LR_TEST_DIR` | Directory of test cases |
523 |
524 | If you need `loadgen` to dynamically start INFINI Gateway based on the configuration, you need to set the following environment variables:
525 |
526 | | Variable Name | Description |
527 | | --------------------- | ---------------------------------------- |
528 | | `LR_GATEWAY_CMD` | Path to the executable of INFINI Gateway |
529 | | `LR_GATEWAY_HOST` | Binding host:port of INFINI Gateway |
530 | | `LR_GATEWAY_API_HOST` | Binding host:port of INFINI Gateway API |
531 |
532 | ### Test Case Configuration
533 |
534 | Test cases are configured in `tests`, each path points to a directory of a test case. Each test case needs to configure a `gateway.yml` (optional) and a `loadgen.dsl`. Configuration files can use environment variables configured in `env` (`$[[env.ENV_KEY]]`).
535 |
536 | Example `gateway.yml` configuration:
537 |
538 | ```yaml
539 | path.data: data
540 | path.logs: log
541 |
542 | entry:
543 | - name: my_es_entry
544 | enabled: true
545 | router: my_router
546 | max_concurrency: 200000
547 | network:
548 | binding: $[[env.LR_GATEWAY_HOST]]
549 |
550 | flow:
551 | - name: hello_world
552 | filter:
553 | - echo:
554 | message: "hello world"
555 | router:
556 | - name: my_router
557 | default_flow: hello_world
558 | ```
559 |
560 | Example `loadgen.dsl` configuration:
561 |
562 | ```
563 | # runner: {
564 | # total_rounds: 1,
565 | # no_warm: true,
566 | # log_requests: true,
567 | # assert_invalid: true,
568 | # assert_error: true,
569 | # },
570 |
571 | GET http://$[[env.LR_GATEWAY_HOST]]/
572 | # assert: {
573 | # _ctx.response: {
574 | # status: 200,
575 | # body: "hello world",
576 | # },
577 | # },
578 | ```
579 |
580 | ### Running Test Suites
581 |
582 | After configuring `loadgen.yml`, you can run Loadgen with the following command:
583 |
584 | ```bash
585 | loadgen -config loadgen.yml
586 | ```
587 |
588 | Loadgen will run all the test cases specified in the configuration and output the test results:
589 |
590 | ```text
591 | $ loadgen -config loadgen.yml
592 | __ ___ _ ___ ___ __ __
593 | / / /___\/_\ / \/ _ \ /__\/\ \ \
594 | / / // ///_\\ / /\ / /_\//_\ / \/ /
595 | / /__/ \_// _ \/ /_// /_\\//__/ /\ /
596 | \____|___/\_/ \_/___,'\____/\__/\_\ \/
597 |
598 | [LOADGEN] A http load generator and testing suit.
599 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files
600 | [02-21 10:50:05] [INF] [app.go:192] initializing loadgen
601 | [02-21 10:50:05] [INF] [app.go:193] using config: /Users/kassian/Workspace/infini/src/infini.sh/testing/suites/dev.yml
602 | [02-21 10:50:05] [INF] [instance.go:78] workspace: /Users/kassian/Workspace/infini/src/infini.sh/testing/data/loadgen/nodes/cfpihf15k34iqhpd4d00
603 | [02-21 10:50:05] [INF] [app.go:399] loadgen is up and running now.
604 | [2023-02-21 10:50:05][TEST][SUCCESS] [setup/loadgen/cases/dummy] duration: 105(ms)
605 |
606 | 1 requests in 68.373875ms, 0.00bytes sent, 0.00bytes received
607 |
608 | [Loadgen Client Metrics]
609 | Requests/sec: 0.20
610 | Request Traffic/sec: 0.00bytes
611 | Total Transfer/sec: 0.00bytes
612 | Avg Req Time: 5s
613 | Fastest Request: 68.373875ms
614 | Slowest Request: 68.373875ms
615 | Number of Errors: 0
616 | Number of Invalid: 0
617 | Status 200: 1
618 |
619 | [Estimated Server Metrics]
620 | Requests/sec: 14.63
621 | Transfer/sec: 0.00bytes
622 | Avg Req Time: 68.373875ms
623 |
624 |
625 | [2023-02-21 10:50:06][TEST][FAILED] [setup/gateway/cases/echo/echo_with_context/] duration: 1274(ms)
626 | #0 request, GET http://$[[env.LR_GATEWAY_HOST]]/any/, assertion failed, skiping subsequent requests
627 | 1 requests in 1.255678s, 0.00bytes sent, 0.00bytes received
628 |
629 | [Loadgen Client Metrics]
630 | Requests/sec: 0.20
631 | Request Traffic/sec: 0.00bytes
632 | Total Transfer/sec: 0.00bytes
633 | Avg Req Time: 5s
634 | Fastest Request: 1.255678s
635 | Slowest Request: 1.255678s
636 | Number of Errors: 1
637 | Number of Invalid: 1
638 | Status 0: 1
639 |
640 | [Estimated Server Metrics]
641 | Requests/sec: 0.80
642 | Transfer/sec: 0.00bytes
643 | Avg Req Time: 1.255678s
644 |
645 | ```
646 |
--------------------------------------------------------------------------------
/docs/content.en/docs/getting-started/install.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 10
3 | title: Installing the Loadgen
4 | asciinema: true
5 | ---
6 |
7 | # Installing the Loadgen
8 |
9 | INFINI Loadgen supports mainstream operating systems and platforms. The program package is small, with no extra external dependency. So, the loadgen can be installed very rapidly.
10 |
11 | ## Downloading
12 |
13 | **Automatic install**
14 |
15 | ```bash
16 | curl -sSL http://get.infini.cloud | bash -s -- -p loadgen
17 | ```
18 |
19 | The above script can automatically download the latest version of the corresponding platform's loadgen and extract it to /opt/loadgen
20 |
21 | The optional parameters for the script are as follows:
22 |
23 | > _-v [version number](Default to use the latest version number)_
24 | > _-d [installation directory] (default installation to /opt/loadgen)_
25 |
26 | ```bash
27 | ➜ /tmp mkdir loadgen
28 | ➜ /tmp curl -sSL http://get.infini.cloud | bash -s -- -p loadgen -d /tmp/loadgen
29 |
30 | @@@@@@@@@@@
31 | @@@@@@@@@@@@
32 | @@@@@@@@@@@@
33 | @@@@@@@@@&@@@
34 | #@@@@@@@@@@@@@
35 | @@@ @@@@@@@@@@@@@
36 | &@@@@@@@ &@@@@@@@@@@@@@
37 | @&@@@@@@@&@ @@@&@@@@@@@&@
38 | @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@
39 | @@@@@@@@@@@@@@@@@@& @@@@@@@@@@@@@
40 | %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
41 | @@@@@@@@@@@@&@@@@@@@@@@@@@@@
42 | @@ ,@@@@@@@@@@@@@@@@@@@@@@@&
43 | @@@@@. @@@@@&@@@@@@@@@@@@@@
44 | @@@@@@@@@@ @@@@@@@@@@@@@@@#
45 | @&@@@&@@@&@@@ &@&@@@&@@@&@
46 | @@@@@@@@@@@@@. @@@@@@@*
47 | @@@@@@@@@@@@@ %@@@
48 | @@@@@@@@@@@@@
49 | /@@@@@@@&@@@@@
50 | @@@@@@@@@@@@@
51 | @@@@@@@@@@@@@
52 | @@@@@@@@@@@@ Welcome to INFINI Labs!
53 |
54 |
55 | Now attempting the installation...
56 |
57 | Name: [loadgen], Version: [1.26.1-598], Path: [/tmp/loadgen]
58 | File: [https://release.infinilabs.com/loadgen/stable/loadgen-1.26.1-598-mac-arm64.zip]
59 | ##=O#- #
60 |
61 | Installation complete. [loadgen] is ready to use!
62 |
63 |
64 | ----------------------------------------------------------------
65 | cd /tmp/loadgen && ./loadgen-mac-arm64
66 | ----------------------------------------------------------------
67 |
68 |
69 | __ _ __ ____ __ _ __ __
70 | / // |/ // __// // |/ // /
71 | / // || // _/ / // || // /
72 | /_//_/|_//_/ /_//_/|_//_/
73 |
74 | ©INFINI.LTD, All Rights Reserved.
75 | ```
76 |
77 | **Manual install**
78 |
79 | Select a package for downloading in the following URL based on your operating system and platform:
80 |
81 | [https://release.infinilabs.com/loadgen/](https://release.infinilabs.com/loadgen/)
82 |
--------------------------------------------------------------------------------
/docs/content.en/docs/release-notes/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 80
3 | title: "Release Notes"
4 | ---
5 |
6 | # Release Notes
7 |
8 | Information about release notes of INFINI Loadgen is provided here.
9 |
10 | ## Latest (In development)
11 | ### ❌ Breaking changes
12 | ### 🚀 Features
13 | ### 🐛 Bug fix
14 | ### ✈️ Improvements
15 |
16 | ## 1.29.4 (2025-05-16)
17 | ### ✈️ Improvements
18 | - This release includes updates from the underlying [Framework v1.1.7](https://docs.infinilabs.com/framework/v1.1.7/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Gateway itself, the improvements inherited from Framework benefit Loadgen indirectly.
19 |
20 | ## 1.29.3 (2025-04-27)
21 | This release includes updates from the underlying [Framework v1.1.6](https://docs.infinilabs.com/framework/v1.1.6/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly.
22 |
23 |
24 | ## 1.29.2 (2025-03-31)
25 | This release includes updates from the underlying [Framework v1.1.5](https://docs.infinilabs.com/framework/v1.1.5/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly.
26 |
27 | ## 1.29.1 (2025-03-14)
28 | This release includes updates from the underlying [Framework v1.1.4](https://docs.infinilabs.com/framework/v1.1.4/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly.
29 |
30 |
31 | ## 1.29.0 (2025-02-28)
32 |
33 | ### Improvements
34 |
35 | - Synchronize updates for known issues fixed in the Framework.
36 |
37 | ## 1.28.2 (2025-02-15)
38 |
39 | ### Improvements
40 |
41 | - Synchronize updates for known issues fixed in the Framework.
42 |
43 | ## 1.28.1 (2025-01-24)
44 |
45 | ### Improvements
46 |
47 | - Synchronize updates for known issues fixed in the Framework.
48 |
49 | ## 1.28.0 (2025-01-11)
50 |
51 | ### Improvements
52 |
53 | - Synchronize updates for known issues fixed in the Framework.
54 |
55 | ## 1.27.0 (2024-12-13)
56 |
57 | ### Improvements
58 |
59 | - The code is open source, and Github [repository](https://github.com/infinilabs/loadgen) is used for development.
60 | - Keep the same version number as INFINI Console.
61 | - Synchronize updates for known issues fixed in the Framework.
62 |
63 | ### Bug fix
64 |
65 | - Fix the abnormal problem of the API interface testing logic.
66 |
67 | ## 1.26.1 (2024-08-13)
68 |
69 | ### Improvements
70 |
71 | - Keep the same version number as INFINI Console.
72 | - Synchronize updates for known issues fixed in the Framework.
73 |
74 | ## 1.26.0 (2024-06-07)
75 |
76 | ### Improvements
77 |
78 | - Keep the same version number as INFINI Console.
79 | - Synchronize updates for known issues fixed in the Framework.
80 |
81 | ## 1.25.0 (2024-04-30)
82 |
83 | ### Improvements
84 |
85 | - Keep the same version number as INFINI Console.
86 | - Synchronize updates for known issues fixed in the Framework.
87 |
88 | ## 1.24.0 (2024-04-15)
89 |
90 | ### Improvements
91 |
92 | - Keep the same version number as INFINI Console.
93 | - Synchronize updates for known issues fixed in the Framework.
94 |
95 | ## 1.22.0 (2024-01-26)
96 |
97 | ### Improvements
98 |
99 | - Unified version number with INFINI Console
100 |
101 | ## 1.8.0 (2023-11-02)
102 |
103 | ### Breaking changes
104 |
105 | - The original Loadrun function is incorporated into Loadgen.
106 | - Test the requests, assertions, etc. that is configured using the new Loadgen DSL syntax.
107 |
108 | ## 1.7.0 (2023-04-20)
109 |
110 | ### Breaking changes
111 |
112 | - The variables with the same `name` are no longer allowed to be defined in `variables`.
113 |
114 | ### Features
115 |
116 | - Add the `log_status_code` configuration to support printing request logs of specific status codes.
117 |
118 | ## 1.6.0 (2023-04-06)
119 |
120 | ### Breaking ghanges
121 |
122 | - The `file` type variable by default no longer escapes the `"` and `\` characters. Use the `replace` function to manually set variable escaping.
123 |
124 | ### Features
125 |
126 | - The variable definition adds an optional `replace` option, which is used to escape characters such as `"` and `\`.
127 |
128 | ### Improvements
129 |
130 | - Optimize memory usage.
131 |
132 | ### Bug fix
133 |
134 | - Fix the problem that the `\n` cannot be used in the YAML strings.
135 | - Fix the problem that invalid assert configurations are ignored.
136 |
137 | ## 1.5.1
138 |
139 | ### Bug fix
140 |
141 | - [DOC] Fix invalid variable grammar in `loadgen.yml`.
142 |
143 | ## 1.5.0
144 |
145 | ### Features
146 |
147 | - Added `assert` configuration, support testing response data.
148 | - Added `register` configuration, support registering dynamic variables.
149 | - Added `env` configuration, support loading and using environment variables in `loadgen.yml`.
150 | - Support using dynamic variables in the `headers` configuration.
151 |
152 | ### Improvements
153 |
154 | - `-l` option: precisely control the number of requests to send.
155 | - Added `runner.no_warm` to skip warm-up stage.
156 |
157 | ### Bug fix
158 |
--------------------------------------------------------------------------------
/docs/content.en/menu/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | headless: true
3 | ---
4 |
5 | - [**Documentation**]({{< relref "/docs/" >}})
6 |
--------------------------------------------------------------------------------
/docs/content.zh/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: INFINI Loadgen
3 | type: docs
4 | bookCollapseSection: true
5 | weight: 4
6 | ---
7 |
8 | # INFINI Loadgen
9 |
10 | ## 介绍
11 |
12 | INFINI Loadgen 是一款专为 Easysearch、Elasticsearch、OpenSearch 设计的轻量级性能测试工具,
13 |
14 | ## 特性
15 |
16 | - 性能强劲
17 | - 轻量级无依赖
18 | - 支持模板化参数随机
19 | - 支持高并发
20 | - 支持压测端均衡流量控制
21 | - 支持服务端返回值校验
22 |
23 | {{< button relref="../docs/getting-started/install/" >}}即刻开始{{< /button >}}
24 |
25 | ## 社区
26 |
27 | [加入我们的 Discord Server](https://discord.gg/4tKTMkkvVX)
28 |
29 | ## 谁在用?
30 |
31 | 如果您正在使用 Loadgen,并且您觉得它还不错的话,请[告诉我们](https://discord.gg/4tKTMkkvVX),感谢您的支持。
32 |
--------------------------------------------------------------------------------
/docs/content.zh/docs/getting-started/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 10
3 | title: 入门指南
4 | bookCollapseSection: true
5 | ---
6 |
--------------------------------------------------------------------------------
/docs/content.zh/docs/getting-started/benchmark.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 50
3 | title: "性能测试"
4 | ---
5 |
6 | # 性能测试
7 |
8 | INFINI Loadgen 是一款专为 Easysearch、Elasticsearch、OpenSearch 设计的轻量级性能测试工具。
9 |
10 | Loadgen 的特点:
11 |
12 | - 性能强劲
13 | - 轻量级无依赖
14 | - 支持模板化参数随机
15 | - 支持高并发
16 | - 支持压测端均衡流量控制
17 | - 支持服务端返回值校验
18 |
19 | > 下载地址:
20 |
21 | ## Loadgen
22 |
23 | Loadgen 使用非常简单,下载解压之后会得到三个文件,一个可执行程序、一个配置文件 `loadgen.yml` 以及用于运行测试的 `loadgen.dsl`,配置文件样例如下:
24 |
25 | ```yaml
26 | env:
27 | ES_USERNAME: elastic
28 | ES_PASSWORD: elastic
29 | ES_ENDPOINT: http://localhost:8000
30 | ```
31 |
32 | 测试文件样例如下:
33 |
34 | ```text
35 | # runner: {
36 | # // total_rounds: 1
37 | # no_warm: false,
38 | # // Whether to log all requests
39 | # log_requests: false,
40 | # // Whether to log all requests with the specified response status
41 | # log_status_codes: [0, 500],
42 | # assert_invalid: false,
43 | # assert_error: false,
44 | # },
45 | # variables: [
46 | # {
47 | # name: "ip",
48 | # type: "file",
49 | # path: "dict/ip.txt",
50 | # // Replace special characters in the value
51 | # replace: {
52 | # '"': '\\"',
53 | # '\\': '\\\\',
54 | # },
55 | # },
56 | # {
57 | # name: "id",
58 | # type: "sequence",
59 | # },
60 | # {
61 | # name: "id64",
62 | # type: "sequence64",
63 | # },
64 | # {
65 | # name: "uuid",
66 | # type: "uuid",
67 | # },
68 | # {
69 | # name: "now_local",
70 | # type: "now_local",
71 | # },
72 | # {
73 | # name: "now_utc",
74 | # type: "now_utc",
75 | # },
76 | # {
77 | # name: "now_utc_lite",
78 | # type: "now_utc_lite",
79 | # },
80 | # {
81 | # name: "now_unix",
82 | # type: "now_unix",
83 | # },
84 | # {
85 | # name: "now_with_format",
86 | # type: "now_with_format",
87 | # // https://programming.guide/go/format-parse-string-time-date-example.html
88 | # format: "2006-01-02T15:04:05-0700",
89 | # },
90 | # {
91 | # name: "suffix",
92 | # type: "range",
93 | # from: 10,
94 | # to: 1000,
95 | # },
96 | # {
97 | # name: "bool",
98 | # type: "range",
99 | # from: 0,
100 | # to: 1,
101 | # },
102 | # {
103 | # name: "list",
104 | # type: "list",
105 | # data: ["medcl", "abc", "efg", "xyz"],
106 | # },
107 | # {
108 | # name: "id_list",
109 | # type: "random_array",
110 | # variable_type: "number", // number/string
111 | # variable_key: "suffix", // variable key to get array items
112 | # square_bracket: false,
113 | # size: 10, // how many items for array
114 | # },
115 | # {
116 | # name: "str_list",
117 | # type: "random_array",
118 | # variable_type: "number", // number/string
119 | # variable_key: "suffix", // variable key to get array items
120 | # square_bracket: true,
121 | # size: 10, // how many items for array
122 | # replace: {
123 | # // Use ' instead of " for string quotes
124 | # '"': "'",
125 | # // Use {} instead of [] as array brackets
126 | # "[": "{",
127 | # "]": "}",
128 | # },
129 | # },
130 | # ],
131 |
132 | POST $[[env.ES_ENDPOINT]]/medcl/_search
133 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } }
134 | # request: {
135 | # runtime_variables: {batch_no: "uuid"},
136 | # runtime_body_line_variables: {routing_no: "uuid"},
137 | # basic_auth: {
138 | # username: "$[[env.ES_USERNAME]]",
139 | # password: "$[[env.ES_PASSWORD]]",
140 | # },
141 | # },
142 | ```
143 |
144 | ### 运行模式设置
145 |
146 | 默认配置下,Loadgen 会以性能测试模式运行,在指定时间(`-d`)内重复执行 `requests` 里的所有请求。如果只需要检查一次测试结果,可以通过 `runner.total_rounds` 来设置 `requests` 的执行次数。
147 |
148 | ### HTTP 响应头处理
149 |
150 | 默认配置下,Loadgen 会自动格式化 HTTP 的响应头(`user-agent: xxx` -> `User-Agent: xxx`),如果需要精确判断服务器返回的响应头,可以通过 `runner.disable_header_names_normalizing` 来禁用这个行为。
151 |
152 | ## 变量的使用
153 |
154 | 上面的配置中,`variables` 用来定义变量参数,根据 `name` 来设置变量标识,在构造请求的使用 `$[[变量名]]` 即可访问该变量的值,变量目前支持的类型有:
155 |
156 | | 类型 | 说明 | 变量参数 |
157 | | ----------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158 | | `file` | 文件型外部变量参数 | `path`: 数据文件路径
`data`: 数据列表,会被附加到`path`文件内容后读取 |
159 | | `list` | 自定义枚举变量参数 | `data`: 字符数组类型的枚举数据列表 |
160 | | `sequence` | 32 位自增数字类型的变量 | `from`: 初始值
`to`: 最大值 |
161 | | `sequence64` | 64 位自增数字类型的变量 | `from`: 初始值
`to`: 最大值 |
162 | | `range` | 数字范围类型的变量,支持参数 `from` 和 `to` 来限制范围 | `from`: 初始值
`to`: 最大值 |
163 | | `random_array` | 生成一个随机数组,数据元素来自`variable_key`指定的变量 | `variable_key`: 数据源变量
`size`: 输出数组的长度
`square_bracket`: `true/false`,输出值是否需要`[`和`]`
`string_bracket`: 字符串,输出元素前后会附加指定的字符串 |
164 | | `uuid` | UUID 字符类型的变量 | |
165 | | `now_local` | 当前时间、本地时区 | |
166 | | `now_utc` | 当前时间、UTC 时区。输出格式:`2006-01-02 15:04:05.999999999 -0700 MST` | |
167 | | `now_utc_lite` | 当前时间、UTC 时区。输出格式:`2006-01-02T15:04:05.000` | |
168 | | `now_unix` | 当前时间、Unix 时间戳 | |
169 | | `now_with_format` | 当前时间,支持自定义 `format` 参数来格式化时间字符串,如:`2006-01-02T15:04:05-0700` | `format`: 输出的时间格式 ([示例](https://www.geeksforgeeks.org/time-formatting-in-golang/)) |
170 |
171 | ### 变量使用示例
172 |
173 | `file` 类型变量参数加载自外部文本文件,每行一个变量参数,访问该变量时每次随机取其中一个,变量里面的定义格式举例如下:
174 |
175 | ```text
176 | # test/user.txt
177 | medcl
178 | elastic
179 | ```
180 |
181 | 附生成固定长度的随机字符串,如 1024 个字符每行:
182 |
183 | ```bash
184 | LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt
185 | ```
186 |
187 | ### 环境变量
188 |
189 | Loadgen 支持自动读取环境变量,环境变量可以在运行 Loadgen 时通过命令行传入,也可以在 `loadgen.dsl` 里指定默认的环境变量值,Loadgen 运行时会使用命令行传入的环境变量覆盖 `loadgen.dsl` 里的默认值。
190 |
191 | 配置的环境变量可以通过 `$[[env.环境变量]]` 来使用:
192 |
193 | ```text
194 | #// 配置环境变量默认值
195 | # env: {
196 | # ES_USERNAME: "elastic",
197 | # ES_PASSWORD: "elastic",
198 | # ES_ENDPOINT: "http://localhost:8000",
199 | # },
200 |
201 | #// 使用运行时变量
202 | GET $[[env.ES_ENDPOINT]]/medcl/_search
203 | {"query": {"match": {"name": "$[[user]]"}}}
204 | # request: {
205 | # // 使用运行时变量
206 | # basic_auth: {
207 | # username: "$[[env.ES_USERNAME]]",
208 | # password: "$[[env.ES_PASSWORD]]",
209 | # },
210 | # },
211 | ```
212 |
213 | ## 请求的定义
214 |
215 | 配置节点 `requests` 用来设置 Loadgen 将依次执行的请求,支持固定参数的请求,也可支持模板变量参数化构造请求,以下是一个普通的查询请求:
216 |
217 | ```text
218 | GET http://localhost:8000/medcl/_search?q=name:$[[user]]
219 | # request: {
220 | # username: elastic,
221 | # password: pass,
222 | # },
223 | ```
224 |
225 | 上面的查询对 `medcl` 索引进行了查询,并对 `name` 字段执行一个查询,每次请求的值来自随机变量 `user`。
226 |
227 | ### 模拟批量写入
228 |
229 | 使用 Loadgen 来模拟 bulk 批量写入也非常简单,在请求体里面配置一条索引操作,然后使用 `body_repeat_times` 参数来随机参数化复制若干条请求即可完成一批请求的准备,如下:
230 |
231 | ```text
232 | POST http://localhost:8000/_bulk
233 | {"index": {"_index": "medcl-y4", "_type": "doc", "_id": "$[[uuid]]"}}
234 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
235 | # request: {
236 | # basic_auth: {
237 | # username: "test",
238 | # password: "testtest",
239 | # },
240 | # body_repeat_times: 1000,
241 | # },
242 | ```
243 |
244 | ### 返回值判断
245 |
246 | 每个 `requests` 配置可以通过 `assert` 来设置是否需要检查返回值。`assert` 功能支持 INFINI Gateway 的大部分[条件判断功能](https://infinilabs.cn/docs/latest/gateway/references/flow/#条件类型)。
247 |
248 | > 请阅读[《借助 DSL 来简化 Loadgen 配置》](https://infinilabs.cn/blog/2023/simplify-loadgen-config-with-dsl/)来了解更多细节。
249 |
250 | ```text
251 | GET http://localhost:8000/medcl/_search?q=name:$[[user]]
252 | # request: {
253 | # basic_auth: {
254 | # username: "test",
255 | # password: "testtest",
256 | # },
257 | # },
258 | # assert: {
259 | # _ctx.response.status: 201,
260 | # },
261 | ```
262 |
263 | 请求返回值可以通过 `_ctx` 获取,`_ctx` 目前包含以下信息:
264 |
265 | | 参数 | 说明 |
266 | | ------------------------- | --------------------------------------------------------------------------------------- |
267 | | `_ctx.response.status` | HTTP 返回状态码 |
268 | | `_ctx.response.header` | HTTP 返回响应头 |
269 | | `_ctx.response.body` | HTTP 返回响应体 |
270 | | `_ctx.response.body_json` | 如果 HTTP 返回响应体是一个有效的 JSON 字符串,可以通过 `body_json` 来访问 JSON 内容字段 |
271 | | `_ctx.elapsed` | 当前请求发送到返回消耗的时间(毫秒) |
272 |
273 | 如果请求失败(请求地址无法访问等),Loadgen 无法获取 HTTP 请求返回值,Loadgen 会在输出日志里记录 `Number of Errors`。如果配置了 `runner.assert_error` 且存在请求失败的请求,Loadgen 会返回 `exit(2)` 错误码。
274 |
275 | 如果返回值不符合判断条件,Loadgen 会停止执行当前轮次后续请求,并在输出日志里记录 `Number of Invalid`。如果配置了 `runner.assert_invalid` 且存在判断失败的请求,Loadgen 会返回 `exit(1)` 错误码。
276 |
277 | ### 动态变量注册
278 |
279 | 每个 `requests` 配置可以通过 `register` 来动态添加运行时参数,一个常见的使用场景是根据前序请求的返回值来动态设置后序请求的参数。
280 |
281 | 这个示例调用 `$[[env.ES_ENDPOINT]]/test` 接口获取索引的 UUID,并注册到 `index_id` 变量。后续的请求定义可以通过 `$[[index_id]]` 来获取这个值。
282 |
283 | ```text
284 | GET $[[env.ES_ENDPOINT]]/test
285 | # register: [
286 | # {index_id: "_ctx.response.body_json.test.settings.index.uuid"},
287 | # ],
288 | # assert: (200, {}),
289 | ```
290 |
291 | ## 执行压测
292 |
293 | 执行 Loadgen 程序即可执行压测,如下:
294 |
295 | ```text
296 | $ loadgen -d 30 -c 100 -compress -run loadgen.dsl
297 | __ ___ _ ___ ___ __ __
298 | / / /___\/_\ / \/ _ \ /__\/\ \ \
299 | / / // ///_\\ / /\ / /_\//_\ / \/ /
300 | / /__/ \_// _ \/ /_// /_\\//__/ /\ /
301 | \____|___/\_/ \_/___,'\____/\__/\_\ \/
302 |
303 | [LOADGEN] A http load generator and testing suit.
304 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files
305 | [07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0
306 | [07-19 16:15:00] [INF] [loader.go:312] warmup started
307 | [07-19 16:15:00] [INF] [app.go:306] loadgen now started.
308 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search
309 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
310 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl
311 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
312 | [07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk
313 | [07-19 16:15:01] [INF] [loader.go:317] status: 200,,{"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}}]}
314 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished
315 |
316 | 5253 requests in 32.756483336s, 524.61KB sent, 2.49MB received
317 |
318 | [Loadgen Client Metrics]
319 | Requests/sec: 175.10
320 | Request Traffic/sec: 17.49KB
321 | Total Transfer/sec: 102.34KB
322 | Avg Req Time: 5.711022ms
323 | Fastest Request: 440.448µs
324 | Slowest Request: 3.624302658s
325 | Number of Errors: 0
326 | Number of Invalid: 0
327 | Status 200: 5253
328 |
329 | [Estimated Server Metrics]
330 | Requests/sec: 160.37
331 | Transfer/sec: 93.73KB
332 | Avg Req Time: 623.576686ms
333 | ```
334 |
335 | Loadgen 在正式压测之前会将所有的请求执行一次来进行预热,如果出现错误会提示是否继续,预热的请求结果也会输出到终端,执行完成之后会输出执行的摘要信息。可以通过设置 `runner.no_warm` 来跳过这个检查阶段。
336 |
337 | > 因为 Loadgen 最后的结果是所有请求全部执行完成之后的累计统计,可能存在不准的问题,建议通过打开 Kibana 的监控仪表板来实时查看 Elasticsearch 的各项运行指标。
338 |
339 | ### 命令行参数
340 |
341 | Loadgen 会循环执行配置文件里面定义的请求,默认 Loadgen 只会运行 `5s` 就自动退出了,如果希望延长运行时间或者加大并发可以通过启动的时候设置参数来控制,通过查看帮助命令如下:
342 |
343 | ```text
344 | $ loadgen -help
345 | Usage of loadgen:
346 | -c int
347 | Number of concurrent threads (default 1)
348 | -compress
349 | Compress requests with gzip
350 | -config string
351 | the location of config file (default "loadgen.yml")
352 | -cpu int
353 | the number of CPUs to use (default -1)
354 | -d int
355 | Duration of tests in seconds (default 5)
356 | -debug
357 | run in debug mode, loadgen will quit on panic immediately with full stack trace
358 | -dial-timeout int
359 | Connection dial timeout in seconds, default 3s (default 3)
360 | -gateway-log string
361 | Log level of Gateway (default "debug")
362 | -l int
363 | Limit total requests (default -1)
364 | -log string
365 | the log level, options: trace,debug,info,warn,error,off
366 | -mem int
367 | the max size of Memory to use, soft limit in megabyte (default -1)
368 | -plugin value
369 | load additional plugins
370 | -r int
371 | Max requests per second (fixed QPS) (default -1)
372 | -read-timeout int
373 | Connection read timeout in seconds, default 0s (use -timeout)
374 | -run string
375 | DSL config to run tests (default "loadgen.dsl")
376 | -service string
377 | service management, options: install,uninstall,start,stop
378 | -timeout int
379 | Request timeout in seconds, default 60s (default 60)
380 | -v version
381 | -write-timeout int
382 | Connection write timeout in seconds, default 0s (use -timeout)
383 | ```
384 |
385 | ### 限制客户端压力
386 |
387 | 使用 Loadgen 并设置命令行参数 `-r` 可以限制客户端发送的每秒请求数,从而评估固定压力下 Elasticsearch 的响应时间和负载情况,如下:
388 |
389 | ```bash
390 | loadgen -d 30 -c 100 -r 100
391 | ```
392 |
393 | > 注意,在大量并发下,此客户端吞吐限制可能不完全准确。
394 |
395 | ### 限制请求的总条数
396 |
397 | 通过设置参数 `-l` 可以控制客户端发送的请求总数,从而制造固定的文档,修改配置如下:
398 |
399 | ```text
400 | #// loadgen-gw.dsl
401 | POST http://localhost:8000/medcl-test/doc2/_bulk
402 | {"index": {"_index": "medcl-test", "_id": "$[[uuid]]"}}
403 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]"}
404 | # request: {
405 | # basic_auth: {
406 | # username: "test",
407 | # password: "testtest",
408 | # },
409 | # body_repeat_times: 1,
410 | # },
411 | ```
412 |
413 | 每次请求只有一个文档,然后执行 Loadgen
414 |
415 | ```bash
416 | loadgen -run loadgen-gw.dsl -d 600 -c 100 -l 50000
417 | ```
418 |
419 | 执行完成之后,Elasticsearch 的索引 `medcl-test` 将增加 `50000` 条记录。
420 |
421 | ### 使用自增 ID 来确保文档的顺序性
422 |
423 | 如果希望生成的文档编号自增有规律,方便进行对比,可以使用 `sequence` 类型的自增 ID 来作为主键,内容也不要用随机数,如下:
424 |
425 | ```text
426 | POST http://localhost:8000/medcl-test/doc2/_bulk
427 | {"index": {"_index": "medcl-test", "_id": "$[[id]]"}}
428 | {"id": "$[[id]]"}
429 | # request: {
430 | # basic_auth: {
431 | # username: "test",
432 | # password: "testtest",
433 | # },
434 | # body_repeat_times: 1,
435 | # },
436 | ```
437 |
438 | ### 上下文复用变量
439 |
440 | 在一个请求中,我们可能希望有相同的参数出现,比如 `routing` 参数用来控制分片的路由,同时我们又希望该参数也保存在文档的 JSON 里面,
441 | 可以使用 `runtime_variables` 来设置请求级别的变量,或者 `runtime_body_line_variables` 定义请求体级别的变量,如果请求体复制 N 份,每份的参数是不同的,举例如下:
442 |
443 | ```text
444 | # variables: [
445 | # {name: "id", type: "sequence"},
446 | # {name: "uuid", type: "uuid"},
447 | # {name: "now_local", type: "now_local"},
448 | # {name: "now_utc", type: "now_utc"},
449 | # {name: "now_unix", type: "now_unix"},
450 | # {name: "suffix", type: "range", from: 10, to 15},
451 | # ],
452 |
453 | POST http://192.168.3.188:9206/_bulk
454 | {"create": {"_index": "test-$[[suffix]]", "_type": "doc", "_id": "$[[uuid]]", "routing": "$[[routing_no]]"}}
455 | {"id": "$[[uuid]]", "routing_no": "$[[routing_no]]", "batch_number": "$[[batch_no]]", "random_no": "$[[suffix]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
456 | # request: {
457 | # runtime_variables: {
458 | # batch_no: "id",
459 | # },
460 | # runtime_body_line_variables: {
461 | # routing_no: "uuid",
462 | # },
463 | # basic_auth: {
464 | # username: "ingest",
465 | # password: "password",
466 | # },
467 | # body_repeat_times: 10,
468 | # },
469 | ```
470 |
471 | 我们定义了 `batch_no` 变量来代表一批文档里面的相同批次号,同时又定义了 `routing_no` 变量来代表每个文档级别的 routing 值。
472 |
473 | ### 自定义 Header
474 |
475 | ```text
476 | GET http://localhost:8000/test/_search
477 | # request: {
478 | # headers: [
479 | # {Agent: "Loadgen-1"},
480 | # ],
481 | # disable_header_names_normalizing: false,
482 | # },
483 | ```
484 |
485 | 默认配置下,Loadgen 会自动格式化配置里的 HTTP 的请求头(`user-agent: xxx` -> `User-Agent: xxx`),如果需要精确设置 HTTP 请求头,可以通过设置 `disable_header_names_normalizing: true` 来禁用这个行为。
486 |
487 | ## 运行测试套件
488 |
489 | Loadgen 支持批量运行测试用例,不需要重复编写测试用例,通过切换套件配置来快速测试不同的环境配置:
490 |
491 | ```yaml
492 | # loadgen.yml
493 | env:
494 | # Set up envrionments to run test suite
495 | LR_TEST_DIR: ./testing # The path to the test cases.
496 | # If you want to start gateway dynamically and automatically:
497 | LR_GATEWAY_CMD: ./bin/gateway # The path to the executable of INFINI Gateway
498 | LR_GATEWAY_HOST: 0.0.0.0:18000 # The binding host of the INFINI Gateway
499 | LR_GATEWAY_API_HOST: 0.0.0.0:19000 # The binding host of the INFINI Gateway API server
500 | # Set up other envrionments for the gateway and loadgen
501 | LR_ELASTICSEARCH_ENDPOINT: http://localhost:19201
502 | CUSTOM_ENV: myenv
503 | tests:
504 | # The relative path of test cases under `LR_TEST_DIR`
505 | #
506 | # - gateway.yml: (Optional) the configuration to start the INFINI Gateway dynamically.
507 | # - loadgen.dsl: the configuration to run the loadgen tool.
508 | #
509 | # The environments set in `env` section will be passed to the INFINI Gateway and loadgen.
510 | - path: cases/gateway/echo/echo_with_context
511 | ```
512 |
513 | ### 环境变量配置
514 |
515 | Loadgen 通过环境变量来动态配置 INFINI Gateway,环境变量在 `env` 里指定。以下环境变量是必选的:
516 |
517 | | 变量名 | 说明 |
518 | | ------------- | ---------------- |
519 | | `LR_TEST_DIR` | 测试用例所在目录 |
520 |
521 | 如果你需要 `loadgen` 根据配置动态启动 INFINI Gateway,需要设置以下环境变量:
522 |
523 | | 变量名 | 说明 |
524 | | --------------------- | ------------------------------------ |
525 | | `LR_GATEWAY_CMD` | INFINI Gateway 可执行文件的路径 |
526 | | `LR_GATEWAY_HOST` | INFINI Gateway 绑定的主机名:端口 |
527 | | `LR_GATEWAY_API_HOST` | INFINI Gateway API 绑定的主机名:端口 |
528 |
529 | ### 测试用例配置
530 |
531 | 测试用例在 `tests` 里配置,每个路径(`path`)指向一个测试用例的目录,每个测试用例需要配置一份 `gateway.yml`(可选)和 `loadgen.dsl`。配置文件可以使用 `env` 下配置的环境变量(`$[[env.ENV_KEY]]`)。
532 |
533 | `gateway.yml` 参考配置:
534 |
535 | ```yaml
536 | path.data: data
537 | path.logs: log
538 |
539 | entry:
540 | - name: my_es_entry
541 | enabled: true
542 | router: my_router
543 | max_concurrency: 200000
544 | network:
545 | binding: $[[env.LR_GATEWAY_HOST]]
546 |
547 | flow:
548 | - name: hello_world
549 | filter:
550 | - echo:
551 | message: "hello world"
552 | router:
553 | - name: my_router
554 | default_flow: hello_world
555 | ```
556 |
557 | `loadgen.dsl` 参考配置:
558 |
559 | ```
560 | # runner: {
561 | # total_rounds: 1,
562 | # no_warm: true,
563 | # log_requests: true,
564 | # assert_invalid: true,
565 | # assert_error: true,
566 | # },
567 |
568 | GET http://$[[env.LR_GATEWAY_HOST]]/
569 | # assert: {
570 | # _ctx.response: {
571 | # status: 200,
572 | # body: "hello world",
573 | # },
574 | # },
575 | ```
576 |
577 | ### 测试套件运行
578 |
579 | 配置好测试 `loadgen.yml` 后,可以通过以下命令运行 Loadgen:
580 |
581 | ```bash
582 | loadgen -config loadgen.yml
583 | ```
584 |
585 | Loadgen 会运行配置指定的所有测试用例,并输出测试结果:
586 |
587 | ```text
588 | $ loadgen -config loadgen.yml
589 | __ ___ _ ___ ___ __ __
590 | / / /___\/_\ / \/ _ \ /__\/\ \ \
591 | / / // ///_\\ / /\ / /_\//_\ / \/ /
592 | / /__/ \_// _ \/ /_// /_\\//__/ /\ /
593 | \____|___/\_/ \_/___,'\____/\__/\_\ \/
594 |
595 | [LOADGEN] A http load generator and testing suit.
596 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files
597 | [02-21 10:50:05] [INF] [app.go:192] initializing loadgen
598 | [02-21 10:50:05] [INF] [app.go:193] using config: /Users/kassian/Workspace/infini/src/infini.sh/testing/suites/dev.yml
599 | [02-21 10:50:05] [INF] [instance.go:78] workspace: /Users/kassian/Workspace/infini/src/infini.sh/testing/data/loadgen/nodes/cfpihf15k34iqhpd4d00
600 | [02-21 10:50:05] [INF] [app.go:399] loadgen is up and running now.
601 | [2023-02-21 10:50:05][TEST][SUCCESS] [setup/loadgen/cases/dummy] duration: 105(ms)
602 |
603 | 1 requests in 68.373875ms, 0.00bytes sent, 0.00bytes received
604 |
605 | [Loadgen Client Metrics]
606 | Requests/sec: 0.20
607 | Request Traffic/sec: 0.00bytes
608 | Total Transfer/sec: 0.00bytes
609 | Avg Req Time: 5s
610 | Fastest Request: 68.373875ms
611 | Slowest Request: 68.373875ms
612 | Number of Errors: 0
613 | Number of Invalid: 0
614 | Status 200: 1
615 |
616 | [Estimated Server Metrics]
617 | Requests/sec: 14.63
618 | Transfer/sec: 0.00bytes
619 | Avg Req Time: 68.373875ms
620 |
621 |
622 | [2023-02-21 10:50:06][TEST][FAILED] [setup/gateway/cases/echo/echo_with_context/] duration: 1274(ms)
623 | #0 request, GET http://$[[env.LR_GATEWAY_HOST]]/any/, assertion failed, skiping subsequent requests
624 | 1 requests in 1.255678s, 0.00bytes sent, 0.00bytes received
625 |
626 | [Loadgen Client Metrics]
627 | Requests/sec: 0.20
628 | Request Traffic/sec: 0.00bytes
629 | Total Transfer/sec: 0.00bytes
630 | Avg Req Time: 5s
631 | Fastest Request: 1.255678s
632 | Slowest Request: 1.255678s
633 | Number of Errors: 1
634 | Number of Invalid: 1
635 | Status 0: 1
636 |
637 | [Estimated Server Metrics]
638 | Requests/sec: 0.80
639 | Transfer/sec: 0.00bytes
640 | Avg Req Time: 1.255678s
641 |
642 | ```
643 |
--------------------------------------------------------------------------------
/docs/content.zh/docs/getting-started/install.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 10
3 | title: 下载安装
4 | asciinema: true
5 | ---
6 |
7 | # 安装 INFINI Loadgen
8 |
9 | INFINI Loadgen 支持主流的操作系统和平台,程序包很小,没有任何额外的外部依赖,安装起来应该是很快的 :)
10 |
11 | ## 下载安装
12 |
13 | **自动安装**
14 |
15 | ```bash
16 | curl -sSL http://get.infini.cloud | bash -s -- -p loadgen
17 | ```
18 |
19 | 通过以上脚本可自动下载相应平台的 loadgen 最新版本并解压到/opt/loadgen
20 |
21 | 脚本的可选参数如下:
22 |
23 | > _-v [版本号](默认采用最新版本号)_
24 | > _-d [安装目录](默认安装到/opt/loadgen)_
25 |
26 | ```bash
27 | ➜ /tmp mkdir loadgen
28 | ➜ /tmp curl -sSL http://get.infini.cloud | bash -s -- -p loadgen -d /tmp/loadgen
29 |
30 | @@@@@@@@@@@
31 | @@@@@@@@@@@@
32 | @@@@@@@@@@@@
33 | @@@@@@@@@&@@@
34 | #@@@@@@@@@@@@@
35 | @@@ @@@@@@@@@@@@@
36 | &@@@@@@@ &@@@@@@@@@@@@@
37 | @&@@@@@@@&@ @@@&@@@@@@@&@
38 | @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@
39 | @@@@@@@@@@@@@@@@@@& @@@@@@@@@@@@@
40 | %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
41 | @@@@@@@@@@@@&@@@@@@@@@@@@@@@
42 | @@ ,@@@@@@@@@@@@@@@@@@@@@@@&
43 | @@@@@. @@@@@&@@@@@@@@@@@@@@
44 | @@@@@@@@@@ @@@@@@@@@@@@@@@#
45 | @&@@@&@@@&@@@ &@&@@@&@@@&@
46 | @@@@@@@@@@@@@. @@@@@@@*
47 | @@@@@@@@@@@@@ %@@@
48 | @@@@@@@@@@@@@
49 | /@@@@@@@&@@@@@
50 | @@@@@@@@@@@@@
51 | @@@@@@@@@@@@@
52 | @@@@@@@@@@@@ Welcome to INFINI Labs!
53 |
54 |
55 | Now attempting the installation...
56 |
57 | Name: [loadgen], Version: [1.26.1-598], Path: [/tmp/loadgen]
58 | File: [https://release.infinilabs.com/loadgen/stable/loadgen-1.26.1-598-mac-arm64.zip]
59 | ##=O#- #
60 |
61 | Installation complete. [loadgen] is ready to use!
62 |
63 |
64 | ----------------------------------------------------------------
65 | cd /tmp/loadgen && ./loadgen-mac-arm64
66 | ----------------------------------------------------------------
67 |
68 |
69 | __ _ __ ____ __ _ __ __
70 | / // |/ // __// // |/ // /
71 | / // || // _/ / // || // /
72 | /_//_/|_//_/ /_//_/|_//_/
73 |
74 | ©INFINI.LTD, All Rights Reserved.
75 | ```
76 |
77 | **手动安装**
78 |
79 | 根据您所在的操作系统和平台选择下面相应的下载地址:
80 |
81 | [https://release.infinilabs.com/loadgen/](https://release.infinilabs.com/loadgen/)
82 |
--------------------------------------------------------------------------------
/docs/content.zh/docs/release-notes/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 80
3 | title: "版本历史"
4 | ---
5 |
6 | # 版本发布日志
7 |
8 | 这里是`loadgen`历史版本发布的相关说明。
9 |
10 | ## Latest (In development)
11 | ### ❌ Breaking changes
12 | ### 🚀 Features
13 | ### 🐛 Bug fix
14 | ### ✈️ Improvements
15 |
16 | ## 1.29.4 (2025-05-16)
17 | ### ❌ Breaking changes
18 | ### 🚀 Features
19 | ### 🐛 Bug fix
20 | ### ✈️ Improvements
21 | - 同步更新 [Framework v1.1.7](https://docs.infinilabs.com/framework/v1.1.7/docs/references/http_client/) 修复的一些已知问题
22 |
23 | ## 1.29.3 (2025-04-27)
24 | - 同步更新 [Framework v1.1.6](https://docs.infinilabs.com/framework/v1.1.6/docs/references/http_client/) 修复的一些已知问题
25 |
26 | ## 1.29.2 (2025-03-31)
27 | - 同步更新 [Framework v1.1.5](https://docs.infinilabs.com/framework/v1.1.5/docs/references/http_client/) 修复的一些已知问题
28 |
29 |
30 | ## 1.29.1 (2025-03-14)
31 | - 同步更新 [Framework v1.1.4](https://docs.infinilabs.com/framework/v1.1.4/docs/references/http_client/) 修复的一些已知问题
32 |
33 |
34 | ## 1.29.0 (2025-02-28)
35 |
36 | - 同步更新 Framework 修复的一些已知问题
37 |
38 | ## 1.28.2 (2025-02-15)
39 |
40 | - 同步更新 Framework 修复的一些已知问题
41 |
42 | ## 1.28.1 (2025-01-24)
43 |
44 | - 同步更新 Framework 修复的一些已知问题
45 |
46 | ## 1.28.0 (2025-01-11)
47 |
48 | - 同步更新 Framework 修复的一些已知问题
49 |
50 | ## 1.27.0 (2024-12-13)
51 |
52 | ### Improvements
53 |
54 | - 代码开源,统一采用 Github [仓库](https://github.com/infinilabs/loadgen) 进行开发
55 | - 保持与 Console 相同版本
56 | - 同步更新 Framework 修复的已知问题
57 |
58 | ### Bug fix
59 |
60 | - 修复 API 接口测试逻辑异常问题
61 |
62 | ## 1.26.1 (2024-08-13)
63 |
64 | ### Improvements
65 |
66 | - 与 INFINI Console 统一版本号
67 | - 同步更新 Framework 修复的已知问题
68 |
69 | ## 1.26.0 (2024-06-07)
70 |
71 | ### Improvements
72 |
73 | - 与 INFINI Console 统一版本号
74 | - 同步更新 Framework 修复的已知问题
75 |
76 | ## 1.25.0 (2024-04-30)
77 |
78 | ### Improvements
79 |
80 | - 与 INFINI Console 统一版本号
81 | - 同步更新 Framework 修复的已知问题
82 |
83 | ## 1.24.0 (2024-04-15)
84 |
85 | ### Improvements
86 |
87 | - 与 INFINI Console 统一版本号
88 | - 同步更新 Framework 修复的已知问题
89 |
90 | ## 1.22.0 (2024-01-26)
91 |
92 | ### Improvements
93 |
94 | - 与 INFINI Console 统一版本号
95 |
96 | ## 1.8.0 (2023-11-02)
97 |
98 | ### Breaking changes
99 |
100 | - 原 Loadrun 功能并入 Loadgen
101 | - 测试请求、断言等使用新的 Loadgen DSL 语法来配置
102 |
103 | ## 1.7.0 (2023-04-20)
104 |
105 | ### Breaking changes
106 |
107 | - `variables` 不再允许定义相同 `name` 的变量。
108 |
109 | ### Features
110 |
111 | - 增加 `log_status_code` 配置,支持打印特定状态码的请求日志。
112 |
113 | ## 1.6.0 (2023-04-06)
114 |
115 | ### Breaking ghanges
116 |
117 | - `file` 类型变量默认不再转义 `"` `\` 字符,使用 `replace` 功能手动设置变量转义。
118 |
119 | ### Features
120 |
121 | - 变量定义增加 `replace` 选项,可选跳过 `"` `\` 转义。
122 |
123 | ### Improvements
124 |
125 | - 优化内存占用
126 |
127 | ### Bug fix
128 |
129 | - 修复 YAML 字符串无法使用 `\n` 的问题
130 | - 修复无效的 assert 配置被忽略的问题
131 |
132 | ## 1.5.1
133 |
134 | ### Bug fix
135 |
136 | - 修复配置文件中无效的变量语法。
137 |
138 | ## 1.5.0
139 |
140 | ### Features
141 |
142 | - 配置文件添加 `assert` 配置,支持验证访问数据。
143 | - 配置文件添加 `register` 配置,支持注册动态变量。
144 | - 配置文件添加 `env` 配置,支持加载使用环境变量。
145 | - 支持在 `headers` 配置中使用动态变量。
146 |
147 | ### Improvements
148 |
149 | - 启动参数添加 `-l`: 控制发送请求的数量。
150 | - 配置文件添加 `runner.no_warm` 参数跳过预热阶段。
151 |
152 | ### Bug fix
153 |
--------------------------------------------------------------------------------
/docs/content.zh/docs/resources/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | weight: 100
3 | title: "其它资源"
4 | ---
5 |
6 | # 其它资源
7 |
8 | 这里是一些和 Loadgen 有关的外部有用资源。
9 |
10 | ## 文章
11 |
12 | - [Elasticsearch 性能测试工具 Loadgen 之 004——高级用法示例](https://mp.weixin.qq.com/s/LhNjooomUK16mTJPMF3kLw) | 2025-01-23
13 | - [Elasticsearch 性能测试工具 Loadgen 之 003——断言模式详解](https://mp.weixin.qq.com/s/-ILvyvAfw61mcQAZUzJRMQ) | 2025-01-23
14 | - [Elasticsearch 性能测试工具 Loadgen 之 002——命令行及参数详解](https://mp.weixin.qq.com/s/M4oE8F2ND63RlL7rAySliA) | 2025-01-23
15 | - [Elasticsearch 性能测试工具 Loadgen 之 001——部署及应用详解](https://mp.weixin.qq.com/s/q3XM6AeMQrTEcWgputABRw) | 2025-01-23
16 | - [Elasticsearch 性能测试工具全解析](https://mp.weixin.qq.com/s/8EpeGzmhwvOqwJv8oL1g-w) | 2025-01-23
17 | - [Loadgen 压测: Elasticsearch VS Easysearch 性能测试](https://infinilabs.cn/blog/2024/elasticsearch-vs-easysearch-stress-testing/) | 2024-12-19
18 | - [借助 DSL 来简化 Loadgen 配置](https://infinilabs.cn/blog/2023/simplify-loadgen-config-with-dsl/) | 2023-10-25
19 | - [如何使用 Loadgen 来简化 HTTP API 请求的集成测试](https://infinilabs.cn/blog/2023/integration-testing-with-loadgen/) | 2023-10-20
20 | - [更多文章 👉](https://infinilabs.cn/blog/)
21 |
22 | ## 视频
23 |
24 | - [Elasticsearch 压测用什么工具](https://www.bilibili.com/video/BV18Z421h7qi/) | 2024-03-15
25 | - [轻量、无依赖的 Elasticsearch 压测工具- INFINI Loadgen 使用教程](https://www.bilibili.com/video/BV16V4y1U7rZ) | 2023-06-06
26 |
--------------------------------------------------------------------------------
/docs/content.zh/menu/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | headless: true
3 | ---
4 |
5 | - [**文档**]({{< relref "/docs/" >}})
6 |
7 |
--------------------------------------------------------------------------------
/docs/static/img/logo-en.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/static/img/logo-zh.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/domain.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) INFINI Labs & INFINI LIMITED.
2 | //
3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0
4 | // and as commercial software.
5 | //
6 | // For commercial licensing, contact us at:
7 | // - Website: infinilabs.com
8 | // - Email: hello@infini.ltd
9 | //
10 | // Open Source licensed under AGPL V3:
11 | // This program is free software: you can redistribute it and/or modify
12 | // it under the terms of the GNU Affero General Public License as published by
13 | // the Free Software Foundation, either version 3 of the License, or
14 | // (at your option) any later version.
15 | //
16 | // This program is distributed in the hope that it will be useful,
17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | // GNU Affero General Public License for more details.
20 | //
21 | // You should have received a copy of the GNU Affero General Public License
22 | // along with this program. If not, see .
23 |
24 | /* Copyright © INFINI Ltd. All rights reserved.
25 | * web: https://infinilabs.com
26 | * mail: hello#infini.ltd */
27 |
28 | package main
29 |
30 | import (
31 | "bytes"
32 | "encoding/base64"
33 | "fmt"
34 | "infini.sh/framework/core/model"
35 | "math/rand"
36 | "strings"
37 | "time"
38 |
39 | "github.com/RoaringBitmap/roaring"
40 | log "github.com/cihub/seelog"
41 | "infini.sh/framework/lib/fasttemplate"
42 |
43 | "infini.sh/framework/core/conditions"
44 | "infini.sh/framework/core/errors"
45 | "infini.sh/framework/core/util"
46 | "infini.sh/framework/lib/fasthttp"
47 | )
48 |
49 | type valuesMap map[string]interface{}
50 |
51 | func (m valuesMap) GetValue(key string) (interface{}, error) {
52 | v, ok := m[key]
53 | if !ok {
54 | return nil, errors.New("key not found")
55 | }
56 | return v, nil
57 | }
58 |
59 | type Request struct {
60 | Method string `config:"method"`
61 | Url string `config:"url"`
62 | Body string `config:"body"`
63 | SimpleMode bool `config:"simple_mode"`
64 |
65 | RepeatBodyNTimes int `config:"body_repeat_times"`
66 | Headers []map[string]string `config:"headers"`
67 | BasicAuth *model.BasicAuth `config:"basic_auth"`
68 |
69 | // Disable fasthttp client's header names normalizing, preserve original header key, for requests
70 | DisableHeaderNamesNormalizing bool `config:"disable_header_names_normalizing"`
71 |
72 | RuntimeVariables map[string]string `config:"runtime_variables"`
73 | RuntimeBodyLineVariables map[string]string `config:"runtime_body_line_variables"`
74 |
75 | ExecuteRepeatTimes int `config:"execute_repeat_times"`
76 |
77 | urlHasTemplate bool
78 | headerHasTemplate bool
79 | bodyHasTemplate bool
80 |
81 | headerTemplates map[string]*fasttemplate.Template
82 | urlTemplate *fasttemplate.Template
83 | bodyTemplate *fasttemplate.Template
84 | }
85 |
86 | func (req *Request) HasVariable() bool {
87 | return req.urlHasTemplate || req.bodyHasTemplate || len(req.headerTemplates) > 0
88 | }
89 |
90 | type Variable struct {
91 | Type string `config:"type"`
92 | Name string `config:"name"`
93 | Path string `config:"path"`
94 | Data []string `config:"data"`
95 | Format string `config:"format"`
96 |
97 | //type: range
98 | From uint64 `config:"from"`
99 | To uint64 `config:"to"`
100 |
101 | Replace map[string]string `config:"replace"`
102 |
103 | Size int `config:"size"`
104 |
105 | //type: random_int_array
106 | RandomArrayKey string `config:"variable_key"`
107 | RandomArrayType string `config:"variable_type"`
108 | RandomSquareBracketChar bool `config:"square_bracket"`
109 | RandomStringBracketChar string `config:"string_bracket"`
110 |
111 | replacer *strings.Replacer
112 | }
113 |
114 | type AppConfig struct {
115 | Environments map[string]string `config:"env"`
116 | Tests []Test `config:"tests"`
117 | LoaderConfig
118 | }
119 |
120 | type LoaderConfig struct {
121 | // Access order: runtime_variables -> register -> variables
122 | Variable []Variable `config:"variables"`
123 | Requests []RequestItem `config:"requests"`
124 | RunnerConfig RunnerConfig `config:"runner"`
125 | }
126 |
127 | type RunnerConfig struct {
128 | // How many rounds of `requests` to run
129 | TotalRounds int `config:"total_rounds"`
130 | // Skip warming up round
131 | NoWarm bool `config:"no_warm"`
132 |
133 | ValidStatusCodesDuringWarmup []int `config:"valid_status_codes_during_warmup"`
134 |
135 | // Exit(1) if any assert failed
136 | AssertInvalid bool `config:"assert_invalid"`
137 |
138 | ContinueOnAssertInvalid bool `config:"continue_on_assert_invalid"`
139 | SkipInvalidAssert bool `config:"skip_invalid_assert"`
140 |
141 | // Exit(2) if any error occurred
142 | AssertError bool `config:"assert_error"`
143 | // Print the request sent to server
144 | LogRequests bool `config:"log_requests"`
145 |
146 | BenchmarkOnly bool `config:"benchmark_only"`
147 | DurationInUs bool `config:"duration_in_us"`
148 | NoStats bool `config:"no_stats"`
149 | NoSizeStats bool `config:"no_size_stats"`
150 | MetricSampleSize int `config:"metric_sample_size"`
151 |
152 | // Print the request sent to server if status code matched
153 | LogStatusCodes []int `config:"log_status_codes"`
154 | // Disable fasthttp client's header names normalizing, preserve original header key, for responses
155 | DisableHeaderNamesNormalizing bool `config:"disable_header_names_normalizing"`
156 |
157 | // Whether to reset the context, including variables, runtime KV pairs, etc.,
158 | // before this test run.
159 | ResetContext bool `config:"reset_context"`
160 |
161 | // Default endpoint if not specified in a request
162 | DefaultEndpoint string `config:"default_endpoint"`
163 | DefaultBasicAuth *model.BasicAuth `config:"default_basic_auth"`
164 | defaultEndpoint *fasthttp.URI
165 | }
166 |
167 | func (config *RunnerConfig) parseDefaultEndpoint() (*fasthttp.URI, error) {
168 | if config.defaultEndpoint != nil {
169 | return config.defaultEndpoint, nil
170 | }
171 |
172 | if config.DefaultEndpoint != "" {
173 | uri := &fasthttp.URI{}
174 | err := uri.Parse(nil, []byte(config.DefaultEndpoint))
175 | if err != nil {
176 | return nil, err
177 | }
178 | config.defaultEndpoint = uri
179 | return config.defaultEndpoint, err
180 | }
181 |
182 | return config.defaultEndpoint, errors.New("no valid default endpoint")
183 | }
184 |
185 | /*
186 | A test case is a standalone directory containing the following configs:
187 | - gateway.yml: The configuration to start the gateway server
188 | - loadgen.yml: The configuration to define the test cases
189 | */
190 | type Test struct {
191 | // The directory of the test configurations
192 | Path string `config:"path"`
193 | // Whether to use --compress for loadgen
194 | Compress bool `config:"compress"`
195 | }
196 |
197 | const (
198 | // Gateway-related configurations
199 | env_LR_GATEWAY_CMD = "LR_GATEWAY_CMD"
200 | env_LR_GATEWAY_HOST = "LR_GATEWAY_HOST"
201 | env_LR_GATEWAY_API_HOST = "LR_GATEWAY_API_HOST"
202 | )
203 |
204 | var (
205 | dict = map[string][]string{}
206 | variables map[string]Variable
207 | )
208 |
209 | func (config *AppConfig) Init() {
210 |
211 | }
212 |
213 | func (config *AppConfig) testEnv(envVars ...string) bool {
214 | for _, envVar := range envVars {
215 | if v, ok := config.Environments[envVar]; !ok || v == "" {
216 | return false
217 | }
218 | }
219 | return true
220 | }
221 |
222 | func (config *LoaderConfig) Init() error {
223 | // As we do not allow duplicate variable definitions, it is necessary to clear
224 | // any previously defined variables.
225 | variables = map[string]Variable{}
226 | if config.RunnerConfig.ResetContext {
227 | dict = map[string][]string{}
228 | util.ClearAllID()
229 | }
230 |
231 | for _, i := range config.Variable {
232 | i.Name = util.TrimSpaces(i.Name)
233 | _, ok := variables[i.Name]
234 | if ok {
235 | return fmt.Errorf("variable [%s] defined twice", i.Name)
236 | }
237 | var lines []string
238 | if len(i.Path) > 0 {
239 | lines = util.FileGetLines(i.Path)
240 | log.Debugf("path:%v, num of lines:%v", i.Path, len(lines))
241 | }
242 |
243 | if len(i.Data) > 0 {
244 | for _, v := range i.Data {
245 | v = util.TrimSpaces(v)
246 | if len(v) > 0 {
247 | lines = append(lines, v)
248 | }
249 | }
250 | }
251 |
252 | if len(i.Replace) > 0 {
253 | var replaces []string
254 |
255 | for k, v := range i.Replace {
256 | replaces = append(replaces, k, v)
257 | }
258 | i.replacer = strings.NewReplacer(replaces...)
259 | }
260 |
261 | dict[i.Name] = lines
262 |
263 | variables[i.Name] = i
264 | }
265 |
266 | var err error
267 | for _, v := range config.Requests {
268 | if v.Request == nil {
269 | continue
270 | }
271 | v.Request.headerTemplates = map[string]*fasttemplate.Template{}
272 | if util.ContainStr(v.Request.Url, "$[[") {
273 | v.Request.urlHasTemplate = true
274 | v.Request.urlTemplate, err = fasttemplate.NewTemplate(v.Request.Url, "$[[", "]]")
275 | if err != nil {
276 | return err
277 | }
278 | }
279 |
280 | if v.Request.RepeatBodyNTimes <= 0 && len(v.Request.Body) > 0 {
281 | v.Request.RepeatBodyNTimes = 1
282 | }
283 |
284 | if util.ContainStr(v.Request.Body, "$") {
285 | v.Request.bodyHasTemplate = true
286 | v.Request.bodyTemplate, err = fasttemplate.NewTemplate(v.Request.Body, "$[[", "]]")
287 | if err != nil {
288 | return err
289 | }
290 | }
291 |
292 | for _, headers := range v.Request.Headers {
293 | for headerK, headerV := range headers {
294 | if util.ContainStr(headerV, "$") {
295 | v.Request.headerHasTemplate = true
296 | v.Request.headerTemplates[headerK], err = fasttemplate.NewTemplate(headerV, "$[[", "]]")
297 | if err != nil {
298 | return err
299 | }
300 | }
301 | }
302 | }
303 |
304 | ////if there is no $[[ in the request, then we can assume that the request is in simple mode
305 | //if !v.Request.urlHasTemplate && !v.Request.bodyHasTemplate&& !v.Request.headerHasTemplate {
306 | // v.Request.SimpleMode = true
307 | //}
308 | }
309 |
310 | return nil
311 | }
312 |
313 | // "2021-08-23T11:13:36.274"
314 | const TsLayout = "2006-01-02T15:04:05.000"
315 |
316 | func GetVariable(runtimeKV util.MapStr, key string) string {
317 |
318 | if runtimeKV != nil {
319 | x, err := runtimeKV.GetValue(key)
320 | if err == nil {
321 | return util.ToString(x)
322 | }
323 | }
324 |
325 | return getVariable(key)
326 | }
327 |
328 | func getVariable(key string) string {
329 | x, ok := variables[key]
330 | if !ok {
331 | return "not_found"
332 | }
333 |
334 | rawValue := buildVariableValue(x)
335 | if x.replacer == nil {
336 | return rawValue
337 | }
338 | return x.replacer.Replace(rawValue)
339 | }
340 |
341 | func buildVariableValue(x Variable) string {
342 | switch x.Type {
343 | case "sequence":
344 | return util.ToString(util.GetAutoIncrement32ID(x.Name, uint32(x.From), uint32(x.To)).Increment())
345 | case "sequence64":
346 | return util.ToString(util.GetAutoIncrement64ID(x.Name, x.From, x.To).Increment64())
347 | case "uuid":
348 | return util.GetUUID()
349 | case "now_local":
350 | return time.Now().Local().String()
351 | case "now_with_format":
352 | if x.Format == "" {
353 | panic(errors.Errorf("date format is not set, [%v]", x))
354 | }
355 | return time.Now().Format(x.Format)
356 | case "now_utc":
357 | return time.Now().UTC().String()
358 | case "now_utc_lite":
359 | return time.Now().UTC().Format(TsLayout)
360 | case "now_unix":
361 | return util.IntToString(int(time.Now().Local().Unix()))
362 | case "now_unix_in_ms":
363 | return util.IntToString(int(time.Now().Local().UnixMilli()))
364 | case "now_unix_in_micro":
365 | return util.IntToString(int(time.Now().Local().UnixMicro()))
366 | case "now_unix_in_nano":
367 | return util.IntToString(int(time.Now().Local().UnixNano()))
368 | case "int_array_bitmap":
369 | rb3 := roaring.New()
370 | if x.Size > 0 {
371 | for y := 0; y < x.Size; y++ {
372 | v := rand.Intn(int(x.To-x.From+1)) + int(x.From)
373 | rb3.Add(uint32(v))
374 | }
375 | }
376 | buf := new(bytes.Buffer)
377 | rb3.WriteTo(buf)
378 | return base64.StdEncoding.EncodeToString(buf.Bytes())
379 | case "range":
380 | return util.IntToString(rand.Intn(int(x.To-x.From+1)) + int(x.From))
381 | case "random_array":
382 | str := bytes.Buffer{}
383 |
384 | if x.RandomSquareBracketChar {
385 | str.WriteString("[")
386 | }
387 |
388 | if x.RandomArrayKey != "" {
389 | if x.Size > 0 {
390 | for y := 0; y < x.Size; y++ {
391 | if x.RandomSquareBracketChar && str.Len() > 1 || (!x.RandomSquareBracketChar && str.Len() > 0) {
392 | str.WriteString(",")
393 | }
394 |
395 | v := getVariable(x.RandomArrayKey)
396 |
397 | //left "
398 | if x.RandomArrayType == "string" {
399 | if x.RandomStringBracketChar != "" {
400 | str.WriteString(x.RandomStringBracketChar)
401 | } else {
402 | str.WriteString("\"")
403 | }
404 | }
405 |
406 | str.WriteString(v)
407 |
408 | // right "
409 | if x.RandomArrayType == "string" {
410 | if x.RandomStringBracketChar != "" {
411 | str.WriteString(x.RandomStringBracketChar)
412 | } else {
413 | str.WriteString("\"")
414 | }
415 | }
416 | }
417 | }
418 | }
419 |
420 | if x.RandomSquareBracketChar {
421 | str.WriteString("]")
422 | }
423 | return str.String()
424 | case "file", "list":
425 | d, ok := dict[x.Name]
426 | if ok {
427 |
428 | if len(d) == 1 {
429 | return d[0]
430 | }
431 | offset := rand.Intn(len(d))
432 | return d[offset]
433 | }
434 | }
435 | return "invalid_variable_type"
436 | }
437 |
438 | type RequestItem struct {
439 | Request *Request `config:"request"`
440 | // TODO: mask invalid gateway fields
441 | Assert *conditions.Config `config:"assert"`
442 | AssertDsl string `config:"assert_dsl"`
443 | Sleep *SleepAction `config:"sleep"`
444 | // Populate global context with `_ctx` values
445 | Register []map[string]string `config:"register"`
446 | }
447 |
448 | type SleepAction struct {
449 | SleepInMilliSeconds int64 `config:"sleep_in_milli_seconds"`
450 | }
451 |
452 | type RequestResult struct {
453 | RequestCount int
454 | RequestSize int
455 | ResponseSize int
456 | Status int
457 | Error bool
458 | Invalid bool
459 | Duration time.Duration
460 | }
461 |
462 | func (result *RequestResult) Reset() {
463 | result.Error = false
464 | result.Status = 0
465 | result.RequestCount = 0
466 | result.RequestSize = 0
467 | result.ResponseSize = 0
468 | result.Invalid = false
469 | result.Duration = 0
470 | }
471 |
--------------------------------------------------------------------------------
/domain_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) INFINI Labs & INFINI LIMITED.
2 | //
3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0
4 | // and as commercial software.
5 | //
6 | // For commercial licensing, contact us at:
7 | // - Website: infinilabs.com
8 | // - Email: hello@infini.ltd
9 | //
10 | // Open Source licensed under AGPL V3:
11 | // This program is free software: you can redistribute it and/or modify
12 | // it under the terms of the GNU Affero General Public License as published by
13 | // the Free Software Foundation, either version 3 of the License, or
14 | // (at your option) any later version.
15 | //
16 | // This program is distributed in the hope that it will be useful,
17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | // GNU Affero General Public License for more details.
20 | //
21 | // You should have received a copy of the GNU Affero General Public License
22 | // along with this program. If not, see .
23 |
24 | package main
25 |
26 | import (
27 | "fmt"
28 | "infini.sh/framework/lib/fasttemplate"
29 | "io"
30 | "log"
31 | "math/rand"
32 | "testing"
33 | )
34 |
35 | func TestVariable(t *testing.T) {
36 | array := []string{"1", "2", "3"}
37 |
38 | for i := 0; i < 100; i++ {
39 | offset := rand.Intn(len(array))
40 | fmt.Println(offset)
41 | }
42 | }
43 |
44 | func TestTemplate(t1 *testing.T) {
45 | template := "Hello, $[[user]]! You won $[[prize]]!!! $[[foobar]]"
46 | t, err := fasttemplate.NewTemplate(template, "$[[", "]]")
47 | if err != nil {
48 | log.Fatalf("unexpected error when parsing template: %s", err)
49 | }
50 | s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
51 | switch tag {
52 | case "user":
53 | return w.Write([]byte("John"))
54 | case "prize":
55 | return w.Write([]byte("$100500"))
56 | default:
57 |
58 | return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag)))
59 | }
60 | })
61 | fmt.Printf("%s", s)
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/loader.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) INFINI Labs & INFINI LIMITED.
2 | //
3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0
4 | // and as commercial software.
5 | //
6 | // For commercial licensing, contact us at:
7 | // - Website: infinilabs.com
8 | // - Email: hello@infini.ltd
9 | //
10 | // Open Source licensed under AGPL V3:
11 | // This program is free software: you can redistribute it and/or modify
12 | // it under the terms of the GNU Affero General Public License as published by
13 | // the Free Software Foundation, either version 3 of the License, or
14 | // (at your option) any later version.
15 | //
16 | // This program is distributed in the hope that it will be useful,
17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | // GNU Affero General Public License for more details.
20 | //
21 | // You should have received a copy of the GNU Affero General Public License
22 | // along with this program. If not, see .
23 |
24 | /* Copyright © INFINI Ltd. All rights reserved.
25 | * web: https://infinilabs.com
26 | * mail: hello#infini.ltd */
27 |
28 | package main
29 |
30 | import (
31 | "bufio"
32 | "compress/gzip"
33 | "crypto/tls"
34 | "encoding/json"
35 | "io"
36 | "net"
37 | "os"
38 | "strconv"
39 | "sync"
40 | "sync/atomic"
41 | "time"
42 |
43 | "github.com/jamiealquiza/tachymeter"
44 |
45 | log "github.com/cihub/seelog"
46 | "infini.sh/framework/core/conditions"
47 | "infini.sh/framework/core/global"
48 | "infini.sh/framework/core/rate"
49 | "infini.sh/framework/core/stats"
50 | "infini.sh/framework/core/util"
51 | "infini.sh/framework/lib/fasthttp"
52 | )
53 |
54 | type LoadGenerator struct {
55 | duration int //seconds
56 | goroutines int
57 | statsAggregator chan *LoadStats
58 | interrupted int32
59 | }
60 |
61 | type LoadStats struct {
62 | TotReqSize int64
63 | TotRespSize int64
64 | TotDuration time.Duration
65 | MinRequestTime time.Duration
66 | MaxRequestTime time.Duration
67 | NumRequests int
68 | NumErrs int
69 | NumAssertInvalid int
70 | NumAssertSkipped int
71 | StatusCode map[int]int
72 | }
73 |
74 | var (
75 | httpClient fasthttp.Client
76 | resultPool = &sync.Pool{
77 | New: func() interface{} {
78 | return &RequestResult{}
79 | },
80 | }
81 | )
82 |
83 | func NewLoadGenerator(duration int, goroutines int, statsAggregator chan *LoadStats, disableHeaderNamesNormalizing bool) (rt *LoadGenerator) {
84 | if readTimeout <= 0 {
85 | readTimeout = timeout
86 | }
87 | if writeTimeout <= 0 {
88 | writeTimeout = timeout
89 | }
90 | if dialTimeout <= 0 {
91 | dialTimeout = timeout
92 | }
93 |
94 | httpClient = fasthttp.Client{
95 | MaxConnsPerHost: goroutines,
96 | //MaxConns: goroutines,
97 | NoDefaultUserAgentHeader: false,
98 | DisableHeaderNamesNormalizing: disableHeaderNamesNormalizing,
99 | Name: global.Env().GetAppLowercaseName() + "/" + global.Env().GetVersion() + "/" + global.Env().GetBuildNumber(),
100 | TLSConfig: &tls.Config{InsecureSkipVerify: true},
101 | }
102 |
103 | if readTimeout > 0 {
104 | httpClient.ReadTimeout = time.Second * time.Duration(readTimeout)
105 | }
106 | if writeTimeout > 0 {
107 | httpClient.WriteTimeout = time.Second * time.Duration(writeTimeout)
108 | }
109 | if dialTimeout > 0 {
110 | httpClient.Dial = func(addr string) (net.Conn, error) {
111 | return fasthttp.DialTimeout(addr, time.Duration(dialTimeout)*time.Second)
112 | }
113 | }
114 |
115 | rt = &LoadGenerator{duration, goroutines, statsAggregator, 0}
116 | return
117 | }
118 |
119 | var defaultHTTPPool = fasthttp.NewRequestResponsePool("default_http")
120 |
121 | func doRequest(config *LoaderConfig, globalCtx util.MapStr, req *fasthttp.Request, resp *fasthttp.Response, item *RequestItem, loadStats *LoadStats, timer *tachymeter.Tachymeter) (continueNext bool, err error) {
122 |
123 | if item.Request != nil {
124 |
125 | if item.Request.ExecuteRepeatTimes < 1 {
126 | item.Request.ExecuteRepeatTimes = 1
127 | }
128 |
129 | for i := 0; i < item.Request.ExecuteRepeatTimes; i++ {
130 | resp.Reset()
131 | resp.ResetBody()
132 | start := time.Now()
133 |
134 | if global.Env().IsDebug {
135 | log.Info(req.String())
136 | }
137 |
138 | if timeout > 0 {
139 | err = httpClient.DoTimeout(req, resp, time.Duration(timeout)*time.Second)
140 | } else {
141 | err = httpClient.Do(req, resp)
142 | }
143 |
144 | if global.Env().IsDebug {
145 | log.Info(resp.String())
146 | }
147 |
148 | duration := time.Since(start)
149 | statsCode := resp.StatusCode()
150 |
151 | if !config.RunnerConfig.BenchmarkOnly && timer != nil {
152 | timer.AddTime(duration)
153 | }
154 |
155 | if !config.RunnerConfig.NoStats {
156 | if config.RunnerConfig.DurationInUs {
157 | stats.Timing("request", "duration_in_us", duration.Microseconds())
158 | } else {
159 | stats.Timing("request", "duration", duration.Milliseconds())
160 | }
161 |
162 | stats.Increment("request", "total")
163 | stats.Increment("request", strconv.Itoa(resp.StatusCode()))
164 |
165 | if err != nil {
166 | loadStats.NumErrs++
167 | loadStats.NumAssertInvalid++
168 | }
169 |
170 | if !config.RunnerConfig.NoSizeStats {
171 | loadStats.TotReqSize += int64(req.GetRequestLength()) //TODO inaccurate
172 | loadStats.TotRespSize += int64(resp.GetResponseLength()) //TODO inaccurate
173 | }
174 |
175 | loadStats.NumRequests++
176 | loadStats.TotDuration += duration
177 | loadStats.MaxRequestTime = util.MaxDuration(duration, loadStats.MaxRequestTime)
178 | loadStats.MinRequestTime = util.MinDuration(duration, loadStats.MinRequestTime)
179 | loadStats.StatusCode[statsCode] += 1
180 | }
181 |
182 | if config.RunnerConfig.BenchmarkOnly {
183 | return true, err
184 | }
185 |
186 | if item.Register != nil || item.Assert != nil || config.RunnerConfig.LogRequests {
187 | //only use last request and response
188 | reqBody := req.GetRawBody()
189 | respBody := resp.GetRawBody()
190 | if global.Env().IsDebug {
191 | log.Debugf("final response code: %v, body: %s", resp.StatusCode(), string(respBody))
192 | }
193 |
194 | if item.Request != nil && config.RunnerConfig.LogRequests || util.ContainsInAnyInt32Array(statsCode, config.RunnerConfig.LogStatusCodes) {
195 | log.Infof("[%v] %v, %v - %v", item.Request.Method, item.Request.Url, item.Request.Headers, util.SubString(string(reqBody), 0, 512))
196 | log.Infof("status: %v, error: %v, response: %v", statsCode, err, util.SubString(string(respBody), 0, 512))
197 | }
198 |
199 | if err != nil {
200 | continue
201 | }
202 |
203 | event := buildCtx(resp, respBody, duration)
204 | if item.Register != nil {
205 | log.Debugf("registering %+v, event: %+v", item.Register, event)
206 | for _, item := range item.Register {
207 | for dest, src := range item {
208 | val, valErr := event.GetValue(src)
209 | if valErr != nil {
210 | log.Errorf("failed to get value with key: %s", src)
211 | }
212 | log.Debugf("put globalCtx %+v, %+v", dest, val)
213 | globalCtx.Put(dest, val)
214 | }
215 | }
216 | }
217 |
218 | if item.Assert != nil {
219 | // Dump globalCtx into assert event
220 | event.Update(globalCtx)
221 | if len(respBody) < 4096 {
222 | log.Debugf("assert _ctx: %+v", event)
223 | }
224 | condition, buildErr := conditions.NewCondition(item.Assert)
225 | if buildErr != nil {
226 | if config.RunnerConfig.SkipInvalidAssert {
227 | loadStats.NumAssertSkipped++
228 | continue
229 | }
230 | log.Errorf("failed to build conditions while assert existed, error: %+v", buildErr)
231 | loadStats.NumAssertInvalid++
232 | return
233 | }
234 | if !condition.Check(event) {
235 | loadStats.NumAssertInvalid++
236 | if item.Request != nil {
237 | log.Errorf("%s %s, assertion failed, skipping subsequent requests", item.Request.Method, item.Request.Url)
238 | }
239 |
240 | if !config.RunnerConfig.ContinueOnAssertInvalid {
241 | log.Info("assertion failed, skipping subsequent requests,", util.MustToJSON(item.Assert), ", event:", util.MustToJSON(event))
242 | return false, err
243 | }
244 | }
245 | }
246 | }
247 |
248 | if item.Sleep != nil {
249 | time.Sleep(time.Duration(item.Sleep.SleepInMilliSeconds) * time.Millisecond)
250 | }
251 | }
252 | }
253 |
254 | return true, nil
255 | }
256 |
257 | func buildCtx(resp *fasthttp.Response, respBody []byte, duration time.Duration) util.MapStr {
258 | var statusCode int
259 | header := map[string]interface{}{}
260 | if resp != nil {
261 | resp.Header.VisitAll(func(k, v []byte) {
262 | header[string(k)] = string(v)
263 | })
264 | statusCode = resp.StatusCode()
265 | }
266 | event := util.MapStr{
267 | "_ctx": map[string]interface{}{
268 | "response": map[string]interface{}{
269 | "status": statusCode,
270 | "header": header,
271 | "body": string(respBody),
272 | "body_length": len(respBody),
273 | },
274 | "elapsed": int64(duration / time.Millisecond),
275 | },
276 | }
277 | bodyJson := map[string]interface{}{}
278 | jsonErr := json.Unmarshal(respBody, &bodyJson)
279 | if jsonErr == nil {
280 | event.Put("_ctx.response.body_json", bodyJson)
281 | }
282 | return event
283 | }
284 |
285 | func (cfg *LoadGenerator) Run(config *LoaderConfig, countLimit int, timer *tachymeter.Tachymeter) {
286 | loadStats := &LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}}
287 | start := time.Now()
288 |
289 | limiter := rate.GetRateLimiter("loadgen", "requests", int(rateLimit), 1, time.Second*1)
290 |
291 | // TODO: support concurrent access
292 | globalCtx := util.MapStr{}
293 | req := defaultHTTPPool.AcquireRequest()
294 | defer defaultHTTPPool.ReleaseRequest(req)
295 | resp := defaultHTTPPool.AcquireResponse()
296 | defer defaultHTTPPool.ReleaseResponse(resp)
297 |
298 | totalRequests := 0
299 | totalRounds := 0
300 |
301 | for time.Since(start).Seconds() <= float64(cfg.duration) && atomic.LoadInt32(&cfg.interrupted) == 0 {
302 | if config.RunnerConfig.TotalRounds > 0 && totalRounds >= config.RunnerConfig.TotalRounds {
303 | goto END
304 | }
305 | totalRounds += 1
306 |
307 | for i, item := range config.Requests {
308 |
309 | if !config.RunnerConfig.BenchmarkOnly {
310 | if countLimit > 0 && totalRequests >= countLimit {
311 | goto END
312 | }
313 | totalRequests += 1
314 |
315 | if rateLimit > 0 {
316 | RetryRateLimit:
317 | if !limiter.Allow() {
318 | time.Sleep(10 * time.Millisecond)
319 | goto RetryRateLimit
320 | }
321 | }
322 | }
323 |
324 | item.prepareRequest(config, globalCtx, req)
325 |
326 | next, err := doRequest(config, globalCtx, req, resp, &item, loadStats, timer)
327 | if global.Env().IsDebug {
328 | log.Debugf("#%v,contine: %v, err:%v", i, next, err)
329 | }
330 | if !next {
331 | break
332 | }
333 |
334 | }
335 | }
336 |
337 | END:
338 | cfg.statsAggregator <- loadStats
339 | }
340 |
341 | func (v *RequestItem) prepareRequest(config *LoaderConfig, globalCtx util.MapStr, req *fasthttp.Request) {
342 | //cleanup
343 | req.Reset()
344 | req.ResetBody()
345 |
346 | if v.Request.BasicAuth != nil && v.Request.BasicAuth.Username != "" {
347 | req.SetBasicAuth(v.Request.BasicAuth.Username, v.Request.BasicAuth.Password.Get())
348 | } else {
349 | //try use default auth
350 | if config.RunnerConfig.DefaultBasicAuth != nil && config.RunnerConfig.DefaultBasicAuth.Username != "" {
351 | req.SetBasicAuth(config.RunnerConfig.DefaultBasicAuth.Username, config.RunnerConfig.DefaultBasicAuth.Password.Get())
352 | }
353 | }
354 |
355 | if v.Request.SimpleMode {
356 | req.Header.SetMethod(v.Request.Method)
357 | req.SetRequestURI(v.Request.Url)
358 | return
359 | }
360 |
361 | bodyBuffer := req.BodyBuffer()
362 | var bodyWriter io.Writer = bodyBuffer
363 | if v.Request.DisableHeaderNamesNormalizing {
364 | req.Header.DisableNormalizing()
365 | }
366 |
367 | if compress {
368 | var err error
369 | gzipWriter, err := gzip.NewWriterLevel(bodyBuffer, fasthttp.CompressBestCompression)
370 | if err != nil {
371 | panic("failed to create gzip writer")
372 | }
373 | defer gzipWriter.Close()
374 | bodyWriter = gzipWriter
375 | }
376 |
377 | //init runtime variables
378 | // TODO: optimize overall variable populate flow
379 | runtimeVariables := util.MapStr{}
380 | runtimeVariables.Update(globalCtx)
381 |
382 | if v.Request.HasVariable() {
383 | if len(v.Request.RuntimeVariables) > 0 {
384 | for k, v := range v.Request.RuntimeVariables {
385 | runtimeVariables.Put(k, GetVariable(runtimeVariables, v))
386 | }
387 | }
388 |
389 | }
390 |
391 | //prepare url
392 | url := v.Request.Url
393 | if v.Request.urlHasTemplate {
394 | url = v.Request.urlTemplate.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
395 | variable := GetVariable(runtimeVariables, tag)
396 | return w.Write(util.UnsafeStringToBytes(variable))
397 | })
398 | }
399 |
400 | //set default endpoint
401 | parsedUrl := fasthttp.URI{}
402 | err := parsedUrl.Parse(nil, []byte(url))
403 | if err != nil {
404 | panic(err)
405 | }
406 | if parsedUrl.Host() == nil || len(parsedUrl.Host()) == 0 {
407 | path, err := config.RunnerConfig.parseDefaultEndpoint()
408 | //log.Infof("default endpoint: %v, %v",path,err)
409 | if err == nil {
410 | parsedUrl.SetSchemeBytes(path.Scheme())
411 | parsedUrl.SetHostBytes(path.Host())
412 | }
413 | }
414 | url = parsedUrl.String()
415 |
416 | req.SetRequestURI(url)
417 |
418 | if global.Env().IsDebug {
419 | log.Debugf("final request url: %v %s", v.Request.Method, url)
420 | }
421 |
422 | //prepare method
423 | req.Header.SetMethod(v.Request.Method)
424 |
425 | if len(v.Request.Headers) > 0 {
426 | for _, headers := range v.Request.Headers {
427 | for headerK, headerV := range headers {
428 | if tmpl, ok := v.Request.headerTemplates[headerK]; ok {
429 | headerV = tmpl.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
430 | variable := GetVariable(runtimeVariables, tag)
431 | return w.Write(util.UnsafeStringToBytes(variable))
432 | })
433 | }
434 | req.Header.Set(headerK, headerV)
435 | }
436 | }
437 | }
438 | if global.Env().IsDebug {
439 | log.Debugf("final request headers: %s", req.Header.String())
440 | }
441 |
442 | //req.Header.Set("User-Agent", UserAgent)
443 |
444 | //prepare request body
445 | for i := 0; i < v.Request.RepeatBodyNTimes; i++ {
446 | body := v.Request.Body
447 | if len(body) > 0 {
448 | if v.Request.bodyHasTemplate {
449 | if len(v.Request.RuntimeBodyLineVariables) > 0 {
450 | for k, v := range v.Request.RuntimeBodyLineVariables {
451 | runtimeVariables[k] = GetVariable(runtimeVariables, v)
452 | }
453 | }
454 |
455 | v.Request.bodyTemplate.ExecuteFuncStringExtend(bodyWriter, func(w io.Writer, tag string) (int, error) {
456 | variable := GetVariable(runtimeVariables, tag)
457 | return w.Write([]byte(variable))
458 | })
459 | } else {
460 | bodyWriter.Write(util.UnsafeStringToBytes(body))
461 | }
462 | }
463 | }
464 |
465 | req.Header.Set("X-PayLoad-Size", util.ToString(bodyBuffer.Len()))
466 |
467 | if bodyBuffer.Len() > 0 && compress {
468 | req.Header.Set(fasthttp.HeaderAcceptEncoding, "gzip")
469 | req.Header.Set(fasthttp.HeaderContentEncoding, "gzip")
470 | req.Header.Set("X-PayLoad-Compressed", util.ToString(true))
471 | }
472 | }
473 |
474 | func (cfg *LoadGenerator) Warmup(config *LoaderConfig) int {
475 | log.Info("warmup started")
476 | loadStats := &LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}}
477 | req := defaultHTTPPool.AcquireRequest()
478 | defer defaultHTTPPool.ReleaseRequest(req)
479 | resp := defaultHTTPPool.AcquireResponse()
480 | defer defaultHTTPPool.ReleaseResponse(resp)
481 | globalCtx := util.MapStr{}
482 | for _, v := range config.Requests {
483 | v.prepareRequest(config, globalCtx, req)
484 |
485 | if !req.Validate() {
486 | log.Errorf("invalid request: %v", req.String())
487 | panic("invalid request")
488 | }
489 |
490 | next, err := doRequest(config, globalCtx, req, resp, &v, loadStats, nil)
491 | for k, _ := range loadStats.StatusCode {
492 | if len(config.RunnerConfig.ValidStatusCodesDuringWarmup) > 0 {
493 | if util.ContainsInAnyInt32Array(k, config.RunnerConfig.ValidStatusCodesDuringWarmup) {
494 | continue
495 | }
496 | }
497 | if k >= 400 || k == 0 || err != nil {
498 | log.Infof("requests seems failed to process, err: %v, are you sure to continue?\nPress `Ctrl+C` to skip or press 'Enter' to continue...", err)
499 | reader := bufio.NewReader(os.Stdin)
500 | reader.ReadString('\n')
501 | break
502 | }
503 | }
504 | if !next {
505 | break
506 | }
507 | }
508 |
509 | log.Info("warmup finished")
510 | return loadStats.NumRequests
511 | }
512 |
513 | func (cfg *LoadGenerator) Stop() {
514 | atomic.StoreInt32(&cfg.interrupted, 1)
515 | }
516 |
--------------------------------------------------------------------------------
/loadgen.dsl:
--------------------------------------------------------------------------------
1 | # // How to use DSL to simplify requests, requests defined in loadgen.yml will be skipped in this mode
2 | # // $ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run loadgen.dsl
3 |
4 | # runner: {
5 | # total_rounds: 1,
6 | # no_warm: true,
7 | # // Whether to log all requests
8 | # log_requests: false,
9 | # // Whether to log all requests with the specified response status
10 | # log_status_codes: [0, 500],
11 | # assert_invalid: false,
12 | # assert_error: false,
13 | # // Whether to reset the context, including variables, runtime KV pairs,
14 | # // etc., before this test run.
15 | # reset_context: false,
16 | # default_endpoint: "$[[env.ES_ENDPOINT]]",
17 | # default_basic_auth: {
18 | # username: "$[[env.ES_USERNAME]]",
19 | # password: "$[[env.ES_PASSWORD]]",
20 | # }
21 | # },
22 | # variables: [
23 | # {
24 | # name: "ip",
25 | # type: "file",
26 | # path: "dict/ip.txt",
27 | # // Replace special characters in the value
28 | # replace: {
29 | # '"': '\\"',
30 | # '\\': '\\\\',
31 | # },
32 | # },
33 | # {
34 | # name: "id",
35 | # type: "sequence",
36 | # },
37 | # {
38 | # name: "id64",
39 | # type: "sequence64",
40 | # },
41 | # {
42 | # name: "uuid",
43 | # type: "uuid",
44 | # },
45 | # {
46 | # name: "now_local",
47 | # type: "now_local",
48 | # },
49 | # {
50 | # name: "now_utc",
51 | # type: "now_utc",
52 | # },
53 | # {
54 | # name: "now_utc_lite",
55 | # type: "now_utc_lite",
56 | # },
57 | # {
58 | # name: "now_unix",
59 | # type: "now_unix",
60 | # },
61 | # {
62 | # name: "now_with_format",
63 | # type: "now_with_format",
64 | # // https://programming.guide/go/format-parse-string-time-date-example.html
65 | # format: "2006-01-02T15:04:05-0700",
66 | # },
67 | # {
68 | # name: "suffix",
69 | # type: "range",
70 | # from: 10,
71 | # to: 1000,
72 | # },
73 | # {
74 | # name: "bool",
75 | # type: "range",
76 | # from: 0,
77 | # to: 1,
78 | # },
79 | # {
80 | # name: "list",
81 | # type: "list",
82 | # data: ["medcl", "abc", "efg", "xyz"],
83 | # },
84 | # {
85 | # name: "id_list",
86 | # type: "random_array",
87 | # variable_type: "number", // number/string
88 | # variable_key: "suffix", // variable key to get array items
89 | # square_bracket: false,
90 | # size: 10, // how many items for array
91 | # },
92 | # {
93 | # name: "str_list",
94 | # type: "random_array",
95 | # variable_type: "number", // number/string
96 | # variable_key: "suffix", // variable key to get array items
97 | # square_bracket: true,
98 | # size: 10, // how many items for array
99 | # replace: {
100 | # // Use ' instead of " for string quotes
101 | # '"': "'",
102 | # // Use {} instead of [] as array brackets
103 | # "[": "{",
104 | # "]": "}",
105 | # },
106 | # },
107 | # ],
108 |
109 | DELETE /medcl
110 |
111 | PUT /medcl
112 |
113 | POST /medcl/_doc/1
114 | {
115 | "name": "medcl"
116 | }
117 |
118 | POST /medcl/_search
119 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } }
120 |
--------------------------------------------------------------------------------
/loadgen.yml:
--------------------------------------------------------------------------------
1 | ## How to use loadgen?
2 | ## $ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -config loadgen.yml
3 |
4 | env:
5 | ES_USERNAME: username
6 | ES_PASSWORD: password
7 | ES_ENDPOINT: http://localhost:9200
8 |
9 | runner:
10 | # total_rounds: 1
11 | no_warm: true
12 | valid_status_codes_during_warmup: [ 200,201,404 ]
13 | # Whether to log all requests
14 | log_requests: false
15 | # Whether to log all requests with the specified response status
16 | log_status_codes:
17 | - 0
18 | - 500
19 | assert_invalid: false
20 | assert_error: false
21 | # Whether to reset the context, including variables, runtime KV pairs, etc.,
22 | # before this test run.
23 | reset_context: false
24 | default_endpoint: $[[env.ES_ENDPOINT]]
25 | default_basic_auth:
26 | username: $[[env.ES_USERNAME]]
27 | password: $[[env.ES_PASSWORD]]
28 |
29 | variables:
30 | # - name: ip
31 | # type: file
32 | # path: dict/ip.txt
33 | # replace: # replace special characters in the value
34 | # '"': '\"'
35 | # '\': '\\'
36 | - name: id
37 | type: sequence
38 | - name: id64
39 | type: sequence64
40 | - name: uuid
41 | type: uuid
42 | - name: now_local
43 | type: now_local
44 | - name: now_utc
45 | type: now_utc
46 | - name: now_utc_lite
47 | type: now_utc_lite
48 | - name: now_unix
49 | type: now_unix
50 | - name: now_with_format
51 | type: now_with_format #https://programming.guide/go/format-parse-string-time-date-example.html
52 | format: "2006-01-02T15:04:05-0700" #2006-01-02T15:04:05
53 | - name: suffix
54 | type: range
55 | from: 10
56 | to: 1000
57 | - name: bool
58 | type: range
59 | from: 0
60 | to: 1
61 | - name: list
62 | type: list
63 | data:
64 | - "medcl"
65 | - "abc"
66 | - "efg"
67 | - "xyz"
68 | - name: id_list
69 | type: random_array
70 | variable_type: number # number/string
71 | variable_key: suffix # variable key to get array items
72 | square_bracket: false
73 | size: 10 # how many items for array
74 | - name: str_list
75 | type: random_array
76 | variable_type: string # number/string
77 | variable_key: suffix #variable key to get array items
78 | square_bracket: true
79 | size: 10 # how many items for array
80 | replace:
81 | '"': "'" # use ' instead of " for string quotes
82 | # use {} instead of [] as array brackets
83 | "[": "{"
84 | "]": "}"
85 |
86 | requests:
87 | - request: #prepare some docs
88 | method: POST
89 | runtime_variables:
90 | batch_no: uuid
91 | runtime_body_line_variables:
92 | routing_no: uuid
93 | url: /_bulk
94 | body: |
95 | {"index": {"_index": "medcl", "_type": "_doc", "_id": "$[[uuid]]"}}
96 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
97 | {"index": {"_index": "infinilabs", "_type": "_doc", "_id": "$[[uuid]]"}}
98 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"}
99 | - request: #search this index
100 | method: POST
101 | runtime_variables:
102 | batch_no: uuid
103 | runtime_body_line_variables:
104 | routing_no: uuid
105 | basic_auth: #override default auth
106 | username: $[[env.ES_USERNAME]]
107 | password: $[[env.ES_PASSWORD]]
108 | url: $[[env.ES_ENDPOINT]]/medcl/_search #override with full request url
109 | body: |
110 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } }
111 |
112 | #add more requests
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) INFINI Labs & INFINI LIMITED.
2 | //
3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0
4 | // and as commercial software.
5 | //
6 | // For commercial licensing, contact us at:
7 | // - Website: infinilabs.com
8 | // - Email: hello@infini.ltd
9 | //
10 | // Open Source licensed under AGPL V3:
11 | // This program is free software: you can redistribute it and/or modify
12 | // it under the terms of the GNU Affero General Public License as published by
13 | // the Free Software Foundation, either version 3 of the License, or
14 | // (at your option) any later version.
15 | //
16 | // This program is distributed in the hope that it will be useful,
17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | // GNU Affero General Public License for more details.
20 | //
21 | // You should have received a copy of the GNU Affero General Public License
22 | // along with this program. If not, see .
23 |
24 | /* Copyright © INFINI Ltd. All rights reserved.
25 | * web: https://infinilabs.com
26 | * mail: hello#infini.ltd */
27 |
28 | package main
29 |
30 | import (
31 | "context"
32 | _ "embed"
33 | E "errors"
34 | "flag"
35 | "fmt"
36 | "os"
37 | "os/signal"
38 | "strings"
39 | "time"
40 |
41 | "github.com/jamiealquiza/tachymeter"
42 |
43 | log "github.com/cihub/seelog"
44 | wasm "github.com/tetratelabs/wazero"
45 | wasmAPI "github.com/tetratelabs/wazero/api"
46 | "infini.sh/framework"
47 | coreConfig "infini.sh/framework/core/config"
48 | "infini.sh/framework/core/env"
49 | "infini.sh/framework/core/global"
50 | "infini.sh/framework/core/module"
51 | "infini.sh/framework/core/util"
52 | stats "infini.sh/framework/plugins/stats_statsd"
53 | "infini.sh/loadgen/config"
54 | )
55 |
56 | //go:embed plugins/loadgen_dsl.wasm
57 | var loadgenDSL []byte
58 |
59 | var maxDuration int = 10
60 | var goroutines int = 2
61 | var rateLimit int = -1
62 | var reqLimit int = -1
63 | var timeout int = 60
64 | var readTimeout int = 0
65 | var writeTimeout int = 0
66 | var dialTimeout int = 3
67 | var compress bool = false
68 | var mixed bool = false
69 | var totalRounds int = -1
70 | var dslFileToRun string
71 | var statsAggregator chan *LoadStats
72 |
73 | func init() {
74 | flag.IntVar(&goroutines, "c", 1, "Number of concurrent threads to use")
75 | flag.IntVar(&maxDuration, "d", 5, "Duration of the test in seconds")
76 | flag.IntVar(&rateLimit, "r", -1, "Maximum requests per second (fixed QPS), default: -1 (unlimited)")
77 | flag.IntVar(&reqLimit, "l", -1, "Total number of requests to send, default: -1 (unlimited)")
78 | flag.IntVar(&timeout, "timeout", 0, "Request timeout in seconds, default: 0 (no timeout)")
79 | flag.IntVar(&readTimeout, "read-timeout", 0, "Connection read timeout in seconds, default: 0 (inherits -timeout)")
80 | flag.IntVar(&writeTimeout, "write-timeout", 0, "Connection write timeout in seconds, default: 0 (inherits -timeout)")
81 | flag.IntVar(&dialTimeout, "dial-timeout", 3, "Connection dial timeout in seconds, default: 3")
82 | flag.BoolVar(&compress, "compress", false, "Enable gzip compression for requests")
83 | flag.BoolVar(&mixed, "mixed", false, "Enable mixed requests from YAML/DSL")
84 | flag.IntVar(&totalRounds, "total-rounds", -1, "Number of rounds for each request configuration, default: -1 (unlimited)")
85 | flag.StringVar(&dslFileToRun, "run", "", "Path to a DSL-based request file to execute")
86 | }
87 |
88 | func startLoader(cfg *LoaderConfig) *LoadStats {
89 | defer log.Flush()
90 |
91 | statsAggregator = make(chan *LoadStats, goroutines)
92 | sigChan := make(chan os.Signal, 1)
93 |
94 | signal.Notify(sigChan, os.Interrupt)
95 |
96 | flag.Parse()
97 |
98 | if cfg.RunnerConfig.MetricSampleSize <= 0 {
99 | cfg.RunnerConfig.MetricSampleSize = 10000
100 | }
101 |
102 | //override the total
103 | if totalRounds > 0 {
104 | cfg.RunnerConfig.TotalRounds = totalRounds
105 | }
106 |
107 | // Initialize tachymeter.
108 | timer := tachymeter.New(&tachymeter.Config{Size: cfg.RunnerConfig.MetricSampleSize})
109 |
110 | loadGen := NewLoadGenerator(maxDuration, goroutines, statsAggregator, cfg.RunnerConfig.DisableHeaderNamesNormalizing)
111 |
112 | leftDoc := reqLimit
113 |
114 | if !cfg.RunnerConfig.NoWarm {
115 | reqCount := loadGen.Warmup(cfg)
116 | leftDoc -= reqCount
117 | }
118 |
119 | if reqLimit >= 0 && leftDoc <= 0 {
120 | log.Warn("No request to execute, exit now\n")
121 | return nil
122 | }
123 |
124 | var reqPerGoroutines int
125 | if reqLimit > 0 {
126 | if goroutines > leftDoc {
127 | goroutines = leftDoc
128 | }
129 |
130 | reqPerGoroutines = int((leftDoc + 1) / goroutines)
131 | }
132 |
133 | // Start wall time for all Goroutines.
134 | wallTimeStart := time.Now()
135 |
136 | for i := 0; i < goroutines; i++ {
137 | thisDoc := -1
138 | if reqPerGoroutines > 0 {
139 | if leftDoc > reqPerGoroutines {
140 | thisDoc = reqPerGoroutines
141 | } else {
142 | thisDoc = leftDoc
143 | }
144 | leftDoc -= thisDoc
145 | }
146 |
147 | go loadGen.Run(cfg, thisDoc, timer)
148 | }
149 |
150 | responders := 0
151 | aggStats := LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}}
152 |
153 | for responders < goroutines {
154 | select {
155 | case <-sigChan:
156 | loadGen.Stop()
157 | case stats := <-statsAggregator:
158 | aggStats.NumErrs += stats.NumErrs
159 | aggStats.NumAssertInvalid += stats.NumAssertInvalid
160 | aggStats.NumAssertSkipped += stats.NumAssertSkipped
161 | aggStats.NumRequests += stats.NumRequests
162 | aggStats.TotReqSize += stats.TotReqSize
163 | aggStats.TotRespSize += stats.TotRespSize
164 | aggStats.TotDuration += stats.TotDuration
165 | aggStats.MaxRequestTime = util.MaxDuration(aggStats.MaxRequestTime, stats.MaxRequestTime)
166 | aggStats.MinRequestTime = util.MinDuration(aggStats.MinRequestTime, stats.MinRequestTime)
167 |
168 | for k, v := range stats.StatusCode {
169 | oldV, ok := aggStats.StatusCode[k]
170 | if !ok {
171 | oldV = 0
172 | }
173 | aggStats.StatusCode[k] = oldV + v
174 | }
175 |
176 | responders++
177 | }
178 | }
179 |
180 | if aggStats.NumRequests == 0 {
181 | log.Error("Error: No statistics collected / no requests found")
182 | return nil
183 | }
184 |
185 | finalDuration := time.Since(wallTimeStart)
186 |
187 | // When finished, set elapsed wall time.
188 | timer.SetWallTime(finalDuration)
189 |
190 | avgThreadDur := aggStats.TotDuration / time.Duration(responders) //need to average the aggregated duration
191 |
192 | roughReqRate := float64(aggStats.NumRequests) / float64(finalDuration.Seconds())
193 | roughReqBytesRate := float64(aggStats.TotReqSize) / float64(finalDuration.Seconds())
194 | roughBytesRate := float64(aggStats.TotRespSize+aggStats.TotReqSize) / float64(finalDuration.Seconds())
195 |
196 | reqRate := float64(aggStats.NumRequests) / avgThreadDur.Seconds()
197 | avgReqTime := aggStats.TotDuration / time.Duration(aggStats.NumRequests)
198 | bytesRate := float64(aggStats.TotRespSize+aggStats.TotReqSize) / avgThreadDur.Seconds()
199 |
200 | // Flush before printing stats to avoid logging mixing with stats
201 | log.Flush()
202 |
203 | if cfg.RunnerConfig.NoSizeStats {
204 | fmt.Printf("\n%v requests finished in %v\n", aggStats.NumRequests, avgThreadDur)
205 | } else {
206 | fmt.Printf("\n%v requests finished in %v, %v sent, %v received\n", aggStats.NumRequests, avgThreadDur, util.ByteValue{Size: float64(aggStats.TotReqSize)}, util.ByteValue{Size: float64(aggStats.TotRespSize)})
207 | }
208 |
209 | fmt.Println("\n[Loadgen Client Metrics]")
210 |
211 | fmt.Printf("Requests/sec:\t\t%.2f\n", roughReqRate)
212 |
213 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoSizeStats {
214 | fmt.Printf(
215 | "Request Traffic/sec:\t%v\n"+
216 | "Total Transfer/sec:\t%v\n",
217 | util.ByteValue{Size: roughReqBytesRate},
218 | util.ByteValue{Size: roughBytesRate})
219 | }
220 |
221 | fmt.Printf("Fastest Request:\t%v\n", aggStats.MinRequestTime)
222 | fmt.Printf("Slowest Request:\t%v\n", aggStats.MaxRequestTime)
223 |
224 | if cfg.RunnerConfig.AssertError {
225 | fmt.Printf("Number of Errors:\t%v\n", aggStats.NumErrs)
226 | }
227 |
228 | if cfg.RunnerConfig.AssertInvalid {
229 | fmt.Printf("Assert Invalid:\t\t%v\n", aggStats.NumAssertInvalid)
230 | fmt.Printf("Assert Skipped:\t\t%v\n", aggStats.NumAssertSkipped)
231 | }
232 |
233 | for k, v := range aggStats.StatusCode {
234 | fmt.Printf("Status %v:\t\t%v\n", k, v)
235 | }
236 |
237 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoStats {
238 | // Rate outputs will be accurate.
239 | fmt.Println("\n[Latency Metrics]")
240 | fmt.Println(timer.Calc().String())
241 |
242 | fmt.Println("\n[Latency Distribution]")
243 | fmt.Println(timer.Calc().Histogram.String(30))
244 | }
245 |
246 | fmt.Printf("\n[Estimated Server Metrics]\nRequests/sec:\t\t%.2f\nAvg Req Time:\t\t%v\n", reqRate, avgReqTime)
247 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoSizeStats {
248 | fmt.Printf("Transfer/sec:\t\t%v\n", util.ByteValue{Size: bytesRate})
249 | }
250 |
251 | fmt.Println("")
252 |
253 | return &aggStats
254 | }
255 |
256 | //func addProcessToCgroup(filepath string, pid int) {
257 | // file, err := os.OpenFile(filepath, os.O_WRONLY, 0644)
258 | // if err != nil {
259 | // fmt.Println(err)
260 | // os.Exit(1)
261 | // }
262 | // defer file.Close()
263 | //
264 | // if _, err := file.WriteString(fmt.Sprintf("%d", pid)); err != nil {
265 | // fmt.Println("failed to setup cgroup for the container: ", err)
266 | // os.Exit(1)
267 | // }
268 | //}
269 | //
270 | //func cgroupSetup(pid int) {
271 | // for _, c := range []string{"cpu", "memory"} {
272 | // cpath := fmt.Sprintf("/sys/fs/cgroup/%s/mycontainer/", c)
273 | // if err := os.MkdirAll(cpath, 0644); err != nil {
274 | // fmt.Println("failed to create cpu cgroup for my container: ", err)
275 | // os.Exit(1)
276 | // }
277 | // addProcessToCgroup(cpath+"cgroup.procs", pid)
278 | // }
279 | //}
280 |
281 | func main() {
282 |
283 | terminalHeader := (" __ ___ _ ___ ___ __ __\n")
284 | terminalHeader += (" / / /___\\/_\\ / \\/ _ \\ /__\\/\\ \\ \\\n")
285 | terminalHeader += (" / / // ///_\\\\ / /\\ / /_\\//_\\ / \\/ /\n")
286 | terminalHeader += ("/ /__/ \\_// _ \\/ /_// /_\\\\//__/ /\\ /\n")
287 | terminalHeader += ("\\____|___/\\_/ \\_/___,'\\____/\\__/\\_\\ \\/\n\n")
288 | terminalHeader += ("HOME: https://github.com/infinilabs/loadgen/\n\n")
289 |
290 | terminalFooter := ("")
291 |
292 | app := framework.NewApp("loadgen", "A http load generator and testing suite, open-sourced under the GNU AGPLv3.",
293 | config.Version, config.BuildNumber, config.LastCommitLog, config.BuildDate, config.EOLDate, terminalHeader, terminalFooter)
294 |
295 | app.IgnoreMainConfigMissing()
296 | app.Init(nil)
297 |
298 | defer app.Shutdown()
299 | appConfig := AppConfig{}
300 |
301 | if app.Setup(func() {
302 | module.RegisterUserPlugin(&stats.StatsDModule{})
303 | module.Start()
304 |
305 | environments := map[string]string{}
306 | ok, err := env.ParseConfig("env", &environments)
307 | if ok && err != nil {
308 | if ok && err != nil {
309 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
310 | panic(err)
311 | } else {
312 | log.Error(err)
313 | }
314 | }
315 | }
316 |
317 | // Append system environment variables.
318 | environs := os.Environ()
319 | for _, env := range environs {
320 | kv := strings.Split(env, "=")
321 | if len(kv) == 2 {
322 | k, v := kv[0], kv[1]
323 | if _, ok := environments[k]; ok {
324 | environments[k] = v
325 | }
326 | }
327 | }
328 |
329 | tests := []Test{}
330 | ok, err = env.ParseConfig("tests", &tests)
331 | if ok && err != nil {
332 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
333 | panic(err)
334 | } else {
335 | log.Error(err)
336 | }
337 | }
338 |
339 | requests := []RequestItem{}
340 | ok, err = env.ParseConfig("requests", &requests)
341 | if ok && err != nil {
342 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
343 | panic(err)
344 | } else {
345 | log.Error(err)
346 | }
347 | }
348 |
349 | variables := []Variable{}
350 | ok, err = env.ParseConfig("variables", &variables)
351 | if ok && err != nil {
352 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
353 | panic(err)
354 | } else {
355 | log.Error(err)
356 | }
357 | }
358 |
359 | runnerConfig := RunnerConfig{
360 | ValidStatusCodesDuringWarmup: []int{},
361 | }
362 | ok, err = env.ParseConfig("runner", &runnerConfig)
363 | if ok && err != nil {
364 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
365 | panic(err)
366 | } else {
367 | log.Error(err)
368 | }
369 | }
370 |
371 | appConfig.Environments = environments
372 | appConfig.Tests = tests
373 | appConfig.Requests = requests
374 | appConfig.Variable = variables
375 | appConfig.RunnerConfig = runnerConfig
376 | appConfig.Init()
377 | }, func() {
378 | go func() {
379 | //dsl go first
380 | if dslFileToRun != "" {
381 | log.Debugf("running DSL based requests from %s", dslFileToRun)
382 | if status := runDSLFile(&appConfig, dslFileToRun); status != 0 {
383 | os.Exit(status)
384 | }
385 | if !mixed {
386 | os.Exit(0)
387 | return
388 | }
389 | }
390 |
391 | if len(appConfig.Requests) != 0 {
392 | log.Debugf("running YAML based requests")
393 | if status := runLoaderConfig(&appConfig.LoaderConfig); status != 0 {
394 | os.Exit(status)
395 | }
396 | if !mixed {
397 | os.Exit(0)
398 | return
399 | }
400 | }
401 |
402 | //test suit go last
403 | if len(appConfig.Tests) != 0 {
404 | log.Debugf("running test suite")
405 | if !startRunner(&appConfig) {
406 | os.Exit(1)
407 | }
408 | if !mixed {
409 | os.Exit(0)
410 | return
411 | }
412 | }
413 |
414 | os.Exit(0)
415 | }()
416 |
417 | }, nil) {
418 | app.Run()
419 | }
420 |
421 | }
422 |
423 | func runDSLFile(appConfig *AppConfig, path string) int {
424 |
425 | path = util.TryGetFileAbsPath(path, false)
426 | dsl, err := env.LoadConfigContents(path)
427 | if err != nil {
428 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
429 | panic(err)
430 | } else {
431 | log.Error(err)
432 | }
433 | }
434 | log.Infof("loading config: %s", path)
435 |
436 | return runDSL(appConfig, dsl)
437 | }
438 |
439 | func runDSL(appConfig *AppConfig, dsl string) int {
440 | loaderConfig := parseDSL(appConfig, dsl)
441 | return runLoaderConfig(&loaderConfig)
442 | }
443 |
444 | func runLoaderConfig(config *LoaderConfig) int {
445 | err := config.Init()
446 | if err != nil {
447 | panic(err)
448 | }
449 |
450 | aggStats := startLoader(config)
451 | if aggStats != nil {
452 | if config.RunnerConfig.AssertInvalid && aggStats.NumAssertInvalid > 0 {
453 | return 1
454 | }
455 | if config.RunnerConfig.AssertError && aggStats.NumErrs > 0 {
456 | return 2
457 | }
458 | }
459 |
460 | return 0
461 | }
462 |
463 | // parseDSL parses a DSL string to LoaderConfig.
464 | func parseDSL(appConfig *AppConfig, input string) (output LoaderConfig) {
465 | output = LoaderConfig{}
466 | output.RunnerConfig = appConfig.RunnerConfig
467 | output.Variable = appConfig.Variable
468 |
469 | outputStr, err := loadPlugins([][]byte{loadgenDSL}, input)
470 | if err != nil {
471 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
472 | panic(err)
473 | } else {
474 | log.Error(err)
475 | }
476 | }
477 | log.Debugf("using config:\n%s", outputStr)
478 |
479 | outputParser, err := coreConfig.NewConfigWithYAML([]byte(outputStr), "loadgen-dsl")
480 | if err != nil {
481 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
482 | panic(err)
483 | } else {
484 | log.Error(err)
485 | }
486 | }
487 |
488 | if err := outputParser.Unpack(&output); err != nil {
489 | if global.Env().SystemConfig.Configs.PanicOnConfigError {
490 | panic(err)
491 | } else {
492 | log.Error(err)
493 | }
494 | }
495 |
496 | return
497 | }
498 |
499 | func loadPlugins(plugins [][]byte, input string) (output string, err error) {
500 | // init runtime
501 | ctx := context.Background()
502 | r := wasm.NewRuntime(ctx)
503 | defer r.Close(ctx)
504 |
505 | var mod wasmAPI.Module
506 | for _, plug := range plugins {
507 | // load plugin
508 | mod, err = r.Instantiate(ctx, plug)
509 | if err != nil {
510 | return
511 | }
512 | // call plugin
513 | output, err = callPlugin(ctx, mod, string(input))
514 | if err != nil {
515 | break
516 | }
517 | // pipe output
518 | input = output
519 | }
520 | return
521 | }
522 |
523 | func callPlugin(ctx context.Context, mod wasmAPI.Module, input string) (output string, err error) {
524 | alloc := mod.ExportedFunction("allocate")
525 | free := mod.ExportedFunction("deallocate")
526 | process := mod.ExportedFunction("process")
527 |
528 | // 1) Plugins do not have access to host memory, so the first step is to copy
529 | // the input string to the WASM VM.
530 | inputSize := uint32(len(input))
531 | ret, err := alloc.Call(ctx, uint64(inputSize))
532 | if err != nil {
533 | return
534 | }
535 | inputPtr := ret[0]
536 | defer free.Call(ctx, inputPtr)
537 | _, inputAddr, _ := decodePtr(inputPtr)
538 | mod.Memory().Write(inputAddr, []byte(input))
539 |
540 | // 2) Invoke the `process` function to handle the input string, which returns
541 | // a result pointer (referred to as `decodePtr` in the following text)
542 | // representing the processing result.
543 | ret, err = process.Call(ctx, inputPtr)
544 | if err != nil {
545 | return
546 | }
547 | outputPtr := ret[0]
548 | defer free.Call(ctx, outputPtr)
549 |
550 | // 3) Check the processing result.
551 | errors, outputAddr, outputSize := decodePtr(outputPtr)
552 | bytes, _ := mod.Memory().Read(outputAddr, outputSize)
553 |
554 | if errors {
555 | err = E.New(string(bytes))
556 | } else {
557 | output = string(bytes)
558 | }
559 | return
560 | }
561 |
562 | // decodePtr decodes error state and memory address of a result pointer.
563 | //
564 | // Some functions may return success or failure as a result. In such cases, a
565 | // special 64-bit pointer is returned. The highest bit in the upper 32 bits
566 | // serves as a boolean value indicating success (1) or failure (0), while the
567 | // remaining 31 bits represent the length of the message. The lower 32 bits of
568 | // the pointer represent the memory address of the specific message.
569 | func decodePtr(ptr uint64) (errors bool, addr, size uint32) {
570 | const SIZE_MASK uint32 = (^uint32(0)) >> 1
571 | addr = uint32(ptr)
572 | size = uint32(ptr>>32) & SIZE_MASK
573 | errors = (ptr >> 63) != 0
574 | return
575 | }
576 |
--------------------------------------------------------------------------------
/plugins/loadgen_dsl.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinilabs/loadgen/2f372fc70da65ae78580ccb778ba7976139150e5/plugins/loadgen_dsl.wasm
--------------------------------------------------------------------------------
/runner.go:
--------------------------------------------------------------------------------
1 | // Copyright (C) INFINI Labs & INFINI LIMITED.
2 | //
3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0
4 | // and as commercial software.
5 | //
6 | // For commercial licensing, contact us at:
7 | // - Website: infinilabs.com
8 | // - Email: hello@infini.ltd
9 | //
10 | // Open Source licensed under AGPL V3:
11 | // This program is free software: you can redistribute it and/or modify
12 | // it under the terms of the GNU Affero General Public License as published by
13 | // the Free Software Foundation, either version 3 of the License, or
14 | // (at your option) any later version.
15 | //
16 | // This program is distributed in the hope that it will be useful,
17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | // GNU Affero General Public License for more details.
20 | //
21 | // You should have received a copy of the GNU Affero General Public License
22 | // along with this program. If not, see .
23 |
24 | /* Copyright © INFINI Ltd. All rights reserved.
25 | * web: https://infinilabs.com
26 | * mail: hello#infini.ltd */
27 |
28 | package main
29 |
30 | import (
31 | "bytes"
32 | "context"
33 | "errors"
34 | "infini.sh/framework/core/global"
35 | "net"
36 | "os"
37 | "os/exec"
38 | "path"
39 | "path/filepath"
40 | "sync/atomic"
41 | "time"
42 |
43 | log "github.com/cihub/seelog"
44 | "infini.sh/framework/core/util"
45 | )
46 |
47 | type TestResult struct {
48 | Failed bool `json:"failed"`
49 | Time time.Time `json:"time"`
50 | DurationInMs int64 `json:"duration_in_ms"`
51 | Error error `json:"error"`
52 | }
53 |
54 | type TestMsg struct {
55 | Time time.Time `json:"time"`
56 | Path string `json:"path"`
57 | Status string `json:"status"` // ABORTED/FAILED/SUCCESS
58 | DurationInMs int64 `json:"duration_in_ms"`
59 | }
60 |
61 | const (
62 | portTestTimeout = 100 * time.Millisecond
63 | )
64 |
65 | func startRunner(config *AppConfig) bool {
66 | defer log.Flush()
67 |
68 | cwd, err := os.Getwd()
69 | if err != nil {
70 | log.Infof("failed to get working directory, err: %v", err)
71 | return false
72 | }
73 | msgs := make([]*TestMsg, len(config.Tests))
74 | for i, test := range config.Tests {
75 | // Wait for the last process to get fully killed if not existed cleanly
76 | time.Sleep(time.Second)
77 | result, err := runTest(config, cwd, test)
78 | msg := &TestMsg{
79 | Path: test.Path,
80 | }
81 | if result == nil || err != nil {
82 | log.Debugf("failed to run test, error: %+v", err)
83 | msg.Status = "ABORTED"
84 | } else if result.Failed {
85 | msg.Status = "FAILED"
86 | } else {
87 | msg.Status = "SUCCESS"
88 | }
89 | if result != nil {
90 | msg.DurationInMs = result.DurationInMs
91 | msg.Time = result.Time
92 | }
93 | msgs[i] = msg
94 | }
95 | ok := true
96 | for _, msg := range msgs {
97 | log.Infof("[%s][TEST][%s] [%s] duration: %d(ms)", msg.Time.Format("2006-01-02 15:04:05"), msg.Status, msg.Path, msg.DurationInMs)
98 | if msg.Status != "SUCCESS" {
99 | ok = false
100 | }
101 | }
102 | return ok
103 | }
104 |
105 | func runTest(config *AppConfig, cwd string, test Test) (*TestResult, error) {
106 | // To kill gateway/other command automatically
107 | ctx, cancel := context.WithCancel(context.Background())
108 | defer cancel()
109 |
110 | //if err := os.Chdir(cwd); err != nil {
111 | // return nil, err
112 | //}
113 |
114 | testPath := test.Path
115 | var gatewayPath string
116 | if config.Environments[env_LR_GATEWAY_CMD] != "" {
117 | gatewayPath, _ = filepath.Abs(config.Environments[env_LR_GATEWAY_CMD])
118 | }
119 |
120 | loaderConfigPath := path.Join(testPath, "loadgen.dsl")
121 | //auto resolve the loaderConfigPath
122 | if !util.FileExists(loaderConfigPath) {
123 | temp := path.Join(filepath.Dir(global.Env().GetConfigFile()), loaderConfigPath)
124 | if util.FileExists(temp) {
125 | loaderConfigPath = temp
126 | } else {
127 | temp := path.Join(filepath.Dir(global.Env().GetConfigDir()), loaderConfigPath)
128 | if util.FileExists(temp) {
129 | loaderConfigPath = temp
130 | }
131 | }
132 | }
133 | loaderConfigPath, _ = filepath.Abs(loaderConfigPath)
134 |
135 | //log.Debugf("Executing gateway within %s", testPath)
136 | //if err := os.Chdir(filepath.Dir(loaderConfigPath)); err != nil {
137 | // return nil, err
138 | //}
139 | //// Revert cwd change
140 | //defer os.Chdir(cwd)
141 |
142 | env := generateEnv(config)
143 | log.Debugf("Executing gateway with environment [%+v]", env)
144 |
145 | gatewayConfigPath := path.Join(testPath, "gateway.yml")
146 | if _, err := os.Stat(gatewayConfigPath); err == nil {
147 | if gatewayPath == "" {
148 | return nil, errors.New("invalid LR_GATEWAY_CMD, cannot find gateway")
149 | }
150 | gatewayOutput := &bytes.Buffer{}
151 | // Start gateway server
152 | gatewayHost, gatewayApiHost := config.Environments[env_LR_GATEWAY_HOST], config.Environments[env_LR_GATEWAY_API_HOST]
153 | gatewayCmd, gatewayExited, err := runGateway(ctx, gatewayPath, gatewayConfigPath, gatewayHost, gatewayApiHost, env, gatewayOutput)
154 | if err != nil {
155 | return nil, err
156 | }
157 |
158 | defer func() {
159 | log.Debug("waiting for 5s to stop the gateway")
160 | gatewayCmd.Process.Signal(os.Interrupt)
161 | timeout := time.NewTimer(5 * time.Second)
162 | select {
163 | case <-gatewayExited:
164 | case <-timeout.C:
165 | }
166 | log.Debug("============================== Gateway Exit Info [Start] =============================")
167 | log.Debug(util.UnsafeBytesToString(gatewayOutput.Bytes()))
168 | log.Debug("============================== Gateway Exit Info [End] =============================")
169 | }()
170 | }
171 |
172 | startTime := time.Now()
173 | testResult := &TestResult{}
174 | defer func() {
175 | testResult.Time = time.Now()
176 | testResult.DurationInMs = int64(testResult.Time.Sub(startTime) / time.Millisecond)
177 | }()
178 |
179 | status := runDSLFile(config, loaderConfigPath)
180 | if status != 0 {
181 | testResult.Failed = true
182 | }
183 | return testResult, nil
184 | }
185 |
186 | func runGateway(ctx context.Context, gatewayPath, gatewayConfigPath, gatewayHost, gatewayApiHost string, env []string, gatewayOutput *bytes.Buffer) (*exec.Cmd, chan int, error) {
187 | gatewayCmdArgs := []string{"-config", gatewayConfigPath, "-log", "debug"}
188 | log.Debugf("Executing gateway with args [%+v]", gatewayCmdArgs)
189 | gatewayCmd := exec.CommandContext(ctx, gatewayPath, gatewayCmdArgs...)
190 | gatewayCmd.Env = env
191 | gatewayCmd.Stdout = gatewayOutput
192 | gatewayCmd.Stderr = gatewayOutput
193 |
194 | gatewayFailed := int32(0)
195 | gatewayExited := make(chan int)
196 |
197 | go func() {
198 | err := gatewayCmd.Run()
199 | if err != nil {
200 | log.Debugf("gateway server exited with non-zero code: %+v", err)
201 | atomic.StoreInt32(&gatewayFailed, 1)
202 | }
203 | gatewayExited <- 1
204 | }()
205 |
206 | gatewayReady := false
207 |
208 | // Check whether gateway is ready.
209 | for i := 0; i < 10; i += 1 {
210 | if atomic.LoadInt32(&gatewayFailed) == 1 {
211 | break
212 | }
213 | log.Debugf("Checking whether %s or %s is ready...", gatewayHost, gatewayApiHost)
214 | entryReady, apiReady := testPort(gatewayHost), testPort(gatewayApiHost)
215 | if entryReady || apiReady {
216 | log.Debugf("gateway is started, entry: %+v, api: %+v", entryReady, apiReady)
217 | gatewayReady = true
218 | break
219 | }
220 | log.Debugf("failed to probe gateway, retrying")
221 | time.Sleep(100 * time.Millisecond)
222 | }
223 |
224 | if !gatewayReady {
225 | return nil, nil, errors.New("can't start gateway")
226 | }
227 |
228 | return gatewayCmd, gatewayExited, nil
229 | }
230 |
231 | func testPort(host string) bool {
232 | conn, err := net.DialTimeout("tcp", host, portTestTimeout)
233 | if err != nil {
234 | return false
235 | }
236 | conn.Close()
237 | return true
238 | }
239 |
240 | func generateEnv(config *AppConfig) (env []string) {
241 | for k, v := range config.Environments {
242 | env = append(env, k+"="+v)
243 | }
244 | // Disable greeting messages
245 | env = append(env, "SILENT_GREETINGS=1")
246 | return
247 | }
248 |
--------------------------------------------------------------------------------