why
科普概念
背景知识
: 分布式事务和2pc在参考链接1中有介绍,2pc协议是分布式事务的一个解决方案,2pc主要缺陷
- 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
- 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
rocksdb 2pc实现
见参考链接2, 3 主要多了prepare操作。这个需求来自myrocks,作为mysql引擎需要xa事务机制myrocks学习可以见参考链接4
2pc实现简单说
txn->Put(...);
txn->Prepare();
txn->Commit();
我一开始是找myrocksxa事务是咋实现的,myrocks引擎在代码storage/myrocks里,但是翻了半天没找到。
找手册,这5有个myrocks配置选项
rocksdb_enable_2pc
- Description: Enable two phase commit for MyRocks. When set, MyRocks will keep its data consistent with the binary log (in other words, the server will be a crash-safe master). The consistency is achieved by doing two-phase XA commit with the binary log.
- Commandline:
--rocksdb-enable-2pc={0|1}
- Scope: Global
- Dynamic: Yes
- Data Type:
boolean
- Default Value:
ON
全程配置allow_2pc就能模拟xa事务吗?
我针对这个改了一版db_bench
dbbench改动,增加allow_2pc配置,如果有这个配置,就true, 调用定义DEFINE_bool就好了(gflags这个库也很好玩,之前吐槽没有命令行的库,孤陋寡闻)
机器32核,脚本参考mark改的,执行脚本
bash r.sh 10000000 60 32 4 /home/vdb/rocksdb-5.14.3/rdb 0 /home/vdb/rocksdb-5.14.3/db_bench
核心代码
#set -x
numk=$1
secs=$2
val=$3
batch=$4
dbdir=$5
sync=$6
dbb=$7
# sync, dbdir, concurmt, secs, dop
function runme {
a_concurmt=$1
a_dop=$2
a_extra=$3
a_2pc=$4
rm -rf $dbdir; mkdir $dbdir
# TODO --perf_level=0
$dbb --benchmarks=randomtransaction --use_existing_db=0 --sync=$sync --db=$dbdir --wal_dir=$dbdir --num=$numk --duration=$secs --num_levels=6 --key_size=8 --value_size=$val --block_size=4096 --cache_size=$(( 20 * 1024 * 1024 * 1024 )) --cache_numshardbits=6 --compression_type=none --compression_ratio=0.5 --level_compaction_dynamic_level_bytes=true --bytes_per_sync=8388608 --cache_index_and_filter_blocks=0 --benchmark_write_rate_limit=0 --write_buffer_size=$(( 64 * 1024 * 1024 )) --max_write_buffer_number=4 --target_file_size_base=$(( 32 * 1024 * 1024 )) --max_bytes_for_level_base=$(( 512 * 1024 * 1024 )) --verify_checksum=1 --delete_obsolete_files_period_micros=62914560 --max_bytes_for_level_multiplier=8 --statistics=0 --stats_per_interval=1 --stats_interval_seconds=60 --histogram=1 --allow_concurrent_memtable_write=$a_concurmt --enable_write_thread_adaptive_yield=$a_concurmt --memtablerep=skip_list --bloom_bits=10 --open_files=-1 --level0_file_num_compaction_trigger=4 --level0_slowdown_writes_trigger=20 --level0_stop_writes_trigger=30 --max_background_jobs=8 --max_background_flushes=2 --threads=$a_dop --merge_operator="put" --seed=1454699926 --transaction_sets=$batch --compaction_pri=3 $a_extra -enable_pipelined_write=false -allow_2pc=$a_2pc
}
for dop in 1 2 4 8 16 24 32 40 48 ; do
for concurmt in 0 1 ; do
for pc in 0 1; do
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.notrx
runme $concurmt $dop "" $pc >& $fn
q1=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
t=transaction_db
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.pessim
runme $concurmt $dop --${t}=1 $pc >& $fn
q2=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
t=optimistic_transaction_db
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.optim
runme $concurmt $dop --${t}=1 $pc >& $fn
q3=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
echo $dop mt${concurmt} allow2pc${pc} $q1 $q2 $q3 | awk '{ printf "%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6 }'
done
done
done
能看到是allow_2pc和和其他项组合的。
测试结果发现数据没有不同
线程数, 是否并发写, 是否开启2pc, 无事务,悲观事务,乐观事务
1 mt0 allow2pc0 39512 22830 21238
1 mt0 allow2pc1 40720 23014 22767
1 mt1 allow2pc0 40539 22683 22131
1 mt1 allow2pc1 36361 21680 23592
2 mt0 allow2pc0 62337 33972 27747
2 mt0 allow2pc1 62725 33941 27553
2 mt1 allow2pc0 62535 33640 31501
2 mt1 allow2pc1 62127 34320 30636
4 mt0 allow2pc0 64864 41878 25235
4 mt0 allow2pc1 65517 41184 26055
4 mt1 allow2pc0 93863 49895 28183
4 mt1 allow2pc1 89718 48726 29027
8 mt0 allow2pc0 79444 52166 26142
8 mt0 allow2pc1 80186 51643 26254
8 mt1 allow2pc0 139753 72598 24661
8 mt1 allow2pc1 136604 73382 25482
16 mt0 allow2pc0 87555 61620 22809
16 mt0 allow2pc1 88055 61812 21631
16 mt1 allow2pc0 193535 98820 21272
16 mt1 allow2pc1 190517 98582 21007
24 mt0 allow2pc0 91718 65400 20736
24 mt0 allow2pc1 92319 64477 20505
24 mt1 allow2pc0 226268 111956 20453
24 mt1 allow2pc1 224815 111901 21005
32 mt0 allow2pc0 88233 65121 20683
32 mt0 allow2pc1 89150 65643 20127
32 mt1 allow2pc0 111623 120843 20626
32 mt1 allow2pc1 230557 120421 20124
40 mt0 allow2pc0 87062 66972 20093
40 mt0 allow2pc1 86632 66814 20590
40 mt1 allow2pc0 113856 60101 20280
40 mt1 allow2pc1 115139 58768 20264
48 mt0 allow2pc0 87093 68637 20153
48 mt0 allow2pc1 87283 68382 19537
48 mt1 allow2pc0 122819 64030 19796
48 mt1 allow2pc1 126721 64090 19907
同事zcw指出这种测试可能不对,我的测试 2pc和悲观乐观事务是组合的形式,这可能并不合理,乐观事务这个参数没意义,allow_2pc只是一个配置,表示rocksdb支持而已,还是要调用prepare才能实现应用的xa,我之前错误的理解allow_2pc配置后会在rocksdb内部有prepare过程(我之前好像看到了)
还是回头看db_bench,看db_bench到底怎么测试的 所有randomtransaction会调用doinsert来真正的执行
定义在transaction_test_util.cc中,果不其然 找到txn->prepare调用
bool RandomTransactionInserter::DoInsert(DB* db, Transaction* txn,
bool is_optimistic) {
...
// pick a random number to use to increment a key in each set
...
// For each set, pick a key at random and increment it
...
if (s.ok()) {
if (txn != nullptr) {
bool with_prepare = !is_optimistic && !rand_->OneIn(10);
if (with_prepare) {
// Also try commit without prepare
s = txn->Prepare();
assert(s.ok());
ROCKS_LOG_DEBUG(db->GetDBOptions().info_log,
"Prepare of %" PRIu64 " %s (%s)", txn->GetId(),
s.ToString().c_str(), txn->GetName().c_str());
db->GetDBOptions().env->SleepForMicroseconds(
static_cast<int>(cmt_delay_ms_ * 1000));
}
if (!rand_->OneIn(20)) {
s = txn->Commit();
注意with_prepare这句,这句表明,不是乐观事务,悲观事务,会注意这个取反,会90%调用prepare,调用prepare的事务可以确定肯定是xa事务。所以我需要加个配置项,改成100%的,也应该加个完全不调用prepare的做对照
另外,这个rand_->OneIn(10)实现的很好玩。看测试代码总能发现这些犄角旮旯的需求以及好玩的实现
改动点6
- 加上transaction_db_xa
- 所有 FLAGS_transaction_db都得或上FLAGS_transaction_db_xa,避免遗漏,或者不复用,单独再写
- randomTransaction入口
void RandomTransaction(ThreadState* thread) {
while (!duration.Done(1)) {
bool success;
// RandomTransactionInserter will attempt to insert a key for each
// # of FLAGS_transaction_sets
if (FLAGS_optimistic_transaction_db) {
success = inserter.OptimisticTransactionDBInsert(db_.opt_txn_db);
} else if (FLAGS_transaction_db) {
TransactionDB* txn_db = reinterpret_cast<TransactionDB*>(db_.db);
success = inserter.TransactionDBInsert(txn_db, txn_options);
} else if (FLAGS_transaction_db_xa) {
TransactionDB* txn_db = reinterpret_cast<TransactionDB*>(db_.db);
success = inserter.TransactionDBXAInsert(txn_db, txn_options);
} else {
success = inserter.DBInsert(db_.db);
}
加上个flags_transaction_db_xa 对应的option也得注意,要enable allow_2pc
没enable allow_2pc做了个测试,结果真的就是降低了10%,没啥参考价值的感觉。 最后一列是100% prepare
1 mt0 37353 21628 22018 21845
1 mt1 38089 21171 22606 21688
2 mt0 62627 31901 27003 32895
2 mt1 62029 33865 31083 33691
4 mt0 64915 41651 26226 40853
4 mt1 88089 51123 29066 48673
8 mt0 79742 51276 25154 49865
8 mt1 134687 72683 25000 71469
16 mt0 88103 61816 21568 60656
16 mt1 192417 98546 21265 97890
24 mt0 91989 64858 20592 63141
24 mt1 232313 111736 20706 110083
32 mt0 91073 65840 20399 64103
32 mt1 221337 61289 20164 118167
40 mt0 85909 66244 20144 64709
40 mt1 116536 59155 20119 55437
48 mt0 86006 68390 19828 66910
48 mt1 125246 63577 19700 61621
我enable allow2pc 100%prepare 测了一组数据,作为对照,测了一个0%prepare
#set -x
numk=$1
secs=$2
val=$3
batch=$4
dbdir=$5
sync=$6
dbb=$7
# sync, dbdir, concurmt, secs, dop
function runme {
a_concurmt=$1
a_dop=$2
a_extra=$3
rm -rf $dbdir; mkdir $dbdir
# TODO --perf_level=0
$dbb --benchmarks=randomtransaction --use_existing_db=0 --sync=$sync --db=$dbdir --wal_dir=$dbdir --num=$numk --duration=$secs --num_levels=6 --key_size=8 --value_size=$val --block_size=4096 --cache_size=$(( 20 * 1024 * 1024 * 1024 )) --cache_numshardbits=6 --compression_type=none --compression_ratio=0.5 --level_compaction_dynamic_level_bytes=true --bytes_per_sync=8388608 --cache_index_and_filter_blocks=0 --benchmark_write_rate_limit=0 --write_buffer_size=$(( 64 * 1024 * 1024 )) --max_write_buffer_number=4 --target_file_size_base=$(( 32 * 1024 * 1024 )) --max_bytes_for_level_base=$(( 512 * 1024 * 1024 )) --verify_checksum=1 --delete_obsolete_files_period_micros=62914560 --max_bytes_for_level_multiplier=8 --statistics=0 --stats_per_interval=1 --stats_interval_seconds=60 --histogram=1 --allow_concurrent_memtable_write=$a_concurmt --enable_write_thread_adaptive_yield=$a_concurmt --memtablerep=skip_list --bloom_bits=10 --open_files=-1 --level0_file_num_compaction_trigger=4 --level0_slowdown_writes_trigger=20 --level0_stop_writes_trigger=30 --max_background_jobs=8 --max_background_flushes=2 --threads=$a_dop --merge_operator="put" --seed=1454699926 --transaction_sets=$batch --compaction_pri=3 $a_extra -enable_pipelined_write=false
}
for dop in 1 2 4 8 16 24 32 40 48 ; do
for concurmt in 0 1 ; do
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.notrx
runme $concurmt $dop "" >& $fn
q1=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
t=transaction_db
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.pessim
runme $concurmt $dop --${t}=1 >& $fn
q2=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
t=optimistic_transaction_db
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.optim
runme $concurmt $dop --${t}=1 >& $fn
q3=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
t=transaction_db_xa
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.pessimxa
runme $concurmt $dop --${t}=1 >& $fn
q4=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
fn=o.dop${dop}.val${val}.batch${batch}.concur${concurmt}.pessimnopre
runme $concurmt $dop --${t}=-1 >& $fn #-1 for no prepare
q5=$( grep ^randomtransaction $fn | awk '{ print $5 }' )
echo $dop mt${concurmt} $q1 $q2 $q3 $q4 $q5 | awk '{ printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7 }'
done
done
线程数 |
是否并发写 |
无事务 |
悲观事务 默认90%prepare allwo_2pc=0 |
乐观事务 |
悲观事务 prepare 100% allwo_2pc=1 |
悲观事务 prepare 0% |
1 |
mt0 |
40631 |
22399 |
23447 |
22085 |
23957 |
1 |
mt1 |
40744 |
21680 |
23316 |
21896 |
24040 |
2 |
mt0 |
59313 |
33031 |
27751 |
32342 |
36653 |
2 |
mt1 |
60690 |
33169 |
30819 |
33349 |
34445 |
4 |
mt0 |
54808 |
41715 |
25583 |
37383 |
46622 |
4 |
mt1 |
74016 |
50699 |
29411 |
48445 |
52160 |
8 |
mt0 |
68584 |
48591 |
25009 |
45397 |
59238 |
8 |
mt1 |
94581 |
64892 |
24612 |
70616 |
83271 |
16 |
mt0 |
86554 |
60897 |
22602 |
58607 |
74842 |
16 |
mt1 |
186053 |
96305 |
21548 |
93654 |
121303 |
24 |
mt0 |
91051 |
63187 |
20792 |
61605 |
79021 |
24 |
mt1 |
209827 |
111059 |
20735 |
106036 |
144641 |
32 |
mt0 |
90318 |
64180 |
20839 |
62339 |
77219 |
32 |
mt1 |
185310 |
113754 |
20439 |
108233 |
84580 |
40 |
mt0 |
87769 |
65888 |
20449 |
63999 |
80699 |
40 |
mt1 |
119916 |
60919 |
19891 |
56265 |
88792 |
48 |
mt0 |
86097 |
67501 |
19838 |
66396 |
81704 |
48 |
mt1 |
119423 |
61086 |
19217 |
59750 |
86127 |
markdown 不能调格间距,真破
这个数据作为参考。
另外,有个2pc的bug 值得关注一下 pr https://github.com/facebook/rocksdb/pull/1768
reference
- 分布式事务,2pc 3pc https://www.hollischuang.com/archives/681
- rocksdb 2pc实现 https://github.com/facebook/rocksdb/wiki/Two-Phase-Commit-Implementation
- rocksdb 事务,其中有2pc事务讲解https://zhuanlan.zhihu.com/p/31255678
- myrocks deep dive,不错,关于rocksdb的部分提纲摰领https://www.percona.com/live/plam16/sites/default/files/slides/myrocksdeepdive201604-160419162421.pdf
- https://mariadb.com/kb/en/library/myrocks-system-variables/
- 我的测试改动 https://github.com/wanghenshui/rocksdb/tree/14.3-modified-db-bench
- 一个excel小知识,生成的数据如何整理成excel格式,选择这列 ->{数据}菜单 ->分列->按照空格分列,https://zhidao.baidu.com/question/351335222
- cockroachdb 用rocksdb 2pc的一个讨论,有时间仔细看看 https://github.com/cockroachdb/cockroach/issues/16948