ESPN DeportesBarcelona aumenta la venta en casaThe Jerusalem PostMilan's Jewish woman describes what it's like to be Jewish amid rising antisemitism - interviewESPNFollow live: USA locked in tight battle with Spain in FIBA semifinalוואלהשריפה פרצה סמוך ל"ביג פאשן" בירכאBBC NewsCricket: Today at the TestCollider7 Years Later, Stephen King’s Forgotten Horror Anthology Series Deserves a RevivalHabertürkİHA saldırılarına kınamaIl Fatto QuotidianoI vincitori di Venezia 83 – Il Leone d’oro va Woman Unknown. Malkovich e Arcel migliori attori. All’israeliano Naza il premio speciale della Giuriaynet בידורהסרט שמציג עדויות על "הרג מכוון" בעזה זכה בפרס יוקרתי בפסטיבל ונציהANSAVenezia, Il Premio Speciale della Giuria a Naza, Coppa Volpi a Mathilde Arcel e John MalkovichNumeramaBlizzCon 2026 en direct : retour de StarCraft, sortie de Diablo V, WoW Forever, série Netflix x Diablo, The Last Titan…ZDF heute"Bombastisch": Para-Schwimmer beenden EM mit 27 Medaillen
The Daily Newsstand · Free, Always
Saturday, September 12, 2026

Два сервера и половинка третьего: как мы строили HA‑инфраструктуру для Totum

Translate

Когда говорят про High Availability, довольно быстро получается классическая картина: три сервера, балансировщик, кластер PostgreSQL, распределённое хранилище, мониторинг и ещё несколько компонентов, про которые никто не вспоминал, пока всё работало на одной виртуалке.

У нас исходная ситуация была намного прозаичнее: была рабочая система на Totum, которую нужно было перенести в новую инфраструктуру и одновременно избавиться от единственной точки отказа.

При этом хотелось выполнить несколько условий:

1) падение одной основной VM не должно останавливать систему;

2) PostgreSQL должен автоматически переключаться;

3) приложение должно понимать, на какой ноде ему разрешено выполнять активные операции;

4) пользовательские файлы должны оставаться доступными после переключения;

5) переключение не должно требовать ручного изменения DNS;

6) всё должно разворачиваться и обслуживаться через Ansible;

7) покупать третью полноценную машину только ради кворума не хотелось.

Последний пункт в итоге и породил архитектуру, которую между собой я называл «HA для бедных». Спойлер: третья машина всё-таки появилась. Но это маленький witness, который практически ничего не делает с точки зрения бизнес-нагрузки.

Что такое Totum и почему с ним вообще возник этот квест

Totum — self-hosted low-code платформа для создания внутренних web-приложений: ERP, учётных систем, личных кабинетов и других кастомных бизнес-систем. Приложения строятся вокруг таблиц и прикладной логики, а сама платформа может работать со сторонними системами через API и инициировать HTTP-запросы.

В нашем случае это уже была не тестовая установка, а рабочая бизнес-система с PostgreSQL, пользовательскими файлами, интеграциями и большим количеством прикладной логики.

Официальная документация Totum отдельно рекомендует перед промышленной эксплуатацией настраивать PHP-FPM, PostgreSQL, память, диски и индексы под реальную нагрузку. Но до оптимизации ещё нужно было решить более фундаментальную задачу: что произойдёт, если сервер просто умрёт?

Как было

Упрощённо старая архитектура выглядела так:

Internet
|
v
+-------------+
| Server |
|-------------|
| nginx |
| PHP-FPM |
| Totum |
| PostgreSQL |
| files |
+-------------+

Она прекрасна ровно до первого серьёзного сбоя. Падает VM — вместе с ней исчезают приложение, база, файлы, nginx, PHP и фоновые процессы. Можно иметь backup, snapshot и прекрасную инструкцию восстановления, но это всё равно Disaster Recovery, а не High Availability.

Что хотелось получить

Первая очевидная схема:

Load Balancer
/ \
/ \
v v

+---------------+ +---------------+
| Node 1 | | Node 2 |
|---------------| |---------------|
| nginx | | nginx |
| PHP-FPM | | PHP-FPM |
| Totum | | Totum |
| PostgreSQL| | PostgreSQL |
| Patroni | | Patroni |
+---------------+ +---------------+

И вот здесь появляется первая классическая проблема распределённых систем: кто из двух главный?

А кто из двух главный?

Для PostgreSQL мы выбрали Patroni. Но Patroni нужен Distributed Configuration Store, который позволит участникам договориться, кто лидер. В нашем случае таким хранилищем стал etcd.

И для etcd две ноды — плохая идея. При двух участниках кворум равен двум. Потеряли одну машину — оставшаяся не имеет большинства.

Получается парадокс: мы построили HA из двух серверов, но потеря одного сервера мешает кластеру нормально принимать решения. Можно поставить третью полноценную VM. Но зачем нам третья машина с такими же CPU/RAM, если приложение на ней работать не будет? Так появился witness.

«Треть сервера», которая спасает две боевые машины

Финальная схема получилась такой:

Internet
|
v
+-------------------+
| L7 Load Balancer |
+-------------------+
/ \
/ \
v v

+---------------+ +---------------+
| Node A | | Node B |
|---------------| |---------------|
| nginx | | nginx |
| PHP-FPM | | PHP-FPM |
| Totum | | Totum |
| PostgreSQL | | PostgreSQL |
| Patroni | | Patroni |
| etcd | | etcd |
+---------------+ +---------------+
\ /
\ /
v v
+---------+
| Witness |
|---------|
| etcd |
+---------+

Важно: witness не является PostgreSQL-репликой. На нём нет копии production-базы и нет Totum. Его задача значительно проще — быть третьим голосом в etcd.

etcd-1 — Node A
etcd-2 — Node B
etcd-3 — Witness

Если Node A исчезает, Node B + Witness дают 2 голоса из 3. Если исчезает Node B — кворум снова сохраняется. А маленькая VM обходится заметно дешевле полноценной третьей application/database-ноды.

Вот это я и называю «HA для бедных». На самом деле правильнее назвать это HA с отдельным quorum witness.

PostgreSQL: Patroni и asynchronous replication

Patroni
|
+------+------+
| |
v v

PostgreSQL PostgreSQL
PRIMARY ---> REPLICA
WAL

База работает на двух основных серверах, репликация асинхронная. У такого решения есть очевидный компромисс: при аварийной потере primary теоретически можно потерять небольшой объём последних транзакций, которые ещё не успели попасть на replica.

Синхронная репликация уменьшила бы этот риск, но увеличила бы зависимость latency записи от второй ноды. Для нашей задачи был выбран asynchronous-вариант.

Но переключить PostgreSQL недостаточно

Допустим, Patroni прекрасно сделал failover: Node A был PRIMARY, упал, Node B стал PRIMARY. С базой всё хорошо. А что делает приложение?

Если nginx на обеих машинах принимает запросы, можно случайно получить две одновременно работающие application-ноды, каждая из которых считает себя активной. Для некоторых stateless-приложений это нормально. Для нашей конфигурации Totum — нет.

Node A = ACTIVE
Node B = PASSIVE

или наоборот.

Но никогда ACTIVE + ACTIVE.

Как мы связали роль PostgreSQL с ролью Totum

На обеих машинах работает небольшой role-check механизм. Он периодически спрашивает локальный Patroni, является ли текущая нода primary.

if patroni_is_primary; then
touch /var/lib/totum-ha/active
else
rm -f /var/lib/totum-ha/active
fi

На практике вокруг этого есть дополнительная логика, но сама идея именно такая: PostgreSQL leader становится источником истины для application-role.

PostgreSQL leader
|
v
Totum ACTIVE

Как балансировщик понимает, куда отправлять пользователей

Внешний L7-балансировщик знает обе Totum-ноды, но health-check смотрит специальный endpoint:

GET /ha-health

ACTIVE отвечает HTTP 200, PASSIVE — HTTP 503.

ALB
|
GET /ha-health
/ \
/ \
200 503
| |
v X
ACTIVE PASSIVE

Если Patroni переключает primary, role-check меняет application-role, health-check видит новую картину и начинает отправлять пользовательский traffic на новую ноду. DNS при этом менять не надо.

А что делать с файлами

С базой всё относительно стандартно: PostgreSQL replication. Но у Totum есть ещё приложение и файлы. И они сами по себе через Patroni не реплицируются. Для этого использовали lsyncd.

ACTIVE
|
| rsync/lsyncd
v
PASSIVE

Главная проблема здесь — направление синхронизации. Если сегодня ACTIVE Node A, данные идут A → B. После failover направление должно автоматически перевернуться: A ← B. Role-check управляет и этим.

Почему двусторонний rsync — плохая идея

Самая опасная версия такой архитектуры выглядела бы как A ↔ B, где обе машины что-то пишут друг другу. Это прямой путь к конфликтам.

Поэтому мы сделали fencing: принимать изменения должна только PASSIVE-нода. Если сервер становится ACTIVE, входящую синхронизацию нужно ограничить.

Иначе можно получить неприятный сценарий:

Node A был ACTIVE.

A упал.

Node B стал ACTIVE.

На B появились новые файлы.

A вернулся со старым состоянием.

Старые данные поехали поверх новых.

Что именно синхронизировать

Мы синхронизировали application files и необходимые каталоги Totum, но исключили то, что должно быть локальным для каждой машины.

Conf.php
.git/
http/fls/
logs/
tmp/
cache/
totumTmpfiles/
backups/

Конкретный список зависит от установки. Главный принцип: не надо превращать lsyncd в распределённую файловую систему. Синхронизировать стоит только то, что действительно должно переехать вместе с application-role.

Отдельная ловушка: локальный PostgreSQL

Для приложения конфигурацию сделали одинаковой на обеих машинах: Totum всегда обращается к PostgreSQL на той же VM.

'host' => '127.0.0.1',
'port' => 5432,

Не нужно писать отдельную логику выбора DB host. Patroni уже гарантирует, какая локальная PostgreSQL-инстанция сейчас primary.

Ansible: после ручного прототипа всё должно стать кодом

Практически всю инфраструктуру в итоге собрали через Ansible.

[totum_nodes]
totum-prod-1
totum-prod-2

[etcd_cluster]
totum-prod-1
totum-prod-2
etcd-witness

[monitoring]
etcd-witness

Идея была принципиальной: если после аварии для восстановления сервера нужно вспоминать историю shell-команд из Telegram, это ещё не инфраструктура.

roles/
├── etcd/
├── patroni/
├── postgresql/
├── totum/
├── ha/
├── lsyncd/
└── monitoring/

Не запускайте весь playbook просто потому, что можете

На production находилась конкретная версия Totum, а application-role в Ansible могла содержать другую Git-версию. Поэтому команда вроде ansible-playbook site.yml могла неожиданно затронуть не только инфраструктуру, но и application code.

После этого большой site.yml перестал восприниматься как кнопка «починить всё». Изменения применяли отдельными ролями или узкими playbook'ами.

Миграция production

Самая неприятная часть любой HA-архитектуры — не построить её с нуля, а перетащить туда существующий production.

Сначала подняли новую инфраструктуру параллельно старой и проверили:

etcd quorum OK
Patroni leader OK
replication OK
Totum ACTIVE OK
Totum PASSIVE OK
health checks OK
lsyncd OK
failover OK

После этого перенесли production-базу и файлы. Старый сервер некоторое время оставался rollback-вариантом. И только после проверок переключили пользовательский traffic на новый Load Balancer.

Очень полезный тест: сломать сервер самому

HA нельзя проверить командой systemctl status patroni. Нужно реально потерять ноду и убедиться, что система восстанавливается в ожидаемое состояние.

До:
Node A = ACTIVE
Node B = PASSIVE

Выключаем A.

Ожидаем:
Node A = DOWN
Node B = PostgreSQL PRIMARY
Node B = Totum ACTIVE
Load Balancer -> Node B

Возвращаем A:
Node A = PostgreSQL REPLICA
Node A = Totum PASSIVE
Node B остаётся ACTIVE

Потом тест повторяется в обратную сторону. Только после таких проверок можно говорить, что failover существует не только на архитектурной диаграмме.

Всё заработало. А потом Totum начал тормозить

После миграции возникла отдельная проблема. Снаружи всё выглядело прекрасно: CPU почти свободен, RAM хватает, диск не забит, PostgreSQL replication в норме, load average небольшой, login page открывается быстро.

Но пользователи периодически говорили: «Totum умер». Интерфейс на несколько десятков секунд практически переставал отвечать.

CPU = 5%

не означает:

application = healthy

PHP-FPM оказался первой половиной проблемы

В production pool было:

pm.max_children = 14

Во время деградации процессы PHP-FPM доходили до лимита. В slowlog одновременно появлялись длительные внешние HTTP-запросы, long polling, проверки изменений таблиц и уведомлений.

PHP worker
|
+---- curl() ------> external API
|
+---- waits 20 sec

Для FPM такой worker всё это время недоступен другим запросам. Если заняты все 14, новые запросы ждут, и UI выглядит так, будто умер целиком.

Временный slowlog PHP-FPM

slowlog = /var/log/php-fpm-totum-slow.log
request_slowlog_timeout = 2s
request_slowlog_trace_depth = 50

После этого стало видно, где именно зависают worker'ы: внешние curl-вызовы через getFromScript, long polling и проверки уведомлений. Так мы получили уже не ощущение пользователя «что-то медленно», а конкретный stack trace.

А потом мы начали измерять curl

Одного slowlog оказалось мало. Мы временно добавили instrumentation вокруг внешних HTTP-вызовов и логировали DNS, CONNECT, TLS, TTFB, TOTAL и HTTP code — без query string, cookies и токенов.

connect = ~0.010 s
TTFB = 76.4 s
total = 76.4 s

connect = ~0.009 s
TTFB = 15.6 s
total = 15.7 s

То есть соединение устанавливалось практически мгновенно. Задержка происходила после подключения — пока внешний application backend готовил ответ.

Почему увеличение PHP-FPM всё-таки помогло

Внешний API от этого быстрее не стал. Но мы изменили pool:

pm.max_children = 40
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 40

После этого эффект был практически мгновенным: отдельный медленный запрос по-прежнему мог выполняться долго, но весь Totum больше не ложился вместе с ним. Это принципиальная разница между «мы ускорили запрос» и «мы локализовали влияние медленного запроса».

Не верьте ps, включите FPM status

На одном из этапов мы чуть не сделали неправильный вывод: посчитали процессы PHP-FPM и приняли количество процессов за количество занятых worker'ов. В dynamic mode это неверно: FPM специально держит idle workers.

Поэтому включили штатный status endpoint:

pm.status_path = /fpm-status

И получили реальные показатели:

idle processes: 27
active processes: 3
total processes: 30
max active processes: 10
max children reached: 0
slow requests: 45

То есть total=30 не означает загрузку 75%. Реальная текущая нагрузка в этом срезе была 3/40 = 7,5%, а максимум после restart — 10/40 = 25%. После изменения pool перестал быть bottleneck.

Раз уж метрики есть — отправляем их в Prometheus

На серверах уже был node_exporter с включённым textfile collector. Поэтому отдельный exporter для PHP-FPM оказался не нужен.

Небольшой скрипт раз в 15 секунд забирает /fpm-status и пишет метрики в файл textfile collector:

/var/lib/prometheus/node-exporter/totum_fpm.prom

totum_php_fpm_active 3
totum_php_fpm_idle 27
totum_php_fpm_total 30
totum_php_fpm_max_children 40
totum_php_fpm_max_active 10
totum_php_fpm_listen_queue 0
totum_php_fpm_slow_requests 45

Grafana

В Grafana добавили три ключевых графика.

Active workers

totum_php_fpm_active

Использование pool

100 *
totum_php_fpm_active
/
totum_php_fpm_max_children

Slow requests за пять минут

increase(totum_php_fpm_slow_requests[5m])

Теперь при следующей жалобе «Totum тормозит» вместо SSH + ps + гаданий достаточно открыть dashboard. Если FPM utilization близок к 100% и queue > 0 — смотрим pool. Если utilization низкий и queue = 0 — ищем конкретный slow request, SQL или внешний сервис.

Что получилось в итоге

Internet
|
v
+----------------+
| Load Balancer |
+----------------+
/ \
/ \
v v

+----------------+ +----------------+
| Node A | | Node B |
|----------------| |----------------|
| nginx | | nginx |
| PHP-FPM | | PHP-FPM |
| Totum | | Totum |
| PostgreSQL | | PostgreSQL |
| Patroni | | Patroni |
| etcd | | etcd |
| node_exporter | | node_exporter |
+----------------+ +----------------+
\ /
\ /
\ /
+-------------+
| Witness |
|-------------|
| etcd |
| Prometheus |
| Grafana |
+-------------+

PostgreSQL:
PRIMARY ---> REPLICA

Files:
ACTIVE ---> PASSIVE

Application:
PRIMARY DB == ACTIVE Totum

Load Balancer:
200 -> sends traffic
503 -> removes backend

Что я вынес из этого проекта

Для кворума не обязательно покупать третью такую же машину. Если две основные ноды достаточно мощные, третий etcd member может жить на маленьком witness. Главное понимать, что это не третья data replica и не запасной application server.

HA базы ещё не означает HA приложения. Можно идеально переключить PostgreSQL и при этом получить split-brain на application layer.

Файлы сложнее базы. PostgreSQL replication решает PostgreSQL. Всё остальное приходится проектировать отдельно.

Low CPU ничего не говорит о latency приложения. Worker, который десятки секунд ждёт HTTP response, почти не использует CPU, но всё равно занимает слот FPM.

Наблюдаемость надо строить до инцидента, а не во время него. Следующую подобную систему стоит сразу запускать с active/idle workers, queue, slow requests, HTTP latency, DB latency и replication lag.

Не оптимизируйте приложение одним увеличением ресурсов. 14 → 40 workers решило systemic failure, но не сделало внешний API быстрее. Это был правильный mitigation, а не магическое лечение всей производительности.

Эксплуатация должна быть простой. Если для обычного reboot каждый раз нужен автор архитектуры, работа ещё не закончена.

Вместо заключения

Мы начинали с задачи «перенести Totum на два сервера». Закончили системой, в которой есть две application/database-ноды, 3-member etcd quorum, Patroni, automatic failover, application fencing, L7 health checks, file replication, Ansible, Prometheus, Grafana и PHP-FPM monitoring.

И маленький witness, который большую часть времени почти ничего не делает. Наверное, именно поэтому это моя любимая часть архитектуры: он нужен ровно в тот момент, когда одна из двух дорогих машин перестаёт отвечать. А до этого просто тихо помогает двум взрослым серверам договориться, кто сегодня главный.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.