[译] 可维护的 ETL:使管道更容易支持和扩展的技巧

modularized code example

任何数据科学项目的核心是...噔噔噔...数据!以可靠和可重复的方式准备数据是该过程的基本部分。若是你正在培训一个模型,计算分析,或者只是未来自多个源的数据组合到另外一个系统中,那么你将须要构建一个数据处理或 ETL1 管道。html

咱们 Stitch Fix 这里从事的是全栈数据科学。这意味着咱们以数据科学家的身份负责项目的构思、生产以致维护的整个过程。咱们的受好奇心驱使喜欢快速行动,即便咱们的工做经常是相互联系的。咱们所处理的问题具备挑战性,所以解决方案可能很复杂,但咱们不想在不须要的地方引入复杂性。由于咱们必须支持咱们在生产中的工做,因此咱们的小团队分担随叫随到的责任,并帮助支持彼此的管道。这让咱们能够作一些重要的事情,好比度假。今年夏天,我和妻子要去意大利度蜜月,这是咱们多年前的打算。当我在那里的时候,我最不想考虑的是个人队友们是否很难使用或理解我写的管道。前端

让咱们也认可数据科学是一个动态的领域,因此同事们会转向公司以外的新计划、团队或机会。虽然一个数据管道可能由一个数据科学家构建,但在其生命周期中,它一般由多个数据科学家支持和修改。像许多数据科学团体同样,咱们来自不一样的教育背景,不幸的是,咱们并不是都是“独角兽” —— 软件工程、统计和机器学习方面的专家。python

虽然咱们的算法小组确实有一个庞大的、使人惊叹的数据平台工程师团队,它们不会也不想写 ETL 来支持数据科学家的工做。相反,他们将精力集中在构建易于使用、健壮可靠的工具上,这些工具使数据科学家可以快速构建 ETL、培训和评分模型,以及建立性能良好的 API,而无需担忧基础设施。android

多年来,我发现了一些有助于使个人 ETL 更易于理解,维护和扩展的关键作法。本文会带你们看看如下作法有什么好处:ios

  1. 创建一系列简单的任务。
  2. 使用工做流程管理工具。
  3. 尽量利用 SQL。
  4. 实施数据质量检查。

讨论细节以前,我要认可一点:没有一套构建 ETL 管道的最佳实践。这篇文章的重点是数据科学环境,其中有两件事情是正确的:支持人员的组成情况的演变是不断发展和多样化的,开发和探索优先于铁定的可靠性和性能。git

创建一系列简单的任务

使 ETL 更容易理解和维护的第一步是遵循基本的软件工程实践,将大型和复杂的计算分解为具备特定目的的离散、易于消化的任务。相似地,咱们应该将一个大型ETL管道划分为较小的任务。这有不少好处:github

  1. 更容易理解每一个任务:只有几行代码的任务更容易审查,所以更容易吸取处理过程当中的任何细微差异。算法

  2. 更容易理解整个处理链:当任务具备明肯定义的目的而且命名正确时,审阅者能够专一于更高级别的构建块以及它们如何组合在一块儿而忽略每一个块的细节。数据库

  3. 更容易验证:若是咱们须要对任务进行更改,咱们只须要验证该任务的输出,并确保咱们遵照与此任务的用户/调用者之间的任何“约定”(例如,结果表的列名称和数据类型与预修订格式相匹配)。apache

  4. 提高模块化程度:若是任务具备必定的灵活性,则能够在其余环境中重用它们。这减小了所需的总代码量,从而减小了须要验证和维护的代码量。

  5. 洞察中间结果:若是咱们存储每一个操做的结果,当出现错误时,咱们将更容易调试管道。咱们能够查看每一个阶段,更容易找到错误的位置。

  6. 提升管道的可靠性:咱们将很快讨论工做流工具,可是将管道分解为任务的话,发生临时故障时就能够更轻松地自动从新运行任务。

咱们从一个简单的示例,就能够看到将管道拆分为较小任务的好处。在 Stitch Fix,咱们可能想知道发送给客户的物品当中,“高价”物品所占的比例。首先,假设咱们已经定义了一个存储阈值的表。请记住,阈值将根据客户群(例如孩子与女性)和物品种类(例如袜子与裤子)而有所不一样。

因为此计算至关简单,咱们能够对整个管道使用单个查询:

WITH added_threshold as (
  SELECT
    items.item_price,
    thresh.high_price_threshold
  FROM shipped_items as items
  LEFT JOIN thresholds as thresh
    ON items.client_segment = thresh.client_segment
      AND items.item_category = thresh.item_category
), flagged_hp_items as (
  SELECT
    CASE
      WHEN item_price >= high_price_threshold THEN 1
      ELSE 0
    END as high_price_flag
  FROM added_threshold
) SELECT
    SUM(high_price_flag) as total_high_price_items,
    AVG(high_price_flag) as proportion_high_priced
  FROM flagged_hp_items
复制代码

这第一次尝试实际上至关不错。它已经经过使用公共表表达式(CTE)或 WITH 块进行了模块化。每一个块都用于特定目的,它们简短且易于吸取,而且别名(例如 added_threshold)提供足够的上下文,以便审阅者能够记住块中所完成的操做。

另外一个积极方面是阈值存储在单独的表中。咱们可使用很是大的 CASE 语句对查询中的每一个阈值进行硬编码,但这对于审阅者来讲很快就会变得难以理解。它也很难维护,由于咱们只要想更新阈值,就必须更改此查询以及使用相同逻辑的任何其余查询。

虽然这个查询是一个良好的开端,但咱们能够改进实现的方式。最大的不足是咱们没法轻松访问任何中间结果:整个计算只需一次操做便可完成。你可能想知道,为何我要查看中间结果?中间结果容许你进行即时调试,得到实施数据质量检查的机会,而且能够证实在其余查询中可重用。

例如,假设企业添加了一个新的物品类别 —— 例如,帽子。咱们开始销售帽子,但咱们忘记更新阈值表。在这种状况下,咱们的聚合指标就会漏掉高价的帽子。因为咱们使用了 LEFT JOIN,由于链接不会删除行,可是 high_price_threshold 的值将为 NULL。到了下一个阶段,全部和帽子有关的行,其 high_price_flag 的值都会是零,而这个数值会带到咱们最终进行计算的 total_high_price_itemsproportion_high_priced

若是咱们将这个大的单个查询分解为多个查询并分别编写每一个阶段的结果,咱们就可使这个管道更易于维护。若是咱们将初始阶段的输出存储到单独的表中,咱们能够轻松检查咱们是否没有丢失任何阈值。咱们须要作的就是查询此表并选择 high_price_threshold 值为 NULL 的行。若是什么都没有返回,就表明咱们遗漏了一个或多个阈值。咱们将在帖子后面介绍这种类型的数据运行时验证。

这种模块化的实现也更容易修改。假设咱们不是要考虑全部曾寄出的物品,而是决定只想计算过去 3 个月发送的高价物品。要是用原来的查询方式,咱们就会对第一阶段进行更改,而后查看最终得出的总数,指望获得正确的数值。经过单独保存第一阶段,咱们能够添加一个具备发货日期的新列。而后,咱们能够修改查询并验证结果表中的发货日期是否都在咱们预期的日期范围内。咱们还能够将咱们的新版本保存到另外一个位置并执行“数据差别”以确保咱们正在删除正确的行。

最后一个示例将此查询拆分为单独的阶段带来了最大的好处之一:咱们能够重用咱们的查询和数据来支持不一样的用例。假设一个团队想要过去 3 个月的高价项目指标,但另外一个团队仅在最后一周须要它。咱们能够修改第一阶段的查询以支持这些并将每一个版本的输出写入单独的表。若是咱们为后期查询动态指定源表 2,相同的查询将支持两种用例。此模式也能够扩展到其余用例:具备不一样阈值的团队,按客户端细分和项目类别细分的最终指标与汇总。

咱们经过建立分阶段管道进行了一些权衡。其中最大的一个是运行时性能,尤为是当咱们处理大型数据集时。从磁盘读取和写入数据会形成很大的开销,而且在每一个处理阶段,咱们读取前一阶段的输出并写出结果。和旧的 MapReduce 范例相比,Spark 的一大优点是临时结果能够缓存在工做节点(执行程序)的内存中。Spark 的 Catalyst 引擎还优化了 SQL 查询和 DataFrame 转换的执行计划,但它优化时没法跨越读/写边界。这些分阶段管道的第二个主要限制是它们使建立自动化集成测试变得更加困难,这涉及测试多个计算阶段的结果。

有了 Spark,就能够解决这些不足之处。若是我必须执行几个小的转换而且我想要保存中间步骤的选项,我就会建立一个管理程序脚本,这个脚本只有在设置了命令行标志时才执行转换,以及输出中间表 3。当我正在开发和调试更改时,我可使用该标志来生成验证新计算是否正确所需的数据。一旦我对个人更改有信心,我能够关闭标记以跳过编写中间数据。

使用工做流程管理工具

使用可靠的工做流管理和调度引擎,能够实现巨大的生产力提高。一些常见的例子包括 AirflowOozieLuigiPinball。这项建议须要时间和专业知识来创建;这不是个别数据科学家可能负责管理的事情。在 Stitch Fix,咱们开发了本身的专有工具,由咱们的平台团队维护,数据科学家用它就能够建立、运行和监控咱们本身的工做流程。

工做流工具能够轻松定义计算的有向非循环图(DAG),其中每一个子任务都依赖于任何父任务的成功完成。这些工具一般能让使用者得以指定运行工做流的计划,在工做流启动前等待外部数据依赖,重试失败的任务,在失败时恢复执行,在发生故障时建立警报,以及运行不相互依赖的任务在平行下。这些功能相结合,使用户可以构建可靠,高性能且易于维护的复杂处理链。

尽量利用SQL

这多是我提出的最具争议性的建议。即便在 Stitch Fix 中,也有许多数据科学家反对 SQL,而是提倡使用通用编程语言。不久以前我仍是这个阵营的一员。在实践方面,SQL 很难测试 — 特别是经过自动化测试。若是你来自软件工程背景,那么测试的挑战可能会让你以为有足够的理由来避免使用 SQL 。我在过去也陷入过关于 SQL 的情感陷阱:“SQL 技术性较差,专业性较差;真正的数据科学家应该编码。”

SQL 的主要优势是全部数据专业人员都能理解:数据科学家、数据工程师、分析工程师、数据分析师、数据库管理员和许多业务分析师。这是一个庞大的用户群,能够帮助构建,审查,调试和维护 SQL 数据管道。虽然 Stitch Fix 没有不少这些数据角色,但 SQL 是咱们这些不一样数据科学家的共同语言。所以,利用 SQL 能够减小对团队中专业角色的需求,这些团队具备强大的 CS 背景,为整个团队建立管道,没法公平地分担支持职责。

经过将转换操做编写为 SQL 查询,咱们还能够实现可伸缩性和某种级别的可移植性。使用适当的 SQL 引擎,能够用相同的查询语句来处理一百行数据,而后针对太字节数量级的数据运行。若是咱们使用内存处理软件包(如 Pandas)编写相同的转换操做,那么随着业务或项目的扩展,咱们将面临超出处理能力的风险。全部东西运行起来都不会有问题,但一到了数据集过大、内存没法容纳时,就会出错。若是这项工做正在进行中,这可能致使急于重写事情以使其恢复运行。

不一样 SQL 语言变体有不少共通之处,咱们从一个 SQL 引擎到另外一个 SQL 引擎具备必定程度的可移植性。在 Stitch Fix 中,咱们使用 Presto 进行 adhoc 查询,使用 Spark 进行生产管道。当我构建一个新的 ETL 时,我一般使用 Presto 来理解数据的结构,并构建部分转换。一旦这些部件到位,我几乎老是用 Spark 4 运行相同的查询语句,不做任何修改。若是我要切换到 Spark 的 DataFrame API,我须要彻底重写个人查询。反过来一样能够体现这种可移植性的好处。若是生产做业存在问题,我能够从新运行相同的查询并添加过滤器和限制以将数据的子集拉回以进行目视检查。

固然,不是全部操做都能用 SQL 完成。你将不会使用它来训练机器学习模型,并且还有许多其余状况下,SQL 实现即便可行,也会过于复杂。对于这些任务,你绝对应该使用通用编程语言。若是你遵循关键的建议,把你的工做分红小块,那么这些复杂的任务将在范围内受到限制,而且更容易理解。在可能的状况下,我尝试在一系列简单准备阶段的末尾隔离复杂的逻辑,例如:链接不一样的数据源、过滤和建立标志列。这使得验证进入最后一个复杂阶段的数据变得容易,甚至能够简化一些逻辑。通常来讲,我在本篇文章的其他部分已经再也不强调自动化测试,但处理有复杂逻辑的任务时,着力实现测试覆盖就颇有意义了。

实施数据质量检查

要验证复杂的逻辑时,自动单元测试很是有用,但对于做为分阶段管道的一部分的相对简单的转换,咱们一般能够手动验证每一个阶段。就 ETL 管道而言,自动化测试提供了混合的好处,由于它们不会覆盖最大的错误来源之一:咱们的管道上游的故障致使咱们的初始依赖关系中出现旧的或不正确的数据。

一个常见的错误来源是在启动管道以前未能确保咱们的源数据已更新。例如,假设咱们依赖于天天更新一次的数据源,而且咱们的管道在数据源更新以前就开始运行。这意味着咱们要么用的是(前一天计算的) 旧数据,要么使用旧数据和当前数据的混合数据。这种类型的错误可能难以识别和解决,由于上游数据源可能在咱们获取旧版本的数据后不久就完成更新。

上游故障还可能致使源数据中出现错误数据:字段计算错误,模式更改和/或缺失值频率更高。在动态且互联的环境中,利用另外一个团队建立的数据源进行实验的作法并很多见,而这些源也经常会出现意外更改;咱们在 Stitch Fix 运做时所处的环境很大程度上就是如此单元测试一般不会标记这些故障,但能够经过运行时验证(有时称为数据质量检查)来发现它们。咱们能够编写单独的 ETL 任务,若是咱们的数据不符合咱们指望的标准,它们将自动执行检查并引起错误。上面提到了一个简单的例子,其中缺乏高价的帽子门槛。咱们能够查询组合出货物品和高价阈值表,并查找缺乏阈值的行。若是咱们找到任何行,咱们能够提醒维护者。这个想法能够推广到更复杂的检查:计算零分数、平均值、标准差、最大值或最小值。

在特定列的缺失值高于预期的状况下,咱们首先须要定义预期的内容,这能够经过查看上个月天天丢失的比例来完成。而后咱们能够定义触发警报的阈值。这个想法能够推广到其余数据质量检查(例如,平均值落在一个范围内),咱们能够调整这些阈值,使咱们对警报的敏感度进行增减。

正在进行的工做

在这篇文章中,咱们已经完成了几个实际步骤,可使你的ETL更易于维护,扩展和生产支持。这些好处能够扩展到你的队友以及你将来的自我。虽然咱们能够为构建良好的流水线而感到自豪,但编写ETL并非咱们进入数据科学的缘由。相反,这些是工做的基本部分,使咱们可以实现更大的目标:构建新模型,为业务提供新看法,或经过咱们的API提供新功能。建造不良的管道不只须要时间远离团队,还会给创新带来障碍。

我在上一份工做中尝到的苦果,让我明白到管道若是难以使用,就会让项目难以维护和扩展。我当时在某个创新实验室工做,该实验室率先使用大数据工具来解决组织中的各类问题。个人第一个项目是创建一条管道来识别信用卡号被盗的商家。我构建了一个使用 Spark 的解决方案,由此产生的系统在识别新的欺诈活动方面很是成功。然而,一旦我把它传递到信用卡部门支持和扩展,问题就开始了。我在编写管道时打破了我列出的全部最佳实践:它包含一个执行许多复杂任务的做业,它是用 Spark 编写的,当时对公司来讲是新的,它依赖于 cron 进行调度而且没有'发生故障时发送警报,它没有任何数据质量检查,以确保源数据是最新的和正确的。因为这些缺陷,管道没有运行的时间延长。尽管有一个普遍的路线图来增长改进,但因为代码很难理解和扩展,所以不多可以实现这些改进。最终,整个管道以一种更容易维护的方式重写

就像你的 ETL 正在进行的数据科学项目同样,你的管道永远不会真正完整,应该被视为永远不断变化。经过每次更改,每次更改都是实现小幅改进的契机:提升可读性,删除未使用的数据源和逻辑,或简化或分解复杂的任务。这些建议并非什么重大突破,但若是要始终如一地践行,就须要自律。就像狮子驯服同样,当管道很小时,它们相对容易控制。然而,它们长得越大,就越难管控,也越容易表现出突发且意外的错乱行为。到了那种地步,你只得从新开始、采起更好的作法,否则就可能会冒着失败的风险 [5][#f5]。


注释

[1]↩ 提取、转换和加载的缩写。

[2]↩ 最简单的方法是使用简单的字符串替换或字符串插值,可是你能够经过模板处理库(如 jinja2)实现更大的灵活性。

[3]↩ 对于 Python,像标准库中的 ClickFire,甚至 argparse 这样的库能够轻松定义这些命令行标志。

[4]↩ 操做日期和从 JSON 中提取字段等操做须要修改查询,但这些更改很微小。

[5]↩ 在撰写博客时,没有狮子或数据科学家受到伤害。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索