ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spark与Hadoop对比:计算模型、选型与生产实践

Spark与Hadoop对比:计算模型、选型与生产实践 1. 先拆掉那堵墙Spark 和 Hadoop 到底是什么关系入行大数据这些年我被问得最多的一句话就是“Spark 是不是比 Hadoop 快所以我们直接用 Spark 就行”这个问题背后的混乱几乎每个团队都经历过。严格来说这种问法本身就暴露出一个关键误解Hadoop 根本不是“一个东西”而是一个生态家族Spark 也不是 Hadoop 的替代品它只是家族里一个更能干的计算引擎。用个生活化的比方。Hadoop 好比一套完整的厨房系统——有储藏室HDFS、有操作台YARN、有一本祖传菜谱MapReduce还有一堆配套工具Hive、HBase、Zookeeper 等。而 Spark 是一台多功能料理机它确实能把菜做得更快但前提是你得有食材数据、有地方放食材存储层并且有人告诉你放多少水、转几分钟调度与资源管理。所以真实的生产环境里Spark 和 Hadoop 从来不是二选一而是深度绑定。很多新人在第一次接触这两个词时会误以为它们是同一层面的竞品。这个误会的根源在于Hadoop 的文档里 MapReduce 占了大量篇幅而 Spark 的教程又动不动就“集群模式”“HDFS 读写”导致大家天然觉得它们在抢同一个生态位。但从架构角度看Spark 和 Hadoop 的关系更多是“寄生”与“协同”——Spark 读写 HDFS 作为存储底座Spark 跑在 YARN 上作为计算调度Spark SQL 又能无缝对接 Hive 的元数据。这不是竞争这是标准的黄金搭档。这篇文章我会从底层计算模型入手讲清楚两者性能差异的根源再结合我在生产环境中做过的日志分析、用户画像、离线报表等项目给出选型判断标准和一个完整的 Spark on YARN 落地案例。如果你正在纠结“团队到底该上 Hadoop 还是 Spark”“两套都要的话怎么分工”这篇应该能给你一个相对清晰的答案。2. MapReduce 与 Spark 计算模型性能差距到底差在哪2.1 中间结果落盘 vs 内存管道最根本的分水岭我们得先回到计算模型本身。MapReduce 的核心流程是 Map 阶段 → Shuffle 阶段 → Reduce 阶段每个阶段结束之后中间结果几乎都要写入磁盘。为什么这么做因为这套架构出生的年代机器内存以 GB 为单位磁盘廉价且可靠为了保证任务挂掉之后能从断点恢复落盘是最稳妥的方案。但代价也肉眼可见一个简单的 WordCount 任务map 输出要落一次盘shuffle 期间的排序合并要落一次盘reduce 拿到数据可能还要落一次。三次磁盘 I/O 算下来大量时间都耗在序列化、反序列化和磁盘读写上。如果数据量上了 TB 级这种设计就是灾难。Spark 走的是完全不同的路。它采用DAG有向无环图计算模型把一个任务拆成若干 Stage每个 Stage 内部尽可能把多个算子串联成一条流水线。数据在内存里以 RDD 分区为基本单位算子之间不需要落盘同一 Stage 内的 map、filter、flatMap 直接在一个内存管道里完成。只有当跨 Stage 发生 shuffle 时才需要把中间结果写到本地磁盘。我用一个非常直观的数据来说明差距。同样的 TPC-DS 基准测试在 100GB 数据规模下Spark SQL 的查询耗时通常是 Hive on MapReduce 的10 到 50 倍。这还是在 Spark 没有做任何调优的情况下。如果你把 Kryo 序列化、数据压缩、内存优化全部打开差距还会进一步拉大。当然这个数字不是绝对的——MapReduce 在极端简单的任务上也能跑出不错的成绩但绝大多数真实业务场景里Spark 的优势是压倒性的。2.2 DAG 调度与懒执行为什么 Spark 更加“聪明”MapReduce 的每次作业Job之间是完全独立的上一个 Job 的产出必须落盘下一个 Job 再重新读入。打个比方你让一个实习生整理一批文件他每完成一步就要把文件放回档案柜下一步再重新拿出来——这不仅慢而且每一步之间没有任何全局优化空间。Spark 的执行计划则是由 Driver 端统一构建的 DAG。它会在真正执行之前做两件 MapReduce 做不到的事第一逻辑优化。Spark SQL 执行前会经历 Catalyst 优化器它会自动做谓词下推、列剪裁、常量折叠等优化。举个例子你对一张 100 个字段的表做SELECT col_a FROM tbl WHERE date 2024-01-01Catalyst 会在扫描阶段就直接把 99 个没用的字段剪掉只读需要的列。而 MapReduce 的 Hive 在早期版本里是会傻乎乎地把整行数据都读出来再过滤的。第二物化策略优化。DAG 调度器会根据算子之间的依赖关系决定哪些 Stage 可以流水线执行哪些必须 shuffle哪些 RDD 需要 cache 到内存供后续复用。你可以显式调用.cache()或.persist()把一个被多次复用的数据集固定在内存里。这在迭代计算、交互式查询场景下效果极其明显——因为不需要反复从磁盘读取同一份数据。就拿我们之前做过的 ALS 协同过滤推荐来说MapReduce 实现每轮迭代都要完整地跑一个 Job从 HDFS 重新读数据、计算、写回一个 5 轮的迭代任务要花 2 小时换成 Spark MLlib 之后同样的数据规模压缩到 15 分钟内而且代码量减少了一半。这就是 DAG 调度带来的范式差异。2.3 Shuffle 机制与容错策略各有取舍谈到 shuffle很多人以为 Spark 一定比 Hadoop 好其实未必。MapReduce 的 shuffle 虽然慢但它足够稳定——它的排序是全局的、确定性的处理数据倾斜的方式也很简单粗暴增加 reduce 数量或者靠 Combiner 做预聚合。Spark 的 shuffle 默认基于 Hash 分区虽然快但一旦数据分布不均就很容易出现某个 Executor 撑爆内存的 OOM 问题。容错策略上两者走的也是不同路线。MapReduce 的容错粒度是“任务级别”——某个 Map 任务挂了单独重跑这个任务即可因为中间结果都落盘了。Spark 的容错则依赖RDD 的血缘 (Lineage)——一个分区数据丢失了就根据血缘关系重新从父 RDD 算一遍。这条线路的优势是省掉了持久化的开销但在血缘链特别长、或者某个 Stage 特别昂贵的情况下重算代价也不小。所以生产里我们经常结合 checkpoint 机制在某些关键 Stage 后手动把 RDD 写到 HDFS 上做快照牺牲一点速度换取恢复效率。这里想强调一个很多初学者没意识到的事实Spark 的“快”建立在足够的内存之上而这恰恰是它在云环境里成本更高的原因。当你评估“要不要从 Hadoop 迁移到 Spark”时CPU 不是主要瓶颈内存和网络带宽才是。内存不够Spark 会退化为反复 GC 甚至 OOM性能可能还不如 MapReduce 稳定。3. 选型不是站队什么场景该用 Hadoop什么场景该用 Spark3.1 十类典型业务场景的适用性对照我在不少企业的技术评审会上看到过类似争论架构师拍板“我们全面转向 Spark”然后运维在台下苦笑——因为批处理里的 ETL 清洗任务Hive on MapReduce 跑了三年都没出过问题有什么必要为了“用新技术”而换引擎反过来也有人非要在 Spark 上跑 5MB 的小数据集结果光启动 Executor 的时间就比整个计算时间长纯粹是浪费资源。真实世界的选型逻辑从来不是“哪个先进用哪个”而是“哪个更匹配你的负载特征”。我根据自己的实践经验整理了一张对照表可以在方案预选时直接参考场景Hadoop/MapReduce 适用性Spark 适用性推荐选择TB 级以上的批量 ETL稳定但耗时可观内存充足时效率极高优先 Spark百 GB 内的临时分析略慢可接受快且交互性好优先 Spark迭代式机器学习算法每轮都落盘几乎不可用内存复用天然契合必须 Spark流式数据处理原生不支持Structured Streaming 可用必须 Spark超大表 JOINshuffle 稳定但慢可能 OOM需调优视内存和倾斜情况定冷数据归档/存储HDFS 是核心底座不适用保留 HDFS实时查询/即席分析延迟太高Spark SQL 快很多优先 Spark数据仓库分层建设Hive 成熟稳定Hive on Spark 效率更高建议 Spark少量文件的小任务可用启动开销大反而不划算用 Hadoop 即可数据湖/存算分离架构HDFS 继续做存储计算层选 Spark二者结合3.2 非要用 Spark 的硬性指标如果你正在做一个架构选型最需要搞清楚的一件事是什么情况下 Spark 是“必须”而不是“可选”根据我的实战经验下面这几个硬指标只要踩中一个你大概率就绕不开 Spark指标一算法需要多轮迭代。机器学习、图计算、推荐系统这类任务天然就是迭代式。比如 K-Means 聚类要跑几十轮收敛逻辑回归要反复计算梯度。Spark 可以在一轮迭代里把需要复用的权重向量、特征矩阵 cache 在内存里Hadoop 每一轮都得全部从 HDFS 重新读入时间成本直接拉满。指标二交互式即席查询。业务方说“我改个过滤条件重新跑一下看看结果”MapReduce 的典型响应时间是几分钟到几十分钟受 Job 启动开销和调度延迟影响。Spark SQL 使用Thrift Server提供常驻服务查询复用 Session 和缓存秒级响应是常态。我们在给运营团队做的自助分析平台里后端就是 Spark SQL Thrift Server配合预热的 Hive 表能把 90% 的查询控制在 10 秒内。指标三需要复杂的数据管道。一段数据管道里有不同的分析场景比如读取 HDFS 文件后做一次过滤再按用户维度聚合再分别输出到多个目标源。Spark 的 DAG 调度会根据血缘关系做最优执行MapReduce 则会拆成多个独立的 Job每个 Job 都要排队等待调度。3.3 保留 Hadoop 生态的不可替代部分选型时最容易犯的错误就是“一刀切”——把 Hadoop 整个生态都排除了。事实上Hadoop 里有几个组件是 Spark 完全替代不了的HDFSSpark 计算一百次数据还是得存 HDFS 上。它提供的高容错存储、多副本机制、跨节点数据分布是 Spark 运行的数据底座。YARN虽然 Spark 也能用 Standalone 模式跑但要让 Spark 和 Hive、MapReduce、Flink 等共享一个集群的资源YARN 是更成熟也更省心的资源调度方案。ZookeeperHadoop 集群的 NameNode 高可用依赖它HBase 的 RegionServer 协调也依赖它。Spark 自身虽然不强制要求但一旦跟 HBase、Kafka 等生态组件集成Zookeeper 基本绕不开。我们团队有一条不成文的原则存储用 HDFS调度用 YARN查询和计算首选 Spark只对极少数轻量级任务保留 Hive on MapReduce 执行引擎。这个组合既发挥了 Spark 的性能优势又稳住了 Hadoop 生态的可靠性和易运维性。4. 生产级 Spark on YARN 部署从资源规划到三步走落地4.1 为什么生产环境优先选择 Spark on YARN技术选型落到部署层面第一个决策就是“Spark 跑在哪种模式”。Standalone 模式部署简单适合学习和测试Mesos 模式现在用的人越来越少了Kubernetes 模式在云原生环境里前景很好但运维门槛高而且与 HDFS 的数据本地性调度还不够成熟。我推荐生产环境老老实实用Spark on YARNyarn-client 或 yarn-cluster原因有三第一是资源统一管理。一套 YARN 集群上可以同时跑 Spark、Flink、MapReduce 任务资源按队列隔离不会出现“Spark 把内存吃光导致 Hive 没法跑”的尴尬局面。第二是高可用和安全性。YARN 的 ResourceManager 支持主备切换且已深度对接 Kerberos 认证体系——这在政企和金融机构几乎是硬性要求。第三是简化运维。Spark 的 Driver 和 Executor 生命周期由 YARN 统一管理不像 Standalone 模式需要额外部署 Master/Worker 进程集群扩容时只需要在 YARN 节点上准备好 Spark 客户端和依赖包即可。4.2 资源规划与内存参数计算不看这篇你迟早踩 OOM 的坑Spark 性能调优里内存配置是重中之重。规划时最核心的公式是单个 Executor 可用的堆内存 spark.executor.memory - spark.memory.offHeap.size - 系统预留约 300MB~600MB而spark.executor.memory里面又分为Execution 内存用于 shuffle、join、aggregation和Storage 内存用于 cache RDD两者通过spark.memory.fraction默认 0.6共享。很多人一上来就把spark.executor.memory调得很大比如 32GB但忽略了 JVM 的 G1GC/ParNew 在超大堆下的停顿问题。我在实践中踩过这个坑一个 64GB RAM 的 Worker 节点我把 Executor 内存设为 48GB结果 Full GC 频繁到任务直接卡死。比较稳妥的做法是单节点 Worker 内存 64GB配置 2 个 Executor每个spark.executor.memory24gspark.executor.cores8预留 16GB 给操作系统页缓存和 YARN NodeManager。单节点 Worker 内存 128GB配置 4 个 Executor每个spark.executor.memory28gspark.executor.cores7。Executor 内存不宜超过 32GB超过后 JVM 对象指针压缩失效内存占用会显著上升GC 停顿也不可控。宁可多开几个小 Executor不要开一个巨无霸。再解释一个经常被忽略的参数spark.memory.fraction。默认 0.6 意味着 Executor 堆内只有 60% 的内存用于 Spark 自身管理剩下的 40% 留给 RDD 对象、用户代码和 JVM 结构。如果你的任务以缓存为主比如反复迭代读取同一份大 RDD可以把这个比例调到 0.7如果任务以 shuffle 为主大量 join、groupBy反而要调低一些留更多堆内存给 shuffle 缓冲区。4.3 部署步骤从 Hadoop 集群到 Spark 跑通任务假设你已经有一套可用的 Hadoop 集群HDFS YARN下面是从零把 Spark 接到 YARN 上的标准流程第一步下载并解压 Spark 二进制包。选择与你的 Hadoop 版本兼容的 Spark 版本。以 Spark 3.5.x Hadoop 3.3.x 为例下载spark-3.5.0-bin-hadoop3包解压到/usr/local/spark。注意不要贪新建议看 Spark 官方文档的 Compatibility Matrix避免出现不兼容的坑。第二步配置核心文件。主要改两个文件。spark-env.sh里设置 Java 路径和 Hadoop 相关环境变量export JAVA_HOME/usr/local/java export HADOOP_HOME/usr/local/hadoop export HADOOP_CONF_DIR$HADOOP_HOME/etc/hadoop export SPARK_HOME/usr/local/spark export SPARK_DIST_CLASSPATH$(hadoop classpath)spark-defaults.conf里配置资源上限和核心参数spark.masteryarn spark.yarn.am.memory2g spark.executor.instances10 spark.executor.memory24g spark.executor.cores8 spark.driver.memory8g spark.serializerorg.apache.spark.serializer.KryoSerializer spark.sql.shuffle.partitions200 spark.dynamicAllocation.enabledtrue spark.dynamicAllocation.initialExecutors5 spark.dynamicAllocation.minExecutors5 spark.dynamicAllocation.maxExecutors50第三步提交测试任务。/usr/local/spark/bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ --driver-memory 4g \ --executor-memory 8g \ --executor-cores 4 \ /usr/local/spark/examples/jars/spark-examples_2.12-3.5.0.jar \ 10如果能看到 YARN 的资源申请记录和最终输出的 Pi 值说明 Spark on YARN 已经打通了。接下来就能把你的业务代码包jar 或 Python 脚本提交上去了。注意部署模式cluster和client最大的区别是 Driver 进程跑在哪里。cluster模式下 Driver 由 YARN 的 AppMaster 托管适合生产环境定时调度client模式下 Driver 在你的提交机上更适合调试阶段实时看日志。我建议调试用client生产全部走cluster。5. 一套可复用的日志分析管道Hadoop 存数据Spark 算数据5.1 整体链路设计与数据模型聊完理论就得上点真东西。我拿一个之前做过的用户行为日志分析项目做例子这套管道从数据采集到最终报表完整体现了“Hadoop 存储 Spark 计算”的经典组合。数据链路是接收集群 NGINX 的访问日志日均约 5 亿条原始数据 600GB 左右→ Flume 写入 HDFS 的原始日志目录 → Hive 建外部表 → Spark 定时任务做清洗、解析、聚合 → 结果写回 Hive 分区表 → 上层报表工具查询。这个设计里最关键的决策是分层存储。HDFS 上分三个层/data/raw/nginx_log/原始日志按天分区保留 30 天。这里除了数据本身不建议做任何处理保证数据不缺不重随时可以追溯原始信息。/data/dwd/user_behavior/清洗后的明细数据Spark 做了解析、过滤、规范化。按天和小时分区。/data/ads/user_metrics_daily/按用户维度聚合的日报表直接被 BI 工具读取。对应的 Hive 外部表分别为ods_nginx_log、dwd_user_behavior、ads_user_metrics_daily。这套模型的好处是ODS 层存的是“事实”DWD 层是“可用的事实”ADS 层是“聚合后的结论”每一层的错误都不会污染上一层。5.2 Spark 清洗与聚合的代码骨架清洗任务用 Spark SQL 写起来非常简洁。下面是一个简化版的每日清洗任务核心逻辑from pyspark.sql import SparkSession from pyspark.sql.functions import col, regexp_extract, to_date, hour spark SparkSession.builder \ .appName(nginx_log_etl) \ .enableHiveSupport() \ .getOrCreate() # 读取前一天 HDFS 上的原始日志 input_path /data/raw/nginx_log/2025-01-20 raw_df spark.read.text(input_path) # 用正则解析 Nginx 日志 parsed_df raw_df.select( regexp_extract(value, r^(\S), 1).alias(remote_addr), regexp_extract(value, r\[([^\]])\], 1).alias(request_time), regexp_extract(value, r(\S)\s(\S)\s([^]), 2).alias(request_path), regexp_extract(value, r(\S)\s(\S)\s([^]), 1).alias(http_method), regexp_extract(value, r\s(\d{3})\s, 1).alias(status_code), regexp_extract(value, r([^]*)\s*$, 1).alias(user_agent) ) # 清洗过滤异常数据 cleaned_df parsed_df.filter( (col(status_code) ! ) (col(request_path).rlike(^/api/)) (col(request_time) ! ) ) # 写回 DWD 层 Hive 分区表 cleaned_df.write \ .mode(overwrite) \ .partitionBy(dt, hh) \ .format(parquet) \ .saveAsTable(dwd_user_behavior)需要注意的是partitionBy的字段要在写入前取出来比如解析请求时间里的日期和小时。我这里为了代码整洁省略了这一步实际生产里必须先withColumn(dt, to_date(col(request_time)))再分区写入。聚合任务也很直观。假设运营要按天统计每个用户的访问次数、独立 IP 数和高频接口 TOP10daily_metrics spark.sql( SELECT user_id, COUNT(*) AS pv, COUNT(DISTINCT remote_addr) AS uv, COUNT(DISTINCT request_path) AS api_cnt, dt FROM dwd_user_behavior WHERE dt 2025-01-20 GROUP BY user_id, dt ) daily_metrics.write \ .mode(overwrite) \ .partitionBy(dt) \ .format(parquet) \ .saveAsTable(ads_user_metrics_daily)这段 SQL 在 Spark 里跑完600GB 原始日志的清洗加聚合我们当时用了 20 个 Executor每个 24GB 内存大约 25 分钟。如果换成 MapReduce同样的数据量经验上至少要 4 小时以上。这个差距在企业日常报表场景里非常致命——当天数据如果跑不出来管理层第二天早上就拿不到前一天的经营分析。5.3 调度与依赖管理生产环境的工程化细节上面的代码只是单机跑通。真正的生产环境还需要解决“任务什么时候跑、跑挂了怎么办、结果对不对”这三个问题。我推荐用Apache DolphinScheduler或 Azkaban来做工作流调度。典型的工作流长这样ODS 层检查任务判断 HDFS 上/data/raw/nginx_log/2025-01-20目录是否存在且文件数大于阈值。Spark 清洗任务依赖第 1 步成功执行上面的 ETL 脚本。数据质量校验任务SQL 统计 DWD 表里的记录数、空值比例如果异常则发告警并阻断下游。Spark 聚合任务依赖第 3 步成功生成 ADS 层数据。报表数据导出任务把 ADS 表的数据同步到 MySQL/ClickHouse供 BI 查询。这套流程加上失败重跑、告警通知机制才是一个能交给运维同学安心睡觉的管道。很多人把 Spark 写得出神入化但整个数据管道一到凌晨就挂原因不在 Spark 本身而在调度和监控没跟上。生产环境里数据管道工程化的稳定性远比某个算子的执行速度重要。6. 真实踩坑记录OOM、小文件和 SparkHive 协作的暗坑6.1 Executor OOM 的排查链路我之前是怎么一步步定位的先说一个我们线上最典型的 OOM 事故。某天凌晨三点的离线任务突然大面积失败错误信息清一色是java.lang.OutOfMemoryError: Java heap space。如果是新手这时候第一反应就是调大spark.executor.memory——但这往往治标不治本甚至会让 OOM 来得更晚一点但更猛烈。我当时的排查链路是这样的第一步先看 Spark UI 上的 Stage 详情。发现 OOM 集中发生在某个 join 操作的 Shuffle Read 阶段而不是数据读取或计算阶段。这就把怀疑范围缩小到了“shuffle 数据倾斜”或“join 键分布不均”。第二步用spark.sql.adaptive.coalescePartitions.enabled和动态资源分配日志确认 Executor 数量。发现某个 Executor 上的 Shuffle Read 数据量是其他 Executor 的 30 倍——典型的热键问题。第三步检查 join 键的分布。因为我们的场景是用设备 ID 关联用户行为有一批老设备 ID 占了所有数据的 80%——这些 ID 对应的日志量巨大所以按哈希分区时全部挤到了同一个分区。最终修复方案是给 join 加了一个Salt 前缀打散技巧把设备 ID 后加一个 0~9 的随机后缀join 时先关联打散后的临时键最后再按真实设备 ID 做二次聚合。这个方法在绝大多数热点倾斜场景里都有效。另外还有一个常用配置是spark.sql.shuffle.partitions当数据量不大时把它调小比如 50可以减少分区数、降低每个分区内的哈希碰撞概率。这里想提醒大家OOM 不是简单的“内存不够”它大概率是“某一块内存被不均匀地塞满了”。只看总量不看分布永远修不到根上。6.2 HDFS 小文件问题Spark 写数据时容易忽视的定时炸弹Spark 是个“制造小文件”的能手。尤其是你用.repartition(500)然后再写入 Hive 分区表时每个分区可能对应 500 个文件时间一长HDFS 上堆满了 10KB、20KB 的小文件。NameNode 要管理所有文件元数据几百万个小文件直接造成内存压力甚至让整个集群进入保护模式。这个问题我在很多团队都遇到过根本原因是 Spark 写文件时分区数由最终 Stage 的并行度决定而不是由你的预期文件数决定。解决办法有以下几种按优先级排序第一写 Hive 表前先repartition(分区个数)或coalesce(更少的分区)。比如你的 T1 日报表最终只想生成 20 个文件那就在写之前把分区数压到 20底层的文件数量会直接减下来。第二如果已经产生大量小文件用Hive 的INSERT OVERWRITE ... SELECT重新合并一次或者用 Spark 读一遍再写一遍的方式做文件重整。我比较习惯的做法是在 DWD 层任务里加一个“文件瘦身”子任务定期把目录下的小文件合并成大文件。第三尽量避免dynamicPartitionOverwrite误删整个分区。开启spark.sql.sources.partitionOverwriteModestatic可以避免你在覆盖某个分区时把其他分区的文件也清掉。这个坑我们栽过一次第二天 BI 组的人来问“为什么昨天的数据没了”排查半天发现就是分区覆盖模式设置不对。6.3 Spark 读 Hive 表和直读 HDFS 的差异最后聊一个容易踩的隐性差异Spark SQL 读 Hive 表走的是Hive Metastore 的元数据读 HDFS 目录则是直接列式扫描 Parquet 文件。前者能拿到表的 schema、分区信息、统计信息所以查询规划更智能后者则是你给的路径里有什么文件就扫什么文件。所以我的建议是如果上游数据最终要提供给报表工具使用尽量建好 Hive 外部表让所有计算统一走spark.sql。先建表CREATE EXTERNAL TABLE IF NOT EXISTS dwd_user_behavior ( remote_addr STRING, request_time STRING, request_path STRING, user_id STRING ) PARTITIONED BY (dt STRING, hh STRING) STORED AS PARQUET LOCATION /data/dwd/user_behavior;然后做MSCK REPAIR TABLE或ALTER TABLE ... ADD PARTITION把 HDFS 上的分区注册到元数据里。这样 Spark 就能利用 Hive 的元数据信息进行列剪裁和分区剪裁效率跟在 Hive 里跑是同一水平。但要注意范式不一致的问题同一个 HDFS 目录如果被外部程序比如 Flume 直接写文件改了文件格式Hive 元数据不会自动感知。所以生产上要约定文件写入必须通过 Spark SQL 或 Hive 任务完成任何绕过元数据的写入都要先重建分区。6.4 参数调优里的几个容易忽略的“小陷阱”第一个陷阱是序列化方式。默认 Java 序列化虽然兼容性最好但性能实在拉胯。我的建议是强制开启 Kryo并注册需要用到的类spark.conf.set(spark.serializer, org.apache.spark.serializer.KryoSerializer) spark.conf.set(spark.kryo.registrationRequired, false)开启后shuffle 和 cache 数据的序列化大小通常能缩小到 Java 序列化的 1/5 到 1/10。在 shuffle 量很大的场景里这个配置带来的提速比加内存还明显。第二个陷阱是 GC 策略。默认的 Parallel GC 在超大数据集上容易频繁 Full GC。建议 JVM 参数里加上-XX:UseG1GC并在 driver 和 executor 中分别设置。G1GC 对几十 GB 的堆更友好可以显著减少大堆下的停顿。第三个陷阱是动态资源分配在团队共享集群上的副作用。这个功能很好但如果你和 Hive 任务共享同一个 YARN 队列Spark 的动态扩容会把队列资源吃满导致 Hive 任务饿死。解决方案是给 Spark 和 Hive 分配不同的 YARN 队列或者设置spark.dynamicAllocation.maxExecutors到合理上限让资源分配有节流机制。7. 写在最后一套稳定的技术栈不是看谁跑得快而是看谁不掉链子聊到这里你应该已经形成了自己的判断。Spark 和 Hadoop 之间不是新与旧的代际更替而是不同层级的组件各司其职。HDFS 负责可靠地存YARN 负责公平地分Spark 负责高效地算——三者组合起来才是真正的大数据分析底座。我个人的体会是技术选型到最后拼的其实是“稳定压倒一切”。Spark 再快如果你们团队没有足够的内存预算或者没人能 hold 住 Executor 参数调优那它带给你的只有半夜三更的告警电话。Hadoop 再慢它的成熟生态和庞大的社区经验积累让它在“不出错”这件事上拥有极强的优势。我的建议是小步快跑先在团队里拿一两个非核心任务从 MapReduce 往 Spark 迁移跑顺了再铺开。而不是一股脑把生产管道全部切过去等出了问题再悔之晚矣。最后分享一个一直沿用到今天的小习惯每次 Spark 任务上线前先在测试环境用近一周的真实数据量压一遍观察它的 Shuffle Read 大小、GC 耗时和 Executor 内存水位。这三项数据比任何基准测试都更能说明问题。等你在生产环境踩过几次 OOM、小文件、倾斜的坑再回头看你就能做到心里有数——知道什么场景该让 Hadoop 慢慢跑什么场景必须把 Spark 推上去。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进