在 v6.0.0 版本,针对悲观事务引入了内存悲观锁的优化(In-memory lock),从压测数据来看,带来的性能提升非常明显(Sysbench 工具压测 oltp_write_only 脚本)。
TiDB 事务模型从最初的乐观事务到悲观事务;在悲观事务上,又针对悲观锁进行的 ”Pipelined 写入“ 和 ”In-memory lock“ 优化,从功能特性上可以看出演进过程(参考TiDB 事务概览)。
乐观事务在提交时,可能因为并发写造成写写冲突,不同设置会出现以下两种不同的现象:
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]
这里事务 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 提交失败,详细报错信息如下:
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 标签下:
in-memory-success.png
每个 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
in-memory-full.png
由于大量悲观锁写入,悲观锁内存达到限制值,监控中 full 值大量出现。
rocks-locks.png
回退到 pipelined 写入流程,通过 raft 写入多副本,Rockdb 的 lock CF 出现 lock 信息,在 {clusterName}-TiKV-Details->RocksDB - kv 标签下。
对乐观锁、悲观锁、pipelined 写入、in-memory lock 进行压力测试。
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:
oltp_write_only_TPS.png
压测结果 Latency:
oltp_write_only_LATENCY.png
从压测结果上来看:
对比下 in-memory、pipelined 两个特性,对于悲观锁的性能提升。
oltp_write_only_tps_promotion.png
TPS 提升:
oltp_write_only_latency_reduce.png
减少 Latency:
从压测数据来看,v6.0.0 版本的内存悲观锁是非常有吸引力的新特性。
通过减少 DML 时延、避免悲观锁 raft 写入多副本、减少 raftstore 处理压力以及磁盘带宽,能达到可观的写入性能提升:
在内存悲观锁的使用中,要注意锁丢失问题,如果影响业务的正确性逻辑,应关闭 in-memory lock 与 pipelined 写入这两个悲观事务特性。
官方文档:内存悲观锁
内存悲观锁 RFC In-memory Pessimistic Locks
Tracking Issue: In-memory Pessimistic Locks