我是靠谱客的博主 迷路灯泡,最近开发中收集的这篇文章主要介绍高性能服务器八股,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

    • 高性能服务器程序框架
      • 服务器模型
        • C/S模型
        • P2P模型
      • 服务器编程框架
      • IO模型
        • 阻塞IO
        • 非阻塞IO
        • IO复用
        • SIGIO信号
        • 异步IO
      • 两种高效的事件处理模式
        • Reactor
        • Proactor
        • 模拟Proactor
      • 两种高效的并发模式
        • 半同步/半异步
        • 领导者/追随者
      • 提高服务器性能的其他建议
        • 数据拷贝
        • 上下文切换和锁

高性能服务器程序框架

服务器模型

C/S模型

所有客户端都通过访问服务器来获取所需资源。

缺点:服务器是通信中心,访问量过大时,可能所有客户端都将得到很慢的相应。

P2P模型

点对点模型摒弃了以服务为中心的格局,让网络上所有主机重新回归对等的地位。

但缺点也很明显:当用户之间传输的请求过多时,网络负载将加重。而用户主机很难发现彼此,故常带一个发现服务器。

服务器编程框架

  • IO处理单元:处理客户端连接,读写网络数据。

    • 等待并接受
      新的客户端连接,接受客户数据将服务器响应数据返回给客户端。但是数据的收发不一定是在IO处理单元,也可能在逻辑单元(取决于事件处理模式)
    • 对服务器集群来说,IO处理单元是专门的接入服务器,实现负载均衡,选取载荷最小的给客户服务。
  • 逻辑单元:业务进程或线程。

    • 通常是一个进程或线程,分析并处理数据,并将结果传递给IO处理单元或直接发给客户端
    • 对服务器集群来说,逻辑单元本身就是一台服务器,以实现对多客户的任务的并行处理。
  • 网络存储单元:本地数据库,文件或缓存。

  • 请求队列:各单元通信方式。

    请求队列:各单元之间的通信方式。

    • 请求队列通常被实现为池的一部分。

IO模型

阻塞IO

针对阻塞IO执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的时间发生为止。

非阻塞IO

针对非阻塞IO执行的系统调用总是立即返回,而不管时间是否已经发生。如果事件没有发生,那么返回-1和出错一样,此时需要通过errno来区分。如对send/recv,errno设置为EAGAIN/EWOULDBLOCK

很显然,只有在事件已经发生时操作非阻塞IO,才能提高程序效率。因此常和IO复用和SIGIO信号一起使用。

IO复用

应用程序通过IO复用函数向内核注册一组事件,内核通过IO复用函数把其中就绪的事件通知给应用程序。常用的IO复用函数是select, poll, epoll

SIGIO信号

后续讨论

异步IO

同步IO中,IO的读写都在IO事件发生之后,由应用程序完成。而异步IO中,用户可以直接对IO读写,总是立即返回,而不论IO是否阻塞,因为真正的IO读写由内核执行。

即同步IO要求用户自行执行IO(将数据从内核缓冲区读入用户缓冲区,或反之),而异步IO则由内核来执行(内核缓冲区和用户缓冲区之间的数据以移动由内核在“后台”完成)

同步IO向应用程序通知的是IO就绪事件,而异步IO向应用程序通知的是IO完成事件。

两种高效的事件处理模式

服务器通常需要处理三类事件:IO事件、信号、定时事件

随网络设计模式兴起,Reactor和Proactor模式应运而生,同步IO通常用于实现Reactor,异步IO用于实现Proactor,但同步IO还可以模拟Proactor。

Reactor

要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生,有就通知工作线程(逻辑单元)接管,除此之外不做任何其他实质性的工作。读写、接受新连接、处理客户请求均在工作线程中完成。

主线程中的epoll_wait既用来检测监听socket上的连接请求,亦监听socket读写事件

  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上写入服务器处理客户请求的结果。

Proactor

和Reactor不同,Proactor将所有IO操作交给主线程和内核处理,工作线程仅仅负责业务逻辑。

连接socket上的读写事件通过aio_read/aio_write向内核注册。

主线程中的epoll_wait仅用来检测监听socket上的连接请求。

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

模拟Proactor

Alexander Libman, Vladimir Gilbourd. Comparing Two High-Performance I/O Design Patterns. 2005

上述参考文献提到使用同步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上写入服务器处理客户端请求的结果。

两种高效的并发模式

并发编程的目的是让程序“同时”执行多个任务。如果程序是计算密集型,并发编程并没有优势;如果是IO密集型,如经常读写文件、访问数据库等,由于IO速度远没有CPU速度快,所以让程序阻塞于IO将浪费大量CPU时间。如果当前线程被IO阻塞,可主动放弃CPU(或操作系统调度),并将执行权交移到其他线程。这样CPU利用率上升。

并发模式是指IO处理单元和多个逻辑单元之间协调完成任务的方法。

半同步/半异步

此处同步异步概念不同于IO的同步异步。

IO模型中同步异步的区别是1. 内核向应用程序通知的是何种事件?就绪or完成。 2. 由谁来完成IO读写?应用程序or内核。

并发模式中的同步异步:同步是指程序完全按照代码序列顺序执行;异步是程序的执行需要由系统事件来驱动(常见的系统驱动:中断、信号)

同步线程逻辑简单,实时性较差;异步线程逻辑复杂但实时性强;对于服务器就采用半同步/半异步模式来实现。

半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理IO事件。当异步线程监听到客户请求后,就将其封装成请求对象插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体通知哪个工作线程则取决于请求队列的设计,如轮询算法。

领导者/追随者

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

提高服务器性能的其他建议

以空间换时间,因为分配系统资源的系统调用都是很耗时的。这会造成“浪费“,但对服务器来说不会构成问题。

  • 内存池:对某些长度有限的请求,如HTTP,预分配一个5000字节的缓冲区是合理的,当客户请求长度大于接收缓冲区时再动态扩大。
  • 进程池/线程池:并发编程的惯用伎俩,当需要一个工作线程时,直接从池子里取。
  • 连接池:发起连接再释放连接很费时,当某个逻辑单元需要访问数据库时,直接从连接池中取一个连接的实体,使用完再放回池子里。

数据拷贝

数据复制发生在用户区和内核区之间的时候很费时,如果用户程序不关心这些数据的内容时(如ftp服务器),这样无需把目标文件拷贝到用户态缓冲区再send发送,而直接使用“零拷贝” sendfile。

此外用户态之间数据传输时也应该避免数据拷贝,如两进程间要传递大量数据时,应使用共享内存而非用管道、消息队列发送数据。

上下文切换和锁

即使是IO密集型服务器,也不应该使用过多的工作线程,否则线程间切换的开销将占用大量CPU时间。因此为每个客户连接都创建一个工作线程的服务器模型是不合理的(当线程数量不大于CPU数目时,上下文切换就不成问题),半同步/半异步模式就比较合理,它允许一个线程同时处理多个客户连接。

另一个问题是锁,尽量减少锁的使用。如果必须使用,可以考虑减少锁的粒度,如读写锁。当所有工作线程都只读取一块共享内存时,读写锁并不会增加系统的额外开销,只有当一个线程要写这块内存时才上锁。

最后

以上就是迷路灯泡为你收集整理的高性能服务器八股的全部内容,希望文章能够帮你解决高性能服务器八股所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(67)

评论列表共有 0 条评论

立即
投稿
返回
顶部