This commit is contained in:
周伟
2022-05-11 18:46:27 +08:00
commit 387f48277a
8634 changed files with 2579564 additions and 0 deletions

View File

@@ -0,0 +1,842 @@
<!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>00 学好 Netty是你修炼 Java 内功的必经之路.md</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 class="current-tab" 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 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>00 学好 Netty是你修炼 Java 内功的必经之路</h1>
<p>你好,我是若地。我曾担任美团点评技术专家,是一名高性能组件发烧友,平时专注于基础架构中间件的研发工作,积累了丰富的分布式架构设计和调优经验。</p>
<p>我们知道网络层是架构设计中至关重要的环节,但 Java 的网络编程框架有很多(比如 Java NIO、Mina、Grizzy为什么我这里只推荐 Netty 呢?</p>
<p>因为 Netty 是目前最流行的一款高性能 Java 网络编程框架,它被广泛使用在中间件、直播、社交、游戏等领域。目前,许多知名的开源软件也都将 Netty 用作网络通信的底层框架,如 Dubbo、RocketMQ、Elasticsearch、HBase 等。</p>
<h3>为什么要学习 Netty</h3>
<p>讲到这里,你可能要问了:如果我的工作中涉及网络编程的内容并不多,那我是否还有必要花精力学习 Netty 呢?</p>
<p>其实在互联网大厂(阿里、腾讯、美团等)的中高级 Java 开发面试中,经常会问到涉及 Netty 核心技术原理的问题,比如:</p>
<ol>
<li>Netty 的高性能表现在哪些方面?对你平时的项目开发有何启发?</li>
<li>Netty 中有哪些重要组件,它们之间有什么联系?</li>
<li>Netty 的内存池、对象池是如何设计的?</li>
<li>针对 Netty 你有哪些印象比较深刻的系统调优案例?</li>
</ol>
<p>这些问题看似简单,但如果你对 Netty 掌握不够深入,回答时就很容易“翻车”。我面试过很多求职者,虽然他们都有一定的 Netty 使用经验,但当深入探讨技术细节及如何解决项目中的实际问题时,就会发现大部分人只是简单使用,并没有深入掌握 Netty 的技术原理。<strong>如果你可以学好 Netty掌握底层原理一定会成为你求职面试的加分项。</strong></p>
<p><strong>而且通过 Netty 的学习,还可以锻炼你的编程思维,对 Java 其他的知识体系起到融会贯通的作用。</strong></p>
<p>当年我刚踏入工作,领到的第一个任务是数据采集和上报。我尝试了各种解决方案最后都被主管否掉了,他说“不用那么麻烦,直接使用 Netty 就好了”。于是我一边学习一边完成工作,工作之余还会挤出时间研究 Netty 源码。</p>
<p>回想起研究源码的那段日子,虽然很辛苦,但仿佛为我<strong>打开了一扇 Java 新世界的大门</strong>,当我理解领悟 Netty 的设计原理之后,对 I/O 模型 、内存管理、线程模型、数据结构等当时理解起来有一定难度的知识,仿佛一瞬间“顿悟”了。而且在我日后再去学习 RocketMQ、Nginx、Redis 等优秀框架时,也明显感觉更加便捷、高效了。</p>
<p>因此,如果你想提升自己的技术水平并找到一份满意的工作,学习掌握 Netty 就非常重要。事实上,在平时的开发工作中,<strong>Netty 的易用性和可靠性也极大程度上降低了开发者的心智负担。</strong></p>
<p>我在学生时代,写过不少网络应用,现在看来,非常冗长。当我熟练掌握 Netty 后一切问题迎刃而解。Netty 对 Java NIO 进行了高级封装,简化了网络应用的开发过程,我们不再需要花费大量精力关注 Selector、SocketChannel、ServerSocketChannel 等繁杂的 API。</p>
<p>当我自己写网络应用时,拆包/粘包、数据编解码、TCP 断线重连等一系列问题都需要考虑到,而现在 Netty 给我们提供了现成的解决方案。此外遇到问题还可以在社区讨论Netty 的迭代周期短修复问题快,其可靠性和健壮性被越来越多的公司所认可和采纳。</p>
<p>不客气地说,<strong>正是因为有 Netty 的存在,网络编程领域 Java 才得以与 C++ 并肩而立</strong></p>
<p>由以上几点出发,我想和你一起学习 Netty希望在工作和求职的过程中能够为你提供帮助也可以为你打开学习思路。</p>
<h3>学习目标与困难</h3>
<p>那么我们该如何学习 Netty 技术呢?作为初学者,你一定会有很多疑问或遇到一些问题:</p>
<ul>
<li>缺乏网络相关的基础知识,学习 Netty 往往理解不深刻,始终不得其法;</li>
<li>Netty 知识点非常多,网上资源比较零散,社区文档对初学者也不够友好,如何系统化学习 Netty</li>
<li>看了这么多 Netty 的基础理论,落到项目开发中却依然毫无头绪;</li>
<li>Netty 源码过于复杂,学习无从下手,抓不住重点,最终半途而废;</li>
<li>工作中缺少实践,仅仅学习理论知识很容易就忘记了。</li>
</ul>
<p>在学习的过程中我也遇到了同样的问题,但幸运的是美团的工作经历让我有了很多实践和解决问题的机会。在这期间,我在系统设计方面不断有新的认知。</p>
<p>这里我想分享一些我的学习经验,供你一同学习。学习方法不但适合 Netty也适合其他技术。希望通过这些经验可以一同进步。</p>
<ul>
<li>首先,兴趣是最好的老师,工作之余我一定会分配出至少 10% 的时间去思考和学习新的知识,像 Netty 如此优秀的学习资源当然不能放过。</li>
<li>其次,如果你工作中缺乏项目实战,其实也不必过于担心,可以尝试实现一些 MVP 的原型系统,例如 RPC、IM 即时聊天HTTP 服务器等。不要觉得这是在浪费时间,实践出真知,在学习 Netty 的同时你也会得到很多收获。</li>
<li>再次,在学习源码之前,首先要让自己成为一个熟练工,掌握基本理论。事实上,不论是学习什么框架,我会先尝试挑战自己。我在心中问自己:“我会如何设计它的架构?”然后再去学习相关的博客、源码等资源,思考作者的设计为什么与自己完全不一样?两者设计的差别在哪里?</li>
<li>最终,反复学习也很重要。有时在汲取新知识的时候会对之前的知识点理解产生新的想法,我会带着疑问去把相关的知识重新学习一遍,打破砂锅问到底,经常收获满满。</li>
</ul>
<h3>Netty 的学习路径</h3>
<p>如果现阶段的你:</p>
<ol>
<li>具备一定的 Java 基础,需要深入学习一款开源框架提升能力和开拓视野;</li>
<li>希望自己在求职面试中增加闪光点,成为精通 Netty 的硬核程序员;</li>
<li>想系统学习 Netty 服务端开发,并希望通过实战来加深理解;</li>
<li>正在从事网络、分布式服务框架等方向的工作,期望自己成为该领域的专家。</li>
</ol>
<p>那么这个课程就是为你量身定做的,课程中我会<strong>结合高频的面试题</strong>,从源码出发剖析 Netty 的核心技术原理,同时将这么多年使我受益匪浅的一些<strong>编程思想</strong><strong>实战经验</strong>分享给你,帮助你在工作中学以致用,避免踩坑。</p>
<p>在这里我也总结归纳出一份 Netty 核心知识点的思维导图,希望可以帮助你梳理本专栏的整体知识脉络。我会由浅入深地带你建立起完整的 Netty 知识体系,夯实你的 Netty 基础知识、Netty 进阶技能、实战开发经验。</p>
<p><img src="assets/CgqCHl-NAQaABGcDAAZa0pmBs40719.png" alt="image " /></p>
<ul>
<li><strong>夯实 Netty 基础知识</strong>:第一、二部分介绍 Netty 的全貌,了解 Netty 的发展现状和技术架构,并且逐一讲解了 Netty 的核心组件原理和使用,以及网络通信必不可少的编解码技能,为后面的源码解析和实践环节打下基础。</li>
<li><strong>Netty 进阶技能</strong>:第三部分讲解 Netty 的内存管理,并希望通过对比介绍 Nginx、Redis 两个著名的开源软件,帮你达到举一反三的能力。第四部分结合高频的面试问题,通过多解读剖析 Netty 的核心源码,帮助你快速准确地理解 Netty 高性能的技术原理,对其中的设计思想学以致用。</li>
<li><strong>实战开发经验</strong>:课程最后带你从 0 到 1 打造一个高性能分布式 RPC 框架,并针对 RPC 框架的核心要点,帮助你掌握网络编程的技巧,加深对 Netty 的理解。</li>
</ul>
<p>除了上述内容,你还可以通过本专栏获得一些额外的福利。</p>
<ul>
<li>万丈高楼平地起,课程会穿插必备的 Linux 网络编程基础知识,助你理解 Netty 时事半功倍。</li>
<li>Netty 源码的调试经验和技巧,从源码中我们可以学习到优秀的设计思想和技巧。</li>
<li>Netty 在实际的项目实践中踩过哪些坑?最佳实践应该是什么?</li>
<li>利用 Netty 如何快速搭建一套高性能的分布式 RPC 框架?我会一步步带你完成这个 MVP 原型。</li>
<li>在技术道路上如何升级打怪?告诉你我是如何学习和打造自己的技术体系的。</li>
</ul>
<p>讲到最后,相信你一定对学习 Netty 满怀激情,那么一起来解锁 Netty 这项技能吧,也欢迎你留言和我一起交流和讨论。希望你能够将 Netty 这门技术融会贯通,让你的开发实践与职业发展走得更加顺利、长远!</p>
</div>
</div>
<div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/01 初识 Netty为什么 Netty 这么流行?.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":"7099733a58bb3d60","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>

View File

@@ -0,0 +1,932 @@
<!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>01 初识 Netty为什么 Netty 这么流行?.md</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 class="current-tab" 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 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>01 初识 Netty为什么 Netty 这么流行?</h1>
<p>你好,我是若地。今天我们将正式开始学习本专栏,一同了解一下 Netty。</p>
<p>众所周知Java 的生态非常完善,同一类型的需求可能会有几款产品供你选择。那为什么 Java 的网络编程框架大家都会向你推荐 Netty而不是 Java NIO、Mina、Grizzy 呢?</p>
<p>本节课,我们就一起来看看 Netty 为什么这么流行,它到底解决了什么问题,以及目前它的发展现状,让你对 Netty 有一个全面的认识。</p>
<h3>为什么选择 Netty</h3>
<p>Netty 是一款用于高效开发网络应用的 NIO 网络框架,它大大简化了网络应用的开发过程。我们所熟知的 TCP 和 UDP 的 Socket 服务器开发,就是一个有关 Netty 简化网络应用开发的典型案例。</p>
<p>既然 Netty 是网络应用框架,那我们永远绕不开以下几个核心关注点:</p>
<ul>
<li><strong>I/O 模型、线程模型和事件处理机制;</strong></li>
<li><strong>易用性 API 接口;</strong></li>
<li><strong>对数据协议、序列化的支持。</strong></li>
</ul>
<p>我们之所以会最终选择 Netty是因为 Netty 围绕这些核心要点可以做到尽善尽美其健壮性、性能、可扩展性在同领域的框架中都首屈一指。下面我们从以下三个方面一起来看看Netty 到底有多厉害。</p>
<h4>高性能,低延迟</h4>
<p>经常听到这么一句话:“网络编程只要你使用了 Netty 框架,你的程序性能基本就不会差。”这句话虽然有些绝对,但是也从侧面上反映了人们对 Netty 高性能的肯定。</p>
<p>实现高性能的网络应用框架离不开 I/O 模型问题,在了解 Netty 高性能原理之前我们需要先储备 I/O 模型的基本知识。</p>
<p>I/O 请求可以分为两个阶段,分别为调用阶段和执行阶段。</p>
<ul>
<li>第一个阶段为<strong>I/O 调用阶段</strong>,即用户进程向内核发起系统调用。</li>
<li>第二个阶段为<strong>I/O 执行阶段</strong>。此时,内核等待 I/O 请求处理完成返回。该阶段分为两个过程:首先等待数据就绪,并写入内核缓冲区;随后将内核缓冲区数据拷贝至用户态缓冲区。</li>
</ul>
<p>为了方便大家理解,可以看一下这张图:</p>
<p><img src="assets/Ciqc1F-NAZ6Ae3bPAAHigveMsIQ514.png" alt="Drawing 0.png" /></p>
<p>接下来我们来回顾一下 Linux 的 5 种主要 I/O 模式,并看下各种 I/O 模式的优劣势都在哪里?</p>
<h5>1. 同步阻塞 I/OBIO</h5>
<p><img src="assets/CgqCHl-OnUKAeEELAAEnHU3FHGA343.png" alt="1.png" /></p>
<p>如上图所表现的那样,应用进程向内核发起 I/O 请求,发起调用的线程一直等待内核返回结果。一次完整的 I/O 请求称为BIOBlocking IO阻塞 I/O所以 BIO 在实现异步操作时,<strong>只能使用多线程模型</strong>,一个请求对应一个线程。但是,线程的资源是有限且宝贵的,创建过多的线程会增加线程切换的开销。</p>
<h5>2. 同步非阻塞 I/ONIO</h5>
<p><img src="assets/Ciqc1F-OnTeAFLNhAAFptS-OxRY266.png" alt="2.png" /></p>
<p>在刚介绍完 BIO 的网络模型之后NIO 自然就很好理解了。</p>
<p>如上图所示,应用进程向内核发起 I/O 请求后不再会同步等待结果而是会立即返回通过轮询的方式获取请求结果。NIO 相比 BIO 虽然大幅提升了性能,但是轮询过程中大量的系统调用导致上下文切换开销很大。所以,<strong>单独使用非阻塞 I/O 时效率并不高</strong>,而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。</p>
<h5>3. I/O 多路复用</h5>
<p><img src="assets/CgqCHl-OnV2ADXBhAAFUZ6oiz6U529.png" alt="3.png" /></p>
<p>多路复用实现了<strong>一个线程处理多个 I/O 句柄的操作</strong>。多路指的是多个<strong>数据通道</strong>,复用指的是使用一个或多个固定线程来处理每一个 Socket。select、poll、epoll 都是 I/O 多路复用的具体实现,线程一次 select 调用可以获取内核态中多个数据通道的数据状态。多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。</p>
<h5>4. 信号驱动 I/O</h5>
<p><img src="assets/CgqCHl-OnWqAddLWAAFUtZ6YHDA683.png" alt="4.png" /></p>
<p>信号驱动 I/O 并不常用,它是一种半异步的 I/O 模型。在使用信号驱动 I/O 时,当数据准备就绪后,内核通过发送一个 SIGIO 信号通知应用进程,应用进程就可以开始读取数据了。</p>
<h5>5. 异步 I/O</h5>
<p><img src="assets/Ciqc1F-OnXSAHOGVAACvxV3_3Mk188.png" alt="5.png" /></p>
<p>异步 I/O 最重要的一点是从内核缓冲区拷贝数据到用户态缓冲区的过程也是由系统异步完成,应用进程只需要在指定的数组中引用数据即可。<strong>异步 I/O 与信号驱动 I/O 这种半异步模式的主要区别</strong>:信号驱动 I/O 由内核通知何时可以开始一个 I/O 操作,而异步 I/O 由内核通知 I/O 操作何时已经完成。</p>
<p>了解了上述五种 I/O我们再来看 Netty 如何实现自己的 I/O 模型。Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 JDK NIO 框架的多路复用器 Selector。一个多路复用器 Selector 可以同时轮询多个 Channel采用 epoll 模式后,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。</p>
<p>在 I/O 多路复用的场景下当有数据处于就绪状态后需要一个事件分发器Event Dispather它负责将读写事件分发给对应的读写事件处理器Event Handler。事件分发器有两种设计模式Reactor 和 Proactor<strong>Reactor 采用同步 I/O Proactor 采用异步 I/O</strong></p>
<p>Reactor 实现相对简单,适合处理耗时短的场景,对于耗时长的 I/O 操作容易造成阻塞。Proactor 性能更高,但是实现逻辑非常复杂,目前主流的事件驱动模型还是依赖 select 或 epoll 来实现。</p>
<p><img src="assets/Ciqc1F-NKE-AWqZfAARsOnKW3pg690.png" alt="6.png" /></p>
<p><a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf]">摘自 Lea D. Scalable IO in Java</a> </p>
<p>上图所描述的便是 Netty 所采用的主从 Reactor 多线程模型,所有的 I/O 事件都注册到一个 I/O 多路复用器上,当有 I/O 事件准备就绪后I/O 多路复用器会将该 I/O 事件通过事件分发器分发到对应的事件处理器中。该线程模型避免了同步问题以及多线程切换带来的资源开销,真正做到高性能、低延迟。</p>
<h4>完美弥补 Java NIO 的缺陷</h4>
<p>在 JDK 1.4 投入使用之前,只有 BIO 一种模式。开发过程相对简单。新来一个连接就会创建一个新的线程处理。随着请求并发度的提升BIO 很快遇到了性能瓶颈。JDK 1.4 以后开始引入了 NIO 技术,支持 select 和 pollJDK 1.5 支持了 epollJDK 1.7 发布了 NIO2支持 AIO 模型。Java 在网络领域取得了长足的进步。</p>
<p>既然 JDK NIO 性能已经非常优秀,为什么还要选择 Netty这是因为 Netty 做了 JDK 该做的事,但是做得更加完备。我们一起看下 Netty 相比 JDK NIO 有哪些突出的优势。</p>
<ul>
<li><strong>易用性。</strong> 我们使用 JDK NIO 编程需要了解很多复杂的概念,比如 Channels、Selectors、Sockets、Buffers 等编码复杂程度令人发指。相反Netty 在 NIO 基础上进行了更高层次的封装,屏蔽了 NIO 的复杂性Netty 封装了更加人性化的 API统一的 API阻塞/非阻塞) 大大降低了开发者的上手难度与此同时Netty 提供了很多开箱即用的工具,例如常用的行解码器、长度域解码器等,而这些在 JDK NIO 中都需要你自己实现。</li>
<li><strong>稳定性。</strong> Netty 更加可靠稳定,修复和完善了 JDK NIO 较多已知问题,例如臭名昭著的 select 空转导致 CPU 消耗 100%TCP 断线重连keep-alive 检测等问题。</li>
<li><strong>可扩展性。</strong> Netty 的可扩展性在很多地方都有体现,这里我主要列举其中的两点:一个是可定制化的线程模型,用户可以通过启动的配置参数选择 Reactor 线程模型;另一个是可扩展的事件驱动模型,将框架层和业务层的关注点分离。大部分情况下,开发者只需要关注 ChannelHandler 的业务逻辑实现。</li>
</ul>
<h4>更低的资源消耗</h4>
<p>作为网络通信框架,需要处理海量的网络数据,那么必然面临有大量的网络对象需要创建和销毁的问题,对于 JVM GC 并不友好。为了降低 JVM 垃圾回收的压力Netty 主要采用了两种优化手段:</p>
<ul>
<li><strong>对象池复用技术。</strong> Netty 通过复用对象,避免频繁创建和销毁带来的开销。</li>
<li><strong>零拷贝技术。</strong> 除了操作系统级别的零拷贝技术外Netty 提供了更多面向用户态的零拷贝技术,例如 Netty 在 I/O 读写时直接使用 DirectBuffer从而避免了数据在堆内存和堆外内存之间的拷贝。</li>
</ul>
<p>因为 Netty 不仅做到了高性能、低延迟以及更低的资源消耗,还完美弥补了 Java NIO 的缺陷,所以在网络编程时越来越受到开发者们的青睐。</p>
<h3>网络框架的选型</h3>
<p>很多开发者都使用过 TomcatTomcat 作为一款非常优秀的 Web 服务器看上去已经帮我们解决了类似问题,那么它与 Netty 到底有什么不同?</p>
<p>Netty 和 Tomcat 最大的区别在于对<strong>通信协议</strong>的支持,可以说 Tomcat 是一个 HTTP Server它主要解决 HTTP 协议层的传输,而 Netty 不仅支持 HTTP 协议,还支持 SSH、TLS/SSL 等多种应用层的协议,而且能够自定义应用层协议。</p>
<p>Tomcat 需要遵循 Servlet 规范,在 Servlet 3.0 之前采用的是同步阻塞模型Tomcat 6.x 版本之后已经支持 NIO性能得到较大提升。然而 Netty 与 Tomcat 侧重点不同,所以不需要受到 Servlet 规范的约束,可以最大化发挥 NIO 特性。</p>
<p>如果你仅仅需要一个 HTTP 服务器,那么我推荐你使用 Tomcat。术业有专攻Tomcat 在这方面的成熟度和稳定性更好。但如果你需要做面向 TCP 的网络应用开发,那么 Netty 才是你最佳的选择。</p>
<p>此外,比较出名的网络框架还有 Mina 和 Grizzly。Mina 是 Apache Directory 服务器底层的 NIO 框架,由于 Mina 和 Netty 都是 Trustin Lee 主导的作品所以两者在设计理念上基本一致。Netty 出现的时间更晚,可以认为是 Mina 的升级版,解决了 Mina 一些设计上的问题。比如 Netty 提供了可扩展的编解码接口、优化了 ByteBuffer 的分配方式让用户使用起来更为便捷、安全。Grizzly 出身 Sun 公司,从设计理念上看没有 Netty 优雅,几乎是对 Java NIO 比较初级的封装,目前业界使用的范围也很小。</p>
<p>综上所述Netty 是我们一个较好的选择。</p>
<h3>Netty 的发展现状</h3>
<p>Netty 如此成功离不开社区的精心运营,迭代周期短且文档比较齐全,如果你遇到任何问题通过 issue 或者邮件都可以得到非常及时的答复。</p>
<p>你可以去官方社区学习相关资料,下面这些网站可以帮助你学习。</p>
<ul>
<li>官方社区。</li>
<li><a href="https://netty.io/">GitHub</a>。截止至 2020 年 7 月2.4w+ star一共被 4w+ 的项目所使用。</li>
</ul>
<p>Netty 官方提供 3.x、4.x 的稳定版本,之前一直处于测试阶段的 5.x 版本已被作者放弃维护。此前,官方从未对外发布过任何 5.x 的稳定版本。我在工作中也会碰到一些业务方在开发新项目时直接使用 Netty 5.x 版本的情况,这是因为不少人信任 Netty 社区,并认为这样可以避免以后升级。可惜这一省事之举随着 5.x 版本废弃后全白费了。不过这也给我们带来了一个经验教训:<strong>尽可能不要在生产环境使用任何非稳定版本的组件。</strong></p>
<p>如果没有项目历史包袱,目前主流推荐 Netty 4.x 的稳定版本Netty 3.x 到 4.x 版本发生了较大变化,属于不兼容升级,下面我们初步了解下 4.x 版本有哪些值得你关注的变化和新特性。</p>
<ul>
<li><strong>项目结构</strong>:模块化程度更高,包名从 org.jboss.netty 更新为 io.netty不再属于 Jboss。</li>
<li><strong>常用 API</strong>:大多 API 都已经支持流式风格,更多新的 API 参考以下网址:<a href="https://netty.io/news/2013/06/18/4-0-0-CR5.html">https://netty.io/news/2013/06/18/4-0-0-CR5.html</a></li>
<li><strong>Buffer 相关优化</strong>Buffer 相关功能调整了现在 5 点。
<ol>
<li>ChannelBuffer 变更为 ByteBufBuffer 相关的工具类可以独立使用。由于人性化的 Buffer API 设计,它已经成为 Java ByteBuffer 的完美替代品。</li>
<li>Buffer 统一为动态变化,可以更安全地更改 Buffer 的容量。</li>
<li>增加新的数据类型 CompositeByteBuf可以用于减少数据拷贝。</li>
<li>GC 更加友好增加池化缓存4.1 版本开始 jemalloc 成为默认内存分配方式。</li>
<li>内存泄漏检测功能。</li>
</ol>
</li>
<li><strong>通用工具类</strong>io.netty.util.concurrent 包中提供了较多异步编程的数据结构。</li>
<li><strong>更加严谨的线程模型控制</strong>,降低用户编写 ChannelHandler 的心智,不必过于担心线程安全问题。</li>
</ul>
<p>可见 Netty 4.x 带来了很多提升性能、健壮性都变得更加强大了。Netty 精益求精的设计精神值得每个人学习。当然,其中还有更多细节变化,感兴趣的同学可以参考以下网址:<a href="https://netty.io/wiki/new-and-noteworthy-in-4.0.html">https://netty.io/wiki/new-and-noteworthy-in-4.0.html</a>。如果你现在对这些概念还不是很清晰,也不必担心,专栏后续的内容中我都会具体讲解。</p>
<h3>谁在使用 Netty</h3>
<p>Netty 凭借其强大的社区影响力越来越多的公司逐渐采用Netty 作为他们的底层通信框架,下图中我列举了一些正在使用 Netty 的公司,一起感受下它的热度吧。</p>
<p><img src="assets/CgqCHl-NAo2AfuSQAAYvH4e8nWc146.png" alt="Drawing 7.png" /></p>
<p>Netty 经过很多出名产品在线上的大规模验证,其健壮性和稳定性都被业界认可,其中典型的产品有一下几个。</p>
<ul>
<li>服务治理Apache Dubbo、gRPC。</li>
<li>大数据Hbase、Spark、Flink、Storm。</li>
<li>搜索引擎Elasticsearch。</li>
<li>消息队列RocketMQ、ActiveMQ。</li>
</ul>
<p>还有更多优秀的产品我就不一一列举了,感兴趣的小伙伴可以参考下面网址:<a href="https://netty.io/wiki/related-projects.html">https://netty.io/wiki/related-projects.html</a></p>
<h3>总结</h3>
<p>作为正式学习专栏前的开胃餐,今天我主要向你介绍了 Netty 的优势与特色,同时提到了 I/O 多路复用、Reactor 设计模式、零拷贝等必备的知识点,帮助你对 Netty 有了基本的认识。相信你一定意犹未尽,在后续的章节中我们将逐步走进 Netty 的世界。</p>
<p>最后我也想给你留一个思考题Netty 的内部结构大概如何?为什么 Netty 能够成为如此优秀的工具?下节课我将为你解答这个问题。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/00 学好 Netty是你修炼 Java 内功的必经之路.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/02 纵览全局:把握 Netty 整体架构脉络.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":"7099733c8d4b3d60","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>

View File

@@ -0,0 +1,856 @@
<!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>06 粘包拆包问题:如何获取一个完整的网络包?.md</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 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 class="current-tab" 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>06 粘包拆包问题:如何获取一个完整的网络包?</h1>
<p>本节课开始我们将学习 Netty 通信过程中的编解码技术。编解码技术这是实现网络通信的基础,让我们可以定义任何满足业务需求的应用层协议。在网络编程中,我们经常会使用各种网络传输协议,其中 TCP 是最常用的协议。我们首先需要了解的是 TCP 最基本的<strong>拆包/粘包问题</strong>以及常用的解决方案,才能更好地理解 Netty 的编解码框架。</p>
<h3>为什么有拆包/粘包</h3>
<p>TCP 传输协议是面向流的,没有数据包界限。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。因此就有了拆包和粘包。</p>
<p>为什么会出现拆包/粘包现象呢?在网络通信的过程中,每次可以发送的数据包大小是受多种因素限制的,如 MTU 传输单元大小、MSS 最大分段大小、滑动窗口等。如果一次传输的网络包数据大小超过传输单元大小,那么我们的数据可能会拆分为多个数据包发送出去。如果每次请求的网络包数据都很小,一共请求了 10000 次TCP 并不会分别发送 10000 次。因为 TCP 采用的 Nagle 算法对此作出了优化。如果你是一位网络新手,可能对这些概念并不非常清楚。那我们先了解下计算机网络中 MTU、MSS、Nagle 这些基础概念以及它们为什么会造成拆包/粘包问题。</p>
<h4>MTU 最大传输单元和 MSS 最大分段大小</h4>
<p><strong>MTUMaxitum Transmission Unit</strong> 是链路层一次最大传输数据的大小。MTU 一般来说大小为 1500 byte。<strong>MSSMaximum Segement Size</strong> 是指 TCP 最大报文段长度它是传输层一次发送最大数据的大小。如下图所示MTU 和 MSS 一般的计算关系为MSS = MTU - IP 首部 - TCP首部如果 MSS + TCP 首部 + IP 首部 &gt; MTU那么数据包将会被拆分为多个发送。这就是拆包现象。</p>
<p><img src="assets/CgqCHl-iZjqAVNpwAAC-5hm9AJA479.png" alt="Drawing 1.png" /></p>
<h4>滑动窗口</h4>
<p><strong>滑动窗口</strong>是 TCP 传输层用于流量控制的一种有效措施,也被称为<strong>通告窗口</strong>。滑动窗口是数据接收方设置的窗口大小,随后接收方会把窗口大小告诉发送方,以此限制发送方每次发送数据的大小,从而达到流量控制的目的。这样数据发送方不需要每发送一组数据就阻塞等待接收方确认,允许发送方同时发送多个数据分组,每次发送的数据都会被限制在窗口大小内。由此可见,滑动窗口可以大幅度提升网络吞吐量。</p>
<p>那么 TCP 报文是怎么确保数据包按次序到达且不丢数据呢首先所有的数据帧都是有编号的TCP 并不会为每个报文段都回复 ACK 响应,它会对多个报文段回复一次 ACK。假设有三个报文段 A、B、C发送方先发送了B、C接收方则必须等待 A 报文段到达,如果一定时间内仍未等到 A 报文段,那么 B、C 也会被丢弃,发送方会发起重试。如果已接收到 A 报文段,那么将会回复发送方一次 ACK 确认。</p>
<h4>Nagle 算法</h4>
<p><strong>Nagle 算法</strong>于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。试想如果每次需要发送的数据只有 1 字节,加上 20 个字节 IP Header 和 20 个字节 TCP Header每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息这就造成了非常大的浪费。Nagle 算法可以理解为<strong>批量发送</strong>,也是我们平时编程中经常用到的优化思路,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。</p>
<p>Linux 在默认情况下是开启 Nagle 算法的,在大量小数据包的场景下可以有效地降低网络开销。但如果你的业务场景每次发送的数据都需要获得及时响应,那么 Nagle 算法就不能满足你的需求了,因为 Nagle 算法会有一定的数据延迟。你可以通过 Linux 提供的 TCP_NODELAY 参数禁用 Nagle 算法。Netty 中为了使数据传输延迟最小化,就默认禁用了 Nagle 算法,这一点与 Linux 操作系统的默认行为是相反的。</p>
<h3>拆包/粘包的解决方案</h3>
<p><img src="assets/CgqCHl-iZk2ALa_sAAD704YRY80575.png" alt="Drawing 3.png" /></p>
<p>在客户端和服务端通信的过程中,服务端一次读到的数据大小是不确定的。如上图所示,拆包/粘包可能会出现以下五种情况:</p>
<ul>
<li>服务端恰巧读到了两个完整的数据包 A 和 B没有出现拆包/粘包问题;</li>
<li>服务端接收到 A 和 B 粘在一起的数据包,服务端需要解析出 A 和 B</li>
<li>服务端收到完整的 A 和 B 的一部分数据包 B-1服务端需要解析出完整的 A并等待读取完整的 B 数据包;</li>
<li>服务端接收到 A 的一部分数据包 A-1此时需要等待接收到完整的 A 数据包;</li>
<li>数据包 A 较大,服务端需要多次才可以接收完数据包 A。</li>
</ul>
<p>由于拆包/粘包问题的存在,数据接收方很难界定数据包的边界在哪里,很难识别出一个完整的数据包。所以需要提供一种机制来识别数据包的界限,这也是解决拆包/粘包的唯一方法:<strong>定义应用层的通信协议</strong>。下面我们一起看下主流协议的解决方案。</p>
<h4>消息长度固定</h4>
<p>每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。</p>
<pre><code>+----+------+------+---+----+
| AB | CDEF | GHIJ | K | LM |
+----+------+------+---+----+
</code></pre>
<p>假设我们的固定长度为 4 字节,那么如上所示的 5 条数据一共需要发送 4 个报文:</p>
<pre><code>+------+------+------+------+
| ABCD | EFGH | IJKL | M000 |
+------+------+------+------+
</code></pre>
<p>消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。</p>
<h4>特定分隔符</h4>
<p>既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上<strong>特定分隔符</strong>,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \n 按行解析,即可得到 AB、CDEF、GHIJ、K、LM 五条原始报文。</p>
<pre><code>+-------------------------+
| AB\nCDEF\nGHIJ\nK\nLM\n |
+-------------------------+
</code></pre>
<p>由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。特定分隔符法在消息协议足够简单的场景下比较高效,例如大名鼎鼎的 Redis 在通信过程中采用的就是换行分隔符。</p>
<h4>消息长度 + 消息内容</h4>
<pre><code>消息头 消息体
+--------+----------+
| Length | Content |
+--------+----------+
</code></pre>
<p><strong>消息长度 + 消息内容</strong>是项目开发中最常用的一种协议,如上展示了该协议的基本格式。消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。接收方在解析数据时,首先读取消息头的长度字段 Len然后紧接着读取长度为 Len 的字节数据,该数据即判定为一个完整的数据报文。依然以上述提到的原始字节数据为例,使用该协议进行编码后的结果如下所示:</p>
<pre><code>+-----+-------+-------+----+-----+
| 2AB | 4CDEF | 4GHIJ | 1K | 2LM |
+-----+-------+-------+----+-----+
</code></pre>
<p>消息长度 + 消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。当然在消息头中不仅只限于存放消息的长度,而且可以自定义其他必要的扩展字段,例如消息版本、算法类型等。</p>
<h3>总结</h3>
<p>本节课我们详细讨论了 TCP 中的拆包/粘包问题,以及如何通过应用层的通信协议来解决拆包/粘包问题。其中基于消息长度 + 消息内容的变长协议是项目开发中最常用的一种方法,需要我们重点掌握,例如开源中间件 Dubbo、RocketMQ 等都基于该方法自定义了自己的通信协议,下节课我们将一起学习如何设计高效、可扩展、易维护的自定义网络通信协议。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/05 服务编排层Pipeline 如何协调各类 Handler .md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/07 接头暗语:如何利用 Netty 实现自定义协议通信?.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":"709973485ea23d60","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>

View File

@@ -0,0 +1,986 @@
<!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>10 双刃剑:合理管理 Netty 堆外内存.md</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 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 class="current-tab" 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>10 双刃剑:合理管理 Netty 堆外内存</h1>
<p>本节课我们将进入 Netty 内存管理的课程学习,在此之前,我们需要了解 Java 堆外内存的基本知识,因为当你在使用 Netty 时,需要时刻与堆外内存打交道。我们经常看到各类堆外内存泄漏的排查案例,堆外内存使用不当会使得应用出错、崩溃的概率变大,所以在使用堆外内存时一定要慎重,本节课我将带你一起认识堆外内存,并探讨如何更好地使用它。</p>
<h3>为什么需要堆外内存</h3>
<p>在 Java 中对象都是在堆内分配的,通常我们说的<strong>JVM 内存</strong>也就指的<strong>堆内内存</strong><strong>堆内内存</strong>完全被<strong>JVM 虚拟机</strong>所管理JVM 有自己的垃圾回收算法,对于使用者来说不必关心对象的内存如何回收。</p>
<p><strong>堆外内存</strong>与堆内内存相对应,对于整个机器内存而言,除<strong>堆内内存以外部分即为堆外内存</strong>,如下图所示。堆外内存不受 JVM 虚拟机管理,直接由操作系统管理。</p>
<p><img src="assets/CgqCHl-06zuAdxB_AAKnPyI9NhA898.png" alt="图片1.png" /></p>
<p>堆外内存和堆内内存各有利弊,这里我针对其中重要的几点进行说明。</p>
<ol>
<li>堆内内存由 JVM GC 自动回收内存,降低了 Java 用户的使用心智,但是 GC 是需要时间开销成本的,堆外内存由于不受 JVM 管理,所以在一定程度上可以降低 GC 对应用运行时带来的影响。</li>
<li>堆外内存需要手动释放,这一点跟 C/C++ 很像,稍有不慎就会造成应用程序内存泄漏,当出现内存泄漏问题时排查起来会相对困难。</li>
<li>当进行网络 I/O 操作、文件读写时,堆内内存都需要转换为堆外内存,然后再与底层设备进行交互,这一点在介绍 writeAndFlush 的工作原理中也有提到,所以直接使用堆外内存可以减少一次内存拷贝。</li>
<li>堆外内存可以实现进程之间、JVM 多实例之间的数据共享。</li>
</ol>
<p>由此可以看出,如果你想实现高效的 I/O 操作、缓存常用的对象、降低 JVM GC 压力,堆外内存是一个非常不错的选择。</p>
<h3>堆外内存的分配</h3>
<p>Java 中堆外内存的分配方式有两种:<strong>ByteBuffer#allocateDirect</strong><strong>Unsafe#allocateMemory</strong></p>
<p>首先我们介绍下 Java NIO 包中的 ByteBuffer 类的分配方式,使用方式如下:</p>
<pre><code>// 分配 10M 堆外内存
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
</code></pre>
<p>跟进 ByteBuffer.allocateDirect 源码,发现其中直接调用的 DirectByteBuffer 构造函数:</p>
<pre><code>DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa &amp;&amp; (base % ps != 0)) {
address = base + ps - (base &amp; (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
</code></pre>
<p>如下图所示,描述了 DirectByteBuffer 的内存引用情况,方便你更好地理解上述源码的初始化过程。在堆内存放的 DirectByteBuffer 对象并不大,仅仅包含堆外内存的地址、大小等属性,同时还会创建对应的 Cleaner 对象,通过 ByteBuffer 分配的堆外内存不需要手动回收,它可以被 JVM 自动回收。当堆内的 DirectByteBuffer 对象被 GC 回收时Cleaner 就会用于回收对应的堆外内存。</p>
<p><img src="assets/CgqCHl-060uANzIXAAK8c10kJxc818.png" alt="图片2.png" /></p>
<p>从 DirectByteBuffer 的构造函数中可以看出,真正分配堆外内存的逻辑还是通过 unsafe.allocateMemory(size),接下来我们一起认识下 Unsafe 这个神秘的工具类。</p>
<p>Unsafe 是一个非常不安全的类,它用于执行内存访问、分配、修改等<strong>敏感操作</strong>,可以越过 JVM 限制的枷锁。Unsafe 最初并不是为开发者设计的,使用它时虽然可以获取对底层资源的控制权,但也失去了安全性的保证,所以使用 Unsafe 一定要慎重。Netty 中依赖了 Unsafe 工具类,是因为 Netty 需要与底层 Socket 进行交互Unsafe 在提升 Netty 的性能方面起到了一定的帮助。</p>
<p>在 Java 中是不能直接使用 Unsafe 的,但是我们可以通过反射获取 Unsafe 实例,使用方式如下所示。</p>
<pre><code>private static Unsafe unsafe = null;
static {
try {
Field getUnsafe = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);
getUnsafe.setAccessible(true);
unsafe = (Unsafe) getUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
</code></pre>
<p>获得 Unsafe 实例后,我们可以通过 allocateMemory 方法分配堆外内存allocateMemory 方法返回的是内存地址,使用方法如下所示:</p>
<pre><code>// 分配 10M 堆外内存
long address = unsafe.allocateMemory(10 * 1024 * 1024);
</code></pre>
<p>与 DirectByteBuffer 不同的是Unsafe#allocateMemory 所分配的内存必须自己手动释放,否则会造成内存泄漏,这也是 Unsafe 不安全的体现。Unsafe 同样提供了内存释放的操作:</p>
<pre><code>unsafe.freeMemory(address);
</code></pre>
<p>到目前为止,我们了解了堆外内存分配的两种方式,对于 Java 开发者而言,常用的是 ByteBuffer.allocateDirect 分配方式,我们平时常说的堆外内存泄漏都与该分配方式有关,接下来我们一起看看使用 ByteBuffer 分配的堆外内存如何被 JVM 回收,这对我们排查堆外内存泄漏问题有较大的帮助。</p>
<h3>堆外内存的回收</h3>
<p>我们试想这么一种场景,因为 DirectByteBuffer 对象有可能长时间存在于堆内内存,所以它很可能晋升到 JVM 的老年代,所以这时候 DirectByteBuffer 对象的回收需要依赖 Old GC 或者 Full GC 才能触发清理。如果长时间没有 Old GC 或者 Full GC 执行,那么堆外内存即使不再使用,也会一直在占用内存不释放,很容易将机器的物理内存耗尽,这是相当危险的。</p>
<p>那么在使用 DirectByteBuffer 时我们如何避免物理内存被耗尽呢?因为 JVM 并不知道堆外内存是不是已经不足了,所以我们最好通过 JVM 参数 -XX:MaxDirectMemorySize 指定堆外内存的上限大小,当堆外内存的大小超过该阈值时,就会触发一次 Full GC 进行清理回收,如果在 Full GC 之后还是无法满足堆外内存的分配,那么程序将会抛出 OOM 异常。</p>
<p>此外在 ByteBuffer.allocateDirect 分配的过程中,如果没有足够的空间分配堆外内存,在 Bits.reserveMemory 方法中也会主动调用 System.gc() 强制执行 Full GC但是在生产环境一般都是设置了 -XX:+DisableExplicitGCSystem.gc() 是不起作用的,所以依赖 System.gc() 并不是一个好办法。</p>
<p>通过前面堆外内存分配方式的介绍,我们知道 DirectByteBuffer 在初始化时会创建一个 Cleaner 对象,它会负责堆外内存的回收工作,那么 Cleaner 是如何与 GC 关联起来的呢?</p>
<p>Java 对象有四种引用方式:强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。其中 PhantomReference 是最不常用的一种引用方式Cleaner 就属于 PhantomReference 的子类如以下源码所示PhantomReference 不能被单独使用,需要与引用队列 ReferenceQueue 联合使用。</p>
<pre><code>public class Cleaner extends java.lang.ref.PhantomReference&lt;java.lang.Object&gt; {
private static final java.lang.ref.ReferenceQueue&lt;java.lang.Object&gt; dummyQueue;
private static sun.misc.Cleaner first;
private sun.misc.Cleaner next;
private sun.misc.Cleaner prev;
private final java.lang.Runnable thunk;
public void clean() {}
}
</code></pre>
<p>首先我们看下当初始化堆外内存时内存中的对象引用情况如下图所示first 是 Cleaner 类中的静态变量Cleaner 对象在初始化时会加入 Cleaner 链表中。DirectByteBuffer 对象包含堆外内存的地址、大小以及 Cleaner 对象的引用ReferenceQueue 用于保存需要回收的 Cleaner 对象。</p>
<p><img src="assets/Ciqc1F-063GAc4TOAATJbR2Lmao239.png" alt="图片3.png" /></p>
<p>当发生 GC 时DirectByteBuffer 对象被回收,内存中的对象引用情况发生了如下变化:</p>
<p><img src="assets/Ciqc1F-063eAQ7AiAAPPC1-cL1I933.png" alt="图片4.png" /></p>
<p>此时 Cleaner 对象不再有任何引用关系,在下一次 GC 时,该 Cleaner 对象将被添加到 ReferenceQueue 中,并执行 clean() 方法。clean() 方法主要做两件事情:</p>
<ol>
<li>将 Cleaner 对象从 Cleaner 链表中移除;</li>
<li>调用 unsafe.freeMemory 方法清理堆外内存。</li>
</ol>
<p>至此,堆外内存的回收已经介绍完了,下次再排查内存泄漏问题的时候先回顾下这些最基本的知识,做到心中有数。</p>
<h3>总结</h3>
<p>堆外内存是一把双刃剑,在网络 I/O、文件读写、分布式缓存等领域使用堆外内存都更加简单、高效此外使用堆外内存不受 JVM 约束,可以避免 JVM GC 的压力,降低对业务应用的影响。当然天下没有免费的午餐,堆外内存也不能滥用,使用堆外内存你就需要关注内存回收问题,虽然 JVM 在一定程度上帮助我们实现了堆外内存的自动回收,但我们仍然需要培养类似 C/C++ 的分配/回收的意识,出现内存泄漏问题能够知道如何分析和处理。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/09 数据传输writeAndFlush 处理流程剖析.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/11 另起炉灶Netty 数据传输载体 ByteBuf 详解.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":"70997351dbfc3d60","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>

View File

@@ -0,0 +1,872 @@
<!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>12 他山之石:高性能内存分配器 jemalloc 基本原理.md</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 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 class="current-tab" 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>12 他山之石:高性能内存分配器 jemalloc 基本原理</h1>
<p>在上节课,我们介绍了强大的 ByteBuf 工具类ByteBuf 在 Netty 中随处可见,那么这些 ByteBuf 在 Netty 中是如何被分配和管理的呢?接下来的我们会对 Netty 高性能内存管理进行剖析这些知识相比前面的章节有些晦涩难懂你不必过于担心Netty 内存管理的实现并不是一蹴而就的,它也是参考了 jemalloc 内存分配器。今天我们就先介绍 jemalloc 内存分配器的基本原理,为我们后面的课程打好基础。</p>
<h3>背景知识</h3>
<p>jemalloc 是由 Jason Evans 在 FreeBSD 项目中引入的新一代内存分配器。它是一个通用的 malloc 实现,侧重于减少内存碎片和提升高并发场景下内存的分配效率,其目标是能够替代 malloc。jemalloc 应用十分广泛,在 Firefox、Redis、Rust、Netty 等出名的产品或者编程语言中都有大量使用。具体细节可以参考 Jason Evans 发表的论文 《A Scalable Concurrent malloc Implementation for FreeBSD》</p>
<p>除了 jemalloc 之外,业界还有一些著名的内存分配器实现,例如 ptmalloc 和 tcmalloc。我们对这三种内存分配器做一个简单的对比</p>
<p>ptmalloc 是基于 glibc 实现的内存分配器它是一个标准实现所以兼容性较好。pt 表示 per thread 的意思。当然 ptmalloc 确实在多线程的性能优化上下了很多功夫。由于过于考虑性能问题,多线程之间内存无法实现共享,只能每个线程都独立使用各自的内存,所以在内存开销上是有很大浪费的。</p>
<p>tcmalloc 出身于 Google全称是 thread-caching malloc所以 tcmalloc 最大的特点是带有线程缓存tcmalloc 非常出名,目前在 Chrome、Safari 等知名产品中都有所应有。tcmalloc 为每个线程分配了一个局部缓存对于小对象的分配可以直接由线程局部缓存来完成对于大对象的分配场景tcmalloc 尝试采用自旋锁来减少多线程的锁竞争问题。</p>
<p>jemalloc 借鉴了 tcmalloc 优秀的设计思路,所以在架构设计方面两者有很多相似之处,同样都包含 thread cache 的特性。但是 jemalloc 在设计上比 ptmalloc 和 tcmalloc 都要复杂jemalloc 将内存分配粒度划分为 Small、Large、Huge 三个分类,并记录了很多 meta 数据,所以在空间占用上要略多于 tcmalloc不过在大内存分配的场景jemalloc 的内存碎片要少于 tcmalloc。tcmalloc 内部采用红黑树管理内存块和分页Huge 对象通过红黑树查找索引数据可以控制在指数级时间。</p>
<p>由此可见,虽然几个内存分配器的侧重点不同,但是它们的核心目标是一致的:</p>
<ul>
<li>高效的内存分配和回收,提升单线程或者多线程场景下的性能。</li>
<li>减少内存碎片,包括内部碎片和外部碎片,提高内存的有效利用率。</li>
</ul>
<p>那么这里又涉及一个概念什么是内存碎片呢Linux 中物理内存会被划分成若干个 4K 大小的内存页 Page物理内存的分配和回收都是基于 Page 完成的Page 内产生的内存碎片称为内部碎片Page 之间产生的内存碎片称为外部碎片。</p>
<p>首先讲下内部碎片,因为内存是按 Page 进行分配的,即便我们只需要很小的内存,操作系统至少也会分配 4K 大小的 Page单个 Page 内只有一部分字节都被使用,剩余的字节形成了内部碎片,如下图所示。</p>
<p><img src="assets/CgqCHl--HxSAH5EGAANWzZRA9Kg017.png" alt="Drawing 0.png" /></p>
<p>外部碎片与内部碎片相反,是在分配较大内存块时产生的。我们试想一下,当需要分配大内存块的时候,操作系统只能通过分配连续的 Page 才能满足要求,在程序不断运行的过程中,这些 Page 被频繁的回收并重新分配Page 之间就会出现小的空闲内存块,这样就形成了外部碎片,如下图所示。</p>
<p><img src="assets/Ciqc1F--HxyAbK3CAAQ1Tsz2fsY135.png" alt="Drawing 1.png" /></p>
<p>上述我们介绍了内存分配器的一些背景知识它们是操作系统以及高性能组件的必备神器如果你对内存管理有兴趣jemalloc 和 tcmalloc 都是非常推荐学习的。</p>
<h3>常用内存分配器算法</h3>
<p>在学习 jemalloc 的实现原理之前,我们先了解下最常用的内存分配器算法:<strong>动态内存分配</strong><strong>伙伴算法</strong><strong>Slab 算法</strong>,这将对于我们理解 jemalloc 大有裨益。</p>
<h4>动态内存分配</h4>
<p>动态内存分配Dynamic memory allocation又称为堆内存分配后面简称 DMA操作系统根据程序运行过程中的需求即时分配内存且分配的内存大小就是程序需求的大小。在大部分场景下只有在程序运行的时候才知道所需要分配的内存大小如果提前分配可能会分配的大小无法把控分配太大会浪费空间分配太小会无法使用。</p>
<p>DMA 是从一整块内存中按需分配,对于分配出的内存会记录元数据,同时还会使用空闲分区链维护空闲内存,便于在内存分配时查找可用的空闲分区,常用的有三种查找策略:</p>
<p><strong>第一种是⾸次适应算法first fit</strong>空闲分区链以地址递增的顺序将空闲分区以双向链表的形式连接在一起从空闲分区链中找到第一个满足分配条件的空闲分区然后从空闲分区中划分出一块可用内存给请求进程剩余的空闲分区仍然保留在空闲分区链中。如下图所示P1 和 P2 的请求可以在内存块 A 中完成分配。该算法每次都从低地址开始查找,造成低地址部分会不断被分配,同时也会产生很多小的空闲分区。</p>
<p><img src="assets/Ciqc1F--Oq2AL3z_AALiP5oG4Kg709.png" alt="图片3.png" /></p>
<p><strong>第二种是循环首次适应算法next fit</strong>该算法是由首次适应算法的变种循环首次适应算法不再是每次从链表的开始进行查找而是从上次找到的空闲分区的下⼀个空闲分区开始查找。如下图所示P1 请求在内存块 A 完成分配,然后再为 P2 分配内存时,是直接继续向下寻找可用分区,最终在 B 内存块中完成分配。该算法相比⾸次适应算法空闲分区的分布更加均匀,而且查找的效率有所提升,但是正因为如此会造成空闲分区链中大的空闲分区会越来越少。</p>
<p><img src="assets/Ciqc1F--OrmAQbSMAALYfPT8Bo0517.png" alt="图片4.png" /></p>
<p><strong>第三种是最佳适应算法best fit</strong>,空闲分区链以空闲分区大小递增的顺序将空闲分区以双向链表的形式连接在一起,每次从空闲分区链的开头进行查找,这样第一个满足分配条件的空间分区就是最优解。如下图所示,在 A 内存块分配完 P1 请求后,空闲分区链重新按分区大小进行排序,再为 P2 请求查找满足条件的空闲分区。该算法的空间利用率更高,但同样也会留下很多较难利用的小空闲分区,由于每次分配完需要重新排序,所以会有造成性能损耗。</p>
<p><img src="assets/Ciqc1F--OsOAIwduAAKS6MtXII4882.png" alt="图片5.png" /></p>
<h4>伙伴算法</h4>
<p>伙伴算法是一种非常经典的内存分配算法,它采用了分离适配的设计思想,将物理内存按照 2 的次幂进行划分,内存分配时也是按照 2 的次幂大小进行按需分配,例如 4KB、 8KB、16KB 等。假设我们请求分配的内存大小为 10KB那么会按照 16KB 分配。</p>
<p>伙伴算法相对比较复杂,我们结合下面这张图来讲解它的分配原理。</p>
<p><img src="assets/Ciqc1F--HzqAdUBdAANa3t7uXSk503.png" alt="Drawing 5.png" /></p>
<p>伙伴算法把内存划分为 11 组不同的 2 次幂大小的内存块集合,每组内存块集合都用双向链表连接。链表中每个节点的内存块大小分别为 1、2、4、8、16、32、64、128、256、512 和 1024 个连续的 Page例如第一组链表的节点为 2^0 个连续 Page第二组链表的节点为 2^1 个连续 Page以此类推。</p>
<p>假设我们需要分配 10K 大小的内存块,看下伙伴算法的具体分配过程:</p>
<ol>
<li>首先需要找到存储 2^4 连续 Page 所对应的链表,即数组下标为 4</li>
<li>查找 2^4 链表中是否有空闲的内存块,如果有则分配成功;</li>
<li>如果 2^4 链表不存在空闲的内存块,则继续沿数组向上查找,即定位到数组下标为 5 的链表,链表中每个节点存储 2^5 的连续 Page</li>
<li>如果 2^5 链表中存在空闲的内存块,则取出该内存块并将它分割为 2 个 2^4 大小的内存块,其中一块分配给进程使用,剩余的一块链接到 2^4 链表中。</li>
</ol>
<p>以上是伙伴算法的分配过程,那么释放内存时候伙伴算法又会发生什么行为呢?当进程使用完内存归还时,需要检查其伙伴块的内存是否释放,所谓伙伴块是不仅大小相同,而且两个块的地址是连续的,其中低地址的内存块起始地址必须为 2 的整数次幂。如果伙伴块是空闲的,那么就会将两个内存块合并成更大的块,然后重复执行上述伙伴块的检查机制。直至伙伴块是非空闲状态,那么就会将该内存块按照实际大小归还到对应的链表中。频繁的合并会造成 CPU 浪费,所以并不是每次释放都会触发合并操作,当链表中的内存块个数小于某个阈值时,并不会触发合并操作。</p>
<p>由此可见,伙伴算法有效地减少了外部碎片,但是有可能会造成非常严重的内部碎片,最严重的情况会带来 50% 的内存碎片。</p>
<h4>Slab 算法</h4>
<p>因为伙伴算法都是以 Page 为最小管理单位,在小内存的分配场景,伙伴算法并不适用,如果每次都分配一个 Page 岂不是非常浪费内存,因此 Slab 算法应运而生了。Slab 算法在伙伴算法的基础上,对小内存的场景专门做了优化,采用了内存池的方案,解决内部碎片问题。</p>
<p>Linux 内核使用的就是 Slab 算法,因为内核需要频繁地分配小内存,所以 Slab 算法提供了一种高速缓存机制,使用缓存存储内核对象,当内核需要分配内存时,基本上可以通过缓存中获取。此外 Slab 算法还可以支持通用对象的初始化操作,避免对象重复初始化的开销。下图是 Slab 算法的结构图Slab 算法实现起来非常复杂,本文只做一个简单的了解。</p>
<p><img src="assets/Ciqc1F--H2KAIoZ_AAcYUn319Hc822.png" alt="image.png" /></p>
<p>在 Slab 算法中维护着大小不同的 Slab 集合,在最顶层是 cache_chaincache_chain 中维护着一组 kmem_cache 引用kmem_cache 负责管理一块固定大小的对象池。通常会提前分配一块内存,然后将这块内存划分为大小相同的 slot不会对内存块再进行合并同时使用位图 bitmap 记录每个 slot 的使用情况。</p>
<p>kmem_cache 中包含三个 Slab 链表:<strong>完全分配使用 slab_full</strong><strong>部分分配使用 slab_partial</strong><strong>完全空闲 slabs_empty</strong>,这三个链表负责内存的分配和释放。每个链表中维护的 Slab 都是一个或多个连续 Page每个 Slab 被分配多个对象进行存储。Slab 算法是基于对象进行内存管理的,它把相同类型的对象分为一类。当分配内存时,从 Slab 链表中划分相应的内存单元当释放内存时Slab 算法并不会丢弃已经分配的对象,而是将它保存在缓存中,当下次再为对象分配内存时,直接会使用最近释放的内存块。</p>
<p>单个 Slab 可以在不同的链表之间移动,例如当一个 Slab 被分配完,就会从 slab_partial 移动到 slabs_full当一个 Slab 中有对象被释放后,就会从 slab_full 再次回到 slab_partial所有对象都被释放完的话就会从 slab_partial 移动到 slab_empty。</p>
<p>至此,三种最常用的内存分配算法已经介绍完了,优秀的内存分配算法都是在性能和内存利用率之间寻找平衡点,我们今天的主角 jemalloc 就是非常典型的例子。</p>
<h3>jemalloc 架构设计</h3>
<p>在了解了常用的内存分配算法之后,再理解 jemalloc 的架构设计会相对轻松一些。下图是 jemalloc 的架构图,我们一起学习下它的核心设计理念。</p>
<p><img src="assets/Ciqc1F--H3KAEYJFAAp4aFcW83A719.png" alt="image" /></p>
<p>上图中涉及 jemalloc 的几个核心概念,例如 arena、bin、chunk、run、region、tcache 等,我们下面逐一进行介绍。</p>
<p><strong>arena 是 jemalloc 最重要的部分</strong>,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena。</p>
<p><strong>bin 用于管理不同档位的内存单元</strong>,每个 bin 管理的内存大小是按分类依次递增。因为 jemalloc 中小内存的分配是基于 Slab 算法完成的,所以会产生不同类别的内存块。</p>
<p><strong>chunk 是负责管理用户内存块的数据结构</strong>chunk 以 Page 为单位管理内存,默认大小是 4M即 1024 个连续 Page。每个 chunk 可被用于多次小内存的申请,但是在大内存分配的场景下只能分配一次。</p>
<p><strong>run 实际上是 chunk 中的一块内存区域</strong>,每个 bin 管理相同类型的 run最终通过操作 run 完成内存分配。run 结构具体的大小由不同的 bin 决定,例如 8 字节的 bin 对应的 run 只有一个 Page可以从中选取 8 字节的块进行分配。</p>
<p><strong>region 是每个 run 中的对应的若干个小内存块</strong>,每个 run 会将划分为若干个等长的 region每次内存分配也是按照 region 进行分发。</p>
<p><strong>tcache 是每个线程私有的缓存</strong>,用于 small 和 large 场景下的内存分配,每个 tcahe 会对应一个 arenatcache 本身也会有一个 bin 数组称为tbin。与 arena 中 bin 不同的是,它不会有 run 的概念。tcache 每次从 arena 申请一批内存,在分配内存时首先在 tcache 查找,从而避免锁竞争,如果分配失败才会通过 run 执行内存分配。</p>
<p>jemalloc 的几个核心的概念介绍完了,我们再重新梳理下它们之间的关系:</p>
<ul>
<li>内存是由一定数量的 arenas 负责管理,线程均匀分布在 arenas 当中;</li>
<li>每个 arena 都包含一个 bin 数组,每个 bin 管理不同档位的内存块;</li>
<li>每个 arena 被划分为若干个 chunks每个 chunk 又包含若干个 runs每个 run 由连续的 Page 组成run 才是实际分配内存的操作对象;</li>
<li>每个 run 会被划分为一定数量的 regions在小内存的分配场景region 相当于用户内存;</li>
<li>每个 tcache 对应 一个 arenatcache 中包含多种类型的 bin。</li>
</ul>
<p>接下来我们分析下 jemalloc 的整体内存分配和释放流程,主要分为 <strong>Samll</strong><strong>Large</strong><strong>Huge</strong> 三种场景。</p>
<p>首先讲下 Samll 场景,如果请求分配内存的大小小于 arena 中的最小的 bin那么优先从线程中对应的 tcache 中进行分配。首先确定查找对应的 tbin 中是否存在缓存的内存块,如果存在则分配成功,否则找到 tbin 对应的 arena从 arena 中对应的 bin 中分配 region 保存在 tbin 的 avail 数组中,最终从 availl 数组中选取一个地址进行内存分配,当内存释放时也会将被回收的内存块进行缓存。</p>
<p>Large 场景的内存分配与 Samll 类似,如果请求分配内存的大小大于 arena 中的最小的 bin但是不大于 tcache 中能够缓存的最大块,依然会通过 tcache 进行分配,但是不同的是此时会分配 chunk 以及所对应的 run从 chunk 中找到相应的内存空间进行分配。内存释放时也跟 samll 场景类似,会把释放的内存块缓存在 tacache 的 tbin 中。此外还有一种情况当请求分配内存的大小大于tcache 中能够缓存的最大块,但是不大于 chunk 的大小,那么将不会采用 tcache 机制,直接在 chunk 中进行内存分配。</p>
<p>Huge 场景,如果请求分配内存的大小大于 chunk 的大小,那么直接通过 mmap 进行分配,调用 munmap 进行回收。</p>
<p>到底为止jemalloc 的基础知识介绍完毕,你需要花点时间消化它,这对于后面学习 Netty 的内存管理很有帮助。</p>
<h3>总结</h3>
<p>内存管理是每个高阶程序员的必备知识万变不离其宗jemalloc 的思想在很多场景都非常适用,在 Redis、Netty 等知名的高性能组件中都有它的原型,你会发现它们的实现思路都是类似的,申请大块内存,避免“细水长流”。趁热打铁吧,下节课我们将继续学习 Netty 是如何设计高性能的内存管理的。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/11 另起炉灶Netty 数据传输载体 ByteBuf 详解.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/13 举一反三Netty 高性能内存管理设计(上).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":"709973569ebb3d60","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>

View File

@@ -0,0 +1,852 @@
<!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>23 架构设计:如何实现一个高性能分布式 RPC 框架.md</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 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 class="current-tab" 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>23 架构设计:如何实现一个高性能分布式 RPC 框架</h1>
<p>在前面的课程中,我们由浅入深地讲解了 Netty 的基础知识和实现原理,并对 Netty 的核心源码进行了剖析,相信你已经体会到了 Netty 的强大之处。本身学习一门技术是一个比较漫长的过程,恭喜你坚持了下来。纸上得来终觉浅,绝知此事要躬行。你是不是已经迫不及待想在项目中使用 Netty 了呢?接下来我会带着你完成一个相对完整的 RPC 框架原型,帮助你加深对 Netty 的理解,希望你能亲自动手跟我一起完成它。</p>
<p>我先来说说,为什么要选择 RPC 框架作为实战项目。RPC 框架是大型企业高频使用的一种中间件框架用于解决分布式系统中服务之间的调用问题。RPC 框架设计很多重要的知识点,如线程模型、通信协议设计、同步/异步调用、负载均衡等,对于提高我们的技术综合能力有非常大的帮助。</p>
<p>我们实战课需要达到什么样的目标呢?市面上有较多出名的 RPC 框架,例如 Dubbo、Thrift、gRPC 等RPC 框架本身是非常负责的,我们不可能面面俱到,而是抓住 RPC 框架的核心流程以及必备的组件,开发一个功能比较丰富的小型 RPC 框架。麻雀虽小,五脏俱全。</p>
<p>在正式开始 RPC 实战项目之前,我们先学习一下 RPC 的架构设计,这是项目前期规划非常重要的一步。</p>
<h3>RPC 框架架构设计</h3>
<p>RPC 又称远程过程调用Remote Procedure Call用于解决分布式系统中服务之间的调用问题。通俗地讲就是开发者能够像调用本地方法一样调用远程的服务。下面我们通过一幅图来说说 RPC 框架的基本架构。</p>
<p><img src="assets/Cip5yF_vL2GAftSLAAOCKnZEdrY576.png" alt="Lark20210101-221749.png" /></p>
<p>RPC 框架包含三个最重要的组件,分别是客户端、服务端和注册中心。在一次 RPC 调用流程中,这三个组件是这样交互的:</p>
<ul>
<li>服务端在启动后,会将它提供的服务列表发布到注册中心,客户端向注册中心订阅服务地址;</li>
<li>客户端会通过本地代理模块 Proxy 调用服务端Proxy 模块收到负责将方法、参数等数据转化成网络字节流;</li>
<li>客户端从服务列表中选取其中一个的服务地址,并将数据通过网络发送给服务端;</li>
<li>服务端接收到数据后进行解码,得到请求信息;</li>
<li>服务端根据解码后的请求信息调用对应的服务,然后将调用结果返回给客户端。</li>
</ul>
<p>虽然 RPC 调用流程很容易理解,但是实现一个完整的 RPC 框架设计到很多内容,例如服务注册与发现、通信协议与序列化、负载均衡、动态代理等,下面我们一一进行初步地讲解。</p>
<h3>服务注册与发现</h3>
<p>在分布式系统中,不同服务之间应该如何通信呢?传统的方式可以通过 HTTP 请求调用、保存服务端的服务列表等,这样做需要开发者主动感知到服务端暴露的信息,系统之间耦合严重。为了更好地将客户端和服务端解耦,以及实现服务优雅上线和下线,于是注册中心就出现了。</p>
<p>在 RPC 框架中,主要是使用注册中心来实现服务注册和发现的功能。服务端节点上线后自行向注册中心注册服务列表,节点下线时需要从注册中心将节点元数据信息移除。客户端向服务端发起调用时,自己负责从注册中心获取服务端的服务列表,然后在通过负载均衡算法选择其中一个服务节点进行调用。以上是最简单直接的服务端和客户端的发布和订阅模式,不需要再借助任何中间服务器,性能损耗也是最小的。</p>
<p>现在思考一个问题,服务在下线时需要从注册中心移除元数据,那么注册中心怎么才能感知到服务下线呢?我们最先想到的方法就是节点主动通知的实现方式,当节点需要下线时,向注册中心发送下线请求,让注册中心移除自己的元数据信息。但是如果节点异常退出,例如断网、进程崩溃等,那么注册中心将会一直残留异常节点的元数据,从而可能造成服务调用出现问题。</p>
<p>为了避免上述问题,实现服务优雅下线比较好的方式是采用主动通知 + 心跳检测的方案。除了主动通知注册中心下线外,还需要增加节点与注册中心的心跳检测功能,这个过程也叫作探活。心跳检测可以由节点或者注册中心负责,例如注册中心可以向服务节点每 60s 发送一次心跳包,如果 3 次心跳包都没有收到请求结果,可以任务该服务节点已经下线。</p>
<p>由此可见,采用注册中心的好处是可以解耦客户端和服务端之间错综复杂的关系,并且能够实现对服务的动态管理。服务配置可以支持动态修改,然后将更新后的配置推送到客户端和服务端,无须重启任何服务。</p>
<h3>通信协议与序列化</h3>
<p>既然 RPC 是远程调用,必然离不开网络通信协议。客户端在向服务端发起调用之前,需要考虑采用何种方式将调用信息进行编码,并传输到服务端。因为 RPC 框架对性能有非常高的要求所以通信协议应该越简单越好这样可以减少编解码的性能损耗。RPC 框架可以基于不同的协议实现,大部分主流 RPC 框架会选择 TCP、HTTP 协议,出名的 gRPC 框架使用的则是 HTTP2。TCP、HTTP、HTTP2 都是稳定可靠的,但其实使用 UDP 协议也是可以的,具体看业务使用的场景。成熟的 RCP 框架能够支持多种协议,例如阿里开源的 Dubbo 框架被很多互联网公司广泛使用,其中可插拔的协议支持是 Dubbo 的一大特色,这样不仅可以给开发者提供多种不同的选择,而且为接入异构系统提供了便利。</p>
<p>客户端和服务端在通信过程中需要传输哪些数据呢?这些数据又该如何编解码呢?如果采用 TCP 协议,你需要将调用的接口、方法、请求参数、调用属性等信息序列化成二进制字节流传递给服务提供方,服务端接收到数据后,再把二进制字节流反序列化得到调用信息,然后利用反射的原理调用对应方法,最后将返回结果、返回码、异常信息等返回给客户端。所谓序列化和反序列化就是将对象转换成二进制流以及将二进制流再转换成对象的过程。因为网络通信依赖于字节流,而且这些请求信息都是不确定的,所以一般会选用通用且高效的序列化算法。比较常用的序列化算法有 FastJson、Kryo、Hessian、Protobuf 等,这些第三方序列化算法都比 Java 原生的序列化操作都更加高效。Dubbo 支持多种序列化算法,并定义了 Serialization 接口规范,所有序列化算法扩展都必须实现该接口,其中默认使用的是 Hessian 序列化算法。</p>
<h3>RPC 调用方式</h3>
<p>成熟的 RPC 框架一般会提供四种调用方式,分别为<strong>同步 Sync</strong><strong>异步 Future</strong><strong>回调 Callback</strong><strong>单向 Oneway</strong>。RPC 框架的性能和吞吐量与合理使用调用方式是息息相关的,下面我们逐一介绍下四种调用方式的实现原理。</p>
<p>Sync 同步调用。客户端线程发起 RPC 调用后当前线程会一直阻塞直至服务端返回结果或者处理超时异常。Sync 同步调用一般是 RPC 框架默认的调用方式,为了保证系统可用性,客户端设置合理的超时时间是非常重要的。虽说 Sync 是同步调用,但是客户端线程和服务端线程并不是同一个线程,实际在 RPC 框架内部还是异步处理的。Sync 同步调用的过程如下图所示。</p>
<p><img src="assets/Cip5yF_vL5iAAPg2AAU3x7WPF14178.png" alt="1.png" /></p>
<ul>
<li><strong>Future 异步调用</strong>。客户端发起调用后不会再阻塞等待,而是拿到 RPC 框架返回的 Future 对象调用结果会被服务端缓存客户端自行决定后续何时获取返回结果。当客户端主动获取结果时该过程是阻塞等待的。Future 异步调用过程如下图所示。</li>
</ul>
<p><img src="assets/CgpVE1_vL6KAMtc4AAWsJs_OVcY000.png" alt="2.png" /></p>
<ul>
<li><strong>Callback 回调调用</strong>。如下图所示,客户端发起调用时,将 Callback 对象传递给 RPC 框架,无须同步等待返回结果,直接返回。当获取到服务端响应结果或者超时异常后,再执行用户注册的 Callback 回调。所以 Callback 接口一般包含 onResponse 和 onException 两个方法,分别对应成功返回和异常返回两种情况。</li>
</ul>
<p><img src="assets/CgpVE1_vL6mAcikTAAYJgBAIkz4743.png" alt="3.png" /></p>
<ul>
<li><strong>Oneway 单向调用</strong>。客户端发起请求之后直接返回忽略返回结果。Oneway 方式是最简单的,具体调用过程如下图所示。</li>
</ul>
<p><img src="assets/CgpVE1_vL6-AKGvaAAUZP14MgzM418.png" alt="4.png" /></p>
<p>四种调用方式都各有优缺点,很难说异步方式一定会比同步方式效果好,在不用的业务场景可以按需选取更合适的调用方式。</p>
<h3>线程模型</h3>
<p>线程模型是 RPC 框架需要重点关注的部分,与我们之前介绍的 Netty Reactor 线程模型有什么区别和联系吗?</p>
<p>首先我们需要明确 I/O 线程和业务线程的区别,以 Dubbo 框架为例Dubbo 使用 Netty 作为底层的网络通信框架,采用了我们熟悉的主从 Reactor 线程模型,其中 Boss 和 Worker 线程池就可以看作 I/O 线程。I/O 线程可以理解为主要负责处理网络数据,例如事件轮询、编解码、数据传输等。如果业务逻辑能够立即完成,也可以使用 I/O 线程进行处理,这样可以省去线程上下文切换的开销。如果业务逻辑耗时较多,例如包含查询数据库、复杂规则计算等耗时逻辑,那么 I/O 必须将这些请求分发到业务线程池中进行处理,以免阻塞 I/O 线程。</p>
<p>那么哪些请求需要在 I/O 线程中执行哪些又需要在业务线程池中执行呢Dubbo 框架的做法值得借鉴,它给用户提供了多种选择,它一共提供了 5 种分发策略,如下表格所示。</p>
<p><img src="assets/Cip5yF_vL7eANr2BAAI2Xxyw-pg628.png" alt="Lark20210101-221822.png" /></p>
<h3>负载均衡</h3>
<p>在分布式系统中,服务提供者和服务消费者都会有多台节点,如何保证服务提供者所有节点的负载均衡呢?客户端在发起调用之前,需要感知有多少服务端节点可用,然后从中选取一个进行调用。客户端需要拿到服务端节点的状态信息,并根据不同的策略实现负载均衡算法。负载均衡策略是影响 RPC 框架吞吐量很重要的一个因素,下面我们介绍几种最常用的负载均衡策略。</p>
<ul>
<li><strong>Round-Robin 轮询</strong>。Round-Robin 是最简单有效的负载均衡策略,并没有考虑服务端节点的实际负载水平,而是依次轮询服务端节点。</li>
<li><strong>Weighted Round-Robin 权重轮询</strong>。对不同负载水平的服务端节点增加权重系数,这样可以通过权重系数降低性能较差或者配置较低的节点流量。权重系数可以根据服务端负载水平实时进行调整,使集群达到相对均衡的状态。</li>
<li><strong>Least Connections 最少连接数</strong>。客户端根据服务端节点当前的连接数进行负载均衡客户端会选择连接数最少的一台服务器进行调用。Least Connections 策略只是服务端其中一种维度我们可以演化出最少请求数、CPU 利用率最低等其他维度的负载均衡方案。</li>
<li><strong>Consistent Hash 一致性 Hash</strong>。目前主流推荐的负载均衡策略Consistent Hash 是一种特殊的 Hash 算法在服务端节点扩容或者下线时尽可能保证客户端请求还是固定分配到同一台服务器节点。Consistent Hash 算法是采用哈希环来实现的,通过 Hash 函数将对象和服务器节点放置在哈希环上,一般来说服务器可以选择 IP + Port 进行 Hash然后为对象选择对应的服务器节点在哈希环中顺时针查找距离对象 Hash 值最近的服务器节点。</li>
</ul>
<p>此外负载均衡算法可以是多种多样的客户端可以记录例如健康状态、连接数、内存、CPU、Load 等更加丰富的信息,根据综合因素进行更好地决策。</p>
<h3>动态代理</h3>
<p>RPC 框架怎么做到像调用本地接口一样调用远端服务呢?这必须依赖动态代理来实现。需要创建一个代理对象,在代理对象中完成数据报文编码,然后发起调用发送数据给服务提供方,以此屏蔽 RPC 框架的调用细节。因为代理类是在运行时生成的,所以代理类的生成速度、生成的字节码大小都会影响 RPC 框架整体的性能和资源消耗所以需要慎重选择动态代理的实现方案。动态代理比较主流的实现方案有以下几种JDK 动态代理、Cglib、Javassist、ASM、Byte Buddy我们简单做一个对比和介绍。</p>
<ul>
<li><strong>JDK 动态代理</strong>。在运行时可以动态创建代理类,但是 JDK 动态代理的功能比较局限,代理对象必须实现一个接口,否则抛出异常。因为代理类会继承 Proxy 类,然而 Java 是不支持多重继承的只能通过接口实现多态。JDK 动态代理所生成的代理类是接口的实现类不能代理接口中不存在的方法。JDK 动态代理是通过反射调用的形式代理类中的方法,比直接调用肯定是性能要慢的。</li>
<li><strong>Cglib 动态代理</strong>。Cglib 是基于 ASM 字节码生成框架实现的,通过字节码技术生成的代理类,所以代理类的类型是不受限制的。而且 Cglib 生成的代理类是继承于被代理类所以可以提供更加灵活的功能。在代理方法方面Cglib 是有优势的,它采用了 FastClass 机制,为代理类和被代理类各自创建一个 Class这个 Class 会为代理类和被代理类的方法分配 index 索引FastClass 就可以通过 index 直接定位要调用的方法,并直接调用,这是一种空间换时间的优化思路。</li>
<li><strong>Javassist 和 ASM</strong>。二者都是 Java 字节码操作框架,使用起来难度较大,需要开发者对 Class 文件结构以及 JVM 都有所了解但是它们都比反射的性能要高。Byte Buddy 也是一个字节码生成和操作的类库Byte Buddy 功能强大,相比于 Javassist 和 ASMByte Buddy 提供了更加便捷的 API用于创建和修改 Java 类,无须理解字节码的格式,而且 Byte Buddy 更加轻量,性能更好。</li>
</ul>
<p>至此,我们已经对实现 RPC 框架的几个核心要点做了一个大致的介绍,关于通信协议、负载均衡、动态代理在 RPC 框架中如何实现,我们后面会有专门的实践课对其进行详细介绍,本节课我们先有个大概的印象即可。</p>
<h3>总结</h3>
<p>如果你可以完成上述 RPC 框架的核心功能,那么一个简易的 RPC 框架的 MVP 原型就完成了,这也是我们实践课的目标。当然实现一个高性能高可靠的 RPC 框架并不容易,需要考虑的问题远不止如此,例如异常重试、服务级别线程池隔离、熔断限流、集群容错、优雅下线等等,在实践课最后我会为你讲解 RPC 框架进阶的拓展内容。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/22 技巧篇:高性能无锁队列 Mpsc Queue.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/24 服务发布与订阅:搭建生产者和消费者的基础框架.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":"709973748f803d60","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>

View File

@@ -0,0 +1,754 @@
<!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>31 结束语 技术成长之路:如何打造自己的技术体系.md</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 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 class="current-tab" 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>31 结束语 技术成长之路:如何打造自己的技术体系</h1>
<p>时间飞逝,不知不觉整个专栏这节课就结束了。首先感谢你一路陪伴和支持,整个专栏的过程对我来说也是一段难忘的经历,希望专栏的内容能够让你有所收获。读完本专栏,我们就能够立刻变成一个 Netty 高手了吗?答案是 NO。Netty 的知识体系非常庞大,需要我们花时间去慢慢消化,并在不断实践中总结,也许在不同时间段你对 Netty 的理解会更加深刻。</p>
<p>相信你在刚开始学习一门技术的时候,多多少少都会遇到一些困难,例如方向不清晰,容易陷入死胡同。我们需要认真地思考如何规划最优的学习路线?如何打造该领域的技术体系?如何能够高效率地执行落地?</p>
<h3>体系化:目标制定与执行</h3>
<p>在学习一门技术之前我都会问自己几个问题:</p>
<ol>
<li>该技术能够解决什么问题,可以提升我的哪些能力?</li>
<li>短期目标和长期目标是什么?</li>
<li>我需要做哪些事情可以实现目标?</li>
</ol>
<p>现在获取知识的成本非常低,通过官方文档、博客等渠道我们都可以快速了解一门技术的概貌。当你下定决心深入研究这门技术的时候,最重要的是制定自己的学习计划。以 Netty 为例,因为刚开始我们对 Netty 不是特别了解,但是应该大概知道 Netty 有哪些重要的概念、特性需要去深入学习,先将这些重要的内容列入我们的学习计划,然后制定一个周期(例如一个星期)学习计划表。在学习的过程中,我们会对 Netty 的理解越发深入,发现有更多的知识点需要去挖掘,此时我们可以再去调整和完善学习计划。就像一个大树的成长过程一样,首先要抓住目标主干,然后再学习分支的知识点,由点到线、线到面不断自我探索和建立自己的技术体系。</p>
<p>明确自己的学习方向后,实现自己学习目标的途径有非常多,项目实战、源码学习、写博客、参加社区等途径都是非常有效的办法。重要的是持之以恒地坚持下去,切忌急于求成或者半途而废。每隔一段时间我们应当回顾下自己的学习计划是否有效,我是否坚持完成了所有事情?如果达成阶段性的成果,可以适当奖励下自己,一定要让自己充满成就感。</p>
<h3>善于思考和总结</h3>
<p>在学习一门技术的时候,大部分人都只是停留在会使用的层面,并不知道该技术到底能够解决什么问题,相比同领域的其他技术有什么优缺点。我们刚开始不可能一下看清楚问题的本质,需要不断在学习中思考,积累实践经验,然后慢慢总结自己的见解。一名优秀的技术人可以从技术原理中去了解问题本质,然后找到问题的解决防范,也让结果更有说服力。学会从优秀的开源项目中挖掘技术原理对我们是非常有帮助的,起码在面对问题的时候可以让我们思路更加开阔,处理问题更加得心应手。</p>
<p>从技术的角度来说,我们一定要培养自己多维度的思考习惯,而不是停留在表面,这样永远都进步不了。一个方案、一个问题、一个功能都可能需要考虑到多种因素,如果我们能够把方方面面都考虑得非常细致,那么也会让自己做事更有技术深度、更具备全面性。在工作中,我们经常会得到别人大量的信息,看别人的观点和学习别人的方案,吸收值得学习的地方,再总结出自己的独特的思考。用多个维度去看待问题,有时候别人的观点并不一定是对的。</p>
<h3>乐于交流与分享</h3>
<p>交流与分享是检验自己学习成果非常有效的方法,例如团队或者公司的技术分享、撰写书籍、博客等都是沉淀知识的绝佳途径。交流与分享不仅可以有机会让我们梳理自己的知识体系,让知识变得更加牢固,而且可以让众人来检验自己对知识的理解是否正确。人外有人,天外有天,避免自己陷入技术人自满的状态。</p>
<p>我相信“会”一门技术并不等于你“会教”一门技术,把自己会的东西分享出来远比学习的过程更加困难。交流与分享需要我们更具备勇气,分享知识是获取勇气的一种方式,不要害怕自己会出错而退缩,也不要为了证明自己“很懂”而去与别人交流,虚心向他人学习,帮助团队成长,每次交流与分享让自己收获满满就足够了。</p>
<h3>最后</h3>
<p>路漫漫其修远兮,吾将上下而求索。我们不是天才,更不可能一蹴而就,成长需要时间的积累,整个过程需要我们不断学习、思考和总结。保持好奇心和热情,抛弃浮躁,相信我们都能成就更好的自己。最后的最后,还是要感谢你的支持和建议,欢迎填写这份调查问卷,还请你留下宝贵的意见和建议。也欢迎给我留言,咱们后会有期!</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Netty 核心原理剖析与 RPC 实践-完/30 实践总结Netty 在项目开发中的一些最佳实践.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":"709973887b5e3d60","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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Some files were not shown because too many files have changed in this diff Show More