白天设计数据流架构,夜晚撰写技术文章。分享经过实战检验的分布式系统工程经验。
数据摄取任务不断出现内存耗尽的问题。增加计算资源为我们争取了时间,但随着导出历史数据的增长,故障再次出现。一个任务在处理最新的投递记录之前,必须先处理不断增长的旧文件清单。
我是一名高级数据工程师,负责将第三方营销供应商的消息投递数据导入我们数据仓库的管道。这些记录支撑着跨电子邮件、短信和推送通知的营销活动分析和合规调查。当数据摄取停滞时,使用这些数据的团队只能等待。
瓶颈跨越了公司边界。我们的消费端在执行工作,但导致工作代价高昂的导出布局却由供应商控制。我与供应商的工程团队合作推进按日期分区的投递方案,并在他们构建新布局的同时,让我们的消费端做好适配准备。
此处不披露供应商名称;本文聚焦于该集成中的工程决策。当供应商交付的文件需要另一个团队去发现、摄取并在故障后恢复时,这一经验都具有参考价值。
症状最初指向我们自己的基础设施。原本几分钟完成的任务开始耗时数小时。有些任务耗尽了内存。重试失败的任务会重复大量的发现工作。增加计算容量虽然降低了故障频率,但并未解决工作负载为何持续膨胀的根本问题。
我们的集成通过列出一个共享的、未分区前缀下的对象来发现导出文件。历史导出和近期投递累积在同一个命名空间中。为了识别与某次运行相关的文件,消费端必须枚举更广泛的对象集合,然后再进行过滤。
这一区别很重要:列出对象元数据与下载并解析每个文件是不同的。当键的数量增长时,仅发现步骤本身就可能变得代价高昂。如果实现方式将这些键累积在内存中,还可能在深入处理实际事件之前就发生故障。
分页可以限制列出操作所使用的内存,前提是消费端逐页处理而不累积完整结果。但它并不能消除对历史对象的重复枚举。我需要减少一次正常运行所需考虑的历史数据量。
有用的诊断问题很简单:摄取成本是跟踪新数据的量,还是我们曾经接收过的所有数据的量?在这个集成中,保留的历史数据正成为每次运行成本的一部分。
我本可以再构建一层,将导出数据复制到我们控制的布局中。但那会引入第二条交付路径,需要运维、监控和对账。我想先与供应商解决源布局问题。
我将讨论作为一次设计评审来进行。我的角色是解释消费方的故障模式,使请求具体化,并让我们的实现为变更做好准备。供应商的工程师负责导出器以及修改它所需的工作。
有三件事帮助推动了对话。
首先,我努力理解他们的导出流程如何生成文件。一个有用的提案必须同时符合生产方和我们的约束。
其次,我将症状与特定的访问模式联系起来:广泛的对象枚举、不断增加的发现开销、内存压力,以及重复相同工作的重试。这比笼统地要求加快交付更有说服力。
第三,我提出按日期分区的输出作为一种可复用的能力。需要特定时间窗口的消费方应该能够直接访问该窗口。这也会使按天重试和历史回填更容易隔离。
这些讨论需要持续跟进。供应商同意增加按日期分区的交付,我同时进行相应的消费方变更。结果取决于双方各自交付接口的部分。
下面的布局是示意性的。它使用 event_date 来展示预期的访问模式;路径、文件名和日期并非供应商导出格式的规范。
之前:一个前缀需要枚举
exports/file-a exports/file-b exports/file-c
之后:可寻址的事件日期前缀
exports/event_date=2026-08-01/file-a exports/event_date=2026-08-02/file-b exports/event_date=2026-08-03/file-c
采用这种布局,针对 8 月 3 日的运行可以列出 8 月 3 日的前缀。针对 8 月 1 日的回填可以寻址该日期,而无需枚举不相关的日期。
在这种对象存储模型中,前缀是对象键的开头,而不是物理目录。按前缀过滤的列表会返回以指定值开头的键。此示例说明了访问模式,但未指明存储提供商。
对于基于列表的消费者,这种差异可以用一个简单的模型来表达:
H = all historical objects under the shared prefix W = objects in the event-date window selected for this run Broad discovery: enumerate H object keys Partitioned discovery: enumerate W object keys
这是一个关于发现工作的模型,而不是基准测试或运行时保证。当所选窗口远小于完整历史时,它会有所帮助。数据量、文件大小、API(应用程序编程接口) 开销和处理成本仍然很重要。单个繁忙日期本身就可能包含许多文件。
分区存储也不是唯一可能的解决方案。对象清单、库存或通知驱动的消费者可以提供另一种发现新文件的方式。重要的特性是,常规摄取应具有有界的发现路径,并有单独的方式来恢复丢失的数据。
日期分区本身并不能使管道变得完整或恰好一次。当分区键是事件日期而不是文件到达日期时,这一点尤为重要。
8 月 1 日的事件可能会在 8 月 3 日送达。只读取今天事件日期前缀的消费者会错过它。昨天成功运行并不能证明昨天的分区永远不会再收到另一个对象。
对于实现此模式的读取者,我会将以下问题纳入生产者-消费者契约:
什么时间戳和时区决定分区日期?
较旧的分区能否接收新文件、更正或替换?
如何发现新对象,以及如何发出完成信号(如果有的话)?
在重试过程中,什么标识对象版本和单个事件?
数据保留多长时间以用于重新处理和核对?
消费者可以重新访问最近的事件日期窗口,前提是该窗口反映了商定的迟到策略。在该窗口之外到达的事件仍然需要恢复路径,例如核对和有针对性的回填。仅靠回看窗口并不能保证完整性。
处理序列还需要谨慎的失败处理。一个有用的设计草图是:
for each date in the agreed reprocessing window: list that date's prefix one page at a time for each unprocessed object version: validate its records write using stable event identities commit the data record durable completion for the object version
这是该模式的伪代码,不是我们的生产实现。如果工作进程在提交数据之后、记录完成之前崩溃,它可能会重放该对象。接收端必须容忍这种重放,或者数据写入和完成标记必须原子性地提交。仅靠对象级跟踪并不能消除在不同文件中传递的重复事件。
正是这些细节使得分区布局在可靠系统中变得有用。文件夹结构创建了更小的工作单元;处理契约决定了该单元能否被安全地重试。
在供应商进行导出更改期间,我为新布局构建了消费者,并在开发环境中进行了部署。下游模式和目标表保持一致。变化在于管道如何发现和读取交付的文件。
我在生产发布之前对照样本输出进行了验证,并处理了不匹配的问题。这使得两个实现能够在仍有时间调整时趋于一致。
当供应商验证其生产输出时,我们的消费者已经准备好进行推广。我们不必在供应商完成后开始单独的实现周期。
对于计划进行类似切换的团队,我会明确切换标准:模式兼容性、重叠期间的记录对账、重复处理、新鲜度检查,以及避免不受控双重摄入的回滚边界。这些是迁移计划的建议,并非声称新的目录布局能保证无损切换。
分区交付使我们的摄入更加可靠,并为我们提供了一种按日期定位重试和回填的实用方法。下游团队可以继续使用相同的表,而摄入路径在其下方发生了变化。
我们的消费者现在可以选择导出历史的一个有界切片。这使得恢复更容易推理:失败的一天可以按天重新处理,而不是被视为对整个集合的另一次搜索。
这段经历改变了我对基于文件的集成所关注的内容。我现在将发现、分区语义、延迟到达和重放行为视为接口的一部分,与模式并列。如果消费者无法可靠地找到它,一条正确的记录就没有多大用处。
这也强化了与接口另一侧团队直接协作的价值。我带来了故障证据和消费方变更。供应商的工程师带来了导出器变更。双方共同准备,才将日期分区从一个提案转变为可工作的集成。
教漏斗理解非线性客户旅程
白天架构数据流,夜晚技术写作。分享分布式系统中经过实战检验的工程经验
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Hacker Noon
原创地址:https://hackernoon.com/how-i-fixed-a-data-ingestion-bottleneck-with-date-partitioning?source=rss
原文作者:Rahul N;如有侵权,请联系管理员删除。