前言:为什么你需要学systemd?
如果你在Linux上跑过任何服务——Nginx、MySQL、你写的Python脚本、一个定时任务——那你一定遇到过这些场景:
- 服务器重启了,你的程序没自动起来,用户开始骂人
- 想用crontab做定时任务,但日志乱七八糟,不知道跑成功没
- 程序挂了,你不知道,直到有人告诉你「网站打不开了」
- 想限制一个服务的内存/CPU,完全不知道怎么搞
这些问题,systemd都能解决。它是现代Linux系统的「大管家」——管启动、管监控、管日志、管资源。本教程从零开始,带你从写第一个Unit文件到用它替代crontab。
适用环境:Ubuntu 24.04 / 22.04,CentOS Stream 9,Debian 12。systemd版本 >= 249。
第一步:理解systemd的三个核心概念
别被systemd的复杂性吓到。日常使用中,你只需要搞懂三个东西:
| 概念 | 是什么 | 类比 |
|---|---|---|
| Unit | 一个「工作单元」的配置文件 | 一个任务说明书 |
| Service | 最常见的Unit类型,定义如何启动/停止一个后台程序 | 员工(干活的人) |
| Timer | 定时触发Service的Unit | 闹钟 |
Unit文件放在哪?两个地方:
/etc/systemd/system/— 你自己写的,优先级最高/lib/systemd/system/— 系统自带的,别改
第二步:写你的第一个Service Unit
假设你有一个Python脚本 /opt/myapp/main.py,需要开机自启、挂了自动重启。以前你可能用nohup、screen、tmux……现在用systemd:
创建Unit文件
sudo vim /etc/systemd/system/myapp.service
写入以下内容:
[Unit]
Description=My Python Application
After=network.target
Documentation=https://docs.myapp.com
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/main.py
Restart=always
RestartSec=10
StandardOutput=append:/var/log/myapp/output.log
StandardError=append:/var/log/myapp/error.log
[Install]
WantedBy=multi-user.target
逐行解释(每一行都有用)
[Unit] 段 — 描述和依赖:
Description=:人类可读的名字,用systemctl status myapp时会显示After=network.target:确保在网络启动后才启动你的服务——不然你的程序连不上数据库Documentation=:可选,给你的同事(和三个月后的自己)看的
[Service] 段 — 核心:怎么启动:
Type=simple:最常见的类型,systemd认为ExecStart启动后服务就运行了。大多数Python/Node/Go程序用这个User=www-data:永远不要用root跑你的应用!WorkingDirectory=:程序的工作目录,影响相对路径ExecStart=:启动命令,必须写绝对路径Restart=always:挂了自动重启(包括正常退出和崩溃)RestartSec=10:挂了等10秒再重启,防止「启动即崩溃」的无限循环StandardOutput/Error=append::日志输出到文件(不设的话输出到journald)
[Install] 段 — 什么时候启动:
WantedBy=multi-user.target:系统进入多用户模式(正常启动完成)时启动。绝大多数服务都用这个
启动你的服务
# 1. 重载systemd配置(每次改了Unit文件都要执行)
sudo systemctl daemon-reload
# 2. 启动服务
sudo systemctl start myapp
# 3. 查看状态
sudo systemctl status myapp
# 4. 设置开机自启
sudo systemctl enable myapp
# 5. 验证开机自启
sudo systemctl is-enabled myapp
# 输出:enabled ✅
如果一切正常,systemctl status 会显示绿色的 active (running),以及最后10行日志。
第三步:日常管理命令速查
# 启动/停止/重启
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
# 重载配置(不重启进程)
sudo systemctl reload myapp # 需要服务支持(如Nginx)
# 查看状态
sudo systemctl status myapp
sudo systemctl is-active myapp # 只返回active/inactive
sudo systemctl is-enabled myapp # 查是否开机自启
# 查看日志
sudo journalctl -u myapp # 全部日志
sudo journalctl -u myapp -f # 实时跟踪(类似tail -f)
sudo journalctl -u myapp --since "1 hour ago"
sudo journalctl -u myapp -n 50 # 最后50行
# 禁用/启用
sudo systemctl disable myapp # 取消开机自启
sudo systemctl enable myapp # 恢复开机自启
sudo systemctl mask myapp # 彻底禁止启动(连手动也不行)
sudo systemctl unmask myapp # 解除mask
第四步:用systemd Timer替代crontab
crontab有几个痛点:日志难查、环境变量一团乱、不知道上次任务跑完没就启动了第二次。systemd Timer完美解决。
Timer需要两个文件:一个Service(定义跑什么),一个Timer(定义什么时候跑)。
场景:每天凌晨3点备份数据库
先写Service:/etc/systemd/system/db-backup.service
[Unit]
Description=Database Backup Job
After=mysql.service
[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal
StandardError=journal
Type=oneshot 表示这是一次性任务(跑完就退出),不是常驻服务。
再写Timer:/etc/systemd/system/db-backup.timer
[Unit]
Description=Daily Database Backup Timer
[Timer]
OnCalendar=daily
# 等价于 OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Timer关键选项解释
OnCalendar=daily:每天一次。等价于*-*-* 00:00:00。其他快捷方式:hourly、weekly、monthlyOnCalendar=*-*-* 03:00:00:精确到秒。格式:年-月-日 时:分:秒。* 表示任意Persistent=true:必加!如果机器在预定时间处于关机状态,开机后立即补跑。不加的话就跳过了RandomizedDelaySec=300:随机延迟0-300秒再执行。防止多台机器同时做同一件事把数据库打爆
更灵活的定时表达式
# 每周一凌晨2点
OnCalendar=Mon *-*-* 02:00:00
# 每月1号和15号上午9点
OnCalendar=*-*-01,15 09:00:00
# 每30分钟
OnCalendar=*:0/30
# 工作日(周一到周五)每小时
OnCalendar=Mon..Fri *-*-* *:00:00
启动和管理Timer
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer # 启用+立即启动
# 查看所有Timer
sudo systemctl list-timers --all
# 查看某个Timer的下次触发时间
sudo systemctl status db-backup.timer
# 手动触发(不等定时)
sudo systemctl start db-backup.service
第五步:进阶技巧——资源限制与安全加固
限制内存和CPU
在你的Service文件 [Service] 段加上:
[Service]
# ... 其他配置 ...
# 内存限制
MemoryMax=512M # 硬限制,超过就杀进程
MemoryHigh=400M # 软限制,超过就开始throttle
# CPU限制
CPUQuota=50% # 最多用半个CPU核心
# CPUQuota=200% # 最多用2个CPU核心
# 进程数限制
TasksMax=100 # 最多开100个线程/进程
安全加固——不让你的服务为所欲为
[Service]
# 禁止服务修改自身的可执行文件
ProtectSystem=strict
# 只读访问/etc,不允许修改系统配置
ProtectSystem=full
ReadWritePaths=/var/log/myapp /var/lib/myapp
# 隔离/home、/root、/run/user
ProtectHome=yes
# 禁止访问网络(适用于纯计算/本地任务)
PrivateNetwork=yes
# 独立的/tmp,服务看不到其他进程的临时文件
PrivateTmp=yes
# 禁止获取新的特权(就算程序有漏洞也无法提权)
NoNewPrivileges=yes
常见问题
Q: systemctl status显示active但程序没跑起来?
最常见的原因:你的程序启动了但马上就退出了(比如Python脚本有语法错误)。systemd看到进程退出,按Restart=always重启……然后你的程序又立刻退出。这会形成一个快速循环。查看日志:sudo journalctl -u your-service -n 50,找报错信息。另外检查Type设置——如果你的程序会fork到后台(daemonize),Type应该用forking而不是simple。
Q: systemd和crontab到底选哪个?
新项目一律推荐systemd Timer。crontab在以下场景依然可以:超级简单的命令(如每天echo一下)、个人临时任务。但只要是生产环境的定时任务——systemd Timer。日志统一(journalctl)、环境变量明确、依赖管理(After=网络/数据库)、失败重试——这些crontab都要额外写脚本处理,而systemd原生支持。
Q: 怎么让systemd在服务启动前先等数据库就绪?
在[Unit]段加:After=mysql.service 和 Requires=mysql.service。After=确保顺序,Requires=确保mysql先启动成功(否则本服务不启动)。如果数据库和你的应用不在同一台机器,可以考虑用ExecStartPre=/usr/bin/sleep 5(简单粗暴),或更优雅地写一个健康检查脚本放在ExecStartPre。
Q: 修改了Unit文件之后必须daemon-reload吗?
是的。任何对Unit文件的修改(包括新建、删除、编辑),都要执行sudo systemctl daemon-reload,否则systemd会用缓存的旧配置。唯一例外是systemctl edit service-name --full这样用systemctl自带编辑器修改的——它会自动reload。
总结
systemd值得学的就这五件事:
- 写Service文件([Unit] + [Service] + [Install])——管后台程序
- systemctl命令(start/stop/status/enable/journalctl)——日常操作
- 写Timer文件(OnCalendar + Persistent)——替代crontab
- 资源限制(MemoryMax / CPUQuota)——防止一个服务拖垮整台机器
- 安全加固(ProtectSystem / NoNewPrivileges)——就算程序有漏洞也炸不了
第一次照着模板写一个Unit文件,5分钟就能跑起来。慢慢你会发现:再也回不去nohup和screen了。
相关阅读: