SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Аналіз продуктивності та налаштування – потужний інструмент перевірки відповідності продуктивності для клієнтів.

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

Go тут особливо добре підходить, оскільки має інструменти профілювання pprof у стандартній бібліотеці.

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

стратегія

Створимо зведений список для нашого структурного аналізу. Ми спробуємо використати деякі дані для прийняття рішень замість того, щоб вносити зміни, що ґрунтуються на інтуїції чи здогадах. Для цього зробимо так:

  • Визначаємо межі оптимізації (вимоги);
  • Розраховуємо транзакційне навантаження для системи;
  • Виконуємо тест (створюємо дані);
  • Спостерігаємо;
  • Аналізуємо — чи всі вимоги дотримуються?
  • Налаштовуємо по-науковому, робимо гіпотезу;
  • Виконуємо експеримент для перевірки цієї гіпотези.

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Архітектура простого сервера HTTP

Для цієї статті ми будемо використовувати маленький HTTP сервер на Golang. Весь код із цієї статті можна знайти тут.

Додаток, що аналізується - HTTP-сервер, який опитує Postgresql на кожен запит. Додатково є Prometheus, node_exporter та Grafana для збору та відображення метрик програми та системи.

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

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

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Визначаємо цілі

На цьому етапі визначаємося з метою. Що ми намагаємось проаналізувати? Як ми дізнаємося, що настав час закінчувати? У цій статті ми уявимо, що у нас є клієнти, і що наш сервіс оброблятиме 10 000 запитів на секунду.

В Google SRE Book докладно розглянуті способи вибору та моделювання. Вчинимо так само, побудуємо моделі:

  • Затримка: 99% запитів мають виконуватись менше ніж за 60мс;
  • Вартість: сервіс повинен споживати мінімальну суму грошей, яка нам здасться розумно можливою. Для цього максимізуємо пропускну спроможність;
  • Планування потужностей: потрібне розуміння та документування того, скільки екземплярів програми потрібно запустити, включаючи загальну функцію масштабування, а також скільки екземплярів знадобиться для задоволення початкових вимог щодо навантаження та забезпечення надмірності n+1.

Затримка може вимагати оптимізації на додачу до аналізу, але пропускну здатність явно треба проаналізувати. При використанні процесу SRE SLO вимога про затримку виходить від клієнта чи бізнесу, представлених власником продукту. І наш сервіс буде виконувати це зобов'язання від початку без будь-яких налаштувань!

Налаштовуємо тестове оточення

За допомогою тестового оточення ми зможемо видати дозоване навантаження на нашу систему. Для аналізу будуть створюватись дані про продуктивність вебсервісу.

Транзакційне навантаження

Це оточення використовує животіти для створення частоти запитів по HTTP, що настроюється, поки не буде зупинено:

$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta report

спостереження

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

Профільування

Профілювання - це тип вимірювання, що дозволяє побачити куди йде процесорний час під час роботи програми. Воно дозволяє визначити, де саме і скільки йде процесорного часу:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Ці дані можна використовувати під час аналізу, щоб отримати уявлення про марно витрачений процесорний час і виконувану непотрібну роботу. Go (pprof) може генерувати профілі та візуалізувати їх у вигляді flame graph, використовуючи стандартний набір інструментів. Я розповім про їх використання та посібник з налаштування трохи нижче в статті.

Виконання, спостереження, аналіз.

Проведемо експеримент. Ми виконуватимемо, спостерігатимемо і аналізуватимемо, поки продуктивність нас не влаштує. Виберемо довільно низьку величину навантаження, щоб застосувати для отримання результатів перших спостережень. На кожному з наступних кроків будемо збільшувати навантаження з деяким коефіцієнтом масштабування, вибраним з деяким розкидом. Кожен запуск тесту навантаження виконується з регулюванням числа запитів: make load-test LOAD_TEST_RATE=X.

50 запитів за секунду

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Зверніть увагу на два верхні графіки. Верхній лівий показує, що наша програма обробляє 50 запитів на секунду (на його думку), а верхній правий - тривалість кожного запиту. Обидва параметри допомагають нам дивитися та аналізувати: чи влазимо в наші межі продуктивності чи ні. Червона лінія на графіку HTTP Request Latency показує SLO 60мс. По лінії видно, що ми набагато нижчі від нашого максимального часу відповіді.

Подивимося з боку вартості:

10000 запитів на секунду / на 50 запитів на сервер = 200 серверів + 1

Ми все ще можемо покращити цей показник.

500 запитів за секунду

Більш цікаві речі починають відбуватися, коли навантаження стає 500 запитів в секунду:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Знову ж таки на лівому верхньому графіку видно, що додаток фіксує звичайне навантаження. Якщо це не так, є проблема на сервері, на якому запущено програму. Графік із затримкою відповіді розташований зверху праворуч, показує, що 500 запитів за секунду призвели до затримки відповіді 25-40мс. 99 перцентиль все ще шикарно влазить у SLO 60мс, вибраний вище.

З погляду вартості:

10000 запитів на секунду / на 500 запитів на сервер = 20 серверів + 1

Досі можна поліпшити.

1000 запитів за секунду

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Чудовий запуск! Програма показує, що обробило 1000 запитів на секунду, однак кордон із затримкою було порушено з боку SLO. Це видно по лінії p99 на правому верхньому графіку. Незважаючи на те, що лінія p100 набагато вища, реальні затримки вищі максимуму в 60мс. Давайте пірнемо в профільування, щоб дізнатися, що насправді робить додаток.

Профільування

Для профілювання ми ставимо навантаження у 1000 запитів на секунду, потім використовуємо pprof для зняття даних, щоб дізнатися, де програма витрачає процесорний час. Це можна зробити активуючи HTTP endpoint pprof, після чого при навантаженні зберегти результати за допомогою curl:

$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.prof

Результати можуть бути відображені так:

$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Графік показує, де і скільки програма витрачає процесорного часу. З опису від Brendan Gregg:

По осі X - заповнення профілю стека, відсортований в алфавітному порядку (це не час), вісь Y показує глибину стека, рахуючи від нуля в [top]. Кожен прямокутник – кадр стека. Чим ширший кадр - тим частіше вона присутня у стеках. Те, що зверху — працює на CPU, а нижче — дочірні елементи. Кольори, зазвичай, нічого не означають, а просто вибираються випадковим чином розрізнення кадрів.

Аналіз - гіпотеза

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

Дотримуючись рекомендацій Brendan Gregg, ми будемо читати графік зверху вниз. Кожен рядок відображає кадр стека (дзвінок функції). Перший рядок - точка входу в програму, батько всіх інших викликів (тобто всі інші виклики будуть мати її у своєму стеку). Наступний рядок вже відрізняється:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Якщо навести курсор на ім'я функції на графіці, відобразиться загальний час, скільки вона була в стеку під час налагодження. Функція HTTPServe була там 65% часу, інші функції runtime, runtime.mcall, mstart и gc, зайняли решту часу. Цікавий факт: 5% загального часу витрачено на запити у DNS:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Адреси, які програма шукає, належать Postgresql. Клацаємо по FindByAge:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Цікаво, що програма показує, що в принципі є три основні джерела, які додають затримки: відкриття закриття з'єднань, запит даних і з'єднання з базою. На графіку видно, що запити DNS, відкриття і закриття з'єднань займають близько 13% всього часу виконання.

Гіпотеза: перевикористання з'єднань за допомогою пулу потрібно скоротити час одного запиту по HTTP, дозволяючи більш високу пропускну здатність і нижчі затримки.

Налаштування програми - експеримент

Оновлюємо вихідний код, пробуємо забрати з'єднання з Postgresql на кожен запит. Перший варіант - використання пула з'єднань на рівні програми. У цьому експерименті ми налаштуємо пул з'єднань за допомогою драйвера sql для go:

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)

if err != nil {
   return nil, err
}

Виконання, спостереження, аналіз

Після перезапуску тесту з 1000 запитами в секунду видно, що p99 за затримками прийшов у норму зі SLO 60мс!

Що за вартістю?

10000 запитів на секунду / на 1000 запитів на сервер = 10 серверів + 1

Давайте зробимо ще краще!

2000 запитів за секунду

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Подвоєння навантаження показує те ж саме, лівий верхній графік демонструє, що програма встигає відпрацювати 2000 запитів за секунду, p100 нижче 60мс, p99 задовольняє SLO.

З погляду вартості:

10000 запитів на секунду / на 2000 запитів на сервер = 5 серверів + 1

3000 запитів за секунду

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Тут програма може обробити 3000 запитів із затримкою p99 менше 60мс. SLO не порушується, а вартість прийнята так:

10000 запитів на секунду / на 3000 запитів на сервер = 4 сервери + 1 (автор округлив до більшого, прим. перекладача)

Спробуємо ще один раунд аналізу.

Аналіз - гіпотеза

Збираємо та відображаємо результати налагодження програми на 3000 запитів на секунду:

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Все ще 6% часу витрачається на встановлення з'єднань. Налаштування пулу покращило продуктивність, але все ще видно, що додаток продовжує роботу зі створення нових з'єднань з базою.

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

Налаштування програми - експеримент

Пробуємо встановити MaxIdleConns рівним розміру пулу (також описано тут):

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
   return nil, err
}

Виконання, спостереження, аналіз

3000 запитів за секунду

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

p99 менше 60мс із значно меншим p100!

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Перевірка flame graph показує, що встановлення з'єднання більше не помітне! Перевіряємо детальніше pg(*conn).query - також не помічаємо установки з'єднання тут.

SRE: Аналіз продуктивності. Спосіб налаштування з використанням простого веб-сервера на Go

Висновок

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

Джерело: habr.com

Додати коментар або відгук