• 卢经理 151 5399 4549
    扫一扫,加我咨询
扫码关注我们
敏捷的数据工程实践
发布时间:2024-05-24

作者 | 廖光明

随着数据在越来越多的企业中被应用,数据技术的发展可谓突飞猛进。不仅基于Hadoop的大数据生态在持续完善,我们也能看到很多新兴的分布式技术如潮水般涌现。

虽然数据技术发展飞快,但是对于做数据开发的我们,整个数据项目开发过程还是很痛苦。我们接触过的客户常常这样抱怨:

  • 搞不懂数据怎么算出来的,反正很复杂
  • 数据库里面好几百个SQL,代码都很长
  • 经常延迟出数据,流水线总是出问题
  • … 

这是什么原因呢?我们发现这常常是由于团队的数据工程实践做得不够好。

想要规模化实施企业数据项目开发,除了数据技术之外,数据工程实践也得跟上。

这篇文章的内容是结合我们在多个客户的数据项目经验,给大家分享一些行之有效的数据工程实践。

数据工程与敏捷数据工程

首先,我们需要了解一下什么是数据工程。

我们一般理解的工程是以解决实际生活中的复杂问题为目标,通常是以团队为单位进行实施,综合利用各种科学知识及经验解决问题(参考wiki)。软件工程则是在软件开发领域的工程,它综合利用各类软件构建知识、技术工具及经验来构建复杂软件。

IEEE将软件工程定义为:

将系统化的、规范的、可度量的方法用于软件的开发、运行和维护的过程,即将工程化应用于软件开发中。

数据工程是在数据开发这个特定的软件开发领域的软件工程,可以定义为综合利用各类软件构建知识、数据技术及工具、经验来构建复杂的数据产品。

1.敏捷数据工程

敏捷软件开发已经成为应用软件开发的主流工程方法。有大量团队验证了敏捷方法中推荐的实践的有效性。

数据开发属于一个特定的软件开发领域,大部分的应用软件开发方法可适用于数据开发,敏捷软件开发方法自然也不例外。因此,我们可以将敏捷数据工程定义为:

将敏捷软件开发的思想应用于数据开发过程中,得到的一系列工程方法的合集。

很多敏捷软件开发思想源于极限编程,其要旨在于通过将好的实践做到极致来改善软件质量。例如,构建持续集成流水线可以让每次提交代码都进行集成,从而避免集成造成的问题。另外,通过将尽可能多的项目内容代码化,可借助版本控制工具来追踪每次修改的内容。

在将敏捷思想应用于数据工程时,也需要根据实际情况进行适当的裁剪和调整。

数据工程内容非常广泛,包括数据需求分析、数仓设计、数据开发过程、数据测试、数据运维、数据项目管理等。结合敏捷的思想,本文希望抛砖引玉,挑选三个方面的实践方法做一些分享:

  • 代码化一切
  • 数据复用与代码复用
  • 以ETL为单位的持续集成

2.代码化一切

在应用软件开发中,“代码化一切”被讨论得很多。常见的代码化XX有:

  • 配置即代码(Configuration as code):将配置文件放入代码仓库进行管理,可以实现配置修改的可追溯性。
  • 基础设施即代码(Infrastructure as code):将基础设施需要的通用能力抽象出来,以便可以用代码来定义基础设施。然后将基础设施代码放入代码仓库进行管理,可实现随时可重建的基础设施。通常借助一些工具实现,比如Kubernetes支持用yaml文件代码来定义基于容器的基础设施,Terraform支持用yaml文件代码定义各类基础设施,并通过插件来支持几乎所有的基础设施提供商(如本地服务器、AWS/GCP/Azure云服务等)。
  • 流水线即代码(Pipeline as code):避免通过持续集成工具(如Jenkins)的UI上的复杂配置来定义流水线,而是通过代码来定义持续集成流水线。早在2016年就在Thoughtworks的技术雷达上提出,后来得到了各种主流的持续集成工具的支持。比如Jenkins支持用Groovy代码来定义流水线,GitHub Actions/GitLab/Circle CI/Travis CI等支持用yaml代码来定义流水线。

还有更多的比如:

  • 微服务的可观察性即代码(observability as code)
  • 安全策略即代码(Security policy as code)
  • 图表即代码(Diagrams as code)
  • ......

3.代码化的优势

从上面这些“代码化XX”中,我们可以看到一个趋势,似乎我们正在尝试把“一切”代码化。为什么“代码化”这么有吸引力?

这要追溯到开发人员日常工作方式中。作为一个程序员,每天做得最多的事情是写代码,最习惯最熟练的工作也是写代码。通过一个熟悉的集成开发环境(IDE)或者文本编辑器,开发人员可以高效的编写、调试代码并完成工作。

正由于此,现在市场上有大量成熟的帮助开发人员写代码的工具。它们大都支持了数量众多的快捷键,可辅助编写代码的智能代码提示,语法检查等等对代码编写非常友好的功能。开发人员还往往会根据自己的习惯对这些工具进行配置,以便达到最高的编码效率。

不难看出,正是由于这些工作方式,所以开发人员会更希望以代码化的方式来工作,这也就推动了“代码化一切”这样的工程思想的发展。

除了可以高效编辑之外,代码化之后还能获得这样一些好处:

  • 可追踪变更历史记录:开发人员有成熟的代码版本控制工具可用于记录每一次修改内容、修改人、修改时间、注释等。如果有必要,还可以比较任意两个版本的差异。对于诊断问题而言,这无疑是非常高效的。
  • 可回退到任一版本:由于待开发的功能往往非常逻辑复杂,因此,常常会隐藏一些问题在交付的软件中。如果实现了“代码化”,则可根据需要随时回退到某一个无问题的版本。
  • 可融入开发人员的日常开发实践:代码化之后,可以更容易的融入到开发团队的日常实践中,比如代码评审Code Review

4.数据开发中的“代码化”

数据开发中,我们一般要面对这样几类开发资源:基础设施、安全配置、ETL代码、ETL任务配置、数据流水线、运维脚本、业务注释等。

事实上,这些资源均可以很容易的被“代码化”。

基础设施可以通过Terraform进行代码化。如果整个系统运行在类似Kubernetes这样的容器平台上,也可以Kubernetes提供的YAML来定义基础设施。

安全配置代码化常常需要一定的开发成本,一般可借助于各类安全管理应用提供的API进行代码化。一个推荐的做法如下。首先根据具体的应用场景设计安全管理模型,并据此模型定义(较少的)配置项,然后提供一个程序读取这些配置,并根据安全管理模型生成安全管理工具提供的API所对应的数据,最后调用安全管理工具提供的API完成安全配置的应用。

ETL一般以代码的形式存在,大部分的数据开发工具都提供了功能,使得开发者可以用SQL的来开发ETL。但是只有SQL常常难以满足开发需求,比如,我们很难在SQL中发送HTTP请求、打印日志或断点调试。这里可以推荐Thoughtworks开源的Easy SQL,它基于SQL进行语法增强,提供了一种方式使得我们可以更加模块化的组织ETL代码,支持了变量、日志打印、断言、调试、外部函数调用等等功能。有了这些功能,我们可以在ETL代码中完成各式各样的工作,无需再结合其他工具使用,也无需自己编写复杂的代码。

ETL任务配置是指ETL任务运行时使用的各类配置。很多数据计算引擎都提供了配置接口,以便我们可以根据需要来配置最适当的计算资源、配置程序运行所需的外部文件、配置算法等。这些配置看起来不起眼,但是也非常重要,因为它们常常可以决定程序运行时性能,而这跟ETL任务的运行时间、稳定性紧密相关。所以,将ETL配置纳入到代码库中管理就显得十分必要。Easy SQL提供了一种能力,使得开发人员可以在ETL文件中定义ETL执行所需的配置,是一种支持将配置与对应的代码放在一起的好的实践。

数据流水线常常以一种“非代码化”的方式进行开发。很多调度工具都提供了界面,使得我们可以通过拖拽及配置来完成流水线的开发。比如阿里的Dataworks,Azure的ADF等。以“非代码化”的方式开发流水线的灵活性很差且无法享受到版本控制的优势。一些开源工具提供了代码化能力,比如受到很多数据开发人员喜爱的Airflow,它支持用python代码来定义数据流水线,然后根据流水线定义进行可视化展示。对开发人员更友好的方式是,提供一种自动管理数据流水线的机制,这样开发人员就无需编写流水线了。这是可能的,事实上,完全可以编写一个程序,解析出ETL代码中的输入输出表,然后根据这些信息自动提取ETL间的依赖关系,接着根据这些依赖关系就可以自动生成数据流水线了。

运维脚本常常以代码的形式呈现,但是很多数据工具希望将此类脚本纳入工具内部管理。这容易让我们丧失代码化的能力,因为它总是鼓励我们将此类代码配置到工具的UI界面里(可以想象一下在Jenkins还不支持用Groovy编写CI/CD流水线时的使用方式)。

业务注释是另一类可以考虑代码化的资源。很多团队将此类信息纳入到一个名为元数据管理的应用中进行管理。元数据管理应用通常可以提供一些基于自然语言的搜索能力,而且可以提供友好的界面展示。这是其优势,但是对于此类信息的维护,就不得不在元数据管理应用中完成。这常常带来另一些问题。比如,当我们重建某些数据库表时,元数据管理应用无法将原来的元数据迁移到新表。还比如,元数据管理应用常常无法提供完善的数据版本管理功能,从而使得我们无法进行版本追溯及回滚。如果将此类业务注释放到代码库中进行管理,就可以享受到代码化的优势,并且,通过调用元数据管理应用的API可以此元数据同步到元数据管理应用,从而我们也能享受到元数据管理应用提供的搜索即友好的数据展示的能力。

当然,实际项目中可能还有其他一些没有提到的资源类型,这里不在于为所有资源列举代码化方案,而是更多的提供一种代码化一切的建议。当我们发现团队正在以一种非代码化的方式进行数据开发时,可能需要思考有没有什么好的方案可以转变为代码化的方式。这将给我们的开发带来非常多的好处。

数据复用与代码复用

1.应用软件开发中的复用

在应用软件开发中,代码复用是一项显而易见的工作,开发人员几乎每天都会进行。良好的代码复用可以有效降低代码重复率,提高效率,并减少潜在的BUG。

应用软件开发中有哪些复用代码的方式呢?从代码复用的粒度上看,有两种基本的形式:

  • 定义函数,在多个地方调用此函数实现代码复用。各种编程语言均有支持。
  • 创建文件,将一系列相关元素置于此文件,在多个地方引用此文件实现代码复用。比如C语言中的include可以包含其他文件的内容。

2.数据开发中传统的复用方式

数据开发与应用软件开发存在一个显著不同,那就是进行数据开发时,我们不仅要关注代码还要关注数据。

(1) 数据计算成本

在应用软件开发中,有了现代CPU的支持,一般而言,一段代码的运行非常快。但是在数据开发中,我们经常会发现运行一个数据任务花费的时间甚至比开发这个任务花费的时间都长。这就导致我们不得不将很大的精力放在运行数据任务上。

我们常常小心地设计或选择算法,谨慎地优化任务运行所需的资源,仔细的比较两种不同的存储类型的性能差异,反复在同一个数据集上面进行验证。

我们不得不这么做,因为一段性能低下的数据计算代码,可能导致10倍的运行时间延长,最后不仅消耗了大量的计算资源,还无法满足业务需求。

在应用软件开发中,这个问题没那么显著,但是在数据开发中,这个问题的重要性就凸显出来。因为我们常常需要调度上百台计算机同时进行运算,这时,计算资源的支出就将成为我们不得不关注的问题。

以AWS云服务的定价进行计算,采用AWS Glue服务做计算引擎,按照本文撰写时的官方定价,如果调度100DPU进行10小时的计算,则将花费的费用是100 * 10 * 0.44 = 440美元,也就是约3000人民币的费用!

这还只是一个数据计算任务的费用,如果我们有100个任务呢?这个费用支出确实不菲!

做应用软件开发时,我们常常说,可以用廉价的计算成本来代替较高昂的人工成本。但是这一条规则在数据开发中并不那么适用。

(2) 基于数据复用

耗费如此长的时间与金钱才能计算出来的数据,自然是一笔重要的企业资产。于是,在数据开发中,我们采用最多的复用方式是基于数据的复用。

在数仓分层设计方法中,我们常常构建可复用的数据分层,下图是一个典型的数仓分层结构。

ODS贴源层作为一个可复用的数据分层,为DWD明细层及公共维度层提供数据。DWD明细层及公共维度层作为基础数据,为上层的众多指标开发提供数据支持。开发出来的指标数据作为一个分层,支持更上层的数据应用层数据。(此处的数据分层命名仅供参考,业界尚无统一的标准)

在实践中,我们常常需要仔细设计数据分层,在不失灵活性的同时达到良好的复用效果。

2.基于数据复用的问题

基于数据分层的方式进行复用应用非常广泛,但是它也存在一些缺点。

(1) 首先是灵活性较差。

后一层对前一层的数据存在很强的依赖,所以,如果前一层的数据结构没有被设计出来时,就无法进行后一层的开发。而当我们希望设计一个数据分层可以满足后一层的大量的数据需求时,这里的设计又会变得特别复杂,常常要左右权衡,花费了大量的后一层开发不愿意等待的时间。当前一层数据构建好了之后,如果后一层需要的数据无法满足时,还不得不修改上一层的代码并重新运行计算任务。

(2) 其次是整体数据计算过程难以理解。

当我们发现计算结果不符合预期时,我们往往要追溯从数据源开始的整个数据计算过程,仔细分析内部转换逻辑,才能找到问题。当存在多个数据分层时,我们不得不往下查找每一层的计算过程。而越往下越难。这通常是由于下层在设计上要保持更高的适用度,以便支持更多的上层数据需求,而这导致很多与当前需要的数据无关的计算杂糅在一起。

在分析问题时,一个较理想的情况是,和某个指标相关的ETL的全部代码都在一个文件里面,这样就不需要多个文件跳转。同时,我们也不希望有不相关的逻辑存在于这个ETL文件中,这样我们就可以专注在问题分析上。基于数据分层的复用恰好产生了与期望相反的副作用。

3.基于代码的复用

在这里我希望给大家介绍“基于代码的复用”这一实践。基于代码的复用方式虽然可能会由于不能共享计算资源而导致付出较大的计算资源成本,但没有上述缺点。而且,如何处理得当,基于代码的复用也可以一定程度上避免计算资源浪费。

基于代码的复用方式在数据开发中实践不太多,但却是非常值得尝试的一个方向。

在数据开发时,如何使用在应用软件开发中广泛使用的基于代码的复用方式呢?

(1) 数据库视图

大部分数据库都提供了视图机制,视图是一个虚拟的表,它本身仅仅包含了一些转换逻辑,但并没有真实的将数据计算出来并存放在物理存储中。这给我们带来了一些启示。是不是可以利用视图的原理进行代码复用呢?视图可以理解为一段代码,查询视图即是在进行代码复用。

事实上,现在的很多数据库还在视图的基础上提供了物化视图的机制,我们可以将一个视图转换为物化视图,让数据库在合适的时机将视图中的数据计算出来,从而自动的提升数据计算性能。

视图及物化视图给我们提供了非常好的灵活性,因为我们轻松的可以在基于数据的复用和基于代码的复用两者之间切换。

物化视图还在一定程度上采用基于代码复用的方式实现了基于数据的复用。

(2) 实现ETL执行驱动器

除了基于视图进行代码复用,还可以自实现一个ETL执行驱动器,由它来提供一些代码复用的机制。比如dbt Easy SQL就是这样一些开源的ETL执行驱动器。

Easy SQL提供了模板来实现类似函数级别的复用,详情可以参考这里。同时它也提供了基于文件的复用,通过Include指令可以将其他ETL文件包含到当前文件,详情可以参考这里。

除了使用这些开源工具,想要自实现一个这样的驱动器也不复杂。如果我们的计算引擎是 Spark,那么我们可以使用Spark的DataFrame API,进行一些开发就可以完成。

如果有足够的研发投入,基于自实现ETL执行驱动器的方式可以做得非常智能,达到甚至超过数据库视图和物化视图的效果。一个可以考虑的方向是,程序可以自动分析所有ETL执行过程,然后用算法识别可以有较多复用的中间结果,然后自动将中间结果保存到某处。在后续ETL执行时,自动从中间结果取数据,而不是重新计算。

目前市场上还未见到此类智能的ETL执行驱动器出现,不过,在我看来,这是一个不错的研究方向。

4.选择哪种复用方式

在实际项目中,如何选择复用方式呢?有以下建议可以参考:

  • 某些ETL要处理大量的数据,计算过程要消耗大量的资源,且运行时间特别长,建议以基于数据的复用方式为主,就可以有效控制资源
  • 某些ETL只需要处理有限的数据,此时可以转换为基于代码的复用方式,从而获得较高的灵活性
  • 难以选择时,优先考虑使用基于代码的复用方式

以ETL为单位的持续集成

我最近和一个做进口贸易的朋友聊天,发现了一件很有意思的事:

他们公司进口国外高端仪器,并帮助销售公司处理竞标、合同签订、物流、海关、进口贸易政策符合、维保等复杂的事务。我很好奇,为什么销售公司不自己处理这些事务,反而出售给其他公司呢?向他请教后,获得了很多启示。

国内工业起步较晚,虽然现在已成为世界工厂,但很多核心生产设备仍需要进口。这个市场是一个万亿级的大市场。这个业务有什么特点呢?

  • 一是产品销售量少,因为高端仪器价格昂贵,一年只能销售数百台。
  • 二是销售流程特别复杂。进口需要处理很多实际问题,包括竞标、合同签订、物流、海关、进口贸易政策符合、维保等等。

那么,如何组织这种业务呢?

销售是必须的,但其他事务是否必须自己做?这值得思考。因为销售量不大,但其他事务特别复杂。如果培养一个专业团队来做这些事,由于销售量不大,团队工作势必不会饱和。如果减少团队人员数量,这些事务又难以做得专业,容易出纰漏。

在市场尝试和调整之后,专门做进口贸易的企业就诞生了。他们负责产品销售之外的大部分事务,包括竞标、合同签订、物流、海关、进口贸易政策符合、税收、维保方式设计等。他们通常是一个非常专业的团队,可负责各个领域不同产品的进口贸易业务。

于是,海外产品研发公司、国内产品销售公司和国内进口贸易公司的模式就在市场上慢慢形成并稳定下来了。这种模式提高了整个行业的效率和质量,也是进口贸易企业得以存在的原因。

从进口贸易企业的兴起中可以看到业务的重构和演变,即,通过合理的抽取和拆分提升了整体的效率。

1.以ETL为单位的持续集成

在应用软件开发中,我们常常仅设计一条持续集成流水线,在流水线中运行所有的测试,接着将所有代码打包成一个大的产品包,然后部署到测试或产品环境中。

在数据应用中,是不是也需要这样做呢?这样做的好处是可以将产品环境的制品与代码仓库中的版本对应。其劣势其实也很多,比如,修改一个局部的代码,就不得不运行所有的测试,然后运行流水线中所有耗时的步骤,可能还需要进入手工测试的环节,最后才能发布到线上。效率非常低下。

这一问题在数据应用中更是被放大了。因为数据应用通常涉及数百个指标计算ETL,这些ETL的自动化测试只能用缓慢的集成测试来覆盖,这就导致流水线中的测试步骤耗时很长。在我们的项目中,常常需要跑半小时到一小时才能跑完。

这就如同做进口高端仪器销售的公司,如果自己来做进口贸易相关业务,不仅耗时特别长,而且出纰漏的可能性大(业务质量低)。

有没有更好的做法?既然只修改了某一个ETL,为什么不能就只部署和测试这个ETL?联想到前面进口贸易业务的抽取和拆分,是不是可以对流水线进行抽取和拆分呢?即,做以ETL为单位的持续集成流水线。

在数据应用开发场景中,这也是具备可行性的。原因在于,相比应用软件代码中的一个一个类或代码文件,ETL间几乎没有依赖。不同的ETL代码通常有不同的入口,存在于一个独立的文件。可以认为一个ETL就是一个独立的数据应用。

事实上,如果以ETL为单位进行持续集成和部署,还不用担心自己的部署会影响到其他的线上指标计算ETL,这也在一定程度上增强了安全性。

看起来,在数据应用开发领域,以ETL为单位的持续集是顺理成章的事。

对比一下微服务实践,还可以发现,这一实践与微服务中推荐的为每一个服务搭建一条持续集成流水线的实践几乎是等同的。

2.如何实现

如何实现以ETL为单位的持续集成呢?

如果基于Jenkins,可以在流水线上面加一个参数,如“ETL文件路径”,在运行流水线时,可以指定这个参数,让流水线仅针对指定的ETL运行测试与部署。

如果觉得在Jenkins上面实施以ETL为单位的持续集成较为麻烦,也可以团队自主开发一个专用的数据持续集成流水线。如果仅实现基本的功能,其实也并不复杂。

需要注意的是,一旦以ETL为单位进行持续集成了,就需要有一种方式记录每一个ETL对应的代码仓库里面的版本号,方便版本追溯。实现方式有多种,比如,可以在部署ETL的时候,在生产环境写入一个该ETL对应的版本文件。

总结

本文介绍了什么是敏捷数据工程, 并分析了几个有效的实践。如果能灵活的在数据项目中应用,将有效提升我们的数据产品交付质量。

在数据开发领域,目前敏捷的应用还处于前期探索阶段,还有很多值得深入的方向,比如自动化的ETL测试、较短的单ETL文件、端到端数据能力的团队等等。希望和大家一起探索!