ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GooseFS写缓存实战:破解自动驾驶数据管道写入瓶颈

GooseFS写缓存实战:破解自动驾驶数据管道写入瓶颈 自动驾驶路上的数据比大多数人想象的要“脏”得多。一辆测试车配备激光雷达、毫米波雷达、多路高清摄像头一天跑下来产生的原始数据经常在 500GB 到 1TB 之间。车队规模一上来回传、抽帧、清洗、标注、训练集生成整条数据处理链路每天都吞吐着几十 TB 的新数据。这个体量下最先被压垮的往往不是模型训练而是数据链路最容易被忽略的一环写入。我所在的团队负责自动驾驶训练数据的存储与预处理平台之前很长一段时间被写链路拖得很难受传感器文件数量巨大单文件又小回传车端数据时要先写本地盘、再定时转移到对象存储转储窗口期经常出现积压训练集群读取数据时又要从对象存储拉回本地冷数据反复出库I/O 成本越来越高。后来我们引入了 GooseFS 的写缓存能力把端到端数据写入、校验、可读生效的耗时压缩了约 77%。这里把整个方案从原理、设计、落地到踩坑完整记录下来给同样在做自动驾驶数据处理或高通量数据入湖的朋友一个参考。1. 自动驾驶数据处理链路与瓶颈定位1.1 一条训练样本的“前半生”自动驾驶数据并非天生就是训练集。一组原始数据从车端到 GPU 训练集群中间要经过至少五个环节采集落盘、回传上传、完整性校验、抽帧清洗、样本打包。其中回传和清洗阶段是 I/O 最重的地方因为在“回传”之后所有下游任务都要基于这些文件重新组织目录结构、做场景切分和样本去重。以我们日常跑的数采车为例每辆车每秒钟会产生 30 到 60 个传感器文件单文件大小往往只有几 KB 到几百 KB。一个 8 小时的工作日跑下来单车的文件数量动辄几十万。即便车端有本地缓存回传到机房的依然是海量小文件。这种分布特征在架构上非常不友好——对象存储OSS/COS擅长承载大对象、高并发读但小文件写入时的请求开销、分片上传耗时都会被成倍放大。更麻烦的是写入链路上的“阻塞感”。早期我们采用“车端直传对象存储 定时扫描做延迟补偿”的方式数据到了对象存储桶里计算平台才能开始消费。一旦某个批次的车端数据上传慢后续的校验、清洗、抽帧任务全部排队。数据平台的同学经常要半夜起来处理转储任务卡死车上数据明明已经传完却因为对象存储的写延迟和失败重试导致几个小时后采集任务才能闭合。1.2 写链路瓶颈直写对象存储到底慢在哪直写对象存储的方案看起来很直接实际上慢在四个细节上第一小文件写入的请求开销。一个 8KB 的雷达点云文件直传对象存储需要完成建立连接、认证签名、上传、返回确认这整套网络交互。传输的数据只有 8KB网络和协议的开销却占了绝对大头。几百万个小文件叠加起来时间成本非常可观。第二失败重试的“长尾效应”。对象存储对写请求返回错误时业务代码通常要做指数退避重试。个别网络抖动会导致一批文件的重试耗时翻好几倍。在数据量大、并发高的场景下这些重试会造成大面积阻塞而日志里往往只能看到“请求超时”这类模糊信息。第三写入后再读取的冷却时间。对象存储写入完成后数据要经历一致性生效的阶段。训练侧为了拿到“刚写完的数据”必须做好等待和轮询。上一套方案里为了确认数据可读我们不得不在业务流程中增加 sleep 和重试逻辑这进一步拉高了端到端耗时。第四集中式上报的波峰压力。车队回传数据基本都集中在收工后的几个小时内几辆车同时回传瞬时写请求量非常高。对象存储的写入性能曲线会瞬间被打满先到先服务后面的数据越积越多清库存的时间不断往后推。1.3 为什么选择 GooseFS 写缓存而不是改应用一开始我们也想过纯应用层改造批量合并小文件再写入对象存储或者直接用自建 Alluxio 集群做加速。但综合考虑后都放弃了。批量合并方案的问题在于合并策略和业务耦合太紧。自动驾驶数据的目录结构、样本切分方式是不断迭代的今天按时间切片明天按场景切分写批次的大小和合并粒度很难固定。一旦框架改了合并规则历史代码就要重构很不划算。自建 Alluxio 的问题则在于运维成本社区版 Alluxio 的写缓存能力相对基础部署在 Kubernetes 上时权限、元数据高可用、与对象存储的深度集成都要自己补齐落地周期会比较长。我们最终选择 GooseFS 的写缓存方案核心原因有三个它是腾讯云基于 Alluxio 开源项目演进出的数据编排引擎兼容开源生态学习成本不高写缓存实现了“数据先进入本地高速层、再异步上传远端”的机制对上层应用暴露的是标准文件语义业务代码几乎零改动它提供了统一命名空间把对象存储、本地缓存、上层计算引擎串成一条流水线自动驾驶数据处理框架可以直接像读写本地文件一样读写数据集不需要关心底层物理位置。2. GooseFS 写缓存机制拆解2.1 核心架构元数据、数据缓存、远端存储三面联动GooseFS 整个系统的核心可以概括为三个平面的协作元数据平面、数据平面、远端持久化平面。元数据平面负责文件系统的命名空间管理维护目录树、文件属性、块信息等。在 GooseFS 里这部分依赖底层内存数据库并可通过 ZooKeeper 实现 Master 的高可用选举。数据平面则由多个 Worker 组成Worker 上的内存和本地 SSD 构成分布式缓存池所有读写操作优先落在缓存池中。远端持久化平面就是底层的对象存储或 HDFS承担最终的数据落地。写缓存的关键在于“三面联动”数据写入时先写到 Worker 的缓存空间同时元数据层立即对上层应用开放该文件的可见性远端持久化任务在后台抽取缓存数据完成上传。这样后端存储的压力被“削峰”写入请求被“短路”到本集群的高速存储中应用的等待时间大幅缩短。实际操作中我们通过goosefs fs mount命令把 OSS 存储桶挂载到 GooseFS 的命名空间下而 Write Cache 的开启则在 worker 的配置文件中指定# 指定写缓存占用的内存/缓存容量上限 goosefs.worker.ramdisk.size120GB goosefs.worker.cache.store.typeSSD goosefs.worker.cache.capacity4TB # 开启异步上传数据先落缓存再异步写远端 goosefs.worker.write.cache.enabledtrue goosefs.worker.write.cache.async.upload.enabledtrue2.2 写缓存的关键路径先落本地再异步上传理解写缓存的核心只需要记住一句话写入请求不再直通远端存储而是先落到本集群的高速缓存层立刻返回写成功远端上传由后台任务异步完成。当自动驾驶数据平台调用createFile写入一个点时GooseFS Worker 会在本地缓存目录中分配一个临时块数据写入被重定向到临时块。这个临时块在写入过程中对上层应用是完全可见的因为文件系统元数据已经在 Master 侧完成注册。整个写入过程结束后Worker 会安排一个小型上传任务把临时块拆分为合适的分片顺序写入远端对象存储。这个路径带来的最直接变化是写延迟的下降。原方案中一次写入请求需要穿透网络到远端存储时间消耗在百毫秒到秒级写缓存方案下一次写入的绝大部分时间消耗在本地内存/SSD 写入上通常只有几毫秒到几十毫秒。对于海量小文件的写入场景相当于把最耗时的网络交互从用户请求主路径中彻底剥离。这里我要特别提醒一点写缓存不是“丢掉数据”而是“延迟上传”。临时块在本地缓存中会一直保留直到远端上传确认成功后才进入淘汰候选队列。如果远端上传失败Worker 会对临时块做保留并触发重试数据不会因为缓存淘汰而丢失。2.3 一致性语义与容错租约机制、TTL、冷数据淘汰写缓存方案最容易被质疑的一点就是数据一致性本地写的文件远端对象存储还没上传完成别人能读到吗能读到的是缓存里的数据吗GooseFS 的处理逻辑是对于同一个命名空间内的访问先读缓存。所有写过缓存且尚未被淘汰的块在上层应用中表现为“存在且最新”因为数据就在本地缓存中。远端持久化是一个异步过程对象存储只是最终存储位置不参与实时读语义。这种“写后本地读可用”的语义正好适合自动驾驶数据流水线采集、清洗、标注都在同一个数据平台内完成写入即消费。为保证系统容错GooseFS 还依赖租约机制防止 Master 与 Worker 之间的会话异常导致的数据错乱。Worker 会周期性向 Master 上报心跳和缓存块状态一旦租约过期Master 会把该 Worker 负责的块标记为不可用等待 Worker 恢复后重新校验。对于缓存块本身系统通过 TTL生存时间管理本地缓存的生命周期同时按 LRU最近最少使用策略淘汰冷数据。被淘汰的块如果尚未完成远端上传会先触发一次紧急上传优先级最高数据不落地不淘汰最大程度避免数据丢失。注示意图要表达的基本路径就是“写入先到 Worker 缓存成功后对应用可见后台任务再异步将数据上传到远端存储桶”。实际部署时可以自行脑补。2.4 写缓存的性能原理合并写、随机写转顺序写、并发通道写缓存的性能提升并不只是“把延时转移”这么简单它还在物理层面改变了数据写入的模式。第一是随机写转顺序写。自动驾驶数据中小文件多但目录组织往往按采集时间、传感器类型建立。如果直写对象存储每个小文件都是一次独立请求物理上缺乏连续性。通过写缓存后台定时的异步任务可以把同一目录下大量的小文件按顺序打包上传一次请求写一批数据显著提升了写入效率。第二是并发通道复用。Worker 缓存是有多块本地磁盘的多个上传任务可以并行运行。我们在配置里将上传并发数设置为 CPU 核数的一半这样既能充分利用带宽又不至于抢走训练任务的资源。第三是写入请求的低延迟反馈。自动驾驶平台要做的校验、抽帧、标注作业很多依赖“文件写入后立即可读”这个特性。写缓存让整套流水线可以连续运转不必等待远端同步各环节之间的等待时间被压缩。3. 自动驾驶场景的方案设计3.1 缓存集群怎么部署独立组还是混部自动驾驶数据处理平台通常由三块任务组成数据回传作业、清洗抽帧作业、训练样本导出作业。这三类任务对缓存的需求并不相同部署方式也需要区别对待。我们当前生产环境采用“独立缓存集群 混部 Worker”的混合模式独立的 GooseFS Master 集群负责元数据和全局命名空间共 3 个节点通过 ZooKeeper 选主形成一主两备的高可用形态。缓存 Worker 一部分与计算任务混部也就是直接部署在自动驾驶预处理任务的机器上数据写入本地盘然后异步上抛。另有一部分 Worker 独立部署主要承担热点数据集的常驻缓存供训练侧反复读取。这种混合部署的原因很有意思混部节点尽量减少网络转发写入性能最好独立节点牺牲了一点写性能换来了缓存队列的稳定性避免计算任务的波动影响上传速度。两者通过同一个命名空间对外提供服务上层平台不需要区分某个文件到底存在哪类 Worker 上。3.2 内存/SSD/远端容量规划写缓存集群的容量规划不是拍脑袋定的我们根据数据量和写入周期做了简单计算。自动驾驶数据平台上单日新增原始数据约 20TB清洗后有效数据约 8TB。回传高峰期集中在每日 18:00 至次日 2:00大约 8 小时。考虑到上传带宽瓶颈和对象存储的配额我们并不要求所有数据在回传当天完成远端上传而是允许数据在缓存中滞留最多 48 小时。因此写缓存容量至少需要覆盖单日新增数据再乘以滞留系数20TB × 2 40TB。考虑到 Worker 磁盘阵列的实际可用率约 80%我们配置了 50TB 的 SSD 缓存池内存缓存则按读取活跃数据集的头部大小来规划约 5TB。远端上传带宽方面按照 48 小时覆盖 20TB 的要求平均上传带宽只需约 480Mbps这个指标对机房内部带宽来说非常宽裕。提示容量规划要给波动留余量。我们曾经因为“按均值配缓存”遇到一天同时回来 6 辆车的大批次数据时缓存池直接打满导致上传任务阻塞。现在的做法是容量按峰值日数据的 2.5 倍配置同时保留 30% 的紧急淘汰预留空间。3.3 接入方式CSI 挂载与 POSIX 读写自动驾驶平台的应用不是都能改造成“GooseFS 原生客户端”的。尤其是一些老旧的 Python 数据回传脚本、基于 C 的下游处理工具直接集成 SDK 的成本太高。我们最终采用 Kubernetes CSI 插件的方式把 GooseFS 以 Volume 的形式挂载进 Pod对应用暴露标准的 POSIX 文件接口。这样一来数据回传脚本只需要维护一个挂载路径比如/data/goosefs/rawdata/2024-06-20写文件的方式和写本地盘一模一样。平台的调度系统、抽帧任务、标注工具链的代码几乎零改动只需要把路径前缀替换为挂载路径。CSI 挂载的 YAML 片段类似这样apiVersion: v1 kind: Pod metadata: name:>
返回列表