多站点部署 AnQiCMS 时,`start.sh` 如何避免不同实例之间的 PID 冲突?

作为一名资深的安企CMS运营人员,我深知在管理多个网站时,系统的稳定性和高效性至关重要。安企CMS凭借其Go语言的高并发特性和多站点管理功能,为我们提供了强大的支持。然而,在同一台服务器上部署多个安企CMS实例时,如何妥善处理进程ID(PID)冲突,确保每个实例独立、稳定运行,是运维中需要关注的核心问题。

理解多站点部署中的 PID 冲突

安企CMS的“多站点管理”功能允许我们在同一套系统架构下管理多个独立网站。这通常意味着每个网站实例需要一个独立的运行进程。Go语言开发的安企CMS通常通过一个二进制文件(默认为anqicms)启动。当我们在服务器上启动多个安企CMS实例时,每个实例都会尝试运行这个二进制文件。

安企CMS提供的 start.sh 脚本,其核心作用是检查名为 anqicms 的进程是否正在运行,如果未运行,则启动它。脚本中的关键部分使用了 ps -ef | grep '\<anqicms\>' 命令来查找进程。在单实例部署中,这没有任何问题。但当多个实例运行在同一台服务器上时,如果所有实例都使用相同的二进制文件名 anqicms,并且都通过各自独立的 start.sh 脚本来管理,那么所有 start.sh 脚本都会同时“看到”所有名为 anqicms 的进程。

这将导致以下问题:

  • 重复启动: 如果 start.sh 检测到 anqicms 进程存在,即使那是其他实例的进程,它也可能认为自己的实例已经运行,从而停止启动。反之,如果检测机制不够精确,可能导致多个脚本尝试启动同一个实例,或者将其他实例误认为是自己。
  • 错误停止: stop.sh 脚本通常通过 kill 命令结合 grep 查找进程ID。如果它查找的是通用的 anqicms 进程名,那么执行 kill -9 $exists 时,可能会错误地终止所有正在运行的安企CMS实例,而不仅仅是目标实例。这种“误杀”对于多站点环境而言是灾难性的。

为了避免这种潜在的风险,我们需要为每个安企CMS实例提供唯一的身份标识,以便它们的管理脚本能够精准地控制各自的进程。

核心策略:为每个实例赋予唯一身份

解决PID冲突的关键在于,让每个安企CMS实例在系统层面拥有一个可区分的唯一标识。安企CMS的官方文档和社区实践中,主要推荐两种策略来达到这个目的。

一、通过唯一的二进制文件名称来区分实例

这是安企CMS官方文档中推荐且最为直观的方法。为每个实例的二进制可执行文件赋予一个独一无二的名称。例如,如果你的网站域名是 site1.comsite2.com,你可以将它们的 anqicms 二进制文件分别重命名为 anqicms_site1anqicms_site2

修改步骤如下:

  1. 复制安企CMS代码并重命名二进制文件: 为每个新站点复制一份完整的安企CMS安装包到一个独立目录(例如 /www/wwwroot/site1/www/wwwroot/site2)。 进入每个站点的根目录,找到 anqicms 可执行文件,并将其重命名。例如:

    
    mv /www/wwwroot/site1/anqicms /www/wwwroot/site1/anqicms_site1
    mv /www/wwwroot/site2/anqicms /www/wwwroot/site2/anqicms_site2
    

  2. 修改 config.json 配置: 确保每个实例的 config.json 文件中配置了唯一的端口号。例如,site1 使用 8001site2 使用 8002

    // /www/wwwroot/site1/config.json
    {
      "port": 8001,
      // ...其他配置
    }
    
    
    // /www/wwwroot/site2/config.json
    {
      "port": 8002,
      // ...其他配置
    }
    
  3. 为每个实例创建独立的 start.shstop.sh 脚本: 每个实例都应该有自己的 start.shstop.sh 脚本,位于其各自的根目录。这些脚本需要更新 BINNAMEBINPATH 变量,以匹配新的二进制文件名和路径。

    start.sh 示例(针对 anqicms_site1 实例):

    #!/bin/bash
    ### check and start AnqiCMS for site1
    # author fesion
    # the bin name is anqicms_site1
    BINNAME=anqicms_site1
    BINPATH=/www/wwwroot/site1 # 更改为当前实例的实际路径
    
    # check the pid if exists using the unique binary name
    exists=`ps -ef | grep '\<${BINNAME}\>' |grep -v grep |wc -l`
    echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} PID check: $exists" >> ${BINPATH}/check.log
    echo "PID ${BINNAME} check: $exists"
    if [ $exists -eq 0 ]; then
        echo "${BINNAME} NOT running"
        cd ${BINPATH} && nohup ${BINPATH}/${BINNAME} >> ${BINPATH}/running.log 2>&1 &
    fi
    

    stop.sh 示例(针对 anqicms_site1 实例):

    #!/bin/bash
    ### stop anqicms for site1
    # author fesion
    # the bin name is anqicms_site1
    BINNAME=anqicms_site1
    BINPATH=/www/wwwroot/site1 # 更改为当前实例的实际路径
    
    # check the pid if exists using the unique binary name
    exists=`ps -ef | grep '\<${BINNAME}\>' |grep -v grep |awk '{printf $2}'`
    echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} PID check: $exists" >> ${BINPATH}/check.log
    echo "PID ${BINNAME} check: $exists"
    if [ $exists -eq 0 ]; then
        echo "${BINNAME} NOT running"
    else
        echo "${BINNAME} is running"
        kill -9 $exists
        echo "${BINNAME} is stop"
    fi
    

    重要提示: 请务必将 BINPATH 变量修改为对应实例的实际部署路径,并确保 BINNAME 与你重命名的二进制文件名称一致。

二、通过 PID 文件进行进程管理(更健壮的运维实践)

虽然上述重命名二进制文件的方法可以有效解决大部分PID冲突问题,但在复杂的运维环境中,依靠PID文件来管理进程是更为健壮和标准化的做法。PID文件存储了特定进程的唯一PID,允许脚本精确地启动、停止和监控单个进程,避免了grep命令可能带来的误判(例如,如果某个不相关的程序恰好包含了anqicms作为其命令行参数的一部分)。

修改步骤如下:

  1. 修改 start.sh 脚本以创建和使用 PID 文件: 在启动AnQiCMS实例时,其start.sh脚本应该将当前进程的PID写入一个特定的PID文件(例如 anqicms.pid)。

    #!/bin/bash
    ### check and start AnqiCMS with PID file
    BINNAME=anqicms # 保持二进制文件名不变,或者使用唯一的名称
    BINPATH=/www/wwwroot/site1 # 更改为当前实例的实际路径
    PIDFILE=${BINPATH}/anqicms.pid # 定义PID文件路径
    
    # 检查PID文件是否存在并且PID是否活跃
    if [ -f "$PIDFILE" ]; then
        PID=$(cat "$PIDFILE")
        if ps -p "$PID" > /dev/null; then
            echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} already running with PID ${PID}" >> ${BINPATH}/check.log
            exit 0
        else
            echo "$(date +'%Y%m%d %H:%M:%S') PID file exists but process not running. Cleaning up stale PID file." >> ${BINPATH}/check.log
            rm -f "$PIDFILE"
        fi
    fi
    
    
    echo "$(date +'%Y%m%d %H:%M:%S') Starting ${BINNAME}..." >> ${BINPATH}/check.log
    cd ${BINPATH}
    nohup ${BINPATH}/${BINNAME} >> ${BINPATH}/running.log 2>&1 &
    echo $! > "$PIDFILE" # 将新启动的进程PID写入PID文件
    echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} started with PID $(cat "$PIDFILE")" >> ${BINPATH}/check.log
    
  2. 修改 stop.sh 脚本以读取 PID 文件并终止进程: stop.sh脚本将从对应的PID文件中读取PID,并只终止该PID的进程。 “`bash #!/bin/bash

    stop AnqiCMS with PID file

    BINNAME=anqicms # 保持二进制文件名不变,或者使用唯一的名称 BINPATH=/www/wwwroot/site1 # 更改为当前实例的实际路径 PIDFILE=${BINPATH}/anqicms.pid # 定义PID文件路径

    if [ -f “$PIDFILE” ]; then

    PID=$(cat "$PIDFILE")
    if ps -p "$PID" > /dev/null; then
        echo "$(date +'%Y%m%d %H:%M:%S') Stopping ${BINNAME} with PID ${PID}..." >> ${BINPATH}/check.log
        kill "$PID" # 发送TERM信号
        sleep 5 # 等待进程优雅关闭
        if ps -p "$PID" > /dev/null; then
            echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} (PID ${PID}) did not terminate gracefully. Forcing kill." >> ${BINPATH}/check.log
            kill -9 "$PID" # 强制终止
        fi
        rm -f "$PIDFILE" # 移除PID文件
        echo "$(date +'%Y%m%d %H:%M:%S') ${BINNAME} stopped." >> ${BINPATH}/check.log
    else
        echo "$(date +'%Y%m%d %H:%M:%S')