У дакладзе прадстаўлены некаторыя падыходы, якія дазваляюць сачыць за прадукцыйнасцю SQL-запытаў, калі іх мільёны ў суткі, а кантраляваных сервераў PostgreSQL – сотні.
Якія тэхнічныя рашэнні дазваляюць нам эфектыўна апрацоўваць такі аб'ём інфармацыі, і як гэта аблягчае жыццё звычайнага распрацоўніка.
Каму цікавы разбор канкрэтных праблем і розныя тэхнікі аптымізацый SQL-запытаў і рашэнні тыпавых DBA-задач у PostgreSQL - можна таксама азнаёміцца з серыяй артыкулаў на гэтую тэму.
Мяне клічуць Кірыл Баравікоў, я ўяўляю кампанію «Тэнзар». Канкрэтна я спецыялізуюся на працы з базамі дадзеных у нашай кампаніі.
Сёння я вам раскажу, як мы займаемся аптымізацыяй запытаў, калі вам трэба не «раскалупаць» прадукцыйнасць нейкага аднаго запыту, а вырашыць праблему масава. Калі запытаў мільёны, і вам трэба знайсці нейкія падыходы да рашэння гэтай вялікай праблемы.
Наогул, «Тэнзар» для мільёна нашых кліентаў - гэта НВІС - наша дадатак: карпаратыўная сацыяльная сетка, рашэнні для відэасувязі, для дакументаабароту ўнутранага і знешняга, уліковыя сістэмы для бухгалтэрыі і склада,… Гэта значыць такі "мегакамбайн" для комплекснага кіравання бізнесам, у якім больш за 100 розных унутраных праектаў.
Каб усе яны нармальна працавалі і развіваліся - у нас 10 цэнтраў распрацоўкі па ўсёй краіне, у іх - больш 1000 распрацоўшчыкаў.
З PostgreSQL мы працуем з 2008 года і назапасілі вялікі аб'ём таго, што мы апрацоўваем - гэта кліенцкія дадзеныя, статыстычныя, аналітычныя, дадзеныя з знешніх інфармацыйных сістэм - больш за 400TB. Толькі "ў прадакшэне" каля 250 сервераў, а сумарна БД-сервераў, якія мы маніторым – каля 1000.
SQL - дэкларатыўны мову. Вы апісваеце не "як" нешта павінна працаваць, а "што" вы хочаце атрымаць. СКБД лепш ведае як зрабіць JOIN - як злучыць вашыя таблічкі, якія ўмовы накласці, што пойдзе па індэксе, што не ...
Некаторыя СКБД прымаюць падказкі: "Не, вось гэтыя дзве таблічкі злучай у вось такой чарзе", але PostgreSQL так не ўмее. Гэта ўсвядомленая пазіцыя вядучых распрацоўшчыкаў: "Лепш мы дапілуем аптымізатар запыту, чым дазволім распрацоўшчыкам карыстацца нейкімі хінтамі".
Але, нягледзячы на тое, што PostgreSQL не дае "звонку" кіраваць сабой, ён выдатна дазваляе убачыць, што адбываецца ў яго «ўнутры», калі вы выконваеце свой запыт, і дзе ўзнікаюць у яго праблемы.
Наогул, з якімі класічнымі праблемамі прыходзіць распрацоўшчык [да DBA] звычайна? «Вось мы тут выканалі запыт, і у нас усё павольна, усё павісла, нешта адбываецца… Бяда нейкая!»
Чыннікі амаль заўсёды адны і тыя ж:
неэфектыўны алгарытм запыту
Распрацоўнік: «Цяпер я ў SQL яму 10 таблічак праз JOIN…» — і чакае, што яго ўмовы цудоўнай выявай эфектыўна «развяжуцца», і ён атрымае ўсё хутка. Але цудаў не бывае, і любая сістэма пры такой варыятыўнасці (10 табліц у адным FROM) заўсёды дае нейкую хібнасць. [артыкул]
неактуальная статыстыка
Момант вельмі актуальны менавіта для PostgreSQL, калі вы вялікі датасет "ўлілі" на сервер, робіце запыт - а ён у вас "сэксаніт" па таблічцы. Таму што ўчора ў ёй ляжала 10 запісаў, а сёньня 10 мільёнаў, але PostgreSQL пра гэта яшчэ не ў курсе, і трэба яму пра гэта падказаць. [артыкул]
«затык» па рэсурсах
Вы паставілі вялікую і цяжкую нагружаную базу паставілі на слабы сервер, у якога бракуе кружэлкі, памяці, прадукцыйнасці самога працэсара. І ўсё… Дзесьці ёсць столь прадукцыйнасці, вышэй якога вы скокнуць ужо не можаце.
блакавання
Складаны момант, але яны найбольш актуальныя для розных мадыфікуюць запытаў (INSERT, UPDATE, DELETE) - гэта асобная вялікая тэма.
Атрыманне плана
… А для ўсяго астатняга нам патрэбен план! Нам трэба бачыць, што адбываецца ўнутры сервера.
План выканання запыту для PostgreSQL - гэта дрэва алгарытму выканання запыту ў тэкставым уяўленні. Менавіта таго алгарытму, які ў выніку аналізу планавальнікам быў прызнаны найболей эфектыўным.
Кожны вузел дрэва - аперацыя: выманне дадзеных з табліцы ці азначніка, пабудова бітавай карты, злучэнне двух табліц, аб'яднанне, скрыжаванне або выключэнне выбарак. Выкананне запыту - праход па вузлах гэтага дрэва.
Каб атрымаць план запыту, самы просты спосаб - выканаць аператар EXPLAIN. Каб атрымаць з усімі рэальнымі атрыбутамі, гэта значыць насамрэч выканаць запыт на базе — EXPLAIN (ANALYZE, BUFFERS) SELECT ....
Дрэнны момант: калі вы яго выконваеце, гэта адбываецца "тут і цяпер", таму падыходзіць толькі для лакальнай адладкі. Калі ж вы бераце нейкі высоканагружаны сервер, які стаіць пад моцным патокам змен даных, і бачыце: «Ай! Вось тут у нас павольна выконваўся запыт.» Паўгадзіны, гадзіну таму - пакуль вы бегалі і даставалі гэты запыт з логаў, неслі яго зноў на сервер, у вас увесь датасет і статыстыка змяніліся. Вы яго выконваеце, каб адладзіць - а ён выконваецца хутка! І вы не можаце зразумець «чаму», чаму было павольна.
Для таго, каб зразумець, што было роўна ў той момант, калі запыт выконваецца на сэрвэры, разумныя людзі напісалі. модуль auto_explain. Ён прысутнічае практычна ва ўсіх найболей распаўсюджаных дыстрыбутывах PostgreSQL, і яго можна проста актываваць у канфіг-файле.
Калі ён разумее, што нейкі запыт выконваецца даўжэй за тую мяжу, якую вы яму сказалі, ён робіць "здымак" плана гэтага запыту і піша іх разам у лог.
Накшталт усё зараз добра, ідзем у лог і бачым там… [партанка тэксту]. Але сказаць нічога пра яго не можам, акрамя таго факта, што гэта выдатны план, таму што выконваўся 11мс.
Накшталт усё добра - але нічога не зразумела, што насамрэч адбывалася. Акрамя агульнага часу асабліва нічога і не бачым. Таму што глядзець на такую «латуху» plain text увогуле ненаглядна.
Але нават няхай ненаглядна, няхай няёмка, але ёсць больш капітальныя праблемы:
У вузле паказваецца сума па рэсурсах усяго поддерева пад ім. Гэта значыць проста так даведацца, колькі вось тут канкрэтна на гэтым Index Scan было выдаткавана часу – нельга, калі пад ім ёсць якое-небудзь укладзенае ўмова. Мы павінны дынамічна глядзець, ці няма ўсярэдзіне "дзяцей" і ўмоўных зменных, CTE - і адымаць гэта ўсё "у розуме".
Другі момант: час, які паказваецца на вузле, - гэта час аднаразовага выканання вузла. Калі гэты вузел выконваўся ў выніку, напрыклад, цыклу па запісах табліцы, некалькі разоў, то ў плане павялічваецца колькасць loops - цыклаў гэтага вузла. Але сам час атамарнага выканання застаецца ў плане ранейшым. Гэта значыць для таго, каб зразумець, а колькі ж гэты вузел выконваўся ўсяго сумарна, трэба адно памнажаць на іншае - ізноў-ткі "у розуме".
Пры такіх раскладах зразумець "Хто самае слабое звяно?" практычна нерэальна. Таму нават самі распрацоўшчыкі ў "мануале" пішуць, што «Разуменне плана – гэта мастацтва, якому трэба вучыцца, досвед…».
Але ў нас 1000 распрацоўшчыкаў, і кожнаму з іх гэты вопыт не перадасі ў галаву. Я, ты, ён - ведаюць, а нехта вунь там - ужо няма. Можа, ён навучыцца, а можа і не, але працаваць яму трэба ўжо зараз - а адкуль бы яму ўзяць гэты вопыт.
Візуалізацыя плана
Таму мы зразумелі - каб разбірацца з гэтымі праблемамі, нам патрэбна добрая візуалізацыя плана. [артыкул]
Мы пайшлі спачатку «па рынку» — давайце ў інтэрнэце пашукаем, што ўвогуле існуе.
Але, аказалася, што адносна «жывых» рашэнняў, якія больш-менш развіваюцца, зусім мала - літаральна, адно: explain.depesz.com ад Hubert Lubaczewski. На ўваход у поле "скормліваеш" тэкставае прадстаўленне плана, ён табе паказвае таблічку з разабранымі дадзенымі:
уласны час адпрацоўкі вузла
час сумарны па ўсім поддереву
колькасць запісаў, якая была вынятая і якая статыстычна чакалася
само цела вузла
Таксама гэты сэрвіс мае магчымасць дзяліцца архівам спасылак. Ты кінуў туды свой план і кажаш: "Гэй, Вася, вось табе спасылка, там нешта не так".
Але ёсць і невялікія праблемы.
Па-першае, велізарная колькасць "капіпасты". Ты бярэш кавалак лога, засоўваеш туды, і зноў, і зноў.
Па-другое, няма аналізу колькасці прачытаных дадзеных - тых самых buffers, якія выводзіць EXPLAIN (ANALYZE, BUFFERS), тут мы не бачым. Ён проста не ўмее іх разбіраць, разумець і з імі працаваць. Калі вы чытаеце шмат дадзеных і разумееце, што можаце няправільна "раскласціся" па дыску і кэшу ў памяці, гэтая інфармацыя вельмі важная.
Трэці адмоўны момант - вельмі слабое развіццё гэтага праекта. Каміты вельмі дробныя, добра калі раз у паўгода, і код на Perl'е.
Але гэта ўсё "лірыка", з гэтым можна было б неяк жыць, але ёсць адна рэч, якая нас моцна адвярнула ад гэтага сэрвісу. Гэта памылкі аналізу Common Table Expression (CTE) і розных дынамічных вузлоў накшталт InitPlan/SubPlan.
Калі верыць гэтай карцінцы, то ў нас сумарны час выканання кожнага асобнага вузла больш, чым агульны час выканання ўсяго запыту. Усё проста - з вузла CTE Scan не аднялі час генерацыі гэтай CTE. Таму мы ўжо не ведаем правільнага адказу, колькі ж заняло само сканіраванне CTE.
Тут мы зразумелі, што час пісаць сваё — ура-ўра! Кожны распрацоўшчык кажа: «Цяпер мы сваё напішам, супер проста будзе!»
Узялі тыповы для web-сэрвісаў стэк: ядро на Node.js + Express, нацягнулі Bootstrap і для дыяграмак прыгожых – D3.js. І нашы чаканні цалкам апраўдаліся - першы прататып мы атрымалі за 2 тыдні:
уласны парсер плана
Гэта значыць, зараз мы можам наогул любы план разбіраць з тых, якія генеруе PostgreSQL.
карэктны аналіз дынамічных вузлоў - CTE Scan, InitPlan, SubPlan
аналіз размеркавання buffers - дзе старонкі дадзеных з памяці чытаюцца, дзе з лакальнага кэша, дзе з дыска
атрымалі нагляднасць
Каб не ў логу ўсё вось гэтае вось «капаць», а бачыць «самае слабое звяно» адразу на малюнку.
Мы атрымалі прыкладна такі малюнак - адразу з падсветкай сінтаксісу. Але звычайна нашы распрацоўшчыкі працуюць ужо не з поўным прадстаўленнем плана, а з тым, што карацей. Бо ўсе цыферкі мы ўжо распарсілі і ў бок іх налева-направа закінулі, а пасярэдзіне пакінулі толькі першы радок, што гэта за вузел: CTE Scan, генерацыя CTE або Seq Scan па нейкай таблічцы.
Вось гэтае ўяўленне скарочанае мы завём шаблонам плана.
Што яшчэ было б зручна? Было б зручна бачыць, якая доля на які вузел ад агульнага часу ў нас размяркоўваецца - і проста "прыляпілі" збоку кругавая дыяграма.
Наводзім на вузел і бачым – у нас, аказваецца Seq Scan ад усяго часу заняў менш за чвэрць, а астатнія 3/4 у нас заняў CTE Scan. Жах! Гэтая маленькая заўвага наконт «хуткастрэльнасці» CTE Scan, калі вы іх актыўна карыстаецеся ў сваіх запытах. Яны не вельмі хуткія - яны прайграюць нават звычайнаму таблічнаму сканаванні. [артыкул][артыкул]
Але звычайна такія дыяграмы бываюць больш цікавыя, больш складаныя, калі мы адразу наводзім на сегмент, і бачым, напрыклад, што больш за палову ўсяго часу нейкі Seq Scan «з'еў». Ды яшчэ ўсярэдзіне там нейкі Filter быў, куча запісаў адкінута па ім… Можна вось гэты малюначак прама кідаць распрацоўніку і казаць: «Вася, у цябе тут наогул усё дрэнна! Разбярыся, паглядзі - нешта не так!
Натуральна, без "граблей" не абышлося.
Першае на што «наступілі» - гэта праблема акруглення. Час вузла кожнага асобнага ў плане паказваецца з дакладнасцю да 1мкс. І калі колькасць цыклаў вузла перавышае, напрыклад, 1000 – пасля выканання PostgreSQL падзяліў "з дакладнасцю да", то пры зваротным разліку мы атрымліваем агульны час "дзесьці паміж 0.95мс і 1.05мс". Калі рахунак ідзе на мікрасекунды – яшчэ нічога, а вось калі ўжо на [мілі] секунды – даводзіцца пры «развязванні» рэсурсаў па вузлах плана «хто ў каго колькі спажыў» гэтую інфармацыю ўлічваць.
Другі момант, больш складаны, гэтае размеркаванне рэсурсаў (тых самых buffers) па дынамічных вузлах. Гэта каштавала нам да першых 2 тыдняў на прататып яшчэ плюсам тыдня 4.
Праблему такую атрымаць дастаткова проста – робім CTE і ў ёй нешта нібыта чытаем. На самай справе, PostgreSQL "разумны" і нічога прама там чытаць не будзе. Потым мы з яе бярэм першы запіс, а да яе – сто першы з той жа самай CTE.
Глядзім план і разумеем – дзіўна, у нас 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.
І гэтая карцінка прывяла нас да разумення, што выкананне плана — гэта ўжо не дрэва, а проста нейкі ацыклічны граф. І ў нас атрымалася вось такая прыкладна дыяграма, каб мы разумелі "што-адкуль наогул прыйшло". Гэта значыць вось тут мы стварылі CTE з pg_class, і два разы яе папрасілі, і практычна ўвесь час у нас сышло па галінцы, калі мы прасілі яе 2й раз. Зразумела, што прачытаць 101-ю запіс - гэта нашмат даражэй, чым проста 1-ю з таблічкі.
Мы на нейкі час выдыхнулі. Сказалі: «Цяпер, Нэо, ты ведаеш кунг-фу! Цяпер наш вопыт у цябе прама на экране. Цяпер ты можаш ім карыстацца.» [артыкул]
Кансалідацыя логаў
Нашы 1000 распрацоўшчыкаў з палёгкай уздыхнулі. Але мы-то разумелі, што ў нас толькі "баявых" сервераў сотні, і ўвесь гэты "капіпаст" са боку распрацоўнікаў зусім не зручны. Мы зразумелі, што трэба гэта самім збіраць.
Наогул, ёсць штатны модуль, які ўмее збіраць статыстыку, праўда, яго гэтак жа трэба ў канфігу актываваць - гэта модуль pg_stat_statements. Але ён нас не задаволіў.
Па-першае, адным і тым жа запытам па розных схемах у рамках адной базы ён прысвойвае розныя QueryId. Гэта значыць, калі спачатку зрабіць SET search_path = '01'; SELECT * FROM user LIMIT 1;, а потым SET search_path = '02'; і такі ж запыт, то ў статыстыцы гэтага модуля будуць розныя запісы, і я не змагу сабраць агульную статыстыку менавіта ў разрэзе гэтага профіля запыту, без уліку схем.
Другі момант, які нам перашкодзіў яго выкарыстоўваць - адсутнасць планаў. Гэта значыць плана - не, ёсць толькі сам запыт. Мы бачым што тармазіла, але не разумеем, чаму. І тут мы вяртаемся да праблемы хутказмяняльнага датасета.
І апошні момант - адсутнасць «фактаў». Гэта значыць, нельга адрасавацца да канкрэтнага экзэмпляра выканання запыту — яго няма, ёсць толькі агрэгаваная статыстыка. З гэтым хаця і можна працаваць, проста вельмі складана.
Таму мы вырашылі з “капіпастай” змагацца і пачалі пісаць. калектар.
Калектар падключаецца па SSH, "нацягвае" з дапамогай сертыфіката абароненае злучэнне да сервера з базай і tail -F "чапляецца" да яго на лог-файл. Такім чынам, у гэтай сесіі мы атрымліваем поўнае "люстэрка" усяго лог-файла, Які генеруе сервер. Нагрузка на сам сервер пры гэтым мінімальная, бо мы там нічога не парсім, проста люстэркуем трафік.
Паколькі мы ўжо пачалі пісаць інтэрфейс на Node.js, то на ім і калектар працягнулі пісаць. І гэтая тэхналогія сябе апраўдала, таму што для працы са слабафарматаваным тэкставымі дадзенымі, якімі і з'яўляецца лог, выкарыстоўваць JavaScript вельмі зручна. А сама інфраструктура Node.js у якасці backend-платформы дазваляе лёгка і зручна працаваць з сеткавымі злучэннямі, ды і ўвогуле з нейкімі патокамі дадзеных.
Адпаведна, мы "нацягваем" два злучэнні: першае, каб "слухаць" сам лог і яго да сябе забіраць, а другое - каб перыядычна ў базы пытаць. "А вось у логу прыляцела, што заблакаваная таблічка з oid 123", але распрацоўніку гэта не кажа ні пра што, і нядрэнна б спытаць у базы "А што ж усёткі такое OID = 123?" І так мы перыядычна пытаемся ў базы тое, што ў сябе яшчэ не ведаем.
"Толькі аднаго ты не ўлічыў, ёсць выгляд сланападобных пчол!.." Мы пачыналі распрацоўваць гэтую сістэму, калі жадалі адманіторыць 10 сервераў. Найбольш крытычных у нашым разуменні, на якіх узнікалі нейкія праблемы, з якімі было складана разбірацца. Але на працягу першага ж квартала мы атрымалі на маніторынг сотню - таму што сістэма «зайшла», усе захацелі, усім зручна.
Усё гэта трэба складаць, дадзеных струмень вялікі, актыўны. Уласна, што маніторым, з чым умеем разбірацца - тое і выкарыстоўваем. Выкарыстоўваны ў якасці сховішчы дадзеных таксама PostgreSQL. А нічога хутчэй, каб "ліць" у яго дадзеныя, чым аператар COPY пакуль няма.
Але проста «ліць» дадзеныя - не зусім наша тэхналогія. Таму што калі ў вас на сотні сервераў адбываецца прыкладна 50k запытаў у секунду, тое гэта вам генеруе 100-150GB логаў у дзень. Таму нам прыйшлося базу акуратна "пілаваць".
Па-першае, мы зрабілі секцыянаванне па днях, таму што, па вялікім рахунку, нікога не цікавіць карэляцыя паміж суткамі. Якая розніца, што ў цябе было ўчора, калі сёння ўначы ты выкаціў новую версію прыкладання - і ўжо нейкая новая статыстыка.
Па-другое, мы навучыліся (вымушаныя былі) вельмі-вельмі хутка пісаць з дапамогай COPY. Гэта значыць не проста COPY, таму што ён хутчэй, чым INSERT, а яшчэ хутчэй.
Трэці момант - прыйшлося адмовіцца ад трыгераў, адпаведна, і ад 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 і посуточное секцыянаванне дазваляюць нам не турбавацца аб магчымых калізіях.
Але каб усё гэта запісаць хутка, нам спатрэбілася мадыфікаваць саму працэдуру запісу.
Як звычайна пішуць дадзеныя? У нас ёсць нейкі датасет, мы яго раскладваем на некалькі табліц, а потым COPY — спачатку ў першую, потым у другую, у трэцюю… Няёмка, таму што мы быццам адзін струмень дадзеных пішам за тры крокі паслядоўна. Непрыемна. Ці можна зрабіць хутчэй? Можна!
Для гэтага дастаткова ўсяго толькі раскласці гэтыя патокі паралельна адзін з адным. Атрымліваецца, што ў нас ляцяць у асобных плынях памылкі, запыты, шаблоны, блакаванні,… — і мы пішам гэта ўсё паралельна. Для гэтага дастаткова трымаць увесь час адкрытым COPY-канал на кожную асобную мэтавую табліцу.
Гэта значыць у калектара заўсёды ёсць стрым, у які я магу запісаць патрэбныя мне дадзеныя. Але каб база гэтыя дадзеныя ўбачыла, а хто-небудзь не вісеў у блакаванні, чакаючы, пакуль гэтыя дадзеныя запішуцца, COPY трэба перарываць з вызначанай перыядычнасцю. Для нас найбольш эфектыўным атрымаўся перыяд парадку 100мс - закрываем і адразу зноў адкрываем на тую ж табліцу. А калі ў нас аднаго патоку не хапае пры нейкіх піках, то мы робім пулінг да пэўнай мяжы.
Дадаткова мы высветлілі, што для такога профілю нагрузкі любая агрэгацыя, калі запісы збіраюцца ў пакеты - гэта зло. Класічнае зло - гэта INSERT ... VALUES і далей за 1000 запісаў. Таму што ў гэты момант у вас узнікае пік запісу па носьбіте, і ўсе астатнія, якія спрабуюць нешта запісаць на дыск, будуць чакаць.
Каб пазбавіцца ад такіх анамалій, проста не агрэгуйце нічога, не буферызуйце наогул. І калі буферызацыя на дыск усёткі ўзнікае (на шчасце, Stream API у Node.js дазваляе гэта пазнаць) - адкладзяце гэтае злучэнне. Вось калі вам прыйдзе падзея, што яна зноў свабодная - пішыце ў яго з назапашанай чаргі. А пакуль яно занятае - бярыце з пула наступнае, свабоднае, і пішыце ў яго.
Да ўкаранення такога падыходу да запісу дадзеных у нас было прыкладна 4K write ops, а такім спосабам скарацілі нагрузку ў 4 разы. Цяпер выраслі яшчэ ў 6 разоў за кошт новых назіраных баз – да 100MB/s. І зараз мы захоўваем логі за апошнія 3 месяцы ў аб'ёме каля 10-15TB, спадзяючыся, што ўжо за тры месяцы любую праблему любы распрацоўшчык здольны вырашыць.
Разумеем праблемы
Але проста сабраць усе гэтыя дадзеныя - добра, карысна, дарэчы, але мала - іх трэба зразумець. Бо гэта мільёны розных планаў за суткі.
Але мільёны - гэта некіравальна, трэба спачатку зрабіць «паменш». І, у першую чаргу, трэба вырашыць, як гэтае «паменш» вы будзеце арганізоўваць.
Мы вылучылі для сябе тры ключавыя моманты:
хто гэты запыт даслаў
Гэта значыць з якога прыкладання ён "прыляцеў": web-інтэрфейс, backend, плацежная сістэма ці нешта яшчэ.
дзе гэта адбылося
На якім канкрэтным сэрвэры. Таму што калі ў вас пад адным дадаткам стаіць некалькі сервераў, і раптам адзін "затупіў" (таму што "дыск згніў", "памяць працякла", яшчэ нейкая бяда), то трэба канкрэтна адрасавацца да сервера.
як менавіта выяўлялася праблема ў тым ці іншым плане
Каб зразумець "хто" даслаў нам запыт, мы карыстаемся штатным сродкам - усталёўкай зменнай сесіі: SET application_name = '{bl-host}:{bl-method}'; - пасылаем імя хаста бізнес-логікі, з якога ідзе запыт, і імя метаду або дадатку, якое яго ініцыявала.
Пасля таго, як мы перадалі "гаспадара" запыту, яго трэба вывесці ў лог - для гэтага канфігуруем зменную log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Каму цікава, можа паглядзець у мануале, што гэта ўсё значыць. Атрымліваецца, што мы ў логу бачым:
час
ідэнтыфікатары працэсу і транзакцыі
імя базы
IP таго, хто даслаў гэты запыт
і імя метаду
Далей мы зразумелі, што не вельмі цікава глядзець карэляцыю па адным запыце паміж рознымі серверамі. Нячаста атрымліваецца сітуацыя, калі ў вас адно прыкладанне аднолькава "лажае" і тут, і там. Але нават калі аднолькава - паглядзіце на любы з гэтых сервераў.
Дык вось, разрэзу "адзін сервер - адзін дзень" нам аказалася дастаткова для любога аналізу.
Першы аналітычны разрэз - гэта той самы «шаблон» - скарочаная форма прадстаўлення плана, вычышчаная ад усіх лікавых паказчыкаў. Другі разрэз - дадатак або метад, а трэці - гэта канкрэтны вузел плана, які выклікаў у нас праблемы.
Калі мы перайшлі ад канкрэтных асобнікаў да шаблонаў, атрымалі адразу дзве перавагі:
кратнае памяншэнне колькасці аб'ектаў для аналізу
Даводзіцца разбіраць праблему ўжо не па тысячах запытаў ці планаў, а па дзясятках шаблонаў.
таймлайн
Гэта значыць, абагульніўшы "факты" у рамках нейкага разрэзу, можна адлюстраваць іх з'яўленне на працягу дня. І тут вы можаце зразумець, што калі ў вас нейкі шаблон адбываецца, напрыклад, раз у гадзіну, а павінен бы - раз у суткі, варта задумацца, што пайшло не так - кім і навошта ён выкліканы, можа, яго і быць тут. не павінна. Гэта яшчэ адзін нелікавы, чыста візуальны, спосаб аналізу.
Астатнія спосабы грунтуюцца на тых паказчыках, якія мы здабываем з плана: колькі разоў адбываўся такі шаблон, сумарны і сярэдні час, колькі дадзеных адыманае з дыска, а колькі з памяці…
Таму што вы, напрыклад, прыходзьце на старонку аналітыкі па хасце, глядзіце - нешта занадта шмат па дыску чытаць пачатак. Дыск на серверы не спраўляецца - а хто з яго чытае?
І вы можаце адсартаваць па любым слупку і вырашыць, з чым вы будзеце прама цяпер разбірацца - з нагрузкай на працэсар або на дыск, або з агульнай колькасцю запытаў ... Адсартавалі, паглядзелі «топавыя», адрамантавалі - выкацілі новую версію прыкладання. [відэалекцыя]
І адразу вы можаце ўбачыць розныя прыкладанні, якія ходзяць з адным і тым жа шаблонам ад запыту тыпу SELECT * FROM users WHERE login = 'Vasya'. Франтэнд, бэкэнд, працэсінг… І вы задумваецеся, навошта б працэсінгу чытаць карыстальніка, калі ён з ім не ўзаемадзейнічае.
Зваротны спосаб - ад прыкладання адразу ўбачыць, што яно робіць. Напрыклад, фронтэнд - гэта, гэта, вось гэта, а яшчэ вось гэта раз у гадзіну (якраз таймлайн дапамагае). І адразу ўзнікае пытанне - быццам бы не справа фронтэнда рабіць нешта раз у гадзіну…
Праз нейкі час мы зразумелі, што нам не хапае агрэгаванай статыстыкі ў разрэзе вузлоў плана. Мы вылучылі з планаў толькі тыя вузлы, якія нешта робяць з дадзенымі саміх табліц (чытаюць/пішуць іх па індэксе ці не). Па сутнасці, адносна папярэдняй карцінкі дадаецца ўсяго адзін аспект. колькі запісаў гэты вузел нам прынёс, а колькі адкінуў (Rows Removed by Filter).
У вас няма прыдатнага азначніка на таблічцы, вы робіце да яе запыт, ён пралятае міма азначніка, падае ў Seq Scan… усе запісы, акрамя адной вы адфільтравалі. А навошта вам за суткі 100M адфільтраваных запісаў, ці не лепш індэкс накаціць?
Разабраўшы ўсе планы па вузлах, мы зразумелі, што ёсць некаторыя тыпавыя структуры ў планах, якія з вельмі вялікай імавернасцю выглядаюць падазрона. І нядрэнна б распрацоўніку падказаць: "Сябар, вось тут ты спачатку чытаеш па індэксе, потым сартуеш, а потым адразаеш" - як правіла, там адзін запіс.
Усе хто пісаў запыты, з такім патэрнам, напэўна, сутыкаліся: «Дай мне апошняя замова па Васі, яго дату» І калі ў вас азначніка па даце няма, або ў які выкарыстоўваўся азначніку няма даты, то вось роўна на такія «граблі» і наступіце .
Але мы ж ведаем, што гэта "граблі" — дык чаму б адразу не падказаць распрацоўшчыку, што яму варта зрабіць. Адпаведна, адкрываючы зараз план, наш распрацоўшчык адразу бачыць прыгожую карцінку з падказкамі, дзе яму адразу кажуць: "У цябе праблемы тут і тут, а вырашаюцца яны так і так."
У выніку, аб'ём таго вопыту, які быў неабходны для вырашэння праблем у пачатку і зараз, упаў у разы. Вось такі інструмент у нас атрымаўся.