2024 年,我要给一个线上服务调一个参数——它跑在 systemd 下,我要做的事很简单:把它的 unit 文件里 ExecStart 那行的一个启动参数从 --workers=4 改成 --workers=8。我熟练地 vim 打开 /etc/systemd/system/myapp.service,改好,保存,然后 systemctl restart myapp。重启成功,没有报错。我 ps 一看进程的启动参数——还是 --workers=4。我以为是自己手抖没存上,回去看文件,文件里清清楚楚写着 --workers=8。我又 restart 了一次,ps 再看——还是 --workers=4。这下我彻底懵了:文件已经是新的了,服务也确确实实重启了,新进程怎么会用着旧参数?一个"重启过"的服务,凭什么还在按"重启前"的配置跑?我盯着这个矛盾想了很久,最后才反应过来,我一直默认了一件根本不成立的事——我以为 systemctl restart 会顺便去把那个 unit 文件重新读一遍。它不会。这件事逼着我把 systemd 加载 unit 文件的机制、daemon-reload 的作用、reload 与 restart 的区别这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个用 systemd 管理的自研服务 myapp
事故现象:
- 改了 myapp.service 里的 ExecStart 启动参数
- systemctl restart myapp —— 重启成功,无报错
- ★ ps 看进程,启动参数还是旧的,改的没生效
现场排查:
# 1. 确认文件确实改了
$ grep ExecStart /etc/systemd/system/myapp.service
ExecStart=/opt/myapp/bin/run --workers=8 # ★ 文件是新的没错
# 2. 重启服务
$ systemctl restart myapp # 成功,没报错
# 3. ★ 看进程实际的启动参数 —— 还是旧的
$ ps -ef | grep myapp
appuser 9100 1 /opt/myapp/bin/run --workers=4 # ★ 还是 4!
# 4. ★ 看 systemd 自己记的 ExecStart 是什么
$ systemctl show myapp -p ExecStart
ExecStart={ path=/opt/myapp/bin/run ; argv[]=...--workers=4 }
# ★ systemd 内存里记的,还是 --workers=4 —— 和文件对不上!
# 5. ★ status 里其实早有提示,我之前没看见
$ systemctl status myapp
Warning: The unit file, source configuration file or drop-ins
of myapp.service changed on disk. Run 'systemctl daemon-reload'
to reload units. # ★ 它在提醒我!
根因(后来想清楚的):
1. ★ systemd 不是每次操作都去读磁盘上的 unit 文件。
它启动时把所有 unit 文件【读进内存,建一份缓存】,
之后的 start/stop/restart,用的都是【内存里那份】。
2. 我改的是【磁盘上】的文件,而 systemd 内存里那份
缓存,还是旧的 —— 它根本不知道文件变了。
3. ★ systemctl restart 只是"用缓存里的定义,把进程
关掉再拉起来" —— 它【不会】顺便去重读磁盘文件。
4. 要让 systemd 重新读磁盘、刷新内存缓存,必须【显式】
执行 systemctl daemon-reload。
5. ★ 正确顺序是:改文件 -> daemon-reload -> restart。
我漏了中间这步,所以 restart 拉起来的,还是旧配置。
改了 unit 文件,必须先 daemon-reload,restart 才会用上新配置。
修复 1:systemd 不是每次都读文件——它有一份内存缓存
# === ★ 先纠正最核心的误解:systemd 不直接"看"磁盘文件 ===
# === systemd 怎么管理一个服务 ===
# 开机时,systemd 会把 /etc/systemd/、/usr/lib/systemd/ 等
# 目录下所有的 .service / .timer / .socket 文件,
# ★ 一次性【全部读进内存】,在内存里建一套完整的
# "我管着哪些服务、每个服务怎么启动" 的数据结构。
# 这份内存里的东西,就是 systemd 的【运行时缓存】。
# === ★ 关键:之后的操作,用的都是内存缓存 ===
# 你执行 systemctl start / stop / restart / status 时,
# systemd 查的、用的,是【内存里那份缓存】——
# 它【不会】每次都跑去磁盘上重读一遍 .service 文件。
# 原因也合理:磁盘 IO 慢,服务又多,每次操作都重读
# 太低效;缓存在内存里,操作才快。
# === ★ 于是"改了文件却没生效"就说得通了 ===
# 我用 vim 改的,是【磁盘上】的 myapp.service 文件。
# 可 systemd 内存里那份缓存,还停在【开机时读到的旧版本】。
# 我没做任何事去通知 systemd "文件变了" ——
# 它当然不知道,继续拿着旧缓存给我 restart。
# === 验证:磁盘文件 vs systemd 内存里的认知,对不上 ===
$ grep ExecStart /etc/systemd/system/myapp.service # 磁盘上的
ExecStart=/opt/myapp/bin/run --workers=8
$ systemctl show myapp -p ExecStart # ★ systemd 内存里的
ExecStart={ ... argv[]=.../run --workers=4 }
# ★ 两者不一致 —— 这就是"配置改了不生效"的铁证。
# systemctl show 看到的,才是 systemd 真正在用的那份。
# === ★ systemd 其实会提醒你,只是提示容易被忽略 ===
$ systemctl status myapp
# Warning: The unit file ... changed on disk.
# Run 'systemctl daemon-reload' to reload units.
# ★ 它明确告诉你:磁盘上的文件变了,请 daemon-reload。
# 排查这类问题,第一眼就该看 status 有没有这条 Warning。
修复 2:daemon-reload 到底做了什么
# === ★ daemon-reload:让 systemd 重新读磁盘、刷新内存缓存 ===
# === daemon-reload 干的事 ===
$ systemctl daemon-reload
# 它做的是:★ 让 systemd 把磁盘上所有的 unit 文件
# 【重新扫描、重新读取一遍】,用读到的新内容,
# ★ 把内存里那份缓存【刷新】掉。
# 一句话:daemon-reload = "systemd 你重新读一遍配置文件"。
# === ★ 关键澄清:daemon-reload【不重启任何服务】 ===
# 这是最容易误解的一点 ——
# daemon-reload 只是刷新 systemd【自己的内存缓存】,
# 它【不会】去动你那些正在跑的服务进程。
# ★ 跑着的服务,daemon-reload 之后还是原样跑着,
# 一秒都不会中断 —— 它只更新"定义",不碰"进程"。
# === ★ 所以新配置要"用上",还得再走一步 ===
# daemon-reload 之后,systemd 内存里的定义是新的了,
# 但你那个服务进程,还是【按旧定义启动的老进程】。
# 要让进程真正按新定义跑,得让进程重新来一遍:
$ systemctl daemon-reload # ① 先刷新 systemd 的缓存
$ systemctl restart myapp # ② 再重启服务,这次用上新定义
# ★ 顺序不能反:先 reload 后 restart,restart 才吃得到新配置。
# === 验证 daemon-reload 生效了 ===
$ systemctl daemon-reload
$ systemctl show myapp -p ExecStart
ExecStart={ ... argv[]=.../run --workers=8 } # ★ 现在对上了
# ★ daemon-reload 后,systemd 内存里的定义和磁盘文件一致了。
# 这时再 restart,拉起来的进程才是 --workers=8。
# === ★ daemon-reload 是全局的,不针对单个服务 ===
# 注意 daemon-reload 后面【不跟服务名】——
# 它一次性重读【所有】 unit 文件。
# 所以你只改了一个服务,也是执行 systemctl daemon-reload,
# 没有 "daemon-reload myapp" 这种写法。
修复 3:改 unit 文件的正确流程
# === ★ 把"改 systemd 配置"的标准流程固定下来 ===
# === 第一步:改文件 ===
$ vim /etc/systemd/system/myapp.service
# 改 ExecStart、Environment、MemoryMax、Restart 等任何字段。
# === ★ 第二步:daemon-reload —— 这步最容易漏 ===
$ systemctl daemon-reload
# 让 systemd 重读磁盘,把内存缓存刷成新的。
# === 第三步:让服务用上新配置 ===
$ systemctl restart myapp
# 重启服务,这次拉起的进程,才是按新定义跑的。
# === ★ 一个好习惯:改完先 verify,再 restart ===
$ systemd-analyze verify /etc/systemd/system/myapp.service
# verify 会检查 unit 文件语法对不对、有没有写错的字段。
# ★ 先 verify 一遍,避免改出语法错误、restart 时才发现服务起不来。
# === ★ 改完一定要回看:systemd 认的和你写的一致吗 ===
$ systemctl show myapp -p ExecStart -p MemoryMax -p Environment
# show 列出的,是 systemd 内存里【真正在用】的值。
# ★ 它和你文件里写的对得上,才算真生效;对不上 = 还没 reload。
# === 如果改的是 enable/开机自启相关,也要留意 ===
$ systemctl is-enabled myapp # 看是否开机自启
$ systemctl enable myapp # 改 [Install] 段后,重新 enable
# ★ [Install] 段(WantedBy 等)的改动,光 daemon-reload 不够,
# 要重新 enable 一次,开机自启的软链接才会按新的来。
# === ★ 把流程记成一句口诀 ===
# 改文件 -> daemon-reload -> restart。
# 三步,缺中间任何一步,新配置都不会真正生效。
修复 4:reload / restart / daemon-reload 别搞混
# === ★ 三个长得像的词,作用完全不同,必须分清 ===
# === systemctl restart 服务名 ===
# 作用:把服务进程【停掉,再重新启动】。
# 用的定义:★ systemd 内存缓存里【当前那份】定义。
# 副作用:服务会【中断一下】(老进程退、新进程起)。
$ systemctl restart myapp
# === ★ systemctl reload 服务名 ===
# 作用:让【服务自己】重新加载它自己的配置,
# 服务进程【不重启、不中断】。
# 注意:这里 reload 的是【服务自己的业务配置】
# (比如 nginx 的 nginx.conf),★ 不是 systemd 的 unit 文件!
$ systemctl reload nginx # nginx 不断连接,重读 nginx.conf
# ★ 前提:这个服务在 unit 文件里写了 ExecReload= 才支持 reload。
# 没写的服务,reload 会报错或退化成 restart。
# === systemctl daemon-reload(后面不跟服务名)===
# 作用:让【systemd 本身】重读所有磁盘上的 unit 文件,
# 刷新 systemd 的内存缓存。★ 不重启任何服务。
$ systemctl daemon-reload
# === ★ 用一张对照表彻底记住 ===
# daemon-reload : systemd 重读【unit 文件】 -> 改了 .service 用它
# reload 服务名 : 服务重读【自己的业务配置】-> 改了 nginx.conf 用它
# restart 服务名: 服务进程停了再起 -> 想让新配置真正"跑起来"用它
# ★ 关键区分:
# - 改的是 .service 文件 -> 要 daemon-reload(+ 之后 restart)
# - 改的是服务自己的配置文件 -> 用 reload 或 restart
# - daemon-reload 和 reload,一个针对 systemd,一个针对服务
# === 一个常见误区 ===
# 有人改了 .service 文件,执行 systemctl reload myapp,
# ★ 这【没用】—— reload 是让 myapp 重读它自己的业务配置,
# 和 systemd 的 unit 文件缓存毫无关系。
# 改 .service 文件,认准 daemon-reload,别用 reload。
修复 5:drop-in 覆盖——systemctl edit 比直接改原文件稳
# === ★ 改 systemd 配置,有比"直接 vim 原文件"更稳的办法 ===
# === 直接改原文件的隐患 ===
# 很多服务的 unit 文件在 /usr/lib/systemd/system/ 下,
# ★ 这是【软件包自带】的文件 —— 你直接改它,
# 将来 yum update 升级这个包时,你的改动【可能被覆盖掉】。
# 改 /etc/systemd/system/ 下自己建的文件没这问题,
# 但"在原文件里翻找某一行去改"本身也容易改错、漏改。
# === ★ 更稳的做法:drop-in 覆盖文件 ===
# systemd 允许你【不动原文件】,另外放一个小片段,
# 只写你想【覆盖/追加】的那几行,systemd 会把它
# 叠加到原定义之上。这个小片段就叫 drop-in。
# === systemctl edit:自动帮你建 drop-in ===
$ systemctl edit myapp
# 它会打开一个空编辑器,你只写要改的部分,比如:
# [Service]
# MemoryMax=2G
# ★ 保存后,systemd 在
# /etc/systemd/system/myapp.service.d/override.conf
# 建好这个 drop-in 文件,并【自动帮你 daemon-reload】。
# 你只要再 restart 一下就行。
# === ★ systemctl edit 的两个好处 ===
# 1. 不碰原文件 -> 软件包升级不会覆盖你的改动。
# 2. 它自动 daemon-reload -> 不会忘了那关键一步。
# === 改完整个文件用 --full ===
$ systemctl edit --full myapp
# --full 会把原 unit 文件完整内容拷给你编辑(存到 /etc 下),
# 适合需要大改的场景;同样会自动 daemon-reload。
# === ★ 看一个服务最终生效的配置 = 原文件 + 所有 drop-in ===
$ systemctl cat myapp
# /etc/systemd/system/myapp.service <- 原文件
# /etc/systemd/system/myapp.service.d/override.conf <- drop-in
# ★ systemctl cat 把原文件和所有 drop-in 按顺序拼给你看,
# 这才是 systemd 实际加载的"完整配置"。排查必看。
# === 看一个服务有没有被 drop-in 覆盖 ===
$ systemctl status myapp | grep Drop-In -A2
# Drop-In: /etc/systemd/system/myapp.service.d
# └─override.conf
# ★ 有 Drop-In 这行,说明原文件之外还有覆盖片段在起作用。
修复 6:systemd 配置排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ systemd 用的是内存缓存,不是每次都读磁盘文件 ===
# 改了磁盘上的 unit 文件,systemd 默认并不知道。
# === 2. ★ 改 .service 文件,必须 daemon-reload ===
$ systemctl daemon-reload
# 让 systemd 重读磁盘、刷新缓存。
# === 3. ★ daemon-reload 不重启服务,还要 restart 才生效 ===
$ systemctl daemon-reload && systemctl restart myapp
# 标准两连:先 reload 刷定义,再 restart 让进程用上。
# === 4. 改完用 systemctl show 回看,确认真生效 ===
$ systemctl show myapp -p ExecStart
# show 出来的值,才是 systemd 真正在用的。
# === 5. ★ status 的 changed on disk 警告,是第一线索 ===
$ systemctl status myapp | grep -i 'changed on disk'
# 看到它,就说明磁盘文件改了但还没 daemon-reload。
# === 6. ★ 改配置优先用 systemctl edit(drop-in)===
# 不碰原文件、自动 daemon-reload,还能扛软件包升级。
# === 7. 分清 daemon-reload / reload / restart ===
# daemon-reload 给 systemd;reload/restart 给服务。
# === 8. 排查"配置改了不生效"的命令链 ===
$ systemctl status myapp # ① 看有没有 changed on disk 警告
$ systemctl cat myapp # ② 看 systemd 加载的完整配置
$ systemctl show myapp -p 字段 # ③ 看 systemd 内存里实际的值
$ systemctl daemon-reload # ④ 刷新 systemd 缓存
$ systemctl restart myapp # ⑤ 重启,让服务用上新配置
# 按这个顺序,配置不生效的问题基本能定位。
命令速查
需求 命令
=============================================================
让 systemd 重读 unit 文件 systemctl daemon-reload
重启服务(用上新配置) systemctl restart 服务名
让服务重读自己的业务配置 systemctl reload 服务名
看 systemd 加载的完整配置 systemctl cat 服务名
看 systemd 实际在用的值 systemctl show 服务名 -p ExecStart
看服务状态/changed 警告 systemctl status 服务名
建 drop-in 覆盖片段 systemctl edit 服务名
完整编辑 unit 文件 systemctl edit --full 服务名
校验 unit 文件语法 systemd-analyze verify 文件路径
看服务是否开机自启 systemctl is-enabled 服务名
口诀:改了 .service 文件,先 daemon-reload 再 restart,缺一不可
daemon-reload 只刷 systemd 缓存不重启服务,改配置优先 systemctl edit
避坑清单
- systemd 启动时把所有 unit 文件读进内存建缓存,之后操作用的都是这份缓存
- systemctl restart 只是用内存缓存里的定义重启进程,它不会重读磁盘文件
- 改了 .service 文件必须执行 systemctl daemon-reload,systemd 才知道文件变了
- daemon-reload 只刷新 systemd 内存缓存,它不重启任何服务,跑着的服务不中断
- daemon-reload 之后还要 restart,新定义才会被进程真正用上,顺序不能反
- daemon-reload 后面不跟服务名,它一次性重读所有 unit 文件,是全局操作
- reload 服务名是让服务重读自己的业务配置,和 systemd 的 unit 文件缓存无关
- 改 .service 文件用 reload 服务名没用,认准 daemon-reload,别和 reload 搞混
- systemctl show 看到的值才是 systemd 真正在用的,改完回看它确认是否生效
- 改配置优先用 systemctl edit 建 drop-in,不碰原文件且自动 daemon-reload
总结
这次"改完 unit 文件、重启了服务,配置却纹丝不动"的事故,纠正了我一个埋得很深、却从未被我审视过的假设——我一直默认,一个程序读取它的配置文件,是一件"实时"的、"随叫随到"的事。在我的想象里,那个 myapp.service 文件,就静静地躺在磁盘上,而 systemd 像一个尽职的管家,每次我对这个服务下命令,它都会先低头去把那份文件重新看一眼,确认"主人最新的意思",然后再照办。正是基于这个想象,我的操作逻辑顺理成章:我改了文件,文件就是新的了;我 restart 了服务,systemd 自然会读到新文件、按新文件办事。这套逻辑严丝合缝,以至于当 ps 打出那个倔强的、旧的 --workers=4 时,我的第一反应不是怀疑逻辑,而是怀疑自己——是不是没存上?是不是改错文件了?我反复确认文件、反复 restart,本质上是在一个错误的前提上,一遍遍地做无用功。复盘到根上,我才看清自己错在哪:systemd 根本不是那个"每次都低头看文件"的管家。它更像一个在上岗第一天,就把所有岗位说明书一次性背进脑子里的人。开机的那一刻,它把磁盘上成百上千个 unit 文件,一次性地、全部读进了内存,在内存里建起了一份完整的"我管着哪些服务、每个服务该怎么伺候"的账本。从那以后,我每一次 start、stop、restart,它查的、用的,都是【脑子里那份账本】,而不是磁盘上的原始文件。这个设计本身毫无问题,甚至很合理——服务那么多,操作那么频繁,每次都去抠磁盘,太慢了。可它带来一个我从没意识到的后果:磁盘上的文件,和 systemd 脑子里的账本,是【两份会脱节的东西】。我用 vim 改的,自始至终,只是磁盘上那份;而 systemd 脑子里那份账本,还停留在开机那一刻的旧版本。我没有做任何一件事,去告诉 systemd"你的账本过期了"。于是它带着一份过期的账本,无比"忠诚"地,一次又一次给我重启出旧配置的进程。systemctl daemon-reload 这个命令,我过去模模糊糊地以为它是某种"重启 systemd"的危险操作,从来不敢碰、也想不起来碰。直到这次我才真正理解它朴素的本质:它不重启任何东西,它做的唯一一件事,就是让 systemd 放下脑子里那份旧账本,回到磁盘上,把所有文件重新通读一遍,把账本更新成最新的。它就是我一直缺失的、那个"通知 systemd 文件变了"的动作。这次最大的收获,是我学会了对"配置文件的生效时机"这件事,多一分警觉。一个东西被写进了文件,和这个东西"已经生效",中间往往隔着一道我看不见的工序——可能是一次缓存刷新,可能是一次重新加载,可能是一次进程重启。我之前的失败,不是因为我改错了,而是因为我以为"改"和"生效"是同一个动作、是零延迟、零中间步骤的。可在真实的系统里,它们常常是两件事,中间那道工序,需要我亲手、显式地去推动。改完文件,别急着以为大功告成,先停下来问一句:它,真的被重新读进去了吗?
—— 别看了 · 2026