高并发设计


系统性能两大核心点

吞吐量(Throughput)
响应延迟(Response Delay)
优化目标
缩短响应时间
提供并发数
让系统处于合理状态
Number of Concurrent Users
上图是高并发软件性能模型。图中三条曲线,分别表示:

资源的利用情况(Utilization,包括硬件资源和软件资源)
吞吐量(Throughput,这里是指每秒事务数)
响应时间(Response Time)。
坐标轴的横轴从左到右表现了并发用户数(Number of Concurrent Users)的不断增长。

随着并发用户数的增长,资源占用率和吞吐量会相应的增长,但是响应时间的变化不大;不过当并发用户数增长到一定程度后,资源占用达到饱和,吞吐量增长明显放缓甚至停止增长,而响应时间却进一步延长。如果并发用户数继续增长,你会发现软硬件资源占用继续维持在饱和状态,但是吞吐量开始下降,响应时间明显的超出了用户可接受的范围,并且最终导致用户放弃了这次请求甚至离开。

根据这种性能表现,图中划分了三个区域,分别是

Light Load(较轻的压力)
Heavy Load(较重的压力)
Buckle Zone(用户无法忍受并放弃请求)。
在Light Load和Heavy Load两个区域交界处的并发用户数,我们称为“最佳并发用户数(The Optimum Number of Concurrent Users)”,而Heavy Load和Buckle Zone两个区域交界处的并发用户数则称为“最大并发用户数(The Maximum Number of Concurrent Users)”。 当系统的负载等于最佳并发用户数时,系统的整体效率最高,没有资源被浪费,用户也不需要等待;当系统负载处于最佳并发用户数和最大并发用户数之间时,系统可以继续工作,但是用户的等待时间延长,满意度开始降低,并且如果负载一直持续,将最终会导致有些用户无法忍受而放弃;而当系统负载大于最大并发用户数时,将注定会导致某些用户无法忍受超长的响应时间而放弃。

所以我们性能优化的目标就是让系统保持在Heavy Load(较重的压力) 区域。

优化手段

1. 空间换时间

系统时间是瓶颈。

这种情况是CPU处理时间很当,但是空间费用我们能接受,所以我们解决的问题的方法就是用空间来换时间。

例如:缓存复用计算结果,降低时间开销,因为CPU时间较内存容量更昂贵

2. 时间换空间

数据大小是瓶颈。

这种情况是空间占用很大,处理时间能接受,所以我们解决问题的方法就是用时间换空间。

例如1:网络传输是瓶颈,使用系统时间换取传输的空间,使用HTTP的gzip压缩算法。解压缩的时候会消耗CPU处理时间。
例如2:APP的请求分类接口,使用版本号判断哪些数据更新,只下载更新的数据。比如一些商品分类类目、城市列表等,一般很少会更新,可以缓存到本地,不能每次登录的时候都去拉取,所以针对这些数据我们可以做一个版本号,如果发现更新了就去拉取数据更新。再比如一些IM社交软件的用户列表,每次登录的时候都去拉一次,其实大家想想每次录的时候都是拉一次挺消耗网络流量的。而这个时候往往你的QQ好友列表变化并不是特别频繁,这个时候怎么办呢?我们会在这个QQ好友的列表有一个时间戳作为版本号,这个版本号在server端存一份,client端存一份,在每次登录的时候会先去判断一下这个版本号是否一致,如果不一致再做更新。

3. 找到系统瓶颈

分析系统业务流程,找到关键路径并分解优化。

一个服务集群4W的QPS,调用量前5的接口贡献了3.5W的QPS
对关键路径的代码优化收益最大,当然系统剩下的部分收益也不能忽视,比如剩下5k QPS接口若性能有问题也可能把整体服务性能拖垮。
所以我们需要抓主要矛盾。大概解决思路总结如下:

整个请求服务调了多少RPC接口;
载入多少数据;
使用什么算法;
非核心流程能否异步化;
没有数据依赖的逻辑能否并行执行
优化层次
从整体到细节,从全局角度到局部视角

1. 架构设计层次

关注系统控制、数据流程
如何拆分系统,如何使各部分系统整体负载更加均衡,充分发挥硬件设施性能优势,减少系统内部开销等。
架构设计层次实现手段:

分布式系统微服务化
分库分表,读写分离,数据分片
无状态化设计,动态水平弹性扩展
调用连路梳理,热点数据尽量靠近用户
分布式Cache、多级多类型缓存
容量规划
提前拒绝,保证柔性可用

2. 算法逻辑层次

算法选择是否高效,是否使时间优先级的还是空间优先级,算法逻辑优化,空间时间优化任务并行处理,使用无锁数据结构等。
空间换时间
ThreadLocal
时间换空间
采用压缩算法压缩数据,更复杂的逻辑减少数据传输

2.1 算法逻辑优化层次实现细节

用更高效的算法替换现有算法,而不改变其接口

增量式算法、复用之前的计算结果,比如一个报表服务,要从全量数据中生成报表数据量很大,但是每次增量的数据较少,则可以考虑只计算增量数据和之前计算结果合并。这样处理的数据量就小很多。

并发和锁的优化,读多写少的业务场景下,基于CAS的LockFree比Mutex性能更好

当系统时间是瓶颈,采取空间换时间逻辑算法,分配更多空间节省系统时间

缓存复用计算结果,降低时间开销,CPU时间较内存容量更加昂贵

当系统空间容量是瓶颈的时候,采用时间换空间算法策略

网络传输是瓶颈,使用系统时间换取空间的压缩,HTTP的gzip的压缩算法

APP的请求分类接口,使用版本号判断那些数据更新,只下载更新的数据,使用更多的代码逻辑处理更细颗粒度的数据

并行执行,比如一段逻辑调用了多个RPC接口,而这些接口之间并没有数据依赖,则可以考虑并行调用,降低响应时间。

异步执行,分析业务流程中的主次流程,把次要流程拆分出来异步执行,更进一步可以拆分到单独的模块去执行,比如使用消息队列,彻底和核心流程解耦,提高核心流程的稳定性以及降低响应是时间。

  1. 代码优化层次
    关注代码细节优化,代码实现是否合理,是否创建了过多的对象,循环遍历是否高效,cache使用的是否合理,是否重用计算结果等。

    3.1 代码优化层次实现细节

    循环遍历是否合理高效,不要在循环里调用RPC接口、查询分布式缓存、执行SQL等
    先调批量接口组装好数据、再循环处理
    代码逻辑避免生成过多对象和无效对象
    输出log时候的log级别判断,避免new无效对象
    ArrayList、HashMap初始容量设置是否合理
    扩容代价
    对数据对象是否合理重用,比如通过RPC查到的数据能复用则必须复用
    根据数据访问特性选择合适数据结构,比如读多写少,考虑CopyOnWriteArrayList(写时Copy副本)
    拼接字符串的时候是使用String相加还是使用StringBuilder进行append(在StringBuilder的容量预分配的情况下StringBuilder的性能比String相加性能高15倍左右)
    是否正确初始化数据。有些全局共享的数据,饿汉式模式,在用户访问之前先初始化好

    3.2 数据库代码优化层次实现细节

    数据库建表语句使用尽量小的数据结构
    表示状态的字段,如果状态值在255以内使用unsigned tinyint,IP使用int而非varchar
    使用enum的场景使用tinyint替代,enum扩展需要该表
    避免使用select * 查询语句,只查询需要的字段,避免浪费数据IO、内存、CPU、网络传输
    分析查询场景经理合适的索引,分析字段的可选择性,索引长度,对长的varchar使用前缀索引
    字段尽量为Not NULL类型,MySQL手册说吗允许NULL的字段需要额外的存储空间去处理NULL ,并且很难查询优化
    以上优化的目的为了降低服务器CPU使用率、IO流量、内存占用、网络消耗、降低响应时间

在做查询时,可以通过代码逻辑,加一下查询条件,利用索引来提升查询速度。

3.3 局部优化层次实现细节

以下两段代码哪个执行速度快?

long[][] a = new long[10000][10000];
for(int i=0;i<a.length;i++){
for(int j =0;j<a[i].length;j++){
a[i][j]=j;
}
}
复制代码
long[][] a = new long[10000][10000];
for(int i=0;i<a.length;i++){
for(int j =0;j<a[i].length;j++){
a[j][j]=j;
}
}
复制代码
这两段代码,唯一的区别在于填充方式,第一种方式是按行填充,第二种方式是按列进行填充。显然是第一种方式更快一些。 虽然是二维数组,但是在内存堆列中还一维数组进行存储的。大家可以在试一下,第一种运行速度比第二种方式快20倍。

那深层次原因是什么呢?

我们知道在计算机体系中,除了有内存来做缓存,在CPU层面也有cache结构。而越靠近CPU读取速度越快。如下图:

缓存的结构大概时这样的,从1级到3级速度越来越慢,最后通过总线与内存连接。 本质上内存是一个大的一维数组,二维数组在内存中按行排列,现存放a[0]行,再存放a[1]行,第一种遍历方式,是行遍历,先遍历完一行再遍历第二行,符合局部性原理,Cache Hit(缓存命中率高),第二行遍历方式,是列遍历,遍历完第一列遍历第二列,由于下一列和上一列的数组元素在内存中并不是连续的,很可能导致Cache Miss(缓存未命中),CPU需要去内存载入数据,速度较CPU L1 Cache的速度降低了很多(主存100ns,L1cache 0.5ns)

扩大到一般场景,业务系统使用缓存降低响应时间提高性能,必须提高缓存命中率。很聚焦的高频访问,时效性要求不高很适合缓存提升性能,很聚焦的高频访问业务如banner,广告位,时效性要求不是特别高,比如更新了可以不用实时体现,很适合使用缓存提升性能。

如果对数据实时性要求很高,比如严格的时效性,需要慎重考虑更新缓存带来一致性问题。

时效性和缓存的冲突,比如商品服务对商品进行了缓存,由于更新缓存和更新商品不是同一个事务,则对数据时效性要求高的如交易,就只能直接从数据库查商品信息。


文章作者: Jone
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jone !
评论
 上一篇
redis使用指南 redis使用指南
redis与redis部署服务的安装下载文件redis-3.0.0.tar.gz 一,redis服务的安装1.环境配置Redis是c开发的,因此安装redis需要c语言的编译环境,即需要安装gcc 查看是否安装gcc gcc -v如果没有g
2018-06-13
下一篇 
java多线程 java多线程
1.创建线程在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。在使用Runnable接口时需要建立一个Thread实例。因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它
2015-04-12
  目录