SQLデータベースでは、分離レベルは更新異常防止の階層です。次に、人々は、高いほど良いと考え、データベースがSerializableを提供する場合、ReadCommittedは必要ないと考えます。ただし:
- ReadCommittedはPostgreSQLのデフォルトです 。その結果、アプリケーションの大部分がいくつかの異常を防ぐためにそれを使用しています(そしてSELECT ... FOR UPDATEを使用しています)
- シリアル化可能 悲観的なロックではスケーリングしません。分散データベースは楽観的ロックを使用しており、トランザクション再試行ロジックをコーディングする必要があります
これら2つでは、PostgreSQLのデフォルト用にビルドされたアプリケーションを実行することは不可能であるため、ReadCommitted分離を提供しない分散SQLデータベースはPostgreSQLの互換性を主張できません。
YugabyteDBは「高いほど良い」という考えから始まり、ReadCommittedは「スナップショットアイソレーション」を透過的に使用しています。これは、新しいアプリケーションに適しています。ただし、読み取りコミット用に構築されたアプリケーションを移行する場合、シリアル化可能な障害(SQLState 40001)に再試行ロジックを実装したくない場合は、データベースがそれを実行することを期待します。 **yb_enable_read_committed_isolation**
を使用してReadCommittedに切り替えることができます gflag。
注:YugabyteDBのGFlagは、データベースのグローバル構成パラメーターであり、yb-tserverリファレンスに記載されています。 ysql_pg_conf_csv
で設定できるPostgreSQLパラメータ GFlagはYSQLAPIのみに関係しますが、GFlagsはすべてのYugabyteDBレイヤーをカバーします
このブログ投稿では、ReadCommitted分離レベルの真の価値をデモします。再試行ロジックをコーディングする必要はありません なぜなら、このレベルでは、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
次の更新を実行し、デフォルトの分離レベルを読み取りコミットに設定します(念のため-ただし、これがデフォルトです)。
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
これにより、1行が更新されます。
同じ行の複数のセッションからこれを実行します。
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
。これらはすべてERROR40001であり、アプリケーションが再試行することを期待するシリアル化エラーです。
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
で失敗したことを忘れないでください すぐに、これは私のラップトップでの通常の1セッションのワークロードです。
デフォルトでは、yb_enable_read_committed_isolation
はfalseであり、この場合、YugabyteDBのトランザクションレイヤーの読み取りコミット分離レベルは、より厳密なスナップショット分離にフォールバックします(この場合、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()")'
バージョン
これはYugabyteDB2.13で実装されており、ここでは2.13.1を使用しています。 DOまたはANALYZEコマンドからトランザクションを実行する場合はまだ実装されていませんが、プロシージャに対しては機能します。 DOまたはANALYZEで必要な場合は、問題#12254をフォローしてコメントすることができます。
https://github.com/yugabyte/yugabyte-db/issues/12254
結論
アプリケーションに再試行ロジックを実装することは致命的ではなく、YugabyteDBでの選択です。分散データベースでは、クロックスキューが原因で再起動エラーが発生する可能性がありますが、可能な場合はSQLアプリケーションに対して透過的にする必要があります。
すべてのトランザクションの異常を防止したい場合(例としてこれを参照)、Serializableで実行し、40001例外を処理できます。より多くのコードが必要であるという考えに惑わされないでください。コードがないと、すべての競合状態をテストする必要があり、これはより大きな労力になる可能性があります。 Serializableでは、データベースにより、シリアルで実行する場合と同じ動作が保証されるため、データの正確性を保証するために単体テストで十分です。
ただし、既存のPostgreSQLアプリケーションでは、デフォルトの分離レベルを使用して、本番環境で何年も実行することで動作が検証されます。アプリケーションがおそらくそれらを回避するので、あなたが望むのは起こりうる異常を回避することではありません。コードを変更せずにスケールアウトしたい。これは、YugabyteDBが追加のエラー処理コードを必要としないReadCommitted分離レベルを提供する場所です。