Chapter 25Lesson 01~180 minutes

Distributed Testing, Remote Engines, Network Topology, and Synchronization: Core Concepts and Mental Model

Chapter 24 turned one JMeter generator into a measured system: JTL, generator telemetry, Backend Listener metrics and SUT metrics were all distinct signals. Distributed testing adds more generators and a second control plane. The hard part is not “starting two machines.” It is preserving the exact workload, data, code, ports, trust boundary and evidence on every engine while remembering one rule that surprises many beginners: JMeter does not divide your Thread Group across remote engines—every engine runs the full supplied plan.

Remote enginesRMI SSLFull-plan replicationData shardingAggregate results

Learning objectives

  • Explain controller/client versus remote-engine responsibilities.
  • Calculate total distributed workload as the sum of full-plan engine workloads.
  • Distinguish JMX transfer from external data/plugin/property distribution.
  • Understand RMI registry, server-engine and reverse callback network paths.
  • Inspect versions, files, properties, ports, logs and results before scaling.
  • Separate target capacity from controller/engine/network/result-transfer capacity.

1. The practical problem: scaling the injector can silently change the experiment

A local plan is configured for 1,000 threads. An engineer adds six remote engines expecting about 167 threads per engine. JMeter actually starts the full 1,000-thread plan on each engine: 6,000 configured threads. If the data file contains only 1,000 unique users or the SUT was authorized for 1,000 users, the mistake is both a validity and a safety failure.

Mandatory chapter boundary: the executable lab stays on loopback/private isolated nodes only. The target is 127.0.0.1:8025; the two-engine checkpoint configures only 2 threads ×3 loops per engine = 12 total samples. RMI must not be exposed to the public internet. No real credentials, production target, shared test data or unbounded remote hosts are used.

2. Mental model: controller sends commands; engines generate load

Remote-mode execution and evidence flow

Distributed JMeter is a load-generation system with multiple execution and evidence boundaries. The controller sends one test plan to each engine; every engine executes the full configured workload, reads its own local files/properties, targets the SUT independently, and returns or exports results through a separate network path.

flowchart TD
C[Controller/client
JMX + remote command + global properties] -->|RMI over SSL| A[Engine A
full JMX]
C -->|RMI over SSL| B[Engine B
full JMX]
A -->|reads local shard A| DA[data/engine-a.csv]
B -->|reads local shard B| DB[data/engine-b.csv]
A -->|HTTP target traffic| T[Authorized SUT]
B -->|HTTP target traffic| T
A -->|remote sample results| C
B -->|remote sample results| C
C --> J[Central aggregate JTL + controller jmeter.log]
A --> LA[engine-a jmeter.log + generator state]
B --> LB[engine-b jmeter.log + generator state]
T --> E[target telemetry/events]

The controller holds the JMX and starts remote engines using secured RMI. It sends the same plan to both engines. Engine A and Engine B each instantiate their own Thread Groups, variables, protocol connections and local file handles; therefore each produces its own configured load. External CSV files are not part of the JMX transfer, so each engine reads an explicitly staged shard. Sample results flow back through the RMI result path and the controller can write one aggregate JTL. Engine logs/resource state remain engine-local. The target sees the sum of both engines.

3. Distributed load math

For a closed Thread Group with one sampler:

per_engine_threads = Thread Group threads
per_engine_samples = threads × loops × samplers_per_loop
total_threads = sum(per_engine_threads across engines)
total_samples = sum(per_engine_samples across engines)

Example:
2 engines × (2 threads × 3 loops × 1 sampler) = 12 total samples
6 engines × 1000 configured threads = 6000 configured threads

Do the total-load calculation before starting any server. If engines intentionally use different properties, calculate each term separately rather than multiplying by engine count.

4. Controller/client versus remote engine

Controller/client Remote engine
Loads JMX, selects remote hosts, initiates/controls test. Receives the JMX and executes the full plan.
Supplies -G global properties to servers. Reads engine-local startup properties and distributed global properties.
Receives remote samples and can write aggregate -l JTL. Creates actual protocol sessions/target traffic.
Owns reverse RMI callback listener ports. Owns RMI registry/server-engine ports.
Can become CPU/network/result-processing bottleneck. Can become CPU/GC/network/socket/data bottleneck.

5. Version and plugin identity is part of correctness

Current JMeter remote guidance requires exactly the same JMeter version on client and servers and discourages different Java versions. The same principle applies to third-party plugins, user JARs, Groovy dependencies and certificates: a remote engine can only instantiate classes that exist on that engine.

The mandatory lab uses JMeter 5.6.3, Java 17 and no third-party plugins, so the plugin manifest is intentionally empty. Production should compare file names/hashes for all non-core additions.

6. What is transferred—and what is not

The controller sends the test plan to servers automatically. It does not send CSV data files. A CSV Data Set relative path must resolve on each server in the correct server-side directory; absolute paths work only when that engine actually has the corresponding path.

Do not assume JMX transfer means “the whole project directory was deployed.” Data, plugins, user JARs, certificates and other files require an explicit deployment/manifest step.

7. Local, remote-global and engine-local properties

  • -Jname=value changes a property in the process where the command runs—the controller.
  • -Gname=value sends a JMeter property to all remote servers.
  • Engine-specific properties such as engine.id or data.file should be set on each server before/startup, not with one global -G value.
  • Thread-local variables still belong to each engine/thread; they are not shared across JVMs.

In the lab, both engines receive threads=2/loops=3 through -G, but engine-a starts with its own engine.id/data.file and engine-b starts with different values.

8. RMI network paths and SSL

JMeter remote mode uses RMI. A controller connects to each server registry (default 1099 unless changed), then also needs reverse connections for returned sample/thread events. Since JMeter 4.0 the default RMI transport is SSL.

For firewall-friendly private environments, explicitly set:

  • one registry port per server instance;
  • server.rmi.localport for the server engine;
  • client.rmi.localport on the controller for up to three consecutive callback ports;
  • the advertised RMI hostname/IP reachable only inside the authorized network;
  • a controlled keystore/truststore shared according to your trust design.
Do not normalize server.rmi.ssl.disable=true. It exists, but the chapter keeps SSL enabled. The generated seven-day/default-passphrase keystore is a lab convenience, not a production PKI standard.

9. Result transfer can become the bottleneck

Remote sample delivery consumes engine/controller/network resources. Current default sender mode is StrippedBatch, which strips successful response bodies and batches results. Standard sends each sample synchronously; Hold retains all samples in memory until the end and is discouraged. Asynch uses a queue that can eventually fill and block sampler threads.

A slow controller or return network can therefore reduce achieved load even when the SUT is healthy.

10. Distributed state checklist

State Read-only question
Controller JMeter/Java version, remote host list, callback ports, CPU/heap/network, sample-sender mode?
Engine JMeter/Java/plugin manifest, engine ID, RMI ports/SSL, CPU/GC/network, local log?
Thread/arrival Per-engine threads/loops/timers and calculated total offered load?
Files/data Does every engine have the exact expected CSV/JAR/cert shard/hash?
Properties Which values are controller-local -J, global -G, or engine-local startup values?
Protocol/session Each engine creates independent HTTP/TCP/session pools; any source-IP/NAT limits?
Target Authorization ceiling accounts for sum of all engines; target sees multiple source paths?
Results Controller aggregate JTL, per-engine logs/resource state, target counts, optional backend telemetry?
Trust RMI SSL/keystore/firewall/private routing; no public jmeter-server exposure?
Validity Per-engine achieved count + aggregate count + controller/engine headroom match configured math?

11. Read-only inspection before starting remote servers

PowerShell:

& "$env:JMETER_HOME\bin\jmeter.bat" -v
java -version

Get-FileHash .\plans\distributed-local.jmx -Algorithm SHA256
Get-FileHash .\data\engine-a.csv,.\data\engine-b.csv -Algorithm SHA256

Get-Content .\config\remote-global.properties
Get-Content .\config\remote-results.properties

Get-NetTCPConnection -State Listen |
  Where-Object LocalPort -in 1099,1100,4000,4100,5000,5001,5002

curl.exe --fail --silent http://127.0.0.1:8025/health

Do not start remote engines if ports are unexpectedly occupied, versions differ, files/hashes are missing, or total-load math is not approved.

12. DevOps connection

Distributed load generation is infrastructure. It needs inventory, version pinning, file deployment, secrets/certificates, ports/firewalls, capacity monitoring, health checks, logs, immutable manifests and cleanup just like any other service fleet. You cannot safely scale the SUT workload until you can operate the generators themselves.

Knowledge check

Two remote engines each receive a plan with 100 threads. How many configured threads are offered?

Which file is sent automatically to remote engines?

Why is -Gthreads=2 appropriate but -Gengine.id=engine-a wrong for a two-engine lab?

Why can the controller invalidate a distributed test even when engine CPU is low?

Why is RMI SSL part of measurement/security validity?

Next lesson

Build the two-engine isolated lab

Lesson 2 creates the target, two six-row shards, secured loopback RMI engines, explicit -R/-G command, aggregate JTL verification and an independent-worker fallback.

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against current Apache JMeter primary documentation on 2026-09-05. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK; JMeter 5.6.3 requires Java 8+. JMeter remote mode sends the test plan to every remote server, but each engine runs the entire test plan; workload is not divided automatically. All controller/server nodes should run exactly the same JMeter version, and JMeter discourages mixing Java versions. External data files are not sent by the controller and must exist on every server in the expected path; plugins/user JARs likewise need an explicit identical deployment. CLI -r starts servers listed in remote_hosts; -Rhost1,host2 explicitly selects/overrides the server list; -Gname=value or -Gpropertyfile sends JMeter properties to remote servers. Since JMeter 4.0, RMI uses SSL by default. JMeter ships create-rmi-keystore, whose generated test certificate is documented as valid for seven days and uses default alias rmi/passphrase changeit; these defaults are suitable only for a disposable private lab. server.rmi.ssl.disable defaults to false and this chapter never disables it. The remote-testing manual describes dynamic server-engine ports conceptually, while the current properties reference lists server.rmi.localport default 4000; production/private labs should set registry/server/callback ports explicitly instead of relying on defaults. The controller reverse callback client.rmi.localport defaults to 0 (random); when set non-zero JMeter can use up to three consecutive ports. Current default remote sample sender mode is StrippedBatch: successful response bodies are stripped and results are batched. Remote mode can consume more resources than equivalent independent CLI workers, and the controller/client or its network can become the bottleneck.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.