ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot线上调试实战:IDEA断点技巧与Arthas插件组合拳

Spring Boot线上调试实战:IDEA断点技巧与Arthas插件组合拳 上周五临近下班同事甩给我一份线上问题记录“Spring Boot 接口偶发超时日志里只有 timeout翻了两天没找到原因。”我打开 IntelliJ 里那个平时很多人根本不点的调试辅助插件十分钟后告诉他数据库连接池被慢调用占满了。他愣了半天“我本地也模拟过并发为什么没复现”答案很简单本地能复现业务逻辑复现不了运行时的真实状态。Spring Boot 项目一旦跑起来类会被 Spring 容器、MyBatis 代理、AOP 切面层层包住普通断点能看到的东西其实非常有限。这篇文章我想认真聊聊为什么我一直说 Spring Boot 调试不能靠“玄学”以及我最近几年越来越依赖的那套 IntelliJ 调试组合拳到底是怎么把 JVM 内部状态直接摆到眼前来的。这篇文章适合被线上问题逼疯的 Java 后端开发也适合刚接触 Spring Boot、觉得调试器就是 F7/F8 走一遍的新手。我会从“为什么断点看不到真相”讲起然后说清楚 IDEA 自带调试能力的正确用法再重点拆解那个藏在不显眼菜单里的 Arthas 配套插件最后用一次真实的线上排查过程把整个链条串起来。你读完可以直接照着配不用再靠加日志、猜参数、反复重启这种原始手段。1. 为什么 Spring Boot 调试容易变成“玄学”1.1 本地能跑线上崩差在哪儿很多人遇到过这种情况本地用application-dev.yml启动接口一切正常到了生产环境同一个接口偶尔卡一下日志里什么像样的异常都没有。第一反应是“出鬼了”。其实不是鬼是环境差异被放大了。Spring Boot 最大的特点是“约定优于配置”但这套约定会掩盖掉大量运行时差异。本地连接池可能建了 10 个连接生产环境是 30 个本地调用第三方接口超时是 3 秒生产环境网关层可能套了 15 秒本地 Redis 缓存命中率很高生产环境一有热点数据就穿透到数据库。这些参数在普通断点调试里根本看不见因为代码逻辑没有变变的是运行环境的“水位”。还有一种更隐蔽的差异是字节码层面的。Spring Boot 项目里几乎人人用 Lombok、MyBatis、Spring AOP框架在启动时动态生成代理类。你在 IDEA 里打断点看到的调用栈往往长这样org.example.service.OrderService.submit(OrderService.java:42) at org.example.service.OrderService$$EnhancerBySpringCGLIB$$xxxxx at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0这个$$EnhancerBySpringCGLIB就是运行时生成的东西。你只在编译后的业务代码上打断点当然是能停住但很多代码路径是走代理之后的IDE 只给你看了冰山一角。线上 JVM 的情况更复杂还有 JIT 编译、逃逸分析、偏向锁在后台悄悄起作用。说白了本地调试默认只能证明“这段代码走到这里时状态是对的”根本不能证明“线上运行时的状态是健康的”。玄学的根源就是对运行时状态的不了解。1.2 普通断点只能看到一半另一半在哪儿我们团队做过一个小实验两个同样接口的 Spring Boot 应用一个直接调用 Service一个经过 Feign 再调用 Service。用普通断点调试时直接调用的那个能清楚看到参数传递过程经过 Feign 的那边断点通常会停在动态代理生成的MethodHandler.invoke里IDE 显示一堆 mock 出来的对象你盯着看也看不出所以然。打个比方普通断点像是看小区门口的监控视频只能看到有人进进出出你没办法知道这个人在楼上具体干了什么。想看屋子里发生了什么你得装室内摄像头。对 JVM 来说这个“室内摄像头”就是运行时诊断工具。线上环境你没办法直接挂断点因为断点会让 JVM 暂停在任意位置。一个高并发的支付接口一旦被断点卡住堆积的请求会瞬间打爆线程池。所以线上排查必须用那种“不影响主流程、只从旁边观察”的手段。我当时给同事展示的就是这样一个观察方式通过动态增强在方法执行前后把入参、返回值、耗时、异常全部采集出来IO 密集的接口也就损失微秒级性能基本无感知。它不需要改代码、不需要重启、不需要重新发布就像给 JVM 加了一个内窥镜。2. 先把 IntelliJ 自带的能力用到极致再谈插件2.1 那些被严重低估的断点条件断点、异常断点、日志断点很多人调试 Spring Boot 就是行断点加 F7/F8遇到循环调用就疯狂按“Step Over”效率极低。IDEA 里真正值得花时间配置的断点有三类。第一类是条件断点。右键断点小红点输入条件表达式只有满足条件才暂停。比如排查订单状态异常order.getStatus().equals(PAY_TIMEOUT)这个技巧的威力在于一批接口进来你只关心状态为PAY_TIMEOUT的其他请求直接放行减少大量无效暂停。我在多线程场景下特别依赖这个功能否则随便一个断点停下来整个应用的并发度就被你打断了。第二类是异常断点。在 Breakpoints 面板点加号选择 Java Exception Breakpoints填入SQLException、HttpMessageNotReadableException之类。这样不需要手动猜代码哪一行异常JVM 抛异常的瞬间 IDE 会自动捕获。注意坑不要把Exception.class全局异常断点勾满尤其不要勾选两栏都选“Any exception”否则 Spring 框架内部捕获的无数异常会停得你怀疑人生。第三类是日志断点这是最容易被人忽略的。勾选 “Log evaluated expression”填入你要打印的变量断点不会暂停只在控制台输出一行日志。对高频接口来说这等于临时塞了个System.out.println但不用改代码也不用重新编译。我经常在处理定时任务循环里用这东西几百次循环全部打出来看规律、看边界值非常直观。2.2 Evaluate Expression 和 Watch 的正确使用姿势断点暂停时大多数人只会看 Variables 面板的变量列表但 Spring Boot 的场景里你往往想动态调用方法。比如你怀疑 UserService 的某个方法返回结果不对在 Variables 面板里只能看到已知对象的状态这时可以用快捷键Alt F8打开 Evaluate Expression老版本是Alt F8新版可能略有差异直接输入方法调用userService.findByIdWithCache(10086L)IDE 会真实执行这个方法并返回结果。这比新建一个测试类快得多。注意一个原则在 Evaluate 里尽量只读不要调用有副作用的方法。我之前同事在 Evaluate 里执行了orderMapper.deleteById(10086L)接口是有状态的删完回过神来已经来不及了。Watch 面板也容易被当成摆设。其实在断点状态下你可以把一些复杂表达式拖到 Watch 里让它随着单步执行实时更新。比如观察transactionManager.getTransactionStatus()、redisTemplate.opsForValue().get(cart:10086)这类组合状态。本质上它就是一张“动态监控卡片”比 Variables 面板更有针对性。2.3 远程调试本地 IDEA 连远端 JVM很多团队解决线上问题的手段是“上服务器tcpdump 看日志”其实还有一招IDEA 的 Remote JVM Debug。只需要在目标应用的启动参数里加上java -agentlib:jdwptransportdt_socket,servery,suspendn,address5005 \ -jar your-app.jar然后在 IDEA 里新建 Remote JVM Debug 配置填上服务器 IP 和端口 5005启动 Debug 模式就能像本地调试一样打断点看变量。这对预发环境、测试环境非常有用尤其适合复现那种“在测试环境偶发、本地稳定复现不了”的问题。但远程调试有两个大坑要提醒。第一suspendn保证应用启动时不等待调试器连接否则会出现调试器没连上、服务一直卡死的情况。第二生产环境千万不要默认开启远程调试端口因为调试协议会让 JVM 在执行断点时暂停这就是人为制造的全局限流。更别说调试端口还会暴露给网络属于严重的安全隐患。线上机器要排查优先用下面要讲的运行时诊断工具而不是挂 JDWP。另外注意 JDK 9 之后address5005和address*:5005有语义差异。如果你监听在某个网卡上建议显式写 IP-agentlib:jdwptransportdt_socket,servery,suspendn,address192.168.1.10:5005只监听内网地址避免暴露到公网。3. 真正的“透视插件”Arthas Idea Plugin3.1 为什么它算是 IntelliJ 里的隐藏插件如果你听说过 Arthas你大概率是直接在服务器上敲命令行用的。它的确很强大但命令行交互方式对不熟悉的人来说有点劝退一条 trace 命令要拼一长串类名和方法名稍微打错就报 class not found。可是很多人不知道IDEA 插件市场里有一个社区插件叫Arthas Idea Plugin简称 AIP专门把这些繁琐的 Arthas 命令变成图形化操作选中一个类、一个方法右键就能生成对应的诊断命令。我之所以说它“隐藏”是因为大多数人在插件市场会搜 “Spring Boot”、“Lombok”、“MyBatis”不会专门搜 “Arthas”。而 Arthas 本身又常被当成“运维工具”和“IDE 调试”天然不在一个心智模型里所以这条链路一直很冷门。真正用过之后你会觉得它就像 IDA 里的“透视镜”把线上 JVM 里正在运行的内容映射到 IDE 上下文里。AIP 不是 JetBrains 官方出的东西但胜在轻量、免费、开源核心功能到现在依然够用。它跟 IDEA 的契合点在于你在编辑器里已经把类名、方法名、参数类型都写好了IDE 掌握这些结构化信息而命令行工具只拿到一串文本。插件做的就是把两边连接起来。3.2 安装与 attach 的完整流程安装非常简单打开Settings - Plugins - Marketplace搜索 “Arthas Idea Plugin”点 Install重启 IDE 即可。如果插件市场加载很慢或者正好遇到网络抽风你也可以从 JetBrains 插件仓库手动下载 zip 包然后在Install Plugin from Disk里选择。安装完之后你还要在目标服务器/容器里把 Arthas 跑起来。一般有两种做法。第一种是直接用官方启动器curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar启动时会列出当前机器上所有 Java 进程输入序号即可 attach。如果你用的是 Docker 容器更推荐美化版命令docker exec -it container_name \ java -jar /tmp/arthas-boot.jar得先把 arthas-boot.jar 拷进容器里或者挂载进去。attach 成功之后Arthas 会自动打印一段信息表示已经连上目标 JVM。接下来你就可以回到 IDEA打开 AIP 的 Arthas Console 窗口也可以在插件设置里配置远程服务器地址和鉴权信息。之后在 Java 文件里选中类或方法右键菜单会出现 “Arthas trace”、“Arthas watch”、“Arthas stack” 等选项插件会自动带出完整命令你只需要把命令贴到目标 JVM 的 Arthas 交互终端里执行。3.3 从“手工拼命令”到“点按式生成”这个插件最大的价值在于它把 Arthas 命令的“生成”环节自动化了。以前我要在服务器上排查一个问题得先sc找到类全名再sm找方法签名再手动敲watch表达式手一抖括号忘了转义就直接报错。现在我在 IDEA 里打开要排查的类右键选择Arthas trace自动生成调用链耗时追踪命令Arthas watch自动生成方法入参出参观察命令Arthas stack自动生成反向调用栈命令Arthas tt自动生成时空隧道记录命令举个例子我在代码里想看OrderService.submit的耗时分布只需要右键这个方法选择Arthas trace插件自动给我生成类似这样的命令trace com.example.service.OrderService submit当然如果你的方法有多个重载插件也会把参数类型带上避免匹配到错误的方法。这一步非常贴心。AIP 还有一个好用的细节它可以直接把生成的命令发送到已经打开的 Arthas Console不用手动复制。在 IntelliJ 2024 上它还能识别你当前选中的 Spring MVC Controller 的 RequestMapping 路径快速定位到对应入口方法省得在巨大的工程里逐个找。3.4 常用命令速查一张表讲清楚Arthas 本身命令很多日常排查真心用不到全部。我整理了一张速查表配合 AIP 生成命令的场景命令用途常见的 Spring Boot 场景dashboardJVM 概览看 CPU、内存、线程、GC先看整体有没有异常再决定下一步thread -n 3查看最繁忙的 3 个线程堆栈接口卡顿、CPU 飙高、线程池耗尽thread -b找出当前阻塞其他线程的锁死锁、连接池等待、锁竞争trace 类名 方法名方法内部各子调用耗时Service 层与 Mapper 层耗时不对watch 类名 方法名 表达式观察入参出参、异常请求参数不对、返回结果不符合预期stack 类名 方法名查看方法被谁调用某个方法突然被不该调用的地方触发tt -t 类名 方法名记录调用现场可回放偶发问题错过第一现场jad 类名反编译线上 class确认发布的版本对不对、线上代码是否最新monitor 类名 方法名统计调用次数、成功率、失败率验证发布后流量健康度用 AIP 生成这些命令后你只需在终端回车剩下就是读结果、分析。4. 一次线上排查实录从玄学走向透视4.1 现象偶发超时 日志空洞讲一个我印象非常深的案例。某个 Spring Boot MyBatis 的项目每天下午三点左右订单查询接口偶发 5 秒超时。日志里只有一行org.springframework.dao.QueryTimeoutException: Read timed out没了。没有慢 SQL 记录没有堆栈没有上下游调用链。团队前两周的做法是加日志、重启、再加日志、再重启。我接手后做的第一件事就是给目标 Pod 装上 Arthas然后打开 IDEA 里的 AIP选中 Controller 入口方法直接 trace。4.2 第一轮dashboard thread 锁定线程池异常我先敲了dashboard结果 CPU 不高、堆内存也很平稳GC 频率正常。这就排除了资源耗尽型问题更像是“有线程在等什么”。接着用thread -n 3看繁忙线程http-nio-8080-exec-42 Id342 WAITING at java.base17/java.lang.Object.wait0 at com.zaxxer.hikari.pool.HikariPool.getConnection at com.zaxxer.hikari.pool.HikariPool.getConnection at org.apache.ibatis.mapping.VendorDatabaseIdProvider.getDatabaseId大量 HTTP 线程都停在HikariPool.getConnection说明连接池的连接被占用光了新请求拿不到连接只能等。那连接池为什么满继续用thread -b找阻塞其他线程的源头http-nio-8080-exec-7 Id299 RUNNABLE at org.springframework.cloud.openfeign.FeignClient.recover at com.example.thirdparty.PaymentClient.query(PaymentClient.java:88)真相浮出水面有个线程卡在调用第三方支付查询接口的 Feign 调用上耗时已经超过 10 秒。由于支付渠道响应慢连接一直被占用后面的请求全部排队形成雪崩。4.3 第二轮watch 验证关键参数锁定了大方向还不够我得搞清楚为什么这个 Feign 调用慢。这时我不想改代码加日志直接用 AIP 对PaymentClient.query生成 watch 命令watch com.example.thirdparty.PaymentClient query {params[0], returnObj, throwExp} -n 5 -x 3等了十几分钟抓到几条现场数据入参订单号是正常的返回结果里有一个字段是UNKNOWN而且每次耗时都在 8~12 秒之间。再结合trace看该方法内部各环节耗时发现大部分时间卡在 HTTP 连接建立和等待响应上不是业务计算。这就是典型的“第三方接口变慢拖垮本地连接池”案例。我还用tt -t记录了几次调用现场。这样即使下次偶发发生也能回放完整入参出参不用再蹲守日志。4.4 根因与修复验证根因很清楚Feign 调用第三方接口超时设置得太大15 秒而本地数据库连接池只有 30 个连接。高峰期几十个请求同时卡在等待第三方响应连接池被占满后续所有请求排队最终表现为接口超时。修复动作分三步首先把第三方调用的读超时从 15 秒降到 3 秒失败快速返回不拖走连接池其次引入线程池隔离支付查询走独立线程池即使第三方故障也只影响支付相关接口不影响核心订单查询最后把重试机制从“无条件重试 3 次”改成“最多重试 1 次”避免恶化。验证阶段我没有直接看“接口通没通”而是用 Arthas 的monitor命令持续观察monitor com.example.thirdparty.PaymentClient query -c 10压测十分钟后连接池等待线程数从 80% 降到 4%接口 P99 从 5 秒回到 200 毫秒。全程没有重启、没有改一行日志、没有添加任何临时打印。这轮排查从开始到定位根因用了不到半小时。5. 常见问题与踩坑速查5.1 Attach 不上的五类原因用 Arthas 或 AIP 时最常遇到的就是 attach 失败。下面这几种情况我都见过现象原因解决办法Can not find java process目标进程在 Docker 容器里宿主机上执行 Arthas 看不到进入容器执行或用docker execPermission denied当前用户不是启动 Java 进程的用户用同一用户身份或sudo -uThe forked VM terminatedJDK 版本太高或 JVM 参数禁用 attach确认 JDK 版本检查DisableAttachMechanismattach 成功但watch无输出高频方法被插件生成的命令影响、表达式匹配失败减少-n数量检查类全名在线程池内 attach 慢Arthas 会尝试加载大量类机器资源不足使用-inst指定进程实例容器环境是重灾区。很多现代部署都是镜像里一个 Java 进程外面套了 Sidecar你从宿主机上跑arthas-boot.jar会看见宿主机一堆进程但就是没有容器里的那个。老实用docker exec进容器再跑最省事。5.2 watch 条件表达式被转义吃掉的坑AIP 生成watch命令时默认会带上方法名和参数但如果你要写条件比如“只看金额大于 100 的调用”不要写成watch com.example.OrderService submit amount 100 -n 5这在 OGNL 里根本解析不了因为你没指定amount是哪个对象。正确写法是watch com.example.OrderService submit {params[0], returnObj} params[0].amount 100 -n 5也就是说表达式要明确从params[0]或target出发。而且要注意命令在 shell 里的引号转义我在服务器上一行命令粘错好几次。AIP 的作用就在这里它自动帮你拼好你只需要改条件部分基本避开了 90% 的语法错误。5.3 redefine 热更新不到万不得已不要用生产中偶尔会遇到“改一行代码就要重新发包”的窘境Arthas 提供的redefine命令可以热更新 class 文件但限制非常多不能新增或删除方法不能改字段结构只能改方法体。我在一两年里也只成功用过一次而且那次还差点出问题。用 AIP 或命令行执行 redefine 时请记住三个习惯改之前先jad反编译当前线上 class保存原始代码便于回滚只改单个方法体不动签名热更新完立即用watch或monitor验证行为确认没问题再继续观察一旦异常立刻走发布流程。真正常态化的临时改代码我更推荐用tt -i对历史调用做回放而不是直接改线上字节码。redefine 更像“应急止血”不是日常工具。5.4 线上用 Arthas 的两个安全习惯线上诊断工具是把双刃剑。我自己有一套强制习惯生产环境只在业务低峰期 attach每次trace、watch针对高频方法都限制输出条数-n 5观察完立刻退出 Arthas 进程。另外Arthas 使用自身的 TCP 端口进行通信默认端口是 3658新版还可能随机。建立诊断会话后一定要在结束时执行stop或直接退出进程否则诊断代理会一直挂在目标 JVM 上虽然性能损耗很小但常年挂着会增加不确定性和安全暴露面。这些习惯看着琐碎关键时候能救你。尤其是大促期间谁也不想在排查问题的同时因为诊断工具本身的字节码增强给核心服务带来额外抖动。5.5 团队协作把 AIP 生成的命令沉淀成模板最后分享一个我们团队沉淀下来的细节。排查问题的时候AIP 生成的一次性命令很有用但遇到同类型问题你还是要重新生成一遍。后来我们把常用 Arthas 命令存成 IDEA 的 Live Template团队内共享。比如输入awt回车自动展开成watch com.example.service.order.OrderService submit {params[0], returnObj, throwExp} -n 5 -x 3输入art回车自动展开成trace com.example.service.order.OrderService submit #cost这样新同事根本不需要背命令打开 IDEA 直接敲缩写就行。再配合 AIP 的右键生成排查效率直接翻倍。这套玩法我用了快两年最大的感受就是线上问题不再是一个“黑盒子”你想看什么随时能看你想确认什么马上能确认。与其继续靠直觉猜不如早点把 Arthas Idea Plugin 这套组合拳用起来。遇到下一个半夜告警你会感谢自己提前掌握了这套工具链。
返回列表