言归正传,遇到一个问题,从库基于writeset并发的情况下,发现从库的worker线程CPU非常的低,导致大量的延迟,如果改为单SQL线程并发效率反而更高,CPU利用率也起来了,延迟消失,现分析如下。
对于主从并发,特别是基于writeset的并发,在主库生成last commit的时候并没有考虑到一些innodb特殊锁的存在(或者innodb的锁BUG)。因此在进行从库并发回放的时候就会出现锁堵塞的情况。遇到锁堵塞的时候就需要考虑到多线程之间唤醒的方式,这样worker线程才能继续下去,下面是多线程并发的时候唤醒的方式。
- 本worker线程遇到行锁堵塞,直接反馈,并且解锁MDL LOCK(Commit_order_manager::check_and_report_deadlock-->Commit_order_manager::report_deadlock),这一这里MDL LOCK为每个woerker线程都有一把锁,需要精准唤醒,也就是唤醒正在处于Waiting for preceding transaction to commit状态的且堵塞了本worker线程进行数据更改的worker线程,然后加入锁等待队列。
lock_rec_lock_slow
->RecLock::add_to_waitq
->thd_report_row_lock_wait
->Commit_order_manager::check_and_report_deadlock
->Commit_order_manager::report_deadlock
->Slave_worker::report_commit_order_deadlock
- 处于Waiting for preceding transaction to commit状态的worker唤醒后会重新判定自己是否堵塞了其他worker线程,(Commit_order_manager::wait_on_graph) ,唤醒可能是,
A:正常的提交序列来到继续,唤醒状态为GRANTS
B:被正在遭遇行堵塞的worker唤醒(上面的MDL LOCK解锁),唤醒后重新检测,唤醒状态为VICTIM - 如果是VICTIM状态唤醒的worker,那么不会进行提交,而是进行事务rollback,回滚后唤醒堵塞等待的worker线程(lock_reset_wait_and_release_thread_if_suspended ->que_thr_move_to_run_state),因为本worker线程可能行锁等待中,而其他worker的事务完成后进行retry transaction
- retry transaction前自己有一个sleep过程。(slave_worker_exec_job_group)
整套唤醒过程主要是借助了MDL LOCK和row lock的唤醒机制交替在使用,处于Waiting for preceding transaction to commit状态的worker如果由于行锁堵塞了其他正在执行的线程,则被堵塞的线程会通过MDL LOCK唤醒处于处于Waiting for preceding transaction to commit状态的worker,其醒来后会回滚事务,然后通过行锁的机制反过来唤醒处于行锁等待的执行woker线程。假设worker2等待worker1事务提交后才能提交,worker1的事务由于worker2的行锁堵塞不能继续那么接下来发生的如下,
WORKER1 WORKER2
--------------------
| |
| |
| |
| -----------------> |事务执行加row lock
事务执行加rowlock |
拿不到行锁 |
| 准备提交事务等待提交序列
| Waiting for preceding
| transaction to commit
|
|------------------> |
| 通过MDL LOCK唤醒 |
| |
| 唤醒后回滚事务
| |
| <----------------- |
| 通过row lock解锁 |
事务继续 |
| 事务retry
| |
事务完成 |
| |
| 事务完成
如果存在大量锁堵塞的情况下会导致MTS效率大大降低,效率远低于单线程,这套流程主要是如下时间耗用
- 锁等待唤醒
- 事务回滚
- retry 事务和之前的sleep
因此如果有大量的锁冲突这套机制就会浪费大量的时间在等待上,并且CPU的利率非常低,既然CPU的利用率低,那么CPU自然就没有执行机器码,不执行机器码当然就跑不动(高级语言-->汇编语言-->机器码)。
而对于模拟来讲可以使用下面的方式(注意:8026以下版本才能模拟,模拟用的大量的replace语句),
mysql> show create table test.testpri2 \G
*************************** 1. row ***************************
Table: testpri2
Create Table: CREATE TABLE `testpri2` (
`id` int NOT NULL AUTO_INCREMENT,
`a` int DEFAULT NULL,
`b` int DEFAULT NULL,
`c` int DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `a` (`a`,`b`)
) ENGINE=InnoDB AUTO_INCREMENT=43255 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
init data:
for ((i=0;i<10000;i++)) do /newdata/mysql/mysql8023/install/bin/mysql -S'/newdata/mysql/mysql8023/tmp/mysql3329.sock' -e "insert into test.testpri2(a,b,c) values($i,$i,$i)";do
Terminal 1:
for ((i=10000;i<20000;i++)) do /newdata/mysql/mysql8023/install/bin/mysql -S'/newdata/mysql/mysql8023/tmp/mysql3329.sock' -e "replace into test.testpri2(a,b,c) values($i,$i,$i)";done
Terminal 2:
for ((i=0;i<10000;i++)) do /newdata/mysql/mysql8023/install/bin/mysql -S'/newdata/mysql/mysql8023/tmp/mysql3329.sock' -e "replace into test.testpri2(a,b,c) values($i,$i,$i)";done
Terminal 3:
for ((i=0;i<10000;i++)) do /newdata/mysql/mysql8023/install/bin/mysql -S'/newdata/mysql/mysql8023/tmp/mysql3329.sock' -e "replace into test.testpri2(a,b,c) values($i,$i,$i)";done
Terminal 4:
for ((i=0;i<10000;i++)) do /newdata/mysql/mysql8023/install/bin/mysql -S'/newdata/mysql/mysql8023/tmp/mysql3329.sock' -e "replace into test.testpri2(a,b,c) values($i,$i,$i)";done
而对于8026版本或者以上,由于innodb修复了BUG,
image.png
因此无法模拟出来。
网友评论