Русский
Русский
English
Статистика
Реклама

Охота за убегающей памятью в Go на этапе разработки

Проблемы

Убегание памяти в кучу влечет за собой следующие потенциально решаемые проблемы:

  1. Снижение производительности из за расходов на выделение памяти

  2. Снижение производительности из-за расходов на сборку мусора

  3. Появление ошибкиOut of Memory , если скорость появления мусора превышает скорость его уборки

Указанные проблемы могут решаться несколькими способами:

  1. Увеличением объема вычислительных ресурсов (память, процессор)

  2. Тонкой настройкой механизма сборщика мусора

  3. Минимизацией числа побегов в кучу

В данной статье я рассмотрю только третий путь.

С чистого листа

Если все еще впереди, но уже поставлена цель добиться производительности, близкой к максимально возможной, то нужно знать в лицо главных замедлителей в плане работы с памятью. Встречаем основные конструкции, число которых следует минимизировать: make , new , map ,go . Есть и более скрытые способы учинить побег, их я рассмотрю уже в процессе "охоты", а пока - основные способы профилактики.

Вместо постоянного выделения памяти через make и new следует максимально переиспользовать уже ранее выделенное. Одним из способов добиться такого переиспользования является sync.Pool(), на habr этот способ был рассмотрен здесь. Чтобы поменьше быть КО замечу, что использовать элементы типа []byte ,как это делается в статье по сылке, не стоит - при каждом возврате будет дополнительно выделяется 32 байта памяти (для go1.14.6 windows/amd64). Мелочь, но неприятно; если стремиться к совершенству, лучше переиспользовать интерфейсы или указатели, а еще лучше использовать butebuferpool от @valyala.

С map история такая. Интенсивное использование map ведет к интенсивному выделению памяти, но это не единственная проблема. Если приложению нужен огромный кэш, и этот кэш реализован через map, то можем получить то, из-за чего Discord перешел на Rust. Т.е. на постоянное, в рамках уборки мусора, сканирование гигантского скопления указателей будут тратится ресурсы, и по каким-то метрикам система выйдет за рамки требований. Для решения этой проблемы великий @valyala сделал fastcache, там же можно найти и ссылки на альтернативные решения, и, опять же у него, наряду с другими советами по повышению производительности, можно найти достаточно детальный разбор, как использовать slices вместо maps.

С оператором go просто - все обработку нужно развести по фиксированному набору горутин и каналов. Запускать горутину на каждый запрос можно, но относительно дорого и плохо предсказуемо по расходу памяти.

Имеет смысл сделать такое замечание, и я его сделаю - предотвращение массовых "побегов" имеет свою цену, в частности, упомянутый fastcache далеко не "идиоматичен". Нам, например, идеально подходит кэш []byte->[]byteно, не факт, что это так для всех. Возможно, дешевле будет усилить аппаратную часть, а то и вообще ничего не делать - все зависит от требований к системе, те самые "rps", "95th percentile latency" и т.д. Возможно, и даже скорее всего, все будет работать и в "коробочном" варианте, да еще и с запасом. Так что будет вполне разумным сделать прототип "горячих путей" обработки и погонять на скорость. Т.е. заняться той самой "охотой".

Охота

Пойдем опять "на поклон" к Александру Валялкину и выполним:

git clone https://github.com/valyala/fasthttp

Чтобы начать охоту не обязательно добавлять в тесты на производительность некий профилирующий код, как это предлагается, например, здесь, просто наберем:

go test -bench=PServerGet10Req -benchmem -memprofile netmem.out

и

go test -bench=kServerGet10Req -benchmem  -memprofile fastmem.out

Первая команда запустит тесты для стандартного http.Server, вторая - для fasthttp.Server. По выводу мы заметим, что fasthttp примерно в десять раз быстрее и все операции проходят в zero-allocation режиме. Но это не все, теперь у нас есть профили netmem.out и fastmem.out. Смотреть их можно по разному, для быстрой оценки ситуации я предпочитаю такой способ:

echo top | go tool pprof netmem.out

Что дает разбивку потребления памяти по 10 самым "прожорливым" функциям:

Showing top 10 nodes out of 53      flat  flat%   sum%        cum   cum%  698.15MB 21.85% 21.85%   710.15MB 22.22%  net/textproto.(*Reader).ReadMIMEHeader  466.13MB 14.59% 36.43%   466.13MB 14.59%  net/http.Header.Clone  423.07MB 13.24% 49.67%  1738.32MB 54.39%  net/http.(*conn).readRequest  384.12MB 12.02% 61.69%   384.12MB 12.02%  net/textproto.MIMEHeader.Set  299.07MB  9.36% 71.05%  1186.24MB 37.12%  net/http.readRequest  137.02MB  4.29% 75.33%   137.02MB  4.29%  bufio.NewReaderSize  134.02MB  4.19% 79.53%   134.02MB  4.19%  net/url.parse  122.45MB  3.83% 83.36%   122.45MB  3.83%  bufio.NewWriterSize (inline)   99.51MB  3.11% 86.47%   133.01MB  4.16%  context.WithCancel   87.11MB  2.73% 89.20%    87.11MB  2.73%  github.com/andybalholm/brotli.(*h5).Initialize

Можно получить подробную схему убеганий в графическом виде через:

go tool pprof -svg -output netmem.svg netmem.out

После выполнения команды в netmem.svg будет картинка типа такой (фрагмент):

Есть и более крутой способ:

go tool pprof -http=:8088 netmem.out

Здесь, по идее, должен запуститься браузер, и этот браузер с какой-то вероятностью покажет текст: Could not execute dot; may need to install graphviz.Те, кто работает на Unix-подобных системах и так знают, что делать, пользователям же Windows могу посоветовать поставить chocolatey а затем, с правами администратора, вызвать cinst graphviz. После этого можно начать по всякому крутить профиль. Моя любимая крутилка вызывается через VIEW/Source:

Здесь, кроме очевидных убеганий через make, мы также видим большие потери на преобразование []byteв string. Операции со строками весьма затратны и, если "идем на рекорд", их следует избегать и работать исключительно с []byte. Еще одним способом "убежать", с которым встречался, является возврат адреса локальной переменной, т.е. return &localVar . Есть и другие варианты, но углубляться не буду - ваш личный профиль их покажет.

Несмотря на сокрушительное превосходство fasthttp в этом тесте, именно эту библиотеку я не рекомендовал бы использовать. Или рекомендовал бы с осторожностью - с fasthttp у вас не будет поддержки HTTP/2.0, поддержка websockets отполирована не с такой тщательностью, как сам fasthttp (на момент, когда я эту тему изучал), ну и, главное, на реальной нагрузке может и не получиться десятикратного выигрыша. У нас в одном тесте на железе типа c5.4xlarge получалось 250.000 RPS для fasthttp.Server против 190.000 RPS для http.Server . Выигрышь есть, но вам точно надо больше, чем 190.000 RPS? Тут очень многое зависит от профиля нагрузки, от того, что с этой нагрузкой делается дальше, ну и от требований к системе, само собой.

Последним моментом, которого хочу коснуться, является сериализация данных. Построив прототип "горячего пути" для своего приложения и погоняв тесты есть шанс увидеть, что самой прожорливой частью являются преобразования из json/yaml в объекты программы и обратно, и на фоне этой прожорливости меркнет все остальное, что бы вы не пытались написать. Выбор решения тут будет весьма нетривиален и его хоть сколько-нибудь полное описание выходит за рамки статьи, поэтому ограничусь кратким результатом наших, далеко не репрезентативных, тестов.

Вполнив ряд экспериментов, мы остановились на flatbuffers, но сделали вокруг него обертку dynobuffers, назначение которой - читать и писать без генерации кода, используя имена полей. Вещица пока сыровата, но, надеюсь, скоро доделаем.

Результаты чтения "все поля большого объекта":

Avro         23394 ns/op    11257 B/opDyno_Untyped  6437 ns/op      808 B/opDyno_Typed    3776 ns/op        0 B/opFlat          1132 ns/op        0 B/opJson         87331 ns/op    14145 B/op

Результаты чтения "несколько полей большого объекта":

Avro         19311 ns/op    11257 B/opDyno_Typed    62.2 ns/op        0 B/opFlat          19.8 ns/op        0 B/opJson         83824 ns/op    11073 B/op 

Последний сценарий является для нас основным, ради которого все и затевалось, и здесь ускорение, по сравнению с тем же linkedin/goavro - весьма и весьма существенное.

Опять скажу - все зависит от конкретных данных и способов их обработки. Например, весь выигрышь на (де)сериализации можно потерять при сохранении, ибо avro часто дает "пакует" данные более компактно, чем flatbuffer.

Заключение

С проблемами, которые ведут к снижению проивзодительности в Go, вполне можно бороться, но нужно иметь ввиду, что эта борьба имеет свою цену. Прежде, чем начинать ее, лучше экспериментально сопоставить возможности выбранного способа обработки данных с требованиями к системе - может быть, все будет работать прямо "из коробки" с минимальными ухищрениями.

Ссылки

Источник: habr.com
К списку статей
Опубликовано: 22.09.2020 16:11:47
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Высокая производительность

Go

Golang

Память

Оптимизация программ

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru