8.Linux 高性能服务器编程 --- 高性能服务器程序框架

1.服务器模型
	1.C/S 模型
	2.P2P 模型

2.服务器编程框架
	1.IO处理单元 : 
		处理用户连接,读写网络数据;(单机)
		作为接入服务器,实现负载均衡;(集群)
	2.请求队列
		各个单元之间通信的抽象,通常被实现为池的一部分;
		一个单元通知另外一个单元,或者多个单元访问同一个存储单元时,竞争的协调;
		对于集群来说,请求队列是预先建立的
	3.逻辑单元
		一个进程或者线程;(单机)
		一台逻辑服务器;(集群)
	4.网络存储单元
		可以是数据库,文件,缓存;(单机)
		数据库服务器;(集群)

3.I/O 模型
	1.阻塞IO
	2.非阻塞IO
	3.IO复用(IO通知机制) :
		select, poll, epoll_wait;
		应用程序通过 IO复用函数向内核注册一组事件,内核通过IO复用函数把其中准备就绪的事件通知给应用程序;
		IO复用本身是阻塞的,高效的原因是它同时监听多个IO事件的能力;

	4.SIGIO 
		也可以用来报告 IO事件 ;
		将一个目标文件描述符指定为宿主进程,被指定的进程将捕获到 SIGIO 事件,
		这样我们就可以在信号处理函数中对目标文件描述符执行非阻塞IO操作了;

	5.异步IO


	理论上说,阻塞IO,IO复用,信号驱动IO 都是同步IO模型。因为这3种IO模型,IO的读写,都在 IO事件发生以后,由应用程序自己完成;
	对异步IO而言,用户可以直接对IO执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及IO完成后内核通知应用程序的方式,总是
	立即返回,真正的读写已经由内核接管。
	同步IO模型要求用户自行执行IO操作,异步IO则由内核来执行IO操作;
	同步IO向应用程序通知的是IO就绪事件,异步IO向应用程序通知的是IO完成事件;


4.两种高效的事件处理模式
	服务器程序通常要处理三类事情:IO事件,信号,以及定时事件

	1.Reactor(同步IO模型) :
		要求主线程只负责监听文件描述符上是否有事件发生,有的话通知工作线程;
		读写数据,接受新的连接,以及处理客户请求均在工作线程中完成;

		1.主线程往epoll内核事件注册socket上的读就绪事件
		2.主线程调用 epoll_wait 等待 socket 上有数据可读
		3.当 socket 上有数据可读时,epoll_wait 通知主线程,主线程将 socket 可读线程事件放入请求队列
		4.睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件
		表中注册该 socket 上的写就绪事件
		5.主线程调用 epoll_wait 等待 socket 可写
		6.当 socket 可写时, epoll_wait 通知主线程.主线程将 socket 可写事件放入请求队列
		7.睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户端请求的结果。


	2.Proactor(异步IO模型)
		与Reactor 模式不同, Proactor 模式将所有的 IO 操作都叫给主线程和内核来处理,
		工作线程仅仅负责业务逻辑。

		1.主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,
		以及读操作完成时如何通知应用程序。
		2.主线程继续处理其他逻辑。
		3.当socket上的数据被读入用户缓冲区后,内核向应用程序发送一个信号,以通知应用程序数据已经可用。
		4.应用程序预先定义好的信号处理函数选择一个工作线程来处理客户的请求。工作线程处理完客户请求后,
		调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成
		时如何通知应用程序。
		5.主线程继续处理其他逻辑
		6.当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
		7.应用程序预先设定好的信号处理函数选择一个工作线程来善后处理,比如决定是否关闭 socket 。

	3.模拟 Proactor 模式
		使用同步IO 模拟 Proactor 。原理:主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一"完成事件"。
		那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的就是对读写的结果进行逻辑操作。

		1.主线程往epoll内核事件表中注册 socket 上的读就绪事件
		2.主线程调用 epoll_wait 等待 socket 上有数据可读
		3.当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,
		然后将读到的数据封装成一个请求对象并插入请求队列
		4.睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件
		5.主线程调用 epoll_wait 等待 socket 可写
		6.当 socket 可写时,epoll_wait 通知主线程,主线程往 socket 上写入服务器处理客户请求的结果
5.两种高效的并发模式
	并发编程主要有: 多进程和多线程
	
	并发编程模式有:
	1.半同步/半异步
		在IO编程中,"同步"和"异步"区分的是内核向应用程序通知的是何种IO事件(是就绪事件还是完成事件),
		以及谁来完成IO读写(是应用程序还是内核).
		在并发模式中,"同步"指的是程序完全按照代码序列的顺序执行;
		"异步"指的是程序的执行需要由系统事件来驱动,常见的系统事件包括中断,信号等;

		同步运行的线程叫同步线程;
		异步方式运行的线程叫异步线程;
		同时采用同步线程和异步线程来实现,即采用半同步/半异步模式来实现;这当中,同步用来实现客户逻辑,
		异步线程用来处理IO事件。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列。请求队列将通知
		某个工作在同步模式的工作线程来读取并处理该请求对象。

		其中有个变体叫半同步/半反应堆 模式:
		异步线程只有一个,由主线程来充当。它负责监听所有socket的事件。
		如果监听 socket 上有可读事件发生,即有新的连接到来,主线程就接受之以得到新的连接socket,
		然后往epoll内核事件表中注册该 socket 上的读写事件;
		如果连接 socket 上有读写事件发生,即有新的客户请求到来或者有数据要发送至客户端,主线程就将该 连接socket
		插入请求队列中。所有工作线程都睡眠在请求队列中,当有任务到来,它们通过竞争获取任务的接管权。

		半同步/半反应堆采用的事件处理模式是 Reactor 模式:它要求工作线程自己从 socket 读取客户请求和往socket 
		写入服务器应答。也可以模拟Proactor 事件处理模型,即由主线程完成数据的读写。

		缺点:
			1.主线程和工作线程共享请求队列。主线程往请求队列添加任务,或者工作线程从请求队列取出任务,都需要对请求队列
			加锁保护,从而白白浪费 cpu 时间。
			2.每个工作线程在同一时间之内处理一个客户请求。客户数量较多,工作线程较少,则请求队列中将堆积很多任务对象。
			如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量 cpu 时间。

		高效的半同步/半反应堆模式: 在工作线程中也维护	自己的事件循环。



	2.领导者/追随者
		领导者和追随者是多个工作线程轮流获得事件源集合,轮流监听,分发并处理事件的一种模式。
		在任意时间点,程序仅有一个领导者,它负责监听IO事件。而其他线程都是它的追随者,它们休眠在线程池中等待
		成为新的领导者。当期领导者如果检测到IO事件,首先要从线程池中推选出现的领导者线程,然后处理IO事件。此时,
		新的领导线程等待新的IO事件,而原来的领导者则处理IO事件,二者实现了并发。

		领导者/追随者模式包括的组件:句柄集,	线程集,事件处理器和具体的事件处理器。
		
		句柄集:
		  表示IO资源,使用 wait_for_event 方法监听这些句柄上的IO事件。将其中的就绪事件,
		通知给领导线程。领导者则调用绑定到 Handle 上的事件处理器来处理事件。领导者将 Handle 和 事件
		处理器绑定是通过调用句柄集中的register_handle 实现的。

		线程集:
		  这个组件是所有工作线程(包括领导线程和追随者线程)的管理者。它负责各个线程之间的同步,以及新领导线程的推选。
		线程集中的线程在任一时间必须处于如下三种状态之一:
		  Leader : 线程当前处于领导者身份,	负责等待句柄集上的IO事件;		
		  Processing : 线程正在处理事件。领导者检测到IO事件后,可以转移到 Processing 状态来处理该事件,
		  			   并调用promote_new_leader方法推选新的领导者;也可以指定其他追随者来处理事件,此时领导者地位不变。
		  			   当处于Processing 状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则
		  			   它就直接转变为追随者。
		  Follower : 线程当前处于追随者身份,通过调用线程集的 join 方法等待成为新的领导者,也可以被当前的领导者指定来处理新任务。

		事件处理器和具体的事件处理器:
			  事件处理器通常包含一个或多个回调函数 handle_event。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到
			某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。具体的事件处理器是事件处理器的派生类。
			它们必须重新实现基类的 handle_event 方法,以处理特定任务。
6.有限机状态
	逻辑单元内部的一种高效编程方式。

7.提高服务器性能的其他建议
	1.池
		一组资源的集合。这组资在服务器启动之初就完全被创建并初始化。分配系统资源的系统调用很耗时。
		避免了对内核的频繁访问。
		内存池 : socket 的接收缓存和发送缓存
		进程池
		线程池
		连接池 : 常用于服务器或服务器集群的内部永久连接。

	2.数据复制
		高性能服务器应该避免不必要的数据复制。尤其是当数据复制发送在用户代码和内核之间的时候。
		如果内核可以直接处理从 socket 或者文件读入的数据,则应用程序就没必要将这些数据从内核
		缓冲区复制到应用程序缓冲区。这里说的"直接处理"指的是应用程序不关心这些数据的内容,不需要对
		它们做任何分析。
		可以使用 "零拷贝" sendfile 函数.
		此外,用户代码内部(不访问内核)的数据复制也应该避免。举例,当2个工作进程之间要传递大量的数据时,
		我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递。

	3.上下文切换
		并发程序必须考虑上下文切换的问题,即进程切换或线程切换导致的系统开销。即使是 IO密集型的服务器,也
		不应该使用过多的工作线程。否则线程间的切换将占用大量的cpu时间,服务器真正用于处理业务逻辑的cpu时间的比重
		就显得不足了。因此,每个客户连接创建一个工作线程的服务器模型是不可取的。

		半同步/半异步模式是一种比较合理的方案,它允许一个线程同时处理多个客户连接。
		此外,多线程服务器的一个优点是不同的线程可以同时运行在不同的cpu上,当线程的数量不大于cpu的数目时,
		上下文切换就不是问题了。

	4.锁
		并发程序需要考虑的另外一个问题就是对共享资源的加锁保护。锁通常被认为是导致服务器效率低下的一个因素。
		因为由它引入的代码,不仅不处理任何业务逻辑,而且需要访问内核资源。
		显然,半同步/半异步模式就比 半同步/半反应堆的效率高。
		如果服务器必须使用锁,可以考虑减小锁的粒度。







1.服务器模型



8.2 服务器编程框架



8.3 I/O 模型



8.4 两种高效的事件处理模型

Reactor 模式:



Proactor 模式:






8.5 两种高效的并发模式





8.6 有限机状态




8.7 提高服务器性能的其他意见



猜你喜欢

转载自blog.csdn.net/enlyhua/article/details/81024206