当GPU竞价实例像潮水一样退去,大模型服务怎么办?这里分享我如何通过Kubebuilder自研的Operator,让算力池自动"涨潮",并通过Kong网关实现多实例负载均衡。以Qwen2-0.5B为例跑通全流程,附控制器代码解读。本文的理论基础均建立在已实际跑通的demo案例。此次GPU实例基于腾讯云,但架构本身不限定云厂商,可迁移到任何支持API调度的实例平台。
一、为什么需要"潮汐"算力?
做AI推理的同学都知道,私有化部署大模型有两个痛点:
痛点一:机器太贵。 一块A100按量计费一小时几十块,长期跑推理成本扛不住。
痛点二:竞价实例太不稳定。 竞价实例(Spot)价格只有按量的1~3折,但平台随时可能回收,模型服务说没就没。
这就引出了一个核心问题:能不能既享受竞价实例的低价,又保证LLM服务的高可用?
我的思路是——让算力像潮汐一样流动,但服务始终稳定。
具体来说:
维护一个"算力池",里面始终保持固定数量的实例(比如n台);
某台实例被回收了,立刻自动创建新实例补齐;
用户始终通过统一的网关地址访问,后端实例的增减对使用者完全透明。
这就是YiFlow中TideGPU模块的由来。
二、整体架构:探测-补偿-同步的闭环设计
用户请求│▼┌─────────────────┐│ Kong 网关层 │ ← 统一入口,负载均衡└────────┬────────┘│▼┌─────────────────┐│ TideGPU Operator│ ← 核心:探测 → 补偿 → 同步│ (Reconcile循环) │└────────┬────────┘│┌────┴────┐▼ ▼┌──────┐ ┌──────┐│CVM-1 │ │CVM-2 │ ← 实例池,被回收时自动补齐└──────┘ └──────┘│ │└────┬────┘│▼┌────────────┐│ 状态变化反馈 │ ← 新IP/旧IP变化,触发Kong同步└────────────┘
关键设计:
Kong网关:固定入口,负责负载均衡和路由,接收用户请求,按 Upstream 配置分发到存活实例;
TideGPU Operator:Kubernetes集群内的"调度大脑",每 n 秒执行探测→补偿→同步,维持期望状态;
实例池:实际跑Ollama推理的后端节点,由 Operator 直接创建/销毁,IP 变化实时反馈到 Kong。
注:因当前 GPU 竞价实例资源紧张,本次验证使用 标准型 S5 CPU 实例 模拟 GPU 场景,架构逻辑完全等价。云厂商和实例类型均可替换,只要提供对应的 API 即可。
三、核心设计:一台"母机"生万物
3.1、 镜像工厂:避免每台机器重复装模型
Ollama部署大模型有个麻烦事:模型文件大,如果每台新实例都现拉模型,启动时间可能要几分钟,这在竞价实例频繁回收的场景下是不可接受的。
我的做法是:
创建一台"母机";
在母机上安装Ollama,并拉取LLM模型。本次实验以 qwen2:0.5b为例;
将这台机器打成自定义镜像,得到imageId;
将这个镜像ID作为TideGPU CR的参数。
这样,Operator每次创建新实例时,直接基于这个镜像启动,开机即服务,无需等待模型下载。
# tidegpu_v1_tidepool.yamlspec:imageId: img-xxxxx # 预装Ollama + qwen2:0.5b的镜像instanceType: "S5.2XLARGE16" # 本次验证用CPU模拟GPUminimum: 2 # 池中最少2台maximum: 2 # 池中最多2台

四、TidePool CRD:用声明式定义算力池
TidePool是我们自定义的Kubernetes资源,描述了一个算力池的期望状态:
apiVersion: tidegpu.eja.dev/v1kind: TidePoolmetadata:name: tidepool-samplespec:secretId: "AKID..."secretKey: "h8JD..."region: ap-singaporeavailabilityZone: ap-singapore-2instanceType: "S5.2XLARGE16"minimum: 2maximum: 2subnetId: subnet-xxxxxvpcId: vpc-xxxxxsecurityGroupIds:- sg-xxxxximageId: img-xxxxxinstanceChargeType: POSTPAID_BY_HOUR # 生产环境用SPOTPAID(竞价)kongGatewayIP: "x.x.x.x"
关键字段解读:
SPOTPAID为竞价实例,POSTPAID_BY_HOUR为按量(演示用) | |
可移植性说明:secretId/secretKey、region、instanceType等字段可根据实际平台调整。如果是阿里云、AWS或其他私有云平台,只需替换对应的SDK调用即可,Operator的核心逻辑不变。
五、控制器核心逻辑:Reconcile循环里的"三件套"
TideGPU Operator的核心是一个Reconciler,它每n秒执行一次"巡检",做三件事:
5.1、第一件事:检查Kong网关配置
func(r *TidePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {// ... 获取TidePool对象// 检查Kong AI代理是否就绪err = r.checkKongAiProxy(tidepool)// 获取当前运行中的实例runningList, err := r.getRunningInstanceIDs(tidepool)// ...}
checkKongAiProxy会查询Kong的Service列表,如果发现ai-YiFlow-proxy服务不存在,就自动创建完整的Kong资源:
func(r *TidePoolReconciler) createKongServiceAndRouteAndUpstream(tidepool *tidegpuv1.TidePool) error {// 1. 创建Upstream:llama2-YiFlow-upstreamupstreamData := map[string]string{"name": "llama2-YiFlow-upstream",}// 2. 创建Service:ai-YiFlow-proxy,指向上游serviceData := map[string]string{"name": "ai-YiFlow-proxy","host": "llama2-YiFlow-upstream","protocol": "http","path": "/api/chat",}// 3. 创建Route:ollama-chat,路径匹配~/ollama-chat$routeData := map[string]interface{}{"name": "ollama-chat","paths": []string{"~/ollama-chat$"},}}
5.2、第二件事:维持实例水位
runningCount := len(runningList)switch {case runningCount < int(tidepool.Spec.Minimum):// 实例少了,创建新的delta := tidepool.Spec.Minimum - int32(runningCount)err = r.runInstances(tidepool, delta)case runningCount > int(tidepool.Spec.Maximum):// 实例多了,销毁多余的delta := int32(runningCount) - tidepool.Spec.Maximumerr = r.terminateInstances(tidepool, delta)}
5.3 第三件事:同步Kong的Upstream Targets
这是最关键的一步。Operator需要确保:Kong知道哪些后端实例是活着的。
// 同步 Kong Upstreamerr = r.syncKongUpstream(tidepool)
syncKongUpstream的逻辑非常清晰——全量比对,增量同步:
1、拉取Kong当前的所有Targets;
2、 与实例列表做diff:
如果某个Target对应的实例已不存在(被回收了),删除该Target;
如果某个实例还没有对应的Target,新增该Target。
func(r *TidePoolReconciler) syncKongUpstream(tidepool *tidegpuv1.TidePool) error {// 1. 获取Kong当前targets// 2. 遍历当前targets,删除已不存在的实例// 3. 遍历实例列表,添加缺失的targets}

六、探测机制:直接对话云平台API
TideGPU不依赖Kubernetes的Node心跳,而是直接调用云平台API探测实例状态:
func(r *TidePoolReconciler) getRunningInstanceIDs(tidepool *tidegpuv1.TidePool) ([]string, error) {// 创建云平台客户端client, err := r.createCVMClient(tidepool.Spec)// 查询实例列表request := cvm.NewDescribeInstancesRequest()response, err := client.DescribeInstances(request)var runningInstanceIDs []stringvar instances []tidegpuv1.Instancesfor _, instance := range response.Response.InstanceSet {// 判断实例是否在运行中(包括PENDING和STARTING状态)if *instance.InstanceState == "RUNNING" ||*instance.InstanceState == "PENDING" ||*instance.InstanceState == "STARTING" {runningInstanceIDs = append(runningInstanceIDs, *instance.InstanceId)}// 检查公网IP是否已分配(分配公网IP是异步的)if len(instance.PublicIpAddresses) == 0 {return nil, fmt.Errorf("instance %s does not have public ip", *instance.InstanceId)}instances = append(instances, tidegpuv1.Instances{InstanceId: *instance.InstanceId,PublicIp: *instance.PublicIpAddresses[0],})}// 更新TidePool的Statustidepool.Status.Instances = instanceserr = r.Status().Update(context.Background(), tidepool)}
设计要点:
RUNNING,还包括PENDING和STARTING,避免实例创建过程中被误判为缺失 | |
tidepool.Status,供syncKongUpstream使用 |
可移植性说明:createCVMClient和DescribeInstances是本次验证用的腾讯云SDK调用。如果迁移到其他平台,只需替换这部分SDK即可,Reconcile的核心逻辑(水位检查、Kong同步)完全不变。
七、实战验证:从搭建到故障模拟
7.1、环境搭建结果

备注:Kong Manager已可访问,x.x.x.x:8002/routes 页面正常打开,Routes列表为空(尚未创建路由)。

备注: Operator自动创建了ollama-chat路由,与代码中"name":"ollama-chat"、"paths": []string{"~/ollama-chat$"}完全对应。左侧Kong Manager显示Route,右侧是IDE中的代码。

备注:Upstreamllama2-YiFlow-upstream已创建,与代码中"name": "llama2-YiFlow-upstream"对应。
7.2、实例注册到Kong

备注: 两台实例已成功注册为Kong的Target:
x.x.x.x:8080
x.x.x.x:8080
右侧Operator日志显示:Created target for x.x.x.x:8080、Created target for x.x.x.x:8080

备注:左侧云平台控制台显示两台运行中的实例(公网IP分别为x.x.x.x和x.x.x.x),右侧KongManager显示UpstreamTargets与之一一对应。
7.3、推理验证:通过Kong网关访问LLM

备注: 向http://x.x.x.x:8000/ollama-chat发起POST请求,返回200,模型成功输出诗句。证明Kong → Upstream → Ollama全链路通畅。
7.4、故障模拟:手动销毁一台实例

备注: 在云平台控制台对实例执行"销毁/退还"操作,模拟竞价实例被平台回收的真实场景。

备注: 左侧云平台控制台显示新实例已创建(公网IP变为x.x.x.x),右侧Kong Manager显示Upstream Targets已更新。下方Operator日志显示:
Deleted target x.x.x.x:8080
Created target for x.x.x.x:8080
自愈完成,服务无中断。
7.5、自愈后验证

备注: 再次向Kong网关发起请求,依然返回200,模型正常响应。新实例已完全接替被销毁的实例工作。
八、关键技术复盘
8.1、为什么用Kubebuilder而不是Terraform alone?
Terraform擅长"一次性"基础设施编排,但竞价实例是动态变化的。Kubebuilder提供的Reconcile循环,天然适合这种"持续维持期望状态"的场景。两者结合:Terraform负责静态资源(VPC、子网、Kong网关),Operator负责动态资源(实例池)。
8.2、为什么Kong而不是Nginx?
Kong的Admin API支持运行时动态增删UpstreamTarget,无需reload。这与Operator的"自动注册&注销"需求完美契合。如果用Nginx,每次实例变化都要改配置并reload,不够优雅。
8.3、镜像工厂的价值
"母机打镜像"策略,将模型部署时间从"几分钟"缩短到"几十秒"。在生产环境中,可结合Packer或云厂商镜像服务,实现版本化管理。
8.4、平台可移植性
本次验证基于腾讯云,但架构设计不限定厂商:
| 实例管理 | |
核心思路是统一的:Operator监听实例状态 → 自动补偿 → 同步网关后端。
8.5、当前局限与改进
九、总结:让不稳定的基础设施,提供稳定的服务
TideGPU Operator的核心价值,在于用软件工程的手段,对冲基础设施的不稳定性。通过Kubebuilder的Reconcile循环,我们将竞价实例的"潮汐"特性抽象为可管理的算力池;通过Kong网关的动态负载均衡,我们将后端实例的频繁更替对用户完全屏蔽。
最终实现了:
资源池化:实例按需创建,维持固定水位;
服务自愈:实例被回收后自动补齐,网关自动切换;
统一入口:用户无感知后端变化,始终通过固定地址访问
这在当前AI推理成本高昂的背景下,是一种务实且可落地的降本增效方案。
后续演进方向:
接入云平台竞价实例中断通知,实现优雅下线(保存模型状态);
基于推理QPS自动扩缩容,动态调整minimum/maximum;
多可用区部署,实现地域级容灾。
如果你也在探索低成本LLM私有化部署,欢迎在评论区交流竞价实例的"避坑"经验。
✅ 附:本文核心组件参考资料
🛠️ 核心技术栈文档
Kubebuilder: https://github.com/kubernetes-sigs/kubebuilder
Kong-Admin-API: https://docs.konghq.com/gateway/api/admin-oss/
Ollama: https://github.com/ollama/ollama
Tencent-Cloud-SDK: https://github.com/TencentCloud/tencentcloud-sdk-go
🧠 扩展阅读(云原生 & LLM)
Kubernetes-Operator: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
Kong-Upstream: https://docs.konghq.com/gateway/latest/how-kong-works/load-balancing/
竞价实例最佳实践: 各云厂商官方文档
🎬 配套视频讲解
本文配套的 《GPU算力说没就没?自动续上》已发布至视频号 —— 1 分钟讲透业务痛点、闭环架构与自愈原理,适合快速建立整体认知。
👉 欢迎关注我的视频号 【子逸Tech】,先看视频再读文章,理解更深。
✨ 我们坚持:
只讲自己跑通的方案,拒绝纸上谈兵。
#AIOps#Golang#Kubernetes#Kubebuilder#LLM#Kong#竞价实例#智能运维#开源项目
夜雨聆风