Skip to content

[chart/redis-ha][BUG] Masterless deadlock: sentinel keeps naming a master whose actual role is replica; split-brain container never heals it #412

Description

@ShmuelOps

Describe the bug

After the Redis master pod is lost (spot node reclaim in our case) and its replacement is rescheduled behind the same announce Service ClusterIP, the set can wedge into a state with zero actual masters that nothing in the chart ever heals:

  • The replacement pod's init.sh can't get a usable answer at boot and falls back to replicating announce-0.
  • Sentinel keeps advertising the old master's announce IP as master — the IP is still up (it's a stable ClusterIP, now answering from the replacement pod), so sentinel's down detection never triggers a failover.
  • The other replicas keep replicating the sentinel-named "master", which is itself a replica: they loop on Master is currently unable to PSYNC but should be in the future: -NOMASTERLINK Can't SYNC while not connected with my master once per second, indefinitely.
  • The split-brain-fix container never intervenes: it only reinitializes when a pod's local master_host disagrees with sentinel (or when sentinel returns no master → MAX_QUORUM_FAILURES reset). Here every member agrees with sentinel about the stale master, so both conditions pass. The one pod that does get reinit'd (the one owning the stale announce IP, since sentinel names it master while its role is replica) just re-runs init.sh, whose fallback makes it a replica again — a ~4-minute reinit loop that never converges.

Observed on chart 4.39.0 (Redis 7.4, 5 replicas, quorum: 3, haproxy enabled, no PVC): the set stayed masterless — accepting zero writes — for 7.5 hours until manual intervention (restarting two members and rescheduling the stale-IP pod finally re-seated the topology). Logs from the two ends of the cycle:

server-0 (agreed with sentinel that the dead master's announce IP was master):

16:13:31 * Connection with master lost.
16:13:31 * Reconnecting to MASTER 172.20.242.95:6379
16:13:31 # Error condition on socket for SYNC: Connection refused
...then hours of: * Trying a partial resynchronization (request ...)

server-4 (the replacement pod behind that announce IP, running as replica of announce-0):

16:16:00 * Connecting to MASTER 172.20.156.64:6379
16:16:00 * Master is currently unable to PSYNC but should be in the future: -NOMASTERLINK Can't SYNC while not connected with my master
(repeats once per second for 7.5h)

To Reproduce

  1. Deploy redis-ha (≥3 replicas, sentinel defaults, no persistence).
  2. Kill the node hosting the current master pod (uncleanly — e.g. spot interruption) so the pod is rescheduled while its announce ClusterIP stays constant.
  3. If the replacement boots before sentinel completes a failover (or init.sh hits its announce-0 fallback), the set wedges: INFO replication shows role:slave on every member, sentinel get-master-addr-by-name still returns the old master's announce IP, and no failover ever fires.

Timing-dependent — it needs the replacement pod to come back on the same announce IP before sentinel declares odown — but node-loss + reschedule makes it reachable in practice.

Expected behavior

The split-brain container (or sentinel config shipped by the chart) should detect that the sentinel-named master does not actually hold role:master and force a failover, instead of leaving the set masterless forever.

Additional context

PR incoming: adds a stale-master check to the split-brain loop's agreement path — if the sentinel-named master reports a non-master role for MAX_STALE_MASTER_FAILURES consecutive checks (default 3), issue SENTINEL FAILOVER <group> so sentinel promotes one of the known replicas. Unreachable masters are still left to sentinel's own down detection.

Activity

  1. hexilic commented on Oct 7, 2026

    @hexilic

    We hit the same thing on chart 4.39.0 with Valkey 9.1.2 (3 replicas, quorum: 2, HAProxy, auth on, no persistence), and we have a reproduction that doesn't depend on timing: force-delete the master pod.

    Reproduction (k3d, four nodes):

    kubectl delete pod <current-master> --grace-period=0 --force

    The replacement starts within a second, while the old container is still answering on the announce ClusterIP. init.sh asks Sentinel, which hasn't noticed the kill yet, and gets that announce IP. It pings it, the dying container answers, and the new pod configures itself as a replica of its own announce address:

    11:13:50 Found redis master (10.43.192.125)
    11:13:50 Found reachable redis master (10.43.192.125)
             we are slave of redis master (10.43.192.125:6379)   <- its own announce IP
    

    From there it played out as you describe in #412, plus two details that may help:

    • Sentinel did reach odown and voted, but the elected leader never acted. Every round was won by the Sentinel in the replacement pod. init.sh had rewritten that Sentinel's config, and it never ran the failover. With the default failover-timeout: 180000 the retries came every 6 minutes, each won by the same Sentinel:

      11:14:23 +odown master mymaster 10.43.192.125 6379 #quorum 2/2
      11:14:23 Next failover delay: I will not start a failover before ... 11:20:23
      11:20:22 +vote-for-leader e5c7e564... 2
      11:26:22 +vote-for-leader e5c7e564... 3
      
    • A manual SENTINEL FAILOVER from another Sentinel aborted with -failover-abort-no-good-slave: the replicas had been disconnected for longer than Sentinel trusts. Meanwhile the wedged pod's redis container crash-looped on its startup probe (role=slave; repl=connect).

    We recovered after 18 minutes by gracefully deleting the pod behind the stale announce IP, which also removed the Sentinel that kept winning the vote. The next scheduled retry then elected another Sentinel, which promoted a replica normally. No data was lost.

    A milder form, from a graceful rolling restart: during a rollout, a restarted master's replacement asked the shared Sentinel Service, reached a Sentinel that hadn't switched yet, and was again told its own address. That time split-brain-fix caught the mismatch ("expected master is , reinitializing") and it healed in about 10 s. It shows the stale answer isn't limited to unclean node loss.

    And a related one: after a hard kill, init.sh forced SENTINEL FAILOVER using a master address it had resolved about 15 s earlier. By then Sentinel had already failed over correctly, so the forced failover moved the healthy new master a second time. Re-querying get-master-addr-by-name right before forcing a failover would avoid that.

    What we've done meanwhile: failover-timeout: 60000, an alert when no pod reports role:master, and never force-deleting a master in drills. #413's approach (fail over when the Sentinel-named master reports a non-master role for N checks) would have healed both our cases. We'd be glad to see it merged and can test a build of it against this reproduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions