Embark on this journey with our collection of 8 quickstarts, guiding you through a wide range of potential use cases that you might encounter in daily operations.
This is the multi-page printable view of this section. Click here to print.
Getting Started
- 1: First benchmark
- 2: Steps and statistics
- 3: Complex workflow
- 4: Phases - basics
- 5: Phases - advanced
- 6: Running the server
- 7: Clustered mode
- 8: Custom components
1 - First benchmark
1. Download latest release and unpack it
wget https://github.com/Hyperfoil/Hyperfoil/releases/download/hyperfoil-all-0.27.1 \
&& unzip hyperfoil-0.27.1.zip \
&& cd <extracted dir>
2. Start Hyperfoil in interactive mode (CLI)
bin/cli.sh
For our first benchmark we’ll start an embedded server (controller) within the CLI:
[hyperfoil]$ start-local
Starting controller in default directory (/tmp/hyperfoil)
Controller started, listening on 127.0.0.1:41621
Connecting to the controller...
Connected!
3. Upload the minimalistic benchmark and run it
As you can see below, the benchmark is really minimalistic as it is doing only single request to http://hyperfoil.io
.
# This is the name of the benchmark. It's recommended to keep this in sync with
# name of this file, adding extension `.hf.yaml`.
name: single-request
# We must define at least one HTTP target, in this case it becomes a default
# for all HTTP requests.
http:
host: http://hyperfoil.io
# Simulation consists of phases - potentially independent workloads.
# We'll discuss phases more in detail in next quickstarts.
phases:
# `example` is the name of the single phase in this benchmark.
- example:
# `atOnce` with `users: 1` results in running the scenario below just once
atOnce:
users: 1
scenario:
# The only sequence in this scenario is called `test`.
- test:
# In the only step in this sequence we'll do a HTTP GET request
# to `http://hyperfoil.io/`
- httpRequest:
GET: /
# Inject helpers to make this request synchronous, i.e. keep
# the sequence blocked until Hyperfoil processes the response.
sync: true
Create the same benchmark in your local environment or download it.
After that, upload it using the upload
command as follows:
[hyperfoil@in-vm]$ upload .../single-request.hf.yaml
Loaded benchmark single-request, uploading...
... done.
[hyperfoil@in-vm]$ run single-request
Started run 0001
Run 0001, benchmark single-request
Agents: in-vm[STARTING]
Started: 2019/11/15 16:11:43.725 Terminated: 2019/11/15 16:11:43.899
<span class="hfcaption">NAME STATUS STARTED REMAINING COMPLETED TOTAL DURATION DESCRIPTION
example TERMINATED 16:11:43.725 16:11:43.899 174 ms (exceeded by 174 ms) 1 users at once
4. Check out performance results:
[hyperfoil@in-vm]$ stats
Total stats from run 000A
<span class="hfcaption">PHASE METRIC REQUESTS MEAN p50 p90 p99 p99.9 p99.99 2xx 3xx 4xx 5xx CACHE TIMEOUTS ERRORS BLOCKED
example test 1 172.49 ms 173.02 ms 173.02 ms 173.02 ms 173.02 ms 173.02 ms 0 1 0 0 0 0 0 0 ns
Doing one request is not much of a benchmark and the statistics above are moot, but hey, this is a quickstart.
In the future you might find editing with schema useful but at this point any editor with YAML syntax highlighting will do the job.
Ready? Let’s continue with something a bit more realistic…
2 - Steps and statistics
In previous quickstart you created a benchmark that fires only one HTTP request. Our next example is going to hit random URLs at this server with 10 requests per second. We’ll see how to generate random data and collect statistics for different URLs.
Let’s start a container that will serve the requests:
podman run --rm -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
If you prefer running this in Docker just replace podman
with docker
. You can explore the handling of requests from this example on GitHub.
Here is the benchmark we’re going to run:
name: random-urls
http:
host: http://localhost:8080
sharedConnections: 10
# 10 users will be starting the scenario every second
usersPerSec: 10
duration: 5s
scenario:
- test:
# Step `randomItem` randomly picks one item from the list below...
- randomItem:
list:
- index.html
- foo.png
- bar.png
- this-returns-404.png
# ... and stores it in users's session under key `my-random-path`
toVar: my-random-path
- httpRequest:
# HTTP request will read the variable from the session and format
# the path for the GET request
GET: /quickstarts/random-urls/${my-random-path}
# We'll use different statistics metric for webpages and images
metric:
- .*\.html -> pages
- .*\.png -> images
- -> other
# Handler processes the response
handler:
# We'll check that the response was successful (status 200-299)
status:
range: 2xx
# When the response is fully processed we'll set variable `completed`
# in the session.
onCompletion:
set: completed <- yes
# For demonstration purposes we will set `sync: false`.
# Next step is executed immediately after we fire the request, not
# waiting for the response.
sync: false
# We'll wait for the `completed` var to be set in this step, though.
- awaitVar: completed
So let’s run this through CLI:
[hyperfoil]$ start-local
...
[hyperfoil@in-vm]$ upload .../random-urls.hf.yaml
...
[hyperfoil@in-vm]$ run
Started run 0002
Run 0002, benchmark random-urls
Agents: in-vm[STARTING]
Started: 2019/11/15 17:49:45.859 Terminated: 2019/11/15 17:49:50.904
NAME STATUS STARTED REMAINING COMPLETED TOTAL DURATION DESCRIPTION
main TERMINATED 17:49:45.859 17:49:50.903 5044 ms (exceeded by 44 ms) 10.00 users per second
[hyperfoil@in-vm]$ stats
Total stats from run 0002
PHASE METRIC REQUESTS MEAN p50 p90 p99 p99.9 p99.99 2xx 3xx 4xx 5xx CACHE TIMEOUTS ERRORS BLOCKED
main images 34 3.25 ms 3.39 ms 4.39 ms 12.58 ms 12.58 ms 12.58 ms 12 13 12 0 0 0 0 1.11 ms
main pages 13 2.89 ms 3.19 ms 4.15 ms 4.33 ms 4.33 ms 4.33 ms 13 0 0 0 0 0 0 0 ns
main/images: Progress was blocked waiting for a free connection. Hint: increase http.sharedConnections.
There are several things worth mentioning in this example:
The command
run
does not have any argument. In this case, the benchmark namerandom-urls
is optional as you’ve just uploaded it and CLI knows that you are going to work with it. The same holds forstats
- you don’t have to write down run ID0002
when displaying statistics as the implicit run ID is set automatically in therun
/status
command.The test did only 47 requests in 5 seconds, instead of 50. It does not execute one request every 100 ms sharp, it randomizes the times of requests as well; this simulates the Poisson point process. Longer runs would have lower variance in the total numbers.
In metric
images
the test reports 1.11 ms being blocked and there’s SLA failure below the stats. This is happening because in the default configuration Hyperfoil opens only one connection to the target server. All (possibly concurrent) requests have to share the common pool of 1 connection and if some request cannot be executed immediatelly we report this as blocked time. All practical benchmarks should increase the pool size to a value that reflects simulated load and prevent this situation.The test took 44 ms longer than the configured 5 seconds. We terminate the test only after all responses for sent requests arrive (or time out).
In the next quickstart you’ll see a more complex scenario
3 - Complex workflow
The previous example was the first ‘real’ benchmark, but it didn’t do anything different from what you could run through wrk
, ab
, siege
or similar tools.
Of course, the results were not suffering from the coordinated omission problem, but Hyperfoil can do more. Let’s try a more complex scenario:
name: choose-movie
http:
host: http://localhost:8080
# Use 80 concurrent HTTP connections to the server. Default is 1,
# therefore we couldn't issue two concurrent requests (as HTTP pipelining
# is disabled by default and we use HTTP 1.1 connections).
sharedConnections: 80
usersPerSec: 10
duration: 5s
# Each session will take at least 3 seconds (see the sleep time below),
# and we'll be running ~10 per second. That makes 30, let's give it
# some margin and set this to 40.
maxSessions: 40
scenario:
# In previous scenarios we have used only single sequence and we could
# define the list of sequences right away. In this scenario, we're going
# to be using 3 different sequences.
# Initial sequences are scheduled at session start and are not linked
# to the other sessions.
initialSequences:
- home:
# Pick a random username from a file
- randomItem:
file: usernames.txt
toVar: username
# The page would load a profile, e.g. to display full name.
- httpRequest:
GET: /quickstarts/choose-movie/profile?user=${username}
sync: false
metric: profile
# Fetch movies user could watch
- httpRequest:
GET: /quickstarts/choose-movie/movies
sync: false
metric: movies
handler:
body:
# Parse the returned JSON that is an array and for each
# element fire the processor.
json:
query: .[]
processor:
# Store each element in a collection `movies`
array:
toVar: movies
# Store as byte[] to avoid encoding UTF-8 into String
format: BYTES
# Every data structure in session has maximum size.
# This space is pre-allocated.
maxSize: 10
# This step waits until responses for all sent requests are received and processed.
- awaitAllResponses
# Wait 3 seconds to simulate user-interaction
- thinkTime:
duration: 3s
# Set variable `quality` and `movieNames` to an uninitialized array
# of 10 elements. We will use them later on.
- set:
var: quality
objectArray:
size: 10
- set:
var: movieNames
objectArray:
size: 10
# For each element in variable `movies` schedule one (new) instance
# of sequence `movies`, defined below. These instances differ in
# one intrinsic "variable" - their index.
- foreach:
fromVar: movies
sequence: addComment
# Schedule one more sequence
- newSequence: watchMovie
# These sequences are defined but don't get scheduled at session start. We activate
# them explicitly (and multiple times in parallel) in foreach step above.
sequences:
# Sequences that can run multiple instances concurrently must declare the maximum
# concurrency level explicitly using the brackets.
- addComment[10]:
# Variables `movies` hosts an array, and in the foreach step
# we've created one sequence for each element. We'll access
# the element through the '[.]' notation below.
- json:
fromVar: movies[.]
query: .quality
# We'll extract quality to another collection under
# this sequence's index. We shouldn't use global variable
# as the execution of sequences may interleave.
toVar: quality[.]
# For high-quality movies we won't post insults (we haven't seen
# the movie yet anyway). Therefore, we'll stop executing
# the sequence prematurely.
- breakSequence:
intCondition:
fromVar: quality[.]
# Note: ideally we could filter the JSON directly using query
# .[] | select(.quality >= 80)
# but this feature is not implemented yet.
greaterOrEqualTo: 80
- json:
fromVar: movies[.]
query: .name
toVar: movieNames[.]
- httpRequest:
# URLs with spaces and other characters don't work well;
# let's encode it (e.g. space -> %20)
POST: /quickstarts/choose-movie/movie/${urlencode:movieNames[.]}/comments
body:
text: This movie sucks.
# The sync shortcut actually sets up a bit in the session state
# cleared before the request and set when the request is complete,
# automatically waiting it after this step.
# You can write your own handlers (using sequence-scoped vars)
# to change this behaviour.
sync: true
# Set value to variable `commented`. The actual value does not matter.
- set: commented <- true
- watchMovie:
# This sequence is blocked in its first step until the variable gets
# set. Therefore we could define it in `initialSequences` and omit
# the `newSequence` step at the end of `home` sequence.
- awaitVar: commented
# Choose one of the movies (including the bad ones, for simplicity)
- randomItem: selectedMovie <- movies
- json:
fromVar: selectedMovie
query: .name
# This sequence is executed only once so we can use global var.
toVar: movieName
# Finally, go watch the movie!
- httpRequest:
GET: /quickstarts/choose-movie/movie/${urlencode:movieName}/watch
sync: true
Start the server and fire the scenario the usual way:
# start the server to interact with
podman run --rm -d -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
# start Hyperfoil CLI
bin/cli.sh
[hyperfoil]$ start-local
...
[hyperfoil@in-vm]$ upload .../choose-movie.hf.yaml
...
[hyperfoil@in-vm]$ run
...
Is this scenario too simplistic? Let’s define phases…
4 - Phases - basics
So far the benchmark contained only one type of load; certain number of users hitting the system, doing always the same (though data could be randomized). In practice you might want to simulate several types of workloads at once: in an eshop users would come browsing or buying products, and operators would restock the virtual warehouse.
Also, driving constant load may not be the best way to run the benchmark: often you want to slowly ramp the load up to let the system adjust (scale up, perform JIT, fill pools) and push the full load only after that. When trying to find system limits, you do the same repetitevely - ramp up the load, measure latencies and if the system meets SLAs (latencies below limits) continue ramping up the load until it breaks.
In Hyperfoil, this all is expressed through phases. We’ve already seen phases in the first quickstart as we wanted to execute a non-default type of load - running the workload only once. Let’s take a look on the “eshop” case first:
# This benchmark simulates operations in an eshop, with browsing/shopping users
# and operators restocking the warehouse.
name: eshop
http:
host: http://localhost:8080
sharedConnections: 80
phases:
# This defines a workload where users just look through the pages.
- browsingUser:
# This is the default type of workload, starting constant number of users
# each second. Note that we don't speak about 'requests per second' as
# the scenario may issue any number of requests.
constantRate:
duration: 10s
usersPerSec: 10
scenario:
# Browse is the name of our only sequence. We will avoid steps generating
# random data for browsing for the sake of brevity.
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
# Workload simulating users that are going to buy something
- buyingUser:
constantRate:
# The length of this phase is not synchronized with other phases.
# You might think that this is too flexible at first.
duration: 10s
usersPerSec: 5
scenario:
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
handler:
body:
json:
query: .[].id
# This is a shortcut to store in array-typed variable
# `itemIds` holding at most 10 elements.
toArray: itemIds[10]
- buy:
# Pick id for a random item
- randomItem: itemId <- itemIds
- httpRequest:
POST: /quickstarts/eshop/items/${itemId}/buy
- operator:
# This is a different type of phase, running fixed number of users.
# It is what most benchmarks do when you set number of threads; here we use
# that as we know that we have fixed number of employees (operators) who
# are restocking the warehouse.
always:
users: 5
duration: 10s
scenario:
- restock:
# Select an id for random item to restock
# Variables in different scenarios are completely unrelated.
- randomInt: itemId <- 1 .. 999
- randomInt: units <- 1 .. 10
- httpRequest:
POST: /quickstarts/eshop/items/${itemId}/restock
body:
# We are using url-encoded form data
form:
- name: addUnits
fromVar: units
# Operators need some pauses - otherwise we would start another
# scenario execution (and fire another request) right away.
- thinkTime:
duration: 2s
Start the same server as you did in the previous quickstarts:
podman run --rm -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
In next quickstart you’ll learn how to repeat and link the phases.
5 - Phases - advanced
Previous quickstart presented a benchmark with three phases that all started at the same moment (when the benchmark was started) and had the same duration - different phases represented different workflows (types of user). In this example we will adjust the benchmark to scale the load gradually up.
At this point it would be useful to mention the lifecycle of phases; phase is in one of these states:
- not started: As the name clearly says, the phase is not yet started.
- running: The agent started running the phase, i.e., performing the configured load.
- finished: When the
duration
elapses, no more new users are started. However, some might be still executing their scenarios. - terminated: When all users complete their scenarios the phase becomes terminated. Users may be forcibly interrupted by setting
maxDuration
on the phase. - cancelled If the benchmark cannot continue further, all remaining stages are cancelled.
Let’s take a look into the example, where we’ll slowly (over 5 seconds) increase load to 10+5 users/sec, run with this load for 10 seconds, again increase it by another 10+5 users/sec and so forth until we reach 100+50 users per second. As we define maxIterations
for these phases the benchmark will actually contain phases browsingUserRampUp/0
, browsingUserRampUp/1
, browsingUserRampUp/2
and so forth.
name: eshop-scale
http:
host: http://localhost:8080
sharedConnections: 80
phases:
- browsingUserRampUp:
# This type of phase is similar to constantRate in the way how new users
# are started but gradually increases the rate from `initialUsersPerSec`
# to `targetUsersPerSec`.
increasingRate:
duration: 5s
# In Hyperfoil, everything is pre-allocated = limited in size. Here we'll
# set that we won't run more than 10 iterations of this phase.
maxIterations: 10
# Number of started users per sec increases with the iteration; in first
# iteration we'll go from 0 to 10 users/second, in second from 10 to 20
# and in last (10th) we'll reach 100 users/second.
initialUsersPerSec:
base: 0
increment: 10
targetUsersPerSec:
base: 10
increment: 10
# Nth iteration of this phase will start when (N-1)th iteration of other
# steady-state phases are finished. First iteration can start
# immediatelly, of course.
startAfter:
- phase: browsingUserSteady
iteration: previous
- phase: buyingUserSteady
iteration: previous
# The &browsingUser syntax below creates YAML alias: we can later
# reference this scenario and it will be used verbatim in another phase.
# It is possible to use aliases for both scenarios and sequences.
scenario: &browsingUser
# We'll use the same scenario as in eshop.hf.yaml
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
- browsingUserSteady:
constantRate:
duration: 10s
maxIterations: 10
usersPerSec:
base: 10
increment: 10
# Nth iteration of this phase will start when Nth iteration of ramp-up
# phases is finished.
# Note that there's implicit rule that Nth iteration of given phase will
# start only after (N-1)th iteration terminates.
startAfter:
- phase: browsingUserRampUp
iteration: same
- phase: buyingUserRampUp
iteration: same
# This refers to the alias created above; in steady state we'll use the
# same scenario.
scenario: *browsingUser
# These two phases will be very similar to browsingUserSteady and RampUp
- buyingUserRampUp:
increasingRate:
duration: 5s
maxIterations: 10
initialUsersPerSec:
base: 0
increment: 5
targetUsersPerSec:
base: 5
increment: 5
startAfter:
- phase: browsingUserSteady
iteration: previous
- phase: buyingUserSteady
iteration: previous
# Again we'll use the same scenario as in eshop.hf.yaml
scenario: &buyingUser
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
handler:
body:
json:
query: .[].id
toArray: itemIds[10]
- buy:
- randomItem: itemId <- itemIds
- httpRequest:
POST: /quickstarts/eshop/items/${itemId}/buy
- buyingUserSteady:
constantRate:
duration: 10s
maxIterations: 10
usersPerSec:
base: 5
increment: 5
startAfter:
- phase: browsingUserRampUp
iteration: same
- phase: buyingUserRampUp
iteration: same
scenario: *buyingUser
# Operator phase is omitted for brevity as we wouldn't scale that up
Don’t forget to start the mock server as we’ve used in the previous quickstart.
podman run --rm -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
Synchronizing multiple workloads across iteration can become a bit cumbersome. That’s why we can keep similar types of workflow together, and split the phase into forks. In fact forks will become different phases, but these will be linked together so that you can refer to all of them as to a single phase. Take a look at the benchmark rewritten to use forks:
name: eshop-forks
http:
host: http://localhost:8080
sharedConnections: 80
phases:
- rampUp:
increasingRate:
duration: 5s
maxIterations: 10
# Note that we have increased both the base and increment from 10 and 5
# to 15. This value is split between the forks based on their weight.
initialUsersPerSec:
base: 0
increment: 15
targetUsersPerSec:
base: 15
increment: 15
startAfter:
phase: steadyState
iteration: previous
forks:
browsingUser:
weight: 2
scenario: &browsingUser
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
buyingUser:
weight: 1
scenario: &buyingUser
- browse:
- httpRequest:
GET: /quickstarts/eshop/items
handler:
body:
json:
query: .[].id
toArray: itemIds[10]
- buy:
- randomItem: itemId <- itemIds
- httpRequest:
POST: /quickstarts/eshop/items/${itemId}/buy
- steadyState:
constantRate:
duration: 10s
maxIterations: 10
usersPerSec:
base: 15
increment: 15
startAfter:
phase: rampUp
iteration: same
forks:
browsingUser:
weight: 2
scenario: *browsingUser
buyingUser:
weight: 1
scenario: *buyingUser
# Operator phase is omitted for brevity as we wouldn't scale that up
This definition will create phases rampUp/0/browsingUser
, rampUp/0/buyingUser
, rampUp/1/browsingUser
etc. - you’ll see them in statistics.
You could orchestrate the phases as it suits you, using startAfter
, startAfterStrict
(this requires the referenced phase to me terminated instead of finished as with startAfter
) or startTime
with relative time since benchmark start.
This sums up basic principles, in next quickstart you’ll see how to start and use Hyperfoil in distributed mode.
6 - Running the server
Until now we have always started our benchmarks using an embedded controller in the CLI, using the start-local
command. This spawns a server in the CLI JVM. CLI communicates with it using standard REST API, though the server port is randomized and listens on localhost only. All the benchmarks and run results are also stored in /tmp/hyperfoil/
- you can change the directory as an argument to the start-local
command.
While the embedded controller might be convenient for a quick test or when developing the scenario it’s not something that you’d use for a full-fledged benchmark.
When testing a reasonably performing system you need multiple nodes driving the load - we call them agents. These agents sync up, receive commands and report statistics to a master node, the controller. This node exposes a RESTful API to upload & start the benchmark, watch its progress and download results.
There are two other scripts in the bin/
directory:
standalone.sh
starts both the controller and (one) agent in a single JVM. This is not too different from the controller embedded in CLI.controller.sh
starts clustered Vert.x and deploys the controller. Agents are started as needed in different nodes. You’ll see this in the next quickstart.
Also note that it is possible to run Hyperfoil in Openshift.
Open two terminals; in one terminal start the standalone server and in second terminal start the CLI.
bin/standalone.sh
and
bin/cli.sh
Then, let’s try to connect to the server (by default running on http://localhost:8090
) and upload the single-request
benchmark:
# This is the name of the benchmark. It's recommended to keep this in sync with
# name of this file, adding extension `.hf.yaml`.
name: single-request
# We must define at least one HTTP target, in this case it becomes a default
# for all HTTP requests.
http:
host: http://hyperfoil.io
# Simulation consists of phases - potentially independent workloads.
# We'll discuss phases more in detail in next quickstarts.
phases:
# `example` is the name of the single phase in this benchmark.
- example:
# `atOnce` with `users: 1` results in running the scenario below just once
atOnce:
users: 1
scenario:
# The only sequence in this scenario is called `test`.
- test:
# In the only step in this sequence we'll do a HTTP GET request
# to `http://hyperfoil.io/`
- httpRequest:
GET: /
# Inject helpers to make this request synchronous, i.e. keep
# the sequence blocked until Hyperfoil processes the response.
sync: true
From the second terminal, the one running the Hyperfoil CLI, issue the following commands:
[hyperfoil@localhost]$ connect
Connected! Server has these agents connected:
* localhost[REGISTERED]
[hyperfoil@localhost]$ upload .../single-request.hf.yaml
Loaded benchmark single-request, uploading...
... done.
[hyperfoil@localhost]$ run single-request
Started run 0001
When you switch to the first terminal (the one running the controller), you can see in the logs that the benchmark definition was stored on the server, the benchmark has been executed and its results have been stored to disk. Hyperfoil by default stores benchmarks in directory /tmp/hyperfoil/benchmark
and data about runs in /tmp/hyperfoil/run
; check it out:
column -t -s , /tmp/hyperfoil/run/0001/stats/total.csv
Phase Name Requests Responses Mean Min p50.0 p90.0 p99.0 p99.9 p99.99 Max MeanSendTime ConnFailure Reset Timeouts 2xx 3xx 4xx 5xx Other Invalid BlockedCount BlockedTime MinSessions MaxSessions
example test 1 1 267911168 267386880 268435455 268435455 268435455 268435455 268435455 268435455 2655879 0 0 0 0 1 0 0 0 0 0 0
Reading CSV/JSON files directly is not too comfortable; you can check the details through CLI as well:
[hyperfoil@localhost]$ stats
Total stats from run 002D
Phase Sequence Requests Mean p50 p90 p99 p99.9 p99.99 2xx 3xx 4xx 5xx Timeouts Errors
example:
test: 1 267.91 ms 268.44 ms 268.44 ms 268.44 ms 268.44 ms 268.44 ms 0 1 0 0 0 0
By the time you type the stats
command into CLI the benchmark is already completed and the CLI shows stats for the whole run. Let’s try running the {% include example_link.md src=‘eshop-scale.hf.yaml’ %} we’ve seen in previous quickstart; this will give us some time to observe on-line statistics as the benchmark is progressing:
podman run --rm -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
[hyperfoil@localhost]$ upload .../eshop-scale.hf.yaml
Loaded benchmark eshop-scale, uploading...
... done.
[hyperfoil@localhost]$ run eshop-scale
Started run 0002
Run 0002, benchmark eshop-scale
...
Here the console would automatically jump into the status
command, displaying the progress of the benchmark online. Press Ctrl+C to cancel that (it won’t stop the benchmark run) and run the stats
command:
[hyperfoil@localhost]$ stats
Recent stats from run 0002
Phase Sequence Requests Mean p50 p90 p99 p99.9 p99.99 2xx 3xx 4xx 5xx Timeouts Errors
buyingUserSteady/000:
buy: 8 1.64 ms 1.91 ms 3.05 ms 3.05 ms 3.05 ms 3.05 ms 8 0 0 0 0 0
browse: 8 2.13 ms 2.65 ms 3.00 ms 3.00 ms 3.00 ms 3.00 ms 8 0 0 0 0 0
browsingUserSteady/000:
browse: 8 2.74 ms 2.69 ms 2.97 ms 2.97 ms 2.97 ms 2.97 ms 8 0 0 0 0 0
Press Ctr+C to stop watching...
You can go back to the run progress using the status
command (hint: use status --all
to display all phases, including those not started or already terminated):
[hyperfoil@localhost]$ status
Run 0002, benchmark eshop-scale
Agents: localhost[INITIALIZED]
Started: 2019/04/15 16:27:24.526
NAME STATUS STARTED REMAINING FINISHED TOTAL DURATION
browsingUserRampUp/006 RUNNING 16:28:54.565 2477 ms
buyingUserRampUp/006 RUNNING 16:28:54.565 2477 ms
Press Ctrl+C to stop watching...
Since we are showing this quickstart running the controller and CLI on the same machine it’s easy to fetch results locally from /tmp/hyperfoil/run/XXXX/...
. To save you SSHing into the controller host and finding the directories in a ’true remote’ case there’s the export
command; This fetches statistics to your computer where you’re running CLI. You can chose between default JSON format (e.g. export 0002 -f json -d /path/to/dir
) and CSV format (export 0002 -f csv -d /path/to/dir
) - the latter packs all CSV files into single ZIP file for your convenience.
When you find out that the benchmark is not going well, you can terminate it prematurely:
[hyperfoil@localhost]$ kill
Kill run 0002, benchmark eshop-scale(phases: 2 running, 0 finished, 40 terminated) [y/N]: y
Killed.
In the next quickstart we will deal with starting clustered Hyperfoil.
7 - Clustered mode
Previously we’ve learned to start Hyperfoil in standalone server mode, and to do some runs through CLI. In this quickstart we’ll see how to run your benchmark distributed to several agent nodes.
Hyperfoil operates as a cluster of Vert.x. When the benchmark is started, it deploys agents on other nodes according to the benchmark configuration - these are Vert.x nodes, too. Together controller and agents form a cluster and communicate over the event bus.
In this quickstart we’ll use the SSH deployer; make sure your machine has SSH server running on port 22 and you can login using your pubkey ~/.ssh/id_rsa
. The SSH deployer copies the necessary JARs to /tmp/hyperfoil/agentlib/
and starts the agent there. For instructions to run Hyperfoil in Kubernetes or Openshift please consult the Installation docs.
When we were running in the standalone or local mode we did not have to set any agents in the benchmark definition. That changes now as we need to inform the controller where the agents should be deployed. Let’s see a benchmark - two-agents.hf.yaml
that has those agents defined.
name: two-agents
# List of agents the Controller should deploy
agents:
# This defines the agent using SSH connection to localhost, port 22
agent-one: localhost:22
# Another agent on localhost, this time defined using properties
agent-two:
host: localhost
port: 22
http:
host: http://localhost:8080
usersPerSec: 10
duration: 10s
scenario:
- test:
- httpRequest:
GET: /
The load the benchmark generates is evenly split among the agents, so if you want to use another agent, you don’t need to do any calculations - just add the agent and you’re good to go.
Open three terminals; in the first start the controller using bin/controller.sh
, in second one open the CLI with bin/cli.sh
and in the third one start the example workload server:
podman run --rm -p 8080:8083 quay.io/hyperfoil/hyperfoil-examples
Connect, upload, start and check out the benchmark using CLI exactly the same way as we did in the previous quickstart:
[hyperfoil@localhost]$ connect
Connected!
[hyperfoil@localhost]$ upload .../two-agents.hf.yaml
Loaded benchmark two-agents, uploading...
... done.
[hyperfoil@localhost]$ run two-agents
Started run 004A
[hyperfoil@localhost]$ status
Run 004A, benchmark two-agents
Agents: agent-one[STARTING], agent-two[STARTING]
Started: 2019/04/17 17:08:19.703 Terminated: 2019/04/17 17:08:29.729
NAME STATUS STARTED REMAINING FINISHED TOTAL DURATION
main TERMINATED 17:08:19.708 17:08:29.729 10021 ms (exceeded by 21 ms)
[hyperfoil@localhost]$ stats
Total stats from run 004A
Phase Sequence Requests Mean p50 p90 p99 p99.9 p99.99 2xx 3xx 4xx 5xx Timeouts Errors
main:
test: 106 3.12 ms 2.83 ms 3.23 ms 19.53 ms 25.30 ms 25.30 ms 106 0 0 0 0 0
You see that we did 106 requests which fits the assumption about running 10 user sessions per second over 10 seconds, while we have used 2 agents.
Vert.x clustering is using Infinispan and JGroups; depending on your networking setup it might not work out-of-the-box. If you experience any trouble, check out the FAQ.
Next quickstart will get back to the scenario definition; we’ll show you how to extend Hyperfoil with custom steps and handlers.
8 - Custom components
Hyperfoil offers some basic steps to do HTTP requests, generate data, alter control flow in the scenario etc., but your needs may surpass the features implemented so far. Also, it might be just easier to express your logic in Java code than combining steps in the YAML. The downside is reduced ability to reuse and more tight dependency on Hyperfoil APIs.
This quickstart will show you how to extend Hyperfoil with custom steps and handlers. As we use the standard Java ServiceLoader
approach, after you build the module you should drop it into extensions
directory. (Note: if you upload the benchmarks through CLI you need to put it to both the machine where you run the CLI and to the controller.)
Each extension will consist of two classes:
Builder
, is loaded as service and creates the immutable extension instance- extension (
Step
,Action
or handler)
Let’s start with a io.hyperfoil.api.config.Step
implementation. The interface has single method invoke(Session)
that should return true
if the step was executed and false
if its execution has been blocked and should be retried later. In case that the execution is blocked the invocation must not have any side effects - e.g. if the step is fetching objects from some pools and one of the pools is depleted, it should release the already acquired objects back to the pool.
We’ll create a step that will divide variable from a session by a (configurable) constant and store the result in another variable.
public class DivideStep implements Step {
// All fields in a step are immutable, any state must be stored in the Session
private final ReadAccess fromVar;
private final IntAccess toVar;
private final int divisor;
public DivideStep(ReadAccess fromVar, IntAccess toVar, int divisor) {
// Variables in session are not accessed directly using map lookup but
// through the Access objects. This is necessary as the scenario can use
// some simple expressions that are parsed when the scenario is built
// (in this constructor), not at runtime.
this.fromVar = fromVar;
this.toVar = toVar;
this.divisor = divisor;
}
@Override
public boolean invoke(Session session) {
// This step will block until the variable is set, rather than
// throwing an error or defaulting the value.
if (!fromVar.isSet(session)) {
return false;
}
// Session can store either objects or integers. Using int variables is
// more efficient as it prevents repeated boxing and unboxing.
int value = fromVar.getInt(session);
toVar.setInt(session, value / divisor);
return true;
}
...
Then we need a builder class that will allow us to configure the step. To keep related classes together we will define it as inner static class:
public class DivideStep implements Step {
...
// Make this builder loadable as service
@MetaInfServices(StepBuilder.class)
// This is the step name that will be used in the YAML
@Name("divide")
public static class Builder extends BaseStepBuilder<Builder> implements InitFromParam<Builder> {
// Contrary to the step fields in builder are mutable
private String fromVar;
private String toVar;
private int divisor;
// Let's permit a short-form definition that will store the result
// in the same variable. Note that the javadoc @param is used to generate external documentation.
/**
* @param param Use myVar /= constant
*/
@Override
public Builder init(String param) {
int divIndex = param.indexOf("/=");
if (divIndex < 0) {
throw new BenchmarkDefinitionException("Invalid inline definition: " + param);
}
try {
divisor(Integer.parseInt(param.substring(divIndex + 2).trim()));
} catch (NumberFormatException e) {
throw new BenchmarkDefinitionException("Invalid inline definition: " + param, e);
}
String var = param.substring(0, divIndex).trim();
return fromVar(var).toVar(var);
}
// All fields are set in fluent setters - this helps when the scenario
// is defined through programmatic configuration.
// When parsing YAML the methods are invoked through reflection;
// the attribute name is used for the method lookup.
public Builder fromVar(String fromVar) {
this.fromVar = fromVar;
return this;
}
public Builder toVar(String toVar) {
this.toVar = toVar;
return this;
}
// The parser can automatically convert primitive types and enums.
public Builder divisor(int divisor) {
this.divisor = divisor;
return this;
}
@Override
public List<Step> build() {
// You can ignore the sequence parameter; this is used only in steps
// that require access to the parent sequence at runtime.
if (fromVar == null || toVar == null || divisor == 0) {
// Here is a good place to check that the attributes are sane.
throw new BenchmarkDefinitionException("Missing one of the required attributes!");
}
// The builder has a bit more flexibility and it can create more than
// one step at once.
return Collections.singletonList(new DivideStep(
SessionFactory.readAccess(fromVar), SessionFactory.intAccess(toVar), divisor));
}
}
...
As the comments say, the builder is using fluent setter syntax to set the attributes. When you want to nest attributes under another builder, you can just add parameter-less method FooBuilder foo()
the returns an instance of FooBuilder
; the parser will fill this instance as well. There are some interfaces your builder can implement to accept lists or different structures, but the description is out of scope of this quickstart.
The builder class has two annotations: @Name
which specifies the name we’ll use in YAML as step name, and @MetaInfServices
with StepBuilder.class
as the parameter. If you were to implement other type of extension, this would be Action.Builder.class
, Request.ProcessorBuilder.class
etc. In order to record the service in META-INF directory in the jar you must also add this dependency to your module:
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<optional>true</optional>
</dependency>
The whole class can be inspected here and it is already included in the extensions
directory. You can try running bin/standalone.sh
, upload and run divide.hf.yaml
. You should see about 5 log messages in the server log.
# This benchmark demonstrates custom steps
name: divide
http:
host: http://localhost:8080
usersPerSec: 1
duration: 5s
scenario:
- test:
- setInt: foo <- 33
- divide: foo /= 3
- log:
message: Foo is {}
vars:
- foo
There are several other integration points but Step
:
io.hyperfoil.api.session.Action
is very similar to step, but it does not allow blocking. ImplementAction.BuilderFactory
to define new actions.StatusHandler
,HeaderHandler
andBodyHandler
inio.hyperfoil.api.http
package process different stages of HTTP response parsing. All these haveBuilderFactory
inner interface for you to implement.io.hyperfoil.api.connection.Processor
performs later generic stages of response processing. As this interface is generic, there are two factories that you could use:i.h.a.c.Request.ProcessorBuilderFactory
andi.h.a.c.HttpRequest.ProcessorBuilderFactory
.
There is quite some boilerplate code in the process of creating a new component; that’s why you can use Hyperfoil Codegen Maven plugin to scaffold the basic outline for you. Go to the module where you want the component generated and run:
mvn io.hyperfoil:hyperfoil-codegen-maven-plugin:skeleton
The plugin will ask you for the package name, component name and type and write down the source code skeleton. You can provide the parameters right on commandline like
mvn io.hyperfoil:hyperfoil-codegen-maven-plugin:skeleton \
-Dskeleton.package=foo.bar -Dskeleton.name=myComponent -Dskeleton.type=step
Note
The generator does not add the dependency toorg.kohsuke.metainf-services:metainf-services
to this module’s pom.xml
, you need to do that manually.If you add io.hyperfoil
as a plugin group to your $HOME/.m2/settings.xml
like this:
<settings>
<pluginGroups>
<pluginGroup>io.hyperfoil</pluginGroup>
</pluginGroups>
...
</settings>
you can use the short syntax for the generator:
mvn hyperfoil-codegen:skeleton -Dskeleton.name=....
See also further information about custom extensions development.
This is the last quickstart in this series; if you seek more info check out the documentation or talk to us on GitHub Discussions.