Архитектура бэкенда
Блокирующий vs неблокирующий I/O: два способа ждать
Замерь время типичного обработчика запроса — и удивишься, насколько мало в нём твоего кода. Он читает строку из Postgres, дёргает платёжный API, пишет строку в лог — и тратит 95% реального времени просто на ожидание ответа. Вся суть бэкенд-конкурентности сводится к одному: что делает программа, пока ждёт? На этот вопрос есть два ответа, делящие всю область пополам. Один паркует поток на каждое ожидание. Другой отказывается кого-либо парковать и просит ядро тронуть его за плечо, когда данные готовы.
Ожидание — это и есть работа
Бэкенд — это в основном I/O-машина. Чтение с диска, запросы к базе, исходящий HTTP, запись в сокет — каждая операция медленная относительно CPU (микросекунды и миллисекунды, тогда как процессор выполняет миллиарды инструкций в секунду). Поэтому первый вопрос проектирования никогда не «как быстр мой код», а «как рантайм проводит ожидание». Две модели I/O дают противоположные ответы, и выбор определяет, как сервер масштабируется, сколько памяти ест и как падает под нагрузкой.
Блокирующий I/O: поток на соединение
В блокирующей модели поток вызывает read(), и операционная система приостанавливает этот поток до прихода байтов. Поток запаркован — занимает свой стек и слот планировщика — и ничего полезного не делает. Чтобы обслужить второе соединение конкурентно, нужен второй поток, третьему — третий, и так далее: поток-на-соединение (thread-per-connection).
Это просто и легко рассуждать — код читается сверху вниз, каждая строка ждёт предыдущую — но масштабируется добавлением потоков, а потоки не бесплатны. Каждый поток ОС резервирует примерно 1–2 МБ стека, поэтому 10 000 конкурентных соединений означают порядка 10+ ГБ памяти только под стеки, плюс тысячи переключений контекста в секунду, пока планировщик тасует запаркованные потоки. Модель меняет память на простоту.
Неблокирующий I/O: один поток, много сокетов
В неблокирующей модели сокет переводится в неблокирующий режим, и read() возвращается немедленно — либо с данными, либо с «пока не готово». Вместо того чтобы парковаться, поток регистрирует интерес к множеству сокетов через средство ядра — epoll на Linux, kqueue на BSD/macOS — и задаёт один вопрос: «кто из этих тысяч файловых дескрипторов готов прямо сейчас?» Ядро возвращает только готовые, примерно за O(1) независимо от того, сколько их под наблюдением. Поток обслуживает эти, затем спрашивает снова. Этот цикл и есть event loop.
Один поток поэтому способен тянуть десятки тысяч соединений, потому что трогает только те сокеты, у которых есть реальная работа. Цена — другая форма кода: нельзя читать сверху вниз и «ждать» — ты регистрируешь колбэк (или await), и цикл вызовет тебя позже. Логика, которая была прямой линией, превращается в набор продолжений (continuations).
Почему это работает
Почему средство ядра так важно? Наивный способ следить за множеством сокетов — пройти по всем и спросить «готов? готов? готов?» — это select/poll, и он стоит O(n) за проход, поэтому слежка за 10 000 сокетов означает сканирование всех 10 000 каждый раз, даже если готов один. epoll/kqueue переворачивают это: ты регистрируешь набор один раз, а ядро отдаёт только те дескрипторы, что стали готовы, поэтому стоимость зависит от числа активных соединений, а не всех. Это и есть механизм, делающий «один поток, 50 000 простаивающих keep-alive соединений» по-настоящему дешёвым — простаивающие почти ничего не стоят, потому что цикл не заглядывает к ним, пока у них нет данных.
Постановка C10k и реальный компромисс
Этот раскол назвали проблемой C10k (~1999): как обслужить 10 000 конкурентных клиентов на одной машине? Поток-на-соединение упёрся в стену памяти и переключений контекста; модель event loop — Nginx, Node.js, Netty, Redis — стала ответом. Честная сводка:
- Блокирующий / поток-на-соединение меняет память и накладные расходы на переключение контекста на простоту. Хорош, когда число соединений умеренное или работа CPU-тяжёлая; код остаётся линейным.
- Неблокирующий / event loop меняет сложность кода (колбэки, продолжения, никакой парковки) на масштабируемость при множестве конкурентных, в основном простаивающих соединений.
Ни одна не «быстрее» в принципе. Для I/O-нагрузок с высокой конкурентностью event loop решительно выигрывает по памяти и числу соединений. Для CPU-нагрузок один поток event loop не быстрее любого другого одного потока — предел, который следующие уроки сделают резким.
| Блокирующий (поток-на-соединение) | Неблокирующий (event loop) | |
|---|---|---|
| Ожидание | Поток запаркован ОС | Ядро следит за FD, поток идёт дальше |
| 10k соединений | ~10+ ГБ стеков, много переключений | Один поток, память ~ активным соединениям |
| Форма кода | Линейная, сверху вниз | Колбэки / await, продолжения |
| Масштабируется через | Добавление потоков | Пропускную способность готовых событий |
| Лучше для | Умеренной конкурентности, CPU-тяжёлой | Высокой конкурентности, I/O-bound |
Почему серверу с потоком-на-соединение тяжело держать 50 000 в основном простаивающих keep-alive соединений?
Что `epoll`/`kqueue` дают event loop, чего не даёт наивное сканирование `select`/`poll`?
Расставь по порядку, что делает неблокирующий сервер, чтобы обслужить чтение на одном из множества сокетов:
- 1 Перевести сокет в неблокирующий режим и зарегистрировать в epoll/kqueue
- 2 Спросить ядро, какие из наблюдаемых дескрипторов готовы
- 3 Ядро возвращает только готовые дескрипторы
- 4 Запустить колбэк для каждого готового сокета, читая доступные байты
- 5 Вернуться к началу и снова спросить ядро
- 01Почему «как рантайм проводит ожидание» — центральный вопрос для бэкенда, а не сырая скорость кода?
- 02Как работает блокирующий поток-на-соединение и какова его цена масштабирования?
- 03Как неблокирующий I/O с event loop обслуживает много соединений на одном потоке и что вносят epoll/kqueue?
Бэкенд большую часть жизни ждёт I/O, поэтому модель того, как он ждёт, определяет всё ниже по течению. Блокирующий I/O паркует поток на каждое ожидание: линейный, лёгкий код, но каждый поток стоит примерно 1–2 МБ и слот планировщика, поэтому поток-на-соединение превращает 10 000 соединений в 10+ ГБ стеков и шторм переключений контекста — память в обмен на простоту. Неблокирующий I/O делает сокеты неблокирующими, возвращается немедленно и регистрирует их в epoll или kqueue, поэтому один поток спрашивает ядро, какие дескрипторы готовы, и обслуживает только их — масштабируясь до десятков тысяч соединений, потому что простаивающие почти ничего не стоят, ценой кода в форме колбэков или await. Проблема C10k назвала этот раздел, и event loop стал стандартным ответом для высококонкурентных I/O-bound серверов. Следующий урок раскрывает сам цикл: упорядоченные фазы, которые он выполняет, очередь микрозадач, которую он осушает между ними, и почему эта конкурентность кооперативная, а не параллельная.
встречается в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