<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>iPotato</title>
    <link href="https:&#x2f;&#x2f;ipotato.me" />
    <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;feed" rel="self" type="application/rss+xml"/>
    
    <updated>2026-03-09T14:30:37Z</updated>
    
    <!-- to ensure it's in canonical form, as described by section 6 of RFC 3986 -->
    <id>https:&#x2f;&#x2f;ipotato.me&#x2f;</id>
    
    <entry>
        <title>Intention Is All You Need</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/intention-is-all-you-need"/>
        <published>2026-03-09T14:01:33Z</published>
        <updated>2026-03-09T14:30:37Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/intention-is-all-you-need</id>
        <content type="html">
            <![CDATA[<p>距离博客的上一次更新正好过去了一年多。这一年世界，至少是计算机世界，发生了某种“翻天覆地式”的变化。更确切地讲，这些变化的转折点几乎都来自于 2025 年 12 月前后，一系列 SOTA 模型的发布，让 Vibe Coding 悄然跨越了某个质变的节点。程序员像是亲手制造了一个铺天盖地的海啸，并站在岸边观摩它的逼近，首当其冲被吞没的同时，也远先于这地球上任何一个其他角落的职业认识到这样一个事实——人类关于生产力的历史要再一次被改写了——哼，还颇有一丝“殉道者”的隐喻。</p>
<p>我几乎已经从 2026 年 2 月的某个时刻开始就再也没有写过代码，尽管如此我依然保持着高强度的产出，甚至从 GitHub 的绿砖上看，AI 似乎真的让我成了那种 “10x 工程师”。</p>
<p><figure><img src="https://webp.ipotato.me/20260309202644439-6e2e73a7c262abdfb432115bba04e266.png" alt="" title="甚至从那时开始我的活跃度更高了" /><figcaption>甚至从那时开始我的活跃度更高了</figcaption></figure></p>
<p>在这种让人事半功倍的加速效果下，我能感受到最深的情绪其实是焦虑，我想没有程序员能在这样摧枯拉朽般的军备竞赛中保持坦然，但伴随着焦虑孕育而生的更有一种难以言表的兴奋，细细品味这份兴奋，甚至不亚于我在数十年前第一次看到终端中输出“Hello, world!”的那一刻。</p>
<p>行业的这一系列剧变期间，我读到过最具启发性的文章是这篇<a href="https://www.piglei.com/articles/ai-programming-is-a-new-framework/">《AI 是一种编程框架》</a>，它让我觉察到了所谓 Vibe Coding 更深层次的那份底色——意图。LLM 的涌现能力，让意图这个萦绕在所有软件上空最本质的抽象概念，以无比具象的方式变成了软件，或者说软件开发它本身。我们所有程序员在过去现在所学习和掌握的编程语言，无非是达成意图的某种命令集合，意图是所有软件存在的根本原因：每一个软件本质上都是某种人类意图的具象化。过去这个抽象概念需要经过需求分析、架构设计、编码实现等漫长链路才能变成软件，但现在 LLM 把这条链路大幅压缩了。无需思考“怎么做”（How），只需要许愿“我想要什么”（What）。意图不再是软件开发的起点，而是几乎等同于软件开发本身。</p>
<p>更进一步的，我想 Vibe Coding 的成立不只是改变了软件开发的方式。从今往后我们为 Agents 搭建框架、围绕它们的能力敲定工作范式时，所需持有的思维方式也需一同彻底革新。意图本身就是高级的，概括的，有思维性的，任何试图为意图搭建舞台的方式，也必须是高级的，概括的，有思维性的。这话听起来有点抽象，但我想用我最近很喜欢的一个 AI 应用作为例子阐述这一点。<a href="https://slock.ai/">Slock</a> 用作者 <a href="https://github.com/stdrc">@stdrc</a> 自己的话讲只是一款 agent-native 的即时通讯工具（IM），应用不管是名字还是用法也都是在玩梗 Slack——这可能整体上来看会缺乏严肃性，给人一种玩具感。但我在使用过程中，愈发觉得这是一款设计理念领先到令人无比敬佩程度的应用。</p>
<p>假设这是一道面试题：设计一个跨 Agent 跨机器编排协作框架。你会怎么做？如果一个从未用过 Slock 的我来解这道题，我想我会下意识地给出类似如下的答案：</p>
<ul>
<li>统一的 Agent 运行沙箱/Wrapper/交互接口</li>
<li>统一的 Agent 通信协议</li>
<li>统一的上下文存储管理</li>
<li>某种类似 Coordinator 的协调组件，用来协调和下发各类任务</li>
<li>......</li>
</ul>
<p>甚至我把这个问题在 Slock 里丢给 Agents 们，它们的思路也大同小异，甚至答得更完备，更全面：</p>
<p><figure><img src="https://webp.ipotato.me/20260309210555858-3edbb652f0c6e0a71a2e3007ec119468.png" alt="" title="Bob 是 gpt-5.4 high" /><figcaption>Bob 是 gpt-5.4 high</figcaption></figure></p>
<p>但实际上，Slock 就是用最符合直觉且自然的群聊功能，完成了跨 Agent 跨机器的编排协作框架，并且效果非常的好。Agent 间协作通信的手段就是普通的群聊消息，用 channel 来隔绝上下文……回到此前我抛出的那个观点：意图本身就是高级的，概括的，有思维性的，任何试图为意图搭建舞台的方式，也必须是高级的，概括的，有思维性的。如果问题变成：现在你需要邀请 N 个人一起协作完成一件事，你会怎么做？我想没有人会觉得“拉所有人进一个群或开一个会”是一个荒唐的选择。同样的，这样的解决问题思路也应该是未来我们围绕 Agent 开发创造协作模式，创建工作流的“第一性思维”——一切从意图出发，软件工程已经是旧时代的“糟粕”啦。</p>
<p>当然，文末不可避免的要来到叠甲环节。当意图本身是模糊的、矛盾的、需要在执行中迭代澄清的，单纯地对着 LLM “许愿”还够用吗？以当下的视角看，意图和软件之间的距离并没有被真正消除，只是被暂时遮蔽了。意图可以是起点，但从意图到可靠、可维护、安全的软件之间，仍然存在一段不可忽视的工程距离。有人戏称 LLM 的到来让计算机科学领域终于涌入了大量“计算机民科”，但我想 Vibe Coding 给越来越多的人类一种“人定胜天”的幻觉其实是一件好事，在 LLM 还没把写 bug 的人类毛病彻底改掉之前，软件工程仍然是一门“逆熵”的艺术，而这也许是我们 CS 从业人员最后的护城河。</p>
<p>Intention Is All You Need...?</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>使用 Cloudflare Workers 搭建轻量级 LLM API 网关</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/build-lightweight-llm-api-gateway-using-cloudflare-workers"/>
        <published>2025-03-04T07:50:18Z</published>
        <updated>2025-03-09T12:50:40Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/build-lightweight-llm-api-gateway-using-cloudflare-workers</id>
        <content type="html">
            <![CDATA[<p>在这个 LLM 服务商们每天都在进行军备竞赛的时代，对我们这种面向 LLM 编程的程序员来说，最常见的一个痛点莫过于管理一大堆 API Endpoint 和 Secret Key 了，再加上：</p>
<ul>
<li>不同的服务商 Host 了相同或不同的模型。</li>
<li>在不同的服务商里都撒了币。</li>
<li>不同服务商的 SLA 可能相去甚远。</li>
<li>要在不同的软件里重复配置这些相同的内容。</li>
</ul>
<p>这一系列问题的解决方案自然是一个统一的 LLM API Gateway，原理上看也并不复杂，只消维护 Model 和 API Provider 之间的映射规则，然后按需转发即可。</p>
<pre style="background-color:#2d2d2d;"><code class="language-json"><span style="color:#d3d0c8;">{&quot;</span><span style="color:#99cc99;">model</span><span style="color:#d3d0c8;">&quot;: &quot;</span><span style="color:#99cc99;">claude</span><span style="color:#d3d0c8;">&quot;}
</span><span style="color:#d3d0c8;">    ⬇️
</span><span style="color:#d3d0c8;">LLM API Gateway
</span><span style="color:#d3d0c8;">    ⬇️
</span><span style="color:#d3d0c8;">{&quot;</span><span style="color:#99cc99;">model</span><span style="color:#d3d0c8;">&quot;: &quot;</span><span style="color:#99cc99;">claude-3-7-sonnet-latest</span><span style="color:#d3d0c8;">&quot;}
</span><span style="color:#d3d0c8;">    ⬇️            ⬇️
</span><span style="color:#d3d0c8;">Anthropic    AWS Bedrock
</span></code></pre>
<p>实际上市面上也早已有成熟的开源方案，例如 <a href="https://github.com/songquanpeng/one-api">songquanpeng/one-api</a>。但对于我来说，部署这么一套管理系统显得有些太重了，作为个人使用，似乎不太需要额外的租户管理和账单系统，加之如果想要进行远端部署，产生额外的服务器、域名等维护成本也有些令人抗拒，所以“造轮子”似乎又成了最终的选择。</p>
<p>比起年轻气盛时动不动就想“万丈高楼平地起”地从零开始造轮子，成年人造轮子的哲学则是“应拼尽拼”，能用现成的预制零件快速拼出来的轮子也是好轮子——于是开始整理造这么一个私人 LLM API Gateway 的基本要求：</p>
<ul>
<li>易于维护，部署成本要尽可能的低，最好能用现成的 SaaS。</li>
<li>开箱即用，使用和配置方式简单，即暴露一个统一的 API 接口，可以自由配置模型的转发映射。</li>
<li>安全，有基本的鉴权以防止滥用。</li>
<li>通用，适配主流的 LLM API Provider 格式。</li>
</ul>
<p>首先，作为一个几乎无状态的 API Gateway，最核心的逻辑就是转发 HTTP 请求，所以立马出现在选品单上的就是 <a href="https://developers.cloudflare.com/workers">Cloudflare Workers</a>，其提供的 Severless 应用部署非常适合写这种 Proxy，再加上配套的 <a href="https://developers.cloudflare.com/kv/">Cloudflare Workers KV</a>，转发配置需要持久化存储的需求也被满足了。于是我基于官方的 Rust SDK <a href="https://github.com/cloudflare/workers-rs">worker-rs</a> 实现了 <a href="https://github.com/JmPotato/one-united">one-united</a>，仅需简单的配置，就可以把一个轻量级的 LLM API Gateway 部署到 Cloudflare Workers 上。</p>
<h1>部署方式</h1>
<p>因为官方提供了非常齐全的配套，整个部署过程需要做的准备仅需提前安装上较新版本的 <a href="https://rustup.rs">Rust</a> 和 <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> 即可。</p>
<p>首先把项目拉到本地，然后开始编辑我们的 <code>wrangler.toml</code>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-bash"><span style="color:#6699cc;">git</span><span style="color:#d3d0c8;"> clone https://github.com/one-united/one-united.git
</span><span style="color:#6699cc;">cp</span><span style="color:#d3d0c8;"> wrangler.example.toml wrangler.toml
</span></code></pre>
<p>其实这里要做的就是<a href="https://developers.cloudflare.com/workers/wrangler/commands/#kv-namespace-create">创建一个 KV namespace</a>，一条命令就能搞定：</p>
<pre style="background-color:#2d2d2d;"><code class="language-bash"><span style="color:#6699cc;">npx</span><span style="color:#d3d0c8;"> wrangler kv:namespace create config
</span></code></pre>
<p>运行成功后把输出中提供的 <code>kv_namespaces</code> 部分粘贴到 <code>wrangler.toml</code> 文件中即可，格式类似于：</p>
<pre style="background-color:#2d2d2d;"><code class="language-toml"><span style="color:#d3d0c8;">[[kv_namespaces]]
</span><span style="color:#d3d0c8;">binding = &quot;config&quot;
</span><span style="color:#d3d0c8;">id = &quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;
</span></code></pre>
<p>然后就是用最后一条命令完成最终部署：</p>
<pre style="background-color:#2d2d2d;"><code class="language-bash"><span style="color:#6699cc;">npx</span><span style="color:#d3d0c8;"> wrangler deploy
</span></code></pre>
<p>然后你的服务就跑在 <code>https://&lt;YOUR_WORKER&gt;.&lt;YOUR_SUBDOMAIN&gt;.workers.dev</code> 上可供访问了！</p>
<h1>配置文件</h1>
<p>刚部署好的 one-united 自然是没有配置任何模型转发的，项目里提供了一个<a href="https://github.com/JmPotato/one-united/blob/main/config.example.yaml">规则模板</a>，其格式还是比较直观的，我以此为例展示一个我自己的使用场景，来看看具体如何配置自己的模型转发。</p>
<p>我最常用 LLM 的一个场景就是翻译，例如 Bob 的 AI 翻译服务。</p>
<p><figure><img src="https://webp.ipotato.me/20250304000356322-9e8aa8f644d79bda3dc9284e252b4929.png" alt="" /></figure></p>
<p>可以看到我定义了一个名为 <code>translator</code> 的模型，其转发逻辑的配置如下：</p>
<pre style="background-color:#2d2d2d;"><code class="language-yaml"><span style="color:#f2777a;">rules</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">  - </span><span style="color:#f2777a;">model</span><span style="color:#d3d0c8;">: </span><span style="color:#99cc99;">translator
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">providers</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">      - </span><span style="color:#f2777a;">identifier</span><span style="color:#d3d0c8;">: </span><span style="color:#99cc99;">oh-my-gpt
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">models</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">          - </span><span style="color:#99cc99;">gpt-4o-mini
</span><span style="color:#d3d0c8;">      - </span><span style="color:#f2777a;">identifier</span><span style="color:#d3d0c8;">: </span><span style="color:#99cc99;">openrouter
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">models</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">          - </span><span style="color:#99cc99;">openai/gpt-4o-mini
</span><span style="color:#d3d0c8;">      - </span><span style="color:#f2777a;">identifier</span><span style="color:#d3d0c8;">: </span><span style="color:#99cc99;">dailyio
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">models</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">          - </span><span style="color:#99cc99;">gpt-4o-mini
</span><span style="color:#d3d0c8;">          - </span><span style="color:#99cc99;">meta-llama/Llama-3.3-70B-Instruct-Turbo
</span></code></pre>
<p>不难发现对于翻译服务，我使用的都是主流模型中 Token 价格较低的模型，如此一来在使用较为频繁的翻译场景下，可以在保证质量的前提下尽可能节省 Token。<code>providers</code> 的配置字段都比较直接，在此不表，按需添加和配置自己的提供商即可。</p>
<p>目前 one-united 还没有图形化界面，所以更新配置需要用 curl 直接把 <code>config.json</code> 扔给接口：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">curl -X POST https://&lt;YOUR_WORKER&gt;.&lt;YOUR_SUBDOMAIN&gt;.workers.dev/config \
</span><span style="color:#d3d0c8;">-H &quot;Content-Type: application/yaml&quot; \
</span><span style="color:#d3d0c8;">--data-binary @config.yaml
</span></code></pre>
<h1>一些提升体验的功能</h1>
<p>在简单转发的基础上，我也根据平时使用的一些经验和习惯加了一些必要的功能和优化。</p>
<h2>设置 API KEY</h2>
<p>虽然说整个流程属于私人部署，但也不免存在接口泄漏的可能，避免被他人滥用导致 Token 额度被迅速消耗完，可以给自己的 Gateway 也设置上 API Key。这里有两个操作办法， 一个是直接通过命令行设置 <code>ONE_API_KEY</code>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">npx wrangler secret put ONE_API_KEY
</span></code></pre>
<p>也可以到 Cloudflare 的 Workers 后台界面添加：</p>
<p><figure><img src="https://webp.ipotato.me/20250304102236420-e9ea514f2c396522a5bd5ad7e1724c31.png" alt="" /></figure></p>
<p>此后所有的 curl 请求都可以带上 <code>-H &quot;Authorization: Bearer $ONE_API_KEY&quot;</code> 进行鉴权了，同样，在类似 Chat Bot 的 API 配置中，也需要填上 <code>$ONE_API_KEY</code> 方可正常调用。</p>
<p><figure><img src="https://webp.ipotato.me/20250304102555408-3309cbba3a52f4c7687744ef77d2a88c.png" alt="" /></figure></p>
<h2>负载均衡</h2>
<p>当同一个模型名配置了多个不同的 <code>(Provider, Model)</code> 映射时，为了保证尽可能好的延迟表现，每次会通过负载均衡机制在不同映射间进行切换，并记录每次请求的耗时，最终尽可能地选择转发至延迟较低的提供商。</p>
<p>目前这个策略还比较简陋，我还在考虑是否要添加诸如基于权重或者 Token 成本的负载均衡策略。</p>
<h2>常用的接口</h2>
<p>one-united 一共提供了以下几个接口：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">WorkerRouter::new()
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">get_async</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">/config</span><span style="color:#d3d0c8;">&quot;, get_config)
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">post_async</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">/config</span><span style="color:#d3d0c8;">&quot;, save_config)
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">get_async</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">/stats</span><span style="color:#d3d0c8;">&quot;, get_stats)
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">get_async</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">/v1/models</span><span style="color:#d3d0c8;">&quot;, get_models)
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">post_async</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">/v1/chat/completions</span><span style="color:#d3d0c8;">&quot;, route_chat_completions)
</span><span style="color:#d3d0c8;">    .</span><span style="color:#66cccc;">run</span><span style="color:#d3d0c8;">(req, env)
</span><span style="color:#d3d0c8;">    .await
</span></code></pre>
<p>其中 <code>/v1/models</code> 和 <code>/v1/chat/completions</code> 都是 OpenAI 兼容的常用接口，后者不用说，就是最常用的 LLM 使用接入口。前者则是 <a href="https://platform.openai.com/docs/api-reference/models/list">List Models</a> 接口，对于一些提供了自动获取模型信息功能的软件来说，可以方便的通过这个接口一键添加所有当前可用的模型信息：</p>
<p><figure><img src="https://webp.ipotato.me/model-list-4678fe0c0a4c3e3e5f3d26c93030475d.png" alt="" /></figure></p>
<p>前面一个小节提到过 one-united 存在负载均衡机制，通过 <code>/stats</code> 这个接口可以看到当前所请求 Workers 实例内的延迟统计信息，方便判断不同服务商的延迟表现如何：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">❯ curl -s -H &quot;Authorization: Bearer $ONE_API_KEY&quot; https://&lt;YOUR_WORKER&gt;.&lt;YOUR_SUBDOMAIN&gt;.workers.dev/stats | jq
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">  &quot;created_at&quot;: &quot;2025-03-06T04:22:13.862Z&quot;,
</span><span style="color:#d3d0c8;">  &quot;hash&quot;: &quot;362c5ee09afe8b5c82f132161496c00072ce850e3a39d204315bf823e8311de8&quot;,
</span><span style="color:#d3d0c8;">  &quot;latency&quot;: [
</span><span style="color:#d3d0c8;">    {
</span><span style="color:#d3d0c8;">      &quot;identifier&quot;: &quot;cf-openrouter&quot;,
</span><span style="color:#d3d0c8;">      &quot;model&quot;: &quot;anthropic/claude-3.7-sonnet&quot;,
</span><span style="color:#d3d0c8;">      &quot;ms&quot;: 1473
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">  ],
</span><span style="color:#d3d0c8;">  &quot;lifetime&quot;: &quot;3m 42s 685ms&quot;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<h2>OpenRouter 统计适配</h2>
<p>如果你搭配 OpenRouter 使用的话，可以看到在 OpenRouter 的 <a href="https://openrouter.ai/activity">Activity</a> 界面上是可以识别到 one-united 转发来的请求标识的，方便掌握具体的用量。</p>
<p><figure><img src="https://webp.ipotato.me/20250304154029411-c208928c23d9801f0c7e97414c3f7c59.png" alt="" /></figure></p>
<p>这个也是根据 OpenRouter 官方的文档加的请求识别头来实现的：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#747369;">// &quot;HTTP-Referer&quot; and &quot;X-Title&quot; will be used by service like OpenRouter to identify the request.
</span><span style="color:#d3d0c8;">headers.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">HTTP-Referer</span><span style="color:#d3d0c8;">&quot;, &quot;</span><span style="color:#99cc99;">https://github.com/JmPotato/one-united</span><span style="color:#d3d0c8;">&quot;)?;
</span><span style="color:#d3d0c8;">headers.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">X-Title</span><span style="color:#d3d0c8;">&quot;, &quot;</span><span style="color:#99cc99;">one-united</span><span style="color:#d3d0c8;">&quot;)?;
</span></code></pre>
<h2>Bypass Rule</h2>
<p>如果你想 Bypass 掉规则，直接请求对应 Provider 的某个模型，可以使用 <code>model@@provider_identifier</code> 这个语法，例如直接请求配置中来自 OpenRouter 的  GPT-4o mini：<code>openai/gpt-4o-mini@@openrouter</code>，请求就会直接发给 OpenRouter，而不会经过负载均衡中转，指哪打哪。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>如何利用全 SaaS 阵容从零免费搭建一个博客？</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/how-to-build-a-blog-for-free-using-saas"/>
        <published>2024-08-23T15:54:47Z</published>
        <updated>2025-03-07T17:28:19Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/how-to-build-a-blog-for-free-using-saas</id>
        <content type="html">
            <![CDATA[<p>即便已经几乎无人再会把“自搭自写”博客的 Programmer 称为 Geek，但我相信拥有一个 Self-Hosted Blog 依然是众多 Programmer 的普遍追求。虽然 GitHub Pages 和 Jekyll 的组合放到现在也依然不算过时，也不失简便，但考虑到如今各类 SaaS 服务层出不穷，那么在 2024 年的今天，我们能否利用全 SaaS 阵容从零免费搭建一个博客？</p>
<h1>rsomhaP 作为博客程序</h1>
<p>关注过我博客的朋友可能会知道在过去的十余载里，<a href="https://github.com/JmPotato/Pomash">Pomash</a> 是我自己拿 Python 写成，也是我自己一直在用的博客程序。不过在使用和维护的过程中我也逐渐发现这个几乎写成于十年前的博客程序似乎有点“过时”了，整理过后，我为造轮子另起炉灶罗列了几点冠冕堂皇的理由：</p>
<ul>
<li>Python 已经不再是我熟悉的语言，诸多语言特性和最佳实践我已经不甚了解。</li>
<li>虽然 Pomash 是个很简单的博客引擎，但从代码中也不难看出我曾经“笔法”的稚嫩——有很多不忍直视的代码。</li>
<li>用一个 SQLite 文件存储我数十年以来的博客文章听起来一点也不高可用。</li>
<li>用 Rust 重写 XXX 是流量密码。</li>
</ul>
<p>于是乎作为一次具有某种“致敬”意味的行为艺术，我用 Rust 重写了 Pomash，并用我很喜欢的命名法给它起了一个新名字：</p>
<blockquote>
<p>'r{}'.format(''.join(sorted('Pomash'))[::-1]) == 'rsomhaP'</p>
</blockquote>
<p>somhaP 是 Pomash 重排后的字符串，加一个字母 r 在开头拼接成 rs 意指用 Rust 重写而成——<a href="https://github.com/JmPotato/rsomhaP">rsomhaP</a>。</p>
<p>为了全面拥抱 SaaS 服务简化实现与部署，在造轮子的同时“反造轮子”，我在重写的时候带着这么几个原则：</p>
<ul>
<li>依然是 Markdown 友好。</li>
<li>保持单体程序，拒绝前后端分离（其实是我不会写前端）。</li>
<li>简洁且易读的样式。</li>
<li>尽可能使用 SaaS 友好的方式去设计部署方式。</li>
</ul>
<p>所以最后，这篇文章也孕育而生，让我们看看要跑起来一个 rsomhaP 的博客程序，我们需要哪些步骤。</p>
<h1>TiDB Serverless 作为数据库</h1>
<p>rsomhaP 抛弃了 SQLite 作为本地数据存储，直接使用现在几乎烂大街的 DB SaaS，目前还只支持 MySQL-compatible 的数据库服务，后续也许会考虑支持 PostgreSQL。</p>
<p>这里我自然选择了利益相关的 <a href="https://www.pingcap.com/tidb-serverless">TiDB Serverless</a> 作为后端数据库 SaaS，注册并创建好集群，拿到数据库的 Host 等参数即可，没有任何额外的步骤。</p>
<p><figure><img src="https://webp.ipotato.me/%E6%88%AA%E5%B1%8F2024-08-23%2023.14.10-bb516c8f9e76b7bf3b17a2391cb9faa7.png" alt="" title="免费额度的集群规格对我们来说绰绰有余" /><figcaption>免费额度的集群规格对我们来说绰绰有余</figcaption></figure></p>
<p>MySQL 相关的配置直接写到 rsomhaP 的配置文件里或用环境变量 <code>MYSQL_CONNECTION_URL</code> 均可。</p>
<pre style="background-color:#2d2d2d;"><code class="language-toml"><span style="color:#d3d0c8;">[mysql]
</span><span style="color:#d3d0c8;"># If `connection_url` is set, other connection-related configs will be ignored.
</span><span style="color:#d3d0c8;"># connection_url = &quot;mysql://root:password@127.0.0.1:4000/rsomhaP&quot;
</span><span style="color:#d3d0c8;">username = &quot;root&quot;
</span><span style="color:#d3d0c8;">password = &quot;password&quot;
</span><span style="color:#d3d0c8;">host = &quot;127.0.0.1&quot;
</span><span style="color:#d3d0c8;">port = 4000
</span><span style="color:#d3d0c8;">database = &quot;rsomhaP&quot;
</span></code></pre>
<p>唯一需要注意的点是这里需要提前创建好一个 database：</p>
<pre style="background-color:#2d2d2d;"><code class="language-sql"><span style="color:#cc99cc;">CREATE DATABASE </span><span style="color:#6699cc;">rsomhaP</span><span style="color:#d3d0c8;">;
</span></code></pre>
<p>除了天然的享受到了 TiDB 作为一个分布式数据库的优势，我们也可以在 TiDB Cloud 的面板上自动设置自己数据库的备份时间与频率，为自己智慧的结晶多上一份保险。</p>
<p><figure><img src="https://webp.ipotato.me/202408232318661-d07387ce5bf3b22557d7f47cd652732c.png" alt="" /></figure></p>
<h1>Fly.io 作为部署机器</h1>
<p>数据库有了，那么我们至少需要一个 Host 或者说机器来实际部署运行我们的 rsomhaP 程序，除了直接去 Vultr 这类地方买虚拟云主机，我们还有一个更“小而美”的选择：<a href="https://fly.io">Fly.io</a>。</p>
<p>其实我曾经也写过一篇关于它的博客：<a href="https://ipotato.me/article/74">Fly.io 初体验之博客搬家</a>。彼时我把 Pomash 迁移了上去，整个体验也非常的丝滑，所以在这里依旧沿用了之前的选择。</p>
<p>我为 rsomhaP 直接写好了一份可以用于 Fly.io 部署的 <a href="https://github.com/JmPotato/rsomhaP/blob/main/Dockerfile">Dockerfile</a> 和 <a href="https://github.com/JmPotato/rsomhaP/blob/main/fly.toml">fly.toml</a>，所以你需要做的仅仅是<a href="https://fly.io/docs/flyctl/install">安装好 flyctl</a>，按自己的配置改好 <a href="https://github.com/JmPotato/rsomhaP/blob/main/config.toml">config.toml</a> 然后在 rsomhaP 的目录下运行：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">fly deploy --config fly.toml
</span></code></pre>
<p>这里如果你是第一次用 Fly.io，那么直接运行可能会提示你没有 <code>rsomhap</code> 这个 App。你可以选择去 Fly.io 的网页端 Dashboard 手动创建，也可以直接在命令行里创建：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">fly apps create rsomhap
</span></code></pre>
<p>紧接着一路按照提示进行配置即可，这里有两个地方需要注意一下。</p>
<p>首先是为了保证和数据库服务的连通性，我在部署时选择了和 TiDB Serverless 集群位于相同地区的日本 Region，可以在 <code>fly.toml</code> 里这样配置：</p>
<pre style="background-color:#2d2d2d;"><code class="language-toml"><span style="color:#d3d0c8;">app = &quot;rsomhap&quot;
</span><span style="color:#d3d0c8;">primary_region = &quot;nrt&quot;
</span></code></pre>
<p>对于全部的可用 Region，可以参考<a href="https://fly.io/docs/reference/regions/">这篇官方文档</a>。</p>
<p>部署完成后便可以通过形如 <a href="https://app-name.fly.dev">https://app-name.fly.dev</a> 的 Hostname 来访问你刚刚部署好的 rsomhaP 了，Fly.io 当然也支持接入自定义域名和配置免费的 SSL 证书，均可通过网页 Dashboard 或 flyctl 做到，<a href="https://fly.io/docs/networking/custom-domain/">参考</a>。</p>
<h1>Cloudflare R2 作为图床</h1>
<p>恭喜你，上面哪些步骤全部完成后，你就已经拥有一个可以访问和写作的博客了🎉</p>
<p>但要想进行图文写作，我们还差这最后一步“图”，由于 rsomhaP 本身并不支持上传和存储图片，所以拥有一个稳定的图床服务是有必要的，这里我们可以用赛博大基建 Cloudflare R2 的对象存储作为我们的图床设施，具体设置方法可以参考这篇文章，写得很好很详细，我就不再次展开了：<a href="https://www.pseudoyu.com/zh/2024/06/30/free_image_hosting_system_using_r2_webp_cloud_and_picgo/">从零开始搭建你的免费图床系统（Cloudflare R2 + WebP Cloud + PicGo）</a>。</p>
<h1>WebP Cloud Services 作为图床代理</h1>
<p>有了图床的“床”，我们还可以更精进一步，用 WebP Cloud Services 这个图片代理 SaaS 来实现更多的功能：</p>
<ul>
<li>不改变画质的情况下进行图片体积压缩，并作为图床缓存，加快博客的加载速度。</li>
<li>隐私擦除，添加水印等自动的图片二次处理。</li>
<li>自定义 Header 来实现更安全的图床访问。</li>
</ul>
<p>同样推荐大家参考上面那篇文章的作者：</p>
<ul>
<li><a href="https://www.pseudoyu.com/zh/2024/06/30/free_image_hosting_system_using_r2_webp_cloud_and_picgo/">从零开始搭建你的免费图床系统（Cloudflare R2 + WebP Cloud + PicGo）</a></li>
<li><a href="https://www.pseudoyu.com/zh/2024/07/02/protect_your_image_using_webp_and_cloudflare_waf/">使用 WebP Cloud 与 Cloudflare WAF 为你的图床添加隐私和版权保护</a></li>
</ul>
<h1>结语</h1>
<p>至此，使用 rsomhaP 搭配一众免费 SaaS 的博客部署就完成了，你可以继续浏览本站或者去 rsomhaP 的 <a href="https://github.com/JmPotato/rsomhaP">GitHub 界面</a>了解更多信息，其实关于 rsomhaP 的开发过程也有很多可以分享的点——例如 axum 库的使用，Markdown 渲染的实现等，待我挖个坑日后再填。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>ThinkPad X1 Carbon x Arch Linux</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/thinkpad-x1-carbon-arch-linux"/>
        <published>2023-08-07T14:43:16Z</published>
        <updated>2025-03-07T17:28:43Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/thinkpad-x1-carbon-arch-linux</id>
        <content type="html">
            <![CDATA[<p>是的，作为一个从 2017 年起就再也没用过除 Mac 以外笔电的人，时隔 6 年，我购入了这款联想的 ThinkPad X1 Carbon Gen 11。购买的动机其实很简单，那就是<del>反抗 Apple 暴政</del> macOS 实在用腻了，迫切需要<del>消费</del>折腾带来的新鲜感。</p>
<h1>Why Arch Linux</h1>
<p>虽然我或多或少对 Ubuntu 和 Debian 这种相对而言更流行的 Linux 发行版比较熟悉，但既然有了“折腾“的初心，所以更希望尝试一些新东西，早有耳闻 Arch Linux 的&quot;简洁主义&quot;和非常 KISS (Keep It Simple and Stupid) 的设计原则，再加上前期采购设备前调研时惊叹于 ArchWiki 的完整和详尽，所以最终决定借此机会直接上手体验一下，看能不能作为自己主力机的主力 OS。</p>
<h1>Installing &amp; Configuring</h1>
<p>安装的时候我基本上只参考了这两个指南：</p>
<ul>
<li><a href="https://github.com/nakanomikuorg/arch-guide">https://github.com/nakanomikuorg/arch-guide</a></li>
<li><a href="https://github.com/nakanomikuorg/arch-guide">https://github.com/ArchLinuxStudio/ArchLinuxTutorial</a></li>
</ul>
<p>前者基于后者进行了一些简略的删改，更适合新手，但也导致里面的有一些描述语焉不详，如果你对 Linux 本身或者 OS 原理不太了解的话，需要谨慎操作，尤其需要注意硬盘分区和引导安装之类的步骤。</p>
<p>目前主流硬件厂商对 Linux 的支持都很完善了，所以安装、配置和使用过程都会比较顺利，但最好还是在开始前提前了解 Arch Linux 对你手上设备的支持情况，例如我这台 <a href="https://wiki.archlinux.org/title/Lenovo_ThinkPad_X1_Carbon_(Gen_11)">ThinkPad X1 Carbon (Gen 11) 的兼容性</a>，可以看到除了前置摄像头外，其他大部分硬件在使用上都没什么问题。</p>
<h1>Troubleshooting</h1>
<p>大多数我遇到的安装和配置问题在 Google 后都有比较直接的解决方案（其中大部分最终都指向了 ArchWiki，可见其内容之靠谱和丰富），这里罗列一下我在配置过程中遇到的一些比较独特的问题。</p>
<h2>AUR 安装旧版本 GCC 速度很慢</h2>
<p>本来想编译 TiKV “烤”一下机，但由于 Arch Linux 自带的 GCC 版本太新了，编译 RocksDB 时各种 Warning 以及 Error，于是需要安装一个旧版本 GCC 来用。考察了一圈 TiKV 和 RocksDB 的相关 Issue 和 PR 后决定装 GCC 9。</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">yay -S gcc9
</span></code></pre>
<p>殊不知这一行命令敲下去，由于 AUR 的包需要跑 PKGBUILD 来手动编译后再安装，这一编就是好几个小时，最终还没完成就被我遭不住地杀掉了。也是根据 ArchWiki 的<a href="https://wiki.archlinuxcn.org/wiki/Makepkg#%E5%87%8F%E5%B0%91%E7%BC%96%E8%AF%91%E6%97%B6%E9%97%B4">建议</a>，我做了这俩操作来加速整个过程：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">nproc # 看一下你设备支持的最大进程数，例如我这里是 16
</span><span style="color:#d3d0c8;">MAKEFLAGS=&#39;-j16&#39; BUILDDIR=/tmp/makepkg yay -S gcc9
</span></code></pre>
<p>参数解释：</p>
<ul>
<li><code>-j16</code> 用来指定 make 的并行 Job 数量，一般都等于 CPU 最大支持的线程数量</li>
<li><code>BUILDDIR</code> 用来指定编译目录，扔到 tmpfs 里来充分利用内存加速编译</li>
</ul>
<p>方子很好，原本数小时的编译时间不到半个小时就完成了。</p>
<h2>开启 Secure Boot</h2>
<p>由于拿到电脑的时候自带是 Windows 11 系统，所以在开机初始化配置时我跟随着引导开启了 Windows Hello 的面部及 PIN 码识别，但因为随后安装 Arch Linux 时关闭了 Secure Boot，待我在安装结束后再次进入 Windows 11，发现由于之前设置 Windows Hello 有 TPM 的参与，在关闭 Secure Boot 后面部识别以及 PIN 码就都无法使用了。</p>
<h3>使用 sbctl 签名内核与启动项</h3>
<p>要解决的话思路也很直接，如果能让 Arch Linux 在开启了 Secure Boot 的情况下也能正确引导启动，那就最好了，经过一番搜寻，我在 reddit 的 archlinux 社区发现一篇<a href="https://www.reddit.com/r/archlinux/comments/10pq74e/my_easy_method_for_setting_up_secure_boot_with/">教程</a>来使用 <code>sbctl</code> 这个工具对引导文件进行签名认证。跟着其中的操作一番尝试后，再次启动 GRUB 发现并不能正常引导启动，提示内核加载错误。经过一番搜寻，在 ArchWiki 中<a href="https://wiki.archlinuxcn.org/wiki/UEFI/%E5%AE%89%E5%85%A8%E5%90%AF%E5%8A%A8#sbctl">对 sbctl 进行介绍</a>的部分看到这样一段：</p>
<blockquote>
<p>现在签署所有的未被签署的文件。通常是<a href="https://wiki.archlinuxcn.org/wiki/%E5%86%85%E6%A0%B8" title="内核">内核</a>与<a href="https://wiki.archlinuxcn.org/wiki/%E5%BC%95%E5%AF%BC%E5%8A%A0%E8%BD%BD%E7%A8%8B%E5%BA%8F" title="引导加载程序">引导加载程序</a>需要被签署。比如：</p>
<p>sbctl sign -s /boot/vmlinuz-linux
sbctl sign -s /boot/EFI/BOOT/BOOTX64.EFI</p>
</blockquote>
<p>看起来大概率是因为没有对 <code>vmlinuz-linux</code> 内核引导文件签名导致的，重新进行上述的操作再次尝试开启 Secure Boot 启动，成功进入 Arch Linux，完成对 Secure Boot 的配置。</p>
<p><figure><img src="https://webp.ipotato.me/sign-vmlinuz-linux-59cf30acc7be79030eca85433efa9ebc.png" alt="" title="对 vmlinuz-linux 进行签名" /><figcaption>对 vmlinuz-linux 进行签名</figcaption></figure></p>
<h3>重新设置 Windows Hello</h3>
<p>由于 sbctl 这个工具不一定能在所有硬件上都可行，所以如果你在上面的操作里失败了，但又想在不开启 Secure Boot 的前提下使用面孔/指纹解锁，其实也是有办法的。根据 <a href="https://learn.microsoft.com/zh-cn/windows/security/hardware-security/tpm/tpm-recommendations#tpm-and-windows-features">Windows 官方的文档</a>，我们会发现 Windows Hello 并不依赖 TPM 去工作。</p>
<p><figure><img src="https://webp.ipotato.me/windows-tpm-bb25453d54400909b9fe7678615540d9.png" alt="" title="Windows 官方文档" /><figcaption>Windows 官方文档</figcaption></figure></p>
<p>所以只需要在关闭了 Secure Boot 的前提下重新设置一遍 Windows Hello 就可以在不使用 TPM 的前提下正常使用面孔/指纹解锁了。</p>
<h2>蓝牙设备无法唤醒 Arch Linux</h2>
<p>这个其实解决方法很简单，遵循 ArchWiki 中蓝牙这个界面下“<a href="https://wiki.archlinuxcn.org/wiki/%E8%93%9D%E7%89%99#%E4%BB%8E%E6%8C%82%E8%B5%B7%E4%B8%AD%E5%94%A4%E9%86%92">从挂起中唤醒</a>”这一节的指引操作即可。但我在第一步就遇到了问题，ThinkPad X1 Carbon 的蓝牙设备并没有那么明显地在 <code>lsusb</code> 后被展示出来：</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">❯ lsusb  
</span><span style="color:#d3d0c8;">Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub  
</span><span style="color:#d3d0c8;">Bus 003 Device 006: ID 30c9:0052 Luxvisions Innotech Limited Integrated RGB Camera  
</span><span style="color:#d3d0c8;">Bus 003 Device 005: ID 06cb:00fc Synaptics, Inc.    
</span><span style="color:#d3d0c8;">Bus 003 Device 003: ID 2c7c:0310 Quectel Wireless Solutions Co., Ltd. Quectel EM05-CN  
</span><span style="color:#d3d0c8;">Bus 003 Device 004: ID 0451:82ff Texas Instruments, Inc.    
</span><span style="color:#d3d0c8;">Bus 003 Device 002: ID 0451:8442 Texas Instruments, Inc.    
</span><span style="color:#d3d0c8;">Bus 003 Device 007: ID 8087:0033 Intel Corp.    
</span><span style="color:#d3d0c8;">Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub  
</span><span style="color:#d3d0c8;">Bus 002 Device 002: ID 0451:8440 Texas Instruments, Inc.    
</span><span style="color:#d3d0c8;">Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub  
</span><span style="color:#d3d0c8;">Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
</span></code></pre>
<p>我在一番搜索后，终于在 <code>lsusb -v</code> 的输出中看到了 Bluetooth 等字样归属于 003 这个 Device，成功拿到了 <code>idVendor</code> 和 <code>idProduct</code> 进行后续的配置。</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">Bus 003 Device 007: ID 8087:0033 Intel Corp.
</span><span style="color:#d3d0c8;">   ...
</span><span style="color:#d3d0c8;">   Interface Descriptor:  
</span><span style="color:#d3d0c8;">     bLength                 9  
</span><span style="color:#d3d0c8;">     bDescriptorType         4  
</span><span style="color:#d3d0c8;">     bInterfaceNumber        0  
</span><span style="color:#d3d0c8;">     bAlternateSetting       0  
</span><span style="color:#d3d0c8;">     bNumEndpoints           3  
</span><span style="color:#d3d0c8;">     bInterfaceClass       224 Wireless  
</span><span style="color:#d3d0c8;">     bInterfaceSubClass      1 Radio Frequency  
</span><span style="color:#d3d0c8;">     bInterfaceProtocol      1 Bluetooth
</span></code></pre>
]]>
        </content>
    </entry>
    
    <entry>
        <title>Fly.io 初体验之博客搬家</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/first-experience-with-fly-io-blog-migration"/>
        <published>2023-01-31T13:47:53Z</published>
        <updated>2025-03-07T17:29:18Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/first-experience-with-fly-io-blog-migration</id>
        <content type="html">
            <![CDATA[<p>之前博客一直部署在 Vultr 每个月 $5 的日本节点上，眼看下个月就又要余额归零了，再加上一直以来整个 VM 上都只跑了 Pomash 这一个程序，算是有点浪费，所以在考虑要不要拥抱一下新时代，找一个做这种小应用部署的 SaaS，把博客程序迁移上去。目前 Pomash 在虚拟机上的搭建方式也十分老手艺：Supervisor 做进程管理，Nginx 做转发。要是能一劳永逸，干掉这些我毕生所学的建站知识，那更是再好不过了。</p>
<p>可能有的朋友看到这里会这样问：为啥不直接找个博客托管平台？为啥不直接用静态博客？答案也很简单，Pomash 算是我初入编程殿堂的启蒙之作，这古老的 Python Tornado Web 框架和前后端不分离的架构，以及谈得上羞耻的代码质量都保留着我那一份青春的回忆。从 14 年的第一个 Commit 算起，到今天（2023-01-31）刚好是 Pomash 的 9 周年，慢慢更新到现在，它绝对不是最好用的博客系统，但一定是我最喜欢的。</p>
<p><figure><img src="https://webp.ipotato.me/20240819160518-afdc65310605113c629b77d762caeb1c.png" alt="The very first commit" /></figure></p>
<h1>Fly.io 是什么</h1>
<p><a href="https://fly.io">Fly.io</a> 其实是跟同事吃饭摆龙门阵的时候了解到的一个容器化部署平台，整个产品都透露出一股小而美的气质，其提供的服务也非常简单：帮助用户用容器化的方式部署应用。人话版本就是每个人都可以<del>讲 5 分钟脱口秀</del>通过写一个 Dockerfile 的工作量（有些情况下甚至连 Dockerfile 都可以不用准备）快速部署可访问的应用。官方文档上所称每个账号的<a href="https://fly.io/docs/about/pricing/#free-allowances">免费额度</a>如下：</p>
<ul>
<li>Up to 3 shared-cpu-1x 256mb VMs</li>
<li>3GB persistent volume storage (total)</li>
<li>160GB outbound data transfer</li>
</ul>
<p>对于我这个无人问津的博客来说，使用起来应该是绰绰有余了，故而直接开整。</p>
<h1>flyctl</h1>
<p>所有的部署运维操作都可以通过官方提供的命令行工具 flyctl 来完成，整个交互也极为简单，在完成 <code>fly auth login</code> 之后，即可开始部署应用了。 flyctl 的使用极为傻瓜，对于比较简单的项目，例如有 <code>main.go</code> 的 Go 项目，只需要调用 <code>flyctl launch</code>，它会扫描你的源代码结构，自动帮你生成 Dockerfile（其他语言的项目也类似），如果你只是用 Go 的标准库实现了一个简单跑在 8080 端口上的 HTTP 程序，基本上这一个命令一路 Y 过去就直接部署成功可以在浏览器里访问了。但是对于 Pomash 来说，它还需要一点额外的步骤，所以我选择自己准备一个 Dockerfile。</p>
<h1>准备 Dockerfile</h1>
<p>Pomash 是一个 Python Web 程序，运行起来很简单：<code>python3 run.py</code> 就完事了。不过不知道为什么，当年的我在实现的时候居然决定在博客跑起来前需要先手动生成 SQLite 的数据库文件，所以还得多来一步，再加上 pip 的依赖安装啥的，一点也不复杂的 Dockerfile 最后写出来长这样：</p>
<pre style="background-color:#2d2d2d;"><code class="language-dockerfile"><span style="color:#d3d0c8;"># syntax=docker/dockerfile:1
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">FROM python:3.8-slim-buster
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">WORKDIR /pomash_deployment
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">COPY requirements.txt requirements.txt
</span><span style="color:#d3d0c8;">RUN pip3 install -r requirements.txt
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">COPY . .
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">RUN python3 init_db.py
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">CMD [&quot;python3&quot;, &quot;run.py&quot; , &quot;--port=8080&quot;]
</span></code></pre>
<p>接下来的操作就很简单了，<code>flyctl deploy</code> 然后根据提示输入 Y or N 就可以完成部署，我的应用名设置的是 <code>pomash</code>，所以最后部署后的地址就是 <a href="https://pomash.fly.dev%E3%80%82">https://pomash.fly.dev。</a></p>
<h1>IPv4 地址分配</h1>
<p>最后一步就是域名绑定了。由于 IPv4 枯竭问题，fly.io 官方选择了省着分配 IPv4 地址，只要你的应用部署时使用了默认的 80 和 443 这两个 HTTP 端口，那么就不会分配到独占的 IPv4 地址，但是每部署一个应用 fly.io 都会为你分配一个独占的公网 IPv6 地址。虽然用 CNAME 记录的方式可以把自己的域名跳转到官方给的 URL 上来解决 IPv4 的访问问题，但毕竟相比于 A 记录有一定限制，所以为了让家里还没有 IPv6 的朋友能够打开我的博客，我们可以手动分配一个独占的 IPv4 地址：<code>flyctl ips allocate-v4</code>。需要注意的是每个账户都只有一个 Dedicated IPv4 的限额，如果你想拥有 2 个及以上的公网独占 IPv4 地址的话，就只能充钱了，价格是 $2 一个月。</p>
<p>完成 DNS 的设置后来到网页的 Dashboard 界面，手动添加对应域名后，fly.io 会通过 Let's Encrypt 自动帮你配置免费的 SSL 证书加密。</p>
<p><figure><img src="https://webp.ipotato.me/20240819160353-6fe1c3b07933443e15cec86a5f9d2227.png" alt="Dashboard" /></figure></p>
<p>当然，一切操作也都可以通过命令行完成，参考<a href="https://fly.io/docs/app-guides/custom-domains-with-fly/#configuring-certificates-before-accepting-traffic">官方文档</a>。</p>
<h1>写在最后</h1>
<p>整个从注册到最后部署成功的过程是比较丝滑的，几乎没有遇到任何问题，官方文档也写的十分详尽，基本上我遇到的所有问题都可以在内找到详细的解决办法（例如单应用内的多进程部署），可见 fly.io 是很懂面向用户群体痛点所在的。值得一提的是，最初同事给我讲到 fly.io 倒不是因为他们的产品，而是比较有趣的招聘方式。通过他们官网的<a href="https://fly.io/docs/hiring/hiring/">招聘流程介绍</a>，可以看到他们的“面试”过程很有趣，这里的面试打了引号是因为他们其实并没有面试这一步，而是通过做 2 到 3 个挑战题的方式第一阶段通关后直接加入他们的公司 Slack 和他们的工程师工作一天，一切顺利的话就会给你发 Offer。从这样一个细节来看，除去好用的产品外，这也真的是一家有趣的公司。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>TiKV Region Split 全流程分析</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/tikv-region-split-process-analysis"/>
        <published>2022-05-26T09:00:32Z</published>
        <updated>2025-03-07T17:29:39Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/tikv-region-split-process-analysis</id>
        <content type="html">
            <![CDATA[<p>分裂可以说是 Region 生命周期中最为重要的一步，如同细胞一般，分裂是 Region 被创造并持续增多的唯一方式。</p>
<p>本文将介绍以下内容：</p>
<ul>
<li>Region Split 是由谁触发的。</li>
<li>Region Split 是如何计算 Split Key 的。</li>
<li>Region Split 最终是如何执行的。</li>
</ul>
<p>我们先来看一个 Region Split 过程的大致流程：</p>
<ol>
<li>TiKV/PD/TiDB 触发 Region Split 事件。</li>
<li>Raftstore 处理 Region Split 事件，计算 Split Key。</li>
<li>Raftstore 执行 Split。</li>
</ol>
<h1>Region Split 的触发方式</h1>
<p>我们可以将 Region 的分裂从动机上分为两类：</p>
<ul>
<li>内部机制导致的 Region 被动分裂（例如 Region 的大小超过阈值，Load Base Split 被触发等）</li>
<li>人工手段对 Region 进行主动分裂（建表或手动 Split Region）</li>
</ul>
<h2>TiKV 触发分裂</h2>
<p>因为 Region 是 TiKV 的逻辑存储单元，Region 最基本的分裂方式也是来源于 TiKV 的控制。</p>
<h3>定期检查</h3>
<p>TiKV 默认会 10s 进行一次 Region 的分裂检查，此举由 Raft 状态机驱动，定期 Tick 进行触发。函数名称为 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L5005"><code>PeerFsmDelegate::on_split_region_check_tick</code></a>。</p>
<p>因为 Region Split 的行为后续会作为一条 Raft log 在副本间进行同步，所以该函数会首先检查当前 Region peer 是否为 leader，以避免进行无用的检查。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.</span><span style="color:#66cccc;">is_leader</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.may_skip_split_check
</span><span style="color:#d3d0c8;">    &amp;&amp; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.compaction_declined_bytes &lt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.cfg.</span><span style="color:#66cccc;">region_split_check_diff</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;">    &amp;&amp; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.size_diff_hint &lt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.cfg.</span><span style="color:#66cccc;">region_split_check_diff</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>紧接着 Leader check 之后，就是对 Split 必要性的检查，为了避免过多的 Split check，我们设置了以下 3 个条件来进行过滤：</p>
<ul>
<li>Region peer 的 <code>may_skip_split_check flag</code> 是否为 True</li>
<li>Region peer 的 <code>compaction_declined_bytes</code> 是否小于 <code>region-split-check-diff</code> 阈值</li>
<li>Region peer 的 <code>size_diff_hint</code> 是否小于 <code>region-split-check-diff</code> 阈值</li>
</ul>
<p><code>may_skip_split_check</code> 的 flag 会在必要时被设置为 False 来确保 Split 检查会尽可能地被执行（例如 TiKV 刚刚启动时）。<code>compaction_declined_bytes</code> 和 <code>size_diff_hint</code> 均是对 Region 大小变化的增量统计（分别统计自 Compaction 数据和 Apply 数据的过程），它们在此隐含了这样一个条件：只有 Region 的大小变化超过 <code>region-split-check-diff</code> 后才需要进行分裂检查（这个配置的默认值是 <code>region-split-size</code> 的 1/16，即 96 / 16 = 6 MB）。</p>
<p>而后就是一些特殊逻辑的检查，在此不进一步展开，他们包括：</p>
<ul>
<li>当前是否有堆积未完成的 Split 任务</li>
<li>当前是否处于 Lightning/BR 的导入过程中</li>
<li>当前是否正在生成 Snapshot</li>
</ul>
<p>需要注意此阶段的检查仅仅是触发了 Region Split 的事件，具体能否分裂以及如何分裂还取决于后续的 Split 触发过程。</p>
<h3>Load Base Split</h3>
<p>TiKV 还有一个会触发 Region Split 的功能来自于 <a href="https://docs.pingcap.com/zh/tidb/dev/configure-load-base-split">Load Base Split</a>。其核心代码位于 <a href="https://github.com/tikv/tikv/blob/b0f67e6128e4596367dba7b0400065b2496c65a3/components/raftstore/src/store/worker/split_controller.rs#L560"><code>AutoSplitController::flush</code></a>。StatsMonitor 会收集读请求的统计信息，包括请求的数目，请求读取的流量以及读取的 Key Range 等。对于 QPS 或 Byte 满足 <code>qps_threshold</code> 和 <code>byte_threshold</code> 的 Region，则会在之前收集的 Key Range 基础上对 Key 进行采样，选择一个切分后左右 Region 上的请求数量最为均衡的 Key 作为切分点进行切分。</p>
<h2>PD 触发分裂</h2>
<p>PD 也可以进行分裂的触发。此举可以通过以下方式进行：</p>
<ul>
<li>调用 /regions/split 的 HTTP API 触发</li>
<li>通过 pd-ctl 创建 Operator 触发</li>
<li>通过调用 gRPC 接口 <code>SplitRegions</code>/<code>SplitAndScatterRegions</code> 来触发</li>
</ul>
<p>其中，pd-ctl 作为主要面向用户的操作，方式如下：</p>
<pre style="background-color:#2d2d2d;"><code class="language-Shell"><span style="color:#d3d0c8;">&gt;&gt; operator add split-region 1 --policy=approximate     // 将 Region 1 对半拆分成两个 Region，基于粗略估计值
</span><span style="color:#d3d0c8;">&gt;&gt; operator add split-region 1 --policy=scan            // 将 Region 1 对半拆分成两个 Region，基于精确扫描值
</span></code></pre>
<p>上述操作的本质都是创建一个 Split 的 Operator 并下发给对应 Region。具体的 PD 侧代码可以通过 <a href="https://github.com/tikv/pd/blob/3792f0751dd9d5c76a03f7cca6243010e0d05e5f/server/schedule/region_splitter.go#L68"><code>RegionSplitter::SplitRegions</code></a> 函数进行自上而下的研究，在此不多做表述。</p>
<p>Operator 通过 Region 心跳下发给 TiKV 后，TiKV 会根据下发的 Split 任务类型去创建对应的事件，具体代码<a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/store/worker/pd.rs#L1434">在此</a>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> resp.</span><span style="color:#66cccc;">has_split_region</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> split_region = resp.</span><span style="color:#66cccc;">take_split_region</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    info!(&quot;</span><span style="color:#99cc99;">try to split</span><span style="color:#d3d0c8;">&quot;; &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; region_id, &quot;</span><span style="color:#99cc99;">region_epoch</span><span style="color:#d3d0c8;">&quot; =&gt; ?epoch);
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> msg = </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> split_region.</span><span style="color:#66cccc;">get_policy</span><span style="color:#d3d0c8;">() == pdpb::CheckPolicy::Usekey {
</span><span style="color:#d3d0c8;">        CasualMessage::SplitRegion {
</span><span style="color:#d3d0c8;">            region_epoch: epoch,
</span><span style="color:#d3d0c8;">            split_keys: split_region.</span><span style="color:#66cccc;">take_keys</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            callback: Callback::None,
</span><span style="color:#d3d0c8;">            source: &quot;</span><span style="color:#99cc99;">pd</span><span style="color:#d3d0c8;">&quot;.</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        CasualMessage::HalfSplitRegion {
</span><span style="color:#d3d0c8;">            region_epoch: epoch,
</span><span style="color:#d3d0c8;">            policy: split_region.</span><span style="color:#66cccc;">get_policy</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            source: &quot;</span><span style="color:#99cc99;">pd</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">            cb: Callback::None,
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    };
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Err(e) = router.</span><span style="color:#66cccc;">send</span><span style="color:#d3d0c8;">(region_id, PeerMsg::CasualMessage(msg)) {
</span><span style="color:#d3d0c8;">        error!(&quot;</span><span style="color:#99cc99;">send halfsplit request failed</span><span style="color:#d3d0c8;">&quot;; &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; region_id, &quot;</span><span style="color:#99cc99;">err</span><span style="color:#d3d0c8;">&quot; =&gt; ?e);
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到根据不同的 Split 方式，所创建的事件也不同——若是给定了分裂点 Key 则会直接下发 <code>CasualMessage::SplitRegion</code> 事件，否则根据不同的分裂策略创建一个 <code>CasualMessage::HalfSplitRegion</code> 事件，期以对 Region 进行对半分。这里的策略主要分为 Scan 和 Approximate 两类，具体的区别会在后文中进行介绍。</p>
<h2>TiDB 触发分裂</h2>
<h3>DDL</h3>
<p>在建表或添加分区时，TiDB 会在 DDL 阶段对表的 Region 进行预切分，为每个表或分区创建单独的 Region，用于避免发生大量建表和写入造成的热点问题。此举也是通过调用 PD 的 Split 接口达成的（早期版本是 TiDB 直接下发给 TiKV，现已废弃）。具体的代码入口在 <a href="https://github.com/pingcap/tidb/blob/15b8ddf7b8e29e7aab7671c4f58358a60e8a217a/ddl/ddl_api.go#L2516"><code>ddl::preSplitAndScatter</code></a> 接口，你可以通过该方法的调用情况来看不同的 Split Table 发生在何时何处。</p>
<h3>SQL</h3>
<p>除了建表时自动为每个表切分出的一个 Region，如果在单表内部存在写入热点，我们也可以通过 SQL 来手动 Split Region。这个原理其实和上述的 DDL 过程相同，均是调用统一的 <a href="https://github.com/pingcap/tidb/blob/d282940b4202a8fe82b7e3298ce62bcdb7d355d1/kv/kv.go#L493"><code>SplitRegions</code></a> 接口来进行 Split 任务的下发。</p>
<p>具体的 SQL 语法可以参考官方文档：<a href="https://docs.pingcap.com/zh/tidb/dev/sql-statement-split-region#split-region-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3">Split Region 使用文档</a>。</p>
<h2>其他</h2>
<p>上面只阐述了 3 大组件的常见 Region Split 触发流程，事实上还有很多其他机制会触发 Region Split，例如 Lightning/BR 这样的工具导入数据前也会对 Region 进行预切分和打散，以求导入后数据的均衡。tikv-ctl 也可以触发 Region 的 Split。</p>
<h1>Region Split Key 的计算方式</h1>
<p>以上述方式触发 Region Split 事件后，具体的 Split 的 Key 可以以多种方式和维度被计算出来。例如通过精确的 Scan 扫描来确定 Region 大小上的中点进行分裂，或通过指定的 Key 直接进行分裂等，不同的方式往往用于不同的场景，具体原理如下。</p>
<h2>Coprocessor</h2>
<p>此 Coprocessor 非 TiKV 中用于下推 SQL 执行的 Coprocessor，而是 raftstore 代码中的一个概念。其主要作用相当于外挂在 TiKV 的 Raft 层上的一个协处理工具集合，用于观测和处理与 Raft 相关的周边事件。<a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/mod.rs#L120"><code>SplitChecker</code></a> 就是其中之一，用于接受，处理和下发与 Region Split 有关的事件。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#747369;">/// SplitChecker is invoked during a split check scan, and decides to use
</span><span style="color:#747369;">/// which keys to split a region.
</span><span style="color:#cc99cc;">pub trait </span><span style="color:#d3d0c8;">SplitChecker&lt;E&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">/// Hook to call for every kv scanned during split.
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">///
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">/// Return true to abort scan early.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;, _: &amp;KeyEntry) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#f99157;">false
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">/// Get the desired split keys.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -&gt; Vec&lt;Vec&lt;</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">&gt;&gt;;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">/// Get approximate split keys without scan.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">approximate_split_keys</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &amp;Region, _: &amp;E) -&gt; Result&lt;Vec&lt;Vec&lt;</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">&gt;&gt;&gt; {
</span><span style="color:#d3d0c8;">        Ok(vec![])
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">/// Get split policy.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">policy</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -&gt; CheckPolicy;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>一个 <code>SplitChecker</code> 包含 4 个方法，分别是：</p>
<ul>
<li><code>on_kv</code>，在使用 Scan 方式时，用于在 Iterator 扫描 Key 的过程中接受 Key，并在内部维护对应的状态来实现不同的分裂方式。</li>
<li><code>split_keys</code>，在完成扫描后通过此方法来拿到最终的 Split Key 结果。</li>
<li><code>approximate_split_keys</code>，在使用 Approximate 方式时，不进行 Scan 而直接拿到 Split Key 结果</li>
<li><code>policy</code>，返回当前的 Split 检查策略，有 Scan/Approximate 两种方式。</li>
</ul>
<p>对这 4 个方法不同的实现也就决定了不同的分裂方式，下面我们分别介绍 TiKV 内部支持的所有不同的分裂方式。</p>
<h2>Half</h2>
<p><a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/split_check/half.rs"><code>HalfCheckObserver</code></a> 实现了对 Region 的 Sizie 对半切策略，在 Scan 模式下，为了找到一个 Region 内 Size 维度上的中点，把所有的 Key 都记录下来显然是不合理的，这样可能会占用大量的内存。取而代之的方式是根据配置计算出一个最小的 Size 单位 n MB，计算函数名为 <a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/coprocessor/split_check/half.rs#L99"><code>half_split_bucket_size</code></a> 通过将 <code>region_max_size</code> 除以 <code>BUCKET_NUMBER_LIMIT</code>（常量，值为 1024），计算出一个 Bucket 大小，最小为 1 MB，最大为 512 MB。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">half_split_bucket_size</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">region_max_size</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">) -&gt; </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> half_split_bucket_size = region_max_size / </span><span style="color:#f99157;">BUCKET_NUMBER_LIMIT </span><span style="color:#d3d0c8;">as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> bucket_size_limit = ReadableSize::mb(</span><span style="color:#f99157;">BUCKET_SIZE_LIMIT_MB</span><span style="color:#d3d0c8;">).</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> half_split_bucket_size == </span><span style="color:#f99157;">0 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        half_split_bucket_size = </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else if</span><span style="color:#d3d0c8;"> half_split_bucket_size &gt; bucket_size_limit {
</span><span style="color:#d3d0c8;">        half_split_bucket_size = bucket_size_limit;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    half_split_bucket_size
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>在后续的扫描过程中，仅在每扫描过 n MB 大小后才记录下当前的 Key，这样可以通过牺牲一定的精度换来了较少的内存占用。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &amp;KeyEntry) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">is_empty</span><span style="color:#d3d0c8;">() || </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size &gt;= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.each_bucket_size {
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size = </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.cur_bucket_size += entry.</span><span style="color:#66cccc;">entry_size</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    </span><span style="color:#f99157;">false
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -&gt; Vec&lt;Vec&lt;</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">&gt;&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> mid = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() / </span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> mid == </span><span style="color:#f99157;">0 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        vec![]
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> data_key = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.buckets.</span><span style="color:#66cccc;">swap_remove</span><span style="color:#d3d0c8;">(mid);
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> key = keys::origin_key(&amp;data_key).</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">        vec![key]
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>在后续计算中点 Key 的过程中，也只需要取我们收集到的 Key 的中间元素，即可获得近似的 Region Size 中点，用于后续的切分。</p>
<p>对于具体 <code>approximate_split_keys</code> 的实现取决于不同的 KV Engine，以默认的 RocksDB 为例，为了避免对整个区间上全 Key-Value 的扫描，我们使用了 RocksDB 的 <a href="https://tikv.github.io/deep-dive-tikv/key-value-engine/rocksdb.html#tableproperties">TableProperties</a> 特性，来在 RocksDB 构建每个 SST 文件的时候就提前收集一些 Key 相关的信息，从而可以在此时避免进行 I/O 操作即可获得近似的 Key Range 上的 Key 信息，再辅之以采样等手段，相较于 Scan 策略会更不精准，但省去了不少资源。对应的代码在 <a href="https://github.com/tikv/tikv/blob/aaf47d0cbb62518a760f3a3deb43acc9e319595e/components/engine_rocks/src/range_properties.rs#L167"><code>RocksEngine::get_range_approximate_split_keys_cf</code></a> 方法中。</p>
<h2>Size</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/size.rs"><code>SizeCheckObserver</code></a> 实现了根据 Region Size 切分 Region 的策略。其逻辑相对简单，在默认配置下，会对 Region 的 KV 进行 Scan 遍历，每扫描过 96 MB 的数据便会记录下当前的 Key，一次最多记录 10 个。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &amp;KeyEntry) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> size = entry.</span><span style="color:#66cccc;">entry_size</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size += size;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> over_limit = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">&gt;= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.batch_split_limit;
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size &gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size &amp;&amp; !over_limit {
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(keys::origin_key(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">()).</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">        </span><span style="color:#747369;">// if for previous on_kv() self.current_size == self.split_size,
</span><span style="color:#d3d0c8;">        </span><span style="color:#747369;">// the split key would be pushed this time, but the entry size for this time should not be ignored.
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size = </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size - size == </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size {
</span><span style="color:#d3d0c8;">            size
</span><span style="color:#d3d0c8;">        } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">            </span><span style="color:#f99157;">0
</span><span style="color:#d3d0c8;">        };
</span><span style="color:#d3d0c8;">        over_limit = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">() as </span><span style="color:#cc99cc;">u64 </span><span style="color:#d3d0c8;">&gt;= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.batch_split_limit;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// For a large region, scan over the range maybe cost too much time,
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// so limit the number of produced split_key for one batch.
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Also need to scan over self.max_size for last part.
</span><span style="color:#d3d0c8;">    over_limit &amp;&amp; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size + </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size &gt;= </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.max_size
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">split_keys</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">) -&gt; Vec&lt;Vec&lt;</span><span style="color:#cc99cc;">u8</span><span style="color:#d3d0c8;">&gt;&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// make sure not to split when less than max_size for last part
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.current_size + </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_size &lt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.max_size {
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">pop</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys.</span><span style="color:#66cccc;">is_empty</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">        std::mem::take(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_keys)
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        vec![]
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p><code>approximate_split_keys</code> 的实现和 Half 类似，在此不表，依然是基于 RocksDB 的 TableProperties 功能。</p>
<h2>Keys</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/keys.rs"><code>KeysCheckObserver</code></a> 实现了根据 Region Key 数量切分 Region 的策略，其原理和 <code>SizeCheckObserver</code> 相同，只不过把计算方式改成了 Key 数量的统计，在此不过多展开，</p>
<h2>Tabel</h2>
<p><a href="https://github.com/tikv/tikv/blob/3f698eb1b7e0ed94547fa8ecd1ef68e061ba5d39/components/raftstore/src/coprocessor/split_check/table.rs"><code>TableCheckObserver</code></a> 实现了根据 Region 范围内 Key 所属的 Table 进行切分的策略。这个 Checker 的实现比较特殊，它在 TiKV 内部引入了 SQL 层的概念。原理也比较简单，在 Scan 时去 Decode 每个 Key，检查其所属的表 ID 和之前 Key 是否相同，若不同则加入 Split Key 进行分裂。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#747369;">/// Feed keys in order to find the split key.
</span><span style="color:#747369;">/// If `current_data_key` does not belong to `status.first_encoded_table_prefix`.
</span><span style="color:#747369;">/// it returns the encoded table prefix of `current_data_key`.
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">on_kv</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, _: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;, </span><span style="color:#f2777a;">entry</span><span style="color:#d3d0c8;">: &amp;KeyEntry) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> current_encoded_key = keys::origin_key(entry.</span><span style="color:#66cccc;">key</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> split_key = </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.first_encoded_table_prefix.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!</span><span style="color:#66cccc;">is_same_table</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">            </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.first_encoded_table_prefix.</span><span style="color:#66cccc;">as_ref</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">unwrap</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            current_encoded_key,
</span><span style="color:#d3d0c8;">        ) {
</span><span style="color:#d3d0c8;">            </span><span style="color:#747369;">// Different tables.
</span><span style="color:#d3d0c8;">            Some(current_encoded_key)
</span><span style="color:#d3d0c8;">        } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">            None
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else if </span><span style="color:#66cccc;">is_table_key</span><span style="color:#d3d0c8;">(current_encoded_key) {
</span><span style="color:#d3d0c8;">        </span><span style="color:#747369;">// Now we meet the very first table key of this region.
</span><span style="color:#d3d0c8;">        Some(current_encoded_key)
</span><span style="color:#d3d0c8;">    } </span><span style="color:#cc99cc;">else </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        None
</span><span style="color:#d3d0c8;">    };
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key = split_key.</span><span style="color:#66cccc;">and_then</span><span style="color:#d3d0c8;">(to_encoded_table_prefix);
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.split_key.</span><span style="color:#66cccc;">is_some</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>由于工作原理决定了它只能基于 Scan 策略进行工作，所以没有提供 <code>approximate_split_keys</code> 方法的实现。</p>
<h2>优先级</h2>
<p>上面一共介绍了 TiKV 支持的 4 种 Split 方式，那么具体工作过程中，实际到底哪一个方式会被触发呢？答案是都有可能。</p>
<p>每个 SplitChecker 都会被加入到一个 SplitCheckerHost 中，并被赋予不同的优先级，每次 Split 都会依次“询问”每个 SplitChecker 的“意见”，如果高优先级的 Checker 不能给出 Split Key 那么就依次向更低优先级的 Checker 轮训，直到得到一个 Split Key 或确认无法 Split。优先级在将 SplitChecker 注册到 Coprocessor 时就被定义好了，代码位于 <a href="https://github.com/tikv/tikv/blob/deb614531835a3820e55d85aad7ced22b0d9b70e/components/raftstore/src/coprocessor/dispatcher.rs#L324"><code>CoprocessorHost::new</code></a>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">pub fn </span><span style="color:#6699cc;">new</span><span style="color:#d3d0c8;">&lt;C: CasualRouter&lt;E&gt; + Clone + Send + </span><span style="color:#cc99cc;">&#39;static</span><span style="color:#d3d0c8;">&gt;(
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">ch</span><span style="color:#d3d0c8;">: C,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">cfg</span><span style="color:#d3d0c8;">: Config,
</span><span style="color:#d3d0c8;">) -&gt; CoprocessorHost&lt;E&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> registry = Registry::default();
</span><span style="color:#d3d0c8;">    registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">        </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">        BoxSplitCheckObserver::new(SizeCheckObserver::new(ch.</span><span style="color:#66cccc;">clone</span><span style="color:#d3d0c8;">())),
</span><span style="color:#d3d0c8;">    );
</span><span style="color:#d3d0c8;">    registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">        </span><span style="color:#f99157;">200</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">        BoxSplitCheckObserver::new(KeysCheckObserver::new(ch)),
</span><span style="color:#d3d0c8;">    );
</span><span style="color:#d3d0c8;">    registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">100</span><span style="color:#d3d0c8;">, BoxSplitCheckObserver::new(HalfCheckObserver));
</span><span style="color:#d3d0c8;">    registry.</span><span style="color:#66cccc;">register_split_check_observer</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">        </span><span style="color:#f99157;">400</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">        BoxSplitCheckObserver::new(TableCheckObserver::default()),
</span><span style="color:#d3d0c8;">    );
</span><span style="color:#d3d0c8;">    CoprocessorHost { registry, cfg }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到 <code>HalfCheckObserver</code> 有最高优先级，其次是 <code>SizeCheckObserver</code> 和 <code>KeysCheckObserver</code>，<code>TableCheckObserver</code> 最低。但是我们所见到的大多数 Region 分裂都是基于 Size 的，Half 分裂尽管有最高优先级，为什么不会被频繁触发呢？答案是我们每次基于注册在 Coprocessor 的 Split Checker 创建 <code>SplitCheckerHost</code> 时（代码入口在 <a href="https://github.com/tikv/tikv/blob/deb614531835a3820e55d85aad7ced22b0d9b70e/components/raftstore/src/coprocessor/dispatcher.rs#L434"><code>CoprocessorHost::new_split_checker_host</code></a>），并不会将所有的 Checker 都导入，而是根据不同的配置以及场景进行有选择的添加。例如只有 <code>auto_split</code> 选项设置为关闭时，<code>HalfCheckObserver</code> 才会被添加到 Host 中，这个选项在 TiKV 定时检查触发 Split 时会开启，所以在对应场景下 <code>HalfCheckObserver</code> 不会起作用。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">derive</span><span style="color:#d3d0c8;">(Clone)]
</span><span style="color:#cc99cc;">pub struct </span><span style="color:#d3d0c8;">HalfCheckObserver;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Coprocessor </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">HalfCheckObserver {}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;">&lt;E&gt; SplitCheckObserver&lt;E&gt; </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">HalfCheckObserver
</span><span style="color:#cc99cc;">where
</span><span style="color:#d3d0c8;">    E: KvEngine,
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">add_checker</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">        &amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">        _: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">host</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">Host&lt;&#39;_, E&gt;,
</span><span style="color:#d3d0c8;">        _: &amp;E,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">policy</span><span style="color:#d3d0c8;">: CheckPolicy,
</span><span style="color:#d3d0c8;">    ) {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> host.</span><span style="color:#66cccc;">auto_split</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">        host.</span><span style="color:#66cccc;">add_checker</span><span style="color:#d3d0c8;">(Box::new(Checker::new(
</span><span style="color:#d3d0c8;">            </span><span style="color:#66cccc;">half_split_bucket_size</span><span style="color:#d3d0c8;">(host.cfg.</span><span style="color:#66cccc;">region_max_size</span><span style="color:#d3d0c8;">().</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">),
</span><span style="color:#d3d0c8;">            policy,
</span><span style="color:#d3d0c8;">        )))
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>再例如只有当 <code>split_region_on_table</code> 配置开启时，<code>TableCheckObserver</code> 才会被添加到 Host 中，该配置默认关闭。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">derive</span><span style="color:#d3d0c8;">(Default, Clone)]
</span><span style="color:#cc99cc;">pub struct </span><span style="color:#d3d0c8;">TableCheckObserver;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Coprocessor </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">TableCheckObserver {}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;">&lt;E&gt; SplitCheckObserver&lt;E&gt; </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">TableCheckObserver
</span><span style="color:#cc99cc;">where
</span><span style="color:#d3d0c8;">    E: KvEngine,
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">add_checker</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">        &amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">ctx</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">ObserverContext&lt;&#39;_&gt;,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">host</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">Host&lt;&#39;_, E&gt;,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">engine</span><span style="color:#d3d0c8;">: &amp;E,
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">policy</span><span style="color:#d3d0c8;">: CheckPolicy,
</span><span style="color:#d3d0c8;">    ) {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if </span><span style="color:#d3d0c8;">!host.cfg.split_region_on_table {
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">        ...
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>所以说在大多数情况下，只有 <code>KeysCheckObserver</code> 和 <code>SizeCheckObserver</code> 主导 Region 的分裂方式。</p>
<h1>Region Split 的执行过程</h1>
<p>通过 Raftstore 的 Coprocessor 确定好 Region 的 Split Key 后，最后就来到了 Split 的执行阶段。Region 的 Split 任务会被下发到具体的 Region，继而触发 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L5075"><code>PeerFsmDelegate::on_prepare_split_region</code></a> 函数，正式开启 Region 的 Split 执行。</p>
<h2>Pre-check</h2>
<p>首先 TiKV 会再次确认当前 Region 为 leader，并检查 Epoch 等属性是否发生了变化，Epoch 内的 Version 属性只有在完成 Split 或 Merge 的情况下才会增加，因为 Version 一定是严格单调递增的，所以 PD 使用了这个规则去判断范围重叠的不同 Region 的新旧。在检查通过后，便向 PD 发送 <a href="https://github.com/tikv/tikv/blob/2ffb7cf01ae75afde9261fb9154304ec1676ad77/components/raftstore/src/store/worker/pd.rs#L970">AskBatchSplit</a> 请求为即将分裂出来的新 Region 获取 Region ID，并触发 Raft 开始进行 Split log 的 Proposal。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;">info!(
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">try to batch split region</span><span style="color:#d3d0c8;">&quot;;
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">new_region_ids</span><span style="color:#d3d0c8;">&quot; =&gt; ?resp.</span><span style="color:#66cccc;">get_ids</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">region</span><span style="color:#d3d0c8;">&quot; =&gt; ?region,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">task</span><span style="color:#d3d0c8;">&quot; =&gt; task,
</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> req = </span><span style="color:#66cccc;">new_batch_split_region_request</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">    split_keys,
</span><span style="color:#d3d0c8;">    resp.</span><span style="color:#66cccc;">take_ids</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">into</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">    right_derive,
</span><span style="color:#d3d0c8;">);
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> region_id = region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">();
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> epoch = region.</span><span style="color:#66cccc;">take_region_epoch</span><span style="color:#d3d0c8;">();
</span><span style="color:#66cccc;">send_admin_request</span><span style="color:#d3d0c8;">(
</span><span style="color:#d3d0c8;">    &amp;router,
</span><span style="color:#d3d0c8;">    region_id,
</span><span style="color:#d3d0c8;">    epoch,
</span><span style="color:#d3d0c8;">    peer,
</span><span style="color:#d3d0c8;">    req,
</span><span style="color:#d3d0c8;">    callback,
</span><span style="color:#d3d0c8;">    Default::default(),
</span><span style="color:#d3d0c8;">);
</span></code></pre>
<h2>Raft Proposal &amp; Apply</h2>
<p>通过 Raft log 将 Split 同步到各个 Peer 之上完成 Commit 之后，<a href="https://github.com/tikv/tikv/blob/b0f67e6128e4596367dba7b0400065b2496c65a3/components/raftstore/src/store/fsm/apply.rs#L2191"><code>ApplyDelegate::exec_batch_split</code></a> 便开始执行 Region 的分裂。创建新 Region，更改 Region 边界，并将 Region 的新信息写入落盘。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#d3d0c8;"> </span><span style="color:#cc99cc;">for</span><span style="color:#d3d0c8;"> new_region in &amp;regions {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">() == derived.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">continue</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> new_split_peer = new_split_regions.</span><span style="color:#66cccc;">get</span><span style="color:#d3d0c8;">(&amp;new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">()).</span><span style="color:#66cccc;">unwrap</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Some(</span><span style="color:#cc99cc;">ref</span><span style="color:#d3d0c8;"> r) = new_split_peer.result {
</span><span style="color:#d3d0c8;">        warn!(
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">new region from splitting already exists</span><span style="color:#d3d0c8;">&quot;;
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">new_region_id</span><span style="color:#d3d0c8;">&quot; =&gt; new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">new_peer_id</span><span style="color:#d3d0c8;">&quot; =&gt; new_split_peer.peer_id,
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">reason</span><span style="color:#d3d0c8;">&quot; =&gt; r,
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">        );
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">continue</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#66cccc;">write_peer_state</span><span style="color:#d3d0c8;">(kv_wb_mut, new_region, PeerState::Normal, None)
</span><span style="color:#d3d0c8;">        .</span><span style="color:#66cccc;">and_then</span><span style="color:#d3d0c8;">(|_| </span><span style="color:#66cccc;">write_initial_apply_state</span><span style="color:#d3d0c8;">(kv_wb_mut, new_region.</span><span style="color:#66cccc;">get_id</span><span style="color:#d3d0c8;">()))
</span><span style="color:#d3d0c8;">        .</span><span style="color:#66cccc;">unwrap_or_else</span><span style="color:#d3d0c8;">(|</span><span style="color:#f2777a;">e</span><span style="color:#d3d0c8;">| {
</span><span style="color:#d3d0c8;">            panic!(
</span><span style="color:#d3d0c8;">                &quot;</span><span style="color:#99cc99;">{} fails to save split region {:?}: {:?}</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.tag, new_region, e
</span><span style="color:#d3d0c8;">            )
</span><span style="color:#d3d0c8;">        });
</span><span style="color:#d3d0c8;">}
</span><span style="color:#66cccc;">write_peer_state</span><span style="color:#d3d0c8;">(kv_wb_mut, &amp;derived, PeerState::Normal, None).</span><span style="color:#66cccc;">unwrap_or_else</span><span style="color:#d3d0c8;">(|</span><span style="color:#f2777a;">e</span><span style="color:#d3d0c8;">| {
</span><span style="color:#d3d0c8;">    panic!(&quot;</span><span style="color:#99cc99;">{} fails to update region {:?}: {:?}</span><span style="color:#d3d0c8;">&quot;, </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.tag, derived, e)
</span><span style="color:#d3d0c8;">});
</span></code></pre>
<p>在默认的分裂方式下，原 Region 要分裂到右侧，举例而言，假设分裂前的 Region 数量一共有 2 个，ID 分别为 1 和 2。2 是即将要分裂的 Region，且 Split Key 为 &quot;b&quot;。</p>
<p>Region 1 [&quot;&quot;, &quot;a&quot;), Region 2 [&quot;a&quot;, &quot;&quot;)</p>
<p>分裂后的新 Region 被分配了 ID 3，那么分裂后的 Region 会形如：</p>
<p>Region 1 [&quot;&quot;, &quot;a&quot;), Region 3 [&quot;a&quot;, &quot;b&quot;), Region 2 [&quot;b&quot;, &quot;&quot;)</p>
<p>在 TiKV 完成 Split log 的 Apply 后，会通过 ApplyResult::Res 事件触发 <a href="https://github.com/tikv/tikv/blob/1fb8980ccab9ff40c1adc206df52952dab8e8ad8/components/raftstore/src/store/fsm/peer.rs#L3519"><code>PeerFsmDelegate::on_ready_split_region</code></a> 来完成 Split 的预后工作。如果当前 Region 是 leader，则会给 PD 发送一个 Report(Batch)Split 的 RPC 请求，仅供 PD 打个日志记录，方便我们在查问题时通过 PD 的日志看到各个 Region 的 Split 记录。由于 Region 的 ID 分配也是严格保证单调递增，所以我们可以说 Region ID 越大的 Region 则越新。</p>
<pre style="background-color:#2d2d2d;"><code class="language-Rust"><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> is_leader {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.approximate_size = estimated_size;
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.approximate_keys = estimated_keys;
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.peer.</span><span style="color:#66cccc;">heartbeat_pd</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx);
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Notify pd immediately to let it update the region meta.
</span><span style="color:#d3d0c8;">    info!(
</span><span style="color:#d3d0c8;">        &quot;</span><span style="color:#99cc99;">notify pd with split</span><span style="color:#d3d0c8;">&quot;;
</span><span style="color:#d3d0c8;">        &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">        &quot;</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">peer_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">        &quot;</span><span style="color:#99cc99;">split_count</span><span style="color:#d3d0c8;">&quot; =&gt; regions.</span><span style="color:#66cccc;">len</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">    );
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Now pd only uses ReportBatchSplit for history operation show,
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// so we send it independently here.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> task = PdTask::ReportBatchSplit {
</span><span style="color:#d3d0c8;">        regions: regions.</span><span style="color:#66cccc;">to_vec</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">    };
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Err(e) = </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.ctx.pd_scheduler.</span><span style="color:#66cccc;">schedule</span><span style="color:#d3d0c8;">(task) {
</span><span style="color:#d3d0c8;">        error!(
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">failed to notify pd</span><span style="color:#d3d0c8;">&quot;;
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">region_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">region_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">peer_id</span><span style="color:#d3d0c8;">&quot; =&gt; </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.fsm.</span><span style="color:#66cccc;">peer_id</span><span style="color:#d3d0c8;">(),
</span><span style="color:#d3d0c8;">            &quot;</span><span style="color:#99cc99;">err</span><span style="color:#d3d0c8;">&quot; =&gt; %e,
</span><span style="color:#d3d0c8;">        );
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>其余则是一些向 PD 上报心跳，统计信息的初始化工作，更新分裂后的 Region epoch 并在 Raft group 中注册 Region 的路由。这些工作完成后，当前 TiKV 上的 Region 可以说是已经完成分裂了。</p>
<h2>Raft Election</h2>
<p>对于分裂前的原 Region 是 Leader 的 Peer 来说，分裂后的 Region 是可以立马发起选举的，而对于原 Region 非 Leader 的 Peer 来说，它分裂创建出的新 Region 是不能立马发起选举的，而是需要等待一个 Raft 的选举超时时间。这样实现的原因是存在下列的 Case：</p>
<ol>
<li>假设有一个 3 副本的 Region</li>
<li>Split 的 Log 已经复制到了所有的 Follower 上</li>
<li>所有的 Follower 完成了 Region Split Log 的 Apply，完成了分裂</li>
<li>Region 的 Leader 还没有开始或完成分裂</li>
</ol>
<p>如果允许原 Peer 非 Leader 的新 Region 分裂出来后立马开始选举，则会出现同一个数据范围内存在两个 Region leader 对外提供服务，一个是分裂后的新的更小的 Region leader，一个是尚未分裂的原 Region leader（Lease 尚未过期），这样一来就存在破坏线性一致性的可能。由于一次 Raft 的选举超时时间要大于 Leader 的 Lease 时间，所以只要我们保证以下两点：</p>
<ol>
<li>完成分裂的 Region 等待一个 Raft 的选举超时时间再开始选举</li>
<li>需要 Split 的 Region 不再续约 Lease</li>
</ol>
<p>所以当新分裂的 Region 开始选举时，旧的 Region leader 早些时候一定会因为发现自身的 Epoch 与其余两个 Follower 不同而选举失败完成退选。</p>
<h1>踩坑经验</h1>
<h2>Split Key 的格式为 Encoded Key without TS</h2>
<p>在 TiDB 和 TiKV 的语境下，当我们说到 Key 编码时，它可能指的是以下几种情况：</p>
<ul>
<li>Raw Key</li>
<li>Encoded Key without TS</li>
<li>Encoded Key with TS</li>
</ul>
<p>TiDB 在发送请求时使用的是 Raw Key，也即不带任何与 MVCC 相关的信息，也没有 Padding，只包括诸如 TableID，RowID 等基本信息。</p>
<p>TiKV 的 Raftstore 以及 PD 在处理诸如 Region 边界，Split 等 Key 时使用的是 Encoded Key without TS，它在 Raw Key 的基础上进行了 Encode，添加了用于保持字典序的 Padding，但由于此层尚未涉及到具体的事务，所以并没有 TS 参与其中。</p>
<p>TiKV 在实际读写底层 RocksDB 数据时，会将请求的 TS 一并 Encode 到 Key 里来区分 MVCC 信息，所以这一层使用的是 Encoded Key with TS。</p>
<p>Region Split 发生在 Raftstore 这一层，所以其格式均为 Encoded Key without TS，在开发相关功能时，要注意对 Key 进行 Encode，并且剔除 TS 信息，以免出现一些预期外的行为。</p>
<h1>参考</h1>
<ul>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-20">TiKV 源码解析系列文章（二十）Region Split 源码解析</a></li>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-19">TiKV 源码解析系列文章(十九) read index 和 local read 情景分析 | PingCAP</a></li>
<li><a href="https://pingcap.com/zh/blog/tikv-source-code-reading-18">TiKV 源码解析系列文章(十八) Raft Propose 的 Commit 和 Apply 情景分析 | PingCAP</a></li>
<li><a href="https://tikv.github.io/deep-dive-tikv/overview/introduction.html">Deep Dive TiKV</a></li>
<li><a href="https://pingcap.feishu.cn/wiki/wikcnkZoy41zHBJy4HTecCf7jig">Region 的一生</a></li>
<li><a href="https://longfangsong.github.io/tipedia/zh/what/Memory%20Comparable%20Encoding.html">Tipedia • Memory Comparable Encoding</a></li>
</ul>
]]>
        </content>
    </entry>
    
    <entry>
        <title>韩寒的四海</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/only-fools-rush-in"/>
        <published>2022-02-04T06:23:10Z</published>
        <updated>2025-03-07T17:30:28Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/only-fools-rush-in</id>
        <content type="html">
            <![CDATA[<p>2022 年春节档，要死不活了大半年后，院线终于出现了一些可以让电影院热闹起来的电影，这里面我最想看的是《四海》。</p>
<p>第一次接触导演身份的韩寒，其实已经是他第三部院线作品《飞驰人生》了，但我猜这可能是这四部里面最不韩寒的一部作品。看完《四海》以后我又补看了《后会无期》，两部拥有极其相似内核的作品在某种程度上证实了我的猜测，飞驰人生确实是一部「例外」（尽管我还没看《乘风破浪》）。</p>
<p>以小岛青年的出走作为开端，讲得到和失去的故事。浩汉和仁耀两个角色的路线是相同的，他们都在故事的某个阶段拥有了一些东西，紧接着便开始失去，浩汉失去了他的父亲（某种意义上来说，失去了 2 次）、失去了自己的爱情（这里是「有情人终成兄妹」的恶趣味），最后失去了自己的车子，以及结尾留白处，可能也失去了的生命。</p>
<p>比起浩汉，仁耀的失去没有那么隐晦，他失去了自己的朋友，失去了赛车，失去了爱人。结尾珠江上的一跃让我很震撼，《飞驰人生》结尾的一跃也让我很震撼，但这两者的跃是不同的。后者的跃，是摆脱了重力的跃，它是角色的自我救赎，是向上的；而前者的跃，是一种注定无法离地的跃，它是角色的坠落，是向下的。伴随着死亡，江水和竹蜻蜓的蒙太奇，背景音乐《无法离地的飞行》让我相信至少在那个镜头，我触摸到了韩寒。</p>
<p>《四海》是掺杂了商业包装的韩寒。比起《后会无期》里更纯粹的裸核，角色直言不讳的说出韩式金句和鸡汤，《四海》用很多商业片的元素取代了这些特点：沈腾的喜感、只出现了半段的赛车飙车、小镇青年在大城市的碰撞......这些是大多数观众喜闻乐见的韩寒。而还有很多观众不太能注意到的韩寒。</p>
<blockquote>
<p>欢颂说：「我讨厌水，水可怕。」</p>
<p>阿耀说：「水有什么可怕的，火才可怕。」</p>
</blockquote>
<p>最后一个人沉入水底，一个人冲进了火焰。</p>
<p>结尾的蒙太奇中，阿耀在隧道里说出过的愿望又重新出现，愿望变成了梦境：在布满乱石的海雾中抓螃蟹，如果不是跌倒了，我们谁也不搀扶谁。这份愿望和广州之行的应照，颇有一种「预言的自我实现」的宿命感。</p>
<blockquote>
<p>总有一扇门，你打不开。总有一条河，你越不过。</p>
</blockquote>
<p>韩寒的主人公，也总是不自觉地让我想起村上春树。毫无世俗气息的那种气质，离开家乡，离开小岛，前往未知地冒险，从祖国最东边来到最西边，从小渔村来到大城市，这种出世感总是围绕着两个人的作品，个体感情的无常才是主旋律。而《四海》的后半部分，在描述两人在大城市求生存时，有那么一部分「入世」的剧情，出世的韩寒显然不擅长拍这描述「主人公和现代社会的正面冲突」的部分，这也是影片节奏失衡最明显的一个段落。不过这部分拙劣反而让我略微安心，这让我确信他所擅长和想要表达的并不在此。</p>
<p>豆瓣 5.6 分的结果显然是低了，但并不意外，韩寒只是在春节档带来了一部不适合在春节档观看的电影。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>浅谈《开端》在剧作上的瑕疵</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/flaws-in-the-scriptwriting-of-reset"/>
        <published>2022-01-26T03:07:17Z</published>
        <updated>2025-03-07T17:31:32Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/flaws-in-the-scriptwriting-of-reset</id>
        <content type="html">
            <![CDATA[<p>在开始之前，我想先念叨几句。</p>
<p>《开端》这部剧其实我很早就关注了，起因是我女朋友是白敬亭的粉丝，所以对他的动向一直有所关注，去年底看到豆瓣条目，彼时的《开端》还是 20 集的长度。后来随着杀青和一系列制作的完成，最终成片为 15 集，单集片长 40 分钟。最初看到这个剧集长度我其实是满意的，毕竟从剧情概要上看也是所谓的“源代码”式国产“无限流”，难免会有场景时空重复，缩短剧集长度其实是一个明智之举，但这也对剧本，对导演来说是一个考验。</p>
<p>对于拍摄剧集，甚至说拍摄电影和拍摄短片来说，叙事节奏其实是比剧情本身更重要，更为考验导演功底的存在，小学语文老师针对写作文常说的一句话就是“详略得当”，如何把控好故事的节奏，在视听语言上做到“详略得当”是影视作品最后的成品能否做到有质感的第一要义。</p>
<p>其次便是人物，事件需要人物的参与和推动。舞台成型，作者退居幕后，角色来到台前。此时故事成品能否做到有质感的第二要义是故事能否仅由角色在舞台框架下的所思所想和所言所行来向前发展，而无需作者从幕后“现形”进行干预。后者在现实中的例子可以是各种剧作上巧合的引入，或是突然抛出的未知剧情，亦或是“机械降神”等强烈出格的手段。</p>
<p>在做好了这两点基础，也即节奏和人物之后，能进一步提升作品质感的，便是冲突的设置。冲突是影视作品中非常核心的一个概念，当一群角色因为不同的理念利益聚集到一起时，冲突的产生，爆发以及解决便是剧情与情感的多个释放点，能否牵动观众，打动观众甚至冒犯观众都会基于此而来。录像带之于《隐秘的角落》，江阳的死之于《沉默的真相》，爆炸案之于《开端》都是剧情的核心冲突点，其他冲突的发展与变化往往都来自于它们。能否处理好，引爆好或是解决这些冲突，也是剧作的功力所在。</p>
<p>那么《开端》在剧作上有些什么瑕疵呢？首先是叙事节奏，前十集把大量的篇幅花费在了男女主通过循环反复尝试和试探凶手上——剧情上这是合理的，但对于上帝视角的观众来说，其中许多情节其实是显而易见的，例如二次元小哥和瓜农大叔，可以很轻易地通过剧作套路和画面信息被观众排除，导演没有选择利用这一点进行一些反套路的设定（例如让前期一些被观众忽略的角色在中后部发挥至关重要的剧情扭转作用，达到出乎意料的效果），而是选择了“顺水推舟”让大量的剧情正中观众下怀，毫不意外的展开很容易让观众失去耐心。其中我比较印象深刻的几集都不是公交车上的动作戏或是警局里的对峙戏，反而是对二次元小哥，西瓜大叔以及见义勇为大叔在戏外人物形象的描写，很能牵动我，更容易让观众完成从一车素不相识的乘客到最后每一位都是鲜活人生写照的认知转变。</p>
<p>说到这就来到了第二部分，人物。如前所言，对公交车上其他乘客进行车外故事描写的部分我很喜欢，但同样的一个问题是，导演没有利用好这些人物的背景故事、动机和观众对他们的感情基础。这里面较为出彩人物设置其实是见义勇为大叔，通过他的背景故事埋下了“消防安全检查提前”的伏笔，以及通过爆炸后警方对其的调查引出了凶手所在地（港务新村），最后在阻止爆炸的过程中也是他发挥了比二次元小哥更直接的作用，而且他朴实的“见义勇为可以拿钱补贴家用”的动机也更纯粹，相比较二次元小哥能被叫出中二名就愿意帮忙来说，更能让观众理解与共情。事实上整部剧结束后我们能看到的，车上乘客在终局中扮演的作用无非只有一个——协助男女主阻止爆炸的工具人。说到这就引出了另外一个问题，如何更好地利用这些人物生平与形象？这里的答案我相信是仁者见仁，智者见智。我的一个想法是也许可以从“爆炸后的人生”这一点入手，首先不能把车上乘客的经历在大结局之前简单的设置成要么被炸死，要么就没有后续。刚出狱的爸爸因身陷爆炸案嫌疑而被影响父子关系，无家可归的父亲身无分文继续在城市中流浪，卢笛因险些“被”献出心脏而被母亲发现秘密基地最终导致猫猫全部被扔……这些更立体的人物困境其实要比死亡更能牵动观众的心，男女主若是能因为目睹到这些而进一步坚定走向 HE 的信念和决心，似乎要比救人一命更有戏剧上的说服力。无奈在当前的循环设定上来看，因为只有从爆炸到睡着的半天时间这些似乎也很难展开。</p>
<p>“循环”这个设定，其实也是《开端》做的好也不好的地方。好在哪里呢？它更像是一种剧情道具，或者说一种情景实验：假设男女主身处于这样一种循环下，面对循环和爆炸他们会怎样反应，怎样行动。这也是这部剧直接的看点来源；不好在哪里？它本应该和爆炸一起是本作中核心的冲突来源，循环因何而起？怎样才能脱离循环？循环的原理是什么？这些问题统统没有解释，也没有看到一丝深入的打算。不过我更愿意相信这是有更深层次的难言之隐，毕竟如果不想突兀地引入科幻元素，考虑神秘主义在审查上会遇到的问题，似乎很难找到一种合适的框架去解释这一切，所以我认为回避这一点其实是制作中的有意为之。对于之前网络上各种双循环的剧情猜测，我觉得也是不错的一个思路，但最终结局选择了更为保守和平稳的女性保护社会议题，中间穿插了一些对网络暴力的反思，这也是一种立意的升华和加分项。</p>
<p>不吐不快，一口气写了这么多，本来还有很多点要吐槽，诸如凶手夫妇的实际作案动机变化过渡，最后一次循环的处理等等……但现在回看 15 集 40 分钟的片长似乎给谁拍都不够用啊，最开始的 20 集好像也挺合适，那就这样吧，毕竟可能被喷“你行你上啊”所以暂且写到这，就目前的成片来看 8.2 分还是过高了，不过作为国产中该类型剧的第一次，以资鼓励还是应该的。只希望以后限制越来越少，创作越来越多，让观众多一些不一样的题材和故事去选择。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>Rust 的 async&#x2f;await 语法是怎样工作的</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/how-rust-async-await-syntax-works"/>
        <published>2022-01-24T07:11:03Z</published>
        <updated>2025-03-07T17:31:53Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/how-rust-async-await-syntax-works</id>
        <content type="html">
            <![CDATA[<p>从最开始的宏到现在的 Rust 关键字，距离 async/await 语法的 <a href="https://github.com/rust-lang/rfcs/blob/master/text/2394-async_await.md">rfc</a> 被提出已经过去将近 4 年了。相比于回调地狱，或者类似 <a href="https://en.wikipedia.org/wiki/Continuation-passing_style">CPS</a>-Style 的铁索连环套娃（此处应有圣经传唱：<del>一个 Monad 说白了不过就是自函子范畴上的一个幺半群而已</del>），async/await 的存在无疑提供了一种良好的异步代码编写方式，它更像是把同步代码写法的异步化，让代码编写者能够最大限度的遵循同步代码编写方式，但同时提供异步的运行时表现。</p>
<p>不过，有言道：”哪有什么岁月静好，不过是有人替你负重前行“。想要代码写的爽，编译器一定会在背后做很多”脏活累活“。Rust 的 async/await 语法具体是怎样工作的？它又是如何将我们写的代码，转化成异步执行的呢？</p>
<p>先来看一段代码。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">() -&gt; </span><span style="color:#cc99cc;">usize </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#f99157;">5
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>再简单不过的一个 async 函数，只会返回一个 5，为了防止被编译器优化掉，我们给它加上了一个 <code>#[inline(never)]</code> 属性。这个异步函数的等价同步代码长这样：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">() -&gt; impl Future&lt;Output = </span><span style="color:#cc99cc;">usize</span><span style="color:#d3d0c8;">&gt; {
</span><span style="color:#d3d0c8;">    async { </span><span style="color:#f99157;">5 </span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>async fn 其实就是会返回一个 <a href="https://docs.rs/rustc-std-workspace-std/latest/std/future/trait.Future.html">Future trait</a> 的函数。不过这一步转化并没有帮助我们更深地理解 async 关键字到底做了什么。为了一探究竟，我们可以尝试看看上述代码的 <a href="https://rustc-dev-guide.rust-lang.org/hir.html">HIR</a> 长什么样。HIR 是 Rust 在编译过程中的一个中间产物，在转化成更为晦涩难懂的 <a href="https://rustc-dev-guide.rust-lang.org/mir/index.html">MIR</a> 之前，它可以帮助我们一窥编译器的小小细节。</p>
<pre style="background-color:#2d2d2d;"><code class="language-shell"><span style="color:#d3d0c8;">cargo rustc -- -Z unpretty=hir
</span></code></pre>
<p>输出如下（为了方便展示，我做了一些格式上的调整）：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;"> -&gt; </span><span style="color:#747369;">/*impl Trait*/ </span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">from_generator</span><span style="color:#d3d0c8;">&quot;](</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> _task_context| { { </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> _t = { </span><span style="color:#f99157;">5 </span><span style="color:#d3d0c8;">}; _t } })
</span></code></pre>
<p>此时我们终于看到了 Rust 中异步语义实现的核心：generator。不过上面这个函数的内容还是过于贫瘠了，甚至都没有涉及到今天文章的另一个主角 await。所以我们先在 <code>x()</code> 的基础上再加一个 <code>y()</code>。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">() -&gt; </span><span style="color:#cc99cc;">i32 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#f99157;">5
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">y</span><span style="color:#d3d0c8;">() -&gt; </span><span style="color:#cc99cc;">i32 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#66cccc;">x</span><span style="color:#d3d0c8;">().await
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p><code>y()</code> 也是一个异步函数，它会在内部调用 <code>x().await</code>，即在 <code>x()</code> 返回结果前 block 住自己，不进行后续的操作。虽然在本例中 <code>x()</code> 并没有任何需要等待的操作，会直接返回 5，但在实际开发中，await 可能作用在各种各样的 Future 上，诸如锁的争用，网络 I/O 等，能够在此类操作不能被立马完成时提前返回并稍后再看也是异步编程的一个核心思想。此时我们再次输出 HIR，可以发现内容果然丰富了许多。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;"> -&gt; </span><span style="color:#747369;">/*impl Trait*/ </span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">from_generator</span><span style="color:#d3d0c8;">&quot;](</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> _task_context| { { </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> _t = { </span><span style="color:#f99157;">5 </span><span style="color:#d3d0c8;">}; _t } })
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">async </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">y</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;"> -&gt; </span><span style="color:#747369;">/*impl Trait*/ </span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">from_generator</span><span style="color:#d3d0c8;">&quot;](</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> _task_context|
</span><span style="color:#d3d0c8;">   {
</span><span style="color:#d3d0c8;">     {
</span><span style="color:#d3d0c8;">       </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> _t =
</span><span style="color:#d3d0c8;">       {
</span><span style="color:#d3d0c8;">         </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">into_future</span><span style="color:#d3d0c8;">&quot;](</span><span style="color:#66cccc;">x</span><span style="color:#d3d0c8;">())
</span><span style="color:#d3d0c8;">         {
</span><span style="color:#d3d0c8;">           </span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> pinned
</span><span style="color:#d3d0c8;">           =&gt;
</span><span style="color:#d3d0c8;">           </span><span style="color:#cc99cc;">loop </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">             </span><span style="color:#cc99cc;">match unsafe
</span><span style="color:#d3d0c8;">             {
</span><span style="color:#d3d0c8;">               #[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">poll</span><span style="color:#d3d0c8;">&quot;](#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">new_unchecked</span><span style="color:#d3d0c8;">&quot;](&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> pinned),
</span><span style="color:#d3d0c8;">               #[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">get_context</span><span style="color:#d3d0c8;">&quot;](_task_context))
</span><span style="color:#d3d0c8;">           }
</span><span style="color:#d3d0c8;">           {
</span><span style="color:#d3d0c8;">             #[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">Ready</span><span style="color:#d3d0c8;">&quot;] {
</span><span style="color:#d3d0c8;">               </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">: result
</span><span style="color:#d3d0c8;">             }
</span><span style="color:#d3d0c8;">             =&gt;
</span><span style="color:#d3d0c8;">             </span><span style="color:#cc99cc;">break
</span><span style="color:#d3d0c8;">             result,
</span><span style="color:#d3d0c8;">             #[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">Pending</span><span style="color:#d3d0c8;">&quot;] {}
</span><span style="color:#d3d0c8;">             =&gt;
</span><span style="color:#d3d0c8;">             {
</span><span style="color:#d3d0c8;">             }
</span><span style="color:#d3d0c8;">           }
</span><span style="color:#d3d0c8;">           _task_context
</span><span style="color:#d3d0c8;">           =
</span><span style="color:#d3d0c8;">           (</span><span style="background-color:#f2777a;color:#2d2d2d;">yield</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">             ());
</span><span style="color:#d3d0c8;">         },
</span><span style="color:#d3d0c8;">       }
</span><span style="color:#d3d0c8;">     };
</span><span style="color:#d3d0c8;">     _t
</span><span style="color:#d3d0c8;">   }
</span><span style="color:#d3d0c8;">})
</span></code></pre>
<p>为了方便讲解，我尝试把上述代码转化成 Rust 伪代码：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">(never)]
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">x</span><span style="color:#d3d0c8;">() -&gt; impl Future&lt;Output = </span><span style="color:#cc99cc;">usize</span><span style="color:#d3d0c8;">&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#66cccc;">from_generator</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> _task_context| {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> _t = </span><span style="color:#f99157;">5</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        _t
</span><span style="color:#d3d0c8;">    })
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">y</span><span style="color:#d3d0c8;">() -&gt; impl Future&lt;Output = </span><span style="color:#cc99cc;">usize</span><span style="color:#d3d0c8;">&gt; {
</span><span style="color:#d3d0c8;">    </span><span style="color:#66cccc;">from_generator</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> _task_context| {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> pinned = </span><span style="color:#66cccc;">into_future</span><span style="color:#d3d0c8;">(</span><span style="color:#66cccc;">x</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">loop </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">match unsafe </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">                Pin::new_unchecked(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> pinned).</span><span style="color:#66cccc;">poll</span><span style="color:#d3d0c8;">(_task_context.</span><span style="color:#66cccc;">get_context</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">            } {
</span><span style="color:#d3d0c8;">                Poll::Ready(result) =&gt; </span><span style="color:#cc99cc;">break</span><span style="color:#d3d0c8;"> result,
</span><span style="color:#d3d0c8;">                Poll::Pending =&gt; {}
</span><span style="color:#d3d0c8;">            }
</span><span style="color:#d3d0c8;">            </span><span style="background-color:#f2777a;color:#2d2d2d;">yield</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    })
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到整个转化主要干了两件事情：</p>
<ul>
<li>把 async 块转化成一个由 <code>from_generator</code> 方法包裹的闭包</li>
<li>把 await 部分转化成一个循环，调用其 poll 方法获取 Future 的运行结果</li>
</ul>
<p>这里的大部分操作还是比较符合直觉的：因为遇到了需要 await 完成的操作，所以运行一个循环去不停的获取结果，完成后再继续。注意到这里，当 x 所代表的 Future 还没有就绪时（即便在本例中并不会存在这种情况），loop 的运行会来到一个 yield 语句，而非 return。在开始阐述 generator 的 yield 之前，我们不妨先来思考一下，如果这里使用了 break 或 return，会有什么问题？</p>
<p>break 很好思考，loop 循环直接结束，如果 y 函数后续还有其它操作那么就会被执行——这显然不符合 await 的语义，我们需要 block 在当前的 Future 上，而不是忽略其结果继续运行后续代码。</p>
<p>那么 return 呢？如果这个 Future 暂时不能 await 出结果，那么我们为了应该尽快完成上层函数的 poll 操作，不 block 当前 Executor 对其他 Future 的执行，直接返回一个 Poll::Pending——到目前为止都没什么问题，但问题的关键在于，如果 <code>y()</code> 这个 Future 被 Waker 唤醒后，再次被 poll 的时候会发生什么？它会把 await 之前的所有代码都再运行一遍，这显然也不是我们想要的。不论是操作系统的线程还是 Future 这种用户态的 Task，我们想要的任务调度切换显然是需要有一个“断点续传”的基本能力。对于系统线程来说，我们知道操作系统进行线程调度时，会将上下文信息保存好，以遍后续线程再次被运行时可以通过上下文切换再次恢复运行时的状态。那么 Rust 的异步是怎么做到这一点的呢？答案就是 generator。</p>
<p>再来看一段代码：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#![</span><span style="color:#f2777a;">feature</span><span style="color:#d3d0c8;">(generators, generator_trait)]
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::ops::{Generator, GeneratorState};
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::pin::Pin;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut </span><span style="color:#6699cc;">generator </span><span style="color:#d3d0c8;">= || {
</span><span style="color:#d3d0c8;">      	</span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> val = </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        </span><span style="background-color:#f2777a;color:#2d2d2d;">yield</span><span style="color:#d3d0c8;"> val;
</span><span style="color:#d3d0c8;">      	val += </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        </span><span style="background-color:#f2777a;color:#2d2d2d;">yield</span><span style="color:#d3d0c8;"> val;
</span><span style="color:#d3d0c8;">      	val += </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> val;
</span><span style="color:#d3d0c8;">    };
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Yielded(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Yielded(</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Complete(</span><span style="color:#f99157;">3</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到 generator 拥有自己的状态，当你在通过调用 <code>resume()</code> 方法来推进其执行状态时，它不会从头来过，而是从上一次 yield 的地方继续向后执行，直到 return。上面的代码会被转换成类似下面的代码：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#![</span><span style="color:#f2777a;">feature</span><span style="color:#d3d0c8;">(generators, generator_trait)]
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::ops::{Generator, GeneratorState};
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::pin::Pin;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> generator = {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">enum </span><span style="color:#d3d0c8;">MyGenerator {
</span><span style="color:#d3d0c8;">            Start,
</span><span style="color:#d3d0c8;">            Yield1(</span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">),
</span><span style="color:#d3d0c8;">            Yield2(</span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">),
</span><span style="color:#d3d0c8;">            Done,
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Generator </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">MyGenerator {
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">Yield = </span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">Return = </span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">resume</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">: Pin&lt;&amp;</span><span style="color:#cc99cc;">mut Self</span><span style="color:#d3d0c8;">&gt;, </span><span style="color:#f2777a;">_resume</span><span style="color:#d3d0c8;">: ()) -&gt; GeneratorState&lt;</span><span style="color:#cc99cc;">Self::</span><span style="color:#d3d0c8;">Yield, </span><span style="color:#cc99cc;">Self::</span><span style="color:#d3d0c8;">Return&gt; {
</span><span style="color:#d3d0c8;">                </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">std::mem::replace(&amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">*</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, MyGenerator::Done) {
</span><span style="color:#d3d0c8;">                    MyGenerator::Start =&gt; {
</span><span style="color:#d3d0c8;">                        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> val = </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">                        *</span><span style="color:#f2777a;">self </span><span style="color:#d3d0c8;">= MyGenerator::Yield1(val);
</span><span style="color:#d3d0c8;">                        GeneratorState::Yielded(val)
</span><span style="color:#d3d0c8;">                    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">                    MyGenerator::Yield1(val) =&gt; {
</span><span style="color:#d3d0c8;">                        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> new_val = val + </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">                        *</span><span style="color:#f2777a;">self </span><span style="color:#d3d0c8;">= MyGenerator::Yield2(new_val);
</span><span style="color:#d3d0c8;">                        GeneratorState::Yielded(new_val)
</span><span style="color:#d3d0c8;">                    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">                    MyGenerator::Yield2(val) =&gt; {
</span><span style="color:#d3d0c8;">                        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> new_val = val + </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">                        *</span><span style="color:#f2777a;">self </span><span style="color:#d3d0c8;">= MyGenerator::Done;
</span><span style="color:#d3d0c8;">                        GeneratorState::Complete(new_val)
</span><span style="color:#d3d0c8;">                    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">                    MyGenerator::Done =&gt; {
</span><span style="color:#d3d0c8;">                        panic!(&quot;</span><span style="color:#99cc99;">generator resumed after completion</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">                    }
</span><span style="color:#d3d0c8;">                }
</span><span style="color:#d3d0c8;">            }
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">        MyGenerator::Start
</span><span style="color:#d3d0c8;">    };
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Yielded(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Yielded(</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">match </span><span style="color:#d3d0c8;">Pin::new(&amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> generator).</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(()) {
</span><span style="color:#d3d0c8;">        GeneratorState::Complete(</span><span style="color:#f99157;">3</span><span style="color:#d3d0c8;">) =&gt; {}
</span><span style="color:#d3d0c8;">        _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">unexpected value from resume</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>以上代码可以被正常编译通过，有兴趣的话可以到 <a href="https://play.rust-lang.org/?version=nightly&amp;mode=debug&amp;edition=2021&amp;gist=49c5d4da4a94b7b8538457c3e4891ec2">Rust Playground</a> 亲自试一试。可以看到整体思路其实就是一个状态机，每次 yield 就是一次对 enum 实现的状态进行推进，直到最终状态被完成。过程中与状态相关的数据还会被存储到对应的枚举类型里，以遍下一次被推进时使用。你可能已经注意到一个 generator 的 <code>resume()</code> 方法和 Future 的 poll 似乎有几分神似——都要求方法的调用对象是 Pin 住的，且都会返回一个表示当前状态的枚举类型。那么回到我们最开始的 x 和 y 函数部分，对应的 generator 代码在接下来的 Rust 编译过程中，也正是会被变成一个状态机，来表示 Future 的推进状态。伪代码如下：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">struct </span><span style="color:#d3d0c8;">GeneratorY {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">state</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">task_context</span><span style="color:#d3d0c8;">: Context&lt;</span><span style="color:#cc99cc;">&#39;static</span><span style="color:#d3d0c8;">&gt;,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">future</span><span style="color:#d3d0c8;">: dyn Future&lt;Output = Vec&lt;</span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">&gt;&gt;,
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Generator </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">GeneratorY {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">Yield = ();
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">Return = </span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">resume</span><span style="color:#d3d0c8;">(</span><span style="color:#cc99cc;">mut </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">: Pin&lt;&amp;</span><span style="color:#cc99cc;">mut Self</span><span style="color:#d3d0c8;">&gt;, </span><span style="color:#f2777a;">resume</span><span style="color:#d3d0c8;">: ()) -&gt; GeneratorState&lt;</span><span style="color:#cc99cc;">Self::</span><span style="color:#d3d0c8;">Yield, </span><span style="color:#cc99cc;">Self::</span><span style="color:#d3d0c8;">Return&gt; {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">match </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.state {
</span><span style="color:#d3d0c8;">            </span><span style="color:#f99157;">0 </span><span style="color:#d3d0c8;">=&gt; {
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.task_context = Context::new();
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.future = </span><span style="color:#66cccc;">into_future</span><span style="color:#d3d0c8;">(</span><span style="color:#66cccc;">x</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.state = </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(resume)
</span><span style="color:#d3d0c8;">            }
</span><span style="color:#d3d0c8;">            </span><span style="color:#f99157;">1 </span><span style="color:#d3d0c8;">=&gt; {
</span><span style="color:#d3d0c8;">                </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> result = </span><span style="color:#cc99cc;">loop </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">                    </span><span style="color:#cc99cc;">if let </span><span style="color:#d3d0c8;">Poll::Ready(result) =
</span><span style="color:#d3d0c8;">                        Pin::new_unchecked(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.future.</span><span style="color:#66cccc;">get_mut</span><span style="color:#d3d0c8;">()).</span><span style="color:#66cccc;">poll</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.task_context)
</span><span style="color:#d3d0c8;">                    {
</span><span style="color:#d3d0c8;">                        </span><span style="color:#cc99cc;">break</span><span style="color:#d3d0c8;"> result;
</span><span style="color:#d3d0c8;">                    }
</span><span style="color:#d3d0c8;">                    </span><span style="color:#cc99cc;">return </span><span style="color:#d3d0c8;">GeneratorState::Yielded(());
</span><span style="color:#d3d0c8;">                };
</span><span style="color:#d3d0c8;">                </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.state = </span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">                GeneratorState::Complete(result)
</span><span style="color:#d3d0c8;">            }
</span><span style="color:#d3d0c8;">            _ =&gt; panic!(&quot;</span><span style="color:#99cc99;">GeneratorY polled with an invalid state</span><span style="color:#d3d0c8;">&quot;),
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>可以看到每一个 Future 的本质其实都是一个 Generator，两者可以互相转换，例如 x 函数其实也是一个 Generator，它的实现会比 y 函数简单不少，毕竟只需要直接返回值，而没有额外需要 await 进行 yield 的状态。由于状态机本身就实现了 Future 方法，所以 into_future 也只是简单的进行了一个类型的转化，代码在<a href="https://github.com/rust-lang/rust/blob/master/library/core/src/future/into_future.rs">这里</a>。具体的 Future trait 实现则在 from_generator 的过程中：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#747369;">/// Wrap a generator in a future.
</span><span style="color:#747369;">///
</span><span style="color:#747369;">/// This function returns a `GenFuture` underneath, but hides it in `impl Trait` to give
</span><span style="color:#747369;">/// better error messages (`impl Future` rather than `GenFuture&lt;[closure.....]&gt;`).
</span><span style="color:#747369;">// This is `const` to avoid extra errors after we recover from `const async fn`
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">lang </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">from_generator</span><span style="color:#d3d0c8;">&quot;]
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">doc</span><span style="color:#d3d0c8;">(hidden)]
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">unstable</span><span style="color:#d3d0c8;">(feature = &quot;</span><span style="color:#99cc99;">gen_future</span><span style="color:#d3d0c8;">&quot;, issue = &quot;</span><span style="color:#99cc99;">50547</span><span style="color:#d3d0c8;">&quot;)]
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">rustc_const_unstable</span><span style="color:#d3d0c8;">(feature = &quot;</span><span style="color:#99cc99;">gen_future</span><span style="color:#d3d0c8;">&quot;, issue = &quot;</span><span style="color:#99cc99;">50547</span><span style="color:#d3d0c8;">&quot;)]
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">inline</span><span style="color:#d3d0c8;">]
</span><span style="color:#cc99cc;">pub const fn </span><span style="color:#6699cc;">from_generator</span><span style="color:#d3d0c8;">&lt;T&gt;(</span><span style="color:#f2777a;">gen</span><span style="color:#d3d0c8;">: T) -&gt; impl Future&lt;Output = </span><span style="color:#cc99cc;">T::</span><span style="color:#d3d0c8;">Return&gt;
</span><span style="color:#cc99cc;">where
</span><span style="color:#d3d0c8;">    T: Generator&lt;ResumeTy, Yield = ()&gt;,
</span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    #[</span><span style="color:#f2777a;">rustc_diagnostic_item </span><span style="color:#d3d0c8;">= &quot;</span><span style="color:#99cc99;">gen_future</span><span style="color:#d3d0c8;">&quot;]
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">struct </span><span style="color:#d3d0c8;">GenFuture&lt;T: Generator&lt;ResumeTy, Yield = ()&gt;&gt;(T);
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// We rely on the fact that async/await futures are immovable in order to create
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// self-referential borrows in the underlying generator.
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;">&lt;T: Generator&lt;ResumeTy, Yield = ()&gt;&gt; !Unpin for GenFuture&lt;T&gt; {}
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">impl</span><span style="color:#d3d0c8;">&lt;T: Generator&lt;ResumeTy, Yield = ()&gt;&gt; Future </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">GenFuture&lt;T&gt; {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">Output = T::Return;
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">poll</span><span style="color:#d3d0c8;">(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">: Pin&lt;&amp;</span><span style="color:#cc99cc;">mut Self</span><span style="color:#d3d0c8;">&gt;, </span><span style="color:#f2777a;">cx</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">mut </span><span style="color:#d3d0c8;">Context&lt;&#39;_&gt;) -&gt; Poll&lt;</span><span style="color:#cc99cc;">Self::</span><span style="color:#d3d0c8;">Output&gt; {
</span><span style="color:#d3d0c8;">            </span><span style="color:#747369;">// SAFETY: Safe because we&#39;re !Unpin + !Drop, and this is just a field projection.
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> gen = </span><span style="color:#cc99cc;">unsafe </span><span style="color:#d3d0c8;">{ Pin::map_unchecked_mut(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, |</span><span style="color:#f2777a;">s</span><span style="color:#d3d0c8;">| &amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> s.</span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">) };
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">            </span><span style="color:#747369;">// Resume the generator, turning the `&amp;mut Context` into a `NonNull` raw pointer. The
</span><span style="color:#d3d0c8;">            </span><span style="color:#747369;">// `.await` lowering will safely cast that back to a `&amp;mut Context`.
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">match</span><span style="color:#d3d0c8;"> gen.</span><span style="color:#66cccc;">resume</span><span style="color:#d3d0c8;">(ResumeTy(NonNull::from(cx).cast::&lt;Context&lt;</span><span style="color:#cc99cc;">&#39;static</span><span style="color:#d3d0c8;">&gt;&gt;())) {
</span><span style="color:#d3d0c8;">                GeneratorState::Yielded(()) =&gt; Poll::Pending,
</span><span style="color:#d3d0c8;">                GeneratorState::Complete(x) =&gt; Poll::Ready(x),
</span><span style="color:#d3d0c8;">            }
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    GenFuture(gen)
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p><a href="https://github.com/rust-lang/rust/blob/42313dd29b3edb0ab453a0d43d12876ec7e48ce0/library/core/src/future/mod.rs#L65">from_generator 的源代码</a>如上，可以看到 Future 转换成 Generator 后的 poll 的实现就等于进行一次 generator 的 resume，获得 <code>GeneratorState::Yielded</code> 即返回 <code>Poll::Pending</code>，获得 <code>GeneratorState::Complete(result)</code> 即返回 <code>Poll::Ready(result)</code> ，Context 则是作为 resume 的参数透传给状态机内部，整体逻辑还是非常清晰的。其中关于 Pin 的相关细节则是另一个比较繁杂的话题了，可以参考这篇博客进行学习：<a href="https://folyd.com/blog/rust-pin-unpin">Rust 的 Pin 与 Unpin</a>。</p>
<h1>参考</h1>
<ul>
<li><a href="https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html">Inside Rust's Async Transform</a></li>
<li><a href="https://cfsamson.github.io/books-futures-explained/4_generators_async_await.html#generators-and-asyncawait">Generators and async/await</a></li>
<li><a href="https://doc.rust-lang.org/beta/unstable-book/language-features/generators.html">generators</a></li>
<li><a href="https://tmandry.gitlab.io/blog/posts/optimizing-await-1">How Rust optimizes async/await I</a></li>
<li><a href="https://tmandry.gitlab.io/blog/posts/optimizing-await-2">How Rust optimizes async/await II: Program analysis</a></li>
<li><a href="https://note.xuanwo.io/#/page/rust%2Fstd%20future">Xuanwo's Note: Rust std/Future</a></li>
</ul>
]]>
        </content>
    </entry>
    
    <entry>
        <title>我为什么不再喜欢 Go 了</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/why-i-no-longer-like-go"/>
        <published>2021-12-20T07:39:14Z</published>
        <updated>2025-03-07T17:32:10Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/why-i-no-longer-like-go</id>
        <content type="html">
            <![CDATA[<p>2 年前我发表了一篇题为<a href="https://ipotato.me/article/54">我为什么要把 Go 作为主力语言</a>的文章。彼时我与 Go 初相识，满满都是蜜月期的欣喜，甚至不惜以留下一篇黑历史为代价，狠狠地对其进行了吹捧。时至今日再次翻及往事，看着字里行间让人难以直视的尬吹，我恨不得凿个地洞钻进去......当然，直面历史，深刻反省才是好的，于是本文孕育而生，主要目的是为了对前述那篇文章进行批判，看看当时年少无知的我犯下了哪些错误，同时再讲讲自己写 Go 这 2 年的一些新感悟。</p>
<blockquote>
<p>那么有没有一门既有动态语言的特性，又能在运行时有良好性能，甚至拥有很好的多线程表现的语言存在呢？结果我认为是肯定的，这门语言就是 Go。</p>
</blockquote>
<p>很遗憾，说的这几个我现在都很难认同了，后面再详细展开。</p>
<blockquote>
<p>所以第一个要提到的便是 Go 的语法设计非常简洁，一共只有 25 个关键字，虽然没有类的存在，但是 Go 通过 Interface 实现了抽象程序行为的特性，其思想有点类似多态的概念，使用起来也十分的灵活。</p>
</blockquote>
<p>所谓「大道<del>智减</del>至简」，现在看来简洁似乎并没有给 Go 带来好写的一面。他确实让「统一语言习惯」这件事变容易了，但是这真的是好事吗？没有泛型（其实有了，但至少还没普及）甚至让排序这件事都需要先 Len/Swap/Less 一把梭，体验实在是一言难尽。同时看着 Rust 等其他语言在表达能力上的丰富，各种闭包，各种链式调用浑然天成。再看看自己只能 for range 乖乖循环遍历 slice 的苦逼样子，说不羡慕只能是假的。</p>
<p>以及 interface 这个设计本身也是让人又爱又恨，爱在简单的抽象让程序写起来没有那么复杂，基于行为定义的接口某种意义上也更符合写「面向过程」代码时的直觉；然而恨就在维护和阅读 interface 实在是太蛋疼了，每次读代码企图学习某些功能的内部实现时，看到参数传入的是一个 interface 我就心凉了一半，你不知道这个 interface 实际在运行路径上会具体传入哪个实现，能做的只有再忍痛读调用前的代码并祈祷不要再遇到更多的 interface。然后你会暮然回首感慨：这 TM 跟动态语言有什么区别？有时候我看着 TiDB 在拿到一条 SQL 语句的 AST 以后，还要用 switch 语句一个一个区分不同类型 Node 的处理逻辑，我会想这一切到底是怎么变成这样的呢？</p>
<blockquote>
<p>尽管 Go 是一门静态的强类型编译语言，但是 Go 也提供了一些类似动态语言的特性。例如使用类型推导来减少代码的工作量。（并不是偷懒）</p>
</blockquote>
<p>确实不是偷懒，但我也不知道一个类型推导在那个时候怎么就成了我眼中动态语言的特性了，也许这就是又菜又爱吹吧，<del>好想从这个世界上消失</del>。</p>
<blockquote>
<p>Go 语言中的并发程序有两大法宝。即 goroutine 和 channel，其基于一种名为「顺序通信进程 Communicating Sequential Processes 」的现代并发编程模型而来，在这种编程模型中值会在不同的运行实例 goroutine 中通过 channel 来传递。</p>
</blockquote>
<p>接触 Rust 后学到了一个词：零成本抽象，大意是你获得某样高层的抽象特性所需要付出的代价（几乎）为零。在 Go 里面，goroutine 显然不是一个零成本抽象，即便它只围绕了 <code>go</code> 这样一个简单的关键字，但在使用 goroutine 这件事情上所要实际付出的代价，有时候也许让 Go 引以为傲的协程并发并没有那么美好。</p>
<p>TSO 是 TiDB 中很重要的一个模块，用于给事务提供满足线性一致性的单增时间戳（我的前两篇博文有其源代码解析和原理介绍，感兴趣的可以一读）。所以它几乎是每一条 SQL 上的 Hot Path，其性能也对 TiDB 的性能产生了至关重要的影响。我们在测试 TSO 性能的过程中对正在处理大量 TSO 请求的 PD 节点（TSO 服务的提供者）进行了 Profile，火焰图如下。</p>
<p><figure><img src="https://webp.ipotato.me/4syHKBL-b41edf0cc5f05c1b77438a629713943a.png" alt="TSO 火焰图" /></figure></p>
<p>可以看到实际 TSO 的生成计算逻辑，只占据了整个堆栈不到 2% 的调用时间，要知道与此同时 PD 的 CPU 几乎是已经被吃满了的。在火焰图里，Golang 的 Runtime 调度占据了大量的 CPU。原因也不难理解，大量不同的 gRPC 请求被同时发送到了 PD 节点，但由于 TSO 计算过程和原理很简单，所以每一个请求的实际计算并不会占用很长时间，于是大量的任务切换夺走了仅剩的 CPU 性能。为了对比协程/线程切换这件事实际给负载带来的影响，我们用一个提供了几乎一样功能的 <a href="https://github.com/nolouch/mini-pd/tree/bench-tso">Rust 版本 PD</a> 进行了测试，得益于可以手动设置的 gRPC 线程池大小，当我们将线程池大小设置为 1 时，对比 Golang 版本的 PD，CPU 消耗降低了几乎 80%，同时并没有严重影响性能（甚至在延迟等表现上有所提高）。</p>
<p>对于这样的问题，独立出来一个专门的线程池给 TSO 服务是一个比较直观且符合测试结果的方法。但很遗憾，Golang 并不能提供给我们这样简单的机会，也几乎没有关于 goroutine 的并发参数的调整能够帮到我们。我们最后只能诉诸于对 TSO 请求进行 Batch 和转发来降低 PD leader 节点高 CPU 占用的方法来曲线救国。goroutine 确实提供了足够简单的抽象让我们去实现并发，但这也从来不是完全没有代价的多线程银弹罢了。</p>
<blockquote>
<p>最后需要再次强调，Go 是一门静态的强类型编译语言，这也注定了其性能和效率非 Python 这样的解释型语言所能比拟。Python 非常适合敏捷开发，即快速写出具有许多高级功能的程序，但并不总是能够提供大型项目所需的高性能。而 C 可以创建高性能的可执行文件，但是添加功能会花费更多时间。Go 被称为 21 世纪的 C 语言，不得不说其确实具有一定两全其美的特性。</p>
</blockquote>
<p>关于 Go 的性能问题，前例也仅仅是一个引子，TiDB 在早期版本（甚至现在）也还在 GC 等 Runtime 问题上被有所牵制。Go 的语言特性也让我们在许多与并发资源相关的工作上开展没有那么顺利。</p>
<blockquote>
<p>这大致就是 Go 相较于 Python 给我的感受。虽然我也是才开始接触 Go，上文所提也仅是 Go 语言特性的冰山一角，还有许多诸如数据类型、包管理和方法等语言特性还未涉及，但我相信这些灵活好用的特性足以支撑起我成为 Go 拥簇的选择，希望 Go 能够日益完善的发展下去，我也能伴随着 Go 的进化一同成长成为一个合格的 Gopher。</p>
</blockquote>
<p>Go 也在一路迭代，甚至在 1.17 版本才引入基于寄存器的函数传参这样<del>史诗级</del>更新，TiDB 也享受了这种语言升级带来的红利（难道不是应该的吗）。现在泛型也呼之欲出，也许在不久的将来就能稳定下来且惠及标准库实现，让我们见到全新的 Go。</p>
<p>此时再回看当年的我在文章一开始写下的这段话：</p>
<blockquote>
<p>这不学不要紧，一展开对 Go 的了解，我便狠狠地喜欢上了这门语言（可能和一见钟情的感觉类似）。也出于此，我想写一篇博客来谈一谈我在与 Go 接触的第一印象中，到底是什么吸引住了我，以至于我想要把 Go 从今往后作为自己的<strong>主力</strong>语言。</p>
</blockquote>
<p>现在的我很喜欢一句话：Cheerleading any kind of inanimate object is silly。我也在 <a href="https://github.com/brupst/brupst">brupst</a> 的项目介绍里这样写道：「我们无意参加各类语言之争，也不倡导说出「Rust 是世界上最好的语言」这种言论 （如果你发自真心这么觉得，倒也不是不可以） ，我们只希望能在 Rust 发展的道路上尽自身一份绵薄之力，并让其优势惠及更多有趣的灵魂和创意。同时宣扬开源精神，让这个世界变得更好！」，私以为一个成熟的程序员不应该花费精力参与到这种类似宗教战争一样的运动中去，没有哪个木匠会为了「扳手好用还是锤子好用」这样的事情而与另一个木匠争执不休。我觉得语言某种意义上来说也是同理。所以说这种所谓的「作为主力语言」还是算了（无非是没机会写其他的语言罢了）。每种语言都有优势和不足，能够掌握多种语言在各种领域游刃有余，在重要的时刻能够正确地选择最适合自己的瑞士军刀也许才是修炼之道，<del>Go 语言修仙？</del></p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>TiDB 5.0 事务分布式授时模块</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/tidb-5-0-distributed-timstamp-oracle-transaction-module"/>
        <published>2021-03-04T04:16:54Z</published>
        <updated>2025-03-07T17:32:46Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/tidb-5-0-distributed-timstamp-oracle-transaction-module</id>
        <content type="html">
            <![CDATA[<p>好吧，其实只是想转载一下发在公司博客上的这篇文章，顺便写（水）一篇博客，反正都是自己写的，不算过分吧？</p>
<p><a href="https://pingcap.com/blog-cn/preliminary-study-on-cross-center-deployment-capability-of-tidb5.0">TiDB 5.0 跨中心部署能力初探 | 中心化还是去中心化？揭秘 TiDB 5.0 事务分布式授时模块</a></p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>PD 授时服务 TSO 设计简析</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/pd-tso-design"/>
        <published>2021-01-27T12:35:08Z</published>
        <updated>2025-03-07T17:33:22Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/pd-tso-design</id>
        <content type="html">
            <![CDATA[<p>本来这是一篇要发到公司博客的技术文，但后来搁置了，最近又在写最新的分布式 TSO 技术分享，于是索性把之前这篇完善一下分享到博客上。由于开发改造，相关的代码存在一些改动，可能与最新的 master 分支存在不一致，所以本篇严格意义上仅针对 PD 的 release-4.0 或更早分支，但整体设计思想和细节还是一脉相承，没有太大变化。</p>
<h1>一些背景</h1>
<p>TiDB 的事务实现基于 Google <a href="https://research.google/pubs/pub36726">Percolator</a> 分布式事务协议，在整个过程中，我们需要一个严格保持线性增长的时间戳来保证事务的 Linearizability。而要在分布式系统中做到这一点，在业界有以下 3 个主流方式：</p>
<ul>
<li>True Time</li>
<li>Hybird Logic Clock</li>
<li>Timestamp Oracle</li>
</ul>
<p>Google Spanner 使用的是 True Time，即用一套基于 GPS 全球同步的原子钟级别硬件设备来达到全球范围内的时间一致性，通过对外暴露几个简单的接口即可帮助分布式系统获得线性的时间戳。不过由于不是所有公司都有 Google 这样的财力，同时作为硬件解决方案，其很高的成本和通用性问题让 True Time 的使用对大多数公司只是不能望其项背的存在。</p>
<p>CockroachDB 采用的是 Hybird Logic Clock 方案，HLC 完全是一个算法方案，通过混合物理时间和逻辑时间来达成时间戳的线性增长。由于 HLC 基于 NTP（网络时间协议），考虑到同步错误等问题可能带来的物理时间误差，往往在 HLC 算法中会存在一个有效的时间边界范围，再结合一些事务机制，CockroachDB 实现了仅对单行事务保证线性一致，对于涉及多行的事务则无法保证绝对的线性一致。</p>
<p>TiDB 采用了 PD 进行全局单点统一授时的 Timestamp Oracle 方案。由于只涉及到全局单点且没有复杂的算法，实现起来较为简单。尽管比起上面两个方案，每一次 TiDB 进行事务时都会与 PD 进行网络通信造成额外的开销，但对于常同处于一个 DC 下的 TiDB 与 PD 集群，这部分开销往往在理想范围内。即便是涉及到多个 DC 的事务，我们也会通过一些机制（例如完全涉及本地表的事务可以只需要一个 Local TSO 而无需立即与全局 TSO 进行同步）来进行优化。经过多方面的考量，我们最终选择了 TSO 来作为分布式系统的时间解决方案。下为 TiDB 的架构图，其中 PD 为 TiDB 提供的两大主要功能就是 TSO 授时和数据位置元信息同步。</p>
<p><figure><img src="https://webp.ipotato.me/tidb-architecture-edc66be50c6de74fb90adc8263c79d57.png" alt="" /></figure></p>
<h1>目标</h1>
<p>由于整个集群的 TSO 授时工作都集中在了 PD 身上，所以怎样做到低延迟，高性能和良好的容错，是我们在实现时需要关注的几个目标点。我们主要通过基本结构，校时，授时以及递进这四个部分来讲解 PD TSO 的具体工作原理。</p>
<h1>设计原理</h1>
<h2>基本结构</h2>
<p>对于 PD 来说，我们要保证它能快速大量地为事务分配 TSO，同时也需要保证分配的 TSO 永远单调递增，即一旦分配了时间戳 <code>t1</code>，往后再也不可能出现一个时间戳 <code>t2</code> 满足 <code>t2 &lt;= t1</code>。</p>
<p>TSO 是一个 int64 的整型，它由 Physical time 和 Logical time 两个部分组成。Physical time 是当前的 Unix 系统时间戳（毫秒），而 Logical time 则是一个范围在 [0, 1 &lt;&lt; 18] 的计数器。这样一来便做到了在每毫秒的物理时间粒度上又可以继续细化成最多 262144 个 TSO，足以满足绝大多数使用场景了。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// server/tso/tso.go
</span><span style="color:#747369;">// atomicObject represents a tso
</span><span style="color:#cc99cc;">type </span><span style="color:#d3d0c8;">atomicObject </span><span style="color:#cc99cc;">struct </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">physical</span><span style="color:#d3d0c8;"> time.Time
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">logical  </span><span style="color:#cc99cc;">int64  </span><span style="color:#747369;">// maxLogical = int64(1 &lt;&lt; 18)
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>实际使用 <code>atomicObject</code> 时，我们会始终将一个指向其值的 <code>UnsafePointer</code> 作为访存 TSO 的唯一方式。比起直接传值，这么做的目的是为了控制 TSO 在调用链之间传递时的行为，避免返回的 TSO 在某一个环节被更改，从而破坏 Linearizability 约束。</p>
<h2>校时</h2>
<p>PD 的 TSO 授时工作是由集群中 leader 来完成的。为了在 PD 集群中持久化当前分配的最大 TSO，避免因为 leader 挂掉而影响 TiDB 的事务，我们需要把 TSO 的物理时间戳存储到 etcd 中去。同时为了提高响应授时 RPC 请求的速度，我们也要避免与 etcd 交互得过于频繁，不能每有一次 TSO 更新就进行一次 etcd 读写。所以我们要存储的并不能是最近一次的授时结果，而是一个时间窗口的范围，这一点我们会在稍后的时间戳递进实现中做进一步阐述。</p>
<p>每当一个新的 PD leader 被<a href="https://pingcap.com/blog-cn/placement-driver/#%E9%80%89%E4%B8%BE">选举</a>出来时，便会进行一次校时，即从 etcd 中取出上一次保存的物理时间戳，并与本地物理时间做比较，进行校对处理。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// server/tso/tso.go
</span><span style="color:#cc99cc;">const</span><span style="color:#d3d0c8;"> updateTimestampGuard = time.</span><span style="color:#f2777a;">Millisecond
</span><span style="color:#d3d0c8;">
</span><span style="color:#747369;">// Load last timestamp stored in etcd
</span><span style="color:#f2777a;">last</span><span style="color:#d3d0c8;">, </span><span style="color:#f2777a;">err </span><span style="color:#d3d0c8;">:= t.</span><span style="color:#6699cc;">loadTimestamp</span><span style="color:#d3d0c8;">()
</span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> err != </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> err
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#f2777a;">next </span><span style="color:#d3d0c8;">:= time.</span><span style="color:#6699cc;">Now</span><span style="color:#d3d0c8;">()
</span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> typeutil.</span><span style="color:#6699cc;">SubTimeByWallClock</span><span style="color:#d3d0c8;">(next, last) &lt; updateTimestampGuard {
</span><span style="color:#d3d0c8;">    next = last.</span><span style="color:#6699cc;">Add</span><span style="color:#d3d0c8;">(updateTimestampGuard)
</span><span style="color:#d3d0c8;">
</span></code></pre>
<p>用当前系统时间 <code>next</code> 减去上一次在 ectd 中存储时间戳 <code>last</code>，如果小于我们设定的常量 <code>updateTimestampGuard</code>（默认为 1 毫秒），我们就认为需要使用 <code>last</code> 来作为下一次持久化时间戳的起点。不难理解，如果当前系统的时间戳和上一次使用的 TSO 靠的太近或者说甚至小于它，就会存在破坏线性一致性的潜在风险，于是需要通过使用 <code>last</code> 并强制增加一个精度范围来进行控制，从而保证新上任的 leader 所分配的 TSO 一定大于之前所有已分配的 TSO。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#f2777a;">save </span><span style="color:#d3d0c8;">:= next.</span><span style="color:#6699cc;">Add</span><span style="color:#d3d0c8;">(t.</span><span style="color:#f2777a;">saveInterval</span><span style="color:#d3d0c8;">)
</span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> err = t.</span><span style="color:#6699cc;">saveTimestamp</span><span style="color:#d3d0c8;">(save); err != </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> err
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#f2777a;">current </span><span style="color:#d3d0c8;">:= &amp;atomicObject{
</span><span style="color:#d3d0c8;">    physical: next,
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">t.</span><span style="color:#f2777a;">lease </span><span style="color:#d3d0c8;">= lease
</span><span style="color:#d3d0c8;">atomic.</span><span style="color:#6699cc;">StorePointer</span><span style="color:#d3d0c8;">(&amp;t.</span><span style="color:#f2777a;">ts</span><span style="color:#d3d0c8;">, unsafe.</span><span style="color:#6699cc;">Pointer</span><span style="color:#d3d0c8;">(current))
</span></code></pre>
<p>紧接着，我们对选出的，需要进行下一次持久化的 TSO 物理时间部分加上一个时间间隔，默认是 3 秒，然后使用 <code>saveTimestamp(save)</code> 将其保存到 etcd 中。PD 这么做的目的是为了能够在这个时间间隔内能直接使用内存里面的 <code>next</code> 的时间戳，避免频繁的与 etcd 进行交互。在内存中直接进行 TSO 计算并返回的性能很高，我们自己内部测试每秒能分配百万级别的 TSO。同时，每当这个时间窗口过期之后，PD 会继续进行同样的动作把 etcd 中的时间更新为 <code>save + 3s</code>。</p>
<h2>授时</h2>
<p>在上述校时完成的基础上，我们已经在内存中存储了可用于授时的计算数据。为了进一步提高效率和减少开销，我们往往会批量地向 PD 获取 TSO。client 会首先收集一批事务的 TSO 请求，譬如 n 个，然后直接向 PD 发送命令，参数就是 n，PD 收到命令之后，会生成 n 个 TSO 返回给客户端。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// server/tso/tso.go
</span><span style="color:#cc99cc;">var </span><span style="color:#f2777a;">resp</span><span style="color:#d3d0c8;"> pdpb.</span><span style="color:#f2777a;">Timestamp
</span><span style="color:#cc99cc;">for </span><span style="color:#f2777a;">i </span><span style="color:#d3d0c8;">:= </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">; i &lt; maxRetryCount; i++ {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">current </span><span style="color:#d3d0c8;">:= (*atomicObject)(atomic.</span><span style="color:#6699cc;">LoadPointer</span><span style="color:#d3d0c8;">(&amp;t.</span><span style="color:#f2777a;">ts</span><span style="color:#d3d0c8;">))
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> current == </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">|| current.</span><span style="color:#f2777a;">physical </span><span style="color:#d3d0c8;">== typeutil.</span><span style="color:#f2777a;">ZeroTime </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> pdpb.</span><span style="color:#f2777a;">Timestamp</span><span style="color:#d3d0c8;">{}, errors.</span><span style="color:#6699cc;">New</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">can not get timestamp, may be not leader</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">    resp.</span><span style="color:#f2777a;">Physical </span><span style="color:#d3d0c8;">= current.</span><span style="color:#f2777a;">physical</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">UnixNano</span><span style="color:#d3d0c8;">() / </span><span style="color:#cc99cc;">int64</span><span style="color:#d3d0c8;">(time.</span><span style="color:#f2777a;">Millisecond</span><span style="color:#d3d0c8;">)
</span><span style="color:#d3d0c8;">    resp.</span><span style="color:#f2777a;">Logical </span><span style="color:#d3d0c8;">= atomic.</span><span style="color:#6699cc;">AddInt64</span><span style="color:#d3d0c8;">(&amp;current.</span><span style="color:#f2777a;">logical</span><span style="color:#d3d0c8;">, </span><span style="color:#cc99cc;">int64</span><span style="color:#d3d0c8;">(count))
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> resp.</span><span style="color:#f2777a;">Logical </span><span style="color:#d3d0c8;">&gt;= maxLogical {
</span><span style="color:#d3d0c8;">        time.</span><span style="color:#6699cc;">Sleep</span><span style="color:#d3d0c8;">(UpdateTimestampStep)
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">continue
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> t.</span><span style="color:#f2777a;">lease </span><span style="color:#d3d0c8;">== </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">|| t.</span><span style="color:#f2777a;">lease</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">IsExpired</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> pdpb.</span><span style="color:#f2777a;">Timestamp</span><span style="color:#d3d0c8;">{}, errors.</span><span style="color:#6699cc;">New</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">alloc timestamp failed, lease expired</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> resp, </span><span style="color:#f99157;">nil
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>当客户端请求 PD 的 TSO 服务时，返回给客户端的是混合了物理与逻辑时间的 TSO。其中 PD 中的的物理时钟会随着系统时间递增，而逻辑时钟部分只会被动地随着授时请求原子增加。这里我们可以注意到：由于逻辑时钟有范围限制，如果超出这个限制，leader 会选择睡眠 <code>UpdateTimestampStep</code> 长度的时间（默认 50 毫秒）来等待时间被推进。<code>UpdateTimestampStep</code> 为 PD 更新系统时间戳操作的时间片间隔，所以至少等待这样一段时间，系统中的物理时间便一定会被推进，相应的逻辑时间也会重置归零，届时便可以继续分配时间戳。TSO 计算过程中，还需要实时对 leader 的 lease 进行检查，如果 lease 过期则不能继续再分配 TSO，保证 PD 集群中每时每刻有且仅有一个 leader 可以进行 TSO 的生成。</p>
<h2>递进</h2>
<p>TSO 的递进更新操作随着系统时间的流逝和 leader 续约 lease 一同进行。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// server/server.go
</span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">select </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">case </span><span style="color:#d3d0c8;">&lt;-leaderTicker.</span><span style="color:#f2777a;">C</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> lease.</span><span style="color:#6699cc;">IsExpired</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">            log.</span><span style="color:#6699cc;">Info</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">lease expired, leader step down</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">return
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">etcdLeader </span><span style="color:#d3d0c8;">:= s.</span><span style="color:#f2777a;">member</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">GetEtcdLeader</span><span style="color:#d3d0c8;">()
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> etcdLeader != s.</span><span style="color:#f2777a;">member</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">ID</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">            log.</span><span style="color:#6699cc;">Info</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">etcd leader changed, resigns leadership</span><span style="color:#d3d0c8;">&quot;, zap.</span><span style="color:#6699cc;">String</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">old-leader-name</span><span style="color:#d3d0c8;">&quot;, s.</span><span style="color:#6699cc;">Name</span><span style="color:#d3d0c8;">()))
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">return
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">case </span><span style="color:#d3d0c8;">&lt;-tsTicker.</span><span style="color:#f2777a;">C</span><span style="color:#d3d0c8;">:
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> err = s.</span><span style="color:#f2777a;">tso</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">UpdateTimestamp</span><span style="color:#d3d0c8;">(); err != </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">            log.</span><span style="color:#6699cc;">Error</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">failed to update timestamp</span><span style="color:#d3d0c8;">&quot;, zap.</span><span style="color:#6699cc;">Error</span><span style="color:#d3d0c8;">(err))
</span><span style="color:#d3d0c8;">            </span><span style="color:#cc99cc;">return
</span><span style="color:#d3d0c8;">        }
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">case </span><span style="color:#d3d0c8;">&lt;-ctx.</span><span style="color:#6699cc;">Done</span><span style="color:#d3d0c8;">():
</span><span style="color:#d3d0c8;">        </span><span style="color:#747369;">// Server is closed and it should return nil.
</span><span style="color:#d3d0c8;">        log.</span><span style="color:#6699cc;">Info</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">server is closed</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p><code>UpdateTimestamp</code> 函数主要做三件事情，一是更新当前内存中 TSO 物理时间（逻辑时间只随着分配请求被动递增，不会主动增加），二是检查当前逻辑时间是否超过阈值，三是适时地更新 etcd 中的时间窗口。同时为了保证 TSO 的线性一致性，<code>UpdateTimestamp</code> 函数在整个过程中要保证以下几个约束：</p>
<ul>
<li>物理时间严格单调递增</li>
<li>存储在 etcd 的时间戳严格单调递增</li>
<li>物理时间必须小于存储的时间戳</li>
</ul>
<p>先来说一，更新当前内存中 TSO 物理时间，同时保证物理时间严格单调递增，其实只要让 TSO 的物理时间与现实世界同步流逝即可，所以符合直觉的做法就是将其实时地更新为当前系统时间。当然，当前系统时间并不能严格保证约束，系统时间被手动更改，网络校时后系统时间回溯，换选后的 PD leader 系统时间更慢等等都是我们需要考虑到的情况。PD 在这一点上也有做处理，即只有当系统时间大于当前（也就是旧）TSO 物理时间戳时才会对其进行更新，保证约束。在系统时间落后的机器上，TSO 的物理时间不会主动推进，仅在逻辑时间突破限制是被动增加，如此一来便可以做到让 TSO 慢下来等待系统时间追上它的进度。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// Physical time now minus prev.physical
</span><span style="color:#f2777a;">jetLag </span><span style="color:#d3d0c8;">:= typeutil.</span><span style="color:#6699cc;">SubTimeByWallClock</span><span style="color:#d3d0c8;">(now, prev.</span><span style="color:#f2777a;">physical</span><span style="color:#d3d0c8;">)
</span><span style="color:#747369;">// If the system time is greater, it will be synchronized with the system time.
</span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> jetLag &gt; updateTimestampGuard {
</span><span style="color:#d3d0c8;">    next = now
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>再来说关于检查逻辑时间是否超过阈值，尽管逻辑时间有 [0, 1 &lt;&lt; 18] 的范围，但我们还是要考虑这个范围有被突破的可能，为了避免溢出的发生，我们会实时检查逻辑时间值，当其超过最大范围的一半时（一半这个设定目前是写死的，经过我们的考量其足以覆盖大多数场景），便会清零逻辑时间并给物理时间加上 1 毫秒。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> prevLogical &gt; maxLogical/</span><span style="color:#f99157;">2 </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// The reason choosing maxLogical/2 here is that it&#39;s big enough for common cases.
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Because there is enough timestamp can be allocated before next update.
</span><span style="color:#d3d0c8;">    log.</span><span style="color:#6699cc;">Warn</span><span style="color:#d3d0c8;">(&quot;</span><span style="color:#99cc99;">the logical time may be not enough</span><span style="color:#d3d0c8;">&quot;)
</span><span style="color:#d3d0c8;">    next = prev.</span><span style="color:#f2777a;">physical</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">Add</span><span style="color:#d3d0c8;">(time.</span><span style="color:#f2777a;">Millisecond</span><span style="color:#d3d0c8;">)
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>最后说三，前文我们提到过 PD 为了能在不频繁与 etcd 进行交互的前提下来进行存储并直接使用内存里的时间戳进行 TSO 分配，会向 etcd 内写入一个时间窗口，并适时地更新这个窗口。注意到代码中这个 <code>updateTimestampGuard</code> 常量，其值为一毫秒，当我们发现上一次存储在 etcd 中的值和当前时间已经接近到一毫秒及以内时，说明上一个窗口时间即将或已经到期耗尽，需要我们对时间窗口进行滑动，开辟新的可用时间空间，即加上默认的 3s 时间间隔并写入 etcd。</p>
<pre style="background-color:#2d2d2d;"><code class="language-go"><span style="color:#747369;">// The time window needs to be updated and saved to etcd.
</span><span style="color:#cc99cc;">if</span><span style="color:#d3d0c8;"> typeutil.</span><span style="color:#6699cc;">SubTimeByWallClock</span><span style="color:#d3d0c8;">(t.</span><span style="color:#f2777a;">lastSavedTime</span><span style="color:#d3d0c8;">.</span><span style="color:#6699cc;">Load</span><span style="color:#d3d0c8;">().(time.</span><span style="color:#f2777a;">Time</span><span style="color:#d3d0c8;">), next) &lt;= updateTimestampGuard {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">save </span><span style="color:#d3d0c8;">:= next.</span><span style="color:#6699cc;">Add</span><span style="color:#d3d0c8;">(t.</span><span style="color:#f2777a;">saveInterval</span><span style="color:#d3d0c8;">)
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">if </span><span style="color:#f2777a;">err </span><span style="color:#d3d0c8;">:= t.</span><span style="color:#6699cc;">saveTimestamp</span><span style="color:#d3d0c8;">(save); err != </span><span style="color:#f99157;">nil </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">return</span><span style="color:#d3d0c8;"> err
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span></code></pre>
<p>由此一来，我们完整回顾了 TSO 授时工作从一个 PD leader 上任进行初始校时到随时间流逝进行递进的过程设计，并且适时注意了 Linearizability 的约束保证。还通过引入时间窗口等概念提高 TSO 分配的速度，期以达到良好的性能，保证 TiDB 的事务效率。接下来我们会在此基础上，继续讨论一些还可能进行优化的点。</p>
<h1>可能的优化点</h1>
<p><figure><img src="https://webp.ipotato.me/9gXOpf9M-939b97a2f90ae6189b65fa1ebf104487.jpeg" alt="" /></figure></p>
<p>PD 采用了中心式的时钟解决方案，本质上还是混合逻辑时钟。但是由于其是单点授时，所以是全序的。中心式的解决方案实现简单，但是跨区域的性能损耗大，因此实际部署时，会将 PD 集群部署在同一个区域，避免跨区域的性能损耗。但是有一个绕不开的场景便是跨 DC 授时，上图展示了这样一种情况——我们只能通过 PD leader 来分配 TS，所以对于 client 2 来说，它需要跨 DC 先从 DC1 的 PD 上面拿 TSO，而这样做势必会影响到 client 2 的延迟。但往往用户的业务是有 DC 关联特性的，如果一次事务所涉及的数据读写只会发生在一个 DC 上面，那么我们其实只需要保证当前 DC 内的 Linearizability，如下图所示。</p>
<p><figure><img src="https://webp.ipotato.me/dxDbq73A-98f3df566a7c1e8d4f428fe9d71dd50f.jpeg" alt="" /></figure></p>
<p>怎样做到在做到本地  TSO Linearizability 的前提下提高效率，又同时能保证之后出现的跨 DC 事务能够不冲突，其实是一个我们现在正在进行，且将来会支持的一个优化点。</p>
<p>除此之外，由于 PD 使用 Go 开发，Go 的 runtime 调度并没有优先级的概念，当 goroutine 越多时，TSO 更新分配的 goroutine 越容易迟迟拿不到执行的机会，从而会致获取 TSO 变慢。goroutine 尽管为开发者提供了非常便利的并发编程体验，但是由于其抽象程度之高，开发者能对调度所做的干涉有限，我们做过诸如绕过 runtime 直接进行网络层面的 syscall 去完成请求的尝试，但效果均不理想，所以 goroutine 调度这一块也是我们需要持续关注去完成优化的一个点。</p>
<p>此外，PD 通过引入 etcd 解决了单点的可用性问题，一旦 leader 节点故障，会立刻选举新的 leader 继续提供服务，理论上来讲由于 TSO 授时只通过 PD 的一个 leader 提供，所以可能是一个潜在的瓶颈，但是从目前使用情况看，PD 的 TSO 分配性能并没有成为过 TiDB 事务中的瓶颈。</p>
<h1>推荐阅读</h1>
<ul>
<li><a href="https://ericfu.me/timestamp-in-distributed-trans">分布式事务中的时间戳</a></li>
<li><a href="https://pingcap.com/blog-cn/placement-driver">TiKV 功能介绍 - Placement Driver</a></li>
</ul>
]]>
        </content>
    </entry>
    
    <entry>
        <title>如何在面试中筛选&#x2f;不做一个「背题家」</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/not-being-a-question-memorizer-in-interview"/>
        <published>2020-07-23T06:51:32Z</published>
        <updated>2025-03-07T17:34:15Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/not-being-a-question-memorizer-in-interview</id>
        <content type="html">
            <![CDATA[<p>众所周知，国内互联网大厂的面试流程一般都比较公开透明，网上也会有不少所谓的面试经验分享，其形式内容基本大同小异，会谈及诸如有几面，每一面都问了哪些问题，做了什么笔试题云云。刨去一些跟每个面试者个人相关的项目问题，剩下的大多都是都是一些通用知识考察，比如操作系统，计算机网络，数据库等。面试者与被面试者往往都是科班出身，要说这些问题的难度也不过尔尔，只要有过系统性的学习，也基本都能从容应对。不过计算机领域之广袤，知识的广度和深度都非常可观，想要面面俱到，也并非易事，但作为企业，面试官总归是想要招到能力更强更好的全才，所以面试前的准备功课不可或缺。但常常出于功利的目的以及追求效率的考虑，很多面试者会选择这样一条道路——背题。上到算法题，下到基础性的概念，无所不背，如此急功近利的做法，往往还会颇有成效。想必在你的漫长面试生涯中，一定听过面试官问过这几个问题（甚至可以做到一字不差）：</p>
<ol>
<li>进程和线程有什么区别？</li>
<li>什么是 TCP 的三次握手和四次挥手？</li>
<li>HTTP 中的 GET 和 POST 方法有什么区别？</li>
<li>B 树和 B+ 树有什么区别？</li>
<li>什么是数据库的 ACID？</li>
<li>......</li>
</ol>
<p>如果你身经百战，这些问题的答案应该信手拈来，很多时候甚至这些知识在你脑中已经不再是一种储备，而是近乎肌肉记忆的条件反射，一听到关键词，答案在嘴边如同施法一般就脱口而出......毕竟面试官问的次数实在是太多了！然而，大多数时候面试官的问题便在此戛然而止，少数面试官会继续问一句「为什么」，但也不过是浅尝辄止，看你说个差不多就「满意」地点了点头，进入下一个问题。几乎没有面试官会继续追问问题更为本质的一面，即「为什么背后的为什么」，也是在此，理解者和背题家的差距便会被暴露。上面这几个问题刚好涵盖了操作系统，计算机网络和数据库，我们挑选其中一些来一一分析讲解，来看看这些问题到底简单在哪里，又难在哪里，以至于大多数人都知其然却不知所以然，通过不停的追问即可到达我们今天的目的：筛选背题家。</p>
<h1>并发 vs 并行</h1>
<p>比起进程和线程的区别，我想先来讲讲「并发」和「并行」这两个词。中文语境下这两个词的区别似乎并不大，意思也难有较大区分，所以我们先来看看它们的英文：Concurrency 和 Parallelism。这两者是有区别的：「并发 Concurrency」意指在同一时间我们同时<strong>处理</strong>多个事情，而「并行 Parallelism」意指在同一时间我们同时<strong>做</strong>多个事情。让我们来用一个比喻解释这两者行为的区别：</p>
<ul>
<li>你吃饭吃到一半，电话来了，你一直到吃完了以后才去接，这是串行。</li>
<li>你吃饭吃到一半，电话来了，你停了下来接了电话，接完后继续吃饭，这是并发。</li>
<li>你吃饭吃到一半，电话来了，你一边打电话一边吃饭，这是并行。</li>
</ul>
<p>可以看到，完成「吃饭」和「接电话」两件事情所需要的时间，串行 == 并发 &gt; 并行。计算机中，并发和并行无论是在底层设计还是上层编程中都是我们用于提高计算机处理效率以及性能的主要思想。看到这你可能会有这样一个疑惑：这并发也没让我们处理事情的效率变高啊，不还是和串行要用一样的时间吗？我们知道，计算机进行计算是需要资源的，这些资源包括但不限于 CPU 算力，内存以及网络 I/O 等，在有限的资源上完成更多的事情显然是我们的一个目标，而并发和并行分别从两个角度来达成这一目的：</p>
<ul>
<li>并发通过提高执行任务时的<strong>资源利用率</strong>来提高效率</li>
<li>并行通过减少执行任务的<strong>耗时</strong>来提高效率。</li>
</ul>
<p>可以看到，前者省下的是资源，而后者省下的是时间（要做的这一点也许还会消耗更多资源）。回到刚才打电话吃饭的例子，串行和并发的两个场景，虽然用时相同，但有一个显著的区别，即在串行的场景下，你的电话在你吃饭的过程中一直在响（暂时忽略你太久不接它会自动或主动挂断这个设定），在整个过程中，你的电话一直处于这一个电话的「等待响应」状态中，此时如果有另一个人也想给你打电话，他显然只能听到忙音而无法联系上你，这时候我们就可以说「电话」这个资源在整个过程中因持续占用却不被处理而浪费了。反观并发的场景，你立马停止了吃饭接听电话，尽管并没有节约时间，但是你的电话资源很快便被处理并释放了出来，此时如果有第二个电话到来，比起第一个场景，你在相同的时间里更多地利用了「电话」，假设第二个电话直接帮助你谈成了一笔 1 个亿的大合同，比起串行场景下接不上可能导致的错亿，可以不可以说并发帮助我们提高了任务处理的效率呢？答案是显然的。</p>
<p>在了解了以上概念后，「进程和线程有什么区别」这个问题又可以引申出多个问题：</p>
<ul>
<li>在多核 CPU 场景下，线程的执行是并行的吗？</li>
<li>在单核 CPU 场景下，使用多线程可以帮助我们提高程序的运行效率吗？</li>
<li>GIL 的存在是否意味着 Python 无法做到并发？它会影响所有的多线程场景吗？</li>
<li>不同语言实现并发主要通过哪几种方法？</li>
</ul>
<p>在此我不提供解答，希望大家能够自己发掘这些问题的答案，帮助自己更好地理解进程，线程，并发以及并行这些玩意儿。</p>
<h1>GET vs POST</h1>
<p>想成为一个 CRUD Boy，HTTP 协议想必你一定了解，面对面试官「HTTP 中的 GET 和 POST 方法有什么区别？」的提问，你心里也许已经想好了一大票答案来回答：</p>
<ul>
<li>GET 作为书签可收藏，POST 作为书签不可收藏。</li>
<li>GET 使用 URL 传递参数，POST 使用表单传递参数。</li>
<li>GET 对传输数据长度有限制，POST 没有限制。</li>
<li>GET 用于获取数据，POST 用于发送数据。</li>
<li>......</li>
</ul>
<p>很遗憾，以上所有都是片面的表象，甚至有些错误，而且也都不是 GET 和 POST 方法的本质区别。事实上，GET 和 POST 从理论上的技术使用来说，没有任何实质区别。GET 能做的事情 POST 也能做到，反之亦然。你完全可以只用 GET 方法来完成你 Web 应用里的所有请求，从读到写一应俱全，POST 也可以在 URL 里带参数，GET 也可以用 Body 来发送数据，实际产生的报文也仅有一些格式上的区别。那么既然「没区别」，那为什么还要区分不同的 HTTP 请求方法呢？要回答这个问题，我们需要从一个词入手：语义。</p>
<p>想必你一定会发现，实际场景中 GET 往往用于获取数据，而 POST 往往作为某些需要发起写数据的请求方式来使用。这几点其实并不是大家逐渐形成的使用习惯，而是早就在 HTTP 的 <a href="https://tools.ietf.org/html/rfc7231">RFC</a> 中有过明确的定义：</p>
<blockquote>
<p>The GET method requests transfer of a current selected representation for the target resource.  GET is the primary mechanism of information retrieval and the focus of almost all performance optimizations. Hence, when people speak of retrieving some identifiable information via HTTP, they are generally referring to making a GET request. A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.</p>
</blockquote>
<blockquote>
<p>The POST method requests that the target resource process the representation enclosed in the request according to the resource’s own specific semantics.</p>
</blockquote>
<p>现在你可以发现，GET 和 POST 其实仅有语义上的区别。</p>
<ul>
<li>GET 意味向请求选择获取一系列资源，也就对应着「读资源」的场景，其需要满足安全，幂等和可缓存，即请求无害，多次请求结果一致，不会改变服务端的状态，并且读结果可以被缓存。对 GET 请求的携带消息并没有任何定义，如前文所述可以通过 URL 也可以通过 Body，但考虑到不同应用（诸如浏览器）的实现不同，选用后者也许会有一些问题。</li>
<li>POST 根据请求负荷对制定资源作出处理，也就对应着「写资源」的场景，其不一定安全，不保证幂等，大部分实现不可缓存。</li>
</ul>
<p>看到这里，再次面对「HTTP 中的 GET 和 POST 方法有什么区别？」的提问，你又会怎么回答呢？</p>
<h1>B Tree vs B+ Tree</h1>
<p>B 树和 B+ 树的区别想必也是老生常谈的话题了，说起来区别，什么一个根结点的儿子数为 [2, M]，一个数据只存在于叶节点等等，大多数人也是朗朗上口，倒背如流。MySQL 默认的存储引擎 InnoDB 会使用 B+ 树来存储数据，无论是表中数据的主索引还是辅助索引都会使用 B+ 树来存储数据，所以......为什么要用 B+ 树而不是 B 树呢？刚才说的那些区别，到底是为什么呢？</p>
<p>B 树和 B+ 树可以说都是平衡二叉树的变种。说区别前，我们先来看看平衡二叉树是为了解决什么问题而存在的。平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树数据结构，学过算法和数据结构的你一定知道，树的高度越矮，两边的节点越平均（平衡），越有利于我们将查找数据的速度优化到近于二分法查找，即 O(log n) 中的树高 n 越小，我们做一次数据访问和修改的代价越低。B 树和 B+ 树均为平衡多路查找树，对比二叉树最明显的区别就是一个父节点的查找路径不再是只有两个，可以是多个，这样一来便很直观地达到了一个目的——让我们的树更矮了。至于其他的区别，我们一一来辨明。</p>
<ul>
<li>B 树的非叶子节点同时保存关键字和对应数据，而 B + 树的非叶子节点只保存关键字，具体数据只存在于叶子结点</li>
</ul>
<p>B 树和 B+ 树均以页为单位，每一页的大小默认即为操作系统的页大小（大多数情况下为 4KB），设想 B+ 树在页大小不变的情况下，只在非叶子结点存储关键字而不是全部数据，此优化让 B+ 树比起 B 树可以在同样的页大小下增加更多的关键字数量，相应的树层数也会减少，增加读取效率。</p>
<ul>
<li>B + 树叶子节点保存了父节点所存储关键字的所有具体数据</li>
</ul>
<p>如上所说，两者均以页为单位划分数据，而操作系统在发现需要的数据不在内存中时，会以页为单位从硬盘上进行加载，这每一次加载便会触发一次 I/O 操作。假设我们要做一次范围查找，而范围的左右边界均在不同的页上，那么意味着想要找所有范围内的关键字数据，B 树需要我们多次从根节点页出发，依照查找路径访问不同的页，而 B+ 树中就不存在这个问，因为所有的数据都存储在叶节点中，并且这些叶节点之间往往又会通过类似链表的方式按顺序进行连接，在范围查找的场景下，我们只需要一次从跟节点页出发，到达范围的左边界后直接在多个子节点之间进行跳转，这样势必能比 B 树的多次换页查找节省大量的磁盘 I/O。</p>
<p>以上两个可以说是 B 树和 B+ 树最大的区别和其背后的设计原理，总结而言，B + 树的优点是：</p>
<ul>
<li>更矮，因为每个非叶子结点只需要存储关键字。</li>
<li>更稳定，因为具体数据只存在于叶子结点上，所以每次查找的次数一定相同——为树的高度。</li>
<li>更快，因为叶子节点间互相链接且保证有序，所以进行扫描遍历更快。</li>
</ul>
<p>B 树也不是一无是处，如果经常访问的数据离根节点很近，也就是说数据的访问频率和树的高度相关联场景下，B 树因为在非叶子结点中本身存了数据，会比 B+ 树更快。</p>
<p>下次再被问同样的问题，除了说出这些区别，主动再讲讲区别背后的考量和出发点，一定会让面试官眼前一亮。比起只能说出区别，而讲不出为什么，高下立判。</p>
<h1>结语</h1>
<p>由于篇幅有限，我不能针对更多的问题进行分析，所以只挑选了几个我认为具有代表性的问题来进行阐述。本文旨在抛砖引玉，作为面试者，我希望你能在日后的面试过程中能够注重对问题本质认识的发掘，这样可以更快的发现候选人身上的闪光点，筛选掉水平良莠不齐的面试「背题家」；作为被面试者，我希望你能在日后的学习过程中更深入的了解自己所学知识，比起知道 How 和 Why，知道 Why why 才是真正对许多事物有透彻理解的终点。在这里送大家一句话：</p>
<blockquote>
<p>Stay foolish, stay hungry.</p>
</blockquote>
<p>求知若渴，虚心若愚。</p>
<h1>参考文献</h1>
<ul>
<li><a href="https://cfsamson.github.io/book-exploring-async-basics/1_concurrent_vs_parallel.html">What's the difference between concurrency and parallelism?</a></li>
<li><a href="https://www.zhihu.com/question/33515481">并发与并行的区别是什么？</a></li>
<li><a href="https://www.zhihu.com/question/28586791">GET 和 POST 到底有什么区别？</a></li>
<li><a href="https://tools.ietf.org/html/rfc7231">Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content</a></li>
<li><a href="https://draveness.me/whys-the-design-mysql-b-plus-tree/">为什么 MySQL 使用 B+ 树</a></li>
</ul>
]]>
        </content>
    </entry>
    
    <entry>
        <title>在读研 &amp; 工作中选择后者</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/choosing-work-over-graduate"/>
        <published>2020-05-29T04:55:26Z</published>
        <updated>2025-03-07T17:34:41Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/choosing-work-over-graduate</id>
        <content type="html">
            <![CDATA[<p>明年就要毕业了，「读研还是工作」也理所应当成为了很多人心中纠结反复，辗转不休的心结。好在我在去年近年末的时候就大致想清楚了这个问题，并且下定决心，拟定了一些目标。到目前来看，实施的还算不错，在稳步中逐渐走上了一个比较符合心里预期的方向，所以也萌生了写一篇文章，讲一讲想法和相关动机与契机。</p>
<p>在开始之前我需要界定一些范围。本篇文章的大多数细节内容可能只适用于 CS 或者其他计算机相关专业，以及，我的经历固然会与我个人的方方面面强相关，个体差异也应该在读者的考量范围内，诚然其背后有普适方法论的存在，但对其总结归纳不是攥写本文的目的。最后，我诚挚地希望每个人都能在这篇文章里有所收获 : -)</p>
<h1>为什么不读研</h1>
<p>这个说来话长，但是非要总结归因，我觉得倒是出乎意料的简单粗暴：我不喜欢学习（狭义上的应试学习），我喜欢做事情的感觉。再具体细分一下其中的因素，应该是以下几点：</p>
<ul>
<li>本科成绩不佳，无法直接保研。</li>
<li>厌恶应试学习，为了考研进行学习对我来说是痛苦且枯燥的。</li>
<li>非常喜欢实践，做具体的事情，把学到的东西真实运用在生产生活中能带给我的满足感远超一切。</li>
<li>自认为有可以本科毕业直接工作的能力。（无自卖自夸之意）</li>
</ul>
<p>前两点是促使我选择工作的直接原因。读研深造固然可以拔高平台，提高能力，让自己的起点比较高，但得到这一结果的付出成本对我而言显然是不可接受的，甚至不考虑考研失败造成的巨大沉没成本，在有可以选择做自己喜欢的事情这个选项的同时去选择花费大量的时间做我不喜欢的事情，无疑是一种自我虐待。其它无须多言，后两者自然是促使我选择工作的根本原因。</p>
<p>人总会为自己辩解，我自然也要为我自己的选择辩解两句。对不读研这个选择总会有人站出来做各种各样的指摘，诸如「你就是条懒狗」，「你是在逃避」云云。关于「为什么要读研？」这个问题，每个人都有自己的答案。拔高平台，深造能力，缓冲进入社会......以结果为导向进行归因，似乎无外乎分两大类：更好地进入工作和更深得进入领域尖端。后者这种强科研属性自然不在本文讨论范围内，前者的目的既然是为了有更好的工作，如果你想要发展的方向并不强调科研背景（诸如 NLP 等算法岗位），那么在条件允许的前提下提前进入工作环境进行历炼也不失为合理的选择。</p>
<p>上面我有讲到「不强调科研背景的方向」，这个有必要单独说说。如果你投过各类大厂小厂的简历，应该能发现大多数算法的校招 or 实习岗位学历要求都会是硕士起步，但你应该很难见到一个前端岗位的最低学历要求是硕士。如果把一个开发者的学历和经验作为天秤的两端，那么以一言蔽之，如果你期望的工作方向更偏工程，那么一个更丰富的工程实践经历也许在企业的 JD （Job Description）中更被看重，这类岗位可能包括但不一定限于：</p>
<ul>
<li>前/后端开发</li>
<li>测试/运维开发</li>
<li>架构开发</li>
<li>......</li>
</ul>
<p>如果你期望的工作方向更需要学术背景，那么一个更高的学历，即更深的学术研究也许更被看重，这类岗位可能包括但不一定限于：</p>
<ul>
<li>机器学习算法</li>
<li>NLP 算法</li>
<li>分布式相关</li>
<li>......</li>
</ul>
<p>是否选择继续读研深造，亦或是提前进入职场积累经验能力，也要通过自己日后想发展的方向来作为考虑因素进行决策。不过有一点需要指出，以上所概括的两个方面在边界上并没有那么清晰：也就不意味着你想要成为一名资深的架构师，学历就一定没有经验重要；不意味着你想成为一名分布式相关领域的从业者一定会被学历这个门槛卡死。修行在个人，无论经验还是学历，都是你个人内在能力的表现，我相信有实力的人一定不会被埋没。</p>
<h1>何时开始实习</h1>
<p>上面扯了一堆关于选择的过程，接下来说一说如果你已经决定早些工作了，该从何做起。关于这一点我不想做太多说教，说「你该怎么怎么样」这样的话，我就讲一讲自己的实际经历，给大家做一个参考吧。</p>
<p>虽然想要直接工作这个想法由来已久，但是真正下定决心其实还是在去年 9 月份，大三上刚刚开学的时候。眼看着毕业临近，对于一个想要在秋招中一展身手的 CS 专业学生来说，至少有一份实习经历显然是格外重要的。比起一张「白纸」，企业更愿意挑选有点内容的「白纸」。</p>
<p>说实话大三上就开始找实习其实是一个有些冒险，但相应收益会比较高的做法。风险点在于学校的课程安排趋近于收尾，一些比较难的专业课也会集中在这个学期，如何平衡好学习和工作是一个首先要考虑的因素，这一点上我的做法比较粗暴：直接翘课。一是因为成绩也够不着保研的尾巴，无需那么在意绩点，二是因为除了体育课这种比较难逃的课，其他一律统统全翘，只是为了挤出了一周 4 天的实习时长，至于课业，只能安排到工作日下班，周末以及考试临近时的请假进行学习。但即便是翘课，对于一些专业课程还是要用心，比如 OS 以及编译原理等课程，可以说是专业的重中之重，不光学习会接触，在面试以及工作中也是非常核心的内容。好在一个学期下来课虽然都翘了，但最后的结果也不坏，没有顾此失彼而整出来个挂科。</p>
<p>大三上开始实习的收益之所以高，其实来自于「趁早」。如果你本科毕业便打算工作的话，可供你实习的时间其实不长，排除掉课业比较繁重的大一和大二，留给你能实习的时间只有两年。而实习这件事情其实是一件复利的事情，你实习的收获会随着你实习的时长和次数不断增加：你在上一份实习中拓展的能力和积累的经验越多，你的下一份实习越有可能拿到更好更有挑战的 offer。所以不难看出尽早开始寻找实习的重要性：越早开始越能在有限的本科时间内有更多的实习收获。其次，早些开始实习的另一方面好处来源于企业招聘的一些心态。作为廉价劳动力的代表，实习生的性价比其实是随着实习的时长逐渐攀升的。培养一个实习生从熟悉业务到独当一面，就是实习生对一个企业逐渐价值最大化的过程，试想，一个实习生如果能干一个正式工 1/2 的活，且还只用拿不到正式工零头的工资，何乐而不为呢？故而在其他各类条件（学历，能力等）相差无几的情况下，公司其实是会更倾向于雇佣离毕业时间更远的候选人的。当然，如上所述仅限日常实习生，如果是春秋招，企业显然是在考虑更长远的候选人价值，毕竟很多像阿里，腾讯这样的大厂用的都是正式工 HC，无法再单纯地用日常实习的价值模型去衡量一个人。但既然我是选择大三上就开始实习，显然不会去和同期的秋招凑热闹，所以我在简历投递时寻找的都是日常实习岗位。</p>
<h1>怎样寻找实习</h1>
<p>这一点其实因人而异，你擅长怎样的技术栈，想望哪个方向发展决定了你要投递的岗位。我个人是因为做过一些 Web 开发，所以对这一块还算是能够应付，也想着自己不要一上来就好高骛远，所以在投递时主要寻找的是后端开发相关岗位。</p>
<p>关于投递的渠道，了解讯息首选当然是各大企业的招聘官网/公众号，不过在上面投递简历，一旦最初投递的岗位被刷，就相当于把简历放入了一个海量备胎池中，运气好被某个部门某个方向相中捞起，运气不好还会遇到简历的冻结期，无法再投。再加上有些大厂的官网日常实习几乎没有任何信息（说的就是你，阿里），就算有，很可能也是一些非常笼统的岗位描述，无法让你自由的选择具体部门方向，所以我个人不建议通过官网投递寻找日常实习。</p>
<p>关于找岗位的渠道，首推北邮人论坛的「兼职实习信息」板块，我 2/3 的实习工作都是通过北邮人论坛的内推贴找到的。一是基本上在这里发贴的都是直接来自各个部门岗位的技术或者 HR，其中不乏很多校友学长学姐，渠道比较直通，信息也比较丰富细化，有什么问题可以直接沟通，时效性也很及时，几乎每天都会有大量的岗位更新发帖，大厂居多，北邮人论坛的存在再次彰显了北邮人互帮互助的魅力......</p>
<p>其次也推荐使用实习僧或者 BOSS 直聘这类 App 进行岗位搜索投递，理由与上面相同，直通，细化且丰富。但由于平台比较大众，鱼目混珠，需要大家多进行一些筛选。</p>
<h1>怎样通过面试</h1>
<p>由于是日常实习，所以基本不会设有像春秋招那样的统一笔试环节，但这一点也不一定，具体会取决于你的简历和不同公司的面试风格。如果你的简历上项目经历比较匮乏，可能难免会让面试官多使用做算法题这样的方式考察能力，如果简历上项目经历写的比较丰富，可能面试官会着重对你的既往经验进行询问交流，从中发掘你的闪光点。</p>
<p>但像数据结构，操作系统，计算机网络以及数据库这些基础中的基础还是非常重要。实习生招聘中的不同能力面的权重，在我面试了这么多家公司以后，大概有如下感受：基础知识 &gt;&gt; 项目经历 &gt; 算法。再次强调，这个权重只是相对而言，不代表算法就一定不重要，对于很多公司来说，面试中的手撕算法环节可能是你通过面试的敲门砖或者底线。具体一些的经历，结合第一份字节跳动实习 offer 来讲。总的来说字节的面试风格比较硬朗，一般分 3 到 4 次面试（包括 HR 面），连续性很强，很多部门基本上一天就能走完所有流程，当天便可知道自己的面试结果，比起吊着你还一直不给你拒信的公司（说的就是你，阿里云），可以说是非常友好了。「手撕算法」这个可以说是字节面试特色了（估计也是从硅谷巨头那里抄来的一套），在聊基础知识和聊项目的同时，穿插着给你来几道从 Easy 到 Medium 不等的数据结构+算法题，较简单的题目一定要快速手写出来，至少通过面试官的人脑编译。比较有难度的题目即便写不出来，面试官也会及时给你引道，听听你的思路。整个过程难度中上，需要针对性地对算法做一些准备，基础知识其次。</p>
<p>关于更多的面试技巧，满打满算我也是面试过十几家大小公司的人了，其中或多或少也有一些总结出来的心得体会，由于篇幅限制不打算放在本文中，日后也许会单独就此写一篇来做分享。</p>
<h1>写在最后</h1>
<p>工作 or 读研的选择不是一道有确定解的简单问题。无论是成为高学历人才的社会共识，还是你的职业规划或个人喜好，在这其中扮演最重要角色的我认为永远是最后一个。在条件允许的范围内，一定要做自己喜欢做的事情。根据我有限的人生经验，为了一些身外之物而选择去做自己不想做之事，是一件相当痛苦的事情。所以当你在某些选择面前摇摆不定时，不如停下来问问自己的内心，你到底喜欢什么？然后去做自己感兴趣的，自己喜欢的。</p>
<p>最后祝大家都能在自己选择的道路上一路顺风，功成名就，共勉。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>几个实用的 Visual Studio Code 插件推荐</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/recommend-useful-visual-studio-code-extensions"/>
        <published>2020-02-23T07:26:35Z</published>
        <updated>2025-03-07T17:35:05Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/recommend-useful-visual-studio-code-extensions</id>
        <content type="html">
            <![CDATA[<p>抛弃各种 IDE 和 Sublime Text 投向 VSCode 的怀抱已经很久了，在这里分享几个我非常喜欢且很实用的插件。</p>
<h2>Git Blame</h2>
<p>插件地址：<a href="https://marketplace.visualstudio.com/items?itemName=waderyan.gitblame">Git Blame</a></p>
<p>团队 Git 项目中，<code>git blame</code> 这个命令相信大家都不陌生，在找黑锅（误）查 bug 的时候查看某文件里某行代码最后的改动是由谁在何时做出显得尤为重要。Git Blame 这个插件可以在光标选中某行的同时直接在 VSCode 的下方显示作出改动 commit 的人和时间，直接点击还可跳转对应的 commit 地址，非常方便。</p>
<p><figure><img src="https://webp.ipotato.me/git-blame-564b497ec5208925513daa1dd0f4237f.png" alt="Git Blame" /></figure></p>
<h2>Git History</h2>
<p>插件地址：<a href="https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory">Git Blame</a></p>
<p>图形化展示 Git 版本变迁的一个插件，可以具体到查看某个分支的某次 commit 做出了那些改动。</p>
<p><figure><img src="https://webp.ipotato.me/git-history-05036e44ac0609e1e550f917817e1dda.png" alt="Git History" /></figure></p>
<p>还可以在 VSCode 中直接展示与本地文件或前一版文件的区别，直观性拉满。</p>
<p><figure><img src="https://webp.ipotato.me/git-diff-efb3c421f75c6e0c842dd8ea6739cdc4.png" alt="改动对比" /></figure></p>
<h2>Bracket Pair Colorizer</h2>
<p>插件地址：<a href="https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2">Bracket Pair Colorizer</a></p>
<p>虽然 VSCode 自带高亮匹配对应括号的功能，但因为括号之间并没有颜色区分，在括号较多的情况下，想一眼分清匹配括号所在还是有点难度。Bracket Pair Colorizer 插件顾名思义，就是给不同对的括号间标上不同的颜色，帮助你进行定位和区分，效果如下图所示。</p>
<p><figure><img src="https://webp.ipotato.me/bracket-pair-colorizer-08840971c83dd96e72dff51308b8898d.png" alt="Bracket Pair Colorizer" /></figure></p>
<p>对于同一代码块的外围，Bracket Pair Colorizer 还会对应颜色的分割线，帮助你区分代码的层级关系。</p>
<p>Bracket Pair Colorizer 的默认颜色只有几个，如果你喜欢花里胡哨的效果，可以自己添加几个颜色。我的设置如下，你可以根据自己的喜好和主题进行定制。</p>
<pre style="background-color:#2d2d2d;"><code class="language-json"><span style="color:#d3d0c8;">&quot;</span><span style="color:#99cc99;">bracket-pair-colorizer-2.colors</span><span style="color:#d3d0c8;">&quot;: [
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Green</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Pink</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Gold</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Orchid</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">LightSkyBlue</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Red</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Purple</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Orange</span><span style="color:#d3d0c8;">&quot;,
</span><span style="color:#d3d0c8;">    &quot;</span><span style="color:#99cc99;">Salmon</span><span style="color:#d3d0c8;">&quot;
</span><span style="color:#d3d0c8;">  ],
</span><span style="color:#d3d0c8;">  &quot;</span><span style="color:#99cc99;">bracket-pair-colorizer-2.forceIterationColorCycle</span><span style="color:#d3d0c8;">&quot;: </span><span style="color:#f99157;">true</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">  &quot;</span><span style="color:#99cc99;">bracket-pair-colorizer-2.forceUniqueOpeningColor</span><span style="color:#d3d0c8;">&quot;: </span><span style="color:#f99157;">true
</span></code></pre>
<h2>rust-analyzer</h2>
<p>插件地址：<a href="https://github.com/rust-analyzer/rust-analyzer">rust-analyzer</a></p>
<p>如果你写 Rust 的话，VSCode 插件商店里有一个名为 Rust Server Language  的插件支持提供各类补全，然而实际使用下来体验并不友好，经常出现不全甚至不出的现象。经过友人推荐，找到了 <a href="https://github.com/rust-analyzer/rust-analyzer">rust-analyzer</a>。虽然还在 WIP 状态，但实际体验已经非常友好，补全分析和函数签名提示都非常流畅。安装需要先在本地进行编译运行，VSCode 插件会在过程中自动载入，而后就可以开始愉快的 Rust 起来了。</p>
<p><figure><img src="https://webp.ipotato.me/rust-analyzer-e64d49b57fc6a4ae233a4fa2b4e58b97.png" alt="rust-analyzer 的类型标注与补全" /></figure></p>
<p>体验到现在唯一的缺点是，由于需要本地进行语法分析以提供 Language Server 的功能，所以如果你的网络不好导致 cargo 更新缓慢或者 build 过程中卡了壳，此时整个编辑器会卡住没法输入任何东西，希望 rust-analyzer 日后能更新做出一些改善。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>纪念李文亮医生</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/in-memory-of-doctor-li"/>
        <published>2020-02-15T12:05:01Z</published>
        <updated>2025-03-07T17:35:57Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/in-memory-of-doctor-li</id>
        <content type="html">
            <![CDATA[<p>能。</p>
<p>明白。</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>A decade passed…where did that bring you?</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/a-decade-passed-where-did-that-bring-you"/>
        <published>2020-02-02T15:06:08Z</published>
        <updated>2025-03-09T02:41:33Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/a-decade-passed-where-did-that-bring-you</id>
        <content type="html">
            <![CDATA[<blockquote>
<p>You could not live with your own failure. Where did that bring you? Back to me…    ——Thanos</p>
</blockquote>
<blockquote>
<p>之所以要用一个英文标题，只是因为在提笔时突然想到了 Thanos 在复联 4 中的这句台词。    ——我</p>
</blockquote>
<p>虽然只是漫长人生中又一个 365 天的结束，但 2020 年的到来多少还是有一点特别——至少这第一个让我有实感的，一个时代的结束。十年有多长？长到足以让我年龄的十位从 1 变成 2，长到让我读完初中，高中并步入大三，长到让我有机会谈一场长达五年的恋爱，长到让我完成了很多曾向往的事，长到让我也足以做出不计其数的选择……写不动排比了，十年太长，回忆起来有点累。</p>
<p>翻看了自己前几年零零碎碎写的年终总结，有一种意外的陌生感，倒不是陌生我本人，只是对文章中那股于未来充满企望的热情有点陌生。再三确定这不是现在的我在故作深沉，矫揉造作之后，我倒有所释然，可能这就是所谓长大了（这种话现在是可以说的对吧，我特么都 20 了）。</p>
<p>再翻了翻自己的其他文章，发现这些年显著增多的其实是技术相关的文章，虽然也没多几篇，但还是我主动去尝试做出的一个变化，说起来原因其实还有点功利，毕竟马上就要毕业了，多少得经营一点关于程序员人设的内容，好作为个人能力的一种轻微佐证。长久以来我一直把博客当成一个自说自话，畅所欲言的树洞，不期望有很多人能看到（其实还是很希望的，自我表达欲不允许我在这一点上撒谎），我在<a href="https://ipotato.me/article/32">个人博客存在的意义</a>这篇文章中探讨过相关观点，写博客就像是在经营自己的一个花园，行人偶尔一瞥带来的赞赏，可能比人来人往的参观更有意义和令人满足。</p>
<p>前面提到毕业的事情，终于，我快要结束了一个从幼儿园开始的漫长求学生涯。这一路上得到的成绩并非总是理想，尤其是大学，主要原因还是我太懒，虽然说这样的话难免会让人觉得我有开脱之嫌，但我不得不承认我是真的不喜欢学习，确切的说，是不喜欢为应试所做的学习。当年之所以选择读计算机，完完全全出于爱好，我非常喜欢这个领域，非常享受写代码这样一个可以通过思考进行直观而又优雅之创造的过程，尤其是在 19 年年底拿到了自己人生中第一份实习 offer 之后，经过几个月的工作生活，我更加确定了我更喜欢在实践中进行探索，进行自我提高的方式，也坚定了自己所选所想，自然而然，考研这件事从一个对我来说虚无缥缈的概念终于彻底转变为了一个被排除在外的选项，二十一年了，终于可以第一次主动对应试教育说不，我将此自诩为一种胜利。</p>
<p>根据大家对「年终总结」这种东西的刻板印象，一般写到这里总要列一列各种「我心目中的 Top 10」。我再三尝试过后，选择了放弃。做不出所谓的 Top List 不是因为每一个都很喜欢，或每一个都不喜欢，只是觉得很难量化地给情绪打上各种各样的标签，“最让我xx的十部电影”，“最让我xx的十本书”，很枯燥，也很无聊，没有人设身处地的和你一起体会在观影阅读时的各种共情同理时刻，仅凭一个 List 就想打动别人，说服别人，实在是有些天真。</p>
<p>本来想写一个「时代总结」，但当我开始回忆十年前时，我竟发现记忆是如此的模糊。也是，十年前我还在上小学，十年前我还没有开始写博客，十年前我还没有认识大多数人，十年前我还没有做很多事，十年前我还很单纯（现在也挺单纯的，张艺柯经常说我幼稚）。</p>
<p>「我每天都问自己，今天的我比昨天更博学了吗？」这句来自《奇葩说》，来自杨齐函，听起来甚至有些扎耳的话，却让我产生了一种略微神往的热血感。十年，在时间这个不可逆转的线性过程中，我一直在试图保持自身的「线性」，我希望我的能力，我的优秀，我的成长是一个关于时间的增函数，保持一阶导数永不为负。几年前曾陷入过对自己的怀疑，但好在我有永远支持我的父母， 永远在我身旁的她，有那么几个比我有心有肺多的朋友，我才走到了如今的所思所在。</p>
<p>遥记前几年中文 Twitter 移动网页版的推文输入框有这样一句 placeholder：</p>
<blockquote>
<p>眼见何事，情系何处，身在何方，心思何人？</p>
</blockquote>
<p>下一个十年，我又会走到哪里？</p>
]]>
        </content>
    </entry>
    
    <entry>
        <title>Rust 常见内置 Traits 详解（一）</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/rust-common-traits-explained-part-1"/>
        <published>2020-02-01T13:12:05Z</published>
        <updated>2025-03-09T02:41:33Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/rust-common-traits-explained-part-1</id>
        <content type="html">
            <![CDATA[<p>本文为《Rust 内置 Traits 详解》系列第一篇，该系列的目的是对 Rust 标准库 <a href="https://doc.rust-lang.org/std/prelude/index.html">std::prelude</a> 中提供的大部分内建 Traits 以适当的篇幅进行解释分析，并辅之以例子（多来自官方文档），旨在帮助读者理解不同 Traits 的使用场景，使用方式及其背后的原因。</p>
<p>本篇作为试水，将包括几个简单的 Traits，均来自于 <a href="https://doc.rust-lang.org/std/cmp/index.html">std::cmp</a></p>
<ul>
<li>Eq &amp; PartialEq</li>
<li>Ord &amp; PartialOrd</li>
</ul>
<h2>Eq &amp; PartialEq</h2>
<blockquote>
<p><a href="https://doc.rust-lang.org/std/cmp/trait.Eq.html">Eq</a>  and  <a href="https://doc.rust-lang.org/std/cmp/trait.PartialEq.html">PartialEq</a>  are traits that allow you to define total and partial equality between values, respectively. Implementing them overloads the == and != operators.</p>
</blockquote>
<p>这两个 Traits 的名称实际上来自于抽象代数中的<a href="https://zh.wikipedia.org/wiki/%E7%AD%89%E4%BB%B7%E5%85%B3%E7%B3%BB">等价关系</a>和局部等价关系，实际上两者的区别仅有一点，即是否在相等比较中是否满足反身性（Reflexivity）。</p>
<p>两者均需要满足的条件有：</p>
<ul>
<li>对称性（Symmetry）：<code>a == b</code> 可推出 <code>b == a</code></li>
<li>传递性（Transitivity）：<code>a == b</code> 且 <code>b == c</code> 可推出 <code>a == c</code></li>
</ul>
<p>Eq 相比 PartialEq 需要额外满足反身性，即 <code>a == a</code>，对于浮点类型，Rust 只实现了 PartialEq 而不是 Eq，原因就是 <code>NaN != NaN</code>。</p>
<p>PartialEq 可使用 <code>#[derive]</code> 来交由编译器实现，这样一个 struct 在进行相等比较时，会对其中每一个字段进行比较，如果遇到枚举，还会对枚举所拥有的数据进行比较。你也可以自己实现自己的 PartialEq 方法，例子如下：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">enum </span><span style="color:#d3d0c8;">BookFormat {
</span><span style="color:#d3d0c8;">    Paperback,
</span><span style="color:#d3d0c8;">    Hardback,
</span><span style="color:#d3d0c8;">    Ebook
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">struct </span><span style="color:#d3d0c8;">Book {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">isbn</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">i32</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">format</span><span style="color:#d3d0c8;">: BookFormat,
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">PartialEq </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">Book {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">eq</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, </span><span style="color:#f2777a;">other</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">Self</span><span style="color:#d3d0c8;">) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.isbn == other.isbn
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>实现时只需要实现 <code>fn eq(&amp;self, other: &amp;Self) -&gt; bool</code> 判断是否相等的函数，Rust 会自动提供 <code>fn ne(&amp;self, other: &amp;Self) -&gt; bool</code>。</p>
<p>实现 Eq 的前提是已经实现了 PartialEq，因为实现 Eq 不需要额外的代码，只需要在实现了 PartialEq 的基础上告诉编译器它的比较满足反身性就可以了。对于上面的例子只需要：<code>#[derive(Eq)]</code> 或 <code>impl Eq for Book {}</code>。</p>
<h2>Ord &amp; PartialOrd</h2>
<blockquote>
<p><a href="https://doc.rust-lang.org/std/cmp/trait.Ord.html">Ord</a>  and  <a href="https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html">PartialOrd</a>  are traits that allow you to define total and partial orderings between values, respectively. Implementing them overloads the &lt;, &lt;=, &gt;, and &gt;= operators.</p>
</blockquote>
<p>类似于 Eq，Ord 指的是 <a href="https://en.wikipedia.org/wiki/Total_order">Total Order</a>，需要满足以下三个性质：</p>
<ul>
<li>反对称性（Antisymmetry）：<code>a &lt;= b</code> 且 <code>a &gt;= b</code> 可推出 <code>a == b</code></li>
<li>传递性（Transitivity）：<code>a &lt;= b</code> 且 <code>b &lt;= c</code> 可推出 <code>a &lt;= c</code></li>
<li>连通性（Connexity）：<code>a &lt;= b</code> 或 <code>a &gt;= b</code></li>
</ul>
<p>而 PartialOrd 无需满足连通性，只满足反对称性和传递性即可。</p>
<ul>
<li>反对称性：<code>a &lt; b</code> 则有 <code>!(a &gt; b)</code>，反之亦然</li>
<li>传递性：<code>a &lt; b</code> 且 <code>b &lt; c</code> 可推出 <code>a &lt; c</code>，<code>==</code> 和 <code>&gt;</code> 同理</li>
</ul>
<p>Ord &amp; PartialOrd 均可通过 <code>#[derive]</code> 交由编译器自动实现，当使用 <code>#[derive]</code> 实现后，将会基于 struct 的字段声明以字典序进行比较，遇到枚举中的数据也会以此类推。可以注意到 Ord &amp; PartialOrd 的性质要求会进行等于的比较，所以有以下对 Eq &amp; PartialEq 的依赖要求：</p>
<ul>
<li>PartialOrd 要求你的类型实现 PartialEq</li>
<li>Ord 要求你的类型实现 PartialOrd 和 Eq（因此 PartialEq 也需要被实现）</li>
</ul>
<p>实现 PartialEq，PartialOrd 以及 Ord 时要特别注意彼此之间不能有冲突。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::cmp::Ordering;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">#[</span><span style="color:#f2777a;">derive</span><span style="color:#d3d0c8;">(Eq)]
</span><span style="color:#cc99cc;">struct </span><span style="color:#d3d0c8;">Person {
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">id</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">u32</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">name</span><span style="color:#d3d0c8;">: String,
</span><span style="color:#d3d0c8;">    </span><span style="color:#f2777a;">height</span><span style="color:#d3d0c8;">: </span><span style="color:#cc99cc;">u32</span><span style="color:#d3d0c8;">,
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">Ord </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">Person {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">cmp</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, </span><span style="color:#f2777a;">other</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">Self</span><span style="color:#d3d0c8;">) -&gt; Ordering {
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.height.</span><span style="color:#66cccc;">cmp</span><span style="color:#d3d0c8;">(&amp;other.height)
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">PartialOrd </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">Person {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">partial_cmp</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, </span><span style="color:#f2777a;">other</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">Self</span><span style="color:#d3d0c8;">) -&gt; Option&lt;Ordering&gt; {
</span><span style="color:#d3d0c8;">        Some(</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">cmp</span><span style="color:#d3d0c8;">(other))
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">impl </span><span style="color:#d3d0c8;">PartialEq </span><span style="color:#cc99cc;">for </span><span style="color:#d3d0c8;">Person {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">eq</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">, </span><span style="color:#f2777a;">other</span><span style="color:#d3d0c8;">: &amp;</span><span style="color:#cc99cc;">Self</span><span style="color:#d3d0c8;">) -&gt; </span><span style="color:#cc99cc;">bool </span><span style="color:#d3d0c8;">{
</span><span style="color:#d3d0c8;">        </span><span style="color:#f2777a;">self</span><span style="color:#d3d0c8;">.height == other.height
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>实现 PartialOrd 需要实现 <code>fn partial_cmp(&amp;self, other: &amp;Self) -&gt; Option&lt;Ordering&gt;</code>，可以注意到这里的返回值是个 Option 枚举，之所以如此是要考虑到与 NaN 作比较的情况：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> result = std::f64::</span><span style="color:#f99157;">NAN</span><span style="color:#d3d0c8;">.</span><span style="color:#66cccc;">partial_cmp</span><span style="color:#d3d0c8;">(&amp;</span><span style="color:#f99157;">1.0</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">assert_eq!(result, None);
</span></code></pre>
<p>完成后会为为你的类型提供 <code>lt()</code>，<code>le()</code>，<code>gt()</code> 和 <code>ge()</code> 的比较操作。</p>
<p>而实现 Ord 需要实现 <code>fn cmp(&amp;self, other: &amp;Self) -&gt; Ordering</code>，完成后会为你的类型提供 <code>max()</code> 和 <code>min()</code>。在目前的 Nightly 版本中，实现 Ord 还会提供一个 <code>clamp()</code> 函数，用来比较类型是否在某个区间中。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#d3d0c8;">#![</span><span style="color:#f2777a;">feature</span><span style="color:#d3d0c8;">(clamp)]
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">assert!((-</span><span style="color:#f99157;">3</span><span style="color:#d3d0c8;">).</span><span style="color:#66cccc;">clamp</span><span style="color:#d3d0c8;">(-</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">) == -</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">assert!(</span><span style="color:#f99157;">0.</span><span style="color:#66cccc;">clamp</span><span style="color:#d3d0c8;">(-</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">) == </span><span style="color:#f99157;">0</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">assert!(</span><span style="color:#f99157;">2.</span><span style="color:#66cccc;">clamp</span><span style="color:#d3d0c8;">(-</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">, </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">) == </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span></code></pre>
]]>
        </content>
    </entry>
    
    <entry>
        <title>当我反对中医时，我在反对什么？</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/what-am-i-against-when-opposing-tcm"/>
        <published>2020-02-01T10:28:13Z</published>
        <updated>2025-03-09T02:41:33Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/what-am-i-against-when-opposing-tcm</id>
        <content type="html">
            <![CDATA[<p>一觉起来，双黄连可以「抑制」新型冠状病毒的新闻铺天盖地袭来，继而许多人连夜聚集排队购买双黄连的图片也出现在网路之上。究其源起，是一则来自人民日报的微博。</p>
<blockquote>
<p>【上海药物所、武汉病毒所联合发现：双黄连可抑制新型冠状病毒】31日从中国科学院上海药物所获悉，该所和武汉病毒所联合研究初步发现，中成药双黄连口服液可抑制新型冠状病毒。此前，上海药物所启动由蒋华良院士牵头的抗新型冠状病毒感染肺炎药物研究应急攻关团队，在前期SARS相关研究和药物发现成果基础上，聚焦针对该病毒的治疗候选新药筛选、评价和老药新用研究。双黄连口服液由金银花、黄芩、连翘三味中药组成。中医认为，这三味中药具有清热解毒、表里双清的作用。现代医学研究认为，双黄连口服液具有广谱抗病毒、抑菌、提高机体免疫功能的作用，是目前有效的广谱抗病毒药物之一。上海药物所长期从事抗病毒药物研究，2003年“非典”期间，上海药物所左建平团队率先证实双黄连口服液具有抗 SARS 冠状病毒作用，十余年来又陆续证实双黄连口服液对流感病毒（H7N9、H1N1、H5N1）、严重急性呼吸综合征冠状病毒、中东呼吸综合征冠状病毒具有明显的抗病毒效应。目前，双黄连口服液已在上海公共卫生临床中心、华中科技大学附属同济医院开展临床研究。（新华社）<a href="http://m.weibo.cn/2803301701/4466929533834665">微博链接&gt;&gt;</a></p>
</blockquote>
<p>此举很难不让人想起曾经所谓板蓝根可以「防治」SARS 的论述。然而时至今日，17 年过去了，依旧没有有关板蓝根实际疗效的临床验证。尽管常见各类不良反应的报道，并且缺乏科学的实验设计来验证其有效性和毒副作用，但你依然可以轻而易举地在药店买到这种药物并服用。<a href="https://zh.wikipedia.org/wiki/%E6%9D%BF%E8%93%9D%E6%A0%B9">相关资料&gt;&gt;</a></p>
<p>在展开对双黄连，板蓝根，或是所有中药和中成药是否有用，是否在科学的实验论证下证实其确有疗效并明确毒理，药理的质疑前，我想先来讲讲，在现代循证医学的科学背景下，一个药物（常说的「西药」）从被发明出来到大规模上市用于临床治疗，需要经过怎样的阶段，并在这些阶段中分别需要得到怎样的验证。</p>
<blockquote>
<p><strong>临床试验</strong>（英语：Clinical trial）是一种根据研究方案利用 <a href="https://zh.wikipedia.org/w/index.php?title=%E5%B7%B2%E4%B8%8A%E5%B8%82%E8%97%A5%E7%89%A9&amp;action=edit&amp;redlink=1">已上市药物</a> 或 <a href="https://zh.wikipedia.org/wiki/%E5%AE%89%E6%85%B0%E5%8A%91">安慰剂</a> 作为 <a href="https://zh.wikipedia.org/wiki/%E5%B0%8D%E7%85%A7%E7%B5%84">对照组</a> 的方式，对 <a href="https://zh.wikipedia.org/wiki/%E8%97%A5%E7%89%A9">药物</a> 或其他 <a href="https://zh.wikipedia.org/wiki/%E9%86%AB%E5%AD%B8">医学</a> 治疗在受试者身上进行比较测试的过程。在临床试验中，研究者要先决定所要测试的疗法，例如药物或装置，再决定用哪种疗法与它比较，以及须要找哪一类型的病人来作为测试对象。治疗用药物的话要证明它能有效延长病人的生命、减轻特定症状或降低不良事件之发生以改善病人生活品质。<a href="https://zh.wikipedia.org/wiki/%E8%87%A8%E5%BA%8A%E8%A9%A6%E9%A9%97">维基百科&gt;&gt;</a></p>
</blockquote>
<p>通常一个药物在被证明有效并安全上市之前后，需要经过这么几个阶段。</p>
<ol>
<li>动物试验</li>
<li>一期临床试验</li>
<li>二期临床试验</li>
<li>三期临床试验</li>
<li>四期临床试验</li>
</ol>
<p>动物试验无需多言，例如为了直接证明某种疫苗的有效性，显然不可能直接在人身上试验，而在动物身上试验，例如试验用猴子，就成了直接有效初步证明药物有效的手段，同时在这个阶段，实验人员也会收集很多数据以支持后期临床试验的开展。</p>
<p><a href="https://zh.wikipedia.org/w/index.php?title=%E2%85%A0%E6%9C%9F%E4%B8%B4%E5%BA%8A%E8%AF%95%E9%AA%8C&amp;action=edit&amp;redlink=1">Ⅰ期临床试验</a>也称临床药理和毒理作用试验期。其是对已通过临床前安全性和有效性评价的新药在人体上验证其安全性。即是根据预先设计的计量，从初始安全剂量开始，逐渐加大，观察人体对该种新药的耐受程度，以确定人体可接受而又不会导致 <a href="https://zh.wikipedia.org/w/index.php?title=%E6%AF%92%E5%89%AF%E5%8F%8D%E5%BA%94&amp;action=edit&amp;redlink=1">毒副反应</a> 发生的剂量大小。之后将进行多次给药试验，以确定适合于Ⅱ期临床试验所需的剂量和程序。同时，还必须进行人体的单剂量和多剂量的 <a href="https://zh.wikipedia.org/wiki/%E8%8D%AF%E5%8A%A8%E5%AD%A6">药动学</a> 研究，以为Ⅱ期临床试验提供合适的治疗方案。Ⅰ期临床试验通常由健康的 <a href="https://zh.wikipedia.org/wiki/%E5%BF%97%E6%84%BF%E8%80%85">志愿者</a> 参与。在 <a href="https://zh.wikipedia.org/wiki/%E6%8A%97%E7%99%8C%E8%97%A5%E7%89%A9">抗癌药物</a> 开发研究中也允许少数患者参与初步实验。一般而言，Ⅰ期临床试验总共需要试验10~80个病人。</p>
<p><a href="https://zh.wikipedia.org/w/index.php?title=%E2%85%A1%E6%9C%9F%E4%B8%B4%E5%BA%8A%E8%AF%95%E9%AA%8C&amp;action=edit&amp;redlink=1">Ⅱ期临床试验</a>也称临床治疗效果的初步探索试验。即是用较小总体的选定适应证的患者，对药物的疗效和安全性进行临床研究，其间将重点观察新药的治疗效果和 <a href="https://zh.wikipedia.org/wiki/%E4%B8%8D%E8%89%AF%E5%8F%8D%E5%BA%94">不良反应</a> 。同时，还要对新药的 <a href="https://zh.wikipedia.org/wiki/%E8%8D%AF%E5%8A%A8%E5%AD%A6">药动学</a> 和 <a href="https://zh.wikipedia.org/wiki/%E7%94%9F%E7%89%A9%E5%88%A9%E7%94%A8%E5%BA%A6">生物利用度</a> 方面进行研究，以确定患者与健康人的药动学差异。Ⅱ期临床试验的主要目的是为Ⅲ期临床试验做准备，以确定初步的 <a href="https://zh.wikipedia.org/w/index.php?title=%E4%B8%B4%E5%BA%8A%E9%80%82%E5%BA%94%E7%97%87&amp;action=edit&amp;redlink=1">临床适应症</a> 和治疗方案。Ⅱ期临床试验总共需要试验100-200个病人。对照组的病人愈多那便能更进一步找到非常见的副作用。</p>
<p><a href="https://zh.wikipedia.org/wiki/%E2%85%A2%E6%9C%9F%E4%B8%B4%E5%BA%8A%E8%AF%95%E9%AA%8C">Ⅲ期临床试验</a>也称治疗的全面评价临床试验。即是在对已通过Ⅱ期临床试验确定了其疗效的新药，与现有已知活性的药物或无药理活性的 <a href="https://zh.wikipedia.org/wiki/%E5%AE%89%E6%85%B0%E5%89%82">安慰剂</a> 进行 <a href="https://zh.wikipedia.org/wiki/%E5%B0%8D%E7%85%A7%E8%A9%A6%E9%A9%97">对照试验</a> 。该期试验对于患者的选择非常严格，其还必须具有明确的疗效标准和安全性评价标准。新药在经过对照试验后，将对其疗效和安全性进行全面的评价，以判断其是否具有 <a href="https://zh.wikipedia.org/w/index.php?title=%E6%B2%BB%E7%96%97%E5%AD%A6&amp;action=edit&amp;redlink=1">治疗学</a> 和安全性特征，这决定着其是否能够批准上市销售。Ⅲ期临床试验总共需要试验个300-500病人，最少要测试100次，否则统计学上会有误差，对照组的数量则无具体规定。</p>
<p><a href="https://zh.wikipedia.org/w/index.php?title=%E2%85%A3%E6%9C%9F%E4%B8%B4%E5%BA%8A%E8%AF%95%E9%AA%8C&amp;action=edit&amp;redlink=1">Ⅳ期临床试验</a>也称药物推出后的临床监察期。即是在新药推出后，通过大量调查药物对病人的临床效果及情况，监视新药有无效，如何最好地使用以及副作用的发生机会和程度。若疗效不理想或出现严重的副作用而且发生率较高，管制部门则会将那新药召回和退市。第4期临床试验会一直进行，只要仍有很多人用这种药物。</p>
<p>由于事关一个人，一个病人的生命健康和生活品质，药物的作用原理，毒副作用，有效性，安全性等等诸多因素均需要在临床试验中被科学的检验通过，方可上市造福广大患者。任何一个环节的疏忽，都有可能带来灾难性的后果。此时回看人民日报所言的最后一句话：「目前，双黄连口服液已在上海公共卫生临床中心、华中科技大学附属同济医院开展临床研究。」一个才刚刚开始进行临床研究的药物，为何就被冠之以可以「有效抑制」新型冠状病毒的名号？请问这其中的作用原理为何？实验设计为何？经过了怎样的检验？在药物有效性上仅仅给出如此模棱两可的回答，实在是让人难以接受。<a href="https://card.weibo.com/article/m/show/id/2309404466991703392332">丁香医生关于双黄连的相关辟谣&gt;&gt;</a></p>
<p>事实上，也有记者就此事采访询问了上海药物研究所，我摘出了其中几个回答，列举在下，完整的采访链接在此，建议阅读。<a href="https://card.weibo.com/article/m/show/id/2309404466967992991875">采访原文链接&gt;&gt;</a></p>
<pre style="background-color:#2d2d2d;"><code><span style="color:#d3d0c8;">问：双黄连这个事情是真的吗？
</span><span style="color:#d3d0c8;">答：对，有抑制作用是初步发现，初步发现对病毒有抑制。
</span><span style="color:#d3d0c8;">问：早期服用能控制病毒吗？早期服用会有好处吗？
</span><span style="color:#d3d0c8;">答：目前还没有这么详细的研究，因为我们只是在武汉病毒所做了一个初步的验证。
</span><span style="color:#d3d0c8;">问：可以抑制病毒的说法是准确的嘛？
</span><span style="color:#d3d0c8;">答：对对对，但也不能太拔高，因为这个科学的事情我们不想说得太过。
</span><span style="color:#d3d0c8;">问：目前还在临床研究阶段吗？
</span><span style="color:#d3d0c8;">答：是这样，我们后续会在上海市临床医学（研究）中心做一些实验，因为双黄连本身就是上市的药物，但是对病人如何有效，我们还要做大量的实验。
</span></code></pre>
<p>如此的专业性，怎能让人不质疑，怎能让人安心？</p>
<p>每当中医被质疑时，总有人会做出包括但不限于以下几个的反驳</p>
<ul>
<li>我的 XXX 病就是中药医好的，你怎么解释？</li>
<li>要是没有中医，你的老祖宗早就死完了，还哪来的你？</li>
<li>你说中医没用，说中医无效，那你倒是拿出证据来啊？</li>
<li>国家都在扶持中医，开设中医院校，中医医院，难道都是笑话吗？</li>
<li>…….</li>
</ul>
<p>很遗憾，上述的每一种说法都站不住脚，有大量的漏洞，鉴于篇幅限制我不想一一列举反驳，但是我想讲一讲我个人对中医到底是什么样的态度。</p>
<p>每当我们讲到中医时，总是不自觉的拿西医作比较，虽然我不想玩文字游戏，但不得不承认这种论述在表达上显然是有问题的。中医和西医的区别，绝不仅仅是地理上的区别，即所谓中国的，东方的医学和外国的，西方的医学。大多数时候，我们所言中医和所认识的中医是指中国古传统医学，是落后的，陈旧的，我固然不否认中医学科在当下所作的现代化，科学化之努力，但是难以否认的是中医依然是基于经验主义的医学，其基础理论为形而上的，缺乏科学理论支持，并难以实证的诸如五行脏象，气血经络之说。<a href="https://zh.wikipedia.org/wiki/%E4%B8%AD%E5%8C%BB%E5%AD%A6#%E5%9F%BA%E7%A4%8E%E7%90%86%E8%AE%BA">维基百科「中医学」定义&gt;&gt;</a></p>
<p>而我们常说的「西医」，其真名应该为循证医学，又称<strong>实证医学</strong>，是一种医学诊疗方法，强调应用完善设计与执行的研究证据将决策最佳化。无论是应用在医学教育，个人决策，适用于群体的指引和政策，还是一般健康服务的管理，循证医学都主张决策和政策皆应尽可能根据证据，而非单单依据从业人员、专家或管理者的信念。因此，它试图确保临床医师的意见（可能受限于知识差距或偏误），有基于科学文献的所有可用知识补足，保证服务为<a href="https://zh.wikipedia.org/wiki/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5">最佳诊疗</a> 。它提倡使用正式且明确的方法来分析证据，并提供给决策者。它推动课程向医学生、从业人员和决策者传授这个方法。<a href="https://zh.wikipedia.org/wiki/%E5%BE%AA%E8%AF%81%E5%8C%BB%E5%AD%A6">维基百科「循证医学」定义&gt;&gt;</a></p>
<p>任何一个生在 21 世纪的人，一个接受了良好科学教育，拥有基本科学素养，有基本批判性思维的理性人，都应该认识到中医理论在科学发展之下的局限性，并且主动拥护循证医学，我从来不反对接受中医使用中医，而是反对把中医和西医作为平起平坐，在当下中国分庭抗礼的两种医学；我从来不认为中药是完全无效的，而是反对在缺乏科学实证的情况下将其用于临床，甚至广泛宣扬；我从来不认为中医治疗手段是完全不可取的，而是反对放弃现代医学治疗手段而完全诉诸于前者；我从来不认为中医是没有意义的，而是反对不以科学的眼光重新审视中医之精华的论述。</p>
<p>关于最后一点我想有一个实例可以扩展，1969年－1972年间，中国科学家<a href="https://zh.wikipedia.org/wiki/%E5%B1%A0%E5%91%A6%E5%91%A6">屠呦呦</a>领导的 <a href="https://zh.wikipedia.org/wiki/523%E8%AF%BE%E9%A2%98%E7%BB%84">523 课题组</a> 发现并从 <a href="https://zh.wikipedia.org/wiki/%E9%BB%84%E8%8A%B1%E8%92%BF">黄花蒿</a> 中提取了青蒿素，使其成为现今所有药物中起效最快的抗<a href="https://zh.wikipedia.org/wiki/%E6%81%B6%E6%80%A7%E7%96%9F%E5%8E%9F%E8%99%AB">恶性疟原虫</a>疟疾药，而黄花蒿作为中草药治疗疟疾的手段在中医中古已有之。试想，还有多少草药中也许还有类似青蒿素一样可以造福人类的物质，以科学的手段发掘背后的原理，是否才是对中医，对现代医学合理的改进手段？</p>
<h2>推荐阅读&amp;聆听</h2>
<ul>
<li><a href="https://www.zhihu.com/question/21476991">中医是否是科学？应该如何看待中医？</a></li>
<li><a href="https://castro.fm/episode/Qq7OhD">太医来了播客 #123 再论中西医之争</a></li>
<li><a href="https://castro.fm/episode/nbiiiw">太医来了播客 #111 循证医学是目前解决医学问题的最好方法</a></li>
<li><a href="https://castro.fm/episode/UCESh5">太医来了播客 #85 我们的中医观</a></li>
</ul>
]]>
        </content>
    </entry>
    
    <entry>
        <title>Rust 中几个智能指针的异同与使用场景</title>
        <author>
            <name>JmPotato</name>
            <uri>https:&#x2f;&#x2f;ipotato.me</uri>
        </author>
        <link href="https:&#x2f;&#x2f;ipotato.me&#x2f;article/rust-smart-pointers-comparison-and-usage"/>
        <published>2020-01-16T18:30:57Z</published>
        <updated>2025-03-09T02:41:33Z</updated>
        <id>https:&#x2f;&#x2f;ipotato.me&#x2f;article/rust-smart-pointers-comparison-and-usage</id>
        <content type="html">
            <![CDATA[<p>想必写过 C 的程序员对指针都会有一种复杂的情感，与内存相处的过程中可以说是成也指针，败也指针。一不小心又越界访问了，一不小心又读到了内存里的脏数据，一不小心多线程读写数据又不一致了……我知道讲到这肯定会有人觉得“出这种问题还不是因为你菜”云云，但是有一句话说得好：“自由的代价就是需要时刻保持警惕”。</p>
<p>Rust 几乎把“内存安全”作为了语言设计哲学之首，从多个层面（编译，运行时检查等）极力避免了许多内存安全问题。所以比起让程序员自己处理指针（在 Rust 中可以称之为 Raw Pointer），Rust 提供了几种关于指针的封装类型，称之为智能指针（Smart Pointer），且对于每种智能指针，Rust 都对其做了很多行为上的限制，以保证内存安全。</p>
<ul>
<li>Box&lt;T&gt;</li>
<li>Rc&lt;T&gt; 与 Arc&lt;T&gt;</li>
<li>Cell&lt;T&gt;</li>
<li>RefCell&lt;T&gt;</li>
</ul>
<p>我在刚开始学习智能指针这个概念的时候有非常多的困惑，Rust 官方教程本身对此的叙述并不详尽，加之 Rust 在中文互联网上内容匮乏，我花了很久才搞清楚这几个智能指针封装的异同，在这里总结一下，以供参考，如有错误，烦请大家指正。</p>
<blockquote>
<p>以下内容假定本文的读者了解 Rust 的基础语法，所有权以及借用的基本概念，这里是<a href="https://kaisery.github.io/trpl-zh-cn/ch04-00-understanding-ownership.html">相关链接</a>。</p>
</blockquote>
<h1>Box&lt;T&gt;</h1>
<p><code>Box&lt;T&gt;</code> 与大多数情况下我们所熟知的指针概念基本一致，它是一段指向堆中数据的指针。我们可以通过这样的操作访问和修改其指向的数据：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Box::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);  </span><span style="color:#747369;">// Immutable
</span><span style="color:#d3d0c8;">println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, a);    </span><span style="color:#747369;">// Output: 1
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">let mut</span><span style="color:#d3d0c8;"> b = Box::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);  </span><span style="color:#747369;">// Mutable
</span><span style="color:#d3d0c8;">*b += </span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">;
</span><span style="color:#d3d0c8;">println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, b);    </span><span style="color:#747369;">// Output: 2
</span></code></pre>
<p>然而 <code>Box&lt;T&gt;</code> 的主要特性是单一所有权，即同时只能有一个人拥有对其指向数据的所有权，并且同时只能存在一个可变引用或多个不可变引用，这一点与 Rust 中其他属于堆上的数据行为一致。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Box::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);  </span><span style="color:#747369;">// Owned by a
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = a;  </span><span style="color:#747369;">// Now owned by b
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, a);  </span><span style="color:#747369;">// Error: value borrowed here after move
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> c = &amp;</span><span style="color:#cc99cc;">mut</span><span style="color:#d3d0c8;"> a;
</span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> d = &amp;a;
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#99cc99;">, </span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, c, d);  </span><span style="color:#747369;">// Error: cannot borrow `a` as immutable because it is also borrowed as mutable
</span></code></pre>
<h1>Rc&lt;T&gt; 与 Arc&lt;T&gt;</h1>
<p><code>Rc&lt;T&gt;</code> 主要用于同一堆上所分配的数据区域需要有多个只读访问的情况，比起使用 <code>Box&lt;T&gt;</code> 然后创建多个不可变引用的方法更优雅也更直观一些，以及比起单一所有权，<code>Rc&lt;T&gt;</code> 支持多所有权。</p>
<p><code>Rc</code> 为 Reference Counter 的缩写，即为引用计数，Rust 的 Runtime 会实时记录一个 <code>Rc&lt;T&gt;</code> 当前被引用的次数，并在引用计数归零时对数据进行释放（类似 Python 的 GC 机制）。因为需要维护一个记录 <code>Rc&lt;T&gt;</code> 类型被引用的次数，所以这个实现需要 Runtime Cost。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::rc::Rc;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Rc::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#99cc99;">count after creating a = </span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, Rc::strong_count(&amp;a));
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = Rc::clone(&amp;a);
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#99cc99;">count after creating b = </span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, Rc::strong_count(&amp;a));
</span><span style="color:#d3d0c8;">    {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> c = Rc::clone(&amp;a);
</span><span style="color:#d3d0c8;">        println!(&quot;</span><span style="color:#99cc99;">count after creating c = </span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, Rc::strong_count(&amp;a));
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#99cc99;">count after c goes out of scope = </span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, Rc::strong_count(&amp;a));
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>输出依次会是 1 2 3 2。</p>
<p>需要注意的主要有两点。首先， <code>Rc&lt;T&gt;</code> 是完全不可变的，可以将其理解为对同一内存上的数据同时存在的多个只读指针。其次，<code>Rc&lt;T&gt;</code> 是只适用于单线程内的，尽管从概念上讲不同线程间的只读指针是完全安全的，但由于 <code>Rc&lt;T&gt;</code> 没有实现在多个线程间保证计数一致性，所以如果你尝试在多个线程内使用它，会得到这样的错误：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::thread;
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::rc::Rc;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Rc::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    thread::spawn(|| {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = Rc::clone(&amp;a);
</span><span style="color:#d3d0c8;">        </span><span style="color:#747369;">// Error: `std::rc::Rc&lt;i32&gt;` cannot be shared between threads safely
</span><span style="color:#d3d0c8;">    }).</span><span style="color:#66cccc;">join</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>如果想在不同线程中使用 <code>Rc&lt;T&gt;</code> 的特性该怎么办呢？答案是 <code>Arc&lt;T&gt;</code>，即 Atomic reference counter。此时引用计数就可以在不同线程中安全的被使用了。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::thread;
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::sync::Arc;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Arc::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    thread::spawn(</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|| {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = Arc::clone(&amp;a);
</span><span style="color:#d3d0c8;">        println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, b);  </span><span style="color:#747369;">// Output: 1
</span><span style="color:#d3d0c8;">    }).</span><span style="color:#66cccc;">join</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<h1>Cell&lt;T&gt;</h1>
<p><code>Cell&lt;T&gt;</code> 其实和 <code>Box&lt;T&gt;</code> 很像，但后者同时不允许存在多个对其的可变引用，如果我们真的很想做这样的操作，在需要的时候随时改变其内部的数据，而不去考虑 Rust 中的不可变引用约束，就可以使用 <code>Cell&lt;T&gt;</code>。<code>Cell&lt;T&gt;</code> 允许多个共享引用对其内部值进行更改，实现了「内部可变性」。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> x = Cell::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> y = &amp;x;
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> z = &amp;x;
</span><span style="color:#d3d0c8;">    x.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    y.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">3</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    z.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">4</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#f99157;">{}</span><span style="color:#d3d0c8;">&quot;, x.</span><span style="color:#66cccc;">get</span><span style="color:#d3d0c8;">());  </span><span style="color:#747369;">// Output: 4
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>这段看起来非常不 Rust 的 Rust 代码其实是可以通过编译并运行成功的，<code>Cell&lt;T&gt;</code> 的存在看起来似乎打破了 Rust 的设计哲学，但由于仅仅对实现了 <code>Copy</code> 的 <code>T</code>，<code>Cell&lt;T&gt;</code> 才能进行 <code>.get()</code> 和 <code>.set()</code> 操作。而实现了 <code>Copy</code> 的类型在 Rust 中几乎等同于会分配在栈上的数据（可以直接按比特进行连续 n 个长度的复制），所以对其随意进行改写是十分安全的，不会存在堆数据泄露的风险（比如我们不能直接复制一段栈上的指针，因为指针指向的内容可能早已物是人非）。也是得益于 <code>Cell&lt;T&gt;</code> 实现了外部不可变时的内部可变形，可以允许以下行为的发生：</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::cell::Cell;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> a = Cell::new(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">    {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = &amp;a;
</span><span style="color:#d3d0c8;">        b.</span><span style="color:#66cccc;">set</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#f99157;">{:?}</span><span style="color:#d3d0c8;">&quot;, a);  </span><span style="color:#747369;">// Output: 2
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>如果换做 <code>Box&lt;T&gt;</code>，则在中间出现的 Scope 就会使 <code>a</code> 的所有权被移交，且在执行完毕之后被 <code>Drop</code>。最后还有一点，<code>Cell&lt;T&gt;</code> 只能在单线程的情况下使用。</p>
<h1>RefCell&lt;T&gt;</h1>
<p>因为 <code>Cell&lt;T&gt;</code> 对 <code>T</code> 的限制：只能作用于实现了 <code>Copy</code> 的类型，所以应用场景依旧有限（安全的代价）。但是我如果就是想让任何一个 <code>T</code> 都可以塞进去该咋整呢？<code>RefCell&lt;T&gt;</code> 去掉了对 <code>T</code> 的限制，但是别忘了要牢记初心，不忘继续践行 Rust 的内存安全的使命，既然不能在读写数据时简单的 <code>Copy</code> 出来进去了，该咋保证内存安全呢？相对于标准情况的静态借用，<code>RefCell&lt;T&gt;</code> 实现了运行时借用，这个借用是临时的，而且 Rust 的 Runtime 也会随时紧盯 <code>RefCell&lt;T&gt;</code> 的借用行为：同时只能有一个可变借用存在，否则直接 Painc。也就是说 <code>RefCell&lt;T&gt;</code> 不会像常规时一样在编译阶段检查引用借用的安全性，而是在程序运行时动态的检查，从而提供在不安全的行为下出现一定的安全场景的可行性。</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::cell::RefCell;
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::thread;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    thread::spawn(</span><span style="color:#cc99cc;">move </span><span style="color:#d3d0c8;">|| {
</span><span style="color:#d3d0c8;">       </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> c = RefCell::new(</span><span style="color:#f99157;">5</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">       </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> m = c.</span><span style="color:#66cccc;">borrow</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    
</span><span style="color:#d3d0c8;">       </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = c.</span><span style="color:#66cccc;">borrow_mut</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    }).</span><span style="color:#66cccc;">join</span><span style="color:#d3d0c8;">();
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Error: thread &#39;&lt;unnamed&gt;&#39; panicked at &#39;already borrowed: BorrowMutError&#39;
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>如上程序所示，如同一个读写锁应该存在的情景一样，直接进行读后写是不安全的，所以 <code>borrow</code> 过后 <code>borrow_mut</code> 会导致程序 Panic。同样，<code>ReCell&lt;T&gt;</code> 也只能在单线程中使用。</p>
<p>如果你要实现的代码很难满足 Rust 的编译检查，不妨考虑使用 <code>Cell&lt;T&gt;</code> 或 <code>RefCell&lt;T&gt;</code>，它们在最大程度上以安全的方式给了你些许自由，但别忘了时刻警醒自己自由的代价是什么，也许获得喘息的下一秒，一个可怕的 Panic 就来到了你身边！</p>
<h1>组合使用</h1>
<p>如果遇到要实现一个同时存在多个不同所有者，但每个所有者又可以随时修改其内容，且这个内容类型 <code>T</code> 没有实现 <code>Copy</code> 的情况该怎么办？使用 <code>Rc&lt;T&gt;</code> 可以满足第一个要求，但是由于其是不可变的，要修改内容并不可能；使用 <code>Cell&lt;T&gt;</code> 直接死在了 <code>T</code> 没有实现 <code>Copy</code> 上；使用 <code>RefCell&lt;T&gt;</code> 由于无法满足多个不同所有者的存在，也无法实施。可以看到各个智能指针可以解决其中一个问题，既然如此，为何我们不把 <code>Rc&lt;T&gt;</code> 与 <code>RefCell&lt;T&gt;</code> 组合起来使用呢？</p>
<pre style="background-color:#2d2d2d;"><code class="language-rust"><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::rc::Rc;
</span><span style="color:#cc99cc;">use </span><span style="color:#d3d0c8;">std::cell::RefCell;
</span><span style="color:#d3d0c8;">
</span><span style="color:#cc99cc;">fn </span><span style="color:#6699cc;">main</span><span style="color:#d3d0c8;">() {
</span><span style="color:#d3d0c8;">    </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> shared_vec: Rc&lt;RefCell&lt;_&gt;&gt; = Rc::new(RefCell::new(Vec::new()));
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Output: []
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#f99157;">{:?}</span><span style="color:#d3d0c8;">&quot;, shared_vec.</span><span style="color:#66cccc;">borrow</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">    {
</span><span style="color:#d3d0c8;">        </span><span style="color:#cc99cc;">let</span><span style="color:#d3d0c8;"> b = Rc::clone(&amp;shared_vec);
</span><span style="color:#d3d0c8;">        b.</span><span style="color:#66cccc;">borrow_mut</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">1</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">        b.</span><span style="color:#66cccc;">borrow_mut</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">2</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    }
</span><span style="color:#d3d0c8;">    shared_vec.</span><span style="color:#66cccc;">borrow_mut</span><span style="color:#d3d0c8;">().</span><span style="color:#66cccc;">push</span><span style="color:#d3d0c8;">(</span><span style="color:#f99157;">3</span><span style="color:#d3d0c8;">);
</span><span style="color:#d3d0c8;">    </span><span style="color:#747369;">// Output: [1, 2, 3]
</span><span style="color:#d3d0c8;">    println!(&quot;</span><span style="color:#f99157;">{:?}</span><span style="color:#d3d0c8;">&quot;, shared_vec.</span><span style="color:#66cccc;">borrow</span><span style="color:#d3d0c8;">());
</span><span style="color:#d3d0c8;">}
</span></code></pre>
<p>通过 <code>Rc&lt;T&gt;</code> 保证了多所有权，而通过 <code>RefCell&lt;T&gt;</code> 则保证了内部数据的可变性。</p>
<h1>参考</h1>
<ol>
<li><a href="https://manishearth.github.io/blog/2015/05/27/wrapper-types-in-rust-choosing-your-guarantees">Wrapper Types in Rust: Choosing Your Guarantees</a></li>
<li><a href="https://kaisery.github.io/trpl-zh-cn/ch15-05-interior-mutability.html">内部可变性模式</a></li>
<li><a href="https://rust.cc/article?id=37d1cb4f-5cc9-4adc-b41a-dbe4914bf4b5">如何理解Rust中的可变与不可变？</a></li>
<li><a href="https://prev.rust-lang.org/zh-CN/faq.html">Rust 常见问题解答</a></li>
</ol>
]]>
        </content>
    </entry>
    
</feed>