数据工程需要自己的 DevOps 时刻:把数据管道当作 24x7 服务
Data needs its DevOps moment
数据栈仍停留在 ELT 模式——把每张原始表逐字节复制进中心数据湖,再由数据团队用 cron 逆向出指标,反馈回路长达数月,团队只能靠永久保存原始数据来兜底。文章主张数据领域补上 DevOps 那一课:管道即代码、SQL 逻辑与环境解耦、CI 门禁每次部署、管道作为带 SLO 的 24x7 服务上报 lag、freshness、completeness 指标。
十五年前,工程师编写代码,然后交给运维团队。运维手动配置服务器(如果幸运的话,用脚本!),按计划部署,并随身携带寻呼机。没有人测试基础设施,因为基础设施不是代码。每个人都期望 100% 的正常运行时间,但没有人能够实现。
DevOps 和 SRE 通过将发布和运维视为软件问题解决了这个问题。基础设施变成了仓库中的代码,部署通过 CI 运行,监控成为一流的工程关注点。SRE 用你可以发布和使用的错误预算取代了不可能的 100%。最重要的是,编写软件的团队获得了发布和操作它的工具。
数据还没有完成这一转变。
现代数据栈建立在提取加载转换(ELT)之上:将每个原始表逐字节复制到中央数据湖,然后由数据团队将其逆向工程为指标。产品团队把表扔过墙。数据团队使用 cron 学习公司中的每个领域,通常没有测试,面对的是 100% 准确率的期望,而这是任何软件系统都从未达到过的。当分析表出错时,修复方法是将所有内容永久存储在数据湖中以重放历史。
数据团队说问题在于需求总是在变化,这是事实。但这也是每个曾经构建的产品都面临的问题,而软件团队并不会不惜一切代价避免模糊的需求。相反,他们发布、衡量并改进。数据之所以陷入困境,是因为反馈循环太长,无法做到这一点:一个糟糕的转换可能几个月都无人察觉,所以团队保留每一个原始字节作为保险。
部分问题在于架构。“数据管道”意味着一个集群、一个 JVM 和一个针对云仓库运行的 Airflow 服务器。你无法将它放在同一个平台、同一个仓库、同一个待命轮换中,紧挨着你的应用。因此管道存在于别处,由另一个团队拥有,每次更改都是一张工单。
这个限制已经消失了。一个实时管道现在占用的内存比一个 Rails 进程还少。这篇博客中的管道解决了数据反馈循环。它存在于仓库中,由 CI 把关,报告自己的健康状况,并遵循 24x7 的软件标准。一旦你能看到管道中断,你就不会再为了以防万一而囤积每一个字节。
将 DevOps 运动应用于数据,有四个部分,每一部分都缩短了反馈循环。
管道即代码。管道是生产团队仓库中的一个配置文件。它在拉取请求中接受审查,与发出数据的应用程序一起进行版本控制,并通过同一个按钮部署。没有人需要提交工单来更改它。
逻辑是 SQL,并且与环境解耦。未来的数据栈具有独立的逻辑,用 SQL 编写,带有输入源和输出接收器。相同的 SQL 可以在笔记本电脑上的 JSON 夹具、CI 中的 Kafka 主题以及生产中的 WebSocket 数据流上运行。这种解耦使得测试成为可能。当你的转换只在仓库中运行时,测试仅仅意味着检查结果。当逻辑可移植时,你可以像测试任何函数一样测试它:固定输入、预期输出、每个用例只需毫秒。
CI 为每次部署把关。输入和输出都是通用的,因此测试可以用文件模拟源,并对汇进行断言。一个 docker compose 文件启动 Kafka 和 Postgres 用于集成测试。一个浸泡测试目标针对实时流运行发布镜像并记录其内存。这些原语是可扩展的,因此团队可以使用相同的标准工作流添加自定义验证检查。可验证的转换能建立信心,并消除存储原始历史记录的需要。
该流水线是一个带有 SLO 的 24x7 服务。它报告一组标准指标,这些指标对每条流水线含义相同:延迟(处理落后于事件时间的程度)、新鲜度(物化输出的陈旧程度)和完整性(实际产生了多少预期窗口)。无需等待有人在早上检查批处理作业,这些指标直接为你的告警和错误预算提供支持,就像任何其他生产服务一样。
本博客的其余部分使用托管在 Render 上的 Bluesky firehose 和分析 API 来展示这些概念的实际应用。
该流水线实时按语言统计每条公开的 Bluesky 帖子。它由三部分组成,全部通过一个 Render Blueprint 部署:
- 一条 SQLFlow 流水线作为 Render 后台工作进程运行。它通过 WebSocket 读取 Bluesky 的 Jetstream firehose,按语言将帖子分组到一分钟的滚动窗口中,并将每个关闭的窗口 upsert 到 Postgres。
- Render Postgres 为每种语言每个关闭的分钟存储一行。
- SQLFlow serve 作为 Render Web 服务运行。它将 SQL 查询暴露为 HTTP API。
流水线定义是可版本化且可测试的 YAML 和 SQL。没有 Kafka,没有调度器、编排器或外部数据质量供应商。turbolytics.io/demos/bluesky 上的仪表板直接调用此 API。
每个 SQLFlow 服务在具有 0.5 CPU 和 512 MB 限制的 Render 实例上运行。Postgres 在 1CPU/512MB 实例上使用不到 0.1 CPU 运行。

使这条流水线小到足以作为服务高效运行的设计决策很简单:先聚合,再存储。
标准 ELT 流水线则相反,最后才进行归约。它们将每条原始记录复制到中央存储,然后运行繁重的计算任务将这些副本建模为少量指标。虽然生成的指标很小,但产生它们所需的计算和存储占用却非常庞大。
通过在摄取点首先进行归约,这种架构大幅削减了运维开销。当大约每秒 40 条帖子到达时,流水线在内存中聚合它们,并且每分钟每种语言只向 Postgres 写入一行汇总数据。例如,在 9 月 12 日至 17 日期间,流水线将 1500 万条原始帖子处理为仅 6,577 行一分钟窗口数据。仪表板直接查询这 6,577 行聚合数据,而不是扫描 1510 万条单独记录。

流式系统聚合这件事已经做了十年。新变化在于,无需集群就能运行这一过程,配置文件与产生数据的应用一起放在 Git 中,还配有单元测试和可观测性。这样的世界需要弹性、可快速配置、大小合适的实例,而不是运行 JVM 的大型、长期存活的实例。
我们已经信任软件工程师在生产环境中处理客户数据。他们拥有构建可验证服务的工具和背景。没有理由让聚合这一步成为唯一脱离他们掌控的环节。
流水线的第一个版本在周五晚上上线。单元测试套件全绿,本地流式测试平稳运行了一个小时。到周六早上,Render 的内存图表看起来是这样的:

工作进程从约 100 MB 增长到约 450 MB,逼近 512 MB 的上限,呈直线上升,仅用了一天。任何测试都不可能发现这个问题,因为这个 bug 需要数小时的真实流量才会显现。Render 的图表之所以能发现它,是因为它默认可用。
找到原因需要使用 Go 调试工具链(称为 pprof)和 DuckDB 诊断工具。Go 内存剖析保持平稳,因此增长发生在 Go 之外。duckdb_memory() 报告了 DuckDB 估计其持有的数据量,我们通过在五次八小时浸泡测试中对比该值与进程的常驻内存,缩小了范围。一旦检查点写入,DuckDB 就永远不会释放从索引表中删除的行。泄漏条件需要同时具备唯一索引和定时删除,而这正是 SQLFlow 滚动窗口所做的事情。
修复方法是更改窗口表结构,图表上的部署标记标出了这次部署。内存降至约 80 MB 并保持稳定。这就是本博客前半部分详述的反馈循环:一个假设、一次部署,以及一张图表,在实时流上大约两天内得到确认。

同一周,图表又暴露了第二个 bug。修复后,明显的泄漏消失了,但内存仍在缓慢增长。DuckDB 的 Postgres 扩展不会将 ON CONFLICT 发送给 Postgres。它在每次 upsert 时都会将整个目标表的键列复制到 DuckDB 中,因此随着表增长,每次写入都会将越来越多的数据拉入应用。我们提交了 duckdb-postgres #575,并将 SQLFlow 迁移到基于 pgx 的原生 Postgres sink。
这两个 bug 都无法通过测试套件发现,也都不需要数据质量供应商来捕捉。数据管道的值班信号与其他任何服务相同:带有部署标记的内存图表、能重启卡住进程的健康检查,以及可以用 Render CLI 跟踪的日志。这次调查还催生了一个新测试:SQLFlow 的 make soak 目标会在任何发布前,针对实时流运行发布镜像并记录其 30 分钟的内存使用情况。

快速的本地反馈能发现逻辑 bug,生产反馈能发现长期存在的 bug。两者都必不可少,而后者只有在管道部署在将其视为服务的地方时才有效。
上周,我们发布了 Deploy to Render Blueprint for SQLFlow。只需一键,你就能获得一个经过 HMAC 认证的 webhook,供你的应用 POST 事件,用 Postgres 存储这些事件,并在前面提供一个查询 API。你通过 HTTP POST 事件,再读回窗口聚合结果。这足以接收大量应用事件,并将它们直接送入面向客户的仪表盘,中间无需任何其他组件。
整个技术栈就是一个 render.yaml。
我们用这套完全相同的技术栈来托管 turbolytics.io 的遥测服务器。SQLFlow 在首次启动时会带着一个匿名安装 ID 和版本号回传一次。(你可以用 SQLFLOW_TELEMETRY_DISABLED=true 关闭它。)一个 webhook POST 会把这个 ping 直接发送到运行在同一个 $7 Render 实例和 Postgres 套餐上的 SQLFlow 管道。

这套相同的技术栈还用来衡量 SQLFlow 的采用情况!当有人点击部署按钮时,他们的实例启动,发送一个 HTTP 请求,我们这边的一个滚动窗口就会计数。我们通过一个 SQLFlow 查询来跟踪这个指标,并在用于识别内存泄漏的同一张内存图表上监控底层管道。如果发布量激增而遥测管道崩溃,我们会以发现 DuckDB 问题同样的方式发现它。
基础设施在代码管理和部署直接落入软件开发者手中时,迎来了它的 DevOps 时刻。数据的时刻尚未到来。不是因为数据本质上更难,而是因为一条管道通常涉及一个集群,而这个集群无法与你的应用、你的代码仓库或你的值班轮换放在一起。
一条高效的云原生数据管道像任何其他服务一样运行:一个后台 worker、一个数据库、一个 web 服务、一个 render.yaml。而一旦它成为一项服务,你就不需要专门的数据平台来运行它。可观测性功能能发现内存泄漏并识别稳态,健康检查处理重启,CI 门控部署。
这些都不是为数据而构建的。云、发布管道和可观测性作为最佳实践已经确立了近 20 年。改变的是数据工具。一个 32 GB 的 JVM 集群无法运行在 $7 实例上,所以它住在自己的平台上,有自己的运维人员。而一条能塞进 256 MB 的管道可以。数据的 DevOps 时刻不需要新的基础设施。它是数据工具终于被构建成能运行在其他人早已拥有的基础设施上。
在这里将 SQLFlow 部署到 Render。。想看看它的实际效果,请访问在线演示站点并深入了解源代码。
来源:Hacker News · render.com