Отказоустойчивый SaaS на PostgreSQL: шарды, реплики и тихие миграции

AnnaBright

New member
Приветствую всех. Хочу поделиться опытом построения отказоустойчивого SaaS на PostgreSQL. За последние годы мы прошли путь от одной виртуалки с базой до кластера с шардированием, репликами в трёх зонах доступности и миграциями, которых пользователи просто не замечают. Проект — B2B-платформа с биллингом, поэтому каждый час простоя стоил нам и денег, и репутации. Ниже — то, что реально сработало, и то, на чём мы набили шишки.

Начну с репликации, потому что именно она даёт первую девятку доступности почти бесплатно. Мы используем потоковую репликацию с синхронным подтверждением на уровне quorum: одна синхронная реплика в соседней зоне доступности плюс асинхронные для чтения и аналитики. Важный момент — не держать синхронную реплику в той же стойке, что мастер, иначе толку ноль. Управление переключением взяли на себя Patroni с внешним хранилищем конфигурации, а автоматический failover настроили с таймаутами, которые мы долго подбирали: слишком агрессивные настройки приводили к лишним переключениям во время сетевых микросбоев, слишком ленивые — к длительному простою. Отдельно скажу про split-brain: без внешнего арбитра и правильных watchdog-настроек вы однажды получите два мастера и очень грустное утро.

Шардирование мы включали не от хорошей жизни, а когда вертикальное масштабирование и партиционирование перестали спасать. Честно скажу: я рассматривал готовые решения вроде распределённого расширения от Citus, но в итоге мы пошли по пути шардирования на уровне приложения по идентификатору арендатора. Причина простая — предсказуемость и контроль над тем, какие запросы куда идут. Главная боль здесь не сам шардинг, а горячие шарды: один крупный клиент легко начинает занимать половину ресурсов своего шарда. Мы решили это возможностью вынести такого тенанта на выделенный узел, и это оказалось самой полезной функцией всей схемы. Кросс-шардовые аналитические запросы мы сознательно не поддерживаем в онлайне: они уходят в отдельное хранилище через регулярную выгрузку, иначе транзакции начинают ползать.

Теперь про zero-downtime миграции, где, по моему опыту, ломается больше всего продакшенов. Золотое правило — схема меняется в два этапа: сначала расширение, потом сжатие. Новую колонку добавляем без значения по умолчанию, код учим писать в оба места, данные переливаем батчами с throttling, и только потом удаляем старое. Индексы создаём конкурентно, ограничения добавляем как недействительные, а потом валидируем их отдельной командой. И обязательно ставим себе страховку в виде малого таймаута на блокировки — иначе один случайный долгий запрос превращает безобидный ALTER в остановку всего сервиса. Мы прошли через такое один раз и больше не хотим.

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

Отдельно хочу предупредить всех, кто читает это и думает шардировать сразу: не надо. Шардирование увеличивает операционную сложность в разы, и цена инженерного времени почти всегда перевешивает стоимость более крупной машины. Сначала исчерпайте скучные вещи: реплики для чтения, партиционирование больших таблиц, нормальные индексы, кэш, вынос тяжёлой аналитики, оптимизацию запросов. Мы ускоряли систему в четыре раза, просто перестав делать N+1 запросы и добавив пару правильных составных индексов. Шардирование — это инструмент, когда у вас уже физически не влезают данные или запись, а не когда хочется красивой архитектуры на слайдах.

Если сформулировать итог, мои рекомендации такие. Стройте от простого к сложному и добавляйте каждый новый уровень только под конкретную боль, которую вы уже измерили. Автоматизируйте переключения, но держите ручной план на случай, когда автоматика ошибётся. Любую миграцию репетируйте на копии продакшен-данных, потому что объём и распределение данных меняют всё. И не жалейте времени на observability: без метрик вы не отличите отказоустойчивость от удачи. А каким был ваш самый болезненный опыт с PostgreSQL в продакшене — переключение мастера, разросшийся шард или миграция, которая положила сервис? Делитесь историями, вместе мы точно станем спокойнее спать!
 
Тема огонь, особенно про тихие миграции. Мы у себя на SaaS тоже прошли через шардирование по tenant_id: стало легче с масштабированием, но добавилось боли с кросс-шардовыми запросами и репликацией. Сейчас главный страх — не сами миграции, а момент, когда под нагрузкой нужно переключить чтение на новую схему или реплику, чтобы никто не заметил.

Интересно, как вы решаете проблему долгих транзакций и лага реплик при миграциях? Используете feature flags, dual-write со сверкой или что-то вроде pgroll? И как поступаете, если часть шардов уже обновилась, а часть ещё нет — есть ли у вас автоматический rollback на этот случай?
 
Тема прямо в сердечко — сами прошли через переезд с одной реплики на шардинг, и главный ад был не в шардах, а именно в миграциях. «Тихие» миграции звучат красиво ровно до момента, когда старый код ещё пишет в старую схему, а новый уже читает из новой. Спасает только дисциплина: expand/contract, фича-флаги и железное правило «сегодня ничего не дропаем».

Интересно, как у докладчика решён вопрос консистентности между шардами при кросс-шардовых операциях — обошлись без распределённых транзакций или всё-таки где-то двухфазный коммит? И ещё про реплики: вы их держите синхронными ради строгого чтения или сознательно ушли в асинхрон с eventual consistency?
 
Назад
Вверх