VACUUM విఫలమైనప్పుడు, మేము పట్టికను మానవీయంగా శుభ్రం చేస్తాము

వాక్యూమ్ PostgreSQLలోని పట్టిక నుండి "క్లీన్ అప్" చేయగలదు ఎవరూ చూడలేరు - అంటే, ఈ రికార్డ్‌లను మార్చడానికి ముందు ప్రారంభించిన ఒక్క క్రియాశీల అభ్యర్థన కూడా లేదు.

అయితే అటువంటి అసహ్యకరమైన రకం (OLTP డేటాబేస్‌లో దీర్ఘకాలిక OLAP లోడ్) ఇప్పటికీ ఉంటే? ఎలా చురుకుగా మారుతున్న పట్టికను శుభ్రం చేయండి సుదీర్ఘ ప్రశ్నలతో చుట్టుముట్టబడి, రేక్‌పై అడుగు పెట్టలేదా?

VACUUM విఫలమైనప్పుడు, మేము పట్టికను మానవీయంగా శుభ్రం చేస్తాము

రేక్ విప్పడం

మొదట, మనం పరిష్కరించాలనుకుంటున్న సమస్య ఏమిటి మరియు అది ఎలా ఉత్పన్నం కాగలదో తెలుసుకుందాం.

సాధారణంగా ఈ పరిస్థితి ఏర్పడుతుంది సాపేక్షంగా చిన్న టేబుల్ మీద, కానీ ఇది సంభవిస్తుంది చాలా మార్పులు. సాధారణంగా ఇది లేదా భిన్నంగా ఉంటుంది మీటర్లు/మొత్తం/రేటింగ్‌లు, దీనిలో UPDATE తరచుగా అమలు చేయబడుతుంది లేదా బఫర్-క్యూ కొన్ని నిరంతరం కొనసాగుతున్న ఈవెంట్‌లను ప్రాసెస్ చేయడానికి, వాటి రికార్డులు నిరంతరం ఇన్‌సర్ట్/తొలగించబడతాయి.

రేటింగ్‌లతో ఎంపికను పునరుత్పత్తి చేయడానికి ప్రయత్నిద్దాం:

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;

మరియు సమాంతరంగా, మరొక కనెక్షన్‌లో, సుదీర్ఘమైన, సుదీర్ఘమైన అభ్యర్థన ప్రారంభమవుతుంది, కొన్ని సంక్లిష్ట గణాంకాలను సేకరిస్తుంది, కానీ మా పట్టికను ప్రభావితం చేయదు:

SELECT pg_sleep(10000);

ఇప్పుడు మనం కౌంటర్లలో ఒకదాని విలువను చాలా సార్లు అప్‌డేట్ చేస్తాము. ప్రయోగం యొక్క స్వచ్ఛత కోసం, దీన్ని చేద్దాం dblink ఉపయోగించి ప్రత్యేక లావాదేవీలలోవాస్తవానికి ఇది ఎలా జరుగుతుంది:

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

ఏం జరిగింది? ఒకే రికార్డ్ యొక్క అత్యంత సరళమైన నవీకరణ కోసం కూడా ఎందుకు అమలు సమయం 7 రెట్లు క్షీణించింది — 0.524ms నుండి 3.808ms వరకు? మరియు మా రేటింగ్ మరింత నెమ్మదిగా పెరుగుతోంది.

ఇదంతా MVCC తప్పు.

ఇది అన్ని గురించి MVCC మెకానిజం, ఇది ఎంట్రీ యొక్క అన్ని మునుపటి సంస్కరణల ద్వారా ప్రశ్నను చూసేలా చేస్తుంది. కాబట్టి మన టేబుల్‌ను “డెడ్” వెర్షన్‌ల నుండి శుభ్రం చేద్దాం:

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

ఓహ్, శుభ్రం చేయడానికి ఏమీ లేదు! సమాంతరంగా నడుస్తున్న అభ్యర్థన మాకు అంతరాయం కలిగిస్తోంది - అన్నింటికంటే, అతను ఏదో ఒక రోజు ఈ సంస్కరణల వైపు తిరగాలనుకోవచ్చు (ఏమైతే?), మరియు అవి అతనికి అందుబాటులో ఉండాలి. అందువల్ల VACUUM FULL కూడా మాకు సహాయం చేయదు.

టేబుల్ "కుప్పకూలుతోంది"

కానీ ఆ ప్రశ్నకు మా పట్టిక అవసరం లేదని మాకు ఖచ్చితంగా తెలుసు. అందువల్ల, మేము ఇప్పటికీ టేబుల్ నుండి అనవసరమైన ప్రతిదాన్ని తొలగించడం ద్వారా సిస్టమ్ పనితీరును తగిన పరిమితులకు తిరిగి ఇవ్వడానికి ప్రయత్నిస్తాము - కనీసం “మాన్యువల్‌గా”, ఎందుకంటే VACUUM ఇస్తుంది.

దీన్ని మరింత స్పష్టం చేయడానికి, బఫర్ టేబుల్ యొక్క ఉదాహరణను చూద్దాం. అంటే, INSERT/DELETE యొక్క పెద్ద ప్రవాహం ఉంది మరియు కొన్నిసార్లు పట్టిక పూర్తిగా ఖాళీగా ఉంటుంది. కానీ అది ఖాళీగా లేకపోతే, మనం తప్పక దాని ప్రస్తుత కంటెంట్‌లను సేవ్ చేయండి.

#0: పరిస్థితిని అంచనా వేయడం

ప్రతి ఆపరేషన్ తర్వాత కూడా మీరు టేబుల్‌తో ఏదైనా చేయడానికి ప్రయత్నించవచ్చని స్పష్టంగా ఉంది, కానీ ఇది చాలా అర్ధవంతం కాదు - నిర్వహణ ఓవర్‌హెడ్ లక్ష్య ప్రశ్నల నిర్గమాంశ కంటే స్పష్టంగా ఉంటుంది.

ప్రమాణాలను రూపొందిద్దాం - "ఇది పని చేయడానికి సమయం" అయితే:

  • వాక్యూమ్ చాలా కాలం క్రితం ప్రారంభించబడింది
    మేము భారీ భారాన్ని ఆశిస్తున్నాము, కాబట్టి అది ఉండనివ్వండి 20 సెకన్లు చివరి [ఆటో]VACUUM నుండి.
  • భౌతిక పట్టిక పరిమాణం లక్ష్యం కంటే పెద్దది
    కనిష్ట పరిమాణానికి సంబంధించి రెండు రెట్లు పేజీల సంఖ్య (8KB బ్లాక్‌లు)గా నిర్వచిద్దాం - హీప్ కోసం 1 blk + ప్రతి సూచికకు 1 blk - సంభావ్య ఖాళీ పట్టిక కోసం. "సాధారణంగా" బఫర్‌లో నిర్దిష్ట మొత్తంలో డేటా ఎల్లప్పుడూ ఉంటుందని మేము ఆశించినట్లయితే, ఈ సూత్రాన్ని సర్దుబాటు చేయడం సహేతుకమైనది.

ధృవీకరణ అభ్యర్థన

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: ఇప్పటికీ వాక్యూమ్

సమాంతర ప్రశ్న మాతో గణనీయంగా జోక్యం చేసుకుంటుందో లేదో మేము ముందుగానే తెలుసుకోలేము - ఇది ప్రారంభమైనప్పటి నుండి ఎన్ని రికార్డులు "ముగింపు" అయ్యాయి. అందువల్ల, మేము పట్టికను ఎలాగైనా ప్రాసెస్ చేయాలని నిర్ణయించుకున్నప్పుడు, ఏ సందర్భంలోనైనా, మేము మొదట దానిపై అమలు చేయాలి వాక్యూమ్ - VACUUM FULL కాకుండా, ఇది రీడ్-రైట్ డేటాతో పనిచేసే సమాంతర ప్రక్రియలకు అంతరాయం కలిగించదు.

అదే సమయంలో, మనం తీసివేయాలనుకుంటున్న వాటిలో చాలా వరకు అది వెంటనే శుభ్రం చేయగలదు. అవును, మరియు ఈ పట్టికలోని తదుపరి ప్రశ్నలు మాకు వెళ్తాయి "హాట్ కాష్" ద్వారా, ఇది వారి వ్యవధిని తగ్గిస్తుంది - మరియు, మా సర్వీసింగ్ లావాదేవీ ద్వారా ఇతరులను నిరోధించే మొత్తం సమయం.

#2: ఇంట్లో ఎవరైనా ఉన్నారా?

పట్టికలో ఏదైనా ఉందో లేదో చూద్దాం:

TABLE tbl LIMIT 1;

ఒక్క రికార్డ్ కూడా మిగిలి ఉండకపోతే, కేవలం చేయడం ద్వారా ప్రాసెసింగ్‌లో మనం చాలా ఆదా చేసుకోవచ్చు కత్తిరించండి:

ఇది ప్రతి టేబుల్‌కి షరతులు లేని DELETE కమాండ్ వలె పనిచేస్తుంది, అయితే ఇది నిజానికి పట్టికలను స్కాన్ చేయనందున ఇది చాలా వేగంగా ఉంటుంది. అంతేకాకుండా, ఇది వెంటనే డిస్క్ స్థలాన్ని ఖాళీ చేస్తుంది, కాబట్టి తర్వాత VACUUM ఆపరేషన్ చేయవలసిన అవసరం లేదు.

మీరు టేబుల్ సీక్వెన్స్ కౌంటర్ (RESTART IDENTITY)ని రీసెట్ చేయాలా వద్దా అనేది మీరు నిర్ణయించుకోవాలి.

#3: అందరూ - వంతులు తీసుకోండి!

మేము చాలా పోటీ వాతావరణంలో పని చేస్తున్నందున, మేము ఇక్కడ పట్టికలో నమోదులు లేవని తనిఖీ చేస్తున్నప్పుడు, ఎవరైనా ఇప్పటికే అక్కడ ఏదైనా వ్రాసి ఉండవచ్చు. మేము ఈ సమాచారాన్ని కోల్పోకూడదు, కాబట్టి ఏమిటి? అది నిజం, ఎవరూ ఖచ్చితంగా వ్రాయలేరని మనం నిర్ధారించుకోవాలి.

దీన్ని చేయడానికి మనం ఎనేబుల్ చేయాలి క్రమీకరించదగినది-మా లావాదేవీ కోసం ఐసోలేషన్ (అవును, ఇక్కడ మేము లావాదేవీని ప్రారంభిస్తాము) మరియు పట్టికను "పటిష్టంగా" లాక్ చేయండి:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;

ఈ స్థాయి నిరోధించడాన్ని మనం నిర్వహించాలనుకుంటున్న కార్యకలాపాల ద్వారా నిర్ణయించబడుతుంది.

#4: ఆసక్తి సంఘర్షణ

మేము ఇక్కడకు వచ్చి గుర్తును "లాక్" చేయాలనుకుంటున్నాము - ఆ సమయంలో ఎవరైనా దానిపై చురుకుగా ఉంటే, ఉదాహరణకు, దాని నుండి చదవడం ఏమిటి? ఈ బ్లాక్ విడుదలయ్యే వరకు మేము "హాంగ్" చేస్తాము మరియు చదవాలనుకునే ఇతరులు మాలోకి ప్రవేశిస్తారు...

ఇది జరగకుండా నిరోధించడానికి, మేము "మనల్ని మనం త్యాగం చేస్తాము" - మేము ఒక నిర్దిష్ట (ఆమోదయోగ్యమైన తక్కువ) సమయంలో లాక్‌ని పొందలేకపోతే, అప్పుడు మేము బేస్ నుండి మినహాయింపును అందుకుంటాము, కానీ కనీసం మేము ఎక్కువగా జోక్యం చేసుకోము. ఇతరులు.

దీన్ని చేయడానికి, సెషన్ వేరియబుల్‌ను సెట్ చేయండి లాక్_టైమ్ అవుట్ (వెర్షన్లు 9.3+ కోసం) లేదా/మరియు ప్రకటన_సమయం ముగిసింది. గుర్తుంచుకోవలసిన ప్రధాన విషయం ఏమిటంటే, స్టేట్‌మెంట్_టైమ్ అవుట్ విలువ తదుపరి స్టేట్‌మెంట్ నుండి మాత్రమే వర్తిస్తుంది. అంటే, అంటుకోవడంలో ఇలా - పని చేయదు:

SET statement_timeout = ...;LOCK TABLE ...;

వేరియబుల్ యొక్క "పాత" విలువను తరువాత పునరుద్ధరించకుండా వ్యవహరించడానికి, మేము ఫారమ్‌ను ఉపయోగిస్తాము స్థానికంగా సెట్ చేయండి, ఇది సెట్టింగ్ పరిధిని ప్రస్తుత లావాదేవీకి పరిమితం చేస్తుంది.

స్టేట్‌మెంట్_టైమ్‌అవుట్ అన్ని తదుపరి అభ్యర్థనలకు వర్తిస్తుందని మేము గుర్తుంచుకోవాలి, తద్వారా టేబుల్‌లో చాలా డేటా ఉంటే లావాదేవీ ఆమోదయోగ్యం కాని విలువలకు సాగదు.

#5: డేటాను కాపీ చేయండి

పట్టిక పూర్తిగా ఖాళీగా లేకుంటే, సహాయక తాత్కాలిక పట్టికను ఉపయోగించి డేటా మళ్లీ సేవ్ చేయబడాలి:

CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;

సంతకం కమిట్ డ్రాప్‌లో లావాదేవీ ముగిసే సమయంలో, తాత్కాలిక పట్టిక ఉనికిలో ఉండదు మరియు కనెక్షన్ సందర్భంలో దాన్ని మాన్యువల్‌గా తొలగించాల్సిన అవసరం లేదు.

"ప్రత్యక్ష" డేటా చాలా లేదని మేము భావించాము కాబట్టి, ఈ ఆపరేషన్ చాలా త్వరగా జరగాలి.

బాగా, అంతే! లావాదేవీని పూర్తి చేసిన తర్వాత మర్చిపోవద్దు విశ్లేషణను అమలు చేయండి అవసరమైతే పట్టిక గణాంకాలను సాధారణీకరించడానికి.

ఫైనల్ స్క్రిప్ట్‌ను సిద్ధం చేస్తోంది

మేము ఈ "సూడో-పైథాన్"ని ఉపయోగిస్తాము:

# собираем статистику с таблицы
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;

డేటాను రెండోసారి కాపీ చేయకుండా ఉండవచ్చా?సూత్రప్రాయంగా, టేబుల్ యొక్క oid BL వైపు నుండి లేదా DB వైపు నుండి FK నుండి ఏదైనా ఇతర కార్యకలాపాలతో ముడిపడి ఉండకపోతే ఇది సాధ్యమవుతుంది:

CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;

సోర్స్ టేబుల్‌పై స్క్రిప్ట్‌ని రన్ చేద్దాం మరియు కొలమానాలను తనిఖీ చేద్దాం:

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

అంతా పని చేసింది! పట్టిక 50 రెట్లు తగ్గిపోయింది మరియు అన్ని అప్‌డేట్‌లు మళ్లీ వేగంగా అమలవుతున్నాయి.

మూలం: www.habr.com

ఒక వ్యాఖ్యను జోడించండి