Skip to content

Commit 0133db9

Browse files
lmicciniclaude
andcommitted
Fix redis split-brain after pod-0 restart during failover
When redis-redis-0 (the bootstrap pod) is deleted during a failover, it restarts and tries to contact sentinel to find the current master. Three problems caused it to fall through to the bootstrap path and start a new independent master, creating a split-brain: 1. Single-try timeout: if sentinel was momentarily unreachable (e.g. the sentinel container on pod-0 itself was still starting), the 3-second timeout expired and pod-0 immediately bootstrapped. 2. Headless service DNS: with PublishNotReadyAddresses: true, the headless service DNS can resolve to pod-0's own IP, so redis-cli connects to its own uninitialized sentinel instead of a peer. 3. Stale master identity: even when contacting a peer sentinel, it may still report the restarting pod as master (within the down-after-milliseconds window before failover completes). Fix by adding a wait_for_master() function in common.sh that: - Contacts each peer pod individually by FQDN (skipping self) - Uses the REPLICAS env var to iterate only over actual peers - Retries up to 10 times (30s total) before allowing bootstrap - Rejects answers where the peer still thinks we are master - Returns immediately if no peers are reachable at all (e.g. first deployment), avoiding unnecessary delay on initial bootstrap Also pass a REPLICAS env var from the StatefulSet spec so the script knows the exact replica count, increase InitialDelaySeconds to 40s on all redis and sentinel probes so Kubernetes doesn't kill the pod before the retry loop completes, and remove unused TCP probe variables that were never referenced by the redis container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b98bf2f commit 0133db9

4 files changed

Lines changed: 57 additions & 31 deletions

File tree

internal/redis/statefulset.go

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,36 +27,15 @@ func StatefulSet(
2727
}
2828
ls := labels.GetLabels(r, "redis", matchls)
2929

30-
livenessProbe := &corev1.Probe{
31-
// TODO might need tuning
32-
TimeoutSeconds: 5,
33-
PeriodSeconds: 3,
34-
InitialDelaySeconds: 3,
35-
}
36-
readinessProbe := &corev1.Probe{
37-
// TODO might need tuning
38-
TimeoutSeconds: 5,
39-
PeriodSeconds: 5,
40-
InitialDelaySeconds: 5,
41-
}
4230
sentinelLivenessProbe := &corev1.Probe{
43-
// TODO might need tuning
4431
TimeoutSeconds: 5,
4532
PeriodSeconds: 3,
46-
InitialDelaySeconds: 3,
33+
InitialDelaySeconds: 40,
4734
}
4835
sentinelReadinessProbe := &corev1.Probe{
49-
// TODO might need tuning
5036
TimeoutSeconds: 5,
5137
PeriodSeconds: 5,
52-
InitialDelaySeconds: 5,
53-
}
54-
55-
livenessProbe.TCPSocket = &corev1.TCPSocketAction{
56-
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(6379)},
57-
}
58-
readinessProbe.TCPSocket = &corev1.TCPSocketAction{
59-
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(6379)},
38+
InitialDelaySeconds: 40,
6039
}
6140
sentinelLivenessProbe.TCPSocket = &corev1.TCPSocketAction{
6241
Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(26379)},
@@ -78,6 +57,9 @@ func StatefulSet(
7857
}, {
7958
Name: "CONFIG_HASH",
8059
Value: configHash,
60+
}, {
61+
Name: "REPLICAS",
62+
Value: strconv.Itoa(int(*r.Spec.Replicas)),
8163
}}
8264

8365
sts := &appsv1.StatefulSet{
@@ -115,13 +97,15 @@ func StatefulSet(
11597
Command: []string{"/var/lib/operator-scripts/redis_probe.sh", "liveness"},
11698
},
11799
},
100+
InitialDelaySeconds: 40,
118101
},
119102
ReadinessProbe: &corev1.Probe{
120103
ProbeHandler: corev1.ProbeHandler{
121104
Exec: &corev1.ExecAction{
122105
Command: []string{"/var/lib/operator-scripts/redis_probe.sh", "readiness"},
123106
},
124107
},
108+
InitialDelaySeconds: 40,
125109
},
126110
}, {
127111
Image: r.Spec.ContainerImage,

templates/redis/bin/common.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,52 @@ function remove_pod_label() {
8484
configure_pod_label $pod "$patch" "(200|422)"
8585
}
8686

87+
# Wait for a peer sentinel to report a valid master for the cluster.
88+
# Contacts each peer pod individually by FQDN (skipping self) to avoid
89+
# the headless service DNS resolving to our own uninitialized sentinel.
90+
# If a peer still reports US as master (stale info before
91+
# down-after-milliseconds triggers failover), keeps retrying until
92+
# failover completes and a different master is elected.
93+
# If no peers are reachable at all (first deployment), returns
94+
# immediately so the bootstrap pod can start without delay.
95+
# Prints the master address on success (FQDN or IP).
96+
function wait_for_master() {
97+
local retries=${SENTINEL_RETRIES:-10}
98+
local delay=${SENTINEL_RETRY_DELAY:-3}
99+
local pod_ordinal=${POD_NAME##*-}
100+
local pod_base=${POD_NAME%-*}
101+
local replicas=${REPLICAS:-3}
102+
103+
for i in $(seq 1 $retries); do
104+
local any_peer_reachable=0
105+
local ordinal=0
106+
while [ $ordinal -lt $replicas ]; do
107+
if [ "$ordinal" != "$pod_ordinal" ]; then
108+
local peer="${pod_base}-${ordinal}.${SVC_FQDN}"
109+
local output
110+
if output=$(timeout ${TIMEOUT} $REDIS_CLI_CMD -h ${peer} -p 26379 sentinel master redis 2>/dev/null); then
111+
any_peer_reachable=1
112+
local master
113+
master=$(echo "$output" | awk '/^ip$/ {getline; print $0; exit}')
114+
# If the peer still thinks WE are master, it has stale
115+
# pre-failover info — try remaining peers before waiting.
116+
if ! echo "$master" | grep -q "^${POD_NAME}\."; then
117+
echo "$master"
118+
return 0
119+
fi
120+
fi
121+
fi
122+
ordinal=$((ordinal + 1))
123+
done
124+
if [ $any_peer_reachable -eq 0 ]; then
125+
return 1
126+
fi
127+
log "Attempt $i/$retries: no valid master found, retrying in ${delay}s..."
128+
sleep $delay
129+
done
130+
return 1
131+
}
132+
87133
function set_pod_label() {
88134
local pod="$1"
89135
local label="$2"

templates/redis/bin/start_redis_replication.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
generate_configs
66
sudo -E kolla_set_configs
77

8-
# 1. check if a redis cluster is already running by contacting sentinel
9-
output=$(timeout ${TIMEOUT} $REDIS_CLI_CMD -h ${SVC_FQDN} -p 26379 sentinel master redis)
8+
# 1. check if a redis cluster is already running by contacting peer sentinels
9+
master=$(wait_for_master)
1010
if [ $? -eq 0 ]; then
11-
master=$(echo "$output" | awk '/^ip$/ {getline; print $0; exit}')
12-
# TODO skip if no master was found
1311
log "Connecting to the existing Redis cluster (master: ${master})"
1412
exec redis-server $REDIS_CONFIG --protected-mode no --replicaof "$master" 6379
1513
fi

templates/redis/bin/start_sentinel.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
generate_configs
66
sudo -E kolla_set_configs
77

8-
# 1. check if a redis cluster is already running by contacting sentinel
9-
output=$(timeout ${TIMEOUT} $REDIS_CLI_CMD -h ${SVC_FQDN} -p 26379 sentinel master redis)
8+
# 1. check if a redis cluster is already running by contacting peer sentinels
9+
master=$(wait_for_master)
1010
if [ $? -eq 0 ]; then
11-
master=$(echo "$output" | awk '/^ip$/ {getline; print $0; exit}')
12-
# TODO skip if no master was found
1311
log "Connecting to the existing sentinel cluster (master: $master)"
1412
echo "sentinel monitor redis ${master} 6379 ${SENTINEL_QUORUM}" >> $SENTINEL_CONFIG
1513
exec redis-sentinel $SENTINEL_CONFIG

0 commit comments

Comments
 (0)