共计 11427 个字符,预计需要花费 29 分钟才能阅读完成。
作者介绍:童牧。
绪论
在程序员的生涯中,bug 始终随同着咱们,尽管咱们冀望写出完满的程序,然而再优良的程序员也无奈保障本人可能不写出 bug。因而,咱们为程序编写测试,通过提前发现 bug 来进步最终交付程序的品质。我从在 PingCAP 的工作中感触到,做好数据库和做好数据库测试是密不可分的,本次分享,咱们将在第一讲的事务隔离级别的根底上,对数据库事务的测试进行钻研,次要讲述,在 PingCAP 咱们是如何保障事务的正确性的。
因为咱们保障事务正确性的办法比拟多,所以本次咱们会着重解说 Jepsen 和 Elle,而其余办法则是作为补充,我也会简略阐明他们的做法和优缺点。我将事务测试的办法划分为以下几个类别:
- 实践正确性的验证
- 基于不变量的正确性验证
- 对执行历史进行查看的验证
- 辅助测试伎俩
回顾 Percolator 提交协定
Percolator
在开始讲述测试方法前,咱们先对 Percolator 提交协定进行回顾,感受一下这一协定下的复杂性。Percolator 提交协定应用 2PC 的提交形式来保障事务的原子性,然而在 shared-nothing 架构下,没有一个节点有全局的事务信息,事务的状态信息被打散到了每个 Key 上,使得对于 Key 状态的解决变得更加简单。
图 1 – Percolator 下的两阶段提交
图 1 是 Percolator 下的两阶段提交的流程,在 Prewrite 阶段,将数据写入到存储引擎中,这些键值对可能存储在不同的实例上,其中 Primary Key(PK) 是这个事务中的第一个 Key,是事务胜利的原子性标记。Prewrite 阶段写入的数据蕴含了事务理论要写入的数据和事务的 PK(图中为 x),所以一旦 Prewrite 阶段实现,则阐明这个事务曾经写入胜利。然而咱们还须要 Commit 阶段来使得这一胜利的事务对外可见,在 Commit 阶段中,会先 Commit PK,这一操作将事务标记为胜利状态;而后 Commit 其余的 Key,让整个事务都能够被间接读取。
图 2 – Percolator 下的读写关系解决
然而在这一提交协定下,另一个事务可能从任意一个工夫点对正在提交的事务进行读取,图 2 给出了从 4 个工夫点对事务进行读取的状况。read1 时,事务还没有 prewrite,所以读取到旧的值即可;read2 时,读事务看到了 Prewrite 的后果,然而对应的 Key 还没有被 Commit,这时候会查问这一事务对应的 PK 来确认状态,但此时 PK 也还未提及,因而须要判断所读取到的事务是否过期,如果没有过期,那么期待一段时间后重试,如果曾经过期,则会将这个事务 Rollback 掉;read3 和 read2 的过程类似,然而因为查到了 PK 是曾经提交的状态,所以会读取到这个事务的内容,并且把这个事务中读取到的未提交的 Key 提交掉;read4 读到的 Key 都是曾经提交状态的,间接读取到数据即可。
一个毁坏原子性的 bug
原子性是事务正确性的重要保障之一,比方在一个转账事务中,如果一个事务只有一半胜利了,可能就会呈现一个账号扣了钱,而另一个账号没有收到钱的状况。
图 3 – 原子性被毁坏 bug
图 3 是一个原子性被毁坏的 bug,是三个事务并发执行产生的状况。Txn1 执行一条语句尝试锁住 x-z 这 3 个 Key,然而语句运行失败,其中曾经上锁的 x-y 须要被异步撤销,随后第二条语句会从新选出一个主键,这里假如新主键是 a,同时也尝试对 y 加锁,此时会阻止异步清锁继续执行。Txn3 对 y 加锁时读到了 stmt1 加的锁,resolve 时发现 pk 曾经被 rollback,因为谬误读取了 Txn2 产生的缓存,Txn3 误 rollback 掉了 stmt2 加上的 y,导致了事务原子性的毁坏。在本来的 Percolator 提交协定中,并没有蕴含主键更换的逻辑,而为了防止在加锁失败时重启事务,咱们在实现中是有这一优化的,也因而使得所实现的事务模型更加简单。
这里的问题是没有思考到执行失败的语句可能会造成事务中选取出新的主键,但原始的 Percolator 提交协定并没有包含主键的更换,即是说咱们在实现分布式事务的实现中所做的优化,使得这一模型变得更加简单了。
实践正确性的验证
咱们应用 TLA+ 对实践正确性进行验证,TLA+ 是为并行和分布式系统设计的建模语言,可能模拟出所有可能产生的状况,来保障实践的正确性。
图 4 – 形式化验证过程
应用 TLA+ 对模型进行形式化验证须要先定义初始状态,而后定义 Next 过程和验证正确性的 THEOREM。Next 指的是可能会产生的过程,在一个并行零碎中,尽管串行过程会一一产生,然而并行过程产生的先后则不可被预测。图 4 是形式化验证的运行过程,执行一次 Next 过程,而后应用定义的 THEOREM 验证状态是否呈现问题。因为 Next 是可能会产生的过程,所有一次 Next 调用可能会产生多种执行门路,TLA+ 会搜寻所有可能的门路,确保在这个并行零碎中,任意一种程序都不会违反咱们所定义的束缚。
尽管 TLA+ 可能从实践上验证正确性,但这一办法也有着各种限度:
- 复杂度随过程量指数级提醒,如果搜寻门路过于简单,要实现冀望深度的搜寻可能须要耗费大量的工夫。
- 实践上的正确不能避免实现上的谬误。
线性一致性和 Snapshot Isolation
首先咱们须要明确,线性一致性和事务的隔离性是不相干的两个概念。
图 5 – 非可线性化
可线性化(Linearizability)本来是多处理器并行操作内存时的概念,后续被引入到数据库中来,对单个元素的事务提出了两点要求:
- 单个元素上的事务可串行化;
- 单个元素上,如果 Txn2 开启的工夫点晚于 Txn1 提交实现的工夫点,Txn2 在串行化的队列中肯定在 Txn1 之后。
图 5 是一个可串行化然而不可线性化的例子,可串行化的执行程序是 Txn2 -> Txn1。尽管 Txn2 开启的工夫点晚于 Txn1 提交的工夫点,然而在可串行化的队列中,Txn2 在 Txn1 之前。Spanner 提出了内部一致性的概念,并且认为内部一致性是强于可线性化的一致性规范,因为其对事务的先后束缚可能拓展到多元素的操作上,内部一致性也被了解为一种可线性化的定义,他们束缚的成果大体上雷同。当咱们综合思考隔离性和一致性时,就会发现可串行化并不是现实中的完满的隔离与一致性级别,例如图 5 中,Txn1 是一个进行生产的事务,在进行生产后,还有事务读取到了生产前的余额,显然这在很多场景下是无奈被承受的。Jepsen 的一致性模型中,在可串行化之上又设定了一个隔离与一致性级别。当一个数据库同时满足可串行化和可线性化时,将其称为严格可串行化(Strict Serializable)。
图 6 – 可线性化与内部一致性
如图 6 所示,在可线性化的执行下,Txn3 将 x 从 1 改写成 2,并且提交工夫是从 ts1 到 ts2,那么对于客户端而言,早于 ts1 的工夫点和晚于 ts2 的工夫点是状态确认的,而在 ts1 和 ts2 之中,因为不能确定 Txn3 理论失效的工夫点,所以 x 的值处于不可知的状态,读取到 1 或 2 都是容许的。
Snapshot Isolation(SI) 是一个被宽泛采纳的隔离级别,同时 TiDB 也是一个 SI 隔离级别的数据库,所以咱们也在议论事务测试之前,须要搞清楚 SI 隔离级别是如何应用事务间依赖进行定义的。须要留神的是,SI 是一个先射箭再画靶的隔离级别,所以咱们对其的定义的指标是防止如“SI 就是事务从一个快照读”这种含糊的语言,而是要给出绝对主观的靶子。
图 7 – 偏序依赖
为了定义 SI,须要引入了一个新的事务依赖,叫事务开始依赖,这一依赖反映了一个事务提交工夫点和另一个事务开启工夫点之间的偏序关系,偏序关系往往和其余依赖同时产生,并且具备传递性。如图 7 所示,Txn2 读取到了 Txn1 的写入,既能够阐明 Txn2 的开始工夫点晚于 Txn1 提交失效的工夫点。须要留神的是,这里的偏序指的是数据库外部的工夫点,须要和线性一致性级别综合思考,能力从内部观测到的程序推断出数据库外部的工夫点的偏序关系,即如果图 7 中,Txn2 没有读取到 Txn1 的写入,即便连贯数据库的客户端可能必定 Txn2 是在 Txn1 齐全提交后才开启的,也不能失去 c1 ≺t s2 的论断,这也有可能是数据库不提供可线性化的线性一致性。
Adya 将 SI 的隔离级别称为 PL-SI,是在 PL-2 上附加了对数据库外部工夫点的束缚,其包含事务开始依赖和一部分读后写依赖。
图 8 – G-SIa 异样
SI 的艰深了解是,一个事务会取有一个快照,其读操作在这个快照上进行,读取的作用范畴是工夫点小于等于这个快照工夫点的所有写入。那么对于两个存在偏序关系的事务,如果 c1 ≺t s2,那么 Txn2 则须要读取到 Txn1 的批改,同时 Txn1 不应该读取到 Txn2 的写入内容(从偏序关系的传递性能够推导 s1 ≺t c1 ≺t s2 ≺t c2),然而图 8 中的 Txn1 却读取到了 Txn2 的写入内容,从而毁坏了 SI 的语义。从另一个角度来了解,事务开始依赖和 WR 依赖和 WW 依赖一样,反映的是事务间的先后关系,当这些关系呈现环的时候,他们的先后关系就无奈被确认了,G-SIa(Interference) 异样指的是就是 WR, WW 和 S 依赖之间造成了环。
图 9 – G-SIb 异样
图 9 展现了 G-SIb(Missed Effects) 的异常现象,指的是 WR, WW, RW 和 S 依赖造成了环,然而其中只容许有一个 RW 依赖。这一景象能够了解为,一个事务没有残缺的读取到另一个事务的写入,图中 Txn2 写入了两个值,然而 Txn1 只读取到一个值,另一个值读取的是旧版本,产生了 RW 依赖。之所以只容许有一个 RW 依赖,是因为一个 RW 依赖就足以查看住这种问题,而两个 RW 则会带来如 Write Skew 的误判。
PL-SI 须要在 PL-2 的根底上避免 G-SIa 和 G-SIb 的产生,这点和对标可反复读的 PL-2.99 是有些轻微的差异的,请小心看待。
Jepsen
提到事务测试,就不得不提 Jepsen。Jepsen 是 TiDB 质量保证的重要一环,除了每一次发版,在日常测试中,Jepsen 也在不间断的运行。
图 10 – 束缚查看的思维
什么是 Jepsen,为什么 Jepsen 是无效、高效的?图 10 是作者的一些想法,如果咱们要验证由 ① 和 ② 组成的方程组的求解是否正确,咱们能够认真地查看求解过程,也能够将后果代入到原式中,查看是否合乎给定条件。我置信大部分人在查看方程后果的时候,都会抉择后者,因为后者可能简略无效的查看出谬误。而 Jepsen 的思维,就是通过设计一些“方程”,将求解交给数据库,最初通过一些约束条件查看数据库是否求解的正确。
Bank
图 10 – Jepsen Bank
Jepsen Bank 是一个十分经典的测试,其模仿的状况也很简略,图 10 是这个用例运行的形式,在一张表中有许多用户和他们的余额纪录,同时会有许多事务并发的进行转账操作。Bank 的查看条件也非常简单,就是所有用户的账户总余额不变。并且在 SI 隔离级别中,任意一个快照都应该满足这一束缚,如果某个快照不合乎这一束缚,则阐明可能呈现了 G-SIb(Missed Effects) 的异常现象,读者能够思考一下起因。Jepsen 会在运行过程中,定时开启事务,查问总余额,查看是否毁坏束缚。
Bank 是一个比拟靠近事实业务的测试场景,逻辑了解简略,然而因为并发结构,在理论运行过程中可能会造成大量的事务抵触,Bank 并不关怀数据库如何解决这些抵触,会不会带来事务失败,大部分谬误最终都会反馈到余额之上。
Long Fork
图 11 – Jepsen Long Fork
Long Fork 是一个为 SI 隔离级别设计的事务测试场景,其中有两种事务,写事务会对一个寄存器进行赋值,而读事务则会查问多个寄存器,最初剖析这些读事务是否读到了毁坏 PL-SI 的状况。图 11 中,Txn1 和 Txn2 是写事务,Txn3 和 Txn4 是读事务,图 11 中存在 G-SIa,毁坏了 PL-SI,然而在这里咱们须要做一些假如能力发现环。
图 12 – Jepsen Long Fork
图 12 是对图 11 例子的剖析,依据 WR 依赖,咱们能够确定 c2 ≺t s3 和 c1 ≺t s4。然而因为咱们不晓得 Txn3 和 Txn4 的开始工夫点,所以咱们须要进行假如,如果 s3 ≺t s4,则如图中左侧的假如,从偏序的传递性,能够推导出 c2 ≺t s4,于是就能发现一个由 S 依赖和 RW 依赖组成的环;如果 s4 ≺t s3,则如图中右侧所示,也能发现一个 G-SIb 异样。如果 s3 和 s4 相等,那么阐明 Txn3 和 Txn4 是从会读取到同样的内容,然而理论读取到的内容却呈现了矛盾,也存在异样,具体找到环的步骤就留给读者自行推导了。
小结
Jepsen 提供了一些通过束缚查看来发现异常的办法,并且设计了一系列测试场景。有比拟好的覆盖率,其长处有:
- 着眼于约束条件,简化了正确性的验证;
- 测试的效率高。
图 13 – Missing Lost Update
然而 Jepsen 也有他的不足之处,图 13 表白了一个在 Bank 下的 Lost Update 异样,T2 的转账失落了,然而最初并不能从后果上查看出这个异样,因为余额的总和没有变。
History Check
通过 BFS 进行检测
有一类测试方法是通过 history check 来进行的,其目标是为了寻找到更多的异常现象,尽可能的开掘执行历史中的信息。「On the Complexity of Checking Transactional Consistency」这篇论文就钻研了从执行历史剖析事务一致性和其复杂度。
图 14 – 可序列化的检测
可串行化的检测遵循其字面意思,咱们只须要找到一个执行序列,可能让所有的事务串行执行的即可,那么很天然的,只须要采纳一个广度优先搜寻(BFS),就能够查看是否是串行的。然而问题在于,这种办法在假如事务执行满足 Sequential Consistency 的前提下,复杂度为 O(deep^(N+3)),其中 deep 为每个线程执行事务的数量,N 为线程数。
图 15 – 通过 guard 变量转化 SI 检测
检测可串行化是简略的,然而要检测一个历史序列是否合乎 SI 隔离级别就无奈间接进行,如图 15 所示,论文通过增加 guard 变量,从而能够通过上文提到的对可串行化的检测来判断 SI 的读写关系是否正当。以 Lost Update 为例,两个事务都对 x 进行了批改,别离写入了第一个版本和第二个版本,因而插入两个 guard 变量,将这两个事务对 x 的这两个批改串联起来,不容许其中插入对 x 的批改,只有验证图中 (b) 的执行历史可能满足可串行化的要求,就能够阐明 (a) 的执行历史满足 SI 的要求。
Elle
为了解决了解艰难和复杂度低等问题,Jensen 的作者 Kyle Kingsbury 发表了名为 Elle 的事务一致性检测办法。
Elle 通过环检测和异样检测的形式,来验证事务的一致性和隔离性是否达到咱们所指定的要求,它的工作形式是尽可能的找出执行历史中的异样,而非尝试找出一个可串行的执行序列,因而在效率是有着显著的劣势,但绝对的,这一测试方法的过程较为简单。
图 16 – 简略的 G1c 检测
图 16 就是检测出 G1c 的例子,依据咱们在第一讲所探讨的实践,咱们不须要依照可串行化的字面定义,去寻找一个执行序列,而只须要检测是否呈现某些不容许呈现的状况。尽管从直觉上,咱们并不能必定不呈现某些异样就等同于字面上的可串行化,然而从在隔离级别定义的钻研根底上来说,通过查看异样来判断是否可串行化是正当的。此外,对于事务测试而言,找到异样曾经是咱们所冀望的后果了。
图 17 – Elle 所设计的模型
如图 17 所示,Elle 设计了四个模型,别离是寄存器、加法计数器、汇合和列表,其对执行历史的检测的办法大同小异,前面会以 List Append 作为对象开展解说。
Elle 中的事务有生成和执行两个阶段,在生成阶段,Elle 会随机产生这个事务须要读写的内容,这些预生成好的读写会在执行阶段失去后果。即一个事务在历史中会存在两条记录,生成阶段的 :invoke 和执行阶段的 :ok/:fail/:info 中的一个。
- :invoke,事务被生成,之后会被执行。
- :ok,事务执行并确定提交胜利。
- :fail,确定事务没有被提交。
- :info,事务状态不肯定(例如提交时呈现连贯谬误)。
为了剖析线性一致性,Elle 在剖析过程中,思考 Adya 所定义的事务间读写关系的依赖的同时,还给出了和工夫相干的依赖,工夫上的依赖在满足对应的线性一致性的前提下也可能反馈事务的先后关系。
- Process Depend,一个线程中事务执行的先后严格遵循执行程序,在反对 Sequential Consistency 的零碎中能够应用。
- Realtime Depend,所有线程中事务执行的先后严格遵循执行工夫,在反对 Linearizability 的零碎中能够应用。
图 18 – 对工夫依赖的环检测
图 18 展现了一个合乎 Sequential Consistency 但不合乎 Linearizability 的例子,客户端分两个线程执行事务,然而客户端晓得不同线程间事务的严格程序,留神在分布式数据库中,这些线程可能连贯到的是不同的数据库节点。如果零碎只满足 Sequential Consistency,那么对应的依赖图应该如左下方所示,其中并没有呈现环;然而如果零碎是满足 Linearizability 的,那么依赖图就会变成右下方所示,Txn3 和 Txn4 之间造成了环,换句话说,Txn4 产生在 Txn3 之前,然而却读取到了 Txn3 的写入,并且这一异样,单纯从 Adya 所定义的读写关系中是发现不了的。
接下来咱们会通过几个例子来解说,Elle 具体是如何工作和发现数据库在事务处理上的异样的。
{:type :invoke, :f :txn, :value [[:a :x [1 2]] [:a :y [1]]], :process 0, :time 10, :index 1}
{:type :invoke, :f :txn, :value [[:r :x nil] [:a :y [2]]], :process 1, :time 20, :index 2}
{:type :ok, :f :txn, :value [[:a :x [1 2]] [:a :y [1]]], :process 0, :time 30, :index 3}
{:type :ok, :f :txn, :value [[:r :x []] [:a :y [2]]], :process 1, :time 40, :index 4}
{:type :invoke, :f :txn, :value [[:r :y nil]], :process 0, :time 50, :index 5}
{:type :ok, :f :txn, :value [[:r :y [1 2]]], :process 0, :time 60, :index 6}
例 1 – 蕴含 G-SIb 的执行历史
图 19 – G-SIb 的依赖图
例 1 是一个呈现 G-SIb 异样的历史,这里用 Clojure 的 edn 的格局来示意,通过 :process 属性,能够推断出第一行和第三行是 Txn1,第二行和第四行是 Txn2,第五行和第六行是 Txn3。从 Txn1 和 Txn2 从 :invoke 到 :ok 状态的 :time 属性来看,他们可能是并行的。而 Txn3 则是在这两个事务都胜利提交之后才开启的,从 Txn3 读取到的 [[:r :y [2 1]] 来看,因为 List 是依照 Append 顺序排列的,所以能够判断 Txn2 产生在 Txn1 之前,然而 Txn2 又读到了 Txn1 写入之前的 x,这里产生了 RW 依赖,呈现了环。在这个例子中,Elle 利用 List 的个性,找出了本来不容易判断的 WW 依赖。
{:type :invoke, :f :txn, :value [[:a :x [1 2]] [:a :y [1]]], :process 0, :time 10, :index 1}
{:type :invoke, :f :txn, :value [[:a :x [3]] [:a :y [2]]], :process 1, :time 20, :index 2}
{:type :ok, :f :txn, :value [[:a :x [1 2]] [:a :y [1]]], :process 0, :time 30, :index 3}
{:type :ok, :f :txn, :value [[:a :x [3]] [:a :y [2]]], :process 1, :time 40, :index 4}
{:type :invoke, :f :txn, :value [[:r :x nil]], :process 2, :time 50, :index 5}
{:type :ok, :f :txn, :value [[:r :x [1 2 3]]], :process 2, :time 60, :index 6}
{:type :invoke, :f :txn, :value [[:r :x nil]], :process 3, :time 70, :index 7}
{:type :ok, :f :txn, :value [[:r :x [1 2]]], :process 3, :time 80, :index 8}
例 2 – 可能毁坏 Linearizability 的执行历史
图 20 – 可能毁坏 Linearizability 的依赖图
例 2 是一个可能毁坏 Linearizability 的例子,从图 20 上方的依赖图来看,呈现了 RW,WR 和 Realtime 依赖组成的环,即 G-SIb 景象,然而这个环里有一个 Realtime 依赖,这个零碎还有可能是因为毁坏了 Linearizability 而产生这个环的。毁坏 Linearizability 的状况能够从图的上面更加清晰的发现,当遇到这种状况时,因为不能断言是呈现了哪一种异样,Elle 会汇报可能产生的异样品种。
图 21 – Elle 论文中的例子
图 21 是 Elle 的论文中给出的例子,令第一行为 Txn1,第二行为 Txn2,第三行为 Txn3。Txn1 和 Txn2 之间存在 Realtime 关系,而 Txn2 对 Key 为 255 的 List 的读取中没有 Txn3 的写入,阐明其中存在一个 RW 依赖,而 Txn1 则读取到了 Txn3 的写入,于是呈现了和图 20 中相似的状况,这里不再开展剖析了。
MIKADZUKI
Elle 展现了依赖图在测试中的巨大作用,在 PingCAP 外部,咱们尝试通过另一种形式来通过依赖图对数据库进行测试。回顾 Elle 的流程,是执行后剖析执行历史,将其转化为依赖图后,判断其是否合乎某个隔离级别或一致性级别。MIKADZUKI 的做法正好相同,尝试在有依赖图的状况下,生成出执行历史,比照生成的执行历史和数据库理论执行的体现,就能够发现数据库是否失常。
图 22 – MIKADZUKI 依赖图档次
图 22 是 MIKADZUKI 外部的图的档次,Process 中的事务会串行执行,而 Process 间的事务会并行执行。同一个 Process 下的事务,首尾之间存在 Realtime 的关系,而 Process 间的事务会生成 Depend,Depend 和 Realtime 都代表了事务执行的先后关系,所以在生成时,这两类依赖不会让事务造成环。
图 23 – MIKADZUKI 的执行流程
图 23 是 MIKADZUKI 的执行流程,一轮有四个阶段:
- 生成一张没有环的 Graph;
- 为 Graph 中的写申请填充随机生成的读写数据,数据以 KV 模式表白,其中 Key 是主键索引或惟一索引,Value 是整行数据;
- 从写申请依据事务间的依赖,揣测出读申请该当读取到的后果;
- 依照图的事务依赖形容,并行执行事务,如果读取到与第三步预测中不同的后果,则阐明后果有误。
这一测试方法帮忙咱们发现过一些问题,在试验前期,咱们尝试增加制作成环的依赖,WW 依赖所成的环,在失常执行下会呈现死锁,而死锁检测则是以往的测试方法不容易发现的,因为死锁检测卡住不会导致任何异样。
小结
通过对执行历史的查看,咱们可能尽可能的发现异常,得益于对隔离级别和一致性的学术研究,对历史的查看复杂度大幅升高。Elle 进而设计了一些模型,可能为剖析事务间关系提供线索,从而使得对残缺历史的查看变得可能且无效。
谬误注入
墨菲法令宣称——任何可能产生谬误的中央都会产生谬误,即再小的产生谬误的概率,也总有一天会产生。另一方面,测试环境的规模和数量又远小于生产环境,失常状况下,绝大部分的谬误都将产生在生产环境,那么简直所有因而引发的常见的零碎的 bug 也将产生在生产环境下,这显然是咱们所不冀望产生的。
为了将 bug 止于测试环境,咱们会应用一些办法进行故障模拟,次要包含:
- Failpoint,为过程注入谬误。
- Chaos Test,模仿外界产生的故障,更靠近真实情况。
Failpoint
Failpoint 是用于向过程中注入一些问题的测试伎俩,能够在编译期决定是否关上,失常公布的版本是敞开 Failpoint 的。TiDB 通过折叠代码来管制 Failpoint,TiKV 则通过宏和编译时的环境变量进行管制。通过 Failpoint,咱们能够高效的模仿一些平时常见但又存在产生可能的状况:
- 存在一些难以被拜访到的代码门路,并且可能是正确性的重要保障;
- 程序可能在任意节点被 kill;
- 代码执行可能在任意一个节点 hang 住。
// disable
failpoint.Inject("getMinCommitTSFromTSO", nil)
// enable
failpoint.Eval(_curpkg_("getMinCommitTSFromTSO"))
例 3 – 关上 Failpoint
例 3 是一个简略的关上 Failpoint 的例子,在敞开状态下,Inject 函数不会做任何事件,而当 Failpoint 关上后,Inject 函数就会变成 Eval 函数,此时咱们能够应用 HTTP 申请去管制 Failpoint 的行为,包含:
- 人为增加 sleep;
- 让 goroutine panic;
- 暂停这个 goroutine 的执行;
- 进入 gdb 中断。
// disable
failpoint.Inject("beforeSchemaCheck", func() {c.ttlManager.close()
failpoint.Return()})
// enable
if _, _err_ := failpoint.Eval(_curpkg_("beforeSchemaCheck"));
_err_ == nil {c.ttlManager.close()
return
}
例 4 – 利用 Failpoint 注入变量
在例 4 中,TiDB 利用 Failpoint 做了更多的注入,在其中敞开了 TTL Manager,这将导致乐观锁的疾速过期,并且中断事务的提交。此外,还能够借助 Failpoint 批改以后作用域下的变量。如果没有 Failpoint,这些故障状况可能极少产生,而通过 Failpoint,咱们就能够疾速的测试当产生故障时,是否会产生如毁坏一致性的异常现象。
图 24 – 对提交 Secondary Keys 的 Failpoint 注入
图 24 展现了在两阶段提交中的提交阶段下,通过注入来达到提早或者跳过 Secondary Keys 的成果,而这些状况在通常状况下是简直不会呈现的。
Chaos Test
在一个分布式系统中,咱们难以要求开发人员总是写出正确的代码,事实上大部分时候咱们都不能做到齐全的正确实现。如果说 Failpoint 是细粒度的管制某段代码可能会呈现的景象,是演习;那么 Chaos Test 就是无差别的对系统进行毁坏,是真正的战场。
图 25 – Chaos Test 概念图
图 25 是 Chaos Test 的概念图,Chaos Test 和 Failpoint 最大的区别在于,Failpoint 依然是开发人员所构想的可能出现异常的中央,那么开发人员在绝大多数状况下,是无奈将 Failpoint 设计的全面的,Chaos Test 为这一纰漏加上了一层额定的保险。
在开发 Chaos Mesh 的过程中,咱们也做了诸多尝试,例如:
- kill node
- 物理机的断电测试
- 网络提早与丢包
- 机器的工夫漂移
- IO 性能限度
Chaos Mesh 在正式公布前,Chaos Test 就在 PingCAP 被证实是无效的,咱们将这些测试心得通过 Chaos Mesh 分享给社区。
总结
尚未提笔写这篇文章的时候,我也曾重复思考过,对于事务测试,到底可能分享些什么?并且一度陷入感觉没有货色好说的窘境。然而当我尝试说明确一些测试方法时,才后知后觉的意识到,测试是一门很深奥也容易被忽视的学识,咱们在开发数据库的过程中破费了不少的心理在设计和运行测试上,本文所提及的,也只是事务测试体系的冰山一角。所有的测试,都是为了更好的产品质量,事务作为数据库的外围个性之一,更应该受到关注。Through the fire and the flames, we carry on.