В начале 2025 года команда Datadog приступила к внедрению новой версии языка программирования Go 1.24. Главной анонсированной инновацией стало внедрение Swiss Tables — новых высокоэффективных хеш-таблиц, способных снизить нагрузку на процессор и объем потребляемой памяти. Ожидания оправдывались техническим прогрессом и улучшенной производительностью, но вскоре после начала развертывания обновления в крупных службах обработки данных команды столкнулись с неожиданной проблемой — резким увеличением использования оперативной памяти примерно на 20%. Эта ситуация стала поводом для детального расследования причин и поиска решения, ибо Go должен был, наоборот, улучшить ситуацию с расходами ресурсов.
Начало поисков причины регрессии памяти связано с тщательным анализом изменений в Go 1.24. Первым шагом стало исключение возможности влияния основных новых функций, таких как Swiss Tables и обновленного механизма работы с мьютексами (spin bit mutex). Для этого исследователи собрали специальные сборки с отключенными экспериментальными функциями. Однако эти эксперименты не привели к снижению потребления памяти, что указывало на то, что корень проблемы лежит в другом месте.
Следующий этап включал глубокий анализ метрик Go runtime — инструментов, предоставляющих данные о состоянии управления памятью интерпретируемого процесса: распределение кучи, сборка мусора и другие параметры. Здесь обнаружилось парадоксальное несоответствие — внутренняя статистика Go показывала стабильное использование памяти, тогда как системные метрики, особенно измеряющие RSS (Resident Set Size, объем памяти, реально находящейся в физической оперативной памяти), фиксировали существенный рост. RSS отражает реальную загруженность RAM, в отличие от виртуальной адресной памяти, количество которой сложнее измерить и которая включает неиспользуемые или отложенные к выделению страницы. Чтобы понять природу расхождения, инженеры перешли к исследованию через инструменты Linux, такие как /proc/[pid]/smaps, которые предоставляют детальную разбивку по виртуальным и реальным страницам памяти для каждого процесса. Здесь было выявлено, что именно область кучи Go, ответственной за размещение динамических объектов, стала причиной аномального роста использования физической памяти.
Сравнение данных между Go 1.23 и Go 1.24 показало, что в новой версии почти вся выделенная виртуальная память кучи оказалась закоммиченной (помещённой в оперативную память), в то время как в предыдущей версии существенная часть виртуальной памяти оставалась отложенной и не занимала реальной RAM. Это наблюдение указывало, что изменился механизм управления или инициализации выделенной памяти. Изучая changelog Go 1.
24, специалисты обратили внимание на крупный рефакторинг функции mallocgc — ключевого аллокатора памяти в runtime Go. Эта функция отвечает за выделение, инициализацию и подготовку памяти для использования программой, что напрямую влияет на производительность и расход ресурсов. Чтобы подтвердить догадки, команда Datadog обратилась к сообществу Go, используя каналы коммуникации разработчиков и экспертов, таких как Gophers Slack. Один из ведущих специалистов по сбору мусора и менеджменту памяти в Go, PJ Malloy (thepudds), помог провести более детальный анализ с применением heapbench — инструмента для проведения бенчмарков и тестирования поведения выделения памяти. Анализ heap profiles выявил, что основная часть памяти уходит на буферизованные каналы и карты (maps), причем ухудшение влияло особенно на большие аллокации свыше 32 Кбайт, содержащие указатели на другие объекты.
В результате экспериментов было зафиксировано, что именно такие крупные блоки памяти в Go 1.24 проявляют двукратно увеличенный RSS, тогда как более мелкие или не содержащие указатели блоки поведенчески сохранялись без изменений. С помощью git bisect удалось локализовать конкретный коммит, в котором в рамках рефакторинга mallocgc была случайно потеряна оптимизация, позволявшая не затирать память нулями в случаях, когда ОС уже предоставляет гарантию нулевой инициализации страниц. Это привело к избыточной инициализации больших объектов с указателями, из-за чего страницы виртуальной памяти вынужденно коммитились в реальную RAM, что объясняло рост RSS без изменений в логировании на уровне Go runtime. Исправление этой ошибки вернуло прежнее поведение, сохранив при этом гарантии безопасности при сборке мусора.
Были проведены тесты с исправленной версией компилятора, которые подтвердили значительное снижение потребления физической памяти и разрешение проблемы. По итогам расследования и тестирования исправление было внесено в будущих версиях Go, и для версии 1.25 планируется официальное включение заплаты, с обсуждением возможности бэкпорта к 1.24. Возвращаясь к развертыванию в продуктиве, специалисты Datadog смогли прогнозировать потребление памяти с учетом исправления.
В средах с низкой нагрузкой использование памяти после обновления стабилизировалось и стало соответствовать внутренним метрикам Go. Интересным наблюдением оказалось иное поведение в высоконагруженных окружениях, где обе метрики — виртуальная и резидентная память — значительно снизились, что связано с оптимизацией Swiss Tables для больших объемов данных в памяти. Этот опыт стал ярким примером важности комплексного мониторинга, тесного взаимодействия с сообществом разработчиков и внимательного анализа поведения системных и прикладных инструментов. Разработка и отладка таких технологий, как Go, которые лежат в основе множества современных сервисов, часто требует умения выявлять тонкие технические детали и нетривиальные взаимодействия между уровнем языка, системой и оборудованием. Благодаря открытому сотрудничеству и совместному поиску решений удается не только обеспечивать высокую производительность ПО, но и поддерживать устойчивость инфраструктуры, обеспечивая пользователям и бизнесу надежность и эффективное использование ресурсов.
Предстоящие публикации продолжат рассказывать, как Swiss Tables в конечном итоге помогли сэкономить сотни гигабайт памяти за счет оптимизации структуры данных и внутренних алгоритмов, приводя к заметным улучшениям скорости и масштаба работы сервисов Datadog.