Ubuntu建议暂缓升级7.0.0-42内核!
2026-10-29 18:08:56,4 台 Ubuntu 26.04 节点跑完 apt upgrade,linux-image-generic 从 7.0.0-38 升到 7.0.0-42,然后逐台重启。
重启完,rsshub(TTRss,我的 rss阅读器。 数据库是 PostgreSQL)、lobe-chat(LobeHub, AI应用。数据库是 PostgreSQL)、semaphore(Ansible 第三方UI。数据库是 MySQL) 三个命名空间的 Pod 陆续进 CrashLoop。
应用侧报的错很有迷惑性:lobe-chat 的 casdoor (用于认证登录) 报 dial tcp 10.43.36.163:5432: connect: no route to host,semaphore 反复 Pinging database。
第一反应是网络。最终证明这个判断只对了一半。真正的问题在于:Ubuntu 内核升级后 containerd 容器内信号投递被 AppArmor stacking 拦截。
症状:几条线索互相矛盾
- 数据库容器日志几乎为空,没有任何启动失败的栈。
- 没配 readiness probe 的库仍然显示
Running 1/1。 - semaphore 和 semaphore-mysql 走 hostNetwork,进程直接出现在宿主机
ps里。
第三条最容易带偏人。进程在宿主机可见,看着像宿主机层面的事。
真正的信号是 no route to host 出现在集群内部的 ClusterIP 上。
Ceph 全程 HEALTH_OK(存储正常),Service 的 EndpointSlice 也正常。
库没 Ready,Service 就没有 endpoint,应用连不上只是下游症状。
第一层:进程表里的 100% CPU
宿主机上 gosu mysql、gosu postgres 各占约 100% CPU,但 mysqld、postgres 一个都没有起来。
gosu 是官方镜像 entrypoint 里降权用的。它卡住,意味着容器根本没 exec 出真正的数据库进程。前面说的数据库日志为空也就解释得通了。
第二层:perf 和 strace 指向信号
perf 抓到的调用栈:
main.SetupUser
-> syscall.Setgid
-> syscall.AllThreadsSyscall
-> runtime.osyield
strace 分析发现,只有无穷无尽的 sched_yield()。也就是说gosu 在等一个永远等不到的结果。
Go 的 AllThreadsSyscall 干的事是向进程内所有线程广播 SIGSETXID,靠这个信号让每个线程同步完成 setgid/setuid。信号投递失败,等待循环就永远不退出。
到这一步,怀疑对象从网络换成了「信号能不能投递」。
第三层:bpftrace 抓到 EPERM
一条 kretprobe 输出如下:
bpftrace -e 'kretprobe:apparmor_task_kill { printf("%s %d\n", comm, retval); }'
gosu 的返回值是 -13,也就是 EPERM,而且没有任何 signal_generate 事件。
容器内直接验证:kill -TERM 兄弟进程返回 Permission denied,目标进程还活着。
证明是 AppArmor 在里面拦了信号。
根因:stacking 之后标签对不上
任务实际的 AppArmor 标签长这样:
cri-containerd.apparmor.d//&unconfined
//& 是 stacking 的写法,表示在 CRI profile 之上再叠一层 unconfined。
containerd 的 CRI profile 里有 signal ... peer=cri-containerd.apparmor.d 这类规则,靠标签精确匹配来放行容器内的信号投递。结果标签变成 stacking 形态后,匹配不上。
内核 7.0.0-42 配合默认的 sysctl 参数: kernel.apparmor_restrict_unprivileged_unconfined=1,容器 profile 的转换方式被改成了 stacking。结果就是容器内任意信号投递返回 EPERM。
📝Notes: 受害的不止 gosu。glibc 多线程 setuid 的广播、postgres 自己的 backend 信号,全都走同一条路。这也解释了为什么是「数据库容器集体起不来」,而不是某一个镜像的问题。
对照实验:把存储、CNI、能力逐个排除
同一节点、同一 mysql:8 镜像,只改容器配置:
| 配置 | gosu 表现 |
|---|---|
privileged: true |
正常 |
appArmorProfile: Unconfined |
正常 |
capabilities.add: [ALL] |
正常 |
capabilities.add: [CHOWN] |
仍然自旋 |
kernel.apparmor_restrict_unprivileged_unconfined=0 |
正常 |
前三行是降级手段,只用于取证。第四行说明这跟 capability 没关系。
修复后 ClusterIP 和 PodIP 都连通。
修复:一行 sysctl,但要落到文件里
临时止血:
sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=0
在 sysctl loop 里加上这一行,落到:
/etc/sysctl.d/90-homelab-prerequisites.conf
文件名里的 90- 是故意的。Ubuntu 自己的 /usr/lib/sysctl.d/10-apparmor.conf 排序在前,90- 排在后面才能覆盖。
为什么必须重建 Pod
AppArmor 标签在容器创建的那一刻就定死了,改 sysctl 不会让已运行的容器自愈。
本次重建了 lobe-chat/{postgresql,casdoor,lobe}、semaphore/{semaphore-mysql,semaphore}、rsshub/database-postgres,其余 Pod 留给自然滚动。
另外几条路为什么不走
- 内核回退到
7.0.0-38。/boot里旧内核还在,能保留 Ubuntu 默认加固。代价是 4 台依次重启,还要 apt pin 防止再升上来。 - 逐 Pod 用 Unconfined / privileged /
capabilities: ALL。只治标,还把隔离削掉一层。 - 改 containerd CRI profile,给 stacking peer 补 signal 规则。profile 由 containerd 内置加载,k3s 没有覆盖入口,此路不通。
- 直接关 AppArmor。影响面太大,拒绝。
验证清单
- 4 台节点该 sysctl 均为 0。
- 三个命名空间无 CrashLoop、无非 Running Pod。
- 两个
pg_isready与一个mysqladmin ping通过。 - lobe / casdoor / semaphore HTTP 均返回 200。
- 容器内
kill -TERM返回kill_rc=0,目标进程 TERMINATED。
风险与遗留
关掉这个限制等于回退了一项 Ubuntu 加固。集群里的容器不使用无特权 userns,暴露面有限,可以接受一段时间。内核修好之后要重新评估。
7.0.0-42 的 stacking 行为和 containerd CRI profile 的 signal 规则不兼容,目前 Ubuntu 和 containerd 已经收到了这个反馈,大概率是 containerd 会修复这个问题, 后续我会每个月查看 containerd 和 k3s 的 changelog。相关 bug 记录见:
- apparmor: allow signals and ptrace for stacked profile peers | containerd
- Regression 7.0.0-42/-43: Go 1.24 programs livelock in setuid (gosu); official DB Docker images hang on start | Ubuntu
总结
Ubuntu 内核升级后容器内信号投递被 AppArmor stacking 拦截,gosu 自旋、数据库日志为空、应用报 no route to host。本次记录了从进程表、perf、strace 到 bpftrace 的定位过程,以及 sysctl 落盘与 Pod 重建的修复方案。并持续关注上游修复。
📚️参考文档
- AppArmor Wiki
- containerd CRI 配置文档
- sysctl.d man page
- 本次故障的现场取证记录(perf / strace / bpftrace 输出、对照实验表格)
浙公网安备 33010602011771号