ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Flink报错/bin/flink No such file or directory的完整排查指南

Flink报错/bin/flink No such file or directory的完整排查指南

刚在群里又看到有人发这条报错截图:监控脚本跑了半夜报/bin/flink: No such file or directory,然后任务直接中断,数据链路断了一大截。这报错在 Flink 运维场景里属于“出现频率极高、坑点极隐蔽”的典型,很多人在/bin/flink这个路径上纠结半天,以为是 FLink 没装好,实际上下面的原因五花八门,有的是脚本解释器挂了,有的是环境变量被调度器干掉了,有的是 Windows 脚本拷到 Linux 忘了换行符。这篇就把这个报错的完整排查思路、背后原理和修复方案一次讲透。

内容适合三类人看:一是刚接手 Flink 集群、被各种启动脚本折腾的新人;二是用 crond、systemd 或者调度平台跑 Flink 任务,时不时遇到环境问题的人;三是想把监控脚本做得更规范、少踩坑的开发者。整个排查过程不涉及复杂原理,按步骤来就行,我会把每个操作背后的为什么也一起说清楚。

1. 先别急着补文件:拆解报错的真实含义

1.1 这个报错根本不是你以为的那个意思

很多人看到No such file or directory第一反应就是“文件不存在”,然后去/bin/flink路径下找文件,发现这个路径下确实没有 flink,于是得出“Flink 没装好”的结论。这个结论往往是不对的——/bin/flink这五个字只是一个表象,真正的报错原因藏在 shell 解释这个命令时的底层机制里。

Linux 下执行一个命令时,内核和 shell 的分工是这样的:shell 拿到flink这个名字,先去 PATH 路径列表里逐个找可执行文件;如果找到了一个可执行文件,就调用execve()系统调用去执行它;如果这个文件是二进制 ELF 文件,内核会去加载它,如果文件是脚本(以#!开头),内核会去读第一行声明的解释器。

这里最经典的坑就来了:当 shell 告诉你No such file or directory,至少有三种互不相关的底层原因:

  • 文件名真的不存在,这个最简单,路径拼错、文件名写错、软链接失效都属于这一类。
  • 文件存在,但是脚本第一行声明的解释器不存在。比如脚本首行是#!/bin/bash,而系统里/bin/bash这个路径不存在,内核在执行脚本时会直接报No such file or directory。
  • 文件存在,但它是一个 ELF 可执行文件,而它依赖的动态链接器(也就是/lib64/ld-linux-x86-64.so.2这类)不存在,加载器根本无法启动这个程序,同样报这个错。

所以这个报错真正的含义是:内核在尝试启动这个可执行文件的过程中,找不到它需要的一个关键文件。这个“关键文件”可能是 flink 本身,也可能是 bash 解释器,也可能是动态链接器。只看报错文本根本区分不了,必须结合命令去查。

1.2 为什么监控脚本特别容易踩这个坑

这个问题在普通的交互终端里很少出现,因为你的终端是一个登录 shell,它会加载/etc/profile、~/.bash_profile、~/.bashrc等一堆环境配置文件,PATH、JAVA_HOME、FLINK_HOME全都配好了,你敲flink自然没问题。

但监控脚本的运行环境完全不一样。crond 执行任务时使用的是非登录、非交互的 shell,环境变量被裁剪到只剩几个基本项,PATH往往只有/usr/bin:/bin。systemd 的 timer 和 service 更狠,默认只保留极少的环境变量,你在/etc/profile里 export 的东西它一概不认。第三方调度平台(比如 Azkaban、海豚调度这类)在执行脚本时同样会用一个受限环境,它不会去 source 你用户下的配置文件。

于是同样一段脚本,你在终端里跑得好好的,拿到 cron 里或者调度平台上就跑不通了。这个“终端能跑、定时任务不能跑”的现象,几乎已经成了 Flink 运维的保留项目。有点经验的老人看到这个报错,第一反应不是去检查文件是否存在,而是先去env看一眼当前环境里有没有FLINK_HOME和JAVA_HOME。

2. 环境变量丢失是头号元凶,排查顺序要正确

2.1 PATH 里根本没有 FLINK_HOME/bin

先看最常见的情况。假设你的 Flink 安装在/opt/flink-1.17.2,你在/etc/profile里配了:

export FLINK_HOME=/opt/flink-1.17.2 export PATH=$FLINK_HOME/bin:$PATH

手动执行脚本一切正常,但 crond 里的监控脚本一跑就报/bin/flink: No such file or directory。原因很简单:cron 执行时环境变量被重置了,FLINK_HOME不存在,PATH也被重置为/usr/bin:/bin。脚本里如果写的是flink list,shell 就会在/usr/bin和/bin下一遍遍找名为flink的可执行文件——找不到,于是报flink: command not found,这还算直观。

但如果你监控脚本里写的是绝对路径/bin/flink,情况就变了。这个路径下当然没有 flink,shell 去执行一个不存在的绝对路径,报的就是No such file or directory。换句话说,脚本里用了绝对路径/bin/flink本身就是一个错误姿势,因为 flink 的启动脚本根本不会安装到/bin下。

正确的做法是用$FLINK_HOME/bin/flink。但这就引出第二个问题了:如果脚本只写了$FLINK_HOME/bin/flink,而 crond 里FLINK_HOME这个变量是空的,展开之后就成了/bin/flink,报错文本一模一样。很多人的脚本之所以暴露出/bin/flink这个路径,就是变量展开失败的结果,不是真的有人写了/bin/flink。

验证方法很简单。把脚本里的flink相关行改成echo $FLINK_HOME,或者手动用纯净环境模拟 cron:

env -i /bin/bash

在这个纯净的 bash 里执行echo $FLINK_HOME,如果输出为空,就说明你的脚本在 cron 下也同样拿不到这个变量。这就是环境变量丢失的实锤。

2.2 修复要从源头做起,不要只在终端里 export

解决环境变量问题,最稳妥的方案不是去改 crond 的全局配置,而是在脚本开头自己加载环境。我见过太多人喜欢去改/etc/crontab里的 PATH,或者往/etc/environment里塞东西,结果搞到系统一半的服务环境都变了,排查起来反而更糟。推荐的做法是:在监控脚本的开头明确写死 Flink 和 Java 的路径。

#!/bin/bash export JAVA_HOME=/usr/local/java/jdk-17 export FLINK_HOME=/opt/flink-1.17.2 export PATH=$JAVA_HOME/bin:$FLINK_HOME/bin:$PATH

注意顺序:$JAVA_HOME/bin要放在$FLINK_HOME/bin前面。因为 Flink 启动脚本里面会调用java命令,如果你把 JAVA_HOME 放在了后面,而系统里某个旧版本 Java 的路径排在了前面,flink 会用错误的 Java 版本启动,轻则报警告,重则连 JobManager 都拉不起来。

如果是在 crond 里直接写命令,可以在 crontab 里给任务单独指定环境:

0 1 * * * source /etc/profile && /opt/monitor/flink_monitor.sh >> /var/log/flink_monitor.log 2>&1

这里先 source 一遍/etc/profile,把环境变量加载进来,再执行脚本,简单粗暴有效。但需要注意,如果系统的/etc/profile里配置了很多对非交互 shell 无效的逻辑,source 时可能会打印一些无用的输出,不影响大局,但建议在脚本里加> /dev/null 2>&1避免污染日志。

2.3 sudo 和 systemd 还有各自的隐藏坑

除了 cron,另外两个常见触发场景是sudo和systemd。

sudo有个特性:出于安全考虑,它默认会把PATH重置为一个“安全路径”,这个路径通常由/etc/sudoers里的secure_path指定。很多发行版的secure_path默认是/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,你自己配置的/opt/flink-1.17.2/bin根本不在里面。于是你在终端里执行sudo /opt/flink-1.17.2/bin/flink list能跑,但执行sudo flink list就会报command not found;而你如果执行sudo /bin/flink list,那就是直接指向了一个不存在的路径,报的就是标题里这个错。解决办法有两种:要么在sudo命令里显式带全路径,要么在/etc/sudoers里给指定用户追加secure_path。生产环境我建议用全路径,不动系统配置,影响面最小。

systemd更严格。在 service unit 文件里,默认PATH是/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin,$HOME可能都是根目录,你在 profile 里配的 JAVA_HOME、FLINK_HOME 完全不生效。正确写法是在[Service]段落里用Environment显式指定:

[Service] Environment=JAVA_HOME=/usr/local/java/jdk-17 Environment=FLINK_HOME=/opt/flink-1.17.2 Environment=PATH=/usr/local/java/jdk-17/bin:/opt/flink-1.17.2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin

这里再提醒一句,Environment里写的PATH不会自动与系统默认 PATH 叠加,而是完全覆盖。所以想省事的话,最安全的方式就是让脚本里的 export 兜底,unit 里写不写都行,但要保证脚本里一定有。

3. 脚本解释器和换行符:两个隐蔽杀手

3.1 先查 shebang,再看文件类型

如果环境变量已经确认没问题,FLINK_HOME和PATH都配好了,手动执行也正常,但放到监控脚本里还是报同样的错,那就得怀疑脚本解释器了。

Flink 自带的bin/flink本身是个 shell 脚本,安装包里一般是#!/usr/bin/env bash开头。这个写法依赖env去 PATH 里找 bash。但如果你的系统很精简,或者某些容器镜像里/usr/bin/env存在但 PATH 里没有/bin,就会变成env: 'bash': No such file or directory。而如果你自己写的监控脚本开头是#!/bin/bash,而该系统是一个把 bash 装在/usr/bin/bash、/bin只是指向/usr/bin的符号链接的发行版,一般没问题;但如果你用了某种精简镜像、或者/bin目录因为某些历史原因被删掉了,那内核加载脚本时就会因为找不到/bin/bash而报No such file or directory。

排查就用三条命令:

head -1 /opt/flink-1.17.2/bin/flink file /opt/flink-1.17.2/bin/flink ls -l /bin/bash /usr/bin/bash

file命令能告诉你这个文件到底是 ASCII 文本脚本、ELF 可执行文件还是符号链接。如果符号链接是坏的,ls -l会显示出目标路径不存在,这种情况在手动移动安装目录、或者把整个 Flink 目录 tar 到另一台机器后特别容易发生。

3.2 Windows 换行符的经典误伤

换行符问题是我见过最容易被忽略的一个,而且报错文本完全一样。假如你的监控脚本在 Windows 上编辑过、或者通过某些工具从 Windows 拷贝到 Linux,文件里的换行符是\r\n,Linux 上会把这行内容解析为#!/bin/bash\r。

内核在解析 shebang 时,会把\r当成解释器文件名的一部分,去查找/bin/bash\r这个文件,当然找不到,于是报No such file or directory。

这种问题在脚本里肉眼根本看不出来,你cat文件、head -3看内容都很正常,因为\r是隐藏字符。最快的检查方式是:

file monitor.sh

如果输出里带CRLF line terminators,那基本可以锁定就是换行符的问题。也可以更直观地用od查看:

od -c monitor.sh | head -5

如果看到M-o M-^? M-^?这种奇怪的输出,或者行尾有\r字符,就是石锤了。

修复方式两种:装dos2unix直接转换,或者用sed批量清理:

sed -i 's/\r$//' monitor.sh

转换完后不要急着跑,先重新file确认没有 CRLF 标志,再执行。这个坑最恶心的地方在于:脚本开头的前几行如果没有报错,后面的命令因为也带\r,会导致某些命令参数尾部多出一个回车符,出现类似FLINK_HOME/bin路径解析失败之类的随机问题。所以一旦发现脚本是从 Windows 传上来的,最好全文件统一转换,不要只改 shebang 那一行。

3.3 ELF 程序加载器缺失,另一种“文件存在但打不开”

还有一类原因比较底层,发生率不高,但一旦碰上很容易一头雾水:如果某个二进制可执行文件本身存在,权限也正确,但执行时还是报No such file or directory,你就要考虑动态链接器的问题。

Flink 的启动脚本归根结底要拉起 JVM,所以真正执行的二进制其实是java。如果 JAVA_HOME 里的java可执行文件是用某个特定版本的 glibc 编译的,而当前系统缺少它依赖的动态链接器(比如/lib64/ld-linux-x86-64.so.2),那么内核在启动这个 ELF 文件时同样会报No such file or directory。注意这里报错的文件名是java或者是启动脚本里的某个可执行文件路径,但本质都是同一个内核错误。

排查方式是先用file查看可执行文件的类型:

file $JAVA_HOME/bin/java

输出会指明它是 ELF 可执行文件、是动态链接还是静态链接、需要哪个解释器。如果是动态链接,可以用ldd检查依赖库是否完整:

ldd $JAVA_HOME/bin/java

出现not found就说明依赖缺失。这种情况在容器迁移、跨平台拷贝 JDK 目录时比较常见,解决办法一般是换一个与系统 glibc 版本匹配的 JDK 发行版,而不是硬着头皮去补一堆 .so 文件。

4. 一次真实的排查流程复盘:从告警到修复只用了十分钟

4.1 现场还原:手动执行没问题,cron 一跑就挂

前几天帮某数据团队处理过一个类似案例。他们的 Flink 集群跑在 Linux 服务器上,监控脚本逻辑很简单:每五分钟列出当前运行中的 job,如果数量异常就触发告警。脚本在工作目录下放着,手动执行./monitor.sh一切正常,但只要注册到 crond 里,第二天早上就会收到一堆任务失败的通知,看日志发现就是标题里那条报错。

到现场后我没有直接去看/bin/flink这个文件,而是先按三步走。第一步,在 crontab 的日志里确认执行用户和具体报错上下文。第二步,手动用同一个用户执行同一个脚本,确认“终端正常”这个前提是真的。第三步,把脚本里所有变量相关的行打印出来,跑一遍纯净环境测试。

用env -i /bin/bash启动一个完全干净的环境后,执行echo $FLINK_HOME输出为空,问题很快锁定:脚本里虽然写了flink list,但它在执行前有一段设置环境变量的逻辑,写的是:

if [ -f /etc/profile ]; then source /etc/profile fi

问题就出在这个 source 在 cron 环境下没有生效。查了一下/etc/profile的末尾,发现导出的变量前有一行根据登录 IP 做判断的旧逻辑,条件在非交互 shell 里永远为假,导致后面的 export 全部被跳过。这就是手动执行没问题、cron 执行就崩的典型结构——不是环境变量没配,而是配在了某些只会被登录 shell 加载的位置,或者加载过程被条件判断卡住了。

4.2 收敛修复方案:脚本内显式 export 替代全局 source

这个问题的修复我建议不要全局去动/etc/profile,而是把监控脚本的环境初始化逻辑独立成一个文件,比如在脚本开头写死关键路径:

#!/bin/bash export JAVA_HOME=/usr/local/java/jdk-17 export FLINK_HOME=/opt/flink-1.17.2 export PATH=$FLINK_HOME/bin:$JAVA_HOME/bin:$PATH

如果团队里脚本比较多,不想在每个脚本里重复写这些 export,可以单独建一个/opt/common/env.sh:

#!/bin/bash export JAVA_HOME=/usr/local/java/jdk-17 export FLINK_HOME=/opt/flink-1.17.2 export PATH=$JAVA_HOME/bin:$FLINK_HOME/bin:$PATH

然后在监控脚本开头 source 它:

#!/bin/bash source /opt/common/env.sh flink list ...

这种方式的好处是环境变量来源单一,修改集群 JDK 路径时只改一个文件就够了。注意 source 的路径要写绝对路径,不要写source ./env.sh,因为在 crond 里工作目录往往不是你写脚本时所在的目录,相对路径 source 很容易失败,失败后脚本还会继续往下走,报错信息更加难猜。

4.3 连带检查:环境变量修复后还会遇到什么

修完环境变量,我顺手把监控脚本里其他几个常见问题也一并处理了。

第一处是脚本内容用的是flink list,但没指定 job manager 地址。如果集群配置了 HA 或者自定义的 rest.address,flink list默认会连本机的。在监控场景里建议显式指定:

flink list -m hostname:8081

第二处是脚本没有设置超时。flink list命令在集群不可达时可能会长时间卡住,导致监控任务堆积。用timeout包一层比较稳妥:

timeout 30 flink list -m hostname:8081 > /tmp/flink_jobs.txt 2>&1

第三处是输出重定向。cron 执行脚本时,如果不把标准输出和错误输出重定向到日志文件,cron 会把输出打包成邮件发给本地用户,收件箱一满,后面的告警就全丢了。在 crontab 里建议统一写成:

*/5 * * * * /opt/monitor/flink_monitor.sh >> /var/log/flink_monitor.log 2>&1

这三处是监控脚本从“能跑”到“跑得稳”的必要补充。

5. 常见问题速查表与规范化脚本模板

5.1 遇到报错先照着这张表逐个排除

我把这类报错的典型触发场景和对应排查命令整理成了速查表,你可以直接截图保存,下次再遇到不用从头翻这篇文章:

报错场景排查命令常见原因
cron 里报错、终端不报env -i /bin/bash后检查$FLINK_HOME环境变量未加载
脚本里写/bin/flinkls -l /bin/flink路径本身就不对,应使用$FLINK_HOME/bin/flink
脚本从 Windows 拷贝file monitor.shCRLF 换行符导致 shebang 失效
sudo 执行才报错sudo envsudoers 里的 secure_path 未包含 Flink 目录
systemd 执行才报错systemctl show xxx -p Environmentunit 文件未定义 Environment,PATH 被重置
文件存在但执行报错file flink、ldd flinkELF 动态链接器缺失
符号链接失效ls -l /opt/flinkFlink 目录被移动或软链目标不存在
JAVA_HOME 未生效echo $JAVA_HOME、which javaprofile 未加载或 JAVA_HOME 拼错

这张表是按排查频率排序的,80% 的情况都集中在第一行和第三行,其余的行偶尔遇到,但排查手段基本一致:先看环境变量,再看文件本身,最后再看加载器。

5.2 一个可以直接拿去改的监控脚本模板

经过多次打磨,我现在的 Flink 监控脚本基本长这样,你可以直接复制改造:

#!/bin/bash # Flink 集群监控脚本:列出当前运行中的 Job 并做数量校验 export JAVA_HOME=/usr/local/java/jdk-17 export FLINK_HOME=/opt/flink-1.17.2 export PATH=$JAVA_HOME/bin:$FLINK_HOME/bin:$PATH FLINK_JOBMANAGER="nodeA:8081" LOG_FILE="/var/log/flink_monitor.log" TIMEOUT_SECONDS=30 MIN_JOB_COUNT=1 echo "$(date '+%F %T') monitor start" >> "$LOG_FILE" # 获取 Job 列表,带超时防止卡死 timeout "$TIMEOUT_SECONDS" \ flink list -m "$FLINK_JOBMANAGER" \ >> "$LOG_FILE" 2>&1 RET_CODE=$? if [ $RET_CODE -ne 0 ]; then echo "$(date '+%F %T') flink list failed, ret=$RET_CODE" >> "$LOG_FILE" # 这里可以替换成你的告警通知命令 exit 1 fi # 统计运行中的 job 数量 JOB_COUNT=$(grep -c 'RUNNING' <<< "$(timeout "$TIMEOUT_SECONDS" flink list -m "$FLINK_JOBMANAGER")") if [ "$JOB_COUNT" -lt "$MIN_JOB_COUNT" ]; then echo "$(date '+%F %T') job count too low: $JOB_COUNT" >> "$LOG_FILE" # 触发告警 exit 2 fi echo "$(date '+%F %T') monitor done, jobs=$JOB_COUNT" >> "$LOG_FILE" exit 0

这个模板有三个设计要点值得说说。第一,export写在脚本最前面,不依赖任何 profile 文件,这样无论从 cron、systemd 还是调度平台调用都不会缺环境。第二,RET_CODE用于区分 flink 命令本身失败和 job 数量异常,告警时可以按不同级别发。第三,脚本里所有路径和主机名都是变量,换环境时只需要改头部配置,不用改逻辑部分。

5.3 踩过几次坑之后,我养成的排查习惯

最后聊点个人的经验。这类报错之所以让人头疼,很大程度上是因为它出现的时机要么在大半夜,要么在任务交接的节骨眼上。我踩过几次坑之后,现在接手任何 Flink 部署环境,第一件事不是去看配置,而是先在纯净环境里模拟一遍调度器的执行方式,用env -i /bin/bash /path/to/script.sh直接跑,一步就能暴露脚本对环境变量的所有隐式依赖。

另一个习惯是,脚本里凡是涉及外部命令的,我尽量写全路径。比如flink写$FLINK_HOME/bin/flink,java写$JAVA_HOME/bin/java,连grep、awk这些基础命令在有条件的情况下也写/usr/bin/grep。这种做法看着啰嗦,但能避免一类“同一个命令在不同用户、不同 shell 下指向不同版本”的隐性问题。

还有就是在 crontab 里加任务时,永远带上>> log 2>&1。很多“无头案件”最后都能靠日志还原现场,日志里没有东西,排查效率至少降一半。有些同事习惯在脚本里用docker exec或者kubectl exec去查 Flink 状态,这里额外提醒一句:容器环境里的问题通常跟宿主机上的报错机制完全不一样,docker exec进入容器后要分别检查容器内的 PATH 和解释器,不要直接用宿主机视角去判断容器内文件是否存在。

这个报错说到底是一场“执行环境假设”的博弈。你的潜意识里假设脚本运行在一个跟终端完全一致的环境里,而调度器只给你一个光秃秃的 shell,所有你认为理所应当存在的变量,它都默认不给。写脚本时多显式声明一步,后续就能少排查好几个小时。

RELATED READING

延伸阅读

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