Hive Metastore 从 nohup 手工启动迁移到 systemd 托管
背景
在 Hadoop 3 + Hive 3 的学习环境中,Hive Metastore 通常是通过如下方式启动的:
bash
nohup hive --service metastore -p 9083 > /tmp/hive-metastore.log 2>&1 &
这条命令简单直接,几秒钟就能把 metastore 拉起来。在初期搭建和调试阶段,它完全够用------能跑通就行。但随着集群使用频率增加,nohup 方式的短板开始逐渐暴露。
本文记录了在 hadoop3 节点上,将 Hive Metastore 从 nohup 手工启动迁移到 systemd 托管的完整过程,包括迁移动机、配置细节、验证步骤,以及一个日常管理脚本。
为什么不建议长期使用 nohup 管理 Hive Metastore
每次重启都要手动恢复
只要 hadoop3 主机重启(断电、维护、内核升级),metastore 进程就会消失。你必须重新登录、手工执行启动命令,才能让集群恢复正常。如果当时不在电脑旁,或者忘记了,所有依赖 metastore 的操作------Hive 查询、Spark 读 Hive 表------全部会报连接失败。排查半天才想起来"哦对,metastore 没启动",这种体验并不愉快。
进程崩溃没有任何通知
nohup 启动的进程没有任何监控和自愈机制。metastore 如果因为内存不足、MySQL 连接超时、或者其他异常原因悄悄崩溃,日志文件里可能有报错,但不会有任何主动通知。你只会在执行 Hive 操作时收到一个莫名其妙的连接错误,再去 jps 一查才发现进程早就没了。如果正在跑一个长时间的作业,中途 metastore 挂掉会直接导致作业失败。
停止进程的方式很原始
要停掉 nohup 启动的 metastore,流程是:先 jps 找到 RunJar 的 PID,再 kill PID。如果机器上同时跑着 HBase、HDFS、YARN 等多个 Java 进程,jps 输出里一堆 RunJar,你得靠经验判断哪个才是 metastore。操作繁琐,而且容易误杀其他进程。
日志管理会失控
/tmp/hive-metastore.log 这个文件没有任何轮转机制。metastore 运行越久,文件越大。/tmp 分区空间本来就有限,时间长了可能被撑满,进而影响其他依赖 /tmp 的程序。出问题要翻日志时,面对一个几百 MB 的单文件也很难快速定位。
总结 :短期学习用 nohup 完全没问题,能跑通就行。但如果集群要持续使用、跑真实作业,这些问题迟早会踩到。
为什么迁移到 systemd
systemd 是 CentOS 7 / RHEL 7 及以后版本的标准服务管理器,用它来托管 Hive Metastore 可以一次性解决上面提到的所有问题:
| 问题 | nohup | systemd |
|---|---|---|
| 开机自启 | 需要手动操作 | systemctl enable 一条命令搞定 |
| 崩溃重启 | 没有任何恢复机制 | Restart=on-failure 自动拉起 |
| 启停管理 | jps + kill,容易误杀 |
systemctl start/stop/restart,明确针对具体服务 |
| 日志管理 | 单文件无限增长 | journald 自动轮转,支持按时间、按服务过滤 |
| 状态查询 | 依赖 ps/jps 猜测 |
systemctl status 实时显示运行状态、PID、资源占用 |
方向明确了,下面进入实操。
迁移步骤
第一步:停掉现有的 nohup 进程
迁移之前,先确保旧的 metastore 进程已经完全退出,端口 9083 空出来。
bash
ss -lntp | grep 9083
找到占用 9083 的进程 PID 后,用 kill 停掉它:
bash
kill <PID>
sleep 2
ss -lntp | grep 9083
如果 grep 9083 还有输出,说明进程没有完全退出,把新看到的 PID 再停一次,直到 9083 端口确认空闲。
第二步:创建 systemd 服务文件
这是迁移的核心步骤。在 /etc/systemd/system/ 下创建 hive-metastore.service:
直接运行下面的命令即可
bash
cat >/etc/systemd/system/hive-metastore.service <<'EOF'
[Unit]
Description=Apache Hive Metastore
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
Environment="JAVA_HOME=/export/servers/jdk1.8.0_241"
Environment="HADOOP_HOME=/export/servers/hadoop-3.3.0"
Environment="HIVE_HOME=/export/servers/hive-3.1.3"
Environment="HIVE_CONF_DIR=/export/servers/hive-3.1.3/conf"
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/export/servers/jdk1.8.0_241/bin:/export/servers/hadoop-3.3.0/bin:/export/servers/hadoop-3.3.0/sbin:/export/servers/hive-3.1.3/bin"
WorkingDirectory=/export/servers/hive-3.1.3
ExecStart=/export/servers/hive-3.1.3/bin/hive --service metastore -p 9083
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
SuccessExitStatus=143
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
几个关键配置项的说明:
Environment行 :这是最容易踩坑的地方。systemd启动的服务不会读取/etc/profile和/etc/profile.d/*.sh中定义的环境变量。所以JAVA_HOME、HADOOP_HOME、HIVE_HOME这些变量必须在 service 文件里显式声明,绝不能省略。你在/etc/profile.d/下配的脚本(比如50-hive.sh)继续保留给日常登录 shell 使用,但 systemd 不会碰它。Type=simple:因为hive --service metastore是前台运行的程序,不会自行 fork 到后台,所以用simple类型即可。Restart=on-failure+RestartSec=5:如果 metastore 异常退出(非正常systemctl stop),systemd 会在 5 秒后自动尝试重新拉起。SuccessExitStatus=143:Java 进程被SIGTERM(即kill)终止时,退出码是 143。把它列为"成功退出状态",可以避免 systemd 在正常停止服务时报 failure。LimitNOFILE=65535:提高文件描述符上限,metastore 在连接较多时可能需要。StandardOutput=journal/StandardError=journal:所有输出统一交给journald管理,不再需要手工维护日志文件。User=root:当前环境为了和原有配置一致,暂时使用 root 运行。更规范的做法是后续创建专用的hive用户,再修改User/Group。
第三步:加载、启用、启动
bash
systemctl daemon-reload
systemctl enable hive-metastore
systemctl start hive-metastore
daemon-reload:让 systemd 加载新建的 service 文件。enable:设置开机自启。start:立即启动服务。
验证
服务启动后,需要从三个维度确认一切正常:
1. 服务运行状态
bash
systemctl status hive-metastore --no-pager -l
预期看到 active (running),以及 enabled 标识(说明开机自启已生效)。
2. 端口监听
bash
ss -lntp | grep 9083
预期看到 9083 端口被一个 java 进程监听。
3. 日志检查
bash
journalctl -u hive-metastore -n 50 --no-pager
预期能看到 metastore 的正常启动日志。如果出现 SLF4J: Class path contains multiple SLF4J bindings 告警,这是 Hive 自带的 jar 与 Hadoop 的 jar 冲突导致的,只是打印告警,不影响 metastore 正常运行,先不用处理。
4. 重启验证(推荐)
如果条件允许,建议做一次完整的重启验证,确认开机自启确实生效:
bash
reboot
重连后再次执行:
bash
systemctl status hive-metastore --no-pager -l
ss -lntp | grep 9083
如果重启后 metastore 自动起来了,整个迁移就彻底闭环了。
日常管理
迁移完成后,以后所有对 Hive Metastore 的管理操作都通过 systemctl 进行:
bash
systemctl start hive-metastore # 启动
systemctl stop hive-metastore # 停止
systemctl restart hive-metastore # 重启
systemctl status hive-metastore --no-pager -l # 查看状态
journalctl -u hive-metastore -f # 实时跟踪日志
不需要再记 nohup 命令,不需要 jps + kill,不需要手动管理日志文件。
管理脚本说明
为了进一步简化日常操作,编写了一个 Bash 管理脚本 hive-metastore.sh,放置在 hadoop3 节点的 /root/ 目录下。
脚本的主要功能包括:
- 启动 / 停止 / 重启 :封装
systemctl命令,启动后自动等待端口就绪并反馈结果 - 状态查看:一次性展示服务运行状态、端口监听情况、开机自启状态
- 日志查看:支持实时跟踪和查看最近 N 行日志
- 开机自启管理:快捷启用/禁用开机自启
- 环境信息:查看 Java 版本、HIVE_HOME、HADOOP_HOME 等环境配置
- 交互菜单 :传入
菜单或menu参数可进入交互式操作界面 - 中英文参数 :支持中文参数(如
启动、停止)和英文参数(如start、stop) - 安全确认 :停止和重启操作会进行二次确认,防止误操作(可通过
-f跳过)
使用示例:
bash
./hive-metastore.sh # 默认查看状态
./hive-metastore.sh 启动 # 启动服务
./hive-metastore.sh stop # 停止服务
./hive-metastore.sh 菜单 # 进入交互菜单
./hive-metastore.sh 帮助 # 查看全部命令
创建脚本文件后,记得授予执行权限:
bash
chmod +x /root/hive-metastore.sh
附录:Hive Metastore 管理脚本(原样)
以下是完整的管理脚本内容,逐字保留,未做任何修改。
bash
#!/bin/bash
set -uo pipefail
# ===== 配置区 =====
SERVICE_NAME="hive-metastore"
PORT="9083"
WAIT_TIMEOUT=30 # 端口等待超时(秒)
SCRIPT_PATH="$(readlink -f "$0")"
# ===== 颜色 =====
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
ok() { echo -e "${GREEN}[✔]${NC} $*"; }
fail() { echo -e "${RED}[✘]${NC} $*"; }
info() { echo -e "${CYAN}[ℹ]${NC} $*"; }
warn() { echo -e "${YELLOW}[⚠]${NC} $*"; }
# ===== 权限检查 =====
require_root() {
if [[ $EUID -ne 0 ]]; then
fail "此操作需要 root 权限,请使用 sudo 或以 root 身份运行"
exit 1
fi
}
# ===== 端口等待 =====
wait_for_port() {
local timeout=$1
local elapsed=0
info "等待端口 ${PORT} 就绪(超时 ${timeout}s)..."
while (( elapsed < timeout )); do
if ss -lntp 2>/dev/null | grep -q ":${PORT} "; then
ok "端口 ${PORT} 已就绪(耗时 ${elapsed}s)"
return 0
fi
sleep 1
(( elapsed++ ))
done
fail "端口 ${PORT} 在 ${timeout}s 内未就绪"
return 1
}
# ===== 二次确认 =====
confirm_action() {
local action="$1"
if [[ "${FORCE:-}" == "true" ]]; then
return 0
fi
if [ ! -t 0 ]; then
# 非交互终端,跳过确认
return 0
fi
read -r -p "确认要${action} ${SERVICE_NAME}?[y/N] " answer
case "$answer" in
[yY]|[yY][eE][sS]) return 0 ;;
*) info "已取消"; exit 0 ;;
esac
}
# ===== 标题 =====
show_title() {
echo -e "${BOLD}========================================"
echo " Hive Metastore 管理脚本"
echo " 服务名: ${SERVICE_NAME}"
echo " 端口: ${PORT}"
echo -e "========================================${NC}"
}
# ===== 状态 =====
show_status() {
echo
info "服务状态:"
if systemctl is-active --quiet "${SERVICE_NAME}"; then
ok "${SERVICE_NAME} 正在运行"
else
fail "${SERVICE_NAME} 未运行"
fi
# 详细状态
systemctl status "${SERVICE_NAME}" --no-pager -l 2>/dev/null || true
echo
info "端口 ${PORT} 监听情况:"
local port_info
port_info=$(ss -lntp 2>/dev/null | grep ":${PORT} ")
if [[ -n "$port_info" ]]; then
ok "端口 ${PORT} 正在监听"
echo "$port_info"
else
warn "未发现端口 ${PORT} 监听"
fi
echo
info "开机自启:"
if systemctl is-enabled --quiet "${SERVICE_NAME}" 2>/dev/null; then
ok "已启用开机自启"
else
warn "未启用开机自启"
fi
}
# ===== 启动 =====
start_service() {
require_root
if systemctl is-active --quiet "${SERVICE_NAME}"; then
warn "${SERVICE_NAME} 已经在运行中,无需重复启动"
show_status
return 0
fi
info "正在启动 ${SERVICE_NAME} ..."
if systemctl start "${SERVICE_NAME}"; then
wait_for_port "${WAIT_TIMEOUT}"
ok "启动命令已执行"
else
fail "启动失败,请检查日志: journalctl -u ${SERVICE_NAME} -n 50"
return 1
fi
show_status
}
# ===== 停止 =====
stop_service() {
require_root
confirm_action "停止"
if ! systemctl is-active --quiet "${SERVICE_NAME}"; then
warn "${SERVICE_NAME} 当前未运行"
return 0
fi
info "正在停止 ${SERVICE_NAME} ..."
if systemctl stop "${SERVICE_NAME}"; then
ok "服务已停止"
else
fail "停止失败"
return 1
fi
systemctl status "${SERVICE_NAME}" --no-pager -l 2>/dev/null || true
}
# ===== 重启 =====
restart_service() {
require_root
confirm_action "重启"
info "正在重启 ${SERVICE_NAME} ..."
if systemctl restart "${SERVICE_NAME}"; then
wait_for_port "${WAIT_TIMEOUT}"
ok "重启命令已执行"
else
fail "重启失败,请检查日志: journalctl -u ${SERVICE_NAME} -n 50"
return 1
fi
show_status
}
# ===== 日志 =====
follow_logs() {
info "实时查看 ${SERVICE_NAME} 日志,按 Ctrl+C 退出"
journalctl -u "${SERVICE_NAME}" -f
}
recent_logs() {
local lines="${1:-50}"
info "最近 ${lines} 行日志"
journalctl -u "${SERVICE_NAME}" -n "${lines}" --no-pager
}
# ===== 版本 =====
show_version() {
info "Hive 版本信息"
if command -v hive &>/dev/null; then
hive --version
else
fail "未找到 hive 命令,请检查 PATH 或 Hive 安装"
fi
}
# ===== 端口 =====
show_port() {
info "端口 ${PORT} 监听情况"
local port_info
port_info=$(ss -lntp 2>/dev/null | grep ":${PORT} ")
if [[ -n "$port_info" ]]; then
ok "端口 ${PORT} 正在监听"
echo "$port_info"
else
warn "未发现端口 ${PORT} 监听"
fi
}
# ===== 开机自启管理 =====
enable_service() {
require_root
if systemctl enable "${SERVICE_NAME}" 2>/dev/null; then
ok "已启用 ${SERVICE_NAME} 开机自启"
else
fail "启用开机自启失败"
fi
}
disable_service() {
require_root
confirm_action "禁用开机自启"
if systemctl disable "${SERVICE_NAME}" 2>/dev/null; then
ok "已禁用 ${SERVICE_NAME} 开机自启"
else
fail "禁用开机自启失败"
fi
}
# ===== 环境信息 =====
show_env() {
info "环境信息"
echo " 脚本路径: ${SCRIPT_PATH}"
echo " 当前用户: $(whoami)"
echo " 主机名: $(hostname)"
echo " 系统时间: $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo
if command -v java &>/dev/null; then
echo " Java 版本:"
java -version 2>&1 | sed 's/^/ /'
else
warn " 未找到 java 命令"
fi
echo
if [[ -n "${HIVE_HOME:-}" ]]; then
echo " HIVE_HOME: ${HIVE_HOME}"
else
warn " HIVE_HOME 未设置"
fi
if [[ -n "${HADOOP_HOME:-}" ]]; then
echo " HADOOP_HOME: ${HADOOP_HOME}"
else
warn " HADOOP_HOME 未设置"
fi
}
# ===== 帮助 =====
show_help() {
cat <<USAGE
用法:
${SCRIPT_PATH} 启动 启动服务
${SCRIPT_PATH} 停止 停止服务(会二次确认)
${SCRIPT_PATH} 重启 重启服务(会二次确认)
${SCRIPT_PATH} 状态 查看服务状态(默认)
${SCRIPT_PATH} 日志 实时查看日志
${SCRIPT_PATH} 最近日志 [N] 查看最近 N 行日志(默认 50)
${SCRIPT_PATH} 端口 查看端口监听
${SCRIPT_PATH} 版本 查看 Hive 版本
${SCRIPT_PATH} 自启 启用开机自启
${SCRIPT_PATH} 禁自启 禁用开机自启
${SCRIPT_PATH} 环境 查看环境信息
${SCRIPT_PATH} 菜单 进入交互菜单
${SCRIPT_PATH} 帮助 显示此帮助
也支持英文参数:
start | stop | restart | status | log | logs [N]
port | version | enable | disable | env | menu | help
选项:
-f, --force 跳过二次确认(停止/重启时)
说明:
不带参数时,默认执行"状态"。
只有传"菜单"或"menu"时才进入交互菜单。
USAGE
}
# ===== 菜单 =====
show_menu() {
if [ ! -t 0 ]; then
fail "当前不是可交互终端,不能进入菜单模式。"
info "请改用参数方式,例如:${SCRIPT_PATH} 状态"
exit 1
fi
# 菜单内操作跳过确认
FORCE="true"
while true; do
echo
show_title
echo -e " ${GREEN}1${NC}. 启动服务"
echo -e " ${RED}2${NC}. 停止服务"
echo -e " ${YELLOW}3${NC}. 重启服务"
echo -e " ${CYAN}4${NC}. 查看状态"
echo -e " ${CYAN}5${NC}. 查看实时日志"
echo -e " ${CYAN}6${NC}. 查看最近日志"
echo -e " ${CYAN}7${NC}. 查看端口"
echo -e " ${CYAN}8${NC}. 查看 Hive 版本"
echo -e " ${CYAN}9${NC}. 查看环境信息"
echo -e " ${GREEN}a${NC}. 启用开机自启"
echo -e " ${RED}b${NC}. 禁用开机自启"
echo -e " ${BOLD}0${NC}. 退出"
echo "----------------------------------------"
if ! read -r -p "请输入编号: " choice; then
echo
warn "输入流已结束,退出菜单。"
exit 1
fi
case "$choice" in
1) start_service ;;
2) stop_service ;;
3) restart_service ;;
4) show_status ;;
5) follow_logs ;;
6) recent_logs ;;
7) show_port ;;
8) show_version ;;
9) show_env ;;
a|A) enable_service ;;
b|B) disable_service ;;
0) echo "退出"; exit 0 ;;
*) warn "无效输入,请重新选择" ;;
esac
echo
echo -e "${CYAN}按回车继续...${NC}"
read -r
done
}
# ===== 解析 -f / --force =====
FORCE="false"
ARGS=()
for arg in "$@"; do
case "$arg" in
-f|--force) FORCE="true" ;;
*) ARGS+=("$arg") ;;
esac
done
set -- "${ARGS[@]+"${ARGS[@]}"}"
# ===== 主入口 =====
case "${1:-}" in
"")
# 无参数:显示状态 + 快捷提示
show_status
echo
echo -e "${BOLD}快捷操作(中/英参数二选一):${NC}"
echo -e " $0 ${GREEN}启动${NC} | ${GREEN}start${NC} 启动服务"
echo -e " $0 ${RED}停止${NC} | ${RED}stop${NC} 停止服务"
echo -e " $0 ${YELLOW}重启${NC} | ${YELLOW}restart${NC} 重启服务"
echo -e " $0 ${CYAN}日志${NC} | ${CYAN}log${NC} 查看实时日志"
echo -e " $0 ${CYAN}菜单${NC} | ${CYAN}menu${NC} 进入交互菜单"
echo -e " $0 ${CYAN}帮助${NC} | ${CYAN}help${NC} 查看全部命令"
;;
启动|start) start_service ;;
停止|stop) stop_service ;;
重启|restart) restart_service ;;
状态|status) show_status ;;
日志|log) follow_logs ;;
最近日志|logs) recent_logs "${2:-50}" ;;
端口|port) show_port ;;
版本|version) show_version ;;
自启|enable) enable_service ;;
禁自启|disable) disable_service ;;
环境|env) show_env ;;
菜单|menu) show_menu ;;
帮助|help|-h|--help) show_help ;;
*)
fail "不支持的参数: $1"
echo
show_help
exit 1
;;
esac