架构师训练营第七周课程笔记及心得
性能测试:系统性能的主要技术指标
性能测试:
性能测试是性能优化的前提和基础,也是性能优化结果的检查和度量标准。不同的视角下的网站性能有不同的标准,也有不同的优化手段。
主观角度:
用户感受到的性能,可能服务端的性能是一样的,但是通过前端展示的方法不同可以带给用户不一样的体验
客观视角:
性能指标衡量的性能,就是服务器处理过程,速度,耗时,等客观的指标
性能测试指标:
不同的视角下有不同的性能标准,不同的标准有不同的性能测试指标,网站性能测试的主要指标有:
响应时间:指应用系统从发出请求开始收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标,只管翻译了系统的“快慢”,基本上等同于用户感受到的时间。
并发数:系统能狗同时处理请求的数量,这个数字也反应了系统负载的特性。对于网站而言,并发数即系统并发用户数量,指同时提交请求的用户数目,与此相应的,还有在线用户数(登录系统的用户数量)和系统用户数量(可能访系统的总用户数)
吞吐量:指单位时间内系统处理的请求数量,体现系统的处理能力。对于网站,可能以“请求数/秒”或者“页面数/秒”来衡量,也可以用“访问人数/天”或者是“处理的业务数量/小时”等来衡量。
性能计数器:是一类数据指标,是描述服务器或者操作系统性能的一些数据指标。包括System Load、对象与线程数、内存使用率、CPU使用率、磁盘与网络I/O等指标。这些指标也是系统监控的重要参数,对这些指标设置报警阀值,当监控系统发现性能计数器超过阀值的时候,就向运维人员和开发人员报警,及时发现处理系统异常。
性能测试方法:
性能测试是一个总称,具体可细分为性能测试、负载测试、压力测试、稳定性测试。
性能测试:
以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受范围能否达到预期
负载测试:
对系统不断的增加并发请求以增加系统压力,指导系统的某项或多项性能指标达到安全临界值,如某种资源已经呈饱和状态,这时继续对系统施加压力,系统的处理能力不但不能提高,反而会下降。
压力测试:
超过安全负载的情况下,对系统继续施加压力,指导系统崩溃或不能再处理任何请求,一次获得系统最大压力承受能例。
稳定性测试:
被测试系统再特定硬件、软件、网络环境条件下,给系统加载一定业务压力,是系统运行一段较长的时间,以此检测系统是否稳定。在生产环境,请求压力是不均匀的,呈波浪特性,因此为了更好的模拟生产环境,稳定性测试也应不均匀的对系统施加压力。
上述测试遵循下图曲线:
在不断的增加并发数(系统资源)情况下,来计算系统资源(并发数和系统资源呈线性关系)和tps的关系在生产上运行的时候,架构师需要考虑能承受的风险和部署的成本
与上图对应的,系统响应时间与并发用户数的对应关系:
性能测试使用场景:
在做性能优化的过程中,也可以构建这样的图表,来对比优化前后,系统性能的对比。
性能测试压测可用性
全链路压测的挑战
什么是全链路压测:
在特定的业务场景下,将相关的链路完整的串联起来同时施压,尽可能的模拟出真是的用户行为,当系统整站流量都被打上来的时候,必定会暴露出性能瓶颈,才能够探测出系统整体的真正的处理能力,以及有指导的在大促前进行容量规划和性能优化,这便是线上试试全链路压测的真正目的
全链路压测的必要性:
在高并发的情况下,在什么样的地方会出现问题,并不是非常明确的,并且各种接口的并发耦合在一起的时候,那一块会成为短板,这是就需要全链路压测才能发现问题所在
全链路压测难点:
压测相关的业务系统上众多,并且牵涉到整条链路上所有的基础设施和中间件:
如何确保压测流量能够通畅无阻,没有死角?
压测的数据怎么构造?
数据模型如何与真是贴近?
直接在线上进行真是环境进行模拟,怎么样来保障对线上无影响?
大型促销活动所带来的巨大流量是要怎么样制造出来?
数据构造:
数据隔离:
逻辑隔离,直接吧测试数据和正常数据写到一起,通过特殊的表示能够区分开:可能污染线上数据,破坏线上数据的安全性
虚拟隔离,在所有写数据的地方做mock,并不真正的写进去:不会对线上数据产生影响,但是mock对压测的结果准确性会产生干扰
物理隔离,所有写数据的地方对压测流量进行识别,判断一旦是压测流量的写,就写到隔离的位置,包括存储,缓存,搜索引擎等等
流量构造:
天猫双十一全链路压测的流量平台:
性能优化-系统性能优化分层思想:
两个软件性能优化的原则:
不能优化一个没有测试的软件
不能优化一个不了解的软件
性能优化的一般方法:
性能测试,获得性能指标
指标分析,发现性能与资源瓶颈点
架构与代码分析,寻找性能与资源瓶颈关键所在
架构与代码及其他优化,优化关键技术点,平衡资源利用
性能测试,进入性能优化闭环
系统性能优化的分层思想:
机房与骨干网络性能优化:异地多活,专线网络与自主CDN建设
服务器与硬件的性能优化:使用更好的CPU,内存,磁盘,网卡,对软件性能优化是数量级的,有时候远超代码,架构的优化
操作系统性能优化:操作系统内核参数优化
虚拟机性能优化:jvm虚拟机优化
基础组件性能优化:dbcp,jboss,apache,jetty等组件优化
软件架构性能优化:三板斧——缓存、异步、集群
软件代码性能优化:遵循面向对象的设计原则与设计模式变成,很多时候程序性能不好不是应为性能上有什么技术挑战,仅仅就是因为代码太烂了
案例:Spark性能优化
从性能测试的指标来分析性能状况
指标观测:通过工具监控一些spark的各种指标
操作系统:计算机如何处理成百上千的并发请求
程序运行时架构:
程序是静态的。程序运行起来以后,被称作进程。
操作系统多任务运行环境:
计算机的cpu核心数是有限的。但是服务器可以同时处理数以百计升值数以千计的并发用户请求
如何做到多任务运行:
进程分时执行
进程的运行期状态:
运行:当一个进程在cpu上运行时,该进程即处于运行状态。处于运行状态的进程数目小于等于cpu数目
就绪:当一个进程获得了除cpu意外的一切所需资源,只要得到cpu即可运行,则此进程处于就绪状态,就绪状态有时也被称为等待运行状态。
阻塞:也称为等待或睡眠状态,当一个进程正在等待某一事件发生(如等待I/O完成,等待锁等等)而暂时停止运行,这是即使把cpu分配给进程也无法运行,即称改进程处于阻塞状态。
进程vs线程:
不同的进程轮流在cpu上执行,每次都要进行进程间cpu切换,代价非常打。因此服务器应用通常是单进程多线程。进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。而每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。
线程栈:
每个线程都有自己的线程栈空间,线程在执行代码的过程中,每个线程内部的参数,是各自分开放在各自的线程栈内的,而不是线程间共享的,使得即使运行的是相同的代码逻辑,但是不同的用户请求进来时,处理的不同用户数据可以相互隔离
为什么使用栈:
栈是先进后出的,在程序执行过程中,线程开始执行是,线程栈会先分配一个栈帧,在某个可能重复定义的变量参数执行时,最新的函数执行的这个变量参数都会放在最顶部的栈帧中,等最后的函数返回后,最顶部的栈帧释放,才会读取到前面的栈帧中的相同函数的变量参数,以防止在各种迭代,嵌套的函数中,相同的变量参数之间不会被混淆调用并且保持调用顺序。保证了即使在处理过程中cpu时间片切换,再次被cpu执行是也只用直接到栈顶读取相关变量参数,不用到处寻找,就能直接恢复线程栈的运行。
Java web应用多线程运行时视图:资源分布视图
线程安全:
当某些代码修改内存堆(进程共享内存)里的数据时,如果是多个线程同时进行,就可能出现多个线程同时修改数据的情况
比如,两个线程同时对一个堆中的数据执行+1操作,最终这个数据只会被加一次,这就是人们常说的线程安全问题,实际上线程的结果应该时依次+1,即最终结果应该是+2
产生线程安全问题的原因:
栈中存放的是临时的变量的引用,而堆中的,是栈中临时,局部变量指向的才是这个变量实际的内容,以及实际内容的存放的内存空间。
线程安全解决方法——临界区:
多个线程访问共享资源的这段代码被称为临界区,解决线程安全问题的主要方法是使用锁,将临界去的代码加锁,只有获得锁的线程才能执行临界区代码
阻塞导致高并发系统崩溃:
锁,解决了线程安全的问题,但是越来越多的线程等待同一个锁会引起死锁,线程阻塞等问题。阻塞导致线程既不能继续执行,也不释放资源。进而导致资源耗尽。最终引起系统崩溃。
避免线程阻塞引起程序崩溃:
限流:控制进入计算机的请求数,进而减少创建的线程数
降级:关闭部分程序的执行,尽早释放线程
反应式:异步;无临界区(Actor模型)
锁原语CAS与各类锁
什么是锁原语:
锁本身也是一种执行的线程,也存在安全问题,这个时候也会出现锁本身临界区问题,为了保证锁本身在读写过程中不会出现多个线程加锁,获取锁状态不一致的情况使用了锁原语。
锁原语CAS(V,E,N):
V:表示要更新的变量
E:表示预期值
N:表示新值
例:java中通过CAS原语在对象头中修改Mark Word实现加锁:
上述java中的几种锁:
偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
轻量级锁:指当所示偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁:当锁是轻量级锁是,另一个线程虽然自旋,但自旋不会一直下去,当自旋到一定次数是,还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低
多CPU情况下的锁:
CAS原语不允许被中断的条件是在单个cpu中执行的情况下的,在多个cpu情况下时:
多个cpu的场景下,会使得多个cas原语同时执行,这时会使得多个cas同时执行失败的情况
解决多cpu场景下cas获取锁的问题(硬件支持):
总线锁:使用cpu的LOCK#信号,当一个cpu在内存总线上输出此信号的时候,其他cpu的请求将被阻塞,该处理其独占内存。
缓存锁:指内存区域如果被缓存在cpu的缓存行中,并且在lock操作期间被锁定,那么当它执行锁操作诙谐到内存时,cpu不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许他的缓存一致性机制(缓存锁的方式)来保证操作的原子性,因为缓存一致性机制会组织同时修改两个以上cpu缓存的内存区域数据,当其他cpu回写已被锁定缓存行的数据时,会使得缓存行无效
其他锁的一些概念:
公平锁:就是多个线程按照申请锁的顺序来获取的锁
非公平锁:就是多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能会造成饥饿现象
可重入锁:就是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
独享、互斥锁:该锁只能被一个线程所持有
共享锁:该锁可以被多个线程所持有,如果所有线程共享就没有意义了,所以可以设置共享条件,共享的线程需满足一定条件才能获得锁,如共享数量等
读写锁:多个读线程之间并不互斥,而写线程则要求与任何线程互斥
悲观锁:对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取的加锁形式。悲观的认为,不加锁的并发操作一定会出现问题。
乐观锁:相对于悲观锁,对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,检查是否已经被修改过,就放弃。例:如读写一个数据时,在读取的时候不加锁,但是回写的时候会对比版本如果已经发生过修改不是读取时的版本就放弃,如果还是读进来的版本就进行修改操作。严格来说乐观锁并不是一种锁,更像是一种数据读写的一种保证一致性的方法
分段锁:分段锁设计的目的是对数据的颗粒度更加细腻的控制,当操作不需要更新整个数组的时候,就仅仅针对数组的一段进行加锁操作。例:JDK ConcurrentHashMap就是通过分段锁的形式来实现搞笑并发操作的。
自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,但是自旋重复尝试的过程会消耗cpu。例:java中的轻量锁
案例:异步并发分布式变成框架akka
版权声明: 本文为 InfoQ 作者【Airs】的原创文章。
原文链接:【http://xie.infoq.cn/article/503cc8f0b0c0b2cbbf22d2822】。未经作者许可,禁止转载。
评论