TiDB 6.0 版本针对悲观事务引入了内存悲观锁的优化,带来了明显的性能提升。本文将从最初的乐观事务到悲观事务入手;介绍 6.0 版本针对悲观锁进行优化的原理,并结合压测数据验证其带来的性能提升。
作者简介:jiyf,开源 NewSQL 爱好者,目前就职于天翼云,后端开发工程师,TiDB 社区资深用户。
在 v6.0.0 版本,针对悲观事务引入了内存悲观锁的优化(In-memory lock),从压测数据来看,带来的性能提升非常明显(Sysbench 工具压测 oltp_write_only 脚本)。
TiDB 事务模型从最初的乐观事务到悲观事务;在悲观事务上,又针对悲观锁进行的 ”Pipelined 写入“ 和 ”In-memory lock“ 优化,从功能特性上可以看出演进过程(参考TiDB 事务概览)。
乐观事务在提交时,可能因为并发写造成写写冲突,不同设置会出现以下两种不同的现象:
T1 | T2 | 说明 |
---|---|---|
mysql> set session tidb_disable_txn_auto_retry = 1(或者 set session tidb_retry_limit=0;);Query OK, 0 rows affected (0.00 sec) | 关闭重试 | |
mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin optimistic;Query OK, 0 rows affected (0.00 sec) | T2开启乐观事务 |
mysql> delete from t where id = 1;Query OK, 1 row affected (0.00 sec) | ||
mysql> delete from t where id = 1;Query OK, 1 row affected (0.00 sec) | 语句执行成功,没有被 T1 阻塞,跟 MySQL 行为不兼容 | |
mysql> commit;Query OK, 0 rows affected (0.00 sec) | T1 提交成功 | |
mysql> commit;ERROR 9007 (HY000): … | T2 提交失败 |
T2 事务提交失败,具体的报错信息如下:
mysql> commit; ERROR 9007 (HY000): Write conflict, txnStartTS=433599424403603460, conflictStartTS=433599425871872005, conflictCommitTS=433599429279744001, key={tableID=5623, handle=1} primary={tableID=5623, handle=1} [try again later]
T1 | T2 | 说明 |
---|---|---|
mysql> set session tidb_disable_txn_auto_retry = 0;Query OK, 0 rows affected (0.00 sec) | 开启重试 | |
mysql> set session tidb_retry_limit = 10;Query OK, 0 rows affected (0.00 sec) | 设置最大重试次数 | |
mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin optimistic;Query OK, 0 rows affected (0.00 sec) | T2开启乐观事务 |
mysql> delete from t where id = 1;Query OK, 1 row affected (0.01 sec) | T1 删除 id = 1的记录 | |
mysql> delete from t where id in (1, 2, 3); Query OK, 1 row affected (0.00 sec) | T2 没有被 T1 阻塞,同样删除了 id = 1 的记录,affected rows 显示为 1. | |
mysql> insert into t (name, age) values(“lihua”, 9), (“humei”, 8); Query OK, 2 rows affected (0.00 sec)Records: 2 Duplicates: 0 Warnings: 0 | T1 插入两条记录,由于自增 id,两条新纪录的 id 分别为 2 和 3. | |
mysql> commit;Query OK, 0 rows affected (0.00 sec) | T1 提交,表 t 中只有 id 为 2 和 3 的记录 | |
mysql> commit;Query OK, 0 rows affected (0.01 sec) | T2 提交成功 | |
mysql> select id, name, age from t;Empty set (0.01 sec) | mysql> select id, name, age from t;Empty set (0.01 sec) | 表 t 中, id 为 2 和 3 的记录也被删除。 |
这里事务 T2 就涉及到乐观事务重试情况下的两个局限性:
针对乐观事务存在的问题,悲观事务通过在执行 DML 过程中加悲观锁,来达到与传统数据库的行为:
悲观事务写入悲观锁,相对乐观事务带来以下开销:
针对悲观锁带来的时延增加问题,在 TiKV 层增加了 pipelined 加锁流程优化,优化前后逻辑对比:
异步 lock 信息 raft 写入流程后,从用户角度看,悲观锁流程的时延降低了;但是从 TiKV 负载的角度,并没有节省开销。
pipelined 优化只是减少了 DML 时延,lock 信息跟优化前一样需要经过 raft 写入多个 region 副本,这个过程会给 raftstore、磁盘带来负载压力。
内存悲观锁针对 lock 信息 raft 写入多副本,做了更进一步优化,总结如下:
从优化逻辑上看,带来的性能提升会有以下几点:
引用下内存悲观锁 RFC In-memory Pessimistic Locks 的介绍:
Here is the general idea:
- Pessimistic locks are written into a region-level lock table.
- Pessimistic locks are sent to other peers before a voluntary leader transfer.
- Pessimistic locks in the source region are sent to the target region before a region merge.
- On splitting a region, pessimistic locks are moved to the corresponding new regions.
- Each region has limited space for in-memory pessimistic locks.
简单理解就是为每个 region 单独维护(只在 leader peer 维护)一个内存 lock table,当出现 region 变动时候例如 Leader transfer、Region merge 会先将 lock table 中的悲观锁通过 raft 同步到其他节点,这个 lock table 有大小限制。
in-memory lock 跟非优化前相比,不会破坏数据一致性,具体的实现细节挺复杂,但是可以简单理解下:
in-memory 悲观锁的设计初衷是在收益与付出之间做的权衡:
锁丢失的原因:in-memory 悲观锁只在 region leader 上维护,这里的锁丢失是指新的 region leader 没有获取到变更前 region 上的悲观锁信息。原因主要是 TiKV 网络隔离或者节点宕机,毕竟对于 region 变更,正常会先通过 raft 将当前 region 的悲观锁同步给其他 region peer。感觉 in-memory 悲观锁比 pipelined 加锁,宕机后锁丢失会更多。
锁丢失的影响(参考Pipelined 加锁流程):
在 pipelined 加锁流程,同样会有悲观锁失效的现象,因为异步写入可能失败,悲观锁没有写成功,但是却通知了上锁成功。
T1 | T2 | OS CLi |
---|---|---|
mysql> begin;Query OK, 0 rows affected (0.00 sec) | mysql> begin;Query OK, 0 rows affected (0.00 sec) | |
mysql> delete from t where id=1;Query OK, 1 row affected (0.00 sec) | ||
mysql> delete from t where id=1; | ||
… | kill -9 tikv | |
Query OK, 1 row affected (19.20 sec) | ||
mysql> commit;Query OK, 0 rows affected (0.00 sec) | ||
mysql> commit;ERROR 1105 (HY000) |
事务 T1 提交失败,详细报错信息如下:
mysql> commit; ERROR 1105 (HY000): tikv aborts txn: Error(Txn(Error(Mvcc(Error(PessimisticLockNotFound { start_ts: TimeStamp(433149465930498050), key: [116, 128, 0, 0, 0, 0, 0, 1, 202, 95, 114, 128, 0, 0, 0, 0, 0, 0, 1] })))))
这里事务 T1 先加锁成功,事务 T2 被阻塞,kill tikv 导致 leader transfer,新的 leader 没有事务 T1 的悲观锁信息,然后事务 T2 被解除阻塞,并提交成功。事务 T1 提交失败,但不会影响数据的一致性。
所以如果业务中依赖这种加锁机制,可能导致业务正确性受影响。如下使用场景:
mysql> begin; mysql> insert into tb values(...) 或者 select 1 from tb where id=1 for update; ...加锁成功... ...业务依赖以上加锁成功做业务选择... ...在锁丢失场景可能多个事务都能加锁成功导致出现不符合业务预期的行为... mysql> commit;
如果对于成功率和事务过程中执行返回结果有强需求或者依赖的业务,可选择关闭内存锁(以及 pipelined 写入)模式。
TiKV 配置文件:
[pessimistic-txn] pipelined = true in-memory = true
只有 pipelined 和 in-memory 同时打开才能开启内存悲观锁。
可以在线动态开启、关闭:
> set config tikv pessimistic-txn.pipelined='true'; > set config tikv pessimistic-txn.in-memory='true';
Grafana 查看 in-memory lock 的写入情况,在 {clusterName}-TiKV-Details->Pessimistic Locking 标签下:
每个 region 的 in-memory 锁内存固定限制为 512 KiB,如果当前 region 悲观锁内存达到限制,新的悲观锁写入将回退到 pipelined 加锁流程(在典型 TP 场景下,很少会超过这个限制)。
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update sbtest1 set k=k+1 limit 10000000; Query OK, 10000000 rows affected (3.26 sec) Rows matched: 10000000 Changed: 10000000 Warnings: 0
由于大量悲观锁写入,悲观锁内存达到限制值,监控中 full 值大量出现。
回退到 pipelined 写入流程,通过 raft 写入多副本,Rockdb 的 lock CF 出现 lock 信息,在 {clusterName}-TiKV-Details->RocksDB - kv 标签下。
对乐观锁、悲观锁、pipelined 写入、in-memory lock 进行压力测试。
类型 | cpu | 内存 | 磁盘 | 网卡 | NUMA | 节点数 | 机器数 |
---|---|---|---|---|---|---|---|
TiKV | 96线程 | 384G | 2 * 2.9T NVME | 40000Mb/s | 2 node | 6 | 3 |
TiKV 部署:在每块 NEME 盘上部署一个 TIKV 节点,分别绑定一个 NUMA node,单台机器 2 个 TiKV 节点,配置参数如下(变动的参数只跟压测的事务类型有关)。
server_configs: tikv: pessimistic-txn.in-memory: true pessimistic-txn.pipelined: true raftdb.max-background-jobs: 6 raftstore.apply-pool-size: 6 raftstore.store-pool-size: 6 readpool.coprocessor.use-unified-pool: true readpool.storage.normal-concurrency: 16 readpool.storage.use-unified-pool: true readpool.unified.max-thread-count: 38 readpool.unified.min-thread-count: 5 rocksdb.max-background-jobs: 8 server.grpc-concurrency: 10 storage.block-cache.capacity: 90G storage.scheduler-worker-pool-size: 12
TiDB、pd 独立部署,均为高配置服务器,其中 TiDB 节点足够多,能使压测性能瓶颈集中在 TiKV 上,使用 LVS DR 模式做负载均衡。
测试工具 sysbench,压测脚本 oltp_write_only,64 张表,1000w 数据,直观比较各种模式下性能差异。
压测结果 TPS:
压测结果 Latency:
从压测结果上来看:
对比下 in-memory、pipelined 两个特性,对于悲观锁的性能提升。
TPS 提升:
减少 Latency:
从压测数据来看,v6.0.0 版本的内存悲观锁是非常有吸引力的新特性。
通过减少 DML 时延、避免悲观锁 raft 写入多副本、减少 raftstore 处理压力以及磁盘带宽,能达到可观的写入性能提升:
在内存悲观锁的使用中,要注意锁丢失问题,如果影响业务的正确性逻辑,应关闭 in-memory lock 与 pipelined 写入这两个悲观事务特性。
官方文档:内存悲观锁
内存悲观锁 RFC In-memory Pessimistic Locks
Tracking Issue: In-memory Pessimistic Locks