PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Read Committed ist ein Muss für Postgres-kompatible verteilte SQL-Datenbanken

In SQL-Datenbanken sind Isolationsstufen eine Hierarchie zur Verhinderung von Aktualisierungsanomalien. Dann denken die Leute, dass je höher desto besser ist und dass, wenn eine Datenbank Serializable bereitstellt, Read Committed nicht erforderlich ist. Allerdings:

  • Read Committed ist die Standardeinstellung in PostgreSQL . Die Folge ist, dass die Mehrheit der Anwendungen es verwendet (und SELECT ... FOR UPDATE verwendet), um einige Anomalien zu verhindern
  • Serialisierbar skaliert nicht mit pessimistischem Sperren. Verteilte Datenbanken verwenden optimistisches Sperren und Sie müssen ihre Transaktionswiederholungslogik codieren

Mit diesen beiden kann eine verteilte SQL-Datenbank, die keine Read Committed-Isolation bereitstellt, keine PostgreSQL-Kompatibilität beanspruchen, da das Ausführen von Anwendungen, die für die PostgreSQL-Standardeinstellungen erstellt wurden, nicht möglich ist.

YugabyteDB begann mit der „je höher, desto besser“-Idee und Read Committed verwendet transparent „Snapshot Isolation“. Dies ist für neue Anwendungen richtig. Wenn Sie jedoch Anwendungen migrieren, die für Read Committed erstellt wurden, möchten Sie keine Wiederholungslogik für serialisierbare Fehler (SQLState 40001) implementieren und erwarten, dass die Datenbank dies für Sie erledigt. Sie können mit dem **yb_enable_read_committed_isolation** zu Read Committed wechseln gflag.

Hinweis:Ein GFlag in YugabyteDB ist ein globaler Konfigurationsparameter für die Datenbank, dokumentiert in der yb-tserver-Referenz. Die PostgreSQL-Parameter, die mit ysql_pg_conf_csv gesetzt werden können GFlag betrifft nur die YSQL-API, aber GFlags deckt alle YugabyteDB-Schichten ab

In diesem Blogbeitrag werde ich den wahren Wert der Read Committed-Isolationsstufe demonstrieren:Es besteht keine Notwendigkeit, eine Wiederholungslogik zu codieren denn auf dieser Ebene kann YugabyteDB dies selbst tun.

Starten Sie YugabyteDB

Ich starte eine YugabyteDB-Einzelknotendatenbank für diese einfache 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

Ich habe ausdrücklich keine GFlags gesetzt, um das Standardverhalten anzuzeigen. Dies ist version 2.13.0.0 build 42 .

Ich überprüfe die Read Committed Related 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 ist die Standardisolationsstufe nach PostgreSQL-Kompatibilität:

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

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

Ich erstelle eine einfache Tabelle:

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

Ich werde das folgende Update ausführen und die Standardisolationsstufe auf Read Committed setzen (nur für den Fall - aber es ist die Standardeinstellung):

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

Dadurch wird eine Zeile aktualisiert.
Ich werde dies aus mehreren Sitzungen in derselben Zeile ausführen:

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

Bei einer Sitzung ist Transaction ... expired or aborted by a conflict aufgetreten . Wenn Sie dasselbe mehrmals ausführen, erhalten Sie möglicherweise auch Operation expired: Transaction aborted: kAborted , All transparent retries exhausted. Query error: Restart read required oder All transparent retries exhausted. Operation failed. Try again: Value write after transaction start . Sie alle sind ERROR 40001, was Serialisierungsfehler sind, die erwarten, dass die Anwendung es erneut versucht.

In Serializable muss die gesamte Transaktion wiederholt werden, und dies ist im Allgemeinen nicht transparent durch die Datenbank möglich, die nicht weiß, was die Anwendung während der Transaktion sonst getan hat. Beispielsweise können einige Zeilen bereits gelesen und an den Benutzerbildschirm oder eine Datei gesendet worden sein. Die Datenbank kann das nicht rückgängig machen. Die Anwendungen müssen damit umgehen.

Ich habe \Timing on gesetzt um die verstrichene Zeit zu erhalten, und da ich dies auf meinem Laptop ausführe, gibt es keine signifikante Zeit für das Client-Server-Netzwerk:

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

Die meisten Updates dauerten hier weniger als 5 Millisekunden. Aber denken Sie daran, dass das Programm bei 40001 fehlgeschlagen ist schnell, also ist dies die normale Arbeitsbelastung für eine Sitzung auf meinem Laptop.

Standardmäßig yb_enable_read_committed_isolation ist falsch und in diesem Fall fällt die Read Committed-Isolationsstufe der Transaktionsschicht von YugabyteDB auf die strengere Snapshot-Isolation zurück (in diesem Fall verwenden READ COMMITTED und READ UNCOMMITTED von YSQL die Snapshot-Isolation).

yb_enable_read_committed_isolation=true

Ändern Sie nun diese Einstellung, was Sie tun sollten, wenn Sie mit Ihrer PostgreSQL-Anwendung kompatibel sein möchten, die keine Wiederholungslogik implementiert.

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=

Läuft wie oben:

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

Ich habe überhaupt keinen Fehler erhalten und beide Sitzungen haben 60 Sekunden lang dieselbe Zeile aktualisiert.

Natürlich war es nicht genau zur gleichen Zeit, als die Datenbank viele Transaktionen wiederholen musste, was in der verstrichenen Zeit sichtbar ist:

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

Während die meisten Transaktionen immer noch weniger als 10 Millisekunden dauern, erreichen einige aufgrund von Wiederholungsversuchen 120 Millisekunden.

Backoff erneut versuchen

Bei einer gemeinsamen Wiederholung wird zwischen den einzelnen Wiederholungen eine exponentielle Zeitspanne bis zu einem Maximum gewartet. Dies ist in YugabyteDB implementiert und wird von den 3 folgenden Parametern gesteuert, die auf Sitzungsebene eingestellt werden können:

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.

Mit meiner lokalen Datenbank sind die Transaktionen kurz und ich muss nicht so lange warten. Beim Hinzufügen von set retry_min_backoff to 10; zu meiner update1.sql Die verstrichene Zeit wird durch diese Wiederholungslogik nicht zu sehr aufgeblasen:

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

Die Neustarts sind transparent. Wenn Sie den Grund für Neustarts sehen möchten oder warum dies nicht möglich ist, können Sie ihn mit yb_debug_log_internal_restarts=true protokollieren lassen

# 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()")'

Versionen

Dies wurde in YugabyteDB 2.13 implementiert und ich verwende hier 2.13.1. Es ist noch nicht implementiert, wenn die Transaktion von DO- oder ANALYZE-Befehlen ausgeführt wird, funktioniert aber für Prozeduren. Sie können Ausgabe Nr. 12254 folgen und kommentieren, wenn Sie dies in DO oder ANALYZE wünschen.

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

Abschließend

Die Implementierung einer Wiederholungslogik in der Anwendung ist kein Todesfall, sondern eine Entscheidung in YugabyteDB. Eine verteilte Datenbank kann aufgrund von Zeitversatz Neustartfehler auslösen, muss sie aber nach Möglichkeit für SQL-Anwendungen transparent machen.

Wenn Sie alle Transaktionsanomalien verhindern möchten (siehe dieses als Beispiel), können Sie Serializable ausführen und die Ausnahme 40001 behandeln. Lassen Sie sich nicht von der Vorstellung täuschen, dass mehr Code erforderlich ist, da Sie ohne ihn alle Rennbedingungen testen müssen, was möglicherweise ein größerer Aufwand ist. In Serializable stellt die Datenbank sicher, dass Sie das gleiche Verhalten wie bei der seriellen Ausführung haben, sodass Ihre Einheitentests ausreichen, um die Korrektheit der Daten zu garantieren.

Bei einer vorhandenen PostgreSQL-Anwendung mit der Standardisolationsstufe wird das Verhalten jedoch durch jahrelangen Betrieb in der Produktion validiert. Was Sie wollen, ist nicht, die möglichen Anomalien zu vermeiden, da die Anwendung sie wahrscheinlich umgeht. Sie möchten horizontal skalieren, ohne den Code zu ändern. Hier bietet YugabyteDB die Read Committed-Isolationsstufe, die keinen zusätzlichen Fehlerbehandlungscode erfordert.