← 返回

信创实战: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 服务管理脚本,满足以下要求:

  1. 自动检测 Java:JAVA_HOME → 常见安装路径 → PATH,三级探测
  2. 自动识别 jar:无需硬编码 jar 文件名
  3. 首次部署兼容:自动创建目录结构、移动配置文件
  4. systemd 集成:一键安装/卸载系统服务
  5. 受控停机:发送 SIGTERM 并等待,超时后再由策略决定是否发送 SIGKILL
  6. 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 start

4.2 日常运维

# 查看状态
./bin/java-service.sh status

# 查看日志(最后 200 行,持续跟踪)
./bin/java-service.sh log 200

# 重启服务
./bin/java-service.sh restart

# 停止服务
./bin/java-service.sh stop

4.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
EOF

7.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

参考资料