人生苦短,不如养狗
作者:闲宇
公众号:Brucebat的伪技术鱼塘
一、前言
无论是在面试中,还是在日常工作、学习中,谈及系统高性能时,经常会聊到这样一个技术:多路复用技术。但是作为一个应用层开发工程师,绝大部分的时间会将精力聚焦于应用本身。对于多路复用技术,更多的只是进行一些较为浅显、模糊的了解,然后快速地去投入使用。
为了更好地去使用这项技术,今天我们就来深入了解一下多路复用技术的实现细节和它为什么能够成为高性能大杀器的原因。
二、什么是多路复用技术
多路复用技术是操作系统提供的一种允许应用程序在一个线程中高效管理大量并发连接,而不需要为每个连接创建单独工作线程的技术。不过,这样的定义显得过于简单,且并没有真正点明多路复用技术实现的关键。而要想真正弄清楚多路复用的实现细节,就一定要弄清楚以下两个关键技术点:
- 事件驱动模型
- 单线程管理
这里,闲宇准备借助大家最为熟悉的Redis内存数据库来分析一下IO多路复用对于这两个关键技术点的使用。
1. 事件驱动模型
事件驱动模型可以说是实现多路复用技术的基石。但我们在实际使用类似epoll
、kqueue
等系统提供的多路复用函数时,其实并不能非常直观地感受到事件驱动模型在这些函数当中的应用。这主要是因为事件驱动模型的实现是依赖于系统内核 和相关的硬件中断 来完成的,对于应用层来说,这部分的细节是完全无感的。所以,要想具体了解对应的实现细节,我们还需要结合底层操作系统来具体分析一下。
从上图中可以看到,Redis在接收到网络请求之后会使用系统调用创建一个Socket文件描述符,然后使用epoll
函数将对应的Socket文件描述符注册到IO多路复用的监听列表当中。从这里开始,Redis就会将对应的监听工作完全托管给系统内核,主线程会调用epoll_wait
函数阻塞式等待,直至内核返回一个需要处理的事件。在这个过程当中,Redis处于等待状态,不会消耗任何的CPU和IO,同时也不需要为每个连接创建一个单独的线程来进行管理。
对于系统内核来说,Socket状态的监听也并不是通过简单的CPU轮询来完成的。当一个网络请求触达服务器网卡时,网卡会产生一个硬件中断 ,告知CPU有新的数据需要处理。CPU在接收到对应的中断信号之后,会交由网卡驱动程序 处理对应的数据包。处理完成的数据包会传递给网络协议 进行数据的解析和验证。在完成对应的解析和验证之后,数据会被写入到对应的Socket缓冲区,并将其状态标记为"可读",同时通知系统内核将对应使用epoll
函数监听该Socket的应用程序唤醒。
可以看到,在操作系统层面,数据的传递和处理是通过一个又一个事件来进行驱动的,整个过程都是以非阻塞 的方式进行。通过这种方式,操作系统可以极大地减少就绪事件的无用轮询 以及可能的线程上下文切换导致的CPU开销和等待。
2. 单线程管理
借助操作系统内部实现的事件驱动模型,Redis可以简单地使用一个线程 就能完成大量并发Socket连接的管理工作。同时,这也是多路复用技术中 "复用" 的体现。需要注意,这里的单线程管理指的是在应用层程序中使用单线程来调用IO多路复用函数。
至于,为什么不推荐使用多线程来调用IO多路复用函数,这里主要有两个原因:
- 第一,单线程模型能够极大地减少线程上下文切换导致的CPU消耗;
- 第二,使用多线程调用IO多路复用函数,可能会出现多个线程同时处理同一个Socket连接的情况。而为了解决这种情况导致的并发问题,就需要额外引入一些同步机制来避免,性能反而会下降,属实有点得不偿失;
三、多路复用究竟高性能在哪里?
从上面对于多路复用关键技术点的分析其实不难发现,多路复用技术对于性能的优化最重要的一点就在于改变了应用层程序在传统BIO模式下需要使用线程绑定连接和阻塞式读写来管理Socket的工作方式。
在传统的BIO模式下,每个Socket连接都需要创建一个单独的线程进行管理,阻塞式地读取Socket缓冲区中的数据。在这个方案中,每有一个Socket连接都需要应用层创建一个单独的工作线程,每个线程的创建都会带来不小的内存开销。同时,多线程模型还会导致频繁CPU进行频繁地上下文切换,这又是一笔不小的开销。除此以外,阻塞式读取数据的方式还有可能阻塞其他线程的处理工作,导致整个应用的响应延迟宾大。
而使用多路复用技术则可以很好地避免这两个问题。通过多路复用技术,应用层将Socket连接的状态管理托管给了系统内核,只需要等待系统内核来通知需要处理的Socket事件,而不需要额外创建多个线程来进行Socket状态管理。当然,这也要求了应用层在进行Socket缓冲区读取时必须使用非阻塞式的读取方式,以保证不会将工作线程阻塞。
四、总结
相比传统的阻塞式IO而言,多路复用技术最本质的优化是在于将Socket的状态管理迁移至系统内核,以此来重构应用层的网络编程范式,驱动应用层的设计从"线程绑定连接"的同步模型向"事件驱动"的异步模型演进。
当然,多路复用技术也不是万能的。对于计算密集型场景和强实时性场景,多路复用技术的优势就会显得不是那么明显。前者更多的是依赖CPU进行复杂计算,如果某个请求计算时间过长,反而会导致后续所有事件被阻塞。而后者则可能会由于事件驱动模型的异步特性导致的响应延迟波动,无法满足其对实时性的要求。
除此以外,对于一些低并发场景和简单业务逻辑场景,使用多路复用技术也不是非常合适。在这些场景中,事件驱动模型带来的性能提升并不能抵消其复杂的编程设计成本。
综上,在我们尝试使用多路复用技术进行系统优化时,还是先对业务场景和实现成本进行准确评估之后再进行对应优化改造。"不要问,就是莽"的行为还是不可取的。
最后,希望各位朋友都能身体健康,心想事成,早日暴富~~