The ramp
"Can this broker do 300,000 a second?" has an honest answer only at a rate where every objective still holds. A ramp finds that rate: it steps the offered rate up until a rule fires, and reports the last step where nothing did.
java -jar acemq-workload.jar -f examples/17-ramp.yaml --report reports/ --format html,md,jsonramp: ramp-to-ceiling
Ramp{ramp-to-ceiling from 2000/s by 25.0% to 200000/s, 20s a step, refined 2x}
offered result published consumed p50 p90 p99 p99.9 stopped by
2,000/s ok 2,000/s 2,000/s 1.1ms 1.6ms 2.4ms 4.0ms
2,500/s ok 2,500/s 2,500/s 1.2ms 1.8ms 3.0ms 5.1ms
...
9,538/s ok 9,538/s 9,538/s 2.9ms 7.7ms 21.4ms 38.0ms
11,923/s FAILED 11,923/s 11,104/s 212.6ms 402.3ms 470.9ms 488.2ms p99<50.0ms, kept-pace
* 10,730/s FAILED 10,730/s 10,511/s 41.0ms 88.1ms 96.4ms 101.3ms p99<50.0ms
* 10,134/s ok 10,134/s 10,134/s 3.4ms 11.2ms 33.0ms 44.7ms
* refinement step, between the last good rate and the first bad one
highest sustained rate: 10,134/s
stopped by: p99<50.0ms, kept-pace at 11,923/s: p99 was 470.9ms, against a budget of 50.0ms
objective: ceiling at least 5,000/s
result: PASSED
(Illustrative: a laptop, broker and generator sharing a CPU.)
The p99 column is the other half of the answer. The rate at which the system is still comfortable is usually well below the one where it falls over, and the ramp shows how latency bent on the way up.
Every step is an ordinary run
A step is the workload run at one fixed rate through the same open-loop engine as any other run: its own warm-up, then a measured window, with latency taken from when each message was due rather than when it went out. A broker that stalls shows the stall as latency; it cannot slow the generator down with it and report a flattering number. See measurement for the arithmetic.
So a ramp is not a second engine with a moving rate. It is a loop over runs, and every step is judged by exactly the rules a single run is.
What stops it
The first bad step ends the climb. A step is bad when:
| an objective is missed | anything under expect: — p99Below: 50ms at
that rate |
| a rule fires | any rule, default or loaded
from the file, with severity failed or
invalid |
kept-pace |
added to every step: what was delivered fell more than
tolerance (5%) short of what was offered |
| failed publishes | any, unless under maxErrorRate |
| a step is invalid | the generator could not keep its schedule, or the broker raised an alarm |
Warnings never stop it: a 20-second step warns that it was short on every step.
kept-pace reads the one-second samples, not the
totals. A run's totals include the drain after the window, and
five seconds of drain lets a slow consumer catch up on a step's backlog
and look as if it kept pace. Without this rule a slow consumer would
never stop a ramp at all, because a filling queue is only a warning on a
single run.
An invalid step means the ceiling is not the broker's. If the generator fell behind its schedule at 60,000/s, the ramp found the generator's limit — and says so, rather than reporting 55,000/s as the broker's ceiling. Raise publisher threads or run the generator elsewhere, and ramp again.
maxRate reached with everything holding ends it too, and
the report says the ceiling is at least that rather than
inventing a number past it.
Refinement
refine: N bisects N times between the last good step and
the first bad one, so a coarse step does not leave the answer that
coarse. Refinement steps are marked * and listed after the
climb.
A refinement step that follows an overloaded one inherits whatever backlog it left in the queue. That can only make the refinement step look worse, never better: a refined ceiling can be low, never high.
In a workload file
ramp:
start: 2000 # the first step, msg/s. Default: publishers.rate
step: 25% # each step adds 25%, or an absolute rate: step: 1000
stepFor: 20s # each step's measured window. Default: runFor
warmup: 5s # before each step's window. Default: warmup
maxRate: 200000 # never offer more than this
maxErrorRate: 0.1% # failed publishes allowed per step. Default 0
tolerance: 5% # delivered may fall this far short of offered. Default 5%
refine: 2 # bisection steps after the climb. Default 0
ceilingAtLeast: 5000 # the gate: exit 1 if the ceiling is lowerOnly step is required, given a rate on the publishers.
Shares are written 5% or 0.05. A step must be
at least 5 seconds: kept-pace averages one-second readings
and drops the first, which straddles the counter reset.
The ramp sets the publishers' rate on every step, so a rate in the
file is only the default start, and unthrottled is
replaced. Everything else — topology, consumers, objectives, rules — is
the same on every step.
In a suite, ramp:
at the top level ramps every entry, and an entry can carry its own. As
with any block, an entry's ramp: replaces the top level's
rather than merging into it.
On the command line
--ramp is the ramp: block written on one
line, for ramping a committed workload file without editing it:
java -jar acemq-workload.jar -f workload.yaml \
--ramp start=1000,step=10%,stepFor=20s,maxRate=50000,refine=3It goes through the same parser as the file, so the two cannot disagree. Where the file already has a ramp, the flag wins key by key. Every report records the settings the ramp ran with, so the run is still reproducible from the report. Scenario files are refused: a scenario has many producers, and which of their rates a ramp would raise is not something to guess.
From Java
RampReport ramp = Ramp.of(workload)
.startAt(2_000)
.stepByPercent(25) // or .stepBy(1_000)
.stepFor(Duration.ofSeconds(20))
.warmupEachStep(Duration.ofSeconds(5))
.maxRate(200_000)
.refine(2)
.run("amqp://guest:guest@localhost:5672");
ramp.highestSustainedRate(); // 10_134
ramp.stoppedBy(); // "p99<50.0ms, kept-pace"
ramp.isValid(); // false when an invalid step ended it
ramp.steps(); // each with its rate and its WorkloadReportramp.stop() ends it after the step running.
Exit codes
0 |
the ceiling met ceilingAtLeast; or, with none set, the
ramp was valid and sustained its first step |
1 |
a sound ramp's ceiling fell short of ceilingAtLeast, or
the start rate already missed |
2 |
an invalid step ended it and the gate was not met — the ceiling found is the generator's or an alarm's |
3 |
the ramp: block is wrong |
4 |
the broker could not be reached |
A ramp that met ceilingAtLeast passes however it ended:
a generator that gave out at 60,000/s has still shown the broker takes
50,000.
Reports
The terminal shows the table above. The HTML report
draws the curve — delivered against offered, with the diagonal a perfect
system would follow, and p99 against offered — with failed steps hollow,
then the step table and the finding that stopped it.
Markdown has the table and the p99 curve as a text bar
chart. JSON carries a ramps array beside
workloads:
"ramps": [
{
"name": "ramp-to-ceiling",
"settings": "Ramp{ramp-to-ceiling from 2000/s by 25.0% to 200000/s, 20s a step, refined 2x}",
"valid": true,
"passed": true,
"verdict": "PASSED",
"highestSustainedRatePerSecond": 10134,
"ceilingAtLeast": 5000,
"stoppedBy": "p99<50.0ms, kept-pace",
"stopReason": "p99<50.0ms, kept-pace at 11,923/s: p99 was 470.9ms, against a budget of 50.0ms",
"steps": [
{"offeredRatePerSecond": 2000, "refinement": false, "sustained": true, "valid": true,
"publishRatePerSecond": 2000.0, "consumeRatePerSecond": 2000.0, "failedTotal": 0,
"endToEnd": {"p50Micros": 1100, "p90Micros": 1600, "p99Micros": 2400, "p999Micros": 4000, "maxMicros": 6100}}
]
}
]ramps is absent when nothing ramped, so a pipeline
reading workloads sees exactly what it saw before.
What it is not
Not a soak, and not a distributed load: one generator, one workload, rates in a straight climb. A ceiling measured on a laptop is the laptop's.