Skip to content

Load Testing ​

Systems ​

The following system was used for the performance evaluation:

SystemProviderCPUCoresRAMSSD
A5N46on-premRyzen 9900X2496 GiB4 TB Samsung 990 Pro
LEA47on-premEPYC 7543P16128 GiB3.2 TB Intel P5600 over vSAN
LEA79on-premEPYC 9555128512 GiB12.8 TB Huawei OceanDisk 300P

All systems were configured according to the Production Configuration guide. Deviating from it, all runs on all systems use a DB_RESOURCE_STORE_KV_THREADS of 128 instead of the default of 4, matching the highest concurrency level of the sweep. That variable sizes the thread pool the resource store uses to read and write resources. Every transaction hands all its resources to that pool as one unit, so the pool size caps how many transactions can write to the resource database at the same time. RocksDB merges concurrent writers into group commits: the writers waiting at any moment are batched into a single write-ahead log write followed by a single fsync, so the cost of that fsync — which dominates a write on systems with slow syncs — is shared by the whole group. With only four threads, at most four transactions can join a group, whereas 128 threads allow much larger groups and so a considerably higher write throughput. Sizing the pool to the concurrency also keeps it from becoming a queue of its own; see Resource Store Write Concurrency.

For the same reason, all systems use a DB_RESOURCE_INDEXER_THREADS of 128 instead of the default of 4. That variable sizes the thread pool that indexes the resources of the transactions, one resource per thread at a time. With the small transactions of this test, the default goes unnoticed on disks with slow syncs, because the disk limits the throughput long before the indexer does. On LEA79, however, four threads cap the transaction load test at about 8,000 transactions/s from 8 clients on, less than half of what the system reaches with 128 threads; see Resource Indexer Concurrency.

In addition, the JVM heap of all systems is fixed at its maximum size by setting -Xms to the value of -Xmx. Left to size the heap itself, the JVM starts with a small one and can shrink it again during a run. On LEA79, such a shrink in the middle of the sweep was followed by a burst of garbage collection pauses that more than doubled the q99 at 64 clients.

Datasets ​

The following datasets were used:

DatasetHistory# Pat. ¹# Res. ²# Obs. ³Size on SSD
1M10 years1 M1044 M593 M1045 GiB

¹ Number of Patients, ² Total Number of Resources, ³ Number of Observations

Methods ​

The load testing tool k6 is used to create load from another host in the same network as the test system.

Each test is a k6 script in the load-testing directory and is run via the Makefile. The FHIR base URL of the system under test — for example a Blaze running in a Docker container — is passed via the BASE environment variable:

sh
BASE=http://localhost:8080/fhir k6 run patient-everything.js

The optional DURATION environment variable (default 60 s) sets how long each concurrency level runs.

The transaction test reads the version of the system under test from its CapabilityStatement and writes it into the version column of its results, so that each result file carries the version it was measured against.

Single runs of the transaction test vary by about 5 % in throughput, so the Makefile runs it three times per system, keeps each run in load-testing/data/raw and publishes the per-level median of the runs. Every run needs an empty server, so for this test k6 runs on the system under test itself, where Blaze is started via the docker-compose-<system>.yml of that system. In front of every run, the reset.sh script shuts Blaze down, deletes the contents of the host directory the Compose file mounts as data volume, starts Blaze again and waits for it to become healthy:

sh
BASE=http://localhost:8080/fhir make data/transaction-LEA47.csv

The transaction test sweeps over the concurrency levels 1 to 128, one after the other. The optional VUS environment variable restricts it to a single level, which is what to use while profiling the server: the batches of the transaction log and the resource store grow with the number of concurrent clients, so every level has its own ratio of per-batch to per-entry work that a profile over the whole sweep would blur. Such a run reports its result line on stdout instead of writing the CSV, so it leaves the committed sweep data alone:

sh
BASE=http://localhost:8080/fhir VUS=64 DURATION=300 k6 run transaction.js

Single Patient Reads ​

Results ​

DatasetSystemVUsReq/smedq95q99
1MA5N46114050.500.741.47
1MA5N46239070.450.570.67
1MA5N46472480.530.590.69
1MA5N468133810.550.670.88
1MA5N4616236780.600.821.21
1MA5N4632383140.731.131.90
1MA5N4648456790.891.583.22
1MA5N4664488681.072.204.12

Patient Everything ​

Results ​

DatasetSystemVUsReq/smedq95q99
1MA5N46140.5015.728.354.7
1MA5N46273.0716.931.741.7
1MA5N464162.921.942.359.0
1MA5N468234.930.560.190.2
1MA5N4616261.257.698.7125.1
1MA5N4632258.7119.4174.3202.1

Transaction ​

This write test measures the throughput and latency of small FHIR transactions.

The transaction.js script repeatedly POSTs a small transaction bundle to the FHIR base URL. Each bundle creates one Patient and one Observation, where the Observation references the Patient via a bundle-internal URN, so reference resolution is exercised as well. The Patient's birthDate and the Observation's systolic blood pressure are randomized per transaction, so the date and quantity search-param indices see a realistic spread of values instead of a single repeated entry. New resources are created on every request, so the database grows over the course of the run.

All runs start from an empty database and grow it with every request, so there is no fixed dataset. The test was run on three systems to show how strongly transaction throughput depends on disk performance (see Disk Performance): LEA79 stores its data on a local NVMe disk with an fsync latency of a few microseconds, LEA47 accesses its disk over vSAN with an fsync latency of about 2 ms, and A5N46 uses a local consumer NVMe SSD that acknowledges a sync only once the data has reached the flash, at about 5 ms.

Results ​

Every transaction durably writes its transaction log entry and its resources before it is answered, so the disk's sync behaviour sets the processing time. It does not cap the throughput, though: the transaction log batches all transactions submitted while the previous write was in flight into a single write followed by one fsync, and the resource store group-commits its concurrent writers the same way. The batches grow with the number of concurrent clients, so throughput scales past the disk's raw fsync rate.

All values are the per-level medians of three runs against a development build of Blaze at commit 5773c58b.

Line chart. Transaction (LEA47). Concurrent Clients from 1 to 128. Left axis Requests/s: Requests/s. Right axis Processing Time (ms): Median RT, P95 RT, P99 RT.Transaction (LEA47)05001000150020002500300035004000010203040506070801248163264128Concurrent Clients 1 · Requests/s: 176.8Concurrent Clients 2 · Requests/s: 360.233Concurrent Clients 4 · Requests/s: 451.933Concurrent Clients 8 · Requests/s: 801.017Concurrent Clients 16 · Requests/s: 1501.233Concurrent Clients 32 · Requests/s: 2629.3Concurrent Clients 64 · Requests/s: 3327.033Concurrent Clients 128 · Requests/s: 3620.767Concurrent Clients 1 · Median RT: 5.284 msConcurrent Clients 2 · Median RT: 5.126 msConcurrent Clients 4 · Median RT: 8.34 msConcurrent Clients 8 · Median RT: 9.781 msConcurrent Clients 16 · Median RT: 10.114 msConcurrent Clients 32 · Median RT: 11.382 msConcurrent Clients 64 · Median RT: 18.044 msConcurrent Clients 128 · Median RT: 33.756 msConcurrent Clients 1 · P95 RT: 5.96 msConcurrent Clients 2 · P95 RT: 6.197 msConcurrent Clients 4 · P95 RT: 10.554 msConcurrent Clients 8 · P95 RT: 11.248 msConcurrent Clients 16 · P95 RT: 12.041 msConcurrent Clients 32 · P95 RT: 14.166 msConcurrent Clients 64 · P95 RT: 22.162 msConcurrent Clients 128 · P95 RT: 41.723 msConcurrent Clients 1 · P99 RT: 7.023 msConcurrent Clients 2 · P99 RT: 7.174 msConcurrent Clients 4 · P99 RT: 11.552 msConcurrent Clients 8 · P99 RT: 12.578 msConcurrent Clients 16 · P99 RT: 13.7 msConcurrent Clients 32 · P99 RT: 17.1 msConcurrent Clients 64 · P99 RT: 39.036 msConcurrent Clients 128 · P99 RT: 74.421 msRequests/sProcessing Time (ms)Concurrent ClientsRequests/sMedian RTP95 RTP99 RT

LEA47 scales to about 2630 transactions/s at 32 clients — more than five transactions per fsync at its 484 fsyncs/s — and on to 3327 at 64 clients, but gains little beyond that with 3621 at 128 clients, about seven and a half transactions per fsync. The median processing time roughly doubles at each of the last two levels instead, from 11 ms at 32 clients to 18 ms at 64 and 34 ms at 128, with a q99 of 74 ms at 128 clients. With the throughput staying put while the concurrency doubles, the clients added beyond 64 only queue.

Line chart. Transaction (LEA79). Concurrent Clients from 1 to 128. Left axis Requests/s: Requests/s. Right axis Processing Time (ms): Median RT, P95 RT, P99 RT.Transaction (LEA79)0500010000150002000002468101248163264128Concurrent Clients 1 · Requests/s: 1510.717Concurrent Clients 2 · Requests/s: 3126.95Concurrent Clients 4 · Requests/s: 5348.067Concurrent Clients 8 · Requests/s: 8096.3Concurrent Clients 16 · Requests/s: 12755.683Concurrent Clients 32 · Requests/s: 14462.85Concurrent Clients 64 · Requests/s: 16924.967Concurrent Clients 128 · Requests/s: 19581.733Concurrent Clients 1 · Median RT: 0.53 msConcurrent Clients 2 · Median RT: 0.505 msConcurrent Clients 4 · Median RT: 0.586 msConcurrent Clients 8 · Median RT: 0.841 msConcurrent Clients 16 · Median RT: 1.081 msConcurrent Clients 32 · Median RT: 1.97 msConcurrent Clients 64 · Median RT: 3.325 msConcurrent Clients 128 · Median RT: 6.098 msConcurrent Clients 1 · P95 RT: 0.66 msConcurrent Clients 2 · P95 RT: 0.629 msConcurrent Clients 4 · P95 RT: 0.814 msConcurrent Clients 8 · P95 RT: 1.054 msConcurrent Clients 16 · P95 RT: 1.308 msConcurrent Clients 32 · P95 RT: 2.379 msConcurrent Clients 64 · P95 RT: 4.423 msConcurrent Clients 128 · P95 RT: 7.868 msConcurrent Clients 1 · P99 RT: 0.803 msConcurrent Clients 2 · P99 RT: 0.712 msConcurrent Clients 4 · P99 RT: 0.934 msConcurrent Clients 8 · P99 RT: 1.149 msConcurrent Clients 16 · P99 RT: 1.453 msConcurrent Clients 32 · P99 RT: 2.648 msConcurrent Clients 64 · P99 RT: 5.103 msConcurrent Clients 128 · P99 RT: 8.761 msRequests/sProcessing Time (ms)Concurrent ClientsRequests/sMedian RTP95 RTP99 RT

LEA79 syncs from a write cache in a few microseconds and is not disk-bound at all: throughput scales to about 12,800 transactions/s at 16 clients and grows more slowly from there, to about 16,900 at 64 and 19,600 at 128 clients, with a median processing time of 6.1 ms and a q99 of 8.8 ms at 128 clients.

Line chart. Transaction (A5N46). Concurrent Clients from 1 to 128. Left axis Requests/s: Requests/s. Right axis Processing Time (ms): Median RT, P95 RT, P99 RT.Transaction (A5N46)010002000300040005000010203040501248163264128Concurrent Clients 1 · Requests/s: 83.067Concurrent Clients 2 · Requests/s: 112.8Concurrent Clients 4 · Requests/s: 128.417Concurrent Clients 8 · Requests/s: 246.933Concurrent Clients 16 · Requests/s: 494.683Concurrent Clients 32 · Requests/s: 1034.733Concurrent Clients 64 · Requests/s: 2038.133Concurrent Clients 128 · Requests/s: 4073.317Concurrent Clients 1 · Median RT: 10.743 msConcurrent Clients 2 · Median RT: 19.269 msConcurrent Clients 4 · Median RT: 29.188 msConcurrent Clients 8 · Median RT: 29.224 msConcurrent Clients 16 · Median RT: 29.331 msConcurrent Clients 32 · Median RT: 29.273 msConcurrent Clients 64 · Median RT: 29.298 msConcurrent Clients 128 · Median RT: 29.358 msConcurrent Clients 1 · P95 RT: 14.944 msConcurrent Clients 2 · P95 RT: 20.25 msConcurrent Clients 4 · P95 RT: 40.201 msConcurrent Clients 8 · P95 RT: 40.279 msConcurrent Clients 16 · P95 RT: 40.357 msConcurrent Clients 32 · P95 RT: 39.773 msConcurrent Clients 64 · P95 RT: 40.186 msConcurrent Clients 128 · P95 RT: 39.904 msConcurrent Clients 1 · P99 RT: 15.534 msConcurrent Clients 2 · P99 RT: 23.748 msConcurrent Clients 4 · P99 RT: 40.912 msConcurrent Clients 8 · P99 RT: 40.798 msConcurrent Clients 16 · P99 RT: 40.962 msConcurrent Clients 32 · P99 RT: 40.45 msConcurrent Clients 64 · P99 RT: 41.117 msConcurrent Clients 128 · P99 RT: 42.256 msRequests/sProcessing Time (ms)Concurrent ClientsRequests/sMedian RTP95 RTP99 RT

A5N46 shows the batching most clearly. Its drive syncs at only 178/s, which pins the median processing time at about 29 ms from 4 clients on — but with the batches growing along the concurrency, throughput roughly doubles with every doubling of clients: 247 transactions/s at 8 clients, 1035 at 32, 2038 at 64 and 4073 at 128 clients, nearly twenty-three transactions per fsync.

The processing time never leaves that plateau: the median stays there and the q99 at about 41 ms at every level up to 128 clients, so each client waits for one fsync no matter how many others share it. That is what makes the doubling possible in the first place, and it holds only as long as the resource store has a free thread for every client — which is why these runs size its pool to the highest concurrency level of the sweep.