本文分享一些高性能相关的设计模式。
模式
是对常见规律的总结,也可以理解为 套路
。
模式
以下是总结的一些常见的模式。
模式1:升级硬件
例如将普通硬盘换成 SSD 固态硬盘,将内存从2G升级到16G,将网络带宽从50兆升级到 1000兆。
模式2:多进程/多线程
多进程/多线程,很多情况下可以更好的利用资源。
模式3:协程
协程是轻量级的用户态线程。Go 语言中的 goroutine、Java 19 中的虚拟线程等都是协程。相比线程,协程的切换开销极小。
模式4:集群
当一台机器不够用时,可以考虑增加机器。比如一个转换图片格式的服务,靠增加机器就可以线性的增加整体处理能力。
模式5:负载均衡
在使用集群时,需要对外部进来的任务进行分配,将任务配置到一个具体的集群下的机器上。此时需要用到负载均衡
。
负载均衡的分类:
- DNS 负载均衡:例如某服务在北京和上海均有部署,用户通过网址访问时,需要通过 DNS 将域名转换为具体的 IP ,DNS 根据一定策略将北京或者上海的服务IP返回给用户。
- 硬件负载均衡:一个对外暴露的 IP 下,会有多个提供相同服务能力的集群或机器,基于硬件实现的负载均衡设备根据一定策略选择某个集群或者某个机器。
- 软件负载均衡:一个对外暴露的 IP 下,会有多个提供相同服务能力的集群或机器,基于软件实现的负载均衡设备根据一定策略选择某个集群或者某个机器。
负载均衡策略:
- 随机
- 加权随机
- 最少活跃优先
- 最短耗时优先
- 一致性哈希
- 等等
模式6:读写分离
若 MySQL 数据库只有一个主库,当查询量增大时,为了分散查询压力,可以增加数据库从库。
例如在1个主库的基础上再部署2个从库,纯查询业务的查询请求用从库来分担。
注意点:
- 写数据肯定走主库。若在一次处理时,先写数据,再查数据,这个查数据必须走主库。
- 从库数据会比主库慢一些,要评估这个
慢
对业务的影响,对于关键业务,一般无法接受这种慢,所以不要做读写分离。
模式7:分库分表
分库分表,既可以分散读写压力,也可以分散存储压力。
分库:
业务分库:例如,用户数据一个库、商品数据是另外一个库。
非业务分库:比如,对于用户数据,一个库压力较大,按照用户id取模拆分成多个库。
分表:
一个表存储亿级别的数据,性能会有问题,可以拆分成多个表。
模式8:缓存
使用缓存的目的:
- 减少数据库访问压力;
- 更快的获取数据
缓存可以分为进程内缓存(如 guava)和独立进程缓存(如 redis)。
模式9:I/O 多路复用
在百科上,多路复用
的解释如下:
“多路复用”也可被称为“复用”。 多路复用通常表示在一个信道上传输多路信号或数据流的过程和技术。
在处理网络连接时,一般是指一个线程处理多个网络连接。常见实现有 select、epoll、kqueue 等。
基于 I/O 多路复用,有一个经典的网络编程模式:Reactor 。
这种模式也可以认为是非阻塞编程,之所以比阻塞更快,还是因为 I/O 相比 CPU 太慢了。
模式10:定制软硬件
经典例子是比特币的挖矿设备。
模式11:让资源离用户更近
经典例子是 CDN 。CDN 依赖 DNS 负载均衡。
模式12:懒加载
这里的懒加载
不是指延迟加载
,而是指在加载资源时偷懒。
举例,在朋友圈发的图片动辄2、3MB 大小,当朋友浏览朋友圈时,微信需要加载原图吗?理论上为了用户体验,不会去加载原图,而是去加载只有几十KB 的缩略图。只有点击图片时,才会尝试加载原图。
模式13:预加载
举例,在图片轮转时,下一个没有展示的图片,可以预先缓存到本地,这样图片轮转会更顺滑。
模式14:轻量级事务
数据库事务的持续时间越短,总体性能越高,尤其是事务中会加锁、多个事务需要争抢锁的时候。
在事务中,不要有RPC 调用,否则会导致事务时间变长,影响性能。
模式15:数据精简与批量处理
例如向监控系统上报某个接口的调用次数,该接口一般可以达到100qps,有两种方式来上报:
方式1:每次调用接口时都进行上报。让监控系统来累计调用次数。
方式2:1s上报1次,当前服务自己先进行一次汇总,比如1s内有50次调用,则将这一秒的总次数50上报到监控系统。
方式2的性能肯定更好。
另外一个例子,是去银行存钱,我们是愿意赚到1块钱,就去银行存,还是当赚的钱到1000块了,再去银行存?
模式16:数据压缩
数据压缩后再传输,会减少很多网络耗时。数据压缩花费的CPU时间,一般远小于减少的网络耗时。
模式17:异步处理
以向监控系统上报某个接口的调用次数为例,同步上报明显会增加业务处理的总耗时,搞一个线程池进行异步上报,会减少这种影响。
模式18:分治
百科上,分治的解释:
字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解。
分治比较常见的例子是:快速排序、归并排序等算法;大数据处理中的 MapReduce。
副作用
追求高性能,不是一帆风顺的。某些模式下实现的高性能,要做一些牺牲。
副作用1:增加复杂度
以缓存为例,系统中肯定要增加一堆关于缓存的代码,缓存的实现方式还需要额外进行考量,比如如何更新缓存,有3种方式:
- 先删缓存,再更新数据库;
- 先更新数据库,再删缓存;
- 先更新数据库,再更新缓存;
对于先更新数据库,再删除/更新缓存,还需要额外考虑因为网络等原因删除缓存失败的问题。
副作用2:一致性损失
以缓存为例,无法做到强一致性,只能做到最终一致性。
以异步上报监控系统为例,如果机器突然宕机,会导致部分数据未上报。
注意,并不是所有的高性能设计模式都会导致一致性损失,比如硬件升级,一致性并不会变。
我们要根据系统定位、系统对一致性的要求,选择合适的方案。