Ali što ako takva neugodna vrsta (dugotrajno OLAP opterećenje OLTP baze podataka) još uvijek postoji? Kako čist aktivno prematanje stol okruženi dugim upitima i ne stati na grablje?
Polaganje grablja
Prvo, odredimo koji je problem koji želimo riješiti i kako može nastati.
Obično se ova situacija događa na relativno malom stolu, ali u kojem se javlja puno promjena. Obično ovo ili drugačije metara/agregata/ocjena, na kojem se često izvršava UPDATE, ili međuspremnik-red za obradu nekog stalno tekućeg toka događaja, čiji se zapisi stalno INSERT/DELETE.
Pokušajmo reproducirati opciju s ocjenama:
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;
I paralelno, u drugoj vezi, kreće dugi, dugi zahtjev, prikupljanje neke složene statistike, ali ne utječe na naš stol:
SELECT pg_sleep(10000);
Sada ažuriramo vrijednost jednog od brojača mnogo, mnogo puta. Radi čistoće eksperimenta, učinimo ovo
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
Što se dogodilo? Zašto čak i za najjednostavnije AŽURIRANJE jednog zapisa vrijeme izvršenja degradirano za 7 puta - od 0.524ms do 3.808ms? A naš rejting raste sve sporije.
Za sve je kriv MVCC.
Sve je o
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
Oh, nema se što čistiti! Paralelno Izvršeni zahtjev nas ometa - uostalom, možda će jednog dana poželjeti okrenuti se tim verzijama (što ako?), a trebale bi mu biti dostupne. I stoga nam ni VACUUM FULL neće pomoći.
“Urušavanje” stola
Ali pouzdano znamo da taj upit ne treba našu tablicu. Stoga ćemo ipak pokušati vratiti performanse sustava u adekvatne okvire eliminirajući sve nepotrebno sa stola - barem "ručno", jer VACUUM popušta.
Da bi bilo jasnije, pogledajmo primjer slučaja međuspremnika. Odnosno, postoji veliki protok INSERT/DELETE, a ponekad je tablica potpuno prazna. Ali ako nije prazno, moramo spremite njegov trenutni sadržaj.
#0: Procjena situacije
Jasno je da možete pokušati učiniti nešto s tablicom čak i nakon svake operacije, ali to nema previše smisla - troškovi održavanja očito će biti veći od propusnosti ciljanih upita.
Formulirajmo kriterij - "vrijeme je za djelovanje" ako:
- VACUUM je lansiran dosta davno
Očekujemo veliko opterećenje, pa neka bude 60 sekundi od zadnjeg [auto]VAKUUMA. - fizička veličina tablice je veća od ciljne
Definirajmo to kao dvostruki broj stranica (8KB blokova) u odnosu na minimalnu veličinu - 1 blk za gomilu + 1 blk za svaki indeks - za potencijalno prazan stol. Ako očekujemo da će određena količina podataka uvijek ostati u međuspremniku "normalno", razumno je prilagoditi ovu formulu.
Zahtjev za provjeru
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: Još uvijek USISAVAJ
Ne možemo unaprijed znati ometa li nas paralelni upit - koliko je točno zapisa postalo "zastarjelo" otkako je počelo. Stoga, kada odlučimo nekako obraditi tablicu, u svakom slučaju, prvo bismo trebali izvršiti na njoj VAKUUM - za razliku od VACUUM FULL, ne ometa paralelne procese koji rade s podacima za čitanje i pisanje.
Istodobno, može odmah očistiti većinu onoga što bismo htjeli ukloniti. Da, i sljedeći upiti o ovoj tablici ići će nama prema "hot cache", što će smanjiti njihovo trajanje - a time i ukupno vrijeme blokiranja drugih putem naše servisne transakcije.
#2: Ima li koga kod kuće?
Provjerimo ima li uopće nečega u tablici:
TABLE tbl LIMIT 1;
Ako ne preostane niti jedan jedini zapis, tada možemo puno uštedjeti na obradi jednostavnim izvođenjem
Djeluje isto kao i bezuvjetna naredba DELETE za svaku tablicu, ali je mnogo brža jer zapravo ne skenira tablice. Štoviše, odmah oslobađa prostor na disku, tako da nema potrebe za izvođenjem operacije VAKUUMIRANJA nakon toga.
Na vama je da odlučite trebate li resetirati brojač redoslijeda tablice (RESTART IDENTITY).
#3: Svi - redom!
Budući da radimo u jako kompetitivnom okruženju, dok mi ovdje provjeravamo da nema unosa u tablici, netko je već mogao tamo nešto napisati. Ne bismo trebali izgubiti ove informacije, pa što? Tako je, moramo se pobrinuti da to nitko ne može sigurno zapisati.
Da bismo to učinili, moramo omogućiti SERIJALIZIVO-izolacija za našu transakciju (da, ovdje započinjemo transakciju) i "čvrsto" zaključati tablicu:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
Ova razina blokiranja određena je operacijama koje želimo izvesti na njemu.
#4: Sukob interesa
Dođemo ovdje i želimo “zaključati” znak – što ako je netko u tom trenutku bio aktivan na njemu, na primjer, čitao s njega? Mi ćemo “visjeti” čekajući da ovaj blok bude pušten, a drugi koji žele čitati će nam naletjeti...
Da se to ne dogodi, "žrtvovat ćemo se" - ako nismo uspjeli dobiti zaključavanje u određenom (prihvatljivo kratkom) vremenu, tada ćemo dobiti iznimku iz baze, ali se barem nećemo previše miješati u drugi.
Da biste to učinili, postavite varijablu sesije
SET statement_timeout = ...;LOCK TABLE ...;
Kako se kasnije ne bismo morali baviti vraćanjem “stare” vrijednosti varijable, koristimo obrazac POSTAVITE LOKALNO, što ograničava opseg postavke na trenutnu transakciju.
Sjećamo se da se statement_timeout primjenjuje na sve sljedeće zahtjeve tako da se transakcija ne može rastegnuti do neprihvatljivih vrijednosti ako u tablici ima puno podataka.
#5: Kopirajte podatke
Ako tablica nije potpuno prazna, podaci će se morati ponovno spremiti pomoću pomoćne privremene tablice:
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
Potpis ON COMMIT DROP znači da će u trenutku završetka transakcije privremena tablica prestati postojati i nema potrebe da je ručno brišete u kontekstu veze.
Budući da pretpostavljamo da nema puno "živih" podataka, ova bi se operacija trebala odvijati prilično brzo.
Pa, to je sve! Ne zaboravite nakon dovršetka transakcije
Sastavljanje konačnog scenarija
Koristimo ovaj "pseudo-python":
# собираем статистику с таблицы
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;
Je li moguće ne kopirati podatke drugi put?U principu, moguće je ako oid same tablice nije vezan ni za jednu drugu aktivnost sa strane BL ili FK sa strane DB:
CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;
Pokrenimo skriptu na izvornoj tablici i provjerimo metriku:
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
Sve je uspjelo! Tablica se smanjila 50 puta i svi UPDATE-i opet rade brzo.
Izvor: www.habr.com