systemd 服务管理实战:Unit 文件编写与 journalctl 日志排查

为什么现在每个 Linux 发行版都在用 systemd

十年前装个软件要开机自启,得写一堆 /etc/init.d/ 脚本,启动顺序靠数字编号控制,服务崩了也不会自动拉起,查日志要在 /var/log/ 里翻各种命名随意的文件。systemd 把这一切统一了:并行启动缩短开机时间,Unit 文件声明服务之间的依赖关系,Restart 策略自动拉起崩溃的服务,journald 把内核和所有服务的日志集中管理。Debian 8、Ubuntu 15.04、CentOS 7 之后的主流发行版默认都是 systemd,写网站服务的个人站长绕不开它。这篇文章从 Unit 文件讲起,手把手把一个 Python 网站服务管起来,再讲 journalctl 日志排查的常用姿势。

一、systemctl 常用命令速查

日常维护离不开下面这些命令,先混个脸熟:

systemctl start myapp       # 启动服务
systemctl stop myapp        # 停止服务
systemctl restart myapp     # 重启服务
systemctl reload myapp      # 重载配置(服务支持才有效,如 Nginx)
systemctl enable myapp      # 设置开机自启
systemctl disable myapp     # 取消开机自启
systemctl status myapp      # 查看运行状态和最近日志
systemctl is-enabled myapp  # 查看是否开机自启
systemctl daemon-reload     # 修改 Unit 文件后必须执行,重新加载定义
systemctl list-units --type=service    # 列出运行中的服务
systemctl list-unit-files --type=service | grep enabled   # 查看哪些服务开机自启

注意一个高频错误:改完 /etc/systemd/system/ 下的 Unit 文件后忘了 systemctl daemon-reload,直接 restart 会发现改动没生效,因为 systemd 还在用旧的配置。想看服务的依赖关系用 systemctl list-dependencies myapp,会递归列出它依赖的所有 Unit,排查启动顺序和开机卡住的问题很有用。

二、Unit 文件的结构

一个典型的服务 Unit 文件放在 /etc/systemd/system/myapp.service,由三节组成。以常见的 Python Flask 网站为例:

[Unit]

Description=My Flask Web App
After=network.target mysql.service
Wants=mysql.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/python3 /var/www/myapp/app.py
Restart=on-failure
RestartSec=5
Environment=FLASK_ENV=production
EnvironmentFile=/etc/myapp.env
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

[Unit] 段描述服务本身:After 声明启动顺序,等 network.target 和 mysql 起来之后再启动本服务;Wants 声明软依赖,mysql 挂了不影响本服务启动,想强制依赖就用 Requires。[Service] 段是核心:Type=simple 表示 ExecStart 启动的进程就是服务主进程(最常用);User/Group 指定以什么身份运行,永远不要用 root 跑应用服务;WorkingDirectory 是工作目录;ExecStart 是启动命令,必须写绝对路径,路径错了服务起不来;Restart=on-failure 让服务异常退出时自动拉起,RestartSec 是重试间隔;Environment 和 EnvironmentFile 注入环境变量,注意 systemd 服务不会读取 .bashrc,环境变量必须在这里声明;TimeoutStartSec 控制启动超时,默认 90 秒,应用启动慢可以调大;KillMode=control-group 是默认值,停止服务时会把整个进程组的子进程一起杀掉,避免留下孤儿进程。[Install] 段声明服务安装到哪个 target,WantedBy=multi-user.target 就是系统进入多用户模式(正常开机)时启动,配合 systemctl enable 生成自启软链接。

三、把服务跑起来

写好 Unit 文件之后执行:

systemctl daemon-reload
systemctl enable --now myapp # enable 加 --now 同时启动
systemctl status myapp

status 输出里 Active: active (running) 表示正常运行,下面几行就是最近的日志。修改了代码后重启:systemctl restart myapp。想让 Nginx 平滑重载配置而不是中断连接,用 systemctl reload nginx。

四、Type 类型怎么选

Type=simple 是最常用的,ExecStart 的进程直接作为服务进程,Python、Node、Go 应用基本都是它。Type=forking 用于启动后把自己 fork 到后台的程序,比如老版本的 Nginx、Redis,systemd 需要配合 PIDFile 指定 pid 文件路径才能判断服务是否启动成功,这类程序现在官方配置一般都会写好。Type=oneshot 用于执行一次性任务,比如初始化脚本,配 RemainAfterExit=yes 可以让服务执行完仍显示 active。Type=notify 需要程序自己调用 sd_notify 通知 systemd 启动完成,Gunicorn 等成熟服务框架支持,能实现更精确的就绪检测。还有个 Type=exec,是 simple 的严格版本,systemd 会先确认 ExecStart 的程序成功启动(exec 成功)才认为服务进入激活状态,能更早暴露路径错误这类问题。

五、journalctl 日志排查实战

服务日志不用再翻文件,journalctl 一条命令搞定:

journalctl -u myapp -f          # 实时跟踪某服务的日志
journalctl -u myapp -n 100 # 查看最近 100 行
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err # 只看 error 级别以上的日志
journalctl -u myapp -o json-pretty # 结构化输出,方便脚本处理

服务崩溃后第一件事就是 journalctl -u 服务名 -n 50 看最后的报错。默认 journald 的日志只存在内存里,重启就没了,想持久化修改 /etc/systemd/journald.conf:

[Journal]
Storage=persistent
SystemMaxUse=500M

改完重启 journald:systemctl restart systemd-journald。Storage=persistent 让日志写入 /var/log/journal,SystemMaxUse 限制日志最多占 500M 磁盘,防止日志把系统盘写满——这是个人服务器最常见的磁盘告警原因之一。日志已经写了很多想清理,用 journalctl --disk-usage 查看占用,journalctl --vacuum-size=200M 一键压缩清理到目标大小。另外两个实用参数:-k 只看内核日志,排查驱动和网络问题用;--list-boots 列出历次开机记录,配合 -b -1 查看上一次开机的日志,服务器半夜重启过、白天想查原因,这就是最快的入口:

journalctl --list-boots
journalctl -b -1 -n 50 # 上次开机的最后 50 行日志

六、常见故障排查套路

服务起不来先 systemctl status 看状态,再 journalctl -xe 看详细日志。几个高频问题:第一,报 Failed at step EXEC spawning 或者退出码 203,说明 ExecStart 里的可执行文件路径不存在或者没有执行权限,用 which python3 确认路径,检查文件是否有 x 权限;第二,报 Address already in use,端口被占用,用 ss -tlnp | grep 端口号 找到占用进程,改端口或者停掉旧进程;第三,服务启动即退出且反复重启,Restart=on-failure 会陷入循环,先 systemctl stop 停掉,再看日志里真正的报错原因,比如数据库连不上、配置文件解析失败;第四,Permission denied,多半是 User 指定的用户对 WorkingDirectory 或日志文件没有写权限,检查目录属主;第五,改完配置不生效,先 daemon-reload 再 restart。另外排查时记住 journalctl -u myapp 只显示服务自己的日志,程序里 print 到标准输出的内容也会被 journald 捕获,不用自己写日志文件。开机慢也可以用 systemd-analyze blame 按耗时排序查看每个服务的启动时间,一眼看出是哪个服务拖慢了开机。

七、用 cgroup 限制服务资源

个人服务器上最常见的故障是某个程序内存泄漏,把整台机器的内存吃光,MySQL、Nginx 跟着一起遭殃,最后只能强制重启。systemd 的每个服务都跑在独立的 cgroup 里,可以直接限制资源:

systemctl set-property myapp MemoryMax=512M
systemctl set-property myapp CPUQuota=50%
systemctl set-property myapp TasksMax=512

MemoryMax 限制服务最多占用 512M 内存,超了直接 OOM 杀掉这个服务而不是拖垮整个系统;CPUQuota=50% 限制最多用半个 CPU 核心,适合备份、转码这类吃 CPU 的批处理服务;TasksMax 限制进程和线程总数,防止程序疯狂 fork 把系统拖死。这些设置会写入 /etc/systemd/system/myapp.service.d/ 下的 override.conf,属于持久化配置,重启不丢。想临时改一下直接 set-property 就行,不用动 Unit 文件。如果不想直接杀进程,可以用 MemoryHigh 代替 MemoryMax:MemoryHigh 是软限制,超过后系统会持续回收这个服务的缓存和内存,尽量不杀进程,适合内存占用有波动但不想中断服务的场景。查看实际限制和用量:

systemctl show myapp -p MemoryMax -p MemoryCurrent
systemd-cgtop # 类似 top,按 cgroup 查看资源占用

给所有服务都加上内存上限是个人服务器性价比很高的防呆措施,即使某个服务出了 bug 也不会影响其他服务。

八、用 systemd timer 替代 crontab

定时任务除了 crontab,systemd timer 是更现代化的选择,好处是日志统一进 journald、可以看上次运行时间、错过执行时间会自动补跑。一个每天凌晨备份的 timer 例子:

# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

/etc/systemd/system/backup.timer

[Unit]
Description=Run backup daily

[Timer]
OnCalendar=--* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

systemctl enable --now backup.timer
systemctl list-timers # 查看所有定时器及下次执行时间

Persistent=true 的意思是如果到点服务器没开机,开机后立即补跑一次,这个特性对备份类任务特别实用,crontab 做不到。OnCalendar 的语法比 crontab 更灵活,既支持 daily、hourly、Mon..Fri 这种英文描述,也支持完整的日历表达式,而且 timer 可以精确到秒,这是 crontab 很难写出来的。查看上次运行结果同样是 journalctl -u backup.service,和查普通服务一样。

九、总结

systemd 的核心就三件事:Unit 文件声明怎么跑、systemctl 管理生命周期、journalctl 看日志。个人站长把这套流程走顺,部署网站服务从"手工 nohup 后台进程、崩了没人管"变成"开机自启、崩溃自动拉起、日志一条命令可查",省下来的运维时间非常可观。遇到问题记住排查顺序:systemctl status 看状态,journalctl -xe 看报错,改配置先 daemon-reload。

Last modification:September 2nd, 2026 at 08:00 am

Leave a Comment