Records the decision to take PHP out of the streaming byte path and remove the tmpfs mounts: native xc_fanout daemon for live fan-out + in-RAM HLS, nginx X-Accel/proxy_pass with auth_request, connection telemetry in Redis. Root cause from P0 load test: the ~400-concurrent ceiling is a linear RAM law (~25 MB per viewer) because each proxy viewer pins a PHP-FPM worker in a socket_read loop — not tmpfs pressure and not pm.max_children.
18 KiB
План миграции: отказ от tmpfs и вынос PHP из байтового пути
Компаньон к docs/adr/0001-tmpfs-free-streaming.md. Здесь — детальный, пофазный план работ на русском: что делаем, какие файлы трогаем, критерии приёмки, флаги, откат и риски.
Область этой итерации: сначала MAIN, LB — отдельной фазой в конце. Каждая фаза самостоятельна, выкатывается за флагом в таблице settings, откатывается без передеплоя.
0. Кратко: зачем и что меняем
Проблема. Потолок ~400 конкурентных соединений. Причина — не давление tmpfs на RAM, а PHP в потоке байт:
- Proxy-режим (
Public/stream/live.php:352-389) держит один PHP-FPM воркер на зрителя на всю сессию (циклsocket_read→echo→flush). Приpm.max_children≈ 400–512 это и есть стена ровно на 400. - HLS-режим отдаёт каждый
.tsчерез PHPreadfileиз tmpfs — потому чтоX-Accel-Redirectв коде отсутствует, nginx сегменты сам не раздаёт. - Везде двойное копирование ядро→PHP→ядро без zero-copy.
Решение. «Произвести один раз → раздать nginx-ом / нативным демоном → PHP только авторизует». После выноса PHP из байтов удаление tmpfs идёт следом.
Целевые компоненты:
xc_fanout— новый нативный демон (Go, один процесс, много потоков): ingest одногоffmpeg -f mpegts -на поток → zero-copy fan-out live + HLS-сегментер в RAM.nginx— единственное перед клиентами:proxy_passна демон по unix-сокету +auth_requestв лёгкий PHP-эндпоинт.Redis(уже в дистрибутиве) — реестр соединений и счётчики битрейта вместо tmpfs-файловopened_cons/divergence.
1. Целевая архитектура (потоки данных)
Live (MPEG-TS, непрерывный):
источник → ffmpeg -f mpegts - →(unix pipe/socket)→ xc_fanout (ring buffer)
│
клиент ──HTTP──> nginx ──auth_request──> PHP (204/403) │
клиент <──chunked mpegts── nginx ──proxy_pass(unix)──────┘ (fan-out на N клиентов)
HLS:
- Этап B (быстрый, малый риск): ffmpeg-муксер HLS пишет сегменты, но отдаёт их nginx через
X-Accel-Redirect/sendfile;STREAMS_PATHпереносится с tmpfs на реальный NVMe (горячие сегменты живут в page cache). PHP — только auth. Маунта tmpfs больше нет. - Этап A (чистый, дороже):
xc_fanoutнарезает сегменты из того же mpegts-фида в памяти, отдаёт.m3u8/.tsиз RAM. Файлов нет вообще. Требует кодеко-зависимого разбора TS (H264/H265 access units, ADTS AAC, PTS).
Сначала B (снимает бутылочное горлышко и убирает маунт), затем A — когда live-путь демона проверен.
2. Спецификация xc_fanout
Язык/поставка. Go, статический бинарь. Кладётся в binaries-релиз рядом с ffmpeg/nginx/redis, ставится и супервизится тем же путём, что и остальные бинари. Один процесс — много потоков (никогда не процесс-на-поток — это возвращает накладные расходы, от которых уходим).
На каждый активный поток:
- Ingest. Принимает один mpegts-фид от
ffmpeg -f mpegts -через unix-сокет/пайп — одно соединение-продюсер. - Live fan-out. Кольцевой буфер, выровненный по 188 байт (размер конфигурируем, напр. 2–10 МБ), с отслеживанием PAT/PMT. Новый подписчик присоединяется на границе PAT/PMT + keyframe, дальше следует за «хвостом». Медленные клиенты получают ограниченную персональную буферизацию, затем дропаются (конфиг) — продюсер никогда не блокируется.
- HLS в RAM (этап A). Нарезка того же фида по keyframe в байтовые слайсы в памяти, поддержка
.m3u8, скользящее окно N сегментов (соответствует текущимhls_list_size/hls_delete_threshold). Опционально AES-128 (.enc) в демоне на существующих key/iv. - Отдача в nginx по unix-сокету (
proxy_pass http://unix:/run/xc_fanout.sock):GET /live/<id>→ chunked mpegts;GET /hls/<id>.m3u8→ плейлист из памяти;GET /hls/<id>_<seq>.ts→ байты сегмента из кольца.
- Телеметрия. Демон владеет сокетами → он источник истины по connect/disconnect/bytes; пишет события в Redis (см. §4).
Fan-out. Go netpoller (epoll) + writev/sendfile-класс записи; 400+ подписчиков на поток — с большим запасом выше текущего PHP-потолка.
Управление жизненным циклом. Старт/стоп потока инициирует существующий супервизор (Domain/Stream/StreamProcess, Cli/Commands/MonitorCommand). Здоровье/PID демона и потоков — в Redis, а не в STREAMS_PATH/<id>_.pid.
Открытые вопросы спеки (решить до P2/P3):
- Протокол ingest: unix-сокет (демон слушает, ffmpeg-обёртка коннектится) vs именованный пайп. Предпочтение — unix-сокет (проще реконнект/супервизия).
- Стратегия медленного клиента: размер персонального буфера и порог дропа.
- Джойн-семантика: сколько истории PAT/PMT/keyframe держать для чистого старта плеера (сверить с текущим prebuffer
ProxyCommand:STORE_PREBUFFER/MAX_PREBUFFER).
3. Изменения nginx
- Клиентские локации
/stream/*дляlive/hls→proxy_passна unix-сокетxc_fanout(вместо PHPreadfile/socket_read). - auth через
auth_request: подзапрос в лёгкий внутренний PHP-эндпоинт, возвращающий204/403и заголовки (разрешённый stream_id, подсказка AES-ключа и т.п.). PHP байты не трогает. - Для этапа B:
internallocation поверхSTREAMS_PATH+X-Accel-Redirectиз PHP;sendfile on(уже есть). - Сохранить
proxy_max_temp_file_size 0/fastcgi_max_temp_file_size 0(уже стоят) — nginx не сбрасывает на диск. - Файлы:
src/bin/nginx/conf/nginx.conf(локации^/stream/(auth|segment|live|...)), позжеlb_configs/nginx.conf.
4. Телеметрия соединений → Redis
Заменяем tmpfs-файлы реестра на Redis (обёртка Infrastructure/Redis/RedisManager, Core/Cache/RedisCache уже есть). Пишет xc_fanout (авторитет по connect/disconnect), читают панель и кроны.
Схема (черновик):
conn:<uuid>(hash:stream_id,user_id,ip,started_at,bytes,bitrate) + TTL-heartbeat — заменаCONS_TMP_PATH/<uuid>иDIVERGENCE_TMP_PATH/<uuid>.stream:<id>:conns(set из uuid) — заменаglob(CONS_TMP_PATH/<id>/*)для подсчёта ёмкости/фан-аута.
Куда перенаправить потребителей:
Domain/Stream/ConnectionTracker::getCapacity(:37), reaping (:934-956);Cli/CronJobs/UsersCronJob— битрейт/анти-фрод (:499,:613);Cli/CronJobs/StreamsCronJob— reaping сокетов (:257-265);Cli/Commands/SignalsCommand(:123);- админ-монитор процессов.
Писатели, которые убираем (после переезда): live.php:296/350/398, segment.php:101, vod.php:194/197, timeshift.php:199/211/307/309.
5. Пофазный план
P0 — Базовый замер и инструментирование
- Цель: воспроизвести стену 400 на MAIN, снять «до»-цифры.
- Задачи: нагрузочный тест (live proxy + HLS); зафиксировать насыщение FPM-воркеров, syscalls (
strace/perf), RAM, load average. - Приёмка: есть воспроизводимый сценарий и профиль узкого места (ожидаем: воркеры FPM в proxy-режиме, readfile в HLS).
- Код: нет.
P1 — HLS без PHP в байтах (этап B) ← самый большой эффект/малое изменение
- Цель: убрать PHP
readfileиз HLS, сделать tmpfs-маунт необязательным. - Задачи:
- В
Public/stream/segment.php(и HLS-веткеlive.php) заменитьreadfile()на выдачуX-Accel-Redirectвinternallocation; оставить только auth/лимиты. - Добавить
internallocation вsrc/bin/nginx/conf/nginx.confповерхSTREAMS_PATH. - Сохранить AES-128: либо
.encчерез тот же internal-редирект, либо отдавать key черезkey.phpкак сейчас.
- В
- Флаг: нет. X-Accel — единственный путь (рантайм-переключателя PHP/nginx намеренно нет, чтобы не усложнять код). Откат —
git revert+ передеплой. - Приёмка: HLS-плеер работает; при 400 клиентах HLS число занятых FPM-воркеров не растёт линейно; сегменты отдаёт nginx (
sendfile). - Откат:
git revertкоммита P1 + передеплойsegment.php/nginx.conf(бэкапы.bakна узле). - Риск: внутренние пути должны точно соответствовать auth (нельзя дать угадать URL сегмента мимо авторизации) — location
internal(клиентам недоступна) + авторизация вsegment.phpдо участка выдачи.
P2 — xc_fanout: live fan-out ← снимает потолок воркер-на-зрителя
- Цель: заменить
socket_read-цикл в PHP на nginx→демон. - Задачи:
- Реализовать демон: ingest
-f mpegts -, ring buffer,GET /live/<id>, PAT/PMT-джойн. - Обёртка запуска ffmpeg-фида в демон вместо
ProxyCommandpopen+socket_sendto. - nginx:
proxy_passна демон +auth_requestдля/live/*. - Ретайр proxy-режима на datagram-сокетах (
ProxyCommand.php, socket-веткаlive.php:352-389).
- Реализовать демон: ingest
- Флаг:
live_fanout. - Приёмка: live-поток идёт через демон; при 400+ клиентах на поток FPM-воркеры не заняты байтами; картинка без артефактов на джойне; латентность не хуже текущей.
- Откат: флаг off → PHP socket-путь (пока не удалён).
- Риск: корректность джойна PAT/PMT+keyframe; поведение медленного клиента.
P3 — xc_fanout: HLS в RAM (этап A)
- Цель: нарезка HLS в памяти демона, ноль файлов.
- Задачи: TS-демукс + сегментация по keyframe;
.m3u8/.tsиз RAM; nginx/hls/*→ демон; убрать-hls_segment_filenameиз дефолтного пути. - Флаг:
hls_inmem. - Приёмка: HLS полностью из памяти; сегментные границы совпадают с текущими (seg_time/list_size); AES-128 сохранён.
- Откат: флаг off → этап B (nginx sendfile с диска).
- Риск: самый кодеко-зависимый и рискованный кусок — потому отложен за B.
P4 — Телеметрия → Redis
- Цель: убрать
opened_cons/divergenceиз tmpfs. - Задачи: демон эмитит connect/disconnect/bytes в Redis (§4); перенаправить
ConnectionTracker,UsersCronJob,StreamsCronJob,SignalsCommand, админ-монитор; двойная запись (tmpfs+Redis) на время сверки, затем срез. - Флаг:
conn_registry_redis. - Приёмка: лимиты соединений и анти-фрод по битрейту работают по Redis; цифры сходятся с tmpfs на этапе двойной записи.
- Откат: флаг off → tmpfs-реестр.
- Риск: трогает лимиты/анти-фрод — обязательна фаза сверки перед срезом.
P5 — Снять tmpfs
- Цель: убрать маунты и тоглы.
- Задачи: удалить fstab-записи (
install,LbInstallFlow) и ramdisk-тогл (RootSignalsCronJob:500-527); остаточный scratchTMP_PATH→ реальный диск/Redis; проверить, что ни один путь не рассчитывает на tmpfs-маунт. - Приёмка: система работает без tmpfs-маунтов; RAM освобождён под page cache/приложение.
- Откат: вернуть fstab +
mount -a.
P6 — Раскатка на LB
- Цель: тот же слой на LB-нодах.
- Задачи: упаковать
xc_fanoutв binaries-релиз; расширитьCli/Commands/LbInstallFlow(установка/супервизия демона + новый nginx-конфиг); канареечно на части LB. - Приёмка: LB-нода на новом слое держит нагрузку; LB-архив остаётся без привилегированного кода (гейты
verify-lb-archive).
6. Тестирование и приёмка (общее)
- Функциональные: live (proxy и direct), HLS (clear + AES-128), timeshift/VOD не сломаны, EPG/лимиты/гео-проверки в
auth_requestидентичны текущим. - Нагрузочные: цель — уверенно держать значительно выше 400 конкурентных на поток; замер FPM-воркеров, RAM, syscalls «до/после» (из P0).
- Регресс auth: порт auth-логики без переписывания; тесты на токен/лимит/гео до включения флага.
- Сверка телеметрии: двойная запись P4, диф tmpfs vs Redis.
7. Риски и принципы
- Каждая фаза — за флагом в
settings; откат = переключение флага без передеплоя.xc_fanoutупал → nginx откатывается на PHP-путь, пока флаг off. - Самый рискованный кусок (HLS-A, §P3) отложен за дешёвым и доказательным HLS-B.
auth_requestдолжен сохранить все текущие проверки — портируем, не переписываем.- Новый нативный бинарь в доверенном binaries-релизе → security-review + воспроизводимая сборка; LB-архив остаётся без привилегий.
- Джойн live-кольца (PAT/PMT + keyframe) должен быть корректным, иначе артефакты у плеера при подключении.
8. Порядок захода
Рекомендую начать с P1 — самый дешёвый и самый доказательный кусок: он один снимает HLS-горлышко и делает 90%-RAM-маунт необязательным, не трогая демон. Параллельно можно писать спеку xc_fanout (§2, закрыть открытые вопросы), чтобы к P2 демон был готов.
Альтернатива — если хочется бить в самое больное: сразу проектировать xc_fanout и заходить с P2 (proxy-режим — источник стены 400).