RSS Telegram YouTube Apple Яндекс Spotify Google Amazon Почта

44. Go tracer, 1BRC

24.03.2024

Скачать

К списку выпусков

Ссылки выпуска:

Улучшенный трейсинг в Go

Речь пойдет про статью, написанную Michael Knyszek, автора proposal по улучшению трейсинга в Go.

Напомню, пакет runtime содержит инструментарий для профайлинга ресурсов: CPU и памяти. Собрав статистику, можно анализировать её с помощью go tool pprof и обнаружить утечки памяти.

runtime/trace накапливает события во время выполнения программы: создание, блокировка, разблокировка go-рутин, момент передачи контроля системному вызову, его блокировка, события GC, работу тредов. Для анализа этих собранных данных используется go tool trace. Также можно использовать gotraceui. В этих инструментах на временной шкале можно увидеть события, которые происходили во время выполнения программы.

Благодаря runtime/trace профайлеру можно обнаружить проблемы конкурентного доступа к ресурсам, например, когда много go-рутин блокируются на чтение из/запись в канал. Для удобной навигации и расстановки нужных вам акцентов можно использовать задачи (NewTask), которые объединяют регионы (WithRegion()) в группы. Регион - это область на временной шкале визуализатора, где происходит какое-то событие. Также можно добавить лог запись в трейсинг данные в нужном вам месте и увидеть её в визуализаторе в контексте go-рутины, которая её вызвала.

Go Execution Tracer (runtime/trace) был создан как инструмент для live профайлинга и для обеспечения производительности был использован бинарный формат данных статистики и получение точного времени из регистра процессора TSC (комнад RDTSC). Несмотря на это runtime/trace потреблял от 10 до 20% CPU для большинства приложений.

Причина потери производительности была в stack unwinding (stack walking). Это процесс, когда профайлер собирает стек вызовов для каждого события, пробегая по по фреймам этого стека. Помимо этого нужно обогатить данные именем файла и строкой кода для вывода данных в визуализаторе. gettraceback (пакет runtime) функция исторически была реализована асинхронной, чтобы как можно меньше препятствовать работе программы, кроме этого пакет trace появился в версии Go 1.5 до того как в стеке вызовов был указатель на фрейм, где выполняется программа. С появлением frame pointer'а в Go 1.7 оптимизация стала возможна, но ей попросту никто не занимался.

Благодаря проведенной оптимизации начиная с Go 1.21 runtime/trace потребляет 1-2% CPU.

Пропоузал можно почитать здесь: proposal 60773. Главная цель - добавить в runtime/trace возможность стриминга данных трейсинга. Для этого необходимо разбить накопленные данные на партиции, что и было сделано и уже доступно в Go 1.22. Но go tool trace еще не зарефакторен, чтобы использовать новый формат стриминговых данных.

The One Billion Row Challenge (1BRC)

Я хотел рассказать про этот челлендж еще в позапрошлом выпуске, но болел и мы решили сделать выпуск коротким. А тут вышла еще одна статья, лучше предыдущей и раз уж мы затронули тему оптимизации, давайте обсудим этот челлендж. Важно, что автор - не мастер по оптимизации и если мне попадется статья от эксперта, то обязательно поделюсь с вами.

Итак, задача проста: прочитать файл с 1 миллиардом строк (13Гб) и вывести репорт. Каждая строка содержит 2 поля, разделенные ";": название метеорологической станции (строка), температура (плавающая точка). Отчет должен содержать: имя_станции=мин,сред,макс_температура, разделенные запятой.

Решение задачи "в лоб" дает результат 95 секунд. Следующий шаг добиться просто быстрого считывания файла, т.е. просто открываем файл, вычитываем все и дискардим, закрываем файл. Можно файл читать через bufio.Scanner, через bufio.Reader и напрямую, используя метод Read(). Метод Read() позвлоляет читать в слайс определенного размера. Слайс размером 4Мб дал наилучший результат - 0.98 секунды.

Следующий шаг - быстрый парсинг. Как раз pprof показывает, что больше всего времени уходит на bytes.Split() и strconv.ParseFloat(). Для этого пишем кастомный Split(), который копирует данные в заранее выделенный буфер, а не создает его каждый раз. А с темпиратурой работаем как с целым числом - читаем побайтово температуру и если это число, то добавляем его в разряд единиц, предварительно умножив результат на 10.

Далее, можно использовать Swiss Map - быстрая хэш-таблица, поддерживаемая комьюнити. Swiss Map использует быстрый механизм хеширования и механизм параллельного лукапа.

Можно читать из файла и писать полученный результат в канал, из которого воркеры (go-рутины) будут читать. А можно просто сделать дескриптор файла доступным/видимым для всех go-рутин.

Выводы: используем буферы, делаем профайлинг.