← All field notes

MongoDB / PART II / MEMORY & MEASUREMENT

MongoDB memory on ARM64: THP, TCMalloc & WiredTiger

Part II: controlled THP experiments, two dataset sizes, and what the evidence says about memory and concurrency.

Original project & source ↗

MongoDB Champion 2026 · Published on Medium

I’m part of the MongoDB Champions 2026 community. This story was also published in MongoDB’s official publication on Medium: read Part II on Medium.

Adapted from the public project README. This is Part II of a two-part series; start with the platform build. Figures are linked to the original repository. Results and limitations describe the documented experiment, not the current state of a private environment.

This part separates mechanism from measurement. It documents the kernel and allocator settings, the WiredTiger working sets, the controlled THP ON/OFF procedure, and results from the 500k and 50M datasets. Claims about causal THP effects are limited to what the repeated application-level measurements and telemetry support.

THP and TCMalloc deployment prerequisites

Enabling Transparent Huge Pages on RK1 requires a kernel compiled with the appropriate option. The stock kernel does not expose the THP interface, so the custom kernel packages must be installed before enabling THP through Ansible. setup_rk1cluster.yml installs the two custom kernel packages from the repository root and holds them so the package upgrade cannot replace them. Both .deb files must be present beside the playbook before running the setup.

# MongoDB 8 on ARM64: enable THP and upgraded TCMalloc per-CPU caches
ansible-playbook --ask-vault-pass -i inventory.yml thp_enable.yml

# Optional A/B comparison: disable THP and restart MongoDB one node at a time
ansible-playbook --ask-vault-pass -i inventory.yml thp_disable.yml

# MongoDB 8 THP values: always, defer+madvise, 0, and 1
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
cat /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
cat /proc/sys/vm/overcommit_memory

# MongoDB nodes: /dev/nvme0n1 is mounted at /data by deploy_mongodb_cluster.yml
mount -o noatime,nodiratime /dev/nvme0n1 /data

# Monitoring node (rk1-node4): /dev/nvme0n1 is mounted at /monitoring by setup_monitoring.yml
mount -o noatime,nodiratime,logbufs=8 /dev/nvme0n1 /monitoring

# Tune network buffers for ARM
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216

MongoDB 8 uses upgraded TCMalloc on supported ARM64 Linux systems. The deployment enables THP before Docker starts MongoDB, sets the required always, defer+madvise, max_ptes_none=0, and vm.overcommit_memory=1 values, and sets GLIBC_TUNABLES=glibc.pthread.rseq=0 in the container so TCMalloc can use per-CPU caches. The thp_enable.yml and thp_disable.yml playbooks provide a serial, restart-and-report A/B comparison for benchmarks. Run the enable playbook again after comparison benchmarks to restore the MongoDB 8-recommended configuration.

The RK1 kernel must expose /sys/kernel/mm/transparent_hugepage with CONFIG_TRANSPARENT_HUGEPAGE enabled. The playbooks cannot enable THP when the kernel omits that interface; install or boot a kernel built with that option first. Check the active kernel with:

uname -r
zgrep CONFIG_TRANSPARENT_HUGEPAGE /proc/config.gz 2>/dev/null || \
  grep CONFIG_TRANSPARENT_HUGEPAGE "/boot/config-$(uname -r)"
ls -ld /sys/kernel/mm/*transparent_hugepage

Controlled THP evaluation on ARM64

Starting with MongoDB 8, Transparent Huge Pages (THP) are recommended on supported Linux systems. Because the stock RK1 kernel does not expose the required THP interface, this cluster uses a custom kernel built with CONFIG_TRANSPARENT_HUGEPAGE=y.

The objective of this benchmark is to determine whether MongoDB’s use of THP produces a measurable application-level benefit on the RK3588 ARM64 platform. Two complementary datasets are used:

Dataset Records Purpose
Small 500,000 Observe THP with a relatively small working set
Large 50,000,000 Exercise WiredTiger eviction and storage under cache pressure

The tests compare identical MongoDB, Docker, YCSB, storage, and hardware configurations. Only the host THP state changes. Docker does not isolate a container from host kernel memory management, so THP applies directly to the containerized mongod process. The MongoDB containers have no CPU, memory, or CPU-affinity limits, and /data/db is a host bind mount rather than Docker’s writable overlay filesystem.

Test Environment

Component Configuration
MongoDB 8.3.8, three-node replica set
Architecture ARM64, Rockchip RK3588
Memory 16 GB per MongoDB node
Storage Local NVMe, XFS
Deployment Docker, without resource limits
WiredTiger cache Approximately 7.2 GiB
YCSB client redis/go-ycsb ARM64 container on rk1-node4
Target Direct connection to the primary
Measured write concern majority

Memory-stack interpretation: THP, TCMalloc, and WiredTiger

The three layers answer different questions. THP is a host-kernel page mapping feature; TCMalloc manages mongod’s user-space allocations; and WiredTiger manages database pages in its own cache. Seeing activity in one layer does not establish a throughput benefit in another.

Layer What it changes Evidence examined Limit of the evidence
THP Backs eligible anonymous memory with larger pages Host AnonHugePages, fault allocations/fallbacks, collapse/split telemetry Host-level huge pages do not, by themselves, quantify exactly how many belong to mongod
TCMalloc Reuses memory in allocator caches and per-CPU structures Allocated/free/unmapped memory and cache breakdown Free memory retained by the allocator is not identical to WiredTiger cache occupancy
WiredTiger Reads, dirties, and evicts database pages Cache fill, read/eviction rates, dirty ratio, eviction triggers Cache churn and NVMe I/O can dominate translation-overhead improvements

The dashboard is interpreted as follows:

  • In the 500k campaign, host AnonHugePages grows to roughly 4–5 GiB under THP ON, while cache fill rises over time without the sustained high-rate NVMe reads seen in the 50M campaign. The TCMalloc free/unmapped and cache panels evolve during both campaigns; they indicate allocator state changes, not a standalone THP performance result.
  • In the 50M campaign, THP-backed anonymous memory grows further, to roughly 6–7 GiB per data node. WiredTiger cache sits around 80% full, with sustained reads, eviction, and dirty-page cycles. These are distinct sources of latency variability that must be considered alongside the YCSB numbers.

The THP dashboard counters and AnonHugePages are host measurements. A process-specific smaps_rollup/smaps check for mongod would strengthen the attribution of huge-page coverage to MongoDB. No separate TCMalloc ON/OFF or WiredTiger-cache-size A/B test was performed here: THP is the controlled variable; the other two subsystems provide explanatory telemetry.

Workload and Measurement Method

Both datasets use the same mixed workload:

  • 60% READ, 25% UPDATE, and 15% SCAN
  • Zipfian request distribution
  • uniform scan-length distribution
  • readPreference=primary
  • directConnection=true
  • 4, 8, 16, 32, and 64 YCSB client threads
  • 600 seconds per concurrency level

The configured operation count is deliberately much larger than can be completed during a run. The playbook uses an external timeout to terminate each test after 600 seconds, making the comparison time-bound instead of operation-count-bound. The go-ycsb limitation and shutdown behavior are documented in the Part I gotchas.

Dataset creation is separate from the measured workload. Loading uses 32 threads and writeConcern=1 to reduce preparation time; measured runs use writeConcern=majority. The load phase is not included in benchmark results.

Benchmark Procedure

  1. Enable THP and reboot if requested:
ansible-playbook --ask-vault-pass -i inventory.yml thp_enable.yml
  1. Confirm the reported THP settings, then run the benchmark and give it a descriptive prefix:
ansible-playbook --ask-vault-pass -i inventory.yml ycsb_cache_pressure_test.yml \
  -e confirm_unauthenticated_ycsb=true \
  -e load_dataset=true \
  -e file_prefix=thpon
  1. Disable THP and reboot if requested:
ansible-playbook --ask-vault-pass -i inventory.yml thp_disable.yml
  1. Confirm the reported THP settings and repeat the benchmark with a different prefix:
ansible-playbook --ask-vault-pass -i inventory.yml ycsb_cache_pressure_test.yml \
  -e confirm_unauthenticated_ycsb=true \
  -e file_prefix=thpoff

The dataset load is opt-in. Load it once before the first comparison run with -e load_dataset=true; subsequent runs reuse the existing dataset and should leave that option unset. Loading uses 32 client threads and primary-only write acknowledgement for faster preparation. The measured workload uses majority write concern and direct primary connections.

For every reuse run, pass the intended ycsb_record_count and database explicitly (for example, -e ycsb_database=ycsbsmall -e ycsb_record_count=500000). The protected playbook compares the actual collection document count with the configured record count before the timed thread sweep and refuses to run on a mismatch. The attached summary CSVs still omit READ error counters, so raw thread logs remain necessary for a final request-validity audit. The record-count failure that motivated this safeguard is described in the Part I gotchas.

Playbook Parameters

The main parameters are defined near the top of ycsb_cache_pressure_test.yml and can be overridden with -e:

Parameter Default Purpose
ycsb_record_count 50000000 Number of documents in the dataset
ycsb_operation_count 50000000000 Deliberately unreachable operation limit
ycsb_run_duration_seconds 600 Duration of each concurrency level
ycsb_threads_sweep 4, 8, 16, 32, 64 Client concurrency levels
load_dataset false Load or reuse the dataset
ycsb_load_threads 32 Concurrent clients used during loading
ycsb_load_write_concern 1 Load-phase acknowledgement level
ycsb_cache_pressure_write_concern majority Measured-run acknowledgement level
file_prefix Current YYYYMMDD_HHMMSS Prefix for all result files

The workload is read/update/scan only: 60% reads, 25% updates, and 15% scans. Inserts are disabled when reusing a populated dataset, avoiding duplicate-key errors. The measured runs use mongodb.write_concern=majority and mongodb.read_preference=primary.

Generated Results

Results are written to ycsb_results/ on the control node. For a prefix of nothp, the playbook creates one raw file per thread level:

ycsb_results/nothp-threads-4.txt
ycsb_results/nothp-threads-8.txt
ycsb_results/nothp-threads-16.txt
ycsb_results/nothp-threads-32.txt
ycsb_results/nothp-threads-64.txt

When load_dataset=true, the loader output is also saved as nothp-load.txt. The thread files contain YCSB throughput, operation counts, average latency, and percentile latency for READ, UPDATE, SCAN, and TOTAL.

Python Report Generator

ycsb_report.py parses the raw thread files and generates an aggregate CSV and PNG chart. Matplotlib is installed by setup_facility-node.sh as python3-matplotlib.

The playbook runs the report generator automatically. To regenerate a report manually or process an existing run:

python3 ycsb_report.py \
  --results-dir ycsb_results \
  --prefix nothp \
  --output-prefix nothp

This creates ycsb_results/nothp-summary.csv and ycsb_results/nothp-report.png. The chart contains throughput, average latency, p95 latency, and operation-count panels plotted against client threads. The CSV is intended for further comparison or archival.

Small Dataset: 500,000 Documents

The 500,000-document campaign contains four repetitions under each THP setting. The load logs confirm recordcount=500000 and report 500,000 inserts. Run 1 is excluded consistently as the post-load transient/warm-up repetition; the tables use the arithmetic mean of the final CSV totals from runs 2-4.

Figure 1 — Small-dataset Grafana overview (THP OFF and THP ON campaigns).

View source figure: MongoDB RK1 THP small-dataset benchmark

Threads THP OFF ops/s THP ON ops/s ON vs. OFF Average total latency OFF/ON P95 OFF/ON
4 561.3 560.2 -0.2% 7.10 / 7.12 ms 32.02 / 32.09 ms
8 972.5 976.8 +0.4% 8.21 / 8.17 ms 32.28 / 32.39 ms
16 1,287.3 1,292.0 +0.4% 12.41 / 12.37 ms 51.89 / 52.15 ms
32 1,341.7 1,335.9 -0.4% 23.83 / 23.94 ms 123.92 / 121.90 ms
64 1,335.0 1,335.8 +0.1% 47.91 / 47.89 ms 304.38 / 297.21 ms

THP effects on throughput are mixed in sign and within roughly 0.5% across these stabilized repetitions. Neither average nor P95 total latency shows a consistent THP advantage. In Figure 1, THP-backed anonymous memory rises to roughly 4-5 GiB on the enabled nodes; allocation and collapse activity appears after THP is switched on, while flow control remains inactive. THP is thus visibly in use, but not associated with a material YCSB performance gain in this campaign.

The summary CSVs contain completed-operation throughput and latency, not READ success/error counters. The load size and configured recordcount are aligned, but a complete request-validity audit requires the raw YCSB thread logs. Error-free READs should be confirmed there before treating these numbers as final.

Sixteen threads is the practical throughput/latency sweet spot for this small-dataset workload. Averaging the THP OFF and ON results exposes the cost of pushing concurrency past the knee:

Threads Mean throughput Mean average latency Throughput vs. 16 Latency vs. 16 Mean P95
4 560.8 ops/s 7.11 ms -56.5% -42.6% 32.06 ms
8 974.7 ops/s 8.19 ms -24.4% -33.9% 32.34 ms
16 1,289.6 ops/s 12.39 ms baseline baseline 52.02 ms
32 1,338.8 ops/s 23.89 ms +3.8% ~1.9x 122.91 ms
64 1,335.4 ops/s 47.90 ms +3.5% ~3.9x 300.80 ms

From 16 to 32 threads, throughput rises by only 3.8% while mean latency almost doubles; at 64 threads throughput slips slightly below the 32-thread level while mean latency nearly quadruples relative to 16 threads. P95 also rises by roughly 2.4x and 5.8x respectively. The 32- and 64-thread settings are poor latency trade-offs for this specific small-dataset profile.

Large Dataset: 50 Million Documents

The large dataset is intentionally much greater than the effective WiredTiger cache. It creates sustained cache churn and exercises the full path through WiredTiger eviction, NVMe I/O, dirty-page processing, checkpointing, and replica-set acknowledgement.

Figure 2 — Large-dataset Grafana overview (50M documents, THP OFF followed by THP ON).

View source figure: MongoDB RK1 THP large-dataset benchmark

The table below uses the arithmetic mean of the final YCSB totals from repeated runs 2-4. Run 1 is excluded consistently as the transient/warm-up repetition. Latency values are total-operation latency.

Threads THP OFF ops/s THP ON ops/s Throughput change Avg latency OFF/ON P95 OFF/ON
4 77.7 79.0 +1.7% 51.4 / 50.6 ms 341.8 / 337.4 ms
8 144.6 150.6 +4.1% 55.3 / 53.1 ms 369.8 / 356.9 ms
16 257.9 261.9 +1.6% 62.0 / 61.1 ms 424.4 / 411.9 ms
32 411.1 440.4 +7.1% 77.8 / 72.7 ms 535.9 / 493.7 ms
64 605.4 638.4 +5.5% 105.6 / 100.1 ms 718.0 / 670.7 ms

Unlike the small dataset, the repeated large-dataset results show a modest THP advantage at every tested concurrency level. The largest mean throughput gain is approximately 7.1% at 32 threads, while average and P95 latency are also lower with THP enabled at all five thread counts. The gain is not linear, so it should be interpreted as a workload-level effect rather than a fixed THP improvement factor.

The dashboard confirms that this is a genuine cache-pressure workload:

  • WiredTiger cache remains around 80% full on the active data nodes.
  • eviction is sustained throughout the campaigns;
  • NVMe reads frequently reach approximately 150-200 MiB/s;
  • dirty-cache ratios rise and fall with checkpoint and writeback cycles;
  • replication lag adds occasional latency variability.

Flow control requires a separate interpretation. The flow-control activity in Figure 2 occurs only during the 50M-document loading step, when documents are initially inserted. It is absent from the measured 4/8/16/32/64-thread YCSB sweeps. Flow control is therefore evidence of pressure during dataset creation, but it is not a limiting factor in the reported benchmark results.

THP use is also much greater than in the small experiment. The MongoDB nodes reach approximately 6-7 GiB of AnonHugePages each. THP allocations are continuous, fallbacks remain rare, and collapse/split activity does not show a pathological storm. Docker therefore does not prevent THP from operating, and the measured benefit is not accompanied by an obvious THP allocation failure or compaction problem.

The 50M-record workload continues to gain throughput through 64 client threads, unlike the small workload. That gain comes with rapidly increasing latency. The following table averages THP ON and OFF values to isolate the concurrency trade-off from the THP comparison:

Threads Mean throughput Mean average latency Throughput vs. 16 Latency vs. 16 Operating interpretation
4 78.4 ops/s 51.0 ms -69.8% -17.1% Under-utilized
8 147.6 ops/s 54.2 ms -43.2% -11.9% Conservative
16 259.9 ops/s 61.6 ms baseline baseline Sweet spot
32 425.8 ops/s 75.3 ms +63.8% +22.2% Throughput-oriented
64 621.9 ops/s 102.9 ms +139.3% +67.0% Maximum throughput, high latency

At 32 and especially 64 threads, average latency rises much faster in absolute terms and P95 latency reaches roughly 494-536 ms and 671-718 ms respectively. Those settings remain useful when batch throughput is the priority, but they are no longer the most balanced choice. Sixteen threads is a reasonable latency-conscious operating point for this large-dataset profile, but not its sweet spot: unlike the small dataset, 32 and 64 threads provide substantial additional throughput. Select those higher settings when the extra completed work justifies the measured latency cost.

View source figure: Threads

Figure 3 — Concurrency operating regions observed on the RK1 cluster.

Global Conclusion

Together, the two datasets show that MongoDB 8.3.8 successfully and extensively uses Transparent Huge Pages on RK1/RK3588 ARM64, including when mongod runs inside Docker. Allocation fallbacks remain very low, so the comparison is not between an effective THP OFF configuration and a nominal-but-nonfunctional THP ON configuration.

The performance effect depends on the working set:

  • With the 500,000-document workload, THP ON and OFF differ by less than approximately 0.5% in mean throughput across the tested thread counts. The 16-thread knee is clear: moving to 32 or 64 adds only 3.8% or 3.5% throughput on average, while average total latency rises by roughly 1.9x or 3.9x, respectively. Raw READ error counters should be checked before final publication of this new campaign.
  • With 50 million documents and sustained WiredTiger cache pressure, repeated runs 2-4 show a small but consistent THP benefit: approximately 1.6-7.1% higher mean throughput, accompanied by lower mean and P95 total latency at every tested concurrency level. Throughput continues to scale above 16 threads, but average and P95 latency rise sharply, making 16 threads the most balanced operating point and 32-64 threads throughput-oriented choices.

The evidence therefore supports a more nuanced conclusion than either “THP always improves MongoDB” or “THP has no effect”:

On this RK1 ARM64 cluster, THP is neutral for the small, relatively cache-resident workload and modestly beneficial for the 50M-document cache-pressure workload in these repeated runs. The larger workload shows both greater THP-backed memory and higher mean throughput with THP ON; these measurements do not isolate the memory-coverage increase as the cause. WiredTiger, NVMe I/O, replication, and concurrency remain the dominant determinants of overall performance. Sixteen client threads is the small-dataset throughput/latency sweet spot; on the large dataset, 32 and 64 threads can offer substantial additional throughput, but also increase average and P95 latency.

The operations-per-watt chart in Part I describes the platform’s power envelope, but it is based on server-side MongoDB operations rather than YCSB requests. A per-thread and time-aligned power comparison would be required to establish whether THP improves completed YCSB work per watt.

These findings are specific to this RK3588 hardware, MongoDB 8.3.8, custom Linux kernel, Docker deployment, WiredTiger configuration, NVMe subsystem, replica-set topology, and YCSB workload. They should not be generalized into a universal MongoDB THP rule. For this project, the MongoDB 8-recommended THP-enabled configuration remains the operational default, while THP OFF is retained for controlled A/B testing.

Further Performance Optimizations Worth Testing

  • A/B test MongoDB in Docker versus bare metal to quantify the actual containerization overhead on RK1. The current container has no resource limits, /data/db is a host bind mount, and the metrics confirm that THP is already active for the containerized mongod, so Docker is not expected to prevent THP operation.
  • Benchmark mongodb_wiredtiger_cache_size_gb at 4, 6, and 8 on the 16 GB RK1 nodes. The system currently defaults to 6 to keep more RAM available for Linux page cache, Docker overhead, and monitoring while still preserving a useful WiredTiger working set.
  • A/B test mongodb_wiredtiger_collection_compressor between zstd and snappy. The current zstd default is space-efficient, but snappy often improves CPU-bound write throughput on ARM.
  • Consider pinning the mongod container to the RK3588 Cortex-A76 cores during write-heavy benchmarks. The big.LITTLE topology can otherwise mix latency-sensitive database threads with background tasks.
  • Compare the current direct-primary workload with a replica-set-aware client profile to quantify driver-routing and failover behavior. The present measured workload already uses writeConcern=majority.
  • Pin the monitoring containers away from :latest before long benchmark campaigns so exporter behavior and Prometheus/Grafana feature changes do not skew repeatability.