跳转到主要内容
Chinese, Simplified

假装数据和软件是一样的,会对数据工程师的成功产生反作用.

近年来,数据工程似乎正在与DevOps融合。两家公司都采用了云基础设施、容器化、CI/CD和GitOps,为客户提供可靠的数字产品。工具子集的聚合导致许多人认为数据工程和软件工程之间没有明显的区别。因此,数据工程相当“粗糙”的事实仅仅是因为数据工程师在采用良好的软件开发实践方面落后了。

这种评估是错误的。数据工程和软件工程共享许多通用的工具和实践,但它们在许多关键领域也有很大的不同。忽视这些差异,像管理软件产品团队一样管理数据工程团队是错误的。把数据工程想象成西红柿:它是一种水果,但这并不意味着它应该被添加到水果沙拉中。这篇文章旨在强调数据工程中的一些独特挑战,以及为什么这需要自定义方法。

数据管道不是应用程序


软件工程倾向于关注构建应用程序。在本文中,应用程序的定义非常宽泛,可以是一个网站、一个桌面应用程序、一个API、一个大型主机应用程序、一个游戏、一个微服务、一个库,或者介于两者之间的任何东西。它们的共同特点是:

  • 通过提供新的交互方式为用户提供价值。一个游戏可以玩,一个网站可以浏览,一个API可以在其他软件中使用。
  • 拥有许多独立的功能。网站可以增加页面数量,游戏可以增加关卡或可玩角色的数量,API可以添加更多端点。因此,应用程序永远不会真正完成。
  • 处理它们创建的相对较少的状态。状态被设计为支持应用程序。这种状态的管理通常被卸载到外部系统。目标是使软件的大部分是无状态的。web应用程序可以在任何时候被终止和重新启动;它的状态由运行在单独进程中的数据库管理。
  • 与其他软件和服务松散耦合。好的软件应该在任何环境下都能独立运行,因此微服务和容器很受欢迎。

数据工程师关心的是构建数据管道。数据管道从产生数据的地方获取数据,对其进行转换,并将其放在消费数据的另一个地方。通常,我们的目标是按照时间表自动化这些管道,这样数据集就会随着时间的推移而更新。与应用程序一样,数据管道通常也是软件的一部分。与应用程序相反,数据管道:

  • 不提供任何直接价值。管道中没有用户。只有管道生成的数据集对下游消费者才有价值。如果数据通过一些精心设计的复制粘贴方案到达目的地,客户也会同样满意。
  • 只有一个与客户相关的特性:生成所请求的数据集。因此,尽管由于上游系统用户需求的变化,管道将需要持续维护,但有一个明确的完成点。
  • 管理大量的状态。管道被设计用来处理来自它不控制的其他软件的现有状态,并将其转换为它可以控制的状态。许多管道以增量方式构建数据集,每次运行都会添加更多数据。从这个意义上说,这些管道可以被视为非常长时间运行的流程,不断地创建越来越多的状态。
  • 具有不可避免的紧密耦合。数据管道的目标就是绑定到数据源。输油管的稳定性和可靠性取决于输油管的来源。

这些基本的差异给数据工程带来了独特的挑战,而业务、IT管理甚至软件工程师往往很难理解这些挑战。我们来看看。

管道要么完成,要么毫无价值


如今,许多组织都在某种程度上使用敏捷来管理他们的软件团队。这些框架的核心理念是通过在短迭代中构建和发布软件,最大限度地提高软件向客户交付价值的速度。这应该尽可能快地交付最小可行产品(MVP),并确保快速反馈循环,从而确保团队始终以最高优先级处理功能。

这些想法并不适用于数据工程。

不能在增加客户价值的小迭代中开发数据管道。在数据管道中没有等价的MVP。它为客户生成所需的数据集,或者不生成。

因此,数据管道开发并不完全适合敏捷框架。复杂的数据管道对应于单个用户故事,但通常需要多个sprint来完成。非技术管理人员很少考虑到这一点,并试图将数据工程师硬塞进scrum团队。结果是用户故事被任务所取代,例如“构建API连接器”和“构建摄取逻辑”,这不可避免地将scrum板变成了微观管理的设备。

当管理人员不了解他们管理的基本原理时,他们往往会做出糟糕的、不可行的决定。一位经理对管道的缓慢进展感到沮丧,他曾经要求我们团队中的数据工程师逐列迭代地构建数据集,这样客户“至少已经有了一些数据可以开始处理”。具有复杂管道实践经验的数据工程师和曾经收到过无用数据集的数据科学家将认识到这种情况的荒谬。对于没有这方面背景的读者,这里有三个原因说明为什么这行不通:

1. 部分数据集没有成比例的效用


如果一个数据集包含10列中的9列,它是否有90%的用处?这取决于省略了哪一列。如果数据科学家的目标是建立一个基于数据的预测模型,但是缺少的列是他们想要预测的标签或值,那么数据集是0%有用的。如果列是一些与标签无关的随机元数据,那么它可能是100%有用的。最常见的是,一列表示一个字段,该字段可能与标签相关,也可能不相关;找出它是否相关正是数据科学家想要通过实验发现的。这就是为什么数据科学家想要一个尽可能完整的数据集来开始实验、探索和逐步优化他们的模型。为他们提供部分数据集,确保一旦有额外的字段可用,就需要重新进行实验和优化。

2. 开发管道的时间与数据集大小无关


即使客户对其中的一半数据集很满意,生成这个数据集所需的时间也不会是生成完整数据集所需时间的一半。数据管道不是由独立的作业组成,每个作业产生一个列。如果多个列来自同一来源,它们可能是相关的,因此在最终数据集中包含一个或多个列的工作量是相同的。但是,将这些列与另一个表中的列合并的逻辑可能像单个连接一样简单,也可能需要一系列复杂的窗口函数。此外,在数据从另一端输出之前,可能需要在数据管道中编写大量样板,例如,访问API的客户端或处理非结构化数据的解析器。一旦这样做了,扩展逻辑来处理其他字段可能就很简单了。因此,最终数据集中的列数是衡量管道复杂性的糟糕指标,就像使用代码行数来衡量生产力一样。

数据集大小也可以指行/记录的数量。设计良好的管道应该能够处理任意数量的记录,因此开发时间应该完全解耦。然而,根据具体需求,开发时间可能会出现“跳跃”,例如:

  • 数据集需要多久更新一次,也就是说,我们需要批处理还是流式处理?
  • 预期的数据量和速度是多少?
  • 数据是否适合RAM?

这些考虑因素应该事先知道,并将影响管道的整个设计。

3.建立数据集的时间和经济成本与数据集的大小相关


就行和列而言,数据集越大,构建和更新它所需的时间就越长。在一个庞大的数据库中编辑一条记录是琐碎和快速的,但是对于分析数据集来说,这是一个不常见的场景。修改分析数据集通常涉及添加/更改整个列(这会更改所有记录)或更新数千或数百万行。有两种方法可以处理数据的变化,但都不便宜。

从开发人员的角度来看,更新数据集最简单的方法是通过更新和重新运行管道来覆盖所有内容。然而,就计算成本和刷新数据集的时间而言,这是最昂贵的。设计一个能够正确覆盖之前运行状态(幂等)的管道也并非总是微不足道的,需要适当的预先规划。

或者,更新数据集的逻辑可以在单独的管道中编码,该管道将旧数据集作为输入。这在计算成本和速度方面可能更经济,但会产生额外的开发时间和精神开销。应用delta的管道不是幂等的,因此跟踪当前状态以及执行特定操作的时间非常重要。即使在第二种情况下,也应该更新旧的管道,以便在新的更新中包含所需的更改。

不管怎样,问题是,数据集有固有的惯性。数据集越大,尝试进行更改需要更多的时间、精力和/或金钱。

结论:将部分管道部署到生产中是浪费的


将部分完成的管道部署到生产环境中对客户没有用处,浪费计算,并且使构建管道的工程师的生活变得复杂,因为他们将不得不处理旧状态。将大量的DevOps和敏捷原则转移到数据管道开发中,在这种开发中,增量更改和频繁部署被鼓励,这完全忽略了数据的惯性。

工程师们希望“第一次就做对”,并尽量减少部署到生产环境中的次数。频繁部署的管道要么表明客户不知道他们想要什么,要么表明数据源非常不稳定,管道需要不断修补。与无状态应用程序相反,在无状态应用程序中,更新往往像杀死几个容器并旋转新容器一样简单,更新数据集与重新部署管道代码不同。将管道代码打包到容器中并在Kubernetes上运行并不能弥补这一差距。

管道开发中的反馈循环是缓慢的


为了快速创建新功能或修复软件中的错误,开发人员需要快速反馈,告诉他们所写的内容是正确的,并将软件推向正确的方向。

在软件开发中,这通常是使用一套单元测试来实现的,开发人员在本地运行单元测试来检查软件的每个组件(仍然)是否按预期工作。单元测试应该是快速的,不与任何外部系统交互,也不依赖于任何状态。他们应该独立地、隔离地测试函数、方法和类。通过这种方式,软件开发人员可以在开发过程中获得快速反馈,并且在打开拉取请求时可以非常确信他们的代码按预期工作。如果需要测试与其他系统的交互,则CI管道也可以运行较慢的集成测试。

这里有一个数据工程的秘密:数据管道很少进行单元测试(喘息!)。数据管道通常通过简单地部署来进行测试——通常首先部署到开发环境中。这需要一个构建和部署步骤,之后工程师必须监视管道一段时间,看看它是否按预期工作。如果管道不是幂等的,重新部署可能首先需要人工干预来重置上次部署留下的状态。与运行单元测试相比,此反馈周期非常缓慢。

那么为什么不直接编写单元测试呢?

1. 数据管道在无法进行单元测试的方面会失败


可以进行单元测试的自包含逻辑通常在数据管道中受到限制。大部分代码都是胶水和管道胶带。因此,几乎所有的故障都发生在系统之间的故障接口或进入管道的意外数据。

系统之间的接口不能用单元测试进行测试,因为这些测试不能单独运行。外部系统可以被模拟,但这只能证明管道与数据工程师认为的外部系统工作方式相同的系统一起工作。实际上,数据工程师很少知道所有相关的细节。让我们举一个现实世界的例子:为了防止DDOS攻击,公共API可能对同一IP在一定时间间隔内可以发送的请求数量有一个未公开的限制。API的模拟可能无法解释这一点,但它存在于实际系统中的事实可能会破坏生产中的管道。此外,外部系统很少是稳定的。事实上,数据管道的建立通常是因为人们希望将数据从不可靠的系统转移到更可靠的系统。模拟不会揭示真实系统是否以破坏管道的方式进行了更改。

众所周知,数据生产者不善于始终如一地交出高质量的数据。数据管道必须总是对将要输入的数据做一些假设。数据中意外的内容或结构会破坏管道,或者至少会产生不正确的结果。保护管道免受松散数据源侵害的一种常用策略是在读取时验证模式。但这并不能防止数据的错误内容和微妙的“数据漏洞”。例如,时间序列是否正确处理日光节约时间?列中是否存在不符合预期模式的字符串?代表真实世界测量值的数值列中的值在物理上有意义吗?这些都与可以用单元测试测试的管道逻辑无关。

2. 单元测试比管道逻辑更精细


测试管道中有限的自包含转换逻辑所需的单元测试比代码本身更复杂,因为它要求开发人员创建具有代表性的测试数据,以及预期的输出数据。这是大量的工作,并没有从本质上提高对管道正确运作的信心。此外,这将把问题从“该功能是否按预期工作?”到“这个测试数据能充分代表我的真实数据吗?”理想情况下,单元测试覆盖了输入参数组合的一个很好的子集。但是在转换数据集的函数中,例如以数据框架的形式,数据集参数本身呈现出接近无限维的参数空间。

结论:开发管道是缓慢的


获得关于数据管道的可靠反馈的最佳方法是部署并运行它。这总是比本地运行单元测试慢,这意味着需要更长的时间来获得反馈。其结果是管道开发,特别是在调试阶段,非常缓慢。

可以考虑比运行整个管道更快的集成测试。然而,这些通常不能在开发人员的机器上运行,因为它不能直接访问相关的源系统。因此,这些测试只能在与管道相同的环境中运行,这同样需要部署。这在很大程度上违背了编写测试以获得快速反馈的目的。

“数据合同”现在在处理鲁莽的数据生产者时非常流行。对进入管道的数据充满信心将消除管道开发中的许多不确定性,并使其不那么脆弱。然而,这些合同似乎很难执行,因为生产者没有动力遵守这些合同。此外,组织将希望使用从外部来源提取的数据,如公共api;祝你在与这些外部方谈判数据合同时好运。

流水线开发不能并行化


我们已经确定,数据管道是一个单一的用户故事,由于反馈周期长,开发速度很慢。但是,由于流水线由多个任务组成,一些管理人员试图将它们分配给多个开发人员,以加快流程。不幸的是,这不起作用。在管道中处理数据的任务是顺序的。为了构建第二步,第一步的输出必须是稳定的。通过构建第二步获得的洞察力会反馈到第一步的改进中。因此,管道作为一个整体必须被视为开发人员迭代的一个特性。

一些管理人员反驳说,这仅仅意味着从一开始就没有充分规划管道。一开始就有数据,最后需要什么数据就很清楚了。那么,在中间需要建立什么就不明显了吗?矛盾的是,提出这种观点的管理者将会激烈地捍卫敏捷的优点。

只要数据源没有得到适当的描述,规划整个管道是行不通的。在没有合同和文档的情况下,数据工程师不得不在黑暗中摸索,以便发现数据的特殊性。这个发现过程塑造了管道的体系结构。从某种意义上说,这是敏捷的;这不是商业利益相关者想要的工作方式。

结论与建议


数据管道是软件,但不是软件产品。他们是服务于制造客户实际要求的汽车的工厂。它们是达到目的的一种手段,是从笨拙的数据源创建易于使用的数据集的自动化配方。它们是系统之间的管道胶带,这些系统不是为相互通信而设计的。它们是数据“最后一英里”问题的丑陋、脆弱和昂贵的解决方案。它们的唯一目的是管理状态,这使得它们的开发缓慢、不可预测,并且经常成为数据分析项目的主要瓶颈。无论数据影响者通过兜售另一种工具来宣称什么,无论有多少层抽象层堆叠在一起,数据从根本上不同于软件。不认识到这些差异,并在数据团队中实施敏捷过程,因为它在软件团队中起作用,只会适得其反。

你能做些什么来让数据团队成功和富有成效?

原文地址
https://betterprogramming.pub/data-engineering-is-not-software-engineering-af81eb8d3949
本文地址
Article

微信

知识星球

微信公众号

视频号