← Все статьи

Реплика Postgres 23 часа крутилась в цикле pg_rewind: пересоздавать, а не чинить

После failover реплика CloudNativePG не смогла сделать rewind: её WAL-сегменты были нулевого размера, а каталог данных — в десять раз меньше, чем у primary. 23 часа никто не заметил, потому что метрики оператора не собирались.

В моей домашней лаборатории сторонний проект работал на кластере CloudNativePG из двух инстансов: Postgres 16 на хранилище Longhorn, WAL архивируется в объектное хранилище. Однажды кластер сообщил «2 инстанса, 1 готов». Readiness-проба реплики падала с HTTP 500 уже 23 часа — почти девять тысяч неудач. Primary всё это время был здоров, так что приложение ни разу не легло. Пропала только высокая доступность.

Что сказал pg_rewind

После failover таймлайн сменился с 14 на 15, и понижённый инстанс должен был сделать rewind на новый таймлайн. Не смог:

pg_rewind: servers diverged at WAL location 23/4B000000 on timeline 14
pg_rewind: error: could not read file ".../pg_wal/0000000E000000230000004A": read 0 of 8192

Локальные WAL-сегменты реплики вокруг точки расхождения были нулевого размера, все с отметкой времени момента инцидента. Каталог данных — 407 МБ против 4,5 ГБ на primary. Без WAL rewind завершается с ошибкой, instance manager повторяет попытку, и readiness никогда не становится зелёной. Вечно.

Дело было не в диске

Первым подозреваемым было хранилище: под этим хостом стоит деградирующий SSD. Но том Longhorn был подключён и здоров, обе реплики тома работали, ничего не падало. На этот раз хранилище было ни при чём.

Чинить было нечего

Каталог данных был не устаревшим — его не было. Поэтому решение — выбросить реплику и дать оператору засеять новую с primary:

kubectl -n <ns> delete pvc <replica> --wait=false
kubectl -n <ns> delete pod <replica>

Оператор создал новый том, запустил join-под с pg_basebackup, стартовал инстанс, дошёл до согласованного состояния восстановления и начал стримить. Около четырёх минут на 4,5 ГБ. После этого primary показывал реплику в режиме streaming с нулевым лагом.

Проверка, которая чуть меня не обманула

Чтобы убедиться, что данные на реплике есть, я посмотрел на pg_stat_user_tables.n_live_tup. Там было ноль строк в сумме. На секунду показалось, что база пустая.

Это не так. В этом представлении — локальная статистика планировщика. Autovacuum и analyze на standby не работают, а статистика не реплицируется, так что на совершенно здоровой реплике там законный ноль. Настоящая проверка — выполнить одинаковый count(*) на обеих сторонах и сравнить pg_current_wal_lsn() на primary с pg_last_wal_replay_lsn() на реплике. Обе базы совпали до строки.

Почему 23 часа никто не заметил

  • Метрики оператора не собирались вообще: у кластера стоял enablePodMonitor: false.
  • Включить его оказалось мало. kube-prometheus-stack подхватывает только PodMonitor с собственной меткой release, а CloudNativePG вешает на них только свои метки. PodMonitor создавался и молча игнорировался, пока я не добавил метку через podMonitorMetadata.

Выводы

  • Реплика, которая не может найти свой WAL, — это не ремонт. Пересоздайте её с primary; оператор делает это за минуты.
  • Не судите о standby по n_live_tup. Сравнивайте реальные количества и LSN.
  • Проверяйте, что ваш стек мониторинга действительно выбирает созданный вами PodMonitor. «Создан» и «собирается» — разные вещи.
  • Тихая потеря высокой доступности — это инцидент, даже если пользователи ничего не видят.