Skip to content

Commit 618c399

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) - Retries up to 10 times (30s total) before allowing bootstrap - Rejects answers where the peer still thinks we are master Also 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 618c399

4 files changed

Lines changed: 46 additions & 31 deletions

File tree

internal/redis/statefulset.go

Lines changed: 4 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)},
@@ -115,13 +94,15 @@ func StatefulSet(
11594
Command: []string{"/var/lib/operator-scripts/redis_probe.sh", "liveness"},
11695
},
11796
},
97+
InitialDelaySeconds: 40,
11898
},
11999
ReadinessProbe: &corev1.Probe{
120100
ProbeHandler: corev1.ProbeHandler{
121101
Exec: &corev1.ExecAction{
122102
Command: []string{"/var/lib/operator-scripts/redis_probe.sh", "readiness"},
123103
},
124104
},
105+
InitialDelaySeconds: 40,
125106
},
126107
}, {
127108
Image: r.Spec.ContainerImage,

templates/redis/bin/common.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,44 @@ 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+
# Prints the master address on success (FQDN or IP).
94+
function wait_for_master() {
95+
local retries=${SENTINEL_RETRIES:-10}
96+
local delay=${SENTINEL_RETRY_DELAY:-3}
97+
local pod_ordinal=${POD_NAME##*-}
98+
local pod_base=${POD_NAME%-*}
99+
100+
for i in $(seq 1 $retries); do
101+
local ordinal=0
102+
while [ $ordinal -le 9 ]; do
103+
if [ "$ordinal" != "$pod_ordinal" ]; then
104+
local peer="${pod_base}-${ordinal}.${SVC_FQDN}"
105+
local output
106+
output=$(timeout ${TIMEOUT} $REDIS_CLI_CMD -h ${peer} -p 26379 sentinel master redis 2>/dev/null) && {
107+
local master
108+
master=$(echo "$output" | awk '/^ip$/ {getline; print $0; exit}')
109+
# If the peer still thinks WE are master, it has stale
110+
# pre-failover info — try remaining peers before waiting.
111+
if ! echo "$master" | grep -q "^${POD_NAME}\."; then
112+
echo "$master"
113+
return 0
114+
fi
115+
}
116+
fi
117+
ordinal=$((ordinal + 1))
118+
done
119+
log "Attempt $i/$retries: no valid master found, retrying in ${delay}s..."
120+
sleep $delay
121+
done
122+
return 1
123+
}
124+
87125
function set_pod_label() {
88126
local pod="$1"
89127
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)