Сервер ТГИД: устройство, настройка и сопровождение
ТГИД — веб Nuxt, API FastAPI, воркеры Celery с очередями в Redis, базы PostgreSQL/PostGIS, GeoServer и martin за nginx. Один API обслуживает все города, схему базы города доводит сам. Считает воркер движком heatnet в профиле fixed; движок, профиль и ядро — переменные службы воркера. Урок объясняет, как это устроено и почему, чтобы администратор понимал, что проверить при сбое и что нельзя потерять.
Сервер ТГИД: устройство, настройка и сопровождение
Кратко. ТГИД — веб Nuxt, API FastAPI, воркеры Celery с очередями в Redis, базы PostgreSQL/PostGIS, GeoServer и martin за nginx. Один API обслуживает все города, схему базы города доводит сам. Считает воркер движком heatnet в профиле fixed; движок, профиль и ядро — переменные службы воркера. Урок объясняет, как это устроено и почему, чтобы администратор понимал, что проверить при сбое и что нельзя потерять.
Зачем это
Пользователь видит карту и окна. Администратор отвечает за то, что под ними: какой процесс что делает, куда уходит расчёт, где лежат данные и секреты. Без этой картины любой сбой («API недоступен», «расчёт висит», «числа не как вчера») превращается в перебор наугад.
Из чего состоит система
flowchart LR
B["Браузер"] --> N["nginx"]
N --> W["Веб Nuxt"]
N --> A["API FastAPI /city/код/…"]
N --> G["GeoServer и martin (проверка входа через API)"]
A --> P["PostgreSQL: база города, UsersDB, sprav"]
A --> R["Redis"]
R --> C["Воркеры Celery tgid.город"]
C --> H["Процесс расчёта heatnet"]
H --> P
- API хранит всю логику: данные сети, права, журналы, выгрузки. Долгую работу он не делает сам, а кладёт задачу в очередь города.
- Воркер берёт задачу, открывает базу её города и запускает расчёт отдельным процессом. Поэтому новый код heatnet подхватывается без перезапуска, а новые переменные окружения — только после перезапуска воркера.
- GeoServer и martin отдают тайлы мимо API. nginx пускает к ним только после проверки сессии в API
(
/auth/check). Всё, что опубликовано в слое, видно в тайле и карточке по клику, и маскирование ПДн на эти слои не действует. - UsersDB одна на все города, база города — своя у каждого. Десктоп работает с той же базой города.
Один API на все города
Город запроса задаёт префикс пути /city/<код>/, базу — список TGID_CITY_DBS=код=база,…. Токен входа общий,
доступ к городу проверяется на каждом запросе. По городам разделены пул соединений, кеши процесса, очередь
Celery tgid.<код>, доводка схемы и файлы тайлов.
Два следствия, о которых чаще всего забывают:
соединений к PostgreSQL ≤ DB_POOL_MAX_SIZE × число городов × число процессов API
пример: 20 × 12 × 2 = 480 — сверить с max_connections
Задача города, очередь которого не слушает ни один воркер, ждёт вечно. Добавили город — добавьте tgid.<код>
в -Q воркеров.
Доводка схемы. Десктоп при переконвертации пересоздаёт базу и теряет таблицы, индексы и функции веба.
API проверяет это при запуске и раз в DB_UPKEEP_INTERVAL_S секунд и применяет недостающее одной транзакцией.
Если база занята конвертером или висит транзакция, API ждёт следующего прохода (busy). Поэтому после
переконвертации ничего вручную запускать не нужно. Исключение — UsersDB: её миграции применяют при выкатке.
Расчётные движки
| Переменная воркера | По умолчанию в коде | Прод |
|---|---|---|
SETY_ENGINE — команда расчёта |
legacy (прежний sety/ww.py) |
heatnet |
HEATNET_PROFILE — особенности десктопа |
legacy (побайтно как десктоп) |
fixed |
HEATNET_CORE — кто считает тяжёлые части |
python |
rust |
Умолчания в коде самые осторожные, поэтому прод задаёт все три явно. Правила:
- Переменные службы важнее
.env, а новое значение действует после мягкого перезапуска воркера: командаshutdownчерез брокер, воркер дорабатывает текущий расчёт и выходит. Перезапуск средствами службы обрывает идущий расчёт. - Все воркеры, слушающие очередь одного города, настроены одинаково — иначе результат зависит от того, какой воркер взял задачу. Профиль в протокол расчёта не пишется: его видно только по переменным службы.
- Ядро Rust (
heatnet/_core.pyd) даёт те же числа, что Python, только быстрее. Не загрузилось — расчёт идёт на Python, а в stderr одна строка «ядро не загружено». Новое ядро проверяют режимомHEATNET_CORE=checkна копии базы.
Чем профиль fixed отличается от десктопа для пользователя — Расчётный движок.
Переменные веба читаются дважды: при сборке (запекаются в .output) и при запуске, где их перекрывают
только имена NUXT_… и NUXT_PUBLIC_…. Всё с NUXT_PUBLIC_ попадает в браузер — секретам там не место.
Выкатка и откат
Принцип: на сервере ничего не собирают и не правят «на живую». Веб собирают в отдельном рабочем дереве
и подменяют каталог .output целиком: сборка в рабочем каталоге удаляет файлы, которые работающий сервер ещё
вставляет в страницы. API переводят fast-forward на заранее проверенный коммит, проверяют импорт, применяют
миграции UsersDB и перезапускают процессы API по одному, чтобы nginx всё время имел живой процесс. Каждый
шаг проверяется, откат — тот же сценарий в обратную сторону.
Резервные копии и состояние
Нельзя потерять: базы городов (всё введённое в вебе, с фото нарядов), UsersDB, .env и переменные служб
(JWT_SECRET, пароли, движок), каталог GeoServer, конфигурацию nginx. Не нужно: Redis, выгрузки, кеши тайлов.
Копия — не копия, пока её не восстановили: скрипт проверяет дамп и раз в неделю восстанавливает одну базу
во временную. Копии содержат ПДн и секреты — храните их как сам сервер.
/health отвечает всегда 200; исправность — в поле status (ok / degraded) и в разделах database,
redis, city_schema.
Персональные данные и права
Маскируется ответ, а не данные. API отдаёт ФИО, телефоны, лицевые счета замаскированными всем, у кого нет
права pii_view. Маскирования нет у тайлов GeoServer и martin, у файловых выгрузок (кроме сверки АЛСЕКО),
у JSON больше PII_MAX_BYTES. Поэтому слои с ПДн публикуют без этих колонок.
Права = шаблон роли + отличия. В UsersDB хранятся только отличия пользователя от шаблона. Сервер проверяет
права на каждом запросе; веб лишь прячет кнопки. Поверх прав стоят флаги сервера: без MUTATIONS_ENABLED
данные сети не правит никто, включая администратора. Безопасный набор для сервера с доступом извне:
AUTH_DISABLED=false AUTH_REQUIRED_GET=true STRICT_AUTH=true DEV_LOGIN_ENABLED=false
JWT_SECRET=<свой длинный секрет>
STRICT_AUTH=true не даст API запуститься, если что-то из этого забыто.
Как сделать в ТГИД
- Компоненты и порты — Из чего состоит система.
- Добавить город, доводка схемы — Несколько городов.
- Переменные воркера и ядро — Расчётные движки; все переменные — Переменные окружения.
- Порядок выкатки и отката — Выкатка и откат.
- Скрипт копий и восстановление — Резервные копии.
/health, метрики, баннеры веба — Состояние и журналы.- ПДн и права — Персональные данные, Роли и права, выдача прав в окне — урок 20.
Задания
1. Прочитайте /health
Ответ API на два города (сокращён, данные вымышленные). Что не так и что делать?
{"status": "degraded",
"database": {"cities": {"demo": {"ok": true}, "testgrad": {"ok": true}}},
"redis": {"ok": true},
"city_schema": {"cities": {"demo": {"status": "ok"},
"testgrad": {"status": "error", "error": "relation \"nodes\" does not exist"}}}}
Ответ
Обе базы и Redis отвечают, но доводка схемы города testgrad упала: в базе нет таблицы сети nodes. Скорее
всего, база создана не до конца или идёт переконвертация из десктопа. Проверьте базу (nodes, linesobj,
heatpipesections, externalcodes). После исправления API доведёт схему сам в ближайший проход, и status
станет ok. Код ответа всё это время был 200 — поэтому сторож должен смотреть на status.
2. Числа «прыгают»
Один и тот же плановый расчёт фрагмента дважды за час дал разный отпуск тепла. Сеть не правили, магистраль не пересчитывали. Город обслуживают два воркера. Что проверить первым?
Ответ
Переменные служб обоих воркеров: SETY_ENGINE, HEATNET_PROFILE, HEATNET_QUIRKS_OFF. Если у одного fixed,
а у другого legacy (или воркер поднят без переменных и считает прежним sety/ww.py), результат зависит
от того, кто взял задачу. В протоколе профиль не виден. Выровняйте переменные и мягко перезапустите воркер.
3. Найдите опасные настройки
Фрагмент .env API сервера, открытого в интернет (значения вымышленные):
AUTH_DISABLED=false
AUTH_REQUIRED_GET=false
STRICT_AUTH=false
DEV_LOGIN_ENABLED=true
PII_MASKING=false
MUTATIONS_ENABLED=true
Ответ
DEV_LOGIN_ENABLED=true — вход с любым логином и любой ролью без пароля. AUTH_REQUIRED_GET=false — чтение
данных сети без входа, и GeoServer пускает всех. PII_MASKING=false — ПДн видны без права. STRICT_AUTH=false
позволил всему этому запуститься. Нужно: DEV_LOGIN_ENABLED=false, AUTH_REQUIRED_GET=true, PII_MASKING
убрать (по умолчанию true), STRICT_AUTH=true и проверить, что JWT_SECRET свой, а не заглушка.
MUTATIONS_ENABLED=true — нормально для рабочего сервера.
