惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
The Register - Security
The Register - Security
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The GitHub Blog
The GitHub Blog
博客园 - 司徒正美
罗磊的独立博客
U
Unit 42
S
SegmentFault 最新的问题
Y
Y Combinator Blog
博客园_首页
Hugging Face - Blog
Hugging Face - Blog
J
Java Code Geeks
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
C
Check Point Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
V
Vulnerabilities – Threatpost
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
阮一峰的网络日志
阮一峰的网络日志
The Hacker News
The Hacker News
博客园 - 叶小钗
C
Cybersecurity and Infrastructure Security Agency CISA
Spread Privacy
Spread Privacy
L
LINUX DO - 热门话题
T
The Exploit Database - CXSecurity.com
P
Palo Alto Networks Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
L
Lohrmann on Cybersecurity
A
About on SuperTechFans
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
S
Securelist
A
Arctic Wolf
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Threatpost
Scott Helme
Scott Helme
博客园 - 聂微东
博客园 - 【当耐特】
T
Tenable Blog
I
Intezer
D
DataBreaches.Net
B
Blog RSS Feed
Security Latest
Security Latest
C
Cisco Blogs
T
Tor Project blog
N
Netflix TechBlog - Medium

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Обновление базы за время смены мастера Patroni
iagmalov · 2026-05-18 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение11 мин

Охват и читатели30

Туториал

Меня зовут Ирек Агмалов, я DBA-SRE в Ви.Tech - IT-дочке ВсеИнструменты.ру.

Мы обновляли PostgreSQL в кластере Patroni и хотели переключить приложение на новую версию без смены строки подключения и без долгого простоя.

Для роутинга у нас уже использовались consul-dns и Patroni, поэтому вместо замены DSN мы попробовали временно взять переключение трафика на себя через записи в Consul. В статье покажу, как перевели реплику на PostgreSQL 18, сохранили синхронизацию через логическую репликацию и переключили master и replica так, чтобы потом вернуть кластер в нормальный режим работы.

Ранее мы уже разбирали обновление PostgreSQL через отдельный кластер с последующим переключением приложения. Здесь пошли другим путем: решили сохранить ту же строку подключения и переключать трафик через consul-dns.

Можно обновляться не меняя строку подключения, например выключив patroni на обновляемом кластере и включив его на свежем кластере. Тогда будут затраты времени на включение/выключение Patroni с промежуточным удалением кластера, что может быть “не быстрым”. 

Описание на основе  кластера Patroni с postgresql v.15 из трёх нод, с ограничением загруженности: сервис должен успешно мочь работать на одной ноде БД в плане мощности хоста/виртуальной машины. По итогу привязки к версиям PostgreSQL нет, важны успешные тесты работы приложения на новой версии. 

Исходный кластер PostgreSQL под управлением Patroni + Consul (+consul-dns)

Исходный кластер PostgreSQL под управлением Patroni + Consul (+consul-dns)

Предполагаемые шаги для выполнения обновления:

1) перевести физ.реплику в логическую с обновлением до pg v.18.

2) Переключаем трафик replica через consul-dns 

3) Переключаем трафик master и синхронизируем sequence. 

4) Возвращаем кластер под управление Patroni.

План описывает самый простой вариант, когда переключение производится на одну ноду. Можно выполнить переключение на кластер без патрони на физической репликации с лидером и репликами с различными наборами тегов для consul-dns.

Перевод физической реплики в логическую на новой версии PostgreSQL

Одна физическая реплика переводится в логическую реплику. Обновляется до версии 18 с сохранением логической репликации. В конфигурации (pg_hba) учесть смену хешей паролей на scram-sha256. Оставляется запущенной: она запущена и работает без patroni, только postgresql.
Потребуется проверить, что на текущем мастере wal_level = logical,а таблицы позволяют однозначно идентифицировать строку для логической репликации.

Выполняемые шаги по аналогии можно глянуть в статье , только в данном случае бэкап разворачивать не надо, так как уже есть физическая реплика. Слот логической репликации надо создать до выключения физической реплики в рамках patroni и запуска как независимой ноды. Далее классический pg_upgrade c подключением к созданному ранее и смещённому слоту репликации.

Одна реплика стала логической, версия PostgreSQL на ней обновлена до целевой

Одна реплика стала логической, версия PostgreSQL на ней обновлена до целевой

Переключение трафика реплик на обновлённую ноду через правку consul-dns

Выполняем запросами через curl к локальным consul агентам на всех хостах репликах кластера, то есть все кроме текущего мастера pg v.15.
Проверка данных прописанных в consul-dns на локальной ноде до внесения изменений:

$ curl http://localhost:8500/v1/agent/services | jq .

Добавление replica на будущем мастере на новой версии pg, указать имя кластера и IP адрес будущего мастера, выполнять на будущем мастере:

$ curl --request PUT --data '{"Name": "<pgcluster-name>","ID": "<pgcluster-name>","Address": "<node_IP>","Port": 5432,"Tags": ["replica"],"Meta": {},"EnableTagOverride": true}' http://127.0.0.1:8500/v1/agent/service/register

Удаление replica на репликах на прошлой версии pg v.15, указать имя кластера и имя ноды используемое в данных из consul-dns, выполнять на репликах:

$ curl -X PUT http://localhost:8500/v1/agent/service/deregister/<pgcluster-name>/<node-name>

Повторяем проверку того что получилось в consul-dns после проведённых изменений используя первую команду.

Переключение трафика на мастер на обновлённую ноду через правку consul-dns

Предварительно создать файл ~/.pgpass для возможности подключиться к ноде будущему мастеру на pg v.18 для синхронизации последовательностей/счётчиков. Проверьте подключение к postgres на будущем мастере с учётом данных введённых в .pgpass.

Само переключение выполняем через bash скрипт. Скрипт выполняется на ноде с мастером pg v.15 из под учётной записи postgres. Требуется ввести параметры для локальной базы (pg15) и базы назначения (pg18). 

Как вариант для предварительного тестирования можно закомментировать всё кроме шагов синхронизации счётчиков/последовательностей. Заложено много логирования и в файл и на экран как на этапе до, так и после. Не нужное логирование можно закомментировать. 

Команда nslookup, срабатывая сразу после изменения consul-dns, выдаёт старые данные, так как днс всё же так быстро не обновляется.

switch_master.sh
#!/bin/bash
 
### switch master from previous cluster to new instance
# --- Configuration ---
LOG_FILE=log_switch_master.log
 
# CURRENT LEADER/MASTER # LOCAL INSTANCE
SOURCE_HOST="localhost"
SOURCE_DB="testdb"
SOURCE_HOSTNAME=`hostname -s`
PGCLUSTER_NAME="pgcluster-testdb"
 
# NEXT LEADER/MASTER
DEST_IP="10.243.187.62"
DEST_PORT="5432"
DEST_DB="$SOURCE_DB"
DEST_USER="postgres"
# ---------------------
 
### START LOGGING
echo "$(date '+%Y-%m-%d %H:%M:%S') | START SCRIPT" "$SOURCE_HOSTNAME" "$PGCLUSTER_NAME" "$SOURCE_DB" "$DEST_IP" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | START SCRIPT" "$SOURCE_HOSTNAME" "$PGCLUSTER_NAME" "$SOURCE_DB" "$DEST_IP"
 
### nslookup for master and replica before switch
RESULT=$(nslookup master."$PGCLUSTER_NAME".service.consul  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER| ERROR | $RESULT"
fi
 
RESULT=$(nslookup replica."$PGCLUSTER_NAME".service.consul  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA| ERROR | $RESULT"
fi
 
 
### check logical slot is active
SQL="select slot_name,database,active,catalog_xmin,restart_lsn,confirmed_flush_lsn from pg_replication_slots where slot_name like '%_slot' and slot_type = 'logical' and active = 't';"
RESULT=$(psql -d "$SOURCE_DB" -t -c "$SQL" 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |LOGICAL SLOT|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |LOGICAL SLOT|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |LOGICAL SLOT| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |LOGICAL SLOT| ERROR | $RESULT"
fi
 
### log current state of consul from current_node (pg15) --not remove now - just log current state
# state before changes
RESULT=$(curl http://localhost:8500/v1/agent/services | jq . 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |BEFORE REMOVE MASTER DNS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |BEFORE REMOVE MASTER DNS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |BEFORE REMOVE MASTER DNS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |BEFORE REMOVE MASTER DNS| ERROR | $RESULT"
fi
 
echo "$(date '+%Y-%m-%d %H:%M:%S') | START SWITCH" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | START SWITCH"
 
### enable read_only on previous master
SQL1="ALTER SYSTEM SET default_transaction_read_only TO on;"
SQL2="SELECT pg_reload_conf();"
SQL3="SHOW default_transaction_read_only;"
psql -d "$SOURCE_DB" -c "$SQL1"
psql -d "$SOURCE_DB" -c "$SQL2"
RESULT=$(psql -d "$SOURCE_DB" -t -c "$SQL3" 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |READ ONLY ON|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |READ ONLY ON|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |READ ONLY ON| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |READ ONLY ON| ERROR | $RESULT"
fi
 
### terminate all user/client connections
SQL_TERM="SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE backend_type = 'client backend' AND pid <> pg_backend_pid() AND datname = '$SOURCE_DB';"
RESULT=$(psql -d "$SOURCE_DB" -c "$SQL_TERM"  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS| ERROR | $RESULT"
fi
 
### synchronize sequences from previous master to new
# Import sequences values as SQL commands from the source
SQL_SEQ="SELECT 'SELECT setval(' || quote_literal(schemaname || '.' || sequencename) || ', ' || last_value || ');' FROM pg_sequences;"
SEQUENCE_VAL=$(psql -d "$SOURCE_DB" -Atc "$SQL_SEQ"  2>&1)
RESULT=$SEQUENCE_VAL
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE IMPORT|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE IMPORT|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE IMPORT| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE IMPORT| ERROR | $RESULT"
fi
 
# Export sequences values by executing on the destination
RESULT=$(psql -h "$DEST_IP" -p "$DEST_PORT" -d "$DEST_DB" -U "$DEST_USER" -Atc "$SEQUENCE_VAL" 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE EXPORT|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE EXPORT|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE EXPORT| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SEQUENCE EXPORT| ERROR | $RESULT"
fi
 
### add master teg for new master instance (pg18)
CURL_ADDR=$"http://127.0.0.1:8500/v1/agent/service/register"
CURL_DATA4="{\"Name\": \"$PGCLUSTER_NAME\",\"ID\": \"$PGCLUSTER_NAME\",\"Address\": \"$DEST_IP\",\"Port\": $DEST_PORT,\"Tags\": [\"primary\",\"master\",\"replica\"],\"Meta\": {},\"EnableTagOverride\": true}"
echo "curl --request PUT --data" "$CURL_DATA4" "$CURL_ADDR"
RESULT=$(curl --request PUT --data "$CURL_DATA4" "$CURL_ADDR"  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SET NEW CONSUL-DNS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SET NEW CONSUL-DNS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SET NEW CONSUL-DNS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |SET NEW CONSUL-DNS| ERROR | $RESULT"
fi
 
### remove old master
# removing master, should be without result
RESULT=$(curl -X PUT http://localhost:8500/v1/agent/service/deregister/"$PGCLUSTER_NAME"/"$SOURCE_HOSTNAME" 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |REMOVE MASTER DNS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |REMOVE MASTER DNS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |REMOVE MASTER DNS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |REMOVE MASTER DNS| ERROR | $RESULT"
fi
# state of local consul-dns after master tag remove
RESULT=$(curl http://localhost:8500/v1/agent/services | jq . 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |AFTER REMOVE MASTER DNS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |AFTER REMOVE MASTER DNS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |AFTER REMOVE MASTER DNS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |AFTER REMOVE MASTER DNS| ERROR | $RESULT"
fi
 
### repeat session termination on previous master
RESULT=$(psql -d "$SOURCE_DB" -c "$SQL_TERM"  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |TERMINATE CLIENTS| ERROR | $RESULT"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') | FINISH SWITCH" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | FINISH SWITCH"
 
### additional consul-dns check after switch
# state after changes
RESULT=$(curl http://localhost:8500/v1/agent/services | jq . 2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |CONSUL-DNS AFTER SWITCH|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |CONSUL-DNS AFTER SWITCH|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |CONSUL-DNS AFTER SWITCH| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |CONSUL-DNS AFTER SWITCH| ERROR | $RESULT"
fi
 
### nslookup for master and replica after switch
RESULT=$(nslookup master."$PGCLUSTER_NAME".service.consul  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP MASTER| ERROR | $RESULT"
fi
 
RESULT=$(nslookup replica."$PGCLUSTER_NAME".service.consul  2>&1)
if [ $? -eq 0 ]; then
    CLEAN_RESULT=$(echo "$RESULT" | xargs)
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA|  $CLEAN_RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA|  $CLEAN_RESULT"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA| ERROR | $RESULT" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') |NSLOOKUP REPLICA| ERROR | $RESULT"
fi
 
echo "$(date '+%Y-%m-%d %H:%M:%S') | FINISH SCRIPT" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | FINISH SCRIPT"

После успешного выполнения скрипта новые соединения обращающиеся с использованием consul-dns как к реплике так и к мастеру будут приходить на postgresql v.18.

Переключились на обновлённый PostgreSQL

Переключились на обновлённый PostgreSQL

Так как consul-dns прописан через ноду бывшего мастера (pg15), то там нельзя ни выключать consul агент, ни сам хост. Пока кластер patroni/pg не будет переразвёрнут на pg.18.

Пересоздание кластера patroni с pg15 на pg18

Далее, при пересоздании кластера в patroni, роутинг к мастеру и репликам сохраняется благодаря ручным записям в consul.
Выключаем сервис patroni на всех оставшихся нодах c pg v.15 ($ sudo service patroni stop)

Удаляем кластер patroni, указываем имя для текущего обновляемого кластера

postgres$ patronictl -c /etc/patroni/patroni.yml remove <pgcluster-name>

На ноде мастере pg18 включаем сервис patroni. Если с конфигурацией всё ранее было выполнено корректно, не были внесены кардинально отличные параметры требующие перезапуска, то сервис запуститься не перезапуская инстанс базы, при необходимости перезагрузив конфигурацию.

На нодах с pg15 устанавливаем ПО PostgreSQL 18. Создаём необходимые папки с корректными правами и пользователем, очищаем папку базы pg15 для освобождения места. Запускаем сервис patroni, запускаем перезаливку.

Проверяем корректность работы кластера patroni. Также можно проверить как обрабатываются записи в consul-dns через nslookup:

postgres$ patronictl -c /etc/patroni/patroni.yml topology
$ nslookup replica."$PGCLUSTER_NAME".service.consul
$ nslookup master."$PGCLUSTER_NAME".service.consul

На нодах прошлого и настоящего мастера очищаем добавленные нами в consul-dns записи. Только проверьте, что они не пересекаются с теми, что выставляет Patroni, в примере всё же под нашу конфигурацию.

postgres$ curl -X PUT http://localhost:8500/v1/agent/service/deregister/<pgcluster-name>

По итогу получаем кластер PostgreSQL на обновлённой версии с переключением трафика и минимальным временем недоступности PostgreSQL. Недоступность будет вызвана тем как быстро приложение переключиться на обновлённую днс запись для создания соединения.

Тестирование и итоги

Самое важное, что мы поняли на тестах: в этом сценарии решает не только сама схема переключения, но и  порядок действий в момент смены мастера. Если сначала убрать старую DNS-запись мастера, а новая еще не начала стабильно резолвиться у клиентов, новые попытки подключения могут зависать в поиске ноды больше 120 секунд, в зависимости от настроек клиента.
Обратная сторона когда новый мастер появляется до удаления старого: подключения могут попасть на старый мастер и получить ошибку вида

ERROR:  cannot execute INSERT in a read-only transaction

Мы в своей практике остановились на сценарии в баш скрипте при котором днс запись всегда хоть какая-то доступна, то есть сперва добавляем новую запись и затем удаляем ссылку на старого лидера.

В одном из приложений столкнулись с тем, что оно проверяло “а не в read-only ли база?”, и если нет, то переподключалось. То есть было заложено подключиться именно на реплику. Про такое поведение приложения надо узнавать на тестах ;) Решается созданием полноценной реплики с нужными записями в consul.

Другой момент: трафик с реплики переводится пораньше на обновлённую базу попутно прогревая кэши; если профиль нагрузки лидера и реплики отличается, то озаботьтесь прогревом нового мастера самостоятельно.

И дополнительно бонус: одновременно с обновлением конвертим int на bigint, где на горизонте нескольких лет маячит предел; и стремимся включить data_checksums где это ещё не было сделано.

По итогу: выполнение скрипта переключения на обновлённый pg на нодах без существенной нагрузки и без существенного количества соединений (< 300) укладывалось в пол секунды, что сопоставимо с переключение лидера через Patroni.