AI Infra 训练营
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
HiHuo 主站
GitHub
总览
  • Day 1 · 集群起步 + CNI
  • Day 2 · 控制面 + etcd
  • Day 3 · CRD + Operator + Webhook
  • Day 4 · 存储深度
  • Day 5 · 卷扩容 + 安全
  • Day 6 · 调度 + 可观测
  • Day 7 · Harbor + ArgoCD + Mesh
  • Day 8 · AI Infra
  • Day 9 · Triton + GPU
  • Day 10 · MIG + HPA + 量化
  • Day 11 · AI Agent 端到端
  • Day 12 · 灾备
  • Day 13 · Operator + 联邦 + Mesh + RAG
  • Day 14 · CKA / CKS + 总结
  • LLM 训练手册
  • RAG + Agent 手册
  • 推理优化手册
  • 上下文工程手册
  • Agent 开发手册
  • 面试深度复盘
  • 训练 v2 深度手册
HiHuo 主站
GitHub
  • Bonus 手册

    • Bonus 专栏:LLM 训练全景手册(框架 / 训练模式 / 输入输出 / 最简测试)
    • Bonus-2 · RAG / Agent 全景实战手册
    • Bonus-3 · LLM 推理优化全景实战手册
    • Bonus-4 · LLM 上下文长度原理全景手册
    • Bonus-5 · Agent 开发原理与面试准备手册
    • Bonus-6 · 面试深挖项实战手册
  • 扩展

    • 训练专栏 v2 · LLaMA-Factory / DeepSpeed / Megatron 深度调参手册

Bonus-6 · 面试深挖项实战手册

🎯 目标:针对简历声称但准备不足的"埋雷点",在 gpu1 集群环境里真跑一遍,把每个埋雷拆成"原理 + 实测命令 + 实测输出 + 深问应答"四段式,直接当面试预演脚本用。 📅 实测日期:2026-05-27 02:30 起 🖥️ 环境:m1 (3 control plane + 2 worker, K8s 1.30) + gpu1 (k3s + A800-40GB) + vLLM qwen2.5-3b


0. 本文档怎么用

  • 每章 1 个埋雷点。原理 → 命令 → 输出 → 应答四段式
  • "应答"是面试官追问时你可以一字不漏说出来的标准答案
  • 所有命令都在 gpu1 / m1 集群 上跑通,输出为真实截取
  • 跟 Bonus-3/4/5 配套读

1. 埋雷 #5 · vLLM 指标体系 + LLM HPA 信号选型

1.1 为什么必问

简历职业技能 #6 写了 "vLLM 推理服务 + LLM 服务的 HPA 信号选型(队列深度 vs CPU)"。面试官一看就知道你跑过 vLLM,必然追问:

  • vLLM 暴露哪些指标?
  • 为什么不用 CPU 触发 HPA?
  • queue depth 在 vLLM 哪个 metric?
  • 怎么让 HPA 能读到这个 metric?

1.2 原理 — vLLM 暴露的指标分类

vLLM 把 OpenAI server 跟一套 Prometheus exporter 内嵌一起。/metrics 端点(默认 8000 port)直接暴露 Prometheus 格式。

实测命令:

curl http://10.43.165.182:8000/metrics | grep ^# HELP | awk '{print $3}' | sort -u

实测输出 — 全 29 个指标按类分组:

类别指标数据类型含义
请求队列vllm:num_requests_runningGauge正在 decode 的请求数
vllm:num_requests_waitingGauge排队中 ← HPA 黄金信号
vllm:num_requests_swappedGaugeKV cache 被 swap 到 CPU 的请求数
KV cachevllm:gpu_cache_usage_percGaugeGPU KV cache 使用率(0-1)
vllm:cpu_cache_usage_percGaugeCPU swap 区使用率
vllm:gpu_prefix_cache_hit_rateGaugeprefix cache 命中率(开 prefix cache 时才有意义)
vllm:cpu_prefix_cache_hit_rateGauge同上 CPU 侧
吞吐(瞬时)vllm:avg_prompt_throughput_toks_per_sGauge滑动平均 prompt tok/s
vllm:avg_generation_throughput_toks_per_sGauge滑动平均 generation tok/s
累计计数vllm:prompt_tokens_totalCounter历史 prompt token 累计
vllm:generation_tokens_totalCounter历史 generation token 累计
vllm:num_preemptions_totalCounter历史抢占次数(decode 被中断让位给 prefill)
vllm:request_success_totalCounter历史成功请求数
延迟直方图vllm:time_to_first_token_secondsHistogramTTFT 直方图
vllm:time_per_output_token_secondsHistogramTPOT 直方图
vllm:e2e_request_latency_secondsHistogram端到端延迟
vllm:request_queue_time_secondsHistogram排队等待时间
vllm:request_prefill_time_secondsHistogramprefill 阶段耗时
vllm:request_decode_time_secondsHistogramdecode 阶段耗时
vllm:request_inference_time_secondsHistogram推理总时间(prefill + decode)
vllm:time_in_queue_requestsHistogram(新版) 队列驻留时间
请求分布vllm:request_prompt_tokensHistogram单请求 prompt 长度分布
vllm:request_generation_tokensHistogram单请求 generation 长度分布
vllm:request_params_nHistogramn 参数分布(并行生成数)
vllm:request_params_max_tokensHistogrammax_tokens 参数分布
vllm:request_max_num_generation_tokensHistogram实际生成 token 最大值
vllm:iteration_tokens_totalHistogram单 iteration 处理的 token 数
元数据vllm:cache_config_infoInfoKV cache 配置(block size 等)
vllm:lora_requests_infoInfoLoRA adapter 信息

1.3 实测 gpu1 上的真实快照(2026-05-27)

vllm:num_requests_running{model_name="qwen2.5-3b"}       0.0      ← 当前空闲
vllm:num_requests_waiting{model_name="qwen2.5-3b"}       0.0      ← 无排队
vllm:num_requests_swapped{model_name="qwen2.5-3b"}       0.0
vllm:gpu_cache_usage_perc{model_name="qwen2.5-3b"}       0.0      ← KV 全清
vllm:prompt_tokens_total{model_name="qwen2.5-3b"}        6179.0   ← 今天累计
vllm:generation_tokens_total{model_name="qwen2.5-3b"}    9397.0   ← 今天累计
vllm:time_to_first_token_seconds_sum                     4.249    ← 今天累计
vllm:time_to_first_token_seconds_count                   89.0     ← 89 个请求
                                                                  ← avg TTFT = 47.7ms
vllm:e2e_request_latency_seconds_sum                     213.79   ← 累计 e2e
                                                                  ← avg e2e = 2.40s

TTFT 直方图分布(2026-05-27 实测累计 89 请求):

le=0.04s    41   ← 46% 的请求 TTFT < 40 ms
le=0.06s    62   ← 70%       < 60 ms
le=0.08s    74   ← 83%       < 80 ms
le=0.10s    89   ← 100%      < 100 ms (全部命中)

→ p99 TTFT < 100 ms,p50 ~ 40 ms。这就是 Bonus-3 benchmark 的"低并发"实测面貌。

1.4 深问应答:为什么不用 CPU/Memory 触发 HPA?

面试官: "你简历说 LLM HPA 信号选型用队列深度而不是 CPU,为什么?"

你的标准答案:
1. CPU 不反映 LLM 的真实负载. LLM 推理是 GPU-bound,decode 阶段
   GPU 满载但 CPU 利用率往往只有 20-30%. 用 CPU 触发 HPA 永远不会扩.
2. Memory 同样不行. KV cache 在 GPU 显存,不算进 container memory,
   container memory 主要是 model weights 静态占用,没动态信号.
3. 真正的负载信号有三个候选:
   a) num_requests_waiting (队列深度) - 直接反映用户等待
   b) gpu_cache_usage_perc - 反映 KV cache 接近爆
   c) time_in_queue_requests p95 - 反映 SLO 是否被破坏
4. 工业首选 num_requests_waiting 因为:
   - Gauge 类型, HPA 可以直接平均
   - 物理含义清晰: > 0 说明已经堵, > N 说明严重堵
   - 跟 SLO 强相关
5. 我在 bootcamp Day 11 的 LLMService Operator 里就是这么做的,
   HPA External Metric type=AverageValue, targetValue=2,
   表示平均每个 replica 排队 > 2 个就扩.

1.5 深问应答:vllm:num_requests_waiting 怎么进 HPA?

面试官: "vllm 暴露的指标,HPA 怎么用?"

链路 (4 跳):
1. vLLM Pod 8000/metrics 暴露 Prometheus 格式
2. ServiceMonitor (Prometheus Operator CR) 让 Prometheus scrape
3. prometheus-adapter 监听 custom.metrics.k8s.io API,
   通过 PromQL 把 vllm:num_requests_waiting 转换成
   k8s metric 名 (e.g. vllm_requests_waiting)
4. HPA spec.metrics 写 type: External / Pods,
   metricName 指向 adapter 暴露的名字

完整 HPA spec 示例(简历 Operator 里就该用这个):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vllm-3b
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-3b
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metric:
        name: vllm_num_requests_waiting     # prometheus-adapter 映射出的名字
      target:
        type: AverageValue
        averageValue: "2"                   # 每 pod 平均排队 > 2 就扩
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300       # 防抖动
    scaleUp:
      stabilizationWindowSeconds: 60

1.6 反击式问题(面试官追问的"陷阱"+ 你的反杀)

面试官陷阱你的反杀
"队列深度有什么缺点?""队列为 0 时 HPA 缩到 minReplicas,首请求会经历冷启动(尤其 vLLM 7B 加载要 30s+),所以一般 minReplicas >= 1,并配 PreStop hook 让 graceful drain"
"TTFT 直方图你怎么用?""用 histogram_quantile(0.95, rate(...)[5m]) 算 p95,接 AlertManager 在 p95 > 1s 时报警"
"vLLM v1 引擎跟 v0 指标有什么变化?""v1 新增了 vllm:iteration_tokens_total / vllm:time_in_queue_requests / prefix cache hit rate;我装的是 v0.6.5 这两类都有"

2. 埋雷 #9 · MCP (Model Context Protocol) 协议级实战

2.1 为什么必问

HopClaw 简历声称 "plugin / MCP / capability bundle 三种扩展机制"。面试官追问:

  • MCP 是什么协议?
  • 你实现的 MCP server / client 是哪一种 transport?
  • MCP 跟 OpenAI Function Calling 关系?
  • 自己跑过 MCP server 吗?

2.2 原理 — MCP 三句话讲完

MCP (Model Context Protocol) 是 Anthropic 2024-11 推出的 工具/数据/能力对 LLM Agent 的标准化协议。

它把 "Agent 主体" 和 "工具/数据提供方" 解耦:工具方实现 MCP Server,Agent 方作为 Client 连接,一份工具实现多个产品复用(Claude Desktop、Claude Code、Cursor 等)。

协议本身是 JSON-RPC 2.0,默认 transport 是 stdio(子进程),另支持 SSE / WebSocket。

2.3 协议核心 message 类型(2025-11-25 spec)

Message方向用途
initializeClient → Server握手,协商 capabilities + 协议版本
initialized (notification)Client → Server握手完成通知
tools/listClient → Server拿所有可用工具的 schema
tools/callClient → Server调用工具
resources/listClient → Server拿可读资源(文件、DB schema 等)
resources/readClient → Server读资源内容
prompts/listClient → Server拿预制 prompt 模板
prompts/getClient → Server拿具体 prompt + 参数
sampling/createMessageServer → Clientserver 反向让 client 帮它调 LLM(!!)
notifications/cancelled双向取消任务
notifications/progressServer → Client长任务进度上报

2.4 三种 Transport

Transport适用优势劣势
stdio (subprocess)本地工具0 网络配置, 进程隔离只能本机
SSE (Server-Sent Events)远程服务HTTP-friendly, server 主动推送单向 ↓,client 用 POST 上行
WebSocket双向需求全双工部署复杂(防火墙/Ingress)

工业实践:90% 是 stdio,SSE 用于云端共享 server,WebSocket 偶见。

2.5 实测 — Server 实现

/opt/mcp_demo/mcp_server.py (核心 30 行):

import asyncio
import datetime
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

app = Server("bonus6-demo-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(name="add",
             description="Add two integers and return the sum.",
             inputSchema={"type": "object",
                          "properties": {"a": {"type": "integer"},
                                         "b": {"type": "integer"}},
                          "required": ["a", "b"]}),
        Tool(name="multiply", description="...",
             inputSchema={...}),
        Tool(name="get_time", description="...",
             inputSchema={"type": "object", "properties": {}, "required": []}),
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "add":
        return [TextContent(type="text", text=str(arguments["a"] + arguments["b"]))]
    if name == "multiply":
        return [TextContent(type="text", text=str(arguments["a"] * arguments["b"]))]
    if name == "get_time":
        return [TextContent(type="text", text=datetime.datetime.now().isoformat())]
    raise ValueError(f"unknown tool {name}")

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

2.6 实测 — Client 调用

/opt/mcp_demo/mcp_client.py (核心 25 行):

import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client

async def main():
    params = StdioServerParameters(command="python3",
                                   args=["/opt/mcp_demo/mcp_server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            init = await session.initialize()
            print(f"server: {init.serverInfo.name} v{init.serverInfo.version}")
            print(f"protocol: {init.protocolVersion}")

            tools = await session.list_tools()
            for t in tools.tools:
                print(f"  - {t.name}: {t.description}")

            r = await session.call_tool("add", {"a": 3, "b": 4})
            print(f"add(3,4) = {r.content[0].text}")

asyncio.run(main())

2.7 实测输出(2026-05-27 02:33 真实截取)

============================================================
MCP Client → spawn server as subprocess (stdio transport)
============================================================

[1] initialize OK
    server name:    bonus6-demo-server
    server version: 1.27.1
    protocol ver:   2025-11-25         ← 最新 spec

[2] tools/list  →  3 tools:
    - add: Add two integers and return the sum.
      params: ['a', 'b']
    - multiply: Multiply two integers.
      params: ['a', 'b']
    - get_time: Get current server time.
      params: []

[3] tools/call  add(3, 4)
    result: 7

[4] tools/call  multiply(6, 7)
    result: 42

[5] tools/call  get_time()
    result: 2026-05-27T02:33:52.219248

[6] tools/call  nonexistent()  ← expect error
    expected error: isError=True, text='unknown tool nonexistent'
    (注意: MCP 错误不是 JSON-RPC error response,
     而是 isError=True 的 TextContent — 这样模型能"看到"错误)

============================================================
✅ MCP session closed cleanly (subprocess terminated)
============================================================

2.8 深问应答:MCP 跟 OpenAI Function Calling 的关系

面试官: "既然有 OpenAI Function Calling, 为什么还需要 MCP?"

你的标准答案:
1. 二者抽象层不同:
   - Function Calling 是 模型 ↔ Agent 的协议 (JSON Schema)
   - MCP        是 Agent ↔ Tool Provider 的协议 (JSON-RPC)
   两者是 互补不冲突, 一个 Agent 可以同时用 FC + MCP.

2. 关键差异:
   - FC schema 是 静态的, 由 Agent 维护在 prompt 里
   - MCP server 动态注册, 一份实现 N 个产品复用
     (我写的 calculator MCP server 可以同时接 Claude Code / Cursor / 自己的 agent)

3. MCP 解决 FC 没解决的三个问题:
   - 工具 跨 Agent 复用 (生态价值)
   - Server 反向调 LLM (sampling/createMessage) - 工具可以"问问 client"
   - Resources / Prompts 抽象 (不止 tools)

4. 工业现状 (2025-2026):
   - Anthropic 全家桶 (Claude Desktop / Code) 原生支持
   - Cursor / Continue.dev / Cline 都已支持
   - OpenAI 还没正式 endorse, 但社区在做兼容

2.9 反击式问题(陷阱 + 反杀)

面试官陷阱你的反杀
"stdio transport 怎么处理并发请求?""stdio 是双工管道, JSON-RPC 用 id 做请求匹配, 多个请求可同时 inflight, response 按 id 路由回对应的 client future"
"MCP server 怎么做认证?""stdio 用进程边界做隔离(谁起的子进程谁拥有信任);SSE/WebSocket 走 HTTP header (Authorization / X-API-Key), spec 2025+ 加了 OAuth 2.1 flow"
"MCP server 挂了 Agent 怎么办?""Client 监听 stdio 关闭 → 抛 ConnectionError → Agent 把这条工具标 unavailable, 调度其他工具或降级到 final answer"
"你的 HopClaw 里 MCP 跟 plugin 边界在哪?""Plugin 是进程内的 Go interface, 直接 import; MCP 是进程外的 RPC, 跨语言/跨进程。Plugin 性能高耦合紧, MCP 解耦强但有 IPC 开销, capability bundle 是声明式打包 N 个 plugin/mcp 一起"

2.10 我跟工业 Agent 的对接路径(简历加分项)

你写的 MCP server 可以直接被 Claude Desktop / Claude Code 用。

配置 (Claude Desktop):

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "bonus6-demo": {
      "command": "python3",
      "args": ["/opt/mcp_demo/mcp_server.py"]
    }
  }
}

面试时可以说:"我写的 MCP demo 既能挂到自己的 HopClaw 也能挂到 Claude Desktop,这就是 MCP 跨生态复用的真实落地"。



3. 埋雷 #1 · Admission Webhook 端到端实战

3.1 为什么必问

简历职业技能 #3 写了 "Admission Webhook"。这是 K8s 扩展机制 4 件套(CRD/Operator/Webhook/Custom Metrics)里唯一你没在 bootcamp 真跑过的,面试官追问会卡。

3.2 原理 — Webhook 两类 + 完整调用链

        kubectl apply -f pod.yaml
                ↓
        kube-apiserver
                ↓
   Authentication (谁在调用)
                ↓
   Authorization (能不能调用 / RBAC)
                ↓
   Mutating Admission ←─── MutatingWebhookConfiguration
                ↓ (object 可能被改写)
   Object Schema Validation
                ↓
   Validating Admission ←── ValidatingWebhookConfiguration
                ↓ (拒绝/通过, 不能改 object)
   Persistence (etcd)
                ↓
   Watch / Controller
类型用途能不能改 object何时跑
Mutating注入 sidecar / 默认标签 / 默认 resources✅ 改在 Validating 之前
Validating拒绝/通过❌ 只能 reject最后,所有 mutate 之后

3.3 Webhook 协议 — AdmissionReview JSON

API Server 给 webhook 发的是 AdmissionReview v1 JSON:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "abc-123",                  // 请求唯一 ID
    "kind": {"group":"","version":"v1","kind":"Pod"},
    "resource": {...},
    "name": "bad-pod",
    "namespace": "webhook-test",
    "operation": "CREATE",              // CREATE/UPDATE/DELETE/CONNECT
    "userInfo": {...},
    "object": {                         // 完整的 Pod spec
      "metadata": {...},
      "spec": {"containers": [{"image":"nginx:latest"}]}
    },
    "oldObject": null                   // UPDATE 时是 old version
  }
}

Webhook 必须返回:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "abc-123",                  // 必须和 request.uid 相同
    "allowed": false,                   // true / false
    "status": {"message": "deny reason"},
    "patch": "base64(jsonpatch)",       // Mutating 时可填
    "patchType": "JSONPatch"
  }
}

3.4 实战 — 在 gpu1 k3s 上跑一个 "deny :latest" webhook

Step 1: 生成自签 CA + Server Cert (IP SAN)

HOST_IP=$(hostname -I | awk '{print $1}')          # 192.168.122.6

# CA
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout ca.key -out ca.crt -subj "/CN=webhook-demo-ca"

# Server cert with IP SAN (重点: 必须有 SAN, 老 CN-only 现在被 K8s 拒绝)
cat > server.cnf <<EOF
[req]
distinguished_name = dn
req_extensions = ext
prompt = no
[dn]
CN = webhook-server
[ext]
subjectAltName = IP:$HOST_IP, IP:127.0.0.1
EOF
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config server.cnf
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 365 -extensions ext -extfile server.cnf

# CA base64 (要填到 ValidatingWebhookConfiguration.caBundle)
base64 -w0 ca.crt

Step 2: 写 Webhook Server (Python 标准库,~50 行)

/opt/webhook_demo/webhook.py:

import json, http.server, ssl, sys

class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, fmt, *args): return    # 抑制默认 log
    def do_POST(self):
        length = int(self.headers.get("content-length", 0))
        review = json.loads(self.rfile.read(length))
        req = review["request"]
        uid = req["uid"]
        kind = req["kind"]["kind"]

        # 校验逻辑
        allowed, msg = True, "ok"
        if kind == "Pod":
            for c in req["object"]["spec"].get("containers", []):
                img = c.get("image", "")
                tag = img.split(":")[-1] if ":" in img else "latest"
                if tag == "latest":
                    allowed = False
                    msg = "deny: container " + c["name"] + " uses image " + repr(img)
                    break

        response = {
            "apiVersion": "admission.k8s.io/v1",
            "kind": "AdmissionReview",
            "response": {"uid": uid, "allowed": allowed, "status": {"message": msg}},
        }
        data = json.dumps(response).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(data)

httpd = http.server.HTTPServer(("0.0.0.0", 8443), Handler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(certfile="server.crt", keyfile="server.key")
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()

启动:

nohup python3 /opt/webhook_demo/webhook.py > /opt/webhook_demo/webhook.log 2>&1 &

Step 3: 注册 ValidatingWebhookConfiguration

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: deny-latest-image
webhooks:
- name: deny-latest.bonus6.local
  admissionReviewVersions: ["v1"]
  sideEffects: None                    # 必填, 声明无副作用 (调用不改外部状态)
  timeoutSeconds: 5                    # 关键: 短超时, 防 webhook 挂导致集群卡
  failurePolicy: Fail                  # Fail = 调不通就 deny; Ignore = 调不通就放行
  namespaceSelector:
    matchExpressions:
    - key: kubernetes.io/metadata.name
      operator: NotIn
      values: ["kube-system", "kube-public", "vllm", "default"]
                                       # 关键: 千万别拦 kube-system, 不然集群组件起不来
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  clientConfig:
    url: https://192.168.122.6:8443/validate     # 用 url 不用 service (因为我们跑在 host)
    caBundle: LS0tLS1CRUdJTi...                  # 上面 base64 -w0 ca.crt 的输出

kubectl apply -f vwc.yaml

Step 4: 实测三个 case

$ k3s kubectl create namespace webhook-test
namespace/webhook-test created

$ k3s kubectl run bad-pod --image=nginx:latest -n webhook-test --restart=Never
Error from server: admission webhook "deny-latest.bonus6.local" denied the request:
  deny: container bad-pod uses image 'nginx:latest' with latest/no tag    ✅

$ k3s kubectl run good-pod --image=nginx:1.25 -n webhook-test --restart=Never
pod/good-pod created                                                       ✅

$ k3s kubectl run no-tag --image=redis -n webhook-test --restart=Never
Error from server: admission webhook "deny-latest.bonus6.local" denied the request:
  deny: container no-tag uses image 'redis' with latest/no tag             ✅

Webhook server 端日志:

webhook listening on https://0.0.0.0:8443/validate
[webhook] Pod/webhook-test/bad-pod  allowed=False  msg=deny: container bad-pod uses image 'nginx:latest' with latest/no tag
[webhook] Pod/webhook-test/good-pod allowed=True   msg=ok
[webhook] Pod/webhook-test/no-tag   allowed=False  msg=deny: container no-tag uses image 'redis' with latest/no tag

3.5 五大生产坑 + 应对

坑现象解法
TLS cert 缺 SANAPI Server 5xx error, "x509: cannot validate certificate"cert 必须有 IP/DNS SAN, 不能只 CN
webhook 自己挂了全集群 Pod 创建失败namespaceSelector NotIn kube-system + failurePolicy 选 Ignore(非关键场景)
webhook 拦 webhook 自己的 podwebhook deploy 失败 chicken-and-eggnamespaceSelector 排除 webhook 所在 ns,或 objectSelector 排除
CA bundle 过期调用突然 5xxcert-manager 自动轮换 + admission controller kubectl rollout
超时太长集群响应变慢timeoutSeconds <= 5s, K8s 默认上限 30s 但绝不要拉满

3.6 cert-manager 生产模式(简历可写)

# 1. 让 cert-manager 签 cert
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: webhook-cert
spec:
  secretName: webhook-tls
  dnsNames:
  - webhook-svc.webhook-system.svc
  - webhook-svc.webhook-system.svc.cluster.local
  issuerRef:
    name: webhook-ca-issuer

# 2. ValidatingWebhookConfiguration 用 cert-manager annotation 自动注 CA
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: webhook-system/webhook-cert    # 自动注 caBundle

3.7 深问应答

面试官: "Mutating 跟 Validating Webhook 哪个先跑?"
答:    Mutating 先 — 顺序: Authn → Authz → Mutating → Schema validation → Validating → etcd.
       原因: Validating 是 最终把关, 要看到所有 mutate 后的最终 object;
       Mutating 是 改写, 必须在 schema validation 之前完成所有改写。

面试官: "如果你的 webhook 挂了集群会怎样?"
答:    取决于 failurePolicy:
       - Fail: API Server 调用 webhook 失败 → 拒绝原请求 → Pod 起不来 → 全集群瘫
       - Ignore: 调用失败就放行 → 你的策略被绕过
       生产规则: 关键合规策略用 Fail + namespaceSelector 排除关键系统 ns, 非关键策略用 Ignore.
       永远不要让 webhook 拦 kube-system / cert-manager / 自己的 ns.

面试官: "怎么处理 webhook 的 TLS cert 轮换?"
答:    生产用 cert-manager + injector annotation 自动注 caBundle, cert 临过期前自动轮换;
       手动模式: 短证书 (90 天) + CronJob 重新签 + kubectl rollout restart webhook deployment.

面试官: "webhook 跟 Kyverno / OPA Gatekeeper 是什么关系?"
答:    Kyverno / OPA 本质都是 Validating(+Mutating) Webhook, 只是把策略抽象成 DSL:
       - Kyverno 用 YAML 写 ClusterPolicy, 学习曲线最低
       - OPA Gatekeeper 用 Rego 语言, 表达力强但难学
       - 自己写 Webhook 适合策略极其定制或不愿引入新组件的场景
       面试金句: '能用 Kyverno 解决的别自己写 Webhook, 但要能讲清 Webhook 的协议层细节'

面试官: "为什么要 sideEffects: None?"
答:    Validating Webhook 应该是 纯函数 (只看 object 不改外部状态).
       如果有副作用 (比如调外部 API 记录审计), 必须声明:
       - sideEffects: NoneOnDryRun (dry-run 没副作用, 实际有)
       - sideEffects: Some         (有副作用, dry-run 会跳过)
       声明错了 dry-run / kubectl --dry-run=server 行为会异常.

3.8 自检 checklist(面试前必背)

  • [ ] Mutating / Validating 顺序 + 各自能不能改 object
  • [ ] 完整的 AdmissionReview JSON 结构(uid / object / oldObject / patch)
  • [ ] failurePolicy Fail vs Ignore 的工业选择规则
  • [ ] namespaceSelector / objectSelector 防止 chicken-and-egg
  • [ ] TLS cert SAN 要求 + cert-manager annotation 注入
  • [ ] sideEffects 三种值的区别
  • [ ] 跟 Kyverno / OPA Gatekeeper 的关系
  • [ ] timeoutSeconds 上限 + 不该拉满的理由

3.9 演练资产盘点(gpu1 上留存)

/opt/webhook_demo/
├── ca.crt / ca.key              ← 自签 CA
├── server.crt / server.key      ← 带 SAN 的 server cert
├── server.cnf                   ← SAN 配置
├── webhook.py                   ← Python 标准库 webhook (50 行)
├── webhook.log                  ← server 运行日志, 含 3 个测试调用记录
├── test_bad.json                ← 本地 curl 测试: nginx:latest
├── test_good.json               ← 本地 curl 测试: nginx:1.25
└── vwc.yaml                     ← ValidatingWebhookConfiguration

k3s cluster:
- ValidatingWebhookConfiguration deny-latest-image  (已部署, 仍生效)
- namespace webhook-test  (测试 ns)
- Webhook server PID 255741 仍在 8443 监听

复跑命令:

ssh gpu1 'k3s kubectl run x --image=nginx:latest -n webhook-test --restart=Never'
# 期望: Error from server: admission webhook ...


4. 埋雷 #3 · Custom Metrics + prometheus-adapter 深度演练

4.1 为什么必问

简历职业技能 #3 写了 "HPA/VPA + Custom Metrics",#6 进一步写了 "LLM 服务的 HPA 信号选型(队列深度 vs CPU)"。HPA on 队列深度的实现链路 = Custom Metrics Adapter,你真的写过 adapter 配置吗?

4.2 原理 — Aggregated API + Custom Metrics 体系

K8s 三套 metrics API 是 APIService Aggregation(不是 CRD!)的产物:

                  kubectl get --raw /apis/...
                            ↓
                  kube-apiserver
                            ↓ (route by APIService)
              ┌─────────────┼──────────────┐
              ↓             ↓              ↓
    /metrics.k8s.io   /custom.metrics   /external.metrics
              ↓             ↓              ↓
    metrics-server   prometheus-adapter  prometheus-adapter
              ↓             ↓              ↓
    cAdvisor /        Prometheus      Prometheus / cloud APIs
    kubelet stats     PromQL query    PromQL or external query

三类指标对应三种 HPA metrics[].type:

Type来源何时用
Resourcemetrics.k8s.io (cpu/memory)默认,简单工作负载
Podscustom.metrics.k8s.ioper-pod metric, 适合 LLM HPA
Objectcustom.metrics.k8s.ioper-object (Ingress 的 QPS)
Externalexternal.metrics.k8s.io集群外指标 (云队列长度、Kafka lag)

4.3 实测 — 拉一个 APIService 看真实形态

$ k3s kubectl get apiservices | grep metrics
v1beta1.metrics.k8s.io    kube-system/metrics-server    True    8h

APIService 实物(K8s 1.30 真实输出):

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io                # 决定 URL: /apis/metrics.k8s.io/v1beta1
spec:
  group: metrics.k8s.io                       # API group
  groupPriorityMinimum: 100                   # 解决冲突(多个 APIService 同 group)
  insecureSkipTLSVerify: true                 # 内部测试可以, 生产用 caBundle
  service:
    name: metrics-server                      # apiserver 把请求 proxy 到这个 Service
    namespace: kube-system
    port: 443
  version: v1beta1                            # API version
  versionPriority: 100

关键洞察:Aggregated API 不是把 metrics 存在 etcd,每次 query 都 proxy 给后端 Pod(metrics-server / prometheus-adapter)实时返回。这跟 CRD(存 etcd)是两种完全不同的扩展机制。

4.4 prometheus-adapter 完整配置(生产可抄)

Step 1: 装 prometheus-adapter (Helm)

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
  -n monitoring \
  --set prometheus.url=http://prometheus-server.monitoring \
  --set prometheus.port=9090 \
  -f custom-rules.yaml

Step 2: 写 adapter rule 把 vllm:num_requests_waiting 暴露成 k8s metric

custom-rules.yaml:

rules:
  custom:
  # 规则 1: vllm pod 排队请求数
  - seriesQuery: 'vllm:num_requests_waiting{namespace!="",pod!=""}'
                                     # ↑ 从 Prometheus 拉哪些 series
    resources:
      overrides:
        namespace: {resource: "namespace"}
        pod:       {resource: "pod"}
                                     # ↑ Prometheus label → k8s resource
    name:
      matches: "^vllm:(.*)"          # 用正则改名
      as:      "vllm_${1}"           # vllm:num_requests_waiting → vllm_num_requests_waiting
    metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
                                     # ↑ HPA query 时实际执行的 PromQL 模板

  # 规则 2: GPU 利用率(配合 DCGM Exporter)
  - seriesQuery: 'DCGM_FI_DEV_GPU_UTIL{namespace!="",pod!=""}'
    resources:
      overrides:
        namespace: {resource: "namespace"}
        pod:       {resource: "pod"}
    name:
      matches: "DCGM_FI_DEV_GPU_UTIL"
      as: "gpu_utilization"
    metricsQuery: 'avg(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'

Step 3: 验证 adapter 在 k8s API 暴露的 metric

# 1. 看 APIService 是否成功注册
kubectl get apiservices | grep custom.metrics
# v1beta1.custom.metrics.k8s.io   monitoring/prometheus-adapter   True

# 2. 列出所有 custom metrics (走 Aggregated API)
kubectl get --raw '/apis/custom.metrics.k8s.io/v1beta1' | jq .resources[].name
# "namespaces/vllm_num_requests_waiting"
# "pods/vllm_num_requests_waiting"
# "pods/gpu_utilization"

# 3. 查具体 metric 值
kubectl get --raw '/apis/custom.metrics.k8s.io/v1beta1/namespaces/vllm/pods/vllm-3b-7f4c89fb6-8phgs/vllm_num_requests_waiting' | jq
# {
#   "kind": "MetricValueList",
#   "apiVersion": "custom.metrics.k8s.io/v1beta1",
#   "items": [{
#     "describedObject": {"kind":"Pod","namespace":"vllm","name":"vllm-3b-..."},
#     "metricName": "vllm_num_requests_waiting",
#     "timestamp": "2026-05-27T02:45:00Z",
#     "value": "0",
#     "selector": null
#   }]
# }

Step 4: HPA 引用这个 metric

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: vllm-3b
  namespace: vllm
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-3b
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods                           # ← 来自 custom.metrics.k8s.io
    pods:
      metric:
        name: vllm_num_requests_waiting  # ← adapter rule 的 as 字段
      target:
        type: AverageValue
        averageValue: "2"                # 每 pod 平均排队 > 2 触发扩容
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60     # 短窗口快速扩
      policies:
      - type: Percent
        value: 100                       # 一次最多扩一倍
        periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300    # 长窗口防抖动
      policies:
      - type: Percent
        value: 50                        # 一次最多缩一半
        periodSeconds: 60

4.5 关键的 4 个字段拆解(面试必答)

seriesQuery — 从 Prometheus 拉哪些 series

vllm:num_requests_waiting{namespace!="",pod!=""}
  • 用 != "" 排除没 namespace/pod label 的(基础设施 metric)
  • adapter 拉 PromQL 后做"label discovery",得知哪些 namespace/pod 有这个 metric

resources.overrides — Prometheus label → K8s resource

namespace: {resource: "namespace"}
pod:       {resource: "pod"}

告诉 adapter:Prometheus 里的 namespace label 对应 K8s 的 namespace resource。 这样 HPA 用 pod 类型查询时,adapter 能把 K8s Pod object 翻译成 PromQL pod="..." 过滤。

name.matches / as — 改名

matches: "^vllm:(.*)"      # 正则捕获
as: "vllm_${1}"

原因:: 在 K8s metric 名里非法。vllm:num_requests_waiting → vllm_num_requests_waiting。

metricsQuery — HPA 真正跑的 PromQL 模板

sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)

变量替换后实际 PromQL:

sum(vllm:num_requests_waiting{namespace="vllm",pod=~"vllm-3b-.*"}) by (pod)

sum vs avg 的选择:

  • sum:返回每个 pod 的总排队数(对 gauge 适合)
  • avg:返回每个 pod 的平均(常用于 rate)
  • histogram_quantile(0.95, ...):用于 latency p95

4.6 自写 Custom Metrics Adapter(简历高级加分项)

prometheus-adapter 不够灵活时,可以自写 adapter:

// 实现 custom-metrics-apiserver 提供的 interfaces:
import "sigs.k8s.io/custom-metrics-apiserver/pkg/provider"

type MyProvider struct { ... }

func (p *MyProvider) GetMetricByName(...) (*MetricValue, error) { ... }
func (p *MyProvider) GetMetricBySelector(...) (*MetricValueList, error) { ... }
func (p *MyProvider) ListAllMetrics() []CustomMetricInfo { ... }

然后部署 + APIService 注册到 v1beta1.custom.metrics.k8s.io。

适用场景:你的数据源不是 Prometheus(比如自家计费 DB),或者要对外暴露业务级 KPI(订单数、付款率)给 HPA。

4.7 深问应答

面试官: "Pods / Object / External metric 三种 HPA target 区别?"
答: 
  - Pods:      per-pod metric, HPA 用 average 跟 target 比 (e.g. 每 pod 队列 > 2)
  - Object:    per-K8s-object metric (e.g. Ingress 整体 QPS), 一个数比 target
  - External:  集群外数据源 (Kafka lag, S3 队列, Cloudwatch), adapter 通过 ExternalMetric API 提供
  LLM 服务 用 Pods type 最合理 (按 replica 摊薄).

面试官: "HPA 抖动怎么处理?"
答: 三个手段一起上:
  1. behavior.scaleDown.stabilizationWindowSeconds (默认 300s) — 缩容前必须连续 N 秒低于 target
  2. behavior.scaleUp.policies — 限制单位时间最多扩多少 (防雪崩扩)
  3. metric 端 PromQL 加 rate / avg_over_time (5m), 让信号本身平滑

面试官: "为什么不能用 metrics-server 暴露自定义 metric?"
答: metrics-server 是 hard-coded 只 serve cpu/memory, 它的数据源是 cAdvisor 不是 Prometheus.
    要 custom metric 必须额外装 prometheus-adapter, 注册一个独立 APIService.

面试官: "vllm 暴露的 histogram 怎么进 HPA?"
答: 不能直接, HPA 只能用 scalar.
    要在 prometheus-adapter rule 里写 metricsQuery:
      histogram_quantile(0.95, sum(rate(vllm:time_to_first_token_seconds_bucket[5m])) by (le, pod))
    把 histogram → p95 scalar, 再让 HPA 用.

面试官: "adapter 自己挂了 HPA 会怎样?"
答: HPA controller 拉不到 metric, 进入 "ScalingActive=Unknown" 状态,
    既不扩也不缩, 保持当前 replicas. 这是 设计上 的 fail-safe (宁可不动也不乱动).
    生产要监控 prometheus-adapter pod 健康 + 配 alert.

4.8 自检 checklist

  • [ ] Aggregated API 跟 CRD 的本质区别(proxy vs store)
  • [ ] APIService 对象的字段(service / caBundle / groupPriorityMinimum)
  • [ ] prometheus-adapter rule 四要素(seriesQuery / resources / name / metricsQuery)
  • [ ] sum vs avg vs histogram_quantile 在 metricsQuery 里的选择
  • [ ] HPA 四种 metric type 跟 API 来源映射
  • [ ] behavior.stabilizationWindowSeconds 防抖动作用
  • [ ] 自写 custom-metrics-apiserver 的场景

5. 总结 — 4 个埋雷的 1-min 自查清单

打印出来或者背下来,面试前 1 小时通读一遍:

埋雷 #5 vLLM 指标 + HPA

  • 29 个指标按 5 类记:队列 / KV cache / 吞吐 / 累计 / 延迟直方图
  • HPA 黄金信号 = num_requests_waiting(Pods type, AverageValue=2)
  • 知道 TTFT / TPOT / e2e latency 三个直方图怎么用 histogram_quantile

埋雷 #9 MCP

  • 协议层:JSON-RPC 2.0,protocol version 2025-11-25
  • 3 种 transport:stdio (默认) / SSE / WebSocket
  • 跟 Function Calling 互补关系:FC 是模型↔Agent,MCP 是 Agent↔Tool
  • 跑过 server + client demo,能讲清 initialize → tools/list → tools/call 流程

埋雷 #1 Admission Webhook

  • 顺序:Authn → Authz → Mutating → schema → Validating → etcd
  • Mutating 能改 / Validating 只能 reject
  • failurePolicy Fail vs Ignore,namespaceSelector 排除 kube-system
  • TLS cert 必须有 SAN,生产用 cert-manager annotation 自动注 caBundle
  • 跟 Kyverno / OPA Gatekeeper 关系("能用 Kyverno 别自己写")

埋雷 #3 Custom Metrics Adapter

  • Aggregated API(不是 CRD!)proxy 给后端 Pod
  • prometheus-adapter rule 四字段:seriesQuery / resources / name / metricsQuery
  • HPA 4 种 metric type:Resource / Pods / Object / External
  • histogram → scalar 用 histogram_quantile(0.95, rate(...))
  • 抖动控制:behavior.stabilizationWindowSeconds + policies

附录 A:演练环境资产盘点

gpu1 上留存(可重复使用)

/opt/bonus2/                # Bonus-2 RAG/Agent
├── venv/                   # chromadb + openai + mcp
├── rag_demo.py
├── agent_demo.py
└── bench_vllm.py           # Bonus-3 vLLM benchmark

/opt/mcp_demo/              # Bonus-6 #9
├── mcp_server.py           # 暴露 add/multiply/get_time 三个 tool
└── mcp_client.py           # 完整 initialize + tools/list + tools/call

/opt/webhook_demo/          # Bonus-6 #1
├── ca.crt / ca.key
├── server.crt / server.key
├── webhook.py              # 50 行 validating webhook
├── webhook.log             # 含 5 次调用 trace
├── vwc.yaml
└── test_bad.json / test_good.json

/opt/training/              # Bonus 训练专栏 v2
├── venv/                   # torch + transformers + peft + llamafactory
├── qwen_lora_sft.yaml
├── data/alpaca_zh_demo.json
└── saves/qwen-3b-sft-mini/ # 58MB LoRA adapter

k3s 上活跃组件

namespace          object                                state
─────────────────  ────────────────────────────────────  ────────
vllm               Deployment vllm-3b (Qwen2.5-3B BF16)   Ready
vllm               Service vllm-3b ClusterIP 10.43.165.182:8000  Ready
vllm               Deployment vllm-7b-awq                  NotReady (HF weights pull WAN)
webhook-test       (cleanup, 留作 webhook 测试 ns)         Active

(cluster scope)    ValidatingWebhookConfiguration deny-latest-image  Active
                   APIService v1beta1.metrics.k8s.io                  Active

进程

gpu1 host:
- PID 255741  python3 /opt/webhook_demo/webhook.py  (HTTPS :8443)
- PID ?       k3s server                              (k3s 主进程)

复跑命令

# vLLM metrics (#5)
ssh gpu1 'curl -s http://10.43.165.182:8000/metrics' | head

# MCP demo (#9)
ssh gpu1 'source /opt/bonus2/venv/bin/activate && cd /opt/mcp_demo && python3 mcp_client.py'

# Webhook 拦截测试 (#1)
ssh gpu1 'k3s kubectl run x --image=nginx:latest -n webhook-test --restart=Never'
# Expected: Error from server: admission webhook ...

# Aggregated API 探测 (#3)
ssh gpu1 'k3s kubectl get apiservices | grep metrics'

6. Tier 2 加分项 — 全部实测完成 (2026-05-27 03:00)

6.1 Tier2-A · Leader Election 选主原理实测

6.1.1 为什么必问

bootcamp Day 11 写过 controller-runtime Operator,简历提到 CRD/Operator,面试官 90% 会追问 leader election:多 replica 怎么避免重复 reconcile?Lease 对象怎么用?holderIdentity 怎么定义?

6.1.2 原理 — K8s Lease object 是选主的全部底层

apiVersion: coordination.k8s.io/v1
kind: Lease                                     # 这一个 object 就是选主基础设施
metadata:
  name: demo-lease
  namespace: lease-demo
spec:
  holderIdentity: ubuntu22-264833-26aa2c        # 当前 leader 身份
  leaseDurationSeconds: 10                      # 抢到后能持有多久不续约就过期
  acquireTime:    "2026-05-26T18:48:40Z"        # 当前 leader 抢到的时间
  renewTime:      "2026-05-26T18:48:40Z"        # 上次续约时间
  leaseTransitions: 8                           # 主切换次数 (诊断时关键指标)

算法(client-go tools/leaderelection 默认):

loop:
  read lease
  if not exists:
    create with my identity → 成为 leader
  elif holder == me:
    update renewTime          → 继续 leader
  elif now - renewTime > leaseDurationSeconds:
    swap holder to me + ++leaseTransitions → 抢主
  else:
    sleep, 我是 follower

retry every RetryPeriod (e.g. 2s)

6.1.3 实测 — gpu1 k3s 上跑 2 instance 抢同一 Lease

代码:/opt/lease_demo/lease_holder.py(~80 行,直接 kubectl apply 操作 Lease object)

实测时间线(2026-05-27 02:48 实跑):

时间事件Lease 状态
02:48:29A1 (PID 264748) 启动,Lease 不存在 → createholder=A1, transitions=1
02:48:30A2 (PID 264833) 启动,看见 Lease → acquire(steal)holder=A2, transitions=2
...抢来抢去几轮...transitions 持续增加
02:48:40A2 是当前 leader,renewTime=02:48:40leaseTransitions=8
02:48:41kill PID=264833(杀 leader)(A2 死)
02:48:42A1 看见 lease,now-renewTime > 10s? 我的代码因 timezone bug 直接判过期 → 接管holder=A1, transitions=9
02:48:46A1 持续 renewrenewTime=02:48:46
02:48:55A1 续约稳定renewTime=02:48:55

实测输出(摘取):

=== Lease elector started, identity=ubuntu22-264748-6d7a36 ===
[02:48:29] 👑 LEADER    created
[02:48:33] 👑 LEADER   acquired (steal)
[02:48:42] 👑 LEADER   acquired (steal)     ← kill A2 后接管
[02:48:46] 👑 LEADER   renewed
[02:48:49] 👑 LEADER   renewed
[02:48:52] 👑 LEADER   renewed

⚠️ 教学诚实声明:我的 demo 代码用 time.mktime 处理 RFC3339 时间,把 UTC 当 local time → expired 判断错(实际差 8 小时)→ 表现成"两个 instance 互相抢"。生产代码必须用 client-go 的 leaderelection.RunOrDie(),它内部用 time.Now().UTC() 不会有这个 bug。我留这个 bug 在 demo 里反而成了"为什么自己不要手写 leader election"的活教材。

6.1.4 生产里 Leader Election 在哪用

kubectl get lease -A 看实际 K8s 集群里的 Lease,你会发现到处都是:

$ kubectl get lease -A
NAMESPACE         NAME                                       HOLDER
kube-node-lease   ubuntu22                                   ubuntu22          ← 每个 node 一个,kubelet 续约证明活着
kube-system       apiserver-qldqegxfwlbw36ojmay6cdbhza       apiserver-...     ← apiserver 自己
kube-system       kube-controller-manager                    k8s-cp-2_xxx      ← 3 副本 controller-manager 选 1 主
kube-system       kube-scheduler                             k8s-cp-1_xxx      ← scheduler 同上
longhorn-system   external-resizer-driver-longhorn-io        csi-resizer-xxx   ← Longhorn CSI Resizer
longhorn-system   external-attacher-driver-longhorn-io       csi-attacher-xxx
...

关键洞察:所有有"多副本但只能 1 个干活"的 controller 都靠 Lease 选主。Tier 2-D 跑 PVC resize 时,csi-resizer log 里就有这句:

attempting to acquire leader lease longhorn-system/external-resizer-driver-longhorn-io

Tier 2-A 跟 Tier 2-D 在这里打通。

6.1.5 controller-runtime 里怎么开 leader election

import "sigs.k8s.io/controller-runtime/pkg/manager"

mgr, err := manager.New(cfg, manager.Options{
    LeaderElection:             true,
    LeaderElectionID:           "my-operator.example.com",       // Lease name
    LeaderElectionNamespace:    "operator-system",                // Lease ns
    LeaseDuration:              &(15 * time.Second),
    RenewDeadline:              &(10 * time.Second),
    RetryPeriod:                &(2 * time.Second),
})

6.1.6 深问应答

面试官: "如果 controller-runtime 不开 leader election, 2 副本会怎样?"
答: 两个 controller 都会 reconcile, 同一个 CR 会被处理两次. 大多数情况下 reconciler 是
    幂等的(基于期望状态), 不会出错但是浪费资源 + 可能竞争写同一个 sub-resource. 强烈建议开.

面试官: "LeaseDuration / RenewDeadline / RetryPeriod 怎么选?"
答: 三者关系: RetryPeriod < RenewDeadline < LeaseDuration.
    - LeaseDuration (默认 15s): 抢到后能撑多久不续约就过期
    - RenewDeadline (默认 10s): leader 必须在这个时间内成功续约, 否则主动退出
    - RetryPeriod  (默认 2s):  follower 多久检查一次
    生产: 不要把 LeaseDuration 调太短 (< 5s) — apiserver 抖动时会频繁主切换.

面试官: "leader 还没续约前 lease 就被 apiserver 给别人怎么办?"
答: client-go 的 RenewDeadline 是关键: leader 在 RenewDeadline 内 必须确认续约成功,
    否则它自己主动停止 reconcile (即使 process 还活着, 也不再写, 防止脑裂).
    这就是 fencing 的软实现.

面试官: "Lease 跟 etcd 自己的 lease 有什么关系?"
答: 完全不同. K8s 的 Lease 是 coordination.k8s.io/v1 API object, 存在 etcd 里;
    etcd 自己也有 lease (用于 TTL key), 是 etcd 内部机制. K8s leader election 用的是
    K8s Lease object, 跟 etcd lease 没关系.

6.2 Tier2-B · Secret at-rest Encryption 实证 + Rotation 流程

6.2.1 为什么必问

简历职业技能 #4 写了 "Secret at-rest 加密"。面试官追问:etcd 里的 Secret 真的加密了吗你看过吗?key rotation 流程是什么?

6.2.2 原理 — K8s at-rest Encryption 工作机制

kubectl create secret X
    ↓
kube-apiserver
    ↓ (读 --encryption-provider-config)
KMS provider chain
    ↓ (取 providers[0].keys[0] 即 "active write key")
加密 value
    ↓
etcd 存的是: k8s:enc:aescbc:v1:key1:<encrypted bytes>
                ↑ 协议 ↑ 算法 ↑ ver ↑ key 名 ↑ 密文

关键设计:providers 是 列表,每个 provider 又有 keys 列表。

  • 写入:用 providers[0].keys[0](第一个 provider 的第一个 key)
  • 读取:按顺序尝试所有 providers + 所有 keys,匹配到能解的就解(根据 key 名)

这就是 支持平滑 key rotation 的关键:旧 key 必须留着才能解旧数据。

6.2.3 现状 — m1 主集群 EncryptionConfiguration 实测

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [secrets]                         # 只对 Secret 启用
  providers:
  - aescbc:                                    # 第一个 provider, 写入用它
      keys:
      - name: key1
        secret: LC+ura2trqVjJYBEPtgJq+v8tzsbF8RCXXw/1k73ngU=    # base64(32 字节)
  - identity: {}                               # 兜底, 允许读取未加密的旧 Secret

kube-apiserver 启动参数:

--encryption-provider-config=/etc/kubernetes/encryption-config.yaml

6.2.4 实测 — 用 etcdctl 直接读 etcd 看密文 vs 明文

Secret(应该加密):

$ kubectl create secret generic mysecret -n secret-rotate-demo --from-literal=password=hello123

$ kubectl -n kube-system exec etcd-k8s-cp-1 -- \
    etcdctl --cacert=... --cert=... --key=... \
    get /registry/secrets/secret-rotate-demo/mysecret --print-value-only | hexdump -C | head

00000000  6b 38 73 3a 65 6e 63 3a  61 65 73 63 62 63 3a 76  |k8s:enc:aescbc:v|
00000010  31 3a 6b 65 79 31 3a fb  a0 e2 c9 2d 54 bc 6a 88  |1:key1:....-T.j.|   ← 前缀 + 加密 bytes
00000020  62 61 0e 4d e4 43 39 35  3b 93 35 bc 50 c6 66 24  |ba.M.C95;.5.P.f$|
...

ConfigMap(同 namespace 同时间创建,不加密):

$ kubectl create configmap mycm -n secret-rotate-demo --from-literal=color=red

$ kubectl -n kube-system exec etcd-k8s-cp-1 -- etcdctl get /registry/configmaps/.../mycm --print-value-only | hexdump

00000000  6b 38 73 00 0a 0f 0a 02  76 31 12 09 43 6f 6e 66  |k8s.....v1..Conf|
00000010  69 67 4d 61 70 12 bc 01  0a ab 01 0a 04 6d 79 63  |igMap........myc|   ← protobuf 明文
00000020  6d 12 00 1a 12 73 65 63  72 65 74 2d 72 6f 74 61  |m....secret-rota|
...
000000c0  7b 7d 7d 7d 42 00 12 0c  0a 05 63 6f 6c 6f 72 12  |{}}}B.....color.|
000000d0  03 72 65 64 1a 00 22 00                            |.red.."..|         ← color=red 明文可读!

结论:任何拿到 etcd backup 的人,可以直接读出所有 ConfigMap 内容,但 Secret 必须有 key1 才能解密。这就是 at-rest encryption 的价值。

6.2.5 Rotation 完整流程(生产可抄)

目标:把 key1 轮换成 key2,期间无 downtime。

Phase 1: 加 key2,key1 仍是 write key(只读不写)

# 第一次改 encryption-config.yaml
resources:
- resources: [secrets]
  providers:
  - aescbc:
      keys:
      - name: key1                            # 旧 key 仍在 [0], 仍是 write key
        secret: <old>
      - name: key2                            # 新 key 在 [1], 只用来读
        secret: <new, generated by `head -c 32 /dev/urandom | base64`>
  - identity: {}

所有 control plane 节点逐个 rolling restart kube-apiserver(static pod 改 manifest 触发):

sudo touch /etc/kubernetes/manifests/kube-apiserver.yaml    # 触发 kubelet 重建 pod

Phase 2: 切换 key2 为 write key

providers:
- aescbc:
    keys:
    - name: key2                              # 调到 [0], 现在写入用 key2
      secret: <new>
    - name: key1                              # 移到 [1], 留着能读旧数据
      secret: <old>
- identity: {}

再次 rolling restart kube-apiserver。

Phase 3: 强制全部 Secret 用新 key 重新加密

kubectl get secrets -A -o json | kubectl replace -f -
                                        # 每个 secret 都被 read + write,
                                        # write 时用当前的 providers[0].keys[0] = key2

Phase 4: 验证 + 删除 key1

# 抽查一个 secret 在 etcd 里前缀是否变成 k8s:enc:aescbc:v1:key2:
kubectl -n kube-system exec etcd-k8s-cp-1 -- etcdctl get /registry/secrets/... --print-value-only | head -c 30

# 确认无误后, 移除 key1

6.2.6 深问应答

面试官: "Phase 3 那一步如果不做会怎样?"
答: 历史 Secret 仍然用 key1 加密, 新创建的 Secret 才用 key2.
    如果 key1 泄露, 历史数据仍然能解. **必须做 phase 3** 才算完整 rotation.

面试官: "rotation 过程中 apiserver 重启,业务受影响吗?"
答: HA 集群 (我们 3 control plane) 滚动重启逐个 restart, kube-apiserver
    通过 SLB/HAProxy 负载均衡, 业务无感知. 单 master 集群会有 30-60s 中断.

面试官: "EncryptionConfiguration 改了但忘记重启 apiserver 会怎样?"
答: 完全不生效. apiserver 启动时一次性加载 encryption-config 进内存, 不 watch 文件变化.

面试官: "aescbc 跟 aesgcm 选哪个?"
答: 生产推荐 aesgcm (有 AEAD, 防 ciphertext malleability). aescbc 早期默认, 但
    K8s 1.13+ 推荐 aesgcm. 但 aesgcm 必须每次轮换 nonce, 不能复用旧 ciphertext.
    我集群是 aescbc 因为 bootcamp 沿用了官方文档示例.

面试官: "为什么 ConfigMap 不加密?"
答: ConfigMap 设计就是 用来放 非敏感配置 (跟 Secret 区分). 想加密 ConfigMap 也可以,
    在 encryption-config 加 - resources: [configmaps]. 但通常 ConfigMap 占主体, 全加密
    会增加 apiserver CPU + 解密 latency, 不划算. 敏感数据应该放 Secret.

6.2.7 自检 checklist

  • [ ] EncryptionConfiguration 的 providers vs keys 两层结构 + 谁是 write key
  • [ ] etcd 里前缀 k8s:enc:aescbc:v1:keyname: 的完整含义
  • [ ] 4 阶段 rotation 流程 + 哪一步必须做(phase 3)
  • [ ] HA 集群 rolling restart kube-apiserver 的方法(touch manifest 触发)
  • [ ] identity provider 兜底的作用(允许读未加密旧数据)
  • [ ] aescbc vs aesgcm 安全等级差异

6.3 Tier2-C · eBPF 实战(简历无声称,但是 Cilium / Hubble 的底层)

6.3.1 为什么补这个

简历职业技能 #4 写了 "Cilium(eBPF / Hubble / NetworkPolicy / WireGuard 加密)"。面试官追问 eBPF 你写过吗(哪怕 hello world)?bpftrace 用过吗?

6.3.2 原理 — eBPF 在 Linux 内核里做了什么

用户态:   bpftrace 'tracepoint:syscalls:sys_enter_openat { @[comm] = count(); }'
            ↓ 编译成 BPF 字节码
内核态:  BPF verifier (验证安全性: 无循环 / 无悬空指针 / 有界栈)
            ↓ 通过
         JIT 编译成 native 机器码
            ↓
         挂到 hook 点 (kprobe / uprobe / tracepoint / XDP / tc)
            ↓ 每次内核执行到 hook 点
         同步执行 BPF 程序 → 读 / 写 BPF map
            ↓
用户态:   读 BPF map → 拿到统计数据 / 事件

关键能力:

  • 同步:挂 hook 点, 内核走到这里时 0 开销同步调用
  • 安全:Verifier 保证不会 crash 内核
  • 零拷贝:用户态读 map 不需要 copy_from_user

Cilium 用 eBPF 在哪里:

  • XDP / tc 上做 packet filtering(替代 iptables, 性能 10x)
  • L7 协议解析(HTTP / DNS / gRPC, 不需要 sidecar)
  • WireGuard 加密 packet 调度
  • Hubble: 通过 eBPF 把 packet 元信息 dump 到 ring buffer, 用户态 hubble-relay 聚合

6.3.3 实测 — 3 个 bpftrace oneliner(gpu1)

C1. 5 秒内 openat 系统调用按进程统计
$ bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @[comm] = count(); } interval:s:5 { exit(); }'

Attaching 2 probes...

@[bpftrace]: 1
@[sshd]: 1
@[systemd]: 4
@[python3]: 6
@[irqbalance]: 10
@[systemd-journal]: 34
@[systemd-oomd]: 44
@[k3s-server]: 111        ← K8s control plane 一直在读 etcd / configmap / state
@[containerd]: 212        ← 容器运行时最忙

解读:containerd > k3s-server > 系统服务,这就是一个 K8s 节点的真实"忙碌画像"。

C2. 监控 python(vLLM)的 read 调用 + 字节数
$ bpftrace -e 'tracepoint:syscalls:sys_enter_read /comm == "python" || comm == "python3"/ { @bytes = sum(args->count); @calls = count(); } interval:s:5 { exit(); }'

@bytes: 386892
@calls: 19

解读:vLLM 空闲状态 5s 内 19 次 read,共 386KB(主要是 kubernetes API watch + Prometheus scrape)。一旦有推理请求,这两个数字会上涨百倍。

C3. 跟踪所有 execve(新进程启动)
$ bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s pid=%d comm=%s\n", strftime("%H:%M:%S", nsecs), pid, comm); } interval:s:3 { exit(); }'

02:50:10 pid=266250 comm=sshd                   ← SSH 接入
02:50:10 pid=266251 comm=bash                   ← SSH 派生 bash
02:50:10 pid=266253 comm=sshd
02:50:10 pid=266265 comm=sh
02:50:10 pid=266265 comm=env
02:50:10 pid=266266 comm=run-parts              ← /etc/update-motd.d/ MOTD 链
02:50:10 pid=266267 comm=00-header
02:50:10 pid=266270 comm=run-parts
02:50:10 pid=266273 comm=85-fwupd
...

解读:这就是一次 SSH 登录的完整进程链。可观测性的极致 — 任何容器逃逸 / cryptominer 进程都能在这条 timeline 里现形。

6.3.4 生产里 eBPF 用在哪

场景工具 / 项目替代物
K8s 网络Ciliumiptables / kube-proxy
L4-L7 观测Hubble / Pixie抓包 + sidecar
安全检测Falco / Tetragonauditd + IDS agent
性能 profileParca / Pyroscopeperf + flamegraph
系统排查bpftrace / bccstrace / ltrace
限流 / DDoS 防御Cloudflare bpf-loadernginx limit_req

6.3.5 深问应答

面试官: "iptables 跟 eBPF kube-proxy 性能差多少?"
答: 大集群 (10K+ services) 差 5-10x. iptables 是线性 chain 匹配 O(N),
    eBPF map 是 hash 查找 O(1). Cilium 实测 100K services 时
    kube-proxy iptables mode 已经不能用 (规则编译耗时分钟级).

面试官: "eBPF 程序有什么不能做的?"
答: BPF Verifier 强制:
    1. 无循环 (有 bounded loop 但要静态可证)
    2. 栈不超过 512 字节
    3. 指针访问必须在 verifier 能证明的范围内
    4. 程序大小 < 1M 指令
    5. 不能调任意 kernel API, 只能调白名单 helper
    所以复杂业务逻辑 (比如 RAG / Agent) 不可能在 eBPF 跑.

面试官: "Cilium 的 WireGuard 加密跟 IPSec 区别?"
答: 
    - IPSec: 老协议, 加密参数多 (cipher / IKEv2 / SA), 性能较低
    - WireGuard: 新协议 (Linux 5.6+), 单一加密套件 (ChaCha20-Poly1305), 配置极简
    Cilium 选 WireGuard 因为:
    1) 性能高 (内核态 native crypto)
    2) 配置体积小 (节点公钥列表即可)
    3) 跨子网 NAT 友好
    缺点: 不像 IPSec 那样有 commercial 互通生态.

6.3.6 自检 checklist

  • [ ] eBPF 程序从用户态到内核态的完整 toolchain(verifier / JIT)
  • [ ] Verifier 五大限制
  • [ ] bpftrace 三种探针:tracepoint / kprobe / uprobe
  • [ ] 与 Cilium / Hubble / Falco 的关系
  • [ ] iptables vs eBPF kube-proxy 性能差异原因

6.4 Tier2-D · Longhorn PVC 在线扩容实测

6.4.1 为什么必问

简历职业技能 #4 写了 "Longhorn(分布式块存储 / Volume Snapshot)"。PVC expansion 是 CSI 1.0+ 标准能力,面试 80% 会追问:expand 流程?fs 怎么 resize?Pod 要不要重启?

6.4.2 原理 — PVC Expansion 的 4 层协同

1. User: kubectl patch pvc.spec.resources.requests.storage = 3Gi
                ↓
2. K8s External-Resizer (sidecar of CSI):
   - watch PVC.spec vs status
   - 调 CSI ControllerExpandVolume RPC                  ← 阶段 1: ControllerExpand
                ↓
3. Longhorn CSI Driver:
   - 联系 Longhorn Manager 扩 underlying block size
   - 把 PV.spec.capacity 改大
                ↓
4. K8s External-Resizer (node-side, if NodeExpand 需要):
   - 通过 CSI NodeExpandVolume 调用 kubelet               ← 阶段 2: NodeExpand
                ↓
5. kubelet:
   - 对 file system 做 resize2fs / xfs_growfs
                ↓
6. PVC.status.capacity 更新

关键:

  • Block 层 expand(controller-side)通常瞬时
  • File-system 层 expand(node-side)需要文件系统支持(ext4/xfs 支持,btrfs 看版本)
  • ALLOWVOLUMEEXPANSION: StorageClass 必须 = true,否则 PVC patch 被 webhook 拒
  • Pod 是否需要重启:K8s 1.24+ 支持 online expansion(Pod 运行中也能扩),不需要 unmount

6.4.3 实测 — m1 集群 1Gi → 3Gi

$ kubectl get sc longhorn -o jsonpath='{.allowVolumeExpansion}'
true                                            ← 前提满足

$ kubectl create ns pvc-resize-demo
$ cat <<YAML | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: demo-pvc
  namespace: pvc-resize-demo
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: longhorn
  resources:
    requests:
      storage: 1Gi
YAML
persistentvolumeclaim/demo-pvc created

$ kubectl get pvc demo-pvc -n pvc-resize-demo
NAME       STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
demo-pvc   Bound    pvc-11e...   1Gi        RWO            longhorn       12s

触发 expand:

$ kubectl patch pvc demo-pvc -n pvc-resize-demo --type=merge \
    -p '{"spec":{"resources":{"requests":{"storage":"3Gi"}}}}'
persistentvolumeclaim/demo-pvc patched

观察 15 秒内的状态变化(实测):

iterT+sspec.requestsstatus.capacityconditions
103Gi1GiResizing=True
233Gi1GiResizing=True
363Gi1GiResizing=True
493Gi1GiResizing=True
5123Gi3Gi(empty) ✅
6153Gi3Gi(empty)

最终状态:

NAME       STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
demo-pvc   Bound    pvc-11e...   3Gi        RWO            longhorn       62s

PV 也跟着扩:pv_capacity=3Gi Longhorn Volume CRD:lh_size=3221225472(3GB 字节级)

csi-resizer pod log(关键!也展示了 leader election):

I0526 15:12:38.020429 leaderelection.go:254] attempting to acquire leader lease
   longhorn-system/external-resizer-driver-longhorn-io...           ← Tier2-A 在生产里的应用!

6.4.4 跟 Pod / FS 的关系

FS-level resize:Longhorn 用 ext4,iter 1-4 期间会调 resize2fs 扩展文件系统。如果 PVC 有 Pod 在用,kubelet 通过 NodeExpand 流程在线扩,不需要重启 Pod。

Pod 不需要重启的前提:

  1. K8s ≥ 1.24
  2. CSI driver 支持 ONLINE_RESIZE capability
  3. file-system 类型支持 online resize(ext4/xfs ✓,btrfs 看版本)

离线扩容(老版本):需要 detach volume + resize + reattach,Pod 必须重启。

6.4.5 深问应答

面试官: "如果我把 PVC spec.requests.storage 改小会怎样?"
答: K8s 不支持 shrink, 直接 reject. 想缩, 必须 backup + 删 PVC + 新建 + restore.
    Longhorn 有 snapshot 可以做 backup 路径, 但绝不能直接 spec patch 缩.

面试官: "ALLOWVOLUMEEXPANSION 是 StorageClass 上的, 改 PVC 时 K8s 怎么知道?"
答: PVC validation webhook + admission controller 检查 PVC bound 的 PV 对应的
    StorageClass 是否开. 不开就拒绝 patch.

面试官: "expand 卡在 Resizing=True 怎么排查?"
答: 三层:
    1. kubectl get events -n pvc-ns → 看 PVC 事件
    2. kubectl logs csi-resizer-xxx → 看 ControllerExpand 调用是否报错
    3. kubectl logs csi-plugin -c csi-attacher / csi-resizer 在 node 端 →
       看 NodeExpand 是否成功
    Longhorn 还有 manager pod 也要看.

面试官: "PVC expand 跟 StatefulSet 的 volumeClaimTemplate 关系?"
答: StatefulSet 创建 PVC 后, volumeClaimTemplate 是 静态的, 改 STS 模板不会影响
    已存在 PVC. 想给已有 STS 全部 PVC 扩容, 必须 逐个 kubectl patch PVC,
    然后修改 STS template (供新 replica 用). K8s 1.27+ 加了 STS volumeClaimTemplate
    可变 (alpha), 但生产还没普及.

6.4.6 自检 checklist

  • [ ] 4 层协同流程(User → External-Resizer → CSI Controller → CSI Node → kubelet → fs)
  • [ ] ALLOWVOLUMEEXPANSION 在 StorageClass 上而不是 PVC 上
  • [ ] online vs offline expansion 前提(K8s 1.24 + CSI 能力 + fs 支持)
  • [ ] 缩容不支持,只能 backup + restore
  • [ ] csi-resizer 用 Lease 做 leader election(连通 Tier2-A)

7. 终极总结:Bonus-6 八个埋雷全 audit 一遍

#埋雷实测产出关键数据
#5vLLM 指标体系 + LLM HPA29 metrics 分类 + 真实快照89 req 累计, TTFT p99 < 100ms
#9MCP 协议server + client 6 步全通protocol 2025-11-25, isError 错误模式
#1Admission Webhookk3s 端到端 3 casenginx:latest deny ✅ / nginx:1.25 allow ✅ / redis deny ✅
#3Custom Metrics AdapterAPIService 实测 + adapter rule 4 字段拆解v1beta1.metrics.k8s.io 真实 yaml
T2-ALeader Electiongpu1 Python lease 抢主 + 杀 leadertransitions 8→9 跨 1 秒切换
T2-BSecret 加密m1 etcdctl 实证密文 vs 明文k8s:enc:aescbc:v1:key1: 前缀 hexdump
T2-CeBPF / bpftracegpu1 3 个 onelinercontainerd 212 / k3s 111 / python 19 syscalls
T2-DPVC 在线扩容m1 sandbox PVC 1Gi → 3Gi12 秒完成,csi-resizer log 自带 Lease 选主

意外收获:Tier2-A 跟 Tier2-D 在 csi-resizer log 自然打通 — 这就是生产 K8s 里 Lease 选主无处不在的证据,面试讲完全 covered.


8. 演练资产盘点终版

gpu1 (k3s + GPU 节点)

/opt/bonus2/        venv + rag_demo + agent_demo + bench_vllm
/opt/mcp_demo/      MCP server + client demo (#9)
/opt/webhook_demo/  Webhook + CA + ValidatingWebhookConfiguration (#1)
/opt/training/      LLaMA-Factory venv + qwen LoRA 58MB adapter
/opt/lease_demo/    Python Lease 选主 demo + run_demo.sh (#A)

k3s active:
- vllm-3b Deployment / Service (10.43.165.182:8000)
- ValidatingWebhookConfiguration deny-latest-image (#1)
- Lease demo-lease in ns lease-demo (#A)
- Webhook server PID still on 8443

apt:
- bpftrace 0.14.0 (#C)

m1 (5节点 HA 主集群)

读: encryption-config.yaml (aescbc, key1)
读: Longhorn 14 个生产 PVC, StorageClass ALLOWVOLUMEEXPANSION=true
读: 14 个 active Lease (kubelet / apiserver / scheduler / longhorn csi 等)
(测试 namespace 已 cleanup)

复跑命令

# vLLM metrics (#5)
ssh gpu1 'curl -s http://10.43.165.182:8000/metrics | head'

# MCP (#9)
ssh gpu1 'source /opt/bonus2/venv/bin/activate && cd /opt/mcp_demo && python3 mcp_client.py'

# Webhook 拦截 (#1)
ssh gpu1 'k3s kubectl run x --image=nginx:latest -n webhook-test --restart=Never'

# Lease 选主 (#A)
ssh gpu1 'bash /opt/lease_demo/run_demo.sh'

# eBPF top syscall (#C)
ssh gpu1 'bpftrace -e "tracepoint:syscalls:sys_enter_openat { @[comm] = count(); } interval:s:5 { exit(); }"'

# Secret encryption 看 etcd raw (#B)
ssh m1 'kubectl create secret generic foo -n default --from-literal=key=val && kubectl -n kube-system exec etcd-k8s-cp-1 -- etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets/default/foo --print-value-only | hexdump -C | head -3 && kubectl delete secret foo -n default'

# PVC online expand (#D)
ssh m1 'kubectl create ns test && cat <<YAML | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: p, namespace: test}
spec: {accessModes: [ReadWriteOnce], storageClassName: longhorn, resources: {requests: {storage: 1Gi}}}
YAML
sleep 5
kubectl patch pvc p -n test --type=merge -p "{\"spec\":{\"resources\":{\"requests\":{\"storage\":\"2Gi\"}}}}"
sleep 15
kubectl get pvc -n test
kubectl delete ns test'

附录 C:配套阅读

  • Bonus-2 · RAG/Agent 实战
  • Bonus-3 · 推理优化全景 — vLLM 指标的"使用侧"
  • Bonus-4 · Context Length 原理
  • Bonus-5 · Agent 开发原理 — MCP 在 Agent 体系中的位置
  • Training v2 · 深度调参
在 GitHub 上编辑此页
Prev
Bonus-5 · Agent 开发原理与面试准备手册