Helm Chart
The VeltrixDB Helm chart deploys a production-ready cluster on Kubernetes with per-pod local NVMe storage, Prometheus monitoring, and safe rolling updates — optimized for GKE but portable to any cloud with local NVMe instance types.
VeltrixDB-Helm-Chart, not in the main veltrixdb repository, and is versioned separately. The template list, values.yaml keys, defaults and the bundled alert rules on this page were taken from that repository and have not been re-verified for server v1.12 — check values.yaml and templates/ in the chart version you install. Statements about what the server does with those values are checked against the main repository.Prerequisites
| Requirement | Detail |
|---|---|
| Kubernetes | 1.27+ |
| Helm | 3.x |
| Prometheus Operator | 0.65+ (only required if serviceMonitor.enabled) |
| GKE node pools | Created with --local-ssd-interface=NVME |
--local-ssd-interface=NVME. Without it, GKE merges all local SSDs into a single RAID-0 device and you lose the parallel I/O benefit.gcloud container node-pools create veltrixdb-pool \
--cluster=my-cluster \
--zone=us-central1-a \
--machine-type=n2-highmem-64 \
--num-nodes=3 \
--local-ssd-count=8 \
--local-ssd-interface=NVME \
--disk-size=100GB
Installation
# Add the chart repo
helm repo add veltrixdb https://charts.veltrixdb.io
helm repo update
# Install with defaults
helm install veltrixdb veltrixdb/veltrixdb \
--namespace veltrixdb \
--create-namespace
# Install with custom values
helm install veltrixdb veltrixdb/veltrixdb \
--namespace veltrixdb \
--create-namespace \
--values my-values.yaml
Verify the deployment
# Wait for PVs (3 nodes x 8 disks = 24 volumes)
kubectl get pv | grep local-nvme
# Check pods are running
kubectl get pods -n veltrixdb -l app.kubernetes.io/name=veltrixdb
# Test connectivity (the image is distroless: no shell or nc inside the pod)
kubectl port-forward -n veltrixdb veltrixdb-0 9000:9000 &
echo PING | nc 127.0.0.1 9000
# Expected: PONG
What the chart deploys
Per the templates in VeltrixDB-Helm-Chart/templates/ (chart repo): statefulset.yaml, service.yaml, service-headless.yaml, servicemonitor.yaml, pdb.yaml, prometheusrule.yaml, configmap.yaml, secret.yaml, rbac.yaml, and _helpers.tpl.
- StatefulSet — VeltrixDB pods with stable per-pod hostnames (
podManagementPolicy: Parallel, one volume-claim template per NVMe disk). - Headless + client Services — the headless service backs gossip/StatefulSet DNS (
publishNotReadyAddresses: trueso bootstrap peers are reachable pre-readiness); the client service fronts application traffic. - ServiceMonitor — Prometheus Operator scrape config, one target per pod.
- PodDisruptionBudget — bounds voluntary disruptions during node drains and upgrades.
- PrometheusRule — a bundled Prometheus alerting ruleset (see below).
- ConfigMap / Secret — engine configuration env vars and a bootstrap token. Server fact: the server reads no bootstrap token. The node-to-node cluster secret it does read (
VELTRIXDB_CLUSTER_SECRETor--cluster-secret-file, ≥ 16 bytes, new in v1.11) must be wired into the pod yourself if your chart version has no value for it; without it (or cluster mTLS) the transfer listener is unauthenticated. - RBAC — ServiceAccount plus any Role/RoleBinding the pods need.
daemonset.yaml in this chart's templates/ directory. Disk formatting (XFS) and the kubelet mount-namespace workaround for GKE/EKS/AKS are handled by the nvme-prep / mount-keeper DaemonSets that the Kubernetes Operator provisions dynamically per VeltrixCluster, or that you apply directly from VeltrixDB-Kubernetes-Operator/config/manager/nvme-mount-keeper.yaml if running the Helm chart without the operator. Deploy it before helm install — see NVMe Disk Provisioning.Pod Disruption Budget
The chart's default values.yaml sets podDisruptionBudget.maxUnavailable: 1 — at most one pod may be voluntarily disrupted at a time, which for a 3-node, RF-3 cluster keeps at least 2 nodes available during drains and rolling upgrades.
podDisruptionBudget:
enabled: true
maxUnavailable: 1
PrometheusRule alerts
The chart's PrometheusRule (rendered when serviceMonitor.enabled: true; chart repo, not re-verified) was counted at 24 alert rules in 10 rule groups, spanning write/read/delete latency, VLog GC health, cluster membership, replication, per-disk health, background scrubbing, and Bloom filter efficiency.
| Rule group | Alerts | Covers |
|---|---|---|
veltrixdb.write | 2 | VeltrixHighWriteLatency (P99 > 50ms), VeltrixWALFlushStalled |
veltrixdb.read | 2 | VeltrixHighReadLatency (P99 > 5ms), VeltrixLowCacheHitRate (< 50%) |
veltrixdb.delete | 2 | VeltrixHighDeleteLatency, VeltrixTombstoneBacklogGrowing |
veltrixdb.vlog | 5 | VeltrixVLogHighGarbageRatio, VeltrixVLogGCStalled, VeltrixVLogGCPermanentlyPaused, VeltrixVLogGCReadErrors, VeltrixVLogFileSizeGrowing |
veltrixdb.cluster | 4 | VeltrixNodeDown, VeltrixClusterSizeLow, VeltrixFrequentRebalances, VeltrixNodeFailed |
veltrixdb.replication | 2 | VeltrixHighReplicationLag (> 100ms) — if it reads veltrixdb_replication_lag_nanoseconds it can never fire, because the server always reports 0 there; VeltrixReplicationFailures. Replication series exist only in --mode=replicated |
veltrixdb.disk | 2 | VeltrixSlowDisk (per-disk latency EWMA ≥ 5x cluster median), VeltrixDiskWriteLatencyHigh (> 20ms) |
veltrixdb.scrub | 2 | VeltrixCorruptionDetected (CRC32C/magic mismatch), VeltrixScrubReadErrors |
veltrixdb.bloom | 1 | VeltrixBloomFalsePositiveRateHigh (> 10%, design point ~1%) |
veltrixdb.gc | 2 | VeltrixGCEmergencyRunsSustained (garbage ≥ 65% for 30 min), VeltrixWriteAdmissionThrottling |
templates/prometheusrule.yaml gave 24. Either way, before relying on a rule, confirm every series it queries exists in metrics/prometheus.go (see Monitoring): v1.16 and earlier export only veltrixdb_* families, no process_* or go_*, and some families are always 0. The main repository's own monitoring/prometheus-rules.yaml (10 alerts in v1.16) is checked against the server. New on main after v1.16 — ships in the next release the server also serves go_*, process_*, search, raft and replication-recovery metrics and the rules file has 24 rules; according to the server repo, the chart’s process-restart, search and raft groups read metrics that releases up to v1.16 do not export, so they render only with prometheusRule.extendedAlerts=true — set it once the image serves veltrixdb_search_* (not re-verified against the chart repository).The most operationally important two: VeltrixCorruptionDetected fires the moment the background scrubber finds a CRC32C or magic mismatch (severity critical, for: 0m) — treat as an immediate SMART-check-and-resync trigger. VeltrixGCEmergencyRunsSustained fires when VLog garbage has sat above the emergency threshold (65%) for 30 minutes and GC is bypassing admission control just to keep up — it means sustained write rate is exceeding GC throughput and you need to reduce load or add disks.
Configuration reference
Key settings from values.yaml. Full defaults are tuned for GKE n2-highmem-96 nodes (480 GB RAM, 96 vCPU, 8 x 375 GiB local NVMe SSDs).
| Parameter | Default | Description |
|---|---|---|
replicaCount | 3 | Number of VeltrixDB pods; must match node pool size for correct anti-affinity placement. |
image.tag | v1.0 | VeltrixDB container image tag. |
storage.disksPerNode | 8 | NVMe disks per pod; must match --local-ssd-count on GKE. |
storage.diskSizeGi | 375 | Capacity per NVMe disk (GKE local SSD = 375 GiB). |
storage.storageClassName | local-nvme | StorageClass created by the infra provisioner. |
storage.numShards | 1024 | Has no effect on the server: the index always uses its compile-time shard count (8192), and the server exposes no shard flag. Leave it alone. |
storage.keyValueSeparation | true | WiscKey-style KV separation — keys in SSTables, values in per-disk VLog. |
storage.vlogGCThreshold | 0.30 | VLog garbage fraction that triggers defragmentation. |
storage.walFlushWindowMs | 10 | WAL group-commit window (ms), passed as -wal-flush-window-ms. Must equal vlogFlushWindowMs. The chart's value overrides the server's own default of 15 ms. |
storage.vlogFlushWindowMs | 10 | VLog fdatasync group-commit window (ms), passed as -vlog-flush-window-ms (server default 15 ms). |
storage.walMaxBatchEntries | 4096 | Max WAL entries per group-commit flush before early timer fire. |
storage.rawVlog.enabled | false | Write VLog directly to /dev/nvmeXnY, bypassing XFS (server --raw-vlogs). Adds SYS_RAWIO capability and a /dev hostPath mount; the server also needs root access to the device nodes. |
storage.rawVlog.devices | [] | Explicit block-device paths; when empty, inferred as /dev/nvme0n1 ... /dev/nvme(N-1)n1 from disksPerNode. |
cache.maxSizeMB | 262144 | LIRS cache size in MiB — 256 GiB, sized for 480 GiB nodes (80% rule). |
cache.lirRatio | 0.90 | Fraction of cache reserved for the high-locality (LIR) resident set. |
server.port | 9000 | Plain TCP protocol port. |
server.tlsPort | 9443 | TLS port, active only when tls.enabled. |
server.metricsPort | 2112 | Prometheus metrics + /healthz + /readyz. |
cluster.gossipPort | 7946 | Gossip port. Server fact: gossip is TCP and peers always dial client port + 3, so the server's derived port is what must be reachable between pods (along with + 1 replication, + 2 Raft, + 5 transfer). |
cluster.replicationFactor | 3 | Desired replica count per shard. Server fact: the server has no replication-factor flag; it uses RF 3. |
resources.requests.memory | 480Gi | Memory request per pod. |
resources.requests.cpu | 64 | CPU request per pod. |
resources.requests.hugepages-2Mi | 2Gi | Reserves hugepages for the pod. The server does not use them: nothing in it enables hugepages. You can drop this request (and the host vm.nr_hugepages requirement) unless your chart version needs it for scheduling. |
autoTune.enabled | false | Auto-detect hardware and self-size the cache (server -auto-tune; the shard count is fixed at 8192 regardless). |
autoTune.sysTune | true | Apply sysctl/hugepages/scheduler tuning; requires CAP_SYS_ADMIN or root. |
auth.enabled | false | Enable RBAC authentication. |
auth.configSecret | "" | Secret name with key auth-config.json. |
tls.enabled | false | Enable TLS 1.3 on the server port. |
tls.existingSecret | "" | Secret with tls.crt/tls.key. |
tls.caSecret | "" | Secret with ca.crt; when set, enables mTLS. |
serviceMonitor.enabled | true | Create the ServiceMonitor + PrometheusRule. |
serviceMonitor.interval | 15s | Prometheus scrape interval. |
podDisruptionBudget.enabled | true | Create the PDB. |
podDisruptionBudget.maxUnavailable | 1 | Max pods voluntarily disrupted at once. |
livenessProbe / readinessProbe | /healthz @ 30s+10s x3 / /readyz @ 15s+5s x6 | HTTP probes on metricsPort. |
terminationGracePeriodSeconds | 60 | Grace period before SIGKILL. A clean SIGTERM shutdown writes a compacted WAL checkpoint; a SIGKILL instead leaves the full WAL to replay on the next start. |
affinity.podAntiAffinity | required, kubernetes.io/hostname | One VeltrixDB pod per physical host (NVMe disks are node-local). |
topologySpreadConstraints | maxSkew: 1, zone | Spreads pods evenly across zones. |
Performance tuning
For 100K+ writes/s, lower the flush windows (both must stay equal):
storage:
walFlushWindowMs: 5
vlogFlushWindowMs: 5
For maximum cache hit rate, give the cache most of the node's RAM, leaving headroom for the index (~142 B/key with the native index), the Go heap and the OS (the server's -auto-tune uses 85% of 80% of RAM):
cache:
maxSizeMB: 409600 # 400 GB on a 512 GB node
Node, seccomp and image notes
- Image. The official image is built with
CGO_ENABLED=1on agcr.io/distroless/cc-debian12base, so it uses the off-heap native index by default. New in v1.6 Build with--build-arg CGO_ENABLED=0for the old static pure-Go image (Go map index). cgo code is compiled for-march=x86-64-v2on amd64, never-march=native. vm.max_map_countmust be set on the node. The native index needsvm.max_map_count ≥ 262144at large key counts. It is a node-level sysctl that a container cannot set, so configure it in the node pool / node image or with a privileged DaemonSet (scripts/sysctl.confhas the value). Otherwise setVELTRIXDB_INDEX=mapin the pod env.- seccomp and io_uring. The Kubernetes
RuntimeDefaultseccomp profile blocksio_uring_setup. That only matters if you opt in to the io_uring VLog write bridge (VELTRIXDB_URING_BRIDGE=on|sqpoll; default off), and even then the bridge falls back topwrite. To actually use it, the pod needs a seccomp profile allowingio_uring_setup,io_uring_enterandio_uring_register. The default configuration and the native index need nothing special.
Security, audit, and data-integrity features
TLS and RBAC auth
auth:
enabled: true
configSecret: veltrixdb-auth
tls:
enabled: true
existingSecret: veltrixdb-tls
At-rest encryption (AES-256-GCM)
kubectl create secret generic veltrixdb-encryption \
-n veltrixdb \
--from-literal=key=$(openssl rand 32 | base64)
security:
encryptionAtRest: true
encryptionSecretName: veltrixdb-encryption
Each pod loads the key from the VELTRIXDB_ENCRYPTION_KEY env var at startup; without the secret, startup fails when security.encryptionAtRest is true. Key rotation is not implemented — never change the Secret under existing data (see Disaster Recovery). On v1.13 and earlier, atomic ops (CAS/INCR/DECR/SETNX) store their values unencrypted. Fixed in v1.14 they are encrypted like PUT.
Background scrubber
scrubber:
enabled: true
mbPerSec: 50
CRC32C re-validation of every VLog record at a controlled rate (server -scrub-mb-per-sec, default 50 MB/s per disk; 0 disables it); 50 MB/s is roughly a 2-hour full pass over 375 GB. Mismatches increment veltrixdb_scrub_corruption_total and log [scrub] disk=N offset=M … MISMATCH.
Raw NVMe block-device VLog
For latency-critical clusters, enable storage.rawVlog.enabled to write the VLog directly to block devices instead of XFS files. WAL and segment files continue to live on the XFS PVCs.
storage:
rawVlog:
enabled: true
devices:
- /dev/nvme0n1
- /dev/nvme1n1
- /dev/nvme2n1
- /dev/nvme3n1
- /dev/nvme4n1
- /dev/nvme5n1
- /dev/nvme6n1
- /dev/nvme7n1
Server requirements for this mode (root, CAP_SYS_RAWIO, one device per data dir, the 4 KB superblock) are on NVMe Disk Provisioning. No raw-vs-XFS latency comparison is published in the main repository, and online backups contain no VLog data in this mode.
mkfs.xfs on devices destined for raw VLog use. WAL/segment XFS partitions must live on a separate slice of each disk or on the boot disk.Production values.yaml example
replicaCount: 3
image:
tag: "1.12" # set to the server release you run; check the tag format your registry uses
storage: # one storage: block (a second one would replace the first)
disksPerNode: 8
diskSizeGi: 375
walFlushWindowMs: 10
vlogFlushWindowMs: 10
cache:
maxSizeMB: 262144 # 256 GiB - recommended for 480 GiB nodes
resources:
requests:
memory: 480Gi
cpu: "60"
limits:
hugepages-2Mi: 2Gi
autoTune:
enabled: false
cluster:
gossipPort: 7946
replicationFactor: 3
auth:
enabled: true
configSecret: veltrixdb-auth
tls:
existingSecret: veltrixdb-tls
serviceMonitor:
enabled: true
interval: 15s
podDisruptionBudget:
maxUnavailable: 1
Upgrading
helm upgrade veltrixdb veltrixdb/veltrixdb \
--namespace veltrixdb \
--values my-values.yaml
The StatefulSet uses RollingUpdate with maxUnavailable: 1, so pods are replaced one at a time. The PodDisruptionBudget bounds voluntary disruptions from node drains that happen concurrently with the rollout. Server facts for upgrades: the binary WAL needs no upgrade step (rolling back to v1.5 or earlier needs one clean restart with --wal-format=text; Fixed in v1.14 rolling back from that release to v1.13 or older needs --wal-format=text-legacy instead, because TTL’d keys’ WAL records carry their expiry; v1.14 and later, v1.16 included, need no WAL step). In raft mode upgrade followers first and the leader last, one pod at a time, each only after the restarted pod has applied a fresh write — maxUnavailable: 1 does not wait for that (see Rolling upgrade), and after each restart searches are refused until the node's index rebuild finishes (INFO → search_ready=1), which /readyz does not wait for.
Validating a deployment
scripts/bench.sh in the main repository is a local harness: it builds its own server and load generator, starts the server on ADDR (default 127.0.0.1:9000) and wipes /tmp data dirs, so it cannot be pointed at a deployed cluster. Run it on a node of the same hardware class to gate a build (packing density ≤ 1.2 × (24 + value size) bytes/record, zero GC emergency runs, read P99 ≤ 5 ms; exit 0 = pass, log in /tmp/veltrixbench-out/run.log).
To exercise the deployed cluster itself, drive it with the load tester through a port-forward:
kubectl port-forward -n veltrixdb veltrixdb-0 9000:9000 2112:2112 &
git clone https://github.com/VeltrixDB/veltrixdb.git && cd veltrixdb
go run ./cmd/loadtest --addr=127.0.0.1:9000 \
--mode=mixed --concurrency=64 --duration=30 \
--num-keys=1000000 --value-size=128 --read-ratio=0.7
curl -s localhost:2112/metrics | grep -E 'gc_emergency_runs_total|write_admission_throttles_total'
A port-forward adds its own latency; use the numbers as a smoke test, not a benchmark.
Uninstalling
helm uninstall veltrixdb --namespace veltrixdb
PVCs are not deleted automatically, to protect your data:
kubectl delete pvc -n veltrixdb -l app.kubernetes.io/name=veltrixdb
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Pods stuck in Pending | Check PVs are Available; verify the nvme-prep DaemonSet pods are Running; confirm --local-ssd-interface=NVME was set on the node pool. |
| Write throughput lower than expected on macOS / dev environments | Do not size from macOS at all: the engine calls plain fsync(2) on Darwin, which returns at the drive cache (~0.02 ms) without flushing it, so macOS write numbers are optimistic and not power-loss-safe. Loopback also caps round trips at ~60K/s. Benchmark on Linux NVMe. |
| WAL batch size is 1 (no amortisation) | Compare veltrixdb_storage_wal_flushes_total to veltrixdb_storage_writes_total; if roughly equal, raise storage.walFlushWindowMs and storage.vlogFlushWindowMs together. |
io_uring buffer registration fails at startup | Only possible if you opted in to the io_uring VLog bridge (VELTRIXDB_URING_BRIDGE=on|sqpoll; default off). It needs RLIMIT_MEMLOCK or CAP_IPC_LOCK (add IPC_LOCK to securityContext.capabilities.add), and a seccomp profile that allows io_uring_* — see Node, seccomp and image notes. |
panic: native index: insert failed (out of memory?) | The node's vm.max_map_count is too low for the native index. Set it to 262144 on the node (see node notes), or set VELTRIXDB_INDEX=map in the pod env. |
| VLog not being compacted | Port-forward 2112 and check the veltrixdb_vlog_gc_* counters (gc_skipped_ratio_total, gc_skipped_paused_total, gc_skipped_empty_total say why GC is not running); the image has no shell, so curl inside the pod is not possible. |