De mi van akkor, ha egy ilyen kellemetlen típus (hosszú távú OLAP terhelés egy OLTP adatbázison) még mindig létezik? Hogyan aktív pelenkázóasztal tisztítása hosszú lekérdezések veszik körül, és nem lépnek gereblyére?
A gereblye lerakása
Először is határozzuk meg, mi a megoldani kívánt probléma, és hogyan merülhet fel.
Általában ez a helyzet fordul elő egy viszonylag kis asztalon, hanem amelyben előfordul sok változás. Általában ez vagy más méter/aggregátum/besorolás, amelyen az UPDATE gyakran lefut, vagy puffer-sor valamilyen folyamatosan zajló eseményfolyam feldolgozására, amelyek rekordjai folyamatosan INSERT/DELETE.
Próbáljuk meg reprodukálni a lehetőséget értékelésekkel:
CREATE TABLE tbl(k text PRIMARY KEY, v integer);
CREATE INDEX ON tbl(v DESC); -- по этому индексу будем строить рейтинг
INSERT INTO
tbl
SELECT
chr(ascii('a'::text) + i) k
, 0 v
FROM
generate_series(0, 25) i;
És ezzel párhuzamosan egy másik összefüggésben elindul egy hosszú-hosszú kérés, némi összetett statisztika gyűjtése, de nem érinti az asztalunkat:
SELECT pg_sleep(10000);
Most az egyik számláló értékét sokszor-sokszor frissítjük. A kísérlet tisztasága érdekében tegyük ezt
DO $$
DECLARE
i integer;
tsb timestamp;
tse timestamp;
d double precision;
BEGIN
PERFORM dblink_connect('dbname=' || current_database() || ' port=' || current_setting('port'));
FOR i IN 1..10000 LOOP
tsb = clock_timestamp();
PERFORM dblink($e$UPDATE tbl SET v = v + 1 WHERE k = 'a';$e$);
tse = clock_timestamp();
IF i % 1000 = 0 THEN
d = (extract('epoch' from tse) - extract('epoch' from tsb)) * 1000;
RAISE NOTICE 'i = %, exectime = %', lpad(i::text, 5), lpad(d::text, 5);
END IF;
END LOOP;
PERFORM dblink_disconnect();
END;
$$ LANGUAGE plpgsql;
NOTICE: i = 1000, exectime = 0.524
NOTICE: i = 2000, exectime = 0.739
NOTICE: i = 3000, exectime = 1.188
NOTICE: i = 4000, exectime = 2.508
NOTICE: i = 5000, exectime = 1.791
NOTICE: i = 6000, exectime = 2.658
NOTICE: i = 7000, exectime = 2.318
NOTICE: i = 8000, exectime = 2.572
NOTICE: i = 9000, exectime = 2.929
NOTICE: i = 10000, exectime = 3.808
Mi történt? Miért akár egyetlen rekord legegyszerűbb FRISSÍTÉSÉRE is a végrehajtási idő 7-szeresére csökkent - 0.524 ms-ról 3.808 ms-ra? A minősítésünk pedig egyre lassabban épül fel.
Az egész az MVCC hibája.
Minden arról szól
VACUUM VERBOSE tbl;
INFO: vacuuming "public.tbl"
INFO: "tbl": found 0 removable, 10026 nonremovable row versions in 45 out of 45 pages
DETAIL: 10000 dead row versions cannot be removed yet, oldest xmin: 597439602
Ó, nincs mit takarítani! Párhuzamos A futó kérés zavar bennünket - elvégre lehet, hogy egyszer rá akar majd fordulni ezekre a verziókra (mi lenne, ha?), és ezek elérhetőek legyenek számára. És ezért még a VÁKUUM TELE sem segít rajtunk.
Az asztal „összecsukása”.
De biztosan tudjuk, hogy ennek a lekérdezésnek nincs szüksége a táblázatunkra. Ezért továbbra is igyekszünk a rendszer teljesítményét megfelelő korlátokra visszaállítani úgy, hogy minden feleslegeset eltávolítunk a táblázatból - legalábbis „manuálisan”, mivel a VACUUM feladja.
Hogy érthetőbb legyen, nézzük meg a puffertábla esetét. Vagyis nagy az INSERT/DELETE áramlása, és néha teljesen üres a tábla. De ha nem üres, akkor muszáj mentse el az aktuális tartalmát.
#0: A helyzet felmérése
Nyilvánvaló, hogy minden művelet után is meg lehet próbálni valamit csinálni a táblával, de ennek nincs sok értelme - a karbantartási többlet egyértelműen nagyobb lesz, mint a céllekérdezések átviteli sebessége.
Fogalmazzuk meg a kritériumokat – „ideje cselekedni”, ha:
- A VACUUM-ot elég régen indították el
Nagy terhelésre számítunk, úgyhogy legyen 60 másodperc az utolsó [auto]VÁKUUM óta. - A fizikai táblázat mérete nagyobb, mint a cél
Határozzuk meg, hogy a minimális mérethez képest kétszer annyi oldal (8 KB blokk) 1 blk a kupacért + 1 blk minden indexért - potenciálisan üres asztalhoz. Ha arra számítunk, hogy bizonyos mennyiségű adat mindig „normálisan” marad a pufferben, akkor indokolt ezt a képletet módosítani.
Ellenőrzési kérelem
SELECT
relpages
, ((
SELECT
count(*)
FROM
pg_index
WHERE
indrelid = cl.oid
) + 1) << 13 size_norm -- тут правильнее делать * current_setting('block_size')::bigint, но кто меняет размер блока?..
, pg_total_relation_size(oid) size
, coalesce(extract('epoch' from (now() - greatest(
pg_stat_get_last_vacuum_time(oid)
, pg_stat_get_last_autovacuum_time(oid)
))), 1 << 30) vaclag
FROM
pg_class cl
WHERE
oid = $1::regclass -- tbl
LIMIT 1;
relpages | size_norm | size | vaclag
-------------------------------------------
0 | 24576 | 1105920 | 3392.484835
#1: Még mindig VÁKUUM
Nem tudhatjuk előre, hogy egy párhuzamos lekérdezés jelentősen zavar-e bennünket – pontosan hány rekord vált „elavulttá” a kezdete óta. Ezért, ha úgy döntünk, hogy valahogy feldolgozzuk a táblát, mindenesetre először végre kell hajtanunk rajta VÁKUUM - ellentétben a VACUUM FULL-lal, nem zavarja az olvasási-írási adatokkal dolgozó párhuzamos folyamatokat.
Ugyanakkor azonnal eltávolíthatja a legtöbbet, amit el szeretnénk távolítani. Igen, és a táblázatra vonatkozó későbbi lekérdezések hozzánk fognak eljutni a "hot cache" által, ami lerövidíti azok időtartamát – és ezáltal a szolgáltatási tranzakciónk által mások letiltásának teljes idejét.
#2: Van itthon valaki?
Nézzük meg, hogy van-e egyáltalán valami a táblázatban:
TABLE tbl LIMIT 1;
Ha már egyetlen rekord sem maradt, akkor egyszerűen futással sokat spórolhatunk a feldolgozáson
Ugyanúgy működik, mint egy feltétel nélküli DELETE parancs minden táblához, de sokkal gyorsabb, mivel valójában nem vizsgálja meg a táblákat. Ráadásul azonnal lemezterületet szabadít fel, így nem kell utána VÁKUUM műveletet végrehajtani.
Ön dönti el, hogy vissza kell-e állítania a táblázat sorozatszámlálóját (ÚJRAINDÍTÁSI IDENTITÁS).
#3: Mindenki – felváltva!
Mivel erős versenykörnyezetben dolgozunk, miközben itt ellenőrizzük, hogy nincsenek-e bejegyzések a táblázatban, valaki már írhatott valamit. Nem szabad elveszítenünk ezeket az információkat, akkor mi van? Így van, ügyelnünk kell arra, hogy senki ne tudja biztosan leírni.
Ehhez engedélyeznünk kell SOROZHATÓ-izoláció a tranzakciónkhoz (igen, itt indítunk egy tranzakciót), és zárjuk le a táblát „szorosan”:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
Ezt a blokkolási szintet a rajta végrehajtani kívánt műveletek határozzák meg.
#4: Összeférhetetlenség
Idejövünk és le akarjuk zárni a táblát – mi lenne, ha valaki abban a pillanatban aktív lenne, például olvasott volna róla? „Akasztva” várjuk ennek a blokknak a megjelenését, és a többi olvasni vágyó is befut hozzánk...
Hogy ez ne történhessen meg, „feláldozzuk magunkat” – ha egy bizonyos (elfogadhatóan rövid) időn belül nem tudtunk zárat szerezni, akkor kivételt kapunk a bázistól, de legalább nem fogunk túlságosan beavatkozni. mások.
Ehhez állítsa be a session változót
SET statement_timeout = ...;LOCK TABLE ...;
Annak érdekében, hogy a későbbiekben ne kelljen a változó „régi” értékének visszaállításával foglalkoznunk, az űrlapot használjuk HELYI BEÁLLÍTÁS, amely a beállítás hatókörét az aktuális tranzakcióra korlátozza.
Emlékezzünk arra, hogy az utasítás_időtúllépése minden további kérésre vonatkozik, így a tranzakció nem nyúlhat elfogadhatatlan értékre, ha sok adat van a táblázatban.
#5: Adatok másolása
Ha a tábla nem teljesen üres, az adatokat egy ideiglenes segédtábla segítségével újra el kell menteni:
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
Aláírás ON COMMIT Drop azt jelenti, hogy abban a pillanatban, amikor a tranzakció véget ér, az ideiglenes tábla megszűnik, és nincs szükség manuális törlésére a kapcsolati környezetben.
Mivel feltételezzük, hogy nincs sok „élő” adat, ennek a műveletnek elég gyorsan meg kell történnie.
Nos, ez minden! Ne felejtse el a tranzakció befejezése után
A végső forgatókönyv összeállítása
Ezt a „pszeudo-pythont” használjuk:
# собираем статистику с таблицы
stat <-
SELECT
relpages
, ((
SELECT
count(*)
FROM
pg_index
WHERE
indrelid = cl.oid
) + 1) << 13 size_norm
, pg_total_relation_size(oid) size
, coalesce(extract('epoch' from (now() - greatest(
pg_stat_get_last_vacuum_time(oid)
, pg_stat_get_last_autovacuum_time(oid)
))), 1 << 30) vaclag
FROM
pg_class cl
WHERE
oid = $1::regclass -- table_name
LIMIT 1;
# таблица больше целевого размера и VACUUM был давно
if stat.size > 2 * stat.size_norm and stat.vaclag is None or stat.vaclag > 60:
-> VACUUM %table;
try:
-> BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# пытаемся захватить монопольную блокировку с предельным временем ожидания 1s
-> SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s';
-> LOCK TABLE %table IN ACCESS EXCLUSIVE MODE;
# надо убедиться в пустоте таблицы внутри транзакции с блокировкой
row <- TABLE %table LIMIT 1;
# если в таблице нет ни одной "живой" записи - очищаем ее полностью, в противном случае - "перевставляем" все записи через временную таблицу
if row is None:
-> TRUNCATE TABLE %table RESTART IDENTITY;
else:
# создаем временную таблицу с данными таблицы-оригинала
-> CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE %table;
# очищаем оригинал без сброса последовательности
-> TRUNCATE TABLE %table;
# вставляем все сохраненные во временной таблице данные обратно
-> INSERT INTO %table TABLE _tmp_swap;
-> COMMIT;
except Exception as e:
# если мы получили ошибку, но соединение все еще "живо" - словили таймаут
if not isinstance(e, InterfaceError):
-> ROLLBACK;
Lehetséges, hogy másodszor ne másolja át az adatokat?Elvileg lehetséges, ha maga a tábla oidja nincs más tevékenységhez kötve BL oldalról vagy FK DB oldalról:
CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;
Futtassuk a szkriptet a forrástáblán, és ellenőrizzük a mutatókat:
VACUUM tbl;
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s';
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
TRUNCATE TABLE tbl;
INSERT INTO tbl TABLE _tmp_swap;
COMMIT;
relpages | size_norm | size | vaclag
-------------------------------------------
0 | 24576 | 49152 | 32.705771
Minden sikerült! A táblázat 50-szeresére zsugorodott, és az UPDATE ismét gyorsan fut.
Forrás: will.com