Отказоустойчивость без Kubernetes
Как построить систему, переживающую отказ сервера, не поднимая Kubernetes.
Kubernetes решает задачу отказоустойчивости через оркестрацию контейнеров — но приносит с собой control plane, etcd, CNI, свою модель сети и хранилища, и собственный класс аварий. Для трёх-пяти серверов это часто дороже проблемы, которую решает.
mitdev предлагает другой путь: репликация на уровне служб + watchdog на каждом узле + единая точка входа. Никакого оркестратора, никакого центрального координатора. Каждый узел сам понимает, что делать, когда сосед умер.
Содержание
- Когда это ваш путь, а когда нужен Kubernetes
- Общая архитектура
- Предпосылка: приватная сеть
- PostgreSQL: репликация с автоматическим failover
- Единая строка подключения: VIP или локальный прокси
- Redis: реплики и Sentinel
- MongoDB: replica set
- RabbitMQ: кластер с quorum-очередями
- MinIO: распределённое хранилище
- Слой приложения: несколько инстансов за nginx
- Полный пример: три сервера
- Сценарии отказов и что происходит
- Учения: проверьте, что оно работает
- Чего эта схема не даёт
- Чек-лист перед продакшеном
Когда это ваш путь, а когда нужен Kubernetes
| Признак | Без Kubernetes | Kubernetes |
|---|---|---|
| Серверов | 2–6 | 6+ |
| Приложений | 1–5 | 10+ |
| Команда | 1–3 инженера | есть выделенный ops |
| Деплой | несколько раз в неделю | десятки раз в день |
| Состав меняется | редко | постоянно, автоскейлинг |
| Нужен ли self-service для команд | нет | да |
| Стоимость простоя обучения | высокая | окупается |
Практическое правило: если вы можете перечислить все свои серверы по именам — Kubernetes вам, скорее всего, не нужен.
Схема из этого документа даёт RTO (время восстановления) порядка 30–40 секунд для БД и секунды для приложений. Этого достаточно для подавляющего большинства бизнес-задач. Kubernetes не даёт принципиально лучше — он даёт лучше при десятках сервисов.
Если решите, что нужен, — KUBERNETES.md описывает, как подготовить проект и развернуть HA-кластер k3s. Заметьте: описанные ниже кластеры БД остаются актуальными и в k8s — базы данных в кластере обычно всё равно живут снаружи.
Общая архитектура
Целевая картина на трёх серверах:
пользователи
│
┌─────────┴─────────┐
│ DNS round-robin │ (или Anycast/LB провайдера)
└─────────┬─────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
╔════╧═════╗ ╔════╧═════╗ ╔════╧═════╗
║ srv-1 ║ ║ srv-2 ║ ║ srv-3 ║
║ ║ ║ ║ ║ ║
║ nginx ║ ║ nginx ║ ║ nginx ║
║ app ×2 ║ ║ app ×2 ║ ║ app ×2 ║
║ ║ ║ ║ ║ ║
║ PG primary◄─── WAL ─╫─►PG replica◄── WAL ─╫─►PG replica
║ Redis mstr◄─────────╫─►Redis rplc◄────────╫─►Redis rplc
║ Sentinel ║ ║ Sentinel ║ ║ Sentinel ║
║ watchdog ║ ║ watchdog ║ ║ watchdog ║
╚════╤═════╝ ╚════╤═════╝ ╚════╤═════╝
│ │ │
└──── WireGuard mesh: 10.100.0.1/.2/.3 ─────┘
│
VIP 10.100.0.100
(keepalived, следует за primary)
Три независимых механизма отказоустойчивости, каждый со своим доменом:
- Репликация данных — PostgreSQL WAL, Redis replication, MongoDB oplog. Отвечает за то, чтобы данные существовали больше чем в одном месте.
- Автоматический failover — watchdog PostgreSQL, Sentinel, выборы в replica set. Отвечает за то, чтобы новая главная реплика появилась без человека.
- Единая точка входа — VIP через keepalived или локальный HAProxy. Отвечает за то, чтобы приложению не пришлось узнавать о смене primary.
Все три обязательны. Репликация без failover — это бэкап, а не HA. Failover без единой точки входа — это failover, о котором приложение не узнает.
Предпосылка: приватная сеть
Кластеры БД обязаны общаться по приватным адресам. Соберите mesh до любых кластерных команд — см. MESH.md.
# srv-1
sudo mitdev net create # → 10.100.0.1
# srv-2, srv-3
sudo mitdev net join <токен> # → 10.100.0.2, 10.100.0.3
# и add-peer на остальных узлах
Дальше везде используются адреса 10.100.0.x. Если вы вводите публичные IP —
вы открываете репликацию в интернет; не делайте так.
PostgreSQL: репликация с автоматическим failover
Что настраивается
Модуль pgcluster (install.sh → PostgreSQL-кластер) поднимает потоковую
репликацию: primary пишет WAL, реплики его читают и применяют. Отставание
обычно единицы миллисекунд в одной сети и десятки — между дата-центрами.
- primary: роль репликации, WAL-параметры (
wal_level=replica,max_wal_senders), слоты репликации, записи вpg_hba.confи правила файрвола по IP каждой реплики; - replica:
pg_basebackupсо слотом, standby-режим,primary_conninfoсsslmode=require.
На каждом узле ставится mitdev-pg-watchdog — та часть, которая делает эту
конструкцию отказоустойчивой, а не просто реплицированной.
Логика watchdog
Watchdog работает от имени postgres и имеет ровно одно право root через
sudoers: systemctl (stop|start|restart) postgresql*. Ничего больше.
На реплике каждые CHECK_INTERVAL (5 с) он пробует primary двумя
способами: pg_isready и сырой TCP-коннект. Оба должны провалиться, чтобы
попытка засчиталась как отказ — это отсекает ложные срабатывания от временной
загрузки БД.
После FAIL_THRESHOLD (6) подряд неудач и паузы PROMOTE_DELAY:
- если другой пир уже объявил себя primary (через role-endpoint на порту
- — эта реплика просто перецепляется к нему: меняется
primary_conninfo, копирование данных не нужно;
- — эта реплика просто перецепляется к нему: меняется
- иначе, при
AUTO_PROMOTE=yes— эта реплика повышается до primary.
На primary watchdog занимается самоограждением (fencing). Он следит за
role-endpoint'ами пиров. Если другой узел сообщает, что он primary, и его
timeline выше — значит, этот узел был недоступен, реплику повысили, и он
устарел. Тогда он сам себя понижает: останавливается, делает pg_rewind от
победителя, стартует его репликой.
Именно поэтому split-brain не случается: узел с более старым timeline никогда не остаётся primary. Ожившый после сбоя старый primary возвращается в кластер репликой — без единой ручной команды.
t=0 srv-1 primary (timeline 1), srv-2 и srv-3 реплики
t=1 srv-1 умирает (питание/сеть/kernel panic)
t=6 srv-2: 6 неудачных проб подряд, никто не объявил себя primary
t=6 srv-2 → promote, timeline 2, поднимает role-endpoint
t=7 srv-3 видит: srv-2 primary → refollow srv-2 (без копирования данных)
t=45 srv-1 оживает, стартует как primary timeline 1
t=50 srv-1 видит: srv-2 primary с timeline 2 > 1 → self-demote
stop → pg_rewind от srv-2 → start как реплика srv-2
t=55 кластер снова здоров: srv-2 primary, srv-1 и srv-3 реплики
Полный цикл — меньше минуты, ноль ручных действий.
Управление
sudo mitdev pg status # роль, базы, состояние репликации, отставание
sudo mitdev pg replication # детально: слоты, WAL-позиции, лаг по реплике
sudo mitdev pg watchdog status # состояние службы + последние события
Настройка порогов (перезапускает службу автоматически):
sudo mitdev pg watchdog set CHECK_INTERVAL 5 # период проб, с
sudo mitdev pg watchdog set FAIL_THRESHOLD 6 # подряд неудач до реакции
sudo mitdev pg watchdog set PROMOTE_DELAY 0 # доп. пауза перед promote, с
sudo mitdev pg watchdog set AUTO_PROMOTE yes # yes|no
sudo mitdev pg watchdog set PEERS "10.100.0.2 10.100.0.3"
CHECK_INTERVAL × FAIL_THRESHOLD + PROMOTE_DELAY = время до повышения.
По умолчанию 5 × 6 + 0 = 30 секунд.
Как выбирать:
- Агрессивнее (
3 × 4= 12 с) — если сеть между узлами стабильна и предсказуема (одна стойка, один ДЦ). Риск: сетевая икота на 15 секунд вызовет ненужный failover. - Консервативнее (
5 × 12= 60 с) — если узлы в разных ДЦ и вы видели всплески задержки. Риск: минута недоступности записи. AUTO_PROMOTE=noна одной из реплик — если она заведомо слабее по железу и не должна становиться primary. Она продолжит перецепляться к новому primary, но сама повышаться не будет.
Ручные операции
sudo mitdev pg add-replica 10.100.0.3 # на primary: разрешить новую реплику
sudo mitdev pg remove-replica 10.100.0.3 # на primary: убрать (слот+pg_hba+ufw)
sudo mitdev pg allow 10.100.0.1 # разрешить серверу приложений
sudo mitdev pg promote # ручное повышение реплики
sudo mitdev pg follow 10.100.0.2 # плановое перецепление к новому primary
sudo mitdev pg import backup.dump # залить бекап (формат — автоопределение)
promote и follow нужны для плановых операций: обслуживание primary,
переезд между ДЦ. При авариях watchdog делает это сам — не мешайте ему
параллельными ручными командами.
Единая строка подключения: VIP или локальный прокси
После failover primary — уже другой сервер. Приложение об этом не знает. Есть два способа не переучивать приложение.
Вариант A: VIP через keepalived — узлы в одной L2-сети
Модуль pgvip ставит keepalived на каждый узел кластера с одинаковым
конфигом. Скрипт проверки отвечает на вопрос «я сейчас primary?»; узел, который
отвечает «да», получает вес VRRP и забирает виртуальный IP.
sudo mitdev pg vip # кто держит VIP прямо сейчас
Приложение подключается по одному адресу навсегда:
postgresql://app:[email protected]:5432/app?sslmode=require
Используется unicast VRRP — работает в облаках, где мультикаст отфильтрован.
В UFW открывается IP-протокол 112 (VRRP) через before.rules.
Ограничение принципиальное: VIP требует общего L2-домена. Узлы в разных дата-центрах через WireGuard L2 не образуют — ARP не ходит через туннель. Для них — вариант B.
Вариант B: локальный HAProxy — узлы где угодно
На сервере приложений поднимается HAProxy, который знает все узлы кластера
и через role-endpoint (порт 8008) определяет, кто сейчас primary. Приложение
подключается к 127.0.0.1.
# на сервере приложений
sudo mitdev pg proxy 10.100.0.1 10.100.0.2 10.100.0.3
postgresql://app:[email protected]:5432/app?sslmode=require
Если порт 5432 на этом сервере занят (локальный PostgreSQL):
sudo PG_PROXY_PORT=6432 mitdev pg proxy 10.100.0.1 10.100.0.2 10.100.0.3
Не забудьте разрешить серверу приложений доступ на каждом узле БД:
sudo mitdev pg allow <ip-сервера-приложений>
HAProxy проверяет конфигурацию перед перезапуском (haproxy -c); при ошибке
изменения откатываются на бэкап.
Как выбрать
VIP (pgvip) |
Прокси (pg proxy) |
|
|---|---|---|
| Узлы БД в одной L2-сети | ✔ | ✔ |
| Узлы в разных ДЦ | ✘ | ✔ |
| Точка отказа | нет (VRRP) | HAProxy на сервере приложений |
| Переключение | 3–5 с (VRRP) | ~2 с (health check) |
| Настройка | на каждом узле БД | на каждом сервере приложений |
| Приложение видит | 10.100.0.100 |
127.0.0.1 |
Если серверов приложений много, а узлов БД три — VIP настраивается один раз. Если приложение и БД живут на одних и тех же серверах в разных ДЦ — прокси.
Redis: реплики и Sentinel
Sentinel — это кворумный наблюдатель. Три Sentinel на трёх узлах голосуют, жив ли master; при согласии большинства повышают реплику.
# srv-1 — инициализация
sudo mitdev redis cluster-init my-cache 2
# ^имя ^кворум
sudo mitdev redis token
# srv-2, srv-3
sudo mitdev redis join <токен>
Кворум — сколько Sentinel должны согласиться, что master мёртв. Для трёх
узлов ставьте 2. Правило: кворум = N/2 + 1. Кворум 1 при трёх узлах
означает, что один изолированный Sentinel может устроить failover — split-brain
гарантирован.
Нечётное число узлов обязательно. Два Sentinel с кворумом 2 не переживут отказ ни одного: оставшийся не наберёт кворум. Два узла — это не HA.
sudo mitdev redis status # роль, реплики, состояние Sentinel
sudo mitdev redis add-replica 10.100.0.4
sudo mitdev redis promote # ручное повышение
Приложение должно подключаться через Sentinel, а не напрямую к master — иначе оно не узнает о смене. Все нормальные клиенты это умеют:
// ioredis
new Redis({
sentinels: [
{ host: '10.100.0.1', port: 26379 },
{ host: '10.100.0.2', port: 26379 },
{ host: '10.100.0.3', port: 26379 },
],
name: 'my-cache', // имя из cluster-init
});
# redis-py
from redis.sentinel import Sentinel
s = Sentinel([('10.100.0.1', 26379), ('10.100.0.2', 26379), ('10.100.0.3', 26379)])
r = s.master_for('my-cache')
Redis-репликация асинхронная. При failover теряются записи, не успевшие дойти до реплики. Для кэша это нормально. Для очереди задач или счётчика платежей — нет; используйте PostgreSQL или RabbitMQ с quorum-очередями.
MongoDB: replica set
Встроенный механизм MongoDB: узлы сами выбирают primary большинством голосов.
# srv-1
sudo mitdev mongo cluster-init rs0
sudo mitdev mongo token
# srv-2, srv-3
sudo mitdev mongo join <токен>
sudo mitdev mongo status
sudo mitdev mongo add-member 10.100.0.4
sudo mitdev mongo remove-member 10.100.0.4
sudo mitdev mongo promote
Как и Sentinel — нечётное число членов. Строка подключения перечисляет всех:
mongodb://user:[email protected]:27017,10.100.0.2:27017,10.100.0.3:27017/db?replicaSet=rs0
Драйвер сам находит primary и переключается при выборах. Для гарантий записи
используйте writeConcern: { w: "majority" } — иначе подтверждённая запись
может исчезнуть при failover.
RabbitMQ: кластер с quorum-очередями
# srv-1
sudo mitdev rabbitmq cluster-init
sudo mitdev rabbitmq token
# srv-2, srv-3
sudo mitdev rabbitmq join <токен>
Узлы объединяются общим Erlang cookie. mitdev настраивает quorum-очереди
(на базе Raft) — в отличие от классических зеркальных, они не теряют сообщения
при разделении сети и не требуют ручной настройки политик зеркалирования.
sudo mitdev rabbitmq status
sudo mitdev rabbitmq forget srv-2 # исключить мёртвый узел из кластера
Очередь должна быть объявлена как quorum на стороне приложения:
await channel.assertQueue('jobs', {
durable: true,
arguments: { 'x-queue-type': 'quorum' },
});
Классическая (не-quorum) очередь на кластере переживёт отказ узла только если она на нём не жила. Это самая частая ошибка: кластер собран, а очереди классические — и половина сообщений исчезает вместе с узлом.
Публикуйте с publisher confirms, потребляйте с manual ack. Без этого
надёжность очереди не спасёт: сообщение потеряется между брокером и вашим кодом.
MinIO: распределённое хранилище
MinIO в распределённом режиме использует erasure coding: объект разрезается на блоки данных и чётности, распределённые по дискам. Кластер переживает отказ до половины дисков без потери данных.
# на КАЖДОМ узле указываются ВСЕ адреса, включая себя
sudo mitdev minio cluster-init 10.100.0.1 10.100.0.2 10.100.0.3 10.100.0.4
Четыре узла — практический минимум. На меньшем числе erasure coding не даёт защиты, ради которой затевался. Состав кластера фиксируется при старте и не меняется на лету — добавление узла означает пересоздание пула.
sudo mitdev minio status
sudo mitdev minio token
sudo mitdev minio join <токен>
Слой приложения: несколько инстансов за nginx
БД реплицирована, но если приложение живёт на одном сервере — этот сервер и есть точка отказа.
На одном сервере: PM2 cluster mode
Переживает падение процесса, не переживает падение сервера.
mitdev app node myapp https://github.com/org/myapp.git app.example.com
pm2 scale myapp 4 # 4 процесса на ядрах
pm2 save
На нескольких серверах: nginx upstream
На каждом сервере nginx проксирует на локальные и удалённые инстансы:
# /etc/nginx/conf.d/upstream-myapp.conf
upstream myapp {
server 127.0.0.1:3000 max_fails=2 fail_timeout=10s;
server 10.100.0.2:3000 max_fails=2 fail_timeout=10s backup;
server 10.100.0.3:3000 max_fails=2 fail_timeout=10s backup;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
location / {
proxy_pass http://myapp;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_connect_timeout 2s;
}
}
backup означает «использовать, только если локальный лёг». Уберите backup
для честной балансировки по всем трём.
Точка входа снаружи
Остаётся вопрос: куда указывает DNS? Варианты по возрастанию цены:
- DNS round-robin — три A-записи на один домен. Браузеры и большинство HTTP-клиентов сами пробуют следующий адрес при отказе. Дёшево, но TTL и кэширование резолверов дают шлейф ошибок на минуты.
- Failover DNS (Cloudflare, Route53 health checks) — записи снимаются автоматически при отказе. Переключение за 30–60 с.
- Балансировщик провайдера — Hetzner LB, AWS ALB. Секунды, но платно и привязывает к провайдеру.
- Anycast + BGP — если у вас своя AS. Если бы у вас была своя AS, вы бы не читали этот документ.
- Cloudflare app-ingress (
mitdev cf) — туннель на каждый узел + Cloudflare Load Balancer с health-монитором. Узел не выставляет наружу ни одного порта (трафик идёт исходящим соединением к edge), больной узел (в т.ч. при «сером отказе» — жив app, мёртв путь к БД) выводится из ротации за секунды локальным watchdog'ом и со стороны edge монитором. Всё настраивается через API. Подробно — modules/24-cloudflare.md.
Для большинства проектов вариант 2 — правильный компромисс. Вариант 5 берут, когда важно не открывать узлы наружу вовсе и ловить серый отказ, а не только смерть узла.
Полный пример: три сервера
Задача: Node.js-приложение, PostgreSQL с failover, Redis-кэш, всё переживает отказ любого одного сервера.
0. Подготовка
# на каждом из трёх серверов
sudo hostnamectl set-hostname srv-1 # srv-2, srv-3 соответственно
curl -fsSL https://ваш-домен/get | sudo bash
# в инсталляторе: Node.js, Nginx, PM2, Certbot, UFW, PostgreSQL, Redis
1. Приватная сеть
# srv-1
sudo mitdev net create # 10.100.0.0/24, порт 51820 → 10.100.0.1
sudo mitdev net token --raw # скопировать TOKEN
# srv-2
sudo mitdev net join <TOKEN> # → 10.100.0.2, печатает PEER_2
# srv-1
sudo mitdev net add-peer <PEER_2>
# srv-3
sudo mitdev net join <TOKEN> # → 10.100.0.3, печатает PEER_3
# srv-1 и srv-2
sudo mitdev net add-peer <PEER_3>
# проверка на любом узле
sudo mitdev net status # три узла, handshake свежий
2. Кластер PostgreSQL
# srv-1 — primary
sudo ./install.sh # модуль pgcluster → роль primary
# PG_REPLICA_IPS="10.100.0.2 10.100.0.3"
sudo mitdev net bind postgresql private
# srv-2 и srv-3 — реплики
sudo ./install.sh # модуль pgcluster → роль replica
# PG_PRIMARY_HOST=10.100.0.1
sudo mitdev net bind postgresql private
# проверка
sudo mitdev pg status # srv-1: primary, 2 реплики, лаг ~0
sudo mitdev pg replication # слоты активны
sudo mitdev pg watchdog status # служба активна на всех трёх
3. Единая строка подключения
Узлы в одной L2-сети (один ДЦ):
sudo ./install.sh # модуль pgvip на каждом узле
# PG_VIP_ADDRESS=10.100.0.100/24
sudo mitdev pg vip # → VIP держит srv-1
Узлы в разных ДЦ:
# на каждом сервере приложений (здесь — на всех трёх)
sudo mitdev pg proxy 10.100.0.1 10.100.0.2 10.100.0.3
# на каждом узле БД
sudo mitdev pg allow 10.100.0.1
sudo mitdev pg allow 10.100.0.2
sudo mitdev pg allow 10.100.0.3
4. Redis + Sentinel
# srv-1
sudo mitdev redis cluster-init app-cache 2
sudo mitdev redis token # → RTOKEN
sudo mitdev net bind redis private
# srv-2, srv-3
sudo mitdev redis join <RTOKEN>
sudo mitdev net bind redis private
sudo mitdev redis status # master + 2 replica, 3 Sentinel, кворум 2
5. Приложение
# на каждом сервере
mitdev app node myapp https://github.com/org/myapp.git app.example.com
.env приложения (одинаковый на всех трёх):
# VIP-вариант
DATABASE_URL=postgresql://app:[email protected]:5432/app?sslmode=require
# или прокси-вариант
DATABASE_URL=postgresql://app:[email protected]:5432/app?sslmode=require
REDIS_SENTINELS=10.100.0.1:26379,10.100.0.2:26379,10.100.0.3:26379
REDIS_MASTER_NAME=app-cache
6. Балансировка и DNS
Добавьте upstream-конфиг nginx (см. выше) на каждом сервере, затем три A-записи
app.example.com → публичные IP всех трёх, с health-check у DNS-провайдера.
7. Проверка
sudo mitdev doctor --deep # реальные пробы: nginx HTTP, PG SELECT 1, Redis PONG, SSL
Сценарии отказов и что происходит
| Отказ | Обнаружение | Реакция | Простой | Действия человека |
|---|---|---|---|---|
| Процесс приложения упал | PM2, мгновенно | перезапуск | < 1 с | нет |
| Сервер приложений (не primary БД) | nginx max_fails, 2 запроса |
трафик на backup-upstream | секунды | нет |
| Primary PostgreSQL | watchdog, 6 проб × 5 с | реплика → primary; вторая реплика перецепляется; VIP переезжает | ~30–35 с | нет |
| Реплика PostgreSQL | primary видит разрыв слота | ничего, кластер работает | 0 | вернуть узел |
| Redis master | Sentinel, кворум 2 из 3 | реплика → master | ~10–15 с | нет |
| Один узел MongoDB | выборы replica set | новый primary | ~10 с | нет |
| Один узел RabbitMQ | Raft в quorum-очереди | лидерство переезжает | секунды | rabbitmq forget если узел мёртв навсегда |
| Один диск MinIO (из 4+) | erasure coding | чтение/запись продолжаются | 0 | заменить диск |
| Разрыв WireGuard-туннеля | net-watchdog, 150 с | reconnect, перерезолв endpoint | до 150 с | нет |
| Оживший старый primary | fencing по timeline | self-demote, pg_rewind, стал репликой |
0 (кластер уже работал) | нет |
| Отказ 2 из 3 узлов | — | кворум потерян, БД read-only | до восстановления | ручное вмешательство |
Последняя строка — принципиальное ограничение любого кворумного кластера. Три узла переживают отказ одного. Хотите переживать два — нужно пять.
Отказ большинства: что делать
Если из трёх узлов остался один, автоматика правильно не повышает его — она не может отличить «остальные умерли» от «я изолирован сетью». Повышение в такой ситуации гарантирует split-brain, когда связь восстановится.
Решение — человеческое, после подтверждения, что узлы действительно мертвы:
sudo mitdev pg watchdog disable # чтобы не мешал
sudo mitdev pg promote # осознанное повышение
# ... восстановить остальные узлы ...
sudo mitdev pg watchdog enable
Учения: проверьте, что оно работает
Отказоустойчивость, которую не проверяли, — это гипотеза. Проверяйте на стенде, воспроизводящем прод, до того, как понадобится.
Учение 1: убить primary PostgreSQL
# srv-1 (текущий primary) — жёсткая эмуляция отказа сервера
sudo systemctl stop postgresql # мягко: только БД
# или
sudo ip link set wg0 down # жёстче: изоляция по сети
Наблюдайте на srv-2:
watch -n1 'sudo mitdev pg status'
journalctl -u mitdev-pg-watchdog -f
Ожидаемо: через ~30 с srv-2 становится primary, srv-3 перецепляется к нему. Приложение переживает: соединения через VIP/прокси переустанавливаются.
Верните srv-1:
sudo ip link set wg0 up
# наблюдайте self-demote: stop → pg_rewind → start как реплика
journalctl -u mitdev-pg-watchdog -f
sudo mitdev pg status # srv-1 теперь реплика
Измерьте: сколько секунд приложение получало ошибки записи. Это ваш реальный
RTO. Если он неприемлем — снижайте FAIL_THRESHOLD.
Учение 2: убить Redis master
sudo systemctl stop redis-server
# на другом узле
redis-cli -p 26379 sentinel masters
Учение 3: разорвать сеть между ДЦ
# на srv-3
sudo ip link set wg0 down
sleep 120
sudo ip link set wg0 up
Проверьте, что srv-3 вернулся сам (mitdev net status на всех узлах) и
PostgreSQL на нём перецепился к актуальному primary.
Учение 4: проверить бэкапы
Failover не спасает от DROP TABLE. Реплика послушно повторит его за
миллисекунды.
sudo mitdev backup --volumes
# на чистом сервере
sudo mitdev restore mitdev-backup-*.tar.gz
Восстановление, которое не проверяли, — тоже гипотеза.
Чего эта схема не даёт
Честный список ограничений.
- Нулевого RPO для Redis и MongoDB. Репликация асинхронная. При failover
теряются последние миллисекунды записей. PostgreSQL можно настроить на
синхронную репликацию (
synchronous_commit=on,synchronous_standby_names), ценой роста задержки записи — mitdev по умолчанию этого не делает. - Защиты от логических ошибок.
DROP TABLE, кривая миграция, баг в коде — реплицируются мгновенно. Нужны бэкапы и PITR. - Автоскейлинга. Число узлов фиксировано. Прирост нагрузки — руками.
- Rolling-обновлений без простоя записи. Обновление primary означает
плановый failover (
pg promoteна реплике) и секунды-минуты недоступности. - Выживания при отказе большинства. Три узла → один отказ. Пять → два.
- Защиты от отказа ДЦ, если все узлы в одном ДЦ. Разносите географически,
и тогда VIP отпадает (только
pg proxy). - Кросс-регионной консистентности. WAL через океан — это сотни миллисекунд лага. Планируйте, что реплика в другом регионе отстаёт.
Чек-лист перед продакшеном
Сеть
- Нечётное число узлов (3 или 5), уникальные hostname
-
mitdev net status— все пиры с свежим handshake на каждом узле -
mitdev net watchdog status— активен везде - Подсеть не пересекается с Docker/k3s/провайдером
Базы данных
-
mitdev pg status— один primary, остальные реплики, лаг < 1 с -
mitdev pg watchdog status— активен на каждом узле -
mitdev net bind postgresql privateвыполнен, 5432 не виден снаружи - Доступ выдан поимённо (
pg allow <ip>), не подсетью - Redis Sentinel: кворум =
N/2 + 1, приложение ходит через Sentinel - RabbitMQ: очереди объявлены как
x-queue-type: quorum - MongoDB:
writeConcern: majorityв приложении
Точка входа
- Приложение использует VIP или локальный прокси, не прямой IP primary
- Строка подключения содержит
sslmode=require - nginx upstream с
max_failsиproxy_next_upstream - DNS с health-check или LB провайдера
Проверено на практике
- Учение «убить primary» проведено, RTO измерен
- Учение «оживить старый primary» проведено, self-demote отработал
-
mitdev restoreпроверен на чистом сервере -
mitdev doctor --deep— без ошибок на всех узлах
Наблюдаемость
- Алерт на
mitdev pg status≠ ожидаемая роль - Алерт на лаг репликации > порога
- Алерт на отсутствие handshake с пиром
-
journalctl -u mitdev-pg-watchdogуезжает в лог-хранилище
Что дальше
- MESH.md — приватная сеть, без которой ничего из этого не работает
- KUBERNETES.md — если решили, что переросли эту схему
- COMMANDS.md — полный справочник команд
- TROUBLESHOOTING.md — разбор конкретных аварий