Эта статья опирается исключительно на сторонние датированные источники, а не на изобретенный шифр Aurabase. Задержка p99, специфичная для нашего бэкэнда, не включена. Ядро нашего приложения мы пишем на Rust, без сборщика мусора: факт, который можно проверить непосредственно в коде, рабочей области Cargo и сервисах axum. Однако мы еще не опубликовали воспроизводимую методологию тестирования p99, чтобы продемонстрировать это в цифрах. Этот текст объясняет механизм, а не измеренный результат.
Самое необходимое
- p99 измеряет самый медленный запрос из ста — место, где пауза сборщика мусора (GC) причиняет боль больше всего, а не в среднем (Дин и Баррозо, «Хвост в масштабе», Google, 2013).
- Сборщик мусора прерывает всю программу («остановить мир»), чтобы освободить неиспользуемую память. В Rust нет GC: память освобождается именно в тот момент, когда значение выходит за пределы области видимости, что проверяется компилятором.
- Discord задокументировал службу кэширования в 2020 году, где Go запускал цикл сборки мусора как минимум каждые две минуты, причем каждый цикл вызывал резкий скачок задержки (блог Discord Engineering).
- Чтобы уменьшить паузу GC, нужны годы инженерных разработок, даже в Google: в период с 2015 по 2018 год коллектор GB увеличился с 300-400 мс до 500 мкс, так и не достигнув нуля (go.dev).
- Бэкенд-ядро Aurabase написано на Rust, без сборщика мусора — проверено в коде. На сегодняшний день данные о задержке p99 Aurabase не опубликованы: это остается механизмом, а не измерением.
Почему р99 не средний
Среднее скрывает главное. Если 99 запросов из 100 отвечают за 5 мс и только один занимает 500 мс, средний показатель остается низким. Но один пользователь из ста испытывает ожидание в сто раз дольше. P99 измеряет именно этот запрос: самый медленный сотый запрос, тот, который нарушает ваше соглашение об уровне обслуживания, в то время как ваша информационная панель средней задержки остается зеленой.
В Google Джеффри Дин и Луис Андре Баррозу формализовали эту проблему в «Хвост в масштабе» (Communications of the ACM, vol. 56, 2013). Их наблюдение, которое с тех пор часто цитируется: «Временные эпизоды с высокой задержкой, которые не имеют значения в системах среднего размера, могут стать доминирующими в общей производительности обслуживания в больших масштабах». Короче говоря: случайные эпизоды задержки, незначительные в небольших масштабах, в конечном итоге доминируют над воспринимаемой производительностью распределенной системы.
Серверная часть, обрабатывающая тысячи запросов в секунду, в тот или иной момент обязательно отправит запрос, который выпадает во время паузы GC. В широком масштабе это не редкость. Это статистическая достоверность.
Что делает сборщик мусора и почему он все ставит на паузу
Сборщик мусора (GC) постоянно отслеживает живые объекты программы — те, на которые еще куда-то ссылаются — и освобождает память объектов, которые стали недоступны. Это отслеживание называется tracing: GC просматривает опорный граф, отмечает то, что еще используется, затем очищает остальное.
Проблема: обход этого графа, пока программа продолжает создавать новые ссылки, приводит к противоречивым результатам. Исторический ответ, который до сих пор используется многими современными сборщиками мусора в качестве крайней меры, — это stop-the-world — вся программа приостанавливает маркировку и сканирование. Чем больше куча, тем продолжительнее обычно бывает пауза: ее продолжительность зависит от размера текущих данных, а не от текущей рабочей нагрузки.
Большинство современных GC используют стратегию поколений: они предполагают, что большинство объектов умирают молодыми. Поэтому недавние выделения сканируются часто, но быстро в небольшой области памяти. Объекты, пережившие несколько циклов, мигрируют в большую область и сканируются нечасто, но когда эту область необходимо очистить, связанная с этим пауза увеличивается вместе с ее размером. Именно этот «серьезный» перерыв, а не маленькие «незначительные» перерывы, доминируют в p99 службы с высоким трафиком и большим распределением ресурсов.
Современные параллельные и генерационные GC сокращают частоту и продолжительность этих перерывов, работая параллельно с программой. Но почти все они имеют запасной механизм остановки мира для пограничных случаев — и для его уменьшения требуются годы инженерных разработок. В разделе 04 приводится количественный пример с указанием источников.
Discord, 2020: перерыв в сборке мусора становится производственным инцидентом
В феврале 2020 года инженер Джесси Ховарт опубликовал пост, ставший эталоном в отрасли: «Почему Discord переходит с Go на Rust» (блог Discord Engineering). Соответствующий сервис Read States управляет статусом чтения сообщений для миллионов пользователей — десятки миллионов записей в кэше — с сотнями тысяч обновлений в секунду.
Диагноз прямой, процитированный в статье: «Go заставит сборку мусора запускаться минимум каждые 2 минуты». Другими словами, Go запускает цикл сборки мусора в этом сервисе как минимум каждые две минуты — и каждый цикл приводит к резкому увеличению задержки, что видно на графиках команды.
Команда сначала уменьшила размер кэша, чтобы сгладить скачки. Компромисс остался неблагоприятным: меньше пауз GC, но больше запросов на пропуск кэша попадает в базу данных — следовательно, общий уровень p99 в других местах снижается. Основным решением было переписывание сервиса на Rust без сборщика мусора, который нужно было бы отслеживать.
Пост вызвал оживленную техническую дискуссию: более 1580 баллов и 642 комментария на Hacker News в тот же день его публикации (4 февраля 2020 г.) — признак того, что проблема выходит далеко за рамки дела Discord.
Три года разработки в Google позволили уменьшить паузу с 400 мс до 500 мкс
Сборщик мусора Go иллюстрирует масштаб усилий, необходимых для управления паузой GC — даже с ресурсами специальной команды в Google. Рик Хадсон, технический руководитель Go GC, задокументировал эту историю в двух официальных сообщениях в блоге Go.
| До августа 2015 г. | 300-400 мс | Исторический коллекционер Го, до редизайна |
|---|---|---|
| Август 2015 · Go 1.5 | 30-40 мс | Первый конкурирующий коллектор, установлено целевое время < 10 мс |
| 2016 · Го 1.6 | < 10 мс (удерживается SLO) | Первоначальная цель достигнута в производстве |
| Март 2017 · Перейти 1.8 | менее миллисекунды | Удаление сканирования стека «Останови мир» |
| Август 2017 · Перейти 1.9 | 100-200 мкс (отметка) | Новый неформальный ориентир, упомянутый командой |
| 2018 · Объявлено SLO | 500 мкс за цикл | Цель обслуживания формализована Риком Хадсоном |
Источник: «Getting to Go: Путешествие сборщика мусора Go», go.dev, 12 июля 2018 г.; и «Go GC: приоритет низкой задержки и простоты», go.dev, 31 августа 2015 г.
Три года самоотверженной работы сократили типичный перерыв в тысячу раз. Но пауза никуда не исчезла: это цель обслуживания (SLO), а не абсолютная гарантия нуля. Трассировка GC по своей конструкции должна время от времени пересекать граф живых объектов. Единственная регулируемая переменная — это частота и продолжительность этого путешествия, а не его существование.
Этот выбор приоритета не является нейтральным. Go в первую очередь нацелен на сетевые сервисы и веб-серверы, где пауза в несколько сотен миллисекунд напрямую нарушает работу пользователя — отсюда огромные усилия, вложенные в задержку, а не в чистую пропускную способность GC. Другие управляемые среды выполнения унаследовали различные компромиссы, сформированные их историческими сценариями использования, прежде чем компенсировать это своими собственными сборщиками с низкой паузой. Общая точка остается той же: все они начинаются с трассировки сборщика мусора, следовательно, с механизма паузы, который должен быть сведен к минимуму и никогда не может быть устранен при построении.
Почему в Rust нет этой проблемы по своей конструкции
Rust не уменьшает паузы GC: он устраняет механизм, вызывающий их. Компилятор при компиляции отслеживает, кому принадлежит каждое значение памяти — этоownership. Когда владелец значения выходит за пределы области видимости, Rust автоматически вставляет вызов, освобождающий эту память, в то же место двоичного кода. Этот механизм называется RAII (инициализация получения ресурсов): выпуск является детерминированным, а не запланированным сборщиком мусора, работающим в фоновом режиме.
Александру Недельку, автор технического блога, признанного в экосистеме Scala/Rust, резюмирует этот компромисс в недавней статье: «Компромисс, на который идет Rust, — это простота использования в пользу производительности с предсказуемой задержкой и безопасностью» (alexn.org, 21 июля 2026 г.). Rust жертвует некоторой простотой написания ради предсказуемой задержки.
В той же статье резюмируется, почему современных сборщиков мусора не всегда достаточно: "Современные сборщики мусора пытаются выполнять свою работу постепенно и одновременно, не влияя на программу. Но их возможности ограничены, и они возвращаются к циклу сборки мусора с остановкой мира, который замораживает всю программу, тем самым влияя на задержку".
Вот механизм примерно в десяти строках — общий пример, а не выдержка из кода Aurabase:
Важный нюанс: не все бесплатно. Типы подсчета ссылок (Rc, Arc) добавляют небольшую стоимость к каждому клонированию и выпуску. Эта стоимость остается локальной и детерминированной. Никогда не бывает паузы, которая останавливает всю программу, пока она проходит через кучу памяти.
Полезное разъяснение по поводу асинхронного бэкэнда: асинхронная среда выполнения Rust (tokio, используемая всеми сервисами Aurabase) не имеет ничего общего со сборщиком мусора. Он планирует совместные задачи в пуле потоков, но никогда не перебирает граф живых объектов для освобождения памяти. Путаница часто возникает в экосистемах, где асинхронная среда выполнения и сборщик мусора управляются одной и той же виртуальной машиной.
Что это меняет для серверной части с высоким трафиком
В серверной части, которая обслуживает тысячи одновременных запросов, отсутствие GC удаляет переменную из уравнения p99. Больше не нужно определять размер кучи памяти, настраивать генерации коллектора или отслеживать цикл, который может выпасть в самый неподходящий момент. Задержка отдельного запроса зависит от его собственной работы, а не от какого-то непредсказуемого глобального события где-то в программе.
Бэкэнд-ядро Aurabase применяет этот принцип: все сервисы (aura-gateway, aura-auth, aura-db, aura-realtime, aura-storage, aura-functions, aura-ai…) написаны на Rust и организованы в едином рабочем пространстве Cargo. Это можно проверить непосредственно в репозитории:
Фактический отрывок из Cargo.toml, рабочая область выпуска 2021, преобразователь v2 — проверено в репозитории Aurabase.
Чего этот факт не доказывает на данном этапе: показатель задержки p99, измеренный для Aurabase. Мы еще не опубликовали воспроизводимую методологию тестирования для нашей собственной серверной части — это работа, а не результат, доступный сегодня. Отсутствие сборщика мусора — механизм, проверенный в коде. Это само по себе не является свидетельством измеренной латентности p99. Помните об этом различии, несмотря на любые маркетинговые аргументы по этому вопросу, включая наши — см. наше техническое сравнение Aurabase и Supabase для получения подробной информации об архитектуре.
Правильное измерение p99 требует своей собственной дисциплины: репрезентативных условий нагрузки, процентилей, рассчитанных в достаточно широком скользящем окне, и тестовой среды, близкой к производственной. Публикация данных без этой методологии подобна публикации маркетинговых данных. Это именно то, что мы отказываемся делать в этой статье.
Что не решает отсутствие GC
Удаление сборщика мусора устраняет только один источник задержки хвоста — но не все. Серверная часть Rust по-прежнему может отображать ухудшенный p99 из-за ожидания сети, насыщенного пула соединений Postgres, оспариваемой блокировки базы данных, плохо индексированного SQL-запроса или медленного вызова стороннего API. Механизм, описанный в этой статье, устраняет структурную причину. Он не обеспечивает иммунитета против других.
Например, в Aurabase каждая служба взаимодействует с Postgres через пул соединений (sqlx) и с другими службами через NATS JetStream. Недостаточный размер пула, медленная в использовании подписка NATS или SQL-запрос без подходящего индекса создают собственный скачок задержки — независимо от отсутствия сборщика мусора.
Практический вывод: отсутствие GC — веская архитектурная причина выбрать серверную часть Rust для системы, поддерживающей p99. Само по себе это не является гарантией задержки – ни в Aurabase, ни где-либо еще. Метод, который имеет значение, остается прежним: измерить, опубликовать методологию, а затем исправить то, что показывают измерения. Если вы переходите с серверной части с помощью GC, в нашем руководстве по миграции Supabase на Aurabase подробно описано, что меняется, а что остается прежним.