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

SRE: Аналіз продуктивності. Спосіб установки з використанням простого вебсервера на Go
10 хв. читання
13 серпня 2020

Аналіз продуктивності і тюнінг — потужний інструмент для перевірки того що продуктивність відповідає  вимогам.

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

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

Стратегія

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Це оточення використовує Vegeta для створення частоти запитів по 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.mcallmstart та 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 надає ефективні інструменти, вбудовані в стандартну бібліотеку, за допомогою яких можна зробити аналіз простим і доступним.

Джерело ENG: medium.com

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація