PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Read Committed est un must pour les bases de données SQL distribuées compatibles avec Postgres

Dans les bases de données SQL, les niveaux d'isolement constituent une hiérarchie de prévention des anomalies de mise à jour. Ensuite, les gens pensent que plus c'est élevé, mieux c'est, et que lorsqu'une base de données fournit Serializable, il n'y a pas besoin de Read Committed. Cependant :

  • Read Committed est la valeur par défaut dans PostgreSQL . La conséquence est que la majorité des applications l'utilisent (et utilisent SELECT ... FOR UPDATE) pour éviter certaines anomalies
  • Sérialisable n'évolue pas avec un verrouillage pessimiste. Les bases de données distribuées utilisent un verrouillage optimiste et vous devez coder leur logique de nouvelle tentative de transaction

Avec ces deux éléments, une base de données SQL distribuée qui ne fournit pas d'isolation en lecture validée ne peut pas prétendre à la compatibilité avec PostgreSQL, car il est impossible d'exécuter des applications conçues pour les valeurs par défaut de PostgreSQL.

YugabyteDB a commencé avec l'idée "plus c'est haut, mieux c'est" et Read Committed utilise de manière transparente "Snapshot Isolation". Ceci est correct pour les nouvelles applications. Cependant, lors de la migration d'applications conçues pour la lecture validée, où vous ne souhaitez pas implémenter une logique de nouvelle tentative sur les échecs sérialisables (SQLState 40001), et attendez-vous à ce que la base de données le fasse pour vous. Vous pouvez passer en lecture validée avec le **yb_enable_read_committed_isolation** gflag.

Remarque :un GFlag dans YugabyteDB est un paramètre de configuration global pour la base de données, documenté dans la référence yb-tserver. Les paramètres PostgreSQL, qui peuvent être définis par le ysql_pg_conf_csv GFlag concerne uniquement l'API YSQL mais GFlags couvre toutes les couches YugabyteDB

Dans cet article de blog, je vais démontrer la valeur réelle du niveau d'isolement Read Committed :il n'y a pas besoin de coder une logique de nouvelle tentative car, à ce niveau, YugabyteDB peut le faire lui-même.

Démarrer YugabyteDB

Je démarre une base de données à nœud unique YugabyteDB pour cette simple démonstration :

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

Je n'ai explicitement défini aucun GFlags pour afficher le comportement par défaut. Il s'agit de la version 2.13.0.0 build 42 .

Je vérifie les gflags liés à la lecture validée

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 est le niveau d'isolement par défaut, par compatibilité PostgreSQL :

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

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

Je crée un tableau simple :

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

Je vais exécuter la mise à jour suivante, en définissant le niveau d'isolement par défaut sur Read Committed (juste au cas où - mais c'est la valeur par défaut):

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

Cela mettra à jour une ligne.
Je vais exécuter ceci à partir de plusieurs sessions, sur la même ligne :

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

Lors de la session rencontrée Transaction ... expired or aborted by a conflict . Si vous lancez plusieurs fois la même chose, vous pouvez également obtenir Operation expired: Transaction aborted: kAborted , All transparent retries exhausted. Query error: Restart read required ou All transparent retries exhausted. Operation failed. Try again: Value write after transaction start . Ce sont toutes des ERROR 40001 qui sont des erreurs de sérialisation qui attendent que l'application réessaye.

Dans Serializable, toute la transaction doit être réessayée, et cela n'est généralement pas possible de le faire de manière transparente par la base de données, qui ne sait pas ce que l'application a fait d'autre pendant la transaction. Par exemple, certaines lignes peuvent avoir déjà été lues et envoyées à l'écran utilisateur ou à un fichier. La base de données ne peut pas annuler cela. Les applications doivent gérer cela.

J'ai activé \Timing on pour obtenir le temps écoulé et, comme je l'exécute sur mon ordinateur portable, il n'y a pas de temps significatif sur le réseau client-serveur:

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 plupart des mises à jour étaient inférieures à 5 millisecondes ici. Mais rappelez-vous que le programme a échoué sur 40001 rapidement, il s'agit donc de la charge de travail normale d'une session sur mon ordinateur portable.

Par défaut yb_enable_read_committed_isolation est faux et dans ce cas, le niveau d'isolement de lecture validée de la couche transactionnelle de YugabyteDB revient à l'isolement de capture d'écran plus strict (auquel cas READ COMMITTED et READ UNCOMMITTED de YSQL utilisent l'isolation de capture d'écran).

yb_enable_read_committed_isolation=true

Modifiez maintenant ce paramètre, ce que vous devez faire lorsque vous souhaitez être compatible avec votre application PostgreSQL qui n'implémente aucune logique de nouvelle tentative.

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=

Exécution de la même manière que ci-dessus :

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

Je n'ai eu aucune erreur et les deux sessions ont mis à jour la même ligne pendant 60 secondes.

Bien sûr, ce n'était pas exactement au même moment car la base de données devait réessayer de nombreuses transactions, ce qui est visible dans le temps écoulé :

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

Alors que la plupart des transactions durent encore moins de 10 millisecondes, certaines atteignent 120 millisecondes à cause des tentatives.

 réessayer l'interruption

Une nouvelle tentative commune attend une durée exponentielle entre chaque nouvelle tentative, jusqu'à un maximum. C'est ce qui est implémenté dans YugabyteDB et les 3 paramètres suivants, configurables au niveau de la session, le contrôlent :

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.

Avec ma base de données locale, les transactions sont courtes et je n'ai pas à attendre autant de temps. Lors de l'ajout de set retry_min_backoff to 10; à mon update1.sql le temps écoulé n'est pas trop gonflé par cette logique de nouvelle tentative :

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

Les redémarrages sont transparents. Si vous voulez voir la raison des redémarrages, ou la raison pour laquelle ce n'est pas possible, vous pouvez l'enregistrer avec 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()")'

Versions

Cela a été implémenté dans YugabyteDB 2.13 et j'utilise 2.13.1 ici. Il n'est pas encore implémenté lors de l'exécution de la transaction à partir des commandes DO ou ANALYZE, mais fonctionne pour les procédures. Vous pouvez suivre et commenter le problème #12254 si vous le souhaitez dans DO ou ANALYZE.

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

En conclusion

L'implémentation de la logique de nouvelle tentative dans l'application n'est pas une fatalité mais un choix dans YugabyteDB. Une base de données distribuée peut générer des erreurs de redémarrage en raison d'un décalage d'horloge, mais doit quand même la rendre transparente pour les applications SQL lorsque cela est possible.

Si vous souhaitez empêcher toutes les anomalies de transactions (voir celle-ci comme exemple), vous pouvez exécuter Serializable et gérer l'exception 40001. Ne vous laissez pas berner par l'idée que cela nécessite plus de code car, sans cela, vous devez tester toutes les conditions de course, ce qui peut représenter un effort plus important. Dans Serializable, la base de données garantit que vous avez le même comportement que l'exécution en série afin que vos tests unitaires soient suffisants pour garantir l'exactitude des données.

Cependant, avec une application PostgreSQL existante, utilisant le niveau d'isolation par défaut, le comportement est validé par des années d'exécution en production. Ce que vous voulez, ce n'est pas éviter les anomalies possibles, car l'application les contourne probablement. Vous souhaitez effectuer un scale-out sans modifier le code. C'est là que YugabyteDB fournit le niveau d'isolation Read Committed qui ne nécessite aucun code de gestion d'erreur supplémentaire.