<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>纯纯的 Blog</title>
  <icon>https://blog.zhuangty.com/img/favicon.ico</icon>
  
  <link href="https://blog.zhuangty.com/atom.xml" rel="self"/>
  
  <link href="https://blog.zhuangty.com/"/>
  <updated>2026-02-19T10:33:34.175Z</updated>
  <id>https://blog.zhuangty.com/</id>
  
  <author>
    <name>TennyZhuang</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Agent 时代的 TDD：只关注行为的&quot;残差&quot;</title>
    <link href="https://blog.zhuangty.com/agent-testing-residual/"/>
    <id>https://blog.zhuangty.com/agent-testing-residual/</id>
    <published>2026-01-12T20:00:00.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>测试和实现，永远只改一边。</p></blockquote><h2 id="“交作业”的问题"><a href="#“交作业”的问题" class="headerlink" title="“交作业”的问题"></a>“交作业”的问题</h2><p>目前很多人用 Agent 的手法就是照搬一个”交作业式”的 Agent workflow：</p><ul><li>整理好需求&#x2F;spec，例如”开发一个数据获取框架，要求实现功能 1 2 3 4 5”</li><li>把 spec 发给 Agent 做实现</li><li>审核 Agent 实现的代码</li><li>然后做对比实验&#x2F;回放验证，争取”敢上线”</li><li>一旦发现问题，再回到上一轮循环</li></ul><p>刚开始用的时候体验确实很好——AI 在很短时间里生成了大量代码，看起来还挺符合预期。但很快瓶颈会集中在三件事上：</p><ul><li>Code Review 困难：审查不是自己亲手写的上千行代码非常消耗心力</li><li>缺乏信心：即使 review 完，依然不敢放心上线，还得花大量时间做手动对比实验</li><li>痛苦的返工：一旦在对比中发现问题，又要重新迭代那个开发-Review-测试的循环</li></ul><p>最后一复盘，往往发现整体浪费的时间可能比自己实现更久。要想迭代快，关键不再是更快地产出代码，而是更便宜、更稳定地得到验证反馈。</p><p>软件开发中，瓶颈永远在人的注意力。当 coding 被 Agent 解放后，验证就成了新的卡脖子环节。本文从代码正确性开始：我们如何用低验证成本确保 Agent 产出的代码是正确的、可靠的、符合预期的？</p><h2 id="交替迭代：不再-review-全量代码，只看变化”残差”"><a href="#交替迭代：不再-review-全量代码，只看变化”残差”" class="headerlink" title="交替迭代：不再 review 全量代码，只看变化”残差”"></a>交替迭代：不再 review 全量代码，只看变化”残差”</h2><p>交替迭代的底层逻辑非常简单：既然人的注意力是稀缺的，那我们就不去看代码本身，而去看代码变化的”残差”。实践上表现为，测试和实现永远只改一边。</p><pre><code class=" mermaid">flowchart TD    Start([开始]) --&gt; ModeA&#123;只动测试，不动实现&#125;    ModeA --&gt;|测试失败| FixTest[Agent 自修]    FixTest --&gt; ModeA    ModeA --&gt;|测试通过| ModeB&#123;只动实现，不动测试&#125;    ModeB --&gt;|功能失败| FixImpl[Agent 自修]    FixImpl --&gt; ModeB    ModeB --&gt;|功能通过| Residual[✓ 残差确认]    Residual --&gt; ModeA        style ModeA fill:#e1f5fe    style ModeB fill:#fff3e0    style Residual fill:#e8f5e9</code></pre><p>它把”写代码”拆成两种模式，并让 Agent 在两种模式之间交替切换：</p><ul><li><p>模式 A：只动测试，不动实现。<br>基于当前现有的工程，让 Agent 去完善测试框架、补充测试用例。此时实现代码尽量不动（最多只在接口层做少量必要调整，保证好 review）。<br>如果新增测试跑不过，默认不是”实现错了”，而是测试没准确刻画现有行为——让 Agent 自己把测试修到能稳定反映现状。</p></li><li><p>模式 B：只动实现，不动测试。<br>基于现有测试框架做工程需求&#x2F;性能迭代，目标是在完全不修改测试框架（最多只修改少量必要用例）的情况下实现功能并通过测试。<br>如果跑不过，默认就是新实现有问题——让 Agent 自己修到满足所有测试。</p></li></ul><p>在这个过程中，开发者本人参与的部分依然很少——测试框架是 Agent 写的，测试用例是 Agent 添加的，功能也是 Agent 迭代的。开发者依然只 review 了部分关键测试样例和少量关键实现代码。这并不像传统 TDD 一样需要开发者投入大量精力编写测试，但 review 心力大幅减少，对代码的信心反而增加。</p><p>增益来自哪里？这套方法为工程迭代引入了一个<strong>不动点</strong>：我们可以永远沿用”上一个版本是好用的”这个关键信息来保证工程项目的可靠性和传递性，而开发者需要审核的仅仅是新功能的迭代和优化——也就是”残差”的那部分。</p><p>将该原则具体化为两条约束:</p><ul><li>写测试的时候，保持代码不变（或者只在接口层变动，非常好 review）。如果 Agent 写的测试跑不过，那说明是测试写错了，它就得自己改，直到测试能准确反映现有代码的行为。</li><li>写功能的时候，保持测试框架不变，最多根据功能变更修改少量用例。如果 Agent 写的功能跑不过测试，那说明是新代码有问题，它就得自己改，直到新功能满足所有测试的要求。</li></ul><p>这样，Agent 就不再是一个只会”交作业”的代码生成器，而是一个可以在每个阶段自我修正的”开发者”了。</p><p>接下来问题就变成：这个”不动点”怎么铸造？怎么让它覆盖足够多的情况、又足够稳定，同时还保持能高效迭代？</p><h2 id="确定性与快照：迭代的”不动点”"><a href="#确定性与快照：迭代的”不动点”" class="headerlink" title="确定性与快照：迭代的”不动点”"></a>确定性与快照：迭代的”不动点”</h2><p>上一章我们引入了一个关键概念：不动点。在交替迭代的 workflow 中，我们永远假设一件事成立——上一个版本是好用的，并以此作为下一次迭代的基准。</p><p>要让这个”不动点”真的可用，它至少需要满足两个条件：</p><ul><li>它必须覆盖到足够多的情况；</li><li>它必须在版本之间足够稳定。</li></ul><p>乍一看，这似乎又回到了传统工程里那些熟悉的词汇：分支覆盖率、功能覆盖率、测试完备性……<br>但当测试本身的规模也被 Agent 放大之后，Code Review 又成了新的瓶颈——大量测试即使交给 Agent 编写，人工逐条 review 依然极其消耗心力。于是我们不得不接受一个听起来有点危险的结论：</p><ul><li>不动点并不要求”正确”，它只要求”行为在版本迭代上的连续性”。</li></ul><p>如果测试记录的行为本身就是正确的，那当然更好；但即便它并不完美，只要老版本是可用的，新版本至少不应该出现我们没有意识到的 regression——这就已经把工程迭代的风险压缩进了一个可控范围。</p><p>这个观点在传统的软件工程叙事中并不”政治正确”。我们习惯强调交付结果的可靠性，而”允许存在未完全验证正确性的测试”，听起来像是在”让生产环境帮你测”。但这里的关键不是放弃正确性，而是用廉价的 token 换取功能迭代的行为连续性——正确性有别的手段来保证（后文会讲）。</p><p>在深入之前，我们先要引入两个好用的工具：确定性测试与快照测试</p><h3 id="确定性测试（Deterministic-Testing）"><a href="#确定性测试（Deterministic-Testing）" class="headerlink" title="确定性测试（Deterministic Testing）"></a>确定性测试（Deterministic Testing）</h3><p>确定性测试对这种 workflow 至关重要。我们必须消除测试中的”噪音”：如果输出本来就不稳定，snapshot diff 只会把人逼疯。</p><p>在单线程程序中，这相对简单，主要关注几点：</p><ul><li>固定 random seed</li><li>注意 hashmap 这类无序数据结构的遍历顺序（往往内置了一个 random seed）</li><li>将获取系统时间戳之类的接口”hook”在最外层，作为输入参数传入</li></ul><p>但在多线程程序中，这事儿会变得非常复杂，这里不过多展开，有兴趣的可以看一下 <a href="https://apple.github.io/foundationdb/testing.html">Foundation DB 的文章</a>，或者 madsim、miri 这样的库。</p><h3 id="快照测试（Snapshot-Testing）"><a href="#快照测试（Snapshot-Testing）" class="headerlink" title="快照测试（Snapshot Testing）"></a>快照测试（Snapshot Testing）</h3><p>在保证测试程序有了确定性的输出之后，Snapshot Testing 的作用就是快速地把测试的输出结果 dump 下来并保存到 Git 与测试的结果对齐。快照的输出可以是非常大量的、难以肉眼验证正确性的，比如一整天的服务器日志聚合结果、API 响应数据流、推荐系统产出的排序结果等。</p><p>在 Python 中我们可以非常简单的用 <a href="https://chatgpt.com/s/t_6969b3219c748191b2f7513d1cf1afde">syrupy</a> 无脑引入一堆 Snapshot Testing（已经投入实践）。核心目标是，只要代码上我们关心的行为发生了任何变化——数值异常、顺序变化、类型调整——都一定要影响到输出的结果。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">test_user_events</span>(<span class="hljs-params">snapshot</span>):<br>    df = read_data(<span class="hljs-string">&quot;2025-12-01&quot;</span>, <span class="hljs-string">&quot;user_events&quot;</span>)<br>    <span class="hljs-keyword">assert</span> df == snapshot<br>    <span class="hljs-comment"># 或者嫌弃输出实在太庞大的话</span><br>    <span class="hljs-keyword">assert</span> df.describe() == snapshot<br>    <span class="hljs-keyword">assert</span> df.shape == snapshot<br>    <span class="hljs-keyword">assert</span> df.dtype == snapshot<br>    <span class="hljs-keyword">assert</span> (df.index.<span class="hljs-built_in">min</span>(), df.index.<span class="hljs-built_in">max</span>()) == snapshot<br></code></pre></td></tr></table></figure><h3 id="用快照构建回归测试（Regression-Testing，简称-RT）"><a href="#用快照构建回归测试（Regression-Testing，简称-RT）" class="headerlink" title="用快照构建回归测试（Regression Testing，简称 RT）"></a>用快照构建回归测试（Regression Testing，简称 RT）</h3><p>如何用有限的注意力管理海量的测试？把 Snapshot Testing 作为 RT 的实现方式。</p><p>回归测试由 Agent 大量生成，开发者可以不逐条 review 测试的行为：</p><ul><li>目标不是证明”输出一定正确”，而是把当前版本的行为固化成基线；</li><li>后续任何改动只要让行为发生变化，就一定会在 diff 上暴露；</li><li>人只在 diff 出现时介入，判断”这次变化是否符合预期&#x2F;是否可接受”；</li><li>回归测试消耗的仅仅是 token：让 Agent 多跑、多 dump、多存档；开发者的注意力投入接近 0。</li></ul><p>具体实践模板包括：</p><ul><li>大输出回放的统计快照：对大规模数据集的统计摘要，如日志量、响应时间分布等，snapshot df.shape、dtypes、describe()、分位数等摘要。不需要证明这些摘要”绝对正确”，但只要发生”悄悄变坏”的 drift，diff 一定会抓住它。</li><li>Schema &#x2F; 接口契约快照：对外 API 返回的字段集合、类型、空值约束、错误码等做 snapshot。这类变化通常比数值变化更致命，Code Review 非常适合兜底。</li><li>边界输入的行为连续性：让 Agent 枚举大量 edge cases（空数据、极端区间、乱序、重复 key、缺失字段、非法编码……），记录”输出摘要&#x2F;错误类型&#x2F;日志摘要”的 snapshot。不追求”正确”，只确保行为变化一定会被 diff 提醒，我们再决定要不要把这个 case 升级成核心测试去严格定义正确性。</li></ul><p>RT 的根本目的是让 Agent 从”上一个版本还不错”这个事实中获取信息。通过快照测试，我们将这个信息固化下来，Agent 的每次修改都必须以不破坏这个不动点为前提。如果快照有变动，那么这个变动就是我们所说的”残差”，是需要重点审查的部分。</p><h2 id="核心测试（CT）与回归测试（RT）"><a href="#核心测试（CT）与回归测试（RT）" class="headerlink" title="核心测试（CT）与回归测试（RT）"></a>核心测试（CT）与回归测试（RT）</h2><pre><code class=" mermaid">flowchart LR    subgraph CT[核心测试 CT]        C1[人工挑选关键路径]        C2[输出清晰可控]        C3[人工确认正确性]        C4[消耗注意力 💰]    end        subgraph RT[回归测试 RT]        R1[Agent 批量生成]        R2[固化行为基线]        R3[diff 时介入]        R4[消耗 token 🪙]    end        CT --&gt;|保证| Correct[正确性 ✅]    RT --&gt;|保证| Cont[连续性 🔄]        style CT fill:#ffebee    style RT fill:#e8f5e9    style Correct fill:#fff8e1    style Cont fill:#fff8e1</code></pre><p>通过上文的方法，我们用近乎零注意力成本的方式构建了大量回归测试，但还有一个问题没回答：RT 不关注正确性，那交付质量谁来保证？</p><p>答案很简单：继续保留需要关注正确性的测试，我们称之为核心测试（CT）。CT 就是原来该怎么测试现在还怎么测试：</p><ul><li>人工挑选最值得测试的关键路径&#x2F;关键行为</li><li>输出尽量”小而清晰”</li><li>编写测试可以交给 Agent，但开发者一定要确认 expected output 的正确性</li><li>CT 消耗的关键资源就是注意力：我们要看它”对不对”</li></ul><p>既然 RT 的构建过程完全不消耗注意力，引入 RT 就不会影响我们在 CT 上的资源投入。所以结论是：<strong>CT 保证了不低于传统开发 workflow 的正确性，RT 额外换来了功能迭代的行为连续性。</strong></p><p>另外一个实用的实践：一旦 RT 中的某个 diff 反复出现，说明它测试的行为是经常改动的关键路径，很值得被”提纯”为 core test（补上语义断言 + 人工认可的 expected output）。注意力永远花在刀刃上。</p><h3 id="工程上怎么区分这两类测试？"><a href="#工程上怎么区分这两类测试？" class="headerlink" title="工程上怎么区分这两类测试？"></a>工程上怎么区分这两类测试？</h3><p>实践上不需要很复杂，最朴素的约定就够了，关键是让”review 与否的约定”可执行：</p><ul><li><strong>目录分层</strong>：<ul><li><code>tests/core/</code>：core behavior tests</li><li><code>tests/regression/</code>：regression tests（快照为主）</li></ul></li><li>迭代时两类都跑，但变更的 review 规则不同：<ul><li>core：PR 必须人工确认断言语义&#x2F;expected output；</li><li>RT：PR 不要求逐条阅读 baseline，只在 snapshot diff 出现时人工判断”变更是否合理”。</li></ul></li></ul><h2 id="结语：验得起才是真的快"><a href="#结语：验得起才是真的快" class="headerlink" title="结语：验得起才是真的快"></a>结语：验得起才是真的快</h2><p>Agent 让 coding 带宽变得几乎不稀缺，于是开发的主战场从”写得快”转向了”验得起”。</p><p>很多人会把瓶颈归因到 Code Review：Agent 像风一样产出几千行代码，人当然看不过来。但这更像是误诊——在很多严肃场景里（比如核心业务系统迭代），真正最费时间的往往不是 review，而是为了”敢上线”去做的对比实验、回放验证、以及线上问题的定位成本。验证才是最贵的那一段。</p><p>交替迭代 + 不动点，让 Agent 在边界内自我修正；CT + RT，让正确性和连续性各归各位。token 不是瓶颈，人脑才是——把注意力从”逐行审查”挪到”审查残差”，验证成本才能真正降下来。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;测试和实现，永远只改一边。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;“交作业”的问题&quot;&gt;&lt;a href=&quot;#“交作业”的问题&quot; class=&quot;headerlink&quot; title=&quot;“交作业”的问题&quot;&gt;&lt;/a&gt;“交作业”的问题&lt;/h2&gt;&lt;</summary>
      
    
    
    
    
    <category term="Agent" scheme="https://blog.zhuangty.com/tags/Agent/"/>
    
    <category term="Testing" scheme="https://blog.zhuangty.com/tags/Testing/"/>
    
    <category term="Engineering" scheme="https://blog.zhuangty.com/tags/Engineering/"/>
    
  </entry>
  
  <entry>
    <title>Tech 公司面试杂谈</title>
    <link href="https://blog.zhuangty.com/tech-company-interview/"/>
    <id>https://blog.zhuangty.com/tech-company-interview/</id>
    <published>2024-06-06T22:00:00.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>从大学找实习开始，断断续续面试、被面试了许多场，想来应该有三位数了，最近又作为 Senior&#x2F;Leader 岗位面了一些公司，算是补全了一块人生体验，写篇文章谈谈 Tech 公司面试的感想。</p><h2 id="算法"><a href="#算法" class="headerlink" title="算法"></a>算法</h2><p>谈到算法面试，首先想到的是 <a href="https://leetcode.com/">LeetCode</a>，这不是我想的，是 Copilot 想的。</p><p><img src="https://s2.loli.net/2024/06/06/8VIokXsr6jpaqzF.png"></p><p>算法是讨论到面试绕不开的话题，我懒得考古，但这股“歪风”应该是从硅谷几家 Big-name Tech 带起来的。这个选择是非常明智的，算法竞赛作为面试的核心部分有非常多的好处：</p><ul><li>形式上标准化，可以避免很多不必要的争议（但也只在形式上）</li><li>题库非常大以至于不可能全靠死记硬背。通过面试的候选人下限有保证，具备能写 CRUD 的基础 coding 能力，有行动力且愿意花时间刷题，具有一个<strong>螺丝钉</strong>必要的品质</li><li>面试官完全不需要提前准备，在题库中随机 Pick 一道题就能面试，减少公司面试的成本</li></ul><p>印象最深的是我校招时 Airbnb 北京的面试，面试官甚至不会给时间自我介绍。任何关于项目的沟通都必须在解完题目之后。那天有一面我状态不好，以至于我和面试官没有哪怕一分钟的时间交流题目外的内容。</p><p>但当将算法面试应用于 Senior 招聘的时候，事情就变得抽象了起来，<strong>算法面试事实上衡量的是一个人算法能力相对大学的退化程度</strong>。最能做算法题的人，一大半是现役高中生（或者本科生），而剩下的一小半，是曾经的最强算法高中生。90% 的 Senior 工程师，算法做不过 Junior 时候的自己，这是一个应该考察人成长的阶段，但是结果是人吃老本的水平，很难说是一个有效面试。这个道理曾经的我也不懂，刚毕业在旷视工作的时候 Leader 经常让我作为一面考察一些 Senior 的 coding，我常常讶异于他们为什么算法那么菜，直到最近我“接雨水”写了四十分钟才写出来 :(。</p><p>一个曾经我很认同的观点是，做 Infrastructure 对算法能力的要求更高，相比 CRUD 来说，各个系统都需要用到一些复杂的算法，因此，在这种情况下，算法面试就变得尤为重要。这个观点的前提是对的，但结论是错的。Infra 需要用到的算法很难，特别是在 RisingWave 工作的这段时间，许多常用算子的增量算法往往都要想不少时间。但这在整个 feature 落地的过程中无论是消耗的时间还是复杂度都占比不到 10%。同时，在 30-40 分钟里切题的能力和有充分的时间去思考工程上真正需要解决的问题和方案，中间也存在着巨大的 gap。</p><h2 id="System-Design"><a href="#System-Design" class="headerlink" title="System Design"></a>System Design</h2><p>System Design 作为面试形式的上下限差异极大，事实上，对面试官提出了很高的要求。面试官必须有丰富的系统设计经验，且本身对题目已经有非常完善的思考了，甚至最好是项目上真实遇到的需求。System Design 的答案本身就是开放性的，更多的是对交流过程中思路的考察，互相之间的启发，而不仅仅是对答案。在之前的一场面试中，我遇到了一道性能优化的题目，它的背景大约是使用一台 8GB RAM 和 1TB 硬盘的服务器，优化某种场景的查询。而当我追问硬盘是什么类型，是 SSD 或者 HDD，读写性质是什么样的，面试官的回答是不知道。我猜测这不在那道题的考察点上，但在我看来这种去匹配答案的形式完全不适用于 System Design 的考察形式。</p><p>如果像算法题一样，面试官仅仅是从题库中 Pick 一道题并阅览一下参考答案就去面候选人，那么最终的面试效果甚至还不如算法题。一方面，候选人很多合理的设计思路会因为面试官的短视而被否定，另一方面，由于 System Design 的题库相比算法题要小很多，如果面试官不会做任何的变通，很容易被刷穿题库的候选人轻松拿到 Strong yes，而实际上完全无法达到水平考察的目的。</p><h2 id="“共享面试”"><a href="#“共享面试”" class="headerlink" title="“共享面试”"></a>“共享面试”</h2><p>“共享面试”是一个不存在的形式，确实一个客观存在的概念。在你投一家公司的第一天，HR 就会问你现在的总包作为参考，有些公司甚至会有一些离谱的规定，比如不允许给候选人涨幅超过一个固定比例。同时，HR 们也会互相追问友商给你的工资（尽管他们还会同时你 Offer 需要保密），作为一个基数来决定或者调整 offer。</p><p>这在很大程度上，就是因为一家公司三到四轮的面试太短，以及可能形式上存在上面所说的种种问题，很难真正地确定候选人的能力和匹配程度，因此借用了候选人的上家，以及其他认可的友商的面试成果，一种事实上的“共享面试”出现了。</p><p><strong>而这个明面上不存在的面试形式，却是决定候选人最终 offer 的关键因素。</strong></p><p>乍一看，“共享面试”并不是一个糟糕的 idea，解决了一家公司能占用候选人的时间过短的问题，从概率意义上增加了候选人匹配的程度，然而这是一种非常低效且不确定的形式。每家公司的面试方式是高度雷同的，相当于把在与 GPT 的对话中 temperature 调得很低然后多次询问试图提高结果的准确性，实际上的效果非常存疑。</p><h2 id="Assignment"><a href="#Assignment" class="headerlink" title="Assignment"></a>Assignment</h2><p>这是一种一直不温不火的面试形式，不火的原因是占用候选人过多时间，太容易被候选人挂上脉脉，被骂剽窃候选人的劳动力。这里也分两种，一种是将公司内项目的一些任务提炼一下得到一份脱敏版本，作为候选人的 assignment。另一种则是一些开源项目的商业公司，直接将 issue assign 给候选人，然后在 review 的同时对候选人能力做一个评估，顺便清理一些项目里低优先级的 backlog issue。</p><p>相比于 Assignment 本身，我更认可 Co-work 的面试方式。在这个任务的完成过程中，面试官也应该花费同等的时间，与候选人协作，通过保持连线甚至直接使用一些协作编辑的方式，共同完成这个 feature。在这个过程中，观察候选人习惯的阅读、编码、调试、解决问题等的方式；当候选人遇到一些项目本身的不足带来的困难时也及时给予指正，免得浪费无谓的时间。观察候选人是否能用合适的方式与同事沟通（例如请求是否足够清晰，让被求助人最快理解问题），获取帮助，也是工作内容中相当重要的一部分。</p><p>除此之外，作为候选人，在一些 Assignment 的完成过程中，我遇到的更多问题往往是 quick start 文档缺失、调试困难、一些配置项隐藏过深等。完整的审视一个新人参与项目中遇到的困难，对拥抱开源的项目来说也是很好的补充。</p><h2 id="Show-your-contribution"><a href="#Show-your-contribution" class="headerlink" title="Show your contribution"></a>Show your contribution</h2><p>面试始终是对工作内容的抽样调查，无论如何高效地利用，想在若干小时内对一个人的工作能力全面了解都是不可能的。这就是给开源项目打工的重要魅力之一。一个好的 GitHub 主页，是对个人能力的最好展示。我作为面试官时，如果候选人贴了 GitHub 链接且有一些个人项目或者对重要项目有贡献，我都会挑一些我比较感兴趣的 PR，真正地从一个 reviewer 的视角去过一遍。如果说那么好的面试就可以对候选人的工作能力了解达到 20% 以上，那候选人如果有开源的全职工作经历，了解程度就能达到 60% 以上。</p><h2 id="不要吝惜时间"><a href="#不要吝惜时间" class="headerlink" title="不要吝惜时间"></a>不要吝惜时间</h2><p>从一个候选人的视角来说，对于真正感兴趣的公司，我并不介意面试花费更多的时间去做一个自我证明。我反而会觉得只通过写两道题来了解我是相当可惜的，有很多信息可以互相沟通了解，而不希望最终拿到好 offer 的原因是另一个好的友商给了一个不错的 compete offer。</p><p>从公司的视角来说，用更少的成本快速找到合格的螺丝钉固然是 Big-name tech 的核心需求，却并不适用于大部分公司，让面试官在一个候选人身上多花费一些成本来提高匹配程度，对很多公司来说是很划算的。</p><p>这里可以看出面试官在面试过程中的重要程度，然而好的面试无论如何都需要面试官花费更多的时间去准备，从公司层面再细分来说，更详尽的面试对 Leader 来说自然是利益趋同的，但对普通的技术面试官来说，就未必愿意占用自己完成工作的时间来准备。这也是为什么标准化面试受欢迎的一个原因。如何设计对面试官的反馈机制是一个难题，这篇文章作为一篇碎碎念就不赘述了，等我理解加深的时候再来补充。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>面试形式与效果是非常多的 big tech、独角兽思考过的难题，同时它也像 system design 一样，是真正的开放性难题，根据不同的场景、需求自然会有不同的结论，所以这篇碎碎念没有答案，只是剖析一下现有的面试形式作为一些个人思考，也希望无论作为面试官还是候选人都能从中找到一些共鸣。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;从大学找实习开始，断断续续面试、被面试了许多场，想来应该有三位数了，最近又作为 Senior&amp;#x2F;Leader 岗位面了一些公司，算是补全了一块人生体验，写篇文章谈谈 Tech 公司面试的感想。&lt;/p&gt;
&lt;h2 id=&quot;算法&quot;&gt;&lt;a href=&quot;#算法&quot; class=</summary>
      
    
    
    
    
    <category term="Interview" scheme="https://blog.zhuangty.com/tags/Interview/"/>
    
    <category term="Non-tech" scheme="https://blog.zhuangty.com/tags/Non-tech/"/>
    
  </entry>
  
  <entry>
    <title>Design a collection with compile-time reference stability in Rust (2)</title>
    <link href="https://blog.zhuangty.com/ref-stable-lru-2/"/>
    <id>https://blog.zhuangty.com/ref-stable-lru-2/</id>
    <published>2024-01-31T18:00:00.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>在<a href="https://blog.zhuangty.com/ref-stable-lru/">上一篇文章</a>中，我们设计了一个允许同时持有多个不可变引用的 <code>LruCache</code>。唯一的问题是，为了足够安全，API 不太易用。这篇文章完美地解决了问题。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">new_lru_cache</span>(|<span class="hljs-keyword">mut</span> perm, <span class="hljs-keyword">mut</span> cache| &#123;<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-string">&quot;c&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;c&quot;</span>, <span class="hljs-string">&quot;d&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    [x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br><br>    (perm, cache)<br>&#125;);<br></code></pre></td></tr></table></figure><p>这里的主要问题是，<code>LruCache</code> 必须通过一个 closure 创建，且携带了一个 <code>&#39;brand</code> 生命周期，如果想存在某个 ADT 里，会将生命周期泛型向上传染。</p><p>在我的朋友的提示下，我发现我们可以将 <code>LruCache</code> 的数据和对它的操作分离，为此，我设计了一套新的 <a href="https://github.com/TennyZhuang/ref-stable-lru/pull/1/files">scope API</a>。</p><p>首先，在原本的代码之上，我们将 <code>&#39;brand</code> 从 <code>LruCache</code> 上删除，然后引入一个新的结构体 <code>CacheHandle</code>：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">LruCache</span>&lt;K, V&gt; &#123;<br>    _marker: PhantomData&lt;(K, V)&gt;,<br>&#125;<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">CacheHandle</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;brand</span>, K, V&gt; &#123;<br>    _lifetime: InvariantLifetime&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;,<br>    cache: &amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">mut</span> LruCache&lt;K, V&gt;,<br>&#125;<br></code></pre></td></tr></table></figure><p>很显然，<code>LruCache</code> 可以通过常规的方式构建：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span> <span class="hljs-title class_">LruCache</span> &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new</span>(cap: <span class="hljs-type">usize</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-keyword">Self</span> &#123; todo!() &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这里 <code>CacheHandle</code> 替代了我们上一篇文章中 <code>LruCache</code> 的职责，需要与 <code>ValuePerm</code> 深度绑定，为此我们引入了 <code>LruCache::scope</code> API，替代了原来的 <code>new_lru_cache</code>。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;K, V&gt; LruCache&lt;K, V&gt;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">scope</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, F, R&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, fun: F) <span class="hljs-punctuation">-&gt;</span> R<br>    <span class="hljs-keyword">where</span><br>        <span class="hljs-keyword">for</span>&lt;<span class="hljs-symbol">&#x27;brand</span>&gt; F: <span class="hljs-title function_ invoke__">FnOnce</span>(CacheHandle&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;brand</span>, K, V&gt;, ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;) <span class="hljs-punctuation">-&gt;</span> R,<br>    &#123;<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">handle</span> = CacheHandle &#123;<br>            _lifetime: <span class="hljs-built_in">Default</span>::<span class="hljs-title function_ invoke__">default</span>(),<br>            cache: <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">into</span>(),<br>        &#125;;<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">perm</span> = ValuePerm &#123;<br>            _lifetime: InvariantLifetime::<span class="hljs-title function_ invoke__">default</span>(),<br>        &#125;;<br>        <span class="hljs-title function_ invoke__">fun</span>(handle, perm)<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这个函数接受的是 <code>&amp;mut LruCache</code>，保证了同一时间只有一个 <code>handle</code>进行操作。</p><p>这里的回调签名 <code>for&lt;&#39;brand&gt; F: FnOnce(CacheHandle&lt;&#39;cache, &#39;brand, K, V&gt;, ValuePerm&lt;&#39;brand&gt;) -&gt; R</code>，对比于上一篇文章，这里不再需要把 <code>handle</code> 和 <code>perm</code> 返回，因为 <code>CacheHandle::cache</code> 仅仅是对 <code>LruCache</code> 的可变引用，即使对 <code>handle</code> 提前 <code>drop</code>，scope 内对 value 的引用仍然是合法的。</p><p>我们将上一篇文章中的方法分别实现在 <code>CacheHandle</code> 上：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;brand</span>, K: Hash + <span class="hljs-built_in">Eq</span>, V&gt; CacheHandle&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;brand</span>, K, V&gt; &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">put</span>&lt;<span class="hljs-symbol">&#x27;handle</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(<br>        &amp;<span class="hljs-symbol">&#x27;handle</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br>        k: K,<br>        <span class="hljs-keyword">mut</span> v: V,<br>        _perm: &amp;<span class="hljs-symbol">&#x27;perm</span> <span class="hljs-keyword">mut</span> ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;<br>    ) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;V&gt; &#123; todo!() &#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;handle</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(<br>        &amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>,<br>        k: &amp;K,<br>        _perm: &amp;<span class="hljs-symbol">&#x27;perm</span> ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;,<br>    ) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;perm</span> V&gt; &#123; todo!() &#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">peek_mut</span>&lt;<span class="hljs-symbol">&#x27;handle</span>, <span class="hljs-symbol">&#x27;key</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(<br>        &amp;<span class="hljs-symbol">&#x27;handle</span> <span class="hljs-keyword">self</span>,<br>        k: &amp;<span class="hljs-symbol">&#x27;key</span> K,<br>        _perm: &amp;<span class="hljs-symbol">&#x27;perm</span> <span class="hljs-keyword">mut</span> ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;,<br>    ) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;perm</span> <span class="hljs-keyword">mut</span> V&gt; &#123; todo!() &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这些 API 的正确性，上一篇文章已经论述过，这里不再重复，有了这些 API 以后，我们就可以这样用我们的 LruCache。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">cache</span> = LruCache::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-number">2</span>);<br>cache.<span class="hljs-title function_ invoke__">scope</span>(|<span class="hljs-keyword">mut</span> cache, <span class="hljs-keyword">mut</span> perm| &#123;<br>    <span class="hljs-built_in">assert_eq!</span>(cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;apple&quot;</span>, <span class="hljs-string">&quot;red&quot;</span>, &amp;<span class="hljs-keyword">mut</span> perm), <span class="hljs-literal">None</span>);<br>    <span class="hljs-built_in">assert_eq!</span>(cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;banana&quot;</span>, <span class="hljs-string">&quot;yellow&quot;</span>, &amp;<span class="hljs-keyword">mut</span> perm), <span class="hljs-literal">None</span>);<br>    <span class="hljs-built_in">assert_eq!</span>(cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;lemon&quot;</span>, <span class="hljs-string">&quot;yellow&quot;</span>, &amp;<span class="hljs-keyword">mut</span> perm), <span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-string">&quot;red&quot;</span>));<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">colors</span>: <span class="hljs-type">Vec</span>&lt;_&gt; = [<span class="hljs-string">&quot;apple&quot;</span>, <span class="hljs-string">&quot;banana&quot;</span>, <span class="hljs-string">&quot;lemon&quot;</span>, <span class="hljs-string">&quot;watermelon&quot;</span>]<br>        .<span class="hljs-title function_ invoke__">iter</span>()<br>        .<span class="hljs-title function_ invoke__">map</span>(|k| cache.<span class="hljs-title function_ invoke__">get</span>(k, &amp;perm))<br>        .<span class="hljs-title function_ invoke__">collect</span>();<br>    <span class="hljs-built_in">assert!</span>(colors[<span class="hljs-number">0</span>].<span class="hljs-title function_ invoke__">is_none</span>());<br>    <span class="hljs-title function_ invoke__">assert_opt_eq</span>(colors[<span class="hljs-number">1</span>], <span class="hljs-string">&quot;yellow&quot;</span>);<br>    <span class="hljs-title function_ invoke__">assert_opt_eq</span>(colors[<span class="hljs-number">2</span>], <span class="hljs-string">&quot;yellow&quot;</span>);<br>    <span class="hljs-built_in">assert!</span>(colors[<span class="hljs-number">3</span>].<span class="hljs-title function_ invoke__">is_none</span>());<br>&#125;);<br></code></pre></td></tr></table></figure><p>由于 <code>cache</code> 现在是 Owned type，不借用任何生命周期，我们可以非常自由地将它存在任何地方。而只需要在修改的时候创建一个 scope 即可。</p><p>更近一步的，我们甚至可以将 <code>LruCache</code> 原来的方法添加回来，当我们不需要 reference stability 时，可以不需要引入额外的代码复杂度。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;K, V&gt; LruCache&lt;K, V&gt; &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">put</span>(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: K, v: V) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;V&gt; &#123;<br>        <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">scope</span>(|<span class="hljs-keyword">mut</span> cache, <span class="hljs-keyword">mut</span> perm| cache.<span class="hljs-title function_ invoke__">put</span>(k, v, &amp;<span class="hljs-keyword">mut</span> perm))<br>    &#125;<br><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;cache</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: &amp;K) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;cache</span> V&gt; &#123;<br>        <span class="hljs-comment">// SAFETY: We actually hold `&amp;&#x27;cache mut self` here, so the only reference should always be valid.</span><br>        <span class="hljs-comment">// We can extend its lifetime to the cache easily.</span><br>        <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">scope</span>(|<span class="hljs-keyword">mut</span> cache, perm| <span class="hljs-keyword">unsafe</span> &#123;<br>            std::mem::transmute::&lt;_, <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;cache</span> V&gt;&gt;(cache.<span class="hljs-title function_ invoke__">get</span>(k, &amp;perm))<br>        &#125;)<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们可以像正常的 collection API 一样来使用它：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">cache</span> = LruCache::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-number">2</span>);<br>cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;apple&quot;</span>, <span class="hljs-string">&quot;red&quot;</span>, &amp;<span class="hljs-keyword">mut</span> perm);<br><span class="hljs-keyword">let</span> <span class="hljs-variable">data</span> = cache.<span class="hljs-title function_ invoke__">get</span>(<span class="hljs-string">&quot;apple&quot;</span>);<br><span class="hljs-comment">// We can&#x27;t call `get` twice when `data` reference is still valid.</span><br><span class="hljs-comment">// cache.get(&quot;lemon&quot;);</span><br>dbg!(data);<br></code></pre></td></tr></table></figure><p>我添加了<a href="https://github.com/TennyZhuang/ref-stable-lru/pull/2/files">一大堆 UI Test</a>，来覆盖各种使用场景和应该编译失败的场景，感兴趣的朋友可以看一下这些 test case。</p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>上一篇文章的末尾，我们提到了这套 API 较差的易用性可能限制了它的应用空间，那么这篇文章的改进在我看来，如果可以证明其 soundness，甚至达到了可以合并到标准库的程度（我会尝试给标准库的 <code>LinkedList</code>、<code>VecDeque</code> 等提 Reference Stability 的 RFC）。这个将数据和操作分离的 API 改进，我们只需要在原本的数据结构上单独添加一个 <code>scope</code> API，并且在对应的 closure 内遵循权限分离的设计。而在不需要 reference stability 的场景，我们不会引入任何额外的代码复杂度，真正地做到了 “pay as you needed”，这非常符合 Rust 的设计哲学。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在&lt;a href=&quot;https://blog.zhuangty.com/ref-stable-lru/&quot;&gt;上一篇文章&lt;/a&gt;中，我们设计了一个允许同时持有多个不可变引用的 &lt;code&gt;LruCache&lt;/code&gt;。唯一的问题是，为了足够安全，API 不太易用。这篇文章完美</summary>
      
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Rust" scheme="https://blog.zhuangty.com/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>Design a collection with compile-time reference stability in Rust (1)</title>
    <link href="https://blog.zhuangty.com/ref-stable-lru/"/>
    <id>https://blog.zhuangty.com/ref-stable-lru/</id>
    <published>2024-01-26T19:03:00.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>LRU Cache 是工业界最常用的数据结构之一，而最简单的实现方式是基于 HashMap 和链表。当访问某个 entry 时，这个 entry 会被移到链表的最前端。</p><p><img src="https://s2.loli.net/2024/01/26/hQNLxjarMkGIi4g.png" alt="LRU-Cache"></p><p>Rust 有个 crate <a href="https://crates.io/crates/lru">lru</a> 实现了这个数据结构，这里摘取了几个关键方法：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;K: <span class="hljs-built_in">Eq</span> + Hash, V&gt; LruCache&lt;K, V&gt; &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new</span>(cap: <span class="hljs-type">usize</span>) <span class="hljs-punctuation">-&gt;</span> LruCache&lt;K, V&gt; &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">len</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">usize</span> &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">put</span>(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: K, v: V) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;V&gt; &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;a</span>&gt;(&amp;<span class="hljs-symbol">&#x27;a</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: &amp;K) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;a</span> V&gt; &#123;todo!()&#125;<br>    <span class="hljs-comment">// get the mutable reference of an entry, but not adjust its position.</span><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">peek_mut</span>&lt;<span class="hljs-symbol">&#x27;a</span>&gt;(&amp;<span class="hljs-symbol">&#x27;a</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: &amp;K) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;a</span> <span class="hljs-keyword">mut</span> V&gt; &#123;todo!()&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>相比于 <a href="https://crates.io/crates/lru">lru</a>，这里对函数签名做了一些简化，省去了与 <code>Borrow</code> trait 有关的优化。</p><p>可以看到，与常规数据结构不同的是，这里的 <code>get</code> 方法，也需要接受 <code>&amp;mut self</code>，这给使用者带来很多困扰。例如我们可能希望同时持有多个 <code>V</code> 的不可变引用，这可以允许我们减少不必要的 copy，或者并发地使用他们。</p><span id="more"></span><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>[x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br></code></pre></td></tr></table></figure><p>我们会得到如下报错：</p><figure class="highlight maxima"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs maxima"><span class="hljs-built_in">error</span>[E0499]: cannot borrow `cache` as mutable more than once <span class="hljs-built_in">at</span> a <span class="hljs-built_in">time</span><br>   |<br><span class="hljs-number">20</span> |     <span class="hljs-built_in">let</span> x = cache.<span class="hljs-built_in">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>).unwrap().as_str();<br>   |             ----- <span class="hljs-built_in">first</span> mutable borrow occurs here<br><span class="hljs-number">21</span> |     <span class="hljs-built_in">let</span> y = cache.<span class="hljs-built_in">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>).unwrap().as_str();<br><span class="hljs-number">22</span> |     <span class="hljs-built_in">let</span> z = cache.<span class="hljs-built_in">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>).unwrap().as_str();<br>   |             ^^^^^ <span class="hljs-built_in">second</span> mutable borrow occurs here<br><span class="hljs-number">23</span> |     [x, y, z].<span class="hljs-built_in">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br>   |      - <span class="hljs-built_in">first</span> borrow later used here<br></code></pre></td></tr></table></figure><p>很显然，<code>LruCache::get</code> 使用了 <code>&amp;mut cache</code> 导致 <code>x</code> 已经占有了 <code>cache</code> 的可变引用，而后面尝试创建 <code>y</code>、<code>z</code> 多个 <code>&amp;mut cache</code> 同时存在违背了 Rust 的 borrow checker。</p><p>那么 Borrowck 认为是错的，就是错的吗？如果我们再看一眼上图演示的 <code>LruCache</code> 在 <code>get</code> 过程中的调整，我们会发现这个限制是完全没有道理的。<code>get</code> 确实会调整 <code>LruCache</code> 的结构，但并不会影响 <code>val</code> 的指针，第二次 <code>get</code> 完全不会让第一个 <code>get</code> 获得的 <code>&amp;V</code> 失效。</p><p>那么反正实现是 unsafe 的，我们可以将 <code>get</code> 改成接受 <code>&amp;self</code> 吗？</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;a</span>&gt;(&amp;<span class="hljs-symbol">&#x27;a</span> <span class="hljs-keyword">self</span>, k: &amp;K) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;a</span> V&gt; &#123;todo!()&#125;<br></code></pre></td></tr></table></figure><p>这显然是错的，接受 <code>&amp;self</code> 意味着我们可以并发调用 <code>get</code> 方法，链表就会被彻底破坏导致 UB，除非我们使用 <code>Arc&lt;Mutex&lt;_&gt;&gt;</code> &#x2F; <code>AtomicPtr</code> 等同步原语保护链表节点引入不必要的 overhead，或者将 <code>LruCache</code> 标记为 <code>!Sync</code>，这与我们原本的目的背道而驰了。</p><p>我们希望有一种接口设计，它不允许我们并发调用 <code>get</code> 方法，但 <code>get</code> 返回的 <code>&amp;V</code> 不独占整个 cache，从而允许我们合法地同时持有多个 <code>&amp;V</code>.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;key</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">self</span>, k: &amp;<span class="hljs-symbol">&#x27;key</span> K) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-string">&#x27;? V&gt; &#123;todo!()&#125;</span><br></code></pre></td></tr></table></figure><p>这个函数目前有两个生命周期参数，一个是对 cache 的引用 <code>&#39;cache</code>（前文中的 <code>&#39;a</code>） ，另一个则是完全无关，甚至可能非常短的 <code>&#39;key</code> （在前文中我们利用 rust 的规则省略了这个参数）。这两个都不可能描述我们返回值的生命周期，我们需要一个更好的生命周期参数，基于这个思路，我们尝试引入了一个新的生命周期：<code>&#39;token</code>。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Token</span>;<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;key</span>, <span class="hljs-symbol">&#x27;token</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">self</span>, k: &amp;<span class="hljs-symbol">&#x27;key</span> K, token: &amp;<span class="hljs-symbol">&#x27;token</span> Token) <span class="hljs-punctuation">-&gt;</span> &amp;<span class="hljs-symbol">&#x27;token</span> V &#123; todo!() &#125;<br></code></pre></td></tr></table></figure><p>这样我们之前同时持有多个 <code>&amp;V</code> 的代码就能编译通过了！Happy Ending？</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-variable">token</span> = Token;<br><span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>());<br>[x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br></code></pre></td></tr></table></figure><p>我们很快发现在修改了 <code>get</code> 的签名后，我们在 safe rust 中，在 <code>&amp;V</code> 仍然合法时调用 <code>put</code>。这个方法会覆盖现有的 value，或者淘汰最旧的 entry。被覆盖的 entry 会在返回后被 drop。在此之后我们继续访问 <code>x</code> 就会导致 UB。办法也非常简单，我们只需要修改 <code>put</code> 的签名即可。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">put</span>&lt;<span class="hljs-symbol">&#x27;cache</span>,<span class="hljs-symbol">&#x27;token</span>&gt;(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: K, v: V, token: &amp;<span class="hljs-symbol">&#x27;token</span> <span class="hljs-keyword">mut</span> Token) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;V&gt; &#123;todo!()&#125;<br></code></pre></td></tr></table></figure><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br><span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;token).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>, &amp;<span class="hljs-keyword">mut</span> cache, &amp;<span class="hljs-keyword">mut</span> token);<br>[x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br></code></pre></td></tr></table></figure><p>由于 put 需要 &amp;mut Token 作为参数，而 <code>&amp;Token</code> 已经被 <code>x</code>&#x2F;<code>y</code>&#x2F;<code>z</code> 不可变引用了，这里会编译失败。</p><p>现在我们可以给 <code>Token</code> 一个更好的名字了，不难发现，<code>Token</code> 其实是对 value 本身的读写权限。我们将它重命名为 <code>ValuePerm</code>。</p><ul><li>持有 <code>&amp;self</code>，代表对 <code>LruCache</code> 的结构有读权限。</li><li>持有 <code>&amp;mut self</code>，代表对 <code>LruCache</code> 的结构的结构有写权限。</li><li>持有 <code>&amp;perm</code>，代表对 <code>LruCache</code> 的 <code>value</code> 有读权限。</li><li>持有 <code>&amp;mut perm</code>，代表对 <code>LruCache</code> 的 <code>value</code> 有写权限。</li></ul><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;K: <span class="hljs-built_in">Eq</span> + Hash, V&gt; LruCache&lt;K, V&gt; &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new</span>(cap: <span class="hljs-type">usize</span>) <span class="hljs-punctuation">-&gt;</span> LruCache&lt;K, V&gt; &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">len</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">usize</span> &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">put</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: K, v: V, perm: &amp;<span class="hljs-symbol">&#x27;perm</span> <span class="hljs-keyword">mut</span> ValuePerm) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;V&gt; &#123;todo!()&#125;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, k: &amp;K, perm: &amp;<span class="hljs-symbol">&#x27;perm</span> ValuePerm) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;perm</span> V&gt; &#123;todo!()&#125;<br>    <span class="hljs-comment">// get the mutable reference of an entry, but not adjust its position.</span><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">peek_mut</span>&lt;<span class="hljs-symbol">&#x27;cache</span>, <span class="hljs-symbol">&#x27;perm</span>&gt;(&amp;<span class="hljs-symbol">&#x27;cache</span> <span class="hljs-keyword">self</span>, k: &amp;K, perm: &amp;<span class="hljs-symbol">&#x27;perm</span> ValuePerm) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Option</span>&lt;&amp;<span class="hljs-symbol">&#x27;perm</span> <span class="hljs-keyword">mut</span> V&gt; &#123;todo!()&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们选取的函数是非常典型的四种用例。一个意外的收获是，由于 <code>peek_mut</code> 不持有 <code>&amp;mut self</code>，它返回的引用可以和 <code>len</code> 同时调用，虽然这可能是个没什么用的 feature。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-variable">val_mut</span> = cache.<span class="hljs-title function_ invoke__">peek_mut</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;<span class="hljs-keyword">mut</span> perm);<br><span class="hljs-keyword">let</span> <span class="hljs-variable">len</span> = cache.<span class="hljs-title function_ invoke__">len</span>();<br>dbg!(val_mut);<br></code></pre></td></tr></table></figure><p>下一个要解决的问题是唯一性。很显然，一个 <code>LruCache</code> 只能被一个 <code>Token</code> 操纵，这里我借鉴了 <a href="https://crates.io/crates/ghost-cell">Ghost Cell</a> 和 <a href="https://doc.rust-lang.org/std/thread/fn.scope.html"><code>std::thread::scope</code></a> 的思路。利用 invariant lifetime 构造了一个唯一的 ID。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">type</span> <span class="hljs-title class_">InvariantLifetime</span>&lt;<span class="hljs-symbol">&#x27;brand</span>&gt; = PhantomData&lt;<span class="hljs-title function_ invoke__">fn</span>(&amp;<span class="hljs-symbol">&#x27;brand</span> ()) <span class="hljs-punctuation">-&gt;</span> &amp;<span class="hljs-symbol">&#x27;brand</span> ()&gt;;<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">ValuePerm</span>&lt;<span class="hljs-symbol">&#x27;brand</span>&gt; &#123;<br>    _lifetime: InvariantLifetime&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;,<br>&#125;<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">LruCache</span>&lt;<span class="hljs-symbol">&#x27;brand</span>, K, V&gt; &#123;<br>    _lifetime: InvariantLifetime&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;,<br>    _marker: PhantomData&lt;(K, V)&gt;,<br>&#125;<br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-symbol">&#x27;brand</span>, K: <span class="hljs-built_in">Eq</span> + Hash, V&gt; LruCache&lt;<span class="hljs-symbol">&#x27;brand</span>, K, V&gt; &#123;<br>   <span class="hljs-comment">// ...</span><br>&#125;<br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new_lru_cache</span>&lt;K, V, F&gt;(fun: F)<br><span class="hljs-keyword">where</span><br>    F: <span class="hljs-keyword">for</span>&lt;<span class="hljs-symbol">&#x27;brand</span>&gt; <span class="hljs-title function_ invoke__">FnOnce</span>(ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;, LruCache&lt;<span class="hljs-symbol">&#x27;brand</span>, K, V&gt;),<br>&#123;<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">perm</span> = ValuePerm &#123;<br>        _lifetime: InvariantLifetime::<span class="hljs-title function_ invoke__">default</span>(),<br>    &#125;;<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">cache</span> = LruCache::&lt;K, V&gt; &#123;<br>        _lifetime: <span class="hljs-built_in">Default</span>::<span class="hljs-title function_ invoke__">default</span>(),<br>        _marker: <span class="hljs-built_in">Default</span>::<span class="hljs-title function_ invoke__">default</span>(),<br>    &#125;;<br>    <span class="hljs-title function_ invoke__">fun</span>(perm, cache);<br>&#125;<br></code></pre></td></tr></table></figure><p>我们移除了 <code>LruCache::new</code> 方法，强制通过 <code>new_lru_cache</code> 创造一个 scope，并且在 scope 内使用。<code>LruCache</code> 和 <code>ValuePerm</code> 共享一个唯一的生命周期作为 ID。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">new_lru_cache</span>(|<span class="hljs-keyword">mut</span> perm, <span class="hljs-keyword">mut</span> cache| &#123;<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-string">&quot;c&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;c&quot;</span>, <span class="hljs-string">&quot;d&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    [x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br>&#125;);<br></code></pre></td></tr></table></figure><p>实现到这里的时候，我发现我忘了一个非常致命的问题，我可以修改所有的方法来接受 <code>&amp;mut Perm</code>，但没法修改 <code>Drop</code> ，这可能导致 <code>cache</code> 早于 <code>&amp;V</code> 被释放。而使用 <code>&#39;cache: &#39;perm</code> 作为 constraint 会导致 <code>&amp;V</code> 再次被 <code>&amp;&#39;cache mut self</code> 限制。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">new_lru_cache</span>(|<span class="hljs-keyword">mut</span> perm, <span class="hljs-keyword">mut</span> cache| &#123;<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-string">&quot;c&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;c&quot;</span>, <span class="hljs-string">&quot;d&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-title function_ invoke__">drop</span>(cache);<br>    [x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>); <span class="hljs-comment">// Boom</span><br>&#125;);<br></code></pre></td></tr></table></figure><p>如果 Rust 有 <a href="https://blog.yoshuawuyts.com/linearity-and-control/">linear type</a> 支持的话，我们可以阻止 <code>LruCache</code> 被 drop，必须通过类似 <code>consume(self, &amp;mut ValuePerm)</code> 之类的方法来销毁，很遗憾的是 linear type 属于有生之年系列，因此我这里用了另一个 workaround：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new_lru_cache</span>&lt;K, V, F&gt;(fun: F)<br><span class="hljs-keyword">where</span><br>    F: <span class="hljs-keyword">for</span>&lt;<span class="hljs-symbol">&#x27;brand</span>&gt; <span class="hljs-title function_ invoke__">FnOnce</span>(ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>&gt;, LruCache&lt;<span class="hljs-symbol">&#x27;brand</span>, K, V&gt;) <span class="hljs-punctuation">-&gt;</span> (ValuePerm&lt;<span class="hljs-symbol">&#x27;brand</span>, LruCache&lt;<span class="hljs-symbol">&#x27;brand</span>, K, V&gt;&gt;),<br></code></pre></td></tr></table></figure><p>这里要求 <code>new_lru_cache</code> 接受的回调必须将 <code>perm</code> 和 <code>value</code> 的所有权返回再统一 drop。由于 <code>&#39;brand</code> 的唯一性，回调必须将变量返回而无法提前销毁。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">new_lru_cache</span>(|<span class="hljs-keyword">mut</span> perm, <span class="hljs-keyword">mut</span> cache| &#123;<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-string">&quot;c&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    cache.<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-string">&quot;c&quot;</span>, <span class="hljs-string">&quot;d&quot;</span>.<span class="hljs-title function_ invoke__">to_string</span>(), &amp;<span class="hljs-keyword">mut</span> perm);<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;a&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;b&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">z</span> = cache.<span class="hljs-title function_ invoke__">get</span>(&amp;<span class="hljs-string">&quot;c&quot;</span>, &amp;perm).<span class="hljs-title function_ invoke__">unwrap</span>().<span class="hljs-title function_ invoke__">as_str</span>();<br>    [x, y, z].<span class="hljs-title function_ invoke__">join</span>(<span class="hljs-string">&quot; &quot;</span>);<br>    <br>    (perm, cache)<br>&#125;);<br></code></pre></td></tr></table></figure><p>至此我们的接口设计就大功告成了，具体实现几乎可以完全从 <a href="https://crates.io/crates/lru">lru</a> copy，反正 unsafe 可以操纵一切，我们只要保证上层接口足够安全就行。</p><p>我实现了一个简单的完整可用版本，开源在 <a href="https://github.com/TennyZhuang/ref-stable-lru">https://github.com/TennyZhuang/ref-stable-lru</a>，并且已经发布到 crates.io <a href="https://crates.io/crates/ref-stable-lru">ref-stable-lru</a>，感兴趣的可以试玩一下，特别是提出一些 unsound 的宝贵意见。</p><h2 id="Reference-Stability-in-C-STL"><a href="#Reference-Stability-in-C-STL" class="headerlink" title="Reference Stability in C++ STL"></a>Reference Stability in C++ STL</h2><p>熟悉 C++ 的朋友都知道，C++ STL 有一个非常黑暗的概念，叫 reference&#x2F;iterator stability，或者叫 reference&#x2F;iterator invalidation。在 <a href="https://en.cppreference.com/w/cpp/container#Iterator_invalidation">cppreference</a> 中，我们可以找到这样一张图，它描述了各个 containers 在各种操作下的行为。</p><p><img src="https://s2.loli.net/2024/01/26/N8jMQfDwR3KsUqH.png" alt="cpp references&#x2F;iterators invalidation"></p><p>这个 feature 是 C++ UB 的重灾区之一，主要原因是只在 doc 里提到，完全没法在签名上约束。而 Rust 完全阻止了相关的行为，Rust 标准库的所有 collection，只要你持有任何一个 reference，你都无法对这个结构进行任何操作，这其实浪费了 collection 的很多特性。这篇文章的设计思路是将 collection 的操作权限分散到各个 Perm 上，从而提供细粒度的读写权限控制，这个思想高度借鉴了 <a href="https://crates.io/crates/ghost-cell">Ghost Cell</a>。用类似的思路，我们也可以实现一些其他常用的数据结构，例如持有引用的同时可以 push 的 <code>VecDeque</code>。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;LRU Cache 是工业界最常用的数据结构之一，而最简单的实现方式是基于 HashMap 和链表。当访问某个 entry 时，这个 entry 会被移到链表的最前端。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2024/01/26/hQNLxjarMkGIi4g.png&quot; alt=&quot;LRU-Cache&quot;&gt;&lt;/p&gt;
&lt;p&gt;Rust 有个 crate &lt;a href=&quot;https://crates.io/crates/lru&quot;&gt;lru&lt;/a&gt; 实现了这个数据结构，这里摘取了几个关键方法：&lt;/p&gt;
&lt;figure class=&quot;highlight rust&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs rust&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;impl&lt;/span&gt;&amp;lt;K: &lt;span class=&quot;hljs-built_in&quot;&gt;Eq&lt;/span&gt; + Hash, V&amp;gt; LruCache&amp;lt;K, V&amp;gt; &amp;#123;&lt;br&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;new&lt;/span&gt;(cap: &lt;span class=&quot;hljs-type&quot;&gt;usize&lt;/span&gt;) &lt;span class=&quot;hljs-punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; LruCache&amp;lt;K, V&amp;gt; &amp;#123;todo!()&amp;#125;&lt;br&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;len&lt;/span&gt;(&amp;amp;&lt;span class=&quot;hljs-keyword&quot;&gt;self&lt;/span&gt;) &lt;span class=&quot;hljs-punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;usize&lt;/span&gt; &amp;#123;todo!()&amp;#125;&lt;br&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;put&lt;/span&gt;(&amp;amp;&lt;span class=&quot;hljs-keyword&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;self&lt;/span&gt;, k: K, v: V) &lt;span class=&quot;hljs-punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;&amp;lt;V&amp;gt; &amp;#123;todo!()&amp;#125;&lt;br&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt;&amp;gt;(&amp;amp;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;self&lt;/span&gt;, k: &amp;amp;K) &lt;span class=&quot;hljs-punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;&amp;lt;&amp;amp;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt; V&amp;gt; &amp;#123;todo!()&amp;#125;&lt;br&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// get the mutable reference of an entry, but not adjust its position.&lt;/span&gt;&lt;br&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;peek_mut&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt;&amp;gt;(&amp;amp;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;mut&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;self&lt;/span&gt;, k: &amp;amp;K) &lt;span class=&quot;hljs-punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;&amp;lt;&amp;amp;&lt;span class=&quot;hljs-symbol&quot;&gt;&amp;#x27;a&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;mut&lt;/span&gt; V&amp;gt; &amp;#123;todo!()&amp;#125;&lt;br&gt;&amp;#125;&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;相比于 &lt;a href=&quot;https://crates.io/crates/lru&quot;&gt;lru&lt;/a&gt;，这里对函数签名做了一些简化，省去了与 &lt;code&gt;Borrow&lt;/code&gt; trait 有关的优化。&lt;/p&gt;
&lt;p&gt;可以看到，与常规数据结构不同的是，这里的 &lt;code&gt;get&lt;/code&gt; 方法，也需要接受 &lt;code&gt;&amp;amp;mut self&lt;/code&gt;，这给使用者带来很多困扰。例如我们可能希望同时持有多个 &lt;code&gt;V&lt;/code&gt; 的不可变引用，这可以允许我们减少不必要的 copy，或者并发地使用他们。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Rust" scheme="https://blog.zhuangty.com/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>RisingWave 中的状态管理</title>
    <link href="https://blog.zhuangty.com/state-management-in-risingwave/"/>
    <id>https://blog.zhuangty.com/state-management-in-risingwave/</id>
    <published>2022-04-30T15:40:38.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://user-images.githubusercontent.com/9161438/166149937-8d1e7c1e-6355-4dae-9488-fb1ced832686.png" alt="RisingWave"></p><p><a href="https://github.com/singularity-data/risingwave">RisingWave</a> 是近期开源的一款 Rust 写的云原生流数据库产品。今天根据下图简单介绍一下 RisingWave 中的状态管理机制：</p><span id="more"></span><p><img src="https://user-images.githubusercontent.com/9161438/166149499-8e95cc67-5841-47d0-8ddb-92af9ee7d269.png" alt="RisingWave StateStore"></p><h2 id="Hummock-Overview"><a href="#Hummock-Overview" class="headerlink" title="Hummock Overview"></a>Hummock Overview</h2><p>在 RisingWave 的架构中，所有内部状态和物化视图的存储都是基于一套名为 Hummock 的存储来实现的。Hummock 并不是一个 storage system，而是一个 storage library。Hummock 目前支持兼容 S3 协议的存储服务作为其后端。</p><p>从接口上，Hummock 提供了类似 Key-Value store 的接口：</p><ul><li>get(key, epoch)：获取一个 value</li><li>iter(range, epoch)：扫描一个范围的 key-value pairs</li><li>batch_ingest(key-value batch)：插入一批 key-value pairs</li></ul><p>可以看到，与一般的 key-value store 接口不同，Hummock 没有提供正常的 put 接口，而是只提供了 batch 的输入接口。同时所有的操作都带了 epoch 的参数。这与 RisingWave 基于 epoch 的状态管理机制有关。</p><h2 id="Epoch-based-checkpoint"><a href="#Epoch-based-checkpoint" class="headerlink" title="Epoch-based checkpoint"></a>Epoch-based checkpoint</h2><p>RisingWave 是一个基于固定 epoch 的 partial synchronized system。每隔一个固定的时间，中心的 meta 节点会产生一个 epoch，并会向整个 DAG 的所有 source 节点发起 <code>InjectBarrier</code> 请求。source 节点收到 barrier 后，将其注入到当前数据流的一个切片。</p><p><img src="https://user-images.githubusercontent.com/9161438/166149434-ca5e7db1-ebbc-452d-af5f-2a9e5bf5533b.png" alt="epoch"></p><figure class="highlight proto"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs proto"><span class="hljs-keyword">message </span><span class="hljs-title class_">Barrier</span> &#123;<br>  Epoch epoch = <span class="hljs-number">1</span>;<br>  <span class="hljs-keyword">oneof</span> mutation &#123;<br>    NothingMutation nothing = <span class="hljs-number">2</span>;<br>    StopMutation stop = <span class="hljs-number">3</span>;<br>    UpdateMutation update = <span class="hljs-number">4</span>;<br>    AddMutation add = <span class="hljs-number">5</span>;<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>对于 DAG 中间的任何一个算子，如果收到了一个 barrier，需要依次做一些事情：</p><ol><li>如果是一个多输入流的算子（Join、Union)，那么需要等待其它流的 barrier，直到收集齐所有输入流的同一个 barrier 以后才处理。</li><li>如果有需要操作的 mutation（用于 scale-out，create mview，drop mview），那么 apply 对应的 conf change。</li><li><strong>dump local state (async checkpoint)</strong></li></ol><p>3 是本文想介绍的重点。简单来说，<strong>RisingWave 既不是一个 local state backend，也不是 remote state backend，而是一个混合形态</strong>。只有最新的 barrier 之后的 state 才是算子自身维护的 local state，而之前的数据则是 remote state。当且仅当收到 barrier 的时候，算子才会选择 dump 状态到 hummock store。这也就是 hummock store 只提供 ingest batch 接口的原因 ———— 算子只会在收到 barrier 的时候将 local state dump 到 hummock 中去。</p><h2 id="Async-Checkpoint"><a href="#Async-Checkpoint" class="headerlink" title="Async Checkpoint"></a>Async Checkpoint</h2><p>前文中我们提到，算子在收到 barrier 时，会选择 dump 数据到 Hummock，但我们也提到了 barrier 是随着数据流一起流动的，如果每个算子都需要同步地将等待状态被上传到 shared storage（目前是 S3），那么数据处理就会 blocking 一整个上传的 Round trip。如果 DAG 中有 N 个有状态算子的话，那么 barrier 在整个传递过程中就会被 delay N 个 round trip，这对整个系统的处理能力会产生很大的影响。因此，我们将 barrier 的处理流程几乎全异步化了。有状态算子在收到 barrier 后需要做的唯一一件事，就是将当前 epoch 的 local state 同步地 <code>std::mem::take</code> 走，重置为一个空的 state，让算子可以接着处理下一个 epoch 的数据。这也引入了一系列的问题：</p><ul><li>这个 epoch 的 local state 被 take 到哪里去了？</li><li>既然 local state 并没有同步地上传到 S3，那么针对这段时间数据的查询应该怎么处理呢？</li><li>在异步上传的时候，算子 crash 了怎么办，如何知道 checkpoint 是否成功？</li></ul><p>为了解答上面的这些问题，我们引入了 Shared Buffer。</p><h2 id="Shared-Buffer"><a href="#Shared-Buffer" class="headerlink" title="Shared Buffer"></a>Shared Buffer</h2><p>Shared Buffer 是一个 Compute Node 的所有算子共享的一个后台任务，当有状态算子收到 barrier 之后，local state 会被 take 到 Shared Buffer 里。</p><p>Shared Buffer 主要负责以下事情：</p><ol><li>（可选）部分算子的状态可能会很小，如 SimpleAgg。根据 local state 的大小，适当地在不同算子的 state 在文件粒度上之间做切分和合并。</li><li>将算子本地的状态上传到 shared storage 上。</li><li><strong>向 meta service 注册已经成功上传成功的 state 记录。</strong></li><li><strong>服务来自算子内部对尚未上传成功的 local state 的查询。</strong></li></ol><p>这里的 3 和 4 很好地回答了上一小节提的问题。</p><ul><li>从用户的视角，只有一个 epoch 内所有算子的 local state 全部上传完成<strong>并在 meta service 注册成功</strong>，才认为这个 checkpoint 是完成的，无论是正常 query 还是 recover，都会基于<strong>最新的完整 checkpoint</strong>。</li><li>从内部算子的视角，在读自己 state 的时候，必然是要求读到完整最新状态的，那么事实上内部算子需要的是 remote state + shared buffer + local state merge 后的结果。这里 RisingWave 也提供了 <code>MergeIterator</code> 来做这个泛化。</li></ul><h2 id="Local-Cache"><a href="#Local-Cache" class="headerlink" title="Local Cache"></a>Local Cache</h2><p>由于大部分状态在 remote state 中，RisingWave 可以很简单地实现 scale-out，然而带来的代价也是很明显的。相比于 Flink 这种 local state 的设计，RisingWave 需要多很多 remote lookup。</p><p>我们以 HashAgg 为例，当 HashAgg 算子收到 Barrier 后，它会把当前 barrier 的统计结果 dump 到 shared buffer，将算子本地的 state 重置为空。然而在处理下一个 epoch 数据的时候，最近处理过的 group key 很可能依然就是热点，我们不得不重新从 shared buffer 甚至 remote state 重新将对应的 key 捞回来。因此我们的选择是，在算子内部不再将之前 epoch 的 local state 重置清空，而是将其标记为 evictable，当且仅当内存不足时，再清理 evictable 的数据记录。</p><p>基于这个设计，在内存充足的情况下，或者对于状态非常小的算子（如 simple agg 仅有一条记录），它的所有状态都在内存里，且都由当前线程去操作，达到了最大化的性能，而 dump 仅用于 recovery 和 query。对于内存不足的情况下，或者对于有明显冷热特征的算子（如 TopN），那么既能保证正确运行（冷数据去 remote lookup），又能充分榨干每一分内存，</p><h2 id="Compaction"><a href="#Compaction" class="headerlink" title="Compaction"></a>Compaction</h2><p>State 并不是上传到 shared storage 就不再修改了，RisingWave 会有后台的 compaction 任务。</p><p>Compaction 主要有以下目的：</p><ol><li>回收垃圾：部分算子会产生 DELETE 记录，这也会产生一条 tombstone 记录，在 compaction 的时候需要删除记录。同时覆盖写也需要被合并，回收空间。</li><li>整理数据：部分算子在上传的时候会倾向于将同一个 epoch 内不同算子的 state 合并，以减少写放大。然而为了面向后续查询的优化，compaction 会倾向于将同一个算子不同 epoch 的 state 合并，减少读放大。另外，RisingWave 倾向于将计算分布和存储分布尽可能对齐，因此发生 scale-out 后也需要 compaction 来整理数据，这里之后有机会介绍 scale-out 设计的时候再展开，本文不赘述。</li></ol><p>执行 compaction 任务的 Compactor 可以灵活部署，既可以挂载在计算节点，也可以由独立进程启动，未来在云上也会支持 serverless 任务来启动。Compaction 任务的调度可以根据用户的需求来调节。如同 <a href="https://blog.zhuangty.com/napa">Napa</a> 里提到的，如果用户同时需要 freshness 和 query latency，那么理应付出更多的 cost 来执行更频繁的 compaction 任务，反之的话则可以帮用户来省钱。</p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>如果我们重新 review 一下整个 state store 的设计的话，就会发现这是一颗基于 cloud 的大 LSM 树。每个算子的 local state 和 shared buffer 对应于 memtable（允许 concurrent write，因为所有 stateful 算子保证了 distribution），而 shared storage 里存储的则是 SSTs，meta service 则是一个中心化的 manifest，作为 source of truth，并且根据元信息触发 compaction 任务。</p><p>本文简单介绍了 RisingWave State Store 的基本架构和设计上的 trade off。核心思路是尽可能利用云上 shared storage 的能力，享受 remote state 的优势 – scalability 和更强的弹性扩缩容能力，又希望在 hot state 较小的场景依然能达到 local state 的性能。当然这一切并非毫无代价，而在云原生的架构下，我们可以让这个 trade-off 由用户来选择。</p><p><img src="https://user-images.githubusercontent.com/9161438/166149580-65a119cf-5071-42ae-89cb-443686365df7.png" alt=" "></p><p>RisingWave 是一个活跃开发的项目，设计也在活跃迭代中，目前我们也在上述设计之上引入了 Shared State，以减少存储的状态，之后有机会展开介绍。更多的设计文档，可以在 <a href="https://github.com/singularity-data/risingwave/tree/main/docs">RisingWave 的 repo</a> 找到。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/9161438/166149937-8d1e7c1e-6355-4dae-9488-fb1ced832686.png&quot; alt=&quot;RisingWave&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/singularity-data/risingwave&quot;&gt;RisingWave&lt;/a&gt; 是近期开源的一款 Rust 写的云原生流数据库产品。今天根据下图简单介绍一下 RisingWave 中的状态管理机制：&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
    <category term="Streaming System" scheme="https://blog.zhuangty.com/tags/Streaming-System/"/>
    
    <category term="RisingWave" scheme="https://blog.zhuangty.com/tags/RisingWave/"/>
    
  </entry>
  
  <entry>
    <title>Zig lang 初体验 -- 『大道至简』的 comptime</title>
    <link href="https://blog.zhuangty.com/zig-lang-comptime/"/>
    <id>https://blog.zhuangty.com/zig-lang-comptime/</id>
    <published>2022-04-05T20:11:08.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>在很长的一段时间里，系统级的编程语言只有 C 与 C++，使用其中任何一种都不是愉快的体验，这里不作展开。现在许多新的系统项目都使用 Rust 开发。然而这些都不是本文的重点，最近我接触了一个新的系统编程语言 – <a href="https://ziglang.org/">Zig</a>，今天分享一下试玩的体验。</p><p><img src="https://user-images.githubusercontent.com/9161438/161515187-07d053b2-7448-4cd9-8338-776896a2f7fa.svg" alt="Zig"></p><span id="more"></span><hr><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>全世界有几千种编程语言，任何一个系统学过编译原理的本科生，都可以设计出自己的 toy language 并实现一个 mini compiler。大部分语言都会设计自己喜欢的语法去表达一些通用的基础设施：基础类型、字符串、变量、条件分支、循环、函数、结构体，这些都是朝三暮四、朝四暮三的区别，也不会成为一个语言本质的创新。 本文不会介绍 Zig 的基础语法，而是想安利一下 Zig 的一个重要 feature —— comptime。</p><p>C++ 有非常强大的编译期运算能力，meta programming 的魔法层出不穷，且在每个 C++ 版本越迭代越博大精深，然而对于学习者来说，是非常陡峭的学习曲线。Meta Programming 完全是内置于 C++ 编译器的另一套语法非常复杂、报错非常不友好的函数式编程语言。曾经看到过一个观点（来源请求），如果只是为了在编译期生成足够高效的代码，与其将元编程做得越来越复杂，不如直接引入 Python 作为编译期的胶水语言。那么 Zig 就做出了一个类似的选择：<strong>Zig 在编译期引入 Zig 自身作为胶水语言来生成代码，这就是 Zig comptime。</strong></p><p>我们将以<a href="https://www.skyzh.dev/posts/articles/2022-01-22-rust-type-exercise-in-database-executors/">迟先生的类型体操（上篇）</a> 中实现的一些例子来学习一下 zig。</p><hr><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><p>迟先生的类型体操中实现的 Array 本质上是 <a href="https://arrow.apache.org/">Apache Arrow 内存格式</a> 的一种实现，我们也可以尝试在 Zig 中实现一下。</p><h3 id="实现-FixedArray"><a href="#实现-FixedArray" class="headerlink" title="实现 FixedArray"></a>实现 <code>FixedArray</code></h3><p>原文中<code>PrimitiveArray</code> 存的是可空的定长元素组成的数组，我们不妨将它改名为 FixedArray，由一个代表是否为空的 Bitmap 和存储元素的 collection （这里是 ArrayList）组成。而 <code>StringArray</code>（这里简化成 <code>BytesArray</code>）是存储变长字符串的集合，由 Bitmap、偏移量数组、和拍平的字符串内容组成。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs zig">const std = @import(&quot;std&quot;);<br>const ArrayList = std.ArrayList;<br>const DynamicBitSet = std.bit_set.DynamicBitSet;<br><br>fn FixedArray(comptime T: type) type &#123;<br>    return struct &#123;<br>        const Self = @This();<br>        data: ArrayList(T),<br>        validity: DynamicBitSet,<br><br>        pub const Ref = T;<br><br>        pub fn deinit(self: *Self) void &#123;<br>            self.data.deinit();<br>            self.validity.deinit();<br>        &#125;<br><br>        pub fn value(self: *const Self, idx: usize) ?Self.Ref &#123;<br>            if (self.validity.isSet(idx)) &#123;<br>                return self.data.items[idx];<br>            &#125; else &#123;<br>                return null;<br>            &#125;<br>        &#125;<br><br>        pub fn len(self: *const Self) usize &#123;<br>            return self.data.len();<br>        &#125;<br>    &#125;;<br>&#125;<br></code></pre></td></tr></table></figure><p>可以看到，在 Zig 中，””泛型类型 <code>FixedArray</code> 本质上就是一种接受一个类型，返回一个结构体的函数而已。在这个函数里，我们可以执行任意的表达式检查输入类型参数的合法性，<strong>甚至可以根据输入参数用 if else 返回不同的结构体</strong>。不妨假设我们添加了一个需求：对于 FixedArray，如果每个元素大于 8 个字节，也应该返回引用而非值本身。我们可以改少数代码完成这个需求：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs zig">// ...<br>const sizeLarge = (@sizeOf(T) &gt; 8);<br>pub const Ref = if (sizeLarge &gt; 8) *T else T;<br>// ...<br>pub fn value(self: *const Self, idx: usize) ?Self.Ref &#123;<br>    if (self.validity.isSet(idx)) &#123;<br>        if (sizeLarge) &#123;<br>            return &amp;self.data.items[idx];<br>        &#125; else &#123;<br>            return self.data.items[idx];<br>        &#125;<br>    &#125; else &#123;<br>        return null;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><code>@This</code> 很像一般语言里的 receiver，但它其实只是一个的编译器内置的函数，返回了正在定义中的类型参数实例，我们用 Self 为它起了个别名。既然类型只是一种编译期变量，那么相应的 Associated type 也只是结构体上的一个编译期常量而已，我们根据编译期计算的 <code>@sizeOf(T)</code> 来确定 <code>Ref</code> 的类型。这里 @sizeOf 是一个例子，如果想要，我们也可以用斐波那契数来确定 Ref 的类型 :(</p><h3 id="实现-BytesArray"><a href="#实现-BytesArray" class="headerlink" title="实现 BytesArray"></a>实现 BytesArray</h3><p>原文中实现了 <code>StringArray</code> 作为变长数组的例子，这里我们简化为 <code>BytesArray</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs zig">const BytesArray = struct &#123;<br>    const Self = @This();<br><br>    data: ArrayList(u8),<br>    offsets: ArrayList(u32),<br>    validity: DynamicBitSet,<br><br>    pub const Ref = []u8;<br><br>    pub fn value(self: *Self, idx: usize) ?Self.Ref &#123;<br>        if (self.validity.isSet(idx)) &#123;<br>            const start = self.offsets.items[idx];<br>            const end = self.offsets.items[idx + 1];<br>            return self.data.allocatedSlice()[start..end];<br>        &#125; else &#123;<br>            return null;<br>        &#125;<br>    &#125;<br><br>    pub fn deinit(self: *Self) void &#123;<br>        self.data.deinit();<br>        self.offsets.deinit();<br>        self.validity.deinit();<br>    &#125;<br><br>    pub fn len(self: *Self) usize &#123;<br>        return self.offsets.len() - 1;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>这里更直接了，<code>BytesArray</code> 就是个类型为 <code>type</code> 的常量。我们也定义了 <code>Ref</code> 常量来模拟 <code>BytesArray</code> 的『关联类型』。通过将 <code>Ref</code> 定义为切片（<code>[]u8</code>），我们轻松实现了返回引用而非拷贝数据。</p><h3 id="实现-BytesArrayBuilder"><a href="#实现-BytesArrayBuilder" class="headerlink" title="实现 BytesArrayBuilder"></a>实现 BytesArrayBuilder</h3><p>我们需要一个 Builder 来构造不可变的 array，构造的过程跟读取是类似的。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><code class="hljs zig">const BytesArrayBuilder = struct &#123;<br>    const Self = @This();<br><br>    pub const Array = BytesArray;<br><br>    data: ArrayList(u8),<br>    offsets: ArrayList(u32),<br>    validity: DynamicBitSet,<br><br>    pub fn init(allocator: Allocator) !Self &#123;<br>        var offsets = try ArrayList(u32).initCapacity(allocator, 1);<br>        offsets.appendAssumeCapacity(0);<br>        return Self&#123;<br>            .data = ArrayList(u8).init(allocator),<br>            .offsets = offsets,<br>            .validity = try DynamicBitSet.initEmpty(allocator, 0),<br>        &#125;;<br>    &#125;<br><br>    fn append(self: *Self, v: Self.Array.Ref) !void &#123;<br>        try self.data.appendSlice(v);<br>        try self.offsets.append(@intCast(u32, self.data.items.len));<br>        try self.validity.resize(self.validity.capacity() + 1, true);<br>    &#125;<br><br>    fn append_null(self: *Self) !void &#123;<br>        try self.offsets.append(@intCast(u32, self.data.items.len));<br>        try self.validity.resize(self.validity.capacity() + 1, false);<br>    &#125;<br><br>    pub fn finish(self: Self) BytesArray &#123;<br>        return BytesArray&#123;<br>            .data = self.data,<br>            .offsets = self.offsets,<br>            .validity = self.validity,<br>        &#125;;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>这里有一些小问题，比如标准库的 <code>DynamicBitSet</code> 并不能高效地 append 一个 bool，不过可以暂时忽略。我们将 <code>ArrayBuilder</code> 和 <code>Array</code> 通过 <code>ArrayBuilder::Array</code> 关联起来，当然我们也可以顺着 Array 再找到 Ref，如 <code>    fn append(self: *Self, v: Self.Array.Ref)</code>。我们也可以在 Array 的结构体中添加对应的 Builder</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs zig">pub const Builder = BytesArrayBuilder;<br></code></pre></td></tr></table></figure><h3 id="关联数据库逻辑类型与物理类型"><a href="#关联数据库逻辑类型与物理类型" class="headerlink" title="关联数据库逻辑类型与物理类型"></a>关联数据库逻辑类型与物理类型</h3><p>这个对应于 <a href="https://www.skyzh.dev/posts/articles/2022-02-01-rust-type-exercise-in-database-executors-final/#%E7%94%A8-macro-%E5%85%B3%E8%81%94%E9%80%BB%E8%BE%91%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AE%9E%E9%99%85%E7%B1%BB%E5%9E%8B">用 Rust 做类型体操 (下篇)</a>，得益于 comptime 的简单设计，我们直接逃课了类型体操的大部分。</p><p>在原文中，作者用了非常复杂的 macro，用类似 callback 的编程范式来实现了逻辑类型和物理类型的关联，而在<strong>类型即编译期变量</strong>的 Zig 里，这一切都非常自然。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs zig">// 定义 DataType，假设我们支持五种类型。<br>const DataType = union(enum) &#123;<br>    SmallInt: void,<br>    Integer: void,<br>    BigInt: void,<br>    Varchar: void,<br>    Char: u16, // Char 的长度<br><br>    // 关联逻辑类型和物理类型<br>    fn ArrayType(self: DataType) type &#123;<br>        return switch (self) &#123;<br>            DataType.SmallInt =&gt; FixedArray(i16),<br>            DataType.Integer =&gt; FixedArray(i32),<br>            DataType.BigInt =&gt; FixedArray(i64),<br>            DataType.Varchar =&gt; BytesArray,<br>            DataType.Char =&gt; BytesArray,<br>        &#125;;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>同样，基于 comptime，我们也可以用非常流畅的逻辑将表达式的类型和 DataType 的数组直接映射到表达式的实现，不过这篇 blog 已经太长了，暂时不过多展开了。</p><h3 id="std-MultiArrayList"><a href="#std-MultiArrayList" class="headerlink" title="std.MultiArrayList"></a>std.MultiArrayList</h3><p>事实上，Zig 已经在标准库里内置了类似 Multi-dimentional FixedArray 的东西，且非常灵活。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs zig">const ally = testing.allocator;<br>const Foo = struct &#123;<br>    a: u32,<br>    b: []const u8,<br>    c: u8,<br>&#125;;<br>var list = MultiArrayList(Foo)&#123;&#125;;<br>defer list.deinit(ally);<br>try list.ensureTotalCapacity(ally, 2);<br>list.appendAssumeCapacity(.&#123;<br>    .a = 1,<br>    .b = &quot;foobar&quot;,<br>    .c = &#x27;a&#x27;,<br>&#125;);<br>list.appendAssumeCapacity(.&#123;<br>    .a = 2,<br>    .b = &quot;zigzag&quot;,<br>    .c = &#x27;b&#x27;,<br>&#125;);<br>try testing.expectEqualSlices(u32, list.items(.a), &amp;[_]u32&#123; 1, 2 &#125;);<br>try testing.expectEqualSlices(u8, list.items(.c), &amp;[_]u8&#123; &#x27;a&#x27;, &#x27;b&#x27; &#125;);<br>try testing.expectEqual(@as(usize, 2), list.items(.b).len);<br>try testing.expectEqualStrings(&quot;foobar&quot;, list.items(.b)[0]);<br>try testing.expectEqualStrings(&quot;zigzag&quot;, list.items(.b)[1]);<br>try list.append(ally, .&#123;<br>    .a = 3,<br>    .b = &quot;fizzbuzz&quot;,<br>    .c = &#x27;c&#x27;,<br>&#125;);<br></code></pre></td></tr></table></figure><p>可以看到，使用体验都跟 <code>ArrayList</code> 几乎完全一模一样，但是内部确实按字段列存储的，它的内部实现大量使用了编译期反射生成友好的代码。这对 <a href="https://en.wikipedia.org/wiki/Data-oriented_design">Data-oriented Programming</a> 的场景是非常友好的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>Zig 的 const function 非常完善，大部分函数和类型都是 const evaluable 的，这也就意味着编译期 zig 可以无缝对接运行时 Zig。相比之下，Rust <code>feature(const_eval)</code> 至今都不支持 heap allocation，只要一个函数用到了 <code>Vec</code>, <code>Box</code>, <code>String</code> 等任何堆上分配的资源，都不能被标记为 <code>const fn</code>。</li><li>Zig 有非常完善且原生支持的编译期反射。在 rust 中，<a href="https://github.com/dtolnay/reflect">https://github.com/dtolnay/reflect</a> 是目前唯一的尝试，且使用起来还是非常不直观。</li><li>Zig comptime 是图灵完备的语言，我们可以自由地实现想要的所有 pattern。</li><li>Zig 可能会面临缺乏类型约束导致编译器报错栈很深的问题，相对来说不是特别友好，但实际体验上，zig comptime 的 stack 还是非常易懂的，而且这可以后续引入 constraints 来解决。</li></ul><h2 id="对比一些其他的方案"><a href="#对比一些其他的方案" class="headerlink" title="对比一些其他的方案"></a>对比一些其他的方案</h2><p>对比 C++ template：</p><ul><li>C++ meta programming 非常强大，图灵完备，绝对可以做到 zig comptime 同等的能力</li><li>C++ meta programming 和 C++ 本身是两套语言</li><li>C++ meta programming 运行非常慢</li><li>C++ meta programming 可读性非常差</li><li>会 C++ 的人很可能学不会 C++ meta programming（比如我）</li><li>Zig comptime 和 Zig 完全是一套语言，会 zig 就会 zig comptime，而且可以复用几乎所有的基础设施（参考上文）</li></ul><p>对比 External generator（ <code>go generate</code> 或 <code>build.rs</code>） 等方案：</p><ul><li>编译器内置支持而不仅仅是工具链支持</li><li>基于字符串的复用在造库的时候编程体验比较差</li><li>由于完全是由工具链（如 cargo）驱动的两个过程，在 compile 和 runtime 互相引用也只能基于字符串作为约定，很不安全。</li></ul><p>对比 rust proc-macro：</p><ul><li>与 zig 相似，proc-macro 也可以用原生 rust 进行开发</li><li>proc-macro 基于语法树开发，相比于大部分抽象需要的信息来说过于底层，coner case 非常多，且很容易 break change。</li><li>zig comptime 拿到的是更上层的信息，<code>@TypeOf</code>, <code>@field</code> 等都可以拿到非常开箱即用的信息。</li></ul><p>对比 Generics：</p><ul><li>相比于语言本身，都没有引入很高的复杂度</li><li>Zig 图灵完备，能够 zero overhead 实现的抽象非常多，且不需要引入复杂的设计和学习成本。</li><li>不用类型体操</li><li>报错会更晦涩，安全性检查上会更弱（可以引入 constraint）。</li></ul><p>如果要用一个合适的词形容 zig comptime 的话，我觉得『大道至简』是一个非常好的描述。这不仅仅是在玩梗，而是一种真实的感受。</p><p>作为大道至简的代表，Golang 在 1.18 之前一直不支持泛型广为人诟病，但 Golang 设计之初就是不希望引入过高的复杂度和学习负担，我其实可以理解这个选择（这不妨碍我不想写 Golang）。在最新的版本里，Golang 引入了一个非常残废的 Generics，支持的功能非常有限。而如果想支持更多的抽象需求，不可避免的要引入一些相对复杂的设计如 covariance，partial specification，甚至 higher kinded type，这也背离了 Golang 的设计初衷。也许对于 Golang 来说，在 1.17 的 IF 线里，选择从 <code>go generate</code> 进化到 comptime 是更好的选择 —— 用更低的复杂度和学习成本换来了非常强大的抽象能力。</p><hr><p>Zig 依然是个比较早期的语言，没有发布 1.0 版本。相比于 Zig，我也更喜欢使用 Rust，但 Zig 依然有一些非常惊艳的 feature，comptime 只是其中之一。同时 Zig 也有非常好的交叉编译基础设施，我很期待 Zig 能成为未来系统编程语言中 C 的一个重要替代品。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在很长的一段时间里，系统级的编程语言只有 C 与 C++，使用其中任何一种都不是愉快的体验，这里不作展开。现在许多新的系统项目都使用 Rust 开发。然而这些都不是本文的重点，最近我接触了一个新的系统编程语言 – &lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig&lt;/a&gt;，今天分享一下试玩的体验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/9161438/161515187-07d053b2-7448-4cd9-8338-776896a2f7fa.svg&quot; alt=&quot;Zig&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Zig" scheme="https://blog.zhuangty.com/tags/Zig/"/>
    
  </entry>
  
  <entry>
    <title>Rust Enum Layout 的优化</title>
    <link href="https://blog.zhuangty.com/rust-enum-layout/"/>
    <id>https://blog.zhuangty.com/rust-enum-layout/</id>
    <published>2022-01-23T13:05:08.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>今天学到了一点关于 Rust Enum 的冷知识，在开始阅读之前，大家可以猜一下下面的 Rust 代码在常见的 64 bit 机器上的输出是什么？</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span> (<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>);<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">B</span> (<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>, <span class="hljs-type">bool</span>);<br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() &#123;<br>    dbg!(std::mem::size_of::&lt;A&gt;());<br>    dbg!(std::mem::size_of::&lt;<span class="hljs-type">Option</span>&lt;A&gt;&gt;());<br>    dbg!(std::mem::size_of::&lt;B&gt;());<br>    dbg!(std::mem::size_of::&lt;<span class="hljs-type">Option</span>&lt;B&gt;&gt;());<br>&#125;<br></code></pre></td></tr></table></figure><p>在这个 <a href="https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=bab644e35609b5475978821378d3560f">Rust Playground</a> 里可以看到结果。</p><span id="more"></span><hr><p>Rust enum 本质是一种 <a href="https://en.wikipedia.org/wiki/Tagged_union">tagged union</a>，对应代数数据类型中的 sum type，这里不过多展开。在 Rust enum 的实现中，通常用一个 byte 来存储 type tag（大部分 enum 不会超过 256 种类型，更多地会相应扩展），也就是说，理想情况下，以下两个结构体是等价的：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">enum</span> <span class="hljs-title class_">Attr</span> &#123;<br>    <span class="hljs-title function_ invoke__">Color</span>(<span class="hljs-type">u8</span>, <span class="hljs-type">u8</span>, <span class="hljs-type">u8</span>),<br>    <span class="hljs-title function_ invoke__">Shape</span>(<span class="hljs-type">u16</span>, <span class="hljs-type">u16</span>),<br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Color</span> &#123;</span> <span class="hljs-type">uint8_t</span> r, g, b &#125;;<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Shape</span> &#123;</span> <span class="hljs-type">uint16_t</span> w, h &#125;;<br><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Attr</span> &#123;</span><br>    <span class="hljs-type">uint8_t</span> tag;<br>    <span class="hljs-class"><span class="hljs-keyword">union</span> &#123;</span><br>        Color color;<br>        Shape shape;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在这个实现下，enum 在很多场景下并不是 zero overhead 的。幸运的是，Rust 从未定义过 ABI，而带有数据的 enum 甚至是无法被 <code>repr(C)</code> 表示的，这给了 Rust 充分的空间对 enum memory layout 进行细粒度的优化。这篇文章会涉及一些在 <code>rustc 1.60.0-nightly</code> 下相关优化的介绍。</p><p>在开始具体的探索之前，我们需要准备一个辅助函数，来帮我我们查看变量的内存结构：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">print_memory</span>&lt;T&gt;(v: &amp;T) &#123;<br>    <span class="hljs-built_in">println!</span>(<span class="hljs-string">&quot;&#123;:?&#125;&quot;</span>, <span class="hljs-keyword">unsafe</span> &#123;<br>        core::slice::<span class="hljs-title function_ invoke__">from_raw_parts</span>(v <span class="hljs-keyword">as</span> *<span class="hljs-keyword">const</span> _ <span class="hljs-keyword">as</span> *<span class="hljs-keyword">const</span> <span class="hljs-type">u8</span>, std::mem::<span class="hljs-title function_ invoke__">size_of_val</span>(v))<br>    &#125;)<br>&#125;<br></code></pre></td></tr></table></figure><p>以上面的 <code>Attr</code> 为例：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">print_memory</span>(&amp;Attr::<span class="hljs-title function_ invoke__">Color</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>));<br><span class="hljs-comment">// [0, 1, 2, 3, 0, 0]</span><br><span class="hljs-comment">//  ^  ^  ^  ^  ^^^^</span><br><span class="hljs-comment">//  |  |  |  |    |</span><br><span class="hljs-comment">//  |  |  |  |    --- padding</span><br><span class="hljs-comment">//  |  |  |  -------- b</span><br><span class="hljs-comment">//  |  |  |---------- g</span><br><span class="hljs-comment">//  |  |------------- r</span><br><span class="hljs-comment">//  ----------------- tag</span><br><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;Attr::<span class="hljs-title function_ invoke__">Shape</span>(<span class="hljs-number">257</span>, <span class="hljs-number">258</span>));<br><span class="hljs-comment">// [1, 0, 1, 1, 2, 1]</span><br><span class="hljs-comment">//  ^  ^  ^^^^  ^^^^</span><br><span class="hljs-comment">//  |  |    |     |</span><br><span class="hljs-comment">//  |  |    |     --- h, 256 + 2</span><br><span class="hljs-comment">//  |  |    --------- w, 256 + 1</span><br><span class="hljs-comment">//  |  |------------- padding</span><br><span class="hljs-comment">//  ----------------- tag</span><br></code></pre></td></tr></table></figure><h2 id="Option"><a href="#Option" class="headerlink" title="Option&lt;P&lt;T&gt;&gt;"></a><code>Option&lt;P&lt;T&gt;&gt;</code></h2><p>P 是常见的智能指针类型，包括 <code>&amp;</code>&#x2F;<code>&amp;mut</code>&#x2F;<code>Box</code>。这应该是关于 enum layout 优化里最著名的一个例子了。Rust 推荐使用 <code>Option&lt;P&lt;T&gt;&gt;</code> 来处理可空指针，这实现了 <a href="https://en.wikipedia.org/wiki/Void_safety">null safety</a>.</p><p><code>Option&lt;T&gt;</code> 在 rust 中被表示为一种 enum：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Option</span>&lt;T&gt; &#123;<br>    <span class="hljs-literal">None</span>,<br>    <span class="hljs-title function_ invoke__">Some</span>(T),<br>&#125;<br></code></pre></td></tr></table></figure><p>如果不作任何优化的话，显然是存在不必要的 overhead 的，空指针可以完整地表示 <code>None</code> 的语义。由于这种情况太过常见，rustc 不仅针对性地做了优化，而且将其标准化了。</p><blockquote><p>If T is an FFI-safe non-nullable pointer type, Option<T> is guaranteed to have the same layout and ABI as T and is therefore also FFI-safe. As of this writing, this covers &amp;, &amp;mut, and function pointers, all of which can never be null.</p></blockquote><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-number">0u64</span>);<br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;p);<br><span class="hljs-comment">// [208, 185, 38, 40, 162, 85, 0, 0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(p));<br><span class="hljs-comment">// [208, 185, 38, 40, 162, 85, 0, 0]</span><br></code></pre></td></tr></table></figure><p>一个不算太冷的冷知识是，这种 hack 并不是针对 <code>Option</code> 的，而是针对指针类型的。任何自定义的 enum 满足条件也可以达到相同的效果。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">enum</span> <span class="hljs-title class_">MyOption</span>&lt;T&gt; &#123;<br>    <span class="hljs-title function_ invoke__">MySome</span>(T),<br>    MyNone,<br>&#125;<br><br><span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-number">0u64</span>);<br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;MyOption::<span class="hljs-title function_ invoke__">MySome</span>(p));<br><span class="hljs-comment">// [208, 185, 38, 40, 162, 85, 0, 0]</span><br><span class="hljs-comment">// The address of `p`.</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;MyOption::&lt;<span class="hljs-type">Box</span>&lt;<span class="hljs-type">u64</span>&gt;&gt;::MyNone);<br><span class="hljs-comment">// [0, 0, 0, 0, 0, 0, 0, 0]</span><br><span class="hljs-comment">// Use nullptr to represent `MyNone`.</span><br></code></pre></td></tr></table></figure><p><code>Option&lt;P&lt;T&gt;&gt;</code> 可以优化的根本原因是，P 的内存表示下有一个永远非法的值，而相应的 enum 仅需要表示一个额外的值来表达多余的类型。超出这个约束就会导致这个优化失效。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">enum</span> <span class="hljs-title class_">MyOption2</span>&lt;T&gt; &#123;<br>    <span class="hljs-title function_ invoke__">MySome</span>(T),<br>    MyNone,<br>    MyNone2,<br>&#125;<br><br><span class="hljs-keyword">let</span> <span class="hljs-variable">p</span> = <span class="hljs-type">Box</span>::<span class="hljs-title function_ invoke__">new</span>(<span class="hljs-number">0u64</span>);<br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;MyOption2::<span class="hljs-title function_ invoke__">MySome</span>(p));<br><span class="hljs-comment">// [0, 0, 0, 0, 0, 0, 0, 0, 208, 185, 38, 40, 162, 85, 0, 0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;MyOption2::&lt;<span class="hljs-type">Box</span>&lt;<span class="hljs-type">u64</span>&gt;&gt;::MyNone2);<br><span class="hljs-comment">// [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]</span><br></code></pre></td></tr></table></figure><h2 id="bool-Ordering"><a href="#bool-Ordering" class="headerlink" title="bool, Ordering"></a><code>bool</code>, <code>Ordering</code></h2><p>rust 中的 <code>bool</code> 占用一个 byte，且仅有两个合法的值，<code>True</code> 和 <code>False</code>，对应的内存表示为 <code>1u08</code> 和 <code>0u08</code>。我们可以理解为 <code>bool</code> 有 254 个非法值可以供 type tag 挥霍。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-literal">false</span>));<br><span class="hljs-comment">// [0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-literal">true</span>));<br><span class="hljs-comment">// [1]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(<span class="hljs-literal">None</span> <span class="hljs-keyword">as</span> <span class="hljs-type">Option</span>&lt;<span class="hljs-type">bool</span>&gt;));<br><span class="hljs-comment">// [2]</span><br></code></pre></td></tr></table></figure><p>更进一步地，我们可以更给力一点：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-literal">false</span>)));<br><span class="hljs-comment">// [0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-literal">true</span>)));<br><span class="hljs-comment">// [1]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-literal">None</span>) <span class="hljs-keyword">as</span> <span class="hljs-type">Option</span>&lt;<span class="hljs-type">Option</span>&lt;<span class="hljs-type">bool</span>&gt;&gt;));<br><span class="hljs-comment">// [2]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(<span class="hljs-literal">None</span> <span class="hljs-keyword">as</span> <span class="hljs-type">Option</span>&lt;<span class="hljs-type">Option</span>&lt;<span class="hljs-type">bool</span>&gt;&gt;));<br><span class="hljs-comment">// [3]</span><br></code></pre></td></tr></table></figure><p>对应的，Ordering 有三种合法值，同样适用于这个优化。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(std::cmp::Ordering::Less));<br><span class="hljs-comment">// [255]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(std::cmp::Ordering::Greater));<br><span class="hljs-comment">// [1]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(std::cmp::Ordering::Equal));<br><span class="hljs-comment">// [0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(<span class="hljs-literal">None</span> <span class="hljs-keyword">as</span> <span class="hljs-type">Option</span>&lt;std::cmp::Ordering&gt;));<br><span class="hljs-comment">// [2]</span><br></code></pre></td></tr></table></figure><h2 id="Enum"><a href="#Enum" class="headerlink" title="Enum"></a>Enum</h2><p>事实上，编译器并没有对 bool、Ordering 进行特判，任何种类少于 256 的 enum 本身都满足被优化的条件，即 type tag 里会有 (256 - kinds) 个空位。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">enum</span> <span class="hljs-title class_">ShapeKind</span> &#123; Square, Circle &#125;<br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(ShapeKind::Square));<br><span class="hljs-comment">// [0]</span><br><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;MyOption::<span class="hljs-title function_ invoke__">MySome</span>(MyOption::<span class="hljs-title function_ invoke__">MySome</span>(<span class="hljs-number">1u8</span>)));<br><span class="hljs-comment">// [0, 1]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(MyOption::MyNone <span class="hljs-keyword">as</span> MyOption&lt;MyOption&lt;<span class="hljs-type">u8</span>&gt;&gt;));<br><span class="hljs-comment">// [2, 0]</span><br><span class="hljs-comment">// 尽管 u8 本身没有空位，但是 MyOption 的 type tag 有 254 个空位，因此外层的 MyOption 的 typetag 被优化掉了。</span><br></code></pre></td></tr></table></figure><h2 id="Struct、Tuple"><a href="#Struct、Tuple" class="headerlink" title="Struct、Tuple"></a>Struct、Tuple</h2><p>Struct、Tuple 等都属于 Product type。在实现中，往往是将所有字段依次存下来，并做额外的 padding。那么一个理所当然的优化是，如果 struct 的其中一个字段有空位，那么就可以将 enum tag 塞进去。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A1</span> (<span class="hljs-type">i8</span>, <span class="hljs-type">bool</span>);<br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-title function_ invoke__">A1</span>(<span class="hljs-number">1</span>, <span class="hljs-literal">false</span>)));<br><span class="hljs-comment">// [1, 0]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-title function_ invoke__">A1</span>(<span class="hljs-number">1</span>, <span class="hljs-literal">true</span>)));<br><span class="hljs-comment">// [1, 1]</span><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;(<span class="hljs-literal">None</span> <span class="hljs-keyword">as</span> <span class="hljs-type">Option</span>&lt;A1&gt;));<br><span class="hljs-comment">// [0, 2]</span><br></code></pre></td></tr></table></figure><p>我们再回到文章开头的例子。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span> (<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>);<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">B</span> (<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>, <span class="hljs-type">bool</span>);<br><br>dbg!(std::mem::size_of::&lt;A&gt;());<br><span class="hljs-comment">// 16</span><br>dbg!(std::mem::size_of::&lt;<span class="hljs-type">Option</span>&lt;A&gt;&gt;());<br><span class="hljs-comment">// 24</span><br>dbg!(std::mem::size_of::&lt;B&gt;());<br><span class="hljs-comment">// 16</span><br>dbg!(std::mem::size_of::&lt;<span class="hljs-type">Option</span>&lt;B&gt;&gt;());<br><span class="hljs-comment">// 16</span><br><br><span class="hljs-title function_ invoke__">print_memory</span>(&amp;<span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-title function_ invoke__">A</span>(<span class="hljs-number">1</span>, <span class="hljs-number">1</span>)));<br><span class="hljs-comment">// [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]</span><br><span class="hljs-comment">//  ^  ^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^ ^  ^^^^^^^^^^^^^^^^^^^</span><br><span class="hljs-comment">//  |           |                      |            |           |</span><br><span class="hljs-comment">//  |           |                      |            |           ------------ padding</span><br><span class="hljs-comment">//  |           |                      |            ------------------------ .1</span><br><span class="hljs-comment">//  |           |                      ------------------------------------- .0</span><br><span class="hljs-comment">//  |           ------------------------------------------------------------ padding</span><br><span class="hljs-comment">//  ------------------------------------------------------------------------ tag</span><br></code></pre></td></tr></table></figure><p>A 和 B 由于 padding，都需要占用 16 个 byte，而 <code>Option&lt;B&gt;</code> 由于存在一个 bool 字段 <code>.2</code>，tag 被优化进 bool 了，因此也只需要 16 个 byte。反而 <code>Option&lt;A&gt;</code> 实打实地用了 24 个 byte。</p><p>一个很容易想到的优化是，使用 padding 中未定义的内存来存储 type tag。比较遗憾的是，A 的 layout 是在编译 A 自身时确定的，而 <code>Option&lt;A&gt;</code> 在 <code>A</code> 的 padding 中存储的数据是未定义行为。这也导致了一个比较滑稽的结果，多存了一个字段，<code>Option</code> 占用的空间反而减少了。</p><p>当然，使用 padding 存储数据是完全可能的，但前提是不能影响子数据结构的 memory layout。如果我们将 <code>A</code> 在 <code>Option</code> 中<a href="https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=964744f99048e78207f6ee120329b9ce">手动展开</a>：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">use</span> std::mem::size_of;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span>(<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>);<br><span class="hljs-keyword">enum</span> <span class="hljs-title class_">OptionA</span> &#123; <span class="hljs-title function_ invoke__">Some</span>(<span class="hljs-type">i64</span>, <span class="hljs-type">i8</span>), <span class="hljs-literal">None</span> &#125;<br>dbg!(size_of::&lt;A&gt;()); <span class="hljs-comment">// 16</span><br>dbg!(size_of::&lt;<span class="hljs-type">Option</span>&lt;A&gt;&gt;()); <span class="hljs-comment">// 24</span><br>dbg!(size_of::&lt;OptionA&gt;()); <span class="hljs-comment">// 16</span><br></code></pre></td></tr></table></figure><p>这种情况下，由于 OptionA 的数据直接保存在 Some 内，实例化的时候完全可以使用 padding 存储 type tag，而不会引起潜在的未定义行为。</p><h2 id="优化自定义结构的可能性"><a href="#优化自定义结构的可能性" class="headerlink" title="优化自定义结构的可能性"></a>优化自定义结构的可能性</h2><p>目前，所有 enum layout 相关的优化都适合由编译器针对特定的类型进行 hack 来实现的，我们无法自己控制我们自定义的 struct 在 enum 中的 layout。为了优化一些常用场景，rust 又提供了 <a href="https://doc.rust-lang.org/std/num/struct.NonZeroI8.html"><code>NonZero*</code></a> 等辅助结构体，用来表示非 0 的整数，与此同时编译器会让 <code>size_of::&lt;Option&lt;NonZeroU8&gt;&gt; == size_of::&lt;u8&gt;</code>。但这只能由标准库 case by case 处理，而真实的需求是非常复杂的，比如有时候我们可能需要 <code>NonMaxU64</code>，或者例如使用了第三方库的 <a href="https://docs.rs/ordered-float/2.10.0/ordered_float/struct.NotNan.html#"><code>Option&lt;ordered_float::NotNan&lt;f64&gt;&gt;</code></a> 就无法被优化。</p><p>针对自定义接口的优化需要引入非常复杂的机制，在编译期告诉编译器一个类型非法的内存结构有哪些。我目前感觉一个可能的实现是 const trait + const iterator，给编译器提供潜在的非法值的迭代器。不过目前没有看到相关的 RFC。</p><p>这篇文章所有的 example 可以在 <a href="https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2cecbb4523443229d32a943bea2e48aa">Rust Playground</a> 找到。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今天学到了一点关于 Rust Enum 的冷知识，在开始阅读之前，大家可以猜一下下面的 Rust 代码在常见的 64 bit 机器上的输出是什么？&lt;/p&gt;
&lt;figure class=&quot;highlight rust&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs rust&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;A&lt;/span&gt; (&lt;span class=&quot;hljs-type&quot;&gt;i64&lt;/span&gt;, &lt;span class=&quot;hljs-type&quot;&gt;i8&lt;/span&gt;);&lt;br&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;B&lt;/span&gt; (&lt;span class=&quot;hljs-type&quot;&gt;i64&lt;/span&gt;, &lt;span class=&quot;hljs-type&quot;&gt;i8&lt;/span&gt;, &lt;span class=&quot;hljs-type&quot;&gt;bool&lt;/span&gt;);&lt;br&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;main&lt;/span&gt;() &amp;#123;&lt;br&gt;    dbg!(std::mem::size_of::&amp;lt;A&amp;gt;());&lt;br&gt;    dbg!(std::mem::size_of::&amp;lt;&lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;&amp;lt;A&amp;gt;&amp;gt;());&lt;br&gt;    dbg!(std::mem::size_of::&amp;lt;B&amp;gt;());&lt;br&gt;    dbg!(std::mem::size_of::&amp;lt;&lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;&amp;lt;B&amp;gt;&amp;gt;());&lt;br&gt;&amp;#125;&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;在这个 &lt;a href=&quot;https://play.rust-lang.org/?version=nightly&amp;mode=release&amp;edition=2021&amp;gist=bab644e35609b5475978821378d3560f&quot;&gt;Rust Playground&lt;/a&gt; 里可以看到结果。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Rust" scheme="https://blog.zhuangty.com/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>25 岁生日 —— 再启航</title>
    <link href="https://blog.zhuangty.com/life-25/"/>
    <id>https://blog.zhuangty.com/life-25/</id>
    <published>2021-08-26T00:00:00.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<h1 id="25-岁生日-——-再启航"><a href="#25-岁生日-——-再启航" class="headerlink" title="25 岁生日 —— 再启航"></a>25 岁生日 —— 再启航</h1><p><img src="https://user-images.githubusercontent.com/9161438/130897365-6a0e684d-8080-4dce-ac62-e95212246e46.png" alt="生日快乐"></p><!-- ![一条推特](https://user-images.githubusercontent.com/9161438/130823941-5360ab01-ed1a-4220-ab45-922cc0751ad3.png) --><p>即将迎来 25 岁生日，随便记录一些想法</p><span id="more"></span><h2 id="加入与离开阿里"><a href="#加入与离开阿里" class="headerlink" title="加入与离开阿里"></a>加入与离开阿里</h2><p>时间飞逝，回忆起决定加入阿里的日子，仿佛没有过去多久一样。</p><p><del>因为真的没有过去多久</del></p><p>我在阿里的这段经历大约一年半，从个人体验来说，甚至可以用<strong>远超预期</strong>来形容（毕竟外界对阿里的风评太差了导致我的预期非常低），我发现阿里也有很多不 PUA，踏踏实实做事的 leader 和氛围健康的组，仅仅因为阿里就贴死标签是不可取的，想来大厂做数据库的同学欢迎找我内推，极力推荐。</p><p>在阿里工作的这段时间，为我们组的产品做了一些还算有意思的 feature，有兴趣的可以看<a href="http://www.zhihu.com/org/polardb-x">我们组的专栏</a>中跟事务有关的文章。</p><p>之所以选择那么快离开，更多的是对自己的一些成长焦虑：比如感觉做的东西都是非常 trivial 的东西，明明花了不到一个月就完全搞懂了，却需要花费十倍以上的时间在跟一些历史包袱作斗争上；做事情很难，推事情很难；还有很多很多</p><p>我一度把这种成长焦虑误以为这是职级焦虑，毕竟某个scope内最年轻的P7听起来是个很好听的title，也带来了巨大的对保持快速晋升的渴望，看不到合适的机会是令人焦虑的。但我很快就发现这只是表象，在我观察了许多P8，发现他们能做的事情甚至可能还不如我之前作为一个 new grad 在旷视能做的事情多后，我明白这与职级无关，是一种对成长停滞的焦虑，这样的P8也并非是我现在想追求的东西<del>（当然他们的package还是很动人的）</del>。</p><p>在内部听过一个高P的经验分享，谈他们的成长经历，我发现他描述的几年前野蛮生长中的阿里云更符合我的期待，而这样的机会随着阿里云各个生态位的补足，已经非常稀缺了。现在的阿里云有很强的数据库团队，但没有我的机会。</p><h2 id="新的征程"><a href="#新的征程" class="headerlink" title="新的征程"></a>新的征程</h2><p>熟悉的朋友应该都已经猜到了，在经过多方比较以后，我最终选择了加入一家姚班学长创立的明星 startup，希望在一个新赛道能做出一些事情。虽然跟之前的方向不太 match，但我很看好这个新方向，也十分相信自己的能力。预期下周就会加入～</p><p>在跟 leader one one 的时候，我能感受到他的遗憾，其实我也十分遗憾，我们聊了很多事情，从团队，到阿里，到行业。在最后的最后，leader 叹了一口气，我跟他说，比起遗憾，你更应该祝我早日财富自由 ( ´ ▽ &#96; )ﾉ</p><p>在 leader 的祝福中，我结束了这段旅程。对于阿里这家公司，我没有什么好感（当然相比加入之前好很多），我更相信“一鲸落，万物生”。但对于团队的很多人，我还是非常感激与祝福。</p><p>在阿里的最后一个月，除了交接工作以外，我把之前积压下来想读的七八篇 paper 都看了一遍，写了好几篇文章，又看了不少 compiler 有关的东西，感觉停滞了一段时间的姿势水平又开始 +1s +1s 地流动了。</p><h2 id="一次生日与另一次生日"><a href="#一次生日与另一次生日" class="headerlink" title="一次生日与另一次生日"></a>一次生日与另一次生日</h2><p>每次生日都是非常开心的事情，但既满怀忐忑又充满希望的上一次生日应该是发生在八年前。</p><p><img src="https://user-images.githubusercontent.com/9161438/130823404-f4e54044-e148-4451-810d-abc57387d002.JPG" alt="破旧的准考证"></p><p>八年前的纯弟弟在一所野鸡区重点放弃学业一个人花了一年自学竞赛，这是他人生中第一次证明自己的考试前的一个生日。这次生日过后纯弟弟成了全校前后几十年唯一一个清北（按照当年人人网友的说法，二本变清华）。恭喜纯弟弟 🎉</p><p>那么这次纯哥哥也请相信自己的选择吧！</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;25-岁生日-——-再启航&quot;&gt;&lt;a href=&quot;#25-岁生日-——-再启航&quot; class=&quot;headerlink&quot; title=&quot;25 岁生日 —— 再启航&quot;&gt;&lt;/a&gt;25 岁生日 —— 再启航&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/9161438/130897365-6a0e684d-8080-4dce-ac62-e95212246e46.png&quot; alt=&quot;生日快乐&quot;&gt;&lt;/p&gt;
&lt;!-- ![一条推特](https://user-images.githubusercontent.com/9161438/130823941-5360ab01-ed1a-4220-ab45-922cc0751ad3.png) --&gt;

&lt;p&gt;即将迎来 25 岁生日，随便记录一些想法&lt;/p&gt;</summary>
    
    
    
    
    <category term="Life" scheme="https://blog.zhuangty.com/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>[Paper Notes] Noria: dynamic, partially-stateful data-flow for high-performance web applications</title>
    <link href="https://blog.zhuangty.com/noria/"/>
    <id>https://blog.zhuangty.com/noria/</id>
    <published>2021-08-22T14:19:34.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p>最近读了 <a href="https://pdos.csail.mit.edu/papers/noria:osdi18.pdf">Noria</a>，一个物化视图系统的实现（虽然它自称是 Dataflow）。这篇 Note 包含大量本人脑补。</p><p><img src="https://user-images.githubusercontent.com/9161438/130350687-07f358b7-4eb1-4e71-ac9c-c48d40aaf033.png" alt="Noria"></p><span id="more"></span><h2 id="Abstraction"><a href="#Abstraction" class="headerlink" title="Abstraction"></a>Abstraction</h2><p>从最简单的概念来说，Noria 就是一个异步的增量物化视图，每条更新被异步地推送到用户创建的每个 External View 上用于查询，这些是计算图的叶子节点。</p><p><img src="https://user-images.githubusercontent.com/9161438/130350909-df3ffc5f-a29c-4c8d-8126-63deb65cd174.png"></p><p>这跟 Streaming system 也比较像，唯一的区别是它本身就是个数据库，因此它持久化了 Base Table。主体不记录了，主要讲讲它的一些特殊设计。</p><h2 id="Join"><a href="#Join" class="headerlink" title="Join"></a>Join</h2><p><img src="https://user-images.githubusercontent.com/9161438/130347182-1092ff0f-6d92-45e7-bba1-5b8788a5b80b.png" alt="Window Join"></p><p>Window Join 是 Flink 的算子之一，Window Join 的两个上游算子的输入流仅在一个时间窗口内的才会被合并，而一些变体如 Interval Join，只不过是把这个时间窗口从一个矩形变成了三角形或者梯形。这种流式 Join 跟传统数据库的 Join 有很大区别，仅能处理一部分场景。很多 Join 往往并不是在一个窗口发生的，比如老用户和新订单之间的 Join 就无法通过这种 Window Join 完成，而需要借助一些其他的系统，比如维度表。当然 Flink 只是个流式计算引擎，不可能在 join 算子维护太多的状态，只支持一个窗口是正常的。Noria 的定位则是一个流式数据库，它希望解决所有的 Join 需求。Noria 会在 Base Table 上保存所有数据，而 Join 算子上仅保存部分状态。在 Join 无法匹配成功时，通过递归的 upquery 向上查找，直到落到 Base Table 为止。（这其实很类似 TP 里增量物化视图的做法了）</p><p><img src="https://user-images.githubusercontent.com/9161438/130350256-295dd847-ec99-43d2-b447-b82c282052c0.png" alt="Join Upquery"></p><h2 id="Partial-State-Upquery"><a href="#Partial-State-Upquery" class="headerlink" title="Partial State &amp;&amp; Upquery"></a>Partial State &amp;&amp; Upquery</h2><p>Partial State 和 Upquery 是贯穿 Noria 整个系统的，而不仅仅是为了解决 join 的问题。当我们在 Noria 里新建一个物化视图时，这个视图以及所有新建的算子都是 Empty State 的。这也就给 Noria 带来了一个好处，算子变更非常地简单且快速。在有对这个视图的查询到来时，会通过 Upquery 向上 Upquery 查询数据（直到 Base Table），并一路向下填充每个算子的状态。而当整个系统状态过多时，又会通过一些策略 Evict 掉一些算子的部分状态，在需要的时候重新 upquery 计算。这样整个系统的空间占用就是 bounded 的。</p><p><img src="https://user-images.githubusercontent.com/9161438/130351024-0c9f6030-3678-434b-a4e4-c4bafb3b0257.png" alt="Noria with evicted state"></p><h2 id="一致性"><a href="#一致性" class="headerlink" title="一致性"></a>一致性</h2><p>Noria 提供了 eventually consistency 的语义保障。对 Noria 的每个算子来说，会有两个操作：来自上游的 Update 和来自下游的 Upquery，Upquery 本身不会修改这个算子的状态，但 Upquery 的结果是会用于计算更下游算子的更新数据的（类似读后写），因此也会影响最终一致性。Noria 的做法是在每个算子上提供 Update&#x2F;Upquery 的 Ordering，类似于 Lock Based 的思路，而不是引入 MVCC。脑补了一下，如果是基于 MVCC 的实现，Upquery 会查到一个 snapshot 的数据来更新下游，那么就会稳定产生 write skew，而达不成 eventually consistent。</p><h2 id="一些个人看法"><a href="#一些个人看法" class="headerlink" title="一些个人看法"></a>一些个人看法</h2><p>我觉得 Noria 最大的亮点是这个设计把 Streaming 和 Query 做到了同一个引擎里，区别仅仅是在计算图上流向的区别 —— Query 是从叶结点（External View）到根节点（Base Table）向前地 Pull 数据并选择性地缓存，而 Streaming 则是从根节点向叶结点 Push 数据的更新。从另一个角度看，Noria 的每个 External View 都是 Logical View 和 Materialized View 按照一定比例混合的结果，而 Eviction 策略调整两者的比例，我们考虑两种情况：</p><ol><li>Evict All：所有算子都被当成 Stateless 的，这种情况下所有的查询都会走 upquery，等于典型的 AP 查询。</li><li>Evict None：所有算子都保存 Full State，所有查询都只走叶子节点的 External View。</li></ol><p>实际的状态则是多个维度 trade off 过的 Partial State：</p><ol><li>访问频率更高、重新计算代价更高、状态存储占用更低的状态更倾向于被保留。</li><li>访问频率更低、重新计算代价更低、状态存储占用更搞的状态更倾向于被淘汰。</li></ol><p>这种 Partial State 符合大量应用的特征，很多分析往往是更关注头部用户的分析结果，头部用户无论是产出内容还是粉丝数交互数都更多，相同的查询重新计算的代价也高。而应用的不活跃用户往往查询频率很低，且重新从 Base Table 计算代价也很低，用跟头部用户相同的状态（空间、写放大）为他们维护所有 View 是很不划算的事情。Noria 可以全自动地做这个事情。</p><p>从这个结果来看，Noria 在开头对自己的定位就非常精准了，它是一个 eventually consistency、near realtime、以及<strong>非常易于使用</strong>的 Redis 替代品，用户可以像直连数据库一样达到缓存的效果，同时还有更好的缓存语义。</p><p>这套设计可以极大改进现在应用开发者对数据库地使用方式，举个实践上更强的例子，现在网站对内容点赞、评论等的计数往往是需要业务手动维护：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">BEGIN</span>;<br><span class="hljs-keyword">INSERT INTO</span> `likes` (`user_id`, `tweet_id`) <span class="hljs-keyword">VALUES</span> (&quot;zty0826&quot;, &quot;496733277274013696&quot;);<br><span class="hljs-keyword">UPDATE</span> `tweets` <span class="hljs-keyword">SET</span> `like_count` <span class="hljs-operator">=</span> `like_count` <span class="hljs-operator">+</span> <span class="hljs-number">1</span> <span class="hljs-keyword">WHERE</span> `tweet_id` <span class="hljs-operator">=</span> &quot;496733277274013696&quot;;<br><span class="hljs-keyword">COMMIT</span>; <br></code></pre></td></tr></table></figure><p>除了简单的业务逻辑，我们还需要维护各个缓存的状态，like_count 只是最简单的状态之一。基于 Noria，我们可以用物化视图的方式去做这个：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">CREATE</span> MATERIALIZED <span class="hljs-keyword">VIEW</span> `tweet_likes` <span class="hljs-keyword">AS</span><br><span class="hljs-keyword">SELECT</span> `tweet_id`, <span class="hljs-built_in">count</span>(<span class="hljs-number">1</span>) <span class="hljs-keyword">FROM</span> `likes` <span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> `tweet_id`;<br></code></pre></td></tr></table></figure><p>不同于经典物化视图的是，Noria 里的 tweet_likes 表可以仅维护远远少于 tweets 表量级的状态。像 496733277274013696 这种热门、常看常新的 tweet 状态肯定会被长期缓存在 tweet_likes 表里，而大量冷门的、过期的甚至从来没有人看过的 tweet 则只会存在于 Base Table（likes）中，即使偶尔需要，查询代价也非常低，当一个冷门 tweet 114514114514 因为某种未知原因被查询，tweet_likes 表里早就淘汰了对应的数据时，Noria 会自动 Upquery，等效于对 Base Table 执行了一个查询：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-built_in">count</span>(<span class="hljs-number">1</span>) <span class="hljs-keyword">FROM</span> `likes` <span class="hljs-keyword">WHERE</span> `tweet_id` <span class="hljs-operator">=</span> &quot;114514114514&quot;;<br></code></pre></td></tr></table></figure><p>这个案例听起来更像 ”HSTP“（自造词） 而不是 HSAP？总之，大量复杂的逻辑都被 Noria 接管了，因此业务代码只需要维护最简单的逻辑就够了，通过一套系统处理这些还是很 fancy 的。</p><p>上面是从数据库物化视图的角度看待这个系统，接下来从 streaming system 的角度来看它解决的问题（主要是有 Base Table 就能做很多事情）：</p><ol><li>有对历史数据查询的能力，解决了 Join 算子语义不足的问题。</li><li>Paper 中没提，但根据我的想象，对 Watermark 的策略可以更激进和自动化，反正如果 Lateness 多了就可以 evict 掉中间算子的状态，重新从 Base Table 算。</li></ol><p>吹完喜欢的点，聊聊局限性：</p><ol><li>状态维护：Incremental materialized view 的状态维护逻辑本身就非常复杂，对所有 SQL 算子完整支持的复杂度远超查询引擎，而 Noria 为这个本来已经非常复杂的状态维护又引进了 Partial State。从 <a href="https://github.com/mit-pdos/noria">Noria 开源的代码</a>来看，它的算子支持是非常有限的。</li><li>操作支持：很多算子对 Update&#x2F;Delete 的支持是非常难的，比如 min&#x2F;max&#x2F;distinct&#x2F;…，Noria 直接绕过了这个问题（暂时只支持 Insert）。</li><li>确定性：基于这个设计，系统可以调整的参数太多，一个查询的不确定性更高，非常依赖于每个算子的 Eviction 策略，一旦预测错误就会引起大量查询甚至雪崩，这个要做好 QoS 的挑战也更大。</li><li>数据库本身的复杂性：Noria 也做了一些其他的工作，包括 Scalability 和容错，不过相比于其他数据库产品做得非常一般，这些都是一个完整的产品需要重复造轮子的部分。</li></ol><p>Noria 这篇 paper 做了一个开源实现，是一个 45k 行代码的 toy，而且我并没有 run 起来，疑似已经停止维护了。相对于真正可以使用的数据库产品，还有很多复杂的情况需要处理，不过这个设计思想还是给了很大的想象空间。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近读了 &lt;a href=&quot;https://pdos.csail.mit.edu/papers/noria:osdi18.pdf&quot;&gt;Noria&lt;/a&gt;，一个物化视图系统的实现（虽然它自称是 Dataflow）。这篇 Note 包含大量本人脑补。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/9161438/130350687-07f358b7-4eb1-4e71-ac9c-c48d40aaf033.png&quot; alt=&quot;Noria&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="Paper Note" scheme="https://blog.zhuangty.com/tags/Paper-Note/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
  </entry>
  
  <entry>
    <title>[Paper Notes] Napa: Powering Scalable Data Warehousing with Robust Query Performance at Google</title>
    <link href="https://blog.zhuangty.com/napa/"/>
    <id>https://blog.zhuangty.com/napa/</id>
    <published>2021-08-12T20:30:57.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p>Materialized View 成了最近数据库的新热潮，大数据三驾马车的原厂 Google 也发了一篇 PVLDB，介绍他们替代 Mesa 的新系统 Napa。<a href="http://vldb.org/pvldb/vol14/p2986-sankaranarayanan.pdf">Paper 链接</a>。随便分享一些 notes 和 unresolved issues（比较乱，不能作为 Paper 的替代品）</p><span id="more"></span><p>首先 brief 一下 Napa 这篇 paper 有比较有意思的点：</p><ol><li>精细的成本管理，把 trade off 的权利交给用户的同时避免了很繁琐的细节配置。</li><li>引入了类似 Watermark 的一个新概念 QT (Queryable Timestamp) 的来向用户描述 freshness（但跟 watermark 有区别）。</li></ol><h2 id="Napa-的核心概念"><a href="#Napa-的核心概念" class="headerlink" title="Napa 的核心概念"></a>Napa 的核心概念</h2><p>Napa 是 Google 用来替代 <a href="https://research.google/pubs/pub42851/">Mesa</a> 的新一代数仓，向 Google 的广告客户以及内部用于分析。一个最重要的变更是引入了 Materialized view 的概念来加速查询。有几个重要的指标是 Napa 关心的：</p><ol><li>Ingestion throughput：导入数据的速度是至关重要的</li><li>Query performance：查询请求的性能（延迟）</li><li>Data freshness：查询到的数据是否更新？一分钟内的数据还是几小时内的数据都行？</li><li>Resource Cost：每导入一定量数据需要的各种资源。</li></ol><p>当然除此之外，Durability 和 Fault tolerance 肯定是必要的。</p><p>Ingestion throughput 是不可放弃的选择（数据都导不进去谈什么查询），剩下的都可以交给客户端来 trade off。</p><h3 id="LSMT"><a href="#LSMT" class="headerlink" title="LSMT"></a>LSMT</h3><p>Napa 中直接导入数据的表称为 base table，而每个 base table 会有若干关联的 materialized view（n to m 的关系，每个 materialized view 也可能不止关联一个 table）。每个 Materialized view 的结构类似一颗 LSM Tree，一条外部数据从其他系统导入到 Napa 需要经过一系列过程：</p><ol><li>Ingestion<br> 对应到 LSM Tree 里应该就是 write memtable + WAL 的过程，不过这里不需要写 memtable，这个类似 WAL 的结构叫做 non-queryable delta，由于更新是 apply 到 base table 上的，所以这个数据只需要写一份 base table，就可以认为 ingestion 成功了，保证了 durability。Ingestion 的过程也会在多个机房同步。</li><li>Compation and view aintenance: Napa 的 Compaction 混合了好几个概念，我们一个个抽出来：<ul><li>Non-queryable delta -&gt; View Queryable delta<br>  根据我们上面的描述，Non-queryable delta 只是 base table 的 WAL，那么我们首先需要把这个 log 中的每一项更新读出来，并根据每个 Materialized view 定义的算子进行转换，确定是否要 apply 到对应的 view 中。同时我们也会做一些排序、索引的工作，最后生成的是 n 个 queryable delta（对应于 LSM 中 SST 的概念），n 为需要更新的 view 的数量。</li><li>Queryable delta merge: 与 LSM 的 merge 完全相同，定期将一部分 Queryable delta 合并为一个更大的 Queryable delta。</li></ul></li></ol><p>而对一个 view 进行一次 query 的过程跟 LSM 也完全相同，即并行地在若干个 delta 进行查询，并将结果合并。</p><p>而几个关键的指标基本都受到这套体系的影响：</p><ol><li>Ingestion throughput：如同 LSM Tree 一样，ingestion 成功即导入成功，高度写优化，导入飞快。</li><li>Query performance，取决于两个要素：<ul><li>view 的数量，view 的查询性能会比 base table 更好</li><li>查询的时候需要对多少个 delta file 查询，越多性能越差</li></ul></li><li>Data freshness：Non-queryable 的数据是完全不可读的，因此查询的结果至少会延迟到 non-queryable delta 的合并。除此之外，我们还可以自己选择仅读取一部分的 queryable delta，这样会牺牲 freshness，但是能提高 query performance。</li><li>Resource costs：Base Table 的更新是必要的，因此可以认为完全是 view 的维护成本，取决于 view 的数量和 compaction 的频率。</li></ol><h3 id="Queryable-Timestamp"><a href="#Queryable-Timestamp" class="headerlink" title="Queryable Timestamp"></a>Queryable Timestamp</h3><p><img src="https://user-images.githubusercontent.com/9161438/129359740-0114a772-9dca-4b06-9480-869001f4dda8.png" alt="QT"></p><p>QT 是一个表示 freshness 的概念，Now() - QT 代表了 frsehness 的 bound。QT 有一个上限，就是不能超过 Non-queryable delta 里的下界（因为 Non-queryable delta 完全不可查询），QT 是受到用户配置影响的。查询时会合并到 QT 为止所有的 delta（而不是全部的 delta）。</p><h3 id="tradeoff-query-performance"><a href="#tradeoff-query-performance" class="headerlink" title="tradeoff query performance"></a>tradeoff query performance</h3><p>要 data freshness，但 query 可以慢点儿</p><ol><li>少建 view，慢慢查</li><li>少做 view aintenance task（delta file 会特别多）</li><li>QT 设置得足够高（查询时会合并所有的 delta file 中的结果）</li></ol><h3 id="tradeoff-data-freshness"><a href="#tradeoff-data-freshness" class="headerlink" title="tradeoff data freshness"></a>tradeoff data freshness</h3><p>需要 query performance，但是读到的数据可以旧点儿</p><ol><li>少做 view aintenance（delta file 会特别多）</li><li>QT 设置得足够低（查询时需要合并的 delta file 特别少）</li></ol><h3 id="tradeoff-resource-costs"><a href="#tradeoff-resource-costs" class="headerlink" title="tradeoff resource costs"></a>tradeoff resource costs</h3><p>既要 query performance，又要 data freshness。</p><p><img src="https://user-images.githubusercontent.com/9161438/129360955-e3701121-8e26-45da-a970-90e40930ae2e.png" alt="我全都要"></p><p>没有什么是充钱解决不了的。</p><ol><li>多建 view。</li><li>频繁做 view aintenance。</li><li>QT 设置得足够高</li></ol><p><img src="https://user-images.githubusercontent.com/9161438/129365009-5c3e40a3-6a95-45ff-b4ad-aaec7355038d.png" alt="tradeoff"></p><h2 id="外部系统"><a href="#外部系统" class="headerlink" title="外部系统"></a>外部系统</h2><p><img src="https://user-images.githubusercontent.com/9161438/129364456-ace178bc-ea96-4250-bf4f-ad88a8c299b3.png" alt="Architecture"></p><p>Google 的 infra 是真的强，所以我感觉其实 Napa 做的最重要的事情就是上面这个 LSMT 了，其他的都通过外部系统解决。Napa 使用了 Colossus（下一代 GFS）做文件存储，并且用 F1 query 做了物化视图的 Planner 和 Optimizer（我觉得是工程量最大的一部分），以及面向客户端的 query servering。Napa 自身更多负责视图的维护。</p><h2 id="Others"><a href="#Others" class="headerlink" title="Others"></a>Others</h2><p><strong>物化视图的自动淘汰</strong></p><p>文中提了很多 challenges，但其中我感觉比较关键的一点，物化视图的及时淘汰。对用户来说，QT 是个 database 级别的概念，如果有一个物化视图的更新比较慢（也许是视图太复杂，或者 plan 优化不够），那么 Now() - QT 就会越来越大，freshness 无法保证，需要及时淘汰掉这些 view（读到这里的时候我才发现原来 view 可能不是用户指定创建的，居然还是可以自动加减的。。就离谱）。</p><p><strong>为什么 QT 不是 Watermark？</strong></p><p>回收一个开头的疑问，因为 Napa 并不是一个 streaming system，它的输入是 Ordering 的（或者说完全基于 Process time 而非 Event time）。Watermark 描述的是 query 的 completeness（即 query time &lt; watermark 代表一定能读到完整的结果），而 QT 描述的是 freshness（即 query time &lt; QT 可以获得符合预期的性能）。</p><p><strong>Napa 提供了怎么样的一致性？</strong></p><p>从我个人的 taste 来看，一个让人用得舒服的数据库，无论是 TP 还是 AP，提供 atomic batch update 和 global snapshot read （即使是 stale snapshot）是必要的选择。这篇 paper 关于一致性的描述非常少，不过两点观察：</p><ol><li>Mesa 内置了 MVCC，提供了 strong consistency，Napa 作为 Mesa 的 drop-in replacement 不提供的话我感觉用户不会买帐？</li><li>LSMT 的架构下做 MVCC 是非常简单的事情。</li></ol><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p>总体来说感觉还是比较中规中矩的一些 idea，不过也可以看出 Google 对工程细节的把控非常深了。比如同样是 LSMT 的架构，我怀疑换成 TiDB 的话，robust query performance 可能更取决于查询线程池的调度、gRPC 各种不确定因素而非 delta file 的数量。只有在工程细节上优化得足够好，才能在各个指标上更加可控的 trade off configuration。</p><p>数据库服务需要很强的确定性，相比于 auto driven 来说，这种 trade off configuration 说不定对用户是个更好的选择。（然后我准备去看另一篇 PVLDB auto driven 了）</p><p><del>彩蛋：<a href="blog.zhuangty.com">blog.zhuangty.com</a> 终于用上 Google analytics 了，说不定 tygg 在看报表的时候也用上 napa 了</del></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Materialized View 成了最近数据库的新热潮，大数据三驾马车的原厂 Google 也发了一篇 PVLDB，介绍他们替代 Mesa 的新系统 Napa。&lt;a href=&quot;http://vldb.org/pvldb/vol14/p2986-sankaranarayanan.pdf&quot;&gt;Paper 链接&lt;/a&gt;。随便分享一些 notes 和 unresolved issues（比较乱，不能作为 Paper 的替代品）&lt;/p&gt;</summary>
    
    
    
    
    <category term="Paper Note" scheme="https://blog.zhuangty.com/tags/Paper-Note/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
  </entry>
  
  <entry>
    <title>用可持久化 B+ 树优化 OFFSET 子句</title>
    <link href="https://blog.zhuangty.com/optimize-offset-with-persistent-bptree/"/>
    <id>https://blog.zhuangty.com/optimize-offset-with-persistent-bptree/</id>
    <published>2021-08-10T22:47:38.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://user-images.githubusercontent.com/9161438/126614791-22d7a7cc-cf5f-4384-8b87-6ff91b7c0047.png" alt="某网站的分页"></p><p>分页功能是网站常见的需求之一，对应到数据库中的实现，通常会用 <code>LIMIT ? OFFSET ?</code> 的子句来实现，然而这是很多网站被攻击的潜在原因之一。在主流的数据库实现中，这种查询的效率往往非常的低下。本文脑洞了一种高效支持 OFFSET LIMIT 的方法。</p><span id="more"></span><p>首先让我们看看到底有多慢。以 MySQL 为例：</p><p>首先我们在 mysql 中创建一张简单的表，并用 sysbench 导入 10M 条数据：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sh">sysbench --mysql-host=127.0.0.1 --mysql-db=<span class="hljs-built_in">test</span> --mysql-user=root oltp_point_select prepare --table-size=10000000<br></code></pre></td></tr></table></figure><p>这并不是很大的一个数据量，然后我们执行一条简单查询：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> test.sbtest1 limit <span class="hljs-number">10</span> <span class="hljs-keyword">offset</span> <span class="hljs-number">9000000</span>;<br><br><span class="hljs-number">10</span> <span class="hljs-keyword">rows</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">set</span><br><span class="hljs-type">Time</span>: <span class="hljs-number">7.993</span>s<br></code></pre></td></tr></table></figure><p>这样一个简单的查询需要花费 8 秒左右的时间，而且当我用 sysbench 去压这条 SQL 的时候它直接挂了。在具体的实现，OFFSET LIMIT 这种操作基本都是通过扫描来实现的，很难跳过前序的行数，而 OFFSET 越大意味着需要扫描的数据越多。一个正常的用户通常只会刷前几页的数据，但是被 hack 的时候就很难说了，也许只需要把 url 里的 pageNo&#x3D;1 改成 pageNo&#x3D;10000 并且高并发请求一下，一些网站就挂了，当然现在这种场景已经很少了。</p><p>很难跳过不代表无法跳过，我们从 MySQL 底层的索引数据结构 B+ 树说起。</p><p><img src="https://user-images.githubusercontent.com/9161438/128887249-df068b4a-bfb2-480a-a12b-95d613103938.png" alt="B+ 树"></p><p>上图是一个简单的两层4阶 B+ 树，由一个非叶节点和四个叶节点组成，叶节点之间通过链表连接，用于提高 scan 性能。</p><p>我们想做一个分页查询</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> t <span class="hljs-keyword">OFFSET</span> <span class="hljs-number">4</span> LIMIT <span class="hljs-number">2</span>;<br></code></pre></td></tr></table></figure><p>对应到存储结构上，就是希望找到这个索引树上第四个到第五个元素 <code>btree.range(4, 6)</code>，20 和 24。目前的实现里，我们显然只能左到右扫描，在无意义地访问了连个不需要的节点后，我们才能找到正确的节点和对应的数据。很显然，如果我们在每个非叶节点上实时维护所有子树的 count 的话，我们就能更快地找到结果。</p><p><img src="https://user-images.githubusercontent.com/9161438/128887368-1cc052a0-6114-433a-bc9d-7a26e57eaa02.png" alt="带计数的 B+ 树"></p><p>通过非叶节点上的 count 记录，我们就可以先通过 root 节点的 count，确认第四条纪录从第三个叶节点开始，然后直接找到数据并开始扫描，总体时间是 $log(n)$ 的复杂度。当然，这会导致我们每次插入或者删除数据的时候也需要更新一整条树路径上的所有非叶节点。</p><p><strong>MVCC</strong></p><p>在支持事务的数据库里，往往使用 MVCC 来减少读写之间的锁冲突。MVCC 在叶节点中的数据，存的是一串链表（O2N），代表了数据的更新记录，而 seq 代表了。支持了 MVCC 后，我们很快发现我们的 count-B+ tree 不好使了。</p><p>我们分别进行了一次插入，一次修改和一次删除，这也就导致了我们查询不同 version 的时候，正确的结果是不一样的。</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-function"><span class="hljs-title">insert</span><span class="hljs-params">(<span class="hljs-number">18</span>, <span class="hljs-string">&quot;j1&quot;</span>, version=<span class="hljs-number">3</span>)</span></span><br><span class="hljs-function"><span class="hljs-title">update</span><span class="hljs-params">(<span class="hljs-number">16</span>, <span class="hljs-string">&quot;d2&quot;</span>, version=<span class="hljs-number">5</span>)</span></span><br><span class="hljs-function"><span class="hljs-title">delete</span><span class="hljs-params">(<span class="hljs-number">20</span>, version=<span class="hljs-number">7</span>)</span></span><br></code></pre></td></tr></table></figure><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> t <span class="hljs-keyword">OFFSET</span> <span class="hljs-number">4</span> LIMIT <span class="hljs-number">2</span>; <span class="hljs-operator">/</span><span class="hljs-operator">/</span> version <span class="hljs-operator">=</span> <span class="hljs-number">2</span><br><span class="hljs-operator">|</span>key<span class="hljs-operator">|</span><span class="hljs-keyword">value</span><span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">20</span> <span class="hljs-operator">|</span>e1   <span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">24</span> <span class="hljs-operator">|</span>f1   <span class="hljs-operator">|</span><br><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> t <span class="hljs-keyword">OFFSET</span> <span class="hljs-number">4</span> LIMIT <span class="hljs-number">2</span>; <span class="hljs-operator">/</span><span class="hljs-operator">/</span> version <span class="hljs-operator">=</span> <span class="hljs-number">4</span><br><span class="hljs-operator">|</span>key<span class="hljs-operator">|</span><span class="hljs-keyword">value</span><span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">18</span> <span class="hljs-operator">|</span>j1   <span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">20</span> <span class="hljs-operator">|</span>e1   <span class="hljs-operator">|</span><br><span class="hljs-keyword">SELECT</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">FROM</span> t <span class="hljs-keyword">OFFSET</span> <span class="hljs-number">4</span> LIMIT <span class="hljs-number">2</span>; <span class="hljs-operator">/</span><span class="hljs-operator">/</span> version <span class="hljs-operator">=</span> <span class="hljs-number">8</span><br><span class="hljs-operator">|</span>key<span class="hljs-operator">|</span><span class="hljs-keyword">value</span><span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">18</span> <span class="hljs-operator">|</span>j1   <span class="hljs-operator">|</span><br><span class="hljs-operator">|</span><span class="hljs-number">20</span> <span class="hljs-operator">|</span>e1   <span class="hljs-operator">|</span><br></code></pre></td></tr></table></figure><p>我们的计数只能支持对最新数据的查询，而在修改发生之前已经创建好的 ReadView，我们就无能为力了。</p><p><strong>多版本计数</strong></p><p>既然数据可以做多版本，那么我们的计数理所当然也可以。</p><p><img src="https://user-images.githubusercontent.com/9161438/128887776-fc7bbbb5-0aef-4fce-b83c-6b56f343be83.png" alt="带多版本计数的 B+ 树"></p><p>在多版本计数上，我们“如愿以偿”地查到了我们想要的结果。</p><p><strong>B+ 树的分裂</strong></p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-function"><span class="hljs-title">insert</span><span class="hljs-params">(<span class="hljs-number">37</span>, <span class="hljs-string">&quot;k1&quot;</span>, version=<span class="hljs-number">9</span>)</span></span><br></code></pre></td></tr></table></figure><p>上面的思路依然是死路一条，我们考虑 B+ 树的分裂和合并，多版本计数是完全不可维护的。</p><p><img src="https://user-images.githubusercontent.com/9161438/128887786-c17dba97-c356-431f-8305-cee179b0aa2d.png" alt="多版本计数B+树的分裂"></p><p><strong>可持久化数据结构</strong></p><p>我们先直接看一下我们用可持久化 B+ 树优化后的结果，为了简化做图，我们假设我们仅做了两次插入：</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-function"><span class="hljs-title">insert</span><span class="hljs-params">(<span class="hljs-number">18</span>, <span class="hljs-string">&quot;j1&quot;</span>, version=<span class="hljs-number">2</span>)</span></span><br><span class="hljs-function"><span class="hljs-title">insert</span><span class="hljs-params">(<span class="hljs-number">37</span>, <span class="hljs-string">&quot;k1&quot;</span>, version=<span class="hljs-number">3</span>)</span></span><br></code></pre></td></tr></table></figure><p><img src="https://user-images.githubusercontent.com/9161438/128887963-4e63911d-7fd2-4eb9-8aac-3a727a30ac79.png" alt="可持久化B+树"></p><p>可以看到，相比于原来的 B+ 树，我们引入了两个变化：</p><ol><li>不再就地修改数据。</li><li>删除了叶节点之间的指针（这删了还能叫 B+ 树吗？-_-）</li></ol><p>在第一次进行插入后，我们直接 copy 了一份根节点和第二个叶节点，并插入数据，新的节点（黄色）另外三个指针依然指向原来的叶节点，只有第二个指针指向了新的节点。</p><p>而第二次插入直接引发了分裂，因此我们一共创建了五个新节点， 包括：</p><ol><li>原先的第四个节点分裂后的新的叶节点（30，37 开头）</li><li>原先的 root 节点分裂后的两个非叶节点上（10、30 开头的绿色节点）</li><li>新的 root 节点</li></ol><p>可以看到一波操作之后，我们拥有了三个版本的 B+ 树！每个版本都是一个符合我们预期的全局快照，我们查找的时候可以先快速找到需要的版本，然后在每个版本上快速地进行符合我们优化预期分页查询。与此同时，我们也并没有占用三倍的空间，理论的空间复杂度是 $n+m log(n)$ 其中 n 是树的大小，m 是维护的版本数。</p><p><a href="https://en.wikipedia.org/wiki/Persistent_data_structure">可持久化数据结构</a> 是一个数据结构上的概念，但跟 MVCC 不谋而合。而当我们尝试在数据库上使用了可持久化B+树后，我们事实上是把 MVCC 做到了整个索引的数据结构里，而非行记录里。整个索引结构从 <code>BPTree&lt;Key, PersistentList&lt;Value&gt;&gt;</code> 转变为了 <code>List&lt;PersistentBPTree&lt;Key, Value&gt;&gt;</code>。</p><p>另一个问题是，为什么我把叶节点之间的连接指针删掉了，因为不删没法做，原因可以留给读者思考一下，这也是可持久化数据结构的局限性之一。</p><p><strong>Mixing mode</strong></p><p>考虑到这种实现的 clone 代价还是太高，也许可以混合三种方式做这个优化：</p><ol><li>UPDATE 时，只添加行记录，做行级 MVCC。</li><li>INSERT&#x2F;DELETE 但不涉及树的结构变更时，使用多版本计数（可以记在内存里，反正可以恢复出来）。</li><li>发生树结构的变更时，直接创建一个新版本的 B+ 树。</li></ol><p>查询时：</p><ol><li>先通过全局的版本链表找到一个 B+ 树。</li><li>通过 B+ 树非叶节点上的多版本计数，快速定位到行记录。</li><li>通过行记录上的多版本 MVCC，定位到需要读取的数据。</li></ol><p>（实现上想想就很恶心，所以连图都不想画了）</p><p><strong>后记</strong></p><p><del>后来还写了个 Rust 的实现，但写得不太好不想开源了，反正可持久化数据结构无脑 Arc 就对了</del></p><p>分页其实是个很小众的需求，在数据库支持不佳的情况下，业务上也有了很多替换解决方案，完全没有必要花很大代价去做优化，所以这篇文章还是搞笑为主的（不搞笑我肯定发 paper 了谁写 blog 啊），但仔细想想发现这个 idea 居然还挺 novel 的。。。</p><ol><li>一种不同于 LSM 的 append only B+ 树存储引擎方案</li><li>不修改数据结构，天然无锁并发，不需要处理 B+ 树复杂的细粒度锁逻辑</li><li>可以在非叶节点上支持很多<strong>满足事务隔离要求</strong>的预聚合，不局限于分页</li><li>做 interval GC 很容易</li></ol><p>当然代价也是很明显的，<strong>写放大和空间放大前所未有的大</strong>，所以还是没有意义 ：）</p><p>好玩为主，很久没有 follow 过存储引擎的东西了，idea 如有雷同都是巧合（</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/9161438/126614791-22d7a7cc-cf5f-4384-8b87-6ff91b7c0047.png&quot; alt=&quot;某网站的分页&quot;&gt;&lt;/p&gt;
&lt;p&gt;分页功能是网站常见的需求之一，对应到数据库中的实现，通常会用 &lt;code&gt;LIMIT ? OFFSET ?&lt;/code&gt; 的子句来实现，然而这是很多网站被攻击的潜在原因之一。在主流的数据库实现中，这种查询的效率往往非常的低下。本文脑洞了一种高效支持 OFFSET LIMIT 的方法。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
    <category term="Algorithm" scheme="https://blog.zhuangty.com/tags/Algorithm/"/>
    
  </entry>
  
  <entry>
    <title>使用 const generics 实现类型安全的 Builder Pattern</title>
    <link href="https://blog.zhuangty.com/builder-pattern-with-const-generics/"/>
    <id>https://blog.zhuangty.com/builder-pattern-with-const-generics/</id>
    <published>2021-08-01T20:55:00.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p>一篇搞笑文章 ：（</p><span id="more"></span><p><strong>Builder Pattern</strong></p><p><a href="https://doc.rust-lang.org/1.0.0/style/ownership/builders.html">Builder Pattern</a> 是 rust 在复杂对象构造上推荐的一种设计模式，一个常见的 Builder 实现：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span> &#123;<br>    a: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-meta">#[derive(Default)]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">ABuilder</span> &#123;<br>    a_: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">ABuilder</span> &#123;<br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">a</span>(<span class="hljs-keyword">self</span>, a1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-keyword">Self</span> &#123;<br>        <span class="hljs-keyword">self</span>.a = a1;<br>        <span class="hljs-keyword">self</span><br>    &#125;<br><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">b</span>(<span class="hljs-keyword">self</span>, b1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-keyword">Self</span> &#123;<br>        <span class="hljs-keyword">self</span>.b = b1;<br>        <span class="hljs-keyword">self</span><br>    &#125;<br><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">c</span>(<span class="hljs-keyword">self</span>, c1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-keyword">Self</span> &#123;<br>        <span class="hljs-keyword">self</span>.c = c1;<br>        <span class="hljs-keyword">self</span><br>    &#125;<br><br>    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">finish</span>(<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> A &#123;<br>        A &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-meta">#[cfg(test)]</span><br><span class="hljs-keyword">mod</span> tests &#123;<br>    <span class="hljs-keyword">use</span> crate::ABuilder;<br>    <span class="hljs-meta">#[test]</span><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">it_works</span>() &#123;<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">_a</span> = ABuilder::<span class="hljs-title function_ invoke__">default</span>().<span class="hljs-title function_ invoke__">a</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">c</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">b</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">finish</span>();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><hr><p><strong>必选参数</strong></p><p>这时候我们接到了一些奇怪的需求，要把 a 和 b 作为必选参数（那为什么不把 a 和 b 传进 ABuilder::new 的参数呢，小编也很好奇，但是这么写就水不了文章了）。</p><p>我们可以为 ABuilder 引入三个 bit 的常量状态，标识每个参数是否被设置：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-meta">#![feature(const_generics)]</span><br><span class="hljs-meta">#![feature(const_evaluatable_checked)]</span><br><br><span class="hljs-keyword">enum</span> <span class="hljs-title class_">Assert</span>&lt;<span class="hljs-keyword">const</span> COND: <span class="hljs-type">bool</span>&gt; &#123;&#125;<br><br><span class="hljs-keyword">trait</span> <span class="hljs-title class_">IsTrue</span> &#123;&#125;<br><br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">IsTrue</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Assert</span>&lt;<span class="hljs-literal">true</span>&gt; &#123;&#125;<br><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span> &#123;<br>    a: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-meta">#[derive(Default)]</span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">ABuilder</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; &#123;<br>    a: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; ABuilder&lt;S&gt; &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">a</span>(<span class="hljs-keyword">self</span>, a1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">1</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">1</span>&#125;&gt; &#123;<br>            a: a1,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">b</span>(<span class="hljs-keyword">self</span>, b1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">0b10</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">0b10</span>&#125;&gt; &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: b1,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">c</span>(<span class="hljs-keyword">self</span>, c1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt; &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: c1,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; ABuilder&lt;S&gt; <span class="hljs-keyword">where</span> Assert::&lt;&#123;S &amp; <span class="hljs-number">0b110</span> == <span class="hljs-number">0b110</span>&#125;&gt;: IsTrue &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">finish</span>(<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> A &#123;<br>        A &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-meta">#[cfg(test)]</span><br><span class="hljs-keyword">mod</span> tests &#123;<br>    <span class="hljs-keyword">use</span> crate::ABuilder;<br>    <span class="hljs-meta">#[test]</span><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">it_works</span>() &#123;<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">_a</span> = ABuilder::&lt;<span class="hljs-number">0</span>&gt;::<span class="hljs-title function_ invoke__">default</span>().<span class="hljs-title function_ invoke__">a</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">c</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">b</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">finish</span>();<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">_b</span> = ABuilder::&lt;<span class="hljs-number">0</span>&gt;::<span class="hljs-title function_ invoke__">default</span>().<span class="hljs-title function_ invoke__">a</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">c</span>(<span class="hljs-number">0</span>).<span class="hljs-title function_ invoke__">finish</span>(); <span class="hljs-comment">// Compilation failed</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>迫于无奈，我们开了两个 incomplete feature 来做这个需求，一路顶着 warnings 编译成功了。</p><p>在上面的实现里，我们通过一个  <code>Assert</code> 的 trick，允许我们在 impl 的 block 上为常量参数 S 添加条件判断，而 S 本质上就是一个 bitflags，标识了某一个参数是否被设置过，为此我们仅为 <code>ABuilder&lt;S&gt; where Assert::&lt;{S &amp; 0b110 == 0b110}&gt;: IsTrue</code> 实现 finish 方法，这就满足了我们的需求。</p><p>报错大概长这样</p><figure class="highlight maxima"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs maxima"><span class="hljs-built_in">error</span>[E0599]: the <span class="hljs-built_in">method</span> `finish` exists <span class="hljs-keyword">for</span> struct `ABuilder&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt;`, but its trait bounds were <span class="hljs-keyword">not</span> satisfied<br>  --&gt; src/lib.rs:<span class="hljs-number">70</span>:<span class="hljs-number">62</span><br>   |<br><span class="hljs-number">4</span>  | enum Assert&lt;const COND: bool&gt; &#123;&#125;<br>   | ----------------------------- doesn&#x27;t satisfy `Assert&lt;&#123;S &amp; <span class="hljs-number">0b110</span> == <span class="hljs-number">0b110</span>&#125;&gt;: IsTrue`<br>...<br><span class="hljs-number">17</span> | struct ABuilder&lt;const S: u64&gt; &#123;<br>   | ----------------------------- <span class="hljs-built_in">method</span> `finish` <span class="hljs-keyword">not</span> found <span class="hljs-keyword">for</span> this<br>...<br><span class="hljs-number">70</span> |         <span class="hljs-built_in">let</span> _b = ABuilder::&lt;<span class="hljs-number">0</span>&gt;::default().a(<span class="hljs-number">0</span>).c(<span class="hljs-number">0</span>).finish();<br>   |                                                     ^^^^^^ <span class="hljs-built_in">method</span> cannot be called on `ABuilder&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt;` due to unsatisfied trait bounds<br>   |<br>   = note: the following trait bounds were <span class="hljs-keyword">not</span> satisfied:<br>           `Assert&lt;&#123;S &amp; <span class="hljs-number">0b110</span> == <span class="hljs-number">0b110</span>&#125;&gt;: IsTrue`<br></code></pre></td></tr></table></figure><hr><p><strong>只能传递一次的参数</strong></p><p>此时我们又对 c 提了一些奇怪需求，我们希望 c 是可选参数，但是最多只会被传递一次（即 0 或 1 次）：</p><p>举一反三，这个需求太好改了。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-meta">#![feature(const_generics)]</span><br><span class="hljs-meta">#![feature(const_evaluatable_checked)]</span><br><br><span class="hljs-keyword">enum</span> <span class="hljs-title class_">Assert</span>&lt;<span class="hljs-keyword">const</span> COND: <span class="hljs-type">bool</span>&gt; &#123;&#125;<br><br><span class="hljs-keyword">trait</span> <span class="hljs-title class_">IsTrue</span> &#123;&#125;<br><br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">IsTrue</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Assert</span>&lt;<span class="hljs-literal">true</span>&gt; &#123;&#125;<br><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">A</span> &#123;<br>    a: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-meta">#[derive(Default)]</span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">ABuilder</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; &#123;<br>    a: <span class="hljs-type">i32</span>,<br>    b: <span class="hljs-type">i32</span>,<br>    c: <span class="hljs-type">i32</span>,<br>&#125;<br><br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; ABuilder&lt;S&gt; <span class="hljs-keyword">where</span> Assert::&lt;&#123;S &amp; <span class="hljs-number">1</span> == <span class="hljs-number">0</span>&#125;&gt;: IsTrue &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">a</span>(<span class="hljs-keyword">self</span>, a1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">1</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">1</span>&#125;&gt; &#123;<br>            a: a1,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">b</span>(<span class="hljs-keyword">self</span>, b1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">0b10</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">0b10</span>&#125;&gt; &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: b1,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; ABuilder&lt;S&gt; <span class="hljs-keyword">where</span> Assert::&lt;&#123;S &amp; <span class="hljs-number">0b100</span> == <span class="hljs-number">0</span>&#125;&gt;: IsTrue &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">c</span>(<span class="hljs-keyword">self</span>, c1: <span class="hljs-type">i32</span>) <span class="hljs-punctuation">-&gt;</span> ABuilder&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt; &#123;<br>        ABuilder::&lt;&#123;S | <span class="hljs-number">0b100</span>&#125;&gt; &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: c1,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">impl</span>&lt;<span class="hljs-keyword">const</span> S: <span class="hljs-type">u64</span>&gt; ABuilder&lt;S&gt; <span class="hljs-keyword">where</span> Assert::&lt;&#123;S &amp; <span class="hljs-number">0b110</span> == <span class="hljs-number">0b110</span>&#125;&gt;: IsTrue &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">finish</span>(<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> A &#123;<br>        A &#123;<br>            a: <span class="hljs-keyword">self</span>.a,<br>            b: <span class="hljs-keyword">self</span>.b,<br>            c: <span class="hljs-keyword">self</span>.c,<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-meta">#[cfg(test)]</span><br><span class="hljs-keyword">mod</span> tests &#123;<br>    <span class="hljs-keyword">use</span> crate::ABuilder;<br><br>    <span class="hljs-meta">#[test]</span><br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">it_works</span>() &#123;<br>        <span class="hljs-keyword">let</span> <span class="hljs-variable">_a</span> = ABuilder::&lt;<span class="hljs-number">0</span>&gt;::<span class="hljs-title function_ invoke__">default</span>().<span class="hljs-title function_ invoke__">a</span>(<span class="hljs-number">1</span>).<span class="hljs-title function_ invoke__">b</span>(<span class="hljs-number">1</span>).<span class="hljs-title function_ invoke__">c</span>(<span class="hljs-number">1</span>).<span class="hljs-title function_ invoke__">finish</span>();<br>        <span class="hljs-comment">// let _b = ABuilder::&lt;0&gt;::default().a(1).c(1).c(1).b(1).finish();</span><br>        <span class="hljs-comment">// let _c = ABuilder::&lt;0&gt;::default().a(1).c(1).finish();</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>总结</strong></p><p>这确实是一篇搞笑文章，所有需求都是我在学习 const generics 先进语法的时候随便做的实验，事实上 Builder Pattern 并不适用于这些奇怪的需求，而且用到了 <code>const_evaluatable_checked</code> 这种纸糊的 feature 也不可能用于生产。有一个真正用于生产的 crate <a href="https://github.com/idanarye/rust-typed-builder">typed-builder</a> 思路跟我类似，不过直接生成了一个长度为 n （n 为 fields 的数量）的 tuple 来记录状态，更合理一些。不过基于 <code>const_evaluatable_checked</code> 可以实现很多奇奇怪怪的需求，比如可以做编译期状态压缩 DP（rustc 爆炸中），还可以做一些奇奇怪怪的限制（比如限制一个方法最少被调用 n 次，最多被调用 m 次，完全想不到什么场景需要），可以认为是对 Rust typesafe state machine 能力的一个强化了，对于一些比较相似的状态转换过程，我们可以直接基于 const generics 来减少重复代码（DRY）。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;一篇搞笑文章 ：（&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Rust" scheme="https://blog.zhuangty.com/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>Copy &amp; Paste 三行代码让 TiDB 性能翻倍</title>
    <link href="https://blog.zhuangty.com/tidb-2-times-faster/"/>
    <id>https://blog.zhuangty.com/tidb-2-times-faster/</id>
    <published>2021-07-10T14:07:02.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>标题党，今天给 TiDB 水了个有意思的 PR，随便写个 blog 记录一下。<strong>文末粉丝福利</strong></p><span id="more"></span><p>上午摸鱼的时候，读了雷宇哥哥的文章 <a href="https://internals.tidb.io/t/topic/174">I beat TiDB with 20 LOC</a>，这篇文章非常有意思，推荐 go 吹&#x2F;go 黑都可以读一读，通过这篇文章，我发现了 cgo 比想象的要快很多，以及 go 编译器比想象更烂，以至于 cgo 的 overhead 完全抵消了还不够。在学习 TiDB 先进经验的时候，看到了一段有意思的代码：</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *builtinArithmeticPlusIntSig)</span></span> plusSS(result *chunk.Column, lhi64s, rhi64s, resulti64s []<span class="hljs-type">int64</span>) <span class="hljs-type">error</span> &#123;<br>    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">len</span>(lhi64s); i++ &#123;<br>        <span class="hljs-keyword">if</span> result.IsNull(i) &#123;<br>            <span class="hljs-keyword">continue</span><br>        &#125;<br><br>        lh, rh := lhi64s[i], rhi64s[i]<br>        <span class="hljs-keyword">if</span> (lh &gt; <span class="hljs-number">0</span> &amp;&amp; rh &gt; math.MaxInt64-lh) || (lh &lt; <span class="hljs-number">0</span> &amp;&amp; rh &lt; math.MinInt64-lh) &#123;<br>            <span class="hljs-keyword">return</span> types.ErrOverflow.GenWithStackByArgs(<span class="hljs-string">&quot;BIGINT&quot;</span>, fmt.Sprintf(<span class="hljs-string">&quot;(%s + %s)&quot;</span>, b.args[<span class="hljs-number">0</span>].String(), b.args[<span class="hljs-number">1</span>].String()))<br>        &#125;<br><br>        resulti64s[i] = lh + rh<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><p>要理解这段代码，首先我们需要理解什么是 chunk。chunk 是一种列式内存格式，是 <a href="https://arrow.apache.org/">Apache Arrow</a> 的一种实现，具体可以参考 <a href="https://pingcap.com/blog-cn/tidb-source-code-reading-10/">TiDB 源码阅读系列文章（十）Chunk 和执行框架简介</a>。对于这里的 Int 类型，Column 的内部其实是一个 int64 array 和一个 bitmap。</p><p><img src="https://download.pingcap.com/images/blog-cn/tidb-source-code-reading-10/1.png" alt="Int64Column 的实现"></p><p>这段代码的目的就是将 lhi64s 和 rhi64s 加和的结果保存到 resulti64s 中。为了彻底理解这段代码，我们还需要关注一下调用它的函数 <code>builtinArithmeticPlusIntSig.vecEvalInt</code>：</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *builtinArithmeticPlusIntSig)</span></span> vecEvalInt(input *chunk.Chunk, result *chunk.Column) <span class="hljs-type">error</span> &#123;<br>n := input.NumRows()<br><br><span class="hljs-comment">// ...</span><br><span class="hljs-comment">// Calculate lh and rh</span><br>result.MergeNulls(lh)<br>result.MergeNulls(rh)<br><br>lhi64s := lh.Int64s()<br>rhi64s := rh.Int64s()<br>resulti64s := result.Int64s()<br><br>isLHSUnsigned := mysql.HasUnsignedFlag(b.args[<span class="hljs-number">0</span>].GetType().Flag)<br>isRHSUnsigned := mysql.HasUnsignedFlag(b.args[<span class="hljs-number">1</span>].GetType().Flag)<br><br><span class="hljs-keyword">switch</span> &#123;<br><span class="hljs-keyword">case</span> isLHSUnsigned &amp;&amp; isRHSUnsigned:<br>err = b.plusUU(result, lhi64s, rhi64s, resulti64s)<br><span class="hljs-keyword">case</span> isLHSUnsigned &amp;&amp; !isRHSUnsigned:<br>err = b.plusUS(result, lhi64s, rhi64s, resulti64s)<br><span class="hljs-keyword">case</span> !isLHSUnsigned &amp;&amp; isRHSUnsigned:<br>err = b.plusSU(result, lhi64s, rhi64s, resulti64s)<br><span class="hljs-keyword">case</span> !isLHSUnsigned &amp;&amp; !isRHSUnsigned:<br>err = b.plusSS(result, lhi64s, rhi64s, resulti64s)<br>&#125;<br><span class="hljs-keyword">return</span> err<br>&#125;<br></code></pre></td></tr></table></figure><p>这段代码主要是根据加法算子两侧表达式的类型，决定调用具体的实现，而 plusSS 则是其中的一种（两个有符号整数相加）。但在调用具体的实现之前，已经对 lh 和 rh 分别调用了 <code>result.MergeNulls()</code>，因此在 <code>plusSS</code> 中只需要对 result 是否为空进行判断，决定是否跳过计算。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-keyword">if</span> result.IsNull(i) &#123;<br>    <span class="hljs-keyword">continue</span><br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-keyword">if</span> (lh &gt; <span class="hljs-number">0</span> &amp;&amp; rh &gt; math.MaxInt64-lh) || (lh &lt; <span class="hljs-number">0</span> &amp;&amp; rh &lt; math.MinInt64-lh) &#123;<br>    <span class="hljs-keyword">return</span> types.ErrOverflow.GenWithStackByArgs(<span class="hljs-string">&quot;BIGINT&quot;</span>, fmt.Sprintf(<span class="hljs-string">&quot;(%s + %s)&quot;</span>, b.args[<span class="hljs-number">0</span>].String(), b.args[<span class="hljs-number">1</span>].String()))<br>&#125;<br></code></pre></td></tr></table></figure><p>理解剩下的代码就非常直观了，做一个 overflow 判断，然后返回相加的结果。这也是针对不同符号的 Plus 函数最大的区别。</p><p>在 MySQL 大部分 expression 的逻辑，对于输入参数包含 NULL 的情况，输出参数往往也是 NULL，TiKV 为了 DRY，<a href="https://github.com/tikv/tikv/pull/8331/files">使用宏简化了这个逻辑</a>。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-meta">#[rpn_fn]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">like</span>&lt;C: Collator&gt;(target: BytesRef, pattern: BytesRef, escape: &amp;<span class="hljs-type">i64</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Result</span>&lt;<span class="hljs-type">Option</span>&lt;<span class="hljs-type">i64</span>&gt;&gt; &#123;<br>    <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-title function_ invoke__">Some</span>(<br>        like::like::&lt;C&gt;(target, pattern, *escape <span class="hljs-keyword">as</span> <span class="hljs-type">u32</span>)? <span class="hljs-keyword">as</span> <span class="hljs-type">i64</span><br>    ))<br>&#125;<br></code></pre></td></tr></table></figure><p>这段代码进行宏展开后和上面的 go 代码类似，如果参数有 null 的情况会直接跳过真实 like 的调用。但对于 Plus 这种基础的数学运算函数，事情就有了一些变化。一个常识是，分支的代价其实比 add 这种基础指令大很多，在高效的列式执行逻辑里，这里引入了两个 branch 操作，相比于运算（一次 add）本身来说，这两个 branch 才是最大的 overhead。</p><p>且这两个 branch 是必要的 check，同时也是 unlikely 的（大概率会走 else 分支）。在完全不改变代码逻辑的情况下，我们可以<a href="https://github.com/pingcap/tidb/pull/25466">将它们优化成一个 branch</a>。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *builtinArithmeticPlusIntSig)</span></span> plusSS(result *chunk.Column, lhi64s, rhi64s, resulti64s []<span class="hljs-type">int64</span>) <span class="hljs-type">error</span> &#123;<br><span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">len</span>(lhi64s); i++ &#123;<br>lh, rh := lhi64s[i], rhi64s[i]<br><br><span class="hljs-keyword">if</span> (lh &gt; <span class="hljs-number">0</span> &amp;&amp; rh &gt; math.MaxInt64-lh) || (lh &lt; <span class="hljs-number">0</span> &amp;&amp; rh &lt; math.MinInt64-lh) &#123;<br><span class="hljs-keyword">if</span> result.IsNull(i) &#123;<br><span class="hljs-keyword">continue</span><br>&#125;<br><span class="hljs-keyword">return</span> types.ErrOverflow.GenWithStackByArgs(<span class="hljs-string">&quot;BIGINT&quot;</span>, fmt.Sprintf(<span class="hljs-string">&quot;(%s + %s)&quot;</span>, b.args[<span class="hljs-number">0</span>].String(), b.args[<span class="hljs-number">1</span>].String()))<br>&#125;<br>&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>是的，代码改动只有三行，且只是原封不动 Copy &amp; Paste（点题），我们来看看 benchmark 的结果。TiDB 非常贴心地提供了表达式模块的 benchmark 脚本，我们可以直接使用：</p><p><img src="https://user-images.githubusercontent.com/9161438/125152817-e68d9000-e181-11eb-970d-ba39ed400017.png" alt="Benchmark PlusInt"></p><p>可以看到，四种函数的提升都是非常可观的。其中第一个 Case 更是直接性能翻倍。</p><p>这个优化的原理是非常简单的，<code>Plus</code> 这种基础函数满足两个特性：</p><ol><li>运算本身比 check 轻量。</li><li>运算不会 crash，且没有副作用。</li></ol><p>那么我们可以直接删除 null check 的三行代码，因为 null 的结果已经被写到 result 的 bitmap 里了，这里无脑做一下计算就可以。但由于 overflow check 的存在，我们不能返回非预期的 overflow 错误（可以说 plus 运算本身无状态，overflow check 引入了状态），因此如果发生了 overflow 的话，我们得确认本身不是 null，如果确认不是 null 的话，才返回错误。从某种意义上，这个优化可以认为是手动帮 CPU 做流水线执行。</p><hr><p><img src="https://user-images.githubusercontent.com/9161438/125152990-49335b80-e183-11eb-82cb-dc800574c956.png"></p><p>上面的 PR 是我花十分钟写的，我是想再给力一点的，但是懒（躺），下面只说失败的尝试和思路。</p><p>首先是想办法优化掉 overflow check 的 branch，我先把 overflow check 的 branch 提取到外侧：</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *builtinArithmeticPlusIntSig)</span></span> plusSS(result *chunk.Column, lhi64s, rhi64s, resulti64s []<span class="hljs-type">int64</span>) <span class="hljs-type">error</span> &#123;<br>+       <span class="hljs-keyword">var</span> hasOverflow <span class="hljs-type">int64</span> = <span class="hljs-number">0</span><br>        <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">len</span>(lhi64s); i++ &#123;<br>                lh, rh := lhi64s[i], rhi64s[i]<br><br>-               <span class="hljs-keyword">if</span> (lh &gt; <span class="hljs-number">0</span> &amp;&amp; rh &gt; math.MaxInt64-lh) || (lh &lt; <span class="hljs-number">0</span> &amp;&amp; rh &lt; math.MinInt64-lh) &#123;<br>-                       <span class="hljs-keyword">if</span> result.IsNull(i) &#123;<br>-                               <span class="hljs-keyword">continue</span><br>-                       &#125;<br>-                       <span class="hljs-keyword">return</span> types.ErrOverflow.GenWithStackByArgs(<span class="hljs-string">&quot;BIGINT&quot;</span>, fmt.Sprintf(<span class="hljs-string">&quot;(%s + %s)&quot;</span>, b.args[<span class="hljs-number">0</span>].String(), b.args[<span class="hljs-number">1</span>].String()))<br>-               &#125;<br>-<br>+               hasOverflow |= (b2i(lh &gt; <span class="hljs-number">0</span>) &amp; b2i(rh &gt; math.MaxInt64-lh)) | (b2i(lh &lt; <span class="hljs-number">0</span>)&amp;b2i(rh &lt; math.MinInt64-lh))&amp;b2iNot(result.IsNull(i))<br>                resulti64s[i] = lh + rh<br>        &#125;<br>+<br>+       <span class="hljs-keyword">if</span> <span class="hljs-type">uint64</span>(hasOverflow) &gt; <span class="hljs-number">0</span> &#123;<br>+               <span class="hljs-keyword">return</span> types.ErrOverflow.GenWithStackByArgs(<span class="hljs-string">&quot;BIGINT&quot;</span>, <span class="hljs-string">&quot;overflow&quot;</span>)<br>+       &#125;<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br> &#125;<br></code></pre></td></tr></table></figure><p>这个优化对逻辑是有变更的，因为报错信息里没法输出正确的数字了，但也不是完全没有办法，考虑到 overflow 是小概率事件，我们可以发现 overflow 以后再循环一次，拿到具体 overflow 的数字。</p><p><code>b2i</code> 和 <code>b2iNot</code> 大概就是 bool2Int，具体懒得贴了，反正大道至简不需要解释。实测效果大概 benchmark 低了一倍，我又懒得看汇编调优（下次高兴了再水一篇 blog）。猜测有几个原因：</p><ol><li>Overflow check 比较重，原来可以短路求值的计算现在强制计算了，代价超过了 branch 运算。</li><li>Go 的优化比较拉，我写的位运算也比较拉。</li></ol><p>但总之想优化掉 overflow check 是一件 non-trivial 的事情，</p><p>既然解决不了问题，我们不妨解决提问题的人。对于 AP 性能有极致追求的用户，我们可以提供某种 non-strict sql_mode，允许整型 overflow，结果是未定义的，这样我们就能自由地去掉 check 了。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs golang"><span class="hljs-comment">// 这里已经不需要四种符号的版本了</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(b *builtinArithmeticPlusIntSig)</span></span> plusNonStrict(result *chunk.Column, lhi64s, rhi64s, resulti64s []<span class="hljs-type">int64</span>) &#123;<br>        <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-built_in">len</span>(lhi64s); i++ &#123;<br>                lh, rh := lhi64s[i], rhi64s[i]<br>                resulti64s[i] = lh + rh<br>        &#125;<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><hr><p><img src="https://user-images.githubusercontent.com/9161438/125152990-49335b80-e183-11eb-82cb-dc800574c956.png"></p><p>能啊，直接看雷宇哥哥文章的 SIMD 版本 <a href="https://internals.tidb.io/t/topic/174">I beat TiDB with 20 LOC</a>。</p><p>只能说，TiDB 的 Executor 还有很长的路要走（</p><hr><p><strong>你是不是标题党？这哪里提升两倍了？</strong></p><p>是，expression 的 benchmark 翻倍，但在整个 SQL 的生命流程中只是微不足道的一部分，特别是 TP 的请求。即使对于复杂的 AP 请求，除非是完全从本地节点的 cache memory engine 中读取的数据，否则相同的数据量网络开销大概率也会超过这部分的优化效果（特别是 TiDB 和 TIKV 交互还用的 gRPC，那就更拉了）。</p><hr><p>说好的福利（伪）：我改完 Plus 就懒得改了，目测 Sub&#x2F;And&#x2F;Or&#x2F;Not&#x2F;Xor&#x2F;… 肯定都能提升的，Mul&#x2F;Div&#x2F;Mod 可能需要 benchmark 一下才知道有没有提升，同时 TiKV 上也可以水一堆，感兴趣的可以去水一个 PR 薅 TiDB 的 new contributor 周边杯子。有能力的也可以把 non-strict Plus 实现一下（也很 trivial，就是要思考一下用户接口）。</p><!--option = {    title: {        text: 'PlusInt',        subtext: ''    },    tooltip: {        trigger: 'axis'    },    legend: {        data: ['优化前', '优化后']    },    toolbox: {        show: true,        feature: {            dataView: {show: false, readOnly: false},            magicType: {show: false, type: ['line', 'bar']},            restore: {show: false},            saveAsImage: {show: true}        }    },    calculable: true,    xAxis: [        {            type: 'category',            data: ['VecBuiltinFunc-12', 'VecBuiltinFunc#01-12', 'VecBuiltinFunc#02-12', 'VecBuiltinFunc#03-12']        }    ],    yAxis: [        {            type: 'value'        }    ],    series: [        {            name: '优化前',            type: 'bar',            data: [251072, 505320, 446617, 375538],            markPoint: {                data: [                    {type: 'max', name: '最大值'},                    {type: 'min', name: '最小值'}                ]            },        },        {            name: '优化后',            type: 'bar',            data: [537535, 806791, 487568, 442723],            markPoint: {                data: [                    {name: '年最高', value: 182.2, xAxis: 7, yAxis: 183},                    {name: '年最低', value: 2.3, xAxis: 11, yAxis: 3}                ]            },        }    ]};-->]]></content>
    
    
    <summary type="html">&lt;p&gt;标题党，今天给 TiDB 水了个有意思的 PR，随便写个 blog 记录一下。&lt;strong&gt;文末粉丝福利&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
    <category term="TiDB" scheme="https://blog.zhuangty.com/tags/TiDB/"/>
    
    <category term="Open Source" scheme="https://blog.zhuangty.com/tags/Open-Source/"/>
    
  </entry>
  
  <entry>
    <title>五一摸鱼周记：更新 Blog 主题、水 PR</title>
    <link href="https://blog.zhuangty.com/update-blog-theme/"/>
    <id>https://blog.zhuangty.com/update-blog-theme/</id>
    <published>2021-05-06T13:09:06.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>灌水一篇，这篇文章会介绍：</p><ul><li>更新Blog主题的<strong>底层逻辑</strong></li><li>利用 vercel serverless <strong>赋能</strong> blog 的 slogan</li><li>打好 hexo-fluid-theme 和 cusdis 的<strong>组合拳</strong></li><li><strong>反哺</strong> cusdis 的生态</li></ul><p>🐶狗头保命</p><span id="more"></span><p>更新 blog 主题是一个比写 blog 文章快乐多了的事情，这也是 blog 新手常常陷入的一个陷阱 —— 精心配置一整天的主题、评论、评论、插件，然后写下一篇 类似于 Hello World 的《使用XXX 搭建 blog》之后从此吃灰。为了避免自己陷入这个陷阱，我搭 blog 的时候给自己定下了一个规则 —— <strong>每次写一篇文章才能更新一次与文章无关的 blog 配置</strong>。</p><p>最终的结果是 —— 我既没有保持合适的更新频率，也没机会折腾主题，直接使用了烂大街的 <a href="https://github.com/theme-next/hexo-theme-next">hexo next</a>，没有评论，没有 Analytics，甚至连 Hello World 都没写，创造了一个三无 blog。</p><p>直到今年开始，我成功更新了两篇文章（这里非常感谢  <a href="https://taio.app/">Taio App</a>，让我在手机上也能快乐地写 blog），适当地让 blog 更易用一些也提上了日程</p><p>第一件事情自然是喜闻乐见的换皮，选了<br><a href="https://github.com/fluid-dev/hexo-theme-fluid">hexo-fluid-theme</a>，就觉得挺好看的，配置也比较完善，代码也不复杂，看了看感觉如果有必要的话（其实很快就有必要了），我自己也改得动。</p><p>配置的过程中，遇到了两个麻烦，一个是这个主题必须配置一个 banner_img，而且默认的太丑了，于是为了提高辨识度，我随手画了一个更丑的，以后有机会再优化（下次一定）。另一个是主页上要求写一个 slogan，我想把 <a href="https://github.com/TennyZhuang/Chi-Corpus">迟语录 chi_corpus</a> 随机显示在主页上，但是 hexo-fluid-theme 只支持 json 格式，即使我 commit 一个 json 格式上去，也没法做到随机返回，独立维护一个转换服务又会极大增加我的运维负担，因此动了薅 vercel 羊毛的心思。翻了翻 <a href="https://vercel.com/docs/serverless-functions/introduction">vercel serverless</a> 的使用文档，感觉使用起来非常简单，而事实上也是如此。仅仅是花了五分钟，添加一个四十行的 go 文件，我就轻松达到了我的目的。可以在<a href="/">首页</a>观看效果。赞美 vercel（*1）！</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">const</span> CHI_CORPUS_URL = <span class="hljs-string">&quot;https://raw.githubusercontent.com/TennyZhuang/Chi-Corpus/master/common.txt&quot;</span><br><br><span class="hljs-keyword">type</span> Data <span class="hljs-keyword">struct</span> &#123;<br>Content <span class="hljs-type">string</span> <span class="hljs-string">`json:&quot;content&quot;`</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fetchChiCorpus</span><span class="hljs-params">()</span></span> (*Data, <span class="hljs-type">error</span>) &#123;<br>resp, err := http.Get(CHI_CORPUS_URL)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br>&#125;<br><span class="hljs-keyword">defer</span> resp.Body.Close()<br>body, err := ioutil.ReadAll(resp.Body)<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err<br>&#125;<br>content := <span class="hljs-type">string</span>(body)<br>lines := strings.Split(content, <span class="hljs-string">&quot;\n&quot;</span>)<br>line := lines[rand.Intn(<span class="hljs-built_in">len</span>(lines))]<br><span class="hljs-keyword">return</span> &amp;Data&#123;<br>Content: line,<br>&#125;, <span class="hljs-literal">nil</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Handler</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;<br>data, err := fetchChiCorpus()<br><span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>w.WriteHeader(http.StatusInternalServerError)<br>w.Write([]<span class="hljs-type">byte</span>(err.Error()))<br>&#125; <span class="hljs-keyword">else</span> &#123;<br>w.Header().Set(<span class="hljs-string">&quot;Content-Type&quot;</span>, <span class="hljs-string">&quot;application/json; charset=utf-8&quot;</span>)<br>json.NewEncoder(w).Encode(data)<br>&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>本来鸽子博主已经决定更新到这里结束了，看了推友 <a href="https://twitter.com/frostming90?s=21">@frostming90</a> 的 <a href="https://frostming.com/2021/04-28/self-host-comment-system/">blog</a>（别人的 blog 真好看啊），于是进行了一番抄作业。再吹一次，vercel 真的好用（*2），无脑就把一个 blog 后台跑起来了，而且还是白嫖。这次遇到一个新问题，就是 hexo-fluid-theme 支持了许多 comment plugin，但还不支持 cusdis。顺手 fork 了一个支持了一下，顺便提了个 PR <a href="https://github.com/fluid-dev/hexo-theme-fluid/pull/474">https://github.com/fluid-dev/hexo-theme-fluid/pull/474</a>。目前 console 里还有个奇妙的报错 <code>Function called outside component initialization</code>，但似乎不影响 plugin 的使用，有知道怎么修的也可以带带我，前端技能不太熟练了：（</p><p>目前对 cusdis 还有一些小小的问题，比如评论仅支持审核后显示，似乎不支持默认显示的方式，使用起来比较麻烦，后续考虑 contribute 一下。欢迎试用新整的评论系统～</p><p>个人搭建并维护 blog 还是比较麻烦的事情，一个关键技巧是在<strong>掌控数据</strong>的前提下尽可能依赖第三方服务。这次的整套组合拳打下来基本也就花了半天的时间在折腾（没有 vercel 可能一天起步了，点赞*3），但数据是以可以掌控的格式（postgresql，可以自己备份）存储的。考虑到 <a href="https://www.45office.com/">Donald Trump 被封禁到搭建了自己的个人博客</a>，可见 blog 这种去中心化的组织形式还是有必要的。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;灌水一篇，这篇文章会介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新Blog主题的&lt;strong&gt;底层逻辑&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;利用 vercel serverless &lt;strong&gt;赋能&lt;/strong&gt; blog 的 slogan&lt;/li&gt;
&lt;li&gt;打好 hexo-fluid-theme 和 cusdis 的&lt;strong&gt;组合拳&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反哺&lt;/strong&gt; cusdis 的生态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🐶狗头保命&lt;/p&gt;</summary>
    
    
    
    
    <category term="Blog" scheme="https://blog.zhuangty.com/tags/Blog/"/>
    
    <category term="Water" scheme="https://blog.zhuangty.com/tags/Water/"/>
    
  </entry>
  
  <entry>
    <title>解锁 TiDB Hackathon 一等奖的新体验：TiDB + Wasm</title>
    <link href="https://blog.zhuangty.com/tidb-hackathon-2020/"/>
    <id>https://blog.zhuangty.com/tidb-hackathon-2020/</id>
    <published>2021-04-20T20:00:00.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>前段时间，因为比较活跃在 TiDB 社区，所以顺手参加了 TiDB 2020 Hackathon。这次选的参赛题目是一个老生常谈的功能：UDF（User Defined Function）。</p><span id="more"></span><h2 id="彩蛋"><a href="#彩蛋" class="headerlink" title="彩蛋"></a>彩蛋</h2><p>尽管是完全与安全方向无关的主题，但我们随手取了个队名 ‘ or 0&#x3D;0 or ‘（读作「引号or零等于零or引号」），除了可以卖萌，而且还能用于 SQL 注入，感兴趣的可以看看<a href="https://zhuanlan.zhihu.com/p/24471576">万能密码</a>这个 topic。很意外的是还引发了 hackathon 过程中一个有趣的小插曲，在看到我们的队名后东旭和姚老板纷纷吐槽太有年代感了：</p><p><img src="https://user-images.githubusercontent.com/9161438/111075420-e4dcf400-8522-11eb-8203-b9b834926bbc.png" alt="年代感.jpg"></p><p>但仅仅过了半天，在我们 hack 的途中，我们的队友 breeswish 就无意中发现 TiDB 的内部 SQL 存在原始的字符串拼接参数且没有校验参数合法性，成功地进行了一次提权攻击 ：D</p><p><img src="https://user-images.githubusercontent.com/9161438/111075565-8fedad80-8523-11eb-9fe9-9bdee5a54435.PNG" alt="提权注入成功"></p><h2 id="我们想做什么？"><a href="#我们想做什么？" class="headerlink" title="我们想做什么？"></a>我们想做什么？</h2><p>UDF 是每届 TiDB Hackathon 的常客，每次都会有 UDF 的 proposal，这里面也包括我在 2018 年参赛时实现的基于 lua 的 UDF。当时的实现非常粗糙，我们在 TiDB&#x2F;TiKV 上直接起了一个 lua vm，然后允许用户以 CREATE FUNCTION 的语法把 lua 函数上传到 TiDB 并保存在 PD，同时支持用户通过 <code>call_lua(fn_name, args)</code> 的语法来调用 UDF。TiDB&#x2F;TiKV 的执行器会从 PD 加载函数 body，并通过 lua vm 执行。这个方案有许多问题，以至于只能是一个 demo，始终无法落地：</p><ol><li>lua 生态较差，仅适用于实现一些功能简单的函数。而类似方法支持更多语言需要 TiDB 内置多种 vm。</li><li>与 vm  的交互开销较高，UDF lua 相比 native 函数，执行效率偏低。</li></ol><p>lua UDF 这个项目也因此很遗憾没有拿奖。<del>一个没被采用的队名：MAKE UDF GREAT AGAIN!</del></p><p>这次重新捡起了 UDF 这个 topic，主要是时机相比与 18 年已经成熟了，有两个重要的情况发生了变化：</p><ol><li>VM 生态：我在 18 年选择使用 lua 做 UDF 是无奈之选。大部分系统的扩展能力都是通过 lua 实现的，因为缺少一个兼具安全性、高性能、通用性的轻量 VM。而 Wasm 的到来很好地帮我们解决了这个问题。很多 infra 项目比如 <a href="https://www.envoyproxy.io/">Envoy</a>、<a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">OpenShift</a> 也开始用 Wasm 作为扩展的 runtime。</li><li>云原生时代：UDF 是满足用户自定义需求的一个重要能力。在私有化部署的 TiDB 上，用户可以通过修改开源版本代码的方式实现各种自定义需求，但随着云原生时代的到来，更多的用户选择基于云的 DBaaS 方案使用 TiDB，云上标准化的 TiDB 需要一个完善的标准 UDF 方案。同时基于云的 UDF 也可以跟用户云上其他生态实现互通，比如可以用 UDF 实现对对象存储数据的读取，达到异构存储的目的，或者利用 UDF 调用一些机器学习模型进行人脸识别，再拿识别的结果与其他数据表进行匹配（join）。基于 UDF 的能力，这些需求都可以在 TiDB 内部完成。</li></ol><p>这次再用同一个 topic 参赛，但我们不再是 idea driven hackathon，更多关注的是方案可落地，<strong>让 UDF 这个选题从未来的 TiDB Hackathon 中消失</strong>，在设计之初，我们就定下了若干目标：</p><ol><li>灵活性：应该支持更多的编程语言进行 UDF 的编写。不同人对编程语言的喜好大相庭径，强迫 TiDB 的用户使用 lua 编写 UDF 并不是一种优美的做法。</li><li>安全性：数据库的安全性至关重要，应该保证函数在沙箱环境运行，不能造成权限泄漏，也应该尽可能减少对业务稳定性的影响。</li><li>高效性：性能接近 Native 函数，用户不需要担心额外的 overhead。</li><li>功能：在保证安全的前提下，提供数据库内部的 API 以及对外的网络 API 到 UDF 的沙箱环境，同时这些 API 也在权限控制的管理下。</li></ol><h2 id="我们做了什么？"><a href="#我们做了什么？" class="headerlink" title="我们做了什么？"></a>我们做了什么？</h2><p>为了不引入过多过重的 runtime，增加维护的心智负担，我们选择了 Wasm 作为解决方案。Wasm 本身我不过多介绍了，感兴趣的同学可以在很多地方找到详细的介绍。最关键的是，Wasm 拥有我们想要的所有 feature。Wasm 是面向浏览器的沙箱环境安全执行设计的一种 low-level 指令，同时也兼备接近 native 代码的高性能，之后也被扩展到非浏览器的平台运行。（其实我觉得这玩意早该有了，但确实还是要依赖各大巨头合作才能定义出一套大家都接受的标准）</p><p>选定了 Wasm 作为技术方案以后，剩下的工作就是选择一个合适的 runtime，并且嵌入到 TiDB&#x2F;TiKV 的执行器中。由于同时需要兼容 C、Go、Rust 语言，我们选择了 <a href="https://wasmer.io/">Wasmer</a> 作为我们 demo 时的 runtime，并且使用了 llvm backend 达到了接近 native 的性能。</p><h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;emscripten/emscripten.h&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdint.h&gt;</span></span><br><br><span class="hljs-type">uint64_t</span> EMSCRIPTEN_KEEPALIVE<br><span class="hljs-title function_">udf_main</span><span class="hljs-params">(<span class="hljs-type">uint64_t</span> a, <span class="hljs-type">uint64_t</span> b)</span> &#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br></code></pre></td></tr></table></figure><p>假设用户先基于 emscripten 工具链实现了一个简单的函数 aplusb，由于这里的类型完全是 Wasm 原生类型，因此也不需要做任何额外的处理。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ emcc aplusb.c -O3 -o aplusb.wasm --no-entry -s ERROR_ON_UNDEFINED_SYMBOLS=0<br><span class="hljs-comment"># 由于 Wasm 是一种 binary 的格式，我们做了一些简单的处理</span><br>$ <span class="hljs-built_in">cat</span> aplusb.wasm | <span class="hljs-built_in">od</span> -v -t x1 -A n | <span class="hljs-built_in">tr</span> -d <span class="hljs-string">&#x27; \n&#x27;</span><br>0061736d010000000117056000017f60000060017f0060017f017f60027e7e017e0307060104000203000405017001020205060101800280020609017f01419088c0020b077a08066d656d6f72790200087564665f6d61696e0001195f5f696e6469726563745f66756e6374696f6e5f7461626c6501000b5f696e697469616c697a650000105f5f6572726e6f5f6c6f636174696f6e000509737461636b5361766500020c737461636b526573746f726500030a737461636b416c6c6f6300040907010041010b01000a30060300010b0700200020017c0b040023000b0600200024000b1000230020006b4170712200240020000b05004180080b<br></code></pre></td></tr></table></figure><p>用户可以通过 CREATE FUNCTION 命令将这段 wasm bytecode 传给 TiDB：</p><figure class="highlight asciidoc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs asciidoc">mysql&gt; CREATE FUNCTION aplusb WASM_BYTECODE x&quot;0061736d010000000117056000017f60000060017f0060017f017f60027e7e017e0307060104000203000405017001020205060101800280020609017f01419088c0020b077a08066d656d6f72790200087564665f6d61696e0001195f5f696e6469726563745f66756e6374696f6e5f7461626c6501000b5f696e697469616c697a650000105f5f6572726e6f5f6c6f636174696f6e000509737461636b5361766500020c737461636b526573746f726500030a737461636b416c6c6f6300040907010041010b01000a30060300010b0700200020017c0b040023000b0600200024000b1000230020006b4170712200240020000b05004180080b&quot;;<br>Query OK, 0 rows affected (0.33 sec)<br><br><span class="hljs-section">mysql&gt; SELECT aplusb(1, 4);</span><br><span class="hljs-section">+--------------+</span><br><span class="hljs-section">| aplusb(1, 4) |</span><br><span class="hljs-section">+--------------+</span><br><span class="hljs-section">|            5 |</span><br><span class="hljs-section">+--------------+</span><br><br>mysql&gt; SELECT aplusb();<br>ERROR 1582 (42000): Incorrect parameter count in the call to native function <span class="hljs-emphasis">&#x27;aplusb&#x27;</span><br></code></pre></td></tr></table></figure><p><img src="https://user-images.githubusercontent.com/9161438/115414212-6d3c6c00-a228-11eb-889d-8f2c83e10335.png" alt="image"></p><p>创建后，UDF 的 Wasm bytecode 会被存在一张系统表中，在某个节点首次执行后会被编译执行并且缓存在该节点上。</p><p>当然对于大段的 bytecode，这种方式其实不友好，可能后续需要提供一些上传工具。（不过这并不是 hack 需要考虑的事情</p><h4 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h4><p>我们分别用 Wasm UDF 和 TiDB&#x2F;TiKV native code 实现了一个叫 [nbody](<a href="https://github.com/tidb-hackathon-2020-wasm-udf/tidb/commit/bbcf0d5748a6462e1030bca07b30d848ea250648">add builtin nbody · tidb-hackathon-2020-wasm-udf&#x2F;tidb@bbcf0d5 (github.com)</a>) 的函数用作性能测试。我们很意外地发现 UDF nbody 比 rust nbody 大致相近（符合预期），但居然比 go nbody 快一些 <del>（说明 golang 辣鸡）</del>，猜测是 allocator 的问题。这个实验也充分证明了 UDF 的高性能。</p><h4 id="灵活性"><a href="#灵活性" class="headerlink" title="灵活性"></a>灵活性</h4><p>这其实是我们在 Wasm 上踩坑最多的地方，Wasm 的接口比较 low level，因此一些高级语言的数据结构（其实从需求上来说主要就是 string）映射到 Wasm ABI 会有不同的表示，我们还是需要为每种语言定制一下工具链来生成符合我们预期的 Wasm bytecode。</p><p>我们分别用 rust&#x2F;golang&#x2F;C 实现了 UDF 的功能，在尝试 Java 时，我们踩了比较大的坑，感觉 Java compile to Wasm 的工具链都比较不成熟。目前来看，Wasm 生态比较好的其实是运行时更轻量的静态语言。</p><h4 id="安全性"><a href="#安全性" class="headerlink" title="安全性"></a>安全性</h4><p><img src="https://user-images.githubusercontent.com/9161438/115404999-6b6eaa80-a220-11eb-8472-d2b28080b2ff.png" alt="execution"></p><p>Wasm 本身是个安全的沙箱执行环境，我们可以主动提供一些 API 给用户调用，并且配合权限认证。为了演示这个功能，我们实现了一个 HTTPGet 的函数，同时对接了 TiDB 本身的 Privilege 系统 —— 有 UDFNetworkPrivilege 权限的用户才能顺利创建和执行这个函数。这其实给 TiDB 与云生态结合提供了很大的想象空间。</p><hr><p>最后我们在 Demo 的时候演示了一个非常 fancy 的东西：我们复用了上一届 Hackathon 二等奖的一个成果，他们做的是<a href="https://pingcap.com/blog-cn/tidb-in-the-browser-running-a-golang-database-on-wasm/">基于 Wasm 让TiDB 运行在浏览器里</a>，然后我们直接复用他们的 Wasm 实现了<strong>让 TiDB 跑在 TiDB 里</strong></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs sql"># TiDB 套娃<br><span class="hljs-keyword">SELECT</span> wasm_tidb(<span class="hljs-string">&#x27;SELECT tidb_version();&#x27;</span>);<br><span class="hljs-keyword">SELECT</span> wasm_tidb(<span class="hljs-string">&#x27;use test; create table t1 (id int primary key); insert into t1 values (1); select * from t1;&#x27;</span>);<br></code></pre></td></tr></table></figure><h2 id="Not-only-UDF"><a href="#Not-only-UDF" class="headerlink" title="Not only UDF"></a>Not only UDF</h2><p>我们的参赛主题写的是 TOT（TiDB over TIDB），事实上我一开始是想展示三种 TOT 的 demo（很遗憾的是我们最后只实现了两种）：</p><ol><li>TiDB 通过 UDF 访问云上的另一个 TiDB，展示 UDF 可以提供受沙箱限制的网络能力，为云上和其他服务联动提供想象空间。</li><li>TiDB 通过 UDF 运行另一个编译成 Wasm 的 TiDB，展示 wadm 强大的可扩展性，我们甚至可以把 TiDB 本身移植上去（见上文）。</li><li><strong>在 UDF 中暴露受控的合适的内部接口，例如执行一些查询&#x2F;修改，达到类似存储过程的效果。</strong></li></ol><p>至少在互联网公司的数据库应用中，存储过程已经是基本被抛弃的功能。存储过程有很多众所周知的缺点：</p><ul><li>语法和主流语言有差异，且缺乏统一标准，难以 port 到其他架构</li><li>通常表达能力较弱，容易写错</li><li>难以根据业务模型封装</li><li>占用数据库计算资源，难以控制存储过程的复杂度，容易把 DB 打挂</li><li>……</li></ul><p>而基于 Wasm 的存储过程，可以基本避免上面提到的缺点：</p><ul><li>不再拘泥于特定的 DSL，凡是能编译到 Wasm 的语言都能运行在 DB 上，业务很容易 port 代码，且 GPL（general purpose language）的表达能力通常比 DSL 强大很多。</li><li>运行在各种架构上，一处编码处处运行（JVM：？）</li><li>借助 jit 达到接近 native 的性能，特别是如果 DB 本身是基于 query compilation 的执行器的话，可以把整个 query 的完整执行逻辑直接编译成媲美手写的最佳性能。（当然 TiDB 不行）</li><li>借助 Wasm 本身的沙盒机制保证安全性。</li></ul><p>这样自由又强大的 ”存储过程“ 已经接近于数据库内部的 Serverless 了，相比直接用 Serverless ，做在数据库内部有什么好处，这又是另一个很大的 topic 了，下次有灵感了再写（🕊）。</p><p>最近经常学习 Manjusaka 老师的 Blog，感受了 ePBF 给内核可观测性带来的变化。事实上 Wasm 也可以做到类似的事情。除了实现存储过程，Wasm 还可以用于用户自定义 Trigger。这个 Trigger 不仅仅可以在数据修改时执行，在如获取 TSO，下推计算、数据复制，等所有具有观测价值的地方都可以进行一些埋点对 UDF 调用，而在 UDF 内部可以灵活高效地采集监控信息，甚至可以通过受限 API 跟用户生态或者云生态内的其他系统汇报采集到的信息，这可以给数据库系统带来巨大的可观测性提升。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总的来说是一次非常充实的 Hackathon，在一次 Hack 的过程中我们不仅需要跟 TiDB、TiKV 打交道，还了解了各个语言工具链特别是大量 Wasm 周边生态有关的知识，工作量非常的大，感谢队友大腿们 breeswish，Fullstop000 和 Hawkingrei 的带飞。</p><p>在给评委答辩<del>画饼</del>的时候，事实上我也感受到了 Wasm+Wasi 在服务器领域的巨大潜力，如果 Wasm 能在更多系统中大规模落地的话，能在保证数据安全的前提下给用户带来更强更灵活的定制能力。这个 Hackathon 项目也是为这个过程做了一点微小的贡献~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;前段时间，因为比较活跃在 TiDB 社区，所以顺手参加了 TiDB 2020 Hackathon。这次选的参赛题目是一个老生常谈的功能：UDF（User Defined Function）。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Database" scheme="https://blog.zhuangty.com/tags/Database/"/>
    
    <category term="TiDB" scheme="https://blog.zhuangty.com/tags/TiDB/"/>
    
    <category term="Hackathon" scheme="https://blog.zhuangty.com/tags/Hackathon/"/>
    
  </entry>
  
  <entry>
    <title>gogo/protobuf 的一个性能 bug</title>
    <link href="https://blog.zhuangty.com/gogo-proto-timestamp/"/>
    <id>https://blog.zhuangty.com/gogo-proto-timestamp/</id>
    <published>2020-02-03T20:17:06.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p>源码阅读笔记是不可能续写的，这辈子都不可能续写的，paper notes 也是几年也不会更新一篇的，还不如把博客随便当个笔记本记录点遇到过的有意思的问题好了。</p><p>Protobuf 是 Google 整的一个序列化&#x2F;反序列化框架，性能不算很好不过用的人比较多，各个语言的实现也比较全，其中 golang 的版本是 google 官方维护的 <a href="https://github.com/golang/protobuf">golang&#x2F;protobuf</a>，但由于比较保守，对各种新 feature request 不太感兴趣，所以社区广泛使用的是一个 fork 的版本 <a href="https://github.com/gogo/protobuf">gogo&#x2F;protobuf</a>，gogo 版本不仅在性能上做了很多优化，而且提供了很多 <a href="https://github.com/gogo/protobuf/blob/master/extensions.md">extensions</a>，可以让生成的代码更符合 go 开发的习惯。</p><p>这篇 blog 记录的是在使用 stdtime extension 中遇到的一个性能问题排查过程和修复方案。stdtime extension 可以把 Google 提供的一个公共库中 Timestamp 的类型定义转化为 golang 标准库 <code>time.Time</code> 的定义。</p><span id="more"></span><p>线上开发某个服务的时候用了 gRPC 和 gogo&#x2F;protobuf，然后其中的 message 定义大概是：</p><figure class="highlight protobuf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs protobuf"><span class="hljs-keyword">import</span> <span class="hljs-string">&quot;github.com/gogo/protobuf/gogoproto/gogo.proto&quot;</span>;<br><span class="hljs-keyword">import</span> <span class="hljs-string">&quot;google/protobuf/timestamp.proto&quot;</span>;<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">A</span> &#123;<br>    <span class="hljs-comment">// ...</span><br>    google.protobuf.Timestamp created_at = <span class="hljs-number">1</span> [(gogoproto.stdtime) = <span class="hljs-literal">true</span>];<br>&#125;<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">B</span> &#123;<br>    <span class="hljs-keyword">repeated</span> A as = <span class="hljs-number">1</span>;<br>&#125;<br><br><span class="hljs-keyword">message </span><span class="hljs-title class_">Empty</span> &#123;&#125;<br><br><span class="hljs-keyword">service </span><span class="hljs-title class_">S</span> &#123;<br>    <span class="hljs-function"><span class="hljs-keyword">rpc</span> RPC (Empty) <span class="hljs-keyword">returns</span> (B) </span>&#123;&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>大概有几千个服务会调这个 RPC，每次返回的 as 的长度大约是 10000 左右。在测试时，从客户端观察，几乎每个请求都要耗时 20 秒以上，但在服务端观察到每个请求的处理时间在 100ms 以下。在排除了网络故障的可能性以后，我开始怀疑是 RPC Framework 的问题。</p><p>Golang 自带的 profile 框架 <code>pprof</code> 是非常好用的，简单跑了个 mutex profile 以后，观察到</p><p><img src="https://i.loli.net/2020/02/05/zsdjBq7XnkapuNH.png" alt="image.png"></p><p>发现阻塞时间基本在 proto 包里的一个 <code>Mutex</code> 上。</p><p>简单翻了一下<a href="https://github.com/golang/protobuf/blob/d23c5127dc24889085f8ccea5c9d560a57a879d8/proto/table_marshal.go#L98-L110">代码</a></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">var</span> (<br>marshalInfoMap  = <span class="hljs-keyword">map</span>[reflect.Type]*marshalInfo&#123;&#125;<br>marshalInfoLock sync.Mutex<br>)<br><br><span class="hljs-comment">// getMarshalInfo returns the information to marshal a given type of message.</span><br><span class="hljs-comment">// The info it returns may not necessarily initialized.</span><br><span class="hljs-comment">// t is the type of the message (NOT the pointer to it).</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getMarshalInfo</span><span class="hljs-params">(t reflect.Type)</span></span> *marshalInfo &#123;<br>marshalInfoLock.Lock()<br>u, ok := marshalInfoMap[t]<br><span class="hljs-keyword">if</span> !ok &#123;<br>u = &amp;marshalInfo&#123;typ: t&#125;<br>marshalInfoMap[t] = u<br>&#125;<br>marshalInfoLock.Unlock()<br><span class="hljs-keyword">return</span> u<br>&#125;<br></code></pre></td></tr></table></figure><p>看起来是为了让每个 Message Type 都只会产生一个 <code>*marshalInfo</code>，用了一个全局的 map 和一个全局的 Mutex 来保护。产生大量 message 的时候，这个 Mutex 成为了瓶颈。这段代码在 gogo&#x2F;protobuf 和 golang&#x2F;protobuf 同时存在。</p><p>当时觉得已经定位到了问题，并且修复方案也很简单，用 RWMutex 做个 double check 就好了，测试过优化明显后，顺手给 golang&#x2F;protobuf 交了一个 <a href="https://github.com/golang/protobuf/pull/1004">PR</a>。</p><p>很不幸的是 golang&#x2F;protobuf 的 maintainer argue 这个函数只会被调用少数次，有几个 message 定义就回被调用几次，与运行时产生的 message 数量无关。并且给出了复现例子，于是只好继续深入定位问题。</p><p>在 demo 中做了若干次详细的试验以后，发现这个 bug 确实只能用 gogo&#x2F;protobuf 复现，并且必须打开 <code>[(gogoproto.stdtime) = true]</code> 的选项才会产生。</p><p>在打开这个特性开关后，gogo&#x2F;protobuf 需要引用 google <code>Timestamp</code> 的定义来反序列化数据，再转化为 <code>time.Time</code>，<code>Timestamp</code> 的定义是由 protoc-gen-gogo 生成的，包路径为 <code>github.com/gogo/protobuf/types</code>，然而所有生成的代码都需要反过来依赖 <code>github.com/gogo/protobuf/proto</code>，所以会形成循环依赖。为了解决这个问题，gogo&#x2F;protobuf 在 <code>github.com/gogo/protobuf/proto</code> 包里 mock 了一个 <a href="https://github.com/gogo/protobuf/blob/5628607bb4c51c3157aacc3a50f0ab707582b805/proto/timestamp_gogo.go#L38-L46">timestamp</a>，只通过 struct tag 定义了最基本的序列化格式，而缺失了一些关键的方法，导致没有满足 <code>newMarshaler</code> 和 <code>Marshaler</code> 的 interface，同时 protobuf 为了满足向后兼容性，入口函数 <a href="https://github.com/gogo/protobuf/blob/5628607bb4c51c3157aacc3a50f0ab707582b805/proto/table_marshal.go#L2936-L2955">Marshal</a> 依然接受不满足 <code>newMarshaler</code>，<code>Marshaler</code> 的参数，只是走了最慢的路径。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Marshal</span><span class="hljs-params">(pb Message)</span></span> ([]<span class="hljs-type">byte</span>, <span class="hljs-type">error</span>) &#123;<br><span class="hljs-keyword">if</span> m, ok := pb.(newMarshaler); ok &#123;<br>siz := m.XXX_Size()<br>b := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-number">0</span>, siz)<br><span class="hljs-keyword">return</span> m.XXX_Marshal(b, <span class="hljs-literal">false</span>)<br>&#125;<br><span class="hljs-keyword">if</span> m, ok := pb.(Marshaler); ok &#123;<br><span class="hljs-comment">// If the message can marshal itself, let it do it, for compatibility.</span><br><span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> This is not efficient.</span><br><span class="hljs-keyword">return</span> m.Marshal()<br>&#125;<br><span class="hljs-comment">// in case somehow we didn&#x27;t generate the wrapper</span><br><span class="hljs-keyword">if</span> pb == <span class="hljs-literal">nil</span> &#123;<br><span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, ErrNil<br>&#125;<br><span class="hljs-keyword">var</span> info InternalMessageInfo<br>siz := info.Size(pb)<br>b := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-number">0</span>, siz)<br><span class="hljs-keyword">return</span> info.Marshal(b, pb, <span class="hljs-literal">false</span>)<br>&#125;<br></code></pre></td></tr></table></figure><p>由于同时涉及了代码生成和循环依赖问题，这个问题的正确修复方式可能需要涉及到很大的重构，比较简单的 Workaround 有：</p><ul><li>使用 RWMutex 来优化这个全局 map，目前<a href="https://github.com/TennyZhuang/protobuf">我 fork 的版本</a>就是这么干的。</li><li>在 <code>github.com/gogo/protobuf/proto</code> 中依赖 <code>github.com/golang/protobuf/ptypes</code> 中的 <code>Timestamp</code> 来避免循环依赖，但会导致 gogo&#x2F;protobuf 依赖 golang&#x2F;protobuf，仍然不是好的解决方案。</li><li>从生成的 <code>github.com/gogo/protobuf/types.Timestamp</code> 中 copy 更多代码到 mock 的 <code>github.com/gogo/protobuf/proto.timestamp</code> 中</li></ul><p>目前提了一个 <a href="https://github.com/gogo/protobuf/issues/656">issue</a>，不过 gogo&#x2F;protobuf 的维护也不太活跃，在这个 issue 解决之前，建议不使用可能会触发该 bug 的 stdtime，stdduration，customtype 等 extension。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;源码阅读笔记是不可能续写的，这辈子都不可能续写的，paper notes 也是几年也不会更新一篇的，还不如把博客随便当个笔记本记录点遇到过的有意思的问题好了。&lt;/p&gt;
&lt;p&gt;Protobuf 是 Google 整的一个序列化&amp;#x2F;反序列化框架，性能不算很好不过用的人比较多，各个语言的实现也比较全，其中 golang 的版本是 google 官方维护的 &lt;a href=&quot;https://github.com/golang/protobuf&quot;&gt;golang&amp;#x2F;protobuf&lt;/a&gt;，但由于比较保守，对各种新 feature request 不太感兴趣，所以社区广泛使用的是一个 fork 的版本 &lt;a href=&quot;https://github.com/gogo/protobuf&quot;&gt;gogo&amp;#x2F;protobuf&lt;/a&gt;，gogo 版本不仅在性能上做了很多优化，而且提供了很多 &lt;a href=&quot;https://github.com/gogo/protobuf/blob/master/extensions.md&quot;&gt;extensions&lt;/a&gt;，可以让生成的代码更符合 go 开发的习惯。&lt;/p&gt;
&lt;p&gt;这篇 blog 记录的是在使用 stdtime extension 中遇到的一个性能问题排查过程和修复方案。stdtime extension 可以把 Google 提供的一个公共库中 Timestamp 的类型定义转化为 golang 标准库 &lt;code&gt;time.Time&lt;/code&gt; 的定义。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Golang" scheme="https://blog.zhuangty.com/tags/Golang/"/>
    
    <category term="Protobuf" scheme="https://blog.zhuangty.com/tags/Protobuf/"/>
    
    <category term="Bug" scheme="https://blog.zhuangty.com/tags/Bug/"/>
    
  </entry>
  
  <entry>
    <title>[Paper Notes] Facebook Haystack and F4</title>
    <link href="https://blog.zhuangty.com/haystack-f4/"/>
    <id>https://blog.zhuangty.com/haystack-f4/</id>
    <published>2019-03-23T23:35:21.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><a href="https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf">Haystack</a> 和 <a href="https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-muralidhar.pdf">F4</a> 是 Facebook 为了解决照片存储的场景开发的一套小文件存储系统。整个设计非常简洁（褒义，虽然简洁到让人怀疑这也能发 OSDI），但是却把每个部分的设计和考虑解释得非常清楚。读完 <a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/gfs-sosp2003.pdf">GFS</a> 会感觉有不少未解之谜在 paper 中没交代清楚，但读完 Haystack 和 F4 就感觉异常通顺。Facebook 一开始开发了 Haystack 是为了覆盖整个照片存储场景，后来发现了温存储场景可以优化的地方，又开发了 F4 将冷数据从 Haystack 中剥离出来，单独存储，并且 F4 的 paper 中描述了整套 BLOB Storage 系统同时修改了一些 Haystack 的设定，因此将这两篇放在一起讲。F4 也分享了很多 Facebook 在这套系统的设计和使用上的很多经验，值得学习。</p><span id="more"></span><h2 id="Haystack"><a href="#Haystack" class="headerlink" title="Haystack"></a>Haystack</h2><blockquote><p>Needle in a haystack</p></blockquote><p>Needle 是 Haystack 中的基本存储单位，英文翻译是针，Haystack 的英文翻译是草垛。出于好奇 Facebook 为什么取了这个名字的目的去搜了一下，发现这是一句类似于“大海捞针”（草垛里捞针）的常用短语，这么理解的话这个名字对于一个存储海量小文件的存储系统就非常形象了~</p><h3 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h3><p>在开发 Haystack 之前，Facebook 使用基于 NFS 的设计方案。每个小文件直接对应 NFS 上的一个物理文件，在 CDN Cache Miss 的文件会直接通过 Photo Server 落到 NFS 上读，这种方案的缺陷非常明显，就是小文件给文件系统带来的太多元数据。POSIX 文件系统在文件节点上存储了大量 Facebook 的场景下不需要的信息（如权限信息等），每个 INode 都要占据大约 500 byte 的空间，导致在大量小文件的场景下，文件系统无法将元信息全部缓存到自己的内存中，访问数据的时候，除了必须要的一次数据读取的磁盘 IO，在获取元数据以定位真实数据位置的过程中也需要经过若干次磁盘 IO，这是基于 NFS 的系统导致图片访问慢的主要原因。</p><p>Haystack 的优化目标非常明确，就是砍掉无用的元信息，压缩元信息到足够小并全部加载到内存中，将对单张图片的访问精确地缩减为一次磁盘 IO。</p><p>Haystack 的优化思路也非常的简单，既然小文件的元信息太多，那么就把大量小文件打包成大文件再存，自己维护小文件需要的少量 元信息。在 Haystack 中，存储的小文件及其元信息称为 Needle，而打成的大文件包称为 Volume。Haystack 的核心部分其实就是这个单机的小文件存储引擎。</p><h3 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h3><p><img src="https://i.loli.net/2019/03/24/5c975a4eb47ba.png" alt="Figure 3 in Haystack"></p><h4 id="Haystack-Cache"><a href="#Haystack-Cache" class="headerlink" title="Haystack Cache"></a>Haystack Cache</h4><p>Facebook 的架构里用户除了访问 CDN 以外，也可以跳过 CDN 直接访问数据，这两种请求最终都会由 Haystack Cache 处理（猜测区别仅仅是内部和外部 Cache）。Haystack Cache 就是个很平凡的 Cache 逻辑，以 Photo ID 为 key 维护了一个分布式哈希表，如果请求的照片没有缓存，就从底层的 Haystack Store 读取数据，并且对<strong>满足一定条件的</strong>查询结果进行缓存。</p><p>这块唯一需要注意一些的就是这个缓存条件，当且仅当满足两个条件的时候，Haystack Cache 才会进行缓存：</p><ol><li>直接来自用户，而非来自 CDN 的请求。对于一般的来自 CDN 的请求，Haystack 直接将缓存的任务交给对方。从这个角度来看，Haystack Cache 的定位基本就等于一个系统内部的 CDN。</li><li>照片存在 write-enabled 节点上的。这个跟之后提到的照片热度的 Timezone 有关，可以简单理解为从 Facebook 的场景来看，上传了很长一段时间以后的照片没有缓存价值。</li></ol><h4 id="Haystack-Store"><a href="#Haystack-Store" class="headerlink" title="Haystack Store"></a>Haystack Store</h4><p>Haystack Store 是最核心的模块，也就是 Haystack 的存储节点。Haystack 放弃了原生的 POSIX 作为小文件存储的接口，但沿用了 POSIX 文件系统的底层，自己基于这个底层开发了一个单机的小文件存储系统，并运行在每块 Disk 或者每个节点上、</p><p>每个 Volume 有唯一的 Volume ID，标识一个 Logical Volume，但为了数据可靠性，每个 Logical Volume 在集群内会有三个副本，这些 Volume 实体称为 Physical Volume，在单机的存储引擎中。这些 Physical Volume 是真实的 POSIX 文件系统下的文件单位，分散在 Haystack Store 的节点中。</p><p>每个 Volume 会存储数百万张照片，并由三个文件组成，Data file 和 Index file 和 Journal file（后续加入）。Data file 由连续的 Needle 组成，每个 Needle 除了存储图片本身的数据以外，还存储了一些额外的元信息，其中比较 重要的有照片的 key 和 alternate key，图片的 size 和 checksum，以及一个删除标志位。</p><p>key 和 alternate key 用于 Facebook 场景下的二层索引，因为 Facebook 对每张照片存了四种不同 size 的图片（包括缩略图，小图，大图，原图），因此每张照片有一个主键，然后再通过 alternate key 对应到需要的尺寸的图片。</p><p>根据 key 和 alternate key，Haystack store 的每个节点在内存中为每个 volume 构建了一个双层的索引，用于快速找到对应的 Needle 在 data file 中的偏移量，并缓存了 size 信息减少一次读取元信息的 IO。而 Index file 是这个索引文件的一个快照。</p><p>一个 Volume 支持数据粒度的 Read，Write 和 Delete 操作，实现在了解数据的定义之后都非常的 trivial，直接通过下面的伪代码展示，但有一些场景需要考虑。</p><p>Index file 是定期 dump 到磁盘中的，因此宕机时会丢失数据，需要恢复，对于新写入的数据这非常简单，因为遗失的数据总是在 data file 的尾部，从 index 中最高的 offset 开始从 data file 恢复这些 meta 信息即可。但是对于删除的数据无法简单地恢复，在旧版本的 Haystack（即 Haystack paper）中，删除是通过修改 data file 中 needle 的标志位来完成持久化的，而在新版本（F4 paper 中提到的 Haystack），每次删除文件仅需要在 journal file 中添加一条记录，这是磁盘 append 操作因此非常快，而 Index 在宕机恢复时仅需要将 Journal file 和 Index file 做一个 merge 即可。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Needle <span class="hljs-keyword">struct</span> &#123;<br>    header       [<span class="hljs-number">4</span>]<span class="hljs-type">byte</span> <span class="hljs-comment">// 标识 Needle 开头，恢复数据的时候比较有用</span><br>    cookie       <span class="hljs-type">uint64</span>  <span class="hljs-comment">// 我也不知道是干嘛的，据说是反爬虫用的</span><br>    key          <span class="hljs-type">uint64</span>  <br>    alternateKey <span class="hljs-type">uint64</span>  <br>    flags        <span class="hljs-type">uint8</span>   <span class="hljs-comment">// 目前看起来只标志 deleted，并且在后续不再需要这个 deleted flag</span><br>    size         <span class="hljs-type">uint16</span><br>    data         []<span class="hljs-type">byte</span><br>    footer       [<span class="hljs-number">4</span>]<span class="hljs-type">byte</span> <span class="hljs-comment">// 标识 needle 结尾，恢复数据的时候比较有用</span><br>    checksum     <span class="hljs-type">uint32</span><br>&#125;<br><br><span class="hljs-keyword">type</span> needleMeta <span class="hljs-keyword">struct</span> &#123;<br>    offset <span class="hljs-type">uint64</span><br>    size   <span class="hljs-type">uint16</span><br>&#125;<br><br><span class="hljs-keyword">type</span> indexItem <span class="hljs-keyword">struct</span> &#123;<br>    needleMeta<br>    key          <span class="hljs-type">uint64</span><br>    alternateKey <span class="hljs-type">uint32</span><br>&#125;<br><br><span class="hljs-keyword">type</span> Volume <span class="hljs-keyword">struct</span> &#123;<br>    index        <span class="hljs-keyword">map</span>[<span class="hljs-type">uint64</span>]<span class="hljs-keyword">map</span>[<span class="hljs-type">uint32</span>]*needleMeta<br>    dataFile     *os.File<br>    journalFile  *os.File<br>    indexFile    *os.File<br>    indexCache   []indexItem<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(v *Volume)</span></span> dump() &#123;<br>    <span class="hljs-comment">// 定期执行</span><br>    v.indexFile.Write(binary.Encode(v.indexCache))<br>    v.indexCache = <span class="hljs-built_in">make</span>([]indexItem, <span class="hljs-number">0</span>)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(v *Volume)</span></span> Recover() &#123;<br>    v.indexFile.Seek(<span class="hljs-number">0</span>)<br>    <span class="hljs-keyword">var</span> it *indexItem<br>    <span class="hljs-keyword">for</span> &#123;<br>        *it, err = Read(v.indexFile, sizeof(indexItem)<br>        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123; <span class="hljs-keyword">break</span> &#125;<br>        v.index[it.key][it.alternateKey] = v.needleMeta<br>    &#125;<br>    <span class="hljs-keyword">var</span> offset <span class="hljs-type">uint64</span><br>    <span class="hljs-keyword">if</span> it == <span class="hljs-literal">nil</span> &#123; offset = <span class="hljs-number">0</span> &#125; <span class="hljs-keyword">else</span> &#123; offset = it.offset &#125;<br>    v.dataFile.Seek(offset)<br>    <span class="hljs-keyword">for</span> &#123;<br>        needle, offset, err := Read(v.dataFile, sizeof(needle))<br>        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123; <span class="hljs-keyword">break</span> &#125;<br>        it := indexItem&#123;offset, needle.size, needle.key, needle.alternateKey&#125;<br>        v.indexCache = <span class="hljs-built_in">append</span>(v.indexCache, it)<br>        index[it.key][it.alternateKey] = it.needleMeta<br>    &#125;<br>    v.jornalFile.Seek(<span class="hljs-number">0</span>)<br>    <span class="hljs-keyword">for</span> &#123;<br>        key, alternateKey, offset, err := Read(v.jornalFile, <span class="hljs-number">64</span>+<span class="hljs-number">32</span>+<span class="hljs-number">64</span>)<br>        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123; <span class="hljs-keyword">break</span> &#125;<br>        meta, ok := v.index[key][alternateKey]<br>        <span class="hljs-keyword">if</span> ok &amp;&amp; meta.offset &lt;= offset &#123;<br>            <span class="hljs-built_in">delete</span>(v.index[key], alternateKey)<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(v *Volume)</span></span> Read(key <span class="hljs-type">uint64</span>, alternateKey <span class="hljs-type">uint32</span>) ([]<span class="hljs-type">byte</span>, <span class="hljs-type">error</span>) &#123;<br>    meta, ok := v.index[key][alternateKey]<br>    <span class="hljs-keyword">if</span> !ok &#123; <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errNotFound &#125;<br><br>    <span class="hljs-keyword">return</span> Pread(v.dataFile, meta.offset, meta.size)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(v *Volume)</span></span> Write(key <span class="hljs-type">uint64</span>, alternateKey <span class="hljs-type">uint32</span>, data []<span class="hljs-type">byte</span>) <span class="hljs-type">error</span> &#123;<br>    n := Needle &#123;<br>        header:       [<span class="hljs-number">4</span>]<span class="hljs-type">byte</span>(<span class="hljs-string">&quot;NEED&quot;</span>),<br>        cookie:       rand.Uint64(),<br>        key:          key,<br>        alternateKey: alternateKey,<br>        flags:        <span class="hljs-number">0</span>,<br>        size:         <span class="hljs-built_in">len</span>(data),<br>        data:         data,<br>        footer:       [<span class="hljs-number">4</span>]<span class="hljs-type">byte</span>(<span class="hljs-string">&quot;DEEN&quot;</span>),<br>        checksum:     crc32(data),<br>    &#125;<br>    offset, err := v.dataFile.Write(binary.Encode(&amp;n))<br>    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123; <span class="hljs-keyword">return</span> err &#125;<br>    v.index[key][alternateKey] = &amp;needleMeta&#123;<br>        offset: offset,<br>        size:   <span class="hljs-built_in">len</span>(data),<br>        flags:  <span class="hljs-number">0</span>,<br>    &#125;<br>    v.indexCache = <span class="hljs-built_in">append</span>(v.indexCache, indexItem&#123;...&#125;)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(v *Volume)</span></span> Delete(key <span class="hljs-type">uint64</span>, alternateKey <span class="hljs-type">uint32</span>) <span class="hljs-type">error</span> &#123;<br>    meta, ok := v.index[key][alternateKey]<br>    <span class="hljs-keyword">if</span> !ok &#123; <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> &#125;<br>    <span class="hljs-built_in">delete</span>(v.index[key], alternateKey)<br>    <span class="hljs-comment">// 这里需要持久化记录 offset，避免恢复 index file 的时候将删除操作之后新添加的同一个 key 的数据也删除</span><br>    <span class="hljs-keyword">return</span> v.journalFile.Write(binary.Encode(key, alternateKey, meta.offset))<br>&#125;<br></code></pre></td></tr></table></figure><p>为了避免垃圾数据过多，volume 还会根据一定条件触发 Compaction，回收已经被 Delete 或者被 Write 覆盖的数据。</p><p>除了单个 volume，我们考虑一下如何在多个 Physical volume 之间保持一致性，Haystack 的答案似乎就是根本不管。由于 Write 和 Delete 非常幂等，并且 Photo Store 稍微不一致也不是特别要紧，Haystack 选择添加监控并且手动处理一些不一致的异常情况。</p><h4 id="Haystack-Directory"><a href="#Haystack-Directory" class="headerlink" title="Haystack Directory"></a>Haystack Directory</h4><p>这是个 paper 笔墨很少，但十分重要的组件，它存储了所有的元信息，比如 volume ID 到 physical volume 位置的映射。它负责调度用户的请求，包括负载均衡写请求的 logical volume，负载均衡读请求打到哪个 physical volume 等的调度。为了避免一个 volume 无限增长造成运维困难，directory 会在 volume 大小达到一定容量时将 volume 标记为 read-only。</p><h4 id="Pitchfork"><a href="#Pitchfork" class="headerlink" title="Pitchfork"></a>Pitchfork</h4><p>这个就是个健康检测后台任务，定期给所有存储节点发请求，观测到一个 volume 发生异常时标记为 read-only 并找运维人肉处理。这不会影响服务的整体可用性，因为写请求可以打到任意一个 volume。</p><h2 id="F4"><a href="#F4" class="headerlink" title="F4"></a>F4</h2><p>F4 是 Facebook 在 Haystack 之后又搞的一个 Warm blob store，这个 warm 就比较魔性，让人想起星巴克的中杯、大杯和超大杯。不过事实上，F4 存储的确实不是冷存储，而是 Facebook 的一些 long tail 的照片，他们仍然会被获取，但是频率较低，也很少被覆盖或删除。相比于一些获取数据需要以天为单位的真正的 cold storage，F4 仍然要求对数据的获取在百毫秒级的时间内完成响应。</p><h3 id="F4-和-Haystack-的关系"><a href="#F4-和-Haystack-的关系" class="headerlink" title="F4 和 Haystack 的关系"></a>F4 和 Haystack 的关系</h3><p>在 Haystack 中的数据因为三副本的原因有比较高的存储成本，F4 的设计目标主要是在保证数据安全的情况下降低数据的存储成本。一点可以利用的性质是从 Haystack 导入 F4 的照片很少会被删除，我们可以认为整个 Volume 都是不可变数据。</p><p>Haystack 和 F4 的接口完全保证一致，通过 router tier 对用户隐藏具体实现。</p><p>在数据导入的时间点上，Haystack 基于底层硬件设备（HDD）的读写能力和 BLOB 的使用情况统计进行设计，以 80 IOPS&#x2F;TB 作为分界线对统计结果进行划分，并确定了三个月的分界线，即对于 Facebook 的大部分 BLOB 数据来说，在经过三个月的时间以后，访问频率就会降到显著低于 80 IOPS&#x2F;TB，以至于使用廉价的 HDD 作为存储介质依然可以提供不影响用户体验的服务。这个设计过程也是充分地利用了软硬件一体的设计思想。</p><h3 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h3><p>为了减少空间的使用，F4 引入了 EC（erasure coding）的技术。n:m 的 EC 可以将一份数据切为 n 份，并且构造 m 个冗余块。在这 (n+m) 个块中任意丢失 m 块数据，都能通过剩下 n 个块恢复。因此保障数据安全仅需要 (n+m)&#x2F;n 的空间。Facebook 选择了 10:4 的比例，比起三副本来说，可以节约大量的空间。为了异地灾备，Facebook 在两个不同的集群之间再次通过对两个 Volume XOR 编码，并将这份冗余块备份到第三个集群中，通过 1.4 * 1.5 &#x3D; 2.1 倍的空间完成了异地灾备级别的数据可靠性。</p><p>在 F4 中，一个从 Haystack 导入的 Volume 前会经过 compaction。在 F4 中，Volume 里较小的 index file 仍然通过三副本，但是占 Volume 主体的 data file 完全通过 EC 来保障数据的可靠性和可用性。每个 data file 会按固定的大小（1GB 左右）切成连续的 data block，然后为每 n 个 data block（称为 strip），生成 m 个 parity block，不足 n 个的部分填零补全。每个 Strip 对应的 n+m 个 block 会被分布在不同的机架中，来保证机架级别的容错域。</p><h3 id="架构-1"><a href="#架构-1" class="headerlink" title="架构"></a>架构</h3><p>架构上分为了五种类型的节点，并将这些节点打包成了一个 F4 Cell。</p><p><img src="https://i.loli.net/2019/03/30/5c9f31fcbea4f.png" alt="F4 Figure 8"></p><ol><li>Name Node：管理了 Volume -&gt; Strip -&gt; Block 的 Mapping 关系的 NameNode，这个功能非常简单直接忽略其实现。</li><li>Storage Node：Storage Heavy 的节点，应该配有大量的 HDD，存储 Block，并管理对 Block 的读取操作。</li><li>Backoff Node：CPU Heavy 的节点，在部分 data blocks 损坏时，在 parity blocks 继续读取数据对外提供服务保证可用性。</li><li>Rebuilder Node：在有 data block 或者 parity block 损坏时，负责恢复数据。由于 F4 paper 中没有提到 Volume 如何从 Haystack 中入库到 F4 中，猜测初始化 parity block 这部分也是由 Rebuilder Node 负责。</li><li>Coordinator Node：一个 Cell 的任务调度节点。进行一些定期检查，容错域调度等维护任务的调度。</li></ol><h3 id="Others"><a href="#Others" class="headerlink" title="Others"></a>Others</h3><h4 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h4><p>虽然 F4 不支持修改数据，但为了用户数据的隐私，允许对数据进行删除。</p><p>在最开始的设计中，F4 的开发者计划在 F4 中保留 Haystack 中的 Jornal file 作为文件的删除记录，并保留 compaction 的策略。很快他们发现在 F4 设计中留这个可变的因素会大大增加设计复杂度。因此 F4 换了个删除的思路，将所有 BLOB 在导入 F4 前用每个 BLOB 唯一的秘钥加密，并将秘钥存在外部数据库中，如果需要删除数据，只需要在外部数据库中删除这个秘钥，就能让加密的 BLOB 无法通过任何手段恢复原来的数据，从逻辑上做到了 BLOB 的删除。</p><h4 id="effective-replication-factor"><a href="#effective-replication-factor" class="headerlink" title="effective-replication-factor"></a>effective-replication-factor</h4><p>这个词翻不太来，大约是指备份的数据相比原始数据的比例。在 Haystack 中这个数字是 3.6，由三备份和 1.2 倍的 RAID 组成，在 F4 中，这个数字被降低到了 2.1。不过这个可以节约空间的前提是建立在 F4 中的数据删除比例比较少，根据 Facebook 的统计结果，对于大于三个月的 BLOB，这个结论成立。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf&quot;&gt;Haystack&lt;/a&gt; 和 &lt;a href=&quot;https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-muralidhar.pdf&quot;&gt;F4&lt;/a&gt; 是 Facebook 为了解决照片存储的场景开发的一套小文件存储系统。整个设计非常简洁（褒义，虽然简洁到让人怀疑这也能发 OSDI），但是却把每个部分的设计和考虑解释得非常清楚。读完 &lt;a href=&quot;https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/gfs-sosp2003.pdf&quot;&gt;GFS&lt;/a&gt; 会感觉有不少未解之谜在 paper 中没交代清楚，但读完 Haystack 和 F4 就感觉异常通顺。Facebook 一开始开发了 Haystack 是为了覆盖整个照片存储场景，后来发现了温存储场景可以优化的地方，又开发了 F4 将冷数据从 Haystack 中剥离出来，单独存储，并且 F4 的 paper 中描述了整套 BLOB Storage 系统同时修改了一些 Haystack 的设定，因此将这两篇放在一起讲。F4 也分享了很多 Facebook 在这套系统的设计和使用上的很多经验，值得学习。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Paper Note" scheme="https://blog.zhuangty.com/tags/Paper-Note/"/>
    
    <category term="Storage" scheme="https://blog.zhuangty.com/tags/Storage/"/>
    
    <category term="System" scheme="https://blog.zhuangty.com/tags/System/"/>
    
  </entry>
  
  <entry>
    <title>CPython 源码（一）： PyObject</title>
    <link href="https://blog.zhuangty.com/pyobject/"/>
    <id>https://blog.zhuangty.com/pyobject/</id>
    <published>2017-07-30T21:56:06.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这是 CPython 源码阅读系列的第一篇，我也不知道能坚持多久，或许连这篇都写不完（如果你能在网上看到这句话，说明至少第一篇写完了）。</p><p>Python 是一门非常强大的动态语言，语法特性多且优美，非常符合人类直觉，是我最喜欢的语言之一，再加上 CPython 做的优化非常少（笑），不像某些 JS 引擎如 V8，为了性能各种 hack 技巧太多不适合阅读学习。</p><p><a href="https://github.com/python/cpython">CPython 源码链接</a></p><p>对 Python 比较精通以后，自然而然会产生疑问，那么强大的 Python，是怎么通过一门语法特性非常少的静态语言 C 来实现的呢？</p><p><del>当然，阅读 CPython 源码的第一步，就是让你抛弃 C 语言是静态类型的错觉。</del></p><p>本系列无明显顺序，更类似于个人阅读中的笔记，可能有错误，欢迎指出。</p><p>本系列文章要求阅读者：</p><ul><li>了解 C 语言的特性，特别是强制转换时的行为</li><li>了解 Python 语言本身的基本行为和高级特性</li></ul><span id="more"></span><p>作者开始阅读源码时，使用的 Python 版本是 <code>Python 3.7.0a0</code>，最新的 commit 号为 <code>984eef7d6d78e1213d6ea99897343a5059a07c59</code>。</p><p>本文涉及的核心文件是</p><ul><li><a href="https://github.com/python/cpython/blob/master/Include/object.h">Include&#x2F;object.h</a></li><li><a href="https://github.com/python/cpython/blob/master/Objects/object.c">Objects&#x2F;object.c</a></li></ul><h2 id="PyObject"><a href="#PyObject" class="headerlink" title="PyObject"></a>PyObject</h2><p>一切都要从 Hello World 开始，CPython 源码阅读的 Hello World，当然是从 <code>PyObject</code> 开始。</p><p>首先，在 <code>object.h</code> 里找到 <code>PyObject</code> 的定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> _<span class="hljs-title">object</span> &#123;</span><br>    _PyObject_HEAD_EXTRA<br>    Py_ssize_t ob_refcnt;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> _<span class="hljs-title">typeobject</span> *<span class="hljs-title">ob_type</span>;</span><br>&#125; PyObject;<br></code></pre></td></tr></table></figure><p>Python 中的一切对象，都至少保存了以上属性，这也是为什么在 Python 中，哪怕是一个简单的 <code>0</code>，也比 C 语言占用了更多内存的原因。</p><p><code>_PyObject_HEAD_EXTRA</code> 这个宏是全空的，应该是为了未来可能的修改而保留修改空间，可以先忽略它。</p><p>观察一下 <code>ob_refcnt</code> 和 <code>ob_type</code>。</p><h2 id="引用计数"><a href="#引用计数" class="headerlink" title="引用计数"></a>引用计数</h2><p>Python 是一门自带 GC 的语言，而 Python 的 GC 是以引用计数机制为主的，<code>ob_refcnt</code> 保存了 Python 每个对象被引用的次数。</p><p>在 <code>object.c</code> 中实现了一个函数 <code>Py_IncRef</code>，这个是暴露给 Python runtime embedders 的管理 <code>PyObject</code> 引用计数的接口，而源码内部的实现基本调用的是 <code>Py_XINCREF</code>（附录 0） 和 <code>Py_INCREF</code> 这两个宏。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-comment">/* Macros to use in case the object pointer may be NULL: */</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> Py_XINCREF(op)                                \</span><br><span class="hljs-meta">    do &#123;                                              \</span><br><span class="hljs-meta">        PyObject *_py_xincref_tmp = (PyObject *)(op); \</span><br><span class="hljs-meta">        <span class="hljs-keyword">if</span> (_py_xincref_tmp != NULL)                  \</span><br><span class="hljs-meta">            Py_INCREF(_py_xincref_tmp);               \</span><br><span class="hljs-meta">    &#125; while (0)</span><br><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> Py_INCREF(op) (                         \</span><br><span class="hljs-meta">    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \</span><br><span class="hljs-meta">    ((PyObject *)(op))-&gt;ob_refcnt++)</span><br></code></pre></td></tr></table></figure><p>我们先假装没看到一系列的强制转换，那么这两个宏的作用就分别是 safe 和 unsafe 地增加一个 PyObject 的引用计数。</p><p>关于 <code>ob_refcount</code> 还定义了一些别的宏，具体的运用方式想放在 GC 的章节一起看。</p><p><code>ob_refcount</code> 的类型是 <code>Py_ssize_t</code>，这个类型在我的系统上等价于 <code>long</code>，再加上为了为了效率，维护引用计数的时候显然不会作边界检查，这就意味着如果你的一个对象引用超过 <code>LONG_MAX</code> 的话应该会溢出，然而虽然很想尝试一下，但我并不知道怎么在爆 Memory Overflow 前让一个对象的引用计数超过 <code>long</code>。</p><h2 id="PyVarObject"><a href="#PyVarObject" class="headerlink" title="PyVarObject"></a>PyVarObject</h2><p><code>ob_type</code> 显然保存着对象的类型信息，然而在观察其具体实现之前，我们可以看一下紧跟着 <code>PyObject</code> 的另一个结构体的定义，<code>PyVarObject</code>。</p><p>根据注释，<code>PyVarObject</code> 用来存储变长的 python 对象（如 list 等），熟悉 C++ 的同学可能已经忍不住脑补出以下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// fake PyVarObject</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">PyVarObject</span> : <span class="hljs-keyword">public</span> PyObject &#123;<br><span class="hljs-keyword">public</span>:<br>    Py_ssize_t ob_size;<br>&#125;;<br></code></pre></td></tr></table></figure><p>那我们再来看它在 CPython 中的实现：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> &#123;</span><br>    PyObject ob_base;<br>    Py_ssize_t ob_size; <span class="hljs-comment">/* Number of items in variable part */</span><br>&#125; PyVarObject;<br></code></pre></td></tr></table></figure><p>看起来非常反人类的做法，难道每次想操作 PyVarObject 中 <code>ob_type</code> 的时候都要多一层 <code>ob_base</code> 吗？</p><p>然后就是一个比较神奇的操作，而且唯有 C 这种结构体严格映射内存结构的语言才能做到（附录 1），对于 C 来说，<code>PyObject ob_base;</code> 的内存结构和 <code>_PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type;</code> 是完全等价的，那么可以直接把 <code>PyVarObject*</code> 类型的对象强制转换成 <code>PyObject*</code> 类型的对象，然后直接当成 PyObject* 类型操作。</p><p><img src="https://i.loli.net/2017/07/30/597dff20cbd40.png" alt="内存结构图"></p><p>现在我们可以回过去看代码中对于 <code>PyObject</code> 某段注释：</p><blockquote><p>Nothing is actually declared to be a PyObject, but every pointer to<br>a Python object can be cast to a PyObject*.  This is inheritance built<br>by hand.  Similarly every pointer to a variable-size Python object can,<br>in addition, be cast to PyVarObject*.</p></blockquote><p><code>PyObject</code> 不是 Python 中的任何一种类型，但是任何任何 Python 对象的指针都能被 cast 成 <code>PyObject*</code>，相当于手动实现了继承（附录 2）。同理如 <code>PyVarObject*</code>。</p><p>为了方便之后 Python 中对象的定义，object.h 中定义了两个宏</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> PyObject_HEAD                   PyObject ob_base;</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> PyObject_VAR_HEAD      PyVarObject ob_base;</span><br></code></pre></td></tr></table></figure><p>利用这两个宏来达到继承 <code>PyObject</code> 和 <code>PyVarObject</code> 的作用。</p><p>（看到这里，你可以再思考一下 C 究竟是不是静态语言，至少有没有被人当静态语言用）</p><p>为了获取 <code>PyObject</code> 和 <code>PyVarObject</code> 中的基础变量，<code>object.h</code> 定义了三个宏</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> Py_REFCNT(ob)           (((PyObject*)(ob))-&gt;ob_refcnt)</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> Py_TYPE(ob)             (((PyObject*)(ob))-&gt;ob_type)</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> Py_SIZE(ob)             (((PyVarObject*)(ob))-&gt;ob_size)</span><br></code></pre></td></tr></table></figure><p>任何 Python 中的对象都能通过这些宏来获取对应的信息来进行读写。</p><h2 id="PyTypeObject"><a href="#PyTypeObject" class="headerlink" title="PyTypeObject"></a>PyTypeObject</h2><p>Python 中一切皆为对象，类型也不例外。</p><p>现在让我们回到被我们跳过的 <code>ob_type</code>，这是一个指向 <code>PyTypeObject</code> 对象的指针。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> _<span class="hljs-title">typeobject</span> &#123;</span><br>    PyObject_VAR_HEAD<br>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *tp_name; <span class="hljs-comment">/* For printing, in format &quot;&lt;module&gt;.&lt;name&gt;&quot; */</span><br>    Py_ssize_t tp_basicsize, tp_itemsize; <span class="hljs-comment">/* For allocation */</span><br>    <br>    <span class="hljs-comment">// ...</span><br>&#125;;<br></code></pre></td></tr></table></figure><p><code>PyTypeObject</code> 继承了 <code>PyVarObject</code>，这个对象非常的复杂，在之后的文章再详细介绍。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>[0]: 关于 <code>do ... while(0)</code> 的<a href="http://www.bruceblinn.com/linuxinfo/DoWhile.html">解释</a>，之后的贴代码的时候可能会省略该部分。</p><p>[1]: 参见 <a href="https://stackoverflow.com/questions/2748995/c-struct-memory-layout">https://stackoverflow.com/questions/2748995/c-struct-memory-layout</a></p><p>[2]: 虽然 C 语言里没有继承的语法，不过这个概念完全是继承，再加上作者钦定了，所以之后将直接用「继承了 <code>PyObject</code>」 这种语言来描述这种行为。</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;这是 CPython 源码阅读系列的第一篇，我也不知道能坚持多久，或许连这篇都写不完（如果你能在网上看到这句话，说明至少第一篇写完了）。&lt;/p&gt;
&lt;p&gt;Python 是一门非常强大的动态语言，语法特性多且优美，非常符合人类直觉，是我最喜欢的语言之一，再加上 CPython 做的优化非常少（笑），不像某些 JS 引擎如 V8，为了性能各种 hack 技巧太多不适合阅读学习。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/python/cpython&quot;&gt;CPython 源码链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对 Python 比较精通以后，自然而然会产生疑问，那么强大的 Python，是怎么通过一门语法特性非常少的静态语言 C 来实现的呢？&lt;/p&gt;
&lt;p&gt;&lt;del&gt;当然，阅读 CPython 源码的第一步，就是让你抛弃 C 语言是静态类型的错觉。&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;本系列无明显顺序，更类似于个人阅读中的笔记，可能有错误，欢迎指出。&lt;/p&gt;
&lt;p&gt;本系列文章要求阅读者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;了解 C 语言的特性，特别是强制转换时的行为&lt;/li&gt;
&lt;li&gt;了解 Python 语言本身的基本行为和高级特性&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    
    <category term="Python" scheme="https://blog.zhuangty.com/tags/Python/"/>
    
    <category term="Source Code" scheme="https://blog.zhuangty.com/tags/Source-Code/"/>
    
  </entry>
  
  <entry>
    <title>HaScheme</title>
    <link href="https://blog.zhuangty.com/HaScheme/"/>
    <id>https://blog.zhuangty.com/HaScheme/</id>
    <published>2017-01-18T23:41:03.000Z</published>
    <updated>2026-02-19T10:33:34.175Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://github.com/TennyZhuang/HaScheme">HaScheme</a> 是用 Haskell 实现的 Scheme 解释器，作为 函数式编程语言课程的 Course Project, 应该是这学期最满意且收获最多的一个大作业了，得益于之前编译原理 Course Project <a href="https://github.com/TennyZhuang/NaiveC">NaiveC</a> 踩了很多坑，对编译器&#x2F;解释器前端相关的一些理论有了一些了解，在连肝五天以后基本完成了 Scheme 标准语法的大部分内容。</p><p>HaScheme 基于 Stack 构建了项目，如经典的解释器架构一样，将项目主要划分为了三个模块，<code>Lexer</code>，<code>Parser</code> 和 <code>Interpreter</code>，输入的 Scheme 代码首先通过 Lexer 转为 token 序列，然后通过 Parser 将 tokens 转换为 AST，最后由 Interpreter 解释 AST 执行。</p><p>HaScheme 参考了很多 <a href="https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours">Write yourself a Scheme in 48 hours</a> 这个教程的内容，这个教程非常初学者友好，不过这个教程实现的文法作用域有一些 bug。HaScheme 中修复了文法作用域的问题，并且扩展了语法特性。</p><span id="more"></span><h2 id="Lexer-and-Parser"><a href="#Lexer-and-Parser" class="headerlink" title="Lexer and Parser"></a>Lexer and Parser</h2><p>Lexer 和 Parser 是写的最舒服的一部分了，主要是 ParseC 实在太好用了，可以用原生 Haskell 代码直接描绘语法生成式的结构，并通过 Parser Monad 很轻松的转换为想要的数据结构。</p><p>ParseC 是自顶向下分析的，遇到不匹配的情况需要用 <code>try</code> 回溯，比较遗憾的是对于需要回溯的情况，ParseC 无法正确地输出报错信息，对于这部分我也没有特别处理，所以 Parser 的报错系统非常简陋。</p><h2 id="文法作用域的实现"><a href="#文法作用域的实现" class="headerlink" title="文法作用域的实现"></a>文法作用域的实现</h2><p>Haskell 原生的数据结构就是 AST 的形式，配合 Pattern Match 解释起来简直爽到起飞，不过 Immutable 的特性就不那么令人愉快了，应该是我姿势水平不足的缘故，不少操作（如 <code>define</code>，<code>set!</code>）在解释的过程中是会对环境造成影响的，一开始我尝试用 State Monad 来实现环境，大概思路是</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs Haskell"><span class="hljs-keyword">import</span> Control.Monad.State<br><br><span class="hljs-title">schemeDefineVar</span> :: <span class="hljs-type">String</span> -&gt; <span class="hljs-type">SchemeValue</span> -&gt; <span class="hljs-type">State</span> <span class="hljs-type">Environment</span> ()<br></code></pre></td></tr></table></figure><p>不过这样的话，非常困扰于文法作用域的实现，因为对于一个作用域，必须能引用父级作用域的中的变量，也可以支持 Variable Shadowing 而不对父级作用域造成影响。</p><p>最终的实现参考了教程，即基于 <code>Data.IORef</code> 来实现变量。</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs Haskell"><span class="hljs-keyword">import</span> Data.Map<br><span class="hljs-keyword">import</span> Data.IORef<br><br><span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-type">Environment</span> = <span class="hljs-type">IORef</span> (<span class="hljs-type">Map</span> <span class="hljs-type">String</span> (<span class="hljs-type">IORef</span> <span class="hljs-type">SchemeValue</span>))</span><br></code></pre></td></tr></table></figure><p>等于在命令式语言中保存了变量实体的指针，这样在新建一个子级作用域的时候，可以简单的拷贝当前环境，子级作用域也能对当前环境的变量进行读写，同时在新增变量时只对新的环境进行修改而不影响父级作用域的环境。</p><p>这部分就是<a href="https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/Adding_Variables_and_Assignment">教程</a>实现错误的地方，教程中，对于 <code>define</code> 一个同名变量</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs Haskell"><span class="hljs-title">defineVar</span> :: <span class="hljs-type">Env</span> -&gt; <span class="hljs-type">String</span> -&gt; <span class="hljs-type">LispVal</span> -&gt; <span class="hljs-type">IOThrowsError</span> <span class="hljs-type">LispVal</span><br><span class="hljs-title">defineVar</span> envRef var value = <span class="hljs-keyword">do</span><br>     alreadyDefined &lt;- liftIO $ isBound envRef var<br>     <span class="hljs-keyword">if</span> alreadyDefined<br>        <span class="hljs-keyword">then</span> setVar envRef var value &gt;&gt; return value<br>        <span class="hljs-keyword">else</span> liftIO $ <span class="hljs-keyword">do</span><br>             valueRef &lt;- newIORef value<br>             env &lt;- readIORef envRef<br>             writeIORef envRef ((var, valueRef) : env)<br>             return value<br></code></pre></td></tr></table></figure><p>他会简单的修改修改变量的值为新的值，这回导致对父级作用域的变量进行修改，而这时候正确的行为是掩蔽父级作用域的同名变量，即新建一个 <code>IORef</code> 替换当前的 <code>IORef</code> 而非修改当前的 IORef。</p><p>教程中的实现会产生如下 bug</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs Scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">f</span> x y)<br>  (<span class="hljs-name"><span class="hljs-built_in">+</span></span> ((<span class="hljs-name"><span class="hljs-built_in">lambda</span></span> (x)<br>    (<span class="hljs-name"><span class="hljs-built_in">+</span></span> x <span class="hljs-number">1</span>)) y) x)<br><br>(<span class="hljs-name">f</span> <span class="hljs-number">2</span> <span class="hljs-number">4</span>)<br></code></pre></td></tr></table></figure><p>正确的输出应该是 7，然而执行的结果却是 9，因为嵌套内部的函数参数 x 修改了外部 x 的值。</p><p>在我的实现中</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs Haskell"><span class="hljs-title">defineVar</span> :: <span class="hljs-type">Environment</span> -&gt; <span class="hljs-type">String</span> -&gt; <span class="hljs-type">SchemeValue</span> -&gt; <span class="hljs-type">IOThrowsError</span> <span class="hljs-type">SchemeValue</span><br><span class="hljs-title">defineVar</span> envRef varname val = <span class="hljs-keyword">do</span><br>  env &lt;- liftIO $ readIORef envRef<br>  liftIO $ <span class="hljs-keyword">do</span><br>    valRef &lt;- newIORef val<br>    writeIORef envRef (<span class="hljs-type">Map</span>.insert varname valRef env)<br>    return val<br></code></pre></td></tr></table></figure><p>删去了变量是否存在的判断，一律插入新的 <code>IORef</code>，修复了这个 bug。</p><h2 id="命令式语言特性的实现"><a href="#命令式语言特性的实现" class="headerlink" title="命令式语言特性的实现"></a>命令式语言特性的实现</h2><p>Haskell 是支持一部分命令式的语法的，所以我实现了 <code>begin</code> 语句 和 <code>while</code> 语句，不过在我的实现方法中很难正确的实现 <code>while</code> 语句，所以我用了一个很 Hack 的实现，在 Parser 的阶段将 <code>while</code> 语句 parse 成 <code>if</code> 语句和 尾递归调用的语法糖，这种实现有很多问题比如栈溢出等，所以其实没有正确实现这个特性。</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs Haskell"><span class="hljs-title">parseWhile</span> :: <span class="hljs-type">Parser</span> <span class="hljs-type">Expr</span><br><span class="hljs-title">parseWhile</span> = <span class="hljs-keyword">do</span><br>  char <span class="hljs-string">&#x27;(&#x27;</span><br>  reserved <span class="hljs-string">&quot;while&quot;</span><br>  spaces<br>  cond &lt;- parseExpr<br>  spaces<br>  body &lt;- parseExpr<br>  char <span class="hljs-string">&#x27;)&#x27;</span><br>  return . <span class="hljs-type">BeginExpr</span> $ <span class="hljs-type">ListExpr</span> [<br>    <span class="hljs-type">DefineVarExpr</span> <span class="hljs-string">&quot;`whilerec&quot;</span> (<br>      <span class="hljs-type">LambdaFuncExpr</span> [] (<br>        <span class="hljs-type">IfExpr</span> cond (<span class="hljs-type">BeginExpr</span> $ <span class="hljs-type">ListExpr</span> [<br>          body,<br>          <span class="hljs-type">FuncCallExpr</span> (<span class="hljs-type">SymbolExpr</span> <span class="hljs-string">&quot;`whilerec&quot;</span>) (<span class="hljs-type">ListExpr</span> [])<br>        ]) cond)<br>    ),<br>    <span class="hljs-type">FuncCallExpr</span> (<span class="hljs-type">SymbolExpr</span> <span class="hljs-string">&quot;`whilerec&quot;</span>) (<span class="hljs-type">ListExpr</span> [])]<br></code></pre></td></tr></table></figure><h2 id="REPL"><a href="#REPL" class="headerlink" title="REPL"></a>REPL</h2><p>没有 REPL 的解释器是不完整的，这里非常感谢 <a href="http://hackage.haskell.org/package/haskeline">Haskeline</a> 的作者，借助 Haskeline 强大的 API，实现了很好用的 REPL。</p><p><img src="http://7xleha.com1.z0.glb.clouddn.com/repl.gif" alt="repl"></p><p>实现了历史记录、自动补全、语法树查看等特性。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://github.com/TennyZhuang/HaScheme&quot;&gt;HaScheme&lt;/a&gt; 是用 Haskell 实现的 Scheme 解释器，作为 函数式编程语言课程的 Course Project, 应该是这学期最满意且收获最多的一个大作业了，得益于之前编译原理 Course Project &lt;a href=&quot;https://github.com/TennyZhuang/NaiveC&quot;&gt;NaiveC&lt;/a&gt; 踩了很多坑，对编译器&amp;#x2F;解释器前端相关的一些理论有了一些了解，在连肝五天以后基本完成了 Scheme 标准语法的大部分内容。&lt;/p&gt;
&lt;p&gt;HaScheme 基于 Stack 构建了项目，如经典的解释器架构一样，将项目主要划分为了三个模块，&lt;code&gt;Lexer&lt;/code&gt;，&lt;code&gt;Parser&lt;/code&gt; 和 &lt;code&gt;Interpreter&lt;/code&gt;，输入的 Scheme 代码首先通过 Lexer 转为 token 序列，然后通过 Parser 将 tokens 转换为 AST，最后由 Interpreter 解释 AST 执行。&lt;/p&gt;
&lt;p&gt;HaScheme 参考了很多 &lt;a href=&quot;https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours&quot;&gt;Write yourself a Scheme in 48 hours&lt;/a&gt; 这个教程的内容，这个教程非常初学者友好，不过这个教程实现的文法作用域有一些 bug。HaScheme 中修复了文法作用域的问题，并且扩展了语法特性。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Haskell" scheme="https://blog.zhuangty.com/tags/Haskell/"/>
    
  </entry>
  
  <entry>
    <title>2016 软工个人总结 与 CamusAPI</title>
    <link href="https://blog.zhuangty.com/se2016/"/>
    <id>https://blog.zhuangty.com/se2016/</id>
    <published>2016-12-30T15:10:55.000Z</published>
    <updated>2026-02-19T10:33:34.179Z</updated>
    
    <content type="html"><![CDATA[<p>这学期（又）上了一遍软工3，一开始准备做个微信端的校园服务公众号，后来感觉不是很喜欢微信开发，需要纠结很多微信 API 和权限的问题，所以更换了纯 API 的项目。</p><p><a href="https://github.com/TennyZhuang/CamusAPI">CamusAPI</a> 这个项目的初衷是希望建立一个清华内部的校园开放 API 平台，供校园应用的开发者使用，不需要处理复杂的爬虫逻辑和页面逻辑，将学校的系统封装成一层清晰完整的 RESTful API 系统。</p><span id="more"></span><p>由于本来就有比较丰富的开发经验，知道架构对整个项目的重要性，所以一开始的项目架构是我独立完成的，将项目根据功能划分成了好几个模块，尽可能地减少模块间的耦合。此外，考虑到去年开发紫荆之声时代码风格的丑陋和整个团队开发风格的不一致，我又提前配置了 <a href="http://eslint.org/">ESLint</a> 和 <a href="http://travis-ci.com/">travis-ci</a>，确保整个团队的代码风格完全统一且严谨，对诸如每个函数的行数，每个文件的行数，最大缩进层数也做出了严格的限制，不通过 ESLint 的 commit 不会被合并到主分支，虽然这会增加一些开发时的成本，但是比起团队成员互相维护代码，以及可能的重构时带来的便利，这些成本是微不足道的，也很感谢组员的配合。</p><p>之前的开发中经常遇到本地开发完到最后不会部署的情况，所以这次在架构完一开始就配置好了 <a href="http://docker.com/">Docker</a>，并且每天从 master 分支部署一次，这样对联合调试也帮助很大。</p><p>测试是软件工程中非常重要的一环，比较遗憾的是我们组由于开发周期非常紧以及测试相对来说困难，没能采用 <a href="https://en.wikipedia.org/wiki/Test-driven_development">TDD</a> 的开发模式，不过在最后的一周我们补全了必要的测试，我们核心模块的测试覆盖率超过 95%。不过这次被坑的是性能测试，由于对 NoSQL 数据库的原理不够了解，一开始设计数据库的时候踩了坑，使用了不合适的组织方式，也因为没有提前性能测试，直到接近 ddl 的时候才发现性能很差，经过重重排查后发现是数据库的锅，连夜重构，索性前期架构模块划分合理，数据库操作被包在一个模块里，所以重构没有遇到很大的困难，吸取的教训是每添加一个功能都该进行一次性能测试，这样既能实时 profile，也能及时发现不合理的操作避免无谓的重构。</p><p>作为 API 小组，我们的文档非常重要，不过我们组都不是很擅长写文档，这里感谢马子俊同学承包了几乎所有的文档工作，写出了完整的接口文档供其他组的同学使用，并受到了好评。</p><p>不过担任 API 小组也让人体会到了甲方的坑爹之处，毕竟别的组的项目都是自己提需求，自己伪装成用户，可以什么好做做什么。起初我组只打算给一到两组提供 API，甚至没有的话完全可以自己搭建客户端来展示，后来在老师的要求下为其他所有组提供相关的 API，不得不说这并不是个愉快的体验，我组面临二十来个甲方提需求不堪重负，甚至相当一部分需求是受限于学校系统无法完成的，虽然让他们自己做他们也完不成，但是要求我们提供的时候丝毫没有觉悟。此外我们原来希望联合的小组开发进度不要太快，这样我们可以比较优雅地组织我们的代码，但部分强力的组加入后我们不得不赶工。出于尽可能让他们能够稳定开发的目的，我组不得不先以功能为第一目的，在安全性、性能等原本很重要的地方做一些让步以适应他们的开发。而且人多口杂需求多，在某些新加入的组的要求下被迫做出一些对已经稳定的 API 修改接口的行为，从某种程度上来说我们拉低了他们的开发进度，他们拉低了我们的代码质量。</p><p>这个项目从功能上来讲并不是我开发过的最复杂的应用，实现的功能也比较基本，但是从组织模式上来说是最工程化的，也在合作中收获了很多，感谢软工课给我的机会。最后，对课程的建议主要是前期文档实在有点多，很多实现细节不可能在开发前就确定，文档也随着每个迭代更新或许更合理。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这学期（又）上了一遍软工3，一开始准备做个微信端的校园服务公众号，后来感觉不是很喜欢微信开发，需要纠结很多微信 API 和权限的问题，所以更换了纯 API 的项目。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/TennyZhuang/CamusAPI&quot;&gt;CamusAPI&lt;/a&gt; 这个项目的初衷是希望建立一个清华内部的校园开放 API 平台，供校园应用的开发者使用，不需要处理复杂的爬虫逻辑和页面逻辑，将学校的系统封装成一层清晰完整的 RESTful API 系统。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Programming" scheme="https://blog.zhuangty.com/tags/Programming/"/>
    
    <category term="Web" scheme="https://blog.zhuangty.com/tags/Web/"/>
    
  </entry>
  
</feed>
