【RocketMQ集群实战】Dashboard自动化部署与运维脚本全解析

1. 为什么需要自动化部署脚本?

在真实的线上环境里,尤其是集群部署的场景下,手动去部署和运维 RocketMQ Dashboard 是一件非常“酸爽”的事情。我踩过的坑太多了:每次重启服务都要手动查进程、杀进程、改配置、启动,一不小心端口就被占用了,或者因为 JDK 版本不对启动失败,查看日志还得切到另一个终端。这些重复、琐碎的操作不仅效率低下,还容易出错。

想象一下,你的 RocketMQ 集群有多个 NameServer 和 Broker,分布在不同的机器上。Dashboard 作为监控和管理入口,如果部署和维护过程不能自动化,一旦需要重启或迁移,运维同学就得像救火队员一样,手动登录服务器,执行一系列命令。这显然不是一种优雅的方式。

所以,我决定用 Shell 脚本把整个部署和运维流程固化下来。脚本化的好处是显而易见的:一键执行、过程可重复、结果可预期。无论是初始化部署,还是日常的启动、停止、重启,都能通过脚本快速完成,大大降低了运维复杂度和人为失误的风险。接下来,我就把自己在实战中打磨出来的这套脚本和思路分享给你,你可以直接拿去用,也可以根据自己集群的实际情况进行调整。

2. 环境准备与项目配置

在写脚本之前,我们需要先把基础环境搭建好。这里假设你已经按照我上一篇文章《RocketMQ集群搭建(Docker版-2主2从)》部署好了 RocketMQ 集群。Dashboard 需要连接到这个集群的 NameServer 才能正常工作。

首先,我们需要获取 RocketMQ Dashboard 的项目。访问其 GitHub 仓库的 Release 页面,选择与你的 RocketMQ 版本对应的 Dashboard 版本。如果你用的是 RocketMQ 4.x,就选择 1.x 版本的 Dashboard;如果是 5.x,则需要选择 2.x 版本。这里我以 4.x 集群配套的 1.0.0 版本为例。

下载源码后,用 IDEA 或你喜欢的编辑器打开,找到 src/main/resources/application.properties 配置文件。这里有几个关键配置项必须修改:

# Dashboard 服务绑定的地址,0.0.0.0表示监听所有网卡
server.address=0.0.0.0
# Dashboard 服务端口,根据实际情况调整,避免冲突
server.port=8888

# 你的 RocketMQ 集群 NameServer 地址列表,多个用分号隔开
rocketmq.config.namesrvAddr=192.168.1.101:9876;192.168.1.102:9876

# Dashboard 存储本地数据(如用户配置)的路径,确保有写入权限
rocketmq.config.dataPath=/data/rocketmq-dashboard/data

# 是否开启登录认证,生产环境建议设为 true
rocketmq.config.loginRequired=false

配置完成后,在项目根目录下执行 Maven 打包命令,生成可执行的 JAR 文件:

mvn clean package -Dmaven.test.skip=true

打包成功后,你会得到一个类似 rocketmq-dashboard-1.0.0.jar 的文件。将这个 JAR 文件上传到你的目标服务器上,我习惯放在一个统一的目录下,例如 /usr/local/rocketmq/dashboard。同时,别忘了根据配置文件中 dataPath 的设定,提前创建好目录并赋予权限:

mkdir -p /data/rocketmq-dashboard/data
chmod -R 755 /data/rocketmq-dashboard/data

这一步是很多新手容易忽略的,如果目录不存在或权限不足,Dashboard 启动时就会报错,无法写入本地数据。

3. 核心启动脚本详解

好了,基础工作准备完毕,现在进入重头戏——编写自动化脚本。我们先从最核心的启动脚本 start-dashboard.sh 开始。这个脚本的目标是:智能、安全地启动 Dashboard 服务。它需要处理可能存在的端口冲突、残留进程,并记录进程 ID 以便后续管理。

3.1 脚本变量定义与初始化

一个好的脚本应该将可变的参数集中在开头定义为变量,这样后续维护和修改会非常方便。我们来定义脚本所需的所有变量:

#!/bin/bash

# ================= RocketMQ Dashboard 启动脚本 =================
# 作者:你的名字
# 描述:用于自动化启动 RocketMQ Dashboard,处理端口占用和旧进程

# Dashboard 部署的根目录
DASHBOARD_HOME="/usr/local/rocketmq/dashboard"
# 指定使用的 JDK 路径(RocketMQ 4.x 的 Dashboard 需要 JDK 8)
JAVA_HOME="/usr/local/java/jdk1.8.0_371"
# Dashboard 的 JAR 包名称
JAR_NAME="rocketmq-dashboard-1.0.0.jar"
# 用于记录进程 PID 的文件
PID_FILE="$DASHBOARD_HOME/dashboard.pid"
# 日志输出文件
LOG_FILE="$DASHBOARD_HOME/dashboard.log"
# Dashboard 服务端口
PORT=8888

# 切换到工作目录,确保相对路径生效
cd $DASHBOARD_HOME || { echo "无法进入目录 $DASHBOARD_HOME"; exit 1; }

这里有几个关键点:

  1. JAVA_HOME:明确指定了 JDK 8 的路径。这是因为 RocketMQ 4.x 版本的 Dashboard 内部依赖了特定版本的 Netty,与高版本 JDK(如 JDK 11+)可能存在兼容性问题。如果你用的是 5.x 的 Dashboard,则需要将其指向 JDK 17+。
  2. PID_FILE:这是一个非常重要的设计。将进程号写入文件,为后续的停止、重启或状态查询提供了可靠的依据,避免了通过 ps 命令模糊查找可能带来的误杀。
  3. cd $DASHBOARD_HOME:确保脚本在正确的目录下执行,这样日志、PID 文件都会生成在预期位置,使用相对路径(如 ./$JAR_NAME)也不会出错。

3.2 端口冲突检查与处理

在启动新服务前,检查目标端口是否被占用是一个好习惯。这可以避免因为忘记停止旧服务而导致的启动失败。

echo "[INFO] 开始检查端口 $PORT 是否被占用..."
# 使用 lsof 命令查找监听指定端口的进程PID
PID_ON_PORT=$(sudo lsof -t -i:$PORT 2>/dev/null)

if [ -n "$PID_ON_PORT" ]; then
    echo "[WARN] 端口 $PORT 已被进程 (PID=$PID_ON_PORT) 占用。"
    echo "[INFO] 正在尝试终止该进程..."
    # 先尝试友好地终止进程
    if sudo kill -15 $PID_ON_PORT 2>/dev/null; then
        sleep 2  # 等待进程自行退出
        # 再次检查进程是否仍然存在
        if ps -p $PID_ON_PORT > /dev/null 2>&1; then
            echo "[WARN] 进程未正常退出,强制终止 (kill -9)..."
            sudo kill -9 $PID_ON_PORT
        fi
    else
        echo "[ERROR] 无法终止进程 $PID_ON_PORT,可能需要手动检查。"
        exit 1
    fi
    echo "[INFO] 端口占用已解除。"
else
    echo "[INFO] 端口 $PORT 未被占用。"
fi

这段脚本的逻辑很清晰:先用 lsof 找 PID,如果找到了,先发 SIGTERM (kill -15) 信号让程序有机会做清理工作;等待2秒后如果还在,再发 SIGKILL (kill -9) 强制结束。这是一种相对优雅的处理方式。

3.3 旧进程清理与 PID 文件管理

接下来,我们需要处理可能存在的“僵尸”进程或陈旧的 PID 文件。有时服务异常崩溃,但 PID 文件还在,这会导致脚本误以为服务仍在运行。

echo "[INFO] 检查并清理旧的 Dashboard 进程..."
# 如果 PID 文件存在,读取其中的 PID
if [ -f "$PID_FILE" ]; then
    OLD_PID=$(cat "$PID_FILE" 2>/dev/null)
    if [ -n "$OLD_PID" ]; then
        # 检查该 PID 对应的进程是否真的在运行
        if ps -p $OLD_PID > /dev/null 2>&1; then
            echo "[WARN] 发现正在运行的旧 Dashboard 进程 (PID=$OLD_PID),正在停止..."
            sudo kill -15 $OLD_PID
            sleep 2
            if ps -p $OLD_PID > /dev/null 2>&1; then
                sudo kill -9 $OLD_PID
            fi
            echo "[INFO] 旧进程已停止。"
        else
            echo "[INFO] PID 文件存在,但进程 $OLD_PID 并未运行。"
        fi
    fi
    # 无论进程是否存在,都删除旧的 PID 文件,避免干扰
    rm -f "$PID_FILE"
    echo "[INFO] 旧的 PID 文件已清理。"
fi

这一步和上一步的端口检查是互补的。端口检查针对的是“任何占用端口的进程”,而这一步是针对“我们之前自己启动的 Dashboard 进程”。双管齐下,能最大程度保证启动环境的干净。

3.4 JVM 参数优化与服务启动

环境清理干净后,就可以启动我们的 Dashboard 服务了。直接 java -jar 启动虽然简单,但为了服务更稳定,我们最好设置一些 JVM 参数。

# 设置 JVM 参数,根据服务器内存情况调整
# -Xms 初始堆大小,-Xmx 最大堆大小,这里设为 1GB
# -XX:+UseG1GC 使用 G1 垃圾收集器,适合多核大内存场景
# -XX:MaxGCPauseMillis 设置目标最大GC停顿时间
JAVA_OPTS="-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

echo "[INFO] 正在启动 RocketMQ Dashboard..."
echo "[INFO] JVM 参数: $JAVA_OPTS"
echo "[INFO] NameServer 地址: $NAMESRV_ADDR"

# 使用 nohup 在后台启动服务,并将输出重定向到日志文件
nohup $JAVA_HOME/bin/java $JAVA_OPTS -jar $JAR_NAME \
  --rocketmq.config.namesrvAddr=192.168.1.101:9876 \
  >> "$LOG_FILE" 2>&1 &

# 获取上一条命令(后台启动的Java进程)的PID,并写入PID文件
NEW_PID=$!
echo $NEW_PID > "$PID_FILE"

echo "[SUCCESS] RocketMQ Dashboard 启动成功!"
echo "[INFO] 进程 PID: $NEW_PID"
echo "[INFO] 日志文件: $LOG_FILE"
echo "[INFO] 你可以使用命令 'tail -f $LOG_FILE' 查看启动日志。"

这里有几个实用的技巧:

  1. JAVA_OPTS:我设置了1G的堆内存,对于 Dashboard 这种管理控制台来说基本够用。G1 收集器在 JDK 8 后期版本和高版本 JDK 中表现不错,比默认的 Parallel GC 在延迟上更有优势。
  2. nohup ... &:这是让进程在后台持续运行的关键,即使你关闭了 SSH 连接,服务也不会退出。
  3. >> “$LOG_FILE“ 2>&1:将标准输出和标准错误都重定向追加到日志文件中,方便排查问题。
  4. $!:这是一个特殊的 Shell 变量,代表上一个在后台运行的进程的 PID。我们及时地把它保存到 PID_FILE 中。

将以上所有代码段组合起来,就是一个功能完整的启动脚本了。给它加上执行权限 chmod +x start-dashboard.sh,然后运行 ./start-dashboard.sh,你就会看到清晰的步骤提示和最终的成功信息。

4. 配套停止与重启脚本

有启动就得有停止。一个健壮的停止脚本 stop-dashboard.sh 同样重要。它的设计原则是:优先通过 PID 文件停止,若失败则尝试通过端口查找,确保进程被彻底清理

4.1 优雅停止脚本实现

#!/bin/bash

# ================= RocketMQ Dashboard 停止脚本 =================
DASHBOARD_HOME="/usr/local/rocketmq/dashboard"
PID_FILE="$DASHBOARD_HOME/dashboard.pid"
PORT=8888

echo "=============== 停止 RocketMQ Dashboard ==============="

# 方式一:通过 PID 文件停止(首选)
if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE")
    if [ -n "$PID" ] && ps -p $PID > /dev/null 2>&1; then
        echo "[INFO] 通过 PID 文件找到运行中的 Dashboard (PID=$PID)。"
        echo "[INFO] 发送 SIGTERM 信号,请求进程优雅退出..."
        kill $PID
        # 等待最多10秒,让进程处理完手头工作
        for i in {1..10}; do
            if ! ps -p $PID > /dev/null 2>&1; then
                echo "[INFO] 进程已优雅退出。"
                break
            fi
            sleep 1
            echo -n "."
        done
        echo

        # 如果等待后进程还在,则强制杀死
        if ps -p $PID > /dev/null 2>&1; then
            echo "[WARN] 进程未在指定时间内退出,强制终止 (SIGKILL)..."
            kill -9 $PID
            sleep 1
        fi
        echo "[SUCCESS] 进程 $PID 已停止。"
    else
        echo "[INFO] PID 文件存在,但进程 $PID 未运行。"
    fi
    # 停止后,无论怎样都删除 PID 文件
    rm -f "$PID_FILE"
    echo "[INFO] PID 文件已清理。"
    exit 0
fi

# 方式二:如果 PID 文件不存在或无效,尝试通过端口查找
echo "[INFO] 未找到有效的 PID 文件,尝试通过端口 $PORT 查找进程..."
PID_ON_PORT=$(sudo lsof -t -i:$PORT 2>/dev/null)
if [ -n "$PID_ON_PORT" ]; then
    echo "[INFO] 发现端口 $PORT 的监听进程 (PID=$PID_ON_PORT),正在停止..."
    kill $PID_ON_PORT
    sleep 2
    if ps -p $PID_ON_PORT > /dev/null 2>&1; then
        kill -9 $PID_ON_PORT
    fi
    echo "[SUCCESS] 进程 $PID_ON_PORT 已停止。"
else
    echo "[INFO] 未找到运行在端口 $PORT 上的 Dashboard 进程。"
fi

echo "========================================================"

这个脚本体现了“优雅退出”的思想。先发 SIGTERM,给进程预留处理收尾工作的时间(比如完成正在响应的 HTTP 请求),超时后再用 SIGKILL。这对于 Web 服务来说是比较友好的停止方式。

4.2 一键重启脚本

有了启动和停止脚本,重启脚本 restart-dashboard.sh 就非常简单了,其实就是按顺序调用这两个脚本:

#!/bin/bash

# ================= RocketMQ Dashboard 重启脚本 =================
SCRIPT_DIR=$(cd $(dirname $0); pwd)

echo "=============== 重启 RocketMQ Dashboard ==============="

# 先停止
if [ -f "$SCRIPT_DIR/stop-dashboard.sh" ]; then
    bash "$SCRIPT_DIR/stop-dashboard.sh"
else
    echo "[ERROR] 停止脚本 stop-dashboard.sh 未找到。"
    exit 1
fi

# 等待2秒,确保资源完全释放
sleep 2

# 再启动
if [ -f "$SCRIPT_DIR/start-dashboard.sh" ]; then
    bash "$SCRIPT_DIR/start-dashboard.sh"
else
    echo "[ERROR] 启动脚本 start-dashboard.sh 未找到。"
    exit 1
fi

echo "======================================================"

重启脚本的好处是,它将“停止-等待-启动”这个固定流程封装起来,你只需要执行一条命令,就能完成服务的滚动重启,非常适合在更新配置或 JAR 包后使用。

5. 高级运维:日志监控与健康检查

脚本帮我们解决了部署和启停的问题,但运维工作不止于此。我们还需要知道服务运行得是否健康。这里我分享两个非常实用的进阶脚本。

5.1 实时日志监控与关键错误告警

Dashboard 的日志里藏着很多信息,我们可以写一个脚本,既能方便地跟踪实时日志,又能自动扫描历史日志中的错误。

#!/bin/bash
# monitor-dashboard.sh

DASHBOARD_HOME="/usr/local/rocketmq/dashboard"
LOG_FILE="$DASHBOARD_HOME/dashboard.log"
ALERT_KEYWORDS="ERROR|Exception|OutOfMemory|ConnectException|拒绝连接"

echo "选择监控模式:"
echo "1. 实时跟踪日志 (tail -f)"
echo "2. 扫描今日错误日志"
echo "3. 扫描指定时间范围内的日志"
read -p "请输入选项 (1/2/3): " MODE

case $MODE in
    1)
        echo "[INFO] 开始实时日志跟踪,按 Ctrl+C 退出..."
        tail -f "$LOG_FILE"
        ;;
    2)
        TODAY=$(date +"%Y-%m-%d")
        echo "[INFO] 正在扫描今日 ($TODAY) 的错误日志..."
        grep -E "$ALERT_KEYWORDS" "$LOG_FILE" | grep "$TODAY" | head -20
        ;;
    3)
        read -p "请输入开始时间 (格式: YYYY-MM-DD HH:MM): " START_TIME
        read -p "请输入结束时间 (格式: YYYY-MM-DD HH:MM): " END_TIME
        # 这里可以使用更复杂的 awk 或 sed 进行时间范围过滤
        echo "[INFO] 扫描 $START_TIME 至 $END_TIME 的日志..."
        # 示例:简单实现,实际可根据日志时间格式调整
        awk -v start="$START_TIME" -v end="$END_TIME" '$0 ~ start, $0 ~ end' "$LOG_FILE" | grep -E "$ALERT_KEYWORDS"
        ;;
    *)
        echo "无效选项。"
        ;;
esac

这个脚本提供了三种模式。最常用的是模式1,直接 tail -f,在排查问题时非常直观。模式2和3则用于定期巡检或事后复盘,能快速定位历史问题。

5.2 服务健康检查与自动重启

我们还可以实现一个更智能的脚本,定期检查 Dashboard 的 HTTP 服务是否存活,如果发现异常,就自动尝试重启。

#!/bin/bash
# health-check-dashboard.sh

DASHBOARD_HOME="/usr/local/rocketmq/dashboard"
PORT=8888
HEALTH_URL="http://localhost:$PORT/#/cluster"
RESTART_SCRIPT="$DASHBOARD_HOME/restart-dashboard.sh"
MAX_FAILURES=3  # 连续失败最大次数
FAILURE_COUNT_FILE="$DASHBOARD_HOME/failure.count"

# 检查服务健康状态
check_health() {
    # 使用curl检查HTTP状态码和响应内容,设置超时
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$HEALTH_URL" 2>/dev/null)
    if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "000" ]; then
        # 000 可能是curl超时,但服务在启动中,这里我们根据实际情况判断
        # 更严谨的做法是检查返回内容是否包含特定关键字
        echo "UP"
        return 0
    else
        echo "DOWN (HTTP Code: $HTTP_CODE)"
        return 1
    fi
}

# 主逻辑
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始健康检查..."
CURRENT_STATUS=$(check_health)

if [ "$CURRENT_STATUS" = "UP" ]; then
    echo "[INFO] Dashboard 服务运行正常。"
    # 服务正常,重置失败计数器
    echo 0 > "$FAILURE_COUNT_FILE"
else
    echo "[ERROR] Dashboard 服务异常: $CURRENT_STATUS"
    # 读取历史失败次数
    FAILURE_COUNT=$(cat "$FAILURE_COUNT_FILE" 2>/dev/null || echo 0)
    ((FAILURE_COUNT++))
    echo $FAILURE_COUNT > "$FAILURE_COUNT_FILE"

    echo "[WARN] 连续失败次数: $FAILURE_COUNT / $MAX_FAILURES"

    if [ $FAILURE_COUNT -ge $MAX_FAILURES ]; then
        echo "[CRITICAL] 达到最大失败次数,尝试自动重启服务..."
        if [ -f "$RESTART_SCRIPT" ]; then
            bash "$RESTART_SCRIPT"
            # 重启后重置计数器
            echo 0 > "$FAILURE_COUNT_FILE"
        else
            echo "[ERROR] 重启脚本未找到,无法自动恢复。"
        fi
    fi
fi

你可以把这个脚本加入到服务器的 crontab 中,让它每分钟执行一次,实现简单的故障自愈。当然,在生产环境中,更复杂的健康检查和告警通常会集成到 Prometheus、Zabbix 或专门的 APM 工具中,但这个脚本作为一个轻量级的补充,非常灵活有效。

6. 实战中遇到的坑与解决方案

脚本写好了,但在实际使用过程中,我还是遇到了不少问题。这里把几个典型的“坑”和解决办法列出来,希望能帮你提前避雷。

6.1 JDK 版本兼容性问题

这是最经典的一个坑。现象是:用 JDK 11 或 17 启动 RocketMQ 4.x 的 Dashboard,控制台看起来启动了,但访问页面一直加载不出来,或者报一些奇怪的 Netty 类找不到的错误。

根本原因:RocketMQ 4.x 版本的 Dashboard (1.x) 依赖的 Netty 版本与高版本 JDK 不兼容。

解决方案

  1. 为 Dashboard 单独安装 JDK 8。不要用系统默认的高版本 JDK。
  2. 在启动脚本中,像我们之前做的那样,通过 JAVA_HOME 变量显式、绝对路径地指定 JDK 8 的位置。不要依赖 PATH 环境变量。
  3. 如果你用的是 RocketMQ 5.x 和 Dashboard 2.x,那么它要求 JDK 17+,情况正好相反,你需要确保环境是 JDK 17。

6.2 数据目录权限与路径问题

启动失败,日志里报 Permission denied 或者 Cannot create data path

原因:我们配置的 rocketmq.config.dataPath 指向的目录,运行 Dashboard 的用户(比如 nobody 或者你指定的用户)没有写入权限。

解决方案

  1. 在启动前,确保创建了该目录。
  2. 更关键的是,要赋予合适的权限。通常我会给 755 权限,如果还有问题,可以尝试 775 并将目录所属组改为运行用户所在的组。
    mkdir -p /data/rocketmq-dashboard/data
    chown -R appuser:appgroup /data/rocketmq-dashboard/data
    chmod -R 755 /data/rocketmq-dashboard/data
    
  3. 不要在脚本里图省事直接用 777 权限,这在生产环境有安全风险。

6.3 端口映射与网络访问问题

在 Docker 集群环境下,Dashboard 部署在宿主机上,去连接 Docker 容器内的 NameServer,可能会遇到连接不上。

原因:Docker 容器有独立的网络命名空间。如果 NameServer 地址配置的是容器内 IP(如 172.17.0.2:9876),宿主机上的 Dashboard 是无法直接访问的。

解决方案

  1. 使用宿主机映射端口:在运行 RocketMQ Docker 容器时,通过 -p 9876:9876 将 NameServer 的端口映射到宿主机。这样 Dashboard 配置 namesrvAddr 时,就可以用 宿主机IP:9876
  2. 使用 Docker 网络:创建一个自定义的 Docker 网络(docker network create rmq-net),将 RocketMQ 所有容器和 Dashboard 容器都加入这个网络。这样它们之间可以通过容器名直接通信。
  3. 在我们的脚本中,namesrvAddr 参数一定要配置成 Dashboard 进程能够真正访问到的地址

6.4 资源限制与启动缓慢

在虚拟机或配置较低的服务器上,Dashboard 启动特别慢,甚至超时失败。

原因:JVM 默认的堆内存参数可能不合适,或者服务器资源不足。

解决方案

  1. 在启动脚本的 JAVA_OPTS 中调整堆内存大小。如果服务器内存小,可以适当调低,比如 -Xms512m -Xmx512m
  2. 可以尝试使用 -XX:+UseSerialGC 替代 -XX:+UseG1GC。Serial GC 是单线程的,在资源极其有限的情况下,开销反而比 G1 小。
  3. 检查服务器磁盘 IO 和 CPU 使用率,确保不是其他进程抢占了资源。

把这些解决方案融入到我们的自动化脚本和运维流程里,就能构建出一个非常稳固的 RocketMQ Dashboard 运行环境。脚本的价值就在于,把这些琐碎但重要的细节固化下来,每次执行都是标准、可靠的操作。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值