<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://wenje.top/feed.xml" rel="self" type="application/atom+xml" /><link href="https://wenje.top/" rel="alternate" type="text/html" hreflang="zh-Hans" /><updated>2026-10-22T09:25:20+00:00</updated><id>https://wenje.top/feed.xml</id><title type="html">Wenje Fan</title><subtitle>I will continue to grow up!
</subtitle><author><name>Your Name</name><email>fanwj1997@163.com</email></author><entry><title type="html">现代网络环境下TCP的缺陷</title><link href="https://wenje.top/post/tcp-fault" rel="alternate" type="text/html" title="现代网络环境下TCP的缺陷" /><published>2024-03-20T00:00:00+00:00</published><updated>2024-03-20T00:00:00+00:00</updated><id>https://wenje.top/post/tcp-fault</id><content type="html" xml:base="https://wenje.top/post/tcp-fault"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>在现代网络通信中，TCP 和 UDP 已经成为事实上的标准协议。绝大多数情况下，TCP 被认为是默认的传输协议，尤其是对可靠性要求较高的应用，比如文件下载和网页加载。然而，随着互联网应用的快速发展，TCP 协议在某些场景下逐渐暴露出性能问题，特别是在对低延迟和高实时性要求极高的场景（如视频聊天和在线游戏）中，TCP甚至出现整体不可用的情况。</p>

<p>尽管 TCP 的设计初衷是为了保证可靠传输，但其顺序传输、拥塞控制和丢包重传机制在这些场景下可能成为瓶颈。因此，越来越多的解决方案选择基于 UDP 实现应用层协议，以满足这些特定场景的需求。</p>

<h1 id="为什么-tcp-会有性能问题">为什么 TCP 会有性能问题？</h1>

<h2 id="传输延迟累积">传输延迟累积</h2>

<p>TCP 以可靠性为核心，其可靠传输特性依赖以下机制：</p>
<ol>
  <li><strong>顺序传输</strong>：TCP 确保数据包按照发送顺序到达接收端。如果某一个数据包丢失，TCP 会暂停后续数据包的交付，直到丢失的数据包重传成功。</li>
  <li><strong>丢包重传</strong>：一旦检测到数据丢失，TCP 会重发丢失的数据包，但这需要额外的时间。</li>
  <li><strong>拥塞控制</strong>：TCP 会根据网络状况调整传输速度，以避免网络过载，但在网络波动较大的情况下，这种机制会显著降低传输速度。</li>
</ol>

<p>现在我们以视频聊天这个场景为例，在视频聊天中，用户往往更关心当前的、最新的传输数据，而视频数据的特点是丢失了部分数据包并不影响整体数据的可读性。</p>

<p class="info">使用TCP</p>

<p>如果使用TCP进行通讯，当出现数据包丢失，TCP 的阻塞特性会阻塞后续的数据传输，无法满足尽可能快的传输最新的数据流需求，且失去时效性的数据已经没有传输价值了，但是TCP仍然会将这些数据进行顺序的传输。</p>

<div align="center">
  <img src="/assets/images/network/video-tcp.drawio.png" style="height: 150px;" />
</div>
<center>图1: TCP下的视频数据流传输</center>

<p>如上图所示，数据流的传输会滞后，且这个滞后延时会不断出现累计、消除，无法带来稳定的体验。</p>

<p class="info">使用UDP</p>

<p>相比之下，基于 UDP 的协议通过放弃内置的重传和顺序控制机制，将这些逻辑交给应用层处理：</p>
<ul>
  <li><strong>不阻塞后续数据传输</strong>：即使某些数据包丢失，后续的数据包仍然可以继续传输，保持实时性。</li>
  <li><strong>自定义丢包处理</strong>：应用可以根据需求决定是否重传关键数据，或者通过插值算法平滑丢失数据的影响。</li>
  <li><strong>避免拥塞控制限制</strong>：UDP 没有内置的拥塞控制，传输速度不会因为网络波动而受限。</li>
</ul>

<div align="center">
  <img src="/assets/images/network/video-udp.drawio.png" style="height: 200px;" />
</div>
<center>图2: UDP下的视频数据流传输</center>

<p>在自定义的UDP传输协议下，数据包之间的传输是独立的，我们可以尽可能的以时效性为主去完成这个传输，忽略部分数据包的丢失。</p>

<h1 id="nat穿透">NAT穿透</h1>

<p>在之前的文章中，我们说过NAT（网络地址转换）是为了解决IPv4地址枯竭而引入的机制，它通过将局域网设备的私有地址映射为公网地址，实现多设备共享一个公网 IP。</p>

<p>然而，NAT 的存在限制了外部设备直接与局域网设备的通信，因为NAT默认只允许内部设备发起的连接通过。换言之，一台在NAT子网下的设备，通常是不能被外部主动通过TCP协议访问的。</p>

<p>相比之下，UDP 的无连接特性使其更容易穿透NAT：</p>
<ul>
  <li><strong>UDP 无需建立连接</strong>：它可以直接发送数据包，而无需像 TCP 那样进行三次握手。</li>
  <li><strong>NAT 映射规则</strong>：当局域网设备通过 UDP 向外发送数据包时，NAT 会为该设备临时打开端口映射，允许外部设备的返回数据通过。</li>
  <li><strong>UDP 打洞（Hole Punching）</strong>：通过中继服务器（如 STUN），局域网设备可以获知对方的公网地址，并直接发送数据实现 P2P 通信。</li>
</ul>

<h1 id="总结">总结</h1>

<p>随着互联网应用的不断演进，传统的 TCP 协议虽然在可靠性方面表现卓越，但其延迟累积、NAT 穿透难度高等问题使其在某些场景下显得力不从心。相比之下，基于 UDP 的自定义协议在低延迟、高实时性需求下展现出了巨大优势。</p>

<p>无论是视频聊天还是实时对战游戏，UDP 的高效性和灵活性已经成为这些场景的首选解决方案。未来，随着网络技术的发展，我们或许会看到更多基于 UDP 的创新协议，为实时通信提供更强大的支持。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="network" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">2022-2023回顾</title><link href="https://wenje.top/post/summary-of-2022-2023" rel="alternate" type="text/html" title="2022-2023回顾" /><published>2024-02-15T00:00:00+00:00</published><updated>2024-02-15T00:00:00+00:00</updated><id>https://wenje.top/post/summary-of-2022-2023</id><content type="html" xml:base="https://wenje.top/post/summary-of-2022-2023"><![CDATA[<!--more-->

<h1 id="开篇---写在2024年2月">开篇 - 写在2024年2月</h1>

<p>2024新年已经过完了，现在正在回北京的飞机上闲来无事，想着利用这点时间写一个总结，对过去这两年做一个简单的回顾。</p>

<h1 id="对职业发展的理解">对职业发展的理解</h1>

<p class="info">如何理解职业？</p>

<p>现在各种社交媒体上，总是充斥着这样的焦虑: xxx干到35岁即失业；</p>

<p>我刚毕业的前2年里，潜意识里都默许了上面这些话，也跟随着这些言论一起吐槽过、焦虑过。我来自一个江西的乡村小镇，包括我父母也一直在给我贩卖这些焦虑，认为”编制”才是最终归宿，而我年纪大了之后就会失业。我也曾被这些话影响到过，有时会思考如果有一天我真的干不动的怎么办？</p>

<p>但现在，我对这些逐渐看明白了，那些对此深以为然的人，往往会希望事情一成不变，这样也就没有了所谓的风险。
举几个典型的例子：</p>
<ol>
  <li>大部分人追捧公务员、国企岗位，就是为了”稳定”；</li>
  <li>老一辈人洗衣服、洗碗坚持手洗，每天乐此不疲并认为就应该这样，不接受洗衣机、洗碗机、拖地机；</li>
  <li>一样东西只要不坏就会一直用，比如家里年纪比我还大的”古董”；</li>
</ol>

<p>但我的价值观却不是如此：</p>
<ol>
  <li>我不喜欢重复、机械性的工作，我会想办法让他能自己运转，而我能解脱出来去做更有意义的事情；</li>
  <li>我希望能创就事业，实现具体的价值，不想躺平；</li>
</ol>

<p>所以这个问题应该变成: “如何拒绝平庸”。而对想实现价值的人来说，这是一个一生的目标，只会恨一生太短。</p>

<h1 id="对技术的理解的转变">对技术的理解的转变</h1>

<p class="info">什么是技术?</p>

<p>刚毕业那段时间，总是热衷于”讨论”技术，也经常会被代入到网络上关于”xx语言现在最厉害”、”xx大火”，也曾漫无目的的去看过一些东西。</p>

<p>技术是什么？技术只是为了解决问题罢了，曾经我们组招了一个小哥来做Golang开发，结果这小哥是抱着”我就是来写Go的”的信念，结果来的时候策划正好需要一个工具来测试游戏，就让他先支持下做个简单的页面，结果这小哥下午就离职了。
如果一个人只是把自己定位成”我就只写xxx”，那得有多狭隘呀？</p>

<p>而在去年，我在公司内部主导了很多技术升级的工作，其中一个重点就是: 将之前总是复制代码重复实现的业务，抽离成基础组件。以及一些开发调试工具，我希望能将大家从无意义的重复工作中解放出来，这才是技术的价值。</p>

<h1 id="角色和心态的转变">角色和心态的转变</h1>

<blockquote>
  <p>角色的变化</p>
</blockquote>

<p>从去年开始，我逐渐开始从一个人做事变成带领一个小团队一起做事。我愈发明白一个道理：一个人哪怕再厉害、全能也是渺小的，真正能做成大事的是一个强大的团队。</p>

<p>带领团队后，我意识到我的思维需要有转变，需要更多的去帮助团队里的人去成长，让有志气的人能够独当一面，把不适合团队的人剔除。</p>

<blockquote>
  <p>心态的变化</p>
</blockquote>

<p>在当前的环境里，技术总在一个”外包”的位置上，不管是真的纯外包公司、还是公司内部的研发团队，只是把业务方的诉求实现出来。</p>

<p>也就是说，提出需求的人往往不具备技术背景，他思考的策略空间是不够的，这个情况在国外往往是反过来的，技术团队需要直接对产品负责，其他人员只作为辅助。如果Google的广告团队是由非技术人员负责，那就永远不会出现现在这个强大的AI广告引擎。</p>

<p>这是我目前会努力寻求的一个改变，我希望能带领一个技术团队直接对产品负责，所做的一切工作都是为了更好的打造产品，而不是造空中楼阁。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="general" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">如何理解传输层协议(TCP、UDP)</title><link href="https://wenje.top/post/tcp-udp-intro" rel="alternate" type="text/html" title="如何理解传输层协议(TCP、UDP)" /><published>2023-02-16T00:00:00+00:00</published><updated>2023-02-16T00:00:00+00:00</updated><id>https://wenje.top/post/tcp-udp-intro</id><content type="html" xml:base="https://wenje.top/post/tcp-udp-intro"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>前面我们介绍过，网络底层的通信是通过一个个小的数据包进行的。当需要传输大量数据时，数据通常会被分割成多个小包，每个小包通过网络传输。这种数据包的传输方式虽然高效，但也带来了一些挑战。网络环境本身是不稳定的，数据包的到达顺序不可预期，甚至数据包可能在传输过程中丢失或者失真，导致传输的数据出错或不完整。</p>

<p>为了应对这些问题，<strong>传输层</strong>协议应运而生。传输层的主要任务是在不可靠的网络环境中确保数据能够正确地从一个点传输到另一个点。TCP和UDP是传输层协议的代表，它们各自解决了不同的网络传输问题。</p>

<h1 id="tcp协议">TCP协议</h1>

<h2 id="tcp的来历与发展">TCP的来历与发展</h2>

<p><strong>TCP</strong>（Transmission Control Protocol, 传输控制协议）最初由 <strong>Vint Cerf</strong> 和 <strong>Bob Kahn</strong> 于1974年提出，作为Internet协议族的一部分。TCP的设计目的是提供一种可靠的、面向连接的数据传输服务，尤其适用于需要高可靠性的应用，如电子邮件和文件传输。</p>

<p>TCP的初步定义在<a href="https://tools.ietf.org/html/rfc675">RFC 675: Specification of Internet TCP</a>中，随着互联网的发展，TCP的规范不断被改进和完善，直到成为今天我们所熟知的协议。</p>

<p>其他相关文档包括：</p>
<ul>
  <li><a href="https://tools.ietf.org/html/rfc793">RFC 793: Transmission Control Protocol</a>: 定义了TCP的核心机制，包括三次握手和四次挥手</li>
  <li><a href="https://tools.ietf.org/html/rfc1122">RFC 1122: Requirements for Internet Hosts – Communication Layers</a>: 对TCP的实现进行了补充说明</li>
  <li><a href="https://tools.ietf.org/html/rfc1323">RFC 1323: TCP Extensions for High Performance</a>: 为提高性能添加了窗口扩大选项和时间戳选项</li>
  <li><a href="https://tools.ietf.org/html/rfc2581">RFC 2581: TCP Congestion Control</a>: 介绍了TCP的快速重传与快速恢复算法，提高了数据传输的性能</li>
</ul>

<h2 id="tcp的特性">TCP的特性</h2>

<p>TCP协议的核心特性包括：</p>

<ol>
  <li><strong>面向连接</strong>：在数据传输之前，必须先通过<code class="language-plaintext highlighter-rouge">三次握手</code>建立连接，保证双方都准备好进行数据交换。</li>
  <li><strong>可靠传输</strong>：通过序列号、确认号、校验和等机制，确保数据包的顺序、完整性和无误。</li>
  <li><strong>流量控制</strong>：通过滑动窗口机制控制数据的传输速率，防止接收端溢出。</li>
  <li><strong>拥塞控制</strong>：在网络拥塞时，动态调整数据的发送速率，避免网络的过载。</li>
</ol>

<h2 id="什么是连接">什么是连接?</h2>

<p>连接的定义在RFC文档中有过明确说明:</p>

<p><strong>RFC675</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   A pair of sockets form a CONNECTION which can be used to carry data
   in either direction [i.e. full duplex]. The connection is uniquely
   identified by the &lt;local socket, foreign socket&gt; address pair, and
   the same local socket can participate in multiple connections to
   different foreign sockets
</code></pre></div></div>

<p><strong>RFC793</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Connections:

    The reliability and flow control mechanisms described above require
    that TCPs initialize and maintain certain status information for
    each data stream.  The combination of this information, including
    sockets, sequence numbers, and window sizes, is called a connection.
    Each connection is uniquely specified by a pair of sockets
    identifying its two sides.
</code></pre></div></div>

<p>在TCP协议中，<strong>连接</strong>是由一对<strong>套接字（Socket）</strong>确立的。套接字是操作系统提供的网络接口，它允许程序在网络中进行数据的收发。本质是为一对通信的进程（客户端和服务端）提供一个稳定的通道，所有通过TCP协议传输的数据都需要通过这个连接。每个连接由五元组唯一标识：</p>
<ul>
  <li>本地IP地址</li>
  <li>本地端口号</li>
  <li>远程IP地址</li>
  <li>远程端口号</li>
  <li>协议类型（TCP）</li>
</ul>

<p>通过这五个元素的组合，操作系统能够唯一识别每个连接，确保数据能够正确发送和接收。</p>

<h2 id="三次握手">三次握手</h2>

<p class="info">为什么要有三次握手?</p>

<p>三次握手的目的是为了初始化连接的传输参数设置，确保双方都准备好进行数据传输：</p>
<ol>
  <li>通过交换序列号来确认双方都准备好接收数据。</li>
  <li>交换必要的连接参数，如窗口大小、序列号等，以保证连接的正常建立。</li>
</ol>

<p>三次握手的过程由以下几个步骤组成：</p>
<ol>
  <li><strong>客户端发送SYN请求</strong>：客户端首先发送一个SYN包，表示请求连接，并选择一个初始序列号。</li>
  <li><strong>服务端回应SYN-ACK</strong>：服务端收到SYN包后，回应一个SYN-ACK包，表示同意连接，并告知其初始序列号。这实际上是两个数据，只是合并在一个数据包里发送出去。</li>
  <li><strong>客户端确认ACK</strong>：客户端收到SYN-ACK包后，发送一个ACK包，确认连接建立。</li>
</ol>

<div align="center">
  <img src="/assets/images/network/tcp-handshake-3.drawio.png" style="height: 300px;" />
</div>
<center>图1: TCP三次握手过程</center>

<p class="warning">为什么需要三次而不是两次？</p>

<p>首先，<strong>三次握手和两次的区别就是</strong>，对<code class="language-plaintext highlighter-rouge">SYNC</code>需不需要进行<code class="language-plaintext highlighter-rouge">ACK</code>？</p>

<p>答案是需要：</p>
<ul>
  <li>
    <p><strong>避免重复连接请求</strong>：客户端和服务端可以通过序列号确认连接请求的唯一性。</p>

    <p>如果SYNC数据包传输延迟较高，客户端进行了重试，那服务端可能会收到2个重复的建立TCP请求(其中一个是已经过期的)，通过ACK的机制，客户端可以通过比对Seq明白哪个ACK是当前连接的。</p>
  </li>
  <li>
    <p><strong>双方同步连接信息</strong>：三次握手确保了客户端和服务端都确认了彼此的接收能力和状态。</p>
  </li>
</ul>

<h2 id="四次挥手">四次挥手</h2>

<p class="info">为什么要有四次挥手?</p>

<p>四次挥手的目的是为了可靠地断开TCP连接，确保双方都能够完成数据的传输，且连接能够彻底关闭。四次挥手的过程如下：</p>

<ol>
  <li><strong>主动关闭方发送FIN包</strong>：当客户端或者服务端完成数据传输后，主动关闭方发送一个FIN包，表示自己不再发送数据。</li>
  <li><strong>接收方确认FIN</strong>：接收方收到FIN包后，发送ACK包确认收到FIN包，并进入关闭等待状态。</li>
  <li><strong>接收方发送FIN包</strong>：接收方也完成数据传输后，向主动关闭方发送一个FIN包，表示自己也不再发送数据。</li>
  <li><strong>主动关闭方确认FIN</strong>：主动关闭方收到接收方的FIN包后，发送ACK包确认，连接正式关闭。</li>
</ol>

<p>四次挥手确保了双方都可以在合适的时机断开连接，且数据不会丢失或被误处理。</p>

<p class="warning">为什么需要四次而不是三次？</p>

<p>因为被关闭方的ACK和FIN不能合在一起发送</p>
<ol>
  <li>当收到FIN后，需要立即回复一个ACK。</li>
  <li>此后将缓冲区未发送的数据发送完毕后，再发送一个FIN包，表示剩余数据传输完毕。</li>
</ol>

<div align="center">
  <img src="/assets/images/network/tcp-close-4.drawio.png" style="height: 300px;" />
</div>
<center>图2: TCP四次握手过程</center>

<h1 id="udp协议">UDP协议</h1>

<h2 id="udp的发展过程">UDP的发展过程</h2>

<p>UDP（用户数据报协议）是TCP协议的对立面，最早由 <strong>David P. Reed</strong> 于1980年提出，并在 <strong>RFC 768</strong> 中进行定义。与TCP不同，UDP是一个无连接的协议，不提供可靠性保障。它的设计目标是低延迟和简单性，适合对速度要求高、对丢包不敏感的应用场景。</p>

<p>相关文档：<a href="https://tools.ietf.org/html/rfc768">RFC 768: User Datagram Protocol</a></p>

<h2 id="有了tcp为什么还需要udp">有了TCP为什么还需要UDP?</h2>

<p>尽管TCP提供了可靠的传输，但它也带来了较大的开销，尤其是在需要快速响应和低延迟的场景中，TCP可能显得过于笨重。UDP没有像TCP那样的连接建立过程、数据确认、流量控制等机制，提供了更为简洁的传输方式。</p>

<p>UDP允许应用层自行处理丢包、顺序、流量控制等问题，因此它更适合需要低延迟且对数据丢失容忍的应用。例如：</p>

<ul>
  <li><strong>实时视频和音频通信</strong>（如视频会议）</li>
  <li><strong>在线游戏</strong>（如多人在线对战游戏）</li>
  <li><strong>DNS查询</strong></li>
</ul>

<p>TCP协议在操作系统内核中得到了统一实现，而UDP则更为灵活，允许用户根据自己的需求实现具体的协议细节。这样，UDP能够提供更大的自由度和效率，但也需要开发者更多的工作来确保协议的可靠性和顺序性。</p>

<h2 id="isp对tcpudp的处理">ISP对TCP、UDP的处理</h2>

<p class="info">数据包的特征与识别</p>

<p>TCP和UDP数据包在协议头部有明确的标识，运营商可以通过这些特征进行识别。例如，TCP数据包包含了源端口、目的端口、序列号等信息，而UDP则通过源端口和目的端口来标识数据包的来源和目标。</p>

<p class="info">ISP(Internet Service Provider, 网络供应商)对UDP的处理</p>

<p>在网络负载较高时，一些ISP会选择性地丢弃UDP数据包，特别是当这些数据包的传输带宽占用较高时。因为UDP没有像TCP那样的流量控制机制，它的传输更容易受到网络拥塞的影响，因此ISP有时会采取策略来丢弃UDP数据包，以保证网络稳定性。</p>

<h1 id="当前tcp和udp的应用">当前TCP和UDP的应用</h1>

<h2 id="tcp的应用">TCP的应用</h2>

<p>由于TCP的可靠性，它已经成为互联网应用的事实标准。大多数需要保证数据可靠传输的应用程序，如HTTP（Web浏览）、FTP（文件传输）、SMTP（电子邮件）等，都使用了TCP。由于TCP实现复杂且在操作系统内核中统一管理，普通用户和应用程序通常无需关心其细节。</p>

<h2 id="udp的应用">UDP的应用</h2>

<p>与TCP不同，UDP的灵活性使其能够在特定场景下提供优异的性能，特别是在实时应用中。UDP常常和应用层协议结合使用，例如：</p>

<ul>
  <li><strong>QUIC协议</strong>：由Google提出，结合了TCP和UDP的优点，用于提升HTTP/2协议的性能，减少延迟。</li>
  <li><strong>实时流媒体</strong>：如视频会议、VoIP等实时传输协议，通常使用UDP以减少延迟。</li>
</ul>

<h1 id="总结">总结</h1>

<p>本文介绍了TCP和UDP协议的基本原理、发展过程及其应用场景。</p>

<ol>
  <li>我们讲解了TCP的本质是由一对相互通讯的<code class="language-plaintext highlighter-rouge">Socket</code>组成的连接，也讲解了建立、断开连接的交互流程和设计本质。</li>
  <li>我们讲解了UDP协议是对TCP的重要补充，基于UDP协议实现的应用层协议在很多场景下解决了TCP的性能问题。</li>
</ol>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="network" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">网络是如何传输的</title><link href="https://wenje.top/post/network-transmit" rel="alternate" type="text/html" title="网络是如何传输的" /><published>2022-10-21T00:00:00+00:00</published><updated>2022-10-21T00:00:00+00:00</updated><id>https://wenje.top/post/network-transmit</id><content type="html" xml:base="https://wenje.top/post/network-transmit"><![CDATA[<!--more-->
<h1 id="前言">前言</h1>

<p>在前面的文章里，我们讲解了现代网络分层模型的发展过程，以及从宏观层面上体现的系统工程思维。
那这篇文章就让我们来看看网络数据的传输过程。我们就以常见的家庭网络环境，说明网络是如何工作的，涉及到网络中的常见设备、设备如何接入网络以及数据包的传输方式。</p>

<h1 id="网络中常见设备">网络中常见设备</h1>

<p>在家庭网络中，最常见的设备包括<strong>交换机</strong>和<strong>路由器</strong>。</p>

<ul>
  <li>交换机: 负责局域网中将不同设备之间的数据转发。交换机依赖MAC地址来进行数据转发，通常它会维护一张 MAC 地址表，通过比对MAC地址来决定将数据包传给哪个接口。</li>
  <li>路由器: 一般作为局域网的互联网接入口，主要用于将局域网与外部网络的数据包转发。</li>
</ul>

<div align="center">
  <img src="/assets/images/network/exchanger-router.drawio.png" style="height: 400px;" />
</div>
<center>图1: 局域网结构</center>

<p>另外，通常来说，现代家用路由器是多功能的，它们统一集成了路由器、交换器、无线AP接入点、DHCP服务器的功能。</p>

<h1 id="网络的常见参数">网络的常见参数</h1>

<p>在家庭局域网中，通常会涉及到以下几个关键的网络组件：</p>

<ul>
  <li><strong>网关（Gateway）</strong>：通常是家用路由器的 IP 地址，它负责在局域网和外部网络之间转发数据包，充当出口。当数据包需要发送给一个局域网外的IP，则都需要先发送给网关，由网关决定下一跳。</li>
  <li><strong>DNS 服务器（DNS Server）</strong>：负责将域名解析为 IP 地址，访问网站时使用。</li>
  <li><strong>子网掩码（Netmask）</strong>：用于确定一个设备的 IP 地址所在的子网范围，帮助设备判断其他设备是否位于同一子网内。</li>
</ul>

<p>这些组件共同作用，使得局域网内的设备能够正常通信并与外部互联网进行交互。</p>

<h1 id="设备接入网络">设备接入网络</h1>

<p>当一个新设备连接到家庭网络时，则首先需要获取上面提到的参数：IP 地址、网关和DNS服务器等信息。这个过程是由DHCP请求来完成的。
具体过程如下:</p>
<ol>
  <li>设备发送一个<code class="language-plaintext highlighter-rouge">DHCP Discover</code>广播请求，用于寻找网络中可用的 DHCP 服务器。</li>
  <li>路由器（局域网中的 DHCP 服务器）收到该请求后，回复一个DHCP Offer，提供为其分配的 IP 地址和其他网络参数。</li>
  <li>设备收到 DHCP Offer 后，发送<code class="language-plaintext highlighter-rouge">DHCP Request</code>，表示它接受分配的 IP 地址。</li>
  <li>最后，DHCP服务器通过<code class="language-plaintext highlighter-rouge">DHCP Acknowledgment</code>确认该 IP 地址的分配，并提供网关和 DNS 等相关信息。</li>
</ol>

<p>这样，设备便成功接入了网络，并拥有了唯一的 IP 地址，能够正常进行数据通信。</p>

<h1 id="数据是如何传输">数据是如何传输</h1>

<h2 id="数据的分包">数据的分包</h2>

<p>在网络中传输的数据通常需要进行<strong>分包</strong>，尤其是当数据量较大时，分包能够有效提高传输效率。这里的分包过程由传输层协议（如 TCP 或 UDP）来完成。通过分包，数据可以更加灵活地在网络中传输，适应不同的网络条件。</p>

<p>这里，我们先不去详细的讲TCP、UDP对数据的拆分和整合，只是先明确数据最终会被拆分成一个个小的数据包进行传输，而这里我们先关注这些数据包是如何在网络间传输的。</p>

<h2 id="设置ip和mac地址">设置IP和MAC地址</h2>

<p>在发送数据包时，首先要明确数据包发送参数：</p>
<ol>
  <li>目标IP地址。这是数据包的最终去向，它标识数据需要到达的目标设备。</li>
  <li>直接转发的MAC地址。用于确定如何在当前子网内进行传输。
IP 地址用于标识网络层的目标，而MAC地址用于标识链路层在局域网内的传输路径。在同一局域网内，MAC地址用于确保数据包能够通过交换机正确转发到目标设备。</li>
</ol>

<p>当设备A要向设备B发送数据时，数据包的发送过程会有所不同，具体取决于B设备是否在同一个子网内(通过子网掩码判断)：</p>

<ul>
  <li><strong>同一局域网内</strong>：如果A和B在同一个子网内，那么数据包的目标MAC地址就是B的MAC地址。数据包通过交换机进行定向传输，交换机会将数据包直接转发给B。</li>
  <li><strong>不同子网</strong>：如果B在外部网络中，A会将数据包的目标IP地址设置为B的IP，但MAC地址设置为网关（通常是路由器）的MAC地址，数据包会先发给路由器，由路由器负责将数据转发到外部网络。</li>
</ul>

<h2 id="arp-协议">ARP 协议</h2>

<p>ARP协议在局域网中起到至关重要的作用，它将IP地址解析为MAC地址，从而实现数据链路层的传输。</p>

<p>通常每个设备都会维护一个<code class="language-plaintext highlighter-rouge">IP-MAC</code>地址表（即ARP表），当设备第一次要向局域网内的某个IP发送数据包，此时会通过ARP协议来局域网内广播，请求目标IP设备的MAC地址。
得到响应回复后，则将MAC地址添加到地址表中保存起来，下次即可直接使用。</p>

<h2 id="有线和无线网络">有线和无线网络</h2>

<ul>
  <li><strong>有线网络</strong>：在有线网络中，交换机负责将数据包定向传输到目标设备，数据只会被传输到匹配的目标端口，其他设备无法接收到该数据包。</li>
  <li><strong>无线网络</strong>：在无线网络中，数据包通过无线信道广播，所有设备都可以接收到广播的数据包，但只有目标设备会继续处理它。无线信道的竞争和冲突避免机制（如 CSMA/CA）确保了无线传输的可靠性。</li>
</ul>

<h2 id="nat过程">NAT过程</h2>

<p>NAT(Network Address Translation)是为了解决IPv4地址短缺问题，允许多个局域网设备共享一个公网IP地址。其工作流程可以分为以下几个步骤：</p>

<ol>
  <li>请求外部访问：局域网内的设备（如设备A）向互联网中的服务器发送请求时，设备A使用私有IP地址作为源地址。该请求首先被路由器接收到。</li>
  <li>地址转换：路由器将设备A的私有IP地址转换为路由器的公网IP地址。为了跟踪多个设备的请求，路由器还会为每个转换记录一个唯一的端口号。路由器维护一个转换表，将局域网内设备的私有IP和端口号映射到公网IP和端口号。</li>
  <li>数据返回：互联网中的服务器响应设备A的请求，并将响应数据包发送回路由器的公网IP地址。路由器根据转换表中的信息，找到对应的私有IP地址和端口号。</li>
  <li>数据包转发：路由器将返回的数据包中的公网IP地址和端口号转换回设备A的私有IP地址和端口号。数据包被正确转发到设备A，完成整个通信过程。</li>
</ol>

<div align="center">
  <img src="/assets/images/network/nat.drawio.png" style="height: 400px;" />
</div>
<center>图2: NAT过程说明</center>

<p>比如在上图中，设备A则是在NAT机制下，和目标服务器完成了通讯。而在目标服务器眼里，和他交互的是<code class="language-plaintext highlighter-rouge">74.125.68.102</code>这个IP，他并不知道A实际上是一个局域网内的机器。</p>

<h2 id="接收端">接收端</h2>

<ol>
  <li>目标IP地址检查: 路由器首先会查看数据包中的目标IP地址是否和自己匹配。如果数据包的目标IP地址匹配，路由器会继续处理；如果不匹配，数据包会被丢弃。</li>
  <li>NAT映射表查找: 路由器检查其NAT映射表。通过匹配数据包中的目标IP地址和目标端口，查找是否有相应的映射条目。</li>
  <li>映射转换: 如果NAT映射表中存在匹配项，路由器会根据映射规则，将数据包的目标地址转换为内网设备的私有IP地址和端口。这个过程确保了数据包能够被正确转发到内网中相应的设备。</li>
  <li>转发到内网设备: 路由器将数据包转发到局域网中指定的设备。通常，路由器会根据私有IP地址和端口号，在局域网内确定目标设备，并将数据包通过交换机或其他网络设备送到目标设备的网络接口。</li>
</ol>

<h1 id="总结">总结</h1>

<p>在本文中提到的各种设备和过程都可以在五层网络模型中找到它们的位置：</p>

<ul>
  <li><strong>物理层</strong>：包括网络接口、网线、无线信号等，所有物理的通信介质和硬件传输。</li>
  <li><strong>数据链路层</strong>：交换机和MAC地址的传输工作，包括<code class="language-plaintext highlighter-rouge">ARP协议</code>，它用于在局域网内将IP地址解析为MAC地址，以便进行链路层的数据传输。</li>
  <li><strong>网络层</strong>：路由器的功能，以及IP地址的寻址和路由的过程，NAT的IP地址转换也属于网络层的内容。</li>
</ul>

<p>所以，总结来看：物理层+数据链路层+网络层这三层实现了将数据包从源地址传输到目标地址的功能。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="network" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">再看现代网络模型</title><link href="https://wenje.top/post/network-layer" rel="alternate" type="text/html" title="再看现代网络模型" /><published>2022-09-10T00:00:00+00:00</published><updated>2022-09-10T00:00:00+00:00</updated><id>https://wenje.top/post/network-layer</id><content type="html" xml:base="https://wenje.top/post/network-layer"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>说起网络分层模型，大家总是会机械地去背那几个名字，物理层、数据链路层、网络层……我们的大脑不善于去记忆一些没有含义的符号，所以只是去记这几个词语没有任何意义。</p>

<p>所以这篇文章的目标是什么呢？是从问题的源头，来探讨<strong>网络分层模型的本质是什么</strong>，分层是为了解决什么问题的，为什么要提出这种方式来处理网络通信。</p>

<p>这里我们不去讨论那些复杂的协议细节，而是从网络分层的演化历史和现状，来说明现代计算机网络的系统结构。</p>

<h1 id="网络的发展">网络的发展</h1>

<p>TCP/IP模型的基础协议定义过程可以从以下RFC文件中找到：</p>

<ul>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc768">RFC 768 - User Datagram Protocol (UDP)</a>：定义了另一种传输层协议UDP，提供无连接、不可靠的数据传输服务。</li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc791">RFC 791 - Internet Protocol (IP)</a>：网络层的核心文档，定义了IP协议的格式、寻址和路由功能。</li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc793">RFC 793 - Transmission Control Protocol (TCP)</a>：描述了传输层的TCP协议，包括连接管理、数据可靠传输等。</li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc1122">RFC 1122 - Requirements for Internet Hosts – Communication Layers</a>：详细描述了每一层在TCP/IP模型中的职责，成为实现TCP/IP协议栈的关键参考。</li>
</ul>

<h2 id="四层模型">四层模型</h2>

<p>也就是所谓的<strong>TCP/IP模型</strong>，基于实际应用的TCP/IP协议栈提出。它是互联网发展的基石，随着ARPANET项目的进行，TCP/IP协议被开发并被用于连接不同的网络设备。</p>

<p>四层模型包含了网络接口层、网络层、传输层和应用层，覆盖了从硬件到用户应用的全过程，是实际网络通信的骨架。</p>

<p>四层模型中的每一层各自承担不同的职责：</p>

<ol>
  <li><strong>网络接口层</strong>：负责物理介质和数据链路的传输，包括通过以太网、Wi-Fi等方式将数据包从设备发送出去或接收进来。典型的网络设备有网卡、集线器（Hub）、交换机等，负责信号的物理传输。</li>
  <li><strong>网络层</strong>：负责不同网络之间的数据传输，主要涉及IP地址的寻址和路由选择，确保数据包能够到达目标设备。典型设备包括路由器，它们通过查看数据包中的目标IP地址决定如何转发。网络层主要考虑的是将单个数据包从一个IP地址传输到另一个IP地址。</li>
  <li><strong>传输层</strong>：负责端到端的数据传输和连接管理，或UDP协议来确保数据的完整性和可靠性。传输层的主要功能是控制数据包的传输顺序、确认和错误重传。通常由操作系统中由协议栈来实现。传输层和网络层的区别在于，网络层只考虑数据包的传递，而传输层则是为了在不可靠的网络环境下构建一个完整、可靠的传输过程。</li>
  <li><strong>应用层</strong>：直接面向用户，提供各种网络服务，例如HTTP、FTP等协议。应用层由运行在设备上的程序实现，例如Web浏览器、电子邮件客户端等。</li>
</ol>

<div align="center">
  <img src="/assets/images/network/network-layer-4.drawio.png" style="height: 300px;" />
</div>
<center>图1: TCP/IP模型</center>

<h2 id="osi七层模型">OSI七层模型</h2>

<p>国际标准化组织（ISO）在20世纪80年代提出，旨在创建一个通用的参考模型，使不同厂商的网络设备可以互相通信。其目标是通过对网络通信过程的精细化分层，提供标准化的协议和接口，确保各层的功能独立、模块化，以便于开发和互操作性。OSI模型希望实现全球范围内网络系统的互联，因此在理论上设计了七个层次，涵盖了从物理传输到应用程序的所有方面。</p>

<p>尽管OSI模型在理论上非常完善，但TCP/IP协议已经在互联网中广泛应用，成为事实上的标准，因此OSI模型未能成功推行。相比之下，TCP/IP协议栈的实现更为简单实用，符合当时互联网快速发展的需求。这种标准化滞后导致了OSI模型的推行失败。</p>

<p>虽然OSI七层模型在理论上非常系统和全面，但它在实践中的推行却遇到了很多问题，导致它未能成为主流标准，原因包括：</p>

<ol>
  <li><strong>复杂性过高</strong>：七层模型在实际实现中显得过于复杂，尤其是会话层和表示层的功能往往在应用层被一并实现，导致理论与实际应用脱节。</li>
  <li><strong>标准化滞后</strong>：在OSI模型被提出时，TCP/IP协议已经在互联网上得到了广泛的应用，成为事实上的标准。因此，OSI模型即使有理论上的优势，但在时间上却未能抢占先机。</li>
  <li><strong>实用性不强</strong>：OSI模型中的某些功能（如会话层的会话管理和表示层的数据格式化）在实际中很难独立实现，更多情况下这些功能被直接集成到应用层中，这使得模型的层次划分显得不够实用。</li>
</ol>

<div align="center">
  <img src="/assets/images/network/network-layer-osi-7.drawio.png" style="height: 400px;" />
</div>
<center>图2: OSI七层模型-TCP/IP模型</center>

<h2 id="五层模型"><strong>五层模型</strong></h2>

<p>是介于这两者之间的存在，通常被用于教育和基础概念的讲解。它简化了OSI模型，但比四层模型多了一些细节划分。五层模型中的层次是物理层、数据链路层、网络层、传输层和应用层，去掉了OSI模型中较为抽象的会话层和表示层，更符合人们理解网络通信的需求。</p>

<div align="center">
  <img src="/assets/images/network/network-layer-5.drawio.png" style="height: 300px;" />
</div>
<center>图3: 五层网络模型</center>

<h1 id="分层的本质职责分离">分层的本质：职责分离</h1>

<p>分层的核心目的是<strong>职责上的拆分</strong>。分层让每一部分都只专注于解决自己的问题，提供清晰的接口和功能，从而简化了开发和维护工作。每一层只专注于和邻近层进行交互，而不需要关心其他层的具体实现。</p>

<p>当一个应用程序（例如浏览器）发送请求时，数据包的传输过程可以分为以下几个步骤：</p>

<ol>
  <li><strong>应用层生成数据</strong>：应用层生成数据并附加目标IP地址和目标端口信息。</li>
  <li><strong>传输层分段并封装</strong>：传输层（如TCP）将数据分割成多个段并加上传输头部。</li>
  <li><strong>网络层选择路径并封装</strong>：根据目标IP地址选择合适的路径，并将数据封装成数据包。</li>
  <li><strong>网络接口层封装成帧并传输</strong>：数据包被传递给网络接口层，在该层中被封装成帧，添加MAC地址，最终通过物理层的介质（如以太网、电缆或Wi-Fi）传输出去。</li>
  <li><strong>路由器转发数据包</strong>：在传输过程中，数据包可能会经过多个路由器，每个路由器负责查看数据包的目标IP地址，并决定将其转发到哪个下一跳。</li>
  <li><strong>目标设备逐层解封装</strong>：目标设备接收到数据包后，逐层进行解封装，最终在应用层呈现给目标应用程序，例如Web服务器收到请求并做出响应。</li>
</ol>

<p class="info">职责分离体现在哪？</p>

<p>对于应用程序来说，它只需要生成数据包并附加目标IP地址和目标端口信息，然后交给传输层和网络层来处理。应用程序并不需要知道数据包如何被拆分、重组，也不关心中间经过多少路由设备。它只负责把数据交给传输层，传输层通过TCP/UDP进行封装，接着由网络层找到合适的路径，最终将数据传输到目标设备的对应端口上。因此，应用层只需要关注它的业务数据，传输的过程由下层协议来保证。</p>

<p>本质上，网络分层模型就是对网络系统的梳理和流程化，是经典的工程思维的体现。通过标准化的流程和接口，将一个极其复杂且不可控的工程问题拆分为若干个小问题，使每一部分都可以独立解决。这种分层的设计使得每一层只需要专注于自己的职责，通过标准化接口与其他层交互，减少了系统的复杂性并提高了开发的可维护性。</p>

<h1 id="总结">总结</h1>

<p>只有真正了解网络发展的历史和分层模型想解决的问题，才能真正理解网络分层模型的意义。计算机网络是人类设计出来的一个非常精妙的系统，也是一个异常庞大的工程，里面很多结构的设计、处理是非常值得我们借鉴到日常工作里的。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="network" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">miniDocker(2) - 文件系统</title><link href="https://wenje.top/post/mdocker-filesystem" rel="alternate" type="text/html" title="miniDocker(2) - 文件系统" /><published>2022-07-16T00:00:00+00:00</published><updated>2022-07-16T00:00:00+00:00</updated><id>https://wenje.top/post/mdocker-filesystem</id><content type="html" xml:base="https://wenje.top/post/mdocker-filesystem"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>在上一篇文章中，我们创建了新的namespace，并在cgroup hierachy上创建对应的节点。同时利用<code class="language-plaintext highlighter-rouge">exec</code>将指定进程放置到了新建的namespace，替换成为了pid为1的<code class="language-plaintext highlighter-rouge">上帝进程</code>，由此形成了一个<code class="language-plaintext highlighter-rouge">容器</code>。</p>

<p>之前我们介绍了docker是如何使用<code class="language-plaintext highlighter-rouge">aufs</code>和<code class="language-plaintext highlighter-rouge">overlay</code>来组织镜像:  <a href="/post/Docker-Storage">Docker是如何生成/保存镜像的?</a>。但此时我们并未在文件系统上做额外处理，所以容器中的进程仍然可以访问到完整的宿主机文件视图，这显然不是我们期望的。</p>

<blockquote>
  <p>所以本文就来解答这个问题: 我该如何修改容器的文件视图，让容器内的进程看起来就像独占了一整个操作系统？</p>
</blockquote>

<h1 id="核心点">核心点</h1>

<h2 id="容器的文件视图组织">容器的文件视图组织</h2>

<p>在项目的<code class="language-plaintext highlighter-rouge">/config/global_config.go</code>文件中，我们可以看到miniDocker对容器文件系统路径的相关定义:</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="p">(</span>
    <span class="n">PathMnt</span>       <span class="o">=</span> <span class="s">"/var/lib/mdocker/overlay2/mnt"</span>
    <span class="n">PathReadWrite</span> <span class="o">=</span> <span class="s">"/var/lib/mdocker/overlay2/rw"</span>
    <span class="n">PathImage</span>     <span class="o">=</span> <span class="s">"/var/lib/mdocker/overlay2/image"</span>
    <span class="n">PathWorkDir</span>   <span class="o">=</span> <span class="s">"/var/lib/mdocker/overlay2/workdir"</span>
<span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PathMnt</td>
      <td>容器根目录挂载点,即overlay2的联合挂载点</td>
    </tr>
    <tr>
      <td>PathReadWrite</td>
      <td>容器的读写层, 会在容器销毁时删除</td>
    </tr>
    <tr>
      <td>PathImage</td>
      <td>镜像的存储路径(如busybox)解压路径</td>
    </tr>
    <tr>
      <td>PathWorkDir</td>
      <td>容器的overlay2 workdir目录</td>
    </tr>
  </tbody>
</table>

<p>我们以创建一个busybox容器为例:</p>
<ul>
  <li>利用以下命令生成<code class="language-plaintext highlighter-rouge">mnt</code>挂载点:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount -t overlay overlay \ 
&gt; lowerdir=&lt;PathImage&gt;/busybox,\
&gt; upperdir=&lt;PathRW&gt;/containerName,\
&gt; workdir=&lt;PathWorkDir&gt;/containerName &lt;PathMnt&gt;/containerName
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="pivot-root">pivot-root</h2>

<p>现在我们得到了一个”看起来像”是操作系统的目录:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/ <span class="c"># ls</span>
bin dev etc home proc root sys tmp usr var
</code></pre></div></div>
<p>那如何让容器的根目录挂载到overlay2生成的文件视图上呢？答案就是<code class="language-plaintext highlighter-rouge">pivot_root</code>这个黑魔法。</p>

<p>我们先来看看<code class="language-plaintext highlighter-rouge">pivot_root</code>的<a href="https://man7.org/linux/man-pages/man2/pivot_root.2.html">man page</a>介绍，pivot_root的command使用方法为:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pivot_root new_root put_old
</code></pre></div></div>
<p>当执行上面的命令后，会发生这么两件事:</p>
<ol>
  <li>当前进程的<code class="language-plaintext highlighter-rouge">new_root</code>会被设置成<code class="language-plaintext highlighter-rouge">/</code>根目录。</li>
  <li>原根目录会被移动到<code class="language-plaintext highlighter-rouge">put_old</code>目录下。</li>
</ol>

<p>当然，这是有限制条件的:</p>
<ol>
  <li>new_root和put_old必须是目录，且new_root必须是mount挂载点并且不能是当前根目录。</li>
  <li>put_old必须是new_root或者其子目录。</li>
</ol>

<p>是不是感觉有点晕？那我们换个说法，<code class="language-plaintext highlighter-rouge">pivot</code>这个单词表示<code class="language-plaintext highlighter-rouge">枢轴、转动</code>，按照上面的描述，name各个路径的关系如下:</p>

<p><img src="/assets/images/mdocker/filesystem-struct.png" alt="" /></p>
<center>图1: pivot_root示意图(1)</center>

<p>这个时候，我们把<code class="language-plaintext highlighter-rouge">old_root</code>和<code class="language-plaintext highlighter-rouge">new_root</code>之间的路径视为一个<code class="language-plaintext highlighter-rouge">pivot</code>:</p>
<p align="center">
 <img src="/assets/images/mdocker/pivot_root.png" width="500" height="350" />
</p>

<center>图2: pivot_root示意图(2)</center>

<p>让pivot旋转以下，是不是就像描述的那样，<code class="language-plaintext highlighter-rouge">new_root</code>被<code class="language-plaintext highlighter-rouge">旋转</code>到了根目录的位置上。这样容器的overlay2挂载点就成为了容器namespace里的root dir。</p>

<p>但是pivot_root并不会改动当前调用进程的workdir，所以我们需要进行手动修改: <code class="language-plaintext highlighter-rouge">chdir /</code></p>

<h2 id="构建新的文件系统">构建新的文件系统</h2>

<p>在利用<code class="language-plaintext highlighter-rouge">pivot_root</code>修改了根目录的挂载点后，剩余的事情就简单很多了:</p>
<ul>
  <li>卸载<code class="language-plaintext highlighter-rouge">old_root</code>这个挂载点，使用detach的方式移除原文件系统的挂载。由于当前系统必然依赖原文件系统，所以使用detach懒卸载的方式移除调原文件系统。</li>
  <li>重新挂载<code class="language-plaintext highlighter-rouge">/proc</code>和<code class="language-plaintext highlighter-rouge">/dev</code>等系统级目录。其中<code class="language-plaintext highlighter-rouge">/dev</code>使用<code class="language-plaintext highlighter-rouge">tmpfs</code>，这是一个基于内存的文件系统。</li>
</ul>

<h1 id="代码实现讲解">代码实现讲解</h1>

<h2 id="总体流程">总体流程</h2>

<center>
 <img src="/assets/images/mdocker/filesystem-flow.png" width="450" height="700" />
  <p>图3: mdocker文件系统创建流程</p>
</center>

<h2 id="创建overlay2文件系统">创建overlay2文件系统</h2>

<p>overlay2文件系统的创建在<code class="language-plaintext highlighter-rouge">mdocker run</code>进程中执行:</p>
<ul>
  <li>根据imageName和用户volume参数，生成overlay2文件系统mnt point。</li>
  <li>创建<code class="language-plaintext highlighter-rouge">mdocker init</code>进行，并将其工作目录设置为刚才创建的overlay2文件系统mnt point。</li>
</ul>

<h2 id="调整容器namespace的mount结构">调整容器(namespace)的mount结构</h2>

<p>本节的主体逻辑在<code class="language-plaintext highlighter-rouge">/container/container_init/mount_init.go</code>中，这个逻辑在<code class="language-plaintext highlighter-rouge">mdocker init</code>命令的进程中完成。</p>

<p>逻辑调用链如下:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// /cmd/init_cmd.go</span>
<span class="k">func</span> <span class="n">initCmdAction</span><span class="p">(</span><span class="n">ctx</span> <span class="o">*</span><span class="n">cli</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">container_init</span><span class="o">.</span><span class="n">ContainerProcessInit</span><span class="p">()</span>
<span class="p">}</span>

<span class="c">// /container/container_init/container_process.go</span>
<span class="k">func</span> <span class="n">ContainerProcessInit</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// ...</span>

	<span class="c">// 初始化容器mount</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">mountInit</span><span class="p">();</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"mount init failed: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// ...</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="c">// /container/container_init/mount_init.go</span>
<span class="k">func</span> <span class="n">mountInit</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// 改变当前Namespace的Mount传播模式</span>
	<span class="n">err</span> <span class="o">:=</span> <span class="n">syscall</span><span class="o">.</span><span class="n">Mount</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="s">"/"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="kt">uintptr</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">MountFlagsPrivate</span><span class="p">),</span> <span class="s">""</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>

	<span class="c">// ...</span>

	<span class="c">// 调用pivot切换rootfs</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">pivotRoot</span><span class="p">(</span><span class="n">pwd</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>

	<span class="c">// ...</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>值得注意的是，在调整容器的mount结构前，需要先将namespace下的mount传播模式改为private，否则这些操作会被传播到外部。</p>

<h1 id="结束语">结束语</h1>

<p>本讲我们降到如何为容器生成overlay2文件系统，并且其作为容器的文件根目录。这样一来，容器就脱离了宿主机的文件系统，在自己看来就是<code class="language-plaintext highlighter-rouge">独占</code>了一个完整的存储设备。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="docker" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">All-In-One Home Server搭建</title><link href="https://wenje.top/post/home-server" rel="alternate" type="text/html" title="All-In-One Home Server搭建" /><published>2022-06-14T00:00:00+00:00</published><updated>2022-06-14T00:00:00+00:00</updated><id>https://wenje.top/post/home-server</id><content type="html" xml:base="https://wenje.top/post/home-server"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>最近组装了一个Linux服务器放在家里，因为家里没有公网ip所以还调研了一些内网穿透方案。这篇文章给大家分享一下我搭建家用服务器的过程。</p>

<h1 id="为什么我要搭一个home-server">为什么我要搭一个home-server?</h1>

<p>主要原因有这么几个:</p>
<ol>
  <li>云服务器性价比低，就阿里云来说一个16G4核的机器一个月动辄几百，而我个人用的云服务器大部分情况下都是空负载。</li>
  <li>最近PC换成了M1的mbp，折腾很多东西都需要x86机构的机器。</li>
  <li>想自己搭建nas服务，存储一些个人数据。</li>
</ol>

<h1 id="硬件采购">硬件采购</h1>

<table>
  <thead>
    <tr>
      <th>配件名</th>
      <th>型号</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>cpu</td>
      <td>intel-12600K</td>
    </tr>
    <tr>
      <td>内存</td>
      <td>16G DDR4 x 4</td>
    </tr>
    <tr>
      <td>主板</td>
      <td>铭瑄 B660M Wifi版</td>
    </tr>
    <tr>
      <td>硬盘</td>
      <td>固态:2T(数据盘)+512G(系统盘), 机械硬盘4Tx2</td>
    </tr>
    <tr>
      <td>机箱</td>
      <td>宝藏盒</td>
    </tr>
    <tr>
      <td>散热</td>
      <td>240水冷</td>
    </tr>
    <tr>
      <td>电源</td>
      <td>300W</td>
    </tr>
  </tbody>
</table>

<p>在选配件的时候得想清楚到底想要一台怎样的机器。对我来说，主要考虑的点有:</p>
<ol>
  <li>机箱体积要小，因为我希望摆到衣柜顶上然后我家猫就霍霍不到了。</li>
  <li>All-In-One，所以性能要稍微好一些。</li>
  <li>运行时安静一些，所以选择了水冷来压这颗i5 12600K。</li>
  <li>我家目前带宽只有千兆，所以我没有单独买网卡，直接用了主板上的千兆网卡。</li>
</ol>

<h1 id="操作系统安装">操作系统安装</h1>

<h2 id="系统选择">系统选择</h2>

<p>由于采用了12代的cpu，所以需要选择Linux内核版本较高的发行版。
我选择的是<code class="language-plaintext highlighter-rouge">Ubuntu 22.04 LTS</code>，内核版本为<code class="language-plaintext highlighter-rouge">5.15.0</code>。镜像从官网下载即可<a href="https://cn.ubuntu.com/download/desktop">ubuntu下载</a></p>

<p>这个版本我目前使用上没有遇到什么问题，12代u能在这个版本上正常工作。</p>

<h2 id="mac下启动盘制作">mac下启动盘制作</h2>

<p>mac系统下引导u盘制作可参考以下过程:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 调整格式为img</span>
hdiutil convert <span class="nt">-format</span> UDRW ubuntu-22.04-desktop-amd64.iso <span class="nt">-o</span> ubuntu.iso
<span class="c"># 找到u盘dev</span>
diskutil list <span class="c"># 找到external的挂载硬件</span>
<span class="c"># 取消挂载</span>
<span class="nb">sudo </span>diskutil umountDisk /dev/disk4
<span class="c"># 格式化u盘</span>
<span class="nb">sudo </span>diskutil eraseDisk ExFAT ud /dev/disk4
<span class="c"># 将系统镜像写入u盘</span>
<span class="nb">sudo dd </span><span class="k">if</span><span class="o">=</span>./ubuntu.iso <span class="nv">of</span><span class="o">=</span>/dev/disk4 <span class="nv">bs</span><span class="o">=</span>1m
</code></pre></div></div>

<h1 id="服务管理方案">服务管理方案</h1>

<h2 id="服务搭建">服务搭建</h2>

<p>所有服务都是用Docker运行的，目前我用到的有:</p>
<ol>
  <li>gitlab -&gt; 个人代码仓库</li>
  <li>codimd -&gt; 笔记服务，codimd的markdown编辑器十分强大，用过的都说好</li>
  <li>nextcloud -&gt; 私有Drive服务</li>
  <li>博客nginx服务</li>
</ol>

<h2 id="硬盘管理">硬盘管理</h2>

<p>服务器上安装的硬盘有:</p>
<ul>
  <li>固态512G+2T</li>
  <li>机械硬盘4T x2</li>
</ul>

<p class="info">使用划分</p>
<ol>
  <li>512G为系统盘，挂载到”/”目录下。</li>
  <li>机械硬盘划分为lvm物理设备，所有的硬盘都由一个<code class="language-plaintext highlighter-rouge">vg(volume group)</code>管理。通过划分<code class="language-plaintext highlighter-rouge">lv(logic volume)</code>来划分硬盘。已划分的lv可以扩容。</li>
</ol>

<h2 id="内网穿透">内网穿透</h2>

<p>由于没有公网ip，所以外部是无法直接访问的。我的方案是用一台云服务器运行<code class="language-plaintext highlighter-rouge">frp</code>转发流量，在外面可以通过云服务器的公网ip访问家用服务器。</p>

<p>具体搭建可参照文档<a href="https://gofrp.org/docs/overview/">gofrp</a></p>

<h2 id="重要数据备份">重要数据备份</h2>

<p>对于一些比较重要的数据，比如codimd中我记录的笔记，如果某一天家里进贼了把我的硬盘都顺走了那我可要哭死了。
所以服务器上创建cron job每天定时将数据同步到云服务器上，以及不定期的备份到移动硬盘里备份。说白了，就是不要把鸡蛋放在一个篮子里。</p>

<h1 id="结束语">结束语</h1>

<p>以上就是我的home-server方案，其实这个方案比较适合的还是家里有公网ip的人。廉价的云服务器带宽并不能满足一些大文件的传输，仅仅只能满足基本的web访问以及git代码同步。</p>

<p>对于那些长期需要有一台服务器工作，但并不是长时间高负载的人来说，在家里搭建一个服务器可以说一个不错的选择。如果不是用于AI模型训练等，电费几乎可以忽略不计，长时间下来成本上肯定会比租云服务器要划算。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="general" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">miniDocker(1) - 容器创建</title><link href="https://wenje.top/post/mdocker-base" rel="alternate" type="text/html" title="miniDocker(1) - 容器创建" /><published>2022-05-16T00:00:00+00:00</published><updated>2022-05-16T00:00:00+00:00</updated><id>https://wenje.top/post/mdocker-base</id><content type="html" xml:base="https://wenje.top/post/mdocker-base"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>之前的几篇文章对docker依赖的Linux底层技术进行了剖析讲解, 我们现在简单回顾一下:</p>
<ul>
  <li>容器实际上是通过Linux的Namespace机制隔离起来的一个”沙箱”， 沙箱中运行着一组进程。</li>
  <li>容器通过cgroup对容器进行分组和使用资源限制。</li>
  <li>容器通过aufs组织文件系统，将不变的镜像作为只读层，在其之上是容器的读写层。</li>
  <li>容器利用虚拟网络设备和iptables进行网络通讯，典型的网络模型为bridge网络。</li>
</ul>

<p>那从这篇开始，我们就根据这些特性实现一个mini版的docker。</p>

<p>代码已经开源在github: <a href="https://github.com/banshee1/miniDocker">miniDocker</a></p>

<h1 id="准备工作">准备工作</h1>

<h2 id="创建容器涉及的代码文件">创建容器涉及的代码文件</h2>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">.</span>
├── boot
│   └── init.go                   // 环境初始化
├── cmd
│   ├── init_cmd.go               // mdocker init 逻辑入口
│   └── run_cmd.go                // mdocker run 逻辑入口
├── config
│   └── global_config.go          // 全局配置
├── container
│   ├── cgroups
│   │   ├── cgroup_manager.go     // cgroup管理
│   │   └── subsystems            // subsystem具体设置处理逻辑
│   └── container_init
│      └── container_process.go   // 创建容器的init进程
├── entry.go                      // 程序入口文件
└── utils
    └── logger_utils.go           // print log方法
</code></pre></div></div>

<h1 id="创建容器">创建容器</h1>

<h2 id="如何创建一个容器">如何创建一个容器?</h2>

<p>我们先来看一个命令:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mdocker run <span class="nt">-ti</span> busybox sh
</code></pre></div></div>
<p>当你敲下这行命令，一个以<code class="language-plaintext highlighter-rouge">busybox</code>为镜像的容器就启动了，在容器中运行着一个<code class="language-plaintext highlighter-rouge">sh</code>进程。</p>

<p>现在让我们来思考一个问题，之前我们在讲解namespace时分别用了<code class="language-plaintext highlighter-rouge">unshare</code>和go程序调用shell命令来启动新进程同时创建namespace，那我们在mdocker中如何来做这件事的呢？</p>

<p><img src="/assets/images/mdocker/container-launch.png" alt="" /></p>
<center>图1: container启动流程</center>

<p>如上图所示，<code class="language-plaintext highlighter-rouge">mdocker run</code>启动容器的流程为:</p>
<ol>
  <li>创建一个新的进程<code class="language-plaintext highlighter-rouge">mdocker init</code>，这个命令只用于创建容器时使用且为非用户指令。创建进程时，会新建一个pipe并将一端设置到init进程上，这个pipe用于传递用户命令。</li>
  <li><code class="language-plaintext highlighter-rouge">mdocker run</code>进程在创建init进程后，还需要在外部执行一些环境设置逻辑，处理完成后将user command通过pipe发送给<code class="language-plaintext highlighter-rouge">init进程</code>。</li>
  <li>init进程读取到用户命令后，先执行容器环境设置，如文件系统等。之后调用<code class="language-plaintext highlighter-rouge">exec</code>执行用户命令。</li>
</ol>

<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// run命令主逻辑</span>
<span class="k">func</span> <span class="n">run</span><span class="p">(</span><span class="n">ctx</span> <span class="o">*</span><span class="n">cli</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// 创建 mdocker init进程</span>
	<span class="n">initCmd</span><span class="p">,</span> <span class="n">initPipe</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">container_init</span><span class="o">.</span><span class="n">NewContainerProcess</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">=</span> <span class="n">initCmd</span><span class="o">.</span><span class="n">Start</span><span class="p">();</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>

	<span class="c">// 将用户命令指定命令通过pipe传递给init进程</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">sendInitCommandParams</span><span class="p">(</span><span class="n">cmdArr</span><span class="p">,</span> <span class="n">initPipe</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>

	<span class="c">// 当设置了ti参数时, 等待init进程退出</span>
	<span class="k">if</span> <span class="n">ctx</span><span class="o">.</span><span class="n">Bool</span><span class="p">(</span><span class="n">runCmdFlagTty</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">_</span> <span class="o">=</span> <span class="n">initCmd</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>容器启动涉及到2个进程，run进程和init进程。run进程是用户直接创建的进程，由于是用户直接创建的，所以run进程是运行在container外的。
而container是在创建init进程时，指定<code class="language-plaintext highlighter-rouge">clone_flag</code>创建namespace后形成的。
这样设置的目的是:</p>
<ul>
  <li>run进程和init进程处于容器内外，可以分别负责为容器设置环境。如run进程负责记录一些容器信息、设置cgroup、初始化网络等。</li>
  <li>实现起来比较简单。</li>
</ul>

<h2 id="exec替换init进程">exec替换init进程</h2>

<p>容器的最终目的是要运行用户目标程序，所以init进程还有一个任务就是要负责启动目标程序。最直接的办法当然是直接创建一个新进程。
但init进程在container的namespace中是pid为1的进程，当init进程退出时namespace会被操作系统自动销毁，并且在namespace下的所有进程都会收到signal退出运行。</p>

<p>这里就要使用到linux的一个系统调用-<code class="language-plaintext highlighter-rouge">exec</code>。exec会用目标程序替换init进程，这个过程中保持pid不变，只是原init进程的上下文和数据栈等被替换为目标程序。</p>

<p>所以我们在init进程处理完容器初始化后，用exec来启动目标进程，这样就能让目标程序在容器中的进程id为1。</p>
<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// ContainerProcessInit 容器init进程初始化</span>
<span class="k">func</span> <span class="n">ContainerProcessInit</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="c">// call exec to replace current process, cmdArray: exec file path(only used for display), params...</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">=</span> <span class="n">syscall</span><span class="o">.</span><span class="n">Exec</span><span class="p">(</span><span class="n">cmdPath</span><span class="p">,</span> <span class="n">cmdArray</span><span class="p">[</span><span class="m">0</span><span class="o">:</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">Environ</span><span class="p">());</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">utils</span><span class="o">.</span><span class="n">LoggerUtil</span><span class="o">.</span><span class="n">Fatalf</span><span class="p">(</span><span class="s">"mount fail: %s"</span><span class="p">,</span> <span class="n">err</span><span class="o">.</span><span class="n">Error</span><span class="p">())</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在linux环境下，也可以用exec命令来使用这个系统调用:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="nv">$$</span>
24871
<span class="nv">$ </span><span class="nb">exec</span> /bin/bash
<span class="nv">$ </span><span class="nb">echo</span> <span class="nv">$$</span> <span class="c"># 进程id没有变化</span>
24871
</code></pre></div></div>

<h2 id="cgroup-hierarchy节点创建">cgroup hierarchy节点创建</h2>

<p>我们需要为container创建一个cgroup节点，然后将init进程分配到对应的节点中。
在介绍cgroup hierarchy时，我们提到linux默认在<code class="language-plaintext highlighter-rouge">/sys/fs/cgroup</code>下创建了默认的cgroup hierarchy tree并将subsystem与之绑定。</p>

<p>对于一个cgroup，我们按照以下结构去创建container节点:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">.</span>
├── ...
├── mdocker
    ├── &lt;container_id 1&gt;
    └── &lt;container_id 2&gt;
</code></pre></div></div>
<p>在cgroup根目录下创建mdocker节点，作为所有容器节点的根节点。在mdocker下，则是对应每个container对应的cgroup node。</p>

<p>cgroup的处理逻辑通过<code class="language-plaintext highlighter-rouge">cgroupManager</code>实现，具体操作则通过路径创建/删除、文件写入实现。</p>

<h1 id="结束语">结束语</h1>

<p>本文讲解了mdocker创建容器的流程，解析了关键的逻辑点。但是具体的代码实现还是要大家自己去结合注释去阅读、调试。本系列文章也不会一行行代码地去解释，而是讲解实现上的关键点，欢迎大家去阅读github仓库中的代码。</p>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="docker" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Docker容器是如何与外界通讯的?</title><link href="https://wenje.top/post/docker-network" rel="alternate" type="text/html" title="Docker容器是如何与外界通讯的?" /><published>2022-04-28T00:00:00+00:00</published><updated>2022-04-28T00:00:00+00:00</updated><id>https://wenje.top/post/docker-network</id><content type="html" xml:base="https://wenje.top/post/docker-network"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>在之前的文章中，我们介绍了Docker容器隔离机制namespace，资源管理工具CGroup以及Docker使用的文件系统unionFS。我们知道了Container其实就是被Linux内核用”沙箱”隔离起来的一组进程。</p>

<p>但是光靠这些还不够，linux新创建的<code class="language-plaintext highlighter-rouge">network namespace</code>初始状态下是不带任何网络功能的，这篇文章就会为大家解析容器是如何实现与外界的网络交互的。</p>

<h1 id="虚拟网络设备">虚拟网络设备</h1>

<h2 id="linux-网络数据流向">Linux 网络数据流向</h2>
<div align="center">
  <img src="/assets/images/docker-network/linux-net-hierarchy.png" style="height: 600px;" />
</div>
<center>图1: Linux接受网络数据流程图示</center>

<p>计算机与外界网络通讯往往要依赖网卡，网卡和物理网络线路相连，将电信号/光信号转化为数字信号。然后内存对信号进行解析，交由上层的协议栈，最终交付给用户层。</p>

<p>而Linux内核在将网络包交付给协议栈之前，有一个<code class="language-plaintext highlighter-rouge">网络设备层</code>层。其中的网络设备可以是真实的(比如默认网卡eth0)或者虚拟的，对内核来说，他们之间没有区别。</p>

<p>这里我们不去讨论具体虚拟网络设备是如何实现的，大家只需要建立一个概念，就是我们可以在内核中添加虚拟网络设备来创建一个内核中的”虚拟网络”。就像局域网一样大家都可以有自己不同的ip地址、mac地址。</p>

<p>而发送数据包的流程和接受类似，希望能更深入了解的可以阅读附录中的文章。</p>

<h2 id="network-namespace的协议栈">network namespace的协议栈</h2>

<p>对于一个<code class="language-plaintext highlighter-rouge">network netspace</code>来说，他拥有自己独立的协议栈，以及独立的网络设备挂载表。
接下来，我们会介绍如何使用veth和bridge设备让子network namespace可以和其他子空间或外部的网络进行交互。</p>

<h1 id="veth">veth</h1>

<h2 id="使用veth连接netns">使用veth连接netns</h2>
<p>veth(virtual ethernet)设备具有以下特点:</p>
<ul>
  <li>成对出现</li>
  <li>一端连接协议栈，另一端peer之间对接</li>
</ul>

<div align="center">
  <img src="/assets/images/docker-network/veth-connect1.png" style="height: 300px;" />
</div>
<center>图2: veth设备连接示意图</center>
<p>一对veth可以连接不同的协议栈，即当veth挂载到不同的<code class="language-plaintext highlighter-rouge">network namespace</code>或者<code class="language-plaintext highlighter-rouge">container</code>时，就变成了这样:</p>

<div align="center">
  <img src="/assets/images/docker-network/veth-connect2.png" style="height: 300px;" />
</div>
<center>图3: Container通过veth-pair相连</center>
<h2 id="如何使用veth">如何使用veth</h2>
<p>首先利用<code class="language-plaintext highlighter-rouge">ip</code>命令创建两个netns:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns add ns1
<span class="nv">$ </span>ip netns add ns2
</code></pre></div></div>
<p>创建一对veth:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip <span class="nb">link </span>add veth1 <span class="nb">type </span>veth peer name veth2
</code></pre></div></div>
<p>启动这两个veth，分配ip并分别设置到新创建的两个<code class="language-plaintext highlighter-rouge">netns</code>中:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip <span class="nb">link set </span>dev veth1 up
<span class="nv">$ </span>ip <span class="nb">link set </span>veth1 netns ns1
<span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ifconfig veth1 192.168.1.1/24 up

<span class="nv">$ </span>ip <span class="nb">link set </span>dev veth2 up
<span class="nv">$ </span>ip <span class="nb">link set </span>veth2 netns ns2
<span class="nv">$ </span>ip netns <span class="nb">exec </span>ns2 ifconfig veth2 192.168.1.2/24 up
</code></pre></div></div>
<p>设置完成后，我们就可以在netns中测试网络可达:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns <span class="nb">exec </span>ns2 ping <span class="nt">-c</span> 1 192.168.1.1 <span class="nt">-I</span> veth2
PING 192.168.1.1 <span class="o">(</span>192.168.1.1<span class="o">)</span> from 192.168.1.2 veth2: 56<span class="o">(</span>84<span class="o">)</span> bytes of data.
64 bytes from 192.168.1.1: <span class="nv">icmp_seq</span><span class="o">=</span>1 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.057 ms

<span class="nt">---</span> 192.168.1.1 ping statistics <span class="nt">---</span>
1 packets transmitted, 1 received, 0% packet loss, <span class="nb">time </span>0ms
rtt min/avg/max/mdev <span class="o">=</span> 0.057/0.057/0.057/0.000 ms
</code></pre></div></div>

<p>我们在ns2中的veth2上进行抓包:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns <span class="nb">exec </span>ns2 tcpdump <span class="nt">-nn</span> <span class="nt">-i</span> veth2
tcpdump: verbose output suppressed, use <span class="nt">-v</span> or <span class="nt">-vv</span> <span class="k">for </span>full protocol decode
listening on veth2, link-type EN10MB <span class="o">(</span>Ethernet<span class="o">)</span>, capture size 262144 bytes

11:09:11.150688 IP 192.168.1.1 <span class="o">&gt;</span> 192.168.1.2: ICMP <span class="nb">echo </span>request, <span class="nb">id </span>12550, <span class="nb">seq </span>1, length 64
11:09:11.150771 IP 192.168.1.2 <span class="o">&gt;</span> 192.168.1.1: ICMP <span class="nb">echo </span>reply, <span class="nb">id </span>12550, <span class="nb">seq </span>1, length 64
11:09:12.679528 IP6 fe80::80de:ccff:fe40:dfe4 <span class="o">&gt;</span> ff02::2: ICMP6, router solicitation, length 16
</code></pre></div></div>
<p>ns1发出的ICMP包转发到了veth2上，至此ns1和ns2之间建立了网络通道。</p>

<h1 id="bridge">bridge</h1>

<p>在上面的例子中，虽然ns1和ns2通过一对veth连接了起来，但是这时只做到了2个namespace之间的访问。如果我们有多个namespace，那所有需要访问的namespace之间都需要一对veth，很明显这不是一种聪明的做法。</p>

<h2 id="bridge介绍">bridge介绍</h2>

<p>bridge作为一个虚拟网络设备，也有自己的ip地址、mac地址。而和普通的网络设备不同，bridge的数据出口有多端，会根据mac地址进行转发，原理和交换机类似。</p>

<p>我们可以对上面的veth进行些许改造，在netns之外创建一个bridge，而netns则通过veth对接入到bridge之上:</p>

<div align="center">
  <img src="/assets/images/docker-network/bridge1.png" style="height: 450px;" />
</div>
<center>图4: container通过bridge相连</center>

<h2 id="使用bridge连接veth-pair">使用bridge连接veth-pair</h2>

<p>使用<code class="language-plaintext highlighter-rouge">brctl</code>创建一个bridge设备并分配ip:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brctl addbr br0
<span class="nv">$ </span>ip <span class="nb">link set </span>dev br0 up
<span class="nv">$ </span>ifconfig br0 192.168.1.11/24 up
</code></pre></div></div>

<p>创建veth-pair，将ns1通过veth-pair连接到br0上:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip <span class="nb">link </span>add veth1 <span class="nb">type </span>veth peer name br-veth1 <span class="c"># 创建veth-pair</span>
<span class="nv">$ </span>brctl addif br0 br-veth1 <span class="c"># 将br-veth1端连接到br0上</span>
<span class="nv">$ </span>ip <span class="nb">link set </span>dev br-veth1 up  <span class="c"># 启动br-veth1</span>
<span class="nv">$ </span>ip <span class="nb">link set </span>veth1 netns ns1  <span class="c"># 将veth1设置到ns1中</span>
<span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ip <span class="nb">link set </span>dev veth1 up  <span class="c"># 启动veth1</span>
<span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ifconfig veth1 192.168.1.1/24 up  <span class="c"># 给veth1分配ip</span>
<span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ip route add default via 192.168.1.11  <span class="c"># 设置ns1默认路由</span>
</code></pre></div></div>
<p>对ns2做相同的处理。</p>

<p>设置完成后，我们就可以测试ns1和ns2之间网络可达:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ping 192.168.1.2 <span class="nt">-c</span> 1
PING 192.168.1.2 <span class="o">(</span>192.168.1.2<span class="o">)</span> 56<span class="o">(</span>84<span class="o">)</span> bytes of data.
64 bytes from 192.168.1.2: <span class="nv">icmp_seq</span><span class="o">=</span>1 <span class="nv">ttl</span><span class="o">=</span>64 <span class="nb">time</span><span class="o">=</span>0.134 ms

<span class="nt">---</span> 192.168.1.2 ping statistics <span class="nt">---</span>
1 packets transmitted, 1 received, 0% packet loss, <span class="nb">time </span>0ms
rtt min/avg/max/mdev <span class="o">=</span> 0.134/0.134/0.134/0.000 ms
</code></pre></div></div>

<p>在br0进行抓包:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>tcpdump <span class="nt">-nn</span> <span class="nt">-i</span> br0
tcpdump: verbose output suppressed, use <span class="nt">-v</span> or <span class="nt">-vv</span> <span class="k">for </span>full protocol decode
listening on br0, link-type EN10MB <span class="o">(</span>Ethernet<span class="o">)</span>, capture size 262144 bytes

12:38:50.845461 IP 192.168.1.1 <span class="o">&gt;</span> 192.168.1.2: ICMP <span class="nb">echo </span>request, <span class="nb">id </span>13124, <span class="nb">seq </span>1, length 64
12:38:50.845549 IP 192.168.1.2 <span class="o">&gt;</span> 192.168.1.1: ICMP <span class="nb">echo </span>reply, <span class="nb">id </span>13124, <span class="nb">seq </span>1, length 64
</code></pre></div></div>
<p>在ns2中抓包:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns <span class="nb">exec </span>ns2 tcpdump <span class="nt">-nn</span> <span class="nt">-i</span> veth2
tcpdump: verbose output suppressed, use <span class="nt">-v</span> or <span class="nt">-vv</span> <span class="k">for </span>full protocol decode
listening on veth2, link-type EN10MB <span class="o">(</span>Ethernet<span class="o">)</span>, capture size 262144 bytes

12:39:41.973639 IP 192.168.1.1 <span class="o">&gt;</span> 192.168.1.2: ICMP <span class="nb">echo </span>request, <span class="nb">id </span>13128, <span class="nb">seq </span>1, length 64
12:39:41.973680 IP 192.168.1.2 <span class="o">&gt;</span> 192.168.1.1: ICMP <span class="nb">echo </span>reply, <span class="nb">id </span>13128, <span class="nb">seq </span>1, length 64
</code></pre></div></div>
<p>可以看到实际上包是由br0转到ns中的veth2的。</p>

<p>再尝试ping一个外网的ip，不过在这之前我们需要先修改下iptables的规则，否则收到的私有地址包默认会被丢弃:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>iptables <span class="nt">-t</span> nat <span class="nt">-A</span> POSTROUTING <span class="nt">-s</span> 192.168.1.0/24 <span class="nt">-j</span> MASQUERADE
<span class="nv">$ </span>iptables <span class="nt">-t</span> nat <span class="nt">-D</span> POSTROUTING <span class="nt">-s</span> 192.168.1.0/24 <span class="nt">-j</span> MASQUERADE <span class="c"># 还原</span>
</code></pre></div></div>

<p>然后我们尝试ping一下baidu的ip:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns <span class="nb">exec </span>ns1 ping 220.181.38.251 <span class="nt">-c</span> 1
PING 220.181.38.251 <span class="o">(</span>220.181.38.251<span class="o">)</span> 56<span class="o">(</span>84<span class="o">)</span> bytes of data.
64 bytes from 220.181.38.251: <span class="nv">icmp_seq</span><span class="o">=</span>1 <span class="nv">ttl</span><span class="o">=</span>52 <span class="nb">time</span><span class="o">=</span>5.23 ms

<span class="nt">---</span> 220.181.38.251 ping statistics <span class="nt">---</span>
1 packets transmitted, 1 received, 0% packet loss, <span class="nb">time </span>0ms
</code></pre></div></div>
<p>成功ping通，现在nets内、外网的通讯都建立了。</p>

<h2 id="清理实验环境">清理实验环境</h2>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ip netns delete ns1 ns2 <span class="c"># 删除ns1和ns2后，创建的veth-pair会自动删除</span>
<span class="nv">$ </span>ifconfig br0 down <span class="c"># 停止br0</span>
<span class="nv">$ </span>brctl delbr br0 <span class="c"># 删除br0</span>
</code></pre></div></div>

<h1 id="结束语">结束语</h1>

<p>本文解析了如何利用虚拟网络设备让容器具有网络交互的能力，结合之前的文档，我们的docker实现原理基础就告一段落了。接下来会从0开始一步一步的实现容器，逐步为它加上隔离、挂载、网络通讯等功能。</p>

<p>敬请期待吧~</p>

<h1 id="reference">reference</h1>
<ol>
  <li><a href="https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-receiving-data/">Monitoring and Tuning the Linux Networking Stack: Receiving Data</a></li>
  <li><a href="http://icyfenix.cn/immutable-infrastructure/network/linux-vnet.html">Linux网络虚拟化</a></li>
  <li><a href="http://www.opencloudblog.com/?p=66">Linux Switching – Interconnecting Namespaces</a></li>
</ol>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="docker" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Docker是如何生成/保存镜像的?</title><link href="https://wenje.top/post/Docker-Storage" rel="alternate" type="text/html" title="Docker是如何生成/保存镜像的?" /><published>2022-04-21T00:00:00+00:00</published><updated>2022-04-21T00:00:00+00:00</updated><id>https://wenje.top/post/Docker-Storage</id><content type="html" xml:base="https://wenje.top/post/Docker-Storage"><![CDATA[<!--more-->

<h1 id="前言">前言</h1>

<p>我们知道，Docker容器中的进程实际上就是跑在Linux Namespace中，与外部全局Namespace隔离开来（<a href="/post/DockerNamespace">Docker容器是如何隔离的?</a>)。但是我们在Docker容器中看见的文件视图是如何来的呢? 容器镜像是如何供容器使用的？本文将围绕Docker镜像的底层存储原理来剖析容器和镜像是如何运作的。</p>

<h1 id="union-file-system">Union File System</h1>

<p>UnionFS(Union File System), 是一种把其他文件系统联合到一个挂载点，提供统一视图的文件系统服务。它使用<code class="language-plaintext highlighter-rouge">read-only</code>或<code class="language-plaintext highlighter-rouge">read-write</code>的branch把<code class="language-plaintext highlighter-rouge">不同文件系统</code>的文件整合，根据层级透明的覆盖低层文件来提供一个统一的文件视图。</p>

<p>UnionFS遵循了<code class="language-plaintext highlighter-rouge">写时复制(copy-on-write)</code>原则，即若文件内容没有任何修改，使用对他的使用都可以指向一个唯一的文件实体。当修改操作发生时，先复制一个新文件，之后的修改和读取操作都将基于新文件之上进行。
COW避免了无意义的文件复制，因为绝大多数文件在使用中都不会发生变化，那我们只需要对那少部分发生修改的文件进行复制就行了。</p>

<h1 id="docker使用的存储驱动">Docker使用的存储驱动</h1>

<p>早期Docker是基于Centos开发的，所以Docker最初使用了Centos上默认集成的aufs作为存储引擎。</p>

<p>aufs(Advanced Multi-Layered Union Filesystem)，是Docker选用的第一种存储驱动，具有快启动，内存、存储友好的优点。但是aufs一直没有合并到Linux内核主线中(据说由于aufs代码的维护性较差)，所以有可能有些Linux发行版内核中默认并不支持该文件系统。</p>

<p>目前为止, Docker支持的存储驱动如下:</p>
<ul>
  <li>aufs</li>
  <li>overlay/overlay2</li>
  <li>btrfs</li>
  <li>device mapper</li>
  <li>zfs</li>
  <li>vfs</li>
</ul>

<p>本文着重解析<code class="language-plaintext highlighter-rouge">aufs</code>和<code class="language-plaintext highlighter-rouge">overlay2</code>在Docker存储中的使用。</p>

<h1 id="aufs">aufs</h1>

<h2 id="aufs挂载和结构说明">aufs挂载和结构说明</h2>

<p>aufs可用以下命令进行挂载:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount <span class="nt">-t</span> aufs <span class="nt">-o</span> <span class="nv">br</span><span class="o">={</span>branch-0-path<span class="o">}={</span>rw/ro<span class="o">}</span>:...:<span class="o">{</span>branch-1-path<span class="o">}={</span>rw/ro<span class="o">}</span> none <span class="o">{</span>mount-path<span class="o">}</span>
</code></pre></div></div>
<ul>
  <li>aufs的branch分为<code class="language-plaintext highlighter-rouge">read-only</code>和<code class="language-plaintext highlighter-rouge">read-write</code>，可以在挂载时在<code class="language-plaintext highlighter-rouge">options</code>中指定，若都不指定，则默认<code class="language-plaintext highlighter-rouge">第一个branch为read-write，其他的branch为read-only</code>。</li>
  <li>branch的顺序按照参数顺序从左到右排列，第一个branch在最上层，最后一个在最下层。</li>
  <li>aufs不需要dev设备，所以设备填none即可。</li>
</ul>

<div align="center">
  <img src="/assets/images/docker-storage/aufs-struct.png" style="height: 400px;" />
</div>
<center>图1: aufs挂载结构</center>
<p>如图所示，对于重名文件，上层文件在统一挂载点上会覆盖下层，但是并不会影响到下层branch真实的文件内容。</p>

<h2 id="修改aufs挂载点上的文件">修改aufs挂载点上的文件</h2>

<p class="info">对于只读挂载</p>

<p>若在挂载时制定所有分支都为<code class="language-plaintext highlighter-rouge">read-only</code>，那么将不被允许在挂载点上修改文件。</p>

<p class="info">对于读写挂载</p>

<p>若branch中存在<code class="language-plaintext highlighter-rouge">read-write</code>，那所有修改文件的操作都会被写入可写的branch路径。若存在多个可写branch，写入的操作会遵循相应的策略，具体可查看<a href="http://manpages.ubuntu.com/manpages/xenial/en/man5/aufs.5.html">aufs文档</a>。这里我们按照Docker的使用方式，只考虑单个可写branch。</p>

<p>创建我们的工作目录:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>tree <span class="nt">-L</span> 2
<span class="nb">.</span>
├── container-layer
│   ├── container-layer.txt
├── img-layer1
│   └── img-layer1.txt
├── img-layer2
│   ├── img-layer1-fake.txt
│   └── img-layer2.txt
├── img-layer3
│   └── img-layer3.txt
├── img-layer4
│   └── img-layer4.txt
└── mnt-aufs
</code></pre></div></div>

<p>每个txt文件内容从上至下如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>container-layer.txt -&gt; I'm container-layer
img-layer1.txt -&gt; I am layer1
img-layer1-fake.txt -&gt; i'm fake img-layer1
img-layer2.txt -&gt; I am layer2
img-layer3.txt -&gt; I am layer3
img-layer4.txt -&gt; I am layer4
</code></pre></div></div>

<p>ok，现在我们将container-layer作为读写层，其他作为只读层挂载到<code class="language-plaintext highlighter-rouge">mnt-aufs</code>路径:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>mount <span class="nt">-t</span> aufs <span class="nt">-o</span> <span class="nv">br</span><span class="o">=</span>./container-layer:./img-layer1:./img-layer2:./img-layer3:img-layer4 none ./mnt-aufs
root@master:aufs <span class="nv">$ </span><span class="nb">ls </span>mnt-aufs/
container-layer.txt  img-layer1-fake.txt  img-layer1.txt  img-layer2.txt  img-layer3.txt  img-layer4.txt
</code></pre></div></div>
<p>挂载完成后，所有branch的文件都出现在了<code class="language-plaintext highlighter-rouge">mnt-aufs</code>路径下。</p>

<p class="info">在mnt-aufs下修改文件</p>
<p>我们可以在mnt-aufs进行文件修改操作，不同的操作会对应不同的变化:</p>
<ul>
  <li>对于<code class="language-plaintext highlighter-rouge">container-layer</code>下文件，所有变动都会直接在对应branch的原始路径下进行。即修改、删除<code class="language-plaintext highlighter-rouge">container-layer.txt</code>都在直接反应在container-layer路径下。</li>
  <li>对于只读层的文件，修改时会在可写层复制一个文件，之后所有基于该文件的修改都会发生在新复制的文件上，这就是COW；删除文件会在可写上创建一个<code class="language-plaintext highlighter-rouge">.wh</code>文件表示该文件已删除。</li>
  <li>若绕过挂载点直接去修改原始路径下的文件，不会遵循上述aufs的规则，但是修改的内容仍然会同步到挂载点下。</li>
</ul>

<h1 id="docker是如何使用aufs的">Docker是如何使用aufs的?</h1>

<p class="warning">本文相关的Docker测试建议新创建Linux虚拟裸机来进行接下来的实验</p>

<h2 id="修改docker存储引擎为aufs">修改Docker存储引擎为aufs</h2>

<p>目前Docker默认的存储引擎是overlay2，具体信息可以通过<code class="language-plaintext highlighter-rouge">Docker info</code>命令查看:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>docker info | <span class="nb">grep</span> <span class="s2">"Storage Driver"</span>
 Storage Driver: aufs
</code></pre></div></div>

<p>可以重启docker daemon进程重新制定存储引擎:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:~ $ systemctl stop docker
root@master:~ $ dockerd --storage-driver=overlay2 &amp;
</code></pre></div></div>

<h2 id="docker-image的aufs存储">Docker Image的aufs存储</h2>

<p>Docker的aufs存储内容默认都在<code class="language-plaintext highlighter-rouge">/var/lib/docker/aufs</code>路径下，结构如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── diff
├── layers
└── mnt
</code></pre></div></div>
<ul>
  <li>diff，存储着Image Layer的内容</li>
  <li>layers，存储着layer堆叠信息metadata</li>
  <li>mnt， 镜像或者容器的挂载点，也就是容器最终看到的文件系统的样子</li>
</ul>

<p class="info">拉取一个镜像</p>
<p>我们在裸机上新安装的Docker没有任何镜像，所以初始状态下的aufs是空的。现在我们拉去一个<code class="language-plaintext highlighter-rouge">ubuntu:15.04</code>镜像.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>tree <span class="nt">-L</span> 2
<span class="nb">.</span>
├── diff
│   ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
│   ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
│   ├── a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
│   └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
├── layers
│   ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
│   ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
│   ├── a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
│   └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
└── mnt
    ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
    ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
    ├── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
    └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164

<span class="c">## 镜像的堆叠顺序</span>
root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>layers/b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>layers/0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>layers/2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>layers/a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
</code></pre></div></div>
<p>可以看到，我们拉取的镜像一共有4个layer，layers中记录的堆叠信息表明了layer的顺序。</p>

<p class="info">在镜像上作修改</p>
<p>我们创建一个Dockerfile，对镜像进行一些修改</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:docker <span class="nv">$ </span><span class="nb">cat </span>Dockerfile
FROM ubuntu:15.04
RUN <span class="nb">echo</span> <span class="s2">"Hello world"</span> <span class="o">&gt;</span> /tmp/newfile
root@master:aufs <span class="nv">$ </span>docker build <span class="nt">-t</span> changed-ubuntu <span class="nb">.</span>
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:15.04
 <span class="nt">---</span><span class="o">&gt;</span> d1b55fd07600
Step 2/2 : RUN <span class="nb">echo</span> <span class="s2">"Hello world"</span> <span class="o">&gt;</span> /tmp/newfile
 <span class="nt">---</span><span class="o">&gt;</span> Running <span class="k">in </span>f88047e60424
Removing intermediate container f88047e60424
 <span class="nt">---</span><span class="o">&gt;</span> a8c4a7335b4b
Successfully built a8c4a7335b4b
Successfully tagged changed-ubuntu:latest
</code></pre></div></div>
<p>查看镜像使用了哪些image layer:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>docker <span class="nb">history </span>changed-ubuntu
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
a8c4a7335b4b   2 minutes ago   /bin/sh <span class="nt">-c</span> <span class="nb">echo</span> <span class="s2">"Hello world"</span> <span class="o">&gt;</span> /tmp/newfile    12B
d1b55fd07600   6 years ago     /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["/bin/bash"]             0B</span>
&lt;missing&gt;      6 years ago     /bin/sh <span class="nt">-c</span> <span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/^#\s*\(deb.*universe\)$…   1.88kB
&lt;missing&gt;      6 years ago     /bin/sh -c echo '</span><span class="c">#!/bin/sh' &gt; /usr/sbin/poli…   701B</span>
&lt;missing&gt;      6 years ago     /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ADD file:3f4708cf445dc1b53…   131MB</span>
</code></pre></div></div>
<p>可以清楚看到每一层是由什么命令添加的，我们在镜像的最上层新加了一层，大小只有12B。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>tree <span class="nt">-L</span> 2
<span class="nb">.</span>
├── diff
    ...
│   └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
├── layers
    ...
│   └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
└── mnt
    ...
    └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da

root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>layers/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
</code></pre></div></div>
<p>查看aufs下，发现多了一个layer，并且这个layer恰好加载了之前4个layer之上。查看该层diff的内容，就只有一个文件，就是我们通过Dockerfile添加的那个文件。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span><span class="nb">cat </span>diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/tmp/newfile
Hello world
root@master:aufs <span class="nv">$ </span><span class="nb">ls
</span>diff  layers  mnt
root@master:aufs <span class="nv">$ </span>tree <span class="nt">-L</span> 2 diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/
diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/
└── tmp
    └── newfile

1 directory, 1 file
root@master:aufs <span class="err">$</span>
</code></pre></div></div>

<h2 id="container看见的视图">Container看见的视图</h2>

<p>我们将容器启动:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span>tree <span class="nt">-L</span> 2
<span class="nb">.</span>
├── diff
    ...
│   ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
│   └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init
├── layers
    ...
│   ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
│   └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init
└── mnt
    ...
    ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
    └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init
</code></pre></div></div>
<p>对于每一个容器，启动后会发生这么几件事:</p>
<ul>
  <li>先创建一个read-only的init layer，用来存储与这个容器环境相关的内容。</li>
  <li>创建一个和init layer同名的read-write layer，用来存储容器运行时所有的文件修改操作。这个可读写层在容器被删除(docker rm)时候删除，停止容器不会被删除。</li>
  <li>容器layer堆叠的顺序为: read-write layer =&gt; read-only init layer =&gt; Image layers</li>
</ul>

<div align="center">
  <img src="/assets/images/docker-storage/aufs-hierarchy.png" style="height: 400px;" />
</div>
<center>图2: aufs容器挂载结构图</center>

<p>容器启动后，实际上就是在镜像的上方添加了read-only和read-write两个layer，我们可以清楚的查看到aufs的堆叠信息:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span> <span class="nb">cat</span> /proc/mounts| <span class="nb">grep </span>aufs
none /var/lib/docker/aufs/mnt/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d aufs rw,relatime,si<span class="o">=</span>b35884dae88adc30,dio,dirperm1 0 0

root@master:aufs <span class="nv">$ </span><span class="nb">cat</span> /sys/fs/aufs/si_b35884dae88adc30/<span class="k">*</span>
/var/lib/docker/aufs/diff/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d<span class="o">=</span>rw
/var/lib/docker/aufs/diff/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init<span class="o">=</span>ro+wh
/var/lib/docker/aufs/diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da<span class="o">=</span>ro+wh
/var/lib/docker/aufs/diff/b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164<span class="o">=</span>ro+wh
/var/lib/docker/aufs/diff/0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c<span class="o">=</span>ro+wh
/var/lib/docker/aufs/diff/2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57<span class="o">=</span>ro+wh
/var/lib/docker/aufs/diff/a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306<span class="o">=</span>ro+wh
</code></pre></div></div>
<p>只有最上层是可读写的，其他全部都是只读层。而容器最终看见的文件系统，就是<code class="language-plaintext highlighter-rouge">aufs/mnt</code>目录下跟最上层layer同名的挂载点:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:aufs <span class="nv">$ </span><span class="nb">ls </span>mnt/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
</code></pre></div></div>
<p>至此，aufs在docker中的使用就讲解完毕了。接下来我们讲一下docker使用的另一种存储引擎<code class="language-plaintext highlighter-rouge">overlay2</code>，相信在我们详细解析了aufs的使用后，大家对overlay2的理解和掌握会手到擒来。</p>

<h1 id="overlay2">overlay2</h1>

<p>overlay/overlay2是一种使用上和aufs非常相似的UnionFS，于Linux 3.18中被纳入到内核主线中。和aufs相比，overlay效率更快、使用简单。</p>

<p>overlay只能在两层的结构上工作，所以为了实现多层镜像，Docker在lower层添加了各层的硬链接，并且由于每个layer都会新建一些目录，会比较消耗系统的inode number。overlay2克服了这个问题，原生支持了多层结构(最多128层)，自Docker 17.04后，默认使用overlay2作为存储引擎。</p>

<h2 id="overlay2结构说明">overlay2结构说明</h2>

<p><img src="/assets/images/docker-storage/overlay2-struct.png" alt="" /></p>
<center>图3: overlay2结构说明</center>

<ul>
  <li>lowerdir: 底层只读的branch，branch顺序越靠前则层级越高，高层会覆盖底层的同名文件，可以设置多个。</li>
  <li>upperdir: 读写层，在修改时会遵循<code class="language-plaintext highlighter-rouge">COW(copy-on-write)</code>原则，对于底层文件复制后修改，修改upperdir则直接修改目标路径文件，层级顺序同上。<strong>需要注意的是upperdir只能设置一个目录</strong>。</li>
  <li>workdir: 起过渡作用，写入upper的内容会先写入到workdir，然后再移动到upper层。需要注意的是workdir必须制定一个空的路径。</li>
  <li>mergedir: 统一挂载点，对外提供统一的文件视图。</li>
</ul>

<h2 id="用overlay2进行挂载">用overlay2进行挂载</h2>

<p>我们利用刚才那个文件目录，将container-layer作为upperdir，其他作为lowerdir进行挂载:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount <span class="nt">-t</span> overlay overlay <span class="nt">-o</span> <span class="nv">lowerdir</span><span class="o">=</span>./img-layer1:./img-layer2:./img-layer3:./img-layer4,upperdir<span class="o">=</span>container-layer,workdir<span class="o">=</span>./workdir ./mnt
</code></pre></div></div>

<p>和aufs类似，修改<code class="language-plaintext highlighter-rouge">mnt</code>中的文件时有以下几种情况:</p>
<ul>
  <li>修改lowerdir中的文件，遵循COW原则，在upperdir中复制一个新文件然后执行修改操作。删除lowerdir中的文件会在upperdir中生成一个特殊的字符设备文件，标记该文件已被删除。</li>
  <li>修改upperdir中的文件会直接在源路径下修改。</li>
</ul>

<p class="info">直接修改源路径</p>
<p>在刚才讲解aufs时，我们曾尝试直接去修改aufs源路径中的文件，而虽然绕过了aufs但是修改仍然同步到了联合挂载点上。这一点在overlay2上是不同的，绕过overlay2去直接修改源路径大多数情况下是不会同步到挂载点的。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:overlay2 <span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-li</span> mnt | <span class="nb">grep </span>fake
1311616 <span class="nt">-rw-r--r--</span> 1 root root 35 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 <span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-li</span> img-layer2 | <span class="nb">grep </span>fake
1311616 <span class="nt">-rw-r--r--</span> 1 root root 35 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 <span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"modification"</span> <span class="o">&gt;&gt;</span> img-layer2/img-layer1-fake.txt
root@master:overlay2 <span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-li</span> img-layer2 | <span class="nb">grep </span>fake
1311618 <span class="nt">-rw-r--r--</span> 1 root root 32 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 <span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-li</span> mnt
total 24
1311616 <span class="nt">-rw-r--r--</span> 0 root root 35 Apr 19 00:34 img-layer1-fake.txt
</code></pre></div></div>
<p>原因如上所示，overlay2在挂载时会将当时各文件的inode number记录下来，而这个记录是一次性的。所以当我们直接修改底层文件时，文件的inode number发生了改变，而挂载点上的文件指向的仍然是修改前文件的inode number。</p>

<h1 id="docker是如何使用overlay2的">Docker是如何使用overlay2的?</h1>

<h2 id="docker-overlay2存储结构">Docker overlay2存储结构</h2>

<p>当docker使用overlay2存储引擎时，镜像默认存储在宿主机的<code class="language-plaintext highlighter-rouge">/var/lib/docker/overlay2</code>目录下。
<img src="/assets/images/docker-storage/overlay2-hierarchy.png" alt="" /></p>
<center>图4: 容器挂载视图和镜像layer结构</center>
<ul>
  <li>镜像层(以该layer hashId为路径名)</li>
</ul>

<table>
  <thead>
    <tr>
      <th>name</th>
      <th>类型</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>link</td>
      <td>file</td>
      <td>记录了该layer在<code class="language-plaintext highlighter-rouge">l</code>中的短哈希软链接名字</td>
    </tr>
    <tr>
      <td>committed</td>
      <td>file</td>
      <td>该layer的commit信息</td>
    </tr>
    <tr>
      <td>lower</td>
      <td>file</td>
      <td>记录该layer依赖的低层layer信息</td>
    </tr>
    <tr>
      <td>work</td>
      <td>directory</td>
      <td>overlay的workdir</td>
    </tr>
    <tr>
      <td>diff</td>
      <td>directory</td>
      <td>该layer文件的存放为止</td>
    </tr>
    <tr>
      <td>merged</td>
      <td>directory</td>
      <td>仅在容器的读写层存在</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>
    <p>l</p>

    <p>这个目录中存放的是指向各层的软连接，名字是一串短哈希。这么做的原因是如果用原始的长哈希将更容易的达到mount命令参数的页大小限制。</p>
  </li>
</ul>

<h2 id="container看见的视图-1">Container看见的视图</h2>

<p>和aufs一样，每个Docker启动后，都会创建一个只读的init layer和一个读写layer。这是Docker在使用UnionFS一个统一的思路，将镜像层作为<code class="language-plaintext highlighter-rouge">只读层</code>，容器运行期间产生的文件修改都统一写入最上层的<code class="language-plaintext highlighter-rouge">读写层</code>。当容器被删除时，直接将这个可读写的目录删除即可。</p>

<p>通过docker inspect可以查看容器的挂载路径信息:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@master:docker <span class="nv">$ </span>docker inspect 5860f54f3561 | <span class="nb">grep </span>GraphDriver <span class="nt">-A</span> 8
        <span class="s2">"GraphDriver"</span>: <span class="o">{</span>
            <span class="s2">"Data"</span>: <span class="o">{</span>
                <span class="s2">"LowerDir"</span>: <span class="s2">"/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041-init/diff:/var/lib/docker/overlay2/a0b52b932c79aa0cef156fdd9d2ac601a6bafd0bdf0ac2b44b2c7d87274c83b0/diff:/var/lib/docker/overlay2/d9da8c3a58195f856664a840bfc4459caab2b17f14ea8f99b54949eddc79a870/diff:/var/lib/docker/overlay2/955671fdd85ce04b4a71269f9d6f22114c86b24cdd8ca207e5eb9e03d4a4ed5a/diff:/var/lib/docker/overlay2/786411b49cf906bb3b49904716ace173f7be85671b0cc3eff798cde85691d6cc/diff"</span>,
                <span class="s2">"MergedDir"</span>: <span class="s2">"/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/merged"</span>,
                <span class="s2">"UpperDir"</span>: <span class="s2">"/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/diff"</span>,
                <span class="s2">"WorkDir"</span>: <span class="s2">"/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/work"</span>
            <span class="o">}</span>,
            <span class="s2">"Name"</span>: <span class="s2">"overlay2"</span>
        <span class="o">}</span>,
</code></pre></div></div>
<p>可以看到，实际的挂载点是每个layer层的diff路径，<code class="language-plaintext highlighter-rouge">UpperDir</code>和<code class="language-plaintext highlighter-rouge">MergedDir</code>都是为容器创建的那个可读写的临时layer。</p>

<h1 id="reference">reference</h1>

<ol>
  <li><a href="https://cizixs.com/2017/09/13/docker-aufs-storage-driver/">aufs 简介以及在 docker 中的使用</a></li>
  <li><a href="https://docs.docker.com/storage/storagedriver/aufs-driver/">docker-aufs文档</a></li>
  <li><a href="https://docs.docker.com/storage/storagedriver/overlayfs-driver/">docker-overlay文档</a></li>
  <li><a href="https://segmentfault.com/a/1190000008489207">Linux文件系统之aufs</a></li>
</ol>]]></content><author><name>Your Name</name><email>fanwj1997@163.com</email></author><category term="docker" /><summary type="html"><![CDATA[]]></summary></entry></feed>