feat(api): add an optional stateless odbingest node group - #14
Open
tdakkota wants to merge 2 commits into
Open
Conversation
Symmetric oteldb nodes weld ingest capacity to storage capacity: absorbing a write spike means adding a node, which then takes ownership of a share of the shards. spec.ingest deploys a pool of stateless odbingest write nodes as a Deployment, fronted by a <name>-ingest Service. It is opt-in: an existing OtelDBCluster with just replicas: N is untouched and keeps meaning symmetric all-in-one nodes. odbingest is not handed a storage node list. It watches etcd for membership and resolves shard owners from the ring locally, so scaling spec.replicas needs no re-render and no restart of the pool. It does need the ring's shape, and a mismatch there fails silently — a different owner set means writes land where no read looks — so both roles render rf/shards_per_tenant/root from one spec.cluster via ringConfig, and spec.ingest.extraConfig reserves the whole cluster block. The storage StatefulSet's selector is left byte-identical (it is immutable after creation); the roles are told apart by app.kubernetes.io/name alone. Refs #11.
This was referenced Aug 17, 2026
tdakkota
marked this pull request as ready for review
August 17, 2026 21:30
This was referenced Aug 17, 2026
tdakkota
added a commit
to oteldb/e2e
that referenced
this pull request
Aug 17, 2026
Deploy an odbingest pool (spec.ingest) and an odbselect pool (spec.query) alongside the storage nodes, then write through the ingest pool's Service and read back through the query pool's for all three signals. That crossing is the point of the split and the one thing a symmetric suite cannot check: with one process doing both, a write-then-read proves nothing about the boundary. Also asserts the pools stay out of the storage ring and that losing a pod from each costs only capacity. Sibling repos can now be pinned to a git ref (OTELDB_REF / OPERATOR_REF), cloned into a cache instead of the local tree, so this runs against the unmerged branches now and main later. odbingest and odbselect go through the same fast build path as oteldb. Three fixes the pools exposed, all pre-existing: the fast image installed binaries only at /<bin>, but the pools' Deployments exec the release path /usr/local/bin/<bin>; WaitRollout raced the operator creating the workload, since `kubectl rollout status` fails NotFound rather than waiting; and `make deploy` leaves a reused cluster running the previous manager, because the image tag never changes. Gated behind E2E_ROLE_POOLS until oteldb/operator#14, #15 and oteldb/oteldb#1266 land.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11 (partially — the query half is deferred, see below).
Adds a role dimension to
OtelDBClusterso ingest capacity scales independently of storage, bydeploying a pool of stateless
odbingestwrite nodes.Design
spec.ingestis a new optional stanza (*IngestSpec) alongside the existingspec.replicas,rather than a restructure into three symmetric stanzas à la
VMCluster.VictoriaMetrics needs three symmetric stanzas because it has no all-in-one binary — there is no
"just
replicas" mode for it to stay compatible with. oteldb has one, it is the default, and it iswhat every existing
OtelDBClustermeans. Making the stanza a pointer keeps "absent" and "presentat defaults" distinguishable: omit it and nothing new is deployed,
ingest: {}opts in.spec.storagewas also already taken, and means the durable tier — aspec.storage.replicaCountnext to
spec.storage.backendwould read as a contradiction, and renaming it is a breakingv1alpha1 change bought for nothing.
oteldbspec.replicas(unchanged)odbingestspec.ingest.replicas(default 2)Deployment for ingest: the pods hold nothing, are not ring members, and need no stable identity, no
PVC, and no ordered rollout.
The divergence from VM that carries the design
vminsertmust be handed an explicit-storageNodeFQDN list, so the VM chart re-templates it fromthe storage replica count and rolls both stateless tiers on every storage scale. oteldb does not
work that way — verified against
cmd/odbingest/app.goon oteldbmain, whererouter.Opentakes{Etcd, Root, RF, ShardsPerTenant, DialTimeout}and no node list at all. etcd holds membership, thering is rendezvous-hashed, and owners are computed locally from the watched member list.
Scaling
spec.replicastherefore requires no change to the ingest pool — no config re-render,no rollout, no restart. The two replica counts are genuinely independent.
Why this belongs in the CRD
What the pool does need is the ring's shape, and
ClusterConfig's own doc comment on the oteldbside is blunt about getting it wrong:
A silent, non-erroring, data-losing misconfiguration split across two workloads is exactly what an
operator should make unrepresentable. Both roles now render
etcd/rf/shards_per_tenant/rootfrom oneringConfig(cr, endpoints)helper,spec.ingest.extraConfigreserves the wholeclusterblock, andTestIngestRingMatchesStorageRingasserts the two renderers agree key for key.Backward compatibility
spec.replicaskeeps its meaning and its default. An existing CR with justreplicas: 3producesbyte-identical output and keeps meaning symmetric all-in-one nodes. Symmetric stays the default.
changing it would wedge every existing cluster on upgrade. The two roles are told apart by
app.kubernetes.io/namealone (oteldbvsodbingest);TestIngestAndStorageSelectorsAreDisjointpins that neither selector matches the other role's pods.
<name>client Service is not touched — same selector, same ports, stillfronting the storage nodes, which still serve every API. A Service cannot select two pod sets, and
silently repointing an in-use ingest endpoint at a new workload during an operator upgrade is a
data-path change nobody asked for. Senders opt in by pointing at
<name>-ingest.spec.ingestagain prunes the Deployment/Service/ConfigMap; owner refs alone would onlycollect them when the whole cluster goes away.
Services and ports
<name>-ingestpublishes4317OTLP/gRPC,4318OTLP/HTTP,19291remote write (when metrics areenabled) and
8090self-metrics.One wrinkle:
odbingestserves OTLP/HTTP, remote write and its health endpoints on a singlelistener —
otlp.Register(mux)andmux.Handle(rw.Path, handler)share one mux bound toprometheus_remote_write.bind— with only OTLP/gRPC on its own port. There is no:4318and noseparate
:13133. So the Service publishes4318, the port stock OTLP/HTTP exporters target, andremaps it onto the
ingest-httpcontainer port, and the probes hit/healthzand/readyzthere./readyzalready answers 503 until the ring has a member, so it is wired straight to the readinessprobe and a starting pod stays out of the load balancer until the cluster is reachable.
HPA: deliberately not modelled
Both stateless roles are plain Deployments with stable names and labels, so a user's own
HPAorKEDA
ScaledObjectcan targetapps/v1 Deployment <name>-ingesttoday with no operator support.Building it into the CRD first requires settling the operator-versus-HPA fight over
.spec.replicas: the operator must learn to omit replicas once an HPA owns the scale subresource,or the two flap against each other every reconcile. Worth doing, cleanly separable, not this PR.
Not in this PR
spec.queryis deferredodbselect's config exists only on the unmerged oteldb/oteldb#1266 branch, and a shared-config-blockextraction is in flight upstream that may still move it. A CRD field is effectively permanent once
shipped, so adding
spec.querynow means either guessing a schema that drifts or shipping a fieldthe operator cannot honour. The role machinery here is generic, so adding the second stanza once
#1266 lands is a small diff.
cmd/odbingest's config is on oteldbmainand stable, which is what this is grounded in.Blocking:
odbingestis not in any released imageFound while grounding this —
odbingestis not built by oteldb's.goreleaser.yamland notcopied by
release.Dockerfile, which installs onlyoteldb,odbbackup,odbrestoreandodbmigrate. No publishedghcr.io/oteldb/oteldbtag contains the binary, and nothing in oteldb oroteldb/chartsreferences it.The operator side is correct regardless — the container runs
/usr/local/bin/odbingestout of thecluster image and
spec.ingest.imageoverrides it — but the pool cannot actually start until oteldbships the binary. Called out in the README, the
IngestSpecgodoc and the sample CR so it is notdiscovered at rollout time. The upstream fix is a
builds:entry plus oneCOPYline.Verification
make lint(0 issues),make test(envtest, controller package at 84.4%) andgo build ./...allpass. Generated manifests and deepcopy are in a separate commit.