Ubuntu service 与 systemctl:服务启停、自启与排错

发布时间:2026/10/1 13:19:50
Ubuntu service 与 systemctl:服务启停、自启与排错 1. 把 Ubuntu 服务管理这件事先说清楚刚接触 Ubuntu 的人几乎都会在同一个地方卡一次装完 Nginx 或者 MySQL想启动它敲了sudo service nginx start屏幕一闪什么都没输出然后心里就没底了——到底起来了没有再敲一次stop还是什么都不说。这种沉默的命令行是很多新手最不适应的地方。这篇文章就把service这条命令从头到尾拆开讲包括它在 Ubuntu 上到底是什么、底层转发给了谁、启动/关闭/重启/重载分别在做什么、什么场景该用哪个、以及真正踩过的那些坑。service命令在 Ubuntu 上的角色其实比很多人想象的复杂。它不是Ubuntu 原生的服务管理工具而是一个兼容层。Ubuntu 15.04 之前的系统用 Upstart 做 1 号进程之后全面切换到了 systemd。但大量老教程、老脚本、老运维习惯里写的都是service xxx start这种写法为了不把这些东西全部打断Ubuntu 保留了一个/usr/sbin/service的 shell 脚本它负责把请求转发到当前系统真正在用的初始化系统上。所以你敲的service在今天的 Ubuntu 上十有八九最后执行的是systemctl。理解了这一点后面很多事情就通了。为什么service nginx enable会报错因为service这个脚本压根没实现 enable 这个动作它只转发 start/stop/restart/reload/status 这几个。为什么service和systemctl有时行为不一致因为转发逻辑里会先看/etc/init.d/下有没有同名脚本有的话优先按 SysV 方式跑没有才去找 systemd 单元。这条分支决定了你写的服务到底以哪种方式被拉起值得记一下。这篇文章的目标读者很明确刚上手 Ubuntu 服务管理的开发者、需要在自己机器或服务器上部署应用的人、以及被service: unrecognized service报错卡住的运维新人。内容从命令语法讲到自建服务再到故障排查速查表全部是我自己在实际机器上跑过的流程尽量给到可以直接复制粘贴的程度。2. service 命令的语法结构与底层转发逻辑2.1 一条命令的完整构成拆解service的语法非常朴素就两种形式service 服务名 动作 service --status-all第一部分是服务名注意这里不带 .service 后缀写的是nginx不是nginx.service。第二部分是动作常见的有动作含义是否所有服务都支持start启动服务是stop停止服务是restart停止后再启动是reload重载配置不中断进程否取决于服务自身实现status查看运行状态是force-reload强制重载必要时才重启否--status-all是一个特殊参数它会遍历系统里所有能识别的服务逐个调用它们的 status 动作然后汇总输出。输出格式长这样[ ] cron [ - ] apache2 [ ? ] alsa-utils三个符号的含义必须记牢这是新手最容易误读的地方[ ]表示服务正在运行[ - ]表示服务已停止[ ? ]表示状态未知通常是这个服务的 status 动作没有返回值或者脚本本身写得不够规范不代表它一定有问题也不代表它一定没问题很多人看到一堆[ ? ]就慌了以为系统坏了。实际上 Ubuntu 里有相当多的 SysV 风格脚本本来就没实现规范的 status 返回码[ ? ]是常态。想看准确状态还是得回到systemctl去查。另外提一句service --status-all执行起来非常慢因为它是一个串行循环逐个调用脚本。在装了几十个服务的机器上等十几秒很正常。如果你只是想快速看几个关键服务的状态别用这个命令。2.2 转发逻辑service 到底调用了谁打开/usr/sbin/service这个文件你会看到它本质上是一堆 shell 函数。核心判断逻辑大致是这样的先检查/etc/init.d/服务名这个文件是否存在且可执行如果存在就按传统 SysV 方式执行/path/服务名 动作如果不存在就去 /lib/systemd/system 或 /etc/systemd/system 里找同名的.service单元找到就转发给systemctl两个都找不到直接报unrecognized service。这个顺序很关键。假设你在/etc/init.d/下手动放了一个叫nginx的脚本那么即使系统里同时存在 systemd 的nginx.serviceservice nginx start也会优先跑你那个脚本。这种两套东西同名打架的情况在迁移老项目时非常常见症状是启动看起来成功了但systemctl status nginx显示 inactive两边状态对不上。想知道自己系统上跑的到底是哪一套一条命令就能确认ps -p 1 -o comm输出systemd就是 systemd 系统输出init就是老式 SysV。现在能见到的 Ubuntu16.04 以后基本都是前者。确认某个服务走哪条路径可以这样查ls -l /etc/init.d/ | grep 服务名 systemctl list-unit-files | grep 服务名哪个存在基本就是走哪条路。两条都存在的话按前面的规则/etc/init.d/优先。2.3 service 与 systemctl 的对应关系对照表既然底层转发给了 systemctl那把两套命令对照起来看会省很多事。下面这张表是我自己整理的常用对照写脚本时按场景挑就行需求service 写法systemctl 写法说明启动service nginx startsystemctl start nginx等价停止service nginx stopsystemctl stop nginx等价重启service nginx restartsystemctl restart nginx等价进程会重新起重载service nginx reloadsystemctl reload nginx不断连接前提是服务支持状态service nginx statussystemctl status nginxsystemctl 信息更全开机自启不支持systemctl enable nginxservice 没这个动作取消自启不支持systemctl disable nginx同上是否自启不支持systemctl is-enabled nginx同上彻底屏蔽不支持systemctl mask nginx比 disable 更狠列出所有service --status-allsystemctl list-units --typeservice后者更快更准看到这里其实就有个结论了新写的东西一律用 systemctlservice 只在维护老脚本时用。这不是喜新厌旧而是因为 service 缺了 enable/disable/mask 这一整块能力而开机自启几乎是每个生产服务都绕不开的需求。我见过不少人用service把服务跑起来了重启机器之后发现服务没了回头到处找原因其实就是不知道自启要另外配。3. 启动、关闭、重启、重载四个动作到底做了什么3.1 start 与 stop 背后的进程生命周期start做的事情本质上是让 systemd 按照服务单元文件里的ExecStart字段fork 出一个进程并且把它纳入这个服务的 cgroup 里管理。所谓 cgroup你可以理解成给进程画了个圈圈里所有进程的生死都由 systemd 统一负责。这个机制带来一个很实用的副作用服务启动后它派生出来的子进程也会被算在这个服务名下。所以用systemctl status nginx看到那一串进程树都是同一次 start 拉起来的。stop则相反systemd 会向 cgroup 里的进程发信号。默认先发SIGTERM也就是礼貌地请它退出给进程一个清理现场的机会。如果超时时间默认TimeoutStopSec90s内还没退就补一个SIGKILL强杀。这个设计很合理但也会带来一个经典现象stop 命令敲下去后卡住不返回就是因为它在那儿等进程自己退等满 90 秒才动手强杀。实操建议遇到 stop 卡住的情况别急着 CtrlC 或者开新终端去 kill。先看一眼目标进程是不是在做什么收尾工作比如把内存里的数据刷盘。数据库服务尤其如此MySQL 停止时会做 buffer pool 的落盘强行打断有可能造成数据文件不一致。真要加速可以调单元文件里的TimeoutStopSec但这属于我明确知道自己在干什么才该动的参数。start还有一个容易被忽略的点它不负责确认服务真的能用了。systemd 只保证进程起来了、没立刻退出业务层面的可用性它管不了。所以正确姿势是start之后紧跟一个status或者直接 curl 一下本地端口确认服务真的对外可用了再去干别的。3.2 restart 与 reload 的本质区别这两个动作经常被混用但差别非常大选错了可能造成线上短暂不可用。restart是杀掉旧进程重新拉起新进程。这个过程中的一小段时间里服务是不可用的。对于 Nginx 这种秒起的服务可能只丢几十毫秒用户感知不到但对于一个要加载几个 G 索引的服务重启可能就是几分钟的事。reload是向正在运行的进程发送一个信号通常是SIGHUP让它自己去重新读取配置文件进程本身不退出、不重启。理论上连接不会断内存里的缓存也能保留。Nginx 的 reload 就是这么工作的主进程收到信号后按新配置起一批新的 worker然后优雅地把老 worker 关掉整个过程对外几乎无感。但这个好是有前提的注意不是所有服务都实现了 reload。有些服务单元里压根没写ExecReload字段这时候执行 reload 会直接报错或者被 systemd 当成 restart 处理。执行前先确认一下服务支不支持最稳妥的做法是查单元文件里有没有 ExecReload。systemctl cat nginx | grep ExecReload有输出说明支持 reload没输出就老老实实用 restart。我自己的习惯是改配置优先 reload改代码或者依赖版本才 restart。这个判断标准简单好用能避开大部分无谓的服务中断。3.3 一条完整的实操演示下面把整套流程走一遍用 Nginx 举例。假设你刚apt install nginx完服务默认已经起来了我们把它停掉再重新走一遍。先确认状态sudo service nginx status输出里会看到Active: active (running)以及主进程 PID 和一串 worker 进程。停掉它sudo service nginx stop这次再查状态会变成Active: inactive (dead)。启动回来sudo service nginx start改配置后重载先验证语法再 reload这个顺序很重要sudo nginx -t sudo service nginx reloadnginx -t会检查配置文件语法输出syntax is ok和test is successful才算过。这一步千万别省语法错了直接 reloadNginx 会保持旧配置不变你以为生效了其实没有排查半天才发现是配置文件写错了。更坑的是重启场景语法错了 restart 会直接失败服务停在那儿起不来这时候如果没有旧进程兜底就是实打实的服务中断。重启sudo service nginx restart设成开机自启这一步 service 做不到sudo systemctl enable nginx验证是否设成功sudo systemctl is-enabled nginx输出enabled就对了。反过来disable就是取消自启。4. 把自写程序注册成系统服务4.1 为什么要把程序做成服务很多人写了个 Python 脚本或者 Go 二进制习惯用nohup ./app 挂在后台。这种方式能用但问题一堆机器重启后进程没了进程崩了没人拉起来日志要自己重定向想停止只能靠ps找 PID 然后 kill。这些事在开发和测试阶段无所谓一旦到了需要长期稳定运行的场景就全是隐患。做成 systemd 服务之后上面这些问题一次性解决开机自启、崩溃自动重启、日志统一进 journald、启停用统一命令。代价只是写一个十几行的配置文件非常划算。4.2 单元文件的完整写法与逐字段解释服务单元文件放在/etc/systemd/system/下文件名就是服务名比如myapp.service。[Unit] DescriptionMy Demo Application Documentationhttps://example.internal/myapp Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userwww-data Groupwww-data WorkingDirectory/opt/myapp EnvironmentPYTHONUNBUFFERED1 EnvironmentAPP_ENVproduction ExecStart/usr/bin/python3 /opt/myapp/app.py ExecReload/bin/kill -HUP $MAINPID Restarton-failure RestartSec3 TimeoutStopSec20 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target几个字段值得单独说清楚这些是最容易配错的地方。Type决定了 systemd 怎么判断服务启动完成。选错了会直接导致启动超时或者假死simpleExecStart 一条命令直接跑在前台不 fork。绝大多数自己写的应用都是这种包括 Node、Python、Go 编译出的二进制。forking程序自己 fork 到后台父进程退出。Nginx、PHP-FPM 这类传统守护进程是这个类型。用 simple 去配它们会导致 systemd 以为进程退出了反复重启。oneshot执行一次就结束适合初始化脚本、挂载动作这类。notify程序通过 sd_notify 主动通知启动完成最精确但需要程序配合改造。选错 Type 的典型症状是服务反复重启journalctl里满屏的 Started/Stopped 记录。Restarton-failure是我最常用的策略意思是进程以非 0 退出码结束、被信号杀死、或者超时的时候自动拉起。RestartSec3是重启前等 3 秒避免疯狂重启把 CPU 打满。需要注意的是如果服务是被systemctl stop主动停的on-failure不会把它拉起来这正是我们想要的。Environment和User这两项在日常排错里出现频率极高。忘记设 User 的话服务以 root 身份跑安全上是减分项设了 User 但工作目录权限不对服务会因为权限不足起不来。我的经验是新建一个专用低权限用户把工作目录和日志目录的属主给它比直接拿 www-data 凑合更清晰。4.3 从零到跑起来的完整步骤写完文件之后按顺序执行这几步缺一步都容易出问题# 1. 修正文件权限避免被 systemd 警告 sudo chmod 644 /etc/systemd/system/myapp.service # 2. 关键一步让 systemd 重新扫描单元文件 sudo systemctl daemon-reload # 3. 启动 sudo systemctl start myapp # 4. 查看状态重点看最后几行日志 sudo systemctl status myapp # 5. 设置开机自启 sudo systemctl enable myapp第 2 步是新手最常漏的。每次新增或修改/etc/systemd/system/下的文件都必须执行daemon-reload否则 systemd 还在用内存里的旧版本。漏了这一步的症状是文件明明改了systemctl start却报Unit myapp.service not found或者行为跟改之前一模一样让人怀疑人生。还有一个习惯值得养成不要直接改/lib/systemd/system/里的原始文件。那个目录属于软件包管理的范围apt upgrade时你的修改会被覆盖掉。想改系统自带服务的配置用 override 机制sudo systemctl edit nginx这会在/etc/systemd/system/nginx.service.d/下生成一个override.conf你写进去的内容会叠加在原始配置之上升级也不会丢。想完全接管用systemctl edit --full nginx它会复制一份完整配置到/etc/systemd/system/下让你随便改。4.4 老式 SysV 脚本方式了解即可在一些老项目或者特定发行版里你还会看到/etc/init.d/下的 shell 脚本。结构上就是接收start/stop/restart这几个参数然后自己实现对应的操作通常配合一个 PID 文件来记录进程号。开机启动靠update-rc.d管理sudo update-rc.d myapp defaults sudo update-rc.d -f myapp remove这种写法今天已经不建议新项目使用了主要是因为它不依赖 cgroup进程跑丢了 systemd 根本不知道也没法自动拉起。只有在维护十年前的遗留系统时才值得花时间研究。5. 服务操作中的典型故障与排查思路5.1 unrecognized service 的三类成因这是service命令最高频的报错看到它先按下面三条排查第一服务名拼错了。service mysql status和service mysqld status是两个不同的名字Ubuntu 上装 MySQL 一般是mysql某些发行版是mysqld。不确定的话先列出所有单元名找找systemctl list-unit-files --typeservice | grep -i mysql第二服务压根没装或者没装全。比如你只装了客户端没装服务端自然找不到对应单元。第三单元文件存在但没被识别。这种情况通常是刚创建完文件没执行daemon-reload执行一次就好。5.2 启动失败但看不到任何报错的排查路径service命令有个很讨厌的特性启动失败时经常什么都不输出。这时候不要瞎猜直接上systemctl statussudo systemctl status myapp --no-pager -l--no-pager避免输出被翻页程序截断-l表示完整显示长行不省略。输出里最关键的是最后那几行日志服务的真实报错都在那里。如果 status 里的信息还不够去翻 journal# 看最近 50 行 sudo journalctl -u myapp -n 50 --no-pager # 实时跟踪类似 tail -f sudo journalctl -u myapp -f # 只看本次开机之后的 sudo journalctl -u myapp -b # 按时间过滤 sudo journalctl -u myapp --since 30 min ago顺便说一下 journal 日志的持久化问题。默认情况下Ubuntu 的 journald 日志是存在内存里的重启就没了。想看历史日志得先开启持久化sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal sudo systemctl restart systemd-journald之后日志会落到/var/log/journal/下重启也能查。查日志占了多少磁盘用journalctl --disk-usage清理用journalctl --vacuum-size500M。5.3 端口占用、权限不足、配置语法错误这三类占了服务启动失败原因的绝大多数值得单独列出来。端口被占是最常见的。现象是服务启动后立刻退出日志里出现Address already in use或者bind: address already in use。查是谁占的sudo ss -tlnp | grep :8080 # 或者 sudo lsof -i :8080ss是netstat的现代替代品速度更快Ubuntu 新版本默认都装了。找到 PID 之后先确认那个进程是不是你需要的别上来就 kill。有时候是自己上次启动的残留进程没退干净那确实该清理有时候是另一个业务在正常使用这个端口那就得改配置换端口。权限不足的症状是日志里出现Permission denied通常发生在几个地方工作目录不可读、配置文件属主不对、要绑定的端口小于 1024Linux 上普通用户不能绑 1024 以下的端口。解决办法要么是给对应权限要么是配置里加AmbientCapabilitiesCAP_NET_BIND_SERVICE让非 root 用户也能绑低端口这比直接拿 root 跑安全得多。配置语法错误相对好办因为大多数成熟软件都提供了自检命令服务语法检查命令Nginxsudo nginx -tApachesudo apachectl configtestMySQLsudo mysqld --validate-configSSHsudo sshd -tPHP-FPMsudo php-fpm -t养成先检查再重载的习惯能省掉大量排查时间。5.4 常见问题速查表把前面这些整理成一张表出问题的时候对着查会更高效现象可能原因排查命令处理方式unrecognized service名字错/未安装/未 daemon-reloadsystemctl list-unit-files | grep 名字修正名字或执行 daemon-reload启动后立刻退出Type 选错journalctl -u 服务 -n 50simple 与 forking 互换试反复重启Restart 策略 程序崩溃systemctl status 服务先修程序本身再考虑 Restartstop 卡住不返回进程收尾慢systemctl status 服务等待或调 TimeoutStopSec端口被占残留进程或其他服务ss -tlnp | grep :端口清理或换端口Permission denied用户/目录/端口权限ls -l 相关目录调整属主或用 capability改了配置没生效忘记 reload 或 daemon-reload—补执行对应命令status 显示[ ? ]SysV 脚本无返回码systemctl is-active 服务用 systemctl 判断6. 几个我实际踩过的坑和习惯养成6.1 重启服务前先问自己三个问题第一个问题这个服务现在有没有活跃连接如果是个对外提供接口的服务restart 的那几秒里新请求会失败。能 reload 就别 restart。判断有没有连接可以用ss -tn state established | grep :端口 | wc -l数一下。第二个问题我改的东西必须重启才能生效吗很多配置项是支持热加载的先翻翻文档再动手能少一次中断就少一次。第三个问题出问题了怎么回滚改配置之前先备份一份cp nginx.conf nginx.conf.bak.20240101这行命令花不了两秒但能救命。我见过太多次改完配置重启失败、服务直接躺平然后在一堆报错里手忙脚乱地找原来那行写的是什么。另外如果是在远程 SSH 会话里操作关键服务建议先在 tmux 或 screen 里跑。万一网络抖一下断连普通会话里的命令会被中止可能停在一个不上不下的状态。tmux 里跑就安全得多重连回来接着看输出。6.2 批量操作与脚本化的小技巧需要一次处理多个服务的时候写个小循环比手动敲快得多#!/bin/bash services(nginx mysql docker redis-server) for svc in ${services[]}; do if systemctl is-active --quiet $svc; then echo $svc 运行中 else echo $svc 未运行尝试启动 sudo systemctl start $svc echo 启动成功 || echo 启动失败 fi donesystemctl is-active --quiet这个组合很实用它只返回退出码不输出内容特别适合放在条件判断里。返回值 0 表示运行中非 0 表示没运行比去 grep status 的文本输出可靠多了。同样的模式可以用来批量重启for svc in nginx php8.1-fpm; do sudo systemctl reload $svc || sudo systemctl restart $svc done先试 reload不支持再退回 restart这个写法在改完一批服务的配置后特别好用。6.3 关于 mask 与防火墙的两个提醒systemctl mask这个操作值得单独提一下因为它比disable彻底得多。disable只是取消开机自启服务仍然可以手动启动mask会在/etc/systemd/system/下建一个指向/dev/null的软链接让这个服务彻底无法被启动无论是手动还是被其他服务依赖触发sudo systemctl mask 服务名 sudo systemctl unmask 服务名这个功能在某个服务总是被别的服务意外拉起但我现在确实不想要它的场景下很有用。但它也是有副作用的mask 掉的服务如果被别的东西强依赖可能导致依赖方也起不来。用之前先想清楚。还有一个常见误区是为了让服务能被外部访问就去关掉防火墙。这种做法我强烈不建议。正确的做法是放行需要的端口规则明确、可追溯sudo ufw status sudo ufw allow 80/tcp sudo ufw allow 443/tcp只开放真正需要的端口比整片拆掉防护墙要稳妥得多。服务连不上有很多原因防火墙只是其中之一别把它当成第一嫌疑人。先用ss -tlnp确认服务确实在监听再用curl 127.0.0.1:端口从本机测一下本机通、外部不通再去查防火墙和云平台安全组。6.4 后续可以这样往下深挖如果这篇内容对你来说大部分都还新鲜那下一步最值得花时间的是把 systemd 的依赖关系搞明白。After、Requires、Wants这几个字段决定了服务之间的启动顺序和依赖强度写多服务应用的时候绕不开。特别是Wants和Requires的区别——前者是希望它在它起不来我也照跑后者是必须有它它起不来我就不跑——这个差别在排查某个服务莫名起不来时经常是根因。再往后就是资源的隔离与限制CPUQuota、MemoryMax、IOWeight这几个字段可以给服务加上资源配额防止一个服务吃光整台机器。这个在单机跑多个服务的时候尤其有用配置起来也就几行的事。