Skip to content

Commit 8351f0a

Browse files
devantlerclaude
andauthored
fix(coroot): run ClickHouse + keeper (and HA-capable components) with redundancy (#2124)
Coroot's own risk view flagged every stack component as "single instance / all instances on one node", so one node loss took down (or risked data loss in) the telemetry store. Make the components the operator can run redundantly actually redundant, and document why the rest stay single-instance. ClickHouse: replicas 1 -> 2 (mirrored within the single shard via ReplicatedMergeTree, survives losing either replica's node). Keeper: replicas 1 -> 3 (Raft quorum must be ODD; 2 is invalid, 3 tolerates one keeper down). Both get hard pod anti-affinity on kubernetes.io/hostname so the redundancy is real (operator exposes affinity, not topologySpreadConstraints; all nodes are in one Hetzner zone so hostname is the spread key). Left single-instance, with rationale in-file: bundled Prometheus and cluster-agent (the CRD exposes no replicas field for either), and the Coroot server StatefulSet (spec.replicas exists but the CRD requires spec.postgres when >1; the default SQLite store can't back multiple replicas — a CNPG-backed follow-up). All fields verified against the live coroot.com/v1 CRD (kubectl explain + server-side dry-run); no invented keys. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 29e5123 commit 8351f0a

1 file changed

Lines changed: 63 additions & 6 deletions

File tree

k8s/bases/infrastructure/coroot/coroot.yaml

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,58 @@ spec:
4848
# Bundled Prometheus = the metrics TSDB. Kept as a real Prometheus (we do
4949
# NOT set storeMetricsInClickhouse) precisely so OpenCost can still query it
5050
# at http://coroot-prometheus.observability.svc.cluster.local:9090.
51+
# SINGLE INSTANCE BY DESIGN: the operator's `spec.prometheus` exposes no
52+
# `replicas` field (verified against coroot.com/v1), so the bundled Prometheus
53+
# has no HA mode here. It recovers by rescheduling onto a healthy node with
54+
# its (durable, hcloud-backed) PVC; the gap is the in-flight scrape window
55+
# during reschedule. Running redundant metrics ingestion would mean an
56+
# external/federated Prometheus, which is a separate architectural decision.
5157
prometheus:
5258
retention: 7d
5359
storage:
5460
size: 10Gi
5561

56-
# ClickHouse stores logs, traces and continuous profiles. A single
57-
# shard/replica (+ one keeper for replication metadata) is plenty for a
58-
# homelab and keeps the footprint small on a memory-constrained cluster.
62+
# ClickHouse stores logs, traces and continuous profiles. Run it with
63+
# redundancy so a single node loss doesn't take the telemetry store down or
64+
# lose data (Coroot's own risk view flagged every component as "single
65+
# instance / all instances on one node"):
66+
# - shards: 1 — one shard is enough data volume for this cluster; we want
67+
# replication, not horizontal partitioning, so the shard count stays 1.
68+
# - replicas: 2 — two replicas *within* the shard, so the data is mirrored
69+
# and survives losing the node hosting either replica (ReplicatedMergeTree
70+
# via Keeper). This is the replica/shard field the operator exposes
71+
# (`spec.clickhouse.replicas`).
72+
# - keeper.replicas: 3 — ClickHouse Keeper is a Raft cluster; it needs an
73+
# ODD quorum to tolerate a failure. 1 keeper is a SPOF and 2 is INVALID
74+
# (no majority on a split), so the minimum fault-tolerant size is 3
75+
# (tolerates 1 keeper down). Field: `spec.clickhouse.keeper.replicas`.
76+
# Hard pod anti-affinity (one replica per node) makes the redundancy real —
77+
# without it the operator can schedule both ClickHouse replicas, or a keeper
78+
# majority, onto one node and a single node loss still takes quorum/data with
79+
# it. All nodes are in one Hetzner zone (fsn1), so the spread key is the node
80+
# hostname. The operator exposes only `affinity` (no topologySpreadConstraints
81+
# field), so anti-affinity is the available placement knob.
5982
clickhouse:
6083
shards: 1
61-
replicas: 1
84+
replicas: 2
85+
affinity:
86+
podAntiAffinity:
87+
requiredDuringSchedulingIgnoredDuringExecution:
88+
- labelSelector:
89+
matchLabels:
90+
app.kubernetes.io/part-of: coroot
91+
app.kubernetes.io/component: clickhouse
92+
topologyKey: kubernetes.io/hostname
6293
keeper:
63-
replicas: 1
94+
replicas: 3
95+
affinity:
96+
podAntiAffinity:
97+
requiredDuringSchedulingIgnoredDuringExecution:
98+
- labelSelector:
99+
matchLabels:
100+
app.kubernetes.io/part-of: coroot
101+
app.kubernetes.io/component: clickhouse-keeper
102+
topologyKey: kubernetes.io/hostname
64103
storage:
65104
size: 10Gi
66105
logsTTL: 3d
@@ -78,4 +117,22 @@ spec:
78117

79118
# cluster-agent is left at its default (enabled) — it provides the
80119
# kube-state-metrics-equivalent cluster inventory, replacing the standalone
81-
# kube-state-metrics from the prometheus stack.
120+
# kube-state-metrics from the prometheus stack. SINGLE INSTANCE BY DESIGN:
121+
# `spec.clusterAgent` exposes no `replicas` field (verified against
122+
# coroot.com/v1) — it is a cluster-wide singleton collector (like
123+
# kube-state-metrics, which also runs one replica), so a second copy would
124+
# only duplicate the same scrape. It recovers by rescheduling onto a healthy
125+
# node; the gap is the reschedule window, during which the node-agents (one
126+
# per node, already tolerations-spread above) keep collecting per-node
127+
# telemetry.
128+
#
129+
# The Coroot server StatefulSet (coroot-coroot) is also left SINGLE INSTANCE.
130+
# `spec.replicas` does exist, but the CRD documents it as requiring
131+
# `spec.postgres` ("Store configuration in a Postgres DB instead of SQLite
132+
# (required if replicas > 1)") — the default SQLite store cannot back more
133+
# than one replica. CloudNativePG IS available in-cluster, so multi-replica
134+
# is achievable, but it is a separate change (provision a CNPG Cluster +
135+
# credentials, point `spec.postgres` at it, migrate state) with its own
136+
# storage/backup story — out of scope for this redundancy pass and tracked as
137+
# a follow-up. Until then the server recovers by rescheduling onto a healthy
138+
# node with its durable PVC.

0 commit comments

Comments
 (0)