对于后端服务器,框架是怎样的?处理事务的逻辑是怎样的?你了解多少?
1、服务器基本结构
服务器基本的架构如下:
图中的各个单元在单一服务器和集群中育有类似的功能:
文章相关视频讲解: c/c++Linux后台服务器开发高级架构师学习视频
PS:视频相关学习文档,点击获取
2、服务器的事件处理模式
服务器需要处理三类事件:IO 事件、信号处理、定时事件。
相应的处理模式和设计模式一样被广泛使用
Reactor 反应器模式:用于同步 IO 模型
Proactor 主动器模式:用于异步 IO 模型两个模式可以这样简单的理解:家里有两个佣人, 一个叫 reactor, 另一个叫 proactor。1.每天早午晚的饭点,reactor 都会提醒你该去做饭了。2.你只要告诉 proactor,家里的饭桌在哪里,proactor 就会在会在每天的饭点做好饭,端到你指定的饭桌上,然后通知你,可以吃饭了。
2.1 Reactor 模式
Reactor 模式要求主 IO 处理线程只负责监听文件描述符或 socket 上是否有事件发生,如果有立刻通知逻辑单元进行读写数据等指定的操作。IO 处理线程不做其他工作,只监听指定事件。Reactor 模式的工作流程(以 epoll_wait IO 复用函数为例)如下:
Reactor 模式的优点有:
实现相对简单,对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离,将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来。
Reactor 模式的缺点有:
Reactor 处理耗时长的操作(如文件 I/O)会造成事件分发的阻塞,影响到后续事件的处理。这种情况使用异步 IO 更好。
2.2 Proactor 模式
Proactor 模式将所有的 IO 操作都交给主线程和内核处理,包括监听和数据读写。工作线程仅负责业务逻辑,不关系 IO 操作何时进行和如何进行。下图是 Proactor 模式的工作流程(以异步 IO 函数 aio_read 和 aio_write 函数为例):
Proactor 模式的优点是:
Proactor 性能更高,能够处理耗时长的并发场景
Proactor 模式的缺点有:
Proactor 实现逻辑复杂;
依赖操作系统对异步的支持
目前实现了纯异步操作的操作系统少,实现优秀的如 windows IOCP,但由于其 windows 系统用于服务器的局限性,目前应用范围较小
Unix/Linux 系统对纯异步的支持有限,应用事件驱动的主流还是通过 select/epoll 来实现;
文章福利 Linux 后端开发网络底层原理知识学习提升 点击学习资料获取,完善技术栈,内容知识点包括 Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux 内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK 等等。
3、两种高效的并发模式
我们知道,多进程和多线程是并发编程的两种方法,这里先讲并发模式,强调 IO 处理单元和逻辑单元间如何协调。
3.1 半同步/半异步模式
并发中同步是指按照代码顺序依次执行;异步是指通过中断、信号等系统事件跳跃执行代码。图示如下:
半同步半异步模式中:
同步线程负责处理客户逻辑,业务逻辑一般是顺序执行的(逻辑单元)
异步线程负责处理 IO 事件,IO 操作可能耗时阻塞,需要和业务逻辑分隔开(IO 处理单元)。异步线程通过消息请求通知同步线程处理新来的事件
半同步半异步并发模式 + Reactor 事件处理模式可以得到一个高效的服务器处理模式:
3.2 领导者/追随者模式
任意时刻,程序只有一个领导者线程,负责监听 IO 事件;其他线程作为追随者,在线程池中休眠等待成为领导者。领导者线程获得 IO 事件后,先选出新的领导者,然后处理 IO 事件,这样新的领导者监听 IO 事件,原来的领导者处理 IO 事件,之后变为追随者休眠。
领导者模式的状态变化图如下:
该模式的工作流程如下:
领导者线程自己监听并处理事件请求,所以不需要线程之间通信,也不需要请求队列。但缺点是仅支持 handleSet 中已有的事件,不能灵活的让工作线程独立管理其他事件。
4、提高性能的其他建议
4.1 池 pool
提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。
这就是池(pool)的概念。池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。
当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无须动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。
当服务器处理完一个客户连接后,可以把相关的资源放回池中,无须执行系统调用来释放资源。从最终的效果来看,池相当于服务器管理系统资源的应用层设施,它避免了服务器对内核的频繁访问。池可以分为多种形式:
内存池:用于 socket 的接收和发送缓存,也可以用于模块内部内存申请释放
进程池和线程池:并发编程常用方法,避免了动态创建进程和线程的过程消耗
连接池:用于服务器集群内部或与数据库之间的永久连接,用于频繁访问某个数据库或服务器,避免频繁创建关闭连接。
4.2 避免不必要的数据复制
高性能服务器应该避免不必要的数据复制,尤其是当数据复制发生在用户代码和内核之间的时候。
如果内核可以直接处理从 socket 或者文件读入的数据,则应用程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区。比如 ftp 服务器,当客户请求一个文件时,服务器只需要检测日标文件是否存在,以及客户是否有读取它的权限,而绝对不会关心文件的具体内容。
这样的话,ftp 服务器就无须把日标文件的内容完整地读人到应用程序缓冲区中并调用 send 函数来发送,而是可以使用“零拷贝”函数 sendfile 来直接将其发送给客户端。用户代码内部(不访问内核)的数据复制也应该避免。
举例来说,当两个进程之间要传递大量的数据时,我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递,因为这样既浪费空间,又效率低下。
4.3 考虑上下文切换和锁
并发程序必须考虑上下文切换(context switch)的问题,即进程切换或线程切换导致的的系统开销。即使是 IO 密集型的服务器,也不应该使用过多的工作线程,否则线程间的切换将占用大最的 CPU 时问,服务器真正用于处理业务逻辑的 CPU 时间的比重就显得不足了。
如果有更好的解决方案(比如半同步半异步模式),尽量避免使用锁。如果一定要使用锁,要考虑减小锁的粒度,做到影响最小。
评论