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.