性能测试
性能优化的第一步是性能测试。通过性能测试的数据结果才能表明性能优化是否起作用。
性能标准分为主观和客观两种视角,主观能感受到的性能,并不一定要通过性能优化实现,也可以通过提升用户体验实现。技术人员更应该关注的是客观性能。
性能测试的四个主要指标
- 响应时间
- 应用系统从发出请求开始到收到最后响应数据所需要的时间,直接反应系统的『快慢』。
- 主管上的性能优化,可以在从开始到结束中间的时间点上做优化。比如在开始之后马上给用户反馈而不是等到结束后;
- 并发数
- 系统能够同时处理请求的数量,反应系统的负载特性
- 对网站而言,同时提交请求的用户数(并发用户数)当前登录系统的用户数(在线用户数),可访问系统的总用户数(系统用户数)
- 吞吐量
- 单位时间内系统处理请求的数量,体现系统的处理能力
- 对网站而言,可以使用『请求数/秒』或『页面数/秒』来衡量,也可以使用『访问人数/ 天』,『处理业务数/小时』来衡量
- 指标
- TPS(每秒事务数)
- HPS(每秒HTTP请求数)
- QPS(每秒查询数)
三者直接的换算
吞吐量=(1000/响应时间ms)*并发数
= (1000*并发数)/响应时间ms
= 并发数/响应时间s
并发数过大时,响应时间就越大,当到达极限,响应时间就会无限大,系统的吞吐量就成了零。
- 指标:性能计数器
描述服务器或操作系统性能的一些数据指标。包括System load,对象与线程数,内存使用率,cpu使用率,磁盘与网络IO等。
System load:正在处理的任务数+等待处理的任务数
当System load大于cpu核数时,表示有任务在等待,理想情况下应该小于等于cpu核数
是使用更多机器实现每台服务器的load少,还是使用少数服务器每台load高,是需要架构师权衡的
性能测试/负载测试/压力测试的区别
性能测试:以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统的资源可接受的范围内,是否能达到性能预期。
负载测试:对系统机型施加压力,直到系统的某项或多项性能达到安全临界值,当压力达到临界值,系统处理能力开始下降。
压力测试:在超过安全负载的情况下,继续施加压力,直到系统崩溃不能处理请求,以获得系统的最大承受能力。
架构师应该让系统运行在b点附近,而不是c点,因为很容易导致系统崩溃
系统运行在b点的左边还是右边,就体现在system load上,这是架构师应该权衡的。
性能优化
性能优化的两个基本原则
- 你不能优化一个没有测试的软件
- 没有测试就没有数据,没有数据说明系统有性能问题
- 不能只为了应用新技术就开始性能优化
- 你不能优化一个你不了解的软件
性能优化的一般方法
- 性能测试,获取性能指标
- 指标分析,获取性能与资源瓶颈点
- 架构与代码分析,寻找性能与资源瓶颈的关键所在
- 架构与代码优化,优化关键技术点,平衡资源利用
- 性能测试,进入性能优化闭环
系统性能优化的分层思想
- 机房与骨干网络性能优化
- 异地多活的多机房架构,用户访问就近的机房
- 专线网络与自主CDN建设
- 服务器与硬件
- 操作系统
- 虚拟机
- 基础组件
- 软件架构
- 软件代码性能
- 遵循面向对象的设计原则和设计模式编程,代码质量要有保证
- 并发编程,多线程与锁
- 资源复用,线程池与对象池
- 异步编程,生产者与消费者
- 数据结构,数组,链表,hash表,树
性能优化的三板斧
缓存
从内存获取数据,减少响应时间
减少数据库访问,降低存储设备负载压力
缓存结果对象,而不是原始数据,减少CPU计算
缓存主要优化读操作,对热点数据的读操作
异步
即时响应,更好的用户体验
控制消费速度,合适的负载压力
异步主要优化写操作
集群
横向扩展,前提是系统架构要支持
要满足扩展性的要求,新增减少节点的时候不能影响系统运行
优化相关的基础知识
操作系统
程序运行时架构
程序是静态的没有生命的,运行起来,程序才是『活的』,被称为『进程』。
多任务运行环境
CPU通过分时的方式执行进程,但只有被CPU调度到执行,才是真正的『活的』,我们感觉到服务器上有很多的进程在同时执行,只是因为分时的粒度足够小。
进程的生命周期:
运行:进程在CPU上正在运行的状态,处于运行状态的进程数量小于等于CPU的数量。
就绪:进程已经获取到了除CPU之外的其他资源,一旦得到CPU资源就能马上运行。
阻塞:也叫等待或睡眠状态。进程正在等待别的资源,比如等待IO完成,等待锁,而暂时停止运行,这是即使得到CPU资源也无法运行;
进程VS线程
不同进程轮流在CPU上执行,每次都需要进行CPU切换,代价非常大。
类似于进程与操作系统的关系,线程是轻量级的进程,线程从进程中获取内存地址,每个线程也有自己私有的内存地址范围。
多个线程可以真正的并发执行。
线程的生命周期与进程相同。
线程栈
先进先出
每个线程有自己的堆栈内存,执行自己的逻辑,避免不同线程之间产生影响。
每个正在执行的函数会有一个位于栈顶的栈帧,执行完之后就会被弹出。
函数内的变量以及运算,在栈帧中完成。
当stack的空间过小,或者方法调用层次过多(比如无限的递归调用),就会导致Stack Overflow异常的出现。
线程安全
当多个线程修改堆内存(进程共享内存)中的数据时,就可能出现线程安全问题。
函数内的变量是在栈中的,每个线程都有自己的栈内存,不会被其他的线程访问,所以函数中的变量不会有安全问题。
不同于基本数据类型是在栈中的,new一个对象时,对象是放在堆内存中的,每个线程的栈中存放的是对象的引用。
多个线程访问共享资源的这段代码被称为临界区,解决线程安全的方法是 将临界区的代码加锁,只有获得锁的线程才能执行临界区的代码。
锁会导致线程阻塞,阻塞导致线程既不能继续执行,也不能是否已经获取的资源,进而导致资源耗尽,最终导致系统崩溃。
如何避免阻塞引起的崩溃
- 限流:限制进入服务器的请求数,进而减少创建的线程数。
- 降级:关闭部分功能程序的执行,尽早释放线程
- 反应式:异步;无临界区(Actor模型)
锁
锁是如何实现的
CAS(compare and set)是一种系统原语,原语的执行必须是连续的,执行过程中不允许被中断。
CAS(V,E,N)
三个参数:
- v要更新的变量
- e预期值,无锁的
- n新值,加锁
Java中是通CAS原语在对象头中修改Mark Word实现加锁的
Java中三种锁类型
- 偏向锁
- 轻量级锁
- 重量级锁
多CPU情况下的锁
- 总线锁:锁定主存
- 缓存锁:不会锁定主存
锁的分类
缓存锁/可重入锁/公平锁、非公平锁/独享锁、互斥锁/共享锁/读写锁/自旋锁
- 乐观锁:乐观锁认为对于同一个数据的并发操作,是不会发生修改的,在更新数据时,检查是否已经被修改过,如果修改过,就放弃;
- 悲观锁:悲观锁认为对于同一个数据的并发操作,一定会发生修改。即时没有被修改也会认为被修改。悲观锁认为不加锁的并发一定会出问题。
存储
追求读写速度快,并且数据安全
硬盘raid
分布式文件系统HDFS
扩展
system load 的意义
发现问题比解决方案更重要,发现问题要靠经验和时间,不是能一蹴而就的。
知识不是知道就能用的。
线程栈。
慢网络请求导致系统崩溃的原理。
反应式编程为什么无临界区
缓存一致性协议
缓存锁/可重入锁/公平锁、非公平锁/独享锁、互斥锁/共享锁/读写锁/自旋锁
分段锁,ConcurrenHashMap的实现原理
一种无锁的实现:异步并发分布式编程,框架Akka
Actor编程模型