
Թափանցիկ աղյուսակների և ցուցակների (bloat) երևույթը լայնահուն հայտնի է և առկա է ոչ միայն Postgres-ում: Կան ձախողման միջոցներ, ինչպիսիք են VACUUM FULL կամ CLUSTER, բայց դրանք արգելափակում են աղյուսակները աշխատանքային ընթացքում ու այդ պատճառով միշտ չեն կարող օգտագործվել:
Այս հոդվածում կլինի մի քիչ théorie այն մասին, թե ինչպես է առաջանում bloat-ը, ինչպես կարելի է պայքարել դրա դեմ, deferred constraints-ի մասին և խնդիրների, որոնք նրանք ուզում են բերել pg_repack ընդլայնման մեջ:
Այս հոդվածը կազմված է PgConf.Russia 2020-ում:

Ինչու է առաջանում bloat
Postgres-ում հիմնված է բազմավարկակային մոդելի (). Բնույթը նրան է, որ աղյուսակում գտնվող յուրաքանչյուր տողի կարող է ունենալ մի քանի տարբերակ, և այդ ժամանակը տեսնում է միայն դրանցից մեկ հոգեթվական, բայց ոչ անպայման նույնը: Դա թույլ է տալիս մի քանի գործառնությունների աշխատել միաժամանակ և գրեթե չազդել միմյանց վրա:
Իհարկե, որ այս բոլոր տարբերակները անհրաժեշտ է պահել: Postgres-ն աշխատում է հիշողությամբ էջերով և էջը՝ տվյալների այն նվազագույն ծավալն է, որը հնարավոր է կարդալ դիսկից կամ գրել: Եկեք դիտարկենք մի փոքր օրինակ, որպեսզի հասկանանք, թե ինչպես է դա տեղի ունենում:
Թող մեր մոտ լինի աղյուսակ, որտեղ ավելացրել ենք մի քանի գրառում: Ֆայլի առաջին էջում, որտեղ պահվում է աղյուսակը, առաջացել են նոր տվյալներ: Սա կենդանի տողի տարբերակներ են, որոնք հասանելի են մյուս գործառնություններին կոմիտից հետո (ապահովման համար անվիճելի համանման, որ isolation level-ը Read Committed է):

Այնուհետև մենք թարմացրել ենք գրառումներից մեկը և այդ կերպ հին տարբերակը նշել ենք անխուսափելի:

Քայլ առ քայլ, թարմացնելով և ջնջելով տողի տարբերակները, մենք ստացել ենք էջ, որտեղ մոտավորապես կեսը տվյալների՝ “թափոններ” են: Այս տվյալները չեն երևում որևէ գործարքի համար:

Postgres-ում գործում է մեխանիզմ , որը մաքրում է անարդյունավետ տարբերակները և ազատում տեղ նոր տվյալների համար: Բայց եթե այն կարգավորված չէ այնքան ագրեսիվ, կամ զբաղված է աշխատում այլ աղյուսակների վրա, ապա “թափոնային տվյալները” մնում են, և մեզ հարկավոր է օգտագործել լրացուցիչ էջեր նոր տվյալների համար:
Այդպիսով, մեր օրինակին, ինչ-որ պահի աղյուսակը կունենա չորս էջ, բայց այնտեղ կենդանի տվյալների միայն կեսը կլինի: Արդյունքում, երբ մենք դիմում ենք աղյուսակին, մենք տեսնում ենք շատ ավելի շատ տվյալներ, քան անհրաժեշտ է:

Շատ անգամ, եթե հիմա VACUUM-ը ջնջի բոլոր անարդյունավետ տողերի տարբերակները, իրավիճակը չի բարելավվի.cardinal. Ունենալու ենք ազատ տեղ էջերում կամ անգամ ամբողջ էջերում նոր տողերի համար, բայց մենք դեռևս կմոռանանք, քան անհրաժեշտ է, երբ կարդանք տվյալները:
Դա ի նկատի ունենալով, եթե ամբողջապես դատարկ էջը (երկրորդը մեր օրինակին) հայտնվի ֆայլի վերջում, VACUUM-ը կարող էր այն կտրել: Բայց հիմա դա գտնվում է կեսին, այդ պատճառով ոչինչ անել չի կարող:

Երբ այսպիսի դատարկ կամ շատ բաց էջերի քանակը մեծանում է, ինչն էլ կոչվում է bloat, դա սկսում է ազդել արտադրողականության վրա.
Ընդհանուր նշվածը՝ bloat-ի ընթացքի մեխանիկան է, որը տեղի է ունենում աղյուսակներում։ Ադմինիստրատիվ նիշերում դա տեղի է ունենում գրեթե նույն կերպ.
Ունեմ արդյոք bloat?
Postgres-ի ներքին վիճակագրությունը օգտվելու մի քանի եղանակներ կան, որպեսզի պարզեք, արդյոք բլում ունեք: Առաջին գաղափարը՝ օգտագործելով Postgres-ի ներքին վիճակագրությունը, ով պարունակում է մոտավոր տեղեկատվություն աղյուսակներում տողերի քանակի, “ապահով” տողերի քանակի եւ այլն: Հայտնի է բազմաթիվ տարբերակներ արդեն պատրաստված սկրիպտների։ Մենք հիմքում վերցրել ենք PostgreSQL Experts-ից, որը կարող է գնահատել աղյուսակների bloat-ը զուգահեռ Toast-ի եւ btree-ազդանշանների bloat-ի հետ: Մեր փորձի համաձայն, դրա սխալը կազմում է 10-20%.
Մյուս եղանակը՝ օգտագործել , որը թույլ է տալիս բացահայտել էջերի ներսում եւ ստանալ ինչպես գնահատական, այնպես էլ ճշգրիտ bloat արժեք: Բայց երկրորդ դեպքում անհրաժեշտ է սկանավորել ամբողջ աղյուսակը.
Մենք bloat-ի փոքր արժեքը, մինչև 20%, ընդունում ենք ընդունելի: Այն կարելի է համեմատել fillfactor-ի հետ և . 50% և ավելի դեպքում կարող են սկսվել արտադրողականության խնդիրներ.
Bloat-ի դեմ պայքարի եղանակներ
Postgres-ում bloat-ի դեմ պայքարի մի քանի եղանակներ կան "փաթեթում", սակայն դրանք բոլորին չեն կարող համար լինել կիրառելի:
Կռահեք AUTOVACUUM-ը, որ bloat-ը չընկնի. Մինչդեռ նկատենք, որ պետք է պահել այն ընդունելի մակարդակի վրա: Ռուսերեն այդ ավարտող խորհուրդը կարծես "կապտիչ" խորհուրդ է, սակայն իրականում դա միշտ չէ, որ հեշտ է հասնել: Օրինակ, եթե ակտիվ զարգացում եք իրականացնում տվյալների սխեմայի կանոնավոր փոփոխությամբ կամ տեղի է ունենում որեւէ տվյալների միգրացիա: Հետեւաբար, ձեր ծանրաբեռնվածության պրոֆիլը կարող է հաճախ փոխվել եւ, սովորաբար, տարբեր է տարբեր աղյուսակների համար: Այսինքն՝ պետք է միշտ մի քիչ ավելի ժամանակ անցկացնել եւ կարգավորել AUTOVACUUM-ը յուրաքանչյուր աղյուսակի փոխվող պրոֆիլին: Բայց ակնհայտ է, որ դա հեշտ չէ անել.
AUTOVACUUM-ի հարցերը, որոնցով կետերը չհասնում են մշակվեն աղյուսակներ, հաճախակի գործարքների ու երկարատեւ գործարքների առկայությունն է, որոնք չեն թույլատրում նրան մաքրել տվյալները, քանի որ դրանք հասանելի են այդ գործարքներին: Այս դեպքում խորհուրդը նույնպես ակնհայտ է՝ ազատվել "օրեր" գործարքներից եւ նվազագույնացնել ակտիվ գործարքների ժամանակը: Իսկ եթե ձեր հավելվածի ծանրության որքանությունը՝ OLAP-ի եւ OLTP-ի հիբրիդ է, ապա կարող եք ունենալ ինչպես շատ հաճախակի թարմացումներ եւ կարճ հարցումներ, այնպես էլ երկարաձիգ գործողություններ՝ օրինակ, որեւէ հաշվետվություն կազմելու: Այդպիսի իրավիճակում արժե մտածել տարբեր տվյալների բազաներ ունենալու վրա, ինչը հնարավորություն կտա ավելի մանրակրկիտ կարգավորում անցկացնել յուրաքանչյուրից.
Այլ մեկ օրինակ՝ նույնիսկ եթե պրոֆիլը homogոն է, սակայն DB-ն գտնվում է շատ բարձր ծանրաբեռնվածության տակ, ապա նույնիսկ առավել ագրեսիվ AUTOVACUUM-ը կարող է չկարողանալ և bloat-ը առաջ կհիմնվի: Շահագործումը (հենակետային կամ գործողային) միակ լուծումն է:
Ինչպես անել այնպիսի վիճակում, երբ դուք AUTOVACUUM-ը կարգավորել եք, սակայն bloat-ը շարունակվում է աճել:
Հրաման VACUUM FULL վերակառուցում է աղյուսակների և ցուցակների բովանդակությունը և թողնում է միայն արդիական տվյալները: Bloat-ը վերացնելու համար այն աշխատում է իդեալական կերպով, բայց դրա կատարման ընթացքում խափանվում է բացառիկ թռցումը աղյուսակի վրա (AccessExclusiveLock), որը թույլ չի տալիս կատարելու հարցումներ այդ աղյուսակների համար, նույնիսկ select-եր: Եթե դուք կարող եք թույլ տալ կանգնեցնել ձեր ծառայությունը կամ դրա մասը մի փոքր ժամանակ (մի քանի րոպեից մինչև մի քանի ժամ, կախված DB-ի չափից և ձեր սարքավորումից), ապա այս տարբերակը լավագույնն է: Ցավոք, մենք ժամանակ չունենք VACUUM FULL անցնելու համար ծրագրավորված սպասարկման ընթացքում, ուստի այս մեթոդը մեզ չի հարմարաում:
Հրաման CLUSTER այնպես չի, որ VACUUM FULL-ը վերակառուցում է աղյուսակների բովանդակությունը, ընդ որում թույլ է տալիս նշել ցուցակ, ըստ որի տվյալները ֆիզիկապես կստեղծվեն սկավառակում (բայց ապագայում նոր տողերի համար կարգը չի երաշխավորվում): Մի շարք հարցերի համար սա լավ օպտիմիզացում է — հաշվի առնելով մի քանի գրառումներ ըստ ցուցակի: Վարչապետի պակասը նույնն է, ինչ VACUUM FULL-ի՝ այն խափանվում է աղյուսակի վրա իր աշխատանքի ընթացքում:
Հրաման REINDEX նման է երկու նախորդներին, բայց կատարում է կոնկրետ ցուցակի կամ բոլոր աղյուսակի ցուցակների վերակառուցումը: Խափանումները մի փոքր թուլացված են: ShareLock աղյուսակի վրա (խափանում է փոփոխությունները, բայց թույլ է տալիս կատարել select) և AccessExclusiveLock վերակառուցված ցուցակի վրա (խափանում է հարցումները, որոնք օգտագործում են այդ ցուցակը): Սակայն 12-րդ տարբերակում Postgres-ի հետն աճեց մի պարամետր , որը թույլ է տալիս վերակառուցել ցուցակը, չխափանելով իրականացման ավելացված, փոփոխված կամ ջնջված գրառումների՝ մնացած մենակատարում:
Մինչո՞ւ ավելի վաղ Postgres միացման միջոցով ստացվի արդյունք, նման REINDEX CONCURRENTLY պատրաստելու միջոցով . Այն թույլ է տալիս ստեղծել ցուցակ առանց խիստ խափանման (ShareUpdateExclusiveLock, որը խափանում է այդ միջանցիկ հարցումները), ապա փոխարենը մեծացնել հին ցուցակը նորի հետ և ջնջել հին ցուցակը: Սա թույլ է տալիս վերացնել ցուցակների bloat-ը, չխափանելով ձեր կիրառման աշխատանքը: Կարևոր է նկատի ունենալ, որ ցուցակների վերակառուցման ժամանակ կլինի լրացուցիչ ծանրաբեռնվածություն սկավառակի ենթադրյալ համակարգերի վրա:
Այդպիսով, եթե ցուցակների համար կան եղանակներ bloat-ը վերացնելու "գովազդդից", ապա աղյուսակների համար նրանք չունեն: Тут սկսվում են տարբեր արտաքին ընդլայնումներ: (նախկին pg_reorg), , և մյուսները: Այս հոդվածի շրջանակներում ես չեմ համեմատելու նրանց և միայն կպատմեմ մասին pg_repack-ի, որը որոշ աշխատանքով մենք օգտագործում ենք մեր մոտ:
Ինչպես է գործում pg_repack

فرض کړئ چې موږ یو عام جدول لرو - د اندیسونو، محدودیتونو سره او، بدبختانه، د بلیوټ سره. د pg_repack لومړی ګام یو لاګ جدول جوړول دي، ترڅو د ټولو بدلونونو معلومات د کار په جریان کې وساتي. تیزر به دا بدلونونه د هر داخل، تازه کولو او حذف پر وخت راجستر کړي. بیا یو جدول جوړونې چې د اصلي جوړښت سره ورته دی، مګر د اندیسونو او محدودیتونو پرته، ترڅو د معلوماتو درج کولو پروسې ته خنډ رامینځته نه کړي.
بیا pg_repack زړو جدول نه نوي جدول ته معلومات لیږي، په اتوماتیک ډول ټول غیر معتبر لیکونه تصفیه کوي او بیا د نوي جدول لپاره اندیسونه رامینځته کوي. د دې عملیاتو د ترسره کولو په موده کې، لاګ جدول کې بدلونونه جمع کیږي.
بله مرحله - بدلونونه نوي جدول ته لیږدول. لیږد په څو تکرارونو کې ترسره کیږي، او کله چې لاګ جدول کې له 20 ریکارډو څخه کم پاتې شي، pg_repack یوه قاعده بندي نیسي، وروستي معلومات لیږدوي او زوی جدول په سیسټمونو کې نوي ته بدلوي. دا یوازینی او خورا لنډه اداره ده، چې تاسو نه شئ کولی د جدول سره کار وکړئ. وروسته له دې زوی جدول او لاګ جدول حذف کیږي او په فایل سیسټم کې ځای ازاد کیږي. پروسه بشپړه شوه.
په تیورۍ کې، هر څه ښه ښکاري، په عمل کې څه؟ موږ pg_repack د بار پرته او لاندې د بار سره آزموینه کړې، د مخکېني درول (په اسانه، د Ctrl+C له لارې) په صورت کې یې فعالیت تایید کړی. ټولې ازموینې مثبتې وې.
موږ پر تولید لاړ شو - او دلته هر څه هغه ډول نه شول لکه څنګه چې موږ توقع لرله.
پر تولید کې لومړی خنډ
په لومړي کلسټر کې موږ د یوازیني محدودیت د نقض کولو خطا ترلاسه وکړه:
$ ./pg_repack -t tablename -o id
INFO: د "tablename" جدول بیا راپکېچینګ
ERROR: پوښتنه بریالۍ نه ده:
ERROR: د تکرار شوي کلیدي ارزښت یوازینې قاعده "index_16508" نقض کوي
تفصیل: کلید (id، index)=(100500، 42) لا دمخه شتون لري.
دا محدودیت د اتوماتیک نوم index_16508 درلود - دا د pg_repack لخوا جوړ شوی و. د دې تعلق لرونکو پراپرټیو په اساس، موږ "زموږ" محدودیت معلوم کړ چې له هغې سره سمون لري. ستونزه دا وه چې دا یوازې یو عادي محدودیت نه و، بلکې یوه ځنډېدلې ()، یعنې د دې تصدیق وروسته له هغه ترسره کیږي چې sql-کمانډ، چې د ناڅاپي پایلو لامل کیږي.
ځنډېدلې محدودیتونه: ولې اړتیا لري او څنګه کار کوي
د ځنډېدلو محدودیتونو په اړه لږه تیوري.
راځئ چې یو ساده مثال راپورته کړو: موږ د موټرونو د حواله جدول لرو چې دوه خصوصیتونه لري - نوم او د حواله کې د موټر ترتیب.

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
);Որպեսզի մեր սկզբնական հարցման տրամաբանությունը երաշխավորի, որ հանձնման պահին բոլոր արժեքները եզակի են, այն հաջողությամբ կկատարի:
Վերն փուլում ներկայացված օրինակն անխուսափելի է, բայց գաղափարը բացահայտում է: մեր հավելվածում մենք օգտագործում ենք ուշացած սահմանափակումներ՝ կիրառելով տրամաբանությունը, որը պատասխանատու է հակասությունները լուծելու համար, երբ օգտվողները միաժամանակ աշխատում են ընդհանուր widget-ների ցուցակով: Ակցիալական կոդը մի փոքր պարզ դարձնել թույլատրող նման սահմանափակումների օգտագործումը:
Ընդհանուր առմամբ, Postgres-ում սահմանափակման հետևի տեսակի մեջ կա երեք մակարդակ դրանց ստուգման: Ստողանի մակարդակ, գործարքների և արտահայտությունների մակարդակ:

Ընտանիք:
CHECK և NOT NULL միշտ проверяются на уровне строки, для остальных ограничений, как видно из таблицы, есть разные варианты. Более подробно можно изучить .
Ամփոփելու համար, ուշացած սահմանափակումները բազմության մի շարք իրավիճակներում տալիս են ավելիReadable code և պակաս թվային հրահանգներ: Բայց դրա համար պետք է քրողորել դեբագի գործընթացը, քանի որ սխալի դնելը և դրա մասին տեղեկանալը տարբեր ժամանակ են: Շարունակական հարց է, որ պլանավորիչը միշտ չի կարող կառուցել օպտիմալ պլան, եթե հարցում օգտագործվի ուշացած սահմանափակում:
pg_repack-ի արդիացում
Մենք հասկացել ենք, թե ինչ են ուշացած սահմանափակումները, բայց ինչ կապ են նրանք ունեն մեր խնդրի հետ? Հիշենք մեզ նախկինում ստացված սխալը:
$ ./pg_repack -t tablename -o id
INFO: د "tablename" جدول بیا راپکېچینګ
ERROR: پوښتنه بریالۍ نه ده:
ERROR: د تکرار شوي کلیدي ارزښت یوازینې قاعده "index_16508" نقض کوي
تفصیل: کلید (id، index)=(100500، 42) لا دمخه شتون لري.Այն ծագում է տվյալների լոգ-թեքից նոր թափանցիկում տվյալների պատճենման պահին: Սա տարօրինակ է թվում, քանի որ լոգ-թեքում տվյալները կատարումն են ստանում ըստ սկզբնական թափանցիկի տվյալների: Եթե դրանք բավարարում են սկզբնական թափանցիկի սահմանափակումներին, ապա ինչպես կարող են դրանք խախտել նույն սահմանափակումները նորում:
Հանկարծ պարզվեց, որ խնդիրը թաքնված է pg_repack-ի նախորդ քայլում, որտեղ ստեղծվում են միայն ինդեքսներ, բայց ոչ սահմանափակումներ: Գոյություն ունեցող թափանցիկում է եղել եզակի սահմանափակում, իսկ նորում դրա փոխարեն ստեղծվել է եզակի ինդեքս:

Այս դեպքում կարևոր է նշել, որ եթե սահմանափակը սովորական է, և ոչ ուշացած, ապա դրա փոխարեն ստեղծված եզակի ինդեքսը հավասարազոր է այդ սահմանափակմանը, քանի որ Postgres-ում եզակի սահմանափակումները իրականացվում են եզակի ինդեքսի ստեղծմամբ: Բայց երբ խոսքը վերաբերում է ուշացած սահմանափակմանը, պահվածքը բոլորովին մեկական չէ, քանի որ ինդեքսը չի կարող լինել ուշացված և միշտ է ստուգվում SQL-հրաման կատարելու ժամանակ:
Այսպիսով, խնդրի սիրտը կայանում է ստուգման 'ուշացածության' մեջ: Սկզբնական թափանցիկում այն տեղի է ունենում կատարումն իրականացնելու պահին, իսկ նորում դա տեղի է ունենում SQL-հրաման կատարելու պահին: Значит нам нужно сделать так, чтобы проверки выполнялись одинаково в обоих случаях: либо всегда отложенно, либо всегда сразу же.
Ահա թե ինչպիսի գաղափարներ ունենք.
Ստեղծել ինդեքս, որը նման է ուշացածին
Առաջին միտքը ՝ կատարել երկուսն էլ ստուգումները անմիջական ռեժիմում: Սա կարող է առաջացնել մի քանի կեղծ դրական արդյունք սահմանափակման գործում, բայց եթե դրանք քիչ լինեն, ապա դա չի անդրադառնա օգտվողների աշխատանքին, քանի որ նրանց համար նման հակասությունները նորմալ իրավիճակ են: Դրանք տեղի են ունենում, օրինակ, երբ երկու օգտվողներ միաժամանակ սկսում են խմբագրել նույն վիջետը, և երկրորդ օգտվողի հաճախորդը չի հասցնում ստանալ տեղեկություն այդ վիջետի մասին, որը արդեն արգելափակվել է առաջին օգտվողի կողմից: Այդպիսի իրավիճակում սերվերը պատասխանատվություն է տալիս երկրորդ օգտվողին մերժում, իսկ նրա հաճախորդը վերադառնում է փոփոխություններ և արգելափակում է վիջետը: Մի փոքր հետո, երբ առաջին օգտվողը կավարտի խմբագրումը, երկրորդը կստանա տեղեկությունը, որ վիջետը այլևս արգելափակված չէ և կարող է կրկնել իր գործողությունները:

Որտեղ ստուգումները միշտ ուշացված ռեժիմում լինեն, մենք ստեղծել ենք նոր ինդեքս, որը նման է սկզբնական ուշացած սահմանափակմանը.
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 - տվյալները այլևս չեն հակասում;
- Միավորում ենք փոփոխությունները.
ստեղծել աղյուսակ test_table
(
id սերիալ,
val int,
սահմանափակում uk_test_table__val եզակի (val) կարող է հետաձգվել սկզբում հետաձգված
);
Տեղադրել test_table (val) արժեքներ (0);
FOR i IN 1..10000 LOOP
BEGIN
Տեղադրել test_table արժեքներ (0) վերադարձնել 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: د "tablename" جدول بیا راپکېچینګ
ERROR: پوښتنه بریالۍ نه ده:
ERROR: د تکرار شوي کلیدي ارزښت یوازینې قاعده "index_16508" نقض کوي
تفصیل: کلید (id، index)=(100500، 42) لا دمخه شتون لري.klassik hal - թեստային միջավայրերում ամեն ինչ աշխատում է, իսկ պրոդում - չէ?!
APPLY_COUNT և երկու բաչերի ստիկ
Մենք սկսեցինք վերլուծել կոդը բառացիորեն առ շարք, և հայտնաբերեցինք կարևոր պահ. տվյալների հաղորդումը հաշվի է առնվում ըստ բաչերի, constante APPLY_COUNT-ը ցույց էր տալիս բաչի չափը:
for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);
if (num > MIN_TUPLES_BEFORE_SWITCH)
continue;
/* միանգամայն պրոցեսում դեռ կան tuple-ներ, կրկնապատկել. */
...
}Problema-ն նրանում է, որ ծագող գործարքի տվյալները, որոնց ընթացքում մի քանի գործողություններ կարող են պոտենցիալ խախտել սահմանափակումը, տեղափոխման ժամանակ կարող են ընկնել երկու բաչերի սահմանը - առաջին բաչում կկատարվի կես հրահանգների, իսկ մյուսում - մյուս կեսը: Եվ այստեղ ինչ-որ կերպ. եթե առաջին բաչի հրահանգները ոչ մի խախտում չեն առաջացնում, ապա ամեն ինչ լավ է, իսկ եթե խախտում են, ապա տեղի կունենա սխալ:
APPLY_COUNT-ն 1000 գրանցման, ինչը բացատրող է, թե ինչու մեր փորձարկումները հաջողությամբ անցել էին - դրանք չ覆盖 էին «բաչերի ստիկ» դեպքը: Մենք օգտագործում էինք երկու հրահանգ - տեղադրում և թարմացում, հետևաբար հարյուրավոր 500 գործարքներ երկու հրահանգներով միշտ տեղադրվում էին բաչում և մենք խնդիրներ չունեինք: Հետաքրքիր է, որ երկրորդ UPDATE-ը ավելացնելիս մեր փոփոխությունը այլևս աշխատել չի եղել:
FOR i IN 1..10000 LOOP
BEGIN
Տեղադրել test_table արժեքներ (1) վերադարձնել id INTO v_id;
UPDATE test_table set val = i where id = v_id;
UPDATE test_table set val = i where id = v_id; -- մեկ նոր թարմացում
COMMIT;
END;
END LOOP;Ուրեմն, հաջորդ խնդիրը՝ հետևողականորեն ապահովել այն, որ մուտքային մուտքերի տվյալները, որոնք փոխվել են մեկ գործարքի ընթացքում, ընդունվեն նոր աղյուսակում նույնպես մեկ գործարքի շրջանակներում:
Բաթչից հրաժարում
Մենք կրկին ունեինք երկու լուծում: Առաջինը՝ հրաժարվել բաթչի ձևաչափից և տվյալների փոխանցումը կատարել մեկ գործարքով։ Այս լուծման օգտին է խոսում նրա պարզությունը՝ անհրաժեշտ կոդի փոփոխությունները նվազագույնի են հասցվում (ասենք, ավելի հին տարբերակներում pg_reorg-ը աշխատում էր հենց այսպես): Բայց կա խնդիր՝ մենք ստեղծում ենք երկարատև գործարք, ինչը, ինչպես արդեն նշվեց, սպառնալիք է նոր bloat- ի առաջացման համար.
Երկրորդ լուծումը ավելի բարդ է, բայց, հավանաբար, ավելի ճիշտ. ստեղծել լոգ-աղյուսակում սյունակ, որը կպահողի գործարքի նույնականիչը, որը ավելացրել է տվյալները աղյուսակում։ Երբ տվյալները կրկնում ենք, մենք կարող ենք խմբավորել դրանք ըստ այս հատկության և երաշխավորել, որ առնչվող փոփոխությունները կներկայացվեն միասին։ Բաթչը ձևավորելու է մի քանի գործարքներից (թեկուզ մեկ մեծ) և դրա չափը փոփոխվելու է՝ կախված նրանից, թե որքան շատ տվյալ է փոխվել այս գործարքներում։ Կարևոր է նշել, որ քանի որ տարբեր գործարքների տվյալները մտնում են լոգ-աղյուսակ կացուցային կարգով, ապա արդեն չի լինի հնարավոր sequential կերպով կարդալ, ինչպես նախկինում։ seqscan յուրաքանչյուր հարցման համար tx_id-ի հաշվարկում – դա չափազանց թանկ է, հարկավոր է ինդեքս, սակայն նա նույնպես կնվազեցնի մեթոդի աշխատանքը՝ նրա թարմացման ավելորդ ծախսերի հաշվին։ Ընդհանրապես, ինչպես միշտ, ինչ-որ բանի զոհաբերել է անհրաժեշտ:
Ուրեմն, մենք որոշեցինք սկսել առաջին տարբերակից, քանի որ դա ավելի հեշտ է։ Նախ, անհրաժեշտ էր հասկանալ արդյոք երկարատև գործարքը իրական խնդիր կլինի։ Քանի որ հիմնական տվյալների փոխանցումը հին աղյուսակից նոր է տեղի ունենում նույնպես մեկ երկարատև գործարքում, ապա հարցը վերածվեց «ինչքանով մենք մեծացնում ենք այս գործարքը»։ Առաջին գործարքի տևողությունը հիմնականում կախված է աղյուսակի չափից։ Նոր գործարքի տևողությունը՝ այն, թե որքան ամենաշատ փոփոխություններ կուտակվեն աղյուսակում տվյալների փոխանցման ընթացքում, այլ կերպ ասաց, բեռի ինտենսիվությունից։ pg_repack-ը անցկացվել է ծառայության ամենացածր բեռի ժամանակ, և փոփոխությունների ծավալը համեմատելի էր աղյուսակի սկզբնական ծավալից շատ քիչ։ Մենք որոշեցինք, որ կարող ենք անտեսել նոր գործարքի տևողությունը (համեմատության համար այս միջինում 1 ժամ 2-3 րոպեանոց է):
Փորձերը դրական էին: Նույնպես՝ պրոդուկցիա գործարկումը։ Ցուցադրության համար՝ պատկեր, որն էլ ներկայացնում է մեկ բազայի չափը՝ անցնում է:

Ինչպես որ այս լուծումը մեզ լիովին գոհացրեց, մենք չենք փորձել իրականացնել երկրորդը, բայց քննարկելու հնարավորության մասին խոսել ենք ընդլայնման մշակողների հետ: Ցավոք, մեր ընթացիկ զարգացում դեռ պատրաստ չէ հրապարակման, քանի որ մենք լուծել ենք միայն եզակի հետաձգված սահմանափակումների խնդիրը, իսկ լիարժեք ներարկման համար անհրաժեշտ է աջակցություն նաև այլ տեսակի հնարավորությունների համար: Հուսով ենք, որ սա կկարողանանք անել ապագայում:
Հնարավոր է, որ դուք ունենաք հարց, թե ինչու՞ մենք überhaupt մ دخեցինք pg_repack-ի հետ աշխատանքին, այլ ոչ թե օգտագործեցինք դրա անալոգները: Որպեսզի ճիշտ ժամանակին մեզ նույնպես դա մտաբերեցինք, բայց դրա օգտագործման դրական փորձը, նախ՝ առանց հետաձգված սահմանափակումների, խթանեց դրան, որպեսզի փորձենք հասկանալ խնդրի էությունը և այն լուծել: Հ además, այլ լուծումների օգտագործման համար նույնպես ժամանակ է պահանջվում թեստերի անցկացնելու համար, այնպես որ մենք որոշեցինք, որ նախ կփորձենք խնդիրը լուծել հենց դրա մեջ, և եթե հասկանանք, որ չենք կարող դա անել ընդունելի ժամանակի ընթացքում, ապա կսկսենք рассматривать аналоги:
Արդուկները
Ինչ կարող ենք խորհուրդ տալ մեր անձնական փորձի հիման վրա:
- Հսկեք ձեր bloat-ը: Հսկողության տվյալների հիման վրա դուք կարող եք հասկանալ, թե որքան լավ է настроен autovacuum-ը:
- Տեղավորվեք AUTOVACUUM-ը ՝ bloat-ը հարմար մակարդակի վրա պահելու համար.
- Եթե bloat-ը դեռ աճում է և չեք կարող նրան հաշիվ դարձնել «из коробки» միջոցների օգնությամբ, անեք չվախենալ օգտագործել արտաքին լայնածավալ տարբերակներ: Գլխավորը՝ ամեն բան լավ փորձարկել:
- Չվախենալ արտասահմանյան լուծումները ձեր կարիքների համար կատարելագործելուց — երբեմն սա կարող է ավելի արդյունավետ լինել և նույնիսկ ավելի հեշտ, քան ձեր սեփական կոդը փոխելը:
Ընտանիք: habr.com
