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

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

前言

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

  • CPU 飙高:某个线程死循环导致服务器负载过高
  • 内存泄漏:对象无法被回收,最终导致 OOM
  • 死锁:多个线程互相等待,系统挂起
  • 线程池满:任务堆积,新任务无法执行
  • 慢方法:某些接口响应时间过长
  • 异常被吞:抛出异常但未记录,难以排查

这些问题在本地开发时容易调试,但在生产环境中,我们通常只能通过日志和监控来推测。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"]})

整体的逻辑结构如下

  • Python 负责进程启停管理、等待 Arthas 服务就绪、创建智能代理、调度测试任务;
  • AI Agent 层基于本地通义千问编码大模型,以诊断专家角色定位,依托 MCP 工具集调用能力,按理解任务 - 选用工具 - 分析数据 - 输出结论流程完成智能诊断;
  • MCP 客户端层通过 HTTP/SSE 协议通信,配置令牌鉴权,维持长连接会话,实现与服务端的标准协议交互;
  • Arthas MCP 服务层监听 9999 端口提供 MCP 接口,封装线程、JVM、内存、链路追踪、反编译等 Arthas 诊断工具能力;
  • 应用层预置 CPU 高占用、死锁、内存泄漏、异常、线程池耗尽、慢方法共 6 类故障测试场景,作为系统诊断的目标被测应用。

默认配置下,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% 左右,系统响应缓慢,影响其他服务正常运行。

  • 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线上诊断的资料请关注脚本之家其它相关文章!

相关文章

  • Logback MDCAdapter日志跟踪及自定义效果源码解读

    Logback MDCAdapter日志跟踪及自定义效果源码解读

    这篇文章主要为大家介绍了Logback MDCAdapter日志跟踪及自定义效果源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Spring JPA事务管理与自定义操作实例解析(最新推荐)

    Spring JPA事务管理与自定义操作实例解析(最新推荐)

    在Spring框架中,数据持久化操作常常与事务管理紧密相关,本文将深入探讨Spring Data JPA中的事务管理机制,并结合具体实例,展示如何自定义事务行为以满足不同的业务需求,感兴趣的朋友一起看看吧
    2024-12-12
  • Java利用SPI实现解耦的示例详解

    Java利用SPI实现解耦的示例详解

    SPI的全称是服务提供接口,可以用其来启动框架的扩展和替换组件。本文将利用SPI实现解耦,文中的示例代码讲解详细,具有一定的借鉴价值,需要的可以参考一下
    2023-04-04
  • 学习非阻塞的同步机制CAS

    学习非阻塞的同步机制CAS

    现代的处理器都包含对并发的支持,其中最通用的方法就是比较并交换(compare and swap),简称CAS。下面我们来一起学习一下吧
    2019-05-05
  • Java字符串格式化功能 String.format用法详解

    Java字符串格式化功能 String.format用法详解

    String类的format()方法用于创建格式化的字符串以及连接多个字符串对象,熟悉C语言的同学应该记得C语言的sprintf()方法,两者有类似之处,format()方法有两种重载形式
    2024-09-09
  • 详解Java使用super和this来重载构造方法

    详解Java使用super和this来重载构造方法

    这篇文章主要介绍了详解Java使用super和this来重载构造方法的相关资料,这里提供实例来帮助大家理解这部分内容,需要的朋友可以参考下
    2017-08-08
  • Spring常用配置及解析类说明

    Spring常用配置及解析类说明

    这篇文章主要介绍了Spring常用配置及解析类说明,涉及Spring常用配置项的方法以及它的解析类等相关内容,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • 简述Java编程语言中的逃逸分析

    简述Java编程语言中的逃逸分析

    这篇文章主要介绍了简述Java编程语言中的逃逸分析,包括其定义、作用、类型及理论基础等相关内容,十分具有参考价值,需要的朋友可以了解下。
    2017-09-09
  • 浅谈如何优雅地停止Spring Boot应用

    浅谈如何优雅地停止Spring Boot应用

    这篇文章主要介绍了浅谈如何优雅地停止Spring Boot应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • 基于Zookeeper实现分布式锁详解

    基于Zookeeper实现分布式锁详解

    Zookeeper是一个分布式的,开源的分布式应用程序协调服务,是Hadoop和hbase的重要组件。这篇文章主要介绍了通过Zookeeper实现分布式锁,感兴趣的朋友可以了解一下
    2021-12-12

最新评论