I SQL-databaser er isolationsniveauer et hierarki af forebyggelse af opdateringsanomalier. Så tror folk, at jo højere er jo bedre, og at når en database leverer Serializable, er der ikke behov for Read Committed. Dog:
- Read Committed er standarden i PostgreSQL . Konsekvensen er, at de fleste applikationer bruger det (og bruger VÆLG ... TIL OPDATERING) for at forhindre nogle uregelmæssigheder
- Serialiserbar skalerer ikke med pessimistisk låsning. Distribuerede databaser bruger optimistisk låsning, og du skal kode deres transaktionsforsøgslogik
Med disse to kan en distribueret SQL-database, der ikke giver Read Committed-isolation, ikke gøre krav på PostgreSQL-kompatibilitet, fordi det er umuligt at køre applikationer, der er bygget til PostgreSQL-standarder.
YugabyteDB startede med "jo højere jo bedre" ideen og Read Committed bruger transparent "Snapshot Isolation". Dette er korrekt for nye applikationer. Men når du migrerer applikationer bygget til Read Committed, hvor du ikke ønsker at implementere en genforsøgslogik på serialiserbare fejl (SQLSate 40001), og forventer, at databasen gør det for dig. Du kan skifte til Read Committed med **yb_enable_read_committed_isolation**
gflag.
Bemærk:et GFlag i YugabyteDB er en global konfigurationsparameter for databasen, dokumenteret i yb-tserver reference. PostgreSQL-parametrene, som kan indstilles af ysql_pg_conf_csv
GFlag vedrører kun YSQL API, men GFlags dækker alle YugabyteDB-lag
I dette blogindlæg vil jeg demonstrere den reelle værdi af Read Committed isolationsniveau:der er ingen grund til at kode en genforsøgslogik fordi på dette niveau kan YugabyteDB gøre det selv.
Start YugabyteDB
Jeg starter en YugabyteDB single node database til denne simple 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
Jeg har udtrykkeligt ikke indstillet nogen GFlags til at vise standardadfærden. Dette er version 2.13.0.0 build 42
.
Jeg tjekker de læste forpligtede relaterede gflags
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 er standardisolationsniveauet efter PostgreSQL-kompatibilitet:
Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"
default_transaction_isolation
-------------------------------
read committed
(1 row)
Jeg laver en simpel tabel:
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
Jeg kører følgende opdatering og indstiller standardisolationsniveauet til Read Committed (for en sikkerheds skyld - men det er standard):
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
Dette vil opdatere en række.
Jeg kører dette fra flere sessioner på samme række:
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
Ved session stødt Transaction ... expired or aborted by a conflict
. Hvis du kører det samme flere gange, kan du også få Operation expired: Transaction aborted: kAborted
, All transparent retries exhausted. Query error: Restart read required
eller All transparent retries exhausted. Operation failed. Try again: Value write after transaction start
. De er alle ERROR 40001, som er serialiseringsfejl, der forventer, at applikationen prøver igen.
I Serializable skal hele transaktionen prøves igen, og dette er generelt ikke muligt at gøre transparent af databasen, som ikke ved, hvad applikationen ellers gjorde under transaktionen. For eksempel kan nogle rækker allerede være læst og sendt til brugerskærmen eller en fil. Det kan databasen ikke rulle tilbage. Det skal ansøgningerne klare.
Jeg har sat \Timing on
for at få den forløbne tid, og da jeg kører dette på min bærbare computer, er der ikke væsentlig tid til klient-server-netværk:
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
De fleste opdateringer var mindre end 5 millisekunder her. Men husk, at programmet fejlede på 40001
hurtigt, så dette er den normale arbejdsbyrde på én session på min bærbare computer.
Som standard yb_enable_read_committed_isolation
er falsk, og i dette tilfælde falder Læs Committed isolationsniveauet af YugabyteDB's transaktionslag tilbage til den strengere Snapshot Isolation (i hvilket tilfælde READ COMMITTED og READ UNCOMMITTED af YSQL bruger Snapshot Isolation).
yb_enable_read_committed_isolation=true
Ændre nu denne indstilling, hvilket er hvad du skal gøre, når du vil være kompatibel med din PostgreSQL-applikation, der ikke implementerer nogen genforsøgslogik.
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=
Kører det samme som ovenfor:
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
Jeg fik ingen fejl overhovedet, og begge sessioner har opdateret den samme række i 60 sekunder.
Det var selvfølgelig ikke præcis samtidig med, at databasen skulle gentage mange transaktioner, hvilket er synligt i den forløbne tid:
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
Mens de fleste transaktioner stadig er mindre end 10 millisekunder, nogle når til 120 millisekunder på grund af genforsøg.
prøv backoff igen
Et almindeligt genforsøg venter et eksponentielt tidsrum mellem hvert genforsøg, op til et maksimum. Dette er, hvad der er implementeret i YugabyteDB, og de 3 følgende parametre, der kan indstilles på sessionsniveau, styrer det:
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.
Med min lokale database er transaktioner korte, og jeg skal ikke vente så meget tid. Når du tilføjer set retry_min_backoff to 10;
til min update1.sql
den forløbne tid pustes ikke for meget op af denne genforsøgslogik:
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
Genstarterne er gennemsigtige. Hvis du vil se årsagen til genstart, eller årsagen til at det ikke er muligt, kan du få det logget med 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()")'
Versioner
Dette blev implementeret i YugabyteDB 2.13, og jeg bruger 2.13.1 her. Det er endnu ikke implementeret, når transaktionen køres fra DO eller ANALYSE kommandoer, men fungerer for procedurer. Du kan følge og kommentere spørgsmål #12254, hvis du vil have det i DO eller ANALYSE.
https://github.com/yugabyte/yugabyte-db/issues/12254
Afslutningsvis
Implementering af genforsøgslogik i applikationen er ikke en dødsulykke, men et valg i YugabyteDB. En distribueret database kan give genstartsfejl på grund af urskævhed, men skal stadig gøre den gennemsigtig for SQL-applikationer, når det er muligt.
Hvis du vil forhindre alle uregelmæssigheder i transaktioner (se denne som et eksempel), kan du køre i Serializable og håndtere undtagelsen 40001. Lad dig ikke narre af tanken om, at det kræver mere kode, fordi uden det skal du teste alle racerforhold, hvilket kan være en større indsats. I Serializable sikrer databasen, at du har den samme adfærd som at køre serielt, så dine enhedstests er tilstrækkelige til at garantere rigtigheden af data.
Men med en eksisterende PostgreSQL-applikation, der bruger standardisolationsniveauet, valideres adfærden af årevis i produktion. Det, du ønsker, er ikke at undgå de mulige uregelmæssigheder, fordi applikationen sandsynligvis løser dem. Du vil skalere ud uden at ændre koden. Det er her, YugabyteDB leverer isolationsniveauet Read Committed, som ikke kræver yderligere fejlhåndteringskode.