Skip to main content

11. 虚拟节点(Virtual Kubelet)

物理机的尽头​

上一章讲了怎么在有限的物理节点上挤更多 Pod。那如果你的物理节点已经满了,不想再加物理机,又不想因为没地方把 Pod 卡在 Pending——有没有"不是物理机但调度器以为它是 Node"的东西?

有。Virtual Kubelet(虚拟节点)就是这个思路。它是一个假装自己是 kubelet 的东西,Scheduler 把 Pod 分配给它,它再把 Pod 的运行转发到别的地方——可能是 Serverless 容器、边缘设备、甚至另一套 K8S 集群。

接口视角:它在冒充什么​

还记得第 2 章里 kubelet 怎么工作的吗?

Scheduler → 把 Pod.Spec.NodeName 写成 "node-1"
kubelet → Watch 到了 Pod,在自己的 Node 上起容器
└→ 定时向 API Server 更新 Pod Status: Running

Virtual Kubelet 做的事情就一件:冒充 kubelet,把自己注册成一个虚拟 Node,API Server 和 Scheduler 完全看不出区别。

// virtual-kubelet 的核心抽象
// 它只暴露自己是一个 Node,实现了 kubelet 的基本协议
type Provider interface {
// 创建一个 Pod(不一定要起容器——可以是任意计算资源)
CreatePod(ctx context.Context, pod *corev1.Pod) error

// 更新 Pod
UpdatePod(ctx context.Context, pod *corev1.Pod) error

// 删除 Pod
DeletePod(ctx context.Context, pod *corev1.Pod) error

// 获取 Pod 的状态
GetPodStatus(ctx context.Context, namespace, name string) (*corev1.PodStatus, error)
}

Scheduler 看到"有个叫 vk-node 的 Node,还剩无限 CPU 和内存",就把 Pod 调度到上面。Virtual Kubelet 收到 Pod 后,不创建 Linux 容器,而是调 AWS Fargate API 或者 Azure Container Instances API 去起一个云上的容器。

K8S 的接口是这样定义的:

kubelet 需要的接口:能创建/启停容器(CRI)、能汇报节点状态(Node Status API) Virtual Kubelet 做的:实现了同样的接口,但容器变成了云上的 Serverless 实例

典型场景:扩容到云上​

你的自建集群有 10 台物理机,高峰期 Pod 多了 3 个没地方放。传统的方案:要么提前备着几台机器闲置(浪费钱),要么等高峰期手动加机器(来不及)。

Virtual Kubelet 的方案:物理 Node 满了 → Scheduler 把溢出的 Pod 调度到虚拟 Node → Virtual Kubelet 在云上按需起容器 → 几秒钟上线。

成本上,你只为高峰那几小时额外消耗的云资源买单,不用常年维护那几台备机。

换个方向:K3S 在虚拟节点上跑​

反过来也可以。Virtual Kubelet 方向是把外部资源"伪装成 Pod 跑进集群"——把 Serverless 容器接入 K8S 的统一调度。

K3S 经常被部署在边缘侧,然后通过 Virtual Kubelet 把边缘节点的空闲算力注入到中心集群。中心集群看到一个"看似无限容量"的虚拟 Node,实际上每次 Pod 调度到虚拟 Node 都发到了远端的 K3S 集群里跑。

这就进入了多集群的范畴——一个集群的 Pod,跑到别的集群上执行。后面的第 17 章再展开这个。

源码:Virtual Kubelet 怎么模拟 Node​

Virtual Kubelet 的代码结构很简单。它启动后做三件事:

  1. 把自己注册成一个 Node
  2. 定期更新 Node Status(心跳 + 容量信息)
  3. Watch 分配到自己的 Pod,按 Provider 接口处理
// 1. 注册 Node
func (p *BaseProvider) registerNode(ctx context.Context) error {
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: p.nodeName,
},
Status: corev1.NodeStatus{
Capacity: corev1.ResourceList{
corev1.ResourceCPU: *resource.NewQuantity(1000, resource.DecimalSI), // 1000 核,假的
corev1.ResourceMemory: *resource.NewQuantity(4096*1024, resource.BinarySI), // 4TB,假的
},
Conditions: []corev1.NodeCondition{
{Type: corev1.NodeReady, Status: corev1.ConditionTrue}, // 宣称自己 Ready
},
},
}
// 用 Create 而不是 Update——如果 Node 已经存在,这是预期的
// 虚拟 Node 在集群生命周期内只创建一次
_, err := p.rm.GetClient().CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{})
return err
}

// 2. 心跳:定期更新 Node Status
func (p *BaseProvider) startNodeHeartbeat(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
node, _ := p.rm.GetClient().CoreV1().Nodes().Get(ctx, p.nodeName, metav1.GetOptions{})
node.Status.Conditions = updateReadyCondition(node.Status.Conditions)
p.rm.GetClient().CoreV1().Nodes().UpdateStatus(ctx, node, metav1.UpdateOptions{})
}
}

虚拟 Node 的容量是程序员随便写的(1000 核、4TB 内存)——实际上它后面可能是个弹性伸缩的服务,真的能供应这么多。Scheduler 安排 Pod 的逻辑不变:它只看 Node 的 Capacity 还有没有余地,不看这个 Node 是真是假。

思考题:虚拟 Node 宣称 1000 核,Scheduler 就会无脑往它上面塞 Pod——但如果你的数据库 Pod、有状态服务绝对不能离开物理集群,怎么防止它们被"骗"到虚拟 Node?用第 14 章调度里的哪几个机制?

思考题:虚拟 Node 上报"无限容量",但后面是云上的 Serverless 容器——调度过去的 Pod 越多,钱烧越快。谁来拦着它不让成本失控?K8S 有没有办法给虚拟 Node 加个配额或总资源上限?

真实产品案例​

Virtual Kubelet 不是实验室项目——几个云厂商已经把它做成了产品:

产品后端使用方式
AWS Fargate (EKS)Fargate 按需容器创建 Fargate Profile → 带标签的 Pod 自动调到虚拟 Node
Azure ACI ConnectorAzure Container Instances部署 Virtual Kubelet → ACI 作为虚拟 Node 接入 AKS
HashiCorp NomadNomad 集群Virtual Kubelet provider 把 Nomad job 映射成 K8S Pod
OpenFaaS函数运行时把 K8S Pod 映射为 Serverless 函数调用

AWS EKS Fargate 的实际体验:你只需要创建一个 Fargate Profile(指定哪些 namespace 的 Pod 用 Fargate),kubectl apply 的 Pod 如果匹配 Profile,Scheduler 就把 nodeName 写成 fargate——kubelet 不处理它,Fargate Controller 直接在 AWS 后台起一个 Firecracker microVM 跑容器。Pod 的 IP 在 VPC 内,可以通过 VPC Peering 访问你的 RDS/ElastiCache。

生产上虚拟 Node 的 Pod 怎么处理日志? 虚拟 Node 上的 Pod 没有 Node 级别的 /var/log/pods——你不能用 kubectl logs 直读。通常配 Fluent Bit sidecar 把日志推到 CloudWatch 或 Loki。这跟物理 Node 上 DaemonSet 收集日志的模式完全不一样——你得在每个用虚拟 Node 的 Pod template 里加 sidecar。

网络怎么做? Fargate Pod 在同一个 VPC 里,安全组可控。Azure ACI 也类似——Pod IP 在 subnet 内。所以这不是"Pod 在公网上乱跑"——虚拟 Node 的网络平面通常跟你的物理集群打通的。

不是万能药​

Virtual Kubelet 不是"没有代价的扩容"。几个现实问题:

  1. 网络:虚拟 Node 上的 Pod 跟你物理 Node 上的 Pod 不在同一个网络中。如果你的服务需要低延迟的 Pod 间通信(比如 Redis 和它的客户端),跨到虚拟 Node 上延迟会飙升。

  2. 存储:PersistentVolume 通常绑定到具体可用区——你的 PV 在 us-east-1a 的 EBS 上,虚拟 Node 可能起在 us-east-1b,挂不上。

  3. 一致性:虚拟 Node 上的 Pod 状态由 Virtual Kubelet 自己汇报。如果 Provider 后端挂掉(比如 AWS 的 API 超时),Pod 显示 Running 但实际可能已经没了。

所以 Virtual Kubelet 不适用于所有负载,最合适的是:无状态、CPU 密集型、不需要持久存储的服务。

小结​

  • Virtual Kubelet 冒充 kubelet,把自己注册成 Node,Scheduler 分不清真假
  • Pod 被调度到虚拟 Node 后,Provider 接口决定"真正的运行位置"(云容器、边缘设备、其他集群)
  • 虚拟 Node 报告容量可以很大(或无限),Scheduler 只管分配
  • 局限:网络延迟、存储跨区、状态不可靠——适合无状态弹性负载
  • K3S 场景下可作为边缘算力注入中心集群的桥梁

解答​

1. 怎么防止 Pod 被调到虚拟 Node?​

三种机制,从强到弱:

Taint + Toleration——最直接。给虚拟 Node 打上 taint,只有明确声明"我愿意去虚拟 Node"的 Pod 才配 toleration:

# 管理员给虚拟 Node 上锁
kubectl taint nodes vk-node virtual-node=true:NoSchedule
# 只有主动配了 toleration 的 Pod 才能调度上去
spec:
tolerations:
- key: "virtual-node"
operator: "Equal"
value: "true"
effect: "NoSchedule"

没配 toleration 的 Pod——数据库、有状态服务——Scheduler 在 Filter 阶段直接淘汰这个 Node。这是最常用的隔离方式。

NodeSelector / NodeAffinity——反过来用。给物理 Node 打上一个标签,关键服务的 Pod 用 nodeSelector 强制要求这个标签:

kubectl label node worker-1 worker-2 node-type=physical
spec:
nodeSelector:
node-type: physical # 只在物理 Node 上跑

虚拟 Node 没有这个标签,自动被筛掉。

ResourceQuota——在 namespace 层面限制 Pod 总数,间接防止 overflow 到虚拟 Node。但这是限制"量"而不是限制"去哪",不如 taint 精确。

2. 谁来限制虚拟 Node 不让成本失控?​

虚拟 Node 本身不设配额——它汇报的 Capacity 是多少,Scheduler 就认为有多少。没有内置机制说"这个 Node 最多收 100 个 Pod"。

但你可以从几个层面拦:

1. 调低虚拟 Node 上报的 Capacity。不让它报 1000 核,而是根据你预算报 100 核——Virtual Kubelet 启动时可以设 --capacity:

virtual-kubelet --provider aws --capacity cpu=100,memory=200Gi,pods=50

Scheduler 看到这个 Node 只有 100 核,50 个 Pod 满了之后就不再调过去。这是最简单也最有效的成本控制——你报多少,Scheduler 就认为你能装多少。

2. 云侧限制。如果后端是 AWS Fargate,在 AWS 侧设 Service Quota——总 vCPU 上限。K8S 看不到这个上限,但当云侧达到上限后 Virtual Kubelet 的 CreatePod 返回失败,Pod 卡在 ContainerCreating。

3. ResourceQuota + Namespace。把虚拟 Node 单独绑到一个 namespace(比如 burst-capacity),给这个 namespace 设 ResourceQuota,限制它的总 CPU 和内存申请量。

4. KEDA + ScaledObject 配合。KEDA 可以限制扩展上限(maxReplicas),配合 Virtual Kubelet 给每个扩展 unit 加 toleration 指向虚拟 Node——这样扩出来的 Pod 都在虚拟 Node 上,但 KEDA 的 maxReplicas 设了天花板。

一句话:K8S 没有原生给虚拟 Node 设配额的能力——你得在 Virtual Kubelet 启动参数、云侧配额、或 namespace ResourceQuota 这三个层面做限制。

下一章,如果你需要的不只是内置的 Deployment/StatefulSet——控制器模式和 CRD。