Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

У доповіді подано деякі підходи, які дозволяють стежити за продуктивністю SQL-запитів, коли їх мільйони на добу, а контрольованих серверів PostgreSQL – сотні.

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


Кому цікавий розбір конкретних проблем та різні техніки оптимізацій SQL-запитів та рішення типових DBA-задач у PostgreSQL — можна також ознайомитись із серією статей на цю тему.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)
Мене звуть Кирило Боровиков, я уявляю компанію «Тензор». Саме я спеціалізуюся на роботі з базами даних у нашій компанії.

Сьогодні я вам розповім, як ми оптимізуємо запити, коли вам треба не «розколупати» продуктивність якогось одного запиту, а вирішити проблему масово. Коли запити мільйони, і вам треба знайти якісь підходи до вирішення цієї великої проблеми.

Взагалі «Тензор» для мільйона наших клієнтів — це НВІС — наш додаток: корпоративна соціальна мережа, рішення для відеозв'язку, для документообігу внутрішнього та зовнішнього, облікові системи для бухгалтерії та складу, тобто такий «мегакомбайн» для комплексного управління бізнесом, в якому понад 100 різних внутрішніх проектів.

Щоб усі вони нормально працювали та розвивалися – у нас 10 центрів розробки по всій країні, у них – більше 1000 розробників.

З PostgreSQL ми працюємо з 2008 року і накопичили великий обсяг того, що ми обробляємо – це клієнтські дані, статистичні, аналітичні, дані із зовнішніх інформаційних систем. більше 400TB. Лише «в продакшені» близько 250 серверів, а сумарно БД-серверів, які ми моніторимо, — близько 1000.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

SQL – декларативна мова. Ви описуєте не "як" щось має працювати, а "що" ви хочете отримати. СУБД краще знає, як зробити JOIN — як з'єднати ваші таблички, які умови накласти, що піде за індексом, що ні…

Деякі СУБД підказують: «Ні, ось ці дві таблички з'єднуй у такій черзі», але PostgreSQL так не вміє. Це усвідомлена позиція провідних розробників: «Краще ми допилемо оптимізатор запиту, ніж дозволимо розробникам користуватися хінтами».

Але, незважаючи на те, що PostgreSQL не дає "зовні" керувати собою, він чудово дозволяє побачити, що відбувається у нього «всередині», коли ви виконуєте свій запит, і де виникають проблеми.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Взагалі, з якими класичними проблемами приходить розробник [DBA] зазвичай? «Ось ми тут виконали запит, і у нас все повільно, все повисло, щось відбувається ... Біда якась! »

Причини майже завжди одні й ті самі:

  • неефективний алгоритм запиту
    Розробник: «Зараз я в SQL йому 10 табличок через JOIN…» — і очікує, що його умови чудово «розв'яжуться», і він отримає все швидко. Але чудес не буває, і будь-яка система за такої варіативності (10 таблиць в одному FROM) завжди дає якусь похибку. [стаття]
  • неактуальна статистика
    Момент дуже актуальний саме для PostgreSQL, коли ви великий датасет "влили" на сервер, робите запит - а він у вас "сексканить" по табличці. Тому що вчора в ній лежало 10 записів, а сьогодні 10 мільйонів, але PostgreSQL про це ще не знає, і треба йому про це підказати. [стаття]
  • «затік» за ресурсами
    Ви поставили велику та важку навантажену базу поставили на слабкий сервер, у якого не вистачає диска, пам'яті, продуктивності самого процесора. І все… Десь є стеля продуктивності, вище за який ви стрибнути вже не можете.
  • блокування
    Складний момент, але вони найбільш актуальні для різних запитів, що модифікують (INSERT, UPDATE, DELETE) — це окрема велика тема.

Отримання плану

… А для решти нам потрібен план! Нам потрібно бачити, що відбувається усередині сервера.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

План виконання запиту для PostgreSQL це дерево алгоритму виконання запиту в текстовому поданні. Саме того алгоритму, який у результаті аналізу планувальником було визнано найефективнішим.

Кожен вузол дерева — операція: вилучення даних із таблиці чи індексу, побудова бітової карти, з'єднання двох таблиць, об'єднання, перетин або виняток вибірок. Виконання запиту – прохід вузлами цього дерева.

Щоб отримати план запиту, найпростіший спосіб – виконати оператор EXPLAIN. Щоб отримати з усіма реальними атрибутами, тобто насправді виконати запит на базі EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Поганий момент: коли ви його виконуєте, це відбувається тут і зараз, тому підходить тільки для локальної налагодження. Якщо ви берете якийсь високонавантажений сервер, який стоїть під сильним потоком змін даних, і бачите: «Ай! Ось тут у нас повільно виконувався запит.» Півгодини, годину тому — поки ви бігали і діставали цей запит із ліг, несли його знову на сервер, у вас весь датасет і статистика змінилися. Ви його виконуєте, щоб налагодити – а він виконується швидко! І ви не можете зрозуміти «чому», чому було повільно.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

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

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

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Начебто все тепер добре, йдемо в ліг і бачимо там… [Бурчанка тексту]. Але сказати нічого про нього не можемо, крім того факту, що це чудовий план, бо виконувався 11мс.

Начебто все гаразд — але нічого не зрозуміло, що насправді відбувалося. Крім загального часу, особливо нічого і не бачимо. Тому що дивитися на таку "латуху" plain text взагалі ненаглядно.

Але навіть нехай ненаглядно, нехай незручно, але є більш капітальні проблеми:

  • У вузлі вказується сума за ресурсами всього піддерева під ним. Тобто просто так дізнатися, скільки саме тут на цьому Index Scan було витрачено часу — не можна, якщо під ним є якась вкладена умова. Ми повинні динамічно дивитися, чи немає всередині «дітей» та умовних змінних, CTE — і віднімати це все «розумно».
  • Другий момент: час, який вказується на вузлі, - це час одноразового виконання вузла. Якщо цей вузол виконувався в результаті, наприклад, циклу за записами таблиці кілька разів, то в плані збільшується кількість loops - циклів цього вузла. Але час атомарного виконання залишається у плані колишнім. Тобто для того, щоб зрозуміти, а скільки ж цей вузол виконувався сумарно, треба одне множити на інше — знову-таки «в умі».

За таких розкладів зрозуміти «Хто найслабша ланка?» Майже неможливо. Тому навіть самі розробники у «мануалі» пишуть, що "Розуміння плану - це мистецтво, якому треба вчитися, досвід ...".

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

Візуалізація плану

Тому ми зрозуміли, щоб розбиратися з цими проблемами, нам потрібна гарна візуалізація плану. [стаття]

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Ми пішли спочатку «по ринку» — давайте в інтернеті пошукаємо, що взагалі існує.

Але виявилося, що щодо «живих» рішень, які більш-менш розвиваються, зовсім мало — буквально одне: objasni.depesz.com від Hubert Lubaczewski. На вхід у поле «годуєш» текстове представлення плану, він тобі показує табличку з розібраними даними:

  • власний час відпрацювання вузла
  • час сумарний по всьому піддереву
  • кількість записів, яка була витягнута і яка статистично очікувалася
  • саме тіло вузла

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

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Але є й невеликі проблеми.

По-перше, величезна кількість копіпасти. Ти береш шматок ліг, засовуєш туди, і знову, і знову.

по-друге, немає аналізу кількості прочитаних даних - тих самих buffers, які виводить EXPLAIN (ANALYZE, BUFFERS), Тут ми не бачимо. Він просто не вміє їх розбирати, розуміти та з ними працювати. Коли ви читаєте багато даних і розумієте, що можете неправильно розкластися по диску і кешу в пам'яті, ця інформація дуже важлива.

Третій негативний момент дуже слабкий розвиток цього проекту. Комміт дуже дрібні, добре якщо раз на півроку, і код на Perl'е.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Але це все «лірика», з цим можна було б якось жити, але є одна річ, яка нас дуже відвернула від цього сервісу. Це помилки аналізу Common Table Expression (CTE) та різних динамічних вузлів на кшталт InitPlan/SubPlan.

Якщо вірити цій картинці, то у нас сумарний час виконання кожного окремого вузла більший, ніж загальний час виконання всього запиту. Все просто - із вузла CTE Scan не відняли час генерації цієї CTE. Тому ми вже не знаємо правильної відповіді, скільки зайняло саме сканування CTE.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Тут ми зрозуміли, що час писати своє — ура-ура! Кожен розробник каже: "Зараз ми своє напишемо, супер просто буде!"

Взяли типовий для web-сервісів стек: ядро ​​на Node.js + Express, натягнули Bootstrap і для красивих діаграм - D3.js. І наші очікування цілком виправдалися – перший прототип ми отримали за 2 тижні:

  • власний парсер плану
    Тобто тепер ми можемо взагалі будь-який план розбирати з тих, що їх генерує PostgreSQL.
  • коректний аналіз динамічних вузлів - CTE Scan, InitPlan, SubPlan
  • аналіз розподілу buffers де сторінки даних з пам'яті читаються, де з локального кешу, де з диска
  • отримали наочність
    Щоб не в лозі все це «копати», а бачити «найслабшу ланку» відразу на картинці.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Ми отримали приблизно таку картинку — одразу з підсвічуванням синтаксису. Але зазвичай наші розробники працюють не з повним уявленням плану, і з тим, що коротше. Адже всі циферки ми вже розпарили і в бік їх ліворуч закинули, а посередині залишили тільки перший рядок, що це за вузол: CTE Scan, генерація CTE або Seq Scan по якійсь табличці.

Ось це уявлення скорочене ми називаємо шаблон плану.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Що ще було зручно? Було б зручно бачити, яка частка на який вузол від загального часу у нас розподіляється — і просто приклеїли збоку кругова діаграма.

Наводимо на вузол і бачимо - у нас, виявляється Seq Scan від усього часу зайняв менше чверті, а решту 3/4 у нас зайняв CTE Scan. Жах! Це маленьке зауваження з приводу «швидкості» CTE Scan, якщо ви їх активно використовуєте у своїх запитах. Вони не дуже швидкі – вони програють навіть звичайному табличному скануванню. [стаття] [стаття]

Але зазвичай такі діаграми бувають цікавішими, складнішими, коли ми відразу наводимо на сегмент, і бачимо, наприклад, що більше половини всього часу якийсь Seq Scan «з'їв». Та ще всередині там якийсь Filter був, купа записів відкинута по ньому… Можна ось цю картинку прямо кидати розробнику і говорити: «Вася, у тебе тут все погано! Розберися, подивися - щось не так!

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Звичайно, без «граблів» не обійшлося.

Перше, на що «наступили», — це проблема округлення. Час вузла кожного окремого у плані вказується з точністю до 1мкс. І коли кількість циклів вузла перевищує, наприклад, 1000 – після виконання PostgreSQL поділив «з точністю до», то при зворотному розрахунку ми отримуємо загальний час «десь між 0.95мс та 1.05мс». Коли рахунок йде на мікросекунди — ще нічого, а от коли вже на [мілі]секунди — доводиться при «розв'язуванні» ресурсів по вузлах плану «хто в кого скільки споживав» цю інформацію враховувати.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Другий момент, більш складний, це розподіл ресурсів (тих самих buffers) динамічними вузлами. Це коштувало нам до перших 2 тижнів на прототип ще плюсом тижня 4.

Проблему таку отримати досить просто — робимо CTE і щось щось нібито читаємо. Насправді PostgreSQL «розумний» і нічого прямо там читати не буде. Потім ми з неї беремо перший запис, а до неї — сто перший з тієї самої CTE.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Дивимося план і розуміємо - дивно, у нас 3 buffers (сторінки даних) були "спожиті" у Seq Scan, ще 1 у CTE Scan, і ще 2 у другому CTE Scan. Тобто якщо все просто підсумувати, у нас вийде 6, але з таблички ми прочитали всього 3! Адже CTE Scan нічого нізвідки не читає, а працює прямо з пам'яттю процесу. Тобто, тут явно щось не так!

Насправді виходить, що тут усі ті 3 сторінки даних, які були запитані у Seq Scan, спочатку 1 попросив 1-й CTE Scan, а потім 2-ї, і йому дочитали ще 2. Тобто всього було прочитано 3 сторінки даних, а чи не 6.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

І ця картинка привела нас до розуміння, що виконання плану це вже не дерево, а просто якийсь ациклічний граф. І в нас вийшла ось така приблизно діаграма, щоб ми розуміли «що звідки взагалі прийшло». Тобто тут ми створили CTE з pg_class, і двічі її попросили, і практично весь час у нас пішло по гілці, коли ми просили її 2й раз. Зрозуміло, що прочитати 101 запис — це набагато дорожче, ніж просто 1 з таблички.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Ми на якийсь час видихнули. Сказали: Тепер, Нео, ти знаєш кунг-фу! Тепер наш досвід маєш прямо на екрані. Тепер ти можеш ним користуватися. [стаття]

Консолідація логів

Наші 1000 розробників полегшено зітхнули. Але ми розуміли, що у нас тільки «бойових» серверів сотні, і весь цей «копіпаст» з боку розробників зовсім не зручний. Ми зрозуміли, що це треба самим збирати.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Взагалі, є штатний модуль, який вміє збирати статистику, щоправда, його так само потрібно в конфізі активувати — це модуль pg_stat_statements. Але він нас не влаштував.

По-перше, одним і тим же запитам за різними схемами в рамках однієї бази він надає різні QueryId. Тобто якщо спочатку зробити SET search_path = '01'; SELECT * FROM user LIMIT 1;, а потім SET search_path = '02'; і такий самий запит, то в статистиці цього модуля будуть різні записи, і я не зможу зібрати загальну статистику саме в розрізі цього профілю запиту без урахування схем.

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

І останній момент - відсутність «фактів». Тобто не можна адресуватися конкретному екземпляру виконання запиту — його немає, є лише агрегована статистика. Із цим хоч і можна працювати, просто дуже складно.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Тому ми вирішили з копіпастою боротися і почали писати. колектор.

Колектор підключається SSH, «натягує» за допомогою сертифіката захищене з'єднання до сервера з базою і tail -F "чіпляється" до нього на лог-файл. Таким чином, у цій сесії ми отримуємо повне «дзеркало» всього лог-файлу, що генерує сервер. Навантаження на сам сервер при цьому мінімальне, адже ми там нічого не паримо, просто дзеркаємо трафік.

Оскільки ми вже почали писати інтерфейс на Node.js, то на ньому і колектор продовжили писати. І ця технологія себе виправдала, тому що для роботи зі слабоформатованими текстовими даними, якими і є балка, використовувати JavaScript дуже зручно. А сама інфраструктура Node.js як backend-платформа дозволяє легко та зручно працювати з мережевими з'єднаннями, та й взагалі з якимись потоками даних.

Відповідно ми «натягуємо» два з'єднання: перше, щоб «слухати» сам лог і його до себе забирати, а друге — щоб періодично біля бази запитувати. «А ось у лозі прилетіло, що заблоковано табличку з oid 123», але розробнику це не говорить ні про що, і непогано б запитати у бази «А що ж таке OID = 123?» І так ми періодично питаємо базу те, що в себе ще не знаємо.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

«Лише одного ти не врахував, є вид слоноподібних бджіл!» Ми починали розробляти цю систему, коли хотіли відмоніторити 10 серверів. Найбільш критичних у нашому розумінні, на яких виникали якісь проблеми, із якими було складно розбиратися. Але протягом першого ж кварталу ми отримали на моніторинг сотню, бо система «зайшла», всі захотіли, всім зручно.

Все це треба складати, даних потік великий, активний. Власне, що моніторимо, із чим вміємо розбиратися — те й використовуємо. Використовуємо як сховища даних також PostgreSQL. А нічого швидше, щоб «лити» в нього дані, ніж оператор COPY поки немає.

Але просто «лити» дані – не зовсім наша технологія. Тому що якщо у вас на сотні серверів відбувається приблизно 50k запитів за секунду, то це вам генерує 100-150GB логів на день. Тому нам довелося основу акуратно «пиляти».

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

По-друге, ми навчилися (змушені були) дуже-дуже швидко писати за допомогою COPY. Тобто не просто COPY, тому що він швидше, ніж INSERT, А ще швидше.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Третій момент - довелося відмовитися від тригерів, відповідно, і від Foreign Keys. Тобто ми не маємо абсолютно посилальної цілісності. Тому що якщо у вас є таблиця, на якій є пара FK, і ви говорите в структурі БД, що «ось запис з лога посилається на FK, наприклад, на групу записів», то коли ви її вставляєте, PostgreSQL нічого не залишається, крім як взяти і чесно виконати SELECT 1 FROM master_fk1_table WHERE ... з тим ідентифікатором, який ви намагаєтеся вставити — просто для того, щоб перевірити, що цей запис там присутній, що ви не обламуєте своєю вставкою цей Foreign Key.

Ми отримуємо замість одного запису до цільової таблиці та її індексів, ще плюсом читання з усіх таблиць, на які вона посилається. А нам це зовсім не треба — наше завдання записати якнайбільше і якнайшвидше з найменшим навантаженням. Так що FK - геть!

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

А тепер уявіть, що у вас є табличка, в якій ви просто вважаєте кількість запитів, що пройшли по конкретному хосту: +1, +1, +1, ..., +1. А вам це, в принципі, не потрібне — це все можна підсумувати у пам'яті на колекторі та відправити в базу за один раз +10.

Так, у вас у разі якихось неполадок може «розвалитися» логічна цілісність, але це практично нереальний кейс — тому що у вас нормальний сервер, на ньому батарейка в контролері, журнал транзакцій, журнал на файловій системі… Загалом, не варте воно того. Не варто втрата продуктивності, яку ви отримуєте за рахунок роботи тригерів/FK, тих витрат, які ви несете при цьому.

Те саме і з хешуванням. Летить до вас якийсь запит, ви від нього обчислюєте в БД ідентифікатор, пишете в базу і всім потім кажете його. Все добре, поки в момент запису до вас не прийде другий бажаючий записати його — і у вас виникне блокування, а це вже погано. Тому якщо ви можете генерацію якихось ID винести на клієнта (щодо бази), то краще це зробити.

Нам просто ідеально підійшло використовувати MD5 від тексту — запиту, плану, шаблону… Ми обчислюємо його на стороні колектора, і «ллємо» в базу вже готовий ID. Довжина MD5 та добове секціонування дозволяють нам не турбуватися про можливі колізії.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Але щоб це все записати швидко нам знадобилося модифікувати саму процедуру запису.

Як зазвичай пишуть дані? У нас є якийсь датасет, ми його розкладаємо на кілька таблиць, а потім COPY — спочатку до першої, потім до другої, третьої… Незручно, тому що ми наче один потік даних пишемо за три кроки послідовно. Неприємно. Чи можна зробити швидше? Можна, можливо!

Для цього достатньо лише розкласти ці потоки паралельно один з одним. Виходить, що в нас летять в окремих потоках помилки, запити, шаблони, блокування, — і ми пишемо все це паралельно. Для цього достатньо тримати постійно відкритим COPY-канал на окрему цільову таблицю.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

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

Додатково ми з'ясували, що для такого профілю навантаження будь-яка агрегація, коли збираються записи в пакети — це зло. Класичне зло - це INSERT ... VALUES та далі 1000 записів. Тому що в цей момент у вас виникає пік запису по носію, і всі інші, хто намагається щось записати на диск, чекатимуть.

Щоб позбавитися таких аномалій, просто не агрегуйте нічого, не буферизуйте взагалі. І якщо буферизація на диск все ж таки виникає (на щастя, Stream API в Node.js дозволяє це дізнатися) - відкладіть це з'єднання. Ось коли вам прийде подія, що вона знову вільна — пишіть у нього з черги, що накопичилася. А поки воно зайняте — беріть із пула наступне, вільне, і пишіть у нього.

До впровадження такого підходу до запису даних ми мали приблизно 4K write ops, а в такий спосіб скоротили навантаження в 4 рази. Зараз виросли ще в 6 разів за рахунок нових баз, що спостерігаються, — до 100MB/s. І тепер ми зберігаємо логи за останні 3 місяці в обсязі близько 10-15TB, сподіваючись, що вже за три місяці будь-яку проблему будь-який розробник здатний вирішити.

Розуміємо проблеми

Але просто зібрати всі ці дані — добре, корисно, доречно, але мало їх треба зрозуміти. Тому що це мільйони різних планів на добу.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Але мільйони — це некеровано, треба спочатку зробити «менше». І, в першу чергу, треба вирішити, як це «менше» ви організовуватимете.

Ми виділили для себе три ключові моменти:

  • хто цей запит надіслав
    Тобто з якого додатку він «прилетів»: web-інтерфейс, backend, платіжна система чи ще щось.
  • де це відбулося
    На якому конкретному сервері? Тому що якщо у вас під одним додатком стоїть кілька серверів, і раптово один "затупив" (бо "диск згнив", "пам'ять протікла", ще якесь лихо), то треба конкретно адресуватися до сервера.
  • як саме виявлялася проблема у тому чи іншому плані

Щоб зрозуміти «хто» надіслав нам запит, ми користуємося штатним засобом – встановленням змінної сесії: SET application_name = '{bl-host}:{bl-method}'; — надсилаємо ім'я хоста бізнес-логіки, з якого йде запит, та ім'я методу або програми, яка його ініціювала.

Після того, як ми передали «хазяїна» запиту, його треба вивести в балку — для цього конфігуруємо змінну log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Кому цікаво, може подивитися у мануаліщо це все означає. Виходить, що ми в лозі бачимо:

  • час
  • ідентифікатори процесу та транзакції
  • ім'я бази
  • IP того, хто надіслав цей запит
  • та ім'я методу

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Далі ми зрозуміли, що не дуже цікаво дивитися кореляцію на один запит між різними серверами. Нечасто виходить ситуація, коли у вас одна програма однаково «лажить» і тут, і там. Але навіть якщо однаково – подивіться на будь-який із цих серверів.

Так ось, розрізу "один сервер - один день" нам виявилося достатньо для будь-якого аналізу.

Перший аналітичний розріз - це той самий «шаблон» - Скорочена форма подання плану, очищена від усіх чисельних показників. Другий розріз – додаток чи метод, а третій – це конкретний вузол плану, який викликав у нас проблеми.

Коли ми перейшли від конкретних екземплярів до шаблонів, отримали одразу дві переваги:

  • кратне зменшення кількості об'єктів для аналізу
    Доводиться розбирати проблему вже не за тисячами запитів чи планів, а за десятками шаблонів.
  • таймлайн
    Тобто, узагальнивши «факти» у межах якогось розрізу, можна відобразити їхню появу протягом дня. І тут ви можете зрозуміти, що якщо у вас якийсь шаблон відбувається, наприклад, раз на годину, а мав би — раз на добу, варто замислитись, що пішло не так — ким і навіщо він викликаний, може його і бути тут. не повинно. Це ще один нечисловий, суто візуальний спосіб аналізу.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

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

Тому що ви, наприклад, приходьте на сторінку аналітики по хосту, дивіться — щось занадто багато читати по диску початок. Диск на сервері не справляється – а хто з нього читає?

І ви можете відсортувати за будь-яким стовпцем і вирішити, з чим ви будете прямо зараз розбиратися — із навантаженням на процесор або на диск, або із загальною кількістю запитів… Відсортували, подивилися «топові», полагодили — викотили нову версію програми.
[відеолекція]

І відразу ви можете побачити різні додатки, які ходять з тим самим шаблоном від запиту типу SELECT * FROM users WHERE login = 'Vasya'. Фронтенд, бекенд, процесинг ... І ви замислюєтеся, навіщо б процесинг читати користувача, якщо він з ним не взаємодіє.

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

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Через якийсь час ми зрозуміли, що нам не вистачає агрегованої статистики у розрізі вузлів плану. Ми вичленували з планів лише ті вузли, які щось роблять із даними самих таблиць (читають/пишуть їх за індексом чи ні). По суті, щодо попередньої картинки додається лише один аспект. скільки записів цей вузол нам приніс, А скільки відкинув (Rows Removed by Filter).

У вас немає відповідного індексу на табличці, ви робите до неї запит, він пролітає повз індекс, падає в Seq Scan ... всі записи, крім одного ви відфільтрували. А навіщо вам за добу 100M відфільтрованих записів, чи не краще індекс накотити?

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Розібравши всі плани по вузлам, ми зрозуміли, що є деякі типові структури в планах, які дуже ймовірно виглядають підозріло. І непогано б розробнику підказати: «Друг, отут ти спочатку читаєш за індексом, потім сортуєш, а потім відрізаєш» — як правило, там один запис.

Всі хто писав запити, з таким патерном, напевно, стикалися: «Дай мені останнє замовлення по Васі, його дату» І якщо у вас індексу за датою немає, або в індексі, що використовувався, немає дати, то ось рівно на такі «граблі» і наступите .

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

В результаті обсяг того досвіду, який був необхідний для вирішення проблем на початку і зараз, впав у рази. Ось такий інструмент у нас вийшов.

Масова оптимізація запитів PostgreSQL Кирило Боровиков (Тензор)

Джерело: habr.com

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