【JAVA】死锁排查与Tomcat深度调优

一、案例故障描述

1.1 具体现象

这是不久前的一个客户案例,客户的一个门户网站系统是基于java开发的,运行多年,一直正常,而最近经常罢工,频繁出现java进程占用CPU资源很高的情况,在CPU资源占用很高的时候,web系统响应缓慢,下图是某时刻服务器的一个状态截图:

下面是htop获取的状态信息:

从图中可以看出,java进程占用CPU资源达到300%以上,而每个CPU核资源占用也比较高,都在30%左右,让客户的运维人员检查后,也没发现什么异常,于是就把问题抛给了程序方面,而研发查看了代码,也没发现什么异常情况,最后又推给运维了,说是系统或者网络问题,研发说正常情况下java进程占用CPU不会超过100%,而这个系统达到了300%多,肯定是系统有问题,然而运维也无计可施了,最后,急中生智,重启了系统,java进程占用的CPU资源一下子就下来了。

就这样,重启成了运维解决问题的唯一办法。

然而,这个重启的办法,用了几天就不行了,今天,又例行重启了系统,但是重启后,web系统仅能正常维持30分钟左右,接着,彻底无法访问了,现象还是java占用CPU大量资源。

1.2 问题分析

从这个案例现象来看,问题应该出在程序方面,原因如下:

  1. java进程占用资源过高,可能是在做GC,也可能是内部程序出现死锁,这个需要进一步排查。
  2. 网站故障的时候,访问量并不高,也没有其他并发请求,攻击也排除了(内网应用),所以肯定不是系统资源不足导致的。
  3. java占用CPU资源超过100%完全可能的,因为java是支持多线程的,每个内核都在工作,而java进程持续占用大量CPU资源就不正常了,具体原因要进一步排查。
  4. 检查发现,web系统使用的是tomcat服务器,并且tomcat配置未作任何优化,因此这个需要配置一些优化参数。

针对上面四个原因,那么下面就具体分析下,如何对这个Web系统进程故障分析和调优。

二、java中进程与线程的概念

  • 要了解java占用CPU资源超过100%的情况,就需要知道进程和线程的概念和关系。
  • 进程是程序的一次动态执行,它对应从代码加载,执行至完毕的一个完整的过程,是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤销。
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

进程和线程的关系
1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
2. 进程作为资源分配的最小单位,资源是分配给进程的,同一进程的所有线程共享该进程的所有资源。
3. 真正在处理机上运行的是线程。

进程与线程的区别:

  1. 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  2. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
  3. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
  4. 系统开销:在创建或撤销进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤销线程时的开销。

Tomcat底层是通过JVM运行的,JVM在操作系统中是作为一个进程存在的,而java中的所有线程在JVM进程中,但是CPU调度的是进程中的线程。因此,java是支持多线程的,体现在操作系统中,就是java进程可以使用CPU的多核资源。那么java进程占用CPU资源300%以上是完全可能的。

三、排查java进程占用CPU过高的思路

下面重点来了,如何有效的去排查java进程占用CPU过高呢,下面给出具体的操作思路和方法。

3.1 提取占用CPU过高的进程

提取占用CPU高进程的方法很多,常用的方法有如下两个:

方法一:使用top或htop命令查找到占用CPU高得进程的pid

  1. top -d 1

方法二:使用ps查找到tomcat运行的进程

  1. ps -ef | grep tomcat

3.2 定位有问题的线程的pid

在Linux中,程序中创建的线程(也称为轻量级进程,LWP)会具有和程序的PID相同的"线程组ID"。同时,各个线程会获得其自身的线程ID(TID)。对于Linux内核调度器而言,线程不过是恰好共享特定资源的标准的进程而已。

那么如何查看进行对应的线程信息呢,方法很多,常用的命令有ps,top和htop命令

3.2.1 ps命令查看进程的线程信息

在ps命令中,"-T"选项可以开启线程查看。下面的命令列出了由进程号为的进程创建的所有线程。

  1. ps -T -p <pid>

例如:

  1. [root@Jenkins ~]# jps
  2. 25394 jenkins.war
  3. 29503 Jps
  4. [root@Jenkins ~]# ps -T -p 25394
  5. PID SPID TTY TIME CMD
  6. 25394 25394 ? 00:00:00 java
  7. 25394 26930 ? 00:00:00 java
  8. 25394 26931 ? 00:00:03 java
  9. 25394 26932 ? 00:00:00 java
  10. 25394 26933 ? 00:00:00 java
  11. 25394 26934 ? 00:00:00 java
  12. 25394 26935 ? 00:00:18 java
  13. 25394 26936 ? 00:00:04 java
  14. 25394 26937 ? 00:00:00 java
  15. 25394 26938 ? 00:00:11 java
  16. 25394 26939 ? 00:00:00 java
  17. 25394 26940 ? 00:00:00 java
  18. 25394 26941 ? 00:00:00 java
  19. 25394 26943 ? 00:00:00 java

在输出中,"SPID"栏表示线程ID,而"CMD"栏则显示了线程名称。
使用ps命令的一个缺点是,无法动态查看每个线程消耗资源的情况,仅仅能看线程id信息,所以我们更多使用的是top和htop

3.2.2 top命令获取线程信息

top命令可以实时显示各个线程情况。要在top输出中开启线程查看,可调用top命令的"-H"选项,该选项会列出所有Linux线程。看下图:

要让top输出某个特定进程并检查该进程内的线程情况,可执行如下命令

top -H -p <pid>

  1. [root@Jenkins ~]# top -H -p 25394 -d 5
  2. top - 16:56:43 up 1 day, 7:36, 3 users, load average: 0.00, 0.01, 0.05
  3. Threads: 61 total, 0 running, 61 sleeping, 0 stopped, 0 zombie
  4. %Cpu(s): 0.2 us, 0.2 sy, 0.0 ni, 99.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. KiB Mem : 997948 total, 113388 free, 469936 used, 414624 buff/cache
  6. KiB Swap: 2097148 total, 2073740 free, 23408 used. 334440 avail Mem
  7. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  8. 25394 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.05 java
  9. 26930 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.81 java
  10. 26931 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:03.43 java
  11. 26932 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.00 java
  12. 26933 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.01 java
  13. 26934 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.00 java
  14. 26935 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:18.80 java
  15. 26936 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:04.47 java
  16. 26937 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.00 java
  17. 26938 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:12.45 java
  18. 26939 jenkins 20 0 2473868 414404 26680 S 0.0 41.5 0:00.00 java

可以看到,每个线程状态是实时刷新的,这样我们就可以观察,哪个线程消耗CPU资源最多,然后把它的SPID(TID)记录下来。

3.2.3 htop命令获取线程信息

通过htop命令查看单个进程的线程信息,更加简单和友好,此命令可以在树状视图中监控单个独立线程。
要在htop中启用线程查看,可先执行htop,然后按F2键来进入htop的设置菜单。选择"设置"栏下面的"显示选项",然后开启"树状视图"和"显示自定义线程名"选项。最后按F10键退出设置。如下图所示:

很简单的把,这样就可以清楚地看到单个进程的线程视图了,并且状态信息也是实时刷新的。通过这个方法也可以查找出CPU利用率最厉害的线程号。然后记录下来。

3.3 将线程的pid转换为16进制数

printf '%x\n' pid

注意,此处的SPID(TID)为上一步找到的占CPU高的线程号

3.4 使用jstack工具将进程信息打印输出

  • jstack是java虚拟机自带的一种堆栈跟踪工具。可以用于生成java虚拟机当前时刻的线程快照。
  • 线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
    • 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

总结一句话:jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。

想要通过jstack命令来分析线程的情况的话,首先要知道线程都有哪些状态

  • 线程堆栈信息可能会看到的线程的几种状态
    • NEW:未启动的。不会出现在Dump中
    • RUNNABLE:在虚拟机内执行的。运行状态,可能里面还能看到locked字样,表明它获得了某把锁
    • BLOCKED:受阻塞并等待监视器锁。被某个锁(synchronizers)给block住了。
    • WAITING:无限期等待另一个线程执行特定操作。等待某个condition或monitor发生,一般停留在park(),wait(),sleep(),join()等语句里。
    • TIMED_WATING:有时限的等待另一个线程的特定操作。和WAITING的区别是wait()等语句加上了时间限制wait(timeout)
    • TERMINATED:已退出的

那么怎么去使用jstack呢,很简单,用jstack打印线程信息,将信息重定向到文件中,可执行如下操作:

jstack pid | grep spid(tid)

例如:

  1. [root@localhost ~]# jstack 2020 | grep -A 15 "7e5"
  2. "main" #1 prio=5 os_prio=0 tid=0x00007f7788009000 nid=0x7e5 runnable [0x00007f778f291000]
  3. java.lang.Thread.State: RUNNABLE
  4. at java.net.PlainSocketImpl.socketAccept(Native Method)
  5. at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
  6. at java.net.ServerSocket.implAccept(ServerSocket.java:545)
  7. at java.net.ServerSocket.accept(ServerSocket.java:513)
  8. at org.apache.catalina.core.StandardServer.await(StandardServer.java:446)
  9. at org.apache.catalina.startup.Catalina.await(Catalina.java:713)
  10. at org.apache.catalina.startup.Catalina.start(Catalina.java:659)
  11. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  12. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  13. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  14. at java.lang.reflect.Method.invoke(Method.java:497)
  15. at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:351)
  16. at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:485)

这里面重点关注下输出的线程状态,如果有异常,会输出相关异常信息或者跟程序相关的信息,将这些信息给开发人员,就可以马上定位CPU过高的问题,所以这个方法非常有效。

3.5 根据输出信息进行具体分析

学会了怎么使用jstack命令之后,我们就可以看看,如何使用jstack分析死锁,这也是我们一定要掌握的内容。什么是死锁?所谓死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外界因素作用,它们都将无法执行下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

最后,我们使用jstack来看一下线程堆栈信息:

  1. Found one Java-level deadlock:
  2. =============================
  3. "Thread-1":
  4. waiting to lock monitor 0x00007f0134003ae8 (object 0x00000007d6ab2c98, a java.lang.Object),
  5. which is held by "Thread-0"
  6. "Thread-0":
  7. waiting to lock monitor 0x00007f0134006168 (object 0x00000007d6ab2ca8, a java.lang.Object),
  8. which is held by "Thread-1"
  9. Java stack information for the threads listed above:
  10. ===================================================
  11. "Thread-1":
  12. at javaCommand.DeadLockclass.run(JStackDemo.java:40)
  13. - waiting to lock <0x00000007d6ab2c98>; (a java.lang.Object)
  14. - locked <0x00000007d6ab2ca8>; (a java.lang.Object)
  15. at java.lang.Thread.run(Thread.java:745)
  16. "Thread-0":
  17. at javaCommand.DeadLockclass.run(JStackDemo.java:27)
  18. - waiting to lock <0x00000007d6ab2ca8> (a java.lang.Object)
  19. - locked <0x00000007d6ab2c98> (a java.lang.Object)
  20. at java.lang.Thread.run(Thread.java:745)
  21. Found 1 deadlock.

这个结果显示的很详细了,它告诉我们“Found one Java-level deadlock”,然后指出造成死锁的两个线程的内容。接着又通过“Java stack information for the threads listed above”来显示更详细的死锁的信息,具体内容解读如下:

  1. Thread-1在想要执行文件名为JStackDemo的第40行代码的时候;
  2. 因为需要等待资源<0x00000007d6ab2c98>;
  3. 所以锁住了资源<0x00000007d6ab2ca8>。
  4. Thread-0在想要执行文件名为JStackDemo的第27行代码的时候;
  5. 因为它在等待资源<0x00000007d6ab2ca8>;
  6. 所以锁住了资源<0x00000007d6ab2c98>。
  7. 由于这两个线程都持有资源,并且都需要对方的资源,所以造成了死锁。 原因我们找到了,就可以具体问题具体分析,解决这个死锁了。

四、tomcat配置调优

在实际工作中接触过很多线上基于java的tomcat应用案例,多多少少都会出现一些性能问题,在追查原因的时候发现,tomcat的配置都是默认的,没有经过任何修改和调优,这肯定会出现性能问题了,在tomcat默认配置中,很多参数都设置的很低,尤其是内存和线程的配置,这些默认配置在web没有大量业务请求时,不会出现问题,而一旦业务量增长,很容易成为性能瓶颈。

  • tomcat的调优,主要是从三个方面进行:
    • 内存
    • 并发
    • 缓存

4.1 内存调优

这个主要是配置tomcat对JVM参数的设置,我们可以在tomcat的启动脚本catalina.sh中设置java_OPTS参数。

  • JAVA_OPTS参数常用的有如下几个:
    • -server:表示启用jdk的server运行模式
    • Xms:设置JVM初始堆内存的大小
    • Xmx:设置JVM最大堆内存的大小
    • XX:PermSize:设置堆内存持久代初始值大小
    • XX:MaxPermSize:设置持久代最大值
    • Xmn1g:设置堆内存年轻代大小

JVM的大小设置,跟服务器的物理内存有直接关系,不能太小,也不能太大,如果服务器内存为32G,可以采用以下配置:

  1. JAVA_OPTS='-server -Xms8192m -Xmx8192m -XX:MaxNewSize=256m -XX:PermSize=256m -XX:MaxPermSize=512m'

对于堆内存大小的设置有如下经验:

  1. 将最小堆大小(Xms)和最大堆大小(Xmx)设置为彼此相等;
  2. 堆内存不能设置过大,虽然堆内存越大,JVM可用的内存就越多。
  3. 但请注意,太多的堆内存可能会使垃圾收集长时间处于暂停状态。
  4. 将Xmx设置为不超过物理内存的50%,最大不超过32GB

4.2 Tomcat并发优化与缓存优化

这部分主要是对Tomcat配置文件server.xml内的参数进行的优化和配置。默认的server.xml文件一些性能参数配置很低,无法达到tomcat最高性能,因此需要有针对性的修改一下,常用的tomcat优化参数如下:

  1. <Connector port="8080"
  2.   protocol="HTTP/1.1"
  3.   maxHttpHeaderSize="8192" #最大的http请求头部大小8K
  4.   maxThreads="1000" #处理用户请求的最大线程数
  5.   minSpareThreads="100" #最少保留100个线程处于空闲状态
  6.   maxSpareThreads="1000" #最大保留1000个线程处于空闲状态
  7.   minProcessors="100" #Tomcat4+版本中,此参数失效
  8.   maxProcessors="1000" #Tomcat4+版本中,此参数失效
  9.   enableLookups="false" #禁止DNS反向解析
  10.   compression="on" #打开文件传输的压缩功能
  11.   compressionMinSize="2048" #当数据大于2KB才会进行压缩
  12.   compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain" #待压缩的文件类型
  13.   connectionTimeout="20000" #连接超时时间2万毫秒(20秒)
  14.   URIEncoding="utf-8" #URL统一编码格式支持中文
  15.   acceptCount="1000" #监听端口的等待队列最大数,(等待1000+maxThreads1000,最大并发2000),超过此数字服务器会直接拒绝此次请求,返回connection refused
  16.   redirectPort="8443" #用于基于安全通道的场景,把用户请求转发到SSL的redirectPort端口
  17.   disableUploadTimeout="true"/> #上传时是否启动超时机制

五、Tomcat Connector三种运行模式(BIO,NIO,APR)比较与优化

5.1 什么是BIO,NIO,APR

  • BIO(blocking I/O):即阻塞式I/O操作的java API;
    • Tomcat7以下版本默认是BIO模式,由于每个请求都要创建一个线程来处理,线程开销较大,不能处理高并发的场景,在三种模式中性能最低
    • 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,因此,当并发量高时,线程数会较多,造成资源浪费。
  • NIO(non-blocking I/O):基于缓冲区,并能提供非阻塞I/O操作的java API;
    • Tomcat8及以上默认是NIO模式,比BIO拥有更好的并发运行性能。
    • 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  • AIO(Apache Portable Runtime I/O):Apache可移植运行时I/O,是Apache HTTP服务器的支持库
    • Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能。
    • Tomcat APR也是tomcat上运行高并发应用的首选模式
    • 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,可以看出,AIO是从操作系统级别来解决异步I/O问题,因此可以大幅度提高性能。

从这个运行模式中,我们基本可以看出,三种模式的优劣了,下面总结三种模式的特点和使用环境

  1. BIO:已经淘汰,JDK1.4以前的唯一选择
  2. NIO:适用于连接数目多且连接比较短的架构,比如消息通信服务器,并发局限于应用中,编程比较复杂,JDK1.4之后支持
  3. AIO:用于连接数目多且长连接的架构中,比如直播服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持

5.2 tomcat中如何使用BIO,NIO,APR模式

这里以tomcat8.5.29为例进行介绍,在tomcat8版本中,默认使用的就是模式,也就是无需做任何配置,Tomcat启动的时候,可以通过catalina.out文件看到Connector使用的是哪一种运行模式,默认情况下会输出如下日志信息:

  1. 15-Nov-2018 23:13:56.737 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
  2. 15-Nov-2018 23:13:56.762 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["ajp-nio-8009"]
  3. 15-Nov-2018 23:13:56.764 INFO [main] org.apache.catalina.startup.Catalina.load Initialization processed in 715 ms

这个日志表面目前tomcat使用的是NIO模式

要让tomcat运行在APR默认的话,首先需要安装apr,apr-utils,tomcat-native等依赖包,简单安装过程如下:

  1. #安装apr与apr-util
  2. #从Download - The Apache Portable Runtime Project 下载apr和apr-utils
  3. [root@lampserver app]# tar xf apr-1.6.3.tar.gz
  4. [root@lampserver app]# cd apr-1.6.3
  5. [root@lampserver apr-1.6.3]# ./configure --prefix=/usr/local/apr
  6. [root@lampserver apr-1.6.3]# make && make install
  7. [root@lampserver /]#yum -y install expat expat-devel
  8. [root@lampserver app]# tar xf apr-util-1.6.1.tar.gz
  9. [root@lampserver app]# cd apr-util-1.6.1
  10. [root@lampserver apr-util-1.6.1]# ./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr
  11. [root@lampserver apr-util-1.6.1]# make && make install
  12. #安装tomcat-native
  13. #从Apache Tomcat® - Tomcat Native Downloads 下载tomcat-native
  14. [root@localhost ~]# tar xf tomcat-native-1.2.18-src.tar.gz
  15. [root@localhost ~]# cd tomcat-native-1.2.18-src/native
  16. [root@localhost ~]# ./configure --with-apr=/usr/local/apr --with-java-home=/usr/local/java/
  17. [root@localhost ~]# make && make install
  18. #设置环境变量,将如下内容添加到/etc/profile文件中
  19. JAVA_HOME=/usr/local/java
  20. JAVA_BIN=$JAVA_HOME/bin
  21. PATH=$PATH:$JAVA_BIN
  22. CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
  23. export JAVA_HOME JAVA_BIN PATH CLASSPATH
  24. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib
  25. #执行source命令
  26. [root@localhost ~]#source /etc/profile
  27. #修改tomcat配置文件server.xml,找到tocmat默认的http的8080端口配置的Connector
  28. <Connector port="8080" protocol="HTTP/1.1"
  29. connectionTimeout="20000"
  30. redirectPort="8443" />
  31. #修改为:
  32. <Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
  33. connectionTimeout="20000"
  34. redirectPort="8443" />;
  35. #接着,修改tocmat默认的ajp的8009端口配置的Connector,找到如下内容:
  36. <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />;
  37. #修改为:
  38. <Connector port="8009" protocol="org.apache.coyote.ajp.AjpAprProtocol" redirectPort="8443" />;

最后,重启tomcat,使配置生效。Tomcat重启过程中,可以通过catalina.out文件看到Connector使用的是哪一种运行模式,如果能看到类似下面的输出,表示配置成功。

  1. 06-Nov-2018 14:03:31.048 信息 [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-apr-8000"]
  2. 06-Nov-2018 14:03:31.103 信息 [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-apr-8009"]

5.3 tomcat三种模式测试

通过测试,随着线程的不断增多,bio模式性能越来越差,就算是在本地,错误率和响应时间都在明显增加,而吞吐量样本数和每秒传输速率都在下降;
而Nio和Aio模式基本上没有变化太多,都保持在一个稳定的状态;
因此,生产环境tomcat到底要用什么模式,具体的服务器配置还需要进行具体的压力测试。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值