摘要:本文介绍了一个面向 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 统一诊断平台,核心能力包括:

  1. 集群管理:对接 K8s,统一管理多集群、多命名空间下的 Java Pod
  2. Agent 注入:通过 Mutating Webhook 自动为 Pod 注入 Arthas sidecar
  3. 诊断命令:将 Arthas 命令封装为 REST API,支持流式命令(trace/monitor/watch)
  4. 自动采集分析:一键采集线程、GC、内存、CPU 数据,生成综合诊断报告
  5. 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"}}

注入流程的关键设计:

  1. 显式注入:必须通过标签 arthas-injection=enabled 或注解显式启用,不会误注入未授权的 Pod
  2. 共享 PID 命名空间:sidecar 容器与业务容器共享 PID 命名空间,使 jps 能看到 Java 进程
  3. 共享 /tmp 目录:Arthas 依赖 /tmp 目录与 Java 进程进行 socket 通信,通过 emptyDir 卷共享
  4. 资源限制:sidecar 容器限制 500m CPU / 512Mi 内存,避免影响业务容器
  5. 容错设计|| true 确保 Arthas 启动失败不会导致 sidecar 容器崩溃,tail -f /dev/null 保持容器存活
  6. 命名空间白名单:通过 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 命令分为两类:一次性命令(如 threadmemory)和流式命令(如 monitortracewatch)。流式命令需要持续读取输出,并在指定时间后发送 Ctrl+C 终止。

我们使用 WebSocket 代理实现前端到 Arthas Agent 的实时通信:

前端 ←WebSocket→ 后端 ←WebSocket→ arthas-tunnel-server ←TCP→ Arthas Agent
3.3.1 一次性命令处理

一次性命令(如 thread -n 10memory)的处理相对简单:通过 WebSocket 连接到 tunnel-server → 等待 Arthas 提示符 → 逐字符发送命令 → 读取输出直到提示符再次出现 → 返回结果。

这个流程在 3.2.4 节 中已经详细描述。一次性命令的关键是检测 Arthas 提示符arthas@PID$ )来判断命令是否执行完毕。不同命令的结束标志略有不同:

  • 普通命令threadmemory):提示符再次出现即结束
  • 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 的安全性至关重要,我们做了多层防护:

  1. 命令白名单:只暴露 12 个只读工具,redefineshutdownstop 等危险命令禁止
  2. 参数正则校验:类名、方法名、字段名均用正则白名单校验,防止注入攻击
  3. 轮数限制:最多 8 轮,防止死循环
  4. 超时控制:单命令 30 秒,总 5 分钟
  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 诊断场景下,我们面临的核心问题是:大模型需要与真实环境交互。纯文本生成模式(如"请分析以下数据")存在两个问题:

  1. 数据获取不可控:大模型无法主动获取数据,需要人工粘贴 Arthas 输出
  2. 分析深度有限:一次性分析容易遗漏,无法根据中间结果调整策略

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 配置管理

配置文件支持多级查找:

  1. 命令行参数 --config /path/to/config.yaml
  2. 环境变量 CONFIG_FILE=/path/to/config.yaml
  3. 当前目录 config.yaml
  4. 可执行文件同目录
  5. /etc/arthas-diag/

七、总结与展望

7.1 项目成果

经过迭代,平台已实现:

  • 集群管理:多集群、多命名空间的统一管理
  • Agent 注入:通过 Mutating Webhook 自动注入 Arthas sidecar
  • 诊断命令:20+ Arthas 命令的 Web 化封装,支持流式命令
  • 自动采集分析:一键采集线程/GC/内存/CPU,生成综合报告
  • AI 智能诊断:ReAct + Function Calling 架构,大模型自主诊断
  • 安全控制:RBAC 权限、操作审计、命令白名单、参数校验

7.2 核心经验

  1. 流式命令处理:Arthas 的流式命令需要精确的读取超时控制,使用 time.Until(deadline) 比固定超时隔更可靠
  2. AI Agent 安全:必须严格控制工具白名单和参数校验,防止大模型生成危险命令
  3. SSE 流式输出:AI 诊断过程较长(1-5分钟),SSE 实时推送是提升用户体验的关键
  4. 纯 Go 技术栈:选择纯 Go 依赖(如 glebarez/sqlite)可以大幅简化部署和交叉编译

7.3 未来规划

  • 告警通知:集成邮件/钉钉/飞书,异常自动推送
  • K8s 事件监听:Pod 重启、OOM 等事件触发自动采集
  • 诊断历史对比:支持同一 Pod 不同时间的诊断结果对比
  • 多实例部署:引入 Redis 支持水平扩展
  • AI 增强:基于历史诊断数据微调模型,提升诊断准确率

更多推荐