safe_sleep.sh: GitHub Actions Runner 中那个偶尔无限挂起的“小睡眠”脚本

在 GitHub Actions Runner 代码库里,有一个看似简单的 Bash 脚本------safe_sleep.sh,它负责让 Runner 在某些场景下"安全地睡眠"一段时间。但这个小脚本却因为一个 subtle 的逻辑缺陷,让许多开发者和 CI 系统管理员困扰不已,甚至引发了性能、资源浪费、Runner 卡死等严重问题。

一、什么是 safe_sleep.sh

GitHub Actions Runner 是负责执行 CI/CD 工作流程的后台服务。某些流程中,Runner 需要暂停一段时间(例如等待自动更新完成),这时就会调用一个名为 safe_sleep.sh 的脚本。

看似是个简单任务:让程序睡眠 N 秒钟 。但设计者并没有调用系统自带的 sleep 命令,而是用 Bash 语言编写了一个自循环脚本。

在旧版本中,脚本内容大致如下:

bash 复制代码
#!/bin/bash

SECONDS=0
while [[ $SECONDS != $1 ]]; do
  :
done

通过 busy-waiting 来实现睡眠,而不是调用标准 sleep

二、问题核心:循环条件有 Bug

乍看这段代码好像没毛病,但它有一个细节 严重依赖了调度时机

bash 复制代码
while [[ $SECONDS != $1 ]]; do ...

这个循环假设 SECONDS 会按 0 → 1 → 2 → ... 逐秒增加并且正好等于目标值。但在真实环境中:

  • 如果机器负载高;

  • 或者 Runner 进程被系统调度延迟;

  • 或者在虚拟化环境里暂停一段时间;

那么 SECONDS 有可能会直接跳过目标值,例如从 0 跳到 2。在这种情况下:

条件永远不会变成 "等于目标值",循环就会永远执行下去 ------ 无限循环

三、实际观测到的问题

Very rarely on update of github actions runner safe_sleep.sh hangs forever:

bash 复制代码
$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
...
actions+   72298 96.3  0.0   4372  3328 ?        R    Apr02 1988:54 /bin/bash /home/actions-runner/safe_sleep.sh 1
...

详见:safe_sleep.sh rarely hangs indefinitely

四、影响案例:更多比预想严重

不仅仅是某个挂起的新 Runner,某些开源项目(例如 Zig)据反馈在自托管 Runner 上观察到了成堆的死循环实例,这些无限循环的 safe_sleep.sh 不仅占满了 CPU,还导致 Runner 服务无法正常运行数日之久。

五、修复版本

https://github.com/actions/runner/commits/main/src/Misc/layoutroot/safe_sleep.sh

1. 最新修复版本
bash 复制代码
#!/bin/bash

# try to use sleep if available
if [ -x "$(command -v sleep)" ]; then
    sleep "$1"
    exit 0
fi

# try to use ping if available
if [ -x "$(command -v ping)" ]; then
    ping -c $(( $1 + 1 )) 127.0.0.1 > /dev/null
    exit 0
fi

# try to use read -t from stdin/stdout/stderr if we are in bash
if [ -n "$BASH_VERSION" ]; then
    if command -v read >/dev/null 2>&1; then
        if [ -t 0 ]; then
            read -t "$1" -u 0 || :;
            exit 0
        fi
        if [ -t 1 ]; then
            read -t "$1" -u 1 || :;
            exit 0
        fi
        if [ -t 2 ]; then
            read -t "$1" -u 2 || :;
            exit 0
        fi
    fi
fi

# fallback to a busy wait
SECONDS=0
while [[ $SECONDS -lt $1 ]]; do
    :
done
2. 执行逻辑(从"优雅"到"原始")
(1)首选:sleep
bash 复制代码
if [ -x "$(command -v sleep)" ]; then
    sleep "$1"
    exit 0
fi
  • 检查 sleep 是否存在且可执行

  • 最准确、最省 CPU

  • 正常系统 99% 会走到这里

(2)备用:ping
bash 复制代码
if [ -x "$(command -v ping)" ]; then
    ping -c $(( $1 + 1 )) 127.0.0.1 > /dev/null
    exit 0
fi

原理:

  • ping 默认 每秒发一次

  • N+1 个包 ≈ 等 N

⚠️ 注意点:

  • 依赖 ping 没被禁用(某些容器里 ping 被移除或需要 CAP_NET_RAW)

  • 时间精度不如 sleep

  • 127.0.0.1 保证不会阻塞网络

(3)再退一步:read -t(仅 Bash)
bash 复制代码
if [ -n "$BASH_VERSION" ]; then
    if command -v read >/dev/null 2>&1; then
        ...
    fi
fi

原理

  • read -t N阻塞 N 秒等待输入

  • 没输入就超时返回

为什么检查 -t 0 / 1 / 2

bash 复制代码
if [ -t 0 ]; then read -t "$1" -u 0; fi
  • read -t 必须绑定到 TTY

  • stdin/stdout/stderr 只要有一个是 TTY 就能用

  • 非交互 shell(CI、cron)通常都不是 TTY

(4)最终兜底:busy wait(CPU 自旋)
bash 复制代码
SECONDS=0
while [[ $SECONDS -lt $1 ]]; do
    :
done
  • SECONDS 是 Bash 内建变量(秒级)

  • : 是 no-op

  • 100% 占用一个 CPU 核心

相关推荐
刘某的Cloud10 小时前
shell脚本-read-输入
linux·运维·bash·shell·read
聪明努力的积极向上16 小时前
【设计优化】卫语句、策略模式、状态模式
bash·状态模式·策略模式
凯新生物1 天前
聚乙二醇二生物素,Biotin-PEG-Biotin在生物检测中的应用
scala·bash·laravel·perl
一勺菠萝丶2 天前
执行 install.sh 报错 `env: ‘bash\r‘: No such file or directory` 怎么解决?
开发语言·bash
Alaia.2 天前
【T级别数据迁移】Oracle 数据库迁移操作手册(oracle-migrate-bash)
数据库·oracle·bash
李斯维2 天前
第14 章 使用 shell:初始化文件
linux·bash·unix
BullSmall3 天前
Shell脚本波浪号避坑指南
linux·bash
顾安r3 天前
12.15 脚本网页 bash内建命令
java·前端·javascript·html·bash
_OP_CHEN3 天前
【Linux系统编程】(十五)揭秘 Linux 环境变量:从底层原理到实战操作,一篇吃透命令行参数与全局变量!
linux·运维·操作系统·bash·进程·环境变量·命令行参数