服务编程模型

常见任务类型

计算机要处理的任务,根据任务本身的性质,大致包括:计算密集型任务、IO密集型任务、混合型任务。

服务编程模型

常见的服务编程模型,可以根据是采用(单/多)进程模型、(单/多)线程模型、对网络IO的处理方式的差异,进行归类。

我们先解释下一些基本概念。

进程 vs 线程

进程是资源分配的基本单位,线程是任务调度的基本单位,一个进程可以包含多个线程,线程共享进程资源。进程隔离相对来说更安全,即一个进程崩溃,不会影响到其他的进程。但是进程中一个线程崩溃则可能导致整个进程崩溃。在Linux中一个线程其实是轻量级进程,它共享进程的某些资源而已。

当执行网络IO阻塞,或者执行一些其他的阻塞型系统调用的时候,线程会被阻塞以让出CPU给其他线程执行。这种情况通常在IO密集型服务中关注比较多,也进一步会带来对同步阻塞、同步非阻塞、异步、实时信号驱动等方式的讨论。在Linux下比较成熟的是IO多路复用技术epoll(属于同步非阻塞)。

下面我们来看看常见的编程模型,我们这里涉及到的网络IO将只限于epoll IO多路复用技术(select、poll在需要高并发场景下有明显的不足)。

单进程+单线程

如果计算场景是IO密集型,单进程+单线程模型,结合epoll IO多路复用,实现的服务依旧可以获得很高的性能,redis 6之前版本就是基于这种编程模型实现的,可以获得很不错的性能。redis 6版本也引入了多线程来实现更快速的IO。

单进程+多线程

单进程+多线程模型,结合epoll IO多路复用,也是比较常见的,比如进程内维护一个线程池,姑且称之为worker threads,然后有一个专门的线程来进行网络IO,收到请求之后将请求dispatch到一个全局request queue中或者各个worker thread特定的request queue中,worker threads从request queue中取走任务完成处理,并将结果写会response queue中,IO线程负责取出响应并回包。

这种编程模型,其实更常见,比如很多Java Web服务都是单进程+多线程的。

多进程+单线程

有些服务采用是采用多进程+单线程模型,并结合epoll IO多路复用,这种也比较常见,典型如nginx。nginx服务启动之后会有一个专门的master进程负责配置加载等工作,然后还会起多个worker进程负责建立连接、处理客户端请求。这里的worker采用了多进程模型,而各个进程内部又采用了单线程+IO多路复用的方式,这里的多个worker可以绑定到不同的cpu core充分利用多核处理能力。

笔者在腾讯期间,也见过类似服务编程模型的一些框架,比如用过的较多的Serverbench Plus Plus(俗称SPP)。一个SPP服务部署后主要包括一个proxy进程(单线程)、多个worker进程(单线程)、一个controller进程(单线程)。proxy进程负责网络IO,将请求通过各worker私有的请求队列(基于共享内存实现的无锁队列)分发请求给worker,并从worker私有的响应队列接受worker的处理结果,proxy再回包,这里的网络IO借助了IO多路复用技术。

多进程+多线程

多进程+多线程模型,结合epoll IO复用技术,这种可以进一步挖掘并发处理潜力。在单线程程序中,虽然我们可以借助非阻塞fd解决网络IO给线程带来的阻塞问题,也可以借助epoll来实现高效的IO事件处理,但是能够造成线程阻塞的原因,并不只有网络IO一种,有些其他的阻塞性系统调用也是可以导致线程阻塞。

如果只是单线程,并且不可避免地要用到一些阻塞型系统调用的话,那性能就会受影响,多线程程序可以减轻这里的影响。nginx中也可以开启线程池可以让nginx处理性能取得明显提升,来避免某些阻塞操作对性能的影响。

协程

进程比线程重,线程比协程重,线程切换的开销比协程切换开销多太多,对于要求更高并发的场景而言,减少线程切换所引入的开销也是一个研究的重点。

对于网络IO而言,前面我们提到借助epoll IO多路复用技术即可避免线程阻塞问题,但是通过这种epoll事件回调处理的方式来编程,并不是一种很友好的方式。开发人员希望像同步的方式来编写代码,同时又能拥有异步执行的高效。协程是一个解决思路,基于对epoll事件的管理,来绝对何时挂起、恢复一个协程的执行,以实现“同步编码、异步运行”之功效。

当然,我们在任务处理中除了网络IO,还会涉及到一些其他的操作。我们可以创建一个线程,但是我们也了解到了线程的创建、销毁、切换开销都是比较大的,而协程是比较轻量的,它是个不错的选择。如果不能理解协程为何轻量,请阅读这篇文章[Why Go so fast](https://laptrinhx.com/why-go-so-fast-1818987478/)。

如果执行时陷入阻塞性系统调用,就会阻塞线程,引起线程切换、恢复的开销,其他任务也可能会被delay,因此对阻塞型系统调用一般也要特殊处理,比如进入之前创建一个新线程,或者协程管理器观测到runnable threads不足时创建新线程来执行任务。

协程从开发者便利性、性能等方面来说,都是很值得追求的,我们也看到不管是Go、Java、C\CPP都有这方面的尝试。

选择合适的编程模型

下面展开描述下几种任务的区别,以及各自适合采用的服务编程模型。

计算密集型任务

需要长时间占据CPU进行计算的任务属于计算密集型任务,这类任务执行所花费的时间大部分都是CPU片上时间,而极少有CPU片下时间。简言之,就是说这类任务执行期间不会主动让度出CPU给其他任务执行。

这类任务,使用多线程编程模型是不合适的,可以考虑使用多进程模型来处理。

多线程得以发挥其并发处理的优势的前提是,线程之间能交替执行,在某个线程实体所依赖的资源暂时不满足时,让度出CPU给其他可以执行的线程实体继续执行。这样在宏观上,多线程在资源有限时可以实现更大程度地并发处理,才可以发挥其优势。但是计算密集型任务,线程没有合适的时机来主动让度CPU给其他线程,一个线程和多个线程相比,并无什么优势。

进程,是操作系统进行资源分配的实体,线程,则是操作系统进行任务调度的实体。通过多进程编程模型,可以整体获得更多的CPU计算资源(时间片),同时结合taskset可以将多个进程分别绑定到不同的CPU或CPU core上,这样多个进程实现并行处理,可以获得更高的并发处理能力。

计算密集型任务,一般是通过多进程编程模型来解决的。

IO密集型任务

任务执行期间花费的时间消耗在CPU上的片上时间极少,大部分都消耗在IO上,可能是文件IO(如nfs server),也可能是网络IO(rpc服务)。

这类任务,主要的优化项在于对IO的非阻塞处理,不管是单/多进程、单/多线程、协程,其根本要处理的问题都是对IO操作的非阻塞优化。

以rpc服务为例,网络IO操作过程中,如果没有对相关socket进行非阻塞处理,那么执行IO操作的线程是一定会被阻塞的,意味着这个线程在网络IO操作完成之前什么其他任务也做不了,只能等到操作系统内核完成该IO操作时才能被重新唤醒恢复执行。不管你有多少个线程,只要并发量一大,这种编程模型实现的服务是根本扛不住的。

但是,对网络IO操作进行非阻塞处理之后,如IO多路复用(如Linux下epoll)、rtsig(实时信号驱动)等,效果就不一样了。这里以IO多路复用为例,线程会被告知何时read ready、何时write ready,执行过程中线程几乎可以立即完成,几乎没有等待时间。通过这种方式,即便是单进程单线程模型也可以获得非常高的性能,典型的业界代表就是redis缓存服务器。

当然了,理论上多线程编程模型会比单线程带来更高的性能,只是理论上,多线程会引入同步互斥相关的操作,锁竞争带来的开销、线程上下文切换带来的开销也会加剧。那多进程单线程呢,多进程多线程呢,我只能说,具体问题具体分析,进行实际的压力测试对服务性能进行量化分析是一个比较靠谱的做法。

线程,在Linux下面其实是轻量级进程LWP来实现的,只不过它与父进程共享了某些计算资源资源,如内存地址空间、名字空间等等,线程数量多了之后,线程切换的开销就不得不考虑了,这也是某些高并发中间件在设计实现时采用单线程或线程池来绕过这个问题的原因之一。

线程切换需要进入内核,涉及到几千条指令、几十上百个寄存器的保存恢复,这里的开销是不可忽略的,还有线程栈本身大小,对内存也是一个比较大的消耗。协程是更加轻量级的任务实体,以goroutine为例或者libmill中的coroutine为例,协程切换只涉及到几百条指令执行、几个寄存器的保存恢复,而且协程栈大小比线程小的多,如Linux下goroutine协程栈初始大小2KB,而线程栈2MB,1000倍!

协程,往往还会结合一些Hook系统调用的操作,如hook系统调用read、write,实现“同步编码、异步运行”的效果,不光有助于性能的提升,也有助于编码效率、代码可读性、可维护性的提高。

混合型任务

混合型,指的就是计算密集型、IO密集型两种任务兼而有之,这种情况下就不能做非黑即白的假设了。

同一个服务中同时实现两种任务是否满足要求,也要根据具体情况来分析。比如,多数任务是IO密集型,计算密集型占少数,但是少量的计算密集型任务也可能会导致大量的IO密集型任务无法得到快速有效的响应,这种是否符合预期的设计要求,就需要权衡。

是否应该将两种类型的计算任务进行重构、分离,并对两种类型的服务做针对性的优化、部署,这些都要结合具体的场景、预期性能、成本核算来做权衡。

参考资料

results matching ""

    No results matching ""