Greenplum 的测试 101

2021/12/05

软件测试是开发过程中十分重要的一环, 在数据库领域更是如此. 一款稳定, 可靠的数据库离不开大量的测试作为支撑. Greenplum 作为一款基于 Postgres 的开源数据库, 在测试方面做出了大量的探索. 除继承了 Postgres 原有的 regress 测试外, 增加了 Fault Injector 框架. 允许开发者在回归测试中, 通过执行简单的 SQL 函数, 对数据库注入真实场景中可能出现各种的故障. 此外, Greenplum 还开发了新的 isolation 测试框架 (isolation2), 开发者可以用更简单易懂的语法编写出在各种并发情况下, 可能出现的数据竞争的测试用例. 配合 Fault Injector 框架, 能够涵盖范围非常广的测试场景. 本文主要结合一些实际的案例介绍这些测试框架的使用场景和使用方式, 在后续的文章中会介绍这些框架的原理, 欢迎大家留言交流.

regress

regress 测试位于 Greenplum/Postgres 源代码的 src/test/regress 目录下. 这些测试通常包含:

这些测试由 pg_regress 调度执行. 执行结束后通过对预期输出和实际输出进行对比 (通常会自动生成一个 regression.diffs 的文件), 即可知道哪些测试没有通过, 以及原因是什么. 利用 regress 测试, 我们可以做一些功能性的测试. 例如: 测试一些带有复杂过滤条件的 SELECT 语句的输出结果是否正确, 测试一些 SQL 生成的执行计划是否符合预期. 这些测试都有一个特点, 那就是不管在什么情况下, 它们的输出都应该是一致且恒定的.

尽管 regress 测试能够满足大部分的功能性测试, 但还有一些有趣的测试场景值得讨论. 比如数据库中的隔离和并发问题. 虽然 pg_regress 支持并行地执行多个测试用例, 但我们并不能利用它来验证一些时序上的问题 (我们倒是可以利用它来加速一些测试). 因为这些并行的测试用例中 SQL 语句的执行次序是没有保证的, 我们无法得到稳定的输出, 也就无法跟预期的输出进行对比了. 而下面要介绍的 isolation & isolation2 就是为这类测试而设计的.

isolation & isolation2

isolation 测试位于 Greenplum/Postgres 源代码的 src/test/isolation 目录下. 目前 Greenplum 中的 isolation 测试框架还不是很完善, Greenplum 仅在 Utility 模式下运行 Postgres 原生的 isolation 测试. Greenplum 大部分的和并发/隔离相关的测试由 isolation2 完成.

Postgres 原生的 isolation 测试包含:

这些测试由 pg_isolation_regress 根据 .spec 文件中定义的执行顺序, 调度不同的 Session 执行, 下面给出的是 Postgres 中, 对于一种死锁的测试用例.

## file: src/test/isolation/specs/deadlock-simple.spec
setup
{
  CREATE TABLE a1 ();
}

teardown
{
  DROP TABLE a1;
}

session s1
setup		{ BEGIN; }
step s1as	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
step s1ae	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE; }
step s1c	{ COMMIT; }

session s2
setup		{ BEGIN; }
step s2as	{ LOCK TABLE a1 IN ACCESS SHARE MODE; }
step s2ae	{ LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE; }
step s2c	{ COMMIT; }

permutation s1as s2as s1ae s2ae s1c s2c

其中, setupteardown 中包含的 SQL 语句分别会在测试运行前和结束后运行, 在这个例子中是创建和删除表 a1. s1s2 分别是这个测试中两个 Session 的名称.

s1 中, 定义了 setup 时需要开始一段事务, 还定义了 s1as, s1ae, s1c 这三个执行步骤:

s2 中, 定义了 setup 时需要开始一段事务, 还定义了 s2as, s2ae, s2c 这三个执行步骤:

permutation 中定义了这些 SQL 语句执行的顺序:

pg_isolation_regress 执行完上面 .spec 文件中定义的 SQL 后, 正确输出如下:

## file: src/test/isolation/expected/deadlock-simple.out
Parsed test spec with 2 sessions
starting permutation: s1as s2as s1ae s2ae s1c s2c
step s1as: LOCK TABLE a1 IN ACCESS SHARE MODE;
step s2as: LOCK TABLE a1 IN ACCESS SHARE MODE;
step s1ae: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE; <waiting ...>
step s2ae: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE;
ERROR:  deadlock detected
step s1ae: <... completed>
step s1c: COMMIT;
step s2c: COMMIT;

开发者可以在 .spec 文件中灵活的描述在不同 Session 中执行 SQL 语句的顺序, 使得一些与并发/隔离相关的测试更为容易编写.

Greenplum 团队开发的 isolation2 测试框架, 也是类似的思路, 但 isolation2 的语法更简单易懂. 下面是笔者利用 isolation2 编写的与上面一致的测试用例.

CREATE TABLE a1();
1:  BEGIN;                                  -- s1 开始一段事务
1:  LOCK TABLE a1 IN ACCESS SHARE MODE;     -- s1 以 ACCESS SHARE 模式对 a1 上锁
2:  BEGIN;                                  -- s2 开始一段事务
2:  LOCK TABLE a1 IN ACCESS SHARE MODE;     -- s2 以 ACCESS SHARE 模式对 a1 上锁
1&: LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE; -- s1 以 ACCESS EXCLUSIVE 模式对 a1 上锁
2:  LOCK TABLE a1 IN ACCESS EXCLUSIVE MODE; -- s2 以 ACCESS EXCLUSIVE 模式对 a1 上锁
1<:                                         -- 等待 s1 返回
1:  COMMIT;                                 -- s1 提交事务
2:  COMMIT;                                 -- s2 提交事务
DROP TABLE a1;

其中不同的数字表示在不同的 Session 中执行对应的 SQL, 由上至下表示了 SQL 的执行顺序. '&' 表示当前的 Session 执行的 SQL 会被阻塞, '<' 表示等待当前的 Session 返回. isolation2 支持的语法非常丰富, 这里就不一一列举了, 有兴趣的读者可以浏览 Greenplum 源代码中 src/test/isolation2/sql 目录下的测试用例.

solation 与 isolation2, 在功能方面, 二者并没有很大的区别; 在语法方面, isolation 的 .spec 文件更为严谨, 当测试用例中需要多次调用相同的 SQL 语句时会十分方便. isolation2 的语法更为灵活直观, 编写起来比较容易上手.

Fault Injector

真实世界中的故障往往要复杂许多, 比如: 网络可能会有很大的延迟, 磁盘中的数据可能丢失, 集群中的某台主机可能突然下线. Greenplum 团队开发的 Fault Injector 框架让这类测试变得十分容易. 例如, 我们希望在 heap 中插入 tuple 的时候, 注入一些故障, 观察这些故障对系统的影响. 在 Greenplum master 分支的 src/backend/access/heap/heapam.c 中, 有如下代码:

// 注: 笔者为了叙述方便, 对这里的代码进行了调整, 与实际代码可能有些出入.
void
heap_insert(Relation relation, HeapTuple tup, CommandId cid,
            int options, BulkInsertState bistate, TransactionId xid)
{
  ...
#ifdef FAULT_INJECTOR
  FaultInjector_InjectFaultIfSet("heap_insert",   /*faultname*/
                                 DDLNotSpecified, /*ddlstatement*/
                                 "",              /*databasename*/
                                 RelationGetRelationName(relation));
#endif
   ...
}

当我们启用了 Fault Injector 后, 每当代码执行到 FaultInjector_InjectFaultIfSet() 时, Fault Injector 会检测当前的注入点是否被注入了故障, 如果有, 则执行相关的故障逻辑. 利用 gp_inject_fault 插件可以很容易的在注入点注入故障逻辑. 下面这段 SQL 演示了如何在 Greenplum 集群中, 在编号为 1 的 Segment Server 上, 'heap_insert' 这一注入点, 注入一段死循环. 以此来模拟实际场景中, 某个 Segment Server 发生了故障, 无法正常地插入 tuple 到表 my_table 中.

CREATE EXTENSION gp_inject_fault;
SELECT gp_inject_fault('heap_insert'    /* inject location */,
                       'infinite_loop'  /* fault type */,
                       ''               /* DDL */,
                       ''               /* database name */,
                       'my_table'       /* table name */,
                       1                /* start occurrence */,
                       10               /* end occurrence */,
                       0                /* extra arg */,
                       dbid)
  FROM gp_segment_configuration WHERE content=1 AND role='p';

除了可以注入 'infinite_loop' 外, Fault Injector 还支持注入以下常见的几种故障:

有关其它的故障类型以及更多的使用方法, 可以参阅 gpcontrib/gp_inject_fault/README. 目前 Postgres 还不支持 Fault Injector 框架, 但是 Greenplum 团队向上游提交了 Patch, 感兴趣的读者可以使用这个 Patch[1] 体验.

后记

笔者自己上手一个新项目时, 喜欢从一些测试用例开始摸索. 通过一些简单的测试用例可以窥知一款软件的基本功能, 之后配合 git blame 便可以顺藤摸瓜, 找到引入这些测试用例的 commit 记录. 有时候它可能是修复了一个 Bug, 也可能是引入了一个新功能, 这样就可以学习到开发这个功能时该如何写测试, 该去修改哪些代码. 最后, 希望大家多多为 Greenplum 贡献代码/Issue :p

参考

[1] Fault Injector Framework

comments powered by Disqus