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.