Масавая аптымізацыя запытаў 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. Кірыл Баравікоў (Тэнзар)

Мы пайшлі спачатку «па рынку» — давайце ў інтэрнэце пашукаем, што ўвогуле існуе.

Але, аказалася, што адносна «жывых» рашэнняў, якія больш-менш развіваюцца, зусім мала - літаральна, адно: explain.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

Дадаць каментар