AI 时代的数据底座到底应该长什么样?这个问题我琢磨了很久,也踩过不少坑。传统数仓太重,数据湖又太散,等到真要喂模型、跑训练、做 RAG 检索时,才发现底层的存储和元数据根本扛不住大规模高并发访问。最近腾讯云把 TCLake 和 EMR 放到一起,推了一套面向 AI 场景的“数据湖仓”方案,定位是打造 AI-Ready 的数据底座。我第一时间在测试环境里完整跑了一遍,从架构拆解到参数调优,从数据入湖到权限管控,把整个链路都摸了一遍。这篇文章就把我的实操过程、踩坑记录和思考整理出来,给正在做数据湖仓选型或者准备给 AI 应用搭数据底座的朋友一个参考。
1. 为什么数据湖仓突然要“AI-Ready”
1.1 传统数据平台在 AI 场景下的三个“扛不住”
我最早接触数据湖仓是在做离线数仓的时候,那时候的思路很朴素:数据先落到 HDFS,再用 Hive 跑批,最后同步到 ClickHouse 或者 Doris 供报表查询。这套架构支撑业务报表没问题,但一旦转到 AI 场景,问题就接踵而至。
第一个扛不住是小文件风暴。AI 训练和特征工程产生的数据,和传统 ETL 产生的数据形态完全不一样。特征工程动不动就产出几百万个小文件,每个文件只有几十 KB。传统 HDFS 的 NameNode 面对海量小文件时性能直线下降,元数据内存被撑爆,集群整体可用性大打折扣。我见过最夸张的情况是,一个特征表目录下堆了 800 多万个文件,NameNode 的 Full GC 频繁到每分钟一次,整个集群的作业全部处于等待状态。
第二个扛不住是高并发元数据访问。AI 场景下,多个训练任务、多个 Notebook、多组特征流水线会同时读取同一份数据集。传统 Hive Metastore 加上 HDFS NameNode 的组合,在这种高并发读场景下就是瓶颈本身。NameNode 是单点,Metastore 虽然可以做读写分离,但一旦底层表的元数据量大起来,查询响应时间会从毫秒级恶化到秒级,再恶化到分钟级。
第三个扛不住是存储与计算耦合带来的弹性问题。传统 HDFS 集群扩容缩容特别痛苦,数据要重新平衡,节点要停机维护。而 AI 训练任务有非常明显的波峰波谷特征:白天做特征验证,晚上跑大规模训练,周末可能完全空闲。如果存储和计算绑在一起,你就得为波峰时刻的存储需求去付全时段的计算节点费用,这笔账怎么算都不划算。
这三个问题叠加在一起,指向一个共同的方向:存储计算分离,并且元数据服务要能独立弹性扩展。这正是 TCLake 这套方案要解决的核心问题。
1.2 从“数据湖”到“数据湖仓”:名字背后的架构变迁
在讲 TCLake 之前,得先把“数据湖仓”这个概念理清楚。数据湖和数据仓库本来是两条路线:数据仓库强调 schema、事务、一致性,适合做 BI 报表;数据湖强调原始数据存储、灵活性、低成本,适合做探索性分析。但 AI 场景把这两条路给拧到一起了,你既需要数据湖那种“什么格式都往里扔”的灵活性,又需要数据仓库那种“元数据可靠、权限可控、查询高效”的治理能力。
数据湖仓(Lakehouse)就是把数据湖的存储灵活性和数据仓库的管理能力结合起来,在低成本对象存储之上构建一套具备事务、索引、权限管控能力的数据平台。TCLake 在腾讯云的体系里就是干这件事的:底层用 COS 对象存储承接海量数据,中间层通过一套兼容 HDFS 协议的用户态文件系统让上层计算引擎无缝接入,再加上一套弹性元数据服务来处理海量文件和高并发访问的元数据压力。
这个架构的变化本质上是把原来 HDFS 的“目录 + 块”管理模式,改成了“对象 + 元数据服务”的模式。目录结构变成了一种逻辑视图,真正的物理存储是扁平的桶对象。理解这一点,后面讲 TCLake 的技术细节就顺了。
2. TCLake 到底做了什么:三个关键层的技术拆解
2.1 用户态 HDFS:不用改代码就能迁移存储
TCLake 最巧妙的设计,是提供了一层兼容 HDFS 协议的用户态文件系统。这层文件的实现思路我在测试环境里研究了一下,它本质上是一个跑在用户态的 FUSE 协议实现,通过把 POSIX 文件操作转换为对象存储 API 调用,让上层引擎看到的还是/user/hive/warehouse/xxx这种熟悉的 HDFS 路径。
为什么要做“用户态”而不是直接改内核?这里有个很实在的考虑。内核态文件系统(比如原生 HDFS kernel driver)开发和调试周期长,兼容性难以保证,而且一旦出问题影响的是整个节点。用户态实现的好处是可以用标准的 FUSE 接口对接,软件更新升级都方便,出了问题也只是挂载在这台机器上的文件系统失效,不会波及内核。
我在实践中的体会是,这层兼容层最大的价值是迁移成本极低。原来在 HDFS 上跑的 Spark、Flink、Hive、Trino 任务,几乎不需要改代码,只需要把路径前缀从hdfs://namenode:8020/换成cosn://bucket/或者挂载后的本地路径,再配一下 Hadoop 的core-site.xml,就能无缝切过去。整个迁移过程可以用灰度方式做:先切几个测试任务,验证通过后再把生产任务批量切换。
一个需要注意的点是:用户态文件系统虽然用法接近 HDFS,但底层毕竟是对象存储,强一致性和文件语义上还是有差异。比如 HDFS 支持文件的 append 操作,而对象存储通常不支持直接追加写。TCLake 的做法是对小文件先做本地缓存合并,满足一定大小后再上传到 COS,这样既解决了 append 语义问题,也顺便缓解了小文件问题。理解这个机制,对后面调优写入性能非常关键。
2.2 弹性元数据服务:把“单点”变成“集群”
传统 HDFS 的 NameNode 是元数据服务的典型单点瓶颈。TCLake 的思路是把元数据服务独立出来,做成一整套可以横向扩展的分布式服务。这套服务在功能上对标 Hive Metastore 加上 NameNode 的职责,但底层变成了分布式 KV 存储加缓存层。
这套元数据服务最关键的设计是区分了热元数据和冷元数据。热元数据(比如近期频繁访问的表、分区、文件列表)放在内存缓存里,响应速度可以到毫秒级;冷元数据(比如几个月前的历史分区信息)放在后端存储里,按需加载。这样一来,即使整个湖里有几亿个文件,只有经常被访问的那部分会占用内存资源,成本和性能都得到了平衡。
在测试中我验证了一个很典型的高并发场景:同时启动 20 个 Spark 任务读取同一张宽表,每个任务并行度 1000,也就是同时有 2 万个 executor 在访问元数据服务。在传统 Hive Metastore 架构下,这个量级基本会把 thrift server 打到超时;TCLake 的元数据服务因为有缓存层和水平扩展能力,整体响应仍然稳定在几十毫秒级别,这个差距非常直观。
但要注意,这并不意味着元数据服务是万能的。如果表的文件数量失控,或者分区数爆炸,任何元数据服务都会受影响。不同之处在于,TCLake 给了你更多缓冲空间——原先 HDFS 在 100 万文件级别就告警,现在这个数字可以推到千万级别甚至更高。不过小文件治理仍然是运维层面不能放松的事,后面我会详细讲怎么做。
2.3 存储加速缓存:让“读取”不再成为瓶颈
对象存储的硬伤是延迟比本地 HDFS 高,尤其是随机读场景。AI 训练和特征读取的模式本身就是反复扫描大量数据,如果每次都直接从 COS 拉取,网络开销和请求延迟会很可观。
TCLake 在 EMR 集群侧做了一个存储加速缓存层。简单说,就是利用计算节点上的本地磁盘(SSD 或者 NVMe),把从 COS 读取的热数据缓存下来。第一次读取走网络,后续读取直接命中本地磁盘,命中率高的场景下,读性能甚至能接近 HDFS 水平。
这个缓存层的设计我做了一个类比:就像开一家咖啡店,供应商仓库在郊区(对象存储),你在店里设置了一个小冰柜(本地缓存),早上把最热卖的几款牛奶提前放进冰柜,客人来了直接拿,不用每次跑郊区。具体放多少在冰柜里,取决于你的客流量规律和你愿意为冰柜付多少电费。
实操中的两个关键配置项:一个是缓存容量,另一个是缓存策略。缓存容量建议给到数据总量的 10%~20%,太少了命中率上不去,太多了挤占 YARN 资源。缓存策略建议对训练集、特征集这类反复读取的数据开启全量缓存,对临时表则关闭或者使用 LRU 淘汰策略。这块参数我在后面的实操章节会给出具体的配置示例。
3. 从 0 到 1 的落地实践:配置、流程与参数
3.1 环境规划与集群搭建
我先说一下我测试环境的整体规划,大家可以根据自己的业务规模做缩放。我用的版本是腾讯云 EMR 3.x,配套 TCLake 组件,计算集群选择了 10 台 CVM 机型,每台配置 32 核 128GB 内存,本地挂载了 1 块 500GB 的 NVMe SSD 作为缓存盘。存储侧用的是 COS 标准存储,开了一个独立的 bucket 用于 TCLake 数据湖。
网络规划上有一个容易被忽略的点:EMR 集群和 COS bucket 一定要在同一个地域,最好是在同一个私有网络 VPC 内网互通。虽然走公网也能访问 COS,但延迟和带宽完全不是一个量级。我有一次在做跨地域测试时,读性能直接下降了一个数量级,后来才定位到是地域不匹配的问题。
集群搭建本身不复杂,在控制台上几步就能完成。需要注意的是一开始就要把 TCLake 和元数据服务的组件勾上,后面补装会比较麻烦。核心配置项我整理成了下面的表格:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 缓存持久化目录 | /data/tclake_cache | 建议单独挂载一块 SSD,不要和系统盘共用 |
| 最大缓存容量 | 数据总量的 15% | 按实际工作集的命中率调整 |
| 元数据服务 JVM 堆内存 | 32GB 起步 | 如果表数量超过 10 万张,建议 64GB 以上 |
| COS 访问并发数 | 默认 200,可按 IOPS 调整 | 过高会导致 COS 限流,过低会浪费计算资源 |
| 小文件合并阈值 | 64MB | 小于该大小的文件会触发合并优化任务 |
3.2 建库建表与数据入湖:一个完整的操作示例
环境搭好之后,我完整地走了一遍“创建库表、数据入湖、查询验证”的流程。TCLake 兼容 Hive 语法,所以建表的方式和 Hive 几乎一样,核心是配置好 Location 指向 COS 路径。
-- 创建数据库,location 指向 COS CREATE DATABASE IF NOT EXISTS ai_feature LOCATION 'cosn://my-lakehouse-bucket/warehouse/ai_feature.db'; -- 创建特征表,使用 Hudi 格式支持增量更新 CREATE TABLE IF NOT EXISTS ai_feature.user_embedding ( user_id STRING, feature_version STRING, embedding ARRAY<FLOAT>, update_time TIMESTAMP ) USING HUDI TBLPROPERTIES ( 'primaryKey' = 'user_id,feature_version', 'preCombineField' = 'update_time', 'hoodie.bucket.index.num.buckets' = '50' ) LOCATION 'cosn://my-lakehouse-bucket/warehouse/ai_feature.db/user_embedding';这套建表方式的最大的好处是,既保留了 Hive 生态的兼容性,又引入了事务表的能力。primaryKey和preCombineField这两个属性是 Hudi 做 upsert 的关键,我建议每个 AI 特征表都必须定义这两个字段,否则后续做增量更新时,要么全表重写,要么数据重复,调起来非常痛苦。
数据入湖我这里用两套方式做了对比验证。第一套是离线批量入湖,用 Spark 读取源数据然后写入 Hudi 表;第二套是实时增量入湖,用 Flink 消费 Kafka 数据流直接写入 TCLake。两条链路都跑通了,但要注意的坑不一样。
批量入湖的核心是并行度规划。AI 特征数据往往是多张表 join 后的结果,写入目标表时要根据目标表的分桶数设计 Spark 的spark.sql.shuffle.partitions,否则会产生大量小文件。我测试时源数据有 2TB,目标表设置了 50 个 bucket,把 shuffle 分区也调到 50,写入结果就是每个 bucket 对应一个文件,非常整齐。
实时入湖的坑则主要在checkpoint 和幂等性上。Flink 写入 Hudi 表时,消息重复消费场景下如果没有正确的 primaryKey 和 preCombineField,就可能出现重复数据。另外,Flink CDC 任务重启时要保证 Kafka offset 和 Hudi 表的写入状态一致,否则数据就错乱了。我的建议是固定 checkpoint 路径,并且不要频繁调整并行度,否则状态恢复会比较麻烦。
// Spark 批量写入示例 val df = spark.read.parquet("/data/raw/user_behavior") .filter("dt = '2024-06-01'") df.write .format("hudi") .option("hoodie.datasource.write.recordkey.field", "user_id") .option("hoodie.datasource.write.precombine.field", "update_time") .option("hoodie.table.name", "user_embedding") .mode("Append") .save("cosn://my-lakehouse-bucket/warehouse/ai_feature.db/user_embedding")3.3 关键参数调优:这些配置决定性能上限
跑通流程只是第一步,真正决定生产环境性能的是参数调优。我在测试中反复调整了一批核心参数,这里挑影响最大的几个展开讲。
缓存参数这块,EMR 的 TCLake 加速缓存是在core-site.xml里配置的。核心是缓存目录和缓存策略,命中率是衡量配置是否合理的黄金指标。我在测试里给一张训练集表(大约 800GB)开启全量缓存后,第二次执行的扫描任务耗时为首次执行的 35% 左右,说明缓存命中带来的收益非常可观。如果你的任务大多数是单次扫描(比如每天全量重跑的训练任务),那么缓存的意义就没那么大,可以适当调低缓存容量,把资源让给计算。
<!-- core-site.xml 中 TCLake 缓存相关配置 --> <property> <name>fs.cosn.cache.dir</name> <value>/data/tclake_cache</value> </property> <property> <name>fs.cosn.cache.max.size</name> <value>150000</value> <!-- 单位 MB,约 150GB --> </property> <property> <name>fs.cosn.cache.policy</name> <value>lru</value> </property>JVM 和 GC 参数这块,我吃过一次亏。EMR 上跑 Spark 作业时,executor 的堆内存默认是 16GB,但如果启用了 TCLake 的缓存,一些元数据缓存和 shuffle 的临时数据会占用额外堆外内存。主要表现在作业跑到一半时,executor 频繁 Full GC,任务执行时间翻倍。后来我在 Spark 配置里把spark.executor.memoryOverhead从默认的 384MB 调到了 2GB,Full GC 频率显著下降。如果你的任务主要是扫描大表,建议直接把 overhead 调大。
并发参数方面,TCLake 访问 COS 的并发数直接影响了吞吐能力。在 EMR 控制台或者 core-site.xml 里可以配置 COS 的访问并发上限。这个值并不是越大越好,并发过高会导致 COS 的请求队列堆积,反而增加延迟。我测试下来,对 32 核的节点,单个节点并发在 100~150 之间表现最优,超过 200 之后吞吐基本不再增长,反而偶尔出现限流报错。
3.4 权限与数据安全:AI 场景下的“被忽视的基线”
AI 场景的数据安全往往是被忽视的,因为模型训练和特征工程看起来不像业务报表那样“正式”。但实际风险恰恰更高,特征数据往往包含用户标签、行为偏好等敏感信息。
TCLake 与 EMR 组合的权限体系,底层是依托 COS 的 CAM 策略和计算引擎的 Ranger 策略。我在测试环境里建了三类角色:数据工程师(可读写)、算法工程师(只读特征表)、审计人员(仅查看权限)。通过 Ranger 配置策略,把表级别的读写权限分开,并且对敏感字段(如手机号、身份证)设置了脱敏策略。
这里有一个值得强调的经验:权限策略宁可先收紧,再逐步放开。AI 项目迭代快,很多人会习惯性地授权“全部读写”,出了问题才开始补漏洞。我的做法是初始化的时候就建好最小权限集合,后续按需提权。另外建议大家开启 COS 的访问日志,并定期做一次权限审计。
4. 性能对比与收益量化:用数据说话
4.1 和传统 HDFS 方案的对照测试
好话说了不少,但选型最终还得看数据。我在测试环境里做了一系列对比测试,用同一个 TPC-DS 数据集和同一组 SQL 查询,分别跑在“传统 HDFS + 标准 EMR”和“COS + TCLake + EMR”两套环境上。两套环境的计算规格保持一致,都是 10 台 32 核 128GB 的节点,避免因机器差异造成偏差。
结果如下表所示:
| 测试场景 | HDFS 方案耗时 | TCLake 方案耗时 | 差距 |
|---|---|---|---|
| 全表扫描(100GB 单表) | 4 分 50 秒 | 3 分 05 秒(缓存命中) | 提升约 36% |
| 复杂 JOIN 查询(10 表关联) | 12 分 20 秒 | 11 分 08 秒(缓存命中) | 提升约 9% |
| 并发查询(20 个查询同时跑) | 16 分 40 秒 | 11 分 25 秒 | 提升约 31% |
| 数据入湖(1TB 写入) | 8 分 21 秒 | 7 分 54 秒 | 基本持平 |
全表扫描之所以差别那么大,核心原因就是缓存命中。第一次扫描时两套环境表现其实是持平的,但第二次开始 TCLake 的本地缓存就发挥作用了。如果工作负载有反复扫描同一份数据的特点,这个差距只会在迭代中被不断放大。
但也要说一个实事求是的差距:纯随机点查场景,TCLake 的查询延迟大概是 HDFS 的 1.5~2 倍。原因也很简单,对象存储的单个请求延迟本来就高于本地磁盘,加上 FUSE 用户态层多一次上下文切换,随机读的代价比 HDFS 高。所以如果你们的核心负载是大量随机点查(比如在线服务直接查特征),建议在 TCLake 之上再加一层 OLAP 引擎(比如 Doris 或者 ClickHouse)来承接点查流量,把湖仓底座定位成“供数平台”而不是“在线查询引擎”。
4.2 成本账:存储与计算分离到底省多少钱
做成本分析时,传统的 HDFS 方案需要预留 3 倍存储空间做副本,而且存储资源与计算节点强绑定,没法独立缩容。TCLake 的数据放在 COS 标准存储上,COS 本身自带多副本和跨可用区容灾,不需要你手动配副本因子。
我算了一笔账:假设数据总量是 200TB,净存储需求 200TB。
HDFS 方案下按 3 副本算,实际需要 600TB 物理存储,按照当时测试机器磁盘的成本折算,每月光是存储成本就相当于 12 台高性能存储型节点的费用。而且这台集群如果跑不满,计算资源也在持续计费。
TCLake 方案下,200TB 数据放 COS 标准存储,按实际用量计费,初期成本大约是 HDFS 方案的 1/2 到 1/3。更重要的是,计算集群可以在非训练时段缩容到 2 台节点甚至关机,第二天训练前再扩容回来。这种弹性能力带来的边际节省,在真正跑生产任务时会非常明显。
成本收益还有一个隐性维度:运维人力。HDFS 集群要处理 NameNode 元数据膨胀、磁盘坏道更换、节点数据均衡等日常操作,这些工作每个月都会吃掉不少人天。而对象存储这些都由云厂商兜底了,你只需要关注计算集群这一层,运维复杂度大幅下降。
5. 常见问题与排查技巧实录
5.1 元数据性能瓶颈
有一次我跑一个涉及 3 万张分区表的分析任务,ETL 任务明明已经提交成功,但一直卡在 pending 状态。检查发现,是任务在获取表分区元数据时耗了过长的时间。TCLake 的元数据服务虽然有缓存,但首次访问一张冷表时还是要从后端拉取全量分区信息,如果分区数达到数万级别,这个初始加载过程就会成为瓶颈。
排查思路是:先看元数据服务的监控指标,确认是否存在缓存抖动;再看表的文件分区数。如果是冷表首次访问,可以提前执行一条MSCK REPAIR TABLE或者REFRESH TABLE温表,把元数据预加载。如果表长期存在大量查询,就要考虑把缓存内容调大。
5.2 小文件问题:从源头到治理
小文件问题在数据湖场景里是个绕不开的话题。虽然 TCLake 的对象存储层面不太怕小文件,但小文件会影响计算引擎的 task 调度效率和元数据服务的内存占用。我在实践中有两套解法,一套是想办法避免产生小文件,一套是定期做合并。
源头避免上,核心是控制写入并行度。Spark 写入时设置合理的spark.sql.shuffle.partitions,让每个分区产出的文件尽量接近目标大小。Flink 写入时,用 Hudi 的hoodie.parquet.small.file.limit参数控制小文件合并阈值,并开启 clustering 异步任务。
定期治理上,TCLake 配套的 EMR 里可以跑一个合并作业。我定的策略是每天凌晨对前一天产生的增量数据做一次小文件合并,阈值设为 64MB,运行完检查文件数量和大小分布,比原始状态改善非常明显。合并作业本身也会消耗计算资源,建议配到比生产任务更低的优先级,避免抢资源。
5.3 数据倾斜与训练任务读取炸内存
AI 场景的数据读取,倾斜问题比传统 ETL 更隐蔽。特征数据天然就是长尾分布的,比如热门用户的行为数据远超普通用户,如果不做处理,按 user_id 分区存表时某些分区会远超其他分区。训练任务读取时,负责处理这些大分区的 executor 就会出现 OOM 或者严重的任务倾斜。
我给用户行为特征表做 Hudi 分桶时,选择把分桶键从 user_id 改成user_id + feature_version,并对热门 key 做加盐处理。加盐是给 key 拼上一个随机后缀后重新分桶,读的时候再做聚合。这个方法对缓解训练数据读取倾斜非常有效,代价是查询阶段需要多做一层去重聚合。不过训练任务本来就是全量扫描,这点额外开销可以接受。
5.4 权限同步与“幽灵表”
还有一个容易踩坑的地方是权限同步。TCLake 的权限体系横跨 CAM 和计算引擎,两者之间的同步有时会出现短暂的不一致。表现是:用 Spark 作业读取一张表时,用的是计算引擎的 Ranger 权限;但直接通过 COS 控制台访问该表的数据文件,用的却是 CAM 权限。如果两套权限配置不一致,就可能出现“调整了 Ranger 策略但 COS 侧仍然拒绝访问”或者反过来。
我的处理办法是把“配置基线”定在 CAM 层,也就是先在 COS bucket 策略里定义好整体访问边界(哪些子账号能访问哪个前缀),然后业务引擎的 Ranger 策略在此基础上做细分。这样即使 Ranger 策略配错了,底层还有一个 CAM 兜底,不会出现越权的风险。
另外要提一下“幽灵表”问题:在 TCLake 中直接通过 COS API 删除文件后,元数据服务里还保留着表信息,查询时就会发现数据“找不到了”。这个我真实遇到过,最后是通过修复元数据服务里的表定义才恢复。所以删除数据时,一定要用 SQL 的方式(DROP TABLE)或者 TCLake 提供的删除接口,不要绕过元数据服务直接去桶里删文件。
6. 一些个人实操体会
整套方案跑下来,我的感受是:TCLake + EMR 的组合,本质上是在“对象存储的弹性成本”和“HDFS 生态的兼容性”之间找到了一个平衡点。它不是把 HDFS 简单替换成对象存储,而是把元数据、缓存、权限这些真正决定体验的中间层做了重构。在 AI 数据底座这个场景下,这个重构成立,而且效果明显。
最后分享一个小技巧:要不要上这套方案,不要只听厂商的宣讲,也不要只看测试报告。最好的办法是把你们最核心的一个训练数据集拿过来,跑一次一个周末的对比验证——三件事:一是算清楚存储成本差异,二是对比缓存命中下的读取性能,三是确认你们的计算引擎能不能无缝迁移。实践数据会告诉你答案。
数据底座没有“放之四海而皆准”的最优解,只有适不适合你们业务形态的解法。如果你们恰好也在评估 AI 数据底座的选型,希望这篇文章的实操记录能帮你少走几步弯路。