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.service,service nginx start也会优先跑你那个脚本。这种"两套东西同名打架"的情况在迁移老项目时非常常见,症状是启动看起来成功了,但systemctl status nginx显示 inactive,两边状态对不上。
想知道自己系统上跑的到底是哪一套,一条命令就能确认:
ps -p 1 -o comm=输出systemd就是 systemd 系统,输出init就是老式 SysV。现在能见到的 Ubuntu(16.04 以后)基本都是前者。
确认某个服务走哪条路径,可以这样查:
ls -l /etc/init.d/ | grep 服务名 systemctl list-unit-files | grep 服务名哪个存在,基本就是走哪条路。两条都存在的话,按前面的规则,/etc/init.d/优先。
2.3 service 与 systemctl 的对应关系对照表
既然底层转发给了 systemctl,那把两套命令对照起来看会省很多事。下面这张表是我自己整理的常用对照,写脚本时按场景挑就行:
| 需求 | service 写法 | systemctl 写法 | 说明 |
|---|---|---|---|
| 启动 | service nginx start | systemctl start nginx | 等价 |
| 停止 | service nginx stop | systemctl stop nginx | 等价 |
| 重启 | service nginx restart | systemctl restart nginx | 等价,进程会重新起 |
| 重载 | service nginx reload | systemctl reload nginx | 不断连接,前提是服务支持 |
| 状态 | service nginx status | systemctl status nginx | systemctl 信息更全 |
| 开机自启 | 不支持 | systemctl enable nginx | service 没这个动作 |
| 取消自启 | 不支持 | systemctl disable nginx | 同上 |
| 是否自启 | 不支持 | systemctl is-enabled nginx | 同上 |
| 彻底屏蔽 | 不支持 | systemctl mask nginx | 比 disable 更狠 |
| 列出所有 | service --status-all | systemctl list-units --type=service | 后者更快更准 |
看到这里其实就有个结论了:新写的东西一律用 systemctl,service 只在维护老脚本时用。这不是喜新厌旧,而是因为 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,也就是礼貌地请它退出,给进程一个清理现场的机会。如果超时时间(默认TimeoutStopSec=90s)内还没退,就补一个SIGKILL强杀。这个设计很合理,但也会带来一个经典现象:stop 命令敲下去后卡住不返回,就是因为它在那儿等进程自己退,等满 90 秒才动手强杀。
实操建议,遇到 stop 卡住的情况,别急着 Ctrl+C 或者开新终端去 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才算过。这一步千万别省,语法错了直接 reload,Nginx 会保持旧配置不变,你以为生效了其实没有,排查半天才发现是配置文件写错了。更坑的是重启场景,语法错了 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] Description=My Demo Application Documentation=https://example.internal/myapp After=network-online.target Wants=network-online.target [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/opt/myapp Environment=PYTHONUNBUFFERED=1 Environment=APP_ENV=production ExecStart=/usr/bin/python3 /opt/myapp/app.py ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=3 TimeoutStopSec=20 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target几个字段值得单独说清楚,这些是最容易配错的地方。
Type决定了 systemd 怎么判断服务"启动完成"。选错了会直接导致启动超时或者假死:
simple:ExecStart 一条命令直接跑在前台,不 fork。绝大多数自己写的应用都是这种,包括 Node、Python、Go 编译出的二进制。forking:程序自己 fork 到后台,父进程退出。Nginx、PHP-FPM 这类传统守护进程是这个类型。用 simple 去配它们会导致 systemd 以为进程退出了,反复重启。oneshot:执行一次就结束,适合初始化脚本、挂载动作这类。notify:程序通过 sd_notify 主动通知启动完成,最精确但需要程序配合改造。
选错 Type 的典型症状是服务反复重启,journalctl里满屏的 Started/Stopped 记录。
Restart=on-failure是我最常用的策略,意思是进程以非 0 退出码结束、被信号杀死、或者超时的时候自动拉起。RestartSec=3是重启前等 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 --type=service | grep -i mysql第二,服务压根没装或者没装全。比如你只装了客户端没装服务端,自然找不到对应单元。
第三,单元文件存在但没被识别。这种情况通常是刚创建完文件没执行daemon-reload,执行一次就好。
5.2 启动失败但看不到任何报错的排查路径
service命令有个很讨厌的特性:启动失败时经常什么都不输出。这时候不要瞎猜,直接上systemctl status:
sudo 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-size=500M。
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,通常发生在几个地方:工作目录不可读、配置文件属主不对、要绑定的端口小于 1024(Linux 上普通用户不能绑 1024 以下的端口)。解决办法要么是给对应权限,要么是配置里加AmbientCapabilities=CAP_NET_BIND_SERVICE让非 root 用户也能绑低端口,这比直接拿 root 跑安全得多。
配置语法错误相对好办,因为大多数成熟软件都提供了自检命令:
| 服务 | 语法检查命令 |
|---|---|
| Nginx | sudo nginx -t |
| Apache | sudo apachectl configtest |
| MySQL | sudo mysqld --validate-config |
| SSH | sudo sshd -t |
| PHP-FPM | sudo php-fpm -t |
养成"先检查再重载"的习惯,能省掉大量排查时间。
5.4 常见问题速查表
把前面这些整理成一张表,出问题的时候对着查会更高效:
| 现象 | 可能原因 | 排查命令 | 处理方式 |
|---|---|---|---|
| unrecognized service | 名字错/未安装/未 daemon-reload | systemctl list-unit-files | grep 名字 | 修正名字或执行 daemon-reload |
| 启动后立刻退出 | Type 选错 | journalctl -u 服务 -n 50 | simple 与 forking 互换试 |
| 反复重启 | Restart 策略 + 程序崩溃 | systemctl status 服务 | 先修程序本身,再考虑 Restart |
| stop 卡住不返回 | 进程收尾慢 | 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=这几个字段可以给服务加上资源配额,防止一个服务吃光整台机器。这个在单机跑多个服务的时候尤其有用,配置起来也就几行的事。