Архитектура бэкенда
Вынос CPU-работы: worker threads и пул libuv
Команда профилирует медленный эндпоинт ресайза картинок, находит CPU-тяжёлую работу и «чинит» её, подняв UV_THREADPOOL_SIZE с 4 до 32. Ничего не улучшается. Ресайз — это JavaScript, бегущий на потоке цикла, а пул libuv, который они только что увеличили, вообще не выполняет JavaScript — он выполняет нативный I/O. Они тюнили не тот пул. Node прячет два ресурса исполнения за единственным event loop, и знание, какая работа куда идёт, — это разница между фиксом и потерянной неделей.
Два пула, две задачи
За единственным циклом — два разных источника параллелизма, и они служат противоположным видам работы:
- Пул потоков libuv — небольшой пул (по умолчанию 4 потока, настраивается до 1024 через
UV_THREADPOOL_SIZE), выполняющий нативные операции без асинхронного примитива ОС: вызовы файловой системы, DNS-резолвинг (dns.lookup) и асинхронные функции crypto/zlib. Он не выполняет твой JavaScript. Когда ты вызываешьfs.promises.readFile, libuv делает блокирующее чтение на потоке пула и постит результат обратно в цикл. Поэтому асинхронныйbcryptне замораживает цикл — хеширование бежит на потоке libuv. - Worker threads (
worker_threads) — реальные, отдельные изоляты V8 со своим event loop, дающие истинный многоядерный параллелизм для твоего CPU-bound JavaScript. Сюда относятся ресайз картинок, парсинг больших полезных нагрузок, сжатие и любые тяжёлые вычисления.
Баг с ресайзом теперь очевиден: ресайз — это JS, поэтому ему нужен worker thread, а не больший пул libuv. Увеличение libuv помогает, только когда упёрся в пропускную способность fs/dns/crypto — и даже тогда больше потоков пула, чем ядер CPU, в основном добавляет конкуренцию.
Цена воркера: перемещение данных
Worker threads не бесплатны, и счёт в основном за передачу данных. По умолчанию всё, что ты postMessage воркеру, глубоко копируется алгоритмом structured-clone — для большого буфера эта копия — реальная работа (копия многомегабайтного ArrayBuffer замерялась около 268 мс против примерно 29 мс при передаче). Два обходных пути важны:
- Transferables — передай
ArrayBufferвtransferList, и владение перемещается воркеру без копии (отправитель больше не может им пользоваться). Почти нулевая цена передачи. SharedArrayBuffer— разделяемая память, которую видят оба потока сразу, сAtomicsдля безопасной координации. Без копии, без передачи владения; правильный инструмент, когда обеим сторонам нужны одни байты.
Поэтому старший расчёт для выноса такой: работа должна быть достаточно CPU-тяжёлой, чтобы затмить цену передачи сообщений. Вынос вычисления на 2 мс воркеру может оказаться медленнее, чем просто выполнить его, как только заплатишь за клон и круговой рейс.
Почему это работает
Почему не поднимать свежий воркер на каждый запрос? Создание потока и старт изолята дороги (десятки миллисекунд и реальная память на воркер), поэтому воркеры-на-запрос превращают CPU-проблему в проблему текучки потоков. Продакшен-паттерн — пул воркеров: создать фиксированный набор воркеров один раз (обычно ~один на ядро CPU), раздавать им задачи через очередь и переиспользовать. Это ограничивает параллелизм железом, которое реально есть — восемь ядер не могут по-настоящему выполнить девять CPU-bound задач разом — и избегает оплаты старта на каждом вызове. Это зеркалит пулинг соединений: ресурс дорог в создании, дёшев в переиспользовании и опасен в неограниченном создании. Библиотеки вроде Piscina существуют именно чтобы этим управлять, чтобы ты не писал очередь и жизненный цикл вручную.
Когда не выносить: нарезай вместо этого
Не каждое долгое вычисление требует потока. Если работу можно разбить на мелкие куски, ты можешь выполнить кусок, затем setImmediate (или await разрешённого промиса), чтобы уступить цикл, затем выполнить следующий кусок — давая I/O-колбэкам вклиниваться между кусками. Это держит всё на потоке цикла без цены передачи, меняя общую пропускную способность (работа теперь конкурирует с запросами) на отзывчивый цикл. Нарезка подходит работе, которая длинная, но прерываемая (итерация большого массива, постраничное вычисление). Worker threads подходят работе монолитной и тяжёлой (один ресайз, операция crypto) или той, что ты искренне хочешь гонять параллельно на другом ядре.
И иногда правильный ответ — ни то ни другое: если CPU — узкое место по всему сервису, горизонтальное масштабирование — больше процессов (cluster) или больше машин — это рычаг, потому что один процесс Node маппится на один цикл, а CPU-bound пропускная способность фундаментально проблема числа ядер.
| Подход | Что выполняет | Истинный параллелизм | Главная цена |
|---|---|---|---|
| Пул libuv (4, настраивается) | Нативный fs/dns/crypto/zlib | Да, для нативного I/O | Не тот инструмент для JS CPU-работы |
| Worker thread | Твой CPU-bound JS | Да, другое ядро | Копия / передача данных + старт |
Нарезка + setImmediate | Твой JS, кусками | Нет (один цикл) | Ниже пропускная, ручное разбиение |
| Cluster / больше машин | Реплика всего процесса | Да, больше циклов | Сложность ops, координация состояния |
CPU-тяжёлый ресайз картинки (чистый JavaScript) блокирует цикл. Почему повышение `UV_THREADPOOL_SIZE` не помогает?
Ты выносишь большой буфер воркеру и обнаруживаешь, что круговой рейс доминирует копированием. Какой самый прямой фикс?
Когда нарезка с `setImmediate` — лучший выбор, чем worker thread?
- 01Какие два пула потоков за event loop и что каждый выполняет?
- 02Что стоит использовать worker thread и как снизить эту цену?
- 03Когда нарезать работу на цикле вместо выноса и когда не подходит ни то ни другое?
Два ресурса исполнения прячутся за единственным event loop, и хороший вынос значит отправить каждый вид работы к правильному. Пул потоков libuv — четыре потока по умолчанию, настраивается до 1024 — выполняет нативный I/O вроде fs, dns, crypto и zlib и никогда твой JavaScript, поэтому его увеличение ничего не даёт JS CPU-узкому месту (ловушка ресайза картинок). CPU-bound JavaScript принадлежит worker threads, отдельным изолятам V8, дающим истинный многоядерный параллелизм, но они выставляют счёт за перемещение данных: structured-clone глубоко копирует по умолчанию (около 268 мс для большого буфера против 29 мс при передаче), поэтому тянись к transferables или SharedArrayBuffer и переиспользуй фиксированный пул воркеров, а не порождай по одному на запрос, чтобы избежать текучки старта. Когда работа длинная, но разбиваемая, нарезай её и уступай через setImmediate, чтобы держать цикл отзывчивым ценой пропускной способности; когда CPU — потолок всего сервиса, масштабируйся горизонтально, потому что один процесс — это один цикл. С тяжёлой работой, убранной с критического пути, следующий урок берётся за управление I/O-работой, что остаётся: backpressure и ограниченная конкурентность, чтобы система соответствовала собственной скорости потребления, а не тонула в неограниченном fan-out.
встречается в185
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое индекс и как он ускоряет запросыjunior
- Leading-column rule: почему порядок столбцов в composite-индексе важенmiddle
- Partial, expression и covering-индексыmiddle
- Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновленияmiddle
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- EXPLAIN и планы выполнения: что решает планировщик и почемуjunior
- Типы сканирования: Seq, Index, Bitmap, Index-Onlymiddle
- Алгоритмы соединения и каскад ошибок оценки строкmiddle
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Расширенная статистика: исправление ошибок оценки для коррелированных колонокsenior
- Кеш планов, настройка константных стоимостей и внутренности планировщикаsenior
- Производственные режимы отказа и стабильность плановsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- Сначала профиль: измерь куда реально уходит времяjunior
- Закон Амдала и self-time: потолок любого ускорения, которое ты можешь выпуститьmiddle
- Измерительный цикл: микробенч, макробенч, prod-профиль, эффект наблюдателяmiddle
- Чтение флейм-графов: формы, профайлеры по языкам и 60-секундный сканmiddle
- Статистические baseline''''ы: почему один запуск — не измерениеmiddle
- История профайлеров и ловушки микробенчей: от Кнута до GWPsenior
- Hardware counters, профили холодного старта и безопасность профилейsenior
- Непрерывное профилирование в масштабе: затраты, CI-гейты, корреляция с трейсами и антипаттерныsenior
- Что делает путь горячим: симптом против причиныjunior
- Пять форм hotspot''''а: CPU, аллокации, кэш, лок, syscallmiddle
- Чтение parent и child chains: где применять правкуmiddle
- JIT deopt, цикл fix-and-verify и PR-time профилированиеmiddle
- Аппаратные счётчики и Intel TMA: диагностика подкатегорийsenior
- False sharing и горячие пути нативных мостовsenior
- Горячие пути в production: безопасность, хвостовая латентность и происхождение инструментовsenior
- Иерархия памяти: почему расстояние важнее числа операцийjunior
- Row-major vs column-major: порядок доступа и разрыв в 9xjunior
- Branch prediction: 10–30 циклов штрафа за неожиданный ifmiddle
- Hardware prefetcher, TLB и memory-level parallelismsenior
- Основы GC: за что рантайм берёт налогjunior
- Алгоритмы GC: поколенческая гипотеза, concurrent marking и write barriermiddle
- GC tradeoffs: пауза, throughput, память и давление аллокацийmiddle
- Настройка GC: пейсинг, форма кучи и наблюдаемость аллокацийmiddle
- Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймыsenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- N+1: одна логическая операция, много round-trip''''овjunior
- Семейства фиксов: JOIN, IN, preload и DataLoadermiddle
- Обнаружение N+1: query logs, APM traces и CI gatesmiddle
- DataLoader: батчинг по дереву резолверовmiddle
- Кросс-протокольный N+1: HTTP fan-out и Redis MGETmiddle
- N+1 в масштабе: исчерпание пула, изменения планов и денормализацияsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- Что на самом деле стоит bundle: download, parse, compile, executejunior
- Core Web Vitals: LCP, INP и CLSmiddle
- Code splitting: route-level, component-level, vendor splittingmiddle
- Tree shaking и compression: удаляем то, что не используемmiddle
- Third-party scripts: тихий убийца бюджетаmiddle
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior