[Design] Valkey and ValkeySentinel CRD Proposal #387
Replies: 13 comments 1 reply
|
|
|
Instead of having type ValkeySpec struct {
// Failover declares how primary failover is performed.
// +optional
Failover *FailoverSpec `json:"failover,omitempty"`
// ...
}
type FailoverSpec struct {
// Mode selects the failover engine.
// Values may be added in future versions; clients must handle unknown values.
// +kubebuilder:validation:Enum=None;Sentinel
// +kubebuilder:default=None
Mode FailoverMode `json:"mode,omitempty"`
// Sentinel configures Sentinel-mode failover. Only valid when mode is Sentinel.
// +optional
Sentinel *SentinelFailoverConfig `json:"sentinel,omitempty"`
}
When ValkeySentinels find Valkey's to monitor, it should filter on Keep FailverSpec self-contained (not referencing any other sibling or parent objects), then it can be reused for |
|
I'd suggest for the first iteration, that we enforce at most one Sentinel monitoring a Valkey instance, instead of allowing multiple and warning about it. This would need to be done on the controller side. TODO: consider migration cases between Sentinels. |
|
Quorum default belongs to the Sentinel and defaults to (sentinel replicas / 2 + 1). Sentinel should compute this when it registers a Valkey to monitor. We can leave in Valkey.spec.failover.sentinel.quorum as an optional explicit pin without a kubebuilder:default. Such that an absent quorum will let the Sentinel decide. But I am fine with leaving out quorum pinning for now. |
|
Agree on making MonitorName default to metadata.name and immutable (at least for this iteration). You'd have to follow some function CEL rule to get it to work: // +kubebuilder:validation:XValidation:rule="has(self.monitorName) == has(oldSelf.monitorName)",message="monitorName cannot be added or removed after the sentinel block is created"
// +kubebuilder:validation:XValidation:rule="!has(self.monitorName) || self.monitorName == oldSelf.monitorName",message="monitorName is immutable"
type SentinelFailoverConfig struct {
// +kubebuilder:validation:MinLength=1
// +optional
MonitorName string `json:"monitorName,omitempty"`
...
}
// shared method for retrieving the monitorName
func (v *Valkey) MonitorName() string {
if v.Spec.Failover != nil && v.Spec.Failover.Sentinel != nil &&
v.Spec.Failover.Sentinel.MonitorName != "" {
return v.Spec.Failover.Sentinel.MonitorName
}
return v.Name
}You could technically rename by setting |
|
Some notes on validations:
|
|
Transitioning between spec.failover.mode.
|
|
I think the Services could be up for debate. I think including Per-pod Service is something desired for ValkeyCluster, but the design hasn't been worked out yet. That uses headless services in the meantime, so could we reuse that pattern here for stable DNS identities? |
|
I would copy rather than share I am thinking we might want to do the same copy for the TLS specs. |
|
ValkeySentinelSpec should allow an ExporterSpec so that we can scrape metrics |
|
I think we should default to having an init container on all ValkeyNodes created by Valkey (on top of the one for Sentinel) that does some pre-boot discoveries if required. Such as in Sentinel mode, it should hit the Sentinel headless server to discover the current primary in order to set Having the init container default to existing will mean that we can make changes to the script it uses without the need for Pods to be rolled (because the podTemplate would not have changed - so long as we pass the init script via a ConfigMap). I have been thinking of having an init container on the ValkeyCluster nodes before hand, I think that would give us a lot of flexibility for adding in some pre-boot logic on the Pods before handing it over to the Valkey server. |
|
Coming at this from having built Sentinel support in a parallel operator, three things that bit me there and are not yet in the thread. Rolling restarts do not come for free from the reuse list. The proactive path today is The Sentinel pods need their own ordered roll. Restarting them faster than they rejoin drops the quorum below majority, which removes the failover authority during exactly the window where a node is being replaced. Worth stating the wait condition explicitly rather than leaving it to a StatefulSet rollout. Sentinel caches the addresses it discovers, and pod IPs churn on every roll, reschedule and scale change. Without a deliberate answer, announce settings or a reset after topology changes, Sentinel can hold a stale replica list at the moment it needs to choose a failover target. One thing already right, worth not losing: PVCs are looked up by name through |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Valkey and ValkeySentinel Design Proposal
This document proposes support for Valkey replication mode with Sentinel in the valkey-operator, alongside the existing cluster mode (architecture.md).
It is written to converge with the discussion in issue #198 and the selector-linked draft in VALKEY_AND_SENTINEL.md and adopts their settled decisions: two CRDs (
Valkey+ValkeySentinel), Sentinel tuning asmap[string]stringpassthrough rather than typed fields,spec.replicascounting replicas in addition to the primary, and Sentinel pods managed as a StatefulSet for the MVP. Where it differs, it says so and why. Design also tries to align with future work on ValkeyCell CRD.Motivation
Today the operator supports cluster mode only (quickstart.md). Replication mode with Sentinel covers a distinct set of users:
SELECT/multiple DBs, Lua scripts touching unrelated keys).SENTINEL GET-MASTER-ADDR-BY-NAME), eg. Jedis/Lettuce/go-redis/ioredis sentinel modes, Spring Data Redis, Sidekiq, etc. Migrating these to cluster mode requires client changes, which might be a deal breaker for many organizations. Migrating to a Sentinel-backed deployment is straightforward.Goals
ValkeyNodefor the data plane without modifying it. Sentinel pods are a StatefulSet for the MVP, but share the pod-template builder withValkeyNoderather than duplicating it.replicas: 0) falls out of the same CRD as a degenerate case.Non-Goals
ValkeyCellthat creates N labelledValkeys).Detailed Design
Required CRDs
ValkeyValkeySentinelValkeysValkeyNodeValkeyNode: no changesValkeyClusterShared API types (
SchedulingSpec,PersistenceSpec,ExporterSpec,TLSConfig,UserAclSpec,PodDisruptionBudgetConfig,WorkloadType) are reused as is fromapi/v1alpha1.PodDisruptionBudgetConfigis the one imperfect fit as its only enabled mode is literally namedCluster. We will work with it for now and refactor and track under Future work.The two-CRD shape is settled (see Settled: CRD shape). The live question is which side declares the link between them; this document recommends a selector on the Sentinel side plus an opt-in requirement flag on the data side. See Linkage direction.
ValkeyConditions (following status-conditions.md):
Ready,Progressing,Degraded,ConfigurationWarning, plus:PrimaryElected: Exactly one node reportsrole:master.ReplicationHealthy: Every replica reportsmaster_link_status:up.Monitored: At least oneValkeySentinelselects this instance and has registered the monitor. It participates inReadywhenspec.sentinel.requiredis true.SentinelQuorumHealthy:SENTINEL CKQUORUM <monitorName>succeeds, meaning there are enough Sentinels to both detect failure and authorize failover.Validation:
spec.sentinel.monitorNameis immutable. Renaming a monitor meansSENTINEL REMOVEplus re-registration. That is a deliberate teardown, not an edit.spec.sentinel.requiredrequiresspec.replicas >= 1. There is nothing to fail over to otherwise.spec.persistenceimmutability and expand-only rules, plus thepersistencewithworkloadType == Deploymentexclusion, copied fromValkeyClusterSpec.workloadType == Deploymentis additionally rejected whenspec.replicas > 0. Replication needs the stable per-pod DNS that only a StatefulSet provides (see Why per-pod DNS).spec.sentinel.quorumcannot be CEL-validated against the selecting Sentinel's replica count, because that lives on a different object. It is checked at registration time instead. The operator raises aConfigurationWarningwhen the value exceeds the available Sentinel count, or when it falls at or below half of it.ValkeySentinelValkeyNode: no changesData pods use
ValkeyNodeexactly as it exists today. Two extensions were considered and both are deferred.A per-node config hint (
spec.replicaOf), deferred. Replication mode would like each pod'svalkey.confto differ. Node 0 carries noreplicaofand nodes 1..N point at the primary. The current wiring cannot express that. The parent renders one ConfigMap for the whole set (config.go:196) and stamps that same name onto every node (valkeycluster_controller.go:939). The node controller then skips creating its own (valkeynode_controller.go:650-653) and mounts the shared one. It is launched by a fixed command with no shell to substitute into (valkeynode_resources.go:170-172,420-422). Every pod in a set therefore mounts identical config.The accepted consequence is that replication is established by command and is runtime-only state.
CONFIG REWRITEcannot persist it either, since the config is a read-only ConfigMap mount. A pod that restarts, whether from a node drain, an upgrade, an eviction or an OOM kill, comes back as its own primary. Because it is not replicating, it never appears in the real primary'sINFO replication, which is exactly where Sentinel discovers replicas. Sentinel cannot see it, so Sentinel cannot fix it. The repair rule is the mitigation for now. The residual exposure is recorded under Risks and the durable fix under Future work.Sentinel as a
ValkeyNode, deferred.ValkeyNodeis defined around a Valkey server pod. Itsstatus.roleis primary or replica, it reports replication state, and it owns a PVC. The singleton-per-pod shape exists so each data pod can be scheduled independently. Sentinel pods are interchangeable, need no PVC, and want no per-pod scheduling divergence. Three identical pods is what a StatefulSet does best. AdoptingValkeyNodewould buy pod-template reuse and naturally quorum-gated rolls. Both are obtainable more cheaply, by sharing the template builder and usingupdateStrategy: OnDelete. Revisit whenValkeyNodegrows alternate-binary, alternate-port, and no-PVC support.Configuration surface
Sentinel's knobs split across three apply paths. A single flat map hides that distinction. Where each key belongs:
monitor, quorumSENTINEL MONITORValkey.spec.sentinel.quorumsupplies the quorum argumentdown-after-milliseconds,failover-timeout,parallel-syncs,notification-scriptSENTINEL SET <master>Valkey.spec.sentinel.configauth-user,auth-pass,sentinel-user,sentinel-passSENTINEL SET/sentinel.confresolve-hostnames,announce-hostnames,announce-ip,announce-portsentinel.confValkeySentinel.spec.config, with operator-owned keys reservedTwo maps rather than one, because per-master and process-global directives have genuinely different scopes and apply paths. A design with only a
SENTINEL SEToriented map leaves no way to set a process-global directive at all.Typed fields were considered and rejected. Sentinel's directive set is very stable. The core has been unchanged since Redis 2.8, with roughly one or two additions per major release and no removals (
deny-scripts-reconfigin 5.0,sentinel-userandsentinel-passin 5.0.1,auth-userin 6.0,resolve-hostnamesandannounce-hostnamesin 6.2,master-reboot-down-after-periodin 7.0). So churn is not the argument. The real arguments are thatValkeyCluster.spec.configalready set this precedent, that the operator has no business validating Valkey's ranges, and that a map which later grows a few typed fields is additive. Typed fields that turn out wrong are a v1alpha1 to v1beta1 break.Operator-owned key handling follows the existing
getBaseConfigprecedent for file-rendered config, where base directives are written last and win. A collision raises aConfigurationWarningso the override is visible rather than silent. ForSENTINEL SETconfig there is no file ordering, so reserved keys are skipped and warned about instead.Topology
Data node names follow the existing convention with the shard dimension dropped, so
<name>-<index>, with index 0 being the initial primary. As in cluster mode, that index is a bootstrap fact rather than live truth. The live role is always read fromINFO replicationor from Sentinel.SENTINEL MONITORusesspec.sentinel.monitorName, which defaults to theValkey'smetadata.name. Namespace-scoped name uniqueness makes collisions structurally impossible in the default case.Client entry points
Three, so both sentinel-aware and sentinel-unaware clients work:
valkey-<name>-primary: A ClusterIP Service whose selector includesvalkey.io/role: primary. The operator maintains avalkey.io/rolelabel on each pod from the observed role, so the endpoint follows failover without clients knowing about Sentinel. Convergence is bounded by reconcile latency after a failover (see Watching failover). This endpoint is therefore eventually correct, not instantly correct.valkey-<name>-replicas: A ClusterIP Service selectingvalkey.io/role: replica, for read scaling.valkey-sentinel-<name>: A headless Service over the Sentinel pods on 26379. Sentinel-aware clients use this and get instant, operator-independent failover awareness. This is the recommended path and must be documented as such. The role-labelled Services are a convenience with a documented lag.Why per-pod DNS?
Sentinel discovers replicas by parsing
INFO replicationon the primary and by gossiping over__sentinel__:hello. It persists the resolved addresses in its own rewritten config. On Kubernetes, pod IPs change on every restart, so an IP-based setup leaves Sentinel holding dead addresses.Resolution: use stable DNS names everywhere and let Sentinel resolve them.
replica-announce-ip <pod-fqdn>andreplica-announce-port 6379.sentinel announce-ip <pod-fqdn>/announce-port.sentinel resolve-hostnames yesandsentinel announce-hostnames yes, and monitors are registered by hostname rather than IP.Per-pod DNS does not exist in this repository today, and this design has to add it. Each
ValkeyNode's StatefulSet setsspec.serviceNameto its own resource name. Nothing ever creates a Service by that name. The only Service is the parent's set-wide headless one, and the operator reaches pods bystatus.podIP, using the headless FQDN solely as a TLSserverName. TheValkeycontroller therefore has to create one headless governing Service per node, matching the name the StatefulSet already points at. The resulting FQDN isvalkey-<name>-<i>-0.valkey-<name>-<i>.<ns>.svc.cluster.local. Note that the subdomain is the per-node Service, not the set-wide one.Stable identity also requires
workloadType: StatefulSet.Deploymentis rejected wheneverspec.replicas > 0. A selectingValkeySentinelalso refuses to monitor a Deployment-backed instance. It emits a warning rather than registering a monitor it cannot address durably. The gate is onreplicasrather than onspec.sentinel, because under the selector model an instance can be monitored without ever settingspec.sentinel. A standalonereplicas: 0cache on a Deployment stays legal, since there is no replication to preserve and nothing to fail over. Cluster mode tolerates Deployments becauseCLUSTER MEETand nodes.conf re-converge on IP changes through the operator. Sentinel has no equivalent operator-mediated repair path.Hostname resolution support must be pinned in docs. It comes from the Redis 6.2 lineage and is present in Valkey. The reconciler surfaces a
ConfigurationWarningif the image reports an older version.Sentinel pod specifics
CONFIG REWRITEon every monitor or failover change, and a ConfigMap mount is read-only. So the pod copiessentinel.conffrom the ConfigMap into/data/sentinel.confat startup, using an init container or an entrypoint copy, then runsvalkey-sentinel /data/sentinel.conf./datais an emptyDir by default, or a PVC whenspec.persistenceis set. This keepsreadOnlyRootFilesystem: trueintact, a posture already asserted bytest/e2e/valkeycluster_readonly_rootfs_test.go./datais safe but not free. A restarted Sentinel with an empty config re-learns monitors from re-registration and the rest from gossip. It does not count toward quorum during that window. Default to emptyDir and document the PVC option.SENTINEL SET <name> auth-userandauth-pass. When the Sentinels themselves require auth, it also needssentinel-userandsentinel-passfor inter-Sentinel traffic. This adds a_sentinelsystem user ininternal/controller/users.goalongside_operator,_exporter, and_replication. Its ACL rawstring comes from the Valkey ACL docs for Sentinel and replicas, the same source already cited for the replication user. The exact rawstring must be validated against the target Valkey version before merge. It needs at minimum+multi +exec +ping +info +role +publish +subscribe +replicaof +config|rewrite +client|setname +client|kill +script|killandallchannels. TheValkeycontroller creates the user and publishes its location instatus.sentinelCredentialsSecret. In practice that isinternal-<name>-system-passwords, key_sentinel, followinggetSystemPasswordSecretName. The ACL file itself lives ininternal-<name>-acland user-supplied passwords in<name>-users. A selectingValkeySentinelcontroller reads only the Secret and key named in status. Without this contract, a selector-linked design implicitly grants the Sentinel controller read access to every matched instance's ACL Secret, with no defined boundary.tls-port 26379,port 0, and the same cert Secret as the servers it monitors. Monitors are registered against the servers' TLS port. Mixed TLS and non-TLS across one Sentinel's selected set is rejected, because a single Sentinel process cannot speak both.CONFIG SET-managed. TheliveConfigAllowlistpath is for data nodes. Sentinel's live changes go throughSENTINEL SET.Reconcile flow
ValkeySentinel controller
Owns the monitoring plane and every monitor-lifecycle command.
publishNotReadyAddresses: trueso Sentinels can gossip before readiness. Upsert the PDB withmaxUnavailable: 1. That is enough for the default 3-pod quorum. See Future work for quorum-aware budgets.sentinel.confbase directives,spec.config, and the copy script. Hash it into the pod template annotation, reusing the existing config-hash mechanism.updateStrategy: OnDelete, so pod replacement order is the operator's decision rather than the StatefulSet controller's.SENTINEL CKQUORUMpasses for every monitor and no failover is in progress, meaningSENTINEL MASTERSreports afailover_stateof none throughout. This is stricter than the data-plane rule. Rolling two Sentinels concurrently in a 3-pod quorum makes failover impossible for the duration.Valkeys in the namespace matchingspec.valkeySelector. For each match:Valkey, notValkey.status.primary. Sentinel followsINFO replicationfrom any member to find the real primary. That avoids a cross-CR status dependency, and the bootstrap race where status is not yet populated. This mechanic is adopted from the selector draft, where it is a better fit than pushing the observed primary. If no pod is reachable, requeue.SENTINEL MONITOR <monitorName> <entry-address> <port> <quorum>on any ready pod missing the monitor. Then issueSENTINEL SETfor the credentials, plus every key in theValkey'sspec.sentinel.config. This stays idempotent by diffing against the liveSENTINEL MASTERSview first.Valkey, and issueSENTINEL REMOVE <name>. A relabel is an intentional opt-out, so there is no grace period. It is still recorded as an event, because it silently removes HA.status.monitorsfromSENTINEL MASTERS,SENTINEL REPLICASandSENTINEL CKQUORUMacross ready pods.Watches
Valkeyobjects so a label change reconciles monitoring within one event hop.The same flow as a sequence:
sequenceDiagram autonumber participant API as Kubernetes API participant VSC as ValkeySentinel Controller participant VSP as ValkeySentinel Pods (26379) participant VP as Valkey Pods (6379) API->>VSC: reconcile (ValkeySentinel, or a watched Valkey label change) VSC->>API: upsert headless Service 26379 and PDB VSC->>API: upsert ConfigMap (sentinel.conf plus copy script), hash into pod template VSC->>API: upsert StatefulSet (updateStrategy OnDelete) Note over VSC,VSP: quorum-gated rollout, one pod at a time loop for each pod whose template hash is stale VSC->>VSP: SENTINEL CKQUORUM and SENTINEL MASTERS alt quorum holds and no failover in flight VSC->>API: delete pod VSC->>VSP: wait for the replacement to report ready else quorum would be at risk VSC-->>VSC: requeue and leave the pod in place end end VSC->>API: list Valkeys matching spec.valkeySelector loop for each matched Valkey VSC->>API: list its data pods, read status.sentinelCredentialsSecret VSC->>VP: PING to pick any reachable member as entry address alt no member reachable VSC-->>VSC: requeue (real data-plane outage) else monitor missing on one or more Sentinels VSC->>VSP: SENTINEL MONITOR monitorName entryAddress port quorum VSC->>VSP: SENTINEL SET auth-user and auth-pass VSC->>VSP: SENTINEL SET keys from the Valkey spec.sentinel.config end end loop for each known monitor the selector no longer matches VSC->>VSP: SENTINEL REMOVE monitorName VSC->>API: emit MonitorRemoved event end VSC->>VSP: SENTINEL MASTERS, SENTINEL REPLICAS, SENTINEL CKQUORUM VSC->>API: patch status.monitors and conditionsValkey controller
Owns the data plane. It issues no monitor-lifecycle commands. Its only Sentinel interaction is the planned-failover trigger, because it is the controller that rolls pods.
_sentinel, the server ConfigMap, and the PDB. The headless Service, ConfigMap, ACL Secret and PDB reuse the ValkeyCluster helpers. The two role-selector Services are new. Publishstatus.sentinelCredentialsSecret.1 + spec.replicasValkeyNodes, all sharing one ConfigMap. Establish replication by command as pods become ready. That meansREPLICAOF NO ONEon node 0 at first bootstrap, andREPLICAOF <primary-fqdn> 6379on the rest. None of this survives a pod restart. SeeValkeyNode: no changes.INFO replicationon every ready node. Whenstatus.monitoring.monitoredByis non-empty, also runSENTINEL GET-MASTER-ADDR-BY-NAMEagainst a selecting Sentinel. The primary is Sentinel's answer when monitored, and the observedrole:masternode otherwise.role:masterthat is not Sentinel's primary and has no clients getsREPLICAOF <primary>. This covers the split-brain rejoin. More commonly it covers a restarted pod that came back as an orphan primary, because replication was never persisted (seeValkeyNode: no changes). Sentinel cannot see such a pod, because a node that is not replicating never appears in the primary'sINFO. Until per-node config lands, this rule is the only thing that repairs it, so unlike both prior drafts it cannot be dropped. Refusing to re-issueREPLICAOFafter bootstrap leaves the set permanently short a replica.HighestOffsetReplica. With a selecting Sentinel, do nothing. The election is Sentinel's.SENTINEL MASTERSreporting no in-flight failover.valkey.io/rolepod labels that drive the primary and replica Services.Monitoredfrom the set ofValkeySentinels selecting this object. Emit aMultipleSentinelsSelectingwarning when more than one does, since their per-masterSENTINEL SETvalues race and the last applied wins.The same flow as a sequence, including the planned-roll branch described in Planned operations:
sequenceDiagram autonumber participant API as Kubernetes API participant VC as Valkey Controller participant VN as ValkeyNode CRs participant VP as Valkey Pods (6379) participant VSP as ValkeySentinel Pods API->>VC: reconcile (Valkey, or an owned ValkeyNode change) VC->>API: upsert Services (headless, primary, replicas), ACL Secret, ConfigMap, PDB VC->>API: publish status.sentinelCredentialsSecret VC->>API: create or update 1 plus spec.replicas ValkeyNodes VN->>API: each renders its own StatefulSet, PVC and pod opt replication not yet established VC->>VP: REPLICAOF NO ONE on node 0 VC->>VP: REPLICAOF primaryFqdn 6379 on the remaining ready nodes Note over VC,VP: runtime-only, lost on pod restart end VC->>VP: INFO replication on every ready node alt a ValkeySentinel selects this Valkey VC->>VSP: SENTINEL GET-MASTER-ADDR-BY-NAME and SENTINEL MASTERS Note over VC,VSP: Sentinel's answer wins, and reports whether a failover is in flight else unmonitored Note over VC,VP: primary is the observed role master node end opt no failover in flight opt orphan primary found (role master, not Sentinel's primary, no clients) VC->>VP: REPLICAOF primaryFqdn 6379 end opt zero primaries and unmonitored VC->>VP: REPLICAOF NO ONE on the highest-offset replica end end opt a node needs a roll alt target holds the primary and a Sentinel selects this Valkey VC->>VSP: SENTINEL FAILOVER monitorName loop poll 1 s, deadline 10 s VC->>VSP: SENTINEL GET-MASTER-ADDR-BY-NAME end alt primary changed VC->>API: update the ValkeyNode, now a replica else handoff timed out VC-->>API: defer the roll, surface it, requeue end else target is a replica, or unmonitored VC->>API: update the ValkeyNode end end VC->>API: patch valkey.io/role pod labels VC->>API: patch status, conditions, MultipleSentinelsSelecting event if neededPlanned operations
master_link_status:upfor the remaining replicas.internal/controller/failover.gobut withSENTINEL FAILOVER <monitorName>instead ofCLUSTER FAILOVER. Sentinel must perform the promotion so that its view and the operator's stay consistent. PollSENTINEL GET-MASTER-ADDR-BY-NAMEon a 1 second tick with a 10 second deadline until it changes, then roll the old primary, which is now a replica. Unlike the prior drafts, the timeout is not soft. If the handoff does not complete, the roll is deferred and surfaced. Rolling the primary anyway would drop writes for up todown-after-millisecondsand risk the orphan case above.REPLICAOF NO ONE, repoint the others, then roll. Ifspec.replicas == 0there is nothing to do but accept the restart.REPLICAOF <primary>once its pod is ready. Sentinel discovers it from the primary'sINFO.SENTINEL RESET <monitorName>once, so Sentinel forgets the removed replica instead of reporting itsdownforever.RESETre-discovers everything, so it is issued once per scale-in and never in steady state.Valkeycascades its own children, and the selectingValkeySentinelobserves the disappearance and issuesSENTINEL REMOVE. Deleting aValkeySentinelcascades its StatefulSet and leaves monitored instances running in whatever topology Sentinel last set, withMonitored=Falseon the next reconcile.shutdown-on-sigterm. Cluster mode setsfailover, which is cluster-specific. Here the primary's graceful shutdown is preceded by the handoff above, soterminationGracePeriodSecondsderives fromfailover-timeoutrather than fromcluster-manual-failover-timeout.Watching failover
There are three ways to learn that a failover happened, ordered by latency. Subscribing to
+switch-masteron Sentinel's pub/sub channel is sub-second, but needs a long-lived connection per Sentinel. PollingSENTINEL GET-MASTER-ADDR-BY-NAMEon a short requeue is slower. Noticing the role change inINFO replicationis slower still.Poll first. It is simpler, consistent with the current reconciler style, and only the convenience Services care about the lag. A pub/sub watcher is a later optimization.
Linkage direction
Both CRDs are agreed. What remains is which side declares the link.
Valkey.spec.sentinel.sentinelRef(initial issue #198)Ready=False/SentinelRefUnresolvedDependentsPresentto stop a shared Sentinel being deleted out from under its dependentsSENTINEL SETwinsValkeySentinel.spec.valkeySelector(later approached in #198)SENTINEL SETvalues race and the last applied winskubectl deleteServiceMonitorprecedentspec.sentinel.required, per-masterspec.sentinel.config, and a published credential Secret referencerequiredis also a second place to look when diagnosing why an instance is not ReadyRecommendation: hybrid. The selector genuinely solves the problem #198 surfaced, where 30 instances each with a dedicated 3-pod quorum means 90 pods of monitoring. It also deletes the finalizer, the apply-order semantics, and three conditions. Its two real costs are that HA becomes unassertable and per-master tuning disappears. Both are recoverable for the price of one bool and one map. The credential direction is the part no draft has addressed, and
status.sentinelCredentialsSecretbounds it.Settled: CRD shape
Recorded for completeness. The discussion in #198 settled this.
Valkey+ValkeySentinel)ValkeyClusterwithspec.modeshardsbecomes meaningless, a 1500-line reconciler grows a second state machine, and the kind's name stops being true.Sentinel placement
replicas: 1yields a 2-Sentinel quorum that tolerates no loss. A node failure removes both a data node and a voterSENTINEL RESETaffects every selected instanceThe selector model gets both ends of the last row: a platform-wide quorum and a per-tenant one are the same object with different selectors.
Monitor registration ownership
SENTINEL MONITOR,SETandREMOVEcalls live in one place. The entry-address mechanic removes any dependency onValkey.status_sentinelcredential, bounded bystatus.sentinelCredentialsSecretKeep the asymmetry this creates explicit. Monitor lifecycle is Sentinel-side. The planned-failover trigger stays data-side, because that is the controller deciding to roll a pod.
Implementation notes
Constraints in the current repository that this design depends on, and what has to change.
RBAC additions.
config/rbac/role.yamltoday grants podsget/list/watchonly, and this design needs two more verbs on pods:patchvalkey.io/rolelabel that drives the primary and replica Services. Without it, those Services cannot work at alldeleteOnDeleterollout one pod at a time under theCKQUORUMgateStatefulSets, ConfigMaps, Secrets, Services and PodDisruptionBudgets already carry full verbs, so no other changes are needed.
role.yamlis generated from kubebuilder markers, so the grants belong on the new controllers and are picked up bymake manifests. It is never hand-edited (see architecture.md).No
valkey.io/rolelabel exists yet.:utils.godefines onlyvalkey.io/cluster,valkey.io/shard-indexandvalkey.io/node-index. The role label is new. It has to be a pod label rather than a workload-template label, because the role changes over the pod's life. That is whypatchon pods is required, rather than an update to the StatefulSet template.The client library covers Sentinel only partially.:
valkey-go v1.0.68provides typed builders forSentinelFailover,SentinelGetMasterAddrByName,SentinelReplicasandSentinelSentinelsonly.SENTINEL MONITOR,SET,REMOVE,RESET,MASTERSandCKQUORUMare most of what the registration path needs, and all of them must go throughB().Arbitrary(...)(internal/cmds/builder.go) with hand-written reply parsing. Budget for a realinternal/valkeySentinel layer rather than assuming the existing client covers it.The config renderer is already reusable, but the pod-template builder is not.:
getBaseConfigandrenderServerConfigtake plain values rather than a*ValkeyCluster(config.go), so both theValkeyand Sentinel config paths can call them directly.buildValkeyNodePodTemplateSpecandbuildContainersDef(valkeynode_resources.go) still take a*ValkeyNode. So the goal of sharing the builder means extracting the parts a Sentinel pod needs, namely volumes, security context, TLS and probe wiring, rather than calling them as they stand.Per-node governing Services are new work.: The StatefulSets already name a governing Service that nothing creates, so the
Valkeycontroller must create it for per-pod DNS to resolve at all.Risks
SENTINEL FAILOVER.REPLICAOFrepair rule, which means repair depends on the operator running. That is the one dependency Sentinel exists to remove. While the operator is down, a restarted replica sits as an orphan primary, and if it is labelledrole=primaryit joins the write Service alongside the real primary. This is bounded by reconcile latency in normal operation, and closed properly by per-node config (Future work). Both prior drafts leave this failure open entirely.spec.sentinel.required, theMonitoredcondition, and an event onSENTINEL REMOVE.SENTINEL SETwins. Mitigated byMultipleSentinelsSelectingand bystatus.monitoring.monitoredBymaking the overlap visible.Deploymentabovereplicas: 0, refusing to monitor Deployment-backed instances, and warning on images withoutresolve-hostnames.OnDelete, one-at-a-time replacement, andCKQUORUMgating. The reused PDB contributesmaxUnavailable: 1, which is sufficient at the default 3 replicas but does not protect a quorum abovereplicas-1. The gate, not the budget, is what makes this safe until aQuorumPDB mode exists.status.readyReplicasandMonitorStatus.replicas, rather than reconciling them into one number.Delivery phases
Valkeywithout Sentinel.: A primary plus replicas over unmodifiedValkeyNodes. The set-wide, role-labelled and per-node governing Services, plus the podpatchRBAC the role labels need. Command-driven replication with the repair rule. Reuse of ACL, TLS, persistence and PDB. Operator-driven promotion. This ships standalone and plain replication, and is independently useful.ValkeySentinelpods.: A StatefulSet withOnDelete, the ConfigMap and copy script, the headless Service, the PDB, andstatusfromSENTINEL MASTERS. No selection yet.valkeySelector, monitor register, update and remove, the credential contract, and theMonitoredandSentinelQuorumHealthyconditions.SENTINEL FAILOVERbefore planned primary rolls,SENTINEL RESETon scale-in, and gating the phase-1 repair rule on Sentinel's view so the two never fight.+switch-masterpub/sub watcher, Sentinel metrics (valkey_operator_sentinel_*), docs forrequiredenforcement, and migrating Sentinel pods ontoValkeyNodeif and when it grows the needed primitives.Testing
internal/valkey/). Parsers forSENTINEL MASTERS,REPLICASandSENTINELS, quorum math, monitor diffing, and reserved-key filtering. These are pure functions, so table-driven tests mirroringclusterstate_test.gofit.internal/controller/). CRD validation, covering immutablemonitorName,requiredneeding replicas, andDeploymentrejected abovereplicas: 0. Selector matching and de-selection. Child-object shape, including the per-node governing Services. One-at-a-time roll ordering. Condition transitions against a faked Sentinel client.SENTINEL REMOVE. Scale in and assert no lingeringsdownentries. Roll the Sentinel image and assert quorum never drops belowquorum. Run one Sentinel monitoring three instances. Cover TLS and read-only-rootfs variants.Future work
ValkeyNode: no changes. There are two viable shapes. The first is to leaveServerConfigMapNameempty and extendgenerateValkeyNodeConfig(config.go) so eachValkeyNoderenders its own file includingreplicaof. Note that this path currently ignoresSpec.Configfor the file. The second is to keep the shared ConfigMap and add a tiny per-node one holding justreplicaof, pulled in by anincludeat the end of the sharedvalkey.conf. There the include target must always exist, empty for node 0, or startup fails. The second option is the smaller diff, and it preserves the single shared ConfigMap and its hash. Either way, the per-node roll hash (nodeServerConfigRollHash,config.go) has to account for the new directive, and the primary-change path should update the file without rolling the pod.PodDisruptionBudgetConfigis reused as is today, which meansmaxUnavailable: 1and a mode value namedCluster. AQuorummode emittingminAvailable = <quorum>would be correct for Sentinel sets whose quorum exceedsreplicas-1. It would also stop the enum reading as cluster-only.spec.namespaceSelector, gated by aReferenceGrantstyle mechanism, so a platform-team quorum can monitor every tenant.ValkeyCell: Ingtegration for non-cluster sharding. N labelledValkeys plus oneValkeySentinelselecting them, managed as a single logical unit.ValkeyNode: Check if ValkeyNode can support an alternate binary, an alternate port, and no PVC.API Changes
No response
User Stories
No response
Alternatives Considered
No response
Backward Compatibility
No response
Testing Strategy
No response
Open Questions
No response
References
No response
Implementation
All reactions