Пакет · thread · глубокое погружение

Безопасность и производительность

Какие гарантии даёт Winter Thread в отношении хранения payload и выполнения кода, и на какую модель стоимости вы соглашаетесь, когда каждая задача — это отдельный процесс ОС.

Модель безопасности

Хранилище payload доступно только владельцу

Оба транспорта без каналов ограничивают доступ к сериализованной задаче запустившим её пользователем:

  • TempFileTransportchmod(0600) применяется сразу после tempnam(), до того как будут записаны любые байты, и файл удаляется через unlink в течение микросекунд после возврата из proc_open. Окно, в течение которого он существует на диске, крошечное, но даже тогда прочитать его может только владелец.
  • ShmTransport — сегмент создаётся через shmop_open($key, 'n', 0600, $size), поэтому подключиться к нему может только пользователь-владелец.

Payload подписываются (по желанию)

Payload всегда (де)сериализуется через opis/closure — никогда не через нативный unserialize() — поэтому сериализованное замыкание или анонимный класс безопасно переживает границу процесса. Поскольку этот payload — исполняемый код, злоумышленник, способный его подделать, мог бы выполнить произвольный код в дочернем процессе. Подпись закрывает эту брешь: настройте секрет на движке — new AdaptiveEngine(secret: '…'), (new ManualEngine())->withSecurity('…') или переменную окружения WINTER_THREAD_SECRET — и родитель подписывает через HMAC каждый payload, а дочерний процесс (wRunner / AdaptiveRunner) проверяет подпись перед десериализацией, отклоняя поддельные или изменённые payload (защита от инъекции объектов). Подпись включается по желанию; всегда включайте её, когда сериализуете замыкания или анонимные классы. Без секрета граница доверия опирается на приватный, доступный только владельцу канал доставки. См. Глубокое погружение в безопасность через метод движка security().

Формирование команды экранируется

Всё, что попадает в командную строку раннера — namespace, name, tag и каждое значение --arg-* — пропускается через escapeshellarg. Ключ shm приводится к int, а проверка зомби-процессов в macOS приводит PID к int перед вызовом ps. Не существует пути, по которому аргумент задачи мог бы вырваться в шелл.

Аргументы видны

Пользовательские аргументы становятся флагами командной строки, поэтому они видны в ps и в списках процессов. Никогда не передавайте секреты (токены, пароли) как аргументы start() — получайте их внутри run() из вашего конфига или хранилища секретов.

Модель производительности

Процесс не бесплатен

Каждый вызов start() выполняет fork/exec нового процесса PHP: запуск интерпретатора, автозагрузчик и собственный расход памяти. Эта изоляция и есть ценностное предложение, но она означает, что схема подходит для крупнозернистой работы — задач, измеряемых десятками миллисекунд и выше. Порождение процесса на каждую тривиальную операцию обойдётся дороже, чем сэкономит; такие операции лучше объединять в пакеты.

Компромиссы доставки payload

Транспорт Стоимость Примечания
PipeTransport без доплаты По умолчанию в CLI; небезопасен со Swoole
TempFileTransport одна запись temp-файла + unlink Универсальный, минимальное касание диска
ShmTransport только RAM Без дискового I/O; нужен ext-shmop

Стоимость сериализации растёт с размером payload, поэтому держите объекты задач лёгкими — id и пути, а не большие наборы данных в памяти. Для крупных или высокочастотных payload ShmTransport полностью обходит диск.

Гранулярность опроса join()

join() ожидает, опрашивая proc_get_status каждые 50 мс (usleep(50_000)). Это разрешение и для детекции завершения, и для timeout, и оно не даёт ожидающему родителю крутить процессор впустую. Это также означает, что join() может сообщить о завершении с задержкой до ~50 мс после фактического выхода дочернего процесса — пренебрежимо мало для реальных задач, но стоит знать при микробенчмарках.

Управление конкурентностью — на вас

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

Связанное