architecture.md 回答「两边怎么分工、控制 API 有哪些」。 lifecycle.md 回答「实例怎么启停、闲置怎么回收」。 host-settings.md 回答「运营旋钮认哪份文件」。
本文放大其中一层:进程挂了谁来拉起来,一户把内存吃光时怎么只死这一户。稳定的目标是可恢复、租户之间不互相踩、冷启动不把机器打满。磁盘在 PVC 上,计算按需在。
| 挂掉的是谁 | 用户看见什么 | 磁盘上的 HOME |
|---|---|---|
| 某一台 dsh | 这一户的页面断,别人不受影响 | 还在 |
| host-agent | 网关和控制口一起停,所有户都打不开 | 还在 |
| 整只容器 / 节点 | 同上,直到编排把容器拉回来 | PVC 还在 |
「该不该开着」只有 host-agent 知道:用户有没有点停止、是不是闲置回收、有没有合法会话来唤醒、环境变量和端口怎么注入。systemd、supervisord、K8s 探针都不懂这些。
- 某一台 dsh 崩了:host-agent 按产品语义救。还该开着就退避重启;连崩太多次就放弃,等下次打开。
- host-agent 自己崩了:交给容器外的 Docker / Kubernetes 整容器重启。镜像里不要再套一层自动重启。
- 操作系统 / 内核:管收尸和炸裂半径。不负责「这台 dsh 现在该不该在」。
以 lifecycle.md §2 状态机为准,这里只写稳定性口径。
- 当时是
running,并且desired仍是running(没人点停止、也不是宿主自己在关进程):按min(500ms × 2^n, 30s)退避再拉起来。 - 连续崩溃超过 5 次:标
error,不再自动重启。下一次 ensure / 网关唤醒会清掉失败计数再试。 - 用户点停止、闲置回收、启动过程中就退出:不走这条自动重启。
- host-agent 进程自己起来时:扫
/data/users/*/meta.json恢复登记,不按desired全量拉起。按闲置策略在lastTrafficAt上推导谁本该还开着,网关就绪后限流拉回;其余desired=running的实例等 cookie / 打开再唤醒。避免容器一醒把所有人同时拉起来。
这些语义写在 host-agent/src/supervisor.ts。不要再请另一个进程管理器同时管同一批 dsh。
| 做法 | 谁来救 host-agent | 容器还在不在 |
|---|---|---|
| A. 镜像里套循环 / supervisord / systemd,node 挂了就在同一容器里再起 | 镜像内 | 容器一直活着 |
B. ENTRYPOINT 只跑 tini -- node ...,进程退出则容器退出,由 Docker / K8s 起一只新容器 |
编排 | 旧容器死、新容器生 |
镜像里已经是 tini -- node /opt/host-agent/main.mjs。compose 用 restart: unless-stopped,Kubernetes Deployment 默认 restartPolicy: Always,chart 策略是 Recreate(单副本 + PVC)。进程一退,kubelet / Docker 会起新容器,cgroup 把旧容器里的 dsh 一起清掉。
选 B 的原因:
- 父死子必须死。 host-agent 是唯一知道端口、会话、注入环境的人。它死了,旧 dsh 继续占着端口和 HOME,新 host-agent 再 spawn,就会出现两套进程抢同一户。镜像内重启会让「容器还活着、里面的 node 换了」变成常态,孤儿问题立刻变成真的。整容器重启反而干净:旧 cgroup 清空,新进程从 PVC 登记开始,按流量再唤醒。
- 崩溃要看得见。 编排重启会计数、打事件;镜像内循环会把崩溃藏成「容器一直 Ready」。
- 不要两个管家。 K8s 已经在管这只容器。容器里再放 supervisord / systemd,存活探针和重启策略会对不上。
- 本产品接受网关短暂停。 HOME 在 PVC 上。host-agent 重启不是要零中断保活 WebSocket;本该开着的进程按闲置策略限流拉回。
本地 Docker 和集群用同一条原则:重启策略写在 compose / Helm 上,不写进镜像入口脚本。
- 进程退出:编排必救。不依赖探针。
- 就绪:chart 已有网关
GET /__healthz。没就绪就不要把流量打进来。 - 存活(可选):只判断 host-agent 死锁,不要去问某一台 dsh 在不在。闲置睡着是正常态。现在没有存活探针也可以,进程崩溃靠退出码。
容器里是完整的 Linux,该用的是隔离和收尸,不是再请一个通用 supervisor。
已经在用:
tini作 PID 1:收僵尸;主进程退出时把剩余孩子带走。- 可选 per-user uid、HOME 0700、回环 iptables(见 architecture.md §9)。
该补、且只补炸裂半径的:
- 进程组 + 父死子死。 停 / 崩 / 容器退出时,dsh 以及它拉起的工具进程一起走,不留半套。
PR_SET_PDEATHSIG防的是「node 单独死了、容器还在」这种缝;编排整容器杀掉时 cgroup 本来就会清。 - 每实例内存与进程数限额。 口径见 §6。内核先掐这一户,再走 §3 的崩溃重启。
- 启动失败也给一两次退避。 现在主要覆盖「已经 running 之后崩了」。
不要做:
- 容器里跑 systemd / supervisord 管 dsh 或 host-agent
- 每台 dsh 一只容器(要改整套密度和 PVC 模型,不进本期)
- dsh 给宿主打心跳来保活(闲置判定认网关流量,见 lifecycle.md §3.4)
同容器密度下,一户把内存吃光,内核在容器这一层杀进程。它不认得 host-agent,可能把网关带走,所有人一起打不开。dsh 里能跑任意 shell,这是真会遇到的邻居问题。
Helm 的 resources.limits(现在默认 32Gi)只包整只容器,划不开户与户。要「一户吃光只死一户」,必须在容器里面再划一刀。
做:防跑飞。全宿主一份偏松的顶,挡住失控的编译、泄漏、fork 炸弹,让 §3 的退避接手。
不做:管理页里每户不同的套餐。那是计费和公平调度,会倒逼解释「为什么我编译到一半被杀」。
没有 RSS 实测之前,默认宁可松(2–4Gi)。架构里写的活跃实例 300–500MB 是健康工作集,拿来当硬顶会误杀正常使用。落地时这两个键进 /data/settings.json,热更新规则跟现有旋钮一样,见 host-settings.md。值是正整数或字面量 "max"。
host-agent 在 spawn 前后写自己那棵 cgroup v2 子树。现代 Docker / Kubernetes 默认是 cgroup v2 + 私有 cgroup 命名空间,容器里的 /sys/fs/cgroup 就是自己的根,可以再挂孩子。
host-agent 已经是 root(setuid、可选 NET_ADMIN)。写自己这棵树不需要 privileged: true,也不要挂宿主机的 cgroup。
ulimit、RLIMIT_AS、Node --max-old-space-size 管不住子进程和原生分配,不能当限额。
cgroup v2:一个节点一旦打开控制器,自己身上就不能再挂进程,进程必须在子节点里。host-agent 起来时先改树,再 spawn。
/sys/fs/cgroup ← 容器根(cgroupns=private)
agent/ ← 先把 tini + host-agent 挪进来
dsh/<userKey>/
memory.max
memory.swap.max
memory.oom.group = 1
pids.max
cgroup.procs ← 这台 dsh 的 pid
启动:
- 读
/sys/fs/cgroup/cgroup.controllers。没有memory,或目录只读 → 打一条日志,按今天这样继续跑,host-agent 不要崩。 - 建
agent/,把 PID 1 和自己写进agent/cgroup.procs。 - 给根写
+memory +pids。 - 给
agent/写memory.min/memory.low(256–512Mi 量级),整容器挨挤时先保网关。
每次拉起一台 dsh:建 dsh/<userKey>/,写入限额,再把进程放进去。Node 的 spawn 没有 CLONE_INTO_CGROUP,进程先出生再入组会有几毫秒窗口。用一个小 wrapper 先进组再 exec dsh。停掉或删除时进程走了再 rmdir。
限额文件可以热改,正在跑的实例写新的 memory.max 立刻生效;已经用超了,内核马上杀。热改把顶改小,等于当场回收正在用的人,运营要当停机操作看。
| 键 | 合法值 | 作用 | 出厂(建议) |
|---|---|---|---|
instance_memory_max_mb |
正整数,或 "max" |
每户 memory.max(整数按 MiB) |
2048~4096 |
instance_pids_max |
正整数,或 "max" |
每户 pids.max |
512~1024 |
不限制就填 "max",和 cgroup 文件同一个词,PUT / 文件里都是 JSON 字符串。两个键相互独立,可以只放开内存、只放开进程数,或两个都写成 "max"。
0、负数、其它字符串一律 400。cgroup 里 memory.max=0 是零字节,进程一进组就会被杀,旋钮不再借用 0 表示放开。null 仍回出厂,跟现有五键一样。
落到内核:正整数换算成字节或进程数再写;"max" 原样写入。两键都是 "max" 时仍建 dsh/<userKey>/ 这棵树(以后加 CPU、读 memory.events 还用得着)。整容器 Helm limit 和 agent/ 的 memory.min 不受影响:单户写成 "max" 之后,这一户可以吃到容器还剩多少,仍可能把别人和网关挤死。
一并写:
memory.oom.group=1:超限时整棵树一起杀(dsh + 它拉起的工具),避免只干掉某个孙子、留下半残的会话。memory.swap.max=0:避免先换到磁盘再慢慢死。尖峰会更脆,换来的是可预期的死亡,不要整机交换抖动。
cpu.max 以后要再加。CPU 抢光、PVC 写满、host-agent 自己漏内存,本期都不靠这套限额。
内核一杀,进程退出,现有 onExit 退避重启照走。连续超限会进 error,等下次打开再清计数。日志必须能读出 memory.events 的 oom_kill,把「超限被杀」和「自己崩了」分开;实例状态以后最好也能区分。
pids.max 太小会表现为 fork 失败,不是干净的 OOM。dsh 会拉包管理器、编译器、并行工具,出厂用 512–1024,不要从 256 起。
| 含义 | 行不行 |
|---|---|
这一户超过自己的 memory.max |
不行。硬顶。 |
| 这一户用掉别人闲置腾出来的内存 | 行,而且应该行。闲置回收已经把进程停了,内存还给容器。上限只卡这一户最多用多少,闲置的户不占份额。 |
| 所有户的上限之和大于容器 32Gi | 行。50 × 2Gi 写在纸上可以是 100Gi。真同时跑满,先撞容器顶,host-agent 仍可能死。 |
单户不能突破自己的顶。能超卖的是整机容量:同时在跑的人少,纸面之和就可以大于容器。不要按 max_instances × 每户上限 去做预留。
保护管家靠三道:每户一个松的 memory.max;agent/ 的 memory.min / memory.low;容器 limit 仍是最后一道。活跃数 × 每户顶不要长期大于容器;闲置回收已经在压活跃数。
要在真实宿主上数「同时能跑多少台」,用仓库里的 scripts/density.py 打控制面,不要手写一次性脚本。它只动 density* 键,默认退出就清掉。
某户合法就要用 8Gi(本地编大项目),同容器模型里只有两条路:把全宿主默认顶抬高,或把这户挪出共享池(per-user Pod)。本期两条都不走,接受「单户有顶、顶以上请停」。
- 限额含 page cache,不只 RSS。读大文件、装依赖、编译都可能先撞顶。
oom.group=1会整段会话一起死,不是只杀掉那一条编译命令。- cgroup v1、只读 cgroup、控制器没委托下来的节点:检测失败就降级,整容器 32Gi 仍在。
- 初始化写错(根上还留着进程就打开控制器)会让宿主起不来。失败路径必须是降级,不能是 crashloop。
- 进程组 /
PR_SET_PDEATHSIG管停机收尸;杀内存靠 cgroup。两边都要,职责分开。
| 事件 | 谁动手 | 结果 |
|---|---|---|
| dsh 自己退出,desired=running | host-agent | 退避重启;超限则 error |
| 用户停止 / 闲置回收 | host-agent | 停进程,不重启;唤醒靠下次打开或带会话的请求 |
| host-agent 进程退出 | Docker / K8s | 新容器;不群起 dsh |
| 节点没了 | 编排 + PVC | 盘还在,有人打开再拉起 |
| 一个租户撞上自己的 memory.max | 内核 cgroup(落地后) | 只杀这一户,再走崩溃重启 |
| 许多户一起把容器 32Gi 吃满 | 内核(容器 cgroup) | 可能带走 host-agent;每户上限挡不住这一层 |
- 不把 host-agent 做成多副本高可用(单副本 + PVC + Recreate 是当前部署事实)。
- 不保证 host-agent 重启期间网关零中断。
- 进程组 / cgroup 已按本文落地:检测失败降级,不把容器改成 privileged。
- 不讨论把 dsh 拆成 per-user Pod / microVM。
- 不做每户不同的内存套餐,也不把容器改成 privileged。
- 不保证纸面超卖之后整容器永不 OOM。
- dsh 崩了谁救?host-agent 自己崩了谁救?
- 为什么镜像里不要再套一层自动重启?
- host-agent 冷启动为什么不把所有 desired=running 的实例拉起来?
- 内核在这套模型里管什么、不管什么?
- 每户内存限额谁来写、检测不到 cgroup 怎么办?
- 单户能不能突破自己的顶?所有户的顶加起来能不能超过容器?
- 不限制是填
"max"还是0?0为什么不收?