java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Arthas MCP对Java线上诊断

使用Arthas MCP对Java应用进行线上诊断的实践指南

作者:zhaojiew10

本文介绍了使用Arthas4.1.9诊断Java应用常见问题(CPU飙高、内存泄漏、死锁、线程池满、慢方法、异常被吞)的方法,并提出构建一个全自动的Java应用诊断系统,通过AIagent结合MCP协议调用Arthas诊断工具,生成结构化的诊断报告,简化问题定位过程,需要的朋友可以参考下

前言

在实际的 Java 应用运维中,我们经常遇到以下问题:

这些问题在本地开发时容易调试,但在生产环境中,我们通常只能通过日志和监控来推测。Arthas 是阿里巴巴开源的 Java 诊断工具,可以帮助我们在线上环境快速定位问题。

传统诊断方式下,我们需要登录到目标环境,并通过Attach 到目标进程来进行线上诊断

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
arithas> thread -b  # 检测死锁
arithas> dashboard  # 查看系统概况
arithas> trace com.example.MyClass myMethod  # 追踪方法调用

本次使用的版本为Arthas 4.1.9,并通过java agent的方式随应用自动启动

# javaagent 方式
$ java -javaagent:arthas-agent.jar -jar app.jar

近期arthas新增了原生的MCP功能,让我们能够更加灵活地结合AI agent进行智能诊断。本文的核心目标为构建一个全自动的 Java 应用诊断系统。让AI Agent 能够理解问题并选择合适的诊断工具,生成结构化的诊断报告,包含问题定位、根因分析、解决建议。

MCP 是一个开放协议,用于连接 AI 应用和外部工具(如数据库、API、文件系统等)。它定义了标准化的接口,让 AI Agent 可以通过统一的 API 调用各种工具。此外,本次的agent框架使用Strands Agent

# 直接获得结构化数据
tools = await session.list_tools()
result = await session.call_tool("thread", arguments={"arguments": ["-b"]})

整体的逻辑结构如下

默认配置下,Arthas 会监听 127.0.0.1,导致外部无法连接。因此需要创建配置文件,明确指定监听地址和端口

# 配置文件路径
~/.arthas/lib/4.1.9/arthas/arthas.properties
# MCP Endpoint 配置
arthas.mcpEndpoint=/mcp
# HTTP Server 配置
arthas.ip=0.0.0.0          # 绑定所有网卡,允许远程访问
arthas.httpPort=9999        # HTTP API 端口
arthas.telnetPort=0         # 禁用 Telnet(0 表示禁用)
# 认证配置
arthas.password=S2TotROqw5zmT8po7m7aiifdZxUzReSrknDslxe5tIBjMWlN5yrpatPIJogSAX0v

启动应用

验证 Arthas MCP Server

$ java -javaagent:~/.arthas/lib/4.1.9/arthas/arthas-agent.jar -Xmx256m -cp out <java-class> 
# 测试 MCP Server,上个命令启动后会自动输出Bearer认证token
$ curl -H "Authorization: Bearer S2TotROqw5zmT8po7m7aiifdZxUzReSrknDslxe5tIBjMWlN5yrpatPIJogSAX0v" \
        http://localhost:9999/mcp
# 返回 SSE 流
event: endpoint
data: {"method": "initialize", ...}

系统提示词结构

GENERIC_SYSTEM_PROMPT = """你是一个资深的 Java 应用诊断专家,拥有 10 年以上的生产环境排查经验。
## 当前诊断目标
{class_name} - {desc}
## 诊断原则
1. **观察驱动**:首先观察系统的整体状态(JVM、线程、内存),形成初步假设
2. **工具优先级**:
   - 简单查询工具优先(thread、jvm、memory、dashboard)
   - 如需流式工具(trace、monitor、watch),注意可能受认证限制
   - 使用 jad 反编译源码来理解代码逻辑
3. **多角度验证**:结合多个工具的结果交叉验证,避免误判
4. **根因导向**:不只找到现象,要深入分析根本原因
5. **解决建议**:给出可操作的解决方案
## Arthas 工具速查
- `thread` / `thread -n 3` - 查看线程列表/CPU 最高线程
- `thread -b` - 检测死锁
- `thread <id>` - 查看特定线程的详细堆栈
- `jvm` - JVM 运行时信息
- `memory` - 内存使用情况
- `dashboard` / `dashboard -i 2000` - 系统概况/定期监控
- `jad <ClassName>` - 反编译类代码
- `sysprop` - 系统属性
- `logger` - 日志配置
## 认证说明
注意:部分流式工具(trace、monitor、watch)可能需要额外的会话认证。
如果遇到 401 错误,请改用其他工具组合(如 thread + jad)来达到同样的诊断目标。
## 输出要求
请按照以下格式输出诊断结果:
1. **观察发现**:通过工具看到的异常现象
2. **问题定位**:问题的具体位置(类名、方法名、行号)
3. **根因分析**:为什么会出现这个问题
4. **解决方案**:具体的修复建议
开始诊断吧!
"""

strands agent构造如下

# 3. 创建 MCP Client 和 Agent(使用通用系统提示词)
arthas_mcp = MCPClient(
    lambda: streamablehttp_client(
        url="http://localhost:9999/mcp",
        headers={"Authorization": "Bearer S2TotROqw5zmTxxxxxpatPIJogSAX0v"}
    )
)
# 构建通用系统提示词
system_prompt = GENERIC_SYSTEM_PROMPT.format(
    class_name=class_name,
    desc=scenario['desc']
)
agent = Agent(
    model=model,
    system_prompt=system_prompt,
    tools=[arthas_mcp], # 将mcp注册到agent中
)

CPU 高利用率

用户报告:某个 Java 进程 CPU 使用率持续在 100% 左右,系统响应缓慢,影响其他服务正常运行。

模拟的故障代码

public class CpuHighDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("PID: " + ProcessHandle.current().pid());
        System.out.println("[CpuHighDemo] CPU飙高场景启动...");
        // 创建一个死循环线程,执行密集计算
        new Thread(() -> {
            while (true) {
                // 密集计算操作(幂运算)
                Math.pow(Math.random(), Math.random());
            }
        }, "cpu-busy-thread").start();  // ← 关键:线程名称
        Thread.currentThread().join();
    }
}

启动进程

启动命令: java -javaagent:/home/ec2-user/.arthas/lib/4.1.9/arthas/arthas-agent.jar -Xmx256m -cp /home/ec2-user/workspace/java-diagnosis-demo/out CpuHighDemo
进程启动 PID: 238433
等待 Arthas MCP Server 启动...
✓ Arthas MCP Server 已就绪 (端口 9999)

测试日志

Tool #1: dashboard
Tool #2: thread
Tool #3: jad
Tool #4: thread

诊断结果

### 1. 观察发现
通过使用 `dashboard` 和 `thread -n 5` 命令观察到以下异常现象:
- 线程 `cpu-busy-thread` (ID 27) 占用了高达 99.97% 的 CPU 使用率。
- 该线程处于 RUNNABLE 状态,表明它正在执行任务且没有等待或阻塞。
- 其他线程的 CPU 使用率都很低,几乎可以忽略不计。
### 2. 问题定位
问题的具体位置在 `CpuHighDemo` 类中,具体是第 12 行的代码:
```java
Math.pow(Math.random(), Math.random());
这个无限循环的操作导致了 CPU 使用率飙升。
### 3. 根因分析
从反编译的代码可以看出,在主线程启动后创建了一个名为 `cpu-busy-thread` 的新线程。此线程执行一个无限循环,不断地调用 `Math.pow(Math.random(), Math.random())` 方法。由于这是一个计算密集型操作,并且在无限循环中不断执行,因此会导致该线程持续占用大量的 CPU 资源,从而使得整个进程的 CPU 使用率达到接近 100% 的水平。
### 4. 解决方案
要解决这个问题,需要根据业务需求调整代码逻辑:
1. **如果是测试场景**:可以在循环中添加适当的休眠时间,例如使用 `Thread.sleep(1)` 来降低CPU利用率。
2. **如果是生产环境**:应该检查是否有类似的死循环或者计算密集型任务未加控制地运行。
3. **长期优化**:对于长时间运行的任务,考虑将其放入线程池中执行,并设置合理的并发度以避免过度消耗系统资源。

死锁

用户报告程序突然挂起,不再处理任何请求,日志也没有新的输出。重启后恢复,但几分钟后又挂起。

代码实现

public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
    public static void main(String[] args) throws Exception {
        System.out.println("PID: " + ProcessHandle.current().pid());
        System.out.println("[DeadlockDemo] 死锁场景启动...");
        // 线程 1:先获取 lockA,再等待 lockB
        new Thread(() -> {
            synchronized (lockA) {              // ← 获取 lockA
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("[Thread-1] 已获取 lockA,等待 lockB...");
                synchronized (lockB) {          // ← 等待 lockB(被 Thread-2 持有)
                    System.out.println("[Thread-1] 已获取 lockB");  // 永远不会执行
                }
            }
        }, "deadlock-thread-1").start();
        // 线程 2:先获取 lockB,再等待 lockA
        new Thread(() -> {
            synchronized (lockB) {              // ← 获取 lockB
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("[Thread-2] 已获取 lockB,等待 lockA...");
                synchronized (lockA) {          // ← 等待 lockA(被 Thread-1 持有)
                    System.out.println("[Thread-2] 已获取 lockA");  // 永远不会执行
                }
            }
        }, "deadlock-thread-2").start();
        Thread.currentThread().join();
    }
}

测试日志

Agent 开始自主诊断...
Tool #1: thread
Tool #2: thread
Tool #3: jad
Tool #4: jvm
Tool #5: memory
Tool #6: sysprop

完整诊断报告

### 1. 观察发现
通过使用 `thread -b` 命令检测到系统中存在死锁:
- 线程 `deadlock-thread-2` (ID: 28) 被阻塞,等待获取锁 `java.lang.Object@26d694600`,该锁被线程 `deadlock-thread-1` (ID: 27) 持有。
- 线程 `deadlock-thread-1` (ID: 27) 被阻塞,等待获取锁 `java.lang.Object@26d65717`,该锁被线程 `deadlock-thread-2` (ID: 28) 持有。
这形成了一个典型的循环依赖死锁。
进一步的 JVM 信息确认了这一点:
- `jvm` 命令输出显示 `DEADLOCK-COUNT` 为 2,证实了存在两个线程参与了死锁。
### 2. 问题定位
通过对 `DeadlockDemo` 类进行反编译 (`jad` 命令),我们找到了问题的具体位置:
- **类名**: `DeadlockDemo`
- **涉及对象**:
  - `lockA` (在代码第10行定义)
  - `lockB` (在代码第11行定义)
- **线程1** (`deadlock-thread-1`):
  - 获取了 `lockA` (第14行)
  - 尝试获取 `lockB` (第16行) 时被阻塞
- **线程2** (`deadlock-thread-2`):
  - 获取了 `lockB` (第21行)
  - 尝试获取 `lockA` (第23行) 时被阻塞
### 3. 根因分析
出现死锁的根本原因是两个线程以不同的顺序获取相同的两把锁:
1. 线程1首先获取 `lockA`,然后尝试获取 `lockB`。
2. 线程2首先获取 `lockB`,然后尝试获取 `lockA`。
由于两个线程都在等待对方释放自己所需的锁,而它们又都不释放自己已经持有的锁,因此造成了永久性的阻塞,即死锁。
这种经典的"交叉锁"问题是并发编程中最常见的死锁形式之一。
### 4. 解决方案
为了避免此类死锁的发生,可以采取以下几种策略:
#### 方案一:统一加锁顺序
确保所有线程都以相同的顺序获取锁。例如,在访问这两个资源时,总是先获取 `lockA` 再获取 `lockB`。
#### 方案二:使用超时机制
使用 `java.util.concurrent.locks.ReentrantLock` 的 `tryLock(long timeout, TimeUnit unit)` 方法。
#### 方案三:使用高级并发工具
考虑使用 `java.util.concurrent` 包中的高级同步工具,如 `Semaphore`、`CountDownLatch` 或者 `BlockingQueue`。

慢方法

用户报告:某个业务接口响应时间过长(>1秒),影响用户体验。监控显示该接口平均响应时间持续在 1000ms 以上。

代码实现

public class SlowMethodDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("PID: " + ProcessHandle.current().pid());
        System.out.println("[SlowMethodDemo] 慢方法场景启动...");

        new Thread(() -> {
            while (true) {
                doSlowBusiness();  // ← 慢方法
                try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
            }
        }, "slow-method-thread").start();

        Thread.currentThread().join();
    }

    // 慢方法:总耗时约 1 秒
    static void doSlowBusiness() {
        step1();  // 200ms
        step2();  // 500ms
        step3();  // 300ms
    }

    static void step1() {
        try { Thread.sleep(200); } catch (InterruptedException e) {}
    }

    static void step2() {
        try { Thread.sleep(500); } catch (InterruptedException e) {}
    }

    static void step3() {
        try { Thread.sleep(300); } catch (InterruptedException e) {}
    }
}

测试日志

Tool #1: thread

Tool #2: jvm

Tool #3: memory

Tool #4: dashboard

Tool #5: sc

Tool #6: sm

Tool #7: jad

Tool #8: thread

Tool #9: thread

完整诊断报告

### 1. 观察发现
通过一系列工具的综合分析,我们发现了以下关键信息:

- **线程状态**: `slow-method-thread` 线程处于 `TIMED_WAITING` 状态,表明它正在执行一个定时等待操作。
- **CPU 使用**: 整体系统负载不高,没有明显的CPU瓶颈。
- **内存使用**: JVM 内存使用正常,GC活动较少,没有出现频繁Full GC的情况。
- **代码结构**: 通过反编译 `SlowMethodDemo` 类,我们看到了其主要逻辑结构,特别是 `doSlowBusiness()` 方法及其调用的三个步骤方法。

### 2. 问题定位
具体的问题出现在 `SlowMethodDemo` 类的 `step2()` 方法中。
- **类名**: `SlowMethodDemo`
- **方法名**: `step2`
- **行号**: 第27行 (`Thread.sleep(500L);`)

### 3. 根因分析
通过对 `SlowMethodDemo` 的源码分析,我们发现该类模拟了一个慢速业务处理流程:
```java
static void doSlowBusiness() {
    SlowMethodDemo.step1(); // 耗时200ms
    SlowMethodDemo.step2(); // 耗时500ms
    SlowMethodDemo.step3(); // 耗时300ms
}

其中,`step2()` 方法包含了长达500毫秒的睡眠操作:

```java
static void step2() {
    try {
        Thread.sleep(500L);
    } catch (InterruptedException e) {
        // empty catch block
    }
}
```
这个人为添加的延迟是导致整个业务接口响应时间超过1秒的主要原因。虽然这不是真实业务场景中的性能问题,但确实反映了在开发或测试环境中如何模拟慢速服务的一种常见做法。

### 4. 解决方案

针对这种情况,可以采取以下几种措施:
1. **如果是测试环境**: 可以根据需要调整延迟时间或者移除不必要的延迟。可以使用配置项来控制这些延迟。
2. **如果是生产环境问题**: 需要进一步分析 `step2()` 中实际执行的业务逻辑,找出真正的性能瓶颈点。
3. **通用优化建议**:
   - 将耗时操作分解为多个小任务并行处理
   - 引入缓存机制减少重复计算
   - 使用连接池管理数据库和网络连接
   - 定期进行性能压测和代码审查

以上就是使用Arthas MCP对Java应用进行线上诊断的实践指南的详细内容,更多关于Arthas MCP对Java线上诊断的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文