Python SDK 性能驱动的运行时类型检查

在这篇博文中,我们将宣布 Beam 的 Python SDK 的一个新的可选运行时类型检查系统即将发布,该系统针对开发和生产环境的性能进行了优化。

但让我们退一步 - 为什么我们首先要关心运行时类型检查?让我们看一个例子。

class MultiplyNumberByTwo(beam.DoFn):
    def process(self, element: int):
        return element * 2

p = Pipeline()
p | beam.Create(['1', '2'] | beam.ParDo(MultiplyNumberByTwo())

在这段代码中,我们将字符串列表传递给一个 DoFn,该 DoFn 明显旨在与整数一起使用。幸运的是,这段代码将在管道构建期间抛出错误,因为 beam.Create(['1', '2']) 的推断输出类型为 str,与 MultiplyNumberByTwo.process 的声明输入类型 int 不兼容。

但是,如果我们使用 no_pipeline_type_check 标志关闭了管道类型检查?或者更现实地说,如果 MultiplyNumberByTwo 的输入 PCollection 来自数据库,这意味着输出数据类型只能在运行时才知道?

无论哪种情况,在管道构建期间都不会抛出错误。即使在运行时,这段代码也能运行。每个字符串都将乘以 2,产生 ['11', '22'] 的结果,但这肯定不是我们想要的结果。

那么如何调试这种“隐藏”错误?更广泛地说,如何调试 Beam 中任何类型或序列化错误?

答案是使用运行时类型检查。

运行时类型检查 (RTC)

此功能通过在管道执行期间检查实际输入和输出值是否满足声明的类型约束来工作。如果使用 runtime_type_check 运行之前的代码,您将收到以下错误消息

Type hint violation for 'ParDo(MultiplyByTwo)': requires <class 'int'> but got <class 'str'> for element

这是一个可操作的错误消息 - 它告诉您,您的代码存在错误,或者您的声明类型提示不正确。听起来很简单,那么有什么问题呢?

它太慢了。自己看看。

元素大小普通管道运行时类型检查管道
15.3 秒5.6 秒
2,0019.4 秒57.2 秒
10,00124.5 秒259.8 秒
18,00138.7 秒450.5 秒

在这个微基准测试中,使用运行时类型检查的管道速度慢了 10 倍以上,随着输入 PCollection 大小的增加,差距只会越来越大。

那么,是否有任何生产环境友好的替代方案?

性能运行时类型检查

有!我们开发了一个名为 performance_runtime_type_check 的新标志,它使用以下方法最大程度地减少了管道时间复杂度的占用量:

  • 高效的 Cython 代码,
  • 智能采样技术,以及
  • 优化的巨型类型提示。

那么新的数字是什么样的呢?

元素大小普通RTC性能 RTC
15.3 秒5.6 秒5.4 秒
2,0019.4 秒57.2 秒11.2 秒
10,00124.5 秒259.8 秒25.5 秒
18,00138.7 秒450.5 秒39.4 秒

平均而言,新的性能 RTC 比普通管道慢 4.4%,而旧的 RTC 慢了 900% 以上!此外,随着输入 PCollection 大小的增加,设置性能 RTC 系统的固定成本会分散到每个元素中,从而减少了对整体管道的影响。使用 18,001 个元素,差异不到 1 秒。

它是如何工作的?

此性能升级有三个关键因素。

  1. 我们不会对所有值进行类型检查,而是只对值的子集进行类型检查,在统计学中称为样本。最初,我们会对大量的元素进行采样,但随着我们对元素类型不会随时间变化的信心越来越强,我们会降低我们的采样率(直到固定最小值)。

  2. 旧的 RTC 系统使用重量级包装器来执行类型检查,而新的 RTC 系统将类型检查转移到代码库中经过 Cython 优化的、未装饰的部分。参考一下,Cython 是一种编程语言,它为 Python 代码提供类似 C 的性能。

  3. 最后,我们使用单个巨型类型提示来仅对转换的输出值进行类型检查,而不是分别对输入和输出值进行类型检查。此巨型类型提示由原始转换的输出类型约束以及所有使用者转换的输入类型约束组成。使用此巨型类型提示,我们可以减少开销,同时还可以抛出更多可操作的错误。例如,考虑以下错误(由旧的 RTC 系统生成)

Runtime type violation detected within ParDo(DownstreamDoFn): Type-hint for argument: 'element' violated. Expected an instance of <class ‘str’>, instead found 9, an instance of <class ‘int’>.

此错误告诉我们 DownstreamDoFn 收到了一个 int,而它期望的是一个 str,但没有告诉我们这个 int 是从哪里来的。哪个违规的上游转换负责这个 int?可以推测,转换的输出类型提示太宽泛了(例如 Any)或者根本不存在,因为在对其输出进行运行时类型检查时没有抛出错误。

这里的问题归结为缺乏上下文。如果我们在对输出进行类型检查时知道我们的使用者是谁,我们就可以同时对输出值进行类型检查,以验证其是否符合我们的输出类型约束和所有使用者的输入类型约束,以了解是否存在任何不匹配的可能性。这正是巨型类型提示所做的,它使我们能够在声明点而不是异常点抛出错误,从而节省您的宝贵时间,同时提供更高质量的错误消息。

那么,使用性能 RTC 后,同一个错误会是什么样的呢?它是完全相同的字符串,但多了一行

[while running 'ParDo(UpstreamDoFn)']

这对于调查来说更有帮助 :)

下一步

去尝试新的 performance_runtime_type_check 功能!

它处于实验状态,因此如果您遇到任何问题,请联系我们