Архитектура бэкенда
Event loop: один поток, упорядоченные фазы
Разработчик пишет setTimeout(fn, 0) рядом с setImmediate(fn) рядом с Promise.resolve().then(fn), ожидая, что они сработают в порядке исходника. Не сработают. Сначала бежит промис, потом — в зависимости от того, где стоит код — либо таймер, либо immediate. Это не баг и не гонка; это опубликованное расписание event loop. Цикл — не чёрный ящик, который «когда-нибудь выполнит асинхронщину». Это строгая упорядоченная машина, и как только видишь её фазы, тайминг асинхронности перестаёт быть фольклором.
Цикл — это последовательность фаз
Единственный поток из прошлого урока не выполняет колбэки в порядке прихода. Он крутит фиксированную последовательность фаз (в libuv, движке цикла Node), и у каждой фазы своя очередь колбэков, которую надо осушить перед переходом дальше. Те, что важны изо дня в день:
- Timers — колбэки, у которых истёк таймаут
setTimeout/setInterval. - Poll — сердце: получить новые I/O-события и выполнить их колбэки (пришёл HTTP-запрос, закончилось чтение файла). Если ничего не ждёт, цикл блокируется здесь, ожидая на epoll/kqueue — именно здесь проводится время простоя.
- Check — колбэки, запланированные через
setImmediate, выполняются сразу после poll. - Close — колбэки событий закрытия (
socket.on('close')).
Один полный проход по фазам — это тик (tick) цикла. Внутри фазы колбэки выполняются по одному, до конца — без вытеснения.
Макрозадачи vs микрозадачи
Два этих слова прячут всю модель тайминга. Очереди фаз держат макрозадачи: колбэк таймера, I/O-колбэк, setImmediate. Отдельно от них — две очереди микрозадач с более высоким приоритетом:
- Колбэки
process.nextTick(специфика Node), осушаются первыми. - Реакции промисов (продолжения
.then/await).
Правило: после завершения каждого отдельного колбэка цикл полностью осушает очереди микрозадач, прежде чем выполнить следующий колбэк или сменить фазу. Поэтому Promise.resolve().then(fn) обгоняет setTimeout(fn, 0) — реакция промиса есть микрозадача, выполняемая на ближайшем осушении, тогда как таймер ждёт фазы timers на более позднем тике.
Почему это работает
Зачем вообще отдельная, более приоритетная очередь микрозадач? Потому что цепочки промисов должны устаканиться прежде, чем программа вернётся к планировщику I/O, иначе порядок был бы непредсказуем между тиками. Микрозадачи позволяют единице синхронно выглядящей асинхронной работы (await разрешился, затем следующий await) завершиться как группа, прежде чем цикл сделает что-либо ещё. Опасность — зеркальная: микрозадача может запланировать другую микрозадачу, та — ещё одну, неограниченная цепочка (рекурсия process.nextTick или сбежавший цикл промисов) морит цикл голодом, потому что микрозадачи осушаются до пустоты прежде, чем выполнится любой I/O-колбэк. Так что микрозадачи дают плотный порядок, но потоп микрозадач способен заблокировать фазу poll так же жёстко, как синхронный цикл — цикл никогда не доберётся до своих сокетов.
setTimeout(0) vs setImmediate
Эти два — классическая путаница. setImmediate всегда бежит в фазе check, сразу после poll; setTimeout(fn, 0) зажат до минимума ~1 мс и бежит в фазе timers. Внутри I/O-колбэка (то есть уже в poll или сразу после) setImmediate надёжно срабатывает раньше следующей фазы timers, поэтому выигрывает. На верхнем уровне скрипта порядок не гарантирован — он зависит от того, сколько времени цикл потратил на запуск. Старший вывод не в мелочи, а в модели: «сразу после I/O» и «после задержки таймера» — это разные фазы, а не одна очередь.
Кооперативная, не параллельная
Поскольку один поток выполняет каждый колбэк до конца без вытеснения, конкурентность event loop кооперативная: каждый колбэк должен добровольно уступить — завершившись или наткнувшись на await, который отдаёт управление — чтобы выполнился любой другой колбэк. Два запроса никогда не выполняют JavaScript в один и тот же момент; они чередуются в точках уступки. В этом вся сила (никаких локов, никаких гонок данных по разделяемому состоянию внутри тика) и вся слабость (один колбэк, который никогда не уступает, замораживает всё), что следующий урок делает конкретным.
| Планировщик | Тип очереди | Фаза / тайминг | Приоритет |
|---|---|---|---|
process.nextTick(fn) | Микрозадача | После текущего колбэка, до промисов | Наивысший |
Promise.then / await | Микрозадача | После текущего колбэка, после nextTick | Высокий |
setTimeout(fn, 0) | Макрозадача | Фаза timers, следующий годный тик | Обычный |
| I/O-колбэк | Макрозадача | Фаза poll | Обычный |
setImmediate(fn) | Макрозадача | Фаза check, сразу после poll | Обычный |
Внутри I/O-колбэка ты планируешь `setTimeout(a, 0)`, `setImmediate(b)` и `Promise.resolve().then(c)`. В каком порядке они выполнятся?
Почему рекурсивный `process.nextTick` (или сбежавшая цепочка промисов) может застопорить I/O, хотя никакой синхронный цикл не выполняется?
Расставь по порядку один тик event loop по его основным фазам:
- 1 Фаза timers: выполнить истёкшие колбэки setTimeout/setInterval
- 2 Осушить микрозадачи (nextTick, затем промисы)
- 3 Фаза poll: выполнить готовые I/O-колбэки (или ждать на epoll/kqueue)
- 4 Фаза check: выполнить колбэки setImmediate
- 5 Фаза close: выполнить колбэки событий закрытия
- 01Какие основные фазы у event loop и что делает каждая?
- 02В чём разница между макрозадачами и микрозадачами и каково правило осушения?
- 03Почему конкурентность event loop называют кооперативной, а не параллельной, и что из этого следует?
Event loop — это строгая упорядоченная машина, а не размытый ящик «выполнит асинхронщину позже». Один поток крутит фиксированные фазы — timers, poll, check, close — и внутри каждой фазы выполняет очереди колбэков по одному до конца. Poll — центр тяжести: он выполняет готовые I/O-колбэки и, простаивая, блокируется на epoll/kqueue. Поперёк фаз идут две более приоритетные очереди микрозадач, сначала process.nextTick, затем реакции промисов, и определяющее правило в том, что цикл осушает микрозадачи до пустоты после каждого колбэка прежде, чем продвинуться — именно поэтому разрешённый промис обгоняет setTimeout(0) и почему setImmediate (фаза check) и setTimeout(0) (фаза timers) не взаимозаменяемы. Поскольку колбэки выполняются до конца без вытеснения, конкурентность кооперативная: запросы чередуются только там, где уступают, давая безопасность без локов внутри тика, но делая цикл заложником любого колбэка, который отказывается уступать. Этот сценарий заложника — что именно блокирует цикл и как это увидеть — следующий урок.
встречается в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