背景
之前在 ArgoCD 落地 GitOps 中记录过这套 K3s 环境。最近将 PVE 从 Intel N150 物理机迁移到 AMD Ryzen 5 5600H 后,集群看起来还能正常工作,但日志持续出现 TLS 错误。
节点是 Ready,大部分时间 Pod 也全部就绪,普通 API 查询正常。问题主要出现在持续运行的资源监听连接上,偶尔还影响租约更新、身份认证和 webhook 调用。
最终有效的调整是:保留原磁盘和集群数据,将这台虚拟机的 CPU 类型从 x86-64-v2-AES 改为 host。截至本次排查结束,连续 15 分钟观察未再复发,随后复核也正常。
这个结果支持故障与虚拟 CPU 特性及其影响的执行路径有关,但还没有定位到具体的 Go、内核或 KVM 缺陷。下面记录判断过程,而不是把 host 当作所有 TLS 错误的通用修复。
环境
| 项目 | 配置 |
|---|---|
| 迁移前物理 CPU | Intel N150 |
| 迁移后物理 CPU | AMD Ryzen 5 5600H |
| PVE | 8.4.1,内核 6.8.12-9-pve |
| QEMU 软件包 | pve-qemu-kvm 9.2.0-5 |
| 虚拟机 | 2 核,14 GiB 内存,Ubuntu 24.04.2 |
| guest 内核 | 6.8.0-147-generic |
| 原虚拟 CPU 类型 | x86-64-v2-AES |
| K3s | v1.33.6+k3s1,Go 1.24.9 |
| Rancher | v2.13.0,Go 1.24.10 |
| 数据存储 | SQLite 和 local-path 持久卷 |
故障现象
最常见的错误有三种:
local error: tls: bad record MAC
tls: unexpected message
http2: frame too large
11 月 7 日零点至 17:40 左右,K3s 累计记录约 1.28 万条相关日志,集中在 323 个秒级时间点。
这里不能把日志条数当作独立故障次数。HTTP/2 的一条连接承载多个资源监听,一次断连可能同时结束大量 watch,随后出现成批的 reflector 错误。
另外,bad record MAC 中的 MAC 是消息认证码,不是网卡 MAC 地址。它表示 TLS 记录校验失败,单凭这句话无法判断是证书、数据传输、端点处理还是运行环境的问题。
先拆开两个不同的问题
网络等待超时
系统启动时,systemd-networkd-wait-online 等待约两分钟后超时,系统状态为 degraded。网卡虽然已经拿到静态 IPv4 地址,却一直停留在 configuring。
检查发现,系统通过 sysctl 禁用了 IPv6,但 Netplan 没有设置 link-local,仍默认要求生成 IPv6 链路本地地址。
在原网卡配置下补充一行即可使两边配置一致:
network:
version: 2
ethernets:
ens18:
# 原有 addresses、nameservers、routes 保留
accept-ra: false
dhcp6: false
link-local: []
这是原配置的局部示意,不能用它覆盖已有的静态地址和路由。修改前备份原文件,再执行:
sudo netplan generate
sudo networkctl reload
sudo networkctl reconfigure ens18
sudo systemctl restart systemd-networkd-wait-online
networkctl status ens18
systemctl is-system-running
修复后,网卡变为 configured,系统恢复为 running,并通过了后续重启验证。Netplan 官方说明明确规定,空的 link-local 列表会关闭链路本地地址配置。
但 TLS 错误仍然复发,说明这是两个不同的问题。
历史时间同步异常
迁移后确实发生过约 8 小时的时钟回拨,需要认真核对证书有效期和时间同步状态。不过,在宿主机和虚拟机均已同步、没有新增校时记录的时段,TLS 错误仍持续发生。
因此,历史时间异常值得修复,却不能解释当前全部故障。
从 Rancher 日志扩展到整个节点
最初 Rancher 的错误最显眼,聚合 API 的证书控制器还反复打印证书更新日志,很容易怀疑证书轮换。
核对 APIService 的 CA、Secret 内的证书和后端实际返回的证书后,证书链、域名和有效期均正常。结合 Rancher 源码,watch 失败后重启证书控制器、重新加载同一张证书,也会打印更新日志,不能将其直接理解为重新签发证书。
继续扩大日志检查范围,发现错误还出现在:
- K3s 自身访问
127.0.0.1:6443的 TokenReview 身份认证接口。 - K3s 访问
127.0.0.1:6444的资源监听。 - metrics-server 的 HTTP/2 响应读取。
- cert-manager 的领导者租约更新。
- kube-state-metrics 等其他控制器的 watch。
这些证据说明故障并非只存在于 Rancher,更不能只靠 Rancher 日志数量判断责任端。
还有一个需要纠正的判断:日志中的 an error on the server 不一定是服务端主动返回的错误。Kubernetes 客户端的 StreamWatcher 会将本地解码失败包装为 Error 事件,再向上层传播。必须结合原始请求和底层错误判断,不能按字面归因。
验证网络和数据完整性
物理网卡更换后,桥接绑定和地址冲突仍需要检查:
# PVE 宿主机
qm config <VMID>
ip -br link
bridge link
bridge fdb show
# 虚拟机
ip -br address
ip route
ip neigh
本次虚拟机配置、guest 网卡、PVE 转发表,以及迁移前多个快照中的虚拟 MAC 一致,未发现其他虚拟机使用重复 MAC。物理网卡和虚拟网卡本来就有各自的 MAC,换物理机不等于虚拟 MAC 必须变化。
更关键的是,部分出错连接走本机回环,根本不经过物理网卡。这个事实降低了物理网卡绑定异常作为统一解释的可能性。
抓包也提供了更直接的证据。对一次真实 Rancher 故障连接,在 Pod 网卡与节点 veth 两侧重组双向 TCP 数据,约 164 KB 的载荷逐字节一致,没有缺口或冲突。这次样本没有显示数据在两个抓包点之间被改写,但不能据此排除所有内核和端点内存问题。
其他检查结果如下:
| 检查 | 结果 | 能说明什么 |
|---|---|---|
| SQLite 只读 quick_check | ok | 未发现本次检查覆盖的数据库结构损坏 |
| K3s 二进制 SHA-256 | 与官方发布一致 | 安装文件未发现变化 |
| 普通 TCP 回环传输 | 512 MiB 校验通过 | 本轮普通传输数据一致 |
| 独立 OpenSSL 回环传输 | AES-GCM、ChaCha20 合计 512 MiB 通过 | 不能认为所有加密运算都不正常 |
| 独立 Go HTTP/2 watch | 16 路、128 路测试均通过 | 简化客户端未复现生产故障 |
其中一次独立 Go 客户端通过测试时,真实 Rancher 连接在同一时间窗口报错。因此,“短请求正常”“独立测试通过”都不能证明间歇性故障不存在。
取证链路本身也要校验。期间一次 SSH 上传的压缩数据校验失败,另一次长测试的 SSH 回传中断,这两次都没有作为有效测试结果。后续执行诊断程序前,分别校验压缩包和可执行文件的 SHA-256;传输复测三次通过,但那次异常的原因没有确证,也不能用来证明物理 CPU 损坏。
用重启和 CPU 类型做对照
证书、数据库和网卡配置检查没有给出直接修复依据后,先尝试保留原数据的重启,再改变一个配置项。
以下时间均为 2026 年 11 月 7 日北京时间:
| 操作 | 结果 |
|---|---|
| 18:00 左右原版本滚动重启 Rancher | kube-state-metrics 在 18:00:38 仍报 TLS 错误 |
| 18:04:55 K3s 重启恢复 | 18:08:00 再次出现 TLS 错误 |
| 保持原 CPU 类型,正常关闭再启动虚拟机,18:09:55 K3s 恢复 | 本次启动日志在 18:10:03 已出现同类错误 |
| 仅将 CPU 类型改为 host,再正常启动,18:16:54 K3s 恢复 | 连续 15 分钟观察无新增错误,18:34:24 复核仍为零 |
每一步都保留原磁盘、证书和持久卷,不卸载 K3s,也不重新安装 Rancher。最后一次配置差异只有:
-cpu: x86-64-v2-AES
+cpu: host
操作步骤如下,<VMID> 需要替换为目标虚拟机编号:
qm config <VMID>
qm shutdown <VMID> --timeout 180
qm status <VMID>
# 确认 status: stopped 后再继续;正常关机超时不要直接强制断电
qm set <VMID> --cpu host
qm start <VMID>
若没有改善,可以按同样的正常关机流程,将 CPU 类型恢复为记录下来的原值。本次因为观察结果改善,保留了 host。
为什么 CPU 类型会影响 TLS
虚拟 CPU 类型决定 guest 能看到哪些指令特性,并不只是修改一个显示名称。Proxmox 文档说明,host 会将宿主机 CPU 特性暴露给虚拟机;跨不同 CPU 的在线迁移则需要考虑兼容性。
原来的 x86-64-v2-AES 配置暴露了 AES,但没有暴露 PCLMULQDQ。在 Go 1.24.10 的 TLS 实现中,amd64 的 AES-GCM 加速判断需要同时满足多个条件:
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3
改为 host 后,guest 获得了 PCLMULQDQ、AVX2 等更多特性。同一个 Go 1.24.10 诊断程序的实际协商结果随之改变:
| CPU 类型 | 诊断连接的 TLS 加密套件 |
|---|---|
| x86-64-v2-AES | TLS_CHACHA20_POLY1305_SHA256 |
| host | TLS_AES_128_GCM_SHA256 |
在 host 环境中,128 路 watch 全部正常结束,10,758 条 TLS 记录通过独立解密校验,没有异常 HTTP/2 帧或采集缺口。实际组件日志也停止出现原来的错误。
这形成了一条有实际对照支持的解释:CPU 特性变化改变了加密和相关代码的执行路径,新的路径避开了本次故障的触发条件。
但它还不能证明 ChaCha20 算法本身有问题,也不能证明某条物理 CPU 指令损坏。host 同时改变了多个 CPU 特性;原类型下的独立 ChaCha20 测试也曾通过。具体是某条优化路径、虚拟化状态处理,还是受执行时序影响的其他缺陷,仍需要更小的复现程序和更长时间的验证。
恢复验证和重建判断
修改后检查了 37 个运行中 Pod 的日志,未检出上述三类错误。持续观察覆盖 K3s、Rancher、metrics-server、kube-state-metrics 和 cert-manager,结束后再次核对:
- 所有常驻 Pod 就绪,API
/readyz返回ok。 - Rancher 聚合 API 为
Available=True。 - 4 个 PV/PVC 的 UID 和绑定关系与重启前一致。
- 10 个 Argo CD 应用均为
Synced / Healthy。 - 节点心跳和资源指标持续更新,系统状态为
running。
这些检查支持保留当前配置继续运行,没有必要因为日志中出现 TLS 错误就清空数据库、重新签发证书或重建集群。
如果最终确实需要重建,也应先保留数据库、server token 和业务卷数据。K3s 的 备份恢复文档特别要求同时保存 server token;GitOps 仓库可以重建资源定义,但不会自动恢复本地持久卷内容。
这次排查中最有价值的做法,是将每个判断绑定到可验证的证据:区分日志条数和故障次数,区分客户端包装错误和服务端原始错误,验证取证数据本身,再通过保留数据、逐项变更的对照决定下一步。最终获得了有效的运行配置,而没有用重装掩盖尚未弄清的原因。