APP下载

IO网络模型的应用-基于执行绪实现&基于事件驱动实现

消息来源:baojiabao.com 作者: 发布时间:2024-05-14

报价宝综合消息IO网络模型的应用-基于执行绪实现&基于事件驱动实现

IO模型两个体系结构介绍

网络IO有很多种实现方式,主要的有两个体系结构,分别是:基于执行绪实现的设计,与基于事件驱动的设计。

一、基于执行绪实现的设计

基于执行绪实现遵循的思想是一个执行绪来处理一个连线(One-Connection-Per-Thread)。适用于:使用了非执行绪安全库而又要避免执行绪竞争的站点。

1. iterative服务器

这是最原始的网络思路,程式只有一个主程序。

主程序是一个死循环,不断的accept埠。当有连线建立后,就开始执行业务逻辑。处理完成后,关闭socket,循环这一过程。

这个方案也不适合长连线,但是很适合daytime这种write-only服务。iterative服务器一次只能处理一个呼叫,这样服务没有办法同时为多个客户端服务。

2. 预派生子程序,主程序呼叫accept

为了可以同时服务多个客户端,产生了称之为process-per-connection的方式。当建立连线后,fork一个子程序用来处理这个请求直到客户端断开连线。同时主程序则立即再次accept监听新的请求。

这使得服务器能同时为多个客户端服务。每个子程序服务于一个客户端的长连线,处理多个任务。客户端数目的唯一限制是操作系统对使用者能拥有多少子程序的限制。该方案适合并发连线数不大且一个连线上有很多有顺序的任务,同时计算响应时间的工作量远大于fork( )的开销的情况。

本方案中,如果使用子执行绪代替子程序的方案叫thread-per-connection。在 Java 1.4引入NIO之前,Java网络服务程式多采用thread-per-connection。执行绪方案中伸缩性受到操作系统执行绪数的限制,操作系统的scheduler一两百个还行,几千个的话是个不小的负担。

注:当主程序accept连线并fork子程序处理连线上的任务时,主程序close不会关闭连线。只有当主程序与子程序都close后,才会关闭连线。

3. 预派生子程序,每个子程序呼叫accept

如果请求以短连线为主,频繁的fork,开销过多。此时就可预先派生程序,多个子程序处理请求,以减少执行过程中fork的开销。

优点:

初始化时建立多个程序处理客户端连线,减少执行过程中fork的开销。

缺点:

预建立程序数目>客户端数目:造成程序资源浪费,增加程序切换开销;预建立程序数目惊群现象:服务程序在程式启动阶段派生N个子程序,各个子程序阻塞在对同一个listenfd的accept呼叫上,当第一个客户端连线到达时,所有阻塞的子程序都将被唤醒,其中只有最先执行的子程序将获得客户端连线。“惊群现象”造成效能受损。

4. 预派生子程序,锁保护accept

如图所示,让应用程序在呼叫accept前后安置某种形式的锁,这样在任意时刻只有一个子程序阻塞在accept呼叫中,不会产生多个程序同时呼叫系统accept,减少频繁的使用者态与系统态的切换。

nginx以在1.11.3在“惊群现象”问题采用同样的方法解决。nginx多程序的锁在底层预设是通过CPU自旋锁来实现。如果操作系统不支援自旋锁,就采用档案锁。

linux 4.5支援了EPOLLEXCLUSIVE,nginx-1.11.3也支援EPOLLEXCLUSIVE,不在自己在accept上实现锁了,交由系统底层实现。

5. 预先派生子程序,父程序accept

惊群现象”另一个方案:预先建立子程序,父程序负责accept监听埠,然后把连线上接收的资料通过套接字传递给子程序,以解决所有子程序的accept呼叫上锁问题。

W.Richard Stevens通过实验指出:父程序通过字节流管道传递到各个子程序,并且各个子程序通过字节流写回管道,比使用上锁的方式要更费时。增加了资料传输拷贝开销。实现上也更复杂,不推荐使用。

基于程序或执行绪实现的设计瓶颈

执行绪:是共享地址空间,从而可以高效地共享资料。

程序:一台机器上的多个程序能高效地共享程式码段(操作系统可以对映为同样的实体内存),但不能共享资料。

因此执行绪模型要比对应的程序模型要快。但执行绪执行在同一地址空间,一个执行绪的崩溃将导致整个程序的崩溃,另外无法实现热升级,nginx就是采用程序模型,支援reload\\upgrade。

程序与执行绪都受到操作系统执行绪数的限制。操作系统的scheduler一两百个程序还行,1千以内的执行绪还行,但是系统scheduler变得无法承受负担。

连线与执行绪之间存在对应关系,一个连线从开始到关闭一直会占用一个执行绪,如果使用Keep-Alive这样减少连线的建立成本的方式,势必会导致大量的工作执行绪在空闲状态下等待。另外,数百甚至数千并发连线所建立的执行绪会浪费储存器中的大量堆叠空间。

二、基于事件驱动的设计

基于事件驱动设计遵循的思想是将执行绪与连线分离开,执行绪只是用来处理特定的回拨或业务逻辑。

1. Reactor

Reactor设计模式是基于事件驱动的一种实现方式,采用基于事件驱动的设计,当有事件触发时,才会呼叫处理器进行资料处理。

主程序acceptor监听埠,一次获取多个连线,顺序处理每个连线的业务逻辑。虽然一次可以处理多个请求,但实现上还是一个执行绪完成所有任务。不适合多核CPU。

2. Reactor+Thread-per-task

在Reactor的基础上,acceptor读取到多个连线的多个任务后,为每个任务建立一个执行绪去处理,然后在处理完成时消毁执行绪。充分利用cpu,但增加了执行时动态建立执行绪的开销。另外多执行绪执行顺序不确定,会导致一个连线的多个请求,在多个执行绪同时处理:新来的还在执行中,后到的请求已经被执行完了。

3. Reactor+Threadpoll

为减少建立执行绪的开销,程式在启动时,预先建立执行绪池。Reactor使用acceptor接到请求事件后,读取请求资料,然后再将资料通过threadPool分配处理。利用执行绪池高效的处理CPU密集型的业务逻辑。

但如果服务是高IO型,只有一个执行绪负责读取没有办法充分利用多核。

4. Multiple Reactors

主执行绪负责acceptor监听埠,获取到多个连线后,将他们分配到多个subReactor。subReactor负责资料的读取&处理,提高的了IO的吞吐量。

subReactor的资料一般是固定的,为CPU核数或CPU核数的2倍。程式可以充多利用多核,当IO的并发升高后,依赖多核能力,总体处理能力不会下降。

一个连线的业务逻辑一直在一个执行绪中处理,可以保证资料的处理是有序的。但是由于subReactor处理是一个执行绪,如一个业务逻辑耗费大量CPU时间时,业务处理能力就开始下降。

5. Multiple Reactors+Threadpoll

为了兼顾 IO密集型与CPU密集弄的问题。使用多个subReactor+ 处理执行绪池的方式。

总结

我们使用的nginx、apache、tomcat、netty、muduo、javaNIO之类的软件,他们都是使用上面设计思想中一种或几种的组合。

我们在应用的时候,需要按我们的具体需要场景来使用,比如:是长连线任务多还是短连线多、一个连线上任务的有序性要求、事务性、IO密集型、CPU密集型等来选定服务实现方式。

欢迎大家关注“58架构师”微信公众号,定期分享云端计算、AI、区块链、大资料、搜寻、推荐、储存、中介软件、移动、前端、运维等方面的前沿技术和实践经验。

2019-11-25 22:53:00

相关文章