做运维这几年,我见过太多因为不懂systemd导致的事故了。有一次,某电商平台大促期间,凌晨三点接到告警——核心支付服务挂了。我冲到机房一看,服务进程还在,但就是没响应。查了一圈发现,原来是systemd的Restart策略没配对,服务异常退出后systemd以为它正常结束了,没自动重启。
那一刻我才真正意识到:systemd这玩意儿,不是简单的启动停止命令,它是Linux系统管理的核心引擎。不懂它,你连服务怎么挂的都不知道。
今天就把我这些年踩过的坑、总结的经验全倒出来,咱们从头把这个systemd讲透。
一、systemd到底是什么?先别急着学命令
很多人一上来就学systemctl start、stop、enable,但说实话,这样学效率很低。你得先搞懂systemd是个什么玩意儿,它怎么工作的。
2010年,Linux发行版们开始用systemd替代传统的SysVinit。老版本的init进程,启动服务是一层一层按顺序来的——先启动网络,再启动数据库,最后启动应用。听起来没问题?问题大了:
顺序启动太慢了。CentOS 6的系统开机到登录界面,动辄两三分钟。为什么?因为所有服务排着队一个一个启动,互相等待。
依赖关系混乱。某个服务要依赖另一个服务,你得手动写脚本,在启动命令里加sleep等待对方。这种做法有多坑,谁踩过谁知道。
systemd解决的就是这些问题。它用的是并行启动——能同时启动的服务同时跑,有依赖关系的才等待。开机速度直接缩短一半以上。
更重要的是,systemd把所有系统资源都抽象成Unit(单元)。服务是Service单元,挂载点是Mount单元,设备是Device单元……所有这些单元都由systemd统一管理,依赖关系用配置文件声明,不用你再写那些蹩脚的等待脚本。
简单说,systemd不是一个命令,它是Linux系统的总管家——管服务、管设备、管挂载、管日志、管定时任务……几乎什么都管。
二、Unit单元:systemd的核心概念
要玩转systemd,你得先理解Unit。
Unit就是systemd管理的各种单元。比如你管理的web服务,在systemd眼里就是一个Service单元。你挂载的硬盘分区,就是一个Mount单元。
Unit的类型有很多,常用的有这些:
• Service:服务单元,最常用的类型。nginx、mysql、redis这些服务,都是Service单元。
• Target:目标单元,相当于一组Unit的集合。比如multi-user.target就包含了所有多用户模式需要的服务。
• Timer:定时任务单元,替代传统的cron。
• Mount/Automount:挂载点管理。
• Socket:套接字单元,用于socket激活机制。
每个Unit都有一个配置文件,通常在/etc/systemd/system/或/usr/lib/systemd/system/目录下。
看一个典型的Service单元配置:
[Unit]
Description=Nginx HTTP Server
After=network.target
Requires=network.target
[Service]
Type=forking
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/usr/sbin/nginx -s stop
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
这个配置分三段:
[Unit]段:定义单元的基本信息和依赖关系。Description是描述信息,After表示在本单元启动之前要先启动哪些单元,Requires是强依赖。
[Service]段:服务特有的配置。Type是服务类型,ExecStart是启动命令,Restart是重启策略。
[Install]段:安装行为。WantedBy表示enable时,本单元会被添加到哪个Target的依赖列表。
这里有个坑我踩过:After和Requires的区别。After只控制启动顺序,Requires才是真正的依赖关系。有一次我只写了After=network.target,没写Requires,结果网络还没准备好,服务就启动失败了。所以,如果真的依赖某个服务,两个都要写。
三、systemctl核心命令详解
说完概念,咱们来看实操。systemctl是systemd的主要命令行工具,功能强大但命令也多。我把常用的按场景分类。
1. 查看系统状态
# 查看systemd版本
systemctl --version
# 查看系统当前target
systemctl get-default
# 查看所有已加载的单元
systemctl list-units
# 只看服务单元
systemctl list-units --type=service
# 查看某个单元的详细信息
systemctl status nginx.service
status命令是最常用的。它显示服务是否在运行、进程ID、最近几条日志、单元配置文件的路径。
2. 服务管理
# 启动服务
systemctl start nginx
# 停止服务
systemctl stop nginx
# 重启服务
systemctl restart nginx
# 重新加载配置(不重启)
systemctl reload nginx
# 查看服务是否开机自启
systemctl is-enabled nginx
# 设置开机自启
systemctl enable nginx
# 启动并开机自启(一条命令搞定)
systemctl enable --now nginx
这里有个常见的误解:enable不是启动服务,它只是创建符号链接,让服务在下次开机时自动启动。要立即启动还要再执行start。enable --now是两者的组合。
3. 单元文件管理
# 查看单元文件内容
systemctl cat nginx
# 编辑单元文件
systemctl edit nginx
# 重新加载所有单元文件
systemctl daemon-reload
daemon-reload这个命令非常重要!每次修改单元配置文件后,都要执行它,否则systemd不会读到新配置。这个坑我踩过多次——改了配置重启服务,发现还是老配置生效。
四、服务类型Type详解:这个坑踩的人最多
Service配置里的Type参数,是最容易踩坑的地方。我见过不少人配服务时随手写个Type=simple,结果服务启动超时、状态异常。
Type有五种可选值:
• Type=simple:默认值。ExecStart启动的进程就是主进程,适用于不会fork的服务。
• Type=forking:ExecStart启动的进程会fork出子进程,然后父进程退出。适用于传统的daemon程序,比如nginx、apache。
• Type=oneshot:ExecStart执行的命令是一次性的,执行完就结束。适用于开机执行一次的任务。
• Type=notify:服务启动后,会通过sd_notify()函数主动通知systemd自己已经准备好。
• Type=dbus:服务启动后,会在D-Bus上注册一个名字。适用于D-Bus服务。
举个典型错误案例:有个同事配了一个Java服务,Type写的是simple。但这个Java服务启动时会fork出一个守护进程,然后主进程退出。systemd看到主进程退出,就认为服务启动失败了。
正确的配置应该是Type=forking,systemd会等待主进程退出(这是预期的),然后找到fork出的子进程作为服务进程。
五、日志管理journalctl:比tail -f好用十倍
传统Linux用syslog记录日志,日志文件散落在/var/log/各个目录下。systemd自带了日志系统journald,统一管理所有日志。
journalctl是查看日志的命令,功能强大:
# 查看所有日志
journalctl
# 实时查看日志(类似tail -f)
journalctl -f
# 查看某服务日志
journalctl -u nginx.service
# 查看最近100条
journalctl -n 100
# 查看今天的日志
journalctl --since today
# 查看某时间段日志
journalctl --since "2024-01-01 10:00" --until "2024-01-01 12:00"
# 只看错误级别日志
journalctl -p err
journald有个特点是日志默认存储在内存里,重启后就没了。要持久化存储,需要创建/var/log/journal目录。
六、定时任务Timer:比cron更强大
systemd的Timer单元是定时任务的替代方案,比传统cron更强大。
Timer的优势:
• 可以精确控制执行时间
• 日志自动记录到journald
• 任务执行失败可以触发其他动作
• 支持随机延迟避免负载高峰
Timer需要配合Service单元使用。创建一个简单的Timer:
# cleanup.timer
[Unit]
Description=每天清理临时文件
[Timer]
OnCalendar=daily
Persistent=true
Unit=cleanup.service
[Install]
WantedBy=timers.target
OnCalendar字段类似cron,格式灵活:
• daily:每天执行
• hourly:每小时执行
• Mon *-*-* 06:00:00:每周一早上6点
Persistent=true的作用是:如果系统在预定执行时间关机了,下次开机时会补执行一次。
七、实战案例:从踩坑到避坑
说这么多理论,不如看几个真实案例。
案例1:服务启动超时
现象:某服务systemctl start后一直显示activating,最后超时变成failed。
排查:先看status,发现超时信息;看日志,发现服务在启动时卡在某一步;检查配置,发现Type=simple但服务启动很慢。
解决:把Type改成notify,或者增加TimeoutStartSec=300。
案例2:服务重启但systemd不知道
现象:进程意外退出后被其他脚本重新启动,但systemctl status显示inactive。
原因:systemd是通过cgroups跟踪进程的。如果进程不在它管理的cgroups里,systemd就找不到它。
解决:不要手动启动服务进程,让systemd来管理。
案例3:开机服务顺序问题
现象:开机后服务A启动失败,日志显示依赖的服务B还没准备好。
解决:检查Unit配置的After和Requires,确保依赖关系正确声明。
八、总结:避坑指南和最佳实践
把这些年的踩坑经验总结成几条建议:
1. Type要选对:不会fork的服务用simple,会fork的传统daemon用forking,执行一次的任务用oneshot。
2. 依赖要写完整:After只控制顺序,Requires才是依赖。需要网络真正可用用network-online.target。
3. 改配置要daemon-reload:每次修改单元文件都要执行,否则systemd用的还是旧配置。
4. 日志要看journalctl:不要到处找/var/log/下的日志文件,journalctl -u服务名能看到所有相关日志。
5. Timer比cron好用:新建定时任务优先用Timer,日志自动记录,错过执行时间可以补执行。
6. 资源限制要配:生产环境服务要配MemoryMax、CPUQuota,防止某个服务失控拖垮整个系统。
systemd这玩意儿,说复杂也复杂,说不复杂也不复杂。关键是理解它的设计思路——一切都是单元,依赖关系用配置文件声明,状态统一管理。掌握了这套思路,排查问题就顺手多了。
下次遇到服务启动失败、开机慢、定时任务执行不了这些问题,先别急着百度,想想systemd是怎么工作的,用journalctl看看日志,多半能自己找到原因。