Урок 2 из 4 · 40 мин

Сервер ТГИД: устройство, настройка и сопровождение

ТГИД — веб 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 запуститься, если что-то из этого забыто.

Как сделать в ТГИД

Задания

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 — нормально для рабочего сервера.

Проверьте себя

Решено 0 из 7
1В параметрах службы воркера поменяли HEATNET_PROFILE с legacy на fixed, но новые расчёты считаются по-старому. Почему?
2Внешний сторож проверяет /health и получает HTTP 200. Значит ли это, что сервер исправен?
3Город добавили в TGID_CITY_DBS и перезапустили API. Карта открывается, а расчёты города бесконечно стоят «в очереди». Что забыли?
4Стенд на копии боевой базы запущен с AUTH_DISABLED=true. Кто увидит персональные данные потребителей без маски?
5Новый воркер запустили без переменных SETY_ENGINE, HEATNET_PROFILE и HEATNET_CORE. Чем он будет считать?
6Что из этого НЕ блокирует флаг MUTATIONS_ENABLED=false?
7Что случится, если при переносе сервера потерять JWT_SECRET из .env API?