Справочник по конфигурации

OxPHP полностью настраивается через переменные окружения. Никаких конфигурационных файлов вести не нужно, а у каждой настройки есть значение по умолчанию, поэтому развёртывание без какой-либо конфигурации работает сразу из коробки.

Булевы значения

Переменные, помеченные как булевы, принимают фиксированный канонический набор значений — без учёта регистра и с обрезкой пробелов:

  • истинные: on, true, 1, yes
  • ложные: off, false, 0, no

Любое непустое значение вне этого набора — например, опечатка вроде ture — приводит к немедленной ошибке при запуске с указанием имени переменной. Так ошибочная конфигурация отлавливается до того, как пойдёт трафик, а не приводит к молчаливому выставлению флага не в ту сторону.

Неустановленная переменная или пустое присваивание (FOO=) откатывается к задокументированному значению по умолчанию. Пустое значение намеренно трактуется как неустановленное: подстановка в Docker Compose / Kubernetes вида FOO=${FOO} даёт FOO=, когда переменной на хосте нет, и это не должно мешать серверу запускаться.

Сервер

Переменная По умолчанию Описание
LISTEN_ADDR 0.0.0.0:80 Адрес и порт основного HTTP-сервера
DOCUMENT_ROOT /var/www/html/public Корневой каталог для отдачи файлов и PHP-скриптов
ENTRY_FILE (не задано) Единственный канонический входной файл. Не задано = прямое отображение файлов. *.php = фронт-контроллер. Не-.php = статический фолбэк (SPA). При WORKER_MODE_ENABLED=true = bootstrap-скрипт воркера. Разрешается относительно DOCUMENT_ROOT (относительные пути и .. допускаются; абсолютные пути используются как есть). См. Маршрутизация
WORKER_MODE_ENABLED false Включает постоянный режим воркеров. Требует, чтобы ENTRY_FILE указывал на .php-скрипт. Булево — см. Булевы значения
MAX_CONNECTIONS 10000 Максимальное число одновременных TCP-соединений. Также потолок для QUEUE_MAX_WAITING по умолчанию (его половина), поэтому некорректное значение — ошибка при запуске, а не молчаливый откат. Его понижение не сдвигает значение по умолчанию QUEUE_CAPACITY, которое рассчитывается только из числа воркеров, — держите его выше PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING, см. Сохранение запаса для цикла приёма
TOKIO_WORKERS CPU / 2 (мин. 1) Потоки асинхронного ввода-вывода. 1 = однопоточный режим, N > 1 = фиксированное число потоков, не задано = автоматически (CPU / 2, мин. 1)

PHP-воркеры

Переменная По умолчанию Описание
EXECUTOR sapi Бэкенд-исполнитель PHP. sapi — исполнение PHP, stub — для бенчмаркинга без PHP
PHP_WORKERS CPU / 2 (мин. 1) Размер пула воркеров. N = фиксированный пул, MIN:MAX = динамическое масштабирование, 0 = автоматически
PHP_WORKERS_IDLE_SECONDS 30 Сколько секунд динамический воркер остаётся простаивающим, прежде чем будет выведен из пула (только в динамическом режиме). Воркер выводится в момент, когда у него нет ничего в обработке, поэтому ничто из обслуживаемого им не обрывается — а воркер, удерживающий запрос, который никогда не завершается, например открытый поток, такого момента не достигает и не выводится вовсе
QUEUE_CAPACITY Начальное число воркеров × 128 Максимальное число ожидающих запросов в очереди PHP. Запросы, пришедшие в заполненную очередь, ждут слота (см. QUEUE_WAIT_TIMEOUT_MS). Для динамических пулов (MIN:MAX) начальное число воркеров = минимальное значение. 0 = автоматически
QUEUE_WAIT_TIMEOUT_MS 1000 Сколько запрос может провести в ожидании PHP-воркера, прежде чем получит 529. Один дедлайн, проставляемый при прибытии и покрывающий оба ожидания, которые могут выпасть запросу: слота в очереди и воркера внутри очереди. Запрос, до которого воркер добирается после истечения дедлайна, отклоняется при заборе, а не выполняется, поэтому бюджет ограничивает всё ожидание, а не только его половину до допуска, — но не ограничивает, сколько затем работает обработчик. Одновременно ждут не более QUEUE_MAX_WAITING запросов; сверх этого запросы отклоняются немедленно. 0 = отклонять немедленно, как только очередь заполнена. Понижайте его или ставьте 0, когда приложение по HTTP обращается обратно к этому же серверу (внутренний вызов не может быть допущен, пока внешний не освободит своего воркера, так что ожидание тратится впустую), или когда у балансировщика нагрузки впереди собственный таймаут короче. Клиент, закрывший соединение посреди ожидания, немедленно возвращает своё место — как на HTTP/1.1, так и на HTTP/2, — поэтому балансировщик, который отваливается по таймауту и закрывает соединение, не заполняет множество ожидающих уже брошенными им попытками. Чего сервер не видит — это клиента, переставшего ждать без закрытия: тот держит своё место, пока не будет допущен или пока не истечёт бюджет, и если воркер освободится раньше, его скрипт отработает впустую, — так что держать бюджет ниже таймаута всего, что стоит впереди, всё равно стоит
QUEUE_MAX_WAITING Начальное число воркеров × 128, не более MAX_CONNECTIONS / 2 Максимум запросов, одновременно припаркованных в ожидании слота очереди. Сверх него запросы отклоняются немедленно, а не ждут. Каждый ожидающий удерживает соединение и полностью буферизованное тело запроса всё время ожидания, поэтому это граница удерживаемых ресурсов, а не ожиданий, которые окупятся. Потолок MAX_CONNECTIONS / 2 на значении по умолчанию ограничивает только эту часть затора; запросы в очереди и выполняющиеся удерживают соединение точно так же, поэтому запас, который сервер реально сохраняет для приёма и отказов, следует из PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING против MAX_CONNECTIONS — см. ниже. Подбирайте его из задержки собственного обработчика — тоже ниже. 0 = автоматически, никогда не ниже 1
QUEUE_MAX_WAITING_BYTES 67108864 (64 МиБ) Максимум байтов тел запросов, которые припаркованные запросы могут удерживать в сумме. Запрос, чьё тело вытолкнуло бы сумму за предел, отклоняется с 529 немедленно, а не ждёт; запрос без тела по этой причине не отклоняется никогда. QUEUE_MAX_WAITING ограничивает то же множество в запросах, что ничего не говорит об их размере — тела буферизуются целиком до того, как запрос достигает очереди, так что с одним лишь счётчиком в качестве преграды припаркованное множество может держать QUEUE_MAX_WAITING × 10 МиБ. Поднимайте его для приложения с обилием загрузок, которое должно поглощать всплески больших тел, а не сбрасывать их; понижайте в контейнере с жёстким лимитом памяти. 0 = автоматически

Подбор размера множества ожидающих

Сколько запросов могут ждать с пользой, следует из того, как быстро пул их разбирает. При W воркерах и обработчике, занимающем T миллисекунд, пул допускает W / T запросов в миллисекунду, поэтому бюджет в B миллисекунд может впустить около W × B / T ожидающих. Всё сверх этого ждёт весь бюджет и всё равно получает отказ, удерживая всё это время соединение и буферизованное тело запроса.

Значение по умолчанию вычислить это не может — скорость обслуживания при запуске неизвестна, — поэтому оно намеренно щедрое. Это подходит быстрым обработчикам, где пул разбирает глубокий затор задолго до конца бюджета, и слишком велико для медленных: при 8 воркерах, бюджете по умолчанию в 1 с и обработчике на 200 мс вовремя могут быть допущены лишь около 40 запросов, тогда как умолчание паркует до 1024.

bash
QUEUE_MAX_WAITING=40

Установка значения около W × B / T превращает излишек в немедленный 529 вместо 529 секундой позже. Сокращение QUEUE_WAIT_TIMEOUT_MS достигает того же через другой множитель. Эти два размениваются друг на друга, и на медленном обработчике меньший бюджет обычно лучший рычаг, потому что он ещё и ограничивает, как долго клиент ждёт отказа.

Множество ограничено и второй раз — в байтах. Каждый ожидающий держит тело своего запроса в памяти всё время ожидания, поэтому предел, считаемый в запросах, оставляет удерживаемую ими память на волю трафика: те же 1024 ожидающих ничего не стоят на GET без тел и обходятся в гигабайты на загрузках. QUEUE_MAX_WAITING_BYTES ограничивает эту сумму напрямую — за его пределами запрос с телом отклоняется сразу, а не паркуется, тогда как запросы без тела продолжают ждать как обычно. Приложению с обилием загрузок, которое должно поглощать всплески, а не сбрасывать их, нужно значение больше; контейнеру с жёстким лимитом памяти — меньше. Отказы по каждому из пределов считаются в oxphp_admission_refused_total отдельно (waiting_full — по счётчику, waiting_bytes — по памяти), так что метрика сама называет ручку, за которую браться.

Сохранение запаса для цикла приёма

Запрос удерживает своё соединение — и одно из разрешений MAX_CONNECTIONS — с момента прибытия и до момента ответа, чем бы он в промежутке ни занимался. Это покрывает три отдельные популяции, потому что слот очереди освобождается в момент, когда воркер забирает запрос, ещё до запуска скрипта:

  • выполняющиеся в воркере: как минимум PHP_WORKERS, а в режиме воркеров больше — там один поток мультиплексирует файберы;
  • в очереди, до QUEUE_CAPACITY;
  • припаркованные на допуске, до QUEUE_MAX_WAITING.

Только у третьей есть потолок, выведенный из MAX_CONNECTIONS. Держите PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING ниже MAX_CONNECTIONS. За этой чертой режим отказа меняется к худшему: цикл приёма берёт разрешение на соединение до того, как начнёт его обслуживать, поэтому, когда путь PHP разберёт их все, цикл встаёт, и клиент, пришедший в этот момент, не получает вообще никакого ответа вместо 529 — а этого балансировщик нагрузки не отличит от мёртвого экземпляра, тогда как проба работоспособности на INTERNAL_ADDR остаётся зелёной, потому что не проходит ни через один из этих лимитов.

Сервер проверяет это при запуске и предупреждает, когда сумма достигает бюджета, называя каждое значение; oxphp config --check сообщает то же самое, не меняя ни вердикта, ни кода выхода. Считайте устранение предупреждения необходимым, но не достаточным: простаивающие keep-alive-соединения, запросы статических файлов и рукопожатия в процессе тоже удерживают разрешения, и ничего из этого при запуске не подсчитать.

Обычный путь к предупреждению — понижение MAX_CONNECTIONS, потому что умолчание QUEUE_CAPACITY рассчитывается из числа воркеров и вслед за ним не опускается — 7 воркеров и MAX_CONNECTIONS=1000 дают 7 + 896 + 500 против бюджета 1000. Обратите внимание, за какую ручку браться: повышение одного лишь MAX_CONNECTIONS предупреждение не убирает, пока QUEUE_MAX_WAITING остаётся на умолчании, — ведь это умолчание равно половине MAX_CONNECTIONS и растёт вместе с ним: при 7 воркерах сумма устаканивается на 1799, и условие снимается только с MAX_CONNECTIONS=1800 и выше. Понизьте QUEUE_CAPACITY или задайте QUEUE_MAX_WAITING явно, а уже потом поднимайте бюджет.

Для динамического пула (MIN:MAX) слагаемое выполняющихся — это минимум, то же число, из которого рассчитаны умолчания очереди, поэтому пул, выросший до максимума, удерживает больше, чем говорит сумма.

Сравнение считает соединения, тогда как бюджет расходуется запросами, поэтому развёртывание с преобладанием HTTP/2 может законно находиться выше него: одно соединение несёт до H2_MAX_CONCURRENT_STREAMS запросов, так что тот же затор удерживается долей соединений. Предупреждение там ожидаемо. Большой пул на стандартном бюджете тоже до него добирается — автоматически подобранный пул из 39 воркеров даёт 39 + 4992 + 4992 против 10 000, — и вот на этот случай стоит реагировать, а не игнорировать его.

Статические и динамические воркеры

Укажите в PHP_WORKERS одно число для фиксированного пула:

bash
PHP_WORKERS=8 # Fixed 8 workers PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)

Укажите в PHP_WORKERS значение MIN:MAX для автоматического масштабирования:

bash
PHP_WORKERS=2:16 # Scale between 2 and 16 workers PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2) PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum

В динамическом режиме OxPHP увеличивает число воркеров, когда все они заняты, и уменьшает его, когда воркеры простаивают дольше PHP_WORKERS_IDLE_SECONDS.

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

Переменная По умолчанию Описание
WORKER_MAX_MEMORY_MIB 0 Максимальный объём памяти в MiB на один воркер до его пересоздания. 0 = без ограничения

Установите WORKER_MODE_ENABLED=true и укажите в ENTRY_FILE bootstrap-скрипт воркера (например, ENTRY_FILE=worker.php или ENTRY_FILE=../worker.php). PHP-процессы тогда остаются живыми между запросами, сохраняя в памяти состояние инициализации (автозагрузчики, соединения с базой данных). Воркеры пересоздаются автоматически, когда превышают WORKER_MAX_MEMORY_MIB, или по требованию, когда приложение вызывает Worker::scheduleExit(). Параметр WORKER_MAX_REQUESTS из ранних выпусков объявлен устаревшим и игнорируется — не задавайте ни его, ни аналог, либо перейдите на Worker::scheduleExit().

Устаревшие: INDEX_FILE и WORKER_FILE

Устаревшие переменные INDEX_FILE и WORKER_FILE по-прежнему разбираются ради обратной совместимости. Если они заданы, при запуске выводится строка лога уровня WARN, и они отображаются на новую модель:

Устаревшая форма Современный эквивалент
INDEX_FILE=index.php ENTRY_FILE=index.php
INDEX_FILE=index.html ENTRY_FILE=index.html
WORKER_FILE=/path/worker.php WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php

Если заданы и старые, и новые переменные, приоритет за ENTRY_FILE / WORKER_MODE_ENABLED. Переходите на них, когда вам удобно; устаревшие формы будут удалены в одном из будущих выпусков.

SAPI / PHP

Переменная По умолчанию Описание
SUPERGLOBALS_ENABLED true Заполняет суперглобальные переменные PHP ($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, php://input) перед выполнением скрипта. Установите ложное значение, чтобы пропустить заполнение — тогда данные запроса доступны только через объектный API (oxphp_http_request()). Полезно для приложений, которые работают напрямую с объектным API и хотят избежать затрат на построение суперглобальных переменных на каждом запросе

Таймауты

Переменная По умолчанию Описание
HEADER_TIMEOUT_SECONDS 5 Максимальное число секунд на приём HTTP-заголовков после установки соединения (защита от Slowloris)
DRAIN_TIMEOUT_SECONDS 25 Максимальное число секунд ожидания завершения текущих соединений при корректном завершении работы

Время выполнения PHP ограничивается собственной ini-директивой PHP max_execution_time (и вызовом set_time_limit() во время выполнения), а не переменной окружения OxPHP.

Ограничение частоты запросов

Переменная По умолчанию Описание
RATE_LIMIT 0 (выкл.) Максимальное число запросов с одного IP за временное окно. 0 отключает ограничение частоты запросов
RATE_WINDOW_SECONDS 60 Длительность окна ограничения частоты запросов в секундах

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

Переменная По умолчанию Описание
FRAME_OPTIONS SAMEORIGIN Защита от кликджекинга. SAMEORIGIN разрешает встраивание только страницам того же источника, DENY запрещает всякое встраивание, off отключает защиту (используйте, когда управляете встраиванием через собственный CSP). Любое другое значение откатывается к умолчанию SAMEORIGIN с предупреждением при запуске. Устанавливает одновременно X-Frame-Options и Content-Security-Policy: frame-ancestors на каждом ответе. Значения выдаваемых заголовков, то, как серверные заголовки уступают заданным приложением, и рекомендации по выбору значения — в разделе Защита от кликджекинга ниже
TRUSTED_PROXIES (не задано) Сети доверенных обратных прокси (CIDR через запятую или private). Если задано, OxPHP извлекает реальный IP клиента из заголовков Forwarded (RFC 7239) или X-Forwarded-For по алгоритму «крайний правый недоверенный». Также обрабатывает X-Forwarded-Proto и X-Forwarded-Host для $_SERVER['HTTPS'], REQUEST_SCHEME, SERVER_NAME и SERVER_PORT. Не задано = функция отключена
PHP_DENY_PATHS (не задано) Glob-шаблоны через запятую, чьи .php-файлы никогда не должны выполняться по прямому URI (например, /uploads/**,/cache/**,/admin/legacy.php). Шаблоны могут указывать на целые каталоги или отдельные файлы. Применяется в режимах прямого отображения — Traditional и SPA; в режимах Framework и Worker, которые никогда не выполняют произвольные .php-файлы напрямую, игнорируется с предупреждением при запуске. Также охватывает скрипты, доступные через разрешение индексного файла каталога (/uploads/uploads/index.php). Для прямых .php-URI сопоставление происходит до обращения к диску, поэтому запрещённые пути дают одинаковый ответ независимо от того, существует файл или нет (без «оракула существования»). Устаревшее имя PHP_DENY_DIRS принимается как устаревший псевдоним и вызывает WARN при запуске. См. Дени-лист исполнения PHP
PHP_DENY_FALLBACK 404 Что возвращать при совпадении с PHP_DENY_PATHS. Либо HTTP-статус 400599 (в паре с ERROR_PAGES_DIR для собственного HTML), либо URI-путь с ведущим / к PHP-скрипту-фолбэку внутри DOCUMENT_ROOT. Скрипт-фолбэк получает OXPHP_DENIED_PATH и OXPHP_DENIED_PATTERN в $_SERVER. Проверяется при запуске: скрипт должен существовать, канонизироваться внутри DOCUMENT_ROOT и сам не должен совпадать с PHP_DENY_PATHS (для предотвращения зацикливания)
SYMLINK_ALLOW_PATHS (не задано) Список абсолютных путей через запятую, под которыми символическим ссылкам разрешено выходить за пределы DOCUMENT_ROOT. Каждый элемент должен уже существовать на диске; относительные и несуществующие пути прерывают запуск. Не задано = выход по символическим ссылкам запрещён. См. Разрешённые пути для симлинков

Специальное значение private разворачивается во все частные сети RFC-1918, loopback- и link-local-адреса (IPv4 и IPv6): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10.

Защита от кликджекинга

Кликджекинг — атака, при которой враждебная страница встраивает ваш сайт в невидимый <iframe> и обманом заставляет пользователя нажать то, чего он не видит: кнопку «да, удалить мой аккаунт», покупку в один клик, запрос авторизации OAuth. Защита — сказать браузеру, кому (и разрешено ли кому-либо вообще) встраивать ваши страницы во фреймы. Этим управляет FRAME_OPTIONS.

Фреймингом управляют два заголовка — устаревший X-Frame-Options (понимаемый всеми браузерами) и современный Content-Security-Policy: frame-ancestors (вытесняющий первый там, где присутствуют оба), — поэтому OxPHP выдаёт оба, и политика действует одинаково в старых и новых браузерах. Одно значение FRAME_OPTIONS отображается в согласованную пару:

FRAME_OPTIONS X-Frame-Options Content-Security-Policy Кто может встраивать ваши страницы
SAMEORIGIN (по умолчанию) SAMEORIGIN frame-ancestors 'self' Только страницы того же источника
DENY DENY frame-ancestors 'none' Никто, даже ваши собственные страницы
off (не отправляется) (не отправляется) Кто угодно — сервер не ограничивает встраивание

Выбор значения. SAMEORIGIN — умолчание: он блокирует межсайтовое встраивание, на которое кликджекинг и опирается, но позволяет вашим собственным страницам встраивать друг друга — что многие приложения законно делают (предпросмотры в админке, виджеты дашбордов, платёжные компоненты на том же источнике). Выбирайте DENY, когда ничто на вашем сайте не должно встраиваться никогда, даже им самим, — для самой строгой позиции. Выбирайте off только тогда, когда управляете фреймингом сами через полный Content-Security-Policy, устанавливаемый вашим приложением, — см. ниже.

Встраивание с внешних источников. Ни одно значение X-Frame-Options не может назвать конкретный разрешённый источник (ALLOW-FROM удалён из стандарта). Чтобы позволить именованной третьей стороне встраивать ваши страницы, установите FRAME_OPTIONS=off и заставьте приложение выдавать собственный Content-Security-Policy с явным списком frame-ancestors, например header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");.

Заголовки приложения побеждают. Серверные заголовки — фолбэки, применяемые только когда в ответе такого заголовка нет: приложение, устанавливающее собственный X-Frame-Options или Content-Security-Policy через PHP header(), сохраняет его нетронутым. Два заголовка фрейминга трактуются как одна политика, поэтому сервер никогда не противоречит приложению:

  • Если приложение установило X-Frame-Options, OxPHP пропускает свой фолбэк frame-ancestors (серверный CSP переопределил бы выбор приложения в современных браузерах).
  • Если приложение установило Content-Security-Policy, содержащий директиву frame-ancestors, OxPHP пропускает свой фолбэк X-Frame-Options (более строгий серверный X-Frame-Options заблокировал бы лишнее в устаревших браузерах, игнорирующих CSP).

Тот же приоритет действует для X-Content-Type-Options, который OxPHP устанавливает в nosniff на каждом ответе: заданное приложением значение сохраняется дословно. Учтите, что nosniff — единственное значение, которое что-то делает: приложение, переопределившее его чем-то другим, молча отключает защиту от MIME-сниффинга.

TLS

Переменная По умолчанию Описание
TLS_CERT (не задано) Путь к TLS-сертификату в кодировке PEM. Для включения TLS должны быть заданы и TLS_CERT, и TLS_KEY
TLS_KEY (не задано) Путь к закрытому TLS-ключу в кодировке PEM
TLS_MIN_VERSION 1.2 Минимальная принимаемая версия протокола TLS: 1.2 или 1.3. Проверяется при запуске (и командой oxphp config --check) даже когда TLS не включён — любое другое значение, включая последовательности байтов вне UTF-8, приводит к жёсткой ошибке при запуске. Пустое значение трактуется как неустановленное

HTTP/2

Переменная По умолчанию Описание
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (мин. 32) Максимальное число одновременно открытых потоков на одно HTTP/2-соединение
H2_MAX_PENDING_RESET 20 Максимальное число кадров RST_STREAM в очереди до закрытия соединения (защита от Rapid Reset)
H2_MAX_HEADER_LIST_BYTES 65536 Максимальный суммарный размер декодированных заголовков на один запрос в байтах
H2_KEEPALIVE_INTERVAL_SECS 20 Число секунд между HTTP/2-кадрами PING; 0 отключает
H2_KEEPALIVE_TIMEOUT_SECS 10 Число секунд ожидания ответа на PING до закрытия соединения

Статические файлы

Переменная По умолчанию Описание
STATIC_MAX_AGE 30d Cache-Control: max-age для статических файлов. Принимает: 30s, 5m, 2h, 30d, 1w, 1y, число секунд без суффикса (3600) или off для отключения заголовка. Заменяет устаревшую STATIC_CACHE_TTL.
STATIC_REVALIDATE off Булево — см. Булевы значения. Установите истинное значение, чтобы включить ревалидацию по mtime для кэша содержимого в памяти: время изменения файла перепроверяется не чаще одного раза в 3 секунды на файл (а не на запрос), а устаревшие записи вытесняются автоматически, так что изменения становятся видны в течение 3 секунд. Заменяет устаревшую STATIC_CACHE (где off имело обратный смысл).
COMPRESSION_LEVEL 4 Качество сжатия Brotli (0–11). 0 отключает сжатие

Логирование

Переменная По умолчанию Описание
LOG_LEVEL info Уровень детализации логов: trace, debug, info, warn, error
ACCESS_LOG (не задано) Журнал доступа по каждому запросу: all = каждый запрос, error = только 4xx/5xx, не задано = выключено
Note

ACCESS_LOG принимает all или error. Оставьте её неустановленной, чтобы полностью отключить журнал доступа.

Наблюдаемость

Переменная По умолчанию Описание
INTERNAL_ADDR (не задано) Адрес внутреннего сервера (/health, /metrics, /config). Если не задано, внутренний сервер не запускается. Значение только с портом (:9090 или 9090) привязывается к 127.0.0.1; чтобы открыть его наружу, укажите явный 0.0.0.0:9090
INTERNAL_ALLOW_IPS (не задано) Список разрешений в формате CIDR/IP через запятую для внутреннего сервера. Узел вне списка получает 403 на /metrics, /config и путях плагинов; проверки работоспособности (/health, /healthz, /readyz, /startupz и их длинные формы) остаются доступны. Не задано/пусто = разрешены все. Loopback не подразумевается неявно — укажите 127.0.0.1/32, чтобы сохранить доступ с localhost. Некорректный список прерывает запуск
ERROR_PAGES_DIR (не задано) Каталог с пользовательскими страницами ошибок с именами {status}.html (например, 404.html, 503.html)
MAX_QUERY_BODY 524288 Максимальный размер тела запроса в байтах для внутренних query-эндпоинтов (512 KiB)
TRACE_CONTEXT false Булево — см. Булевы значения. Если истинно, включает распространение W3C Trace Context: читает заголовки traceparent/tracestate и передаёт их в PHP через $_SERVER

OpenTelemetry

Переменная По умолчанию Описание
OTEL_ENABLED false Включает экспорт спанов OpenTelemetry. Автоматически устанавливает TRACE_CONTEXT=true. Булево — см. Булевы значения
OTEL_EXPORTER_OTLP_PROTOCOL grpc Протокол экспорта: grpc или http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) или http://localhost:4318 (HTTP) Эндпоинт OTLP-коллектора
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Таймаут экспорта в миллисекундах
OTEL_EXPORTER_OTLP_HEADERS (не задано) Заголовки аутентификации: key=value,key2=value2
OTEL_SERVICE_NAME oxphp Имя сервиса в экспортируемых спанах
OTEL_SERVICE_VERSION (не задано) Атрибут версии сервиса
OTEL_RESOURCE_ATTRIBUTES (не задано) Дополнительные атрибуты ресурса: env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio Стратегия сэмплирования: always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 Доля сэмплирования (0.0–1.0) для семплеров на основе отношения
Note

Недопустимые или выходящие за диапазон значения OTEL_TRACES_SAMPLER_ARG ограничиваются интервалом [0.0, 1.0] и логируются на уровне warn. Неизвестные значения OTEL_TRACES_SAMPLER откатываются к parentbased_traceidratio и логируются.

APM

Переменная По умолчанию Описание
OTEL_APM_ENABLED false Включает APM: автоматическую инструментацию, захват ошибок и PHP-SDK трассировки. Требует OTEL_ENABLED=true. Булево — см. Булевы значения
OTEL_APM_SLOW_QUERY_MS 100 Порог медленного запроса в миллисекундах. Запросы к базе данных, превышающие его, получают атрибут спана oxphp.db.slow=true
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false Записывает связанные параметры в атрибут спана db.params. Отключите в продакшене, если параметры могут содержать чувствительные данные. Булево — см. Булевы значения
OTEL_APM_STACKTRACE_MAX_BYTES 8192 Максимальный размер атрибута exception.stacktrace в байтах. При превышении предела стектрейс обрезается с конца с маркером …(truncated). 0 отключает обрезку
OTEL_APM_MESSAGE_MAX_BYTES 4096 Максимальный размер атрибута exception.message в байтах (значение по умолчанию совпадает с ограничением New Relic на значение одного атрибута). При превышении предела сообщение обрезается с конца с маркером …(truncated). 0 отключает обрезку

Когда APM включён, OxPHP автоматически перехватывает 34 внутренние функции PHP (PDO, mysqli, cURL, Redis, Memcached, файловый ввод-вывод), чтобы создавать дочерние спаны. PHP-функции oxphp_apm_*() регистрируются независимо от того, включён ли APM — когда он выключен, они являются безопасными no-op-заглушками.

Асинхронные воркеры

Переменная По умолчанию Описание
ASYNC_WORKERS 0 (отключено) Число выделенных потоков асинхронных воркеров. При 0 асинхронные функции (oxphp_async и т. д.) регистрируются, но при вызове выбрасывают OxPHP\Async\AsyncException. Установите положительное значение, чтобы включить фоновое выполнение задач
ASYNC_QUEUE_CAPACITY ASYNC_WORKERS × 64 Максимальное число ожидающих задач в асинхронной очереди. 0 = автоматически (воркеры × 64)
ASYNC_MAX_FIBERS 256 Ограничение на число одновременных файберов асинхронных задач на один воркер. Глобальный для процесса лимит выполняющихся (в очереди + запущенных) равен ASYNC_MAX_FIBERS × ASYNC_WORKERS; диспетчеризация сверх него немедленно отклоняется с OxPHP\Async\AsyncException, чтобы композиция с fan-out не могла привести к взаимоблокировке

Пул асинхронных воркеров обрабатывает фоновые задачи типа «запустил и забыл», диспетчеризуемые из PHP. Он отделён от пула PHP-воркеров и не требуется для обработки обычных запросов.

Некорректное значение любой из этих трёх переменных (например, ASYNC_WORKERS=8x) приводит к ошибке при запуске — откат к значению по умолчанию молча отключил бы или неправильно настроил пул. Строго пустое значение трактуется как неустановленное.

Хуки времени выполнения

Переменная По умолчанию Описание
RUNTIME_HOOKS (отключено) Включаемая по желанию замена блокирующих встроенных функций PHP реализациями, приостанавливающими файбер. 1/true/all включает все категории хуков; список через запятую включает отдельные категории (например, RUNTIME_HOOKS=sleep,streams)
Категория Что перехватывает
sleep Нативные sleep() и usleep() приостанавливают текущий файбер ровно как oxphp_sleep()/oxphp_usleep()
streams Два ожидания приостанавливают текущий файбер вместо удержания потока воркера: блокирующее чтение из сокет-потока tcp:// и stream_select(). Чтение покрывает клиентов, блокирующихся на одном сокете, — fsockopen(), stream_socket_client(), HTTP-обёртки потоков, mysqlnd (PDO_MySQL, mysqli), phpredis; stream_select() покрывает циклы, ждущие несколько сокетов сразу. В обоих случаях без изменений кода. Клиенты, ждущие каким-то иным способом, не затрагиваются (см. ниже)

Хуки действуют внутри файберов запросов режима воркеров и файберов асинхронных задач. Вне файбера (контекст запроса traditional/framework/SPA, CLI) сохраняется исходное нативное поведение, включая ошибки валидации аргументов. С включёнными хуками sleep сторонний код, вызывающий sleep(), перестаёт удерживать поток воркера — изменения кода не нужны. Отмена асинхронной задачи во время перехваченного sleep разматывает её через OxPHP\Async\AsyncException, а перехваченный sleep() всегда возвращает 0 (возвращаемое значение нативной функции при прерывании сигналом не возникает).

Что покрывает хук streams

Кооперативными хук streams делает блокирующее чтение из PHP-сокет-потока и ожидание внутри stream_select(). Прежде чем на него рассчитывать, проверьте, что ваш клиент ждёт одним из этих двух способов. Несколько распространённых ждут иначе, и для них ничего не меняется:

  • ext/curl. curl_exec() и curl_multi_* общаются с сокетами сами, ниже уровня PHP-потоков, поэтому хук их никогда не видит. Это касается каждого HTTP-клиента, построенного на curl, — в том числе Guzzle с его обработчиком по умолчанию. Curl-клиент можно направить на обработчик через обёртку потоков — тот через PHP-потоки проходит.
  • socket_select(). Это ext/sockets, другой API на сырых дескрипторах, и он не перехватывается. stream_select() — перехватывается. То же относится к ожиданию внутри stream_socket_accept(), которое тоже не перехватывается.
  • Потоки из socket_export_stream(). Они несут другую таблицу операций, приватную для PHP, которую нельзя пропатчить из расширения.
  • Потоки unix://, udp:// и udg://. По той же причине: их таблицы операций приватны для PHP.
  • ssl:// и tls:// после активации шифрования. Пока stream_socket_enable_crypto() не завершилась успешно, чтения SSL-потока делегируются чтению обычного сокета и потому приостанавливают файбер; начиная с рукопожатия — нет.
  • Установка соединения и разрешение DNS. Клиент, держащий соединение открытым, выигрывает на каждом запросе; установка соединения — нет.
  • MySQL, достигаемый через localhost. Клиент MySQL читает localhost как «использовать unix-сокет», а это не tcp://-поток — пишите в DSN 127.0.0.1. Это касается и PDO_MySQL, и mysqli, и это легко упустить, потому что всё продолжает работать — только без выигрыша.
  • Записи. Ждёт только чтение. Готовность к чтению сохраняется, пока дескриптором владеет один файбер и никто другой его не вычитывает, тогда как место в буфере отправки выдаётся и отбирается пиром, поэтому файбер, разбуженный по возможности записи, может обнаружить, что окно снова закрылось к моменту выполнения записи, — после чего PHP в любом случае блокирует поток на весь её таймаут. Запись, заполняющая буфер сокета, ведёт себя поэтому ровно так же, как и без хука. На практике это стоит немного: время занимает ожидание ответа, а не передача запроса ядру. (На записи всё же смотрят — ради одного: чей обмен соединение сейчас несёт, — см. примечание об общих соединениях ниже. На соединении, которым не пользуется никакой другой файбер, — а это каждое соединение в обычном случае, — запись идёт нетронутым нативным путём.)

В этих рамках хук сохраняет нативный контракт: таймауты сокетов (stream_set_timeout(), default_socket_timeout) действуют без изменений, чтение, вышедшее по таймауту, по-прежнему сообщает timed_out через stream_get_meta_data(), и идентичность потока не затронута, так что socket_import_stream() и подобные продолжают работать. Одно ограничение на таймаут: дедлайн проверяется лишь раз за тик планировщика, поэтому срабатывает не раньше следующего тика — в лучшем случае 100 мкс в режиме воркеров и 50 мкс в асинхронном пуле, а под нагрузкой дольше, поскольку тик длится столько, сколько выполняется текущая работа воркера. Асинхронный пул, когда ничего не происходит, делает паузы длиннее этого, но не дальше удерживаемого им дедлайна: дедлайн чтения или записи укорачивает паузу так, чтобы она пришлась на него самого, — так что более долгое ожидание не делает таймаут позже. При default_socket_timeout в 60 секунд это не то, что большинство развёртываний способно заметить.

stream_select() перехватывается ожиданием на дескрипторах, названных тремя её массивами, с последующей передачей вызова PHP с нулевым таймаутом, так что всё наблюдаемое по-прежнему решает PHP: возвращаемый счётчик, перезапись массивов до готовых потоков, предупреждения и ошибки аргументов. Ожидание пропускается — и вызов выполняется ровно так, как без хука, — всякий раз, когда в массивах есть что-то, за что хук не будет ручаться: поток на чтение с уже забуференными данными (stream_select() отвечает на такой из буфера, не глядя на дескриптор), поток вовсе без дескриптора, элемент, не являющийся живым потоком (скажем, закрытый, оставленный в массиве, — PHP отвечает на это ошибкой, а не ожиданием), дескриптор, за готовностью которого ядро вообще не следит (обычный файл — типичный случай, и он считается готовым в момент запроса), или дескриптор на границе FD_SETSIZE и дальше, который собственный select() PHP отвергает сразу. Последнее — реальный потолок, а не формальность: занятый воркер может держать больше 1024 открытых дескрипторов, и stream_select(), называющий один из них, падает одинаково с хуком и без.

Общие соединения между конкурентными файберами

Одно соединение, разделяемое конкурентными файберами, безопасно для клиентов, которых OxPHP охраняет, и ничего не выигрывает. Это нормальная форма приложения в режиме воркеров, а не крайний случай: WordPress, Laravel и Symfony открывают клиентов базы данных и кэша один раз при загрузке воркера и выдают их каждому запросу, и, кроме как переписав их слой доступа к данным, от них не добиться одного соединения на файбер.

Протокол клиента — это последовательность обменов: записать команду, прочитать ответ, — и ничто на соединении не отмечает, где заканчивается один обмен. Файбер, вставший на чтении, стоит посреди обмена, и команда второго файбера, приземлившаяся туда, ломает протокол. Два клиента ломаются на этом по-разному, и оба нехорошо: mysqlnd отслеживает состояние своего соединения и отклоняет команду, ничего не отправив, тогда как у phpredis такой проверки нет, и два файбера читают ответы друг друга — данные одного запроса возвращаются другому без единой ошибки.

Поэтому файбер занимает соединение перед использованием — на обоих уровнях, где это должно происходить: в операциях сокета, которые сохраняют порядок байтов, и во входных точках клиентов PDO и mysqli, поскольку отказ mysqlnd происходит до всякого ввода-вывода и на уровне сокета ничего нельзя успеть сделать, чтобы его предотвратить. Другой файбер, добравшийся до того же соединения, ждёт его освобождения на уровне клиента, где не удерживается ничего, кроме идентичности соединения; на уровне сокета он не ждёт, а получает отказ — так же, как отказывает таймаут сокета, — потому что файбер, приостановленный внутри операции над чужим потоком, держал бы указатель, который владелец может освободить. phpredis по той же причине охраняется тоже на уровне клиента, метод за методом. На уровне клиента занимается само соединение, а не PHP-объект, его держащий, поэтому постоянное соединение, достигаемое через несколько объектов PDO, считается одним, а соединение, открытое через PDO::connect() — который возвращает собственный подкласс драйвера, а не PDO, — покрывается как любое другое.

Читайте выигрыш хука, стало быть, как принадлежащий соединениям, которые файбер открывает для себя: асинхронной задаче с собственными HTTP- или БД-вызовами, запросу, открывающему собственного клиента. Что получает общее соединение — это возвращённый на время ожидания поток воркера, чтобы другие запросы могли выполнять работу, не относящуюся к этому соединению; его собственные обмены идут друг за другом, ровно как с выключенным хуком. Четыре границы стоит знать:

  1. Файбер, выполнивший хотя бы один запрос к базе, держит соединение до конца своего HTTP-запроса, потому что конец запроса — первый момент, заведомо лежащий за концом обмена. Он держит его и пока стоит на чём-то другом, поэтому запрос, который обратился к базе и затем ждёт собственной работы, нуждающейся в том же соединении, ждёт сам себя; двое расходятся по границе из следующего пункта, а не продолжаются.
  2. Ожидание всегда ограничено: меньшим из max_execution_time и default_socket_timeout. Не задайте ни того ни другого — и граница составит 30 секунд, поскольку серверный SAPI берёт умолчания движка: 30 у первого и 60 у второго; 60 от default_socket_timeout она составляет только там, где max_execution_time равен 0. max_execution_time читается таким, каким он в данный момент у запроса, поскольку set_time_limit() — это способ запроса заявить, сколько ему можно работать; default_socket_timeout читается таким, с каким процесс стартовал, потому что это дефолтный дедлайн операции с сокетом, и запрос, сузивший его для собственного вызова — обычное дело для библиотеки, как и оставить это после себя, — не должен укорачивать эту границу для запросов, следующих за ним на том же воркере. За границей вызов откатывается на неохраняемое поведение и объясняет причину в журнале сервера: для PDO и mysqli он передаётся клиенту, чей собственный отказ от команды, выданной посреди обмена, — та самая ошибка, которую приложение уже обрабатывает, тогда как для phpredis, у которого такого отказа нет и который вместо этого прочитал бы чужой ответ, поднимается RedisException и ничего не отправляется. У конфликта на уровне сокета собственной границы нет, потому что он никогда не ждёт: операция сразу завершается неудачей, как таймаут, так что stream_get_meta_data() сообщает timed_out. Два файбера, каждый из которых держит то, чего ждёт другой, расходятся поэтому по этой границе, а не ждут друг друга вечно.
  3. Некоторые случаи намеренно оставлены непокрытыми. Объект выражения или результата, удерживаемый между запросами, которому не предшествует ни один занимающий вызов: PDOStatement::execute() на выражении, подготовленном в более раннем запросе, ведёт себя как совсем без занятия. Конструирование второго дескриптора постоянного соединения, пока другой файбер посреди обмена на нём: PDO проверяет, что соединение из пула живо, прежде чем выдать его, эта проверка посреди обмена проваливается, и PDO отвечает сбросом соединения. Протокол, написанный вручную на сыром сокете — записать команду, приостановиться на чём-то другом, прочитать ответ позже, — поскольку уровень сокета отказывает, только пока владелец стоит на самом ответе, а между этими двумя точками другой файбер перенимает соединение; три клиента выше покрыты здесь своим занятием на уровне клиента, эквивалента которому у рукописного протокола нет. И всё, что достигает соединения вообще вне файбера, например деструктор, выполненный сборщиком циклов движка между запросами, — этого не видит никакое занятие.
  4. Закрытие общего соединения, пока другой запрос стоит на его чтении, завершает тот запрос — с 500 и строкой в журнале, называющей произошедшее. Занятие разводит два обмена; сохранить соединение живым оно не может, а ответ стоящего запроса — на соединении, которого больше не существует, — так что честный выход — завершить его, а не вернуть то, что теперь лежит в освобождённой памяти. Какие вызовы способны на это, зависит от клиента, и лишь один из них ждёт: mysqli::close() и mysqli_close() — занимающие вызовы, поэтому они ждут владельца и закрывают, когда это ожидание исчерпано, тогда как Redis::close() тоже занимающий, но поднимает RedisException, а не закрывает. Всё остальное закрывает сразу — fclose() на сыром потоке и, что важно, PDO, у которого метода close() нет вовсе: соединение освобождается сбросом последней ссылки на дескриптор (unset($pdo), переприсваивание, выход из области видимости), и этот путь выполняется внутри собственного демонтажа объектов движка, где никакое занятие не проверяется. Так что помощник переподключения, переприсваивающий общий дескриптор PDO, завершает запросы, стоящие на старом соединении, без ограниченного ожидания, которое получает его аналог на mysqli. Запросу сообщают об этом, как только закрытие произошло, а не на его собственном дедлайне чтения, которым для mysqlnd был бы mysqlnd.net_read_timeout — сутки из коробки. Приложениям, закрывающим общее соединение ради переподключения (обычный случай — $wpdb->check_connection() в WordPress), следует ожидать, что запросы, стоящие на нём в этот момент, завершатся неудачей, а не вернут неверные данные.

Ещё одна граница: работа хука под пользовательским планировщиком файберов (AMPHP, Revolt) откатывается к блокирующему вводу-выводу. Файбер, запущенный таким планировщиком, выполняется в собственном контексте, который планировщик OxPHP возобновить не может, поэтому хук это обнаруживает и идёт нативным путём, а не повреждает какой-либо из планировщиков. Речь о файбере, который пользовательский планировщик запускает внутри запроса. Собственный файбер запроса — тот, которым управляет OxPHP, и в режиме воркеров это настоящий FiberFiber::getCurrent() внутри запроса возвращает его, — так что библиотеки, которым нужно лишь различать конкурентные запросы, работают без всякого отката. Тот же факт — причина, по которой Revolt отказывается запускать свой цикл событий изнутри запроса режима воркеров.

Включение и цена

1, true и all включают каждую категорию, включая streams. Развёртывание, уже задавшее RUNTIME_HOOKS=1 ради хуков sleep, при обновлении начинает перехватывать сокеты без единой собственной правки; перечислите категории явно (RUNTIME_HOOKS=sleep), если это не то, чего вы хотите. Включение streams также патчит в памяти одну запись таблицы операций потоков PHP при запуске; страница возвращается в то состояние, в каком была найдена, но на платформе, где её исходную защиту определить нельзя, она остаётся записываемой — небольшая потеря харденинга, о которой сообщается в журнале сервера.

Цена, по замерам: около 3–5 мкс на цикл обращения к сокету на воркере, где больше ничего не стоит, около 5–6 мкс с 64 файберами, стоящими на дескрипторах, и около 7–11 мкс с 200. Готовность разрешается через множество, которое ядро хранит между ожиданиями, поэтому цифра отслеживает, сколько дескрипторов стало готовыми, а не сколько ждёт. Простаивающий воркер ждёт на этих дескрипторах, а не спит фиксированный интервал, замечая готовность на следующем тике, и это значит очень много: слепой сон стоил примерно 2 мс на цикл обращения.

Широкий stream_select() несёт накладные расходы, которых нет в узком случае, потому что каждый названный вызовом дескриптор регистрируется перед ожиданием и снимается после. Измерено с вычетом самого ожидания — каждый дескриптор уже читаем, вызов возвращается сразу, остаются одни накладные расходы: около 6 мкс против 5 мкс без хука на одном дескрипторе, 56 мкс против 9 мкс на 64 и 130–150 мкс против 16–21 мкс на 200 — примерно 0,65 мкс на дескриптор.

Читайте это как фиксированную цену за вызов, а не замедление той же работы. stream_select(), который действительно ждёт — а ради этого цикл select и пишут, — затмевает её: миллисекунда ожидания делает даже цифру для 200 дескрипторов примерно десятой долей вызова, а покупает она поток воркера, который неперехваченный вызов держит всё ожидание. Две формы — исключение, и для них категорию лучше не включать: вызов по многим дескрипторам, который почти никогда не ждёт, потому что нечего возвращать — времени потока не выигрывается; и запрос, который сам есть цикл событий, где потоку всё равно больше нечего выполнять. Клиенты, ждущие на одном соединении — mysqlnd, phpredis, HTTP-обёртки потоков, — сидят в верхних строках таблицы, где накладные расходы около микросекунды.

RUNTIME_HOOKS против oxphp_sleep()

Хук не заменяет собственный примитив — они покрывают разные случаи:

  • Берите oxphp_sleep() / oxphp_usleep() в коде, который пишете сами. Они приостанавливают файбер в режиме воркеров безусловно, без флага окружения, а oxphp_sleep() принимает дробные секунды (oxphp_sleep(0.25)) — точность, которую нативный sleep() (целые секунды) выразить не может.
  • Включайте RUNTIME_HOOKS=sleep для кода, который править не можете, — фреймворка или вендорной библиотеки, вызывающей нативные sleep()/usleep() напрямую. Он выключен по умолчанию, действует только внутри файбера и сохраняет нативный контракт (перехваченный sleep() по-прежнему принимает int и возвращает 0).

Полагаться на RUNTIME_HOOKS в собственных обработчиках — значит привязать их кооперативность к настройке развёртывания, а не к коду; предпочитайте там явный oxphp_sleep().

Разделяемое состояние

Внутрипроцессные примитивы конкурентности (OxPHP\Shared\Counter, Map, Channel, Mutex, Once, Pool, Atomic, Flag, Registry). Обзор API см. в разделе Разделяемое состояние.

Переменная По умолчанию Описание
SHARED_ENABLED true Булево — см. Булевы значения. Главный переключатель всей подсистемы OxPHP\Shared\*
SHARED_MAX_ENTRIES 100000 Глобальное ограничение на суммарное число всех записей Shared. Вставка сверх него завершается с CapacityException
SHARED_MAX_BYTES 1073741824 (1 GiB) Глобальное ограничение на оценочный объём памяти по всем записям Shared
SHARED_SOFT_LIMIT_RATIO 0.7 Начинает сбрасывать наименее приоритетную работу, когда использование пересекает эту долю от SHARED_MAX_BYTES / SHARED_MAX_ENTRIES
SHARED_METRICS_ENABLED true Булево. Включает/выключает экспозицию метрик oxphp_shared_* в Prometheus
SHARED_INTROSPECTION_ENABLED true Булево. Включает/выключает API интроспекции /__ox_shared/* на внутреннем сервере
SHARED_INTROSPECTION_PREVIEW_ENABLED true Булево. Включает/выключает предпросмотр значений в ответах интроспекции (отключите, когда предпросмотр может раскрыть чувствительные данные)
SHARED_CYCLE_DETECT_DEPTH 16 Глубина BFS при проверке на циклы. Увеличьте для глубоких допустимых графов
SHARED_CYCLE_DETECT_EDGES 10000 Число рёбер, обходимых при проверке на циклы. Увеличьте для плотных допустимых графов
SHARED_MAX_VALUE_SIZE 1048576 (1 MiB) Ограничение размера одного значения. Вставка большего значения завершается сразу с ошибкой
SHARED_MAX_CHANNEL_BYTES 67108864 (64 MiB) Ограничение суммарного объёма данных на один канал
SHARED_POISON_STRICT false Булево. Если истинно, паника внутри замыкания Mutex/Once навсегда «отравляет» примитив вместо попытки восстановления по мере возможности
SHARED_LOCK_DIAGNOSTICS off Диагностика конкуренции за блокировки: off, count или trace
SHARED_LOCK_POLL_INTERVAL_MS 100 Интервал опроса, используемый сэмплером диагностики блокировок
SHARED_PREVIEW_STRING_LIMIT 256 Обрезка каждой строки в предпросмотре /__ox_shared/preview, в байтах (по границе символа)
SHARED_PREVIEW_ARRAY_LIMIT 20 Число записей, выбираемых в предпросмотре /entry?id=…

Профилирование

Сэмплирующий профилировщик, который выдаёт трассы xhprof / speedscope. Форматы вывода и интеграцию с просмотрщиками см. в разделе Профилирование.

Переменная По умолчанию Описание
PROFILER_ENABLED false Булево — см. Булевы значения. Главный переключатель. Все остальные переменные PROFILER_* всё равно разбираются при запуске, поэтому опечатки всплывают сразу
PROFILER_SAMPLE_RATE 0.0 Вероятность (0.0–1.0) того, что запрос будет засэмплирован. Значения вне диапазона ограничиваются границами
PROFILER_INTERNAL false Булево. Если истинно, запросы к внутреннему серверу (/health, /metrics, эндпоинты плагинов) также подпадают под сэмплирование
PROFILER_AUTH_TOKEN (не задано) Необязательный bearer-токен. Если задан, PHP-функции oxphp_profiler_* требуют, чтобы запросы несли этот токен для включения профилирования по требованию
PROFILER_MAX_SPANS 50000 Ограничение числа спанов профиля на один запрос. Профили, превышающие лимит, обрезаются
PROFILER_MAX_DEPTH 256 Максимальная глубина стека вызовов, захватываемая на один сэмпл. Жёстко ограничена значением 65535
PROFILER_OUTPUT_DIR /tmp/oxphp-profiles Каталог для файлов профилей на диске
PROFILER_OUTPUT_FORMATS xhprof,speedscope Список форматов вывода через запятую для записи на диск
PROFILER_DISK_MAX_PER_SEC 10 Ограничение частоты записи файлов профилей на диск в секунду
PROFILER_RETENTION_COUNT 100 Максимальное число файлов профилей, хранимых в PROFILER_OUTPUT_DIR. Более старые файлы удаляются
PROFILER_EXPORT_URL (не задано) Удалённый эндпоинт, на который отправляются профили методом POST. Если задан, запись на диск всё равно происходит, если только PROFILER_OUTPUT_FORMATS не пуст
PROFILER_EXPORT_FORMAT xhprof Формат передачи для отправок на PROFILER_EXPORT_URL
PROFILER_EXPORT_AUTH_TOKEN (не задано) Необязательный bearer-токен, отправляемый с каждым запросом экспорта
PROFILER_EXPORT_XHGUI (автоопределение) Булево. Принудительно включает XHGui-совместимую обёртку для экспортируемого тела. Не задано = автоопределение, когда путь PROFILER_EXPORT_URL оканчивается на /run/import (подсказки в хосте/строке запроса не учитываются)
PROFILER_EXPORT_BUGGREGATOR (автоопределение) Булево. Принудительно включает конверт Buggregator. Не задано = автоопределение, когда путь PROFILER_EXPORT_URL оканчивается на /api/profiler/store. Конверт всегда выдаёт xhprof, поэтому PROFILER_EXPORT_FORMAT для него игнорируется (не-xhprof значение вызывает предупреждение, не фатально). Взаимоисключающ с PROFILER_EXPORT_XHGUI — включение обоих приводит к ошибке при запуске
PROFILER_EXPORT_APP_NAME (не задано) Buggregator app_name для группировки по проектам
PROFILER_EXPORT_TAGS (не задано) Buggregator tags в виде key=value,key2=value2; некорректный токен, пустой ключ или дублирующийся ключ приводит к ошибке при запуске

Примеры конфигураций

Разработка

bash
LISTEN_ADDR=127.0.0.1:8080 DOCUMENT_ROOT=./public LOG_LEVEL=debug ACCESS_LOG=all PHP_WORKERS=1 INTERNAL_ADDR=127.0.0.1:9090

Продакшен (Framework)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php PHP_WORKERS=8 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error MAX_CONNECTIONS=10000 INTERNAL_ADDR=127.0.0.1:9090 RATE_LIMIT=100 RATE_WINDOW_SECONDS=60 TRUSTED_PROXIES=private HEADER_TIMEOUT_SECONDS=5 DRAIN_TIMEOUT_SECONDS=25 COMPRESSION_LEVEL=4 STATIC_MAX_AGE=30d

Продакшен (режим воркеров)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public WORKER_MODE_ENABLED=true ENTRY_FILE=../worker.php PHP_WORKERS=8 WORKER_MAX_MEMORY_MIB=128 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error INTERNAL_ADDR=127.0.0.1:9090

TLS

bash
LISTEN_ADDR=0.0.0.0:443 TLS_CERT=/etc/ssl/oxphp/cert.pem TLS_KEY=/etc/ssl/oxphp/key.pem DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php

Проверка активной конфигурации

Когда внутренний сервер запущен, обратитесь к эндпоинту /config, чтобы увидеть итоговую конфигурацию:

bash
curl -s http://localhost:9090/config | jq .
json
{ "listen_addr": "0.0.0.0:80", "document_root": "/var/www/html/public", "entry_file": "/var/www/html/public/index.php", "log_level": "warn", "executor_type": "sapi", "php_workers": "8", "tokio_workers": 4, "queue_capacity": 1024, "queue_wait_timeout_ms": 1000, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "max_connections": 10000, "drain_timeout_seconds": 30, "header_timeout_seconds": 5, "rate_limit": 100, "rate_window_seconds": 60, "tls_enabled": true, "compression_level": 4, "access_log": "all", "max_query_body": 524288, "worker_mode_enabled": false, "worker_max_memory_mib": 0, "static_max_age": 2592000, "static_revalidate": false, "async_workers": 0, "async_queue_capacity": 0, "async_max_fibers": 256, "async_in_flight_cap": 0, "trace_context": true, "superglobals_enabled": true, "trusted_proxies": false, "plugins": { "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" }, "apm": { "enabled": true, "slow_query_ms": 100, "db_capture_params": false, "hooks_registered": 34 } } }
Note

Отдаваемый ответ /config вычищает несколько ключей, которые несёт внутреннее представление Config: пути к TLS-сертификату и ключу никогда не выводятся (tls_enabled показывает, активен ли TLS), а internal_addr и error_pages_dir удаляются — это топология развёртывания и пути в файловой системе, которые помогают злоумышленнику и не нужны сборщикам метрик.

См. также

Нашли ошибку? Сообщите →