经典高并发服务器设计逻辑

news2024/11/28 10:35:21

都是面试必问的八股,不管理不理解用不用得上,背就完事了。

服务器模型

对于并发量比较大的服务器,即listen监听端口一直忙碌于处理新建连接的场景,一般在主线程里面accept新的客户端连接并生成新连接的socket,然后将这些新连接的socket按既定规则(如轮询、哈希等)传递给工作线程池;每个工作线程各自持有一个epollfd,管理主线程分配过来的socket事件,由这些工作线程处理新连接上的网络IO事件(即收发数据+处理数据)。此外,工作线程还处理系统中的另外一些事务,如定时器等。

总体来说就是1+N+M模型,一个主线程专门用于处理新建连接事件,N个工作线程处理网络消息,M个线程做数据加工(如操作数据库等)。

30d9cc40a461ef25009d5e161da1521f.png

经典服务器的优点在于:

主线程只关注处理新连接,不用处理网络IO事件,可以尽快响应新建客户端请求。主线程接受的新连接(每个连接对应一个socket fd)可以根据配置的负载均衡策略分配给各个工作线程。主线程记录各个工作线程上在线的socket fd数量,平衡服务器资源。此外还可以在工作线程做其他的处理逻辑。

如果不是高并发类的业务场景,listenfd所在的主线程比较空闲,可以不让listenfd独占一个线程,即主线程也参与clientfd的读写事件处理。新版本redis的多线程实现方式就是这样的。

工作线程设计逻辑

每个工作线程函数里面有一个循环流程,这些循环流程里面做的都是相同的事情。这个线程函数的内容细节如下:

//工作线程
void thread_func(void* thread_arg)
{
//这初始化资源
  while (没有接收到主线程发送过来的要求工作线程退出的信号)
  {
    //检查定时器,定时器任务一般放在最开始处理,保证时间间隔更精确
    check_and_handle_timers();
    //调用select/poll/epoll等多路复用接口,分离出读写事件
    epoll_or_select_func();
    //处理读事件或写事件
    handle_io_events();
    //其他的事情放最后做
    handle_other_things();
  }
//执行线程退出的资源清理工作
}

一些额外的工作如定时任务、信号处理任务等可以放到 handle_other_thing() 中处理,这些事件采用特殊的唤醒策略。如管道fd、enevtfd、socketpair。为什么epoll能管理这些异构的fd呢?在设备驱动层面实现了file_operations.poll方法的fd可以交给epoll管理,包括:socket、eventfd、timerfd。相反,反而是文件系统fd没有实现file_operations.poll方法,无法由epoll管理。

业务处理

void handle_io_events()
{
  //收发数据
  recv_or_send_data();
  //解包并处理数据
  decode_packages_and_process();
}

如果解析包及处理数据比较耗时,需要将业务处理逻辑单独拆出来交给另外的业务工作线程池处理。

发消息逻辑设计

处理后的数据需要从业务线程交给网络线程发送。

1、最简单的办法就是直接调用相应的发数据的接口,此时可能出现多个线程同时调用该socket的send函数。一个socket描述符如果在多个线程之间共享,就会出现竞态条件。为了保证数据正确性,需要对该socket发送加,确保同一时刻只有一个线程调用socket send方法。相当于业务线程做了网络线程的事。

2、业务线程将需要发送的数据放入共享区域,由定时器定时从共享区域取出来,再发送出去。缺点是存在延迟。

3、使用pipe等通知机制,唤醒epoll,在工作线程循环体的handle_other_things()中完成处理发送工作。这里发送数据也可以使用缓冲区缓存。

如果业务不耗时,可以直接放在网络IO工作线程中处理,直接在网络线程中发送。

可写事件与可读事件不太一样,有新连接到来时可以立即设置可读,但是只有有数据需要发送时才设置可写。所以在数据都发出去以后,要移除可写事件。

缓冲区设计

每一个socket连接都需要配置各自的缓冲区,业务层的缓冲区分为读缓冲区和写缓冲区,缓存的本质就是空间换时间。接收缓冲区主要用于HTTP等协议解析,写缓冲区用于业务层可持续追加发送数据。

如何设置大小呢?可以参考redis的设置,读缓冲限制大小,防止客户端乱搞。写缓冲不设置大小,使用链表或者vector自动适配,毕竟是自己生成的数据。

缓冲区内存的有效时间必须比I/O操作的时间要长,实现方式为:连接类需要继承自enabled_shared_from_this,然后在内部保存它需要的缓冲区,而且每次异步调用都要传递一个智能指针(shared_from_this())给this操作。

流量控制

使用流量统计可以对所有客户端或者某一个客户端做一个限速。

当客户端连接数目比较多的时候,服务器在处理网络数据的时候,如果同时有多个socket上有数据要处理,由于cpu核数有限,根据上面先检测iO事件再处理IO事件可能会出现工作线程一直处理前几个socket的事件,直到前几个socket处理完毕后再处理后面几个socket的数据。

对于epollwait返回的客户端可读事件,对客户端进行流量统计,如果同时触发的可读事件超过某个阈值(按CPU核数设置),且某个客户端流量超过阈值,那么该轮循环不处理这个客户端的读事件。同时启动一个100ms的定时器,100ms后清除该客户端的流量统计值,该客户端又可以被服务器处理了。该项优化不适用于某些要求强顺序性的业务如游戏对战等业务场景。

异步connect实现方式

一般业务场景是不会调用connect接口的,但是代理服务器等场景会用到。为了让多路复用IO支持异步connect事件,流程如下:

1.创建socket,并将socket设置成非阻塞模式;

2.调用connect函数,此时无论是否连接成功都会立即返回;如果返回-1且错误码是EINPROGRESS,说明在连接中;

3. 接着调用select/epoll函数,在指定的时间内判断该socket是否可写,如果可写说明连接成功,反之则为连接失败。

setsockopt、bind、accept等接口的最后一个参数socklen_t既是入参又是出参,需要根据该参数来决定读取的用户内存区长度,因此传入的值需要初始化。这一点容易忽视。

Writev加速写的逻辑

也就是IO向量机制。

write操作的是连续内存块,writev操作的是分散的数据块,两个函数的最终操作结果都是将内容写入连续的空间。writev的固有开销比write大,因此对于小内存的写而言,很可能也没有copy+write高效。

wirte返回值处理

write经常不能够一次写完,此时会返回已经写了多少字节,如果业务继续写,此时就会阻塞;对于非阻塞socket而言,write会在buf不可写时返回的EAGAIN,那么在下一次write时,便可通过之前返回的值重新确定基址和长度。

writev也会返回已经写入的长度或者EAGAIN(errno)。此时writev并不是每次传同样的iovec就能解决问题,需要调用者重新处理iovec,即需要通过遍历iovec来计算新的基址。

writev适用于磁盘IO,对于写socket,尤其是非阻塞socket,尽量不要用writev,实现连续的内存块反而可以简化实现。

信号创建

在handle_other_thing()中需要对一些信号进行响应。采用signation,而不是signal调用来设置信号处理函数,因为signal调用会有一些未解决的已知问题。此外在使用signation设置信号处理函数时,可以设置SA_RESTART来自动恢复被信号中断的系统调用。如果不设置自动恢复,在有信号时read/write会返回-1且errno被设置为 EINTR,表示被信号打断,需要手动重新调用。

SIGPIPE

对一个已经收到FIN包的socket调用recv方法,如果接收缓冲已空,则返回0,表明连接关闭。
对一个已经收到FIN包的socket第一次调用send方法时,如果发送缓冲区未阻塞,则send调用会返回写入的数据量,同时进行数据发送。但是发送出去的报文会触发对端发回RST报文,因为对端的socket已经调用了close进行了完全关闭(不然本端不会收到FIN报文)。所以第二次调用send方法时(需要在收到RST之后)会触发SIGPIPE信号,这就是为什么第二次send才能触发 SIGPIPE的原因。

可以使用signation对SIGPIPE信号进行捕获,这样当第二次调用write方法时,会返回 -1,同时 errno错误码会被设置成 EPIPE,而不是直接杀死进程。

NONBLOCK

为避免返回值0具有二义性,对一个非阻塞的描述符如果无数据可读,则read返回-1,而且errno被设置为 EAGAIN。返回0,表示接收到对端的FIN,即对端写关闭。

Unix域协议

Unix域协议并不是一个网络协议族,而是在单个主机上执行客户/服务通信的一种方式。是进程间通信(IPC)的一种方式。
它提供了两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP,但是是可靠的)。
UNIX域数据报服务是可靠的,不会丢失消息,也不会传递出错。(如何实现可靠的?)

8962fd5e336c3fb641db7d3139c5588e.png

线程安全

1、当一个进程正在阻塞在epoll_wait的时候,另一个线程调用epoll_ctl是否安全?eventpoll中有mutex互斥锁,添加、修改或者删除监听fd的时候, 以及epoll_wait返回, 向用户空间传递数据时都会持有这个互斥锁。

2、在ET模式下,如果用多线程epoll_wait同一个epoll-fd,那么当其监听的fd产生了事件,此时epoll采用的排它式唤醒,也就是仅唤醒等待队列的第一个线程。

如果此时fd又触发了新的事件,那么就会唤醒新的线程,这将会导致多个线程操作同一个fd,可能导致线程安全问题。

解决方案是使用EPOLLNESHOT标志,即在一次wait返回后禁止fd再产生事件,并在处理完成后使用epoll_ctl的MOD操作重新开启。

综上,可以说epoll_wait是多线程安全的。

3、对于客户端,只会有一个异步操作在等待。假如在某些情况,一个客户端有两个异步方法在等待,就需要互斥量了。这是因为两个等待的操作可能正好在同一个时间完成,然后会在两个不同的线程中间同时调用他们的完成处理函数。

io_service

io_service就是基于select/epoll等多路复用IO实现的I/O事件循环的框架,它提供了对同步异步I/O,定时器以及信号等事件的支持。

io_service可以将需要操作的文件描述符socket、定时器、信号等注册到epoll实例中,如下所示:

epoll_create(1)

epoll_ctl(epfd, EPOLL_CTL_ADD, socket1, &event1);

epoll_ctl(epfd, EPOLL_CTL_ADD, socket2, &event2);// ...

然后,io_service不断地调用epoll_wait函数阻塞在事件循环中,直到某个文件描述符有事件被触发。在每个循环迭代中,io_service会检查是否有“就绪”(ready)的I/O事件,并将其添加到事件队列中。这样,io_service就可以调度这些事件的回调函数来处理I/O操作。在回调函数中,可以使用socket.async_read_some、socket.async_write_some等函数来启动异步读写操作,然后将读写缓冲区和回调函数的handler参数一起传递给底层的I/O系统函数,比如read和write等。

为什么一个io_service实例可以有多个处理线程呢?推测是内部io_service采用经典服务器模型实现了1+N的线程模型,通过特定的分发策略将新事件分发至多个线程处理。

关于io_service::strand

strand实现有序的原理是,内部维护一个任务队列,将所有异步操作包装成handler回调函数,并将这些回调函数绑定到strand上。

惊群现象

商用服务器系统如CentOS7中使用多线程对同一listenfd调用accept接口是没有惊群现象的,不过也可以参考一下。

1、由一个主进程进行accept监听,接受一个新连接之后再fork出一个子进程,把连接丢给子进程去进行业务处理,然后主进程继续监听。此时只有一个进程监听,无惊群现象。

2、由主进程fork出一批子进程,子进程继承了父进程的这个监听端口,多进程/线程共享该listenfd,然后都调用accept监听。多个子进程的PID挂在fd的waitqueue队列上,当全连接队列触发唤醒accept时,内核采用exclusive排他唤醒,即只唤醒队列头的PID,不会将waitqueue队列上的所有线程唤醒。

排他唤醒机制

linux对进程/线程唤醒提供了两种模式,一种是prepare_to_wait,一种是prepare_to_wait_exclusive。linux大部分接口都通过调用__wake_up_common唤醒任务等,该函数都会判断waitflags是否互斥,也就是通过prepare_to_wait_exclusive( )进行排他唤醒,不会有惊群现象。

综上,linux解决epoll惊群的方式如下:

1、给epoll添加一个EPOLLEXCLUSIVE的标志位,如果设置了这个标志位,那epoll将进程挂到等待队列时将会设置一下互斥标志位,此时内核在唤醒时判断该标志位,完成排他唤醒。

2、给socket提供SO_REUSEPORT标志,该flag允许不同进程的socket绑定到同一个端口。不同于父子进程共享socket监听的方式,此时每个进程的监听socket将指向open_file_tables下的不同节点,即不同的socket,也就是说不同进程是在自己的设备等待队列waitqueue下被挂起的,不存在共享fd的问题。内核中将设置了SO_REUSEPORT并且绑定同一端口的这些socket分到同一个group中,当有tcp连接事件到达的时候,内核将会对源IP+源端口取hash然后指定这个group中其中一个进程来接受连接,相当于内核实现了一个负载均衡。

epoll相关API使用注意点

epoll_create

epoll_create采用红黑树实现,为毛不用hashtable呢,hashtable insert可能触发rehash,时间不固定, 可能造成某些IO请求超时。epoll_create 的参数max_size在新版本内核中没有处理,但是必须大于0,小于等于0会返回EINVAL。

epoll_ctl

epollfd是文件句柄,是持有file文件结构的。所以epoll_ctl里面会做判断,不能监听epollfd自身,否则会形成嵌套。但是其他epollfd可以监听该epollfd。

在做增删改eventpoll结构的rbtree之前,会调用互斥锁eventpoll.mtx,所以该函数是线程安全的。

epoll_ctl会有最大监听数限制,超过会返回错误。

往epollfd增加监听的fd时,会将对应的epitem挂载到fd对应的file结构的链表上,即fd与epollfd相互指向,以O(1)的复杂度相互找到对方。

epoll_wait

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

函数中的 struct epoll_event 数量设置多少个才合理?使用vector动态设置,比如初始值为4096,增长阈值为0.75,达到阈值时自动增长1倍。events会拷贝至内核,不是使用mmap进行映射。

epoll_wait何种情况下会返回?至少一个注册的事件发生;

1、被信号中断;

2、超时,此时返回值为0;

3、正常捕获事件,此时返回值为返回事件的个数.

epollwait可以管理的相关事件

EPOLLOUT

只有在socket需要写数据时才注册该事件,写完后移除该事件,否则会一直触发可写事件。一般listen socket不需要注册可写事件。

EPOLLRDHUP

判断对端是否关闭,需要通过调用recv函数且返回为0进行判断,当有EPOLLIN事件且recv返回值为0说明读到了fin标志,对端已经关闭了socket,此时可以调用close关闭本端的socket。如果系统支持也可以注册EPOLLRDHUP事件。

EPOLLONESHOT

采用EPOLLONESHOT事件的文件描述符上的注册事件只触发一次,要想重新注册事件则需要调用 epoll_ctl 重置文件描述符上的事件,这样 socketfd 就不会出现竞态了。
备注:不能将监听描述符listenfd设置EPOLLONESHOT,否则会丢失客户端连接。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/607525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity制作二次元卡通渲染角色材质——2、色阶化光影的多种做法对比

Unity制作二次元材质角色 回到目录 大家好,我是阿赵。 这里继续讲二次元角色渲染。之前说过,最基本的卡通渲染,包含了色阶化光影和描边二个元素。所以这里先来说一下色阶化光影的多种做法对比。 一、光照模型和色阶化的说明 从上一篇文章里…

拿捏-哈夫曼树构建及编码生成(建议收藏)

文章目录 哈夫曼树的基本概念哈夫曼树的构建构建思路代码实现 哈夫曼编码的生成编码生成思路代码实现 完整代码展示及代码测试 哈夫曼树的基本概念 在认识哈夫曼树之前,你必须知道以下几个基本术语: 1、什么是路径? 在一棵树中,从…

Kafka 分区

分区是 Kafka 的核心功能,对于 Kafka 的存储结构、消息的生产消费方式都至关重要。 Partition(分区) 每 topic 都可以分成多分区,每个分区都是一组有序的、不可变的记录序列,每个分区在存储层面是 append log 文件。…

python中Requests发送json格式的post请求方法

问题:做requests请求时遇到如下报错: {“code”:“500”,“message”:"JSON parse error: Cannot construct instance of com.bang.erpapplication.domain.User (although at least one Creator exists): no String-argument constructor/factory …

唠一唠程序员的那些事

作为一名互联网摸爬滚打多年的老兵,我可以从以下角度提供一些信息: 加班是家常便饭:程序员往往需要加班来满足项目需求或完成任务。这意味着他们经常会在晚上、周末或节假日工作。代码不仅仅是工作:对于大多数程序员来说&#xff…

【C++】成员对象和成员函数分开存储

欢迎来到博主 Apeiron 的博客,祝您旅程愉快 ! 时止则止,时行则行。动静不失其时,其道光明。 目录 1、缘起 2、详解 3、代码清单 1 3.1、类中定义成员变量 3.2、类中定义成员函数 4、代码清单 2 5、总结 1、缘起 “成员变量…

SpringBoot 增强Controller方法,@ControllerAdvice注解的使用

参考资料 ControllerAdvice 用法SpringBoot使用ControllerAdvice注解 目录 一. ControllerAdvice注解作用二. ControllerAdvice注解 assignableTypes属性2.1 ControllerAdvice增强类2.2 Controller层2.3 效果 三. ControllerAdvice注解 basePackages属性3.1 ControllerAdvic…

踩坑|以为是Redis缓存没想到却是Spring事务!

前言 最近碰到了一个Bug,折腾了我好几天。并且这个Bug不是必现的,出现的概率比较低。一开始我以为是旧数据的问题,就让测试重新生成了一下数据,重新测试。由于后面几轮测试均未出现,我也就没太在意。 可惜好景不长&…

【LeetCode】260. 只出现一次的数字 III

260. 只出现一次的数字 III(中等) 思路 这道题是136. 只出现一次的数字 的进阶版,需要找出两个仅出现一次的元素。有了上一题的基础,我们很容易就想到要用异或来解决,但是由于这题最终会剩下两个不同的元素&#xff0…

设置和使用DragGAN:搭建非官方的演示版

DragGAN的官方版还没有发布,但是已经有非官方版的实现了,我们看看如何使用。DragGAN不仅让GAN重新回到竞争轨道上,而且为GAN图像处理开辟了新的可能性。正式版本将于本月发布。但是现在已经可以在一个非官方的演示中试用这个新工具了 DragGAN…

数据结构:二叉树(初阶)

朋友们、伙计们,我们又见面了,本期来给大家解读一下二叉树方面的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精通 …

Unix/C/C++进阶--SocketCAN 编程

Unix/C/C进阶--SocketCAN 编程 1 介绍1.1 socketcan 简介1.2 can 发展历程1.3 can总线优点 2 知识点2.1 CAN详解--书籍、网站2.2 CAN详解--CAN与com口介绍2.3 CAN详解--各家CAN分析仪与软件的比较2.4 转载:CAN总线终端电阻2.5 如何破解汽车--CAN协议(can…

3.8 电路布线

博主简介:一个爱打游戏的计算机专业学生博主主页: 夏驰和徐策所属专栏:算法设计与分析 1.最优子结构的证明: 我的理解: 对于电路布线问题的最优子结构性质,我们可以通过数学推导进行证明。下面是对证明的…

conda在 powershell下不能激活虚拟环境

这里写自定义目录标题 问题原因解决办法增加环境变量修改PowerShell 策略初始化conda环境安装或更新conda 结果 问题原因 powershell正常是不行的,但是在cmd中是可以的 问题产生的原因有很多: 必须无法识别activate.bat激活无反应 解决办法 增加环…

【JavaSE】Java基础语法(四十六):枚举

文章目录 1. 概述2. 定义格式3. 枚举的特点4. 枚举的方法 1. 概述 枚举是一种特殊的数据类型,它列出了一组预定义的常量,并使用标识符来引用这些常量。枚举的用途很广泛,下面列举了几个常见的应用场景: 管理常量:如果您…

计算机组成原理---第三章存储系统 习题详解版

(一)精选课内习题 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 (二)精选课后习题 1.设有一个具有20位地址和32位字长的存储器,问: (1)该存储器能存储多少个字节的信息? (2)如果存储器由512k8位的SR…

Linux :: vim 编辑器:详解:文本复制/粘贴/剪切/删除 与 撤销普通操作及撤销撤销操作

前言:本篇是 Linux 基本操作篇章的内容! 笔者使用的环境是基于腾讯云服务器:CentOS 7.6 64bit。 学习集: C 入门到入土!!!学习合集Linux 从命令到网络再到内核!学习合集 前文&#x…

chatgpt赋能python:Python去掉None:提高代码效率,优化SEO

Python去掉None:提高代码效率,优化SEO 作为一名有10年Python编程经验的工程师,我发现Python中会频繁出现None类型的变量。这种情况在代码中一旦过多,就会影响程序的效率,同样也会影响SEO的排名。因此,为提…

【数据仓库架构】什么是 Azure Synapse,它与 Azure Data Bricks 有何不同?

Azure Synapse Analytics 是一项针对大型公司的无限信息分析服务,它被呈现为 Azure SQL 数据仓库 (SQL DW) 的演变,将业务数据存储和宏或大数据分析结合在一起。 在处理、管理和提供数据以满足即时商业智能和数据预测需求时,Synapse 为所有工…

Hive学习---5、文件格式和压缩、企业级调优

1、文件格式和压缩 1.1 Hadoop压缩概述 由于Hive是相当于与Hadoop的客户端,所以hadoop会啥压缩,Hive基本就会啥压缩。 压缩格式算法文件扩展名是否可切分DEFLATEDEFLATE.deflate否GzipDEFLATE.gz否bzip2bzip2.bz2是LZOLZO.lzo是SnappySnappy.snappy否…