信创实战:Java 服务管理脚本(生产级)
系列:信创国产化适配 | 作者:lin | 日期:2026-05-15
一、背景与问题
1.1 为什么需要专业的 Java 服务管理脚本
在很多项目中,Java 服务的启动方式还停留在:
# ❌ 野蛮启动方式
nohup java -jar app.jar > app.log 2>&1 &
echo $! > app.pid这种方式存在诸多问题:
| 问题 | 影响 |
|---|---|
| 进程意外退出后无法自动重启 | 服务不可用,需人工介入 |
停止服务直接 kill -9 | 数据丢失、连接池未关闭 |
| 没有 PID 管理 | 重复启动、僵尸进程 |
| 日志输出到 nohup.out | 日志无法轮转,磁盘被打满 |
| 无法集成 systemd | 不符合 Linux 服务管理规范 |
| 每次部署手动操作 | 容易出错,效率低下 |
1.2 设计目标
我们要实现一个生产级的 Java 服务管理脚本,满足以下要求:
- 自动检测 Java:JAVA_HOME → 常见安装路径 → PATH,三级探测
- 自动识别 jar:无需硬编码 jar 文件名
- 首次部署兼容:自动创建目录结构、移动配置文件
- systemd 集成:一键安装/卸载系统服务
- 受控停机:发送 SIGTERM 并等待,超时后再由策略决定是否发送 SIGKILL
- PID 文件管理:防止重复启动,精确控制进程
二、JVM 堆内存分配公式
在配置 JAVA_OPTS 之前,理解 JVM 内存分配至关重要:
- JVM Total Memory = Heap + Metaspace + Stack + Code Cache + Direct Memory + Others
- Heap = Young Gen + Old Gen
- Young Gen = Eden + 2 × Survivor
对于 G1GC,堆内存建议配置:
- Xms = Xmx(避免堆动态伸缩带来的 GC 停顿)
- Xmx ≤ 0.75 × Container Memory(预留内存给 OS 和非堆区域)
连接池大小的经验公式:Max Pool Size = CPU Cores × 2 + Disk Spindles
信创环境内存配置
信创国产 CPU(申威、龙芯)通常性能低于同代 x86,GC 停顿时间更长。建议适当减小堆大小(降低 GC 压力),增大 Code Cache(弥补 CPU 指令集效率差异)。例如:
-Xmx1g -XX:ReservedCodeCacheSize=256m。三、完整脚本代码
#!/bin/bash
# ============================================================
# java-service.sh - 生产级 Java 服务管理脚本
#
# 功能:启动/停止/重启/状态/日志/安装systemd服务
# 适用:信创环境(申威/龙芯/鲲鹏/x86_64 通用)
# 作者:lin
# 版本:1.1.0
# ============================================================
set -euo pipefail
# ============================================================
# 配置区(可根据项目需要修改)
# ============================================================
# 服务名称(用于 systemd 服务名、日志目录名等)
SERVICE_NAME="${SERVICE_NAME:-my-java-app}"
# 应用根目录(脚本所在目录的上一级)
APP_HOME="$(cd "$(dirname "$0")/.." && pwd)"
# 目录结构
APP_BIN="${APP_HOME}/bin" # 脚本目录
APP_CONF="${APP_HOME}/conf" # 配置目录
APP_LIB="${APP_HOME}/lib" # jar 包目录
APP_LOGS="${APP_HOME}/logs" # 日志目录
APP_PID_FILE="${APP_HOME}/${SERVICE_NAME}.pid"
# JVM 参数(可通过环境变量覆盖)
JAVA_OPTS="${JAVA_OPTS:--Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200}"
# 应用参数(可通过环境变量覆盖)
APP_ARGS="${APP_ARGS:-}"
# 停止超时(秒):先 SIGTERM 等待,超时后 SIGKILL
STOP_TIMEOUT="${STOP_TIMEOUT:-10}"
# 日志文件
APP_LOG="${APP_LOGS}/${SERVICE_NAME}.log"
GC_LOG="${APP_LOGS}/gc.log"
# ============================================================
# 颜色输出
# ============================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ============================================================
# 日志函数
# ============================================================
# 日志输出到 stderr,避免污染 find_java / find_jar 这类函数的 stdout 返回值
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
# ============================================================
# 函数:自动检测 Java 路径
# ============================================================
find_java() {
local java_cmd=""
# 优先级 1:JAVA_HOME 环境变量
if [ -n "${JAVA_HOME:-}" ] && [ -x "${JAVA_HOME}/bin/java" ]; then
java_cmd="${JAVA_HOME}/bin/java"
log_info "使用 JAVA_HOME: ${JAVA_HOME}"
echo "${java_cmd}"
return 0
fi
# 优先级 2:常见安装路径探测
local search_paths=(
# Amazon Corretto
"/usr/lib/jvm/java-1.8.0-amazon-corretto/bin/java"
"/usr/lib/jvm/java-11-amazon-corretto/bin/java"
"/usr/lib/jvm/java-17-amazon-corretto/bin/java"
# OpenJDK(RPM 系)
"/usr/lib/jvm/java-1.8.0-openjdk/bin/java"
"/usr/lib/jvm/java-11-openjdk/bin/java"
"/usr/lib/jvm/java-17-openjdk/bin/java"
# OpenJDK(DEB 系)
"/usr/lib/jvm/java-8-openjdk-amd64/bin/java"
"/usr/lib/jvm/java-11-openjdk-amd64/bin/java"
# 申威定制 JDK
"/usr/local/swjdk/bin/java"
"/opt/swjdk/bin/java"
# 龙芯定制 JDK
"/usr/local/loongjdk/bin/java"
"/opt/loongjdk/bin/java"
# 通用路径
"/usr/local/java/bin/java"
"/opt/java/bin/java"
)
for path in "${search_paths[@]}"; do
if [ -x "${path}" ]; then
java_cmd="${path}"
log_info "自动检测到 Java: ${path}"
echo "${java_cmd}"
return 0
fi
done
# 优先级 3:PATH 中查找
if command -v java &>/dev/null; then
java_cmd="$(command -v java)"
log_info "从 PATH 中找到 Java: ${java_cmd}"
echo "${java_cmd}"
return 0
fi
log_error "未找到 Java!请设置 JAVA_HOME 或确保 java 在 PATH 中"
return 1
}
# ============================================================
# 函数:自动查找 jar 文件
# ============================================================
find_jar() {
local jar_file=""
# 在 lib 目录中查找 jar 文件
if [ -d "${APP_LIB}" ]; then
jar_file=$(find "${APP_LIB}" -maxdepth 1 -name "*.jar" -type f | head -1)
fi
# 如果 lib 目录没有,在应用根目录查找
if [ -z "${jar_file}" ]; then
jar_file=$(find "${APP_HOME}" -maxdepth 1 -name "*.jar" -type f | head -1)
fi
if [ -z "${jar_file}" ]; then
log_error "未找到 jar 文件!请将 jar 包放入 ${APP_LIB}/ 或 ${APP_HOME}/ 目录"
return 1
fi
echo "${jar_file}"
}
# ============================================================
# 函数:首次部署智能初始化
# ============================================================
init_deployment() {
log_info "检测到首次部署,初始化目录结构..."
# 创建标准目录
mkdir -p "${APP_BIN}" "${APP_CONF}" "${APP_LIB}" "${APP_LOGS}"
# 如果根目录有 jar 文件,移动到 lib 目录
local root_jars
root_jars=$(find "${APP_HOME}" -maxdepth 1 -name "*.jar" -type f 2>/dev/null || true)
if [ -n "${root_jars}" ]; then
echo "${root_jars}" | while read -r jar; do
log_info "移动 jar 文件到 lib 目录: $(basename "${jar}")"
mv "${jar}" "${APP_LIB}/"
done
fi
# 如果根目录有配置文件,移动到 conf 目录
local config_patterns=("*.yml" "*.yaml" "*.properties" "*.xml" "*.conf")
for pattern in "${config_patterns[@]}"; do
local files
files=$(find "${APP_HOME}" -maxdepth 1 -name "${pattern}" -type f 2>/dev/null || true)
if [ -n "${files}" ]; then
echo "${files}" | while read -r f; do
log_info "移动配置文件到 conf 目录: $(basename "${f}")"
mv "${f}" "${APP_CONF}/"
done
fi
done
# 如果存在配置文件,显示信息
if [ -d "${APP_CONF}" ] && [ "$(ls -A "${APP_CONF}" 2>/dev/null)" ]; then
log_info "配置文件已就绪: ${APP_CONF}/"
ls -la "${APP_CONF}/"
fi
log_info "目录结构初始化完成"
echo ""
echo " ${APP_HOME}/"
echo " ├── bin/ # 脚本目录"
echo " ├── conf/ # 配置文件"
echo " ├── lib/ # jar 包"
echo " ├── logs/ # 日志目录"
echo " └── ${SERVICE_NAME}.pid # PID 文件"
echo ""
}
# ============================================================
# 函数:构建启动命令
# ============================================================
build_start_command() {
local java_cmd="$1"
local jar_file="$2"
# 构建 classpath 参数(如果有 conf 目录)
local cp_args=""
if [ -d "${APP_CONF}" ] && [ "$(ls -A "${APP_CONF}" 2>/dev/null)" ]; then
cp_args="--spring.config.additional-location=file:${APP_CONF}/"
fi
# 构建 GC 日志参数(JDK 8)
local gc_log_args=""
if [ -f "${GC_LOG}" ] || [ ! -e "${GC_LOG}" ]; then
gc_log_args="-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${GC_LOG}"
fi
# 组装完整启动命令
# shellcheck disable=SC2086
echo "${java_cmd} \
${JAVA_OPTS} \
${gc_log_args} \
-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} \
-Dapp.home=${APP_HOME} \
-Dapp.name=${SERVICE_NAME} \
-jar ${jar_file} \
${cp_args} \
${APP_ARGS}"
}
# ============================================================
# 函数:读取 PID
# ============================================================
get_pid() {
if [ -f "${APP_PID_FILE}" ]; then
local pid
pid=$(cat "${APP_PID_FILE}" 2>/dev/null)
if [ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null; then
echo "${pid}"
return 0
fi
fi
return 1
}
# ============================================================
# 命令:start(启动服务)
# ============================================================
cmd_start() {
# 检查是否已在运行
if pid=$(get_pid); then
log_warn "服务已在运行中 (PID: ${pid})"
log_warn "如需重启,请使用: $0 restart"
return 1
fi
# 检测 Java
local java_cmd
java_cmd=$(find_java) || return 1
# 检测 jar 文件
local jar_file
jar_file=$(find_jar) || return 1
log_info "========================================="
log_info " 启动服务: ${SERVICE_NAME}"
log_info "========================================="
log_info " Java: ${java_cmd}"
log_info " Jar: ${jar_file}"
log_info " Opts: ${JAVA_OPTS}"
log_info " Logs: ${APP_LOG}"
log_info "========================================="
# 确保日志目录存在
mkdir -p "${APP_LOGS}"
# 构建启动命令
local start_cmd
start_cmd=$(build_start_command "${java_cmd}" "${jar_file}")
# 启动进程(后台运行,日志输出到文件)
nohup bash -c "${start_cmd}" >> "${APP_LOG}" 2>&1 &
local pid=$!
# 保存 PID
echo "${pid}" > "${APP_PID_FILE}"
# 等待 2 秒检查进程是否存活
sleep 2
if kill -0 "${pid}" 2>/dev/null; then
log_info "✅ 服务启动成功 (PID: ${pid})"
log_info "日志文件: ${APP_LOG}"
log_info "查看日志: tail -f ${APP_LOG}"
else
log_error "❌ 服务启动失败,请检查日志: ${APP_LOG}"
rm -f "${APP_PID_FILE}"
return 1
fi
}
# ============================================================
# 命令:stop(停止服务 - 优雅停机)
# ============================================================
cmd_stop() {
local pid
if ! pid=$(get_pid); then
log_warn "服务未运行"
rm -f "${APP_PID_FILE}"
return 0
fi
log_info "正在停止服务 (PID: ${pid})..."
# 第一步:发送 SIGTERM(优雅停机)
log_info "发送 SIGTERM 信号,等待 ${STOP_TIMEOUT} 秒..."
kill "${pid}" 2>/dev/null
# 等待进程退出
local count=0
while [ ${count} -lt ${STOP_TIMEOUT} ]; do
if ! kill -0 "${pid}" 2>/dev/null; then
log_info "✅ 服务已优雅停止"
rm -f "${APP_PID_FILE}"
return 0
fi
sleep 1
count=$((count + 1))
# 每 3 秒输出一次等待状态
if [ $((count % 3)) -eq 0 ]; then
log_info "等待进程退出... (${count}/${STOP_TIMEOUT}s)"
fi
done
# 第二步:SIGTERM 超时,发送 SIGKILL 强制停止
log_warn "SIGTERM 超时,发送 SIGKILL 强制停止..."
kill -9 "${pid}" 2>/dev/null
# 再等 2 秒确认
sleep 2
if kill -0 "${pid}" 2>/dev/null; then
log_error "❌ 无法停止进程 (PID: ${pid})"
return 1
fi
log_warn "⚠️ 服务已被强制停止"
rm -f "${APP_PID_FILE}"
}
# ============================================================
# 命令:restart(重启服务)
# ============================================================
cmd_restart() {
log_info "重启服务: ${SERVICE_NAME}"
cmd_stop
sleep 1
cmd_start
}
# ============================================================
# 命令:status(查看服务状态)
# ============================================================
cmd_status() {
local pid
if pid=$(get_pid); then
log_info "✅ 服务运行中 (PID: ${pid})"
echo ""
echo " 服务名称: ${SERVICE_NAME}"
echo " PID: ${pid}"
echo " PID 文件: ${APP_PID_FILE}"
echo " 启动时间: $(ps -o lstart= -p "${pid}" 2>/dev/null || echo 'N/A')"
echo " 内存占用: $(ps -o rss= -p "${pid}" 2>/dev/null | awk '{printf "%.1f MB", $1/1024}' || echo 'N/A')"
echo " CPU 占用: $(ps -o %cpu= -p "${pid}" 2>/dev/null || echo 'N/A')%"
echo ""
# 显示 Java 版本信息
if [ -f "${APP_PID_FILE}" ]; then
local java_version
java_version=$(cat /proc/"${pid}"/cmdline 2>/dev/null | tr '\0' ' ' | grep -oP '(?<=-jar )\S+' || echo 'N/A')
echo " Jar 文件: ${java_version}"
fi
else
log_warn "❌ 服务未运行"
# 清理残留 PID 文件
rm -f "${APP_PID_FILE}" 2>/dev/null
return 1
fi
}
# ============================================================
# 命令:log(查看日志)
# ============================================================
cmd_log() {
local lines="${1:-100}"
if [ -f "${APP_LOG}" ]; then
tail -n "${lines}" -f "${APP_LOG}"
else
log_error "日志文件不存在: ${APP_LOG}"
return 1
fi
}
# ============================================================
# 命令:install(安装 systemd 服务)
# ============================================================
cmd_install_systemd() {
log_info "安装 systemd 服务: ${SERVICE_NAME}"
# 检查 systemd 是否可用
if ! command -v systemctl &>/dev/null; then
log_error "systemctl 不可用,当前系统可能不使用 systemd"
return 1
fi
# 自动检测 Java 路径
local java_cmd
java_cmd=$(find_java) || return 1
# 创建 systemd 服务文件
local service_file="/etc/systemd/system/${SERVICE_NAME}.service"
cat > "${service_file}" << EOF
[Unit]
Description=${SERVICE_NAME} Java Service
Documentation=https://github.com/your-org/${SERVICE_NAME}
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=$(whoami)
Group=$(id -gn)
# 工作目录
WorkingDirectory=${APP_HOME}
# 环境变量
Environment=JAVA_HOME=$(dirname $(dirname "${java_cmd}"))
Environment=SERVICE_NAME=${SERVICE_NAME}
Environment=APP_HOME=${APP_HOME}
EnvironmentFile=-${APP_HOME}/conf/${SERVICE_NAME}.env
# JVM 参数(优先使用环境变量文件中的配置)
Environment="JAVA_OPTS=${JAVA_OPTS}"
# 启动命令(使用脚本的 start 命令)
ExecStart=${APP_BIN}/$(basename "$0") start-foreground
# 停止命令
ExecStop=${APP_BIN}/$(basename "$0") stop
# 重启策略
Restart=on-failure
RestartSec=10
StartLimitBurst=3
StartLimitIntervalSec=60
# 资源限制
LimitNOFILE=65536
LimitNPROC=4096
# 安全加固
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=${APP_HOME}/logs ${APP_HOME}/${SERVICE_NAME}.pid
# 日志输出到 journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}
[Install]
WantedBy=multi-user.target
EOF
# 重载 systemd 配置
systemctl daemon-reload
# 设置开机启动
systemctl enable "${SERVICE_NAME}"
log_info "✅ systemd 服务安装完成"
echo ""
echo " 服务文件: ${service_file}"
echo ""
echo " 使用方法:"
echo " systemctl start ${SERVICE_NAME} # 启动"
echo " systemctl stop ${SERVICE_NAME} # 停止"
echo " systemctl restart ${SERVICE_NAME} # 重启"
echo " systemctl status ${SERVICE_NAME} # 状态"
echo " journalctl -u ${SERVICE_NAME} -f # 查看日志"
echo " systemctl enable ${SERVICE_NAME} # 开机启动"
echo " systemctl disable ${SERVICE_NAME} # 取消开机启动"
echo ""
}
# ============================================================
# 命令:uninstall(卸载 systemd 服务)
# ============================================================
cmd_uninstall_systemd() {
log_info "卸载 systemd 服务: ${SERVICE_NAME}"
systemctl stop "${SERVICE_NAME}" 2>/dev/null || true
systemctl disable "${SERVICE_NAME}" 2>/dev/null || true
local service_file="/etc/systemd/system/${SERVICE_NAME}.service"
if [ -f "${service_file}" ]; then
rm -f "${service_file}"
log_info "已删除服务文件: ${service_file}"
fi
systemctl daemon-reload
log_info "✅ systemd 服务已卸载"
}
# ============================================================
# 命令:start-foreground(前台启动,供 systemd 使用)
# ============================================================
cmd_start_foreground() {
local java_cmd
java_cmd=$(find_java) || return 1
local jar_file
jar_file=$(find_jar) || return 1
local start_cmd
start_cmd=$(build_start_command "${java_cmd}" "${jar_file}")
log_info "前台启动服务: ${SERVICE_NAME}"
exec bash -c "${start_cmd}"
}
# ============================================================
# 使用说明
# ============================================================
usage() {
echo "用法: $0 <命令>"
echo ""
echo "命令:"
echo " start 启动服务(后台运行)"
echo " stop 停止服务(优雅停机)"
echo " restart 重启服务"
echo " status 查看服务状态"
echo " log [lines] 查看日志(默认最后100行,持续跟踪)"
echo " install 安装为 systemd 服务"
echo " uninstall 卸载 systemd 服务"
echo " init 初始化目录结构"
echo " start-foreground 前台启动(供 systemd 使用)"
echo ""
echo "环境变量:"
echo " SERVICE_NAME 服务名称(默认: my-java-app)"
echo " JAVA_OPTS JVM 参数(默认: -Xms512m -Xmx1024m)"
echo " APP_ARGS 应用参数"
echo " STOP_TIMEOUT 停止超时秒数(默认: 10)"
echo " SPRING_PROFILES_ACTIVE Spring 激活的 Profile(默认: prod)"
echo ""
echo "示例:"
echo " # 基本使用"
echo " $0 start"
echo " $0 stop"
echo " $0 status"
echo ""
echo " # 自定义服务名"
echo " SERVICE_NAME=user-service $0 start"
echo ""
echo " # 自定义 JVM 参数"
echo " JAVA_OPTS=\"-Xms1g -Xmx2g -XX:+UseZGC\" $0 start"
echo ""
echo " # 安装为系统服务"
echo " $0 install"
echo " systemctl start my-java-app"
}
# ============================================================
# 主入口
# ============================================================
main() {
local command="${1:-}"
case "${command}" in
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
status) cmd_status ;;
log) cmd_log "${2:-100}" ;;
install) cmd_install_systemd ;;
uninstall) cmd_uninstall_systemd ;;
init) init_deployment ;;
start-foreground) cmd_start_foreground ;;
*) usage ;;
esac
}
main "$@"四、使用方法示例
4.1 首次部署
# 1. 创建应用目录
mkdir -p /opt/my-service/bin
cd /opt/my-service
# 2. 放入脚本
cp java-service.sh bin/java-service.sh
chmod +x bin/java-service.sh
# 3. 放入 jar 包(脚本会自动识别并移动到 lib/ 目录)
cp /path/to/my-service-1.0.0.jar .
# 4. 放入配置文件(可选)
cp application.yml .
# 5. 初始化目录结构(脚本会自动创建目录并移动文件)
./bin/java-service.sh init
# 6. 启动服务
./bin/java-service.sh start4.2 日常运维
# 查看状态
./bin/java-service.sh status
# 查看日志(最后 200 行,持续跟踪)
./bin/java-service.sh log 200
# 重启服务
./bin/java-service.sh restart
# 停止服务
./bin/java-service.sh stop4.3 安装为 systemd 服务
# 安装(需要 root 权限)
sudo ./bin/java-service.sh install
# 之后使用 systemctl 管理
sudo systemctl start my-java-app
sudo systemctl stop my-java-app
sudo systemctl restart my-java-app
sudo systemctl status my-java-app
# 查看服务日志
journalctl -u my-java-app -f
# 开机自启(安装时已自动设置)
sudo systemctl enable my-java-app五、systemd 配置详解
5.1 核心配置说明
[Unit]
Description=my-java-app Java Service
After=network.target # 在网络就绪后启动
Wants=network-online.target # 依赖网络在线
[Service]
Type=simple # Java 应用是简单进程类型
User=appuser # 运行用户(不要用 root!)
Group=appgroup # 运行用户组
WorkingDirectory=/opt/my-service
# 环境变量(支持从文件加载)
Environment=JAVA_HOME=/usr/lib/jvm/java-1.8.0-amazon-corretto
Environment="JAVA_OPTS=-Xms512m -Xmx1024m"
EnvironmentFile=-/opt/my-service/conf/my-java-app.env
# 注意:EnvironmentFile 前面的 - 表示文件不存在时不报错
# 启动/停止命令
ExecStart=/opt/my-service/bin/java-service.sh start-foreground
ExecStop=/opt/my-service/bin/java-service.sh stop
# 重启策略
Restart=on-failure # 仅在异常退出时重启
RestartSec=10 # 重启间隔 10 秒
StartLimitBurst=3 # 60 秒内最多重启 3 次
StartLimitIntervalSec=60 # 重启限制时间窗口
# 资源限制
LimitNOFILE=65536 # 文件描述符限制(Java 应用需要较高值)
LimitNPROC=4096 # 进程数限制
# 安全加固
PrivateTmp=true # 使用私有 /tmp 目录
ProtectSystem=strict # 只读文件系统
ReadWritePaths=/opt/my-service/logs # 允许写入日志目录
# 日志输出到 journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-java-app
[Install]
WantedBy=multi-user.target # 多用户模式启动5.2 环境变量文件
创建 /opt/my-service/conf/my-java-app.env:
# JVM 参数
JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/my-service/logs/heapdump.hprof
# Spring Profile
SPRING_PROFILES_ACTIVE=prod
# 应用参数
APP_ARGS=--server.port=8080
# 停止超时
STOP_TIMEOUT=30六、容器化部署方式
在信创环境中,除了 systemd,容器化部署也是主流方案。以下是将 Java 服务脚本集成到 Docker 容器中的方法。
6.1 容器化 Dockerfile
# ============================================================
# Java 服务容器化部署
# 使用 java-service.sh 管理容器内的 Java 进程
# ============================================================
FROM xinchuang.nexus.com/base/corretto8-jre:8u422-alpine3.20
# 安装脚本依赖
RUN apk add --no-cache bash procps
# 创建应用目录
ENV APP_HOME=/opt/app
WORKDIR ${APP_HOME}
# 复制管理脚本
COPY java-service.sh bin/java-service.sh
RUN chmod +x bin/java-service.sh
# 复制应用 jar
COPY target/*.jar lib/
# 复制配置文件
COPY src/main/resources/application.yml conf/
# 设置环境变量
ENV SERVICE_NAME=my-java-app \
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.security.egd=file:/dev/./urandom" \
SPRING_PROFILES_ACTIVE=prod
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 使用管理脚本的前台模式启动(容器内不使用 nohup)
ENTRYPOINT ["bin/java-service.sh", "start-foreground"]6.2 Docker Compose 编排
# docker-compose.yml
version: "3.8"
services:
my-java-app:
build: .
image: xinchuang.nexus.com/app/my-java-app:1.0.0
container_name: my-java-app
restart: unless-stopped
ports:
- "8080:8080"
environment:
- JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC
- SPRING_PROFILES_ACTIVE=prod
- SERVICE_NAME=my-java-app
volumes:
- ./conf:/opt/app/conf:ro
- ./logs:/opt/app/logs
deploy:
resources:
limits:
memory: 1536M # 容器内存限制(应大于 Xmx + 非堆内存)
cpus: "2.0"
reservations:
memory: 768M
cpus: "0.5"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"容器内存与 JVM 堆的匹配
容器内存限制应大于 JVM 上限堆(
-Xmx)加上非堆内存(Metaspace、Stack、Code Cache 等)。经验公式:Container Memory ≥ 1.5 × Xmx。例如 -Xmx1024m 时,容器内存限制应设为至少 1536M。6.3 K8s Deployment
# k8s-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-java-app
labels:
app: my-java-app
spec:
replicas: 3
selector:
matchLabels:
app: my-java-app
template:
metadata:
labels:
app: my-java-app
spec:
containers:
- name: my-java-app
image: xinchuang.nexus.com/app/my-java-app:1.0.0
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-Xms512m -Xmx1024m -XX:+UseG1GC"
- name: SPRING_PROFILES_ACTIVE
value: "prod"
resources:
requests:
memory: "768Mi"
cpu: "500m"
limits:
memory: "1536Mi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
volumeMounts:
- name: config
mountPath: /opt/app/conf
readOnly: true
volumes:
- name: config
configMap:
name: my-java-app-config七、生产环境注意事项
7.1 文件描述符限制
Java 应用(尤其是使用 Netty/NIO 的微服务)需要大量文件描述符:
# 查看当前限制
ulimit -n
# 临时修改
ulimit -n 65536
# 永久修改:/etc/security/limits.conf
cat >> /etc/security/limits.conf << 'EOF'
# Java 应用文件描述符限制
appuser soft nofile 65536
appuser hard nofile 65536
appuser soft nproc 4096
appuser hard nproc 4096
EOF7.2 JVM 参数调优
# 基础生产配置
JAVA_OPTS="\
-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/my-service/logs/heapdump.hprof \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/opt/my-service/logs/gc.log \
-Djava.security.egd=file:/dev/./urandom \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Shanghai"
# 信创环境特殊配置
JAVA_OPTS="${JAVA_OPTS} \
-XX:-UseCompressedOops \
-XX:ReservedCodeCacheSize=256m"信创 CPU 的 JVM 兼容性
部分国产 CPU(如早期申威 JDK)不支持压缩指针(CompressedOops),需要显式关闭:
-XX:-UseCompressedOops。龙芯 LoongArch64 从 JDK 17 开始已原生支持。升级 JDK 版本后记得验证此参数是否仍需保留。7.3 日志分离
# 方案 1:使用 systemd journald(推荐)
journalctl -u my-java-app -f # 实时查看
journalctl -u my-java-app --since today # 查看今天的日志
# 方案 2:使用 logrotate 轮转应用日志
cat > /etc/logrotate.d/my-java-app << 'EOF'
/opt/my-service/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
dateext
dateformat -%Y%m%d
}
EOF
# 手动测试 logrotate
logrotate -d /etc/logrotate.d/my-java-app八、目录结构规范
部署后的标准目录结构:
/opt/my-service/
├── bin/
│ └── java-service.sh # 服务管理脚本
├── conf/
│ ├── application.yml # Spring Boot 配置
│ ├── bootstrap.yml # Nacos 配置中心
│ └── my-java-app.env # systemd 环境变量
├── lib/
│ └── my-service-1.0.0.jar # 应用 jar 包
├── logs/
│ ├── my-java-app.log # 应用日志
│ ├── gc.log # GC 日志
│ └── heapdump.hprof # 堆转储(OOM 时生成)
├── my-java-app.pid # PID 文件
└── README.md # 部署说明九、总结
从手动到自动的演进
| 阶段 | 方式 | 可靠性 | 运维成本 |
|---|---|---|---|
| 青铜 | nohup java -jar | ⭐ | 高 |
| 白银 | 管理脚本 | ⭐⭐⭐ | 中 |
| 黄金 | 脚本 + systemd | ⭐⭐⭐⭐⭐ | 低 |
| 铂金 | 容器化 + K8s | ⭐⭐⭐⭐⭐ | 更低 |
脚本特性总结
- ✅ 自动检测 Java(三级探测:JAVA_HOME → 常见路径 → PATH)
- ✅ 自动识别 jar 文件
- ✅ 首次部署智能初始化
- ✅ 优雅停机(SIGTERM → 等待 → SIGKILL)
- ✅ PID 文件管理,防止重复启动
- ✅ systemd 服务一键安装/卸载
- ✅ 容器化部署支持(Dockerfile + Compose + K8s)
- ✅ 信创环境兼容(申威/龙芯/鲲鹏/x86_64)
快速上手
# 3 条命令完成部署
cp java-service.sh /opt/my-service/bin/
chmod +x /opt/my-service/bin/java-service.sh
/opt/my-service/bin/java-service.sh start
# 2 条命令完成 systemd 集成
sudo /opt/my-service/bin/java-service.sh install
sudo systemctl start my-java-app