软件设计开发某种意义上是“取”与“舍”的艺术。你无法获取无限的资源去完成无限膨胀的需求。只能在有限的条件内去完成限定范围内的事情。
在性能方面,高性能软件系统也意味着更高的实现成本,有时候与其他质量属性甚至会冲突,比如安全性、可扩展性、可观测性等等。
性能优化总结下来有两个方向:
- 资源置换——也就是“时间”和“空间”的互换取舍。
- 并行处理。
下面来分别说明这两个方向。
资源置换
在“时间”和“空间”的互换舍取有常见的6种方法:
- 索引
- 压缩
- 缓存
- 预取
- 削峰填谷
- 批量处理
索引
索引的原理是拿额外的存储空间换取查询时间,增加了写入数据的开销,但使读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)。
有很多种索引的类型,根据不同的场景需要使用不同的索引类型。索引还涉及到主键和分库分表的处理。
缓存
缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间。
缓存的形式同样多种多样。从廉价的磁盘到昂贵的CPU高速缓存,最终目的都是用来换取宝贵的时间。
Phil Karlton 曾说过:There are only two hard things in Computer Science: cache invalidation and naming things.
计算机科学中只有两件困难的事情:缓存失效和命名规范。
缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。
压缩
压缩是一个“时间换空间”的办法。
压缩的原理消耗计算的时间,换一种更紧凑的编码方式来表示数据。
为什么要拿时间换空间?时间不是最宝贵的资源吗?
举一个视频网站的例子,如果不对视频做任何压缩编码,因为带宽有限,巨大的数据量在网络传输的耗时会比编码压缩的耗时多得多。
对数据的压缩虽然消耗了时间来换取更小的空间存储,但更小的存储空间会在另一个维度带来更大的时间收益。
这个例子本质上是:“操作系统内核与网络设备处理负担 vs 压缩解压的CPU/GPU负担”的权衡和取舍。
预取
预取通常搭配缓存一起用,其原理是在缓存空间换时间基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间。
当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度。
是否用预取模式就像自助餐餐厅与厨师现做的区别,在自助餐餐厅可以直接拿做好的菜品,一般餐厅需要坐下来等菜品现做。
削峰填谷
削峰填谷的原理也是“时间换时间”,谷时换峰时。
削峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做。就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不是用三峡大坝实现,而是用消息队列、异步化等方式。
针对不同的场景同样会使用不同的应用手段。
批量处理
批量处理同样可以看成“时间换时间”,其原理是减少了重复的事情,是一种对执行流程的压缩。以个别批量操作更长的耗时为代价,在整体上换取了更多的时间。
并行处理
并行处理也有为4种常见手段:
- 榨干计算资源。
- 水平扩容
- 分片
- 无锁
榨干计算资源
让硬件资源都在处理真正有用的逻辑计算,而不是做无关的事情或空转。
从晶体管到集成电路、驱动程序、操作系统、直到高级编程语言的层层抽象,每一层抽象带来的更强的通用性、更高的开发效率,多是以损失运行效率为代价的。
水平扩容
本节的水平扩容以及下面一节的分片,可以算整体的性能提升而不是单点的性能优化,会因为引入额外组件反而降低了处理单个请求的性能。
但当业务规模大到一定程度时,再好的单机硬件也无法承受流量的洪峰,就得水平扩容了,毕竟”众人拾柴火焰高”。
在这背后的理论基础是,硅基半导体已经接近物理极限,随着摩尔定律的减弱,阿姆达尔定律的作用显现出来。
分片
水平扩容针对无状态组件,分片针对有状态组件。二者原理都是提升并行度,但分片的难度更大。
负载均衡也不再是简单的加权轮询了,而是进化成了各个分片的协调器
无锁
有些业务场景,比如库存业务,按照正常的逻辑去实现,水平扩容带来的提升非常有限,因为需要锁住库存,扣减,再解锁库存。
票务系统也类似,为了避免超卖,需要有一把锁禁锢了横向扩展的能力。
不管是单机还是分布式微服务,锁都是制约并行度的一大因素。比如上篇提到的秒杀场景,库存就那么多,系统超卖了可能导致非常大的经济损失,但用分布式锁会导致即使服务扩容了成千上万个实例,最终无数请求仍然阻塞在分布式锁这个串行组件上了,再多水平扩展的实例也无用武之地。
避免竞争Race Condition 是最完美的解决办法。