I/O多路复用 + Reactor和Proactor + 一致性哈希

网络系统

1. I/O多路复用

1)原始Socket模型通信方式

  • 首先服务端调用socket函数,创建网络协议为IPv4,传输协议为TCP的socket;
  • 调用bind()绑定IP地址和端口号;
  • 调用listen()进行监听;
  • 服务器进入监听状态后,调用accpet()函数,从内核获取客户端的连接,如果没有则会阻塞等待。
  • 客户端这边创建socket后,调用connect(参数为服务端ip和端口号)发起连接,然后开始TCP三次握手。
    • TCP连接过程中服务器内核为每个socket连接维护两个队列,一个是TCP半连接 队列,服务端处于syn_rcvd的状态,另一个是TCP全连接 队列,此时服务端处于established状态。
    • TCP全连接队列存放的是已连接socket。服务端的accpet()函数会拿一个已连接socket用于数据传输。所以监听有监听socket,传输数据有已连接socket,是两个socket。双方通过read()和write()函数来进行读写。
    • 缺点:原始Socket模型只能一对一通信,服务端没处理完一个客户端的I/O请求或发生读写阻塞时,其他客户端无法连接。

2)多进程模型

  • 父进程只用关心监听socket负责连接,子进程只用关心已连接socket负责通信,因为子进程会复制父进程的文件描述符。
  • 缺点
    • 子进程退出系统仍会保留该进程一些信息,不做好回收工作,会变成僵尸进程慢慢耗尽系统资源;
    • 进程资源开销太大;
    • 进程上下文切换代价较大。

3)多线程模型

  • 服务器与客户端建立连接后,通过pthread_create()函数创建线程,将已连接socket的文件描述符传递给线程函数,接着就可以使用该线程进行通信。
  • 使用线程池避免线程繁重的创建和销毁的开销。

4)I/O多路复用

  • I/O多路复用,一个进程处理多个请求,进程可以调用一个系统函数从内核获取多个事件。

select/poll

  • select:已连接socket的文件描述符集合,调用selcet()拷贝到内核,内核遍历整合集合检查看是否有事件产生,有的话将该socket标记为可读或可写,再将整个集合拷贝到用户态,然后用户态再遍历整个集合,找到可读或可写的socket进行处理。
  • selcet使用固定长度的BitsMap存储文件描述符,最大1024。
  • poll改用链表存储文件描述符,个数仍受系统限制。
  • select/poll都要2次拷贝2次遍历,都使用线性结构存储socket文件描述符,时间复杂度O(n)。

epoll

  • epoll()在内核里使用红黑树来保存socket,有新的socket,只需拷贝这一个到内核,无需像select/poll那样拷贝整个集合到内核。
  • epoll使用事件驱动机制,内核维护了一个链表来记录就绪事件,当已连接socket有事件产生,内核通过回调函数将其加入到链表中,用户调用epoll_wait()函数将链表中的socket拷贝到用户态中,无需再拷贝整个集合且无需再遍历整个集合。
  • epoll用法:
    • 调用epoll_create创建一个epoll对象epfd;
    • 再通过epoll_ctl将需要监听的socket添加到epfd中;
    • 最后调用epoll_wait()等待数据。

边缘触发和水平触发

  • 边缘触发 :当被监控的socket有可读事件发生,调用epoll_wait()会触发一次epollin事件(表示有数据可读),需循环读取数据,防止数据未读完,因此搭配非阻塞I/O使用。若未读完,后面再调用epoll_wait()不会再触发epollin事件,直到有新的数据到来。
  • 水平触发:当被监控的socket有可读事件发生,每次调用epoll_wait()都会触发epollin事件,直到缓冲区数据全部读完。若未读完,下一次epoll_wait()会再次触发epollin事件。只要缓冲区有数据没读完,就会一直触发epollin事件。

2. Reactor和Proactor

1)Reactor模式

  • 通过面向对象的思想对I/O多路复用做了一层封装。
  • Reactor模式由Reactor和处理资源池组成。Reactor负责监听和分发事件,事件类型包含连接事件和读写事件。处理资源池可以是单个进/线程,也可以是多个进/线程,负责处理事件,如:read()--->业务逻辑--->send()。

2)Reactor模式四种方案

  • 单reactor单进/线程;
  • 单reactor多进/线程;
  • 多reactor单进/线程(无应用);
  • 多reactor多进/线程;

3)单Reactor单进程:

  • Reactor对象的作用是监听和分发事件,select()监听,dispatch()分发;

  • Acceptor对象通过accept()方法获取连接;

  • Handler对象的作用是处理事件,read()--->业务逻辑--->send()。

  • 工作流程:

    • Reactor对象通过select()监听事件,收到事件后,根据事件类型,是连接事件还是读写事件,通过dispatch()进行分发;
    • 如果是连接事件分发给Acceptor对象,Acceptor对象会调用accept()方法,建立连接,并创建一个Handler对象用于处理事件;
    • 如果是读写事件,则直接分发给Handler对象进行处理。
  • 缺点:

    • 无法利用多核CPU的性能;
    • Handler对象在处理业务时,整个进程无法处理其他连接的事件,如果业务处理耗时长,怎么造成响应的延迟。
    • 所以单Reactor单进/线程不适合计算机密集型的场景,只适用于业务逻辑处理非常快速的场景,如Redis6.0之前的版本。

4)单Reactor多线程:

  • 工作流程:

    • 前面步骤和单Reactor单线/进程一样;
    • 不同的是:Handler对象只负责数据的接收和发送,不再进行业务处理,read()数据后发送给线程池里的子线程的Processor对象进行业务处理。
    • 处理完结果由主线程的Handler对象通过send()将响应结果发送给客户端。
  • 优点:能够充分利用多核CPU的性能。

  • 缺点:一个Reactor承担所有事件的监听和分发,且只在主线程中运行,在瞬间高并发的场景下,容易成为性能瓶颈。

5)多Reactor多线程

  • 工作流程:

    • 主线程中的MainReactor对象通过select()监听事件,收到事件后通过Acceptor对象中的accept()建立连接,并将先建立的连接分配给子线程。
    • 子线程中的SubReactor对象将MainReactor对象分配的连接加入select()继续监听,并创建Handler()对象用于事件处理。
    • 新的读写事件到来,SubReactor会调用dispatch()分发给相应的Handler对象进行处理。
  • 多Reactor多线程实际实现比单Reactor多线程简单的原因:

    • 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责连接的事件监听和处理.
    • 主线程和子线程交互简单,主线程只需将新连接传给子线程;子线程处理完结果无需返回给主线程,直接返回给客户端。
  • 应用:

    • 多Reactor多线程:
      • Netty(高性能的NIO网络框架,用于构建高并发的应用);
      • Memcache(分布式缓存系统,主要用于存储键值对);
    • 多Reactor多进程
      • Nginx

6)Proactor

  • Reactor和Proactor都是一种基于事件分发的网络模式;
  • Reactor非阻塞网络同步模式,Proactor是异步网络模式;
  • Reactor模式是基于待完成的I/O事件,而Proactor模式则是基于已完成的I/O事件。
  • 阻塞I/O阻塞等待的是:内核数据准备好的过程。
  • 无论是阻塞I/O还是非阻塞I/O,都是同步调用,同步调用就是指数据从内核态缓冲区拷贝到用户态缓冲区这个过程是需要等待的。
  • 而异步I/O无需等待这个过程,发起异步I/O之后,这两个动作由内核自动完成,应用程序不需要主动发起拷贝动作。
  • Proactor工作流程:
    • 收到请求Proactor Initiatord创建一个Proactor和Handler,并将它们通过Asynchronous Operation Processer注册到内核;
    • Asynchronous Operation Processer负责处理注册请求和I/O请求,Asynchronous Operation Processer完成I/O请求(数据已经拷贝到用户缓冲区)后通知Proactor;
    • Proactor根据事件类型回调应用进程中的Handler 完成业务处理。

3. 一致性哈希

分布式系统如何分配请求--->Hash--->扩缩困难--->一致性Hash--->数据节点分布不均衡--->虚拟节点+一致性Hash

  • 如何分配请求 :引入一个负载均衡层,硬件配置好的权重更大,这种算法叫加权轮询,前提是每个服务节点都存储一样的数据。
  • 但是分布式系统,每个服务节点存储的数据都不一样。这时我们想到Hash算法,hash(key)%节点数,进行映射。但若节点数量发生变化,进行扩容或缩容困难,最坏的情况下所有数据都要迁移。
  • 一致性哈希算法 ,它时对232这个固定值进行取模(首尾相连的有232个点的哈希环),要进行两步hash:
    • 第一步是对存储节点进行hash;
    • 第二步是对数据进行hash;
    • 数据保存在顺时针找到的第一个节点上。
  • 一致性哈希算法对于存储节点增删,只对与该节点顺时针相邻的后继数据节点有影响。
  • 但一致性哈希算法不能保证节点在哈希环上均匀分布,仍存在节点分布不均匀的问题。这时使用虚拟节点提高均衡度。
  • 虚拟节点: 做两层映射,真实存储节点和虚拟存储节点的映射,虚拟存储节点和哈希环的映射。
  • 好处
    • 存储节点数量变多,数据节点分布更均衡。
    • 当存储节点发生变化,有不同的存储节点分担它的数据,稳定性更高。
    • 而且对硬件配置高的存储节点,可以增加其虚拟节点。
    • 因此一致性哈希算法+虚拟节点 适用于硬件配置不同 的节点场景和节点规模会发生变化的场景。
  • 总结 :分布式系统如何分配请求--->Hash--->扩缩困难--->一致性Hash--->数据节点分布不均衡--->虚拟节点+一致性Hash

4.参考

https://www.xiaolincoding.com/os/8_network_system/selete_poll_epoll.html

相关推荐
小李同学_LHY3 分钟前
三.微服务架构中的精妙设计:服务注册/服务发现-Eureka
java·spring boot·spring·springcloud
非ban必选27 分钟前
spring-ai-alibaba第四章阿里dashscope集成百度翻译tool
java·人工智能·spring
非ban必选33 分钟前
spring-ai-alibaba第五章阿里dashscope集成mcp远程天气查询tools
java·后端·spring
遥不可及~~斌1 小时前
@ComponentScan注解详解:Spring组件扫描的核心机制
java
高林雨露1 小时前
Java 与 Kotlin 对比示例学习(三)
java·kotlin
极客先躯1 小时前
高级java每日一道面试题-2025年3月22日-微服务篇[Nacos篇]-Nacos的主要功能有哪些?
java·开发语言·微服务
爱喝醋的雷达2 小时前
Spring SpringBoot 细节总结
java·spring boot·spring
coderzpw3 小时前
当模板方法模式遇上工厂模式:一道优雅的烹饪架构设计
java·模板方法模式
直裾3 小时前
Mapreduce初使用
java·mapreduce
悠夏安末3 小时前
intellij Idea 和 dataGrip下载和安装教程
java·ide·intellij-idea