记一次Jenkins调用python脚本未显式重定向导致的BrokenPipe问题

116 阅读3分钟

在 Jenkins 环境中,nohup python3 keep_running_script.py > output.log 2>&1 & 和 nohup python3 keep_running_script.py & 的关键区别在于 标准输出 / 错误流的处理方式,这直接影响子进程(如 multiprocessing.Pool)的稳定性。以下是详细分析:

一、命令差异解析

1. nohup python3 keep_running_script.py > output.log 2>&1 &

  • 标准输出(stdout)和标准错误(stderr)  被重定向到 output.log 文件。

  • 子进程继承的文件描述符

    • stdout 和 stderr 指向 output.log,即使 Jenkins 构建结束,文件仍可写入。
    • stdin 保持打开状态(默认连接到 /dev/tty),但通常不会被使用。

2. nohup python3 keep_running_script.py &

  • 标准输出和标准错误 未被显式重定向:

    • 默认情况下,nohup 会将输出写入当前目录的 nohup.out 文件。
    • 但在 Jenkins 环境中,构建结束后,Jenkins 会 关闭其创建的 shell 会话的所有文件描述符,包括 nohup.out
  • 子进程继承的文件描述符

    • stdout 和 stderr 指向 Jenkins 构建的输出流,构建结束后这些流会被关闭。

二、为什么第二种写法会导致 BrokenPipeError

1. 问题根源

当 Jenkins 构建结束时:

  • Jenkins 会关闭其创建的 shell 会话的所有文件描述符,包括 nohup.out(若未显式重定向)。
  • keep_running_script.py 中的 multiprocessing.Pool 子进程可能仍在尝试向这些已关闭的文件描述符写入数据(如日志、错误信息),触发 BrokenPipeError

2. 具体场景

假设 keep_running_script.py 包含以下代码:

from multiprocessing import Pool

def worker(task):
    print(f"Processing task {task}")  # 尝试写入 stdout
    return task * 2

if __name__ == "__main__":
    with Pool(processes=4) as pool:
        results = pool.map(worker, range(10))
  • 当使用 nohup ... > output.log ... 时
    stdout 指向 output.log,构建结束后仍可写入,不会报错。
  • 当使用 nohup ...(无重定向)时
    stdout 指向 Jenkins 构建的输出流,构建结束后该流被关闭,子进程写入时触发 BrokenPipeError

三、解决方案

1. 始终显式重定向输出

确保所有输出被定向到持久文件,而非 Jenkins 构建的临时流:

nohup python3 keep_running_script.py > /var/log/device_client.log 2>&1 &

2. 在 Python 代码中禁用输出缓冲

避免子进程的输出缓冲导致管道阻塞:

# 在 keep_running_script.py 开头添加
import os
import sys
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)  # 行缓冲
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1)

3. 使用 spawn 启动方法

强制子进程独立初始化,避免继承父进程的文件描述符:

import multiprocessing as mp
mp.set_start_method('spawn')  # 在主模块顶部设置

4. 在 Jenkins 中管理后台进程

记录进程 ID(PID)并在构建结束前终止进程:

nohup python3 keep_running_script.py > /var/log/device_client.log 2>&1 &
echo $! > /tmp/device_client.pid

# 在 Jenkinsfile 中添加清理步骤
cleanup() {
    if [ -f /tmp/device_client.pid ]; then
        PID=$(cat /tmp/device_client.pid)
        echo "Stopping process $PID"
        kill -TERM $PID || kill -KILL $PID
        rm -f /tmp/device_client.pid
    fi
}

trap cleanup EXIT  # 构建结束时执行清理

四、总结

命令输出处理Jenkins 构建结束后BrokenPipeError 风险
nohup ... > output.log ...定向到文件文件保持打开
nohup ...(无重定向)定向到 Jenkins 输出流流被关闭,子进程写入报错

核心建议:在 Jenkins 中启动任何后台进程时,必须显式重定向标准输出 / 错误到持久文件,并妥善管理进程生命周期。这能避免因 Jenkins 构建结束导致的管道关闭问题,确保 multiprocessing.Pool 稳定运行。