上一篇,总结了六种普适的性能优化方法,包括 索引、压缩、缓存、预取、削峰填谷、批量处理,简单讲解了每种技术手段的原理和实际应用。

本篇讲另外几类涉及一些技术细节的性能优化方向,仍然选取了《火影忍者》的配图和命名方式帮助理解:

  • 八门遁甲 —— 榨干计算资源
  • 影分身术 —— 水平扩容
  • 奥义 —— 分片术
  • 秘术 —— 无锁术

八门遁甲 —— 榨干计算资源

让硬件资源都在处理真正的业务,而不是做无关的事情或空转

从门电路到汇编、驱动软件、操作系统、直到高级编程语言的层层抽象,每一层抽象带来的更高的开发效率,是以损失运行效率为代价的。但我们可以在用高级编程语言写代码的时候,在保障可读性基础上运行效率更高的方式去写,减少额外的性能损耗,这也是《Effective C++》、《More Effective C++》、《Effective Java》这类书籍所传递的思想。

在这一小节之前我们先聊一聊:程序运行期间,时间和空间到底消耗在哪里。

100ns

30024

时间都去哪儿了?

人眨一次眼大约100毫秒,而1核CPU在一眨眼的功夫就可以执行数亿条指令。

一些CPU指令需要多个时钟周期,但由于有流水线机制的存在,大约每个时钟周期能执行1条指令,比如一个3GHz频率的CPU核心,每秒大概可以执行20亿到40亿左右的指令数量。

存取一次CPU高速缓存的时间大约1纳秒;存取一次主存(RAM)的时间大概在100纳秒级;固态硬盘的一次随机读写大约在10微秒到1毫秒这个数量级;网络包在局域网传输一个来回大约是0.5毫秒。

这些时间指标是硬件自身的属性,我们可以拿当前水平作为基线,来衡量软件的实现有多大的额外开销。

随着新的技术和工艺的出现,硬件会越来越厉害,比如CPU的更新换代、NVMe固态硬盘相对SATA接口的固态硬盘,读写速率和延迟有质的提升等等。这里有一个非常棒的网站“Latency Numbers Every Programmer Should Know”,可以直观地查看从1990年到现在,高速缓存、内存、硬盘、网络时间开销的具体数值。下图是2020年的截图,的确是“每个开发者应该知道的数字”。


通过这些数据之间数量级的差距,就很容易理解性能优化的一些技术手段了。比如一次网络传输的时间是主存访问的5000倍,大概也会避免写出for循环发HTTP请求的代码了。放大到我们容易感知的时间范围,来理解5000倍的差距:一次主存访问如果是1天完成,一次网络传输就要13.7年。

单纯看存取RAM的时间或者读写硬盘的时间是没有用的,实现某个功能往往包含大量组合操作。

https://colin-scott.github.io/personal_website/research/interactive_latency.html

less copy, less context switch, less system call
fsync 10-50ms, ssd 100-10000μs (SATA NVME)
ctx switch : system call -> mode switch, thread switch: cache change, work set change, full ctx switch (1-30 μs)

memcache/redis 1-5 ms, db 5-50ms
intranet 1-10ms internet 10-100ms , pure net pack trip in kernel about 1.5ms
memory: 1-10 nano sec, context switch/system call 100nano-1 μs ssd 10 μs - 100 μs hdd 1 - 10 ms

Interpacket gap https://en.wikipedia.org/wiki/Interpacket_gap

9.6 µs for 10 Mbit/s Ethernet,
0.96 µs for 100 Mbit/s (Fast) Ethernet,
96 ns for Gigabit Ethernet,
38.4 ns for 2.5 Gigabit Ethernet,
19.2 ns for 5 Gigabit Ethernet,
9.6 ns for 10 Gigabit Ethernet,
3.84 ns for 25 Gigabit Ethernet,
2.4 ns for 40 Gigabit Ethernet,
1.92 ns for 50 Gigabit Ethernet,
0.96 ns for 100 Gigabit Ethernet,
0.48 ns for 200 Gigabit Ethernet, and
0.24 ns for 400 Gigabit Ethernet.[1]

https://stackoverflow.com/questions/21887797/what-is-the-overhead-of-a-context-switch
https://stackoverflow.com/questions/23599074/system-calls-overhead

空间都去哪儿了?

object size,
thread size,
cache size ,
system obj
tcp socket size, etc.

sizeof xxx

MTU https://en.wikipedia.org/wiki/Maximum_transmission_unit

IP 64KB header min 20byte max 60byte (https://tools.ietf.org/html/rfc791)
Ethernet 1500Byte header min 18byte

空间利用率
https://en.wikipedia.org/wiki/Jumbo_fr ame

。比如:系统调用的耗时、编程语言运行时环境提供的库函数耗时、依赖的各个分布式组件的耗时;单次业务平均占多大内存和磁盘空间

理解了时间和空间的消耗在哪之后,我们再看有哪些办法提升硬件资源的利用率

采用更高效的数据结构和算法

逻辑短路

二分法
FisherYates

因地制宜,适应特定的运行环境

在浏览器中主要是优化方向是I/O、UI渲染引擎、JS执行引擎三个方面。I/O越少越好,能用WebSocket的地方就不用Ajax,能用Ajax的地方就不要刷整个页面;UI渲染方面,减少重排和重绘,比如Vue、React等MVVM框架的虚拟DOM用额外的计算换取最精简的DOM操作;JS执行引擎方面,少用动态性极高的写法,比如eval、随意修改对象或对象原型的属性。前端的优化有个神器:Light House,在新版本Chrome已经嵌到开发者工具中了,可以一键生成性能优化报告,按照优化建议改就完了。

与浏览器环境颇为相似的Node.js环境,
https://segmentfault.com/a/1190000007621011#articleHeader11

Java

Linux

  • 各种参数优化
  • 内存分配和GC策略
  • Linux内核参数 Brendan Gregg
    内存区块配置(DB,JVM,V8,etc.)

利用语言特性和运行时环境 - 比如写出利于JIT的代码

  • 多静态少动态 - 舍弃动态特性的灵活性 - hardcode/if-else,强类型,弱类型语言避免类型转换 AOT/JIT vs 解释器, 汇编,机器码 GraalVM

减少内存的分配和回收,少对列表做增加或删除

对于RAM有限的嵌入式环境,有时候时间不是问题,反而要拿时间换空间,以节约RAM的使用。

减少系统调用与上下文切换

大部分互联网应用服务,耗时的部分不是计算,而是I/O。

减少I/O wait

http://jolestar.com/parallel-programming-model-thread-goroutine-actor/

  • 利用DMA减少CPU负担 - 零拷贝

避免不必要的调度 - Context Switch

对于存在CPU密集型业务的场景,除了把CPU密集型业务的计算资源需求用多线程异步化的方式化解掉以外,还可以在部署架构层面与I/O密集型应用服务互补调度。一些大厂有自己的容器调度平台,多个互补型服务混合部署,间接榨干了计算资源,最大化整体效率。

从底层下手,在硬件上优化

如果参数调教、代码重构、并发编程模式转变这些路子都满足不了性能要求,那还剩一条路可以走,通往底层硬件的路。

其中最简单易行的几个办法,就是花钱,买更好或更多的硬件基础设施。

  • 云服务厂商提供的各种字母开头的instance,网络设备带宽的速度和稳定性,磁盘的I/O能力
  • 舍弃虚拟机 - Bare Mental 神龙服务器
  • 用ARM架构的CPU的服务器,同等价格可以买到更多的服务器,对于多数可以跨平台运行的服务端系统来说与x86区别并不大,而x86系列的服务器,AMD也比Intel的便宜,性价比更高。

但要注意,软件系统的性能遵循木桶原理,一定要找到瓶颈在哪个硬件资源,把钱花在刀刃上。如果是服务端带宽瓶颈导致的性能问题,升级再多核CPU也是没有用的。

除了这条花钱能直接解决问题的办法,剩下的办法难度就大了:

  • 利用更底层的特性实现功能,比如FFI调用,字节码生成,甚至汇编等等
  • 使用硬件提供的更高效的指令
  • 各种提升TLB命中率的机制,减少内存的大页表
  • 魔改Runtime,Facebook的PHP,阿里定制的JRE
  • 网络设备参数,MTU
  • 专用硬件:GPU加速(cuda)、AES硬件卡加速TLS
  • 可编程硬件:地狱级难度,FPGA硬件设备加速特定业务

小结

用了这些手段,是凭空换出来更多的空间和时间?没有免费的午餐,想要榨干硬件资源,需要花费大量的时间成本压测和调优和专家级的经验,副作用可能就是资深专家级的发际线吧。这里只是总结一下方法,很多复杂的技术手段我也不会,所以本人发际线还保留完好。

影分身术 —— 水平扩容

本节的水平扩容以及下面一节的分片,可以算整体的性能提升而不是单点的性能优化,会因为引入额外组件反而降低了处理单个请求的性能。但当业务规模大到一定程度时,再好的单机硬件也无法承受流量的洪峰,就得水平扩容了,毕竟”众人拾柴火焰高”。

多副本
水平扩容的前提是无状态
读>>写, 多个读实例副本 (CDN)
自动扩缩容
负载均衡策略的选择

原理:并行化

奥义 —— 分片术

水平扩容针对无状态组件,分片针对有状态组件。二者原理都是提升并行度,但分片的难度更大。

分片 - 百科全书分册
Java1.7的及之前的 ConcurrentHashMap分段锁 https://www.codercto.com/a/57430.html
有状态数据的分片
如何选择Partition/Sharding Key
负载均衡难题
热点数据,增强缓存等级,解决分散的缓存带来的一致性难题
数据冷热分离,SSD - HDD

分开容易合并难

区块链的优化,分区域

秘术 —— 无锁术

Don’t communicate by sharing memory, share memory by communicating

不管是单机还是分布式微服务,锁都是制约并行度的一大因素。比如上篇提到的秒杀场景,库存就那么多,系统超卖了可能导致非常大的经济损失,但用分布式锁会导致即使服务扩容了成千上万个实例,最终无数请求仍然阻塞在分布式锁这个串行组件上了,再多水平扩展的实例也无用武之地。

避免竞争Race Condition 是最完美的解决办法。上篇说的应对秒杀场景,预取库存就是减轻竞态条件的例子,虽然取到服务器内存之后仍然有多线程的锁,但锁的粒度更细了,并发度也就提高了。
线程同步锁
分布式锁
数据库锁 update select子句
事务锁
顺序与乱序
乐观锁/无锁 CAS Java 1.8之后的ConcurrentHashMap
pipeline技术 - CPU流水线 Redis Pipeline 大数据分析 并行计算
原理:并行化

TCP的缓冲区排头阻塞 QUIC HTTP3.0

总结

以ROI的视角看软件开发,初期人力成本的投入,后期的维护成本,计算资源的费用等等,选一个合适的方案而不是一个性能最高的方案。

本篇结合个人经验总结了常见的性能优化手段,这些手段只是冰山一角。在初期就设计实现出一个完美的高性能系统是不可能的,随着软件的迭代和体量的增大,利用压测,各种工具(profiling,vmstat,iostat,netstat),以及监控手段,逐步找到系统的瓶颈,因地制宜地选择优化手段才是正道。

切忌过早优化、过度优化。

持续观测,做80%高投入产出比的优化。

除了这些设计和实现时可能用到的手段,在技术选型时选择高性能的框架和组件也非常重要。

另外,部署基础设施的硬件性能也同样,合适的服务器和网络等基础设施往往会事半功倍,比如云服务厂商提供的各种字母开头的instance,网络设备带宽的速度和稳定性,磁盘的I/O能力等等。

有利必有弊,有一些手段慎用。