sql >> Databáze >  >> RDS >> PostgreSQL

Read Committed je nutností pro distribuované databáze SQL kompatibilní s Postgres

V SQL databázích jsou úrovně izolace hierarchií prevence aktualizací anomálií. Lidé si pak myslí, že čím vyšší, tím lepší, a že když databáze poskytuje serializovatelný, není potřeba zadávat čtení. Nicméně:

  • Read Committed je výchozí nastavení v PostgreSQL . Důsledkem je, že jej používá většina aplikací (a pomocí SELECT ... FOR UPDATE), aby se předešlo některým anomáliím
  • Serializovatelné neškáluje s pesimistickým zamykáním. Distribuované databáze používají optimistické zamykání a musíte kódovat jejich logiku opakování transakce

S těmito dvěma nemůže distribuovaná databáze SQL, která neposkytuje izolaci Read Committed, nárokovat kompatibilitu s PostgreSQL, protože spouštění aplikací, které byly vytvořeny pro výchozí nastavení PostgreSQL, je nemožné.

YugabyteDB začal s myšlenkou "čím vyšší, tím lepší" a Read Committed transparentně používá "Snapshot Isolation". To je správné pro nové aplikace. Při migraci aplikací vytvořených pro Read Committed, kde nechcete implementovat logiku opakování při serializovatelných selháních (SQLState 40001), a očekáváte, že to databáze udělá za vás. Můžete přepnout na Read Committed pomocí **yb_enable_read_committed_isolation** gflag.

Poznámka:GFlag v YugabyteDB je globální konfigurační parametr pro databázi, zdokumentovaný v odkazu yb-tserver. Parametry PostgreSQL, které lze nastavit pomocí ysql_pg_conf_csv GFlag se týká pouze YSQL API, ale GFlags pokrývá všechny vrstvy YugabyteDB

V tomto příspěvku na blogu ukážu skutečnou hodnotu úrovně izolace Read Committed:není není potřeba kódovat logiku opakování protože na této úrovni to YugabyteDB dokáže sám.

Spusťte YugabyteDB

Spouštím databázi jednoho uzlu YugabyteDB pro toto jednoduché demo:

Franck@YB:~ $ docker  run --rm -d --name yb       \
 -p7000:7000 -p9000:9000 -p5433:5433 -p9042:9042  \
 yugabytedb/yugabyte                              \
 bin/yugabyted start --daemon=false               \
 --tserver_flags=""

53cac7952500a6e264e6922fe884bc47085bcac75e36a9ddda7b8469651e974c

Explicitně jsem nenastavil žádné GFlagy, které by ukazovaly výchozí chování. Toto je version 2.13.0.0 build 42 .

Kontroluji přečtené související gflagy

Franck@YB:~ $ curl -s http://localhost:9000/varz?raw | grep -E "\
(yb_enable_read_committed_isolation\
|ysql_output_buffer_size\
|ysql_sleep_before_retry_on_txn_conflict\
|ysql_max_write_restart_attempts\
|ysql_default_transaction_isolation\
)"

--yb_enable_read_committed_isolation=false
--ysql_max_write_restart_attempts=20
--ysql_output_buffer_size=262144
--ysql_sleep_before_retry_on_txn_conflict=true
--ysql_default_transaction_isolation=

Read Committed je výchozí úroveň izolace podle kompatibility PostgreSQL:

Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"

 default_transaction_isolation
-------------------------------
 read committed
(1 row)

Vytvořím jednoduchou tabulku:

Franck@YB:~ $ psql -p 5433 -ec "
create table demo (id int primary key, val int);
insert into demo select generate_series(1,100000),0;
"

create table demo (id int primary key, val int);
insert into demo select generate_series(1,100000),0;

INSERT 0 100000

Spustím následující aktualizaci a nastavím výchozí úroveň izolace na Read Committed (jen pro případ - ale je to výchozí):

Franck@YB:~ $ cat > update1.sql <<'SQL'
\timing on
\set VERBOSITY verbose
set default_transaction_isolation to "read committed";
update demo set val=val+1 where id=1;
\watch 0.1
SQL

Tím se aktualizuje jeden řádek.
Spustím to z více relací na stejném řádku:

Franck@YB:~ $ timeout 60 psql -p 5433 -ef update1.sql >session1.txt &
Franck@YB:~ $ timeout 60 psql -p 5433 -ef update1.sql >session2.txt &
[1] 760
[2] 761

psql:update1.sql:5: ERROR:  40001: Operation expired: Transaction a83718c8-c8cb-4e64-ab54-3afe4f2073bc expired or aborted by a conflict: 40001
LOCATION:  HandleYBStatusAtErrorLevel, pg_yb_utils.c:405

[1]-  Done                    timeout 60 psql -p 5433 -ef update1.sql > session1.txt

Franck@YB:~ $ wait

[2]+  Exit 124                timeout 60 psql -p 5433 -ef update1.sql > session1.txt

Při relaci došlo k Transaction ... expired or aborted by a conflict . Pokud totéž spustíte několikrát, může se vám také zobrazit Operation expired: Transaction aborted: kAborted , All transparent retries exhausted. Query error: Restart read required nebo All transparent retries exhausted. Operation failed. Try again: Value write after transaction start . Všechny jsou ERROR 40001, což jsou chyby serializace, které očekávají, že se aplikace znovu pokusí.

V Serializable se musí celá transakce opakovat a to obecně není možné transparentně provést databází, která neví, co dalšího aplikace během transakce udělala. Například některé řádky již mohly být přečteny a odeslány na uživatelskou obrazovku nebo do souboru. Databáze to nemůže vrátit zpět. Aplikace to musí zvládnout.

Nastavil jsem \Timing on získat uplynulý čas, a protože to spouštím na svém notebooku, není zde žádný významný čas sítě klient-server:

Franck@YB:~ $ awk '/Time/{print 5*int($2/5)}' session?.txt | sort -n | uniq -c

    121 0
     44 5
     45 10
     12 15
      1 20
      1 25
      2 30
      1 35
      3 105
      2 110
      3 115
      1 120

Většina aktualizací zde byla kratší než 5 milisekund. Pamatujte však, že program selhal na 40001 rychle, takže na mém notebooku je to běžné jedno sezení.

Ve výchozím nastavení yb_enable_read_committed_isolation je nepravda a v tomto případě úroveň izolace Read Committed transakční vrstvy YugabyteDB spadá zpět na přísnější Snapshot Isolation (v takovém případě READ COMMITTED a READ UNCOMMITTED z YSQL používají Snapshot Isolation).

yb_enable_read_committed_isolation=true

Nyní změňte toto nastavení, což byste měli udělat, když chcete být kompatibilní s vaší aplikací PostgreSQL, která neimplementuje žádnou logiku opakování.

Franck@YB:~ $ docker rm -f yb

yb
[1]+  Exit 124                timeout 60 psql -p 5433 -ef update1.sql > session1.txt

Franck@YB:~ $ docker  run --rm -d --name yb       \
 -p7000:7000 -p9000:9000 -p5433:5433 -p9042:9042  \
 yugabytedb/yugabyte                \
 bin/yugabyted start --daemon=false               \
 --tserver_flags="yb_enable_read_committed_isolation=true"

fe3e84c995c440d1a341b2ab087510d25ba31a0526859f08a931df40bea43747

Franck@YB:~ $ curl -s http://localhost:9000/varz?raw | grep -E "\
(yb_enable_read_committed_isolation\
|ysql_output_buffer_size\
|ysql_sleep_before_retry_on_txn_conflict\
|ysql_max_write_restart_attempts\
|ysql_default_transaction_isolation\
)"

--yb_enable_read_committed_isolation=true
--ysql_max_write_restart_attempts=20
--ysql_output_buffer_size=262144
--ysql_sleep_before_retry_on_txn_conflict=true
--ysql_default_transaction_isolation=

Běží stejně jako výše:

Franck@YB:~ $ psql -p 5433 -ec "
create table demo (id int primary key, val int);
insert into demo select generate_series(1,100000),0;
"

create table demo (id int primary key, val int);
insert into demo select generate_series(1,100000),0;

INSERT 0 100000

Franck@YB:~ $ timeout 60 psql -p 5433 -ef update1.sql >session1.txt &
Franck@YB:~ $ timeout 60 psql -p 5433 -ef update1.sql >session2.txt &
[1] 1032
[2] 1034

Franck@YB:~ $ wait

[1]-  Exit 124                timeout 60 psql -p 5433 -ef update1.sql > session1.txt
[2]+  Exit 124                timeout 60 psql -p 5433 -ef update1.sql > session2.txt

Nezaznamenal jsem vůbec žádnou chybu a obě relace aktualizovaly stejný řádek během 60 sekund.

Samozřejmě to nebylo přesně ve stejnou dobu, kdy databáze musela opakovat mnoho transakcí, což je vidět v uplynulém čase:

Franck@YB:~ $ awk '/Time/{print 5*int($2/5)}' session?.txt | sort -n | uniq -c

    325 0
    199 5
    208 10
     39 15
     11 20
      3 25
      1 50
     34 105
     40 110
     37 115
     13 120
      5 125
      3 130

Zatímco většina transakcí stále trvá méně než 10 milisekund, některé z důvodu opakování až 120 milisekund.

opakovat stažení

Běžné opakování čeká exponenciální množství času mezi každým opakováním, až do maxima. Toto je implementováno v YugabyteDB a řídí to 3 následující parametry, které lze nastavit na úrovni relace:

Franck@YB:~ $ psql -p 5433 -xec "
select name, setting, unit, category, short_desc
from pg_settings
where name like '%retry%backoff%';
"

select name, setting, unit, category, short_desc
from pg_settings
where name like '%retry%backoff%';

-[ RECORD 1 ]---------------------------------------------------------
name       | retry_backoff_multiplier
setting    | 2
unit       |
category   | Client Connection Defaults / Statement Behavior
short_desc | Sets the multiplier used to calculate the retry backoff.
-[ RECORD 2 ]---------------------------------------------------------
name       | retry_max_backoff
setting    | 1000
unit       | ms
category   | Client Connection Defaults / Statement Behavior
short_desc | Sets the maximum backoff in milliseconds between retries.
-[ RECORD 3 ]---------------------------------------------------------
name       | retry_min_backoff
setting    | 100
unit       | ms
category   | Client Connection Defaults / Statement Behavior
short_desc | Sets the minimum backoff in milliseconds between retries.

S mojí lokální databází jsou transakce krátké a nemusím tak dlouho čekat. Při přidávání set retry_min_backoff to 10; na můj update1.sql uplynulý čas není touto logikou opakování příliš nafouknutý:

Franck@YB:~ $ awk '/Time/{print 5*int($2/5)}' session?.txt | sort -n | uniq -c

    338 0
    308 5
    302 10
     58 15
     12 20
      9 25
      3 30
      1 45
      1 50

yb_debug_log_internal_restarts

Restarty jsou transparentní. Pokud chcete vidět důvod restartování nebo důvod, proč to není možné, můžete jej nechat zaprotokolovat pomocí yb_debug_log_internal_restarts=true

# log internal restarts
export PGOPTIONS='-c yb_debug_log_internal_restarts=true'

# run concurrent sessions
timeout 60 psql -p 5433 -ef update1.sql >session1.txt &
timeout 60 psql -p 5433 -ef update1.sql >session2.txt &

# tail the current logfile
docker exec -i yb bash <<<'tail -F $(bin/ysqlsh -twAXc "select pg_current_logfile()")'

Verze

Toto bylo implementováno v YugabyteDB 2.13 a já zde používám 2.13.1. Ještě není implementován při spouštění transakce z příkazů DO nebo ANALYZE, ale funguje pro procedury. Pokud chcete, můžete sledovat a komentovat problém #12254 v DO nebo ANALÝZA.

https://github.com/yugabyte/yugabyte-db/issues/12254

Na závěr

Implementace logiky opakování v aplikaci není fatální, ale volba v YugabyteDB. Distribuovaná databáze může způsobit chyby restartu z důvodu zkreslení hodin, ale stále musí být transparentní pro aplikace SQL, pokud je to možné.

Pokud chcete zabránit všem anomáliím transakcí (viz tento příklad), můžete spustit v Serializable a zpracovat výjimku 40001. Nenechte se zmást myšlenkou, že to vyžaduje více kódu, protože bez něj musíte otestovat všechny závodní podmínky, což může být větší úsilí. V Serializable databáze zajišťuje, že se budete chovat stejně jako při sériovém běhu, takže vaše testy jednotek jsou dostatečné k zaručení správnosti dat.

Avšak se stávající aplikací PostgreSQL, která používá výchozí úroveň izolace, je chování ověřeno roky provozu v produkci. Co chcete, není vyhnout se možným anomáliím, protože aplikace je pravděpodobně obchází. Chcete škálovat bez změny kódu. Zde YugabyteDB poskytuje úroveň izolace Read Committed, která nevyžaduje žádný další kód pro zpracování chyb.


  1. PostgreSQL Incremental Backup a Point-In-Time Recovery

  2. Proč má klauzule Oracle IN limit 1000 pouze pro statická data?

  3. PostgreSQL:mezi datem a časem

  4. Použití funkce Oracle to_date pro řetězec data s milisekundami