阅读 2053

亿级流量网站性能优化的方法论步骤

欢迎关注 肖哥弹架构好友 树下搜胡 新专题 你真的了解亿级流量网站性能优化吗? juejin.cn/post/689314…

并发通识&压力测试&服务端优化

一、并发&高并发

1、什么是并发?

? 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

就像前面提到的操作系统的时间片分时调度。打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。

2、并发与并行

我们两个人在吃午饭。你在吃饭的整个过程中,吃了米饭、吃了蔬菜、吃了牛肉。吃米饭、吃蔬菜、吃牛肉这三件事其实就是并发执行的。

对于你来说,整个过程中看似是同时完成的的。但其实你是在吃不同的东西之间来回切换的。

还是我们两个人吃午饭。在吃饭过程中,你吃了米饭、蔬菜、牛肉。我也吃了米饭、蔬菜和牛肉。

我们两个人之间的吃饭就是并行的。两个人之间可以在同一时间点一起吃牛肉,或者一个吃牛肉,一个吃蔬菜。之间是互不影响的。

所以,并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

并发和并行的区别:

并发,指的是多个事情,在同一时间段内同时发生了。

并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。

并行的多个任务之间是不互相抢占资源的、

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

image-20200825014603735.png

3、什么是高并发?

高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程(他们没有必然的直接联系)

多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。

  高并发是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。
    
    该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。
    
    如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……而多线程只是其中解决方法之一。
    
 实现高并发需要考虑:
   (1)系统的架构设计,如何在架构层面减少不必要的处理(网络请求,数据库操作等)例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间;
   (2)网络拓扑优化减少网络请求时间、如何设计拓扑结构,分布式如何实现?
   (3)系统代码级别的代码优化,使用什么设计模式来进行工作?哪些类需要使用单例,哪些需要尽量减少new操作?
   (4)提高代码层面的运行效率、如何选取合适的数据结构进行数据存取?如何设计合适的算法?
   (5)任务执行方式级别的同异步操作,在哪里使用同步,哪里使用异步?
   (6)JVM调优,如何设置Heap、Stack、Eden的大小,如何选择GC策略,控制Full GC的频率?
   (7)服务端调优(线程池,等待队列)
   (8)数据库优化减少查询修改时间。数据库的选取?数据库引擎的选取?数据库表结构的设计?数据库索引、触发器等设计?是否使用读写分离?还是需要考虑使用数据仓库?
   (9)缓存数据库的使用,如何选择缓存数据库?是Redis还是Memcache? 如何设计缓存机制?
   (10)数据通信问题,如何选择通信方式?是使用TCP还是UDP,是使用长连接还是短连接?NIO还是BIO?netty、mina还是原生socket?
   (11)操作系统选取,是使用winserver还是Linux?或者Unix?
   (12)硬件配置?是8G内存还是32G,网卡10G还是1G? 例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
    
    以上的这些问题在高并发中都是必须要深入考虑的,就像木桶原理一样,只要其中的某一方面没有考虑到,都会造成系统瓶颈,影响整个系统的运行。而高并发问题不仅仅涉及面之广,同时又要求有足够的深度!!!
   而多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。
	多线程在解决高并发问题中所起到的作用就是使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。
    
	unix是第一个成熟的计算机操bai作系统,一开始都du是作为服务器操作系统,zhi企业或是高校才dao能买得起,之后出现过几类其他基于unix的操作系统,有一个miniunix是开发用来教学使用的,功能很有限,所以Linus决定自己在mini的基础上开发一个系统,他在互联网上发布了这个想法并开发了第一个linux版本,之后开发人员越来越多,之后就有公司或团队开发,就有了现在的ubuntu、Suse、deban、red hat等发行版。
    
好坏不是绝对的,unix作为服务器绝对是最牛的,不然IBM早不干了,但是他的价格也是很高的。所以就选linux服务器版,好处很明显,开源价廉,现在虽然linux免费,但作为服务器很多其他功能还是收费的,并不是完全免费。
    
而对于个人用户,linux绝对是没的选,现在各种发行版有桌面版,使用很方便。而且由于linux对于unix的继承性,学会linux指令就基本上学会了unix指令,所以linux相对好一点。
复制代码

4、你真的了解高并发吗?

? 高并发就是大家臆想的吹牛逼,其实大部分业务场景不存在并发竞争数据的情况,那么加服务 加机器基本上都能解决问题,你要事务压力大 那就分表,要是查询压力大 就主从 + 缓存,总有办法解决问题的。

# 所谓的亿级流量,100w级qps ,你真的懵逼了吗??

例:某服务136万笔(粗粒度统计),平均RT在100ms,指标计算如下

**指标=(日量36万/10小时高峰期/3600秒)x5倍保险系数**=50qps、rt100ms

例:某服务近期监控请求峰值为1小时36万笔(粗粒度统计),平均RT在100ms,,指标计算如下

**指标=(时量36万/3600秒)x5倍保险系数**=500qps、rt100ms

    
# 衡量高并发的指标
Qps , TPS , RT , 吞吐量
    
# 商品详情页可能有以下查询 
    --- 亿级流量:每天大概会有 100w 单,这么多商品详情的流量会有多少呢?
	1、商品数据信息查询  2~32、规格选择查询(每一个规格都点一遍) 203、商品评论查询 34、几个商品反复比较 105、再去其他平台查询商品,回来再看2-346次 ,姑且按照 50次算
    经过50次查询,可能才能成交一单。
    峰值:假设节假日期间流量增大3倍,50*3 = 150 
    那么 100w单就可能有: 100w * 150 = 1.5亿
做系统设计的时候,统计流量都必须往多了算, 为了应付突发情况,流量会突然一下变大,一般将算出来的值再乘以三倍。
    
只算高峰期请求,1.8 亿可能有 1.5 亿都在 12 个小时的高峰期内
每小时的流量是:1.8亿 / 12 = 1250w
每分的流量是:1250w / 60 = 20.8w
每秒的流量是:20.8w / 60 = 3472
    
# 内存预估
它内部的属性跟它是引用关系,意思就是:这 8 mb 在堆内存中大大小小可能会有很多个对象, 它的每个引用变量背后都是一个对象。(这个一个套娃的关系 懂的伐)

深圳 - 上海属于热门航线所以报文会比较大, 冷门航线比如 海拉尔 - 银川 这种 可能只有几条航线 报文可能只有几百 kb 但热门航线被请求的概率又要大一些,综合来看:我们把每次报文大小算作 5mb

qps110 * 5 mb = 550 mb

这意味着:新生代每秒钟将会有,550mb 的空间会被占用。
    
    
    五、其他系统如何分析
注意:这个系统跟其他系统不太一样

因为这个核心服务的核心接口就两个:列表查询、指定查询

但是其他系统 , 比如订单系统可能会有很多个接口 , 除了比较核心的订单查询接口 , 还有创单、下单、查库存等接口

一般核心接口逻辑会要复杂一点 , 对象占用的大小要更大一点 , 我们用最占内存的那个接口来分析

因为最差的情况就是所有的流量都打到核心接口

指定查询其实相对于列表查询来说频率会低一些 , 这里仅拿列表查询的情况当做一个系统所有接口来作为参考

因为这个接口的一次平均 5mb 其实也挺大了, 当然这里只是作为一个参考

分析思路
根据 集群的 qps 除以机器数量 , 估算出每分钟每个接口的 qps

计算出各个接口中所有对象实际占用的内存

qps 不是很高可以用每分钟的内存大小做参考即可

并发量很高 就用每秒占用内存的大小来做参考

六、总结
今天先分享这个亿级流量系统的大概情况 , 部分数据只是作为参考

其实这个系统高峰期流量不止亿级 , 机器也不只是 8C 16G

不过来用做案例分析是 ok 的

复制代码

二、并发指标分析

1、吞吐量

? 在了解qps、tps、rt、并发数之前,首先我们应该明确一个系统的吞吐量到底代表什么含义,一般来说,系统吞吐量指的是系统的抗压、负载能力,代表一个系统每秒钟能承受的最大用户访问量。

一个系统的吞吐量通常由qps(tps)、并发数来决定,每个系统对这两个值都有一个相对极限值,只要某一项达到最大值,系统的吞吐量就上不去了。

所谓的系统吞吐量其实就是 : 系统每秒请求数

2、QPS

Queries Per Second,每秒查询数,即是每秒能够响应的查询次数,注意这里的查询是指用户发出请求到服务器做出响应成功的次数,简单理解可以认为查询=请求request。

qps=每秒钟request数量

3、TPS

Transactions Per Second 的缩写,每秒处理的事务数。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。

	针对单接口而言,TPS可以认为是等价于QPS的,比如访问一个页面/index.html,是一个TPS,而访问/index.html页面可能请求了3次服务器比如css、js、index接口,产生了3个QPS。
复制代码

tps=每秒钟事务数量

QPS和TPS区别个人理解如下:
	1、Tps即每秒处理事务数,包括了用户请求服务器 服务器自己的内部处理 服务器返回给用户这三个过程,每秒能够完成N个这三个过程,Tps也就是N;
    
	2、Qps基本类似于Tps,但是不同的是,对于一个页面的一次访问,形成一个Tps;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“Qps”之中。例子: 例如:访问一个页面会请求服务器3次,一次放,产生一个“T”,产生3个“Q” 
    
	例如:一个大胃王一秒能吃10个包子,一个女孩子0.1秒能吃1个包子,那么他们是不是一样的呢?答案是否定的,因为这个女孩子不可能在一秒钟吃下10个包子,她可能要吃很久。这个时候这个大胃王就相当于TPS,而这个女孩子则是QPS。虽然很相似,但其实是不同的。

# 大部分情况不用纠结QPS  TPS
TPS=QPS(每秒请求数),所以大部分条件下,这两个概念不用纠结的!

	举个例子,我需要进行一次查询,但这个查询需要调用A服务和B服务,而调用B服务需要2次调用,那么这种情况,以我查询这个场景成功作为一次事务的话,我一秒请求一笔就是1tps,当然对于A系统是1tps=1qps的.
    但对于B系统而言,就是2qps,因为调用了两次(如果只看B服务的话,把每次请求当做一次事务的话2qps=2tps,还是可以等同的)所以仅仅是关注维度的不同,绝大多数时候我们不用去刻意区分的,毕竟我可以说我流程是1tps,B系统受到的双倍额压力是2tps的量(压测过程中也务必关注这样的流量放大服务,因为很有可能前面的服务抗的住,后面扛不住),这样也是完全没有问题的。

复制代码

? 如果单个接口请求,QPS = TPS ,但是观察这个变量的维度不一样,如果从请求的发起到请求的结束,且中间没有任何服务的远程调用,那么QPS = TPS 是没有任何疑义的,但是如果存在多个远程调用的话,就那么对于一台服务器 QPS = TPS 也是没有问题的,但是站在整个请求链路来说 ,QPS > TPS

image-20200825021751451.png

4、RT

? Response Time缩写,简单理解为系统从输入到输出的时间间隔,宽泛的来说,他代表从客户端发起请求到服务端接受到请求并响应所有数据的时间差。一般取平均响应时间。

? 对于RT,客户端和服务端是大不相同的,因为请求从客户端到服务端,需要经过广域网,所以客户端RT往往远大于服务端RT,同时客户端的RT往往决定着用户的真实体验,服务端RT往往是评估我们系统好坏的一个关键因素。

	在开发过程中,我们一定面临过很多的线程数量的配置问题,这种问题往往让人摸不到头脑,往往都是拍脑袋给出一个线程池的数量,但这可能恰恰是不靠谱的,过小的话会导致请求RT极具增加,过大也一样RT也会升高。所以对于最佳线程数的评估往往比较麻烦。
	
复制代码

并发数:

简而言之,系统能同时处理的请求/事务数量。

计算方式:

QPS=并发数/RT 或者 并发数=QPS*RT

? 举个栗子:

? 假设公司每天早上9点到10点1个小时内都有员工要上厕所,公司有3600个员工,平均每个员工上厕所时间为10分钟,我们来计算一下。

QPS = 3600/60*60 1

RT = 10*60 600秒

并发数 = 1 * 600 600

这样就意味着如果想达到最好的蹲坑体验,公司需要600个坑位来满足员工需求,否则的话上厕所就要排队等待了。

	随着请求数量的增加,带来大量的上下文的切换、gc和锁变化。qps更高,产生对象越多,gc越频繁,cpu time和利用率都受到影响,尤其在串行的时候,锁自旋、自适应、偏向等等也成为影响par的因素。
	总结,为了提升达到最好的性能,我们需要不断的进行性能测试,调整小城池大小,找到最合适的参数来达到提高性能的目的。
复制代码

三、压测报告瓶颈分析

? 等到服务上线后,在业务压力的冲击下,会发现程序运行非常的慢,或者是宕机,莫名其妙的出现各种问题,只会进行一些无脑的扩容,扩容真的能解决问题吗??

可能能解决问题,但是同时也会带来一些其他的问题,因此在项目上线之前,还必须要有一步性能压力测试的步骤,以便于发现服务的一些问题,提前对服务的问题进行修复,优化等等。

1、应用部署

1.1、服务打包

项目打包: 可以使用idea直接打包上传,也可以在gitlab服务器直接通过maven进行打包,或者使用jenkins来进行打包,现在我们先使用idea的maven进行打包。

 # 注意打包必须的依赖
 <build>
        <plugins>
     		<--此依赖必须有,否则项目的依赖包无法被打进项目:mysql.jar,spring*.jar都无法打包进入-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- 编译项目,然后打包,只会打包自己的代码 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
复制代码

idea的maven打包:

image-20200717153959241.png

注意: 打包服务的时候必须注意服务的配套的ip地址,由于此时服务和mysql,redis都在同一个服务器上,因此连接访问地址设置为localhost即可。

server:
  port: 9000
spring:
  application:
    name: sugo-seckill-web
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
#    url: jdbc:mysql://47.113.81.149:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    druid:
      #配置初始化大小、最小、最大
      initial-size: 1
      min-idle: 5
      max-active: 5
      max-wait: 20000
      time-between-eviction-runs-millis: 600000
      # 配置一个连接在池中最大空闲时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查
      test-on-borrow: true
      #设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查
      test-on-return: true
      # 设置从连接池获取连接时是否检查连接有效性,true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查;false时,不检查
      test-while-idle: true
      # 检验连接是否有效的查询语句。如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用validationQuery查询进行检查。(Oracle jdbc Driver目前不支持ping方法)
      validation-query: select 1 from dual
      keep-alive: true
      remove-abandoned: true
      remove-abandoned-timeout: 80
      log-abandoned: true
      #打开PSCache,并且指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久启动一次DestroyThread,对连接池内的连接才进行一次检测,单位是毫秒。
      #检测时:
      #1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的值则直接物理关闭。
      #2.在minIdle以内的不处理。
  redis:
    host: 127.0.0.1
    port: 6379
mybatis:
  type-aliases-package: com.supergo.pojo
mapper:
  not-empty: false
  identity: mysql
复制代码

1.2、打包上传

image-20200720100932671.png

启动命令: java -jar jshop-web-1.0-SNAPSHOT.jar

注意:服务器部署的时候,由于服务器环境的不同,往往都需要额外的修改服务的配置文件,重新编译打包,必须服务器ip地址,本地开发环境的ip和线上的ip是不一样的,部署的时候,每次都需要修改这些配置,非常麻烦。因此服务部署时候应该具有一个外挂配置文件的能力。

#启动命令,注意:配置文件的名称必须是application.yaml,或者application.properties
java -jar xxx.jar --spring.config.addition-location=/usr/local/src/application.yaml

复制代码

外挂配置文件使用本地连接地址:

spring:
  application:
    name: sugo-seckill-web
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    druid:
      #配置初始化大小、最小、最大
      initial-size: 1
      min-idle: 5
      max-active: 5
      max-wait: 20000
      time-between-eviction-runs-millis: 600000
      # 配置一个连接在池中最大空闲时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查
      test-on-borrow: true
      #设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查
      test-on-return: true
      # 设置从连接池获取连接时是否检查连接有效性,true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查;false时,不检查
      test-while-idle: true
      # 检验连接是否有效的查询语句。如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用validationQuery查询进行检查。(Oracle jdbc Driver目前不支持ping方法)
      validation-query: select 1 from dual
      keep-alive: true
      remove-abandoned: true
      remove-abandoned-timeout: 80
      log-abandoned: true
      #打开PSCache,并且指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久启动一次DestroyThread,对连接池内的连接才进行一次检测,单位是毫秒。
      #检测时:
      #1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的值则直接物理关闭。
      #2.在minIdle以内的不处理。
  redis:
    host: 127.0.0.1
    port: 6379
复制代码

1.3、启动脚本

创建deploy.sh 这样一个shell脚本文件,执行java程序的后端启动工作

#使用nohup启动,使得Java进程在后台以进程模试运行
nohup java -Xms500m -Xmx500m -XX:NewSize=300m -XX:MaxNewSize=300m -jar jshop-web-1.0-SNAPSHOT.jar --spring.config.addition-location=application.yaml > log.log 2>&1 &

#授权
chmod 777 deploy.sh

#查询进程
jps 
jps -l  
复制代码

浏览器接口测试访问,发现服务已经启动成功:

2、开始压测

2.1、压测准备

压测目标:

线程梯度:500、1000,1500,2000,2500,3000个线程,即模拟这些数目的用户并发;

时间设置:Ramp-up period(inseconds)的值设为1(即1s,5s,10s启动500、1000,1500,2000,2500,3000并发访问)

循环次数:50次

1)设置压测请求

2.2、添加监听器

聚合报告:添加聚合报告

查看结果树:添加查看结果树

TPS统计分析:每秒事务数

image-20200724141409194.png

服务器性能监控:CPU、内存、IO

image-20200724144358806.png

注意: 对于jmeter的cpu监控来说,只能监控单核cpu,没有太大的参考价值,真实测试可以使用top指令观察cpu使用情况。

3、参数原理

我们设置线程数 n = 5,循环次数a = 1000,请求www.google.com,得到聚合报告如图:

image-20200724121138765.png)

图中得到谷歌首页的平均请求时间大约为t = 0.2秒

这里,我们为了方便分析,将Ramp-Up Period 设置为T = 10秒(实际合理的时间后面会说明)

依然是n = 5,得到 S = (T- T/n) = 8 ,也就是说,从第一个线程启动到第8秒的时候,最后一个线程开始启动,若需要在最后一个线程启动的时候第一个线程仍未关闭,则需要满足 a·t > S ,已知S = 8,t = 0.2,得到 a > 40 。

线程数:n=5
循环次数:a = 1000
平均响应时间: t = 0.2s
Ramp-Up Period T=10s
S = (T-T/n) = 8
# 循环次数 * 平均响应时间 
a * t > S  ==>  a > S/t = 40 

Ramp-Up Period:

【1】决定多长时间启动所有线程。如果使用10个线程,ramp-up period是100秒,那么JMeter用100秒使所有10个线程启动并运行。每个线程会在上一个线程启动后10秒(100/10)启动。Ramp-up需要要充足长以避免在启动测试时有一个太大的工作负载,并且要充足小以至于最后一个线程在第一个完成前启动。  一般设置ramp-up=线程数启动,并上下调整到所需的。

【2】用于告知JMeter 要在多长时间内建立全部的线程。默认值是0。如果未指定ramp-up period ,也就是说ramp-up period 为零, JMeter 将立即建立所有线程。假设ramp-up period 设置成T 秒, 全部线程数设置成N个, JMeter 将每隔T/N秒建立一个线程。

【3】Ramp-Up Period(in-seconds)代表隔多长时间执行,0代表同时并发
复制代码

OK,既然循环次数要大于40,我们不妨把循环设置成100,那么单个线程运行时间就是R = a·t = 20秒,也就是说第一个线程会在第20秒的时候停止,整个测试的理论运行时间为 S + R = (1-1/n)·T + a·t = 28秒

我们用一张图来直观的看看每个线程的运行情况

从图中可以得到从第8秒开始,到第20秒,5个线程同时在运行中,此时才是真正的模拟5个用户同时并发

说了这么多,我们的目的到底是什么?无非是如何设置线程数,Ramp-Up Period以及循环次数。线程数我就不多说了,看各个项目的测试需求,而刚刚我说了这么多,实质上只是介绍了一些概念和如何合理的设置循环次数,至于Ramp-Up Period如何合理这是,请看下面大神的分析。

4、性能参数分析

4.1、TPS吞吐能力

聚合报告:TPS 3039 ,但是在这里看不见TPS的峰值是多少,需要进行改进

image-20200724151038362.png

样本(sample): 发送请求的总样本数量
平均值(average):平均的响应时间
中位数(median): 中位数的响应时间,50%请求的响应时间
90%百分位(90% Line): 90%的请求的响应时间,意思就是说90%的请求是<=1765ms返回,另外10%的请求是大于等于1765ms返回的。
    
95%百分位(95% Line): 95%的请求的响应时间,95%的请求都落在1920ms之内返回的
99%百分位(99% Line): 99%的请求的响应时间
最小值(min):请求返回的最小时间,其中一个用时最少的请求
最大值(max):请求返回的最大时间,其中一个用时最大的请求
异常(error): 出现错误的百分比,错误率=错误的请求的数量/请求的总数
吞吐量TPS(throughout): 吞吐能力,这个才是我们需要的并发数
Received KB/sec----每秒从服务器端接收到的数据量
Sent KB/sec----每秒从客户端发送的请求的数量
复制代码

使用Tps监控曲线图:

可以看见,TPS在1s+的位置,TPS能力最大,其他时候TPS都一直平稳过渡。

随着并发压力的加大,以及时间延长,系统性能所发生的变化。正常情况下,平均采样响应时长曲线应该是平滑的,并大致平行于图形下边界。

4.2、性能曲线

随着并发压力的加大,以及时间延长,系统性能所发生的变化。正常情况下,平均采样响应时长曲线应该是平滑的,并大致平行于图形下边界。

可能存在性能问题:

平均值在初始阶段跳升,而后逐渐平稳起来

一是系统在初始阶段存在性能缺陷,需要进一步优化,如数据库查询缓慢

二是系统有缓存机制,而性能测试数据在测试期间没有变化,如此一来同样的数据在初始阶段的响应时长肯定较慢;这属于性能测试数据准备的问题,不是性能缺陷,需调整后在测试

三是系统架构设计导致的固有现象,例如在系统接收到第一个请求后,才去建立应用服务器到数据库的链接,后续一段时间内不会释放连接。

平均值持续增大,图片变得越来越陡峭

一是可能存在内存泄漏,此时可以通过监控系统日志、监控应用服务器状态等常见方法,来定位问题。

平均值在性能测试期间,突然发生跳变,然后又恢复正常

一是可能存在系统性能缺陷

二是可能由于测试环境不稳定所造成的(检查应用服务器状态【CPU占用、内存占用】或者检查测试环境网络是否存在拥塞)

四、服务优化

1、线程数量提升

# server端线程数到245就已经上不去了,导致服务端的性能提升不上去
# pstree 查询线程数量
jps -l
# 查询所有线程
pstree -p pid   
# 统计线程数量
pstree -p pid | wc -l
#开启压测,再次统计线程数量,查看线程数量是否增大,是否还有增大的空间

复制代码

2、内嵌配置

Springboot开发的服务使用内嵌的tomcat服务器来启动服务,那么tomcat配置使用的是默认配置,我们需要对tomcat配置进行一些适当的优化,让tomcat性能得以提升。

当然内嵌tomcat内嵌线程池的配置也是比较小的,我们可以通过外挂配置文件,把tomcat的相关配置进行改写,然后重新启动服务器进行测试。修改配置如下所示:

Tomcat的maxConnections、maxThreads、acceptCount三大配置,分别表示最大连接数,最大线程数、最大的等待数,可以通过application.yml配置文件来改变这个三个值,一个标准的示例如下:

server:
  tomcat:
    uri-encoding: UTF-8
    #最大工作线程数,默认200, 4核8g内存,线程数经验值800
    #操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。
    max-threads: 1000
    # 等待队列长度,默认100
   accept-count: 1000
   max-connections: 20000
    # 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量
   min-spare-threads: 100
复制代码

1)、accept-count:最大等待数

? 官方文档的说明为:当所有的请求处理线程都在使用时,所能接收的连接请求的队列的最大长度。当队列已满时,任何的连接请求都将被拒绝。accept-count的默认值为100。 详细的来说:当调用HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数,默认100。如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。

2)、maxThreads:最大线程数

每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务容器可以同时处理多少个请求。maxThreads默认200,肯定建议增加。但是,增加线程是有成本的,更多的线程,不仅仅会带来更多的线程上下文切换成本,而且意味着带来更多的内存消耗。JVM中默认情况下在创建新线程时会分配大小为1M的线程栈,所以,更多的线程异味着需要更多的内存。线程数的经验值为:1核2g内存为200,线程数经验值200;4核8g内存,线程数经验值800。

3)、maxConnections:最大连接数

官方文档的说明为:

这个参数是指在同一时间,tomcat能够接受的最大连接数。对于Java的阻塞式BIO,默认值是maxthreads的值;如果在BIO模式使用定制的Executor执行器,默认值将是执行器中maxthreads的值。对于Java 新的NIO模式,maxConnections 默认值是10000。 对于windows上APR/native IO模式,maxConnections默认值为8192,这是出于性能原因,如果配置的值不是1024的倍数,maxConnections 的实际值将减少到1024的最大倍数。 如果设置为-1,则禁用maxconnections功能,表示不限制tomcat容器的连接数。 maxConnections和accept-count的关系为:当连接数达到最大值maxConnections后,系统会继续接收连接,但不会超过acceptCount的值。

3、问题解决

最近在用jmeter做压力测试时,发现一个问题,当线程持续上升到某个值时,报错:java.net.BindException: Address already in use: connect,如下图所示:

原因:windows提供给TCP/IP链接的端口为 1024-5000,并且要四分钟来循环回收它们,就导致我们在短时间内跑大量的请求时将端口占满了,导致如上报错。

解决办法(在jmeter所在服务器操作):

1.cmd中输入regedit命令打开注册表;

2.在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters右键Parameters;

3.添加一个新的DWORD,名字为MaxUserPort;

4.然后双击MaxUserPort,输入数值数据为65534,基数选择十进制;

5.完成以上操作,务必重启机器,问题解决,亲测有效;

image-20200725033341612.png

4、性能对比

未优化测试性能对比表:

并发数 / 5s样本数平均响应时间(ms)吞吐量(TPS)错误率KB/sec
20010000718490791
3001500014265401135
5002500071292501251
70035000141286501225
100050000244289101236
150075000418291001224
2000100000553292001249
300015000088925330.051086
3200
3500
4000
4500
5000

优化后性能对比表:

# 优化的参数配置
server:
  tomcat:
    accept-count: 800
    max-connections: 20000
    max-threads: 800
    min-spare-threads: 100

# 优化后,性能不升反降
	# 1、 由于根据主键从数据库查询数据,基本不耗时,也就是说这个是一个不耗时操作,增加线程反而会增加cpu切换的时间
	# 2、由于从数据库查询数据封装到对象,然后对象被垃圾回收(每次请求封装一个对象,然后垃圾回收),这个过程在并发下不停的重复,也会造成大量的性能的消耗。
	# 总结:想要看见优化服务器参数后的性能提升,那么必须要增加方法的业务耗时,如果消耗的时间变长,才能看出优化参数的效果,否则不可能看出效果。
	
	# 服务的单纯优化,已经能看出效果: 让线程睡了1s,然后在测试为优化,和已经优化后的结果,发现优化后效果tps明显提升。
复制代码

性能测试对比:

并发数 / 5s样本数平均响应时间(ms)吞吐量(TPS)错误率KB/sec
200100001118360785
3001500017248601063
5002500085269301152
70035000151276901184
100050000260273601171
150075000445276601183
2000100000582281301203
3000150000937279401195
32001600001147242901039
3500175000115027960.051198
40002000001292285201220
45002250001428282501208
5000250000158128670.031228

5、keepalive

长连接会消耗大量资源,如果连接不能及时释放,系统的TPS就提升不上去,因此我们需要改造web服务,提升web服务的长连接的性能。

package com.sugo.seckill.web.config;

//当Spring容器内没有TomcatEmbeddedServletContainerFactory这个bean时,会吧此bean加载进spring容器中
@Component
public class WebServerConfig implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory configurableWebServerFactory) {
            //使用对应工厂类提供给我们的接口定制化我们的tomcat connector
        ((TomcatServletWebServerFactory)configurableWebServerFactory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

                //定制化keepalivetimeout,设置30秒内没有请求则服务端自动断开keepalive链接
                protocol.setKeepAliveTimeout(30000);
                //当客户端发送超过10000个请求则自动断开keepalive链接
                protocol.setMaxKeepAliveRequests(10000);
            }
        });
    }
}

// 监控ESTABLISHED,TIME_WAIT 线程的数量
netstat -n | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'

//查看网络连接数:
netstat -an |wc -l
netstat -an |grep xx |wc -l       // 查看某个/特定PID的连接数
netstat -an |grep TIME_WAIT |wc -l   // 查看连接数等待time_wait状态连接数
netstat -an |grep ESTABLISHED |wc -l  //  查看建立稳定连接数量
    
//常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关
复制代码

TCP状态转移要点 TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接,所以很有必要保证无用连接完全断开,否则大量僵死的连接会浪费许多服务器资源。在众多TCP状态中,最值得注意的状态有两个:CLOSE_WAIT和TIME_WAIT。

1、LISTENING状态   FTP服务启动后首先处于侦听(LISTENING)状态。

2、ESTABLISHED状态   ESTABLISHED的意思是建立连接。表示两台机器正在通信。

*3、CLOSE_WAIT*

*对方主动关闭连接或者网络异常导致连接中断*,这时我方的状态会变成CLOSE_WAIT 此时我方要调用close()来使得连接正确关闭

*4、TIME_WAIT*

***我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT*。**TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。

目前有一种避免TIME_WAIT资源浪费的方法,就是关闭socket的LINGER选项。但这种做法是TCP协议不推荐使用的,在某些情况下这个操作可能会带来错误。

socket的状态

1.1 状态说明

CLOSED没有使用这个套接字[netstat 无法显示closed状态]
LISTEN套接字正在监听连接[调用listen后]
SYN_SENT套接字正在试图主动建立连接[发送SYN后还没有收到ACK]
SYN_RECEIVED正在处于连接的初始同步状态[收到对方的SYN,但还没收到自己发过去的SYN的ACK]
ESTABLISHED连接已建立
CLOSE_WAIT远程套接字已经关闭:正在等待关闭这个套接字[被动关闭的一方收到FIN]
FIN_WAIT_1套接字已关闭,正在关闭连接[发送FIN,没有收到ACK也没有收到FIN]
CLOSING套接字已关闭,远程套接字正在关闭,暂时挂起关闭确认[在FIN_WAIT_1状态下收到被动方的FIN]
LAST_ACK远程套接字已关闭,正在等待本地套接字的关闭确认[被动方在CLOSE_WAIT状态下发送FIN]
FIN_WAIT_2套接字已关闭,正在等待远程套接字关闭[在FIN_WAIT_1状态下收到发过去FIN对应的ACK]
TIME_WAIT这个套接字已经关闭,正在等待远程套接字的关闭传送[FIN、ACK、FIN、ACK都完毕,这是主动方的最后一个状态,在过了2MSL时间后变为CLOSED状态]

状态变迁图:(tcp连接状态变迁图)

Java NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件,这种通信模型是怎么实现的呢?呵呵,我们一起来探究它的奥秘吧。java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:

事件名对应值
服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件SelectionKey.OP_CONNECT(8)
读事件SelectionKey.OP_READ(1)
写事件SelectionKey.OP_WRITE(4)

服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。 我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。 下面是我理解的java NIO的通信模型示意图:

Selector。可以说它是NIO中最关键的一个部分,Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。

从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。

查询springboot内置tomcat使用的io类型,源码简单分析:

image-20200825165920608.png

6、nio2

使用nio2的http协议对请求进行改写,看服务器性能是否有提升

# 使用nio2
#嵌入tomcat配置
spring.server.port=8095
#和CPU数
spring.server.acceptorThreadCount=4
spring.server.minSpareThreads=50
spring.server.maxSpareThreads=50
spring.server.maxThreads=1000
spring.server.maxConnections=10000
#10秒超时
spring.server.connectionTimeout=10000
spring.server.protocol=org.apache.coyote.http11.Http11Nio2Protocol
spring.server.redirectPort=443
spring.server.compression=on
#文件请求大小
spring.server.MaxFileSize=300MB
spring.server.MaxRequestSize=500MB
    
    
@Slf4j
@Component
class AppTomcatConnectorCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
 
    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        ((TomcatServletWebServerFactory) factory).setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
        ((TomcatServletWebServerFactory) factory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                ProtocolHandler protocol = connector.getProtocolHandler();
 
                log.info("Tomcat({})  -- MaxConnection:{};MaxThreads:{};MinSpareThreads:{}", //
                        protocol.getClass().getName(), //
                        ((AbstractHttp11Protocol<?>) protocol).getMaxConnections(), //
                        ((AbstractHttp11Protocol<?>) protocol).getMaxThreads(), //
                        ((AbstractHttp11Protocol<?>) protocol).getMinSpareThreads());
 
            }
        });
    }
}
复制代码

7、undertow

# Undertow是Red Hat公司的开源产品, 它完全采用Java语言开发,是一款灵活的高性能Web服务器,支持阻塞IO和非阻塞IO。由于Undertow采用Java语言开发,可以直接嵌入到Java项目中使用。同时, Undertow完全支持Servlet和Web Socket,在高并发情况下表现非常出色


# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
# 不要设置过大,如果过大,启动项目会报错:打开文件数过多

server.undertow.io-threads=16

# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
server.undertow.worker-threads=256

# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
server.undertow.buffer-size=1024

# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
server.undertow.buffers-per-region=1024

# 是否分配的直接内存(NIO直接分配的堆外内存)
server.undertow.direct-buffers=true

# 配置说明
	undertow默认配置情况下,官方默认配置的是 CPU核数*8,比如8核CPU,实际工作线程数也就8*8=64,这个配置对于高并发场景来看,一台8核CPU的机器一般内存都会32G或以上,即使跑满64线程,占用的资源远远无法充分利用该机器的性能。
	当然啦,官方也是建议工作线程数的配置,取决于你机器的负载情况,其实也就是跟你的具体业务有关,我们现在的业务场景,64线程跑满的情况,CPU利用率仅仅百分之十几、CPU内存远远没利用完,再有请求过来,undertow则直接阻塞队列中,无法正常处理,资源浪费严重,还导致了服务中断的情况。

	因此,从实际情况来看,一定不要采用undertow的官方默认配置。像我们线上业务来看,每个API接口业务耗时大多数在10ms以内,少部分耗时30-50ms时间,单接口耗用资源不大。 修改undertow配置 worker-threads=256,单节点可以承载256的并发任务,剩下的就是根据实际业务情况动态扩展节点数量即可。
复制代码

五、常用性能分析方法

? Java 语言是当前互联网应用最为广泛的语言,作为一名 Java 程序猿,当业务相对比较稳定之后平常工作除了 coding 之外,大部分时间(70%~80%)是会用来排查突发或者周期性的线上问题。

由于业务应用 bug(本身或引入第三方库)、环境原因、硬件问题等原因,Java 线上服务出现故障 / 问题几乎不可避免。例如,常见的现象包括部分请求超时、用户明显感受到系统发生卡顿等等。

尽快线上问题从系统表象来看非常明显,但排查深究其发生的原因还是比较困难的,因此对开发测试或者是运维的同学产生了许多的困扰。

排查定位线上问题是具有一定技巧或者说是经验规律的,排查者如果对业务系统了解得越深入,那么相对来说定位也会容易一些。

不管怎么说,掌握 Java 服务线上问题排查思路并能够熟练排查问题常用工具 / 命令 / 平台是每一个 Java 程序猿进阶必须掌握的实战技能。

1、Java 服务常见线上问题

所有 Java 服务的线上问题从系统表象来看归结起来总共有四方面:CPU、内存、磁盘、网络。例如 CPU 使用率峰值突然飚高、内存溢出 (泄露)、磁盘满了、网络流量异常、FullGC 等等问题。

基于这些现象我们可以将线上问题分成两大类: 系统异常、业务服务异常。

1.1、 系统异常

常见的系统异常现象包括: CPU 占用率过高、CPU 上下文切换频率次数较高、磁盘满了、磁盘 I/O 过于频繁、网络流量异常 (连接数过多)、系统可用内存长期处于较低值 (导致 oom killer) 等等。

这些问题可以通过 top(cpu)、free(内存)、df(磁盘)、dstat(网络流量)、pstack、vmstat、strace(底层系统调用) 等工具获取系统异常现象数据。

此外,如果对系统以及应用进行排查后,均未发现异常现象的更笨原因,那么也有可能是外部基础设施如 IAAS 平台本身引发的问题。

1.2、业务服务异常

常见的业务服务异常现象包括: PV 量过高、服务调用耗时异常、线程死锁、多线程并发问题、频繁进行 Full GC、异常安全攻击扫描等。

2、问题定位

我们一般会采用排除法,从外部排查到内部排查的方式来定位线上服务问题。

  • 首先我们要排除其他进程 (除主进程之外) 可能引起的故障问题;
  • 然后排除业务应用可能引起的故障问题;
  • 可以考虑是否为运营商或者云服务提供商所引起的故障。

2.1、系统异常排查流程

问题定位流程,在linux系统中排查问题的方法,流程。

image-20200824184005113.png

2.2、业务应用排查流程

3. Linux 常用的性能分析工具

Linux 常用的性能分析工具使用包括 : top(cpu)、free(内存)、df(磁盘)、dstat(网络流量)、pstack、vmstat、strace(底层系统调用) 等。

3.1、CPU

CPU 是系统重要的监控指标,能够分析系统的整体运行状况。监控指标一般包括运行队列、CPU 使用率和上下文切换等。

top 命令是 Linux 下常用的 CPU 性能分析工具 , 能够实时显示系统中各个进程的资源占用状况 , 常用于服务端性能分析。

image-20200824184503219.png

top 命令显示了各个进程 CPU 使用情况 , 一般 CPU 使用率从高到低排序展示输出。其中 Load Average 显示最近 1 分钟、5 分钟和 15 分钟的系统平均负载,上图各值为 2.46,1.96,1.99。

我们一般会关注 CPU 使用率最高的进程,正常情况下就是我们的应用主进程。第七行以下:各进程的状态监控

# 各个组件的作用说明
PID : 进程 id
USER : 进程所有者
PR : 进程优先级
NI : nice 值。负值表示高优先级,正值表示低优先级
VIRT : 进程使用的虚拟内存总量,单位 kb。VIRT=SWAP+RES
RES : 进程使用的、未被换出的物理内存大小,单位 kb。RES=CODE+DATA
SHR : 共享内存大小,单位 kb
S : 进程状态。D= 不可中断的睡眠状态 R= 运行 S= 睡眠 T= 跟踪 / 停止 Z= 僵尸进程
%CPU : 上次更新到现在的 CPU 时间占用百分比
%MEM : 进程使用的物理内存百分比
TIME+ : 进程使用的 CPU 时间总计,单位 1/100 秒
COMMAND : 进程名称
复制代码

3.2、内存

内存是排查线上问题的重要参考依据,内存问题很多时候是引起 CPU 使用率较高的见解因素。

系统内存:free 是显示的当前内存的使用 ,-m 的意思是 M 字节来显示内容。

# free 查询当前内存使用情况
free -m

# 部分参数说明
total 内存总数: 7821M
used 已经使用的内存数: 713M
free 空闲的内存数: 3107M
shared 当前已经废弃不用 , 总是 0
buffers Buffer 缓存内存数: 3999M
复制代码

2.3、磁盘

# 使用df查看磁盘是否被占满,占用情况
df -h

# 查看具体目录下的磁盘使用情况
du -m /path
复制代码

2.4、网络

dstat 命令集成了 vmstat、iostat、netstat lsof 等等工具能完成的任务。

1)vmstat指令详解:

# 【在5秒时间内进行5次采样】
vmstat 5  5 

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 519512 199436 4079816    0    0     1     7   12    1  0  0 99  0  0
 0  0      0 519232 199436 4079816    0    0     0    10 1920 3413  0  0 99  0  0
 0  0      0 519380 199436 4079816    0    0     0    10 1854 3348  0  0 100  0  0

字段说明:
Procs(进程):
  r: 运行队列中进程数量
  b: 等待IO的进程数量

Memory(内存):
  swpd: 使用虚拟内存大小
  free: 可用内存大小
  buff: 用作缓冲的内存大小
  cache: 用作缓存的内存大小

Swap:
  si: 每秒从交换区写到内存的大小
  so: 每秒写入交换区的内存大小

IO:(现在的Linux版本块的大小为1024bytes)
  bi: 每秒读取的块数
  bo: 每秒写入的块数

系统:
  in: 每秒中断数,包括时钟中断。【interrupt】
  cs: 每秒上下文切换数。        【count/second】

CPU(以百分比表示):
  us: 用户进程执行时间(user time)
  sy: 系统进程执行时间(system time)
  id: 空闲时间(包括IO等待时间),中央处理器的空闲时间 。以百分比表示。
  wa: 等待IO时间
  
备注:
 # 1、如果r经常大于4,id经常少于40,表示cpu的负荷很重。
 # 2、如果bi,bo长期不等于0,表示内存不足。
 # 3、如果disk经常不等于0,且在b中的队列大于3,表示io性能不好。
 Linux在具有高稳定性、可靠性的同时,具有很好的可伸缩性和扩展性,能够针对不同的应用和硬件环境调整,优化出满足当前应用需要的最佳性能。因此企业在维护Linux系统、进行系统调优时,了解系统性能分析工具是至关重要的。

# 显示活跃和非活跃内存
vmstat -a 2 5 【-a 显示活跃和非活跃内存,所显示的内容除增加inact和active】

# 显示从系统启动至今的fork数量,【 linux下创建进程的系统调用是fork】
vmstat -f 

复制代码

2)lsof: 用于查看你进程开打的文件,打开文件的进程,进程打开的端口(TCP、UDP)。找回/恢复删除的文件

# lsof打开的文件可以是:
1.普通文件
2.目录
3.网络文件系统的文件
4.字符或设备文件
5.(函数)共享库
6.管道,命名管道
7.符号链接
8.网络文件(例如:NFS file、网络socket,unix域名socket)
9.还有其它类型的文件,等等
复制代码

3)dstat -c cpu 情况 -d 磁盘读写 -n 网络状况 -l 显示系统负载 -m 显示形同内存状况 -p 显示系统进程信息 -r 显示系统 IO 情况

注: 如果没有dstat指令,需要下载(yum -y install dstat)

4)pstack strace

# 有时我们需要对程序进行优化、减少程序响应时间。除了一段段地对代码进行时间复杂度分析,我们还有更便捷的方法吗?

	若能直接找到影响程序运行时间的函数调用,再有针对地对相关函数进行代码分析和优化,那相比漫无目的地看代码,效率就高多了。
	将strace和pstack工具结合起来使用,就可以达到以上目的。strace跟踪程序使用的底层系统调用,可输出系统调用被执行的时间点以及各个调用耗时;pstack工具对指定PID的进程输出函数调用栈。

复制代码

4、 JVM 定位问题工具

在 JDK 安装目录的 bin 目录下默认提供了很多有价值的命令行工具。每个小工具体积基本都比较小,因为这些工具只是 jdk\lib\tools.jar 的简单封装。

其中,定位排查问题时最为常用命令包括:jps(进程)、jmap(内存)、jstack(线程)、jinfo(参数) 等。

  • jps: 查询当前机器所有 JAVA 进程信息;
  • jmap: 输出某个 java 进程内存情况 (如:产生那些对象及数量等);
  • jstack: 打印某个 Java 线程的线程栈信息;
  • jinfo: 用于查看 jvm 的配置参数。

4.1、jps

jps 用于输出当前用户启动的所有进程 ID,当线上发现故障或者问题时,能够利用 jps 快速定位对应的 Java 进程 ID

# jps 命令查询
jps -l -m -m -l -l 参数用于输出主启动类的完整路径

ps -ef | grep tomcat
#我们也能快速获取 tomcat 服务的进程 id。
复制代码

当然,我们也可以使用 Linux 提供的查询进程状态命令,例如:

4.2、jmap

map命令是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。
打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。

# 输出当前进程 JVM 堆新生代、老年代、持久代等请情况,GC 使用的算法等信息 
jmap -heap pid

# 输出当前进程内存中所有对象包含的大小
jmap -histo:live {pid} | head -n 10  

# 以二进制输出档当前内存的堆情况,然后可以导入 MAT 等工具进行
jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid} 

# jmap(Java Memory Map) 可以输出所有内存中对象的工具 , 甚至可以将 VM 中的 heap, 以二进制输出成文本。
jmap -heap pid
# 输出当前进程 JVM 堆新生代、老年代、持久代等请情况,GC 使用的算法等信息
jmap -heap pid



using parallel threads in the new generation.  ##新生代采用的是并行线程处理方式
using thread-local object allocation.   
Concurrent Mark-Sweep GC   ##同步并行垃圾回收

 

Heap Configuration:  ##堆配置情况,也就是JVM参数配置的结果[平常说的tomcat配置JVM参数,就是在配置这些]
   MinHeapFreeRatio = 40 ##最小堆使用比例
   MaxHeapFreeRatio = 70 ##最大堆可用比例
   MaxHeapSize      = 2147483648 (2048.0MB) ##最大堆空间大小
   NewSize          = 268435456 (256.0MB) ##新生代分配大小
   MaxNewSize       = 268435456 (256.0MB) ##最大可新生代分配大小
   OldSize          = 5439488 (5.1875MB) ##老年代大小
   NewRatio         = 2  ##新生代比例
   SurvivorRatio    = 8 ##新生代与suvivor的比例
   PermSize         = 134217728 (128.0MB) ##perm区 永久代大小
   MaxPermSize      = 134217728 (128.0MB) ##最大可分配perm区 也就是永久代大小 

Heap Usage: ##堆使用情况【堆内存实际的使用情况】
New Generation (Eden + 1 Survivor Space):  ##新生代(伊甸区Eden区 + 幸存区survior(1+2)空间)
   capacity = 241631232 (230.4375MB)  ##伊甸区容量
   used     = 77776272 (74.17323303222656MB) ##已经使用大小
   free     = 163854960 (156.26426696777344MB) ##剩余容量
   32.188004570534986% used ##使用比例

Eden Space:  ##伊甸区
   capacity = 214827008 (204.875MB) ##伊甸区容量
   used     = 74442288 (70.99369812011719MB) ##伊甸区使用
   free     = 140384720 (133.8813018798828MB) ##伊甸区当前剩余容量
   34.65220164496263% used ##伊甸区使用情况

From Space: ##survior1区
   capacity = 26804224 (25.5625MB) ##survior1区容量
   used     = 3333984 (3.179534912109375MB) ##surviror1区已使用情况
   free     = 23470240 (22.382965087890625MB) ##surviror1区剩余容量
   12.43827838477995% used ##survior1区使用比例

To Space: ##survior2 区
   capacity = 26804224 (25.5625MB) ##survior2区容量
   used     = 0 (0.0MB) ##survior2区已使用情况
   free     = 26804224 (25.5625MB) ##survior2区剩余容量
   0.0% used ## survior2区使用比例

PS Old  Generation: ##老年代使用情况
   capacity = 1879048192 (1792.0MB) ##老年代容量
   used     = 30847928 (29.41887664794922MB) ##老年代已使用容量
   free     = 1848200264 (1762.5811233520508MB) ##老年代剩余容量
   1.6416783843721663% used ##老年代使用比例

Perm Generation: ##永久代使用情况
   capacity = 134217728 (128.0MB) ##perm区容量
   used     = 47303016 (45.111671447753906MB) ##perm区已使用容量
   free     = 86914712 (82.8883285522461MB) ##perm区剩余容量
   35.24349331855774% used ##perm区使用比例
复制代码

jmap 可以查看 JVM 进程的内存分配与使用情况,使用 的 GC 算法等信息。

jmap -histo:live {pid} | head -n 10:
jmap -histo:live {pid} | head -n 10  输出当前进程内存中所有对象包含的大小

输出当前进程内存中所有对象实例数 (instances) 和大小 (bytes), 如果某个业务对象实例数和大小存在异常情况,可能存在内存泄露或者业务设计方面存在不合理之处。

jmap -dump:
jmap -dump:format=b,file=/usr/local/logs/gc/dump.hprof {pid}

-dump:formate=b,file= 以二进制输出当前内存的堆情况至相应的文件,然后可以结合 MAT 等内存分析工具深入分析当前内存情况。

一般我们要求给 JVM 添加参数 -XX:+Heap Dump On Out Of Memory Error OOM 确保应用发生 OOM 时 JVM 能够保存并 dump 出当前的内存镜像。

当然,如果你决定手动 dump 内存时,dump 操作占据一定 CPU 时间片、内存资源、磁盘资源等,因此会带来一定的负面影响。

此外,dump 的文件可能比较大 , 一般我们可以考虑使用 zip 命令对文件进行压缩处理,这样在下载文件时能减少带宽的开销。

下载 dump 文件完成之后,由于 dump 文件较大可将 dump 文件备份至制定位置或者直接删除,以释放磁盘在这块的空间占用。
复制代码

4.3、jstack

printf ‘%x\n’ tid –> 10 进制至 16 进制线程 ID(navtive 线程)

10 进制: jstack pid | grep tid -C 30 –color

ps方法查询当前线程tid,以及当前线程占用的时间长度: ps -mp 8278 -o THREAD,tid,time | head -n 40

某 Java 进程 CPU 占用率高,我们想要定位到其中 CPU 占用率最高的线程。

(1) 利用 top 命令可以查出占 CPU 最高的线程 pid
top -Hp {pid}

(2) 占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出)
printf ‘%x\n’ 6900

(3) 利用 jstack 打印出 java 线程调用栈信息
jstack 6418 | grep ‘0x1af4’ -A 50 –color

注意:jstack pid 也可以直接根据进程id查询此进行下所有线程的堆栈情况,但是已经知道问题线程,应该直接定位问题线程。

# grep 参数说明
grep [-acinv] [--color=auto] [-A n] [-B n] '搜寻字符串' 文件名
参数说明:
-a:将二进制文档以文本方式处理
-c:显示匹配次数
-i:忽略大小写差异
-n:在行首显示行号
-A:After的意思,显示匹配字符串后n行的数据
-B:before的意思,显示匹配字符串前n行的数据
-v:显示没有匹配行
--color:以特定颜色高亮显示匹配关键字
复制代码

4.4、jinfo

#命令:jinfo pid
#描述:输出当前 jvm 进程的全部参数和系统属性

#命令:jinfo -flag name pid
#描述:输出对应名称的参数,使用该命令,可以查看指定的 jvm 参数的值。如:查看当前 jvm 进程是否开启打印 GC 日志。
# 查看某个 JVM 参数值 
jinfo -flag ReservedCodeCacheSize 28461 
jinfo -flag MaxPermSize 28461

#命令:jinfo -flag [+|-]name pid
#描述:开启或者关闭对应名称的参数,使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用。

#命令:jinfo -flag name=value pid
#描述:修改指定参数的值。

复制代码

4.5、jstat

# 垃圾回收器统计
jstat -gc pid
	S0C:第一个幸存区的大小
	S1C:第二个幸存区的大小
	S0U:第一个幸存区的使用大小
	S1U:第二个幸存区的使用大小
	EC:伊甸园区的大小
	EU:伊甸园区的使用大小
	OC:老年代大小
	OU:老年代使用大小
	MC:方法区大小
	MU:方法区使用大小
	CCSC:压缩类空间大小
	CCSU:压缩类空间使用大小
	YGC:年轻代垃圾回收次数
	YGCT:年轻代垃圾回收消耗时间
	FGC:老年代垃圾回收次数
	FGCT:老年代垃圾回收消耗时间
	GCT:垃圾回收消耗总时间

jstat -gcutil `pgrep -u admin java`

#jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
#命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
复制代码

4.6、VisualVM工具

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的 对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。 VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的 所有功能。

	内存信息
?	线程信息
?	Dump 堆(本地进程)
?	Dump 线程(本地进程)
?	打开堆 Dump。堆Dump可以用jmap来生成。
?	打开线程 Dump
?	生成应用快照(包含内存信息、线程信息等等)
?	性能分析。 CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查哪些类占用内存多)

复制代码

1)监控远程jvm VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。

2)什么是JMX?

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

3)监控远程的tomcat 想要监控远程的tomcat,就需要在远程的tomcat进行对JMX配置,方法如下:

保存退出,重启tomcat。

4)使用VisualJVM连接远程tomcat 添加远程主机: 在一个主机下可能会有很多的 jvm需要监控,所以接下来要在该主机上添加需要监控的 jvm:

#在tomcat的bin目录下,修改catalina.sh,添加如下的参数
JAVA_OPTS="‐Dcom.sun.management.jmxremote ‐
Dcom.sun.management.jmxremote.port=9999 ‐
Dcom.sun.management.jmxremote.authenticate=false ‐
Dcom.sun.management.jmxremote.ssl=false"
#这几个参数的意思是:
#-Djava.rmi.server.hostname
#‐Dcom.sun.management.jmxremote :允许使用JMX远程管理
#‐Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口
#‐Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用
户都可以连接
#‐Dcom.sun.management.jmxremote.ssl=false :不使用ssl
复制代码

连接成功。使用方法和前面就一样了,就可以和监控本地 jvm进程一样,监控远程的tomcat进程。

文章分类
后端
文章标签