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.