Men hvad hvis en sådan ubehagelig type (langvarig OLAP-belastning på en OLTP-database) stadig eksisterer? Hvordan rent aktivt puslebord omgivet af lange forespørgsler og ikke træde på en rake?
Udfoldning af riven
Lad os først finde ud af, hvad problemet er, vi ønsker at løse, og hvordan det kan opstå.
Normalt sker denne situation på et relativt lille bord, men hvor det forekommer en masse ændringer. Normalt dette eller anderledes målere/aggregater/vurderinger, hvor UPDATE ofte udføres, eller buffer-kø at behandle en eller anden konstant igangværende strøm af hændelser, hvoraf optegnelser konstant er INSERT/DELETE.
Lad os prøve at gengive muligheden med vurderinger:
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;
Og sideløbende, i en anden forbindelse, starter en lang, lang anmodning, der samler nogle komplekse statistikker, men ikke påvirker vores bord:
SELECT pg_sleep(10000);
Nu opdaterer vi værdien af en af tællerne mange, mange gange. For renheden af eksperimentet, lad os gøre dette
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
Hvad skete der? Hvorfor endda for den enkleste OPDATERING af en enkelt post udførelsestiden forringet med 7 gange — fra 0.524 ms til 3.808 ms? Og vores rating bygger langsommere og langsommere.
Det hele er MVCC's skyld.
Det handler om
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
Åh, der er ikke noget at gøre rent! Parallel Den kørende anmodning forstyrrer os - han kan jo en dag have lyst til at vende sig til disse versioner (hvad nu hvis?), og de burde være tilgængelige for ham. Og derfor vil selv VACUUM FULL ikke hjælpe os.
"Skjuler sammen" bordet
Men vi ved med sikkerhed, at den forespørgsel ikke har brug for vores tabel. Derfor vil vi stadig forsøge at bringe systemets ydeevne tilbage til passende grænser ved at fjerne alt unødvendigt fra tabellen - i det mindste "manuelt", da VACUUM giver efter.
For at gøre det mere klart, lad os se på eksemplet med tilfældet med en buffertabel. Det vil sige, at der er et stort flow af INSERT/DELETE, og nogle gange er bordet helt tomt. Men hvis det ikke er tomt, må vi det gemme dets nuværende indhold.
#0: Vurderer situationen
Det er klart, at du kan prøve at gøre noget med tabellen selv efter hver operation, men det giver ikke meget mening - vedligeholdelsesomkostningerne vil klart være større end gennemløbet af målforespørgslerne.
Lad os formulere kriterierne - "det er tid til at handle", hvis:
- VACUUM blev lanceret for ganske lang tid siden
Vi forventer et tungt læs, så lad det være 60 sekunder siden sidste [auto]VAKUUM. - fysisk bordstørrelse er større end målet
Lad os definere det som dobbelt så mange sider (8KB blokke) i forhold til minimumsstørrelsen - 1 blk for bunke + 1 blk for hvert indeks - for et potentielt tomt bord. Hvis vi forventer, at en vis mængde data altid vil forblive i bufferen "normalt", er det rimeligt at justere denne formel.
Bekræftelsesanmodning
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: Stadig VAKUUM
Vi kan ikke på forhånd vide, om en parallel forespørgsel i væsentlig grad forstyrrer os - præcis hvor mange poster, der er blevet "forældede", siden den begyndte. Derfor, når vi beslutter os for at behandle tabellen på en eller anden måde, skal vi under alle omstændigheder først udføre på den VACUUM - i modsætning til VACUUM FULL, forstyrrer det ikke parallelle processer, der arbejder med læse-skrive-data.
Samtidig kan den med det samme rense det meste af det, vi gerne vil fjerne. Ja, og efterfølgende forespørgsler på denne tabel vil gå til os af "hot cache", hvilket vil reducere deres varighed - og dermed den samlede tid for blokering af andre ved vores servicetransaktion.
#2: Er der nogen hjemme?
Lad os tjekke, om der overhovedet er noget i tabellen:
TABLE tbl LIMIT 1;
Hvis der ikke er en eneste post tilbage, så kan vi spare meget på behandlingen ved blot at gøre det
Den fungerer på samme måde som en ubetinget DELETE-kommando for hver tabel, men er meget hurtigere, da den faktisk ikke scanner tabellerne. Desuden frigør det øjeblikkeligt diskplads, så der er ingen grund til at udføre en VACUUM operation bagefter.
Om du skal nulstille tabelsekvenstælleren (GENSTART IDENTITET) er op til dig at afgøre.
#3: Alle – skiftes til!
Da vi arbejder i et stærkt konkurrencepræget miljø, mens vi er her og tjekker, at der ikke er nogen poster i tabellen, kunne nogen allerede have skrevet noget der. Vi bør ikke miste disse oplysninger, hvad så? Det er rigtigt, vi skal sørge for, at ingen kan skrive det ned med sikkerhed.
For at gøre dette skal vi aktivere SERIALISERbar-isolation for vores transaktion (ja, her starter vi en transaktion) og lås bordet "tæt":
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
Dette blokeringsniveau bestemmes af de operationer, vi ønsker at udføre på det.
#4: Interessekonflikt
Vi kommer her og vil "låse" skiltet - hvad nu hvis nogen var aktive på det i det øjeblik, for eksempel læste fra det? Vi vil "hænge" og vente på, at denne blok bliver frigivet, og andre, der vil læse, vil løbe ind i os...
For at forhindre dette i at ske, vil vi "ofre os selv" - hvis vi ikke var i stand til at opnå en lås inden for en vis (acceptabel kort) tid, så vil vi modtage en undtagelse fra basen, men vi vil i det mindste ikke blande os for meget med andre.
For at gøre dette skal du indstille sessionsvariablen
SET statement_timeout = ...;LOCK TABLE ...;
For ikke at skulle beskæftige os med at genoprette den "gamle" værdi af variablen senere, bruger vi formularen SÆT LOKAL, hvilket begrænser omfanget af indstillingen til den aktuelle transaktion.
Vi husker, at statement_timeout gælder for alle efterfølgende anmodninger, så transaktionen ikke kan strække sig til uacceptable værdier, hvis der er mange data i tabellen.
#5: Kopier data
Hvis tabellen ikke er helt tom, skal dataene gemmes igen ved hjælp af en midlertidig hjælpetabel:
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
Underskrift PÅ KOMMIT DROP betyder, at i det øjeblik transaktionen slutter, vil den midlertidige tabel ophøre med at eksistere, og der er ingen grund til manuelt at slette den i forbindelseskonteksten.
Da vi antager, at der ikke er mange "live" data, bør denne operation foregå ret hurtigt.
Nå, det er alt! Glem ikke efter at have gennemført transaktionen
Sammensætning af det endelige manuskript
Vi bruger denne "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;
Er det muligt ikke at kopiere dataene en anden gang?I princippet er det muligt, hvis selve bordets oid ikke er bundet til andre aktiviteter fra BL-siden eller FK fra DB-siden:
CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;
Lad os køre scriptet på kildetabellen og tjekke metrics:
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
Alt fungerede! Tabellen er skrumpet med 50 gange, og alle OPDATERINGER kører hurtigt igen.
Kilde: www.habr.com