关于服务器性能优化的思考和实践

完善中...

每年双十一之前,参加双十一保障的各个应用都需要对各自的性能进行摸底,再结合双十一的流量评估、集团对机器数量的要求、机器使用率的考核来进行容量评估和性能分析优化。

一般来说机器容量评估的维度有下面几个方面:

  • 双十一的流量,根据经验非核心交易链路的应用流量一般在平时的3到4倍,一般会按4到5倍流量来进行评估。
  • 机器数量,对于每个大的业务部门,总的机器有一个池子,整个团队进行共享,所以针对具体的保障应用的团队来说,其能申请到的机器数也不是无限的,需要根据自己应用的流量评估还有性能摸底在大团队内分享机器,所以如果自己应用的性能很差占用过多的资源,自己也会很丢脸。
  • 机器使用率,近年来集团对大促期间线上机器的使用率有了一定的要求,以避免过度申请机器造成浪费的情况,同时也要求应用的保障人员对自己业务的预估、对应用的性能情况、对应用大促期间的表现要有更好的把握。

下面以一个大流量系统的双十一前性能压测和优化为例来总体的总结下一个大流量高并发的系统如何进行性能预估、压测和优化。

当前应用单机运行情况如下:

维度 当前数据 压测指标 双十一指标
cpu 30% 50% 50%
load 1.5~2.1 4 4
mem使用率
gc
qps 90 400 400
rt 70 40 40
xms
线程数
接收消息
发送消息
缓存
数据库
rpc调用
日志

面对这样一个应用和场景,希望能通过在实际的性能排查和优化的操作来总结出一些思路和常用的手段方法。

1、性能分析的指标和理论

在之前列出的单机运行情况中,我们考核一个应用的性能的指标可以分为两大类:

  1. 机器的性能指标:cpu、load、mem
  2. 应用的性能指标:qps、rt

而我们的目标是在满足机器性能指标的前提下来达到甚至超过应用性能指标。

那么面对这样一个大流量系统的单机运行情况和目标,从何下手是首先应该考虑的问题。

所以第一步我们需要弄清楚他们之间的关系,比如:rt是由什么构成、qps由谁决定、load和cpu使用率的关系、mem如何影响load和qps、其他影响load的因素。

如果对这些还没有了解或者没有清晰认识的同学可以先看下这篇文章关于服务器性能的思考

从这篇文章中我们可以得出几个结论:

  1. 系统性能压测中需要密切关注load,一般load数不要超过cpu核数,load偏高带来的最直接后果就是rt升高应用吞吐下降。如果load过高,优先解决load的问题。
  2. load和cpu没有必然的关系,低cpu高load、高cpu低load、高cpu高load都有可能。导致load高有许多因素,如:频繁gc、io瓶颈(网络和磁盘)、锁竞争、上下文切换等。
  3. 在平均load情况下,应用性能指标中qps、rt、最佳线程数有密切关系,同时在做性能优化的时候,也要明确系统的瓶颈在哪里,是想提高qps还是降低rt,因为二者的思路是完全不同的。

2、性能分析的思路和方向

大型业务中,系统间的关系会变得非常复杂,上下游的调用依赖、存储、缓存、消息的流转、业务量的变化都有可能影响并制约系统运行的情况。

所以在进行系统性能分析的时候我们首要是搞清楚系统的上下游关系、系统的核心链路和分支链路的业务量、存储的容量能力和瓶颈、应用机器的数量和负载情况、业务的变化和峰值预估数据等。

下图是对XXC这个系统的依赖关系分析,以及双十一期间各个业务量的预估,其中关键点有写入消息xx万、列表读xx万、db读写xx万。所以在系统能力分析和测试的时候,我们要密切关注这三个点,以及系统的性能表现。

系统依赖分析

3、性能分析的方法和工具

找到了影响系统性能表现的关键路径,接下来需要构建符合系统业务的压测数据模型。通过压测,观察和分析系统的运行情况,找到性能瓶颈,优化并解决。再次压测分析和优化,重复以上步骤直达达到预定指标。

3.1、压测

3.1.1 压测模型

3.1.2 数据离散

3.2、调用链路分析

3.3、jstack

3.4、jmap

3.5、pref

4、JVM和系统层面

4.1 JVM调优

4.2 线程数&线程切换

5、应用设计层面

5.1 调用放大

  • 重复查询
  • 批量查询

5.2 接口rt

讨论服务器性能的时候我们知道,在系统平均load情况下,接口的rt越低,接口的qps就会越高,应用的性能指标就会越高。可以从两个方面入手:

  • 进一步降低qps最高的接口rt

    XXC应用qps最高的手淘查询接口中,需要根据交易id去查询交易信息,但是交易应用采用单元化部署,而XXC采用中心化部署,会产生跨机房调用,一次跨机房调用的消耗在20~30ms,而该查询接口总rt在60ms左右,即将近一半的时间消耗在跨机房调用上。

    优化方案,采用交易的中心缓存接口,一次查询消耗降低到1ms左右,手淘查询接口的rt从60ms降低到30ms左右,同等条件下该接口性能相当于提升一倍。

    但是需要考虑中心机房和单元化机房数据同步的问题,特别是大促高峰期,单元直接的数据同步会延时,所以在查询交易时,如果缓存接口失败需要再查单元化接口。

  • 进一步降低rt最高的接口rt或迁移接口

    XXC应用中有两个接口rt在600ms左右,压测时候发现虽然这两个接口的调用量只占总量的5%,但是应用资源消耗上占到20%左右。所以应用上要特别注意rt非常高的接口,这些接口可能请求不是特别高,但是他们对系统带来的影响同样不能忽视。

    对于XXC应用中高rt的接口,从代码上进行接口耗时分析,发现这是一个根据ids进行批量获取的接口,而每一个id都会进行一次后端爬虫服务的调用,导致接口整体rt就非常高。再仔细从业务角度分析,发现ids中大部分在不同业务场景下是不需要去调用后端服务的,因此通过业务规则的判断,将接口的整体rt降到200ms以下,再次压测同等条件该接口的应用资源消耗下降到7%左右。

5.3 热点方法

尝试查找系统是否有被频繁调用的hot method。下图是XXC系统中热点方法的统计,可以看到LocFeatureCodec相关的方法调用大约占了总方法调用次数的30%以上。经过代码分析,LocFeatureCodec是用来按照一定的格式解析feature字符串,并转换为map对象。在一次rpc调用过程中,LocFeatureCodec可能会被解析10多次,所以导致其方法调用次数占比很高。

解决方案,使用localcache缓存feature的解析结果,避免多次重复解析feature。

5.4 合理使用本地缓存

在使用localcache的时候,需要注意以下几点:

  • LocalCache的大小、失效时间、是否启用必须要可控,最好不要自己动手写
  • 必须要保证上层模块不会修改LocalCache中的对象的属性
  • 尽量输出LocalCache的命中率,用于帮助调整LocalCache参数
  • LocalCache需要识别压测流量,避免数据污染
  • LocalCache的具体大小需要结合对象的大小和应用当前内存情况来确定,小心LocalCache过大撑爆内存
  • 关闭LocalCache时,最好主动清空下LocalCache中的数据,虽然LocalCache中的数据有失效时间,但guava cache并不会维护一个线程定时去清理

5.5 强弱依赖梳理

5.6 存储(DB&缓存&hbase)

5.7 内存优化-大内存移除

XXC应用的内存配置为,-Xms4g -Xmx4g -Xmn2g -XX:CMSInitiatingOccupancyFraction=80,在应用重启的时候老年代约为1G,占比50%,CMS GC触发比例为80%,老年代可用比例偏低,线上系统大约10个小时一次fullgc。

通过dump内存分析,存在着两块比较大但是使用频率很低的内存块:

  • 国际地址库前置缓存,在XXC中有一处需要判断是否是新马泰收货地址的订单的分支逻辑,但是为此引入了整个国际地址库的前置缓存,这块内存大约300M。解决方案使用国际地址库rpc接口代替,移除该缓存。
  • 包裹面单前置缓存,同样也有一处对包裹面单处理的分支逻辑,引入了包裹面单前置缓存约100M,移除该缓存通过rpc接口代替。

在移除两块前置缓存之后,应用重启之后老年代下降到600M,线上系统fullgc频率拉长到一天左右。

很多数据类的应用,往往是读多写少,更新频率很低,为了提高接口性能,这些应用的client一般都会同时提供缓存接口和rpc接口。因此在使用这类接口时需要根据业务场景去做具体的分析,看看自己的应用中对这类数据的使用频率是多少,如果本来使用的频率就不是很高,则可以考虑直接使用rpc接口,避免引入不需要的数据到自己的系统中。

6、语言使用层面

6.1 序列化

合适的序列化方式,对于应用的性能和使用都有很大的帮助,特别是在大量使用缓存和消息系统的应用中。
通过对常用序列化方式对比,可以从序列化的性能、空间消耗、易用性上进行选择。json在性能和空间上都比较折中,使用上很简单,并且结果清晰便于检查,根据使用习惯和场景可以选择fastjson、jackson、gson等。如果追求性能则可以选择hessian和kryo,对性能和空间有更高的要求可以选择protobuf,不过protobuf不是自说明式的序列化方式,它需要额外的类型说明文件,使用上稍有不便。如果对性能和空间没有要求而且追求使用简便,或者应用中使用序列化的地方很少,则可以使用java原生序列化方式。

6.2 bean拷贝

在应用中有时候会频繁的使用bean拷贝,bean拷贝的性能也会影响到应用的性能和运行情况。常用的bean拷贝方式有:

  • org.springframework.beans.BeanUtils: copyProperties
  • org.apache.commons.beanutils.BeanUtils:copyProperties
  • org.springframework.cglib.beans.BeanCopier:copy
  • set/get方法

他们的性能排行:
set/get方法>BeanCopier.create>>>BeanUtils.copyProperties(spring)>>BeanUtils.copyProperties(apache) ,所以在选择bean拷贝方式时,拷贝最频繁的类使用set/get方法,其次可以使用BeanCopier.create。

序列化也是实现bean拷贝的一种方式,先将对象序列化再将结果反序列化得到结果,但是序列化需要额外的存储空间。

6.3 日志异步化&日志裁剪

频繁的打印日志,会大大增加磁盘的io,对应用性能产生不利影响,在不改变日志打印级别的前提下,可以考虑对日志进行裁剪和使用异步打印。

  • 在应用中不要打印无关内容的日志,不要乱使用日志级别,日志内容尽量简明扼要。

  • 注意控制异常堆栈的层级,某些极端场景下,比如限流、超时,短时间会抛出大量异常,如果频繁的输出完整的堆栈,只会加剧系统当前的负载。

    采用logback输出日志,可以在配置文件的pattern中加入 %ex{7}用于控制堆栈输出的深度。
    采用log4j输出日志,还没找到类似的参数,一种方案是重写log4j appender的subAppend方法裁剪堆栈

  • 如果应用可以接受丢失部分日志,则可以选择异步打印日志,配置合理的log buffer,批量打印会大大减少对磁盘io压力。

6.4 SimpleDateFormat

代码中涉及到日期的格式化,一般会使用到SimpleDateFormat,比较常见的用法是在需要的时候生成一个SimpleDateFormat对象

SimpleDateFormat dateFormat = new SimpleDateFormat(format);
return dateFormat.format(date);

如果频繁的进行日期格式化,则需要大量生成SimpleDateFormat对象,而基本上日期格式的大同小异,可以考虑生成一个全局的格式化对象。

但是SimpleDateFormat不是线程安全的,所以需要使用JodaTime来生成一个全局的日期格式化对象,在真正需要进行日期格式化的时候使用全局对象即可。

6.5 异常

推荐阅读更多精彩内容

  • 一、服务器性能 平常的工作中,在衡量服务器的性能时,经常会涉及到几个指标,load、cpu、mem、qps、rt,...
    zqrferrari阅读 1,044评论 0 8
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 124,772评论 16 535
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 70,839评论 12 116
  • 课程任务 1.注册 GitHub 账号, 把github用户名发送给老师 账号:pededa 已发送老师 2.看G...
    呦泥酷阅读 28评论 0 0
  • 十年前,因为不敢干,你错过了; 五年前,因为不相信,你拒绝了; 三年前,因为不可能,你放弃了! 今天,因为怀疑,你...
    妮说阅读 91评论 0 0