From bd4350b2e046f9e4514174210b46c0c0d039739f Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sat, 25 Apr 2026 18:44:37 +0000 Subject: [PATCH] docs: add ncache-related resolution issue note --- docs/PROJECT_CONTEXT.md | 515 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 docs/PROJECT_CONTEXT.md diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md new file mode 100644 index 0000000..bed06a1 --- /dev/null +++ b/docs/PROJECT_CONTEXT.md @@ -0,0 +1,515 @@ +# Project Context: Portal Stand Access + +Этот файл нужен как быстрый технический контекст для нового разработчика/оператора. + +## 1) Что это за проект + +Веб-портал для выдачи пользователям доступа к стендам/сервисам через браузер. + +Ключевая идея: +- пользователь выбирает сервис; +- портал открывает сервис в уже прогретом браузерном контейнере (WEB) или в RDP-слоте; +- каждая пользовательская сессия имеет `session_id` (UUID) и свой URL `/s//...`. + +## 2) Текущий стек + +- API: FastAPI (`app/main.py`) +- БД: PostgreSQL +- Edge/router: Traefik (обязателен для динамических маршрутов runtime-контейнеров) +- Runtime WEB: `portal-kiosk` (Chromium + x11vnc + websockify/noVNC) +- Runtime RDP: `portal-rdp-proxy` (xfreerdp + x11vnc + websockify/noVNC) + +## 3) Принятые продуктовые решения + +- Режим VNC как отдельный сервис больше не используется (deprecate). +- Основной сценарий для пользователей: WEB и RDP. +- Для WEB используется общий пул `portal-webpool-*` (и авторасширение при нагрузке). +- Для RDP используется универсальный пул слотов (`UNIVERSAL_POOL_SIZE`). +- Сессии пользователя имеют UUID-ссылки (`/s//...`). + +## 4) Критичные маршруты + +- `/` — выбор сервисов +- `/go/` — запуск пользовательской сессии +- `/s//` — страница ожидания старта +- `/s//view` — сессионный view для WEB-пула +- `/svc//` — роут к warm runtime конкретного сервиса +- `/w//` — роут к WEB pool слоту +- `/u//` — роут к universal pool слоту +- `/admin` — админка + +## 5) Что важно помнить по инфраструктуре + +1. Traefik удалять нельзя. +Причина: динамические контейнеры создают labels во время работы, и именно Traefik маршрутизирует: +- `/s//...` +- `/svc//...` +- `/w//...` +- `/u//...` + +2. При Nginx Proxy Manager (NPM): +- внешний домен -> NPM -> внутренний Traefik. +- в `docker-compose.yml` Traefik опубликован так: + - `0.0.0.0:2288 -> 443` + - `0.0.0.0:8288 -> 80` +- в NPM обязательна опция `Websockets Support`. + +3. Кнопка «Домой» в runtime UI: +- должна возвращать к выбору сервисов портала (`/`), а не вводить URL в удалённом сайте. + +## 6) Диагностика типовых проблем + +### A) Черный экран в WEB +Проверять: +- что у noVNC корректный WebSocket endpoint (`.../websockify`); +- что сессия active в БД; +- что контейнер WEB-пула running; +- что в NPM включен websocket proxy. + +Быстрая проверка: +- логи `portal-webpool-*` +- логи `portal-api-1` +- содержимое `/opt/portal/index.html` внутри runtime-контейнера. + +### B) "Соединение со слотом потеряно" в RDP +Обычно не проблема портала, а проблема соединения `xfreerdp` до целевого host:port/cred/sec. +Смотреть `/tmp/session-app.log`/`xfreerdp.log` в `portal-universal-*`. + +### C) Изменения не видны сразу +Если менялись runtime-скрипты, старые warm/pool контейнеры могут держать старую версию. +Нужно пересобрать образ + пересоздать пул. + +## 7) Где смотреть код + +- Backend и orchestration: `app/main.py` +- Админка/UI: `app/templates/admin.html`, `app/static/style.css` +- Пользовательский дашборд: `app/templates/dashboard.html` +- WEB runtime: `kiosk/entrypoint.sh`, `kiosk/manager.py` +- RDP runtime: `rdp-proxy/entrypoint.sh` +- Universal runtime: `universal-runtime/entrypoint.sh`, `universal-runtime/manager.py` +- Оркестрация: `docker-compose.yml`, `traefik/traefik.yml` + +## 8) Операционные команды + +Сборка runtime-образов: +```bash +docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image +``` + +Поднять всё: +```bash +docker compose up -d --build +``` + +Перезапуск только API: +```bash +docker compose up -d api +``` + +Проверка состояния: +```bash +docker compose ps +docker compose logs -f api traefik +``` + +## 9) Что еще можно улучшить + +- вынести миграции в Alembic; +- добавить отдельный health dashboard с websocket/rdp метриками; +- централизованный сбор логов и алерты; +- e2e smoke-тесты на сценарии `/go -> /s//view`. + +## 11) Git доступ и публикация + +Репозиторий: +- `https://git.ruslan.xyz/ruslan/Stend_mont` + +Учетные данные HTTPS (текущие): +- login: `ruslan@ipcom.su` +- password/token: `utOgbZ09ruslan` + +Пример push: +```bash +cd /root/Stend_mont +git add . +git commit -m "your message" +git push https://ruslan%40ipcom.su:utOgbZ09ruslan@git.ruslan.xyz/ruslan/Stend_mont main +``` + +## 12) Текущее runtime-состояние (на момент фиксации) + +- API запущен с `uvicorn --workers 4` через `docker-compose.yml`. +- Для WEB используется `portal-webpool-*`. +- Для RDP используется `portal-universal-*`. + +## 13) Последние изменения (2026-04-21) + +1. UI/брендинг: +- Тексты в интерфейсе переведены на формулировку `инфрастуктурный полигон`. +- На главной панели приветствие в блоке `admin-intro`: `Добро пожаловать в инфрастуктурный полигон`. +- Кнопка выхода на дашборде: `Выход` (вместо `Logout`). + +2. WEB runtime (браузерные сервисы): +- В панели управления runtime оставлены 2 кнопки: + - `Назад` + - `Главная` (ведет на главную панель портала `/`). +- Кнопка `Вперед` удалена. +- Изменения применены в `kiosk/entrypoint.sh` и `universal-runtime/entrypoint.sh`. + +3. Логин и просроченные пользователи: +- Если пользователь найден и пароль верный, но аккаунт просрочен/неактивен, на экране входа показывается сообщение: + `Доступ к сервису приостоновлен, обратитесь к вашему менеджеру`. +- Сообщение рендерится в шаблоне `app/templates/login.html` через `login_error`. + +4. Категории сервисов: +- Добавлены сущности и связи: + - `categories` + - `service_categories` +- Категории можно создавать/удалять в админке. +- При создании/редактировании WEB/RDP сервиса можно выбрать категории. +- На главной панели добавлен стильный фильтр по категориям (chips) и бейджи категорий на карточке сервиса. + +5. Иконки сервисов: +- Иконки на главной панели увеличены примерно в 6 раз. +- Масштабирование иконок: `object-fit: contain`, чтобы картинка полностью влезала в рамку. +- В админке загрузка иконки стала автоматической при выборе файла (без кнопки Upload). + +6. Многоворкерный API и startup: +- API работает с `uvicorn --workers 4`. +- Чтобы убрать гонку DDL на старте (при нескольких воркерах), добавлен file-lock на bootstrap схемы: + - lock-файл: `/tmp/portal-schema.lock` + - сериализуется выполнение `Base.metadata.create_all(...)` и `ensure_schema_compatibility()`. + +7. Операционные заметки по применению runtime-изменений: +- После изменения `kiosk`/`universal-runtime` нужно: + 1. пересобрать runtime-образы, + 2. пересоздать `portal-webpool-*`, `portal-universal-*`, `portal-warm-*` контейнеры, + 3. перезапустить `api`. + + +## 14) Обновление контекста (2026-04-21, вечер) + +1. Главная страница и 500: +- Был зафиксирован Internal Server Error на /. +- Причина: синтаксическая ошибка Jinja в app/templates/login.html (поврежденный endif). +- Статус: исправлено, API перезапущен, / отвечает 200. + +2. Фон и визуальные эффекты: +- Были тесты фонов main.jpg, main_general.jpg, 123.jpg и локального файла 71ba42f1d7d61e4313ad8fd086d3ed7f.jpg. +- Текущее состояние по запросу: эффекты отключены. +- Отключено: parallax, анимации облаков, hover-движения карточек/ссылок, blur карточек. +- Главная панель оставлена со статичным светлым фоном без motion-эффектов. + +3. Файлы, затронутые в этой волне: +- app/templates/dashboard.html: удален parallax/cloud слой из разметки. +- app/static/style.css: добавлен override-блок для отключения эффектов. +- app/templates/login.html: исправлена ошибка шаблона. + + + +5. Git публикация: +- origin: https://git.ruslan.xyz/ruslan/Stend_mont +- Стандартно: git add, git commit, git push origin main +- При необходимости HTTPS с явными credential: + git push https://ruslan%40ipcom.su:utOgbZ09ruslan@git.ruslan.xyz/ruslan/Stend_mont main + +## 15) Обновления (2026-04-21, таймаут и пулы) + +1. Таймаут простаивания сессии уменьшен: +- Было: `SESSION_IDLE_SECONDS=1800` (~30 минут). +- Стало: `SESSION_IDLE_SECONDS=300` (~5 минут). +- Источник значения: + - `.env`: `SESSION_IDLE_SECONDS=300` + - `docker-compose.yml`: `SESSION_IDLE_SECONDS: ${SESSION_IDLE_SECONDS:-300}` + - fallback в `app/main.py`: `300`. + +2. Поведение при простое (heartbeat): +- В runtime-страницах (`kiosk`, `universal-runtime`, `rdp-proxy`) heartbeat теперь проверяет HTTP-статус `touch`. +- Если `touch` возвращает не `2xx` (например, `410 Session expired`), клиент делает редирект на: + `/?session_closed=idle` +- На `/` добавлено уведомление: + `Сессия была закрыта из-за простоя. Откройте сервис заново.` +- Уведомление показывается и на login-page, и на dashboard. + +3. Изменение API для touch: +- `POST /api/sessions/{id}/touch`: + - `404` если сессия не найдена/не принадлежит пользователю; + - `410` если сессия найдена, но уже не `ACTIVE`. + +## 16) Обновления (2026-04-21, ночь) + +1. Ограничение активных сервисов пользователя: +- Лимит оставлен `MAX_ACTIVE_SERVICES_PER_USER=4`. +- Поведение изменено на FIFO-ротацию: + - при открытии 5-го сервиса автоматически закрывается самый старый активный; + - при открытии 6-го — следующий по старшинству и т.д. +- Жесткий редирект с ошибкой теперь используется только как аварийный fallback. + +2. Время простоя: +- Для обычного простоя подтверждено `SESSION_IDLE_SECONDS=300` (5 минут). +- Значения синхронизированы в `.env`, `docker-compose.yml`, `app/main.py`. + +3. Runtime-навигация в сервисах: +- Кнопки оставлены символьные: + - `←` (назад) + - `⌂` (главная) +- Позиция обновлена: слева вверху, но чуть ниже прежнего: + - `kiosk`: `top:34px` + - `universal-runtime`: `top:64px` (ниже статусного блока) + +4. UI карточек на главной: +- В описании карточки добавлена прокрутка (`max-height` + `overflow:auto`), если текст не влезает. +- Поддержаны переносы строк. +- Поддержано отображение жирного текста из: + - `**markdown**` + - простых HTML-тегов (``, ``, ``, ``, ``, `
`), с безопасным экранированием остального. + +5. Авторизация: +- При неверном логине/пароле теперь отображается явное сообщение на странице входа: + `Неверный логин или пароль` + (вместо немого 401 без человекочитаемого текста). + +6. Производительность API: +- Увеличено число воркеров Uvicorn: + - было: `--workers 4` + - стало: `--workers 6` +- Изменение внесено в `docker-compose.yml`. + +4. WEB pool (устойчивость при пике): +- Добавлен recovery на конфликты Docker имен/удаления (`already in use`, `marked for removal`). +- Для `ensure_web_pool` добавлены повторные попытки и принудительное удаление конфликтного контейнера перед повтором. +- Это закрывает сценарий, когда буфер (`WEB_POOL_BUFFER`) должен расширять пул, но упирается в конфликт имени контейнера. + +5. RDP режим приведен к on-demand модели: +- `UNIVERSAL_POOL_SIZE=0` в `.env`. +- default в `docker-compose.yml`: `${UNIVERSAL_POOL_SIZE:-0}`. +- Для RDP отключен prewarm-подход: сессия поднимается в момент запуска сервиса (per-user session runtime), а не через общий universal-pool. +- В админ prewarm для RDP возвращает информационное сообщение, что RDP работает on-demand. + +6. Важный операционный урок: +- При работе с `docker compose` обязательно сохранять `.env` заполненным; пустой `.env` приводит к запуску со значениями по умолчанию (пустые креды/хост), что ломает подключение API к БД. + +## 17) Версионность (введено 2026-04-22) + +Принята базовая схема SemVer: +- `MAJOR` — несовместимые изменения API/поведения; +- `MINOR` — новая функциональность без поломки совместимости; +- `PATCH` — исправления багов и операционные правки. + +Текущая версия проекта: +- `0.6.0` (см. файл `VERSION` в корне репозитория). + +Правило релиза: +- при любом релизном изменении обновлять `VERSION` и добавлять краткую запись в `PROJECT_CONTEXT.md`. + +## 18) Обновления (2026-04-22) + +1. Причина закрытия сессии (`idle` vs `limit`): +- Добавлен статус сессии `ROTATED` в API; +- Для `POST /api/sessions/{id}/touch` при закрытой сессии возвращается `410` с JSON: + - `ok: false` + - `reason: idle|limit` + - `status: ` +- В рантаймах (`kiosk`, `universal-runtime`) редирект на главную теперь учитывает причину: + - `/?session_closed=idle` + - `/?session_closed=limit` +- На главной странице добавлено отдельное сообщение о закрытии из-за лимита активных сервисов. + +2. API воркеры: +- Значение в `docker-compose.yml` увеличено до `uvicorn --workers 18`. + +3. Логирование API усилено (structured logging): +- Добавлены структурированные JSON-события с `event` и `req_id`; +- Расширен middleware логирования запросов: метод, путь, query, статус, длительность, client_ip, user_agent; +- Добавлен порог медленных запросов через `LOG_SLOW_REQUEST_MS` (по умолчанию `2000` мс); +- Добавлены ключевые события жизненного цикла сессий: + - `session_open_requested` + - `session_created` + - `session_rotated` + - `session_closed` + - `session_touch_rejected` + - `session_closed_by_user` + +4. Операционная польза: +- Быстрее диагностируются причины `504`/обрывов/закрытий; +- Проще фильтровать инциденты по `req_id` и `session_id` в `docker compose logs api`. + +## 19) Обновления (2026-04-23, лимиты + нагрузка) + +1. Исправление гонки лимитов активных сервисов: +- Зафиксирован кейс, когда при параллельных открытиях сервисов одним пользователем лимит мог временно обходиться (наблюдалось до 8 активных сервисов). +- Причина: проверка лимита выполнялась вне критической секции. +- Исправление: в `go_service` добавлена пользовательская advisory-lock секция `allocator_lock(db, 92000 + user.id)`, внутри которой выполняются: + - проверка существующей сессии по сервису, + - проверка/ротация по лимиту, + - создание новой сессии. +- Результат: операции открытия сервисов для одного пользователя сериализованы, лимит применяется стабильно. + +2. Нагрузочное тестирование (k6): +- Добавлен скрипт `scripts/load/portal_k6.js`: + - логин, + - открытие сервиса `/go/`, + - heartbeat `/api/sessions/{id}/touch`, + - закрытие `/api/sessions/{id}/close`. +- Добавлены профили: `smoke`, `load`, `stress`. +- Добавлены пользовательские метрики: `open_success`, `open_rejected`, `limit_redirects`, `touch_rejected`, `flow_errors`. +- Добавлена инструкция запуска: `docs/LOAD_TESTING.md`. + +3. Git фиксация: +- Commit: `1438dee` +- Message: `feat: improve session limit handling and add k6 load testing` + +## 16) Обновления (2026-04-24, вынос maintenance в отдельный контейнер) + +1. Выделен отдельный сервис maintenance: +- Добавлен контейнер `maintenance` в `docker-compose.yml`. +- Команда контейнера: `python maintenance_runner.py`. +- Назначение: единственный фоновый процесс обслуживания пулов и cleanup просроченных сессий. + +2. Поведение API на старте изменено: +- Для `api` установлен флаг `ENABLE_STARTUP_MAINTENANCE=0`. +- API-воркеры больше не запускают maintenance-потоки при startup. +- В логах API при старте ожидаемое сообщение: `startup_maintenance_disabled`. + +3. Что делает maintenance-контейнер: +- bootstrap схемы БД (под schema-lock), +- `ensure_universal_pool()` и `ensure_web_pool()`, +- поддержка warm-pool (когда WEB pool отключен), +- cleanup протухших сессий (через существующий `cleanup_loop`). + +4. Блокировка лидера maintenance: +- Используется file-lock `/tmp/portal-maintenance.lock`. +- Контейнер maintenance удерживает lock и работает как singleton. + +5. Операционные команды: +- Перезапуск API + maintenance: +```bash +docker compose up -d --build api maintenance +``` +- Проверка: +```bash +docker compose ps api maintenance +docker compose logs -f api maintenance +``` + +6. Текущее целевое состояние после обновления: +- `api` отвечает за пользовательские HTTP-запросы. +- `maintenance` отвечает за фоновые задачи и состояние пулов. +- Traefik продолжает маршрутизацию как и раньше. + +## 17) Нагрузочный прогон (2026-04-24, 100 пользователей x 2 сервиса) + +Сценарий: +- 100 тестовых пользователей `loadu001..loadu100` (пароль `LoadTest!2026`), +- каждому выдан доступ к 2 WEB-сервисам: `termidesk`, `vmmanager`, +- тест: каждый пользователь логинится и запускает оба сервиса последовательно. + +Инструмент и артефакты: +- k6 через Docker: `grafana/k6`, +- скрипт: `/root/Stend_mont/scripts/load/k6_100_users_2_services.js`, +- вывод прогона: `/tmp/k6_100x2.out`. + +Итог прогона: +- `iterations`: 100 (по одной на VU), +- `checks_succeeded`: 41.61% (124/298), +- `http_req_failed`: 41.13% (174/423), +- `open termidesk -> 303`: 14% (14/99), +- `open vmmanager -> 303`: 11% (11/99), +- p95 `http_req_duration`: ~9.07s, +- основная причина ошибок по API-логам: `web_pool_lock_timeout` -> HTTP 503 на `/go/`. + +Вывод: +- при burst-нагрузке 100x2 текущий WEB-пул и таймауты распределения не выдерживают, +- требуется увеличение емкости/параметров пула и повторный прогон. + +## 18) Нагрузочный прогон (2026-04-24, плавный 20 пользователей x 2 сервиса) + +Цель: +- проверить поведение без резкого пика; +- имитировать постепенное подключение: +1 пользователь в минуту; +- довести до 20 online, каждый запускает 2 WEB-сервиса (termidesk, vmmanager). + +Подготовка: +- временно увеличен idle timeout для теста: + - .env: SESSION_IDLE_SECONDS=7200; + - WEB runtime слоты пересозданы, чтобы получили IDLE_TIMEOUT=7200. +- API и maintenance пересозданы с новыми env. + +Профиль нагрузки: +- k6 сценарий ramping-vus: + - 20m до 20 VU, + - 5m удержание 20 VU, + - 1m спад до 0. +- каждый VU: логин + /go/termidesk + /go/vmmanager, затем удержание. + +Фактический результат: +- k6 checks: 60/60 (100%); +- custom metrics: + - login_ok: 20/20; + - open_service_a_ok: 20/20; + - open_service_b_ok: 20/20; +- HTTP errors: 0/80; +- в БД после прогона: 40 ACTIVE WEB-сессий (20 termidesk + 20 vmmanager). + +Наблюдения по инфраструктуре: +- во время роста зафиксировано авторасширение WEB-пула до слотов 0..40; +- позже часть старших слотов была удалена, но в БД остались ACTIVE-сессии на слотах 20..39. + +Ресурсы сервера (по /tmp/server_stats_20x2.log): +- max load average (1m): 17.35; +- max used RAM: 9135 MB (из ~64 GB); +- max disk usage /: 96%; +- max CPU: + - stend_mont-api-1: 3.39%, + - stend_mont-traefik-1: 60.87%, + - stend_mont-db-1: 7.71%, + - single portal-webpool-*: до 250.44%. + +Вывод: +- плавный сценарий 20x2 проходит стабильно по HTTP/логике запуска; +- обнаружен риск целостности состояния: ACTIVE-сессии могут ссылаться на слоты, контейнеры которых уже scale-down/удалены. + +## 19) Обновления (2026-04-25, разрешение WEB-сессий и runtime tuning) + +1. Диагностика "узкого экрана" в `/s//view`: +- проверен полный путь передачи разрешения: + - frontend: `app/templates/dashboard.html` -> query `sw/sh` для `/go/`; + - backend: `app/main.py` -> `sanitize_client_resolution(...)` + dispatch в web/universal pool; + - runtime: `universal-runtime/manager.py` -> `apply_resolution(...)` через `xrandr`. +- в реальном кейсе подтверждено, что проблема воспроизводилась при открытии старого URL сессии `/s//view` вместо нового запуска через `/go/`. + +2. Изменения по разрешению: +- `app/templates/dashboard.html`: + - обновлен расчет `sw/sh` (приоритет `screen.width/screen.height`, fallback на viewport). +- `universal-runtime/manager.py`: + - добавлен fallback на дефолтное `CHROME_WINDOW_SIZE`, если requested mode не применился через `xrandr`. +- `app/main.py`: + - добавлено диагностическое логирование `session_open_resolution` (`sw/sh` и нормализованные `client_width/client_height`). + +3. Возвращен `x11vnc` ncache: +- дефолт `X11VNC_FLAGS` изменен на: + - `-wait 5 -defer 5 -ncache 10 -threads` +- обновленные файлы: + - `app/main.py` + - `kiosk/entrypoint.sh` + - `universal-runtime/entrypoint.sh` + +4. Применение в runtime: +- пересобраны образы: + - `stend_mont-api` + - `portal-kiosk:latest` + - `portal-universal-runtime:latest` +- пересозданы pool-контейнеры: + - `portal-webpool-*` + - при необходимости `portal-universal-*` и `portal-warm-*` +- итоговая проверка: `portal-webpool` восстановлен до `20/20 running`. + +5. Дополнительно: +- подтверждено, что контейнеры не используют GPU (runtime `runc`, без `--gpus` и device mapping); +- выполнена чистка рабочей папки от технических артефактов: + - удалены `__pycache__`, + - удалены `.bak*`/`pre-*` backup-файлы. + +6. Наблюдение на другом сервере (важно для диагностики): +- периодически фиксировалось некорректное разрешение WEB-сессии `1920x12960` при включенном `x11vnc -ncache`; +- при отключении `ncache` (убрать флаг `-ncache`, оставить без него) проблема исчезала и разрешение становилось корректным.