放弃阿帕奇气流

251 阅读8分钟

我在听这个关于Prefect的播客,一个试图从Apache Airflow的缺点中学习的数据流调度器。我突然想到,我讨论了我们公司内部部署和随后放弃Airflow的决定,实际上我在很多场合都讨论过这个问题,所以我不妨把它写下来。

起始阶段

我们有一个工作调度器,在一个不具名的系统中触发各种工作(为了避免痛苦的回忆)。有一天我们意识到我们不需要使用它的企业版,因为它在社区版的基础上带来的只是一个作业调度器。我可以在一个周末做到这一点(c)!

当时(2017年底),Apache Airflow是首选的解决方案,竞争对手--Luigi和Pinball--还没有那么成熟,或者缺乏关键功能。一开始我有点担心,因为Apache项目严重倾向于JVM,但这是Python,我们在其他任务中采用了Python,所以最初的部署是超级简单。安装一个包,把它和(我们现有的)Postgres实例连接起来,然后我们就开始了。

摆弄了几天后,我们设置了一个Jenkins作业来部署这个安装,它在我们的基础设施中生活了大约6个月,成功启动了数万个作业。然后有一天,我把它全部删除了。以下是其中的原因。Tl; dr: 它做的事情远远超过了我们的需要,而且在开发过程中,它的复杂性经常妨碍我们。

重新加载工作

我们的第一个障碍是刷新Airflow的运行状态。我们把所有的工作都放在git repo中,当我们推送新的代码时,我们需要Airflow来反映它并开始调度新的工作(或停止调度现有的工作)。这不是一个可以解决的问题,我们最终制定了一些杀戮/启动顺序(是的,我们把它关掉又打开)。

这不仅从清洁的角度来看是次优的,而且有时也不合适--数据库中会有残渣,一些工作会被挂起,这只是一个非常明显的问题的愚蠢的解决方案。

我们的ETL中有太多的Airflow代码

类似Airflow的系统的一个主要优点是,它可以解耦任务,让你运行它们,必要时重试,并促进它们之间的沟通(例如,"我在s3://foo/bar创建了一个S3对象")。听起来很酷,但这被证明是相当有噱头的,并导致越来越多的Airflow库整合到我们的内部ETL代码中。我们想把事情分开,但这实在是太综合了。

另一个例子是执行时间--而不是使用now() ,你会得到一个工作执行的时间戳--如果你的工作失败了,你需要回填它,这是非常方便的--用过去的 "假 "时间戳运行它。理论上很好,但这是一个令人困惑的概念,很多人(不包括我)都不太理解,导致了令人惊讶的作业调用和错误(尤其是在午夜时分)。

这方面的最后一个障碍--也许是最大的障碍--是本地使用中的调试。我非常相信在本地机器上可以构建和调试的程序。我不想为了运行一项工作而产生12个Docker容器,我希望看到正在发生的事情,以适当的方式进行调试(print everywhere),并且总体上处于控制状态。这在Airflow中是不可能的,因为它接管了作业的调用,而且克隆和调试也非常麻烦。

部署和依赖性

我们部署Airflow的方式(可能不是现在的方式)是,我们有一个Jenkins作业,将其安装在一个长期运行的EC2盒子上,并在该机器上运行作业(Airflow提供了独立于调度器的工作者,但我们不需要,因为我们的作业相当轻量级)。

现在,这个方法在运行时效果很好,但在更新时我们遇到了问题。无论是Python的依赖性还是Python本身。或者环境变量。或者是操作系统。这是一个操作所有东西的大的持久性的东西,而且很难将事情隔离开来,也很难有可重复的运行。

缺少与我们的基础设施的整合

我们在公司内部已经有一大堆技术来运行东西(Gitlab、Mesos、Graphite、Grafana、ELK),而我们的Airflow解决方案在监控、日志或资源管理方面没有与任何这些技术集成。Airflow后来获得了对Kubernetes的支持,所以它在这方面越来越好,但在当时,我们处于黑暗之中,不得不建立自己的Slack集成和其他报告,只是为了让我们了解发生了什么。

意识到的时刻

有一次,我们意识到一件事--我们没有使用Airflow提供的一半功能,其余的功能妨碍了我们的工作,而不是帮助我们。那么,有没有一种更好的方法来做这件事呢?我们需要一种方法来安排事情,对它们进行记录、监控和处理依赖关系等。

我们已经有一个类似于cron的解决方案在我们的基础设施中,所以我们只是使用它。没有作业的依赖性,没有进程间的通信,只有cron的时间表和失败时的重试,但它最终工作得很好,我们只需要做一些事情来使它发挥作用。

让所有工作独立

在使用Airflow之前,我们必须检查 "工作B现在是否正在运行?如果是,就放弃",因为有些工作是不能重叠的。有了Airflow,我们只需将它们一个接一个地缝合起来,就能很好地工作。由于我们的新解决方案没有这个功能,所以我们必须解决这个问题,但这并不难。我们只是确保工作可以同时运行。最后,我们意识到,不应该有理由让两个作业不能同时运行--如果我们让所有的作业都是原子的,并且是empotent的(后面会详细介绍),那么一个同时运行的作业要么看到旧的数据,要么看到新的数据,但中间没有任何数据,所以什么时候运行作业并不重要。

要做到这一点,我们必须确保两件事。

  1. 原子性--每个作业要么写所有的东西,要么不写。没有部分结果。这在数据库中是相当微不足道的,因为数据库是为此而建的,我们不得不在其他地方考虑一些事情。例如,在S3中,我们只是在本地缓冲结果,或使用一些标志,或在数据库中使用元数据,只有在持久化数据后才会写入等等。
  2. Idempotency--这是任何工作的一个关键组成部分,不管是什么调度器。它说的是,不管你运行一个作业多少次,它的行为总是一样的。这救了我们很多次。

这也导致了清理工作的缺乏(在此之前,我们总是要确保删除所有的部分结果,这些结果可能是由于作业中途的失败而出现的),一切都更加可靠。

链式工作

这是一个棘手的问题,说实话,是我们解决方案中最不令人满意的地方。你如何以一种很好的方式组成你的工作的一部分(整个DAG口诀)?我们通过将作业分解成更小的部分来实现,这些作业可以独立运行,当我们需要按特定顺序运行时,我们只需将这些作业导入一个小的协调器并在那里运行。

为了使其发挥作用,我们必须有一个简单的系统--每个工作最终都有一个main() 函数,当主文件被调用时,它就会被运行。但是,你同样可以有一个不同的文件来导入这些不同的主函数,并代替它们进行调用。因此,如果我们想的话,我们可以组成一些较小的作业,并行地运行它们,但我们大多没有这样做。

结论

我们最终得到的是一个更好的代码库,不仅仅是因为我们的代码中没有Airflow的残渣,而且还因为我们不得不考虑建立可靠的作业,这些作业可能会在任何时候重叠或失败。从devops的角度来看,这个系统更容易操作,有更好的可靠性、监控和日志记录--但这些只是因为我们已经有了一个类似cron的系统。如果你没有,你的里程可能会有所不同。另外,我们没有巨大的、有几十种复杂关系的dags--对于这些用例,像Airflow这样的数据流系统仍然是很适合的。

我个人认为最重要的是,这对本地调试来说是非常容易的。你只需克隆一个 repo,在虚拟环境中安装一堆依赖,就可以了。你可以建立一个Docker镜像,你会看到生产中会发生什么。最重要的是,你只是在运行简单的Python脚本,你可以用一百万种不同的方式来反省它(但是,你知道,打印是最重要的方式)。

我不是说人们应该走这条路,但随着我获得更多的经验,我珍视简单性和可调试性(这两者往往是携手并进的)。这让我知道,虽然Airflow很好,提供了很多很酷的功能,但它可能只是妨碍了你的工作。