Operator AIOps实战

2025-11-08T14:11:02+08:00 | 32分钟阅读 | 更新于 2025-11-08T14:11:02+08:00

@

学习目标

学完本章你应该能够:

  1. 开发一个能"补位"的 SpotPool Operator:用 RequeueAfter 周期巡检,把被云厂商随时释放的 GPU 竞价实例维持在一个声明的最小/最大实例数区间内,核心是接入腾讯云 CVM SDK 做创建/查询/终止,并通过 CRD 的 status 记录当前实例状态。
  2. 在实战一基础上深入一层,做一个基于多家网关负载均衡的大模型私有部署 Operator:用自定义虚拟机镜像让新实例启动即自带 Ollama 与模型,用 OpenAI 兼容的 /v1/chat/completions 接口做推理,并借助 Kong 网关的 Admin API 动态同步 upstream(把新实例公网 IP 加进负载均衡表、把被释放实例剔除)。
  3. 开发一个基于大模型的日志流监测 Operator(log-pilot):周期性地从 Loki 按时间窗口取日志,命中关键字后交给 LLM 分析错误等级、给出建议,遇到严重问题自动把建议发到飞书。
  4. 开发一个基于运维专家知识库的故障排查 Operator(rag-log-pilot):直接从命名空间下的 Pod 取日志,转发给配置了内部专家知识库的 RAGFlow(而不是裸调大模型),用"基于你业务的知识解答"替换"模型自己泛泛而谈",再把答案发到飞书。
  5. 说清 Assets/竞价的成本账,理解为什么 AIOps 里"基于竞价实例做自托管大模型"是高性价比选型,以及它带来的"随时被回收"问题是如何被 Operator 自动闭环的。

前置知识:

  • Operator 基本结构、Reconcile 机制、RequeueAfter(来自 Operator 基础章节)
  • Go + kubebuilder 的基本使用(kubebuilder init / create api / make 那套流程)
  • 基本 K8s 概念:CRD、Pod、namespace、RBAC、label selector
  • 大模型的 OpenAI 兼容聊天接口、飞书机器人 webhook 的简单 POST 用法

本章你会动手做的事:

  1. 用 kubebuilder 脚手架初始化 spot-pool Operator,定义带"最小/最大实例数"的 CRD,接入腾讯云 CVM SDK,跑通"检测缺少实例 → 自动补开一台"的闭环。
  2. 制作用于 Ollama 的虚拟机镜像、用 docker compose 起一个监听公网的 Kong 网关,改造实战一的 Operator,让新开的 GPU 实例自动进入 Kong 的 upstream,实现多实例推理负载均衡。
  3. 初始化 Loki + Promtail + 一个会打印日志的示例应用,开发 log-pilot,观察它把命中关键字的日志发给 LLM 并推送到飞书。
  4. 在 RAGFlow 里建"运维专家知识库"、上传内部故障解决方案,开发 rag-log-pilot,观察它基于知识库回答 Pod 日志中的错误并推送到飞书。
Operator AIOps 实战实战一 竞价资源池实战二 大模型私有部署实战三 日志流监测实战四 RAG 故障排查腾讯云 CVM SDKmin/max + RequeueAfterOllama 镜像自启Kong upstream 同步Loki 时间窗取日志LLM 分析 → 飞书Pod 日志 → RAGFlow专家知识库 → 飞书

从一个冲动开始:为什么自托管大模型离不开竞价实例

进入实战之前,先算一笔账。因为后面四个实战全都是围绕"GPU + 竞价实例 + 用 Operator 自动运维"这一件事展开的。

计费方式小时价逻辑
按量计费约 8.68 元/小时CPU、内存、GPU 全按正价
竞价实例(Spot)约 1.71 元/小时CPU/内存/GPU 按折扣价,磁盘、带宽等其他部分正价

以某个 GN7 实例(T4 显卡、16G 显存、8 核 32G 内存)为例,总价几乎能便宜到 八分之一。如果你要做大模型自托管,或者训练/推理要长期占用一群 GPU,竞价实例基本是"成本最优解"。云厂商把暂时闲置的库存以一个极低价格租给你,本质是填它的空仓位——作为交换,你接受"随时可能被回收"。

这就是问题所在:竞价实例随时会被云厂商释放。当市场上有更高优先级(出价更高、库存竞争)的建机请求进来,云厂商就可能把你手上这台 GPU 收回去,导致你的推理服务或训练任务直接中断。手动盯着控制台去补机器是不现实的——所以第一件事,我们要写一个 Operator 来自动维护竞价实例资源池。

复习提示:类比:竞价资源池就像你在高峰期叫车——便宜车(竞价实例)随时可能被"更贵的乘客抢走",你得一直盯着接下来的车是不是够数。这个 Operator 就是你雇的"自动补位调度员":它每隔一段时间数一遍资源池里的车,少了一台就立刻再叫一台,把数量稳定在你设定的区间内。

实战一:开发 Operator 调度 GPU 竞价实例资源池

整体架构

Operator 运行在 K8s 集群里,通过 watch 一个叫 SpotPool 的自定义资源来控制"竞价实例计算资源池"。SpotPool 的 spec 声明了允许的最小实例数和最大实例数,剩下的参数(地域、可用区、实例类型、子网、VPC、安全组、镜像、扣费类型等)其实是"开通腾讯云虚拟机"所需的参数。

当某台 GPU 竞价实例被云厂商自动释放后,Operator 会以一个固定周期监听资源池里的实例数量是否符合预期;不符合(也就是发现少了一台),它就调用腾讯云 SDK 重新创建一台补上,把资源池维持在三台左右。

flowchart LR
    O[SpotPool Operator
运行在 K8s] -->|watch SpotPool CRD| C[(SpotPool CRD
声明 min/max 实例数)] O -->|周期 RequeueAfter 巡检| Q[查询当前实例数] Q --> N{在 min/max 区间?} N -->|少于 min| K[调用腾讯云 SDK 创建实例] N -->|多于 max| T[调用腾讯云 SDK 终止实例] N -->|符合| W[等待下一轮] K --> C T --> C

前置准备与脚手架

写这个 Operator 前需要:通过 kind / K3S 创建本地集群、安装 kubebuilder、准备 Go 环境。然后初始化工程(整体流程就是 kubebuilder 标准四步):

mkdir demo1 && cd demo1
go mod init spot-pool
kubebuilder init --domain example.com

# 创建一个 API:group = tcluster / iac,version = v1,kind = SpotPool
kubebuilder create api --group iac --version v1 --kind SpotPool \
    --resource --controller

CRD 设计

SpotPool 的字段比较多,本质上就是"开通腾讯云虚拟机所需的参数"再加上"我们自己要用的最小/最大实例数"。

type SpotPoolSpec struct {
	// 初始化腾讯云 SDK 的密钥
	SecretID  string `json:"secretId"`
	SecretKey string `json:"secretKey"`

	Region        string `json:"region"`        // 地域,如 ap-singapore
	AvailableZone string `json:"availableZone"` // 可用区,要跟 VPC/子网匹配
	InstanceType  string `json:"instanceType"`  // 实例类型,如 GN7 系带 T4 GPU

	// 网络相关
	SubnetId        string `json:"subnetId"`
	VPCId           string `json:"vpcId"`
	SecurityGroupId string `json:"securityGroupId"` // 注意复数 groups,敲错会报错
	ImageId         string `json:"imageId"`         // 使用腾讯云官方公开镜像 ID

	InstanceChargeType string `json:"instanceChargeType"` // 计费类型,这里用竞价实例 spot

	// 我们自己关心的控制参数
	MinInstances int32 `json:"minInstances"` // 最小实例数
	MaxInstances int32 `json:"maxInstances"` // 最大实例数
}

type SpotPoolStatus struct {
	Size       int32          `json:"size"`       // 当前实例数
	Conditions []metav1.Condition `json:"conditions"`
	Instances  []InstanceInfo `json:"instances"` // 实例列表
}

type InstanceInfo struct {
	InstanceId string `json:"instanceId"`
	PublicIp   string `json:"publicIp"`
}

写好之后跑 make generate && make manifests,把 CRD 和生成的代码都搞定。

接腾讯云 SDK:创建 CVM 客户端

后面创建/查询实例都依赖一个初始化好的 CVM 客户端。用 secretId / secretKey 做 credentials,指定 region 和签名方式(HmacSHA1)创建 cvm.NewClient:

func createCVMClient(sp v1.SpotPool) (*cvm.Client, error) {
	credential := common.NewCredential(sp.Spec.SecretID, sp.Spec.SecretKey)
	client, err := cvm.NewClient(credential, sp.Spec.Region,
		&sdkprofile.ClientProfile{SignMethod: "HmacSHA1"})
	if err != nil {
		return nil, err
	}
	return client, nil
}

查询并统计运行中的实例

核心工具方法是 getRunningInstanceIds:发起 DescribeInstances 查询,只统计处于 running / pending 状态的实例,同时检查公网 IP 是否已经分配(公网 IP 是异步分配的,可能刚创建完还没有,需要靠重试)。

func getRunningInstanceIds(sp v1.SpotPool) ([]string, error) {
	client, err := createCVMClient(sp)
	if err != nil {
		return nil, err
	}

	request := cvm.NewDescribeInstancesRequest()
	// 镜像、可用区等过滤条件从这里带进去
	response, err := client.DescribeInstances(request)
	if err != nil {
		return nil, err
	}

	var running []string
	for _, instance := range response.Response.InstanceSet {
		state := *instance.InstanceState
		// 只看运行中 / 开通中 / 启动中的实例,跳过其它状态
		if state != "RUNNING" && state != "PENDING" && state != "STARTING" {
			continue
		}
		// 公网 IP 还没分配就返回错误,交给上层稍后重试
		if *instance.PublicIpAddresses[0] == "" {
			return nil, fmt.Errorf("public ip not ready, retry later")
		}
		running = append(running, *instance.InstanceId)
	}
	return running, nil
}

Reconcile 主逻辑:compare 出缺失或超员

类比:这不像是点名"张三在不在、李四在不在"的逐一核对,更像仓库盘库时只数"总共多少箱"——少了一箱就补,多了就退,只要总量落在合理区间,至于具体是哪一箱被换掉,没人关心。

对应到工程里就是:Reconcile 拿腾讯云当前的运行实例数 runningCount,和 CRD 声明的 min / max 比大小——runningCount < min 就补开差量,runningCount > max 就终止差量,在区间内就什么都不做。我们只做"数量对齐",不做"实例逐一对比",因为资源池的核心诉求是凑够数。

Reconcile 拿到腾讯云当前的运行实例数后,和 CRD 声明的 min / max 做对比,决定要创建还是终止。注意这里是"数量对齐"而非"逐一对比",因为我们只关心资源池凑够数。

func (r *SpotPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	var sp iacv1.SpotPool
	if err := r.Get(ctx, req.NamespacedName, &sp); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	runningIDs, err := getRunningInstanceIds(sp)
	if err != nil {
		// 查询失败(比如公网 IP 还没分配),10 秒后重试
		return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
	}
	runningCount := len(runningIDs)

	switch {
	case runningCount < int(sp.Spec.MinInstances):
		delta := int(sp.Spec.MinInstances) - runningCount
		log.FromContext(ctx).Info("need to create instances", "count", delta)
		if err := runInstance(sp, int32(delta)); err != nil {
			return ctrl.Result{RequeueAfter: 40 * time.Second}, nil
		}
	case runningCount > int(sp.Spec.MaxInstances):
		delta := runningCount - int(sp.Spec.MaxInstances)
		log.FromContext(ctx).Info("too many instances, terminate", "count", delta)
		if err := terminateInstance(sp, int32(delta)); err != nil {
			return ctrl.Result{RequeueAfter: 40 * time.Second}, nil
		}
	}

	// 无论如何都要把最新实例状态写回 status,供观测
	_ = r.Status().Update(ctx, &sp)
	// 40 秒后再来一轮,这就是"周期巡检"的来源
	return ctrl.Result{RequeueAfter: 40 * time.Second}, nil
}

创建实例:RunInstances

runInstance 构造 RunInstancesRequest,把 CRD 里的镜像、可用区、计费类型、实例数、实例名、公网带宽、登录密码、安全组、系统盘、VPC/子网全部填进去:

func runInstance(sp iacv1.SpotPool, count int32) error {
	client, err := createCVMClient(sp)
	if err != nil {
		return err
	}

	request := cvm.NewRunInstancesRequest()
	request.ImageId = &sp.Spec.ImageId
	request.Placement = &cvm.Placement{Zone: &sp.Spec.AvailableZone}
	request.InstanceChargeType = &sp.Spec.InstanceChargeType
	request.InstanceCount = &count
	// 实例名带时间戳,方便识别是新补出来的
	name := fmt.Sprintf("spot-%d", time.Now().Unix())
	request.InstanceName = &name
	request.InstanceType = &sp.Spec.InstanceType

	// 公网:按流量计费,带宽 100M,需要公网 IP
	request.InternetAccessible = &cvm.InternetAccessible{
		InternetChargeType:      strPtr("TRAFFIC_POSTPAID_BY_HOUR"),
		InternetMaxBandwidthOut: intPtr(100),
		PublicIpAssigned:        boolPtr(true),
	}
	// 登录与安全组
	request.LoginSettings = &cvm.LoginSettings{Password: strPtr("password123")}
	request.SecurityGroupIds = []*string{&sp.Spec.SecurityGroupId}
	// 系统盘 100G 高性能 SSD
	request.SystemDisk = &cvm.SystemDisk{
		DiskType: strPtr("CLOUD_BSSD"), DiskSize: intPtr(100),
	}
	// 网络归属:VPC + 子网
	request.VirtualPrivateCloud = &cvm.VirtualPrivateCloud{
		VpcId: &sp.Spec.VPCId, SubnetId: &sp.Spec.SubnetId,
	}

	response, err := client.RunInstances(request)
	if err != nil {
		return err
	}
	for _, id := range response.Response.InstanceIdSet {
		fmt.Println("created instance:", *id)
	}
	return nil
}

终止实例:TerminateInstances

terminateInstance 用来把超出的实例"退还",也就是先 query 出当前运行实例,再对其中的若干台执行终止:

func terminateInstance(sp iacv1.SpotPool, count int32) error {
	client, err := createCVMClient(sp)
	if err != nil {
		return err
	}
	runningIDs, err := getRunningInstanceIds(sp)
	if err != nil {
		return err
	}
	// 只终止前 count 台
	request := cvm.NewTerminateInstancesRequest()
	request.InstanceIds = runningIDs[:count]
	if _, err := client.TerminateInstances(request); err != nil {
		return err
	}
	return nil
}
复习提示:几个容易踩的坑:① securityGroupId(复数)拼错成 securityGroupId / securityGroupsId 会在泛型渲染和部署时报错;② VPC 所在区域和实例所在可用区必须匹配,否则报"VPC 所在区域实例所在区不匹配"——这是排查半天最常见的原因,务必在腾讯云控制台确认子网的可用区跟 availableZone 一致;③ 竞价实例有配额限制,创建过多会报"竞价实例超限,请提交工单提升配额",说明请求是成功的,不代表逻辑错了。

运行与验证

把样例 config/samples 填好真实参数后,用 make install 把 CRD 装进集群、kubectl apply -f config/samples/... 创建 SpotPool、最后 make run 在本地跑起 Operator。关键验证场景是手工模拟释放一台:

  1. 初始 min=2, max=2,Operator 发现没有实例,自动创建两台。
  2. 在腾讯云控制台手动销毁其中一台(模拟云厂商回收竞价实例)。
  3. 等一个周期(默认 40 秒的 RequeueAfter),Operator 通过查询发现只有 1 台、少于 min,便自动再补开一台。

整个"自动补位"闭环跑通,实战一就完成了。


实战二:Operator 实现大模型私有部署(Ollama + Kong 负载均衡)

为什么私有部署但又要多实例

实战一我们把 GPU 竞价实例维持在了一个稳定的资源池里,但这只是第一步——我们还没把大模型推理服务真正跑起来、利用起这一池 GPU。这一节要解决的是"怎么在私有环境里做高吞吐的推理"。

  • 用 Ollama:它提供一个 /v1/chat/completions 接口,兼容 OpenAI 的数据结构。这意味着你引入 openai / langchain 这类 SDK 后,只需把请求的 base URL 换成 Ollama 的地址,model、messages 这些结构都不用改,就能直接做推理。
  • 为什么要负载均衡:单实例推理慢,尤其在响应时延要求高的私有化场景。多开几台 GPU 实例,前置一个网关做加权轮询,把推理请求分发到多台机器,即可提升吞吐:请求 A 打到 GPU1、请求 B 打到 GPU2……不需要像单实例那样排队。

这时候竞价实例"随时可能被回收"的老问题又回来了:假设负载均衡表里挂着 GPU3,而 GPU3 恰好被云厂商释放了,那么网关把请求转发到它的 IP 时必然直接报错、拿不到正确结果。所以我们的 Operator 除了维持资源池数量,还要实时把 GPU 实例的公网 IP 和网关的 upstream 做自动同步——新开一台就加进负载均衡表,释放一台就把它剔除。

flowchart LR
    C[外部调用 /v1/chat/completions] --> K[Kong 网关
加权轮询负载均衡] K --> G1[(GPU1 Ollama:8080)] K --> G2[(GPU2 Ollama:8080)] K --> G3[(GPU3 Ollama:8080)] O[Operator] -->|创建/释放实例| G3 O -->|同步 upstream:新增/剔除 target| K G1 -.->|被释放?剔除 target| K

两个"让新实例开箱即用"的准备

推理服务裸装会很慢:装 Ollama、下载模型(动辄几 GB)、启动服务,一个实例串下来要十几分钟。两个关键工程手段解决它:

1) 自定义虚拟机镜像让实例"启动即 ready"。 我们先用一台 GPU 实例装好 Ollama、配好自启动、预下载好要用的模型,再基于它做一个自定义虚拟机镜像。之后 Operator 补开新实例时直接指定这个镜像 ID,启动完自带 Ollama + 模型,且监听在 8080 端口,直接就能接后端流量。

2) Ollama 自启动。 Ollama 本身是个 CLI 程序,用官方推荐的 systemd service 方式实现开机自启:创建 service 文件、systemctl daemon-reload && systemctl enable ollama && systemctl start ollama,让它随系统启动并常驻监听推理端口。

我们可以把这两件事打包成一个 shell 脚本,一条命令跑完"安装 Ollama + 配置自启动 + 预加载模型(如 qwen2:0.5B 小模型测试用,生产可用 qwen2:7B)+ 制作自定义镜像",最后输出镜像 ID。示例脚本骨架:

#!/bin/bash
# 1. 用 Ollama 官方脚本安装
curl -fsSL https://ollama.com/install.sh | sh

# 2. 配置 systemd 实现自启动
cat > /etc/systemd/system/ollama.service <<'EOF'
[Unit]
Description=Ollama
After=network-online.target
[Service]
ExecStart=/usr/local/bin/ollama serve
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable ollama && systemctl start ollama

# 3. 预加载模型(这一步会联网下载,比较耗时)
ollama pull qwen2:0.5b
ollama create qwen2:0.5b  # 或直接使用已拉取模型

# 4. 把当前系统做成自定义镜像,输出 imageId
echo "完成:请把当前实例导出为自定义镜像,拿到 imageId"

部署 Kong 网关

选 Kong 的一个重要原因是它提供了 Admin API,可以动态控制 upstream / target。我们用一个 shell 脚本安装 docker、把 Kong 的 npm / docker-compose 配置拷过来、修改监听参数后 docker compose up 起来。

有一个关键必改项:要让 Operator 能通过公网控制 Kong,必须让 Kong Admin API 监听在 0.0.0.0,这样 Operator 才能用公网 IP 去调用它的控制接口。

CRD 增加 Kong 网关地址

在实战一的基础上,给 SpotPool 的 spec 加一个字段,让 Operator 知道往哪找 Kong:

KongGatewayIp string `json:"kongGatewayIp"` // Kong 网关的公网 IP

Kong 的端口约定:Admin API 在 8001(控制用),网关对外流量在 8000。

新增业务逻辑:同步 Kong 的 upstream

核心方法 syncKongUpstream,目标是让 Kong upstream(ollama-upstream)里的 target 列表与当前实例的公网 IP 列表保持一致。流程是:

1. 构造 Kong Admin API URL:http://<kongGatewayIp>:8001/upstreams/ollama-upstream/targets
2. GET 当前所有 target,解析出 {targetId, host(=实例IP:8080)}
3. 遍历实例列表,构造目标 IP 集
4. 对"在 target 里但不在实例里"的 target → DELETE 掉(实例被释放了)
5. 对"在实例里但不在 target 里"的实例 → POST 新增 target(新补的实例加进负载均衡表)
func syncKongUpstream(sp iacv1.SpotPool) error {
	apiBase := "http://" + sp.Spec.KongGatewayIp + ":8001/upstreams/ollama-upstream/targets"

	// 1. 取当前 targets
	resp, err := http.Get(apiBase + "/list")
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("list targets: %d", resp.StatusCode)
	}
	body, _ := io.ReadAll(resp.Body)
	var targetResp struct {
		Data []struct {
			ID   string `json:"id"`
			Host string `json:"host"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &targetResp); err != nil {
		return err
	}

	// 2. 从实例状态构建目标 IP 集(Ollama 监听在 8080)
	instanceSet := map[string]bool{}
	instanceList := map[string]string{} // instance publicIp -> target host
	for _, inst := range sp.Status.Instances {
		host := fmt.Sprintf("%s:8080", inst.PublicIp)
		instanceSet[host] = true
		instanceList[inst.PublicIp] = host
	}

	// 3. 剔除:target 存在但实例已释放
	for _, t := range targetResp.Data {
		if !instanceSet[t.Host] {
			delURL := fmt.Sprintf("%s/%s", apiBase, t.ID)
			req, _ := http.NewRequest(http.MethodDelete, delURL, nil)
			client := http.DefaultClient
			if resp, err := client.Do(req); err == nil {
				resp.Body.Close()
			}
		}
	}

	// 4. 新增:实例存在但 target 里没有
	for _, inst := range sp.Status.Instances {
		host := fmt.Sprintf("%s:8080", inst.PublicIp)
		exist := false
		for _, t := range targetResp.Data {
			if t.Host == host {
				exist = true
			}
		}
		if !exist {
			postData, _ := json.Marshal(map[string]string{"target": host})
			http.Post(apiBase, "application/json",
				strings.NewReader(string(postData)))
		}
	}
	return nil
}

初始化 Kong 的 Service / Route / Upstream

首次运行时 Kong 里可能什么都没有。我们需要一个 checkKongProxy + createKongServiceAndRouteAndUpstream 方法做初始化:

  • upstream:ollama-upstream(前面同步 target 都是挂在它下面的)。
  • service:ollama-chat,host 指向 ollama-upstream,protocol = http,path = /v1/chat/completions。
  • route:匹配某个 /ollama/chat 路径,把外部请求路由进 service。
sequenceDiagram
    participant O as Operator
    participant K as Kong Admin API :8001
    O->>K: GET /services 检查是否存在 ollama-chat
    alt 不存在
        O->>K: POST /upstreams 创建 ollama-upstream
        O->>K: POST /services 创建 ollama-chat(host=ollama-upstream, path=/v1/chat/completions)
        O->>K: POST /services/ollama-chat/routes 创建路由匹配 /ollama/chat
    end
    O->>K: POST/DELETE /upstreams/ollama-upstream/targets 同步实例 IP

Reconcile 里先做初始化检查(失败则 40 秒后重试),再做 upstream 同步。

运行与端到端验证

把 CRD 的 imageId 换成"带 Ollama + qwen2 模型"的自定义镜像,kongGatewayIp 填好,重新 make install / apply sample,然后 make run。打开 Kong 管理后台(Web UI)可看到它自动创建了 route;等 Operator 补开两台实例后,upstream 里会自动出现两个 target(对应两台 GPU 实例的 xxx.xxx:8080)。

用 curl 直接打网关的对外端口验证推理:

curl -X POST http://<kong-ip>:8000/ollama/chat \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen2:0.5b",
       "messages": [{"role": "user", "content": "你好,你是谁"}]}'
复习提示:注意:默认是流式输出(stream: true)。想直接拿完整结果,把 body 里的 stream 设成 false。然后手工释放一台实例,等 Operator 下一轮 reconcile 之后,刷新 Kong upstream 会发现被释放那台的 target 被自动剔除、并换成新补实例的 target——整个"资源池补位 + 负载均衡表同步"闭环跑通。

有个可以观察到的现象:同步并不是即时的——实例被释放后,它的 target 可能要等 Operator 下一次 reconcile 才被剔掉,期间这个 IP 仍然挂在负载均衡表里。两个改进方向:① 把 RequeueAfter 调短;② 更优的做法是——一旦发现某个 IP 已不可达,就把它的 target 权重改为 0(weight=0),这样网关就不会再把流量打给它,等你确认再删除。


实战三:开发基于 LLM 的日志流监测 Operator

思路:日志 → LLM → 飞书

实战三做一个开箱跑路的 AIOps 报警器:周期性地从 Loki 取出日志,命中关键字后交给 LLM 分析,发现问题直接扔到飞书。它叫 log-pilot(日志助理)。整体链路:

Loki 日志系统 ──按时间窗口持续取日志──▶ 命中关键字(error/warning) ──▶ LLM 分析 ──▶ 严重问题 ──▶ 飞书通知
                                                          │
                                            └── 不是所有日志都发给大模型,只按级别过滤 ──┘
复习提示:类比:它是你"值班的日志只读员"。它不停翻接在 Loki 上的日志流,只挑出"可疑"的行(含 error / warning 等关键字的),拿去问懂运维的专家(LLM)“这行日志有多严重、该怎么办”,一旦专家觉得严重(比如数据库连接异常),它就立刻把建议发给你群的飞书机器人。异常不严重的,它就记着不发,避免打扰。

CRD 设计

LogPilot 的 spec 里需要这些参数,status 里记录上一次取日志的时间戳(用于窗口续传):

type LogPilotSpec struct {
	LokiUrl      string `json:"lokiUrl"`      // Loki 地址
	LokiPromQL   string `json:"lokiPromQL"`   // 查日志的 LogQL 语句,如查 payment 服务
	LLMEndpoint  string `json:"llmEndpoint"`  // LLM 的 OpenAI 兼容接口地址
	LLMToken     string `json:"llmToken"`     // API Key(用自托管 Ollama 可留空)
	LLMModel     string `json:"llmModel"`     // 模型名,如 gpt-4o
	FeishuWebhook string `json:"feishuWebhook"` // 飞书机器人 webhook
}

type LogPilotStatus struct {
	PreviousTimestamp string `json:"previousTimestamp"` // 上一次取日志的时间
}

Reconcile:按时间窗口去 Loki 取日志

类比:log-pilot 就像值班室的"闹钟巡检员"。它每隔 10 秒翻一次 Loki 的日志本,只把写着 error / warning 的页扯下来递给懂运维的专家(LLM),专家说"这条严重、该惊动人了",它就立刻 push 到你群里的飞书机器人;不严重的就记进本子(previousTimestamp),下次从上次记到的地方接着翻,绝不重复也绝不漏页。

对应到工程里就是:Operator 用 RequeueAfter 周期性地跑 Reconcile,时间窗口的下界是 status.previousTimestamp(续传),命中关键字才交给 LLM 分析,模型判断严重时调 sendFeishuAlert 发到飞书 webhook。

这张图在讲 log-pilot 一个完整巡检周期的决策闭环:

flowchart LR
    S[Reconcile 每10秒一轮] --> W[按时间窗口 previous,now
从 Loki 查日志] W --> X{命中 error 关键字?} X -->|无命中日志| NS[更新 previousTimestamp
等下一轮再查] X -->|有日志| LLM[OpenAI 兼容接口分析
让模型判断严重程度] LLM --> F{返回内容含"飞书"?} F -->|是| FEI[POST 飞书 webhook 告警
推送修复建议] F -->|否| NS NS --> S

每个 reconcile 周期计算一个时间窗口 [previous, now],用 previousTimestamp 续传(避免漏日志)。

now := time.Now()
previous := time.Now().Add(-5 * time.Second) // 默认最近 5 秒
if sp.Status.PreviousTimestamp != "" {
	if ts, err := strconv.ParseInt(sp.Status.PreviousTimestamp, 10, 64); err == nil {
		previous = time.Unix(0, ts*int64(time.Millisecond))
	}
}
start := previous.UnixNano() // Loki 需要纳秒级时间戳
end := now.UnixNano()
if start > end {
	// 时间反了,跳过本轮,10 秒后再试
	return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}

然后用 LogQL 拼出 Loki 的 query_range 查询地址:

lokiURL := fmt.Sprintf("%s/loki/api/v1/query_range?query=%s&start=%d&end=%d",
	sp.Spec.LokiUrl, url.QueryEscape(sp.Spec.LokiPromQL), start, end)
logs, err := queryLoki(lokiURL)

queryLoki 发一个 GET,解析返回的 data.result,再判断结果非空才进入后续分析:

func queryLoki(queryURL string) (string, error) {
	resp, err := http.Get(queryURL)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("loki status: %d", resp.StatusCode)
	}
	body, _ := io.ReadAll(resp.Body)

	var result struct {
		Data struct {
			Result []interface{} `json:"result"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return "", err
	}
	if len(result.Data.Result) == 0 {
		return "", nil // 没有命中日志,返回空
	}
	return string(body), nil
}

LLM 分析:让模型当"运维助手"

拿到日志后,调用 OpenAI 兼容接口(这里示例用 OpenAI Go SDK)分析。关键是把 prompt 写清楚:你是谁、日志是什么、要分析什么、什么情况算严重、返回格式。

func analyzeLogsWithLLM(endpoint, token, model, logs string) (analysedResult, error) {
	conf := openai.DefaultConfig(token)
	conf.BaseURL = endpoint // 重点:替换成我们的 LLM endpoint(可指向自托管 Ollama)
	client := openai.NewClientWithConfig(conf)

	resp, err := client.CreateChatCompletion(context.Background(),
		openai.ChatCompletionRequest{
			Model: model,
			Messages: []openai.ChatCompletionMessage{
				{
					Role: "user",
					Content: fmt.Sprintf(`你现在是一名运维专家,以下日志是从日志系统里获取到的日志,请分析日志错误等级。
如果遇到严重问题(例如:外部系统请求失败、外部系统故障、致命错误、数据库连接异常),请给出简短的建议。
并且对于你认为严重且需要通知运维人员的,请在内容里返回"飞书"标识。以下为日志:
%s`, logs),
				},
			},
		})
	if err != nil {
		return analysedResult{}, err
	}
	content := resp.Choices[0].Message.Content
	res := analysedResult{
		Analysis: content,
		HasError: strings.Contains(strings.ToLower(content), "飞书"),
	}
	return res, nil
}

注意 HasError 是靠模型返回内容里是否包含"飞书"关键字来判断的——是让 LLM 自己决定"这事要不要惊动运维",我们只做检查。

⚠️ 新手必踩的坑:用关键字判严重非常脆弱。① 这里用 strings.Contains(strings.ToLower(content), "飞书") 判断,是因为模型可能写"飞书"也可能写"FS"、大小写还不一定,一旦换了个说法(比如只写"推送告警"不写"飞书")就检测不到、漏报;② 时间窗口也容易反了——若 previousTimestamp 比当前时间还大(时钟回拨或状态写入出错),start > end,Loki 会查不到或报错,所以代码里要显式判断"时间反了就跳过本轮、10 秒后再试"。这类"靠约定信号串起多系统"的方案,一定要留兜底。

发飞书通知

如果分析结果命中"飞书"标识,就构造一段 text 消息发到飞书机器人的 webhook:

func sendFeishuAlert(webhook, analysis string) error {
	message := map[string]interface{}{
		"msg_type": "text",
		"content":  map[string]string{"text": analysis},
	}
	body, _ := json.Marshal(message)
	resp, err := http.Post(webhook, "application/json", bytes.NewReader(body))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("feishu status: %d", resp.StatusCode)
	}
	return nil
}

收尾与部署

每次处理完后把它写回 status.previousTimestamp(当前时间),好让下一轮续传不重复不遗漏,最后 RequeueAfter: 10 * time.Second 再查一轮。

sp.Status.PreviousTimestamp = fmt.Sprintf("%d", currentTime)
_ = r.Status().Update(ctx)
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil

部署前需要一个可查的 Loki + 一个会产生日志的示例应用。我们用自带的 IaC(Terraform/Promtail)脚本把 Loki + Promtail 起起来,再部署一个 payment 示例服务(默认几秒打印一条日志),在 Grafana 里确认能搜到业务日志(含 error 行)。然后 make install 装 CRD、apply config/samples 创建 LogPilot、make run 启动,观察它:

  1. 从 Loki 拿到含 error 的日志;
  2. 用 LLM 分析后给出建议(如"payment 服务存在错误");
  3. 把严重告警推送到飞书群。
复习提示:选型提示:这里的 llmEndpoint 当然可以填第三方的 GPT 服务;但如果你想完全私有化,就填实战二里 Kong 网关暴露的地址(它背后是 Ollama),并且可以不给 llmToken。这也是课后作业一要做的事——把实战二和实战三结合起来。

实战四:实现基于运维专家知识库故障排查 Operator

为什么 Need RAGFlow:裸调大模型不够"懂你"

类比:你问一个刚入职的实习生"公司服务器挂了怎么办",他只会背教科书里的通用步骤;但你问一个在这个公司干了五年的老运维,他会直接告诉你"先找小王看内存、再联系小李查依赖"。差别不在双方智商,而在有没有一本你们公司自己的"排障手册"。RAG 就是让大模型在回答问题前去翻一遍这本手册。

对应到工程里就是:不裸调大模型,而是请求配置了"运维专家知识库"的 RAGFlow——把内部故障解决方案写成 markdown 后向量化进知识库,让大模型基于检索到的内部方案作答,把"泛泛而谈"变成"可落地执行"。

实战三里,LLM 是靠"自己训练时学到的知识"来回答的——它并不知道你公司内部的业务约定、负责人是谁、现网有什么历史坑。所以对它来说,“外部系统请求失败"这种回答常常很泛、没法落地执行。

实战四换个思路:不裸调大模型,而是请求配置了"运维专家知识库"的 RAGFlow。我们把内部故障解决方案写进一个知识库(markdown),RAGFlow 基于它做检索增强生成(RAG),把大模型"泛泛而谈"变成"基于你业务知识的具体可执行方案”。

flowchart LR
    P[Operator
读取命名空间下 Pod 日志] --> R[RAGFlow API] R -->|检索增强| K[(运维专家知识库
内部解决方案.md)] K --> R R -->|data.answer| O[Operator] O --> F[飞书机器人]

架构上和实战三有点像,但取日志的方式和问的对象都变了:

维度实战三实战四
日志来源Loki(按 LogQL 查)直接读 Pod 的容器日志
分析引擎裸调 LLM调用 RAGFlow(背后接专家知识库)
回答质量依赖模型通用知识基于你内部凭证化方案

准备一个"运维专家知识库"

RAGFlow 本身支持 OpenAI / Ollama 等多种模型后端。我们基于 “embedding” 把一份内部运维方案 ops.md 做向量化,上传进知识库。示例内容(用来自定义负责人与处置动作):

# 服务 500 错误
- 网关层 500 错误:先检查上游服务健康,联系小王。
- 服务异常:联系小李检查服务与依赖。

# 数据库连接异常
- 连接池耗尽:检查连接配置,提高 connection limit;联系小王提高内存限制。

# memory 相关
- 内存不足导致 OOM:联系小王提高内存限制。

CRD 设计

实战四的 CRD 叫 RagLogPilot,非常简单,只需要"去哪儿取日志 + 问谁"两组参数:

type RagLogPilotSpec struct {
	WorkloadNamespace string `json:"workloadNamespace"` // 取日志的命名空间
	RagflowEndpoint   string `json:"ragflowEndpoint"`   // RAGFlow API 地址(带 /v1/api 后缀)
	RagflowToken      string `json:"ragflowToken"`      // RAGFlow API Key
}

(RAGFlow 用到的两个核心接口:创建对话 返回 data.id 作为 conversation id;获取回复 传 conversation_id + message,从返回的 data.answer 里拿答案。)

从 Pod 取日志并按关键字过滤

因为要读命名空间下所有 Pod 的容器日志,需要在 RBAC 里给 Pod 的 get / list 权限。核心方法 getAllPodLogs:

func getAllPodLogs(ctx context.Context, client client.Client, ns string) (string, error) {
	// 1. 列出该 namespace 下所有 Pod
	var pods corev1.PodList
	if err := client.List(ctx, &pods, client.InNamespace(ns)); err != nil {
		return "", err
	}
	// 2. 对每个 Pod 取容器日志(用 controller-runtime 的 client 直接取)
	var sb strings.Builder
	for _, p := range pods.Items {
		req := client.GetLogs(ns, p.Name, &corev1.PodLogOptions{Container: p.Spec.Containers[0].Name})
		rc, err := req.Stream(ctx)
		if err != nil {
			continue
		}
		data, _ := io.ReadAll(rc)
		rc.Close()
		sb.Write(data)
	}
	// 3. 只保留我们关心的行(含 error 等关键字)
	var lines []string
	for _, line := range strings.Split(sb.String(), "\n") {
		if strings.Contains(strings.ToLower(line), "error") {
			lines = append(lines, line)
		}
	}
	return strings.Join(lines, "\n"), nil
}

请求 RAGFlow 做"知识库问答"

拿到日志后调用 queryRagSystem:先创建对话拿到 conversationId,再发消息(拼接日志 + 指示"请基于专家知识库解答,不知道就说不知道",并设置 stream=false 让它处理完再返回),最后取 data.answer:

func queryRagSystem(endpoint, token, logs string) (string, error) {
	const kbPrompt = `以下是获取到的应用日志,请基于运维专家知识库进行解答:
%s
请基于我们内部的专家知识库给出解决方案。如果知识库中没有相关信息,请明确回答"不知道"。`

	// 1. 创建对话,拿到 conversationId
	convURL := endpoint + "/conversation"
	convBody, _ := json.Marshal(map[string]string{"user": "aidesk"})
	req, _ := http.NewRequest(http.MethodPost, convURL, bytes.NewReader(convBody))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	var cr struct {
		Data struct{ ID string `json:"id"` } `json:"data"`
	}
	_ = json.Unmarshal(b, &cr)
	conversationID := cr.Data.ID

	// 2. 基于这个对话发消息,取 data.answer
	chatBody, _ := json.Marshal(map[string]interface{}{
		"conversation_id": conversationID,
		"message":         fmt.Sprintf(kbPrompt, logs),
		"stream":          false, // 非流式,等它回答完再取
	})
	req2, _ := http.NewRequest(http.MethodPost, endpoint+"/chat", bytes.NewReader(chatBody))
	req2.Header.Set("Authorization", "Bearer "+token)
	req2.Header.Set("Content-Type", "application/json")
	resp2, err := http.DefaultClient.Do(req2)
	if err != nil {
		return "", err
	}
	defer resp2.Body.Close()
	b2, _ := io.ReadAll(resp2.Body)
	var ar struct {
		Data struct{ Answer string `json:"answer"` } `json:"data"`
	}
	if err := json.Unmarshal(b2, &ar); err != nil {
		return "", err
	}
	return ar.Data.Answer, nil
}

拿到 answer 后,复用实战三的 sendFeishuAlert 把答案推到飞书(这里为了演示也可以硬编码一个飞书机器人 webhook)。Reconcile 末尾 RequeueAfter: 30 * time.Second 周期性重查。

RAGFlow 侧的配置还原

  1. 用准备好的 shell 脚本开通一台腾讯云虚拟机,安装 docker、克隆 RAGFlow,用 docker compose 启动(镜像较大,首次启动耗时较长)。
  2. 打开 RAGFlow 管理台 → 注册/登录 → 知识库里新建"运维专家知识库",选中文、团队。
  3. 必须先配置模型供应商(拿 API Key,如 openai 兼容端点),否则无法做 embedding;配置好后再上传 ops.md,点击启动 embedding,等待解析完成。
  4. 在"聊天"里新建助理(如"运维专家知识库"),选择该知识库,模型设置选择 OpenAI 兼容的模型(如 gpt-4o-mini)。
  5. 手动测一下:“服务 500 错误应该找谁?” 应能从知识库返回"联系小王"这类答案,确认知识库生效。
  6. 在 API 菜单里创建一个 API Key,把它填到 RagLogPilot.spec.ragflowToken(endpoint 记得带 /v1/api 后缀)。

运行与验证

make install 装 CRD,apply config/samples(workloadNamespace=default,endpoint / token 填好),make run 启动。部署一个会打印 error 的示例应用,观察 Operator:

  1. 从 default 命名空间的 Pod 拿到含 error 的日志;
  2. 请求 RAGFlow,基于专家知识库返回答案(如"xxx 错误,联系小李检查服务……"、“内存 OOM,联系小王提高内存限制”);
  3. 把答案推送到飞书。
复习提示:常见报错排错:① 拼接请求地址时 RAGFlow 的 endpoint 要带 /v1/api 之类后缀,否则 404;② 如果报 “message”(单词拼错成 mesage)解析报错,通常是 body JSON 的字段名敲错、解析空指针——先 json.Unmarshal 一格一层打印出来排查(比如 data.answer 的层级)。这些都是实战里真实踩过、靠打印 result 才定位到的。

总结

这章用四个 Operator 串起来一条"用 Operator 把 AIOps 做落地"的主线,每节都在前一节基础上叠加一个能力:

  • 实战一盘点基础资产:开发 SpotPool Operator 维护 GPU 竞价实例资源池——用 CRD 声明最小/最大实例数,用 RequeueAfter 周期巡检,用腾讯云 CVM SDK 自动补位被释放的竞价格 GPU。竞价实例约 8 倍的价格差,是自托管大模型的高性价比选型,代价是"随时被回收",正好让 Operator 来闭环。
  • 实战二补上推理能力:用 Ollama(OpenAI 兼容 /v1/chat/completions)做私有推理,用自定义虚拟机镜像让新实例"启动即 ready",用 Kong 网关做多 GPU 加权轮询负载均衡,并让 Operator 实时把实例 IP 同步到 Kong 的 upstream(新实例加入、被释放实例剔除)。
  • 实战三让机器学习替你盯日志:log-pilot 周期性从 Loki 按时间窗口取日志,命中关键字交给 LLM 分析错误等级并给建议,严重问题自动推送飞书,status.previousTimestamp 保证续传不丢日志。
  • 实战四让回答"懂你业务":rag-log-pilot 改从 Pod 读日志,调用配置了运维专家知识库的 RAGFlow,把"模型泛泛而谈"升级成"基于你内部方案的落地处置",再推送到飞书。

这一章真正要体会的是:竞价实例让"算力很贵"这件事便宜了下来,但带来了"随时被回收"的新不确定性;而 Operator 的声明式 + 周期性 reconcile,正是把这种不确定性包起来、让它对外表现为"一个稳定资源池"的关键手段。 把 LLM/RAG 接进来,就是把 AI 的"分析判断"能力也编排进 K8s 控制平面,这就是 AIOps 的核心形态之一。


自测题与动手练习

面试官
为什么要用竞价实例作为自托管大模型的算力选型?它相比按量计费有优势,但也有致命弱点,是什么?
候选人
竞价实例的 CPU / 内存 / GPU 计价便宜(例如 GN7 主机整机报价可降到按量计费的约 1/8),磁盘、带宽仍是正价,整体对"长期占用一批 GPU 做私有化推理"非常划算。
弱点是随时会被云厂商回收:当库存竞争/更高优先级建机请求进入时,云厂商为了把资源卖给出价更高的人,可能随时把你的竞价实例释放掉,导致业务中断。所以我们用 Operator 自动巡检、补位,把"可能少一台"变成"一旦少一台就自动补一台"。
面试官
SpotPool Operator 是怎么做到"维持资源池实例数在声明的区间内"的?周期巡检靠什么机制实现?
候选人
CRD 声明了 MinInstances / MaxInstances,Reconcile 里查询腾讯云当前运行实例数 runningCount,然后分三种情况:runningCount < min 就 runInstance 按差值补齐;runningCount > max 就 terminateInstance 按差值退还;符合区间则不动。
周期巡检靠 RequeueAfter(本例约 40 秒):Reconcile 不立即完成,而是告诉 controller-runtime 40 秒后再触发一轮,从而形成固定周期的"补位巡检"。实例名带时间戳便于识别新补出的机器。
面试官
实战一里讲了几个容易踩的配置坑?分别是什么?
候选人
① securityGroupId(复数 groups)容易拼错,会在 Schema/部署时报 400;
② VPC 所在区域和实例可用区必须匹配,否则报"VPC 所在区域实例所在区不匹配"——要先在控制台确认子网可用区与 availableZone 一致;
③ 竞价实例有配额限制,一次性开太多会提示"竞价实例超限,请提交工单提升配额",这是配额问题而非逻辑错误,说明创建请求已经生效;
④ 新实例的公网 IP 是异步分配的,查询时可能还没拿到,需要靠"未分配到公网 IP 就返回错误、稍后重试"来兜底。
面试官
Ollama 的接口为什么方便私有化接入?多实例负载均衡是怎么做到高吞吐的?
候选人
Ollama 提供了兼容 OpenAI 的 /v1/chat/completions 接口,数据结构(model / messages)与 OpenAI 一致。接 OpenAI / langchain 等 SDK 时只需把 baseURL 指向 Ollama,其他代码几乎不用改,Kong 这类网关也能当普通 REST 后端转发。
多实例负载均衡:Kong 做加权轮询,把并发推理请求分发到多台 GPU 实例(请求 A→GPU1、请求 B→GPU2……),QPS 远高于单实例排队串行,是私有化高吞吐推理的常用做法。
面试官
Kong 网关在实战二里扮演什么角色?为什么必须让它的 Admin API 监听在 0.0.0.0 上?
候选人
Kong 是负载均衡 / 流量网关:外部请求经它(8000 端口)分发到多台 Ollama 实例。它的 Admin API(8001 端口) 提供 REST 接口,可动态创建/删除 upstream、service、route、target——这正是 Operator 控制负载均衡表的入口。
必须监听在 0.0.0.0,是让 Operator 能通过公网 IP 调用 Admin API来动态同步 upstream;如果只监听本地回环地址,Operator 就够不着它的控制接口了。
面试官
新补开的实例为什么要用"自定义虚拟机镜像"而不是脚本初始化?syncKongUpstream 具体做了哪些事?
候选人
若用脚本初始化,每台实例都要走"装 Ollama + 下载模型(动辄几 GB)+ 启动服务",一轮要十几分钟。用自定义虚拟机镜像(先把一台实例配好 Ollama 自启动 + 预下拉模型,再做成镜像),之后补开的新实例直接指定该镜像 ID,启动即 ready 且监听在 8080 端口,可直接接后端流量。
syncKongUpstream 做的是"CRD 实例列表 ↔ Kong upstream target 对齐":GET 当前 targets → 遍历实例构造目标 IP 集 → 对"在 targets 但实例已释放"的 target 执行 DELETE → 对"在实例但 targets 没有"的 IP POST 新增 target,实现自动同步。
面试官
实战三(log-pilot)是怎么做到"不漏日志、不重复分析"的?它是如何判断要不要发飞书的?
候选人
用 status.previousTimestamp 记录上一次取日志的时间,每个 reconcile 周期构造时间窗口 [previous, now],下次从上次结束点继续,从而实现续传、不重不漏;每轮结束把当前时间写回 previousTimestamp,再 RequeueAfter 短周期循环。
“要不要发飞书"由 LLM 判断:prompt 里让模型对严重问题(外部系统请求失败/故障、致命错误、数据库连接异常等)返回"飞书"标识,Operator 检查返回内容是否含"飞书"关键字(区分大小写用 ToLower),命中才调 sendFeishuAlert。
面试官
实战四相比实战三,在"提问对象"上做了什么升级?为什么能给出更落地的答案?
候选人
实战三裸调大模型,模型靠"训练时学到的通用知识"回答,不懂你内部业务的负责人、历史坑;实战四把日志交给配置了运维专家知识库的 RAGFlow,通过检索增强生成(RAG)基于内部 markdown 方案作答。
因为答案来自 RAGFlow 检索到的专家知识库(比如"服务 500 找谁、OOM 谁排查、提高内存上限"),所以给的是"基于你业务的可执行方案",而不是模型泛泛而谈的通用建议。取日志方式也从 Loki 换成了直接读 Pod 容器日志。

动手练习(建议真做一遍):

  1. 实战一:初始化 spot-pool Operator,填好 min=2 / max=2 的真实腾讯云参数,跑通"检测到少一台 → 自动补开一台";再手动创建一台,验证 runningCount > max 时会被自动终止(放宽到 max=3)。
  2. 实战二:用脚本做出"带 Ollama + qwen2"的自定义镜像,部署 Kong(Admin 监听 0.0.0.0),改造 Operator 的 syncKongUpstream;手工释放一台实例,观察下一轮 reconcile 后 Kong upstream 的 target 被替换的时效,并尝试把"坏 IP"的 target 权重改成 0 的优化。
  3. 实战三选作:修改 log-pilot,把 llmEndpoint 指向实战二的 Kong 网关地址(背后是 Ollama),去掉 llmToken,实现"完全自托管"的日志监测——等于把实战二和实战三串起来。
  4. 实战四选作:修改 rag-log-pilot 的 RAGFlow 模型供应商,配置成 Ollama(OpenAI 兼容后端,RAGFlow 原生支持),让整条"知识库故障排查"也完全走本地自托管推理,验证 RAGFlow 能正常出答案。

本章小结

复习提示:
  • 竞价实例 = 高性价比 + 高不确定性:用约 1/8 的价格拿 GPU,代价是"随时被释放"。Operator 的 RequeueAfter 周期巡检 + 自动补位,把它包装成"始终稳定"的资源池。
  • SpotPool CRD:spec 里放"开通云虚拟机所需的全部参数 + 最小/最大实例数",status 里放当前实例列表,Reconcile 对比查询结果和 min/max 后决定 runInstance 还是 terminateInstance。排查重点:securityGroupId 复数、VPC/可用区匹配、竞价配额、公网 IP 异步分配。
  • 多实例私有推理:Ollama 的 OpenAI 兼容 /v1/chat/completions 让接入极其方便;用自定义虚拟机镜像让新实例"启动即 ready(自带 Ollama + 模型,监听 8080)";Kong 网关做加权轮询负载均衡,Admin API(监听 0.0.0.0)让 Operator 动态同步 upstream(新实例加入 / 释放实例剔除)。
  • log-pilot(LLM 日志监测):按 status.previousTimestamp 续传地从 Loki 取日志,命中关键字后交 LLM 分析错误等级与建议(prompt 让模型对严重问题返回"飞书"标识),严重即发飞书,RequeueAfter 短周期循环。
  • rag-log-pilot(RAG 故障排查):改从 Pod 读日志、按 error 过滤,请求配置了内部专家知识库的 RAGFlow(建会话拿 conversationId → 取 data.answer),把"模型通用回答"升级为"基于你业务落地方案",再推飞书。
  • 课后作业方向:把实战二(Ollama 自托管)与实战三/四结合,让日志监测和知识库故障排查也 100% 走本地私有化推理,是完整打通 AIOps 私有栈的一步。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!