从零构建 Arthas 统一诊断平台:K8s 集群下的 Java 应用智能诊断实践
摘要:本文介绍了一个面向 K8s 集群的 Java 应用统一诊断平台的完整实现。该平台通过 Web UI 封装 Arthas 诊断命令,支持 K8s 集群管理、Agent 自动注入、流式命令处理、自动采集分析,并基于 ReAct + Function Calling 架构实现了 AI 智能诊断——用户只需描述问题现象,大模型就能自主选择 Arthas 命令采集数据,多轮推理后输出结构化诊断结论。





一、项目背景与动机
1.1 运维痛点
在大型 Java 应用集群(数十甚至上百个 Pod)的生产环境中,性能诊断一直是运维团队的痛点:
- 命令门槛高:Arthas 命令繁多,运维人员需要记忆大量命令和参数
- 流程繁琐:定位一个问题往往需要登录跳板机 → 查找 Pod → 执行多条命令 → 人工综合分析
- 经验依赖:诊断结果高度依赖个人经验,缺乏标准化的分析流程
- 响应滞后:人工介入慢,问题从发现到定位往往需要数十分钟
1.2 解决思路
我们想到:能否将 Arthas 的能力通过 Web UI 封装起来,让运维人员通过点击按钮就能完成诊断?更进一步,能否让 AI 像专家一样,根据问题现象自主决定采集什么数据、如何分析?
基于这个思路,我们开发了 Arthas 统一诊断平台,核心能力包括:
- 集群管理:对接 K8s,统一管理多集群、多命名空间下的 Java Pod
- Agent 注入:通过 Mutating Webhook 自动为 Pod 注入 Arthas sidecar
- 诊断命令:将 Arthas 命令封装为 REST API,支持流式命令(trace/monitor/watch)
- 自动采集分析:一键采集线程、GC、内存、CPU 数据,生成综合诊断报告
- AI 智能诊断:大模型自主选择命令、多轮推理、输出结构化结论
二、技术架构
2.1 技术栈
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 后端 | Go 1.22 + Gin + GORM | 高性能、编译为单二进制、部署极简 |
| 前端 | Vue3 + TypeScript + Element Plus | 组件丰富、TypeScript 类型安全 |
| 数据库 | SQLite(纯 Go 驱动) | 无需 CGO,单文件存储,适合单机部署 |
| K8s 交互 | client-go v0.30.3 | 官方客户端,支持多集群连接池 |
| AI | OpenAI 兼容协议 | 支持 DeepSeek/通义/GPT 等多种大模型 |
2.2 整体架构
┌──────────────────────────────────────────────────────┐
│ 前端 (Vue3) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 集群管理 │ │ 诊断控制台│ │ 自动分析 │ │ AI诊断 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬────┘ │
└───────┼────────────┼────────────┼────────────┼──────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ 后端 (Go + Gin) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │Handler │ │Service │ │Repository│ │AI Agent │ │
│ │HTTP 路由 │ │业务逻辑 │ │数据访问 │ │大模型 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬────┘ │
└───────┼────────────┼────────────┼────────────┼──────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│ K8s API │ │arthas- │ │ SQLite │ │ 大模型 API │
│ Server │ │tunnel │ │ Database │ │(DeepSeek等) │
└──────────┘ └──────────┘ └──────────┘ └──────────────┘
2.3 项目结构
arthas-diag-platform/
├── backend/
│ ├── cmd/server/main.go # 入口,路由注册,依赖注入
│ ├── config/config.go # 配置结构体,YAML加载
│ ├── config.yaml # 配置文件
│ ├── internal/
│ │ ├── handler/ # HTTP处理器
│ │ ├── service/ # 业务逻辑层
│ │ ├── repository/ # 数据访问层
│ │ ├── model/model.go # 数据模型
│ │ ├── k8s/ # K8s客户端管理
│ │ ├── tunnel/ # tunnel-server交互
│ │ ├── webhook/ # Mutating Webhook
│ │ ├── scheduler/ # 定时采集调度
│ │ └── middleware/ # 中间件(JWT/审计)
│ └── arthas.db # SQLite数据库
└── frontend/
└── src/
├── api/ # API模块
├── stores/ # Pinia Store
├── views/ # 页面组件
├── components/ # 可复用组件
└── router/ # 路由配置
三、核心功能实现
3.1 K8s 集群管理与 Agent 注入
平台需要管理多个 K8s 集群,每个集群可能有多个命名空间。我们使用 client-go 构建了多集群连接池:
// internal/k8s/manager.go
type ClientManager struct {
mu sync.RWMutex
clients map[uint]*kubernetes.Clientset
}
func (m *ClientManager) AddCluster(id uint, apiServer, token, kubeConfig string) error {
// 支持两种认证方式:KubeConfig YAML 或 API Server + Token
config, err := rest.InClusterConfig()
if err != nil {
// 使用提供的 kubeconfig 或 token 构建配置
config, err = buildConfig(apiServer, token, kubeConfig)
}
clientset, err := kubernetes.NewForConfig(config)
m.mu.Lock()
m.clients[id] = clientset
m.mu.Unlock()
return nil
}
Agent 注入通过 K8s Mutating Webhook 实现。当 Pod 被打上 arthas-injection=enabled 标签或注解时,Webhook 通过 JSON Patch 自动注入 Arthas sidecar 容器。
实际注入的 sidecar 容器定义如下(对应 webhook.go 中的 buildSidecarPatch):
// internal/webhook/webhook.go
sidecar := map[string]interface{}{
"name": "arthas-agent",
"image": "hengyunabc/arthas:3.7.2",
"command": []string{"sh", "-c", fmt.Sprintf(
// 1. 复制 Arthas 到共享 /tmp 目录
// Attach API 要求目标 JAR 在主容器可访问的路径
"cp -r /opt/arthas /tmp/arthas && "+
"echo 'Waiting for Java process...'; "+
"for i in $(seq 1 60); do JAVA_PROC=$(jps -l 2>/dev/null | grep -v Jps | head -1) && [ -n \"$JAVA_PROC\" ] && break; sleep 2; done; "+
"if [ -z \"$JAVA_PROC\" ]; then echo 'No Java process found, keeping container alive'; exec tail -f /dev/null; fi; "+
"SELECT_NAME=$(echo \"$JAVA_PROC\" | awk '{print $2}' | sed 's/.*\\///' | sed 's/\\.jar//'); "+
"echo \"Found Java process: $SELECT_NAME, waiting for JVM to initialize...\"; sleep 10; "+
"java -jar /tmp/arthas/arthas-boot.jar --tunnel-server %s --app-name $SELECT_NAME --agent-id $(hostname) --select $SELECT_NAME --attach-only; "+
"echo 'Attach completed, keeping container alive'; exec tail -f /dev/null",
h.tunnelAddress,
)},
"env": []map[string]interface{}{
{"name": "ARTHAS_TUNNEL_ADDRESS", "value": h.tunnelAddress},
{"name": "ARTHAS_APP_NAME", "valueFrom": fieldRef("metadata.name")},
{"name": "ARTHAS_NAMESPACE", "valueFrom": fieldRef("metadata.namespace")},
},
"resources": map[string]interface{}{
"limits": map[string]string{"cpu": "500m", "memory": "512Mi"},
"requests": map[string]string{"cpu": "100m", "memory": "128Mi"},
},
"volumeMounts": []map[string]interface{}{
{"name": "tmp-dir", "mountPath": "/tmp"},
},
}
同时,Webhook 还会自动注入以下 Pod 级配置:
// 1. 启用共享 PID 命名空间(sidecar 需要看到业务容器的 Java 进程)
{"op": "add", "path": "/spec/shareProcessNamespace", "value": true}
// 2. 添加共享 emptyDir 卷(/tmp 用于 Arthas socket 通信)
{"op": "add", "path": "/spec/volumes/-", "value": {"name": "tmp-dir", "emptyDir": {}}}
// 3. 为每个已有业务容器挂载共享卷
{"op": "add", "path": "/spec/containers/0/volumeMounts/-", "value": {"name": "tmp-dir", "mountPath": "/tmp"}}
注入流程的关键设计:
- 显式注入:必须通过标签
arthas-injection=enabled或注解显式启用,不会误注入未授权的 Pod - 共享 PID 命名空间:sidecar 容器与业务容器共享 PID 命名空间,使
jps能看到 Java 进程 - 共享 /tmp 目录:Arthas 依赖
/tmp目录与 Java 进程进行 socket 通信,通过emptyDir卷共享 - 资源限制:sidecar 容器限制 500m CPU / 512Mi 内存,避免影响业务容器
- 容错设计:
|| true确保 Arthas 启动失败不会导致 sidecar 容器崩溃,tail -f /dev/null保持容器存活 - 命名空间白名单:通过
injectNamespaces配置限制可注入的命名空间,默认仅default
3.2 Arthas Tunnel Server 对接
Arthas 诊断的核心链路是:平台后端 → arthas-tunnel-server → Arthas Agent(注入在 Pod 侧的 sidecar 容器中)。tunnel-server 是 Arthas 官方提供的代理组件,负责管理所有 Agent 的连接、认证和命令路由。
3.2.1 整体通信链路
┌──────────────┐ HTTP API ┌──────────────────┐ WebSocket ┌──────────────────┐
│ 平台后端 │ ←────────────→ │ arthas-tunnel- │ ←────────────→ │ Arthas Agent │
│ (Go/Gin) │ │ server │ relay 通道 │ (Pod sidecar) │
│ │ 获取Agent列表 │ │ 逐字符转发 │ │
│ │ 登录认证 │ 管理所有Agent │ │ 执行Arthas命令 │
└──────────────┘ └──────────────────┘ └──────────────────┘
tunnel-server 提供两种通信方式:
- HTTP API:获取 Agent 列表、健康检查、登录认证
- WebSocket:建立 relay 通道,逐字符转发 Arthas 命令和输出
3.2.2 认证与登录
tunnel-server 使用 Spring Security 表单登录,包含 CSRF 保护。登录流程需要两步:
// internal/tunnel/tunnel.go
func (c *TunnelClient) login() error {
// 第一步: GET /login 获取 JSESSIONID 和 CSRF token
resp, err := c.httpClient.Do(req)
// 从响应中提取 JSESSIONID Cookie
for _, cookie := range resp.Cookies() {
if cookie.Name == "JSESSIONID" {
c.sessionCookie = cookie
}
}
// 从 HTML 中提取 CSRF token
// <input name="_csrf" type="hidden" value="xxx" />
csrfPrefix := `name="_csrf" type="hidden" value="`
// ... 提取 csrfToken
// 第二步: POST /login 提交用户名密码和 CSRF token
formData := url.Values{}
formData.Set("username", c.username)
formData.Set("password", c.password)
formData.Set("_csrf", csrfToken)
req, _ = http.NewRequest("POST", loginURL, strings.NewReader(formData.Encode()))
req.AddCookie(c.sessionCookie)
// 不跟随重定向,检查 302 响应判断登录成功
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, _ = client.Do(req)
if resp.StatusCode == 302 || resp.StatusCode == 200 {
c.loggedIn = true
return nil
}
return fmt.Errorf("登录失败: HTTP %d", resp.StatusCode)
}
关键设计:Session Cookie 过期后(HTTP 302/303),自动重新登录并重试请求,对上层调用者透明。
3.2.3 Agent 列表获取
通过 HTTP API 获取所有已注册到 tunnel-server 的 Agent:
func (c *TunnelClient) GetAgents() ([]map[string]interface{}, error) {
// 先登录获取 Session Cookie
if err := c.login(); err != nil {
return nil, err
}
// GET /actuator/arthas 获取 Agent 列表
// 返回格式: {"agents":{"agentId1":{"host":"x","port":y},...}}
url := fmt.Sprintf("%s/actuator/arthas", c.httpAddress)
req, _ := http.NewRequest("GET", url, nil)
req.AddCookie(c.sessionCookie)
// Session 过期自动重新登录重试
if resp.StatusCode == http.StatusFound {
c.loggedIn = false
c.sessionCookie = nil
c.login() // 重新登录
// 用新 Cookie 重试...
}
// 解析 agents map 为数组
var rawResult struct {
Agents map[string]map[string]interface{} `json:"agents"`
}
json.Unmarshal(body, &rawResult)
// 将 agentId 加入每个 agent 信息中
for agentID, info := range rawResult.Agents {
agent := map[string]interface{}{"agentId": agentID}
for k, v := range info { agent[k] = v }
agents = append(agents, agent)
}
return agents, nil
}
3.2.4 WebSocket 连接与命令执行
tunnel-server 的 WebSocket 连接使用自定义 relay 协议,逐字符转发。这是整个对接中最复杂的部分:
func (c *TunnelClient) SendCommand(agentID, command string) (string, error) {
// 1. 建立 WebSocket 连接
// ws://<tunnel-address>/ws?method=connectArthas&id=<agentId>
wsURL := fmt.Sprintf("%s/ws?method=connectArthas&id=%s", baseURL, url.QueryEscape(agentID))
conn, _ := dialer.Dial(wsURL, nil)
// 2. 读取 Arthas banner,等待提示符出现
// Arthas 连接后会发送欢迎信息和提示符 "arthas@PID$ "
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
for {
_, msgBytes, _ := conn.ReadMessage()
msg := string(msgBytes)
if strings.Contains(msg, "$ ") || strings.Contains(msg, "arthas@") {
break // 提示符出现,可以发送命令了
}
}
// 3. 逐字符发送命令
// relay 通道协议: {"action":"read","data":"<char>"}
for _, ch := range command {
msg, _ := json.Marshal(map[string]string{"action": "read", "data": string(ch)})
conn.WriteMessage(websocket.TextMessage, msg)
time.Sleep(1 * time.Millisecond)
}
// 发送回车
msg, _ := json.Marshal(map[string]string{"action": "read", "data": "\r"})
conn.WriteMessage(websocket.TextMessage, msg)
// 4. 读取输出,等待 Arthas 提示符再次出现
// 提示符格式: [arthas@PID]$ 或 arthas@PID$
// 对于 sysprop/sysenv 等长输出命令,额外检查 "Affect(" 消息
for {
_, msgBytes, _ := conn.ReadMessage()
msg := string(msgBytes)
accumulated += msg
// 检测到提示符 → 命令执行完毕
if strings.Contains(msg, "$ ") && strings.Contains(accumulated, "arthas@") {
break
}
// 检测到 Affect → 命令影响统计
if strings.Contains(accumulated, "Affect(") {
// 再读取一些数据确保完整,然后 break
}
}
return accumulated, nil
}
协议要点:
- 每条消息是一个 JSON 对象
{"action":"read","data":"x"},data是单个字符 - 这是 tunnel-server 的 relay 通道协议,不是简单的文本转发
- 每次命令执行都创建新的 WebSocket 连接,因为 tunnel-server 的
connectArthas会为每个连接创建独立的 relay 通道
3.2.5 WebSocket 代理(诊断控制台)
前端诊断控制台的 WebSocket 代理实现双向数据转发:
func (c *TunnelClient) ProxyWebSocket(agentID string, clientConn *websocket.Conn) {
// 每次代理会话创建新的 WebSocket 连接到 tunnel-server
tunnelConn, _ := c.ConnectToAgent(agentID)
// 双向转发
// 前端 → tunnel-server
go func() {
for {
_, message, _ := clientConn.ReadMessage()
tunnelConn.WriteMessage(websocket.TextMessage, message)
}
}()
// tunnel-server → 前端
go func() {
for {
tunnelConn.SetReadDeadline(time.Now().Add(300 * time.Second))
_, message, _ := tunnelConn.ReadMessage()
clientConn.WriteMessage(websocket.TextMessage, message)
}
}()
<-done // 等待任一方向断开
}
Mock 模式:当 tunnel-server 不可用时(tunnelAddress 为空或连接失败),自动进入 Mock 模式,返回模拟数据,确保前端页面可以正常开发和测试。后台每 10 秒健康检查一次,tunnel-server 恢复后自动退出 Mock 模式。
3.2.6 配置与容错
// Tunnel 客户端配置
type TunnelConfig struct {
TunnelAddress string // WebSocket 地址: ws://arthas-tunnel:7777/ws
HTTPAddress string // HTTP 地址: http://arthas-tunnel:7777
Timeout time.Duration // 连接超时
RetryCount int // 重试次数
Username string // Basic Auth 用户名
Password string // Basic Auth 密码
}
容错机制:
- 连接重试:WebSocket 连接失败自动重试(默认 3 次)
- Session 续期:Cookie 过期自动重新登录
- 后台健康检查:Mock 模式下每 10 秒检测 tunnel-server 是否恢复
- 自动切换:tunnel-server 不可用时自动降级为 Mock 模式,恢复后自动切回真实模式
3.3 Arthas 命令封装与流式处理
Arthas 命令分为两类:一次性命令(如 thread、memory)和流式命令(如 monitor、trace、watch)。流式命令需要持续读取输出,并在指定时间后发送 Ctrl+C 终止。
我们使用 WebSocket 代理实现前端到 Arthas Agent 的实时通信:
前端 ←WebSocket→ 后端 ←WebSocket→ arthas-tunnel-server ←TCP→ Arthas Agent
3.3.1 一次性命令处理
一次性命令(如 thread -n 10、memory)的处理相对简单:通过 WebSocket 连接到 tunnel-server → 等待 Arthas 提示符 → 逐字符发送命令 → 读取输出直到提示符再次出现 → 返回结果。
这个流程在 3.2.4 节 中已经详细描述。一次性命令的关键是检测 Arthas 提示符(arthas@PID$ )来判断命令是否执行完毕。不同命令的结束标志略有不同:
- 普通命令(
thread、memory):提示符再次出现即结束 sysprop/sysenv等长输出命令:先检测Affect(消息,再等待提示符trace命令:检测Affect(后,额外等待 2 秒读取完整输出
3.3.2 流式命令处理:monitor
monitor 命令按固定间隔输出方法调用统计数据,需要持续读取指定时间后自动终止:
// internal/tunnel/tunnel.go
func (c *TunnelClient) MonitorCommand(agentID, command string, durationSec int) (string, error) {
monitorCmd := fmt.Sprintf("%s -c 1", command) // -c 1 表示每1秒输出一次
conn, _ := c.ConnectToAgent(agentID)
defer conn.Close()
// 读取 banner,等待 Arthas 提示符
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
for {
_, msgBytes, _ := conn.ReadMessage()
if strings.Contains(string(msgBytes), "$ ") { break }
}
// 逐字符发送命令(relay 通道协议)
for _, ch := range monitorCmd {
msg, _ := json.Marshal(map[string]string{"action": "read", "data": string(ch)})
conn.WriteMessage(websocket.TextMessage, msg)
time.Sleep(1 * time.Millisecond)
}
// 读取输出,持续 durationSec 秒
var rawBuf strings.Builder
deadline := time.Now().Add(time.Duration(durationSec+2) * time.Second)
affectReceived := false
for {
if time.Now().After(deadline) {
// 到达截止时间,发送 Ctrl+C 终止
stopMsg, _ := json.Marshal(map[string]string{"action": "read", "data": string(rune(0x03))})
conn.WriteMessage(websocket.TextMessage, stopMsg)
break
}
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
_, msgBytes, err := conn.ReadMessage()
if err != nil { break }
rawBuf.Write(msgBytes)
// 检测到 Affect( 消息后,持续读取直到 deadline
if !affectReceived && strings.Contains(rawBuf.String(), "Affect(") {
affectReceived = true
// 持续读取直到 deadline,每次读取超时用剩余时间
for {
remaining := time.Until(deadline)
if remaining <= 0 { break }
readTimeout := remaining
if readTimeout < 2*time.Second { readTimeout = 2 * time.Second }
conn.SetReadDeadline(time.Now().Add(readTimeout))
_, extraBytes, extraErr := conn.ReadMessage()
if extraErr != nil { break }
rawBuf.Write(extraBytes)
if time.Now().After(deadline) { break }
}
break
}
}
// 后处理:去除 ANSI 转义、修复换行分割的数据行
rawStr := ansiRegex.ReplaceAllString(rawBuf.String(), "")
rawStr = strings.ReplaceAll(rawStr, "\r", "")
// 修复 YYYY-MM-DD 数据行被终端宽度换行分割的问题
// ...
return rawStr, nil
}
关键设计:
- 使用
time.Until(deadline)计算剩余读取超时,确保精确控制采样时长 - Affect 消息检测:
monitor命令最后会输出Affect(class count: X, method count: Y)统计信息,检测到后继续读取直到截止时间,确保数据完整 - 换行修复:终端宽度可能导致数据行被分割(如
2026-06-01被拆成多行),需要按日期正则合并续行
3.3.3 流式命令处理:trace
trace 命令在每次方法调用时输出调用链,同样是流式输出。处理逻辑与 monitor 类似,但输出格式不同:
func (c *TunnelClient) TraceCommand(agentID, command string, durationSec int) (string, error) {
conn, _ := c.ConnectToAgent(agentID)
defer conn.Close()
// 读取 banner → 发送命令(与 monitor 相同)
// ...
// 读取输出,持续 durationSec 秒后 Ctrl+C 终止
var rawBuf strings.Builder
deadline := time.Now().Add(time.Duration(durationSec+2) * time.Second)
affectReceived := false
for {
if time.Now().After(deadline) {
// 发送 Ctrl+C
stopMsg, _ := json.Marshal(map[string]string{"action": "read", "data": string(rune(0x03))})
conn.WriteMessage(websocket.TextMessage, stopMsg)
break
}
// ... 读取逻辑与 monitor 相同
}
// 去除 ANSI 转义序列
rawStr := ansiRegex.ReplaceAllString(rawBuf.String(), "")
return rawStr, nil
}
关键区别:trace 的输出是调用链树形结构(+---[0.2ms] com.example.Service:method()),需要完整读取每个调用的多行输出。同样通过 Affect( 消息检测确保读取完整。
3.3.4 watch 命令的特殊处理
watch 命令观察方法的入参和返回值,输出是异步的(每次方法调用触发一次输出)。处理时需要更灵活的超时策略:
// watch 命令的特殊处理逻辑
if isWatchCmd {
// 等待提示符出现表示用户手动退出
if strings.Contains(msg, "$ ") && strings.Contains(accumulated, "arthas@") {
break
}
// 已读取大量数据(超过200条)也结束
if len(outputParts) > 200 {
break
}
// 每条消息后短暂等待,给流式输出更多时间
if len(outputParts) > 2 {
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
}
continue
}
关键区别:watch 不像 monitor/trace 那样有明确的结束标志,而是通过提示符或数据量上限来判断结束。
3.4 自动采集分析
自动采集分析是一键式的综合诊断功能,按固定流程采集多项指标:
Step 1: thread -n 10 → 线程状态分析
Step 2: thread -b → 死锁检测
Step 3: perfcounter -c gc → GC 分析
Step 4: memory → 内存分析
Step 5: profiler start/dumpFlat/stop → CPU 热点采样
Step 6: 关联分析 → 跨指标关联
Step 7: 生成报告 → 综合结论
CPU 热点分析是一个比较有挑战性的功能。Arthas 的 profiler start --duration 60 --format flat 会把结果写入 Pod 上的文件,终端只输出 “OK” 和文件路径。我们改为手动控制流程:
// internal/service/analysis.go
func (s *AnalysisService) collectCPUProfile(agentID string, durationSec int) *model.AutoCPUReport {
// Step 1: 启动 profiler(不带参数,输出到终端)
tunnelClient.SendCommand(agentID, "profiler start")
// Step 2: 等待采样完成
time.Sleep(time.Duration(durationSec) * time.Second)
// Step 3: dumpFlat 将结果输出到终端
output, _ := tunnelClient.SendCommand(agentID, "profiler dumpFlat")
// Step 4: 停止 profiler
tunnelClient.SendCommand(agentID, "profiler stop")
// 解析 flat 格式输出
return s.parseFlatProfile(output, durationSec)
}
flat 格式输出的解析使用正则匹配:
// 匹配格式: ns percent samples top
lineRegex := regexp.MustCompile(`^\s*(\d+)\s+([\d.]+)%\s+(\d+)\s+(.+)$`)
关联分析是自动采集的亮点,它跨指标发现潜在问题:
// GC频繁 + 内存高 → 可能内存泄漏
if gcReport.GCCount > 100 && memReport.HeapPercent > 85 {
correlations = append(correlations, {
Type: "gc_memory",
Severity: "critical",
Title: "疑似内存泄漏",
Detail: fmt.Sprintf("GC次数=%d,堆内存使用率=%.1f%%,GC后内存未有效回收",
gcReport.GCCount, memReport.HeapPercent),
})
}
3.5 AI 智能诊断(核心亮点)
这是平台最核心的功能——让大模型像 JVM 诊断专家一样工作。
3.5.1 架构设计:ReAct + Function Calling
我们采用 ReAct(Reasoning + Acting) 模式,让大模型在"思考"和"行动"之间循环:
用户输入: "接口响应变慢,偶发超时"
↓
┌─── Agent 循环(最多 8 轮)───┐
│ │
│ 大模型思考: "响应慢可能是 │
│ 线程阻塞或GC导致,先查线程" │
│ ↓ │
│ 选择工具: thread_top(n=10) │
│ ↓ │
│ 后端执行: thread -n 10 │
│ 返回数据: 8个BLOCKED线程... │
│ ↓ │
│ 大模型思考: "发现大量阻塞, │
│ 需要查看堆栈确认锁等待" │
│ ↓ │
│ 选择工具: thread_state │
│ ↓ │
│ ...(继续循环) │
│ ↓ │
│ 大模型输出最终结论 JSON │
└──────────────────────────────┘
↓
输出: 根因分析 + 优化建议 + 风险等级
3.5.2 工具定义
我们将 Arthas 命令封装为大模型可调用的"工具",只暴露安全的只读命令:
// internal/service/ai_agent.go
func (s *AIAgentService) getToolDefinitions() []map[string]interface{} {
return []map[string]interface{}{
{
"type": "function",
"function": map[string]interface{}{
"name": "thread_top",
"description": "查看CPU占用最高的N个线程,用于排查CPU飙高问题",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"n": map[string]interface{}{
"type": "integer",
"description": "返回线程数量,默认10",
"default": 10,
},
},
},
},
},
// ... 其他 11 个工具
}
}
12 个可用工具覆盖了线程、内存、GC、CPU、方法追踪、源码查看等诊断场景。
3.5.3 Agent 循环核心逻辑
func (s *AIAgentService) Diagnose(req DiagnoseRequest, onStep func(step model.AIDiagnosisStep)) (*DiagnoseResult, error) {
messages := s.initMessages(req.Symptom, req.PodName)
deadline := time.Now().Add(5 * time.Minute)
for round := 1; round <= maxRounds; round++ {
if time.Now().After(deadline) { break }
// 1. 调用大模型,让它思考并选择工具
llmResp, err := s.callLLMWithTools(messages)
// 2. 如果大模型未调用工具,说明它认为可以下结论了
if len(llmResp.ToolCalls) == 0 {
conclusion := s.parseConclusion(llmResp.Content)
result.Conclusion = conclusion
result.Status = "completed"
break
}
// 3. 处理每个工具调用
for _, tc := range llmResp.ToolCalls {
// 安全校验:白名单 + 正则校验
cmd, output, err := s.executeTool(req.AgentID, tc.Function.Name, args)
// 截断过长输出(8KB),防止 token 爆炸
if len(output) > 8*1024 {
output = output[:8*1024] + "\n... [截断]"
}
// 4. 通过 SSE 实时推送到前端
onStep(step)
// 5. 把工具结果追加到对话历史,供下一轮推理使用
messages = append(messages, map[string]interface{}{
"role": "tool",
"tool_call_id": tc.ID,
"content": output,
})
}
}
return result, nil
}
3.5.4 安全控制
AI Agent 的安全性至关重要,我们做了多层防护:
- 命令白名单:只暴露 12 个只读工具,
redefine、shutdown、stop等危险命令禁止 - 参数正则校验:类名、方法名、字段名均用正则白名单校验,防止注入攻击
- 轮数限制:最多 8 轮,防止死循环
- 超时控制:单命令 30 秒,总 5 分钟
- 输出截断:单条命令输出不超过 8KB,防止 token 爆炸
// 安全校验正则
var classNameRegex = regexp.MustCompile(`^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$`)
var identifierRegex = regexp.MustCompile(`^[a-zA-Z_$][a-zA-Z0-9_$]*$`)
func isValidClassName(s string) bool {
s = strings.TrimSpace(s)
if s == "" || len(s) > 200 { return false }
return classNameRegex.MatchString(s)
}
3.5.5 流式输出(SSE)
前端通过 Server-Sent Events 实时接收 Agent 的每一步进展,用户体验类似 ChatGPT 的工具调用展示:
// 前端使用 fetch + ReadableStream 处理 SSE
const resp = await fetch('/api/analysis/ai-diagnose', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id, symptom }),
})
const reader = resp.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const msg = JSON.parse(line.slice(6))
if (msg.type === 'step') {
steps.value.push(msg.data) // 实时展示每一步
} else if (msg.type === 'done') {
conclusion.value = msg.data.conclusion // 最终结论
}
}
}
3.6 RBAC 权限控制
平台支持三种角色:admin(管理员)、operator(运维人员)、viewer(只读用户)。通过 JWT + 中间件实现:
// 路由级权限控制
users := api.Group("/users")
users.Use(middleware.RequireRole(model.RoleAdmin)) // 仅 admin 可访问用户管理
{
users.GET("", authH.ListUsers)
users.POST("", authH.CreateUser)
users.DELETE("/:id", authH.DeleteUser)
}
// 前端按钮级权限控制
// <el-button v-if="canDelete()" @click="handleDelete">删除</el-button>
3.7 操作审计
所有写操作自动记录审计日志,使用带缓冲通道异步写入,不阻塞请求:
// internal/middleware/audit.go
func AuditLogger(logRepo *repository.LogRepository) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "DELETE" {
// 异步写入审计日志
go func() {
logRepo.CreateOperationRecord(&model.OperationRecord{
Operator: c.GetString("username"),
Action: c.Request.Method,
Target: c.FullPath(),
Detail: fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL),
IP: c.ClientIP(),
})
}()
}
c.Next()
}
}
四、AI 智能诊断的深入设计
4.1 为什么选择 ReAct + Function Calling
在 AI 诊断场景下,我们面临的核心问题是:大模型需要与真实环境交互。纯文本生成模式(如"请分析以下数据")存在两个问题:
- 数据获取不可控:大模型无法主动获取数据,需要人工粘贴 Arthas 输出
- 分析深度有限:一次性分析容易遗漏,无法根据中间结果调整策略
ReAct + Function Calling 模式让大模型具备了"动手能力"——它可以根据问题现象自主选择采集什么数据,根据返回结果决定下一步行动,形成完整的诊断闭环。
4.2 Prompt 设计
系统提示词是 Agent 行为的关键。我们精心设计了诊断流程指引:
你是一位资深JVM性能诊断专家,正在诊断 Pod "xxx" 的 Java 应用问题。
你可以调用以下工具(Arthas命令):
- thread_top: 查看CPU占用最高的N个线程
- thread_blocked: 检测死锁
- memory: 查看JVM内存各区域使用情况
- ...
诊断流程:
1. 根据问题现象,选择1-2个最相关的工具采集数据
2. 分析返回的数据,判断是否需要补充其他命令
3. 最多8轮,得出结论时返回JSON格式的最终诊断
最终结论JSON格式:
{"summary":"总体结论","root_causes":["根因1"],"suggestions":["建议1"],"risk_level":"warning"}
4.3 大模型兼容性
我们使用 OpenAI Chat Completions 兼容协议,一套代码可对接多种大模型:
# config.yaml - DeepSeek
ai:
provider: "deepseek"
base_url: "https://api.deepseek.com/v1"
model: "deepseek-chat"
api_key: "sk-xxxxxxxx"
# config.yaml - 通义千问
ai:
provider: "qwen"
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
model: "qwen-plus"
api_key: "sk-xxxxxxxx"
HTTP 调用核心逻辑:
func (s *AIAgentService) callLLMWithTools(messages []map[string]interface{}) (*llmResponse, error) {
payload := map[string]interface{}{
"model": s.cfg.Model,
"messages": messages,
"tools": s.getToolDefinitions(),
"tool_choice": "auto",
"temperature": 0.3,
"max_tokens": s.cfg.MaxTokens,
}
url := strings.TrimRight(s.cfg.BaseURL, "/") + "/chat/completions"
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+s.cfg.APIKey)
resp, err := httpClient.Do(req)
// 解析 OpenAI 格式的 tool_calls 响应
// ...
}
4.4 成本与效果
以 DeepSeek 为例,单次诊断约消耗 5K-15K tokens,成本约 0.01-0.03 元。对于生产环境的关键问题诊断,这个成本完全可接受。
五、前端设计要点
5.1 诊断控制台
诊断控制台是核心交互页面,左侧为 Agent 选择器(集群 → 命名空间 → Pod → Agent 四级级联),右侧为命令输出区。
<!-- 4级级联选择器 -->
<el-select v-model="selectedCluster" @change="onClusterChange">
<el-option v-for="c in clusters" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<el-select v-model="selectedNamespace" @change="onNamespaceChange">
<el-option v-for="ns in namespaces" :key="ns" :label="ns" :value="ns" />
</el-select>
<el-select v-model="selectedPod" @change="onPodChange">
<el-option v-for="p in pods" :key="p.name" :label="p.name" :value="p.name" />
</el-select>
<el-select v-model="selectedAgent">
<el-option v-for="a in agents" :key="a.agentId" :label="a.agentId" :value="a.agentId" />
</el-select>
5.2 AI 诊断页面
AI 诊断页面的核心交互是实时展示 Agent 的思考过程:
<!-- 诊断过程流式展示 -->
<div v-for="(step, i) in steps" :key="i" class="border-l-2 border-blue-400 pl-3">
<div class="text-xs text-gray-400">第 {{ step.round }} 轮 · {{ step.action }}</div>
<div v-if="step.thought" class="text-sm mt-1 text-purple-400">
💭 {{ step.thought }}
</div>
<div class="text-xs font-mono mt-1 text-green-400">$ {{ step.command }}</div>
<pre class="text-xs mt-1 p-2 bg-gray-800 rounded max-h-40 overflow-auto">
{{ step.observation }}
</pre>
</div>
<!-- 最终结论 -->
<div v-if="conclusion" class="rounded-lg bg-gradient-to-br from-purple-500/10 to-blue-500/10 p-4">
<h3 class="text-sm font-medium mb-3">诊断结论</h3>
<div class="text-sm mb-3">{{ conclusion.summary }}</div>
<div v-if="conclusion.root_causes?.length">
<div class="text-xs text-gray-400 mb-1">根因分析:</div>
<ul class="list-disc list-inside text-sm">
<li v-for="(r, i) in conclusion.root_causes" :key="i">{{ r }}</li>
</ul>
</div>
</div>
5.3 深色/浅色主题
支持一键切换深色/浅色主题,使用 CSS 变量实现:
html.dark {
--bg-primary: #1a1a2e;
--bg-card: #16213e;
--text-primary: #e4e4e7;
--text-secondary: #9ca3af;
}
html.light {
--bg-primary: #f8fafc;
--bg-card: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
}
六、部署与运维
6.1 编译部署
后端编译为单二进制文件,无需运行时依赖:
# Linux 交叉编译(在 Windows 上执行)
$env:GOOS="linux"; $env:GOARCH="amd64"
go build -o arthas-diag-server ./cmd/server
# 部署到 Linux 服务器
scp arthas-diag-server user@10.0.0.72:/opt/arthas-diag/
scp config.yaml user@10.0.0.72:/opt/arthas-diag/
# 生成 TLS 证书(指定 K8s API Server IP)
./arthas-diag-server -generate-cert 10.0.0.72
# 启动
./arthas-diag-server
项目使用纯 Go SQLite 驱动(github.com/glebarez/sqlite),无需 CGO,交叉编译无依赖问题。
6.2 配置管理
配置文件支持多级查找:
- 命令行参数
--config /path/to/config.yaml - 环境变量
CONFIG_FILE=/path/to/config.yaml - 当前目录
config.yaml - 可执行文件同目录
/etc/arthas-diag/
七、总结与展望
7.1 项目成果
经过迭代,平台已实现:
- 集群管理:多集群、多命名空间的统一管理
- Agent 注入:通过 Mutating Webhook 自动注入 Arthas sidecar
- 诊断命令:20+ Arthas 命令的 Web 化封装,支持流式命令
- 自动采集分析:一键采集线程/GC/内存/CPU,生成综合报告
- AI 智能诊断:ReAct + Function Calling 架构,大模型自主诊断
- 安全控制:RBAC 权限、操作审计、命令白名单、参数校验
7.2 核心经验
- 流式命令处理:Arthas 的流式命令需要精确的读取超时控制,使用
time.Until(deadline)比固定超时隔更可靠 - AI Agent 安全:必须严格控制工具白名单和参数校验,防止大模型生成危险命令
- SSE 流式输出:AI 诊断过程较长(1-5分钟),SSE 实时推送是提升用户体验的关键
- 纯 Go 技术栈:选择纯 Go 依赖(如
glebarez/sqlite)可以大幅简化部署和交叉编译
7.3 未来规划
- 告警通知:集成邮件/钉钉/飞书,异常自动推送
- K8s 事件监听:Pod 重启、OOM 等事件触发自动采集
- 诊断历史对比:支持同一 Pod 不同时间的诊断结果对比
- 多实例部署:引入 Redis 支持水平扩展
- AI 增强:基于历史诊断数据微调模型,提升诊断准确率
更多推荐
所有评论(0)