K3s ServiceLB 云负载均衡控制器迁入云控制器管理器(CCM):基于 cloudprovider.LoadBalancer 接口的架构演进深度解析
【免费下载链接】k3sLightweight Kubernetes项目地址: https://gitcode.com/GitHub_Trending/k3/k3s
导读
本文基于 K3s 官方架构决策记录(ADR)docs/adrs/servicelb-ccm.md,完整还原 K3s 将 ServiceLB 负载均衡控制器从"独立接入 Wrangler 核心控制器"的私有实现,重构为"内嵌云控制器管理器(CCM)中 cloudprovider.LoadBalancer 接口后端"的演进全过程。你将理解 K3s 为何放弃自建 Service 监听与 finalizer 管理逻辑、如何通过 Kubernetes 官方云提供商接口复用核心代码,以及--disable-cloud-controller、--disable=servicelb两个开关在重构前后的语义变化与资源收益。
一、背景:一个只做"节点生命周期"的桩云提供商
K3s 一直内置一个"桩(stub)云提供商",它只实现了cloudprovider.Instances接口中与节点生命周期相关的极少部分功能:
- 正确设置节点的地址(node addresses);
- 清除节点首次加入集群时被添加的
Uninitialized污点(taint),使节点能正常参与调度。
这一点可以在源码中得到印证:pkg/cloudprovider/instances.go 中的k3s结构体实现了cloudprovider.InstancesV2接口,InstanceExists恒返回true(K3s 节点始终存在)、InstanceShutdown恒返回false(K3s 节点从不被云侧关闭),而InstanceMetadata则负责从节点注解(annotation)与标签(label)中解析内网 IP、外网 IP、DNS 与主机名。
Kubernetes 的云提供商接口(cloudprovider.Interface)本身为负载均衡控制器预留了扩展点(cloudprovider.LoadBalancer),但 K3s 当时并未实现它。相反,K3s 选择运行一个独立(standalone)的 ServiceLB 控制器,直接挂接在核心 Wrangler 控制器体系上。ADR 记录的时间点为 2022-09-29,状态为Accepted(已采纳)。
二、问题所在:重复造轮子的独立控制器
ADR 明确指出,由于没有走 Kubernetes 官方的负载均衡接口,独立 ServiceLB 控制器被迫从零实现大量本应由核心 Kubernetes 代码代劳的逻辑:
- 监听 Service 资源:需要自己注册 informer 并编写变更回调;
- 类型与状态过滤:需要自行判断一个 Service 是否是
LoadBalancer类型、当前状态是否符合处理条件; - 管理 finalizer:需要在 Service 上手动添加/移除 finalizer,以控制资源的清理顺序与所有权转移。
这些逻辑如果实现了cloudprovider.LoadBalancer接口,全部由 Kubernetes 核心的 service 控制器统一调度处理,属于典型的"重复造轮子"。同时,独立控制器与云提供商机制并存,也让 K3s 的组件边界变得模糊。
三、决策:把 ServiceLB 搬进云控制器管理器
ADR 的决策只有一句话,但信息量很大:
将 ServiceLB 代码迁移到云控制器(cloud-controller)中,作为
LoadBalancer接口实现的后端。
同时明确了两条必须保留的既有行为:
- 保留禁用节点生命周期功能的既有行为:用户仍然可以一边使用 ServiceLB,一边挂载其他负责节点生命周期的 cloud-controller-manager(即 ServiceLB 不再与 CCM 强绑定);
- 保留通过节点标签(node labels)定制 ServiceLB 行为的支持。
这一决策在源码中的直接体现是 pkg/cloudprovider/cloudprovider.go:k3s结构体通过var _ cloudprovider.Interface = &k3s{}声明完整实现 Kubernetes 云提供商接口,并在 init() 中调用cloudprovider.RegisterCloudProvider(version.Program, ...)完成注册。
3.1 可外部配置的 Config 结构
pkg/cloudprovider/cloudprovider.go 定义了 JSON 反序列化的配置结构,这是理解"哪些功能可以被开关控制"的关键:
| 配置字段 | JSON 键 | 说明 | 默认值 |
|---|---|---|---|
LBDefaultPriorityClassName | lbDefaultPriorityClassName | ServiceLB Pod 默认 PriorityClass | system-node-critical |
LBEnabled | lbEnabled | 是否启用负载均衡控制器 | true |
LBImage | lbImage | ServiceLB 使用的镜像 | rancher/klipper-lb:v0.4.17 |
LBNamespace | lbNamespace | ServiceLB DaemonSet 所在命名空间 | kube-system |
NodeEnabled | nodeEnabled | 是否启用节点生命周期功能 | true |
Rootless | rootless | 是否以 rootless 模式运行 | false |
值得注意的是默认值的硬编码位置:pkg/cloudprovider/servicelb.go 中定义了DefaultLBNS = meta.NamespaceSystem(即kube-system)、DefaultLBPriorityClassName = "system-node-critical"与DefaultLBImage。若传入的 config 同时关闭了 LB 与节点功能,init()会直接返回错误"all cloud-provider functionality disabled by config"。
3.2 接口分派:LB 与节点功能可独立开关
pkg/cloudprovider/cloudprovider.go 中接口方法的返回值直接决定了功能开关的语义:
LoadBalancer()返回k, k.LBEnabled:仅当LBEnabled为真时,K3s 的 CCM 才向 Kubernetes 暴露负载均衡能力;InstancesV2()返回k, k.NodeEnabled:仅当NodeEnabled为真时才暴露节点实例能力;Zones()、Clusters()、Routes()均返回nil, false,即 K3s 云提供商不支持这些扩展。
这正对应 ADR 中"ServiceLB 可与其他处理节点生命周期的 CCM 并存"的承诺:LBEnabled与NodeEnabled是正交的。
四、ServiceLB 控制器的工作原理(源码级拆解)
迁移后的 ServiceLB 全部实现位于 pkg/cloudprovider/servicelb.go,下面按职责拆解其核心链路。
4.1 标签与 finalizer 常量
pkg/cloudprovider/servicelb.go 定义了控制器全生命周期使用的标识符:
finalizerName = "svccontroller.k3s.cattle.io/daemonset":旧实现遗留在 Service 上的 finalizer;svcNameLabel/svcNamespaceLabel:DaemonSet 与 Pod 上标记所属 Service 的标签;daemonsetNodeLabel = "svccontroller.k3s.cattle.io/enablelb":节点参与 ServiceLB 的开关标签;daemonsetNodePoolLabel = "svccontroller.k3s.cattle.io/lbpool":节点池标签;nodeSelectorLabel、priorityAnnotation、tolerationsAnnotation:分别用于 DaemonSet 自身标记、优先级类与容忍度定制;controllerName = names.ServiceLBController:使用 Kubernetes 官方定义的控制器名。
4.2 注册与事件驱动
Register()(servicelb.go)是控制器的心脏,它把三类资源变化接入处理:
- Node 变化(
onChangeNode):当节点带enablelb标签时,触发updateDaemonSets()刷新所有 ServiceLB DaemonSet 的节点选择器; - Pod 变化(
onChangePod):当带 svc 标签的 Pod 获得 IP 后,向工作队列投递对应 Service; - EndpointSlice 变化(
onChangeEndpointSlice):用于在externalTrafficPolicy: Local场景下确保负载均衡地址只列出有就绪 Pod 的节点。
同时Register()还会做三件启动期工作:确保命名空间存在(ensureServiceLBNamespace)、确保名为svclb的 ServiceAccount 存在(ensureServiceLBServiceAccount)、以及清除旧实现遗留的 Service finalizer(removeServiceFinalizers),从而把所有权平稳移交给 CCM 实现。
4.3 轻量工作队列,而非完整 Service 控制器
一个很关键的设计细节:ServiceLB 并没有启用完整的 Wrangler Service 控制器,而是使用workqueue.RateLimitingInterface搭建了一个轻量工作队列(servicelb.go)。代码注释明确说明这是为了在 Pod 频繁更新时降低抖动(thrashing),并注明参考了 rancher/lasso 的 controller 实现。队列项经processSingleItem解析为 namespace/name 后,调用updateStatus完成状态回写。
4.4 DaemonSet 的生成、部署与回收
- 生成:
newDaemonSet(servicelb.go)根据 Service 的端口生成名为lb-<proto>-<port>的容器,注入SRC_PORT、SRC_RANGES、DEST_PROTO、DEST_PORT、DEST_IPS等环境变量,并设置NET_ADMIN能力与 IPv4/IPv6 转发 sysctl。命名规则见generateName:svclb-<service>-<uid前8位>,并对超长名称做了 48 字符截断与尾连字符保护(有对应单测); - 部署:
deployDaemonSet通过 Wrangler 的apply处理器以 Service 为 Owner 应用对象集; - 回收:
deleteDaemonSet在 Service 删除时清理对应 DaemonSet,deleteAllDaemonsets则在负载均衡功能被禁用时按标签批量清理全部托管 DaemonSet(见 Initialize 的 else 分支); - 节点选择:
nodeHasDaemonSetLabel检测是否存在任意带enablelb标签的节点,一旦存在,DaemonSet 就带enablelb=true节点选择器;若 Service 带lbpool标签则进一步限定节点池。
4.5 负载均衡接口的四件套
pkg/cloudprovider/loadbalancer.go 实现cloudprovider.LoadBalancer接口:
| 方法 | 行为 |
|---|---|
GetLoadBalancer | 查询对应 DaemonSet 是否存在并返回状态 |
GetLoadBalancerName | 返回generateName(svc)生成的负载均衡名称 |
EnsureLoadBalancer | 部署 DaemonSet,随后返回cloudprovider.ImplementedElsewhere(状态回写交由别处完成) |
UpdateLoadBalancer | 直接返回cloudprovider.ImplementedElsewhere(注释解释了原因:Kubernetes 核心对节点更新的过滤条件与 K3s DaemonSet 的节点选择逻辑不兼容) |
EnsureLoadBalancerDeleted | 删除对应 DaemonSet |
EnsureLoadBalancer返回ImplementedElsewhere是一个关键细节:K3s 的 ServiceLB 选择"部署完 DaemonSet 后由自己的轻量工作队列回写 Service 状态",而非依赖核心 service 控制器——这既复用了接口带来的类型过滤与生命周期管理,又保留了 K3s 对节点选择、状态计算的完全控制。
4.6 状态计算:IP 族过滤与 Local 流量策略
getStatus(servicelb.go)与podIPs(servicelb.go)实现了地址计算:
- 优先使用节点 ExternalIP,若无则回退 InternalIP;
filterByIPFamily按 Service 的IPFamilies对地址排序过滤,支持 IPv4 单栈、IPv6 单栈与双栈(详见单测 servicelb_test.go);externalTrafficPolicy: Local时通过 EndpointSlice 的就绪状态筛出有就绪 Pod 的节点;- Rootless 模式下统一回退为
127.0.0.1。
4.7 行为定制:注解与标签
ADR 承诺保留的"节点标签定制"之外,ServiceLB 还支持两种 Service 注解(servicelb.go):
svccontroller.k3s.cattle.io/priorityclassname:覆盖 DaemonSet Pod 的 PriorityClass;svccontroller.k3s.cattle.io/tolerations:以 JSON/YAML 追加 Pod 容忍度,且validateToleration会对操作符与键值做合法性校验。
五、后果:开关语义与资源收益
ADR 列出了四条后果,均能在 pkg/daemons/control/server.go 中得到直接验证:
- 资源节约:ServiceLB 被禁用时,K3s 不再无条件启动若干核心控制器,资源占用更低。从
Initialize的实现看,LBEnabled=false时甚至不会创建 Wrangler factory 与各类缓存(cloudprovider.go); --disable-cloud-controller语义变化:该开关现在只禁用 CCM 的cloud-node与cloud-node-lifecycle两个控制器——这正是历史上 K3s CCM 唯一支持的控制器。见 cloudControllerManager 的参数拼接:controllers = "*,-route,-cloud-node,-cloud-node-lifecycle"且secure-port=0;--disable=servicelb语义变化:该开关现在禁用 CCM 的service控制器(server.go:controllers += ",-service"),servicelb从独立组件名变为 CCM 内部 controller 名;- 完全禁用时 CCM 不运行:Server 启动逻辑 中
if !cfg.DisableCCM || !cfg.DisableServiceLB才会调用cloudControllerManager,两者同时禁用时整个 cloud-controller-manager 进程都不再启动。
反过来,当 CCM 启用时,kube-controller-manager 也会相应让位:在 controllerManager 中会追加controllers = "*,-service,-route,-cloud-node-lifecycle"并设置configure-cloud-routes=false,避免与 CCM 抢活。
命令行层面,--disable-cloud-controller定义在 pkg/cli/cmds/server.go,绑定到ServerConfig.DisableCCM(server.go)。
六、测试保障
迁移并非一刀切重写,而是在复用核心代码的同时保留了原有行为,这从测试可以窥见一斑:
- servicelb_test.go 的
Test_UnitFilterByIPFamily覆盖无 IPFamily、IPv4 单栈、IPv6 单栈、双栈四种组合; Test_UnitFilterByIPFamily_Ordering验证地址排序稳定性;Test_UnitGenerateName验证 DaemonSet 命名规则,包括短名、长名、以及含连续连字符的长名的截断行为。
七、总结
这份 ADR 记录了一次典型的"向 Kubernetes 标准看齐"的重构:ServiceLB 从"挂接在 Wrangler 上的私有大轮子"收敛为"云控制器管理器内 LoadBalancer 接口的后端实现"。收益清晰可见——Service 监听、类型过滤、finalizer 等繁重逻辑交由 Kubernetes 核心代码处理,K3s 得以把精力集中在节点选择、IP 族过滤、Local 流量策略等真正体现差异化价值的实现上,同时通过--disable-cloud-controller与--disable=servicelb两个正交开关,让用户自由组合"负载均衡"与"节点生命周期"两种能力。
【免费下载链接】k3sLightweight Kubernetes项目地址: https://gitcode.com/GitHub_Trending/k3/k3s
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考