
先说一个场景。某个周三上午vivo大数据平台的值班大屏上核心集群的DataNode磁盘使用率曲线比往年提前三周触达了85%水位线而下一批硬盘扩容的到货周期是45天。这是所有HDFS集群都会遇到的选择题要么继续堆硬件扛数据增长要么从存储机制上做文章。我们选择了后者——把HDFS ECErasure Coding纠删码从特性预览推向了大规模生产环境。这篇记录就是我们做这件事的完整复盘包括为什么做、怎么做、踩了哪些坑以及最后到底省下了什么。如果你正在为存储成本发愁或者已经在评估HDFS EC但不确定它适不适合你的业务这篇文章应该能帮你少走不少弯路。我会把方案选型、目录策略、数据迁移、故障恢复这些环节的考量都摊开讲重点是大规模落地后那些文档里不会写的真实问题。1. 为什么必须动HDFS的冗余结构成本账与数据画像1.1 三年翻一倍的数据和越来越贵的扩容vivo的大数据平台主要承载离线数仓、用户行为日志、业务报表和部分特征工程任务。这套体系跑了好几年整体规模早已是PB级别起步。我们当时面临一个很朴素的困境数据量的年增速稳定地保持在50%以上而硬件采购周期和机房机架资源始终是瓶颈。按照当时的速度推算再过一年即便机架全部塞满也扛不住新的数据写入。那时候HDFS默认的冗余机制是三副本。一份数据写入后NameNode会调度DataNode在本地机架放一个副本在另一个机架放两个副本用来应对单机、单机架故障。三副本的好处是可靠性极高、读取时可以就近选择副本坏一块盘直接复制一份就能恢复。但它的代价也极其直观1TB逻辑数据在物理上要占用3TB存储空间。换句话说我们辛辛苦苦扩容进来的硬盘有三分之二其实是在给冗余机制买单。这还不是最难受的。更难受的是这3倍冗余是“一刀切”的——无论这块数据是每天被全量扫描的活跃特征表还是三年前归档后几乎没人再碰的日志明细都享受同样的冗余待遇。1.2 数据画像告诉我们冷数据占比高得吓人在决定引入EC之前我们花了两周时间做了一次系统的数据画像分析。结果其实不意外但量化之后还是很震撼整个集群约70%的存量数据是最近90天内没有任何写入、访问频率也极低的“准冷数据”。这些数据包括历史订单明细、老的埋点日志、已过期的报表分区它们的特点是写入后基本不再修改属于“只读”数据访问模式以全表扫描、ETL重跑、审计抽查为主文件普遍是大文件单个文件几百MB甚至几GB起步对延迟不敏感跑批任务晚几秒结束完全能接受。这个画像几乎就是为EC量身定制的。EC可以把冗余比例从200%压到50%甚至33%代价是牺牲一部分写入/读取性能、CPU开销增加、以及对一些操作的限制。如果我们只对冷数据做EC热数据继续用三副本就能在不动硬件的前提下释放出大量物理空间。这一步想清楚之后整个项目的目标就变得非常聚焦了在不影响在线业务的前提下把冷数据的副本率降下来。我们当时的判断是EC不是要替代三副本而是和三副本形成互补。三副本留给热数据和小文件EC留给冷数据和大文件。这个“冷热分层”的思路决定了后面所有方案的方向。2. EC的基本功纠删码、条带化与HDFS里的实现逻辑2.1 从三副本到纠删码空间和可靠性的再平衡要说清楚EC为什么能省空间得先理解纠删码最核心的数学逻辑。咱们不推公式用一个生活化的类比假设你有6箱行李要托运怕路上丢箱子。三副本的做法是每箱行李复印两份一共18箱丢任意2箱都能找原件补回来。EC的做法是先把6箱行李拆成数据块A1~A6再额外打包出3箱“校验行李”P1~P3这9箱行李里任意丢3箱回家后都能用剩下的6箱把原始6箱行李完整还原。这里的关键是三副本把每份数据复制3份冗余数据是原始数据的2倍而EC只额外产生校验数据冗余比例可以按需设计。HDFS里最常用的RS-6-3策略就是6个数据块加3个校验块额外产生的校验数据是原始数据的50%。也就是说一个文件的物理占用从3倍降到了1.5倍空间利用率一下子提升了近一倍。可靠性方面也不用担心。RS-6-3允许任意3个数据块同时损坏而不丢失数据这和三副本容忍3个副本损坏的能力在数学上是等价的。换句话说在同等容错能力下我们只需要原来一半多一点的空间。2.2 HDFS里的EC长什么样stripe、cell与块放置原理归原理真正落地到HDFS里EC文件有一套完全不同于副本文件的组织方式。这里要引入两个概念条带stripe和条带单元cell。以RS-6-3-1024k为例。这里的1024k是cell size也就是每个条带单元的大小。写入时客户端会把数据流切成一个个1024KB的cell每6个cell组成一个stripe然后通过编码器计算出3个校验cell。一个stripe总共有9个cell它们分别对应9个block并且会被分配到不同的DataNode上。这9个分布在多节点上的block共同构成一个完整的条带化EC文件。具体到块放置策略EC和副本模式有明显区别。三副本是“meta块在哪个机架、副本如何分布”由BlockPlacementPolicy决定EC同样有放置策略默认会尽量把同一个stripe的cell分散到不同机和不同机架以避免机架级故障导致同一stripe内多个cell同时丢失。在实际部署时我们看到的布局是数据块和校验块会交错分布在多个机架的多个节点上每个节点只保存其中一个cell。这种条带化设计的直接后果是任何一个cell的损坏都可能需要从其他节点读取整条stripe的数据才能恢复。这为后面我们要踩的“重建风暴”埋下了伏笔先按下不表。2.3 EC天然不支持的写操作原因其实很简单HDFS文档里明确写着EC文件不支持append、truncate操作。很多人一开始不理解觉得“为什么副本文件可以追加、EC就不行”道理其实很直观。假设你在一个stripe的某一个cell后面追加了几百字节这个stripe里6个数据块的内容就发生了变化那3个校验块对应的编码结果也得重新计算。而且要命的是校验块是跨节点分布的要更新校验块就必须把同一个stripe的其他数据块全部拉回来重新编码。对于一次“追加几KB”的操作来说这个代价是灾难性的。所以HDFS选择了彻底禁止。EC文件一旦写入完成并close就是只读的不能再追加也不能再截断。这个限制我们在初期评估时觉得“还好”但后续落地时它真的咬了我们一口——具体细节放在第5章的踩坑部分。3. 方案选型RS-6-3-1024k是怎么被我们定下来的3.1 候选策略与评估维度HDFS EC支持的策略不止一种除了RS-6-3还有RS-3-2、RS-10-4、XOR等。我们当时认真对比过的主流向策略如下策略数据块校验块空间利用率容错能力适用场景三副本12个副本33%容忍2个副本同时故障热数据、小文件、在线查询RS-3-23260%容忍2块同时故障中小文件、较热数据RS-6-36366.7%容忍3块同时故障大文件、冷数据、全表扫描RS-10-410471.4%容忍4块同时故障超大数据块但编码开销高只看表格的话RS-10-4的空间利用率最高RS-6-3次之。但选型不能只看空间利用率还要看编码计算开销、故障重建代价、数据块分散难度。RS-10-4虽然节省空间更多但每个stripe要管理14个block编码矩阵更大单次重建要读取的数据块也更多对CPU和网络的消耗都更重。另一个关键点是“数据块分散难度”。RS-10-4要求一个stripe里的14个block尽量分散到14个不同节点但在一些机架规模不够大的机房里这会把block逼到少数几个机架上反而降低故障隔离能力。相比之下RS-6-3的9个block更容易在现有拓扑里做到合理分散。综合下来我们最终选择了RS-6-3-1024k。理由概括成一句话空间节省和容错能力都足够编码开销和重建代价相对可控部署复杂度在可接受范围内。3.2 基准测试我们到底测了什么选型不能拍脑袋我们在决定之前搭了一套3节点的小规模测试环境跑了大概两周的基准测试。测试内容分三块写入吞吐用TestDFSIO分别向三副本目录和EC目录写入相同大小的数据集对比写吞吐量。实测下来EC目录的写吞吐比三副本低了约30%~40%CPU占用明显上升。这个结果符合预期因为EC写入时客户端要多做编码计算数据还要切成cell分发到更多节点。读取吞吐对全文件顺序读进行测试。这里有个反直觉的发现EC文件的全量顺序读吞吐和三副本差距不大甚至在并行度足够高的时候还能略快一点。原因不难理解——三副本读一个文件通常只从一个节点拉数据而EC文件可以同时从多个节点拉取不同cell并行组装等于天然获得了并发度。随机读性能模拟查询EC文件内某几行数据。这个测试结果就不太乐观了随机读延迟比三副本高了一个数量级。原因也清楚普通副本模式下读某个位置的数据只需要定位到对应block再读而EC模式下随机读往往要拉取额外数据块做解码网络开销成倍增加。这三组测试结果直接影响了我们后续的落地策略EC只能放“顺序读为主、随机读几乎没有”的冷数据。任何需要频繁随机读的目录不管数据多冷都不能盲目EC化。3.3 更适合EC的目录画像在测试过程中我们还总结了一个“适合EC化”的目录特征清单后来成了全团队的统一判断标准文件平均大小要足够大。EC的最小管理单元是cell我们设置为1024KB如果一个stripe里塞不满6个数据块就close校验块比例会偏低空间收益不明显。我们实测下来文件小于64MB时EC化的空间节省就没那么划算了不如继续用三副本。写入后能“封板”。这个分区一旦写完未来不会再有任何数据追加。典型的就是Hive的分区表日期分区一旦跑完第二天再也不会往前一天的分区写数据。读取是全表扫描或大范围扫描。也就是ETL、报表、模型训练这类任务不要是点查、明细行级查询。据这个画像我们在集群里筛选出来几个典型冷目录比如历史明细表分区、老日志目录、已经归档的报表数据。这些目录的共性是体量大、写入后不变、定期被离线任务全量扫描。它们就是最合适的EC试点。4. 大规模落地目录策略、数据迁移与灰度节奏4.1 目录级别策略别想着给整个集群一刀切HDFS EC在Hadoop 3.x里是通过目录级策略来生效的这一点非常重要。你不需要也不应该对整个集群开启EC只需在特定目录上绑定某个EC策略这个目录下之后新写入的文件就会按照该策略进行条带化编码。常用的命令如下# 查看集群当前支持的所有EC策略 hdfs ec -listPolicies # 查看某个目录当前绑定的EC策略 hdfs ec -getPolicy -path /data/cold # 将指定目录绑定为RS-6-3-1024k策略 hdfs ec -setPolicy -path /data/cold -policy RS-6-3-1024k # 删除目录上的EC策略绑定 hdfs ec -unsetPolicy -path /data/cold在我们生产环境中策略粒度一般是“业务根目录”或“表的分区级目录”。举个例子Hive表dw_order_detailed按日期分区路径是/user/hive/warehouse/dw.db/order_detailed/dt20240101。对正在写入的昨天分区不做EC等T1任务跑完、这个分区确认不再更新后再在分区目录上执行setPolicy。这样就避免了EC不支持append带来的业务冲突。4.2 已有文件必须重写迁移别指望setPolicy一步到位这里有一个非常容易踩的坑setPolicy只对“之后新建”的文件生效已经存在的文件不会自动变成EC文件。如果你以为绑定了策略就等于已有数据被EC化了那就大错特错了。存量文件要真正获得EC的空间收益必须经历一次“重写迁移”读取旧文件内容写入目标EC目录生成符合EC条带布局的新文件然后清理旧文件。这一步是迁移动作里最重的一块也是整个项目周期里最花时间的环节。我们采用了distcp迁移 目录切换的两阶段方案。第一阶段在目标冷数据根目录下建一个版本目录把需要EC化的历史数据通过distcp拷贝过去第二阶段确认新目录数据校验一致后删掉旧目录再把原路径映射切到新目录。实际执行的distcp命令参考hadoop distcp -i \ -update \ -skipcrccheck \ -prbp \ -Ddfs.blocksize134217728 \ hdfs://namenode/data/cold_v1 \ hdfs://namenode/data/cold有几个参数要特别说明-skipcrccheck很重要。EC文件和普通副本文件的checksum计算方式不同直接按默认校验会报大量校验失败加上它能跳过这一层靠其他手段保证一致性。-update保证增量迁移时只拷贝新增或变化的文件避免每次全量重来。-prbp是保留原文件的block位置偏好可以减少跨机架的数据搬迁量。迁移期间要实时关注DataNode的IO负载。我们一开始没太注意把几十TB的冷数据一次性丢进distcp任务里结果把底层磁盘阵列的读写延迟拉高了间接影响了同机架上的在线任务。后来改成按天粒度调度迁移任务每天只在凌晨到上午的低峰窗口搬一部分数据才把影响控制在可接受范围。4.3 灰度节奏先试点再铺开最后做全量大改动最怕一上来就梭哈。我们规划了三阶段灰度第一阶段是试点只选了一个几十TB级别的历史日志目录做EC化。这一阶段的目标是验证全链路策略绑定是否正确、存量迁移是否顺利、EC写操作对线上NameNode和DataNode有无异常、跑批任务读取EC文件的兼容性如何。第二阶段是单业务线铺开把整个数仓里所有满足条件的历史分区都纳入EC同时对实时计算、报表查询等下游做全面回归。这个阶段发现了一个实时写入冲突的问题后面会专门讲。第三阶段才是全量铺开把集群里所有“冷”目录统一切到EC策略。这个时候我们的EC数据规模已经从TB级增长到了PB级流程、监控、应急预案都已经跑熟了铺开过程反而没出大乱子。这套渐进式的节奏事后复盘是被证明非常值得的。EC一旦大规模铺开出问题时的回滚成本极高。灰度期间我们积累下来的故障预案和性能基线是后面从容应对生产问题的基础。5. 生产环境里的坑从append异常到重建风暴5.1 实时写入任务在EC目录下直接报错第一个让我们措手不及的坑来自一个看起来“无害”的目录。有一个实时特征流任务每天都会把当天累积的新数据以追加小文件的方式写进HDFS然后再由离线任务读取合并。当初评估时我们只注意到“这个目录是冷数据”的标签却忘了它还有一个“会持续接收写入”的属性。EC化之后某个时间点起这个实时任务开始频繁抛出IOException: Append is not supported for striped file。一开始大家以为是集群抖动排查了半天才发现EC文件根本不支持append。虽然日志文件本身很小但对实时链路来说这个异常直接导致数据产出延迟影响到了下游的报表。这个问题的处理方式有两个层面。一个是战术层面的把实时写入目录从EC策略中摘除回滚成三副本。另一个是战略层面的最终我们形成了一条规定——所有目录必须先过“是否还有持续写入”这道检查才能允许绑定EC策略。写入一旦停止且分区封板再执行EC化。凡是“边写边EC”的目录一律不放行。5.2 随机读变慢问题出在条带化第二个坑用一句话可以概括看起来是冷数据的目录不代表完全没有随机读。我们有一个归档的运营报表目录平时主要是离线任务全量扫描看起来很适合EC。EC化之后全量扫描任务都正常但有一天业务方反馈说“有个例行查询从5秒变成了40秒”。排查之后发现这个“例行查询”其实会对归档报表做单日维度过滤本质上是随机定位到某个cell再去读取。在副本模式下NameNode可以直接告诉你目标数据在哪个节点哪个block读一个块就够了而在EC模式下要取出某个cell的数据至少得从同一条带的其他节点拉取足够的block来做解码网络的往返次数从1次变成了N次延迟自然就上去了。这个问题的教训是判断目录适不适合EC不能只看“最近没写入、没全量扫”这种粗粒度指标还得看有没有细粒度点查的需求。我们把这条例也加进了目录准入规则凡是下游有SQL需要按某个维度精确过滤到特定行的一律不EC化。5.3 一次机架故障引发的重建风暴这一条是我们踩得最狠、也最值得说的一个坑。某次机房升级一个机架上的数台DataNode同时掉线整个机架上的block进入丢失状态。副本模式下坏了一个副本可以从另一个副本拷回来总共要拷贝的字节数等于坏掉block的大小放大系数是1。而EC模式下丢了一个cell要恢复它理论上至少要读取该stripe内的其他6个存活cell数据块或校验块来做解码读放大系数接近6倍。当时坏掉的数据量其实不算大但因为这6倍的读放大再加上EC文件分散在多个机架重建流量在核心交换机上瞬间形成了带宽风暴。结果不但重建变慢了还连累了同链路其他业务的网络质量。那次之后我们紧急加入了重建流控把同时进行的重限制在一个可控窗口内并且按交换机端口做了限速宁可重建慢一点也不能让关键业务跟着遭殃。这块的经验可以总结为EC省下的空间本质上是用“磁盘冗余”换来了“网络和CPU开销”。空间账要算故障恢复账更要算。特别是机架级故障这种低概率但高破坏力的场景一定要在压测环境里提前模拟别等真出了事故再去调参数。5.4 用hdfs fsck检查EC文件时看到的东西EC文件在HDFS里是通过条带化的block来表示的用hdfs fsck检查文件健康状态的方式和普通副本文件不太一样。这里分享几条我们常用的命令和判定经验。# 检查某个EC目录下所有文件的健康状态 hdfs fsck /data/cold -files -blocks -locations # 查看EC文件的块布局 hdfs fsck /data/cold/dt20240101/part-0001 -files -blocks执行后EC文件会以类似下面的形式展示条带信息EC policyRS-6-3-1024k, stripedBlocks1, blockGroups1, cellsize1048576这里最关键的是关注stripedBlocks和blockGroups的数量是否正常。如果某个blockGroup的存活block数低于数据块数量hdfs fsck会报CORRUPT状态意味着当前可用块不足以完成解码。这种情况必须立即介入确认是临时故障还是永久损坏必要时要启动数据恢复流程。另外一个常被忽略的点fsck输出的文件总大小和物理占用大小是两个概念。EC模式下逻辑大小和物理占用需要按利用率换算我们监控容量时专门加了指标避免直接拿逻辑大小做统计导致高估或低估。6. 上线一年后的数据节省了多少、性能如何、后续优化6.1 容量账副本率从3.0降到1.8左右EC全量落地大约一年后我们核心集群的存储结构发生了明显变化。统计下来全集群约35%左右的存量数据完成了EC化整体有效存储利用率大幅提升。拿逻辑数据量去除物理存储量集群的副本率从EC化前的3.0降到了1.8左右。换句话说同样一批硬盘现在能装下比以前多近四成的逻辑数据。说一个更直观的数字。实施EC化之前核心集群的有效数据量是几十PB量级每天还在增加。EC化之后原本快满的集群腾出了数PB级别的物理空间直接帮我们撑过了一轮扩容周期的等待。那批45天到货的硬盘到位时集群压力已经不那么紧迫了。6.2 性能与CPU开销全表扫描没有想象中那么糟容量收益之外大家最关心的是性能。上线后我们持续监控了各类任务的耗时情况结论是对于典型的大规模全表扫描类离线任务EC目录下的读取速度和三副本目录基本持平部分测试场景因为多节点并行读取耗时反而略有下降。这印证了我们当初在测试环境里的判断。CPU侧的开销确实增加了主要在于EC写入时的编码计算。好在现代服务器CPU算力富余而且我们确认启用了底层native编解码库纯Java实现的编解码计算基本被绕开了单次编码的CPU开销比预想小很多。在运行的DataNode集群上CPU平均水位上升大概几个百分点在可接受范围内。受影响的场景主要集中在随机读和少量并发查询上。这类任务我们全部留在三副本目录不碰EC。6.3 下一步让计算引擎更理解EC跑了一年之后EC在容量收益上的价值已经毋庸置疑。但我们也清楚当前实现还有很多优化空间。最大的一个点是目前的计算引擎在读取EC文件时还是把它当作普通block文件来对待的并没有深度感知条带结构。比如Spark或Hive在读取一个EC文件时可以明确告诉调度层“这个文件由9个block组成其中只有6个是数据block”从而跳过无意义的校验块扫描、按顺序优先拉取数据块和必要的校验块。再比如某些场景下可以尝试在任务本地缓存已解码的条带避免重复解码。这些属于引擎层的深度适配短期收益可能不如存储层那么直接但长期看值得持续投入。另外一个方向是自动冷热分层。现在我们还是靠人工判断哪些目录该EC化未来我们希望引入基于访问频率、写入时间、文件大小的自动化分层机制让数据在“三副本”和“EC”之间自动流转减少人工介入。当然数据自动迁移带来的风险也需要更完善的机制来兜底这是下一步要啃的硬骨头。回过头来说vivo这次HDFS EC大规模落地我觉得最核心的收获不是省了多少空间而是建立了一套“数据分类-策略绑定-迁移调度-故障预案”的完整方法。EC本身是个成熟的机制真正决定成败的是你对业务数据有多了解、对故障场景有多敬畏。把冷热边界划清楚把灰度节奏控住把重建风暴预案做好HDFS EC就能成为一套非常可靠的空间释放手段。