《架构师训练营》之性能 & 操作系统
极客时间《架构师训练营》第七周学习笔记
性能
性能指标
首先来介绍几个常见的性能指标:
-
QPS
QPS:Queries Per Second ——“每秒查询率”——是一台服务器每秒能够响应的查询次数,是对一个特定的查询服务器在规定时间内所能处理流量多少的衡量标准。
-
TPS
TPS 是 Transactions Per Second 的缩写,也就是事务数/秒。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
-
并发数(Concurrency)
并发数是指系统同时能处理的请求数量,反映了系统的负载能力。
-
响应时间(Response-time)
响应时间:执行一个请求从开始到最后收到响应数据所花费的总体时间,即从客户端发起请求到收到服务器响应结果的时间。
-
吞吐量(Throughput)
吞吐量是指系统在单位时间内处理请求的数量,TPS、QPS 都是吞吐量的常用量化指标。吞吐量与请求对 CPU 的消耗、外部接口、IO 等紧密关联。单次请求对 CPU 消耗越高,外部系统接口、IO 速度越慢,系统吞吐能力越低,反之越高。
性能测试
性能测试是一个总称,具体可以细分为性能测试、负载测试、压力测试、稳定测试等等。下图是一张经典的负载-性能关系图。通常,性能测试包含三个阶段:轻负载阶段、重负载阶段、压垮阶段。这块我在作业第一题里已经有所描述了,这里不再展开了。
![](https://img.haomeiwen.com/i14368237/b8f741cb93ad6c4e.png)
性能测试是通过自动化的测试工具,模拟多种正常、峰值以及异常负载条件下,对系统的各项运行指标进行衡量的手段。通过性能测试,我们可以大致确定在各种负载下系统的运行状况。一般而言,只有在系统基础功能测试验证完成、系统趋于稳定的情况下才会进行性能测试,否则性能测试是无意义的。
性能优化
性能测试结束后,便显示出相应的系统瓶颈,我们就可以着手性能优化了。
-
代码
我觉得代码重构是第一位的,绝大多数的性能瓶颈都是垃圾代码造成的,比如 for 循环次数过多、作了很多无谓的条件判断、相同逻辑重复多次等等。所以,发现性能问题,第一步就应该是分析相关的代码,找出相应的瓶颈,再来考虑其他具体的优化策略。
-
数据库
-
SQL 调优:这是后端技术人员都应该掌握基本的调优手段,比如通过慢查询日志定位到具体出问题的 SQL 语句
-
连接池调优:随着业务访问量或者数据量的增长,原有的连接池参数可能无法满足现行需求,这个时候就需要根据当前使用情况调优出最适合的连接参数
-
其他复杂手段:读写分离、多从库负载均衡、水平和垂直分库分表等等
-
-
缓存
-
缓存是否满了:设置缓存逐出算法(如LRU),设置TTL
-
缓存是否丢失:尝试缓存(如redis)的持计划服务
-
缓存击穿问题:如使用mutex策略
-
-
异步
如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。可以通过背压来限制队列大小,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。
-
JVM 调优
可以通过监控系统上对一些机器关键指标的报警分析问题所在;也可以看 gc log 和 jstat 等命令的输出,或是线上 JVM 进程服务的一些关键接口的性能数据和请求体验定位出当前的 JVM 是否有问题,以及是否需要调优。
-
多线程
-
单机多线程:离线任务、异步任务、大数据任务、耗时较长任务,适当地利用多线程可以达到加速的效果
-
线程池:享元模式在多线程上的应用,以节省线程创建和销毁的开销
-
限流:给线程池一个固定的容量,达到这个容量值后再有任务进来就放进队列排队,保障机器极限压力下的稳定处理能力
-
如果单机的处理能力不能满足需求,还可以使用多机多线程的方式
-
-
其他
- 更换硬件
- 异地多活
- 组件升级
- 操作系统调优
线程相关
知识点
-
进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
-
线程
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈);但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
-
协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
-
堆
堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
-
栈
栈是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立;因此,栈是 thread safe 的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP 寄存器。栈空间不需要在高级语言里面显式的分配和释放。
线程安全
当多个线程共享同一个全局变量或静态变量时,写操作有可能会发生数据冲突的问题,也就是线程安全问题。但是做读操作是不会发生线程安全问题。解决写冲突的方法就是“锁”:给代码加锁,让临界区的资源只被一个线程执行;代码执行完成后释放锁,其他线程再执行,这样就可以解决线程不安全问题。
但是锁会引起线程阻塞。阻塞导致线程既不能执行也不能释放,进而耗尽资源,最终造成系统奔溃。解决方案如下:
- 限流:控制进入计算机的请求数,进而减少创建的线程数
- 降级:关闭部分功能程序的执行,尽早释放线程
- 避免阻塞:异步 IO;无临界区(Actor 模型)
锁
CAS 原语
CAS(V,E,N)
:V 表示更新变量;E 表示预期值;N 表示新值。当且仅当预期值 E 和内存变量 V 相同时,才将变量 V 修改为 N,否则什么都不做。在 Java 中,CAS 通过调用 JNI 的代码实现的——sun.misc.unsafe.compareandswapint
;该方法为 JAVA 提供 C/C++ 方法来执行 CPU 的 CAS 指令,完成该任务。
各种锁
系统设计上有各种各样的锁:偏向锁、轻量级锁、重量级锁、总线锁、缓存锁、公平锁、非公平锁、可重入锁、独享锁、共享锁、读写锁、分段锁、自旋锁、乐观锁、悲观锁等等。
锁在各种范畴下有太多种类了,篇幅有限就不一一介绍了。我当面试官时问过别人乐观锁和悲观锁,这里就稍微展开一下:
-
乐观锁
顾名思义就是在操作时比较乐观,认为发生线程安全问题的可能性很低,因此不上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,实现上一般会存一个时间戳,读取时带上这个值:写回时,若 DB 里的时间戳等于该值,则写成功,并更新时间戳;若 DB 时间戳大于该值,则表示数据已被人修改,则返回写失败。
-
悲观锁
总是假设最坏情况,每次取数时都认为其他线程会修改,所以表现地很悲观。一旦加锁,其他线程只能等待,直到锁释放。
系统设计时:若读得多,冲突几率小,用乐观锁;若写得多,冲突几率大,用悲观锁。
文件系统
-
FCB:文件控制块,操作系统为文件设置的用于描述和控制文件的数据结构
-
inode:linux 的文件控制索引节点,用来存放档案及目录的基本信息,包含时间、档名、使用者及群组等。
-
RAID: 独立冗余磁盘阵列,由很多块独立的磁盘组合而成的一个大容量磁盘组,利用个别磁盘提供数据所产生的加成效果提升整个磁盘系统的效能。
-
HDFS:最常见的分布式文件系统,被设计成适合运行在通用硬件上的分布式文件系统。一式三份,保证高可用高容错,一般用于大数据模块。
结束语
这周的知识点太多了,后半部分写不过来了。操作系统快还给大学老师了。不过,复习了一下还是蛮有收获的,学生时代对上述知识点都是死记硬背的(虽然当年考试还是拿了很高分的😅);工作后相关专业名词也有接触,但是用得很少。这次老师把知识点串起来讲了一下,总算让我有点打通筋脉的感觉了。性能调优这块,太倚重经验了,虽然从网上参考了一些调优手段,但是让我自己来还是很难做到的;唉,路漫漫其修远兮呀。