Dalam database SQL, tingkat isolasi adalah hierarki pencegahan anomali pembaruan. Kemudian, orang berpikir bahwa semakin tinggi semakin baik, dan bahwa ketika database menyediakan Serializable, tidak perlu Read Committed. Namun:
- Baca Dikomit adalah default di PostgreSQL . Konsekuensinya adalah sebagian besar aplikasi menggunakannya (dan menggunakan SELECT ... FOR UPDATE) untuk mencegah beberapa anomali
- Dapat dibuat serial tidak skala dengan penguncian pesimis. Basis data terdistribusi menggunakan penguncian optimis, dan Anda perlu mengkodekan logika percobaan ulang transaksi
Dengan keduanya, database SQL terdistribusi yang tidak menyediakan isolasi Read Committed tidak dapat mengklaim kompatibilitas PostgreSQL, karena menjalankan aplikasi yang dibuat untuk default PostgreSQL tidak mungkin.
YugabyteDB dimulai dengan ide "semakin tinggi semakin baik" dan Read Committed secara transparan menggunakan "Snapshot Isolation". Ini benar untuk aplikasi baru. Namun, saat memigrasikan aplikasi yang dibuat untuk Read Committed, di mana Anda tidak ingin menerapkan logika coba lagi pada kegagalan serial (SQLState 40001), dan mengharapkan database melakukannya untuk Anda. Anda dapat beralih ke Read Committed dengan **yb_enable_read_committed_isolation**
gflag.
Catatan:GFlag di YugabyteDB adalah parameter konfigurasi global untuk database, didokumentasikan dalam referensi yb-tserver. Parameter PostgreSQL, yang dapat disetel oleh ysql_pg_conf_csv
GFlag hanya menyangkut YSQL API tetapi GFlags mencakup semua lapisan YugabyteDB
Dalam posting blog ini saya akan mendemonstrasikan nilai sebenarnya dari tingkat isolasi Read Committed:tidak perlu mengkode logika coba lagi karena, pada level ini, YugabyteDB dapat melakukannya sendiri.
Mulai YugabyteDB
Saya memulai database node tunggal YugabyteDB untuk demo sederhana ini:
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
Saya secara eksplisit tidak mengatur GFlag untuk menunjukkan perilaku default. Ini adalah version 2.13.0.0 build 42
.
Saya memeriksa gflags terkait yang telah dibaca
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=
Baca Berkomitmen adalah tingkat isolasi default, dengan kompatibilitas PostgreSQL:
Franck@YB:~ $ psql -p 5433 \
-c "show default_transaction_isolation"
default_transaction_isolation
-------------------------------
read committed
(1 row)
Saya membuat tabel sederhana:
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
Saya akan menjalankan pembaruan berikut, menyetel tingkat isolasi default ke Read Committed (untuk berjaga-jaga - tetapi ini adalah default):
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
Ini akan memperbarui satu baris.
Saya akan menjalankan ini dari beberapa sesi, di baris yang sama:
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
Pada sesi ditemukan Transaction ... expired or aborted by a conflict
. Jika Anda menjalankan hal yang sama beberapa kali, Anda mungkin juga mendapatkan Operation expired: Transaction aborted: kAborted
, All transparent retries exhausted. Query error: Restart read required
atau All transparent retries exhausted. Operation failed. Try again: Value write after transaction start
. Semuanya adalah ERROR 40001 yang merupakan kesalahan serialisasi yang mengharapkan aplikasi untuk mencoba lagi.
Di Serializable, seluruh transaksi harus dicoba ulang, dan ini umumnya tidak mungkin dilakukan secara transparan oleh database, yang tidak tahu apa lagi yang dilakukan aplikasi selama transaksi. Misalnya, beberapa baris mungkin sudah dibaca, dan dikirim ke layar pengguna atau file. Basis data tidak dapat mengembalikannya. Aplikasi harus menangani itu.
Saya telah menyetel \Timing on
untuk mendapatkan waktu yang telah berlalu dan, saat saya menjalankan ini di laptop saya, tidak ada waktu yang signifikan untuk jaringan server-klien:
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
Sebagian besar pembaruan kurang dari 5 milidetik di sini. Tapi ingat bahwa program gagal pada 40001
cepat jadi ini adalah beban kerja satu sesi yang normal di laptop saya.
Secara default yb_enable_read_committed_isolation
salah dan dalam hal ini tingkat isolasi Read Committed dari lapisan transaksional YugabyteDB jatuh kembali ke Isolasi Snapshot yang lebih ketat (dalam hal ini READ COMMITTED dan READ UNCOMMITTED dari YSQL menggunakan Isolasi Snapshot).
yb_enable_read_committed_isolation=true
Sekarang ubah pengaturan ini, yang harus Anda lakukan ketika Anda ingin kompatibel dengan aplikasi PostgreSQL Anda yang tidak menerapkan logika coba lagi.
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=
Menjalankan sama seperti di atas:
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
Saya tidak mendapatkan kesalahan sama sekali, dan kedua sesi telah memperbarui baris yang sama selama 60 detik.
Tentu saja, itu tidak tepat pada saat yang sama karena database harus mencoba lagi banyak transaksi, yang terlihat dalam waktu yang telah berlalu:
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
Sementara sebagian besar transaksi masih kurang dari 10 milidetik, beberapa sampai 120 milidetik karena percobaan ulang.
coba mundur lagi
Percobaan ulang umum menunggu jumlah waktu eksponensial antara setiap percobaan ulang, hingga maksimum. Inilah yang diimplementasikan di YugabyteDB dan 3 parameter berikut, yang dapat diatur pada tingkat sesi, mengontrolnya:
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.
Dengan database lokal saya, transaksi menjadi singkat dan saya tidak perlu menunggu terlalu lama. Saat menambahkan set retry_min_backoff to 10;
ke update1.sql
saya waktu yang telah berlalu tidak terlalu meningkat oleh logika coba lagi ini:
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
Restart transparan. Jika Anda ingin melihat alasan restart, atau alasan mengapa itu tidak mungkin, Anda bisa memasukkannya ke log dengan 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()")'
Versi
Ini diimplementasikan di YugabyteDB 2.13 dan saya menggunakan 2.13.1 di sini. Itu belum diimplementasikan saat menjalankan transaksi dari perintah DO atau ANALYZE, tetapi berfungsi untuk prosedur. Anda dapat mengikuti dan mengomentari masalah #12254 jika Anda menginginkannya di LAKUKAN atau ANALISIS.
https://github.com/yugabyte/yugabyte-db/issues/12254
Kesimpulan
Menerapkan logika coba lagi dalam aplikasi bukanlah kematian tetapi pilihan di YugabyteDB. Basis data terdistribusi dapat meningkatkan kesalahan mulai ulang karena kemiringan jam, tetapi masih perlu membuatnya transparan untuk aplikasi SQL bila memungkinkan.
Jika Anda ingin mencegah semua anomali transaksi (lihat yang ini sebagai contoh), Anda dapat menjalankan di Serializable dan menangani pengecualian 40001. Jangan terkecoh dengan gagasan bahwa itu membutuhkan lebih banyak kode karena, tanpanya, Anda perlu menguji semua kondisi balapan, yang mungkin membutuhkan upaya yang lebih besar. Di Serializable, database memastikan bahwa Anda memiliki perilaku yang sama dengan menjalankan secara serial sehingga pengujian unit Anda cukup untuk menjamin kebenaran data.
Namun, dengan aplikasi PostgreSQL yang ada, menggunakan tingkat isolasi default, perilaku divalidasi oleh tahun berjalan dalam produksi. Yang Anda inginkan bukanlah menghindari kemungkinan anomali, karena aplikasi mungkin mengatasinya. Anda ingin melakukan scale-out tanpa mengubah kode. Di sinilah YugabyteDB menyediakan tingkat isolasi Read Committed yang tidak memerlukan kode penanganan kesalahan tambahan.