Presto 验证器

Presto 验证器是一个用于运行查询并验证正确性的工具。它可以用来测试新的 Presto 版本是否产生正确的查询结果,或者测试一对 Presto 查询是否具有相同的语义。

在每次 Presto 版本发布期间,都会运行验证器以确保没有正确性回归。

使用验证器

在 MySQL 数据库中,创建以下表并用您要运行的查询加载它

CREATE TABLE verifier_queries (
    id int(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
    suite varchar(256) NOT NULL,
    name varchar(256) DEFAULT NULL,
    control_catalog varchar(256) NOT NULL,
    control_schema varchar(256) NOT NULL,
    control_query text NOT NULL,
    control_username varchar(256) DEFAULT NULL,
    control_password varchar(256) DEFAULT NULL,
    control_session_properties text DEFAULT NULL,
    test_catalog varchar(256) NOT NULL,
    test_schema varchar(256) NOT NULL,
    test_query text NOT NULL,
    test_username varchar(256) DEFAULT NULL,
    test_password varchar(256) DEFAULT NULL,
    test_session_properties text DEFAULT NULL)

接下来,创建一个 config.properties 文件

source-query.suites=suite
source-query.database=jdbc:mysql://127.0.0.1:3306/mydb?user=my_username&password=my_password
control.hosts=127.0.0.1
control.http-port=8080
control.jdbc-port=8080
control.application-name=verifier-test
test.hosts=127.0.0.1
test.http-port=8081
test.jdbc-port=8081
test.application-name=verifier-test
test-id=1

验证器可以通过设置配置 running-mode 来在 query-bankcontrol-test 模式下运行。

  • control-test: 这是默认模式。控制查询和测试查询都会执行,并比较它们的校验和结果。

  • query-bank: 在此模式下,控制查询将被跳过,比较将在保存的快照结果和测试结果之间进行。

创建一个 verifier_snapshots

CREATE TABLE verifier_snapshots (
    id int(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
    suite varchar(256) NOT NULL,
    name varchar(256) NOT NULL DEFAULT '.',
    is_explain BOOLEAN NOT NULL DEFAULT false,
    snapshot json NOT NULL,
    updated_at datetime NOT NULL DEFAULT now(),
    UNIQUE(suite, name, is_explain));

下载 presto-verifier-0.289-executable.jar 并将其重命名为 verifier.jar。要运行验证器

java -Xmx1G -jar verifier.jar verify config.properties

query-bank 模式下运行之前,必须保存快照。添加配置

running-mode=query-bank
save-snapshot=true

运行验证器,快照将被保存到表 verifier_snapshots 中。

要在 query-bank 模式下运行,请设置 save-snapshot=false 或直接删除它

running-mode=query-bank
#save-snapshot=true

验证器流程

以下步骤总结了验证器的流程。

  • 导入源查询
    • 从 MySQL 表中读取源查询列表(带有配置的查询对)。

  • 查询预处理和过滤
    • 将覆盖应用于每个查询的目录、模式、用户名和密码。

    • 根据白名单和黑名单过滤查询。白名单在黑名单之前应用。

    • 过滤掉语法无效的查询。

    • 过滤掉不支持验证的查询。支持 SelectInsertCreateTableAsSelectcreate tablecreate view

  • 查询重写
    • 在执行之前重写查询,以确保不会修改生产数据。

    • Select 查询重写为 CreateTableAsSelect
      • 列名是通过运行带有 LIMIT 0Select 查询来确定的。

      • 对于未命名的列使用人工名称。

    • InsertCreateTableAsSelect 查询重写以替换其表名。
      • 构造一个设置查询以创建 Insert 查询所需的表。

    • 如果设置了配置,则根据 nondeterministic-function-substitutes 重写函数调用。

  • 查询执行
    • 从概念上讲,验证器配置了控制集群和测试集群。但是,对于某些测试,它们可能指向同一个 Presto 集群。在 Query-bank 模式下,控制集群会被跳过,取而代之的是使用保存的快照。

    • 对于每个源查询,按以下顺序执行以下查询。
      • 控制设置查询

      • 控制查询

      • 测试设置查询

      • 测试查询

      • 控制和测试拆卸查询

    • 查询会受到超时和重试的影响。
      • 集群连接失败和短暂的 Presto 故障会进行重试。

      • 查询重试可能会隐藏可靠性问题,因此验证器会记录所有发生的 Presto 查询故障,包括重试。

    • 某些查询故障会自动提交以进行重新验证,例如查询期间分区删除或表删除。

    • 有关查询故障的自动解决,请参阅 故障解决

  • 结果比较
    • 对于 SelectInsertCreateTableAsSelect 查询,结果会写入临时表中。

    • 构造并运行控制和测试的校验和查询。如果在 query-bank 模式下运行,控制校验和将从 mysql 数据库中保存的快照中恢复。

    • 如果配置 save-snapshot 设置为 true,控制校验和将被保存到 mysql 数据库中。

    • 验证表模式和行数对于控制和测试结果表是否相同。

    • 验证每列的校验和是否匹配。有关不同列类型的特殊处理,请参阅 列校验和

    • 有关处理非确定性查询,请参阅 确定性

  • 输出结果
    • 验证结果可以导出为 JSON 或人类可读的文本。

列校验和

对于控制/测试查询中的每一列,将在校验和查询中生成一个或多个列。

  • 浮点列
    • 对于 DOUBLEREAL 列,将生成 4 列用于验证
      • 列的有限值的总和

      • NAN 列的计数

      • 列的正无穷大计数

      • 列的负无穷大计数

    • 检查 NAN 计数、正无穷大和负无穷大计数是否匹配。

    • 检查控制总和和测试总和的空值。

    • 如果控制均值或测试均值非常接近 0,则检查两者是否都接近 0。

    • 检查控制总和和测试总和之间的相对误差。

  • VARCHAR 列
    • 对于 VARCHAR 列,将使用 checksum() 为验证生成一个简单的校验和列。

    • 如果设置了 validate-string-as-double,则将生成以下七列。如果在将所有值强制转换为 DOUBLE 之前和之后,NULL 计数相等,则应用浮点验证。否则,检查简单的校验和是否匹配。
      • 校验和

      • NULL 值的计数

      • 将所有值转换为DOUBLE类型后的NULL值数量

      将所有值转换为DOUBLE类型后
      • 有限值的总和

      • NAN值的计数

      • 正无穷值的计数

      • 负无穷值的计数

  • 数组列
    • 对于类型为array(E)的数组列arr,将生成三个列用于验证
      • 基数的总和

      • 基数的校验和

      • 数组校验和
        • 如果E不可排序,则数组校验和为checksum(arr)

        • 如果E可排序,则数组校验和为coalesce(checksum(try(array_sort(arr))), checksum(arr))

    • 如果设置了use-error-margin-for-floating-point-arrays并且EDOUBLEREAL,则将生成以下六个列。检查基数的总和是否匹配,以及基数的校验和是否匹配。将浮点验证应用于其余结果。
      • 基数的总和

      • 基数的校验和

      • 所有数组值的有限元素的总和

      • 所有数组值的NAN元素的计数

      • 所有数组值的正无穷元素的计数

      • 所有数组值的负无穷元素的计数

    • 如果设置了validate-string-as-double并且EVARCHAR,则将生成以下九个列。检查基数的总和和校验和是否匹配。如果在将所有数组元素转换为DOUBLE类型之前和之后,NULL计数相等,则应用浮点验证。否则,检查数组校验和是否匹配。
      • 基数的总和

      • 基数的校验和

      • 数组校验和checksum(array_sort(arr))

      • 所有数组值的NULL元素的计数

      • 将所有数组元素转换为DOUBLE类型后的NULL元素的计数

      将所有数组元素转换为DOUBLE类型后
      • 所有数组值的有限元素的总和

      • 所有数组值的NAN元素的计数

      • 所有数组值的正无穷元素的计数

      • 所有数组值的负无穷元素的计数

  • 映射列
    • 对于类型为map(K, V)的映射列,将生成四个列用于验证
      • 基数的总和

      • 映射的校验和

      • 键集的数组校验和

      • 值集的数组校验和

    • 如果设置了validate-string-as-double并且KVARCHAR,则将生成六个附加列
      • 所有键集的NULL元素的计数

      • 将所有映射键转换为DOUBLE类型后的键集的NULL元素的计数

      将所有映射键转换为DOUBLE类型后
      • 所有键集的有限元素的总和

      • 所有键集的NAN元素的计数

      • 所有键集的正无穷元素的计数

      • 所有键集的负无穷元素的计数

    • 如果设置了validate-string-as-double并且VVARCHAR,则将生成六个附加列
      • 所有值集的NULL元素的计数

      • 将所有映射值转换为DOUBLE类型后的值集的NULL元素的计数

      将所有映射值转换为DOUBLE类型后
      • 所有值集的有限元素的总和

      • 所有值集的NAN元素的计数

      • 所有值集的正无穷元素的计数

      • 所有值集的负无穷元素的计数

  • 行列
    • 根据字段的类型递归地对行字段进行校验和计算。

对于所有其他列类型,使用checksum()函数生成一个简单的校验和。

确定性

结果不匹配,无论是行计数不匹配还是列不匹配,都可能是由非确定性查询功能引起的。为了避免错误警报,我们对控制查询执行确定性分析。如果发现查询是非确定性的,我们将跳过验证,因为它不会提供任何见解。

确定性分析遵循以下步骤。如果在任何时候发现查询是非确定性的,则分析将得出结论。

  • 可以使用determinism.non-deterministic-catalog指定非确定性目录。如果查询引用了这些目录中的任何表,则该查询将被视为非确定性。

  • 再次运行控制查询,并将结果与初始控制查询运行进行比较。

  • 如果查询在顶层有一个LIMIT n子句,但没有ORDER BY子句
    • 运行一个查询来计算在没有LIMIT子句的情况下,控制查询产生的行数。

    • 如果结果行数大于n,则将控制查询视为非确定性。

故障解决

配置差异,包括集群大小,会导致查询在控制集群上成功,但在测试集群上失败。校验和查询也可能失败,这可能是由于Presto或Presto Verifier的限制造成的。因此,我们允许Verifier自动解决某些查询故障。

  • EXCEEDED_GLOBAL_MEMORY_LIMIT:如果控制查询使用的内存比测试查询多,则解决。

  • EXCEEDED_TIME_LIMIT:无条件解决。

  • TOO_MANY_HIVE_PARTITIONS:如果测试集群没有足够的worker来确保分配给每个worker的分区数保持在限制范围内,则解决。

  • COMPILER_ERRORGENERATED_BYTECODE_TOO_LARGE:如果控制校验和查询以该错误失败,则解决。如果控制查询有太多列,则在某些情况下,生成的校验和查询可能会太大。

在结果不匹配的情况下,Verifier可能正在发出噪声信号,我们允许Verifier自动解决某些不匹配。

  • 结构化类型列:如果数组元素或映射键/值包含浮点类型,则列校验和不太可能匹配。
    • 对于数组列,如果元素类型包含浮点类型并且基数校验和匹配,则解决。

    • 对于映射列,当以下两个条件都为真时,解决不匹配
      • 基数校验和匹配。

      • 不包含浮点类型的键或值的校验和匹配。

    • 仅当所有列都被解决时,才解决测试用例。

  • 已解决的函数:在结果不匹配的情况下,如果查询在

    指定列表中使用了一个函数,则测试用例将被标记为已解决。

解释模式

在解释模式下,Verifier检查源查询是否可以被解释,而不是它们是否产生相同的结果。当控制查询和测试查询都可以被解释时,验证将被标记为成功。

输出事件中的字段matchType可以用作控制运行和测试运行之间是否存在计划差异的指示器。

对于非DML查询,将跳过控制查询和计划比较。

扩展Verifier

除了配置属性之外,Verifier还可以扩展以进一步更改行为。

AbstractVerifyCommand显示了可以扩展的组件。实现抽象类并创建一个类似于PrestoVerifier的命令行包装器。

配置参考

常规配置

名称

描述

whitelist

一个逗号分隔的列表,指定要验证的套件中查询的名称。

blacklist

一个逗号分隔的列表,指定要从套件中排除的查询的名称。blacklistwhitelist之后应用。

source-query-supplier

源查询提供程序的名称。支持mysql

source-query.table-name

保存验证器查询的表的名称。仅当source-query-suppliermysql时可用。

event-clients

一个逗号分隔的列表,指定应该将输出事件发射到哪里。支持jsonhuman-readable

json.log-file

JSON事件的输出文件。如果没有设置,则将JSON事件发射到stdout

human-readable.log-file

人类可读事件的输出文件。如果没有设置,则将人类可读事件发射到stdout

control.table-prefix

要附加到控制目标表的表前缀。

test.table-prefix

要附加到测试目标表的表前缀。

control.reuse-table

如果 true,则重用控制源 Insert 和 CreateTableAsSelect 查询的输出表。否则,运行控制源查询并写入临时表。

test.reuse-table

如果 true,则重用测试源 Insert 和 CreateTableAsSelect 查询的输出表。否则,运行测试源查询并写入临时表。

test-id

要附加到输出事件的字符串。

max-concurrency

并发验证的最大数量。

suite-repetition

套件验证的次数。

query-repetition

源查询验证的次数。

relative-error-margin

控制总和与浮点列测试总和之间可容忍的最大相对误差。

absolute-error-margin

低于此阈值的浮点平均值被视为 0

run-teardown-on-result-mismatch

结果不匹配时是否运行拆卸查询。

verification-resubmission.limit

源查询重新提交以进行验证的次数限制。

running-mode

设置为 query-bank 使验证器在 query-bank 模式下运行。支持 query-bankcontrol-test。默认值为 control-test

save-snapshot

设置为 true 将校验和保存到 mysql 数据库。

extended-verification

设置为 true 对写入的表运行扩展的表布局验证。它仅适用于 InsertCreateTableAsSelect 查询。如果插入的表是分区的,它将验证每个分区的校验和。如果插入的表是分桶的,它将验证每个桶的校验和。

function-substitutes

函数替换规范,格式为 /foo(c0,_)/bar(c0)/,/fred(c0,c1)/baz(qux(c1,c0))/,/foobar(c0)/if(qux(c1),bar(c0),baz(c1))/,...,其中 foo(c0, _) 将被替换为 bar(c0),声明的论据应用于相应的位置。

用逗号连接函数替换。

选择一个返回值类型和参数类型与原始函数兼容的函数替换,以生成有效的源查询。例如,/array_agg(z)/array_sort(array_agg(z))/,/approx_percentile(x,y)/avg(x)/

如果需要将函数参数应用于函数替换,则将其声明为标识符。

查询覆盖配置

以下配置控制验证开始之前查询元数据修改的行为。测试查询的对应项也可用,前缀 control 被替换为 test

名称

描述

control.catalog-override

如果指定,要应用于所有查询的目录。

control.schema-override

如果指定,要应用于所有查询的模式。

control.username-override

如果指定,要应用于所有查询的用户名。

control.password-override

如果指定,要应用于所有查询的密码。

control.session-properties-override-strategy

支持 3 个值。NO_ACTION:使用为每个查询指定的会话属性。OVERRIDE:将每个查询的会话属性与覆盖合并,覆盖为主导。SUBSTITUTE,每个查询的会话属性将被覆盖替换。

control.session-properties-override

要应用于所有查询的会话属性。

查询执行配置

以下配置控制控制集群上查询执行的行为。测试集群的对应项也可用,前缀 control 被替换为 test

名称

描述

control.hosts

控制集群主机名或 IP 地址的逗号分隔列表。

control.jdbc-port

控制集群的 JDBC 端口。

control.http-host

控制集群的 HTTP 端口。

control.jdbc-url-parameters

表示控制 JDBC 的附加 URL 参数的 JSON 映射。

control.query-timeout

控制查询和测试查询的执行时间限制。

control.metadata-timeout

的执行时间限制 DESC 查询和 LIMIT 0 查询。

control.checksum-timeout

校验和查询的执行时间限制。

control.application-name

要传递到 ClientInfo 的 ApplicationName。可用于设置源。

确定性分析器配置

名称

描述

determinism.run-teardown

是否为确定性分析中生成的表运行拆卸查询。

determinism.max-analysis-runs

要检查控制查询确定性的附加控制运行的最大次数。

determinism.handle-limit-query

是否启用对具有顶级 LIMIT 子句的查询的特殊处理。

determinism.non-deterministic-catalogs

非确定性目录的逗号分隔列表。引用来自这些目录的表的查询被视为非确定性的。

故障解决配置

名称

描述

exceeded-global-memory-limit.failure-resolver.enabled

是否启用测试查询失败的故障解决程序 EXCEEDED_GLOBAL_MEMORY_LIMIT

exceeded-time-limit.failure-resolver.enabled

是否启用测试查询失败的故障解决程序 EXCEEDED_TIME_LIMIT

verifier-limitation.failure-resolver.enabled

是否启用由于验证器限制而导致的故障的故障解决程序。

too-many-open-partitions.failure-resolver.enabled

是否启用测试查询失败的故障解决程序 HIVE_TOO_MANY_OPEN_PARTITIONS

too-many-open-partitions.max-buckets-per-writer

控制集群和测试集群上配置的每个编写器的最大桶数。

too-many-open-partitions.cluster-size-expiration

测试集群大小缓存的时间限制。

structured-column.failure-resolver.enabled

是否启用结构化类型列的列不匹配的故障解决程序。

ignored-functions.failure-resolver.enabled

是否启用 IgnoredFunctions 结果不匹配故障解决程序。

ignored-functions.functions

逗号分隔的函数列表。如果查询使用列表中的任何函数,则解决不匹配。