模糊测试已成为近几年一个炙手可热的研究课题,用于查找软件中的问题,包括崩溃、错误输出和安全漏洞。数据库也不例外,并且已经开发了许多研究工具。
ClickHouse 也正积极地使用模糊器进行测试——多年来,已经使用了多种模糊器来测试 ClickHouse,包括 SQLancer、SQLsmith、AST 模糊器,以及最近的 WINGFUZZ 模糊器。
自从我加入 ClickHouse 以来,我一直在审查 ClickHouse 中现有的测试基础设施,并注意到他们的模糊器中存在一个明显的差距。它们都无法在保持查询正确性的前提下生成广泛复杂性的查询。
因此,在过去的 5 个月中,我开发了 BuzzHouse,一个新的模糊器,以弥补现有测试基础设施中的差距。BuzzHouse 已经发现了大约 100 个新问题 在 ClickHouse 中,在这篇博文中,我将展示模糊测试数据库时需要考虑的事项。
什么是模糊测试或模糊测试?
在讨论为什么模糊测试数据库很难之前,让我们简要定义一下什么是模糊测试。模糊测试,或称模糊测试,是一种软件测试技术,它涉及向程序提供随机、意外或无效的输入,以发现错误、崩溃或漏洞。通过模拟不可预测的用户行为,模糊测试有助于识别在标准测试期间可能被忽视的边缘情况。它被广泛用于提高软件的健壮性和安全性,从操作系统到编译器和数据库。
为什么模糊测试数据库很难
数据库是一个复杂的系统,包括查询处理和优化、数据存储、缓冲区管理,以及对于像 ClickHouse 这样的情况,跨多个节点的分布式处理。由于如此高的复杂性,构建一个模糊器来测试数据库绝非易事。同时,尝试测试数据库系统的所有层级变得更具挑战性,因为需要更多样化的输入。
如何生成查询?
我们可以从最顶层开始,即输入客户端的查询解析和处理。大多数数据库,包括 ClickHouse,都使用 SQL 作为其输入语言,并且大多数数据库模糊器都是为了生成 SQL 查询而开发的。第一个设计问题是我们将如何生成查询。一些模糊器完全随机地执行此操作,而另一些则牢记积压策略。虽然这两种方法都是正确的,但根据所选择的方法,某些情况将不会生成,并且某些错误可能不会被发现。让我们看一下 TPC-H Q5 作为示例
1SELECT
2 n_name,
3 sum(l_extendedprice * (1 - l_discount)) AS revenue
4FROM
5 customer,
6 orders,
7 lineitem,
8 supplier,
9 nation,
10 region
11WHERE
12 c_custkey = o_custkey
13 AND l_orderkey = o_orderkey
14 AND l_suppkey = s_suppkey
15 AND c_nationkey = s_nationkey
16 AND s_nationkey = n_nationkey
17 AND n_regionkey = r_regionkey
18 AND r_name = 'ASIA'
19 AND o_orderdate >= DATE '1994-01-01'
20 AND o_orderdate < DATE '1994-01-01' + INTERVAL '1' year
21GROUP BY
22 n_name
23ORDER BY
24 revenue DESC;
当用户编写这样的查询时,他们期望所有表和列都是有效的。但是,如果模糊器仅生成有效的标识符,则永远不会发现来自不存在的表和列的错误。
另一个有趣的案例是分组案例。SQL 具有复杂的语义,这使得随机查询生成更加困难。在这种情况下,我们必须确保投影要么使用 n_name
列,要么使用聚合函数内的任何其他列才能正确。也可能应用其他规则,例如不在 WHERE 子句中使用聚合,并且窗口函数不能在聚合函数内部。考虑到这些语义,随机生成正确的查询变得困难。如果我们采用这些限制,我们将减少生成的查询域,以及我们可能发现的错误数量。
某些语句,例如 DROP 和 DETACH,会使表不可用。模糊器应该潜在地意识到这一点,以便在查询中使用有效的表。我们可以跟踪这些语句所做的更改,但稍后,当添加更多功能时,例如可能依赖于表的 SQL 视图,处理此信息将变得复杂。
随着我们添加更多要测试的功能,例如 SQL 函数、SQL 查询子句、SQL 类型、要使用的表/列/数据库的数量、ClickHouse 中的其他语句(例如 INSERT 或 OPTIMIZE),或更多表引擎,组合的数量将急剧增加。当我们尝试生成正确的查询或在模糊器中测试新功能时,这成为一个相关的问题。正如我之前所说,我们可以尝试避免这些错误并限制生成的域,或者忽略它们并提高查询正确性。在 BuzzHouse 中,我尝试为大多数情况生成正确的输出,同时回退到随机情况几次。
查找错误结果
借助当前最先进的研究,我们可以在 BuzzHouse 中找到具有错误结果的查询,这在 SQLsmith 等其他模糊器中是不可能的。这些策略包括
- 转储表并再次读取它,这样我们也可以测试格式。
- 使用 oracle 运行和比较等效查询。此策略由 SQLancer 开创。
- 使用不同的设置运行相同的查询,例如线程数、启用/禁用外部排序或分组,或设置不同的连接算法。
- 将表与来自另一个关系数据库的表交换,然后将谓词推送到其中,然后将计算结果与相同的排序子句或全局聚合查询进行比较。
以下问题是在将值转储并重新插入到表中后发现的。插入后块顺序不同,因此影响了 LowCardinality
类型的排序。
1SET allow_suspicious_low_cardinality_types = 1;
2CREATE TABLE t0 (c0 LowCardinality(Nullable(Int))) ENGINE = MergeTree()
3ORDER BY (c0) SETTINGS allow_nullable_key = 1;
4INSERT INTO TABLE t0 (c0) VALUES (1);
5INSERT INTO TABLE t0 (c0) VALUES (0), (NULL);
6SELECT c0 FROM t0 ORDER BY c0 DESC NULLS LAST;
7-- 1
8-- NULL
9-- 0
使用代码覆盖率?
AFL 和 libFuzzer 等模糊器中的当前最先进技术使用代码覆盖率来查找模糊测试时的新代码路径。乍一看这听起来很有希望,但由于数据库的复杂性,找到新路径变得越来越困难。首先,如上所述,SQL 语言有许多语义要遵循。其次,许多问题需要更多输入,例如多个客户端或服务器重启。第三,代码覆盖率模糊测试变得慢得多,并且由于每次突变在每个循环中对查询进行微小更改,因此生成的查询的多样性要低得多。同时,建议备份生成的语料库,以便稍后继续相同的会话。在 BuzzHouse 中,我们决定目前不使用代码覆盖率来补充具有此功能的现有模糊器。
事件频率
下一个重要的决定是事件的随机性。模糊器执行的每个决定都将基于概率。符合设置的概率,模糊器被限制为它可以生成的事件的可能性。让我们以模糊器必须选择要生成的下一个查询的情况为例
- 10% 的几率创建表。
- 50% 的几率运行 SELECT 查询(这些在分析型数据库中更频繁地发生,因此我们应该生成更多)。
- 20% 的几率插入表。
- 5% 的几率删除表。
- 5% 的几率从表中删除(ClickHouse 中的轻量级删除)。
- 10% 用于 ALTER 语句。
此概率图表可能听起来合理,但是,每 20 个查询中,将生成一个 DROP 语句。这有什么后果?由于组合的数量,DROP 语句比 CREATE TABLE 更容易成功。还可能删除目录中的所有表,而没有创建任何表。此外,很难对服务器上持久存在的表进行长期测试,同时随着时间的推移发出许多 INSERT/UPDATE/DELETE 语句。这些小的决定可能会影响模糊器将来能够做什么。
为 ClickHouse 设计模糊器
除了上面提到的所有内容之外,在构建模糊器时还有其他设计决策。以下是一些问题以及已实施的内容
- 应该并行运行多个客户端吗?如果是,如何在它们之间同步?目前,BuzzHouse 中只有一个客户端。
- 所有模糊器都在客户端运行。那么模糊测试服务器呢?为服务器使用单独的模糊器。
- 并非所有问题都与崩溃有关,我们必须查找查询中的错误结果、错误消息的质量和性能。BuzzHouse 检测到一些错误结果。对于性能,稍后我们可以对连续查询运行进行基准测试并进行比较。
- 通过查看 TPC-H Q5,模糊器生成的查询大小应该是多少?我们应该连接多少个表?查看叉积,它们生成大的中间结果并使模糊器变慢。BuzzHouse 具有可以为此调整的“深度”和“宽度”参数。
- 模糊器平均应在目录中保留表多长时间?更新表元数据呢?BuzzHouse 将始终在目录中保留至少 3 个表。ALTER 语句对每个表运行,但与其他数据库中具有对等项的表除外。
- 查看错误消息,某些错误会输出奇怪的错误消息而不是使服务器崩溃。有时错误消息可能是正确的或错误的。将来,我们可以保留服务器应抛出的预期错误消息列表。
- 对于慢查询,哪些是合理地慢,哪些不是?这是一个难以回答的问题。但是,我们可以将性能与其他数据库进行比较。
考虑到以上所有要点,构建一个将涵盖所有情况,然后找到所有现有问题的模糊器变得非常困难。做一个简单的设计决定将影响模糊器将能够生成什么。
BuzzHouse 尝试通过使用语法假设来生成尽可能正确的查询,例如
- 始终为函数生成正确数量的参数。
- 使用查询中存在的表中的列。
- 使用 CREATE/ALTER/DROP 更改备份目录。
- 保持查询语法始终正确。
但是,代码库变得更加复杂,需要处理所有这些假设,并且需要在添加新功能时进行更新。也不要忘记一些简单的查询,例如 SELECT 1 FROM idontexist;
将不会生成。
总结
BuzzHouse 的开发是为了解决 ClickHouse 使用的模糊测试领域中的关键差距。
通过专注于生成复杂但正确的查询,并识别超出简单崩溃的问题,它可以补充当前用于测试数据库的工具套件
- 使用 AFL 和 libFuzzer 进行代码覆盖率引导的模糊测试。
- SQLsmith (https://github.com/anse1/sqlsmith) 用于复杂的查询生成。
- SQLancer (https://github.com/sqlancer/sqlancer) 用于查询正确性。
- Pstress (https://github.com/Percona-QA/pstress) 用于重负载。计划稍后可能将其集成到 ClickHouse CI 中。
- Sysbench (https://github.com/akopytov/sysbench) 用于持续工作负载(+1 小时)。也在 CI 的计划中。
- BuzzHouse 用于随机生成的目录支持的查询。
- AST 模糊器用于变异来自测试的查询,以及可能的其他模糊器。
- 一个自定义脚本,用于模糊测试服务器并将任何其他现有模糊器插入其中。
BuzzHouse 能够发现已经超过 100 个新问题,这突显了它的价值以及多样化模糊测试方法在提高 ClickHouse 等数据库的健壮性和可靠性方面的重要性。
BuzzHouse 预计很快将合并到 ClickHouse 源代码中。如果您好奇,请随时浏览 pull request。