PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Read Committed jest koniecznością w przypadku rozproszonych baz danych SQL zgodnych z Postgres

W bazach danych SQL poziomy izolacji stanowią hierarchię zapobiegania anomaliom aktualizacji. Następnie ludzie myślą, że im wyższy, tym lepiej, a gdy baza danych zapewnia możliwość serializacji, nie ma potrzeby wykonywania odczytu. Jednak:

  • Zatwierdzenie odczytu jest domyślnym ustawieniem w PostgreSQL . Konsekwencją jest to, że większość aplikacji go używa (i używa SELECT ... FOR UPDATE), aby zapobiec pewnym anomaliom
  • Możliwość serializacji nie skaluje się z pesymistycznym blokowaniem. Rozproszone bazy danych używają optymistycznego blokowania i musisz zakodować ich logikę ponawiania transakcji

Dzięki tym dwóm rozproszona baza danych SQL, która nie zapewnia izolacji Read Committed, nie może twierdzić, że jest kompatybilna z PostgreSQL, ponieważ uruchamianie aplikacji zbudowanych z domyślnymi ustawieniami PostgreSQL jest niemożliwe.

YugabyteDB zaczął od pomysłu „im wyższy, tym lepszy”, a Read Committed w przejrzysty sposób wykorzystuje „Snapshot Isolation”. Dotyczy to nowych aplikacji. Jednak podczas migrowania aplikacji utworzonych dla Read Committed, gdzie nie chcesz implementować logiki ponawiania prób w przypadku awarii serializowalnych (SQLState 40001) i oczekiwać, że baza danych zrobi to za Ciebie. Możesz przełączyć się na Odczyt zatwierdzony za pomocą **yb_enable_read_committed_isolation** flaga g.

Uwaga:GFlag w YugabyteDB jest globalnym parametrem konfiguracyjnym bazy danych, udokumentowanym w dokumentacji yb-tserver. Parametry PostgreSQL, które można ustawić za pomocą ysql_pg_conf_csv GFlag dotyczy tylko interfejsu API YSQL, ale GFlags obejmuje wszystkie warstwy YugabyteDB

W tym poście na blogu zademonstruję prawdziwą wartość poziomu izolacji Odczytu zatwierdzonego:nie ma potrzeby kodowania logiki ponawiania próby ponieważ na tym poziomie YugabyteDB może to zrobić sam.

Uruchom YugabyteDB

Uruchamiam bazę danych pojedynczego węzła YugabyteDB dla tego prostego 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

Ja wyraźnie nie ustawiłem żadnych GFlags, aby pokazać domyślne zachowanie. To jest version 2.13.0.0 build 42 .

Sprawdzam przeczytane popełnione powiązane gflagi

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 to domyślny poziom izolacji, zgodnie z kompatybilnością PostgreSQL:

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

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

Tworzę prostą tabelę:

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

Uruchomię następującą aktualizację, ustawiając domyślny poziom izolacji na Odczyt zatwierdzony (na wszelki wypadek - ale jest to ustawienie domyślne):

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

To zaktualizuje jeden wiersz.
Wykonam to z wielu sesji w tym samym wierszu:

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

Podczas sesji napotkano Transaction ... expired or aborted by a conflict . Jeśli uruchomisz to samo kilka razy, możesz również otrzymać Operation expired: Transaction aborted: kAborted , All transparent retries exhausted. Query error: Restart read required lub All transparent retries exhausted. Operation failed. Try again: Value write after transaction start . Wszystkie są to BŁĘDY 40001, które są błędami serializacji, które oczekują, że aplikacja spróbuje ponownie.

W Serializable należy ponowić próbę całej transakcji, a na ogół nie jest to możliwe w sposób przejrzysty dla bazy danych, która nie wie, co jeszcze aplikacja zrobiła podczas transakcji. Na przykład niektóre wiersze mogły zostać już odczytane i wysłane do ekranu użytkownika lub do pliku. Baza danych nie może tego cofnąć. Aplikacje muszą to obsłużyć.

Ustawiłem \Timing on aby uzyskać czas, który upłynął, a ponieważ używam tego na moim laptopie, nie ma znaczącego czasu w sieci klient-serwer:

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

Większość aktualizacji trwała mniej niż 5 milisekund. Pamiętaj jednak, że program się nie powiódł 40001 szybko, więc jest to normalne obciążenie jednej sesji na moim laptopie.

Domyślnie yb_enable_read_committed_isolation jest fałszywe i w tym przypadku poziom izolacji Read Committed warstwy transakcyjnej YugabyteDB powraca do bardziej rygorystycznego Snapshot Isolation (w takim przypadku READ COMMITTED i READ UNCOMMITTED z YSQL używają Snapshot Isolation).

yb_enable_read_committed_isolation=prawda

Teraz zmień to ustawienie, co powinieneś zrobić, jeśli chcesz być kompatybilny z aplikacją PostgreSQL, która nie implementuje żadnej logiki ponawiania.

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=

Działa tak samo jak powyżej:

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

Nie mam żadnego błędu, a obie sesje aktualizują ten sam wiersz w ciągu 60 sekund.

Oczywiście nie było to dokładnie w tym samym czasie, kiedy baza musiała ponawiać wiele transakcji, co widać po upływie czasu:

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

Podczas gdy większość transakcji jest nadal krótsza niż 10 milisekund, niektóre do 120 milisekund z powodu ponownych prób.

ponów próbę wycofywania

Zwykła ponowna próba czeka wykładniczo przez czas między kolejnymi próbami, aż do maksimum. To jest to, co jest zaimplementowane w YugabyteDB, a kontrolują to 3 następujące parametry, które można ustawić na poziomie sesji:

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.

Z moją lokalną bazą danych transakcje są krótkie i nie muszę czekać tak długo. Podczas dodawania set retry_min_backoff to 10; do mojego pliku update1.sql czas, który upłynął, nie jest zbytnio zawyżony przez tę logikę ponawiania próby:

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

Restarty są przezroczyste. Jeśli chcesz zobaczyć powód ponownego uruchomienia lub powód, dla którego nie jest to możliwe, możesz zarejestrować to za pomocą 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()")'

Wersje

Zostało to zaimplementowane w YugabyteDB 2.13 i używam tutaj 2.13.1. Nie jest jeszcze zaimplementowany podczas uruchamiania transakcji z poleceń DO lub ANALIZA, ale działa w przypadku procedur. Możesz śledzić i komentować numer #12254, jeśli chcesz go w DO lub ANALIZA.

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

Podsumowując

Zaimplementowanie logiki ponawiania w aplikacji nie jest fatalizmem, ale wyborem w YugabyteDB. Rozproszona baza danych może zgłaszać błędy restartu z powodu przesunięcia zegara, ale nadal musi być przezroczysta dla aplikacji SQL, jeśli to możliwe.

Jeśli chcesz zapobiec wszystkim anomaliom transakcji (zobacz ten przykład jako przykład), możesz uruchomić w Serializable i obsłużyć wyjątek 40001. Nie daj się zwieść idei, że wymaga więcej kodu, ponieważ bez niego musisz przetestować wszystkie warunki wyścigu, co może być większym wysiłkiem. W Serializable baza danych zapewnia takie samo zachowanie jak uruchamianie szeregowe, dzięki czemu testy jednostkowe są wystarczające do zagwarantowania poprawności danych.

Jednak w przypadku istniejącej aplikacji PostgreSQL, używającej domyślnego poziomu izolacji, zachowanie jest sprawdzane na podstawie lat działania w środowisku produkcyjnym. To, czego chcesz, to nie unikanie możliwych anomalii, ponieważ aplikacja prawdopodobnie je obejdzie. Chcesz skalować w poziomie bez zmiany kodu. W tym miejscu YugabyteDB zapewnia poziom izolacji Read Committed, który nie wymaga dodatkowego kodu obsługi błędów.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak mogę (lub mogę) WYBRAĆ ODRĘBNE w wielu kolumnach?

  2. Jak odblokować możliwe blokady rzędów w Postgresie?

  3. Jak stworzyć unikalny indeks, w którym kolejność kolumn nie będzie brana pod uwagę (ustawiona?)

  4. Dlaczego podczas sprawdzania typu wiersza jest NOT NULL false?

  5. Jak dokładnie wyczyścić i ponownie zainstalować postgresql na ubuntu?