Spark History Server 架构原理介绍

本文详细介绍了Spark HistoryServer的工作原理,包括eventLog日志文件的配置与管理,两个定时任务的解析与清理过程,以及HistoryServer的架构和缓存机制。同时探讨了可能遇到的OOM问题及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Spark History Server 是spark内置的一个http服务,通过 sbin/sbin/start-history-server.sh启动。History Server启动后,会监听一个端口,同时启动两个定时任务线程,分别用来解析eventLog日志文件和清理过期的eventLog日志文件。

Spark History Server启动后,我们可以直接在浏览器输入 http://ip:port 访问。一般默认端口是18080

一、eventLog日志文件以及相关参数

eventLog日志文件介绍

eventLog需要将配置spark.eventLog.enabled设置为true来开启,默认是关闭的。

开启这个配置后,当我们提交spark job到集群中运行时,之后spark job在运行过程中会不断的一些运行信息写到相关的日志文件中。具体的eventLog存放目录由配置spark.eventLog.dir决定的。

Spark job在运行中,会调用EventLoggingListener#logEvent()来输出eventLog内容。spark代码中定义了各种类型的事件,一旦某个事件触发,就会构造一个类型的Event,然后获取相应的运行信息并设置进去,最终将该event对象序列化成json字符串,追加到eventLog日志文件中。

所以,eventLog日志文件是由一行一行的json串组成的,每一行json串都代表了一个事件。如下图:

在这里插入图片描述

在eventLog目录中,我们可以看到各个任务的eventLog日志文件

在这里插入图片描述

eventLog日志的文件名组成是APPID_ATTEMPTID,其中带.inprogress的表示该任务还在运行中。

相关配置参数

一般这些配置在放在spark-defaults.conf

配置名称默认值备注
spark.eventLog.enabledfalse执行spark job时是否需要输出eventLog到指定目录,建议开启
spark.eventLog.dir/tmp/spark-eventseventLog输出的hdfs路径
spark.history.fs.update.interval10shistory server每隔一段时间就会检查一下eventLog日志目录下的文件是否发生变动,然后进行解析或者更新。如果想要更及时的查看到任务的最新信息,这个时间可以设置的短一些,但太短的周期也会加重服务器的负担。
spark.history.ui.maxApplicationintMaxValue限制web界面最多查询多少个任务信息。该值如果设置的太小,会导致webUI上看不到排在后面的一些任务。
spark.history.ui.port18080history server监听端口
spark.history.fs.cleaner.enabledfalse是否开启过期eventLog日志清除,建议开启。否则eventLog就非常多
spark.history.fs.cleaner.interval1deventLog日志清除线程执行的周期。规定每隔多久检查一次eventLog并清除过期的eventLog日志
spark.history.fs.cleaner.maxAge7d规定eventLog的过期时间
spark.eventLog.compressfalse是否压缩eventLog日志文件。
spark.history.retainedApplications50在内存中缓存任务信息详情的个数,不建议设置的太大。后面就详细介绍这个缓存机制。
spark.history.fs.numReplayThreadsceil(cpu核数/4)解析eventLog的线程数量

二、两个定时任务

解析eventLog日志文件线程

该线程在FsHistoryProvider调用startPolling()方法时,通过以下代码启动:

pool.scheduleWithFixedDelay(getRunner(checkForLogs), 0, UPDATE_INTERVAL_S, TimeUnit.SECONDS)

从上面的代码可以看出,该线程每隔一段时间就会执行checkForLogs方法。这个时间间隔由配置spark.history.fs.update.interval决定,默认是10s执行一次。

该线程启动后,会扫描spark.eventLog.dir目录下的所有文件,根据过滤条件筛选出需要解析的eventLog日志文件列表,之后每一个eventLog日志文件都会开启一个线程去解析,这些线程会放到一个线程池中统一调度。该线程池的大小由spark.history.fs.numReplayThreads配置决定,默认会根据服务器的cpu核数动态调整,公式为 ceil(cpu核数/4)

过滤eventLog日志的相关代码:

      // scan for modified applications, replay and merge them
      val logInfos: Seq[FileStatus] = statusList
        .filter { entry =>
          val prevFileSize = fileToAppInfo.get(entry.getPath()).map{_.fileSize}.getOrElse(0L)
          !entry.isDirectory() &&
            !entry.getPath().getName().startsWith(".") &&
            prevFileSize < entry.getLen() &&
            SparkHadoopUtil.get.checkAccessPermission(entry, FsAction.READ)
        }
        .flatMap { entry => Some(entry) }
        .sortWith { case (entry1, entry2) =>
          entry1.getModificationTime() >= entry2.getModificationTime()
      }

注意,这里的解析并不会解析整个eventLog文件信息,只会获取application相关的一些基本信息,如下:

        val attemptInfo = new FsApplicationAttemptInfo(
          logPath.getName(),
          appListener.appName.getOrElse(NOT_STARTED),
          appListener.appId.getOrElse(logPath.getName()),
          appListener.appAttemptId,
          appListener.startTime.getOrElse(-1L),
          appListener.endTime.getOrElse(-1L),
          lastUpdated,
          appListener.sparkUser.getOrElse(NOT_STARTED),
          appCompleted,
          fileStatus.getLen()
        )

在所有的eventLog日志都解析成FsApplicationAttemptInfo后,这些信息都会被放到applications对象中。applications是一个LinkedHashMap[String, FsApplicationHistoryInfo]类型的Map。key是eventLog的路径。

清理过期的eventLog日志文件的线程

该线程在FsHistoryProvider调用startPolling()方法时,通过以下代码启动:

pool.scheduleWithFixedDelay(getRunner(cleanLogs), 0, CLEAN_INTERVAL_S, TimeUnit.SECONDS)

从上面的代码可以看出,该线程每隔一段时间就会执行cleanLogs方法。这个时间间隔由配置spark.history.fs.cleaner.interval决定,默认是1天执行一次。

该线程启动后,会遍历内存中applications对象的所有item,然后获取FsApplicationHistoryInfo.lastUpdated的值,根据spark.history.fs.cleaner.maxAge配置判断是否过期,如果过期了就准备删了对应的eventLog日志文件。(注意:这里遍历的对象是applications的item,而不是eventLog目录下的所有文件。另外,判断规则也不是获取eventLog日志文件的更新时间,而是FsApplicationHistoryInfo对象中的lastUpdated属性

三、History Server的架构

History Server是基于内嵌的jetty来构建http服务的。

这里简单介绍一下jetty的架构,jetty架构的核心是Handler。一个请求过来时,会解析然后被封装成Request,之后会交给Server对象中的Handler处理。Server的Handler可以是各种各样类型的Handler,因为History Server里面注入的是ContextHandlerCollection,我们这里只介绍ContextHandlerCollection。这个类也是Handler的一个实现类,可以理解为是Handler的集合,持有一系列Handler对象,同时还能起到路由器的作用。ContextHandlerCollection基于ArrayTernaryTrie构造了一个字典树,用于快速匹配路径。当收到一个请求时,ContextHandlerCollection根据url找到对应的Handler,然后把请求交给这个Handler去处理。Handler里面封装了各种我们自己实现的Servlet,最终请求就落到了具体的那个Servlet上执行了。

History Server在启动时,会往ContextHandlerCollection中加入一个ServletContextHandler,这里放着jersey的ServletContainer类,用来提供restful api。jersey会自动解析org.apache.spark.status.api.v1包下面的类,然后将对应的请求转发过去。

History Server启动时还会注册其他的handler,这里不多做介绍。

缓存机制

任务的applications信息是长期驻留在内存并不断更新的。当我们在页面点击查看某个任务的运行详情时,History Server就会重新去解析对应eventLog日志文件,这时就是解析整个eventLog文件了,然后将构建好的详情信息保存到缓存中。它的缓存使用了guava的CacheLoader,缓存的个数限制由配置spark.history.retainedApplications决定,在将任务信息放入缓存的同时,History Server还会提前构建好这个任务的各种状态的sparkUI(也就是web界面),并创建好ServletContextHandler,然后放到ContextHandlerCollection中去。

我们可以通过阿里的arthas来观察一下ContextHandlerCollection的变化情况:

  1. 服务刚启动时,就5个GzipHandler,他们的底层也都是ServletContextHandler。

在这里插入图片描述

  1. 随意在WebUI上点击查看某个任务的详情信息后,我们可以看到增加了20来个的handler,大多都是和这个任务相关的handler。

在这里插入图片描述

  1. 再点一个任务详情

在这里插入图片描述

通过缓存任务详情信息以及UI,用户就可以很快的查看任务的各种维度的运行信息以及相关界面。

四、一些潜在的问题

1. spark.history.retainedApplications 设置太大导致的OOM问题

由于每个任务的详情信息数据量都比较大,有的任务能达到G级别。spark.history.retainedApplications如果设置的过大,很可能会导致java堆内存空间放不下这些信息,最终导致OOM。建议维持在默认值50即可。

2. eventLog 日志文件过大导致的OOM问题

就算spark.history.retainedApplications设置的很小,但是有些时候任务产生的eventLog本身就很大,比如一个eventLog日志就达到10G。只要解析几个类似的eventLog并缓存,就可能造成OOM了。对于这种情况,我们可以通过修改spark的源码来解决,目前可以通过2个方面入手:

  • 在eventLog解析线程过滤处加一个过滤条件,即eventLog文件大小大于100M的我们就过滤不处理。即过滤代码中加上entry.getLen()<104857600
  • 找出eventLog日志太大的原因,比如我们集群是由于Accumulator的信息过多,所以可以修改JsonProtocol#accumulablesToJson()方法,在spark job运行时不统计Accumulator的信息

3. History Server 突然不可用的问题

表现为history页面无数据,抓了一下包,发现所有的请求都被转发到首页对应的那个handler中去了。也就是所有的请求都返回了首页的html内容。比如在浏览器输入 “/xxxx/xxx/xxx/xx” 也被转发到了 “/”。归根究底就是jetty的路由问题。

经过观察,发现当ContextHandlerCollection中的handler到达一定数量,就会发生这种情况。一般当handler数量达到14000就可能导致jetty路由失效。

目前可以通过调小spark.history.retainedApplications来控制handler的数量,因为缓存一个任务的详情会增加23个handler,因此理论上将spark.history.retainedApplications控制在500以下都可以认为是安全的。

经测试,将spark.history.retainedApplications从1000调整到100后,不会发生类似问题

目前还未找到jetty路由失效的真正原因

附录

jetty架构详解

JMV进程诊断利器—arthas 介绍

<think>我们有三台服务器,需要部署多个组件,并展示它们之间的部署关系和高可用机制。以下是部署方案的可视化描述,使用Mermaid图表展示。###部署架构图```mermaidgraphLRsubgraph服务器1[高性能节点:64核CPU,128G内存]NN1[HDFSNameNode(主)]RM1[YARNResourceManager(主)]HIVE[HiveServer2]SPARK[SparkHistoryServer]AZ_WEB[AzkabanWebServer]PROM[Prometheus]GRAF[Grafana]endsubgraph服务器2[中等性能节点:32核CPU,64G内存]NN2[HDFSNameNode(备)]RM2[YARNResourceManager(备)]ZK1[ZookeeperNode1]SQOOP[Sqoop]FLUME1[FlumeAgent]endsubgraph服务器3[中等性能节点:32核CPU,64G内存]DN1[HDFSDataNode]NM1[YARNNodeManager]ZK2[ZookeeperNode2]AZ_EXEC[AzkabanExecutor]FLUME2[FlumeAgent]end%%高可用机制NN1--HA心跳-->NN2RM1--HA心跳-->RM2ZK1--选举-->ZK2ZK2--选举-->ZK1%%组件间关系FLUME1-->|数据采集|DN1FLUME2-->|数据采集|DN1SQOOP-->|数据导入|HIVEHIVE-->|元数据|NN1SPARK-->|日志存储|HDFSAZ_WEB-->|调度任务|AZ_EXECAZ_EXEC-->|执行任务|YARNPROM-->|监控数据|所有组件GRAF-->|数据源|PROM%%HDFS和YARN基础组件NN1-->DN1RM1-->NM1RM2-->NM1```###高可用机制说明1.**HDFSNameNode高可用(HA)**:-主NameNode部署在服务器1,备NameNode部署在服务器2。-通过ZooKeeper(部署在服务器2和服务器3)实现自动故障转移。-当主NameNode故障时,备NameNode会自动切换为主节点[^1]。2.**YARNResourceManager高可用(HA)**:-主ResourceManager部署在服务器1,备ResourceManager部署在服务器2。-同样依赖ZooKeeper进行状态同步和故障转移[^2]。3.**ZooKeeper集群**:-部署两个节点(服务器2和服务器3),满足最小高可用要求(至少2n+1个节点,但这里部署2个节点,实际生产环境建议至少3个节点,但资源有限情况下可以部署2个节点,但牺牲了一些容错能力)。-通过选举机制保证服务可用性[^3]。###组件间关系说明-**数据采集**:-FlumeAgent部署在服务器2和服务器3,从WebAPI采集数据,写入HDFSDataNode(部署在服务器3)。-**数据导入**:-Sqoop部署在服务器2,从数据库导入数据到Hive(HiveServer2部署在服务器1)。-**元数据管理**:-Hive的元数据存储在HDFSNameNode(主节点)和MySQL(图中未展示,但实际需要部署,可部署在服务器1)。-**任务调度**:-AzkabanWebServer(服务器1)将任务调度到AzkabanExecutor(服务器3),Executor通过YARN执行任务(ResourceManager和NodeManager)。-**监控**:-Prometheus(服务器1)从所有组件采集监控数据,Grafana(服务器1)从Prometheus获取数据进行可视化。###注意事项1.**ZooKeeper集群节点数**:实际生产环境中,ZooKeeper集群建议至少3个节点以实现高可用。但这里只有3台服务器,且服务器1已部署较多核心服务,因此将两个ZooKeeper节点分别部署在服务器2和服务器3。这样配置下,如果一台ZooKeeper节点故障,另一台仍可提供服务(但无法选举,因此严格来说不具备高可用性)。如果资源允许,建议在服务器1也部署一个ZooKeeper节点,形成3节点集群[^3]。2.**HDFSDataNode部署**:图中只画了一个DataNode(服务器3),但实际上所有三台服务器都应部署DataNode以实现数据冗余。修改如下:-服务器1、服务器2、服务器3都部署DataNode和NodeManager。3.**HiveMetastore**:HiveMetastore服务应独立部署(通常与MySQL数据库一起部署在服务器1)。4.**Prometheus数据存储**:建议使用独立磁盘或SSD存储监控数据,避免影响HDFS性能。###修正后的部署图(考虑所有节点部署DataNode和NodeManager)```mermaidgraphTBsubgraph服务器1[高性能节点:64核CPU,128G内存]NN1[HDFSNameNode(主)]RM1[YARNResourceManager(主)]HIVE[HiveServer2+Metastore]SPARK[SparkHistoryServer]AZ_WEB[AzkabanWebServer]PROM[Prometheus]GRAF[Grafana]DN1[HDFSDataNode]NM1[YARNNodeManager]ZK3[ZookeeperNode3]%%新增endsubgraph服务器2[中等性能节点:32核CPU,64G内存]NN2[HDFSNameNode(备)]RM2[YARNResourceManager(备)]ZK1[ZookeeperNode1]SQOOP[Sqoop]FLUME1[FlumeAgent]DN2[HDFSDataNode]NM2[YARNNodeManager]endsubgraph服务器3[中等性能节点:32核CPU,64G内存]DN3[HDFSDataNode]NM3[YARNNodeManager]ZK2[ZookeeperNode2]AZ_EXEC[AzkabanExecutor]FLUME2[FlumeAgent]end%%高可用机制NN1--HA心跳-->NN2RM1--HA心跳-->RM2ZK1--选举-->ZK2ZK1--选举-->ZK3ZK2--选举-->ZK1ZK2--选举-->ZK3ZK3--选举-->ZK1ZK3--选举-->ZK2%%组件间关系FLUME1-->|数据采集|DN2FLUME2-->|数据采集|DN3SQOOP-->|数据导入|HIVEHIVE-->|元数据|MySQL[MySQLonServer1]SPARK-->|日志存储|HDFSAZ_WEB-->|调度任务|AZ_EXECAZ_EXEC-->|执行任务|YARNPROM-->|监控数据|所有组件GRAF-->|数据源|PROM%%HDFS和YARN基础组件NN1-->DN1NN1-->DN2NN1-->DN3RM1-->NM1RM1-->NM2RM1-->NM3```###修正说明1.**ZooKeeper集群**:现在在服务器1、2、3各部署一个节点,形成3节点集群,确保高可用。2.**DataNode和NodeManager**:所有三台服务器均部署,充分利用存储和计算资源。3.**HiveMetastore**:与MySQL数据库一同部署在服务器1(图中标注为HiveServer2+Metastore,并连接到MySQL)。4.**数据采集**:FlumeAgent将数据写入本机的DataNode(或根据配置写入其他节点)。此架构实现了核心组件的高可用,并合理分配了资源。注意,实际部署时需根据具体组件配置调整内存和CPU分配。§§相关问题§§1.ZooKeeper集群节点数为什么建议是奇数?2.HDFSHA切换过程中如何避免脑裂问题?3.如何优化Prometheus在大规模监控下的存储性能?4.在资源有限的情况下,如何平衡YARN资源分配与系统服务资源需求?5.Flume如何实现故障转移和负载均衡?</think>### Hadoop生态系统部署架构图与高可用机制 以下是基于三台服务器(1台高性能+2台中性能)的分布式系统部署架构图,使用Mermaid语法绘制,展示组件部署关系和高可用机制: ```mermaid graph TD %% 服务器节点定义 subgraph Server1[64核/128G 高性能节点] direction TB NN1[NameNode主] --> JN1[JournalNode] RM1[ResourceManager主] --> ZK1[Zookeeper Node1] HIVE[Hive Metastore] --> HMS_DB[(MySQL)] SPARK[Spark Driver] --> HIS[History Server] PROM[Prometheus] --> GRAF[Grafana] AZ_WEB[Azkaban Web] end subgraph Server2[32核/64G 中性能节点] direction TB NN2[NameNode备] --> JN2[JournalNode] RM2[ResourceManager备] --> ZK2[Zookeeper Node2] DN1[DataNode] --> NM1[NodeManager] SQOOP[Sqoop] --> FLUME1[Flume Agent] end subgraph Server3[32核/64G 中性能节点] direction TB DN2[DataNode] --> NM2[NodeManager] DN3[DataNode] --> NM3[NodeManager] AZ_EXEC[Azkaban Executor] --> FLUME2[Flume Agent] ZK3[Zookeeper Node3] end %% 高可用机制 NN1 -. QJM HA .- NN2 RM1 -. ZK Failover .- RM2 ZK1 -. Election .- ZK2 ZK1 -. Election .- ZK3 %% 组件间数据流 FLUME1 -->|采集| DN1 FLUME2 -->|采集| DN2 SQOOP -->|ETL| HIVE HIVE -->|查询| SPARK SPARK -->|计算| YARN AZ_WEB -->|调度| AZ_EXEC PROM -->|监控| METRICS[所有组件] ``` ### 关键高可用机制说明 1. **HDFS HA (QJM机制)**[^1] - 主NameNode(Server1) + 备NameNode(Server2) - 通过JournalNode集群(跨三台服务器)同步元数据 - 故障切换流程: ```mermaid sequenceDiagram Zookeeper集群->>备NN: 检测主NN心跳丢失 备NN->>JournalNode: 确认最后事务ID 备NN->>Zookeeper: 获取活动锁 备NN->>集群: 宣布成为新主节点 ``` 2. **YARN HA (基于ZKRMStateStore)**[^2] - 双ResourceManager(Server1主, Server2备) - Zookeeper存储ApplicationMaster状态 - 故障切换时间 < 60秒 3. **Zookeeper集群** - 三节点部署(满足Paxos算法要求) - 选举机制: $$ quorum = \left\lfloor \frac{n}{2} \right\rfloor + 1 $$ - 可容忍1节点故障 ### 组件交互关系 | **交互类型** | **组件A** | **组件B** | **协议/端口** | **说明** | |-------------|-----------|----------------|-------------------|----------------------------| | 数据存储 | HDFS | DataNodes | 8020/50010 | 块存储三副本策略 | | 资源调度 | YARN | NodeManagers | 8030-8040 | 容器资源分配 | | 元数据管理 | Hive | NameNode | 9083/9000 | 表位置映射 | | 协调服务 | Zookeeper | 所有HA组件 | 2181/2888/3888 | 分布式锁和状态同步 | | 数据采集 | Flume | HDFS | HTTP/41414 | 事件数据写入HDFS | | ETL作业 | Sqoop | Hive Metastore | JDBC/10000 | 数据库导入Hive表 | | 监控数据流 | 所有组件 | Prometheus | HTTP/9090 | 指标暴露与抓取 | ### 架构设计要点 1. **服务分布原则** - 核心控制节点(NameNode/RM)部署在**高性能节点** - 无状态服务(Zookeeper/JournalNode)跨**所有节点** - 计算密集型(Spark Executor)通过YARN动态分配 2. **故障域隔离** ```mermaid graph LR WebAPI --> Flume1 WebAPI --> Flume2 DB --> Sqoop 故障域1(Server1故障) --> 影响[NameNode主/RM主/Hive] 故障域2(Server2故障) --> 影响[NameNode备/RM备/Sqoop] 故障域3(Server3故障) --> 影响[Executor/Flume] ``` 3. **数据流验证** ```bash # 检查HDFS高可用状态 hdfs haadmin -getServiceState nn1 # 应返回active hdfs haadmin -getServiceState nn2 # 应返回standby # 测试Zookeeper选举 echo stat | nc localhost 2181 | grep Mode ``` > **部署建议** > 1. JournalNode需部署在**所有三台服务器**确保元数据安全 > 2. Zookeeper的`myid`文件需唯一(Server1:1, Server2:2, Server3:3) > 3. Prometheus配置`scrape_configs`抓取所有组件指标
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值