wuhuaji | blog

Web 服务器演化之路

2019/11/16 Share

本文试图简洁扼要地总结 Web 服务器发展之路,如有错误请指正。

单进程

我们从最基本的单进程考虑。一个 Web 服务器的最基本功能:接受请求并处理响应。

那么自然可以得出处理逻辑:服务器建立 socket 监听,随后一直循环检测是否 有客户端请求,如果有客户端请求,那么则处理请求。

伪代码如下:

//服务器端处理程序
socket.listen(8080)
while(client = socket.accept()){
    client.read(client); //从客户端读出请求信息
    // do something 
    client.write(response); //发送响应到客户端
    client.close()
}

这样的简单逻辑,有很多缺点,首先我们关注第一个缺点:一次只能处理一个请求。如果 同时有两个请求,后一个必须等前一个处理完毕后,才会被处理。

这样的效率显然是不能接受的。

多进程 / 多线程

既然单进程一次只能处理一个请求,那么我们考虑使用多进程来处理。父进程专注于accept 请求,然后创建子进程处理。

可得到以下伪代码:

//服务器端处理程序
socket.listen(8080)
while(client = socket.accept()){
    pid = fork(); // 创建子进程
    if(pid == 0){ // 如果是父进程,就此返回以便能继续accept
        continue ; 
    }else{ //如果是子进程,则处理请求
        client.read(client); //从客户端读出请求信息
        // do something 
        client.write(response); //发送响应到客户端
        client.close();
        exit()
    }
}

到这里就能够“并发”处理多个请求了,请求一来就 fork 一个子进程来处理。那么如果同时有一万个请求呢?按照这个思路,就需要创建一个一万个子进程来处理。这显然是不可接受的,因为:

  • 创建和销毁进程是一笔不小的开销,需要消耗的资源很大。
  • 进程间切换也是不小的开销,就算创建了一万个进程,CPU也是按照时间分片来处理进程。每次切换需要保存当前进程上下文,恢复上一个进程上下文 等,也是非常昂贵的开销。

对于这些问题,可以有一些改善方法:

  • 创建和销毁进程开销很大,可以换为「线程」。线程可视为轻量级进程,创建和销毁所需资源要小得多。
  • 其次可使用 进程池 / 线程池。提前创建比如 100 个进程/线程,每次有请求来了首先看看线程池中有无空闲的线程,如有就分配给其处理,如无就等待有线程空闲。这样就避免反复创建和销毁进程/线程的开销。

Apache 似乎就是 多进程模型,父进程提前创建成子进程,请求来了交给子进程处理,但我没怎么用过Apache,具体细节不太了解。

多进程 + io 多路复用

前面多进程模型可以知道,一个进程同时只能处理一个请求。而处理一个请求,多数时间是花在「等待」上,专业的属于则是「阻塞」,而不是在计算上。伪代码举一个例子:

client = server.accept();
request = client.read(client);
//从客户端读出请求信息, 如果客户端的请求信息还未到达 网络读缓冲区,这里就会阻塞,无法进行下面的步骤

response = forward_to_cgi_or_upstream(request) 
// 读取到完整请求后,大多数情况下需要转发到cgi处理程序(如php的php-fpm), 或者上游服务器(在代理或者负载均衡场景中)
// 这里也会阻塞,需要等待上游处理完后,才能生成响应

send(response)
//经过上面各种阻塞后,终于生成了响应,可以发给客户端了。

可以看到,处理请求中,我们大多数时候是在等待 io ,io 未完成,就会阻塞。那么考虑是否有一种机制让我们无需阻塞在等待 io 就绪,而是等 io 就绪时(也就是此时调用我们不会被阻塞)提醒我们操作呢?

IO 多路复用 API 就是为此存在的。所谓 多路复用 就是 我们可以委托 内核帮我们检测多个 文件描述符,当它们有事件发生时(可读/可写/异常)时通知我们,这样就可以直接调用而无需阻塞,大大提高处理请求的效率。

IO 多路复用API 有:select,poll,epoll,kqueue,iocp 。其中 select、poll 为SUSv3 规范中规定的标准接口,能跨平台(貌似Windows上也有select,但没有poll,不太确定没写过windows程序),epoll 则为 Linux 独有,kqueue为Unix独有,iocp 为Windows。其中 select、poll 都有其局限性,性能会随着检测的文件描述符增多而递减。所以一般会选择 epoll / kqueue / iocp 。

Nginx 则正是使用了 多进程 + IO 多路复用的技术,通过一个 Master 进程管理 指定数量的 Worker 进程,Worker 进程使用 epoll 等技术(具体使用哪种可以配置)可以同时接管海量请求,使得 Nginx 的性能能够傲视群雄。

关于IO多路复用一组API,这里只做初步介绍,如果想深入学习可另找资料。也可参考我的 select_poll_epoll 示例代码

总结

本文介绍了从Web 服务器单进程 到 多进程/多线程(包括进程池、线程池) 到 IO 多路复用 进化的历史,也是我的一个总结,如有错误还请指出:)

CATALOG
  1. 1. 单进程
  2. 2. 多进程 / 多线程
  3. 3. 多进程 + io 多路复用
  4. 4. 总结