Hadoop 3.x升级实践:从纠删码到Kubernetes容器化部署 这两年做大数据基础设施跟 Hadoop 集群打交道下来最明显的感受是Hadoop 2.x 时代那套“三副本 固定物理机”的玩法越来越贵也越来越笨重。业务方要省钱老板要效率运维要省心这三个诉求叠在一起逼着团队往 Hadoop 3.x 走——先是上纠删码把存储成本降下来再是借着云原生的风把集群塞进 Kubernetes。这篇文章就把我实际落地 Hadoop 3.x 的过程、取舍和踩坑记录一次理清楚从纠删码讲到容器化部署给正在做同样演进的团队一个参考。1. 为什么企业选择 Hadoop 3.x 而不是继续守在 2.x1.1 从 2.x 到 3.x到底改了哪些东西很多团队其实不是不想升而是不清楚 3.x 跟 2.x 的本质差别。我见过不少集群还在 2.7/2.9 上跑着运维同学每天都在跟容量告警和 Full GC 耗时间。Hadoop 3.x 最大的变化并不是某个参数调优而是一套新的底层能力我把关键差异列出来能力点Hadoop 2.xHadoop 3.x我的评价默认副本策略3 副本兼容 3 副本但默认支持纠删码EC3.x 真正的杀手锏存储成本直接对半砍NameNode HA需要手动配置 QJM内置更成熟的 Quorum Journal Manager可用性提升故障切换更稳支持多 NameNode 联邦有但配置繁琐Router-based FederationRBF更成熟超大规模集群必备YARN 资源模型只认 CPU 和内存支持 GPU、FPGA 等异构资源机器学习负载也能统一调度容器化支持基本没有官方开始支持 NodeManager 容器化社区方案成熟上云、上 K8s 的基础默认 Java 版本Java 7/8官方支持 Java 8/11新集群没有历史包袱这里要特别说一句3.x 不是“改个版本号重启一下”那么简单。它是把 HDFS 的存储模型从“复制存储”改成“编码存储”把 YARN 从“单一调度”改成“可扩展资源调度”这套底层的改变才支撑了后面的云原生容器化。如果只是升了版本但策略全用旧的那升了跟没升差不多。1.2 什么业务值得为升级买单不是所有集群都需要立刻升但从我接触的企业来看下面这几种情况越早升越划算。第一种是存储大头在冷数据的。比如数据仓库的离线层、日志归档、历史订单这类数据写入之后很少再改但量大占着 3 倍副本的空间。这类场景上纠删码存储成本能肉眼可见地降下来。第二种是被 NameNode 性能卡脖子的。3.x 在 NameNode 内存模型、RPC 处理上有明显优化大目录、超多小文件的场景比 2.x 稳不少。第三种是准备做容器化、混合云或者多集群统一调度的。2.x 里的 YARN 在容器支持上很弱想在 K8s 上跑几乎得靠各种 hack。3.x 配合社区方案才能谈得上“云原生大数据”。1.3 升级前需要掂量的成本与风险我见过太多团队仓促升级然后回滚的案例核心问题不是技术而是没想清楚成本。首先是兼容性成本。Hive、Spark、Flink 这些上层组件要跟着 Hadoop 3.x 匹配尤其是 Spark 和 Hadoop 的 RPC 协议、shuffle 机制在 3.x 有变化版本搭配不对跑起来全是坑。其次是人肉成本3.x 的新特性你不能光看文档要真的在小集群压一遍光 EC 重建、滚动升级这两项我建议预留至少两周的验证时间。最后才是资源成本上 EC 不是零成本后面会专门讲。所以我的建议是先小规模试点比如几十个节点把核心业务跑上去观察一两个月稳定之后再扩大范围。直接把整个集群一刀切升级的风险比想象中大得多。2. 纠删编码EC用 1.5 倍存储留住 3 倍可靠性2.1 纠删码的数学直觉与 HDFS 落地方案如果让我用最简单的话解释纠删码Erasure CodingEC我一般会这样说三副本的本质是把一份数据复制三份存着哪份坏了用哪份EC 的本质是给数据算一道校验题坏掉一部分用剩下的部分也能算出原数据。HDFS 3.x 里默认的 EC 策略是 RS-6-3-1024k。把 1024k1MB的单元作为计算粒度一份数据逻辑上切成 6 份数据块再用里德-所罗门Reed-Solomon编码算出 3 份校验块总共 6 3 9 份分布在集群里。这里的“6 3”意味着任意丢失 3 份剩下的 6 份依然能还原全部原始数据。从数学上讲RS 编码是在有限域GF(2^8)上做矩阵乘法和逆运算。通俗点说6 个数据块相当于 6 个“已知量”你通过编码矩阵生成 3 个“校验方程”只要剩下的块数不少于 6 个方程组就能解出原始数据。这也是为什么 EC 能容忍丢失 3 个块的原因。对应到存储开销三副本是 3 倍存储而 RS-6-3 只需要 (63)/6 1.5 倍。一个 100TB 的数据目录用三副本要占 300TB 物理空间用 EC 只要 150TB直接省一半。这是我在生产环境里亲眼验证过的数字不是概念推演。2.2 三种编码策略怎么选Hadoop 3.x 内置的 EC 策略不止一种我给团队培训时常说一句话别默认选 RS-6-3策略选错比不选更难受。策略数据块校验块存储开销容错能力适用场景RS-6-3-1024k6 31.5 倍任意 3 块丢失冷数据、历史数据、备份归档RS-3-2-1024k3 21.67 倍任意 2 块丢失温数据、近实时分析XOR-2-1-1024k2 11.5 倍仅 1 块丢失临时数据、重建优先级低的场景注意看XOR-2-1 的存储开销也是 1.5 倍但容错能力只有 1 块为什么因为 XOR 编码的数学复杂度远低于 RS没有 RS 那么强的纠错空间。所以它在成本和容错上并不比 RS-6-3 划算只有在 CPU 资源紧张、临时数据损坏可接受的情况下才建议用。对于大多数企业我建议冷数据直接用 RS-6-3因为容错能力强重建窗口内再坏一块也能扛住。RS-3-2 适合那些还要被 Spark SQL 偶尔扫一下的温数据存储开销略高一点但编码和解码的 CPU 开销更低查询性能会好一些。2.3 生产落地的设置步骤与实操记录EC 的落地很简单但细节决定成败。第一步是确认集群开了 EC 相关配置然后在目标目录上设置策略最后检查写入的块分布。先用 hdfs ec 命令查看并设置策略# 查看当前集群支持的 EC 策略 hdfs ec -getPolicies # 为冷数据目录设置 RS-6-3 策略 hdfs ec -setPolicy -path /data/cold/warehouse -policy RS-6-3-1024k # 确认策略已生效 hdfs ec -getPolicy -path /data/cold/warehouse这里有一个关键点EC 策略只对目录下“新建的文件”生效已有的文件不会自动重编码。所以生产上正确的做法是先建好新目录并设置策略再用 distcp 把旧数据迁过去# 建新目录并设置 EC 策略 hdfs dfs -mkdir -p /data/cold/warehouse_ec hdfs ec -setPolicy -path /data/cold/warehouse_ec -policy RS-6-3-1024k # 把旧目录数据全部迁移过去 hadoop distcp -i -m 100 -numListstatusThreads 20 \ hdfs://namenode:8020/data/cold/warehouse \ hdfs://namenode:8020/data/cold/warehouse_ec # 迁移后用 fsck 检查块健康度 hdfs fsck /data/cold/warehouse_ec -files -blocks -locations我在实际迁移中吃过一个亏distcp 默认会保留源目录的复制策略如果源目录没有设置 EC迁移到目标目录后文件可能仍然是 3 副本。解决办法是在 distcp 命令里带上-p相关属性或者迁移完成后用 fsck 抽查块数量确认当前文件确实是 EC 编码而不是 3 副本。再补充几个生产环境里的经验不要对正在高频写入或随机更新的目录开 ECEC 不支持追加写append和随机写这类场景等着被坑。先看业务再上策略像是 Spark Streaming 写临时状态表的目录不建议开 EC宁愿多花点存储也要保证写入性能。EC 和 HDFS 快照Snapshot不兼容如果业务依赖快照做误删恢复这块要做额外备份方案。EC 重建会吃 CPU 和网络带宽默认情况下后台重建线程有限遇到批量坏块会导致长时间降级。建议在业务低峰期做数据迁移并预留足够的网络带宽。从成本角度算一笔账一个 500TB 的冷数据目录三副本要 1500TB用 RS-6-3 只要 750TB按 1TB 硬盘成本折算省下的硬件成本非常可观。EC 绝对是我认为 Hadoop 3.x 最值得先上的特性。3. 把 Hadoop 搬进 Kubernetes存储计算分离的容器化实践3.1 容器化面临的三座大山EC 是存储层的改造容器化是部署形态的改造。很多团队卡在“Hadoop 能不能上 K8s”这个问题上我的答案是能但前面有三座山要翻。第一座山是“有状态服务”。HDFS 的 DataNode 和 NameNode 天生是有状态的NameNode 的元数据、DataNode 的数据块都得持久化。K8s 默认的 Deployment 是无状态的Pod 说没就没数据不能跟着没。解决方案是用 StatefulSet 加 PVC 把数据和 Pod 生命周期绑定Pod 可以重建数据不能丢。第二座山是“数据本地性”。Hadoop 的设计哲学是“计算靠近数据”DataNode 在哪NodeManager 最好就在哪。但 K8s 的调度器可不管你这套它把容器放哪完全看资源水位。如果不干预很可能计算出现在 A 节点数据在 B 节点每次 MR/Spark 都走远程读性能直接崩。第三座山是“服务发现和网络”。原来的 Hadoop 集群靠 hostname 互相认容器化之后 Pod 的 IP 和名称动态变化NameNode 怎么知道 DataNode 在哪YARN 的 ResourceManager 怎么知道 NodeManager 在哪这就要靠 Headless Service 和稳定的网络标识来兜底。我的建议是在动手写 YAML 之前先想清楚栈怎么搭。先用 StatefulSet 解决“有状态”用“DataNode 和 NodeManager 同 Pod”解决“数据本地性”用 Headless Service 解决“服务发现”。这三件事想透了后面都是细节。3.2 可复用的部署骨架StatefulSet 加本地盘下面是我在项目里实际用过的 DataNode NodeManager 同 Pod 的骨架跑在 Kubernetes 1.24、Hadoop 3.3.x 上12 个节点、单节点 8Ti 本地盘的配置下表现稳定apiVersion: apps/v1 kind: StatefulSet metadata: name: hadoop-datanode namespace: bigdata spec: serviceName: hadoop-datanode-headless replicas: 12 selector: matchLabels: app: hadoop-datanode template: metadata: labels: app: hadoop-datanode spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - hadoop-datanode topologyKey: kubernetes.io/hostname containers: - name: datanode image: hadoop:3.3.6 command: [hdfs, datanode] ports: - containerPort: 9866 name: dfs-datanode volumeMounts: - name: dfs-data mountPath: /data/dn - name: nodemanager image: hadoop:3.3.6 command: [yarn, nodemanager] ports: - containerPort: 8042 name: nm-web volumeMounts: - name: dfs-data mountPath: /data/dn env: - name: YARN_NODEMANAGER_LOCAL_DIRS value: /data/dn/nm-local - name: YARN_NODEMANAGER_LOG_DIRS value: /data/dn/nm-log volumeClaimTemplates: - metadata: name: dfs-data spec: accessModes: [ReadWriteOnce] storageClassName: local-hdd resources: requests: storage: 8Ti这个方案有几个设计点值得讲DataNode 和 NodeManager 共享同一个 Pod 和同一个/data/dn目录。因为它们在同一个网络命名空间、共享主机文件系统NodeManager 分配出去的容器读取的数据其实就在本机天然满足数据本地性。这比用 podAffinity 去“尽量凑在一起”要可靠得多。用 StatefulSet 的volumeClaimTemplates为每个 DataNode 绑定一块独立的本地盘。Pod 重建后PVC 重新挂载数据不丢。注意要用支持本地盘的 StorageClass比如 local-static-provisioner否则把云硬盘跨节点重挂载会有性能问题。Pod 反亲和性podAntiAffinity保证同一条物理机不会同时跑两个 DataNode避免单节点故障导致多个副本同时不可用。这是一个非常“朴素但管用”的方案。不用高深的 Operator不搞复杂的调度策略一个 StatefulSet 就把数据本地性和有状态问题解决了。3.3 NameNode 高可用怎么容器化DataNode 容器化之后NameNode 必须保持高可用这是整个集群的命门。我在 K8s 上部署了两台 NameNode 外加三台 JournalNode全部用 StatefulSet 管理。关键是 JournalNode 的数量和故障域三台 JournalNode 必须分散到三个不同节点并且保证同一时刻最多只能挂一台。这样才能在 QJM 协议下维持“多数派”可用。JournalNode 的 edits 日志目录要用独立的 PVC跟 DataNode 数据盘分开。容器化的 NameNode 与物理机部署的主要差别在于ar 和服务的互认需要通过 DNS 而不是 IP。所以在 core-site.xml 里把fs.defaultFS配成hdfs://hadoop-nn这个地址由 K8s 的 Service 解析到当前 active 的 NameNode。然后用hdfs haadmin -transitionToActive做主动切换或者依赖 ZKFC 自动切换。这里有一个容易踩的坑YARN 的 ResourceManager 也需要高可用但很多团队只配了 NameNode HA忘了 RM。实际跑起来RM 挂了整个集群任务全挂。我的建议是 RM 也要至少两个实例配合 ZK 做 active/standby并且保证 RM 和 NM 的地址能通过 Service 解析否则节点重启后 NM 找不到 RM。3.4 数据本地性怎么保前面说了“同 Pod”方案是最省心的但如果团队出于隔离性或者升级便利考虑一定要把 DataNode 和 NodeManager 拆开部署那数据本地性就得靠调度策略硬撑。可以用 podAffinity 让 NodeManager 尽量调度到 DataNode 所在节点affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - hadoop-datanode topologyKey: kubernetes.io/hostname但我要提醒preferred是“尽量”不是“必须”。如果 DataNode 所在节点资源不足NodeManager 仍然会被调度到别处数据本地性就破了。如果一定要强制可以把preferred改成required但那样做容易导致资源碎片化Pod 调度不上去。我个人还是推荐同 Pod 方案它的本地性是 100% 保证的而且运维起来就是“重启一个 Pod 就是重启一个 DNNM”心智负担小很多。4. 云原生数据湖的周边配套CMDB、监控与对象存储4.1 HDFS、对象存储与访问协议的选择Hadoop 3.x 容器化之后很多团队会顺手把数据湖的那套也接进来。这时候一个绕不开的问题是到底是继续用 HDFS 还是上对象存储OSS/S3。我的观点很明确HDFS 不该被完全替代但也不会是唯一存储。对于高并发、低延迟、强一致性的核心数仓层HDFS 依然是最稳的选择对于海量非结构化数据、日志归档、机器学习样本这类“写一次很少改”的数据直接放在对象存储里更省钱、扩容性也更好。Hadoop 3.x 对对象存储的兼容已经做得很成熟S3A 文件系统插件可以直接让你用 HDFS 的命令行和 API 去读写对象存储property namefs.s3a.endpoint/name valuehttp://oss-cn-hangzhou-internal.aliyuncs.com/value /property property namefs.s3a.access.key/name valueyour-access-key/value /property property namefs.s3a.secret.key/name valueyour-secret-key/value /property配置好之后在 Spark、Hive 里可以直接这样读SELECT * FROM parquet.s3a://your-bucket/path/to/table;这么做的最大好处是计算容器化的 Spark/Flink和存储对象存储完全分离计算集群可以随时缩容到零存储成本按量付费。这就是标准的云原生数据湖姿势。HDFS 在中间扮演的角色会逐步降级为“本地热数据缓存”或“数仓核心层”而不是所有数据都往里面塞。4.2 容器化集群如何纳入 CMDB 和监控体系容器化之后最直观的痛点是主机和进程变成了“动态资源”今天这个 Pod 在 node-3明天可能就漂到 node-5。传统 CMDB 里维护“哪台机器跑了哪个进程”的做法彻底失效了。我在项目里的做法是让 CMDB 丢掉静态主机清单改成对接 K8s API 和 YARN REST API。定期拉取 Pod 列表、Node 列表、Pod IP、标签和状态把 DataNode、NodeManager 的动态 IP 和端口登记到 CMDB 的资产表里。只要 Pod 重建IP 变了CMDB 在下一轮采集周期就能自动更新核心是“以 API 为准不做人工维护”。与之配套的是监控体系。HDFS 和 YARN 都暴露 JMX 指标我们通过 Prometheus 定期抓取 NameNode、DataNode、ResourceManager、NodeManager 的 JMX 端口再配合 Grafana 做告警。我维护监控时必看的几个指标指标来源告警阈值我的经验值NameNode RPC 处理平均时间JMXRpcActivityForPort超过 50ms 就要排查DataNode 存活数HDFS FSNamesystem低于配置副本数即告警EC 重建任务积压数JMXPendingReconstructionBlocks持续上涨要关注NodeManager 可用内存YARN ClusterMetrics低于集群总量 10% 告警容器本地读命中率YARN NodeManager 指标低于 70% 检查调度策略容器化集群里网络监控特别容易被忽略。Pod 之间的流量经过 service 转发后ServiceMesh 或 DNS 解析失败会直接让跨 Pod 的 RPC 超时。我建议把 CoreDNS 的监控指标也加进去Pod 没有就重启 CoreDNS是排查分布式集群诡异问题时最常做的操作之一。4.3 渐进式迁移路线从双跑到存量搬迁最后说迁移路线。真的到了要把生产 Hadoop 集群从物理机迁到 K8s 的阶段我强烈建议不要做“大爆炸式迁移”而是下面三步走第一步做双跑。新集群容器化部署成功之后把核心的数据导入流程复制一份跑增量或抽样任务两侧做数据校验确认新老集群跑出来的结果一致。这个阶段别急着切流量观察至少两周。第二步切写入。把新的写入任务比如 Flink 实时入库、离线调度逐步切到新集群老集群只保留只读服务避免双写带来的数据不一致问题。这时候重点观察新集群的吞吐和延迟是否满足业务要求。第三步存量搬迁。老集群的历史数据用 distcp 或 ReplicationManager 迁移到新集群或对象存储。迁移过程要分批执行每迁完一批就做一次 fsck 和数据校验确认无误后再释放老集群资源。我个人看到过太多团队跳过前两步直接做第三步结果数据校验不过关、业务回滚链路断裂最后花了几倍的时间收拾烂摊子。云原生是趋势但迁移一定要稳扎稳打。5. 常见问题与排错实录5.1 纠删码相关的坑问题 1设置了 EC 策略但新文件还是 3 副本这个坑我几乎每次跟人聊都会提。EC 策略是绑定“目录路径”的而且是目录创建时生效的一种属性。如果你是在已有目录上执行hdfs ec -setPolicy那它只对设置之后“新创建的文件”生效已有的文件不会自动重编码。另外如果你用 distcp 迁移默认情况下 distcp 会把源目录的复制策略带过去目标目录如果本身没有 EC 策略文件可能仍是 3 副本。排查方式很简单hdfs ec -getPolicy -path /path看目录策略再hdfs fsck /path -files -blocks -locations看具体文件的块数量。如果是 3 副本每个 block 会出现在 3 个 location如果是 ECblock 组会分布在 9 个 locationRS-6-3。问题 2EC 目录快照失败EC 和 Snapshot 不兼容是官方限制。如果你业务上依赖目录快照要么放弃在这个目录上用 EC要么改造备份方案用 distcp 定期把 EC 目录同步到另一个普通目录做快照。问题 3EC 重建慢坏了块几天都没补回来我在实际环境里遇到过一台物理机宕机180 多个 EC block 处于重建状态但后台重建速度很慢。原因有两个一是 NameNode 默认的 EC 重建调度线程数量有限二是重建任务本身要大量读取其他数据块做解码计算带宽被业务流量挤占。解决方式是在低峰期临时调大重建并发量并把重建流量限制在一个可控范围比如调整dfs.namenode.ec.reconstruction.threads和相关带宽设置。具体配置名称以你所用发行版文档为准不同发行版可能不太一样。5.2 容器化相关的坑问题 1NodeManager 找不到 ResourceManagerK8s 里最常见的故障。NM 启动后要向 RM 注册如果 NM 的配置里 RM 地址是某个 Pod IP 或者已经变化的 Service 地址注册就失败。解决方法是在 yarn-site.xml 里配置 RM 的地址为 K8s 的 Service DNS 名称别写 IP并且在所有 NM 的配置里保持一致。问题 2DataNode 反复重启报端口冲突或盘挂不上DataNode 用的端口如果和其他服务冲突容器起不来要先kubectl logs看日志。盘挂不上的问题通常出在 PVC 的 StorageClass 上本地盘和网络盘要区分清楚别把ReadWriteOnce的盘配成跨节点共享那是 CephFS 和 NFS 该干的活。问题 3Pod 调度不上去通常是资源不足或者亲和性把 Pod 卡死了。我用kubectl describe pod xxx看调度事件时经常见到0/12 nodes are available这类信息。如果是反亲和性太严格把requiredDuringSchedulingIgnoredDuringExecution改成preferredDuringSchedulingIgnoredDuringExecution会缓解很多。5.3 一条好用的排错路径容器化集群排错我总结了一套固定路线团队新同学照着走基本能解决 80% 的问题先用kubectl get pods -n bigdata -o wide确认 Pod 状态和所在节点。再用kubectl logs pod -c container --tail200看日志优先找 ERROR、WARN、Exception。如果是跨 Pod 通信问题kubectl exec -it pod -- curl telnet://service:port或nslookup service检查 DNS 解析和连通性。最后再用 Hadoop 侧命令确认集群状态hdfs dfsadmin -report、yarn node -list、hdfs fsck。这套路径的好处是先看 K8s 层再看 Hadoop 层逻辑清晰不会在两边来回跳。我见过很多同事在 Hadoop 日志里翻半天结果发现就是 DNS 解析失败导致连不上另一台 Pod。6. 最后说几句实操心得内容写到这儿该聊的架构、步骤、坑都聊得差不多了最后就分享几个花了钱和时间才换来的体会。第一纠删码不要贪心。RS-6-3 省空间但不适合所有数据。我的原则是冷数据、大文件、批量导入的数据用 EC热数据、小文件、要求低延迟的路径老老实实留 3 副本。省下的空间最终要还回去一部分给性能和运维成本。第二容器化不是目的弹性和成本才是。如果你当前物理机集群跑得稳、容量够没有资源利用率低或者扩缩容慢的痛点没必要为了“上容器”而上容器。容器化最大的价值是把基础设施变成可编程资源适合业务波动大、多环境隔离要求高的团队。第三Hadoop 3.x 的升级路径最好是“小步快跑”。先在一个低风险业务上做 EC 试点再在一个新环境上做容器化验证每一步都跑出结果再扩大范围。不要同一时间既改存储模型又改部署方式否则出了问题你都分不清是 EC 的锅还是 K8s 的锅。这套从 EC 到容器化的玩法我前后推了接近一年才稳定下来。如果你正在折腾类似的事希望这篇能帮你少走几条弯路。