原文:Python后台开发的高并发场景优化解决方案 - 2019.07.08

作者:金山办公AI平台研发工程师黄思涵

视频:

1. 背景

首先我们来看一组数据。第一个数据是春晚红包,相信大家在今年春晚,大家已经用过这个功能;互动游戏数据是互动次数达到了208亿次。在晚上的短短四个小时之内,去完成了这样的请求量,说明这个互联网的规模是非常大的。把它平摊到14.8亿人的头上,平均每个人也是点击了14.8次,这当中还没有算上这些非网民的次数,所以说,整个互联网的规模是非常庞大的。

再接下来,我们再看一个双十一的一个数据,这个双十一的订单量,在去年数据是13.52亿订单量,我们再看一下,在这个屏幕,左边这里有一个图,从2014年,它大概是1点多亿件,到2015年会有一个增长,到了四点多亿件,到2016年,这边是逐渐的增长,直到2018年增长到了一个13亿次的一个订单量,那么有了这样的订单量,大家可以看到互联网规模的迅速发展,它是逐年在增加的,说明发展速度是非常快。

接下来,我们再看一个数据,那么就是微信,也是平时大家用的比较多的一个软件。微信的日活可以达到十个亿,相应的一个请求量,可能比这个更多。那么看这些数据,它有一个什么意义?这样的一些数据,它就说明了,我们的互联网的一个规模是非常庞大的。而且它的增长也是非常快的。这就引出了今天的一个问题,那么也许大家平时在一些开发过程当中,或者在大家自己的遇到的一个业务场景当中,没有前面那么多比较恐怖的数据,但是,大家知道,公司要去增长,我们的业务要增长,业务的增长,就意味着用户量会去增加,用户量增加就导致了一个请求量的增加。请求量的增加对我们开发人员来讲,或者对我们后台的服务器来讲的最直接的反映,就是服务器压力的增加。那么压力的增加就意味着对服务器来讲,它的一个请求量到达了一个比较高的水平,或者说它的压力呢,已经不能够继续往上增加了,那么请求量已经到达了一个瓶颈。

接下来,我们看一下,它会导致的直接的问题是什么?大家可以看到这样一个简单的模型。左边是一个客户端,右边是一个服务端。客户端向服务端发出请求,当服务端压力过大的时候,对服务器来说,那么就是请求太多,来不及处理,甚至是这样的请求呢,它还会被丢弃掉。但是,对于这个客户端来讲,或者是使用这个客户端的一个用户来讲,它可能面临的一个问题,就是等待时间过长,或者是出现错误,对客户来讲,客户端来讲,他并不知道,你这个后台是面临着这样大的一个压力,或者是你能处理的请求是多大的一个数量,他只能直观感受到,我这个请求时间长,或者我这个请求有问题。这在我们的业务场景当中都是不能接受的。

所以,今天我就将跟大家去探讨一下,如何尽可能多的去处理这样的一个客户端的请求,对于我们来讲,这个服务端的压力大,最简单的来讲就是资源不足。我们可能第一印象就是这个资源不足,那么资源不足呢,非常简单,就是去提升硬件配置,比如说这里CPU,或者是内存,直接去升级硬件就可以了,这样是一个简单的处理方式。但大家有没有想过,还会有一种问题,这种场景下面,就是当我资源充足的情况下,我的一个服务器的请求依然不能够进行提升,或者请求还依然维持在一个比较低的水平,不能够将我所有的资源进行一个很好的利用。这就存在着一个浪费的问题,它的性价比呢,就是不高的。

2. 优化思路和方案

接下来,我们今天呢,主要就是针对下面的这种情况,会去给大家分享我们优化的思路和方案。说到服务端的压力大,最简单的可能想到的一个原因,就是CPU。毕竟CPU在计算机当中处于一个非常核心的位置。

2.1. 查看CPU使用情况

我们先来看一下CPU。我使用TOP命令在这个主机上,去查看CPU的使用情况。在这里,我截了一个图,这个图中,CPU使用率是接近于百分之百,说明CPU,已经是完全被代码,被测试程序是完全占满了。所以它就导致了请求量无法去再进行一个提升。它也就是说明我这个资源不足了。如何解决呢?那么就是去增加资源,去升级这样的一个CPU,这就是我讲的一个比较简单的一个场景。

大家有没有想过另外一种情况,假如说我的CPU它的利用率是在非常低的水平,比如这里,它只有这样一个大概1/10的一个利用率,但是在这个情况下,我的一个请求数量依然不能够增加,我的服务器的压力依然很大。如果在这种情况下,要去给老板说,我这个请求无法提升,我需要去增加配置,这样的话,老板看到这样的情况肯定不会很满意,因为你资源明显没有占满,你还要去申请这样的一个机器。我们继续去查看它的详情,在这里我使用这个TOP命令,在这个之后继续去按一个一,就可以展开四个CPU的一个详情,这里呢,我的CPU是四核心的,看到这里有四个CPU的核心数在这里运行,大家可以看到在这里,第一个确实是占满了第一个核心,但是呢,后面的三个是在这里,空闲的,这说明了一个什么问题呢?就是在前面的这个程序当中,实际上它对整个CPU的一个利用,只是用了它的一部分,而没有全部用,没有全部去用上,那么这种情况,解决方案就变得比较简单了。大家可以想一下,如何去解决呢?很显然,就是要将这样的一个CPU,其他的核心数,全部用起来。另外在现在的CPU几乎都是多核心的,这里我们要尽可能的多的使用我们已有的资源。这里有同学说的多进程,所以我们就可以考虑使用多进程,将所有的CPU去给它利用起来,尽可能的多的使用我们已有的资源。

2.2. 多进程

接下来,我使用Python去给大家讲解一下,如何去使用多进程,来利用这样的一个资源。这里我贴了一段代码,这个代码是Python去实现的一个多进程,然后来利用多核CPU完成任务。

这里我大概给大家讲一下。在Python当中,我们使用multiprocessing这个库去完成这样的一个多进程,这里有一个-1大家要注意一下,通常我们会去留下这么一些核心数,去用作其他的一些任务,防止整个所有的核心数被占满导致其他的任务不能够正常运行。这里算出一个核心的CPU数。下面我是用了一个进程池,进程池大概是一个什么概念呢?这里用了Pool函数,这个函数,会创建出这样一个进程池。创建出了这样的进程池之后,下面它会在当中去加入这样的一些任务,这个Task它实际上是写的一个函数。在这里,就将它加入到这样的一个进程池当中。

实际上,它是异步加入的,会有一个队列,这些任务都会被在非常短的时间内加载到下面的这样的一个队列当中,由于这上面已经有了进程池,这个multiprocessing帮我们实现好了。它会去创建好一些进程,创建好之后,它会自动去把这些加入进来的任务呢,放到这个进程当中去进行调度,然后执行。当前面的一些执行完了之后,后面的议程会再次被调度进去,这个进程池它相对于普通的直接去创建进程的方式有什么好处呢?如果我们直接去创建这样进程,比如有一个任务,我就创建一个进程,然后当这个任务执行结束之后呢,我再销毁这个进程,这样创建和一个删除的过程都会有这样一个开销,这里使有进程池,它就会很好的去解决这样一个问题。接下来,Close,就是去当你不需要加入的时候,就对它进行一个Close掉,Join在这里是进行一个阻塞等待,这里给大家讲了这一段代码,大概做了一个什么事情。

下面,我就给大家看一下我是如何去测试这样的一个多进程,如何去进行一个对比的。我执行的测试流程,是这样的一个流程:首先我会创建了一个队列,这个队列是一个全局的一个队列,而且它在这些进程当中是进行共享,当然它是用的multiprocessing当中的一个队列去实现的。然后,下面会有一个循环,当然这个是单进程的时候,下面会有一个循环,然后循环每次从这个队列当中,这个队列当中去拿一个操作号,这个队列它是从0到9这样的一个数组,从0、1、2、3、4一直到9,这样的一个序列。后面,我在这里会拿到了一个执行号之后,然后再去执行的一个函数,执行了这个函数;下一次循环,就是再次从这个队列当中去再拿出一个执行号,拿出执行号之后,再执行一下这样的一个函数,最终执行结束,当队列为空的时候这里就执行结束了。

这里我再给大家说一下,这个函数是一个什么函数呢?是这边定义的一个计算斐波那契数列的一个函数,这个函数使用递推实现的。说到这个斐波那契数列可以给大家提一下,斐波那契数列大概是长这个样子,写出来可能大家就知道了,第一,1、2、3、5,然后是8,这样的一个数列。就是前两个数相加就等于后一个数,这样的一个数列。我实现它的方式是实现递推实现的,为什么使用这样一个函数在这里呢?比较简单的一个理由,就是它比较耗时,我没有进行优化。耗时的话,我这里就可以去清楚地来对比单进程和多进程它们之间的一个优化的一个那个结果,那么多进程的时候,同样是有一个全局的队列。全局的队列之后呢,下面会有多个进程,同时在这个队列当中去拿这个操作号,依然是0到9操作号。拿了操作号之后,每一个进程独立的去拿到进程号之后,独立的去执行斐波那契数列,直到这个执行为空,这个队列为空的时候,就算作是整体执行结束。

这里讲了我的—个测试脚本的流程之后,我们来看一下它的结果。在单核的时候,它也就是前面我执行这个单进程的时候,它依然是把一个核心代码,这样我是展开来看的,一个核心代码。后面使用多核的时候,大家可以看到,它将所有的核心数几乎都是再一个比较高的使用水平上面,这里呢,我的代码呢,它并没有去留下一个核心数来作为预留,因为我为了去测试它的效果,让它看起来更明显,我直接将四个核心都用上了,这里大家也可以看到它的执行时间的结果,当使用单线程的时候是15秒,使用多进程的时候是在4.8秒。它看起来大致是在一个4倍的一个左右的时间,当中要考虑这个进程的开销,它这个4倍是一个理论值。

这里讲到多进程,大家可能有些同学也会有有想,既然多进程可以,那么我们是否可以用多线程实现呢?是的,多线程也能完成这样的一个工作。

2.3. 多线程

接下来我们看一下多线程如何去执行,首先在这个进程的使用进程的时候,它的进程时间的通信是比较不方便的,因为大家知道这个进程是系统分配资源的一个最小单位,所以说呢,它这个一些变量,包括你要在进程之间去进行共享数据,这样的时候,就会变得不太方便。面临一个问题,就是创建或撤销的时候,它的系统开销是比较大的,所以说因为它的开销大,你不可能去创建大量的一个进程在这个地方去执行这样的任务。还有一个,就是进程的切换,它的耗时是比较大的,当然它这个耗时,这里指的耗时是跟与多线程比较来讲的。

那么多线程相对于进程来讲,它就会有如下三个特点。第一个,就是通信简单,可以在这个多个线程之间去共享数据,会比进程之间来的比较简单,而且它的开销小,就意味着,可以去使用更多的线程,同时呢,它的切换也是比这个进程更轻量,更快的。

那么接下来,我们看一下,如何使用这个多线程去改写当前的一个测试的一个代码,那么这里是多线程的一部分关键代码,这里呢,大家注意到,队列queue 它实际上是一个全局变量,这样的全局变量在这里就并不是使用了一个Multiprocessing里面队列来实现的,而是简单的,直接使用了这个Python当中的列表去实现的,同样的,它依然是去拿到操作号之后,执行的斐波那契的序列,这里去起动线程的时候,在Python当中,这里我使用了Thread这个库,然后去创建线程.所以同样的,大家看到右边这个流程图,它和我们多进程的一个流程图类似。在这里,因为它也是有一个全局的一个队列,在这个队列,实际上在这里,它是一个List,这个List,当然下面所有的线程都依次的从这个列表当中去拿到了这样一个操作号,然后再去执行斐波那契数列,最终呢,直到这个List为空的时候,所有的程序执行结束。

我们看一下它的执行结果,同样的是单线程的时候,依然是一个单核CPU被使用的,利用率非常高,那么多线程的时候,大家注意一下这样的一个结果,它所有的核心这里看起来,都使用起来的,而且它处在一个非常低的一个使用水平,这里是我们程序被优化了吗?大家想一下这个问题,看起来是,就如果直观的从这样的一个结果来看,好像是被优化了,而且它的一个利用率还比较低,但是我们来看一下它的一个结果,在单线程的时候,它是14.52秒,在这个多线程的时候有一个,时间相对于这个单线程它还增加了,这是一个什么问题呢?这里有同学说,数量越大,效果就能体现出来。

2.4. 全局解释器锁(GIL)

接下来,我给大家解释一下,大家可能就会明白了,要解释这样的,为什么多线程反而在这里会比单线程的时间长?首先我们要去知道Python当中的一个非常著名的问题,就是全局解释器锁这样的一个问题。为什么Python当中会有这样的一个东西呢?这个东西叫GIL(Global Interpreter Lock的缩写),是在很早之前,并发编程出现的时候,就会涉及到数据一致性的问题。Python社区最开始为了解决这个问题,提出了全局锁。全局锁控制线程之间,或者是并行的任务之间的数据一致性的问题。全局锁确实很好地解决了这个问题,不过它就会带来一些问题,就是使得任何一个时刻,只会有一个线程在执行。这样也就会带来一些性能上的问题,我们经常会听到的Python的并发性不好这样的一些说法,后面我给大家讲一下,大家可能知道这样的一个原因所在,是为什么,就清楚了。

接下来,我们先看一下这个数据一致性是什么样的问题?假设我这里有两个线程,一个是线程一,一个是线程二,如果这两个任务我们同时是执行的了一个同样的任务,有一个A是全局的变量,它是线程一和线程二都能够同时访问的一个变量。线程一,它要执行的任务,就是先给这个A赋上一个值,比如说赋上一个1,再第二步它再去,给这个值进行一个自加,即A等于A加一;线程二,它也是执行的这个功能,那么也是A,然后它是等于二,然后它是进行一个自加,即A等于A加二。这里,如果两个线程单独执行,那第一个线程执行的一个结果就是A等于1,A=1+1,那它执行的一个结果就是A就等于2;下面这个,如果是单独去执行的话,它执行的一个结果呢,就等于4。这样是没有问题的。

但是呢,如果这两个线程,同时去运行,而且不去控制顺序的话,那么有可能,我先执行了一个A=1,然后再执行了A=2,这个时候,A=2;这个时候,大家去在这个线的位置,假如说它们是有一个交叉的,在执行的上面这个,A=A+1,那么这个最后呢,在线程一执行出来A的结果,它就等于3了。在上面这个执行完了以后,再去执行下面这一个线程二的结果的时候,它就变成了A=5。大家注意没有,这里它会有两个结果,对于一个程序来说它的一个运行时机居然决定了它的一个结果,这种方式是大家不能接受的。也就是说如果有可能它运行的线程,第一个,先运行,第二个,后运行;如果是在这个地方,它的结果是第一个,如果是这种重叠运行的时候,结果是第二个,那么这就会有一个歧义。

在这个地方,为了解决这样一个一致性的问题,就提出了一个全局锁的概念,这个全局锁,那么它是如何去解决这样的一个问题的呢?接下来,我们看一下,在下面这个位置,同样的,先是这个线程一开始执行,执行到这个地方的时候,假如它碰上了一个I/O,碰到了一个I/O的时候呢,它就会进行一个释放锁的动作,释放锁的动作之后,会被第二个线程去获取锁,这里有个获取锁的动作,它再进行执行,也就是说我每一个线程要去执行的时候,我就必须先要去有个获取锁的动作,获取锁之后,再进行执行。这里呢,假如说线程二,它执行的时间比较长,这里我就没有用这个I/O来进行举例了,还有一种情况,Python,这个解释器它内部会去计算它所执行的一个微代码的一个数量,当它微代码的数量执行的足够多的时候,它会进行一个释放,将这样一个线程强制释放锁,强制释放锁之后,下一次,比如说这里,就是线程4,它获取到了这样一个锁,他再进行一个执行。这个地方,微代码的数量大家可以简单的去理解它就是一个执行时间,当它执行时间长了以后,就系统不可能让这样一个线程一直在这里,带着CPU去执行,它就会去对它进行强制释放,当然大家可以简单的这样去理解。当这个线程4,拿到了这样一个锁之后,它又会执行,所以这些线程就在这样的切换当中,依次的去进行一个切换,轮流着来执行,而这里呢,也就解释了,下面这一句话,就是全局解释器锁,使得在任意一个时刻,仅有一个线程在执行,这样的处理方式也就解决了我们上面这样的一个计算的问题,也就是数据一致性的问题,它保证同一时刻只有一个线程去执行。那么同一时刻,也就只有一个线程去对这样的一个数据进行修改,也就不会出现一个不一致的情况。

这里讲了全局解释器锁,我们再回过头来看一下前面的那个问题,为什么我们的一个多线程,反而比这个单线程的一个执行时间会长。大家看到,当多线程它进行一个计算的时候,因为大家知道,这里它会进行切换,绿色的呢,表示是这个线程正在计算,那么它会不停的去进行切换,因为我们前面的这个程序呢,它是一个计算密集型的,它的主要的任务就是进行计算,而且它在这个当中切换了,就相当于是当它执行的时间足够长之后,被这个系统强制进行释放。

比如第二个CPU,拿到的这样一个锁之后,它进一个执行,这里再进行一次释放,然后被CPU0拿到了进行执行,所以它这里会进行非常多的切换,这样的切换,它当中会是有代价的。它这样切换的代价带来的结果,最终执行的时间会是比原来单线程执行的要高的,这里就是一个原因。

2.5. CPU密集型

这里,那么看起来,多线程它会不会是没用呢?其实多线程它也有自己的一个使用场景,那么我们这里已经提到了一个CPU密集型,也就是说计算密集型,它也称之为是CPU密集型,简单的去理解,就是在程序当中,大部分时间花在计算上面的,比如说,前面我讲的这个事例,它计算了一个斐波那契数列,它的所有的时间都是耗在计算上面的。并且这样的一个程序呢,没有去进行使用递推没有进行优化的话,计算量会非常的大。这个大家下来自己去尝试一下,去计算这样的一个数列,然后去比较一下它的时间,就知道它的计算量。由于它的计算量都在计算上面,所以说呢,它比较适用于这个多进程,多进程它就可以让真正的让所有的CPU都去参与到这个计算当中。

2.6. I/O密集型

好,那么还有一种计算的类型,就是这个I/O密集型,所谓I/O密集型,就是程序当中,大部分的时间是花在这个数据传输上面的。比如我们的要在程序当中去读入一个比较大的一个图片,或者是读入一些比较大的一些支点数据,或者从数据库读一些比较大的数据出来,那么要读取很大的数据,它就会称之为是I/O密集型。当然大部分时间,也是相对于这个CPU的计算时间来讲的。同学们再回忆一下,前面我讲的一个多线程切换的时候,当遇到I/O的时候,它就会进行切换,所以当它进行一个I/O的时候,它切换到其他的线程进行一个执行,那么多线程是可以对这种场景下的一个程序进行优化的,这里呢,就给大家讲,解释讲了两种,一个是多进程,一个是多线程。

2.7. 协程

大家在平时的Python编程当中,还会听到一个值,叫协程。那么协程是什么?这里我再补充给大家说一下。协程它就是一个用户太轻量级的这样一个线程,它有自己的一个寄存器,而且上下文切换,比线程还要更轻量,这样说起来可能比较抽象,所以我在这里用代码,再给大家去演示一下协程是如何工作的。这里我有三个工作的一个函数,第一个函数,task0,当中我使用了这样的一个Sleep,去模拟I/O,Task一我又Sleep 3秒,Task2,就去Sleep 0秒,然后呢,在这个下面这个地位Join去把所有的任务加到这个协程当中,然后去来进行一个计算,它最终执行的流程,是从task0开始,当它执行到task0的时候,遇到了这样一个I/O,5秒这个位置,遇到I/O之后,它就会进行切换,切换到下一个开始执行;切换到task1的时候,它也发现,这里也有一个I/O,那么它会继续切换,切换到Task2的时候,这里它也会发现,这里有一个sleep0,大家注意在这个,这个协程当中呢,它遇到了这样的一个sleep0的时候,虽然这里Sleep0,但它依然会触发一次切换,所以它最后还是会切换到Task0;之后,它会继续进行下一轮的一个执行,当然这里看起来会有这么多流程。

实际上呢,它的一个切换的速度是非常快的,它的执行速度非常快。所以看起来,这样的一个程序最终的一个结果看起来会是一个并发进行的,所以这里最终执行的一个结果呢,大家看一下,如果直接去串行执行这三个任务的话,它就会是一个8点多秒,大概是在5和3的加起来的一个值。如果是协程来计算,大概在5秒的样子,那么5秒的样子,它也就是5、3和0,这三个取得一个最大值,因为它这里会不停地进行切换了。最终它的值会处在,就以这个最大的值是它的一个结果,来讲它也是适用于I/O密集型的程序。实际上在这个工程开发当中,协程用的多的是在这个非阻塞,异步并发的情况下,用的是比较多的,那么比如这个有一个Web框架,它就是用协程去做的,这个服务器的压力依然很大,就是对我们来讲,这个服务器的请求的数量依然不能够有一个提升。

2.8. 评估磁盘I/O

那么接下来,我们还要从哪些方面去考虑呢?接下来就是磁盘。大家想一想,磁盘有没有可能成为阻碍我们这个请求提升的一个因素?

要去查看一个磁盘目前的状态,这里我提供两点思路。一种,就是使用TOP命令,可以去看到磁盘信息,这里有大家注意到,磁盘信息,这边有一个WA,这个词它就表示的是等待输入输出的一个时间一个比例,当然这个值是我模拟出来的,它当时处在一个非常高的水平。这就说明,目前的一个I/O读写的一个能力,是在一个比较有压力的位置。还有一种情况,就是去直接查看业务代码,大家对自己的一个业务代码相信是非常熟悉的,那么要去查,是否对磁盘的处理能力有要求,或者是否磁盘处理的能力压力大的情况下,就查看业务代码也可以,这也是比较方便的一种做法。大家知道在这个业务代码当中,比如说这里有一种读文件的Read函数,这里我写的是伪代码,假如大家在这个平常的一个业务代码当中会发现有这种场景,就是去读一些大文件,或者最常见的是读一些图片,或者是呢,读一些比较大的二进制文件,或者是一些其他什么,比如一些AI训练当中有一些模型训练当中一些文件,这个对这个磁盘的要求非常高。

好,接下来,我们就希望去提升这磁盘的读写能力,我们接下来就会去针对这个磁盘进行优化。当然说到优化,依然是从硬件开始。这里我有一个程序,这个程序它是在读取磁盘。对它进行优化最简单,直接就将这个磁盘换为SID,从机械盘换成一个固态盘。方式虽然简单,但是对于企业来讲,或者对于大家的业务来讲,这是一个成本非常高的一个选择,大家知道这个固态盘它是非常贵的,相对于机械盘来说,它是非常不划算的,那么我们有没有一种方式,能够用很多的这样的一些机械盘,比较廉价的机械盘,把它拼起来成一个大的盘,或者让它并行的去进行一些处理?这样我们的速度就能够进行一个提升,这样就优化了我们的思路。

但是大家看一下,如果我放了这么多盘在这里,大家看到中间这么一个图,我放了很多盘在这里,那么对程序来讲,我到底是访问第一个盘呢,还是访问第二个盘呢,还是访问第三个盘呢?这对程序来讲,又会是一个很大的难题,那么这里,我就想,如果我有这么一个工具,假如在这里有一个工具,它能够帮我把上面的这些磁盘全部屏蔽掉,让我不知道下面有这些磁盘,它来帮我做这样的一些事情;而这个程序,只需要去读这样一个工具就好了,并且我希望读这个工具,跟我直接读一块盘是一样的,那么就更好了,那么有没有这样的一个工具帮我们去做这样的事情呢?

2.9. 磁盘阵列

接下来,就是我要给大家讲的一个磁盘阵列,这个磁盘阵列技术(RAID),那么这里呢,会有几种RAID,实际上RAID对读操作来讲,它本身是一个硬件,是一个RAID卡,这个卡可以插在这个主板上面,或者是有些主板上面它直接就带有一种RAID卡。RAID卡的作用就是将这些磁盘去连接起来,可以认为这上面它就是一个RAID卡,把这个磁盘进行一个连接,它当中会去做一些事情,来把这些磁盘组织起来,具体如何去读来一块数据,都是由它来进行组织的。而对于我们的一个计算机来讲,直接去读这个RAID卡就可以了。

这里讲的RAID也会有几种类型,它对磁盘的管理,比如说这里有个RAID0,第一个,我们给大家讲一下,那么RAID0,它就是使用两个以上的磁盘并联,它的速度是在所有型号当中最快的。这是一个什么含义呢?就是比如这里我用一个D0和D1,这是两块盘那么就将它并起来,这就是我们前面所讲到的我们所希望完成的一个功能,就是把这两块磁盘并列起来,把它的数据由这个RAID卡自己去决定,这一份数据是写在第0块盘上,或者是第一块盘上,这个都是由RAID自己去决定的。

但是这里大家会发现一个问题,它的是没有容错能力的,因为我的一份数据,要么写在第0号上面,要么写在第1号上面,如果这个数据丢失了,那么它就没了,就跟我们使用一块盘的时候是一样的,而且大家知道这个随着我的这个盘,比如这里我再接上一块盘,随着盘的增加,它出错的一个概率,或者说数据丢失概率也会增加。那么我们就想一下,有没有一种方式它更快,能够满足我们快速的需求,而且它还能够帮我们进行一些备份,或者进行一些容错?这个就是要引出今天要讲的RAID1。RAID1是如何做得呢?它就是两组以上的磁块互做进项,比如我有一个D0和D1,那么有了这两块盘之后,它就是对于这个RAID来讲,来了一份数据之后,是第一块盘上存储一份,第二块盘上存一份,那么这样的一个数据呢,相当于,它就做了一个备份,而且它的读写速度也几乎是等于这个RAID1的一个读写速度,几乎是等于RAID0的,它们两个读写速度几乎一样。

但是呢,大家知道,它的缺点是什么呢?虽然它有一个容错能力,比如说我第一块盘丢失了,那么第二块盘依然存在一个完整的数据,并不影响我们上层的逻辑的一个功能。但是这里,同学们注意一下,它的每一份数据都是存了两份的,而且如果这里还有第三块盘的时候,那么相当于第三块盘,它也会把样的数据再拷贝一份到这来,这样的RAID1对磁盘无疑是造成了一种浪费,那么这个也是一种不经济的行为,就是比较浪费的。

对我们来说,这两个方案各有优缺点,第一个没有容错能力,但是它一个对磁盘的空间的利用率是比较高的。如果是第二个方案,它虽然是有容错能力,但是它对磁盘的利用率不高,现在有没有一种比较折中的方案,让我们能够去把两种方案进行一个结合,如果有这种方案,这种对磁盘的一个阵列的一个解决呢,就算是比较完美的。

接下来,我再给大家讲一下这个RAID5,RAID5就属于一个这种的方案,那么这个折中的方案是如何实现的呢?它的一个原理是什么,至少需要3个盘,这个是必不可少的。这里我是用四块盘来作为演示的,数据存到上面,它是不是直接存的,就是对于这种备份据它也不是直接备份的,然后,它呢,是将这些数据进行一个奇偶校验,校验之后,然后存到了另外的一块盘上,比如说这里,我对A1,A2,A3这样三个数据,对它进行计算一个奇偶校验,计算出的结果我放在了第四块盘上,对于B1、B2、B3进行一个计算,放到了中间这块盘上面。这样做有什么好处?假如现在我的一个磁盘,比如最后一个磁盘已经坏掉了,那么这个磁盘坏掉了之后,我就依然可以对它数据进行一个重建。现在坏掉了之后,当我插上一块新的盘之后,这个RAID它会对这个数据进迁移,迁移之后,比如这里,我要去计算这个AP的时候,重新根据这个A1,A2,A3去进行计算,就能够得到这个AP,那么对于这个B3它依然可以根据前面已有的三个数据去计算出这样的一个A3的数据,所以最终得到的一个结果,就是这个第四块盘可以被完整的一个重建出来。这个RAID5,它也有速度快,而且呢,它也可以去进行一定的容错,所以那么这个方案算是比较折中的一个方案,这里就给大家介绍了三个RAID的一个型号。

这里大家还会想一个问题,就是说这个磁盘虽然它的速度比较快,但是我们还能不能去更快的访问呢?这里就涉及到一个问题,就是大家想一下,在这个计算机当中,我们除了能够在磁盘上面去存储数据,还能够在哪里去存储这样的一些数据呢?也是说在计算机当中,我们的存储设备还可以有哪些呢?这里有同学说到了这个内存。

2.10. 内存

那么接下来,我们就想一下,能不能把这样的数据直接存储内存里?因为内存的数据相对于这个磁盘来讲,它是一个数量级上的提升。

这里就给大家讲到了一个优化的点,那么就是去运用缓存。比如我们这里有两个场景,一个是外卖,一个是电商。假如说外卖,我们去点这个外卖信息的时候,或者查看这样的一些商家信息的时候,每个人都去查找,而且它返回的数据都是同一份,都会需要去从这个数据库当中去查找这样的一份数据,而且这样的数据,它变更并不是非常的频繁,对于电商来讲,他也是,电商来讲,他给我们去看一个商品的信息,那么这个商品的信息它也变换的不是非常频繁,而且信息都是相同的,而且每个用户去访问得到这样的信息也都是相同的。如果放在数据库当中,或者放在这种磁盘上面去访问的时候,势必效率会非常低,这里我们就考虑把它放在内存当中。

这里给大家介绍Redis。它是一个内存数据库,那么用Redis来做一个缓存,像这样不是经常更新的数据,而且不是这样经常更改的数据,将它放在这个内村当中,当这个程序去访问想要访问数据库的时候,先要查这个缓存,通过缓存当中拿到数据,这样它的效率就会进一步的提升,我这里呢,给大家讲一下,如何去通过这样的一个缓存去读写数据,大家可以看到,第一个,如果去获取数据的时候,上面我们是直接读的数据库,而下面呢,我是先去读这个缓存,如果缓存当中有,那么直接就返回,就得到了一个这样的数据,如果缓存当中没有,我就去读这个数据库,读了数据库之后,大家一定记住,下一步是要去把这个读到的数据写到缓存里面,方便下一次读。下一次读取的时候,我就知道这个缓存当中有这个数据,就不用再去查到这个数据库,这个就是一个读取数据库,如果要写入数据的时候,要如何去做呢?写入数据的时候,就是直接写数据库,同时写的时候,依然要记住去更新一下这个缓存当中的一些数据。

2.11. 软件存储优化

这里就是缓存的一个最简单的一个读写的模型,就是右边这种形式。那么如果大家想一下在这种情况下面,它有没有一些问题?或者有没有一些容易出错的地方?这里给大家提示一下,第一种呢,假如我这个数据库,这个程序,假如我这个程序是访问这个缓存的时候,把缓存当中没有数据,那么它是不是要继续去访问这样的一个数据库?访问这个数据库,它依然是没有数据库,那么相当于,我这个缓存并没有起作用,而且它还增加了这样的两次访问,这是一个问题,还有一个问题,假如我这样的一个缓存宕机了,那么宕机了之后,缓存崩溃,宕机了之后,所有的请求都会走到后面的这样一个数据库去,那么这里也会存在一个问题。

接下来给大家讲一下这两个问题。它的一个比较常见的解决方式,第一个就是缓存穿透的问题,就是刚才给大家讲到的,假如说我在这个缓存当中去访问,并没有找到这个数据,而且在这个数据库当中也没有找到,那么这种情况呢,相当于是绕过了这样的一个缓存,如果解决的话呢,第一个去给这个查询的时候,在数据库当中查询为空的时候,那么也去给这个缓存当中写上这样一份缓存,那么下次获取的时候,但是我知道这个数据是不存在的,而且数据库当中也没有,那么我就不会再去访问这个数据库了;还有可以去增加一些过滤器,去给它提前进行过滤。

那么还有一个,刚刚有同学提到了,是一个缓存失效,缓存崩溃的问题,缓存崩溃,或者是缓存失效,因为缓存我们会有更新时间,假如你设置了一个同样的更新时间,在这个统一时间内,这个缓存都失效了,那么他们就会大量的一个重复更新的一些请求,需要从数据库更新到这个缓存当中,这个请求对数据库来讲也是一个比较有压力的实行,我们设置缓存失效时间的时候,就可以设置成一个随机的时间。或者在这个数据库读写的时候,我们要去控制它的一个读写的线程,那么这里呢,就是提到缓存可能会存在的两个问题,讲到这里呢,几乎我们已经涉及到了一个对CPU的优化,和对这个存储方面的优化。

2.12. 网络优化

那么接下来,大家想一下,还有可能是哪一方面?可能会去进行限制请求数量的增长。那么就是网络问题。

首先要去了解网络问题,我们可以先去查看一些,最基础的可以查看一些硬件信息,比如说查看网络带宽,或者去查看一下程序在运行的时候,我们在后台查看一下网络当前的流量是否已经到达了非常高的值,这里给大家提一下,两个点,这个使用的命令是IFTOP命令,如果没有软件的话,可以直接去装上这样的一个软件,就可以了,那么下面还介绍一下这个TX和RX,这两个呢,分别是发送和接收的一个流量,就是发送和接收的流量,大家通过这两个值也可以看到当前网络的一个情况。

针对网络的问题,依然我们最简单的能够想到,一个硬件问题,那么硬件问题呢,就比较好解决了,那么就去选取网卡,如果是自建机房,就比如用千兆网卡换成万兆网卡,或者是变口换成端口,如果是云平台,大家就需要在这个控制台上去升级带宽。

当然这样的方式是比较简单的。还有一种呢,就是DNS域名解析的问题,大家知道这个我们访问域名的时候,实际上是通过这个域名解析的一个服务器,当中去拿到了这样的一个实际的IP,并且通过这个IP去进行访问,那么如果说这个域名服务器它解析的速度非常的慢,这个就会导致我们的服务器,整个请求链时间也会变得慢,那么这个解决方法也是比较简单的,就是会去更换域名服务器,这些域名服务器的提供商,我们也可以去选择一些更优质的一些服务提供商去来进行服务。

2.13. 内核参数优化

接下来,网络优化呢,接下来给大家讲一个稍微复杂一点的优化,那么这个呢,就是一个内核参数的一个优化。

大家装系统的时候,默认了系统内核的一些参数,它不一定适合你当前的业务场景,包括你的一些硬件配置,这些内核参数,可以去对它进行一些调整。

这里要讲这个内核参数的优化。我就先给大家去讲一下这个关于握手连接,给大家回忆一下。在这个连接过程当中,首先是服务器会去监听你的端口,绑定一个地址,监听你的端口;然后,由客户端去发起一个请求,发起了请求之后,发送一个SYN包到服务端,服务端接收了线包之后,他会回一个SYN加ACK,最后客户端接收到之后,它会再回一个ACK到服务端,这三步完成之后,整个连接就建立起来了.基于这样的一个思路,那我们在这个系统当中,实际上它会维护两个队列,一个队列是半连接队列,第一个队列它维护的是,里面存的是一个什么样的一些东西,存的就是这个客户端,过来请求的时候,它一些来不及处理的这样的一些连接,那么它会放到这样一个队列里,大家知道,如果说这个连接是一个一个过来,是没有问题的,当如果是大量的这样的请求过来,服务器不能在瞬间去处理好这些请求的时候,它就会需要放到这样的一个队列里面,去进行一个处理,接下来一个全连接队列,就是这样已经建立好的一些连接,就会放到这个全连接队列当中。这里假设两个队列的原因是什么呢?因为大家想一下,这两个队列的长度就决定了,我们这个系统当前能够接收到的一个请求的一个上限。

这里我给大家讲几个比较关键的参数,比如第一个参数,当然这个参数比较长,后续大家拿着课件之后,可以再仔细的去研究。第一个参数呢,是来控制这个SYN半连接队列长度的,后面的1024是它的一个默认的一个值,也就是装好系统之后,默认的是1024,当然这个值对系统来讲是一个比较小的一个值,我们可以通过去修改这样的一个值,让它的把这样的半连接队列去变得更长,我能够允许这样一个名词一个连接的长度可以变得更长。

那么下面还有一个就是这个全连接队列长度,就是这个程序控制的,当然这个值,这个地方是128。它控制的就是这个全连接的长度,它的长度也就决定了,我这个系统当前能够维护多长的一个监听的一个队列,这里还要给大家提一点就是,我的半连接长度,实际上受制于这个全连接的长度,也就是说,我半连接长度的这个值,哪怕这个地方是1024,但是它实际上能够起效的一个值是128,它会选这两个值当中最小的一个值,去进行一个生效,这里讲了两个连接队列。

最后再给大家讲一个,这个是一个Time_Wait,而维持Time_Wait最大数量的一个参数,那么它们是为什么是在Search,Watch断开当中会出现的一个状态?那么这个Time_Wait状态如果过多的话,也有可能去把这个系统就拖死,所以呢,设置这样的一个值也可以去帮我们进行优化,这里讲呢,这几个值要如何去配置呢?也稍微提一下,要配置的时候,这里先讲一个sysctl-a,去查看目前已经生效的一个参数,通过sysctl-a可以去查看,但是如果直接去使用了时候,它的一个会显示出来的值会非常的长。

所以呢,这里用Grep去过滤一下,这里我过滤了一个值。要修改也是比较简单的,就直接去修改这样的ETC下面的配置文件,然后使用Sysctl-p去进行一个生效就可以了,当然这里需要给大家提醒一下,需要去注意的,修改内核参数的时候,需要对系统有充分的了解,然后再去修改,而且这个值也并不是越大越好,需要经过一系列的测试。根据你当前系统的一个运行配置,然后来测试,还有你的业务才能决定,到底什么样的值是比较符合这样的一个系统的。

2.14. 负载均衡

那么我们接下来再看一下,因为我们已经讲到了CPU,也讲了存储,还讲了这样的一些网络上的优化。如果我还想,这个系统能够再进一步的去提升它对外服务的一个能力,还有哪些方面可以去进行一个优化。

在目前的业务当中。单机是比较难去满足这样的一些场景的,通常我们就会去进行一个横向扩展,横向扩展之后,它可以去优化整体的服务能力,因为对用户来讲,或者对客户端来讲,它只要能够访问到这样的一个服务,它并不关心后面是一个服务器,还是一个集群对它进服务的。但是这里也会带来一个问题,那么就是集群会带来一个你的程序,或者是你后端的一些设计复杂度提升。

这里讲到了个集群。那么对用户来讲,我要如何知道,我去访问后面,到底访问哪一台机器呢?那么这里就会去讲到一个负载均衡的问题,对用户来讲我期望去访问到中间这台服务器,那么我期望有这样一个东西在这里,让我去访问它,而它帮我去做一些事情,它来由中间这个服务器来决定,最终这个请求是到了那一个后端,那么这样呢,对客户端来讲,那就是比较友好的。所以这里呢,中间这样的一个服务器就承担了任务分发的一个角色,那么这个中间这个一个抽象的服务器,它可以是硬件,它也可以是单独的一个服务器,上面装了这样一些软件,硬件这里我列了两家,一个是F5,一个是A10 Networks,这两个公司他们都是非常好的硬件提供商,他们的硬件能够很好的去解决这样一个业务分发的问题。

但是在这样的硬件虽然好,它的价格是非常昂贵的。比如像这个F5的话,这样的设备应该都是在百万级别的,那么这样的一些昂贵的设备,可能在一些业务,对一些业务来讲,它的一个收益并不是那么的明显,或者说对它来说性价比不是很高,这里我们就可以去用这种软件的一个负载均衡,那么软件的负载均衡呢,这里也给大家提供两个思路,一个是LVS,这个是现在已经被纳入了这个Linux内核的一个技术,它是基于OSI网络模型当中第四层(传输层)来做得,那么这个nginx是基于应用层(7层)来做的,当然这两个,他们也有各自的一个应用场景,也并不是说这个层数越低越好,或者层数越高的越好。大家需要注意一下。

这里讲到了负载均衡,它中间这个服务器会进行任务分发。那么它是如何知道,我要把这个任务发给谁呢?这里是随便发都可以吗?还是说有一定的规则?这里就给大家讲一下它的一个常见的这个算法,那么当然随便发我这里是可以是一种叫随机的算法,最常见的有一种算法,叫轮询算法。轮询算法的含义就是它追求的就是一个绝对的均衡,就比如这个地方我有一个请求,要发送,这个请求发送,它是一个中间抽象的这个任务分发的一个主机,那么就是将请求比如来了一个请求,发给第一个机器,那么第二个机器我发给第二个机器。第三个请求就发给第三个机器,那么这样就是一个绝对的均衡算法,它就是进行一个轮询。

还有一个,就是最小连接数算法。它这里会,就计算,去计算每一个服务器的一个当前的一个连接数,然后呢,它会把这个请求转发到一个最小的一个,当前连接数最小的一个服务器上面,这个连接算法,虽然它会比上面的这种RR算法要好,但是这个LC算法实现难度,也会去相对来更高一点。

还有一种就是源地址HASH,这个它能够保证我客户端的一个IP始终访问某一台服务器,它的大致的思路呢,就是首先对这个客户端的IP进行HASH,对服务器的一个列表进行一个取模,取模之后,最后指定到某一台服务器上面。这样就能保证,我同一个IP过来的请求能够到达某一个指定的服务器上面,这里就是一个常见的负载均衡算法。

3. 峰值场景优化

当然前面,这边已经给大家讲了这么多种优化的场景,其实这些场景当中,没有考虑到一个问题,那么就是请求波动很大的情况,那么这种情况是比较难处理的,我单独去把它拎出来说。

在请求这个波动很大的就是我们所遇到的一些峰值场景,峰值场景下,比如,我们来看一下,第一个,它在一个时间上分布不均,假如说是一天的一个时间,这个数轴它是一个请求的次数,单位是百万次,大家可以看到,这里呢,平常的请求都是维持在一个很低的水平,但是到了某一个时间点,它会到达一个比较高的一个峰值,那么到达这个顶尖之后,也就对服务器产生的要求很大,这种场景比如说是外卖平台,它到了这个中午的时候,可能大家点外卖的这个请求量就集中的去爆发了;或者票务网站,比如看演唱会的门票,到达某一时间放票的时候,或者到了某一个流量的明星,他的一个票务的时候,那么这个对网站来说,它的请求量就会是一个有明显的峰值,那么针对这样的一个场景,要如何去进行优化?或者能不能去进行优化呢?

我们看一下,这种情况也会分为两种,第一种就是有规律的一个场景,比如说外卖网站,每天我知道到了中午就会有一个小高峰,每天中午都会有个小高峰,我就可以去运用这种容器技术,或者多机集群的方式,来进行一个定时的扩容,每天到了中午的时候,比如这里到了预测到达的这个高峰之前,我就可以在这个点提前的对它进行扩容,让这个整体的一个服务能力有一个提升,然后能够很好地去处理这样一个峰值情况。然后渡过了高峰之后,比如在这个地方,然后对这样的一些容器进行销毁,可以去节约资源。

3.1. 容器(Docker)技术

那么接下来,我给大家再提一下,一个当前比较流行的一个容器技术,以Docker为例来讲一下。

它的优势,是在于它的一个机器的轻量,它虽然说名字叫容器,但是它只打造了一些代码和代码相关的一些运行环境,而且它的部署也是非常快的,当然部分的一些容器甚至可以做到毫秒级。这个也是与具体的业务场景相关的,平常有一些是秒级,当然有些也可能时间更长一点,这就是它的一个部署的方便性。还有呢,它就是一个移植性非常好,镜像打包,它打包了这个代码和运行环境,那么它在其他平台上去运行也是非常方便的。而不需要去依赖这个环境上面的本身的这个数字机,身的一个这些环境配置的依赖。

还有一个好处是弹性伸缩,基于Docker,基于这个技术现在有很多的调度平台的一些解决方案。比如Kubernetes,这样的一个调度平台。它是非常强大的,而且是背靠Google的,这样的一个调度平台它可以让这个Docker的生命力变得更加的顽强。

3.2. Docker应用

那么接下来,我给大家看一下如何去使用Docker的一个流程,将它和传统方式进行一个对比。

这里我已启动Flask作为Web服务器。例如,当传统方式,我要去启动一个Flask来作为Web服务器的时候,把这里可能首先就要有一台主机,我要去配置运行环境,配置运行环境之后呢,我需要在当中去装上一些工具或者软件,比如Python,首先要有Python,而且,它还要有Flask这样的一个软件,使用PIP去安装上。或者我还需要一个Request这样的一个库,如果有一些其他功能,我甚至还需要去安装更多的一些库。

那么最后呢,就是启动这样的一个程序进行运行。如果是使用容器的方式,我要怎么操作呢?可能在最开始,我就只需要打包好这样的一个容器,有了这样一个Image(镜像)之后呢,我只需要这样一个宿主机上面有这样一个Docker Engine这样的容器引擎,然后通过这个容器引擎下载了这个镜像之后就对它进行启动。

这里就容器的启动方式,可能这里大家的感觉,两种方式都是3步,而且看起来是差不多的。实际上,这里容器在这里它会有两个优势,一个优势就是它比如有非常复杂的代码,在于它的一个可移植性是非常方便的,因为我只要求,这样的一个数据集上面有一个Docker Engine我就可以启动了。或者是,我需要进行多机扩展的时候,比如我现在要再创建一台虚拟机,我不需要去装其他的依赖的运行环境,只需要去有这样一个Docker Engine在这里,那么我就可以去运行这样的一个容器,或者运行一些其他的容器都可以。

这里,就是给大家讲了一个关于规则流量下的一个处理方式。最后大家也许会问,如果实在有这样不规则的请求,那要怎样去处理呢?

3.3. 无规律峰值

接下来我们就讲一个无规律峰值的情况,那么这种无规律峰值的情况,首先当然服务是要允许可异步的服务,就可以对这个流量进行一个削峰,将流量进行分摊。比如说我到这个地方有了一个流量的突增,突增之后对于这个服务器来讲,它肯定是处理不了的,比如对这个地方对服务器来讲,它肯定是处理不了这样突增的请求,那么对如果时间是这样,它的一个请求可能被丢弃,或者被延迟处理。

在这里,我们中间增加一个这样的消息队列,对它进行一个缓冲,当有了这样一个流量洪峰过来之后,我用这个消息队列把这样一个流量给它接住,接住之后,然后再这边再平缓的去给它把这些流量平滑的去放给它的下端,这里就类似于我们的那个防洪大坝这样的一个概念,它可以先把这样的洪水接住,然后再平滑的去放给下游,这样对下游的一个压力比较小的。

3.4. 流量削峰

如何去处理这样的一个队列呢?这里就是削峰的一个场景。

假如这里我的一个客户端,它的一个发送的能力是150万次每秒,而对于消息队列呢,它能够去承载的是一个一千万的一个请求,那么对于这个服务端呢,它是600次每秒的一个处理能力,如果这样的150万的能力请求,直接到后端,是没有问题的,但是呢,我们这个请求如果到达了一个雪山的高峰,比如说这里,到达了八百万,或者九百万次,对这个服务端的压力是非常大,那么它处理不了的请求可能就会丢失了。如果中间有了这样的一个消息队列之后,消息队列就可以将这样上面这样一些多余的流量先将它接住,接住之后,由这个消息队列通过一个平缓的方式将这个流量再发送给服务端去进行一个处理,那么这样呢,所有的请求就会被分摊。大家看一下这个红色的线是六百万次,当请求把这些上面的高峰都处理掉之后,大概就成了下面这个样子,它引入这种又高又尖的这样一个流量请求,最终会变得平缓。

4. 小结

好,那么今天到这里,差不多的一个优化的一个方案大概就讲到这里,差不多结束了,我给大家稍微总给一下今天的主要内容。

那么前面先给大家讲了请求压力的来源,我们要先分析是资源不足,还是资源不合理,如果不足的话,那么就去增加资源,这样就是没有其他更好的办法了;如果是资源不合理,这个就是我们今天主要讲的要去面对的一个问题,资源不合理的时候,就需要去,先去判断它是如何不合理的,当不合理当中,我们还会有一个没有峰值的场景呢,就会去从计算上面去优化,从存储上面去优化,或者从网络上面去优化,或者从集群上面去优化,分为这几个方面去进行优化。

对于有峰值这样比较难处理的请求,我们依然会有两种方案去解决,也会分为规律或者是不规律的,如果是规律的呢,就使用这种容器化定时扩容的思路;如果是可以去异步的服务呢,我们可以去使用队列,进行消息的削峰。

Q&A

(1) 如何不重启服务,把一台服务器加到Nginx负载下?

那么这个NGINX这个加到负载下,Nginx它本身是可以去进行动态加载的,这个使用命令去配置好之后,使用命令去动态加载就可以了。

(2) 这里还有缓存不存在,缓存不存在的键如何避免查询,不存在的键攻击?

这个问题呢,就需要去从这个缓存前面,大家可能去从前面去进行一些处理,缓存本身这样去做处理这样的一个问题可能比较难,大家可以考虑在缓存前面增加一些过滤,这样的一些方法,可以去达到这样的一个目的,防止进行一个恶意攻击。这里还有,我们看一下还有什么。

(3) 云用的是什么方式存储数据的?

这个云用的,现在公有云的存储方式,这种大型的存储用的多的一般是用对象存储,比如像这种开源的一个存储方案,像Ceph,这样的一个存储方案,大家可以去了解一下,在屏幕上写一下。Ceph,这样Ceph是一个现在非常强大的一个对象存储,还有一个呢,是Swift 这样的一个,也是一个对象存储方案,这两个都是开源的,这个Swift 还是基于Python去实现的。这里是两个开源的对象存储,那么像公有云用的多的,像亚马逊的ES3,或者像我们金山云也有 KS3 这样的,它也是基于这样对象存储去实现的。

(4) 内核参数拥抱半连接,全连接?

半连接,如果熟悉三次握手的话,你就会比较方便的去理解这样的一个东西,这里我大概画一下,在第一次握手的时候它会发送一个SYN的一个号给这个服务端,发送给这个服务端,发送给服务端之后,大家知道,这个发送过来,如果是一个,那当然没有问题,服务端会立马处理这样的一个连接。如果是发送了很多,比如说这里是成千上万,或者是几十万这样的一个请求过来之后,服务端它不能够立即去处理这样的一个请求,所以它本身在这个内核当中,它会有这样的一个队列,它会有这样的一个队列当中,它会去存放来的这样一些请求的SYN的一些数据,那么它存放之后,对于这个内核来讲,它处理的实际上是从这个队列当中出来的一些数据,一些出来的一些SYN,它就从这个队列当中去拿到这样的一些结果,那么这个就是一个我们所说的一个半连接的一个队列,那么全连接呢,就是三次握手结束之后,对于这个客户端和服务端已经建立好了一个连接,比如说这里已经建立好了连接,建立好了连接之后,这样的一些连接的请求,它也会存放在一个队列当中,这个队列就称之为是一个全连接的队列,不知道这样说,这位同学可清楚吗?

(5) 流量削峰是系统默认吗?

这个是需要你本身从这个业务层面去实现的这样一个东西,就是你需要去在你的一个代码层面去处理你的一个连接。

(6) 典型的I/O密集场景是什么?

协程不是只适用于这个I/O密集场景,协程在工程当中,实际上协程用的多是这种非阻塞,异步并发的场景用的比较多。典型的I/O密集型场景,典型的IO密集型场景就是和计算相关的。I/O密集型场景就是去读取数据比较多的情况,就是在你的这个程序当中,你要去读取数据,比如说我现在要去对一张图片进行处理,如果我只是对图片进行一个读取,并且保存,这样的一个简单请求,那么这个,它就可能是一个I/O密集型的,或者是我要读入一个一些比较大的一些,比如说我要去读取一个字典,比如一个字典数据,这些数据去读到这个内存当中,那么这都是一些I/O密集型的。

(7) 三次握手,SYN,ACK时候是什么连接?

这里可能这位同学没明白刚刚说的半连接和全连接,半连接和全连接,它应该是这个在为了方便去理解这个系统的时候,给出的一个定义,它定义的呢,就是在这个SYN,最开始发送到服务端的时候,那么这个我再说一下这里,可能有点不清楚,SYN发过来的时候,由于服务端它来不及处理,它这里会有一个队列,去对这个进行一个保存。这个队列就在系统层面,它取了一个名字就叫半连接队列,它跟这个本身的一个三次握手连接的一些定义是没有关系的。

Last modification:July 22nd, 2021 at 03:57 pm