生产环境使用jdb命令行添加断点调试java应用

关键词:调试 jdb 命令行加断点 命令行调试java应用 生产环境添加断点

原因/背景

生产环境有段旧代码如下:

public String saveMapping(JSONObject jsonObject) {
   try {
      String mapping = jsonObject.getJSONArray("mapping").toJSONString();
      Long id = jsonObject.getLong("id");
      repository.saveMapping(mapping, id);
      return "保存mapping配置成功";
   } catch (Exception e) {
      throw new ServiceException("保存mapping配置失败");
   }
}

该代码有时正常有时异常,但没有把异常变量打印到日志中,导致排查问题不方便,也就有了在生产环境加断点debug的需求。(最后发现e是org.springframework.dao.DataIntegrityViolationException “Value too long for column”,相信小伙伴已经知道怎么处理了)

目前的arthas(当前最新版本:3.5.5)很强大,但不支持断点调试,所以只有使用java内置的jdb.

原理

-Xrunjdwp:JVM使用(java debug wire protocol)来运行调试环境;

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=xxx

dt_socket:使用的通信方式
server:是主动连接调试器还是作为服务器等待调试器连接
suspend:是否在启动JVM时就暂停,并等待调试器连接
address:地址和端口,地址可以省略,两者用冒号分隔

根据文档和stackoverflow上的讨论,JVM 1.5以后的版本应该使用类似下面的命令(老的还是可以使用的):

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxx

步骤

服务端

在服务上执行一个web应用,命令行如下:

java -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar lab-jdb-remote-debug-0.0.1-SNAPSHOT.jar

客户端

注意:客户端和服务端可以是同一台机器。

连接

jdb -connect com.sun.jdi.SocketAttach:hostname=IP,port=5005

jdb -attach IP:5005

调试

  1. 添加方法断点/清除方法断点

用法: stop in ${classFullName}.${methodName}

如:stop in cn.valuetodays.lab.IndexService.saveUser

清除:clear ${classFullName}.${methodName}

  1. 添加行断点/清除行断点

用法: stop at ${classFullName}:${lineNumber}

如:stop at cn.valuetodays.lab.IndexService:19

清除:clear ${classFullName}:${lineNumber}

  1. 下一步

next

  1. 继续执行,若有断点则跳到下一个断点

cont

  1. 打印栈桢中的局部变量表(用到了jvm知识)

locals

  1. 打印变量的内容

print xxx

  1. 列出所有断点

clear (对,没错,就是列出所有断点,尽管命令的名字是clear)

  1. 退出

exit

回到第一部分的“原因”处

  • 确认生产环境代码的tag
  • 确认该类的该方法所在的位置及“throw new ServiceException(“保存mapping配置失败”);”的行号
  • 使用行断点
  • 前端再次请求
  • 若能进入到断点,就print e即可看到内容。

实操

代码

create table if not exists USER (
id int not null primary key auto_increment,
name varchar(16)
);
package cn.valuetodays.lab;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class IndexService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public String saveUser(String name) {
        String sql = "insert into user(name) values(?)";
        try {
            int update = jdbcTemplate.update(sql, name);
            return "suc: " + update;
        } catch (Exception e) {
            return "fail";
        }
    }

}

实操步骤

## 以两个##开头的是说明
## 先连接到服务端,我是使用同一台机器操作的
[root@server201 ~]# jdb -connect com.sun.jdi.SocketAttach:hostname=server201,port=5005
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
## 在方法上加断点
> stop in cn.valuetodays.lab.IndexService.saveUser
Set breakpoint cn.valuetodays.lab.IndexService.saveUser
## 查看所有断点
> clear
Breakpoints set:
    breakpoint cn.valuetodays.lab.IndexService.saveUser
## 此时触发请求(curl -XGET http://server201:50001/saveUser?name=a1111111b3333333333333 ),会在断点外停下
> 
Breakpoint hit: "thread=http-nio-50001-exec-1", cn.valuetodays.lab.IndexService.saveUser(), line=14 bci=0

## 查看本地变量,有方法参数和本地变量
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
## 下一步
http-nio-50001-exec-1[1] next
> 
Step completed: "thread=http-nio-50001-exec-1", cn.valuetodays.lab.IndexService.saveUser(), line=16 bci=3

## 再次查看本地变量
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
sql = "insert into user(name) values(?)"
## 下一步
http-nio-50001-exec-1[1] next
## 再次查看本地变量,此时就对应代码中的catch处,可以看到变量e
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
sql = "insert into user(name) values(?)"
e = instance of org.springframework.dao.DataIntegrityViolationException(id=6545)
## 查看变量e的内容,可以知道是字段太长了
http-nio-50001-exec-1[1] print e
 e = "org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into user(name) values(?)]; Value too long for column """NAME"" VARCHAR(16)": "'a1111111b3333333333333' (22)"; SQL statement:
insert into user(name) values(?) [22001-199]; nested exception is org.h2.jdbc.JdbcSQLDataException: Value too long for column """NAME"" VARCHAR(16)": "'a1111111b3333333333333' (22)"; SQL statement:
insert into user(name) values(?) [22001-199]"
## 退出
http-nio-50001-exec-1[1] exit
[root@server201 ~]#

小伙伴们可以操作一下。

参考

其它

使用jdb命令连接到远程应用后输入help可以查看所有命令。

在docker中,我尝试了多种connectors,发现只有com.sun.jdi.SocketAttach能正常使用,理论上说在同一台机器(且同一个linux用户)中,提供进程id就应该能连接了,但是不行,我使用的docker镜像是openjdk:8-jdk-slim。但在linux系统中就能正常。

# docker中并未成功,linux系统中正常
jdb -connect com.sun.jdi.ProcessAttach:pid=18405,timeout=10
# docker中并未成功,,linux系统中正常
jdb -connect sun.jvm.hotspot.jdi.SAPIDAttachingConnector:pid=18405
作者:张三  创建时间:2022-04-19 19:44
最后编辑:张三  更新时间:2022-06-29 15:20