多线程、事件驱动与推荐引擎框架选型

  事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特色是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。多线程是另外一种经常使用编程范式,而且更容易理解。html

  高性能通用型C++网络框架 Nebula 是基于事件驱动的多进程网络框架(适用于即时通信、数据采集、实时计算、消息推送等应用场景),已有即时通信、埋点数据采集及实时分析的生产应用案例。常常有人问Nebula的每一个进程里是单线程仍是多线程的?又问为何不用多线程?不用多线程又怎么处理并发问题?nginx

  最近 Nebula 将会用于一个新的生产项目——推荐引擎,在此以前团队已有使用某知名度较高的RPC框架多线程版推荐引擎(业界许多推荐引擎都用了目前比较知名的开源RPC框架来开发)。本文不作Nebula与各知名RPC框架的比较,也无心说明哪一个框架更适合作推荐引擎,只说明Nebula能够用于推荐引擎,且有信心效果会很好。最终结果如何,等推荐引擎研发出来,拭目以待。git

  为何是事件驱动而不是多线程?事件驱动无须多线程。咱们先来回顾一下服务器编程范式。程序员

1. 服务器程序设计范式

  《UNIX网络编程》卷一里介绍了9种服务器设计范式:github

服务器设计范式图

  九种服务器设计范式并非全都有实用价值,在《UNIX网络编程》卷一最后一节里给出了几种TCP服务器设计范式代码示例:web

  • TCP并发服务器程序,每一个客户一个子进程
  • TCP预先派生子进程服务器程序
  • TCP预先派生子进程服务器程序,传递描述符
  • TCP并发服务器程序,每一个客户一个线程
  • TCP预先建立线程服务器程序,每一个线程各自accept
  • TCP预先建立线程服务器程序,主线程统一accept

  Nginx采用的是九种服务器设计范式里的第5种“预先派生子进程,使用互斥锁上锁方式保护accept”,Nebula采用的是九种服务器设计范式里的第6种“预先派生子进程,由父进程向子进程传递套接字文件描述符”。redis

2. 单线程、多线程以及事件驱动编程模型比较

  一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照必定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。算法

  某种意义上说,服务端程序大可能是事件驱动的,或者说是IO请求事件驱动的。这里比较的编程模型里的事件驱动是指事件处理部分是异步的,即不只IO请求事件驱动,还有IO响应事件驱动,它的特色是当外部IO响应事件发生时使用回调机制来触发相应的处理。编程

单线程、多线程、事件驱动比较图

  在单线程同步模型中,任务按照顺序执行。若是某个任务由于I/O而阻塞,其余全部的任务都必须等待,直到它完成以后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。若是任务之间并无互相依赖的关系,但仍然须要互相等待的话这就使得程序没必要要的下降了运行速度。后端

  在多线程模型,每一个任务分别在独立的线程中执行。这些线程由操做系统来管理,在多处理器系统上能够并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其余线程得以继续执行。与完成相似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,由于这类程序不得不经过线程同步机制如锁、可重入函数、线程局部存储或者其余机制来处理线程安全问题,若是实现不当就会致使出现微妙且使人痛不欲生的bug。另外一个问题,操做系统内核在切换线程的同时也要切换线程的上下文,当线程数量过多时,时间将会被耗用在上下文切换中。因此在大并发量时,多线程结构仍是没法作到强大的伸缩性。

  在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其余昂贵的操做时,注册一个回调到事件循环中,而后当I/O操做完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询全部的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽量的得以执行而不须要用到额外的线程。当无IO操做时每一个任务占用cpu的时间又比较少,进程就会处于空闲状态。同等并发量状况下,事件驱动占用的系统资源会更好,负载足够大时,事件驱动程序能够将cpu利用到100%。事件驱动型程序比多线程程序更容易推断出行为,由于程序员不须要关心线程安全问题。

3. 事件驱动 != 只有一个线程

  事件驱动的一个很是有表明性的实现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就没法作定向路由。为何选择多进程而不是多线程?先看看多进程与多线程的优缺点比较:

  多进程:

  • 编程相对容易;一般不须要考虑锁和同步资源的问题
  • 更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其余进程
  • 有内核保证的隔离:数据和错误隔离
  • 进程切换开销大

  多线程:

  • 建立速度快
  • 共享数据,多线程间能够共享同一虚拟地址空间,多进程间的数据共享就须要用到共享内存、信号量等IPC技术
  • 较轻的上下文切换开销
  • 一旦有一个线程挂掉,整个进程均可能会挂掉
  • 须要对共享资源的访问进行同步

  多进程的前三点都是优势,第四点是缺点。Nebula选择多进程就不须要考虑锁和同步资源问题,数据和错误隔离,worker进程崩溃不会影响整个节点服务,会被master进程迅速拉起。第四点缺点在Nebula不须要考虑,由于Nebula事件驱动的进程之间是不须要切换的,能够近似地认为每一个worker进程都是一个节点,节点与节点之间只有网络通讯,不须要共享资源更不须要作切换。

4. 事件驱动适用场景

  对于IO密集型的业务,事件驱动比多线程同步的并发能力要高不少,能够说不是一个数量级的。而大部分互联网业务都属于IO密集型业务,所以事件驱动的适用场景很是普遍。程序中有许多高度独立的任务,在等待事件到来时,某些任务会阻塞,单个任务须要占用较少CPU资源。

  Nebula 适用于即时通信、数据采集、实时计算、消息推送等应用场景,也适用于web后台服务。Nebula已有即时通信、埋点数据采集及实时分析的生产应用案例,很快将有一个面向亿级用户的推荐引擎生产应用案例。

5. 推荐引擎框架选型

  说到推荐系统,首先被想到的多是基于内容、协同过滤、基于人口统计学、基于知识、基于社区、混合推荐等推荐技术。推荐技术的实施一般基于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类的巧妙设计实现下降了异步编程的复杂度,开发者真正意义上只需聚焦业务逻辑开发。

Actor类图

  Nebula框架提供的Cmd类很是适合推荐服务的逻辑入口,支持动态加载,随时不停机升级推荐算法推荐模型。Step类异步获取redis等存储中的数据,无阻塞等待让cpu资源只用于推荐逻辑。session类用于缓存用户、item、模型等数据。全部的数据获取、传递都可经过session智能指针十分方便而高效地获得。

  在那些基于rpc框架的推荐引擎中,许多开发人员提到了反射功能,而且经过大量宏以很费劲很难理解的方式实现了所谓的反射功能。这些都不是IoC框架,Bwar不理解为何须要实现反射功能,若是用Nebula来作将是很是简单的事,Nebula是IoC框架,全部的actor实例建立都是经过反射建立的,无须开发者作业务逻辑以外的任何事情。Nebula的反射实现很优雅,若是感兴趣,能够参考这篇文章《C++反射机制:可变参数模板实现C++反射》

  开发Nebula框架目的是致力于提供一种基于C++快速构建高性能的分布式服务。若是以为本文对你有用,别忘了到Nebula的 Github 或 码云 给个star,谢谢。

参考资料:

相关文章
相关标签/搜索