Количество просмотров — это первое число, которому доверяет посетитель. Оно говорит ему, стоит ли видео тридцати секунд или тридцати минут его времени. В TopVideoHub это число растет стремительно. Трендовый ролик может набрать 40 000 просмотров за десять минут. Если счетчик на странице замирает, кажется, что в зале пусто. Пользователи уходят.

Доставить это число в браузер кажется тривиальной задачей. Но это не так. Первое решение, к которому прибегают большинство команд, — это опрос (polling). Его легко настроить, и в стейджинге он работает отлично. Но стейджинг лжет.

Когда опрос превращается в DDoS-атаку на самого себя

Команда TopVideoHub создала простой JavaScript-поллер. Он запрашивал последнее количество просмотров каждые пять секунд. В тестовой среде с тремя открытыми браузерами всё выглядело замечательно. В продакшне это обрушило платформу.

Восемь тысяч одновременных зрителей, обновляющих страницу каждые пять секунд, генерировали 1600 запросов в секунду. Каждый запрос нагружал базу данных. Резко выросла задержка репликации. Реплики для чтения задыхались. Слои кэширования обходились. Команда не раздавала видео — она обслуживала нагрузку, которую создала сама.

Опрос невинен, пока не станет проблемой. Для панелей управления или дашбордов с низкой нагрузкой он вполне подходит. Но для страницы вирусного видео это бомба замедленного действия. Команде требовался постоянный канал от сервера к браузеру, но им не нужна была сложность полнодуплексного протокола.

Почему SSE подходит для односторонних каналов

Server-Sent Events (SSE) созданы именно для таких задач: у сервера есть данные, а браузеру нужно только их слушать.

В отличие от WebSockets, SSE работает поверх обычного HTTP. Это важнее, чем кажется. Вам не нужны новые правила проксирования, заголовки upgrade или акробатика с балансировщиками нагрузки. Если ваш сервер поддерживает HTTP/1.1 или HTTP/2, SSE будет работать. Отладка проходит безболезненно, потому что поток — это просто текст. Вы можете направить curl на эндпоинт и наблюдать за бегущими цифрами в реальном времени, что гораздо лучше, чем гадать, почему бинарный фрейм сокета пошел не так.

Браузер берет на себя всю рутину бесплатно. Если соединение разрывается, SSE автоматически переподключается с использованием заголовка Last-Event-ID, чтобы сервер знал, на чем остановиться. API в JavaScript минималистично: создайте EventSource, прикрепите обработчик onmessage, и готово.

Кэширование — это и есть настоящая архитектура

Самая большая архитектурная ошибка в живых счетчиках — рассматривать каждое браузерное соединение как повод для запроса к базе данных. Если 8 000 человек смотрят одно и то же видео, запускать 8 000 запросов каждые две секунды — это безумие. Ваша база данных не переживет вирусный трафик.

TopVideoHub решила эту проблему с помощью APCu — кэша в оперативной памяти для опкодов и пользовательских данных PHP. Схема проста. Фоновый процесс (или легковесный эндпоинт, вызываемый по таймеру) записывает текущее количество просмотров в APCu раз в две секунды. SSE-эндпоинт, который могут держать открытыми тысячи браузеров, читает данные исключительно из APCu.

Результат: база данных получает всего один запрос каждые две секунды, независимо от количества зрителей. Кэш становится амортизатором. APCu — это не что-то экзотическое. Он поставляется вместе с PHP, живет в разделяемой памяти и читается быстрее, чем любой сетевой запрос. Для одного числа, которое часто меняется, но не мгновенно, это идеальный инструмент.

Если вы не используете APCu, подойдут Redis или Memcached. Принцип остается тем же: отделите путь горячего чтения от базы данных.

Как заставить PHP, LiteSpeed и Cloudflare работать в режиме потоковой передачи

PHP хочет завершить работу и «уйти домой». Веб-серверы хотят буферизировать вывод и отдавать аккуратный ответ. SSE нужно обратное: соединение, которое остается открытым, сбрасывая байты по мере их поступления. Без должной настройки ваш «поток» придет одним куском через тридцать секунд, сводя на нет всю пользу.

Вот как TopVideoHub сделала так, чтобы канал не засорялся.

Отключите буферизацию вывода. В начале SSE-скрипта отключите все уровни буферизации, которые может включить PHP. Вызовите ob_end_flush(), если буфер активен, и отключите неявный сброс с помощью ob_implicit_flush(true) после отправки заголовков.

Скажите прокси-серверам не вмешиваться. Отправьте заголовок X-Accel-Buffering: no. Nginx его понимает. LiteSpeed тоже. Это сигнал о том, что ответ не должен буферизироваться или сжиматься в кэшируемый блок.

Установите короткое время жизни. Каждое SSE-соединение занимает PHP-воркер. TopVideoHub ограничивает время жизни потока 55 секундами. Когда таймер срабатывает, сервер отправляет финальный комментарий, закрывает поток, и браузер автоматически переподключается. Это переподключение попадает на новый воркер, не позволяя одному процессу занимать ресурсы вечно.

Пинг для поддержания соединения. Отправляйте строку-комментарий — что-то вроде : ping — каждые двадцать секунд. Комментарии в SSE игнорируются обработчиком сообщений браузера, но они поддерживают TCP-соединение «теплым». Балансировщики нагрузки и CDN часто разрывают неактивные соединения через тридцать или шестьдесят секунд. Дешевый символ переноса строки спасет вас от этого разрыва.

Уважайте вкладку пользователя. Когда посетитель сворачивает или скрывает вкладку, прекращайте работу. Слушайте visibilitychange в браузере и вызывайте eventSource.close(). Сервер также должен обнаруживать отключение клиента и завершать цикл. В PHP можно проверять connection_aborted() внутри цикла. Не позволяйте «призрачным» соединениям сжигать воркеры ради людей, которые ушли десять минут назад.

Жесткий потолок: PHP-воркеры

SSE в PHP честен в своих ограничениях. Каждое открытое SSE-соединение потребляет один PHP-воркер. Если в вашем пуле сто воркеров, у вас будет сто потоков. И точка. В рамках модели процессов Apache или PHP-FPM никаких обходных путей через асинхронность не существует. Вы можете настроить pm.max_children, но реальную границу устанавливают память и процессор.

Этот лимит наступает быстро, если вы также используете воркеры для обычной загрузки страниц, API-вызовов и генерации ресурсов. Тщательно следите за насыщением воркеров. Если ваш SSE-эндпоинт начинает вставать в очередь, потому что все воркеры заняты двадцатиминутными потоками, замедлится весь ваш сайт.

Когда цифры перестают сходиться, пора переходить на другой уровень. Обычно следующим шагом становится Go, хотя Rust, Node.js или Erlang могут выполнять ту же роль. Ключ к успеху — горутины (goroutines) в Go. Одна горутина стоит всего несколько килобайт. Вы сможете удерживать десятки тысяч потоков на скромном оборудовании без особых усилий. Основная логика остается прежней — чтение из кэша, запись в сокет — но среда выполнения меняется с тяжелых процессов на легковесные потоки.

Однако не стоит начинать с этого. PHP позволит вам зайти на удивление далеко. Сначала проверьте жизнеспособность продукта. Когда на странице метрик вместо перегрузки базы данных вы увидите исчерпание воркеров, это будет означать, что вы переросли текущий стек. Это хорошая проблема.

Итог

Живые счетчики — это не вопрос чистых технологий. Это вопрос защиты вашей базы данных от ваших же пользователей. Начинайте с SSE, потому что это проще, чем кажется. Агрессивно кэшируйте данные между потоком и базой данных, чтобы количество соединений не превратилось в количество запросов. Следите за лимитами воркеров как коршун. И начинайте с простого. PHP достаточно до тех пор, пока его не станет мало, и к тому моменту вы будете точно знать, зачем делаете рефакторинг.

Для вашего следующего проекта:

  • Используйте SSE, когда данные текут в одну сторону: от сервера к браузеру.
  • Поставьте слой кэширования перед базой данных. Один запрос в несколько секунд лучше, чем тысячи.
  • Ограничивайте время SSE-соединения менее чем минутой и позволяйте браузеру переподключаться.
  • Закрывайте потоки при скрытии вкладки. Не тратьте живые воркеры на простаивающие соединения.
  • Мониторьте использование PHP-воркеров. Когда вы достигнете предела, перенесите слой стриминга на Go.