Async/await ломает продакшен: разработчик Rust предложил замену, фреймворк Tina

Async/await ломает продакшен: разработчик Rust предложил замену, фреймворк Tina

Автор инженерного эссе на pmbanugo.me утверждает, что async/await, модель конкурентности (concurrency, работа с несколькими задачами одновременно), победившая за последнее десятилетие в Rust (Tokio) и Node.js, на самом деле путает асинхронность (ожидание ввода-вывода) с конкурентностью и скрывает под привычным синтаксисом сложную машину состояний. На практике это оборачивается конкретной production-проблемой: кооперативный планировщик Tokio или Node.js не переключается на другую задачу, пока код не дойдёт до await. Если внутри async-функции оказывается CPU-тяжёлая операция, например, разбор 10-мегабайтного JSON или криптографические вычисления на 50 миллисекунд, она блокирует весь поток исполнения целиком. В результате резко растёт задержка у тысяч несвязанных сетевых запросов, а процессор при этом простаивает.

Стандартный ответ индустрии, разносить работу по двум разным рантаймам: I/O-задачи оставлять в Tokio, а CPU-задачи отправлять в отдельный пул потоков вроде Rayon. Автор ссылается на опубликованные postmortem-разборы команд PostHog и Meilisearch, которые описали, как болезненно на практике разводить эти два мира: разработчикам приходится вручную анализировать каждую функцию, решать, в какой из пулов её отправить, вручную обеспечивать передачу данных между двумя рантаймами с разными моделями и следить, чтобы граница между пулами не привела к дедлоку. По мнению автора, это значит, что абстракция async/await не справилась со своей задачей, она должна была скрыть сложность конкурентности, а вместо этого превратила самого разработчика в «планировщика в контуре управления».

Вторая проблема, отсутствие ограничений по умолчанию. Вызов tokio::spawn(...) ничего не стоит: когда база данных ниже по цепочке начинает тормозить под нагрузкой, сетевой цикл приёма соединений продолжает принимать новые подключения и порождать задачи без ограничения, потому что асинхронные задачи и выделение памяти в этих экосистемах по умолчанию не ограничены. Задачи копятся в очереди бесконечно, память растёт, пока OOM killer операционной системы не убьёт процесс. Автор подчёркивает вывод из реальных postmortem-отчётов крупных платформ: очереди не решают проблему перегрузки, а лишь откладывают крах, делая его катастрофичнее.

Третья проблема, так называемые work-stealing-планировщики, когда простаивающее ядро «ворует» задачи у занятого ради справедливого распределения нагрузки. Автор утверждает, что на большом масштабе справедливость становится врагом пропускной способности, потому что перенос задачи на другое ядро разрушает локальность CPU-кеша (L1/L2). В пример приводится масштабирование виртуальной машины Erlang BEAM в WhatsApp на серверах со 100+ ядрами: по разбору инженера Робина Мориссе, простаивающие потоки, пытавшиеся украсть работу, тратили весь процессорный ресурс на борьбу за единую глобальную блокировку очереди планировщика (runq_lock).

В качестве альтернативы автор построил и выложил в открытый доступ проект Tina, opinionated (с жёсткими архитектурными решениями) shared-nothing-фреймворк конкурентности по модели «поток на ядро». В Tina нет async/await, промисов и futures: разработчик пишет Isolate, единицу конкурентной работы, обработчик которой является обычной синхронной функцией, реагирующей на сообщение и возвращающей Effect. Нагрузка шардируется по OS-потокам без work stealing: изоляты никогда не мигрируют между ядрами, а вся связь между ядрами идёт только через систему обмена сообщениями. Память в Tina строго ограничена и выделяется заранее при старте процесса; почтовые ящики (mailboxes) имеют жёсткий лимит, и если во время всплеска нагрузки ящик переполняется, вызывающая сторона узнаёт об этом немедленно, система предсказуемо сбрасывает лишнюю нагрузку вместо непредсказуемого падения из-за нехватки памяти. Наконец, планировщик Tina, это строгий, видимый однопоточный цикл на каждое ядро, поэтому поведение системы детерминировано; это открывает возможность Deterministic Simulation Testing (детерминированного симуляционного тестирования), с одним и тем же random seed можно воспроизвести сетевые обрывы связи или потерю сообщений и каждый раз получать абсолютно одинаковый порядок исполнения.

Ключевые факты

  • Автор утверждает: async/await путает асинхронность (ожидание I/O) с конкурентностью, CPU-тяжёлая операция внутри async-функции блокирует весь поток кооперативного планировщика Tokio или Node.js.
  • Postmortem-разборы команд PostHog и Meilisearch показывают: разработчикам приходится вручную разносить I/O-нагрузку в Tokio, а CPU-нагрузку, в Rayon, и вручную следить за границей между пулами, чтобы не поймать дедлок.
  • tokio::spawn(...) ничего не стоит вызвать: при просадке зависимого сервиса очередь задач растёт неограниченно, пока OOM killer операционной системы не убивает процесс.
  • Work-stealing-планировщики разрушают локальность CPU-кеша: при масштабировании Erlang BEAM в WhatsApp на серверах со 100+ ядрами потоки тратили весь ресурс на борьбу за глобальную блокировку очереди планировщика.
  • Автор выпустил открытый фреймворк Tina: модель «поток на ядро» без разделяемой памяти, без work stealing, со строго ограниченной памятью и детерминированным планировщиком, который позволяет воспроизводимо симулировать сбои (Deterministic Simulation Testing).

Почему это важно

Async/await стал доминирующей моделью конкурентности в Rust (Tokio) и Node.js, потому что выглядит как обычный синхронный код и прост в реализации для авторов языков. Автор материала утверждает, что эта простота обманчива: за привычным синтаксисом скрыта сложная машина состояний, а сама модель путает асинхронность (ожидание ввода-вывода) с конкурентностью (работой с несколькими задачами одновременно). Кооперативный планировщик Tokio или Node.js не переключается на другую задачу, пока код не дойдёт до точки await, поэтому 50-миллисекундная CPU-тяжёлая операция внутри async-функции блокирует весь поток исполнения целиком, тысячи несвязанных сетевых запросов резко тормозят, а процессор при этом простаивает.

Кому это важно

Материал адресован инженерам на Rust/Tokio и Node.js, которые строят высоконагруженные бэкенды, и в целом архитекторам систем конкурентности. Автор ссылается на опубликованные postmortem-разборы команд PostHog и Meilisearch: обеим пришлось вручную разносить I/O-работу в пул Tokio, а CPU-работу, в отдельный пул Rayon, вручную анализируя каждую функцию и следя за границей между двумя рантаймами, чтобы не получить дедлок. По формулировке автора, разработчик в этой схеме фактически становится «человеком-планировщиком в контуре управления» вместо того, чтобы эту роль выполняла система.

Как это применить

В ответ на описанные проблемы автор построил и выложил в открытый доступ на GitHub фреймворк Tina, shared-nothing-модель «поток на ядро». В Tina нет async/await, промисов и futures: единица работы, Isolate с обычным синхронным обработчиком, который реагирует на сообщение и возвращает Effect. Работа шардируется по OS-потокам без work stealing (изоляты не мигрируют между ядрами, обмен только через сообщения), память выделяется заранее при старте процесса, почтовые ящики строго ограничены, при переполнении вызывающая сторона узнаёт об этом сразу, вместо непредсказуемого OOM. Планировщик, детерминированный цикл на ядро, что даёт Deterministic Simulation Testing: один и тот же seed воспроизводит одинаковый порядок исполнения, включая симуляцию сетевых сбоев на одном потоке.

Можно ли доверять

Это авторское инженерное эссе (блог pmbanugo.me), опубликованное на Hacker News, где за первые часы оно набрало 25 баллов и 21 комментарий. Автор прямо называет источники и даёт на них ссылки: доклад Рича Хикки «Simple Made Easy» (Strange Loop, 2011), выступление Роба Пайка на GopherConAU 2023, публичные postmortem-разборы PostHog и Meilisearch, а также разбор инженера Робина Мориссе о масштабировании планировщика Erlang BEAM в WhatsApp. При этом текст одновременно продвигает собственный проект автора (Tina), поэтому его аргументы стоит читать как одну из сторон давнего спора об async/await, а не как независимый нейтральный вердикт.

Риски и подводные камни

Tina, новый узкоспециализированный фреймворк без истории эксплуатации в масштабном проде; переход с async/await на его thread-per-core модель требует полного пересмотра архитектуры приложения, а не точечной правки кода. Строгие ограничения, фиксированные почтовые ящики, отсутствие work stealing, снижают гибкость при неравномерной нагрузке между ядрами: если один изолят получает непропорционально много сообщений, фреймворк не перераспределит работу автоматически, и балансировку придётся решать архитектурно, а не полагаться на рантайм.

««Лёгкое», это то, что привычно и под рукой, а «простое», то, что структурно не запутано.»

— Рич Хикки, доклад «Simple Made Easy» (Strange Loop, 2011)

Компания Meta Platforms признана экстремистской организацией, её деятельность на территории РФ запрещена.