← All field notes

MongoDB / PART I / BUILD & OPERATIONS

Building a tiny MongoDB cluster on Turing Pi RK1

Part I: four ARM nodes, Ansible automation, monitoring, and the lessons of operating a compact MongoDB replica set.

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 I on Medium.

Adapted from the public project README. This is Part I of a two-part series; continue to the memory experiments. Figures are linked to the original repository. Results and limitations describe the documented experiment, not the current state of a private environment.

Part I is about building and operating the machine. Part II asks the more interesting question: when this tiny replica set is busy, does the ARM64 memory stack actually help? Keep reading for the THP A/B tests—there is a surprise in how differently the two dataset sizes behave.

What’s This All About? 🤔

Ever wondered what happens when you take four tiny ARM computers, stuff them into a mini-ITX sized board, and ask them to handle enterprise database workloads? Well, grab some coffee ☕ and step into the world of distributed ARM computing !

We’re building a MongoDB replica set on the TuringPi 2 platform using RK1 compute modules - basically creating a data center that fits in your backpack.

Meet the TuringPi 2: The ARM Avengers’ Headquarters 🦾

The TuringPi 2 is like the Tony Stark of single-board computers - it takes regular compute modules and turns them into something extraordinary. Think of it as a 4-slot carrier board that lets you cluster ARM-based compute modules together, including NVIDIA Jetson modules if you fancy.

The img/ directory contains the hero artwork, the software-stack diagram, and the captured benchmark chart used throughout this article. Feel free to check it out !

Why TuringPi is Cool:

  • Compact Design: Mini-ITX form factor (same size as the smallest PC motherboard)
  • Modular Architecture: Hot-swappable compute modules
  • Real Networking: Gigabit Ethernet switching between nodes
  • Storage: NVMe dedicated to each module
  • Enterprise Features: BMC (Baseboard Management Controller) for remote management View source figure: Compact platform

The Hardware Squad 💪

Shopping list for our MongoDB Cluster:

Component Quantity Specs Role
TuringPi 2 1x Mini-ITX carrier board The foundation 🏗️
RK1 Modules 4x ARM64 RK3588, 16GB RAM The workhorses 🐎
NVMe SSDs 4x 400GB each Speed demons ⚡
Heatsinks 4x Active cooling Chill vibes 🧊
24-pin PSU 1x Standard ATX power The juice ⚡

RK1 Module Deep Dive 🔍

Each RK1 is basically a smartphone SoC on steroids. Here’s what makes it tick:

  • CPU: ARMv8-A RK3588 (4x Cortex-A76 + 4x Cortex-A55)
  • GPU: Mali-G610 MP4 (yes, it can game!)
  • NPU: Built-in AI acceleration providing 6TOPS (we will come to this in a next chapter…)
  • Memory: 16GB LPDDR4 (exists in 8/16/32 flavor)
  • Storage: 32G eMMC and NVMe support
  • Form Factor: 260-pin SO-DIMM connector (Nvidia Jetson Compatible)
  • Networking: 1x Gigabit Ethernet
  • Power: ~9W per module including NVME (40W total cluster max!)

The Software Stack: From Zero to Hero 🛠️

Our Technology Choices:

View source figure: Software Stack Overview

MongoDB Deployment Architecture:

┌─────────────┬─────────────┬─────────────┬─────────────┐
│  rk1-node1  │  rk1-node2  │  rk1-node3  │  rk1-node4  │
│   PRIMARY   │ SECONDARY   │ SECONDARY   │  CONTROL    │
├─────────────┼─────────────┼─────────────┼─────────────┤
│ MongoDB     │ MongoDB     │ MongoDB     │ Ansible     │
│ Data Node   │ Data Node   │ Data Node   │ Monitoring  │
│             │             │             │ Prometheus  │
│             │             │             │ Grafana     │
│             │             │             │ LoadTest    │
│             │             │             │             │
│ 16GB RAM    │ 16GB RAM    │ 16GB RAM    │ 16GB RAM    │
│ 256GB NVMe  │ 256GB NVMe  │ 256GB NVMe  │ 256GB NVMe  │
│ /data       │ /data       │ /data       │ /monitoring │
└─────────────┴─────────────┴─────────────┴─────────────┘
                            │
                   ┌────────┴────────┐
                   │   Replica Set   │
                   │  "turingpi_rs"  │
                   └─────────────────┘

Connection Strings and Primary Failover

Applications should connect to all three data-bearing nodes and include the replica set name. The MongoDB driver will discover the current primary and automatically select a secondary after a primary failure and election:

# Normal deployment with authentication enabled
mongodb://<user>:<password>@rk1-node1:27017,rk1-node2:27017,rk1-node3:27017/admin?authSource=admin&replicaSet=turingpi_rs

# Optional non-production/testing deployment after disable_mongodb_auth.yml
mongodb://rk1-node1:27017,rk1-node2:27017,rk1-node3:27017/test?replicaSet=turingpi_rs

Do not use directConnection=true or list only the current primary when you need automatic failover. rk1-node4 is the Ansible control and monitoring host, not a MongoDB replica-set member, so it should not be included as an application connection target. Replace the hostnames with the nodes’ reachable IP addresses or DNS names when connecting from outside the cluster network.

Performance: David vs Goliath 📊

Platform-capacity snapshots (PLGM and earlier YCSB workloads)

These are illustrative results from separate workload profiles, not the controlled THP comparison in Part II. Do not compare their ops/s directly with the 500k/50M YCSB sweeps: operation mixes, acknowledgement settings, and measurement scopes differ.

🔥 The ARM Crusher (50% Write Workload)

  • Throughput: 6100 ops/sec
  • **Avg Write Latency **: <10 ms
  • Power Consumption: 42W total cluster
  • Performance/Watt: 145 ops/sec/watt

🏃‍♂️ ARM Endurance Test (25% Write Workload)

  • Throughput: 6700 ops/sec
  • **Avg Read Latency **: <1 ms
  • Power Consumption: 36W total cluster
  • Performance/Watt: 180 ops/sec/watt

View source figure: Performance Chart

The important comparison here is within the RK1 platform: how much useful work it performs at its observed cluster power. No matched x86 benchmark was run, so x86 performance, savings, and cooling claims are not inferred from these snapshots.

The Installation Journey 🗺️

Phase 1: Hardware Assembly (The Fun Part!)

# Step 1: Insert RK1 modules into TuringPi 2
# Step 2: Attach heatsinks (ARM modules run cool, but better safe!)
# Step 3: Install NVMe drives
# Step 4: Connect power and networking
# Step 5: Power on and watch the ARM magic happen! ✨

Phase 2: Software Deployment (The Ansible Way!)

# Clone our automation repository
gh repo clone vthinsel/mongodb-TuringPI-rk1

# Run on rk1-node4 (facility/control node): installs & validates Ansible, then sets up SSH keys
./setup_facility-node.sh

# Configure RK1 pre-requisites : NVMe , DNS resolution, custom kernel, common packages, etc.
ansible-playbook --ask-vault-pass -i inventory.yml setup_rk1cluster.yml

# Deploy MongoDB cluster
ansible-playbook --ask-vault-pass -i inventory.yml deploy_mongodb_cluster.yml

# Optional: remove MongoDB login/password requirements for convenience or tests
# Run this only on non-production clusters; it disables authentication entirely.
ansible-playbook --ask-vault-pass -i inventory.yml disable_mongodb_auth.yml -e disable_mongodb_auth=true

# Set up monitoring
ansible-playbook --ask-vault-pass -i inventory.yml setup_monitoring.yml

# Run the first unauthenticated mixed YCSB test against the primary with a very small dataset of 50k documents
ansible-playbook --ask-vault-pass -i inventory.yml ycsb_load_test.yml -e confirm_unauthenticated_ycsb=true

The three MongoDB nodes format and mount /dev/nvme0n1 as /data for database files. rk1-node4 is not a MongoDB member: its /dev/nvme0n1 is formatted and mounted as /monitoring by setup_monitoring.yml. Prometheus time-series data is stored at /monitoring/prometheus/data, Grafana state at /monitoring/grafana/data, and the monitoring configuration and dashboard are kept alongside them. The playbook uses XFS with noatime,nodiratime,logbufs=8 and persists the mount in /etc/fstab through Ansible’s mount module.

Grafana loads the dedicated TuringPi MongoDB Cluster dashboard automatically into the TuringPi folder. Its source is grafana/turingpi-mongodb-dashboard.json; the playbook copies it to /monitoring/grafana/provisioning/dashboards/ and provisions the Prometheus datasource at http://prometheus:9090. No manual dashboard import is needed. Feel free to add or import any other dashboards from Grafana

First YCSB Load Test

ycsb_load_test.yml runs the pinned ARM64 redis/go-ycsb image from rk1-node4 against the primary, rk1-node1, using an unauthenticated direct connection. It first loads the YCSB dataset, then runs a common mixed profile: The playbook bypasses the image’s incompatible bundled init wrapper and invokes the native ARM64 /go-ycsb binary directly. Note that the results can’t be directly compared with other ycsb release, but can be used to compare different settings such as THP on/off, zippy Vs std, different wiredtiger cache size, etc.

  • 50% reads
  • 30% updates
  • 20% scans/searches

The default test loads 50,000 records and runs operations for 5 minutes using 16 threads. Adjust the workload without editing the playbook:

ansible-playbook --ask-vault-pass -i inventory.yml ycsb_load_test.yml \
  -e confirm_unauthenticated_ycsb=true \
  -e ycsb_record_count=1000000 \
  -e ycsb_run_duration_seconds=3600 \
  -e ycsb_threads=32

This test is intended only for isolated, non-production use. The confirmation flag is required, and the direct connection keeps operations on the primary. This quick and simple test doesn’t hit memory limits, thus wiredtiger settings don’t come into the game as there is no pressure, we are in-memory workload scenario.

By increasing the run duration, you can easily perform endurance testing. It is really up to you to tweak the variables, and adjust the playbook if needed.

Percona Load Generator MongoDB

PLGM is the platform-capacity stress test in this part of the story. The sample below is a three-hour configuration with 50 workers, running against the replica set. It demonstrates the cluster sustaining a varied operation mix; it does not isolate THP, TCMalloc, or WiredTiger effects. A 32 GB RK1 could be an interesting follow-up for memory-heavy workloads, but its benefit has not been measured here.

ubuntu@rk1-node4:~/rk1-ansible/plgm-workloads$ ./percona-load-generator-mongodb-4.6.0/bin/plgm config.std-3hours-50workers.yaml
  [INFO] Performance: Automatically disabled driver compression (compressors=none)
Enter MongoDB Username:
Warning: Collections path 'resources/collections' not found. Using embedded default.json.
Warning: Queries path 'resources/queries' not found. Using embedded default.json.

  plgm dev
  --------------------------------------------------
  Target URI:     mongodb://rk1-node1:27017,rk1-node2:27017,rk1-node3:27017?compressors=none
  Namespaces:     airline.flights
  Workers:        50 active
  Duration:       3h
  Workload Mode:  Default (Only default.json)

  WORKLOAD DEFINITION
  --------------------------------------------------
  Distribution:  Select (79%)  Update (11%)
                 Insert (3%)   Delete (5%)
                 Agg    (0%)   Trans  (0%)

2026/08/28 06:18:07 [Lifecycle] Phase=initializing: preparing collections, indexes, sharding setup, and optional seed data
2026/08/28 06:18:07 [Lifecycle] Init Step 1/3: preparing collections
  [INFO] Skipping sharding for 'flights': Cluster is not sharded (Replica Set)
2026/08/28 06:18:07 [Lifecycle] Init Step 2/3: creating indexes (declared indexes=6)
  [INFO] Created 6 indexes on 'airline.flights'
2026/08/28 06:18:07 [Lifecycle] Init Step 3/3: optional seed (skip_seed=true, documents_count=100000)
2026/08/28 06:18:07 [Lifecycle] Initialization completed in 210ms
2026/08/28 06:18:07 [Lifecycle] Phase=running: execution timers now track query workload only
2026/08/28 06:18:07 Starting Standard Workload iteration 1 of 1

> Starting Workload...
 TIME    | TOTAL OPS |  SELECT |  INSERT |  UPSERT |  UPDATE |  DELETE |    AGG | TRANS
 -----------------------------------------------------------------------------------------
 00:01   |     4,462 |   2,568 |   1,390 |       0 |     336 |     168 |      0 |     0
 00:02   |     8,796 |   4,904 |   2,950 |       0 |     638 |     304 |      0 |     0
 00:03   |     7,559 |   4,095 |   2,610 |       0 |     604 |     250 |      0 |     0
 00:04   |     6,973 |   3,680 |   2,570 |       0 |     480 |     243 |      0 |     0
 00:05   |     5,890 |   3,105 |   2,150 |       0 |     433 |     202 |      0 |     0
 00:06   |     5,384 |   3,026 |   1,760 |       0 |     414 |     184 |      0 |     0
 00:07   |     5,782 |   3,007 |   2,170 |       0 |     428 |     177 |      0 |     0
 00:08   |     5,310 |   2,911 |   1,810 |       0 |     418 |     171 |      0 |     0
 00:09   |     4,772 |   2,659 |   1,590 |       0 |     350 |     173 |      0 |     0
 00:10   |     5,613 |   3,117 |   1,920 |       0 |     400 |     176 |      0 |     0
 00:11   |     4,831 |   2,632 |   1,630 |       0 |     387 |     182 |      0 |     0

In addition you will find some other plgm workloads, including a maxperf one that will push the cluster to its limits but reflects in no way a production like environment.

Updating the RK1 Cluster

Use update_rk1cluster.yml to refresh the OS and container images on all cluster nodes, including the control node rk1-node4. The playbook runs one host at a time (serial: 1) to avoid rolling reboots or package conflicts during the upgrade window.

ansible-playbook --ask-vault-pass -i inventory.yml update_rk1cluster.yml
ansible-playbook --ask-vault-pass -i inventory.yml update_rk1cluster.yml -e reboot=true

What it does:

  • updates the apt cache and performs a full apt upgrade on every managed RK1 node
  • pulls all currently tagged Docker images from the local Docker daemon on each host
  • checks whether a reboot is needed because of kernel or package updates
  • reboots only when the reboot=true flag is set and /var/run/reboot-required exists

This is intended for regular maintenance windows and is especially useful after the cluster has been running for a while and Docker image versions need refreshing.

Ansible Vault Variables

The playbooks load encrypted variables from group_vars/all/vault.yml. Create or edit this file with Ansible Vault; do not store a plaintext copy in Git. The file must define the following variables:

Variable Purpose
vault_ansible_user SSH user used to connect to the RK1 nodes
vault_ansible_ssh_private_key_file Path to the SSH private key used by Ansible
vault_bmc_base_url TuringPi BMC HTTPS URL
vault_bmc_user BMC login user
vault_bmc_password BMC login password
vault_bmc_validate_certs Whether Ansible validates the BMC TLS certificate
vault_mongodb_admin_user MongoDB administrator username
vault_mongodb_admin_password MongoDB administrator password
vault_mongodb_admin_password_uri URL-encoded MongoDB password for connection URIs
vault_mongodb_keyfile Replica-set internal authentication key
vault_grafana_admin_password Grafana administrator password
vault_mongodb_monitoring_uri Authenticated connection URI used by the MongoDB exporters on rk1-node1..3

Initialize the encrypted file, or update it after rotating credentials:

ansible-vault create group_vars/all/vault.yml
ansible-vault edit group_vars/all/vault.yml
ansible-vault view group_vars/all/vault.yml

Use the vault password when running any playbook that loads the inventory:

ansible-playbook --ask-vault-pass -i inventory.yml deploy_mongodb_cluster.yml

The vault password is separate from all service passwords. Store it in a password manager or provide it through a protected ANSIBLE_VAULT_PASSWORD_FILE that is excluded by .gitignore. Rotate any credentials that were previously exposed before using this setup in production.

⏻ Stopping & Starting the Whole Platform

Two playbooks manage the full lifecycle of the cluster, useful for power savings overnight or just saving the planet:

# Graceful stop: step down primary → stop mongod containers → stop monitoring
# → shutdown rk1-node1..3's OS → cut power to rk1-node1..3 via the BMC
# (rk1-node4 is left running - it's the Ansible control node)
ansible-playbook --ask-vault-pass -i inventory.yml stop_platform.yml

# Power rk1-node1..3 back on via the BMC → wait for SSH → (re)start mongod containers
# → wait for a PRIMARY to be elected → start the monitoring stack
ansible-playbook --ask-vault-pass -i inventory.yml start_platform.yml

Power control goes through the TuringPi 2 BMC’s REST API (bmcd), not the OS, so rk1-node1..3 are fully de-powered between runs. rk1-node4 is intentionally excluded from both shutdown and BMC power-off since it’s the Ansible control and monitoring host running these very playbooks - the node itself always stays on. BMC connection settings and credentials are stored in Ansible Vault.

View source figure: powersequence

Monitoring: Keeping an Eye on Our ARM Warriors 👁️

We’ve got a comprehensive monitoring setup running on rk1-node4:

Metrics We Track:

  • System Metrics: CPU, Memory, Disk I/O, Network
  • MongoDB Metrics: Operations/sec, Connections, Replica Set Health, and WiredTiger cache usage
  • Power Consumption: Via IoT smart plug integration with metrics fetched from HomeAssistant (full configuration not covered here)
  • Custom ARM Metrics: Temperature, frequency scaling

Dedicated Grafana Dashboard

The provisioned dashboard is designed for the three MongoDB data nodes and uses a six-hour default time range with five-second refresh available. Panels are grouped into three practical areas:

  • Hardware and host health: cluster power, per-node CPU load, RK1 temperature, NVMe read/write throughput, and node memory pressure.
  • MongoDB workload: cluster-wide total operations/sec plus insert, read/query, and update rates.
  • MongoDB engine and cluster health: WiredTiger cache used, configured cache size, cache utilization, current connections, and replica-set state.

The MongoDB exporter is Percona 0.40.0 with --collect-all. The dashboard uses its emitted metric families, including mongodb_ss_opcounters, mongodb_ss_wt_cache_bytes_currently_in_the_cache, mongodb_ss_wt_cache_maximum_bytes_configured, and mongodb_ss_connections. Replica state is read from the rs_state label: 1 means PRIMARY and 2 means SECONDARY.

Grafana’s image renderer is also enabled and available for the dashboard. Use Grafana’s panel or dashboard share controls to generate rendered PNG images for reports, issue tickets, and quick status updates. The renderer is deployed automatically as grafana-image-renderer by setup_monitoring.yml and is secured with a shared authentication token between Grafana and the renderer service.

To install or refresh the dashboard and monitoring stack:

ansible-playbook --ask-vault-pass -i inventory.yml setup_monitoring.yml

Real-World Performance Stories 📈

Story 1: The Endurance Test

We ran our cluster for 72 hours straight with mixed workloads:

  • Zero downtime: Replica set handled failovers gracefully
  • Consistent performance: No degradation over time
  • Power consumption: Averaged <40 Watts

Story 2: The Scalability Surprise

  • Memory efficiency: 16GB proved to be sufficient, but is workload dependent of course.
  • Storage performance: NVMe drives delivered consistent latency

Story 3: The Failover Test

Deliberately powered off rk1-node1 (primary):

  • Failover time: <5 seconds
  • Data consistency: Zero data loss
  • Recovery: Automatic rejoin/resync/take-over when node returned

Operations per Watt: The Little Board’s Superpower ⚡

There is more to the platform than peak ops/s. The dashboard divides MongoDB server-side operations per second by the measured total cluster power. It is a useful live efficiency indicator while the three-node replica set, NVMe, and monitoring stack are all running.

View source figure: MongoDB operations per second per watt

Phase in this capture Approximate MongoDB ops/s/W Reading the graph
50M-document loading ~200–550 Fast inserts; not comparable with the mixed benchmark
Mixed benchmark campaigns Typically ~180–320 Useful sustained operating band, with short higher peaks
Between runs ~20–50 Background activity divided by baseline cluster power

This is MongoDB server operations, not completed YCSB requests. A YCSB SCAN can generate multiple server operations, and the graph does not by itself rank thread counts or prove an ON/OFF THP energy advantage. Part II keeps the power story honest by separating application throughput, server-side operations, and latency.

The Economics: Compact, Not a Matched x86 Shootout 💰

The carrier board is mini-ITX sized, and the four RK1 modules provide three database members plus a dedicated control/monitoring node. The parts list below gives an indicative build budget; prices and power tariffs vary. We did not benchmark a like-for-like x86 system, so no quantified TCO or percentage savings against x86 are claimed here.

Lessons Learned & Pro Tips 🎓

What Worked Amazingly:

  1. ARM Performance: Way better than expected for database workloads
  2. Power Efficiency: Game-changer for edge deployments
  3. Ansible Automation: Made deployment reproducible, idempotent and fast
  4. Container Strategy: Docker simplified MongoDB management - probably slightly better performance on bare metal.
  5. Monitoring Stack: Node Exporter + Prometheus + Grafana provided excellent visibility, and easy connection to external datasource (HomeAssistant for power)
  6. Percona Load Generator for MongoDB (PLGM) : Excellent load/stress test coming with a web UI, insights, and lots of stuff. Got easily recompiled for ARM from the go source from github.
  7. YCSB: Old load testing application, quite complex to run on a recent ARM Linux distro. Using a container image simplified this considerably, while also making results less directly comparable to existing benchmarks.

Gotchas We Hit:

  1. ARM Docker Images: Make sure you pull arm64 variants
  2. Memory Management: Tune MongoDB for ARM’s memory characteristics
  3. NVMe Formatting: XFS with specific mount options crucial for performance
  4. Transparent Huge Pages: Since MongoDB 8, THP is recommended. The issue is that the RK1 default kernel is not built with THP. Recompiling the kernel is mandatory and quite time consuming….
  5. The 500k record-count trap: An earlier 500,000-document load was followed by reuse runs with the playbook’s 50,000,000 default recordcount. YCSB generated keys for documents that did not exist; one observed run recorded 3,876 successful READs and 490,899 failed READs. Those runs were discarded. The playbook now checks the actual usertable document count against ycsb_record_count before benchmarking and passes that value explicitly to go-ycsb. Always specify -e ycsb_database=ycsbsmall -e ycsb_record_count=500000 for the 500k collection.
  6. No reliable dataset drop in the bundled go-ycsb workflow: A load against an existing collection produced duplicate-key errors instead of a clean replacement. When load_dataset=true, the playbook explicitly drops MongoDB’s usertable collection on the primary (with majority write concern) before running go-ycsb load. This destroys the old test collection; use it only when a fresh benchmark load is intended.
  7. No effective maxexecutiontime in this go-ycsb build: A very large operationcount keeps the workload from ending early. The playbook uses GNU timeout to end each run after 600 seconds, sending SIGINT and allowing a 30-second shutdown grace period. The loader is a separate, untimed preparation phase.

Where the Cluster Fits ⭐

This build is particularly interesting for edge computing, IoT ingestion, development replica sets, and teaching distributed systems. The RK1 is not a drop-in substitute for a high-end x86 server: analytics, legacy x86 software, and workloads that need maximum single-thread performance deserve separate capacity tests.

Your Turn: Build and Operate It 🚀

The indicative parts list is one TuringPi 2 board, four 16 GB RK1 modules, four NVMe drives, heatsinks, an ATX PSU, cables, and a case. See the hardware table above for roles and specifications; confirm current prices and storage capacity before buying.

gh repo clone vthinsel/mongodb-TuringPI-rk1
cd mongodb-TuringPI-rk1
./setup_facility-node.sh
ansible-playbook --ask-vault-pass -i inventory.yml deploy_mongodb_cluster.yml
ansible-playbook --ask-vault-pass -i inventory.yml setup_monitoring.yml

To power down or bring the cluster back, use stop_platform.yml and start_platform.yml. Authentication should remain enabled outside an isolated benchmark environment. Contributors are welcome to bring another workload, ARM platform, or real-world power measurement to the GitHub repository.

What Part I Proves—and What It Does Not

Four compact RK1s can host a three-node MongoDB replica set plus its own facility node, survive deliberate primary failover, and run meaningful PLGM stress workloads within a small power envelope. The platform story does not tell us why performance changes when the working set grows. That is the question for Part II: does THP help once WiredTiger starts evicting pages, what is TCMalloc doing with memory, and where do extra YCSB threads stop paying for themselves?