greys在线问题诊断工具

线上系统为何经常出错?数据库为何屡遭黑手?业务调用为何频频失败?连环异常堆栈案,究竟是哪次调用所为?
数百台服务器意外雪崩的背后又隐藏着什么?是软件的扭曲还是硬件的沦丧? 走进科学带你了解Greys, Java线上问题诊断工具。

Greys的诞生

  • LOGO

很早的时候,我们使用BTrace排查问题,在感叹BTrace的强大之余,也曾好几次将线上系统折腾挂掉。2012年淘宝的聚石写了HouseMD,将常用的几个Btrace脚本整合在一起形成一个独立风格的应用,但其核心代码用的是Scala,我们没这方面的编程维护经验,所以只好艳羡HouseMD的才思敏捷而无法在其上增加功能。

于是乎,Greys诞生了!
PS:目前Greys仅支持Linux/Unix/Mac上的Java6+,Windows暂时无法支持

Greys是一个JVM进程执行过程中的异常诊断工具。 在不中断程序执行的情况下轻松完成JVM相关问题排查工作。

和HouseMD一样,Greys-Anatomy取名同名美剧“实习医生格蕾”,目的是向前辈致敬。代码编写的时候参考了BTrace和HouseMD两个前辈的思路。

  • 目标群体

有时候突然一个问题反馈上来,需要入参才能完成定位,但恰恰没有任何日志。回去加上重新部署,一杯咖啡时间过去了,是不是很崩溃?

当你经过反复这样几次折腾之后变得聪明了,在自己的代码的所有入参和出参地方都加上debug日志,但这次问题似乎暴露在别人的代码中了…是不是很无奈?

突然遇到线上一个性能问题无法确定到底是哪个环节的耗时,只能反复抓jstack猜,还有没有办法可以好好的过日子啦?

遇到以上问题时,你就是我们这类工具的目标客户,此类工具能利用Java6的Instrumentation特性,动态增强你所指定的类,获取你想要到的信息。

软件特点

  • ClassLoader隔离

在设计和实现这款程序的时候,花费了非常多的精力在隔离目标类与Greys的ClassLoader隔离上。你可以放心大胆的使用Greys,而不用担心Greys会干扰到现有业务代码所使用的三方类库。

  • 运行时加载

要求目标JVM在JDK6+的基础上,且当前执行人拥有与目标JVM相同权限。可以做到不中断当前JVM而动态进行加载、问题分析定位。

  • 常用问题定位命令化

Greys与BTrace、HouseMD等同类软件最大的不同在于,她拥有我多年来业务代码疑难杂症定位的常用技术手段,并将这些排查思路和技巧命令化,将我的问题定位经验Share给大家。

  • 表达式支持

HouseMD相比BTrace最强大的地方就在于能快速指定拦截的类与方法,但却无法支持对观察到的对象进行展开、条件过滤等操作。BTrace的脚本是自己所编写,可以实现此类功能。但编写学习成本很高,且容易出错。

表达式的引用能综合这两款软件的特长同时弥补他们的不足。目前Greys所采用的是OGNL表达式。

  • 多用户同时访问

远程DEBUG最大的问题就在于,只允许一个人访问DEBUG端口,而且一但断点条件设置不当,很有可能将其他正常的业务请求拦下,影响其他用户的使用。

Greys采用的思路是做观察者,其所设置的断点不允许阻塞正常业务的流程,但你可以观察到断点所拦截到的所有信息。

  • 高性能

精心用ASM设计了字节码增强,核心的数据结构用数组针对实际场景做裁剪优化。可以放心的用在高负载要求下的JVM环境。

  • 纯Java编写

Greys定位是专业的JVM的业务问题定位工具,既然是JVM那我们所面对的大部分就是Java程序员。我希望能Share自己在编写软件时候的所有技巧与思路,让更多的Java程序员能参与开发或从中受益。

目前已经有非常多的热心网友在给我的代码挑错,非常感谢这些朋友的支持!

不适合的场景

Greys并不万能,我也没有计划让她能成为万能的问题定位工具。所以如果你在某些场合能用上更专业的工具,我会非常乐意推荐你使用。

  • 性能环境下的性能损耗定位

性能分析需要有更专业的软件,我自己常用的则是JProfiler(当然是付费的了)。Greys虽然性能损耗很小,但其分析的维度太少,所以只适合做简单的性能损耗定位。当你用过专业的性能分析软件之后,就会发现什么叫专业!

  • 开发环境下的远程DEBUG

虽然Greys能取代部分的远程DEBUG行为,但毕竟没不像DEBUG工具那样可以看到局部变量的值,而且可操作性上也没有JVM下Eclipse/IDEA等优秀的IDE自带的DEBUG工具这么人性化操作。

  • 线上环境大规模部署

与BTrace一样,Greys获取到的权限太高,如果线上大规模部署会遭受黑客的攻击,而今天我为了实现简单是没有做过多的鉴权控制。

JDK类库分析

JDK的类库存放在rt.jar中,启动时加载到BootstrapClassLoader中(Hotspot-JVM),但由于Greys也是用Java语言所编写,所以自身也用到了这些基础类库,默认情况下关闭了对这些类的增强。

当然,对于Spring、ibatis、Tomcat等三方类库是可以放心大胆使用的。

  • 其它不适合场景

BTrace、HouseMD、Greys、JavOSize此类工具都会对Perm区、CodeCache(影响JIT)产生干扰,如果你的程序对这两块非常敏感,也请不要在这些场合下使用。

我们的座右铭

让程序解决繁琐的事情

  1. 特性功能
  2. 交互方式
  3. 命令行交互

因为很多场景下我们都是用在远程问题分析中(本地我就直接DEBUG了)。一般Java都会使用在Linux/BSD等类UNIX操作系统下。所以命令行是我最开始不二的选择,也是目前支持最成熟的交互方案。

图形界面交互

在2.x.x.x版本中将会支持WEB方式访问,HTTP采用websocket与后台服务进行交互,预计过年之后能发布上线。

  • 内置主要功能

查看已被JVM所加载的类、方法信息

  • 方法执行监控

调用量,成功失败率,响应时间

  • 方法执行数据操作

入参、返回值、异常信息记录与查看;支持动作回放

  • 性能开销渲染

跟踪指定路径中的方法调用轨迹、耗时

  • 查看方法调用堆栈

软件特点

纯Java实现的开源项目

安装使用便捷,仅一个jar包

可无需重启JVM进行CT式诊断

观察变量的出入参

OGNL表达式展开变量、过滤条件,方便你查看入参、出参、异常、当前对象的各种属性细节

常用分析命令集成,monitor、trace等

时间隧道,tt命令能以时间维度纪录下监控期内的每一次调用环境

  • 多人并行协作

基于C/S架构的任务模式甚至能让多人同时远程到同一进程上执行不同的指令、脚本,非常适合团队一起进行线上问题排查与跟踪。Greys采用纯Java编写并留有良好的扩展,如果你有需求,只要你会Java,就可以为你自己编写想要的功能。 Greys最有利的武器是他的表达式,能让你在感受到HouseMD集成功能便利的同时,也能发挥出自定义Btrace脚本的灵活。

  • 多人协作

应用管理员拥有JVM进程权限,由他来首先在目标JVM上启动Greys
技术专家A和B平时没有对应机器的权限,但只要网络能访问,他们可以通过指定ip:port直接访问目标机器的JVM进程,仿佛在本地一般

Greys入门

软件安装
Greys支持在线安装和本地安装两种安装方案,安装即可用,推荐使用在线安装。

在线安装(推荐)

请复制以下内容,并粘贴到命令行中。

curl -sLk http://ompc.oss.aliyuncs.com/greys/install.sh|bash
命令将会下载启动脚本文件greys.sh到当前目录,你可以放在任何地方或加入到$PATH中

本地安装

在某些情况下,目标服务器无法访问远程阿里云主机,此时你需要自行下载greys的安装文件。

下载最新版本的GREYS

http://ompc.oss.aliyuncs.com/greys/release/greys-stable-bin.zip

解压zip文件后,执行以下命令

cd greys
sh ./install-local.sh
即完成本地安装。

常见安装问题
下载失败

通常这样的原因你需要检查你的网络是否畅通,核对是否能正确访问这个网址

http://ompc.oss.aliyuncs.com/greys/greys.sh

downloading…
download failed!
没有权限

安装脚本首先会将greys文件从阿里云服务器上下载到当前执行脚本的目录,所以你必须要拥有当前目录的写权限。

permission denied, target directory is not writable.
启动Greys
参数说明

./greys.sh [@IP:PORT]
**PID:**目标Java进程ID(请确保执行当前执行命令的用户必须有足够的权限操作对应的Java进程)

**IP:**目标服务器IP地址,当远程服务开启之后,其他人可以通过指定IP的形式加载到对应目标机器的Java进程中,从而实现远程协助。专门用于解决目标主机账号没有权限,但对方兄弟却非常需要你支援的时候。Greys允许多个用户同时访问,并且各自的命令不会相互干扰执行。

**PORT:**目标服务器端口号,设计端口号的初心则是希望解决同台机器上存在多个Java进程需要被Greys分析的情况,默认的端口号是3658,如果不做区分则会引起端口冲突。

启动范例

如果不指定IP和PORT,默认是127.0.0.1和3658

./greys.sh 12345
等价于

./greys.sh 12345@127.0.0.1
等价于

./greys.sh 12356@127.0.0.1:3658
sudo支持

成熟的线上管理环境一般都不会直接开放JVM部署用户权限给你,而是通过sudo-list来控制和监控用户的越权操作。由于greys.sh脚本中会对当前用户的环境变量产生感知,所以需要加上-H参数

sudo -u admin -H ./greys.sh 12345
TELNET的支持

Greys支持通过telnet来访问服务端,如果当你手头的机器没有安装Greys的客户端,你可以简单的通过telnet命令来进行访问。

telnet 10.232.12.113 3658
当然了,telnet命令和Greys自带的Console在使用友好度上还是有一定的差距,不过解决应急之需没有问题。

会话与任务
Greys是一个C/S架构的程序,所以当Client访问到Server时,Server会维护一个session(会话),以及session的心跳、超时机制。事务(Tx)机制则是建立在session的基础上,所有的命令交互都会创建一个事务,并且产生对应的队列进行输出缓冲。

事务伴随着命令的生命周期而存在,命令分两种:

立即返回

立即返回的命令定义是:敲下命令后Server端立即返回最终结果,后续无持续反馈信息,释放Client对输入的锁定,重新开放让用户输入信息,比如version、sc、sm等。

等待中止

等待中止的命令则是需要用户主动输入Ctrl+D完成的命令中止操作。命令执行后无法立即返回最终结果,而是不断的将中间产生的输出源源不断的输出到客户端中,这种命令比如stack、monitor等。

当session关闭时,所有挂在session的事务也会立即被关闭。

表达式
Greys相对于HouseMD、BTrace而言最灵活的地方就是在用表达式来灵活的支持不同的问题排查、分析场景。

表达式分两种:条件表达式与观察表达式

条件表达式

条件表达式用在使用表达式表达TRUE或FALSE的场景,从1.6.0.6版本开始,trace、stack、tt、watch命令都增加了条件表达式支持。

条件表达式将会使用greys内置的表达式解析引擎,识别OGNL语法。

特别指出的是,如果你书写了一个错误的条件表达式,greys为了兼容错误会解析为FALSE。

以下是一些条件表达式使用的例子和预测结果

条件表达式 预测结果 解析结果说明
11 TRUE 条件表达式为真
true TRUE 条件表达式为真
@@@ FALSE 非法的条件表达式
params
null FALSE 条件表达式为假
false FALSE 条件表达式为假
1!=1 FALSE 条件表达式为假
观察表达式

观察表达式用在使用表达式表达输出内容的场景,尤其在watch和tt命令中,观察表达式至关重要。

条件表达式将会使用greys内置的表达式解析引擎,识别OGNL语法,将表达式转换为待输出的对象。

以下是一些观察表达式使用的例子

字符串拼接

clazz.name+"."+method.name
数字运算

clazz.name.length()+method.name.length()
表达式核心变量
无论是匹配表达式也好、观察表达式也罢,他们核心判断变量都是围绕着一个greys中的通用通知对象Advice进行。

它的简略代码结构如下

public class Advice {

    private final ClassLoader loader;
    private final Class<?> clazz;
    private final GaMethod method;
    private final Object target;
    private final Object[] params;
    private final Object returnObj;
    private final Throwable throwExp;
    private final boolean isBefore;
    private final boolean isThrow;
    private final boolean isReturn;

    // getter/setter  
}  

这里列一个表格来说明不同变量的含义

变量名 变量解释
loader 本次调用类所在的ClassLoader
clazz 本次调用类的Class引用
method 本次调用方法反射引用
target 本次调用类的实例
params 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组
returnObj 本次调用返回的对象。当且仅当isReturntrue成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值void,则值为null
throwExp 本次调用抛出的异常。当且仅当isThrow
true成立时有效,表明方法调用是以抛出异常的方式结束。
isBefore 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时isBeforetrue成立,同时isThrowfalse和isReturn==false,因为在方法刚开始时,还无法确定方法调用将会如何结束。
isThrow 辅助判断标记,当前的方法调用以抛异常的形式结束。
isReturn 辅助判断标记,当前的方法调用以正常返回的形式结束。
所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合OGNL脚本语法或者引入了不在表格中的变量,

对条件表达式、检索表达式而言,则一律当成false来处理

对观察表达式而言,则放弃当前方法调用的处理(不输出)

JDK类支持
JDK的类默认由BootstrapClassLoader负责加载,由于Greys自己也适用了大量的JDK类,所以我不建议使用Greys直接对JDK相关类进行增强、代理。

默认而言,Greys会拒绝执行关于JDK类的操作命令。你需显式用options命令打开。

ga?>options unsafe true
+--------+--------------+-------------+
| NAME   | BEFORE-VALUE | AFTER-VALUE |
+--------+--------------+-------------+
| unsafe | false        | true        |
+--------+--------------+-------------+
Affect(row-cnt:1) cost in 4 ms.
ga?>

模式匹配
一些命令需要对类、方法进行模式匹配过滤,从1.5.4.6及其之后的版本之后,Greys默认支持通配符匹配,目前仅支持*和?两个通配符,正则表达式需要显式指定-E参数激活。

模式匹配举例

原sc命令的正则表达式匹配

sc com.apache.commons.lang.StringUtils
在1.5.4.6及其之后的版本中将会默认使用通配符表达式

sc com.apache.commons.lang.StringUtils
sc *lang.StringUtils
若想继续使用正则表达式匹配,需要显式-E参数激活

sc -E com.apache.commons.lang.StringUtils
sc -E com…*StringUtils
支持模式匹配的命令

所有需要模式匹配的命令都支持参数-E,他们分别是:sc、sm、stack、monitor、watch、tt、trace

Greys命令详解
命令清单
命令 说明
help 查看命令的帮助文档,每个命令和参数都有很详细的说明
sc 查看JVM已加载的类信息
sm 查看已加载的方法信息
monitor 方法执行监控
trace 渲染方法内部调用路径,并输出方法路径上的每个节点上耗时
ptrace 强化版的trace命令。通过指定渲染路径,并可记录下路径中所有方法的入参、返值;与tt命令联动。
watch 方法执行数据观测
tt 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
stack 输出当前方法被调用的调用路径
js 支持使用JavaScript脚本;支持CommonJS部分规范模块化(BMD规范)
version 输出当前目标Java进程所加载的Greys版本号
quit 退出greys客户端
shutdown 关闭greys服务端
reset 重置增强类,将被greys增强过的类全部还原
session 查看当前会话
jvm 查看当前JVM的信息
help命令
help命令会是你第一个在Greys中使用的命令,也会是今后使用最频繁的命令之一,当你在使用的过程中有任何不熟悉的疑问,请直接help吧~

查看命令清单

进入Greys的欢迎界面后,所有命令都可以通过help获取帮助。此时你直接输入一个help,Greys则会返回所有命令的大概用途介绍。

ga?>help
+----------+----------------------------------------------------------------------------------+
|       sc | Search all the classes loaded by JVM                                             |
+----------+----------------------------------------------------------------------------------+
|       sm | Search the method of classes loaded by JVM                                       |
+----------+----------------------------------------------------------------------------------+
|  monitor | Monitor the execution of specified Class and its method                          |
+----------+----------------------------------------------------------------------------------+
|    watch | Display the details of specified class and method                                |
+----------+----------------------------------------------------------------------------------+
|       tt | Time Tunnel                                                                      |
+----------+----------------------------------------------------------------------------------+
|    stack | Display the stack trace of specified class and method                            |
+----------+----------------------------------------------------------------------------------+
|   ptrace | Display the detailed thread path stack of specified class and method             |
+----------+----------------------------------------------------------------------------------+
|    trace | Display the detailed thread stack of specified class and method                  |
|       js | Enhanced JavaScript                                                              |
+----------+----------------------------------------------------------------------------------+
|  session | Display current session information                                              |
+----------+----------------------------------------------------------------------------------+
|     quit | Quit Greys console                                                               |
+----------+----------------------------------------------------------------------------------+
|  version | Display Greys version                                                            |
+----------+----------------------------------------------------------------------------------+
|      jvm | Display the target JVM information                                               |
+----------+----------------------------------------------------------------------------------+
|    reset | Reset all the enhanced classes                                                   |
+----------+----------------------------------------------------------------------------------+
| shutdown | Shut down Greys server and exit the console                                      |
+----------+----------------------------------------------------------------------------------+
|     help | Display Greys Help                                                               |
+----------+----------------------------------------------------------------------------------+
Affect(row-cnt:1) cost in 9 ms.
ga?>

嗯,囋囋,我知道我的英文翻译很烂,就不用吐槽了。期望能有人能帮我重新打理英文的帮助界面和英文xwiki,小生感激不尽!

查看命令详细帮助

help命令同时也支持对其他命令的一个解释说明,比如我们键入help watch,greys将会返回watch命令的所有参数解释、用法介绍等详细信息。

ga?>help watch
+---------+----------------------------------------------------------------------------------+
|   USAGE | -[bfesx:En:] class-pattern method-pattern express condition-express              |
|         | Display the details of specified class and method                                |
+---------+----------------------------------------------------------------------------------+
| OPTIONS |              [b] | Watch before invocation                                       |
|         | -----------------+-------------------------------------------------------------- |
|         |              [f] | Watch after invocation                                        |
|         | -----------------+-------------------------------------------------------------- |
|         |              [e] | Watch after throw exception                                   |
|         | -----------------+-------------------------------------------------------------- |
|         |              [s] | Watch after successful invocation                             |
|         | -----------------+-------------------------------------------------------------- |
|         |             [x:] | Expand level of object (0 by default)                         |
|         | -----------------+-------------------------------------------------------------- |
|         |              [E] | Enable regular expression to match (wildcard matching by def  |
|         |                  | ault)                                                         |
|         | -----------------+-------------------------------------------------------------- |
|         |             [n:] | Threshold of execution times                                  |
|         | -----------------+-------------------------------------------------------------- |
|         |    class-pattern | Path and classname of Pattern Matching                        |
|         | -----------------+-------------------------------------------------------------- |
|         |   method-pattern | Method of Pattern Matching                                    |
|         | -----------------+-------------------------------------------------------------- |
|         |          express | express, write by OGNL.                                       |
|         |                  |                                                               |
|         |                  | FOR EXAMPLE    params[0]                                      |
|         |                  |     params[0]+params[1]                                       |
|         |                  |     returnObj                                                 |
|         |                  |     throwExp                                                  |
|         |                  |     target                                                    |
|         |                  |     clazz                                                     |
|         |                  |     method                                                    |
|         |                  |                                                               |
|         |                  | THE STRUCTURE                                                 |
|         |                  |           target : the object                                 |
|         |                  |            clazz : the object's class                         |
|         |                  |           method : the constructor or method                  |
|         |                  |     params[0..n] : the parameters of method                   |
|         |                  |        returnObj : the returned object of method              |
|         |                  |         throwExp : the throw exception of method              |
|         |                  |         isReturn : the method ended by return                 |
|         |                  |          isThrow : the method ended by throwing exception     |
|         | -----------------+-------------------------------------------------------------- |
|         |  condition-expre | Conditional expression by OGNL                                |
|         |               ss |                                                               |
|         |                  | FOR EXAMPLE                                                   |
|         |                  |      TRUE : 1==1                                              |
|         |                  |      TRUE : true                                              |
|         |                  |     FALSE : false                                             |
|         |                  |      TRUE : params.length>=0                                  |
|         |                  |     FALSE : 1==2                                              |
|         |                  |                                                               |
|         |                  | THE STRUCTURE                                                 |
|         |                  |           target : the object                                 |
|         |                  |            clazz : the object's class                         |
|         |                  |           method : the constructor or method                  |
|         |                  |     params[0..n] : the parameters of method                   |
|         |                  |        returnObj : the returned object of method              |
|         |                  |         throwExp : the throw exception of method              |
|         |                  |         isReturn : the method ended by return                 |
|         |                  |          isThrow : the method ended by throwing exception     |
+---------+----------------------------------------------------------------------------------+
| EXAMPLE | watch -Eb org\.apache\.commons\.lang\.StringUtils isBlank params[0]              |
|         | watch -b org.apache.commons.lang.StringUtils isBlank params[0]                   |
|         | watch -f org.apache.commons.lang.StringUtils isBlank returnObj                   |
|         | watch -bf *StringUtils isBlank params[0]                                         |
|         | watch *StringUtils isBlank params[0]                                             |
|         | watch *StringUtils isBlank params[0] params[0].length==1                         |
+---------+----------------------------------------------------------------------------------+
Affect(row-cnt:1) cost in 15 ms.
ga?>

帮助信息组成

帮助文档分成Usage、Options、Example三个区域,分别是用途说明、参数列表、实际例子

ga?>help session
+---------+----------------------------------------------------------------------------------+
|   USAGE | -[c:]                                                                            |
|         | Display current session information                                              |
+---------+----------------------------------------------------------------------------------+
| OPTIONS |             [c:] | Modify the character set of session                           |
+---------+----------------------------------------------------------------------------------+
| EXAMPLE | session                                                                          |
|         | session -c GBK                                                                   |
|         | session -c UTF-8                                                                 |
+---------+----------------------------------------------------------------------------------+
Affect(row-cnt:1) cost in 2 ms.
ga?>

参数选项说明

[]中的参数为选填项,比如[d],意思是该命令可接受一个名称为d的选填参数,且不用参数值。

[:]中的参数则为选填,但有值的参数,比如[c:]

class-pattern/method-pattern,这两个参数为隐性参数,即在输入的时候不需要特意声明参数。class-pattern为类路径.类名称的表达式匹配,method-pattern则为方法名的表达式匹配。

sc命令
“Search-Class”的简写,这个命令能搜索出所有已经加载到JVM中的Class信息。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
[d] 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的e2ClassLoader等详细信息。
如果一个类被多个ClassLoader所加载,则会出现多次
[f] 输出当前类的成员变量信息
[E] 支持正则表达式匹配
使用参考

ga?>sc -df *alibaba.Address
+------------------+-----------------------------------------------+
|       class-info | com.alibaba.Address                           |
+------------------+-----------------------------------------------+
|      code-source | /Users/vlinux/temp/agent-test/target/         |
+------------------+-----------------------------------------------+
|             name | com.alibaba.Address                           |
+------------------+-----------------------------------------------+
|      isInterface | false                                         |
+------------------+-----------------------------------------------+
|     isAnnotation | false                                         |
+------------------+-----------------------------------------------+
|           isEnum | false                                         |
+------------------+-----------------------------------------------+
| isAnonymousClass | false                                         |
+------------------+-----------------------------------------------+
|          isArray | false                                         |
+------------------+-----------------------------------------------+
|     isLocalClass | false                                         |
+------------------+-----------------------------------------------+
|    isMemberClass | false                                         |
+------------------+-----------------------------------------------+
|      isPrimitive | false                                         |
+------------------+-----------------------------------------------+
|      isSynthetic | false                                         |
+------------------+-----------------------------------------------+
|      simple-name | Address                                       |
+------------------+-----------------------------------------------+
|         modifier | public                                        |
+------------------+-----------------------------------------------+
|       annotation |                                               |
+------------------+-----------------------------------------------+
|       interfaces |                                               |
+------------------+-----------------------------------------------+
|      super-class | com.alibaba.CountObject                       |
|                  |   `-java.lang.Object                          |
+------------------+-----------------------------------------------+
|     class-loader | sun.misc.Launcher$AppClassLoader@2a139a55     |
|                  |   `-sun.misc.Launcher$ExtClassLoader@5fb20bfd |
+------------------+-----------------------------------------------+
|           fields | modifier : private                            |
|                  |     type : java.lang.String                   |
|                  |     name : username                           |
|                  |                                               |
|                  | modifier : private                            |
|                  |     type : int                                |
|                  |     name : addressId                          |
|                  |                                               |
|                  | modifier : private                            |
|                  |     type : java.lang.String                   |
|                  |     name : addressName                        |
|                  |                                               |
+------------------+-----------------------------------------------+
Affect(row-cnt:1) cost in 9 ms.
ga?>

sm命令
“Search-Method”的简写,这个命令能搜索出所有已经加载了Class信息的方法信息。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
[d] 展示每个方法的详细信息
[E] 支持正则表达式匹配
使用参考

ga?>sm -d *alibaba.Address *
+-----------------------------+-------------------------------------------------+
| DECLARED-CLASS              | VISIBLE-METHOD                                  |
+-----------------------------+-------------------------------------------------+
| com.alibaba.Address         | declaring-class : class com.alibaba.Address     |
|                             |     method-name : getAddressId                  |
|                             |        modifier : public                        |
|                             |      annotation :                               |
|                             |      parameters :                               |
|                             |          return : int                           |
|                             |      exceptions :                               |
+-----------------------------+-------------------------------------------------+
| com.alibaba.Address         | declaring-class : class com.alibaba.Address     |
|                             |     method-name : getAddressName                |
|                             |        modifier : public                        |
|                             |      annotation :                               |
|                             |      parameters :                               |
|                             |          return : java.lang.String              |
|                             |      exceptions :                               |
+-----------------------------+-------------------------------------------------+
| com.alibaba.Address         | declaring-class : class com.alibaba.CountObject |
|   `-com.alibaba.CountObject |     method-name : finalize                      |
|                             |        modifier : protected                     |
|                             |      annotation :                               |
|                             |      parameters :                               |
|                             |          return : void                          |
|                             |      exceptions : java.lang.Throwable           |
+-----------------------------+-------------------------------------------------+
| com.alibaba.Address         | declaring-class : class com.alibaba.CountObject |
|   `-com.alibaba.CountObject |     method-name : count                         |
|                             |        modifier : public                        |
|                             |      annotation :                               |
|                             |      parameters :                               |
|                             |          return : int                           |
|                             |      exceptions :                               |
+-----------------------------+-------------------------------------------------+
Affect(row-cnt:4) cost in 9 ms.
ga?>

monitor命令
对匹配class-pattern/method-pattern的类.方法的调用进行监控。

monitor命令是介绍到的第一个非实时返回命令,实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标Java进程返回信息,直到用户输入Ctrl+D为止。服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而停止执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何Greys的命令也不会引起任何原有业务逻辑的改变。

监控的维度说明

监控项 说明
timestamp 时间戳
class Java类
method 方法(构造方法、普通方法)
total 调用次数
success 成功次数
fail 失败次数
rt 平均RT
fail-rate 失败率
参数说明

方法拥有一个命名参数[c:],意思是统计周期(cycle of output),拥有一个整形的参数值

参数名称 参数说明
[c:] 统计周期,默认值为120秒
使用参考

ga?>monitor -c 5 *alibaba*Test printAddress
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 22 ms.
+-----------+-------+--------+-------+---------+------+-----------+------------+------------+------------+
| TIMESTAMP | CLASS | METHOD | TOTAL | SUCCESS | FAIL | FAIL-RATE | AVG-RT(ms) | MIN-RT(ms) | MAX-RT(ms) |
+-----------+-------+--------+-------+---------+------+-----------+------------+------------+------------+

+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+
| TIMESTAMP           | CLASS                 | METHOD       | TOTAL | SUCCESS | FAIL | FAIL-RATE | AVG-RT(ms) | MIN-RT(ms) | MAX-RT(ms) |
+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+
| 2015-12-06 16:34:56 | com.alibaba.AgentTest | printAddress | 5     | 3       | 2    | 40.00%    | 0.20       | 0          | 1          |
+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+

+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+
| TIMESTAMP           | CLASS                 | METHOD       | TOTAL | SUCCESS | FAIL | FAIL-RATE | AVG-RT(ms) | MIN-RT(ms) | MAX-RT(ms) |
+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+
| 2015-12-06 16:35:01 | com.alibaba.AgentTest | printAddress | 5     | 3       | 2    | 40.00%    | 0.00       | 0          | 0          |
+---------------------+-----------------------+--------------+-------+---------+------+-----------+------------+------------+------------+

ga?>

trace命令
命令能主动搜索class-pattern/method-pattern所渲染的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[n:] 命令执行次数
[E] 支持正则表达式匹配
注意事项

trace能方便的帮助你定位和发现因RT高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路,目前暂时没有精力去解决往下几个层级的调用。如果真有需求可以Issues我。

使用参考

ga?>trace com.alibaba.manager.DefaultAddressManager toStringPass2
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 19 ms.
`---+Tracing for : thread_name="agent-test-address-printer" thread_id=0xb;is_daemon=false;priority=5;
    `---+[0,0ms]com.alibaba.manager.DefaultAddressManager:toStringPass2()
        +---[0,0ms]com.alibaba.Address:getAddressId()
        +---[0,0ms]com.alibaba.Address:getAddressId()
        +---[0,0ms]java.lang.Integer:valueOf()
        +---[0,0ms]com.alibaba.Address:getAddressName()
        +---[0,0ms]com.alibaba.Address:count()
        +---[0,0ms]java.lang.Integer:valueOf()
        `---[0,0ms]java.lang.String:format()

是不是很眼熟,没错,在JProfiler等收费软件中你曾经见识过类似的功能,这里你通过命令就能打印出指定调用路径。

[10,1ms]的含义,10所代表的含义是:当前节点的整体耗时;1的含义是:当前节点在当前步骤的耗时;两者之间用逗号分割,单位为毫秒。

ptrace命令
命令解释

命令为trace命令的强化版,通过指定渲染路径来完成对方法执行路径的渲染过程

命令能主动搜索tracing-path-pattern所渲染的路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[t] 记录下渲染路径上所有方法的入参与返回值,记录下的返回值可以与tt命令联动
[n:] 命令执行次数
[E] 支持正则表达式匹配
[path:] 渲染路径表达式匹配,该参数可多次使用
[Epath:] 正则表达式渲染路径表达式匹配,该参数可多次使用
使用例子

ga?>ptrace -t *alibaba*Test printAddress --path=*alibaba*
Press Ctrl+D to abort.
Affect(class-cnt:10 , method-cnt:36) cost in 148 ms.
`---+pTracing for : thread_name="agent-test-address-printer" thread_id=0xb;is_daemon=false;priority=5;process=1004;
    `---+[2,2ms]com.alibaba.AgentTest:printAddress(); index=1021;
        +---+[1,1ms]com.alibaba.manager.DefaultAddressManager:newAddress(); index=1014;
        |   +---[1,1ms]com.alibaba.CountObject:<init>(); index=1012;
        |   `---[1,0ms]com.alibaba.Address:<init>(); index=1013;
        +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toString(); index=1020;
        |   +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toStringPass1(); index=1019;
        |   |   +---+[2,1ms]com.alibaba.manager.DefaultAddressManager:toStringPass2(); index=1017;
        |   |   |   +---[1,0ms]com.alibaba.Address:getAddressId(); index=1015;
        |   |   |   +---+[1,0ms]com.alibaba.manager.DefaultAddressManager:throwRuntimeException(); index=1016;
        |   |   |   |   `---[1,0ms]throw:java.lang.RuntimeException
        |   |   |   `---[1,0ms]throw:java.lang.RuntimeException
        |   |   +---[2,0ms]com.alibaba.AddressException:<init>(); index=1018;
        |   |   `---[2,0ms]throw:com.alibaba.AddressException
        |   `---[2,0ms]throw:com.alibaba.AddressException
        `---[2,0ms]throw:com.alibaba.AddressException

±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| INDEX | PROCESS-ID | TIMESTAMP | COST(ms) | IS-RET | IS-EXP | OBJECT | CLASS | METHOD |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1012 | 1004 | 2015-12-06 16:46:49 | 0 | true | false | 0x943cff | CountObject | |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1013 | 1004 | 2015-12-06 16:46:49 | 0 | true | false | 0x943cff | Address | |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1014 | 1004 | 2015-12-06 16:46:49 | 1 | true | false | 0x6833b8a5 | DefaultAddressManager | newAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1015 | 1004 | 2015-12-06 16:46:49 | 0 | true | false | 0x943cff | Address | getAddressId |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1016 | 1004 | 2015-12-06 16:46:49 | 0 | false | true | 0x6833b8a5 | DefaultAddressManager | throwRuntimeException |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1017 | 1004 | 2015-12-06 16:46:49 | 0 | false | true | 0x6833b8a5 | DefaultAddressManager | toStringPass2 |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1018 | 1004 | 2015-12-06 16:46:49 | 0 | true | false | 0x67e7a923 | AddressException | |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1019 | 1004 | 2015-12-06 16:46:49 | 1 | false | true | 0x6833b8a5 | DefaultAddressManager | toStringPass1 |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1020 | 1004 | 2015-12-06 16:46:49 | 1 | false | true | 0x6833b8a5 | DefaultAddressManager | toString |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1021 | 1004 | 2015-12-06 16:46:49 | 2 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
watch命令
能方便的让你观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写OGNL表达式进行对应变量的查看。

参数说明

watch的参数比较多,主要是因为它能在4个不同的场景观察对象

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
express 观察表达式
[b] 在方法调用之前观察
[e] 在方法异常之后观察
[s] 在方法返回之后观察
[f] 在方法结束之后(正常返回和异常返回)观察
这里重点要说明的是观察表达式,观察表达式的构成主要由OGNL表达式组成,所以你可以这样写params[0]+"$"+target,只要是一个合法的OGNL表达式,都能被正常支持。

观察的维度也比较多,主要体现在参数advice的数据结构上。Advice参数最主要是封装了通知节点的所有信息。参考表达式核心变量中关于该节点的描述。

使用参考

ga?>watch -b *Test printAddress '"params[0]="+params[0]'
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 32 ms.
params[0]=3163
params[0]=3164
params[0]=3165
params[0]=3166

这里需要说明的一个参数x,这个参数决定了输出的结果的层级遍历输出对象,当加上这个参数之后,greys会将输出的对象按照指定层级进行剥开。-x 1表明展开第1层级。

ga?>watch -s com.alibaba.manager.DefaultAddressManager newAddress returnObj -x 1
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 34 ms.
@Address[
    username=@String[dukun],
    addressId=@Integer[3244],
    addressName=@String[ADDRESS],
]

tt命令
时间隧道命令是我在使用watch命令进行问题排查的时候衍生出来的想法。watch虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。

这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。

于是乎,TimeTunnel命令就诞生了。

记录方法的调用

基本用法

对于一个最基本的使用来说,就是记录下当前方法的每次调用环境现场。

ga?>tt -t -n 3 *Test printAddress
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 33 ms.

±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| INDEX | PROCESS-ID | TIMESTAMP | COST(ms) | IS-RET | IS-EXP | OBJECT | CLASS | METHOD |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1036 | 1009 | 2015-12-06 16:57:06 | 1 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1037 | 1010 | 2015-12-06 16:57:07 | 0 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1038 | 1011 | 2015-12-06 16:57:08 | 0 | true | false | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
ga?>
命令参数解析

-t

tt命令有很多个主参数,-t就是其中之一。这个参数的表明希望记录下类*Test的print方法的每次执行情况。

-n 3

当你执行一个调用量不高的方法时可能你还能有足够的时间用CTRL+D中断tt命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的JVM内存撑爆。

此时你可以通过-n参数指定你需要记录的次数,当达到记录次数时greys会主动中断tt命令的记录过程,避免人工操作无法停止的情况。

表格字段说明

表格字段 字段解释
INDEX 时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。
PROCESS-ID 过程编号,我们认为同一个线程的一次同步调用为一个过程
TIMESTAMP 方法执行的本机时间,记录了这个时间片段所发生的本机时间
COST(ms) 方法执行的耗时
IS-RET 方法是否以正常返回的形式结束
IS-EXP 方法是否以抛异常的形式结束
OBJECT 执行对象的hashCode(),注意,曾经有人误认为是对象在JVM中的物理内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体
CLASS 执行的类名
METHOD 执行的方法名
条件表达式

不知道大家是否有在使用过程中遇到以下困惑

似乎很难区分出重载的方法
我只需要观察特定参数,但是tt却全部都给我记录了下来
从1.6.0.6版本之后,应广大妇女群众的要求,增加了条件表达式,这样你可以通过简单的条件表达式解决上边的困惑。

条件表达式也是用OGNL来编写,核心的判断对象依然是Advice对象。

除了tt命令之外,watch、trace、stack命令也都支持条件表达式

解决方法重载

tt -t *Test print params[0].length==1

通过制定参数个数的形式解决不同的方法签名,如果参数个数一样,你还可以这样写

tt -t *Test print ‘params[1].class == Integer.class’

解决指定参数

tt -t *Test print params[0].mobile==“13989838402”

检索调用记录

当你用tt记录了一大片的时间片段之后,你希望能从中筛选出自己需要的时间片段,这个时候你就需要对现有记录进行检索。

假设我们有这些记录

ga?>tt -l
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| INDEX | PROCESS-ID | TIMESTAMP | COST(ms) | IS-RET | IS-EXP | OBJECT | CLASS | METHOD |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1047 | 1020 | 2015-12-06 17:03:00 | 1 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1048 | 1021 | 2015-12-06 17:03:01 | 0 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1049 | 1022 | 2015-12-06 17:03:01 | 1 | true | false | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1050 | 1023 | 2015-12-06 17:03:01 | 0 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1051 | 1024 | 2015-12-06 17:03:02 | 1 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1052 | 1025 | 2015-12-06 17:03:02 | 1 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1053 | 1026 | 2015-12-06 17:03:02 | 0 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1054 | 1027 | 2015-12-06 17:03:03 | 0 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1055 | 1028 | 2015-12-06 17:03:03 | 0 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1056 | 1029 | 2015-12-06 17:03:03 | 0 | true | false | 0x2062a3d | AgentTest | printUser |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
Affect(row-cnt:10) cost in 3 ms.
ga?>
我需要筛选出printAddress方法的调用信息

ga?>tt -s method.name==“printAddress”
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| INDEX | PROCESS-ID | TIMESTAMP | COST(ms) | IS-RET | IS-EXP | OBJECT | CLASS | METHOD |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1049 | 1022 | 2015-12-06 17:03:01 | 1 | true | false | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1052 | 1025 | 2015-12-06 17:03:02 | 1 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
| 1055 | 1028 | 2015-12-06 17:03:03 | 0 | false | true | 0x2062a3d | AgentTest | printAddress |
±---------±-----------±---------------------±-----------±---------±---------±----------------±-------------------------------±-------------------------------+
Affect(row-cnt:3) cost in 8 ms.
ga?>
你需要一个-s参数。同样的,搜索表达式的核心对象依旧是Advice对象。

查看调用信息

对于具体一个时间片的信息而言,你可以通过-i参数后边跟着对应的INDEX编号查看到他的详细信息。

ga?>tt -i 1055
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| INDEX | 1055 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| PROCESS-ID | 1028 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| GMT-CREATE | 2015-12-06 17:03:03 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| COST(ms) | 0 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| OBJECT | 0x2062a3d |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| CLASS | com.alibaba.AgentTest |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| METHOD | printAddress |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| IS-RETURN | false |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| IS-EXCEPTION | true |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| PARAMETERS[0] | 3789 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| THROW-EXCEPTION | com.alibaba.AddressException: java.lang.RuntimeException: test |
| | at com.alibaba.manager.DefaultAddressManager.toStringPass1(DefaultAddressManager.java:22) |
| | at com.alibaba.manager.DefaultAddressManager.toString(DefaultAddressManager.java:15) |
| | at com.alibaba.AgentTest.printAddress(AgentTest.java:80) |
| | at com.alibaba.AgentTest.access$300(AgentTest.java:7) |
| | at com.alibaba.AgentTest$3.null(Unknown Source) |
| | Caused by: java.lang.RuntimeException: test |
| | at com.alibaba.manager.DefaultAddressManager.throwRuntimeException(DefaultAddressManager.java:39) |
| | at com.alibaba.manager.DefaultAddressManager.toStringPass2(DefaultAddressManager.java:29) |
| | at com.alibaba.manager.DefaultAddressManager.toStringPass1(DefaultAddressManager.java:20) |
| | … 4 more |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| STACK | thread_name=“agent-test-address-printer” thread_id=0xb;is_daemon=false;priority=5; |
| | @com.alibaba.AgentTest.access$300() |
| | at com.alibaba.AgentTest$3.null(null:-1) |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
Affect(row-cnt:1) cost in 7 ms.
ga?>
重做一次调用

当你少少做了一些调整之后,你可能需要前端系统重新触发一次你的调用,此时得求爷爷告奶奶的需要前端配合联调的同学再次发起一次调用。而有些场景下,这个调用不是这么好触发的。

tt命令由于保存了当时调用的所有现场信息,所以我们可以自己主动对一个INDEX编号的时间片自主发起一次调用,从而解放你的沟通成本。此时你需要-p参数。

ga?>tt -i 1055 -p
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| INDEX | 1055 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| PROCESS-ID | 1028 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| GMT-CREATE | 2015-12-06 17:03:03 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| COST(ms) | 1 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| OBJECT | 0x2062a3d |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| CLASS | com.alibaba.AgentTest |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| METHOD | printAddress |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| IS-RETURN | true |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| IS-EXCEPTION | false |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| PARAMETERS[0] | 3789 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| RETURN-OBJ | 1 |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
| STACK | thread_name=“ga-command-execute-daemon” thread_id=0x22;is_daemon=true;priority=9; |
| | @com.github.ompc.greys.core.command.TimeTunnelCommand$6.action() |
| | at com.github.ompc.greys.core.server.DefaultCommandHandler.execute(DefaultCommandHandler.java:210) |
| | at com.github.ompc.greys.core.server.DefaultCommandHandler.executeCommand(DefaultCommandHandler.java:87) |
| | at com.github.ompc.greys.core.server.GaServer 4. r u n ( G a S e r v e r . j a v a : 332 ) ∣ ∣ ∣ a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r . r u n W o r k e r ( T h r e a d P o o l E x e c u t o r . j a v a : 1142 ) ∣ ∣ ∣ a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r 4.run(GaServer.java:332) | | | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) | | | at java.util.concurrent.ThreadPoolExecutor 4.run(GaServer.java:332)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)atjava.util.concurrent.ThreadPoolExecutorWorker.run(ThreadPoolExecutor.java:617) |
| | at java.lang.Thread.run(Thread.java:745) |
±----------------±-------------------------------------------------------------------------------------------------------------------------------------------------------+
Time fragment[1055] successfully replayed.
Affect(row-cnt:1) cost in 2 ms.
ga?>
你会发现结果虽然一样,但调用的路径发生了变化,有原来的程序发起变成了greys自己的内部线程发起的调用了。

得益于Greys的ClassLoader隔离策略,Greys在内部自己发起线程请求调用的时候,依然采用的是目标类所归属的ClassLoader,所以在OSGI、Tomcat容器等场景下,Greys依然能正确的重做此次调用。

需要强调的点

ThreadLocal信息丢失

很多框架偷偷的将一些环境变量信息塞到了发起调用线程的ThreadLocal中,由于调用线程发生了变化,这些ThreadLocal线程信息无法通过greys保存,所以这些信息将会丢失。

一些常见的CASE比如:阿里鹰眼系统的TraceId、阿里全链路平台的压测流量标记位。

引用的对象

需要强调的是,tt命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在tt查看的时候将无法看到当时最准确的值。这也是为什么watch命令存在的意义。

stack命令
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多。或者你根本就不知道这个方法是从那里被执行了,正在郁闷,正在彷徨。此时你需要的是stack命令。

参数说明

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
[n:] 命令执行次数
[E] 支持正则表达式匹配
使用例子

ga?>stack com.alibaba.manager.DefaultAddressManager newAddress
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 36 ms.
thread_name="agent-test-address-printer" thread_id=0xb;is_daemon=false;priority=5;
    @java.lang.reflect.Method.invoke()
        at com.alibaba.manager.DefaultAddressManager.newAddress(DefaultAddressManager.java:-1)
        at com.alibaba.AgentTest.printAddress(AgentTest.java:73)
        at com.alibaba.AgentTest.access$300(AgentTest.java:7)
        at com.alibaba.AgentTest$3.null(null:-1)

js命令
js命令几经波折,几乎在两个重要版本中绝迹,所以我不得不先介绍这个命令的历史背景。

在GREYS的1.5版本时代,字节码增强使用的是javassist进行,里面的黑盒严重,我比较难介入和调试其中的字节码生成过程。在这种情况下我们发现了一个JavaScript引擎rhino与Javassist结合存在严重漏洞的问题却没有更多调试的手段。

考虑到性能、后续扩展的发展,GREYS从1.6开始替换成asm,之后我拥有了最精细的字节码控制权限。在1.7.4.0版本中我恢复了GREYS对JavaScript的支持,并通过了之前BUG的测试。一切正常。

现有的GREYS命令不能完全满足所有人的需求,有一些需要根据业务场景进行个性化处理的场景,现有的GREYS命令就比较难支持了。

比如我需要查看目标JVM中所有SQL的执行情况,因为java.sql.PreparedStatement在不同的JDBC协议实现下,存储的原生SQL获取的方式也不尽相同,JDBC规范中并未提供标准的API获取原生的SQL。这个时候需要对原生SQL进行统计就必须进行个性化开发。

参数说明

参数名称 参数说明
[c:] 指定脚本字符编码
[E] 支持正则表达式匹配
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
script-path 脚本存放位置,支持HTTP/HTTPS
使用例子

运行第一个JavaScript

目标

我们编写一个watch.js脚本,这个脚本的主要是在方法运行之前输出方法的大概信息。类似于

watch -b Test print clazz.name+"."+method.name+"()"
步骤

首先我们先生成一个脚本文件

touch /tmp/watch.js
然后往里面写入以下内容

require([‘greys’], function (greys) {
greys.watching({

    before: function (o, a) {
        o.println(a.clazz.name+"."+a.method.name+"()");
    },

});

})
接下来启动GREYS之行命令运行

js Test print /tmp/watch.js
执行效果

ga?>js Test print /tmp/watch.js
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:2) cost in 35 ms.
com.alibaba.AgentTest.printAddress()
com.alibaba.AgentTest.printUser()
com.alibaba.AgentTest.printAddress()
com.alibaba.AgentTest.printUser()
com.alibaba.AgentTest.printUser()
com.alibaba.AgentTest.printAddress()
运行一个远程的JavaScript

除了本地临时写代码之外,你也可以将平时积累好的脚本代码放在远程服务端(比如Github),可以使用远程加载的方式运行。

我在Github上提前准备了一个watch.js文件,内容和原来差不多

执行命令

js Test print https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/watch.js
执行效果

ga?>js Test print https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/watch.js
Press Ctrl+D to abort.
Affect(class-cnt:1 , method-cnt:2) cost in 43 ms.
call from remote script: com.alibaba.AgentTest.printUser()
call from remote script: com.alibaba.AgentTest.printAddress()
call from remote script: com.alibaba.AgentTest.printUser()
call from remote script: com.alibaba.AgentTest.printAddress()
更详细的命令帮助可以见JavaScriptSupport

version命令
这估计是最好理解的一个命令了,输出当前Greys的版本号,这里输出的版本号不是Client的版本,而是当前加载到目标Java进程中的Greys版本。

quit命令
这里说明下与shutdown命令的区别,quit命令仅仅是将客户端关闭,而不会将目标Java进程中的与Greys的Server关闭。所以如果仅仅是希望简单的退出Greys控制台,则使用quit命令足矣。

一旦Greys控制台退出,控制台所绑定的Session将会被关闭,Session上所有存活的事务也都会被中止并释放。

shutdown命令
命令执行后将会完成两件事:

关闭Greys在目标Java所加载的Socket服务,所占用的端口将会被释放,同时,本地的Greys控制台也因为远程Socket关闭而主动退出。

重置所有被Greys所增强的类。同reset命令。

reset命令
重置指定被Greys所增强的类。

session命令
会话命令是在1.6版本之后新增,整个命令的定位是维护好会话级的参数。目前可修改的就一个字符集。

参数说明

参数名称 参数说明
[c:] 指定会话字符集
使用例子

直接查看会话信息

ga?>session
±-----------±-----------------+
| JAVA_PID | 8609 |
±-----------±-----------------+
| SESSION_ID | 2 |
±-----------±-----------------+
| DURATION | 300000 |
±-----------±-----------------+
| CHARSET | UTF-8 |
±-----------±-----------------+
| PROMPT | ga?> |
±-----------±-----------------+
| FROM | /127.0.0.1:58186 |
±-----------±-----------------+
| TO | /127.0.0.1:3658 |
±-----------±-----------------+
Affect(row-cnt:1) cost in 0 ms.
ga?>
修改字符集

ga?>session -c GBK
change charset before[UTF-8] -> new[GBK]
Affect(row-cnt:1) cost in 26 ms.
jvm命令
查看当前JVM的信息,无参数。

ga?>jvm
±-------------------±----------------------------------------------------------------------------------------------------+
| CATEGORY | INFO |
±-------------------±----------------------------------------------------------------------------------------------------+
| RUNTIME | MACHINE-NAME : 25428@vlinux-air.local |
| | JVM-START-TIME : 2015-06-16 22:12:20 |
| | MANAGEMENT-SPEC-VERSION : 1.2 |
| | SPEC-NAME : Java Virtual Machine Specification |
| | SPEC-VENDOR : Oracle Corporation |
| | SPEC-VERSION : 1.8 |
| | VM-NAME : Java HotSpot™ 64-Bit Server VM |
| | VM-VENDOR : Oracle Corporation |
| | VM-VERSION : 25.25-b02 |
| | INPUT-ARGUMENTS : [] |
| | CLASS-PATH : . |
| | BOOT-CLASS-PATH : /Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/li |
| | b/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Conte |
| | nts/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_25.j |
| | dk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachin |
| | es/jdk1.8.0_25.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVir |
| | tualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/lib/jce.jar:/Library/Jav |
| | a/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/lib/charsets.ja |
| | r:/Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/l |
| | ib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/H |
| | ome/jre/classes |
| | LIBRARY-PATH : /Users/vlinux/Library/Java/Extensions:/Library/Java/Extensions:/Networ |
| | k/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java |
| | :. |
±-------------------±----------------------------------------------------------------------------------------------------+
| CLASS-LOADING | LOADED-CLASS-COUNT : 3045 |
| | TOTAL-LOADED-CLASS-COUNT : 3045 |
| | UNLOADED-CLASS-COUNT : 0 |
| | IS-VERBOSE : false |
±-------------------±----------------------------------------------------------------------------------------------------+
| COMPILATION | NAME : HotSpot 64-Bit Tiered Compilers |
| | TOTAL-COMPILE-TIME : 8903(ms) |
±-------------------±----------------------------------------------------------------------------------------------------+
| GARBAGE-COLLECTORS | PS Scavenge : 4/40(ms) |
| | [count/time] |
| | PS MarkSweep : 0/0(ms) |
| | [count/time] |
±-------------------±----------------------------------------------------------------------------------------------------+
| MEMORY-MANAGERS | CodeCacheManager : Code Cache |
| | Metaspace Manager : Metaspace |
| | Compressed Class Space |
| | PS Scavenge : PS Eden Space |
| | PS Survivor Space |
| | PS MarkSweep : PS Eden Space |
| | PS Survivor Space |
| | PS Old Gen |
±-------------------±----------------------------------------------------------------------------------------------------+
| MEMORY | HEAP-MEMORY-USAGE : 163053568/134217728/1908932608/54796080 |
| | [committed/init/max/used] |
| | NO-HEAP-MEMORY-USAGE : 32571392/2555904/-1/31757608 |
| | [committed/init/max/used] |
| | PENDING-FINALIZE-COUNT : 0 |
±-------------------±----------------------------------------------------------------------------------------------------+
| OPERATING-SYSTEM | OS : Mac OS X |
| | ARCH : x86_64 |
| | PROCESSORS-COUNT : 4 |
| | LOAD-AVERAGE : 2.50634765625 |
| | VERSION : 10.10.3 |
±-------------------±----------------------------------------------------------------------------------------------------+
| THREAD | COUNT : 8 |
| | DAEMON-COUNT : 7 |
| | LIVE-COUNT : 9 |
| | STARTED-COUNT : 19 |
±-------------------±----------------------------------------------------------------------------------------------------+
Affect cost in 22 ms.
常见问题答疑
安装问题
安装失败

$:>curl -sLk http://ompc.oss.aliyuncs.com/greys/install.sh|ksh
downloading… greys.zip.43678
download file failed!
问:

为什么我在安装Greys的时候失败了?

答:

Greys的安装脚本首先先从阿里云上下载greys.zip,然后进行解压、安装。所以必须要求执行安装脚本的用户必须有对当前目录的写权限,一般出现这个问题,可以检查网络、磁盘空间以及当前目录是否有写权限。

安装目录

问:

如果我在/tmp/目录下执行安装脚本,请问Greys会怎么安装?

答:

安装在/tmp/greys/目录下,该目录下一共有三个重要文件

/tmp/greys/greys-agent.jar
/tmp/greys/greys-core.jar
/tmp/greys/greys.sh
其中greys-core.jar为greys的程序主体,启动类、加载类都在这个jar包当中;greys-agent.jar则为目标JVM的加载引导程序;greys.sh为一个可执行脚本,为Greys的启动脚本。

如何删除

问:

安装的Greys如何进行删除?

答:

Greys是一个绿色环保软件,不会修改你的注册表,但会在 H O M E 目 录 下 创 建 隐 藏 目 录 HOME目录下创建隐藏目录 HOMEHOME/.greys,用于保存不同的版本和Jline的历史命令,你可以直接删除。

启动问题
启动报Operation not permitted错误

问:

我在启动Greys的时候报这样的错误

$:> ./greys.sh 11064
start greys failed, because : Operation not permitted
答:

Greys要求执行执行启动命令的用户必须拥有和目标进程ID同样的权限(在这个Case中,目标进程ID为11064),否则JVM将无法挂载Greys对应的jar包

启动报No such process错误

问:

我在启动Greys的时候报这样的错误

$:> ./greys.sh 11063
start greys failed, because : No such process
答:

目标进程ID不存在。报这样的错误,请核对你的目标进程是否存在。

启动报Unable to open socket file: target process not responding or HotSpot VM not loaded

问:

我在启动Greys的时候报这样的错误

$:> sudo -u admin ./greys.sh 12592
密码:
start greys failed, because : Unable to open socket file: target process not responding or HotSpot VM not loaded
答:

目标进程ID不是JVM进程,或目标JVM进程不支持加载操作,比如低于JDK6的版本等。 一般遇到这种问题一定要非常小心谨慎的执行,如果对方进程编程不严谨,很可能会让对方进程CORE掉。上次我就弄死了个nginx的worker -_-!!

启动没有响应

问:

启动之后就什么反应也没有,也没有出现预期的ga?>提示符

答:

很有可能你的3658端口已经被别的进程占据,请核对你当前机器所开的端口

netstat -anp|grep LIST
解决方案也很简单,换个端口

./greys.sh 4567@127.0.0.1:6666
sudo -u方式启动报权限不足

请给你的sudo命令加上-H参数

sudo -u admin -H ./greys.sh 4567
使用问题
哪些命令会导致性能问题

Greys的大部分命令性能开销都非常低廉,当然前提是一次性操作的类不要太多。

是否能增强由BootstrapClassLoader所加载的类

当然是可以的,但默认我封印了这个能力。主要是Greys自己也使用了大量BootstrapClassLoader所加载的类,如果处理不好极其容易造成故障。

你可以通过隐藏命令options激活这个功能

ga?>options unsafe true

±-------±-------------±------------+
| NAME | BEFORE-VALUE | AFTER-VALUE |
±-------±-------------±------------+
| unsafe | false | true |
±-------±-------------±------------+
Affect(row-cnt:1) cost in 2 ms.
接下来你可以尝试增强系统类了

ga?>monitor -c 5 java.lang.String substring
Press Ctrl+D or Ctrl+X to abort.
Affect(class-cnt:1 , method-cnt:2) cost in 35 ms.

±--------------------±-----------------±----------±------±--------±-----±-----±----------+
| timestamp | class | method | total | success | fail | rt | fail-rate |
±--------------------±-----------------±----------±------±--------±-----±-----±----------+
| 2015-06-16 23:44:54 | java.lang.String | substring | 30 | 30 | 0 | 0.23 | 0.00% |
±--------------------±-----------------±----------±------±--------±-----±-----±----------+
但我话就放在这里,随意增强系统类。后果自负!

其他问题
Greys支持将信息输出到文件中么?

支持,不过要做一些小命令。

./greys.sh 4567|tee -a ./greys.log
Greys能使用在Sun JDK5的版本么?

很遗憾抱歉的说,不行。

Greys的原理和Btrace一样,依赖了JDK6+提供的Instumentation特性,所以必须要求目标的JDK环境是JDK6及其以上的版本。

理论上Greys应该能在各种实现了SUN标准的各种JVM实现中运行,比如JRockit、Zing等,但我自己没有机会尝试,若有朋友能提供环境能进行测试并反馈,我将不胜感激。

JRE中由于缺少tools.jar,所以无法直接运行Greys,需要稍作一些修改。同样的也没有需求要在JRE中运行Greys,我这里也偷个懒,留给其他有需要的人来实现吧。

程序中是否有彩蛋?

有,我当初做这个软件的唯一目的就是希望能快速定位问题,然后好陪她逛街。为了不忘记这个初心,我将这位她的英文名作为命令整合到了Greys中,可以尝试找找!

版本管理

多版本管理

从1.7.x.x版本开始,greys.sh脚本支持自动更新,在网络允许的情况下会自动监测远程服务器上是否存在可升级的最新版本。

若网络不可达(网络隔离的环境)则需要进行本地安装。本地安装也一样会纳入到多版本管理识别范围。

大版本兼容性问题

大版本之间不做任何兼容性保障,比如1.7.x.x版本的客户端不保证能访问1.6.x.x启动的服务端。

版本号说明
主版本.大版本.小版本.漏洞修复

主版本

这个版本更新说明程序架构体系进行了重大升级,比如之前的0.1版升级到1.0版本,整个软件的架构从单机版升级到了SOCKET多机版。并将Greys的性质进行的确定:Java版的HouseMD,但要比前辈们更强。

大版本

程序的架构设计进行重大改造,但不影响用户对这款软件的定位。

小版本

增加新的命令和功能

漏洞修复

对现有版本进行漏洞修复和增强

版本升级说明

主版本、大版本之间不做任何向下兼容的承诺。即0.1版本的Client不保证一定能正常访问1.0版本的Server。
小版本不兼容的功能会在版本升级中指出
漏洞修复保证所有功能向下兼容

心路感悟

多年的问题排查经验我没有过多的分享,一个Java程序员个中的苦闷也无从分享,一切我都融入到了这款软件的命令中,希望这些沉淀能帮助到可能需要到的你少走一些弯路,同时我也非常期待你们对她的反馈,这样我将感到非常开心和有成就感。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
诞生 很早的时候,我们使用BTrace排查问题,在感叹BTrace的强大之余,也曾好几次将线上系统折腾挂掉。2012年淘宝的聚石写了HouseMD,将 常用的几个Btrace脚本整合在一起形成一个独立风格的应用,但其核心代码用的是Scala,我们没这方面的编程维护经验,所以只好艳羡HouseMD 的才思敏捷而无法在其上增加功能。 于是乎,Greys诞生了。 PS:目前Greys仅支持Linux/Unix/Mac上的Java6 ,Windows暂时无法支持 Greys是一个java进程执行过程中的异常诊断工具。 在不中断程序执行的情况下轻松完成问题排查工作。 和HouseMD一样,Greys-Anatomy取名同名美剧“实习医生格蕾”,目的是向前辈致敬。代码编写的时候参考了BTrace和HouseMD两个前辈的思路。 目标群体 有时候突然一个问题反馈上来,需要入参才能完成定位,但恰恰没有任何日志。回去加上重新部署,一杯咖啡时间过去了,是不是很崩溃? 当你经过反复这样几次折腾之后变得聪明了,在自己的代码的所有入参和出参地方都加上debug日志,但这次问题似乎暴露在别人的代码中了...是不是很无奈? 突然遇到线上一个性能问题无法确定到底是哪个环节的耗时,只能反复抓jstack猜,还有没有办法可以好好的过日子啦? 遇到以上问题时,你就是我们这类工具的目标客户,此类工具能利用Java6的Instrumentation特性,动态增强你所指定的类,获取你想要到的信息。 我们的座右铭 让程序解决繁琐的事情 特性功能 交互方式 命令行交互 内置功能 查看加载类,方法信息 方法执行监控(调用量,成功失败率,响应时间) 方法执行数据观测(参数,返回结果,异常信息等) 方法执行数据记录 性能开销渲染 方法执行数据自定义观测(js脚本) 查看方法调用堆栈 软件特点 纯Java实现的开源项目 安装使用便捷,仅一个jar包 可无需重启JVM进行CT式诊断 Groovy表达式展开变量,方便你查看入参、出参、异常、当前对象的各种属性细节 常用分析命令集成,monitor、trace等 观察变量的出入参 时间隧道,tt命令能以时间维度纪录下监控期内的每一次调用环境 多人并行协作 基于C/S架构的任务模式甚至能让多人同时远程到同一进程上执行不同的指令、脚本,非常适合团队一起进行线上问题排查与跟踪。Greys采用纯Java编写并留有良好的扩展,如果你有需求,只要你会Java,就可以为你自己编写想要的功能。 Greys最有利的武器是他的ONGL表达式,能让你在感受到HouseMD集成功能便利的同时,也能发挥出自定义Btrace脚本的灵活。 应用管理员拥有JVM进程权限,由他来首先在目标JVM上启动Greys 技术专家A和B平时没有对应机器的权限,但只要网络能访问,他们可以通过指定ip:port直接访问目标机器的JVM进程,仿佛在本地一般 标签:greys

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值