I SQL-databaser är isoleringsnivåer en hierarki för att förhindra uppdateringsavvikelser. Sedan tror folk att ju högre är desto bättre, och att när en databas tillhandahåller Serializable finns det inget behov av Read Committed. Men:
- Read Committed är standard i PostgreSQL . Konsekvensen är att majoriteten av applikationerna använder det (och använder VÄLJ ... FÖR UPPDATERING) för att förhindra vissa anomalier
- Serialiserbar skalar inte med pessimistisk låsning. Distribuerade databaser använder optimistisk låsning, och du måste koda deras transaktionsförsökslogik
Med dessa två kan en distribuerad SQL-databas som inte tillhandahåller Read Committed-isolering inte göra anspråk på PostgreSQL-kompatibilitet, eftersom det är omöjligt att köra applikationer som byggdes för PostgreSQL-standardinställningar.
YugabyteDB började med "ju högre desto bättre"-idén och Read Committed använder transparent "Snapshot Isolation". Detta är korrekt för nya applikationer. Men när du migrerar applikationer byggda för Read Committed, där du inte vill implementera en logik för ett nytt försök på serialiserbara misslyckanden (SQLSate 40001), och förväntar dig att databasen gör det åt dig. Du kan byta till Read Committed med **yb_enable_read_committed_isolation**
gflagga.
Obs:en GFlag i YugabyteDB är en global konfigurationsparameter för databasen, dokumenterad i yb-tserver-referens. PostgreSQL-parametrarna, som kan ställas in av ysql_pg_conf_csv
GFlag gäller endast YSQL API men GFlags täcker alla YugabyteDB-lager
I det här blogginlägget kommer jag att demonstrera det verkliga värdet av Read Committed isolationsnivå:det finns inget behov av att koda en logik för att försöka igen eftersom YugabyteDB kan göra det själv på den här nivån.
Starta YugabyteDB
Jag startar en YugabyteDB singelnodsdatabas för denna enkla 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
Jag har uttryckligen inte ställt in några GFlags för att visa standardbeteendet. Detta är version 2.13.0.0 build 42
.
Jag kollar de läsrelaterade relaterade gflaggorna
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 är standardisoleringsnivån, genom PostgreSQL-kompatibilitet:
Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"
default_transaction_isolation
-------------------------------
read committed
(1 row)
Jag skapar en enkel tabell:
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
Jag kör följande uppdatering och ställer in standardisoleringsnivån till Read Committed (för säkerhets skull - men det är 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
Detta kommer att uppdatera en rad.
Jag kör detta från flera sessioner, på samma rad:
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
Vid session påträffade Transaction ... expired or aborted by a conflict
. Om du kör samma flera gånger kan du också 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 är alla ERROR 40001 som är serialiseringsfel som förväntar sig att programmet ska försöka igen.
I Serializable måste hela transaktionen försökas igen, och detta är i allmänhet inte möjligt att göra transparent av databasen, som inte vet vad mer applikationen gjorde under transaktionen. Till exempel kan vissa rader redan ha lästs och skickats till användarskärmen eller en fil. Databasen kan inte återställa det. Det måste ansökningarna hantera.
Jag har ställt in \Timing on
för att få den förflutna tiden och när jag kör detta på min bärbara dator, finns det ingen betydande tid för klient-server-nätverk:
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 flesta uppdateringar var mindre än 5 millisekunder här. Men kom ihåg att programmet misslyckades på 40001
snabbt så det här är den normala arbetsbelastningen för en session på min bärbara dator.
Som standard yb_enable_read_committed_isolation
är falsk och i det här fallet faller Read Committed-isoleringsnivån för YugabyteDB:s transaktionslager tillbaka till den strängare Snapshot Isolation (i vilket fall READ COMMITTED och READ UNCOMMITTED av YSQL använder Snapshot Isolation).
yb_enable_read_committed_isolation=true
Ändra nu denna inställning, vilket är vad du ska göra när du vill vara kompatibel med din PostgreSQL-applikation som inte implementerar någon logik för att försöka igen.
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ör samma som ovan:
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
Jag fick inget fel alls, och båda sessionerna har uppdaterat samma rad under 60 sekunder.
Naturligtvis var det inte exakt samtidigt som databasen var tvungen att göra om många transaktioner, vilket är synligt under den förflutna tiden:
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
Medan de flesta transaktioner fortfarande är mindre än 10 millisekunder, vissa när till 120 millisekunder på grund av omförsök.
försök backoff igen
Ett vanligt nytt försök väntar en exponentiell tid mellan varje nytt försök, upp till ett maximum. Detta är vad som är implementerat i YugabyteDB och de 3 följande parametrarna, som kan ställas in på sessionsnivå, styr 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 lokala databas är transaktionerna korta och jag behöver inte vänta så mycket tid. När du lägger till set retry_min_backoff to 10;
till min update1.sql
den förflutna tiden blåses inte upp för mycket av denna logik för ett nytt försök:
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
Omstarterna är genomskinliga. Om du vill se orsaken till omstarter, eller anledningen till att det inte är möjligt, kan du få det loggat 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
Detta implementerades i YugabyteDB 2.13 och jag använder 2.13.1 här. Det är ännu inte implementerat när transaktionen körs från DO- eller ANALYSE-kommandon, men fungerar för procedurer. Du kan följa och kommentera nummer #12254 om du vill ha det i DO eller ANALYSE.
https://github.com/yugabyte/yugabyte-db/issues/12254
Sammanfattningsvis
Att implementera omförsökslogik i applikationen är inte ett dödsfall utan ett val i YugabyteDB. En distribuerad databas kan orsaka omstartsfel på grund av klockskev, men måste fortfarande göra den transparent för SQL-applikationer när det är möjligt.
Om du vill förhindra alla transaktionsavvikelser (se detta som ett exempel), kan du köra i Serializable och hantera undantaget 40001. Låt dig inte luras av tanken att det kräver mer kod, för utan den måste du testa alla tävlingsförhållanden, vilket kan vara en större ansträngning. I Serializable säkerställer databasen att du har samma beteende som att köra seriellt så att dina enhetstester är tillräckliga för att garantera att data är korrekta.
Men med en befintlig PostgreSQL-applikation, som använder standardisoleringsnivån, valideras beteendet genom år av drift i produktion. Vad du vill är att inte undvika de möjliga anomalierna, eftersom applikationen förmodligen löser dem. Du vill skala ut utan att ändra koden. Det är här YugabyteDB tillhandahåller isoleringsnivån Read Committed som inte kräver någon ytterligare felhanteringskod.