#!/usr/bin/env bash # ============================================================================== # s-file 一键部署/运维脚本(生产环境) # # 用法: # ./sfile-ctl.sh start 启动服务 # ./sfile-ctl.sh stop 停止服务 # ./sfile-ctl.sh restart 重启服务 # ./sfile-ctl.sh status 查看状态 # ./sfile-ctl.sh deploy 发布新版 jar(自动备份旧版并重启) # ./sfile-ctl.sh log [行数] 查看启动日志(默认 200 行) # ./sfile-ctl.sh db-upgrade 在达梦库执行升级 SQL(需配置 disql) # 上线用: db/upgrade-20260915-release.sql # # 部署目录结构(脚本首次 start 时自动创建,配置需手工放置): # /opt/sfile/ # ├── s-file.jar 服务 jar # ├── config/application.yml 生产配置(模板见 docs/生产部署说明.md) # ├── backup/ 历史版本备份 # ├── logs/ 日志目录 # └── data/ 上传/摆渡目录(upload/transfer/send/transfer/receive) # ============================================================================== set -euo pipefail # ------------------------- 环境变量(按服务器实际修改) ------------------------- APP_NAME="s-file" INSTALL_DIR="${SFILE_INSTALL_DIR:-/opt/sfile}" JAR_FILE="${INSTALL_DIR}/${APP_NAME}.jar" CONFIG_FILE="${INSTALL_DIR}/config/application.yml" LOG_DIR="${INSTALL_DIR}/logs" BACKUP_DIR="${INSTALL_DIR}/backup" STDOUT_LOG="${LOG_DIR}/stdout.log" GC_LOG="${LOG_DIR}/gc.log" # 运行用户(root 部署时可改为专用低权用户,如 sfile) RUN_USER="${SFILE_RUN_USER:-$(whoami)}" # JVM 参数:按机器内存调整 -Xmx;生产建议 2g 起 JAVA_OPTS="${SFILE_JAVA_OPTS:--Xms1g -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -Xloggc:${GC_LOG} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M}" # 健康检查地址与超时 HEALTH_URL="http://127.0.0.1:8080/user/info" START_TIMEOUT=120 # 达梦 disql(db-upgrade 用;达梦容器则为 docker exec -i <容器> /opt/dmdbms/bin/disql ...) DISQL_CMD="${SFILE_DISQL:-disql /@:5236}" # ----------------------------------------------------------------------------- GREEN='\033[32m'; RED='\033[31m'; YELLOW='\033[33m'; NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } get_pid() { pgrep -f "java .*${JAR_FILE}" 2>/dev/null || true } require_env() { [ -f "${JAR_FILE}" ] || { err "jar 不存在: ${JAR_FILE},先用 deploy 发布"; exit 1; } [ -f "${CONFIG_FILE}" ] || { err "配置不存在: ${CONFIG_FILE}(模板见 docs/生产部署说明.md)"; exit 1; } # 项目要求 JDK 8 运行(字节码 52.0),防护误用高版本 JDK if command -v java >/dev/null 2>&1; then local major major=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+)\..*"/\1/') if [ "${major}" != "1" ] && [ "${major}" != "8" ]; then err "检测到 java 版本为 ${major},本项目要求 JDK 8(请 export JAVA_HOME 指向 JDK8 并确认 java 命令)" exit 1 fi else err "未找到 java 命令,请安装 JDK 8 并配置 PATH"; exit 1 fi } prepare_dirs() { mkdir -p "${INSTALL_DIR}/config" "${LOG_DIR}" "${BACKUP_DIR}" \ "${INSTALL_DIR}/data/upload" \ "${INSTALL_DIR}/data/transfer/send" \ "${INSTALL_DIR}/data/transfer/receive" } start() { require_env local pid pid=$(get_pid) if [ -n "${pid}" ]; then warn "${APP_NAME} 已在运行 (PID ${pid}),如需重启请先 stop" exit 0 fi prepare_dirs info "启动 ${APP_NAME} ..." # workerId 说明:多节点部署时每台机器配置不同的 code.snowflake.workid.<本机IP>, # 最简单的方式是用环境变量(. 映射为 _),如: # export CODE_SNOWFLAKE_WORKID_192_168_1_10=1 # shellcheck disable=SC2086 if [ "${RUN_USER}" = "$(whoami)" ]; then (cd "${INSTALL_DIR}" && nohup java ${JAVA_OPTS} \ -Dspring.config.additional-location=file:${CONFIG_FILE} \ -jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo $! > "${INSTALL_DIR}/app.pid") else su -s /bin/bash "${RUN_USER}" -c "cd ${INSTALL_DIR} && nohup java ${JAVA_OPTS} \ -Dspring.config.additional-location=file:${CONFIG_FILE} \ -jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo \$! > ${INSTALL_DIR}/app.pid" fi # 启动健康检查轮询 info "等待服务就绪(最多 ${START_TIMEOUT}s)..." local waited=0 while [ ${waited} -lt ${START_TIMEOUT} ]; do if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then info "启动成功 (PID $(cat "${INSTALL_DIR}/app.pid")),耗时约 ${waited}s" info "日志: tail -f ${STDOUT_LOG}" return 0 fi # 进程若已退出则立即报错,不干等 if [ -z "$(get_pid)" ]; then err "进程已退出,最近日志:" tail -30 "${STDOUT_LOG}" exit 1 fi sleep 3; waited=$((waited + 3)) done err "${START_TIMEOUT}s 内未就绪,请检查日志: tail -100 ${STDOUT_LOG}" exit 1 } stop() { local pid pid=$(get_pid) if [ -z "${pid}" ]; then info "${APP_NAME} 未在运行" return 0 fi info "停止 ${APP_NAME} (PID ${pid}) ..." kill "${pid}" local waited=0 while kill -0 "${pid}" 2>/dev/null && [ ${waited} -lt 60 ]; do sleep 2; waited=$((waited + 2)) done if kill -0 "${pid}" 2>/dev/null; then warn "60s 未退出,强制 kill -9" kill -9 "${pid}" 2>/dev/null || true fi rm -f "${INSTALL_DIR}/app.pid" info "已停止" } status() { local pid pid=$(get_pid) if [ -z "${pid}" ]; then info "${APP_NAME}: 未运行" exit 1 fi info "${APP_NAME}: 运行中 (PID ${pid})" if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then info "健康检查: 正常" else warn "健康检查: 8080 未响应(可能仍在启动)" fi info "内存: $(ps -o rss= -p "${pid}" | awk '{printf "%.0fMB", $1/1024}')" } deploy() { local new_jar="$1" [ -f "${new_jar}" ] || { err "找不到 jar: ${new_jar}"; exit 1; } prepare_dirs local ts; ts=$(date +%Y%m%d-%H%M%S) if [ -f "${JAR_FILE}" ]; then # 运行中先停 if [ -n "$(get_pid)" ]; then stop fi cp "${JAR_FILE}" "${BACKUP_DIR}/${APP_NAME}-${ts}.jar" info "旧版本已备份: ${BACKUP_DIR}/${APP_NAME}-${ts}.jar" fi cp "${new_jar}" "${JAR_FILE}" # 保留最近 5 个备份 ls -1t "${BACKUP_DIR}/${APP_NAME}-"*.jar 2>/dev/null | tail -n +6 | xargs -r rm -f info "新版本已就位: ${JAR_FILE} ($(du -h "${JAR_FILE}" | cut -f1))" start } db_upgrade() { local sql_file="$1" [ -f "${sql_file}" ] || { err "找不到 SQL: ${sql_file}"; exit 1; } case "${DISQL_CMD}" in *""*) err "请先在脚本头部或环境变量 SFILE_DISQL 配置真实 disql 连接串"; exit 1;; esac info "执行升级 SQL: ${sql_file}" # 达梦 disql 执行 SQL 文件语法: disql 用户/密码@host:port \`文件路径\` ${DISQL_CMD} \`"${sql_file}" info "执行完成,请核对上方输出无 ERROR 后再继续部署" } log() { local lines="${1:-200}" tail -n "${lines}" "${STDOUT_LOG}" 2>/dev/null || warn "暂无日志: ${STDOUT_LOG}" } case "${1:-}" in start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; deploy) [ $# -ge 2 ] || { err "用法: $0 deploy "; exit 1; }; deploy "$2" ;; log) log "${2:-200}" ;; db-upgrade) [ $# -ge 2 ] || { err "用法: $0 db-upgrade "; exit 1; }; db_upgrade "$2" ;; *) sed -n '2,20p' "$0"; exit 1 ;; esac