Go-биддер, который теряет аукционы на таймауте, пока его среднее выглядит здоровым, — это две проблемы под одним симптомом: дедлайн, который принадлежит бирже и покрывает путь туда и обратно, и сборщик, списывающий mark assist на горутину, считающую ставку. Здесь — как делится дедлайн, какие ручки Go окупаются и чего не чинит горячий путь на Rust.
Биддер, который теряет аукционы на таймауте, пока его среднее выглядит здоровым, описан не той цифрой. Дедлайн принадлежит бирже и покрывает сеть в обе стороны. Сборщик — свойство процесса, поэтому его пауза списывается сразу на все соединения, а всплеск на одном отдельном соединении требует другого объяснения.
«Переписать на Rust или тюнить рантайм» — это выбор между двумя ответами на вопрос, который никто не задал: какая часть дедлайна тратится и на что. Дальше бюджет отделён от сборщика, сборщик от планировщика, а переписывание — от компонента, который его заслуживает.
Короткий ответ структурный. Дедлайн задаёт биржа, и он включает сеть, поэтому первое исправление раскладывает p99 одного соединения на время в обработчике, время ожидания процессора и время в сети. Что amBrain может подтвердить публично: мы построили RTBBidder — demand-side платформу, сделанную с нуля, где каждое решение по ставке проверяет десятки условий таргетинга на показ. Цифры задержки, которые мы публикуем как измеренные, взяты с торговых путей, а не с рекламного биддера, и ни одна цифра ниже не измерена на нашем биддере.
Спецификация OpenRTB определяет tmax как максимальное время в миллисекундах, которое биржа отводит на получение ставок, включая задержку интернета, и говорит, что это значение отменяет любые прежние указания. Бюджет — это путь туда и обратно, который приезжает внутри каждого запроса.
Порог, по которому вас судят, — не тот, что на вашем дашборде. Документация Google Authorized Buyers требует, чтобы 85 процентов ответов приходили внутри дедлайна по замеру в торговой локации, и троттлит биддеров, которые в него не укладываются. Между их часами и вашими лежит всё, что не является вычислением:
Поэтому внутренний дедлайн стоит ниже tmax ровно на то, во что, по вашей же гистограмме, обходится ответ на этом соединении, и выводится заново для каждой биржи, а не задаётся один раз на весь флот. Дедлайн — не план по ёмкости: работа, отменённая на дедлайне, свой CPU уже потратила.
При перегрузке это означает платить полную цену за ответы, которые никто не считает, поэтому недостающее исправление — контроль допуска: прочитайте tmax, сравните его с задержкой в очереди, которую вы измеряете, и отвечайте no-bid, когда арифметика не сходится. Быстрый no-bid идёт в зачёт 85 процентов; опоздавшая ставка — нет.
Сборщик мусора — свойство процесса, поэтому цикл, запущенный на любом соединении, списывается на все соединения. Первым промахивается то, у которого самый жёсткий tmax и самый тяжёлый запрос. Исключите то, что даёт ту же картину без сборщика:
Ни один из четырёх не чинится настройкой сборщика, и у разделения есть инструменты. CPU-профиль разделяет счета сборщика по символам: runtime.gcAssistAlloc — списания на обработчик, runtime.gcBgMarkWorker — фоновая маркировка. Время stop-the-world — это /sched/pauses/total/gc:seconds, ожидание в состоянии runnable — /sched/latencies:seconds, а accept-очередь читается вне процесса через ListenOverflows.
Какое исправление в Go вам нужно, решает одно различие. Пауза stop-the-world списывается сразу на все горутины, поэтому выглядит как ровный всплеск на всех соединениях. Mark assist списывается на горутину, которая аллоцировала, поэтому попадает на запросы, аллоцировавшие больше всех. Assist снижают две вещи: меньше байт на bid request или более длинный цикл, в котором фоновые воркеры покрывают больше маркировки. Смену состава трафика переживает только первая.
Сборщик Go конкурентный, и официальное руководство прямо говорит, что длина паузы не растёт с размером кучи, поэтому переходы stop-the-world коротки. Значимый источник — assist: горутины помогают сборщику, когда аллокации идут быстро, потому что фоновой маркировке достаётся фиксированная четверть процессоров, а нехватка списывается на того, кто аллоцирует.
Темп превращает это в порог, а не в наклон. Темп аллокаций — это QPS, умноженное на байты на bid request, против фиксированной фоновой доли, поэтому код, который ни разу не уходит в assist на пятой части вашего трафика, при 100K QPS может уходить в assist почти на каждом запросе.
Стоимость маркировки пропорциональна живому графу указателей, а не мусору, и биддер держит неудачную для этого форму: индексы кампаний, сегменты аудитории, кеши частоты. Discord опубликовал такой же вывод в 2020: сборщик сканировал весь LRU-кеш, чтобы решить, свободна ли память. Под одной фразой прячется пять механизмов:
Читайте это как пять отдельных счетов. Ровно один закрывается настройкой сборщика, и ни один — сменой языка до того, как разделение измерено.
Гил Тене назвал этот отказ coordinated omission: измеряющая система согласуется с системой под тестом так, что выбросы просто не попадают в замер, потому что замкнутый контур ждёт ответа и перестаёт слать во время затыка. ScyllaDB опубликовала в 2021 сравнение, где одна нагрузка показала p99 в 249 микросекунд в замкнутом контуре и 665 ms под разомкнутой нагрузкой с коррекцией — расхождение примерно в 2,700 раз.
Генератор нагрузки, который ждёт ответа, перестаёт слать запросы ровно во время того затыка, который он и был построен найти, а потом усредняет эту тишину в результат. Перцентиль, который он печатает после, описывает генератор, а не ваш биддер.
Назовите цифру остановки до того, как начнёте тюнить, потому что переписывание, решённое от усталости, — не решение. Цифры две, а не одна: аллокации кучи на запрос, которые двигают assist, и живая куча, которая двигает маркировку.
Порядок такой: сначала источник, потом потолок, а не сначала самый большой эффект. Начинайте там, где рождается assist: escape analysis на пути ставки, переиспользуемые буферы вместо новых аллокаций и кодек, который читает нужные вам поля, а не материализует свежий граф объектов. Держите его слайсы короткоживущими: слайс в буфер запроса удерживает весь этот буфер на всё время жизни ставки.
У подхода есть потолок: Uber сообщил в 2021, что настройка GOGC относительно лимита памяти контейнера вернула около 70,000 ядер на его критичных для бизнеса сервисах. Это результат по стоимости, а не по перцентилю.
RTB House в июне 2025 описала JVM-сервис ставок, где дробление на микросервисы дало большой объём мелких запросов: добавленная задержка должна была уложиться в 7 ms при среднем запросе около 2.5 ms, а 98-й и 99-й перцентили ломались на частых паузах G1. Они перешли на поколенческий ZGC и заплатили памятью.
Заметьте, что потребовалось для этой починки: второй сборщик, на который можно переключиться. В Go он один и не подключаемый, поэтому рычаги Go — темп аллокаций, форма живого набора и GOGC против GOMEMLIMIT. Горячий путь на Rust убирает вместо этого конкретные вещи: нет assist, нет фоновой маркировки, нет принудительного цикла. Список того, что никуда не денется, длиннее, чем ждёт большинство команд:
Если хвост живёт в разборе запроса, в соединении с биржей или в очереди планировщика, Rust не вернёт ни одной из этих миллисекунд, а переписывание не того компонента тратит квартал ради той же доли таймаутов.
Поэтому переносите не сервис, а самый маленький кусок, которому принадлежат аллокации: цикл оценки показа и его индексы, отбор кандидатов, таргетинг, обращения за частотой и бюджетом, скоринг. Считайте цену границы за одно пересечение: один раз на bid request с плоским буфером и никогда не по разу на правило таргетинга. Проверяйте на отдельных инстансах, которым подают зеркальный поток, и никогда внутри тестируемого процесса, где теневой путь удваивает обе величины, которые вы измеряете.
Приёмочный тест, который работает на любом из путей: прогоните короткое окно с выключенным сборщиком, под потолком памяти, который вы контролируете, и запишите p99.9 внутри обработчика. Прогоняйте на одном инстансе под долей трафика и с автоматическим откатом, потому что при выключенном GOGC всплеск кучи в потолок загоняет рантайм в циклы подряд, а руководство говорит, что этот затык может быть бесконечным. Читайте результат, только если ограничитель GC CPU ни разу не включился: как только он включился, перцентиль описывает ограничитель.
У второй половины вопроса — кто делает такую работу — есть проверка, которой не нужен список подрядчиков. Компания, которая чинит задержку, вызванную GC, ведёт себя не так, как та, которая продаёт переписывание:
Ни один из них не требует доверия: каждый — документ, который можно запросить в первом же разговоре, и если он приходит расплывчатым, значит диагностику пропускают.
Поэтому первый вопрос не в том, какой язык. Он в том, какому из трёх слагаемых — обработчику, планировщику или сети — принадлежат недостающие миллисекунды на том соединении, которое отваливается по таймауту, и сдвигаются ли байты, аллоцированные на bid request, когда вы за это беретесь.
Что amBrain может подтвердить публично: amBrain — компания по разработке ПО, специализирующаяся на торговых платформах, matching engine, системах real-time bidding и инженерии казино-платформ; мы пишем софт с 2019 года, а в AdTech делаем разработку DSP, платформ real-time bidding и ad exchange. Мы работаем в трёх форматах: полная разработка, выделенная команда или наши инженеры в вашей команде. Если вы взвешиваете переписывание против прохода по тюнингу, стоящий разговор сначала делает разделение и только потом выбирает язык.
Принесите текущую архитектуру и тот сценарий отказа, который вас беспокоит, — разберём его вместе за полчаса.