Problem Description
During rolling restarts or horizontal pod autoscaling of a PostgreSQL StatefulSet, repeated REINDEX CONCURRENTLY operations abort with lock timeouts or out‑of‑memory errors. The symptoms observed in production are:
- Log entries such as:
2024-07-12 14:03:21.123 UTC [12345] LOG: index build (concurrently) aborted: could not obtain exclusive lock on relation "orders_pkey"
2024-07-12 14:03:21.124 UTC [12345] ERROR: lock timeout while attempting to REINDEX CONCURRENTLY
2024-07-12 14:03:21.125 UTC [12345] ERROR: out of memory while allocating memory for index build (maintenance_work_mem exceeded)
- Read‑only mode is temporarily enforced on affected tables, causing client queries to fail.
- Pod restarts trigger a surge in
pg_stat_progress_create_indexentries, with each step stuck atphase = 'building'for >30 seconds. - Metrics show CPU throttling and memory OOM kills during the index rebuild window.
Root Cause Analysis
The failure originates from the interaction of three Kubernetes‑level factors with PostgreSQL’s index rebuild mechanics:
- Lock acquisition semantics of
REINDEX CONCURRENTLY– The command first takes aSHARE UPDATE EXCLUSIVElock, then later attempts anACCESS EXCLUSIVElock to replace the old index. If another session holds a conflicting lock (e.g., a long‑running transaction that started before the pod restart), PostgreSQL will wait up tolock_timeoutbefore aborting. This behavior is documented in the PostgreSQL REINDEX CONCURRENTLY reference. - Resource constraints during pod startup – When a pod restarts, the container’s
maintenance_work_memdefaults to a low value (often 64 MiB) while the pod’s CPU limit may be hit by the intensive index build. The maintenance_work_mem setting controls the amount of memory PostgreSQL can use for sorting and hashing during index creation. If the process exceeds this limit, the server emits “out of memory” errors and may be OOM‑killed by the kubelet, as seen in the fintech incident report. - Persistent Volume I/O latency – During pod initialization the PVC may be in a cold state, causing checkpoint and WAL replay to stall. The checkpoint documentation notes that high I/O latency can extend the time needed to acquire the final exclusive lock, pushing the operation beyond the configured
lock_timeout.
Combined, these factors cause the index rebuild to exceed the lock timeout and/or exhaust memory, leading to the observed aborts.
Investigation and Debugging
The following step‑by‑step process reproduces the diagnostic workflow used in the CrunchyData and Zalando GitHub issues.
- Identify the failing REINDEX statements from the PostgreSQL logs:
2024-07-12 14:03:21.123 UTC [12345] ERROR: lock timeout while attempting to REINDEX CONCURRENTLY
DETAIL: Statement: REINDEX INDEX CONCURRENTLY orders_pkey;
- Check lock wait information using
pg_locksandpg_stat_activity:
SELECT pid, locktype, mode, granted, query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT granted AND relation = 'orders_pkey'::regclass;
Typical output shows a long‑running transaction holding a ROW SHARE lock.
pid | locktype | mode | granted | query
-----+----------+-------------------+---------+---------------------------------
987 | relation | ROW SHARE | f | SELECT * FROM orders WHERE ...
- Inspect index build progress via
pg_stat_progress_create_index:
SELECT pid, relid::regclass, phase, lockers, blocks_total, blocks_done
FROM pg_stat_progress_create_index;
If phase = 'building' and blocks_done stalls, the operation is likely blocked on the exclusive lock.
- Validate resource limits on the PostgreSQL pod:
kubectl get pod postgres-0 -o yaml | grep -E 'memory|cpu'
Example snippet:
resources:
limits:
cpu: "2"
memory: "2Gi"
requests:
cpu: "500m"
memory: "1Gi"
Compare against the maintenance_work_mem setting inside the container:
psql -c "SHOW maintenance_work_mem;"
Typical output:
maintenance_work_mem
----------------------
64MB
(1 row)
- Capture I/O latency during pod startup with
iostatorexecinto the container:
kubectl exec -it postgres-0 -- iostat -x 1 5
High await values (>30 ms) indicate PVC warm‑up delays.
Resolution
The fix consists of three coordinated changes:
1. Increase lock_timeout and tune maintenance_work_mem
Set a higher lock timeout (e.g., 5 minutes) to give the index rebuild enough time to acquire the exclusive lock after pod startup, and raise maintenance_work_mem to a value that matches the pod’s memory request.
# postgresql.conf (or ConfigMap)
lock_timeout = '5min' # default 0 (no timeout)
maintenance_work_mem = '512MB' # must be ≤ pod memory request
max_parallel_maintenance_workers = 2 # optional, speeds up REINDEX
After updating the ConfigMap, trigger a rolling restart so each pod picks up the new settings.
2. Adjust Kubernetes resource requests
Ensure the pod’s memory request comfortably exceeds maintenance_work_mem plus overhead for shared buffers and WAL buffers.
| Parameter | Current | Recommended |
|---|---|---|
| memory request | 1 Gi | ≥ 2 Gi |
| memory limit | 2 Gi | ≥ 3 Gi |
| cpu request | 500 m | ≥ 1 CPU |
Update the StatefulSet spec:
spec:
template:
spec:
containers:
- name: postgres
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "3Gi"
cpu: "2"
3. Serialize REINDEX operations during rolling updates
Introduce a pre‑stop hook that pauses any automatic REINDEX CONCURRENTLY jobs until the pod reports Ready and the WAL replay has completed.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "
echo 'Waiting for WAL replay...';
while ! pg_isready -U postgres; do sleep 1; done;
echo 'WAL replay finished, allowing REINDEX';
"]
Alternatively, use a Kubernetes Job that runs REINDEX CONCURRENTLY after the StatefulSet rollout completes, ensuring only one pod performs the operation at a time.
Validation
After applying the changes, verify the fix with the following steps:
- Trigger a rolling restart:
kubectl rollout restart statefulset/postgres
kubectl logs -f postgres-0 | grep "index build (concurrently) completed"
Expected output:
2024-07-12 14:10:45.321 UTC [12345] LOG: index build (concurrently) completed: "orders_pkey"
pg_stat_progress_create_index to confirm the phase reaches done within the new timeout.
SELECT relid::regclass, phase, blocks_total, blocks_done
FROM pg_stat_progress_create_index
WHERE relid = 'orders_pkey'::regclass;
All rows should show phase = 'done' and blocks_done = blocks_total.
kubectl get pod postgres-0 -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
Result should be empty or Completed, not OOMKilled.
Best Practices and Prevention
- Set sensible defaults for
lock_timeout(e.g., 2–5 minutes) in environments where rolling restarts are frequent. - Allocate sufficient
maintenance_work_membased on the largest index size; a rule of thumb is 10 % of the pod’s memory request. - Monitor
pg_stat_progress_create_indexand create alerts whenphasestays inbuildinglonger thanlock_timeout. - Use readiness probes that wait for
pg_isreadyand WAL replay completion before marking the pod as ready. - Serialize maintenance jobs during rollouts using a Kubernetes
Jobor a leader‑election sidecar to avoid concurrent REINDEX on multiple replicas. - Benchmark index rebuilds in a staging environment with realistic PVC latency to tune
maintenance_work_memandmax_parallel_maintenance_workersbefore production deployment.
Related Topic Hub: Data Infrastructure Troubleshooting Hub
FAQ
- Why does
REINDEX CONCURRENTLYsucceed locally but fail after a pod restart?
Because the local instance typically has ample memory and no I/O cold‑start, while the restarted pod experiences PVC warm‑up latency and tighter resource limits, causing the exclusive lock acquisition to exceedlock_timeout. - Can increasing
max_parallel_maintenance_workershelp?
It can reduce total build time, but it also raises CPU usage. Ensure the pod’s CPU request can accommodate the additional parallel workers; otherwise you may hit CPU throttling, which can also delay lock acquisition. - Is it safe to set
lock_timeoutto a very high value (e.g., 30 minutes)?
A high timeout prevents premature aborts but may mask underlying contention. Prefer fixing the root cause (resource limits, long‑running transactions) and keep the timeout to a reasonable bound (5 minutes) to surface genuine deadlocks. - How do I know if PVC I/O is the bottleneck?
During pod startup, runiostat -xinside the container. Ifawaitorsvctmvalues are consistently high (>30 ms) and the index build progress stalls, PVC latency is likely contributing to the timeout. - Should I use
REINDEXinstead ofREINDEX CONCURRENTLYin a StatefulSet?
A plainREINDEXacquires an exclusive lock immediately, causing downtime for the table. In a high‑availability setup,REINDEX CONCURRENTLYis preferred despite its complexity, provided the environment is tuned as described.