VeltrixDB Docs
v1.1.0 GitHub FAQ Quickstart

Kubernetes Operator

The VeltrixDB Kubernetes Operator manages VeltrixCluster custom resources — provisioning the StatefulSet, NVMe storage, and networking, and reconciling scale, upgrade, and recovery events without manual intervention.

Overview

The Operator is built with Kubebuilder and watches a single CRD, VeltrixCluster (group veltrix.io/v1alpha1, short name vc). On each reconcile pass it creates or updates the child resources needed to run a cluster: a StorageClass, an NVMe-provisioning DaemonSet, a ServiceAccount, headless and client Service objects, a ConfigMap, a ServiceMonitor (best-effort, if the Prometheus Operator CRDs are installed), and the StatefulSet itself.

Prerequisites

RequirementDetail
Kubernetes1.27+
Go1.21+ (only needed if building the operator from source)
NodesLocal NVMe SSDs — see the per-cloud notes below
GKENode pool created with --local-ssd-interface=NVME
AWSInstance types with local NVMe storage: i3, i3en, i4i, im4gn, is4gen
AzureLsv2/Lsv3 series with local NVMe storage
GKE requirement Create node pools with --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 that VeltrixDB's per-disk WAL/VLog layout depends on.

Installation

Install the CRDs, then deploy the operator's controller manager:

# 1. Install the CustomResourceDefinitions
kubectl apply -f config/crd/bases/

# 2. Deploy the operator (controller manager Deployment + RBAC)
kubectl apply -f config/manager/manager.yaml

Before creating any VeltrixCluster, deploy the NVMe mount-keeper DaemonSet so real XFS filesystems are bind-mounted at the PV source paths before kubelet bind-mounts them into the veltrixdb pod. Skipping this step causes ENOSPC on the very first WAL write.

kubectl apply -f config/manager/nvme-mount-keeper.yaml
kubectl rollout status daemonset/nvme-mount-keeper -n veltrixdb

See NVMe Disk Provisioning for why the keeper is required on GKE COS, AWS Bottlerocket, and any OS where kubelet runs inside its own mount namespace.

VeltrixCluster spec reference

The fields below are read directly from the VeltrixClusterSpec Go type (api/v1alpha1/veltrixcluster_types.go). Defaults marked operator default are filled in by the reconcile loop's applyDefaults step when the field is left unset in the CR.

Core

FieldTypeDefaultDescription
sizeint323Number of VeltrixDB nodes. Each node owns a contiguous shard range. Minimum 1.
versionstring1.0.0Container image tag.
imagestringghcr.io/veltrixdb/veltrixdbContainer image repository (combined with version to form the full image reference).
replicationFactorint323Replica copies per shard (min 1, max 5). The operator reports a ReplicationFactorMet condition when live replicas fall below this.
cacheMaxSizeMBint3265536LIRS cache size in MiB, passed as -cache. Minimum 256.
storageClassNamestringlocal-nvmeStorageClass used for NVMe PVCs — must match the class the provisioner creates.
disksPerNodeint328NVMe disks (and PVCs) per pod. Min 1, max 16.
diskSizequantity375GiCapacity requested per PVC.
resourcesResourceRequirementsStandard Kubernetes compute resource requests/limits for the veltrixdb container.

Networking

FieldDefaultDescription
gossipPort7946UDP port for the gossip failure detector.
serverPort9000TCP port for the binary/text protocol.
metricsPort2112Prometheus metrics, /healthz, /readyz.
tlsPort9443TLS listener; only active when tlsSecretName is set. The plain serverPort stays up for internal cluster traffic.

Scheduling

FieldDefaultDescription
podAntiAffinityTopologyKeykubernetes.io/hostnameTopology key for required pod anti-affinity — one VeltrixDB pod per physical host.
tolerationsApplied verbatim to the StatefulSet pod template.
nodeSelectorConstrains pod scheduling to matching nodes.
imagePullSecretsFor private registries.

Storage engine tuning

FieldDefaultDescription
numShards1024Index shard count. Must be a power of 2 and match the compiled C++ kNumShards constant. Changing on a live cluster requires a full data migration.
keyValueSeparationtrueEnables WiscKey-style KV separation — only key metadata lives in SSTables; values append to a per-disk VLog.
vlogGCThreshold"0.40" (operator default; CRD doc states 0.30)Decimal string in [0.0, 1.0]. VLog garbage fraction that triggers defragmentation.
lirRatio"0.90"Fraction of the LIRS cache reserved for the high-locality (LIR) resident set.
autoTunefalseWhen true, the binary runs hardware detection at startup and derives cache size, shard count, compaction threads, and I/O mode itself; the operator omits -cache and passes -auto-tune instead.
sysTunetrueHost-level tuning (sysctl, hugepages, CPU governor, IRQ balance, RLIMIT_NOFILE) via hardware.SysTune. Linux only; only meaningful when autoTune is true. Requires CAP_SYS_ADMIN or root.
checkpointIntervalMs0 (disabled)WAL checkpoint interval. When non-zero, passed as -wal-checkpoint-interval-ms; checkpoint always runs on clean shutdown regardless.

WAL / VLog flush

FieldDefaultDescription
walFlushWindowMs10WAL group-commit flush window (ms). Must equal vlogFlushWindowMs.
vlogFlushWindowMs10VLog fdatasync group-commit window (ms). WAL and VLog are submitted concurrently; effective P99 is the max of the two.
walMaxBatchEntries4096Caps the WAL group-commit batch size before the flush timer fires early. Min 1, max 65536.

Security

FieldDescription
tlsSecretNameName of a kubernetes.io/tls Secret with tls.crt/tls.key. When set, the binary is started with -tls-addr, -tls-cert, -tls-key.
tlsCASecretNameSecret containing ca.crt for mTLS client verification; passed as -tls-ca alongside tlsSecretName.
authConfigSecretSecret with key auth-config.json containing RBAC user definitions. Passed as -auth-config. Permission bitmask: PermRead=1, PermWrite=2, PermDelete=4, PermAdmin=8.

Raw NVMe block-device VLog

Set rawVLog to write the VLog directly to /dev/nvmeXnY block devices, skipping the filesystem for the value-bytes path. WAL and segment files still use the per-pod XFS PVCs.

FieldDefaultDescription
rawVLog.enabledfalseWhen true, the operator adds SYS_RAWIO to the container's capability set, mounts /dev as a HostPath volume, passes -raw-vlogs to the binary, sets seccompProfile: Unconfined and an unconfined AppArmor annotation, and runs the container privileged: true as root (required to pass the cgroup device allowlist on GKE COS).
rawVLog.devices[]Explicit block-device list, paired index-by-index with the per-pod NVMe PVCs. When empty, the operator infers /dev/nvme0n1/dev/nvme(disksPerNode-1)n1 (or the p2 partition suffix when xfsPartitionPercent > 0) — the GKE local-NVMe convention.
rawVLog.xfsPartitionPercent0 (whole-disk XFS)Percentage of each NVMe disk reserved as an XFS partition for WAL/segments; the remainder stays a raw partition for VLog values. Min 5, max 50; typical production value 20.
Provisioning gotcha Devices intended for raw VLog use must not be mkfs.xfs'd by the NVMe provisioner. WAL/segment XFS partitions belong on a separate slice of each disk (set via xfsPartitionPercent) or on the boot disk.

Example CustomResource

Adapted from the operator's shipped production sample (config/samples/veltrix_v1alpha1_veltrixcluster.yaml) — 3 nodes, 480 GiB RAM, 8 NVMe disks each, replication factor 3, raw block-device VLog with a 20/80 XFS/raw partition split:

apiVersion: veltrix.io/v1alpha1
kind: VeltrixCluster
metadata:
  name: veltrixdb-prod
  namespace: veltrixdb
spec:
  # Cluster topology
  size: 3
  version: "1.0.0"
  image: "ghcr.io/veltrixdb/veltrixdb"
  replicationFactor: 3

  # Cache — 256 GiB LIRS cache per node (80% rule for 480 GiB nodes)
  cacheMaxSizeMB: 262144

  # Storage
  storageClassName: local-nvme
  disksPerNode: 8
  diskSize: "375Gi"

  # Resources (n2-highmem-96 class)
  resources:
    requests:
      memory: "480Gi"
      cpu: "60"
    limits:
      memory: "480Gi"
      cpu: "60"

  # Network ports
  serverPort: 9000
  metricsPort: 2112
  gossipPort: 7946

  # Raw NVMe block-device VLog: 20% XFS (keys+WAL), 80% raw (values)
  rawVLog:
    enabled: true
    xfsPartitionPercent: 20

  # Scheduling — one pod per physical host
  podAntiAffinityTopologyKey: "kubernetes.io/hostname"
  nodeSelector:
    cloud.google.com/gke-local-nvme-ssd: "true"
  tolerations:
    - key: workload
      value: veltrixdb
      effect: NoSchedule
kubectl apply -f veltrixcluster.yaml
kubectl get veltrixcluster -n veltrixdb

Status fields

FieldValuesMeaning
phaseProvisioning / Running / Scaling / Degraded / TerminatingOverall cluster lifecycle state, computed from ready-pod count each reconcile.
readyReplicasintegerPods currently passing the readiness probe.
currentVersionstringImage tag from spec.version at last reconcile.
partitionMapstringSummary of the current shard-to-node assignment; updated after a resharding event.
conditionsReady, ReplicationFactorMetStandard Kubernetes conditions. ReplicationFactorMet=False carries the message "DATA LOSS RISK" when ready pods drop below replicationFactor.
lastScaleTimetimestampSet when a resharding event is triggered.
kubectl describe veltrixcluster my-cluster -n veltrixdb

What the reconcile loop actually does

This section is scoped to what is verifiably implemented in controllers/veltrixcluster_controller.go, not just described in prose elsewhere.

Child resource management

Each reconcile pass creates or updates, in order: the local-nvme StorageClass, an NVMe-provisioner DaemonSet (plus its ServiceAccount/ClusterRole/ClusterRoleBinding), the pod ServiceAccount, headless and client Service objects, a best-effort ServiceMonitor (a reconcile failure here only logs a warning — it does not fail the whole reconcile, since the Prometheus Operator CRDs may not be installed), the ConfigMap of engine env vars, and finally the StatefulSet.

Rolling upgrades — confirmed

The StatefulSet is built with updateStrategy.type: RollingUpdate and rollingUpdate.maxUnavailable: 1. Bumping spec.version (or spec.image) and re-applying the CR causes Kubernetes' native StatefulSet controller to replace one pod at a time, waiting for each to become ready before proceeding — the operator does not implement custom upgrade orchestration beyond setting this strategy.

kubectl patch veltrixcluster my-cluster -n veltrixdb \
  --type merge --patch '{"spec":{"version":"1.1.0"}}'

Auto-reshard on replica count changes — confirmed, with caveats

The reconciler compares the StatefulSet's previous replica count to spec.size on every pass. When the cluster has grown (prevSize < spec.size), it calls triggerResharding, which:

  1. Blocks (up to 5 minutes) until all desired pods report PodReady.
  2. Annotates every pod with veltrix.io/cluster-size and veltrix.io/prev-cluster-size. The VeltrixDB binary is expected to watch these annotations via the downward API and recompute its consistent-hash partition map / trigger the actual shard rebalance — the operator's role is limited to triggering the reshard, not performing the data movement itself.
  3. Records status.lastScaleTime and a status.partitionMap summary string.
Scale-down is not specially handled The resharding trigger only fires when the new size is greater than the previous size. Scaling down updates the StatefulSet's replica count (removing the highest-ordinal pods) but does not go through the same annotate-and-wait resharding path.

Self-healing — partially confirmed

The operator itself does not run a custom pod-replacement loop; it relies on the standard StatefulSet controller to recreate any pod that is deleted or fails its liveness probe, which is the same "self-healing" every StatefulSet gets from Kubernetes. What the operator adds on top:

Scope of "self-healing" Pod replacement is delegated to the built-in StatefulSet controller, not a bespoke operator routine. Data rebuild after a replacement is a function of the VeltrixDB binary's own replication/recovery path (WAL replay, replica resync), not code inside the operator's reconcile loop.

Graceful WAL flush on termination — confirmed

Every pod gets a preStop lifecycle hook (kill -SIGTERM 1 && sleep 55) and a 60-second terminationGracePeriodSeconds, giving the engine time to flush its WAL before the container is killed. On VeltrixCluster deletion, the operator's finalizer (veltrix.io/finalizer) annotates pods as draining before removing itself — the actual flush is still performed by the pod's own SIGTERM handling and the grace period, not a separate operator-issued flush command.

Scaling

kubectl patch veltrixcluster my-cluster -n veltrixdb \
  --type merge --patch '{"spec":{"size":5}}'

Increasing spec.size triggers the StatefulSet to add pods and, once they are ready, the reshard-trigger flow described above.

Troubleshooting

SymptomCause / fix
Pod crashes with write /mnt/nvmeN/wal.log: no space left on deviceThe pod started before the mount-keeper bound real XFS at the PV source path, so kubelet bind-mounted a 256 KB tmpfs placeholder. Confirm the keeper DaemonSet is Running, then delete the pod so kubelet re-creates the bind mount.
Cluster stuck in ProvisioningCheck kubectl get pv; on GKE confirm the node pool used --local-ssd-interface=NVME; check operator logs via kubectl logs -n veltrixdb-system deploy/veltrixdb-controller-manager.
Resharding not triggering after adding nodesThe operator only watches spec.size changes on reconcile. Force one with kubectl annotate veltrixcluster my-cluster force-reconcile=$(date +%s).
WAL flush timeout on pod terminationIf pods terminate faster than the 30–60s grace window, raise terminationGracePeriodSeconds in the StatefulSet spec.
Cluster Autoscaler deletes database nodes (local NVMe data lost permanently)The operator adds cluster-autoscaler.kubernetes.io/safe-to-evict: "false" automatically; if pods were created by an older operator version, annotate them manually.