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

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

重启之后什么都没起来

在 TT Lab 中继续学习

本实验在真正的 VM 中运行

一台 Ubuntu 24.04 VM。systemd 是 PID 1,没有 Kubernetes。准备阶段会部署一个小型订单查询应用(/opt/orders-svc/app.py),并重现前任负责人在 shell 里用 nohup 启动的状态。评分由 VM 内的代理执行,所以不要重启——评分会中断。开机时是否启动,通过 enable 状态和 target 依赖来判定。

目标

在客户 VM 上,把应用安装为带有专用用户、环境文件、重新启动策略和开机 target 的 systemd 服务,亲自确认漏掉 enable、kill -9、漏掉 daemon-reload、更换环境文件各自会改变什么,最后用交接用的检查脚本收尾。

为什么重要

“装好了,运行正常”只表示现在正在运行。在 shell 里启动的进程是服务管理器不知道的进程,所以死了没人让它复活,重启后也没人把它启动。只做了 systemctl start 而漏掉 enable 也是一样。开机时的启动取决于 [Install] 一节在 enable 时创建的一个符号链接;修改了单元文件后忘记 daemon-reload,systemd 会一直按旧配置运行。只有亲手把这些差异各制造一遍,才能在客户现场 5 分钟内回答“为什么什么都没起来”。

预计 60 分钟。准备需要几分钟。会话结束后 VM 会消失,想保留的文件要在结束前另外保存。

步骤

  1. 找出占用 8181 端口的前任负责人的进程,并在 /root/svc/legacy.txt 中用五行写下 pid=、user=、cgroup=(/proc/<pid>/cgroup 的路径)、unit=(该路径的最后一段)、survives_reboot=(yes 或 no)。
  2. 创建无法登录的系统账户 orders,以及 /var/lib/orders(所有者 orders,750)和 /etc/orders/orders.env(root:orders,640)。环境文件中写入 ORDERS_PORT=8181、16 个字符以上的新 ORDERS_TOKEN 和 ORDERS_DATA_DIR=/var/lib/orders。
  3. 编写 /etc/systemd/system/orders.service(User=orders、EnvironmentFile、ExecStart=/usr/bin/python3 /opt/orders-svc/app.py、Restart=on-failure、Wants 和 After=network-online.target、WantedBy=multi-user.target)。daemon-reload 后启动会失败。在 journal 中找到应用留下的原因行(bind failed pid=…),原样抄写到 /root/svc/why-failed.txt。
  4. 停掉前任负责人的进程并启动 orders.service。curl http://127.0.0.1:8181/healthz 必须以 orders 用户和服务主进程的 PID 响应。
  5. 对服务执行 enable,并把当时的输出(包括所创建的符号链接路径)保存到 /root/svc/enable.txt。
  6. 用 kill -9 杀掉主进程,看 systemd 是否让它复活。在 /root/svc/kill.txt 中写下 old_pid= 和 new_pid=。
  7. 给单元加上沙箱化(NoNewPrivileges=yes、ProtectSystem=strict、ProtectHome=yes、PrivateTmp=yes、ReadWritePaths=/var/lib/orders)。修改之前先测好 systemd-analyze security 的分数,修改文件之后,把 systemctl 给出的 daemon-reload 警告保存到 /root/svc/reload.txt,然后 daemon-reload、重新启动并再次测量。在 /root/svc/security.txt 中写下 before= 和 after=。
  8. 把环境文件中的 ORDERS_TOKEN 换成新值并使服务生效。在 /root/svc/rotate.txt 中写下 old_token_sha=、new_token_sha=(令牌的 sha256 十六进制)和 daemon_reload_needed=(yes 或 no)。
  9. 编写交接用的检查脚本 /root/svc/verify.sh。bash verify.sh <유닛>(占位符为单元名)在该单元具备重启后也会启动的条件(已加载、enabled、WantedBy 中有 multi-user.target、Restart 为 on-failure 或 always、Wants 和 After 中有 network-online.target、User 不为空且不是 root、NeedDaemonReload=no)全部满足时返回 0,只要有一项不满足,就输出不满足的项目并以非 0 值结束。评分器还会对准备好的其他单元(orders-audit、orders-report、orders-worker、orders-sync、orders-rootjob)运行它。

参考

占着 8181 的游离进程

把监听 8181 的进程的 pid、user、cgroup、unit,以及重启后是否存活,写入 /root/svc/legacy.txt。

ss 的 -p 会显示持有套接字的进程。/proc//cgroup 的路径会告诉你这个进程是在哪个单元里诞生的。请想一想,那个单元是不是负责启动这个应用的单元。

专用账户与环境文件

创建系统账户 orders、/var/lib/orders(orders,750)、/etc/orders/orders.env(root:orders,640)。

useradd 的 --system 使用系统 UID 段,--shell 指定禁止登录的 shell。环境文件里有令牌,所以要调整组和权限,让只有该服务的专用账户能读。wiki 上的旧令牌已经是泄露出去的值。

在 journal 中找首次启动失败的原因

编写 /etc/systemd/system/orders.service 并尝试启动,然后把 journal 中的失败原因行抄写到 /root/svc/why-failed.txt。

应用的标准输出会进入 journal。journalctl -u 유닛 -o cat(占位符为单元名)只显示消息,不带前缀。即使 systemctl start 以成功结束,Type=simple 也是在进程启动的那一刻就算成功,所以实际结果要在 status 和 journal 中看。

停掉游离进程,让服务立起来

停掉前任负责人的进程,让 orders.service 处于 active。healthz 必须以 orders 用户和主进程 PID 响应。

只挑出以 root 身份运行的应用来停止(不能把 orders 账户的进程一起杀掉)。如果前一步的失败反复发生而触及启动次数限制,就需要 reset-failed。

enable 创建的那一个符号链接

对 orders.service 执行 enable,并把输出(包括符号链接路径)保存到 /root/svc/enable.txt。

enable 不会启动服务。它只读取 [Install] 中的 WantedBy,在该 target 的 .wants 目录中创建符号链接,开机时由那个 target 把这个服务拉进来。消息输出在标准错误上。

kill -9 之后会复活吗

用 kill -9 杀掉主进程,把复活后的 PID 连同原来的 PID 一起,用 old_pid=、new_pid= 写入 /root/svc/kill.txt。

主进程 PID 是 systemctl show 的 MainPID。请看手册中的表,了解 Restart=on-failure 把哪些退出算作失败。等待 RestartSec 那么久之后才会出现新 PID。

沙箱化与漏掉 daemon-reload

测好分数,给单元加上沙箱化,把 daemon-reload 警告保存到 /root/svc/reload.txt 后使其生效,再用 before=、after= 把结果写入 /root/svc/security.txt。

systemd-analyze security 的最后一行是总体暴露分数(越低表示限制得越严)。ProtectSystem=strict 会让整个文件系统变成只读,所以必须另外打开应用写入的路径。请读一读修改文件之后立刻执行的 systemctl status。

令牌更换靠什么生效

把 ORDERS_TOKEN 换成新值并使服务生效,然后在 /root/svc/rotate.txt 中写下 old_token_sha=、new_token_sha=、daemon_reload_needed=。

EnvironmentFile 不是单元配置,而是在启动进程之前读取的文件。请想一想,这与修改单元文件有什么不同,以及已经在运行的进程的环境何时才会改变。哈希要对不含换行符的令牌本身来计算。

不重启就能证明开机启动的检查脚本

编写判定单元是否具备重启后也会启动的全部条件的 /root/svc/verify.sh。

不要去 grep 文件,要用 systemctl show 和 is-enabled 查看 systemd 加载的值。不存在的单元 show 也会成功,但 LoadState 是 not-found。不要在某一项上直接结束,把不满足的项目全部输出,接手的人就能一次改完。