TT Lab
开始
学习 学习路径 课程

FDE综合实战:仓库收到了三次相同订单

正在运行不等于开机会启动

在 TT Lab 中继续学习

一句话总结

“正在运行”和“开机时会启动”是两个不同的事实。前者表示现在有这个进程,后者表示 systemd 会把这个服务放进启动事务,因为 enable 已经创建了符号链接。客户 VM 的交接,要证明的是后者才算完成。

为什么需要它

现场最常见的安装是这样的。复制应用,在 shell 里敲 nohup python3 app.py &,用浏览器打开看看,汇报“运行正常”。几周后客户为了安全补丁重启了 VM,什么都没有启动。因为那个进程是服务管理器不知道的进程。它死了没人让它复活,开机时也没人把它启动。日志在 nohup.out 的某个地方,而那个文件也不知道被谁删掉了。

把它作为 systemd 服务部署上去,这些问题就都有了答案:谁来启动(systemd)、以哪个用户(User=)、配置从哪里来(EnvironmentFile=)、死了怎么办(Restart=)、开机时什么时候启动(WantedBy=、After=)、日志在哪里(journal)。FDE 如果要在客户 VM 上安装什么东西,这六行就是交接文档的骨架。

工作原理

单元文件和已加载的配置是两回事。管理员创建的单元放在 /etc/systemd/system/,这个路径的优先级高于发行版软件包的 /usr/lib/systemd/system/。但 systemd 并不是每次都去读文件。它按已加载的配置运行,所以如果修改了已经加载的单元的文件,就必须用 systemctl daemon-reload 让它重新读取。修改后忘记 reload,会在 systemctl show 的 NeedDaemonReload=yes 中显现出来,systemctl 也会发出警告。

Wants 和 After 不能互相替代。systemd.unit 手册明确写道,需求依赖(Wants=、Requires=)不影响启动顺序,顺序要用 After=、Before= 单独设定。所以,对于需要在网络配置完成之后才启动的服务,按照 systemd.special 手册的说法,要用 Wants= 拉入 network-online.target,同时用 After= 排在它后面。只写一个的话,要么只拉入而没有顺序,要么只有顺序而没有人拉入那个 target。

enable 不是启动,而是符号链接。[Install] 一节在平时运行中不会被解析。systemctl enable 会读取 WantedBy=,在 multi-user.target.wants/ 中创建符号链接,这个符号链接在开机时就起到从 multi-user.target 到此服务的 Wants= 依赖的作用。相反,systemctl start 只是现在启动一次,不创建符号链接。所以“正在运行,但重启后不会启动”,几乎总是漏了 enable。

Restart= 的默认值是 no。on-failure 在退出码非 0、被信号终止、超时、看门狗时重新启动。不过手册把 SIGHUP、SIGINT、SIGTERM、SIGPIPE 视为干净退出。所以用 kill -9(SIGKILL)杀掉会复活,但 systemctl stop 自不必说,普通的 kill(SIGTERM)也不会触发 on-failure。重启间隔 RestartSec= 的默认值是 100ms,启动过于频繁就会触及 StartLimitIntervalSec=、StartLimitBurst= 的限制,不再继续启动(用 reset-failed 解除)。

EnvironmentFile 是在启动进程时读取的。手册写道,这些文件在进程执行之前读取。每行是 KEY=VALUE,以 # 开头的行被忽略,文件不存在时服务启动失败(在路径前加 - 则不存在也会跳过)。所以,如果换了令牌,单元文件没变,不需要 daemon-reload,而是重新启动,新进程才能拿到新值。

沙箱化看分数。用 systemd.exec 的选项把服务限制起来。NoNewPrivileges=yes 禁止通过 execve 获得新权限,ProtectSystem=strict 把除 /dev、/proc、/sys 之外的整个文件系统以只读方式挂载(需要写入的路径用 ReadWritePaths= 打开),ProtectHome=yes 让 /home、/root、/run/user 看起来是空的,PrivateTmp=yes 提供专用的 /tmp。systemd-analyze security <유닛>(占位符为单元名)会扫描这些设置,给出 0.0 到 10.0 之间的暴露分数。越高表示限制得越少。正如手册所强调的,这个分数只看 systemd 设置的机制,所以它不是“有漏洞”的判定,而是比较的尺子。

[Unit]
Wants=network-online.target
After=network-online.target

[Service]
User=orders
EnvironmentFile=/etc/orders/orders.env
ExecStart=/usr/bin/python3 /opt/orders-svc/app.py
Restart=on-failure
ProtectSystem=strict
ReadWritePaths=/var/lib/orders

[Install]
WantedBy=multi-user.target

在现场相遇的样子

在实验 VM 上追踪前任负责人的进程(实测),/proc/<pid>/cgroup 显示为 /system.slice/cloud-final.service。它只是搭在运行准备脚本的单元里,那个单元没有责任重新启动这个应用。即使部署了新服务,第一次启动也会失败,因为游离进程仍然占着端口。systemctl start 是 Type=simple,在进程启动的那一刻就返回成功,所以原因只能在 journal 的 bind failed … Address already in use 中看到。Restart=on-failure 加上 RestartSec=1 的话,每隔 1 秒就重复同样的失败,journal 里 Failed with result 'exit-code' 一行接一行地堆积(实测)。超过 5 次,就会触及默认的启动限制(10 秒内 5 次)。相反,kill -9 在 status=9/KILL 之后 1 秒就以新的 PID 复活了,而普通的 kill(SIGTERM)以 Deactivated successfully 结束,保持 inactive(实测)。

开启沙箱化时也有典型的顺序。修改文件后,systemctl 会警告 “Warning: The unit file, source configuration file or drop-ins of orders.service changed on disk. Run 'systemctl daemon-reload' to reload units.”(实测)。reload 并重新启动之后,如果漏了 ReadWritePaths,应用就写不进数据目录而立刻死掉。本实验的服务在只给了 User=orders 的状态下暴露分数是 9.2,加上上面五个选项之后降到了 8.3(实测)。

实际工作中真正重要的事

下一项实验要做什么

在 Ubuntu VM 上找出前任负责人用 nohup 启动的应用并记录其身份,创建专用账户和环境文件后部署单元。在 journal 中找出首次启动失败的原因,停掉游离进程后让服务起来,并确认 enable 创建的符号链接。用 kill -9 看它是否复活,加入沙箱化,记录 daemon-reload 警告和暴露分数的变化,更换令牌并通过重新启动使其生效。最后,把不重启就能判定开机条件的检查脚本,对准备好的各个单元运行。

参考文档:systemd.service(5)、systemd.exec(5)、systemd.unit(5)、systemd.special(7)、systemd-analyze(1)