失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 疑难杂症:系统雪崩到底是为什么

疑难杂症:系统雪崩到底是为什么

时间:2021-09-12 14:56:08

相关推荐

疑难杂症:系统雪崩到底是为什么

这周二笔者参加了由CSDN举办的“创作扶持计划”宣讲会,笔者完全被副总裁于邦旭的激情与执着所感染,说实话已近不惑之年的笔者最近已经很少被这样的打动到了,或者我这样的IT老兵除了给CSDN公众号贡献一些评论文章,也还是有机会在纯技术层面给读者们分享一些心得。

在年后尝试写过几篇“疑难杂症”系列的文章,不过想在分析故障原因时,迅速判断问题解决的方向却需要底层机制的“元认知”加持。还是举“创作扶持计划”的当中的例子,在直播时有网友提问说CSDN的网站为什么总爱崩溃呢?于总当时就提到提到使用Springboot架构部署的网站很难出现崩溃现象,一旦崩溃基本就是Redis的误用,这其实就是解决疑难杂症时的底层机制把握在起作用,当然有关Redis的问题处理总结我们在这个系列的下一篇文章中就会有详细介绍,本文先来回答一下如何建立快速判断解决问题方向的感觉。

雪崩的种类

雪崩效应是我们生产环境中所经常遇到的一种情况,一般指系统在平时运行情况良好,而且资源也有冗余,但是就是无法突发流量或者异常情况冲击,只要外界一加压,或者有其它超出预期的情况就会瞬间崩溃的现象,雪崩主要分为两种情况,都与临界值有关:

交易并发问题:一旦并发数超过某个临界值,那么系统的处理效率就会成倍下降,举例说明如下。假如正常情况下每秒能处理100笔并发交易,但如果交易量达到200的临界值,那么系统的处理能力有可能直线下降到10笔以内。

失败交易占比问题:一旦失败交易占比超过某个临界值,也会导致系统处理能力成倍下降。

而使系统处理能力直线下降的原因有很多,不过归纳其本质往往都是CPU流水线的惩罚机制和CPU缓存命中机制所造成的,接下来先从处理器的流水线模型说起。

为什么资源够冗余,可还是雪崩

我们知道电子计算机的一切动作都是依靠晶体振荡所触发的,因此CPU的震荡频率也被称为主频,是CPU处理性能的直接体现,但是由于摩尔定律日趋终结,单纯提高CPU主频以获得更高性能的道路已经逐渐无法走通了,因此芯片厂商开始朝着提升指令处理效率的方向着手进行优化。

以加法ADD指令为例,想完成这个执行指令需要取指、译码、取操作数、执行以及取操作结果等若干步骤,而每个步骤都需要一次晶体震荡才能推进,因此在流水线技术出现之前执行一条指令至少需要5到6次晶体震荡周期才能完成如下图所示:

那么针对这样的问题芯片设计人员就提出了参考工厂流水线机制的想法,因为取指、译码这些模块其实都是独立的,完成可以在同一时刻并行手,那么只要将多条指令的相关步骤放在同一时刻执行,比如指令1取指,指令2译码,指令3取操作数等等依此类推,就跟以达到 大幅提升CPU的执行效果,以5级流水线为例,具体原理详见下图:

由上图可知,T5也就是第5个震荡周期,指令流水线就建立成型,自此以后每个震荡周期T,都可以取到一个指令的结果了,也就是说平均每条指令就只需要一个震荡周期就可以完成。

当然指令流水线的建立也有一个前提,就是CPU必须能提前知悉指令的执行顺序,这也就是指令预测技术,因为一旦预测错了接下来要执行的指令,那么CPU就要清空当前所有的流水线,重新从正确指令的取指执行,此时CPU又退化到需要5次震荡周期才能执行一条指令的地步,这也是造成雪崩的元凶之一。

而根据英特尔的统计,一般CPU平均每执行7条指令就会遇到一次可能跳转的情况。也就是说平均每7行代码就会出现一次if判断,而每进行一次判断,程序就有跳转的可能,而一旦跳转造成指令预测不准确就会成倍的降低CPU的效率。

因此一般CPU都在汇编语言层提供了指令预测的功能,而高级语言一般也有提供给CPU进行指令预测的修饰符,我们在Linux内核代码就可以经常看到unlikely这样的修饰,unlikely本质上对于业务逻辑没有作何影响,只是提示CPU此判断结果出现的可能性不大,不要本段代码放入流水线,以最大程度的保证系统在正常运行时的运行效率。

而一般来讲雪崩时就是这个unlikely分支被大规模集中执行,由于unlikely分支在正常情况下基本没有被执行的可能,因此加上这个修饰符非常有助于提升正常情况下的执行效率,但是一旦发生这样的冷门分支被大规模执行,其惩罚效也非常的明显。这也是一旦错误情况超过阈值,其对于效率的影响是成倍的下滑。

CPU缓存也能惹祸

一般程序员心中往往有一个误区,就是内存访问速度很快,不过事实却并非如此,以英特尔的CPU为例一般分为三级缓存,其中一级缓存速度是寄存器级别的,访问只需要几个指令周期,二级缓存约比一级缓存慢6到8倍,三级缓存再比二级缓存慢8倍,而内存比三级缓存还慢10倍。其实内存的快只是相对于磁盘而言的,与CPU缓存相比内存的速度根本不够看。

为了说清楚这个问题,这里简要回顾一下内存映射到CPU缓存直接连接、全连接和组连接的三种机制。

直接连接:最简单的是直接相连,使用这种策略的CPU一般内存地址划分为区号+块号+块内地址进行寻址,具体原理如下图:

在这种策略下内存与缓存中区的数量是不一样的,一般内存中区的数量远大于主存,例如主存中可能有1024个区,但是内存中可能只有16个区。假设每个区的长度为2^k,那么CPU缓存调度时无论要映射的内存块处于主内存中哪个区,第0块就只能放在对应缓存的块只能映射到缓存中的第0、2^k+1、2^k*n+1(其中2^k为区地址的长度)个块,即将内存只能被映射到相对有限的几个块当中,而不能放在其它位置,这就可能造成比较多的问题,因为很可能出现缓存明明有富裕,但是所有缓存区中的第0块全部被占满的话,这时候其它位于第0块的内存单元就无法再被调入缓存。因此这种策略在实际中使用很少。

全连接:应该说全连接是所有缓存策略中效率最高的,因为他完全没有直接连接策略中限定条件,所有的内存单元均可以自由连接存放,如下图所示:

这也就极大提升了CPU缓存的运行效率,但从这密密麻麻的线路中也可看出来,这样做的缺点是成本太高了,所有的内存单元都要与缓存进行连接,电路设计的难度也很大。

组连接:组相联映射方式下,将缓存分成2^k个组,将内存分为u*2^k个组(内存组个数的正数倍)。内存组与缓存组之间采用直接连接,而与组内各块则采用全连接。也就是说,主存的第m组只能映射到j=mmod2^k的缓存组内,但是内存m组内的缓存块可以映射到缓存j组中的任何块上。如下图所示

组连接其实是一种成本与效率的折中方案,也是目前Intel、ARM等主流CPU使用最多的一种连接方式。

缓存映射策略带来的衍生问题:由于目前缓存策略中使用最多的就是组相连,而由于组内各块是连接的,所以而组相连一般是贪心策略,即如果缓存的组内还有其它空闲的块,那么CPU缓存就将本次调度涉及内存组中的其它块也一并映射到缓存中,因为这样的操作几乎不需要付出额外的时间成本。也就是说内存中相邻的内存数据很有可能会被同时映射到缓存当中。

这也就会造成一个有趣的推论,当你用a[i][j]去遍历一维数据时,其效率远远调a[j][i]

for (i=0,i++,i<len1){for (j=0,j++,j<len2){print(a[i][j];}}

也就是上面这段代码的执行效率远高于下面这个,

for (i=0,i++,i<len1){for (j=0,j++,j<len2){print(a[j][i];}}

这里的原因也很简单,就是由于二维数组的行在内存中是连续分布的,因此当代码读取a[i][j]时a[i][j+1]、a[i][j+2]其实也很可能被CPU映射到缓存内,但是a[j][i]和a[j+1][i]在内存中不连续,因此读取a[j][i]时a[j+1][i]不太可能被读取缓存里,而由于缓存相比内存速度快得多,所以会造成这样奇怪的现象。

而在实际生产环境也会出现类似的现象,当并发数过高时,CPU无法处理连续的报文请求,而需要不断的上下文切换确保将每笔报文都先读取内存,以避免网卡的缓冲区溢出异常的发生。而在CPU不断切换上下文时,缓存不断的重新映射也会极大的影响CPU的执行效率。这也是造成系统雪崩式破坏的重要原因之一。

上述就是我近些年来总结的系统雪崩的底层机制,当然想快速解决生产问题那绝对是台上十分钟,台下十年功的本事,本文先主要讲解一下CPU相关的机制问题,下一次再讨论内存管理的底层机制,接下来初步策划是一个专门针对于Redis的专题案例,也帮助CSDN解决一下可能存在的Redis问题,希望通过这一系列的博客,让读者位快速积累经验,早日成为可以快速解决疑难杂症的老专家。

如果觉得《疑难杂症:系统雪崩到底是为什么》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。