认识事件驱动

针对什么代码做事件驱动

什么代码值得基于事件做拆分?目标是为了高性能,服务里对资源操作速度由快到慢:CPU > 内存 > 磁盘 > 网络。由于CPU和内存都是纳秒级,故只有磁盘和网络可以考虑采用事件驱动的异步方式处理。机械硬盘最慢也就几十毫秒,属于可控,而网络速度慢波动大,既受制于连接对端性能,也受制于网络传输路径。故,一般事件驱动,都指网络事件

多路复用

基本概念

一个进程任一时刻虽然只能处理一个请求,但如果处理单个请求产生的事件控制在1毫秒内,那么1秒就可以处理上千个请求。从更长的时间维度看,多个请求复用了一个进程,就叫多路复用(时分多路复用)。

Linux内核多路复用接口

select接口/poll接口/epoll接口

select接口

获取事件时,将所有并发连接传给内核,再由内核返回产生了事件的连接,最后处理这些连接对应的请求。

epoll接口

像select这样,会有频繁的用户态到内核态数据拷贝到消耗。C10M 意味着有一千万个连接,若每个 socket 是 4 字节(unsigned int),那么 1 千万连接就是 40M 字节。这样,每收集一次事件,就需要从用户态复制 40M 字节到内核态。而且,高性能 Server 必须及时地处理网络事件,所以每隔几十毫秒就要收集一次事件,性能消耗巨大。

epoll为了降低性能消耗,把获取事件拆分成了两步。

  1. 把需要监控的socket传给内核(epoll_ctl函数),仅在连接建立等有限的时机调用传递;
  2. 由于内核已管理了socket,故收集事件不再需要传递socket。

这样便只有一次socket复制。

获取到了产生事件的socket后,如何处理?

处理事件的代码分为三类来看。

  1. 计算任务,虽然内存、CPU 的速度很快,然而循环执行也可能耗时达到秒级。所以,如果一定要引入需要密集计算才能完成的请求,为了不阻碍其他事件的处理,要么把这样的请求放在独立的线程中完成,要么把请求的处理过程拆分成多段,确保每段能够快速执行完,同时每段执行完都要均等地处理其他事件,这样通过放慢该请求的处理时间,就保障了其他请求的及时处理。

  2. 读写磁盘,由于磁盘的写入操作使用了 PageCache 的延迟写特性,当 write 函数返回时只是复制到了内存中,所以写入操作很快。磁盘的读取操作就比较慢了,这时,通常要把大文件的读取,拆分成许多份,每份仅有几十 KB,降低单次操作的耗时。

  3. 通过网络访问上游服务。与处理客户端请求相似,我们必须使用非阻塞 socket,用事件驱动方式处理请求。需要注意的是,许多网络服务提供的 SDK,都是基于阻塞 socket 实现的,使用前必须先做完非阻塞改造。比如 Memcached 的官方 SDK 是用阻塞 socket 实现的,Nginx 如果直接使用该 SDK 访问它,性能就会一落千丈。正确的访问方式,是使用第三方提供的ngx_http_memcached_module 模块,它用非阻塞 socket 重新封装了 SDK。

总之,网络报文到达后,内核就产生了读、写事件,而 epoll 函数使得进程可以高效地收集到这些事件。接下来,要确保在进程中处理每个事件的时间足够短,才能及时地处理所有请求,这个过程中既要避免阻塞 socket 的使用,也要把耗时过长的操作拆成多份执行。最终,通过快速、及时、均等地执行所有事件,异步 Server 实现了高并发。

posted @ 2023-03-01 23:19  kiper  阅读(36)  评论(0编辑  收藏  举报