多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Hadoop集群资源利用率优化:内存、调度与数据形态实战

Hadoop集群资源利用率优化:内存、调度与数据形态实战 上个月帮一家客户排查Hadoop集群性能问题看监控第一眼就愣住了CPU平均利用率不到18%内存用了不到一半任务队列却每天都有作业在排队。机器配置不差业务方天天抱怨“集群太慢”运维照着参数文档调了一周效果像隔靴搔痒。这应该是很多大数据团队都遇到过的场景集群资源利用率优化难点从来不是“会不会敲调参命令”而是你能不能透过表象找到资源到底浪费在哪一环。这篇文章我会从内存、调度、数据形态、并行度几个层面拆开讲适合负责集群运维和调优的工程师也适合准备大数据面试的朋友。你会发现利用率低往往不是机器不够而是配置、数据和调度策略三者互相“锁死”的结果。1. 先说清楚利用率低通常不是机器不够而是资源被“锁死”和“空转”1.1 三类典型“低利用率”表象背后机理完全不一样先别急着改参数一定要先分清楚你遇到的是哪一类问题。我见过太多人拿着一份网上的“Hadoop性能调优手册”从头到尾改一遍最后CPU还是上不去内存还是空一半作业该排队还是排队。不同表象对应的是完全不同的病根混在一起调参只会越调越乱。第一类是“CPU和内存都低但作业在排队”。这时候资源明明有剩余新作业却进不来大概率是调度层的限制比如队列被切死、并行度上限被卡住或者容器最小分配单元设得太大导致资源碎片化。打个比方餐厅明明有空位但你把餐厅分成几个固定包间A包间坐满了B包间空着新来的客人只能在外面排队。第二类是“内存水位很高CPU却跑不满”。这种最迷惑人。内存看着紧张但算力闲着通常是容器内存设置过大导致单节点同时运行的Task数量太少物理CPU核数被白白浪费。举个具体例子16核64GB的节点你给YARN分配48GB内存如果每个Map容器要4GB那单机最多同时跑12个Map任务16个核里有4个全程空转。这还只是理论并发实际调度、IO等待一掺和浪费更多。第三类是“节点之间负载冰火两重天”部分节点CPU跑爆部分节点闲着。这种基本指向数据本地性差、机架感知没配好或者个别慢节点拖后腿。Map任务本来应该优先调度到数据所在节点但当副本分布、机架信息、任务分配算法配合不到位时数据在A节点任务被派到B节点跨网络拉数据CPU时间全耗在IO上资源利用率自然上不去。1.2 YARN的资源分配模型天生就有“碎片”盲区要理解资源利用率必须先理解YARN的抽象方式。YARN把每个节点的资源看成一个大池子按“Container”为单位切分每个Container包含一定内存和vCore。调度器分配资源时只能按整块整块来不能把半个Container分给两个任务。这里就出现一个非常典型的浪费点yarn.scheduler.minimum-allocation-mb。默认是1024MB意思是任何任务至少要申请1GB内存的Container。但如果你把mapreduce.map.memory.mb设成4GB那么哪怕一个任务只需要500MB内存调度器也只能在剩余资源中找4GB的整块给它。节点内存是固定的切了几块4GB之后剩下的碎片不够再开一个Container这部分资源就永久闲置了。你可以把它理解成一张只能坐4个人的桌子来了5个客人就得再开一桌结果两桌都只坐了一半人场地利用率惨不忍睹。所以调优的第一步不是照着某个最佳实践把参数改掉而是去算清楚单节点到底能跑多少个Container才算“不浪费”。计算公式很简单——节点可用YARN内存除以单个Container内存得出的并发数再去和物理CPU核数对比。比如48GB可用内存、单Container 2GB并发就是2416核机器上每个任务占1.73个核这个比例比较健康。如果单Container 4GB并发变成1216核只剩12个任务在跑四分之一算力直接蒸发。1.3 优化之前先拍一张“基线照片”这是我做任何集群调优的第一个习惯动手前先把现状量化。只靠YARN ResourceManager页面上的“Used Memory”判断不够那个数字只代表Container占了多少不代表物理CPU和内存的真实水位。你需要至少收集下面几项数据每个节点的物理CPU利用率通过Ganglia、Prometheus node_exporter或直接top采样每个节点的物理内存和Swap使用量防止看到YARN内存“健康”但物理内存早撑爆的情况队列的Pending任务数和Waiting时间反映调度层有没有堵点线上作业平均运行时长和资源申请量这是优化前后的核心对比指标拿到这些数据后再动参数每改一个配置隔48小时重新拍一次用数字验证效果。我在实际项目里见过不少团队改了十几个参数问效果如何只能回答“好像快了一点”这种状态没法持续优化。2. 内存配置一半内存被“预留虚高”另一半在等GC2.1 容器内存的“虚高”是怎么吃掉并发度的YARN上跑的MapReduce任务资源申请逻辑是这样的每个Map或Reduce Task在启动前ApplicationMaster会向ResourceManager申请一个ContainerContainer大小由mapreduce.map.memory.mb和mapreduce.reduce.memory.mb决定。这两个参数是“物理内存预算”而真正给JVM堆的是由mapreduce.map.java.opts里的-Xmx决定。很多人栽跟头的地方就在这里为了不让任务OOMmapreduce.map.memory.mb被设得很大比如8GB而JVM堆-Xmx也跟到6GB。从YARN页面看每个任务占8GB节点上同时只能跑6个任务48GB/8GB但实际每个任务JVM只用了6GB堆剩下2GB是给Native内存、网络缓冲和页缓存预留的。如果这6个任务连一半CPU都用不满那这2GB的“安全余量”就成了纯浪费。我给一个参考配比单Container内存2GB时-Xmx设在1536MB左右4GB容器时-Xmx设在3GB左右。核心思路是Container内存的75%-80%给堆剩下20%-25%留给堆外。这里的依据很简单——MapReduce的Map阶段有环形缓冲区shuffle bufferReduce阶段有归并排序和copy线程这些都要吃堆外内存预留太少会导致进程被NodeManager杀掉预留太多则白白降低并发度。2.2 JVM Overhead参数被忽视的连锁反应容器内存和JVM堆之间那个“看不见的夹层”官方叫“JVM Overhead”在YARN里没有直接参数对应但你可以通过mapreduce.admin.user.shell或直接看容器启动命令里的-XX:MaxDirectMemorySize之类的参数去感知它。这个夹层装的是什么主要是Direct Memory、JNI缓冲区、线程栈、以及Netty或Protobuf框架的分配。如果这个夹层给得太小你会遇到一个非常诡异的现象系统物理内存明明还有富余但任务容器被NodeManager杀掉日志里写着“Container killed by the ApplicationMaster”或“Remote RPC server”相关的内存超限。我排查过一次最后发现是某个任务大量使用堆外缓存Direct Memory涨得飞快把预留空间冲爆了。处理方式不是盲目往上加Container内存而是先弄清楚任务的内存画像再决定是调-Xmx还是给堆外留更多空间。2.3 虚拟内存比例的“假性OOM”再往下走还有一个经典大坑yarn.nodemanager.vmem-pmem-ratio。这个参数默认是2.1意思是NodeManager允许容器使用的虚拟内存是物理内存预算的2.1倍。本来这个设计是给JVM的虚拟内存保留余量但很多任务用了mmap之类的方式映射文件虚拟内存会夸张地涨到几十GB。这时候NodeManager会误判为“超过内存限制”直接把容器杀掉。排查方法很简单看系统日志里是“Container killed by the NodeManager”还是真正的Java OutOfMemoryError。前者十有八九就是虚拟内存比例不够。解决办法可以调整这个比例到3或4极端情况下设成-1关闭虚拟内存校验但不建议一上来就关——虚拟内存暴涨往往伴随着真实的内存压力关掉校验后物理内存被撑爆那才是真正的灾难。我在生产环境更偏好把比例放宽到3同时配合Swap使用量监控两头都能兜住。3. 调度策略队列切分不灵活高峰期资源互相“看得到吃不到”3.1 容量调度器队列设计的典型病根大多数生产集群用Capacity Scheduler但很多人把队列理解成了“分区”——root下面建一堆队列每个队列配一个固定capacity百分比然后任务就按业务往里扔。这么做最大的问题是白天临时查询任务把队列资源吃满晚上批量作业需要跑的时候发现另一个队列资源是空闲的但调度器就是不让跨队列用作业只能干等。合理的设计思路是队列之间要有“软上限”和“硬上限”的区别。capacity配置的是软上限表示正常情况下这个队列能占到多少maximum-capacity配置的是硬上限表示这个队列最多能挤占到多少。比如生产队列capacity设为50maximum-capacity设为80那么在集群整体繁忙时生产队列最多只能拿到80%但空闲时段它可以借用其他队列闲置资源。我见过一个客户案例三类队列分别按60、30、10切死结果夜间跑T1离线作业的生产队列只有60%资源而临时查询队列几乎空了也不释放。最后我把临时队列的maximum-capacity从30提高到70同时开启抢占夜间离线作业跑完之后临时查询依然能及时拿到资源集群整体利用率从40%拉到70%以上。核心不是把队列做大而是让“闲资源”能流动起来。3.2 抢占机制宁可杀掉重跑也不要让高峰死等Capacity Scheduler默认不抢占。也就是说即使A队列超用了且B队列急需资源A队列已经启动的Container也不会被回收除非任务自己结束。这导致高优先级作业永远被低优先级长任务堵住。开启抢占需要两步。第一步是打开ResourceManager的调度监控器对应配置是yarn.resourcemanager.scheduler.monitor.enable设为true。第二步是配置抢占策略和阈值。在Hadoop 3.x里可以通过yarn.resourcemanager.scheduler.monitor.policies指定ProportionalCapacityPreemptionPolicy再设置yarn.resourcemanager.monitor.capacity.preemption.observe_only先观察。这里我强烈建议先开observe模式跑几天看看日志里哪些Container会被标记为“可抢占”确认没有误伤关键任务再真正启用抢占。抢占的时间阈值也很讲究。设太短队列一紧张就立刻杀任务长任务反复被杀永远跑不完设太长高优先级作业等半小时都拿不到资源抢占失去意义。我在线上的做法是从yarn.resourcemanager.monitor.capacity.preemption.windows设成5000毫秒起步意思是持续超用5秒仍然不够再触发标记同时观察任务被杀频率再逐步调整。3.3 一个可以直接抄的队列配置模板下面给出一个适合中小规模生产集群的Capacity Scheduler配置片段配置文件为capacity-scheduler.xml核心思路是生产队列保证基础份额、允许弹性借用临时队列控制硬上限property nameyarn.scheduler.capacity.root.queues/name valueprod,adhoc/value /property property nameyarn.scheduler.capacity.root.prod.capacity/name value60/value /property property nameyarn.scheduler.capacity.root.prod.maximum-capacity/name value90/value /property property nameyarn.scheduler.capacity.root.adhoc.capacity/name value40/value /property property nameyarn.scheduler.capacity.root.adhoc.maximum-capacity/name value70/value /property property nameyarn.resourcemanager.scheduler.monitor.enable/name valuetrue/value /property看到没有prod队列虽然基础份额只有60%但硬上限到90%adhoc队列基础份额有40%硬上限却被压到70%。这样在凌晨prod作业排队时它可以一直借用到整个集群90%的资源而白天临时查询多的时候adhoc最多占用70%剩下的留给生产。这种“基础保证弹性借用”的组合比静态切死更能适应波动的业务节奏。4. 数据与任务形态小文件和数据倾斜才是隐性吞资源大户4.1 小文件是如何“悄悄”拖垮整个集群的很多时候你想尽办法调内存、调调度利用率还是上不去结果一查底层问题出在数据文件数量上。小文件对资源利用率的杀伤力主要体现在三个层面。第一层是NameNode内存。每个文件、每个Block在NameNode内存里都有对应的元数据对象一个文件大概150字节以上一个有副本的Block还要再占100多字节。1000万个1KB的小文件光元数据就是好几个GBNameNode内存吃紧整个集群的元数据操作都会变慢。第二层是MapTask数量失控。Hadoop里Map并行度由InputSplit决定每个Split基本对应文件的一个Block。如果数据由1000万个小文件组成跑一次全表扫描就要启动上千万个MapTask。每个Task启动一个JVM光启动就有1到2秒的开销再加上调度排队、shuffle连接集群资源全被这些“即生即死”的短命Task吃掉了真正的计算只占了一点点。第三层是任务历史文件的膨胀。太多小文件导致YARN的HistoryServer、LogAggregation处理压力变大日志清理变慢磁盘IO被元数据和日志操作牵着走。4.2 数据治理方案合并文件、换列存、控制动态分区产出处理小文件第一步是治源头。Hive里最常见的产生源是动态分区插入每插入一个分区就生成一批文件分区值又多文件数直接爆炸。我建议在Hive上打开自动合并SET hive.merge.mapfilestrue; SET hive.merge.mapredfilestrue; SET hive.merge.size.per.task268435456; SET hive.merge.smallfiles.avgsize16777216;这三个参数的意思是说Map-only和MapReduce任务结束后如果发现输出文件平均大小低于16MB就触发合并目标是每个任务输出约256MB的文件。256MB这个值不是拍脑袋定的它和HDFS默认Block大小128MB匹配两到三个Block左右一个文件后续读取时InputSplit数量合理MapTask启动成本摊薄。第二步是换存储格式。同样是2GB数据存成纯文本和存成ORC/Parquet对集群资源的消耗完全两个量级。列式存储自带压缩、谓词下推和索引过滤一个数仓分析任务扫描的数据量能降低一个数量级。配合Snappy或ZSTD压缩磁盘IO和网络传输压力也随之下降。这个优化对“CPU利用率低但IO饱和”的集群尤其明显因为扫描数据量减少后CPU才真正有余力去做计算。4.3 数据倾斜CPU看起来在跑实际上资源被“锁”在少数Task上说完小文件再来看数据倾斜。很多集群利用率低的假象其实是被倾斜任务“撑起来”的某个Reduce拿到全表80%的数据跑3小时另外几个Reduce 10分钟就结束了整个集群在这3小时里所有的Container都在等这个长尾Reduce完成。从YARN页面看内存占用是满的、CPU也一直在动但从业务角度绝大部分算力都被一个Task空耗掉了。定位倾斜的办法很简单。Hive跑完看每个Reduce的输入行数如果有一个Reduce明显远超平均基本就是Key分布不均。对Spark作业打开WebUI看同一个Stage里不同Task的Shuffle Read大小差距在十倍以上基本就能确认。处理倾斜可以在两个地方下手。一是SQL层面做两阶段聚合比如先给Key加随机前缀打散到一批Reduce做预聚合再去掉前缀做最终聚合二是开启框架的自动倾斜优化Spark 3.0以后的spark.sql.adaptive.skewJoin.enabled会自动把倾斜的Shuffle分区拆成多个小分区并行处理。我自己的经验是优先检查Join的关联键是否有大量默认值或空值比如一张表里某个业务字段大量为0直接把所有行都shuffle到同一批Reduce上这种Case加一个WHERE过滤或者把0值换成随机前缀效果立竿见影。5. 并行度与JVM参数相同代码跑出两倍耗时的差别在哪里5.1 Map和Reduce并行度到底怎么算资源利用率最终要落到“一个任务到底起多少个并行的Task”上。Map并行度由InputSplit决定而InputSplit数量大致等于输入文件大小除以Block大小。这里有个常见的误区为了“提高并行度”故意把Block设小一点以为MapTask多就是跑得快。结果恰恰相反MapTask过小的文件会导致每个Task只处理几十MB数据启动JVM的开销、任务调度开销、shuffle连接开销全部变成浪费。合理的目标是单个MapTask处理100MB到256MB之间这样既不会让启动开销占比太高也能让数据本地性充分生效。Reduce并行度则有另一种逻辑。如果最终输出不需要全局排序Reduce数量可以按“期望的每个Reduce输出文件大小”来估算。比如最终结果有4TB想让每个Reduce输出控制在512MB到1GB之间Reduce数就在4000到8000之间。但Reduce数也不能无限大——每个Reduce都会在shuffle阶段向所有MapTask拉取数据Reduce太多会让网络连接数膨胀Map端的内存和IO被并发连接压垮。我一般会参考集群可用vCore总数把Reduce数控制在“可用vCore的0.5到1倍”这个量级再结合输出体量做机动调整。5.2 推测执行到底是在省时间还是在烧资源另一个容易被忽视的问题是mapreduce.map.speculative默认情况下Map任务开推测执行。推测执行的逻辑是当某个Task运行明显慢于同一批其他Task时调度器会为它再启动一个相同的Task谁先跑完就用谁的结果另一个直接杀掉。从利用率视角看这个机制本质上是“用双倍CPU换时间”。在物理机统一、节点性能稳定的集群里推测执行经常是纯浪费——慢Task不是节点问题而是数据本身倾斜严重再推测一份也快不到哪去只是白白多烧一台机器的CPU。而在虚拟化、容器化、节点性能差异大的环境里推测执行又很有价值因为上游邻居负载可能导致某节点大幅变慢这时多跑一份反而能赢回时间。我建议在物理机集群里把mapreduce.map.speculative关掉同时保留Reduce推测执行默认关闭可按需开启。关闭后在YARN Application页面留意一下有没有“Killed”状态的Task如果因为慢节点导致大量Task被杀再重新打开也不迟但要设置合理的阈值参数避免一有波动就触发。5.3 压缩与序列化资源利用率里的“隐形杠杆”最后别忘了压缩这一层。Map和Reduce之间shuffle的数据量往往比最终落盘的数据量还大。开启Map输出压缩只需要两个参数property namemapreduce.map.output.compress/name valuetrue/value /property property namemapreduce.map.output.compress.codec/name valueorg.apache.hadoop.io.compress.SnappyCodec/value /property代价是Map端压缩和解压会额外吃掉一些CPU收益是shuffle网络流量和Reduce端的IO压力大幅下降。在带宽健康、CPU紧张的集群里这个优化要谨慎但在绝大多数大内存、多核但网络带宽一般的生产集群里开压缩通常利大于弊。压缩算法的选择也讲究Snappy是速度和压缩比的均衡点LZ4更快但压缩比略低ZSTD压缩比最好但CPU开销高。如果任务是IO密集型的优先选ZSTDCPU密集型则建议Snappy或LZ4。我把几个常用编码的典型表现整理成一个粗略对照表方便选型编码压缩比压缩速度解压速度CPU开销适用场景Snappy中等快快低默认均衡选择适合大多数分析任务LZ4较低极快极快极低高吞吐、CPU敏感场景ZSTD较高中等中等中高IO敏感、存储空间紧张场景Gzip最高慢慢高冷数据存储极少计算6. 从“拍脑袋调参”到“指标驱动”一套可复用的巡检流程6.1 日常巡检要重点盯哪几个指标调优不是一次性工程而是一个持续闭环。我给自己定了一个底线看一眼监控就能在三分钟内判断集群现在处于什么状态。核心就三个问题资源有没有空转作业是不是在排队哪个环节成了瓶颈对应到具体指标我会盯这几项队列Pending状态的数量、Container实际使用量、节点物理CPU平均水位、节点物理内存水位、HDFS DataNode的读写吞吐。命令行层面常用的检查包括# 查看所有节点资源与Container分布 yarn node -list -all # 查看指定队列的容量占用与Pending情况 yarn queue -status queue_name # 查看HDFS整体健康度和块分布 hdfs dfsadmin -report可视化层面Prometheus Grafana是一条成熟路径。NodeManager暴露的JMX接口里有ContainerUsage和AvailableResource之类的指标配合node_exporter采集物理CPU直接建一张“物理资源 vs YARN资源”的对比图。这种图最有价值——它能直接暴露出YARN认为“资源已用满”但物理CPU只有20%的错配问题。6.2 一次典型优化案例的完整复盘拿我最近处理的一个案例当示范。现象是集群CPU平均15%但每天下午任务高峰期生产队列队列深度超过几百个作业排队。最初运维以为是机器不够计划加节点我拦住了。排查链路是这样的第一步看yarn queue -status发现生产队列长期处于“资源明显不足”状态临时队列倒是空空荡荡。第二步看节点的Container分布发现所有节点上的Container数量远低于理论并发上限——说明不是机器不够而是队列资源切分太死。第三步查看单个作业的AM日志确认作业等待是在队列层面而不是节点资源层面。到这里基本确认问题出在maximum-capacity没有合理配置生产队列的硬上限被设得跟基础容量一样队列之间的资源完全不能流动。调整方案就是3.3里那个模板生产队列的maximum-capacity拉到90临时队列压到70并开启抢占。调整后48小时生产队列平均等待时间从35分钟降到5分钟以内集群CPU平均利用率从15%升到45%没有再新增节点。这个案例里我唯一动的东西就是两个XML配置项。整个过程遵循一条原则每次只动一个变量观察48小时。如果同时改了内存、调度、队列出了问题根本分不清是哪一项惹的祸。6.3 个人经验调优的优先级先内存、再调度、后数据踩过这么多坑之后我形成了一套固定的优化顺序。第一阶段先查内存配置因为容器的内存大小直接决定并发度上限这是整个集群资源利用率的根基第二阶段查调度策略让资源能流动起来第三阶段才去治理数据问题因为小文件和倾斜一旦形成单靠改参数只是治标得从数据生产链路上去调。如果只想快速见效我会优先处理两件事一是小文件合并把数据文件控制在合理粒度二是队列弹性调度把maximum-capacity和抢占配好。这两项几乎在所有集群上都能看到立竿见影的提升。最后再分享一个小技巧调优前后把资源利用率、作业平均耗时、队列等待时间三个数字打印出来贴在工位上。不要凭感觉判断“优化有没有效”只看数字。因为一个集群的资源利用率本质上就是这几个数字背后的资源配置、数据形态和调度策略在打架的结果。你每一次改动最后都会反映在它们身上。
返回列表