一文搞定 Perf 和 Gpertools

系统 Linux
在Linux下开发是幸福的,尤其是在发生问题的时候。永远忘不了在Windows下应用发生问题时那种无助的感觉。

[[390062]]

本文转载自微信公众号「小姐姐味道」,作者小姐姐养的狗。转载本文请联系小姐姐味道公众号。

在Linux下开发是幸福的,尤其是在发生问题的时候。永远忘不了在Windows下应用发生问题时那种无助的感觉。

Java提供了非常多的工具来应对故障排查、性能分析,比如jstat、jmap、jmc等。但大多数情况下,资源的瓶颈在操作系统上。如果宿主机并不能正常工作,那么在之上进行各种Java的应用分析就会变得没有意义。皮之不存毛将焉附,就是这个道理。

要做性能分析,Linux下有一个非常好用的工具,叫做perf。几乎每个发行版都有它的安装包。perf诞生于2009年,是一个内核级的工具;另外,还有一个工具叫做gperftools,是google的产品,是一个应用级别的产品。

虽然它们都有perf字样,但使用场景和处理的问题也是不一样的。

1. perf:CPU暴涨问题排查

顾名思义,perf是做性能分析用的。perf支持两种模式,计算模式和采样模式。比如,perf stat使用的是计算模式,而perf record采用的是采样模式。拿采样来说,它的原理是这样的:每隔一个固定的时间,产生一个中断,然后统计对应的pid和函数。采样就预示着与实际运行情况并不能保持一致,但如果一个函数运行的时间越长,被时钟中断的机会就越大。鉴于perf最终显示的是统计值,所以它的测量结果是高度可信的。

通过包管理工具可以很容易的获取perf。比如在centos下,直接通过yum install perf进行安装。perf提供了非常多的命令,我们可以直接输入perf输出这些选项。

Perf的功能非常多,常用的有perf list、perf stat、perf top、perf record、perf report等。下面以几个常见的例子,来说明它的应用场景。

使用下面的脚本,使得某一核CPU使用飙升到100%。

  1. cat /dev/zero > /dev/null 

使用下面脚本,耗光CPU资源。(先取得cpu的核数,然后循环生成任务)。这段脚本将数据输出到/dev/null,所以只占用CPU资源,没有占用任何I/O资源。

  1. for i in `seq 1 $(cat /proc/cpuinfo |grep "physical id" |wc -l)`; 
  2.  do dd if=/dev/zero of=/dev/null &  
  3. done 

我们就拿下面的这个脚本来说明情况。从top的截图中,可以看到sy和ni的占用,达到了100%,我的脚本起作用了!(温馨提示:执行完脚本后,如果你想要杀死这些进程,除了重启之外,还可以直接通过ps找到相应的进程,然后使用kill命令终止)。

接下来使用record指令来录制CPU的使用情况。从上面的描述可以得知,统计结果是采样结果。

  1. <># perf record -a -e cycles -o perf.perf -g sleep 10 
  2.  
  3. [ perf record: Woken up 55 times to write data ] 
  4. [ perf record: Captured and wrote 14.282 MB perf.perf (160302 samples) ] 

程序将会运行10秒钟,然后将采样结果输出到perf.perf文件中。通过report命令可以展示统计结果。

  1. perf report -i perf.perf  

可以看到大多数cpu的损耗都是在dd命令上,甚至里面的调用树,也能够清晰的展示。这在调试一些c++语言写的程序,或者调试jvm的一些内部行为时,非常的有用,因为它可以直接跟踪到系统调用层面。但有些细节,如果对Linux内核不是非常了解的话,下手就比较困难。所以通常情况下,我们只能通常粗略的定位到有问题的模块,然后再深入进行调试。

perf还可以通过指定进程号进行性能追踪,来获取性能数据。

  1. perf top -g -p 2343 

2. 示例代码

了解到perf的基本用法,我们拿一个经常实际遇到的例子来说明一下perf的使用。堆外内存是通过JNI等类库进行调用所产生的内存,在实际排查中定位非常困难。传统的工具包括JMC,都不能快速有效的找到问题的元凶。黔驴技穷的时候,一般就到了perf上场的时候了。

为了演示这个过程,我特意做了一段非常精致的JAVA代码。代码片段较长,可以访问下面的gist链接,下面只说明关键代码。这段代码是典型的堆外内存泄露问题。

  1. https://gist.github.com/lycying/70ff3897d8516011c7ffc702aa0d03c2 

使用com.sun.net.httpserver.HttpServer自带的简易server,可以非常容的构造一个服务器,我们可以通过请求去改变一些应用行为。

使用下面的JVM参数启动这段代码。

  1. java -Xmx1G -Xmn1G \ 
  2.  -XX:+AlwaysPreTouch  \ 
  3.  -XX:MaxMetaspaceSize=10M \ 
  4.  -XX:MaxDirectMemorySize=10M \ 
  5.  -XX:NativeMemoryTracking=detail LeakExample 

程序将人为创建一个停顿状态,具体测试步骤如下。

  1. 程序运行一小段时间,内存使用率迅速达到60%时,这时候程序将自动挂起
  2. 启动perf进行采样,相当于问题发生中进行切入 (perf record -g -p $pid)
  3. 访问http://localhost:8888 端口,将会将内存阈值提高到85%,内存会迅速达到这个状态
  4. 停止采样,将生成perf数据
  5. 使用perf report进行分析 (perf report -i perf.data )

这将会得到下面的一张图。

但我们不能从中得到有用的信息。是方法错了么?是的。采样内存,要使用perf mem record指令,但是这个指令在大多数机器上都不能工作,得到的信息也有限。

perf记录的是CPU的性能数据,这里要特别说明一下。只要是使用率上5%的,我一般都会关注。一般情况下,占用的cpu时间片多,证明使用内存也比较多。但事情总有例外的时候,比如频繁申请1byte的方法块,和一次性申请1MB的方法块,并不能同日而语。

所以perf能不能发现内存问题,要看运气。

3. gperftools:找到堆外内存的元凶

要找到内存问题,要使用google的gperftools,我们主要用到它的 Heap Profiler,功能很强大。https://github.com/gperftools/gperftools

它的启动方式有点特别,安装成功之后,你只需要输出两个环境变量即可。

  1. mkdir -p /opt/test  
  2. export LD_PRELOAD=/usr/lib64/libtcmalloc.so  
  3. export HEAPPROFILE=/opt/test/heap 

在同一个终端,再次启动我们的应用程序,可以看到内存申请动作都被记录到了 opt 目录下的 test 目录。

接下来,我们就可以使用 pprof 命令分析这些文件。

  1. cd /opt/test 
  2. pprof -text *heap  | head -n 200 

使用这个工具,能够一眼追踪到申请内存最多的函数。Java_java_util_zip_Inflater_init 这个函数立马就被发现了。

这就是我们模拟内存泄漏的整个过程,到此问题就解决了。

GZIPInputStream 使用 Inflater 申请堆外内存、Deflater 释放内存,调用 close() 方法来主动释放。如果忘记关闭,Inflater 对象的生命会延续到下一次 GC,有一点类似堆内的弱引用。在此过程中,堆外内存会一直增长。

问题发生在我们的decompress函数上。它在使用的时候,忘记关闭流了。我们可以看一下异常和正常情况的区别。

这段是忘了关闭流的函数。这种情况在编码中经常会发生。

  1. public static String decompress(byte[] input) throws Exception { 
  2.         ByteArrayOutputStream out = new ByteArrayOutputStream(); 
  3.         copy(new GZIPInputStream(new ByteArrayInputStream(input)), out); 
  4.         return new String(out.toByteArray()); 
  5.     } 

下面是修改后正常的函数。

  1. public static String decompress(byte[] input) throws Exception { 
  2.         ByteArrayOutputStream out = new ByteArrayOutputStream(); 
  3.         GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(input)); 
  4.         try { 
  5.             copy(gzip, out); 
  6.             return new String(out.toByteArray()); 
  7.         }finally { 
  8.             try{ gzip.close(); }catch (Exception ex){} 
  9.             try{ out.close(); }catch (Exception ex){} 
  10.         } 
  11.     } 

4. 题外话

使用pprof,还可以输出图形化的分析报告,需要安装图形生成工具graphviz,可以说是非常nice了。

另外不得不提的一点是,perf和gperftools对性能的影响,虽然不是特别大,但也尽量不要在线上环境使用它们。据我实际使用的经验判断,这个性能损耗率大概在30%左右。如果你的问题可以复现,通过常规手法又无法解决的情况下,可以使用这些工具去分析。比如你的应用实例有5个,完全可以分20%的流量到专用的机器上,把profile打开,相信你会很快定位到问题。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

责任编辑:武晓燕 来源: 小姐姐味道
相关推荐

2024-01-09 08:24:47

JMM核心线程

2021-08-13 05:50:01

ContainerdDockerKubernetes

2021-10-25 16:01:01

Linux设备树字符串

2019-09-23 10:51:14

JavaJava虚拟机Linux

2022-08-17 18:25:37

Java分布式搜索引擎

2021-08-31 07:02:20

Diff算法DOM

2020-03-03 17:47:07

UDP TCP面试题

2020-10-29 08:55:04

微服务

2021-10-06 20:23:08

Linux共享内存

2021-04-19 17:32:34

Java内存模型

2021-08-31 07:02:34

数据响应Vue侦测数据变化

2022-04-15 08:03:41

SaaS应用管理市场

2021-04-02 06:17:10

大数加减乘除数据结构算法

2021-08-31 06:37:35

Java 语言 Java 基础

2020-11-30 12:32:40

PyTorch语义分割python

2023-08-01 08:27:15

Java I/ONIO

2023-10-30 18:08:50

2024-01-29 09:04:15

Wi-Fi数据设备

2022-06-10 09:04:24

Python读取文件代码

2024-03-26 00:33:59

JVM内存对象
点赞
收藏

51CTO技术栈公众号