3. I/O多路复用¶

I/O多路复用I/O multiplexing

在互联网早期,为了实现一个服务器可以处理多个客户端的连接,程序猿是这样做的。服务器得知来了一个请求后,就去创建一个线程处理这个请求,假如有10个客户请求,就创建10个线程,这在当时联网设备还比较匮乏的时代,是没有任何问题的。

但随着科技的发展,人们越来越富裕,都买得起电脑了,网民也越来越多了,由于一台机器的能开启的线程数是有限制的,当请求非常集中量大到一定量时,服务器的压力就巨大无比。

终于到了 1983年,人们意识到这种问题,提出了一种最早的 I/O 多路复用的模型(select实现),这种模型,对比之前最大的不同就是,处理请求的线程不再是根据请求来定,后端请求的进程只有一个。虽然这种模型在现在看来还是不行,但在当时已经大大减小了服务器系统的开销,可以解决服务器压力太大的问题,毕竟当时的电脑都是很珍贵的。

再后来,家家都有了电脑,手机互联网的时代也要开始来了,联网设备爆炸式增长,之前的 select ,早已不能支撑用户请求了。

由于使用 select 最多只能接收 1024 个连接,后来程序猿们又改进了 select 发明了 pool,pool 使用的链表存储,没有最大连接数的限制。

select 和 pool ,除了解决了连接数的限制 ,其他似乎没有本质的区别。

都是服务器知道了有一个连接来了,由于并不知道是哪那几个流(可能有一个,多个,甚至全部),所以只能一个一个查过去(轮循),假如服务器上有几万个文件描述符(下称fd,file descriptor),而你要处理一个请求,却要遍历几万个fd,这样是不是很浪费时间和资源。

由此程序员不得不持续改进 I/O多路复用的策略,这才有了后来的 epoll 方法。

epoll 解决了前期 select 和 poll 出现的一系列的尴尬问题,比如:

  • select 和 poll 无差别轮循fd,浪费资源,epoll 使用通知回调机制,有流发生 IO事件时就会主动触发回调函数

  • select 和 poll 线程不安全,epoll 线程安全

  • select 请求连接数的限制,epoll 能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)

  • select 和 pool 需要频繁地将fd复制到内核空间,开销大,epoll通过内核和用户空间共享一块内存来减少这方面的开销。

虽然 I/O 多路复用经历了三种实现:select -> pool -> epoll,这也不是就说 epoll 出现了, select 就会被淘汰掉。

epoll 关注的是活跃的连接数,当连接数非常多但活跃连接少的情况下(比如长连接数较多),epoll 的性能最好。

而 select 关注的是连接总数,当连接数多而且大部分的连接都很活跃的情况下,选择 select 会更好,因为 epoll 的通知回调机制需要很多的函数回调。

另外还有一点是,select 是 POSIX 规定的,一般操作系统均有实现,而 epoll 是 Linux 所有的,其他平台上没有。

IO多路复用除了以上三种不同的具体实现的区别外,还可以根据线程数的多少来分类

  • 一个线程的IO多路复用,比如 Redis

  • 多个线程的IO多路复用,比如 goroutine

IO多路复用 + 单进(线)程有个好处,就是不会有并发编程的各种坑问题,比如在nginx里,redis里,编程实现都会很简单很多。编程中处理并发冲突和一致性,原子性问题真的是很难,极易出错。