sql >> Database teknologi >  >> RDS >> PostgreSQL

Read Committed er et must for Postgres-kompatible distribuerede SQL-databaser

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.


  1. MySQL hente variabel fra Stored Procedure i PHP PDO

  2. I SQL, hvordan man vælger de øverste 2 rækker for hver gruppe

  3. Laravel 5 PDOException kunne ikke finde driver

  4. Find de kolonner, der returneres af en funktion med tabelværdi (T-SQL-eksempler)