网站并发----提升网站并发能力以使有限的资源下,网站的并发数更大----------如何让单个请求处理效率更高。
如何提高单台服务器的并发能力:
1.多线程技术,
--如何让一个线程运行的更快同时还要让一个线程执行的时候消耗的系统资源更低。
--有个在linux上做过一个测试,即一个线程创建和消耗至少要消耗2M内存。如果要创建1000个线程,至少要消耗2G内存。这个消耗是非常惊人的。
-- 我们希望请求处理流程只在线程执行这块作用。
--解决方案:线程池。
--JDK里的线程池对线程池大小的设定使用了两个参数,一个是核心线程个数,一个是最大线程个数,核心线程在系统启动时候就会被创建,如果用户请求没有超过核心线程处理能力,那么线程池不会再创建新线程,如果核心线程个数已经处理不过来了,线程池就会开启新线程,新线程第一次创建后,使用完毕后也不是立即对其销毁,也是被会收到线程池里,当线程池里的线程总数超过了最大线程个数,线程池将不会再创建新线程,这种做法让线程数量根据实际请求的情况进行调整,这样既达到了充分利用计算机资源的目的,同时也避免了系统资源的浪费,JDK的线程池还有个超时时间,当超出核心线程的线程在一定时间内一直未被使用,那么这些线程将会被销毁,资源就会被释放,这样就让线程池的线程的数量总是处在一个合理的范围里;如果请求实在太多了,线程池里的线程暂时处理不过来了,JDK的线程池还提供一个队列机制,让这些请求排队等待,当某个线程处理完毕,该线程又会从这个队列里取出一个请求进行处理,这样就避免请求的丢失,jdk的线程池对队列的管理有很多策略,有兴趣的童鞋可以查查百度,这里我还要说的是jdk线程池的安全策略做的很好,如果队列的容量超出了计算机的处理能力,队列会抛弃无法处理的请求,这个也叫做线程池的拒绝策略。
--问题及处理策略:一台服务器由于受限于自身能力的限制如CPU和内存,可以创建的有效线程是有临界值的,当业务请求剧增时,处理策略:当业务要求超出线程池最大线程数的时候,用一个队列先缓存这些请求。等线程池里的线程空闲出来后,再去队列里取出业务请求交付处理。
--线程池中存放多少线程即是合理,如果一开始创建了太多的线程,而且这些线程大部分都会被闲置,就会让有效线程的执行效率大大降低,同时闲置的线程还会消耗系统资源,成熟的设计方案就是设计核心线程数和最大线程数。
--以Java的网络编程为例进一步说明这个问题,网站的网络传输协议是HTTP协议,HTTP协议使用的是TCP协议进行网络通讯的,java里使用socket技术来编写TCP通讯程序,最基本的socket编程里当客户端有数据传递到服务端后,服务端的ServerSocket就会开启一个线程处理这个请求了,但是服务端的处理需要客户端把全部数据传输完毕后才能做后续处理,所以当客户端数据还没传输完毕,处理线程就要在哪里等待数据传输完毕,我们由上表可以知道线程的等待时间相对于CPU执行时间是何等长了,等待的线程什么都没有做的时候还要参入线程的轮询处理里,因此它还会影响其他线程的执行效率。单个服务器本身所能承载的线程数量是有限的,假如某个线程就这样的被闲置起来就会导致线程的利用率十分低下。
--网站的请求其实包含两个操作步奏,这两个步奏分别是IO操作和CPU操作,而线程的作用主要是体现在CPU的操作上,如果我们在一个线程里包含IO操作和CPU操作,特别是使用到很慢的网络IO操作时候,那么IO操作的效率就会影响到CPU操作的执行效率,如果我们能把这两个操作分解开来,让线程只去关心CPU操作,这样单个线程就能被更加充分的利用起来,可是IO操作毕竟是请求操作里不可分割的操作,那么我们到底该如何破这个局了?
--破这个局的方法就是使用reactor设计模式,reactor模式里有一个组件就是reactor组件,它其实是一个单独的线程,客户端的请求首先是发送到服务端的reactor线程进行处理,reactor线程采取轮询的方式轮询客户端的请求,当它发现某一个请求的数据传输完毕后,reactor就以事件通知的方式从线程池里取出一个线程进行后续处理,这样线程池里的每个线程都能被充分的使用。
--Reactor模式处理请求的模式和上节里讲到请求处理模式,它们的核心问题都是发生在请求的IO处理问题上,所以上节的IO处理场景在java技术领域里有个专有名词表述那就是BIO,中文解释就是阻塞的IO,这个阻塞的含义就是指当一个IO操作执行时候它会独占IO处理的线程,其他IO操作就被此IO操作阻塞起来,而Reactor模式下的IO操作在java技术里也有个专有名词那就是NIO,NIO最早出现在JDK1.4版,当时官方解释是New IO,通过我们上面的论述我们发现NIO其实是针对BIO的阻塞问题设计的,所以我们习惯把NIO称之为非阻塞IO,非阻塞IO操作不会阻塞线程的操作。
--想让apache支持上万并发在实际应用中基本无法完成,而且它的并发越高,单个线程的处理性能也下降很快。但ngnix基本上可以做到硬件性能提升,其并发性能也能线性提升的目的。
--首先我们要分析下多线程做并发的问题,线程本身是要消耗系统资源的,如果我们开启线程很多,计算机就得抽取更多资源维护这些线程,所以线程越多浪费掉的系统资源也就更多,这是多线程做并发的第一个问题。多线程做并发第二个问题就是多线程的并行处理机制了,线程的并发要通过线程轮询或者说线程切换来保证,而线程的切换会影响单个线程的执行效率,而且CPU管理线程切换也会消耗CPU的计算能力,所以当我们线程开启的越多,单个线程的执行效率也会随之下降。这也就是Apache容器在高并发下表现出来的问题。
明确了问题,解决方法就出来了,我们如果想让线程执行效率更高我们就不要创建太多线程,这样就可以减少维护线程的开销,同时也能减少线程切换的开销,最理想的方案是我们只使用一个线程来处理所有并发,如果一个线程可以处理好所有并发,那么线程的开销问题就基本可以忽略不计了。按照这个思路我们就要抛弃多线程处理并发的思路了,这样的想法咋一看是不是有点毁人三观了。那么一个线程到底可不可以处理并发了?问题的答案是肯定的。
这里我先不讲解ngnix的设计,我们先讲讲Node.js技术,Node.js作者之所以要创建Node.js,源自于他对如何设计一个高效的Web容器的思考,他认为高效的Web容器要采取异步机制,事件驱动机制和非阻塞的IO处理,看到这个描述我们发现这个和我前面讲到reactor模式是何其的相似,但是Node.js在异步机制和事件驱动做的比上面的reactor模式方案更加优秀,在网络IO处理上,Node.js和reactor模式基本一致,在Node.js有一个专门的模块异步处理IO操作,不过Node.js对IO操作已经完成的请求的后续处理就和reactor模式大不相同了,Node.js只用一个线程完成这个请求的后续处理,这个线程处理请求的方式借鉴了多线程里并行处理的原理,因为请求最耗时的IO操作被抽取出来异步处理,所以处理后续请求的单线程只需要执行请求环节里效率最高的部分,这就让每个请求的处理速度变得非常快了,人眼基本感觉不出这个顺序性操作,不过Node.js单线程处理请求的方式和多线程的轮询机制是不太一样的,首先轮询本身的效率是很低下的,为了轮询,操作系统把线程拆分成若干个步奏执行,这个做法就增加了我们对多线程处理的难度,如果碰到不同线程操作一个共享资源,因为步奏的拆分就很有可能产生错误的操作共享资源的行为,这也就是线程安全问题的源头了,这个问题我后面会将详细讨论,这里就不展开论述了。Node.js吸取了多线程方案的经验教训,它没有采取轮询方式处理请求,而是以队列方式一个个执行请求,这样就可以充分利用CPU的操作时间,同时也规避了线程安全的问题,而且采取这个方式,当一个请求到达服务端后服务端增加的系统资源消耗基本和请求本身的系统资源消耗一致,所以和Node.js机制类似的ngnix服务器当并发上去后,请求处理性能也不会像Apache那样陡降下去。
不过Node.js处理机制里还有一个细节我们要特别注意下,那就是异步IO操作如何和主线程处理协同起来的,也就是当IO处理完毕后我们如何把后续操作插入到主线程下,这个问题看起来很简单,我们直接把新的请求放到请求队列的最后不就行了吗,那么问题来了,这个问题就和Ajax技术的问题类似,Ajax可以异步请求,但是因为请求要通过网络给服务端处理,所以Ajax从开始执行到最后获取响应处理响应结果之间就存在很大的时间差,而这个时间差是很难把控的,也许有时是1秒,有时就变成了3秒,如果我们简单把结果代码放在Ajax处理后面,那么我们只能烧香拜佛希望响应能在代码执行到结果处理位置时候到达,那么Ajax技术是如何解决这个问题呢?Ajax使用回调函数机制来破这个题,当服务端响应完全返回到客户端后,javascript里有相关的事件就会通知回调函数马上执行,这其实就把请求发送和响应处理做成了一个异步模式,这也就是Node.js解释里提到的异步机制和事件驱动了,异步机制其实就是指的是回调函数的使用,这个机制放到Node.js对主线程请求队列的操作里,那就是异步IO处理完毕后,事件机制就会把请求后续操作以回调函数的方式放在请求队列的后面,这样就能有效的保证请求处理的时序性了。
Ngnix的设计思想和Node.js的设计思想类似,不过ngnix使用的不是谷歌的V8引擎完成这个机制的,而是直接使用操作系统的类似机制完成,所以ngnix的并发能力比Node.js会强很多,它和Apache相比那就是质的飞越了。