Postgres: bloat, pg_repack і deferred constraints

Postgres: bloat, pg_repack і deferred constraints

Эфект раздзіманні табліц і азначнікаў (bloat) шырока вядомы і прысутнічае не толькі ў Postgres. Ёсць спосабы барацьбы з ім "са скрынкі" накшталт VACUUM FULL або CLUSTER, але яны блакуюць табліцы падчас працы і таму не заўсёды могуць быць скарыстаны.

У артыкуле будзе крыху тэорыі аб тым, як узнікае bloat, як з ім можна змагацца, аб deferred constraints і аб праблемах, якія яны прыўносяць у выкарыстанне пашырэння pg_repack.

Гэты артыкул напісаны на аснове майго выступу на PgConf.Russia 2020.

Чаму ўзнікае bloat

У аснове Postgres ляжыць шматверсійная мадэль (MVCC). Яе сутнасць у тым, што кожны радок у табліцы можа мець некалькі версій, пры гэтым транзакцыі бачаць не больш за адну з гэтых версій, але неабавязкова адну і тую ж. Гэта дазваляе працаваць некалькім транзакцыям адначасова і практычна не ўплываць адзін на аднаго.

Відавочна, што ўсе гэтыя версіі трэба захоўваць. Postgres працуе з памяццю пастаронкава і старонка - гэта мінімальны аб'ём дадзеных, які можна лічыць з дыска або запісаць. Давайце разгледзім невялікі прыклад, каб зразумець, як гэта адбываецца.

Дапусцім, у нас ёсць табліца, у якую мы дадалі некалькі запісаў. У першай старонцы файла, дзе захоўваецца табліца, з'явіліся новыя дадзеныя. Гэта жывыя версіі радкоў, якія даступныя іншым транзакцыям пасля коміта (для прастаты будзем меркаваць, што ўзровень ізаляцыі Read Committed).

Postgres: bloat, pg_repack і deferred constraints

Затым мы абнавілі адзін з запісаў і тым самым пазначылі старую версію як неактуальную.

Postgres: bloat, pg_repack і deferred constraints

Крок за крокам, абнаўляючы і выдаляючы версіі радкоў, мы атрымалі старонку, у якой прыкладна палова дадзеных - гэта "смецце". Гэтыя дадзеныя не бачныя ніводнай транзакцыі.

Postgres: bloat, pg_repack і deferred constraints

У Postgres існуе механізм Вакуумныя, Які вычышчае неактуальныя версіі і вызваляе месца для новых дадзеных. Але калі ён настроены не дастаткова агрэсіўна або заняты працай у іншых табліцах, то "смеццевыя дадзеныя" застаюцца, і нам даводзіцца выкарыстоўваць дадатковыя старонкі для новых дадзеных.

Так, у нашым прыкладзе ў нейкі момант часу табліца будзе складацца з чатырох старонак, але жывых дадзеных у ёй будзе толькі палова. У выніку, пры звароце да табліцы мы будзем вычытваць значна больш дадзеных, чым неабходна.

Postgres: bloat, pg_repack і deferred constraints

Нават калі зараз VACUUM выдаліць усе неактуальныя версіі радкоў, сітуацыя не палепшыцца кардынальна. У нас з'явіцца свабоднае месца ў старонках ці нават цэлыя старонкі для новых радкоў, але мы па-ранейшаму будзем вычытваць больш дадзеных, чым трэба.
Дарэчы, калі б цалкам пустая старонка (другая ў нашым прыкладзе) апынулася ў канцы файла, то VACUUM змог бы яе абразаць. Але зараз яна знаходзіцца пасярэдзіне, таму зрабіць з ёй нічога не атрымаецца.

Postgres: bloat, pg_repack і deferred constraints

Калі колькасць такіх пустых ці моцна разраджаных старонак становіцца вялікім, што і завецца bloat, гэта пачынае адбівацца на прадукцыйнасці.

Усё апісанае вышэй - механіка ўзнікнення bloat у табліцах. У азначніках гэта адбываецца прыкладна гэтак жа.

А ці ёсць у мяне bloat?

Ёсць некалькі спосабаў, каб вызначыць, ці ёсць у вас bloat. Ідэя першага - выкарыстанне ўнутранай статыстыкі Postgres, у якой змяшчаецца прыблізная інфармацыя аб колькасці радкоў у табліцах, колькасці "жывых" радкоў і інш. У інтэрнэце можна знайсці мноства варыяцый ужо гатовых скрыптоў. Мы за аснову ўзялі скрыпт ад PostgreSQL Experts, які можа ацаніць bloat табліц разам з toast і bloat btree-індэксаў. Па нашым досведзе яго хібнасць складае 10-20%.

Іншы спосаб - выкарыстоўваць пашырэнне pgstattuple, якое дазваляць зазірнуць унутр старонак і атрымаць як ацэначнае, так і дакладнае значэнне bloat. Але ў другім выпадку давядзецца прасканаваць усю табліцу.

Невялікае значэнне bloat, да 20%, мы лічым прымальным. Яго можна лічыць як аналаг fillfactor для табліц и індэксаў. Пры 50 працэнтах і вышэй могуць пачацца праблемы з прадукцыйнасцю.

Спосабы барацьбы з bloat

У Postgres ёсць некалькі спосабаў барацьбы з bloat "са скрынкі", аднак яны далёка не заўсёды і не ўсім могуць падысці.

Наладзіць AUTOVACUUM, каб bloat не ўзнікаў. А калі дакладней, каб ён трымаўся на прымальным для вас узроўні. Здаецца, што гэта "капітанская" рада, але ў рэальнасці гэтага не заўсёды лёгка дасягнуць. Напрыклад, у вас ідзе актыўная распрацоўка з рэгулярнай зменай схемы дадзеных ці адбываецца нейкая міграцыя дадзеных. Як следства, ваш профіль нагрузкі можа часта мяняцца і, як правіла, ён бывае розным для розных табліц. Значыць, вам трэба ўвесь час працаваць трохі на апярэджанне і падладжваць AUTOVACUUM пад які змяняецца профіль кожнай табліцы. Але відавочна, што зрабіць гэта няпроста.

Іншым распаўсюджаным чыннікам таго, што AUTOVACUUM не паспявае апрацоўваць табліцы, з'яўляецца наяўнасць працяглых транзакцый, якія не дазваляюць яму вычышчаць дадзеныя з-за таго, што яны даступныя гэтым транзакцыям. Рэкамендацыя тут таксама відавочная - пазбавіцца ад "вісячых" транзакцый і мінімізаваць час актыўных транзакцый. Але калі нагрузка на ваша прыкладанне - гэта гібрыд OLAP і OLTP, то ў вас адначасова можа быць як мноства частых абнаўленняў і кароткіх запытаў, так і працяглыя аперацыі - напрыклад, пабудова якой-небудзь справаздачы. У такой сітуацыі варта задумацца аб разнясенні нагрузкі на розныя базы, што дазволіць правесці больш тонкую настройку кожнай з іх.

Яшчэ адзін прыклад нават калі профіль аднастайны, але БД знаходзіцца пад вельмі высокай нагрузкай, то нават максімальна агрэсіўны AUTOVACUUM можа не спраўляцца, і bloat будзе ўзнікаць. Маштабаванне (вертыкальнае або гарызантальнае) - адзінае рашэнне.

Як жа быць у сітуацыі, калі AUTOVACUUM вы наладзілі, але bloat працягвае расці.

Каманда VACUUM FULL перабудоўвае змесціва табліц і азначнікаў і пакідае ў іх толькі актуальныя дадзеныя. Для ўхілення bloat яна працуе ідэальна, але падчас яе выканання захопліваецца эксклюзіўнае блакаванне на табліцу (AccessExclusiveLock), якая не дазволяць выконваць запыты да гэтай табліцы, нават select-ы. Калі вы можаце дазволіць сабе прыпынак вашага сэрвісу ці яго часткі на нейкі час (ад дзясяткаў хвілін да некалькіх гадзін у залежнасці ад памеру БД і вашага жалеза), то гэты варыянт - лепшы. Мы, нажаль, не паспяваем прагнаць VACUUM FULL за час запланаванага maintenance, таму такі спосаб нам не падыходзіць.

Каманда КЛАСТАР гэтак жа перабудоўвае змесціва табліц, як і VACUUM FULL, пры гэтым дазваляе паказаць індэкс, паводле якога дадзеныя будуць фізічна спарадкаваны на дыску (але ў будучыні для новых радкоў парадак не гарантуецца). У пэўных сітуацыях гэта нядрэнная аптымізацыя для шэрагу запытаў - з чытаннем некалькіх запісаў па індэксе. Недахоп у каманды той жа, што ў VACUUM FULL - яна блакуе табліцу падчас працы.

Каманда REINDEX падобная на дзве папярэднія, але выконвае перастраенне канкрэтнага індэкса ці ўсіх індэксаў табліцы. Блакіроўкі ледзь слабейшыя: ShareLock на табліцу (перашкаджае мадыфікацыям, але дазваляе выконваць select) і AccessExclusiveLock на перабудоўваны індэкс (блакуе запыты з выкарыстаннем гэтага індэкса). Аднак у 12-й версіі Postgres з'явіўся параметр CONCURRENTLY, які дазваляе перабудоўваць індэкс, не блакуючы паралельнае даданне, змяненне або выдаленне запісаў.

У больш ранніх версіях Postgres можна дабіцца выніку, падобнага з REINDEX CONCURRENTLY, з дапамогай CREATE INDEX CONCURRENTLY. Ён дазваляе стварыць азначнік без строгай блакавання (ShareUpdateExclusiveLock, які не мяшае раўналежным запытам), затым падмяніць стары азначнік на новы і выдаліць стары азначнік. Гэта дазваляе ліквідаваць bloat індэксаў, не замінаючы працы вашага прыкладання. Важна ўлічыць, што пры перастраенні азначнікаў будзе дадатковая нагрузка на дыскавую падсістэму.

Такім чынам, калі для азначнікаў ёсць спосабы для ўхілення bloat "на гарачую", то для табліц іх няма. Тут у справу ўступаюць розныя вонкавыя пашырэнні: pg_repack (раней pg_reorg), pgcompact, pgcompacttable і іншыя. У рамках гэтага артыкула я не буду іх параўноўваць і раскажу толькі пра pg_repack, якое пасля некаторай дапрацоўкі мы выкарыстоўваем у сябе.

Як працуе pg_repack

Postgres: bloat, pg_repack і deferred constraints
Дапушчальны, у нас ёсць цалкам сабе звычайная табліца з індэксамі, абмежаваннямі і, нажаль, з bloat. Першым крокам pg_repack стварае лог-табліцу, каб захоўваць дадзеныя аб усіх зменах падчас працы. Трыгер будзе рэплікаваць гэтыя змены на кожны insert, update і delete. Затым ствараецца табліца, аналагічная зыходнай па структуры, але без індэксаў і абмежаванняў, каб не запавольваць працэс устаўкі даных.

Далей pg_repack пераносіць у новую табліцу дадзеныя са старой, аўтаматычна фільтруючы ўсе неактуальныя радкі, а затым стварае індэксы для новай табліцы. За час выканання ўсіх гэтых аперацый у лог-табліцы назапашваюцца змены.

Наступны крок - перанесці змены ў новую табліцу. Перанос выконваецца ў некалькі ітэрацый, і калі ў лог-табліцы застаецца меней 20 запісаў, pg_repack захоплівае строгае блакаванне, пераносіць апошнія дадзеныя і падмяняе старую табліцу на новую ў сістэмных табліцах Postgres. Гэта адзіны і вельмі кароткі момант часу, калі вы ня зможаце працаваць з табліцай. Пасля гэтага старая табліца і табліца з логамі выдаляюцца і ў файлавай сістэме вызваляецца месца. Працэс завершаны.

У тэорыі ўсё выглядае выдатна, што ж на практыцы? Мы пратэставалі pg_repack без нагрузкі і пад нагрузкай, праверылі яго працу ў выпадку дачаснага прыпынку (прасцей кажучы, па Ctrl+C). Усе тэсты былі станоўчымі.

Мы пайшлі на прод - і тут усё пайшло не так, як мы чакалі.

Першы блін на продзе

На першым жа кластары мы атрымалі памылку аб парушэнні ўнікальнага абмежавання:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Гэтае абмежаванне мела аўтазгенераванае найменне index_16508 – яго стварыў pg_repack. Па атрыбутах, якія ўваходзяць у яго склад, мы вызначылі "наша" абмежаванне, якое яму адпавядае. Праблема аказалася ў тым, што гэта не зусім звычайнае абмежаванне, а адкладзенае (deferred constraint), г.зн. яго праверка выконваецца пазней, чым sql-каманда, што прыводзіць да нечаканых наступстваў.

Deferred constraints: навошта патрэбны і як працуюць

Трохі тэорыі аб deferred-абмежаваннях.
Разгледзім просты прыклад: у нас ёсць табліца-даведнік аўтамабіляў з двума атрыбутамі найменнем і парадкам аўтамабіля ў даведніку.
Postgres: bloat, pg_repack і deferred constraints

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique
);



Дапусцім, нам спатрэбілася памяняць першы і другі аўтамабілі месцамі. Рашэнне "ў лоб" - абнавіць першае значэнне на другое, а другое на першае:

begin;
  update cars set ord = 2 where name = 'audi';
  update cars set ord = 1 where name = 'bmw';
commit;

Але пры выкананні гэтага кода мы чакана атрымаем парушэнне абмежавання, таму што парадак значэнняў у табліцы ўнікальны:

[23305] ERROR: duplicate key value violates unique constraint “uk_cars”
Detail: Key (ord)=(2) already exists.

Як зрабіць па-іншаму? Варыянт першы: дадаць дадатковую замену значэння на парадак, якога гарантавана не існуе ў табліцы, напрыклад "-1". У праграмаванні гэта называецца "абменам значэнняў двух зменных праз трэцюю". Адзіны недахоп гэтага метаду - дадатковы update.

Варыянт другой: перапраектаваць табліцу, каб выкарыстоўваць для значэння парадку тып дадзеных з якая плавае кропкай замест цэлых лікаў. Тады пры абнаўленні значэння з 1, напрыклад, на 2.5 першы запіс аўтаматычна "ўстане" паміж другой і трэцяй. Гэтае рашэнне працуе, але ёсць два абмежаванні. Па-першае, яно не падыдзе вам, калі значэнне выкарыстоўваецца недзе ў інтэрфейсе. Па-другое, у залежнасці ад дакладнасці тыпу дадзеных вы будзеце мець абмежаваную колькасць магчымых уставак да правядзення пераразліку значэнняў усіх запісаў.

Варыянт трэці: зрабіць абмежаванне адкладзеным, каб яно правяралася толькі на момант коміта:

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique deferrable initially deferred
);

Паколькі логіка нашага першапачатковага запыту гарантуе, што да моманту коміта ўсе значэнні ўнікальныя, то ён выканаецца паспяхова.

Разгледжаны вышэй прыклад, вядома, вельмі сінтэтычны, але ідэю раскрывае. У нашым дадатку мы выкарыстоўваем адкладзеныя абмежаванні для рэалізацыі логікі, якая адказвае за дазволы канфліктаў пры адначасовай працы карыстальнікаў з агульнымі аб'ектамі-віджэтамі на дошцы. Выкарыстанне такіх абмежаванняў дазваляе нам зрабіць прыкладны код крыху прасцей.

У цэлым, у залежнасці ад тыпу абмежавання ў Postgres існуе тры ўзроўня гранулярнасці іх праверкі: узровень радка, транзакцыі і выразы.
Postgres: bloat, pg_repack і deferred constraints
Крыніца: begriffs

CHECK і NOT NULL заўсёды правяраюцца на ўзроўні радка, для астатніх абмежаванняў, як відаць з табліцы, ёсць розныя варыянты. Падрабязней можна пачытаць тут.

Калі коратка падсумаваць, то адкладзеныя абмежаванні ў шэрагу сітуацый даюць больш зразумелы для чытання код і меншую колькасць каманд. Аднак за гэта даводзіцца плаціць ускладненнем працэсу дэбагу, паколькі момант узнікнення памылкі і момант, калі вы пра яе даведаецеся, разнесены ў часе. Яшчэ адна магчымая праблема звязана з тым, што планавальнік не заўсёды можа пабудаваць аптымальны план, калі ў запыце ўдзельнічае адкладзенае абмежаванне.

Дапрацоўка pg_repack

Мы разабраліся з тым, што такое адкладзеныя абмежаванні, але як яны злучаны з нашай праблемай? Успомнім памылку, якую мы раней атрымалі:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Яна ўзнікае ў момант капіявання дадзеных з лог-табліцы ў новую табліцу. Гэта выглядае дзіўна, т.я. дадзеныя ў лог-табліцы коміціцца разам з дадзенымі зыходнай табліцы. Калі яны задавальняюць абмежаванні зыходнай табліцы, то як яны могуць парушаць тыя ж самыя абмежаванні ў новай?

Як аказалася, корань праблемы тоіцца ў папярэднім кроку працы pg_repack, на якім ствараюцца толькі індэксы, але не абмежаванні: у старой табліцы было ўнікальнае абмежаванне, а ў новай замест яго стварыўся ўнікальны індэкс.

Postgres: bloat, pg_repack і deferred constraints

Тут важна адзначыць, што калі абмежаванне звычайнае, а не адкладзенае, то створаны замест яго ўнікальны азначнік раўнасільны гэтаму абмежаванню, т.к. унікальныя абмежаванні ў Postgres рэалізуюцца з дапамогай стварэння ўнікальнага індэкса. Але ў выпадку з адкладзеным абмежаваннем паводзіны не аднолькава, таму што азначнік не можа быць адкладзеным і заўсёды правяраецца ў момант выканання sql-каманды.

Такім чынам, сутнасць праблемы заключаецца ў "адкладзенасці" праверкі: у зыходнай табліцы яна адбываецца ў момант комміта, а ў новай - у момант выканання sql-каманды. Значыць, нам трэба зрабіць так, каб праверкі выконваліся аднолькава ў абодвух выпадках: альбо заўсёды адкладзена, альбо заўсёды адразу ж.

Такім чынам, якія ідэі ў нас былі.

Стварыць індэкс, аналагічны deferred

Першая ідэя - выконваць абедзве праверкі ў рэжыме immediate. Гэта можа спарадзіць некалькі false positive спрацоўванняў абмежавання, але калі іх будзе няшмат, то гэта не павінна адбіцца на працы карыстальнікаў, паколькі для іх такія канфлікты - нармальная сітуацыя. Яны адбываюцца, напрыклад, калі два карыстальніка пачынаюць адначасова рэдагаваць адзін і той жа віджэт, і кліент другога карыстальніка не паспявае атрымаць інфармацыю аб тым, што віджэт ужо заблакаваны на рэдагаванне першым карыстальнікам. У такой сітуацыі сервер адказвае другому карыстачу адмовай, а яго кліент адкочвае змены і блакуе віджэт. Крыху пазней, калі першы карыстач завершыць рэдагаванне, другі атрымае інфармацыю, што віджэт больш не заблакаваны, і зможа паўтарыць сваё дзеянне.

Postgres: bloat, pg_repack і deferred constraints

Каб праверкі былі заўсёды ў неадкладзеным рэжыме, мы стварылі новы індэкс, аналагічны арыгінальнаму адкладзенаму абмежаванню:

CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- run pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;

На тэставым асяроддзі мы атрымалі ўсяго некалькі чаканых памылак. Поспех! Зноў запусцілі pg_repack на продзе і атрымалі 5 памылак на першым кластары за гадзіну працы. Гэта прымальны вынік. Аднак ужо на другім кластары колькасць памылак павялічылася ў разы і нам прыйшлося спыніць pg_repack.

Чаму так здарылася? Верагоднасць узнікнення памылкі залежыць ад таго, як шмат карыстальнікаў працуюць адначасова з аднымі і тымі ж віджэтамі. Мяркуючы па ўсім, у той момант з дадзенымі, якія захоўваюцца на першым кластары, было значна менш канкурэнтных змен, чым на астатніх, г.зн. нам проста "пашанцавала".

Ідэя не спрацавала. У той момант мы бачылі два іншыя варыянты рашэння: перапісаць наш прыкладны код, каб адмовіцца ад адкладзеных абмежаванняў, або “навучыць” pg_repack працаваць з імі. Мы выбралі другі.

Замяніць індэксы ў новай табліцы на адкладзеныя абмежаванні з зыходнай табліцы

Мэта дапрацоўкі была відавочная - калі зыходная табліца мае адкладзенае абмежаванне, то для новай трэба ствараць такое абмежаванне, а не індэкс.

Каб правяраць нашы змены, мы напісалі просты тэст:

  • табліца з адкладзеным абмежаваннем і адным запісам;
  • устаўляемы ў цыкле дадзеныя, якія канфліктуюць з наяўным запісам;
  • які робіцца update – дадзеныя ўжо не канфліктуюць;
  • камітым змены.

create table test_table
(
  id serial,
  val int,
  constraint uk_test_table__val unique (val) deferrable initially deferred 
);

INSERT INTO test_table (val) VALUES (0);
FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (0) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    COMMIT;
  END;
END LOOP;

Зыходная версія pg_repack заўсёды падала на першым insert, дапрацаваная версія працавала без памылак. Выдатна.

Ідзем на прод і зноў атрымліваем памылку на той жа фазе капіявання дадзеных з лог-табліцы ў новую:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Класічная сітуацыя: на тэставых асяродках усё працуе, а на продзе - не?!

APPLY_COUNT і стык двух батчоў

Мы пачалі аналізаваць код літаральна парадкова і выявілі важны момант: пераліўка дадзеных з лог-табліцы ў новую адбываецца батчамі, канстанта APPLY_COUNT паказвала на памер батча:

for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  /* there might be still some tuples, repeat. */
...
}

Праблема ў тым, што дадзеныя зыходнай транзакцыі, у якой некалькі аперацый могуць патэнцыйна парушыць абмежаванне, пры пераносе могуць патрапіць на стык двух батчоў - палова каманд будзе закамітчына ў першым батчы, а іншая палова - у другім. І тут як пашанцуе: калі каманды ў першым батчы нічога не парушаюць, то ўсё добра, а калі парушаюць - адбываецца памылка.

APPLY_COUNT роўны 1000 запісаў, што тлумачыць, чаму нашы тэсты праходзілі паспяхова - яны не пакрывалі выпадку "стыку батчоў". Мы выкарыстоўвалі дзве каманды - insert і update, таму роўна 500 транзакцый па дзве каманды заўсёды змяшчаліся ў батч і мы не адчувалі праблем. Пасля дадання другога update наша праўка перастала працаваць:

FOR i IN 1..10000 LOOP
  BEGIN
    INSERT INTO test_table VALUES (1) RETURNING id INTO v_id;
    UPDATE test_table set val = i where id = v_id;
    UPDATE test_table set val = i where id = v_id; -- one more update
    COMMIT;
  END;
END LOOP;

Такім чынам, наступная задача - зрабіць так, каб дадзеныя з зыходнай табліцы, якія змяняліся ў адной транзакцыі, пападалі ў новую табліцу таксама ўсярэдзіне адной транзакцыі.

Адмова ад батчынгу

І ў нас зноў было два варыянты рашэння. Першы: давайце наогул адмовімся ад разбіцця на батчы і будзем рабіць перанос дадзеных адной транзакцыяй. У карысць гэтага рашэння была яго прастата - патрабаваныя змены кода мінімальныя (дарэчы, у больш старых версіях тады яшчэ pg_reorg працаваў менавіта так). Але ёсць праблема - мы ствараем працяглую транзакцыю, а гэта, як было раней сказана, - пагроза для ўзнікнення новага bloat.

Другое рашэнне - больш складанае, але, напэўна, больш правільнае: стварыць у лог-табліцы слупок з ідэнтыфікатарам той транзакцыі, якая дадала дадзеныя ў табліцу. Тады пры капіяванні дадзеных мы зможам групаваць іх па гэтым атрыбуце і гарантаваць, што злучаныя змены будуць пераносіцца сумесна. Батч будзе фармавацца з некалькіх транзакцый (ці адной вялікай) і яго памер будзе вар'іравацца ў залежнасці ад таго, як шмат дадзеных змянілі ў гэтых транзакцыях. Важна адзначыць, што паколькі дадзеныя розных транзакцый пападаюць у лог-табліцу ў выпадковым парадку, то ўжо не атрымаецца чытаць яе паслядоўна, як гэта было раней. seqscan пры кожным запыце з фільтраваннем па tx_id - гэта занадта дорага, патрэбен азначнік, але і ён запаволіць працу метаду з-за накладных выдаткаў на яго абнаўленне. Увогуле, як заўсёды трэба нечым ахвяраваць.

Такім чынам, мы вырашылі пачаць з першага варыянту, як прасцейшага. Для пачатку неабходна было зразумець, ці будзе працяглая транзакцыя рэальнай праблемай. Паколькі асноўны перанос дадзеных са старой табліцы ў новую адбываецца таксама ў адной працяглай транзакцыі, то пытанне трансфармавалася ў "наколькі мы павялічым гэтую транзакцыю?" Працягласць першай транзакцыі залежыць у асноўным ад памеру табліцы. Працягласць новай - ад таго як шмат змен назапасіцца ў табліцы за час пераліўкі дадзеных, г.зн. ад інтэнсіўнасці нагрузкі. Прагон pg_repack адбываўся падчас мінімальнай нагрузкі на сэрвіс, і аб'ём змен быў несупаставімы малы ў параўнанні з зыходным аб'ёмам табліцы. Мы вырашылі, што можам занядбаць часам новай транзакцыі (для параўнання асераднёна гэта 1ч і 2-3 хвіліны).

Эксперыменты былі станоўчыя. Запуск на продзе таксама. Для навочнасці – карцінка з памерам адной з баз пасля прагону:

Postgres: bloat, pg_repack і deferred constraints

Паколькі гэтае рашэнне нас цалкам задаволіла, то спрабаваць рэалізаваць другое мы не сталі, але разглядаем магчымасць яго абмеркавання з распрацоўшчыкамі пашырэння. Нашы бягучая дапрацоўка, на жаль, пакуль яшчэ не гатова да публікацыі, паколькі мы вырашылі праблему толькі з унікальнымі адкладзенымі абмежаваннямі, а для паўнавартаснага патча неабходна зрабіць падтрымку і іншых тыпаў. Спадзяемся, што ўдасца зрабіць гэта ў будучыні.

Магчыма, у вас узнікла пытанне, навошта мы наогул увязаліся ў гэтую гісторыю з дапрацоўкай pg_repack, а не сталі, напрыклад, выкарыстоўваць яго аналагі? У нейкі момант мы таксама думалі пра гэта, але станоўчы досвед яго выкарыстання раней, на табліцах без адкладзеных абмежаванняў, матываваў на тое, каб паспрабаваць разабрацца ў сутнасці праблемы і выправіць яе. Да таго ж для выкарыстання іншых рашэнняў гэтак жа патрабуецца час на правядзенне тэстаў, таму мы вырашылі, што перш паспрабуем выправіць праблему ў ім, і калі зразумеем, што не зможам зрабіць гэта за разумны час, то тады пачнем разглядаць аналагі.

Высновы

Што мы можам рэкамендаваць на аснове ўласнага досведу:

  1. Маніторце ваш bloat. На аснове дадзеных маніторынгу вы зможаце зразумець, наколькі добра настроены autovacuum.
  2. Наладжвайце AUTOVACUUM, каб трымаць bloat на дапушчальным узроўні.
  3. Калі ўсё ж bloat расце і вы не можаце яго перамагчы з дапамогай сродкаў "са скрынкі", не бойцеся выкарыстоўваць вонкавыя пашырэнні. Галоўнае - усё добра тэставаць.
  4. Не бойцеся дапрацоўваць вонкавыя рашэнні пад свае патрэбы - часам гэтым можа быць больш эфектыўна і нават прасцей, чым змена вашага ўласнага кода.

Крыніца: habr.com

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