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

Read Committed è un must per i database SQL distribuiti compatibili con Postgres

Nei database SQL, i livelli di isolamento sono una gerarchia di prevenzione delle anomalie di aggiornamento. Quindi, le persone pensano che più alto è, meglio è e che quando un database fornisce Serializable non è necessario Read Committed. Tuttavia:

  • Lettura impegnata è l'impostazione predefinita in PostgreSQL . La conseguenza è che la maggior parte delle applicazioni lo sta utilizzando (e usa SELECT ... FOR UPDATE) per prevenire alcune anomalie
  • Serializzabile non scala con il blocco pessimistico. I database distribuiti utilizzano il blocco ottimistico ed è necessario codificare la loro logica dei tentativi di transazione

Con questi due, un database SQL distribuito che non fornisce l'isolamento Read Committed non può rivendicare la compatibilità con PostgreSQL, perché è impossibile eseguire applicazioni che sono state compilate per i valori predefiniti di PostgreSQL.

YugabyteDB è iniziato con l'idea "più alto è, meglio è" e Read Committed utilizza in modo trasparente "Snapshot Isolation". Questo è corretto per le nuove applicazioni. Tuttavia, durante la migrazione di applicazioni create per Read Committed, in cui non si desidera implementare una logica di ripetizione degli errori serializzabili (SQLState 40001) e ci si aspetta che il database lo faccia per te. Puoi passare a Read Committed con **yb_enable_read_committed_isolation** gflag.

Nota:un GFlag in YugabyteDB è un parametro di configurazione globale per il database, documentato in riferimento a yb-tserver. I parametri PostgreSQL, che possono essere impostati da ysql_pg_conf_csv GFlag riguarda solo l'API YSQL ma GFlags copre tutti i livelli YugabyteDB

In questo post del blog mostrerò il valore reale del livello di isolamento Read Committed:non è necessario codificare una logica di ripetizione perché, a questo livello, YugabyteDB può farlo da solo.

Avvia YugabyteDB

Sto avviando un database a nodo singolo YugabyteDB per questa semplice 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

Non ho impostato esplicitamente alcun GFlags per mostrare il comportamento predefinito. Questa è la version 2.13.0.0 build 42 .

Controllo i gflag relativi al read commit

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 è il livello di isolamento predefinito, in base alla compatibilità con PostgreSQL:

Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"

 default_transaction_isolation
-------------------------------
 read committed
(1 row)

Creo una semplice tabella:

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

Eseguirò il seguente aggiornamento, impostando il livello di isolamento predefinito su Read Committed (per ogni evenienza, ma è l'impostazione predefinita):

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

Questo aggiornerà una riga.
Lo eseguirò da più sessioni, sulla stessa riga:

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

Nella sessione rilevata Transaction ... expired or aborted by a conflict . Se esegui lo stesso più volte, potresti anche ricevere Operation expired: Transaction aborted: kAborted , All transparent retries exhausted. Query error: Restart read required o All transparent retries exhausted. Operation failed. Try again: Value write after transaction start . Sono tutti ERROR 40001 che sono errori di serializzazione che prevedono che l'applicazione riprovi.

In Serializable, l'intera transazione deve essere ritentata, e questo generalmente non è possibile farlo in modo trasparente dal database, che non sa cos'altro ha fatto l'applicazione durante la transazione. Ad esempio, alcune righe potrebbero essere già state lette e inviate allo schermo dell'utente oa un file. Il database non può ripristinarlo. Le applicazioni devono gestirlo.

Ho impostato \Timing on per ottenere il tempo trascorso e, poiché lo sto eseguendo sul mio laptop, non c'è un tempo significativo della rete client-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

La maggior parte degli aggiornamenti sono stati meno di 5 millisecondi qui. Ma ricorda che il programma non è riuscito su 40001 rapidamente, quindi questo è il normale carico di lavoro di una sessione sul mio laptop.

Per impostazione predefinita yb_enable_read_committed_isolation è falso e in questo caso il livello di isolamento Read Committed del livello transazionale di YugabyteDB ricade sullo Snapshot Isolation più rigoroso (nel qual caso READ COMMITTED e READ UNCOMMITTED di YSQL utilizzano Snapshot Isolation).

yb_enable_read_committed_isolation=true

Ora cambia questa impostazione, che è ciò che dovresti fare quando vuoi essere compatibile con la tua applicazione PostgreSQL che non implementa alcuna logica di ripetizione.

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=

Funzionando come sopra:

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

Non ho ricevuto alcun errore ed entrambe le sessioni hanno aggiornato la stessa riga per 60 secondi.

Ovviamente, non è stato esattamente nello stesso momento in cui il database ha dovuto riprovare molte transazioni, il che è visibile nel tempo trascorso:

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

Mentre la maggior parte delle transazioni è ancora inferiore a 10 millisecondi, alcune arrivano a 120 millisecondi a causa dei tentativi.

riprova indietro

Un tentativo comune attende un intervallo di tempo esponenziale tra ogni tentativo, fino a un massimo. Questo è ciò che è implementato in YugabyteDB e i 3 parametri seguenti, impostabili a livello di sessione, lo controllano:

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.

Con il mio database locale, le transazioni sono brevi e non devo aspettare così tanto tempo. Quando aggiungi set retry_min_backoff to 10; al mio update1.sql il tempo trascorso non viene gonfiato troppo da questa logica di tentativi:

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

Le ripartenze sono trasparenti. Se vuoi vedere il motivo del riavvio, o il motivo per cui non è possibile, puoi farlo registrare con 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()")'

Versioni

Questo è stato implementato in YugabyteDB 2.13 e sto usando 2.13.1 qui. Non è ancora implementato quando si esegue la transazione dai comandi DO o ANALYZE, ma funziona per le procedure. Puoi seguire e commentare il numero 12254 se lo desideri in DO o ANALYZE.

https://github.com/yugabyte/yugabyte-db/issues/12254

In conclusione

L'implementazione della logica dei tentativi nell'applicazione non è una fatalità ma una scelta in YugabyteDB. Un database distribuito può generare errori di riavvio a causa dello sfasamento dell'orologio, ma deve comunque renderlo trasparente alle applicazioni SQL quando possibile.

Se vuoi prevenire tutte le anomalie delle transazioni (vedi questo come esempio), puoi eseguire Serializable e gestire l'eccezione 40001. Non lasciarti ingannare dall'idea che richieda più codice perché, senza di esso, è necessario testare tutte le condizioni di gara, il che potrebbe essere uno sforzo maggiore. In Serializable, il database garantisce che tu abbia lo stesso comportamento dell'esecuzione in serie in modo che i tuoi unit test siano sufficienti per garantire la correttezza dei dati.

Tuttavia, con un'applicazione PostgreSQL esistente, utilizzando il livello di isolamento predefinito, il comportamento viene convalidato da anni di esecuzione in produzione. Quello che vuoi non è evitare le possibili anomalie, perché l'applicazione probabilmente le risolve. Si desidera eseguire la scalabilità orizzontale senza modificare il codice. È qui che YugabyteDB fornisce il livello di isolamento Read Committed che non richiede codice aggiuntivo di gestione degli errori.