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

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

服务器模型

对于并发量比较大的服务器,即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,否则会丢失客户端连接。

整理的高性能高并发服务器架构文章,内容预览:  初创网站与开源软件 6  谈谈大型高负载网站服务器的优化心得! 8  Lighttpd+Squid+Apache搭建高效率Web服务器 9  浏览量比较大的网站应该从哪几个方面入手? 17  用负载均衡技术建设高负载站点 20  大型网站的架构设计问题 25  开源平台的高并发集群思考 26  大型、高负载网站架构和应用初探 时间:30-45分钟 27  说说大型高并发高负载网站的系统架构 28  mixi技术架构 51 mixi.jp:使用开源软件搭建的可扩展SNS网站 51 总概关键点: 51 1,Mysql 切分,采用Innodb运行 52 2,动态Cache 服务器 -- 52 美国Facebok.com,中国Yeejee.com,日本mixi.jp均采用开源分布式缓存服务器Memcache 52 3,图片缓存和加 52  memcached+squid+apache deflate解决网站大访问量问题 52  FeedBurner:基于MySQL和JAVA的可扩展Web应用 53  YouTube 的架构扩展 55  了解一下 Technorati 的后台数据库架构 57  Myspace架构历程 58  eBay 的数据量 64  eBay 的应用服务器规模 67  eBay 的数据库分布扩展架构 68  从LiveJournal后台发展看大规模网站性能优化方法 70 一、LiveJournal发展历程 70 二、LiveJournal架构现状概况 70 三、从LiveJournal发展中学习 71 1、一台服务器 71 2、两台服务器 72 3、四台服务器 73 4、五台服务器 73 5、更多服务器 74 6、现在我们在哪里: 75 7、现在我们在哪里 78 8、现在我们在哪里 79 9、缓存 80 10、Web访问负载均衡 80 11、MogileFS 81  Craigslist 的数据库架构 81  Second Life 的数据拾零 82  eBay架构的思想金矿 84  一天十亿次的访问-eBay架构(一) 85  七种缓存使用武器 为网站应用和访问加速发布时间: 92  可缓存的CMS系统设计 93  开发大型高负载类网站应用的几个要点 105  Memcached和Lucene笔记 110  使用开源软件,设计高性能可扩展网站 110  面向高负载的架构Lighttpd+PHP(FastCGI)+Memcached+Squid 113  思考高并发高负载网站的系统架构 113  "我在SOHU这几年做的一些门户级别的程序系统(C/C++开发)" 115  中国顶级门户网站架构分析1 116  中国顶级门户网站架构分析 2 118  服务器的大用户量的承载方案 120  YouTube Scalability Talk 121  High Performance Web Sites by Nate Koechley 123 One dozen rules for faster pages 123 Why talk about performance? 123 Case Studies 124 Conclusion 124  Rules for High Performance Web Sites 124  对于应用高并发,DB千万级数量该如何设计系统哪? 125  高性能服务器设计 130  优势与应用:再谈CDN镜像加速技术 131  除了程序设计优化,zend+ eacc(memcached)外,有什么办法能提高服务器的负载能力呢? 135  如何规划您的大型JAVA多并发服务器程序 139  如何架构一个“Just so so”的网站? 148  最便宜的高负载网站架构 152  负载均衡技术全攻略 154  海量数据处理分析 164  一个很有意义的SQL的优化过程(一个电子化支局中的大数据量的统计SQL) 166  如何优化大数据量模糊查询(架构,数据库设置,SQL..) 168  求助:海量数据处理方法 169 # re: 求助:海量数据处理方法 回复 更多评论 169  海量数据库查询方略 169  SQL Server 2005对海量数据处理 170  分表处理设计思想和实现 174  Linux系统高负载 MySQL数据库彻底优化(1) 179  大型数据库的设计与编程技巧 本人最近开发一个访问统计系统,日志非常的大,都保存在数据库里面。 我现在按照常规的设计方法对表进行设计,已经出现了查询非常缓慢地情形。 大家对于这种情况如何来设计数据库呢?把一个表分成多个表么?那么查询和插入数据库又有什么技巧呢? 谢谢,村里面的兄弟们! 183  方案探讨,关于工程中数据库的问题. 184  web软件设计时考虑你的性能解决方案 190  大型Java Web系统服务器选型问题探讨 193  高并发高流量网站架构 210 1.1 互联网的发展 210 1.2 互联网网站建设的新趋势 210 1.3 新浪播客的简介 211 2.1 镜像网站技术 211 2.2 CDN内容分发网络 213 2.3 应用层分布式设计 214 2.4 网络层架构小结 214 3.1 第四层交换简介 214 3.2 硬件实现 215 3.3 软件实现 215  网站架构的高性能和可扩展性 233  资料收集:高并发 高性能 高扩展性 Web 2.0 站点架构设计及优化策略 243  CommunityServer性能问题浅析 250 鸡肋式的多站点支持 250 内容数据的集中式存储 250 过于依赖缓存 250 CCS的雪上加霜 250 如何解决? 251  Digg PHP's Scalability and Performance 251  YouTube Architecture 253 Information Sources 254 Platform 254 What's Inside? 254 The Stats 254 Recipe for handling rapid growth 255 Web Servers 255 Video Serving 256 Serving Video Key Points 257 Serving Thumbnails 257 Databases 258 Data Center Strategy 259 Lessons Learned 260 1. Jesse • Comments (78) • April 10th 261 Library 266 Friendster Architecture 273 Information Sources 274 Platform 274 What's Inside? 274 Lessons Learned 274  Feedblendr Architecture - Using EC2 to Scale 275 The Platform 276 The Stats 276 The Architecture 276 Lesson Learned 277 Related Articles 278 Comments 279 Re: Feedblendr Architecture - Using EC2 to Scale 279 Re: Feedblendr Architecture - Using EC2 to Scale 279 Re: Feedblendr Architecture - Using EC2 to Scale 280  PlentyOfFish Architecture 281 Information Sources 282 The Platform 282 The Stats 282 What's Inside 283 Lessons Learned 286  Wikimedia architecture 288 Information Sources 288 Platform 288 The Stats 289 The Architecture 289 Lessons Learned 291  Scaling Early Stage Startups 292 Information Sources 293 The Platform 293 The Architecture 293 Lessons Learned 294  Database parallelism choices greatly impact scalability 295  Introduction to Distributed System Design 297 Table of Contents 297 Audience and Pre-Requisites 298 The Basics 298 So How Is It Done? 301 Remote Procedure Calls 305 Some Distributed Design Principles 307 Exercises 308 References 309  Flickr Architecture 309 Information Sources 309 Platform 310 The Stats 310 The Architecture 311 Lessons Learned 316 Comments 318 How to store images? 318 RE: How to store images? 318  Amazon Architecture 319 Information Sources 319 Platform 320 The Stats 320 The Architecture 320 Lessons Learned 324 Comments 329 Jeff.. Bazos? 329 Werner Vogels, the CTO of 329 Re: Amazon Architecture 330 Re: Amazon Architecture 330 Re: Amazon Architecture 330 It's WSDL 330 Re: It's WSDL 331 Re: Amazon Architecture 331  Scaling Twitter: Making Twitter 10000 Percent Faster 331 Information Sources 332 The Platform 332 The Stats 333 The Architecture 333 L
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值