关于服务性能优化思考

来源:互联网 发布:python 录屏 编辑:程序博客网 时间:2024/05/05 11:50


        在开发过程中常常有这样的抱怨:
        RD1:“这服务怎么老超时,到底行不行啊?”
        RD2:   “啥这点量高峰期就不行了,我得找运维给我加机器,并发量不行啊!”
        开发者在开发的过程中都需要时刻注意自己服务的性能,但有时候会常常在思想上犯一个毛病 :“性能做得好没啥用处,能用就行,流量高峰期扩容吧 ” 。那我们谈谈为什么需要对服务进行性能优化?至少有两点,一是提高产品体验,服务性能越好响应时间越低,降低超时率,客户体验越好;二是节约经济成本,服务性能好能带来QPS的提升,现有的资源能承受更大容量。
其实这和衡量服务性能息息相关的,那么我么想讨论服务的性能指标有哪些呢?

一、服务指标

1.应用性能指标

     QPS/QPM :能承受的并发数,通常一秒为单位

     Response Meantime (RT):请求的平均响应时间,通常单位为毫秒

     TP 50/90/99:响应时间的分布,通常关心的是top 99,平均响应时间反没有多大价值

2.机器性能指标

1)内存和线程指标

JVM本身提供了一组管理的API,通过该API,我们可以获取得到JVM内部主要运行信息,包括内存各代的数据、JVM当前所有线程及其栈相关信息等等。各种JDK自带的剖析工具,包括jps、jstack、jinfo、jstat、jmap、jconsole等,都是基于此API开发的。

很多公司的组件采集的JVM的一些参数也是利用JVM 本身的API,例如:
ThreadMXBean来记录JVM使用内存状况,jvm.thread.count,jvm.thread.daemon.count,jvm.thread.totalstarted.count等实时运行指标。
MemoryInformations来记录JVM内存使用状况,jvm.memory.used,jvm.memory.oldgen.used,jvm.memory.eden.used等指标。
GarbageCollectorMXBean来记录GC状况,ygc,fgc等。

2)cpu指标

jvm.process.cputime:java 进程占用的cpu时间,单位是ns

load.1minPerCPU :从线程等待cpu处理的个数来衡量CPU的空闲程度

cpu.idle /cpu.busy :idle是从时间的角度衡量CPU的空闲程度

cpu.switches:线程上下文切换

cpu.iowait:等待IO或者阻塞

 

说明:

*** 调度器:

其负责调度两种资源(线程,中断)。调度器针对不同的资源有不同的优先级,下面三个优先级从高到低依 次如下:
(中断)设备完成操作后通知内核,例如硬件设备受到一个IO请求;
(系统进程)所有内核操作都在这个级别;
(用户进程)java进程一般都运行在用户空间 中,其在调度器中优先级比较低;

*** 上下文切换概念:

干活的不是线程,而是cpu。Linux内核看每颗CPU处理器都是独立处理器。在每颗处理器中调度器都需要调度线程合理的使用CPU。每个线程分配了一个时间片的时间使用CPU,一旦时间片用完了或者高优先级资源(例如中断)需要占用CPU,该线程就会放回到调度队列中让高优先级资源使用CPU。这样的线程切换就是所谓的Context Switch(上下文切换)。每发生一次上下文切换资源就会从CPU的使用中挪到调度队列中。一个系统单位之间内上下文切换的次数越多,表明内核进行的工作越繁忙。

*** CPU运行队列概念:

每个CPU都维护了一个运行队列,调度器会一直处于运行和执行线程,这些线程不是处于运行状态就是休眠状态(等待IO或者阻塞)。如果CPU子系统的使用率非常高的情况下,内核调度程序可能跟不上系统的需要。会导致运行队列越来越长,一些可运行的线程一直得不到调度。

*** Load的概念:

正在执行的线程个数与运行队列中线程个数的和。例如一个4核系统,现在有4个线程在执行,有4个线程在运行队列。所以此时load为8。Linux提供了最近1min,5min,15min的统计。

*** CPU的使用率 :

CPU的使用率是指CPU使用百分比,该指标是系统CPU使用情况重要指标。大部分监控工具提供了如下四个维度的监控:
User Time(用户时间):用户空间中的用户态线程使用CPU耗时占整个时间段比例,一般战65%~90%是合理的
System Time(系统时间):系统线程(or 进程)使用CPU耗时占整个时间段比例,一般不超过30%比较合理
Wait IO:因为等待IO请求空闲时间占整个时间段比例
Idle:完全空闲时间占比


一图便知厉害~:


来讨论指标之间的关系

QPS 与 RT 计算 :对于单线程这个公式是永远正确,QPS=(1000/RT)*cpu核数*cpu利用率,  其中RT由包括 :线程的cpu计算时间+线程的等待时间(io/网络/锁);

最佳线程数 :最佳线程数=((线程cpu时间+线程等待时间)/线程cpu时间)* cpu 核数;

线程数与qps、rt之间关系

 

一般来说要提高qps,最好的方法是降低cpu时间,优化cpu时间能达到更好的效果,比如:减少json操作、减少for循环、减少数据逻辑运算、序列化等操作。

一般来说要降低rt,则是降低线程的等待时间(io/网络/锁),比如依赖的远程服务、数据库的读写、锁等,并且降低着先并不能带来qps的明显提升,因为对于我们大多数系统来说,业务逻辑都不是很复杂,需要耗费大量cpu计算的场景很少,因此cpu运算在rt中的占比不是很高,占比高的还是线程等待时间。


二、优化工具和步骤

利器: 

1.压测平台(测试环境和生产环境流量回放)

2.Jprofile(CPU热点)

3.JProfiler或者MAT(内存分析)

4.接口校验工具(进行一边优化后对服务提供的接口进行数据校验,一般适用http+json接口),类似git diff

5.gregs(在线诊断工具,一般查看加载类,方法信息,方法调用追踪渲染 

6.通过ngix等调整线上机器的权重,对某台线上机器施加流量压力,特别注意这是最后一步,是最真实最有说服力的压测。


优化一般步骤:

1.服务是干什么的?即梳理服务,明确上下游服务以及各依赖服务的性能。

2.为什么要优化?明确当前服务的性能,明确性能优化的目标,提高qps,rt,机器指标。

3. 怎么搞?循环(压测 =>热点分析/瓶颈分析=>优化=>服务接口校验/接口diff=>同步生产环境)

 

三、优化方向


1.cpu热点

利用 JProfiler抓CPU 热点, 经常会有预期之外的收获 ,不仅可以顶住项目本身代码引起的cpu热点,也很容易发现调用依赖服务封装的客户端引起的cpu热点。如容易发现“富客户端”引起的cpu热点,所谓的富客户端是指一些耗cpu的逻辑代码封装在jar包里,这必然导致使用者的服务增加cpu压力。

案例分析:

因为历史原因,项目严重依赖于org.json框架,依赖服务提供的接口基本上都是json接口,这就导致调用方拿到json数据之后进行一些列的加工检查,对JSONObject和JSONArray的get、put、parse、toString等操作满天飞,,还需要把json解析成java的model。所以json的一系列操作是非常耗cpu的。

总结来说使用org.json的至少面临两个严重的问题:

(1)业务处理过程混乱:业务处理类之间JSONObject和JSONArray对象到处传,遇到问题排查代码不容易发现在哪里和哪层添加了什么字段。

(2)性能差:做过实验

  • 在简单json场景下,org.json序列化耗时是jackson的2.5倍,反序列化耗时是2倍。
  • 中等复杂场景下,org.json的耗时是jackson的4倍。
  • 预计随着场景更加复杂,jackson的优势将会更加明显

解决方案:

(1)基于自定义java模型,实际上操作是依赖服务提供的http+json接口改造为thrift接口。因为有强类型约束之后代码会清晰很多,确定属性的名称和类型。

(2)如果“改造为基于java模型”无法做到或推动时间较长,必须得依赖某个json库。那么要基于jackson2,而不是jackson1,原因是restlet的JacksonRepresentation是绑死在jackson1上,因为最终要把restlet框架更换到springMVC,而springMVC更好支持异步的改造。

(3)jackson提供三种模式,API流模式(性能最好)、树模型(类似XML的DOM树)、数据绑定模型(使用最方便)。



2.依赖服务异步化

依赖的服务进行异步化调用,主要是避免通过不断新建线程或通过线城池来实现并发引起的线程瓶颈。

案例分析:
一个服务的调用顺序流程图如下,

通过调用的链路分析,可以对蓝色方框的依赖进行利用线程池进行并发调用,这样高并发的时候会很容易造成线程太多造成瓶颈,举个例子:

服务接受到request,开启一个jetty的线程,这个线程会并发调用10个依赖服务,则会从线程池取出10个线程进行并发。当request并发量到达1000,会对线程的需求放大至1w,假设每个依赖耗时100ms,每秒处理10个依赖,需要对线程池大小为1w/10=1000个线程,这是不能接受的,因为海量线程带来的非堆内存占用、上下文切换等开销,会使得进程不堪重负,稳定性下降,更早的达到性能瓶颈。

改造思路:并发+异步改造,底层服务采用NIO等方式去发起请求和返回结果,依靠事件驱动,在业务层将其转换为可组合为CompletableFuture(java8)ListenableFuture(java7,基于guava库),不再需要线程池去等待每一个依赖的响应而不再需要业务线程去等待,这样线程池不会出现瓶颈。


改造后流程图:



延伸:这里只讨论依赖服务异步化调用,整个服务并没有异步化,在服务接口层也是可以异步化的,详细的调研结果可以参考文档,https://www.ibm.com/developerworks/cn/java/j-lo-servlet30/

3.服务框架选型

因为历史原因,项目的restful接口一直采用restlet框架提供服务。可能当时公司成立之初,springMVC技术还没成熟,采用更稳妥的restlet框架。

restlet容易暴露的问题:

(1)restlet性能比springMVC差, 性能对比结果是:裸Servlet > SpringMVC >> Restlet

(2)对拦截器的支持,springMVC可以定义Interceptor,restlet要通过maven-aspectj编译期织入打点机制。


4.JVM 调优


JVM调优一般来说都是出问题或告警的时候注意进行优化,这块可谓“水无常形 兵无常势”,具体问题具体分析。

回收器的选择:

   - CMS?

   - G1?

      ....

关键参数
     – 决定Heap大小:-xms -xmx –xmn(-xms=-xmx) ,初始堆的大小==可调堆最大值,避免堆动荡

         ● 取决与操作系统位数和CPU能力
         ● 过小GC频繁,过大GC中断时间过长

    – Eden/From/To:决定YounGC,如:-XX:SurvivorRatio
    
– 新生代存活周期:决定FullGC,如: XX:MaxTenuringThreshold

 新生代/旧生代  

    – 避免新生代设置过小

      ● 频繁YoungGC
      ● 大对象,From/To不足拿Old增长快,FullGC

   – 避免新生代设置过大
      ● 旧生代变小,频繁FullGC
      ● 新生代变大,YoungGC更耗时

  – 对于我们组大部分系统,可以分配 : 新生代:Heap=33%Young:Old=1:2

5.序列化和反序列化协议优化

高效的序列化协议不仅可以提高系统的通用性、强健性、安全性、优化系统性能,而且会让系统更加易于调试、便于扩展。在做CPU热点分析的时候,发现redis序列化和反序列化占比非常高,已经在重构优化中逐渐成为瓶颈。可以调研从可否redis使用不同hessian版本,能否支持异步,利用异步化提高性能。后期遇到什么问题再补充。

针对序列化协议的学习,可以参考http://tech.meituan.com/serialization_vs_deserialization.html。

四、优化总结

1.利用性能诊断工具,依靠数据驱动,不能凭感觉

2.见招拆招,具体问题具体分析

1 0