前阵子凌晨2点多,线上一个实时链路的数据倾斜任务突然失败,当时我人在家里只来得及瞄了一眼异常概览,想着第二天到公司再拉完整日志复盘。结果早上到公司打开 JobManager 的 Web UI,里面干干净净——因为集群在这期间被运维重新拉起过,JobManager 内存里的作业状态全没了,运行到一半的异常堆栈、算子耗时分布、Checkpoint 记录全部清零。从那一刻起,我就在每一套 Flink 集群上部署了 History Server。现在哪怕整个集群已经停机,我依然能打开 Web UI 查看所有已完成作业的运行数据,也可以通过 REST API 把失败作业的异常信息批量拉出来做复盘。这篇文章就系统聊聊这套东西的部署配置、实际用法和我在生产环境踩过的坑。
1. 先搞清楚 History Server 到底解决了什么问题
1.1 一个真实痛点:集群重启后作业数据一夜清零
先做个模拟。假设你凌晨有一个作业跑挂了,JobManager 重启之前,你根本来不及记录完整信息。等你回到工位,想打开 Web UI 找到那个作业,看一眼失败原因、哪个算子出问题、Checkpoint 卡在哪一步,结果页面空白,作业 ID 都查不到。之所以会这样,是因为 Flink 作业的运行时状态默认保存在 JobManager 的内存里,JobManager 进程一去,这些信息就跟着丢了。做过大数据运维的朋友都知道,这种“事后想复盘却找不到数据”的体验有多折磨人。
History Server 的核心价值就在这:JobManager 在作业到达终态(FINISHED、CANCELED、FAILED)时,会把作业的完整元数据、执行计划、运行统计、异常信息等打包成归档文件,写到一份持久化存储里(一般是 HDFS、S3,也可以是本地磁盘)。History Server 是一个独立的轻量级进程,它不依赖 JobManager 是否存活,只需要能读到归档文件,就能在 Web UI 和 REST API 上还原出这些作业的运行情况。简单说,只要归档文件还在,集群停了也不妨碍你看数据。
1.2 理解它的原理:从归档文件里恢复作业视图
History Server 的工作机制可以拆成三步。第一步,JobManager 在作业运行期间和结束之后,会定期把作业的概要信息、执行图、各顶点的指标、异常信息等序列化成 JSON 格式的归档文件,写入 jobmanager.archive.fs.dir 指定的目录。第二步,History Server 进程启动后,会周期性地扫描 historyserver.archive.fs.dir 指定的目录,把新出现的归档文件读取并缓存到自己的内存里,重建出作业的运行视图。第三步,用户访问 History Server 的 Web UI 或 REST 接口时,看到的就是从这些归档文件还原出来的数据。
这里有个细节值得注意:归档文件在作业运行过程中就可能持续写入,但 History Server 通常只把“已经进入终态”的作业加载到自己的列表里,还在 RUNNING 状态的作业,即使归档文件里有部分数据,也不会通过 History Server 对外展示。所以你要记住一个边界——History Server 管的是“历史完成作业”,实时看运行状态还得靠 JobManager 自己的 Web UI。
1.3 它和 JobManager Web UI 的分工
生产环境里这两者最好搭配着用,而不是二选一。JobManager Web UI 负责“此刻发生了什么”,比如当前正在运行的作业、实时指标、连接状态、反压情况,这些都是需要从 JobManager 内存里拿的,集群一重启就没了。History Server 负责“之前发生了什么”,比如上周某个作业失败时的完整异常堆栈、当时每个算子的耗时分布、Checkpoint 成功与否的时间线,这些数据从持久化的归档文件里读,不受集群生命周期影响。
所以我在每套集群上都把这两套东西同时部署。日常排查用 JobManager UI,事后复盘、周报统计、故障回溯用 History Server,两者数据互补,基本能覆盖所有场景。而且 History Server 的 REST API 和 JobManager 的 REST API 路径一致,很多脚本可以直接复用,这一点后面会详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零搭建 History Server
2.1 前置条件:版本对齐与共享存储
部署 History Server 之前,最重要的一条原则是版本对齐。History Server 的版本需要和 JobManager 的主版本一致,比如集群跑的是 Flink 1.17.2,那 History Server 也尽量用 1.17.2。因为归档文件的格式和内部结构在不同大版本之间可能有变化,版本不对容易出现解析失败,或者作业信息读出来是残缺的。我自己就踩过 1.16 的 History Server 去读 1.17 归档文件的坑,作业列表能显示,但点进详情页很多字段是空的,排查了半天才意识到是版本问题。
其次是存储准备。因为 JobManager 写入归档和 History Server 读取归档要对接同一份区域,你需要提前准备好一个共享存储路径,生产环境建议用 HDFS 路径,比如 hdfs:///flink/completed-jobs/。如果只是本地开发测试,也可以用本地路径,但要注意:如果 JobManager 和 History Server 不在同一台机器上,本地路径就必须是两边都能访问的共享目录(比如挂在同一个 NFS 上),否则 JobManager 写的文件 History Server 根本读不到。
2.2 关键配置项详解:JobManager 侧和 History Server 侧
配置分两边,先说 JobManager 侧。在 conf/flink-conf.yaml 里设置归档目录:
yaml复制# JobManager 侧:作业归档写到哪里
jobmanager.archive.fs.dir: hdfs:///flink/completed-jobs/
这个配置决定了作业到达终态后,归档文件写到哪个目录。需要注意,这里配置的是 JobManager 进程的写入权限,所以要确保运行 JobManager 的用户对该目录有写权限。如果配置得比较晚,已经跑完的作业不会补写归档,只对配置生效之后结束的作业有效。
再说 History Server 侧,也是在 conf/flink-conf.yaml 里设置:
yaml复制# History Server 侧:从哪里扫描归档文件(可以配置多个目录,用逗号分隔)
historyserver.archive.fs.dir: hdfs:///flink/completed-jobs/
# Web UI 监听地址与端口
historyserver.web.address: 0.0.0.0
historyserver.web.port: 8082
# 扫描目录的刷新间隔,单位毫秒
historyserver.archive.fs.refresh-interval: 10000
这里有几个点值得展开。historyserver.archive.fs.dir 支持配置多个目录,用逗号分隔,我在公司就把多个业务线的归档目录都挂到了同一个 History Server 上,统一入口查看所有集群的历史作业,非常方便。historyserver.web.port 默认是 8082,如果和现有服务冲突可以改掉。refresh-interval 表示 History Server 每隔多久扫描一次归档目录,默认是 10000 毫秒,如果你的作业比较频繁,想更快看到新完成作业,可以调小到 5000,但也不要太小,否则频繁扫描会白白增加 HDFS 的压力。
另外还有一个隐性的关键参数:History Server 的 JVM 内存。归档文件加载到内存后,History Server 会常驻缓存所有作业的元数据。如果作业量很大,默认的堆内存可能不够,需要在 conf/flink-conf.yaml 里设置:
yaml复制# 增大 History Server 的 JVM 堆内存
historyserver.jvm.memory: 2048m
注意这个参数不是所有版本都有,如果版本不支持直接用 JVM 参数,可以在启动脚本里调整 FLINK_HISTORY_SERVER_OPTS 环境变量,本质一样。
2.3 启动 History Server 并做基础验证
配置完成后,启动很简单。Flink 自带启动脚本,在 Flink 安装目录下执行:
bash复制bin/historyserver.sh start
启动成功之后,先看日志,确认没有报错:
bash复制tail -f logs/flink-${USER}-historyserver-${HOSTNAME}.log
日志里正常情况下会打出类似 “Restoring /flink/completed-jobs/xxx" 的扫描信息,能说明 History Server 已经成功连上了归档目录。如果日志里一直没有任何扫描记录,优先检查两边目录配置是否一致,以及归档目录权限是否可读。
然后用浏览器打开 http://<你的机器IP>:8082/,正常情况下会看到作业列表页面。如果你手头有一个已经结束的作业,此时应该能在列表里找到它。也可以直接用 REST 接口快速验证:
bash复制curl -s http://localhost:8082/api/v1/jobs/overview | python3 -m json.tool
返回 JSON 里如果能看到作业数组,就说明这套 History Server 已经能正常工作了。注意,如果你是用 Flink on YARN 的模式,还有一种启动方式,在 YARN 上以 JobManager 附属服务的方式启动 History Server,但那个依赖 YARN 集群本身还活着,和我们“集群停了也能看数据”的目标有冲突,所以我一般是把 History Server 部署在独立的边缘节点上,而不是跟着 YARN 走。
3. Web UI 与 REST API 的实际使用
3.1 Web UI 能看哪些数据,怎么看
History Server 的 Web UI 布局和 JobManager 的 Web UI 几乎一样,所以用过 Flink 自带界面的人上手没有成本。进入首页后,你会看到一个作业列表,每一行包含作业 ID、作业名、状态、开始时间、结束时间、耗时等基础信息。状态这里特别有用,你能按 FINISHED、CANCELED、FAILED 快速筛选,一眼看到哪些作业有问题。
点进任意一个作业,就能看到更详细的面板。我这里挑几个最常用的说。
第一个是 Overview 页面,展示作业的整体信息,比如执行的开始结束时间、总耗时、是否开启了 Checkpoint、配置过的并行度,还有作业的运行模式(STREAMING 还是 BATCH)。排查“作业为什么跑了 8 个小时”这类问题,这里能先看个大概。
第二个是 Exceptions 页面,这是我用得最多的。作业失败时,异常堆栈会完整记录在这里。你不需要去翻 TaskManager 日志,直接在这个页面就能定位失败原因,比如反序列化异常、外部系统连接超时、数据里出现了脏值等等。注意,异常信息可能分布在多个子任务里,页面上会按子任务维度列出来,建议优先看第一个报错的 sub task。
第三个是 Tasks 页面,它展示每个算子的子任务状态分布,比如多少个成功、多少个失败、多少个取消。这个页面对定位数据倾斜很有帮助:如果某个算子的某个 sub task 处理时间远高于其他 sub task,那大概率就是数据倾斜了。我把鼠标放到对应节点上,能看到具体子任务的 ID、运行时长、处理记录数等。
第四个是 Checkpoints 页面,展示作业 Checkpoint 的历史记录。虽然作业已经结束,但曾经的 Checkpoint 成功失败时间线都在这里。如果一个作业频繁失败,看看 Checkpoint 是不是一直在超时,往往能发现端倪。
最后补一句,History Server 上的动态指标数据是靠归档快照提供的,不是实时采集,所以像实时 CPU、内存曲线这些信息,能看到的粒度取决于归档时保存了什么,不要期望和运行时 JobManager 上的监控一样细致。这个限制是“事后复盘”这个场景本身决定的,并不影响它对故障定位的价值。
3.2 REST API 操作指南与自动化复盘脚本
Web UI 适合人肉排查,但如果要做批量复盘、自动告警、对接内部的运维平台,就必须用 REST API 了。History Server 的 REST 接口路径和 JobManager 基本一致,都是 /api/v1 开头,很多脚本可以直接在两个场景里复用。
我常用的是这几个接口:
| 接口路径 | 作用 |
|---|---|
/api/v1/jobs/overview |
获取所有作业的简略信息列表,含状态、起止时间 |
/api/v1/jobs |
获取所有作业 ID 列表 |
/api/v1/jobs/:jobid |
获取某个作业的详细信息 |
/api/v1/jobs/:jobid/exceptions |
获取作业的异常信息 |
/api/v1/jobs/:jobid/checkpoints |
获取作业的 Checkpoint 历史 |
/api/v1/jobs/:jobid/vertices/:vertexid/subtasks |
获取某个算子下所有子任务的详细指标 |
实际使用的时候,直接用 curl 加 jq 就能做很多事。比如我想快速列出过去 24 小时失败的所有作业,可以这样:
bash复制curl -s http://localhost:8082/api/v1/jobs/overview \
| jq '.jobs[] | select(.state == "FAILED") | {id, name, "start-time": .["start-time"], "end-time": .["end-time"]}'
再比如,我想把每个失败作业的异常堆栈统一拉出来,写个简单的 shell 循环:
bash复制#!/bin/bash
HISTORY_SERVER="http://localhost:8082"
for job_id in $(curl -s $HISTORY_SERVER/api/v1/jobs/overview \
| jq -r '.jobs[] | select(.state == "FAILED") | .id'); do
echo "===== Job: $job_id ====="
curl -s $HISTORY_SERVER/api/v1/jobs/$job_id/exceptions \
| jq -r '.exceptions[].exception'
done
执行完,失败作业的异常信息就全部汇总到终端里了。你可以把它进一步接到告警平台,比如作业 FAILED 之后,通过 REST 接口自动拉取异常并推送到企业微信或钉钉群,整个流程基本不依赖人工操作。
这里我也提醒一句:REST API 返回的 JSON 里很多字段名带连字符,比如 start-time、end-time、last-modification,在 jq 里访问的时候不能直接用 .start-time 这种写法,必须写成 .["start-time"],不然 jq 会把减号当成减号操作符,返回 null 或者直接报错。这个细节我在刚开始写脚本时卡了挺久。
4. 生产环境配置策略与避坑指南
4.1 归档目录管理与保留策略
History Server 本身不负责清理归档文件,这一点很多人容易忽略。作业量大的集群,归档文件会越积越多,尤其是那种一天跑几千个任务的场景。归档文件虽然单个体积不大,但数量多到一定程度后,History Server 扫描目录、加载文件都会越来越慢,内存占用也会水涨船高。所以归档目录的保留策略一定要提前想好,不能放任自流。
我的做法是两条线并行。第一,在 JobManager 侧通过 HDFS 的上层机制做目录配额,给归档目录设置容量上限,防止存储被打爆。第二,写一个定时清理脚本,比如每天凌晨清理 30 天前的归档文件。如果环境允许,更优雅的方式是直接用 HDFS 的 TTL 策略,让文件自动过期删除,省得自己维护脚本。保留多少天要根据你们的复盘习惯定,我这边是按 30 天来,超过一个月的作业数据,直接查底层日志就够了,很少会再用 History Server 翻。
另外,前面提到 historyserver.archive.fs.dir 支持配置多个归档目录。这个特性在多团队共用一套 History Server 时特别有用。比如数据组、算法组、BI 组各自有一套 Flink 集群,归档目录各不相同,但可以在同一台 History Server 上统一配置,这样所有人的历史作业都能在一个 Web UI 里查到,省去了维护多套服务的成本。不过也要注意,目录越多,History Server 的扫描压力越大,建议控制在几个以内,并且错开写入高峰。
4.2 常见问题排查与避坑实录
我从部署和使用 History Server 到现在,遇到过不少问题,挑几个典型的说,你们大概率也能遇到。
第一个问题:History Server 启动了,但作业列表是空的。先别急着怀疑配置,最常见的两个原因是归档目录写权限和读权限不一致。JobManager 写归档时用的用户和 History Server 读取时的用户通常不一样,如果 HDFS 目录权限控制得比较严,History Server 可能根本没有权限列出目录里的文件。排查方法很简单,用启动 History Server 的用户手动执行 hdfs dfs -ls hdfs:///flink/completed-jobs/,看能不能列出文件,列不出来就是权限问题。还有一种情况是 jobmanager.archive.fs.dir 配置晚了,作业是在配置之前结束的,那它当然不会出现在 History Server 里,这种就只能等新作业了。
第二个问题:作业列表里有作业,但点进去很多页面是空白的。这个大多数是版本问题。我前面强调过归档文件格式和 Flink 版本强绑定,如果 History Server 和 JobManager 的主版本不一致,解析出来的字段就可能对不上,表现就是列表能看到,详情页却是残缺的。遇到这个情况,先确认两边的 flink.version,不一致就换版本。还有一种可能是 1.15 之前的老版本归档里,某些信息本身就没有被持久化,那就只能接受现实。
第三个问题:History Server 启动后,日志里出现大量重复扫描记录,或者 CPU 打满。这个一般是你把 refresh-interval 调得太小,同时归档文件数量又太大。我在压测环境试过 5000 毫秒的刷新频率配合几十万个文件,History Server 卡到页面都打不开。先把间隔调大,比如 30000 毫秒,再结合清理策略把文件数量降下来,基本能解决。
第四个问题:History Server 进程在,但 Web 页面连接不上。既然进程在,大概率是 historyserver.web.address 绑定问题。如果绑定的是 127.0.0.1,其他机器当然访问不到。要对外开放,就把它配成 0.0.0.0。另外还要检查防火墙,8082 端口是否放行,这个属于基础网络问题,但特别容易被忽略。
边用边踩坑的过程中,我的体会是:History Server 最怕的事情其实不是配置复杂,而是“平时没人在意它,等到要用的时候才发现它没存下东西”。所以我的建议是,每套环境都早点把 jobmanager.archive.fs.dir 配好,专门抽一台机器把 History Server 跑起来,然后在一个作业结束后立刻验证一下能不能看到。这个验证动作只花 10 分钟,却能保证关键时刻不抓瞎。再往深了说,如果你们对链路稳定性要求很高,可以顺手把 History Server 的 REST 接口接到自己的监控平台上,每天自动巡检一遍失败作业,把异常堆栈归档成周报,这样很多问题根本不用等人发现,它就自己浮出水面了。
