高并发Web服务的演变—节约系统内存和CPU

#0 系列目录#php

#1 愈来愈多的并发链接数# 如今的Web系统面对的并发链接数在近几年呈现指数增加,高并发成为了一种常态,给Web系统带来不小的挑战。以最简单粗暴的方式解决,就是增长Web系统的机器和升级硬件配置。虽然如今的硬件愈来愈便宜,可是一味地经过增长机器来解决并发量的增加,成本是很是高昂的。结合技术优化方案,才是更有效的解决方法前端

并发链接数为何呈指数增加?实际上,从这几年的用户基数上看,这个数量并无出现指数增加,所以它并不是主要缘由。主要缘由,仍是web变得更复杂,交互更丰富所致使的web

##1.1 页面元素增多,交互复杂## Web页面元素愈来愈多,更为丰富。更多的资源元素,意味着更多的下载请求。Web系统的交互愈来愈复杂,交互场景和次数也大幅增长。以“www.qq.com”的首页为例子,刷新一次,大概会有244个请求。而且,在页面打开完成以后,还会有一些定时的查询或者上报请求持续运做。ajax

输入图片说明

目前的Http请求,为了减小反复的建立和销毁链接行为,一般都创建长链接(Connection keep-alive)。一经创建,这个链接会被保持住一段时间,被后续请求复用。然而,它也带来了另外一个新的问题,链接的保持是会占用Web系统服务端资源的,若是不充分使用这个链接,会致使资源浪费长链接被建立后,首批资源传输完毕,以后几乎没有数据交互,一直到超时时间,才会自动释放长链接占据的系统资源后端

输入图片说明

除此以外,还有一些Web需求自己就须要长期保持链接的,例如Web socket浏览器

##1.2 主流的浏览器的链接数在增长## 面对愈来愈丰富的Web资源,主流浏览器并发链接数也在增长,同一个域下,早期的浏览器通常只有1-2个下载链接,而目前的主流浏览器一般在2-6个。增长浏览器并发链接数目,在须要下载资源比较多的场景下,能够加快页面的加载速度。更多的链接对浏览器加载页面元素是有好处的,在某些链接遭遇“网络阻塞”的状况下,其余正常的下载链接能够继续工做。缓存

这样天然无形增长了Web系统后端的压力,更多的下载链接意味着占据了更多的Web服务器的资源。而在用户访问高峰期,自热而然就造成了“高并发”场景。这些链接和请求,占据了服务器的大量CPU和内存等资源。尤为在资源数目超过100+的网站页面中,使用更多的下载链接,很是有必要。安全

#2 Web前端优化,下降服务端压力# 在缓解“高并发”的压力,须要前端和后端的共同配合优化,才能达到最大效果。在用户第一线的Web前端,能够起到减小或者减轻Http请求的效果服务器

##2.1 减小Web请求## 经常使用的实现方法是经过Http协议头中的expire或max-age来控制,将静态内容放入浏览器的本地缓存,在以后的一段时间里,再也不请求Web服务器,直接使用本地资源。还有HTML5中的本地存储技术(LocalStorage),也被做为一个强大的数据本地缓存。网络

输入图片说明

这种方案缓存后,根本不发送请求到Web服务器,大幅下降服务器压力,也带来了良好的用户体验。可是,这种方案,对首次访问的用户无效,同时,也影响部分Web资源的实时性

##2.2 减轻Web请求## 浏览器的本地缓存是存在过时时间的,一旦过时,就必须从新向服务器请求。这个时候,会有两种情形:

  1. 服务器的资源内容没有更新,浏览器请求Web资源,服务器回复“能够继续使用本地缓存”。(发生通讯,可是Web服务器只须要作简单“回复”)

  2. 服务器的文件或者内容已经更新,浏览器请求Web资源,Web服务器经过网络传输新的资源内容。(发生通讯,Web服务器须要完成复杂的传输工做)

这里的协商方式是经过Http协议的Last-Modified或Etag来控制,这个时候请求服务器,若是是内容没有发生变动的状况,服务器会返回304 Not Modified。这样的话,就不须要每次请求Web服务器都作复杂的传输完整数据文件的工做,只要简单的http应答就能够达到相同的效果。

输入图片说明

虽然上述请求,起到“减轻”Web服务器的压力,可是链接仍然被创建,请求也发生了。

##2.3 合并页面请求## 若是是比较老一些的Web开发者,应该会更有印象,在ajax盛行以前。页面大部分都是直接输出的,并无这么多的ajax请求,Web后端将页面内容彻底拼凑好了,再返回给前端。那个时候,页面静态化,是一个挺普遍的优化方式。后来,被交互更友好的ajax渐渐替代了,一个页面的请求也变得愈来愈多。

因为移动端的网络(2G/3G)比起PC宽带差不少,而且部分手机配置比较低,面对一个超过100个请求的网页,加载的速度会缓慢不少。因而,优化的方向又从新回到合并页面元素,减小请求数量

  1. 合并HTML展现内容。将CSS和JS直接嵌入到HTML页面内,不经过链接的方式引入。

  2. Ajax动态内容合并请求。对于动态内容,将10次Ajax请求合并为1次的批量信息查询。

  3. 小图片合并,经过CSS的偏移量技术Sprites,将不少小图片合并为一张。这个优化方式,在PC端的Web优化中,也很是常见。

输入图片说明

合并请求,减小了传输数据的次数,也就是至关于将它们从一个一个地请求,变为一次的“批量”请求。上述优化方法,到达“减轻”Web服务器压力的目的,减小了须要创建的链接。

#3 节约Web服务端的内存# 前端的优化完成,咱们就须要着眼于Web服务端自己。内存是Web服务器很是重要的资源,更多的内存一般意味着能够同时放入更多的工做任务。就Web服务占用内存而言,能够粗略划分:

  1. 用来维持链接的基本内存,进程初始化时,会载入一些基础模块到内存。

  2. 被传输的数据内容载入到各个缓冲区,占据的内存。

  3. 程序执行过程当中,申请和使用的内存。

若是维持一个链接,可以尽量少占用内存,那么咱们就能够维持更多的并发链接,从而让Web服务器支持更多的并发链接数

Apache(httpd)是一个成熟而且古老的Web服务,而Apache的发展和演变,一直在追求作到这一点,它试图不断减小服务占据的内存,以支持更大的并发量。以Apache的工做模式的演变为视角,咱们一块儿来看看,它们是如何优化内存的问题的。

##3.1 prefork MPM,多进程工做模式## prefork是Apache最成熟和稳定的工做模式,即便是如今,仍然被普遍使用。主进程生成后,它先完成基础的初始化工做,而后,经过fork预先产生一批的子进程(子进程会复制父进程的内存空间,不须要再作基础的初始化工做)。而后等待服务,之因此预先生成,是为了减小频繁建立和销毁进程的开销。多进程的好处,是进程之间的内存数据不会相互干扰,同时,某个进程异常终止也不会影响其余进程。可是,就内存而言,每一个httpd子进程占用了不少的内存,由于子进程的内存数据是复制父进程的。咱们能够粗略认为,这里存在大量的“重复数据”被放在内存中。最终,致使咱们可以生成的子进程最大数量是颇有限。在面对高并发时,由于有很多Keep-alive的长链接,将这些子进程“霸占”住,极可能致使可用子进程耗尽。所以,prefork并不太适合高并发场景。

输入图片说明

优势:成熟稳定,兼容全部新老模块。同时,不须要担忧线程安全的问题。(例如,咱们经常使用的mod_php,将PHP编译为Apache的子模块,就不须要支持线程安全)。

缺点:一个服务进程占用不少内存。

##3.2 worker MPM,多进程和多线程的混合模式## worker模式比起prefork,是使用了多进程和多线程的混合模式。它也预先fork了几个子进程(数量不多),而后每一个子进程建立一些线程(其中包括一个监听线程)。每一个请求过来,会被分配到1个线程来服务。线程比起进程会更轻量,由于线程一般会共享父进程的内存空间,所以,内存的占用会减小一些。在高并发的场景下,由于比起prefork更省内存,所以会有更多的可用线程

输入图片说明

可是,它并无解决Keep-alive的长链接“霸占”线程的问题,只是对象变成了比较轻量的线程。

有些人会以为奇怪,那么这里为何不彻底使用多线程呢,还要引入多进程?由于还须要考虑稳定性,若是一个线程挂了,会致使同一个进程下其余正常的子线程都挂了若是所有采用多线程,某个线程挂掉,就致使整个Apache服务“全军覆没”。而目前的工做模式,受影响的只是Apache的一部分服务,而不是整个服务。

线程共享父进程的内存空间,减小了内存的占用,却又引发了新的问题。就是“线程安全”,多个线程修改共享资源致使的“竞争行为”,又强迫咱们所使用的模块必须支持“线程安全”。所以,它有必定程度上增长Web服务的不稳定性。例如,mod_php所使用的PHP拓展,也一样须要支持“线程安全”,不然,不能在该模式下使用。

优势:占据更少的内存,高并发下表现更优秀。

缺点:必须考虑线程安全的问题,同时锁的引入又增长了CPU的开销。

##3.3 event MPM,多进程和多线程的混合模式,引入Epoll## 这个是Apache中比较新的模式,在如今的版本(Apache 2.4.10)已是稳定可用的模式。它和worker模式很像,最大的区别在于,它解决了keep-alive场景下,长期被占用的线程的资源浪费问题event MPM中,会有一个专门的线程来管理这些keep-alive类型的线程,当有真实请求过来的时候,将请求传递给服务线程,执行完毕后,又容许它释放。它减小了“占据”链接而又不使用的资源浪费,加强了高并发场景下的请求处理能力。由于减小了“闲等”的线程,线程的数量减小,同等场景下,内存占用会降低一些。

输入图片说明

event MPM在遇到某些不兼容的模块时,会失效,将会回退到worker模式,一个工做线程处理一个请求。新版Apache官方自带的模块,所有是支持event MPM的。注意一点,event MPM须要Linux系统(Linux 2.6+)对EPoll的支持,才能启用。Apache的三种模式中在真实应用场景中,event MPM是最节约内存的

##3.4 使用比较轻量的Nginx做为Web服务器## 虽然Apache的不断优化,减小了内存占用,从而增长了处理高并发的能力。可是,正如前面所说,Apache是一个古老而成熟的Web服务,同时,集成不少稳定的模块,是一个比较重的Web服务。Nginx是个比较轻量的Web服务,占据的内存自然就少于Apache。并且,Nginx经过一个进程来服务于N个链接。所使用的方式,并非Apache的增长进程/线程来支持更多的链接。对于Nginx来讲,它少建立了大量的进程/线程,减小了不少内存的开销

输入图片说明

静态文件的QPS性能压测结果,Nginx性能大概3倍于Apache对静态文件的处理。PHP等动态文件的QPS,Nginx的作法一般是经过FastCGI的方式和PHP-FPM通讯的方式完成,PHP做为一个与之无关的外部服务存在。而Apache一般将PHP编译为本身的子模块(新版的Apache也支持FastCGI)。PHP动态文件,Nginx的表现略逊于Apache。

##3.5 sendfile节约内存## Apache、Nginx等很多Web服务,都带有sendfile支持的。sendfile能够减小数据到“用户态内存空间”(用户缓冲区)的拷贝,进而减小内存的占用。固然,不少同窗第一个反应固然是问Why?为了尽量清楚讲述这个原理,咱们就先回Linux内核态和用户态的存储空间的交互。

通常状况下,用户态(也就是咱们的程序所在的内存空间)是不会直接读写或者操做各类设备(磁盘、网络、终端等),中间一般用内核做为“中间人”,来完成对设备的操做或者读写。

以最简单的磁盘读写例子,从磁盘中读取A文件,写入到B文件。A文件数据是从磁盘开始,而后载入到“内核缓冲区”,而后再拷贝到“用户缓冲区”,咱们才能够对数据进行处理。写入的时候,也同理,从“用户态缓冲区”载入到“内核缓冲区”,最后写入到磁盘B文件。

输入图片说明

这样写文件很累吧,因而有人以为这里能够跳过“用户缓冲区”的拷贝。其实,这就是MMP(Memory-Mapping,内存映射)的实现,创建一个磁盘空间和内存的直接映射,数据再也不复制到“用户态缓冲区”,而是返回一个指向内存空间的指针。因而,咱们以前的读写文件例子,就会变成,A文件数据从磁盘载入到“内核缓冲区”,而后从“内核缓冲区”复制到B文件的“内核缓冲区”,B文件再从”内核缓冲区“写回到磁盘中。这个过程,减小了一次内存拷贝,同时也少内存占用。

输入图片说明

好了,回到sendfile的话题上来,简单的说,sendfile的作法和MMP相似,就是减小数据从”内核态缓冲区“到”用户态缓冲区“的内存拷贝

默认的磁盘文件读取,到传输给socket,流程(不使用sendfile)是

输入图片说明

使用sendfile以后

输入图片说明

这种方式,不只节省了内存,并且还有CPU的开销。

#4 节约Web服务器的CPU# 对Web服务器而言,CPU是另外一个很是核心的系统资源。虽然通常状况下,咱们认为业务程序的执行消耗了咱们主要CPU。可是,就Web服务程序而言,多线程/多进程的上下文切换,也是比较消耗CPU资源的。一个进程/线程一般不能长期占有CPU,当发生阻塞或者时间片用完,就没法继续占用CPU,这个时候,就会发生上下文切换,CPU时间片从老进程/线程切换到新的。除此以外,在并发链接数目很高的场景下,对这些用户创建的链接(socket文件描述符)状态的轮询和检测,也是比较消耗CPU的

而Apache和Nginx的发展和演变,也在努力减小CPU开销。

##4.1 Select/Poll(Apache早期版本的I/O多路复用)## 一般,Web服务都要维护不少个和用户通讯的socket文件描述符,I/O多路复用,其实就是为了方便对这些文件描述符的管理和检测。Apache早期版本,是使用select的模式,简单的说,就是将这些咱们关注的socket文件描述符交给内核,让内核告诉咱们,那些描述符可操做。Poll与select原理基本相同,所以放在一块儿,它们之间的区别,就不赘叙了哈。

select/poll返回的是一个咱们以前提交的文件描述符集合(内核将其中可读、可写或者异常状态的socket文件描述符的标识位修改了),咱们须要经过轮询检查才能得到咱们能够操做的文件描述符。在这个过程当中,不断重复执行。在实际应用场景中,大部分被咱们监控的socket文件描述符,都是”空闲的“,也就是说,不能操做。咱们对整个集合轮询,就是为了找了少部分咱们能够操做的socket文件描述符。因而,当咱们监控的socket文件描述符越多(用户并发链接数愈来愈多),这个轮询工做,也就愈来愈沉重,进而致使增大了CPU的开销

输入图片说明

若是咱们监控的socket文件描述符,几乎都是”活跃的“,反而使用这种模式更合适一点。

##4.2 Epoll(新版的Apache的event MPM,Nginx等支持)## Epoll是Linux2.6开始正式支持的I/O多路复用,咱们能够理解为它是对select/poll的改进。首先,咱们一样将咱们关注的socket文件描述符集合告诉给内核,同时,给它们注册”回调函数“,若是某个socket文件准备好了,就经过回调函数通知咱们。因而,咱们就不须要专门去轮询整个全量的socket文件描述符集合,直接能够获得已经可操做的socket文件描述符。那么,那些大部分”空闲“的描述符,咱们就不遍历了。即便咱们监控的socket文件描述愈来愈多,咱们轮询的也只是”活跃可操做“的socket文件描述符

输入图片说明

其实,有一种极端点的场景,就是咱们所有文件描述符几乎都是”活跃“的,这样反而致使了大量回调函数的执行,又增长了CPU的开销。可是,就Web服务的真实场景,绝大部分时候,都是链接集合中都存在不少”空闲“链接

##4.3 线程/进程的建立销毁和上下文切换## 一般,Apache某一个时间内,是一个进程/线程服务于一个链接。因而,Apache就有不少的进程/线程,服务于不少的链接。Web服务在高峰期,会创建不少的进程/线程,也就带来不少的上下文切换开销。而Nginx,它一般只有1个master主进程和几个worker子进程,而后,1个worker进程服务不少个链接,进而节省了CPU的上下文切换开销

输入图片说明

两种模式虽然不一样,但实际上不能直接出分好坏,综合来讲,各有各自的优点,就不妄议了哈。

##4.4 多线程下的锁对CPU的开销## Apache中的worker和event模式,都有采用多线程。多线程由于共享父进程的内存空间,在访问共享数据的时候,就会产生竞争,也就是线程安全问题。所以一般会引入锁(Linux下比较经常使用的线程相关的锁有互斥量metux,读写锁rwlock等),成功获取锁的线程能够继续执行,获取失败的一般选择阻塞等待引入锁的机制,程序的复杂度每每增长很多,同时还有线程“死锁”或者“饿死”的风险(多进程在访问进程间共享资源的时候,也有一样的问题)

死锁现象(两个线程彼此锁住对方想要获取的资源,相互阻塞等待,永远没法达不到知足条件)

输入图片说明

饿死现象(某个线程,一直获取不到它想要锁资源,永远没法执行下一步)

输入图片说明

为了不这些锁致使的问题,就不得不加大程序的复杂度,解决方案通常有

  1. 对资源的加锁,根据约定好的顺序,你们都先对共享资源X加锁,加锁成功以后才能加锁共享资源Y。

  2. 若是线程占有资源X,却加锁资源Y失败,则放弃加锁,同时也释放掉以前占有的资源X。

在使用PHP的时候,在Apache的worker和event模式下,也必须兼容线程安全。一般,新版本的PHP官方库是没有线程安全方面的问题,须要关注的是第三方扩展。PHP实现线程安全,不是经过锁的方式实现的。而是为每一个线程独立申请一份全局变量的副本,至关于线程的私人内存空间,可是这样作相对消耗多一些内存。不过,这样的好处,是不须要引入复杂的锁机制实现,也避免了锁机制对CPU的开销。

这里顺便提到一下,常常和Nginx搭配工做的PHP-FPM(FastCGI)使用的是多进程,所以不会有线程安全的问题

输入图片说明

相关文章
相关标签/搜索