Node.js Internals
Child Processes: Мультипроцессность
Express сервер обрабатывает тысячи запросов в секунду. Вдруг приходит задача: **сжать видео**. FFmpeg крутится 30 секунд, а всё это время Event Loop стоит колом - **ни один запрос не обрабатывается**. Заказчик в ярости, мониторинг орёт, Slack взрывается. Как избежать катастрофы?
- **Видео-хостинги** (YouTube, Twitch) - конвертация загруженных видео в разные форматы через `ffmpeg` в child processes. Главный сервер продолжает принимать загрузки, воркеры обрабатывают очередь.
- **CI/CD системы** (GitHub Actions, GitLab CI) - каждый build/test запускается в изолированном процессе. При падении одного job другие продолжают работать. Graceful shutdown при деплое новой версии.
- **Микросервисы в Kubernetes** - контейнер получает SIGTERM при деплое, закрывает соединения, дожидается завершения запросов (grace period 30 сек), потом SIGKILL если не успел.
Зачем нужны дочерние процессы?
Event Loop хорош для I/O, но **беспомощен перед CPU-тяжёлыми задачами**. Классический сценарий: сервер обрабатывает 1000 запросов в секунду, и вдруг один запрос требует сжать 4K видео. Event Loop встанет колом - все остальные запросы будут ждать.
Есть **два пути**: 1. **Worker Threads** - для JavaScript-кода (криптография, вычисления, парсинг больших JSON) 2. **Child Processes** - для запуска внешних программ (`ffmpeg`, `imagemagick`, Python скриптов) или изоляции Node.js модулей Child Processes создают **отдельный процесс ОС** с собственной памятью. Это тяжелее Worker Threads, но даёт полную изоляцию и возможность запускать любые программы.
**Ключевое отличие:** Worker Threads делят память с главным процессом (через SharedArrayBuffer), а Child Processes живут в своей памяти и общаются только через IPC каналы или stdio.
Когда использовать Child Process вместо Worker Thread?
spawn: Потоковая работа с внешними программами
`spawn()` - это **низкоуровневый способ** запустить внешнюю программу. Он создаёт процесс и **стримит** данные через `stdin/stdout/stderr`. Не ждёт завершения - начинает отдавать вывод сразу.
**Когда использовать spawn:** - Большой вывод (логи, видео, архивы) - не хочешь загружать всё в память - Интерактивное взаимодействие (отправляешь команды в `stdin` процесса) - Контроль за каждым байтом вывода в реальном времени
**spawn vs exec:** spawn стримит данные чанками, exec ждёт завершения и возвращает весь вывод одной строкой. exec имеет лимит буфера (по умолчанию 1MB) - если вывод больше, процесс убьётся с ошибкой `maxBuffer exceeded`.
Почему spawn лучше exec для обработки 10GB лог-файла?
exec / execFile: Команды шелла vs прямой запуск
`exec()` и `execFile()` - **высокоуровневые обёртки** над `spawn()`. Они **ждут завершения** команды и возвращают весь вывод сразу (не стримят). Подходят для коротких команд с небольшим выводом.
**exec** - запускает команду **через shell** (`/bin/sh` на Unix, `cmd.exe` на Windows). Это означает: - Можно использовать **пайплайны**: `ls | grep .js` - Можно использовать **переменные окружения**: `echo $HOME` - **Опасность shell injection** - если команда строится из пользовательского ввода
**execFile** - запускает программу **напрямую**, без shell. Безопаснее, но: - Нельзя использовать пайплайны и шелл-фишки - Быстрее (не тратит время на запуск shell)
**Правило:** Используй `execFile` по умолчанию (безопаснее). Используй `exec` только если нужны возможности shell (пайплайны, глобы). Никогда не передавай пользовательский ввод в `exec` без санитизации!
Какая команда безопасна от shell injection?
fork: Запуск Node.js модулей в отдельном процессе
`fork()` - это **специализированный spawn** для запуска других Node.js скриптов. Он создаёт **отдельный процесс Node.js** и автоматически настраивает **IPC канал** (межпроцессное общение).
**Когда использовать fork:** - Изоляция ненадёжного кода (плагины, пользовательские скрипты) - CPU-тяжёлые задачи в Node.js (если Worker Threads не подходит) - Создание пула воркеров для обработки задач (как cluster module) - Защита от memory leaks - если воркер течёт, убиваешь и создаёшь новый
**fork vs Worker Threads:** fork создаёт полноценный процесс (свой V8 instance, своя память) - тяжелее но изолированнее. Worker Threads легковеснее, но делят некоторые структуры с главным процессом.
В чём главное преимущество fork перед spawn для Node.js скриптов?
IPC: Межпроцессное общение
**IPC (Inter-Process Communication)** - канал для обмена сообщениями между родительским и дочерним процессом. В Node.js это **структурированные данные** (JSON, объекты, буферы), а не просто строки как в stdio.
**Как работает:** 1. Родитель: `child.send(data)` → отправляет сообщение 2. Ребёнок: `process.on('message', (data) => {...})` → получает 3. Ребёнок: `process.send!(response)` → отправляет обратно 4. Родитель: `child.on('message', (response) => {...})` → получает Под капотом Node.js использует **Unix domain sockets** (или named pipes на Windows). Данные сериализуются через **structuredClone algorithm** (как в postMessage).
**Что можно передавать:** Объекты, массивы, примитивы, Buffers, TypedArrays. **Что нельзя:** Функции, классы с методами, сокеты (кроме специального handle passing для TCP серверов).
Что произойдёт если отправить класс через IPC?
Сигналы и управление жизненным циклом
**Сигналы** - это способ ОС попросить процесс завершиться (или выполнить другое действие). В Node.js самые важные: - **SIGTERM** - вежливая просьба завершиться (позволяет закрыть соединения, сохранить данные) - **SIGINT** - Ctrl+C в терминале - **SIGKILL** - грубое убийство процесса (не ловится, процесс умирает мгновенно) - **SIGHUP** - переподключение терминала (в демонах = reload конфига)
**Graceful shutdown** - корректное завершение: 1. Перестаём принимать новые запросы 2. Дожидаемся завершения текущих запросов (с таймаутом) 3. Закрываем соединения с БД, очередями 4. Выходим с кодом 0 **Detached processes** - процессы, которые продолжают жить после завершения родителя. Используются для демонов и фоновых задач.
**SIGTERM vs SIGKILL:** SIGTERM можно поймать и обработать (graceful shutdown). SIGKILL убивает процесс мгновенно, обработчик не вызывается. Kubernetes отправляет SIGTERM, ждёт 30 секунд, потом SIGKILL.
SIGKILL можно поймать и обработать как SIGTERM
SIGKILL убивает процесс мгновенно, обработчик не вызывается
SIGKILL (kill -9) - это команда ядра ОС убить процесс немедленно. Процесс не может её перехватить или игнорировать. Для graceful shutdown нужно использовать SIGTERM и завершаться за разумное время.
Что произойдёт если процесс не обработает SIGTERM за 30 секунд в Kubernetes?
Ключевые идеи
- **spawn** - стримит данные, подходит для больших выводов (логи, видео). **exec** - ждёт завершения, возвращает весь вывод одним куском (лимит 1MB).
- **execFile** безопаснее **exec** - не запускает shell, нет риска shell injection. Используй exec только для пайплайнов и shell-фишек.
- **fork** создаёт Node.js процесс с автоматическим IPC каналом. Подходит для изоляции кода, CPU-задач, пулов воркеров. Тяжелее Worker Threads, но изолированнее.
- **IPC** передаёт структурированные данные (объекты, буферы), но теряет методы и прототипы. **SIGTERM** - graceful shutdown (можно обработать), **SIGKILL** - мгновенная смерть (не ловится).
Связанные темы
Child Processes - часть архитектуры масштабируемых Node.js приложений:
- Worker Threads — Альтернатива для CPU-задач в JavaScript. Легковеснее child processes, но без изоляции памяти.
- Cluster Module — Использует fork под капотом для создания пула Node.js процессов, слушающих один порт.
- Event Loop — Child processes решают проблему блокировки Event Loop при CPU-тяжёлых задачах.
Вопросы для размышления
- В каких случаях оправдан выбор Worker Thread вместо Child Process, и когда наоборот?
- Как реализовать graceful shutdown для WebSocket сервера (дать время закрыть активные соединения)?
- Что произойдёт при отправке 10MB объекта через IPC? А 1GB? Как это оптимизировать?