Greenplum测试框架最全解析

5a3b6da3-08d6-4740-ac94-418595aa0eb1.jpg
e58c94ef-a9c7-4406-8db9-eb42d181ff24.png 46c6798e-ba61-4f4a-86e6-dc44b91365c2.png


软件测试是开发过程中十分重要的一环,在数据库领域更是如此。一款稳定、可靠的数据库离不开大量的测试作为支撑。

6f739683-eb3f-4557-9655-a4cab52d4f2b.png 48db87c1-440e-4e68-af1d-9bc7e3b4c147.png
Greenplum 作为一款基于 Postgres 的开源数据库,在测试方面做出了大量的探索。除继承了 Postgres 原有的 regress 测试外,增加了 Fault Injector 框架。允许开发者在回归测试中,通过执行简单的 SQL 函数,对数据库注入真实场景中可能出现各种的故障。此外, Greenplum 还开发了新的 isolation 测试框架 (isolation2),开发者可以用更简单易懂的语法编写出在各种并发情况下,可能出现的数据竞争的测试用例。配合 Fault Injector 框架,能够涵盖范围非常广的测试场景。
本文主要结合一些实际的案例介绍这些测试框架的使用场景和使用方式 在后续的文章中会介绍这些框架的原理,欢迎大家留言交流。

9cc29f45-8deb-4ab4-9f28-cee2f1851613.png

regress

8573b9ed-c95c-4788-90ef-06c6e6492c7c.png
regress 测试位于 Greenplum/Postgres 源代码的 src/test/regress 目录下。这些测试通常包含:
  • 测试用例: 一组 SQL 语句 (.sql 文件)
  • 期待输出:测试用例执行后, Postgres 的正确输出 (.out 文件)

这些测试由 pg_regress 调度执行。执行结束后通过对预期输出和实际输出进行对比 (通常会自动生成一个 regression.diffs 的文件),即可知道哪些测试没有通过,以及原因是什么。利用 regress 测试,我们可以做一些功能性的测试。例如:测试一些带有复杂过滤条件的 SELECT 语句的输出结果是否正确,测试一些 SQL 生成的执行计划是否符合预期。这些测试都有一个特点,那就是不管在什么情况下,它们的输出都应该是一致且恒定的。
尽管 regress 测试能够满足大部分的功能性测试,但还有一些有趣的测试场景值得讨论。 比如数据库中的隔离和并发问题。虽然 pg_regress 支持并行地执行多个测试用例, 但我们并不能利用它来验证一些时序上的问题 (我们倒是可以利用它来加速一些测试)。因为这些并行的测试用例中 SQL 语句的执行次序是没有保证的,我们无法得到稳定的输出,也就无法跟预期的输出进行对比了。而下面要介绍的 isolation & isolation2 就是为这类测试而设计的。

9cc29f45-8deb-4ab4-9f28-cee2f1851613.png

isolation & isolation2

8573b9ed-c95c-4788-90ef-06c6e6492c7c.png
isolation 测试位于 Greenplum/Postgres 源代码的 src/test/isolation 目录下。目前 Greenplum 中的 isolation 测试框架还不是很完善,Greenplum 仅在 Utility 模式下运行 Postgres 原生的 isolation 测试。Greenplum 大部分的和并发/隔离相关的测试由 isolation2 完成。
Postgres 原生的 isolation 测试包含:
  • 测试用例:一组 .spec 文件,这些 .spec 文件除了包含待测试的 SQL 语句外,最重要的是还包含了对执行这些 SQL 语句顺序的定义。
  • 期待输出:测试用例的 spec 被执行后, Postgres 的正确输出 (.out 文件)。

这些测试由 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

其中,setup 和 teardown 中包含的 SQL 语句分别会在测试运行前和结束后运行,在这个例子中是创建和删除表 a1. s1 和 s2 分别是这个测试中两个 Session 的名称。
在 s1 中,定义了 setup 时需要开始一段事务,还定义了 s1as, s1ae, s1c 这三个执行步骤:
  • s1as:对表 a1 以 ACCESS SHARE 模式上锁
  • s1ae:对表 a1 以 ACCESS EXCLUSIVE 模式上锁
  • s1c:提交当前事务

在 s2 中,定义了 setup 时需要开始一段事务,还定义了 s2as, s2ae, s2c 这三个执行步骤:
  • s2as: 对表 a1 以 ACCESS SHARE 模式上锁
  • s2ae: 对表 a1 以 ACCESS EXCLUSIVE 模式上锁
  • s2c: 提交当前事务

permutation 中定义了这些 SQL 语句执行的顺序:
  • s1: 对表 a1 以 ACCESS SHARE 模式上锁
  • s2: 对表 a1 以 ACCESS SHARE 模式上锁
  • s1: 对表 a1 以 ACCESS EXCLUSIVE 模式上锁 <等待, s2 拿了 ACCESS SHARE 模式的锁>
  • s2: 对表 a1 以 ACCESS EXCLUSIVE 模式上锁 <死锁发生, s1, s2 互相等待>
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 目录下的测试用例。
通过对比 isolation 与 isolation2,在功能方面,二者并没有很大的区别;在语法方面, isolation 的 .spec 文件更为严谨,当测试用例中需要多次调用相同的 SQL 语句时会十分方便。solation2 的语法更为灵活直观, 编写起来比较容易上手。

9cc29f45-8deb-4ab4-9f28-cee2f1851613.png

Fault Injector

8573b9ed-c95c-4788-90ef-06c6e6492c7c.png
真实世界中的故障往往要复杂许多,比如:网络可能会有很大的延迟,磁盘中的数据可能丢失,集群中的某台主机可能突然下线。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 还支持注入以下常见的几种故障:
  • error:等价于 elog(ERROR)
  • fatal:等价于 elog(FATAL)
  • panic:等价于 elog(PANIC)
  • sleep:休眠一段时间
  • suspend:阻塞当前的进程, 并且不检查中断信号
  • resume:恢复被注入 ‘suspend’ 故障的进程
  • skip:用来注入一些自定义的故障逻辑
  • reset:移除之前注入的故障
  • segv:使当前的进程崩溃 (发送 SIGSEGV 信号)
...
有关其它的故障类型以及更多的使用方法,可以参阅 gpcontrib/gp_inject_fault/README。目前 Postgres 还不支持 Fault Injector 框架, Greenplum 团队向上游提交了 Patch,感兴趣的读者可以下载这个 Patch[1] 体验。

9cc29f45-8deb-4ab4-9f28-cee2f1851613.png

后 记

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



参考资料


[1] Fault Injector Framework: https://www.postgresql.org/message-id/flat/CANXE4TdxdESX1jKw48xet-5GvBFVSq%3D4cgNeioTQff372KO45A%40mail.gmail.com


郭兴,VMware Greenplum实习生

东南大学研究生在读, 2021 年 9 月加入 Greenplum 团队开启实习工作, 参与 Greenplum Extension 研发。



点击文末“ 阅读原文 ”,获取Greenplum中文资源。




6c01ca1f-0491-479d-88d6-b8623db5f185.gif 08505129-fd59-4f98-a010-c530159a8abb.png来一波  “在看”、“分享” 和  “赞”  吧!

本文分享自微信公众号 - Greenplum中文社区(GreenplumCommunity)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

我们现在所处的时代是计算机信息技术迅猛发展的时代,信息的处理方式、IT管理方式、应用模式都发生了巨大的变化: 1)首先,过去10来年,计算机硬件技术得到了飞速发展,计算机的CPU、内存、网络等硬件设施在性能上得到极大的提升的同时,在价格上也是越来越廉价,单个PC服务器就能拥有6~8 core的强大处理能力,因此,我们现在很容易就能搭建一个包含100个core的服务器集群,投资额只要几十万RMB,而这样一个集群所具备的计算能力远远胜于过去的专用小型机甚至大型机系统,而价格却便宜得多; 2)云计算和虚拟化技术是当前最为炙手可热的技术和潮流趋势,通过这个技术,企业可以有效减少硬件成本投入,实现按需灵活分配和回收资源,降低管理复杂度、减少系统故障率; 3)在应用方面,IT技术从以前主要应用于OLTP类型的事务处理系统,发展到以数据为基础的分析型商业智能系统,IT系统不再仅局限于对联机业务的支撑功能,以数据仓库为特征的分析型OLAP系统,为企业的营销管理、风险控制、客户分析等等提供了决策支持(DSS)所需的技术支持,为企业营运提供商业智能手段;而数据仓库的基本特征就是对大规模数据的存储和分析计算,包括对几百TB、甚至PB级数据的计算,传统的架构越来越难以满足对这些海量数据的处理需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值