前言
在【性能优化(上)】你真的了解你的系统吗?中,我们了解了如何来评估自己的系统,了解了各种系统评估指标。今天这篇文章主要是聊一下如何确定我们的优化方向,和一些引起性能问题的常见场景和优化手段。
如何确定优化方向
jvm调优?SQL优化?重构代码?到底从哪里开始着手
优化手段千千万,但是我们怎么来确定我们的优化方向呢? 一上来咔咔咔就是一通JVM参数调整, 一顿操作猛如虎,一看效果0.5。直接进行JVM调优可以吗? 可以,但是没必要。 我们应该要先找到系统的最大的性能瓶颈开始入手,收益才是最大的。
如何寻找系统性能瓶颈
监控
我们可以通过各种监控手段来找到系统中的性能瓶颈,比如:
- 慢查询日志,MYSQL、Redis等慢查询日志,SQL-> 接口 ->功能 性能问题
- APM链路监控,对于微服务架构,APM是必须的。通过APM链路的统计,我们可以获取包括RT99线、耗时请求排序、链路分析、SLA等大量的有效指标,对于我们分析性能瓶颈或性能问题有极大帮助
...
APM可以选择自研,也可以采用开源框架,现在可选的框架有很多,包括:skywalking、zipkin、pinpoint、esapm等,各有优劣,大家可以自行对比选型。
全链路压测
如果通过上面的手段都无法找到性能瓶颈,那么还有一个大招,就是全链路压测。在当前的场景下没有瓶颈,不代表在请求量增加后,还是没有。
当你在以下几种情况时,可以考虑进行一次全链路压测:
- 当前的访问量级下无法找到系统瓶颈
- 有明确的性能目标
- 确定系统的极限指标
全链路压测有诸多好处,只有一个坏处,贵! 在系统规模较大的情况下,一次压测可能要花费数十万元的费用。
前端反馈
最后一个是前端用户反馈,比如用户反馈某个功能卡顿、响应时间超长等等,这个是需要马上进行优化的。
不过如果等到用户来反馈性能问题,那么这个系统的问题真的已经在大到一定程度了。
常见性能问题和解决
下面我们说一下一些常见的导致性能问题的场景和处理办法。
设计问题
首先是设计问题,在设计上存在问题或考虑不周全可能会导致性能问题
请求阻塞
小事件,大问题。 处理请求速度变最导致线程堆积
请求阻塞最常见的发生原因就是由于个别接口的异常影响到全局,导致整个服务不可用。比如某个请求可能导致:
CPU使用率上升 -> 影响RT -> 线程继续堆积
内存使用率上升 -> FullGC可回收 -> 继续上升 -> FullGC
内存使用率上升 -> 不可回收 -> OOM
常见场景
请求阻塞的常见场景可能有以下这些:
- 依赖的某个下游服务异常,导致服务本身的逻辑处理线程堆积
- 要但是很重的业务逻辑和核心逻辑耦合在一起
- 询,MySQL或Redis存在慢查询
- ...
举个栗子
def login(username, password):
"""
用户登录接口
"""
# do something
user = doLogin(username, password)
report(user)
def report(user):
# 数据上报 timeout = -1即无限等待
http.post(user, timouet = -1)
上面是一段很简单的登录逻辑,先登录,登录成功之后需要把事件上报到数据平台。这里面存在什么问题?
- 非核心功能和核心功能强耦合,假如数据上报接口出现异常或超时,会直接导致所有用户无法登录
- 接口未设置超时间,所以对系统外部接口的调用,尽量都设置超时时间
上面的代码就很可能会导致请阻塞,进而导致服务不可用。那我们可以怎么来优化?
参考
- 异步化
- 增加异常处理
- 增加超时间
- 增加熔断和限流措施
如何避免
- 隔离,核心逻辑和非核心逻辑隔离,通过异步化、拆分、或者设计上隔离
- 超时时间,所有的外部依赖最好都要有超时时间设置
- 熔断保护,外部服务要有对应的熔断措施
- 良好的SQL和代码习惯,全面的测试(并发、异常)
请求扩散
指数级流量, 用户的一次请求,对应了服务端的多个请求,服务端的每一次请求,可能又对应了下层服务的多个请求,层级越多,产生杠杆效应。
流量激增的情况下,可能导致下游服务承受指数级的压力,无法通过线性的增加机器解决问题
用户的一次请求,可能对应了服务端的多个请求,而服务端的每一次请求,可能又对应了下一层服务的多个请求。 请求的层级越多,则会产生杠杆效应,特别是在种微服务化的分布式系统中,处于深层次的服务需要处理大量的请求,很有可能成为系统瓶颈。而且如果处理的数据量很大的话,也会给网络带来巨大的压力,网络也有可能崩溃。
常见场景
请求扩散常见的场景可能有以下这些:
- 流量高峰期,前端页面可能由于各种原因刷新不出来,用户疯狂的刷新
- 上层服务请求瞬间增加5倍,开发心里暗喜,提前扩容果然是明智之举,底层服务直接被50倍流量打跨,当场暴毙
- ...
如何避免
- 前端减少无效请求
- 合理的合并多个请求,减少请求数量
- 适当的增加缓存,将请求拦截在上层
- 设计时站在全局,考虑上下游服务影响,站得高看得远
缓存爆炸
无限制的本地缓存
常来说,缓存数据越多,命中率越高,响应时间越快。在正常场景下,对性能有极大的提升。但是如果无限制使用的缓存,当出现流量高峰,可能会导致服务宕机。优化手段变成自己挖的坑。
常见场景
缓存爆炸常见的场景可能有以下这些:
- 系统中无差别使用缓存,没有对冷热数据分类
- 缓存不设置过期时间
如何避免
- 考虑缓存的命中率和失效情况,权衡是否需要引入缓存
- 控制缓存大小,避免无限增大
- 控制缓存过期时间,要有对应的过期策略
- ...
锁
我们在开发的时候,不可避免的要用上各种各样的锁,锁真的会影响性能吗?
当然会!但是一般情况下竞争才是锁影响性能的元凶
比如Java,当锁出现大量竞争时,会出现锁升级的现象,从偏向锁最终上升到重量级锁,在等待和唤醒时都需要进行系统调用。 关于Java中的锁,大家可以查阅相关资料~
常见场景
由锁引起的性能问题常见的场景可能有以下这些:
- 锁范围过大,比如直接一个synchronize修饰一个方法,但实际上可能只是其中某几行代码有共享数据
- 锁粒度过大,比如更新订单时增加一个分布式锁,直接进行全局加锁,而不是只针对订单ID来加锁
- 在加锁范围内存在不必要的耗时操作,比如网络IO
如何避免
- 减少持有锁的时间,不要在里面做耗时操作
- 减小锁粒度,能锁两行代码就解决的问题,就不要锁一个方法
- 适当场景下使用无锁编程,比如在高并发且大部分时候没有数据冲突的时候,使用CAS
常见框架中的锁性能优化:
- Druid和hikariCP两个DB连接池,HikariCP中通过大量的CAS操作代替了锁,在压测中性能表现比Druid好了近一倍,缺点就是CAS会导致> CPU使用率上升
- log4j2使用disruptor并发框架(大量无锁)代替锁操作,在压测中的性能表现比log4j和logback要好非常多
连接池
连接池是越大越好吗?
我们在使用各种线程池,或者在使用数据连接池时,有没有遇到过获取连接速度很慢的情况? 你的反应是连接池不够用,然后增大连接池数量?
连接池真的是越大越好吗?
CPU在进行线程切换时,是存在切换上下文成本的,多线程的目的是为了更好的利用CPU的空闲等待时间(比如DB连接的网络IO)。但是当我们线程内的程序处理速度非常快的时候,频繁的进行上下文切换反而会拖慢我们程序的速度。
如何合理的设置连接池
参考HikariCP官网推荐的一个经验值是:connections = ((core_count * 2) + effective_spindle_count) ,比如一块硬盘的4核服务器,那么可以将连接池设置为 (4 * 2) + 1 = 9,生产中可以根据实际情况上下浮动。
我们在压测过程中,有一个阶段就是瓶颈被卡在了DB连接上,表现出来的现象就是在APM链路中,大部分的耗时是在getConnection方法上。我们的应用实例数大概在100台,DB连接池配置是
max-active = 60。在遇到这个瓶颈时,最初始的尝试增加连接数,修改max-active=80,一直增加到120,瓶颈还是在,并且更慢了。 最终尝试降低连接数,将max-active = 10,瓶颈消失了,性能指标上升了近2倍。
重视CPU上下文切换造成的影响。合理的设置所有线程池的数量,线程不是越多越好
兜底手段
兜底,熔断、限流必不可少
性能再怎么优化,我们的系统也有可能遭受到预期之外的流量峰值,那这时候我们需要有对应的兜底手段,保证服务的可用定。
设定保护数值
我们在系统的设计阶段,需要确定系统的保护数值:比如RT到达多少服务即不可用,线程到达多少内存使用量就超标了实时告警
系统开始恶化时,可以及时报警,通知到相关的负责人自动降级 & 人工介入
系统异常时,可以自动降级,如果系统做不到,可以人工介入,控制降级服务分级
系统需要分区服务的等级, 要将核心服务和可选服务区分,在降级时可以完全隔离自动恢复
服务恢复正常时,可以自动恢复
结语
最后,我们总结一个整个性能优化篇的内容:
- 了解自己负责的服务性能指标,在优化时有的放矢
- 通过设计进行优化,提升系统可用性
- 减小锁对性能的影响
- 避免频繁的上下文切换
- 不管怎么优化,都要有兜底手段