注:本文首发于CSDN,转载请标明出处。javascript
【编者按】 本文来自拥有十年IT从业经验、擅长站点架构设计、Web前端技术以及Java企业级开发的夏俊。此文也是《关于大型站点技术演进的思考》系列文章的最新出炉内容。首发于CSDN。各位技术人员不容错过。php
下面为正文:html
《关于大型站点技术演进的思考》已经连载完了两个系列,它们各自是《存储的瓶颈》和《站点静态化的处理》。这两个系列相应到站点里的组件就是存储端和浏览器端。站点除了这两端外,另外一端那就是服务端了,服务端上接浏览器端。下承存储端,因此当咱们想让站点的浏览器端或存储端性能更加优秀的时候,就不得不去考虑服务端的问题,因为服务端和它们永远都是剪不断理还乱的关联性。前端
现在我要开启《关于大型站点技术演进的思考》这个主题下最后一个系列,这个系列就是 讨论站点组件里最后一端服务端了,因为服务端和浏览器端以及存储端存在着一种永远都剪不断的关系。因此本系列还会解说和其它两端相关联的技术,只是本系列的讲述的深度会更高些,但愿经过这样的深刻的研究让咱们更加深刻的理解那些能做用于浏览器端和存储端的服务端技术。固然服务端除了上接浏览器端,下承存储端做用外。它自身还有本身的技术范畴,那就是怎样使用服务端技术实现站点的业务逻辑了,以上这些就是本系列将要讨论的主题了。java
本系列大概会按下面思路进行讨论:react
固然上面的知识都是我本身多年经验和本身所掌握知识的总结,现在尚未完整的知识雏形,因此在写的过程里很是有可能会依据实际状况进行调整。不管最后结果怎样,上面的列举的慷慨向我都会尽力讲到的。redis
如下就我開始讲服务端和浏览器端相关部分的技术了,首先从站点的并发性開始讲起吧。算法
1)《关于大型站点技术演进的思考》前两系列的内容数据库
存储的瓶颈和站点静态化处理參见本人的 博客。编程
2)站点并发问题概述
什么是站点的并发?这个问题答案很是easy,站点的并发就是指站点在同一个时间可以同一时候处理多个用户请求。谈到站点的并发,很是多朋友很是天然的会想到多线程技术。多线程可以使得一个应用程序并行处理多个计算任务,这个技术的做用类推到Web应用里那就是多线程技术可以让站点并行处理多个请求,所以多线程技术是可以用来处理站点的并发问题的。
因此当咱们要去理解站点的并发问题时候,首先要解决的问题就是怎样使用多线程技术。
当咱们学好了多线程技术。是否是就可以解决站点的并发问题了?回答固然是可以的,只是一个站点对并发的要求绝对不是只要求咱们会使用多线程技术那么简单了,当站点并发的问题解决后。咱们当即就要面临一个相同迫切的问题了,那就是怎样提高站点的并发能力。这个问题落到实处就是怎样让站点在有限的系统资源下并发能力变得更强,换句话说就是怎样让有限的系统资源下。站点的并发数更大,当站点并发数变大之后,咱们又要考虑怎样让单个请求处理效率更高。这两个内容就是本篇文章的主题了。
本篇文章主要是谈论如何提高单台server的并发能力问题,下一篇文章谈论的是当站点处理用户请求的服务端使用了集群技术后,针对并发的处理会发生如何的变化呢。
3)多线程技术
并发技术相应的是多线程技术,而多线程技术又是创建在线程技术上。那么咱们这里首先谈谈线程技术的问题。
第一步我要作的是明白什么是线程,如下是百度百科里对线程的解释。详细例如如下:
线程,有时被称为轻量级进程(Lightweight Process。LWP),是程序执行流的最小单元。
一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体。是被系统独立调度和分派的基本单位,线程本身不拥有系统资源。仅仅拥有一点儿在执行中不可缺乏的资源,但它可与同属一个进程的其余线程共享进程所拥有的全部资源。
从这个定义里咱们知道线程有例如如下特色:
单个线程就是一个独立的程序运行流。咱们在学习计算机语言,写的练习代码都是在一个单线程的场景下进行的,单个线程放到站点并发处理的范畴里,它有个问题是咱们必定要注意的,那就是怎样提高单个线程的运行效率问题,也就是说咱们怎样让一个线程运行的更快同一时候还要让一个线程运行时候所消耗的系统资源更低。为了解决问题,咱们就要分析下单个线程的运做特色,看看线程运做流程里那些方面会影响到线程的运行效率问题。
线程运做流程很是easy。咱们使用线程时候首先是建立一个线程,线程建立好之后就是使用它了,线程使用完成之后就要销毁它了。把这个流程放到请求处理场景里,咱们就会发现线程的建立过程和线程的销毁过程事实上是和处理请求的逻辑无关。但是一个线程又必须经历这三个阶段,所以线程建立的时间和线程销毁的时间也会被统计到请求处理时间里,那么咱们就会想有没有办法可以消除线程建立和销毁所花的时间对请求处理的影响呢?
之前有人在Linux上作过一个測试。这个測试结果就是一个线程建立和消耗至少要消耗2MB的内存。依照这个结论。假设咱们在Linux上建立1000个线程那么系统至少要消耗2G以上的内存。这个消耗是很是惊人,假设咱们每次使用一个线程就来个建立销毁,那么请求处理时候就会要新增很是多无谓的系统资源消耗。假如碰到server系统资源很是紧张时候,线程的频繁建立和销毁的过程就会侵占不少其它系统资源从而影响到线程的运行,这个问题咱们又该怎样来攻克了?
要解决问题咱们首先要回想下线程的运做流程,这个结果例如如下图所看到的:
这张图不是咱们但愿看到的,咱们但愿看到如下这种流程图。例如如下图所看到的:
咱们但愿请求处理流程仅仅是做用在线程运行这块。那么在实际的生产实践里咱们又是怎样来解决问题了?解决方式就是使用线程池技术,线程池技术就是基于线程建立和销毁操做影响性能的角度来设计的。只是线程池技术并非简单的事先建立好一批线程,而后统一销毁一批线程那么简单,这里我以Java的JDK里自带的线程池技术为例来说讲关于线程池技术的使用,内容详细例如如下:
JDK里的线程池对线程池大小的设定使用了两个參数,一个是核心线程个数,一个是最大线程个数。核心线程在系统启动时候就会被建立,假设用户请求没有超过核心线程处理能力,那么线程池不会再建立新线程,假设核心线程个数已经处理只是来了,线程池就会开启新线程,新线程第一次建立后,使用完成后也不是立刻对其销毁,也是被会收到线程池里,当线程池里的线程总数超过了最大线程个数,线程池将不会再建立新线程,这样的作法让线程数量依据实际请求的状况进行调整,这样既达到了充分利用计算机资源的目的。同一时候也避免了系统资源的浪费,JDK的线程池还有个超时时间。当超出核心线程的线程在必定时间内一直未被使用,那么这些线程将会被销毁,资源就会被释放,这样就让线程池的线程的数量老是处在一个合理的范围里;假设请求实在太多了,线程池里的线程临时处理只是来了。JDK的线程池还提供一个队列机制,让这些请求排队等待,当某个线程处理完成,该线程又会从这个队列里取出一个请求进行处理,这样就避免请求的丢失,jdk的线程池对队列的管理有很是多策略,有兴趣的童鞋可以查查百度,这里我还要说的是jdk线程池的安全策略作的很是好,假设队列的容量超出了计算机的处理能力,队列会抛弃没法处理的请求,这个也叫作线程池的拒绝策略。
由上面对JDK自带线程池的介绍,咱们发现使用线程池咱们要考虑例如如下的问题。详细例如如下:
问题三的我在介绍JDK里线程池的设计方案时候已经提到了,解决方法就是当业务方要求超出临界值时候。详细就是超出线程池最大线程数的时候,咱们用一个队列先缓存这些请求,等线程池里的线程空暇出来后,再去从队列里取出业务请求交付给线程进行处理。
至于问题一和问题二。这个就和多线程的技术相关了,为了更好的解答它,我这里先来介绍下多线程技术。
多线程技术的核心就是让多个线程同一时候被运行。用户的感觉就是计算机可以并行运行计算任务,那么咱们首先要理解下多个线程是怎样进行并发操做的。详细例如如下:
一个程序包括两个部分,它们就是计算和存储了,好的程序就和一个活生生的人同样,存储至关于人的臭皮囊也就是身体了,而计算就是人的大脑了,而程序的计算是经过CPU来完毕的,因此线程的并行运行效果就是看CPU是怎样并行处理多个线程的机制了。那么CPU怎样作到并行处理呢?事实上CPU并不能作到并行处理,CPU仅仅能一次运行一个计算指令。听到这个回答,咱们的头是否是一会儿变大了,CPU无法作到并行处理,那咱们看到活生生的并行操做到底是怎么回事呢?
线程技术里有个概念叫作时间片,要解释时间片咱们就要从进程提及了,当一个进程被操做系统运行时候,CPU会给这个进程分配一段运行时间,当进程里面建立了线程之后,操做系统会把进程分配到的运行时间分红片断赋予给线程,假如进程里仅仅有一个线程,那么这个线程的时间片的长度就和进程的时间段基本一致,假设进程里开启的线程有多个。假设这些线程没有设置什么优先级策略。那么每个线程就会平分到一样的时间片,时间片就是线程被CPU运行的单位时间,当CPU依照时间片规定的时间运行了某个线程后。假设线程的CPU计算没有全部运行完成,那么CPU就会让线程先挂起来等因而让线程处于一个等待状态。CPU的调度机制找到下一个线程。当下一个线程时间片所规定的时间运行完成后,CPU就会让这个线程挂起来。再去找第三个线程,这个过程依次进行下去。等进程里开启的线程都运行完成后。第一个线程才会又一次開始运行。这个机制就是线程的轮询机制了,也是咱们常说的线程切换机制背后的原理了。
该机制可以保障在一个固定时间范围内,全部线程都能获得运行,由于线程的运行速度很快,因此给用户的感受就是多个线程是可以并行运行的。
由上面的原理咱们来看看这个实例,详细例如如下:
有一个线程被分配到的时间片长度是10毫秒。假设这个线程没有其它干扰。它运行完成需要花费50毫秒,那就等于要运行5次时间片。假如咱们再新增了一个线程,该线程时间片也是10毫秒。也要运行50毫秒才干运行完成,那么当中一个线程运行完一次时间片。依照轮询机制另一个线程也要被运行。最后咱们就会发现第一个线程运行时间就变成了100毫秒,假设咱们再加上CPU的调度所花的时间,该线程的运行时间就会远远大于100毫秒。尽管100多毫秒人的感官是很是难觉察到,但是这个作法毕竟让单个线程的运行效率大幅度减小了,假设咱们线程开启的不少其它,那么单个线程运行效率也就会变得更低。
有了这个结论咱们再去看看线程池问题一和问题二,假设咱们一開始建立了太多线程。而且这些线程大部分都会被闲置,那么这些闲置的线程就会让有效线程的运行效率大大下降。同一时候闲置的线程还会消耗系统资源。而这些被消耗的系统资源都没实用到业务处理上,因此成熟的线程池方案就会设计核心线程和最大线程的概念。它们可以让线程池依据实际业务需求和系统负载能力作到动态调节。这样就可以下降开启不少其它线程影响线程运行效率的问题,也可以让计算机的系统资源获得更加有效的利用。
4)站点的并发与多线程技术
多线程技术可以实现并发操做。站点的并发场景很适合使用多线程技术,那么咱们就先来谈谈怎样使用多线程技术来实现站点并发,首先咱们从单个站点请求处理场景開始提及吧。
单个网络请求是从浏览器端发送,经过网络传输到服务端,服务端接收到数据后进行处理,处理完毕后服务端再把响应经过网络发送给浏览器端。而这个过程都是使用服务端一个线程全程陪同的完毕,这个作法彷佛没什么问题,这里我先给你们看一个表格,这个表格是Node.js 的做者 Ryan Dahl 为 JSConf 大会所做的演讲里提供的,详细例如如下所看到的:
I/O设备 |
CPU 调用周期 |
CPU一级缓存 |
3 |
CPU二级缓存 |
14 |
内存 |
250 |
硬盘 |
41000000 |
网络 |
240000000 |
从这个表格里咱们发现,网络IO的处理时间是CPU一级缓存处理时间的一亿倍。假如咱们把CPU一级缓存的处理速度等同于CPU的计算速度。这里我若是CPU运行时间是1毫秒。那么当一个线程里有网络操做时候,CPU要等待将近1亿毫秒的时间才被运行,这1亿毫秒时间里CPU不知道可以作多少事情啊。
咱们再以Java的网络编程为例进一步说明这个问题。站点的网络传输协议是HTTP协议,HTTP协议使用的是TCP协议进行网络通信的。java里使用socket技术来编写TCP通信程序,最主要的socket编程里当client有数据传递到服务端后,服务端的ServerSocket就会开启一个线程处理这个请求了。但是服务端的处理需要client把全部传输数据完成后才干作兴许处理,因此当client数据还没传输完成,处理线程就要在哪里等待传输数据完成,咱们由上表可以知道线程的等待时间相对于CPU运行时间是何等长了,等待的线程什么都没有作的时候还要參入线程的轮询处理里。所以它还会影响其它线程的运行效率。
单个server自己所能承载的线程数量是有限的,假如某个线程就这种被闲置起来就会致使线程的利用率十分低下。这些问题咱们究竟该怎样来攻克了?
5)怎样提高线程的使用效率?
要解答标题的问题,咱们首先要分析下站点请求的特色,站点的请求事实上包括两个操做步奏,这两个步奏各自是IO操做和CPU操做,而线程的做用主要是体现在CPU的操做上,假设咱们在一个线程里包括IO操做和CPU操做,特别是使用到很是慢的网络IO操做时候,那么IO操做的效率就会影响到CPU操做的运行效率,假设咱们能把这两个操做分解开来,让线程仅仅去关心CPU操做,这样单个线程就能被更加充分的利用起来,可是IO操做毕竟是请求操做里不可切割的操做,那么咱们究竟该怎样破这个局了?
破这个局的方法就是使用赫赫有名的reactor设计模式,咱们来看看reactor模式的设计图。例如如下图所看到的:
reactor模式里有一个组件就是reactor组件。它事实上是一个单独的线程。client的请求首先是发送到服务端的reactor线程进行处理,reactor线程採取轮询的方式轮询client的请求,当它发现某一个请求的传输数据完成后,reactor就以事件通知的方式从线程池里取出一个线程进行兴许处理。这样线程池里的每个线程都能被充分的使用。
咱们再细致分析下reactor模式,咱们发现啊,reactor模式事实上并无提高一个请求的整体运行效率,而是把请求里效率最低的IO操做和CPU计算操做分红两步进行运行,这个方式就等因而把同步请求操做成了一个异步运行操做,尽管该方式没有提高请求整体的处理效率,但是它能让服务端的线程利用率更高,这也就变相的让站点的并发处理能力加强了,而且该方式可以避免线程被闲置在那里空转的问题。假如咱们能有效的控制好线程的合理数量,那么该方式仍是有可能提高单个请求的运行效率的。
Reactor模式处理请求的模式和上节里讲到请求处理模式。它们的核心问题都是发生在请求的IO处理问题上,因此上节的IO处理场景在java技术领域里有个专有名词表述那就是BIO。中文解释就是堵塞的IO,这个堵塞的含义就是指当一个IO操做运行时候它会独占IO处理的线程。其它IO操做就被此IO操做堵塞起来,而Reactor模式下的IO操做在java技术里也有个专有名词那就是NIO,NIO最先出现在JDK1.4版。当时官方解释是New IO。经过咱们上面的论述咱们发现NIO事实上是针对BIO的堵塞问题设计的。因此咱们习惯把NIO称之为非堵塞IO,非堵塞IO操做不会堵塞线程的操做。
古老的Apacheserver和java里的tomcat容器新版本号都採取了NIO技术。但是Apache和它同类型的ngnixserver相比,所能承载的并发能力实在差太多了,实际场景下Apache能支持5000个并发就要惊为天人了,而ngnix官方文档里就说它可以支持5万并发,而实际场景里支持3万并发那是一点问题都没有的。为何ngnix可以达到如此高的性能呢?它有什么独门绝技呢?
6)C10K的目标
业界有一个C10K的目标。所谓c10k问题,指的是server同一时候支持成千上万个client的问题,也就是concurrent 10 000 connection(这也是c10k这个名字的由来)。
咱们想让Apache同一时候支持上万并发这个在实际场景下是一件基本没法完毕的事情。听到个人说法不知道会不会有朋友不服气。我要是把server的配置搞得高高的。我就不信Apache不能支持上万并发,假设咱们这么作了咱们就会发现server硬件的提高并不能致使Apache并发能力的线性提高。假如咱们设定Apache在5000并发下咱们经过添加硬件性能可以近似的提高Apache的并发能力,当Apache并发达到5000后。咱们再把硬件性能提高一倍,终于咱们必定会发现Apache的并发数并不会变成1万,它能支持到7000以上就谢天谢地了。而且Apache的并发越高。其单个链接的处理性能降低的也很厉害。而这个场景迁移到ngnix上。结果就大不一样样了,ngnix基本可以作到硬件性能提高,其并发性能也能达到线性提高的目的。
因而可知ngnix实在太优秀了。它究竟用什么独门秘籍了?你们是否是很是急迫的想知道了,如下我就来说讲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机制相似的ngnixserver当并发上去后。请求处理性能也不会像Apache那样陡降下去。
只是Node.js处理机制里另外一个细节咱们要特别注意下,那就是异步IO操做怎样和主线程处理协同起来的。也就是当IO处理完成后咱们怎样把兴许操做插入到主线程下,这个问题看起来很是easy,咱们直接把新的请求放到请求队列的最后不便可了吗,那么问题来了。这个问题就和Ajax技术的问题相似。Ajax可以异步请求。但是因为请求要经过网络给服务端处理,因此Ajax从開始运行到最后获取响应处理响应结果之间就存在很是大的时间差,而这个时间差是很是难把控的。或许有时是1秒,有时就变成了3秒,假设咱们简单把结果代码放在Ajax处理后面,那么咱们仅仅能烧香拜佛但愿响应能在代码运行到结果处理位置时候到达。那么Ajax技术是怎样解决问题呢?Ajax使用回调函数机制来破这个题。当服务端响应全然返回到client后,javascript里有相关的事件就会通知回调函数当即运行,这事实上就把请求发送和响应处理作成了一个异步模式,这也就是Node.js解释里提到的异步机制和事件驱动了,异步机制事实上就是指的是回调函数的使用,这个机制放到Node.js对主线程请求队列的操做里,那就是异步IO处理完成后。事件机制就会把请求兴许操做以回调函数的方式放在请求队列的后面,这样就能有效的保证请求处理的时序性了。
Ngnix的设计思想和Node.js的设计思想相似,只是ngnix使用的不是谷歌的V8引擎完毕这个机制的,而是直接使用操做系统的相似机制完毕,因此ngnix的并发能力比Node.js会强很是多,它和Apache相比那就是质的飞越了。
好了。本篇内容讲完了,下篇文章我開始再补充下单台server并发处理的内容。以后我就要讨论并发和集群之间的问题了。
(责编/钱曙光)
做者简单介绍:夏俊,拥有十年的IT从业经验,擅长的技术领域有站点的架构设计、Web前端技术以及java企业级开发。热爱技术,喜欢分享本身的技术研究成果和经验
由“2015 OpenStack技术大会”、“2015 Spark技术峰会”、“2015 Container技术峰会” 所组成的 OpenCloud 2015大会于4月17-18日在北京召开。 日程已经全部公开!懂行的人都在这里!(优惠票价期,速来)
不少其它《问底》内容:
《问底》邀请对技术具备独特/深入看法的你一块儿打造一片仅仅属于技术的天空。详情可邮件至qianshg@csdn.net。