You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
async mode: mysql pod stays 1/2 Running indefinitely after restart — chicken-and-egg between reconcileReplication and HeadlessService.PublishNotReadyAddresses=false #1342
In async clusters, any single mysql pod restart (helm upgrade rolling restart, kubectl delete pod, node drain, eviction) can leave the restarted pod stuck 1/2 Running indefinitely. MySQL is up, replication is healthy and applying binlog events, but the startupProbe never returns success and the pod's started flag stays false. The cluster stays in state: initializing forever; manual kubectl delete pod --force recovers (the second attempt wins the race).
This is the "fresh issue with the latest details" requested when #626 was closed (#626 (comment)).
More about the problem
Three concurrent log signatures confirm the pattern (all from a real reproduction on a 3-node async cluster, mysql-0 was the affected pod):
Operator — every reconcile loop (~5 s):
INFO reconcileReplication mysql is not ready, host not found. skip
{"controllerKind": "PerconaServerMySQL", "name": "mcx-ps-db", "namespace": "mcx"}
Headless service Endpoints — affected pod is missing from the ready service but present in the -unready sibling:
$ kubectl -n mcx get endpoints mcx-ps-db-mysql mcx-ps-db-mysql-unready
NAME ENDPOINTS
mcx-ps-db-mysql 10.42.2.227:3306, 10.42.3.102:3306 # only mysql-1 and mysql-2
mcx-ps-db-mysql-unready 10.42.1.143:3306, 10.42.2.227:3306, 10.42.3.102:3306 # all three
Orchestrator (<release>-ps-db-orc-0 orchestrator container) sees the unready pod as a phantom one-node cluster it cannot reach:
WARNING DiscoverInstance(mcx-ps-db-mysql-0.mcx-ps-db-mysql.mcx:3306) instance is nil
error=dial tcp: lookup mcx-ps-db-mysql-0.mcx-ps-db-mysql.mcx on 10.43.0.10:53: no such host
WARNING executeCheckAndRecoverFunction: ignoring analysisEntry that has no action plan:
DeadMasterWithoutReplicas; key: mcx-ps-db-mysql-0.mcx-ps-db-mysql.mcx:3306
Meanwhile, on the affected pod's own mysql container, replication is fully operational:
Root cause — chicken-and-egg between two design choices on main:
pkg/mysql/mysql.go#L440-L458 — HeadlessService sets PublishNotReadyAddresses: cr.Spec.MySQL.IsGR(). In async mode this is false, so the ready service <release>-mysql deliberately omits unready pods. The per-pod FQDN <release>-mysql-N.<release>-mysql.<ns> only resolves once the pod is in the ready endpoints.
pkg/controller/ps/controller.go#L905 — reconcileReplication calls orchestrator.Discover(ctx, ..., mysql.ServiceName(cr), DefaultPort). Orchestrator's discover endpoint resolves each pod by its per-pod FQDN against the ready service, which NXDOMAINs for the unready pod. The error maps to orchestrator.ErrNoSuchHost → log line in 2 above → return nil. No fallback to pod IP, no fallback to the -unready service.
The deadlock:
pod unready → ready-service Endpoints omits pod → per-pod FQDN NXDOMAINs
↑ ↓
↑ operator's orchestrator.Discover returns ErrNoSuchHost
↑ ↓
↑ reconcileReplication never registers pod in topology
↑ ↓
↑ startupProbe `/opt/percona/bootstrap` never confirmed ←
Deploy a healthy 3-replica async cluster (clusterType: async, mysql.size: 3) and let it reach state: ready.
Restart any single mysql pod: kubectl delete pod <release>-ps-db-mysql-0 (or trigger via any spec change, helm upgrade, node drain, etc.).
Observe the pod come back, replicate from the primary, but stay 1/2 Running indefinitely. The PerconaServerMySQL resource stays in state: initializing.
Confirm by grepping the operator log for reconcileReplication ... host not found. skip and by checking that the pod's IP is in <release>-ps-db-mysql-unready Endpoints but NOT in <release>-ps-db-mysql Endpoints.
Recover with kubectl delete pod <release>-ps-db-mysql-0 --force --grace-period=0. The second attempt usually wins the race because the new pod's IP races into the ready service before the operator's next reconcile pass classifies it as unready.
The bug is timing-sensitive — sometimes the first restart self-heals, sometimes it deadlocks. Helm upgrade rolling restarts hit it most often (we see it almost every release).
Operator: 1.0.0 (verified the same code paths persist on main and v1.1.0).
Database: percona/percona-server:8.0.44
Anything else?
Affected versions. The relevant code in pkg/mysql/mysql.go and pkg/controller/ps/controller.go is unchanged between v1.0.0 and main (as of this filing). PR #722 / K8SPS-372 (v0.10.0/v0.11.0) fixed an adjacent async bootstrap race in 2-pod clusters but does not touch this code path.
Suggested fix directions (ranked from least to most invasive):
reconcileReplication fall back to the -unready service. Change the mysql.ServiceName(cr) argument at controller.go:905 to mysql.UnreadyServiceName(cr) in async mode. The unready service already exists and is already used by the bootstrap container itself (per PR #16) — extending it to the operator's reconcile path is internally consistent.
reconcileReplication retry on ErrNoSuchHost with pod IP. When the FQDN lookup fails, fall back to querying orchestrator with pod.Status.PodIP (the operator already has each mysql pod object via r.Get). Slightly larger change but doesn't depend on the secondary service.
Flip HeadlessService.PublishNotReadyAddresses=true for async. Most direct fix, single line at mysql.go:458. The downside: applications connecting to the headless service by name (rare — almost everyone uses the -haproxy or -router services) would briefly see unready pods in DNS responses. Probably worth the tradeoff but the other two options sidestep it entirely.
Workaround until a fix lands. Force-delete the stuck pod:
kubectl -n <namespace> delete pod <release>-ps-db-mysql-N --force --grace-period=0
Force-delete is safe because the affected pod is a replica that was never confirmed into orchestrator's topology — there's no clean-shutdown state to lose. The new pod boots from the existing PVC, reconnects to the primary, and on this second attempt the operator's reconcile loop wins the race against the startup-probe deadline.
Happy to test a candidate fix on our staging cluster if it'd help.
Report
In async clusters, any single mysql pod restart (helm upgrade rolling restart,
kubectl delete pod, node drain, eviction) can leave the restarted pod stuck1/2 Runningindefinitely. MySQL is up, replication is healthy and applying binlog events, but the startupProbe never returns success and the pod'sstartedflag staysfalse. The cluster stays instate: initializingforever; manualkubectl delete pod --forcerecovers (the second attempt wins the race).This is the "fresh issue with the latest details" requested when #626 was closed (#626 (comment)).
More about the problem
Three concurrent log signatures confirm the pattern (all from a real reproduction on a 3-node async cluster, mysql-0 was the affected pod):
Operator — every reconcile loop (~5 s):
Headless service Endpoints — affected pod is missing from the ready service but present in the
-unreadysibling:Orchestrator (
<release>-ps-db-orc-0orchestrator container) sees the unready pod as a phantom one-node cluster it cannot reach:Meanwhile, on the affected pod's own mysql container, replication is fully operational:
Root cause — chicken-and-egg between two design choices on
main:pkg/mysql/mysql.go#L440-L458—HeadlessServicesetsPublishNotReadyAddresses: cr.Spec.MySQL.IsGR(). In async mode this is false, so the ready service<release>-mysqldeliberately omits unready pods. The per-pod FQDN<release>-mysql-N.<release>-mysql.<ns>only resolves once the pod is in the ready endpoints.pkg/controller/ps/controller.go#L905—reconcileReplicationcallsorchestrator.Discover(ctx, ..., mysql.ServiceName(cr), DefaultPort). Orchestrator's discover endpoint resolves each pod by its per-pod FQDN against the ready service, which NXDOMAINs for the unready pod. The error maps toorchestrator.ErrNoSuchHost→ log line in 2 above →return nil. No fallback to pod IP, no fallback to the-unreadyservice.The deadlock:
Group replication is unaffected because
HeadlessServiceflipsPublishNotReadyAddressestotruewhenIsGR()— the per-pod FQDN resolves regardless of readiness, and the operator's discover succeeds on the first attempt.Steps to reproduce
clusterType: async,mysql.size: 3) and let it reachstate: ready.kubectl delete pod <release>-ps-db-mysql-0(or trigger via any spec change, helm upgrade, node drain, etc.).1/2 Runningindefinitely. ThePerconaServerMySQLresource stays instate: initializing.reconcileReplication ... host not found. skipand by checking that the pod's IP is in<release>-ps-db-mysql-unreadyEndpoints but NOT in<release>-ps-db-mysqlEndpoints.kubectl delete pod <release>-ps-db-mysql-0 --force --grace-period=0. The second attempt usually wins the race because the new pod's IP races into the ready service before the operator's next reconcile pass classifies it as unready.The bug is timing-sensitive — sometimes the first restart self-heals, sometimes it deadlocks. Helm upgrade rolling restarts hit it most often (we see it almost every release).
Versions
mainand v1.1.0).Anything else?
Affected versions. The relevant code in
pkg/mysql/mysql.goandpkg/controller/ps/controller.gois unchanged between v1.0.0 andmain(as of this filing). PR #722 / K8SPS-372 (v0.10.0/v0.11.0) fixed an adjacent async bootstrap race in 2-pod clusters but does not touch this code path.Suggested fix directions (ranked from least to most invasive):
reconcileReplicationfall back to the-unreadyservice. Change themysql.ServiceName(cr)argument atcontroller.go:905tomysql.UnreadyServiceName(cr)in async mode. The unready service already exists and is already used by the bootstrap container itself (per PR #16) — extending it to the operator's reconcile path is internally consistent.reconcileReplicationretry onErrNoSuchHostwith pod IP. When the FQDN lookup fails, fall back to querying orchestrator withpod.Status.PodIP(the operator already has each mysql pod object viar.Get). Slightly larger change but doesn't depend on the secondary service.HeadlessService.PublishNotReadyAddresses=truefor async. Most direct fix, single line atmysql.go:458. The downside: applications connecting to the headless service by name (rare — almost everyone uses the-haproxyor-routerservices) would briefly see unready pods in DNS responses. Probably worth the tradeoff but the other two options sidestep it entirely.Workaround until a fix lands. Force-delete the stuck pod:
Force-delete is safe because the affected pod is a replica that was never confirmed into orchestrator's topology — there's no clean-shutdown state to lose. The new pod boots from the existing PVC, reconnects to the primary, and on this second attempt the operator's reconcile loop wins the race against the startup-probe deadline.
Happy to test a candidate fix on our staging cluster if it'd help.
Related
-unreadyservice for the bootstrap container's peer-list discovery.