Linux systemd服务管理完全指南:从编写Unit文件到定时任务替代crontab(2026)

📝 484 字 · ☕ 2 分钟阅读

前言:为什么你需要学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。其他快捷方式:hourlyweeklymonthly
  • OnCalendar=*-*-* 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.serviceRequires=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值得学的就这五件事:

  1. 写Service文件([Unit] + [Service] + [Install])——管后台程序
  2. systemctl命令(start/stop/status/enable/journalctl)——日常操作
  3. 写Timer文件(OnCalendar + Persistent)——替代crontab
  4. 资源限制(MemoryMax / CPUQuota)——防止一个服务拖垮整台机器
  5. 安全加固(ProtectSystem / NoNewPrivileges)——就算程序有漏洞也炸不了

第一次照着模板写一个Unit文件,5分钟就能跑起来。慢慢你会发现:再也回不去nohup和screen了。

相关阅读:

📤 分享这篇文章