PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Read Committed é uma obrigação para bancos de dados SQL distribuídos compatíveis com Postgres


Em bancos de dados SQL, os níveis de isolamento são uma hierarquia de prevenção de anomalias de atualização. Então, as pessoas pensam que quanto maior melhor, e que quando um banco de dados fornece Serializable não há necessidade de Read Committed. No entanto:
  • Leitura confirmada é o padrão no PostgreSQL . A consequência é que a maioria dos aplicativos está usando (e use SELECT ... FOR UPDATE) para evitar algumas anomalias
  • Serializável não escala com travamento pessimista. Bancos de dados distribuídos usam bloqueio otimista e você precisa codificar sua lógica de repetição de transação

Com esses dois, um banco de dados SQL distribuído que não fornece isolamento Read Committed não pode reivindicar a compatibilidade do PostgreSQL, porque a execução de aplicativos que foram criados para os padrões do PostgreSQL é impossível.

O YugabyteDB começou com a ideia "quanto maior, melhor" e o Read Committed está usando de forma transparente o "Snapshot Isolation". Isso é correto para novos aplicativos. No entanto, ao migrar aplicativos criados para Read Committed, em que você não deseja implementar uma lógica de repetição em falhas serializáveis ​​(SQLState 40001) e espera que o banco de dados faça isso por você. Você pode alternar para Read Committed com o **yb_enable_read_committed_isolation** gflag.

Observação:um GFlag no YugabyteDB é um parâmetro de configuração global para o banco de dados, documentado na referência yb-tserver. Os parâmetros do PostgreSQL, que podem ser definidos pelo ysql_pg_conf_csv O GFlag diz respeito apenas à API YSQL, mas o GFlags cobre todas as camadas do YugabyteDB

Nesta postagem do blog, demonstrarei o valor real do nível de isolamento Read Committed:não há necessidade de codificar uma lógica de repetição porque, nesse nível, o YugabyteDB pode fazer isso sozinho.

Inicie o YugabyteDB


Estou iniciando um banco de dados de nó único YugabyteDB para esta demonstração simples:

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

Eu explicitamente não configurei nenhum GFlags para mostrar o comportamento padrão. Esta é a version 2.13.0.0 build 42 .

Eu verifico os gflags relacionados ao commit de leitura

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 é o nível de isolamento padrão, por compatibilidade com PostgreSQL:

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

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

Eu crio uma tabela simples:

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

Vou executar a seguinte atualização, definindo o nível de isolamento padrão para Read Committed (apenas no caso - mas é o padrão):

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

Isso atualizará uma linha.
Vou executar isso de várias sessões, na mesma linha:

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


Na sessão encontrada Transaction ... expired or aborted by a conflict . Se você executar o mesmo várias vezes, também poderá obter 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 . Eles são todos ERROR 40001, que são erros de serialização que esperam que o aplicativo tente novamente.

Em Serializable, toda a transação deve ser tentada novamente, e isso geralmente não é possível fazer de forma transparente pelo banco de dados, que não sabe o que mais a aplicação fez durante a transação. Por exemplo, algumas linhas podem já ter sido lidas e enviadas para a tela do usuário ou para um arquivo. O banco de dados não pode reverter isso. Os aplicativos devem lidar com isso.

Configurei \Timing on para obter o tempo decorrido e, como estou executando isso no meu laptop, não há tempo significativo na rede 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

A maioria das atualizações foi inferior a 5 milissegundos aqui. Mas lembre-se que o programa falhou em 40001 rapidamente, então essa é a carga de trabalho normal de uma sessão no meu laptop.

Por padrão yb_enable_read_committed_isolation é false e, nesse caso, o nível de isolamento Read Committed da camada transacional do YugabyteDB volta para o Snapshot Isolation mais estrito (nesse caso, READ COMMITTED e READ UNCOMMITTED do YSQL usam o Snapshot Isolation).

yb_enable_read_committed_isolation=true


Agora alterando essa configuração, que é o que você deve fazer quando quiser ser compatível com seu aplicativo PostgreSQL que não implementa nenhuma lógica de repetição.

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=

Executando o mesmo que acima:

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


Não recebi nenhum erro e ambas as sessões atualizaram a mesma linha durante 60 segundos.

Claro, não foi exatamente ao mesmo tempo que o banco de dados teve que tentar novamente muitas transações, o que é visível no tempo decorrido:

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

Embora a maioria das transações ainda tenha menos de 10 milissegundos, algumas chegam a 120 milissegundos devido a novas tentativas.

tente novamente a retirada


Uma nova tentativa comum espera um tempo exponencial entre cada tentativa, até um máximo. Isso é o que é implementado no YugabyteDB e os 3 parâmetros a seguir, que podem ser definidos no nível da sessão, o controlam:

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.

Com meu banco de dados local, as transações são curtas e não preciso esperar tanto tempo. Ao adicionar set retry_min_backoff to 10; para meu update1.sql o tempo decorrido não é inflado muito por esta lógica de repetição:

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


As reinicializações são transparentes. Se você quiser ver o motivo das reinicializações ou o motivo pelo qual isso não é possível, você pode registrá-lo com 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()")'


Versões


Isso foi implementado no YugabyteDB 2.13 e estou usando o 2.13.1 aqui. Ele ainda não é implementado ao executar a transação a partir dos comandos DO ou ANALYZE, mas funciona para procedimentos. Você pode seguir e comentar o problema #12254 se quiser em DO ou ANALYZE.

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

Em conclusão


A implementação da lógica de repetição no aplicativo não é uma fatalidade, mas uma escolha no YugabyteDB. Um banco de dados distribuído pode gerar erros de reinicialização devido à distorção do relógio, mas ainda precisa torná-lo transparente para aplicativos SQL quando possível.

Se você deseja evitar todas as anomalias de transações (veja este como exemplo), você pode executar em Serializable e manipular a exceção 40001. Não se deixe enganar pela ideia de que requer mais código porque, sem ele, você precisa testar todas as condições de corrida, o que pode ser um esforço maior. Em Serializable, o banco de dados garante que você tenha o mesmo comportamento que executar serialmente para que seus testes de unidade sejam suficientes para garantir a exatidão dos dados.

No entanto, com um aplicativo PostgreSQL existente, usando o nível de isolamento padrão, o comportamento é validado por anos de execução em produção. O que você quer não é evitar as possíveis anomalias, pois o aplicativo provavelmente as contorna. Você deseja dimensionar sem alterar o código. É aqui que o YugabyteDB fornece o nível de isolamento Read Committed que não requer código adicional de tratamento de erros.