Runner 开发指南
邵凯 译 邮箱: shaokai132333@163.com CSDN:shaokai132333
英文原版参见: Runner Authoring Guide
本指南将介绍如何实现新的Runner程序,目标人群是有一个数据处理系统,并希望使用它来执行Beam管道的开发者。本指南从基础出发,帮助您评估未来的工作。接着,后续部分越来越详细,可以用作整个开发过程中的参考资料。
包括如下主题;
- Beam 模型基础
- Pipeline
- PTransforms
- PCollections
- 有界与无界
- 时间戳
- Watermarks
- 窗口元素
- Coder
- 窗口化策略
- 用户自定义函数
- Runner
- 实现Beam原语
- 如果你还没有实现这些特性呢?
- 实现ParDo原语
- Bundles
- DoFn的生命周期
- 旁路输入
- 状态和计算器
- 可拆分的DoFn
- 实现GroupByKey(和窗口)原语
- 按字节编码分组
- 窗口合并
- 通过GroupByKeyOnly + GroupAlsoByWindow实现
- 丢弃迟到数据
- 触发
- 时间戳合并器
- 实现窗口原语
- 实现Read原语
- 从无界数据源读入
- 从有界数据源读入
- 实现Flatten原语
- 特别说明:Combine合成
- 与管道协作
- 遍历pipeline
- 更改pipeline
- 测试你的Runner
- 将你的Runner与SDK完美结合
- 与Java SDK集成
- 允许用户传递配置选项给Runner
- 在SDK中注册Runner供命令行使用
- 与Python SDK集成
- 与Java SDK集成
- 编写SDK无关的Runner
- Fn API
- Runner API
- Runner API proto说明
- FunctionSpec proto
- SdkFunctionSpec proto
- Primitive transform payload protos
- ParDoPayload proto
- ReadPayload proto
- WindowIntoPayload proto
- CombinePayload proto
- Runner API 远程调用
- PipelineRunner.run(Pipeline) RPC
- PipelineResult aka “Job API”
Beam 模型基础
如果您有一个数据处理引擎,它可以非常容易地处理操作图。您希望将它与Beam生态系统集成,以支持其他语言,出色的事件时间处理和连接器库。你需要知道核心词汇:
- Pipeline- 管道是一个用户构造的转换图,定义了他们想要进行的数据处理。
- PCollection-在管道中处理的数据是PCollection的一部分。
- PTransforms-在管道中执行的操作。最好将这些视为对PCollection的操作。
- SDk-特定语言的库,Pipeline 开发者用SDK构建转换、构建管道并将其提交给Runner;
- Runner-你要写的一个叫做Runner的软件,它持有一个Beam Pipeline,并使用你的数据处理引擎的能力来执行它。
这些概念可能非常类似于处理引擎的概念。由于beam的设计是用于跨语言操作和可重用的转换库,因此有一些特殊的特性值得强调。
PipeLine
Beam中的Pipeline是PTransforms组成的的图,PTransforms是对PCollection的操作。Pipeline由用户使用其选择的SDK构建,并直接通SDK或通过Runner API(即将推出)的RPC接口向Runner传递管道。
PTransforms
在Beam中,PTransform可以是五个原语之一,也可以是封装子图的复合变换。原语是:
- Read-外部系统的并行连接器
- ParDo-每个元素处理
- GroupByKey-按键和窗口聚合元素
- Flatten-PCollection的并集
- Window-设置PCollection的窗口化策略
在实现Runner时,这些是您需要实现的操作。复合转换对Runner可能重要,也可能不重要。如果你暴露一个UI,维护一些复合结构将使pipeline更容易让用户理解。但无论是否使用复合转换处理结果一致。
PCollections
PCollection是一个无序的元素包。你的Runner将负责存储这些元素。PCollection的一些主要方面需要注意:
一个PCollection可以是有界或者无界的。
- 有界-是有限的,你知道它什么情况结束,就像在批处理中一样。
- 无界-它可能永远不会结束,你不知道它什么时候结束,就像在流式处理中的那样。
它们来源于批处理和流处理的直觉,但两者在Beam中是统一的,并且有界和无界的集合可以在同一Pipeline中共存。如果你的Runner只能支持有界的的PCollection,则需要拒绝包含无界PCollection的管道。如果您的Runner只针对流,那么在我们的支持代码中有适配器可以将所有内容转换为API要求的无界数据。
PCollection 中的每个元素都有一个与其相关联的时间戳。
当您对某个存储系统执行基本连接器时,该连接器负责提供初始时间戳。您的Runner将需要传递和聚合时间戳。如果时间戳不重要,就像某些批处理作业中的元素不表示事件一样,用最小值来表示时间戳,通常通俗地称为“负无穷大”。
每个PCollection都必须有一个watermarks来估计PCollection的完成程度。
Watermarks有一个假定,“我们将永远看不到将来时间的元素”。数据源负责生成Watermarks。当处理、合并和分区PCollections时,Runner需要实现Watermarks传播。
当Watermarks前进到“无穷大”时,PCollection的内容就完成了。通过这种方式,你会发现一个无边界的pcollection是有限的。
PCollection中的每个元素都驻留在一个窗口中。没有元素驻留在多个窗口(两个元素可以相等,但它们的窗口不同);
当从外部世界读取元素时,它们将到达全局窗口。当它们被写到外部世界时,它们被有效地放回到全局窗口中(任何不从这个角度看的写转换都可能会有数据丢失的风险)。
每个窗口有个最大时间戳,当Watermarks超过此时间戳加上用户指定的允许延迟时,窗口将过期。与过期窗口相关的所有数据都可以随时丢弃。
每个PCollection都有一个coder,coder是一个元素二进制格式的规范。
在Beam中,用户的管道可以用Runner语言以外的语言编写。Beam不要求Runner可以实际反序列化用户数据。因此,Beam模型主要在编码的数据上运行——“just bytes”。每个pcollection都有一个声明的元素编码,称为coder。coder有一个标识编码的URN,并且可能有附加的子coder(例如,列表的coder可能包含列表元素的coder)。特定于语言的序列化技术可以使用,并且经常使用,但有一些通用的键格式(如键-值对和时间戳),因此Runner可以理解它们。
每个PCollection都有一个窗口化策略,窗口化策略是分组和触发操作的基本信息规范。
下面我们将在讨论窗口原语(它设置了窗口化策略)和GroupByKey原语(它的行为受窗口化策略控制)时详细讨论。
用户自定义函数(UDFs)
Beam有七种用户自定义函数(UDF)。Beam Pipeline可能包含以Runner以外的语言编写的UDF,甚至同一管道中包含多种语言(请参阅运行程序API),因此定义与语言无关(请参阅Fn API)。
Beam 的UDF为:
- DoFn-每个元素处理函数(用于ParDo)
- WindowFn-在窗口中放置元素并合并窗口(用于Window和GroupBykey)
- Source-发出从外部源读取的数据,包括并行机制的初始化和动态拆分(用于Read)。
- ViewFn-将物化PCollection适配到特定接口(用于旁路输入)
- WindowMappingFn-将一个元素的窗口转换到另一个窗口,并指定结果窗口过去的范围(用于旁路输入)
- CombineFn-结合和交换聚合(用于Combine和state)
- Coder-编码用户数据;有些coder有标准格式,并非真正的UDF。
用户定义函数的各种类型将与使用它们的原语一起进一步描述。
Runner
“runner”一词多用,它通常指的是持有Beam Pipeline并且以某种方式执行它的软件,也指你编写的翻译pipeline 的代码,它还包括为你的处理引擎定制的操作,有时候指的是全栈。
Runner仅有一个方法run(Pipeline)。从这里开始,我将经常在我们的API中使用代码字体作为专有名词,虽然这些标识符并非所有SDK中都一致。
run(Pipeline)方法应该是异步的,在PipelineResult里的results它通常是你的数据处理引擎的作业描述符,提供检查任务状态、取消任务以及等待任务终止的方法。
实现Beam原语
除了编码和持久化数据(这可能是您的引擎已经以某种方式完成的),您需要做的大部分工作是实现Beam原语。本节详细介绍了每个原语,包括您需要知道的可能不明显的内容以及提供了哪些支持代码。
这些原语是为pipeline作者而不是Runner作者的利益而设计的。每个代表不同的概念操作模式(外部IO、按元素、分组、开窗、联合),而不是特定的实现决策。同一个原语可能需要非常不同的实现,这取决于用户如何实例化它。例如,使用状态或计时器的ParDo可能需要键分区,具有推测性触发的GroupByKey可能需要更昂贵或更复杂的实现,并且对有界和无界数据的读取完全不同。
没关系!你不必一次完成所有的任务,甚至可能有一些功能对你的Runner来说根本没有意义。我们在Beam站点上维护一个能力矩阵,这样您就可以告诉用户您支持什么。当您收到pipeline时,您应该遍历它,并确定是否可以执行找到的每个DoFn。如果无法在pipeline中执行某些DoFn(或者如果Runner缺少任何其他要求),则应拒绝pipeline。在您的原生的环境中,这可能看起来像抛出了一个UnsupportedOperationException。为了跨语言的可移植性,Runner API RPCs将明确说明这一点。
实现ParDo原语
ParDo原语描述了PCollection的元素转换。ParDo是最复杂的原语,它是描述每个元素处理的地方。除了非常简单的操作(如来自函数式编程的标准map或flatMap),ParDo还支持多个输出、侧输入、初始化、刷新、拆卸和状态处理。
应用于每个元素的UDF称为DoFn。DoFn的确切API可能因语言/SDK而异,但通常遵循相同的模式,因此我们可以用伪代码来讨论它。我也经常提到Java支持代码,因为我熟悉它,并且我们当前和将来的大多数Runner都是基于Java的。
为了正确起见,DoFn应该表示一个面向元素的函数,但实际上它是一个长期存在的对象,它以称为bundles的小组处理元素。
你的Runner可以决定在一个包中包含多少元素以及哪些元素,甚至可以在处理过程中动态地决定当前bundle已经“结束”。如何处理一个bundle与DoFn生命周期的其余部分联系在一起。
通常尽可能大的bundle能提供吞吐量,从而使初始化和结束成本分摊到许多元素上。但是,如果您的数据是以流的形式到达的,那么您将希望终止一个包以实现适当的延迟,因此包可能只是几个元素。
DoFn生命周期
虽然每个语言的SDK都可以自由地做出不同的决定,但是Python和Java SDKs共享一个具有以下阶段的DoFn的生命周期的API。但是,如果你选择直接执行DoFn以提高性能或单语言的简单性,那么您的Runner需要负责实现以下序列:
- Setup-在执行任何其他操作之前对每个DoFn实例调用一次;这在Python SDK中没有实现,用户只需进行延迟初始化即可解决问题。
- StartBundle-作为初始化对每个bundle调用一次(实际上,延迟初始化几乎总是等价的,而且效率更高,但是为了用户的简单性,这个钩子仍然存在)
- ProcessElement|OnTimer-为每个元素和计时器激活调用
- FinishBundle-基本上是“flush”;在实际处理的元素之前需要调用
- Teardown-释放跨bundle资源;由于失败,调用此资源可能是最大的努力。
这是一个支持类,在Java代码库和Python代码库中都有出现。
Java
在Java中,beam-runners-core-java库为bundle处理提供一个接口DoFnRunner ,并实现了许多情况。
interface DoFnRunner<InputT, OutputT> {
void startBundle();
void processElement(WindowedValue<InputT> elem);
void onTimer(String timerId, BoundedWindow window, Instant timestamp, TimeDomain timeDomain);
void finishBundle();
}
对于不同的场景,这有一些实现和变化:
- SimpleDoFnRunner-一点都不简单;实现了ParDo的很多核心功能。这就是大多数Runner如何执行大多数DoFn的方式。
- LateDataDroppingDoFnRunner-包装一个DoFnRunner并且并从过期窗口中删除数据,这样包装的DoFnRunner不会得到任何不愉快的惊喜。
- StatefulDoFnRunner-收集过期状态的句柄
- PushBackSideInputDoFnRunner-在等待侧输入准备就绪的同时缓冲输入。
这些都在Java Runner的实现中大量使用。通过Fn API进行的调用可能表现为DoFnRunner的另一个实现,尽管它所做的远不止运行DoFn。
Python
参见Python DoFnRunner pydoc.
旁路输入
主要设计文档:https://s.apache.org/beam-side-inputs-1-pager
旁路输入是PCollection窗口的全局视图。这将它与一次处理一个元素的主输入区分开来。SDK/用户充分准备了一个PCollection,Runner将其具体化,然后Runner将其提供给DoFn。您将需要实现的是检查为侧输入请求的物化,并适当地准备它,以及当DoFn读取旁路输入时的相应交互。
详细信息和可用的支持代码因语言而异。
Java
如果您正在使用上面的某个DoFnRunner类,那么允许它们请求侧输入的接口是SideInputReader。它是从旁路输入和窗口到值的简单映射。DoFnRunner将使用WindowMappingFn执行映射,以请求适当的窗口,这样您就不用担心调用这个UDF了。当使用Fn API时,它也将是映射窗口的SDK工具。
构建SideInputReader的一个简单但不一定是最佳方法是使用状态后端。在我们的Java支持代码中,这被称为StateInternals 并且您可以构建一个SideInputHandler 它将使用你的StateInternals 具体化一个PCollection为合适的旁路输入视图,然后然后在特定的旁路输入和窗口请求时产生该值。
当需要一个侧输入,但是侧输入没有与给定窗口相关联的数据时,该窗口中的元素必须延迟,直到侧输入有一些数据。上面提到的PushBackSideInputDoFnRunner用于实现这一点。
Python
在Python中,SideInputMap 将窗口映射到侧输入值。WindowMappingFn 显示为一个简单函数。参见 sideinputs.py.
状态和计时器
主要设计文档: https://s.apache.org/beam-state
当ParDo包含状态和计时器时,它在Runner上的执行通常是非常不同的。参阅此处所述内容之外的全部详细信息。状态和计时器按键和窗口进行分区。你可能需要或想要显式地shuffle数据以支持这一点。
Java
我们提供StatefulDoFnRunner来帮助状态清理。非面向用户的接口StateInternals通常由Runner实现,然后Beam支持代码可以使用它来实现面向用户的状态。
主要设计文档:https://s.apache.org/splittable-do-fn
可拆分的DoFn是ParDo和Read的泛化和组合。它是按元素处理的,其中每个元素都具有与有界源或无界源相同的“拆分”功能。这使您能够更好地处理,例如你要读取名称存在PCollection中的每个大文件,之类的情况。以前它们必须是pipeline中的静态数据,或者以不可分割的方式读取。
这一功能仍在开发中,但很可能成为新的读入原语。最好注意并跟踪开发进展。
实现GroupByKey(and window)原语
GroupByKey操作(有时简称为gbk)按键和窗口对键值对PCollection进行分组,根据PCollection的触发配置发送结果。它比简单地将元素按照相同的键放在一起要复杂得多,并且使用了PCollection窗口策略中的许多字段。
对与键和窗口,你的Runner将它们视为“just bytes”。因此,您需要以与按这些字节分组一致的方式进行分组,即使您对涉及的类型有一些特殊的了解。
您正在处理的元素将是键值对,您需要提取键值。因此,键值对的格式被标准化,并在所有SDK中共享。关于二进制格式的文档,Java中看KvCoder或者Python中看TupleCoder 。
除了按键分组之外,你的Runner还必须按元素的窗口对其进行分组。WindowFn可以声明它根据每个键合并窗口。例如,如果相同键的会话窗口重叠,则它们将被合并。因此,Runner必须在分组期间调用WindowFn的merge方法。
通过GroupByKeyOnly + GroupAlsoByWindow实现
Java代码库包含用于实现完全GroupByKey
操作的一种特别常见的方法的支持代码:首先按照键分组,然后按照窗口分组。对于合并窗口,这基本上是必需的,因为合并是按键进行的。
主要设计文档:https://s.apache.org/beam-lateness
如果输入PCollection的watermark超过了输入PCollection允许的延迟,则PCollection中的窗口将过期。
过期窗口的数据可以随时删除,应在GroupByKey处删除。如果您使用的是GroupAlsoByWindow,则在执行此转换之前。如果在GroupByKeyOnly之前删除数据,则可以减少数据的无序移动,但只能安全地对未合并的窗口执行,因为出现过期的窗口可能合并为未过期的窗口。
主要设计文档:https://s.apache.org/beam-triggers
输入PCollection的触发器和累积模式指定从GroupByKey操作发出输出的时间和方式。在Java中,在GroupAlsoByWindow 实现、ReduceFnRunner 和TriggerStateMachine中有很多执行触发器的支持代码,这是一种很明显的方法,可以将所有触发器作为一个事件驱动的机器在元素和计时器上实现。
当从多个输入生成聚合输出时,GroupByKey操作必须为组合选择时间戳。为此,首先,Windowfn有机会移动时间戳-这是为了确保watermarks不会阻止诸如滑动窗口之类的窗口的进展(详细信息在本文档之外)。然后,需要组合移动的时间戳-这是由TimestampCombiner指定的,它可以选择其输入的最小值或最大值,也可以忽略输入并选择窗口的结尾。
实现窗口原语
窗口原语应用WindowFn UDF将每个输入元素放入其输出PCollection的一个或多个窗口中。请注意,窗口原语还通常为PCollection配置窗口化策略的其他方面,但是Runner收到的完全构造的图已经为每个PCollection提供了完整的窗口化策略。
要实现这个原语,你需要在每个元素上调用提供的WindowFn,它将为该元素返回一些窗口集,并成为输出PCollection的一部分。
实现注意事项
“窗口”只是具有“最大时间戳”的第二个分组键。它可以是任意的用户定义类型。WindowFn为窗口类型提供编码器。
Beam的支持代码提供了WindowedValue,它是多个窗口中一个元素的压缩表示。您可能需要使用这个,或者您自己的压缩表示。请记住,它只是同时表示多个元素;没有在多个窗口中的元素这样的东西。
对于全局窗口中的值,您可能希望使用一个更进一步的压缩表示,它根本不需要包括窗口。
将来,如果增强ParDo的功能以允许输出到新窗口,则可以作为ParDo来实现此原语,从而使其退役。
实现Read原语
您可以实现这个原语来从外部系统读取数据。这些API经过精心设计以实现高效的并行执行。从无边界源读取与从有边界源读取有点不同。
无界源是潜在无限数据的源;您可以将其视为流。功能包括:
- split(int)-你的Runner应该调用调用它来获得所需的并行性。
- createReader(...)-调用此函数开始读取元素;它是一个增强的迭代器,也提供:
- watermark(对于此数据源)应将其传播到下游时间戳,并应将其与读取的元素相关联。
- 记录标识符,以便在需要时对下游进行重复数据消除
- 其积压工作的进度指示
- 检查点
- requiresDeduping-这表示源可能发出重复数据;你的Runner应尽最大努力根据附加到已发出记录的标识符进行重复数据消除。
无边界源有一个自定义类型的检查点和一个用于序列化它们的相关编码器。
从有界数据源读取
BoundedSource是有限数据的数据源,例如日志文件的静态集合或数据库表。它的功能是:
- split(int)-你的Runner应该调用这个来获得所需的初始并行性(但你以后可以经常偷工减料)
- getEstimatedSizeBytes(...)-此功能见名知意
createReader(...)
-调用此函数以开始读取元素;它是一个增强的迭代器,还具有:与读取的每个元素关联的时间戳splitAtFraction
-用于动态拆分以实现工作窃取,以及其他支持它的方法-请参阅 Beam blog post on dynamic work rebalancing
BoundedSource 当前不汇报watermark。大多数情况下,从一个有界数据源读取数据可以以完全无序的方式进行并行处理,因此watermark不是十分有用处。因此,在整个读取过程中,来自有界读取的输出PCollection的watermark应保持在最小时间戳(否则数据可能会丢失),并在所有数据耗尽时前进到最大时间戳。
实现Flatten原语
这个简单-它用PCollection的一个有限集作为输入,并输出它们的并集,同时保持窗口的完整性。
为了使这个操作有意义,SDK负责确保窗口策略是兼容的。
还要注意,没有要求所有集合的coder都是相同的。如果你的Runner想要这样做(为了避免冗长的重新编码),你必须自己去执行。或者您可以将快速路径作为优化实现。
特别说明:复合Combine
Combine
(按键)是一个总是被Runner专门处理的复合转换,它对PCollection的元素应用关联和交换操作符。此复合不是原语。它是以ParDo和GroupByKey的形式实现的,因此Runner不需要处理它就可以工作——但是它确实携带了一些您可能希望用于优化的附加信息:关联交换操作符,即 CombineFn
。
与pipeline协作
当您收到来自用户的管道时,需要对其进行翻译。这是一个您将使用的API教程。
你可能要做的事情是遍历pipeline,将其转换为引擎的原语。一般的模式是编写一个访问者,在浏览PTransforms图时构建一个作业规范。遍历pipeline的入口点在Java 中是 Pipeline.traverseTopologically 在Python中是Pipeline.visit 。有关详细信息,请参阅生成的文档。
更改pipeline
通常,保持翻译简单的最好方法是在翻译之前更改pipeline。你可能会进行一些更改:
- 将Beam 原语转换为使用Runner特定原语的复合transform
- 将Beam 组合优化为你的Runner的特定原语
- 将Beam组合替换为适合你的Runner的不同的扩展
Java SDK和“runner核心构建”库(构件是beam-runners-core-construction-java 命名空间是 org.apache.beam.runners.core.construction)包含这类工作的帮助代码。Python语言的支持代码还在开发中。
所有的pipeline更改都是通过 Pipeline.replaceAll(PTransformOverride)方法完成的。PTransformOverride是一对PTransformMatcher(用于选择要替换的transform)和PTransformOverrideFactory (用于生成目标transform)。Runner迄今为止需要的所有PTransformMatchers都提供了。例如:匹配一个特定的类,匹配一个其DoFn使用状态或计时器的ParDo,等等。
测试你的Runner
Beam Java SDK和Python SDK具有Runner验证测试套件。配置可能比本文档发展得更快,因此请检查其他Beam Runner的配置。但请注意,我们有测试,您可以很容易地使用它们!使用Gradle 基于Java语言的Runner为了启用这些测试,你可以扫描SDK依赖关系,以查找JUnit类别ValidatesRunner的测试。
task validatesRunner(type: Test) {
group = "Verification"
description = "Validates the runner"
def pipelineOptions = JsonOutput.toJson(["--runner=MyRunner", ... misc test options ...])
systemProperty "beamTestPipelineOptions", pipelineOptions
classpath = configurations.validatesRunner
testClassesDirs = files(project(":sdks:java:core").sourceSets.test.output.classesDirs)
useJUnit {
includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner'
}
}
使用其他语言启用这些测试是还未探索过。
将你的Runner与SDK完美集成
无论您的Runner是否与SDK(如Java)相同的语言,如果您希望SDK的用户(如Python)使用它,你将希望提供一个楔子从其他SDK调用它。
允许用户将选项传递给你的Runner
配置的机制是PipelineOptions一个与普通Java对象完全不同的接口。忘记你所知道的,遵循规则,PipelineOptions会对你很好。你必须为你的Runner实现一个子接口,它具有与配置选项对应名称的getters和setters方法,如:
public interface MyRunnerOptions extends PipelineOptions {
@Description("The Foo to use with MyRunner")
@Required
public Foo getMyRequiredFoo();
public void setMyRequiredFoo(Foo newValue);
@Description("Enable Baz; on by default")
@Default.Boolean(true)
public Boolean isBazEnabled();
public void setBazEnabled(Boolean newValue);
}
您可以设置默认值等。有关详细信息,请参阅javadoc。当使用PipelineOptions对象实例化Runner时,可以通过options.as(MyRunnerOptions.class)访问接口,获取配置项。
要使这些选项在命令行上可用,请使用PipelineOptionsRegistrar注册选项。使用@AutoService很容易:
@AutoService(PipelineOptionsRegistrar.class)
public static class MyOptionsRegistrar implements PipelineOptionsRegistrar {
@Override
public Iterable<Class<? extends PipelineOptions>> getPipelineOptions() {
return ImmutableList.<Class<? extends PipelineOptions>>of(MyRunnerOptions.class);
}
}
在SDKs中注册运行程序以供命令行使用
要使运行程序在命令行上可用,请使用PipelineRunnerRegistrar 注册选项。使用@AutoService很容易:
@AutoService(PipelineRunnerRegistrar.class)
public static class MyRunnerRegistrar implements PipelineRunnerRegistrar {
@Override
public Iterable<Class<? extends PipelineRunner>> getPipelineRunners() {
return ImmutableList.<Class<? extends PipelineRunner>>of(MyRunner.class);
}
}
与Python sdk集成
在python sdk中,代码的注册不是自动的。因此,在创建新的Runner时,几乎没有什么需要记住的。
对新的Runner的任何依赖都应该是可选项,因此在新的Runner所需的setup.py 的extra_requires中创建一个新目标。
所有的Runner代码都应该放在apache_beam/runners目录中它自己的包中。
在runner.py的create_runner函数中注册新的runner,以便部分名称与要使用的正确类相匹配。
编写一个独立于SDK的Runner
有两个方面使您的Runner 独立于SDK,能够运行用其他语言编写的pipeline:Fn API和Runner API。
设计文档:
- https://s.apache.org/beam-fn-api
- https://s.apache.org/beam-fn-api-processing-a-bundle
- https://s.apache.org/beam-fn-api-send-and-receive-data
要运行用户的pipeline,您需要能够调用用户的自定义函数(UDF)。Fn API 是用于beam标准UDF的RPC接口,使用gRPC上的protocol buffers实现。
Fn API包括:
- 用于注册UDF子图的API
- bundle 流元素的API
- 共享数据格式(键值对、时间戳、iterables等)
我们完全欢迎您在实用程序代码的语言中使用SDK,或者为相同语言的UDF提供bundle处理的优化实现。
Runner API是pipeline的一个独立于SDK的schema,以及用于启动pipeline和检查作业状态的RPC接口。RPC接口仍在开发中,因此目前我们主要关注与SDK无关的pipeline表示。仅通过Runner API接口检查pipeline,可以消除你的Runner对SDK语言的依赖性,以便进行pipeline分析和作业转换。
要执行这样一个与SDK无关的管道,您需要支持Fn API。UDF作为函数的规范(通常只是特定语言的不透明序列化字节)以及可以执行它的环境的规范(本质上是特定的SDK)嵌入到管道中。到目前为止,这个规范应该是承载sdk的Fn API线束的Docker容器的URI。
完全欢迎您使用您的语言的SDK,它可能提供有用的实用程序代码。
pipeline的独立于语言的定义是通过protocol buffers模式描述的,下面介绍以供参考。但您的Runner不应该直接操作protobuf消息。相反,Beam 代码库提供了处理管道的工具程序,这样你就不需要知道pipeline是否已经被序列化或传输,也不需要知道它可能是用什么语言编写的。
Java
如果你的Runner是基于Java的,以SDK无关的方式操作pipeline的工具在beam-runners-core-construction-java 构件的org.apache.beam.runners.core.construction 命名空间里。这些实用程序的命名是一致的,就像这样:
PTransformTranslation
-已知转换和标准URN的注册表ParDoTranslation
-以独立于语言的方式使用ParDo的实用程序WindowIntoTranslation
- 与Window的相同FlattenTranslation
- 与Flatten
相同- WindowingStrategyTranslation – 与窗口化策略相同
- CoderTranslation-与coder相同
- 等等
只通过这些类检查transform,您的运行程序将不依赖于Java SDK的细节。
Runner API protos
Runner API是Beam 模型中概念的具体表现形式,作为protocol buffers 模式。即使您不应该直接操作这些信息,了解组成pipeline的规范数据也是很有帮助的。
大多数API与高级描述完全相同;您可以开始实现一个Runner,而不必了解所有低级细节。
Runner API对你来说最重要的一点是它是一个独立于语言的Beam pipeline定义。你可能总是通过一个特定的SDK的支持代码进行交互,该支持代码用合理的惯用API包装这些定义,但请始终注意,这是规范,任何其他数据不一定是pipeline固有的,但可能是特定于SDK的丰富功能(或bug!).
Pipeline中的UDF可以为任何Beam SDK编写,甚至同一pipeline中包含多种Beam SDK。因此,我们将从这里开始,采用自下而上的方法来理解UDF的protocol buffers定义,然后再回到更高级别(通常是显而易见的)记录定义。
FunctionSpec
proto
跨语言可移植性的核心是FunctionSpec。这是一个与语言无关的函数规范,在通常的编程意义上,该规范包括副作用等。、
message FunctionSpec {
string urn;
google.protobuf.Any parameter;
}
FunctionSpec包括标识函数的urn和任意固定参数。例如,(假设的)“max”CombineFn可能具有urn beam:combineefn:max:0.1和一个参数,该参数表明通过如何比较来取最大值。
在使用特点语言的SDK构建的pipeline中大多数UDF,URN将指示sdk必须对其进行解释,例如,beam:dofn:javasdk:0.1或者 beam:dofn:pythonsdk:0.1。该参数将包含序列化代码,例如Java序列化DoFn
或Python序列化的DoFn
。
FunctionSpec不仅适用于UDF。它只是命名/指定任何函数的通用方法。它还用作PTransform的规范。但在PTransform中使用时,它描述了从PCollection到PCollection的函数,并且不能特定于SDK,因为Runner负责运算转换并生成PCollections。
SdkFunctionSpec
proto
当一个用FunctionSpec 表示一个UDF时,通常只有序列化它的SDK才能保证理解它。在这种情况下,它总是带有一个能够理解和执行该函数的环境。这由SdkFunctionSpec表示。
message SdkFunctionSpec {
FunctionSpec spec;
bytes environment_id;
}
在Runner API中,许多对象是通过引用存储的。这里的environment_id是一个指针,位于pipeline的本地,由序列化它的SDK组成,可以取消引用以生成实际的环境proto。
到目前为止,环境应该是可以执行指定的UDF的SDK工具包的Docker容器规范。
Primitive transform payload protos
原始转换的有效负载只是其规范的原型序列化。与其在这里复制它们的完整代码,我只突出显示重要的部分,以展示它们是如何组合在一起的。
值得再次强调的是,虽然您可能不会直接与这些有效负载交互,但它们是转换固有部分的唯一数据。
ParDoPayload
proto
ParDo转换在SdkFunctionSpec中携带其DoFn,然后为其其他特性(侧输入、状态声明、计时器声明等)提供独立于语言的规范。
message ParDoPayload {
SdkFunctionSpec do_fn;
map<string, SideInput> side_inputs;
map<string, StateSpec> state_specs;
map<string, TimerSpec> timer_specs;
...
}
ReadPayload
proto
Read转换为其 Source
UDF携带一个SdkFunctionSpec。
message ReadPayload {
SdkFunctionSpec source;
...
}
WindowIntoPayload
proto
Window转换为其WindowFn UDF携带一个SdkFunctionSpec。Runner传递这个UDF并告诉SDK工具使用它来分配窗口(而不是合并),这是Fn API的一部分。
message WindowIntoPayload {
SdkFunctionSpec window_fn;
...
}
CombinePayload
proto
Combine不是原语。但是,非原语完全能携带额外信息来进行更好的优化。Combine转换携带的最重要的东西是SdkFunctionSpec记录中的 CombineFn
。为了有效地进行所需的优化,还需要了解中间积累器的coder,因此它也携带了这个coder的一个引用。
message CombinePayload {
SdkFunctionSpec combine_fn;
string accumulator_coder_id;
...
}
PTransform
proto
PTransform是从PCollection到PCollection的函数。这是用FunctionSpec在proto中表示的。请注意,这不是一个SdkFunctionSpec,因为是Runner程序遵守这些。它们永远不会被传递回SDK工具包;它们不代表一个UDF。
message PTransform {
FunctionSpec spec;
repeated string subtransforms;
// Maps from local string names to PCollection ids
map<string, bytes> inputs;
map<string, bytes> outputs;
...
}
如果是一个复合的PTransform,则它可能具有子transform,在这种情况下,由于子transform定义了它的行为,因此可以省略FunctionSpec。
输入和输出PCollections是无序的,由本地名称引用。SDK决定这个名称是什么,因为它很可能嵌入到序列化的UDF中。
PCollection
proto
PCollection只存储一个coder、窗口策略以及它是否有界。
message PCollection {
string coder_id;
IsBounded is_bounded;
string windowing_strategy_id;
...
}
Coder
proto
这是一个非常有趣的proto。编码程序是一个参数化函数,它只能被特定的SDK理解,因此是一个SdkFunctionSpec,但也可能有组件coder可以完全定义它。例如,ListCoder只是一种元格式,而ListCoder(VarIntCoder)是一种完全指定的格式。
message Coder {
SdkFunctionSpec spec;
repeated string component_coder_ids;
}
The Runner API RPCs
虽然你的语言的SDK可能会使你无法直接接触到Runner API protos,但你可能需要为Runner实现适配器,以便将其暴露给其他语言。因此,本节将介绍你可能会直接与之交互的proto。
现有的Runner方法调用将被表示为RPCs的具体方式尚未实现为proto。这个RPC层可以使用Python SDK来构建一个pipeline,并将它发送到用Java编写的一个Runner上。预计一个小Python 垫片将与Java进程或托管Runner API的服务进行通信。
RPC本身必然会遵循PipelineRunner和PipelineResult的现有API,但会被修改为最小的后端通道,而不是丰富而方便的API。
PipelineRunner.run(Pipeline)
RPC
这将采用相同的形式,但PipelineOptions必须序列化到JSON(或proto Struct)并传递。
message RunPipelineRequest {
Pipeline pipeline;
Struct pipeline_options;
}
message RunPipelineResponse {
bytes pipeline_id;
// TODO: protocol for rejecting pipelines that cannot be executed
// by this runner. May just be REJECTED job state with error message.
// totally opaque to the SDK; for the shim to interpret
Any contents;
}
PipelineResult
aka “Job API”
当前这个API中的两个核心功能是获取作业状态和取消作业。它很可能会演变,例如,被泛化为支持排出作业(停止读取输入,让watermark无限)。当前,验证我们的测试框架有益于(但不完全依赖于)在此通道上查询度量信息。
message CancelPipelineRequest {
bytes pipeline_id;
...
}
message GetStateRequest {
bytes pipeline_id;
...
}
message GetStateResponse {
JobState state;
...
}
enum JobState {
...
}