Профайлинг в Go: як зменшити час виконання запитів

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт, мене звати Владислав Пістун, я Tech Lead однієї з команд у Solidgate — українській продуктовій фінтех-компанії, що працює в ніші пейментів і допомагає розбудовувати платіжну інфраструктуру для інтернет-бізнесів у всьому світі.

Основний стек, на якому працює Solidgate, — Go та Kotlin. Моя команда працює на Go.

Фінтех-ринок вимагає дуже швидких реакцій системи на зміни, стабільної роботи та розвиненої інфраструктури. Зокрема, ми не можемо дозволити собі занадто тривалих затримок у відпрацюванні коду — це впливає на швидкість оплати, завантаження платіжних сторінок тощо.

Оскільки тривалий процес проведення платежів стає негативним для користувача, наша задача — оптимізувати код так, щоб будь-яка операція відпрацьовувала максимально швидко.

Профайлинг у Go: навіщо

Профайлинг є важливою технікою для аналізу програм і виявлення вузьких місць, які перешкоджають ефективності. Корисно виявити, які частини коду виконуються надто довго або споживають забагато ресурсів, таких як СPU і пам’ять.

Профайлинг — відмінний інструмент для покращення та оптимізації коду там, де система проходить критичні навантаження.

Це дозволяє швидко зрозуміти, які саме функції забирають значну частину пам’яті, більш того — візуалізує час виконання та використання СPU. Таким чином набагато легше та швидше зрозуміти проблеми та виправити їх, коли можна побачити дані на графіку.

Нещодавно я передивлявся виступи спікерів на GopherCon 2023. Крутий виступ про профайлинг надихнув мене також використати техніку для нас. Я взяв один сервіс та спробував перевірити, наскільки якісний код створює наша команда.

Усе почалось ввечері п’ятниці, взявши під руку чашку кави та сервіс платіжної форми Solidgate, я почав «копати», чи ефективно він працює.

Етап підготовки

Для початку я додав в сервіс профайлинг-бібліотеку і почав профайлинг. Щоб зібрати достатньо даних для перевірки, я використав vegeta для навантаження сервісу. А що це?

Перш ніж можна приступити до профайлингу, потрібно згенерувати великий обсяг трафіку та створити пікове навантаження на систему. Це потрібно, що побачити певні шаблони поведінки сервісу та відпрацювання коду під час спайків.

Для таких випадків існує відмінний інструмент тестування систем під навантаженням — Vegeta. Це універсальне рішення для тестування навантаження, яке народилося через необхідність критично грузити HTTP-сервіси великою кількістю запитів, що надходять до сервісів із заданою частотою.

У нашому випадку Vegeta — саме той інструмент, який мені був потрібен, оскільки він дозволяє створити безперервний потік запитів до системи. Цими запитами можна «обстрілювати» проєкт стільки, скільки потрібно, щоб з’ясувати такі показники, як виділення/використання пам’яті, особливості роботи горутин, та час, витрачений на «збір сміття».

Підсумовуючи — це потрібно, щоб визначити, як ми алокуємо пам’ять і використовуємо СPU time.

Збір даних

Golang має дві попередньо визначені конфігурації асинхронного профайлера: профайлинг CPU і розподілу пам’яті. Обидві конфігурації налаштовані для отримання максимально точних результатів, тому не потребують змін. Однак, якщо вам однаково потрібно змінити налаштування, це можна зробити за допомогою Async Profiler на GitHub.

Що ж, для початку нам слід додати бібліотеку в код і вибрати режим профайлинга. У цій статті ми розглянемо саме CPU profiling, оскільки саме він цікавий у нашому кейсі.

CPU профайлинг показує, які функції споживають який відсоток процесорного часу. Ця інформація допоможе краще зрозуміти, як виконується програма та як саме розподіляються ресурси. Після завершення аналізу профайлер візуалізує вихідні дані.

Ми пройшли підготовчий етап — тож запускаємо проєкт і навантажувальні тести для збору датасета.

import "github.com/pkg/profile"


func main() {
    p := profile.Start(profile.CpuProfile, profile.ProfilePath("."), profile.NoShutdownHook)
              defer p.Stop()
...
}

Непрості дії, правда ж? Тепер залишилось візуалізувати датасет.

go tool pprof -http=:6060 cpu.pprof

Після запуску даної команди, ми отримуємо візуалізацію на localhost:6060 і можемо перейти до етапу аналізу даних, що ми отримали.

Аналіз зібраних даних

Що ж, почнемо аналіз того, що ми зібрали.

На візуалізації видно, що багато часу процесора забирає WeightedChoice. Оскільки це — код, що був потрібний для минулих А/В-тестів, можемо видалити його для полегшення програми.

Після першої оптимізації — знову запускаємо профайлинг і проводимо збір даних повторно.

На наступній візуалізації помітно, що найбільше CPU використовує один із запитів до бази даних. Щоб уникнути такого навантаження, ми трансформуємо декілька запитів у один, щоб зекономити процесинговий час.

Покращуємо код і повторюємо. Ще раз і ще раз...

Після попередніх оптимізацій і ще одного запуску профайлингу бачимо, що впираємось в UUID. UUID (універсальний унікальний ідентифікатор) — це 128-бітне значення, яке використовується для унікальної ідентифікації об’єкта або сутності в Інтернеті.

Яка найбільша проблема з UUID? Оскільки вставки відбуваються у випадкових місцях, це вимагає багато операцій вводу-виводу.

Випадкові UUID генеруються повільніше, ніж цілі числа. Проблема виникає через те, що випадкові UUID рівномірно розподілені в їхньому великому діапазоні значень. Отже вставлення даних в індекси за допомогою цих UUID призводить до поганої локальності даних.

UUID мають довжину 128 біт, порівняно з 32 бітами для типового цілого числа — цей більший розмір може призвести до збільшення вимог до пам’яті та зниження продуктивності, особливо при роботі з великими наборами даних. Індекси, побудовані на стовпцях UUID, можуть працювати не так ефективно, як індекси на менших типах даних.

Для полегшення програми в нашому випадку рухаємось іншим шляхом — беремо декілька бібліотек для UUID, а також власну реалізацію. Запускаємо go test bench (benchmark test) та обираємо найкращу з точки зору перформанса.

Benchmark test — це тип функції, яка виконує сегмент коду декілька разів і порівнює кожен результат зі стандартом, оцінюючи загальний рівень продуктивності коду. Go має вбудовані інструменти для написання тестів у пакет тестування та інструмент Go, тож можна писати корисні тести без встановлення будь-яких залежностей.

У benchmark test важливо зберегти декілька речей, які суттєво впливають на результати. Зокрема значно вплинути може стан машини, на якій виконується тест. Вплив керування живленням, фонові процеси, керування температурою — все це може вплинути на результати тестування, зробивши їх неточними та нестабільними.

Тому важливо мінімізувати вплив навколишнього середовища на код, наскільки це можливо. Для цього ідеально використовувати віддалений сервер, де нічого більше не запущено, щоб виконувати benchmark test.

Однак якщо доступу до такого сервера немає, слід закрити якомога більше програм перед запуском тесту, щоб мінімізувати вплив інших процесів на результати тесту. Ще один варіант — обмежити ресурси при запуску теста. Як? Виставляємо ліміт по CPU, тоді вплив ресурсів не буде таким критичним. Приклад коду:

GOMAXPROCS=1 go test -bench

Крім того, щоб забезпечити більш стабільні результати, варто запускати тестування декілька разів перед записом вимірювань, переконавшись, що система достатньо розігріта. І нарешті, дуже важливо ізолювати код, що перевіряється, від решти програм ( наприклад, імітуючи мережеві запити).

Бенчмарк тести точно заслуговують на окрему статтю. Але найкраща UUID бібліотека — тут.

Результати в PROD

Що ж, наші махінації з кодом, використання профайлингу, навантаження системи та невеликі оптимізації допомогли нам зменшити час виконання запитів майже на 30%!

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую за статтю на основі реального прикладу

Ви запити до Postgres пишете на голому SQL й покриваєте тестами?
Чи використовуєте sqlc?
У вас в проєкті Postgres чи якась інша сумісна БД?

Запити на чистому sql пишемо та база Postgres

Щодо тестів, то запити покриваємо функціональними тестами

Запити на чистому SQL приємніші за github.com/go-gorm/gorm та все ж таки придивіться до github.com/sqlc-dev/sqlc, який вже використовується у EVO Fintech

У DocHQ, sqlc зберігає багато часу розробникам

Щодо gorm згоден повністю, дякую за посилання, теж гляну

Есть ещё xorm, API там идентичное с gorm, только поддерживает больше баз данных. У gorm комьюнити больше. Если выбирать проработанный ORM для Go, то выбор будет между gorm и xorm.

Ще є ось таке: jmoiron.github.io/sqlx. Не так давно ThePrimagen розбирав обидві лібки. youtu.be/...​ly7CU?si=HBw2sm2FdsowR_FZ

Підписатись на коментарі