当我们再说性能优化时我们在优化什么?

760 阅读7分钟

前言

当我们提出要性能优化的时候,一定是我们的系统在处理业务逻辑时开始出现与预期不相符的效率。比如交易链路耗时严重,消息队列出现积压。综上所述,我们优化的目的是要解决两个问题:

  • 响应耗时

tps耗时,一个tp包含若干qp。根据阿姆达尔定律,若想降低整体耗时,必然需要降低大部分qp的耗时,尤其响应耗时占比比较高的链路。

  • 吞吐量

吞吐量是指单位时间内处理的交易量,包含tps和并发数。若想提高吞吐量,简单的方式就是提高并发数,进行横向扩展,但这意味着更多的资源投入。如果此时机器性能没有被很好的压榨,扩容意味着浪费。所以提高tps,降低响应耗时就成了程序员必备的技能。

是什么影响了我们系统的性能?

要知道是哪个环节影响了性能,前提是我们要知道交易链路有哪些环节。我们通常会在系统接口的入口处打印一行日志,记录我们接收的报文和时间。忽略了在这之前请求是先进入了tomcat,然后转发给jvm。

Tomcat

tomcat共有三种连接器模式: BIO,NIO和APR。高版本tomcat默认NIO。

标题描述
BIO阻塞式IO,采用传统的java IO进行操作,该模式下每个请求都会创建一个线程,适用于并发量小的场景
NIO同步非阻塞,比传统BIO能更好的支持大并发,tomcat 8.0 后默认采用该模式
APRtomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作,需要编译安装APR库
AIO异步非阻塞,tomcat8.0后支持

配置方法:在tomcat conf 下找到server.xml

<Connector port="8080" protocol="配置如下"/>

BIO: protocol =" org.apache.coyote.http11.Http11Protocol" 
NIO: protocol ="org.apache.coyote.http11.Http11NioProtocol"
AIO: protocol ="org.apache.coyote.http11.Http11Nio2Protocol"
APR: protocol ="org.apache.coyote.http11.Http11AprProtocol"

在默认的配置下,NIO的模式的实现模型如下:

  • Acceptor线程:全局唯一,负责接受请求,并将请求放入Poller线程的事件队列。Accetpr线程在分发事件的时候,采用的Round Robin的方式来分发的
  • Poller线程:官方的建议是每个处理器配一个,但不要超过两个,由于现在几乎都是多核处理器,所以一般来说都是两个。每个Poller线程各自维护一个事件队列(无上限),它的职责是从事件队列里面拿出socket,往自己的selector上注册,然后等待selector选择读写事件,并交给SocketProcessor线程去实际处理请求
  • SocketProcessor线程:Ali-tomcat的默认配置是250(参见server.xml里面的maxThreads),它是实际的工作线程,用于处理请求。

一个典型的请求处理过程

图片.png

如图所示,是一个典型的请求处理过程。其中绿色代表线程,蓝色代表数据。

  • 1.Acceptor线程接受请求,从socketCache里面拿出socket对象(没有的话会创建,缓存的目的是避免对象创建的开销),
  • 2.Acceptor线程标记好Poller对象,组装成PollerEvent,放入该Poller对象的PollerEvent队列
  • 3.Poller线程从事件队列里面拿出PollerEvent,将其中的socket注册到自身的selector上
  • 4.Poller线程等到有读写事件发生时,分发给SocketProcessor线程去实际处理请求
  • 5.SocketProcessor线程处理完请求,socket对象被回收,放入socketCache

所以tomcat的线程配置也是一个可能的优化点。比如上游发送与下游接受总是存在延迟 ,那么就有可能是在tomcat线程池发生了阻塞。参考java线程池原理:

图片.png

除了tomcat线程阻塞造成接受延迟,另一个因素就是tcp连接。

TCP

说到tcp我们就不得不说,tcp的三次握手和四次挥手。图如下:

图片.png

图片.png

我们知道频繁的建立连接断开连接是十分消耗性能和时间的。一次tcp请求就涉及两次socketIO,两次用户空间和内核空间转换,以及上线文切换。尽管零拷贝可以减少一次用户空间和内核空间的数据拷贝,但是socketIO是减少不了的。那么一次连接和一次断开就有7次tcp请求,资源和时间的浪费触目惊心。

执行如下命令:netstat -antop|grep 8091 查看tcp状态,如果每个连接都长时间保持established状态,那么意味着tcp是长链接;反之,如果频繁出现time_wait,close_wait等断开连接时的状态,那么意味tcp是短链接。优化方向是连接池+长链接。

STW

说完了请求进入jvm之前的tomcat和tcp可能存在的问题,接下来说jvm内部影响性能的几种可能。jvm本身可能的就是垃圾收集器中的stop the word,尽管jvm在垃圾收集上下足了功夫,无论是最初的串行和并行收集器,以及以最短回收停顿时间为目标的CMS,还是号称准实时的G1,他们都无法避免的要暂停用户线程。所以优化jvm,减少fullgc次数,降低gc停顿时间也是性能优化的一个关键环节。

说完了jvm,接下来就要说说程序员自己编写的代码了。在高并发环境下,多线程访问共享变量,不可避免的使用到锁来做同步。执行如下命令jstack pid 分析线程状态看是否存在大量的wait已经timed_wait在申请同一个对象。

资源申请

在程序运行过程中,我们的本地缓存肯能存在资源扩容操作,比如HashMap,CopyOnWriteArrayList等数据结构在存储达到阈值后会进行扩容操作。尤其是数组扩容,申请的都是连续空间,如果此时堆里没有连续空间可用,就会触发fullgc,如果fullgc依然没有整理出足够的连续空间就可能因为晋升失败而发生OOM。 说到资源申请,顺便说一下jvm的tlab内存分配策略。tlab是为了解决线程并发申请堆内存造成的线程不安全和效率低下问题而提出的解决方案,就是预先为线程分配一定大小的内存,线程内的对象优先在栈上分配,不能栈上分配的就看是否能进行逃逸分析,用标量替换的方式争取把new的对象在栈上分配,如果依然不成功,则在tlab内存上进行分配 ,如果tlab内存不足,则重新申请tlab内存。原来的tlab剩余的内存作为碎片存在,等待内存整理。

伪共享

在java中我们对一些共享变量常常使用volitle关键字,以实现共享变量的可见性和有序性。但是如果该变量和另一个共享变量共同存在cpu的同一个缓存行里,这意味着任何一个变量的更新操作都会造成本缓存行的所有变量失效。

缓存失效

缓存是提高性能的杀手锏,因为我们知道内存查询与磁盘查询在性能上的差距是100倍(如果没有记错的话)。但是如果缓存失效,带来缓存击穿,缓存穿透和雪崩,不说压垮mysql,就是mysql查询的磁盘io就是严重的性能问题。所以防止缓存失效既是提供性能也是保证系统稳定性的关键。

IO

IO数据读写是最消耗性能的。io一般存在网络io,磁盘io和内存io。一般读写过程步骤为:io外部设备->DMA缓冲区->用户进程缓冲区。反之亦然。零拷贝就是我们优化的一个方向,减少了一次cpu将数据从DMA缓冲区拷贝到用户进程缓冲区的步骤。另外,io繁忙,会阻塞请求进入,或者引起户线程暂停。比如logback异步落盘的时候,如果配置的缓存太大,会占用io,导致cpuwait。建议1024m,2g就可以导致停顿。当然也看机器配置。

参考: developer.aliyun.com/article/288…