Архитектура¶
Общая схема¶
flowchart TB
visitor([Посетитель]) -->|HTTPS| traefik
subgraph node["Узел: Ubuntu 24.04, 4 ядра, 4 ГБ памяти, k3s"]
traefik["Traefik<br/>вход, TLS, ограничение частоты"]
subgraph ns_app["Пространство metroskop"]
web["web<br/>Nginx + собранный React"]
app["metroskop<br/>Spring Boot, 2 реплики"]
pg[("PostgreSQL 16<br/>обращения жителей")]
load["loadgen<br/>синтетическая нагрузка"]
end
subgraph ns_obs["Пространство observability"]
prom["Prometheus<br/>метрики и правила"]
graf["Grafana<br/>дашборды"]
loki["Loki<br/>логи"]
tempo["Tempo<br/>трейсы"]
alloy["Alloy<br/>сбор логов"]
am["Alertmanager"]
adapter["prometheus-adapter<br/>прикладная метрика для HPA"]
end
subgraph ns_del["Доставка"]
rollouts["Argo Rollouts<br/>канарейка и откат"]
cm["cert-manager<br/>Let's Encrypt"]
end
end
traefik --> web
traefik --> app
traefik --> graf
traefik --> prom
traefik --> am
app --> pg
app -->|OTLP| tempo
load -->|запросы| app
prom -->|срез метрик| app
prom -->|срез метрик| traefik
prom --> am
alloy -->|поток логов| loki
graf --> prom
graf --> loki
graf --> tempo
adapter --> prom
rollouts -->|анализ канарейки| prom
cm -.->|сертификаты| traefik
Как рождается кадр карты¶
- Планировщик приложения раз в секунду просит
TrainServiceрассчитать состояние сети. Для каждой линии берётся её длина, время хода по перегонам и интервал выпуска, зависящий от времени суток. - Положение каждого состава — чистая функция от модельного времени: одна и та же секунда всегда даёт одну и ту же картинку. Это важно и для тестов, и для того, чтобы две реплики показывали одно и то же.
- Кадр сериализуется один раз и рассылается всем подписчикам потока событий. Сериализация на каждого подписчика была бы самой дорогой операцией в сервисе.
- Браузер получает кадр и до следующего плавно доводит составы между двумя положениями. Поэтому движение выглядит непрерывным при одном кадре в секунду от сервера.
Такт рассылки измеряется двумя гистограммами: сколько занял расчёт и насколько такт опоздал относительно заданной секунды. Вторая метрика — прямой признак паузы сборщика мусора, и именно она первой реагирует на сценарий с утечкой памяти.
Почему такой набор технологий¶
Java 21 и виртуальные потоки. Поток событий — это сотни соединений, которые почти всё время ждут. На платформенных потоках каждое соединение стоило бы мегабайта стека; на виртуальных — сотни байт. Приложение живёт в лимите 512 МБ памяти и держит расчёт четырёхсот составов в секунду.
PostgreSQL для обращений жителей. Схема без состояния выглядела бы неправдоподобно: в городском сервисе всегда есть очередь заявок со сроком обработки. Обращения дают предметный смысл срокам: доля обращений, закрытых в срок, — такая же цель по качеству, как и доступность.
Canvas 2D, а не WebGL. Четыреста точек и триста сегментов линий — задача, которую двумерное полотно решает за две миллисекунды на кадр. WebGL добавил бы шейдеры и сложность там, где нечего оптимизировать.
Traefik. Он уже входит в k3s, умеет ограничивать частоту запросов и проставлять заголовки безопасности через свои промежуточные обработчики, и его метрики сразу попадают в Prometheus.
Что чем управляется¶
| Слой | Инструмент | Где лежит |
|---|---|---|
| Узел: подкачка, параметры ядра, файрвол, k3s | Ansible | ansible/ |
| Платформа: пространства имён, cert-manager, стек наблюдаемости, Argo Rollouts | Terraform + Helm | terraform/ |
| Приложение, база, публикация, правила, дашборды | Kustomize | deploy/ |
| Справочник линий и станций | Python + Overpass API | data/ |
Разделение не случайное. Всё, что переживает пересоздание кластера, живёт в Ansible. Всё, что описывает платформу и требует управления версиями чартов, — в Terraform. Всё, что меняется вместе с кодом приложения, — в Kustomize, чтобы конвейер обновлял только образ и не трогал платформу.
Данные¶
Справочник линий и станций собирается из OpenStreetMap через Overpass API:
скрипт data/build_dataset.py вытягивает отношения маршрутов, упорядочивает
станции, считает расстояния и сохраняет metro.json. Файл попадает в образ
приложения на этапе сборки, поэтому во время работы сервис не зависит ни от
одной внешней системы.