sql >> Base de Datos >  >> RDS >> PostgreSQL

Read Committed es imprescindible para las bases de datos SQL distribuidas compatibles con Postgres

En las bases de datos SQL, los niveles de aislamiento son una jerarquía de prevención de anomalías de actualización. Entonces, la gente piensa que cuanto más alto es mejor, y que cuando una base de datos proporciona Serializable no hay necesidad de Read Committed. Sin embargo:

  • Read Committed es el predeterminado en PostgreSQL . La consecuencia es que la mayoría de las aplicaciones lo usan (y usan SELECCIONAR... PARA ACTUALIZAR) para evitar algunas anomalías
  • Serializable no escala con bloqueo pesimista. Las bases de datos distribuidas utilizan el bloqueo optimista y debe codificar su lógica de reintento de transacciones

Con esos dos, una base de datos SQL distribuida que no proporciona aislamiento de lectura confirmada no puede reclamar compatibilidad con PostgreSQL, porque es imposible ejecutar aplicaciones creadas para los valores predeterminados de PostgreSQL.

YugabyteDB comenzó con la idea de "cuanto más alto, mejor" y Read Committed utiliza de forma transparente "Snapshot Isolation". Esto es correcto para aplicaciones nuevas. Sin embargo, al migrar aplicaciones creadas para lectura confirmada, no desea implementar una lógica de reintento en errores serializables (SQLState 40001) y espera que la base de datos lo haga por usted. Puede cambiar a lectura confirmada con **yb_enable_read_committed_isolation** gbandera.

Nota:un GFlag en YugabyteDB es un parámetro de configuración global para la base de datos, documentado en la referencia de yb-tserver. Los parámetros de PostgreSQL, que se pueden establecer mediante ysql_pg_conf_csv GFlag se refiere solo a la API de YSQL, pero GFlags cubre todas las capas de YugabyteDB

En esta publicación de blog, demostraré el valor real del nivel de aislamiento de lectura confirmada:no es necesario codificar una lógica de reintento porque, a este nivel, YugabyteDB puede hacerlo por sí mismo.

Iniciar YugabyteDB

Estoy iniciando una base de datos de un solo nodo YugabyteDB para esta demostración simple:

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

Explícitamente no configuré ningún GFlags para mostrar el comportamiento predeterminado. Esta es la version 2.13.0.0 build 42 .

Compruebo los gflags relacionados leídos comprometidos

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 es el nivel de aislamiento predeterminado, por compatibilidad con PostgreSQL:

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

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

Creo una tabla 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

Ejecutaré la siguiente actualización, configurando el nivel de aislamiento predeterminado en Lectura confirmada (por si acaso, pero es el valor predeterminado):

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

Esto actualizará una fila.
Ejecutaré esto desde varias sesiones, en la misma fila:

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

En la sesión se encontró Transaction ... expired or aborted by a conflict . Si ejecuta lo mismo varias veces, también puede obtener 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 . Todos son ERROR 40001, que son errores de serialización que esperan que la aplicación vuelva a intentarlo.

En Serializable, se debe volver a intentar toda la transacción y, por lo general, la base de datos no puede hacerlo de forma transparente, ya que no sabe qué más hizo la aplicación durante la transacción. Por ejemplo, es posible que algunas filas ya se hayan leído y enviado a la pantalla del usuario o a un archivo. La base de datos no puede revertir eso. Las aplicaciones deben manejar eso.

He configurado \Timing on para obtener el tiempo transcurrido y, como estoy ejecutando esto en mi computadora portátil, no hay un tiempo significativo en la red cliente-servidor:

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 mayoría de las actualizaciones fueron de menos de 5 milisegundos aquí. Pero recuerda que el programa falló en 40001 rápidamente, por lo que esta es la carga de trabajo normal de una sesión en mi computadora portátil.

Por defecto yb_enable_read_committed_isolation es falso y, en este caso, el nivel de aislamiento de lectura confirmada de la capa transaccional de YugabyteDB vuelve al aislamiento de instantáneas más estricto (en cuyo caso, LECTURA COMPROMETIDA y LECTURA NO COMPROMETIDA de YSQL utilizan el aislamiento de instantáneas).

yb_enable_read_committed_isolation=verdadero

Ahora cambie esta configuración, que es lo que debe hacer cuando desea ser compatible con su aplicación PostgreSQL que no implementa ninguna lógica de reintento.

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=

Ejecutando lo mismo que arriba:

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

No recibí ningún error y ambas sesiones han estado actualizando la misma fila durante 60 segundos.

Por supuesto, no fue exactamente al mismo tiempo que la base de datos tuvo que volver a intentar muchas transacciones, lo cual es visible en el tiempo transcurrido:

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

Si bien la mayoría de las transacciones aún duran menos de 10 milisegundos, algunas llegan a los 120 milisegundos debido a los reintentos.

reintentar retroceder

Un reintento común espera una cantidad de tiempo exponencial entre cada reintento, hasta un máximo. Esto es lo que está implementado en YugabyteDB y los 3 parámetros siguientes, que se pueden configurar a nivel de sesión, lo controlan:

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 mi base de datos local, las transacciones son cortas y no tengo que esperar tanto tiempo. Al agregar set retry_min_backoff to 10; a mi update1.sql el tiempo transcurrido no se infla demasiado por esta lógica de reintento:

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

Los reinicios son transparentes. Si desea ver el motivo de los reinicios, o el motivo por el cual no es posible, puede registrarlo 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()")'

Versiones

Esto se implementó en YugabyteDB 2.13 y estoy usando 2.13.1 aquí. Todavía no está implementado cuando se ejecuta la transacción desde los comandos DO o ANALYZE, pero funciona para los procedimientos. Puede seguir y comentar el problema n.º 12254 si lo desea en HACER o ANALIZAR.

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

En conclusión

La implementación de la lógica de reintento en la aplicación no es una fatalidad sino una opción en YugabyteDB. Una base de datos distribuida puede generar errores de reinicio debido al sesgo del reloj, pero aún debe hacerlo transparente para las aplicaciones SQL cuando sea posible.

Si desea evitar todas las anomalías de transacciones (vea esta como ejemplo), puede ejecutar en Serializable y manejar la excepción 40001. No se deje engañar por la idea de que requiere más código porque, sin él, necesita probar todas las condiciones de carrera, lo que puede ser un esfuerzo mayor. En Serializable, la base de datos garantiza que tenga el mismo comportamiento que si se ejecutara en serie, de modo que sus pruebas unitarias sean suficientes para garantizar la exactitud de los datos.

Sin embargo, con una aplicación PostgreSQL existente, utilizando el nivel de aislamiento predeterminado, el comportamiento se valida por años de funcionamiento en producción. Lo que quiere no es evitar las posibles anomalías, porque la aplicación probablemente las solucione. Quiere escalar horizontalmente sin cambiar el código. Aquí es donde YugabyteDB proporciona el nivel de aislamiento de lectura confirmada que no requiere ningún código de manejo de errores adicional.