Postgres: bloat, pg_repack və deferred constraints

Postgres: bloat, pg_repack və deferred constraints

Cədvəl və indekslərin balloon effekti (bloat) yaxşı məlumdur və bu yalnız Postgres-də deyil, geniş yayılıb. Bununla mübarizə aparmaq üçün "qutudan kənar" bəzi üsullar, məsələn, VACUUM FULL və ya CLUSTER, mövcuddur, lakin bunlar iş zamanı cədvəlləri bloklayır və buna görə də hər zaman istifadə oluna bilmir.

Məqalədə bloat-un necə meydana gəldiyi, onunla necə mübarizə aparılacağı, deferred constraints və onların pg_repack genişlənməsinin istifadəsində yaratdığı problemlərlə bağlı bir az nəzəriyyə olacaq.

Bu məqalə mənim çıxışımın əsasında yazılıb PgConf.Russia 2020.

Videonu izləyin

Bloat niyə yaranır

Postgres-in əsasını çoxversiyalı model (MVCC) təşkil edir. Bunun mahiyyəti, cədvəldəki hər bir sətirin bir neçə versiyaya malik ola bilməsidir, bu zaman tranzaksiyalar bu versiyalardan yalnız birini görə bilər, lakin mütləq eyni versiyanı görməzlər. Bu, bir neçə tranzaksiyanın eyni vaxtda işləməsinə imkan tanıyır və bir-birinə demək olar ki, təsir etmədən həyata keçir.

Aydındır ki, bu versiyaları saxlamaq lazım olacaq. Postgres yaddaşla səhifə şəklində işləyir və səhifə, diskin üzərindən oxumaq və ya yazmaq üçün minimum verilənlər həcmi deməkdir. Bu prosesin necə baş verdiyini başa düşmək üçün kiçik bir misala baxaq.

Təsəvvür edək ki, cədvələ bir neçə qeyd əlavə etmişik. Cədvəlin saxlandığı faylın ilk səhifəsində yeni məlumatlar meydana çıktı. Bu, tranzaksiyalar tərəfindən komit əməliyyatından sonra əlçatan olan sətirlərin canlı versiyalarıdır (sadələşdirmək üçün Read Committed izolyasiyası səviyyəsini qəbul edək).

Postgres: bloat, pg_repack və deferred constraints

Sonra biz qeydlərdən birini yenilədik və bununla köhnə versiyanı aktual olmayan olaraq qeyd etdik.

Postgres: bloat, pg_repack və deferred constraints

Addım-addım, sətirlərin versiyalarını yenilədikcə və silərkən, səhifədə təxminən yarıdan çox "zibil" verilənlər əldə etdik. Bu verilənlər heç bir tranzaksiyaya görünmür.

Postgres: bloat, pg_repack və deferred constraints

Postgres-də aktual olmayan versiyaları silən və yeni verilənlər üçün yer boşaldan VACUUMmexanizmi mövcuddur. Ancaq əgər bu mexanizm kifayət qədər aqressiv konfiqurasiya edilməmişsə və ya digər cədvəllərlə işləyirsə, "zibil verilənlər" qalır və yeni verilənlər üçün əlavə səhifələr istifadə etməli oluruq.

Beləliklə, misalımızda cədvəl bir müddət ərzində dörd səhifədən ibarət olacaq, lakin içərisində yalnız yarıdan çox canlı verilənlər olacaq. Nəticədə, cədvələ müraciət etdikdə, lazımdan çox verilənləri oxuyacağıq.

Postgres: bloat, pg_repack və deferred constraints

İndi VACUUM bütün aktual olmayan sətir versiyalarını silsə belə, vəziyyət köklü şəkildə yaxşılaşmayacaq. Səhifələrdə ya da yeni sətirlər üçün tam səhifələrdə boş yer yarada biləcəyik, amma hələ də lazımdan çox verilənləri oxuyacağıq.
Qeyd edək ki, tamamilə boş olan bir səhifə (bizim misalımızda ikinci) faylın sonunda olsaydı, VACUUM onu kəsə bilərdi. Ancaq hazırda o, ortada yerləşir, buna görə də onunla heç nə edə bilmirik.

Postgres: bloat, pg_repack və deferred constraints

Boş və ya güclü şəkildə azalmış səhifələrin sayı çox olduqda, buna bloat deyilir, bu da performansa təsir etməyə başlayır.

Üstündə izah olunanlar — cədvəllərdə bloat yaranma mexanikasıdır. İndekslərdə də buna bənzər bir şəkildə baş verir.

Mənim bloatım varmı?

Bloat-ın olub-olmadığını müəyyən etmək üçün bir neçə yol var. Birincisinin ideyası, Postgres-in daxili statistikasından istifadə edərək cədvəllərdəki sətir sayı, “canlı” sətir sayı və s. haqqında təxmini məlumat əldə etməkdir. İnternette müxtəlif hazır skriptlər tapa bilərsiniz. Biz aşağıdakıları əsas götürdük. skript PostgreSQL Experts tərəfindən, cədvəllərin bloatını, habelə toast və bloat btree indekslərini qiymətləndirməyə imkan verir. Təcrübəmizə görə, onun xəta payı 10-20% təşkil edir.

Başqa bir yol — pgstattuple, səhifələrin içində nəzər salmaq və həm təxmini, həm də dəqiq bloat dəyərlərini əldə etməyə imkan tanıyan bir uzantıdır. Ancaq ikinci halda, cədvəlin tam taranması lazım olacaq.

20%-dək olan kiçik bloat dəyərini biz qəbul edilən hesab edirik. Bunu cədvəllərindekslər üçün fillfactor-a bənzər bir şey olaraq düşünmək olar.. 50% və daha yuxarı olduqda, performans problemləri başlaya bilər.

Bloat ilə mübarizə yolları

Postgres-də “qutu içindən” bloat ilə mübarizə aparmaq üçün bir neçə yol var, lakin bunlar hər zaman hər kəs üçün uyğun olmaya bilər.

AUTOVACUUM-u tənzimləmək ki, bloat yaranmasın. Dəqiq desək, o xərcini qəbul edilən səviyyədə saxlamalıdır. Bu “kapitan” məsləhəti kimi görünür, lakin reallıqda bunu həmişə asanlıqla başa çatdırmaq olmur. Məsələn, sizin aktiv inkişaf prosesiniz var, məlumat sxeminin mütəmadi dəyişməsi və ya bir migrations prosesi baş verir. Nəticə olaraq, yük profiliniz tez-tez dəyişə bilər və adətən müxtəlif cədvəllər üçün müxtəlif olur. Deməli, siz daima bir az irəlidə işləməli və hər cədvəlin dəyişən profilinə uyğun AUTOVACUUM-u tənzimləməlisiniz. Lakin aşkar olaraq, bunu etmək asan deyil.

AUTOVACUUM-un cədvəlləri işləməyə kifayət qədər vaxt tapmamasının digər bir yaygın səbəbi uzun müddətli tranzaksiyaların mövcudluğudur, çünki onlar məlumatların silinməsinə imkan vermir, çünki bu tranzaksiyalara açıqdır. Burada da tövsiyə aydındır – “asılı” tranzaksiyalardan xilas olun və aktiv tranzaksiyaların vaxtını minimuma endirin. Ancaq əgər tətbiqinizin yükü OLAP və OLTP-nin hibrididirsə, o zaman eyni anda həm çox sayda tez yeniləmələr və qısa sorğular, həm də uzun müddətli əməliyyatlar – məsələn, hər hansı bir hesabatın hazırlanması – ola bilər. Bu cür bir vəziyyətdə yükü fərqli bazalara yaymağı düşünmək yaxşı olar; bu, hər birini daha incə tənzimləməyə imkan verəcəkdir.

Başqa bir misal – profil homojen olsa belə, amma verilənlər bazası çox yüksək yük altındadırsa, ən agresiv AUTOVACUUM belə işini apara bilməz, və bloat meydana çıxar. Şkalalaşma (şaquli və ya üfüqi) – yeganə həll yoludur.

AUTOVACUUM-u tənzimlədikdə, amma bloat artmağa davam edirsə, nə etməlisiniz.

Əmr VACUUM FULL cədvəllərin və indekslərin məzmununu yenidən qurur və onlarda yalnız aktual məlumatları saxlayır. Bloat-ı aradan qaldırmaq üçün mükəmməl işləyir, lakin icrası zamanı cədvəl üzərində eksklüziv bloklanma (AccessExclusiveLock) alır ki, bu da o cədvələ sorğuların, hətta seçimlərin də icrasına icazə vermir. Əgər xidmətinizi və ya onun bir hissəsini bir müddət (dəyişən verilənlər bazası ölçüsünə və avadanlığınıza görə dəqiqələrdən saatlara qədər) dayandırmağı bacarırsınızsa, bu variant ən yaxşısıdır. Təəssüf ki, biz VACUUM FULL-u planlaşdırılmış təmir müddətində icra etməyə yetişmirik, buna görə bu üsul bizim üçün uyğun deyil.

Əmr CLUSTER həmçinin cədvəlin məzmununu VACUUM FULL kimi yenidən qurur, bu zaman məlumatların diskdə fiziki olaraq sıralanacağı indeksi göstərə bilərsiniz (ancaq gələcəkdə yeni sətirlər üçün sıralama təmin edilmir). Müəyyən hallarda, bu, indeks üzrə bir neçə qeydi oxumaq üçün müəyyən sorğular üçün yaxşı bir optimallaşdırmadır. Komandanın çatışmazlığı VACUUM FULL ilə eynidir – iş zamanı cədvəl üzərində bloklama edir.

Əmr REINDEX iki əvvəlki ilə bənzəyir, lakin müəyyən bir indeksin və ya cədvəlin bütün indekslərinin yenidən qurulmasını həyata keçirir. Bloklamalar bir az zəifdir: cədvəldə ShareLock (dəyişikliklərə mane olur, lakin seçim icrasına icazə verir) və yenidən qurulan indeksdə AccessExclusiveLock (bu indeksi istifadə edən sorğuları bloklayır). Lakin Postgres-in 12-ci versiyasında aşağıdakı parametr təqdim olundu CONCURRENTLY, hansı ki, indeks alışlarının, dəyişikliklərinin və ya silinmələrinin paralel icrasını bloklamadan indeksin yenidən qurulmasına imkan verir.

Postgres-in daha əvvəlki versiyalarında REINDEX CONCURRENTLY ilə bənzər bir nəticə əldə etmək mümkündür CREATE INDEX CONCURRENTLY. Bu indeksin ciddi bloklama olmadan yaradılmasına imkan verir (ShareUpdateExclusiveLock, paralel sorğulara mane olmur), daha sonra köhnə indeks yeni ilə əvəz edilir və köhnə indeks silinir. Bu, indekslərin bloatının aradan qaldırılmasına imkan tanıyır, tətbiqinizin işini maneə törətmədən. İndekslərin yenidən qurulması zamanı disk alt sistemində əlavə yük olacağını nəzərə almaq vacibdir.

Buna görə də, indekslər üçün bloat-ı “isti” şəkildə aradan qaldırmağın yolları varsa, cədvəllər üçün yoxdur. Burada müxtəlif xarici əlavələr devreye girir: pg_repack (əvvəlki pg_reorg), pgcompact, pgcompacttable və başqaları. Bu məqalə çərçivəsində onları müqayisə etməyəcəyəm və yalnız bizdə müəyyən işləmələrdən sonra istifadə etdiyimiz pg_repack haqqında danışacağam.

pg_repack necə işləyir

Postgres: bloat, pg_repack və deferred constraints
Təsəvvür edin ki, bizdə adi bir cədvəl var - indekslər, məhdudiyyətlər və, təəssüf ki, bloat ilə. İlk addımda pg_repack bir log-cədvəl yaradır ki, bütün dəyişikliklərlə bağlı məlumatları saxlasın. Trigger, bu dəyişiklikləri hər bir insert, update və delete əməliyyatı zamanı təkrarlayacaq. Sonra isə, indekslər və məhdudiyyətlər olmadan məlumatları daxil etmək prosesini yavaşlatmamaq üçün orijinalı ilə eyni struktura sahib yeni bir cədvəl yaradılır.

Sonra pg_repack köhnə cədvəldən yeni cədvələ məlumatları köçürür, eyni zamanda bütün köhnəlmiş sətirləri avtomatik olaraq filtr edir və yeni cədvəl üçün indekslər yaradır. Bütün bu əməliyyatlar zamanı log-cədvəldə dəyişikliklər toplanır.

Növbəti addım - dəyişiklikləri yeni cədvələ köçürməkdir. Köçürmə bir neçə iterasiyada həyata keçirilir və log-cədvəldə 20-dən az qeyd qaldıqda, pg_repack ciddi kilid götürür, son məlumatları köçürür və köhnə cədvəli yeni ilə sistemi cədvəllərində əvəz edir. Bu, cədvəl ilə işləməyi dayandırdığınız çox qısa bir vaxtdır. Bundan sonra, köhnə cədvəl və log-cədvəl silinir və fayl sistemində yer boşaldılır. Proses başa çatdı.

Nəzəriyyədə hər şey gözəl görünür, bəs praktikada? Biz pg_repack-i yük altında və yük olmadan sınaqdan keçirdik, erkən dayanma halında (yəni Ctrl+C basıb) işini yoxladıq. Bütün testlər müsbət nəticələndi.

Biz istehsalata yollandıq - burda isə hər şey gözlədiyimiz kimi getmədi.

İstehsaldakı ilk cəhd

İlk klasterdə biz unikal məhdudiyyətin pozulması haqqında bir səhv aldıq:

$ ./pg_repack -t tablename -o id
INFO: "tablename" cədvəlini yenidən yığır
ERROR: sorğu uğursuz oldu: 
    ERROR: təkrarlanan açar dəyəri unikal məhdudiyyəti "index_16508" pozur
DETAL:  Açar (id, index)=(100500, 42) artıq mövcuddur.

Bu məhdudiyyət avtomatik yaradılmış index_16508 adlandı - bunu pg_repack yaratmışdır. Onun tərkibinə daxil olan atributlar ilə biz ona uyğun "bizim" məhdudiyyətimizi müəyyən etdik. Problemin səbəbi isə budur ki, bu tamamilə adi bir məhdudiyyət deyil, gecikmişdir (deferred constraint) yəni onun yoxlanması sql əmri tamamlandıqdan sonra həyata keçirilir, bu da gözlənilməz nəticələrə səbəb olur.

Gecikmiş məhdudiyyətlər: nə üçün lazımdır və necə işləyir

Gecikmiş məhdudiyyətlər haqqında bir az nəzəriyyə.
Sadə bir nümunəyə baxaq: bizdə avtomobillər üçün iki atributlu bir cədvəl var - ad və cədvəldə avtomobilin sırası.
Postgres: bloat, pg_repack və deferred constraints

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



Təsəvvür edin ki, biz ilk və ikinci avtomobillərin yerlərini dəyişmək istəyirik. "Birbaşa" həll - ilk dəyəri ikinci ilə, ikinci dəyəri birinci ilə yeniləməkdir:

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

Amma bu kodu icra edərkən gözləyə bildiyimizdən, cədvəldəki dəyərlərin sırası unikal olduğuna görə pozulmanı alacağıq:

[23305] XƏTA: təkrar açar dəyəri unikal məhdudiyyəti pozur “uk_cars”
Təfərrüat: Açar (ord)=(2) artıq mövcuddur.

Başqa cür necə etmək olar? Birinci variant: cədvəldə mütləq mövcud olmayan sıra dəyərini əlavə etmək, məsələn “-1”. Proqramlaşdırmada buna “iki dəyişənin üçüncü vasitəsilə mübadiləsi” deyilir. Bu metodun yeganə çatışmazlığı – əlavə update-dir.

İkinci variant: cədvəli yenidən dizayn etmək, belə ki, sıra dəyəri üçün tam saylar yerinə onluq nömrə tipindən istifadə olunsun. Bu zaman 1 kimi dəyəri 2.5-ə yenilədikdə, ilk yazı avtomatik olaraq ikinci və üçüncü arasında “yer alacaq”. Bu həll işləyir, lakin iki məhdudiyyət var. Birincisi, bu dəyər haradasa interfeysdə istifadə edilirsə, uyğun gəlməyəcək. İkincisi, məlumat növünün dəqiqlik səviyyəsindən asılı olaraq, bütün yazıların dəyərlərinin yenidən hesablanması həyata keçirilmədən, mümkün olan müəyyən sayda insert olacaq.

Üçüncü variant: məhdudiyyəti uzadılmış etmək, yalnız komit vaxtında yoxlanılması üçün:

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

Çünki ilkin sorğumuzun məntiqi komit vaxtında bütün dəyərlərin unikal olmasını təmin edir, ona görə də bu, uğurla icra olunacaq.

Yuxarıda nəzərdən keçirilən nümunə, əlbəttə ki, çox sintetikdir, lakin ideyayı açıqlayır. Bizim tətbiqimizdə istifadəçilərin ortaq obyekt-vidjetlərlə paralel işləməsi üçün ziddiyyətlərin həllinə cavab verən məntiqin tətbiqi üçün uzadılmış məhdudiyyətlərdən istifadə edirik. Belə məhdudiyyətlərin istifadəsi tətbiq kodunu bir qədər sadə etməyə imkan tanıyır.

Ümumilikdə, Postgres-də məhdudiyyət növündən asılı olaraq üç yoxlama səviyyəsi var: sətir, tranzaksiya və ifadə səviyyəsi.
Postgres: bloat, pg_repack və deferred constraints
Mənbə: begriff

CHECK və NOT NULL həmişə sətir səviyyəsində yoxlanılır, qalan məhdudiyyətlər üçün, cədvəldən göründüyü kimi, müxtəlif variantlar mövcuddur. Daha ətraflı oxuya bilərsiniz. burada.

Qısa şəkildə yekunlaşdırsaq, uzadılmış məhdudiyyətlər müəyyən hallarda oxumaq üçün daha aydın kod və daha az əmrlər verir. Lakin bunun müqabilində, düzəliş prosesinin çətinləşməsi mövcuddur, çünki xətanın meydana gəlmə vaxtı və xətanın başa düşülmə vaxtı ayrıdır. Ayrıca, mümkün olan bir problem odur ki, planlaşdırıcı həmişə optimal plan qura bilmir, əgər sorğuda uzadılmış məhdudiyyət iştirak edirsə.

pg_repack-in təkmilləşdirilməsi

Uzadılmış məhdudiyyətlərin nə olduğunu anladıq, lakin bunların bizim problemimizlə necə bağlı olduğunu düşünək. Təkrar aldığımız xətanı xatırlayaq:

$ ./pg_repack -t tablename -o id
INFO: "tablename" cədvəlini yenidən yığır
ERROR: sorğu uğursuz oldu: 
    ERROR: təkrarlanan açar dəyəri unikal məhdudiyyəti "index_16508" pozur
DETAL:  Açar (id, index)=(100500, 42) artıq mövcuddur.

Bu, log cədvəlindən yeni cədvələ məlumatların köçürülməsi anında baş verir. Bu, qəribə görünür, çünki log cədvəlindəki məlumatlar əsas cədvəl ilə birlikdə təsdiqlənir. Əgər onlar əsas cədvəlin məhdudiyyətlərinə uyğundursa, o zaman necə olur ki, yeni cədvəldə eyni məhdudiyyətləri pozurlar?

Məsələnin kökü, yalnız indekslərin yaradıldığı, lakin məhdudiyyətlərin yaradılmadığı pg_repack-in əvvəlki addımında gizlidir: köhnə cədvəldə unikal məhdudiyyət vardı, lakin yeni cədvəldə bunun əvəzinə unikal indeks yaradıldı.

Postgres: bloat, pg_repack və deferred constraints

Burada qeyd etmək vacibdir ki, əgər məhdudiyyət adi, yəni gecikdirilmiş deyilsə, onun yerinə yaradılan unikal indeks bu məhdudiyyətə bərabərdir, çünki Postgres-də unikal məhdudiyyətlər unikal indeks yaratmaqla həyata keçirilir. Lakin gecikdirilmiş məhdudiyyət halında davranış eyni deyil, çünki indeks gecikdirilə bilmir və həmişə sql əmri icra edildiyi vaxtda yoxlanılır.

Beləliklə, problemin mahiyyəti "gecikdirilmə" yoxlamasıdır: əsas cədvəldə bu, təsdiqləmə anında baş verir, yeni cədvəldə isə sql əmri icra edildiyi an baş verir. Deməli, biz müəyyən etməliyik ki, yoxlamalar hər iki halda eyni şəkildə həyata keçirilsin: ya həmişə gecikdirilmiş, ya da həmişə dərhal.

Bəs, bizdə hansı fikirlər var idi?

Gecikdirilmişə bənzər bir indeks yaratmaq.

Birinci fikir - hər iki yoxlamanı dərhal rejimdə həyata keçirməkdir. Bu, məhdudiyyətlərin bir neçə yanlış müsbət nəticə verməsinə səbəb ola bilər, lakin əgər bu, az olsa, istifadəçilərin işinə təsir etməməlidir, çünki bu cür mübahisələr onların üçün normal bir vəziyyət hesab olunur. Məsələn, iki istifadəçi eyni vaxtda eyni vidjetə redaktə etməyə başlayanda baş verir və ikinci istifadəçinin müştərisi vidjetin ilk istifadəçi tərəfindən redaktə edilməsi üçün bloklandığı barədə məlumatı ala bilmir. Bu vəziyyətdə server ikinci istifadəçiyə imtina cavabı verir, onun müştərisi isə dəyişiklikləri ləğv edir və vidjeti bloklayır. Bir az sonra, birinci istifadəçi redaktəni tamamladıqda, ikinci istifadəçi vidjetin artıq bloklanmadığını və öz hərəkətini təkrar edə biləcəyini öyrənir.

Postgres: bloat, pg_repack və deferred constraints

Yoxlamaların həmişə dərhal rejimdə olması üçün, biz süni unikal indeks yaratdıq, bu orijinal gecikdirilmiş məhdudiyyətə bənzəyir:

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

Test mühitində yalnız bir neçə gözlənilən səhv aldıq. Uğur! Yenidən pg_repack-i istehsalda işə saldıq və bir saat ərzində birinci klasterdə 5 səhv aldıq. Bu, qəbul edilən nəticədir. Lakin artıq ikinci klasterdə səhvlərin sayı dəfələrlə artdı və biz pg_repack-i dayandırmalı olduq.

Niyə belə oldu? Xətanın yaranma ehtimalı, eyni vaxtda eyni vidgetlərlə işləyən istifadəçi sayından asılıdır. Görünür, həmin an birinci klasterdə saxlanılan məlumatlarla bağlı rəqabətə səbəb olan dəyişikliklər digər klasterlərdən çox az idi, yəni sadəcə “şanslı” olduq.

İdeya işləmədi. Həmin an iki digər həll variantını gördük: tətbiq kodumuzu yenidən yazmaq, geriyə çəkilmiş məhdudiyyətlərdən imtina etmək, ya da pg_repack'i onları idarə etməyə “öyrətmək”. Biz ikinci yolu seçdik.

Yeni cədvəldəki indeksləri mənbə cədvəlindən geriyə çəkilmiş məhdudiyyətlərlə əvəz edin.

Düzəlişin məqsədi aydın idi – əgər mənbə cədvəlində geriyə çəkilmiş məhdudiyyət varsa, yeni cədvəldə də belə bir məhdudiyyət yaratmaq lazımdır, indeks deyil.

Dəyişikliklərimizi yoxlamaq üçün sadə bir test yazdık:

  • geriyə çəkilmiş məhdudiyyəti və bir qeydi olan cədvəl;
  • münaqişə yaradan məlumatları döngüdə əlavə edirik;
  • update edirik – məlumatlar artıq münaqişə yaratmır;
  • dəyişiklikləri təsdiqləyirik.

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;

Müəllifin ilkin versiyası ilk insert zamanı düşürdü, düzəliş edilmiş versiya səhvsiz işləyirdi. Əla.

Prosesə daxil oluruq və yenidən həmin məlumatın köçürülməsi fazasında xəta alırıq:

$ ./pg_repack -t tablename -o id
INFO: "tablename" cədvəlini yenidən yığır
ERROR: sorğu uğursuz oldu: 
    ERROR: təkrarlanan açar dəyəri unikal məhdudiyyəti "index_16508" pozur
DETAL:  Açar (id, index)=(100500, 42) artıq mövcuddur.

Klassik vəziyyət: test mühitlərində hər şey işləyir, amma prodakşnda – yoxdur?!

APPLY_COUNT və iki partiyanın birleşməsi

Kodun həqiqətən sətir-sətir analizinə başladıq və vacib bir məsələni aşkar etdik: məlumatların log cədvəlindən yeni cədvələ köçürülməsi partiya şəklində baş verir, APPLY_COUNT konstantası partiyanın ölçüsünü göstərirdi:

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

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  /* hələ də bəzi tuple-lar ola bilər, təkrar edin. */
...
}

Problem odur ki, bir neçə əməliyyatın potensial olaraq məhdudiyyəti pozması mümkündür, məlumatların ilk iki partiya arasında köçürülməsi zamanı – komandaların bir yarısı bir partiyada təsdiqlənəcək, digər yarısı isə ikinci partiyada. Və burada şans işə düşür: əgər birinci partiyadakı komandalar heç bir məhdudiyyəti pozmursa, hər şey yaxşıdır, amma əgər pozursa – xəta baş verir.

APPLY_COUNT 1000 qeydi eşitdir, bu da niyə testlərimizin uğurla keçdiyini açıqlayır – onlar “partiyaların kəsişməsi” halını əhatə etmirdilər. Biz iki əmrdən istifadə etdik – insert və update, ona görə də hər bir partiyada həmişə 500 tranzaksiya iki əmrlə yerləşirdi və biz problem yaşamırdıq. İkinci update əlavə edildikdən sonra düzəlişimiz işləmədi:

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; -- bir update daha
    COMMIT;
  END;
END LOOP;

Beləliklə, növbəti vəzifə - bir tranzaksiya içində dəyişən məlumatların orijinal cədvəldən yeni cədvələ də eyni tranzaksiya daxilində keçməsini təmin etməkdir.

Bölgüləmədan imtina

Yenə də bizdə iki həll variantı var idi. Birincisi: gəlin, təsadüfi olaraq, data transferini bir tranzaksiya ilə edək. Bu həllin lehinə onun sadəliyi var idi - kodda tələb olunan dəyişikliklər minimumdur (bu arada, daha köhnə versiyalarda, o zaman hələ pg_reorg elə belə işləyirdi). Amma problem var - uzundur tranzaksiya yaratmış oluruq, bu da əvvəllər də qeyd edildiyi kimi, yeni bloat yaranması üçün bir təhlükədir.

İkinci həll - daha çətin, amma bəlkə də daha düzgün: log-cədvəldə o cədvələ məlumat əlavə etmiş tranzaksiyanın identifikatorunu saxlayan bir sütun yaratmaqdır. Beləliklə, məlumatları kopyalayarkən, bu atribut əsasında qruplaşdıra bilərik və əlaqəli dəyişikliklərin birlikdə köçürülməsini təmin edə bilərik. Batch bir neçə tranzaksiyadan (və ya bir böyükdən) formalaşacaq və onun ölçüsü, bu tranzaksiyalarda nə qədər məlumatın dəyişdiyinə görə dəyişəcək. Əhəmiyyətli odur ki, müxtəlif tranzaksiyaların log-cədvələ təsadüfi qaydada daxil olmasına görə, onu əvvəllər olduğu kimi ardıcıllıqla oxumaq artıq mümkün olmayacaq. tx_id filtrasiyasına malik seqscan hər sorğuda çox baha başa gələcək, indeks lazımdır, amma o da onun yenilənməsi üçün xərclər səbəbindən metodun işini yavaşladacaq. Ümumiyyətlə, həmişə bir şeylə təhlükə yaratmaq lazımdır.

Beləliklə, biz sadə olduğu üçün birinci variantdan başlamağa qərar verdik. İlk öncə uzun müddətli tranzaksiyanın real bir problem olub-olmayacağını anlamaq lazım idi. Esasən, köhnə cədvəldən yeni cədvələ məlumatların köçürülməsi də eyni bir uzun müddətli tranzaksiyada gerçəkləşdiyi üçün, sual “bu tranzaksiyanı nə qədər artıracağıq?” formasına çevrildi. İlk tranzaksiyanın davamlılığı əsasən cədvəlin ölçüsündən asılıdır. Yeninin davamlılığı isə məlumatların transferi əsnasında cədvəldə nə qədər dəyişiklik toplanacağından asılıdır, yəni yük intensivliyindən. pg_repack, xidmətin minimal yüklənməsində işləndi və dəyişiklik həcmi orijinal cədvəl həcmi ilə müqayisədə qeyri-müqayisə olunacaq dərəcədə kiçik idi. Biz qərara gəldik ki, yeni tranzaksiyanın vaxtını gözardı edə bilərik (müqayisə üçün, bu təxminən 1 saat 2-3 dəqiqədir).

Eksperimentlər müsbət oldu. Prod-da başlama da. Daha aydın olması üçün - bir şəkil, birinin ölçüsü ilə bazadan sonra:

Postgres: bloat, pg_repack və deferred constraints

Bu həll bizi tamamilə qane etdiyi üçün ikinciyi sınaqdan keçirmək istəmədik, amma genişləndirmə üzrə proqramçılarla müzakirə etməyi nəzərdən keçiririk. Hal-hazırki ədalətimiz, təəssüf ki, hələ də yayımlanmağa hazır deyil, çünki biz yalnız unikal defer edilmiş məhdudiyyətlərlə bağlı problemi həll etdik, tam bir patch üçün digər tipləri də dəstəkləmək lazımdır. Ümid edirik ki, gələcəkdə bunu edə biləcəyik.

Bəlkə də, sizdə sual yaranıb ki, niyə biz pg_repack-in bu düzəlişinə qarışdıq, məsələn, onun analoqlarından istifadə etmədik? Bir vaxtlar biz də bunun haqqında düşünmüşdük, amma daha əvvəl defer edilmiş məhdudiyyətlər olmayan cədvəllərdə onun istifadə müddətindən müsbət təcrübə yaradaraq, problemi anlamağa və düzəltməyə çalışmaq üçün motivasiya tapdıq. Həmçinin, digər həllərin istifadəsi üçün testlər aparmaq üçün də vaxt tələb olunur, buna görə əvvəlcə problemin bu həllində düzəldilməsini sınamağa qərar verdik, və əgər bunu mülahizəli bir müddətdə etməyəcəyimizi başa düşsək, o zaman analoqları nəzərdən keçirəcəyik.

Sonuçlar

Öz təcrübəmizə əsaslanaraq nə tövsiyə edə bilərik:

  1. Bloatınızı izləyin. Monitorinq məlumatlarına əsaslanaraq, autovacuum-un nə qədər yaxşı tənzimləndiyini anlayacaqsınız.
  2. Bloat-u qəbul edilən səviyyədə saxlamaq üçün AUTOVACUUM-u tənzimləyin.
  3. Bloat hələ də artırsa və siz onu ‘out of the box’ vasitələrlə aradan qaldıra bilmirsinizsə, xarici genişləndirmələrdən istifadə etməkdən çəkinməyin. Əsas məsələ – hər şeyi yaxşı test etməkdir.
  4. Xarici həlləri öz ehtiyaclarınıza uyğun üzə çıxarmaqdan çəkinməyin – bəzən bu, sizin öz kodunuzu dəyişdirməkdən daha effektiv və hətta daha asan ola bilər.

Mənbə: habr.com

DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər 🔥 DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər | ProHoster