Постгрес: надувавање, пг_репацк и одложена ограничења

Постгрес: надувавање, пг_репацк и одложена ограничења

Ефекат надимања на табеле и индексе је опште познат и присутан је не само у Постгресу. Постоје начини да се то реши из кутије, као што је ВАЦУУМ ФУЛЛ или ЦЛУСТЕР, али они закључавају табеле током рада и стога се не могу увек користити.

Чланак ће садржати мало теорије о томе како настаје надимање, како се можете борити против њега, о одложеним ограничењима и проблемима које они доносе у коришћењу екстензије пг_репацк.

Овај чланак је написан на основу мој говор на ПгЦонф.Русија 2020.

Зашто се јавља надимање?

Постгрес је заснован на моделу са више верзија (МВЦЦ). Његова суштина је да сваки ред у табели може имати неколико верзија, док трансакције не виде више од једне од ових верзија, али не нужно исту. Ово омогућава да неколико трансакција раде истовремено и да практично немају утицај једна на другу.

Очигледно, све ове верзије морају бити сачуване. Постгрес ради са меморијом страницу по страницу и страница је минимална количина података која се може прочитати са диска или написати. Погледајмо мали пример да бисмо разумели како се то дешава.

Рецимо да имамо табелу у коју смо додали неколико записа. Нови подаци су се појавили на првој страници датотеке у којој се табела налази. Ово су живе верзије редова који су доступни другим трансакцијама након урезивања (ради једноставности, претпоставићемо да је ниво изолације Реад Цоммиттед).

Постгрес: надувавање, пг_репацк и одложена ограничења

Затим смо ажурирали један од уноса, чиме смо стару верзију означили као више нерелевантну.

Постгрес: надувавање, пг_репацк и одложена ограничења

Корак по корак, ажурирањем и брисањем верзија редова, на крају смо добили страницу на којој је отприлике половина података „смеће“. Ови подаци нису видљиви ниједној трансакцији.

Постгрес: надувавање, пг_репацк и одложена ограничења

Постгрес има механизам ВАЦУУМ, који чисти застареле верзије и отвара простор за нове податке. Али ако није довољно агресивно конфигурисан или је заузет радом у другим табелама, онда „подаци о смећу“ остају, а ми морамо да користимо додатне странице за нове податке.

Дакле, у нашем примеру, у неком тренутку табела ће се састојати од четири странице, али само половина ће садржати податке уживо. Као резултат тога, када приступамо табели, прочитаћемо много више података него што је потребно.

Постгрес: надувавање, пг_репацк и одложена ограничења

Чак и ако ВАЦУУМ сада избрише све небитне верзије редова, ситуација се неће драматично побољшати. Имаћемо слободан простор на страницама или чак целе странице за нове редове, али ћемо и даље читати више података него што је потребно.
Узгред, ако би се на крају датотеке налазила потпуно празна страница (друга у нашем примеру), тада би ВАЦУУМ могао да је исече. Али сада је она у средини, тако да се са њом ништа не може.

Постгрес: надувавање, пг_репацк и одложена ограничења

Када број таквих празних или веома ретких страница постане велики, што се назива надувавањем, то почиње да утиче на перформансе.

Све горе описано је механика појаве надимања у табелама. У индексима се то дешава на исти начин.

Да ли имам надимање?

Постоји неколико начина да утврдите да ли имате надимање. Идеја првог је да се користи интерна Постгрес статистика, која садржи приближне информације о броју редова у табелама, броју „живих“ редова итд. На Интернету можете пронаћи многе варијације готових скрипти. Узели смо као основу скрипта од ПостгреСКЛ Екпертс, који могу да процене надувене табеле заједно са индексима тост и бтрее индекса. Према нашем искуству, његова грешка је 10-20%.

Други начин је коришћење екстензије пгстаттупле, што вам омогућава да погледате унутар страница и добијете и процењену и тачну вредност надувености. Али у другом случају, мораћете да скенирате целу табелу.

Сматрамо да је мала вредност надимања, до 20%, прихватљива. Може се сматрати аналогом фактора попуњавања за столови и индекси. На 50% и више, могу почети проблеми са перформансама.

Начини борбе против надимања

Постгрес има неколико начина да се избори са надимањем из кутије, али они нису увек погодни за све.

Конфигуришите АУТОВАКУУМ тако да не дође до надимања. Тачније, да то буде на нивоу који вам је прихватљив. Ово изгледа као „капетанов” савет, али у стварности то није увек лако постићи. На пример, имате активан развој са редовним променама шеме података или се одвија нека врста миграције података. Као резултат тога, ваш профил учитавања може се често мењати и обично ће се разликовати од табеле до табеле. То значи да морате стално да радите мало унапред и прилагодите АУТОВАКУУМ променљивом профилу сваког стола. Али очигледно ово није лако учинити.

Још један уобичајени разлог зашто АУТОВАЦУУМ не може да држи корак са табелама је тај што постоје дуготрајне трансакције које га спречавају да очисти податке који су доступни тим трансакцијама. Препорука је такође очигледна - ослободите се „висећих“ трансакција и минимизирајте време активних трансакција. Али ако је оптерећење ваше апликације хибрид ОЛАП-а и ОЛТП-а, онда можете истовремено имати много честих ажурирања и кратких упита, као и дугорочне операције - на пример, прављење извештаја. У таквој ситуацији, вреди размислити о распоређивању оптерећења на различите базе, што ће омогућити финије подешавање сваке од њих.

Други пример - чак и ако је профил хомоген, али је база података под веома великим оптерећењем, онда чак ни најагресивнији АУТОВАКУУМ можда неће да се носи, и доћи ће до надимања. Скалирање (вертикално или хоризонтално) је једино решење.

Шта учинити у ситуацији када сте подесили АУТОВАКУУМ, али надимање наставља да расте.

Тим ВАЦУУМ ФУЛЛ поново гради садржај табела и индекса и у њима оставља само релевантне податке. Да би се елиминисао надувавање, ради савршено, али током његовог извршавања се хвата ексклузивна брава на табели (АццессЕкцлусивеЛоцк), која неће дозволити извршавање упита на овој табели, чак ни бира. Ако можете да приуштите да зауставите своју услугу или њен део на неко време (од десетина минута до неколико сати у зависности од величине базе података и вашег хардвера), онда је ова опција најбоља. Нажалост, немамо времена да покренемо ВАЦУУМ ФУЛЛ током планираног одржавања, тако да овај метод није погодан за нас.

Тим КЛАСТЕР Поново гради садржај табела на исти начин као ВАЦУУМ ФУЛЛ, али вам омогућава да наведете индекс према коме ће подаци бити физички поређани на диску (али убудуће редослед није загарантован за нове редове). У одређеним ситуацијама, ово је добра оптимизација за велики број упита – са читањем више записа по индексу. Недостатак команде је исти као код ВАЦУУМ ФУЛЛ - закључава сто током рада.

Тим РЕИНДЕКС слично као претходна два, али поново гради одређени индекс или све индексе табеле. Закључавања су нешто слабија: СхареЛоцк на табели (спречава модификације, али дозвољава избор) и АццессЕкцлусивеЛоцк на индексу који се поново гради (блокира упите који користе овај индекс). Међутим, у 12. верзији Постгреса појавио се параметар У исто време, што вам омогућава да поново изградите индекс без блокирања истовременог додавања, модификације или брисања записа.

У ранијим верзијама Постгреса, можете постићи резултат сличан РЕИНДЕКС-у истовремено користећи КРЕИРАЈТЕ ИНДЕКС ИСПОРЕДНО. Омогућава вам да креирате индекс без строгог закључавања (СхареУпдатеЕкцлусивеЛоцк, који не омета паралелне упите), затим замените стари индекс новим и избришите стари индекс. Ово вам омогућава да елиминишете надувавање индекса без мешања у вашу апликацију. Важно је узети у обзир да ће приликом обнављања индекса доћи до додатног оптерећења на подсистему диска.

Дакле, ако за индексе постоје начини да се елиминише надимање „у ходу“, онда их нема за табеле. Овде се појављују различити екстерни додаци: пг_репацк (раније пг_реорг), пгцомпацт, пгцомпацттабле и други. У овом чланку нећу их упоређивати и говорићу само о пг_репацк-у, који, након неких модификација, и сами користимо.

Како пг_репацк функционише

Постгрес: надувавање, пг_репацк и одложена ограничења
Рецимо да имамо сасвим обичну табелу - са индексима, ограничењима и, нажалост, са надимањем. Први корак пг_репацк-а је креирање табеле евиденције за складиштење података о свим променама док је покренута. Окидач ће реплицирати ове промене за свако уметање, ажурирање и брисање. Затим се креира табела, по структури слична оригиналној, али без индекса и ограничења, како се не би успоравао процес убацивања података.

Затим, пг_репацк преноси податке из старе табеле у нову табелу, аутоматски филтрирајући све ирелевантне редове, а затим креира индексе за нову табелу. Током извршавања свих ових операција, промене се акумулирају у табели евиденције.

Следећи корак је пренос промена у нову табелу. Миграција се врши у неколико итерација, а када је остало мање од 20 уноса у табели евиденције, пг_репацк добија снажно закључавање, мигрира најновије податке и замењује стару табелу новом у табелама система Постгрес. Ово је једино и веома кратко време када нећете моћи да радите са столом. Након тога, стара табела и табела са евиденцијама се бришу и ослобађа се простор у систему датотека. Процес је завршен.

Све изгледа одлично у теорији, али шта се дешава у пракси? Тестирали смо пг_репацк без оптерећења и под оптерећењем и проверили његов рад у случају превременог заустављања (другим речима, помоћу Цтрл+Ц). Сви тестови су били позитивни.

Отишли ​​смо у продавницу хране - и онда све није ишло како смо очекивали.

Прва палачинка у продаји

На првом кластеру смо добили грешку о кршењу јединственог ограничења:

$ ./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.

Ово ограничење је имало аутоматски генерисано име индек_16508 - креирао га је пг_репацк. На основу атрибута укључених у њен састав, одредили смо „наше“ ограничење које му одговара. Испоставило се да је проблем у томе што ово није сасвим обично ограничење, већ одложено (одложено ограничење), тј. његова верификација се врши касније од скл команде, што доводи до неочекиваних последица.

Одложена ограничења: зашто су потребна и како функционишу

Мало теорије о одложеним ограничењима.
Размотримо једноставан пример: имамо табелу-референцу аутомобила са два атрибута - именом и редоследом аутомобила у именику.
Постгрес: надувавање, пг_репацк и одложена ограничења

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“. У програмирању се то назива „размена вредности две променљиве кроз трећу“. Једини недостатак ове методе је додатно ажурирање.

Друга опција: Редизајнирајте табелу тако да користите тип података са помичним зарезом за вредност поруџбине уместо целих бројева. Затим, када ажурирате вредност са 1, на пример, на 2.5, први унос ће аутоматски „стајати“ између другог и трећег. Ово решење функционише, али постоје два ограничења. Прво, неће радити за вас ако се вредност користи негде у интерфејсу. Друго, у зависности од прецизности типа података, имаћете ограничен број могућих уметања пре поновног израчунавања вредности свих записа.

Опција три: учините ограничење одложеним тако да се проверава само у време урезивања:

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

Пошто логика нашег почетног захтева осигурава да су све вредности јединствене у време урезивања, то ће успети.

Пример о коме се горе говори је, наравно, веома синтетички, али открива идеју. У нашој апликацији користимо одложена ограничења за имплементацију логике која је одговорна за решавање конфликата када корисници истовремено раде са дељеним објектима виџета на табли. Коришћење таквих ограничења нам омогућава да учинимо код апликације мало једноставнијим.

Генерално, у зависности од типа ограничења, Постгрес има три нивоа грануларности за њихову проверу: ниво реда, трансакције и ниво израза.
Постгрес: надувавање, пг_репацк и одложена ограничења
Извор: бегриффс

ЦХЕЦК и НОТ НУЛЛ су увек проверени на нивоу реда; за остала ограничења, као што се може видети из табеле, постоје различите опције. Можете прочитати више овде.

Да укратко резимирамо, одложена ограничења у бројним ситуацијама пружају читљивији код и мање команди. Међутим, за то морате да платите компликацијом процеса отклањања грешака, јер су тренутак када се грешка појави и тренутак када сазнате за њу временски раздвојени. Други могући проблем је тај што планер можда неће увек моћи да направи оптималан план ако захтев укључује одложено ограничење.

Побољшање пг_репацк-а

Покрили смо шта су одложена ограничења, али како се она односе на наш проблем? Подсетимо се грешке коју смо раније добили:

$ ./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.

Јавља се када се подаци копирају из табеле дневника у нову табелу. Ово изгледа чудно јер... подаци у табели евиденције се урезују заједно са подацима у изворној табели. Ако задовољавају ограничења оригиналне табеле, како могу да прекрше иста ограничења у новој?

Како се испоставило, корен проблема лежи у претходном кораку пг_репацк-а, који креира само индексе, али не и ограничења: стара табела је имала јединствено ограничење, а нова је уместо тога креирала јединствени индекс.

Постгрес: надувавање, пг_репацк и одложена ограничења

Овде је важно напоменути да ако је ограничење нормално и није одложено, онда је јединствени индекс креиран уместо њега еквивалентан овом ограничењу, јер Јединствена ограничења у Постгресу се имплементирају креирањем јединственог индекса. Али у случају одложеног ограничења, понашање није исто, јер се индекс не може одложити и увек се проверава у време извршавања скл команде.

Дакле, суштина проблема лежи у „кашњењу“ провере: у оригиналној табели се јавља у време урезивања, а у новој табели у време извршавања скл команда. То значи да морамо да се уверимо да се провере обављају на исти начин у оба случаја: или увек одложено или увек одмах.

Па какве смо идеје имали?

Направите индекс сличан одложеном

Прва идеја је да извршите обе провере у тренутном режиму. Ово може да генерише неколико лажно позитивних ограничења, али ако их је мало, то не би требало да утиче на рад корисника, јер су такви сукоби за њих нормална ситуација. Настају, на пример, када два корисника почну да уређују исти виџет у исто време, а клијент другог корисника нема времена да прими информацију да је виџет већ блокиран за уређивање од стране првог корисника. У таквој ситуацији, сервер одбија другог корисника, а његов клијент враћа промене и блокира виџет. Мало касније, када први корисник заврши уређивање, други ће добити информацију да виџет више није блокиран и да ће моћи да понови своју радњу.

Постгрес: надувавање, пг_репацк и одложена ограничења

Да бисмо осигурали да су провере увек у не-одложеном режиму, направили смо нови индекс сличан оригиналном одложеном ограничењу:

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

У тестном окружењу смо добили само неколико очекиваних грешака. Успех! Поново смо покренули пг_репацк у производњи и добили 5 грешака на првом кластеру за сат времена рада. Ово је прихватљив резултат. Међутим, већ на другом кластеру број грешака се значајно повећао и морали смо да зауставимо пг_репацк.

Зашто се то догодило? Вероватноћа да дође до грешке зависи од тога колико корисника ради са истим виџетима у исто време. По свему судећи, у том тренутку је било много мање конкурентских промена са подацима ускладиштеним на првом кластеру него на осталима, тј. само смо имали "срећу".

Идеја није успела. У том тренутку смо видели још два решења: да препишемо наш код апликације да бисмо се ослободили одложених ограничења или „научили“ пг_репацк да ради са њима. Изабрали смо другу.

Замените индексе у новој табели са одложеним ограничењима из оригиналне табеле

Сврха ревизије је била очигледна - ако оригинална табела има одложено ограничење, онда за нову треба да креирате такво ограничење, а не индекс.

Да бисмо тестирали наше промене, написали смо једноставан тест:

  • табела са одложеним ограничењем и једним записом;
  • убацити податке у петљу која је у сукобу са постојећим записом;
  • извршите ажурирање – подаци више нису у сукобу;
  • изврши промене.

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 -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.

Класична ситуација: све ради у тест окружењима, али не и у производњи?!

АППЛИ_ЦОУНТ и спој две групе

Почели смо да анализирамо код буквално ред по ред и открили смо важну тачку: подаци се преносе из табеле евиденције у нову у серијама, константа АППЛИ_ЦОУНТ је означавала величину групе:

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

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

Проблем је у томе што подаци из оригиналне трансакције, у којој би неколико операција потенцијално могло да наруши ограничење, када се пренесу, могу завршити на споју две групе – половина команди ће бити извршена у првој групи, а друга половина и секунди. И овде, у зависности од ваше среће: ако тимови не прекрше ништа у првој серији, онда је све у реду, али ако то ураде, долази до грешке.

АППЛИ_ЦОУНТ је једнако 1000 записа, што објашњава зашто су наши тестови били успешни – нису покрили случај „батцх споја“. Користили смо две команде – инсерт и упдате, тако да је тачно 500 трансакција две команде увек било стављено у пакету и нисмо имали никаквих проблема. Након додавања другог ажурирања, наша измена је престала да ради:

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;

Дакле, следећи задатак је да обезбедимо да подаци из оригиналне табеле, која је промењена у једној трансакцији, заврше у новој табели такође у оквиру једне трансакције.

Одбијање мешања

И опет смо имали два решења. Прво: хајде да потпуно напустимо партиционисање на групе и преносимо податке у једној трансакцији. Предност овог решења је била његова једноставност - потребне промене кода су биле минималне (узгред, у старијим верзијама пг_реорг је радио управо тако). Али постоји проблем - ми стварамо дуготрајну трансакцију, а то, као што је раније речено, представља претњу за појаву новог надимања.

Друго решење је сложеније, али вероватно исправније: направите колону у табели евиденције са идентификатором трансакције која је додала податке у табелу. Затим, када копирамо податке, можемо их груписати према овом атрибуту и ​​осигурати да се повезане промене пренесу заједно. Група ће бити формирана од неколико трансакција (или једне велике) и њена величина ће варирати у зависности од тога колико је података промењено у овим трансакцијама. Важно је напоменути да пошто подаци из различитих трансакција улазе у табелу евиденције насумичним редоследом, више неће бити могуће читати их секвенцијално, као што је то било раније. сексцан за сваки захтев са филтрирањем по тк_ид је прескуп, потребан је индекс, али ће такође успорити метод због превеликих трошкова његовог ажурирања. Генерално, као и увек, треба нешто жртвовати.

Дакле, одлучили смо да почнемо са првом опцијом, јер је једноставнија. Прво, било је неопходно разумети да ли би дуга трансакција била прави проблем. Пошто се главни пренос података из старе табеле у нову такође дешава у једној дугој трансакцији, питање се трансформисало у „за колико ћемо повећати ову трансакцију?“ Трајање прве трансакције зависи углавном од величине табеле. Трајање новог зависи од тога колико се промена акумулира у табели током преноса података, тј. на интензитет оптерећења. Покретање пг_репацк-а се догодило у време минималног оптерећења услуге, а обим промена је био непропорционално мали у поређењу са оригиналном величином табеле. Одлучили смо да можемо занемарити време нове трансакције (поређења ради, у просеку је 1 сат и 2-3 минута).

Експерименти су били позитивни. Покрени и производњу. Ради јасноће, ево слике са величином једне од база података након покретања:

Постгрес: надувавање, пг_репацк и одложена ограничења

Пошто смо били у потпуности задовољни овим решењем, нисмо покушавали да имплементирамо друго, али разматрамо могућност да о томе разговарамо са програмерима проширења. Наша тренутна ревизија, нажалост, још увек није спремна за објављивање, јер смо проблем решили само јединственим одложеним ограничењима, а за пуноправни закрпу потребно је обезбедити подршку за друге типове. Надамо се да ћемо то моћи да урадимо у будућности.

Можда имате питање зашто смо се уопште умешали у ову причу са модификацијом пг_репацк-а, а нисмо, на пример, користили његове аналоге? У неком тренутку смо и ми размишљали о овоме, али позитивно искуство коришћења раније, на столовима без одложених ограничења, мотивисало нас је да покушамо да разумемо суштину проблема и решимо га. Поред тога, коришћење других решења такође захтева време за спровођење тестова, па смо одлучили да прво покушамо да решимо проблем у њему, а ако бисмо схватили да то не можемо да урадимо у разумном року, онда бисмо почели да тражимо аналоге .

Налази

Шта можемо препоручити на основу сопственог искуства:

  1. Пратите своју надутост. На основу података праћења, можете разумети колико је добро конфигурисан аутовакуум.
  2. Подесите АУТОВАКУУМ да бисте одржали надимање на прихватљивом нивоу.
  3. Ако надимање и даље расте и не можете да га превазиђете помоћу алата који нису у кутији, немојте се плашити да користите екстерне екстензије. Главна ствар је да све добро тестирате.
  4. Немојте се плашити да модификујете спољна решења како би одговарала вашим потребама – понекад ово може бити ефикасније и чак лакше од промене сопственог кода.

Извор: ввв.хабр.цом

Додај коментар