2020-07-18- 第七周学习总结

用户头像
路易斯李李李
关注
发布于: 2020 年 07 月 22 日

1 性能测试



性能测试,不同视角下网站的性能有不同的标准,也有不同的优化手段。主观视角下是用户感受到的性能,比如展示列表的速度等;客观视角下是性能指标衡量的性能,是量化后的数字,比如响应速度等。

1.1 软件性能优化的两个基本原则

  • 你不能优化一个没有测试的软件

  • 你不能优化一个你不了解的软件

性能的度量不应该个人主观的臆断,而是应该通过性能指标进行度量。

1.2 性能测试的主要指标



  • 响应时间:完成一次任务花费的时间,直观反映系统的“快慢”

  • 并发数:同时处理的任务数,反映系统的负载特性

  • 吞吐量:单位时间完成的任务数,反映系统的处理能力。

  • 性能计数器:数据指标,比如System Load,线程数,进程数,CPU、内存、磁盘、网络使用率等。

指标通常有:响应时间、并发数、吞吐量(TPS/HPS/QPS)

TPS每秒事务数,HPS每秒HTTP请求数,QPS每秒查询数



吞吐量 = (1000/响应时间ms)* 并发数

性能计数器:是描述服务器或操作系统性能的一些数据指标。比如System Load、对象与线程数、内存使用、CPU使用、磁盘与网络I/O等。通过监控这些指标,能够及时发现系统的异常行为。



1.3 性能测试方法



性能测试是一个总称,具体可细分为性能测试、负载测试、压力测试、稳定性测试。

性能测试以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受范围内,是否能达到性能预期。

负载测试对系统不断增加并发请求以增加系统压力,直到系统的某项或多项性能指标达到安全临界值,如果某种资源已经呈饱和状态,这时候继续对系统施加压力,系统的处理能力不但不能提高,反而会下降。

压力测试在超过安全负载的情况下,对系统继续施加压力,知道系统崩溃或者不能再处理任何请求,以此获得系统最大压力承受能力。

稳定性测试,被测试系统在特定硬件、软件、网络环境下,给系统加载一定业务压力,使系统运行一段较长时间,以此检测系统是否稳定。在生产环境,请求压力是不均匀的,呈波浪特性,因此为了更好地模拟生产环境,稳定性测试也应不均匀地对系统施加压力。

全链路压测其实指的是在特定的业务场景下,将相关的链路完整地串联起来同时施压,尽可能模拟出真实的用户行为,当系统整站流量都被打上来的时候,必定会暴露出性能瓶颈,才能够探测出系统整体的真实处理能力,以及有指导地在大促前进行容量规划和性能优化,这便是线上全链路压测的真正目的。



下图横轴是并发数,纵轴是系统吞吐量,可以看到当并发数在[0,a]性能测试区间时,系统的吞吐量是0。在[a-b]区间随着并发数的增加,系统吞吐量也随之增加,这一段接近于线性增长。在[b-c]负载测试区间继续增加并发数,系统吞吐量也在增加,但是增长很慢。到了c点,便是系统最大吞吐量。在[c-d]压力测试区间继续增加并发数,会使系统吞吐量骤减,这是因为并发请求越多,消耗的系统资源也就越多,线程不断争夺资源,而此时的并发请求数已经达到系统的极限,系统处理不了这么多的请求,响应时间变得越来越长,对应的系统吞吐量也就越来越小了。直到到达d点后,系统进入崩溃状态。







2 性能优化的一般方法



  • 性能测试,获得性能指标

  • 指标分析,发现性能与资源瓶颈点

  • 架构与代码分析,寻找性能与资源瓶颈关键所在

  • 架构与代码优化,优化关键技术点,平衡资源利用

  • 性能测试,进入性能优化闭环



2.1 系统性能优化的分层思想



  • 机房与骨干网路性能优化,异地多活的多机房架构,专线网络与自主CDN建设

  • 服务器与硬件性能优化,垂直伸缩优化,使用更优的CPU,磁盘,内存,网卡,对软件的性能优化可能是数量级的,有时候远远超过代码和架构的性能优化。

  • 操作系统性能优化,用户指令和系统指令执行时间优化,资源利用分析,发现大量CPU操作为sys类型,消耗大量计算资源。调查发现,起因是部分Linux版本缺省情况下打开transparent huge page导致,优化方案是关闭transparent huge page。

  • 虚拟机性能优化,jvm垃圾回收,编译,锁等

  • 基础组件性能优化,Tomcat Web容器,Spring MVC框架,json序列化工具

  • 软件架构性能优化,分布式缓存,分布式消息队列

  • 软件代码性能优化

每一层对性能都有很大的影响

2.2 软件架构性能优化三板斧



  • 缓存,从内存获取数据,减少响应时间,减少数据库访问,降低存储设备负载压力,缓存结果对象,而不是原始数据,减少CPU计算,缓存主要优化读操作。

  • 异步,即时响应,更好的用户体验,控制消费速度,合适的负载压力异步,主要优化写操作。

  • 集群,目标是如何使很多太服务器对使用者而言看起来像是一台服务器。

2.3 软件代码性能优化



遵循面向对象的设计原则与设计模式编程,很多时候程序性能不好不是因为性能上有什么技术挑战,仅仅就是因为代码写的太烂了。

并发编程,多线程与锁

资源复用,线程池与对象池

异步编程,生产者消费者

数据结构,数组、链表、hash表、树

3 操作系统

3.1 程序运行时架构



程序是静态的,当运行起来以后,被称作进程。代码从磁盘加载到内存,由CPU执行对应解析后的指令集。代码或者可执行程序在磁盘上不会自动运行,当装载到内存中才能作为一个进程被CPU执行,程序才算做运行起来了。

服务器可以同时处理数以百计甚至数以千计的并发用户请求,这是由于进程的分时执行的。每个进程的运行都需要获取到CPU资源,进程是有多个,CPU只有一个(单核),进程需要分时对CPU进行资源占有,才能将所有进程进行并发执行。这使得用户看起来这些进程是同时执行的。

当进程在一个CPU上运行时,则称该进程进入了运行状态。当一个进程获得了CPU以外的一切所需资源,只要得到CPU就能运行,则称此状态为就绪状态。当一个进程正在等待某一个时间发生(例如等待I/O完成,等待锁)而暂停运行,这时即使把CPU分配给进程也无法运行,称此状态为阻塞状态。

3.2 进程 VS 线程



由于进程切换的开销代价比较大,因此服务器应用通常是单进程多线程。线程与进程相似,也有三种状态。所有线程共享进程的内存地址空间。而每个线程均有自己的地址空间,其他线程不能访问。



线程栈是每个线程独有的一段地址空间,先进先出栈结构。不同方法中的局部变量不会相互影响,这是因为每个方法都有自己的堆栈空间,即栈帧,栈帧之间互不影响。



Java Web应用多线程运行时视图

应用程序war包添加到Tomcat中,启动Tomcat容器,Tomcat启动(java org.apache.catalina.startup.Bootstrap "$@" start)JVM虚拟机,然后Java虚拟机创建一个主线程执行Main线程执行Bootstrap中的Main方法。接着启动用户线程,开启80端口等,当用户请求来,为每个用户分配一个线程。



3.3 线程安全



当某些代码修改内存堆(进程共享内存)里的数据时候,如果有多个线程同时执行,就可能会出现同时修改的情况。比如两个线程对堆中的数据执行+1操作,最终这个数据只会被加一次,这就是线程安全问题,实际上结果应该时依次加1,结果应该是+2。



多个线程访问共享资源的这段代码称为临界区,解决线程安全问题的主要方法是使用锁。将临界区的代码加锁,只有获得所得线程才能执行临界区代码。



lock.lock();// 线程获得锁
i++; // 临界区代码,i位于堆中
lock.unlock(); // 线程释放锁



3.4 锁



锁是如何实现的?

锁原语CAS(Compare And Set)

CAS(V, E, N),其中V表示要更新的变量,E表示预期值,N表示新值。

如果V值等于E,则将V设置为N。否则什么都不做。

CAS是一种系统原语,原语的执行顺序是连续的,在执行过程中不允许中断。

3.4.1 java对象结构与锁原理



Java对象存储在堆中,每个Java对象都可概括为三部分:对象头、对象体、对齐字节。其中对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外可以配合GC、存放该对象的hashCode。Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例。数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分。对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型。对齐字是为了减少堆内存的碎片空间(不一定准确)。



以上是Java对象处于5种不同状态时,Mark Word中64位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:





biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。



age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。



identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。



重量级锁使用Monitor队列,是公平锁。轻量级锁使用CAS操作不断轮询,是非公平锁。



3.4.2 总线锁与缓存锁



总线锁使用处理器的LOCK#信号,当一个处理器在内存总线上输出此信号的时候,其他处理器的请求将被阻塞,该处理器独占内存。



缓存锁是指内存区域如果被缓存在处理器的缓存行中,并且在LOCK操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效。

3.4.3 公平锁与非公平锁



公平锁就是多个线程按照申请锁的顺序来获取锁。



非公平锁就是多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能会造成饥饿现象。

3.4.4 可重入锁

可重入锁就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

3.4.5 独享锁/互斥锁 共享锁 读写锁



  • 独享锁/互斥锁:该锁一次只能被一个线程所持有

  • 共享锁:该锁可以被多个线程所持有

  • 读写锁:多个读线程之间并不互斥,而写线程则要求与任何线程互斥

3.4.6 乐观锁与悲观锁

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,检查是否已经被修改过,如果修改过,就放弃。

3.4.7 分段锁



分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组的一段进行加锁操作。



JDK ConcurrentHashMap是通过分段锁的形式来实现高并发操作的。

3.4.8 自旋锁



自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。



轻量级锁是自旋锁。

## 3.5 异步并发分布式编程框架akka

Dew

Himeter



4 文件与硬盘I/O



机械硬盘需要移动磁臂移动到指定扇区进行读写。SSD固态硬盘是固态电子存储芯片阵列组成的硬盘。



B+树与LSM树:数据库使用B+树进行查找和写入,而磁头移动是毫秒级的,每次读取都要移动磁头。Hbse存储结构是LSM树。



硬盘访问特性会影响到数据结构和算法。



文件系统将硬盘空间以块为单位进行划分,每个文件占据若干块,然后再通过一个文件控制块FCB记录每个文件占据的硬盘数据块。每个块大小有4K,即使文件不到4K也会占据整个块。



4.1 Linux Inode文件控制块



  • inode中记录着文件权限、所有者、修改时间和文件大小等文件属性信息,以及文件数据块硬盘地址索引。

  • inode是固定结构的,能够记录的硬盘地址索引数也是固定的,只有15个索引。

  • 每个inode最多可以存储12+256+256*256+256*256*256个数据库,如果每个数据块的大小为4K,也就是单个文件最大不超过70G。





4.2 RAID独立硬盘冗余阵列



数据写在一个硬盘上,如果这个硬盘发生故障那么数据将不可恢复。如果数据写入多个硬盘上,即使其中一个硬盘发生损坏,也可以从其他硬盘进行数据恢复。这就是RAID硬盘阵列的思想初衷。





用户头像

路易斯李李李

关注

还未添加个人签名 2020.05.11 加入

还未添加个人简介

评论

发布
暂无评论
2020-07-18-第七周学习总结