原文在简书上,

https://www.jianshu.com/p/f8336b835978

1.背景

目前有一个kafka消费者工程,此工程会消费kafka中的消息,并通过fastjson解析该消息为java实体,然后存入到阻塞队列 BlockingQueue中。另外有若干个线程会从queue中批量拿消息,然后以批量形式写入到 elasticsearch 中。目前在使用中发现存在性能瓶颈,需要定位是该工程对消息转化处理较慢,还是写es操作比较慢。

2.改造准备

为测试此工程最大处理速率,并排出写 es 的干扰,于是打算直接将最后写es这一步替换为写入到本地日志文件中,来看看其处理性能如何。

2.1 修改写消息到文件代码

1.将写es的逻辑改为直接写入到本地单独的日志文件

2.单独的日志logger 命名为 dummy.es.transport

3.使用计数器,每写入一条消息,就记录一次,然后通过定时线程每秒打印一次请求速率。

@Component
public class DummyEsTransportOpt {
    /**
     * 定义 dummy.es.transport 作为logger名,将所有收到的消息追加到指定的日志文件中去.
     */
    private static Logger esLogger = LoggerFactory.getLogger("dummy.es.transport");     private static Logger logger = LoggerFactory.getLogger(DummyEsTransportOpt.class);     private static ScheduledExecutorService schedule = Executors.newScheduledThreadPool(1);     /**
     * 消息插入计数器,记录处理消息数并计算每秒处理速度.
     */
    private static AtomicLong counter = new AtomicLong();     private static long initialCounter = 0L;     /**
     * 写 es 消息主要通过这个方法进行批量写,这里直接替换为记录到本地日志中。
     */
    public <T extends BaseElasticsearchEntity> boolean batchAddDocument(List<T> entityList) {         for (int i = 0; i < entityList.size(); i++) {
            //计数器,以判断速率.
            counter.incrementAndGet();
            T t = entityList.get(i);
            String json = JSON.toJSONString(t);
            if (t instanceof Annotation) {
                //esLogger 为专门的写消息的日志文件 appender.
                esLogger.info("A => " + json);
            } else {
                esLogger.info("S => " + json);
            }
        }
        return true;
    }
    /**
     * schedule 统计
     */
    static {
        schedule.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                long count = counter.get();
                logger.info("运行已处理消息数量: " + count + ",对比上一秒处理速率: " + (count - initialCounter) + "/s");
                initialCounter = count;
            }
        }, 0, 1, TimeUnit.SECONDS);
    }
}

2.2 日志输出配置

日志框架使用的是 log4j, 单独配置 dummyEsAppender, logger 名为 dummy.es.transport 的日志将会写入到 dummy-es-transport.log 这个文件中。


<appender name="dummyEsAppender" class="org.apache.log4j.DailyRollingFileAppender">     <param name="File" value="./logs/dummy-es-transport.log"/>     <param name="DatePattern" value="'.'yyyy-MM-dd'.log'"/>     <layout class="org.apache.log4j.PatternLayout">         <param name="ConversionPattern"                value="[%d{yyyy-MM-dd HH:mm:ss SSS\} %-5p] [%t] %c.%M() - %m%n"/>     </layout> </appender> <logger name="dummy.es.transport" additivity="false">     <level value="info"/>     <appender-ref ref="dummyEsAppender"/> </logger>

3.测试

3.1第一次测试

打包程序上线运行,观察定时线程每秒打印的速率,大概每秒处理写入的消息为 23000条/s。

有点疑惑这个处理速率是不是太低了一点。怀疑日志框架同步 appender 比较慢,于是将其改为异步。

3.2 第二次测试

日志输出同步转异步

在同步模式的基础上,增加异步的配置模式:

<logger name="dummy.es.transport" additivity="false">
    <level value="info"/>
    <appender-ref ref="async"/>
</logger> <appender name="async" class="org.apache.log4j.AsyncAppender">
    <param name="BufferSize" value="500"/>
    <appender-ref ref="dummyEsAppender"/>
</appender>

修改好之后,重新启动程序,观察处理速率。此时发现处理速度在 4万/s 左右。

将日志级别调整为 warn,即不输出日志,重新启动程序,再次观察。此时速度能够达到 70000/s

想不通为什么日志appender的性能消耗这么严重,正当不知道如下下手时,领导推荐使用 async-profiler 来定位时间到底耗在了哪里。

4. Async-profiler 和 火焰图分析

Async-profiler 可以观测运行程序,每一段代码所占用的cpu的时间和比例,从而可以分析并找到项目中占用cpu时间最长的代码片段,优化热点代码,达到优化内存的效果。

4.1采集cpu profile数据

我们将代码回退到第一次测试的情况,并启动程序,并找到当前进程号(627891),然后通过如下命令进行采集,并转换为火焰图格式 svg。

./profiler.sh -d 15 -i 50ms -o svg -e cpu 627891  > 627891.svg
  • -d N   分析持续时间(以秒为单位)。如果未提供启动,恢复,停止或状态选项,则探查器将运行指定的时间段,然后自动停止

  • -i N 设置分析间隔(以纳秒或者毫秒等作为单位),默认分析间隔为10ms

  • -o  specifies what information to dump when profiling ends,如果选择 svg,produce Flame Graph in SVG format.

等待15s,就会产生结果,生成 627891.svg 文件。vim 627891.svg 并删除第一行,然后下载到本地并使用浏览器打开。

结果如下图,此图俗称火焰图,主要看每个方法的横轴长度,占用横坐标越长的操作,其占用的 cpu 即最长,很直观的。

4.2 性能黑点分析

我们首先发现下图红框内的代码存在严重的性能问题。在append的过程中,获取线程 stack 的过程耗时比较大。从火焰图中分析,耗时占据了接近50%的是,一个logger操作中去拿线程堆栈的过程,那么我们打印的日志的时候,为啥会进行这个操作呢?

首先观察消息日志文件,和一般的日志不同,这里还会打印方法名称,即当前日志时在哪个方法下打印出来的。那么日志框架它是通过线程 stack 去获取到方法名称的,如果配置了 %L,即打印日志所在代码行的情况也是同理。

[2019-10-07 11:50:38 251 INFO ] [PoolDrainDataThreadPool-3] dummy.es.transport.batchAddDocument() - A => {"@timestamp":"2019-10-07T03:50:38.251Z","ipv4":"10.0.49.96:14160;10.0.49.85:14159;10.0.49.85:14160;10.0.49.84:14160;10.0.49.97:14160;10.0.49.96:14159;10.0.49.89:14159;10.0.49.97:14159;10.0.49.86:14159;10.0.49.84:14159;10.0.49.86:14160;10.0.49.89:14160","key":"ss","serviceName":"Redis","spanId":"-496431972509502272","startTs":1570420237,"tag":-1,"timestamp":1570420237329,"traceId":"-2375955836973083482"}

[2019-10-07 11:50:38 251 INFO ] [PoolDrainDataThreadPool-3] dummy.es.transport.batchAddDocument() - A => {"@timestamp":"2019-10-07T03:50:38.251Z","ipv4":"10.0.49.96:14160;10.0.49.85:14159;10.0.49.85:14160;10.0.49.84:14160;10.0.49.97:14160;10.0.49.96:14159;10.0.49.89:14159;10.0.49.97:14159;10.0.49.86:14159;10.0.49.84:14159;10.0.49.86:14160;10.0.49.89:14160","key":"ss","serviceName":"Redis","spanId":"6195051521513685066","startTs":1570420237,"tag":-1,"timestamp":1570420237333,"traceId":"-2375955836973083482"}

观察配置的日志格式:

<appender name="dummyEsAppender" class="org.apache.log4j.DailyRollingFileAppender">
    <param name="File" value="./logs/dummy-es-transport.log"/>
    <param name="DatePattern" value="'.'yyyy-MM-dd'.log'"/>
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern"
               value="[%d{yyyy-MM-dd HH:mm:ss SSS\} %-5p] [%t] %c.%M() - %m%n"/>
    </layout>
</appender>

注意输出格式中的 %M() 一行,这里意味着在打印日志的时候,需要打印当前日志所在执行的方法,这样看来,这个操作严重影响到了性能。

4.3 日志配置规则去除方法名

修改一下日志 append 格式,去掉方法输出,处理速率一下子就达到了7万多。

采用上文中的方法继续使用 async-profiler 生成火焰图,并用浏览器打开。这时候,日志 append 操作所占用的横轴长度显著下降,并且此时速度已经达到了关闭日志append 时的速度,说明修改日志输出格式后能够带来显著的性能提升。

但是观测上图,我们发现了新的性能黑点,如红框所述,我们将其展开,见详细图:

这里主要是一个 toHexString 的操作,竟然占用的cpu资源这么大,这里需要定位。

4.4 ObjectId.toHexString 性能优化

查看这一步转换为16进制的字符串的代码如下,我们结合上面的火焰图可以看出来,主要耗时是在 String.format()

这一步操作。


private String toHexString() {     StringBuilder buf = new StringBuilder(24);     byte[] bytes = new byte[12];     bytes[0] = int3(this.timestamp);     bytes[1] = int2(this.timestamp);     bytes[2] = int1(this.timestamp);         bytes[3] = int0(this.timestamp);     bytes[4] = int2(this.machineIdentifier);     bytes[5] = int1(this.machineIdentifier);     bytes[6] = int0(this.machineIdentifier);     bytes[7] = short1(this.processIdentifier);     bytes[8] = short0(this.processIdentifier);     bytes[9] = int2(this.counter);     bytes[10] = int1(this.counter);     bytes[11] = int0(this.counter);     for (byte b : bytes) {         buf.append(String.format("%02x", new Object[]{Integer.valueOf(b & 0xFF)}));     }     return buf.toString(); }

上面这种模式存在比较大的性能问题。

byte 数组转换为 16进制字符串性能最好的代码:


private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private String toHexString2() {
//这一步获取到bytes数组,和上面的操作相同,单独抽离出来。
   byte[] bytes = this.toByteArray();     char[] hexChars = new char[bytes.length * 2];     for (int j = 0; j < bytes.length; j++) {         int v = bytes[j] & 0xFF;         hexChars[j * 2] = HEX_ARRAY[v >>> 4];         hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];     }     return new String(hexChars); }

4.5 重新测试

修改完耗时的 toHexString() 操作之后,打包上传到服务器,并重新启动程序,此时发现每秒处理速率已经飙升到了 12万。这种使用频繁又耗时的黑点操作解决以后,果然性能能够得到翻倍的提升。

此时日志输出格式已经优化,并且 toHexString()操作也进行了优化。重新使用 async-profiler 查看一下最新的火焰图信息。

之前的 toHexString() 耗时已经几乎看不到了,但是感觉日志append 的操作横轴还是略长,于是将日志输出关闭来看看极限处理速度。

将日志级别调整为 warn,并启动程序,观测到处理速度已经能够达到 18万/s了,这相当于 toHexString()优化前的快3倍了。

此时决定再将日志append 模式改为异步模式,然后启动程序,观察,处理速率也能够达到 18万/s。

总结

从最开始的 2万/s 的处理速率提升到 18万/s。这个提升还是很惊人的,而如果没有使用 火焰图进行定位,或许我并不能找到这个程序处理耗时到底耗在哪里。得益于 async-profiler 此神器,能够非常直观的进行定位。

日志框架打印方法名或者行号的模式对比正常模式其性能理论上下降至少有10倍,通过本文的逐步分析,我们也能够看到,确实其性能降低是非常明显的。建议日志框架禁止打印方法名称和行号的操作。

使用Async-profiler 对程序性能优化实战的更多相关文章

  1. 小程序Canvas性能优化实战

    以下内容转载自totoro的文章<小程序Canvas性能优化实战!> 作者:totoro 链接:https://blog.totoroxiao.com/canvas-perf-mini/ ...

  2. [python]用profile协助程序性能优化

    转自:http://blog.csdn.net/gzlaiyonghao/article/details/1483728 本文最初发表于恋花蝶的博客http://blog.csdn.net/lanph ...

  3. 网站性能优化实战——从12.67s到1.06s的故事

    文章摘自https://juejin.im/post/5b0b7d74518825158e173a0c 作为互联网项目,最重要的便是用户体验.在举国“互联网+”的热潮中,用户至上也已经被大多数企业所接 ...

  4. Golang 性能优化实战

    小结: 1. 性能查看工具 pprof,trace 及压测工具 wrk 或其他压测工具的使用要比较了解. 代码逻辑层面的走读非常重要,要尽量避免无效逻辑. 对于 golang 自身库存在缺陷的,可以寻 ...

  5. 我把阿里、腾讯、字节跳动、美团等Android性能优化实战整合成了一个PDF文档

    安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...

  6. C++ 应用程序性能优化

    C++ 应用程序性能优化 eryar@163.com 1. Introduction 对于几何造型内核OpenCASCADE,由于会涉及到大量的数值算法,如矩阵相关计算,微积分,Newton迭代法解方 ...

  7. Java程序性能优化技巧

    Java程序性能优化技巧 多线程.集合.网络编程.内存优化.缓冲..spring.设计模式.软件工程.编程思想 1.生成对象时,合理分配空间和大小new ArrayList(100); 2.优化for ...

  8. 《Java程序性能优化:让你的Java程序更快、更稳定》

    Java程序性能优化:让你的Java程序更快.更稳定, 卓越网更便宜,不错的书吧

  9. [JAVA] java程序性能优化

    一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util ...

随机推荐

  1. logstash可以考虑在项目中用起来

    在用Node.js开发项目的时候,我们常用 log4js 模块来进行日志的记录,可以通过配置 log4js 的 Appenders 将日志输出到Console.File和GELF等不同的地方. log ...

  2. yum用法笔记

    yum是指通过linux系统的一个命令也是一个软件包管理工具,基于rpm管理,通过命令下载指定网站的包源,下载好之后自动解压和分配 下载yum:一般linux的服务器在出厂前都自带yum,包括虚拟机 ...

  3. centos 下 sphinx安装和配置

    一.安装前提必备先安装工具 yum -y install make gcc g++ gcc-c++ libtool autoconf automake imake mysql-devel libxml ...

  4. Jquery 操作DOM元素

    一.文本输入框: text <input type=”text” value=”99.com” size=12 id=”input1” /> 1.获取文本值: $("#input ...

  5. 用CSS 实现 非浮动元素的 水平居中/垂直居中/水平垂直居中

    一.水平居中 (1)行内元素解决方案 只需要把行内元素包裹在一个属性display为block的父层元素中,并且把父层元素添加如下属性即可:   .parent { text-align:center ...

  6. 前端实现在线预览pdf、docx、xls、ppt等文件

    思路:前台将各种格式的附件上传到服务器----后台通过方法将这些格式的文件转化成图片,前台通过放映ppt的方式将其展示在页面上. 关键点:reveal.js 参考文章:https://www.awes ...

  7. 面试官:讲讲mysql表设计要注意啥

    内容时参考一个博主的,内容写的很好,就忍不住拿过来了,如遇到,请见谅 参考连接:https://www.cnblogs.com/rjzheng/p/11174714.html

  8. CF277B Set of Points——构造题

    题意 构造 $n$ 个点使其凸度(顶点数最多的凸多边形的顶点数)恰好为 $m$,且任意三点不能共线. (题意有点绕,建议看英文原文 分析 首先,题目并不是求凸包上的顶点数,而是求能形成的凸多边形的最大 ...

  9. [NOIP 2018]旅行

    题目链接 题意简介:现有一个图,小Y要把它走完,每个点只去一次,路径字典序最小. 分析:这道题我认为很重要的一个点就是它的数据范围.它只有两种 m=n-1 或 m=n.我们先考虑第一种:m=n-1也就 ...

  10. KindEditor3.x-自动上传Word图片功能.

    Chrome+IE默认支持粘贴剪切板中的图片,但是我要发布的文章存在word里面,图片多达数十张,我总不能一张一张复制吧?Chrome高版本提供了可以将单张图片转换在BASE64字符串的功能.但是无法 ...