Orbtrace

Setting up Doris

How to provide the Doris telemetry store that the Helm chart points at with doris.host — choosing where Doris runs, the requirements, why in-cluster Doris uses the operator, installing the operator, applying the DorisCluster, and bringing Orbtrace up against it.

On Kubernetes the Helm chart does not deploy Doris — doris.mode defaults to external. You provide Doris and point the chart at it with doris.host. This page covers every supported way to do that, end to end.

Do this before `helm install`

The Orbtrace pod applies its schema to Doris before it binds the web port, so it stays un-Ready until Doris is reachable. Stand Doris up and confirm it is healthy first, then install Orbtrace.

Choose where Doris runs

Your setupHow to run DorisOperator?
Evaluation / demo / single hostUse Docker Compose instead of Helm. The Compose bundle includes a Doris FE + BE — one command, nothing to set up separately.No
Doris on a VM or a managed serviceRun Doris off-cluster, reachable from the cluster, and set doris.host to its FE.No
Doris inside the cluster (Kubernetes)Run the doris-operator and apply a DorisCluster (below). The chart connects to the operator-managed FE Service.Yes

Why the in-cluster path needs an operator

Doris is a clustered database — its frontend (FE) and backend (BE) nodes find each other by network address and have to stay registered as one group. On Kubernetes a pod gets a new IP every time it restarts, so the nodes would lose track of each other. Doris also needs certain steps done in a careful order: joining or removing a node, and upgrading the cluster one node at a time without downtime. Kubernetes' built-in pieces (a StatefulSet running the stock Doris image) don't know how to do any of that. The doris-operator is a small controller you install once that handles it for you — it joins new FE/BE nodes, keeps the membership correct when pods restart, and sequences safe rolling upgrades. On a VM or managed Doris the addresses never change, so there is nothing to coordinate and no operator is needed.


Requirements

For any in-cluster Doris (the operator path):

  • A Kubernetes cluster and Helm 3.8+.

  • A fast StorageClass — SSD/NVMe-backed. Doris BE compaction is IOPS-bound; spinning disks will not keep up.

  • A few Linux kernel settings on every node a Doris BE runs on. These live on the node (the host/VM), not inside the container — a normal pod can't change them, which is why you set them on the node up front. Elasticsearch and OpenSearch need the very same ones:

    • vm.max_map_count >= 2000000 — the maximum number of memory-mapped regions a process may hold. Doris BE memory-maps many data files; the Linux default (65530) is far too low and the BE refuses to start.
    • fs.inotify.max_user_instances = 8192 and fs.inotify.max_user_watches = 1048576 — how many files the node may watch for changes at once. Doris opens a lot of files; the defaults run out.
    • swap off — the BE runs swapoff -a at startup and expects real RAM, not swap. Size the node's RAM to the workload.

    How to apply them depends on where the nodes are — concrete recipes (VM sysctl, a plain-Kubernetes DaemonSet, or the OpenShift Node Tuning Operator) are in Applying the node settings just below.

  • Cluster-admin rights — once — to install the operator. The doris-operator is cluster-scoped: it adds new resource types (CRDs) and cluster-wide permissions (RBAC), which only a cluster-admin may create. So an admin installs it one time for the whole cluster. After that, regular teams create a DorisCluster in their own namespace with ordinary (namespaced) permissions — no admin rights needed again.

For off-cluster Doris (VM/managed) you only need the node kernel settings above on the Doris hosts; there is no operator and no cluster-admin step.

Applying the node settings

Pick the recipe that matches where Doris BE runs.

On a VM / bare-metal host

Run on every Doris BE host, as root — a sysctl drop-in that also survives reboots:

/etc/sysctl.d/99-doris.conf
vm.max_map_count = 2000000
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 1048576

Applysave as /etc/sysctl.d/99-doris.conf, then runsudo sysctl --system && sudo swapoff -a

Also remove or comment the swap line in /etc/fstab so swap stays off after a reboot.

On plain Kubernetes

A small privileged DaemonSet sets the sysctls on each node (scope it with a nodeSelector if you label a storage pool for Doris BE):

doris-node-sysctl.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: doris-node-sysctl
  namespace: kube-system
spec:
  selector:
    matchLabels: { app: doris-node-sysctl }
  template:
    metadata:
      labels: { app: doris-node-sysctl }
    spec:
      initContainers:
        - name: sysctl
          image: busybox:1.36
          securityContext:
            privileged: true # needed only to write node sysctls; the BE itself stays unprivileged
          command:
            - sh
            - -c
            - |
              sysctl -w vm.max_map_count=2000000
              sysctl -w fs.inotify.max_user_instances=8192
              sysctl -w fs.inotify.max_user_watches=1048576
      containers:
        - name: pause
          image: registry.k8s.io/pause:3.9

Applysave as doris-node-sysctl.yaml, then runkubectl apply -f doris-node-sysctl.yaml

(Kubernetes nodes already run with swap off, so there is nothing extra to do there.)

On OpenShift

Don't use a privileged DaemonSet; apply the sysctls cluster-wide with the Node Tuning Operator (a Tuned custom resource) so the BE pods stay unprivileged under restricted-v2.

First check the operator is on your cluster — the Tuned resource type only exists when it is (real OpenShift ships it by default; OpenShift Local/CRC and some minimal OKD builds don't). If this returns NotFound, skip the YAML and use the fallback in the tip below:

oc get crd tuneds.tuned.openshift.io
doris-node-tuning.yaml
apiVersion: tuned.openshift.io/v1
kind: Tuned
metadata:
  name: doris-node-sysctls
  namespace: openshift-cluster-node-tuning-operator
spec:
  profile:
    - name: doris-node-sysctls
      data: |
        [main]
        summary=Kernel settings for Apache Doris BE
        [sysctl]
        vm.max_map_count=2000000
        fs.inotify.max_user_instances=8192
        fs.inotify.max_user_watches=1048576
  recommend:
    - match:
        - label: node-role.kubernetes.io/worker
      priority: 20
      profile: doris-node-sysctls

Applysave as doris-node-tuning.yaml, then runoc apply -f doris-node-tuning.yaml

No operator on this cluster? Set the sysctls on the nodes directly

Without the operator the cluster does not know the Tuned resource type, so the oc apply above fails with no matches for kind "Tuned" in version "tuned.openshift.io/v1" / ensure CRDs are installed first. That error means exactly this case — write the same sysctls directly on each node (machine, not pod) Doris BE will run on instead. They hold only until the node reboots, and nodes added later need the same command. Fine for an evaluation:

# List the candidate nodes (the NAME column; OpenShift Local/CRC has one),
# then apply on each — or on all workers in one shot with the loop:
oc get nodes -l node-role.kubernetes.io/worker
 
for n in $(oc get nodes -l node-role.kubernetes.io/worker -o jsonpath='{.items[*].metadata.name}'); do
  oc debug node/$n -- chroot /host sysctl -w \
    vm.max_map_count=2000000 \
    fs.inotify.max_user_instances=8192 \
    fs.inotify.max_user_watches=1048576
done
 
# Verify: oc debug node/<node-name> -- chroot /host sysctl vm.max_map_count

Scope the match to the nodes Doris BE lands on if you label a storage pool for it. Everything else OpenShift-specific (the chart overlay, SCC, Route) is on the OpenShift page.


Replication factor (RF) — match it to your BE count

The replication factor (RF) is how many copies of each row Doris keeps — one copy per backend (BE) node. It's what protects your data when a BE dies: with RF 2, every row lives on two BEs, so losing one is survivable. Orbtrace creates its Doris tables at replication_num = doris.profile.replicationNum (2 on the medium / large profiles — two copies).

The rule follows from that: RF = N needs at least N live BE nodes — you can't keep 2 copies of a row with only 1 node to put them on. So if you have fewer BEs than the RF, the very first CREATE TABLE fails (Doris returns replication num … available backend num is 1), Orbtrace stops on purpose during startup (a safety check in the migration runner), and its pod never turns Ready — it hangs un-Ready / crashloops. This is one of the most common "installed it but it won't come up" causes.

Two sides must agree — the number of BE nodes (Doris side) and the RF the app requests (Orbtrace chart values):

SideValueWhat it is
DorisBE node count — beSpec.replicas (operator) or the number of BE hosts (VM)the ceiling on RF
Orbtracedoris.profile.replicationNumthe RF the app creates tables at — must be ≤ your BE count
Orbtracedoris.allowSingleReplicaset true only to accept RF = 1 on a single BE

So the app-side parameter that changes with your Doris RF is doris.profile.replicationNum (plus doris.allowSingleReplica for a single BE). It is applied when the tables are created (first boot) — raising it later only affects new tables; existing tables keep their RF until you change it on the Doris side (ALTER TABLE … SET ("replication_num" = "N")). Decide your RF before first boot.

  • Production: run ≥ 2 BE with replicationNum ≥ 2 (3 on ≥ 3 BE for hyperscale). The reference DorisCluster below uses beSpec.replicas: 3, which satisfies the default RF = 2.
  • Single-BE (single-node / PoC): a single-BE Doris cannot satisfy RF = 2. To run against it, set both values in your my-values.yaml — accept the risk and actually create RF = 1 tables:
    my-values.yaml
    doris:
      allowSingleReplica: true # accept the data-loss risk
      profile:
        replicationNum: 1 # create tables at RF=1
    RF = 1 means a single BE loss is permanent, unrecoverable data loss — never use it in production.

Why both keys, not just `replicationNum: 1`?

They do different jobs — one is the value, the other is a consent gate:

  • doris.profile.replicationNum is the RF the app writes into CREATE TABLE. Leave it at the default 2 and Doris itself rejects the create on a single BE (replication num should be less than the number of available backends).
  • doris.allowSingleReplica is a durability guard in the app (DorisMigrationRunner): it refuses to run below RF 2 unless you explicitly opt in — a fail-fast, exactly like doris.bundledAck, so nobody silently ships fragile RF=1 tables.
replicationNumallowSingleReplicaResult
2 (default)false✅ normal (needs ≥ 2 BE)
1falseapp aborts at boot — "below the durability floor … allow-single-replica is false"
2true❌ RF=2 CREATE TABLE still fails on one BE
1true✅ runs, with a loud data-loss warning

Only replicationNum: 1 → the guard stops boot. Only allowSingleReplica: true → the RF=2 create fails. Both together = it runs.

Option A — Doris on a VM or managed service (no operator)

Stand up a Doris FE + BE cluster on VMs (or use a managed Doris) on the same network as your cluster, with the node kernel settings applied on every BE host. Then point the chart at its FE — these keys go in your my-values.yaml (see Install with Helm):

my-values.yaml
doris:
  mode: external # default — the chart never runs Doris
  host: <your-doris-fe-host> # FQDN of your Doris FE, reachable from the cluster
  queryPort: 9030 # default Doris MySQL query port
  httpPort: 8030 # default Doris HTTP (Stream Load) port
  password: <your-doris-root-password> # empty ("") for a fresh Doris with no password

Add these to your my-values.yaml — the small overrides file you create in Install with Helm (step 3); step 4 there runs the install.

Sizing (FE/BE counts, CPU/RAM/disk) is in Capacity planning. That's all — skip the operator section below and continue at the Bring Orbtrace up section.


Option B — Doris inside the cluster with the operator

What you install

  • The doris-operator (SelectDB) — the controller that manages Doris on Kubernetes. Installed once per cluster.
  • A DorisCluster resource — your Doris instance (FE + BE), which the operator reconciles into running pods using the stock apache/doris FE/BE images.

Step 1 — Install the operator (one-time, cluster-admin)

helm repo add doris-repo https://charts.selectdb.com
helm repo update
helm install doris-operator doris-repo/doris-operator \
  --namespace doris-operator --create-namespace

On OpenShift: one-time grants in the Doris namespace

Two OpenShift security defaults block the operator's pods. The operator injects a privileged default-init container into its BE pods (it raises vm.max_map_count) — the default namespace Pod Security Admission (restricted/baseline) rejects any privileged container, so the BE pod is never created and the cluster stays initializing. And the stock apache/doris images run as root: under restricted-v2's arbitrary UID the FE crashloops with start_fe.sh: Permission denied — the anyuid SCC lets the pods keep the image's own user. Create the doris namespace (Step 2 deploys into it), relax its PSA, and grant both SCCs — the block is idempotent, safe to re-run:

oc create namespace doris --dry-run=client -o yaml | oc apply -f -
oc label namespace doris pod-security.kubernetes.io/enforce=privileged --overwrite
oc adm policy add-scc-to-user privileged -z default -n doris
oc adm policy add-scc-to-user anyuid -z default -n doris

(Running the label/policy commands without the namespace fails with namespaces "doris" not found — the create line above is why it's part of the block. Step 2's kubectl create namespace doris will then say AlreadyExists; that's fine.)

Skip it and the operator log reads would violate PodSecurity "…": privileged (container "default-init" must not set securityContext.privileged=true), the DorisCluster never leaves initializing, and the Orbtrace app crashloops on the missing BE. vm.max_map_count must still be >= 2000000 — the privileged init sets it, or apply it node-wide via the Node Tuning Operator (Requirements); either way these two grants are required. To avoid the privileged init and both grants entirely, run Doris off-cluster (Option A) — the recommended OpenShift path. Step-by-step on the OpenShift page.

Step 2 — Apply the DorisCluster

This DorisCluster is testbed-verified (a clean 60-minute sustained write on a 3-FE / 3-BE topology). The BE ConfigMap carries the compaction tuning that keeps writes from being rejected under sustained load — apply it together with the cluster. Set storageClassName to your fast SSD/NVMe class and scale BE replicas/memory per your Capacity planning.

doriscluster-orbtrace.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orbtrace-doris-be-conf
  namespace: doris
data:
  be.conf: |
    # Keep compaction ahead of sustained single-tablet load (avoids [E-235]).
    max_tablet_version_num = 100000
    time_series_max_tablet_version_num = 100000
    compaction_task_num_per_disk = 8
    cumulative_compaction_num_threads_per_disk = 2
    base_compaction_num_threads_per_disk = 1
    cumulative_compaction_max_deltas = 200
    time_series_compaction_goal_size_mbytes = 1024
    time_series_compaction_file_count_threshold = 2000
    streaming_load_max_mb = 256
    max_running_txn_num_per_db = 2000
    mem_limit = 80%
    inverted_index_storage_format = V2
    enable_metric_calculator = true
---
apiVersion: doris.selectdb.com/v1
kind: DorisCluster
metadata:
  name: orbtrace-doris
  namespace: doris
spec:
  feSpec:
    # 3-node Raft quorum. The master coordinates every load txn — keep memory
    # >= 12Gi (it OOMs at 6Gi under sustained load).
    replicas: 3
    electionNumber: 3
    image: apache/doris:fe-4.1.1
    requests: { cpu: "4", memory: 8Gi }
    limits:   { cpu: "8", memory: 12Gi }
    persistentVolumes:
      - name: fe-meta
        mountPath: /opt/apache-doris/fe/doris-meta
        persistentVolumeClaimSpec:
          accessModes: ["ReadWriteOnce"]
          # storageClassName: <your-fast-ssd-class>
          resources: { requests: { storage: 100Gi } }
  beSpec:
    # Production minimum 3; scale out per the capacity plan (32–64Gi at 100 TB/day).
    replicas: 3
    image: apache/doris:be-4.1.1
    requests: { cpu: "8", memory: 16Gi }
    limits:   { cpu: "16", memory: 32Gi }
    configMapInfo:
      configMapName: orbtrace-doris-be-conf
      resolveKey: be.conf
    persistentVolumes:
      - name: be-storage
        mountPath: /opt/apache-doris/be/storage
        persistentVolumeClaimSpec:
          accessModes: ["ReadWriteOnce"]
          # storageClassName: <your-fast-ssd-class>   # BE compaction is IOPS-bound
          resources: { requests: { storage: 1Ti } }
kubectl create namespace doris
kubectl apply -f doriscluster-orbtrace.yaml
kubectl -n doris get doriscluster -w   # wait for the FE/BE phase to go Ready

Evaluating on a small cluster? The reference CR won't schedule

The reference DorisCluster above is production-sized: 3 FE × 4 CPU + 3 BE × 8 CPU ≈ 36 CPU of requests. On a small cluster (OpenShift Local, single node, kind) its pods sit Pending with Insufficient cpu forever. For an evaluation only, replace the DorisCluster document in the file (keep the ConfigMap) with this 1 FE / 1 BE variant:

apiVersion: doris.selectdb.com/v1
kind: DorisCluster
metadata:
  name: orbtrace-doris
  namespace: doris
spec:
  feSpec:
    replicas: 1
    electionNumber: 1
    image: apache/doris:fe-4.1.1
    requests: { cpu: 500m, memory: 2Gi }
    limits: { cpu: "2", memory: 4Gi }
    persistentVolumes:
      - name: fe-meta
        mountPath: /opt/apache-doris/fe/doris-meta
        persistentVolumeClaimSpec:
          accessModes: ["ReadWriteOnce"]
          resources: { requests: { storage: 5Gi } }
  beSpec:
    replicas: 1
    image: apache/doris:be-4.1.1
    requests: { cpu: "1", memory: 4Gi }
    limits: { cpu: "4", memory: 8Gi }
    configMapInfo:
      configMapName: orbtrace-doris-be-conf
      resolveKey: be.conf
    persistentVolumes:
      - name: be-storage
        mountPath: /opt/apache-doris/be/storage
        persistentVolumeClaimSpec:
          accessModes: ["ReadWriteOnce"]
          resources: { requests: { storage: 20Gi } }

A single BE can only hold one copy of each row, so the chart must create tables at RF = 1 — set both doris.allowSingleReplica: true and doris.profile.replicationNum: 1 in your my-values.yaml (the Replication factor section explains why both). Never use this variant in production.

Don't override the FE config

Do not add an fe.conf via feSpec.configMapInfo — the operator's configMapInfo replaces the operator-injected fe.conf (priority networks, edit-log port) and the FE crashloops. The BE be.conf above is the safe path. For the one enterprise FE knob, set it at runtime once the cluster is up:

kubectl -n doris exec orbtrace-doris-fe-0 -- mysql -h127.0.0.1 -P9030 -uroot \
  -e "ADMIN SET FRONTEND CONFIG ('max_dynamic_partition_num'='2000')"

Verify before you install Orbtrace

Confirm the cluster is healthy first — it saves you debugging an un-Ready Orbtrace pod later. For the operator path:

kubectl -n doris exec -it orbtrace-doris-fe-0 -- mysql -uroot -P9030 -h127.0.0.1 \
  -e "SHOW FRONTENDS\G SHOW BACKENDS\G"

Every FE should be Alive: true and at quorum; every BE should be Alive: true. (For a VM/managed Doris, run the same query against its FE.)

Bring Orbtrace up

Point the chart at the Doris you just verified. The chart defaults already assume an operator Doris named orbtrace-doris in namespace doris:

my-values.yaml
doris:
  mode: external
  host: orbtrace-doris-fe-service.doris.svc.cluster.local # or your VM/managed FE
  queryPort: 9030
  httpPort: 8030
  password: "" # match the Doris root password (empty = fresh Doris)

Add these to your my-values.yaml — the small overrides file you create in Install with Helm (step 3); step 4 there runs the install.

These doris.* keys go into your my-values.yaml (the file you edit in Install with Helm, step 3). Two fields carry the connection:

  • doris.host — the FE address. For the operator, the default already matches the reference DorisCluster (orbtrace-doris in namespace doris), so if you used those names you don't need to change it. For a VM/managed Doris, set your FE's FQDN. (queryPort 9030 = MySQL wire the app reads over; httpPort 8030 = Stream Load — the standard Doris ports, override only if yours differ.)
  • doris.password — the Doris root password. Leave empty for a fresh operator Doris; set it (or use --set-string doris.password=…) if your DorisCluster/VM has a root password, or the app hangs un-Ready on an auth failure.

Then install Orbtrace per Install with Helm. On first boot the app's migration runner creates the otel_* tables and the Orbtrace extension columns — watch for it to finish:

kubectl -n orbtrace logs -f deploy/orbtrace-app -c orbtrace
# ready when you see: [doris-migration] complete

The Orbtrace pod turns Ready once the schema is applied and it can reach Doris.

Troubleshooting

  • FE not reaching quorum — an even FE count or cross-node scheduling. The reference DorisCluster uses 3 FE with anti-affinity.
  • BE Alive: false — almost always vm.max_map_count not set on the BE node (Requirements), or the BE can't reach the FE edit-log port.
  • Orbtrace stays un-Ready / reports STORAGE_UNAVAILABLEdoris.host or doris.password mismatch, or Doris wasn't Ready when you installed. Re-check against the verify step above.