learn.lianglianglee.com/文章/异步网络模型.md.html
2022-05-11 19:04:14 +08:00

991 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>异步网络模型.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>异步网络模型</h1>
<p>异步网络模型在服务开发中应用非常广泛,相关资料和开源库也非常多。项目中,使用现成的轮子提高了开发效率,除了能使用轮子,还是有必要了解一下轮子的内部构造。</p>
<p>这篇文章从最基础的5种I/O模型切入到I/O事件处理模型再到并发模式最后以Swoole开源库来做具体分析逐步深入。文中提到的模型都是一些通用的模型在《linux高性能服务器编程》中也都有涉及。文章不涉及模型的实现细节最重要的是去理解各个模型的工作模式以及其优缺点。</p>
<p>文中涉及接口调用的部分都是指Linux系统的接口调用。 共分为5部分</p>
<p><strong>I/O模型</strong></p>
<p>从基础的系统调用方法出发给大家从头回顾一下最基本的I/O模型虽然简单但是不可或缺的基础</p>
<p><strong>事件处理模型</strong></p>
<p>这部分在同步I/O、异步I/O的基础上分别介绍Reactor模型以及Proactor模型着重两种模型的构成以及事件处理流程。Reactor模型是我们常见的不同平台对异步I/O系统接口的支持力度不同这部分还介绍了一种使用同步I/O来模拟Proactor模型的方法。</p>
<p><strong>并发模式</strong></p>
<p>就是多线程、多进程的编程的模式。介绍了两种较为高效的并发模型,半同步/半异步(包括其演变模式)、FollowerLeader模式。</p>
<p><strong>Swoole异步网络模型分析</strong></p>
<p>这部分是结合已介绍的事件处理模型、并发模式对Swoole的异步模型进行分析 从分析的过程来看,看似复杂的网络模型,可以拆分为简单的模型单元,只不过我们需要权衡利弊,选取合适业务需求的模型单元进行组合。 我们团队基于Swoole 1.8.5版本做了很多修改部分模块做了重构计划在17年6月底将修改后版本开源出去敬请期待。</p>
<p><strong>改善性能的方法</strong></p>
<p>最后一部分是在引入话题,介绍的是几种常用的方法。性能优化是没有终点的,希望大家能贡献一些想法和具体方法。</p>
<h2>I/O模型</h2>
<p>POSIX 规范中定义了同步I/O 和异步I/O的术语
<strong>同步I/O</strong> : 需要进程去真正的去操作I/O</p>
<p><strong>异步I/O</strong>内核在I/O操作完成后再通知应用进程操作结果。</p>
<p>在《UNIX网络编程》中介绍了5中I/O模型阻塞I/O、非阻塞I/O、I/O复用、SIGIO 、异步I/O本节对这5种I/O模型进行说明和对比。</p>
<h3>I/O阻塞</h3>
<p>通常把阻塞的文件描述符file descriptorfd称之为阻塞I/O。默认条件下创建的socket fd是阻塞的针对阻塞I/O调用系统接口可能因为等待的事件没有到达而被系统挂起直到等待的事件触发调用接口才返回例如tcp socket的connect调用会阻塞至第三次握手成功不考虑socket 出错或系统中断如图1所示。另外socket 的系统API accept、send、recv等都可能被阻塞。</p>
<p><img src="assets/connect_bloackxx-1.png" alt="img" /></p>
<blockquote>
<pre><code> 图1 I/O 阻塞模型示意图
</code></pre>
</blockquote>
<p>另外补充一点网络编程中通常把可能永远阻塞的系统API调用 称为慢系统调用,典型的如 accept、recv、select等。慢系统调用在阻塞期间可能被信号中断而返回错误相应的errno 被设置为EINTR我们需要处理这种错误解决办法有</p>
<p><strong>1. 重启系统调用</strong></p>
<p>直接上示例代码吧以accept为例被中断后重启accept 。有个例外若connect 系统调用在阻塞时被中断是不能直接重启的与内核socket 的状态有关)有兴趣的同学可以深入研究一下connect 的内核实现。使用I/O复用等待连接完成能避免connect不能重启的问题。</p>
<pre><code class="language-c">int client_fd = -1;
struct sockaddr_in client_addr;
socklen_t child_addrlen;
while (1) {
call_accept:
client_fd = accept(server_fd,NULL,NULL)
if (client_fd &lt; 0) {
if (EINTR == errno) {
goto call_accept;
} else {
sw_sysError(&quot;accept fail&quot;);
break;
}
}
}
</code></pre>
<p><strong>2. 信号处理</strong></p>
<p>利用信号处理可以选择忽略信号或者在安装信号时设置SA_RESTART属性。设置属性SA_RESTART信号处理函数返回后被安装信号中断的系统调用将自动恢复示例代码如下。需要知道的是设置SA_RESTART属性方法并不完全适用对某些系统调用可能无效这里只是提供一种解决问题的思路示例代码如下</p>
<pre><code class="language-c">int client_fd = -1;
struct sigaction action,old_action;
action.sa_handler = sig_handler;
sigemptyset(&amp;action.sa_mask);
action.sa_flags = 0;
action.sa_flags |= SA_RESTART;
/// 若信号已经被忽略,则不设置
sigaction(SIGALRM, NULL, &amp;old_action)
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGALRM, &amp;action, NULL)
}
while (1) {
client_fd = accept(server_fd,NULL,NULL)
if (client_fd &lt; 0) {
sw_sysError(&quot;accept fail&quot;);
break;
}
}
</code></pre>
<h3>I/O非阻塞</h3>
<p>把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd或者使用fcntl将fd设置为非阻塞。</p>
<p>对非阻塞fd调用系统接口时不需要等待事件发生而立即返回事件没有发生接口返回-1此时需要通过errno的值来区分是否出错有过网络编程的经验的应该都了解这点。不同的接口立即返回时的errno值不尽相同recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCKconnect 则为EINPRO- GRESS 。</p>
<p>以recv操作非阻塞套接字为例如图2所示。</p>
<p><img src="assets/recv_noblock-1.png" alt="img" /></p>
<blockquote>
<pre><code> 图2 非阻塞I/O模型示意图
</code></pre>
</blockquote>
<p>当我们需要读取在有数据可读的事件触发时再调用recv避免应用层不断去轮询检查是否可读提高程序的处理效率。通常非阻塞I/O与I/O事件处理机制结合使用。</p>
<h3>I/O复用</h3>
<p>最常用的I/O事件通知机制就是I/O复用(I/O multiplexing)。Linux 环境中使用select/poll/epoll 实现I/O复用I/O复用接口本身是阻塞的在应用程序中通过I/O复用接口向内核注册fd所关注的事件当关注事件触发时通过I/O复用接口的返回值通知到应用程序如图3所示,以recv为例。I/O复用接口可以同时监听多个I/O事件以提高事件处理效率。</p>
<p><img src="assets/io---1.png" alt="img" /></p>
<blockquote>
<pre><code> 图 3 I/O复用模型示意图
</code></pre>
</blockquote>
<p>关于select/poll/epoll的对比可以参考[]epoll使用比较多但是在并发的模式下需要关注惊群的影响。</p>
<h3>SIGIO</h3>
<p>除了I/O复用方式通知I/O事件还可以通过SIGIO信号来通知I/O事件如图4所示。两者不同的是在等待数据达到期间I/O复用是会阻塞应用程序而SIGIO方式是不会阻塞应用程序的。</p>
<p><img src="assets/------1.png" alt="img" /></p>
<blockquote>
<pre><code> 图 4 信号驱动I/O模型示意图
</code></pre>
</blockquote>
<h3>异步I/O</h3>
<p>POSIX规范定义了一组异步操作I/O的接口不用关心fd 是阻塞还是非阻塞异步I/O是由内核接管应用层对fd的I/O操作。异步I/O向应用层通知I/O操作完成的事件这与前面介绍的I/O 复用模型、SIGIO模型通知事件就绪的方式明显不同。以aio_read 实现异步读取IO数据为例如图5所示在等待I/O操作完成期间不会阻塞应用程序。</p>
<p><img src="assets/--io-1.png" alt="img" /></p>
<blockquote>
<pre><code> 图 5 异步I/O 模型示意图
</code></pre>
</blockquote>
<h3>I/O模型对比</h3>
<p>前面介绍的5中I/O中I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 都会在不同程度上阻塞应用程序而只有异步I/O模型在整个操作期间都不会阻塞应用程序。</p>
<p>如图6所示列出了5种I/O模型的比较</p>
<p><img src="assets/-------1--1.png" alt="img" /></p>
<blockquote>
<pre><code> 图6 五种I/O 模型比较示意图
</code></pre>
</blockquote>
<h2>事件处理模型</h2>
<p>网络设计模式中如何处理各种I/O事件是其非常重要的一部分Reactor 和Proactor两种事件处理模型应运而生。上章节提到将I/O分为同步I/O 和 异步I/O可以使用同步I/O实现Reactor模型使用异步I/O实现Proactor模型。</p>
<p>本章节将介绍Reactor和Proactor两种模型最后将介绍一种使用同步I/O模拟Proactor事件处理模型。</p>
<h3><strong>Reactor事件处理模型</strong></h3>
<p>Reactor模型是同步I/O事件处理的一种常见模型关于Reactor模型结构的资料非常多一个典型的Reactor模型类图结构如图7所示</p>
<p><img src="assets/import5.png" alt="img" /></p>
<blockquote>
<pre><code> 图 7 Reactor 模型类结构图
</code></pre>
</blockquote>
<p><strong>Reactor的核心思想</strong>将关注的I/O事件注册到多路复用器上一旦有I/O事件触发将事件分发到事件处理器中执行就绪I/O事件对应的处理函数中。模型中有三个重要的组件</p>
<ul>
<li><strong>多路复用器</strong>由操作系统提供接口Linux提供的I/O复用接口有select、poll、epoll</li>
<li><strong>事件分离器</strong>:将多路复用器返回的就绪事件分发到事件处理器中;</li>
<li><strong>事件处理器</strong>:处理就绪事件处理函数。</li>
</ul>
<p>图7所示Reactor 类结构中包含有如下角色。</p>
<ul>
<li><strong>Handle</strong>:标示文件描述符;</li>
<li><strong>Event Demultiplexer</strong>执行多路事件分解操作对操作系统内核实现I/O复用接口的封装用于阻塞等待发生在句柄集合上的一个或多个事件如select/poll/epoll</li>
<li><strong>Event Handler</strong>:事件处理接口;</li>
<li><strong>Event Handler A(B)</strong>:实现应用程序所提供的特定事件处理逻辑;</li>
<li><strong>Reactor</strong>:反应器,定义一个接口,实现以下功能:</li>
</ul>
<pre><code> a)供应用程序注册和删除关注的事件句柄;
b)运行事件处理循环;
c)等待的就绪事件触发,分发事件到之前注册的回调函数上处理.
</code></pre>
<p>接下来介绍Reactor的工作流程如图8所示为Reactor模型工作的简化流程。</p>
<p><img src="assets/-------4-.png" alt="img" /></p>
<blockquote>
<pre><code> 图8 Reactor模型简化流程示意图
</code></pre>
</blockquote>
<ol>
<li>注册I/O就绪事件处理器</li>
<li>事件分离器等待I/O就绪事件</li>
<li>I/O事件触发激活事件分离器分离器调度对应的事件处理器</li>
<li>事件处理器完成I/O操作处理数据.</li>
</ol>
<p>网络设计中Reactor使用非常广在开源社区有很许多非常成熟的、跨平台的、Reactor模型的网络库比较典型如libevent。</p>
<h3>Proactor事件处理模型</h3>
<p>与Reactor不同的是Proactor使用异步I/O系统接口将I/O操作托管给操作系统Proactor模型中分发处理异步I/O完成事件并调用相应的事件处理接口来处理业务逻辑。Proactor类结构如图9所示。</p>
<p><img src="assets/import8.png" alt="img" /></p>
<blockquote>
<pre><code> 图9 Proactor模型类结构图
</code></pre>
</blockquote>
<p>图9所示Proactor类结构中包含有如下角色</p>
<ul>
<li><strong>Handle</strong> 用来标识socket连接或是打开文件</li>
<li><strong>Async Operation Processor</strong>:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;</li>
<li><strong>Async Operation</strong>:异步操作;</li>
<li><strong>Completion Event Queue</strong>:完成事件队列;异步操作完成的结果放到队列中等待后续使用;</li>
<li><strong>Proactor</strong>:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;</li>
<li><strong>Completion Handler</strong>:完成事件接口;一般是由回调函数组成的接口;</li>
<li><strong>Completion Handler A(B)</strong>:完成事件处理逻辑;实现接口定义特定的应用处理逻辑。</li>
</ul>
<p>Proactor模型的简化的工作流程,如图10所示。</p>
<p><img src="assets/-------3-.png" alt="img" /></p>
<blockquote>
<pre><code> 图10 Proactor模型简化工作流程示意图
</code></pre>
</blockquote>
<ol>
<li>发起I/O异步操作注册I/O完成事件处理器;</li>
<li>事件分离器等待I/O操作完成事件</li>
<li>内核并行执行实际的I/O操作并将结果数据存入用户自定义缓 冲区;</li>
<li>内核完成I/O操作通知事件分离器事件分离器调度对应的事件处理器</li>
<li>事件处理器处理用户自定义缓冲区中的数据。</li>
</ol>
<p>Proactor利用异步I/O并行能力可给应用程序带来更高的效率但是同时也增加了编程的复杂度。windows对异步I/O提供了非常好的支持常用Proactor的模型实现服务器而Linux对异步I/O操作(aio接口)的支持并不是特别理想而且不能直接处理accept因此Linux平台上还是以Reactor模型为主。</p>
<p>Boost asio采用的是Proactor模型但是Linux上采用I/O复用的方式来模拟Proactor另启用线程来完成读写操作和调度。</p>
<h3>同步I/O模拟Proactor</h3>
<p>下面一种使用同步I/O模拟Proactor的方案原理是</p>
<p><strong>主线程执行数据读写操作读写操作完成后主线程向工作线程通知I/O操作“完成事件”</strong></p>
<p>工作流程如图 11所示。</p>
<p><img src="assets/11.png" alt="img" /></p>
<blockquote>
<pre><code> 图11 同步I/O模拟Proactor模型
</code></pre>
</blockquote>
<p>简单的描述一下图11 的执行流程:</p>
<ol>
<li>主线程往系统I/O复用中注册文件描述符fd上的读就绪事件</li>
<li>主线程调用调用系统I/O复用接口等待文件描述符fd上有数据可读</li>
<li>当fd上有数据可读时通知主线程。主线程循环读取fd上的数据直到没有更多数据可读然后将读取到的数据封装成一个请求对象并插入请求队列。</li>
<li>睡眠在请求队列上的某个工作线程被唤醒它获得请求对象并处理客户请求然后向I/O复用中注册fd上的写就绪事件。主线程进入事件等待循环等待fd可写。</li>
</ol>
<h2>并发模式</h2>
<p>在I/O密集型的程序采用并发方式可以提高CPU的使用率可采用多进程和多线程两种方式实现并发。当前有高效的两种并发模式半同步/半异步模式、Follower/Leader模式。</p>
<h3>半同步/半异步模式</h3>
<p>首先区分一个概念,并发模式中的“同步”、“异步”与 I/O模型中的“同步”、“异步”是两个不同的概念</p>
<p><strong>并发模式中</strong>“同步”指程序按照代码顺序执行“异步”指程序依赖事件驱动如图12 所示并发模式的“同步”执行和“异步”执行的读操作;</p>
<p><strong>I/O模型中</strong>“同步”、“异步”用来区分I/O操作的方式是主动通过I/O操作拿到结果还是由内核异步的返回操作结果。</p>
<p><img src="assets/12.png" alt="img" /></p>
<blockquote>
<pre><code> 图12(a) 同步读操作示意图
</code></pre>
</blockquote>
<p><img src="assets/d4293cab-d80d-4396-9790-425c1e414cc5.png" alt="img" /></p>
<blockquote>
<pre><code> 图12(b) 异步读操作示意图
</code></pre>
</blockquote>
<p>本节从最简单的半同步/半异步模式的工作流程出发,并结合事件处理模型介绍两种演变的模式。</p>
<h4><strong>半同步/半异步工作流程</strong></h4>
<p>半同步/半异步模式的工作流程如图13 所示。</p>
<p><img src="assets/13-1.png" alt="img" /></p>
<blockquote>
<pre><code> 图13 半同步/半异步模式的工作流程示意图
</code></pre>
</blockquote>
<p>其中异步线程处理I/O事件同步线程处理请求对象简单的来说</p>
<ol>
<li>异步线程监听到事件后,将其封装为请求对象插入到请求队列中;</li>
<li>请求队列有新的请求对象,通知同步线程获取请求对象;</li>
<li>同步线程处理请求对象,实现业务逻辑。</li>
</ol>
<h4>半同步/半反应堆模式</h4>
<p>考虑将两种事件处理模型即Reactor和Proactor与几种I/O模型结合在一起那么半同步/半异步模式就演变为半同步半反应堆模式。先看看使用Reactor的方式如图14 所示。</p>
<p><img src="assets/15.png" alt="img" /></p>
<blockquote>
<pre><code> 图14 半同步/半反应堆模式示意图
</code></pre>
</blockquote>
<p>其工作流程为:</p>
<ol>
<li>异步线程监听所有fd上的I/O事件若监听socket接可读接受新的连接并监听该连接上的读写事件</li>
<li>若连接socket上有读写事件发生异步线程将该连接socket插入请求队列中</li>
<li>同步线程被唤醒并接管连接socket从socket上读取请求和发送应答</li>
</ol>
<p>若将Reactor替换为Proactor那么其工作流程为</p>
<ol>
<li>异步线程完成I/O操作并I/O操作的结果封装为任务对象插入请求队列中</li>
<li>请求队列通知同步线程处理任务;</li>
<li>同步线程执行任务处理逻辑。</li>
</ol>
<h4>一种高效的演变模式</h4>
<p>半同步/半反应堆模式有明显的缺点:</p>
<ol>
<li>异步线程和同步线程共享队列,需要保护,存在资源竞争;</li>
<li>工作线程同一时间只能处理一个任务任务处理量很大或者任务处理存在一定的阻塞时任务队列将会堆积任务的时效性也等不到保证不能简单地考虑增加工作线程来处理该问题线程数达到一定的程度工作线程的切换也将白白消耗大量的CPU资源。</li>
</ol>
<p>下面介绍一种改进的方式如图15 所示,每个工作线程都有自己的事件循环,能同时独立处理多个用户连接。</p>
<p><img src="assets/16.png" alt="img" /></p>
<blockquote>
<pre><code> 图 15 半同步/半反应堆模式的演变模式
</code></pre>
</blockquote>
<p>其工作流程为:</p>
<ol>
<li>主线程实现连接监听只处理网络I/O连接事件</li>
<li>新的连接socket分发至工作线程中这个socket上的I/O事件都由该工作线程处理工作线程都可以处理多个socket 的I/O事件</li>
<li>工作线程独立维护自己的事件循环监听不同连接socket的I/O事件。</li>
</ol>
<h3>Follower/Leader 模式</h3>
<p>Follower/Leader是多个工作线程轮流进行事件监听、事件分发、处理事件的模式。</p>
<p>在Follower/Leader模式工作的任何一个时间点只有一个工作线程处理成为Leader 负责I/O事件监听而其他线程都是Follower并等待成为Leader。</p>
<p>Follower/Leader模式的工作流概述如下</p>
<ol>
<li>当前Leader Thread1监听到就绪事件后从Follower 线程集中推选出 Thread 2成为新的Leader</li>
<li>新的Leader Thread2 继续事件I/O监听</li>
<li>Thread1继续处理I/O就绪事件执行完后加入到Follower 线程集中等待成为Leader。</li>
</ol>
<p>从上描述Leader/Follower模式的工作线程存在三种状态工作线程同一时间只能处于一种状态这三种状态为</p>
<ul>
<li>Leader线程处于领导者状态负责监听I/O事件</li>
<li>Processing线程处理就绪I/O事件</li>
<li>Follower等待成为新的领导者或者可能被当前Leader指定处理就绪事件。</li>
</ul>
<p>Leader监听到I/O就绪事件后有两种处理方式:</p>
<ol>
<li>推选出新的Leader后并转移到Processing处理该I/O就绪事件</li>
<li>指定其他Follower 线程处理该I/O就绪事件此时保持Leader状态不变</li>
</ol>
<p>如图16所示为上面描述的三种状态的转移关系。</p>
<p><img src="assets/17.png" alt="img" /></p>
<blockquote>
<pre><code> 图16 Follower/Leader模式状态转移示意图
</code></pre>
</blockquote>
<p>如图16所示处于Processing状态的线程处理完I/O事件后若当前不存在Leader就自动提升为Leader否则转变Follower。</p>
<p>从以上描述中可知Follower/Leader模式中不需要在线程间传递数据线程间也不存在共享资源。但很明显Follower/Leader 仅支持一个事件处理源集无法做到图15所示的每个工作线程独立监听I/O事件。</p>
<h2>Swoole 网络模型分析</h2>
<p>Swoole为PHP提供I/O扩展功能支持异步I/O、同步I/O、并发通信并且为PHP多进程模式提供了并发数据结构和IPC通信机制Swoole 既可以充当网络I/O服务器也支持I/O客户端较大程度为用户简化了网络I/O、多进程多线程并发编程的工作。</p>
<p>Swoole作为server时支持3种运行模式分别是多进程模式、多线程模式、多进程多线程模式多进程多线程模式是其中最为复杂的方式其他两种方式可以认为是其特例。</p>
<p>本节结合之前介绍几种事件处理模型、并发模式来分析Swoole server的多进程多线程模型如图17。<img src="assets/2.png" alt="img" /></p>
<blockquote>
<pre><code> 图17 swoole server多进程多线程模型结构示意图
</code></pre>
</blockquote>
<p>图17所示整体上可以分为Master Process、Manger Process、Work Process Pool三部分。这三部分的主要功能</p>
<ol>
<li>**Master Process**监听服务端口接收用户连接收发连接数据依靠reactor模型驱动</li>
<li>**Manager Process**Master Process的子进程负责fork WorkProcess并监控Work Process的运行状态</li>
<li>**Work Process Pool**工作进程池与PHP业务层交互将客户端数据或者事件如连接关闭回调给业务层并将业务层的响应数据或者操作如主动关闭连接交给Master Process处理工作进程依靠reactor模型驱动。</li>
</ol>
<p>Manager Process 监控Work Process进程本节不做进一步讲解主要关注Master和Work。</p>
<h4><strong>Master Process</strong></h4>
<p>Master Process 内部包括主线程(Main Thread)和工作线程池(Work Thread Pool),这两部分主要功能分别是:</p>
<p><strong>主线程:</strong> 监听服务端口接收网络连接将成功建立的连接分发到线程池中依赖reactor模型驱动</p>
<p><strong>工作线程池:</strong> 独立管理连接收发网络数据依赖Reactor事件处理驱动。</p>
<p>顾一下前面介绍的半同步/半异步并发模式很明显主进程的工作方式就是图15所示的方式。</p>
<h4>Work Process</h4>
<p>如上所描述Work Process是Master Process和PHP层之间的媒介</p>
<ol>
<li>Work Process接收来自Master Process的数据包括网络数据和连接事件回调至PHP业务层</li>
<li>将来自PHP层的数据和连接控制信息发送给Master Process进程Master Process来处理。</li>
</ol>
<p>Work Process同样是依赖Reactor事件模型驱动其工作方式一个典型的Reactor模式。</p>
<p>Work Process作为Master Process和PHP层之间的媒介将数据收发操作和数据处理分离开来即使PHP层因消息处理将Work进程阻塞一段时间也不会对其他连接有影响。</p>
<p>从整体层面来看Master Process实现对连接socket上数据的I/O操作这个过程对于Work Process是异步的结合图11 所描述的同步I/O模拟Proactor模式两种方式如出一辙只不过这里使用的是多进程。</p>
<h4><strong>进程间通信</strong></h4>
<p>Work Process是Master Process和PHP层之间的媒介那么需要看看Work Process 与Master Process之间的通信方式并在Swoole server 的多进程多线程模型进程中整个过程还是有些复杂下面说明一下该流程如图18所示。
<img src="assets/-------12--2.png" alt="img" /></p>
<blockquote>
<pre><code> 图18 swoole server 多进程多线程通信示意图
</code></pre>
</blockquote>
<p>具体流程为:</p>
<ol>
<li>Master 进程主线程接收客户端连接连接建立成功后分发至工作线程工作线程通过Unix Socket通知Work进程连接信息</li>
<li>Work 进程将连接信息回调至PHP业务层</li>
<li>Maser 进程中的工作线程接收客户端请求消息并通过Unix Socket方式发送到Work进程</li>
<li>Work 进程将请求消息回调至PHP业务层</li>
<li>PHP业务层构造回复消息通过Work进程发送Work进程将回复消息拷贝至共享内存中并通过Unix Socket通知发送至Master进程的工作线程有数据需要发送</li>
<li>工作线程从共享内存中取出需发送的数据,并发送至客户端;</li>
<li>客户端断开连接工作线程将连接断开的事件通过UnixSocket发送至Work进程</li>
<li>Work进程将连接断开事件回调至PHP业务层.</li>
</ol>
<p>需要注意在步骤5中Work进程通知Master进程有数据需要发送不是将数据直接发送给Master进程而是将数据地址(在共享内存中)发送给Master进程。</p>
<h2>改善性能的方法</h2>
<p>性能对于服务器而言是非常敏感和重要的当前硬件的发展虽然不是服务器性能的瓶颈作为软件开发人员还是应该考虑在软件层面来上改善服务性能。好的网络模块除了稳定性还有非常多的细节、技巧处理来提升服务性能感兴趣的同学可以深入了解Ngnix源码的细节以及陈硕的《Linux多线程服务器编程》。</p>
<h3>数据复制</h3>
<p>如果应用程序不关心数据的内容就没有必要将数据拷贝到应用缓冲区可以借助内核接口直接将数据拷贝到内核缓冲区处理如在提供文件下载服务时不需要将文件内容先读到应用缓冲区在调用send接口发送出去可以直接使用sendfile (零拷贝)接口直接发送出去。</p>
<p>应用程序的工作模块之间也应该避免数据拷贝,如:</p>
<ol>
<li>当两个工作进程之间需要传递数据,可以考虑使用共享内存的方式实现数据共享;</li>
<li>在流媒体的应用中对帧数据的非必要拷贝会对程序性能的影响特备是在嵌入式环境中影响非常明显。通常采用的办法是给每帧数据分配内存下面统称为buffer当需要使用该buffer时会增加该buffer的引用计数buffer的引用计数为0时才会释放对应的内存。这种方式适合在进程内数据无拷贝传递并且不会给释放buffer带来困扰。</li>
</ol>
<h3>资源池</h3>
<p>在服务运行期间,需要使用系统调用为用户分配资源,通常系统资源的分配都是比较耗时的,如动态创建进程/线程。可以考虑在服务启动时预先分配资源,即创建资源池,当需要资源,从资源池中获取即可,若资源池不够用时,再动态的分配,使用完成后交还到资源池中。这实际上是用空间换取时间,在服务运行期间可以节省非必要的资源创建过程。需要注意的是,使用资源池还需要根据业务和硬件环境对资源池的大小进行限制。</p>
<p>资源池是一个抽象的概念,常见的包括进程池、线程池、 内存池、连接池;这些资源池的相关资料非常多,这里就不一一介绍了。</p>
<h3>锁/上下文切换</h3>
<p>1.<strong>关于锁</strong>
对共享资源的操作是并发程序中经常被提起的一个话题,都知道在业务逻辑上无法保证同步操作共享资源时,需要对共享资源加锁保护,但是锁不仅不能处理任何业务逻辑,而且还存在一定的系统开销。并且对锁的不恰当使用,可能成为服务期性能的瓶颈。</p>
<p>针对锁的使用有如下建议:</p>
<ol>
<li>如果能够在设计层面避免共享资源竞争就可以避免锁如图15描述的模式;</li>
<li>若无法避免对共享资源的竞争,优先考虑使用无锁队列的方式实现共享资源;</li>
<li>使用锁时,优先考虑使用读写锁;此外,锁的范围也要考虑,尽量较少锁的颗粒度,避免其他线程无谓的等待。</li>
</ol>
<p>2.<strong>上下文切换</strong>
并发程序需要考虑上下文切换的问题,内核调度线程(进程)执行是存在系统开销的,若线程(进程)调度占用CPU的时间比重过大那处理业务逻辑占用的CPU时间就会不足。在项目中线程(进程)数量越多上下文切换会很频繁因此是不建议为每个用户连接创建一个线程如图15所示的并发模式一个线程可同时处理多个用户连接是比较合理的解决方案。</p>
<p>多核的机器上并发程序的不同线程可以运行在不同的CPU上只要线程数量不大于CPU数目上下文切换不会有什么问题在实际的并发网络模块中线程(进程)的个数也是根据CPU数目来确定的。在多核机器上可以设置CPU亲和性将进程线程与CPU绑定提高CPU cache的命中率建好内存访问损耗。</p>
<h3>有限状态机器</h3>
<p>有限状态机是一种高效的逻辑处理方式在网络协议处理中应用非常广泛最典型的是内核协议栈中TCP状态转移。有限状态机中每种类型对应执行逻辑单元的状态对逻辑事务的处理非常有效。 有限状态机包括两种,一种是每个状态都是相互独立的,状态间不存在转移;另一种就是状态间存在转移。有限状态机比较容易理解,下面给出两种有限状态机的示例代码。</p>
<p><strong>不存在状态转移</strong></p>
<pre><code class="language-c">typedef enum _tag_state_enum{
A_STATE,
B_STATE,
C_STATE,
D_STATE
}state_enum;
void STATE_MACHINE_HANDLER(state_enum cur_state) {
switch (cur_state){
case A_STATE:
process_A_STATE();
break;
case B_STATE:
process_B_STATE();
break;
case C_STATE:
process_C_STATE();
break;
default:
break;
}
return ;
}
</code></pre>
<p><strong>存在状态转移</strong></p>
<pre><code class="language-c">void TRANS_STATE_MACHINE_HANDLER(state_enum cur_state) {
while (C_STATE != cur_state) {
switch (cur_state) {
case A_STATE:
process_A_STATE();
cur_state = B_STATE;
break;
case B_STATE:
process_B_STATE();
cur_state = C_STATE;
break;
case C_STATE:
process_C_STATE();
cur_state = D_STATE;
break;
default:
return ;
}
}
return ;
}
</code></pre>
<h3>时间轮</h3>
<p>经常会面临一些业务定时超时的需求,用例子来说明吧。</p>
<p><strong>功能需求</strong>服务器需要维护来自大量客户端的TCP连接假设单机服务器需要支持的最大TCP连接数在10W级别如果某连接上60s内没有数据到达就认为相应的客户端下线。</p>
<p>先介绍一下两种容易想到的解决方案,</p>
<p><strong>方案a</strong> <strong>轮询扫描</strong></p>
<p>处理过程为:</p>
<ol>
<li>维护一个map&lt;client_id, last_update_time &gt; 记录客户端最近一次的请求时间;</li>
<li>当client_id对应连接有数据到达时更新last_update_time</li>
<li>启动一个定时器轮询扫描map 中client_id 对应的last_update_time若超过 60s则认为对应的客户端下线。</li>
</ol>
<p>轮询扫描,只启动一个定时器,但轮询效率低,特别是服务器维护的连接数很大时,部分连接超时事件得不到及时处理。</p>
<p><strong>方案b</strong> <strong>多定时器触发</strong></p>
<p>处理过程为:</p>
<ol>
<li>维护一个map&lt;client_id, last_update_time &gt; 记录客户端最近一次的请求时间;</li>
<li>当某client_id 对应连接有数据到达时更新last_update_time同时为client_id启用一个定时器60s后触发;</li>
<li>当client_id对应的定时器触发后查看map中client_id对应的last_update_time是否超过60s若超时则认为对应客户端下线。</li>
</ol>
<p>多定时器触发,每次请求都要启动一个定时器,可以想象,消息请求非常频繁是,定时器的数量将会很庞大,消耗大量的系统资源。</p>
<p><strong>方案c 时间轮方案</strong></p>
<p>下面介绍一下利用时间轮的方式实现的一种高效、能批量的处理方案,先说一下需要的数据结构:</p>
<ol>
<li>创建0~60的数据构成环形队列time_wheelcurrent_index维护环形队列的当前游标如图19所示</li>
<li>数组元素是slot 结构slot是一个set&lt;client_id&gt;,构成任务集;</li>
<li>维护一个map&lt;client_id,index&gt;记录client_id 落在哪个slot上。</li>
</ol>
<p><img src="assets/1.png" alt="img" /></p>
<blockquote>
<pre><code> 图19 时间轮环形队列示意图
</code></pre>
</blockquote>
<p>执行过程为:</p>
<ol>
<li>启用一个定时器运行间隔1s更新current_index指向环形队列下一个元素0-&gt;1-&gt;2-&gt;3...-&gt;58-&gt;59-&gt;60...0</li>
<li>连接上数据到达时从map中获取client_id所在的slot在slot的set中删除该client_id</li>
<li>将client_id加入到current_index - 1锁标记的slot中</li>
<li>更新map中client_id 为current_id-1 。</li>
</ol>
<p><strong>与a、b两种方案相比方案c具有如下优势</strong></p>
<ol>
<li>只需要一个定时器运行间隔1sCPU消耗非常少</li>
<li>current_index 所标记的slot中的set不为空时set中的所有client_id对应的客户端均认为下线即批量超时。</li>
</ol>
<p>上面描述的时间轮处理方式会存在1s以内的误差若考虑实时性可以提高定时器的运行间隔另外该方案可以根据实际业务需求扩展到应用中。我们对Swoole的修改中包括对定时器进行了重构其中超时定时器采用的就是如上所描述的时间轮方案并且精度可控。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980565bdb8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>