httpd是如何实现高并发服务的

进行网络通信的时候,需要建立一个 socket ,这是大家都知道的。如果一个套接字只能被一个监听进(线)程监听,那么岂不是同一时刻 httpd 只能处理一个请求,处理完这个请求之后,释放 80 端口在给其他请求使用。可是显然 httpd 没有那么笨,那么 apache httpd 是如何支持高并发的呢?

简单的说就是有 两种套接字 ,一种是 监听套接字 ,供监听进(线)程监听使用。另一种是 链接套接字 ,供传输数据使用。监听进(线)程好比丽春院里面接客的老鸨,而为客人提供弹唱服务的姑娘相当于 worker 模式下的工作线程。老鸨可以有一个,但是姑娘有很多。在 apache httpdworker 模式下,每个进程会产生多个工作线程及一个监听线程。监听线程负责监听,其他工作线程负责传输数据。问题回答完了,就这么简单。

那么 监听套接字连接套接字 是如何配合工作的呢?

要回答这个问题,需要有两个背景知识做铺垫, 网络编程TCP三次握手

背景知识1-网络编程

有过网络编程经验的同学都知道,把’大象放冰箱’一般是如下几个步骤。

socket()
bind()
listen()
accept()
recv()/send()
close()

大家记住这几个步骤,后面用的到。现在来说TCP的三次握手过程。

背景知识2-TCP三次握手

我们经常见到的TCP三次握手过程都是这样的。

如下图:

上面这张图,只是表面现象。下面我把这张图没有体现出来的细节给各位说一说,这对回答上面的问题至关重要。

TCP 的协议栈中维护着两个队列。一个是半连接队列,一个是全链接队列。

客户端发送 SYN 报文到服务端指定端口比如 httpd 的默认 80 端口;此时客户端的状态变为 SYN-SEND ;服务端收到 SYN 报文之后,将这个连接状态,存储到半连接队列中,同时服务端的状态变更为 SYN-RECV ;然后服务端立刻回一个 ACK 报文给客户端;客户端收到 ACK 之后,回给服务端一个 ACK ,服务端收到这个 ACK 之后,客户端跟服务端都变为 ESTABLISHE 状态;表明TCP三次握手成功。这个时候会把半连接队列中的连接,转移到全链接队列。全链接队列中的连接都是已经完成三次握手的连接。监听进(线)程监听 监听套接字 ,就是工作在上面这个过程的。

全链接队列当中的连接都是可以用来直接进行数据传输的连接了。这个时候可以使用 accept() 函数,将全链接当中的连接取出来,生成连接 socket ,进行数据传输。传输完毕之后,经过四次挥手,关掉连接,释放资源。这就是一次完整的TCP传输过程。

俗话说的好,一图胜千言。我将上面的过程简单的画了一个流程图出来。

图中标绿的部分分别是三次握手过程跟四次挥手过程。这两个阶段发生在内核空间。

Linux 2.2 以前, listen() 函数有一个 backlog 的参数,用于设置这两个队列的最大总长度,从 Linux 2.2 开始,这个参数只表示全链接队列的最大长度,而 /proc/sys/net/ipv4/tcp_max_syn_backlog 则用于设置半连接队列的最大长度。 /proc/sys/net/core/somaxconn 则是硬限制已完成队列的最大长度,默认为 128 ,如果 backlog 大于 somaxconn ,则 backlog 会被强制等于 somaxconn

回到apache httpd

prefork模式

apache httpdprefork 模式会创建子进程处理 http 请求。每个子进程,即负责监听又负责处理数据。也就是说从建立三次握手到数据传输,再到四次挥手,一个子进程负责到底。如果传输的数据量比较大,同时系统高并发有比较高,那就需要建立很多子进程,对服务器的硬件资源是个不小的考验。

worker模式

worker 模式是进线程混合模式,主进程会创建子进程,每个子进程会创建多个工作子线程及一个监听进程。监听进程专门负责处理监听套接字,数据传输过程交给其他工作线程。所以 worker 模式要比 prefork 模式在性能上提高不少。

event模式

event 模式跟 worker 模式基本相同,都是进线程混合模式,不同的是,该模式还会生成一个专门处理 keep-alive 的线程。 keep-alive 虽好,但是某些情况下存在站着茅坑不拉屎的情况,这样可以把保持 keep-alive 的空闲连接资源进行回收。 keep-alive 的问题可以看 我这篇文章

结束

既然 eventworker 都是进线程混合模式,那么为什么 event 模块的性能要比 worker 模块的性能好很多呢?我将在下篇博文中详细说明。