Обзор архитектуры

OxPHP — это HTTP-сервер в виде единого бинарного файла, который заменяет традиционный стек nginx + PHP-FPM. Он берёт на себя разбор HTTP, терминирование TLS, маршрутизацию, выполнение PHP, сжатие и наблюдаемость в одном процессе, без внешних зависимостей во время работы.

Как работает OxPHP

OxPHP объединяет два слоя выполнения в одном процессе:

  1. Асинхронный HTTP-слой. Событийно-ориентированный сетевой слой принимает TCP-соединения, выполняет TLS-рукопожатия, разбирает HTTP-запросы и отправляет ответы. Он обрабатывает тысячи одновременных соединений с помощью неблокирующего ввода-вывода, так что один медленный клиент никогда не блокирует другого.
  2. Пул PHP-воркеров. Пул выделенных PHP-воркеров выполняет ваши PHP-скрипты. В стандартном режиме каждый воркер обрабатывает по одному запросу за раз. В режиме воркеров с включённым мультиплексированием файберов один воркер может обслуживать несколько одновременных запросов: когда скрипт вызывает oxphp_sleep() или oxphp_async_await(), файбер уступает поток, и воркер переключается на следующий запрос.
  3. Асинхронный пул (опционально). Отдельные потоки ОС для задач, отправленных через oxphp_async(). Включается установкой ASYNC_WORKERS > 0. Изолирован от пула воркеров, поэтому фоновые задачи не блокируют обработку HTTP-запросов.

Два слоя обмениваются данными через ограниченную очередь. Когда приходит HTTP-запрос, требующий выполнения PHP, асинхронный слой помещает его в очередь. Свободный PHP-воркер забирает его, выполняет скрипт и возвращает ответ асинхронному слою для доставки клиенту.

Такое разделение означает, что сетевой ввод-вывод (приём соединений, чтение заголовков, сжатие ответов, отдача статических файлов) никогда не конкурирует за ресурсы с выполнением PHP. Каждый слой масштабируется независимо.

Пул воркеров

Пул PHP-воркеров определяет, сколько PHP-скриптов может выполняться одновременно. OxPHP поддерживает два режима пула.

Статический пул

Фиксированное число воркеров запускается при старте и остаётся работать на всё время жизни сервера. Это режим по умолчанию.

bash
PHP_WORKERS=8 # exactly 8 workers PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1

Динамический пул

Воркеры масштабируются вверх и вниз в зависимости от нагрузки. Укажите минимальное и максимальное количество через двоеточие:

bash
PHP_WORKERS=2:16 # start with 2, scale up to 16 under load

Когда все текущие воркеры заняты, OxPHP создаёт новые воркеры вплоть до максимума. Когда воркер простаивает дольше, чем PHP_WORKERS_IDLE_SECONDS (по умолчанию: 30 секунд), он выводится из работы обратно к минимуму.

Очередь и противодавление

Между асинхронным HTTP-слоем и пулом воркеров располагается ограниченная очередь. Её ёмкость по умолчанию равна начальному числу воркеров, умноженному на 128, и может быть переопределена через QUEUE_CAPACITY. Для статического пула начальное число — это настроенное количество воркеров. Для динамического пула (MIN:MAX) начальное число — это минимум.

Когда очередь заполнена (все воркеры заняты и очередь достигла ёмкости), OxPHP немедленно возвращает клиенту ответ 529 Site is Overloaded с заголовком Retry-After. Код состояния 529 (нестандартный, используется Cloudflare и другими) чётко отличает перегрузку от ошибок приложения (500) и обслуживания (503), что упрощает настройку оповещений и балансировщиков нагрузки.

Поток запросов

Каждый запрос проходит через один и тот же конвейер, независимо от того, отдаёт ли он статический файл или выполняет PHP:

graph TD
  Client(["Client"]) --> TLS["TLS termination<br/>(if configured)"]
  TLS --> Parse["HTTP parsing + Request ID"]
  Parse --> Proxy["Trusted proxy resolution<br/>(if TRUSTED_PROXIES set)"]
  Proxy --> Rate["Rate limiting check"]
  Rate --> Route{"Route resolution"}
  Route -->|Static file| Cache["File cache / disk read"]
  Cache --> Compress["Compression + Response headers"]
  Route -->|PHP request| Queue["Bounded queue<br/>(529 if full)"]
  Queue --> Worker["PHP worker executes script"]
  Worker --> Normal["Normal response"]
  Worker --> SSE["SSE streaming (chunked)"]
  Worker --> Early["Early response (finish_request)<br/>+ background work"]
  Compress --> Deliver(["Response to client"])
  Normal --> Deliver
  SSE --> Deliver
  Early --> Deliver
  1. Терминирование TLS. Если настроены TLS_CERT и TLS_KEY, OxPHP обрабатывает TLS напрямую. Отдельный обратный прокси не нужен.
  2. Разбор HTTP и идентификатор запроса. Запрос разбирается, и генерируется уникальный идентификатор запроса (или сохраняется входящий заголовок X-Request-ID).
  3. Разрешение доверенного прокси. Если задан TRUSTED_PROXIES и подключающийся IP является доверенным, OxPHP извлекает реальный IP клиента, протокол и хост из заголовков Forwarded (RFC 7239) или X-Forwarded-*. Разрешённый IP используется на всех последующих шагах, включая ограничение частоты запросов и журналирование доступа. См. Доверенные прокси.
  4. Ограничение частоты запросов. Если задан RATE_LIMIT, IP клиента проверяется по счётчику запросов на каждый IP. Запросы, превышающие лимит, немедленно получают ответ 429 Too Many Requests.
  5. Разрешение маршрута. URL сопоставляется с настроенным режимом маршрутизации (традиционный, framework или SPA). Результатом становится либо статический файл, либо PHP-скрипт, либо 404. Режим воркеров, если он включён, меняет то, как PHP выполняет разрешённый скрипт, но не меняет само разрешение маршрута.
  6. Статические файлы. Отдаются напрямую из кэша в памяти (для часто запрашиваемых файлов) или потоково с диска. OxPHP автоматически добавляет заголовки ETag, Last-Modified и Cache-Control.
  7. Выполнение PHP. Запрос помещается в ограниченную очередь и забирается свободным воркером. Если очередь заполнена, клиент немедленно получает 529.
  8. Сжатие. Текстовые ответы сжимаются с помощью Brotli перед отправкой, когда клиент присылает Accept-Encoding: br (настраивается через COMPRESSION_LEVEL).
  9. Потоковая передача SSE. Если скрипт устанавливает Content-Type: text/event-stream или вызывает oxphp_stream_flush(), OxPHP переключается в режим потоковой передачи: каждый вызов flush() немедленно отправляет клиенту фрагмент без буферизации всего ответа. В режиме воркеров SSE работает совместно с мультиплексированием файберов.
  10. Ранний ответ. Вызов oxphp_finish_request() немедленно отправляет HTTP-ответ клиенту. Скрипт продолжает выполняться в фоне (запись журналов, обновление кэшей, отправка уведомлений), не удерживая соединение открытым.
  11. Доставка ответа. Готовый ответ отправляется обратно по соединению, и если включено журналирование доступа, записывается запись в журнал.

Режим воркеров и стандартный режим

OxPHP поддерживает две модели выполнения PHP:

Стандартный режим (по умолчанию)

Создаёт свежее окружение PHP для каждого запроса. Автозагрузчики, конфигурация и подключения к базе данных инициализируются на каждый запрос и уничтожаются после него. Эта модель совместима со всеми PHP-приложениями «из коробки».

Режим воркеров

Держит процессы PHP живыми между запросами. Ваше приложение проходит бутстрап один раз (загрузка автозагрузчика, конфигурации и установление подключений к базе данных), а затем входит в цикл обработки запросов. Между запросами OxPHP автоматически сбрасывает суперглобальные переменные, буферы вывода и заголовки ответа, сохраняя при этом состояние, полученное при бутстрапе.

Режим воркеров устраняет накладные расходы на запуск при каждом запросе, что может значительно сократить время ответа для приложений на основе фреймворков (Laravel, Symfony и т.д.), где бутстрап обходится дорого.

Чтобы включить режим воркеров, установите WORKER_MODE_ENABLED=true и укажите в ENTRY_FILE PHP-скрипт, вызывающий oxphp_worker():

php
<?php require __DIR__ . '/../vendor/autoload.php'; $app = new MyApp\Application(); oxphp_worker(function () use ($app) { $app->handle(); });

Подробное руководство см. в разделе Режим воркеров.

Внутренний сервер

Если задана переменная INTERNAL_ADDR, OxPHP запускает отдельный HTTP-сервер на указанном порту. Он обслуживает три эндпоинта:

Эндпоинт Описание
GET /health Статус работоспособности в формате JSON (аптайм, счётчики запросов, соединения, состояние воркеров). Возвращает 200 при нормальной работе, 503 при деградации.
GET /metrics Метрики в формате Prometheus — счётчики запросов, время ответа, время ожидания в очереди, статистика воркеров, экономия от сжатия.
GET /config Снимок активной конфигурации в формате JSON. Пути к файлам TLS скрыты.

Внутренний сервер не проходит через пул PHP-воркеров или ограниченную очередь. Он отвечает напрямую из асинхронного HTTP-слоя, поэтому остаётся доступным даже когда пул PHP полностью загружен. Это делает /health пригодным для проб liveness/readiness в Kubernetes.

Подробнее см. в разделе Внутренний сервер.

Безопасность

OxPHP предоставляет несколько гарантий, чтобы ваше приложение надёжно работало в продакшене:

  • Изоляция запросов. Если PHP-скрипт падает или вызывает фатальную ошибку, затрагивается только этот единственный запрос. Сервер продолжает нормально обрабатывать все остальные запросы. Упавший воркер автоматически заменяется свежим.
  • Автоматическое пересоздание воркеров. OxPHP следит за работоспособностью всех PHP-воркеров. Если воркер неожиданно умирает, на его месте запускается новый воркер без ручного вмешательства.
  • Защита противодавлением (backpressure). Ограниченная очередь запросов предотвращает перегрузку. Когда сервер достигает предела, новые запросы получают ответ 529 с заголовком Retry-After, вместо того чтобы бесконечно накапливаться в очереди и вызывать каскадные таймауты.
  • Защита от обхода пути (path traversal). Все URL-пути очищаются перед обращением к файловой системе. Попытки обхода через процентное кодирование, сегменты .. и пути, выходящие за пределы корня документа, блокируются.
  • Корректное завершение работы. По сигналу SIGTERM или SIGINT (Ctrl+C) OxPHP прекращает приём новых соединений и ждёт завершения выполняющихся запросов (вплоть до настраиваемого таймаута дренирования) перед выходом.

См. также