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 核心

相关推荐
dingdingfish4 小时前
Bash 学习 - 第1章:Introduction
bash·shell·programming·introduction
xuchaoxin13751 天前
bash中的字符串处理@输出和格式化打印@echo@printf
chrome·bash
xuchaoxin13751 天前
bash@特殊字符@环境变量符号@特殊参数@参数扩展和替换@字符串处理用法总结
开发语言·bash
dingdingfish1 天前
Bash 学习 - 第2章:Definitions
bash·definition
xuchaoxin13751 天前
bash@参数扩展@参数转换@参数扩展操作符
bash
提娜米苏2 天前
非Root环境下的数据挂载解决方案:SSHFS与Mount详解
bash·sshfs
这儿有一堆花2 天前
任何东西都可以转成 Base64!?
bash
wasp5202 天前
Banana Slides 深度解析:AI Core 架构设计与 Prompt 工程实践
人工智能·prompt·bash
市场部需要一个软件开发岗位2 天前
一个无人机平台+算法监督平台的离线部署指南
java·python·算法·bash·无人机·持续部署
dingdingfish2 天前
Bash学习笔记总目录
bash·script·programming