The Go Blog
Go GC: Приоритет низкой задержки и простоты
Настройка
Go создаёт сборщик мусора (GC), не только для 2015 года, но и для 2025 и дальнейших лет: сборщик мусора, который поддерживает современную разработку программного обеспечения и масштабируется вместе с новым ПО и оборудованием в течение следующего десятилетия. Такое будущее не может позволить себе пауз сборщика мусора, которые останавливают мир (stop-the-world), поскольку они являются препятствием для более широкого использования безопасных и защищённых языков, таких как Go.
Go 1.5, первое проявление этого будущего, достигает задержек сборки мусора, которые значительно ниже целевой отметки в 10 миллисекунд, которую мы установили год назад. Мы представили некоторые впечатляющие цифры в докладе на Gophercon. Улучшения задержек вызвали большой интерес; блог-пост Робина Верлангена Billions of requests per day meet Go 1.5 подтверждает наш путь с конечными результатами. Мы также особенно ценили графики сервера Алан Шрева и его комментарий «Holy 85% reduction».
Сегодня 16 гигабайт оперативной памяти стоят $100, а процессоры оснащаются множеством ядер, каждое из которых имеет несколько аппаратных потоков. Через десять лет такое оборудование будет казаться устаревшим, но программное обеспечение, разрабатываемое сегодня на Go, должно масштабироваться, чтобы удовлетворять растущим потребностям и следующему великому достижению. Учитывая, что оборудование обеспечит мощность для увеличения пропускной способности, сборщик мусора Go разрабатывается с приоритетом низкой задержки и настройки только через одну ручку. Go 1.5 — это первый крупный шаг по этому пути, и эти первые шаги навсегда повлияют на Go и приложения, которые он лучше всего поддерживает. Эта статья даёт общую картину того, что было сделано для сборщика мусора Go 1.5.
Улучшение
Чтобы создать сборщик мусора на следующее десятилетие, мы обратились к алгоритму из десятилетий назад. Новый сборщик мусора Go — это конкурентный, трёхцветный, маркирующий-сборочный сборщик, идея, впервые предложенная Дейкстрой в 1978 году. Это намеренное отклонение от большинства «корпоративных» сборщиков мусора современности, и мы считаем, что такой подход хорошо подходит для свойств современного оборудования и требований к задержкам современного ПО.
В трёхцветном сборщике каждая объект находится в одном из трёх состояний: белый, серый или чёрный, и мы рассматриваем кучу как граф связанных объектов. В начале цикла сборки мусора все объекты белые. Сборщик мусора посещает все корни, которые представляют собой объекты, напрямую доступные приложению, такие как глобальные переменные и элементы стека, и окрашивает их в серый цвет. Затем сборщик мусора выбирает серый объект, делает его чёрным и сканирует его в поиске указателей на другие объекты. Когда этот скан находит указатель на белый объект, он окрашивает этот объект в серый цвет. Этот процесс повторяется до тех пор, пока не останется серых объектов. В этот момент белые объекты признаны недоступными и могут быть переиспользованы.
Все это происходит параллельно с работой приложения, известного как мутатор, который изменяет указатели во время работы сборщика мусора. Следовательно, мутатор должен поддерживать инвариант, согласно которому ни один чёрный объект не указывает на белый объект, иначе сборщик мусора может потерять отслеживание объекта, установленного в части кучи, которую он уже посетил. Поддержание этого инварианта — задача барьерной записи (write barrier), которая представляет собой небольшую функцию, вызываемую мутатором всякий раз, когда модифицируется указатель в куче. Барьерная запись в Go окрашивает теперь достижимый объект в серый цвет, если он был белым, обеспечивая тем самым, что сборщик мусора в конечном итоге просканирует его на наличие указателей.
Определение момента завершения задачи по поиску всех серых объектов — это тонкий момент, который может быть затратным и сложным, если требуется избежать блокировки мутаторов. Чтобы упростить процесс, Go 1.5 выполняет максимально возможную часть работы параллельно, а затем кратковременно останавливает мир (stop-the-world), чтобы проверить все потенциальные источники серых объектов. Нахождение оптимального баланса между временем, необходимым для этого финального stop-the-world, и общим объемом работы, которую выполняет GC, стало важной задачей для Go 1.6.
Конечно, как говорится, диавол кроется в деталях. Когда начинать цикл сборки мусора? Какие метрики использовать для принятия этого решения? Как сборщик мусора должен взаимодействовать со шедулером Go? Как остановить поток мутатора достаточно долго, чтобы просканировать его стек? Как представить белые, серые и чёрные объекты, чтобы эффективно находить и сканировать серые объекты? Как определить, где находятся корни? Как определить, где в объекте находятся указатели? Как минимизировать фрагментацию памяти? Как справиться с проблемами производительности кэша? Какого размера должна быть куча? И так далее — некоторые вопросы связаны с аллокацией, некоторые — с поиском достижимых объектов, некоторые — со шедулингом, но многие — с производительностью. Низкоуровневые обсуждения каждого из этих аспектов выходят за рамки данного блог-поста.
На более высоком уровне один из подходов к решению проблем производительности — это добавление регулировочных элементов (knobs) для каждой из проблем производительности. Программист может затем поворачивать эти элементы в поисках подходящих настроек для своего приложения. Недостатком такого подхода является то, что, если в течение десяти лет добавлять по одному или двум новым регулировкам ежегодно, в итоге получается закон, подобный «Закону о занятости на регуляторах GC» (GC Knobs Turner Employment Act). Go не пойдёт этим путём. Вместо этого мы предоставляем один регулировочный элемент, называемый GOGC. Это значение контролирует общую величину кучи относительно размера достижимых объектов. Значение по умолчанию 100 означает, что общая величина кучи теперь на 100% больше (то есть в два раза) размера достижимых объектов после последней сборки. Значение 200 означает, что общая величина кучи на 200% больше (то есть в три раза) размера достижимых объектов. Если вы хотите сократить общее время, затрачиваемое на сборку мусора, увеличьте GOGC. Если вы хотите пожертвовать более длительным временем сборки мусора ради меньшего объема памяти, уменьшите GOGC.
Более важно то, что при удвоении объема оперативной памяти в следующем поколении оборудования просто удвоение GOGC уменьшит количество циклов сборки мусора вдвое. С другой стороны, поскольку GOGC рассчитан на размер достижимых объектов, удвоение нагрузки путем удвоения достижимых объектов не требует перенастройки. Приложение просто масштабируется. Более того, несмотря на отсутствие поддержки десятков регулировок, команда среды выполнения может сосредоточиться на улучшении производительности на основе обратной связи от реальных приложений клиентов.
Заключение
Сборщик мусора Go 1.5 открывает будущее, в котором паузы stop-the-world больше не являются барьером для перехода на безопасный и надежный язык программирования. Это будущее, в котором приложения масштабируются без усилий вместе с оборудованием, и по мере того как оборудование становится более мощным, сборщик мусора не станет препятствием для создания более эффективного и масштабируемого программного обеспечения. Это хорошее место для следующего десятилетия и далее.
За дополнительными сведениями о сборщике мусора 1.5 и о том, как мы устранили проблемы задержек, см. презентацию «Go GC: Latency Problem Solved» или слайды.
Следующая статья: Golang UK 2015
Предыдущая статья: Выпущен Go 1.5
Индекс блога