云计算百科
云计算领域专业知识百科平台

20000 个文件跑了 11500 秒:你压的是 YARN 调度器,不是 HDFS

20000 个文件跑了 11500 秒:你压的是 YARN 调度器,不是 HDFS

关键词:TestDFSIO、HDFS 基准测试、YARN 调度器、maximum-offswitch-assignments、心跳间隔、容器分配速率、Hadoop 性能测试、TeraSort、小文件、基准测试设计

目录

  • 一、先说结论
  • 二、测试现场
  • 三、结果:五轮写入,稳定得不像 I/O
  • 四、第一反应:是不是磁盘不行
  • 五、转折:有人问了一句"分配 container 要多久"
  • 六、算账:集群每秒能发多少个任务
  • 七、三个参数锁死了调度速率
  • 八、1333 秒只是下限
  • 九、90% 的时间,磁盘在空转
  • 十、读取为什么快 2.1 倍
  • 十一、同一批测试里的 TeraSort
  • 十二、怎么改:少任务,大文件
  • 十三、如果真的要压小文件
  • 十四、复盘

一、先说结论

5 节点的 HDFS 集群,TestDFSIO 写 20000 个 100MB 文件,五轮平均耗时 11572 秒(约 3.2 小时),吞吐只有 177 MB/s。

第一反应一定是"磁盘不行"或者"网络不行"。

但算完一笔账之后,结论是另一个方向:

这批数据不能代表 HDFS 的性能。它测的是 YARN 控制平面的任务分发能力。

支撑这个结论的有四层证据:

  • 调度速率被三个参数锁死:全集群每秒最多分发 15 个容器,20000 个 task 光"发号"就要 1333 秒
  • 动态心跳是关闭的:节点没有任何加速汇报的可能,即使任务队列堆积如山
  • 五轮耗时极差只有 4.2%,读取 4 轮只有 1%:这种一致性是确定性瓶颈的特征,不是 I/O 抖动
  • 单任务粒度太小:每个 task 只写 100MB(约 1 秒的 I/O),却要走完整的容器申请与 JVM 启动流程
  • 一句话:

    你测到的不是"字节吞吐",是"任务吞吐"。


    二、测试现场

    2.1 命令

    hadoop jar hadoop-mapreduce-client-jobclient-3.3.3-tests.jar TestDFSIO -write -nrFiles 20000 -fileSize 100

    项值
    文件数 20000
    单文件大小 100 MB
    总数据量 20000 × 100MB = 2 TB
    集群规模 5 节点(2 NN + 3~5 DN/NM)
    写入轮次 5
    读取轮次 4

    -nrFiles 20000 这个参数,直接决定了这个测试会创建 20000 个 map task。

    而 20000 个 task,就是这个问题的起点。

    2.2 基准脚本

    #!/bin/bash
    # hadoop_benchmark.sh
    HADOOP_BIN="/opt/hadoop/bin/hadoop"
    TEST_JAR="/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.3.3-tests.jar"
    EXAMPLE_JAR="/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.3.jar"

    LOG_FILE="./benchmark_$(date +%Y%m%d-%H%M%S).log"
    CSV_FILE="./benchmark_result_$(date +%Y%m%d-%H%M%S).csv"

    RUN_TIMES=${1:-1}
    MODE=${2:-"dfsio-write"}

    echo "round,test_type,start_time,end_time,cost_seconds" > $CSV_FILE

    clean_hdfs() {
    $HADOOP_BIN fs -rm -r -skipTrash /test/terasort-input >/dev/null 2>&1
    $HADOOP_BIN fs -rm -r -skipTrash /test/terasort-output >/dev/null 2>&1
    $HADOOP_BIN fs -rm -r -skipTrash /benchmarks/TestDFSIO* >/dev/null 2>&1
    }

    run_test() {
    local i=$1
    clean_hdfs

    START_TIME=$(date +%s); START_HUMAN=$(date "+%Y-%m-%d %H:%M:%S")

    case $MODE in
    dfsio-write) $HADOOP_BIN jar $TEST_JAR TestDFSIO -write -nrFiles 20000 -fileSize 100 | tee -a $LOG_FILE ;;
    dfsio-read) $HADOOP_BIN jar $TEST_JAR TestDFSIO -read -nrFiles 20000 -fileSize 100 | tee -a $LOG_FILE ;;
    teragen) $HADOOP_BIN jar $EXAMPLE_JAR teragen 10000000000 /test/terasort-input | tee -a $LOG_FILE ;;
    terasort) $HADOOP_BIN jar $EXAMPLE_JAR terasort /test/terasort-input /test/terasort-output | tee -a $LOG_FILE ;;
    *) echo "[ERROR] Unknown mode: $MODE"; exit 1 ;;
    esac

    END_TIME=$(date +%s); END_HUMAN=$(date "+%Y-%m-%d %H:%M:%S")
    COST=$((END_TIME – START_TIME))
    echo "$i,$MODE,$START_HUMAN,$END_HUMAN,$COST" >> $CSV_FILE
    }

    for ((i=1; i<=RUN_TIMES; i++)); do run_test $i; done

    脚本本身没问题——清理、计时、落 CSV、多轮循环都有。

    问题不在脚本,在参数。


    三、结果:五轮写入,稳定得不像 I/O

    3.1 写入 5 轮

    轮次耗时(秒)
    1 11855
    2 11538
    3 11701
    4 11399
    5 11370
    平均 11572

    极差 485 秒,波动率 4.2%。

    3.2 读取 4 轮

    轮次耗时(秒)
    1 5324
    2 5360
    3 5304
    4 5341
    平均 5332

    极差 56 秒,波动率 1.05%。

    3.3 换算吞吐

    写入:2 TB ÷ 11572 s ≈ 177 MB/s
    读取:2 TB ÷ 5332 s ≈ 384 MB/s

    3.4 这里有个细节值得盯一下

    读取 4 轮的波动只有 1%。

    真实的磁盘 I/O 很难这么稳——页缓存命中率、后台 compaction、GC、网络抖动,都会让结果上下浮动个几秒到几十秒。

    1% 的波动率,说明瓶颈是确定性的、每次都会发生、且每次都消耗几乎相同的时间。

    这不是"磁盘状态有波动"的特征,是"每个任务都要走一遍固定流程"的特征。


    四、第一反应:是不是磁盘不行

    看到 177 MB/s,第一反应是排查硬件:

    检查项结果
    磁盘健康(SMART / iostat) 正常
    网络带宽 正常
    DataNode 存活 5 台都在
    副本数 / 块大小 默认,正常
    NameNode RPC 队列 不积压

    硬件层面没有明显短板。

    照这个方向继续查下去,会陷入"每块盘看起来都正常,但整体就是慢"的困境。


    五、转折:有人问了一句"分配 container 要多久"

    当时已经准备去测单盘 IOPS 了。

    然后被问了一个很简单的问题:

    “20000 个 map,光分配 container 要多久?跟 HDFS 性能有关系吗?”

    这句话把方向整个掰了过来。

    因为 TestDFSIO 的 20000 个文件 = 20000 个 map task,而每一个 task 都必须:

    排队 → 向 RM 申请容器 → 拿到容器 → 启动 JVM → 干活 → 回收

    "干活"只是这条链上的一环。

    如果这条链的入口每秒只能放进 15 个,那 20000 个就要 1300 多秒——这跟磁盘快慢一点关系都没有。


    六、算账:集群每秒能发多少个任务

    YARN 是拉取式调度:ResourceManager 不会主动把任务推给节点,必须等 NodeManager 发心跳来领。

    所以"每秒能派发多少任务"由三个东西决定:

    分配速率 = 计算节点数 × 心跳频率 × 单次心跳可分配容器数

    代入实测参数:

    计算节点数 : 3(5 节点集群中 2 台是 NN)
    心跳频率 : 1 次/秒(1000ms)
    单次可分配容器数 : 5

    分配速率 = 3 × 1 × 5 = 15 容器/秒

    6.1 20000 个任务需要多久才能"发完"

    20000 ÷ 15 ≈ 1333 秒 (约 22 分钟)

    如果 5 个计算节点全在(而不是 3 个):

    5 × 1 × 5 = 25 容器/秒
    20000 ÷ 25 = 800 秒 (约 13 分钟)

    光把任务派发下去,最少也要 13–22 分钟。

    6.2 这段时间里集群在干什么

    任务不是"一次性全发下去然后同时跑",而是:

    t=0s 发出第 1–15 个,开始跑
    t=1s 发出第 16–30 个
    t=2s …
    ↓
    前面发下去的任务早就跑完了(每个只写 100MB)
    后面的任务还在排队等号
    ↓
    集群的真实状态:跑一阵,等一阵

    集群不是被数据压垮的,是被"发号"的速度限制住的。


    七、三个参数锁死了调度速率

    这三个数字都不是拍脑袋,全部来自配置和源码。

    7.1 maximum-offswitch-assignments = 5

    yarn.scheduler.capacity.per-node-heartbeat.maximum-offswitch-assignments = 5

    这个参数的含义是:每个节点、每次心跳,最多分配多少个"非本地(off-switch)"容器。

    默认值就是 5。

    这是 Capacity Scheduler 的保护机制:防止一次心跳把大量容器塞给一个节点,造成节点过载和调度抖动。

    它就是那个"每秒最多领 5 个号"的来源。

    7.2 heartbeat-interval-ms = 1000

    yarn.resourcemanager.nodemanagers.heartbeat-interval-ms = 1000

    NodeManager 每秒向 RM 汇报一次。

    源码里它是硬编码的默认值:

    // YarnConfiguration.java
    public static final int DEFAULT_RM_NM_HEARTBEAT_INTERVAL_MS = 1000;

    7.3 heartbeat-interval-scaling-enable = false(最关键的一个)

    yarn.resourcemanager.nodemanagers.heartbeat-interval-scaling-enable = false

    这一项才是让人绝望的地方。

    YARN 其实提供了一个"动态心跳"机制:当队列堆积时,可以让节点加快汇报频率。

    # 动态心跳的相关参数
    yarn.resourcemanager.nodemanagers.heartbeat-interval-scaling-enable = false
    yarn.resourcemanager.nodemanagers.heartbeat-interval-min-ms = 1000
    yarn.resourcemanager.nodemanagers.heartbeat-interval-max-ms = 1000

    但 scaling-enable 默认是 false。

    也就是说:

    系统完全忽略 min-ms / max-ms 的调整逻辑,强制使用固定的 1000ms。

    NodeManager 没有任何加速汇报状态的可能——哪怕任务队列堆到两万个,它依然保持"每秒一次"的节奏。

    7.4 三个参数合起来

    scaling-enable = false → 心跳频率锁死在 1 Hz,不能加速
    heartbeat-interval-ms = 1000 → 每秒只有 1 次机会
    maximum-offswitch-assignments = 5 → 每次机会最多领 5 个
    ↓
    每个节点每秒最多 5 个容器
    ↓
    整个集群每秒最多 15 个(3 个计算节点)


    八、1333 秒只是下限

    1333 秒是"任务全部就绪、资源充足、完美匹配"情况下的理论最小值。

    实际还要叠加三块开销:

    开销量级说明
    AM ↔ RM 交互延迟 每次约 2 秒 ApplicationMaster 心跳与资源协商
    JVM 启动/销毁 1–3 秒/任务 每个 map task 都要起一个 JVM
    资源碎片等待 不确定 偶发性的等待,可能重试(speculative / retry)

    所以真实的"任务级固定开销"远大于 1333 秒。

    这也解释了为什么 11572 秒会出现在这么"整齐"的区间里——因为它不是被 I/O 决定的,是被这条流水线的节奏决定的。


    九、90% 的时间,磁盘在空转

    9.1 每个 task 到底干了多少活

    单文件大小 : 100 MB
    单个 task : 写 100 MB
    按单盘 100 MB/s 估算 : 约 1 秒

    每个 task 实际搬数据的时间,只有 1 秒左右。

    但每个 task 的完整生命周期是:

    ResourceManager 分配容器
    ↓
    NodeManager 创建容器目录
    ↓
    启动 JVM
    ↓
    加载 Hadoop 配置
    ↓
    建立 HDFS 连接(含 Kerberos 认证)
    ↓
    写入 100MB ← 干活的部分,约 1 秒
    ↓
    关闭文件、汇报状态、回收容器

    开销是干活的数倍。

    9.2 那真实带宽是多少

    按经验估算,这类"小文件 + 海量任务"的评测里,真正在写数据的时间通常只占总时长的 5%–10%。

    反推一下:

    总时长 11572 s
    有效 I/O 时间 ≈ 1157 s(按 10% 估)
    真实写入带宽 ≈ 2048000 MB ÷ 1157 s ≈ 1770 MB/s ≈ 1.7 GB/s

    注意这是推算值,不是实测值。 但它指出了一个量级差:

    如果把这个测试改对,量出来的结果可能是 1.7 GB/s 量级,而不是报告上的 177 MB/s。 差了将近十倍。

    而那 90% 的时间里,磁盘在干什么?

    在等。


    十、读取为什么快 2.1 倍

    读取 5332s,写入 11572s,比例 2.16 倍。

    这个比例同时被三层因素影响:

    层写入读取
    调度开销(共同下限) 约 800–1333s 约 800–1333s
    单任务开销 建文件、分配块、写 pipeline 只读,流程更短
    实际 I/O 2TB 写入 2TB 读取

    HDFS 本身"读快写慢"是正常的(写入要走 3 副本 pipeline,读取可以直接从最近副本读)。

    但有个反直觉的点:

    因为两边都垫着同一块固定的调度成本,"2.1 倍"这个比值是被稀释过的。

    也就是说,真实的磁盘读写性能差距,很可能比 2.1 倍更大——把这个固定成本扣掉之后,差距才会显出来。


    十一、同一批测试里的 TeraSort

    同一批基准里还跑了 MapReduce 计算性能:

    测试参数耗时
    TeraGen 10,000,000,000 行 4714 秒(约 1.3 小时)
    TeraSort 1 TB 741 秒(约 12.3 分钟)

    这两组数据的价值,不在于和 TestDFSIO 做性能对比(测试对象不同),而在于:

    集群在连续数小时的压力测试下,没有出现服务中断、节点掉线、任务大面积失败。

    一次测错的基准,数据不能用来评估性能,但可以证明"能撑住"。

    这也是这次测试唯一可靠的产出。


    十二、怎么改:少任务,大文件

    核心原则只有一条:

    单任务 I/O 时间 >> 单任务启动开销

    12.1 参数怎么改

    调整项原来改成理由
    文件数 20000 100 任务数降两个数量级,调度几秒内完成
    单文件大小 100 MB 10 GB 单任务 I/O 从 1s 提到 100s
    总数据量 2 TB 1 TB 测试时长可控

    12.2 新命令

    # 写入
    hadoop jar hadoop-mapreduce-client-jobclient-3.3.3-tests.jar TestDFSIO -write -nrFiles 100 -fileSize 10000MB

    # 读取
    hadoop jar hadoop-mapreduce-client-jobclient-3.3.3-tests.jar TestDFSIO -read -nrFiles 100 -fileSize 10000MB

    12.3 为什么这样就对了

    原来:20000 × 100MB
    单任务 I/O ≈ 1s,启动开销 ≈ 2s
    → 开销占比 ≈ 66%,调度耗时 ≈ 1333s

    改后:100 × 10GB
    单任务 I/O ≈ 100s,启动开销 ≈ 2s
    → 开销占比 ≈ 2%,调度耗时 ≈ 7s

    同样的思路,从"测任务吞吐"变成"测字节吞吐"。

    12.4 预期结果与判定标准

    指标预期
    调度开销 < 10 秒
    测试总时长 完全由磁盘写入速度决定
    吞吐 大幅提升,接近硬件物理极限

    判定方法:

    • 如果吞吐显著提升 → 之前的瓶颈确实是调度,结论成立
    • 如果吞吐几乎没变 → 这时候才轮到查磁盘、查网络、查副本策略

    十三、如果真的要压小文件

    有一种场景是合理的:你就是要评估"海量小文件"对集群的冲击。

    这时候要测的不是 HDFS 带宽,而是 YARN 调度器 + NameNode 元数据的能力。

    13.1 临时放开调度限制(测试后必须还原)

    # 开启动态心跳
    yarn.resourcemanager.nodemanagers.heartbeat-interval-scaling-enable = true

    # 允许最快 100ms 一次心跳
    yarn.resourcemanager.nodemanagers.heartbeat-interval-min-ms = 100

    # 单次心跳放开到 100 个容器
    yarn.scheduler.capacity.per-node-heartbeat.maximum-offswitch-assignments = 100

    改完之后,分配速率从:

    3 × 1 × 5 = 15 /s

    变成:

    3 × 10 × 100 = 3000 /s

    理论上快了 200 倍。

    13.2 三个必须提醒的地方

    ① 这组参数有风险

    maximum-offswitch-assignments 调大,会让单节点在极短时间内被塞入大量容器,可能造成:

    • NodeManager 本地磁盘 IO 打满
    • 容器启动风暴,JVM 争抢 CPU
    • 调度器抖动,影响其他队列

    ② 必须还原

    压测完立刻改回默认值。这类参数一旦遗留在生产环境,下一次流量高峰可能就是事故。

    ③ 不要用改参数来"美化" HDFS 带宽测试

    你如果只是想测磁盘,改参数是错误的方向——那是在改被测系统,而不是改测试方法。


    十四、复盘

    14.1 六个判断

    ① 基准测试的第一步,是算清楚"这套测试最快能跑多快"

    理论下限 1333 秒,实测 11572 秒 —— 先算天花板,才知道大头在哪。

    ② "结果太稳定"本身就是一个信号

    读取 4 轮波动 1%、写入 5 轮波动 4.2%。当一个本该有抖动的指标变得极其稳定时,说明瓶颈是确定性的,不是物理的。

    ③ 拉取式调度系统,天然有"发号上限"

    YARN、K8s、Mesos 都是拉取式。

    它们的能力上限不是"能跑多快",而是"每秒能派出去多少"。

    ④ 动态心跳默认关闭,是很多人不知道的一层

    heartbeat-interval-scaling-enable=false 意味着堆积不会触发加速。

    很多人以为"队列长了系统会自己加快",不会。

    ⑤ 任务粒度决定了你测的是"任务吞吐"还是"字节吞吐"

    20000 × 100MB 和 100 × 10GB,前者测调度器,后者测磁盘。

    任务数超过某个量级,被测对象就自动换了。

    ⑥ 测错的基准不是废数据

    这次 11500 秒不能评估 HDFS 性能,但证明了集群在 3 小时连续压力下稳定运行。

    很多测试的价值,不在它原本要测的方向上。

    14.2 下次做基准测试前的检查清单

    检查项怎么判断
    任务数是否合适 总任务数 ÷ 调度速率 应远小于预估总耗时
    单任务 I/O 是否够大 单任务 I/O 时间应 ≥ 启动开销的 10 倍
    是否已知调度速率 节点数 × 心跳频率 × 单次分配数
    动态心跳是否开启 heartbeat-interval-scaling-enable
    结果波动是否合理 波动过小 → 怀疑确定性瓶颈
    读/写是否分开 分开跑,不要混在一轮里
    是否动了生产参数 不应该动

    附:改进版脚本

    #!/bin/bash
    # hadoop_benchmark.sh —— 两段式:大文件测 I/O,小文件测调度
    HADOOP_BIN="/opt/hadoop/bin/hadoop"
    TEST_JAR="/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.3.3-tests.jar"
    EXAMPLE_JAR="/opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.3.jar"

    STAMP=$(date +%Y%m%d-%H%M%S)
    LOG_FILE="./benchmark_${STAMP}.log"
    CSV_FILE="./benchmark_result_${STAMP}.csv"
    RUN_TIMES=${1:-1}
    MODE=${2:-"dfsio-write-big"}

    echo "round,test_type,start_time,end_time,cost_seconds" > "$CSV_FILE"

    clean_hdfs() {
    echo "[INFO] cleaning HDFS…" | tee -a "$LOG_FILE"
    $HADOOP_BIN fs -rm -r -skipTrash /test/terasort-input >/dev/null 2>&1
    $HADOOP_BIN fs -rm -r -skipTrash /test/terasort-output >/dev/null 2>&1
    $HADOOP_BIN fs -rm -r -skipTrash /benchmarks/TestDFSIO* >/dev/null 2>&1
    }

    run_test() {
    local i=$1
    echo "" | tee -a "$LOG_FILE"
    echo "———- Round $i ($MODE) ———-" | tee -a "$LOG_FILE"
    clean_hdfs

    local START_TIME START_HUMAN END_TIME END_HUMAN COST
    START_TIME=$(date +%s)
    START_HUMAN=$(date "+%Y-%m-%d %H:%M:%S")

    case $MODE in
    # 大文件:测真实 I/O 带宽(推荐)
    dfsio-write-big) $HADOOP_BIN jar $TEST_JAR TestDFSIO -write -nrFiles 100 -fileSize 10000MB | tee -a "$LOG_FILE" ;;
    dfsio-read-big) $HADOOP_BIN jar $TEST_JAR TestDFSIO -read -nrFiles 100 -fileSize 10000MB | tee -a "$LOG_FILE" ;;
    # 小文件:测调度与元数据压力(对比用)
    dfsio-write-small) $HADOOP_BIN jar $TEST_JAR TestDFSIO -write -nrFiles 20000 -fileSize 100 | tee -a "$LOG_FILE" ;;
    dfsio-read-small) $HADOOP_BIN jar $TEST_JAR TestDFSIO -read -nrFiles 20000 -fileSize 100 | tee -a "$LOG_FILE" ;;
    # 计算性能
    teragen) $HADOOP_BIN jar $EXAMPLE_JAR teragen 10000000000 /test/terasort-input | tee -a "$LOG_FILE" ;;
    terasort) $HADOOP_BIN jar $EXAMPLE_JAR terasort /test/terasort-input /test/terasort-output | tee -a "$LOG_FILE" ;;
    *) echo "[ERROR] Unknown mode: $MODE" | tee -a "$LOG_FILE"; exit 1 ;;
    esac

    END_TIME=$(date +%s)
    END_HUMAN=$(date "+%Y-%m-%d %H:%M:%S")
    COST=$((END_TIME – START_TIME))

    echo "[RESULT] Round $i cost: ${COST}s" | tee -a "$LOG_FILE"
    echo "$i,$MODE,$START_HUMAN,$END_HUMAN,$COST" >> "$CSV_FILE"
    }

    for ((i=1; i<=RUN_TIMES; i++)); do
    run_test "$i"
    done

    echo "" | tee -a "$LOG_FILE"
    echo "=========== Completed. CSV: $CSV_FILE ===========" | tee -a "$LOG_FILE"

    用法:

    # 大文件写入(测 I/O 带宽)
    ./hadoop_benchmark.sh 1 dfsio-write-big

    # 小文件写入(对照:看调度影响)
    ./hadoop_benchmark.sh 1 dfsio-write-small

    # TeraGen + TeraSort
    ./hadoop_benchmark.sh 1 teragen && ./hadoop_benchmark.sh 1 terasort


    最后一句

    这次测试最有价值的产出,不是那 177 MB/s,而是想明白了一件事:

    当你测一个系统时,如果测试本身的开销超过了被测对象的开销, 你测到的就是测试框架,不是那个系统。

    下次跑基准之前,先花五分钟算一下"这套测试最快能跑多快"。

    五分钟的算术,能省下三小时的白跑。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 20000 个文件跑了 11500 秒:你压的是 YARN 调度器,不是 HDFS
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!