<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
    <channel>
        <title>beihai blog</title>
        <link>https://wingsxdu.com/</link>
        <description>beihai blog</description>
        <generator>Hugo -- gohugo.io</generator><language>zh-cn</language><managingEditor>beihai@wingsxdu.com (beihai)</managingEditor>
            <webMaster>beihai@wingsxdu.com (beihai)</webMaster><lastBuildDate>Sat, 06 Jul 2024 21:00:00 &#43;0800</lastBuildDate>
            <atom:link href="https://wingsxdu.com/index.xml" rel="self" type="application/rss+xml" />
        <item>
    <title>实现高并发短链接服务</title>
    <link>https://wingsxdu.com/posts/system-design/tiny-url/</link>
    <pubDate>Sat, 06 Jul 2024 21:00:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/system-design/tiny-url/</guid>
    <description><![CDATA[<blockquote>
<p>最近在用 Golang 语言实现一个短链接服务<a href="https://github.com/beihai0xff/turl" target="_blank" rel="noopener noreferrer">Tiny-URL</a>，这期间了解了一些关于短链接服务的设计思路，以及分布式系统中 ID 生成、缓存、限流、负载均衡等方面的知识，这篇文章将会对短链接服务的设计思路进行总结。</p>
</blockquote>
<h2 id="概述">概述</h2>
<p>在社交媒体、用户增长、广告投放等场景中，经常会遇到长链接转短链接的需求，提高用户点击率更高，同时能规避原始链接中一些关键词、域名屏蔽等。常见微博、微信等社交软件中，比如微博限制字数为140，如果包含的链接过长，会占用很多字数，所以需要将长链接转换为短链接，以节省字数。</p>
<p>短链接除了具有美观清爽的特性外，利用短链每次跳转都需要经过后端的特性，可以在跳转过程中做异步埋点，用于效果数据统计，常见的应用场景如下：</p>
<ul>
<li>注册、收藏、加购、下单、支付效果统计；</li>
<li>用户分享效果追踪；</li>
<li>减少字符占用。</li>
</ul>
<p>本文将会介绍短链接服务的设计思路，以及在实现过程中遇到的问题与解决方案。</p>
<h2 id="需求与预算评估">需求与预算评估</h2>
<h4 id="功能需求">功能需求</h4>
<ul>
<li>短链接生成：给定一个长链接，能够生成一个唯一的短链接，即使多次输入同一个长链接，也能够得到唯一的短链接；</li>
<li>短链接重定向：通过短链接能够访问到原始的长链接，通过 302 临时重定向的方式，将用户重定向到原始的长链接，临时重定向的方式可以保证搜索引擎不会抓取短链接，而是抓取原始的长链接，并且便于统计短链接的访问次数。</li>
<li>访问限流：对短链接的生成与访问进行限流，防止恶意刷短链接，保证服务的稳定性。</li>
<li>短链接删除：短链接可以删除，删除之后，短链接将失效，无法再访问到原始的长链接。</li>
<li>过期时间：短链接可以设置过期时间，过期时间到了之后，短链接将失效，无法再访问到原始的长链接。</li>
</ul>
<h4 id="非功能需求">非功能需求</h4>
<ul>
<li>高可用：短链接服务需要保证高可用，即使某个节点宕机，也不影响整个服务的正常使用。</li>
<li>高性能：短链接服务需要保证高性能，能够支撑每秒十万 QPS 的访问量。</li>
<li>低延迟：短链接服务需要保证低延迟，用户访问短链接时，能够快速的重定向到原始的长链接。</li>
<li>高可扩展性：短链接服务需要保证高可扩展性，能够支持大量的短链接生成和访问。</li>
<li>高可靠性：短链接服务需要保证高可靠性，能够保证短链接的生成和访问的正确性。</li>
</ul>
<h4 id="资源预算">资源预算</h4>
<ol>
<li>假设我们的系统每天有 100M 用户在线，即 1亿日活；</li>
<li>每天平均每10个用户写 1 个帖子，为每个帖子生成一个对应的短链接，即每天总共生成 10M 个短链接：
<ul>
<li>平均每个短链接-长链接映射关系占用 500Bytes 空间，即每天总共需要 5GB 的存储空间；</li>
<li>每日 10,000,000 次写入操作，1kw/86400s ≈ 116，即平均每秒需要处理 116qps 的写入操作；</li>
<li>假设峰值写入量约平均写入量的 10 倍，即 1160qps，为便于估算，可理解写峰值 qps 为 1k；</li>
</ul>
</li>
<li>平均每个用户每天访问 10 个帖子，即每天总共访问 10亿次短链接，读写比例为 100:1：
<ul>
<li>平均每秒需要处理 11600 的读取操作，即约为 10k/s；</li>
<li>假设峰值读取量约平均读取量的 10 倍，即约为 100k/s；</li>
</ul>
</li>
<li>缓存资源预算：
<ul>
<li>由于短链服务具有明显的热点数据特征，因此需要使用缓存来提高访问性能，我们假设 20% 的数据贡献了 99% 的访问量</li>
<li>每日 10亿次的访问量中，我们假设 99% 的访问量是固定在 10% 的热点数据上，即需要缓存 2亿条数据，缓存服务器需要 100GB 内存空间；</li>
<li>再进一步，我们利用本地缓存来缓存最热的 1% 的数据，即需要缓存 2M 条数据，每台 Server 节点需要 1GB 内存空间用于本地缓存；</li>
<li>缓存命中率为 99%，即每日 10亿次的访问量中，有 1% 的访问量需要访问数据库，即 10M 次/日，即每秒需要处理 116 次数据库读取操作，即约为 100qps；</li>
</ul>
</li>
</ol>
<p>综上所述：</p>
<ol>
<li>数据库存储空间：每日消耗 5GB 磁盘，三年时间需要约 5.5TB；</li>
<li>数据库读写请求数：平均每秒 116qps 的写入操作与读取操作，峰值 1k/qps 的写入操作与读取操作；</li>
<li>缓存服务器内存空间：总共需要 50GB 内存空间，缓存约 1亿条数据；</li>
<li>本地缓存存储空间：每台 Server 节点需要 500MB 内存空间用于本地缓存，缓存约 1M 条数据；</li>
</ol>
<h2 id="时序流程">时序流程</h2>
<p>短链接服务的写数据时序流程如下：</p>
<ol>
<li>用户发起创建短链接请求，服务端接收到请求，将会生成一个唯一的短链接，并尝试将短链接-长链接映射关系写入数据库中；</li>
<li>数据存储端需要保证短链接-长链接映射关系的一致性，即使多次为同一个长链接生成短链接，也能保证生成的短链接是唯一的，因此可以在数据库中为 original URL 设置唯一索引；</li>
<li>当插入的长链接已经存在时，可以直接返回已经存在的短链接，无需再次生成短链接；</li>
<li>当插入的长链接不存在时，需要生成一个唯一的短链接，可以使用分布式 ID 生成器生成唯一的短链接 ID，然后将短链接 ID 转换为 Base58 编码，即可生成短链接；</li>
<li>长链接写入数据库成功后，服务端将短链接-长链接映射关系写入分布式缓存与本地缓存中，以提高访问性能；</li>
</ol>
<p>短链接服务的读数据时序流程如下：</p>
<ol>
<li>用户访问短链接时，服务端接收到请求，将会有限从本地缓存中获取短链接-长链接映射关系，如果本地缓存中不存在，则从分布式缓存中获取，如果分布式缓存中不存在，则从数据库中获取；</li>
<li>如果本地缓存不存在，分布式缓存中存在，则将接映射关系写入本地缓存中，以提高访问性能；如果缓存中不存在，数据库中存在，则将映射关系写入缓存中；</li>
<li>如果数据库中也不存在，则返回 404 错误，表示短链接不存在，同时可以设置黑名单，对恶意访问进行限制；</li>
<li>如果获取短链接成功，通过 302 临时重定向的方式，将用户重定向到原始的长链接；</li>
<li>访问短链接成功后，可以统计短链接的访问次数，以便后续分析短链接的访问情况；</li>
</ol>
<h2 id="模块设计">模块设计</h2>
<p>短链接服务涉及 分布书 ID 生成器、数据库存储、缓存系统、访问限流等模块，下面将会对这些模块进行详细设计。</p>
<h4 id="分布式-id-生成器">分布式 ID 生成器</h4>
<p>短链接发号器需要保证生成的短链接是唯一的，可以使用分布式 ID 生成器，如 Twitter 的 Snowflake 算法，或者使用数据库自增 ID 生成器等等，下面将描述不同方案的优缺点：</p>
<h5 id="数据库自增-id">数据库自增 ID</h5>
<p>数据库的主键是唯一且自增的，可以保证生成的 ID 是唯一的。将数据库自增 ID 作为短链接 ID，然后将 UID 转换为 Base58 编码，即可生成短链接。</p>
<p>优点：简单易用，生成的 ID 是唯一的；
缺点：依赖于数据库，数据库的性能将成为瓶颈，不适合高并发场景，如果采用数据库集群，需要避免 ID 重复；并且数据库自增 ID 是有序的，可能会暴露业务规模；</p>
<h5 id="redis-自增序列">Redis 自增序列</h5>
<p>Redis 的自增序列可以使用 INCR 命令，每次调用 INCR 命令，Redis 会将自增序列加 1，并返回自增后的值。因此可以将 Redis 的自增序列作为短链接 ID，然后将 UID 转换为 Base58 编码，即可生成短链接。</p>
<p>优点：高性能，生成的 ID 是唯一的；
缺点：Redis 是基于内存的，如果 Redis 宕机，可能会导致 ID 重复，需要保证 Redis 的高可用；ID 自增是有序的，可能会暴露业务规模；</p>
<h5 id="uuid">UUID</h5>
<p>UUID 是由一组 16 个字节（128 位）组成的标识符，可以用于唯一标识信息。UUID 的生成方式有多种，其中最为常用的是基于算法的 UUID 生成方式和基于硬件的 UUID 生成方式。
基于时间戳的 UUID 生成方式，可以保证生成的 UUID 是唯一的，并且是有序的，但是如果多台机器的时钟存在差异，或时钟回拨或闰秒，可能会导致 ID 重复。基于硬件的 UUID 生成方式，可以保证生成的 UUID 是唯一的，且随机性更强。但是 UUID 是 128 位的，转换为 Base58 编码后，短链接长度过长，不适合短链接服务；且 UUID 是无序的，插入数据时可能会导致数据库的性能问题；</p>
<p>优点：生成的 ID 是唯一的，不依赖于数据库；
缺点：UUID 是 128 位的，转换为 Base58 编码后，短链接长度过长，不适合短链接服务；且 UUID 是无序的，插入数据时可能会导致数据库的性能问题；</p>
<h5 id="snowflake-算法">Snowflake 算法</h5>
<p>雪花算法（Snowflake）是 Twitter 开源的分布式 ID 生成算法，可以生成 64 位的唯一 ID，结构如下：</p>
<ul>
<li>1 位符号位，始终为 0；</li>
<li>41 位时间戳，精确到毫秒级，可以使用 69 年；</li>
<li>10 位机器 ID，可以部署 1024 台机器；</li>
<li>12 位序列号，每毫秒可以生成 4096 个 ID。</li>
</ul>
<p>Snowflake 算法生成的 ID 是有序的，可以保证 ID 的唯一性，但是需要依赖于时钟，时钟回拨会导致 ID 重复，需要保证时钟的稳定性。同时，Snowflake 算法存在较大的序列号浪费问题，因为每毫秒只能生成 4096 个 ID，即使不使用完，也会浪费掉。</p>
<p>优点：高性能，高可用，生成的 ID 是唯一的；
缺点：需要依赖于时钟，时钟回拨或闰秒调整会导致 ID 重复，需要保证时钟的稳定性；存在较大的序列号浪费。</p>
<p>目前也出现了一些基于雪花算法的改进版本，其中时间戳不依赖于系统时钟，而是使用逻辑时钟，服务启动后，每次生成 ID 时，都会从逻辑时钟中获取时间戳，这样可以避免时钟回拨导致 ID 重复的问题，但仍然无法避免较大的序列号浪费问题。</p>
<h5 id="tddl-序列">TDDL 序列</h5>
<p>TDDL 序列是指在数据库中创建一个序列表，用于存储序列的当前值，然后通过数据库的 CAS 原子操作来获取下一个序列区间，然后在内存中递增序列值，当序列值用尽时，再次获取下一个序列区间。</p>
<ul>
<li>优点：生成的 ID 是唯一的，避免了数据库自增 ID 的性能瓶颈；同时减小了序列号浪费；</li>
<li>缺点：具有一定的维护成本；</li>
</ul>
<h5 id="表结构">表结构</h5>
<p>综上所述，我们可以选择 TDDL 序列算法作为短链接发号器，保证生成的短链接是唯一的，同时 TDDL 序列算法也便于根据 short ID 进行分库分表，适合高并发场景。</p>
<p>可以选择关系型数据库、NoSQL 数据库等维护 TDDL 的序列表，Tiny-URL 首要支持 MySQL 等关系型数据库，未来考虑支持 MongoDB 等 NoSQL 数据库。</p>
<p>MySQL 数据库表结构可参考 <a href="https://github.com/beihai0xff/turl/blob/main/docs/ddl/sequences.sql" target="_blank" rel="noopener noreferrer">sequences.sql</a>。</p>
<h4 id="数据库存储">数据库存储</h4>
<p>短链接服务需要存储短链接-长链接映射关系，可以使用关系型数据库、NoSQL 数据库等存储短链接-长链接映射关系，下面将介绍使用 MySQL 数据库存储短链接-长链接映射关系的设计。</p>
<p>tiny_urls 表的几个关键字段：</p>
<ul>
<li>id：数据库自增 ID，作为主键，保证新插入的 record 是顺序写入的；</li>
<li>long_url：原始的长链接，作为唯一索引，保证长链接是唯一的；</li>
<li>short：短链接 ID，作为唯一索引，保证短链接是唯一的；</li>
<li>deleted_at：删除时间，用于标记短链接是否删除；</li>
</ul>
<p>对应表结构如下：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">create</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="n">tiny_urls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w">         </span><span class="nb">bigint</span><span class="w"> </span><span class="n">unsigned</span><span class="w"> </span><span class="n">auto_increment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">primary</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="w">  </span><span class="k">null</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">updated_at</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="w">  </span><span class="k">null</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">deleted_at</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="w">  </span><span class="k">null</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">long_url</span><span class="w">   </span><span class="nb">varchar</span><span class="p">(</span><span class="mi">500</span><span class="p">)</span><span class="w"> </span><span class="k">not</span><span class="w"> </span><span class="k">null</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">short</span><span class="w">      </span><span class="nb">bigint</span><span class="w">       </span><span class="k">not</span><span class="w"> </span><span class="k">null</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">constraint</span><span class="w"> </span><span class="n">idx_tiny_urls_long_url</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">unique</span><span class="w"> </span><span class="p">(</span><span class="n">long_url</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">constraint</span><span class="w"> </span><span class="n">idx_tiny_urls_short</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">unique</span><span class="w"> </span><span class="p">(</span><span class="n">short</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">create</span><span class="w"> </span><span class="k">index</span><span class="w"> </span><span class="n">idx_tiny_urls_deleted_at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">on</span><span class="w"> </span><span class="n">tiny_urls</span><span class="w"> </span><span class="p">(</span><span class="n">deleted_at</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>在表结构中，我们对长链接与短链接分别建立了唯一索引，保证了长链接与短链接的唯一性。但我们并没有将 short ID 作为主键，因为 short ID 具有一定的离散性，将其作为主键会影响写入性能。</p>
<h4 id="分布式缓存">分布式缓存</h4>
<p>短链接服务具有明显的热点数据特征，因此需要使用缓存来提高访问性能，我们可以使用 Redis、Memcached 等缓存服务器，将热点数据缓存到缓存服务器中，以提高访问性能。</p>
<p>分布式缓存需要考虑缓存的一致性、缓存的命中率、缓存的淘汰策略、缓存的预热、缓存的雪崩、缓存的击穿等问题。</p>
<ul>
<li>缓存命中率：缓存命中率是衡量缓存性能的重要指标，可以通过缓存命中率来评估缓存的有效性，缓存命中率越高，说明缓存的有效性越好，缓存的性能越高。</li>
<li>缓存淘汰策略：缓存淘汰策略是指当缓存空间不足时，如何选择淘汰哪些缓存数据，常见的缓存淘汰策略有：FIFO（先进先出）、LRU（最近最少使用）、LFU（最少使用频率）等。</li>
<li>缓存预热：缓存预热是指在系统启动时，将热点数据加载到缓存中，以提高系统的访问性能，缓存预热可以通过定时任务、异步加载等方式来实现。</li>
<li>缓存雪崩：缓存雪崩是指缓存中的大量数据同时失效，导致大量请求直接访问数据库，从而导致数据库的性能问题，为了避免缓存雪崩，可以采用多级缓存、缓存预热、缓存失效时间随机等方式来避免。</li>
<li>缓存击穿：缓存击穿是指缓存中的某个数据失效，导致大量请求直接访问数据库，从而导致数据库的性能问题，为了避免缓存击穿，可以采用分布式锁、热点数据永不过期等方式来避免。</li>
</ul>
<p>Tiny URL 使用 Redis 作为缓存服务器，将热点数据缓存到 Redis 中，以提高访问性能。同时将淘汰策略设置为<code>volatile-lfu</code>，根据短链接的访问频率来淘汰缓存数据，以提高缓存的命中率。
缓存更新与访问流程如下：</p>
<ol>
<li>用户发起创建短链接请求，服务端在数据库中成功插入短链接-长链接映射关系；</li>
<li>服务端将短链接-长链接映射关系写入 Redis 缓存中，假设配置的过期时间为 t，服务端会生成 [t,2t] 时间内的随机值，作为缓存的过期时间；</li>
<li>用户访问短链接时，服务端会先从 Redis 缓存中获取短链接-长链接映射关系，如果缓存中不存在，则从数据库中获取，并写入 Redis 缓存中，同时更新缓存的过期时间；</li>
</ol>
<h4 id="本地缓存">本地缓存</h4>
<p>单机 Redis 能够支持 3W～4W QPS 的访问量，同时我们也可以使用 Redis Proxy 来负载均衡，以支撑更大的缓存容量和访问量。当如果某个热点 key 的访问量过大，可能会导致 Redis 的性能问题，此时可以使用本地缓存来缓存热点数据，以提高访问性能。</p>
<p>本地缓存可以使用 bigcache、freecache 等内存缓存库，将热点数据缓存到本地内存中，以提高访问性能。本地缓存的更新与访问流程如下：</p>
<ol>
<li>用户访问短链接时，服务端会先从本地缓存中获取短链接-长链接映射关系，如果本地缓存中不存在，则从 Redis 缓存中获取，如果 Redis 缓存中不存在，则从数据库中获取；</li>
<li>如果本地缓存中不存在，Redis 缓存或数据库中存在，则更新本地缓存，并设置缓存的过期时间；</li>
</ol>
<p>本地缓存能够解决热点 Key 问题，单机支撑上万 QPS 的访问量，同时也可以使用本地缓存来缓存最热的 1% 的数据，以降低访问延迟。</p>
<p>本地缓存与分布式缓存作为一个独立的模块，可以定义统一的<code>Cache</code>接口，然后实现<code>RedisCache</code>、<code>LocalCache</code>、混合缓存器等多种具体实现，在不同的场景下选择更合适的缓存器。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Interface cache interface
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">Interface</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Set the key value to cache
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Set</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">k</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">v</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="nx">ttl</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Get the key value from cache
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Get</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">k</span> <span class="kt">string</span><span class="p">)</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Close the cache
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="访问限流">访问限流</h4>
<p>短链接服务需要对短链接的生成与访问进行限流，防止恶意刷短链接，保证服务的稳定性。可以使用令牌桶算法、漏桶算法等限流算法，对短链接的生成与访问进行限流。</p>
<p>令牌桶算法是一种固定容量的令牌桶，按照固定速率往桶中放入令牌，如果桶中令牌已满，则不再放入令牌，当请求到来时，如果桶中有令牌，则允许通过，否则拒绝请求。令牌桶算法能够对单位时间内的请求进行限流，保证服务的稳定性，同时又可以平滑处理突发流量，保证服务的稳定性。</p>
<p>Tiny-URL 采用了 Redis 分布式令牌桶与单机令牌桶两种实现，对短链接的生成与访问进行全局限流，如果 Redis 故障，则会回退到单机令牌桶限流。</p>
<p>限流器的实现通过定义统一的<code>RateLimiter</code>接口，然后实现<code>RedisRateLimiter</code>、<code>LocalRateLimiter</code>、混合限流器等多种具体实现，在不同的场景下选择更合适的限流器。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// RateLimiter is an interface that knows how to limit the rate at which something is processed
</span></span></span><span class="line"><span class="cl"><span class="c1">// It provides methods to decide how long an item should wait, to stop tracking an item, and to get the number of failures an item has had.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">RateLimiter</span><span class="p">[</span><span class="nx">T</span> <span class="nx">comparable</span><span class="p">]</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Take gets an item and gets to decide whether it should run now or not,
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// use this method if you intend to drop / skip events that exceed the rate.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Take</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">item</span> <span class="nx">T</span><span class="p">)</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// When gets an item and gets to decide how long that item should wait,
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">When</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">item</span> <span class="nx">T</span><span class="p">)</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Forget indicates that an item is finished being retried. Doesn&#39;t matter whether it&#39;s for failing
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// or for success, we&#39;ll stop tracking it
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Forget</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">item</span> <span class="nx">T</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="c1">// Retries returns back how many failures the item has had
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nf">Retries</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">item</span> <span class="nx">T</span><span class="p">)</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="小结">小结</h4>
<p>本章节介绍了 Tiny URL 主要模块的设计思路与技术选型，包括分布式 ID 生成器、数据库存储、缓存系统、访问限流等模块，以及实现这些模块的接口定义与实现。在编码过程中，我们要尽可能将模块的能力抽象出来，定义统一的接口，然后提供多种具体的实现，以便在不同的场景下选择更合适的实现。</p>
<h2 id="部署方案">部署方案</h2>
<p>Tiny URL 服务依赖 MySQL8 与 Redis 作为存储与缓存，Tiny URL 服务本身是无状态的，可以通过 Docker 镜像部署到 Kubernetes 集群中，以提高服务的可用性与可扩展性。</p>
<p>Tiny URL 服务支持读写分离模式，可以将读请求与写请求分发到不同的服务节点，以提高服务的性能。默认情况下，Server 会以读写模式运行，添加启动参数<code>--readonly</code>可以将 Server 设置为只读模式。</p>
<h4 id="快速体验">快速体验</h4>
<p>在 <a href="https://github.com/beihai0xff/turl/blob/main/internal/example/docker-compose.yaml" target="_blank" rel="noopener noreferrer">Git Repo</a> 中提供了 docker compose all in one 方案，确保本地已经安装了 Docker 与 Docker Compose，然后执行以下命令：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">make deploy
</span></span></code></pre></td></tr></table>
</div>
</div><p>终端输出<code>turl service containers start successfully</code>后，说明服务已经启动成功。 该模式会部署 MySQL 与 Redis 服务，作为本地存储与缓存服务器。同时会启动两个服务节点，一个用于读写操作，另一个用于只读操作。</p>
<ul>
<li>读写服务：http://localhost:8080，用于生成短链接、更新远程缓存、更新数据库等；</li>
<li>只读服务：http://localhost:80，只用于访问短链接，不支持生成短链接，生产环境中可以部署多个只读服务节点，用于分流读取请求。</li>
</ul>
<h4 id="生成短链接">生成短链接</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl -X POST http://localhost:8080/api/shorten -H <span class="s1">&#39;Content-Type: application/json&#39;</span> -d <span class="s1">&#39;{&#34;long_url&#34;: &#34;https://google.com&#34;}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>返回结果：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;short_url&#34;</span><span class="p">:</span><span class="s2">&#34;http://localhost/24rgcX&#34;</span><span class="p">,</span><span class="nt">&#34;long_url&#34;</span><span class="p">:</span><span class="s2">&#34;https://google.com&#34;</span><span class="p">,</span><span class="nt">&#34;created_at&#34;</span><span class="p">:</span><span class="s2">&#34;2024-07-08T15:06:26.434Z&#34;</span><span class="p">,</span><span class="nt">&#34;deleted_at&#34;</span><span class="p">:</span><span class="kc">null</span><span class="p">,</span><span class="nt">&#34;error&#34;</span><span class="p">:</span><span class="s2">&#34;&#34;</span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="访问短链接">访问短链接</h4>
<p>访问短链接 <code>http://localhost/24rgcX</code>，将会被重定向到原始的长链接 <code>https://google.com</code>。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl -L http://localhost/24rgcX
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="性能测试">性能测试</h2>
<p>上面一小节介绍了如果通过 Docker Compose 部署 Tiny URL 服务，下面将对 Tiny URL 服务进行性能测试，以验证服务的性能表现。</p>
<p>服务器：Apple MacBook Pro 14 M1Pro 2021, 16G Memory 512G SSD</p>
<p>测试数据：获取全球访问量前 10k 的域名，每个域名添加 10 个 API 后缀，共计 100k 条无重复数据。使用 100 个协程并发请求，统计写入耗时。</p>
<p>写入性能测试代码可参考：<a href="https://github.com/beihai0xff/turl/tree/main/internal/tests/benchmark" target="_blank" rel="noopener noreferrer">api_benchmark_test</a></p>
<p>测试结果如下：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Benchmark_Create
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">4550</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">10040</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">15422</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">20977</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">26532</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">32452</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">38095</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">42921</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">47579</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">52629</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">58464</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">63961</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">69651</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">75325</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">80952</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">87016</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">92535</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:93: send requests: <span class="m">98344</span>
</span></span><span class="line"><span class="cl">api_benchmark_test.go:111: success requests:  <span class="m">100000</span> costs 18.365219458s
</span></span></code></pre></td></tr></table>
</div>
</div><p>读取性能测试的情况比较复杂，涉及到分布式缓存命中率、本地缓存命中率、数据库读取性能等多个因素，因此需要综合实际应用场景，才能得出准确的性能测试结果。</p>
<p>下面是使用<a href="https://github.com/wg/wrk" target="_blank" rel="noopener noreferrer">wrk</a> 针对单个短链接的 API 性能测试，即所有流量都命中本地缓存的情况下，测试结果如下：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"> wrk -t4 -c100 -d30s --latency http://localhost/24rgcY
</span></span><span class="line"><span class="cl">Running 30s <span class="nb">test</span> @ http://localhost/24rgcY
</span></span><span class="line"><span class="cl">  <span class="m">4</span> threads and <span class="m">100</span> connections
</span></span><span class="line"><span class="cl">  Thread Stats   Avg      Stdev     Max   +/- Stdev
</span></span><span class="line"><span class="cl">    Latency     2.12ms    1.75ms  63.13ms   98.39%
</span></span><span class="line"><span class="cl">    Req/Sec    12.18k     1.27k   15.10k    70.17%
</span></span><span class="line"><span class="cl">  Latency Distribution
</span></span><span class="line"><span class="cl">     50%    1.96ms
</span></span><span class="line"><span class="cl">     75%    2.36ms
</span></span><span class="line"><span class="cl">     90%    2.78ms
</span></span><span class="line"><span class="cl">     99%    4.56ms
</span></span><span class="line"><span class="cl">  <span class="m">1454811</span> requests in 30.01s, 260.83MB <span class="nb">read</span>
</span></span><span class="line"><span class="cl">Requests/sec:  48481.30
</span></span><span class="line"><span class="cl">Transfer/sec:      8.69MB
</span></span></code></pre></td></tr></table>
</div>
</div><p>综合来看，API 的性能表现良好，理想条件下写操作能够达到 5000+ QPS。在单一测试场景下（命中本地缓存），极限读取性能能够达到 50000 QPS，p99 延迟在 5ms 内。</p>
<h2 id="总结">总结</h2>
<p>本文介绍了短链接服务的设计思路与实现，包括需求与预算评估、时序流程、模块设计、部署方案、性能测试等内容。短链接服务是一个典型的高并发、低延迟、高可用的服务，需要考虑到短链接的生成与访问的性能、可用性、可扩展性等方面的问题，以保证服务的稳定性与可靠性。</p>
<p>短链接服务可以足够简单，最核心的接口只有两个：生成短链接与访问短链接，但是在实现过程中，有足够复杂，需要考虑到短链接的生成与访问的性能、可用性、可扩展性等方面的问题，涉及到数据库、缓存、限流、分布式 ID 生成器等多个模块，需要综合考虑这些模块的设计与实现。</p>
]]></description>
</item><item>
    <title>谈谈 ZooKeeper 的局限性</title>
    <link>https://wingsxdu.com/posts/database/zookeeper-limitations/</link>
    <pubDate>Sat, 17 Feb 2024 21:00:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/database/zookeeper-limitations/</guid>
    <description><![CDATA[<blockquote>
<p>Zookeeper 是最常用的分布式协调服务之一，用于配置管理、分布式锁、服务注册与发现等。笔者在使用 Zookeeper 的过程中，也意识到了 Zookeeper 局限性，本文将对这些痛点问题进行总结与分析，并提出一些可行的解决方案。</p>
</blockquote>
<h2 id="概述">概述</h2>
<p>Zookeeper 是大名鼎鼎的谷歌分布式锁服务 <em><a href="https://research.google/pubs/the-chubby-lock-service-for-loosely-coupled-distributed-systems/" target="_blank" rel="noopener noreferrer">Chubby</a></em> 的开源实现，它提供了一种分布式系统数据一致性的解决方案，通过类 Paxos 算法（ZAB 协议）保证数据的一致性与服务的高可用，同时也提供了一些高级特性：比如 Watcher 机制、ACL 权限控制等。Zookeeper 的出现大大简化了分布式系统的开发，但是由于初期的系统设计缺陷，也积累了一些问题，本文将对这些局限性进行总结与分析，并与后起之秀分布式 KV 存储系统 ETCD 进行对比，分析他们在设计思想上的差异。</p>
<blockquote>
<p>本文建立在对分布式共识算法、Zookeeper/ETCD 的基本原理有一定了解的基础上，如果对这些概念不熟悉，建议先阅读相关资料。<a href="https://wingsxdu.com/posts/algorithms/distributed-consensus-and-data-consistent/" target="_blank" rel="noopener noreferrer">漫谈分布式共识算法与数据一致性</a>、<a href="https://wingsxdu.com/posts/database/etcd/" target="_blank" rel="noopener noreferrer">分布式键值存储 etcd 原理与实现</a></p>
</blockquote>
<h2 id="存储性能">存储性能</h2>
<p>Zookeeper 是一个分布式 KV 存储系统，内部实现了一个内存数据库<code>ZKDatabase</code>，其中有两个核心数据结构，一个是以基数树为逻辑数据模型的纯内存的存储引擎<code>DataTree</code>，另一个是基于文件系统的持久化存储模块<code>snapLog</code>。这两部分组成了 Zookeeper 的 KV 数据库<code>ZKDatabase</code>，并保证服务重启后数据不会丢失。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ZKDatabase</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">protected</span> <span class="n">DataTree</span> <span class="n">dataTree</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">protected</span> <span class="n">FileTxnSnapLog</span> <span class="n">snapLog</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="datatree">DataTree</h4>
<p>Zookeeper 对外暴露的数据模型是一个基于路径的树形结构，类似于文件系统的目录结构，在 Zookeeper 实现了一个内存数据库<code>DataTree</code>，每个路径节点都是一个 ZNode，我们可以向 DataTree 中添加、删除、更新 ZNode，同时也可以在 ZNode 上注册 Watcher。</p>
<p></p>
<p>ZNode 由五部分组成：<code>path</code>、<code>data</code>、<code>stat</code>、<code>acl</code>、<code>children</code>，其中<code>path</code>是以<code>/</code>开始的全路径，剩余的四部分都存储在一个独立的<code>DataNode</code>数据结构中：<code>data</code>是 ZNode 的数据，<code>stat</code>是 ZNode 的元数据如版本号、数据长度等，<code>acl</code>是 ZNode 的权限控制，<code>children</code>是 ZNode 的子节点集合，同时<code>DataNode</code>也包含了一些辅助方法，比如<code>getChildren</code>、<code>getData</code>、<code>setData</code>等，用于操作 ZNode 的数据。</p>
<p>DataTree 的所有<code>path</code>都被保存在一个哈希表中，<code>path</code>到<code>DataNode</code>的映射关系是一一对应，因此我们可以通过<code>path</code>在 <em>O(1)</em> 时间复杂度内找到对应的<code>DataNode</code>，这样就能够保证数据的快速查询。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">DataTree</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">final</span> <span class="n">NodeHashMap</span> <span class="n">nodes</span><span class="o">;</span> <span class="c1">// 路径到 ZNode 的映射
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="n">IWatchManager</span> <span class="n">dataWatches</span><span class="o">;</span> <span class="c1">// 路径 Watcher 管理
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="n">IWatchManager</span> <span class="n">childWatches</span><span class="o">;</span> <span class="c1">// 子节点 Watcher 管理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">DataNode</span> <span class="kd">implements</span> <span class="n">Record</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">byte</span><span class="o">[]</span> <span class="n">data</span><span class="o">;</span> <span class="c1">// ZNode 数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">public</span> <span class="n">StatPersisted</span> <span class="n">stat</span><span class="o">;</span> <span class="c1">// ZNode 元数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">Long</span> <span class="n">acl</span><span class="o">;</span> <span class="c1">// ZNode 权限控制
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="n">Set</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span> <span class="n">children</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="c1">// ZNode 子节点集合
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>DataTree 本质上是一个巨大的哈希表，虽然单次查询非常快，但是 Zookeeper 需要使用哈希表来维护基数树的路径关系，在创建、删除 ZNode 时，需要多次查询哈希表，增加写操作的延迟，降低写入性能。同时，Zookeeper 的内存管理是基于 JVM 的，JVM 的 GC 机制会导致一些停顿，同样会影响读写性能。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// 代码逻辑有删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">public</span> <span class="kt">void</span> <span class="nf">createNode</span><span class="o">(</span><span class="kd">final</span> <span class="n">String</span> <span class="n">path</span><span class="o">,</span> <span class="kt">byte</span> <span class="n">data</span><span class="o">[],</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">ACL</span><span class="o">&gt;</span> <span class="n">acl</span><span class="o">,</span> <span class="kt">long</span> <span class="n">ephemeralOwner</span><span class="o">,</span> <span class="kt">int</span> <span class="n">parentCVersion</span><span class="o">,</span> <span class="kt">long</span> <span class="n">zxid</span><span class="o">,</span> <span class="kt">long</span> <span class="n">time</span><span class="o">,</span> <span class="n">Stat</span> <span class="n">outputStat</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">String</span> <span class="n">parentName</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="n">0</span><span class="o">,</span> <span class="n">lastSlash</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">DataNode</span> <span class="n">parent</span> <span class="o">=</span> <span class="n">nodes</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">parentName</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">   <span class="kd">synchronized</span> <span class="o">(</span><span class="n">parent</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Set</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span> <span class="n">children</span> <span class="o">=</span> <span class="n">parent</span><span class="o">.</span><span class="na">getChildren</span><span class="o">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="o">(</span><span class="n">children</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">childName</span><span class="o">))</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">throw</span> <span class="k">new</span> <span class="n">NodeExistsException</span><span class="o">();</span>
</span></span><span class="line"><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="cl">       <span class="c1">// 更新 父节点的版本
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="o">(</span><span class="n">parentCVersion</span> <span class="o">&gt;</span> <span class="n">parent</span><span class="o">.</span><span class="na">stat</span><span class="o">.</span><span class="na">getCversion</span><span class="o">())</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">parent</span><span class="o">.</span><span class="na">stat</span><span class="o">.</span><span class="na">setCversion</span><span class="o">(</span><span class="n">parentCVersion</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">            <span class="n">parent</span><span class="o">.</span><span class="na">stat</span><span class="o">.</span><span class="na">setPzxid</span><span class="o">(</span><span class="n">zxid</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="cl">       <span class="c1">// ..
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="o">}</span>
</span></span><span class="line"><span class="cl">   <span class="c1">// ..
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="snaplog">SnapLog</h4>
<p>SnapLog 是 Zookeeper 的持久化存储模块，用于将 Zookeeper 的内存数据备份到磁盘上。SnapLog 由两部分组成：事务日志（Transaction Log）和快照文件（Snapshot File）。事务日志用于记录所有的数据变更操作，快照文件会定期全量备份 DataTree 中的所有数据。当 Server 启动时，会先加载最近日期的快照文件，然后逐个加载事务日志文件，最终恢复到最新的状态。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">FileTxnSnapLog</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">dataDir</span><span class="o">;</span> <span class="c1">// 事务日志文件目录
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">snapDir</span><span class="o">;</span> <span class="c1">// 快照文件目录
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="n">TxnLog</span> <span class="n">txnLog</span><span class="o">;</span> <span class="c1">// 事务日志管理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">private</span> <span class="n">SnapShot</span> <span class="n">snapLog</span><span class="o">;</span> <span class="c1">// 快照管理器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>客户端的每次写入操作都会同步到磁盘，这会增加写操作的延迟，因此事务日志的写入性能直接决定 Zookeeper Server 对请求的响应速度，为了增加写入性能，Zookeeper 采用磁盘预分配的策略，在事务日志文件创建之初就向操作系统预分配一个很大的磁盘块，默认是64M，而一旦已分配的文件剩余空间不足 4KB 时，那么将会再次进行预分配</p>
<h4 id="性能瓶颈">性能瓶颈</h4>
<p>相信各位读者看到这里，已经对 Zookeeper 的存储机制有熟悉的感觉了，可以将<code>ZKDatabase</code>看作是一个简化版本的 Redis 实现，只支持基数树这种 KV 数据结构，同样也使用 WAL 日志和快照文件来保证数据的持久化。Zookeeper 的数据存储是基于内存的，所有的数据都存储在内存中，虽然能够保证数据的快速查询，但是也会带来一些问题：</p>
<ol>
<li><strong>内存空间</strong>：Zookeeper 的所有数据都存储在内存中，包括 DataNode、Key Path、Watcher 等，内存上限就是 Zookeeper Server 的数据存储上限，因此 Zookeeper 只能存储 GB 级别的数；另一方面过多的数据量也会增加 GC 的压力，降低哈希表查询的性能，都会请求响应速度；</li>
<li><strong>持久化</strong>：Zookeeper 的持久化机制是基于文件系统的，每次写入操作都会同步操作日志到磁盘，同样会增加写入操作的延迟，降低写入性能；</li>
</ol>
<p>综上所述，内存空间是 Zookeeper 的死穴，内存决定了 Zookeeper 的数据存储上限，而磁盘 I/O 决定了 Zookeeper 的写入延迟与响应速度，使得 Zookeeper 只能支持几 GB 级别的数据存储，这是 Zookeeper 最大的局限性，也是 Zookeeper 在大规模集群中的瓶颈。</p>
<h4 id="etcd-存储实现">ETCD 存储实现</h4>
<p>做为对比，我们再来看看 ETCD 的存储模块实现思路。ETCD 内部实现了一个基于 MVCC（多版本并发控制）的存储引擎，ETCD 的数据存储是基于磁盘的，所有的数据都存储在磁盘中，ETCD 的数据存储突破了内存的上限，因此 ETCD 能够存储几十甚至上百 GB 级别的数据。</p>
<p>ETCD 的 MVCC 模块实现了状态机存储功能，其底层使用的是开源的嵌入式键值存储数据库 BoltDB，但是这个项目已经由作者归档不再维护了，因此 ETCD 社区自己维护了一个 <em><a href="https://github.com/etcd-io/bbolt" target="_blank" rel="noopener noreferrer">bbolt</a></em> 版本。</p>
<p>为了实现多版本并发控制，ETCD 会将键值对的每个版本都保存到 BoltDB 中，ETCD 在 BoltDB 中存储的 Key 是修订版本<code>reversion</code>，Value 是客户端发送的键值对组合。为了更好地理解这一概念，假设我们通过读写事务接口写入了两个键值对，分别是(key1, value1)和(key2, value2)，之后我们再调用读写事务接口更新这两个键值对，更新后为(key1, update1)和(key2, update2)，虽然两次写操作更新的是两个键值对，实际上在 BoltDB 中写入了四条记录：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">rev</span><span class="o">={</span><span class="m">1</span> 0<span class="o">}</span>, <span class="nv">key</span><span class="o">=</span>key1, <span class="nv">value</span><span class="o">=</span><span class="s2">&#34;valuel&#34;</span> 
</span></span><span class="line"><span class="cl"><span class="nv">rev</span><span class="o">={</span><span class="m">1</span> 1<span class="o">}</span>, <span class="nv">key</span><span class="o">=</span>key2, <span class="nv">value</span><span class="o">=</span><span class="s2">&#34;value2&#34;</span> 
</span></span><span class="line"><span class="cl"><span class="nv">rev</span><span class="o">={</span><span class="m">2</span> 0<span class="o">}</span>, <span class="nv">key</span><span class="o">=</span>key1, <span class="nv">value</span><span class="o">=</span><span class="s2">&#34;updatel&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">rev</span><span class="o">={</span><span class="m">2</span> 1<span class="o">}</span>, <span class="nv">key</span><span class="o">=</span>key2, <span class="nv">value</span><span class="o">=</span><span class="s2">&#34;update2&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>其中，reversion 主要由两部分组成， 第一部分是 main reversion，每次事务递增一；第二部分是 sub reversion，同一个事务的每次操作都会递增一，两者结合就可以保证 Key 唯一且递增。在上面的示例中，第一个事务的 main reversion 是 1，第二个事务的 main reversion 是 2。</p>
<p>从 MVCC 模块保存的数据格式我们可以看出，如果要从 BoltDB 中查询键值对，必须通过<code>reversion</code>进行查找。但客户端只知道具体键值对中的 Key 值，并不清楚每个键值对对应的 reversion 信息。</p>
<p></p>
<p>为了将客户端提供的原始键值对信息与<code>reversion</code>关联起来，ETCD 使用谷歌开源实现的 <em><a href="https://github.com/google/btree" target="_blank" rel="noopener noreferrer">btree</a></em> 数据结构维护 Key 与<code>reversion</code>之间的映射关系，BTree 的键部分存储了原始的 Key，值部分存储了一个 keyIndex 实例。一个 keyIndex 实例维护着某个 Key 全部历史修订版本信息。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// etcd/mvcc/backend/backend.go
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">keyIndex</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">key</span> <span class="p">[]</span><span class="kt">byte</span>        <span class="c1">// 客户端提供的原始 Key 值
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">modified</span> <span class="nx">revision</span> <span class="c1">// 该 Key 值最后一次修改时对应的 revision 信息
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">generations</span> <span class="p">[]</span><span class="nx">generation</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">revision</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">main</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="cl">    <span class="nx">sub</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>由此可见，ETCD 的存储实现是基于磁盘的，存储数据不会被限制在内存大小，同时也能够保证数据的持久化。另一方面，ETCD 核心团队对 BoltDB 的读写性能做了众多优化：<em><a href="https://github.com/etcd-io/etcd/issues/10525" target="_blank" rel="noopener noreferrer">Fully concurrent reads design proposal</a></em>、<em><a href="https://www.cncf.io/blog/2019/05/09/performance-optimization-of-etcd-in-web-scale-data-scenario/" target="_blank" rel="noopener noreferrer">Performance optimization of etcd in web scale data scenario</a></em> 等，尽可能避免读写事务之间互相阻塞。虽然 ETCD 需要大量的内存来维护索引与数据缓存，当与 Zookeeper 相比，ETCD 能够支撑上百 GB 级别的数据存储，并且能够保证请求的响应速度。</p>
<h2 id="危险的全局-session">危险的全局 Session</h2>
<p>在 Zookeeper 中，Session 是个非常重要的概念，客户端与 Server 之间的任何交互都是通过 Session 来完成的，包含临时节点的生命周期、Watcher 通知、客户端与 Server 之间的心跳等。Zookeeper 的 Session 是一个全局的概念，每个客户端首先会与服务器建立一个 TCP Socket 连接，从连接建立开始，客户端会话的生命周期也开始了，并为该 Session 分配一个全局唯一的<code>SessionId</code>，标识客户端的身份。通过这个连接，客户端能够通过心跳检测与服务器保持有效的会话，也能够向 Zookeeper Server 发送请求并接受响应，同时还能够通过该连接接收来自服务器的 Watch 事件通知。</p>
<h4 id="实现原理">实现原理</h4>
<p>Session 通过心跳检测来保持有效，如果客户端在一定时间内没有向服务器发送心跳检测，那么服务器会认为客户端已经失效，Session 也会被关闭。如果 Session 超时，Zookeeper 服务器会将 Session 关联的所有临时节点删除。Session 设计在一定程度上简化了开发，能够在客户端故障时自动释放资源，在分布式锁、服务注册与发现等场景中也能够发挥作用，但是全局 Session 也带来了一些问题。</p>
<p>每个客户端实例同一时刻只能有一个 Session，这意味着如果一个客户端实例同时创建了多个临时节点，那么这些临时节点的生命周期是一致的。如果我们想要显式地删除某个临时节点，那么我们只能通过<code>delete</code>操作来删除，而不能通过关闭 Session 来让 ZNode 失效，这样操作会带来额外的复杂性：</p>
<ul>
<li>
<p><strong>增加 Client 实现的复杂度</strong>：由于 Session 是全局的，因此我们需要在客户端实现中维护 Session 的生命周期，确保 Session 超时后能够去激活所有的 Watcher，并在 Session 超时后重新创建 Session，这样会增加客户端实现的复杂性，也会增加客户端出现问题的概率；</p>
</li>
<li>
<p><strong>异常处理</strong>：如果我们的客户端实例在删除临时节点时发生了异常，那么这个临时节点可能会一直存在，直到 Session 超时。因此我们在编写业务逻辑代码时，需要特别小心，确保我们的代码在发生异常时，能够处理异常并重试，保证临时节点能够被正确删除；</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">deleteNode</span><span class="p">(</span><span class="nx">client</span> <span class="o">*</span><span class="nx">zk</span><span class="p">.</span><span class="nx">Conn</span><span class="p">,</span> <span class="nx">path</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">zk</span><span class="p">.</span><span class="nx">ErrNoNode</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>重复创建</strong>：如果我们的客户端实例在执行<code>create</code>临时节点操作后，该临时节点在 Zookeeper 集群上创建成功，但是客户端没有及时接收到创建成功的响应，或是响应丢失，那么这个临时节点可能会一直存在，直到 Session 超时。例如在实现分布式锁时，我们需要为每个服务实例创建一个临时有序节点，如果某个实例第一次创建临时节点<code>/service/lock-100</code>成功，但是没有及时接收到创建成功的响应，那么它可能会再次创建一个临时节点<code>/service/lock-102</code>，这样就会导致同一个服务实例同时持有两个分布式锁节点，导致死锁：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># znode: /service/lock-100 session: node-1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># znode: /service/lock-101 session: node-2</span>
</span></span><span class="line"><span class="cl"><span class="c1"># znode: /service/lock-102 session: node-1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>如上所示，node-1 同时持有<code>/service/lock-100</code>和<code>/service/lock-102</code>两个分布式锁节点，同时 node-2 持有<code>/service/lock-101</code>分布式锁节点，node-1 会认为自己持有<code>/service/lock-102</code>分布式锁节点，等待 node-2 释放<code>/service/lock-101</code>分布式锁节点，而 node-2 也在等待释放<code>/service/lock-100</code>节点，这样就会导致死锁，所有的实例都抢占到分布式锁而无法继续执行。</p>
<p>由于 node-1 的所有临时节点的生命周期是一致的，上述问题只能在 node-1 的 Session 超时或主动关闭 Client 后才能解决，因此我们在编写业务逻辑代码时，需要针对这种情况进行处理：node-1 在创建临时节点后，如果没有及时接收到创建成功的响应，那么它需要检查自己是否已经创建了临时节点，遍历<code>/service</code>目录下的所有临时节点，检查是否有存在临时节点的 SessionID 与当前客户端一致，如果存在，则复用这个临时节点，否则创建新的临时节点；</p>
</li>
</ul>
<p>上述问题是笔者在使用 Zookeeper 时踩到过的坑，虽然可以通过一些手段来规避——增加大量的重试代码与边界条件处理代码，但是这些方式会增加客户端与业务逻辑的复杂度，另一方面，在系统初期实现时，如果对 Zookeeper 的 Session 机制不够熟悉，我们很容易忽略这些问题，导致系统出现 BUG。</p>
<h4 id="etcd-设计思想">ETCD 设计思想</h4>
<p>为了规避上述问题，在 ETCD 的设计中，采用了更加灵活的 Session 概念，客户端与服务端连接建立后，不需要绑定一个特定的 Session，每个客户端实例可以创建并持有多个 Session，每个 Session 可以关联一个或多个临时节点，单个 Session 的失效只会影响与其关联的临时节，避免了全局 Session 带来的安全性问题。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewLock</span><span class="p">(</span><span class="nx">client</span> <span class="nx">clientv3</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">lockKey</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">ttl</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="nx">concurrency</span><span class="p">.</span><span class="nx">Mutex</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">session</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewSession</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">WithTTL</span><span class="p">(</span><span class="nx">ttl</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">rerurn</span> <span class="kc">nil</span><span class="p">,</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">rerurn</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewMutex</span><span class="p">(</span><span class="nx">session</span><span class="p">,</span> <span class="nx">lockKey</span><span class="p">),</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的示例代码中，我们可以看到，每次创建分布式锁时，我们都会创建一个新的 Session，并将其绑定到 key 上，单个 Session 的生命周期与绑定到这个 Session 的所有 key 的生命周期是一致的，当我们创建分布式锁失败时，无论错误原因是是什么，我们只需要确保 Session 被关闭，或停止续租，当 TTL 超时后绑定到这个 Session 的所有 key 都会被删除，最终临时 key 都会被清理。</p>
<h2 id="不可靠的-watcher">不可靠的 Watcher</h2>
<p>Zookeeper 的 Watcher 机制是 Zookeeper 提供的一种事件通知机制，当我们在某个 ZNode 上注册了 Watcher 时，如果这个 ZNode 发生了变化，Zookeeper 服务器会通知客户端，客户端可以通过 Watcher 机制来实现一些高级特性，比如分布式锁、配置管理等。</p>
<h4 id="实现原理-1">实现原理</h4>
<p><code>WatchManager</code>是 Zookeeper Watcher 机制的核心组件，它负责管理所有的 Watcher，其内部维护了两个哈希表：<code>watchTable</code> 和 <code>watch2Paths</code>，<code>watchTable</code> 是一个从 ZNode 到 Watcher 的映射表，<code>watch2Paths</code> 是一个从 Watcher 到 ZNode 的映射表，其内部实现如下：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">WatchManager</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">final</span> <span class="n">HashMap</span><span class="o">&lt;</span><span class="n">String</span><span class="o">,</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="n">Watcher</span><span class="o">&gt;&gt;</span> <span class="n">watchTable</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">        <span class="k">new</span> <span class="n">HashMap</span><span class="o">&lt;</span><span class="n">String</span><span class="o">,</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="n">Watcher</span><span class="o">&gt;&gt;();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">final</span> <span class="n">HashMap</span><span class="o">&lt;</span><span class="n">Watcher</span><span class="o">,</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;&gt;</span> <span class="n">watch2Paths</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">        <span class="k">new</span> <span class="n">HashMap</span><span class="o">&lt;</span><span class="n">Watcher</span><span class="o">,</span> <span class="n">HashSet</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;&gt;();</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Watcher 机制的实现是通过在 ZNode 上注册 Watcher，当 ZNode 发生变化时，通过<code>watchTable</code>找到所有注册在这个 ZNode 上的 Watcher，然后通知这些 Watcher。Zookeeper Server 会将 Watcher 事件通知发送给客户端，从而实现 Watch 事件通知机制。但 Zookeeper 的 Watcher 机制存在一些问题。</p>
<p><strong>Watcher 机制是一次性的</strong>，当 ZNode 发生变化时，Zookeeper Server 会调用<code>WatchManager.triggerWatch</code>方法触发数据变更事件，同时将这些 Watcher 从<code>watchTable</code>中删除，这意味着每个 Watcher 只能接收到一次通知，如果我们想要继续监听 ZNode 的变化，那么我们需要重新注册 Watcher。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kt">void</span> <span class="nf">process</span><span class="o">(</span><span class="n">WatchedEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getType</span><span class="o">()</span> <span class="o">==</span> <span class="n">Event</span><span class="o">.</span><span class="na">EventType</span><span class="o">.</span><span class="na">NodeDataChanged</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 重新注册 Watcher
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">zk</span><span class="o">.</span><span class="na">getData</span><span class="o">(</span><span class="s">&#34;/service/lock&#34;</span><span class="o">,</span> <span class="k">this</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>并且 Watcher 与客户端的 Session 绑定，当 Session 超时或关闭时，所有的 Watcher 都会失效，客户端需要重新注册 Watcher，在重新建立连接前，任何 ZNode 的变化都不会通知客户端。那么我们在接收到通知后，或出现网络故障，都需要重新注册 Watcher，如果我们在重新注册 Watcher 之前，ZNode 发生了变化，那么我们就会错过这次变化，从而导致客户端观测到的数据变化过程少于真实的数据变化过程，因此 Zookeeper 的 Watcher 机制只能保证最终一致性，而不能保证线性一致性。</p>
<p></p>
<p>综上所述，Zookeeper 的 Watcher 机制是一次性的，且与 Session 绑定，当 Session 超时或关闭时，所有的 Watcher 都会失效，在重新建立连接前，任何 ZNode 数据变化事件都会丢失，无法保证 Watcher 事件通知的可靠性，因此 Watcher 机制只能保证最终一致性，而不能保证线性一致性或顺序一致性。</p>
<h4 id="etcd-实现思路">ETCD 实现思路</h4>
<p>ETCD 也在 Watcher 机制上做了一些改进，ETCD 的 Watcher 机制是持久性的，当客户端收到通知后，Watcher 不会被删除，而是会一直保持有效，直到客户端主动删除 Watcher。ETCD Watcher 也与客户端的连接状态无关，即使客户端断开连接，Watcher 仍然有效，当客户端重新连接后，仍然可以接收到在断开期间发生的所有事件。具体来说，ETCD 的 Watcher 事件不会丢失的原理如下：</p>
<ol>
<li>
<p>当客户端注册 Watcher 后，ETCD Server 会创建<code>watcher</code>实例并加入到 boltdb 存储的 Watchers 集合中管理，同时将<code>watcher</code>绑定到 Key 上，当 Key 的值发生变化时，ETCD 服务器会将这个变化事件发送给所有注册在该 Key 上的 Watcher。</p>
</li>
<li>
<p>ETCD 的 Watcher 机制依赖于其多版本并发控制（MVCC）机制，实现了历史事件的持久化存储，如果客户端故障，Server 会将数据变更事件按照 FIFO 顺序持久化在存储引擎中，等待客户端恢复；</p>
</li>
<li>
<p>ETCD Watcher 能够在异常场景重试，并对历史事件进行重放：当客户端重新连接后，ETCD 服务器会将在断开期间发生的所有历史事件重新发送给客户端，确保客户端不会错过任何事件。</p>
</li>
</ol>
<p>由此可见，ETCD 的 Watcher 机制是持久性的，且与客户端的连接状态无关，当客户端发生故障时，ETCD 服务器会保存这个事件，等待客户端重新连接后按照 FIFO 顺序重新发送，这样就能够保证 Watcher 事件通知的可靠性与有序性。ETCD 的 Watcher 机制具有更高的一致性级别，能够保证顺序一致性。</p>
<h2 id="总结">总结</h2>
<p>本文总结了 Zookeeper 的存储性能、全局 Session、Watcher 机制等局限性，针对这些问题给出了一些可行的规避方案，并与新兴的 ETCD 实现原理进行了对比。</p>
<p>ZooKeeper 的出现填补了分布式协调组件的空白，在经历了多年的业务发展与技术迭代后，ZooKeeper 暴露了许多问题，难以支撑更大规模的集群。ETCD 是一个新兴的分布式 KV 存储系统，相较于 Zookeeper，ETCD 的存储性能更好，支持上百 GB 级别的数据存储；ETCD Watcher 机制更加灵活，支持持久性 Watcher，能够保证 Watcher 事件通知的可靠性与有序性。</p>
<p>在笔者看来，ETCD 与 ZooKeeper 并非是孰优孰劣的关系，ETCD 能够覆盖 Zookeeper 的应用场景，并且对 Zookeeper 暴露的许多问题进行了改进，是 ZooKeeper 的上位替代品，社区内的许多中间件也在考虑剥离对 Zookeeper 的依赖，替换为 Raft 实现或其它分布式协调服务：<em><a href="https://www.confluent.io/blog/removing-zookeeper-dependency-in-kafka/" target="_blank" rel="noopener noreferrer">Kafka Needs No Keeper - Removing ZooKeeper Dependency (confluent.io)</a></em>、<em><a href="https://streamnative.io/blog/moving-toward-zookeeper-less-apache-pulsar" target="_blank" rel="noopener noreferrer">Moving Toward a ZooKeeper-Less Apache Pulsar (streamnative.io)</a></em>。在新的分布式系统设计中，我们可以考虑使用 ETCD 来替代 Zookeeper，提高系统的性能与可靠性。</p>
]]></description>
</item><item>
    <title>构建可持续迭代的 Golang 应用</title>
    <link>https://wingsxdu.com/posts/golang/clean-go/</link>
    <pubDate>Sun, 11 Feb 2024 21:00:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/golang/clean-go/</guid>
    <description><![CDATA[<blockquote>
<p>本文是 Golang 程序编码指南，主要介绍 Golang 程序的编码风格、项目结构、开发工具链等，从而快速了解 Golang 程序的开发规范，提高代码质量。希望通过这篇文章，能够帮助大家快速地上手 Golang 程序的开发，减少后期维护成本。</p>
</blockquote>
<h2 id="概述">概述</h2>
<p>Golang 是一门非常优秀的编程语言，它的设计目标是提高程序员的生产力，因此 Golang 语言的设计非常注重简洁、高效、易用。经过数十年的发展，Golang 社区已经积累了大量的最佳实践，也形成了一些比较成熟的编码风格和项目结构规范。在构建多人协作的大型应用时，遵循这些规范可以提高代码的可读性、可维护性，减少代码的重构成本。</p>
<p>本文将从项目结构、代码质量、自动化工具与 CI/CD、项目拆分与重构、编程模式等多个方面介绍 Golang 编程规范与实用技巧，这些内容是我在实际项目中总结的经验，希望能够帮助大家构建可持续维护的 Golang 应用。</p>
<h2 id="项目结构">项目结构</h2>
<p>Golang 项目的结构对于项目的可维护性和可读性非常重要，一个好的项目结构可以让团队成员快速地定位到模块的位置，了解模块的功能和用法。Golang 社区中的优秀的项目结构大致可以分为两种：一种是以构建 SDK、开发框架为主的平铺式项目结构，另一种是以构建一个或多个二进制文件为主的标准 Go 项目结构。这两种项目结构都有各自的适用场景，需要根据实际的项目需求来选择。</p>
<h4 id="平铺式项目结构">平铺式项目结构</h4>
<p>平铺式项目结构适用于构建 SDK、开发框架等需要提供给其他开发者使用的项目，这种项目结构的特点是将所有的代码放在一个目录下，方便其他开发者快速地了解框架的功能和用法。由于在 Go 语言中一个目录就是一个 Namespace，采用平铺式结构的项目，可以将框架的所有功能都放在同一个命名空间下，其他开发者只需要通过 import 一个包路径就可以使用框架的所有功能。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── LICENSE
</span></span><span class="line"><span class="cl">├── README.md
</span></span><span class="line"><span class="cl">├── go.mod
</span></span><span class="line"><span class="cl">├── go.sum
</span></span><span class="line"><span class="cl">├── gin.go
</span></span><span class="line"><span class="cl">├── context.go
</span></span><span class="line"><span class="cl">├── xxx.go
</span></span></code></pre></td></tr></table>
</div>
</div><p>平铺式项目结构的优点是简单、易用，适合于小型项目，但是当项目规模变大时，这种项目结构会导致代码文件过多，不利于代码的维护和管理，所以一些复杂的开发框架会将不同的子模块放在不同的目录下，以便于更好地组织代码。例如 <em><a href="https://github.com/jedib0t/go-pretty" target="_blank" rel="noopener noreferrer">go-pretty</a></em> 将不同的子模块具有独立的依赖路径，并且可能存在依赖关系：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── list/
</span></span><span class="line"><span class="cl">├── progress/
</span></span><span class="line"><span class="cl">├── table/
</span></span><span class="line"><span class="cl">├── text
</span></span></code></pre></td></tr></table>
</div>
</div><p>常见的平铺式项目结构案例有：</p>
<ul>
<li>Web 开发框架 <em><a href="https://github.com/gin-gonic/gin" target="_blank" rel="noopener noreferrer">Gin</a></em>；</li>
<li>数据库 ORM 引擎 <em><a href="https://github.com/go-gorm/gorm" target="_blank" rel="noopener noreferrer">Gorm</a></em>；</li>
<li>社区为 Golang 开发人员提供的 API SDK 或 Client，例如 <em><a href="https://github.com/hashicorp/consul/tree/main/api" target="_blank" rel="noopener noreferrer">Consul API SDK</a></em>、<em><a href="https://github.com/redis/go-redis" target="_blank" rel="noopener noreferrer">Redis GO Client</a></em> 等。</li>
</ul>
<h4 id="标准-go-项目结构">标准 Go 项目结构</h4>
<p><em><a href="https://github.com/golang-standards/project-layout" target="_blank" rel="noopener noreferrer">Standard Go Project Layout</a></em> 是 Golang 社区常见的项目布局模式，虽然它不是 Go 核心开发团队定义的官方标准，但已经被 Golang 社区广泛采用。</p>
<p>标准项目结构的优点是清晰、通用，每个目录都有特定的作用，适用于大型项目。如果开发者都遵循这种项目结构，可以帮助团队成员快速地定位到模块的位置，了解模块的功能和用法。</p>
<p>标准项目结构详细介绍了其组织结构和每个目录的作用，感兴趣的读者可以点击链接查看，本小节仅阐述一些常用的目录：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── api
</span></span><span class="line"><span class="cl">├── build
</span></span><span class="line"><span class="cl">├── cmd
</span></span><span class="line"><span class="cl">├── docs
</span></span><span class="line"><span class="cl">├── internal
</span></span><span class="line"><span class="cl">├── pkg
</span></span><span class="line"><span class="cl">├── third_party
</span></span><span class="line"><span class="cl">├── tools
</span></span></code></pre></td></tr></table>
</div>
</div><h5 id="api">api</h5>
<p><code>api</code>目录用于存放 OpenAPI 或者 gRPC Proto 的定义文件，这些文件可以用于生成客户端代码、服务端代码、Swagger 文档等。通过 api 目录，能够清晰地了解项目的接口定义，方便其他开发者快速地了解项目的 API 功能和用法。</p>
<p>某些项目会将 api 目录作为对外的 SDK 或者 Client 的封装，例如 <em><a href="https://github.com/hashicorp/consul/tree/main/api" target="_blank" rel="noopener noreferrer">Consul API SDK</a></em>，这也符合 api 目录的作用。</p>
<h5 id="build">build</h5>
<p><code>build</code>目录用于存放构建 shell 脚本、Dockerfile、CI 等配置文件，这些文件可以用于自动化构建、测试、部署。将构建相关的文件放在 build 目录下，可以避免将构建脚本和业务代码混在一起，使得项目的结构更加简洁清晰。</p>
<h5 id="cmd">cmd</h5>
<p><code>cmd</code>目录是 Golang 项目的入口目录，用于存放项目的<code>main.go</code>文件。通常一个 Golang 项目需要构建出多个二进制可执行文件，由于每一个二进制文件的入口函数都需要一个独立的 main Package，我们可以将每个入口文件放在子目录下，例如<code>cmd/server/main.go</code>、<code>cmd/cli/main.go</code>等。</p>
<p>cmd 目录不应该包含业务逻辑，只包含 main 函数的入口文件，代码行数也应该尽量少，只包含一些简单的初始化逻辑。业务逻辑应该放在 internal 或 pkg 目录下，由 main 函数调用。</p>
<h5 id="docs">docs</h5>
<p><code>docs</code>目录用于存放项目的文档，包括设计文档、API 文档、使用手册等。通过 docs 目录，能够清晰地了解项目的设计思路、接口定义、使用方法等。</p>
<h5 id="internal">internal</h5>
<p><code>internal</code>是一个特殊的目录，它是 Golang 1.4 <a href="https://go.dev/doc/go1.4#internalpackages" target="_blank" rel="noopener noreferrer">Release Notes</a> 版本引入的一个新特性，用于限制包的可见性。</p>
<p>Go 语言鼓励模块化和封装，将代码组织成 Package 的形式，提高代码的可读性和复用性。但在某些场景下，我们希望某些 Package 只能在当前模块中使用，而不希望被其他模块引用。这种情况下，就可以使用 internal 目录来限制包的可见性：internal 目录下的代码只能被当前项目的其他 Package 引用，当其他项目引用当前项目时，internal 目录下的代码是不可见的。</p>
<p>我们可以将项目的私有代码，例如项目的内部核心逻辑、不希望被其他项目引用的客户端代码等，放在 internal 目录下，但外部模块引用该项目时，也无法访问内部的逻辑，并且减少了不必要的依赖。</p>
<h5 id="pkg">pkg</h5>
<p><code>pkg</code>目录用于存放项目的通用 Package，pkg 目录下的模块应该具有良好的封装和抽象，以便于业务代码复用。pkg 目录下的模块可以被其他项目引用，但是某些模块可能只是当前项目的内部实现细节，不希望被其他项目引用，这种情况下，可以将模块放在 internal 目录。</p>
<h5 id="third_party">third_party</h5>
<p><code>third_party</code>目录用于存放第三方 repo 的依赖，例如 swagger、依赖的 submodules、fork 的 repo 等。当项目需要独自维护第三方代码时，可以将第三方代码放在 third_party 目录下，以便于管理和维护。</p>
<h5 id="tools">tools</h5>
<p><code>tools</code>目录是 golang 项目的独有目录，用于约定项目的内/外部工具链，可以是<code>/pkg</code>或<code>/internal</code>目录下的内部工具，也可以 import 外部工具。</p>
<p>如下代码所示，我们利用<code>tools/tools.go</code>文件来约定项目的外部工具链，通过匿名导入的方式，将工具链的版本锁定在<code>go.mod</code>文件中。每次初始化开发环境时，只需要执行<code>go generate -tags tools tools/tools.go</code>命令，就可以自动安装工具链。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">//go:build tools
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">//go:generate go install github.com/swaggo/swag/cmd/swag
</span></span></span><span class="line"><span class="cl"><span class="c1">//go:generate go install github.com/fatih/gomodifytags
</span></span></span><span class="line"><span class="cl"><span class="c1">//go:generate go install go.uber.org/mock/mockgen
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">	<span class="nx">_</span> <span class="s">&#34;github.com/swaggo/swag/cmd/swag&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">_</span> <span class="s">&#34;github.com/fatih/gomodifytags&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">_</span> <span class="s">&#34;go.uber.org/mock/mockgen&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="代码质量">代码质量</h2>
<p>Golang 语言的设计目标是提高程序员的生产力，因此 Golang 语言的设计非常注重简洁、高效、易用。Golang 社区已经形成了一些比较成熟的编码风格，在构建多人协作的大型应用时，遵循这些规范可以提高代码的可读性、可维护性，减少代码的编写与维护成本。</p>
<h4 id="编码规范与代码格式化">编码规范与代码格式化</h4>
<p>做为有技术追求的软件工程师，我们应该在条件允许的情况下，遵循 Golang 社区的编码规范，保持代码的一致性，<em><a href="https://github.com/uber-go/guide" target="_blank" rel="noopener noreferrer">Uber Go Style Guide</a></em> 是 Golang 社区比较成熟的编码格式，它提供了一些比较成熟的编码技巧，例如代码规范、性能优化技巧、编程范式等。感兴趣的读者可以阅读上述链接内容，以助于我们编写出更加优雅的 Golang 代码。</p>
<p>Go 语言天然注重代码的格式化，它内置了<code>gofmt</code>、<code>goimports</code>等工具，可以帮助我们自动格式化代码，保持代码的一致性。在笔者看来，代码格式化是 Golang 语言的一大特色，它可以让开发者专注于业务的逻辑，而不用花费时间在争论代码的格式上。</p>
<p>笔者倾向于使用 <em><a href="https://golangci-lint.run/" target="_blank" rel="noopener noreferrer">golangci-lint</a></em> 进行代码格式化，它是一个集成了多种代码检查工具的运行器，可以帮助开发者自动格式化代码、检查命名规范、控制圈复杂度、并发安全检查等。使用 golangci-lint 可以帮助我们保持代码风格的一致性，减少 easy issue 的发生概率。通常情况下，笔者会开启所有的默认检查规则，并补充额外的检查规则，下面是我个人常用的 golangci-lint 配置文件，可以在此基础上根据自己的需求进行修改：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</span><span class="lnt">192
</span><span class="lnt">193
</span><span class="lnt">194
</span><span class="lnt">195
</span><span class="lnt">196
</span><span class="lnt">197
</span><span class="lnt">198
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># 检测基本配置</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">run</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># The default concurrency value is the number of available CPU.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">concurrency</span><span class="p">:</span><span class="w"> </span><span class="m">8</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Timeout for analysis, e.g. 30s, 5m.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: 1m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tests</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Which dirs to skip: issues from them won&#39;t be reported.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">skip-dirs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">third_party</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Which files to skip: they will be analyzed, but issues from them won&#39;t be reported.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">skip-files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">_test.go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">_mock.go</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;.*\\.pb\\.go&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;.*\\.gen\\.go&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">linters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">disable-all</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enable</span><span class="p">:</span><span class="w"> </span><span class="c"># please keep this alphabetized</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Don&#39;t use soon to deprecated[1] linters that lead to false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># https://github.com/golangci/golangci-lint/issues/1841</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">bodyclose</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">dogsled</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">dupl</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">errcheck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">exportloopref</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">exhaustive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">goconst</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">gocritic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">gofmt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># - gomnd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">gocyclo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># - gosec</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">gosimple</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">govet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">ineffassign</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">misspell</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">nolintlint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">nakedret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">prealloc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">predeclared</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">revive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">staticcheck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">stylecheck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">thelper</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">tparallel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">unconvert</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">unparam</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">whitespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">wsl</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># - unused</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">linters-settings</span><span class="p">:</span><span class="w"> </span><span class="c"># please keep this alphabetized</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">revive</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ignore-generated-header</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">confidence</span><span class="p">:</span><span class="w"> </span><span class="m">0.8</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">errorCode</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">warningCode</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">atomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">warning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">package-comments</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">unhandled-error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">arguments </span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;fmt.Printf&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">blank-imports</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">context-as-argument</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">context-keys-type</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dot-imports</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">error-return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">error-strings</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">error-naming</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">exported</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">warning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">arguments</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="l">disableStutteringCheck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">if-return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">increment-decrement</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">var-naming</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">var-declaration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">package-comments</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">range</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">receiver-naming</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">time-naming</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">unexported-return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">indent-error-flow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">errorf</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">argument-limit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">arguments</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="m">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">function-result-limit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">arguments</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">empty-block</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">confusing-naming</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">superfluous-else</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c">#      - name: unused-parameter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">unreachable-code</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">unnecessary-stmt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">struct-tag</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">atomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">empty-lines</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">duplicated-imports</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">import-shadowing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">confusing-results</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">modifies-parameter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">redefines-builtin-id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">staticcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">checks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;all&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;-SA1019&#34;</span><span class="w"> </span><span class="c"># TODO(fix) Using a deprecated function, variable, constant or field</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stylecheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">checks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;ST1019&#34;</span><span class="w">  </span><span class="c"># Importing the same package multiple times.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lll</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">line-length</span><span class="p">:</span><span class="w"> </span><span class="m">120</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">gocyclo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Minimal code complexity to report.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Default: 30 (but we recommend 10)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">min-complexity</span><span class="p">:</span><span class="w"> </span><span class="m">8</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">issues</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># List of regexps of issue texts to exclude.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">EXC0012  # EXC0012 revive</span><span class="p">:</span><span class="w"> </span><span class="l">exported (.+) should have comment( \(or a comment on this block\))? or be unexported</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">EXC0013  # EXC0013 revive</span><span class="p">:</span><span class="w"> </span><span class="l">package comment should be of the form &#34;(.+)...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">EXC0014  # EXC0014 revive</span><span class="p">:</span><span class="w"> </span><span class="l">comment on exported (.+) should be of the form &#34;(.+)...&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">EXC0015  # EXC0015 revive</span><span class="p">:</span><span class="w"> </span><span class="l">should have a package comment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Excluding configuration per-path, per-linter, per-text and per-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">exclude-rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Exclude some `typecheck` messages.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">linters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">typecheck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">text</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;undeclared name:&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">linters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">revive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">text</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;var-naming: don&#39;t use an underscore in package name&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Exclude `lll` issues for long lines with `go:generate`.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">linters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">lll</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;^//go:generate &#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Fix found issues (if it&#39;s supported by the linter).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">fix</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># output configuration options</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">output</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Multiple can be specified by separating them by comma, output can be provided</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># for each of them by separating format name and path by colon symbol.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Output path can be either `stdout`, `stderr` or path to the file to write to.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Example: &#34;checkstyle:report.json,colored-line-number&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: colored-line-number</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">colored-line-number,github-actions</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Print lines of code with issue.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">print-issued-lines</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Print linter name in the end of issue text.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">print-linter-name</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Make issues output unique by line.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">uniq-by-line</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Add a prefix to the output file references.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default is no prefix.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">path-prefix</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Sort results by: filepath, line and column.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sort-results</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">severity</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Set the default severity for issues.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># If severity rules are defined and the issues do not match or no severity is provided to the rule</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># this will be the default severity applied.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Severities should match the supported severity names of the selected out format.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default value is an empty string.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">default-severity</span><span class="p">:</span><span class="w"> </span><span class="l">error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># If set to true `severity-rules` regular expressions become case-sensitive.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">case-sensitive</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># When a list of severity rules are provided, severity information will be added to lint issues.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Severity rules have the same filtering capability as exclude rules</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># except you are allowed to specify one matcher per severity rule.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Only affects out formats that support setting severity information.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default: []</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">linters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">revive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">error</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="显式声明">显式声明</h4>
<p>Golang 团队对显式声明非常看重，他们认为显式声明可以提高代码的可读性，减少代码的歧义。在 Golang 语言中，显式声明主要体现在以下几个方面：</p>
<ul>
<li>变量声明时需要显式指定类型；</li>
<li>函数声明时需要显式指定参数和返回值的类型；</li>
<li>结构体声明时需要显式指定字段的类型。</li>
<li>接口声明时需要显式指定接口的方法。</li>
<li>&hellip;</li>
</ul>
<p>可以看到，Golang 语言中的显式声明非常严格，一些 Java 工程师可能会觉得 Golang 的显式声明过于繁琐，倾向于使用依赖注入、反射等技术来减少代码的重复。依赖注入通常在运行时解析依赖关系，因此可能会导致运行时错误，例如，如果一个依赖项没有被正确地注入，那么在运行时可能会出现空指针错误。过度使用依赖注入还会导致过度抽象，在 debug 代码分析时需要 trace 大量的依赖关系，才能找到真正的被注入对象，使得代码更难理解和维护。</p>
<p>笔者不会在 Golang 项目中使用依赖注入，而是遵循 Golang 团队的显式声明原则，尽量减少代码的歧义。在编写代码时，我们应该尽量避免使用隐式声明：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUserServer</span><span class="p">()</span> <span class="o">*</span><span class="nx">UserServer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">UserServer</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span>  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">server</span> <span class="o">:=</span> <span class="nf">NewUserServer</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Go 语言中另一个常见的隐式声明是<code>init</code>函数，它会在包被引用时自动执行，用于初始化包的状态。下面的示例代码是经典的<code>init</code>函数错误使用案例，它在 package 被引用时自动执行，用于初始化包的状态，创建数据库连接对象，并将连接对象分配给了一个全局变量。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">DB</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">db</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;mysql&#34;</span><span class="p">,</span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;dataSource&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">DB</span> <span class="p">=</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>init</code>函数会增加程序的不确定性，由于<code>init</code>函数是没有参数和返回值的，因此在示例中，需要通过环境变量来获取数据库连接的配置信息，这样就使得<code>init</code>函数的行为不可预料，如果环境变量设置错误，那么<code>init</code>函数无法处理连接建立失败的情况，只能粗暴地调用<code>panic</code>函数来终止程序执行。</p>
<p><code>init</code>函数还会降低代码的文档性，初次使用包时，开发者可能并不知道这里执行了一个<code>init</code>函数，也不知道<code>init</code>函数有哪些隐形的环境变量依赖或配置，也无从得知这个<code>init</code>函数的执行结果。因此它的执行结果是不可预料的，可能会导致一些难以预料的问题。在编写<code>init</code>函数时需要特别小心，避免在<code>init</code>函数中执行一些复杂的逻辑，尽量保持<code>init</code>函数的简单和干净。</p>
<p>上述代码中的<code>init</code>函数带来的另一个不确定性是将数据库连接对象分配给了全局变量<code>DB</code>，我们可能会在程序中的任何地方更改<code>DB</code>变量的值，这就使得<code>DB</code>变量的生命周期不可控，也不利于单元测试的编写。</p>
<p>这并不意味着<code>init</code>函数是一个坏东西，它在某些场景下是非常有用的，例如在<code>database/sql</code>包中，<code>init</code>函数用于自动注册数据库驱动，这样在程序中就可以通过<code>sql.Open</code>函数来创建数据库连接对象。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;database/sql&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">_</span> <span class="s">&#34;github.com/go-sql-driver/mysql&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">New</span><span class="p">(</span><span class="nx">dsn</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">db</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;mysql&#34;</span><span class="p">,</span> <span class="nx">dsn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Ping</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">db</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在日常编码中，笔者也会注意避免使用隐式声明与隐式依赖，提高代码的可读性与 debug 的可追踪性。</p>
<h4 id="错误处理">错误处理</h4>
<p>Go 的错误处理也遵循了显式声明的原则，不同于 Python 或 Java 等语言的 Try-Catch 机制，Go 语言中的错误处理是通过显式声明错误来实现的，我们可以在任何地方返回错误，并在错误发生时及时处理。</p>
<p>Go error 设计的也具有一定的缺陷，例如<code>err != nil</code>条件成立时不再意味着一定发生了错误，在标准库中<code>io.Reader</code>返回<code>io.EOF Error</code>来告知调用者数据已经读取完毕，虽然这并不意味着发生了错误，但是我们仍然需要显式地处理这个错误。</p>
<p>显式错误处理的也具有一些缺点，我们需要在每个函数调用处都进行错误处理，业务逻辑中编写大量的<code>if err != nil</code>会使得代码段看起来较为冗余。 为了简化错误处理的逻辑，Go 1.13 版本借鉴了<code>github.com/pkg/error</code>的设计思想，引入了<code>errors.Is</code>和<code>errors.As</code>、<code>fmt.Errorf(&quot;%w&quot;, err)</code>等函数，使得错误处理更加简洁：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;errors&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;fmt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">ErrNotFound</span> <span class="p">=</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;not found&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nf">do</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">ErrNotFound</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;not found&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>显式错误处理使得代码的行为更加可预测。当函数遇到错误时，它会显式地返回一个错误值，调用者可以根据这个错误值来决定如何处理错误。这种方式使得错误处理的逻辑更加清晰，也利于编写单元测试，我们可以模拟函数返回错误，然后检查调用者是否正确地处理了这个错误，提高代码的质量。</p>
<h4 id="单元测试">单元测试</h4>
<p>单元测试是保证代码质量的重要手段，Golang 语言内置了<code>testing</code>单元测试框架，可以帮助开发者编写高质量的单元测试用例，提高代码的正确性。在面向测试的开发中，要有意识地编写可测试的代码，笔者也积累了一些小技巧：</p>
<ul>
<li>编写可测试的代码，避免使用全局变量、单例模式等，模块之间的依赖关系应该通过 interface 来定义；</li>
<li>使用测试框架 <em><a href="https://github.com/uber-go/mock" target="_blank" rel="noopener noreferrer">gomock</a></em> 对 interface 进行 Mock，以便于验证上层模块的逻辑；</li>
<li>使用猴子补丁*<a href="https://github.com/agiledragon/gomonkey" target="_blank" rel="noopener noreferrer">gomonkey</a>* 对不便于测试的函数或方法进行 monkey patch；</li>
<li>使用断言库 <em><a href="https://github.com/stretchr/testify" target="_blank" rel="noopener noreferrer">testify</a></em> 对测试结果进行断言，以保证测试用例的正确性；</li>
<li>在执行单元测试时执行<code>go test -race</code>静态竞争检查，以保证代码的并发安全性；</li>
<li>测试用例要尽可能地覆盖所有的分支，包括正常分支、异常分支、边界分支等，避免将单元测试当做 Happy Path。</li>
</ul>
<p>单元测试可以自动化地执行大量的测试用例，从而节省了手动测试的时间，提高了开发效率。除此之外，我们在标准库的源码中还可以看到<code>example_test.go</code>文件，这些文件中包含了函数的使用示例，通过示例代码来展示函数的使用方法，作为函数的 API 文档。</p>
<p>当我们需要对代码进行重构时，单元测试可以作为一个安全网，确保重构不会引入新的错误，也不会更改现有的逻辑，提高代码的可维护性。</p>
<p>总体来说，单元测试是保证代码质量的必要手段，它可以帮助我们提早发现代码中的错误，即使项目经历了多名工程师的迭代开发，也能够通过单元测试了解代码的用途和行为。</p>
<h2 id="自动化工具与-cicd">自动化工具与 CI/CD</h2>
<p>Golang 语言的工具链非常丰富，在前面的内容中我们已经介绍了代码格式化工具<code>golangci-lint</code>、测试框架<code>gomock</code>、<code>testify</code>等，Go 社区十分喜欢使用自动化工具来自动生成代码，减少重复劳动，提高开发效率。本章节我们将介绍一些常用的 Golang 工具链：</p>
<ul>
<li><a href="https://buf.build/" target="_blank" rel="noopener noreferrer">buf.build</a>：buf 是一个用于管理 Protocol Buffers 文件的工具，它可以帮助开发者管理 Protocol Buffers 文件的版本、依赖、生成代码等，简化项目开发配置；</li>
<li><a href="https://github.com/fatih/gomodifytags" target="_blank" rel="noopener noreferrer">gomodifytags</a>：gomodifytags 是一个用于修改 Golang 结构体标签的工具，它可以帮助开发者自动添加、删除、修改结构体标签，简化代码的维护；</li>
<li><a href="https://github.com/go-gorm/gen" target="_blank" rel="noopener noreferrer">go-gorm gen</a>：go-gorm gen 是一个用于生成 GORM 模型的工具，它可以帮助开发者自动生成数据库 CRUD 代码，简化数据库操作；</li>
<li><a href="https://github.com/swaggo/swag" target="_blank" rel="noopener noreferrer">swaggo</a>：swaggo 是一个基于注释自动生成 Swagger API 文档的工具，简化 API 文档的维护；</li>
</ul>
<p>上述工具可以帮助我们自动生成代码、文档，简化项目的开发配置，当配置发生变化时，也能够自动更新代码，替代人工的重复劳动，提高人效比。</p>
<p>除了自动生成代码、文档外，我们还需要使用 CI/CD 工具来自动化构建、测试、部署，常见的 CI/CD 工具有 GitHub Actions、Jenkins 等，通常情况下，我们会在项目中维护一个<code>Makefile</code>文件，用于定义项目的构建、测试、部署等命令，然后在 CI/CD 工具中调用<code>make</code>命令来执行这些命令。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-Makefile" data-lang="Makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">lint</span>
</span></span><span class="line"><span class="cl"><span class="nf">lint</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    golangci-lint run
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">test</span>
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    go <span class="nb">test</span> -race -coverprofile<span class="o">=</span>coverage.out ./...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">    go build -o build/bin/app1 cmd/app1/main.go
</span></span></code></pre></td></tr></table>
</div>
</div><p>自动化在软件开发中有许多优点，一个稳定、完善的 CI/CD 系统可以帮助我们实现以下目标：</p>
<ol>
<li><strong>提高效率</strong>：通过自动执行耗时的任务（如构建和测试），可以节省大量时间，使工程师可以将更多的时间和精力投入到解决问题和添加新功能上，专注于更复杂的业务逻辑，减少不必要的心智负担，从而提高生产力；</li>
<li><strong>减少错误</strong>：人为操作更容易出错，例如我们在更新结构体标签时可能会有 typo，而自动化工具可以确保每次执行任务时都遵循相同的步骤和标准，从而提高结果的一致性，避免这种低级错误；</li>
<li><strong>快速反馈</strong>：CI/CD 可以在代码提交后立即运行，提供快速反馈，帮助工程师及时发现并修复问题；</li>
<li><strong>文档化过程</strong>：CI/CD 不仅能够自动执行任务，还记录了任务的执行过程，当任务失败时，可以通过 CI/CD 工具的日志来查看任务的执行过程，帮助工程师定位问题；</li>
</ol>
<h2 id="编程范式与设计模式">编程范式与设计模式</h2>
<p>编程范式与设计模式是软件工程师在编写代码时需要考虑的问题，它们可以帮助我们编写出更加优雅、扩展性更高的代码。在 Golang 语言中，我们可以使用一些编程范式和设计模式来提高代码的质量。</p>
<p>Go 语言是一门多范式编程语言，它支持面向对象编程、函数式编程、面向接口编程等多种编程范式，我们可以根据项目的需求选择合适的编程范式。而设计模式可以弥补编程语言表达能力的不足，它可以帮助我们解决一些常见的问题，提高代码的可维护性。本小节将会介绍一些常见的编程范式和设计模式。</p>
<h4 id="面向接口编程">面向接口编程</h4>
<p>面向接口编程是 Golang 语言的一大特色，它可以帮助我们提高代码的可扩展性。在 Golang 语言中，接口是一种抽象类型，它定义了一组方法，任何实现了这组方法的类型都可以被赋值给这个接口类型的变量。这种特性使得 Golang 语言可以很方便地实现依赖注入、多态等特性。</p>
<p>在 Golang 语言中，我们可以使用接口来定义模块的依赖关系，例如：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">FindByID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的代码中，我们定义了一个 Repository 接口，它定义了两个方法：<code>FindByID</code>和<code>Save</code>。任何实现了这两个方法的类型都可以被赋值给<code>Repository</code>类型的变量。这种特性使得我们可以很方便地实现依赖注入，例如：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserService</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Repository</span> <span class="nx">Repository</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">UserService</span><span class="p">)</span> <span class="nf">FindByID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">Repository</span><span class="p">.</span><span class="nf">FindByID</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">UserService</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">Repository</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>面向接口编程也可以帮助我们实现多态，在下面的代码中，我们定义了一个<code>MockRepository</code>类型与<code>MySQLRepository</code>类型，它实现了 Repository 接口的两个方法。在业务逻辑中我们可以根据上下文来选择不同的 Repository 实现：：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MockRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">MockRepository</span><span class="p">)</span> <span class="nf">FindByID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">User</span><span class="p">{},</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">MockRepository</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MySQLRepository</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">MySQLRepository</span><span class="p">)</span> <span class="nf">FindByID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">User</span><span class="p">{},</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">MySQLRepository</span><span class="p">)</span> <span class="nf">Save</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">repository</span> <span class="nx">Repository</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;ENV&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s">&#34;test&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">repository</span> <span class="p">=</span> <span class="o">&amp;</span><span class="nx">MockRepository</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">repository</span> <span class="p">=</span> <span class="o">&amp;</span><span class="nx">MySQLRepository</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nx">userService</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">UserService</span><span class="p">{</span><span class="nx">Repository</span><span class="p">:</span> <span class="nx">repository</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">userService</span><span class="p">.</span><span class="nf">FindByID</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>interface 接口可以将具体的实现和使用者解耦，使得代码更加模块化。使用者只需要知道接口的定义，而不需要关心具体的实现，这使得我们可以在不改变使用者代码的情况下更换实现，或者为同一个接口提供多种实现。interface 还可以让我们更容易地为代码编写测试，前文介绍过我们可以使用 gomock 创建 mock 对象来实现接口，在测试中使用这些 mock 对象，在不依赖外部系统的情况下测试代码。</p>
<h4 id="面向对象编程">面向对象编程</h4>
<p>面向对象编程是一种常见的编程范式，具有 封装、继承、多态三大特性，它将数据和操作数据的方法封装在一起，使得对象的功能更加聚合。相比于其他编程语言 Go 的面向对象模型更为简洁。在 Go 语言中，没有类（class）和继承（inheritance），而是通过结构体（struct）和接口（interface）来实现面向对象编程。这种简洁的设计使得 Go 语言的面向对象编程更易于理解和使用：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">ID</span>   <span class="kt">int</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">id</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">ID</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">Save</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;save user %s&#34;</span><span class="p">,</span> <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Tom&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">user</span><span class="p">.</span><span class="nf">Save</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的代码中，我们定义了一个 User 结构体，它有两个字段<code>ID</code>和<code>Name</code>，以及一个公开的<code>Save</code>方法。这种方式使得我们可以将数据和操作数据的方法封装在一起。</p>
<p>在继承方面，Go 语言采用了组合优于继承的方式，我们可以通过嵌入（embedding）其他的结构体或接口来复用代码，而不需要通过继承来实现代码的复用。这种设计使得我们可以更灵活地组合代码，而不需要考虑复杂的继承关系。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">NewUser</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">User</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Age</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">Save</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;save user %s, age %d&#34;</span><span class="p">,</span> <span class="nx">u</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="nx">u</span><span class="p">.</span><span class="nx">Age</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">NewUser</span><span class="p">{</span><span class="nx">User</span><span class="p">:</span> <span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Tom&#34;</span><span class="p">},</span> <span class="nx">Age</span><span class="p">:</span> <span class="mi">18</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">user</span><span class="p">.</span><span class="nf">Save</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的代码中，我们定义了一个<code>NewUser</code>结构体，它嵌入了<code>User</code>结构体，这使得<code>NewUser</code>结构体拥有了<code>User</code>结构体的所有字段和方法。我们也可以重写<code>NewUser</code>的<code>Save</code>方法，来实现多态特性。</p>
<p>我们已经介绍了 Go 语言的面向接口与面向对象编程，面向对象编程能够封装数据和操作数据的方法，隐藏对象的内部状态，只暴露必要的方法给外部，增强内聚度。面向接口编程能够将具体的实现和使用者解耦，使得代码更加模块化。它们可以帮助我们编写出更加模块化、可扩展的代码，降低模块的耦合度。在实际的项目中，我们可以根据实际需要选择合适的编程范式。</p>
<h4 id="选项模式">选项模式</h4>
<p>选项模式（Functional Options）可以说是 Go 语言中最常见的设计模式之一，它可以帮助我们简化代码的配置。选项模式的核心思想是将配置选项封装为一个函数，这个函数会返回一个可执行函数来更新构建时的配置项，这样我们就可以通过链式调用的方式来配置对象。</p>
<p>使用选项模式的原因是，不同于 Python 的构造函数在初始化时支持选择性传入多个参数，并为每个参数提供默认值，即使我们不传入任何参数，也可以创建一个新对象：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&#34;Tom&#34;</span><span class="p">,</span> <span class="n">age</span><span class="o">=</span><span class="mi">18</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="n">age</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&#34;Jerry&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>  <span class="c1"># Jerry</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">age</span><span class="p">)</span>   <span class="c1"># 18</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>要解决这个问题，最常见的方式是使用一个配置对象，如下所示，我们将非必要的选项都移到一个结构体里，：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Config</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Timeout</span>  <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span>
</span></span><span class="line"><span class="cl">    <span class="nx">TLS</span>      <span class="o">*</span><span class="nx">tls</span><span class="p">.</span><span class="nx">Config</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Request</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">host</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">    <span class="nx">port</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl">    <span class="nx">config</span> <span class="o">*</span><span class="nx">Config</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewRequest</span><span class="p">(</span><span class="nx">host</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">port</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">config</span> <span class="o">*</span><span class="nx">Config</span><span class="p">)</span> <span class="o">*</span><span class="nx">Request</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Request</span><span class="p">{</span><span class="nx">host</span><span class="p">:</span> <span class="nx">host</span><span class="p">,</span> <span class="nx">port</span><span class="p">:</span> <span class="nx">port</span><span class="p">,</span> <span class="nx">config</span><span class="p">:</span> <span class="nx">config</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的代码中，我们定义了一个<code>Config</code>结构体，它包含了一些可选的配置选项，然后我们在<code>NewRequest</code>函数中传入了一个<code>Config</code>结构体，与必要的参数，这样我们就可以通过<code>NewRequest</code>函数来创建一个<code>Request</code>对象。这种解决方式已经能够满足需求了，但还不够优雅，因为即使在我们并不需要配置这些选项的情况下，我们仍需要传入一个<code>Config</code>结构体，并要对其进行是否为 nil 校验。</p>
<p>选项模式可以进一步优化这个问题，我们定义一个<code>Option </code>函数类型，它接收一个<code>Request</code>指针类型的参数，然后返回一个函数，这个函数的参数是一个<code>Request</code>指针类型的参数，这样我们就可以通过链式调用的方式来配置<code>Request</code>对象：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Option</span> <span class="kd">func</span><span class="p">(</span><span class="o">*</span><span class="nx">Request</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Request</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">host</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">    <span class="nx">port</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl">    <span class="nx">tls</span> <span class="o">*</span><span class="nx">tls</span><span class="p">.</span><span class="nx">Config</span>
</span></span><span class="line"><span class="cl">    <span class="nx">timeout</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">timeout</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="nx">Option</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">r</span><span class="p">.</span><span class="nx">timeout</span> <span class="p">=</span> <span class="nx">timeout</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">WithTLS</span><span class="p">(</span><span class="nx">tls</span> <span class="o">*</span><span class="nx">tls</span><span class="p">.</span><span class="nx">Config</span><span class="p">)</span> <span class="nx">Option</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">r</span><span class="p">.</span><span class="nx">tls</span> <span class="p">=</span> <span class="nx">tls</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewRequest</span><span class="p">(</span><span class="nx">host</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">port</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">options</span> <span class="o">...</span><span class="nx">Option</span><span class="p">)</span> <span class="o">*</span><span class="nx">Request</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">r</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Request</span><span class="p">{</span><span class="nx">host</span><span class="p">:</span> <span class="nx">host</span><span class="p">,</span> <span class="nx">port</span><span class="p">:</span> <span class="nx">port</span><span class="p">,</span> <span class="nx">timeout</span><span class="p">:</span> <span class="mi">10</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">option</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">options</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">option</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">r</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">request</span> <span class="o">:=</span> <span class="nf">NewRequest</span><span class="p">(</span><span class="s">&#34;localhost&#34;</span><span class="p">,</span> <span class="mi">8080</span><span class="p">,</span> <span class="nf">WithTimeout</span><span class="p">(</span><span class="mi">5</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">),</span> <span class="nf">WithTLS</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">tls</span><span class="p">.</span><span class="nx">Config</span><span class="p">{}))</span>
</span></span><span class="line"><span class="cl">    <span class="nx">request</span> <span class="o">:=</span> <span class="nf">NewRequest</span><span class="p">(</span><span class="s">&#34;localhost&#34;</span><span class="p">,</span> <span class="mi">8080</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>使用选项模式可以选择性地配置对象，使得代码更加灵活，当我们需要添加新的配置选项时，只需要添加一个新的函数和配置字段即可。</p>
<h4 id="修饰器模式">修饰器模式</h4>
<p>修饰器模式（Decorator Pattern）是一种常见的设计模式，它可以帮助我们动态地为对象添加新的功能，降低代码的重复编写。在 Python 语言中，我们可以使用语法糖<code>@</code>来实现修饰器模式，例如：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python3" data-lang="python3"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">log</span><span class="p">(</span><span class="n">func</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;call </span><span class="si">{</span><span class="n">func</span><span class="o">.</span><span class="vm">__name__</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">wrapper</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl"><span class="nd">@log</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">add</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>在上面的代码中，我们定义了一个<code>log</code>修饰器，它接收一个函数作为参数，然后返回一个函数，这个函数的参数是一个函数，这样我们就可以通过<code>@log</code>语法糖来为<code>add</code>函数添加日志功能。</p>
<p>在 Golang 语言中，我们可以使用函数封装来实现修饰器模式：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">log</span><span class="p">(</span><span class="nx">f</span> <span class="kd">func</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span><span class="p">)</span> <span class="kd">func</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;call %s\n&#34;</span><span class="p">,</span> <span class="nx">runtime</span><span class="p">.</span><span class="nf">FuncForPC</span><span class="p">(</span><span class="nx">reflect</span><span class="p">.</span><span class="nf">ValueOf</span><span class="p">(</span><span class="nx">f</span><span class="p">).</span><span class="nf">Pointer</span><span class="p">()).</span><span class="nf">Name</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nf">f</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">add</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">add</span> <span class="p">=</span> <span class="nf">log</span><span class="p">(</span><span class="nx">add</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">add</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>相比之下，Golang 语言中的修饰器模式没有 Python 语言中的语法糖那么优雅，但它同样可以帮助我们动态地为对象添加新的功能。Go 语言中装饰器最常见的应用场景是自定义中间件，例如 Gin 框架中的中间件就大量使用了修饰器模式：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Logger</span><span class="p">()</span> <span class="nx">gin</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">t</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="nx">c</span><span class="p">.</span><span class="nf">Next</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="nx">latency</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Since</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Print</span><span class="p">(</span><span class="nx">latency</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">r</span> <span class="o">:=</span> <span class="nx">gin</span><span class="p">.</span><span class="nf">Default</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nf">Logger</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nf">GET</span><span class="p">(</span><span class="s">&#34;/ping&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">gin</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">c</span><span class="p">.</span><span class="nf">JSON</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="nx">gin</span><span class="p">.</span><span class="nx">H</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s">&#34;message&#34;</span><span class="p">:</span> <span class="s">&#34;pong&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="nx">r</span><span class="p">.</span><span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="小结">小结</h4>
<p>本小节简单介绍了 Golang 语言中的一些编程范式和设计模式，它们可以帮助我们编写出更加优雅、高可扩展的代码。如果读者想要深入了解这些编程范式和设计模式，可以参考已故程序员《左耳朵耗子》的 <em><a href="https://coolshell.cn/articles/21128.html" target="_blank" rel="noopener noreferrer">GO编程模式</a></em> 系列文章。</p>
<p>总的来说，Go 的语法本身就已经非常简洁，而且 Go 语言的接口和函数式编程特性使得我们可以更加灵活地组合代码，而不需要过多地依赖设计模式。但是设计模式仍然是一种很好的编程范式，它可以帮助我们解决一些 Go 语言的设计缺陷，让我们的代码更加优雅。</p>
<h2 id="项目拆分与重构">项目拆分与重构</h2>
<p>在大型的 Golang 项目中，随着业务的发展，代码的规模会变得越来越大，这时候就需要考虑对项目进行拆分与重构。项目拆分可以提高代码的可读性和可维护性，也可以提高开发效率。以下是一些常见的 Golang 项目拆分策略。</p>
<h4 id="水平拆分">水平拆分</h4>
<p>水平拆分或按照功能模块拆分是最常见的拆分策略，将不同的功能模块放在不同的包（Package）中。例如，用户管理、订单管理、商品管理等功能可以分别放在 user、order、product 等包中。这样做的好处是可以清晰地看到每个包的职责。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="err">├──</span> <span class="nx">user</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">entity</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">user</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">└──</span> <span class="nx">user_test</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">├──</span> <span class="nx">order</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">entity</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">order</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">└──</span> <span class="nx">order_test</span><span class="p">.</span><span class="k">go</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>由于 Golang 语言中的包是一个独立的 Namespace，因此不同的包可以有相同的函数名，我们可以通过包名来区分不同的函数：<code>user.FindByID</code>、<code>order.FindByID</code>。</p>
<p>随着业务的发展，单体应用可能会变得越来越复杂，这时候可能hui考虑将项目拆分为多个微服务。每个微服务都是一个独立的应用，微服务之间通过 API 进行通信，可以独立部署和扩展。如果我们在项目中使用了水平拆分的策略，那么将项目拆分为多个微服务会变得更加容易，因为每个子模块都是一个独立的包，天然具有独立性，降低微服务拆分时的改动成本。</p>
<h4 id="垂直拆分">垂直拆分</h4>
<p>垂直拆分或按照层次结构拆是另一种常见的拆分策略，将不同的层次放在不同的包中。例如，将数据访问层（Repository）、业务逻辑层（Service）、控制器层（Controller）放在独立的子目录中。每个层次都有其特定的职责，例如数据访问层负责与数据库交互，业务逻辑层负责处理业务逻辑，控制器层负责处理 HTTP 请求。</p>
<p>垂直拆分通常与水平拆分结合使用，以确保模块间是完全解耦的，每个功能模块都有自己的数据访问层、业务逻辑层、控制器层，例如将用户管理模块拆分为 order/repository、order/service、order/controller 等包。不同功能模块的内部代码对其它模块是不可见的，只能通过暴露的 Interface 进行通信。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="err">├──</span> <span class="nx">order</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">repository</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">│</span>   <span class="err">├──</span> <span class="nx">history</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">│</span>   <span class="err">├──</span> <span class="nx">order</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">├──</span> <span class="nx">service</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">│</span>   <span class="err">├──</span> <span class="nx">order</span><span class="p">.</span><span class="k">go</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>   <span class="err">└──</span> <span class="nx">controller</span>
</span></span><span class="line"><span class="cl"><span class="err">│</span>       <span class="err">├──</span> <span class="nx">order</span><span class="p">.</span><span class="k">go</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="重构">重构</h4>
<p>重构是一种常见的优化代码的手段，可以帮助我们提高性能、甩掉历史包袱、提高可扩展性等。重构可以帮助我们优化代码的设计，当新的需求出现时，我们可以更容易地在现有的代码基础上进行扩展。在前面的内容中我们介绍过了一些 Golang 的自动化工具、代码格式化、单元测试等，在重构的过程中，我们可以使用这些工具来确保重构前后功能一致性，避免引入新的错误。</p>
<p>重构往往发生在项目的后期，当项目的代码规模变得越来越大，代码的质量变得越来越差，或是项目的业务需求发生了变化，现有的代码无法满足新的需求时，我们就需要考虑对项目进行重构。<strong>但在笔者看来，重构不仅仅是在项目的后期才需要考虑的问题，它应该是项目的一部分，随着项目的发展时时刻刻都在进行。</strong></p>
<p>我们在开发新需求或是维护项目的过程中，可能会遇到一些代码质量不高的代码，例如：</p>
<ul>
<li>重复的代码段：在不同的地方出现了相同的代码段，这样的代码段不利于维护，因为每次修改都需要修改多个地方，或是发生了遗漏，我们可以将这些代码段抽取出来，维护在一个函数或方法中，在需要的地方调用这个函数；</li>
<li>不合理的结构体或接口：结构体或接口的定义不合理，我们可以通过重构来简化结构体或接口的定义；</li>
<li>冗余的参数：函数或方法的参数过多，或是参数的类型不合理，我们可以通过重构来简化参数。</li>
</ul>
<p>在项目早期，往往由于排期紧张、需求频繁变化等原因，我们的项目可能不会那么规范，只关注功能的实现，所以在功能迭代的过程中，我们可能每天都会对小段的代码进行重构，提高代码的整洁。</p>
<h2 id="总结">总结</h2>
<p>本篇文章主要介绍了 Golang 语言的一些最佳实践，包括代码格式化、错误处理、单元测试、自动化、编程模式、拆分与重构等。归根结底，我们的终极目标是写出优雅、干净、整洁的代码，提高项目的可维护性、可扩展性，降低维护的成本</p>
<p>在实际的工程中，我们需要在项目早期就开始执行严格的代码审查和 Code Review，遵循「先紧后松」的原则，保证代码的质量，如果我们在项目的早期没有注重代码规范，那么随着项目的发展，往往会变得越来越难以维护，即使想要添加更为严格的代码规范也会遭到许多质疑和反对的声音。</p>
<p>在项目的中后期，我们需要不断进行重构，对子模块进行重新设计与拆分，保持代码的整洁。在重构的过程中，我们可以使用自动化工具来确保重构前后功能一致性，避免引入新的错误。</p>
<p>希望本篇文章能够帮助各位工程师实现更加稳定可靠的 Go 服务。</p>
]]></description>
</item><item>
    <title>Golang 程序配置管理</title>
    <link>https://wingsxdu.com/posts/golang/service-config/</link>
    <pubDate>Sun, 27 Aug 2023 21:00:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/golang/service-config/</guid>
    <description><![CDATA[<blockquote>
<p>通常情况下，我们的应用程序会涉及到很多配置，比如 HTTP/RPC 监听端口、日志相关配置、数据库配置等。这些配置可以来自不同的配置源，例如本地配置文件、环境变量、命令行参数，或是 Consul 一类的远程配置中心，同时一项配置可能在多个不同的配置源中同时存在，需要进行优先级处理。这篇文章介绍使用 koanf 在 Golang 程序中进行配置管理。</p>
</blockquote>
<p><a href="https://github.com/spf13/viper" target="_blank" rel="noopener noreferrer">viper</a> 是 Golang 程序中被广泛使用的配置解析库，是一个开箱即用的配置 SKD，但是在使用过程中，viper 也暴露出来一些问题：</p>
<ul>
<li>强制规定配置 key 为小写格式，破坏了 TOML、HCL 等配置文件原有的语义定义：<a href="https://github.com/spf13/viper/pull/635" target="_blank" rel="noopener noreferrer">forcibly lowercasing keys</a>；</li>
<li>强制规定配置源的优先级：<a href="https://github.com/spf13/viper#why-viper" target="_blank" rel="noopener noreferrer">default precedence order</a>；</li>
<li>File、CLI、ENV 等配置解析实现硬编码在代码库中，没有提供在应用层面新增 Parser 或定制解析过程的 API，无法进行扩展；</li>
<li>一次拉取所有的第三方依赖，即使没有使用到对应的配置源和 Parser，viper 依然会拉取相应的依赖，例如 ETCD、Consul、gRPC 等：<a href="https://github.com/spf13/viper/issues/707" target="_blank" rel="noopener noreferrer">Why all the new dependencies?</a>；</li>
</ul>
<p>viper 的代码库实现很复杂，短时间内很难了解它的设计思路。viper 对外没有暴露可扩展的语义，在实际使用过程中，如果遇到无法覆盖的应用场景，往往需要在业务层进行单独处理，会对业务逻辑增加额外的侵入性。因此需要一个更加轻量级，且易于扩展的配置管理实现。</p>
<h2 id="概述">概述</h2>
<p><a href="https://github.com/knadh/koanf" target="_blank" rel="noopener noreferrer">knanf</a> 是一个轻量且易于扩展定制的 Golang 配置库，v2 版本通过独立的 Golang Module，将外部依赖项都与核心分离，并且可以根据需要单独安装，使得其核心代码只有一千余行：</p>
<ul>
<li>
<p>JSON、Yaml 等数据格式的解析扩展，只需要实现<code>Parser</code>接口，独立于 Parser 目录中：<a href="https://github.com/knadh/koanf/tree/master/parsers" target="_blank" rel="noopener noreferrer">koanf/parsers</a>；</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Parser</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nf">Unmarshal</span><span class="p">([]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">interface</span><span class="p">{},</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="nf">Marshal</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">interface</span><span class="p">{})</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>Consul、File、Env 等配置源的数据解析，只需要实现<code>Provider</code>接口，独立 Provider 目录中：<a href="https://github.com/knadh/koanf/tree/master/providers" target="_blank" rel="noopener noreferrer">koanf/providers</a>；</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Provider</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nf">ReadBytes</span><span class="p">()</span> <span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="nf">Read</span><span class="p">()</span> <span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">interface</span><span class="p">{},</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>koanf 提供了 Provider 与 Parser 接口，通过实现对应的接口，可以接入更多的配置解析方式；</p>
</li>
<li>
<p>koanf 自身并未规定配置源的优先级，koanf 会按照 Provider 的调用顺序，覆盖已有的值；</p>
</li>
</ul>
<p>koanf 的基础操作可以参考 <a href="https://github.com/knadh/koanf#readme" target="_blank" rel="noopener noreferrer">knanf/readme</a>，文中将不再赘述，以下内容将会在了解使用方式的基础上进行介绍。</p>
<h2 id="配置优先级">配置优先级</h2>
<p>koanf 会按照 Provider 的调用顺序，覆盖已有的值。因此我们可以通过封装统一的配置解析步骤，达到默认优先级的效果。例如下面的代码实现了一个 <code>Parse()</code> 函数，其参数有配置路径与数据格式，以及对应的 Parser（在示例中，支持本地文件与 Consul）：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Parse</span><span class="p">(</span><span class="nx">configPath</span><span class="p">,</span> <span class="nx">format</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">reader</span> <span class="nx">ParserFunc</span><span class="p">,</span> <span class="nx">opts</span> <span class="o">...</span><span class="nx">OptionFunc</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="kd">var</span> <span class="nx">parser</span> <span class="nx">koanf</span><span class="p">.</span><span class="nx">Parser</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">switch</span> <span class="nx">format</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">case</span> <span class="nx">ConfigFormatYAML</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">		<span class="nx">parser</span> <span class="p">=</span> <span class="nx">kyaml</span><span class="p">.</span><span class="nf">Parser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">	<span class="k">case</span> <span class="nx">ConfigFormatJSON</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">		<span class="nx">parser</span> <span class="p">=</span> <span class="nx">kjson</span><span class="p">.</span><span class="nf">Parser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">	<span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;unsupported config format: %s&#34;</span><span class="p">,</span> <span class="nx">format</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">reader</span><span class="p">(</span><span class="nx">configPath</span><span class="p">,</span> <span class="nx">parser</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// OptionFunc is the option function for config.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">OptionFunc</span> <span class="kd">func</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">any</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ParserFunc Parse config option func
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">ParserFunc</span> <span class="kd">func</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">parser</span> <span class="nx">koanf</span><span class="p">.</span><span class="nx">Parser</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">ReadFromFile</span><span class="p">(</span><span class="nx">filePath</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">parser</span> <span class="nx">koanf</span><span class="p">.</span><span class="nx">Parser</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">k</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">kfile</span><span class="p">.</span><span class="nf">Provider</span><span class="p">(</span><span class="nx">filePath</span><span class="p">),</span> <span class="nx">parser</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="c1">// Config file was found but another error was produced
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>		<span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;error loading config from file [%s]: %w&#34;</span><span class="p">,</span> <span class="nx">filePath</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ReadFromConsul read config from consul with format
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">ReadFromConsul</span><span class="p">(</span><span class="nx">configPath</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">parser</span> <span class="nx">koanf</span><span class="p">.</span><span class="nx">Parser</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">k</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">kconsul</span><span class="p">.</span><span class="nf">Provider</span><span class="p">(</span><span class="nx">kconsul</span><span class="p">.</span><span class="nx">Config</span><span class="p">{}),</span> <span class="nx">parser</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;error loading config from consul: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>配置数据可以以不同的数据格式与存储方式进行组织，得益于 koanf 优秀的解耦思想，我们可以很容易的支持内部的配置中心</p>
<p>在容器化部署的情况下，我们还会根据环境变量来修改当前程序的某些配置项，以便于在不修改镜像内容的情况下，进行动态配置。ENV Provider 通过统一前缀来标识所需要解析的环境变量，并提供了一个可选的 callback 函数，让用户自定义环境变量名称的处理逻辑。在示例代码中，我们自定义的回调函数去除了统一前缀，并将所有大写字符转为小写：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl">	<span class="c1">// Second, read config from environment variables,
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// Parse environment variables and merge into the loaded config.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// &#34;PUDDING&#34; is the prefix to filter the env vars by.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// &#34;.&#34; is the delimiter used to represent the key hierarchy in env vars
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// The (optional, or can be nil) function can be used to transform
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="c1">// the env var names, for instance, to lowercase them
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">k</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">kenv</span><span class="p">.</span><span class="nf">Provider</span><span class="p">(</span><span class="s">&#34;PUDDING/&#34;</span><span class="p">,</span> <span class="nx">defaultDelim</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">s</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">ReplaceAll</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">ToLower</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">			<span class="nx">strings</span><span class="p">.</span><span class="nf">TrimPrefix</span><span class="p">(</span><span class="nx">s</span><span class="p">,</span> <span class="s">&#34;PUDDING/&#34;</span><span class="p">)),</span> <span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="s">&#34;.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}),</span> <span class="kc">nil</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;error loading config from env: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="cli-优先级">CLI 优先级</h4>
<p>CLI 优先级是一个特殊的场景，他具有默认值，但是可以被命令行参数覆盖。由于 koanf repo 内并未实现 Golang 标准库 flag 的配置解析。因此需要自己实现一个 Provider。</p>
<p>为了便于理解，以下代码段仅贴出了关键部分。Golang flagSet 提供了<code>VisitAll</code>方法，以便于调用方能够访问 flagSet 中所有被命令行参数设置与未被设置的 flag。同时标准库中的所有 flag Value 类型都实现了<code>flag.Getter</code>方法，能够获取该 flag 的当前值，以及该值的字符串类型表示方法。我们将当前值与初始值进行比对，就能感知到命令行参数是否被设置。同时 koanf 也实现了 Exist 方法，用来判断 key 是否已经存在：</p>
<ul>
<li>如果命令行参数没有设置，并且低优先级的配置文件也没有设置，那么就使用 CLI 默认值；</li>
<li>如果命令行参数没有设置，但是低优先级的配置文件设置了，那么就使用低优先级的配置文件的值；</li>
<li>如果命令行参数设置了，那么就使用命令行参数的值；</li>
</ul>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Read reads the flag variables and returns a nested conf map.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Flag</span><span class="p">)</span> <span class="nf">Read</span><span class="p">()</span> <span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">any</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">mp</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">any</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">p</span><span class="p">.</span><span class="nx">flagSet</span><span class="p">.</span><span class="nf">VisitAll</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">f</span> <span class="o">*</span><span class="nx">flag</span><span class="p">.</span><span class="nx">Flag</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="kd">var</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">			<span class="nx">key</span>   <span class="kt">string</span>
</span></span><span class="line"><span class="cl">			<span class="nx">value</span> <span class="nx">any</span>
</span></span><span class="line"><span class="cl">		<span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">cb</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="nx">p</span><span class="p">.</span><span class="nf">cb</span><span class="p">(</span><span class="nx">f</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="nx">f</span><span class="p">.</span><span class="nx">Value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="c1">// All Value types provided by flag package satisfy the Getter interface
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>			<span class="c1">// if user defined types are used, they must satisfy the Getter interface
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>			<span class="nx">getter</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nx">Value</span><span class="p">.(</span><span class="nx">flag</span><span class="p">.</span><span class="nx">Getter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">				<span class="nb">panic</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;flag %s does not implement flag.Getter&#34;</span><span class="p">,</span> <span class="nx">f</span><span class="p">.</span><span class="nx">Name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">			<span class="p">}</span>
</span></span><span class="line"><span class="cl">			<span class="nx">key</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="nx">f</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="nx">getter</span><span class="p">.</span><span class="nf">Get</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="c1">// if the key is set, and the flag value is the default value, skip it
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>		<span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">ko</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">f</span><span class="p">.</span><span class="nx">Value</span><span class="p">.</span><span class="nf">String</span><span class="p">()</span> <span class="o">==</span> <span class="nx">f</span><span class="p">.</span><span class="nx">DefValue</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="k">return</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="nx">mp</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">=</span> <span class="nx">value</span>
</span></span><span class="line"><span class="cl">	<span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">maps</span><span class="p">.</span><span class="nf">Unflatten</span><span class="p">(</span><span class="nx">mp</span><span class="p">,</span> <span class="nx">p</span><span class="p">.</span><span class="nx">delim</span><span class="p">),</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>最后<code>Read</code>方法的返回值是一个<code>map[string]any</code>哈希表，koanf 会将该哈希表与已有的配置进行合并覆盖。</p>
<h2 id="配置-watch">配置 Watch</h2>
<p>配置的<code>Watch</code>方法并不包含在 koanf 的核心模块中，而是由 Provider 独自实现。我们需要向 Provider 注册一个回调函数，当有配置变更事件到来时，加载新的配置信息，并让业务模块感知到配置的变化。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl">	<span class="nx">f</span> <span class="o">:=</span> <span class="nx">file</span><span class="p">.</span><span class="nf">Provider</span><span class="p">(</span><span class="s">&#34;mock/mock.json&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">k</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">f</span><span class="p">,</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Parser</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">f</span><span class="p">.</span><span class="nf">Watch</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">event</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">k</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">f</span><span class="p">,</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Parser</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">		<span class="nx">k</span><span class="p">.</span><span class="nf">Print</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// trigger reload config
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="配置反序列化">配置反序列化</h2>
<p>koanf 所有的配置都会被保存在一个全局的 map 中，但是 map 是一个无类型的容器，是一个宽松的数据结构，不利于做类型检查与数据校验。
在实际使用过程中，我们往往需要将配置反序列化为结构体，以便于在代码中使用。koanf 提供了<code>Unmarshal</code>方法，可以将配置反序列化为结构体，因此我们需要在结构体中添加<code>mapstructure</code>标签，以便于 koanf 反序列化时能够正确的解析字段：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">BaseConfig</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">HostDomain</span> <span class="kt">string</span> <span class="s">`json:&#34;host_domain&#34; yaml:&#34;host_domain&#34; mapstructure:&#34;host_domain&#34;`</span>
</span></span><span class="line"><span class="cl">	<span class="nx">GRPCPort</span> <span class="kt">int</span> <span class="s">`json:&#34;grpc_port&#34; yaml:&#34;grpc_port&#34; mapstructure:&#34;grpc_port&#34;`</span>
</span></span><span class="line"><span class="cl">	<span class="nx">HTTPPort</span> <span class="kt">int</span> <span class="s">`json:&#34;http_port&#34; yaml:&#34;http_port&#34; mapstructure:&#34;http_port&#34;`</span>
</span></span><span class="line"><span class="cl">	<span class="nx">EnableTLS</span> <span class="kt">bool</span> <span class="s">`json:&#34;enable_tls&#34; yaml:&#34;enable_tls&#34; mapstructure:&#34;enable_tls&#34;`</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">UnmarshalToStruct</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">c</span> <span class="nx">any</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">k</span><span class="p">.</span><span class="nf">UnmarshalWithConf</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">c</span><span class="p">,</span> <span class="nx">koanf</span><span class="p">.</span><span class="nx">UnmarshalConf</span><span class="p">{</span><span class="nx">Tag</span><span class="p">:</span> <span class="s">&#34;mapstructure&#34;</span><span class="p">});</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to unmarshal config: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="总结">总结</h2>
<p>koanf 最精髓的设计之处在于，将 Parser、Provider 与 Core 解耦，通过插件的形式引入，每一个模块都可以根据需要单独安装。并且自定义实现 Provider 的成本低廉，能够便捷地进行功能扩展。</p>
]]></description>
</item><item>
    <title>漫谈分布式锁实现</title>
    <link>https://wingsxdu.com/posts/algorithms/distributed-lock/</link>
    <pubDate>Sun, 26 Dec 2021 21:50:48 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/algorithms/distributed-lock/</guid>
    <description><![CDATA[<blockquote>
<p>分布式锁是控制分布式系统之间同步访问共享资源的一种方式，本文介绍了使用 Redlock、etcd 等常见分布式锁的实现方式与 Google Chubby 的设计思路，并探讨不同类型的分布式锁的适用场景。</p>
</blockquote>
<p>在分布式系统与微服务架构中，不同的系统或同一个系统的不同节点之间共享同一个或一组资源，那么访问这些资源的时候，往往需要互斥来防止彼此干扰，保证一致性。</p>
<p>分布式锁提供了分布式环境下共享资源的互斥访问，业务依赖分布式锁追求效率提升，或者依赖分布式锁追求访问的绝对互斥。同时，在接入分布式锁服务过程中，要考虑接入成本、服务可靠性、分布式锁切换精度以及正确性等问题，正确和合理的使用分布式锁，是需要持续思考并予以优化的。</p>
<h2 id="概述">概述</h2>
<p>一般来说，分布式锁需要满足以下特性：</p>
<ol>
<li>排他性：在任意时刻，只有一个客户端能持有锁；</li>
<li>无死锁：即使有一个客户端在持有锁期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁；</li>
<li>加锁和解锁必须是同一个客户端，客户端不能把其他客户端加的锁释放掉；</li>
<li>容错性：少数节点失效，锁服务仍可以对外提供加解锁服务。</li>
</ol>
<p>其中分布式锁的死锁与编程语言提供锁的死锁概念不同，后者死锁描述的是多个线程由于相互等待而永远被阻塞的情况，分布式锁的死锁是指，如果请求执行因为某些原因意外退出了，导致创建了锁但是没有释放锁，那么这个锁将一直存在，以至于后续锁请求被阻塞住。为了防止死锁通常会给分布式锁设置 TTL(Time To Live)，TTL 过期后被自动释放。TTL 策略也有一定的弊端，如果执行任务的进程还没有执行完，但是锁因为 TTL 过期被自动释放，可能被其它进程重新加锁，这就造成多个进程同时获取到了锁。</p>
<h2 id="redis-分布式锁">Redis 分布式锁</h2>
<h4 id="redis-单机锁">Redis 单机锁</h4>
<p>Redis 单机锁的实现十分简单，只需要执行指令<code>SET key random_value NX PX 60000 </code>就可以获得一个 TTL 60s 的锁。如果这个 key 已经被强占了，那么客户端会获取锁失败。Redis 锁需要客户端实现算法保证所有获取锁请求生成随机的唯一 value，并将 value 保存下来，当客户端执行完代码释放锁时，需要先获取 Redis value 并与本地存储的 value 进行比较，只有两者一致时才会执行<code>DEL</code>操作释放锁：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">if redis.call(&#34;GET&#34;, KEYS[1]) == ARGV[1] then
</span></span><span class="line"><span class="cl">    return redis.call(&#34;DEL&#34;, KEYS[1])
</span></span><span class="line"><span class="cl">else
</span></span><span class="line"><span class="cl">    return 0
</span></span><span class="line"><span class="cl">end
</span></span></code></pre></td></tr></table>
</div>
</div><p>random value 是为了保证某个客户端持有的锁不会被其它客户端错误地释放掉，试想一种场景：客户端 A 拿到了 key1 的锁，但被某个耗时操作阻塞了很长时间，达到超时时间后 Redis 自动释放了这个锁；随后客户端 B 拿到了 key1 的锁，这时客户端 A 操作完成，尝试删除已经被客户端 B 持有的 key1 锁。使用上面的原子操作脚本可以保证每个客户端用一个随机字符串作为「签名」，保证每个锁只能被获得锁的客户端删除释放。</p>
<p>得益于 Redis 基于内存存储数据与优秀的程序设计，单机 Redis 能够支撑 10w+ QPS 的请求量，可以满足大多数场景。而单机锁的问题在于一是无法保障容错性，如果 Redis 发生单点故障，那么所有需要获取分布式锁的服务将全部阻塞，二是即使 Redis 采用主从复制架构，主节点崩溃时还未将最新的数据复制到从节点，使得从节点接替主节点时部分数据丢失，违反了锁的排他性。</p>
<h4 id="redlock-分布式锁">Redlock 分布式锁</h4>
<p>Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁，可以看作是单机锁实现的一种扩展。它基于 N 个完全独立的 Redis Master 节点实现，这些主节点间不会复制数据或使用任何隐含的分布式协调算法。一个客户端要获得锁，需要执行以下步骤：</p>
<ol>
<li>客户端获取当前时间，单位是毫秒；</li>
<li>客户端用相同的 key 和 random_value 顺序地在 N 个节点上请求锁。在这一步中，客户端在每个 Master 上请求锁时，会有一个比总的锁 TTL 时长小的多的超时时间，例如如果锁自动释放时间是 10s，那每个节点锁请求的超时时间可能是 5~50ms 的范围。超时时间可以防止客户端在某个宕掉的 Master 节点上阻塞过长时间，如果一个 Master 节点不可用了，客户端会尽快尝试下一个 Master 节点；</li>
<li>客户端计算第二步中获取锁所花的时间，如果客户端在超过 N/2 +1 个 Master 节点上成功获取了锁，并且总消耗的时间不超过 TTL，那么这个锁就认为是获取成功了；</li>
<li>如果锁获取成功，那么锁的真正 TTL 为原有的 TTL - 总消耗时间；</li>
<li>如果锁获取失败，不管是因为获取成功的锁不超过一半（N/2+1)还是因为总消耗时间超过了锁释放时间，客户端都会向每个 Master 节点释放锁，包括那些没有获取锁。</li>
</ol>
<p></p>
<p>Redlock 获取锁失败后，会在随机延时后不断进行重试，直至最大次数，采用随机延时是为了避免不同客户端同时重试，导致谁都无法拿到锁的情况出现。</p>
<p>虽然 Redlock 采用过半写入策略来保障锁的互斥性，但是严重依赖于客户端反复请求锁服务。如果我们的节点没有开启数据持久化，假设一共有 5 个 Redis 节点：A、B、C、D、E，发生了如下的事件序列：</p>
<ol>
<li>Client1 成功锁住了 A、B、C，获取锁成功，但 D 和 E 没有锁住；</li>
<li>节点 C 崩溃宕机，Client1 在 C 上加的锁丢失；</li>
<li>节点 C 重启后，Client2 锁住了C、D、E，获取锁成功。</li>
</ol>
<p></p>
<p>这样，Client1 和 Client2 在同一时刻都获得了锁，为了解决这个问题，Redis 的作者 antirez 提供了两个解决方案：</p>
<ul>
<li>开启 AOF 持久化：因为 Redis key 的过期机制是基于时间戳的，在节点宕机期间时间依旧在流逝，重启之后锁状态不会受到污染。但是 AOF 数据刷回磁盘默认是每秒写一次磁盘，可能部分数据还未刷写到磁盘上数据就已经丢失了，因此需要我们配置策略为 fsnyc = always，但这会降低 Redis 的性能。</li>
<li>解决这个问题的另一个方法是，为 Redis 锁服务规定一个 Max TTL，当一个节点重启之后，这个节点在 Max TTL 期间是不可用的，这样它就不会干扰原本已经申请到的锁，等到它 crash 前的历史锁都过期了，这个节点才会重新加入集群。这个方案的缺点在于如果 Max TTL 设置得过长，那么会导致重启的节点在数个小时内不可用的，即使这个节点是正常的。</li>
</ul>
<h4 id="争论">争论</h4>
<p>Redlock 是一个没有使用共识算法、基于时间实现的分布式锁，这也让很多人怀疑它的可靠性。《DDIA》的作者 Martin Kleppmann 在 2016 年就发表了一篇文章*<a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener noreferrer">How to do distributed locking</a>* ，从以下两个方面批判了 Redlock 的安全性：</p>
<ul>
<li>Redlock 构建在一个基于时间戳的系统模型之上，多台服务器难以保证时间一致，这使得锁的实际失效时间是不同的；</li>
<li>带有自动过期功能的分布式锁，必须提供某种 fencing token（唯一性约束）机制，例如单调递增的 ID，来保证对共享资源的互斥性，而 Redlock 没有提供这样一种机制。</li>
</ul>
<blockquote>
<p>关于第二点中提到的  fencing token 机制，我们将在下文 Chubby 一节中详细论述。</p>
</blockquote>
<p>随后 antirez 在*<a href="http://antirez.com/news/101" target="_blank" rel="noopener noreferrer">Is Redlock safe?</a>* 一文中也进行了回应。这里先讨论第一种情况：当时钟发生跳跃时，当前服务器的时间会突然变大或变小，这都会影响锁的过期时间。例如 Client1 成功锁住了 A、B、C，但是节点 C 的时间突然向前跳跃了 5 秒钟提前失效，这时 Client2 成功锁住了 C、D、E，这种情况下就违反了锁的互斥性。</p>
<p></p>
<p>antirez 认为第一种情况可以通过合理的运维手段来避免：将一次时钟同步过程中大范围的时钟跳跃改为多次小范围时钟跳跃，并尽可能地保证服务器间的时间差保持在较低的范围内。其实从 antirez 的回应可以看出 redlock 是无法解决服务器间时钟不同步问题的。</p>
<p>对于第二点，antirez 认为锁 ID 的大小顺序与操作真正执行的顺序是无关的，只需要保障互斥访问就即可。因此，锁的 ID 是递增的，还是一个随机字符串，自然也就不那么重要了。Redlock 虽然无法提供递增的 fencing token，但利用 Redlock 产生的 random value 可以达到同样的效果。这个随机字符串虽然不是递增的，但却是唯一的。所以 Redlock 是可以保证锁的唯一性约束的。</p>
<h4 id="小结">小结</h4>
<p>综上所述，Redlock 是一种自旋式分布式锁实现，是基于异步复制的分布式系统，需要客户端反复请求锁服务来判断能否获取锁。</p>
<p>Redlock 通过 TTL 的机制承担细粒度的锁服务，适用于对时间很敏感，期望设置一个较短有效期，并且丢锁对业务影响相对可控的服务。</p>
<h2 id="etcd-分布式锁">etcd 分布式锁</h2>
<p>etcd 是一个基于 Raft 共识算法实现的分布式键值存储服务，并提供了分布式锁的功能。一个 etcd 集群包含若干个服务器节点，并通过『领导选举机制』选举出一个 Leader，所有的写请求都会被转发给 Leader，由它全权管理日志复制来实现一致性。其他的节点其实都是当前节点的副本，它们只是维护一个数据的拷贝并会在主节点更新时对它们持有的数据库进行更新，并只响应客户端的读请求。</p>
<p>基于共识算法的分布式系统，会内置一些措施来防止脑裂和过期的数据副本，进而实现线性化的数据存储，<strong>即整个集群表现得好像只有一个数据副本，且其上的所有操作都是原子的</strong>。客户端无论将请求发送到哪一个节点，最后都能得到相同的结果。</p>
<blockquote>
<p>有关 etcd 的实现原理可参考文章*<a href="https://wingsxdu.com/post/database/etcd/" target="_blank" rel="noopener noreferrer">分布式键值存储 etcd 原理与实现</a>*，本节主要讨论基于 etcd 的分布式锁实现。</p>
</blockquote>
<h4 id="etcd-锁的使用">etcd 锁的使用</h4>
<p>etcd 可以为存储的键值对设置租约，当租约到期，键值对将失效删除。同时也支持续租，客户端可以在租约到期之前续约， 当一个客户端持有锁期间，其它客户端只能等待，为避免等待期间租约失效， 客户端需创建一个定时任务 KeepAlive 作为「心跳」不断进行续约，以避免处理还未完成而锁已经过期失效。</p>
<p>如果客户端在持有锁期间崩溃，心跳停止，key 将因租约到期而被删除，从而释放锁，避免死锁。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewSession</span><span class="p">(</span><span class="nx">client</span> <span class="o">*</span><span class="nx">v3</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">opts</span> <span class="o">...</span><span class="nx">SessionOption</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Session</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">ops</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">sessionOptions</span><span class="p">{</span><span class="nx">ttl</span><span class="p">:</span> <span class="nx">defaultSessionTTL</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">:</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Ctx</span><span class="p">()}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">opt</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">opts</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opt</span><span class="p">(</span><span class="nx">ops</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">id</span> <span class="o">:=</span> <span class="nx">ops</span><span class="p">.</span><span class="nx">leaseID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">ops</span><span class="p">.</span><span class="nx">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">keepAlive</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">KeepAlive</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">donec</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{})</span>
</span></span><span class="line"><span class="cl">    <span class="nx">s</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Session</span><span class="p">{</span><span class="nx">client</span><span class="p">:</span> <span class="nx">client</span><span class="p">,</span> <span class="nx">opts</span><span class="p">:</span> <span class="nx">ops</span><span class="p">,</span> <span class="nx">id</span><span class="p">:</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">cancel</span><span class="p">:</span> <span class="nx">cancel</span><span class="p">,</span> <span class="nx">donec</span><span class="p">:</span> <span class="nx">donec</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// keep the lease alive until client error or cancelled context
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">donec</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="k">range</span> <span class="nx">keepAlive</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// eat messages until keep alive channel closes
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>为了更方便理解 etcd 锁的使用，下面贴出了一个简单的示例程序，go1 与 go2 协程抢占同一个锁，即使 go1 协程只设置了 2s 的 TTL 而 5s 后才能释放锁，客户端也可以自动续约保证锁的独占：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">c</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">client</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">clientv3</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">clientv3</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Endpoints</span><span class="p">:</span>   <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;9.135.90.44:2379&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nx">DialTimeout</span><span class="p">:</span> <span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">defer</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">lockKey</span> <span class="o">:=</span> <span class="s">&#34;/test_lock&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">session</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewSession</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">WithTTL</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">m</span> <span class="o">:=</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewMutex</span><span class="p">(</span><span class="nx">session</span><span class="p">,</span> <span class="nx">lockKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Lock</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">TODO</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="s">&#34;go1 get mutex failed &#34;</span> <span class="o">+</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;go1 get mutex key: %s\n&#34;</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Key</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">m</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">TODO</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;go1 release lock\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">session</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewSession</span><span class="p">(</span><span class="nx">client</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">m</span> <span class="o">:=</span> <span class="nx">concurrency</span><span class="p">.</span><span class="nf">NewMutex</span><span class="p">(</span><span class="nx">session</span><span class="p">,</span> <span class="nx">lockKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Print</span><span class="p">(</span><span class="s">&#34;go2 try to get mutex&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Lock</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">TODO</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="s">&#34;go2 get mutex failed &#34;</span> <span class="o">+</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;go2 get mutex key: %s\n&#34;</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Key</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">m</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">TODO</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;go2 release lock\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">c</span> <span class="o">&lt;-</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="p">}()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">os</span><span class="p">.</span><span class="nf">Exit</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">c</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="etcd-锁机制">etcd 锁机制</h4>
<p>除了上文提到的租约机制，etcd 的还提供了以下三个特性来保障分布式锁的安全性。</p>
<h5 id="统一前缀">统一前缀</h5>
<p>在上面的示例程序中，两个协程争抢一个名为<code>/test_lock</code>的锁，而 etcd 实际写入的 key 分别为 key1<code>/test_lock/LeaseID1</code>和 key2<code>/test_lock/LeaseID2</code>。其中，LeaseID 是一个经由 raft 协议广播生成的全局 UUID，确保两个 key 的唯一性。</p>
<p>统一前缀与 Redlock 中客户端生成的随机 value 的作用是一致的，保证锁不会被其它客户端错误地删除。</p>
<h5 id="revision">Revision</h5>
<p>etcd 为每个 key 生成一个 64 位的 Revision 版本号，每进行一次数据的写操作就加一，因此 Revision 是全局唯一且递增的， 通过 Revision 的大小就可以知道 etcd Server 处理写操作的顺序。</p>
<p></p>
<p>在上面的程序示例中，这两个 key 都会写入成功，但他们的 Revision 信息是不同的。客户端需要通过区间查询获取所有前缀为<code>/test_lock</code>的 key 的版本号，通过 Revision 大小可判断自己是否获得锁。</p>
<p>在实现分布式锁时，如果出现多个客户端同时抢锁，那么根据 Revision 号大小可以依次获得锁，避免「惊群效应」，实现公平锁。</p>
<h5 id="watch">Watch</h5>
<p>Watch 机制支持 Watch 某个固定的 key，也支持 Watch 一个区间范围。当被 Watch 的 key 发生变化，客户端将收到通知。</p>
<p>在实现分布式锁时，如果抢锁失败，可通过区间查询返回的 Key-Value 列表获得 Revision 相差最小的 pre-key， 并对它进行监听，当 watch 到 pre-key 的 DELETE 事件， 说明 pre-key 已经释放，此时才能持有锁。</p>
<p></p>
<h4 id="etcd-锁实现">etcd 锁实现</h4>
<p>了解了上述的四个机制的概念后，再来看 etcd 加锁解锁的过程就很简单了：</p>
<ol>
<li>组装需要持有的锁名称和 LeaseID 为真正写入 etcd 的 key；</li>
<li>执行 put 操作，将创建的 key 绑定租约写入 etcd，客户端需记录 Revision 以便下一步判断自己是否获得锁；</li>
<li>通过前缀查询键值对列表，如果自己的 Revision 为当前列表中最小的则认为获得锁；否则监听列表中前一个 Revision 比自己小的 key 的删除事件，一旦监听到 pre-key 则自己获得锁；</li>
<li>完成业务流程后，删除对应的 key 释放锁。</li>
</ol>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 代码已删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">Mutex</span><span class="p">)</span> <span class="nf">Lock</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span> <span class="p">=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s%x&#34;</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">pfx</span><span class="p">,</span> <span class="nx">s</span><span class="p">.</span><span class="nf">Lease</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nx">cmp</span> <span class="o">:=</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">Compare</span><span class="p">(</span><span class="nx">v3</span><span class="p">.</span><span class="nf">CreateRevision</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span><span class="p">),</span> <span class="s">&#34;=&#34;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// put self in lock waiters via myKey; oldest waiter holds lock
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">put</span> <span class="o">:=</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">OpPut</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span><span class="p">,</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">WithLease</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nf">Lease</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// reuse key in case this session already holds the lock
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">get</span> <span class="o">:=</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">OpGet</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// fetch current holder to complete uncontended path with only one RPC
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">getOwner</span> <span class="o">:=</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">OpGet</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">pfx</span><span class="p">,</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">WithFirstCreate</span><span class="p">()</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Txn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">).</span><span class="nf">If</span><span class="p">(</span><span class="nx">cmp</span><span class="p">).</span><span class="nf">Then</span><span class="p">(</span><span class="nx">put</span><span class="p">,</span> <span class="nx">getOwner</span><span class="p">).</span><span class="nf">Else</span><span class="p">(</span><span class="nx">get</span><span class="p">,</span> <span class="nx">getOwner</span><span class="p">).</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">myRev</span> <span class="p">=</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nx">Revision</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// wait for deletion revisions prior to myKey
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">hdr</span><span class="p">,</span> <span class="nx">werr</span> <span class="o">:=</span> <span class="nf">waitDeletes</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">client</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">pfx</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">myRev</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// release lock key if wait failed
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="nx">werr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">m</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">(</span><span class="nx">client</span><span class="p">.</span><span class="nf">Ctx</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">werr</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">Mutex</span><span class="p">)</span> <span class="nf">Unlock</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">client</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">s</span><span class="p">.</span><span class="nf">Client</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">myKey</span> <span class="p">=</span> <span class="s">&#34;\x00&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">myRev</span> <span class="p">=</span> <span class="o">-</span><span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>可以看到，加锁时传入了一个 Context，这使得加锁过程中如果出现整体请求超时或者上层逻辑主动退出，那么 etcd 也会主动释放锁，减少锁的空占期。</p>
<blockquote>
<p>Lock locks the mutex with a cancelable context. If the context is canceled while trying to acquire the lock, the mutex tries to clean its stale lock entry.</p>
</blockquote>
<p>同样地，虽然 lock 时只 watch Revision 相差最小的 pre-key，但是如果 pre-key 的客户端主动释放了锁，而其它的客户端依然在持有锁，这也破坏了锁的排他性性。因此 waitDeletes() 函数在监听到 pre-key 的删除事件后，仍然会去访问 etcd 来判断前面是否还有其他客户端仍持有锁，并监听他们的删除事件。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">waitDeletes</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">client</span> <span class="o">*</span><span class="nx">v3</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span> <span class="nx">pfx</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">maxCreateRev</span> <span class="kt">int64</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">pb</span><span class="p">.</span><span class="nx">ResponseHeader</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">getOpts</span> <span class="o">:=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">v3</span><span class="p">.</span><span class="nf">WithLastCreate</span><span class="p">(),</span> <span class="nx">v3</span><span class="p">.</span><span class="nf">WithMaxCreateRev</span><span class="p">(</span><span class="nx">maxCreateRev</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">pfx</span><span class="p">,</span> <span class="nx">getOpts</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Kvs</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">lastKey</span> <span class="o">:=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">Kvs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="p">=</span> <span class="nf">waitDelete</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">client</span><span class="p">,</span> <span class="nx">lastKey</span><span class="p">,</span> <span class="nx">resp</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nx">Revision</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="小结-1">小结</h4>
<p>基于 ZooKeeper 或 etcd 实现的分布式锁属于监听类分布式锁，这类实现客户端只需 watch 某个 key，当锁可用时锁服务端会通知客户端，更复杂的共识逻辑由服务端来完成，无需客户端不停请求锁服务。</p>
<p>对比 redlock 与 etcd 锁的实现，可以看出 redis 本身并不具有共识过程，更多地是依赖于客户端不断地轮训请求，也无法解决不同机器间时钟同步与时钟跳跃问题。而 etcd 使用全局唯一且递增的 Revision 来排列获取锁的顺序，并利用共识算法来保障节点间的数据一致性，解决了 Redlock 不同节点间 key 失效时间不同的问题 。</p>
<p>分布式锁是 etcd 较为复杂的应用场景，其缺点在于 Grant（生成租约 ID）、Unlock、Expire 等操作都要经过一次 raft 协议共识，Lock 过程中可能要进行多次查询操作，成本较高，并且同时 watch 多个 key 也会影响集群的性能。在特定的测试环境下*<a href="https://etcd.io/docs/v3.5/op-guide/performance/" target="_blank" rel="noopener noreferrer">Performance | etcd</a>*，etcd 可以处理每秒 50,000 的写操作请求量，但是对于复杂的分布式锁实现，通常无法支撑 QPS 超过一万的应用场景。</p>
<p>综合来看，etcd 实现的等监听锁的安全性较高，但性能并不出众，这类锁往往通过租约/会话机制承担粗粒度的锁服务，适用于对安全性很敏感，希望长期持有锁，不期望发生丢锁现象的服务。</p>
<h2 id="chubby">Chubby</h2>
<p>Chubby 是 Google 设计的提供粗粒度锁的分布式锁服务，GFS 和 Bigtable 都使用了 Chubby 以解决主节点的选举等问题。由于 Chubby 是谷歌内部的服务，我们只能从这篇 2006 发表的论文*<a href="https://ai.google/research/pubs/pub27897" target="_blank" rel="noopener noreferrer">The Chubby lock service for loosely-coupled distributed systems</a>*来窥探它的设计思路。</p>
<p>与 etcd 和 zookeeper 类似，Chubby 使用 Paxos 算法来保证数据一致性。在一个 Chubby 集群中，只有主节点会对外提供读写服务，客户端通过向副本发送请求获取主节点的位置，一旦它获取到了主节点的位置，就会向所有的读写请求发送给主节点，直到其不再响应为止。写请求都会通过一致性协议传播到所有的副本中，当集群中的多数节点都同步了请求时就会认为当前的写入已经被确认。</p>
<h4 id="程序停滞">程序停滞</h4>
<p>不同之处在于，Chubby 在他们之上做了更进一步的锁可靠性保证。</p>
<p>无论我们的程序是由那种编程语言编写的，都有可能由于 GC、系统调度、网络延时等原因，产生一个较长时间的程序停滞，造成已经持有的分布式锁的超时、自动释放，随后，该锁会被其他实例获取，再次进入临界区，使得锁的排他性的被破坏。为此 Martin 还给出了一个由客户端 GC pause 引发 Redlock 失效的例子：</p>
<ol>
<li>Client1 向 Redis 集群发起锁请求；</li>
<li>各个 Redis 节点已经把获得锁的结果返回给了 Client1，但 Client1 在收到请求结果前进入了长时间的 GC pause；</li>
<li>在所有的 Redis 节点上，锁过期了；</li>
<li>Client2 在 获取到了锁；</li>
<li>Client1 从 GC pause 中恢复，收到了第 2 步来自各个 Redis 节点的请求结果。Client1 认为自己成功获取到了锁；</li>
<li>Client1 和 Client2 同时持有了同一个锁。</li>
</ol>
<p>Martin 的例子不仅仅适用于 Redis，基于 zookeeper、etcd 等实现的分布式锁都会出现该问题。并且后者通过心跳消息来保证锁的有效性，如果由于网络延迟或 GC 导致心跳消息未能及时送达 etcd server，也会使得锁提前失效，导致多个客户端同时持有锁。</p>
<h4 id="sequencer">sequencer</h4>
<p>针对该场景，为了保证锁最终可以被调度，Chubby 给出的用于缓解这一问题的机制称为 sequencer。锁的持有者可以随时请求一个由三部分组成的字节串 sequencer：</p>
<ul>
<li>锁的名称；</li>
<li>锁的获取模式：排他锁或共享锁；</li>
<li>lock generation number，一个 64 位的单调递增数字，相当于唯一标识 ID；</li>
</ul>
<p>客户端拿到 sequencer 之后，在操作资源的时候把它传给资源服务器。然后，资源服务器负责对sequencer的有效性进行检查。检查可以有两种方式：</p>
<ol>
<li>调用 Chubby 提供的 API<code>CheckSequencer()</code>，将 sequencer 传入 Chubby 进行有效性检查，保证客户端持有的锁在进行资源访问时仍然有效；</li>
<li>将客户端传来的 sequencer 与资源服务器当前观察到的最新的 sequencer 进行大小比较，如果 lock generation number 较小则拒绝其对资源进行操作。</li>
</ol>
<p>其中第二种方式与 Martin 描述的 fencing token 唯一性约束类似，人为地为客户端操作的顺序进行排序，并按照顺序获取锁。即使由于各种原因锁的排他性被破坏，如果版本号为 34 的客户端已经更新了资源，那么版本号比他小的任何操作都是不合法的。</p>
<p></p>
<p>Chubby 上述两种方案的缺点是对于被请求的资源系统有一定的侵入性，如果资源服务本身不容易修改，Chubby 还提供了 lock-delay 机制：Chubby 允许客户端为持有的锁指定一个 lock-delay 的时间值，当 Chubby 发现客户端被动失去联系的时候，并不会立即释放锁，而是会在 lock-delay 指定的时间内阻止其它客户端获得这个锁。</p>
<p>lock-delay 机制是为了在把锁分配给新的客户端之前，让之前持有锁的客户端有充分的时间完成对资源的操作。</p>
<h4 id="小结-2">小结</h4>
<p>为了应对锁失效问题，Chubby 提供的三种处理方式：CheckSequencer() 校验、与上次处理时最新的 sequencer 对比、lock-delay 机制，这就允许资源服务器在需要的时候，利用它提供更强的安全性保障。</p>
<p>Chubby 的缺点也很明显，前两种方案需要资源服务器为其定制校验锁是否仍然有效的功能，除非系统要求及其高的互斥性，否则这样的改造对于很多系统来说是不必要的。</p>
<h2 id="总结">总结</h2>
<p>本篇文章首先讨论了分布式锁必须具有的几个特性，随后介绍了 Redlock 与 etcd 分布式锁的具体实现，最后讨论了 Google Chuby 的由于程序停滞导致锁的排他性失效情况下的解决方案，并引用了 Martin 与 antirez 对于分布式锁的讨论。</p>
<p>目前为止，分布式锁并没有一个能够完全保证安全性的解决方案，即使是 Chubby 也需要第三方服务来二次校验锁的有效性。在 Martin 批判 Redlock 的那篇文章中，也提出了一个很有见地的观点，将锁的用途分为两种：</p>
<ul>
<li>为了效率：协调各个客户端避免做重复的工作，即使锁偶尔失效了，只是把某些操作多做一遍而已，不会产生其它的不良后果，例如重复发送了一封相同的邮件；如果是为了效率而使用分布式锁，允许锁的偶尔失效，那么使用 Redis 单机锁就足够了，简单而且效率高，Redlock 则是个过重的实现（Redlock 还可以提高锁服务的可用性，Redis 单机锁无法避免单点故障 ）；</li>
<li>为了正确性：与常见的内存中的锁类似，在任何情况下都不允许锁失效的情况发生，因为一旦发生，就可能意味着数据不一致，数据丢失、文件损坏、或是其它严重的问题；如果是为了正确性，在很严格的场合使用分布式锁，那么不要使用 Redlock，它不是一个能够在异步系统中严格保证数据一致性的算法，应该考虑类似 Zookeeper/etcd 的方案。</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener noreferrer">How to do distributed locking</a></li>
<li><a href="https://redis.io/topics/distlock" target="_blank" rel="noopener noreferrer">Distributed locks with Redis</a></li>
<li><a href="http://antirez.com/news/101" target="_blank" rel="noopener noreferrer">Is Redlock safe?</a></li>
<li><a href="https://www.youtube.com/watch?v=VnbC5RG1fEo" target="_blank" rel="noopener noreferrer">Distributed Lock Manager</a></li>
<li><a href="https://zhangtielei.com/posts/blog-redlock-reasoning.html" target="_blank" rel="noopener noreferrer">基于Redis的分布式锁到底安全吗（上）</a></li>
<li><a href="http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html" target="_blank" rel="noopener noreferrer">基于Redis的分布式锁到底安全吗（下）</a></li>
<li><a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/chubby-osdi06.pdf" target="_blank" rel="noopener noreferrer">The Chubby lock service for loosely-coupled distributed systems</a></li>
<li><a href="https://tech.youzan.com/bond/" target="_blank" rel="noopener noreferrer">有赞 Bond 分布式锁</a></li>
</ul>
]]></description>
</item><item>
    <title>Radix Tree 与 Gin 实现</title>
    <link>https://wingsxdu.com/posts/data-structure/radix-tree/</link>
    <pubDate>Fri, 26 Nov 2021 22:00:07 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/data-structure/radix-tree/</guid>
    <description><![CDATA[<blockquote>
<p>简单介绍 Radix Tree 的原理与实现。</p>
</blockquote>
<h2 id="概览">概览</h2>
<h4 id="trie">Trie</h4>
<p>Trie ，又叫字典树、前缀树（Prefix Tree）是一种多叉树结构，其核心思想是空间换时间，利用字符串的公共前缀来减少无谓的字符串比较以达到提高查询效率的目的。如下图：</p>
<p></p>
<p>可以看出，两个有公共前缀的关键字，在 Trie 中前缀部分的路径相同，所以Trie树又叫做前缀树。</p>
<p>Trie 的关键字<code>word</code>一般是字符串，而且 Trie 把每个关键字保存在一条路径上，而不是一个节点中。所以通常在实现时，会在节点中设置一个标志，用来标记该节点处是否构成一个关键字。Trie 具有以下特性：</p>
<ol>
<li>为了实现简单，根节点不包含字符，除根节点外的每一个子节点都只包含一个字符；</li>
<li>从根节点到某一个节点，路径上经过的字符连接起来，为该节点对应的字符串；</li>
<li>每个节点的所有子节点包含的字符互不相同。</li>
</ol>
<p>Trie 的实现十分简单，其主要问题是树的高度依赖于存储的字符串的长度，查询与写入的时间复杂度为<em>O(m)</em>，m 为字符串的长度，因此搜索耗时会比较高。</p>
<h4 id="radix-tree">Radix Tree</h4>
<p>Radix Tree，也被称为压缩前缀树（Compact Prefix Tree）是一种空间优化的 Trie 数据结构。 如果树中某个节点是父节点的唯一子节点，那么该子节点将会与父节点进行合并，所以 Radix Tree 的节点可以包含一个或者多个元素。例如我们有<code>/App1/Config</code>和<code>/App/State</code>两个关键字，那么它们的存储结构可能是下面这样的：</p>
<p></p>
<p>总体来看，Radix Tree ，常被用于 IP 路由、字符串匹配等具有较多相同前缀且字符串长度有限的场景，下面将介绍 Gin Web 框架的路由实现。</p>
<h2 id="gin-路由实现">Gin 路由实现</h2>
<p>Gin 框架为每种 HTTP 方法都维护了一个单独的 Radix Tree，所以不同方法的路由空间是隔离的。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">methodTree</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">method</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">	<span class="nx">root</span>   <span class="o">*</span><span class="nx">node</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Radix Tree 的每一个节点不仅保存了当前节点的字符串，也保存了一个路由的完整路径。除此之外还对查询过程进行了优化，<code>indices</code>字段保存了子节点的首个字符，以快速判断当前路径在哪个子节点中。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">node</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">path</span>      <span class="kt">string</span>        <span class="c1">// 该节点对应的 path
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">indices</span>   <span class="kt">string</span>        <span class="c1">// 子节点 path 的第一个字符的集合
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">wildChild</span> <span class="kt">bool</span>          <span class="c1">// 子节点是否包含通配符节点
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">nType</span>     <span class="nx">nodeType</span>      <span class="c1">// 当前节点类型
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">priority</span>  <span class="kt">uint32</span>        <span class="c1">// 优先级，子节点、子子节点等注册的handler数量
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">children</span>  <span class="p">[]</span><span class="o">*</span><span class="nx">node</span>       <span class="c1">// 子节点集合，每个 children 中只能包含一个参数节点，并且在集合的最后一位
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">handlers</span>  <span class="nx">HandlersChain</span> <span class="c1">// 处理函数链
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="nx">fullPath</span>  <span class="kt">string</span>        <span class="c1">// 完整路径
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="路由注册">路由注册</h4>
<p>Radix Tree 写入数据的逻辑也不复杂，如果公共前缀长度小于当前节点保存的字符串长度，那么会分裂当前节点，否则递归进入子节点进行操作：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 代码有删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">addRoute</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">handlers</span> <span class="nx">HandlersChain</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">fullPath</span> <span class="o">:=</span> <span class="nx">path</span>
</span></span><span class="line"><span class="cl">	<span class="nx">n</span><span class="p">.</span><span class="nx">priority</span><span class="o">++</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="c1">// 空节点直接插入
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">path</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">n</span><span class="p">.</span><span class="nf">insertChild</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">fullPath</span><span class="p">,</span> <span class="nx">handlers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="nx">n</span><span class="p">.</span><span class="nx">nType</span> <span class="p">=</span> <span class="nx">root</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">parentFullPathIndex</span> <span class="o">:=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">walk</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">	<span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="c1">// 找到最长的公共前缀
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>		<span class="nx">i</span> <span class="o">:=</span> <span class="nf">longestCommonPrefix</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">n</span><span class="p">.</span><span class="nx">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="c1">// 如果公共前缀长度小于当前节点保存的字符串长度，分裂当前节点
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>		<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="c1">// 插入子节点
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>		<span class="k">if</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nx">path</span> <span class="p">=</span> <span class="nx">path</span><span class="p">[</span><span class="nx">i</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">			<span class="nx">c</span> <span class="o">:=</span> <span class="nx">path</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">      
</span></span><span class="line"><span class="cl">			<span class="nx">n</span><span class="p">.</span><span class="nf">insertChild</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">fullPath</span><span class="p">,</span> <span class="nx">handlers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="k">return</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="k">return</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="路由匹配">路由匹配</h4>
<p>Gin 通过<code>node.getValue()</code>方法查询路由，在这个超长的函数中，其核心逻辑是是下面的广度优先遍历：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">walk</span><span class="p">:</span> <span class="c1">// Outer loop for walking the tree
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>	<span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">prefix</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">path</span>
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">&gt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">prefix</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="k">if</span> <span class="nx">path</span><span class="p">[:</span><span class="nb">len</span><span class="p">(</span><span class="nx">prefix</span><span class="p">)]</span> <span class="o">==</span> <span class="nx">prefix</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">				<span class="nx">path</span> <span class="p">=</span> <span class="nx">path</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="nx">prefix</span><span class="p">):]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">				<span class="c1">// Try all the non-wildcard children first by matching the indices
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>				<span class="nx">idxc</span> <span class="o">:=</span> <span class="nx">path</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">				<span class="k">for</span> <span class="nx">i</span><span class="p">,</span> <span class="nx">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">indices</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">					<span class="k">if</span> <span class="nx">c</span> <span class="o">==</span> <span class="nx">idxc</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">						<span class="nx">n</span> <span class="p">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">						<span class="k">continue</span> <span class="nx">walk</span>
</span></span><span class="line"><span class="cl">					<span class="p">}</span>
</span></span><span class="line"><span class="cl">				<span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div>]]></description>
</item><item>
    <title>Google B-Tree 实现</title>
    <link>https://wingsxdu.com/posts/data-structure/btree/</link>
    <pubDate>Sat, 20 Nov 2021 21:50:48 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/data-structure/btree/</guid>
    <description><![CDATA[<blockquote>
<p>B-Tree 及其变种数据结构被广泛用于存储系统、数据库系统中，主要作为索引使用，适用于动态随机访问数据的场景。<em><a href="https://github.com/google/btree" target="_blank" rel="noopener noreferrer">google/btree (github.com)</a></em>  是一个使用 Go 编写的纯内存 B-Tree 实现，本文对它的源码进行了分析。</p>
</blockquote>
<h2 id="概览">概览</h2>
<h4 id="b-tree-结构">B-Tree 结构</h4>
<p><em><a href="https://dl.acm.org/doi/10.1145/1734663.1734671" target="_blank" rel="noopener noreferrer">Organization and maintenance of large ordered indices</a></em> 这篇论文提出了 B-Tree 数据结构，B-Tree 的查询从根结点开始，对节点内的有序数据进行二分查找，如果命中则结束查询，否则进入子节点查询，直至叶子结点。B-Tree 查询的特点是搜索有可能在非叶子节点结束。作为一个典型的树形结构，其包含的节点类型有：</p>
<ul>
<li>根节点（Root Node）：一个 B-Tree 只有一个根节点，位于树的最顶端；</li>
<li>分支节点（Branch Node）：包含数据项和指向子节点的指针；</li>
<li>叶子节点（Leaf Node）：只存储数据项。</li>
</ul>
<p></p>
<p>B-Tree 的结构如上图所示，一个非空、高度为 h、最小 degree 为 k 的 B-Tree 都有以下属性：</p>
<ol>
<li>从 root 到叶子节点的长度均为 h；</li>
<li>root 和叶子节点可以含有 [1, 2k] 条数据，且root 和分支节点的子节点数量为该节点的数据数 +1；</li>
<li>除 root 和叶子节点外，其它节点都至少有 k 条数据，最多有 2k 条数据。</li>
<li>除 root 和叶子节点外，其它节点至少有 k+1 个子节点，最多有 2k+1 个子节点。</li>
</ol>
<h4 id="api">API</h4>
<p>btree 提供了基础的 CRUD API，注释中详细描述了这些接口的功能，也能 Google 到许多文章介绍 btree 的使用。因此仅列出几个关键的 API 以方便对下文的理解：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">Get</span><span class="p">(</span><span class="nx">key</span> <span class="nx">Item</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">ReplaceOrInsert</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">Delete</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">Has</span><span class="p">(</span><span class="nx">key</span> <span class="nx">Item</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">Clear</span><span class="p">(</span><span class="nx">addNodesToFreelist</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">Clone</span><span class="p">()</span> <span class="p">(</span><span class="nx">t2</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="p">{}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>数据插入到 btree 前需要实现 <code>Item</code> interface，而这个接口只包含一个 Less 方法，用来将 btree 内的所有数据以增序排列：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Item</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">Less</span><span class="p">(</span><span class="nx">than</span> <span class="nx">Item</span><span class="p">)</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>通过 interface 传值是对数据的一层抽象，进而实现不同数据类型的兼容性，而不是将数据序列化为 byte slice 进行存储。缺点在于每次读取值时需要进行类型断言与转换，不过通常情况下使用时都会将 API 封装一层，调用方可以不用关注这些内容。</p>
<h4 id="strict-weak-ordering">Strict Weak Ordering</h4>
<p>btree 中一个重要的概念是 Strict Weak Ordering：即如果<code>!(a&lt;b) &amp;&amp; !(b&lt;a)</code>，那么就视为 a 和 b 是相等的。在这里，<strong>相等并不意味着  a 和 b 是同一个对象实体或其值完全一致，而是只要满足表达式<code>!(a&lt;b) &amp;&amp; !(b&lt;a)</code>就可以视为<code>a == b</code></strong>。</p>
<p>例如在下面的代码示例中，<code>foo</code>结构体具有三个内部字段，并且实现了 Less 方法，这个方法可以视为小于号<code>&lt;</code>操作运算符。a 和 b 都是它的两个实体对象，并且结构体没有手动实现<code>==</code>号操作运算符。虽然它们具有不同的变量值与内存地址，但他们满足条件<code>!(a&lt;b) &amp;&amp; !(b&lt;a)</code>，那么就可以视为 a 和 b 是相等的。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">foo</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">key</span>   <span class="kt">int64</span>
</span></span><span class="line"><span class="cl">  <span class="nx">value</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span>  <span class="kt">string</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">i</span> <span class="o">*</span><span class="nx">foo</span><span class="p">)</span> <span class="nf">Less</span><span class="p">(</span><span class="nx">b</span> <span class="o">*</span><span class="nx">foo</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">i</span><span class="p">.</span><span class="nx">key</span> <span class="p">&lt;</span> <span class="nx">b</span><span class="p">.</span><span class="nx">key</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">a</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">foo</span><span class="p">{</span><span class="nx">key</span><span class="p">:</span> <span class="mi">123</span><span class="p">,</span> <span class="nx">value</span><span class="p">:</span> <span class="mi">456</span><span class="p">,</span> <span class="nx">name</span><span class="p">:</span> <span class="s">&#34;test1&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">b</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">foo</span><span class="p">{</span><span class="nx">key</span><span class="p">:</span> <span class="mi">123</span><span class="p">,</span> <span class="nx">value</span><span class="p">:</span> <span class="mi">789</span><span class="p">,</span> <span class="nx">name</span><span class="p">:</span> <span class="s">&#34;test2&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">!</span><span class="nx">a</span><span class="p">.</span><span class="nf">Less</span><span class="p">(</span><span class="nx">b</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">b</span><span class="p">.</span><span class="nf">Less</span><span class="p">(</span><span class="nx">a</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">println</span><span class="p">(</span><span class="s">&#34;a == b&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>具体到 btree 的查询方法实现中，会利用二分查找找出<strong>第一个</strong>满足<code>(item &lt; s[i])</code>条件的值，这也就说明<code>(item &lt; s[i-1])</code>是不成立的（值为 false），这时再进行一次<code>(s[i-1] &lt; item)</code>判断，如果也不成立，那么就可以认为 item 与 s[i-1] 是相等的。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// &#39;found&#39; is true if the item already exists in the list at the given index.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="nx">items</span><span class="p">)</span> <span class="nf">find</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">)</span> <span class="p">(</span><span class="nx">index</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">found</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">i</span> <span class="o">:=</span> <span class="nx">sort</span><span class="p">.</span><span class="nf">Search</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">s</span><span class="p">),</span> <span class="kd">func</span><span class="p">(</span><span class="nx">i</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">item</span><span class="p">.</span><span class="nf">Less</span><span class="p">(</span><span class="nx">s</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">i</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">s</span><span class="p">[</span><span class="nx">i</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="nf">Less</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">i</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="数据结构">数据结构</h2>
<p>btree 的数据结构比较清晰，包含 B-Tree 的最小度数<code>degree</code>、存储的数据条目数量<code>length</code>、与根节点<code>root</code>。每个节点都含有自己的子节点与数据条目：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Write operations are not safe for concurrent mutation by multiple goroutines, but Read operations are.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">BTree</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">degree</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl">  <span class="nx">length</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl">  <span class="nx">root</span>   <span class="o">*</span><span class="nx">node</span>
</span></span><span class="line"><span class="cl">  <span class="nx">cow</span>    <span class="o">*</span><span class="nx">copyOnWriteContext</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">node</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">items</span>    <span class="nx">items</span>
</span></span><span class="line"><span class="cl">  <span class="nx">children</span> <span class="nx">children</span>
</span></span><span class="line"><span class="cl">  <span class="nx">cow</span>      <span class="o">*</span><span class="nx">copyOnWriteContext</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">children</span> <span class="p">[]</span><span class="o">*</span><span class="nx">node</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>btree 并没有对不同类型的节点进行区分，都统一为 node 表示，以复用节点操作的代码。如果该节点是叶子节点，那么它的<code>children</code>字段就是空的。</p>
<h4 id="copyonwritecontext">copyOnWriteContext</h4>
<p>copyOnWriteContext 是 btree 中每个节点都持有的一个结构体，从名字就可以看出，这是一个写时复制有关的内容。由于 btree 是一个纯内存的实现，当我们使用内部提供的<code>Clone()</code>方法对原有的 btree 进行拷贝时，新树与旧树会使用写时复制技术来共享同一片内存空间，进而节约内存开销与数据复制所需的时间。只有要对节点进行写操作时，才会真正地新建一个节点。</p>
<p></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">copyOnWriteContext</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">freelist</span> <span class="o">*</span><span class="nx">FreeList</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">FreeList</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">mu</span>       <span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
</span></span><span class="line"><span class="cl">   <span class="nx">freelist</span> <span class="p">[]</span><span class="o">*</span><span class="nx">node</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>Redis 的 RDB 持久化中也使用到了 COW 技术，Redis 主进程 fork 出子进程进行数据备份，父进程继续对外提供服务。而子父进程只是虚拟空间不同，对应的物理空间是相同的，这与<code>Clone()</code>方法有异曲同工之处。区别在于 btree 的写时复制直接共享了虚拟内存地址，</p>
</blockquote>
<p>从整体来看，copyOnWriteContext 具有两个作用：</p>
<ul>
<li>
<p>标记 btree 是否具有当前节点的写操作权限，若没有，则需要创建一个新的节点并替换当前节点，然后才能进行写操作：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">mutableFor</span><span class="p">(</span><span class="nx">cow</span> <span class="o">*</span><span class="nx">copyOnWriteContext</span><span class="p">)</span> <span class="o">*</span><span class="nx">node</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">n</span><span class="p">.</span><span class="nx">cow</span> <span class="o">==</span> <span class="nx">cow</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">n</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">out</span> <span class="o">:=</span> <span class="nx">cow</span><span class="p">.</span><span class="nf">newNode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// copy node to out code is omitted
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">copyOnWriteContext</span><span class="p">)</span> <span class="nf">newNode</span><span class="p">()</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n</span> <span class="p">=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">freelist</span><span class="p">.</span><span class="nf">newNode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n</span><span class="p">.</span><span class="nx">cow</span> <span class="p">=</span> <span class="nx">c</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>node 的<code>mutableFor</code>方法展示了这一过程，如果当前节点的 cow 与 btree 的 cow 一致，那么直接返回当前节点，否则会新建一个节点，并拷贝当前节点的值至新的节点中。</p>
</li>
<li>
<p>封装了可复用的 node 节点列表<code>freelist</code>，当一个节点被销毁时会被放入回收列表中，新建节点时可直接从中取出复用，减少了申请节点内存的操作频率。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="o">*</span><span class="nx">FreeList</span><span class="p">)</span> <span class="nf">newNode</span><span class="p">()</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">f</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="nx">index</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="nx">f</span><span class="p">.</span><span class="nx">freelist</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">index</span> <span class="p">&lt;</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">f</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">new</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">n</span> <span class="p">=</span> <span class="nx">f</span><span class="p">.</span><span class="nx">freelist</span><span class="p">[</span><span class="nx">index</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">f</span><span class="p">.</span><span class="nx">freelist</span><span class="p">[</span><span class="nx">index</span><span class="p">]</span> <span class="p">=</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">  <span class="nx">f</span><span class="p">.</span><span class="nx">freelist</span> <span class="p">=</span> <span class="nx">f</span><span class="p">.</span><span class="nx">freelist</span><span class="p">[:</span><span class="nx">index</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">f</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
<p>copyOnWriteContext 是 btree 优化内存开销的重要手段，从复用/共享内存这两个角度出发，减少内存申请、销毁的频率，并在大批量数据复制的场景下，使用写时复制共享相同的数据。</p>
<h2 id="对外接口与应用">对外接口与应用</h2>
<h4 id="replaceorinsert">ReplaceOrInsert</h4>
<p>ReplaceOrInsert 会插入一条新数据到 btree 中，如果该条数据已经存在，则会用新值替换旧值。这个接口的实现中除去边界条件处理，本质上是调用内部的<code>insert</code>方法插入数据：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">BTree</span><span class="p">)</span> <span class="nf">ReplaceOrInsert</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">out</span> <span class="o">:=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">root</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span> <span class="nx">t</span><span class="p">.</span><span class="nf">maxItems</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">out</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">t</span><span class="p">.</span><span class="nx">length</span><span class="o">++</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">insert</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">,</span> <span class="nx">maxItems</span> <span class="kt">int</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">i</span><span class="p">,</span> <span class="nx">found</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">found</span> <span class="p">{</span>  <span class="c1">// 当前节点中存在该条数据，更新数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">out</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="p">=</span> <span class="nx">item</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>  <span class="c1">// 当前节点没有子节点，在当前节点中插入新数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">insertAt</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">n</span><span class="p">.</span><span class="nf">maybeSplitChild</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">maxItems</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">inTree</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">switch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="nx">item</span><span class="p">.</span><span class="nf">Less</span><span class="p">(</span><span class="nx">inTree</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// no change, we want first split node
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">case</span> <span class="nx">inTree</span><span class="p">.</span><span class="nf">Less</span><span class="p">(</span><span class="nx">item</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">      <span class="nx">i</span><span class="o">++</span> <span class="c1">// we want second split node
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="nx">out</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="p">=</span> <span class="nx">item</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">n</span><span class="p">.</span><span class="nf">mutableChild</span><span class="p">(</span><span class="nx">i</span><span class="p">).</span><span class="nf">insert</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span> <span class="nx">maxItems</span><span class="p">)</span> <span class="c1">// 递归调用
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>insert</code>方法的执行路径如下：</p>
<ol>
<li>先判断当前节点中是否存在该条数据，如果存在则更新数据；</li>
<li>如果当前节点不存在该条数据，并且当前节点没有子节点，那么直接在当前节点中插入新数据；</li>
<li>如果当前节点不存在该条数据，并且当前节点含有子节点，那么递归调用子节点的<code>insert</code>方法，在 Strict Weak Ordering 一小节中提到过，<code>find</code>方法返回的 i 是<strong>第一个</strong>满足<code>(item &lt; s[i])</code>条件的值，既然已经确定当前节点不存在该条数据，那么它应该被存储在第 i 个子节点中。</li>
</ol>
<h4 id="delete">Delete</h4>
<p>B-Tree 节点删除数据时都需要进行以下判断：</p>
<ul>
<li>节点的 items 数量大于 minItems，直接从中删除指定的 item；</li>
<li>节点的 items 数量小于等于 minItems，此时需要为节点填充 item：
<ol>
<li>左节点含有足够的 items，从左节点偷取一个 item；</li>
<li>右节点含有足够的 items，从右节点偷取一个 item；</li>
<li>与右节点合并。</li>
</ol>
</li>
</ul>
<p>btree 对删除操作进行了一定的简化，在递归调用子节点的<code>remove</code>方法之前，先确保子节点具有足够的 items 以移除节点，然后再调用一次<code>remove</code>方法以真正移除数据。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// 以下代码已经简化
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">remove</span><span class="p">(</span><span class="nx">item</span> <span class="nx">Item</span><span class="p">,</span> <span class="nx">minItems</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">typ</span> <span class="nx">toRemove</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">i</span><span class="p">,</span> <span class="nx">found</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">found</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">removeAt</span><span class="p">(</span><span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// If we get to here, we have children.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">items</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="nx">minItems</span> <span class="p">{</span> <span class="c1">// 子节点需要填充 item
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="nx">n</span><span class="p">.</span><span class="nf">growChildAndRemove</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">item</span><span class="p">,</span> <span class="nx">minItems</span><span class="p">,</span> <span class="nx">typ</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">child</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nf">mutableChild</span><span class="p">(</span><span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">found</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">out</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="p">=</span> <span class="nx">child</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="kc">nil</span><span class="p">,</span> <span class="nx">minItems</span><span class="p">,</span> <span class="nx">removeMax</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">out</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">child</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span> <span class="nx">minItems</span><span class="p">,</span> <span class="nx">typ</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// growChildAndRemove 确保子节点含有足够的 items
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">growChildAndRemove</span><span class="p">(</span><span class="nx">i</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">item</span> <span class="nx">Item</span><span class="p">,</span> <span class="nx">minItems</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">typ</span> <span class="nx">toRemove</span><span class="p">)</span> <span class="nx">Item</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nx">i</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">i</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="nx">items</span><span class="p">)</span> <span class="p">&gt;</span> <span class="nx">minItems</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Steal from left child
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">i</span><span class="o">+</span><span class="mi">1</span><span class="p">].</span><span class="nx">items</span><span class="p">)</span> <span class="p">&gt;</span> <span class="nx">minItems</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// steal from right child
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// merge with right child
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">n</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span> <span class="nx">minItems</span><span class="p">,</span> <span class="nx">typ</span><span class="p">)</span> <span class="c1">// 第二次调用 remove 方法真正移除 item
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="iterate">Iterate</h4>
<p>btree 支持增序/降序范围查询，以增序查询为例，这是一个标准的迭代 BFS 实现，并将迭代到的值以调用方传入的<code>ItemIterator</code>函数进行处理。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span><span class="nx">node</span><span class="p">)</span> <span class="nf">iterate</span><span class="p">(</span><span class="nx">dir</span> <span class="nx">direction</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">stop</span> <span class="nx">Item</span><span class="p">,</span> <span class="nx">includeStart</span> <span class="kt">bool</span><span class="p">,</span> <span class="nx">hit</span> <span class="kt">bool</span><span class="p">,</span> <span class="nx">iter</span> <span class="nx">ItemIterator</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">ok</span><span class="p">,</span> <span class="nx">found</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">index</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl">  <span class="k">switch</span> <span class="nx">dir</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nx">ascend</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">start</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">index</span><span class="p">,</span> <span class="nx">_</span> <span class="p">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="nx">start</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="nx">index</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">hit</span><span class="p">,</span> <span class="nx">ok</span> <span class="p">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nf">iterate</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">stop</span><span class="p">,</span> <span class="nx">includeStart</span><span class="p">,</span> <span class="nx">hit</span><span class="p">,</span> <span class="nx">iter</span><span class="p">);</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="k">return</span> <span class="nx">hit</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// 删除了边界条件处理代码
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="k">if</span> <span class="p">!</span><span class="nf">iter</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">hit</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="nx">hit</span><span class="p">,</span> <span class="nx">ok</span> <span class="p">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">children</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="nf">iterate</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="nx">start</span><span class="p">,</span> <span class="nx">stop</span><span class="p">,</span> <span class="nx">includeStart</span><span class="p">,</span> <span class="nx">hit</span><span class="p">,</span> <span class="nx">iter</span><span class="p">);</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">hit</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">hit</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="应用">应用</h4>
<p>etcd 为了实现多版本并发控制，会将键值对的每个版本<code>reversion</code>都保存到 BoltDB 中。为了将客户端提供的原始键值对信息与 reversion 关联起来，etcd 使用 btree 维护 Key 与 reversion 之间的映射关系，然后再利用获取到的 reversion 去 BoltDB 中查找 value。</p>
<p>下面是 etcd 的封装的 btree 代码，由于 btree 支持并发读，但是只能串行写，所以在<code>treeIndex</code>中加了一个读写锁。这部分的详细内容可以参考 <em><a href="https://wingsxdu.com/post/database/etcd/#%E7%8A%B6%E6%80%81%E6%9C%BA%E5%AD%98%E5%82%A8" target="_blank" rel="noopener noreferrer">etcd 状态机存储</a></em> 一文，本文不再详细缀述。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">treeIndex</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span>
</span></span><span class="line"><span class="cl">  <span class="nx">tree</span> <span class="o">*</span><span class="nx">btree</span><span class="p">.</span><span class="nx">BTree</span>
</span></span><span class="line"><span class="cl">  <span class="nx">lg</span>   <span class="o">*</span><span class="nx">zap</span><span class="p">.</span><span class="nx">Logger</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="总结">总结</h2>
<p>Google 实现的 btree 中有许多值得深思的点， 使用写时复制与回收列表来减少内存开销，并且尽可能地复用底层代码，整体的设计十分简洁。</p>
<p>通过上面的内容可以得知，B-Tree 的查找性能不稳定，最好情况是只查根节点，最坏情况是查到叶子节点。并且作为有序的数据组合，其范围查询的性能也不算好。因此在 B-Tree 的基础上提出了改进的数据结构 B+Tree：</p>
<ul>
<li>B-Tree 的所有节点都会存储数据，而 B+Tree 只有叶子节点存储数据，查询复杂度稳定为 log(n)，；</li>
<li>B+Tree 的叶子节点增加了指向左右节点的指针，即变为链表结构，在进行范围查询时由 BFS 退化为链表的遍历。</li>
</ul>
<p></p>
<p>B+Tree 的另一个优点是，由于非叶子节点不存储数据，只存储 key 和指向子节点的指针，相同大小的非叶子节点可以索引到更多的子节点。举例来说，如果我们实现一个基于磁盘的索引系统，每个节点大小固定为 16KB，每条数据的大小为 1KB，以一个 unit64 的整数作为主键，一个指向子节点的指针为 unit64，也就是说一个节点可以存储 16 条数据，。那么高度为 3 的 B+Tree 第一层为 1 个 根节点，第二层约有 1,000 个节点（一个主键和一个指针的组合占用 16Bytes），第三层约有 1,000,000 个节点，可以存储约 1600 万条数据。而 B-Tree 的第二层只有 16 个节点，第三层只有 256 个节点，只能存储 4096 条数据，需要 6 层才能存储 1600 万条数据。</p>
<p>从上面的例子可以看出，在千万级数据的情况下，不考虑在内存中缓存节点，B-Tree 最差需要 6 次 I/O 才能查询到数据，而 B+Tree 稳定为 3 次 I/O。因此在笔者看来，在基于磁盘的存储系统中，B-Tree 无论是在单个值查询还是范围查询都没有优势；而在纯内存存储的时，B-Tree 也只适用于范围查询不频繁的情况下。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.geeksforgeeks.org/difference-between-b-tree-and-b-tree/" target="_blank" rel="noopener noreferrer">Difference between B tree and B+ tree - GeeksforGeeks</a></li>
<li><a href="https://dl.acm.org/doi/10.1145/1734663.1734671" target="_blank" rel="noopener noreferrer">Organization and maintenance of large ordered indices</a></li>
<li><a href="https://medium.com/@shiansu/strict-weak-ordering-and-the-c-stl-f7dcfa4d4e07" target="_blank" rel="noopener noreferrer">Strict Weak Ordering and the C++ STL</a></li>
</ul>
]]></description>
</item><item>
    <title>Linux 内核监测技术 eBPF</title>
    <link>https://wingsxdu.com/posts/linux/ebpf/</link>
    <pubDate>Tue, 24 Aug 2021 22:30:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/linux/ebpf/</guid>
    <description><![CDATA[<blockquote>
<p>BPF 是 Linux 内核中一个非常灵活与高效的<strong>类虚拟机</strong>组件， 能够在许多内核 hook 点安全地执行字节码。本文简单整理了 eBPF 的技术原理与应用场景。</p>
</blockquote>
<h2 id="ebpf-起源">eBPF 起源</h2>
<p>BPF 全称为 <strong>B</strong>erkeley <strong>P</strong>acket <strong>F</strong>ilter，顾名思义，BPF 最初用于数据包过滤，被用于<code>tcpdump</code>指令，例如运行<code>tcpdump tcp and dst port 443</code>这样的过滤规则会复制协议为 tcp 并且目的端口是 443 的数据包到用户态。</p>
<p>BPF 程序运行在内核中，以便于过滤掉不必要的流量，仅检索那些我们需要监视的数据包，从而减少了将不必要的数据包复制到用户空间并随后进行筛选的开销。</p>
<p>BPF 是基于内核中的一个虚拟机来实现的，通过翻译 BPF 规则到字节码运行到内核中的虚拟机当中。</p>
<p>eBPF（extended BPF）由 BPF 发展而来，相比于之前的 BPF，eBPF 有了更丰富的指令，从 2 个 32 位寄存器扩展到了 11 个 64 位寄存器，而之前的版本被称为 cBPF（classic BPF）。在 linux 内核 v3.15 之后，内核开始支持 eBPF，所以有专门的程序会负责将 cBPF 指令翻译成 eBPF 指令来执行。</p>
<blockquote>
<p>关于 eBPF 的成长史，可以阅读相关文章 <a href="https://arthurchiao.art/blog/ebpf-and-k8s-zh/#7-ebpf-%E5%B9%B4%E9%89%B4" target="_blank" rel="noopener noreferrer">eBPF 年鉴</a> 。</p>
</blockquote>
<p>eBPF 大大扩展了功能，使其不仅限于包过滤，使得我们能够在内核中运行任意 eBPF 代码，例如，可以通过将程序附加到<code>kprobe</code>事件上，在相应的内核功能启动时会触发 eBPF 程序运行。借用 <a href="http://www.brendangregg.com/blog/index.html" target="_blank" rel="noopener noreferrer">Brendan Gregg</a> 对 eBPF 技术的描述：</p>
<blockquote>
<p><em>eBPF does to Linux what JavaScript does to HTML.</em></p>
</blockquote>
<p>就像我们可以使用 JavaScript 在网页上开发一些事件触发程序，在发生诸如鼠标单击按钮之类的事件时运行对应的函数，这些微型程序在浏览器的安全虚拟机中运行。使用 eBPF 时，我们可以编写当磁盘 I/O、系统调用等事件发生时运行的微型程序，这些程序在内核上的安全虚拟机中运行。更确切地说，eBPF 更像是运行 JavaScript 的 v8 虚拟机，我们可以将高级编程语言编译成字节码，运行在 eBPF 的沙盒环境中。</p>
<p>事件触发任务是指实时操作系统中的一个任务只有在与之相关的特定事件发生的条件下才能开始运行。</p>
<p>如今 eBPF 不仅仅特定于网络，还可以用于内核跟踪、安全等场景，eBPF 的通用性也吸引了更大的社区进行创新，开发相关的生态软件。</p>
<h4 id="为什么使用">为什么使用？</h4>
<p>由于 eBPF 程序可以由一系列事件触发，因此基于 eBPF 的应用程序可以帮助我们发现系统中正在发生的事情。eBPF 的一个应用场景是程序监控——识别应用程序的异常行为，例如当将文件写入重要的系统目录时，eBPF 代码可以响应文件事件而运行，以检查该程序的行为是否合法。</p>
<p>虽然有一些现成的工具也可以完成类似的任务，例如<code>ps</code>命令可以报告当前系统的进程状态。但是，此类监控程序的监测精度在于程序采样频率，执行时间比监视程序的采样间隔短的程序将不会被检测到，增加采样频率又会影响系统的性能。<strong>而 eBPF 程序是事件触发的，而不是基于采样的，并且它在内核中运行，速度非常快</strong>，所以基于 eBPF 的探测工具比传统的基于采样的方法更加准确。</p>
<p>除此之外，使用 eBPF 程序也无需重新编译内核。我们可以用 C 语言的一个子集编写程序，然后用编译器后端将其编译成 eBPF 字节码，最后内核再通过一个位于内核中的 JIT 即时编译器将 eBPF 指令映射成处理器的原生指令（opcode ），以取得在内核中的最佳执行性能。</p>
<p>基于 eBPF 的安全工具的缺点是，我们只能在新进程或文件访问事件启动时检测到它们，但是我们不能阻止它们启动。这些监测结果可以告诉我们进程何时以异常方式运行，并且可以通过 eBPF 来触发防护操作，例如关闭进程或关闭容器。但是，如果此意外行为是恶意的，则可能已经造成了损害。</p>
<p>cilium 的文档 <em><a href="https://docs.cilium.io/en/v1.9/bpf/" target="_blank" rel="noopener noreferrer">BPF and XDP Reference Guide</a></em> 中总结了 eBPF 的一些优点：</p>
<ul>
<li>无需在内核/用户空间切换就可以实现内核的可编程，需要在 eBPF 程序之间或内核/用户空间之间共享状态时，可以使用 eBPF Map。</li>
<li>eBPF 程序能在编译时将不需要的特性禁用掉， 例如，如果容器不需要 IPv4，那编写 eBPF 程序时就可以只处理 IPv6 的情况；</li>
<li>eBPF 给用户空间提供了一个稳定的 ABI，而且不依赖任何第三方内核模块：eBPF 是 Linux 内核的一个核心组成部分，而 Linux 已经得到了广泛的部署，因此可以保证现有的 eBPF 程序能在新的内核版本上继续运行。这种保证与系统调用是同一级别的，并且 eBPF 程序在不同平台上是可移植的；</li>
<li>在网络场景下，eBPF 程序可以在无需重启内核、系统服务或容器的情况下实现原子更新，并且不会导致网络中断，除此之外，更新 eBPF Map 不会导致程序状态的丢失；</li>
<li>BPF 程序与内核协同工作，复用已有的内核基础设施（例如驱动、netdevice、 隧道、协议栈和 socket）和工具（例如 iproute2），以及内核提供的安全保证。<strong>与 Linux Module 不同，eBPF 程序会被一个位于内核中的校验器（in-kernel verifier）进行校验， 以确保它们不会造成内核崩溃、程序永远会终止等等</strong>。例如，XDP 程序会复用已有的内核驱动，能够直接操作存放在 DMA 缓冲区中的数据帧，而不用像某些模型（例如 DPDK） 那样将这些数据帧甚至整个驱动暴露给用户空间。而且，XDP 程序复用内核协议栈而不是绕过它。eBPF 程序可以看做是内核设施之间的通用<code>胶水代码</code>，利用中间层设计巧妙的程序 ，解决特定的问题。</li>
</ul>
<h2 id="ebpf-编程">eBPF 编程</h2>
<p>eBPF 被设计为一个通用目的 RISC 指令集，能够直接映射到 <code>x86_64</code>、<code>arm64</code>，因此所有的 eBPF 寄存器可以一一映射到 CPU 硬件的寄存器，并且通用操作都是 64 位的，这样可以对指针进行算术操作。</p>
<p>虽然指令集中包含前向和后向跳转，但内核中的 eBPF 校验器禁止程序中有循环，以此来保证程序最终会停止。因为 eBPF 程序运行在内核，校验器的工作是保证这些程序在运行时是安全的，不会影响到系统的稳定性。这意味着，从指令集的角度来说循环是可以实现的，但校验器会对其施加限制。</p>
<p>下面介绍一些 eBPF 编程中的概念与约定。</p>
<h4 id="寄存器和调用约定">寄存器和调用约定</h4>
<p>BPF 由下面三部分组成：</p>
<ol>
<li>11 个 64 位寄存器，这些寄存器包含 32 位子寄存器；</li>
<li>一个程序计数器 (Program Counter, PC)；</li>
<li>一个 512 字节大小的 eBPF 栈空间。</li>
</ol>
<p>寄存器的名字从 <code>r0</code> 到 <code>r10</code>，eBPF 的调用约定如下：</p>
<ul>
<li><code>r0</code> 存放被调用的辅助函数的返回值，还用于保存 eBPF 程序的退出值；</li>
<li><code>r1</code> - <code>r5</code> 存放 eBPF 调用内核辅助函数时传递的参数；</li>
<li><code>r6</code> - <code>r9</code> 由被调用方保存，在函数返回之后调用方可以读取；</li>
<li><code>r10</code> 是唯一的只读寄存器，存放着 eBPF 栈空间的栈帧指针的地址；</li>
<li>栈空间用于临时保存<code>r1</code> - <code>r5</code> 的值，由于寄存器数量有限，如果要在多次辅助函数调用之间重用这些寄存器内的值，那 eBPF 程序需要负责将这些值临时转储到 eBPF 栈上 ，或者保存到被调用方保存的寄存器中。</li>
</ul>
<blockquote>
<p>注意：默认的运行模式是 64 位，32 位子寄存器只能通过特殊的 ALU（Arithmetic Logic Unit）访问，向 32 位子寄存器写入时，会用 0 填充到 64 位。</p>
</blockquote>
<p>BPF 程序开始执行时，<code>r1</code> 寄存器中存放的是程序的上下文，也就是程序的输入参数。eBPF 只能在单个上下文中工作，这个上下文是由程序类型定义的， 例如网络程序可以将网络包的内核表示<code>skb</code>作为输入参数。</p>
<p><strong>在内核版本 5.2 之前 eBPF 程序的最大指令数严格限制在 4096 条以内</strong>，这意味着从设计上就可以保证每个程序都会很快结束，但是随着 eBPF 程序越来越复杂，从 5.2 版本开始放宽到了 100 万条指令。另外，eBPF 中有尾部调用的概念，允许一 个 eBPF 程序调用另一个 eBPF 程序。尾部调用也是有限制的，目前上限是 32 层调用。现在这个功能常用来对程序逻辑进行解耦，解耦成几个不同阶段。</p>
<h4 id="程序类型与辅助函数">程序类型与辅助函数</h4>
<p>每个 eBPF 程序都属于某个特定的程序类型（Program Types），我们可以在 <em><a href="https://github.com/torvalds/linux/blob/v5.9/include/uapi/linux/bpf.h#L168" target="_blank" rel="noopener noreferrer">linux/bpf.h#L168 at v5.9</a></em> 文件中查看当前版本支持的程序类型，并且还在不断增加中。可以大致分为网络、跟踪、安全等几大类，eBPF 程序的输入参数也根据程序类型有所不同。</p>
<p>辅助函数（Helper Functions）主要用来协助处理用户空间和内核空间的交互，比如从内核获取 PID、GID、时间、操作内核的对象等。不同的内核版本支持的辅助函数也是不一样的。我们可以从 <em><a href="https://github.com/torvalds/linux/blob/v5.9/include/uapi/linux/bpf.h#L3399" target="_blank" rel="noopener noreferrer">linux/bpf.h#L3399 at v5.9</a></em> 查看当前版本支持的辅助函数。</p>
<p>不同类型的 BPF 程序能够使用的辅助函数可能是不同的，例如 eBPF 程序类型为<code>BPF_PROG_TYPE_SOCKET_FILTER</code>的只能使用下面几种辅助函数：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">BPF_FUNC_skb_load_bytes</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_skb_load_bytes_relative</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_get_socket_cookie</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_get_socket_uid</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_perf_event_output</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">Base</span> <span class="n">functions</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>受于篇幅限制，文章只举了几个简单的例子，详细的辅助函数分类与关系可以参考 BCC 的文档 <em><a href="https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md" target="_blank" rel="noopener noreferrer">bcc/docs/kernel-versions</a></em> 。</p>
<h4 id="ebpf-映射">eBPF 映射</h4>
<p>eBPF 映射（eBPF Maps）是<strong>驻留在内核空间中的高效键值仓库</strong>。Map 中的数据可以被任意eBPF 程序访问，如果想在多次 eBPF 程序调用之间保存状态，可以将状态信息放到 Map。Map 还可以从用户空间通过文件描述符访问，可以在任意 eBPF 程序以及用户空间应用之间共享。因此可以通过 Map 来达到 eBPF 程序与 eBPF 程序之间，eBPF 程序与用户态程序之间的数据交互。</p>
<p>共享 Map 的 eBPF 程序不要求是相同的程序类型，例如监控程序可以和网络程序共享 Map。目前单个 eBPF 程序最多可直接访问 64 个不同 Map。</p>
<p></p>
<h5 id="映射类型">映射类型</h5>
<p>Map 为上层程序提供了一层基础数据结构的映射，现在有二十几种数据类型，均由 core kernel 实现，因此我们无法增添或修改数据结构。Map 分为通用 Map 和非通用 Map 两种。下面列出了几个通用 Map 的类型：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 通用 MAP
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_TYPE_HASH</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_ARRAY</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_LRU_HASH</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_PERCPU_HASH</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_PERCPU_ARRAY</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_LRU_PERCPU_HASH</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>通用 Map 提供了哈希表、数组、LRU 等数据结构的映射，除此之外，还提供了对应的单 CPU 映射类型：我们可以给类型映射分配 CPU，每个 CPU 会看到自己独立的映射版本，这样更有利于高性能查找和指标聚合。</p>
<p>eBPF 程序可以通过辅助函数读写 Map，用户态程序也可以通过<code>bpf()</code>系统调用读写 Map，下面列出了所有对通用 Map 进行操作的函数和命令：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// Map 相关的辅助函数
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">BPF_FUNC_map_lookup_elem</span><span class="p">()</span>		<span class="c1">// 查找元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">BPF_FUNC_map_update_elem</span><span class="p">()</span>		<span class="c1">// 更新或创建元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">BPF_FUNC_map_delete_elem</span><span class="p">()</span>		<span class="c1">// 删除元素
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">BPF_FUNC_map_peek_elem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_map_pop_elem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">BPF_FUNC_map_push_elem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">// bpf() 系统调用可操作的 API
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_LOOKUP_ELEM</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_UPDATE_ELEM</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_DELETE_ELEM</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_GET_NEXT_KEY</span><span class="p">,</span>			<span class="c1">// 用于迭代查询
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_LOOKUP_AND_DELETE_ELEM</span><span class="p">,</span>	<span class="c1">// 查找并删除
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_LOOKUP_BATCH</span><span class="p">,</span>			<span class="c1">// 对应操作的批量处理
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_LOOKUP_AND_DELETE_BATCH</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_UPDATE_BATCH</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_DELETE_BATCH</span><span class="p">,</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>数据操作并不复杂，从函数名称可以看出它的作用，用户态<code>bpf()</code>系统调用则又封装了一些高级操作，方便进行批量数据处理。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 非通用 MAP
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">BPF_MAP_TYPE_PROG_ARRAY</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_ARRAY_OF_MAPS</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_HASH_OF_MAPS</span>
</span></span><span class="line"><span class="cl"><span class="n">BPF_MAP_TYPE_CGROUP_ARRAY</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>上面列出了几个非通用 Map，它们只用于特定的场景，例如<code>BPF_MAP_TYPE_PROG_ARRAY</code> 用于保存其它的 eBPF 程序的引用，可以与尾部调用配合实现程序间的跳转；<code>BPF_MAP_TYPE_ARRAY_OF_MAPS</code> 和 <code>BPF_MAP_TYPE_HASH_OF_MAPS</code> 都用于持有其他 Map 的指针，这样整个 Map 就可以在运行时实现原子替换，<code>BPF_MAP_TYPE_CGROUP_ARRAY</code>则用于保存对 cgroup 的引用。</p>
<h5 id="ebpf-虚拟文件系统">eBPF 虚拟文件系统</h5>
<p>eBPF 映射的基本特征是文件描述符<code>fd</code>，这意味着当文件描述符关闭后保存的数据也会消失。为了使这些数据在创建它的程序终止后依然保存，从 Linux 内核 4.4 版本开始引入了 eBPF 虚拟文件系统，默认将数据挂载在<code>/sys/fs/bpf/</code>目录下，通过路径来标识这些持久化对象。</p>
<p>我们只能通过系统调用<code>bpf()</code>来操作这些对象，<code>BPF_OBJ_PIN</code>命令用于将 Map 对象保存到文件系统中，<code>BPF_OBJ_GET</code>用于获取已经固定到文件系统中的对象。</p>
<h5 id="并发访问">并发访问</h5>
<p>由于 eBPF 映射可以发生许多程序同时并发访问同一个 Map，这可能会产生竞争条件。为了防止数据竞争，eBPF 引入了<strong>自旋锁</strong>的概念，对访问的映射元素进行锁定。</p>
<blockquote>
<p>自旋锁这一特性由 Linux 5.1 版本引入，且仅适用于数组、哈希、cgroup 映射。</p>
</blockquote>
<p>在 eBPF 程序中，我们可以使用<code>BPF_FUNC_spin_lock()</code>和<code>BPF_FUNC_spin_unlock()</code>这两个辅助函数对数据加锁解锁，释放锁后其它程序就可以安全地访问该元素。而在用户空间，我们在更新或读取元素时可以使用<code>BPF_F_LOCK</code>标志，从而避免数据竞争。</p>
<h2 id="ebpf-应用场景">eBPF 应用场景</h2>
<p>在了解 eBPF 编程的基本概念后，我们来看看 eBPF 的几个应用场景。</p>
<h4 id="跟踪">跟踪</h4>
<p>跟踪的目的是提供运行时有用的信息以便将来进行问题的分析。使用 eBPF 进行跟踪的主要优点是几乎可以访问 Linux 内核和应用程序的任何信息，并且 eBPF 对系统性能和延迟造成的开销最小，也不需要为了收集数据而去修改业务程序。 eBPF 提供了探针和追踪点两种跟踪方式</p>
<h5 id="探针">探针</h5>
<p>eBPF 提供的探针分为两种：</p>
<ul>
<li><strong>内核探针</strong>：提供对内核中内部组件的动态访问；</li>
<li><strong>用户空间探针</strong>：提供对用户空间运行程序的动态访问。</li>
</ul>
<p>其中内核探针分为两类：kprobes 允许在执行任何内核指令之前插入 eBPF 程序，kretprobes 则是在内核指令有返回值时插入  eBPF 程序。用户空间探针允许在用户空间运行的程序中设置动态标志，它们等同于内核探针，分为 uprobes 和 ureprobes。</p>
<p>在下面的 BCC 示例代码中，我们编写了一段 kprobes 探针程序，该程序通过跟踪<code>execve()</code>系统调用来演示 eBPF 的功能。监视<code>execve()</code>系统调用对于检测意外的可执行文件很有用。</p>
<p>代码中的 中<code>do_sys_execve()</code>函数用于获取内核正在运行的命令名，并打印至控制台；然后我们利用 BCC 提供的 bpf.attach_kprobe() 方法将<code>do_sys_execve()</code>函数和<code>execve()</code>系统调用绑定起来。运行这段程序将会看到，每当内核执行<code>execve()</code>系统调用时都会在控制台打印相应的命令名称。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">bcc</span> <span class="kn">import</span> <span class="n">BPF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bpf_source</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">#include &lt;uapi/linux/ptrace.h&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">int do_sys_execve(struct pt_regs *ctx) {
</span></span></span><span class="line"><span class="cl"><span class="s2">  char comm[16];
</span></span></span><span class="line"><span class="cl"><span class="s2">  bpf_get_current_comm(&amp;comm, sizeof(comm));
</span></span></span><span class="line"><span class="cl"><span class="s2">  bpf_trace_printk(&#34;executing program: </span><span class="si">%s</span><span class="se">\\</span><span class="s2">n&#34;, comm);
</span></span></span><span class="line"><span class="cl"><span class="s2">  return 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">}
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bpf</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">bpf_source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">execve_function</span> <span class="o">=</span> <span class="n">bpf</span><span class="o">.</span><span class="n">get_syscall_fnname</span><span class="p">(</span><span class="s2">&#34;execve&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bpf</span><span class="o">.</span><span class="n">attach_kprobe</span><span class="p">(</span><span class="n">event</span><span class="o">=</span><span class="n">execve_function</span><span class="p">,</span> <span class="n">fn_name</span><span class="o">=</span><span class="s2">&#34;do_sys_execve&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bpf</span><span class="o">.</span><span class="n">trace_print</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>需要注意的是，内核探针没有稳定的应用程序二进制接口(ABI)，因此在不同的内核版本中同样的程序代码可能无法工作。</p>
<h5 id="追踪点">追踪点</h5>
<p>追踪点是内核代码的静态标记，由内核人员开发编写，并且保证旧版本上的追踪点在新版本上存在。</p>
<p>在 Linux 上每个跟踪点都对应一个<code>/sys/kernel/debug/tracing/events</code>条目。例如，查看网络相关的追踪点：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># sudo ls -la /sys/kernel/debug/tracing/events/net</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 篇幅限制删减了部分追踪点</span>
</span></span><span class="line"><span class="cl">total <span class="m">0</span>
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 net_dev_queue
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 net_dev_start_xmi
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_receive_skb
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_receive_skb_entry
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_receive_skb_exit
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_rx
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_rx_entry
</span></span><span class="line"><span class="cl">drwxr-xr-x  <span class="m">2</span> root root <span class="m">0</span> Nov <span class="m">20</span> 21:00 netif_rx_exit
</span></span></code></pre></td></tr></table>
</div>
</div><p>下面的示例中，我们将 eBPF 程序绑定到<code>net:netif_rx</code>追踪点上，并在控制台打印出调用程序名称。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">bcc</span> <span class="kn">import</span> <span class="n">BPF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bpf_source</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">int trace_netif_rx(void *ctx) {
</span></span></span><span class="line"><span class="cl"><span class="s2">  char comm[16];
</span></span></span><span class="line"><span class="cl"><span class="s2">  bpf_get_current_comm(&amp;comm, sizeof(comm));
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  bpf_trace_printk(&#34;</span><span class="si">%s</span><span class="s2"> is doing netif_rx&#34;, comm);
</span></span></span><span class="line"><span class="cl"><span class="s2">  return 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">}
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bpf</span> <span class="o">=</span> <span class="n">BPF</span><span class="p">(</span><span class="n">text</span> <span class="o">=</span> <span class="n">bpf_source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bpf</span><span class="o">.</span><span class="n">attach_tracepoint</span><span class="p">(</span><span class="n">tp</span> <span class="o">=</span> <span class="s2">&#34;net:netif_rx&#34;</span><span class="p">,</span> <span class="n">fn_name</span> <span class="o">=</span> <span class="s2">&#34;trace_netif_rx&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bpf</span><span class="o">.</span><span class="n">trace_print</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="网络">网络</h4>
<p>eBPF 程序在网络中主要有两个用途：数据包捕获和过滤，用户空间的程序可以将过滤器加到任何套接字中，提取数据包的相关信息，或者对特定类型的数据包放行、禁止、重定向等。</p>
<h5 id="数据包过滤">数据包过滤</h5>
<p>数据包过滤是 eBPF 最常用的场景之一，主要用于三种情况：</p>
<ul>
<li>实时流量丢弃，例如只允许 UDP 流量通过，丢弃其它数据包；</li>
<li>实时观测特定条件过滤后的数据包；</li>
<li>对实时系统中抓取的网络流量进行后续分析。</li>
</ul>
<p><code>tcpdump</code>是典型的 eBPF 数据包过滤应用，现在我们可以编写一些自定义的网络程序，在 <em><a href="https://github.com/iovisor/bcc/tree/master/examples/networking" target="_blank" rel="noopener noreferrer">bcc/examples/networking</a></em> 中可以看到一些使用示例</p>
<h5 id="流量控制分类器">流量控制分类器</h5>
<p>流量控制的几个用例如下：</p>
<ul>
<li>优先处理某些类型的数据包；</li>
<li>丢弃特定类型的数据包；</li>
<li>带宽分配。</li>
</ul>
<h5 id="xdp">XDP</h5>
<p>XDP（eXpress Data Path）为 Linux 内核提供了高性能、可编程的网络数据路径。由于网络包在还未进入网络协议栈之前就处理，它给 Linux 网络带来了巨大的性能提升。</p>
<p></p>
<p>通过上面的图片我们可以大致看到一个完整的数据包接收过程，可以看到整个处理链路还是比较长的，且需要在内核态与用户态之间做内存拷贝、上下文切换、软硬件中断等。处理大量的数据包会占用大量 CPU 资源，难以满足高并发网络的需求。</p>
<p>XDP 解决了这个问题。它相当于在 Linux 网络栈中加了新的一层。在报文到达 CPU 的最早时刻进行处理，甚至避免了<code>sk_buff</code>的分配，减少了内存拷贝上的负荷。在特定网卡硬件与驱动的支持下，甚至可以将 XDP 程序卸载到网卡上执行，进一步减少了 CPU 的使用</p>
<p>XDP 依赖 eBPF 技术，并提供了一套完整的、可编程的报文处理方案，我们可以利用 XDP 实现数据包的转发、重定向与向下传递。相关示例可以在 <a href="https://github.com/iovisor/bcc/tree/master/examples/networking/xdp" target="_blank" rel="noopener noreferrer">bcc/examples/networking/xdp</a> 中查看。</p>
<h4 id="安全">安全</h4>
<p>Seccomp（Secure Computing）在 2.6.12 版本中引入 Linux 内核，将进程可用的系统调用限制为四种：<code>read</code>、<code>write</code>、<code>_exit</code>、<code>sigreturn</code>。最初 Seccomp 模式采用白名单方式实现，在这种安全模式下，除了已打开的文件描述符和允许的四种系统调用，如果尝试其他系统调用，内核就会使用 SIGKILL 或 SIGSYS 终止该进程。</p>
<p>尽管 Seccomp 保证了主机的安全，但由于限制太强实际作用并不大。在实际应用中需要更加精细的限制，为了解决此问题，引入了 Seccomp-BPF 。Seccomp-BPF 是 Seccomp 和 cBPF 规则的结合（注意并不是 eBPF），它允许用户使用可配置的策略过滤系统调用，该策略使用 BPF 规则实现，它可以对任意系统调用及其参数进行过滤。</p>
<p>使用示例可以参考 <a href="http://wh4lter.icu/2020/04/20/seccomp/" target="_blank" rel="noopener noreferrer">seccomp</a> 。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://blog.aquasec.com/intro-ebpf-tracing-containers" target="_blank" rel="noopener noreferrer">A Deep Dive into eBPF: The Technology that Powers Tracee (aquasec.com)</a></li>
<li><a href="http://arthurchiao.art/blog/cilium-bpf-xdp-reference-guide-zh/" target="_blank" rel="noopener noreferrer">[译] Cilium：BPF 和 XDP 参考指南（2019）</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md" target="_blank" rel="noopener noreferrer">bcc Reference Guide</a></li>
<li><a href="https://www.anquanke.com/post/id/208364" target="_blank" rel="noopener noreferrer">Seccomp从0到1</a></li>
</ul>
]]></description>
</item><item>
    <title>Linux 虚拟文件系统</title>
    <link>https://wingsxdu.com/posts/linux/vfs/</link>
    <pubDate>Sun, 22 Aug 2021 23:30:00 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/linux/vfs/</guid>
    <description><![CDATA[<blockquote>
<p>文件系统是对一个存储设备上的数据和元数据进行组织的机制，由于定义如此宽泛，各个文件系统的实现也大不相同，其中常见的文件系统有 ext4、NFS、/proc 等。Linux 采用为分层的体系结构，将用户接口层、文件系统实现和存储设备的驱动程序分隔开，进而兼容不同的文件系统。</p>
</blockquote>
<p>虚拟文件系统（Virtual File System, VFS）是 Linux 内核中的软件层，它在内核中提供了一组标准的、抽象的文件操作，允许不同的文件系统实现共存，并向用户空间程序提供统一的文件系统接口。下面这张图展示了 Linux 虚拟文件系统的整体结构：</p>
<p></p>
<blockquote>
<p>上图修改自：<a href="https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/index.html" target="_blank" rel="noopener noreferrer">《Linux 文件系统剖析》图 1. Linux 文件系统组件的体系结构</a></p>
</blockquote>
<p>从上图可以看出，用户空间的应用程序直接、或是通过编程语言提供的库函数间接调用内核提供的 System Call 接口（如<code>open()</code>、<code>write()</code>等）执行文件操作。System Call 接口再将应用程序的参数传递给虚拟文件系统进行处理。</p>
<p>每个文件系统都为 VFS 实现了一组通用接口，具体的文件系统根据自己对磁盘上数据的组织方式操作相应的数据。当应用程序操作某个文件时，VFS 会根据文件路径找到相应的挂载点，得到具体的文件系统信息，然后调用该文件系统的对应操作函数。</p>
<p>VFS 提供了两个针对文件系统对象的缓存 INode Cache 和 DEntry Cache，它们缓存最近使用过的文件系统对象，用来加快对 INode 和 DEntry 的访问。Linux 内核还提供了 Buffer Cache 缓冲区，用来缓存文件系统和相关块设备之间的请求，减少访问物理设备的次数，加快访问速度。Buffer Cache 以 LRU 列表的形式管理缓冲区。</p>
<p>VFS 的好处是实现了应用程序的文件操作与具体的文件系统的解耦，使得编程更加容易：</p>
<ul>
<li>应用层程序只要使用 VFS 对外提供的<code>read()</code>、<code>write()</code>等接口就可以执行文件操作，不需要关心底层文件系统的实现细节；</li>
<li>文件系统只需要实现 VFS 接口就可以兼容 Linux，方便移植与维护；</li>
<li>无需关注具体的实现细节，就实现跨文件系统的文件操作。</li>
</ul>
<p>了解 Linux 文件系统的整体结构后，下面主要分析 Linux VFS 的技术原理。由于文件系统与设备驱动的实现非常复杂，笔者也未接触过这方面的内容，因此文中不会涉及具体文件系统的实现。</p>
<h2 id="vfs-结构">VFS 结构</h2>
<p>Linux 以一组通用对象的角度看待所有文件系统，每一级对象之间的关系如下图所示：</p>
<p></p>
<h4 id="fd-与-file">fd 与 file</h4>
<p>每个进程都持有一个<code>fd[]</code>数组，数组里面存放的是指向<code>file</code>结构体的指针，同一进程的不同<code>fd</code>可以指向同一个<code>file</code>对象；</p>
<p><code>file</code>是内核中的数据结构，表示一个被进程打开的文件，和进程相关联。当应用程序调用<code>open()</code>函数的时候，VFS 就会创建相应的<code>file</code>对象。它会保存打开文件的状态，例如文件权限、路径、偏移量等等。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// https://elixir.bootlin.com/linux/v5.4.93/source/include/linux/fs.h#L936 结构体已删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">struct</span> <span class="n">file</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">path</span>                   <span class="n">f_path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">inode</span>                  <span class="o">*</span><span class="n">f_inode</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span>  <span class="o">*</span><span class="n">f_op</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span>                  <span class="n">f_flags</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">fmode_t</span>                       <span class="n">f_mode</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">loff_t</span>                        <span class="n">f_pos</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">fown_struct</span>            <span class="n">f_owner</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// https://elixir.bootlin.com/linux/v5.4.93/source/include/linux/path.h#L8
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">struct</span> <span class="n">path</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">vfsmount</span>  <span class="o">*</span><span class="n">mnt</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">dentry</span>    <span class="o">*</span><span class="n">dentry</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>从上面的代码可以看出，文件的路径实际上是一个指向 DEntry 结构体的指针，VFS 通过 DEntry 索引到文件的位置。</p>
<p>除了文件偏移量<code>f_pos</code>是进程私有的数据外，其他的数据都来自于 INode 和 DEntry，和所有进程共享。不同进程的<code>file</code>对象可以指向同一个 DEntry 和 Inode，从而实现文件的共享。</p>
<h4 id="dentry-与-inode">DEntry 与 INode</h4>
<p>Linux文件系统会为每个文件都分配两个数据结构，目录项（DEntry, Directory Entry）和索引节点（INode, Index Node）。</p>
<p>DEntry 用来保存文件路径和 INode 之间的映射，从而支持在文件系统中移动。DEntry 由 VFS 维护，所有文件系统共享，不和具体的进程关联。<code>dentry</code>对象从根目录“/”开始，每个<code>dentry</code>对象都会持有自己的子目录和文件，这样就形成了文件树。举例来说，如果要访问&quot;/home/beihai/a.txt&quot;文件并对他操作，系统会解析文件路径，首先从“/”根目录的<code>dentry</code>对象开始访问，然后找到&quot;home/&ldquo;目录，其次是“beihai/”，最后找到“a.txt”的<code>dentry</code>结构体，该结构体里面<code>d_inode</code>字段就对应着该文件。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// https://elixir.bootlin.com/linux/v5.4.93/source/include/linux/dcache.h#L89 结构体已删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">struct</span> <span class="n">dentry</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">dentry</span> <span class="o">*</span><span class="n">d_parent</span><span class="p">;</span>     <span class="c1">// 父目录
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">qstr</span> <span class="n">d_name</span><span class="p">;</span>          <span class="c1">// 文件名称
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">inode</span> <span class="o">*</span><span class="n">d_inode</span><span class="p">;</span>       <span class="c1">// 关联的 inode
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">d_child</span><span class="p">;</span>    <span class="c1">// 父目录中的子目录和文件
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">d_subdirs</span><span class="p">;</span>  <span class="c1">// 当前目录中的子目录和文件
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>每一个<code>dentry</code>对象都持有一个对应的<code>inode</code>对象，表示 Linux 中一个具体的目录项或文件。INode 包含管理文件系统中的对象所需的所有元数据，以及可以在该文件对象上执行的操作。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// https://elixir.bootlin.com/linux/v5.4.93/source/include/linux/fs.h#L628 结构体已删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">struct</span> <span class="n">inode</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">umode_t</span>                 <span class="n">i_mode</span><span class="p">;</span>          <span class="c1">// 文件权限及类型
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">kuid_t</span>                  <span class="n">i_uid</span><span class="p">;</span>           <span class="c1">// user id
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">kgid_t</span>                  <span class="n">i_gid</span><span class="p">;</span>           <span class="c1">// group id
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="k">const</span> <span class="k">struct</span> <span class="n">inode_operations</span>    <span class="o">*</span><span class="n">i_op</span><span class="p">;</span>  <span class="c1">// inode 操作函数，如 create，mkdir，lookup，rename 等
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">super_block</span>      <span class="o">*</span><span class="n">i_sb</span><span class="p">;</span>           <span class="c1">// 所属的 SuperBlock
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">    <span class="kt">loff_t</span>                  <span class="n">i_size</span><span class="p">;</span>          <span class="c1">// 文件大小
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">timespec</span>         <span class="n">i_atime</span><span class="p">;</span>         <span class="c1">// 文件最后访问时间
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">timespec</span>         <span class="n">i_mtime</span><span class="p">;</span>         <span class="c1">// 文件最后修改时间
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">timespec</span>         <span class="n">i_ctime</span><span class="p">;</span>         <span class="c1">// 文件元数据最后修改时间（包括文件名称）
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span>    <span class="o">*</span><span class="n">i_fop</span><span class="p">;</span>  <span class="c1">// 文件操作函数，open、write 等
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">void</span>                    <span class="o">*</span><span class="n">i_private</span><span class="p">;</span>      <span class="c1">// 文件系统的私有数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>虚拟文件系统维护了一个 DEntry Cache 缓存，用来保存最近使用的 DEntry，加速查询操作。当调用<code>open()</code>函数打开一个文件时，内核会第一时间根据文件路径到 DEntry Cache 里面寻找相应的 DEntry，找到了就直接构造一个<code>file</code>对象并返回。如果该文件不在缓存中，那么 VFS 会根据找到的最近目录一级一级地向下加载，直到找到相应的文件。期间 VFS 会缓存所有被加载生成的<code>dentry</code>。</p>
<p>INode 存储的数据存放在磁盘上，由具体的文件系统进行组织，当需要访问一个 INode 时，会由文件系统从磁盘上加载相应的数据并构造 INode。一个 INode 可能被多个 DEntry 所关联，即相当于为某一文件创建了多个文件路径（通常是为文件建立硬链接）。</p>
<h4 id="superblock">SuperBlock</h4>
<p>SuperBlock 表示特定加载的文件系统，用于描述和维护文件系统的状态，由 VFS 定义，但里面的数据根据具体的文件系统填充。每个 SuperBlock 代表了一个具体的磁盘分区，里面包含了当前磁盘分区的信息，如文件系统类型、剩余空间等。SuperBlock 的一个重要成员是链表<code>s_list</code>，包含所有修改过的 INode，使用该链表很容易区分出来哪个文件被修改过，并配合内核线程将数据写回磁盘。SuperBlock 的另一个重要成员是<code>s_op</code>，定义了针对其 INode 的所有操作方法，例如标记、释放索引节点等一系列操作。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// https://elixir.bootlin.com/linux/v5.4.93/source/include/linux/fs.h#L1425 结构体已删减
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">struct</span> <span class="n">super_block</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">struct</span> <span class="n">list_head</span>    <span class="n">s_list</span><span class="p">;</span>               <span class="c1">// 指向链表的指针
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">dev_t</span>               <span class="n">s_dev</span><span class="p">;</span>                <span class="c1">// 设备标识符
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">unsigned</span> <span class="kt">long</span>       <span class="n">s_blocksize</span><span class="p">;</span>          <span class="c1">// 以字节为单位的块大小
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kt">loff_t</span>              <span class="n">s_maxbytes</span><span class="p">;</span>           <span class="c1">// 文件大小上限
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">file_system_type</span>    <span class="o">*</span><span class="n">s_type</span><span class="p">;</span>       <span class="c1">// 文件系统类型
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">const</span> <span class="k">struct</span> <span class="n">super_operations</span>    <span class="o">*</span><span class="n">s_op</span><span class="p">;</span>   <span class="c1">// SuperBlock 操作函数，write_inode、put_inode 等
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">const</span> <span class="k">struct</span> <span class="n">dquot_operations</span>    <span class="o">*</span><span class="n">dq_op</span><span class="p">;</span>  <span class="c1">// 磁盘限额函数
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">struct</span> <span class="n">dentry</span>        <span class="o">*</span><span class="n">s_root</span><span class="p">;</span>             <span class="c1">// 根目录
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>SuperBlock 是一个非常复杂的结构，通过 SuperBlock 我们可以将一个实体文件系统挂载到 Linux 上，或者对 INode 进行增删改查操作。所以一般文件系统都会在磁盘上存储多份 SuperBlock，防止数据意外损坏导致整个分区无法读取。</p>
<h2 id="应用">应用</h2>
<h4 id="procfs">procfs</h4>
<p><code>/proc</code> 目录是 Linux 提供的一个虚拟文件系统，存储的是当前内核运行状态的一系列特殊文件。用户可以通过这些文件查关系统硬件及当前正在运行进程的信息，甚至可以通过更改其中某些文件来改变内核的运行状态。</p>
<p><code>/proc</code> 不是一个真正的文件系统，它并不占用存储空间，只是占用有限的内存。但 <code>/proc</code> 实现了虚拟文件系统的接口，使得我们可以像操作一个普通的文件那样操作<code>/proc</code> 目录下的内容：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># more /proc/{pid}/status</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ll /proc/{pid}/fd</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>有关<code>/proc</code> 更多的用法可以参考文档*<a href="https://man7.org/linux/man-pages/man5/proc.5.html" target="_blank" rel="noopener noreferrer">proc(5) — Linux manual page</a>*，在 Linux 系统中类似于 procfs 的伪文件系统还有 sysfs、tmpfs 等。</p>
<p>Linux 的一个重要概念就是「一切皆是文件」，从这里可以看出，不论是普通的文件，还是特殊的目录、设备等，只要实现了相关的接口，VFS 就可以将它们同等看待成文件，通过同一套文件操作方式来对它们进行操作。当我们打开文件时，VFS 会获取该文件对应的文件系统格式，当 VFS 把控制权传给实际的文件系统时，实际的文件系统再做出具体区分，对不同的文件类型执行不同的操作。</p>
<h2 id="总结">总结</h2>
<p>虚拟文件系统是操作系统中非常重要的一层抽象，<strong>其主要作用在于让上层的软件，能够用统一的方式，与底层不同的文件系统沟通</strong>。在操作系统与底层的各类文件系统之间，虚拟文件系统提供了标准的操作接口，让操作系统能够很快地支持新的文件系统。也因为 VFS 的支持，众多不同的实际文件系统才能在 Linux 中共存，跨文件系统的操作才能实现。</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/index.html" target="_blank" rel="noopener noreferrer">Linux 文件系统剖析 </a></li>
<li><a href="https://zh.wikipedia.org/wiki/Procfs" target="_blank" rel="noopener noreferrer">procfs</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/69289429" target="_blank" rel="noopener noreferrer">浅谈Linux虚拟文件系统</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/filesystems/vfs.html" target="_blank" rel="noopener noreferrer">Overview of the Linux Virtual File System</a></li>
</ul>
]]></description>
</item><item>
    <title>2020 年总结：勇敢面对</title>
    <link>https://wingsxdu.com/posts/summary/2020/</link>
    <pubDate>Thu, 11 Feb 2021 21:02:37 &#43;0800</pubDate><author>beihai@wingsxdu.com</author><dc:creator>beihai</dc:creator><guid>https://wingsxdu.com/posts/summary/2020/</guid>
    <description><![CDATA[<blockquote>
<p>2020 年发生了许多黑天鹅事件，笔者在这一年也经历了许多意料之外的事情，既定的学习计划一再延期。总体来看，任何选择都伴随着一定的风险，充斥着不确定性，但是不管遇到什么意外事件，笔者认为我们都要学会拥抱变化，勇敢地去面对问题、接受问题、解决问题。</p>
</blockquote>
<p>在 2019 年，笔者一直渴望拥有专注学习的能力，因此逼迫自己去啃下一块硬骨头，不管用多少时间都要咬牙学完。这段经历对于笔者的意义是重大的，懂得「抬头看路，低头做事」，多花费一些时间思考如何能够让自己达成目标，前进的路径的是什么，需要掌握的技能是什么，最终发现很多事情也并不是这么难做到。</p>
<p></p>
<p>由于专业原因，这一年的上半年与下半年做着截然不同的两种事情。上半年以 etcd 为中心，初探分布式系统，这段时光是非常充实的。而在下半年，为了顺利毕业，笔者不得不在秋季学期重修课程，停滞了所有的学习计划。强迫自己去做不喜欢的东西是一件痛苦的事情，但是笔者也利用这段时间沉淀认知，思考更适合自己的未来方向。</p>
<h2 id="etcd-与-raft">etcd 与 Raft</h2>
<p>etcd 是笔者第一个从零开始学习的开源项目，etcd 是由 CoreOS 发起的开源项目，旨在构建一个高可用的分布式键值存储系统，可以用于存储关键数据和实现分布式协调与配置服务，在现代化的集群运行中起到关键性的作用。</p>
<p>作为分布式系统中一个重要的组件，etcd 涉及分布式共识、数据库事务、数据一致性等问题。笔者将任务分解，从点到面不断扩展技能版图，例如通过阅读 Raft 论文来了解分布式共识算法，通过 MySQL 来理解事务与锁，通过阅读 BoltDB 源码去理解 etcd 的底层存储。先理解相关理论知识，再去阅读 etcd 源码，就不会觉得一头雾水了。这期间也整理了几篇文章，在下面列出：</p>
<ol>
<li><em><a href="https://wingsxdu.com/post/algorithms/raft" target="_blank" rel="noopener noreferrer">分布式一致性协议 Raft 原理</a></em></li>
<li><em><a href="https://wingsxdu.com/post/database/boltdb" target="_blank" rel="noopener noreferrer">可嵌入式数据库 BoltDB 实现原理</a></em></li>
<li><em><a href="https://wingsxdu.com/post/database/etcd" target="_blank" rel="noopener noreferrer">分布式键值存储 etcd 原理与实现</a></em></li>
<li><em><a href="https://wingsxdu.com/post/algorithms/distributed-consensus-and-data-consistent" target="_blank" rel="noopener noreferrer">漫谈分布式共识算法与数据一致性</a></em></li>
</ol>
<p>随着对分布式系统的理解不断深入，笔者也对这几篇文章进行修正，因此个人对这几篇文章的质量与内容深度是比较满意的，较为清晰地解读了分布式系统面临的一些问题。</p>
<p>etcd 这个计划直到 7 月份才开始收尾，投入了近半年的时间与精力，最终也取得了不错的收益。对于笔者来说，这段经历最大的收获并不在于我学会了多么困难的知识，而在于掌握了解决复杂问题的方法与途径，以及培养了我的自学能力。笔者认为<strong>一个人最重要的能力就是学习能力</strong>，其它的能力可以通过学习能力后天获得，并以此维持自己的竞争力。</p>
<p>在学习 etcd 的过程中笔者接触到了 <strong><a href="https://book.douban.com/subject/30329536/" target="_blank" rel="noopener noreferrer">《数据密集型应用系统设计》</a></strong> 一书，这是我今年读到的最好的一本技术类书籍。这本书把分布式环境中面临的问题、相关的技术与解决方案都讲得非常清楚，并且深入浅出，把复杂的东西简单化，帮助我们加深理解，非常值得一读。</p>
<h2 id="重修与秋招">重修与秋招</h2>
<p>得益于上半年的在线网课，笔者可以将更多的时间用于钻研理论知识与阅读项目源码，这也带来了严重的后果：在 8 月份的期末考试中，我挂科了一门非常重要的专业主课，这意味着，如果我在大四不能顺利重修通过考试的话，就要延迟毕业一年继续重修。</p>
<p>除此之外，今年的秋招在 7 月末就已经开始，而我 8 月份要准备期末考试，9 月初还要做课程设计。当我开始投递简历时，已经错过了很多大厂的秋招。</p>
<p>同时面临毕业与就业问题，当时的心态是非常焦虑的。幸运的是，专业教学大纲进行了调整，原定于 2021 春季重修的课程调至 2020 年秋季。因此下半年的目标就是全力保证顺利毕业，其实当时也没有其余的选择，因为即使我秋招拿到了 Offer，如果不能顺利毕业，那么这些 Offer 最终也是无效的。</p>
<p>重修的过程是比较煎熬的，心中对不喜欢做的事情总是有各种各样的不情愿。调整心态后，笔者最终坦然接受这件事情，还好，并未出现最坏的结果，可以顺利毕业了。</p>
<p></p>
<p>秋招的经历就比较惨痛了，没有刷算法与面经，寥寥几场面试全凭临场发挥，在 12 月的面试时，已经近四个月没怎么接触计科相关的内容，这导致某些面试官对我的能力水平产生了怀疑，为什么一个在博客上侃侃而谈谈的人，却说不出 Cookie 与 Session 的区别，心中只能一丝苦笑。</p>
<p>下半年的经历可以用一句「天道好轮回」来概括，笔者一直在偿还曾经欠下的债务。期间也会时不时地感叹，如果当初好好复习期末考试，注意一下秋招动态，或许就不会发生现在的状况了。但转念一想，当时全身心地投入到分布式系统的学习中，确实没有多余的时间来关注其它的事情。</p>
<h2 id="项目">项目</h2>
<p>笔者一直梦想着维护一个自己的开源项目，或者为开源社区贡献代码。由于个人能力有限，时间也比较紧缺，这件事只能无限期搁置了。但是我会坚持更新自己的博客，闲暇时也会写一些玩具项目。</p>
<h4 id="tinyurl">tinyUrl</h4>
<p><a href="https://github.com/wingsxdu/tinyurl" target="_blank" rel="noopener noreferrer"><em>tinyUr</em>l</a> 是笔者使用 Go 语言与 Base36 编码实现的短链接服务，并且支持 URL 替换功能，即可以将<code>https://www.amazon.com/%E6%9C%9D%E8%8A%B1%E5%A4%95%E6%8B%BE-%E9%B2%81%E8%BF%85/dp/7519015432</code>这样的长链接转换为类似于<code>http://localhost/t/6c7f</code>的短链接。</p>
<p>tinyUrl 底层使用 BoltDB 存储数据，并添加了 LRU 缓存。最初做这个项目是想练练如何使用 BoltDB，虽然是一个玩具项目，但是已经用于本博客的后台服务中，希望未来会去增添一些新的 feature，扩大应用场景。</p>
<h4 id="博客">博客</h4>
<p>正式写博客有一年多的时间了，笔者在这里记录着学习过程中的思考与总结。一年以来，写作的方式在不断改进，与最初相比，文章质量也有了很大提升。</p>
<p>下图是 Google Analytics 的统计数据，虽然访问量很低，但是大部分是通过搜索引擎浏览博客的。出乎我意料的是，一些前辈们搜到了我的文章，在文章下面留言或者发送邮件联系，提出自己的见解和建议。与仰慕的大神们取得联系内心是非常激动的，这无形之中也敦促我写出更有深度、质量更好的文章。</p>
<p></p>
<p>最后要对几位给我提出了中肯的建议的前辈们说一声感谢，帮助我决定未来的发展发向与职业选择，作为一名学生无以回报，那就祝各位前辈事业顺利，早日实现财富自由吧。<del>可能已经实现财富自由</del></p>
<h2 id="写在最后">写在最后</h2>
<p>任何选择都伴随着一定的风险，充斥着不确定性甚至是黑天鹅事件，但是不管发生什么，都要勇敢地去面对，对曾经做过的决定也不懊悔。从长远来看，我更愿意相信理性的力量、温和的力量、包容的力量。理性未必会战胜不理性，但是不理性常常会战胜它自己。</p>
<p>最后，附上 2020 年书单，希望 2021 会是新的开始。</p>
<h2 id="2020-书单">2020 书单</h2>
<ul>
<li>《人类简史》</li>
<li>《图解 HTTP》</li>
<li>《HTTPS 权威指南》</li>
<li>《白帽子讲 web 安全》</li>
<li>《MySQL 技术内幕：InnoDB 存储引擎》</li>
<li>《数据库索引设计与优化》</li>
<li>《etcd 技术内幕》</li>
<li>《从 Paxos 到 Zookeeper》</li>
<li>《小岛经济学》</li>
<li>《最后的演讲》</li>
<li>《数据密集型应用系统设计》</li>
<li>《Essential C++》</li>
<li>《Linux 内核观测技术 BPF》</li>
</ul>
]]></description>
</item></channel>
</rss>
