В SQL бази данни нивата на изолация са йерархия за предотвратяване на аномалии при актуализиране. Тогава хората смятат, че колкото по-високо е, толкова по-добре и че когато базата данни предоставя Serializable, няма нужда от Read Committed. Въпреки това:
- Read Committed е по подразбиране в PostgreSQL . Последствието е, че повечето приложения го използват (и използват SELECT ... FOR UPDATE), за да предотвратят някои аномалии
- Може да се сериализира не се мащабира с песимистично заключване. Разпределените бази данни използват оптимистично заключване и трябва да кодирате тяхната логика за повторни транзакции
С тези две разпределена SQL база данни, която не осигурява изолация на Read Committed, не може да претендира за съвместимост с PostgreSQL, тъй като стартирането на приложения, които са били изградени за PostgreSQL по подразбиране, е невъзможно.
YugabyteDB започна с идеята "колкото по-високо, толкова по-добре" и Read Committed използва прозрачно "Snapshot Isolation". Това е правилно за нови приложения. Въпреки това, когато мигрирате приложения, създадени за Read Committed, където не искате да внедрите логика за повторен опит при сериализиращи се откази (SQLState 40001) и очаквате базата данни да го направи вместо вас. Можете да превключите към Read Committed с **yb_enable_read_committed_isolation**
gflag.
Забележка:GFlag в YugabyteDB е глобален конфигурационен параметър за базата данни, документиран в справка за yb-tserver. Параметрите на PostgreSQL, които могат да бъдат зададени от ysql_pg_conf_csv
GFlag засяга само YSQL API, но GFlags покрива всички слоеве на YugabyteDB
В тази публикация в блога ще демонстрирам реалната стойност на нивото на изолация Read Committed:няма не е необходимо да се кодира логика за повторен опит защото на това ниво YugabyteDB може да го направи сам.
Стартирайте YugabyteDB
Започвам база данни с един възел YugabyteDB за тази проста демонстрация:
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
Изрично не зададох никакви GFlags за показване на поведението по подразбиране. Това е version 2.13.0.0 build 42
.
Проверявам прочетените извършени свързани gflags
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 е нивото на изолация по подразбиране чрез съвместимост с PostgreSQL:
Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"
default_transaction_isolation
-------------------------------
read committed
(1 row)
Създавам проста таблица:
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
Ще стартирам следната актуализация, като задам нивото на изолация по подразбиране на Read Committed (за всеки случай - но това е по подразбиране):
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
Това ще актуализира един ред.
Ще стартирам това от няколко сесии, на един и същи ред:
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
При възникнала сесия Transaction ... expired or aborted by a conflict
. Ако стартирате същото няколко пъти, може също да получите Operation expired: Transaction aborted: kAborted
, All transparent retries exhausted. Query error: Restart read required
или All transparent retries exhausted. Operation failed. Try again: Value write after transaction start
. Всички те са ERROR 40001, които са грешки в сериализацията, които очакват приложението да опита отново.
В Serializable цялата транзакция трябва да бъде изпробвана отново и това обикновено не е възможно да се направи прозрачно от базата данни, която не знае какво друго е направило приложението по време на транзакцията. Например някои редове може вече да са прочетени и изпратени до потребителския екран или файл. Базата данни не може да върне това. Приложенията трябва да се справят с това.
Зададох \Timing on
за да получа изминалото време и тъй като изпълнявам това на моя лаптоп, няма значително време за мрежата клиент-сървър:
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
Повечето актуализации тук бяха по-малко от 5 милисекунди. Но не забравяйте, че програмата се провали на 40001
бързо, така че това е нормалното натоварване за една сесия на моя лаптоп.
По подразбиране yb_enable_read_committed_isolation
е невярно и в този случай нивото на изолация Read Committed на транзакционния слой на YugabyteDB се връща към по-строгата изолация на моментни снимки (в този случай READ COMMITTED и READ UNCOMMITTED на YSQL използват изолация на моментни снимки).
yb_enable_read_committed_isolation=true
Сега променяте тази настройка, което трябва да направите, когато искате да бъдете съвместими с вашето PostgreSQL приложение, което не прилага никаква логика за повторен опит.
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=
Работи по същия начин, както по-горе:
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
Изобщо не получих грешка и и двете сесии актуализираха един и същ ред в продължение на 60 секунди.
Разбира се, не беше точно по същото време, когато базата данни трябваше да опита отново много транзакции, което се вижда през изминалото време:
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
Въпреки че повечето транзакции все още са по-малко от 10 милисекунди, някои до 120 милисекунди поради повторни опити.
повторен опит за отмяна
Един общ повторен опит изчаква експоненциално време между всеки опит, до максимум. Това е, което е внедрено в YugabyteDB и 3-те следните параметъра, които могат да бъдат зададени на ниво сесия, го контролират:
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.
С моята локална база данни транзакциите са кратки и не ми се налага да чакам толкова време. Когато добавяте set retry_min_backoff to 10;
към моя update1.sql
изминалото време не се увеличава твърде много от тази логика за повторен опит:
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
Рестартите са прозрачни. Ако искате да видите причината за рестартирането или причината, поради която това не е възможно, можете да го регистрирате с 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()")'
Версии
Това беше внедрено в YugabyteDB 2.13 и аз използвам 2.13.1 тук. Той все още не е внедрен при изпълнение на транзакцията от команди DO или ANALYZE, но работи за процедури. Можете да следвате и коментирате проблем #12254, ако го искате в DO или ANALYZE.
https://github.com/yugabyte/yugabyte-db/issues/12254
В заключение
Внедряването на логиката за повторен опит в приложението не е фатално, а избор в YugabyteDB. Разпределената база данни може да доведе до грешки при рестартиране поради изкривяване на часовника, но все пак трябва да я направи прозрачна за SQL приложенията, когато е възможно.
Ако искате да предотвратите всички аномалии на транзакциите (вижте тази като пример), можете да стартирате в Serializable и да обработвате изключението 40001. Не се заблуждавайте от идеята, че изисква повече код, защото без него трябва да тествате всички условия на състезанието, което може да е по-голямо усилие. В Serializable базата данни гарантира, че имате същото поведение, отколкото при серийно изпълнение, така че вашите модулни тестове са достатъчни, за да гарантират коректността на данните.
Въпреки това, със съществуващо приложение PostgreSQL, използващо нивото на изолация по подразбиране, поведението се потвърждава от години на работа в производство. Това, което искате, е да не избягвате възможните аномалии, защото приложението вероятно ги заобикаля. Искате да мащабирате, без да променяте кода. Тук YugabyteDB предоставя ниво на изолация Read Committed, което не изисква допълнителен код за обработка на грешки.