在 Alpine Linux 上部署服务时,遇到问题,当执行 rc-service <服务名> stop 时,命令提示成功,但通过 psss 查看时,发现进程依然还在运行,甚至端口依然被占用。

1. 问题现象

最初,编写了一个简单的 OpenRC 脚本来启动我的 Go 程序。程序能够正常启动,端口也能正常监听。

但在尝试停止服务时,奇怪的事情发生了:

# 尝试停止服务
v6:~# rc-service phj stop
 * WARNING: phj is already stopped
# 或者
 * Stopping phj ... [ ok ]

# 查看服务状态
v6:~# rc-service phj status
 * status: stopped

# 但是,查看进程列表,它竟然还在!
v6:~# ps aux | grep phj
 5278 root     18:35 [phj_stock_check]

# 端口也还在监听
v6:~# ss -tunlp | grep 37376
tcp   LISTEN  0  4096  *:37376  *:*  users:(("phj_stock_check",pid=5278,fd=3))

进程 5278 就像一个“孤魂野鬼”,脱离了 OpenRC 的管控,kill 命令有时甚至无法杀掉它(或者杀掉后立刻被某种机制重启,或者由于 pid 丢失无法管理)。

2. 原因分析

OpenRC 默认使用 start-stop-daemonsupervise-daemon 来管理后台进程。问题的核心在于:服务管理器找不到对应的 PID 文件(pidfile)来跟踪进程。

在最初的脚本中,我并没有显式指定 pidfile。这就导致了以下后果:

  1. 启动时supervise-daemon 启动了进程,因为它不知道应该把 PID 写到哪里,或者没有锁定 PID。
  2. 停止时:当你执行 stop 命令时,supervise-daemon 尝试去读取 PID 文件以获取需要杀死的进程 ID。由于找不到 PID 文件(或 PID 不匹配),它认为进程已经不存在了,于是什么也没做。
  3. 结果:服务状态显示 stopped,但二进制进程依然在后台运行。

3. 解决方案

要让 supervise-daemon 正确地管理进程的生命周期(启动、停止、重启),我们必须显式指定 pidfile,并确保脚本能够正确处理这个文件。

以下是修复后的服务脚本,路径为 /etc/init.d/phj

#!/sbin/openrc-run

# 1. 指定使用 supervise-daemon 作为监控器
supervisor=supervise-daemon

name="PHJ Stock Checker"
description="拼好鸡监控"

# 2. 配置命令路径、运行用户和工作目录
command="/opt/phj/phj_stock_checker_linux_amd64"
command_user="root"
directory="/opt/phj"

# 3. 【关键修复】显式指定 pidfile
# supervise-daemon 需要这个文件来跟踪进程状态
# ${RC_SVCNAME} 会自动解析为当前服务的名字 (即 phj)
pidfile="/var/run/${RC_SVCNAME}.pid"

# 环境变量
export API_KEY=123

depend() {
    need localmount net
}

start_pre() {
    # 4. 确保目录存在
    mkdir -p /var/run
  
    # 5. 清理可能存在的旧 pid 文件
    # 这一步可以防止因为上次非正常退出(如崩溃)导致的 pid 文件残留,
    # 从而避免服务认为"进程已经在运行"而拒绝启动
    if [ -f "$pidfile" ]; then
        rm -f "$pidfile"
    fi
}

关键修复点解析

  1. pidfile="/var/run/${RC_SVCNAME}.pid"
    这是最重要的一行。它告诉 OpenRC 把进程 ID 写入 /var/run/phj.pid。当执行 stop 时,OpenRC 会读取这个文件,找到对应的 PID 并发送 TERM 信号。

  2. supervisor=supervise-daemon
    确保使用 OpenRC 自带的守护进程管理器,它会自动将你的程序放入后台运行,无需你在命令后面手动加 & 或使用 nohup(这反而会导致双重 fork 或 PID 丢失)。

  3. start_pre() 清理逻辑
    在启动前检查并删除旧的 pid 文件。这是一种健壮性做法,可以避免 "PID file exists, is service already running?" 类型的错误。

4. 验证修复结果

应用新脚本后,执行以下步骤进行验证:

# 1. 确保旧进程已清理 (如果有残留进程)
pkill -9 phj_stock_check

# 2. 重启服务
v6:~# rc-service phj restart
 * Starting PHJ Stock Checker ... [ ok ]

# 3. 检查 PID 文件是否生成
v6:~# cat /var/run/phj.pid
1000
# (这里的数字应该与下面查到的 PID 一致)

# 4. 检查进程和端口
v6:~# ss -tunlp | grep 37376
tcp   LISTEN  0  4096  *:37376  *:*  users:(("phj_stock_check",pid=1000,fd=3))
# 注意这里的 PID 是 1000

# 5. 【终极测试】停止服务
v6:~# rc-service phj stop
 * Stopping PHJ Stock Checker ... [ ok ]

# 6. 确认进程已消失,端口已释放
v6:~# ss -tunlp | grep 37376
v6:~# (没有输出,表示成功停止)

5. 总结

在 Alpine Linux 上编写 OpenRC 服务脚本时,如果遇到“服务停止但进程仍在”的问题,请检查以下几点:

  1. 是否显式指定了 pidfile
  2. pidfile 路径是否有写入权限?
  3. 是否使用了 supervise-daemon 而不是简单的后台启动?
  4. 是否在 start_pre 中处理了残留的 pid 文件?

正确的 pidfile 配置是连接 OpenRC 管理器与实际进程的桥梁,缺少它,服务管理器就是个“瞎子”。

希望这篇文章能帮到你!