华为云 ECS 运维:用 systemd 托管常驻任务的五个细节
为什么不再用 nohup
很多脚本上线时都是这样启动的:nohup python worker.py > worker.log 2>&1 &。白天没问题,夜里机器重启、OOM 或磁盘写满之后,进程就悄悄消失了。日志只有一个不断增长的 worker.log,没有轮转;想看退出码,只能靠猜。nohup 解决的是“终端断开后进程别死”,但常驻任务还需要开机自启、异常重启、资源限制、日志轮转和优雅退出,这些都应该交给 init 系统。
一个最小可用的 unit
在华为云 ECS 上,以 root 运行的服务放到 /etc/systemd/system/worker.service:
[Unit]
Description=Order callback worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
WorkingDirectory=/opt/app
Environment="PYTHONUNBUFFERED=1"
ExecStart=/usr/bin/python3 /opt/app/worker.py
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillSignal=SIGTERM
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
几个参数值得解释。Type=simple 表示 ExecStart 启动的就是主进程,不要再用 shell 包一层,否则 systemd 跟踪到的是 shell,脚本挂了它也感知不到。Restart=on-failure 只对非零退出、信号杀死和超时生效,正常退出不会拉起;如果希望无论怎么退出都重启,用 always,但要配合 StartLimitIntervalSec 防止崩溃循环。TimeoutStopSec 给进程 30 秒处理 SIGTERM,超时后 systemd 才发 SIGKILL。
让进程真正接住 SIGTERM
Restart 能不能做到“不丢正在处理的任务”,取决于代码有没有处理优雅退出。Python 里可以这样写:
import signal, threading
stop = threading.Event()
def handle(signum, frame):
stop.set()
signal.signal(signal.SIGTERM, handle)
signal.signal(signal.SIGINT, handle)
while not stop.is_set():
msg = queue.get(timeout=1)
process(msg)
# 退出前把已取出的消息确认或放回队列
queue.nack_unfinished()
注意不要在信号处理器里做耗时操作,只置标志位,让主循环自己收尾。如果用了线程池,还要给每个任务设置超时,否则主线程会一直等在 join 上,systemd 最后只能 SIGKILL。
日志交给 journald,而不是自己写文件
服务里直接 print 到 stdout 即可,systemd 会把输出收进 journal。查最近日志用 journalctl -u worker -n 200 --no-pager,按时间过滤用 --since "10 min ago"。需要长期留存再配 /etc/systemd/journald.conf 的 SystemMaxUse 和 MaxRetentionSec,不要在应用里自己切文件。这样做的好处是日志和启动、退出、重启事件在同一条时间线上,排查“为什么 3 点重启了”时不用再翻两个地方。
资源限制不是可选项
常驻任务最常见的故障不是代码异常,而是把整台机器的内存吃光。unit 里可以用 MemoryMax=512M、CPUQuota=80% 给服务划一条硬边界;超过内存上限时,内核只杀这个 cgroup 里的进程,不会把 ECS 拖死。注意 MemoryMax 不是预留,机器空闲时服务仍然可以用更多,只是不能越过上限。若进程被 OOM kill,systemd 会在 journal 里留下 oom-kill 记录,配合 Restart=on-failure 可以自动拉起,但更重要的是看趋势,别让它每天被杀一次。
定时任务优先用 timer
如果任务不是常驻而是每天跑一次,不要写 while True + sleep,也不一定要用 crontab。systemd timer 能记录上次执行结果,支持单调时钟和随机延迟:
[Unit]
Description=Daily report
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true 表示关机期间错过的任务在开机后补跑一次,这对账单、报表类任务很重要。systemctl list-timers 可以直接看到下次触发时间和上次结果,比 crontab 的“黑盒”友好得多。
上线前检查四件事
- systemctl daemon-reload 之后再用 enable --now,改 unit 不 reload 不会生效;
- 用 systemd-analyze verify worker.service 检查语法,别等重启时才发现拼错;
- 确认 User 对 WorkingDirectory 和日志目录有权限,SELinux 开启时还要看 audit.log;
- 做一次 kill -TERM 演练,确认进程能在 TimeoutStopSec 内退出,而不是每次都被 SIGKILL。
最后给服务加一个独立的 timer 做健康检查,确认端口真的在监听,而不是进程还在、业务已经卡死。把这几步做完,常驻任务才算从“能跑”变成“可运维”。nohup 不是不能用,但它只适合一次性实验;真正要跑几个月的东西,还是交给 systemd。
- 点赞
- 收藏
- 关注作者
评论(0)