K8s 部署 RustDesk:hbbs/hbbr 中继与 UDP 端口实践 远程协助这套东西真到了要给几十台内网机器做技术支持的时候你就会发现市面上的现成方案要么按并发数收费要么画质压缩得看不清窗口标题。RustDesk 的开源服务端给了另一条路——自己搭 hbbs 和 hbbr数据不出自己的机房。但很多人卡在第一步服务端往哪放。扔一台云主机上确实最省事可一旦你手里已经有 k8s 集群就会本能地想把这两个进程也纳管起来用统一的清单、统一的探针、统一的升级流程去管。这篇就是我在集群里做k8s 部署 rustdesk的完整记录从端口语义拆到清单编写再到实测中撞上的 UDP 丢包、PVC 抢占、RELAY 地址写错这几类问题。适合已经能看懂 Deployment 和 Service、想把这套远程桌面服务端跑在自己集群里的同学也适合跑通了但连接质量一直不对劲、怀疑是中继配置有问题的人。1. 先想清楚为什么值得把 RustDesk 服务端塞进集群1.1 一台裸机也能跑集群带来的到底是什么RustDesk 的服务端本体非常轻两个静态编译的二进制文件加起来几十兆内存就能跑理论上开一台最低配的云主机、装个 Docker、两条docker run命令就完事了。所以每次有人问我为什么要费劲上 k8s我都会先反问一句你手里是不是已经有集群了。如果答案是肯定的那把它纳管的收益主要落在三件事上。第一是配置的版本化。裸机上跑的那两条docker run时间一长就没人记得当时加了什么参数RELAY环境变量指向哪个地址-k有没有开。写成 YAML 放进仓库谁改了什么在 diff 里一目了然回滚就是一次kubectl apply。第二是生命周期的统一。集群里通常已经有现成的日志采集、指标抓取、告警通道。RustDesk 服务端自己没有暴露 Metrics 端点但你至少可以让它的容器日志进统一的日志系统让节点的网络流量进统一的监控大盘出问题时不用再单独 SSH 上去docker logs。第三是中继节点的横向扩展。hbbs 只有一个实例好说hbbr 中继是无状态的纯粹转发字节流。当并发连接数上来单台中继的出口带宽顶不住时多开几个 hbbr 副本分摊流量是很自然的做法而这正是编排系统擅长的事情。反过来说如果集群本身只有一台单点控制面、你也没有任何运维体系那老老实实开一台云主机跑 Docker 反而更省心。工具要匹配场景不要为了用而用。1.2 这两个组件的角色差异决定了部署方式完全不同很多人第一次看 RustDesk 自建文档会把 hbbs 和 hbbr 当成一对孪生兄弟用同一份清单改个名字就部署上去。这是后面一系列问题的根源。它们的职责差别非常大hbbs是信令服务器负责客户端注册 ID、维护在线状态、交换双方的网络信息、做 NAT 类型探测。它是有状态的数据落在本地一个 SQLite 文件里还持有一对 Ed25519 密钥。hbbr是中继服务器只有在两端无法直连时才介入把画面数据包原样转发。它完全无状态重启不丢任何东西也不关心谁是谁。这个差异直接推导出两条设计原则hbbs 必须单副本、必须持久化、密钥不能丢hbbr 可以多副本、可以随时重建、扩容只看带宽。后面所有的清单细节本质上都是这两条原则的展开。还有一个容易被忽略的点两个组件的端口语义不一样。hbbs 的 21116 同时监听 TCP 和 UDPUDP 那一路是 NAT 类型探测和心跳的关键hbbr 的 21117 只有 TCP。这意味着你在写 Service 的时候hbbs 那一个端口要拆成两条记录命名还得区分开否则 API Server 直接拒绝。1.3 端口的完整清单与作用在动手写 YAML 之前先把端口表列清楚这份表在后面配 Service、配安全组、排查连接失败时都会反复用到。端口协议归属组件用途是否必须21115TCPhbbsNAT 类型探测TCP 侧必须21116TCPhbbsID 注册、心跳、信令交换必须21116UDPhbbsID 注册、心跳、NAT 类型探测UDP 侧必须21117TCPhbbr中继数据转发必须21118TCPhbbsWeb 客户端信令按需21119TCPhbbrWeb 客户端中继按需最后两个端口只有在你要用浏览器直接连的时候才需要开。如果团队里都是装客户端用把 21118 和 21119 关掉能少两个攻击面。注意21116 的 UDP 那一路是整套系统里最容易被漏掉的一条。云厂商安全组、集群节点的防火墙、Service 的端口声明三个地方任意一处漏了 UDP客户端就会探测失败、判定为对称型网络、然后所有连接强制走中继。表面上能用实际上画质和延迟都会明显变差而且中继带宽消耗会成倍上升。2. 网络模型选型hostNetwork、NodePort 还是 LoadBalancer2.1 为什么普通的 ClusterIP Service 在这里不够用集群内部的 Service 是一层虚拟 IP靠 kube-proxy 做 DNAT 转发客户端拿到的是一个只有集群内部能路由的地址。RustDesk 的客户端是跑在用户桌面上、公司外网、甚至家里的根本不在集群网络里所以 ClusterIP 天然不适用。真正麻烦的地方在于hbbs 在信令交换过程中会把服务端的地址信息告诉客户端。具体来说RELAY这个环境变量的值会被原样下发给客户端客户端拿着它去连中继。如果你在这里填了集群内部的 Service 名或者 Pod IP客户端拿到之后只会超时。这个坑我在第一次部署时踩得很结实日志里看不出任何异常客户端就是连不上排查了半天才发现是地址下发的问题。所以选型的第一条硬约束是服务端对外暴露的地址必须是客户端视角下真实可达的地址而不是集群视角下的。2.2 三种暴露方式的取舍对比方案源 IP 保留UDP 支持部署复杂度适用场景hostNetwork 固定节点完整保留原生支持低单节点、自有公网 IP、追求最简链路LoadBalancer externalTrafficPolicy: Local保留支持中云上环境、有 LB 组件、需要多节点容灾NodePort会做 SNAT源 IP 丢失支持但受 conntrack 超时影响最低临时验证、内网自用先说我最终选的那个hostNetwork。理由很直接Pod 直接使用节点网络命名空间端口直接绑在节点的网卡上中间没有任何转发层UDP 包不经过 conntrack源地址天然保留。对 hbbs 这种强依赖 UDP 探测的组件来说这是链路最短的做法出问题的环节也最少。代价也很明确端口独占。节点上 21115 到 21119 这几个端口不能被别的进程占着而且 Pod 重建时如果调度到了别的节点对外地址就变了。解决办法是给目标节点打标签然后用nodeSelector把 Pod 固定住。kubectl label node node-01 rustdesk-rolesignal kubectl label node node-02 rustdesk-rolerelay加载不出来那是你的节点名和这里不一样kubectl get nodes看一眼就知道。NodePort 我不是没用过早期验证阶段就是它胜在不用改节点。但用下来有两个明显问题。一是 kube-proxy 的 iptables 模式下UDP 会话的空闲超时默认只有 120 秒客户端长时间空闲后再发心跳NAT 映射可能已经失效表现就是挂一会儿就掉线。二是源地址经过 SNAT 之后服务端看到的所有连接都来自节点 IP日志里完全没法区分是谁排查问题基本靠猜。LoadBalancer 方案在云上是更优雅的选择但必须显式设置externalTrafficPolicy: Local让流量只落到真正跑着 Pod 的节点上这样才能保留源 IP。如果设成默认的 Cluster又会绕回 SNAT 的老路。2.3 用域名而不是 IP 来解耦节点漂移即使固定了节点机器总有维护、扩容、迁移的时候。所以我在RELAY里填的从来都是域名不是 IP。做法是给 hbbs 和 hbbr 各配一条 A 记录TTL 设短一点比如 60 秒。节点换了改 DNS 就行客户端那边完全无感——它每次连接都会重新解析。这个小技巧帮我省掉了好几次大规模通知用户改配置的麻烦。再配合nodeSelector限定一组候选节点而不是绑定死某一个Pod 重建时会在这组节点里挑一个域名也跟着指向新地址整体上就是一个低成本的高可用。3. 镜像、持久化与密钥地基必须先铺平3.1 镜像标签千万别用 latest官方镜像rustdesk/rustdesk-server同时包含 hbbs 和 hbbr 两个二进制通过command指定跑哪个。这一点很友好不用维护两份镜像。但标签必须钉死版本号。我见过不止一次有人用了 latest某天节点重启拉了个新镜像hbbs 起来之后直接读不了旧的 SQLite 库报 schema 不匹配然后所有已注册的 ID 全部要重新配。SQLite 数据库是有版本演进的跨大版本回退非常麻烦。image: rustdesk/rustdesk-server:1.1.11升级策略应该是先备份/root目录再改 tag灰度观察一两天没问题再清理旧备份。别指望编排系统的自动回滚它能帮你把 Pod 拉起来但救不了已经被新版本写坏的数据文件。3.2 为什么必须挂持久卷hbbs 首次启动时会在工作目录默认/root下生成两个关键文件id_ed25519服务端私钥id_ed25519.pub对应的公钥也就是客户端要填的那个 Keydb_v2.sqlite3ID 注册表记录哪些设备 ID 属于哪个公钥前两个是密钥对第三个是数据。任何一个丢了都有麻烦密钥丢了所有客户端的 Key 都要重新下发数据库丢了已注册的设备 ID 全部失效用户会发现自己的固定 ID 变了。所以 PVC 是必须的而且必须在第一次启动前就挂好。如果先跑起来生成了密钥、再补挂 PVC那密钥就落在容器可写层里Pod 一重建就没了。apiVersion: v1 kind: PersistentVolumeClaim metadata: name: rustdesk-data namespace: rustdesk spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi容量给 1Gi 足够SQLite 存的是设备 ID 和公钥指纹几百台设备也就几兆。但别给太小比如 100Mi某些存储插件对最小卷有要求。3.3 RWO 卷 滚动更新 必然卡住这是我在集群里踩得最典型的坑值得单独说。Deployment 默认的更新策略是 RollingUpdate它会先起一个新 Pod、等它就绪、再杀掉旧的。但 PVC 的访问模式是 RWO单节点读写同一时刻只能被一个节点挂载。新 Pod 调度到另一个节点时挂载会失败报Multi-Attach error for volume然后整个发布就卡在 ContainerCreating 上旧的 Pod 还活着服务没断但新版本永远起不来。第一次遇到时我以为是存储插件的问题查了半天 CSI 日志才发现是自己没改策略。spec: replicas: 1 strategy: type: RecreateRecreate的语义是先把旧 Pod 全部干掉再起新的。对单副本 RWO 的组合来说这是唯一正确的选择。代价是发布期间有几十秒的服务中断但 hbbs 短暂不可用只影响新的注册请求已经建立的中继连接不受影响实际感知很小。3.4 公钥怎么取出来部署完成之后第一件事是把公钥抄下来客户端要用。kubectl exec -n rustdesk deploy/hbbs -- cat /root/id_ed25519.pub输出的是一串 Base64 字符形如xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。这就是客户端的 Key 字段要填的内容。顺便建议把这串东西存进你们内部的密码管理系统。它本身不是秘密公钥嘛但它是所有客户端配置的一致性来源丢了就得重建密钥对。提示如果你希望密钥完全可控也可以先在本地跑一次容器生成密钥对把id_ed25519和id_ed25519.pub放进 Secret再挂载到/root。好处是密钥纳入了 Secret 管理坏处是权限处理稍麻烦Secret 默认以只读方式挂载而 hbbs 可能需要写目录。我的做法是直接用 PVC密钥跟着数据卷走备份的时候一起打包。4. hbbs 的清单落地UDP 那一路是重点4.1 完整 Deployment 清单先看代码再逐条解释。apiVersion: apps/v1 kind: Deployment metadata: name: hbbs namespace: rustdesk labels: app: hbbs spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: hbbs template: metadata: labels: app: hbbs spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet nodeSelector: rustdesk-role: signal containers: - name: hbbs image: rustdesk/rustdesk-server:1.1.11 command: [hbbs] args: [-k, _] env: - name: RELAY value: relay.example.com:21117 ports: - name: signal-tcp containerPort: 21116 protocol: TCP - name: signal-udp containerPort: 21116 protocol: UDP - name: nat-test containerPort: 21115 protocol: TCP volumeMounts: - name: data mountPath: /root readinessProbe: tcpSocket: port: 21116 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: tcpSocket: port: 21116 initialDelaySeconds: 30 periodSeconds: 20 volumes: - name: data persistentVolumeClaim: claimName: rustdesk-data几个关键点逐一说。hostNetwork: true之后容器的端口直接监听在节点上containerPort的声明其实只是文档性质k8s 不会因为它去做什么。但还是建议写上一是可读性二是一些策略工具会读这个字段。dnsPolicy: ClusterFirstWithHostNet是必须的。用了 hostNetwork 之后Pod 默认继承节点的 DNS 配置集群内部的 Service 域名就解析不了了。虽然 hbbs 本身不需要访问集群内部服务但如果你的镜像仓库是集群内的私有 registry加上这一行能省掉拉取失败的问题。args: [-k, _]这个参数的意思是强制要求客户端携带正确密钥不带 Key 的连接一律拒绝。下划线的含义是使用默认路径下的密钥文件。不加这个参数的话任何知道地址的人都能把你的服务器当免费中继用非常危险。RELAY填的是中继服务器的对外地址不是集群内部的 Service 名。这一条前面提过是最高频的错误。4.2 探针怎么设才不误报RustDesk 服务端没有 HTTP 健康端点所以只能用 TCP 探针。这里有个细节探针连 21116 的 TCP这个端口是监听状态就返回成功它不验证业务逻辑是否真的可用。也就是说探针能覆盖的只是进程还活着、端口还在听覆盖不了数据库损坏这类问题。initialDelaySeconds别设太小。hbbs 启动时要打开 SQLite、读密钥首次启动还要建表SSD 上大概一两秒机械盘或者网络存储上可能到十几秒。设 5 秒比较稳妥。liveness 的initialDelaySeconds我给到 30 秒就是为了避免启动慢导致被 kubelet 反复杀。值得强调的是liveness 探针配得激进反而会制造故障。如果 hbbs 因为存储后端抖动卡了几秒探针连续三次失败就会重启容器然后又要重新挂卷、重新初始化雪上加霜。宁可比业务实际需要的宽容一些。4.3 Service 要不要建用了 hostNetwork节点上已经直接监听端口了Service 不是必须的。但我还是建了一个主要目的是两件事一是内网的其他服务可以用一个固定域名访问它二是给未来的迁移留个抽象层。apiVersion: v1 kind: Service metadata: name: hbbs namespace: rustdesk spec: type: ClusterIP selector: app: hbbs ports: - name: signal-tcp port: 21116 targetPort: 21116 protocol: TCP - name: signal-udp port: 21116 targetPort: 21116 protocol: UDP - name: nat-test port: 21115 targetPort: 21115 protocol: TCP注意这里的命名。signal-tcp和signal-udp必须是两个不同的名字因为同一个 Service 里端口名必须唯一即使协议不同也不行。这一点如果漏了kubectl apply会直接报duplicate port name错误信息还算清晰不至于卡太久。5. hbbr 中继部署带宽账要先算清楚5.1 中继吃的是出口带宽不是 CPUhbbr 这个进程很有意思它几乎不消耗 CPU——就是内存拷贝加 socket 转发。真正的瓶颈是节点的出口带宽。算一笔账一次中继连接如果客户端要的是 1080p 画质码率大概在 2 到 5 Mbps 之间取中间值 3 Mbps。一条中继连接意味着数据要从 A 到服务器、再从服务器到 B所以出口带宽消耗是双向的实际占用约 6 Mbps。一台 100 Mbps 出口的节点理论上只能承载 15 到 16 条并发中继连接。这个数字很吓人所以第一件事是尽量让客户端直连中继只作为兜底。RustDesk 默认就是这个策略能打洞直连就走直连。但如果你的网络环境里大量客户端处在对称型 NAT 后面直连成功率会很低中继压力会远超预期。我当时是按并发中继连接数 设备总数 × 0.3来估算带宽的实测下来这个系数偏保守一点但好用。5.2 hbbr 的清单与多副本考虑apiVersion: apps/v1 kind: Deployment metadata: name: hbbr namespace: rustdesk spec: replicas: 1 selector: matchLabels: app: hbbr template: metadata: labels: app: hbbr spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet nodeSelector: rustdesk-role: relay containers: - name: hbbr image: rustdesk/rustdesk-server:1.1.11 command: [hbbr] ports: - name: relay containerPort: 21117 protocol: TCP readinessProbe: tcpSocket: port: 21117 initialDelaySeconds: 3 periodSeconds: 10hbbr 不需要挂载任何卷重启零成本这是它和 hbbs 最大的区别。也正因为如此它的副本数调整可以很随意。不过要注意一个约束在 hostNetwork 模式下同一个节点上的端口不能复用所以多副本必须落在不同节点上。如果你想让 replicas 大于 1得给足够多的节点打上rustdesk-role: relay标签并且配置反亲和让它们分散开。affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: hbbr topologyKey: kubernetes.io/hostname这里有个现实问题RELAY环境变量在 hbbs 里只能填一个地址。所以多副本 hbbr 的时候要么用域名做负载均衡DNS 轮询但客户端不会自动重试另一个要么用四层负载均衡器在前面挡一层。我的实践是hbbr 单副本 大带宽节点比多副本 DNS 轮询更省心。真到了单节点扛不住的时候再考虑在前面挂一个四层 LB把 21117 端口做转发。这个演进路线比较平滑不用一上来就搞复杂。5.3 一个降低中继压力的配置开关ALWAYS_USE_RELAY这个环境变量默认是关的意思是能直连就直连。千万别手贱把它打开设为Y除非你在做故障排查。打开了之后所有流量都走中继带宽账单会立刻教你做人。反过来如果你确实遇到了直连质量很差、频繁卡顿的情况可以临时打开它做对照实验如果强制中继反而更流畅说明打洞虽然成功但链路质量不行比如走了低质量的 P2P 路径这时候可以考虑长期开启中继用带宽换体验。同类还有一个ENCRYPTED_ONLY1的环境变量作用等价于-k _参数强制加密。两个都写也不会冲突但选一个就行我用的是命令行参数因为它在kubectl describe里更显眼。6. 客户端接入Key 填错的表现和排查6.1 三处配置必须完全一致客户端的网络设置里有三个字段需要注意字段填什么常见错误ID 服务器hbbs 的对外域名如signal.example.com填了 IP 且节点换过或填了内网地址中继服务器留空由服务端下发手填了一个错误地址覆盖了服务端的值Keyid_ed25519.pub的完整内容只填了前半段或复制时多了空格第二个字段很多人会手填这是错的。留空的话客户端会使用 hbbs 通过RELAY下发的地址一旦你手填就以你填的为准。如果填错了表现是能连上、能看到对方桌面、但特别卡——因为它走了一条绕远路的中继甚至可能是一条不通的路由。Key 字段的坑更隐蔽。公钥那串 Base64 大概 44 个字符从终端复制的时候特别容易带上换行或者末尾空格。有些客户端版本对空格敏感Key 不对就会报密钥不匹配但错误提示很含糊容易误判成服务端没起来。我一般的验证顺序是先kubectl logs确认 hbbs 启动正常、日志里没有报错再从一台外网机器nc -vz signal.example.com 21116确认 TCP 通然后nc -vzu signal.example.com 21116确认 UDP 通UDP 的 nc 结果不完全可靠通了只说明包发出去了但至少能排除明显的防火墙拦截最后才去查 Key。6.2 NAT 类型决定了你会不会把中继打爆客户端首次连接时会做一次 NAT 类型探测结果分为几类。如果显示的是对称型Symmetric意味着几乎不可能直连成功所有会话都会走中继。这个探测依赖 21116 的 UDP 和 21115 的 TCP。如果 UDP 那一路在某个环节被拦了客户端会直接判定为不可直连。所以前面反复强调 UDP 端口就是因为这个判定一旦出错后果是长期的中继带宽浪费而不是连接失败——它不会报错只会默默变慢。排查方法是在客户端看连接信息里的连接类型显示 Relayed 就是走了中继显示 Direct 就是直连。如果一群人本来应该能直连却全是 Relayed先查 UDP。7. 实测撞上的五个问题与处理链路7.1 发布卡在 Multi-AttachPod 永远起不来现象改了镜像 tag 之后kubectl rollout status一直不返回kubectl get pods看到新 Pod 是 ContainerCreatingkubectl describe里的事件写着Multi-Attach error for volume pvc-xxxx。排查链路先看 PVC 的 accessModes 是不是 RWO是再看 Deployment 的 strategy 是不是 RollingUpdate是基本就定位了。RWO 卷同一时间只能挂在一个节点上滚动更新期间新旧两个 Pod 同时存在新 Pod 调度到另一个节点就抢不到卷。处理把策略改成Recreate。改完之后发布会有短暂中断但因为 hbbs 是单副本本来就没有冗余这个中断是不可避免的接受它就好。如果你不能接受那说明 hbbs 该上真正的多副本方案但受限于 SQLite这条路走不通——只能靠快速重建来缩短中断时间。7.2 全部连接都走了中继直连率是零现象所有客户端连接信息都显示 Relayed中继节点的带宽被打满画面明显卡顿。排查链路第一步从外网测 21116 UDP 是否可达发现不通。第二步看云厂商安全组TCP 21116 有规则UDP 21116 没有。第三步补上 UDP 规则后测试还是不通。第四步看节点本机的防火墙规则发现默认策略是 DROP只放行了部分端口。第五步放行 UDP 21116 后恢复正常直连率从 0 涨到七成以上。这个坑的教训是安全组和主机防火墙是两层独立的过滤任何一层漏了都不行。而且 UDP 的问题不像 TCP 那样有明显的连接拒绝它就是静默丢包表现是超时很容易被误判成服务没起来。7.3 RELAY 填了集群内部地址客户端全部超时现象hbbs 起来了客户端能注册 ID但一发起连接就卡住最后超时。排查链路看 hbbs 日志能看到客户端注册成功的记录说明信令通了。说明问题出在拿到中继地址之后。用kubectl exec进容器看环境变量RELAY填的是hbbr.rustdesk.svc.cluster.local:21117——这是集群内部域名外部客户端当然解析不了。处理改成对外域名relay.example.com:21117重启 hbbs环境变量变更需要重建 Pod问题消失。这个错误的迷惑性在于hbbs 自己就运行在集群里它填内部地址看起来完全合理。但你要时刻记住这个值最终是给客户端用的不是给自己用的。7.4 升级镜像后 hbbs 起来就崩现象把 tag 从旧版本改成 latestPod 一直 CrashLoopBackOff日志里报数据库 schema 相关的错误。排查链路kubectl logs --previous看上一次崩溃的日志确认是打开db_v2.sqlite3时失败。基本可以确定是新版本对数据库做了不兼容变更而旧库还在。处理先把 PVC 里的数据完整备份出来起一个临时 Pod 挂同一个卷tar打包然后决定是回退镜像版本还是让新版本重建库。如果选择重建库意味着所有设备 ID 要重新注册用户侧感知就是我的固定 ID 变了需要提前通知。预防措施就一条永远不用 latest永远先备份再升级。SQLite 这种嵌入式数据库没有自动迁移回滚机制出问题只能靠备份。7.5 空闲一段时间后连接断开现象远程桌面挂着不动十几分钟后回来发现连接断了要重连。排查链路先看是不是客户端本身的超时设置。排除之后怀疑是中间网络设备的 NAT 表项老化。如果是 NodePort 方式暴露的kube-proxy 的 UDP conntrack 超时默认 120 秒正好对上。改成 hostNetwork 之后链路上少了一层 conntrack问题缓解了很多。补充手段让客户端定期产生一点流量或者缩短心跳间隔。但这是治标根本还是减少转发层数。8. 长期运行的几件事备份、监控与扩容判断8.1 备份只备份 PVC 就够了hbbs 的所有状态都在/root下所以备份策略很单纯定期把整个目录打包出来。不需要 dump 数据库不需要导配置只要id_ed25519、id_ed25519.pub、db_v2.sqlite3三个文件在随时可以原地复活。我用的方式是一个 CronJob每周跑一次把 PVC 挂进一个临时容器tar打包后推到对象存储。这个作业最大的风险是它可能和 hbbs 同时挂载同一个 RWO 卷——又回到那个老问题。解决办法是把备份 Job 的调度时间放在低峰期并且临时把 hbbs 缩到 0kubectl scale备份完再拉起来。听起来笨但可靠。相比搞一套复杂的快照方案这个流程三分钟就能跑完中断时间可控。8.2 没有 Metrics 端点怎么监控RustDesk 服务端不暴露 Prometheus 格式的指标这是它的一个短板。能做的监控有几层监控层手段能发现什么容器层kube-state-metricsPod 重启次数、就绪状态端口层黑盒探测TCP 21116/21117服务是否在监听网络层node-exporter 的网卡流量中继带宽是否异常日志层容器日志采集 关键字告警连接失败、密钥错误其中最有价值的是网卡流量。hbbr 的流量曲线如果突然抬升通常意味着大量客户端从直连退化成中继这往往对应着网络环境的某处变更。这条曲线比任何应用层指标都灵敏。日志告警可以抓几个关键词比如连接被拒绝、密钥不匹配。这类日志如果短时间内大量出现基本可以确定是配置被改错了或者遭到了扫描。8.3 什么时候该拆节点、什么时候该加中继判断依据可以量化中继带宽利用率长期超过 60%该考虑把 hbbr 拆到独立节点或者扩容出口带宽。直连率低于 50%问题在网络环境而不是容量先去查 UDP 和 NAT 类型别急着加机器。hbbs 的 CPU 长期高于 30%不太可能除非设备数上万这时候要关注 SQLite 的写锁竞争。Pod 重启次数每周超过 2 次查探针配置和存储后端不是业务问题。我个人的经验是几百台设备以内的规模单节点跑 hbbs hbbr 完全够用真正需要拆分的时候往往是中继带宽先顶不住而不是 CPU 或内存。还有一点值得提如果你有多个办公地点可以考虑在每个地点部署一个 hbbr 中继让RELAY指向最近的那个。不过RELAY只能填一个地址这个方案需要客户端侧配合实际用起来比较折腾不如先把基础链路的质量做好。8.4 一点收尾的经验我把这套东西从裸机 Docker 迁到集群前后折腾了大概三个周末。真正花时间的不是写 YAML而是搞明白哪些地址是给谁看的这件事。hbbs 里的RELAY是给客户端看的Service 的 ClusterIP 是给集群内部看的DNS 记录是给外网看的这三者混在一起的时候出问题的表现都是连不上但原因完全不同。现在回头看最值得提前做对的三件事是钉死镜像版本、挂好 PVC 再启动、把 UDP 端口在三个地方都确认一遍。这三件事做到位后面基本不会再有什么幺蛾子。至于清单本身照着上面的模板改改域名就能用不需要什么花哨的技巧。