Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Աղյուսակների և ինդեքսների վրա փքվածության ազդեցությունը լայնորեն հայտնի է և առկա է ոչ միայն Postgres-ում: Կան միջոցներ, որոնք կարող են կարգավորվել դրա հետ առանց տուփի, օրինակ՝ VACUUM FULL կամ CLUSTER, բայց դրանք արգելափակում են աղյուսակները շահագործման ընթացքում և, հետևաբար, չեն կարող միշտ օգտագործվել:

Հոդվածը կպարունակի մի փոքր տեսություն այն մասին, թե ինչպես է առաջանում փքվածությունը, ինչպես կարող եք պայքարել դրա դեմ, հետաձգված սահմանափակումների և pg_repack ընդլայնման օգտագործման հետ կապված խնդիրների մասին:

Այս հոդվածը գրված է հիման վրա իմ ելույթը PgConf.Russia 2020-ում:

Ինչու է առաջանում փորկապություն:

Postgres-ը հիմնված է բազմատեսակ մոդելի վրա (MVCC). Դրա էությունն այն է, որ աղյուսակի յուրաքանչյուր տող կարող է ունենալ մի քանի տարբերակ, մինչդեռ գործարքները տեսնում են ոչ ավելի, քան այս տարբերակներից մեկը, բայց պարտադիր չէ, որ նույնը լինի: Սա թույլ է տալիս մի քանի գործարքներ աշխատել միաժամանակ և գործնականում ոչ մի ազդեցություն չունենալ միմյանց վրա:

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

Ենթադրենք, ունենք աղյուսակ, որին ավելացրել ենք մի քանի գրառում։ Նոր տվյալներ են հայտնվել ֆայլի առաջին էջում, որտեղ պահվում է աղյուսակը: Սրանք տողերի կենդանի տարբերակներն են, որոնք հասանելի են այլ գործարքների կատարման ավարտից հետո (պարզության համար մենք կենթադրենք, որ մեկուսացման մակարդակը Read Committed է):

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

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

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

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

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Postgres-ն ունի մեխանիզմ VACUUM, որը մաքրում է հնացած տարբերակները և տեղ է բացում նոր տվյալների համար։ Բայց եթե այն բավականաչափ ագրեսիվ կազմաձևված չէ կամ զբաղված է այլ աղյուսակներում աշխատելով, ապա «աղբի տվյալները» մնում են, և մենք պետք է լրացուցիչ էջեր օգտագործենք նոր տվյալների համար։

Այսպիսով, մեր օրինակում ժամանակի ինչ-որ պահի աղյուսակը բաղկացած կլինի չորս էջից, բայց դրա միայն կեսը կպարունակի կենդանի տվյալներ: Արդյունքում, աղյուսակին մուտք գործելու ժամանակ մենք կկարդանք շատ ավելի շատ տվյալներ, քան անհրաժեշտ է:

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Նույնիսկ եթե VACUUM-ը ջնջի բոլոր անհամապատասխան տողերի տարբերակները, իրավիճակը կտրուկ չի բարելավվի: Մենք ազատ տեղ կունենանք էջերում կամ նույնիսկ ամբողջ էջերում նոր տողերի համար, բայց մենք դեռ կկարդանք ավելի շատ տվյալներ, քան անհրաժեշտ է:
Ի դեպ, եթե ֆայլի վերջում լիներ լրիվ դատարկ էջը (մեր օրինակի երկրորդը), ապա VACUUM-ը կկարողանար կտրել այն։ Բայց հիմա նա մեջտեղում է, ուստի նրա հետ ոչինչ անել հնարավոր չէ:

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Երբ նման դատարկ կամ խիստ նոսր էջերի թիվը մեծանում է, ինչը կոչվում է bloat, այն սկսում է ազդել աշխատանքի վրա:

Վերևում նկարագրված ամեն ինչ աղյուսակներում փքվածության առաջացման մեխանիզմն է: Ինդեքսներում դա տեղի է ունենում մոտավորապես նույն կերպ:

Ես ունե՞մ փքվածություն:

Գոյություն ունեն մի քանի եղանակներ՝ որոշելու, թե արդյոք ունեք փքվածություն: Առաջինի գաղափարը ներքին Postgres վիճակագրության օգտագործումն է, որը պարունակում է մոտավոր տեղեկություններ աղյուսակների տողերի քանակի, «կենդանի» տողերի քանակի և այլնի մասին: Ինտերնետում կարող եք գտնել պատրաստի սցենարների բազմաթիվ տարբերակներ: Մենք հիմք ենք ընդունել սցենար PostgreSQL Experts-ից, որը կարող է գնահատել bloat աղյուսակները տոստերի և bloat btree ինդեքսների հետ միասին: Մեր փորձով դրա սխալը 10-20% է:

Մեկ այլ միջոց է օգտագործել ընդլայնումը pgstattuple, որը թույլ է տալիս նայել էջերի ներսը և ստանալ ինչպես գնահատված, այնպես էլ ճշգրիտ bloat արժեքը։ Բայց երկրորդ դեպքում դուք ստիպված կլինեք սկանավորել ամբողջ աղյուսակը:

Մենք ընդունելի ենք համարում փոքր փքվածության արժեքը՝ մինչև 20%: Այն կարելի է համարել որպես լրացնող գործոնի անալոգիա սեղաններ и ցուցանիշները. 50% և ավելի բարձր մակարդակի դեպքում կարող են սկսվել աշխատանքի հետ կապված խնդիրներ:

Փորկապության դեմ պայքարելու ուղիներ

Postgres-ը մի քանի եղանակ ունի՝ արկղից դուրս փքվածության դեմ պայքարելու համար, բայց դրանք միշտ չէ, որ հարմար են բոլորի համար:

Կարգավորեք AUTOVACUUM-ը այնպես, որ փքվածություն չառաջանա. Ավելի ճիշտ՝ պահել այն ձեզ համար ընդունելի մակարդակի վրա։ Սա կարծես «կապիտանի» խորհուրդ է, բայց իրականում դա միշտ չէ, որ հեշտ է հասնել: Օրինակ, դուք ակտիվ զարգացում ունեք տվյալների սխեմայի կանոնավոր փոփոխություններով, կամ ինչ-որ տվյալների միգրացիա է տեղի ունենում: Արդյունքում, ձեր բեռնվածության պրոֆիլը կարող է հաճախակի փոխվել և սովորաբար տարբերվում է աղյուսակից աղյուսակ: Սա նշանակում է, որ դուք պետք է անընդհատ աշխատեք մի փոքր առաջ և հարմարեցնեք AUTOVACUUM-ը յուրաքանչյուր սեղանի փոփոխվող պրոֆիլին: Բայց ակնհայտ է, որ դա հեշտ չէ անել։

Մեկ այլ ընդհանուր պատճառ, թե ինչու AUTOVACUUM-ը չի կարող հետևել աղյուսակներին, այն է, որ կան երկարատև գործարքներ, որոնք թույլ չեն տալիս մաքրել այդ գործարքների համար հասանելի տվյալները: Առաջարկությունն այստեղ նույնպես ակնհայտ է՝ ազատվել «կախված» գործարքներից և նվազագույնի հասցնել ակտիվ գործարքների ժամանակը: Բայց եթե ձեր հավելվածի ծանրաբեռնվածությունը OLAP-ի և OLTP-ի հիբրիդն է, ապա դուք կարող եք միաժամանակ ունենալ շատ հաճախակի թարմացումներ և կարճ հարցումներ, ինչպես նաև երկարաժամկետ գործողություններ, օրինակ՝ հաշվետվություն կազմելը: Նման իրավիճակում արժե մտածել բեռը տարբեր հիմքերի վրա տարածելու մասին, ինչը թույլ կտա ավելի լավ կարգավորել դրանցից յուրաքանչյուրը:

Մեկ այլ օրինակ. նույնիսկ եթե պրոֆիլը միատարր է, բայց տվյալների բազան շատ մեծ ծանրաբեռնվածության տակ է, ապա նույնիսկ ամենաագրեսիվ AUTOVACUUM-ը կարող է չդիմանալ, և փքվածություն առաջանա: Սանդղակը (ուղղահայաց կամ հորիզոնական) միակ լուծումն է:

Ինչ անել մի իրավիճակում, երբ դուք ստեղծել եք AUTOVACUUM, բայց փքվածությունը շարունակում է աճել:

Թիմ ՎԱԿՈՒՈՒՄԸ ԼՐԱՑՎԱԾ է վերակառուցում է աղյուսակների և ինդեքսների բովանդակությունը և դրանցում թողնում միայն համապատասխան տվյալներ: Փքվածությունը վերացնելու համար այն հիանալի է աշխատում, բայց դրա կատարման ընթացքում սեղանի վրա գրավվում է բացառիկ կողպեք (AccessExclusiveLock), որը թույլ չի տա այս աղյուսակում հարցումներ կատարել, նույնիսկ ընտրում է: Եթե ​​դուք կարող եք ձեզ թույլ տալ դադարեցնել ձեր ծառայությունը կամ դրա մի մասը որոշ ժամանակով (տասնյակ րոպեից մինչև մի քանի ժամ՝ կախված տվյալների բազայի չափից և ձեր սարքաշարից), ապա այս տարբերակը լավագույնն է: Ցավոք, մենք ժամանակ չունենք VACUUM FULL-ը գործարկելու պլանավորված սպասարկման ընթացքում, ուստի այս մեթոդը մեզ համար հարմար չէ:

Թիմ ՔԼԱՍՏԵՐ Վերակառուցում է աղյուսակների բովանդակությունը այնպես, ինչպես VACUUM FULL-ը, բայց թույլ է տալիս նշել ինդեքս, ըստ որի տվյալները ֆիզիկապես կպատվիրվեն սկավառակի վրա (սակայն ապագայում նոր տողերի պատվերը երաշխավորված չէ): Որոշակի իրավիճակներում սա լավ օպտիմիզացում է մի շարք հարցումների համար՝ ըստ ինդեքսների բազմաթիվ գրառումների ընթերցմամբ: Հրամանի թերությունը նույնն է, ինչ VACUUM FULL-ը. այն կողպում է սեղանը շահագործման ընթացքում:

Թիմ ՌԵԻՆԴԵՔՍ նման է նախորդ երկուսին, բայց վերակառուցում է որոշակի ինդեքս կամ աղյուսակի բոլոր ինդեքսները: Կողպեքները մի փոքր ավելի թույլ են. ShareLock-ը սեղանի վրա (կանխում է փոփոխությունները, բայց թույլ է տալիս ընտրել) և AccessExclusiveLock-ը վերակառուցվող ինդեքսի վրա (արգելափակում է հարցումները՝ օգտագործելով այս ինդեքսը): Սակայն Postgres-ի 12-րդ տարբերակում հայտնվեց պարամետր ՄԻԱԺԱՄԱՆԱԿ, որը թույլ է տալիս վերակառուցել ինդեքսը՝ առանց արգելափակելու գրառումների միաժամանակյա ավելացումը, փոփոխումը կամ ջնջումը:

Postgres-ի ավելի վաղ տարբերակներում դուք կարող եք հասնել REINDEX-ի նման արդյունքի՝ միաժամանակ օգտագործելով ՍՏԵՂԾԵԼ ԻԴԵՔՍԸ ՄԻԱԺԱՄԱՆԱԿ. Այն թույլ է տալիս ստեղծել ինդեքս առանց խիստ կողպման (ShareUpdateExclusiveLock, որը չի խանգարում զուգահեռ հարցումներին), այնուհետև փոխարինել հին ինդեքսը նորով և ջնջել հին ինդեքսը։ Սա թույլ է տալիս վերացնել ինդեքսային փքվածությունը՝ չխանգարելով ձեր դիմումին: Կարևոր է հաշվի առնել, որ ինդեքսների վերակառուցման ժամանակ սկավառակի ենթահամակարգի վրա լրացուցիչ բեռ կլինի:

Այսպիսով, եթե ինդեքսների համար կան «թռիչքի» փքվածությունը վերացնելու ուղիներ, ապա սեղանների համար չկան: Այստեղ է, որ գործում են տարբեր արտաքին ընդարձակումներ. pg_repack (նախկինում pg_reorg), pgcompact, pgcompactable եւ ուրիշներ. Այս հոդվածում ես դրանք չեմ համեմատի և կխոսեմ միայն pg_repack-ի մասին, որը որոշ փոփոխություններից հետո մենք ինքներս ենք օգտագործում։

Ինչպես է աշխատում pg_repack-ը

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ
Ենթադրենք, մենք ունենք միանգամայն սովորական աղյուսակ՝ ինդեքսներով, սահմանափակումներով և, ցավոք, փքվածությամբ։ pg_repack-ի առաջին քայլը գրանցամատյանների աղյուսակի ստեղծումն է, որը կպահի բոլոր փոփոխությունների մասին տվյալները, երբ այն աշխատում է: Գործարկիչը կկրկնի այս փոփոխությունները յուրաքանչյուր ներդիրի, թարմացման և ջնջման համար: Այնուհետև ստեղծվում է աղյուսակ, որը կառուցվածքով նման է սկզբնականին, բայց առանց ինդեքսների և սահմանափակումների, որպեսզի չդանդաղեցնի տվյալների ներդրման գործընթացը։

Այնուհետև 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-ի կողմից: Ելնելով դրա կազմի մեջ ներառված հատկանիշներից՝ մենք որոշեցինք «մեր» սահմանափակումը, որը համապատասխանում է դրան: Խնդիրը պարզվեց, որ սա լրիվ սովորական սահմանափակում չէ, այլ հետաձգված (հետաձգված սահմանափակում), այսինքն. դրա ստուգումը կատարվում է ավելի ուշ, քան sql հրամանը, ինչը հանգեցնում է անսպասելի հետևանքների:

Հետաձգված սահմանափակումներ. ինչու են դրանք անհրաժեշտ և ինչպես են դրանք գործում

Մի փոքր տեսություն հետաձգված սահմանափակումների մասին.
Դիտարկենք մի պարզ օրինակ. մենք ունենք մեքենաների աղյուսակ-տեղեկատու գիրք երկու ատրիբուտներով՝ մեքենայի անվանումը և կարգը գրացուցակում:
Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

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
);

Քանի որ մեր նախնական հարցման տրամաբանությունը երաշխավորում է, որ բոլոր արժեքները եզակի են կատարման պահին, այն կհաջողվի:

Վերը քննարկված օրինակը, իհարկե, շատ սինթետիկ է, բայց բացահայտում է գաղափարը։ Մեր հավելվածում մենք օգտագործում ենք հետաձգված սահմանափակումներ՝ տրամաբանությունն իրականացնելու համար, որը պատասխանատու է կոնֆլիկտների լուծման համար, երբ օգտատերերը միաժամանակ աշխատում են տախտակի վրա ընդհանուր վիջեթի օբյեկտների հետ: Նման սահմանափակումների օգտագործումը թույլ է տալիս մեզ մի փոքր ավելի պարզեցնել հավելվածի կոդը։

Ընդհանուր առմամբ, կախված սահմանափակումների տեսակից, Postgres-ն ունի դրանց ստուգման երեք մակարդակ՝ տող, գործարք և արտահայտման մակարդակ:
Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ
Source: աղաչողներ

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 և հետաձգված սահմանափակումներ

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

Այսպիսով, խնդրի էությունը ստուգման «ուշացման» մեջ է. սկզբնական աղյուսակում այն ​​տեղի է ունենում commit-ի պահին, իսկ նոր աղյուսակում՝ sql հրամանի կատարման պահին: Սա նշանակում է, որ մենք պետք է համոզվենք, որ ստուգումները երկու դեպքում էլ նույնն են կատարվում՝ կա՛մ միշտ հետաձգված, կա՛մ միշտ անմիջապես:

Այսպիսով, ի՞նչ գաղափարներ ունեինք։

Ստեղծեք հետաձգվածի նման ինդեքս

Առաջին գաղափարն այն է, որ երկու ստուգումները կատարվեն անմիջական ռեժիմով: Սա կարող է առաջացնել մի քանի կեղծ դրական սահմանափակումներ, բայց եթե դրանք քիչ են, դա չպետք է ազդի օգտատերերի աշխատանքի վրա, քանի որ նման կոնֆլիկտները նրանց համար սովորական իրավիճակ են: Դրանք տեղի են ունենում, օրինակ, երբ երկու օգտատեր սկսում են միաժամանակ խմբագրել նույն վիջեթը, իսկ երկրորդ օգտատիրոջ հաճախորդը չի հասցնում տեղեկատվություն ստանալ, որ վիջեթն արդեն արգելափակված է առաջին օգտատիրոջ կողմից խմբագրելու համար: Նման իրավիճակում սերվերը հրաժարվում է երկրորդ օգտագործողից, իսկ նրա հաճախորդը հետ է գլորում փոփոխությունները և արգելափակում է վիջեթը։ Մի փոքր ուշ, երբ առաջին օգտատերը կավարտի խմբագրումը, երկրորդը տեղեկատվություն կստանա, որ վիջեթն այլևս արգելափակված չէ և կկարողանա կրկնել իր գործողությունը:

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Ապահովելու համար, որ ստուգումները միշտ լինեն ոչ հետաձգված ռեժիմում, մենք ստեղծեցինք նոր ինդեքս, որը նման է սկզբնական հետաձգված սահմանափակումին.

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-ին աշխատել դրանց հետ: Մենք ընտրեցինք երկրորդը։

Փոխարինեք ինդեքսները նոր աղյուսակում սկզբնական աղյուսակի հետաձգված սահմանափակումներով

Վերանայման նպատակն ակնհայտ էր. եթե սկզբնական աղյուսակն ունի հետաձգված սահմանափակում, ապա նորի համար պետք է ստեղծել նման սահմանափակում, այլ ոչ թե ինդեքս։

Մեր փոփոխությունները ստուգելու համար մենք գրեցինք պարզ թեստ.

  • աղյուսակ հետաձգված սահմանափակումով և մեկ գրառումով.
  • մուտքագրել տվյալները մի օղակում, որը հակասում է գոյություն ունեցող գրառումին.
  • կատարել թարմացում. տվյալները այլևս չեն հակասում.
  • կատարել փոփոխությունները.

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-ի սկզբնական տարբերակը միշտ խափանում էր առաջին ներդիրում, փոփոխված տարբերակը աշխատում էր առանց սխալների: Հիանալի:

Մենք գնում ենք արտադրության և նորից սխալ ենք ստանում գրանցամատյանների աղյուսակից նորը պատճենելու նույն փուլում.

$ ./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 գրառումների, ինչը բացատրում է, թե ինչու են մեր թեստերը հաջողվել. դրանք չեն ընդգրկել «խմբաքանակի հանգույցի» դեպքը: Մենք օգտագործեցինք երկու հրաման՝ տեղադրել և թարմացնել, այնպես որ երկու հրամանների ուղիղ 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;

Այսպիսով, հաջորդ խնդիրն է համոզվել, որ սկզբնական աղյուսակի տվյալները, որոնք փոխվել են մեկ գործարքում, հայտնվում են նոր աղյուսակում նաև մեկ գործարքի ընթացքում:

Հրաժարում խմբաքանակից

Եվ կրկին ունեինք երկու լուծում. Նախ. եկեք ամբողջությամբ հրաժարվենք խմբաքանակների բաժանումից և տվյալները փոխանցենք մեկ գործարքով: Այս լուծման առավելությունը նրա պարզությունն էր. կոդերի պահանջվող փոփոխությունները նվազագույն էին (ի դեպ, հին տարբերակներում pg_reorg-ն աշխատում էր հենց այդպես): Բայց կա մի խնդիր՝ մենք ստեղծում ենք երկարաժամկետ գործարք, և դա, ինչպես նախկինում ասվեց, սպառնալիք է նոր փչովի առաջացման համար։

Երկրորդ լուծումն ավելի բարդ է, բայց, հավանաբար, ավելի ճիշտ՝ գրանցամատյանի աղյուսակում ստեղծել սյունակ՝ գործարքի նույնացուցիչով, որը տվյալներ է ավելացրել աղյուսակում: Այնուհետև, երբ մենք պատճենում ենք տվյալները, մենք կարող ենք դրանք խմբավորել այս հատկանիշով և ապահովել, որ համապատասխան փոփոխությունները փոխանցվեն միասին: Խմբաքանակը կձևավորվի մի քանի գործարքներից (կամ մեկ մեծից), և դրա չափը կտարբերվի՝ կախված նրանից, թե որքան տվյալներ են փոխվել այդ գործարքներում: Կարևոր է նշել, որ քանի որ տարբեր գործարքների տվյալները պատահական կարգով մուտք են գործում գրանցամատյանների աղյուսակ, այլևս հնարավոր չի լինի դրանք հաջորդաբար կարդալ, ինչպես նախկինում էր: seqscan-ը tx_id-ով զտող յուրաքանչյուր հարցման համար չափազանց թանկ է, անհրաժեշտ է ինդեքս, բայց այն նաև կդանդաղեցնի մեթոդը՝ դրա թարմացման գերբեռնվածության պատճառով: Ընդհանրապես, ինչպես միշտ, պետք է ինչ-որ բան զոհաբերել։

Այսպիսով, մենք որոշեցինք սկսել առաջին տարբերակից, քանի որ այն ավելի պարզ է: Նախ, պետք էր հասկանալ, թե արդյոք երկար գործարքը իրական խնդիր կլինի։ Քանի որ տվյալների հիմնական փոխանցումը հին աղյուսակից նորը նույնպես տեղի է ունենում մեկ երկարատև գործարքի ժամանակ, հարցը վերածվեց «որքանո՞վ ենք մենք մեծացնելու այս գործարքը»: Առաջին գործարքի տեւողությունը հիմնականում կախված է աղյուսակի չափից։ Նորի տեւողությունը կախված է նրանից, թե տվյալների փոխանցման ընթացքում քանի փոփոխություն է կուտակվում աղյուսակում, այսինքն. բեռի ինտենսիվության վրա. pg_repack-ի գործարկումը տեղի է ունեցել ծառայության նվազագույն բեռնվածության ժամանակ, և փոփոխությունների ծավալը անհամաչափ փոքր է եղել՝ համեմատած աղյուսակի սկզբնական չափի հետ: Մենք որոշեցինք, որ կարող ենք անտեսել նոր գործարքի ժամանակը (համեմատության համար՝ միջինը 1 ժամ 2-3 րոպե է)։

Փորձերը դրական էին. Գործարկել նաև արտադրության վրա: Պարզության համար ստորև ներկայացված է տվյալների բազաներից մեկի չափով նկար՝ գործարկումից հետո.

Postgres. bloat, pg_repack և հետաձգված սահմանափակումներ

Քանի որ մենք լիովին գոհ էինք այս լուծումից, մենք չփորձեցինք իրականացնել երկրորդը, բայց մենք դիտարկում ենք այն քննարկելու հնարավորությունը ընդլայնման մշակողների հետ։ Մեր ընթացիկ վերանայումը, ցավոք, դեռ պատրաստ չէ հրապարակման, քանի որ մենք խնդիրը լուծեցինք միայն եզակի հետաձգված սահմանափակումներով, իսկ լիարժեք կարկատելու համար անհրաժեշտ է աջակցություն տրամադրել այլ տեսակների: Հուսով ենք, որ ապագայում կկարողանանք դա անել:

Երևի ձեզ մոտ հարց է առաջանում, թե ինչու մենք նույնիսկ ներգրավվեցինք այս պատմության մեջ pg_repack-ի մոդիֆիկացմամբ և չօգտագործեցինք, օրինակ, դրա անալոգները: Ինչ-որ պահի մենք նույնպես մտածեցինք այս մասին, բայց այն ավելի վաղ օգտագործելու դրական փորձը, առանց հետաձգված սահմանափակումների սեղանների վրա, դրդեց մեզ փորձել հասկանալ խնդրի էությունը և շտկել այն: Բացի այդ, այլ լուծումների օգտագործումը նաև ժամանակ է պահանջում թեստեր անցկացնելու համար, ուստի մենք որոշեցինք, որ նախ կփորձենք շտկել խնդիրը դրա մեջ, և եթե հասկանանք, որ չենք կարող դա անել ողջամիտ ժամկետում, ապա կսկսենք դիտարկել անալոգները: .

Արդյունքները

Ինչ կարող ենք խորհուրդ տալ՝ հիմնվելով մեր սեփական փորձի վրա.

  1. Հետևեք ձեր փքվածությանը: Մոնիտորինգի տվյալների հիման վրա դուք կարող եք հասկանալ, թե որքան լավ է կազմաձևված ավտովակուումը:
  2. Կարգավորեք AUTOVACUUM-ը` փքվածությունը ընդունելի մակարդակի վրա պահելու համար:
  3. Եթե ​​փքվածությունը դեռ աճում է, և դուք չեք կարող հաղթահարել այն՝ օգտագործելով ներդաշնակ գործիքներ, մի վախեցեք օգտագործել արտաքին ընդլայնումներ: Հիմնական բանը ամեն ինչ լավ փորձարկելն է։
  4. Մի վախեցեք փոփոխել արտաքին լուծումները ձեր կարիքներին համապատասխան. երբեմն դա կարող է լինել ավելի արդյունավետ և նույնիսկ ավելի հեշտ, քան փոխել ձեր սեփական կոդը:

Source: www.habr.com

Добавить комментарий