BTrace简介及使用

背景 

在生产环境中可能经常遇到各种问题,定位问题需要获取程序运行时的数据信息,如方法参数、返回值、全局变量、堆栈信息等。为了获取这些数据信息,我们可以通过改写代码,增加日志信息的打印,再发布到生产环境。通过这种方式,一方面将增大定位问题的成本和周期,对于紧急问题无法做到及时响应;另一方面重新部署后环境可能已被破坏,很难重新问题的场景。

对于程序员来说最头大的问题之一就是线上出了故障了,但是我们无法debug来找出问题原因,同时在上线的时候日志级别限定了我们不可能把所有的细节都打印到log上,这个时候故障都等在哪里,能办的手段无非看源码,通过仔细看代码来找出问题,并编译重新上线解决,这种手段能解决一部分代码,但是对于一些隐藏较深的bug就无能为力了,例如OOM或是频繁的full gc,一般是一个很多的对象没有被释放或是一个对象被频繁的创建调用。在一个复杂的项目中,一个OOM问题可能是一个对象被频繁创建,这种方式指望通过看源码是很难解决的,但是BTrace可以迅速帮助我们定位到问题所在地。

介绍 

Btrace (Byte Trace)是sun推出的一款java 动态、安全追踪(监控)工具,可以不停机的情况下监控线上情况,并且做到最少的侵入,占用最少的系统资源。 BTrace应用较为广泛的原因应该是其安全性和无侵入性,其中热交互技术,使得我们无需启动Agent的情况下动态跟踪分析,其安全性不会导致对目标Java进程的任何破坏性影响,使得BTrace成为我们线上产品问题定位的利器。无侵入性无需我们对原有代码做任何修改,降低上线风险和测试成本,并且无需重启启动目标Java进程进行Agent加载即可动态分析和跟踪目标程序,可以说BTrace可以满足大部分的应用场景。

安装
  • 设置BTRACE_HOME,将bin目录加至环境变量PATH中。
 使用
  1. jps命令查出需要监控的jvm pid 
  2. 编写BTrace跟踪程序 (安装包中带有大量的Demo,附录会对各个Demo的功能进行一一说明) 
  3. 执行:btrace <pid> BTrace跟踪程序

BTrace主要包含btracec和btracer两个命令编译和启动BTrace脚本: 
1. btrace 
功能: 用于运行BTrace跟踪程序。 
命令格式: 
btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>] 

  • -I:没有这个表明跳过预编译
  • include-path:指定用来编译脚本的头文件路径(关于预编译可参考例子ThreadBean.java)
  • port:btrace agent端口,默认是2020
  • classpath:编译所需类路径,一般是指btrace-client.jar等类所在路径
  • pid:java进程id
  • btrace-script:btrace脚本可以是.java文件,也可以是.class文件
  • args:传递给btrace脚本的参数, 在脚本中可以通过$(), $length()来获取这些参数(定义在BTraceUtils中)

示例: 
btrace -cp build/  1200 AllCalls1.java 
参数含义: 
include-path指定头文件的路径,用于脚本预处理功能,可选; 
port指定BTrace agent的服务端监听端口号,用来监听clients,默认为2020,可选; 
classpath用来指定类加载路径,默认为当前路径,可选; 
pid表示进程号,可通过jps命令获取; 
btrace-script即为BTrace脚本;btrace脚本如果以.java结尾,会先编译再提交执行。可使用btracec命令对脚本进行预编译。 
args是BTrace脚本可选参数,在脚本中可通过"$"和"$length"获取参数信息。 

2. btracec 
功能: 用于预编译BTrace脚本,用于在编译时期验证脚本正确性。 
btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files> 
参数意义同btrace命令一致,directory表示编译结果输出目录。 

3. btracer 
功能: btracer命令同时启动应用程序和BTrace脚本,即在应用程序启动过程中使用BTrace脚本。而btrace命令针对已运行程序执行BTrace脚本。 
命令格式: 
btracer <pre-compiled-btrace.class> <application-main-class> <application-args> 
参数说明: 
pre-compiled-btrace.class表示经过btracec编译后的BTrace脚本。 
application-main-class表示应用程序代码; 
application-args表示应用程序参数。 
该命令的等价写法为: 
java -javaagent:btrace-agent.jar=script=<pre-compiled-btrace-script1>[,<pre-compiled-btrace-script1>]* <MainClass> <AppArguments>

局限性

虽然BTrace很强大,但Btrace脚本就是一个普通的用@Btrace注解的Java类,其中包含一个或多个public static void修饰的方法,为了保证对目标程序不造成影响,Btrace脚本对其可以执行的动作做了很多限制,如下:

  1. 不能创建对象
  2. 不能抛出或者捕获异常
  3. 不能用synchronized关键字
  4. 不能对目标程序中的instace或者static变量
  5. 不能调用目标程序的instance或者static方法
  6. 脚本的field、method都必须是static的
  7. 脚本不能包括outer,inner,nested class
  8. 脚本中不能有循环,不能继承任何类,任何接口与assert语句
重要概念

方法上的注解 

  • @ OnMethod  用来指定trace的目标类和方法以及具体位置, 被注解的方法在匹配的方法执行到指定的位置会被调用。
    1、"clazz"属性用来指定目标类名, 可以指定全限定类名, 比如"java.awt.Component", 也可以是正则表达式(表达式必须写在"//"中, 比如"/java\\.awt\\..+/")。
    2、"method"属性用来指定被trace的方法. 表达式可以参考自带的例子(NewComponent.java 和Classload.java, 关于方法的注解可以参考MultiClass.java)。
    3、有时候被trace的类和方法可能也使用了注解. 用法参考自带例子WebServiceTracker.java。
    4、针对注解也是可以使用正则表达式, 比如像这个"@/com\\.acme\\..+/ ",也可以通过指定超类来匹配多个类, 比如"+java.lang.Runnable"可以匹配所有实现了java.lang.Runnable接口的类. 具体参考自带例子SubtypeTracer.java。
  • @OnTimer定时触发Trace,时间可以指定,单位为毫秒,具体参考自带例子 Histogram.java。
  • @OnError 当trace代码抛异常或者错误时,该注解的方法会被执行. 如果同一个trace脚本中其他方法抛异常, 该注解方法也会被执行。
  • @OnExit 当trace方法调用内置exit(int)方法(用来结束整个trace程序)时, 该注解的方法会被执行. 参考自带例子ProbeExit.java。
  • @OnEvent 用来截获"外部"btrace client触发的事件, 比如按Ctrl-C 中断btrace执行时,并且选择2,或者输入事件名称,将执行使用了该注解的方法, 该注解的value值为具体事件名称。具体参考例子HistoOnEvent.java
  • @OnLowMemory 当内存超过某个设定值将触发该注解的方法, 具体参考MemAlerter.java
  • @OnProbe 使用外部文件XML来定义trace方法以及具体的位置,具体参考示例SocketTracker1.java和java.net.socket.xml。

参数上的注解 

  • @Self 用来指定被trace方法的this,可参考例子AWTEventTracer.java 和 AllCalls1.java
  • @Return 用来指定被trace方法的返回值,可参考例子Classload.java
  • @ProbeClassName (since 1.1) 用来指定被trace的类名, 可参考例子AllMethods.java
  • @ProbeMethodName (since 1.1) 用来指定被trace的方法名, 可参考例子WebServiceTracker.java。
  • @TargetInstance (since 1.1) 用来指定被trace方法内部被调用到的实例, 可参考例子AllCalls2.java
  • @TargetMethodOrField (since 1.1) 用来指定被trace方法内部被调用的方法名, 可参考例子AllCalls1.java 和AllCalls2.java。

非注解的方法参数 

未使用注解的方法参数一般都是用来做方法签名匹配用的, 他们一般和被trace方法中参数出现的顺序一致. 不过他们也可以与注解方法交错使用, 如果一个参数类型声明为*AnyType[]*, 则表明它按顺序"通吃"方法所有参数. 未注解方法需要与*Location*结合使用: 

  • Kind.ENTRY-被trace方法参数
  • Kind.RETURN- 被trace方法返回值
  • Kind.THROW - 抛异常
  • Kind.ARRAY_SET, Kind.ARRAY_GET - 数组索引
  • Kind.CATCH - 捕获异常
  • Kind.FIELD_SET - 属性值
  • Kind.LINE - 行号
  • Kind.NEW - 类名
  • Kind.ERROR - 抛异常

属性上的注解 

  • @Export 该注解的静态属性主要用来与jvmstat计数器做关联, 使用该注解之后,btrace程序就可以向jvmstat客户端(可以用来统计jvm堆中的内存使用量)暴露trace程序的执行次数, 具体可参考例子ThreadCounter.java。
  • @Property 使用了该注解的trace脚本将作为MBean的一个属性,一旦使用该注解, trace脚本就会创建一个MBean并向MBean服务器注册, 这样JMX客户端比如VisualVM, jconsole就可以看到这些BTrace MBean, 如果这些被注解的属性与被trace程序的属性关联, 那么就可以通过VisualVM 和jconsole来查看这些属性了, 具体可参考例子ThreadCounterBean.java 和HistogramBean.java。
  • @TLS 用来将一个脚本变量与一个ThreadLocal变量关联, 因为ThreadLocal变量是跟线程相关的, 一般用来检查在同一个线程调用中是否执行到了被trace的方法, 具体可参考例子OnThrow.java 和 WebServiceTracker.java。

类上的注解 

  • @com.sun.btrace.annotations.DTrace 用来指定btrace脚本与内置在其脚本中的D语言脚本关联, 具体参考例子DTraceInline.java。
  • @com.sun.btrace.annotations.DTraceRef 用来指定btrace脚本与另一个D语言脚本文件关联, 具体参考例子DTraceRefDemo.java。
  • @com.sun.btrace.annotations.BTrace 用来指定该java类为一个btrace脚本文件。
Samples

1.跟踪WildFly内存信息,用@OnTimer 这个annotation每隔4秒钟打印一次内存堆栈信息:

import com.sun.btrace.annotations.BTrace;

import com.sun.btrace.annotations.OnTimer;

import static com.sun.btrace.BTraceUtils.*;

@BTrace

public class TraceMemory {

        //heapUsage()/nonHeapUsage() – 打印堆/非堆内存信息,包括init、used、commit、max

        @OnTimer(4000)

        public static void printM(){

                //打印内存信息

                println("heap:");

                println(heapUsage());

                println("no-heap:");

                println(nonHeapUsage());

        }

}

查找 WildFly pid:

[root@mdwareweb1 test]# jps

7119 Jps

5513 jboss-modules.jar

执行:

 btrace 5513 TraceMemory.java

结果:

[root@mdwareweb1 test]# btrace 5513 TraceMemory.java
heap:
init = 67108864(65536K) used = 70036960(68395K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43569672(42548K) committed = 78348288(76512K) max = 318767104(311296K)
heap:
init = 67108864(65536K) used = 70893560(69231K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43569672(42548K) committed = 78348288(76512K) max = 318767104(311296K)
heap:
init = 67108864(65536K) used = 70893560(69231K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43573768(42552K) committed = 78348288(76512K) max = 318767104(311296K)

2、打印WildFly运行时系统信息:

import static com.sun.btrace.BTraceUtils.*;

import com.sun.btrace.annotations.BTrace;

@BTrace

public class TraceJInfo {

         static{

                println("java vm properties:===>");

                printVmArguments();

                println("System properties:===>");

                printProperties();

                println("OS properties:===>");

                printEnv();

                exit();

        }

}

执行结果:

[root@mdwareweb1 test]# btrace 5513 TraceJInfo.java
java vm properties:===>
[-D[Standalone], -Xms64m, -Xmx512m, -XX:MaxPermSize=256m, -Djava.net.preferIPv4Stack=true, -Djboss.modules.system.pkgs=org.jboss.byteman, -Djava.awt.headless=true, -Dorg.jboss.boot.log.file=/opt/jboss/wildfly-8.0.0.Final/standalone/log/server.log, -Dlogging.configuration=file:/opt/jboss/wildfly-8.0.0.Final/standalone/configuration/logging.properties]
System properties:===>
org.jboss.security.context.ThreadLocal = true
java.vm.version = 23.25-b01
sun.jnu.encoding = UTF-8
java.vendor.url = http://java.oracle.com/
java.vm.info = mixed mode
org.apache.xml.security.ignoreLineBreaks = true
jboss.server.name = mdwareweb1
java.awt.headless = true
user.dir = /opt/jboss/wildfly-8.0.0.Final/bin
sun.cpu.isalist =
logging.configuration = file:/opt/jboss/wildfly-8.0.0.Final/standalone/configuration/logging.properties

......

3、排查ClassNotFoundException

写过Java代码的读者估计都碰到过ClassNotFoundException/NoClassDefFoundError/NoSuchMethodException(还有一个常见的ClassCastException就不在这里说了)。当碰到ClassNotFoundException/NoClassDefFound时,如果很确定这个class应该是从哪个路径装载的,则可以去相应的路径找下是否有对应的class文件存在,例如web应用通常会在*.war(ear)/WEB-INF/lib或classes目录下,对于lib下的jar包,可通过写个小脚本jar -tvf的方式找找;如不确定class是从哪装载的,则可以先看看日志里是否有堆栈信息,如果有的话则可以看到具体是哪个ClassLoader实现在装载class,之后则可以通过www.grepcode.com或jar包反编译看看具体是从哪装载的class;如果日志中没有,则可以用btrace来跟踪下抛出以上两个异常的堆栈信息,btrace脚本类似如下:

import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
    @OnMethod(
        clazz="java.lang.ClassNotFoundException",
        method="<init>"
    )
    public static void traceExecute(){
         jstack(); 
    }
}

拿到堆栈信息后,可以继续使用上面的方法进行排查,在确认了class装载的位置后,则可将相应的class/jar加上即可。

4、排查OutOfMemoryError

尽管JVM是自动管理内存的分配和回收的,但Java程序员们还是会经常碰到各种各样的内存问题。最常见的第一个问题是java.lang.OutOfMemoryError,估计写Java的读者都碰到过。在日志中可能会看到java.lang.OutOfMemoryError: Unable to create new native thread,可以先统计下目前的线程数(例如ps -eLf | grep java -c),然后可以看看ulimit -u的限制值是多少,如线程数已经达到限制值,如限制值可调整,则可通过调整限制值来解决;如不能调限制值,或者创建的线程已经很多了,那就需要看看线程都是哪里创建出来的,同样可通过btrace来查出是哪里创建的,脚本类似如下:

import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    )
    public static void traceExecute(){
         jstack(); 
    }
}
在找到是哪里创建造成了后,之后就可以想办法解决了,例如这种情况下常见的有可能是用了Executors.newCachedThreadPool这种来创建了一个没限制大小的线程池。
综合实例

以下是笔者编写的一个demo,主要用于测试OOM,当然也会对BTrace的功能进行介绍。

首先,笔者编写了两个用于测试的类,如下:

StartObject.java

import java.util.Random;

public class StartObject {

    private static int totalTime = 0;

    public int work(int sleepTime) throws InterruptedException{

        System.out.println("sleep " + sleepTime);

        totalTime += sleepTime;

        return totalTime;

    }

}

HeapOOM.java

import java.util.ArrayList;

import java.util.List;

import java.util.Random;

public class HeapOOM {

      public static void main(String agrs[]) throws InterruptedException{

          Random random = new Random();

              List<Integer> list = new ArrayList<Integer>();

          Thread.sleep(10000);

          while(true){  

          int sleepTime = random.nextInt(1000);

list.add(new StartObject().work(sleepTime));

      }  

      }  

}

由于笔者将运行时jvm参数设置为:-Xms10m -Xmx10m

因此程序会因为HeapOOM.java中红色一行代码不断创建对象而发生OutOfMemoryError异常。

以下,我们便通过StartObject这个类来查找是在哪不断新建对象并调用其work方法,于是编写BTrace脚本如下:

import static com.sun.btrace.BTraceUtils.*;  

import com.sun.btrace.annotations.*;  

@BTrace

public class TraceObject {

     @OnMethod(clazz = "StartObject", method = "work", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))  

        public static void checkWhoCallMe() {  

            println("check who ActionObject.work method:");  

            jstack();  

        }  

}

通过jps查找到HeapOOM执行的pid

执行:btrace [pid] TraceObject.java

在终端我们看到HeapOOM类中执行StratObject.work方法,如此即可找到我们想要的执行对象,从而可以去修改。

若是得到work方法的执行时间的话,脚本如下:

import static com.sun.btrace.BTraceUtils.*;

import com.sun.btrace.annotations.*;

@BTrace

public class TraceObjectCost {

    @TLS

    static long startTime;

    @OnMethod(clazz = "StartObject", method = "work", location = @Location(Kind.RETURN))

    public static void start() {

         startTime = timeMillis();

    }

 @OnMethod(clazz = "StartObject", method = "work", location = @Location(Kind.RETURN))

    public static void getMethodExecuteCost(int sleepTime,@Return int totalTime) {

        String str = str(timeMillis() - startTime);

        String strcat = strcat("execute work method cost:", str);

        String strcat2 = strcat(strcat, " ms");

        println(strcat2);

    }

}

若是期望的到一个method哪几行被执行了的话,脚本如下:

import static com.sun.btrace.BTraceUtils.*;

import com.sun.btrace.annotations.*;

@BTrace

public class TraceObjectLineCall {

    @OnMethod(clazz="StartObject",method="work",location=@Location(value=Kind.CALL, clazz="/.*/", method="/.*/"))

    public static void lineCall(@Self StartObject self, @TargetMethodOrField String method, @ProbeMethodName String probeMethod){

        println(Strings.strcat(method, Strings.strcat(" in ", probeMethod)));

    }

}

个人见解

虽然 Btrace 在关键时候能起到迅速排查问题的作用,但我个人感觉,这还是不到万不得已才使用的好。首先,我们代码上线前,应该充分 review ,充分和相关方进行沟通,以避免不必要的问题发生。其次,我们应该养成记 log 的良好习惯。遇到问题,如果有相关日志可以排查,是最方便的,同时也是最安全,成本最低的一种排查方法。最后,我们可以结合 btrace 和 jdk 自带的 tool 来排查问题,比如 jstack 、 jstat 等等,快速的定位问题。

 

参考资料

http://kenai.com/projects/btrace/pages/UserGuide

http://learnworld.iteye.com/blog/1402763

http://inter12.iteye.com/blog/1759882

http://www.cnblogs.com/serendipity/archive/2012/05/14/2499840.html


附录

相关实例说明

AWTEventTracer.java - 演示了对EventQueue.dispatchEvent()事件进行trace的做法, 可以通过instanceof来对事件进行过滤, 比如这里只针对focus事件trace.

AllLines.java - 演示了如何在被trace的程序到达probe指定的类和指定的行号时执行指定的操作(例子中指定的行号是-1表示任意行).

AllSync.java - 演示了如何在进入/退出同步块进行trace.

ArgArray.java - 演示了打印java.io包下所有类的readXXX方法的输入参数.

Classload.java - 演示打印成功加载指定类以及堆栈信息.

CommandArg.java - 演示如何获取btrace命令行参数.

Deadlock.java - 演示了@OnTimer注解和内置deadlock()方法的用法

DTraceInline.java - 演示@DTrace注解的用法

DTraceDemoRef.java - 演示@DTraceRef 注解的用法.

FileTracker.java - 演示了如何对File{Input/Output}Stream构造函数中初始化打开文件的读写文件操作进行trace.

FinalizeTracker.java - 演示了如何打印一个类所有的属性, 这个在调试和故障分析中非常有用. 这里的例子是打印FileInputStream类的close() /finalize() 方法被调用时的信息.

Histogram.java - 演示了统计javax.swing.JComponent在一个应用中被创建了多少次.

HistogramBean.java - 同上例, 只不过演示了如何与JMX集成, 这里的map属性通过使用@Property注解被暴露成一个MBean.

HistoOnEvent.java - 同上例, 只不过演示了如何在通过按ctrl+c中断当前脚本时打印出创建次数, 而不是定时打印.

JdbcQueries.java - 演示了聚合(aggregation)功能. 关于聚合功能可参考DTrace.

JInfo.java - 演示了内置方法printVmArguments(), printProperties() 和printEnv() 的用法

JMap.java - 演示了内置方法dumpHeap()的用法. 即将目标应用的堆信息以二进制的形式dump出来

JStack.java - 演示了内置方法jstackAll()的用法, 即打印所有线程的堆栈信息.

LogTracer.java - 演示了如何深入实例方法(Logger.log)并调用内置方法(field() )打印私有属性内容.

MemAlerter.java - 演示了使用@OnLowMememory 注解监控内存使用情况. 即堆内存中的年老代达到指定值时打印出内存信息.

Memory.java - 演示每隔4s打印一次内存统计信息.

MultiClass.java - 演示了通过使用正则表达式对多个类的多个方法进行trace.

NewComponent.java - 使用计数器每隔一段时间检查当前应用中创建java.awt.Component的个数.

OnThrow.java - 当抛出异常时, 打印出异常堆栈信息.

ProbeExit.java - 演示@OnExit注解和内置exit(int)方法的用法

Profiling.java - 演示了对profile的支持.  // 我执行没成功, BTrace内部有异常

Sizeof.java - 演示了内置的sizeof方法的使用.

SocketTracker.java - 演示了对socket的creation/bind方法的trace.

SocketTracker1.java - 同上, 只不过使用了@OnProbe.

SysProp.java - 演示了使用内置方法获取系统属性, 这里是对 java.lang.System的getProperty方法进行trace.

SubtypeTracer.java - 演示了如何对指定超类的所有子类的指定方法进行trace.

ThreadCounter.java - 演示了在脚本中如何使用jvmstat 计数器. (jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count 需要这样来从外部通过jstat来访问)

ThreadCounterBean.java - 同上, 只不过使用了JMX.

ThreadBean.java - 演示了对预编译器的使用(并结合了JMX).

ThreadStart.java - 演示了脚本中DTrace的用法.

Timers.java - 演示了在一个脚本中同时使用多个@OnTimer

URLTracker.java - 演示了在每次URL.openConnection成功返回时打印出url. 这里也使用了D语言脚本.

WebServiceTracker.java - 演示了如何根据注解进行trace.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值