sql >> Databasteknik >  >> RDS >> PostgreSQL

Read Committed är ett måste för Postgres-kompatibla distribuerade SQL-databaser

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.


  1. Skillnaden mellan Inner join och Outer join i SQL

  2. PHP laddar inte php_pgsql.dll på Windows

  3. Förstå vyer i SQL

  4. Förstå PostgreSQL-datumtyper och funktioner (genom exempel)