前言
《中国互联网发展状况统计报告》指出,截至2020年6月,我国网民规模已经达到9.40亿,较2020年3月年增长3625万,除了如此庞大的用户基数,如今人们接入互联网的方式也越来越多样,小到智能手表,手机,大到笔记本,汽车,这也意味着实际使用互联网服务的entry point正以亿级的增长速率膨胀。也就是基于如此规模急剧扩大的环境下,服务厂商开始面临着两大压力,一个是更大的并发访问压力,一个是海量数据存储的压力。
高并发问题是各大平台必须解决的问题之一,它关系着平台可以承担多大的用户量以及能否提供可靠的服务。传统的单机模式已经无法满足需要,单纯的增加服务器数量也无法彻底解决问题,必须从整体系统架构和设计上全面考虑。
因此,本文汇总了一些常用的高并发解决思路及巧妙的工程实践,与大家分享一哈,具体细节先不展开,随用随查。
技术汇总
池化技术
池化技术就是把一些资源预先分配好,然后组织到对象池中,之后的业务使用资源从对象池中获取,使用完后放回到对象池中。这样做带来几个明显的好处:
- 资源重复使用, 减少了资源分配和释放过程中的系统消耗。比如,在IO密集型的服务器上,并发处理过程中的子线程或子进程的创建和销毁过程,带来的系统开销将是难以接受的。所以在业务实现上,通常把一些资源预先分配好,如线程池,数据库连接池,Redis连接池,HTTP连接池等,来减少系统消耗,提升系统性能。
- 可以对资源的整体使用做限制。这个好理解,相关资源预分配且只在预分配是生成,后续不再动态添加,从而 限制了整个系统对资源的使用上限。类似一个令牌桶的功能。.
- 池化技术分配对象池,通常会集中分配,这样有效避免了碎片化的问题。
比较经典的就是内存池的设计是SGI STL内存分配器:
std::allocator:默认分配器
__pool_alloc :SGI内存池分配器
__mt_alloc : 多线程内存池分配器
array_allocator : 全局内存分配,只分配不释放,交给系统来释放
malloc_allocator :堆std::malloc和std::free进行的封装
缓存
缓存技术 一般是指,用一个更快的存储设备存储一些经常用到的数据,供用户快速访问。 用户不需要每次都与慢设备去做交互,因此可以提高访问效率。 分布式缓存 就是指在分布式环境或系统下,把一些热门数据存储到离用户近、离应用近的位置,并尽量存储到更快的设备,以减少远程数据传输的延迟,让用户和应用可以很快访问到想要的数据。
缓存技术比较热门的就是memcache和redis。
垂直/水平扩展
扩展就是典型的技术不够设备来凑,在云计算领域我们认为算力和内存是无穷的,原因就在于易于垂直/水平扩展。
垂直扩展:提升单机处理能力。
- 增强单机硬件性能,例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
- 提升单机架构性能,例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间;
水平扩展:只要增加服务器数量,就能线性扩充系统性能。
限流
“限流”是指对用户请求进行一定程度的拦截,实现请求延时或者请求丢弃处理,相关的解决方案如下:
- 使用缓存对数据处理层进行限制,存储一些热点数据,加快访问数据的同时也防止了大量请求到达后端数据库,如 Cookie 或 Session 等;
- 使用消息队列(Message Queue,MQ)中间件将一些非即时的流量缓冲到 MQ 中,后续来实现异步处理;
- 使用网络流量高并发处理算法进行流量整形,以服务器能够承受的速率发送到 Web 应用系统后端进行处理;
- 从业务层面上,限制用户单位时间内的频繁访问操作,可以限制一部分冗余请求。
比较有代表性的限流算法是Google的开源项目RateLimiter,它是基于可配置的速率来分发令牌,数据包在获取令牌之后才能够被转发到网络中,从而实现对接口速率的限制。
负载均衡
负载均衡主要通过分担负载,通过选用合适的负载均衡策略,将请求分发到不同的服务节点上,解决网络拥堵问题,从而提高网络利用率,充分的利用服务器的各种资源让集群中的节点负载情况处于平衡状态来,提高系统的灵 活性和扩展能力,以达到提高系统整体的并发量的目的,从而使得外部用户体验更佳。常用的负载均衡的调度算法如下:
- 轮询(Round Robin)
- 加权轮询(Weighted Round Robin)
- 最少连接(Least Connections)
- 加权最少连接(Weighted Least Connections)
- 随机(Random)
- 加权随机(Weighted Random)
- 源地址散列(Source Hashing)
- 源地址端口散列(Source&Port Hashing)
削峰填谷
高并发处理可以抽象为消费者与生产者模型,当高峰期产出现的时候,消费者很容易出现瞬时流量非常大,但一般情况流量相对较少,这样平时的性能就浪费了。这个时候就可以引入消息队列RabbitMQ,它有三个好处:
- 解耦。生产者无需关注有多少消费者,它只需要和消息队列MQ交互,生成的数据传给MQ即可;
- 异步。生产者将数据交给MQ直接可以返回,消费者什么时候消费,无需专注
- 削峰填谷。MQ有限制消费的机制,比如之前生产者生成速率为3000,但MQ做了一个限制只有1000,那么高峰期的数据就会挤压在MQ里,高峰被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完挤压的消息,这就做“填谷”。
无锁化
环形队列一般符合生产者-消费者模型,用于报文收发的缓存区或者是线程收发消息的队列,DPDK提供了一种无锁的环形队列,大家知道加锁操作十分耗费性能,下图是横向多种锁机制的比较,其中第一个是无锁:
DPDK的无锁环形队列的优缺点:
具体原理可见:dpdk无锁队列
GC优化
当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度。
GC优化一般步骤可以概括为:确定目标、优化参数、验收结果。
- 明确目标。明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。
2. 优化参数。通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。
3. 验收优化结果。将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。
读写分离
读写分离是指将读写操作由不同的服务器执行,本质上是将数据库分为主库和从库,主数据库只用来处理对数据库的写入请求,即insert、update、delete等操作,从数据库只用作处理对数据的读请求,即select操作。一般情况下,数据库的写入操作是比较耗费时间的,如果系统在分布式场景下对数据库同时进行读写操作时,系统的并发压力增大,且若事务当前处理的是写请求,在处理过程中会对操作的数据加互斥锁,当其他请求想要操作该被加锁数据时,只能等待前一个线程处理完毕释放锁之后才能接着执行,如果等待时间太长,有可能会出现死锁问题,导致数据库服务不可用,因此实现读写分离可以避免因为数据库写操作对其他查询操作的影响,同时可以有效提高数据库并发性能,缓解数据库并发压力。
具体做法可以对单实例Redis进行分组扩展,将其扩展一组内只有一个mater数据库负责写,多个slave数据库负责读,如下图所示:
这个架构也可继续扩展为多机房场景:
冷热分离
冷热分离是目前ES非常火的一个架构,它充分的利用的集群机器的优劣来实现资源的调度分配。ES集群的索引写入及查询速度主要依赖于磁盘的IO速度,冷热数据分离的关键点为使用固态磁盘存储数据。若全部使用固态,成本过高,且存放冷数据较为浪费,因而使用普通机械磁盘与固态磁盘混搭,可做到资源充分利用,性能大幅提升的目标。因此我们可以将实时数据(5天内)存储到热节点中,历史数据(5天前)的存储到冷节点中,并且可以利用ES自身的特性,根据时间将热节点的数据迁移到冷节点中,这里因为我们是按天建立索引库,因此数据迁移会更加的方便。
分库分表
不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是,可用数据库连接少甚至无连接可用。针对IO瓶颈和CPU瓶颈可分为两种优化方式:
- IO瓶颈。第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。
2. CPU瓶颈。第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。
分库分表的5大方案,百度、腾讯、阿里等大厂都在用! - 腾讯云开发者社区-腾讯云 (tencent.com)
火焰图
火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的用于分析性能瓶颈的可视化图表,火焰图以一个全局的视野来看待时间分布,它从顶部往底部列出所有可能导致性能瓶颈 Span。
绘制逻辑如下:
• 纵轴(Y轴)代表调用 Span 的层级深度,用于表示程序执行片段之间的调用关系:上面的 Span 是下面 Span 的父 Span(数据上也可以通过子 span 的 parent_id 等于父 Span 的 span_id 来关联来对应)。
• 横轴(X轴)代表单个 Trace 下 Span 的持续时间(duration),一个格子的宽度越大,越说明该 Span 的从开始到结束的持续时间较长,可能是造成性能瓶颈的原因。
程序员精进之路:性能调优利器--火焰图 - 知乎 (zhihu.com)
如何读懂火焰图? - 阮一峰的网络日志 (ruanyifeng.com)
巧用 “ 火焰图 ” 快速分析链路性能 - 掘金 (juejin.cn)
异步
在高并发场景下,另一种提高CPU利用率的方法是异步化。我们可以编写异步的,非阻塞的代码。在执行IO操作的时候,让线程继续拥有CPU的时间片同时切换到另一个任务上,当异步任务执行完成时,再返回当前的线程继续执行。异步系统通常每个CPU核心使用一个线程处理所有的请求和响应,因为并不是每个请求都需要创建一个线程,所以连接成本很便宜。 它的成本只是文件描述符和监听器(例如epoll)。由于数据保留在同一CPU上,从而可以更好地利用CPU级别的缓存,并且只有较少的上下文切换,因此可以提高效率。
架构设计 | 异步处理流程,多种实现模式详解 - 知了一笑 - 博客园 (cnblogs.com)
异步架构和响应式(Reactive)编程 - 掘金 (juejin.cn)
分片
分片这里面是指计算量分片。
计算量分片可以运用到某一种计算量非常大的场景,比如说计算直播中礼物数量的排行榜,这里面我们就可以通过多机分片计算各个地区数据,然后再交由统一的汇总系统去合并计算出排行榜,这个思想很像归并排序。
动静分离
动静分离是指服务器对动态和静态请求分开处理,将动态请求(php,servlet等)发送给tomcat服务器处理,而对静态请求(jpg,html,js,css等),则直接访问定义的静态资源目录,不用发送给服务器,这样可以提高网站相应速度,减轻服务器负载。因为Tomcat程序本身是用来处理jsp代码的,但tomcat本身处理静态效率不高,还会带来资源开销。
静态请求和动态请求_qq_42728930的博客-CSDN博客_动态请求和静态请求区别
什么是动静分离?_dijichen0911的博客-CSDN博客
网站“动静分离”分析及实战 - 知乎 (zhihu.com)
分布式消息队列详解:10min搞懂同步和异步架构_处理 (sohu.com)
预计算
预计算是一种用于信息检索和分析的常用技术, 其基本含义是提前计算和存储中间结果,再使用这些预先计算的结果加快进一步的查询。
预计算广泛应用于数据库技术中。比如,关系数据库中的索引其实就是一种预计算。为了快速地检索数据,数据库会主动维护一个数据索引的结构,用来描述表格中一列或者多列数据的缩影。一旦索引的预计算完成,数据库不用每次都重新查找表格的每一行,就能快速地定位数据。假设 N 是表格的行数,有了索引的预计算,数据检索的时间可以从 O(N)减少至 O(log(N)) 甚至到 O(1)。索引作为一种预计算,带来便利的同时也存在一些弊端。当表格中插入新的行数时,就需要重新进行的计算和储存。 当索引越多,查询响应越快时,那其实也意味着要进行更多的预计算,这当然也会显著减缓数据更新的速度。 下列图表展示了索引数量增加后,表格插入行的性能也相应降低 。
Apache Kylin 的预计算是怎么回事?_Shockang的博客-CSDN博客_kylin预计算
为什么预计算技术代表大数据行业的未来,一文读懂_数据库_apachekylin_InfoQ精选文章
Reference
赵俊哲. 网络流量高并发优化处理研究[D]. 江苏:南京邮电大学,2020.
【池化技术】池化技术基础和原理_小熊coder的博客-CSDN博客_池化技术
内存池介绍与经典内存池的实现 - 腾讯云开发者社区-腾讯云 (tencent.com)
负载均衡_魏言华的博客-CSDN博客_负载均衡
1.1MQ的基本概念,优劣势介绍及 RabbitMQ简介_苹水相峰的博客-CSDN博客
超详细的RabbitMQ入门,看这篇就够了!-阿里云开发者社区 (aliyun.com)
C++性能榨汁机之无锁编程 - 知乎 (zhihu.com)
dpdk无锁队列_七夜落幕丶的博客-CSDN博客
从实际案例聊聊Java应用的GC优化 - 美团技术团队 (meituan.com)
如何优化Java GC - 简书 (jianshu.com)
读写分离的几种方式_Morning sunshine的博客-CSDN博客_读写分离
ElasticSearch实战系列十: ElasticSearch冷热分离架构 - 虚无境 - 博客园 (cnblogs.com)
数据架构:概念与冷热分离 - 知乎 (zhihu.com)
分库分表的5大方案,百度、腾讯、阿里等大厂都在用! - 腾讯云开发者社区-腾讯云 (tencent.com)