一、epoll模式
在前面分析了select和poll两种IO多路复用的模式,但总体给人的感觉有一种力不从心的感觉。尤其是刚刚接触底层网络开发的程序员,被很多双十一千万并发,游戏百万并发等等已经给唬的一楞一楞的。一听说只支持一两千个并发,便心里感觉这玩意儿是不是在应付人。
这就得多说两句,其实所谓的百万千万并发,并不是说在一台机器上搞的。现在的网络服务端一般都是在云上或者能自己弹性扩容。另外大家需要搞清楚的,很多高并发,看上去非常多,但不是真正的在一秒两秒内完成,它会利用一个缓冲的时间进行最终一致性处理。换句话说,实际的应用工程上,大多在设计上采用一些方法来规避在单机上实现太多的并发。不过反回头来说,不是说单机上不能实现这么高的并发。这需要成本、设计以及开发和网络等的共同努力,并不划算。
需求是技术进步的最重要的推手,正是有上面提到的实际业务需求,才会倒推技术不断的提高并发量。再强的设计,也脱离不开底层网络技术的支持。如果一台单机只支持十个并发,那么如何弹性扩容也就成为了一种灾难的存在。也就是工程创新和技术创新需要协同作战。
高并发的要求,就使的Linux提供了epoll这种新的网络通信模式。可以把epoll当成一个稳定版的select和epoll优化版本,它提供了更强大的并发和更多的数据通信支持。另外,根据实际情况,到底是IO密集型还是CPU密集型或者是二者的混合,epoll提供了水平触发和边缘触发两种方式。不过,在实地看到的版本中,水平触发应用还是比较常见的。但不代表边缘触发用得少。
epoll解决了前面两种模式的两个大问题即文件描述符在用户空间和内核空间的拷贝和描述符控制的限制以及其太多引起的效率的急剧降低。
二、epoll模式的基础
这里要澄清一些问题,就是epoll是不是确实如大家想象的千百倍的提高了网络通信性能。答案可能出乎大家的意料,在一般的应用场景下,可能有很大的提高,但很多场景下,其实epoll未必如此,甚至可能都不如select。大家在网上看到的的连接几十万个甚至上百万个连接的例程,都只是单纯的连接,并没有业务处理或者说极其简单的打印一个类似“Hello”的通信。所以这种例程只是向大家证明,epoll是可以支持上百万个连接的(当然,这需要一系列参数的完善和修改,请参看前面的文章“Socket并发配置之一config的配置”)。
那么怎么判断孰优孰劣呢?还是那句老话,看场景和实际应用环境。比较总是要有限定条件的,不然哪里有什么包打天下的技术。如果单纯从并发的效率来看,如果在小活动连接并发的情况下,epoll几乎是碾压式的胜出select和poll等,看下面的图:
注意:图中增加了kqueue这种网络模型
这个图需要简单的说明一下,图中是限制了100个活跃连接的基础测试,每个连接进行1000次的读写操作。图中的横轴代表了Socket句柄的数量,纵轴代表了请求响应的时间。从测试结果来看,随着Socket句柄的增加epoll和kqueue几乎不受影响,但Select等两个则有一个明显的上升趋势。
也就是说,在这种场景下,epoll的优势是非常明显的,并且随着Socket句柄的不断的增加性能依然保持稳定,从而更优于Select和Poll。但是,如果活跃的并发是1000个,1万个甚至全都是呢?epoll的优势同样会丧失殆尽。
可实际情况比这种场景可能更复杂,比如客户端同时传输大文件(视频)等,所以实际的类似网络通信会在设计上规避这种响应逻辑或者使用其它的方式(比如类似P2P的方法)。
这种实际的情况告诉开发者,不要迷信技术,要实事求是的综合运用技术来解决问题。
好,从上面的分析,可以明显看出来epoll更适合高并发但瞬时活跃连接并不多的情况下,这种情况比较典型的就是IM及时通信。看上去并发非常多,但同一时间内活跃的并不多,比如一个人聊天,一般也就是和一两个人聊天,而且每次聊天是有间隔的。当然,这并不是说,IM开发简单,恰恰相反,由于其整体并发量的海量存在,使得其更难。或者说,实现一个简单的模型或者框架容易,但真正投入商用需要很长的路。
当然,在边缘触发的情况下,又增加了对大数据量的支持。
三、初步分析
epoll的通信分为边缘触发和水平触发两种情况:
1、水平触发
水平触发,Level Triggered即LT是默认的触发方式,只要在缓冲区内有数据可以读写操作时,epoll_wait则会触发事件,而且只要缓冲区内的数据没有被处理完成一直存在,则会一直触发此事件,直到数据处理完成。这样,就可以保证数据被上层应用准确完整的操作防止出现数据的丢失。但有得就有失,这种连续的触发事件(数据量大时会引起),必然会导致内核态和用户态的切换,降低效率。
特别是EPOLLOUT事件,为了防止连续触发(会产生大量不必要的通知)一定记得删除数据或删除此事件使用时再注册。
2、边缘触发
边缘触发,Edge Triggered即ET也很好理解,就是在发现缓冲区有可操作数据状态变换时,即触发事件,但这种事件只触发一次,直到下一次状态的变化。可能对于有一些硬件经验的开发者更容易理解,当一个电平被拉高时,会有一个上拉的曲线,在这个曲线到达一定位置后(上升沿),状态从0转成1,触发一些动作,然后一直持续到被拉低(下降沿)才会从1变成0.这时才会再次动作。这对于处理一些大数据的传输时很能提高效率,但缺点就是必须自己处理缓存中的所有数据直到确保数据已经操作完成。
注意,这个状态是内核控制的状态而不是用户操作的状态,对于读缓冲,是有新数据到添加到缓冲时触发;对于写缓冲则在是缓冲容量发生变化即内核删除确认的分组使空间空闲此时导致缓冲容易变化。
所以,在边缘触发的情况下,读状态下,必须尽快保证把数据读出,否则下次新增加的数据也不会触发事件(EPOLLIN);写状态下,特别是数据比较大时,数据未发送完成,需要动态的进行写事件(EPOLLOUT)的再次注册。否则无法再次发送。不过正常的情况下,一般对EPOLLOUT的处理比较宽松,只会在异常情况(比如缓冲区溢出)才注册此事件,从而控制再次发送的可能。
在早期的版本中,使用ET的方式,还可以在某些情况下避免惊群的效应。但在新的内核版本中,此种情况已经解决。
在前面Select和Poll的分析中可以知道,其对文件描述符是要进行一个全面的遍历的过程,这就意味着其的复杂度为O(n),而反观epoll则O(1),即其查找的时间是一个恒定的值。
原因就在于,epoll使用的是红黑树+双向链表,它分为为两种情况,一种是传入内核的文件描述符不变,则可以使用上次的结果;另外一种是变化,内核使用红黑树查找并修改双向链表。此时的查询仍然是O(1)。
另外,对于网上很多提及epoll使用了mmap这个事情,目前看源码中并未找到直接调用mmap的代码,可以说epoll并未使用mmap。但epoll确实借鉴了mmap的机制,实现了内核状态与用户状态的事件通知机制。有兴趣可以看一下代码fs/eventpoll.c中的源码。
四、总结
学习epoll可以明显看到技术栈升级的过程。这也再次证明,技术的发展,更多是在前面技术的基础上不断延革的,而技术革命本身就是一种小概率事件。技术革命更类似于基因突变,而正常的技术进步更类似于生物的自然进化。
万物相同,确实如此。