基础 Web 服务简介
Web 服务是互联网的核心组成部分之一,它允许用户通过浏览器访问信息和应用程序。一个基础的 Web 服务通常由 Web 服务器软件、静态网页内容、以及可选的动态内容生成程序组成。
Web 服务器软件
Web 服务器软件是运行在服务器上的程序,它负责接收来自客户端(如 Web 浏览器)的 HTTP 请求,并返回相应的响应。常见的 Web 服务器软件有 Apache HTTP Server、Nginx 和 Microsoft IIS 等。
Apache HTTP Server
Apache HTTP Server 是一款广泛使用的开源 Web 服务器软件,支持多种操作系统,包括 Linux、Unix 和 Windows。它提供了丰富的功能,如 URL 重写、身份验证、SSL/TLS 加密等。
Nginx
Nginx 是一款高性能的 Web 和反向代理服务器,以其高效的 I/O 处理能力而闻名。它通常用于负载均衡和反向代理,但也经常作为独立的 Web 服务器使用。
Microsoft IIS
Microsoft Internet Information Services (IIS) 是 Microsoft 提供的一款 Web 服务器软件,专为 Windows 操作系统设计。它支持多种应用程序开发框架,如 ASP.NET 和 PHP。
在这其中,Microsoft Internet Information Services (IIS)较为少见,且应用相对于apache和nginx而言占有率较低。
Apache 模型
Apache 服务器可以通过不同的 MultiProcess Module (MPM) 来实现不同的并发处理策略。MPM 是 Apache 中负责处理并发请求的核心模块,它决定了服务器如何管理进程和线程以响应客户端的请求。Apache 提供了几种不同的 MPM,包括但不限于:
Apache prefork 模型
- 预派生模式,有一个主控制进程,然后生成多个子进程,使用select模型,最大并发1024
- 每个子进程有一个独立的线程响应用户请求
- 相对比较占用内存,但是比较稳定,可以设置最大和最小进程数
- 是最古老的一种模式,也是最稳定的模式,适用于访问量不是很大的场景
优点:稳定
缺点:每个用户请求需要对应开启一个进程,占用资源较多,并发性差,不适用于高并发场景
Apache worker 模型
- 一种多进程和多线程混合的模型
- 有一个控制进程,启动多个子进程
- 每个子进程里面包含固定的线程
- 使用线程程来处理请求
- 当线程不够使用的时候会再启动一个新的子进程,然后在进程里面再启动线程处理请求
- 由于其使用了线程处理请求,因此可以承受更高的并发
优点:相比prefork 占用的内存较少,可以同时处理更多的请求
缺点:使用keepalive的长连接方式,某个线程会一直被占据,即使没有传输数据,也需要一直等待到超
时才会被释放。如果过多的线程,被这样占据,也会导致在高并发场景下的无服务线程可用(该问题在
prefork模式下,同样会发生)
Apache event模型
Apache中最新的模式,2012年发布的apache 2.4.X系列正式支持event 模型,属于事件驱动模型(epoll)
每个进程响应多个请求,在现在版本里的已经是稳定可用的模式
它和worker模式很像,最大的区别在于,它解决了keepalive场景下长期被占用的线程的资源浪费问题
(某些线程因为被keepalive,空挂在哪里等待,中间几乎没有请求过来,甚至等到超时)
event MPM中,会有一个专门的线程来管理这些keepalive类型的线程
当有真实请求过来的时候,将请求传递给服务线程,执行完毕后,又允许它释放。这样增强了高并发场
景下的请求处理能力
优点:单线程响应多请求,占据更少的内存,高并发下表现更优秀,会有一个专门的线程来管理keepalive类型的线程,当有真实请求过来的时候,将请求传递给服务线程,执行完毕后,又允许它释放
缺点:没有线程安全控制
当然可以!下面是一段关于 Nginx 的工作模型及其内部架构的介绍。
Nginx 的工作模型
Nginx 的工作模型主要基于事件驱动模型,这使得它能够在单个进程中处理大量的并发连接,而不需要为每个连接创建单独的进程或线程。以下是 Nginx 工作模型的关键特点:
事件驱动模型
Nginx 使用事件驱动模型来处理连接。这意味着它可以在单个进程中处理多个连接,而不必为每个连接创建额外的进程或线程。这种模型的核心是事件循环机制,它不断地监听事件的发生,并在事件发生时采取相应的行动。
非阻塞 I/O
Nginx 采用了非阻塞 I/O 模型,这意味着它可以在等待 I/O 操作完成的同时继续处理其他连接。这种非阻塞模型可以显著提高服务器处理并发连接的能力。
事件处理器
Nginx 包含了一个或多个事件处理器,这些处理器负责处理连接和 I/O 操作。事件处理器使用高效的事件模型来处理网络事件,如连接建立、数据接收和发送等。
工作进程与主进程
Nginx 在启动时会创建一个主进程和多个工作进程。主进程负责接受新的连接,并将它们传递给工作进程处理。工作进程负责处理连接请求、发送响应等。
Nginx 的内部架构
Nginx 的内部架构设计得非常简洁高效,主要包括以下几个关键组件:
主进程
- 配置加载:主进程负责加载配置文件。
- 管理子进程:主进程管理着多个工作子进程,并负责监控这些子进程的健康状况。
工作进程
- 事件处理器:每个工作进程都有自己的事件处理器,用于处理连接和 I/O 操作。
- 请求处理:工作进程负责处理客户端请求,包括解析请求、处理请求逻辑、生成响应等。
事件处理模块
- 事件驱动模块:Nginx 的事件驱动模块负责管理事件循环,监听和处理网络事件。
- 事件模型:Nginx 支持多种事件模型,如 epoll、kqueue 等,这些模型的选择取决于运行 Nginx 的操作系统。
缓冲区管理
- 输入缓冲区:用于暂存客户端发送的数据。
- 输出缓冲区:用于暂存待发送到客户端的数据。
HTTP 处理模块
- 解析请求:解析客户端发送的 HTTP 请求。
- 路由:根据请求 URL 路由到相应的处理逻辑。
- 响应生成:生成 HTTP 响应并发送给客户端。
反向代理模块
- 负载均衡:将客户端请求分发到后端服务器。
- 健康检查:监控后端服务器的健康状况。
日志记录
- 访问日志:记录客户端的访问信息。
- 错误日志:记录运行期间遇到的错误信息。
配置与模块化
- 模块化架构:Nginx 采用模块化架构,可以根据需要加载不同的模块来扩展功能。
- 动态加载模块:可以在运行时动态加载或卸载模块。
Nginx 的优点
- 高性能:能够处理大量的并发连接,具有较低的内存占用。
- 稳定性:长时间运行而不会出现内存泄漏或崩溃等问题。
- 灵活性:支持多种模块,可以根据需要定制功能。
- 易配置:配置文件简洁明了,易于理解和修改。
用户访问体验与I/O
在互联网中存在用户体验速率的1-3-10原则,即1秒最优,1-3秒较优,3~10秒比较慢,10秒以上用户无法接
受。用户放弃一个产品的代价很低,只是换一个URL而已。
影响用户体验的因素包含
客户端
- 客户端硬件配置
- 客户端网络速率
- 客户端与服务端距离
服务器
- 服务端网络速率
- 服务端硬件配置
- 服务端架构设计
- 服务端应用程序工作模式
- 服务端并发数量服务端响应文件大小及数量 buffer cache
- 服务端I/O压力
从服务器维护角度上看,客户端的影响很难由服务端解决,但是服务端的io流程却影响很大。
服务器 I/O 流程简述
I/O在计算机中指Input/Output, IOPS (Input/Output Per Second)即每秒的输入输出量(或读写次数),
是衡量磁盘性能的主要指标之一。IOPS是指单位时间内系统能处理的I/O请求数量,一般以每秒处理的
I/O请求数量为单位,I/O请求通常为读或写数据操作请求。
一次完整的I/O是用户空间的进程数据与内核空间的内核数据的报文的完整交换,但是由于内核空间与用
户空间是严格隔离的,所以其数据交换过程中不能由用户空间的进程直接调用内核空间的内存数据,而
是需要经历一次从内核空间中的内存数据copy到用户空间的进程内存当中,所以简单说I/O就是把数据从
内核空间中的内存数据复制到用户空间中进程的内存当中。
服务器的I/O
磁盘I/O
网络I/O : 一切皆文件,本质为对socket文件的读写
1. 用户请求
一切 I/O 操作都始于用户或应用程序发起的一个请求。例如,当用户通过 Web 浏览器访问一个网站时,浏览器会向服务器发送一个 HTTP 请求。
2. 应用程序接收请求
服务器上的应用程序(如 Web 服务器软件)接收到请求,并开始处理。处理过程中可能需要从磁盘或网络读取数据,或者向磁盘或网络写入数据。
3. 发起 I/O 操作
应用程序为了处理请求,会发起 I/O 操作。这些操作可以分为两大类:读操作和写操作。
- 读操作:从磁盘或网络读取数据。
- 写操作:将数据写入磁盘或网络。
磁盘 I/O
磁盘I/O是进程向内核发起系统调用,请求磁盘上的某个资源比如是html 文件或者图片,然后内核通过相
应的驱动程序将目标文件加载到内核的内存空间,加载完成之后把数据从内核内存再复制给进程内存,
如果是比较大的数据也需要等待时间
网络IO
网络通信就是网络协议栈到用户空间进程的IO就是网络IO
4. 操作系统调度
当应用程序发起 I/O 操作时,操作系统会介入并调度 I/O 操作。操作系统通过内核来管理这些操作,包括缓冲区管理、设备驱动程序调用等。
4.1 缓冲区管理
操作系统通常会在内存中为 I/O 操作设立缓冲区,这样可以减少对物理设备的直接访问次数,提高效率。当应用程序请求读取数据时,操作系统会先尝试从缓冲区中读取,如果缓冲区中没有所需数据,则会发起实际的物理读取操作。
4.2 设备驱动程序
操作系统通过设备驱动程序与物理设备进行通信。设备驱动程序是操作系统与硬件设备之间的桥梁,负责将 I/O 请求转换为硬件设备能够理解的指令。
5. 物理 I/O 操作
设备驱动程序将 I/O 请求发送给相应的硬件设备,如磁盘控制器或网络适配器。
- 磁盘 I/O:数据从磁盘读取到内存或从内存写入磁盘。
- 网络 I/O:数据通过网络接口卡(NIC)发送到网络或从网络接收。
6. 数据处理
一旦数据被读取或写入,操作系统会将其返回给应用程序。如果是读操作,数据会被加载到应用程序的内存空间中;如果是写操作,数据会被从应用程序的内存空间写入磁盘或网络。
7. 完成 I/O 操作
当 I/O 操作完成后,操作系统会通知应用程序,告知其 I/O 操作已完成。应用程序随后可以继续处理请求或返回结果给用户。
8. 用户响应
应用程序处理完请求后,会将结果返回给用户,完成一次完整的 I/O 流程。
I/O 模型
I/O 模型相关概念
同步/异步:关注的是消息通信机制,即调用者在等待一件事情的处理结果时,被调用者是否提供完成状
态的通知。
- 同步:synchronous,被调用者并不提供事件的处理结果相关的通知消息,需要调用者主动询问事 情是否处理完成
- 异步:asynchronous,被调用者通过状态、通知或回调机制主动通知调用者被调用者的运行状态
阻塞/非阻塞:关注调用者在等待结果返回之前所处的状态
- 阻塞:blocking,指IO操作需要彻底完成后才返回到用户空间,调用结果返回之前,调用者被挂 起,干不了别的事情。
- 非阻塞:nonblocking,指IO操作被调用后立即返回给用户一个状态值,而无需等到IO操作彻底完
成,在最终的调用结果返回之前,调用者不会被挂起,可以去做别的事情
网络 I/O 模型
阻塞型 I/O 模型(blocking IO)
阻塞IO模型是最简单的I/O模型,用户线程在内核进行IO操作时被阻塞
用户线程通过系统调用read发起I/O读操作,由用户空间转到内核空间。内核等到数据包到达后,然
后将接收的数据拷贝到用户空间,完成read操作
用户需要等待read将数据读取到buffer后,才继续处理接收的数据。整个I/O请求的过程中,用户线
程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够
优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源
缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销
较apache 的preforck使用的是这种模式。
同步阻塞:程序向内核发送I/O请求后一直等待内核响应,如果内核处理请求的IO操作不能立即返回,则进程将一直等待并不再接受新的请求,并由进程轮询查看I/O是否完成,完成后进程将I/O结果返回给Client,在IO没有返回期间进程不能接受其他客户的请求,而且是有进程自己去查看I/O是否完成,这种方式简单,但是比较慢,用的比较少。
非阻塞型 I/O 模型 (nonblocking IO)
用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据
到达后,才真正读取到数据,继续执行。即 “轮询”机制存在两个问题:如果有大量文件描述符都要等,
那么就得一个一个的read。这会带来大量的Context Switch(read是系统调用,每调用一次就得在用户
态和核心态切换一次)。轮询的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,
程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式,一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。
非阻塞:程序向内核发送请I/O求后一直等待内核响应,如果内核处理请求的IO操作不能立即返回IO结果,进程将不再等待,而且继续处理其他请求,但是仍然需要进程隔一段时间就要查看内核I/O是否完成。
查看上图可知,在设置连接为非阻塞时,当应用进程系统调用 recvfrom 没有数据返回时,内核会立即返
回一个 EWOULDBLOCK 错误,而不会一直阻塞到数据准备好。如上图在第四次调用时有一个数据报准
备好了,所以这时数据会被复制到 应用进程缓冲区 ,于是 recvfrom 成功返回数据当一个应用进程这样循环调用 recvfrom 时,称之为轮询 polling 。这么做往往会耗费大量CPU时间,实际使用很少
多路复用 I/O 型(I/O multiplexing)
上面的模型中,每一个文件描述符对应的IO是由一个线程监控和处理
多路复用IO指一个线程可以同时(实际是交替实现,即并发完成)监控和处理多个文件描述符对应各自
的IO,即复用同一个线程
一个线程之所以能实现同时处理多个IO,是因为这个线程调用了内核中的SELECT,POLL或EPOLL等系统调
用,从而实现多路复用IO
I/O multiplexing 主要包括:select,poll,epoll三种系统调用,select/poll/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select/poll/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
Apache prefork是此模式的select,worker是poll模式。
IO多路复用(IO Multiplexing) :是一种机制,程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”IO多路复用一般和NIO一起使用的。NIO和IO多路复用是相对独立的。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用,可以只用IO多路复用 + BIO,这时还是当前线程被卡住。IO多路复用和NIO是要配合一起使用才有
实际意义
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,就通知该进程多个连接共用一
个等待机制,本模型会阻塞进程,但是进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真
正的IO操作上用户首先将需要进行IO操作添加到select中,同时等待select系统调用返回。当数据到达时,IO被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视IO,以及调用select函数的额外操作,效率更差。并且阻塞了两次,但是第一次阻塞在select上时,select可以监控多个IO上是否已有IO操作准备就绪,即可达到在同一个线程内同时处理多个IO请求的目的。而不像阻塞IO那种,一次只能监控一个IO虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只是注册自己需要的IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因它使用了会阻塞线程的select系统调用。因此IO多路复用只能称
为异步阻塞IO模型,而非真正的异步IO
信号驱动式 I/O 模型 (signal-driven IO)
信号驱动I/O的意思就是进程现在不用傻等着,也不用去轮询。而是让内核在数据就绪时,发送信号通知
进程。
调用的步骤是,通过系统调用 sigaction ,并注册一个信号处理的回调函数,该调用会立即返回,然后主
程序可以继续向下执行,当有I/O操作准备就绪,即内核数据就绪时,内核会为该进程产生一个 SIGIO信
号,并回调注册的信号回调函数,这样就可以在信号回调函数中系统调用 recvfrom 获取数据,将用户进
程所需要的数据从内核空间拷贝到用户空间
此模型的优势在于等待数据报到达期间进程不被阻塞。用户主程序可以继续执行,只要等待来自信号处
理函数的通知。
在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续
运行并不阻塞
在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续
运行并不阻塞
当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。优
点:线程并没有在等待数据时被阻塞,内核直接返回调用接收信号,不影响进程继续处理其他请求因此
可以提高资源的利用率
缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知
异步阻塞:程序进程向内核发送IO调用后,不用等待内核响应,可以继续接受其他请求,内核收到进程
请求后
进行的IO如果不能立即返回,就由内核等待结果,直到IO完成后内核再通知进程。
异步 I/O 模型 (asynchronous IO)
异步I/O 与 信号驱动I/O最大区别在于,信号驱动是内核通知用户进程何时开始一个I/O操作,而异步I/O
是由内核通知用户进程I/O操作何时完成,两者有本质区别,相当于不用去饭店场吃饭,直接点个外卖,把
等待上菜的时间也给省了
相对于同步I/O,异步I/O不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备
好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直
接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
信号驱动IO当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到
用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后,内核直接通知用户线程可以进行后续
操作了
优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠
缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的
异步 I/O,在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编
程时以 IO 复用模型模式+多线程任务的架构基本可以满足需求
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、
libuv。
异步非阻塞:程序进程向内核发送IO调用后,不用等待内核响应,可以继续接受其他请求,内核调用的
IO如果不能立即返回,内核会继续处理其他事物,直到IO完成后将结果通知给内核,内核在将IO完成的
结果返回给进程,期间进程可以接受新的请求,内核也可以处理新的事物,因此相互不影响,可以实现
较大的同时并实现较高的IO复用,因此异步非阻塞使用最多的一种通信方式。
五种IO对比
这五种 I/O 模型中,越往后,阻塞越少,理论上效率也是最优前四种属于同步 I/O,因为其中真正的 I/O
操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配