当前位置: 首页 > 科技观察

单机高并发模型设计

时间:2023-03-21 22:30:32 科技观察

背景在微服务架构下,我们习惯使用多机、分布式存储、缓存来支撑高并发请求模型,而忽略了单机高并发是如何实现的模型作品。本文通过解构客户端与服务端的连接建立和数据传输过程,来讲解如何设计单机高并发模型。经典的C10K题如何在一台物理机上同时服务10K用户和10000用户,对于java程序员来说并不难。使用netty可以搭建一个支持10000以上并发的服务器程序。那么netty是如何实现的呢?首先我们忘掉netty,从头开始分析。每个用户一个连接对于服务器来说意味着两件事:管理这10,000个连接和处理10,000个连接的数据传输。TCP连接与数据传输1.连接建立我们以一个普通的TCP连接为例。好熟悉的画面。本文侧重于服务器端分析,因此暂时忽略客户端细节。服务器端通过创建套接字、绑定端口和侦听就绪。最后通过accept与客户端建立连接。得到一个connectFd,它是一个connectionsocket(Linux中都是文件描述符),用来唯一标识一个连接。之后数据传输以此为基础。2.数据传输为了进行数据传输,服务器开启一个线程来处理数据。具体过程如下:select应用程序向系统内核空间询问数据是否准备好(因为有窗口大小限制,如果没有数据可以读取),数据没有准备好,应用程序程序已被阻止,等待响应。读内核判断数据准备好,将数据从内核复制到应用程序,完成后成功返回。应用程序进行解码,业务逻辑处理,最后编码,然后发送出去,返回给客户端。因为是一个线程处理一个连接数据,对应的线程模型如下:多路复用1.阻塞vs非阻塞因为一个连接传输,一个线程需要的线程太多,占用资源多。同时,连接结束,资源被销毁。必须重新建立连接。所以一个很自然的想法就是重用线程。即多个连接使用同一个线程。这导致了一个问题。本来在我们数据传输的入口处,假设线程正在处理某个连接的数据,但是数据一直不好,因为select被阻塞了,所以即使其他连接有数据要读,也可以也读它。所以不能阻塞,否则多个连接不能共享一个线程。所以它必须是非阻塞的。2、轮询VS事件通知改为非阻塞后,应用需要不断轮询内核空间,判断某个连接是否就绪:for(connectfdfd:connectFds){if(fd.ready){process();}}轮询效率比较低,CPU消耗大,所以通常的做法是被叫方发送事件通知通知调用方,而不是一直由调用方轮询。这就是IO多路复用,一路指的是标准输入和连接套接字。通过预先在某个组中注册一批套接字,当这个组中有任何IO事件发生时,就会通知阻塞对象准备就绪。3.select/poll/epollIO多路复用技术一般用select和poll来实现。select和poll区别不大,主要是poll对最大文件描述符没有限制。从轮询到事件通知,在使用了多路复用IO优化之后,虽然应用程序不用一直轮询内核空间。但是应用收到内核空间的事件通知后,并不知道对应的连接事件,所以要遍历:onEvent(){//监听事件for(connectfdfd:registerConnectFds){if(fd.ready){过程();}}}可以预见,随着连接数的增加,耗时也会成比例增加。相比poll返回的事件数,epoll返回的是一个有事件发生的connectFd数组,避免了应用程序的轮询。onEvent(){//监听事件for(connectfdfd:readyConnectFds){process();}}当然epoll的高性能不仅如此,还有边沿触发(edge-triggered),本文不再赘述。非阻塞IO+多路复用流程如下:select应用向系统内核空间询问数据是否准备好(因为有窗口大小限制,没有数据也可以读取),直接返回,非-阻塞调用。内核空间有数据ready,发送readyread给应用程序应用程序读取数据,进行decode,业务逻辑处理,最后encode,然后发送出去,返回给客户端线程池。上面我们主要使用非阻塞+MultiplexingIO来解决partialselect和read的问题。我们把整体流程重新梳理一下,看看整个数据处理流程是如何分组的。每个阶段使用不同的线程池来提高效率。首先,事件分为两种,连接事件acceptaction来处理,传输事件select,read,sendaction来处理。连接事件处理流程比较固定,没有额外的逻辑,不需要再拆分。发送事件read、send相对固定,每个连接的处理逻辑都差不多,可以放在一个线程池中处理。具体逻辑decode、logic、encode每个connection的处理逻辑是不同的。整体可以在一个线程池中处理。server分为三部分:reactor部分,统一处理事件,然后根据类型将连接事件分发给acceptor,将数据传输事件分发给handler。如果是数据传输类型,handler读取后会交给processorc处理。因为1、2处理都比较快,都是在线程池中处理,业务逻辑在另外一个线程池中处理。以上就是大名鼎鼎的reactor高并发模型。