CDH6.2 Spark on YARN 三节点集群搭建与调优实战 1. 先定版本和角色别上来就埋头装CDH6.2 装 Spark 集群基础环境这件事我在不同规模的项目里搭过五六遍每次最耗时间的都不是敲命令而是装之前的规划。原因很实在CDH 这套东西把 Hadoop 生态几十个组件用 Parcel 包统一管起来CMCloudera Manager负责部署、监控、配置下发本身是个重型平台。规划错了后面拆集群比装集群还贵。所以我习惯先把版本基本盘和角色分布钉死再动手。先说版本。CDH 6.2 对应的大数据底座是 Hadoop 3.0.0、Spark 2.4.0、Hive 2.1.1、ZooKeeper 3.4.5、HBase 2.1.0CM 版本是 6.2.x。Spark 2.4 走的是 Scala 2.11 编译执行引擎还是基于内存的 DAG 调度没到 3.x 那一套自适应执行和动态合并的层次。这些细节看着偏门但你在后面挑 jar 包、写--packages依赖、决定用 Python 2.7 还是 Python 3 的时候全用得上。CDH 6.2 的 PySpark 默认绑定 Python 2.7想换 3.x 得在 CM 里单独配spark.pyspark.python和PYSPARK_PYTHON环境变量。1.1 这套组合解决的是什么问题很多人问既然可以自己拉 Apache 二进制包手撸 Spark on YARN为什么还要上 CDH答案取决于你的团队规模和使用场景。手撸适合一两个人维护、跑单一业务任务的场景而 CDH 这类平台真正省事的地方在于统一了配置源、角色生命周期和监控告警。一个几十个节点的集群节点坏了、角色迁移、配置批量下发、日志集中检索靠人肉 ssh 是撑不住的。从能力边界看这套环境落成之后能覆盖的很明确离线批量计算Spark SQL、DataFrame 作业、Spark Streaming 微批处理、Spark on YARN 的多种提交模式、以及与 Hive 元数据打通的数仓查询。它不适合的场景也很明确毫秒级延迟的在线服务、超大规模参数服务器训练、需要 GPU 调度的场景。想清楚这层你对装完能干什么就有底了也不会在后期被业务方追着问为什么延迟下不去。1.2 三节点起步的角色分配我把最小可用集群的角色分布列成表这是我在多个测试环境里反复验证过的组合能跑通全流程又不至于资源打架。注意这里说的最小可用是指能完整走完 CM 部署、Parcel 分发、Spark 提交、History Server 查看的全链路不是生产建议。主机角色节点A管理主节点B工作节点C工作CM Server是否否CM Agent是是是NameNode / JournalNode是是是DataNode是是是ResourceManager是是备否NodeManager是是是ZooKeeper是是是Hive Metastore是否否Spark History Server是否否MySQL元数据库是否否内存分配上我踩过一个坑节点A同时扛 CM Server、NameNode、ResourceManager、MySQL8G 内存会非常吃力CM 的服务端进程本身就是 Java 应用堆内存 4G 起步比较稳。节点A给 16G节点B/C 各给 16G磁盘每节点至少两块系统盘 数据盘数据盘挂/data并单独给 DataNode 目录用。三节点加起来 48G 内存、6T 左右的盘跑一个中小规模的验证环境足够了。提示节点数量的边界取决于 ZooKeeper 的 quorum 和 HDFS 副本数。ZK 至少 3 个才能容忍 1 个节点故障HDFS 默认 3 副本节点数低于 3 时副本永远处于 under-replicated 状态虽然不影响跑作业但你会天天看到告警。2. 操作系统这一层绝大多数坑都埋在这里我见过太多人把 CM 装完、集群起来结果 Spark 作业跑到一半 OOM 或者卡在 shuffle 阶段最后发现是操作系统层面vm.swappiness没调、透明大页没关、文件句柄数没放开。这些东西在文档里都是一行命令但漏一个就够你排查一整晚。所以这一章我按必须做和建议做分层写别跳。系统我选 CentOS 7.x这是 CDH 6.2 支持最稳的发行版。7.9 也可以用内核 3.10 那一套透明大页和 cgroup 的行为都经过大量验证。别用更新的发行版repo 里的依赖版本对不上后面你会被 glibc 和 systemd 的差异折磨。2.1 主机名、hosts 与时钟的联动主机名这件事有两个硬要求每台机器用完全限定域名FQDN风格命名比如node-a.example.lab并且在/etc/hosts里所有节点互相能解析到。CM 的角色分配、Agent 注册、Kerberos 后面的 principal 命名全都依赖主机名的一致性。主机名改了又没重启或者 hosts 里写的是短名而机器名是 FQDNAgent 就会出现注册到 CM 时显示成另一个节点的情况非常隐蔽。三台机器统一执行hostnamectl set-hostname node-a.example.lab cat /etc/hosts EOF 10.0.0.11 node-a.example.lab node-a 10.0.0.12 node-b.example.lab node-b 10.0.0.13 node-c.example.lab node-c EOF时钟同步用 chronydCDH 对时间漂移敏感Kerberos 默认允许的时钟偏差只有 5 分钟Spark 的 event log 时间戳错乱也会让 History Server 的事件顺序看着莫名其妙。配置一个内网时间源yum install -y chrony sed -i s/^server .*/server 10.0.0.1 iburst/ /etc/chrony.conf systemctl enable --now chronyd chronyc sources -v实测下来集群运行一段时间后节点间偏差控制在 100ms 以内是没问题的。我建议把chronyc tracking的结果也纳入日常巡检遇到过磁盘 IO 打满导致时间同步进程被挤掉的情况偏差一旦超过阈值YARN 的 container 认证会直接失败。2.2 内核参数、透明大页与服务裁剪这一段是重点。透明大页THP对 JVM 是灾难它会把内存分配延迟从微秒级拉到毫秒级表现出来就是 Spark 作业 GC 时间异常长、Task 长尾严重。必须关掉且要保证重启后依然生效。网上一堆教程只写了echo never结果机器一重启又变回always作业又开始抖动。echo never /sys/kernel/mm/transparent_hugepage/enabled echo never /sys/kernel/mm/transparent_hugepage/defrag cat /etc/rc.d/rc.local EOF echo never /sys/kernel/mm/transparent_hugepage/enabled echo never /sys/kernel/mm/transparent_hugepage/defrag EOF chmod x /etc/rc.d/rc.local/etc/rc.d/rc.local需要chmod x才会被执行很多人漏了这一步等于白配。sysctl 和 limits 一起改cat /etc/sysctl.conf EOF vm.swappiness 1 vm.overcommit_memory 1 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_keepalive_time 600 net.core.somaxconn 32768 EOF sysctl -p cat /etc/security/limits.d/99-cloudera.conf EOF * soft nofile 65536 * hard nofile 65536 * soft nproc 65536 * hard nproc 65536 EOFvm.swappiness1而不是 0这是我个人的取舍完全禁用 swap 在一些内存吃紧的节点上会触发 OOM Killer 直接杀进程留一点点交换余量能让系统在极端情况下撑住代价是可接受的。vm.overcommit_memory1是为了让 JVM 申请堆内存时不被内核拒绝Spark executor 启动失败的报错里有一大半和这个有关。SELinux 和 firewalld 直接关掉内网环境没必要折腾策略setenforce 0 sed -i s/^SELINUX.*/SELINUXdisabled/ /etc/selinux/config systemctl disable --now firewalld注意setenforce 0只对当前生效/etc/selinux/config才是永久配置。两个都得改我见过只改前者重启后 SELinux 恢复 EnforcingCM Agent 起不来日志里只有一行 permission denied排查半天。2.3 JDK、用户与目录规划CDH 6.2 支持 Oracle JDK 8 和 OpenJDK 8我一般用 OpenJDK 8u 的最新小版本省去授权麻烦。装完确认java -version输出 1.8.0_2xx 以上并且JAVA_HOME在/etc/profile.d/里统一配好。所有节点必须路径一致因为 CM 下发的角色配置里写的是绝对路径。目录规划我习惯这样分/opt/cloudera放 CM 和 Parcel/data/dfs放 DataNode/data/yarn放 NodeManager/data/log放各角色日志。数据盘独立挂载有个好处磁盘满了不会连带把系统盘撑爆而且做快照、扩容都方便。用cloudera-scm系统用户跑服务这个用户由 CM 的 rpm 包自动创建不需要手工建。还有一项容易被忽略Python。Spark 2.4 的 PySpark 依赖 Python 2.7CentOS 7 自带但要确认python -V能输出 2.7.5。如果团队用的是 Python 3 为主后面在 CM 里给 Spark 单独配spark.pyspark.python和spark.pyspark.driver.python不要试图改系统默认的 python 软链会连带影响 yum。3. 元数据库与 CM 平台落地CM 的所有状态都存在关系型数据库里集群的角色、配置、审计日志、监控指标全在里头。这个库挂了整个管理平台就失忆。所以这一步的配置要有冗余意识虽然小规模环境用单实例 MySQL 也行但至少把参数调对、定期备份。3.1 MySQL 的初始化脚本与必改参数我选 MySQL 5.7兼容性最稳。装完之后第一件事是改my.cnf下面这几项直接决定后面集群跑不跑得顺[mysqld] datadir/var/lib/mysql socket/var/lib/mysql/mysql.sock transaction-isolation READ-COMMITTED max_connections 550 max_allowed_packet 64M innodb_flush_log_at_trx_commit 2 innodb_log_buffer_size 16M innodb_buffer_pool_size 2G innodb_flush_method O_DIRECT binlog_format ROW log_bin /var/lib/mysql/mysql-bin expire_logs_days 7transaction-isolation READ-COMMITTED是硬要求CM 在 REPEATABLE-READ 下会出现一些诡异的状态更新丢失。max_allowed_packet调到 64M 是因为 CM 会把大段配置快照写进库默认 4M 会直接报错而且报错信息藏在 CM 的服务端日志里界面上只显示一个泛泛的失败。innodb_flush_log_at_trx_commit2是性能与安全的折中牺牲极端断电场景下最后 1 秒的事务。建库和授权一次性写进脚本CREATE DATABASE scm DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE metastore DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE amon DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE hue DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE oozie DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE nav DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE DATABASE navms DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; CREATE USER scm% IDENTIFIED BY StrongPass#2024; GRANT ALL PRIVILEGES ON scm.* TO scm%; GRANT ALL PRIVILEGES ON metastore.* TO scm%; GRANT ALL PRIVILEGES ON amon.* TO scm%; GRANT ALL PRIVILEGES ON hue.* TO scm%; GRANT ALL PRIVILEGES ON oozie.* TO scm%; GRANT ALL PRIVILEGES ON nav.* TO scm%; GRANT ALL PRIVILEGES ON navms.* TO scm%; FLUSH PRIVILEGES;就算你这次只装 Spark 和 YARN也建议把 metastore、hue 这些库一起建了后面想加 Hive 不用重新折腾权限。库名和用户名用默认的别自作聪明改成别的CM 内部有些初始化逻辑对名字有假设。3.2 CM Server 与 Agent 的安装顺序流程是先装 Server 的 rpm跑scm_prepare_database.sh初始化库再启动 Server最后装 Agent 并指向 Server。顺序错了会很麻烦尤其是先启动 Agent 再去初始化库Agent 会一直重连失败。# 节点A yum install -y cloudera-manager-daemons cloudera-manager-server /opt/cloudera/cm/schema/scm_prepare_database.sh mysql scm scm StrongPass#2024 systemctl start cloudera-scm-server tail -f /var/log/cloudera-scm-server/cloudera-scm-server.log日志里出现Started Jetty server或者Monitoring services started就说明起来了端口 7180 可以访问。这个启动过程第一次比较慢因为要建表等两三分钟很正常别急着判断失败去重启重启反而会把初始化的表搞成半成品。Agent 这边# 所有节点 yum install -y cloudera-manager-agent cloudera-manager-daemons sed -i s/^server_host.*/server_hostnode-a.example.lab/ /etc/cloudera-scm-agent/config.ini systemctl start cloudera-scm-agentAgent 起来之后在 7180 界面里能看到待加入的主机。这里有个细节server_host必须和 Server 端证书里的主机名一致用 IP 有时候也能连上但后面 Kerberos 或者 TLS 一开就会出问题。养成用 FQDN 的习惯。3.3 Parcel 离线分发的正确姿势Parcel 是 CDH 的分发机制本质上是一个 tar.gz 加一个 sha1 校验文件。内网环境走离线把 parcel 放到/opt/cloudera/parcel-repo再补一个.sha文件。文件名必须和 CM 期望的完全一致包括最后那段 hash 值。mkdir -p /opt/cloudera/parcel-repo cp CDH-6.2.0-1.cdh6.2.0.p0.967373-el7.parcel /opt/cloudera/parcel-repo/ sha1sum CDH-6.2.0-1.cdh6.2.0.p0.967373-el7.parcel | awk {print $1} CDH-6.2.0-1.cdh6.2.0.p0.967373-el7.parcel.sha chown -R cloudera-scm:cloudera-scm /opt/cloudera/parcel-repoCM 界面里点检查新 Parcel能识别出来就说明路径和校验都对。分发到所有节点、解压、激活三步走完。解压时 CM 会在/opt/cloudera/parcels/下建软链比如CDH - CDH-6.2.0-1.cdh6.2.0.p0.967373。后面所有 spark-submit 的命令都用这个软链路径升级到 6.2.1 时软链自动指过去脚本不用改。提示Parcel 分发失败时先在目标节点看/opt/cloudera/parcel-cache里文件大小对不对。遇到过磁盘配额问题导致文件被截断CM 界面上只显示 timeout实际是文件不完整。4. Spark 服务上线与关键参数终于到正题。但要清楚Spark 在这套架构里不是独立集群它跑在 YARN 上属于按需申请资源的客户端角色。所以真正的底座是 YARN、HDFS、ZooKeeper这三个跑不稳Spark 一定跟着抖。4.1 先让 YARN 和 ZooKeeper 跑起来在 CM 里先加 ZooKeeper 服务三个节点各一个角色这是所有协调类组件的依赖。然后加 YARNResourceManager 放在节点A和B做 HANodeManager 三台全放。HDFS 类似NameNode 两台 HAJournalNode 三个DataNode 三台。YARN 的容器内存配置是我每次都要调的地方。默认yarn.nodemanager.resource.memory-mb可能给的很大或者很小得按实际物理内存减掉系统占用和其他角色开销来算。节点B有 16G 内存跑 NodeManager、DataNode、ZooKeeper我给 NodeManager 12G剩下 4G 留给系统和其他进程。yarn.scheduler.maximum-allocation-mb设成 8G意味着单个 container 最多能申请 8G这样--executor-memory 6g这种配置不会因为超出上限被拒。计算公式很简单NodeManager 可用内存 物理内存 - 系统预留(2~4G) - 其他 Java 进程堆内存。三节点 48G 物理内存实际能提供给 YARN 的容器内存大约 34G 左右。这个数就是你 Spark 集群的算力天花板后面做容量规划全从这儿推导。4.2 添加 Spark 服务时的几个关键选择在 CM 首页点添加服务选 Spark。CM 会自动带出一组角色建议History Server 一个放节点AGateway 类型角色分布在需要提交作业的节点上。Gateway 这个角色容易被误解它不运行任何进程作用是把客户端配置/etc/spark/conf下发到指定节点。没有 Gateway 的节点自己装了 spark 客户端也连不上集群因为配置不完整。关键配置项我列成表这些都是我实际调过的配置项建议值为什么这么设spark.masteryarn走 YARN 调度资源统一管理spark.eventLog.enabledtrue不开就没有 History 可看事后排查全靠 godspark.eventLog.dirhdfs://ns1/user/spark/applicationHistoryHDFS 上集中存储重启不丢spark.history.fs.logDirectory同上必须和上面完全一致多一个斜杠都不行spark.history.fs.cleaner.enabledtrue否则日志无限增长半年后 HDFS 被塞满spark.dynamicAllocation.enabledtrue按需伸缩 executor集群共享场景必备spark.shuffle.service.enabledtrue动态分配的前提不开会报错spark.executor.memory4g起步值按作业特征再调spark.yarn.maxAppAttempts2默认 2太多会拖长失败判定时间spark.eventLog.dir和spark.history.fs.logDirectory必须严格一致这是我踩过的第一个坑。前者是写日志的路径后者是读的路径不一致的话 History Server 起来是个空界面作业跑完了点进去什么都没有你会误以为是权限问题。4.3 History Server、Gateway 与客户端验证History Server 启动后默认端口 18088。访问http://node-a.example.lab:18088能看到应用列表。刚装完是空的跑个作业才有内容。客户端这边CM 会把配置下发到/etc/spark/conf包括spark-defaults.conf、spark-env.sh、hive-site.xml用于访问 Hive 元数据和core-site.xml、hdfs-site.xml、yarn-site.xml。这套配置的来源是 CM 数据库所以永远不要在节点上手工改这些文件下一次 CM 重启服务就会覆盖掉。要改配置走 CM 界面。验证客户端配置完整性最快的办法source /etc/profile spark-submit --version能打出 Spark 2.4.0-cdh6.2.0 和 Scala 2.11.12 的版本信息说明基本链路通了。如果报ClassNotFoundException或者找不到 HADOOP_CONF_DIR检查是不是在 Gateway 角色节点上以及/etc/spark/conf里的软链是否正常。5. 跑通第一个作业才算真的装完界面全绿不等于能用。我判断一套 Spark 环境是否交付完成的标准是能提交作业、能在 History Server 看到明细、能在 YARN 界面看到资源占用、日志能正常写出。这四件事都过了才算装完。5.1 spark-shell 与 spark-submit 实测先进交互式环境试水这是最快发现配置问题的方式spark-shell --master yarn --deploy-mode client进去之后会看到 Spark context 初始化sc和spark两个变量就绪。跑个最简单的val rdd sc.parallelize(1 to 1000000, 20) rdd.map(x x * 2).reduce(_ _)如果这一步能出结果说明资源申请、shuffle、结果回收整条链路都通了。接着跑 Pi 的示例这个能验证更完整的任务调度spark-submit \ --master yarn \ --deploy-mode client \ --executor-memory 4g \ --executor-cores 2 \ --conf spark.dynamicAllocation.enabledtrue \ --conf spark.dynamicAllocation.minExecutors2 \ --conf spark.dynamicAllocation.maxExecutors6 \ --class org.apache.spark.examples.SparkPi \ /opt/cloudera/parcels/CDH/lib/spark/examples/jars/spark-examples_2.11-2.4.0-cdh6.2.0.jar 200jar 路径要精确到带_2.11和cdh6.2.0后缀的那个文件。CDH 里的示例 jar 和 Apache 原版命名不同直接用网上抄来的路径会报文件不存在。这一步跑通YARN 界面里能看到 application 状态从 ACCEPTED 到 RUNNING 再到 FINISHEDHistory Server 里点进去能看到 stage 和 task 明细。5.2 动态资源分配与 shuffle service 的打通动态资源分配这块有个隐藏依赖spark.shuffle.service.enabledtrue要求 YARN 的 NodeManager 侧启用了对应的辅助服务。CM 里 Spark 服务添加时会自动往 YARN 配置里注入yarn.nodemanager.aux-services的增加项但不会自动重启 NodeManager。所以顺序很重要先装 YARN跑起来。再装 SparkCM 会修改 YARN 的yarn.nodemanager.aux-services加入spark_shuffle并配置yarn.nodemanager.aux-services.spark_shuffle.classorg.apache.spark.network.yarn.YarnShuffleService。手动重启 YARN 服务让 NodeManager 加载新的 aux service。再跑带动态分配的作业。我见过有人第三步漏了作业报YarnShuffleService is not initialized或者 executor 释放后 shuffle 数据找不到报错信息指向FetchFailedException看着像网络问题实际是 shuffle service 没起来。排查方法在 NodeManager 节点上jps看有没有YarnShuffleService相关进程或者查 NodeManager 日志里有没有Initialized spark_shuffle。动态分配的参数也别照抄。minExecutors设 2 保底maxExecutors按前面算的容器总量来定executorIdleTimeout默认 60 秒如果你的作业有长时间的空闲间隙可以调到 120 秒避免反复申请释放带来的调度开销。6. 踩过的坑与排查速查表这一章是整篇里我最想写的部分。文档里能查到的配置项大家都会配真出问题的时候往往是几行日志里藏着的细节。下面是我在多个环境里攒下来的对照表。现象大概率原因处理方式Agent 在 CM 界面显示心跳丢失主机名与证书不一致或 Agent 进程挂了核对server_host与 FQDNsystemctl restart cloudera-scm-agentParcel 分发卡在 0%目标节点磁盘空间不足或校验失败查看/opt/cloudera/parcel-cache文件大小与 sha1spark-shell 启动即报Connection refusedHistory Server 未起或spark.eventLog.dir路径不可写检查 HDFS 目录权限hdfs dfs -ls /user/spark作业卡在 ACCEPTED 不进入 RUNNINGYARN 容器内存不足或队列资源被占满看 YARN 界面的队列使用率调整maximum-allocation-mbExecutor 频繁丢失节点内存溢出被 OOM Killer 杀掉查/var/log/messages里的 oom 记录下调 executor 内存FetchFailedException大量出现shuffle service 未启用或数据被清理确认 aux service 已加载检查keepAlive配置History Server 页面为空读写路径不一致对比spark.eventLog.dir和spark.history.fs.logDirectory作业执行极慢但 CPU 不高透明大页未关闭GC 抖动复核 THP 配置与swappinessCM 启动报数据库连接失败MySQL 隔离级别或max_allowed_packet不达标按前面章节核对my.cnf并重启 MySQL6.1 几点不写在文档里的实战经验第一别一次把所有服务全加上。很多人图省事CM 的安装向导里勾一堆组件结果某个服务起不来导致整个集群状态异常其他服务也跟着受影响。我的做法是分三批第一批 ZooKeeper HDFS确认 HDFS 能正常读写第二批 YARN确认能提交一个 mapreduce 的 sleep 作业第三批才上 Spark。每批之间留出验证时间出问题定位范围小得多。第二/etc/spark/conf下的文件不要手改。这条前面说过一次但值得再说一次因为它的后果很隐蔽你手工改了spark-defaults.conf调大了 executor 内存作业跑得好好的某天 CM 重启服务或者某个节点 Agent 重连配置回滚作业开始 OOM你完全想不起来中间发生过什么。所有变更走 CM 界面改完点部署客户端配置一键下发。第三日志保留策略要提前定。spark.history.fs.cleaner.enabled打开只是启用清理spark.history.fs.cleaner.interval和spark.history.fs.cleaner.maxAge决定清多快。默认 7 天如果你的作业量特别大7 天的 event log 可能上百 G。我一般配成 3 天并且在 HDFS 上给/user/spark/applicationHistory设配额防止把整个 HDFS 撑爆。第四spark.executor.cores和spark.executor.memory的配比有讲究。经验值是每个 core 配 2~4G 内存超过 5G 一个 core 容易导致 HDFS 客户端连接数过多。反过来core 太多而内存太少会频繁 spill。三节点的环境我通常用--executor-cores 2 --executor-memory 4g单个 executor 跑 2 到 3 个 task并发和 GC 压力比较均衡。6.2 影响范围与后续扩展这套环境落成之后能支撑的作业规模大概是几十 GB 级的数据量、十几个并发的 Spark 应用。往下游走最常见的扩展方向有三个接 Hive 打通数仓查询、加 Kerberos 做认证、把存储换成对象存储适配。其中 Kerberos 是最容易出问题的一旦启用所有提交作业的客户端都要有 keytabHistory Server 要配 principalSpark 的spark.yarn.principal和spark.yarn.keytab必须正确否则作业提交直接被拒。我个人在实际操作中的体会是CDH 6.2 这套东西的价值不在于组件本身多先进而在于它把版本兼容、配置管理、角色生命周期这堆杂活收敛到一个界面里。代价是你必须尊重它的规则不要在它管理的目录上手工改东西不要绕过 CM 去动服务配置。每次想就改一个小地方的时候先想一想下一次配置下发会不会把它覆盖回去。想清楚这一点这套环境能稳定跑很久。