事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。多线程是另一种常用编程范式,并且更容易理解。 高性能通用型C++网络框架 Nebula 是基于事件驱动的多进程网络框架(适用于即时通讯、数据采集、实时计算、消息推送等应用场景),已有即时通讯、埋点数据采集及实时分析的生产应用案例。经常有人问Nebula的每个进程里是单线程还是多线程的?又问为什么不用多线程?不用多线程又怎么处理并发问题? 最近 Nebula 将会用于一个新的生产项目——推荐引擎,在此之前团队已有使用某知名度较高的RPC框架多线程版推荐引擎(业界许多推荐引擎都用了目前比较知名的开源RPC框架来开发)。本文不做Nebula与各知名RPC框架的比较,也无意说明哪个框架更适合做推荐引擎,只说明Nebula可以用于推荐引擎,且有信心效果会很好。最终结果如何,等推荐引擎研发出来,拭目以待。 为什么是事件驱动而不是多线程?事件驱动无须多线程。我们先来回顾一下服务器编程范式。 《UNIX网络编程》卷一里介绍了9种服务器设计范式: 九种服务器设计范式并不是全都有实用价值,在《UNIX网络编程》卷一最后一节里给出了几种TCP服务器设计范式代码示例: Nginx采用的是九种服务器设计范式里的第5种“预先派生子进程,使用互斥锁上锁方式保护accept”,Nebula采用的是九种服务器设计范式里的第6种“预先派生子进程,由父进程向子进程传递套接字文件描述符”。 一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。 某种意义上说,服务端程序大多是事件驱动的,或者说是IO请求事件驱动的。这里比较的编程模型里的事件驱动是指事件处理部分是异步的,即不仅IO请求事件驱动,还有IO响应事件驱动,它的特点是当外部IO响应事件发生时使用回调机制来触发相应的处理。 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。 在多线程模型,每个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。另一个问题,操作系统内核在切换线程的同时也要切换线程的上下文,当线程数量过多时,时间将会被耗用在上下文切换中。所以在大并发量时,多线程结构还是无法做到强大的伸缩性。 在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。当无IO操作时每个任务占用cpu的时间又比较少,进程就会处于空闲状态。同等并发量情况下,事件驱动占用的系统资源会更好,负载足够大时,事件驱动程序可以将cpu利用到100%。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。 事件驱动的一个非常有代表性的实现Node.js和redis,都是一个单进程(单线程)的服务(redis的数据落地或主从同步线程排除,其服务就是单线程的),事件处理都通过异步回调执行。第二节中单线程、多线程、事件驱动编程模型等类似比较中看起来事件驱动是单线程的,Node.js这一典型的事件驱动服务也是单线程的,导致许多人以为事件驱动只能是单线程的,不能充分利用多CPU多核资源。其实不然,Nginx也是一个典型的事件驱动服务,而Nginx是多进程的。从逻辑上划分后端服务,Nginx归为接入通信层(openresty这种nginx+lua实现业务逻辑的不在讨论范围),Node.js归为业务逻辑层。接入通信层的特点都是IO行为几乎不大消耗CPU是天然适合事件驱动的,也比较容易实现,而业务逻辑层的特点决定了事件驱动方式实现非常复杂,但这并意味着业务逻辑层的多线程事件驱动难以实现。 Nebula就是一个多进程事件驱动服务的典型。事件驱动的每一个进程都足够高效,多个进程(多线程)又充分利用多CPU多核资源。Nebula的进程模型与Nginx相似,区别在于Nginx是各worker互斥锁上锁accept,而Nebula是由master进程accept后将连接对应的文件描述符传送给worker进程(跟Memcached相似)。Nebula是从满足即时通讯应用而开发的Starship框架发展而来的,与nginx的进程(线程)模型存在相似纯属偶然。为什么Nebula选择传送文件描述符而不是各worker进程抢accept?跟Nebula定位有关系,Nebula不仅需要做接入通信层、数据代理层,更要做业务逻辑层,分布式服务的各层服务都可以且应该用Nebula实现,这意味着每一个worker进程接近于分布式服务的一个节点的功能,如果是worker抢占式accept就无法做定向路由。为什么选择多进程而不是多线程?先看看多进程与多线程的优缺点比较: 多进程: 多线程: 多进程的前三点都是优点,第四点是缺点。Nebula选择多进程就不需要考虑锁和同步资源问题,数据和错误隔离,worker进程崩溃不会影响整个节点服务,会被master进程迅速拉起。第四点缺点在Nebula不需要考虑,因为Nebula事件驱动的进程之间是不需要切换的,可以近似地认为每个worker进程都是一个节点,节点与节点之间只有网络通信,不需要共享资源更不需要做切换。 对于IO密集型的业务,事件驱动比多线程同步的并发能力要高很多,可以说不是一个数量级的。而大部分互联网业务都属于IO密集型业务,因此事件驱动的适用场景非常广泛。程序中有许多高度独立的任务,在等待事件到来时,某些任务会阻塞,单个任务需要占用较少CPU资源。 Nebula 适用于即时通讯、数据采集、实时计算、消息推送等应用场景,也适用于web后台服务。Nebula已有即时通讯、埋点数据采集及实时分析的生产应用案例,很快将有一个面向亿级用户的推荐引擎生产应用案例。 说到推荐系统,首先被想到的可能是基于内容、协同过滤、基于人口统计学、基于知识、基于社区、混合推荐等推荐技术。推荐技术的实施通常基于hadoop,用hive、spark、storm、flink等来实现。这些通常被称为推荐的数据挖掘部分。 推荐引擎是推荐系统核心之一,负责将数据挖掘的结果按一定排序推送给用户,这就是推荐引擎的主要功能。 已知业界推荐引擎有使用C++开发也有使用Java开发,C++开发占大多数。在Bwar了解到的C++开发的推荐引擎中多使用rpc框架,使用thrift的4个,使用brpc的2个,使用grpc的1个,使用tars的1个。因这些开源rpc框架不是专为推荐引擎所开发的框架,开发人员通常会在这些框架之上再架设一层框架,然后才是业务逻辑开发。Bwar接触的一个推荐引擎就是基于brpc再开发了自己的框架然后才做业务逻辑开发,其开发难度比较大,且不容易扩展。也许是开发人员对这些开源rpc框架理解不够深入,导致业务逻辑开发比较复杂,对后续需求扩展不易。 Nebula是Bwar开发的C++网络框架,生而为分布式服务,经过两个生产环境的应用。Nebula不是rpc框架而是一个基proactor(框架层实现proactor而非操作系统支持)事件驱动(回调)的框架。并不像大多数异步事件回调框架那样开发者需要自己注册回调函数,Nebula同时也是个IoC框架,通过actor类的巧妙设计实现降低了异步编程的复杂度,开发者真正意义上只需聚焦业务逻辑开发。 Nebula框架提供的Cmd类非常适合推荐服务的逻辑入口,支持动态加载,随时不停机升级推荐算法推荐模型。Step类异步获取redis等存储中的数据,无阻塞等待让cpu资源只用于推荐逻辑。session类用于缓存用户、item、模型等数据。所有的数据获取、传递均可通过session智能指针十分方便而高效地得到。 在那些基于rpc框架的推荐引擎中,许多开发人员提到了反射功能,并且通过大量宏以很费劲很难开发云主机域名理解的方式实现了所谓的反射功能。这些都不是IoC框架,Bwar不理解为什么需要实现反射功能,如果用Nebula来做将是非常简单的事,Nebula是IoC框架,所有的actor实例创建都是通过反射创建的,无须开发者做业务逻辑之外的任何事情。Nebula的反射实现很优雅,如果感兴趣,可以参考这篇文章《C++反射机制:可变参数模板实现C++反射》。 开发Nebula框架目的是致力于提供一种基于C++快速构建高性能的分布式服务。如果觉得本文对你有用,别忘了到Nebula的 Github 或 码云 给个star,谢谢。参考资料:
由于代码1处执行完后直接进入2、3,那么netty服务端就会关闭退出。解决一、直接在代码1后面处加上同步阻塞sync,那么只有服务端正常关闭channel时才会执行下面的语句解决二、把代码2和3移到operationComplete里面,那么也只有channe…
免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。