尧图精选

Linux下jar包自动重启与开机自启:systemd服务化托管

🕒 发布时间:2026/10/1 23:20:12 📁 来源:尧图网络
1. 一个 jar 包服务的自动重启与开机自启真有那么难吗Linux 上跑 jar 包最原始的方式基本都差不多nohup java -jar xxx.jar 然后关终端走人。第一次这么干觉得挺方便但项目一旦上线半夜进程崩了没人拉起来服务器重启后还得手动登录执行一遍日志散落在nohup.out里越滚越大排查问题全靠猜。尤其是在企业内网、测试环境、边缘设备这类没有完整运维平台的地方jar 包自动重启和开机自启就是绕不过去的基本功。这篇内容围绕 Linux 中 jar 包实现自动重启、开机自启方案展开把 systemd、Supervisor、crontab 守护脚本、老系统 init 脚本几条路线都拆开讲清楚重点放在可复现的 systemd 方案上。适合刚接触 Linux 部署的 Java 开发者、需要维护单体 jar 服务的小团队也适合想从“手动 nohup”升级到“服务化托管”的运维新人。我见过太多项目把 jar 包放在/root目录下用 root 用户跑日志输出到nohup.out端口、环境变量、JVM 参数全写在命令历史里。这种部署方式在开发机上没问题放到需要长期运行的环境里稳定性完全靠运气。自动重启和开机自启看似只是两个功能实际牵涉到进程管理、用户权限、日志轮转、退出码语义、启动依赖顺序、资源限制等一整套细节。把它做扎实后面升级、排障、迁移都会轻松很多。1.1 手动 nohup 的三大痛点nohup解决的只是“终端关闭后进程不被挂断信号杀掉”这一个问题。它并不负责进程崩溃后重新拉起也不负责系统重启后自动启动。很多人以为加了就是后台守护其实那只是 shell 作业控制层面的后台和真正的服务管理是两码事。一旦 Java 进程因为 OOM、未捕获异常、依赖服务不可用而退出nohup不会做任何反应服务就彻底断了。第二个痛点是日志。nohup.out默认会不断追加没有切割没有级别分类也没有时间戳规范。磁盘写满之后可能引发更严重的系统问题。即使应用内部用了 Logback 或 Log4j2标准输出和标准错误仍然可能混进nohup.out排查时要在多个文件之间来回跳。第三个痛点是权限和路径。手动启动时用的是当前登录用户工作目录是执行命令时所在目录环境变量来自当前 shell。等到系统重启或换一个人登录同样的命令可能因为 Java 路径、配置文件路径、环境变量不同而启动失败。服务化托管的核心价值就是把这些“隐式依赖”变成“显式配置”。1.2 自动重启和开机自启到底要解决什么自动重启的目标不是“无脑拉起”而是区分正常停止和异常退出。手动执行systemctl stop时进程应该老老实实停掉不能又被拉起来进程崩溃、被kill -9、OOM 退出时才应该按策略重启。这就需要进程管理器能够识别退出原因并且支持重启间隔、重启次数限制避免服务因为配置错误无限重启把机器资源耗尽。开机自启的目标也不只是“开机后执行一条命令”而是要按照依赖顺序启动。比如 jar 包依赖 MySQL、Redis、Nginx至少要在网络就绪之后再启动否则应用可能启动失败。systemd 通过Afternetwork.target、Wantsnetwork-online.target这类单元依赖可以把启动顺序表达清楚。老系统里的 init 脚本也有运行级别和启动序号但灵活度差很多。另外自动重启和开机自启还必须可观测。服务现在是什么状态、最近有没有重启、为什么重启、日志在哪里这些信息必须能一条命令查出来。systemctl status和journalctl -u就是最常用的入口。自己写脚本守护时这些都要额外实现维护成本反而更高。1.3 几条常见路线先摆出来方案自动重启开机自启日志管理适合场景主要缺点systemd原生支持策略丰富原生支持journald 统一收集主流 Linux 发行版单元文件语法需要熟悉Supervisor支持 autorestart依赖自身开机启动自带 stdout 日志Python 生态、多进程需要额外安装守护进程crontab 脚本定时检查 PID可配合 reboot需自行处理临时、简单环境延迟大容易重复启动init.d 脚本需自己实现支持运行级别自行重定向老系统、无 systemd脚本复杂兼容性差Docker restart policy容器级重启容器服务自启取决于日志驱动容器化部署不是直接托管宿主机 jar如果你的系统是 CentOS 7/8、Ubuntu 16.04 之后的版本、Rocky Linux、AlmaLinux、Debian 新版本systemd 基本都是默认初始化系统直接用它最省事。Supervisor 更适合已经在用 Python 进程管理、或者需要管理大量非 Java 进程的场景。crontab 守护脚本可以作为兜底但不建议作为主方案。2. 方案选型为什么我首推 systemd 托管 jar 包systemd 经常被吐槽复杂但在“让一个 jar 包成为系统服务”这件事上它的表达能力和稳定性明显更好。你不需要额外安装守护进程不需要自己维护 PID 文件也不需要写复杂的 start/stop/restart 逻辑。一个 unit 文件写完systemctl enable --now myapp就能同时完成开机自启和立即启动。后面查状态、看日志、限制资源、设置环境变量都有标准入口。我自己的选型习惯是只要目标机器有 systemd并且 jar 包是长期运行的后端服务就优先用 systemd。Supervisor 放在第二梯队主要用于已经统一使用 Supervisor 的旧环境或者一台机器上要管理几十个异构进程的情况。crontab 更适合当作“监控补充”比如每分钟检查一次健康接口发现异常再调用 systemctl 重启而不是直接用它拉起 Java 进程。init.d 只在确实没有 systemd 的老系统上使用。2.1 systemd 在自动重启上的硬实力systemd 的Restart支持多种策略no、on-success、on-failure、on-abnormal、on-abort、on-watchdog、always。对于 jar 包服务最常用的是on-failure和always。on-failure只在进程非零退出、被信号杀死、超时等异常情况下重启always则不管怎么退出都重启但手动systemctl stop不会触发。这个语义非常关键很多人配了always后发现停止服务时进程又起来了其实是用了错误的方式停止或者 unit 类型配置有问题。RestartSec控制重启间隔默认 100ms生产环境建议改为 5 到 15 秒避免崩溃循环消耗资源。StartLimitIntervalSec和StartLimitBurst可以限制在指定时间窗口内最多重启几次超过后 systemd 会放弃并进入 failed 状态防止无限重启。这个组合比 crontab 每分钟盲目拉起要可靠得多。2.2 Supervisor 和 systemd 的对比Supervisor 的优势是配置文件直观、跨平台、对进程组管理友好。它的autorestarttrue、startretries、startsecs也很好理解。但 Supervisor 本身也是一个需要被托管的进程通常还得靠 systemd 或 init 脚本来保证它开机启动。也就是说在 Linux 上使用 Supervisor往往变成“用 systemd 管 Supervisor再用 Supervisor 管 jar 包”多了一层。对单个或少量 Java 服务来说这层并不划算。systemd 直接管 jar少一层故障点日志也直接进 journald。Supervisor 的日志默认写到文件需要自己配轮转。systemd 可以用journalctl按服务过滤、按时间过滤、按优先级过滤排查效率更高。当然Supervisor 在“一个配置文件管理多个进程”“进程分组启停”方面有它的便利如果团队已经标准化了 Supervisor继续用也没问题。2.3 目录、用户、日志规划先定好在写 unit 之前我建议先把目录和用户规划清楚。下面这套结构比较通用/opt/myapp/ # 应用主目录 /opt/myapp/app.jar # jar 包 /opt/myapp/config/ # 外部配置文件 /opt/myapp/scripts/ # 启停、健康检查脚本 /data/logs/myapp/ # 应用日志目录 /etc/sysconfig/myapp # 环境变量文件可选 /etc/systemd/system/myapp.service不要用 root 跑 Java 服务。创建一个专用系统用户比如appuser禁止登录只用于运行服务sudo useradd -r -s /sbin/nologin appuser sudo mkdir -p /opt/myapp /data/logs/myapp sudo chown -R appuser:appuser /opt/myapp /data/logs/myapp sudo chmod 750 /opt/myapp这样做的原因是权限最小化。如果 Java 服务被利用攻击者拿到的是低权限用户而不是 root。日志目录独立也方便后续挂载单独磁盘或做轮转。unit 文件里用Userappuser、Groupappuser指定运行身份工作目录用WorkingDirectory/opt/myapp固定下来避免相对路径问题。3. 手把手用 systemd 托管 jar 包并实现自动重启这一部分直接给一套可以抄作业的流程。假设 jar 包叫myapp.jar监听 8080 端口Spring Boot 项目生产环境配置文件外置在/opt/myapp/config/application-prod.yml。服务器是 Ubuntu 22.04 或 CentOS 7 以上已经安装 JDK 17。目标服务异常退出自动重启服务器重启后自动启动日志用 journald 和应用日志双通道。3.1 准备 Java 环境和运行目录先确认 Java 路径systemd 不会自动继承 shell 里的JAVA_HOME所以 unit 里最好写绝对路径或者显式配置EnvironmentJAVA_HOME...。which java java -version readlink -f $(which java)假设输出/usr/lib/jvm/java-17-openjdk-amd64/bin/java。接着创建用户和目录上传 jar 包sudo useradd -r -s /sbin/nologin appuser sudo mkdir -p /opt/myapp/config /opt/myapp/scripts /data/logs/myapp sudo cp myapp.jar /opt/myapp/app.jar sudo cp application-prod.yml /opt/myapp/config/ sudo chown -R appuser:appuser /opt/myapp /data/logs/myapp sudo chmod 640 /opt/myapp/app.jar /opt/myapp/config/application-prod.yml这里有个细节jar 包和配置文件权限用 640属主是 appuser其他用户不可读。如果 jar 里有敏感配置这一步能减少泄露面。目录权限 750保证只有属主和同组能进入。3.2 启动脚本还是直接 ExecStartsystemd 的ExecStart可以直接写 java 命令也可以调用一个 shell 脚本。两种方式我都用过。直接写ExecStart的好处是进程树干净systemd 管理的 PID 就是 Java 进程信号能直接送达。用脚本的好处是可以在启动前做检查、拼接复杂参数、准备目录。但脚本启动时要注意脚本里如果用nohup ... systemd 会认为主进程退出了导致服务状态异常。正确做法是脚本最后用exec java ...让 Java 进程替换 shell 进程。如果参数不复杂我建议直接写ExecStart。如果启动前需要拉取配置、检查数据库连接、设置动态参数再考虑用脚本。下面是一个启动脚本示例#!/bin/bash set -e APP_HOME/opt/myapp JAVA_BIN/usr/lib/jvm/java-17-openjdk-amd64/bin/java LOG_DIR/data/logs/myapp mkdir -p $LOG_DIR cd $APP_HOME exec $JAVA_BIN \ -Xms512m \ -Xmx1024m \ -XX:UseG1GC \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath$LOG_DIR \ -Dfile.encodingUTF-8 \ -Duser.timezoneAsia/Shanghai \ -jar $APP_HOME/app.jar \ --spring.config.location$APP_HOME/config/application-prod.yml \ --spring.profiles.activeprod注意exec不能丢否则 systemd 管理的是 bash 进程Java 只是子进程停止服务时信号可能只发给 bashJava 收不到优雅停机信号。脚本权限也要设置好sudo chmod 750 /opt/myapp/scripts/start.sh sudo chown appuser:appuser /opt/myapp/scripts/start.sh3.3 编写 systemd unit 文件在/etc/systemd/system/myapp.service写入以下内容。这是最核心的一步[Unit] DescriptionMyApp Java Service Documentationhttps://example.com/myapp Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userappuser Groupappuser WorkingDirectory/opt/myapp EnvironmentJAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64 EnvironmentSPRING_PROFILES_ACTIVEprod EnvironmentFile-/etc/sysconfig/myapp ExecStart/opt/myapp/scripts/start.sh SuccessExitStatus143 Restartalways RestartSec10 StartLimitIntervalSec60 StartLimitBurst5 TimeoutStartSec120 TimeoutStopSec30 KillSignalSIGTERM KillModecontrol-group StandardOutputjournal StandardErrorjournal SyslogIdentifiermyapp LimitNOFILE65535 [Install] WantedBymulti-user.targetAfternetwork-online.target和Wantsnetwork-online.target表示服务希望在网络就绪后启动。注意有些系统需要启用NetworkManager-wait-online.service或systemd-networkd-wait-online.service否则network-online.target可能不会真正等待网络。如果应用启动时强依赖数据库更稳妥的做法是在应用内部做重试或者用健康检查脚本延迟启动。EnvironmentFile-/etc/sysconfig/myapp前面的减号表示文件不存在也不报错。这个文件可以放敏感环境变量比如数据库密码权限设为 600属主 root由 systemd 读取后注入进程。这样比把密码写在 unit 里更安全。Restartalways表示只要不是手动 stop进程退出就重启。RestartSec10表示重启前等 10 秒。StartLimitIntervalSec60和StartLimitBurst5表示 60 秒内最多启动 5 次超过后进入 failed避免疯狂重启。SuccessExitStatus143是因为 Java 进程收到 SIGTERM 后正常退出码可能是 143systemd 把它视为成功避免误判为失败。KillModecontrol-group表示停止服务时向整个控制组发信号能清理子进程。3.4 JVM 参数和超时参数怎么定JVM 内存参数不能拍脑袋。以一台 2 核 4G 的云服务器为例操作系统、systemd、日志、监控大约占 500MB 到 800MBMySQL 如果同机部署还要占 1G 以上。如果只跑这个 Java 服务-Xmx可以给到 1536m 到 2048m但不要超过物理内存的 70%。更稳妥的是-Xms512m -Xmx1024m先观察 GC 和内存使用再逐步调整。-Xms和-Xmx设置成一样可以减少堆伸缩带来的抖动但启动时会立即占用内存小内存机器要权衡。元空间默认不限制但生产环境建议加-XX:MaxMetaspaceSize256m防止类加载器泄漏拖垮系统。G1 在大多数 Web 服务上表现稳定如果堆小于 4G也可以用-XX:UseParallelGC减少 CPU 开销。堆转储参数-XX:HeapDumpOnOutOfMemoryError很重要OOM 时能留下现场。转储目录必须可写否则参数无效。TimeoutStartSec120表示 systemd 等待服务启动完成的最长时间。Spring Boot 如果启动慢比如要初始化大量缓存可以调大。Typesimple下 systemd 认为启动命令执行后服务就算启动不会等端口监听。如果希望 systemd 等待端口可以改用Typenotify但需要应用支持 sd_notifySpring Boot 默认不集成。更实用的方式是健康检查脚本放在下一节讲。3.5 启用开机自启并启动服务写完 unit 后必须让 systemd 重新加载配置sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo systemctl status myappenable会在/etc/systemd/system/multi-user.target.wants/下创建符号链接实现开机自启。start是立即启动。可以用enable --now myapp一次完成。查看状态时重点看Active:、Main PID:、CGroup:和最近日志。如果状态是active (running)说明服务已经在跑。查看实时日志sudo journalctl -u myapp -f只看最近 100 行sudo journalctl -u myapp -n 100 --no-pager按时间过滤sudo journalctl -u myapp --since 2025-01-01 08:00:00 --until 2025-01-01 09:00:00systemd 会把标准输出和标准错误收集到 journaldSyslogIdentifiermyapp让日志更容易过滤。如果应用本身用 Logback 写文件到/data/logs/myapp两套日志可以互为补充。journald 适合看启动阶段和崩溃瞬间文件日志适合长期分析。3.6 验证自动重启是否真的生效配置完不要只看status正常就结束一定要做破坏性验证。先找到主进程 PIDsystemctl show -p MainPID myapp然后模拟异常崩溃sudo kill -9 MainPID等待 10 秒左右再次查看状态systemctl status myapp journalctl -u myapp -n 50 --no-pager如果看到服务重新变为active (running)并且日志里有重新启动的记录说明自动重启生效。还可以在 Java 代码里主动调用System.exit(1)测试异常退出码。注意System.exit(0)属于正常退出Restartalways会重启Restarton-failure不会重启。手动执行systemctl stop myapp时服务应该停止且不被拉起。如果 stop 之后又自动起来检查是否有其他定时任务或 Supervisor 在同时管理这个进程。4. 自动重启的进阶细节退出码、健康检查与优雅停机自动重启不是配一个Restartalways就万事大吉。生产环境里很多故障不是进程退出而是进程还在但接口已经不可用。比如线程池打满、数据库连接池耗尽、死锁导致请求全部超时。这种情况下 systemd 看到进程还活着不会重启。要解决这个问题需要健康检查配合。另一个常见问题是应用收到停止信号后没有优雅关闭导致正在处理的请求失败、数据写一半。systemd 的信号配置和应用的停机钩子要配合好。4.1 Restart 策略和退出码的对应关系Restart 值触发重启的条件适用场景no从不重启一次性任务、调试on-success退出码为 0 时重启特殊循环任务on-failure非零退出、信号杀死、超时、看门狗大多数常驻服务on-abnormal被信号杀死、超时、看门狗允许正常退出不重启always除手动 stop 外都重启希望始终存活的服务on-watchdog仅看门狗超时配合 WatchdogSec对于 jar 包服务我通常用Restartalways然后通过SuccessExitStatus143把 SIGTERM 正常退出视为成功。这样即使应用因为某些原因主动退出也会被拉起来保证可用性。如果服务启动成本很高频繁重启会拖垮系统就配合StartLimitBurst限制。4.2 StartLimit 防止无限重启StartLimitIntervalSec60和StartLimitBurst5的组合表示在 60 秒内如果服务启动超过 5 次systemd 会停止尝试服务进入failed状态。这个机制非常重要。假设配置文件写错Java 进程启动后立刻退出没有限制的话 systemd 会每 10 秒拉起一次日志瞬间刷爆CPU 也被浪费。有了限制系统会保留现场运维可以从容排查。查看是否触发启动限制systemctl status myapp journalctl -u myapp | grep -i start-limit如果触发了修复配置后执行sudo systemctl reset-failed myapp sudo systemctl start myappreset-failed会清除失败计数让服务可以重新启动。4.3 健康检查脚本与看门狗进程活着不代表服务健康。我常用的做法是写一个健康检查脚本用 systemd timer 定时执行。比如 Spring Boot Actuator 暴露/actuator/health脚本请求这个接口连续失败就调用systemctl restart myapp。#!/bin/bash URLhttp://127.0.0.1:8080/actuator/health STATE_FILE/tmp/myapp_health_fail_count MAX_FAIL3 if curl -fsS --max-time 5 $URL | grep -q status:UP; then echo 0 $STATE_FILE exit 0 fi count$(cat $STATE_FILE 2/dev/null || echo 0) count$((count 1)) echo $count $STATE_FILE if [ $count -ge $MAX_FAIL ]; then systemctl restart myapp echo 0 $STATE_FILE fi对应的 timer[Unit] DescriptionMyApp Health Check Timer [Timer] OnBootSec2min OnUnitActiveSec1min Unitmyapp-health.service [Install] WantedBytimers.target[Unit] DescriptionMyApp Health Check [Service] Typeoneshot ExecStart/opt/myapp/scripts/health-check.sh Userroot注意健康检查脚本重启服务需要权限通常用 root 运行 timer 服务。也可以给 appuser 配置 sudo 只允许systemctl restart myapp。另外健康检查不要过于频繁一分钟一次比较合理连续失败三次再重启避免网络抖动误判。4.4 优雅停机SIGTERM 和 KillModesystemd 停止服务时默认发送 SIGTERM等待TimeoutStopSec超时后发送 SIGKILL。Spring Boot 默认支持优雅停机但需要开启server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s开启后应用收到 SIGTERM 会停止接收新请求等待正在处理的请求完成再关闭线程池和数据库连接。systemd 的TimeoutStopSec30要大于应用的优雅停机时间否则还没处理完就被 SIGKILL。KillModecontrol-group确保子进程也被停止。如果应用启动了子进程比如调用外部脚本这个配置能避免子进程残留。4.5 日志管理journald 和文件轮转journald 默认会占用磁盘空间长期运行可能写满/var/log/journal。可以限制 journal 大小sudo journalctl --vacuum-size500M永久配置可以修改/etc/systemd/journald.confSystemMaxUse500M SystemMaxFileSize50M MaxRetentionSec2week应用文件日志用 Logback 按天切割保留 30 天。示例appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file/data/logs/myapp/myapp.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern/data/logs/myapp/myapp.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender如果日志目录单独挂盘记得在 unit 里加ReadWritePaths/data/logs/myapp配合ProtectSystemstrict等安全选项。这些加固项后面再讲。5. 备选方案Supervisor、crontab 和老系统 init 脚本systemd 是首选但实际环境五花八门。有些老服务器还在用 SysV init有些团队已经用 Supervisor 管了很多进程有些临时环境只需要一个简单的 crontab 兜底。把这些方案也整理出来方便你在不同场景下做选择。需要强调的是备选方案不是“更简单”只是“更适合当前约束”。如果系统支持 systemd我还是建议优先迁移到 systemd。5.1 Supervisor 自动重启配置Supervisor 安装方式因发行版而异Debian/Ubuntu 可以apt install supervisorCentOS 可以yum install supervisor也可以用 pip 安装。安装后主配置文件通常在/etc/supervisord.conf子配置放在/etc/supervisor/conf.d/。为 jar 包写一个配置[program:myapp] command/usr/lib/jvm/java-17-openjdk-amd64/bin/java -Xms512m -Xmx1024m -jar /opt/myapp/app.jar --spring.config.location/opt/myapp/config/application-prod.yml directory/opt/myapp userappuser autostarttrue autorestarttrue startsecs10 startretries5 stopsignalTERM stopwaitsecs30 redirect_stderrtrue stdout_logfile/data/logs/myapp/supervisor-stdout.log stdout_logfile_maxbytes50MB stdout_logfile_backups10 environmentJAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64,SPRING_PROFILES_ACTIVEprodautostarttrue表示 Supervisor 启动时自动启动该程序autorestarttrue表示进程退出后自动重启。startsecs10表示进程持续运行 10 秒才算启动成功避免启动即崩溃时误判。startretries5限制重试次数。修改配置后执行sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl status myapp sudo supervisorctl restart myappSupervisor 本身需要开机自启通常安装包会注册 systemd 服务或 init 脚本。检查systemctl is-enabled supervisor。Supervisor 的缺点是日志文件需要自己轮转进程管理多一层且对 systemd 的 cgroup 资源限制支持较弱。5.2 crontab 每分钟检查 PIDcrontab 方案适合临时环境或作为兜底。核心是写一个检查脚本判断进程是否存在不存在则启动。为了避免重复启动用flock加锁#!/bin/bash LOCK_FILE/tmp/myapp_cron.lock APP_HOME/opt/myapp JAVA_BIN/usr/lib/jvm/java-17-openjdk-amd64/bin/java PID_FILE/tmp/myapp.pid exec 9$LOCK_FILE flock -n 9 || exit 0 if [ -f $PID_FILE ] kill -0 $(cat $PID_FILE) 2/dev/null; then exit 0 fi cd $APP_HOME nohup $JAVA_BIN -Xms512m -Xmx1024m -jar $APP_HOME/app.jar \ --spring.config.location$APP_HOME/config/application-prod.yml \ /data/logs/myapp/stdout.log 21 echo $! $PID_FILEcrontab 添加* * * * * /opt/myapp/scripts/check.sh reboot /opt/myapp/scripts/check.sh这个方案有几个明显缺点故障发现延迟最多一分钟如果进程启动后很快崩溃脚本每分钟都会尝试拉起容易刷日志PID 文件可能因为进程被 kill 后残留需要kill -0校验。它不适合对可用性要求高的服务但作为“最后一道保险”还是可以的。5.3 init.d 脚本兼容老系统没有 systemd 的老系统比如 CentOS 6可以用 init.d 脚本。脚本需要支持 start、stop、restart、status并遵循 LSB 规范。核心片段#!/bin/bash # chkconfig: 2345 90 10 # description: MyApp Java Service APP_HOME/opt/myapp JAVA_BIN/usr/lib/jvm/java-17-openjdk-amd64/bin/java PID_FILE/var/run/myapp.pid LOG_FILE/data/logs/myapp/stdout.log start() { if [ -f $PID_FILE ] kill -0 $(cat $PID_FILE) 2/dev/null; then echo myapp already running return 0 fi cd $APP_HOME nohup $JAVA_BIN -Xms512m -Xmx1024m -jar $APP_HOME/app.jar \ --spring.config.location$APP_HOME/config/application-prod.yml \ $LOG_FILE 21 echo $! $PID_FILE } stop() { if [ -f $PID_FILE ]; then kill $(cat $PID_FILE) rm -f $PID_FILE fi } case $1 in start) start ;; stop) stop ;; restart) stop; sleep 2; start ;; status) [ -f $PID_FILE ] kill -0 $(cat $PID_FILE) echo running || echo stopped ;; *) echo Usage: $0 {start|stop|restart|status}; exit 1 ;; esac放到/etc/init.d/myapp设置可执行然后chkconfig --add myapp或update-rc.d myapp defaults注册开机自启。这个方案需要自己处理守护重启通常还要配合 crontab 或 monit。维护成本比 systemd 高非必要不推荐。5.4 容器场景的简单提法如果 jar 包已经容器化宿主机上通常不需要直接托管 Java 进程。Docker 的--restartalways或unless-stopped可以实现容器退出后重启配合 Docker 服务开机自启就能实现类似效果。Kubernetes 里用 Deployment 的restartPolicy: Always和 livenessProbe 做健康检查。但容器方案和本文的宿主机 systemd 方案不是替代关系很多时候是“宿主机 systemd 管 DockerDocker 管 jar”。理解 systemd 的进程管理逻辑对排查容器退出原因也有帮助。6. 常见问题与排查技巧实录自动重启和开机自启配好之后问题往往集中在启动失败、权限不足、环境变量丢失、日志爆盘、重启过于频繁这几类。下面这些现象和排查方法都是我在实际环境里反复遇到的。建议收藏成速查表出问题时按顺序排查能省很多时间。6.1 服务启动失败先看 status 和 journalctlsystemctl status myapp会显示退出码和简短日志。常见错误码错误码含义常见原因解决方式203/EXEC无法执行 ExecStart路径错误、没有执行权限检查绝对路径、chmod x200/CHDIR无法切换工作目录WorkingDirectory 不存在创建目录并授权217/USER用户不存在Userappuser 未创建useradd 或改 User226/NAMESPACE命名空间错误安全加固选项不兼容检查 ProtectSystem 等1/FAILURE应用自身失败配置错误、端口占用看应用日志和 journalctl143收到 SIGTERM 正常退出被停止或超时配 SuccessExitStatus143137被 SIGKILL 杀死OOM、超时强制杀查内存、调 TimeoutStopSec排查命令组合systemctl status myapp -l --no-pager journalctl -u myapp -n 200 --no-pager journalctl -u myapp -p err --no-pager-l显示完整状态--no-pager避免进入分页。先看 systemd 层面的错误再看 Java 应用日志。很多时候 systemd 只告诉你进程退出码真正原因在应用日志里。6.2 环境变量、工作目录和 jar 路径问题systemd 启动的服务不会加载/etc/profile、~/.bashrc所以手动在 shell 里能跑通的命令写成 unit 后可能报JAVA_HOME not found或配置文件找不到。解决办法有两个一是在 unit 里用Environment显式声明二是用EnvironmentFile引入环境变量文件。路径一律用绝对路径WorkingDirectory设成应用主目录。如果应用依赖当前目录下的相对路径比如config/application.ymlWorkingDirectory必须正确。可以在启动日志里打印user.dir确认System.out.println(System.getProperty(user.dir));另外systemd 的ProtectHometrue或ProtectSystemstrict可能让应用无法读取某些目录。如果启用了安全加固记得用ReadWritePaths、ReadOnlyPaths放行必要路径。6.3 开机自启没生效怎么查先确认是否 enablesystemctl is-enabled myapp输出enabled才是开机自启。如果输出disabled执行systemctl enable myapp。再看[Install]段是否有WantedBymulti-user.target。如果服务依赖网络检查Afternetwork-online.target是否生效以及系统是否真的等待网络systemctl status network-online.target有些云主机网络由 cloud-init 或 NetworkManager 管理network-online.target可能很快返回。可以在应用内部加重试或者用 timer 延迟启动。还有一种情况是服务启动了但很快失败开机自启本身执行了只是服务没撑住。这种要看journalctl -b -u myapp其中-b表示本次启动。6.4 自动重启过于频繁或启动两份进程自动重启过于频繁通常是因为应用启动即崩溃systemd 不断拉起。检查StartLimitBurst是否生效查看是否有类似start-limit hit的日志。修复配置后systemctl reset-failed myapp。启动两份进程通常是同时用了 systemd、Supervisor、crontab 多种方式管理同一个 jar。用ps -ef | grep java查看进程数量ss -lntp查看端口占用保留一种管理方式停掉其他。还有一种隐蔽情况unit 里Typeforking配错。如果应用本身不 fork却配了Typeforkingsystemd 会等待主进程退出可能误判启动失败或重复拉起。大多数直接运行的 Java 服务用Typesimple即可。6.5 日志乱码、时区和 OOM日志中文乱码通常是编码问题JVM 加-Dfile.encodingUTF-8应用日志组件也指定 UTF-8。时区不对加-Duser.timezoneAsia/Shanghai。OOM 退出码 137 表示进程被 SIGKILL可能是系统 OOM Killer 触发也可能是 systemd 的TimeoutStopSec超时后强杀。查看内核日志dmesg | grep -i oom journalctl -k | grep -i oom如果是 OOM需要调整-Xmx、增加内存、检查内存泄漏。可以用jmap、jstack、arthas分析堆和线程。堆转储文件配合 MAT 分析效果更好。6.6 端口占用和权限细节非 root 用户不能绑定 1024 以下端口。如果应用要监听 80 或 443建议用 Nginx 反向代理到 8080而不是给 Java 提权。检查端口占用ss -lntp | grep 8080 lsof -i :8080如果端口被旧进程占用先确认是不是同一服务残留。不要直接kill -9未知进程先看ps -fp PID。防火墙方面如果外部访问不了检查firewalld、ufw或云安全组。应用本机curl 127.0.0.1:8080通外部不通基本都是防火墙或安全组问题。7. 生产环境加固监控、升级与多实例管理服务能自动重启只是第一步长期运行还要考虑监控、升级、多实例和资源限制。systemd 提供了不少安全加固和资源控制选项配合外部监控可以把 jar 包服务管得比较稳。这里分享一些我实际用过的配置和习惯不一定适合所有场景但可以作为起点。7.1 监控与告警怎么做最基础的监控是进程存活和端口存活。可以用systemctl is-active myapp判断服务状态用curl检查健康接口。Prometheus 生态里可以用blackbox_exporter探测 HTTP 健康接口用node_exporter收集系统指标。如果不想引入复杂监控可以写一个简单的告警脚本在健康检查失败时发送邮件或企业微信机器人消息。systemd 本身没有内置告警但可以通过OnFailure配置一个失败单元[Unit] OnFailuremyapp-failure-notify%n.service然后写一个模板服务在服务进入 failed 时执行通知脚本。这个方式适合关键服务。通知脚本里可以获取失败服务名、时间、最近日志发给值班人员。注意不要在通知脚本里做太重的事情避免阻塞 systemd。7.2 升级发布如何减少中断直接systemctl stop myapp然后替换 jar 再start会有秒级到分钟级中断。对可用性要求高的服务可以用双实例加 Nginx upstream滚动停止和启动。也可以用软链接切换版本/opt/myapp/releases/20250101/app.jar /opt/myapp/releases/20250102/app.jar /opt/myapp/current - /opt/myapp/releases/20250102unit 里ExecStart指向/opt/myapp/current/app.jar。升级时上传新版本到 releases切换软链接然后systemctl restart myapp。这样回滚只需要把软链接切回旧版本再重启。注意软链接权限和属主确保 appuser 可读。如果应用支持优雅停机重启过程中 Nginx 可以把流量切到另一个实例用户几乎无感知。单实例场景下至少把重启安排在低峰期并提前确认健康检查通过。7.3 多 jar 包批量管理一台机器上跑多个 jar 包时不建议复制多份几乎一样的 unit 文件。可以用 systemd 模板单元。创建/etc/systemd/system/myapp.service[Unit] DescriptionMyApp Instance %i Afternetwork-online.target [Service] Typesimple Userappuser WorkingDirectory/opt/myapp/%i ExecStart/opt/myapp/%i/start.sh Restartalways RestartSec10 StartLimitIntervalSec60 StartLimitBurst5 [Install] WantedBymulti-user.target然后为每个实例准备目录和脚本启动systemctl enable --now myapporder systemctl enable --now myappuser每个实例的端口、配置、日志目录不同可以通过目录或环境变量区分。模板单元能显著减少重复配置但要注意实例之间的资源竞争尤其是内存和 CPU。7.4 我踩过的坑和几条硬经验第一条不要在ExecStart里用 shell 重定向。比如ExecStart/usr/bin/java -jar app.jar /data/logs/app.log 21是无效的systemd 不会通过 shell 解析。需要重定向就用StandardOutputfile:/data/logs/app.log或者写脚本并用exec。我更推荐StandardOutputjournal让 journald 统一收集。第二条修改 unit 文件后必须systemctl daemon-reload否则 systemd 仍然使用旧配置。很多人改了参数发现不生效就是漏了这一步。systemctl edit myapp可以创建覆盖片段避免直接修改原文件升级时更清晰。第三条Restartalways配合systemctl stop是安全的但如果用kill直接杀主进程systemd 会认为异常退出并重启。这是预期行为。要彻底停止服务用systemctl stop。如果确实要临时禁止重启可以systemctl mask myapp。第四条日志目录和堆转储目录要提前创建并授权。HeapDumpPath指向的目录如果不可写OOM 时不会生成转储关键现场就丢了。我一般把堆转储目录设成/data/logs/myapp/dump并设置独立轮转策略。第五条时区和编码问题在跨平台部署时很常见。容器里基础镜像可能没有时区数据宿主机 systemd 服务也可能使用 UTC。统一加-Duser.timezoneAsia/Shanghai和-Dfile.encodingUTF-8能避免很多“时间差八小时”“中文变问号”的问题。第六条JVM 参数不要直接照搬网上的“最优配置”。G1、ZGC、Parallel GC 各有适用场景。小内存机器上开 ZGC 可能适得其反。先压测再看 GC 日志最后定参数。开启 GC 日志-Xlog:gc*:file/data/logs/myapp/gc.log:time,uptime,level,tags:filecount10,filesize50M第七条健康检查不要只检查端口。端口监听不代表接口可用最好检查业务健康接口并且验证返回内容中的状态字段。检查频率和失败阈值要结合业务容忍度太敏感会导致频繁重启太迟钝会导致故障时间过长。最后再分享一个很实用的小习惯给每个 jar 服务在 unit 里加Description和Documentation写清楚服务用途和负责人。半年后回头看或者交接给同事时这两个字段能省下大量沟通成本。服务管理不仅是让进程跑起来更是让后来的人能看懂、能接手、能快速定位问题。systemd 这套方案我用了很多年从单体 jar 到多实例服务只要目录、用户、日志、重启策略这几件事规划清楚后面基本不会出大乱子。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →