总结
- Ngnix+PHP-FPM的工作方式,似乎是LNMP架构最节省系统资源的工作方式。
- 当然,具体的技术选型更多的应该参考自己想要实现的业务需求。
1. 原因:越来越多的并发连接数
- 页面的元素增多,交互复杂
- 主流浏览器的连接数在增加
2. 辅助方案:通过WEB前端优化,降低服务器的压力
- 减少web请求
- 减轻web请求
- 合并页面请求
3. 推荐方案:节约web服务器的内存
- prefork MPM(Apache服务器多道处理器模块),多进程工作模式:
- 主进程生成后,他先完成初始化的工作,通过fork的方式预先产生一批子进程(子进程复制父进程的内存空间,不需要再做初始化的工作)
- 多进程的好处:进程之间的内存数据是互不干扰的。
- 优点:成熟稳定,兼容新老版本,不用担心线程安全的问题
- 缺点:不适合高并发的业务场景,一个服务进程占很多内存
- worker MPM, 多线程和多进程的混合模式
- 和prework一样,也预先fork了几个子进程(数量少),然后每个子进程创建几个线程(包含一个监听线程)。每个请求过来,会交给一个线程来服务。(线程比进程轻量级,所以内存占用更少)。
- 注意:并没有解决“keep-alive”长连接的问题,只是把对象编程了更轻量级的线程
- 疑问:既然多线程轻量级,为什么不完全采用多线程的方式呢?因为多进程能确保程序的稳定性,如果采用单进程多线程的方式,有一个线程挂了,那么该进程下的其他线程也挂了,会导致全军覆没。
- 优点:更优秀的内存管理,高并发场景下表现更优秀。
- 缺点:需要考虑线程安全问题,需要引入锁,加大cpu的消耗。
- event MPM
- 和worker的方式很像,最大的差别在于解决了“keep-alive”场景下,长期被占用的线程资源问题。
- 注意:event MPM遇到不兼容的模块时会失效,将回退到worker模式,一个工作线程处理一个请求。
- Apache的3种模式中,event MPM是最节约内存的。(需要Linux系统对Epoll的支持才能启用)
- 使用轻量级的Ngnix作为web服务器
- Ngnix本身就是一个轻量级的web服务器,天生萝莉,比Apache要轻量。
- Ngnix通过一个进程来服务N个链接,采用的方式不同于Apache的方式。
- sendfile节约内存(这个概念非常重要)
- sendfile可以减少数据到“用户态内存空间”(用户缓存区)的拷贝,进而减少对内存的占用。
- 为了更好的理解上面所说的原理。笔者先引入下面的概念:内核态和用户态的区别,内核态的优先级高Ring0,而用户态(运行态)的优先级低Ring3;并且当执行用户程序是突然中断,运行状态会从“用户态”切换到“内核态”。
- 一般情况下,用户态(程序所在的内存空间)不能直接操作(读写等)各种设备(磁盘,网络,终端等)的。需要使用内核作为中间人来完成对设备的操作。
- 来吃个栗子(例子)吧:以最简单的磁盘读写为例,从磁盘A读取文件到磁盘B,其过程是这样的:A文件数据从磁盘开始,然后载入到“内核缓冲区”,然后拷贝到“用户缓存区”,这完成了读操作。写操作也是一样的,从“用户缓存区”拷贝到“内核缓冲区”,最后写入到磁盘B中。
- 这样读写文件很累吧。有大神提出了要删繁就简,取消“用户缓存区”那部分拷贝工作,引入了MMP(Memory-Mapping,内存映射)的概念。实现原理是这样的:建立一个磁盘空间和内存的直接映射,数据不再拷贝到“用户缓存区”,而是返回一个指向内存空间的指针。这样我们之前的文件拷贝就变成了如下步骤:A磁盘中文件将数据载入到“内核缓冲区”,B磁盘从“内核缓冲区”拷贝写入文件。减少了一次拷贝过程,减少了内存的占用。
- 回到正题:简单来说,sendfile的原理和mmp的方式类似,核心也是减少了“内核缓冲区”到“用户缓冲区”的拷贝。
- 优点: 不仅节省了内存,还节省了CPU的开销。
4. 节约web服务器的CPU
- 对于web服务器而言,CPU是另一个非常核心的系统资源。就web服务器而言,除了业务程序消耗CPU外,多线程/多进程的上下文切换,也是比较消耗CPU资源的。
- 一个进程/线程无法长时间占用CPU,当发生阻塞或者时间片用完时,就无法继续占用CPU,这时会发生时间上下文的切换,即老的时间片切换到新的时间片,也是耗CPU的。
- 在并发连接数目很高的情况下,去轮询检测用户建立的连接状态(socket文件描述符),也是消耗CPU的。
- 笔者在这里只介绍一下终极问题及解决办法:
5. 多线程下的锁对CPU的开销
- Apache中的worker和event模式,都有采用多线程。多线程因为共享父进程的内存空间,在访问共享数据的时候,就会产生竞争,也就是线程安全问题。因此通常会引入锁(Linux下比较常用的线程相关的锁有互斥量metux,读写锁rwlock等),成功获取锁的线程可以继续执行,获取失败的通常选择阻塞等待。引入锁的机制,程序的复杂度往往增加不少,同时还有线程“死锁”或者“饿死”的风险(多进程在访问进程间共享资源的时候,也有同样的问题)。
- 死锁现象(两个线程彼此锁住对方想要获取的资源,相互阻塞等待,永远无法达不到满足条件)
- 饿死现象(某个线程,一直获取不到它想要锁资源,永远无法执行下一步)
- 为了避免这些锁导致的问题,就不得不加大程序的复杂度,解决方案一般有:
- 对资源的加锁,根据约定好的顺序,大家都先对共享资源X加锁,加锁成功之后才能加锁共享资源Y。
- 如果线程占有资源X,却加锁资源Y失败,则放弃加锁,同时也释放掉之前占有的资源X。
- 在使用PHP的时候,在Apache的worker和event模式下,也必须兼容线程安全。通常,新版本的PHP官方库是没有线程安全方面的问题,需要关注的是第三方扩展。PHP实现线程安全,不是通过锁的方式实现的。而是为每个线程独立申请一份全局变量的副本,相当于线程的私人内存空间,但是这样做相对多消耗一些内存。这样的好处:不需要引入复杂的锁机制实现,也避免了锁机制对CPU的开销。