暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

用 BuzzHouse 填补 ClickHouse 数据库模糊测试的空白

ClickHouseInc 2025-02-18
47


本文字数:5651;估计阅读时间:15 分钟



Meetup活动


ClickHouse Shanghai User Group第2届 Meetup 火热报名中,详见文末海报!


近年来,模糊测试 (Fuzzing) 已成为热门研究方向,广泛用于发现软件缺陷,包括崩溃、错误输出和安全漏洞。数据库同样是研究重点,许多专门针对数据库的模糊测试工具相继问世。

ClickHouse 也积极采用模糊测试工具进行测试。多年来,ClickHouse 采用了多种模糊测试工具,包括 SQLancer、SQLsmith、AST fuzzer 以及近期的 WINGFUZZ fuzzer。

在审查 ClickHouse 现有的测试基础设施时,我发现这些模糊测试工具存在明显不足——它们无法在保证查询正确性的同时,生成复杂度较高的 SQL 语句。

因此,在过去 5 个月里,我开发了 BuzzHouse,这是一款新型模糊测试工具,旨在弥补 ClickHouse 现有测试体系的这一缺陷。迄今,BuzzHouse 已发现约 100 个 ClickHouse 相关问题。在这篇文章中,我将分享数据库模糊测试中的关键考虑因素。


什么是模糊测试(Fuzzing)?

在讨论数据库模糊测试的挑战之前,我们先来了解一下模糊测试的概念。模糊测试(Fuzzing)是一种测试方法,它通过向程序输入随机、意外或无效的数据,来发现潜在的错误、崩溃或安全漏洞。通过模拟不可预测的用户行为,模糊测试能够揭示传统测试难以覆盖的边界情况,因此被广泛应用于提升软件的稳定性和安全性,涵盖操作系统、编译器和数据库等多个领域。


为什么数据库模糊测试如此困难?

数据库系统本身十分复杂,涉及查询处理与优化、数据存储、缓冲管理,以及像 ClickHouse 这样的分布式数据库还涉及跨多个节点的计算。由于这些复杂性,开发一个高效的数据库模糊测试工具并不容易。此外,数据库系统的各个层面都需要测试,而这需要多种不同类型的输入,进一步增加了测试的难度。


如何生成 SQL 查询?

数据库的模糊测试通常从最顶层开始,即从客户端的查询解析和处理入手。大多数数据库(包括 ClickHouse)都使用 SQL 作为主要的查询语言,因此,模糊测试工具通常会自动生成 SQL 查询。然而,如何生成这些查询是模糊测试设计中的关键问题。有些工具采用完全随机的方式生成 SQL 语句,而另一些则基于历史查询数据来生成。虽然两种方法各有优缺点,但不同的策略可能会导致某些边界情况未被覆盖,从而遗漏潜在的 Bug。我们以 TPC-H Q5 查询为例,来看这种差异可能带来的影响:

    SELECT
    n_name,
    sum(l_extendedprice * (1 - l_discount)) AS revenue
    FROM
    customer,
    orders,
    lineitem,
    supplier,
    nation,
    region
    WHERE
    c_custkey = o_custkey
    AND l_orderkey = o_orderkey
    AND l_suppkey = s_suppkey
    AND c_nationkey = s_nationkey
    AND s_nationkey = n_nationkey
    AND n_regionkey = r_regionkey
    AND r_name = 'ASIA'
    AND o_orderdate >= DATE '1994-01-01'
    AND o_orderdate < DATE '1994-01-01' + INTERVAL '1' year
    GROUP BY
    n_name
    ORDER BY
    revenue DESC;

    当用户编写 SQL 查询时,他们通常默认所有的表和列都是有效的。然而,如果模糊测试工具仅生成现有的表和列,那么因引用不存在的表或列而引发的错误就无法被检测到。

    另一个复杂的情况是 SQL 分组(grouping)。SQL 语法规则较为严格,使得随机生成查询更具挑战性。例如,为了保证查询正确,查询结果(projection)必须包含 n_name 列,或者将其他列用于聚合函数。此外,还需遵循一些 SQL 语义规则,例如 `WHERE` 子句中不能使用聚合函数,窗口函数不能嵌套在聚合函数内。正因如此,随机生成正确 SQL 查询的难度较大。如果严格遵循这些规则,虽然能保证 SQL 语法的正确性,但也会缩小可生成查询的范围,并减少可能发现的错误数量。

    某些 SQL 语句(如 `DROP` 和 `DETACH`)会使表变得不可用,因此模糊测试工具需要考虑这些因素,确保查询仍然使用有效的表。虽然可以跟踪这些语句的影响,但如果再加入 SQL 视图等依赖于表的功能,管理这些关系就会变得更复杂。

    随着测试范围的扩大,例如引入更多 SQL 函数、查询子句、数据类型、不同的表/列/数据库组合,以及 ClickHouse 特有的 `INSERT` 或 `OPTIMIZE` 语句,或者支持更多的表引擎,可能的查询组合数量将急剧增长。这使得生成正确查询或测试新功能变得更加困难。正如前面提到的,我们可以选择严格约束查询范围,以减少错误,但这可能会降低测试的覆盖率;或者允许一定程度的随机性,以提高查询的多样性。在 BuzzHouse 中,我的目标是在大多数情况下生成正确的查询,但仍会在少数情况下随机生成查询,以确保模糊测试的广度和有效性。


    定位错误结果

    当前最先进的模糊测试技术使 BuzzHouse 能够发现返回错误结果的查询,而 SQLsmith 等其他模糊测试工具无法实现这一点。BuzzHouse 采用了以下策略:

    • 数据回读测试:导出表数据并重新读取,以验证数据格式的正确性。
    • 等效查询对比:运行等效的 SQL 查询,并使用“预言机”方法(oracle-based approach)对比查询结果。这种策略最早由 SQLancer 提出。
    • 不同设置的执行对比:在不同的数据库配置下运行相同的查询,例如调整线程数、启用或禁用外部排序和分组,或选择不同的 JOIN 算法,以检测可能的执行差异。
    • 跨数据库结果比对:用另一个关系数据库中的表替换当前表,应用相同的查询条件,并通过相同的排序规则或全局聚合查询对比计算结果,以验证查询逻辑的一致性。

    以下是通过数据回读测试发现的一个问题:在将表数据导出并重新插入后,数据块的存储顺序发生了变化,导致 LowCardinality 类型的排序结果出现异常。

      SET allow_suspicious_low_cardinality_types = 1;
      CREATE TABLE t0 (c0 LowCardinality(Nullable(Int))) ENGINE = MergeTree()
      ORDER BY (c0) SETTINGS allow_nullable_key = 1;
      INSERT INTO TABLE t0 (c0) VALUES (1);
      INSERT INTO TABLE t0 (c0) VALUES (0), (NULL);
      SELECT c0 FROM t0 ORDER BY c0 DESC NULLS LAST;
      -- 1
      -- NULL
      -- 0

      是否应该使用代码覆盖率?

      当前最先进的模糊测试技术(如 AFL 和 libFuzzer)依赖代码覆盖率来探索新的代码路径。这种方法在理论上很有吸引力,但在数据库领域却面临诸多挑战。首先,SQL 语言有着严格的语义规则,需要遵循特定的约束。其次,许多数据库 bug 需要更复杂的输入场景,例如多个客户端的并发访问或服务器的重启。第三,基于代码覆盖率的模糊测试运行速度较慢,因为它的变异方式是对查询进行微小调整,而这会降低生成 SQL 查询的多样性。此外,为了维持测试的连贯性,还需要对生成的数据集进行备份,以便在后续测试中复用。在 BuzzHouse 中,我们决定暂不采用代码覆盖率,而是专注于补充现有具备该功能的模糊测试工具。


      事件触发的概率设定

      模糊测试工具的行为通常由概率驱动,而不同的概率设定会直接影响它能生成的 SQL 事件。例如,在选择下一个要执行的查询时,可以采用如下概率分布:

      • 10% 创建一个新表 (CREATE TABLE)。
      • 50% 执行 SELECT 查询(分析型数据库中最常见,因此应提高其占比)。
      • 20% 插入数据 (INSERT)。
      • 5% 删除表 (DROP TABLE)。
      • 5% 删除表中的部分数据(ClickHouse 中的轻量级删除)。
      • 10% 执行 ALTER 语句。

      这个分布看起来合理,但仔细分析会发现潜在的问题:如果每 20 个查询就会生成一个 `DROP` 语句,那么 `DROP` 操作的执行成功率可能会高于 `CREATE TABLE`,导致数据库中的表可能被频繁删除,而没有足够的新表创建。此外,极端情况下,所有表可能会被 `DROP` 掉,导致测试无法继续进行。同时,如果 `INSERT`、`UPDATE` 和 `DELETE` 语句执行频率较高,而表结构不稳定(频繁 `DROP`),就难以进行长期测试。所有这些看似微小的决策,都可能极大地影响模糊测试工具最终的测试效果。


      设计适用于 ClickHouse 的模糊测试工具

      在构建模糊测试工具时,除了前面提到的关键点,还有许多重要的设计决策需要考虑。以下是一些核心问题,以及 BuzzHouse 针对这些问题的解决方案:

      • 是否需要支持多个并行客户端?如何进行同步?  目前,BuzzHouse 仅支持单个客户端运行,未实现多客户端同步机制。
      • 现有模糊测试工具主要运行在客户端侧,那么如何测试服务器端? 服务器端的模糊测试需要一个单独的工具,而 BuzzHouse 主要用于客户端查询的测试。
      • 如何发现非崩溃类问题,例如错误的查询结果、异常的错误消息或性能问题?BuzzHouse 目前可以检测部分错误查询结果。针对性能问题,可以在后续阶段通过基准测试比较连续查询的执行时间。
      • 查询的复杂度应该如何控制?  以 TPC-H Q5 为例,查询的规模和联接表的数量会影响模糊测试的效率。尤其是大规模的笛卡尔积操作会产生庞大的中间数据,导致测试变慢。BuzzHouse 提供了“深度”(depth)和“宽度”(width)参数,可用于调整查询规模。
      • 表在数据库目录中的生命周期应如何管理?元数据如何更新?BuzzHouse 确保目录中始终保留至少 3 张表,并且 `ALTER` 语句会作用于所有表,但不会影响那些依赖于外部数据库的表。
      • 如何有效分析错误消息?许多数据库错误不会直接导致崩溃,而是返回异常的错误消息。因此,除了崩溃检测外,BuzzHouse 未来可能会维护一个错误消息白名单,用于比对 ClickHouse 预期返回的错误。
      • 如何区分合理的慢查询与异常的慢查询?这一点较难精确界定,不过可以通过对比 ClickHouse 与其他数据库的查询性能来分析哪些查询可能存在问题。

      以上这些因素使得模糊测试工具的设计变得异常复杂,一个简单的架构决策都会影响其测试范围和发现问题的能力。

      为了确保测试的有效性,BuzzHouse 采用了一些关键策略来尽可能生成符合 SQL 语法的正确查询:

      • 为函数调用生成正确数量的参数。

      • 仅使用查询中涉及表的列,避免引用不存在的字段。  

      • 跟踪 CREATE、ALTER 和 DROP 语句的变更,确保目录的一致性。

      • 始终保持 SQL 语法正确。

      然而,实现这些功能的代价是代码库的复杂性增加,并且需要在 ClickHouse 版本更新时进行维护。此外,某些特殊情况仍无法被测试,例如 SELECT 1 FROM idontexist; 这样的查询不会被生成。


      总结

      BuzzHouse 旨在弥补现有 ClickHouse 模糊测试工具的关键缺口。

      通过生成复杂但语法正确的查询,并识别超出简单崩溃范围的问题,它补充了当前数据库测试工具的生态:

      • AFL 和 libFuzzer:基于代码覆盖率的模糊测试。

      • SQLsmith:用于生成复杂 SQL 查询(https://github.com/anse1/sqlsmith)

      • SQLancer:用于验证查询的正确性(https://github.com/sqlancer/sqlancer) 

      • Pstress:用于高负载测试,计划未来集成到 ClickHouse CI (https://github.com/Percona-QA/pstress)

      • Sysbench:用于长时间(超过 1 小时)的持续工作负载测试,也计划用于 CI(https://github.com/akopytov/sysbench)

      • BuzzHouse:用于随机生成基于数据库目录的查询。

      • AST fuzzer:通过变异测试查询,可能与其他模糊测试工具结合使用。

      • 自定义服务器模糊测试脚本:可用于测试服务器端,并集成其他现有的模糊测试工具。

      BuzzHouse 已经发现超过 100 个新问题,这表明采用多样化的模糊测试方法对提升 ClickHouse 这样的数据库的稳定性和可靠性至关重要。

      目前,BuzzHouse 即将合并到 ClickHouse 源代码中。如果你感兴趣,欢迎查看相关的 Pull Request【https://github.com/ClickHouse/ClickHouse/pull/71085】


      Meetup 活动报名通知

      好消息:ClickHouse Shanghai User Group第2届 Meetup 已经开放报名了,将于2025年03月01日在上海 阿里巴巴徐汇滨江园区X区3层X7-301龙门书院 举行,扫码免费报名


      注册ClickHouse中国社区大使,领取认证考试券

      ClickHouse社区大使计划正式启动,首批过审贡献者享原厂认证考试券!


      试用阿里云 ClickHouse企业版


      轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G



      征稿启示

      面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

      文章转载自ClickHouseInc,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

      评论