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_running | Gauge | 正在 decode 的请求数 |
vllm:num_requests_waiting | Gauge | 排队中 ← HPA 黄金信号 | |
vllm:num_requests_swapped | Gauge | KV cache 被 swap 到 CPU 的请求数 | |
| KV cache | vllm:gpu_cache_usage_perc | Gauge | GPU KV cache 使用率(0-1) |
vllm:cpu_cache_usage_perc | Gauge | CPU swap 区使用率 | |
vllm:gpu_prefix_cache_hit_rate | Gauge | prefix cache 命中率(开 prefix cache 时才有意义) | |
vllm:cpu_prefix_cache_hit_rate | Gauge | 同上 CPU 侧 | |
| 吞吐(瞬时) | vllm:avg_prompt_throughput_toks_per_s | Gauge | 滑动平均 prompt tok/s |
vllm:avg_generation_throughput_toks_per_s | Gauge | 滑动平均 generation tok/s | |
| 累计计数 | vllm:prompt_tokens_total | Counter | 历史 prompt token 累计 |
vllm:generation_tokens_total | Counter | 历史 generation token 累计 | |
vllm:num_preemptions_total | Counter | 历史抢占次数(decode 被中断让位给 prefill) | |
vllm:request_success_total | Counter | 历史成功请求数 | |
| 延迟直方图 | vllm:time_to_first_token_seconds | Histogram | TTFT 直方图 |
vllm:time_per_output_token_seconds | Histogram | TPOT 直方图 | |
vllm:e2e_request_latency_seconds | Histogram | 端到端延迟 | |
vllm:request_queue_time_seconds | Histogram | 排队等待时间 | |
vllm:request_prefill_time_seconds | Histogram | prefill 阶段耗时 | |
vllm:request_decode_time_seconds | Histogram | decode 阶段耗时 | |
vllm:request_inference_time_seconds | Histogram | 推理总时间(prefill + decode) | |
vllm:time_in_queue_requests | Histogram | (新版) 队列驻留时间 | |
| 请求分布 | vllm:request_prompt_tokens | Histogram | 单请求 prompt 长度分布 |
vllm:request_generation_tokens | Histogram | 单请求 generation 长度分布 | |
vllm:request_params_n | Histogram | n 参数分布(并行生成数) | |
vllm:request_params_max_tokens | Histogram | max_tokens 参数分布 | |
vllm:request_max_num_generation_tokens | Histogram | 实际生成 token 最大值 | |
vllm:iteration_tokens_total | Histogram | 单 iteration 处理的 token 数 | |
| 元数据 | vllm:cache_config_info | Info | KV cache 配置(block size 等) |
vllm:lora_requests_info | Info | LoRA 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 | 方向 | 用途 |
|---|---|---|
initialize | Client → Server | 握手,协商 capabilities + 协议版本 |
initialized (notification) | Client → Server | 握手完成通知 |
tools/list | Client → Server | 拿所有可用工具的 schema |
tools/call | Client → Server | 调用工具 |
resources/list | Client → Server | 拿可读资源(文件、DB schema 等) |
resources/read | Client → Server | 读资源内容 |
prompts/list | Client → Server | 拿预制 prompt 模板 |
prompts/get | Client → Server | 拿具体 prompt + 参数 |
sampling/createMessage | Server → Client | server 反向让 client 帮它调 LLM(!!) |
notifications/cancelled | 双向 | 取消任务 |
notifications/progress | Server → 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 缺 SAN | API Server 5xx error, "x509: cannot validate certificate" | cert 必须有 IP/DNS SAN, 不能只 CN |
| webhook 自己挂了 | 全集群 Pod 创建失败 | namespaceSelector NotIn kube-system + failurePolicy 选 Ignore(非关键场景) |
| webhook 拦 webhook 自己的 pod | webhook deploy 失败 chicken-and-egg | namespaceSelector 排除 webhook 所在 ns,或 objectSelector 排除 |
| CA bundle 过期 | 调用突然 5xx | cert-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 | 来源 | 何时用 |
|---|---|---|
Resource | metrics.k8s.io (cpu/memory) | 默认,简单工作负载 |
Pods | custom.metrics.k8s.io | per-pod metric, 适合 LLM HPA |
Object | custom.metrics.k8s.io | per-object (Ingress 的 QPS) |
External | external.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:29 | A1 (PID 264748) 启动,Lease 不存在 → create | holder=A1, transitions=1 |
| 02:48:30 | A2 (PID 264833) 启动,看见 Lease → acquire(steal) | holder=A2, transitions=2 |
| ...抢来抢去几轮... | transitions 持续增加 | |
| 02:48:40 | A2 是当前 leader,renewTime=02:48:40 | leaseTransitions=8 |
| 02:48:41 | kill PID=264833(杀 leader) | (A2 死) |
| 02:48:42 | A1 看见 lease,now-renewTime > 10s? 我的代码因 timezone bug 直接判过期 → 接管 | holder=A1, transitions=9 |
| 02:48:46 | A1 持续 renew | renewTime=02:48:46 |
| 02:48:55 | A1 续约稳定 | 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 网络 | Cilium | iptables / kube-proxy |
| L4-L7 观测 | Hubble / Pixie | 抓包 + sidecar |
| 安全检测 | Falco / Tetragon | auditd + IDS agent |
| 性能 profile | Parca / Pyroscope | perf + flamegraph |
| 系统排查 | bpftrace / bcc | strace / ltrace |
| 限流 / DDoS 防御 | Cloudflare bpf-loader | nginx 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 秒内的状态变化(实测):
| iter | T+s | spec.requests | status.capacity | conditions |
|---|---|---|---|---|
| 1 | 0 | 3Gi | 1Gi | Resizing=True |
| 2 | 3 | 3Gi | 1Gi | Resizing=True |
| 3 | 6 | 3Gi | 1Gi | Resizing=True |
| 4 | 9 | 3Gi | 1Gi | Resizing=True |
| 5 | 12 | 3Gi | 3Gi | (empty) ✅ |
| 6 | 15 | 3Gi | 3Gi | (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 不需要重启的前提:
- K8s ≥ 1.24
- CSI driver 支持 ONLINE_RESIZE capability
- 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 一遍
| # | 埋雷 | 实测产出 | 关键数据 |
|---|---|---|---|
| #5 | vLLM 指标体系 + LLM HPA | 29 metrics 分类 + 真实快照 | 89 req 累计, TTFT p99 < 100ms |
| #9 | MCP 协议 | server + client 6 步全通 | protocol 2025-11-25, isError 错误模式 |
| #1 | Admission Webhook | k3s 端到端 3 case | nginx:latest deny ✅ / nginx:1.25 allow ✅ / redis deny ✅ |
| #3 | Custom Metrics Adapter | APIService 实测 + adapter rule 4 字段拆解 | v1beta1.metrics.k8s.io 真实 yaml |
| T2-A | Leader Election | gpu1 Python lease 抢主 + 杀 leader | transitions 8→9 跨 1 秒切换 |
| T2-B | Secret 加密 | m1 etcdctl 实证密文 vs 明文 | k8s:enc:aescbc:v1:key1: 前缀 hexdump |
| T2-C | eBPF / bpftrace | gpu1 3 个 oneliner | containerd 212 / k3s 111 / python 19 syscalls |
| T2-D | PVC 在线扩容 | m1 sandbox PVC 1Gi → 3Gi | 12 秒完成,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 · 深度调参