ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

systemctl 与 .service 文件全解:从命令到服务管理底层逻辑

systemctl 与 .service 文件全解:从命令到服务管理底层逻辑 我一个跑运维多年的人第一次接触 systemctl 的时候也是一脸懵明明以前service sshd start挺好用的怎么到了新系统上全变成systemctl start sshd了后来真把 systemd 这套体系摸透了才发现它不是换了个马甲而是把整个 Linux 服务管理的方式给重写了。这篇东西我就把 systemctl 命令和 .service 文件的参数一次讲明白送给所有刚开始接触新系统、或者想彻底搞懂服务管理底层逻辑的朋友。1. systemctl 的本质它不只是一个命令而是一套服务管理协议1.1 从 init 到 systemd为什么你会感觉命令变了老运维都记得早期的 Linux 用的是 SysV init开机时内核启动 PID 为 1 的 init 进程然后 init 按照 /etc/rc.d/rc?.d 目录里的脚本顺序启动服务。那个时代的服务脚本本质上是 shell 脚本用start、stop、restart参数控制逻辑全靠脚本自己写且服务之间几乎没有依赖关系管理启动顺序基本靠脚本文件名里的数字编号硬排。systemd 出现后PID 1 的角色被 systemd 取代它不再靠 shell 脚本去执行启动动作而是把每个服务抽象成一个unit单元用配置文件来描述这个服务长什么样、依赖谁、怎么启、怎么停。systemctl 就是用来和这套 unit 体系交互的前端命令。换句话说systemd后台守护进程负责管理所有 unitsystemctl命令行工具用来向 systemd 下发指令、查询状态.service 文件描述某个服务的配置文件是 systemd 管理服务的依据这三者加在一起才构成了你看到的启动一个服务这个动作。1.2 unit 类型不止 service 一种但 service 是最常用的很多人以为 systemd 只管服务其实 unit 类型有很多种常见的包括.service后台服务最常用.socket套接字监听单元可以延迟启动服务.target一组 unit 的集合相当于启动级别runlevel的替代品.timer定时器替代 cron 的方案之一.mount / .automount文件系统挂载单元.path路径监控单元当目录或文件变化时触发服务一个 .service 文件只是 unit 的一种但理解了它其他类型的 unit 配置也能触类旁通。因为所有 unit 文件在语法上高度一致区别只在于各个段落里的专用参数不同。提示用systemctl --typeservice可以只看 service 类型的 unitsystemctl --typetimer只看定时器实际排查问题时这招能帮你快速缩小范围。2. 高频 systemctl 操作别死记命令要理解状态流转2.1 最基本的启停和查询操作先列一份我日常用的最频繁的命令清单后面再解释为什么要这样用# 启动、停止、重启、重载服务 systemctl start servicename systemctl stop servicename systemctl restart servicename systemctl reload servicename # 查看状态 systemctl status servicename # 开机自启相关 systemctl enable servicename systemctl disable servicename # 查看所有 unit 及其状态 systemctl list-units --typeservice systemctl list-unit-files --typeservice # 查看服务是否在运行 systemctl is-active servicename systemctl is-enabled servicename这里有一个新手最容易忽略的点restart和reload不是一回事。restart完全停掉进程再重新启一个新进程适合配置变更后需要彻底重来的场景但会有短暂的服务中断。reload向进程发送 SIGHUP 信号让进程自己重新读取配置文件很多守护进程支持这种平滑重载方式比如 nginx、sshd。用 reload 不会中断正在处理的连接。所以如果只是改了配置文件优先试systemctl reload不行再restart。这样能最大程度保证服务的可用性。2.2 enable/disable 到底做了什么别和 start/stop 混淆这是被误解最多的命令组合。初学者经常以为systemctl enable就是启动服务disable就是停止服务实际上完全两码事start/stop控制服务当前是否运行只影响这一刻的状态重启系统后服务不会记住你的操作。enable/disable控制服务开机是否自动启动它不改变当前运行状态。enable的底层动作其实是在 /etc/systemd/system/ 目录下创建符号链接指向 /usr/lib/systemd/system/ 下的 unit 文件。这个符号链接会被放入某个 .target 的 wants 目录里比如 multi-user.target.wants/。开机时 systemd 会根据 multi-user.target 的依赖去启动这些链接指向的服务。打个比方start是现在把电脑开机enable是设置通电后自动开机。两者互不干扰。2.3 怎么看懂 status 的输出信息systemctl status的输出包含的信息量很大我逐行拆给你看● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled) Active: active (running) since Mon 2024-12-09 09:23:45 CST; 2h 30min ago Process: 12345 ExecStart/usr/sbin/nginx -c /etc/nginx/nginx.conf (codeexited, status0/SUCCESS) Main PID: 12346 (nginx) Tasks: 3 (limit: 1024) Memory: 8.2M CPU: 12ms CGroup: /system.slice/nginx.service ├─12346 nginx: master process /usr/sbin/nginx └─12347 nginx: worker process重点看两个字段Loaded后面有enabled或disabled表示开机自启状态Active括号里是running、exited、failed、dead等。如果服务挂了这里会显示failed并且下方可能带有错误信息Main PID 显示主进程号Tasks 显示进程内线程/任务数Memory 显示内存占用。CGroup 下方列出的是这个服务拉起的所有进程有时候你可以在这里看到服务派生出的子进程。3. .service 文件逐段拆解Unit、Service、Install 分别管什么3.1 [Unit] 段描述服务的元信息和依赖关系一个典型的 .service 文件开头长这样[Unit] DescriptionMy Custom Application Service Documentationhttps://example.com/docs Requiresnetwork-online.target Afternetwork-online.target Wantsmysql.service各参数含义如下Description服务的文字描述systemctl status第一行会显示它。建议写清楚这个服务是干什么的别偷懒。Documentation文档链接帮助后续维护的人快速找到资料非必需但推荐。After/Before定义启动顺序不定义依赖。Afternetwork-online.target表示本服务要等网络就绪后再启动但不代表它依赖网络服务。顺序关系是单方面的。Requires强依赖如果列出的 unit 启动失败本服务也不会启动。但如果依赖单元运行中挂了本服务不会自动停止。Wants弱依赖列出的 unit 会尽量启动但即使失败也不影响本服务。它是 Requires 的温和版实际项目中 Wants 比 Requires 用得更多。Conflicts冲突关系写在这里的 unit 存在时本服务不会被启动适用于互斥场景。BindsTo比 Requires 更强的绑定依赖单元退出时本服务也会跟着停止。关键理解After 管顺序Requires/Wants 管依赖两者要配合使用。如果你只写Requiresnetwork-online.target而不写Afternetwork-online.target可能本服务先启动网络还没就绪依赖等于没有。正确姿势是两条一起写Requiresnetwork-online.target Afternetwork-online.target3.2 [Service] 段核心中的核心定义进程如何运行这是 .service 文件里信息量最大的一段也是排错时最常看的地方。Type 参数Type 决定了 systemd 如何判断服务是否启动成功常见取值有Type 取值含义适用场景simple默认值ExecStart 启动的进程就是主进程只要进程 fork 出来就算启动成功常规进程、脚本exec与 simple 类似但会等 execve 成功后才认为启动成功需要确认可执行文件能正确加载的场景forking主进程启动后会 fork 出子进程父进程退出systemd 认为子进程才是真正的服务进程传统 daemon 程序如 nginx、sshdoneshot执行一次就退出类似初始化任务常配合 RemainAfterExityes一次性初始化脚本notify进程启动后主动通过 sd_notify 接口通知 systemd 就绪需要确认初始化完成的应用程序idle等系统当前任务排空后才启动用于避免输出日志交错输出型脚本Type 选错会带来非常隐蔽的问题。比如你写了一个 python 脚本作为 simple 类型脚本里又用 subprocess 去派生子进程父进程很快就退出了systemd 会发现主进程没了然后判定服务启动失败。而如果用 forking 类型同时正确设置 PIDFilesystemd 会跟踪子进程的状态。ExecStart、ExecStop、ExecReloadExecStart/usr/bin/python3 /opt/myapp/server.py --port 8080 ExecStop/usr/bin/kill -15 $MAINPID ExecReload/bin/kill -HUP $MAINPID注意几个规矩ExecStart 只能写一条如果要写多条命令应该用ExecStartPre和ExecStartPost分别在主进程启动前和执行后运行。ExecStart 里不要用 shell 的管道符、重定向、等语法systemd 不是把这一行丢给 shell 去解释的。如果非要 pipe应该写成ExecStart/bin/bash -c cmd | grep xxx。$MAINPID是 systemd 提供的内建变量代表服务主进程 PID在 ExecStop、ExecReload 里经常用到。Restart 参数崩溃后怎么处理这是生产环境里最重要的参数之一。常见取值no默认值进程退出后不做任何处理。on-success进程正常退出退出码为 0或信号为 SIGHUP、SIGINT、SIGTERM、SIGPIPE时重启。on-failure进程非正常退出时重启包括退出码非 0、被信号杀死、超时看门狗触发等。on-abnormal进程被信号杀死、超时、看门狗触发时重启不包含退出码非 0 的情况。always不管什么方式退出永远重启。我在生产环境里最常用的组合是Restarton-failure RestartSec3sRestartSec表示重启前的等待时间防止服务疯狂重启。但要注意如果你的服务被 systemctl stop 主动停止不管 Restart 设成什么都不会自动重启这是 systemd 特意设计的逻辑不用担心手动停掉又自己起来。用户、权限与资源限制Usermyapp Groupmyapp WorkingDirectory/opt/myapp EnvironmentJAVA_HOME/usr/lib/jvm/java-11-openjdk EnvironmentFile/etc/myapp/config.env UMask0022 LimitNOFILE65535 LimitNPROC5120User/Group指定运行身份。永远不要用 root 跑业务服务这是底线除非它确实需要特殊权限。Environment直接写环境变量多条写多个 Environment 行。EnvironmentFile从外部文件加载环境变量文件格式是KEYVALUE每行一条。因为密码、密钥这类敏感信息通常不想写在 unit 文件里我会用 EnvironmentFile 把运行配置抽离出来同时也方便在服务运行时动态改配置。LimitNOFILE文件描述符上限。很多高并发服务默认 1024 不够用需要调大。UMask决定服务创建文件的默认权限掩码比如 0022 就是让新文件变成 644 权限。3.3 [Install] 段开机自启的挂载点[Install] WantedBymulti-user.target这里最常见的就是WantedBymulti-user.target意思是把本服务挂到 multi-user.target 的 wants 目录下开机进入多用户模式时就会自动启动。如果写WantedBymulti-user.targetenable 时 systemd 会在 /etc/systemd/system/multi-user.target.wants/ 下创建符号链接。如果写RequiredBy则会创建到 .required 目录语义更强硬关联单元启动失败会影响本单元。Aliasservicename.service可以给服务设置别名比如systemctl start name能直接解析到真实服务。Also可以指定 enable/disable 本服务时顺带启用/禁用其他 unit。systemctl enable操作只在 [Install] 段存在时才有意义。你打开一个 .service 文件如果发现没有 [Install] 段那这个服务设计上就不需要 enable它通常是被其他服务或者 target 拉起来的。4. 从零写一个 .service 文件用实例说话4.1 一个最简的 Python Web 服务假设我有一个 Flask 应用启动命令是python3 /srv/myapp/app.py --port 8080对应的服务文件 /usr/lib/systemd/system/myapp.service[Unit] DescriptionMy Flask Application Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Usermyapp Groupmyapp WorkingDirectory/srv/myapp ExecStart/usr/bin/python3 /srv/myapp/app.py --port 8080 Restarton-failure RestartSec3s [Install] WantedBymulti-user.target写好后执行sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo systemctl status myapp这里有个细节Typesimple对 python 进程来说是可以的因为 python 进程会直接占据前台不会 fork 子进程。但如果你这个应用内部用了 gunicorn 或者 uwsgi启动方式变成gunicorn app:app它默认会 fork worker 进程父进程还在前台所以仍然可以用 simple。只有那种父进程启动后立刻退出、由子进程继续工作的 daemon 程序才要用 forking。4.2 一个 Java 服务的完整配置Java 服务在 systemd 场景里有几个坑内存占用大、需要环境变量、可能产生 core dump。下面是我在生产环境打磨过的配置[Unit] DescriptionJava Backend Service Requiresnetwork-online.target Afternetwork-online.target mysql.service Wantsmysql.service [Service] Typesimple Userappuser Groupappuser WorkingDirectory/srv/backend EnvironmentJAVA_HOME/usr/local/jdk17 EnvironmentSPRING_PROFILES_ACTIVEprod EnvironmentFile/etc/backend/backend.env ExecStart/usr/local/jdk17/bin/java -Xms512m -Xmx1024m -jar /srv/backend/backend.jar SuccessExitStatus143 Restarton-failure RestartSec5s TimeoutStartSec120 TimeoutStopSec30 LimitNOFILE65535 UMask0027 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target几个参数单独解释SuccessExitStatus143Java 程序被 SIGTERM 信号终止时退出码通常是 14312815。如果不加这个配置systemd 会认为非正常退出触发重启逻辑。加上后表示退出码 143 也算成功退出。TimeoutStartSec服务启动超时时间。Java 应用启动慢默认 90 秒可能不够调大一些。TimeoutStopSec服务停止超时时间给了 JVM 优雅停机的缓冲。StandardOutputjournal / StandardErrorjournal把服务的 stdout 和 stderr 输出到 journald 日志系统这样可以直接用journalctl -u backend看日志非常方便。4.3 一个服务完成后必须执行清理动作的场景有些应用停止时希望执行干净的收尾比如通知注册中心下线、删除临时文件。这时候用 forking ExecStop 组合能实现很好的效果[Service] Typeforking PIDFile/run/app.pid ExecStart/srv/app/bin/app_server ExecStop/bin/bash -c kill -TERM $(cat /run/app.pid) sleep 5 rm -f /run/app.pid ExecReload/bin/kill -HUP $MAINPID Restartalways KillModecontrol-groupPIDFile告知 systemd 主进程 PID 存放位置。forking 类型下systemd 需要靠这个文件找到真正的 daemon 进程。KillModecontrol-group服务停止时杀掉整个 cgroup 内的所有进程而不是只杀主进程。这个参数能防止子进程成了漏网之鱼继续占用资源。5. 排查链路服务起不来的时候我按这个顺序查5.1 改了配置文件必须 daemon-reload这是最常见的坑很多新手改了 .service 文件后直接systemctl restart结果发现改动没生效甚至报错。原因很简单systemd 在启动时会缓存unit 文件你改了文件后缓存里还是旧的必须用systemctl daemon-reload让 systemd 重新读取全部 unit 文件。正确姿势sudo vim /usr/lib/systemd/system/myapp.service sudo systemctl daemon-reload sudo systemctl restart myapp忘了 daemon-reload 的表现很迷惑有时候你改了 ExecStart 但 restart 后还是只跑旧命令。这不是系统坏了是缓存没刷新。我现在改完任何 unit 文件第一反应就是 daemon-reload宁可多执行一次不要白等半天查不出问题。5.2 用 status 和 journalctl 定位问题服务启动失败时第一站永远是systemctl status 服务名它会给结果提示。比如● myapp.service - My Flask Application Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: disabled) Active: failed (Result: exit-code) since Mon 2024-12-09 10:00:00 CST; 3s ago Process: 12345 ExecStart/usr/bin/python3 /srv/myapp/app.py --port 8080 (codeexited, status1/FAILURE) Main PID: 12345 (codeexited, status1/FAILURE)注意看status1说明应用进程自己退出且退出码为 1通常是应用代码报错不是 systemd 配置问题。这时要去看应用日志# 查看指定服务的全部日志 journalctl -u myapp.service -n 50 # 实时跟踪日志 journalctl -u myapp.service -f # 看从上次启动至今的日志排除历史陈旧内容 journalctl -u myapp.service --since 5 minutes ago如果日志显示的是Permission denied那问题可能出在 User/WorkingDirectory 配置上检查目录权限。如果日志是Address already in use那就是端口冲突用ss -lnp | grep 端口号查看是谁占用了。5.3 使用 systemd-analyze 做依赖和启动耗时分析这个命令容易被忽略但排查依赖问题很管用# 查看关键 unit 的依赖树 systemd-analyze critical-chain myapp.service # 查看启动耗时排名 systemd-analyze blame | head -20 # 验证 unit 文件是否有语法错误 systemd-analyze verify /usr/lib/systemd/system/myapp.servicesystemd-analyze verify会在不启动服务的前提下检查配置文件语法能发现拼写错误的参数名、缺失的可执行文件等问题。我在写完一个 .service 文件后执行一遍 verify 再 daemon-reload基本能过滤掉低级错误。6. 实战中总结的经验参数优先级、修改方式和几个隐蔽大坑6.1 配置优先级/etc/systemd/system 覆盖 /usr/lib/systemd/system服务文件的查找路径有优先级之分。systemd 会按以下顺序查找 unit 文件/etc/systemd/system/系统管理员自定义配置优先级最高/run/systemd/system/运行时配置重启丢失/usr/lib/systemd/system/软件包安装时提供的默认配置这意味着如果软件包自带的 /usr/lib/systemd/system/nginx.service 不满足你的需求不要直接改原文件而是把文件复制到 /etc/systemd/system/ 同名路径下再修改sudo cp /usr/lib/systemd/system/nginx.service /etc/systemd/system/nginx.service sudo vim /etc/systemd/system/nginx.service sudo systemctl daemon-reload systemctl status nginx.service这样升级软件包时新版本的默认 unit 文件可能被覆盖但你的自定义配置在 /etc 下不受影响。用systemctl cat nginx.service可以同时查看所有生效配置systemd 会合并显示低优先级文件内容和你的覆盖配置。6.2 不要在一行 ExecStart 里写复杂 shell 逻辑systemd 的 ExecStart 不做 shell 解析这意味着ExecStart/usr/bin/command | grep x /tmp/log不会按预期工作。管道、重定向、通配符、环境变量展开这些 shell 特性都需要借助/bin/bash -c才能实现。但尽量别这么用。我在实际项目中见过有人把几百字符的 shell 命令塞进 ExecStart 里排错的时候修改一行配置就必须 daemon-reload非常痛苦。更优雅的做法是把复杂启动逻辑写成一个独立的启动脚本ExecStart 只负责调用这个脚本ExecStart/srv/myapp/start.sh启动脚本可以随便用 shell 语法灵活度高调试也方便还能在脚本里加日志输出。6.3 确认服务真的跑起来了别只看 Active: runningsystemctl status显示active (running)只代表 systemd 认为服务活着不代表业务正常。比如一个 Tomcat 服务systemd 显示 running但 Web 端口没监听很可能应用启动失败但进程没退出。我习惯三步确认# 1. systemd 角度 systemctl is-active myapp # 2. 进程角度 ps -ef | grep myapp # 3. 端口角度服务是监听端口的话 ss -tlnp | grep 8080三方面都正常才算真的没问题。生产环境里经常出现 systemd 与业务状态不一致的情况尤其是在应用内部有健康检查失败、连接池耗尽这类进程活着但服务不可用的场景。如果应用本身支持健康检查接口建议定时 curl 探测一下。6.4 用 systemd 的定时器还是 cron 既然讲到了 systemd顺便说一句它自带的 .timer 单元。timer 完全可以替代大部分 cron 场景而且优势明显可以把任务配置成与 .service 分离的单元支持实时看状态、日志、依赖控制、错过上次执行时间后的补跑逻辑。一个最简 timer 例子每天凌晨 2 点执行备份服务的配置# /etc/systemd/system/backup.timer [Unit] DescriptionDaily backup timer [Timer] OnCalendar02:00:00 Persistenttrue [Install] WantedBytimers.target# /etc/systemd/system/backup.service [Unit] DescriptionDaily backup job [Service] Typeoneshot ExecStart/usr/local/bin/backup.sh然后执行systemctl daemon-reload systemctl enable --now backup.timer即可。Persistenttrue表示如果到点系统处于关机状态开机后会自动补跑一次。这在 cron 里可没这么简单。6.5 最后分享一个我处理过的真实案例有一次生产环境里的 Java 服务每隔几天就神不知鬼不觉地消失systemctl status 显示active (exited)但明明我们配置了Restartalways。排查了很久发现原来启动脚本里用了nohup java -jar xx.jar 程序被放到了后台systemd 认为主进程脚本进程已经退出于是把服务标记为 exited。而 Java 进程变成了一个不属于任何 systemd unit 的孤儿进程。这个问题的根因是启动方式与 Type 不匹配。正确做法是不要在启动脚本里用nohup、、setsid等让进程脱离前台Type 改成 forking并且让脚本显式将子进程 PID 写入 PIDFilesystemd 才能正确追踪后来我把脚本里的 nohup 去掉Type 改为 simple世界清净了。这个案例给我的教训是systemd 的判定逻辑基于主进程是否存活你要让 systemd 清楚地知道哪个进程才是服务的命脉。systemctl 和 .service 这套体系看着复杂但只要理解了 unit 抽象、状态机、参数语义这三件事绝大多数问题都能自己解决。把这篇文章里的配置模板拿过去改一改基本能覆盖日常服务的 95% 场景。真遇到奇怪的启动失败按照我说的方法逐层排查从 unit 语法到应用日志一步步来问题早晚会浮出来。
返回列表