高并发Web服务的演变(3)

四、节约Web服务器的CPU

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

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

1. Select/Poll(Apache早期版本的I/O多路复用)

通常,Web服务都要维护很多个和用户通信的socket文件描述符,I/O多路复用,其实就是为了方便对这些文件描述符的管理和检测。Apache早期版本,是使用select的模式,简单的说,就是将这些我们关注的socket文件描述符交给内核,让内核告诉我们,那些描述符可操作。Poll与select原理基本相同,因此放在一起,它们之间的区别,就不赘叙了哈。

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

高并发Web服务的演变

 

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

2. Epoll(新版的Apache的event MPM,Nginx等支持)

Epoll是Linux2.6开始正式支持的I/O多路复用,我们可以理解为它是对select/poll的改进。首先,我们同样将我们关注的socket文件描述符集合告诉给内核,同时,给它们注册”回调函数“,如果某个socket文件准备好了,就通过回调函数通知我们。于是,我们就不需要专门去轮询整个全量的socket文件描述符集合,直接可以得到已经可操作的socket文件描述符。那么,那些大部分”空闲“的描述符,我们就不遍历了。即使我们监控的socket文件描述越来越多,我们轮询的也只是”活跃可操作“的socket文件描述符。

高并发Web服务的演变

 

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

3. 线程/进程的创建销毁和上下文切换

通常,Apache某一个时间内,是一个进程/线程服务于一个连接。于是,Apache就有很多的进程/线程,服务于很多的连接。Web服务在高峰期,会建立很多的进程/线程,也就带来很多的上下文切换开销。而Nginx,它通常只有1个master主进程和几个worker子进程,然后,1个worker进程服务很多个连接,进而节省了CPU的上下文切换开销。

 

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

4. 多线程下的锁对CPU的开销

Apache中的worker和event模式,都有采用多线程。多线程因为共享父进程的内存空间,在访问共享数据的时候,就会产生竞争,也就是线程安全问题。因此通常会引入锁(Linux下比较常用的线程相关的锁有互斥量metux,读写锁rwlock等),成功获取锁的线程可以继续执行,获取失败的通常选择阻塞等待。引入锁的机制,程序的复杂度往往增加不少,同时还有线程“死锁”或者“饿死”的风险(多进程在访问进程间共享资源的时候,也有同样的问题)。

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

高并发Web服务的演变

 

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

高并发Web服务的演变

 

为了避免这些锁导致的问题,就不得不加大程序的复杂度,解决方案一般有:

(1)对资源的加锁,根据约定好的顺序,大家都先对共享资源X加锁,加锁成功之后才能加锁共享资源Y。

(2)如果线程占有资源X,却加锁资源Y失败,则放弃加锁,同时也释放掉之前占有的资源X。

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

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

高并发Web服务的演变

 

五、 小结

可能有些同学看完之后,会得出结论,Nginx+PHP-FPM的工作方式,似乎是最节省系统资源的Web系统工作方式。某种程度上说,的确是可以这么说的,但是Web系统的搭建,需要从实际业务应用的角度出发,具体问题需要具体分析,寻求最合适的技术方案。

Web服务的不断演变和发展,努力地追求用尽可能少的系统资源,来支撑更多的用户请求,这是一条波澜壮阔的前进之路。这些技术方案,汇聚了很多值得学习和借鉴的解决问题的思路。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/1fc2f173b4c139a556d0290a0935d18e.html