基于Linux高性能并发服务器要点

172 阅读11分钟

我做的项目是基于Linux的轻量级的服务器,应用层实现了一个使用I/O多路复用,同时监听多个请求,使用线程池进行处理请求,使用模拟的Proactor模式,主线程负责监听,监听到有事件后,从socket中循环读取数据,将读取到的数据封装成任务对象放入请求队列。睡眠在请求队列上的工作线程被唤醒,使用状态机进行HTTP的请求处理。并最终使用webbench对服务器进行测试。可以实现上万的并发量。

我认为这个项目存在的难点是:

怎么样提高服务器的并发能力—

多线程并发的情况下,怎么样保证线程安全—

用户是怎么进行对服务器的访问呢?

  1. 解析域名,获得主机IP

  2. 三次握手建立TCP连接。浏览器会发以一个随机端口向服务端的web程序80端口发起TCP连接

  3. 建立TCP连接后,浏览器想服务器发送HTTP请求

  4. 服务器响应请求,返回请求的数据

  5. 渲染解析数据,呈现给用户

如何接受客户端发来的HTTP请求报文呢?

‘‘’事件驱动!‘’‘

当浏览器发送出http连接请求时候,主线程船舰http类的对象数组用来接收请求并将所有的请求读入各个对象的buffer中,然后将该对象插入任务队列。

具体讲呢就是,通过内核事件表,若是连接请求,那么就将他注册到内核事件表中,线程池中的任务队列就从中取出一个任务进行处理。

I/O多路复用

select、poll、epoll的区别

select通过线性表线性扫描文件描述符,且最大的文件描述符数量为一般为1024。

poll是链表描述的文件描述符

select、poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝

select、poll线性遍历,随着文件描述符的增加遍历速度会下降

select、poll只返回发生事件的文件描述符的个数,具体哪个事件还需要遍历

epoll是红黑树描述的文件描述符

epoll通过epoll_create()建立一棵红黑树,通过epoll_wait()将要监听的文件描述符注册到红黑树上

epoll因为epoll内核中实现是根据贝格文件描述符的回调函数来实现的,只有活跃可用的FD才会调用callback函数。

epoll返回的是发生事件的个数和结构体数组,结构体包含socket信息

同时epoll还具有高效的ET模式,并且还可以在socket连接在任意时刻都只被一个线程处理的epolloneshot事件。

LT水平触发模式和ET电平触发模式:

LT:epollwait检测到文件描述符上有事件发生,则将其通知给应用程序,直到用户把数据读取完才会停止通知。不做任何操作的话内核是会持续通知的

ET:只支持非阻塞socket,避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。epollwait检测到文件描述符上有事件发生,则将其通知给应用程序,用户若不读取数据,数据一直在缓冲区内,epoll下次检测的时候就不通知了。需要一次性将数据读取完。

事件接受处理模式

在处理请求的同时,还需要继续监听其他客户端的请求并分配其逻辑单元进行处理

使用epoll这种I/O多路复用技术来实现对监听socket(listenfd)和连接socekt(客户请求)的同时监听。

虽然i/o多路复用可以同时监听多个文件描述符,但是它本身是阻塞的,如果多个文件描述符同时就绪的话,程序就按照顺序处理。

故在此,可以使用多线程并发,实现为每个就绪的fd进行分配逻辑处理单元

两种事件并发处理模式:reactor 和 proactor

Reactor模式是主线程负责监听文件描述符上是否有事件发生,若有,则通知工作线程将socket可读写事件放入请求队列,交给工作线程来处理

Proactor模式是主线程负责执行I/O读写操作,处理完成后选择一个工作线程来处理客户请求。工作线程仅处理业务逻辑。

读就绪事件:当有事件到来,epoll_wait()单纯通知主线程有事件来了,主线程把事件放入请求队列。应用程序利用工作线程通过read()等函数把数据从内核缓冲区读到用户缓冲区。

读完成事件:有事件来了,主线程往内核注册这个读时间(就是告诉内核注意了一会要读数据)。注册了之后,主线程就去干其他事情,内核就自动会负责将数据从内核缓冲区放到用户缓冲区。不用用户程序管。

而对于用reactor模式模拟的的proactor模式来说,之前proactor是用主线程调用aio_read函数向内核注册读事件,这里它主线程使用epoll向内核注册读事件。但是这里内核不会负责将数据从内核读到用户缓冲区,最后还是要靠主线程也就是用户程序read()函数等负责将内核数据循环读到用户缓冲区。对于工作线程来说,收到的都是已读完成的数据,模拟就体现在这里。

有人可能会问,他们都是通过主线程调用不同函数进行注册,然后一个注册之后可以直接内核负责数据从内核到用户。另一个注册之后好像没啥用,那注册还有什么用?直接主线程循环读取然后封装放请求队列不就行了么?

不对,如果数据一直没来,直接进行循环读取就会持续在这里发生阻塞,这就是同步IO的特点,所以一定要注册一下然后等通知,这样就可以避免长期阻塞等候数据。

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

使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下 :

  1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读。3. 当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。

\4. 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件。

  1. 主线程调用 epoll_wait 等待 socket 可写。
  2. 当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。 如何处理HTTP请求报文

线程池中的并发处理模式

半同步 半异步 半同步半反应堆

半同步半反应堆模式,是主线程充当异步程序,只负责监听客户端请求以及向内核注册读写事件,和reactor模式类似。


什么是同步/异步

同步是指用户线程发起I/O请求后.需要等待或者轮询内核I/O操作完成后才能继续执行;

异步是指用户线程发起I/O请求后仍继续执行,当内核I/O操作完成后会通知用户线程或者调用用户线程注册的回调函数。


同步就是程序按顺序执行,否则就阻塞等待

异步是程序的执行有系统事件驱动


半同步半反应堆工作流程

主线程充当异步程序,监听所有socket的事件

若有新请求到来,主线程接受得到新的连接socket,然后往epoll内核事件表注册该socket上的读写事件

若socket上有读写事件,那么主线程从socket上接受数据,并将数据封装成任务对象插入请求队列

所有工作线程睡眠在请求队列上,有任务到来的时候,通过竞争获得任务的处理权

该项目使用半同步半反应堆工作流程,主线程负责读写,工作线程负责处理业务逻辑,处理请求

线程池线程数量的选择:

最佳线程数 = cpu当前可使用核心数 * cpu当前利用率 *(1+cpu等待时间/cpu处理时间)

线程池的工作线程一直是等待的嘛?

是的,是一直处于阻塞状态下的。在run函数中,为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞在请求队列不为空的条件上。

线程池工作线程处理完一个任务后的状态是什么?

这里要分两种情况考虑

(1)当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态

(2)当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁准就获得了处理事件的资格。

如果同时有1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

本项目是通过对子线程循环调用来解决高并发的问题的。

首先在创建线程的同时就调用了 pthread_detach 将线程进行分离,不用单独对工作线程进行回收,资源自动回收;

我们通过子线程的run调用函数进行while循环,让每一个线程池中的线程永远都不会停,访问请求被封装到请求队列( list )中,如果没有任务线程就一直阻塞等待,有任务线程就抢占式进行处理,直到请求队列为空,表示任务全部处理完成。

如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?

会影响接下来的客户请求,因为线程池内线程的数量是有限的,如果客户请求占用线程时间对久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。

应对策略:

我们可以为线程处理请求对象设置处理超时时间,超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。

Webbench是什么,介绍一下原理

父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。

介绍一下生产者消费者?

生产者和消费者主要用于对于数据的同步使用,生产者生产数据,然后放到共享缓冲区中,消费者在缓冲区没有数据之前会阻塞等待,当生产者生产数据之后,会用signal函数唤醒阻塞,开始消费数据,而当数据生产充满缓冲区之后,生产者就会阻塞等待。其中的阻塞都使用条件变量。