0
1
0
0
专栏/.../

内存悲观锁

 jiyf  发表于  2022-05-12

背景

在 v6.0.0 版本,针对悲观事务引入了内存悲观锁的优化(in-memory lock),从压测数据来看,带来的性能提升非常明显。

TiDB 事务模型从最初的乐观事务到悲观事务;在悲观事务上,又针对悲观锁进行的 ”pipelined 写入“ 和 ”in-memory lock“ 优化,从功能特性上可以看出演进过程。

乐观事务

乐观事务在提交时,可能因为并发写造成写写冲突,不同设置会出现以下两种不同的现象:

  • 关闭乐观事务重试,事务提交失败:也就是执行 DML 成功(不会被阻塞),但是在执行 commit 时候失败,表现出与 MySQL 等数据库不一致的行为。
  • 开启乐观事务重试,自动重试后返回成功,但是因为重试 DML 使用的 start_ts 是重新获取的,不是事务开始的 start_ts,也就是说实际执行结果相当于同一个事务中读和写是使用不同的 start_ts,执行结果可能跟预期不一致。

悲观事务

针对乐观事务存在的问题,悲观事务通过在执行 DML 过程中加悲观锁,来达到与传统数据库的行为:

  • 并发执行 DML,对同一行数据进行更改,先执行者会加悲观锁,后执行者被锁阻塞
  • 悲观锁避免了写冲突的问题,使得 commit 顺利完成

悲观事务写入悲观锁,相对乐观事务带来以下开销:

  • 悲观锁写入 TiKV,增加了 RPC 调用流程并同步等待悲观锁写入成功,导致 DML 时延增加
  • 悲观锁信息会通过 raft 写入多个副本,给 TiKV raftstore、磁盘等带来处理压力

pipelined

针对悲观锁带来的时延增加问题,在 TiKV 层增加了 pipelined 加锁流程优化,优化前后逻辑对比:

  • 优化前:满足加锁条件,等待 lock 信息通过 raft 写入多副本成功,通知 TiDB 加锁成功
  • pipelined :满足加锁条件,通知 TiDB 加锁成功、异步 lock 信息 raft 写入多副本(两者并发执行)

异步 lock 信息 raft 写入流程后,从用户角度看,悲观锁流程的时延降低了。

in-memory

pipelined 优化只是减少了 DML 时延,lock 信息跟优化前一样需要经过 raft 写入多个 region 副本,这个过程会给 raftstore、磁盘带来负载压力。

内存悲观锁针对 lock 信息 raft 写入多副本,做了更进一步优化,总结如下:

  • lock 信息只保存在内存中,不用写入磁盘
  • lock 信息不用通过 raft 写入多个副本,只要存在于 region leader
  • lock 信息写内存,延迟相对于通过 raft 写多副本,延迟极小

从优化逻辑上看,带来的性能提升会有以下几点:

  • 减小 DML 时延
  • 降低磁盘的使用带宽
  • 降低 raftstore CPU 消耗

实现原理

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、Region split,会先将 lock table 中的悲观锁通过 raft 同步到其他节点,这个 lock table 有大小限制。

in-memory lock 跟非优化前相比,不会破坏数据一致性,具体的实现细节挺复杂,但是可以简单理解下:

  • in-memory 悲观锁正常存在 region leader lock table 情况下

    • 对于读写 leader,跟普通悲观锁读写一致
    • 对于 follow read,基于 snapshot 读,只有 prewrite lock 会影响读取结果,而 prewrite 的数据是会同步到 follower 的,所以仍然没问题
  • in-memory 悲观锁丢失

    • 对于 write 操作,事务提交 prewrite 阶段会检查版本冲突,有冲突会因为冲突提交失败,没冲突正常提交
    • 对于 read 操作,同上面 follower read,悲观锁不会影响读

锁丢失

in-memory 悲观锁只在 region leader 上维护,所以这里的锁丢失是指新的 region leader 没有获取到变更前 region 上的悲观锁信息,事务在 region leader 变更前上的锁,不能阻塞后来事务的更改(悲观锁失效了)。

在 pipelined 加锁流程,同样会有悲观锁失效的现象,因为异步写入可能失败,悲观锁没有写成功,但是却通知了上锁成功。

in-memory 悲观锁丢失产生的原因主要是 TiKV 网络隔离或者节点宕机,毕竟对于 region 变更,正常会先通过 raft 将当前 region 的悲观锁同步给其他 region peer。感觉 in-memory 悲观锁比 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 被解除阻塞,并提交成功。

所以如果业务中依赖这种加锁机制,可能导致正确性受影响。如下使用场景:

mysql> begin;
mysql> insert into tb values(...) 或者 select 1 from tb where id=1 for update;
...加锁后,达到串行执行业务逻辑的目的...
mysql> commit;

开启 in-memory

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 的写入情况:

in-memory-success.png

内存限制

每个 region 的 in-memory 锁内存限制为 512 KiB,如果当前 region 悲观锁内存达到限制,新的悲观锁写入将回退到 pipelined 加锁流程。

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 信息。

性能测试

对乐观锁、悲观锁、pipelined 写入、in-memory lock 进行压力测试。

TiDB、TiKV 独立部署,均为高配置服务器,其中 TiDB 节点足够能使压测性能瓶颈集中在 TiKV 上。

测试工具 sysbench,压测脚本 oltp_write_only,64 张表,1000w 数据;不列出具体机器配置信息,只直观比较各种模式下性能差异。

压测结果 TPS:

oltp_write_only_TPS.png

压测结果 Latency:

oltp_write_only_LATENCY.png

从压测结果上来看:

  • 性能排行从高到底:in-memory > optimistic > pipelined > pessimistic
  • 在压测线程较小时,in-memory 和 optimistic 性能接近,等到并发增大,可能是 optimistic 事务冲突重试的原因导致 in-memory 后来居上
  • 随着并发数增大,TiKV 磁盘 iops、带宽很快增长,pessimistic 和 pipelined 磁盘负载较早出现压力,后面时延增加较快,对应 TPS 增长相对缓慢
  • 当接近 TiKV 磁盘性能瓶颈时,in-memory 和 optimistic 能支撑集群更大的 TPS。

悲观锁优化

对比下 in-memory、pipelined 两个特性,对于悲观锁的性能提升。

oltp_write_only_tps_promotion.png

TPS 提升:

  • in-memory 提升明显,始终维持在一个较高值 35% 以上

    • 同并发下 Latency 减少,对应 TPS 增长
    • 高并发下,减少磁盘 io 压力、减少了 raftstore 压力
  • pipelined 提升在 10% 左右,在较小并发时异步写入 Latency 减少,支撑了较大的 TPS 提升;当磁盘压力增大,慢慢出现性能瓶颈,提升越来越小。

oltp_write_only_latency_reduce.png

减少 Latency:

  • 在并发小时,时延提升明显,分别能到到 20%、10% 的提升。

  • 在并发增大后,磁盘出现较大压力,由于时延增加太大,总提升越来越不明显

    • in-memory 维持在 10% 以上
    • pipelined 降到 5% 以下

总结

从压测数据来看,v6.0.0 版本的内存悲观锁是非常有吸引力的新特性。

通过减少 DML 时延、避免悲观锁 raft 写入多副本、减少 raftstore 处理压力以及磁盘带宽,能达到可观的写入性能提升:

  • Tps 提升 30% 上下
  • 减少 Latency 在 15% 上下

在内存悲观锁的使用中,要注意锁丢失问题,如果影响业务的正确性逻辑,应关闭 in-memory lock 与 pipelined 写入这两个悲观事务特性。

参考

官方文档:内存悲观锁

内存悲观锁 RFC In-memory Pessimistic Locks

Tracking Issue: In-memory Pessimistic Locks

0
1
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论