learn.lianglianglee.com/专栏/Netty 核心原理剖析与 RPC 实践-完/04 事件调度层:为什么 EventLoop 是 Netty 的精髓?.md.html
2022-05-11 18:52:13 +08:00

1287 lines
33 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>04 事件调度层:为什么 EventLoop 是 Netty 的精髓?.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="/专栏/Netty 核心原理剖析与 RPC 实践-完/00 学好 Netty是你修炼 Java 内功的必经之路.md">00 学好 Netty是你修炼 Java 内功的必经之路.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/01 初识 Netty为什么 Netty 这么流行?.md">01 初识 Netty为什么 Netty 这么流行?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/02 纵览全局:把握 Netty 整体架构脉络.md">02 纵览全局:把握 Netty 整体架构脉络.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/03 引导器作用:客户端和服务端启动都要做些什么?.md">03 引导器作用:客户端和服务端启动都要做些什么?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Netty 核心原理剖析与 RPC 实践-完/04 事件调度层:为什么 EventLoop 是 Netty 的精髓?.md">04 事件调度层:为什么 EventLoop 是 Netty 的精髓?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/05 服务编排层Pipeline 如何协调各类 Handler .md">05 服务编排层Pipeline 如何协调各类 Handler .md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/06 粘包拆包问题:如何获取一个完整的网络包?.md">06 粘包拆包问题:如何获取一个完整的网络包?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/07 接头暗语:如何利用 Netty 实现自定义协议通信?.md">07 接头暗语:如何利用 Netty 实现自定义协议通信?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/08 开箱即用Netty 支持哪些常用的解码器?.md">08 开箱即用Netty 支持哪些常用的解码器?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/09 数据传输writeAndFlush 处理流程剖析.md">09 数据传输writeAndFlush 处理流程剖析.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/10 双刃剑:合理管理 Netty 堆外内存.md">10 双刃剑:合理管理 Netty 堆外内存.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/11 另起炉灶Netty 数据传输载体 ByteBuf 详解.md">11 另起炉灶Netty 数据传输载体 ByteBuf 详解.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/12 他山之石:高性能内存分配器 jemalloc 基本原理.md">12 他山之石:高性能内存分配器 jemalloc 基本原理.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/13 举一反三Netty 高性能内存管理设计(上).md">13 举一反三Netty 高性能内存管理设计(上).md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/14 举一反三Netty 高性能内存管理设计(下).md">14 举一反三Netty 高性能内存管理设计(下).md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/15 轻量级对象回收站Recycler 对象池技术解析.md">15 轻量级对象回收站Recycler 对象池技术解析.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/16 IO 加速:与众不同的 Netty 零拷贝技术.md">16 IO 加速:与众不同的 Netty 零拷贝技术.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/17 源码篇:从 Linux 出发深入剖析服务端启动流程.md">17 源码篇:从 Linux 出发深入剖析服务端启动流程.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/18 源码篇:解密 Netty Reactor 线程模型.md">18 源码篇:解密 Netty Reactor 线程模型.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/19 源码篇:一个网络请求在 Netty 中的旅程.md">19 源码篇:一个网络请求在 Netty 中的旅程.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/20 技巧篇Netty 的 FastThreadLocal 究竟比 ThreadLocal 快在哪儿?.md">20 技巧篇Netty 的 FastThreadLocal 究竟比 ThreadLocal 快在哪儿?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/21 技巧篇:延迟任务处理神器之时间轮 HashedWheelTimer.md">21 技巧篇:延迟任务处理神器之时间轮 HashedWheelTimer.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/22 技巧篇:高性能无锁队列 Mpsc Queue.md">22 技巧篇:高性能无锁队列 Mpsc Queue.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/23 架构设计:如何实现一个高性能分布式 RPC 框架.md">23 架构设计:如何实现一个高性能分布式 RPC 框架.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/24 服务发布与订阅:搭建生产者和消费者的基础框架.md">24 服务发布与订阅:搭建生产者和消费者的基础框架.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/25 远程通信:通信协议设计以及编解码的实现.md">25 远程通信:通信协议设计以及编解码的实现.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/26 服务治理:服务发现与负载均衡机制的实现.md">26 服务治理:服务发现与负载均衡机制的实现.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/27 动态代理:为用户屏蔽 RPC 调用的底层细节.md">27 动态代理:为用户屏蔽 RPC 调用的底层细节.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/28 实战总结RPC 实战总结与进阶延伸.md">28 实战总结RPC 实战总结与进阶延伸.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/29 编程思想Netty 中应用了哪些设计模式?.md">29 编程思想Netty 中应用了哪些设计模式?.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/30 实践总结Netty 在项目开发中的一些最佳实践.md">30 实践总结Netty 在项目开发中的一些最佳实践.md.html</a>
</li>
<li>
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/31 结束语 技术成长之路:如何打造自己的技术体系.md">31 结束语 技术成长之路:如何打造自己的技术体系.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>04 事件调度层:为什么 EventLoop 是 Netty 的精髓?</h1>
<p>你好,我是若地。通过前面课程的学习,我们已经知道 Netty 高性能的奥秘在于其 <strong>Reactor 线程模型。</strong> EventLoop 是 Netty Reactor 线程模型的核心处理引擎,那么它是如何高效地实现事件循环和任务处理机制的呢?本节课我们就一起学习 EventLoop 的实现原理和最佳实践。</p>
<h3>再谈 Reactor 线程模型</h3>
<p>网络框架的设计离不开 I/O 线程模型,线程模型的优劣直接决定了系统的吞吐量、可扩展性、安全性等。目前主流的网络框架几乎都采用了 I/O 多路复用的方案。Reactor 模式作为其中的事件分发器,负责将读写事件分发给对应的读写事件处理者。大名鼎鼎的 Java 并发包作者 Doug Lea<em>Scalable I/O in Java</em> 一文中阐述了服务端开发中 I/O 模型的演进过程。Netty 中<strong>三种 Reactor 线程模型</strong>也来源于这篇经典文章。下面我们对这三种 Reactor 线程模型做一个详细的分析。</p>
<h4>单线程模型</h4>
<p><img src="assets/Ciqc1F-ZNCCANWF0AAG4qWOzD48243.png" alt="1.png" />
<a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf]">摘自 Lea D. Scalable IO in Java</a></p>
<p>上图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等),都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:</p>
<ul>
<li>一个线程支持处理的连接数非常有限CPU 很容易打满,性能方面有明显瓶颈;</li>
<li>当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时;</li>
<li>线程在处理 I/O 事件时Select 无法同时处理连接建立、事件分发等操作;</li>
<li>如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用。</li>
</ul>
<h4>多线程模型</h4>
<p><img src="assets/Ciqc1F-ZNCyAeFxmAAH_Xaxv5gc975.png" alt="2.png" />
<a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf]">摘自 Lea D. Scalable IO in Java</a></p>
<p>由于单线程模型有性能方面的瓶颈多线程模型作为解决方案就应运而生了。Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外多线程模型其他的操作与单线程模型是类似的例如读取数据依然保留了串行化的设计。当客户端有数据发送至服务端时Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。</p>
<h4>主从多线程模型</h4>
<p><img src="assets/CgqCHl-ZNDiAPgGOAAHx74H-t44265.png" alt="3.png" />
<a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf]">摘自 Lea D. Scalable IO in Java</a></p>
<p>主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。</p>
<p>Netty 推荐使用主从多线程模型,这样就可以轻松达到成千上万规模的客户端连接。在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。</p>
<p>介绍了上述三种 Reactor 线程模型,再结合它们各自的架构图,我们能大致总结出 Reactor 线程模型运行机制的四个步骤,分别为<strong>连接注册</strong><strong>事件轮询</strong><strong>事件分发</strong><strong>任务处理</strong>,如下图所示。</p>
<p><img src="assets/CgqCHl-ZNEGAMU-zAAEsYdWKArA085.png" alt="4.png" /></p>
<ul>
<li>连接注册Channel 建立后,注册至 Reactor 线程中的 Selector 选择器。</li>
<li>事件轮询:轮询 Selector 选择器中已注册的所有 Channel 的 I/O 事件。</li>
<li>事件分发:为准备就绪的 I/O 事件分配相应的处理线程。</li>
<li>任务处理Reactor 线程还负责任务队列中的非 I/O 任务,每个 Worker 线程从各自维护的任务队列中取出任务异步执行。</li>
</ul>
<p>以上介绍了 Reactor 线程模型的演进过程和基本原理Netty 也同样遵循 Reactor 线程模型的运行机制,下面我们来了解一下 Netty 是如何实现 Reactor 线程模型的。</p>
<h3>Netty EventLoop 实现原理</h3>
<h4>EventLoop 是什么</h4>
<p>EventLoop 这个概念其实并不是 Netty 独有的,它是一种<strong>事件等待和处理的程序模型</strong>,可以解决多线程资源消耗高的问题。例如 Node.js 就采用了 EventLoop 的运行机制,不仅占用资源低,而且能够支撑了大规模的流量访问。</p>
<p>下图展示了 EventLoop 通用的运行模式。每当事件发生时,应用程序都会将产生的事件放入事件队列当中,然后 EventLoop 会轮询从队列中取出事件执行或者将事件分发给相应的事件监听者执行。事件执行的方式通常分为<strong>立即执行、延后执行、定期执行</strong>几种。</p>
<p><img src="assets/Ciqc1F-ZNFKAAZr4AANvWWMqnKw586.png" alt="5.png" /></p>
<h3>Netty 如何实现 EventLoop</h3>
<p>在 Netty 中 EventLoop 可以理解为 Reactor 线程模型的事件处理引擎,每个 EventLoop 线程都维护一个 Selector 选择器和任务队列 taskQueue。它主要负责处理 I/O 事件、普通任务和定时任务。</p>
<p>Netty 中推荐使用 NioEventLoop 作为实现类,那么 Netty 是如何实现 NioEventLoop 的呢?首先我们来看 NioEventLoop 最核心的 run() 方法源码,本节课我们不会对源码做深入的分析,只是先了解 NioEventLoop 的实现结构。</p>
<pre><code>protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false)); // 轮询 I/O 事件
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys(); // 处理 I/O 事件
} finally {
runAllTasks(); // 处理所有任务
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys(); // 处理 I/O 事件
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio); // 处理完 I/O 事件,再处理异步任务队列
}
}
} catch (Throwable t) {
handleLoopException(t);
}
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
</code></pre>
<p>上述源码的结构比较清晰NioEventLoop 每次循环的处理流程都包含事件轮询 select、事件处理 processSelectedKeys、任务处理 runAllTasks 几个步骤,是典型的 Reactor 线程模型的运行机制。而且 Netty 提供了一个参数 ioRatio可以调整 I/O 事件处理和任务处理的时间比例。下面我们将着重从<strong>事件处理</strong><strong>任务处理</strong>两个核心部分出发,详细介绍 Netty EventLoop 的实现原理。</p>
<h4>事件处理机制</h4>
<p><img src="assets/Ciqc1F-ZNGGAcJWSAATcxrhDB1U168.png" alt="6.png" /></p>
<p>结合 Netty 的整体架构,我们一起看下 EventLoop 的事件流转图,以便更好地理解 Netty EventLoop 的设计原理。NioEventLoop 的事件处理机制采用的是<strong>无锁串行化的设计思路</strong></p>
<ul>
<li><strong>BossEventLoopGroup</strong><strong>WorkerEventLoopGroup</strong> 包含一个或者多个 NioEventLoop。BossEventLoopGroup 负责监听客户端的 Accept 事件,当事件触发时,将事件注册至 WorkerEventLoopGroup 中的一个 NioEventLoop 上。每新建一个 Channel 只选择一个 NioEventLoop 与其绑定。所以说 Channel 生命周期的所有事件处理都是<strong>线程独立</strong>的,不同的 NioEventLoop 线程之间不会发生任何交集。</li>
<li>NioEventLoop 完成数据读取后,会调用绑定的 ChannelPipeline 进行事件传播ChannelPipeline 也是<strong>线程安全</strong>的,数据会被传递到 ChannelPipeline 的第一个 ChannelHandler 中。数据处理完成后,将加工完成的数据再传递给下一个 ChannelHandler整个过程是<strong>串行化</strong>执行,不会发生线程上下文切换的问题。</li>
</ul>
<p>NioEventLoop 无锁串行化的设计不仅使系统吞吐量达到最大化,而且降低了用户开发业务逻辑的难度,不需要花太多精力关心线程安全问题。虽然单线程执行避免了线程切换,但是它的缺陷就是不能执行时间过长的 I/O 操作,一旦某个 I/O 事件发生阻塞,那么后续的所有 I/O 事件都无法执行,甚至造成事件积压。在使用 Netty 进行程序开发时,我们一定要对 ChannelHandler 的实现逻辑有充分的风险意识。</p>
<p>NioEventLoop 线程的可靠性至关重要,一旦 NioEventLoop 发生阻塞或者陷入空轮询,就会导致整个系统不可用。在 JDK 中, Epoll 的实现是存在漏洞的,即使 Selector 轮询的事件列表为空NIO 线程一样可以被唤醒,导致 CPU 100% 占用。这就是臭名昭著的 JDK epoll 空轮询的 Bug。Netty 作为一个高性能、高可靠的网络框架,需要保证 I/O 线程的安全性。那么它是如何解决 JDK epoll 空轮询的 Bug 呢?实际上 Netty 并没有从根源上解决该问题,而是巧妙地规避了这个问题。</p>
<p>我们抛开其他细枝末节,直接定位到事件轮询 select() 方法中的最后一部分代码,一起看下 Netty 是如何解决 epoll 空轮询的 Bug。</p>
<pre><code>long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) &gt;= currentTimeNanos) {
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD &gt; 0 &amp;&amp;
selectCnt &gt;= SELECTOR_AUTO_REBUILD_THRESHOLD) {
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
</code></pre>
<p>Netty 提供了一种检测机制判断线程是否可能陷入空轮询,具体的实现方式如下:</p>
<ol>
<li>每次执行 Select 操作之前记录当前时间 currentTimeNanos。</li>
<li>time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) &gt;= currentTimeNanos如果事件轮询的持续时间大于等于 timeoutMillis那么说明是正常的否则表明阻塞时间并未达到预期可能触发了空轮询的 Bug。</li>
<li>Netty 引入了计数变量 selectCnt。在正常情况下selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到 SELECTOR_AUTO_REBUILD_THRESHOLD默认512 阈值时,会触发重建 Selector 对象。</li>
</ol>
<p>Netty 采用这种方法巧妙地规避了 JDK Bug。异常的 Selector 中所有的 SelectionKey 会重新注册到新建的 Selector 上,重建完成之后异常的 Selector 就可以废弃了。</p>
<h4>任务处理机制</h4>
<p>NioEventLoop 不仅负责处理 I/O 事件,还要兼顾执行任务队列中的任务。任务队列遵循 FIFO 规则可以保证任务执行的公平性。NioEventLoop 处理的任务类型基本可以分为三类。</p>
<ol>
<li><strong>普通任务</strong>:通过 NioEventLoop 的 execute() 方法向任务队列 taskQueue 中添加任务。例如 Netty 在写数据时会封装 WriteAndFlushTask 提交给 taskQueue。taskQueue 的实现类是多生产者单消费者队列 MpscChunkedArrayQueue在多线程并发添加任务时可以保证线程安全。</li>
<li><strong>定时任务</strong>:通过调用 NioEventLoop 的 schedule() 方法向定时任务队列 scheduledTaskQueue 添加一个定时任务,用于周期性执行该任务。例如,心跳消息发送等。定时任务队列 scheduledTaskQueue 采用优先队列 PriorityQueue 实现。</li>
<li><strong>尾部队列</strong>tailTasks 相比于普通任务队列优先级较低,在每次执行完 taskQueue 中任务后会去获取尾部队列中任务执行。尾部任务并不常用,主要用于做一些收尾工作,例如统计事件循环的执行时间、监控信息上报等。</li>
</ol>
<p>下面结合任务处理 runAllTasks 的源码结构,分析下 NioEventLoop 处理任务的逻辑,源码实现如下:</p>
<pre><code>protected boolean runAllTasks(long timeoutNanos) {
// 1. 合并定时任务到普通任务队列
fetchFromScheduledTaskQueue();
// 2. 从普通任务队列中取出任务
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
// 3. 计算任务处理的超时时间
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
// 4. 安全执行任务
safeExecute(task);
runTasks ++;
// 5. 每执行 64 个任务检查一下是否超时
if ((runTasks &amp; 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime &gt;= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
// 6. 收尾工作
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
</code></pre>
<p>我在代码中以注释的方式标注了具体的实现步骤,可以分为 6 个步骤。</p>
<ol>
<li>fetchFromScheduledTaskQueue 函数:将定时任务从 scheduledTaskQueue 中取出,聚合放入普通任务队列 taskQueue 中,只有定时任务的截止时间小于当前时间才可以被合并。</li>
<li>从普通任务队列 taskQueue 中取出任务。</li>
<li>计算任务执行的最大超时时间。</li>
<li>safeExecute 函数:安全执行任务,实际直接调用的 Runnable 的 run() 方法。</li>
<li>每执行 64 个任务进行超时时间的检查,如果执行时间大于最大超时时间,则立即停止执行任务,避免影响下一轮的 I/O 事件的处理。</li>
<li>最后获取尾部队列中的任务执行。</li>
</ol>
<h3>EventLoop 最佳实践</h3>
<p>在日常开发中用好 EventLoop 至关重要,这里结合实际工作中的经验给出一些 EventLoop 的最佳实践方案。</p>
<ol>
<li>网络连接建立过程中三次握手、安全认证的过程会消耗不少时间。这里建议采用 Boss 和 Worker 两个 EventLoopGroup有助于分担 Reactor 线程的压力。</li>
<li>由于 Reactor 线程模式适合处理耗时短的任务场景,对于耗时较长的 ChannelHandler 可以考虑维护一个业务线程池,将编解码后的数据封装成 Task 进行异步处理,避免 ChannelHandler 阻塞而造成 EventLoop 不可用。</li>
<li>如果业务逻辑执行时间较短,建议直接在 ChannelHandler 中执行。例如编解码操作,这样可以避免过度设计而造成架构的复杂性。</li>
<li>不宜设计过多的 ChannelHandler。对于系统性能和可维护性都会存在问题在设计业务架构的时候需要明确业务分层和 Netty 分层之间的界限。不要一味地将业务逻辑都添加到 ChannelHandler 中。</li>
</ol>
<h3>总结</h3>
<p>本节课我们一起学习了 Netty Reactor 线程模型的核心处理引擎 EventLoop熟悉了 EventLoop 的来龙去脉。结合 Reactor 主从多线程模型,我们对 Netty EventLoop 的功能用处做一个简单的归纳总结。</p>
<ul>
<li>MainReactor 线程:处理客户端请求接入。</li>
<li>SubReactor 线程数据读取、I/O 事件的分发与执行。</li>
<li>任务处理线程:用于执行普通任务或者定时任务,如空闲连接检测、心跳上报等。</li>
</ul>
<p>EventLoop 的设计思想被运用于较多的高性能框架中,如 Redis、Nginx、Node.js 等,它的设计原理是否对你有所启发呢?在后续源码篇的章节中我们将进一步介绍 EventLoop 的源码实现,吃透 EventLoop 这个死循环,可以说你就是一个 Netty 专家了。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/03 引导器作用:客户端和服务端启动都要做些什么?.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/05 服务编排层Pipeline 如何协调各类 Handler .md">下一页</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":"70997343aba03d60","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>