<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Random Thoughts</title><link>https://blog.joway.io/</link><description>100% human native.</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><managingEditor>Joway</managingEditor><webMaster>Joway</webMaster><lastBuildDate>Sat, 29 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.joway.io/index.xml" rel="self" type="application/rss+xml"/><item><title>尽职编程：AI Coding 时代的个体产出差异的来源</title><link>https://blog.joway.io/posts/software-engineering-with-ai-coding/</link><pubDate>Sat, 29 Aug 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/software-engineering-with-ai-coding/</guid><category>Thought</category><description>这一年多的时间以来，AI Coding 已经从一个新潮的编程方式变成了默认方式，每个人都用着近乎相似的模型，相似的设备，相似的 Agent，理论上所有人产出的代码水平也应该相似，但如果你在一线工作过，你会发现实际情况恰恰相反，大家的代码产出水平方差反而比以往更大。这是一个相当有意思的话题，但是我却发现在互联网上鲜有人讨论。</description><content:encoded>&lt;p&gt;这一年多的时间以来，AI Coding 已经从一个新潮的编程方式变成了默认方式，每个人都用着近乎相似的模型，相似的设备，相似的 Agent，理论上所有人产出的代码水平也应该相似，但如果你在一线工作过，你会发现实际情况恰恰相反，大家的代码产出水平方差反而比以往更大。这是一个相当有意思的话题，但是我却发现在互联网上鲜有人讨论。&lt;/p&gt;
&lt;p&gt;为什么大家明明用着一样的编程方法，但是产出质量却会有如此大的差异？特别是当我反思自己每天跟 AI 的对话的时候，我觉得没有任何技术含量，我既没有什么魔法的提示词，也没有装什么花里胡哨的 Skills，纯粹就是把 AI 当作一个伙伴一起协作完成工作。我认为每个人的工作方式也都是如此，但是为什么还会存在个体差异？&lt;/p&gt;
&lt;p&gt;在研究这个问题许久后，我认为我找到了一个很少有人讨论，但是在我看来是 AI Coding 核心要义的工作方法，我把它叫做「尽职编程」，用更口语化的表述就是「得靠谱」。&lt;/p&gt;
&lt;p&gt;如果你和 HR，物业，政府机关等社会人员打交道多了，你就会知道在这个社会，要求这些行政人员帮你办成一件事有多难。但是我相信每个人的社交圈都会存在一种特能帮你办成事的能人，在古代，他们有另一种名字，叫做乡绅或者师爷，他们的工作就是两个字「办事」。需要注意的是，这类人自己往往并没有具体的专业知识，事情也多半不是他们自己执行的，但是他们就是有能力让别人高高兴兴把事情办成了。&lt;/p&gt;
&lt;p&gt;在我看来，AI Coding 需要的就是这种能力。这个时代由于编程的速度极大地提升，工作分配的颗粒度也大大加粗，上一个时代每个人分到的活可能只是写某个模块甚至是某个函数，你不需要关心最后大家工作内容合并做成的事情是什么。但是如今更多时候是要让你把某件事情做成。而做成一件事情可能分一百个子事情，AI 有能力做完每件子事情，但是 AI 没有能力决定要做哪一百件子事情，这个的上下文可能在老板与你的沟通中，也可能存在你历史积累的工作经验里，还可能是你生活的常识，甚至还可能要预测未来的发展。而 AI 完全没办法通过你聊天窗口那几句话知道这么多上下文信息。&lt;/p&gt;
&lt;p&gt;我在上一家公司接触过一种我觉得最可怕的人，甚至我都一度想申请让公司禁止他写代码，那就是他会让 AI 来决定，做成某件事情，需要做哪一百件事情，然后让 AI 全完成了。这就引出了今天的主题，那就是「尽职编程」。在我看来，他最大的错误就是没有做到「尽职」。坦率讲，AI 在自主决策的时候，正确率其实也不低，但绝对没有达到 100% 的地步。你和 AI 说要写一个文章编辑功能，如果不提供其他信息，AI 会自己决策是实时保存还是点击按钮保存，编辑框是固定大小还是会随着内容变长。但是 AI 依然会漏掉一些细节，比如多人同时编辑会如何。因为你给他的提示词就几个字符，他只会把它当简单的需求完成，况且如果 AI 思考过多可能还有过度设计的嫌疑。这时候，非常依赖人在其中多思考，多反问 AI，更重要的是，虽然代码不是你写的，但是你要能够回答得出在技术评审的时候别人的提出 —— 这是我认为的达到了「尽职编程」的标准。&lt;/p&gt;
&lt;p&gt;如果只是为了实现需求，我甚至感觉在 AI Coding 时代到来后，我每天只需要工作 2 小时。但我其他时间其实往往是花在了「理解 AI 的设计」上。我会通过让 AI 画架构图，设计文档的方式，来理解 AI 写出来的代码而不是阅读代码本身，我甚至有一个百试不爽的魔法提示词是让 AI 写一个「面向产品经理」的文档，这样甚至连枯燥的技术语言都不需要阅读就能理解 AI 的设计了，还能把这个文档丢给产品经理让其他人也能理解系统的实现。&lt;/p&gt;
&lt;p&gt;但「尽职」是一个主观行为，且没有尽头。什么样的需求需要尽职到什么程度才够，这完全凭个人自己的理解。比如近期我写的一个用来评测 Agent 质量的系统， 我就没做太多「尽职」工作，因为他的代码并非核心代码，且极容易推翻重改。但如果你要写一个 Agent 的核心代码，尽职就没有止境了，它不仅仅在你提交代码之前要 review，每过一段时间，随着代码的持续演进，还需要反复对系统进行「回归尽职调查」，才能对系统有最新的认识。而这些工作内容，不会产出可看见的工作结果，做与不做，全凭个人，做了也不一定有立竿见影的效果，不见得每家公司都能容纳这种工作方式尤其是大公司那种不懂行的中层领导的团队里。&lt;/p&gt;
&lt;p&gt;尽管如此，也还是有一些方法可以在 AI Coding 工作流里，注入一些客观工具，鼓励这种「尽职精神」在公司内开枝散叶。比如我最近在我们的主仓库加了一个 Codex 自动 Review 代码的插件。注意，这个不是网上那些浅显的看几行代码评价几个无关痛痒问题的那种插件。而是会把代码库 clone 下来，结合代码库看修改本身产生的所有副作用，然后再给出 review 意见。而它的 Agent 上下文，完全独立用户 AI Coding Agent 的上下文，所以它会评价的非常客观。如果你就是执意要在一个 Get 接口修改数据，Claude Code 会因为你的命令而偏袒让你这么干，Review 的 Agent 不知道你的固执，所以依然不会让你这么干。由于你每一次修改都会有 Review 意见产生，你就会被迫进行多轮修改，或者你要直说你不同意 Review 意见。而且 github 上能看得到你的「尽职」过程，review 你代码的人本质上也是在 review 你的尽职过程。这就把主观的决策变成了客观可见的努力。我的体验下来，这个 Review Agent 不知道是不是因为提示词的原因已经有点演化成 Review 喷子了，但是在这个每个人都被 AI 阿谀奉承的时代需要喷子。&lt;/p&gt;
&lt;p&gt;当我发现了这种在同样的工具下，产生不同的个体差异的现象时，说实话我有点松了一口气，至少 AI 短期内还没有那么容易能替代我的工作。我们和 AI 的区别在于，我们想把事情干成，AI 只想让这个对话完成，而干成事情的能力，在历史的长河里一直是稀缺品，从来没有因为工具的进步而被取代过。&lt;/p&gt;</content:encoded></item><item><title>大公司与创业公司的 AI Coding 体验差异</title><link>https://blog.joway.io/posts/ai-coding-in-big-and-startup-company/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/ai-coding-in-big-and-startup-company/</guid><category>Thought</category><description>我在 AI Coding 能力达到生产级水平后，先后在一家世界500强大公司和一家创业公司任职，有意思的是，我个人对待 AI Coding 的态度和实践在这两家公司截然不同。
在大公司的时候，由于我们在负责一个直接和商品定价有关系的重要系统，且系统已经历经了十几年的持续迭代，所以我对我自己和同事提交的每一行代码都要求极高，不管这行代码是不是 AI 写的，写代码的人必须要看过且看懂了每一行代码在做什么。如果有一个家伙提交了一大片根本无法人工审查的代码，我会对这个家伙非常生气，并认为这是对这个系统和其他同事的不尊重。在这家公司，AI Coding 对整体工作效率的提升其实微乎其微，三分之一的时间在开会对齐对需求的理解，三分之一的时间在处理审批之类的各类杂活，剩下的三分之一时间也不在写代码而在验证代码，真正的写代码时间微乎其微。</description><content:encoded>&lt;p&gt;我在 AI Coding 能力达到生产级水平后，先后在一家世界500强大公司和一家创业公司任职，有意思的是，我个人对待 AI Coding 的态度和实践在这两家公司截然不同。&lt;/p&gt;
&lt;p&gt;在大公司的时候，由于我们在负责一个直接和商品定价有关系的重要系统，且系统已经历经了十几年的持续迭代，所以我对我自己和同事提交的每一行代码都要求极高，不管这行代码是不是 AI 写的，写代码的人必须要看过且看懂了每一行代码在做什么。如果有一个家伙提交了一大片根本无法人工审查的代码，我会对这个家伙非常生气，并认为这是对这个系统和其他同事的不尊重。在这家公司，AI Coding 对整体工作效率的提升其实微乎其微，三分之一的时间在开会对齐对需求的理解，三分之一的时间在处理审批之类的各类杂活，剩下的三分之一时间也不在写代码而在验证代码，真正的写代码时间微乎其微。&lt;/p&gt;
&lt;p&gt;然而后来我来到了一家创业公司，我自己变成了那个每天提交大量人力无法 review 的代码量改动的家伙，但于此同时我对改动的自信却不比之前在大公司谨小慎微的时候少。我会通过让 Agent 画流程图，写文档等多种方式帮助我去理解产出的代码，并以一目十行的速度大致检查下产出代码又没有特别让人意外的片段。&lt;/p&gt;
&lt;p&gt;在大公司，个人对公司成长发挥的作用微乎其微，可以说基本为零。甚至个人对团队的贡献，有时候都是微不足道的。很多团队的存在意义，是维持这个系统的/正常运转，而不是创新。相反，如果你因为创新而引发了事故，那么你对团队的伤害往往远大于贡献。特别是现在 AI 放大了个人的能力，也放大了破坏力，一个不靠谱的人，现在要捅出超大篓子的可能性远大于以往，基于此，最适合团队正常发展的策略就是谨慎谨慎再谨慎，不然我很怀疑你作为团队负责人每天看着大伙儿的编程速度大跃迁能否晚上睡得着觉。&lt;/p&gt;
&lt;p&gt;而在创业公司，每个人摊上的是大公司几个团队的活，如果不充分利用 AI 的 Coding 速度，根本不可能干完日常的工作。况且创业公司要么没赚钱要么赚的钱不多，而且创业公司的价值在于未来能赚多少钱而不是这个季度赚了多少钱，在这种情况下，走得快比走路姿势稳不稳重要的多。这也是我觉得现在在创业公司的编程体验远超大公司的一点。对于一份工作来说，我们当然要保证工作的完成质量，但因为大公司的代码量之大和审批流程之复杂，保证质量这件工作往往相当之无聊。如果环境允许你充分发挥自己的主观能动性，在质量与速度之间，发挥自己的审美和判断力来做取舍，这种工作的创造感其实远高于传统人工编码的时代，但也对实操人的素质提出了更高的要求。&lt;/p&gt;</content:encoded></item><item><title>LLM 训练与推理的基本理解</title><link>https://blog.joway.io/posts/deep-into-llm/</link><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-llm/</guid><category>Tech</category><description>学习一个技术最好的方式就是能够写一片文章把这个技术的原理解释清楚，本文记录了我在阅读 《Build a Large Language Model (From Scratch)》一书以及和 Claude Code 对话过程中的笔记，仅供参考。
术语解释 向量点积 定义：向量点积为标量
a = (a1, a2, a3) b = (b1, b2, b3) a · b = a1*b1 + a2*b2 + a3*b3 几何意义：
a · b = |a| |b| cos(theta) 其中 theta 是两个向量的夹角。</description><content:encoded>&lt;p&gt;学习一个技术最好的方式就是能够写一片文章把这个技术的原理解释清楚，本文记录了我在阅读 《Build a Large Language Model (From Scratch)》一书以及和 Claude Code 对话过程中的笔记，仅供参考。&lt;/p&gt;
&lt;h2 id="术语解释"&gt;术语解释&lt;/h2&gt;
&lt;h3 id="向量点积"&gt;向量点积&lt;/h3&gt;
&lt;p&gt;定义：向量点积为标量&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a = (a1, a2, a3)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b = (b1, b2, b3)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a · b = a1*b1 + a2*b2 + a3*b3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;几何意义：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a · b = |a| |b| cos(theta)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中 theta 是两个向量的夹角。&lt;/p&gt;
&lt;p&gt;用途：衡量向量相似度&lt;/p&gt;
&lt;h3 id="矩阵转置"&gt;矩阵转置&lt;/h3&gt;
&lt;p&gt;符号表示：Kᵀ&lt;/p&gt;
&lt;p&gt;定义：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A =
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 2 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4 5 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Aᵀ =
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3 6
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="线性层"&gt;线性层&lt;/h3&gt;
&lt;p&gt;假设某个词的向量是 2 个数字：x = [2, 5]。我要 3 个输出，就准备 3 个配方（权重和偏置都是训练学出来的数字）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方1： 1×x₁ + 0×x₂ + 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方2： 0.5×x₁ + 2×x₂ + 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方3： −1×x₁ + 1×x₂ + 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;代入 x = [2, 5]：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输出₁ = 1×2 + 0×5 + 0 = 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输出₂ = 0.5×2 + 2×5 + 1 = 1 + 10 + 1 = 12
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输出₃ = −1×2 + 1×5 + 0 = 3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结果：[2, 5] → [2, 12, 3]。 一个线性层的全部工作。&lt;/p&gt;
&lt;p&gt;用途：对向量升维或者降维。&lt;/p&gt;
&lt;h3 id="softmax"&gt;Softmax&lt;/h3&gt;
&lt;p&gt;softmax 把&amp;quot;一排任意分数&amp;quot;变成&amp;quot;一个概率分布&amp;quot;：每个值都在 0~1 之间，且全部加起来 = 1。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; e^(xᵢ)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;softmax(xᵢ) = ─────────────
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Σ e^(xⱼ)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="layernorm"&gt;LayerNorm&lt;/h3&gt;
&lt;p&gt;LayerNorm 是 Transformer Block 的常见操作。向量在几十层里被反复加加乘乘（注意力、MLP、残差……），里面的数字很容易漂移——有的飙得很大、有的接近 0、整组忽大忽小。这会让训练不稳定（梯度爆炸/消失、loss 乱跳、训不动）。&lt;/p&gt;
&lt;p&gt;LayerNorm 的作用：每过一个子层前，把这组数字&amp;quot;重新校准&amp;quot;回一个稳定、统一的范围。&lt;/p&gt;
&lt;p&gt;算法：对一个 token 的数字（n_embd 个），做三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;去均值：算出这组数的平均值，每个数减掉它 → 平均值变成 0（居中）&lt;/li&gt;
&lt;li&gt;除标准差：算出这组数的标准差，每个数除以它 → 波动幅度变成 1（统一尺度）（为防止除 0，分母会加一个极小的 ε）。
做完①②，向量被&amp;quot;标准化&amp;quot;成：均值≈0，波动≈1&lt;/li&gt;
&lt;li&gt;乘可学习的缩放 γ、加可学习的偏移 β：让模型自己学要不要、以及怎么把尺度调回去——不强制死守0/1，而是&amp;quot;先归一化，再让模型自己微调到最合适&amp;quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同一个 LayerNorm 会对不同 token 都套用一遍。&lt;/p&gt;
&lt;h2 id="文本编码"&gt;文本编码&lt;/h2&gt;
&lt;h3 id="tokenize"&gt;Tokenize&lt;/h3&gt;
&lt;p&gt;与常规的分词不同的是，对于例如空格之类的特殊字符，需要取决于特殊场景决定是否过滤。比如对 Python 代码，空格有独特语意，不应该被剔除。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;This is a example.&amp;#34; =&amp;gt; [&amp;#34;This&amp;#34;, &amp;#34;is&amp;#34;, &amp;#34;a&amp;#34;, &amp;#34;example&amp;#34;, &amp;#34;.&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="token-id"&gt;Token ID&lt;/h3&gt;
&lt;p&gt;训练集中的每个词都需要有一个 Token ID 来数值化。有多种不同方法可以来数值化训练集词汇。&lt;/p&gt;
&lt;h4 id="简单统计"&gt;简单统计&lt;/h4&gt;
&lt;p&gt;遍历训练集分词后的词表，排序后，给每个词设置一个 token id。&lt;/p&gt;
&lt;p&gt;一般还需要在不相关的文本之间插入一个 &amp;lt;|endoftext|&amp;gt; 标记。这有助于让 LLM 理解文本间是无关的。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;This&amp;#34; =&amp;gt; 40134
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;is&amp;#34; =&amp;gt; 2052
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;a&amp;#34; =&amp;gt; 133
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;example&amp;#34; =&amp;gt; 389
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;.&amp;#34; =&amp;gt; 12
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &amp;#34;&amp;lt;|endoftext|&amp;gt;&amp;#34; =&amp;gt; 50000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="byte-pair-encoding-bpe"&gt;Byte pair encoding (BPE)&lt;/h4&gt;
&lt;p&gt;简单基于训练集分词结果建立词表有一个缺点是无法处理例如 &amp;ldquo;xasdasd&amp;rdquo; 这类未知词汇。BPE 编码方法可以解决这个问题。&lt;/p&gt;
&lt;p&gt;假设语料库只有：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;see 出现 3 次
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set 出现 1 次
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;基础的字符集合为：[&amp;rsquo;e&amp;rsquo;, &amp;rsquo;s&amp;rsquo;, &amp;rsquo;t&amp;rsquo;]，所以初始词表为:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;e&amp;#34; → 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;s&amp;#34; → 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;t&amp;#34; → 3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第 1 轮：把所有挨着的字母对，数一数（要乘以这个词出现的次数）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌────────────┬────────────────────────────┬───────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 挨着的一对 │ 在哪出现 │ 总次数 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────────────┼────────────────────────────┼───────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ s e │ see 里有(×3)、set 里有(×1) │ 4 ✅ 最多 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────────────┼────────────────────────────┼───────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ e e │ see 里有(×3) │ 3 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────────────┼────────────────────────────┼───────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ e t │ set 里有(×1) │ 1 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└────────────┴────────────────────────────┴───────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;s e 出现 4 次最多 → 把 s e 粘成一个新块，叫它 se。&lt;/p&gt;
&lt;p&gt;文字变成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;see → [se] e ×3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set → [se] t ×1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第 2 轮：再数一次&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌────────────┬───────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 挨着的一对 │ 总次数 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────────────┼───────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ se e │ 3 ✅ 最多 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────────────┼───────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ se t │ 1 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└────────────┴───────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文字变成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;see → [see] ×3 ← 整个词变成 1 个块了！
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;set → [se] t ×1 ← 2 个块
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;于是我们有了一张完整的对照表（词表 vocab）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────────┬────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ token id │ 代表的文字 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 1 │ e │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 2 │ s │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 3 │ t │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 4 │ se │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 5 │ see │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────┴────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个词表能够 encode 未知的单词，比如 &amp;ldquo;tee&amp;rdquo; 对应的是 [3,1,1] 。&lt;/p&gt;
&lt;h3 id="token-embedding"&gt;Token Embedding&lt;/h3&gt;
&lt;p&gt;有了 Token ID 后，还需要把一个 Token ID 转成一个向量，因为向量间有距离，距离便可以表示两个词间词意的相似度。比如 &amp;lsquo;dog&amp;rsquo; 和 &amp;lsquo;cat&amp;rsquo; 虽然字符差别很大但是距离就很近，但是 &amp;lsquo;cat&amp;rsquo; 和 &amp;lsquo;car&amp;rsquo; 虽然字符相似但是距离很远。这是 Token ID 所做不到的。&lt;/p&gt;
&lt;p&gt;一开始词表都是随机数，预训练时，这个词表也会被作为参数训练。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────────┬────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ token id │ 向量 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 1 │ vector_A │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 2 │ vector_B │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 3 │ vector_C │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ... │ ... │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────┴────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="positional-embedding"&gt;Positional Embedding&lt;/h3&gt;
&lt;p&gt;上面训练的向量没有位置相关的信息，然而词的意思和位置有直接的关系。&lt;/p&gt;
&lt;p&gt;例如两句话：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;狗 咬 人
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;人 咬 狗
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在词向量嵌入后得到:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;狗&amp;#34;→[0.8, 0.9] &amp;#34;咬&amp;#34;→[0.1,-0.9] &amp;#34;人&amp;#34;→[0.5, 0.3]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两次话词嵌入得到的向量相同，只是顺序不同。而 Transformer 是并行处理每一个向量，所以会丢失位置信息。所以位置信息也必须嵌入到词向量中去。&lt;/p&gt;
&lt;p&gt;建立一个位置向量表，注意，这个位置表一开始数字随机，也是作为参数被训练出来的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─────────────┬────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 位置 │ 位置向量 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 第 1 个位置 │ [0.0, 0.1] │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 第 2 个位置 │ [0.0, 0.5] │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────┼────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 第 3 个位置 │ [0.0, 0.9] │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─────────────┴────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;位置向量表的行数等于最大上下文长度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新向量 = 词向量 + 它所在位置的向量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;句子 A：「狗 咬 人」&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;狗(第1位): [0.8, 0.9] + [0.0, 0.1] = [0.8, 1.0]&lt;/li&gt;
&lt;li&gt;咬(第2位): [0.1,-0.9] + [0.0, 0.5] = [0.1,-0.4]&lt;/li&gt;
&lt;li&gt;人(第3位): [0.5, 0.3] + [0.0, 0.9] = [0.5, 1.2]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;句子 B：「人 咬 狗」&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人(第1位): [0.5, 0.3] + [0.0, 0.1] = [0.5, 0.4]&lt;/li&gt;
&lt;li&gt;咬(第2位): [0.1,-0.9] + [0.0, 0.5] = [0.1,-0.4]&lt;/li&gt;
&lt;li&gt;狗(第3位): [0.8, 0.9] + [0.0, 0.9] = [0.8, 1.8]&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="前向推理"&gt;前向推理&lt;/h2&gt;
&lt;h3 id="自注意力"&gt;自注意力&lt;/h3&gt;
&lt;p&gt;上一部分，我们最终得到了每个词自己的意思和位置信息，但是不知道上下文信息。自注意力机制就是为了解决上下文的问题。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;我吃了一个苹果&amp;#34; → 这里苹果是水果
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;我买了一台苹果&amp;#34; → 这里苹果是手机/公司
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了解决这个问题，我们需要引入 Q(Query)/K(Key)/V(Value) 三个向量，每个词的向量（n_embd 维），分别乘以三个权重矩阵 Wq、Wk、Wv（所有 token 用的是同一套矩阵，形状为 n_embd × n_embd ），就得到它自己的 Q、K、V （形状 n_embd × n_embd）。Wq、Wk、Wv 一开始随机，也是作为参数被训练的。&lt;/p&gt;
&lt;p&gt;处理一个词时，做这 5 步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;算相关度分数：用我的 Q 去和每个词的 K 做点积（点积大 = 方向接近 = 相关）。&lt;/p&gt;
&lt;p&gt;分数(我, 某词) = 我的Q · 某词的K&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缩放：除以 √d（d 是向量长度）。
目的：向量越长，点积数值越大，会让后面 Softmax 变得极端（几乎只剩一个词
100%）。除一下把数值压回合理范围，训练更稳。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加因果掩码：把&amp;quot;后面的词&amp;quot;的分数设成 −∞。
因为生成时后面的词还没出现，不能偷看未来。设成 −∞，下一步 Softmax 后它的占比就是 0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Softmax：把这一行分数变成加起来 = 1 的占比（注意力权重）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按占比加权求和各词的 V：得到这个词最终吸收到的信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程就是这一个著名公式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Attention(Q, K, V) = softmax( Q·Kᵀ / √d ) · V
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ 谁多重要(占比) ───┘ └─取走对应内容─┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Kᵀ 为矩阵转置
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="kv-cache"&gt;KV Cache&lt;/h4&gt;
&lt;p&gt;在 LLM 推理的过程中，当仅新增了 Token 时，前面 Token 所有的 K/V 以及对应的 Attention 输出都不需要重新算。原因是因为因果掩码的存在：第 i 个 token 只能看 ≤ i 的位置。后面新增第 j 个 token（j &amp;gt; i），第 i 个 token 根本看不到它，所以第 i 个词的注意力结果不会因为后面追加了新词而改变。&lt;/p&gt;
&lt;p&gt;每个 Transformer Block 各有一份独立的缓存（因为每层 Wk/Wv 不同，K/V也不同）。&lt;/p&gt;
&lt;h3 id="多头自注意力"&gt;多头自注意力&lt;/h3&gt;
&lt;p&gt;单个注意力，做一次&amp;quot;打分→softmax→加权混合&amp;quot;，只能捕捉一种关系。但一句话里，词之间的关系是好几种同时存在的。例如：&amp;ldquo;小明 把 书 给 了 她 ， 因为 它 很 有趣&amp;rdquo;&lt;/p&gt;
&lt;p&gt;要真正理解它，模型得同时关注好几条线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法关系：&amp;ldquo;给&amp;quot;要关注 谁给(小明)、给什么(书)、给谁(她)&lt;/li&gt;
&lt;li&gt;指代关系：&amp;ldquo;它&amp;quot;指的是&amp;quot;书&amp;rdquo;；&amp;ldquo;她&amp;quot;指某个人&lt;/li&gt;
&lt;li&gt;话题关系：&amp;ldquo;有趣&amp;quot;和&amp;quot;书&amp;quot;是相关的话题词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个注意力头忙不过来——它做一次加权混合，只能照顾到其中一种。这就是瓶颈。&lt;/p&gt;
&lt;p&gt;多头注意力把每个词的向量切成 h 段，每一段交给一个&amp;quot;头&amp;rdquo;，每个头独立、并行地做一遍完整的 Q/K/V 注意力，最后把 h 个头的结果拼回来，再融合一下。&lt;/p&gt;
&lt;p&gt;举例说明：&lt;/p&gt;
&lt;p&gt;假设 n_embd = 8（每个词 8 个数字），n_head = 4（4 个头）。则每个头分到 8 / 4 = 2 个数字（这叫 head_size）。&lt;/p&gt;
&lt;p&gt;第1步：把每个词的 8 维向量，切成 4 段、每段 2 维&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;词向量: [ a₁ a₂ | b₁ b₂ | c₁ c₂ | d₁ d₂ ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 头1 头2 头3 头4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第2步：每个头，在自己那 2 维上，独立做完整的注意力（各自算各自的 Q·K → 各自的 softmax 占比 → 各自加权混合 V）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头1（比如学到&amp;quot;语法&amp;rdquo;）：算出它自己的一套注意力占比和结果&lt;/li&gt;
&lt;li&gt;头2（比如学到&amp;quot;指代&amp;rdquo;）：算出另一套完全不同的占比和结果&lt;/li&gt;
&lt;li&gt;头3、头4：各管各的关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ 每个头都吐出一个 2 维的结果。&lt;/p&gt;
&lt;p&gt;第3步：把 4 个头的结果拼回成 8 维&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ 头1结果(2) | 头2结果(2) | 头3结果(2) | 头4结果(2) ] → 8 维
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第4步：再过一个线性层（输出投影）把这 8 维&amp;quot;融合&amp;quot;一下，让各头信息互通，得到这个词最终的注意力输出。输出投影是一个可学习的线性层（一个权重矩阵 Wo 做矩阵乘法，Wo 是训练学出来的参数）。它的作用：让输出的每一个数字，都能用上&amp;quot;所有头&amp;quot;的信息——把各头的结果重新打散、加权、混合成一个连贯的整体向量。输出投影就来决定每个头各占多少权重。&lt;/p&gt;
&lt;h3 id="残差连接"&gt;残差连接&lt;/h3&gt;
&lt;p&gt;注意力算出来的结果，不是直接替换原向量，而是加回到原向量上：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;新向量 = 原向量 + 注意力的输出
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把注意力的产出，看成是&amp;quot;根据上下文，给这个词的一点修改建议/补充信息&amp;quot;。&lt;/p&gt;
&lt;p&gt;举例来说：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;苹果的原向量: [0.8, 0.9]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;多头注意力算出来的结果: [0.1, -0.4]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ───────────── ← 对应位置相加
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;残差相加后的新向量： [0.9, 0.5]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="mlp-前馈网络"&gt;MLP (前馈网络)&lt;/h3&gt;
&lt;p&gt;MLP = 两个线性层，中间夹一个 GELU。注意力是&amp;quot;词之间交流&amp;quot;；MLP 是&amp;quot;每个词，自己单独，再深加工一遍&amp;quot;。它内部就三步：升维 → GELU → 降维。&lt;/p&gt;
&lt;ol start="0"&gt;
&lt;li&gt;
&lt;p&gt;初始向量&lt;/p&gt;
&lt;p&gt;某个词（注意力+残差之后）的向量，2 个数字：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x = [2, 1]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第一步：升维线性层（一组线性层，2 个输入 → 4 个输出）&lt;/p&gt;
&lt;p&gt;4 个配方（权重+偏置，都是训练学来的）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方h₁： 1×x₁ + 0×x₂ + 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方h₂： 0×x₁ + 3×x₂ + (−1)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方h₃： −2×x₁ + 1×x₂ + 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方h₄： 1×x₁ + 1×x₂ + (−5)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;代入 x = [2, 1]：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;h₁ = 1×2 + 0×1 + 0 = 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;h₂ = 0×2 + 3×1 − 1 = 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;h₃ = −2×2 + 1×1 + 0 = −3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;h₄ = 1×2 + 1×1 − 5 = −2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;升维结果：[2, 2, −3, −2]（2 个数字 → 变 4 个，每个是输入的一种加权组合）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二步：GELU（对每个数字单独套，软开关）&lt;/p&gt;
&lt;p&gt;正数基本保留，负数压向 0。对照表：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────┬─────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 输入 │ GELU 后 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼─────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 2 │ ≈ 2.0 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼─────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ −2 │ ≈ 0.0 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────┼─────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ −3 │ ≈ 0.0 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────┴─────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;逐个套 [2, 2, −3, −2]：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[2, 2, −3, −2] → [2.0, 2.0, 0.0, 0.0]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ h₃、h₄ 是负的，被 GELU &amp;ldquo;关掉&amp;quot;了（变 ~0）；h₁、h₂ 是正的，&amp;ldquo;放行&amp;rdquo;。这就是&amp;quot;软开关&amp;rdquo;，也是MLP 里唯一带非线性、让模型能学复杂规律的地方（没它，两个线性层会塌缩成一个）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三步：降维线性层（另一组&amp;quot;配方&amp;quot;，4 个输入 → 回到 2 个输出）&lt;/p&gt;
&lt;p&gt;2 个配方：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方y₁： 1×g₁ + 1×g₂ + 1×g₃ + 1×g₄ + 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;配方y₂： 2×g₁ + (−1)×g₂ + 0×g₃ + 0×g₄ + 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;代入 g = [2.0, 2.0, 0.0, 0.0]：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;y₁ = 1×2.0 + 1×2.0 + 1×0 + 1×0 = 4.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;y₂ = 2×2.0 + (−1)×2.0 + 0 + 0 = 2.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MLP 的最终输出：[4.0, 2.0]&lt;/p&gt;
&lt;p&gt;整条路： [2,1] → 升维 [2,2,−3,−2] → GELU [2,2,0,0] → 降维 [4.0, 2.0]。&lt;/p&gt;
&lt;p&gt;（然后这个 [4.0, 2.0] 会被残差加回 MLP 的输入 [2,1] → [6.0, 3.0]）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="transformer-block-完整循环"&gt;Transformer Block 完整循环&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输入 x
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├──────────────┐ (残差①)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LayerNorm │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;多头自注意力 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(＋)◄─────────────┘ x = x + 注意力(LN(x))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├──────────────┐ (残差②)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LayerNorm │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MLP │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(＋)◄─────────────┘ x = x + MLP(LN(x))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输出（形状同输入，可堆下一个 Block）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="预测下一个词"&gt;预测下一个词&lt;/h3&gt;
&lt;p&gt;上面过完所有 Block，最后一个词的最终向量 = [1.2, -0.4]。开始预测下一个词：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最后做一次 LayerNorm（数字校准）&lt;/p&gt;
&lt;p&gt;把 [1.2, -0.4] 再规整一下数字范围（和之前一样，只是&amp;quot;校准&amp;quot;，不改变意义）。假设校准后 ≈ [1.0, -0.5]。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出头 &amp;ndash; 把向量变成&amp;quot;每个词的分数&amp;quot;&lt;/p&gt;
&lt;p&gt;用一组线性层，把这 2 个数字，算成词表里每一个词的分数。&lt;/p&gt;
&lt;p&gt;假设词表只有 4 个词：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;人 : 3.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;猫 : 2.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;苹果 : 0.4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;跑 : -1.5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Softmax——分数变概率&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;人 : 68%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;猫 : 23%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;苹果 : 7%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;跑 : 2%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;按概率抽一个（大概率抽到&amp;quot;人&amp;quot;）→ 这就是模型预测的下一个词 → 解码成文字。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;PS：为什么这里时按概率抽取而不是选择概率最高的词？&lt;/p&gt;
&lt;p&gt;因为基于不可解释的原因，实践中发现如果永远选概率最高的词，得到的结果平淡无奇，而有时选取概率低的词会产生奇妙的结果。这也是 LLM API 里 temperature 参数的作用，如果 temperature 为 0 ，则永远会选择概率最高的词。&lt;/p&gt;
&lt;h2 id="模型训练"&gt;模型训练&lt;/h2&gt;
&lt;h3 id="loss-函数"&gt;Loss 函数&lt;/h3&gt;
&lt;p&gt;loss = − log( 模型给&amp;quot;正确答案&amp;quot;的那个概率 )&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─────────────────────┬──────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 模型给正确词的概率 │ loss = −log(概率) │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────┼──────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 1.0（完全自信且对） │ 0 ← 完美，零惩罚 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────┼──────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 0.5 │ ≈ 0.69 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────┼──────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 0.1 │ ≈ 2.3 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────┼──────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 0.01（基本没押中） │ ≈ 4.6 ← 惩罚很大 │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─────────────────────┴──────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="反向传播"&gt;反向传播&lt;/h3&gt;
&lt;p&gt;我们手上有一个 loss 数字（比如 2.30），我们想让它变小。模型有几百万个参数（各种&amp;quot;配方&amp;quot;的权重）。问题是：每一个参数，到底该调大还是调小？&lt;/p&gt;
&lt;p&gt;核心思想：把 loss 想成一座山&lt;/p&gt;
&lt;p&gt;想象 loss 是一座山的高度，你站在山上，目标是往山下走（loss 变小）。&lt;/p&gt;
&lt;p&gt;对某一个参数，关键就问一句话：&lt;/p&gt;
&lt;p&gt;▎ &amp;ldquo;我把这个参数动一点点，loss 是变高还是变低？变多少？&amp;rdquo;&lt;/p&gt;
&lt;p&gt;这个&amp;quot;动一点点会怎样&amp;quot;的量，就叫 梯度（gradient）。&lt;/p&gt;
&lt;p&gt;反向传播就是一个数学技巧（链式法则）：只需从 loss 出发，沿着网络反着扫一遍（输出层 →
往前一层 → 再往前……一直回到输入），就能一次性把所有参数的梯度全部算出来，不用一个个试。&lt;/p&gt;
&lt;h3 id="梯度下降"&gt;梯度下降&lt;/h3&gt;
&lt;p&gt;反向传播跑完，每个参数都有了梯度（坡度：哪边是上坡、多陡）。&lt;/p&gt;
&lt;p&gt;梯度下降公式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;新参数 = 老参数 − 学习率 × 梯度
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;梯度永远和参数同形状，如果参数是矩阵，梯度也会是矩阵。更新参数时，也是所有参数一同更新。&lt;/p&gt;
&lt;h3 id="训练流程图"&gt;训练流程图&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ 拿一小批数据（题目） │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────────┬──────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;① 模型做题（预测下一个词）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;② 对答案，打一个&amp;#34;错误分&amp;#34;（loss）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;③ 找出每个参数对这次错误的&amp;#34;责任&amp;#34;（梯度）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;④ 按责任，每个参数各调一点点（更新）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;错误分降一点，模型变好一点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──── 再拿下一批，重复很多很多次 ───┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌──────────────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;练够了 → 得到训练好的模型（一堆调好的数字）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;强化训练主要在这个过程中修改了第二步，不再是对答案，而是用奖励函数评分。&lt;/p&gt;
&lt;h2 id="模型运行"&gt;模型运行&lt;/h2&gt;
&lt;h3 id="推理流程"&gt;推理流程&lt;/h3&gt;
&lt;p&gt;模型文件分为三个部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;模型结构 （config.json）&lt;/li&gt;
&lt;li&gt;模型本体（参数）&lt;/li&gt;
&lt;li&gt;分词器 (tokenizer.json)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要运行模型，本质上就是按照模型结构搭建出模型的前向流程，然后自回归跑前向循环，直到遇到结束符。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;输入「我 爱」
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 前向#1（过完全部N层）→ 预测出「北」 ← 跑了一整遍
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;拼成「我 爱 北」
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 前向#2（又过完全部N层）→ 预测出「京」 ← 又跑了一整遍
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;拼成「我 爱 北 京」
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 前向#3 → 预测出「。」
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 前向#4 → 预测出 &amp;lt;结束符&amp;gt; → 停止
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title>昨日的世界不再重来</title><link>https://blog.joway.io/posts/the-past-before-ai/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/the-past-before-ai/</guid><category>Thought</category><description>最近在做一个思想实验，如果你面前有一个按钮，按下就可以让 AI 从世界上消失，你是否会按下。身边很多朋友的答案都是会毫不犹豫的按下，即便有一些人还是那种最拥抱 AI 的人。
拥抱 AI 和厌恶 AI 可以同时在一个人身上发生，我自己就是这样的人。我厌恶 AI，是因为对昨日世界的怀念，我拥抱 AI，是因为今日世界无法回退，我没有不拥抱的选择。</description><content:encoded>&lt;p&gt;最近在做一个思想实验，如果你面前有一个按钮，按下就可以让 AI 从世界上消失，你是否会按下。身边很多朋友的答案都是会毫不犹豫的按下，即便有一些人还是那种最拥抱 AI 的人。&lt;/p&gt;
&lt;p&gt;拥抱 AI 和厌恶 AI 可以同时在一个人身上发生，我自己就是这样的人。我厌恶 AI，是因为对昨日世界的怀念，我拥抱 AI，是因为今日世界无法回退，我没有不拥抱的选择。&lt;/p&gt;
&lt;p&gt;现在的 AI 虽然可以代替我完成很多事情，但是在事情完成后，带给我的并不是成就感而是一种无尽的空虚。有了一个 idea，找一个趁手的模型，用几句提示词让 AI 吭哧吭哧干活，再做几轮调整，最后几个小时项目完成了，剩下一段贤者时间，你不知道这个项目数据库的 schema，也不确定架构是否能支撑多少的用户规模，未来有新的需求，你只能对着这个黑盒许愿，在许愿前完全没有关于需求可行性的任何概念。当 AI 给你完成了修改后，你不知道会不会损坏已有的用户的数据，只能按照 AI 的要求上线。一通操作下来，身体很累，精神很空虚，干了一天的活，又好像什么也没干。伫立在这个黑盒面前，反反复复操作几个月，不知道自己究竟学到了什么。AI 有了更多的数据，一天天变得更聪明，而我们自己呢？&lt;/p&gt;
&lt;p&gt;在昨日的世界，虽然我们工作很累，但是工作结束会有一种智力的满足。当其他人来咨询某个需求是否可行，我们可以给出斩钉截铁的答案。当需求可行的时候，我们知道需要修改哪些地方让它可行，当需求不可行的时候，我们知道因为哪些原因所以它不可行。我们面对的是一个充满确定性的世界，我们确定昨天的工作，确定明天的工作，确定自己的职业生涯，确定未来能够有一份收入养家糊口生儿育女。今日的世界一切都不确定，不确定昨天做了什么，不确定明天会做什么，不确定这每天光聊天的工作能否有职业生涯，不确定未来什么样的人才可能凌驾于 AI 之上能有一份糊口的工作。&lt;/p&gt;
&lt;p&gt;今日的世界互联网的整体审美也在逐渐倒退。互联网到处充斥着用 AI 生成的机械感十足的图片，也没人在乎是否是真的符合自己脑海中想象的模样，只要和提示词差不多能用即可。各种「高效率人士」用 AI 创作着各种垃圾的文章，宣扬自己最高效的工作流，攀比着各自的 token 消耗量和同时并行启动的 Agent 数量。每天都把一些简单的步骤封装出各种下一个月就会消失的名词概念，并以自己掌握的名词数量而沾沾自喜。&lt;/p&gt;
&lt;p&gt;我时常在刷新推特时间线的时候，感受到自己是在一群推土机中漫步，页面滚动，一颗颗大树被连根拔起，群众在欢呼，木匠们做出一件件精美的家具。而我只想回到自己的瓦尔登湖旁边，但是小木屋也早已被掀翻，地上留着一张告示，顺之者昌逆之者亡。&lt;/p&gt;</content:encoded></item><item><title>Magic Brush - 画出你自己的产品宇宙</title><link>https://blog.joway.io/posts/magic-brush/</link><pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/magic-brush/</guid><category>Product</category><description>上周末用 AI 写了一个新的产品： Magic Brush。
这个产品的 idea 来自于我经常用 AI vibe coding 一些小的前端产品，但是如果每次都为这些小产品创建独立的域名和部署太过于繁琐，而且目前也缺乏一个聚合站点可以浏览其他人创建的小产品。另外，我在写博客的时候，也经常会想要插入一些交互页面来展示我的 idea，但是如果要在博客里插入一堆 js/css 代码又会导致博客无法长期维护。我把这些需求收集了下，整理并抽象出了一个产品形态：</description><content:encoded>&lt;p&gt;上周末用 AI 写了一个新的产品： &lt;a href="https://brush.elsetech.app"&gt;Magic Brush&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这个产品的 idea 来自于我经常用 AI vibe coding 一些小的前端产品，但是如果每次都为这些小产品创建独立的域名和部署太过于繁琐，而且目前也缺乏一个聚合站点可以浏览其他人创建的小产品。另外，我在写博客的时候，也经常会想要插入一些交互页面来展示我的 idea，但是如果要在博客里插入一堆 js/css 代码又会导致博客无法长期维护。我把这些需求收集了下，整理并抽象出了一个产品形态：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个产品可以允许用户使用自然语言创作页面，然后通过 &lt;code&gt;iframe&lt;/code&gt; 嵌入到任意网页中去。并且这个产品还可以有一个广场功能，可以看到其他人的作品。不仅仅能用来做博客的交互页面插入，也可以作为产品原型设计的工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="产品设计"&gt;产品设计&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://brush.elsetech.app"&gt;Magic Brush&lt;/a&gt; 由 2 主要页面组成。&lt;/p&gt;
&lt;h3 id="页面设计页"&gt;页面设计页&lt;/h3&gt;
&lt;p&gt;用户可以在设计页右边会话框写自己的提示词与 AI 交互，左边的页面会实时渲染最新的页面。其他用户可以以只读的方式访问该页面，并且也能看到提示词，方便其他人模仿修改并创建自己的页面。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/magic-brush/preview_hu_832270fc1dcb514a.webp" srcset="https://blog.joway.io/images/magic-brush/preview_hu_c82576833ab59198.webp 800w, https://blog.joway.io/images/magic-brush/preview_hu_1a448cf5364ae05d.webp 1200w, https://blog.joway.io/images/magic-brush/preview_hu_832270fc1dcb514a.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="799" alt="Magic Brush - 画出你自己的产品宇宙" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="广场页"&gt;广场页&lt;/h3&gt;
&lt;p&gt;广场页面允许看到所有 Public 的页面设计，可以以 Like 数量排序或者时间排序：&lt;/p&gt;
&lt;iframe src="https://brush.elsetech.app/square" style="width:100%;height:600px;border:0;" loading="lazy"&gt;&lt;/iframe&gt;
&lt;h2 id="use-cases"&gt;Use Cases&lt;/h2&gt;
&lt;h3 id="创作可交互文章"&gt;创作可交互文章&lt;/h3&gt;
&lt;p&gt;对于有些文章来说，文字并不是最适宜的表达载体，例如游记。你可以把你的游记喂给 AI 生成可交互的页面，然后再插入到文章中。例如我的 &lt;a href="https://blog.joway.io/posts/japan-kumano-kodo/"&gt;&amp;laquo;熊野古道中边路纪行&amp;raquo;&lt;/a&gt; 游记中，可以被转变为：&lt;/p&gt;
&lt;iframe src="https://brush-api.elsetech.app/pages/2ZkCB6KD0F" style="width:100%;height:2500px;border:0;" loading="lazy"&gt;&lt;/iframe&gt;
&lt;h3 id="创作流程图"&gt;创作流程图&lt;/h3&gt;
&lt;p&gt;传统流程图需要用截图的形式插入到文章，导致不易修改。我们可以让 AI 生成流程图页面，然后直接插入到文章中，并且后续实时渲染最新的修改。&lt;/p&gt;
&lt;iframe src="https://brush-api.elsetech.app/pages/KBDTrLJfrt" style="width:100%;height:600px;border:0;" loading="lazy"&gt;&lt;/iframe&gt;
&lt;h3 id="创作自定义工具"&gt;创作自定义工具&lt;/h3&gt;
&lt;p&gt;你还可以根据自己需要，写一个自己用的趁手的前端工具，例如：&lt;/p&gt;
&lt;iframe src="https://brush-api.elsetech.app/pages/l6IldHx-Ol" style="width:100%;height:600px;border:0;" loading="lazy"&gt;&lt;/iframe&gt;
&lt;p&gt;甚至能在网页上插入一个小型浏览器:&lt;/p&gt;
&lt;iframe src="https://brush-api.elsetech.app/pages/cJ3w_m4e3K" style="width:100%;height:600px;border:0;" loading="lazy"&gt;&lt;/iframe&gt;
&lt;p&gt;这些工具都能用 &lt;code&gt;https://brush-api.elsetech.app/pages/cJ3w_m4e3K&lt;/code&gt; 的方式作为工具本身全屏打开。&lt;/p&gt;
&lt;h2 id="实现过程"&gt;实现过程&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://brush.elsetech.app"&gt;Magic Brush&lt;/a&gt; 的实现非常简单优雅，整个 Design Agent 在前端运行，后端代码无法拿到用户的 API Token。后端部署在 Cloudflare Worker 上，数据库使用 Cloudflare R2 。整个产品的维护成本接近于 0，除非用户变多。&lt;/p&gt;
&lt;p&gt;Design Agent 的设计与传统 Code Agent 类似，约束是产出只能是一个 HTML 文件。在最开始的时候模型会返回一个根据初始提示词生成的 HTML 页面作为起始文件，而在后续 Modify 过程中，只允许调用一系列 tool call 来修改页面，最大化减少了 token 的消耗。&lt;/p&gt;
&lt;p&gt;开发过程中，我使用 Claude Code 作为起始项目一把梭的 Agent，在后续的增删改前后端过程中我转用了 codex 进行开发。总耗时大约为 10 小时。&lt;/p&gt;
&lt;p&gt;如果使用过程中有任何建议，欢迎联系我(&lt;a href="mailto:i@elsetech.app"&gt;i@elsetech.app&lt;/a&gt;)，也希望你会喜欢这个产品 :) 。&lt;/p&gt;</content:encoded></item><item><title>写在 AI Coding 奇点之后</title><link>https://blog.joway.io/posts/after_ai_coding_singularity/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/after_ai_coding_singularity/</guid><category>Thought</category><description>说来惭愧，我已经好几个月没有完全手写代码了，甚至像是把某个变量改一个名字这种活我都宁愿输入更多字符告诉 AI 来做而不是自己直接改了，因为我知道，哪怕是变量重命名这种简单的活，AI 也能做的比我仔细，它会检查所有用到这个变量的代码、相关的注释以及单元测试，然后一并都修改了，而我自己其实历史上就犯过不少忘记修改关联地方的错误。</description><content:encoded>&lt;p&gt;说来惭愧，我已经好几个月没有完全手写代码了，甚至像是把某个变量改一个名字这种活我都宁愿输入更多字符告诉 AI 来做而不是自己直接改了，因为我知道，哪怕是变量重命名这种简单的活，AI 也能做的比我仔细，它会检查所有用到这个变量的代码、相关的注释以及单元测试，然后一并都修改了，而我自己其实历史上就犯过不少忘记修改关联地方的错误。&lt;/p&gt;
&lt;p&gt;过去一两年的时间，AI Coding 引来了飞速的发展，就我的个人体验而言，大概经历了三个阶段。AI Coding 1.0 时代，AI 只是简单给你提供代码补全，代码总体还是人在写。而在 AI Coding 2.0 时代，人更像是在指导 AI 写代码，一次 prompt 只能高质量地写一百来行代码，你需要仔细检查代码是否符合你的需求。而最近两个月的时间，AI Coding 3.0 来到了一个奇点时刻，你甚至都不需要精心设计你的 prompt，只要简单描述下需求，AI 自己会根据代码上下文，以极高质量生成成千上万行代码，而人无论是体力还是智力已经无法 review AI 的代码，只能粗浅把握一个编程方向。在两个月之前，我或许还会认为过去的编程工作还能继续苟活一两年的时间，而现在我认为此刻所有传统的编程工作已经在事实上被淘汰，我们这些传统程序员之所以还没有被开除的原因仅仅只是公司自己在制度流程上还没有完全适应 AI 作为员工的方式，而这个适应过程可能会持续几年之久。&lt;/p&gt;
&lt;p&gt;在完全不手写代码的这几个月里，我时常在 AI reasoning 的间隙问自己到底在做什么。我似乎做着完全没有技术含量的『传话』工作，但与此同时，如果真要把我淘汰直接让 PM 和 Agent 对话，似乎也很难实现他们的需求，而且出事的风险极高。这意味着我依然在提供自己的价值，只是这个价值无法再被很好的「可视化」。我之所以比 PM 能让 AI 更好地干活是因为我具备一些专有的知识，这些知识的产出可能仅仅只是一个 Yes 或 No，但这不影响他们的价值。我之所以会觉得浑身难受的原因还是之前的日子过得太苦，觉得必须一定要自己手动干很多活才能体现自己的价值，没有真正把自己从一个「劳动者」换位到「管理者」。一个好的管理者肯定不是办公室里最忙碌的那一个人，那些做出对公司最有价值决定的管理者最终的产出也无非就是一个 Yes 或 No。能力强的员工往往希望老板管理的越少越好，AI Agent 也是如此。&lt;/p&gt;
&lt;p&gt;我们已经近乎实现了 Coding 的自动化，但是在一个公司里，Coding 只是整个公司工作的很小一部分，甚至都只是程序员工作的一小部分。我们现在拥有了一个无限智能的机器，但是整个公司运行制度都是面向人类设计的，Agent 需要发挥能力必须建立一个正向的 Loop，现在的 Loop 仅仅只建立在 Agent 内部，公司的每个环节与 Agent 并没有建立起这个 Loop，甚至连 Input/Output 渠道都没有建立。未来 Agent 会进一步渗透进整个公司一切流程的方方面面，不仅改变软件工程师的工作内容，也会改变产品经理，HR等一切职能的工作内容。我觉得这些传统工作岗位的消失会成为必然事件，但并不意味着不会创造新的工作岗位。最可能发生的事情是工作岗位从过去的不断细化的趋势开始变得不断泛化。现在你还在那区分前后端工程师属实是一个笑话，各种软件工种现在都可以统称为工程师岗位，如果一个后端工程师现在还写不好前端需求只能说明你不是一个合格的工程师。而所有工程师的工作会变成编排管理成百上千 Agent 的编码任务，解决 Agent 运行过程中遇到的无法自我解决的问题，帮助公司完善流程让 Agent 干活干得更舒心。&lt;/p&gt;
&lt;p&gt;在现在这个时刻，已经不需要怀疑这是一次工业革命级别的巨变，很高兴也很惶恐可以经历这场巨变。即便工作被淘汰了，至少也是被更高级的文明所淘汰，保持学习，保持好奇，毕竟是一个青壮年劳动力，世界总会出现新的工作留给我们这些人糊口的。Keep Calm and Carry On !&lt;/p&gt;</content:encoded></item><item><title>尼泊尔布恩山小环线纪行</title><link>https://blog.joway.io/posts/trek_poon_hill/</link><pubDate>Sun, 11 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/trek_poon_hill/</guid><category>Travel</category><description>2025 年 12 月跨年之际，为了赶在假期作废之前销假，我报了国内稻草人的一个徒步团，前往尼泊尔安纳普尔那山区进行了为期四天的 Poon Hill 小环线徒步。
尼泊尔最有名的两条徒步线路分别为 EBC 和 ABC。就 EBC 而言，我目前的身体状态和高海拔徒步经验远不足以应对，ABC 相对简单一些，但是依然有高反的风险。由于我印象里自己似乎从未到达过海拔超过 3500 的地区，所以我对自己身体适应高原反应的能力完全没有认知，于是就退而求其次报名了最高登顶海拔 3800 米，最高过夜海拔 3200 米的 Poon Hill 小环线，以此作为我高海拔徒步的入门路线。</description><content:encoded>&lt;p&gt;2025 年 12 月跨年之际，为了赶在假期作废之前销假，我报了国内稻草人的一个徒步团，前往尼泊尔安纳普尔那山区进行了为期四天的 Poon Hill 小环线徒步。&lt;/p&gt;
&lt;p&gt;尼泊尔最有名的两条徒步线路分别为 EBC 和 ABC。就 EBC 而言，我目前的身体状态和高海拔徒步经验远不足以应对，ABC 相对简单一些，但是依然有高反的风险。由于我印象里自己似乎从未到达过海拔超过 3500 的地区，所以我对自己身体适应高原反应的能力完全没有认知，于是就退而求其次报名了最高登顶海拔 3800 米，最高过夜海拔 3200 米的 Poon Hill 小环线，以此作为我高海拔徒步的入门路线。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/map_hu_e929916e2596fab9.webp" srcset="https://blog.joway.io/images/poon_hill/map_hu_f2ddebe34e072efe.webp 800w, https://blog.joway.io/images/poon_hill/map_hu_e2b89e3dd91fa9a9.webp 1200w, https://blog.joway.io/images/poon_hill/map_hu_e929916e2596fab9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1019" alt="尼泊尔布恩山小环线纪行" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="day-1"&gt;Day 1&lt;/h3&gt;
&lt;p&gt;第一天的路程主要以爬升为主。从 Ghandruk 到 Tadapani，累计爬升 1116 米，下降 112 米，住宿海拔 2718 米。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day1_hu_15afae6a49e07836.webp" srcset="https://blog.joway.io/images/poon_hill/day1_hu_c2e4ec77aacab62e.webp 800w, https://blog.joway.io/images/poon_hill/day1_hu_ddec7c6f0609a2e1.webp 1200w, https://blog.joway.io/images/poon_hill/day1_hu_15afae6a49e07836.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1214" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;第一天的景色比较一般，全程主要在林子间穿越，最后还走了一小时不到的夜路才抵达住宿点。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day1_1_hu_f1661d6dbdc468df.webp" srcset="https://blog.joway.io/images/poon_hill/day1_1_hu_5ede4f0831d49f40.webp 800w, https://blog.joway.io/images/poon_hill/day1_1_hu_b750491a8c8212ce.webp 1200w, https://blog.joway.io/images/poon_hill/day1_1_hu_f1661d6dbdc468df.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day1_2_hu_907265825bfeee2d.webp" srcset="https://blog.joway.io/images/poon_hill/day1_2_hu_a36edb5269d3545.webp 800w, https://blog.joway.io/images/poon_hill/day1_2_hu_7fd17383c89a891.webp 1200w, https://blog.joway.io/images/poon_hill/day1_2_hu_907265825bfeee2d.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day1_3_hu_eada5970fe677d7e.webp" srcset="https://blog.joway.io/images/poon_hill/day1_3_hu_f2b78a9b2220b34b.webp 800w, https://blog.joway.io/images/poon_hill/day1_3_hu_5f494fff02ea844.webp 1200w, https://blog.joway.io/images/poon_hill/day1_3_hu_eada5970fe677d7e.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day1_4_hu_d90f891026bf2508.webp" srcset="https://blog.joway.io/images/poon_hill/day1_4_hu_ce344c292ef6160e.webp 800w, https://blog.joway.io/images/poon_hill/day1_4_hu_682a2c895693d50d.webp 1200w, https://blog.joway.io/images/poon_hill/day1_4_hu_d90f891026bf2508.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-2"&gt;Day 2&lt;/h3&gt;
&lt;p&gt;第二天路程较短，但是徒步海拔较高。从 Tadapani 上升到 Dobato，累计爬升 848 米，下降 108 米，住宿海拔 3456 米。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day2_hu_7d58d2e760e8dca4.webp" srcset="https://blog.joway.io/images/poon_hill/day2_hu_aacfa12ff0f0901c.webp 800w, https://blog.joway.io/images/poon_hill/day2_hu_1a00b186aabc91b8.webp 1200w, https://blog.joway.io/images/poon_hill/day2_hu_7d58d2e760e8dca4.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1220" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;早上起来，我们就在 Tadapani 看到了壮丽的安纳普尔纳南峰和鱼尾峰的日照金山：&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_1_hu_5278ff9903bf626c.webp" srcset="https://blog.joway.io/images/poon_hill/day2_1_hu_810186348dd81c73.webp 800w, https://blog.joway.io/images/poon_hill/day2_1_hu_e34dcace1aede065.webp 1200w, https://blog.joway.io/images/poon_hill/day2_1_hu_5278ff9903bf626c.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_2_hu_d0b78abf5ad3e7dd.webp" srcset="https://blog.joway.io/images/poon_hill/day2_2_hu_fa81c60bcdbe3eb8.webp 800w, https://blog.joway.io/images/poon_hill/day2_2_hu_64a9f8fd4795ac0b.webp 1200w, https://blog.joway.io/images/poon_hill/day2_2_hu_d0b78abf5ad3e7dd.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;p&gt;这一天的路上我们还时不时能从林子缝隙里从不同角度看到这两座山峰的容貌，在路程末尾还看到了道拉吉里峰和Tukuche peak。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_3_hu_223dc82927ccefea.webp" srcset="https://blog.joway.io/images/poon_hill/day2_3_hu_eb7aa8fdb8f1ebce.webp 800w, https://blog.joway.io/images/poon_hill/day2_3_hu_3764b1f9c685a529.webp 1200w, https://blog.joway.io/images/poon_hill/day2_3_hu_223dc82927ccefea.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_4_hu_bc992d13366dd682.webp" srcset="https://blog.joway.io/images/poon_hill/day2_4_hu_c11a639fbabad1e1.webp 800w, https://blog.joway.io/images/poon_hill/day2_4_hu_1bdfa3032aed1df9.webp 1200w, https://blog.joway.io/images/poon_hill/day2_4_hu_bc992d13366dd682.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_5_hu_481bf6862108607f.webp" srcset="https://blog.joway.io/images/poon_hill/day2_5_hu_ca3cf0b779c1452a.webp 800w, https://blog.joway.io/images/poon_hill/day2_5_hu_ebced507730a2ac2.webp 1200w, https://blog.joway.io/images/poon_hill/day2_5_hu_481bf6862108607f.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_6_hu_6012cad8f743cde5.webp" srcset="https://blog.joway.io/images/poon_hill/day2_6_hu_ad35d70f413ca700.webp 800w, https://blog.joway.io/images/poon_hill/day2_6_hu_fc26680197bfaae5.webp 1200w, https://blog.joway.io/images/poon_hill/day2_6_hu_6012cad8f743cde5.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_7_hu_7913b51dca1a955d.webp" srcset="https://blog.joway.io/images/poon_hill/day2_7_hu_981b378995a920bc.webp 800w, https://blog.joway.io/images/poon_hill/day2_7_hu_11f8b548a3ce54d6.webp 1200w, https://blog.joway.io/images/poon_hill/day2_7_hu_7913b51dca1a955d.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_8_hu_4773074d7375a47c.webp" srcset="https://blog.joway.io/images/poon_hill/day2_8_hu_a5c3950326f1460.webp 800w, https://blog.joway.io/images/poon_hill/day2_8_hu_d7ae9e9202f7c8a6.webp 1200w, https://blog.joway.io/images/poon_hill/day2_8_hu_4773074d7375a47c.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_9_hu_2262be8860bd895.webp" srcset="https://blog.joway.io/images/poon_hill/day2_9_hu_4440d2f5ddc4537d.webp 800w, https://blog.joway.io/images/poon_hill/day2_9_hu_6a35fdc4407515c4.webp 1200w, https://blog.joway.io/images/poon_hill/day2_9_hu_2262be8860bd895.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day2_10_hu_6ad845937abc0e56.webp" srcset="https://blog.joway.io/images/poon_hill/day2_10_hu_68802cfdf18ed486.webp 800w, https://blog.joway.io/images/poon_hill/day2_10_hu_41cd50b933f3a5b4.webp 1200w, https://blog.joway.io/images/poon_hill/day2_10_hu_6ad845937abc0e56.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;p&gt;这一天我来到了有生以来最高的过夜海拔，在路上的时候倒还是适应，但是到了住宿的地方休息了一阵子之后，发现一做剧烈动作就有点晕，问了下 ChatGPT 似乎是这个海拔的正常轻微高反表现。这天气温也特别的冷，住宿条件也不高，甚至房间还有点漏风。&lt;/p&gt;
&lt;h3 id="day-3"&gt;Day 3&lt;/h3&gt;
&lt;p&gt;第三天经过一晚上的休息适应后，高反的症状完全消失了，从 Dobato 开始起身前往海拔 3637 米的 Mulde View Point ，然后一路下降来到 Ghorepani，累计爬升 526 米，下降 1106 米，住宿海拔 2881 米。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day3_hu_e0ff16eaa4af34b7.webp" srcset="https://blog.joway.io/images/poon_hill/day3_hu_46c8167aadcf4653.webp 800w, https://blog.joway.io/images/poon_hill/day3_hu_f44553c3fbb91df5.webp 1200w, https://blog.joway.io/images/poon_hill/day3_hu_e0ff16eaa4af34b7.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这一天的景色是最为壮观的，全程路上都在高海拔行进，不再有森林的遮挡，各路山峰尽收眼底。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_1_hu_b2c3c4e96996b48f.webp" srcset="https://blog.joway.io/images/poon_hill/day3_1_hu_81b8462fa66a9a31.webp 800w, https://blog.joway.io/images/poon_hill/day3_1_hu_856c43a9443977ce.webp 1200w, https://blog.joway.io/images/poon_hill/day3_1_hu_b2c3c4e96996b48f.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_2_hu_c7ab30a4b7bf6ece.webp" srcset="https://blog.joway.io/images/poon_hill/day3_2_hu_2bd94ae30cb05378.webp 800w, https://blog.joway.io/images/poon_hill/day3_2_hu_d7de15f56a2459a5.webp 1200w, https://blog.joway.io/images/poon_hill/day3_2_hu_c7ab30a4b7bf6ece.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_3_hu_fb5c380a607ab9ac.webp" srcset="https://blog.joway.io/images/poon_hill/day3_3_hu_66d08b670858c294.webp 800w, https://blog.joway.io/images/poon_hill/day3_3_hu_835316d2283fb103.webp 1200w, https://blog.joway.io/images/poon_hill/day3_3_hu_fb5c380a607ab9ac.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_4_hu_fc2df7ff9f72e942.webp" srcset="https://blog.joway.io/images/poon_hill/day3_4_hu_e91b9ce8e6dfd844.webp 800w, https://blog.joway.io/images/poon_hill/day3_4_hu_45cbba050b5f4c6b.webp 1200w, https://blog.joway.io/images/poon_hill/day3_4_hu_fc2df7ff9f72e942.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_5_hu_88978602fdfb98b6.webp" srcset="https://blog.joway.io/images/poon_hill/day3_5_hu_d97a438a9b964965.webp 800w, https://blog.joway.io/images/poon_hill/day3_5_hu_8e80057c48de356a.webp 1200w, https://blog.joway.io/images/poon_hill/day3_5_hu_88978602fdfb98b6.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_6_hu_e1d70740219bb023.webp" srcset="https://blog.joway.io/images/poon_hill/day3_6_hu_493fdb8fe7a0d997.webp 800w, https://blog.joway.io/images/poon_hill/day3_6_hu_a85638bacd55781b.webp 1200w, https://blog.joway.io/images/poon_hill/day3_6_hu_e1d70740219bb023.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_7_hu_22aafd14ab0d30b3.webp" srcset="https://blog.joway.io/images/poon_hill/day3_7_hu_9fe6f32f730a9497.webp 800w, https://blog.joway.io/images/poon_hill/day3_7_hu_7e81eb22a68c31bd.webp 1200w, https://blog.joway.io/images/poon_hill/day3_7_hu_22aafd14ab0d30b3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day3_8_hu_6552e9711fcf0ee5.webp" srcset="https://blog.joway.io/images/poon_hill/day3_8_hu_fafd70c4693e1697.webp 800w, https://blog.joway.io/images/poon_hill/day3_8_hu_9de88ce3353552b8.webp 1200w, https://blog.joway.io/images/poon_hill/day3_8_hu_6552e9711fcf0ee5.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day3_full_hu_5a49ba446261b63f.webp" srcset="https://blog.joway.io/images/poon_hill/day3_full_hu_7d2ff0f5848224b3.webp 800w, https://blog.joway.io/images/poon_hill/day3_full_hu_57dccd903a8a2fbe.webp 1200w, https://blog.joway.io/images/poon_hill/day3_full_hu_5a49ba446261b63f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="403" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;Ghorepani 是一个非常繁华的小村落，晚上住宿的山屋，无论是景色还是硬件条件，也都是此行最好的。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day3_hotel_hu_50c9d7d4305c2401.webp" srcset="https://blog.joway.io/images/poon_hill/day3_hotel_hu_93f8a55a9e7ecb3a.webp 800w, https://blog.joway.io/images/poon_hill/day3_hotel_hu_8224bf1ea0fe95ef.webp 1200w, https://blog.joway.io/images/poon_hill/day3_hotel_hu_50c9d7d4305c2401.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="day-4"&gt;Day 4&lt;/h3&gt;
&lt;p&gt;第四天我们先是早起登顶 Poon Hill 山顶，观看日出。然后就是一路的下降来到 Ulleri 坐车回博卡拉，累计爬升 425 米，下降 1615 米。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/poon_hill/day4_hu_b1e128505eb518ae.webp" srcset="https://blog.joway.io/images/poon_hill/day4_hu_bc1c582b380cf8e3.webp 800w, https://blog.joway.io/images/poon_hill/day4_hu_9fceadcfed8213f4.webp 1200w, https://blog.joway.io/images/poon_hill/day4_hu_b1e128505eb518ae.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1221" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;经过前几天的徒步，日照金山已经有点审美疲劳，但是 Poon Hill 的日出伴随着云霞的变化，依旧十分震撼。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_1_hu_98c41a18e7ddcfbc.webp" srcset="https://blog.joway.io/images/poon_hill/day4_1_hu_a77c97106c1d5d56.webp 800w, https://blog.joway.io/images/poon_hill/day4_1_hu_99cd742ece900aaa.webp 1200w, https://blog.joway.io/images/poon_hill/day4_1_hu_98c41a18e7ddcfbc.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_2_hu_16b11441eeb688f7.webp" srcset="https://blog.joway.io/images/poon_hill/day4_2_hu_b04662fc52bf1c8e.webp 800w, https://blog.joway.io/images/poon_hill/day4_2_hu_36b78583e31bbae3.webp 1200w, https://blog.joway.io/images/poon_hill/day4_2_hu_16b11441eeb688f7.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_3_hu_a18184394d4edbac.webp" srcset="https://blog.joway.io/images/poon_hill/day4_3_hu_adc96f1ec7f5b552.webp 800w, https://blog.joway.io/images/poon_hill/day4_3_hu_d6dfa1c7b00e1904.webp 1200w, https://blog.joway.io/images/poon_hill/day4_3_hu_a18184394d4edbac.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_4_hu_3840ee74b0ac9d81.webp" srcset="https://blog.joway.io/images/poon_hill/day4_4_hu_99da464bb349b6bc.webp 800w, https://blog.joway.io/images/poon_hill/day4_4_hu_a1b23b5b7cacea51.webp 1200w, https://blog.joway.io/images/poon_hill/day4_4_hu_3840ee74b0ac9d81.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_5_hu_8a03d1f63b2b7ba5.webp" srcset="https://blog.joway.io/images/poon_hill/day4_5_hu_95ecffe5ef11a97c.webp 800w, https://blog.joway.io/images/poon_hill/day4_5_hu_13d3d733c3eebeea.webp 1200w, https://blog.joway.io/images/poon_hill/day4_5_hu_8a03d1f63b2b7ba5.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_6_hu_212c3005dd3013a3.webp" srcset="https://blog.joway.io/images/poon_hill/day4_6_hu_8a94f458d2f608e2.webp 800w, https://blog.joway.io/images/poon_hill/day4_6_hu_23a81b94d4b9835b.webp 1200w, https://blog.joway.io/images/poon_hill/day4_6_hu_212c3005dd3013a3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;p&gt;去往 Ulleri 的路上，虽然自然景色乏善可称，但是沿途会路过大量的村庄，可以更好地了解当地人的日常生活，甚至在这个印度教和佛教主导的国家的偏僻山村里看到了一座教堂。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_7_hu_757811405102636d.webp" srcset="https://blog.joway.io/images/poon_hill/day4_7_hu_d1fdab99f5be7562.webp 800w, https://blog.joway.io/images/poon_hill/day4_7_hu_4b25150bf5a47cb8.webp 1200w, https://blog.joway.io/images/poon_hill/day4_7_hu_757811405102636d.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_8_hu_b79d1d4ea5f27256.webp" srcset="https://blog.joway.io/images/poon_hill/day4_8_hu_a706dc3514a94bbd.webp 800w, https://blog.joway.io/images/poon_hill/day4_8_hu_61875bafeca77111.webp 1200w, https://blog.joway.io/images/poon_hill/day4_8_hu_b79d1d4ea5f27256.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_9_hu_68421a9274e1fc67.webp" srcset="https://blog.joway.io/images/poon_hill/day4_9_hu_e2661beb7e7031b9.webp 800w, https://blog.joway.io/images/poon_hill/day4_9_hu_2ec7ca6a14d0564b.webp 1200w, https://blog.joway.io/images/poon_hill/day4_9_hu_68421a9274e1fc67.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_10_hu_907d313defc0e7ef.webp" srcset="https://blog.joway.io/images/poon_hill/day4_10_hu_91ea9405ebd70a2a.webp 800w, https://blog.joway.io/images/poon_hill/day4_10_hu_a3f4ed0c2103925a.webp 1200w, https://blog.joway.io/images/poon_hill/day4_10_hu_907d313defc0e7ef.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_11_hu_e26cec0dad0b3b85.webp" srcset="https://blog.joway.io/images/poon_hill/day4_11_hu_e91bb6d7d94ccd61.webp 800w, https://blog.joway.io/images/poon_hill/day4_11_hu_2df4a7dbbdf4d42e.webp 1200w, https://blog.joway.io/images/poon_hill/day4_11_hu_e26cec0dad0b3b85.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/poon_hill/day4_12_hu_566683cdbeb997d3.webp" srcset="https://blog.joway.io/images/poon_hill/day4_12_hu_bc72ab880dac82da.webp 800w, https://blog.joway.io/images/poon_hill/day4_12_hu_30f73cc80ad54eeb.webp 1200w, https://blog.joway.io/images/poon_hill/day4_12_hu_566683cdbeb997d3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="尼泊尔布恩山小环线纪行" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="后记"&gt;后记&lt;/h3&gt;
&lt;p&gt;尼泊尔虽然基础设施落后，但是在徒步产业周边的基础设施还算是发展的良好，甚至比日本熊野古道周边发展的都要好许多，至少这里基本不用担心路上水短缺的问题，大部分住宿也可以直接 walk in 进去入住。&lt;/p&gt;
&lt;p&gt;经过这次旅程，我对自己的高海拔徒步算是有了不少信心，下一次打算在尼泊尔继续更高海拔的徒步旅程。我在 CMGS 的一篇&lt;a href="https://cmgs.me/life/upper-mustang"&gt;博客&lt;/a&gt; 中偶然发现了尼泊尔北部的 Upper Mustang 这个地方，在 YouTube 上看了到国外旅行者的纪录片，赞叹不已。可惜在我做功课的时候发现，这个地方的通行证必须有至少两个外国人同行，每天需要额外支付 50 美元的通行证费用，如果 Solo，就必须支付 100 美元一天，这属实有点夸张了，而这个路线相对比较小众，很难找到合适的 Group 一同前往。另外一条我种草的路线是 Annapurna Circuit Trek(ACT) ，但这条路线耗时比较长，需要消耗比较多的年假，且最高海拔会到达 5000 米。&lt;/p&gt;
&lt;p&gt;总之尼泊尔注定是我很快会再一次回来的地方，希望我有生之年，身体素质能够健壮到可以挑战 EBC 的地步，但在此之前，我还有很多路需要走。&lt;/p&gt;</content:encoded></item><item><title>2025 投资组合年报</title><link>https://blog.joway.io/posts/2025-portfolio-annual-report/</link><pubDate>Thu, 08 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2025-portfolio-annual-report/</guid><category>Invest</category><description>2025 年，股市经历了政治的风云变化，也经历了 AI 浪潮的大洗礼。我的整体投资组合也引来了巨大的变化。这一年，标普 500 的年回报是 16.35%，而我的投资组合的回报只有 4.0%，远远跑输了标普 500。</description><content:encoded>&lt;p&gt;2025 年，股市经历了政治的风云变化，也经历了 AI 浪潮的大洗礼。我的整体投资组合也引来了巨大的变化。这一年，标普 500 的年回报是 16.35%，而我的投资组合的回报只有 4.0%，远远跑输了标普 500。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2025-portfolio/statistics_hu_31714905c534ceeb.webp" srcset="https://blog.joway.io/images/2025-portfolio/statistics_hu_4efec39cce1f429f.webp 800w, https://blog.joway.io/images/2025-portfolio/statistics_hu_31714905c534ceeb.webp 1184w" sizes="(max-width: 800px) 100vw, 800px" width="1184" height="192" alt="2025 投资组合年报" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2025-portfolio/performance_hu_41d45f060b16ec50.webp" srcset="https://blog.joway.io/images/2025-portfolio/performance_hu_8f52e3e3c9575f1e.webp 800w, https://blog.joway.io/images/2025-portfolio/performance_hu_428430d27919fa51.webp 1200w, https://blog.joway.io/images/2025-portfolio/performance_hu_41d45f060b16ec50.webp 1344w" sizes="(max-width: 800px) 100vw, 800px" width="1344" height="916" alt="2025 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;其实在 &lt;a href="https://blog.joway.io/posts/2024-portfolio-annual-report/"&gt;2024 年底&lt;/a&gt; 的时候，我还有 86.4% 的持仓还是在 VOO 和 QQQ 两大ETF，那时候 AI 浪潮才刚起步，世界政治局势也波澜不惊，在我不需要大量现金支出的情况下，这样的持仓非常舒服。&lt;/p&gt;
&lt;p&gt;然而川普上台后，在 4 月疯狂玩弄世界经济，而美国的政体却对这样的疯子没有一点制裁的办法，甚至还有一堆拥趸对其齐呼万岁，这彻底动摇了我对美国体制的信心。所以在四月大跌到一半的时候，我大幅度卖出了美股的持仓，在下跌到接近最低点的时候，又卖出了一批，卖出的资金后续陆续投向了美债市场和新加坡股市。当时伴随着美国股市，新加坡股市也引来了大跌，许多优质股如 DBS 引来了历史最佳位置，所以我乘机入手了一些 DBS 和海峡指数基金。&lt;/p&gt;
&lt;p&gt;然而后半年让我意想不到的是，标普 500 依然表现强劲，即便川普继续做了更多疯狂的操作，美股似乎依旧不为所动，持续创下历史高位。所以从后视镜看，我的调整无疑是失败的，但我的目标始终是建立一个稳健可持续并舒服的资产组合，从这个角度来说，我目前的组合是真正能符合我的目标的。我现在的持仓基本维持着股债分别占 50% 的水平。其中股票部分里，26% 在新加坡股市。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2025-portfolio/allocation_hu_57a457969238378.webp" srcset="https://blog.joway.io/images/2025-portfolio/allocation_hu_29eafc8c52aac545.webp 800w, https://blog.joway.io/images/2025-portfolio/allocation_hu_57a457969238378.webp 1176w" sizes="(max-width: 800px) 100vw, 800px" width="1176" height="540" alt="2025 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在绝大部分情况下，这样的组合基本可以让我一整年都不需要特别关心股市，实际上我可能现在打开证券软件的频率是一个月一两次。在美股大涨的时候，我的收益也不会差的特别多，美股大跌的时候，我甚至会特别开心，因为终于可以操作调仓补进美股了。&lt;/p&gt;
&lt;p&gt;但是我有一件事情其实到如今也想不明白的是，这一次的调仓，究竟是我对美国信心的动摇，还是我因为下跌而恐慌了。如果是因为后者，我就无法原谅自己了，因为在美股下跌的同时，我还有另一个稳健账户里存有不少的新加坡国债，我完全可以动用那笔国债进行抄底，这对我的持仓组合来说是巨大的天赐良机。但如果我是因为真的对美国的信心动摇，那么我的操作就是正确的至少是不应该后悔的。即便我十分擅长反思自己，但我至今也无法分辨自己当时的心态属于哪一种。我也深刻认识到「知行合一」的困难，它远远不是你知道了某件事然后就去做某件事这么简单，而是反过来，你做了某件事，你却不知道自己是因为哪一种「知」而做了这件事。而人性往往会倾向于帮你选择那一个你最希望的「知」来合理化你的行为，最终真正的「知」就消失在了一堆道不清的妄念之中了。你脑子里所臆想的你自己也会远远和你实际行动塑造的真实自己所背离。&lt;/p&gt;
&lt;p&gt;哎，学吧，太深了！&lt;/p&gt;</content:encoded></item><item><title>三十而笠</title><link>https://blog.joway.io/posts/2025-30/</link><pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2025-30/</guid><category>Thought</category><description>到 12 月底，我就迈过了人生的第三十个年头，进入到奔四的阶段。听年长的人谈起过，人对时间流逝速度的感知是指数变快的。可能这也是为什么对我而言，抵达三十岁的路程是极为漫长和艰辛的。
这三十年里有近二十年的时间，我的人生是不受自己把控的。你无法决定自己应该看什么书，学习什么知识，交怎么样的朋友，尊敬怎么样的人，甚至连每天吃什么都无法决定。上帝给你随机分配了一个国籍，一个家庭，你的国家不管你认同与否强制把你塞进了一个社会制度，你的家庭又直接决定了你接近二十年的生活质量和初始品格。我的前二十年基本就是活在被权威压迫的氛围中，在我对世界的理解尚浅时，我被中国老师以严厉为美德，以恐惧为工具的教学方法所压迫，而最糟糕的是，我一开始甚至都没有认识到这是一种压迫，你可能还会真的相信老师是在“真心为你好”。“真心”大部分时候倒是没错，没有老师会主观希望学生差，但是客观上，整个中国的教育制度出发点更多是“为了国家好，为了集体好”，而非真心为了你个人好。如果是为了你个人好，那无论你贫穷还是富有，聪明还是弱智，你都应该从这个教育制度里获益，成为了一个更好的自己，但是显然这个教育制度一切的出发点是为了选拔能够更好地建设国家的人，而不在乎每一个个体最终的结果如何。</description><content:encoded>&lt;p&gt;到 12 月底，我就迈过了人生的第三十个年头，进入到奔四的阶段。听年长的人谈起过，人对时间流逝速度的感知是指数变快的。可能这也是为什么对我而言，抵达三十岁的路程是极为漫长和艰辛的。&lt;/p&gt;
&lt;p&gt;这三十年里有近二十年的时间，我的人生是不受自己把控的。你无法决定自己应该看什么书，学习什么知识，交怎么样的朋友，尊敬怎么样的人，甚至连每天吃什么都无法决定。上帝给你随机分配了一个国籍，一个家庭，你的国家不管你认同与否强制把你塞进了一个社会制度，你的家庭又直接决定了你接近二十年的生活质量和初始品格。我的前二十年基本就是活在被权威压迫的氛围中，在我对世界的理解尚浅时，我被中国老师以严厉为美德，以恐惧为工具的教学方法所压迫，而最糟糕的是，我一开始甚至都没有认识到这是一种压迫，你可能还会真的相信老师是在“真心为你好”。“真心”大部分时候倒是没错，没有老师会主观希望学生差，但是客观上，整个中国的教育制度出发点更多是“为了国家好，为了集体好”，而非真心为了你个人好。如果是为了你个人好，那无论你贫穷还是富有，聪明还是弱智，你都应该从这个教育制度里获益，成为了一个更好的自己，但是显然这个教育制度一切的出发点是为了选拔能够更好地建设国家的人，而不在乎每一个个体最终的结果如何。&lt;/p&gt;
&lt;p&gt;我花了快三十年时间才想明白一件事情，最好的考试成绩其实应该是零分。学习知识的目的并不是为了考试，而是为了学习知识，考试的目的是为了检查哪一部分知识没有学到，如果你考了一百分，意味着你白花了几个小时做检测，没有找到半点没学到的知识。而只有以选拔为目的的考试，分数才是越高越好，但即便你拿到了满分，这也只能证明你掌握了这极小一部分的知识集合，且你超过了人类中极小一部分人群，而整个教育体系只覆盖了极少一部分知识而且有些知识还是被歪曲的，在这样的高度偏差还不干净的数据集下花上十多年的青春来训练得出一个高分，我实在想不到有什么更蠢的行为了。&lt;/p&gt;
&lt;p&gt;这个道理虽然如此浅显易懂，却在这个社会里属于歪理邪说，我也断然不敢和任何二十岁以下的人灌输这种观点，因为我知道，在这种教育体制下，你要逃脱这个愚蠢的游戏，唯一的办法就是首先尽量在这个游戏里取得高分，然后快点熬过青少年时期，成长到一个你有自主决定权玩什么样的游戏的年纪。如果你不慎在青少年时期就明白这个道理，却又不得已要继续玩这个游戏，除了让你更加痛苦没有任何好处。庆幸我小时候网上没有人写这种大逆不道的文章。&lt;/p&gt;
&lt;p&gt;在好不容易熬过前二十年后，我的人生终于引来了可以自主决定近乎一切事情的阶段。我在前二十年的那场游戏里并没有取得高分，只上了一所普通一本大学，但是所幸的是我成功读到了我唯一想读的计算机专业。我大学时候唯一的客观制约就是最好不要被学校开除以及尽可能拿到毕业证书，在这个底线之上，我几乎可以做任何合法的事情。我完全可以无视常规的从大一到大四的学习路径，在大一学习大四的知识，在大二就开始实习积累工作经验，在大四别人实习的时候我去世界各地旅行，而且并没有任何人会因为我的目无章法而指责我相反倒是收获了不少羡慕和赞美。恰好这段时光也是中国互联网乃至全行业发展最好的时间段，对我来说，我根本无需担心现在做什么和没做什么会影响到未来，因为那时对未来预期已经美好到现在做任何事情都影响不了的程度。&lt;/p&gt;
&lt;p&gt;在二十岁后的前五年里，我高频跳槽，并不断尝试做新的事情，学会了后端就开始学前端，学会了安卓开始学 iOS，做完了业务开发做基础设施，以工作时间来讲，那个时候是我工作时间最长的时候，甚至周末也会真的自愿加班来学习新的知识，但同时这段时间也是我工作最开心的时候。在后五年里，随着我逐渐打工越打越深入，以及打工的公司越来越大之后，工作的重心慢慢从“做有意思的事”转变为了“做有利于晋升和绩效的事”。我刚毕业那一会儿，虽然绩效也很不错，但其实根本是不管绩效在干活的，一门心思只想做有意思的事情并把事情做好。而且说实话我根本不在乎什么绩效和老板的看法，因为我已经拿到了学位证了，消除了这个社会制约我的最后一个把柄，从此我做任何事情，只要它合法，那最差的结果无非是公司把我开除了，而这个最差的结果根本对我就无从轻重，况且谁开除谁还不一定呢。然而这个淳朴的少年慢慢地在社会的锤炼下被一点点磨平了心气，我半主动半被动地，被带入了“成人高考”的游戏 —— 优绩主义。&lt;/p&gt;
&lt;p&gt;在我这些年的打工生涯里，我也逐渐认识到人和人之间有着巨大的分别，每个人工作的目的也多种多样。有赚钱养家型的，有生活工作平衡型的，有埋头只想晋升型的，还有像我一样只想纯粹写代码型的。可能就是因为这种差异性，公司必须发明一种统一的价值衡量体系，不管每个人主观意愿如何，只要你还想在这个公司干，甭管你个人喜欢什么型，都必须围绕着这个公司制定的价值衡量体系转。是不是很熟悉？这和中国的教育体制如出一辙，甚至还更残酷和荒谬。考试的试卷对每个人都一样，评判成绩的标准也是统一和公平的，而且每个人都可以考一百分。但是企业的绩效制度却充满了人情世故甚至是尔虞我诈，并不单纯只是你对公司有贡献就能取得好的绩效，而且你绩效好大概率就有人得要绩效差。有时候我会觉得大家很可笑，就多和少那点奖金，何苦一整年全部一门心思在这个上面勾心斗角。工作的目的原本应该是把工作本身给做好，现在反倒成了一定要比别人做的“好”，且得是你老板所认为的“好”。我不喜欢这个游戏并不是我无法赢这游戏，恰恰相反，我大部分工作年份拿的都是好的甚至是 Top 的绩效，我和我大部分老板也都是非常好的朋友，只是最近这些年互联网公司以及其中的员工有点在这个游戏里玩的太入戏了，让我非常出戏。&lt;/p&gt;
&lt;p&gt;所幸的是，经过这些年的积累，我多少也具备了能够逃离这种游戏的能力和底气。在 2025 年初，我给自己立下了一个目标，在 30 岁之前，尽可能不做自己不喜欢做的事情，达到自己想达到的生活状态。为此，我换了更健康的工作，恶补了英语，锻炼了身体并近乎解决了长期困扰我的颈椎病，还申请了新加坡公民。我不敢说自己达到了这个目标，但至少比年初的时候要距离目标接近了一大截。&lt;/p&gt;
&lt;p&gt;最重要的是，我认为我现在在玩一个我真正喜欢玩的游戏。我可以自由地学习我想要学习的知识，我也可以自由地选择不学习只摆烂，我可以选择去赚更多的钱，我也可以选择少赚一些钱多出来一些时间锻炼身体，我的社会成就比很多人低的多但是没有人会以此来评价我个人。我其实明白，这个游戏的危险之处在于，这是一个高度以自我为中心的游戏，脱离了任何客观评价的制约，最终容易度过一个碌碌无为的一生。但是，又有谁能如此武断地去评价一个人碌碌无为呢，每一朵花开自有它的原因，不一定要被你看到才是实现了一朵花的价值。&lt;/p&gt;
&lt;p&gt;我小学就很喜欢《江雪》这首诗和它的配图，甚至在课桌板下画了“孤舟蓑笠翁”的画，没想到二十年过去，我终于活成了“南洋蓑笠翁”。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/jiangxue_hu_cb5a8896d7d945a8.webp" width="667" height="500" alt="三十而笠" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;</content:encoded></item><item><title>我的 8 年职业生涯回顾</title><link>https://blog.joway.io/posts/career-8-years/</link><pubDate>Sat, 02 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/career-8-years/</guid><category>Thought</category><description>我正式 Onsite 的工作经历是从 2017 年 3 月加入即刻实习开始，到如今也已经度过 8 年多的时间。这 8 年时间，我先后任职了 4 家公司，从十几个人的天使轮公司，到一百多人的创业公司，再到五千多人的独角兽公司，最后到十几万人的超大型互联网公司。相伴着这段经历的，也是中国移动互联网快速发展的 8 年，我有过为了理想而自愿加班奋斗的经历，也有单纯为了工资勉强捏着鼻子工作的经历。社交媒体上对各种类型公司的吐槽和夸赞，我大体都有过亲身的体验，也更清楚自己每个阶段需要什么样的环境。恰好现在正式结束了第四家公司的工作，同时又即将到了三十岁的年纪，所以是时候回顾下我这 8 年的职业生涯，也供其他处于早于我的人生阶段的读者在迷茫公司选择时的参考。</description><content:encoded>&lt;p&gt;我正式 Onsite 的工作经历是从 2017 年 3 月加入即刻实习开始，到如今也已经度过 8 年多的时间。这 8 年时间，我先后任职了 4 家公司，从十几个人的天使轮公司，到一百多人的创业公司，再到五千多人的独角兽公司，最后到十几万人的超大型互联网公司。相伴着这段经历的，也是中国移动互联网快速发展的 8 年，我有过为了理想而自愿加班奋斗的经历，也有单纯为了工资勉强捏着鼻子工作的经历。社交媒体上对各种类型公司的吐槽和夸赞，我大体都有过亲身的体验，也更清楚自己每个阶段需要什么样的环境。恰好现在正式结束了第四家公司的工作，同时又即将到了三十岁的年纪，所以是时候回顾下我这 8 年的职业生涯，也供其他处于早于我的人生阶段的读者在迷茫公司选择时的参考。&lt;/p&gt;
&lt;p&gt;2017 年，当时的我还在读大三，但是已经在给一家业务主要在美国的在线教育公司利用课间业余时间远程实习赚外快。这家公司的系统经历过许多名美国各路大厂一线工程师的参与，所以整体技术 sense 都非常好，技术栈也算新颖，为我的技术生涯起步奠定了非常好的基础。在这家公司，我最开始只是做单纯的软件开发，后来因为做基础设施的同学的淡出，我逐渐参与甚至主导了很多基础设施的搭建和运维的工作，于是就误打误撞上了基础设施这辆车。&lt;/p&gt;
&lt;p&gt;后来，我偶然用到了一款叫做「即刻」的 App，会对互联网上的 RSS 甚至是非 RSS 的信息源，设置一些抓取过滤条件订阅，构建自己的信息流，并为这些互联网上原本没有评论区的信息构建了一个有高质量用户的评论社区。用着用着，当时正觉得学校的课程没意义，想要出来找实习，于是就顺着找到官网，投递了简历。今天回过头去看，当时的想法和简历都相当稚嫩，并且我当初完全不懂什么叫做校招，秋招，夏招，不知道实习招聘也分季节，我只知道我当时的自己希望积累更多的实习经验，就这么单纯。而我当时的课程安排实际上是完全不可能出去实习的，我就想了一招只有那些大学富二代混子才会干的事情，我拿我实习工资的一部分出来，花钱找人给我代课并且告诉我课程作业。就这样，我最后通过了面试，从杭州去到了上海实习，而且是五天 Fulltime 的实习。为什么会直接 Fulltime 也是因为我当时完全不知道实习原来可以不全勤，甚至实习生还可以因为本身工资就比正式员工低可以潜规则默认可以早下班，我当时就单纯认为实习就得和正式员工一样上班，得有一样的产出比较，稀里糊涂就跑去了实习。&lt;/p&gt;
&lt;p&gt;我在即刻的经历分为两段，从 2017 年 3 月工作到了 2018 年 10 月，去了一家更小的数据库创业公司呆了 5 个月，又回到了即刻，一直工作到了 2020 年 9 月。我在即刻第一段实习经历期间主要是在做基础设施的工作，这块工作只有我和我当时的 mentor 在负责，所以即便我只是一个实习生，做的事情也非常多。而且基本没有人主动来告诉我应该做哪些事情，应该做的事情总是自然而然地就自己出现了。我去的时候当时大家刚从 rancher 迁移到 Kubernetes 上，可以说基础设施是完全啥也没有，也不知道应该有啥。于是，我看大家捞日志不方便我就搭了个日志平台，服务配置一开始大家都写在代码里我就搞了一个配置中心，公司内部没有鉴权管理我就搞了一个接入了所有平台的鉴权中心，看大家都是裸用命令行发布服务我就搭了一个部署平台，我印象里这些系统都是用了极短的时间完成的，也丝毫没有方案设计和讨论这些步骤，先花最短时间做出来再说，而因为你只用了短的时间所以最差结果大不了不上也是能够接受的结果。我细数了这几套系统，在大公司可能已经足够养上百人的团队，光是方案设计讨论完都得加起来花上一年，但是在我实习的一年半时间里都做完甚至优化过好几波了。&lt;/p&gt;
&lt;p&gt;从今天回过头看，当时我作为实习生的 Scope 简直可以说大到一手遮天的地步了。以鉴权中心来说，最开始只是对一些内部网站做基本的 RBAC 权限控制，后来公司发展出来一个人数众多的审核团队，不同审核人员的权限细分也被纳入到了这个 RBAC 体系内，以至于任何新员工要入职都要来我这里注册一堆权限。我觉得我人生中所有遇到过的工程师，都没有一个能够有我当初实习时拥有的机会，一次性参与到这么多系统的开发，特别是这些系统还能伴随着一个创业公司一起成长，不像是在开发系统更像是在搭建一个创业公司。以至于我今天去看系统设计的题库，发现里面大半都是我自己实实在在亲手设计过的系统。&lt;/p&gt;
&lt;p&gt;虽然今天的我无比感激当初的经历，然而在我当时的稚嫩想法里，我却觉得我做的事情都太简单了，因为这些都不是那种服务千万级用户的系统，也没有什么值得拿出来说道的独家设计架构，所有合格工程师只要花上时间都应该能够写这些系统，这也促成了我最后离职去了一家数据库公司的原因，因为我想做更加有挑战的基础软件。但是今天的我在新加坡面试各路公司时，我惊奇地发现，我用到的大部分知识居然都来自我这段实习经历。这些系统虽然简单，但是需要你系统性，全链路地用到各种计算机领域的数据库，消息队列，软件架构的知识，而且你得真正独自一人手把手开发过这些系统才能有不会忘记的深刻经验，而不是网上看几篇系统设计文章然后夸夸其谈。而绝大部分在大公司工作的人，可能要花十几年才能积累到我这段经历的广度。如果今天再来问我哪段职业经历我一定不会选择放弃，那一定是这段经历，甚至这也不一定是加入创业公司就一定能有的经历，不是每个创业公司都会允许一个实习生操刀这么多重要系统，这是我 8 年职业生涯最宝贵的财富。&lt;/p&gt;
&lt;p&gt;2018 年 10 月，我加入了一家叫做 Dashbase 的专注于日志搜索引擎的美国创业公司，最初我的预期是能够参与到引擎的核心部分开发，但是入职后发现事与愿违，大部分工作都是在做 2B 的对接和问题排查，而引擎的核心代码，由于我们中国员工由于国籍的原因甚至连接触代码的权利都没有。加上另外的一些私人原因，最后我还是很快离开了这家公司回到了即刻，不过这家公司在我离开后过了几年也被 Cisco 收购了，证明了公司本身确实是一家不错的公司，如果当初选择留下也能有合理的回报，但是我也没有丝毫后悔当时的决定。&lt;/p&gt;
&lt;p&gt;在我回到即刻后，我依然做着和实习时候类似的工作，建立了一些新的中台系统，维护着公司的数据库等基础组件，直到 2019 年 7 月，因为一些众所周知的原因，即刻被勒令关停。具体的细节我无法谈起，但是因为我当时负责管理域名解析和网关的原因，我其实反倒是那个真正执行了关停命令的人，在我停掉域名解析之前，我在 Kibana 上用 access log 的地理信息导出了一张当时活跃用户的地图，保存到了自己的 iCloud 里。&lt;/p&gt;
&lt;p&gt;虽然在一年后，即刻又恢复了运作，但是关于这个产品的大部分抱负也停留在了过去，公司在这关停的一年间，集思广益各自领头去孵化了众多不同的新产品，而我甚至还短暂地转行做过一段时间的产品 Owner 的角色，但是当时大家的气氛只是迫切想要成功，对自己能够做的和真正想做的事情并没有确切的认知，所以最后经历一番头脑风暴不知怎么地我作为一个当时不健身不自律的人居然牵头做了一款健身自律主题的 App，最终结果自然也是以失败告终。但是这段失败的经历其实让我更加清楚自己适合做什么，所以我在 2020 年 9 月裸辞了一段时间，准备沉下心踏踏实实专注于技术，在这条路上往深度去走。&lt;/p&gt;
&lt;p&gt;2020 年 11 月，我加入了小红书的基础架构组。其实在当初的上海，互联网公司一共就那么几家，没有太多选择。当时的小红书也就三四千号人，处于不大不小的状态。我在小红书的技术经历还是非常愉快的，当时小红书有一小部分的服务开始使用 Golang，内部生态才刚刚开始，我得以像之前在即刻一样，参与众多框架和组件的设计实现，工作热情很高。我们组当时还维护这一个公司最核心的 Go 的网关系统，所以我写的那些框架和SDK组件，可以在这个网关上先行试验，然后再给其他 Go 的用户上线。我先后做了服务发现和流量调度，各种中间件的连接池实现，还有一个 RPC 框架。然而小红书的 Go 生态还是太小，对接来对接去也就只有那么几个组的人在用。所以后来没有多久，我就打算跳槽去一家 Go 生态更大的公司。&lt;/p&gt;
&lt;p&gt;2021 年 6 月，我加入了字节跳动继续做 RPC 框架。在我加入的时候，其实公司的 RPC 框架已经基础骨架已经搭建完了，能做的事情完全不像我之前的工作经历那样，能够有全新的系统或是组件能让你从零到一地设计实现。我先是作为入门任务做了一些性能压测的工作，因为做了压测，所以后面自然而然地开始全身心投入到了性能优化上。在最早期的时候，性能优化工作更像是一个非常有意思的研究性质的工作，每天投入精力去看代码看火焰图，找到可优化点，然后优化完了再跑一次压测看看效果。工作专注且愉快也没有什么压力，还可以非常深入地对着某一个方向深挖。对上面汇报的时候，数据也非常粗暴简单，就是性能提升了多少的百分比。然而后面这份工作优化代码部分在工作时间中的占比越来越低，收集收益和汇报的占比越来越高，毕竟上面真正看的是后者。对于我们这种有上万个微服务在使用的框架而言，你要去一个个统计不同业务升级框架的收益，实际上是一件几乎做不到的事情，除非你的收益非常大。举个例子，一个服务常态平均 CPU Usage 是 40%，你把框架性能提升了 5%，且假设这个服务的全部开销都来自框架，你预期看到的 CPU Usage 下降应该是 2% 。但实际上平均 CPU Usage 每天的波动就有 2% ，还不算上每两天的同一时刻的 QPS 波动也远超 2% ，更可况业务代码不可能只有框架开销，所以这里所谓的「收益」是很难有公允的数字的。有时候你啥也不干重启下被分配到其他性能更好的机器上，收益都能有 2% 。再后来更夸张的是，上面要的收益变成了服务缩容的 CPU 核数。即便你真的把框架性能优化了 5% ，也不见得会有人因为你这个 5% 而敢去缩容机器，更何况优化到了后期，基本不太可能找的到能超过 5% 效果的优化了。整个工作过程有让我非常惊喜的地方也有诸多不如意的地方，但我对技术的深入理解，大部分也确实是这部分工作期间建立的。大公司的工作节奏相对较慢，一个问题有更充足的时间可以去慢慢琢磨慢慢研究。而在创业公司，许多事情并需要知道原理，能够最快时间实现出来更为关键。一个例子就是使用一个外部库，在创业公司的时候看完主页简介就直接上手用了，而在大公司，基本我用到的每个库都是深入了解过它的代码实现后才会敢去用，甚至更为鼓励的是自己实现而不是用现成的。&lt;/p&gt;
&lt;p&gt;在性能优化的日常工作之外，我还会经常帮一些各种各样的业务去排查一些线上问题，这个过程虽然琐碎且常常伴随着晚上加班，但是却是我走出团队，与更广泛的群体打交道的好机会，也让我看到了人间百态。和这些各路人马的打交道过程中，让我逐渐对很多东西祛魅，对大公司光环祛魅，对高职级领导祛魅，对职场的绩效游戏祛魅。在我看来，很多人已经被工作异化了人性，大部分人追求的东西根本就是管理层忽悠出来的东西，实际上我们这份工作唯一必要的就是一份基本工资，奖金这些天然就应该是有最好没有也罢。但是如果你秉承这种心态，显然管理层就没啥东西可以要挟你了，所以发明了绩效和额外奖金，发明了晋升，这些东西就好像是房东的押金，假如你租房的时候就做好准备不要押金，你会比房东活得更像房东，反倒是房东还担心你这么不在乎押金会不会把他房子毁了。当我发现职场里的大部分人的工作重心和利益冲突都来自于这些东西，顿时感觉非常没有意义。&lt;/p&gt;
&lt;p&gt;我在字节跳动的最后一年时间，基本上感觉已经学不到什么新的知识，随着国内的就业环境变差工作环境也在逐步变差，大家的脾气和心态也越来越恶化。公司在 2025 年初还出台了一项针对新加坡员工匪夷所思的政策，让我感觉是时候开始新的旅程了，于是结束了这段我任职时间最长的工作。&lt;/p&gt;
&lt;p&gt;在我过去八年多的工作里，虽然有一些磕磕绊绊，但是总体上我运气还是非常好的，遇到了许多非常好的老板，也积累了许多非常珍贵的项目经历和人脉，有些事情虽然赚钱不多但是做得非常开心，有些事情做着难受但是金钱回报还算不错。站在如今的时间点上，我也更清楚自己想要的东西是什么，也知道了如果想要更多其他的东西代价是哪些。我在找新的工作的时候，与其说是找不如说是做选择题，在工资，工作时间，工作内容和个人收获中找出最适合自己的比例组合。但如果要问我最怀念哪段经历，还是最初那个不在乎工资，不在乎加班，不在乎他人看法的实习生的我，那个时候我是字面意义的一无所有，但是今天回过头去看，其实那个时候我拥有的东西和拥有的可能是最多的。&lt;/p&gt;</content:encoded></item><item><title>我的颈椎病康复之旅 - 关于选择的故事</title><link>https://blog.joway.io/posts/cervical-spondylosis/</link><pubDate>Sun, 29 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/cervical-spondylosis/</guid><category>Thought</category><description>从 2014 年上大一开始到现在，我的颈椎已经日均面对电脑超过 8 小时 11 年了，世间的物理器件应该很少有能工作超过 10 年之久的寿命，而我的颈椎也在差不多工作到第 10 年的时候出现了严重的问题。
其实早在四年前的时候，我的颈椎已经给了我警告。但那时候只是偶尔会出现僵硬疲劳的感受，通过每周的按摩差不多都能有所好转，如果专心于工作，其实并不能时刻感觉到不适。后来拍片发现了颈椎已经开始出现了生理性变直的问题，但是由于现代人有相当一部分都有相似的问题，所以我也并没有很在意。并且依然维持着每天十点多到晚上八九点钟的高强度工作节奏，更为糟糕的是，我在那时候还长期保持着趴睡的习惯，让我的颈椎曲度一天 24 小时都没有一个正常的状态。</description><content:encoded>&lt;p&gt;从 2014 年上大一开始到现在，我的颈椎已经日均面对电脑超过 8 小时 11 年了，世间的物理器件应该很少有能工作超过 10 年之久的寿命，而我的颈椎也在差不多工作到第 10 年的时候出现了严重的问题。&lt;/p&gt;
&lt;p&gt;其实早在四年前的时候，我的颈椎已经给了我警告。但那时候只是偶尔会出现僵硬疲劳的感受，通过每周的按摩差不多都能有所好转，如果专心于工作，其实并不能时刻感觉到不适。后来拍片发现了颈椎已经开始出现了生理性变直的问题，但是由于现代人有相当一部分都有相似的问题，所以我也并没有很在意。并且依然维持着每天十点多到晚上八九点钟的高强度工作节奏，更为糟糕的是，我在那时候还长期保持着趴睡的习惯，让我的颈椎曲度一天 24 小时都没有一个正常的状态。&lt;/p&gt;
&lt;p&gt;这种生活方式维持了差不多两年多的时候，我的颈椎问题已经恶化到了两眼偶尔会模糊，严重时已经无法正常工作的程度了。并且此时按摩已经连短期恢复的效果都达不到了。最严重的一次是，我因为脖子难受而在旋转脖子的时候，似乎不小心压到了一根神经，导致那天我甚至都紧急去了急诊室。那次遭遇让我意识到这个问题已经严重到没有任何其他问题能超过的地步，我甚至都为此备好了后事比如把我的银行账号都发给了我姐一防哪天真出现了意外。从那天之后，我开始下定决心要认认真真解决这个问题。&lt;/p&gt;
&lt;p&gt;我在新加坡先是去拍了 MRI ，以确认我颈椎是否真的有物理上的形变问题，但是结果是除了正常的曲度变化外，并没有看到有其他严重的问题。此后，我又去看了专门做美式整脊的 Discover Chiropractic 诊所的 Dr Joachim Low，通过他的正脊治疗，反倒是彻底解决了我之前伴随着的腰痛问题，迄今为止一直没有复发，但是我的脖子问题却丝毫没有改善甚至还有加重。接着我又去看了专门的脊柱专科医生 Dr Wu Pang Hung，他也根据 MRI 认为我的脊柱本身没有什么问题，给我开了加强骨关节的药物，但是也没有什么效果。后来我又专门找了一家名叫 The Pain Clinic 的 Dr. Ho Kok Yuen，他给我做了验血发现我有维生素 D 缺乏的问题，先是给我开了一些维生素片，然后在脖子上针对疼痛的部位注射了类固醇，在注射完的当下，确实感觉效果非常不错，但是一般维持个三四天疼痛感就又会回来，并且通过一个多月的高剂量维生素 D 补充也没有发现有丝毫改善的迹象。在实在不行的时候，我就又会回去找 Dr. Ho 再给我来一针。我觉得这也不是长久之计，最后，我去看了 Singapore Paincare Center 的 Dr Bernard Lee Mun Kam ，这是我整段治疗过程的最后一站，而 Dr Lee 也是世界上我除了父母之外最感激的人，甚至可能都没有之一。&lt;/p&gt;
&lt;p&gt;在 Dr Lee 的诊断下，他认为我的根本性问题还是在于神经而非物理的颈椎或是肌肉上。但是在神经的影响下，我的颈部肌肉已经出现了严重的筋膜炎，所以需要先用一种叫做 PRP(Platelet-Rich Plasma) 的血小板注射手术，快速让脖子肌肉先恢复，然后再使用抗抑郁症的药物去舒缓神经的紧张问题。我是在 2024 年 8 月 31 日做的 PRP 注射，一共花了 6500 SGD ，注射打了局部麻药，所以那天我回家又睡了一整天。但是注射完后，我感觉真正意义上的生活又重新回来了。与此同时，我还报了 20 节私教课，每周训练肌肉力量，让身体找回年轻的感觉。在 10 月份我的身体状态已经恢复到了能完成环勃朗峰高强度徒步的程度。&lt;/p&gt;
&lt;p&gt;在接下来的一年时间里，Dr Lee 让我陆陆续续地减少神经药物的摄入量，以求能彻底停药。但是在中间停药的一段时间里，我的脖子僵硬的感受又快速回来，导致我又临时去找 Dr Lee 让他给我重新配药。在确定了药物本身没有副作用后，我们商定的治疗方案是先一直维持吃最轻度的药物先，看看后续再如何发展。但是其实这一年时间，我肉体能够感觉到，PRP 手术的治疗效果在逐渐减弱，从最初感觉要比做手术前好了 90% 到一年后的现在下降到感觉好了差不多 80% 。最近一次我再去找 Dr Lee 续药的时候，我问他有没有一种办法能够让我一劳永逸地解决那最后 20% 的问题，他给我说了一句意味深长的话，他说，这是一个选择的问题。我可以选择暂时先维持在这 80% 的治疗效果，不进一步治疗但是保持运动看看能否自然地让他变好，我也可以选择重新再做一次 PRP 回到最初 90% 治疗效果的时候，甚至我还可以选择进一步做一个暂时没有临床科学依据的实验性干细胞注射的疗法只是要 20000 新币。这主要是看我的选择。&lt;/p&gt;
&lt;p&gt;这段话让我回想起这一两年时间，我其实已经做过了很多次选择。我不再在工作上投入过多精力，下班后完全忘记工作的烦心事，在工作上选择更多让我轻松愉悦的事情而非有精神压力的事情。我重新开始学习生活，过上了每天养鱼浇花的日子，研究如何更好地维持鱼缸的生态平衡，什么样的过滤器是好的，如何控制光照能让水草生长的同时不让水藻泛滥，我搭建了全自动浇花和喂食系统来保证我出去旅行超过 10 天家里的生态也能继续运行。我在财务上也开始重新选择，我重构了原本风险资产占比极高的投资组合，设计了一套有多重缓冲屏障保障的组合，确保我在没有工作的情况下，现金储备和低风险资产储备依然能维持我超过五年空窗期，巩固了我这种杞人忧天性格的人的精神健康。颈椎痛疼是我的工作方式，生活方式，治疗方式的综合反映，它不是人生的意外，而是人一系列综合选择的必然结果，过去的我做了这些选择，所以我收获了职业进步财富进步但是换来了病痛，我不可能在不牺牲一些事物的情况下一味地去追求病痛的消除。&lt;/p&gt;
&lt;p&gt;我人生大部分的经验都是在追逐目标，这些目标本身并非我自己设定，而是自然而然目标就自己立在了那里。更好的成绩，更大的职级，更高的工资，这些都是社会中毋庸置疑的目标，也有标准的追求目标的方法，直到有一天因为一些旁路发生的意外，被动的人生重新开始主动的思考，自己是为何来到了这里。&lt;/p&gt;
&lt;p&gt;随着我对颈椎病了解的逐渐深入，我发现世界上并非只有我在这么想，社交媒体上甚至还有很多人撰文感谢颈椎病，感谢它让自己重新反思了自己的生活工作方式，明白了什么才是健康的人生意义。而那些颈椎没有问题的人，根本很难理解我们的这种心境，如果某一种病痛 365 天 24 小时缠绕着你，你对世界上事情的优先级排布一定会有完全不同的看法。而如果没有颈椎病，可能许多人要走到更严重的疾病发生时才会明白这一切，如果从这个意义上，颈椎病更像是一剂人生疫苗，用不危害生命的轻度反应引起你对健康人生的重新思考。&lt;/p&gt;</content:encoded></item><item><title>熊野古道中边路纪行</title><link>https://blog.joway.io/posts/japan-kumano-kodo/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/japan-kumano-kodo/</guid><category>Travel</category><description>大约是在六七年前第一次听说熊野古道，后来又在任宁的播客 声音：时速三公里 里，对这条路线有了更加清晰的认识，暗暗把它加入了愿望清单。后来由于疫情的原因迟迟未能如愿，终于在今年 5 月，得以来到纪伊半岛踏上了这条古老的朝圣之路。
纪伊半岛毗邻京都，奈良和大阪，在日本历史上一直是重要的佛教圣地，众多高僧名士在此活动，他们往来半岛各处庙宇间的道路便逐渐成了现今的熊野古道。熊野古道分为多条路线，有大边路，小边路，中边路，伊势路等。我们此行选择的是住宿相对最容易预定，难度也适中的中边路。</description><content:encoded>&lt;p&gt;大约是在六七年前第一次听说熊野古道，后来又在任宁的播客 &lt;a href="https://podcast.weareones.com/episodes/118"&gt;声音：时速三公里&lt;/a&gt; 里，对这条路线有了更加清晰的认识，暗暗把它加入了愿望清单。后来由于疫情的原因迟迟未能如愿，终于在今年 5 月，得以来到纪伊半岛踏上了这条古老的朝圣之路。&lt;/p&gt;
&lt;p&gt;纪伊半岛毗邻京都，奈良和大阪，在日本历史上一直是重要的佛教圣地，众多高僧名士在此活动，他们往来半岛各处庙宇间的道路便逐渐成了现今的熊野古道。熊野古道分为多条路线，有大边路，小边路，中边路，伊势路等。我们此行选择的是住宿相对最容易预定，难度也适中的中边路。&lt;/p&gt;
&lt;p&gt;当地的观光协会在途径的神社门口会设立印有神社图案的盖章点，朝圣者集齐特定数量和路线的盖章，可以前往特定几个城市的游客中心获得特别的踏破认证。但由于其中一些匪夷所思的流程设计 —— 比如盖章本是在徒步起点十多公里处才会售卖 —— 就我的体验来看，盖章活动和纯粹徒步之间衔接的并不是很好，如果你追求的是集齐所有盖章，那么你不得不走回头路，或者单纯为了盖某个章去坐公车。出于不想舍本逐末的原因，我们放弃了一些并不顺路的盖章点，只走了全程的大概 90% 顺路路线。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/map_hu_b25ffad11600bb1d.webp" srcset="https://blog.joway.io/images/kumano/map_hu_d54692df0773abe9.webp 800w, https://blog.joway.io/images/kumano/map_hu_f62c7cc6a6e3a189.webp 1200w, https://blog.joway.io/images/kumano/map_hu_b25ffad11600bb1d.webp 1558w" sizes="(max-width: 800px) 100vw, 800px" width="1558" height="1566" alt="熊野古道中边路纪行" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;全程行迹图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/route_hu_aeb352b458b0dff6.webp" srcset="https://blog.joway.io/images/kumano/route_hu_8cd618f7c51e2fd5.webp 800w, https://blog.joway.io/images/kumano/route_hu_9b85abb6c12ffa31.webp 1200w, https://blog.joway.io/images/kumano/route_hu_aeb352b458b0dff6.webp 1282w" sizes="(max-width: 800px) 100vw, 800px" width="1282" height="1258" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;朝圣盖章：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/stamp_hu_f142ac6f8238aed8.webp" srcset="https://blog.joway.io/images/kumano/stamp_hu_ef62fa996e3112fb.webp 800w, https://blog.joway.io/images/kumano/stamp_hu_703094585aacdcee.webp 1200w, https://blog.joway.io/images/kumano/stamp_hu_f142ac6f8238aed8.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="333" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="行前准备"&gt;行前准备&lt;/h2&gt;
&lt;p&gt;熊野古道成行最大的困难点在于中途的住宿预订，其中绝大部分停留村镇基本无正规意义的酒店可言，当地民宿也大多没有入驻到传统预订平台，而是要通过熊野古道官方的预订网站 &lt;a href="www.kumano-travel.com"&gt;kumano-travel&lt;/a&gt; 进行预订。这个网站每点击都要等待约 10 秒钟，而且你需要一次性把你的徒步行程都规划好，然后一次性预订所有沿途住宿，住宿提供方不仅会审查是否有空余房间还会检查你的行程规划合理性，如果有其中一个点无法成功订到住宿，整个预订单都会失败。由于 5 月初恰好赶上了日本的黄金周，所以我提前了 6 个月就开始预订，但即便如此也有很多住宿点已经没有空余 slot 了。&lt;/p&gt;
&lt;p&gt;纪伊半岛 5 月大概处于春季，加之一共也才 4 天的徒步，为了减轻每天徒步的背负重量，我把随行装备减少到了极限，只有身上穿的一套春装和备的一套春装，以及一些急救装备，电子设备和登山杖。总共仅 5 千克。&lt;/p&gt;
&lt;h2 id="day-1-滝尻王子-到-近露王子"&gt;Day 1: 滝尻王子 到 近露王子&lt;/h2&gt;
&lt;p&gt;上午 9 点坐公交从纪伊田边抵达熊野古道馆，正式开始徒步。这天上午一直下着小雨，不过所幸这是这四天旅程唯一一段雨中行走。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-1_hu_c5725af797cbb965.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-1_hu_7882db7b18a9304b.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-1_hu_4eea27d5fa9333a9.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-1_hu_c5725af797cbb965.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;一个半小时后，抵达一处展望台，依稀可见远处群山。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-2_hu_fd7fa546a1f62b86.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-2_hu_dac77f543b466e48.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-2_hu_e724cf50cb73bf51.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-2_hu_fd7fa546a1f62b86.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;中午 12 点，抵达高原的休憩所，天气彻底转晴，在阳光下解决午餐。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-3_hu_614f63825c21f750.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-3_hu_1944575fa4540d7c.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-3_hu_52fa077eb757376f.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-3_hu_614f63825c21f750.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午全程都在森林中行走，雨后山蟹也跑到了道路上，空气异常清新。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-4_hu_f4cfdc5242db2496.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-4_hu_841c32a53f06290f.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-4_hu_debb48bf09906b6d.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-4_hu_f4cfdc5242db2496.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-5_hu_c97eb9958244f50e.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-5_hu_923ae8442f799d40.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-5_hu_74f60abd4281b39d.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-5_hu_c97eb9958244f50e.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-6_hu_19a062de27b682a1.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-6_hu_9cbd912eeb949cb1.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-6_hu_2fd3b52f7a97f191.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-6_hu_19a062de27b682a1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-7_hu_6a25ea099a2f1947.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-7_hu_a1118b04dd116fcd.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-7_hu_9128aa190d11fa85.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-7_hu_6a25ea099a2f1947.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-8_hu_b6a064608a4e196f.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-8_hu_e72972c9bbe33acc.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-8_hu_48a219e324ce1ee8.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-8_hu_b6a064608a4e196f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午 4 点 30，抵达近露。远看近露只是一个小村庄，但却在这里找到了相比后面几天最大的超市。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-9_hu_44e0af75a5deb10c.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-9_hu_12aeb0978a66446f.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-9_hu_ffb666661955b764.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-9_hu_44e0af75a5deb10c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day1/kumano-10_hu_883fa16be1906f1d.webp" srcset="https://blog.joway.io/images/kumano/day1/kumano-10_hu_13f65f078dc3e659.webp 800w, https://blog.joway.io/images/kumano/day1/kumano-10_hu_fc98c975949722f1.webp 1200w, https://blog.joway.io/images/kumano/day1/kumano-10_hu_883fa16be1906f1d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;第一天一共行走了 16.8 km，爬升 1000 m。&lt;/p&gt;
&lt;h2 id="day-2-継桜王子-到-熊野本宫大社"&gt;Day 2: 継桜王子 到 熊野本宫大社&lt;/h2&gt;
&lt;p&gt;上午 8 点坐巴士抵达継桜王子的山脚处，开始今日的徒步。今天大约有 1/4 的路程都在马路上，但是道旁的景色一点也不输山里。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-1_hu_b15adb3f2e330d62.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-1_hu_32b0c783ee482e7.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-1_hu_82686a7d0aaf066d.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-1_hu_b15adb3f2e330d62.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-2_hu_6d403b0b3d315497.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-2_hu_c677c83ce9ea902.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-2_hu_d8e77fd0a02898e2.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-2_hu_6d403b0b3d315497.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-3_hu_f2fe79a6e7f742ae.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-3_hu_b53770f27544145d.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-3_hu_f6363b6fe9ef87a5.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-3_hu_f2fe79a6e7f742ae.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-4_hu_1835bff18c509541.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-4_hu_881978b456b0ebd5.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-4_hu_1939ec48fed9f784.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-4_hu_1835bff18c509541.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;公路结束，进入山路，今天的下坡山路一直非常陡峭，不是很好走。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-5_hu_2ec82bbe766fbfc2.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-5_hu_fdecdc5c1d4915b1.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-5_hu_d8d8a35423df8aa7.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-5_hu_2ec82bbe766fbfc2.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-6_hu_33f254c7ed6a6b22.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-6_hu_2bd376f2ae4c4f46.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-6_hu_8ab827dd6c97ebb2.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-6_hu_33f254c7ed6a6b22.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-7_hu_eb93d41ca8df7406.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-7_hu_7a5426327242ade0.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-7_hu_c7daca279570ebb2.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-7_hu_eb93d41ca8df7406.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-8_hu_db6cf11b2ff5d688.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-8_hu_55fe9ab2e28bd7f7.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-8_hu_f7091e63d6e4a135.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-8_hu_db6cf11b2ff5d688.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;进入溪流旁的一条道路，整个行程中，最喜欢的一条路。路旁的景色非常优雅。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-9_hu_189675964556bf62.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-9_hu_2459281941419242.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-9_hu_411eeab6109769e5.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-9_hu_189675964556bf62.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-10_hu_924ddad8d03326d2.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-10_hu_4a29a252f082a187.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-10_hu_d4dafd338ac9e5ed.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-10_hu_924ddad8d03326d2.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-11_hu_579d7fe5051089ec.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-11_hu_e2c01b9fd6a6db8c.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-11_hu_f4f851d1f49ef0f4.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-11_hu_579d7fe5051089ec.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-12_hu_84c40dfa01635154.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-12_hu_7ce19c4a9709cddb.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-12_hu_47c6e8bfd558c705.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-12_hu_84c40dfa01635154.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午 2 点 30，抵达发心门王子。今天全程走了 18 km，爬升 800 米，如果要继续走到熊野本宫大社，预计还会有 3 小时共 7 km，虽然也不是不能走，但是到了本宫后可能会面临无法购买补给同时也没有时间浏览本宫当地的问题，所以选择坐巴士从发心门到达熊野本宫。&lt;/p&gt;
&lt;p&gt;在熊野本宫的观光协会，为上半程路程去得到朝圣之路完成的认证，顺路把几个景点逛了一圈。事实证明选择坐巴士是正确的，日本这些乡镇的超市餐厅关门时间都在五六点左右，务必不要把徒步结束时间安排在四五点之后。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-13_hu_e808da6753bb29.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-13_hu_ed559736f3b8bc59.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-13_hu_4470da18e1463ec1.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-13_hu_e808da6753bb29.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day2/kumano-14_hu_573faa400df6b2f1.webp" srcset="https://blog.joway.io/images/kumano/day2/kumano-14_hu_be0722aae7478edb.webp 800w, https://blog.joway.io/images/kumano/day2/kumano-14_hu_1f604f4c2afb6762.webp 1200w, https://blog.joway.io/images/kumano/day2/kumano-14_hu_573faa400df6b2f1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="day-3-小云取越"&gt;Day 3: 小云取越&lt;/h2&gt;
&lt;p&gt;上午 8 点 30，从熊野本宫坐巴士抵达请川，开始小云取越的徒步。小云取越按理应该是整个中边路最简单的一段，但是体感并不觉得轻松，可能是和大云取越相比，它比较简单罢了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-1_hu_56612f6bf2801254.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-1_hu_1e2be4e6a7ec2e30.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-1_hu_7cb57e8ebdb23f4a.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-1_hu_56612f6bf2801254.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-2_hu_2cc6c3726d60209a.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-2_hu_f4d36ee420b54a08.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-2_hu_36d1c143eca2a0e6.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-2_hu_2cc6c3726d60209a.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;和大云取越一样，小云取越也是先连续的爬升，然后再连续的下降。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-3_hu_6d691f238cf67d9.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-3_hu_9144644105c76f59.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-3_hu_ae38112a42024a60.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-3_hu_6d691f238cf67d9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-4_hu_a2b3796925d1ac68.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-4_hu_d2e1c17cd8a2ce9.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-4_hu_bb68c57b43d2d76.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-4_hu_a2b3796925d1ac68.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;10 点 40 分，抵达山顶百间，俯瞰熊野群山。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-5_hu_64c3fbd199147d4f.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-5_hu_72fedf3c4f135fbe.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-5_hu_5774ade2d32a5055.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-5_hu_64c3fbd199147d4f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-6_hu_52a8a7f00ba17c33.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-6_hu_fe73a8f95a5c39a3.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-6_hu_fc4cd95ec5969b45.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-6_hu_52a8a7f00ba17c33.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-7_hu_5babd090538a807e.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-7_hu_ed628726038aab54.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-7_hu_13eac4828ce6e7d1.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-7_hu_5babd090538a807e.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-8_hu_9e3cd84c3d7ae8d5.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-8_hu_a6e8cb615b3916cb.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-8_hu_9f64fc6817421ad2.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-8_hu_9e3cd84c3d7ae8d5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午 2 点，抵达小口。又花了一个小时，抵达 3 km 外的住宿点 @koguchi 。全程走了 17 km，爬升 700 m。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-9_hu_28ff31bc042ae45d.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-9_hu_21b6e3e48c579b48.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-9_hu_8887136b4c606707.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-9_hu_28ff31bc042ae45d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-10_hu_dcdc9474dc1edfc8.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-10_hu_1183bd49a8bfc306.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-10_hu_f39dbe747dee96b0.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-10_hu_dcdc9474dc1edfc8.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-11_hu_369081123509a15b.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-11_hu_977275424f787385.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-11_hu_af46eca7e3022ec8.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-11_hu_369081123509a15b.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;小口本身就是一个偏僻的村落，而 @koguchi 更是坐落在这个偏僻村落的偏僻角落。但也得以在此享受了日本真正农村的生活方式。房东是一个日本女生，继承了祖母的农村老宅，将其改造为了民宿。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-12_hu_cec2d9e19628070b.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-12_hu_2132c923c8f2fcc.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-12_hu_f71acf6c5abaf0ee.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-12_hu_cec2d9e19628070b.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day3/kumano-13_hu_57ff5ea2faf7c883.webp" srcset="https://blog.joway.io/images/kumano/day3/kumano-13_hu_e82fe6fae0aac971.webp 800w, https://blog.joway.io/images/kumano/day3/kumano-13_hu_6be1c22f385bd31.webp 1200w, https://blog.joway.io/images/kumano/day3/kumano-13_hu_57ff5ea2faf7c883.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="day-4-大云取越"&gt;Day 4: 大云取越&lt;/h2&gt;
&lt;p&gt;早上 6 点 40，@koguchi 的房东把我们送到了大云取越的入口。在网上一直听说大云取越难度非常高，特别是一上来的连续 800 米爬升，可能是由于做了过多的预期建设，当爬完这段连续上坡后，反而觉得并没有网上说的那么难。当然另一方面也可能是因为这一天的天气也非常棒，降低了路线的难度。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-1_hu_9b21b46c9783b621.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-1_hu_74bc829fb2784c.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-1_hu_179a6a3d39b9ceb8.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-1_hu_9b21b46c9783b621.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-2_hu_dc8f99f0685018f5.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-2_hu_4448f82c0bb3b595.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-2_hu_6df2e84241d72e02.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-2_hu_dc8f99f0685018f5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-3_hu_c86dd70d18ee32aa.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-3_hu_c9cec0de9206523e.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-3_hu_f49263af2f917682.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-3_hu_c86dd70d18ee32aa.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-4_hu_1727c5fc14c89e5d.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-4_hu_e48ffd3e37190fcd.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-4_hu_5268fa7206485728.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-4_hu_1727c5fc14c89e5d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-5_hu_bb06d84a502d18ce.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-5_hu_93e0d1ad70f90e5a.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-5_hu_9a8bf64554eff6bc.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-5_hu_bb06d84a502d18ce.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-6_hu_94b8e696a573f315.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-6_hu_20d40353092d7399.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-6_hu_8af9f3bcf234ad1e.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-6_hu_94b8e696a573f315.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="2133" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-7_hu_bb53c2574131d9e5.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-7_hu_246f3dc87beaac55.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-7_hu_a5fa74c834522a30.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-7_hu_bb53c2574131d9e5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-8_hu_680df83eb8471b45.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-8_hu_2e0cecf3be9c0a44.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-8_hu_fd6b505c0be971b6.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-8_hu_680df83eb8471b45.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-9_hu_2d7106152a81a025.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-9_hu_f5061760c776c069.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-9_hu_50a326b3b44de06b.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-9_hu_2d7106152a81a025.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-10_hu_2e3a576832f251f2.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-10_hu_36f58c5338855d2.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-10_hu_d1e20fd456a4d0e3.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-10_hu_2e3a576832f251f2.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-11_hu_da1f885bde872b8c.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-11_hu_1df9681199b6830.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-11_hu_ff3da014dab440b9.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-11_hu_da1f885bde872b8c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;10 点 30，抵达地藏茶屋迹。意味着今天最难的部分都已经结束。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-12_hu_a788e60a432a10f6.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-12_hu_8963f2eea90beb2a.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-12_hu_7d1dee39615095d0.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-12_hu_a788e60a432a10f6.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;12 点 30，抵达舟见茶屋迹，远眺纪伊胜浦港。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-13_hu_d3c6694d0cd7e284.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-13_hu_cb1247ccec8e53f3.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-13_hu_9f91e6fc609450f8.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-13_hu_d3c6694d0cd7e284.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午 1 点 50，抵达大云取越出口处，继续下降，前往旅程终点 —— 那智神社。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-14_hu_e89747ff147a5627.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-14_hu_5cb64f59468751a6.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-14_hu_9746528134900925.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-14_hu_e89747ff147a5627.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;从那智高原的林间小道抵达那智神社的出口，从了无人迹到人声鼎沸，一时间无法适应。今天一共走了 19 km，爬升 1200 m。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-15_hu_39599a36bee8b075.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-15_hu_dda6cdcb6e2335dd.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-15_hu_793720407632cce4.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-15_hu_39599a36bee8b075.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-16_hu_2114c28adae3f6e6.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-16_hu_99db84ee0918e58.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-16_hu_db4b59f9d7b10052.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-16_hu_2114c28adae3f6e6.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;乘坐巴士从那智神社前往纪伊胜浦，漫步街头时恰好看到一个广告说今天晚上有花火会。于是到了 8 点，备好酒水零食，前往胜浦码头赏花火。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-17_hu_6dc952660c3a933c.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-17_hu_8a238da99cb02b95.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-17_hu_8f0319d51572f6f3.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-17_hu_6dc952660c3a933c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-18_hu_827ed3cdfd9e48f5.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-18_hu_883eeb875a83ac4e.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-18_hu_fab1a4c8110cd21a.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-18_hu_827ed3cdfd9e48f5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/kumano/day4/kumano-19_hu_efbf0673d3a2ccab.webp" srcset="https://blog.joway.io/images/kumano/day4/kumano-19_hu_dbd37bcea1726226.webp 800w, https://blog.joway.io/images/kumano/day4/kumano-19_hu_312cdd43d82ec23e.webp 1200w, https://blog.joway.io/images/kumano/day4/kumano-19_hu_efbf0673d3a2ccab.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="熊野古道中边路纪行" loading="lazy" decoding="async"&gt;&lt;/p&gt;</content:encoded></item><item><title>新加坡旅居三年再回首</title><link>https://blog.joway.io/posts/singapore-3-years-review/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/singapore-3-years-review/</guid><category>Singapore</category><description>自从 2022 年 5 月 4 号抵达新加坡，已经近乎快满三年了。我来新加坡的旅程相当坎坷，具体过程记录在了《那一天，我决定踏出一步》 中。此后，我几乎没有在博客里记录过关于这个国家的任何评论，很大程度是因为我想要更加深入了解这个国家，才好更客观地评价，特别是拿这个国家与之前呆了二十几年的国家进行对比，这样才不失公平。三年过去了，我想是时候重新回顾下这段旅程，也为后来人做一些参考。</description><content:encoded>&lt;p&gt;自从 2022 年 5 月 4 号抵达新加坡，已经近乎快满三年了。我来新加坡的旅程相当坎坷，具体过程记录在了&lt;a href="https://blog.joway.io/posts/run-away-from-shanghai/"&gt;《那一天，我决定踏出一步》&lt;/a&gt; 中。此后，我几乎没有在博客里记录过关于这个国家的任何评论，很大程度是因为我想要更加深入了解这个国家，才好更客观地评价，特别是拿这个国家与之前呆了二十几年的国家进行对比，这样才不失公平。三年过去了，我想是时候重新回顾下这段旅程，也为后来人做一些参考。&lt;/p&gt;
&lt;p&gt;在 2022 年来之前，我在上海的境遇不能说不好(抛开封城特殊时期不谈)，但也绝对算不上体面。我应该算是有一个体面收入的软件工程师，基本享受了外卖自由，超市自由，打车自由和餐馆自由，我在大部分事物的消费上几乎不需要怎么在乎价格，因为日常消费的东西很少能超过一天的工资。但是这座城市似乎论资排辈并不看重这些，在另一种评价体系里，我是一个没有上海暂住证更没有户口的外地人，租着一个一居室的老破小，依靠自己的收入几乎不可能在上海比较体面的地段买得起体面小区的房子，更别提这些房子还在大幅度涨价。至于户口，在当时的政策下，我基本没有什么可预期的途径拿到上海户口，最快的方式可能就是花个大几十万以及综合两年的时间成本去读个英国硕士然后回来落户。而没有户口又影响了能够购买的房子以及何时能够购买，后者又影响了买房的价格 —— 在当时永远上涨的预期下。所以按照正常的发展路径，大概率最后我还是要“降级”回二线城市。&lt;/p&gt;
&lt;p&gt;而三年后在新加坡的今天，我这三年里点外卖的次数低于五次，超市几乎每样东西都要关注价格是否打折，迫不得已不会打车，除非重要日子否则只去价格 Bottom 20% 的那些餐厅。从生活质量上看，与上海对比，显然是极大地降级了。但是另一方面，我在大约 2 年的时候拿到了新加坡的永久居民，然后以外国人的身份在这里买了房子，并且有了自己的养老金和公积金。按照政府的正常规范路径，只要我自己愿意，并且持续工作下去，我可以很快还清房贷并且在 55 岁有着不错的退休现金流。至于到时候我要去哪里花这笔现金流，完全由我自己了。&lt;/p&gt;
&lt;p&gt;所以当我客观地回顾对比在这两个国家生活的境况时，很难得出一个简单结论 ———— 哪里生活更好。如果以证券来比喻，上海更像是股票，新加坡更像是债券。持有股票，你不知道明天是涨是跌，但是你很清楚至少今天在涨，先把今天过好。持有债券，名义上票面利率已经确定，本金也一定会归还，你明确知道未来的收益预期，但就今天而言你最多只能获得今天的利息。&lt;/p&gt;
&lt;p&gt;但是如果把这个问题更加主观化 ———— 你愿意在哪个国家生活 。 我会毫不犹豫地选择新加坡，下面是我为什么选择新加坡的理由，也是我这三年的生活体验。&lt;/p&gt;
&lt;h3 id="生活半径"&gt;生活半径&lt;/h3&gt;
&lt;p&gt;许多人一提到新加坡，会用一种轻蔑的口气谈论它的「小」。这是事实，但是这仅在把它作为国家而言，才是事实。作为一个城市，新加坡恐怕要比大部分发达国家的城市要大得多，特别是欧洲。柏林人口 370 多万面积 891 平方公里，马德里人口 330 万面积 604 平方公里，而新加坡人口 600 万，面积 735.2 平方公里。虽然人均面积肯定较小，但是似乎也没有差距很大？刚好就是一个城市大概应该有的面积大小。&lt;/p&gt;
&lt;p&gt;另一方面，作为一个市民，只有自己能够实实在在会去的地方，才属于这个城市的有效面积，而非地图上的实际面积。上海人恐怕大部分都没有去过临港，即便那里地铁都能到达，也属于上海辖区。我也从没有听说过在上海有人周末会约去海滩，抑或是爬山，骑自行车更加是通勤才会做的事情而非休闲。绝大部分上海的小区附近也绝非是跑步的好地方。而这些实实在在都是在新加坡的人周末甚至下班后会做的事情。在上海，休闲活动往往选择不在上海，这才让上海人总是把江苏和浙江当作自己的后花园。所以从这个意义上看，上海才是真正的小，小到得离开上海才能有正经的休闲活动。&lt;/p&gt;
&lt;p&gt;最后，有效的生活半径也有一定的时间尺度极限，比如开车 20 分钟内。在新加坡，绝大部分人的居住范围内，20 分钟车程都能覆盖从海滩到山林再到市中心的各种博物馆餐馆，小区楼下也大概率一定有适合跑步的步道，如果住的是公寓更会自带健身房和游泳池。这些都是在上海无法想象的市政设施的可触达便利性，这一方面有面积小所带来的紧凑优势，另一方面也是市政规划的刻意为之。&lt;/p&gt;
&lt;h3 id="运动氛围"&gt;运动氛围&lt;/h3&gt;
&lt;p&gt;正是因为生活半径内能达到的地方非常多，而这里大部分地方又都是运行场所，加上这里一年 365 天都是夏天，所以在新加坡很容易养成喜欢运动的爱好。我在上海几乎不怎么运动，首先上海的市政规划就基本没有给你运动的选项，其次上海的天气在一年大部分时间里也并不适合室外运动，再者周围的人确实也都没有运动的氛围。&lt;/p&gt;
&lt;p&gt;但在新加坡，地铁上一眼看过去大部分年轻人的身材都能看到运动的痕迹。特别是男性公民，在 40 岁之前，政府都会强制每年都要回军队复训和体检，如果不合格会要求参加额外体能训练。与此同时，各种保险以及政府软件也会有各种各样的运动优惠和补贴，总之在新加坡，运动能赚钱是真的。&lt;/p&gt;
&lt;p&gt;我现在每个周末基本都会安排至少一次运动，可能是骑车，可能是徒步，也可能是跑步，也可以是跑步完后徒步，或者骑车完后跑步。在我家往北 2 公里可以到达一个非常大的碧山公园，往南 5 公里可以直达国家体育馆和旁边非常大的一个公园联合体，所以每周可以在地点和运动项目间互相组合出不同样的运动安排。&lt;/p&gt;
&lt;h3 id="人民素质"&gt;人民素质&lt;/h3&gt;
&lt;p&gt;我不会说新加坡人的素质就比上海人高。在我看来，大家都是一样的人，但是，新加坡的教育和法律让这里的人「看起来」素质确实要比中国高一大截，特别是遵守规则方面。我搬进现在住的公寓后，发现一个奇怪的现象，所有人的鞋子都放在门槛的位置，而非连接门槛的走廊上。在我的观念里，你在你家门口放你的鞋子总不能算是过分的事情，虽然严格来说，家门口的走廊确实也是公共区域。但是新加坡人对公共区域的不侵犯到这种程度也是让我非常震惊。而在国内生活过的人应该都知道，侵占公共空间在国内不是什么大新闻。即便有物业，大部分业主也并没有把物业的规则当一回事。&lt;/p&gt;
&lt;p&gt;和穷山恶水出刁民反过来，养尊处优也比较容易出善良的国民。新加坡人就是这样的群体。我在新加坡遇到的善意要比我在上海多很多。我遇到过给我修锁的师傅觉得我外国人来打工不容易主动给谈好的价格减价，给我缝针的阿姨不要我钱，甚至给我来装门铃的小哥看到我大门关门特别响额外专门给我调整了下螺丝。作为回报，我也为新加坡人贡献过很多善意。这是一份互相传播的善意氛围，激励每个人都加入到这个互帮互助的氛围中。而在中国，很多时候你的善意无法得到同等的回馈，或者是对方每天过于忙碌而没时间来为你的善意反馈，久而久之你也不会习惯于付出善意了。特别是，如果对方本身就是依靠自己的技能来谋生，反倒是你要求对方牺牲自己的利益贡献善意显得特别邪恶了。而新加坡，做这些工作的不一定是非常缺钱的人，甚至真的有住别墅的老人为了打发时间出来做一些手艺活的。他们的善意对他们来说成本就要比中国这些欠发达地区低很多了。&lt;/p&gt;
&lt;h3 id="法制环境"&gt;法制环境&lt;/h3&gt;
&lt;p&gt;对我而言，法制的意思就是有事情我们提前说好，有地方可以检查我要做这件事要符合的所有准则，只要我按照你说的做，你就别来找我麻烦。有在中国的生活经验的人应该都明白，在中国，要达到这么基础的要求有多难。举些简单的例子，谁能列出来开家咖啡店一共需要哪些明里暗里的程序，色弱到底能不能考驾照，我交了养老金到底未来能够给我多少钱是不是一定会给我钱，甚至是天价买的房子 70 年后到底归谁怎么归属都没人能说明白。&lt;/p&gt;
&lt;p&gt;在中国，如果你按照程序办事，恐怕你会寸步难行。我在中国的色盲鉴定标准就是色盲，即便我能够分辨绝大部分颜色。但是我依然考出来了驾照，依然读了规定色盲不能读的计算机专业，你说我怎么办到的 :) 。如果按照新加坡人的遵守规则的准则，我恐怕现在在中国也是一个彻彻底底的失败者。然而我就是一个想要遵守规则的人，但是同时我也不想要失败。&lt;/p&gt;
&lt;p&gt;新加坡的法制精髓在于执法的严格。这并不是说每一项违法行为都一定会被执法，但是只要被抓到，那一定是从严惩罚。所以在新加坡生活，保留证据是非常重要的保护自己的手段。举个例子，如果你保留了对方的聊天记录，你大体可以放心对方不太会欺骗你，因为对方输不起。反过来如果有人只愿意给口头承诺，不愿意打字，你可能就知道这里或许存在一些模糊地带。而在中国，骗局首先往往就是从文字开始的，如果你觉得书面承诺就一定不会骗人，在中国可能有的是跟头要栽。这也是很多国人来新加坡自己栽跟头的地方，最典型的就是签租房合同的时候不当回事，面对白纸黑字没有敬畏感。等到自己遇到不能按白纸黑字履行合同的时候，倒打一耙觉得房东不是人。或许合同确实有偏向房东的地方，但是再偏袒也是你自己自愿签署的，第一责任人依然是你自己。&lt;/p&gt;
&lt;h3 id="政治环境"&gt;政治环境&lt;/h3&gt;
&lt;p&gt;对新加坡政治的典型解读就是这里是一个「威权国家」。这实际上是洋人的解读，洋人对比的是自己自由主义的国家。而对于真正从威权国家出来的人来说，这根本算不上威权。&lt;/p&gt;
&lt;p&gt;为了了解新加坡的政治，我基本把这里所有异见人士写的华文书籍都看完了，我的目的就是为了看看所有批评政府的声音里，政府做的最过分的事情是什么。目前看到的最过分的就是把几位在人民行动党起步阶段有功劳甚至是创党功劳的人，用莫须有的罪名关押了从几年到几十年不等。这绝对不是什么光鲜或是可以被洗白的事情，但是我从中国过来的人应该都明白我心里在想什么。不说别的，上山下乡就把一大批年轻人无缘无故浪费了几年到几十年的青春。你可能觉得上山下乡不是坐牢，问题是新加坡的坐牢待遇可要是比上山下乡好上不少，至少没有任何人反馈说有挨饿的情况。今天上海哪一位居民敢拍着胸脯说，自己没有挨饿经历的？更何况，这里坐牢最长的傅树介医生，中间是给过服软出狱的机会的，是傅树介医生自己选择坚持自己的观点而导致坐了如此长的牢。当年中国的情况，可没有给予过这些人这种选择服软的待遇。&lt;/p&gt;
&lt;p&gt;抛开李光耀时代的旧事，今天的新加坡社会，以我个人的体验来说，绝对算得上是一个民主的社会。首先，选举制度是真实的，有反对党批评在选区划分上，人民行动党经常做一些所谓的小动作，但是即便如此，选举本身确实是真实的。即便选区划分不改变，也无法改变行动党依然能取得多数选票的事实，改变的是部分议员或许无法胜选，进而影响到行动党原本设想的部长岗位分配。其次，人民行动党是可以输掉政权的，李光耀本身就说过如果选举失败会和平交接政权，包括从行动党各种行为来看，并没有把反对党彻底杀死，甚至现行的议会制度是有扶持反对声音的一些设计的，比如当反对党选票不够时，最少也一定要保留 12 位反对党议员。最后，老百姓是有民主意识的，老百姓清醒地知道政府是给自己打工的，而不是领导人民的，这是民主社会最关键的意识，也是今天的中国所没有的。&lt;/p&gt;
&lt;p&gt;如果未来真的有人民行动党落选的一天，我相信不管愿意不愿意，老百姓有足够的能力给予政府足够压力和平移交权力。&lt;/p&gt;
&lt;h3 id="财务自由"&gt;财务自由&lt;/h3&gt;
&lt;p&gt;对于一个现代的全球公民来说，在新加坡通过个人努力实现财务自由是要比其他国家容易很多的，这里的前提是「现代的全球公民」，如果你是一个拿到工资存本地定存的老派人士，这里和其他国家相比没有太大区别，政府设计的面向个人财务的优惠政策你也无法充分享受。&lt;/p&gt;
&lt;p&gt;首先，新加坡没有资本管制，你的钱是自由的，没有资本管制，这意味着你可以合法地投资全球，这就直接决定了你收益率的上限。&lt;/p&gt;
&lt;p&gt;其次，新加坡个人所得税也极低，普遍人在 10%+ 左右的水平。这决定了你积累本金的速度。&lt;/p&gt;
&lt;p&gt;再者，新加坡没有资本利得税，你的盈利是 100% 属于你的。而地球上大部分发达国家都是有高额的资本利得税的，这直接决定了你收益实际真实的到手价值。&lt;/p&gt;
&lt;p&gt;另外，新币是稳定货币，理论上不能被随意滥发，每一新币必须有等值的一篮子货币资产储备支撑。在这个持有任何国家货币都不安全的世界，最好的现金持有方式也是持有一篮子货币而非挂钩单一货币。&lt;/p&gt;
&lt;p&gt;然后，新加坡的房地产税极低。很多国家比如美国，即便是自住，房地产税也是奇高，基数是房屋总价值，而且大概率还一直在涨，意味着你近乎一辈子永远处于租房的状态。一旦没有了收入，你的投资收益首先要减去一部分在房地产税上。新加坡也有房地产税，但是基数是房屋年值即市场年租金的 4%+ ，自住基本可以忽略。&lt;/p&gt;
&lt;p&gt;最后，新加坡有独特的 CPF 制度，在年轻时，可以作为房贷月供的补充资金来源，在年老时，自动变成退休金的资金来源，并且独立于破产清算资产之外，在积累到一定额度的 CPF 后，个人可以有一份近乎 100% 的退休后现金流保证。这是任何商业保险无法替代的政府为收益率兜底的现金流产品。而且虽然是政府提供的产品，但你缴纳的每一分钱的收益都归还给了你自己而没有用于补贴他人。&lt;/p&gt;
&lt;p&gt;如果我们假设平民级的财务自由标准是，在没有工作后，也能过上社会中位数的生活的话。新加坡 2025 年财务自由的月现金流要求大概是在 3000 新币左右，假如没有家人也没有房贷的话。它所能达到的生活水准基本在衣食住自由的水平，但不能拥有私人汽车。要满足这个要求，需要你在退休时，CPF 拥有大约 426000 SGD。或者你用自己的投资组合收益达到同样的标准。虽然现实中很多普通新加坡人确实不一定能够达到这个标准，对于新移民的收入而言，这个标准是非常容易的，否则政府也根本不会给你新加坡的身份。&lt;/p&gt;
&lt;h3 id="新加坡的缺点"&gt;新加坡的缺点&lt;/h3&gt;
&lt;p&gt;前面只说了新加坡的优点部分，如同任何国家一样，新加坡也同样充满着缺点。比如在没有拿到 PR 前，作为纯外国人打工的境遇可以说是提心吊胆。这里对外来劳工也缺乏保证，处处能感受到压迫。作为一个中间偏右翼的国家，传统右翼带来的社会问题比如贫富差距大等等也都存在。但是这些缺点说实话，和你日常关系倒也不是很大，良心上过不去罢了。对我来说最大的缺点还是「小」，这不是说国土面积的小，恰恰这倒是最不重要也是最能通过物理方法克服的，而我认为的「小」是这里容易产生「精神层面的小」。这是不容易克服反倒容易沉迷的。&lt;/p&gt;
&lt;p&gt;和新加坡人甚至马来西亚人聊天，经常让我觉得一些话题无法深入展开，其中一个原因就是我发现他们不了解「大国语境」。这或许也是中国在全世界话语体系里总是占据不到上风的一个原因，因为很少有国家的人能够体会到这种「大国语境」。对于苏联，中国，美国这样的国家，很多问题是天然不成立的 —— 比如所有以「中国人如何如何」，「中国做了 XXX 所以 XXX」这样句式的话语。因为中国就不存在一个主语叫做「中国」。有一些人可能来自某个省，所以他们有这种特性，但不代表全体中国，甚至连 5% 的中国都代表不了。又或者有些事情是某家中国公司凑巧做的，和国家其实没关系，比如 deepseek 就和中国政府可以说半毛钱关系都没有。但是无论内部有多少细节，外部都会以「中国」两个字概括过去。这在我看来就是一种「小」的心态。&lt;/p&gt;
&lt;p&gt;对于新加坡来说，上述句式很容易成立，新加坡人确实有着高度的一致性，新加坡的大公司也大多和国家脱离不了关系，这套逻辑体系在新加坡非常自洽，所以以这套逻辑理解世界，简化世界也变得非常得心应手。但是如果真正理解中国发生的这些事情背后的复杂性，你往往会绝望地发现这个世界是无法聊的，无法被解释的。这种绝望，我很难给新加坡和马来西亚人讲清楚。&lt;/p&gt;
&lt;p&gt;但同时，我们作为一个新移民来到这个新国家，不单纯是为了人家的优点来的，也是为这个土地做贡献来的。所以与其说是缺点，不如说是我对自我的警示，亦是新移民可以为这篇土地带来的新的视野。特别是，在新的移民政策越来越严格的时候，新移民本身的自身素质也是在迅速提高，与此同时中国本身年轻人的素质也在飞速发展，所以未来的新移民所引入的新视野，只会越来越前沿和宏大。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;最后，我想说，新加坡是一个乱世安身立命的好居所，也是一个观察世界，理解世界，投资世界好的支点，「立足小岛」是我对这三年的总结，「放眼世界」是我对未来在这片土地生活的期望。&lt;/p&gt;</content:encoded></item><item><title>高加索三国 - 阿塞拜疆行记</title><link>https://blog.joway.io/posts/caucasus-azerbaijan/</link><pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/caucasus-azerbaijan/</guid><category>Travel</category><description>我是在 2023 年 9 月 28 号去到的阿塞拜疆，而刚刚在一周多前的 9 月 19 日，阿塞拜疆刚刚针对被亲亚美尼亚势力实际控制的纳卡地区展开了所谓的“反恐行动”，控制了纳戈尔诺-卡拉巴赫(简称纳卡)地区。我在阿塞拜疆的旅程也被笼罩在阿塞拜疆与亚美尼亚的领土争端气氛中展开。</description><content:encoded>&lt;p&gt;我是在 2023 年 9 月 28 号去到的阿塞拜疆，而刚刚在一周多前的 9 月 19 日，阿塞拜疆刚刚针对被亲亚美尼亚势力实际控制的纳卡地区展开了所谓的“反恐行动”，控制了纳戈尔诺-卡拉巴赫(简称纳卡)地区。我在阿塞拜疆的旅程也被笼罩在阿塞拜疆与亚美尼亚的领土争端气氛中展开。&lt;/p&gt;
&lt;h2 id="纳卡问题"&gt;纳卡问题&lt;/h2&gt;
&lt;p&gt;如果你看到纳卡地区的地图，你便知道为什么阿塞拜疆一定要拿下这片土地：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/azerbaijan/nk-map_hu_423f37c0579f80ca.webp" width="595" height="387" alt="高加索三国 - 阿塞拜疆行记" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;纳卡地区就是一块国中国飞地，虽主权也并不归属亚美尼亚，但是其土地上生活的大部分人都是亚美尼亚人。但倘若你再完整看一看阿塞拜疆的地图，可能又会有另一个疑问，这个纳希切万又是哪里来的，为什么也横插进了亚美尼亚的国土里？&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/azerbaijan/az-map_hu_66307e9db75f2e6e.webp" width="330" height="355" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;纳希切万恰好就是纳卡地区的反面，虽然身处亚美尼亚腹地但生活的大部分却是阿塞拜疆人，名义上也是连亚美尼亚都承认的阿塞拜疆正统领土。这么一看，亚美尼亚反向申索对纳卡地区的控制似乎又显得没有那么强词夺理了。&lt;/p&gt;
&lt;p&gt;纳卡问题历史上发生过大大小小多次战争，总体来说，俄罗斯支持的是亚美尼亚，土耳其支持的是阿塞拜疆，两边都在打着代理人战争，长期以来，阿塞拜疆实际上是被亚美尼亚压着打，属于下风，毕竟整个高加索地区无论从历史还是现实来说都还是俄罗斯人控制的地盘。然而之所以在 2023 年一举被阿塞拜疆拿下纳卡地区，一方面是由于俄罗斯自己都在乌克兰战场自顾不暇，另一方面也是因为此时的亚美尼亚帕希尼扬政府也在寻求渐渐脱离俄罗斯的控制，转向美国甚至寻求加入欧盟，导致俄罗斯对亚美尼亚的态度也发生了逆转。&lt;/p&gt;
&lt;p&gt;事实上，以今天发生的现实来说，亚美尼亚这个国家的整体利益已经不能再一直为了另一个国家土地上的亚美尼亚裔人而一直被绑架，为了自身利益不得不放弃了对这块土地的实际控制和支援，然而纳卡飞地上的居民并不这么想，依然坚定认为自己就不属于阿塞拜疆，至少也应该维持自治地位，从而爆发了冲突，但是又孤立无援，进而被阿塞拜疆完全掌控。&lt;/p&gt;
&lt;h2 id="艺术品中的领土痕迹"&gt;艺术品中的领土痕迹&lt;/h2&gt;
&lt;p&gt;前面讲到了阿塞拜疆复杂的领土问题，所以自然阿塞拜疆人在这件事情上也非常敏感。在阿利耶夫文化中心，最让我印象深刻的是一系列用阿塞拜疆的传统工艺——毛毯和纺织——来表达阿塞拜疆完整国土的作品。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/azerbaijan/art-map1_hu_7014ad2a4c8b03b0.webp" srcset="https://blog.joway.io/images/azerbaijan/art-map1_hu_7b520827a6984c7a.webp 800w, https://blog.joway.io/images/azerbaijan/art-map1_hu_aabf2e38c4dc3cc8.webp 1200w, https://blog.joway.io/images/azerbaijan/art-map1_hu_7014ad2a4c8b03b0.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/art-map2_hu_8edd21aa341070c.webp" srcset="https://blog.joway.io/images/azerbaijan/art-map2_hu_da253c3a3bf1977.webp 800w, https://blog.joway.io/images/azerbaijan/art-map2_hu_8edd21aa341070c.webp 960w" sizes="(max-width: 800px) 100vw, 800px" width="960" height="1280" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="露天的战争博物馆"&gt;露天的战争博物馆&lt;/h2&gt;
&lt;p&gt;前面讲到在大部分历史时间里，阿塞拜疆一直是被亚美尼亚压着打，所以长期以来积累了无数的窝火，直到最近几年才胜仗频出，特别是一周多前彻底解决了纳卡问题，所以自然要向民众大肆宣扬战果。但我从来没有见过一个国家会对战争的宣扬像阿塞拜疆这么露骨，直接在市区腾出一大片空地，把真实的战场摆进市区，而且摆的还是亚美尼亚的军营，所有装备素材全部都是从亚美尼亚那里得到的战利品，从床铺，宣传海报到实实在在的坦克军车和飞机，直接 1:1 还原，甚至还建了大量战壕和铁丝网带。从博物馆角度来说，这是我去过的最真实的博物馆。从人性角度来说，看着那些小孩笑嘻嘻地在坦克上爬来爬去，心里很不是滋味。&lt;/p&gt;
&lt;p&gt;战利品的意思是，这里每一个军事装备背后至少有一条人命的丧生。&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-2_hu_5bf0fef1873966a4.webp" srcset="https://blog.joway.io/images/azerbaijan/war-2_hu_3410e55c49f6ab26.webp 800w, https://blog.joway.io/images/azerbaijan/war-2_hu_5ce1e46a8ead9593.webp 1200w, https://blog.joway.io/images/azerbaijan/war-2_hu_5bf0fef1873966a4.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-3_hu_50e0ed1815d5cf.webp" srcset="https://blog.joway.io/images/azerbaijan/war-3_hu_8d52c794eb8ffe6e.webp 800w, https://blog.joway.io/images/azerbaijan/war-3_hu_741cca338c45f45b.webp 1200w, https://blog.joway.io/images/azerbaijan/war-3_hu_50e0ed1815d5cf.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-4_hu_19a810b5599baf4.webp" srcset="https://blog.joway.io/images/azerbaijan/war-4_hu_e7eb44ea838bcb0e.webp 800w, https://blog.joway.io/images/azerbaijan/war-4_hu_ecc57f0096d54a97.webp 1200w, https://blog.joway.io/images/azerbaijan/war-4_hu_19a810b5599baf4.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-5_hu_b6d59bf9d446d8b6.webp" srcset="https://blog.joway.io/images/azerbaijan/war-5_hu_19a25518081f28f2.webp 800w, https://blog.joway.io/images/azerbaijan/war-5_hu_490886ebe13344be.webp 1200w, https://blog.joway.io/images/azerbaijan/war-5_hu_b6d59bf9d446d8b6.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-6_hu_d4222e8a86766d4b.webp" srcset="https://blog.joway.io/images/azerbaijan/war-6_hu_67754cb5d4801982.webp 800w, https://blog.joway.io/images/azerbaijan/war-6_hu_dec2488ca5eedfa2.webp 1200w, https://blog.joway.io/images/azerbaijan/war-6_hu_d4222e8a86766d4b.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-9_hu_8f2bf5271e6de658.webp" srcset="https://blog.joway.io/images/azerbaijan/war-9_hu_4adcdadc82c87f4c.webp 800w, https://blog.joway.io/images/azerbaijan/war-9_hu_f21723ec03f5e245.webp 1200w, https://blog.joway.io/images/azerbaijan/war-9_hu_8f2bf5271e6de658.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-10_hu_3228ba0fef6e4903.webp" srcset="https://blog.joway.io/images/azerbaijan/war-10_hu_a88ff8ccf4848bd9.webp 800w, https://blog.joway.io/images/azerbaijan/war-10_hu_d6ec2d34a433ffcc.webp 1200w, https://blog.joway.io/images/azerbaijan/war-10_hu_3228ba0fef6e4903.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-11_hu_c370c43e1507682e.webp" srcset="https://blog.joway.io/images/azerbaijan/war-11_hu_6ad9c129c27807d7.webp 800w, https://blog.joway.io/images/azerbaijan/war-11_hu_78c599a2aeee6137.webp 1200w, https://blog.joway.io/images/azerbaijan/war-11_hu_c370c43e1507682e.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-12_hu_bb5935c148465fd4.webp" srcset="https://blog.joway.io/images/azerbaijan/war-12_hu_686cfa9beffdfee7.webp 800w, https://blog.joway.io/images/azerbaijan/war-12_hu_f7c67ebbc5f1047c.webp 1200w, https://blog.joway.io/images/azerbaijan/war-12_hu_bb5935c148465fd4.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-15_hu_83d093d7c8a45b05.webp" srcset="https://blog.joway.io/images/azerbaijan/war-15_hu_87a90023a6c201dc.webp 800w, https://blog.joway.io/images/azerbaijan/war-15_hu_9b261f859760f647.webp 1200w, https://blog.joway.io/images/azerbaijan/war-15_hu_83d093d7c8a45b05.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-17_hu_ef988c0e088b265a.webp" srcset="https://blog.joway.io/images/azerbaijan/war-17_hu_b9e2d8a5be201824.webp 800w, https://blog.joway.io/images/azerbaijan/war-17_hu_e2395efbdfcdf6b6.webp 1200w, https://blog.joway.io/images/azerbaijan/war-17_hu_ef988c0e088b265a.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-18_hu_a0326dd268375a8a.webp" srcset="https://blog.joway.io/images/azerbaijan/war-18_hu_70eae33040642ac5.webp 800w, https://blog.joway.io/images/azerbaijan/war-18_hu_e8c48b9d197d0491.webp 1200w, https://blog.joway.io/images/azerbaijan/war-18_hu_a0326dd268375a8a.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-19_hu_16c09092e6392bf7.webp" srcset="https://blog.joway.io/images/azerbaijan/war-19_hu_a557fbb1b7fe0bb3.webp 800w, https://blog.joway.io/images/azerbaijan/war-19_hu_5aa56bc1b45cb5a6.webp 1200w, https://blog.joway.io/images/azerbaijan/war-19_hu_16c09092e6392bf7.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-21_hu_ede23fcc81b054e3.webp" srcset="https://blog.joway.io/images/azerbaijan/war-21_hu_a76a361d7630bba2.webp 800w, https://blog.joway.io/images/azerbaijan/war-21_hu_584ae353b0c6084f.webp 1200w, https://blog.joway.io/images/azerbaijan/war-21_hu_ede23fcc81b054e3.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-23_hu_602f0542adffc0c6.webp" srcset="https://blog.joway.io/images/azerbaijan/war-23_hu_5860bf5c09f0a272.webp 800w, https://blog.joway.io/images/azerbaijan/war-23_hu_627d5af98bfd59cd.webp 1200w, https://blog.joway.io/images/azerbaijan/war-23_hu_602f0542adffc0c6.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-24_hu_8275fbd77655f748.webp" srcset="https://blog.joway.io/images/azerbaijan/war-24_hu_ba3fa8c09527703b.webp 800w, https://blog.joway.io/images/azerbaijan/war-24_hu_b219ba53f87816ef.webp 1200w, https://blog.joway.io/images/azerbaijan/war-24_hu_8275fbd77655f748.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/war-26_hu_ec2496b028a71fb3.webp" srcset="https://blog.joway.io/images/azerbaijan/war-26_hu_59f9ff798feedbdf.webp 800w, https://blog.joway.io/images/azerbaijan/war-26_hu_e572a4b763e0b434.webp 1200w, https://blog.joway.io/images/azerbaijan/war-26_hu_ec2496b028a71fb3.webp 1280w" sizes="(max-width: 48em) 100vw, 400px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;p&gt;从照片的背景也能看出，这是一个接近市中心的地方，背后是静静地里海。我很难用复仇来形容这种公开的展示，因为大部分前来的阿塞拜疆人都是微笑的，甚至都没有很严肃对待，可能这是我永远无法理解的情感。&lt;/p&gt;
&lt;h2 id="流油的巴库"&gt;流油的巴库&lt;/h2&gt;
&lt;p&gt;阿塞拜疆首都巴库地区是世界上最早开采石油的地方，巴库油田有一个特点是分布分散，这导致这个国家到处都字面意义地在流着石油，也到处可以看到一些小到像是农具的石油钻井，还有一些地方存在着所谓的永恒之火。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/azerbaijan/oil-1_hu_6b9f5f8c1650a7e8.webp" srcset="https://blog.joway.io/images/azerbaijan/oil-1_hu_3346e9048ed28a8a.webp 800w, https://blog.joway.io/images/azerbaijan/oil-1_hu_37418e49569ac856.webp 1200w, https://blog.joway.io/images/azerbaijan/oil-1_hu_6b9f5f8c1650a7e8.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/gas-1_hu_ff7e17cc5db129b3.webp" srcset="https://blog.joway.io/images/azerbaijan/gas-1_hu_c069182d57c44fd7.webp 800w, https://blog.joway.io/images/azerbaijan/gas-1_hu_1dab714d10bea29b.webp 1200w, https://blog.joway.io/images/azerbaijan/gas-1_hu_ff7e17cc5db129b3.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/gas-2_hu_ab52085fb5c5e425.webp" srcset="https://blog.joway.io/images/azerbaijan/gas-2_hu_3285fa0151af5c8c.webp 800w, https://blog.joway.io/images/azerbaijan/gas-2_hu_ab52085fb5c5e425.webp 960w" sizes="(max-width: 800px) 100vw, 800px" width="960" height="1280" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="自由的巴库"&gt;自由的巴库&lt;/h2&gt;
&lt;p&gt;作为一个伊斯兰国家，巴库的自由程度是我在其他国家所没见过的，甚至是以世俗化著称的土耳其我觉得与之相比都有所逊色。不仅饮食上无所禁忌，甚至在博物馆里，到处都陈列着前卫露骨的艺术作品，在部分街区，有一种来到了巴黎的感觉。而在另一些角落，还能看到一些前苏联审美，更是让这里显得文化更为多元和包容。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/azerbaijan/art-0_hu_97e02da835065a98.webp" srcset="https://blog.joway.io/images/azerbaijan/art-0_hu_d38e6495033bec11.webp 800w, https://blog.joway.io/images/azerbaijan/art-0_hu_1a24e2b2ff19f773.webp 1200w, https://blog.joway.io/images/azerbaijan/art-0_hu_97e02da835065a98.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/art-1_hu_367b09849f5d3d5d.webp" srcset="https://blog.joway.io/images/azerbaijan/art-1_hu_cc35b1602f18dfb6.webp 800w, https://blog.joway.io/images/azerbaijan/art-1_hu_5812bae1a49b46be.webp 1200w, https://blog.joway.io/images/azerbaijan/art-1_hu_367b09849f5d3d5d.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/art-2_hu_1eea8434de1b87c6.webp" srcset="https://blog.joway.io/images/azerbaijan/art-2_hu_7424a48c0fcc325c.webp 800w, https://blog.joway.io/images/azerbaijan/art-2_hu_2e7095a3374d55ee.webp 1200w, https://blog.joway.io/images/azerbaijan/art-2_hu_1eea8434de1b87c6.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/art-3_hu_d553d64ca77dafca.webp" srcset="https://blog.joway.io/images/azerbaijan/art-3_hu_cd4a2cb81e793022.webp 800w, https://blog.joway.io/images/azerbaijan/art-3_hu_f93311a4b26972f1.webp 1200w, https://blog.joway.io/images/azerbaijan/art-3_hu_d553d64ca77dafca.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/azerbaijan/art-4_hu_426450b7a01b7499.webp" srcset="https://blog.joway.io/images/azerbaijan/art-4_hu_d5e2436db415cfac.webp 800w, https://blog.joway.io/images/azerbaijan/art-4_hu_4874663f8ca21a68.webp 1200w, https://blog.joway.io/images/azerbaijan/art-4_hu_426450b7a01b7499.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="高加索三国 - 阿塞拜疆行记" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="离开阿塞拜疆"&gt;离开阿塞拜疆&lt;/h2&gt;
&lt;p&gt;事实上，之所以把阿塞拜疆作为高加索三国的第一站，也是因为阿塞拜疆的特殊政治。因为阿塞拜疆对亚美尼亚有着极为强烈的仇恨，导致如果是先去了亚美尼亚，会极难入境阿塞拜疆，而亚美尼亚对阿塞拜疆的旅游经历倒不特别关心。但是这两国之间本身并不开放通行，所以为了去亚美尼亚，还得先到格鲁吉亚中转。&lt;/p&gt;
&lt;p&gt;即便你生性不关心政治，但是在高加索，政治才是这里最大的景观。&lt;/p&gt;</content:encoded></item><item><title>2024 投资组合年报</title><link>https://blog.joway.io/posts/2024-portfolio-annual-report/</link><pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2024-portfolio-annual-report/</guid><category>Invest</category><description>2024 年，是我大规模重构投资组合的一年，主要还是由于在新加坡的身份转变后，具备了投资新加坡房产的资格，从而让投资结构从简单的股债配比转向了更多元复杂并且加入了房贷这一杠杆的组合。
我的投资组合计算 我在思考投资组合的时候并非像部分人一样，只是把资产的一部分活钱作为投资资产然后仅计算这部分资产的涨跌。这种方式虽然计算上更加简单，但是实际上会出现股票账户翻倍，个人总资产却没啥波动的情况。所以我会把所有资产甚至是公司期权和公积金都算入投资组合中。</description><content:encoded>&lt;p&gt;2024 年，是我大规模重构投资组合的一年，主要还是由于在新加坡的身份转变后，具备了投资新加坡房产的资格，从而让投资结构从简单的股债配比转向了更多元复杂并且加入了房贷这一杠杆的组合。&lt;/p&gt;
&lt;h2 id="我的投资组合计算"&gt;我的投资组合计算&lt;/h2&gt;
&lt;p&gt;我在思考投资组合的时候并非像部分人一样，只是把资产的一部分活钱作为投资资产然后仅计算这部分资产的涨跌。这种方式虽然计算上更加简单，但是实际上会出现股票账户翻倍，个人总资产却没啥波动的情况。所以我会把所有资产甚至是公司期权和公积金都算入投资组合中。&lt;/p&gt;
&lt;p&gt;我不会记录每笔交易，因为不只是投资，生活中处处存在着交易，记账不完整等于没有记账。但是我会大约每个季度花一个小时左右的时间，基于那个时间点的最终资产份额以及价格，构建一个 Snapshot ，这样我可以建立起一个「资产状态」的时间序列：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/0_hu_3f782c464c3c46e4.webp" srcset="https://blog.joway.io/images/2024-portfolio/0_hu_a0995ea10c5bcaf7.webp 800w, https://blog.joway.io/images/2024-portfolio/0_hu_ad6ac7b0f31b455a.webp 1200w, https://blog.joway.io/images/2024-portfolio/0_hu_3f782c464c3c46e4.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="41" alt="2024 投资组合年报" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;基于这些表之间的 Diff，我能够清楚知道任意两个时间段间，我做了哪些有实质性大变化的操作，一些小操作比如个股的小规模买入卖出则会反应在最终资金的盈亏上但是剔除交易本身的细节。&lt;/p&gt;
&lt;h2 id="2024-的重大操作"&gt;2024 的重大操作&lt;/h2&gt;
&lt;h3 id="更换券商"&gt;更换券商&lt;/h3&gt;
&lt;p&gt;由于众所周知的原因和对未来不确定风险的防御，我在 2 月从富途彻底转到了 IBKR ， 转仓方式是现在富途先全部卖出所有股票然后买入 JEPI ， 然后通过 JEPI 这一更稳定的 SPY 载体， 转仓到 IBKR。&lt;/p&gt;
&lt;p&gt;原本的预期是这样做，可以在 SPY 不暴涨的情况下，最小化中间时间的收益损失。且不用付高额的银行电汇手续费。不过转仓时，SPY 有了一些下跌，所以马后炮看，远不如当时直接换成现金，再在 IBKR 买入。不过好在最终损失也不大。&lt;/p&gt;
&lt;h3 id="大规模买入美债后再卖出"&gt;大规模买入美债后再卖出&lt;/h3&gt;
&lt;p&gt;2024 年中的时候，10 年期美债利率已经来到了 4.9% ， 在上半年我一直在构建我的债券阶梯，具体方法是定投买入从 1 年期到 15 年期的美债，每个期限的美债份额固定。这样做就几个好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我每半年都会有一大笔美债利息到账，可以继续拿来投资。&lt;/li&gt;
&lt;li&gt;如果我都采取持有到期的策略，我在未来 15 年每年都会有一笔美债到期，我可以根据当时的利率以及市场，决定是否继续买入新的 15 年美债，抑或是直接投资到股票市场。&lt;/li&gt;
&lt;li&gt;如果人生出现了变化，需要用到大笔资金，由于我是在利率非常高的时候买入的美债，且平均期限才 7.5 年，我亏本卖出债券的概率非常低，可以直接把这部分美债作为活钱来用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后的事实也证明了这个策略非常明智，我最终走到了第三步方案，在 8 月因为要准备购房资金清仓了刚建完的美债阶梯。巧合与惊喜的是，在 2024 年 9 月的时候，美债利率还不降反升了，而我的美债都是在 9 月之前卖出的，所以还躲过了一波下跌。最终以盈利几千美元结束了这笔因为生活改变引发的混乱操作。&lt;/p&gt;
&lt;h3 id="精简持仓专注两大指数"&gt;精简持仓，专注两大指数&lt;/h3&gt;
&lt;p&gt;过去我虽然相信指数是穷人的最强武器，但是依然忍不住发挥自己的主观能动性在这个信念之上做一些微操。比如买入 JEPI。&lt;/p&gt;
&lt;p&gt;JEPI 是在 SPY 基础上执行 Covered Call 策略的类指数基金。它的好处是，在指数震荡或者下跌或者微涨的时候依然能盈利，暴涨的时候会损失超过正常范围的涨幅。所以它可以被理解是一个更为稳健的 SPY 指数。但是 2024 年是美股疯涨的一年，所以这个策略就不生效了。&lt;/p&gt;
&lt;p&gt;在年中的时候，我重新坚定了对于指数的信念，放弃了引入任何一点点个人自作聪明的主观判断，把大部分的投资资金全部放入 SPY 和 QQQ 的两大指数中，并且决定即便未来一定要操作，也只调整这两个指数之间的比例分配。及时悬崖勒马让我守住了今年指数的大部分收益，虽然总体上依然跑输了这两大指数。&lt;/p&gt;
&lt;h3 id="通过银行贷款无风险套利"&gt;通过银行贷款无风险套利&lt;/h3&gt;
&lt;p&gt;正常情况下，我是绝对不会在投资上用任何杠杆。但是因为房贷，我被迫还是上了杠杆。&lt;/p&gt;
&lt;p&gt;受益于新加坡金管局掏出自己真金白银对通货膨胀的压制，即便是在 2024 年 8 月这个利率高点时期，新加坡的 30 年房贷利率也只有 2.65% （固定 2 年， 但是 2 年后大概率只会更低）。而此时，新加坡政府债券的利率都能轻轻松松到 2.8% 。&lt;/p&gt;
&lt;p&gt;我在之前利率高点时期，囤积了大量利率高于 3% 的新加坡债券，以及利率高于 4.5% 的美债，而此时新加坡银行又愿意以低息借我钱，这不是送钱我想不到什么算送钱了。&lt;/p&gt;
&lt;p&gt;虽然美债利率远高于贷款利率，但是美国这国家过于动荡，汇率风险太高，所以我卖出了大部分美债，但是新加坡债券只卖出到刚好够凑够首付为止。这样，我就可以在无汇率风险的情况下，以 AAA 的评级安全性来套利新加坡银行。&lt;/p&gt;
&lt;p&gt;这还只是无风险套利，我还有一大部分资金是在指数资产上，从过去 100 年的历史看，30 年尺度的年化收益率应该在 8% 左右，从风险套利看利润就更大了。&lt;/p&gt;
&lt;h2 id="投资账户收益报告"&gt;投资账户收益报告&lt;/h2&gt;
&lt;p&gt;最后，简单贴下我今年投资账户侧的收益报告：年化收益 17% ，最大月回撤 2.8% 。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/1_hu_649a6b1d6ba4c3cc.webp" srcset="https://blog.joway.io/images/2024-portfolio/1_hu_da5376d952878b4f.webp 800w, https://blog.joway.io/images/2024-portfolio/1_hu_649a6b1d6ba4c3cc.webp 1038w" sizes="(max-width: 800px) 100vw, 800px" width="1038" height="164" alt="2024 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/2_hu_a1401d5a93a36632.webp" srcset="https://blog.joway.io/images/2024-portfolio/2_hu_f91a40fa79d3a932.webp 800w, https://blog.joway.io/images/2024-portfolio/2_hu_cfbd4fce6721ef97.webp 1200w, https://blog.joway.io/images/2024-portfolio/2_hu_a1401d5a93a36632.webp 1500w" sizes="(max-width: 800px) 100vw, 800px" width="1500" height="620" alt="2024 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/3_hu_a671e18967d90483.webp" srcset="https://blog.joway.io/images/2024-portfolio/3_hu_b0d45b3f51ac663c.webp 800w, https://blog.joway.io/images/2024-portfolio/3_hu_a128c75d78ac6914.webp 1200w, https://blog.joway.io/images/2024-portfolio/3_hu_a671e18967d90483.webp 1470w" sizes="(max-width: 800px) 100vw, 800px" width="1470" height="536" alt="2024 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/4_hu_39d7049b0c9161a1.webp" srcset="https://blog.joway.io/images/2024-portfolio/4_hu_803109a33e3da390.webp 800w, https://blog.joway.io/images/2024-portfolio/4_hu_ea7d84c6bc245a16.webp 1200w, https://blog.joway.io/images/2024-portfolio/4_hu_39d7049b0c9161a1.webp 1346w" sizes="(max-width: 800px) 100vw, 800px" width="1346" height="780" alt="2024 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/2024-portfolio/5_hu_38066b96993f6a21.webp" srcset="https://blog.joway.io/images/2024-portfolio/5_hu_277f9cc82e99fe6.webp 800w, https://blog.joway.io/images/2024-portfolio/5_hu_fcde33f7a62c5555.webp 1200w, https://blog.joway.io/images/2024-portfolio/5_hu_38066b96993f6a21.webp 1432w" sizes="(max-width: 800px) 100vw, 800px" width="1432" height="702" alt="2024 投资组合年报" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="未来展望"&gt;未来展望&lt;/h2&gt;
&lt;p&gt;这几年美股的牛市基本只要在场，收益都不会差，和个人努力和能力基本没啥太大关系。从长期看，现在的暴涨一定会在未来产生均值回归的重力。所以我丝毫不认为现在的涨势未来会继续，但同时我也绝对不会放弃信仰去清仓。甚至于，我现在的心态，是在持仓的同时，迎接暴跌。这是我认为的最健康的投资心态。&lt;/p&gt;
&lt;p&gt;之所以能这么自信，是因为首先我的杠杆实际上非常低，而且房贷实际上是属于最安全的杠杆，特别是新加坡的房贷。新加坡政府在过去几年利率暴涨的时代，已经用能力证明了政府的金融能力足够应对外部环境的变化。在欧美房贷固定期限的利率普遍 5% 以上的时候，新加坡的利率仍然属于非常低的水平，甚至比中国还低。&lt;/p&gt;
&lt;p&gt;其次，我的年龄还不到 30 岁，从长期利益来看，最好 SPY 现在跌个 90% 最符合我的利益。因为大概率我投资的钱要到 30 年后才会进入提现周期。尤其是，我现在已经买完房后，基本看不到未来要用大笔现金支出的场景。所以要论我的心情，每次下跌我的开心反而会比上涨时更大。&lt;/p&gt;</content:encoded></item><item><title>重新思考 Go：了解程序在线上是如何运行的</title><link>https://blog.joway.io/posts/golang-rethink-program-in-realworld/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-program-in-realworld/</guid><category>Tech</category><description> 重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。
前言 过去一段时间，在大量的线上服务 case study 过程中，逐步深入了解了如今的业务 Go 进程是如何在一系列繁杂的基础设施之上运行的。有些表现在意料之中，也有一些出乎意料的发现。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;过去一段时间，在大量的线上服务 case study 过程中，逐步深入了解了如今的业务 Go 进程是如何在一系列繁杂的基础设施之上运行的。有些表现在意料之中，也有一些出乎意料的发现。&lt;/p&gt;
&lt;p&gt;我很喜欢一个叫做「 机械同理心/Mechanical Sympathy 」的概念，大体的意思是你必须深刻了解你的程序/机械装置是在一种怎么样的环境下运行的，设身处地地在这个运行环境下去思考，才能帮助你写出更好的程序，或是解答一些奇怪现象的原因。&lt;/p&gt;
&lt;p&gt;本文希望达到的目的，也是构建这份「机械同理心」。&lt;/p&gt;
&lt;h2 id="程序能使用多少计算资源"&gt;程序能使用多少计算资源？&lt;/h2&gt;
&lt;p&gt;对于很多人来说，这个问题似乎非常简单，大多的计算平台都会要求你建立服务的时候就指定好「需要的计算资源」，但这与你的「能使用的计算资源」是两件事情。实际上这个问题的复杂性远远超出大部分人现象，甚至都不是一个常量，也无法使用公式简单计算。抽象地讲，这取决于天时地利人和。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「天时」指的是内核 Cgroups 以及容器调度平台的基本原理和参数设置。这是硬指标，大部分时候也是常量。&lt;/li&gt;
&lt;li&gt;「地利」指的是部署的实际物理机的繁忙程度。&lt;/li&gt;
&lt;li&gt;「人和」指的是写程序的人得在能够有更多计算资源可用的时候真的让自己程序用上这些计算资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们逐步分节来详细探究这个问题。&lt;/p&gt;
&lt;h3 id="被封装的-cpu-谎言"&gt;被封装的 CPU 谎言&lt;/h3&gt;
&lt;p&gt;在 Oncall 中常见的一个误解是，研发人员在容器平台上申请了 4 核 CPU 的容器，然后自然而然认为自己的程序最多只能使用 4 个 CPU，于是按照这个计算能力去估算需要的容器数量，以及对自己程序套上这个假设进行参数调优。&lt;/p&gt;
&lt;p&gt;上线后，进到容器用 top 一看，各项指标确实是按照 4 核的标准在进行。甚至用 &lt;code&gt;cat /proc/cpuinfo&lt;/code&gt; 一看，不多不少刚好看到有 4 个 CPU。。。&lt;/p&gt;
&lt;p&gt;但实际上，这一切都只是容器平台为你封装出来的一个美好的假象。之所以要把这个假象做的这么逼真，只是为了让你摆脱编程时的心智负担，顺便再让那些传统的 Linux 观测工具在容器环境中也能正常运行。&lt;/p&gt;
&lt;p&gt;但是假象终究不是真相，如果有一天你遇到一些疑难问题或是想要去编写极致性能的代码，你会发现这些封装出来的抽象把你的理解带的有多偏。&lt;/p&gt;
&lt;h3 id="我到底能同时使用多少个-cpu-"&gt;我到底能同时使用多少个 CPU ？&lt;/h3&gt;
&lt;p&gt;在 K8s 的环境里，CPU 是一个时间单位而非数量。正常情况下，一个拥有 64 核心的机器，你最多能同时使用的理论 CPU 个数是 64 ，即便你创建容器的时候申请的是 4 核。&lt;/p&gt;
&lt;p&gt;但是由于你申请的是所谓的 4 CPU，所以你最终使用的 CPU 时间加起来最多只能相当于 4 CPU 的时间。&lt;/p&gt;
&lt;p&gt;即，程序的最大并行数 = 物理机 CPU 核心个数；程序的最大执行时间 = 容器 CPU Quota 。&lt;/p&gt;
&lt;h3 id="go-程序能同时使用多少个-cpu-"&gt;Go 程序能同时使用多少个 CPU ?&lt;/h3&gt;
&lt;p&gt;上一节讲的是容器本身的最大并行数并没有明确的限制，上限是 CPU 核心数量。用多用少实际上完全取决于你的程序自身设计。&lt;/p&gt;
&lt;p&gt;但由于 Golang 对并行数的设计是隐藏在 Runtime 中的，也就是所谓的 GOMAXPROCS。这个值并没有一个明确的 Guideline 告诉用户应该如何调整，实际上不同公司的部署架构以及容器平台实现对于这个值的调整都会有截然不同的标准。被广泛采用的是 &lt;a href="https://github.com/uber-go/automaxprocs"&gt;Uber 的标准&lt;/a&gt; ，即根据容器的 CPU Quota 上限来设置 GOMAXPROCS 。所以虽然 K8s 对容器的原本设定虽然不限制你同时使用的 CPU 个数，但其实在 Uber 规范的这种隐式行为设定下，容器的 CPU quota 实际上间接决定了程序的最大并发数。&lt;/p&gt;
&lt;p&gt;假如你是一个 1 秒钟内，会同时运行 16 个并行任务且每个仅持续 10ms 的程序，当你部署在一个申请了 4 个 CPU quota 的容器中时，你会发现虽然你 CPU 利用率极低，但是程序无论如何都跑不快。这就是因为这套 GOMAXPROCS 设置给你创造了一个自我受限的运行环境。&lt;/p&gt;
&lt;h3 id="我到底能用多少计算资源"&gt;我到底能用多少计算资源&lt;/h3&gt;
&lt;p&gt;前面讲的多少个 CPU 并不等价于「计算资源」，计算资源通俗理解就是单位时间内，这物理机的所有 CPU 核心最终能轮到执行你程序的时间。&lt;/p&gt;
&lt;p&gt;你在 K8s 上配置的 CPU quota 之所以叫做 quota ，是说你最多可以运行到这个数字，但不代表一定给你这个数字的计算资源。至于你真的能够有多少计算资源，说实话，nobody knows &amp;hellip;&amp;hellip;.&lt;/p&gt;
&lt;p&gt;一般来说，这个问题取决于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;物理机自身负载：如果物理机负载已经很高 ，你基本上很难有运气运行满 CPU quota 标注的计算量。比如我在某容器平台上部署一个完全无业务逻辑的 PingPong 服务，它的延迟也会随着一天时间变化而变化。这是因为物理机的繁忙程度在变化，所以实际上你程序可获得的计算资源也在变化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序设计问题：假设你是一个单线程程序，你的上限也就 1 CPU 运行满，自然无法使用满全部的计算资源。你可能会说我是一个 Golang 程序，不是单线程程序。然而 Go 的设计偏向于高并发低并行，许多 Go 代码在调度器的作用下，实际上就是一个倾向于单线程运行的代码。这其中的奥秘是后续深入展开的重点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="go-如何调度计算资源"&gt;Go 如何调度计算资源&lt;/h2&gt;
&lt;p&gt;每一个 Goroutine 都可以理解为一个任务，CPU 是任务的执行器。一个任务从「可以运行 - Ready」到 「开始运行 - Running 」的时间即为调度延迟。&lt;/p&gt;
&lt;p&gt;调度延迟的产生可能是因为 CPU 太忙在执行其他 Goroutine 甚至是其他进程的线程，也可能是因为虽然有其他 CPU 空闲，但是我任务挂在本地线程队列里，没有被其他 CPU 上的线程拿取。&lt;/p&gt;
&lt;h3 id="调度时机与开销"&gt;调度时机与开销&lt;/h3&gt;
&lt;p&gt;一个 Goroutine 被真正调度到执行存在下以下时机：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;【开销最低】前一个 Goroutine 执行完毕或者主动进入 Waiting 状态，当前 G 存在于当前 P 的 local runq 队列&lt;/li&gt;
&lt;li&gt;【开销低】前一个 Goroutine 执行时间过长(~=10ms)，在调用它的某个函数时，执行了协作式抢占，当前 G 存在于当前 P 的 local runq 队列&lt;/li&gt;
&lt;li&gt;【开销中】sysmon 线程异步抢占（通过中断信号）执行时间过长的 Goroutine，强行发起调度。当前 G 存在于当前 P 的 local runq 队列。&lt;/li&gt;
&lt;li&gt;【开销大】上述事件发生时，当前 local runq 队列不存在可运行 G，从全局队列中获取当前 G&lt;/li&gt;
&lt;li&gt;【开销最大】上述事件发生时，local 和 全局 runq 都不存在可运行 G，从别的 P 偷取任务&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="调度状态切换"&gt;调度状态切换&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// G1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// G2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// do something&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面这段代码，一共有两个 Goroutine: G1, G2。&lt;/p&gt;
&lt;p&gt;这两个 Goroutine 的状态变化为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;G1 runnable=&amp;gt;running: 一开始函数开始运行&lt;/li&gt;
&lt;li&gt;G2 dead=&amp;gt;runnable: 创建完 G2 后，初始状态&lt;/li&gt;
&lt;li&gt;G1 running=&amp;gt;waiting: 阻塞在 select 语句&lt;/li&gt;
&lt;li&gt;G2 runnable=&amp;gt;running: G2 开始运行。有可能和 G1 在同一个 P，也可能被别的 P 偷走了。&lt;/li&gt;
&lt;li&gt;G2 running=&amp;gt;waiting: 阻塞在 channel 操作。因为 done 是无缓冲的，所以此时 G2 并不会退出&lt;/li&gt;
&lt;li&gt;G1 waiting=&amp;gt;runnable: channel send 会知道 G1 阻塞在这里，所以会先修改 G1 状态为 runnable&lt;/li&gt;
&lt;li&gt;G1 runnable=&amp;gt;running: G1 恢复执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然这段代码很简单，但存在多次协程切换。这还不算每一行代码之间还可能存在异步抢占（假设代码逻辑复杂的话）。&lt;/p&gt;
&lt;p&gt;每一次切换，都意味着下一次必须依赖 runtime 调度器调度才能获得执行机会。&lt;/p&gt;
&lt;h3 id="调度的代价"&gt;调度的代价&lt;/h3&gt;
&lt;p&gt;每一个 Goroutine 从 runnable =&amp;gt; running 都会存在一定延迟。这部分延迟称为调度延迟。&lt;/p&gt;
&lt;p&gt;每一次调度，都不保证会立刻执行，也不保证一定在某个 P 上执行，更不保证一定在一个物理线程 M 上执行。&lt;/p&gt;
&lt;p&gt;所以即便上述代码理论上应该是一个纳秒级别的代码开销，在真实繁忙的场景下，调度延迟很可能在毫秒级别。甚至 5ms 以上也是常见线上情况。&lt;/p&gt;
&lt;p&gt;除此之外，这里假设的是底层物理 CPU 资源是充足的情况下，而真实线上线程本身也有内核的调度延迟，甚至是 cgroup throttle 故意休眠线程。此时真正看到的延迟甚至能达到几十毫秒级别。&lt;/p&gt;
&lt;h3 id="衡量调度开销"&gt;衡量调度开销&lt;/h3&gt;
&lt;p&gt;调度开销都在火焰图的 &lt;code&gt;runtime.mcall&lt;/code&gt; stack 下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 &lt;code&gt;checkTimers&lt;/code&gt; 开销特别大，意味着你一定创建了大量 timer，且这些 timer 还大概率都能到期。如文章开头这种超时控制的 timer，虽然很多，但是绝大部分都不会到期，所以性能损耗还好。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;mPark&lt;/code&gt; 开销特别大，意味着你线程切换频率很高。比如你每秒运行一次，每一次创建几百个 goroutine 批处理一些任务，然后 10ms 后就完成了，休眠 990ms。那么每一次你程序运行任务，都要唤醒一大批已经因为没事可做被休眠的线程。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;runtime.netpoll&lt;/code&gt; 开销特别大，大概率你访问数据库等网络 IO 特别频繁。注意，Kitex 框架时，网络并不走 runtime 所以并没有这个开销。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;stealWork&lt;/code&gt; 开销特别大，大概率你的程序经常在一个 goroutine 中，一次性创建一大堆 goroutine，这样导致大量 goroutine 积压在本地 local runq，依赖其他 P 去偷走执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="go-gc-如何影响延迟"&gt;Go GC 如何影响延迟&lt;/h2&gt;
&lt;p&gt;和所有依赖 GC 的编程语言一样，GO 的 GC 延迟也是一大延迟来源。&lt;/p&gt;
&lt;h3 id="触发-gc-的时机"&gt;触发 GC 的时机&lt;/h3&gt;
&lt;p&gt;除去长时间不发生 GC 引起的定时 GC 外，GC 通常发生在用户创建堆对象调用 mallocgc 函数时。例如如下代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编程者的通常预期最差这行代码也应该在 1ms 之内完成，然而实际上由于这个操作有可能被 Runtime 切去帮助执行 GC 的任务，所以该调用的延迟是完全不受控的，甚至长达两位数毫秒也是可能的。&lt;/p&gt;
&lt;h3 id="gc-流程"&gt;GC 流程&lt;/h3&gt;
&lt;p&gt;Go GC 分为 Mark 和 Sweep 两个阶段。&lt;/p&gt;
&lt;h4 id="mark---标记对象"&gt;Mark - 标记对象&lt;/h4&gt;
&lt;p&gt;Go 的三色扫描 Mark 过程的原理相对简单，即把全局变量，所有 Goroutine 的栈上变量作为根对象开始扫描标记。&lt;/p&gt;
&lt;p&gt;如果某一个堆变量已经不被持有，则没有 Mark 阶段的成本。&lt;/p&gt;
&lt;h4 id="sweep---清理释放对象的内存"&gt;Sweep - 清理释放对象的内存&lt;/h4&gt;
&lt;h5 id="分配器协助-sweep"&gt;分配器协助 Sweep&lt;/h5&gt;
&lt;p&gt;Sweep 被嵌入到程序的每一次 Malloc 操作中判断是否要额外执行 sweep 操作。也就是说，程序即便只是调用一个 make([]int, 1024) ，也可能会被切去执行 sweep，从而产生用户意想不到的延迟。&lt;/p&gt;
&lt;p&gt;在 Mallocgc 的时，如果当前 mcache span 没有空间，且上一次 GC mark 后还有 sweep 工作没有完成，则协助参与 sweep，从所有没有被 sweep 的 span 中，找到能够分配当前需要内存的 span 。一直会执行 100 次（Go &amp;gt;= 1.19 前是死循环一直执行直到找到或者完成 sweep 工作）。&lt;/p&gt;
&lt;p&gt;所以如果存在大量没有空余对象可以分配的 span 会极大影响分配器效率。&lt;/p&gt;
&lt;h5 id="后台-bgsweep"&gt;后台 bgsweep&lt;/h5&gt;
&lt;p&gt;bgsweep goroutine 是低优先级的异步 sweep，在每次 GC Mark 结束后，会被唤起。&lt;/p&gt;
&lt;p&gt;每 sweep 10 个 span，就判断下是否有其他 idle P，没有就 gosched 让其他 goroutine 执行。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;bgsweep&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sweep_10_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;goschedIfBusy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;goschedIfBusy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;gp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;npidle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mcall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gosched_m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="衡量-mark--sweep-开销"&gt;衡量 Mark &amp;amp; Sweep 开销&lt;/h3&gt;
&lt;p&gt;系统中通常存在两种生命周期的对象：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;longLife&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;shortLife&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 因为 n 是变量，所以 shortLife 会分配在堆上&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;longLife&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="对于短生命周期的对象"&gt;对于短生命周期的对象&lt;/h4&gt;
&lt;h5 id="mark-开销较低"&gt;Mark 开销较低&lt;/h5&gt;
&lt;p&gt;因为 GC 只会扫描当前有指针指向的对象，短生命周期对象已经没有 goroutine 持有了，所以不需要被扫描。&lt;/p&gt;
&lt;h5 id="sweep-开销较低"&gt;Sweep 开销较低&lt;/h5&gt;
&lt;p&gt;理论上，sweep 开销应该是和长生命周期一样的。但是这里有一个问题是，RPC 服务的对象一般是突然分配一波，例如 RPC Request/Response 结构体，且由于需要分配大量对象，这类对象大概率都集中在一些独占的 span 中。也就是说多个对象占用同一个 span，即 span 密度很高 。&lt;/p&gt;
&lt;p&gt;而 Sweep 的开销与 span 个数相关，也就是说同样的对象数量，所涉及到的 span 数量更少。而且一旦 Mark 后，整段 span 剩余空间很大，可以继续分配。&lt;/p&gt;
&lt;h4 id="对于长生命周期的对象"&gt;对于长生命周期的对象&lt;/h4&gt;
&lt;h5 id="mark-开销较高"&gt;Mark 开销较高&lt;/h5&gt;
&lt;p&gt;由于每次 GC 都需要扫描这类对象，如果这类对象就不会释放，这里的扫描都是浪费的。&lt;/p&gt;
&lt;h5 id="sweep-开销较高"&gt;Sweep 开销较高&lt;/h5&gt;
&lt;p&gt;和前面的原因一样，一直需要去重复 sweep 相同的 span。&lt;/p&gt;
&lt;p&gt;此外，由于这类对象大概率也是被集中分配出来的，所以他们也会集中在小部分 span 中，而这些对象如果整体都不释放，这类 span 的空余空间很少，这就导致 malloc 的时，需要先去处理这些 sweep 工作，进而导致分配延迟极大。&lt;/p&gt;
&lt;h3 id="numa-架构下-sweep-额外的访存延迟"&gt;NUMA 架构下 Sweep 额外的访存延迟&lt;/h3&gt;
&lt;p&gt;NUMA 架构的一大缺点是在出现跨 NUMA 节点访问内存时，会出现巨大的访问内存延迟。&lt;/p&gt;
&lt;p&gt;正常 Go 程序都符合局部性原理，不太可能出现极高密度跨节点访问内存的情况，但是 sweep 过程不一样，它的代码设计就是不同线程从一个全局队列中取可能在不同 NUMA node 上分配的地址，所以这个阶段会出现大量跨节点访问内存的情况。&lt;/p&gt;
&lt;p&gt;PS：其他带 GC 的语言其实也是这几年才引入 NUMA 架构的优化的，比如 &lt;a href="https://blog.gceasy.io/2023/07/04/java-zgc-algorithm-tuning/"&gt;Java&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;根据上面罗列的众多线上 Case 总结，我们可以看到哪怕是一个最简单的并发程序，也可能并非我们单纯从程序自身所看到的那样简单地运行，既可能被物理资源设定所间接影响，也可能被 Go Runtime 注入执行非代码所描述的不相关任务。要在一个计算资源不稳定，执行代码段也不稳定的现实世界，写出一段执行时间稳定的 Go 代码并非易事。&lt;/p&gt;
&lt;p&gt;即便 Go 编译器做出十足的进步，以至于 Go 静态编译的出来的代码性能能够媲美 C 编译器。实际运行时，这套 Runtime 机制依然会造成程序 avg 性能持平 C 但是有 1% 的概率突然延迟抖动一下。而实际上研发在做服务容量规划时，看的往往就是这 1% 概率出现的性能短板，为这 1% 的情况堆上大部分时候可能并用不到的计算资源。&lt;/p&gt;
&lt;p&gt;虽然这里的每一个子问题或多或少都有一定的通用解法，但对程序性能影响最大的往往就是程序自身的代码。只有了解这些限制与原理，才能针对不确定的现实世界写出相对有确定性性能的代码。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 环勃朗峰之旅</title><link>https://blog.joway.io/posts/tmb/</link><pubDate>Fri, 18 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/tmb/</guid><category>Travel</category><description>TMB - Tour du Mont Blanc 很多年前就听说勃朗峰周边有一条多日徒步线路，从霞慕尼出发，环绕勃朗峰徒步一整圈最后回到霞慕尼。但是由于很难请到这么长时间的假期，所以一直没能如愿。甚至四年前的时候还去到过霞慕尼，但是那时候只是把它作为一个景点，简单去了下南针峰从山顶观看勃朗峰。那时甚至都霞慕尼这个地方大头来头，也没来得及在当地住一晚细细品味它的户外氛围。</description><content:encoded>&lt;h2 id="tmb---tour-du-mont-blanc"&gt;TMB - Tour du Mont Blanc&lt;/h2&gt;
&lt;p&gt;很多年前就听说勃朗峰周边有一条多日徒步线路，从霞慕尼出发，环绕勃朗峰徒步一整圈最后回到霞慕尼。但是由于很难请到这么长时间的假期，所以一直没能如愿。甚至四年前的时候还去到过霞慕尼，但是那时候只是把它作为一个景点，简单去了下南针峰从山顶观看勃朗峰。那时甚至都霞慕尼这个地方大头来头，也没来得及在当地住一晚细细品味它的户外氛围。&lt;/p&gt;
&lt;p&gt;今年十月，终于得愿完成了这条此生必去的徒步线路。出于安全和方便的考虑，我在网上订了一个包含了住宿餐食和山地向导的国人徒步团，基本把除了走路之外的麻烦事都帮我解决了，每天就是到点徒步，到站睡觉。唯一的遗憾是，这次走的是所谓的精华路线而非完整路线，大概只覆盖了完整线路的一半左右。希望未来有一天可以以个人的形式重新完整走一次这条线路。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/map_hu_d7476a9bd4af53b0.webp" srcset="https://blog.joway.io/images/tmb/map_hu_2d95b5c54a5bd3f3.webp 800w, https://blog.joway.io/images/tmb/map_hu_1a6e5045849bc300.webp 1200w, https://blog.joway.io/images/tmb/map_hu_d7476a9bd4af53b0.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1086" alt="欧游散记 —— 环勃朗峰之旅" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="行前准备"&gt;行前准备&lt;/h2&gt;
&lt;h3 id="身体准备"&gt;身体准备&lt;/h3&gt;
&lt;p&gt;在 2024 年前半年的时间里，颈椎病让我的身体状态特别糟糕，别说徒步，感觉长时间的伏案工作都已经很成问题。所以我上半年的大部分业余时间都花在了锻炼身体和用各种手段治疗颈椎病上。这也让我的身体素质得以迅速提升，刚好在九月已经恢复到几年前的水平。&lt;/p&gt;
&lt;p&gt;由于长期居住在新加坡，所以很难找到一些较大强度的多日运动能够来测试我的身体状态，这次，我也是拿 TMB 来为我半年的身体训练做一次压力测试。说实话，在去之前，我是做好了不行就撤退的准备。所幸的是，我不仅没有撤退，甚至走的还算是在队伍的前列，这可能是整趟旅程最让我高兴的事情。&lt;/p&gt;
&lt;h3 id="装备准备"&gt;装备准备&lt;/h3&gt;
&lt;p&gt;十月 TMB 的气温大概在 5 度到 20 度左右，我最后真正用到的装备有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;防护层
&lt;ul&gt;
&lt;li&gt;1 件 Patagonia Boulder Fork Rain Jacket&lt;/li&gt;
&lt;li&gt;1 条 Outdoor Research Ferrosi Convertible Pants&lt;/li&gt;
&lt;li&gt;1 条 KAILAS 徒步裤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;中间层
&lt;ul&gt;
&lt;li&gt;1 件 Patagonia Down Sweater&lt;/li&gt;
&lt;li&gt;1 件 Patagonia R1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;贴身层
&lt;ul&gt;
&lt;li&gt;1 件 Decathlon 美丽奴羊毛 T 恤&lt;/li&gt;
&lt;li&gt;1 件 Smartwool 美利奴羊毛衣&lt;/li&gt;
&lt;li&gt;1 条 Icebreaker 美利奴羊毛裤&lt;/li&gt;
&lt;li&gt;2 双 Smartwool Full Cushion 美利奴羊毛袜&lt;/li&gt;
&lt;li&gt;4 条 Smartwool 美利奴羊毛内裤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;徒步装备
&lt;ul&gt;
&lt;li&gt;Hanchor MARL X 轻量化背包&lt;/li&gt;
&lt;li&gt;Salomon X Ultra 4 Mid GTX 徒步鞋&lt;/li&gt;
&lt;li&gt;Black Diamond Distance Carbon Z 登山杖&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，Patagonia 的 Boulder Fork Rain Jacket 配合 Hanchor MARL X 防雨背包，绝对是提升我整段徒步旅程体验最大的装备。在风雨雪交加的情况下，我没有一次用到雨衣和背包防雨罩。在降水量最大的一天，也只是导致了我中间层羽绒个别地方略微湿润，背包里非常潮湿，但是都没有明显进水的迹象。考虑到我非常讨厌行动受限的感觉，这两款装备的互相配合基本能让我在中等程度降雨量的情况下，都维持自由行走的状态。&lt;/p&gt;
&lt;p&gt;另外就是我贴身层全部使用了美丽奴羊毛，让我全程绝大部分时候都没有过出汗粘身的感觉，衣服也没有任何异味。如果不是装备带了太多冗余量，否则其实完全可以一套走完全程，不需要更换。&lt;/p&gt;
&lt;p&gt;Black Diamond 的碳纤维登山杖也是我非常喜欢的装备，在下坡路能够足够信赖杖给我的支撑性，重量足够轻以至于有人杖合一的感觉。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/run_hu_e91ecb987fe9cdde.webp" srcset="https://blog.joway.io/images/tmb/run_hu_fbd4bde5409e44b4.webp 800w, https://blog.joway.io/images/tmb/run_hu_df3ca481ad4dd1e3.webp 1200w, https://blog.joway.io/images/tmb/run_hu_e91ecb987fe9cdde.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="旅程记录"&gt;旅程记录&lt;/h2&gt;
&lt;h3 id="day-1"&gt;Day 1&lt;/h3&gt;
&lt;p&gt;从 Le Tour 到 Argentière 最后返回到 Le Tour。&lt;/p&gt;
&lt;p&gt;第一天是我觉得全程最难的一天，爬升强度非常大，而且前半程还下着雨。但是后半程的景色相当壮丽。最后还看到了野生的羚羊。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day1/map_hu_39bf6ed04c35fd84.webp" srcset="https://blog.joway.io/images/tmb/day1/map_hu_dd11654ae3756547.webp 800w, https://blog.joway.io/images/tmb/day1/map_hu_39bf6ed04c35fd84.webp 1154w" sizes="(max-width: 800px) 100vw, 800px" width="1154" height="1192" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/1_hu_110b7c341327b67e.webp" srcset="https://blog.joway.io/images/tmb/day1/1_hu_a328076e78848cb1.webp 800w, https://blog.joway.io/images/tmb/day1/1_hu_4ecb8895c51ffa42.webp 1200w, https://blog.joway.io/images/tmb/day1/1_hu_110b7c341327b67e.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/2_hu_b701340946a5222d.webp" srcset="https://blog.joway.io/images/tmb/day1/2_hu_382a057a37256e78.webp 800w, https://blog.joway.io/images/tmb/day1/2_hu_9b0db0a792c4fcdb.webp 1200w, https://blog.joway.io/images/tmb/day1/2_hu_b701340946a5222d.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/3_hu_80e6d15e90d6da94.webp" srcset="https://blog.joway.io/images/tmb/day1/3_hu_d48e094779d83bd1.webp 800w, https://blog.joway.io/images/tmb/day1/3_hu_5563b01f05e73b3b.webp 1200w, https://blog.joway.io/images/tmb/day1/3_hu_80e6d15e90d6da94.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/4_hu_e2ccdef8b5eb30f4.webp" srcset="https://blog.joway.io/images/tmb/day1/4_hu_dab305a33fbd9811.webp 800w, https://blog.joway.io/images/tmb/day1/4_hu_aa7bab707edfbdd1.webp 1200w, https://blog.joway.io/images/tmb/day1/4_hu_e2ccdef8b5eb30f4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/5_hu_909ac1a98f481f57.webp" srcset="https://blog.joway.io/images/tmb/day1/5_hu_58fa7355d45b47c7.webp 800w, https://blog.joway.io/images/tmb/day1/5_hu_80b1614ed07f057b.webp 1200w, https://blog.joway.io/images/tmb/day1/5_hu_909ac1a98f481f57.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/6_hu_75feb1500ec1584.webp" srcset="https://blog.joway.io/images/tmb/day1/6_hu_ec6a13109fca772d.webp 800w, https://blog.joway.io/images/tmb/day1/6_hu_620a56bdc0601cc1.webp 1200w, https://blog.joway.io/images/tmb/day1/6_hu_75feb1500ec1584.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/7_hu_4b7dd25ffd16b473.webp" srcset="https://blog.joway.io/images/tmb/day1/7_hu_594ab2569c32e43b.webp 800w, https://blog.joway.io/images/tmb/day1/7_hu_86d199f40a0e8fff.webp 1200w, https://blog.joway.io/images/tmb/day1/7_hu_4b7dd25ffd16b473.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day1/8_hu_1031cd46683e95b4.webp" srcset="https://blog.joway.io/images/tmb/day1/8_hu_9138696e856dcc34.webp 800w, https://blog.joway.io/images/tmb/day1/8_hu_c979a3541ddfed8a.webp 1200w, https://blog.joway.io/images/tmb/day1/8_hu_1031cd46683e95b4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-2"&gt;Day 2&lt;/h3&gt;
&lt;p&gt;从 Le Tour 到 Trient 再坐车到 La Fouly。&lt;/p&gt;
&lt;p&gt;第二天从法国走到了瑞士。这一天基本看不到什么景色，全身都下着雨，在山顶又刮着狂风。因为路程过于无聊，所以我全程都在尝试不同的徒步方法，甚至尝试了一小段越野跑。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day2/map_hu_e46fe90dcb6f6048.webp" srcset="https://blog.joway.io/images/tmb/day2/map_hu_618906321fccb75e.webp 800w, https://blog.joway.io/images/tmb/day2/map_hu_e46fe90dcb6f6048.webp 1154w" sizes="(max-width: 800px) 100vw, 800px" width="1154" height="1192" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/1_hu_52a66ece5b39e37e.webp" srcset="https://blog.joway.io/images/tmb/day2/1_hu_fbd9b9e32f0f7750.webp 800w, https://blog.joway.io/images/tmb/day2/1_hu_3a92f6e140037da5.webp 1200w, https://blog.joway.io/images/tmb/day2/1_hu_52a66ece5b39e37e.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/2_hu_b778203abf1cc4d9.webp" srcset="https://blog.joway.io/images/tmb/day2/2_hu_503dec25cfa2c84c.webp 800w, https://blog.joway.io/images/tmb/day2/2_hu_d0cb5f474309c6d8.webp 1200w, https://blog.joway.io/images/tmb/day2/2_hu_b778203abf1cc4d9.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/3_hu_8a87c671e9d94b36.webp" srcset="https://blog.joway.io/images/tmb/day2/3_hu_798ba151c084c6cb.webp 800w, https://blog.joway.io/images/tmb/day2/3_hu_c5230aff7bf53221.webp 1200w, https://blog.joway.io/images/tmb/day2/3_hu_8a87c671e9d94b36.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/4_hu_e41693642c66c580.webp" srcset="https://blog.joway.io/images/tmb/day2/4_hu_5dd69bde81fa02fb.webp 800w, https://blog.joway.io/images/tmb/day2/4_hu_d400968a51b8d940.webp 1200w, https://blog.joway.io/images/tmb/day2/4_hu_e41693642c66c580.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/5_hu_1eea5085720ec62a.webp" srcset="https://blog.joway.io/images/tmb/day2/5_hu_aad8ef744a27c364.webp 800w, https://blog.joway.io/images/tmb/day2/5_hu_d2132e1b1528aef2.webp 1200w, https://blog.joway.io/images/tmb/day2/5_hu_1eea5085720ec62a.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/6_hu_3bcd2813232ce457.webp" srcset="https://blog.joway.io/images/tmb/day2/6_hu_83feaacb440bf417.webp 800w, https://blog.joway.io/images/tmb/day2/6_hu_344073776073fde5.webp 1200w, https://blog.joway.io/images/tmb/day2/6_hu_3bcd2813232ce457.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day2/7_hu_ad493f8a6fbd5cd.webp" srcset="https://blog.joway.io/images/tmb/day2/7_hu_5f2bc609331cf7b5.webp 800w, https://blog.joway.io/images/tmb/day2/7_hu_3b69c975644ffc76.webp 1200w, https://blog.joway.io/images/tmb/day2/7_hu_ad493f8a6fbd5cd.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-3"&gt;Day 3&lt;/h3&gt;
&lt;p&gt;从 La Fouly 到 Arp Nouva 再坐车到 Courmayeur。&lt;/p&gt;
&lt;p&gt;从瑞士走到了意大利。一出了瑞士，就结束了阴雨绵绵的天气，翻越垭口穿过云雾迎面而来的是著名的大乔格拉斯峰，以及远处不断随着海拔变化而变化的高山植被。相当震撼。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day3/map_hu_332b40f4e1dd2dd0.webp" srcset="https://blog.joway.io/images/tmb/day3/map_hu_24e8d7aee9307f68.webp 800w, https://blog.joway.io/images/tmb/day3/map_hu_332b40f4e1dd2dd0.webp 1125w" sizes="(max-width: 800px) 100vw, 800px" width="1125" height="1187" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/1_hu_b4c76c3eabdfdde.webp" srcset="https://blog.joway.io/images/tmb/day3/1_hu_7b1f7851b9c7b678.webp 800w, https://blog.joway.io/images/tmb/day3/1_hu_c105dc05e49a789e.webp 1200w, https://blog.joway.io/images/tmb/day3/1_hu_b4c76c3eabdfdde.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/2_hu_acbe02d7cd196bba.webp" srcset="https://blog.joway.io/images/tmb/day3/2_hu_6fe23cf765a9fea9.webp 800w, https://blog.joway.io/images/tmb/day3/2_hu_2a6735e83e7b6282.webp 1200w, https://blog.joway.io/images/tmb/day3/2_hu_acbe02d7cd196bba.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/3_hu_510b165d2f36af09.webp" srcset="https://blog.joway.io/images/tmb/day3/3_hu_67b0e6623359eb00.webp 800w, https://blog.joway.io/images/tmb/day3/3_hu_4e71b201877cdef0.webp 1200w, https://blog.joway.io/images/tmb/day3/3_hu_510b165d2f36af09.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/4_hu_6cf35160c30aafa7.webp" srcset="https://blog.joway.io/images/tmb/day3/4_hu_d060de223c10adca.webp 800w, https://blog.joway.io/images/tmb/day3/4_hu_4702eb0983772c9b.webp 1200w, https://blog.joway.io/images/tmb/day3/4_hu_6cf35160c30aafa7.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/5_hu_9cc6578f24a93912.webp" srcset="https://blog.joway.io/images/tmb/day3/5_hu_66db226ee8d30d25.webp 800w, https://blog.joway.io/images/tmb/day3/5_hu_800f63792d46aceb.webp 1200w, https://blog.joway.io/images/tmb/day3/5_hu_9cc6578f24a93912.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/6_hu_692a5029af4a2f7.webp" srcset="https://blog.joway.io/images/tmb/day3/6_hu_8e38748189523b24.webp 800w, https://blog.joway.io/images/tmb/day3/6_hu_ebe10bb4e74e6ecb.webp 1200w, https://blog.joway.io/images/tmb/day3/6_hu_692a5029af4a2f7.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/7_hu_c0af29348dc27164.webp" srcset="https://blog.joway.io/images/tmb/day3/7_hu_30360d25977719f1.webp 800w, https://blog.joway.io/images/tmb/day3/7_hu_faae7804b531c3d3.webp 1200w, https://blog.joway.io/images/tmb/day3/7_hu_c0af29348dc27164.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/8_hu_18875f24630a1a56.webp" srcset="https://blog.joway.io/images/tmb/day3/8_hu_59de8f2de3277972.webp 800w, https://blog.joway.io/images/tmb/day3/8_hu_2870cb2150e05080.webp 1200w, https://blog.joway.io/images/tmb/day3/8_hu_18875f24630a1a56.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/9_hu_16eab0c664f5c1f4.webp" srcset="https://blog.joway.io/images/tmb/day3/9_hu_afafce83f7426e12.webp 800w, https://blog.joway.io/images/tmb/day3/9_hu_674bd47d316aa64.webp 1200w, https://blog.joway.io/images/tmb/day3/9_hu_16eab0c664f5c1f4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/10_hu_a37cb0dba39b8b0d.webp" srcset="https://blog.joway.io/images/tmb/day3/10_hu_3819bc131a6b4138.webp 800w, https://blog.joway.io/images/tmb/day3/10_hu_5708b03de669b572.webp 1200w, https://blog.joway.io/images/tmb/day3/10_hu_a37cb0dba39b8b0d.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/11_hu_156a38cc020c3a25.webp" srcset="https://blog.joway.io/images/tmb/day3/11_hu_99e61caac4f6e728.webp 800w, https://blog.joway.io/images/tmb/day3/11_hu_4c9f7374c5070e96.webp 1200w, https://blog.joway.io/images/tmb/day3/11_hu_156a38cc020c3a25.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/12_hu_49ad55ab46434e30.webp" srcset="https://blog.joway.io/images/tmb/day3/12_hu_93181fb03db4182d.webp 800w, https://blog.joway.io/images/tmb/day3/12_hu_56807c4f4b1fb48.webp 1200w, https://blog.joway.io/images/tmb/day3/12_hu_49ad55ab46434e30.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/13_hu_7c728518a843b754.webp" srcset="https://blog.joway.io/images/tmb/day3/13_hu_b50bb31f4ede306f.webp 800w, https://blog.joway.io/images/tmb/day3/13_hu_4cd42a89f907ceea.webp 1200w, https://blog.joway.io/images/tmb/day3/13_hu_7c728518a843b754.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/14_hu_898ad53b190987b4.webp" srcset="https://blog.joway.io/images/tmb/day3/14_hu_2de44d8b929a6609.webp 800w, https://blog.joway.io/images/tmb/day3/14_hu_ecd2fb95131ad847.webp 1200w, https://blog.joway.io/images/tmb/day3/14_hu_898ad53b190987b4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/15_hu_83610afc0e3bbf50.webp" srcset="https://blog.joway.io/images/tmb/day3/15_hu_2cd74d32be19564d.webp 800w, https://blog.joway.io/images/tmb/day3/15_hu_b874ac49f62ee3e8.webp 1200w, https://blog.joway.io/images/tmb/day3/15_hu_83610afc0e3bbf50.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/16_hu_20f95eca2e7bfce3.webp" srcset="https://blog.joway.io/images/tmb/day3/16_hu_b56de95fbcfc2f98.webp 800w, https://blog.joway.io/images/tmb/day3/16_hu_6e5021f02260f1d0.webp 1200w, https://blog.joway.io/images/tmb/day3/16_hu_20f95eca2e7bfce3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/17_hu_2399044d5b802062.webp" srcset="https://blog.joway.io/images/tmb/day3/17_hu_62ad7bad4aa1ffe1.webp 800w, https://blog.joway.io/images/tmb/day3/17_hu_da95cab3b80ff051.webp 1200w, https://blog.joway.io/images/tmb/day3/17_hu_2399044d5b802062.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/18_hu_e9c400a2877a97f2.webp" srcset="https://blog.joway.io/images/tmb/day3/18_hu_3740cc91afc16a3d.webp 800w, https://blog.joway.io/images/tmb/day3/18_hu_391e55cb80470f42.webp 1200w, https://blog.joway.io/images/tmb/day3/18_hu_e9c400a2877a97f2.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/19_hu_87a5e6bdc2935b38.webp" srcset="https://blog.joway.io/images/tmb/day3/19_hu_62bb1a759ab6f714.webp 800w, https://blog.joway.io/images/tmb/day3/19_hu_f13d7d8238f00fb5.webp 1200w, https://blog.joway.io/images/tmb/day3/19_hu_87a5e6bdc2935b38.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/20_hu_d10b0b3925bd9bd7.webp" srcset="https://blog.joway.io/images/tmb/day3/20_hu_a339a4088e1d9e13.webp 800w, https://blog.joway.io/images/tmb/day3/20_hu_b9ffffc978a0f1c1.webp 1200w, https://blog.joway.io/images/tmb/day3/20_hu_d10b0b3925bd9bd7.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/21_hu_7c683bb24fe61d18.webp" srcset="https://blog.joway.io/images/tmb/day3/21_hu_40ac2ff346793800.webp 800w, https://blog.joway.io/images/tmb/day3/21_hu_b435b98f7b8c3d1d.webp 1200w, https://blog.joway.io/images/tmb/day3/21_hu_7c683bb24fe61d18.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/22_hu_ff05d5246daeee51.webp" srcset="https://blog.joway.io/images/tmb/day3/22_hu_c606e01904b57ef1.webp 800w, https://blog.joway.io/images/tmb/day3/22_hu_2820ede969745903.webp 1200w, https://blog.joway.io/images/tmb/day3/22_hu_ff05d5246daeee51.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day3/23_hu_b366e958b668c478.webp" srcset="https://blog.joway.io/images/tmb/day3/23_hu_81f516245580c41b.webp 800w, https://blog.joway.io/images/tmb/day3/23_hu_c710c35a6f661f52.webp 1200w, https://blog.joway.io/images/tmb/day3/23_hu_b366e958b668c478.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-4"&gt;Day 4&lt;/h3&gt;
&lt;p&gt;从 Mont de La Saxe 回到 Courmayeur 。&lt;/p&gt;
&lt;p&gt;这一天属于散步级别的休闲徒步。回到前一天坐车离开的地点附近，在高山植被中穿梭。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day4/map_hu_6cfe737d3817be00.webp" srcset="https://blog.joway.io/images/tmb/day4/map_hu_a2807c334ee6b28a.webp 800w, https://blog.joway.io/images/tmb/day4/map_hu_6cfe737d3817be00.webp 1125w" sizes="(max-width: 800px) 100vw, 800px" width="1125" height="1192" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day4/1_hu_18255fb9732a4312.webp" srcset="https://blog.joway.io/images/tmb/day4/1_hu_9f53ef1234c96c13.webp 800w, https://blog.joway.io/images/tmb/day4/1_hu_ec0456723a7e1a9f.webp 1200w, https://blog.joway.io/images/tmb/day4/1_hu_18255fb9732a4312.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day4/2_hu_b94934f5515577a3.webp" srcset="https://blog.joway.io/images/tmb/day4/2_hu_8799375eaf3effa8.webp 800w, https://blog.joway.io/images/tmb/day4/2_hu_b3a9909781818bad.webp 1200w, https://blog.joway.io/images/tmb/day4/2_hu_b94934f5515577a3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day4/3_hu_1efbc47b8d243319.webp" srcset="https://blog.joway.io/images/tmb/day4/3_hu_bd0e6402f1e34aa2.webp 800w, https://blog.joway.io/images/tmb/day4/3_hu_d9b82a3f5636c51d.webp 1200w, https://blog.joway.io/images/tmb/day4/3_hu_1efbc47b8d243319.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day4/4_hu_2277cb57dd6d46c4.webp" srcset="https://blog.joway.io/images/tmb/day4/4_hu_760839ece8a5346c.webp 800w, https://blog.joway.io/images/tmb/day4/4_hu_8a22bf850357aa07.webp 1200w, https://blog.joway.io/images/tmb/day4/4_hu_2277cb57dd6d46c4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day4/5_hu_a4ea0613e4609342.webp" srcset="https://blog.joway.io/images/tmb/day4/5_hu_24b16cabba26e7ee.webp 800w, https://blog.joway.io/images/tmb/day4/5_hu_f6d426ecf29c4663.webp 1200w, https://blog.joway.io/images/tmb/day4/5_hu_a4ea0613e4609342.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-5"&gt;Day 5&lt;/h3&gt;
&lt;p&gt;从 La Visaille 到 Les Chapieux，再坐车到 Bourg St. Maurice。&lt;/p&gt;
&lt;p&gt;从意大利重新回到了法国。这一天的徒步环境相当恶劣，但也是景色最多变和壮观的一天，甚至都不像是在地球上能见到的场景。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day5/map_hu_b232083dd68367d1.webp" srcset="https://blog.joway.io/images/tmb/day5/map_hu_63de2c1754c603f6.webp 800w, https://blog.joway.io/images/tmb/day5/map_hu_b232083dd68367d1.webp 1125w" sizes="(max-width: 800px) 100vw, 800px" width="1125" height="1192" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/1_hu_85ecf613c62ce530.webp" srcset="https://blog.joway.io/images/tmb/day5/1_hu_7d07bcb259cc5c68.webp 800w, https://blog.joway.io/images/tmb/day5/1_hu_133280eded99977c.webp 1200w, https://blog.joway.io/images/tmb/day5/1_hu_85ecf613c62ce530.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/2_hu_d662a60557cf0b79.webp" srcset="https://blog.joway.io/images/tmb/day5/2_hu_5bd41a71f6f6c8bf.webp 800w, https://blog.joway.io/images/tmb/day5/2_hu_970f67e6300ee872.webp 1200w, https://blog.joway.io/images/tmb/day5/2_hu_d662a60557cf0b79.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/3_hu_5f30eab04607dd94.webp" srcset="https://blog.joway.io/images/tmb/day5/3_hu_c4f90e2d3b4fb21d.webp 800w, https://blog.joway.io/images/tmb/day5/3_hu_33635b066d15f5b8.webp 1200w, https://blog.joway.io/images/tmb/day5/3_hu_5f30eab04607dd94.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/4_hu_5b60d84871019fe1.webp" srcset="https://blog.joway.io/images/tmb/day5/4_hu_4dc684f65084e3ab.webp 800w, https://blog.joway.io/images/tmb/day5/4_hu_8a00e4659f81d852.webp 1200w, https://blog.joway.io/images/tmb/day5/4_hu_5b60d84871019fe1.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/5_hu_7830cb75e22105a3.webp" srcset="https://blog.joway.io/images/tmb/day5/5_hu_1554d6f3469ad89.webp 800w, https://blog.joway.io/images/tmb/day5/5_hu_eece2f0b4d279ff5.webp 1200w, https://blog.joway.io/images/tmb/day5/5_hu_7830cb75e22105a3.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/6_hu_9b48de8df5e99ef8.webp" srcset="https://blog.joway.io/images/tmb/day5/6_hu_84d7d835fcc22d57.webp 800w, https://blog.joway.io/images/tmb/day5/6_hu_efd60070b464719a.webp 1200w, https://blog.joway.io/images/tmb/day5/6_hu_9b48de8df5e99ef8.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/7_hu_1a57c887105b75de.webp" srcset="https://blog.joway.io/images/tmb/day5/7_hu_c7ee68a61b4d4c3f.webp 800w, https://blog.joway.io/images/tmb/day5/7_hu_b61d0a73231ed102.webp 1200w, https://blog.joway.io/images/tmb/day5/7_hu_1a57c887105b75de.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/8_hu_d9691fe9d92465f9.webp" srcset="https://blog.joway.io/images/tmb/day5/8_hu_a8c5e024facfa38e.webp 800w, https://blog.joway.io/images/tmb/day5/8_hu_a6312e81f2c784b1.webp 1200w, https://blog.joway.io/images/tmb/day5/8_hu_d9691fe9d92465f9.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/9_hu_12bb21e04916d967.webp" srcset="https://blog.joway.io/images/tmb/day5/9_hu_60d6f16f55390814.webp 800w, https://blog.joway.io/images/tmb/day5/9_hu_36e8ff2197eac1d4.webp 1200w, https://blog.joway.io/images/tmb/day5/9_hu_12bb21e04916d967.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/10_hu_32aebd2ecd6c2f58.webp" srcset="https://blog.joway.io/images/tmb/day5/10_hu_6c765c6850fc09f7.webp 800w, https://blog.joway.io/images/tmb/day5/10_hu_c560996150425f85.webp 1200w, https://blog.joway.io/images/tmb/day5/10_hu_32aebd2ecd6c2f58.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/11_hu_817984085c0e9a36.webp" srcset="https://blog.joway.io/images/tmb/day5/11_hu_48e1052cd2a6531c.webp 800w, https://blog.joway.io/images/tmb/day5/11_hu_a8de9faba7eaeed9.webp 1200w, https://blog.joway.io/images/tmb/day5/11_hu_817984085c0e9a36.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/12_hu_bb73fd7b6035b9a4.webp" srcset="https://blog.joway.io/images/tmb/day5/12_hu_d4297d61bdcf4d09.webp 800w, https://blog.joway.io/images/tmb/day5/12_hu_6d0b5433c2ffd1db.webp 1200w, https://blog.joway.io/images/tmb/day5/12_hu_bb73fd7b6035b9a4.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/13_hu_1e34884bae8cfe8e.webp" srcset="https://blog.joway.io/images/tmb/day5/13_hu_d49733c8b0de7f76.webp 800w, https://blog.joway.io/images/tmb/day5/13_hu_24777a2a80d68d74.webp 1200w, https://blog.joway.io/images/tmb/day5/13_hu_1e34884bae8cfe8e.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/14_hu_50da210c9bd84008.webp" srcset="https://blog.joway.io/images/tmb/day5/14_hu_29559d27ecd45e50.webp 800w, https://blog.joway.io/images/tmb/day5/14_hu_81c7209389f3aeaf.webp 1200w, https://blog.joway.io/images/tmb/day5/14_hu_50da210c9bd84008.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/15_hu_30a0f7970457be2e.webp" srcset="https://blog.joway.io/images/tmb/day5/15_hu_da29cd88fff7d977.webp 800w, https://blog.joway.io/images/tmb/day5/15_hu_f3dabe69ff407675.webp 1200w, https://blog.joway.io/images/tmb/day5/15_hu_30a0f7970457be2e.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/16_hu_ff79e8f7c5b621fb.webp" srcset="https://blog.joway.io/images/tmb/day5/16_hu_b62903e83536ef63.webp 800w, https://blog.joway.io/images/tmb/day5/16_hu_2be4ca0d8986644d.webp 1200w, https://blog.joway.io/images/tmb/day5/16_hu_ff79e8f7c5b621fb.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/17_hu_e63353c962fa7253.webp" srcset="https://blog.joway.io/images/tmb/day5/17_hu_79b37e2de49d8257.webp 800w, https://blog.joway.io/images/tmb/day5/17_hu_34c290618954bd7.webp 1200w, https://blog.joway.io/images/tmb/day5/17_hu_e63353c962fa7253.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/18_hu_e7bd5d71be2dcc23.webp" srcset="https://blog.joway.io/images/tmb/day5/18_hu_6cc6459c1a2ce1b2.webp 800w, https://blog.joway.io/images/tmb/day5/18_hu_2f43d2b51465b292.webp 1200w, https://blog.joway.io/images/tmb/day5/18_hu_e7bd5d71be2dcc23.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/19_hu_120f6f38ab22eddf.webp" srcset="https://blog.joway.io/images/tmb/day5/19_hu_59d66c4b112347e5.webp 800w, https://blog.joway.io/images/tmb/day5/19_hu_d7c30984ba84452d.webp 1200w, https://blog.joway.io/images/tmb/day5/19_hu_120f6f38ab22eddf.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/20_hu_7b5f605f4d34022c.webp" srcset="https://blog.joway.io/images/tmb/day5/20_hu_13dd3ae0e8f27cc4.webp 800w, https://blog.joway.io/images/tmb/day5/20_hu_6c71b6215deae331.webp 1200w, https://blog.joway.io/images/tmb/day5/20_hu_7b5f605f4d34022c.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day5/21_hu_30c34a33ea8ab3d7.webp" srcset="https://blog.joway.io/images/tmb/day5/21_hu_d2dbd62a0b660d64.webp 800w, https://blog.joway.io/images/tmb/day5/21_hu_347a87a31038a03d.webp 1200w, https://blog.joway.io/images/tmb/day5/21_hu_30c34a33ea8ab3d7.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;
&lt;h3 id="day-6"&gt;Day 6&lt;/h3&gt;
&lt;p&gt;从 Bourg St. Maurice 坐车到 Les Contamines 徒步走到霞慕尼旁边的 Les Houches 小镇。&lt;/p&gt;
&lt;p&gt;由于天气原因，这一天从原本应该是徒步难度最大的一天，因为修改了路线，反倒成了最简单的一天。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/tmb/day6/map_hu_1db2b697c587b478.webp" srcset="https://blog.joway.io/images/tmb/day6/map_hu_4bb26976f15f965d.webp 800w, https://blog.joway.io/images/tmb/day6/map_hu_1db2b697c587b478.webp 1187w" sizes="(max-width: 800px) 100vw, 800px" width="1187" height="1192" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;div class="post-gallery"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/1_hu_5f296a409a8c260a.webp" srcset="https://blog.joway.io/images/tmb/day6/1_hu_750be89b9a7f902d.webp 800w, https://blog.joway.io/images/tmb/day6/1_hu_823022e0d3ac721f.webp 1200w, https://blog.joway.io/images/tmb/day6/1_hu_5f296a409a8c260a.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/2_hu_3a9e5574e7a2cd7f.webp" srcset="https://blog.joway.io/images/tmb/day6/2_hu_69a41afbde632f53.webp 800w, https://blog.joway.io/images/tmb/day6/2_hu_27e820a904b4d93e.webp 1200w, https://blog.joway.io/images/tmb/day6/2_hu_3a9e5574e7a2cd7f.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/3_hu_ed4ca2d919bf50e8.webp" srcset="https://blog.joway.io/images/tmb/day6/3_hu_8c61fa7225bf7ac7.webp 800w, https://blog.joway.io/images/tmb/day6/3_hu_a4e735546000c3ec.webp 1200w, https://blog.joway.io/images/tmb/day6/3_hu_ed4ca2d919bf50e8.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/4_hu_70a699ff933a07c6.webp" srcset="https://blog.joway.io/images/tmb/day6/4_hu_7c2f437c06664ad9.webp 800w, https://blog.joway.io/images/tmb/day6/4_hu_2c5c07c9a5fd5c9d.webp 1200w, https://blog.joway.io/images/tmb/day6/4_hu_70a699ff933a07c6.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/5_hu_4535d80e58a37345.webp" srcset="https://blog.joway.io/images/tmb/day6/5_hu_23401477b754709e.webp 800w, https://blog.joway.io/images/tmb/day6/5_hu_b20993b8b0539f9.webp 1200w, https://blog.joway.io/images/tmb/day6/5_hu_4535d80e58a37345.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/6_hu_9360b20bde8a6d07.webp" srcset="https://blog.joway.io/images/tmb/day6/6_hu_5e553886039864de.webp 800w, https://blog.joway.io/images/tmb/day6/6_hu_72f4dd59f9ef864f.webp 1200w, https://blog.joway.io/images/tmb/day6/6_hu_9360b20bde8a6d07.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/7_hu_914cc5ff762ba35.webp" srcset="https://blog.joway.io/images/tmb/day6/7_hu_6b6eaa27570e5d9c.webp 800w, https://blog.joway.io/images/tmb/day6/7_hu_4adac5100e0348f6.webp 1200w, https://blog.joway.io/images/tmb/day6/7_hu_914cc5ff762ba35.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/8_hu_4cb4f3479cc64cbf.webp" srcset="https://blog.joway.io/images/tmb/day6/8_hu_ce6dc0db3efcd77d.webp 800w, https://blog.joway.io/images/tmb/day6/8_hu_da40dd03be85ab58.webp 1200w, https://blog.joway.io/images/tmb/day6/8_hu_4cb4f3479cc64cbf.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/tmb/day6/9_hu_bd1d695fcf35d1df.webp" srcset="https://blog.joway.io/images/tmb/day6/9_hu_733cd62050875a17.webp 800w, https://blog.joway.io/images/tmb/day6/9_hu_70b069f9693651c2.webp 1200w, https://blog.joway.io/images/tmb/day6/9_hu_bd1d695fcf35d1df.webp 1600w" sizes="(max-width: 48em) 100vw, 400px" width="1600" height="1200" alt="欧游散记 —— 环勃朗峰之旅" loading="lazy" decoding="async"&gt;
&lt;/div&gt;</content:encoded></item><item><title>欧游散记 —— 特摩索斯古城</title><link>https://blog.joway.io/posts/turkey-termessos/</link><pubDate>Sun, 07 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/turkey-termessos/</guid><category>Travel</category><description>特摩索斯（Termessos）位于土耳其南部城市安塔利亚的北面大约 30 公里，是一座起源神秘，消亡也神秘的深山古城。它第一次被历史提到是在公元前 333 年亚历山大大帝包围这座城市时，但即便是征服了希腊世界的亚历山大大帝也并未能成功征服特摩索斯。
特摩索斯建于海拔 1000 多米的山口位置，由于特殊的地理位置，加上完善的水利设施，所以即便被包围也只需要一小只军队便可守卫，这帮助这座城市在那个群雄逐鹿的希腊化时期和罗马帝国时期得以一直以独立城邦的状态存活下来。一直到公元 5 世纪的一场地震摧毁了这座城市的蓄水库，导致居民不得已陆续搬迁，最终荒废并被人们所遗忘。一直到 19 世纪，欧洲的探险家才重新发现了这座城市，但即便到今天，特摩索斯一直是一个非常冷门的旅行目的地。</description><content:encoded>&lt;p&gt;特摩索斯（Termessos）位于土耳其南部城市安塔利亚的北面大约 30 公里，是一座起源神秘，消亡也神秘的深山古城。它第一次被历史提到是在公元前 333 年亚历山大大帝包围这座城市时，但即便是征服了希腊世界的亚历山大大帝也并未能成功征服特摩索斯。&lt;/p&gt;
&lt;p&gt;特摩索斯建于海拔 1000 多米的山口位置，由于特殊的地理位置，加上完善的水利设施，所以即便被包围也只需要一小只军队便可守卫，这帮助这座城市在那个群雄逐鹿的希腊化时期和罗马帝国时期得以一直以独立城邦的状态存活下来。一直到公元 5 世纪的一场地震摧毁了这座城市的蓄水库，导致居民不得已陆续搬迁，最终荒废并被人们所遗忘。一直到 19 世纪，欧洲的探险家才重新发现了这座城市，但即便到今天，特摩索斯一直是一个非常冷门的旅行目的地。&lt;/p&gt;
&lt;p&gt;我知道特摩索斯，是机缘巧合在 YouTube 搜索安塔利亚的视频时，偶然间看到了一个特摩索斯徒步视频，即便画质极其粗糙，依然还是被这座山顶古城给震撼到了。更因此将安塔利亚作为了土耳其旅行的其中一站。&lt;/p&gt;
&lt;p&gt;到达安塔利亚，我们在 Airbnb 上约了一个本地向导 Onder 带我们开车到达北部的居呂克山-特摩索斯国家公园，从国家公园徒步前往特摩索斯所在的山顶。Onder 的本职工作是老师，但十分热爱特摩索斯甚至他的硕士论文写的课题便是特摩索斯，所以业余时间也通过做特摩索斯的向导赚点外快。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/start_hu_d946e14bfdf1fe46.webp" srcset="https://blog.joway.io/images/turkey/termessos/start_hu_9fda1df4252ed3cf.webp 800w, https://blog.joway.io/images/turkey/termessos/start_hu_f60c3943599c4ab5.webp 1200w, https://blog.joway.io/images/turkey/termessos/start_hu_d946e14bfdf1fe46.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;虽然特摩索斯的城市部分主要集中在半山腰和山顶，但是山底相当于这座城市的郊区，和现代城市的郊区一样，往往是平民的生活聚居地。&lt;/p&gt;
&lt;p&gt;首先看到的是城市的宗教区域，哈德良庙，但如今只剩下了一扇门孤零零地在山脚下伫立，既神圣又凄凉：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/temple_of_hadrian_hu_435b1f31dc9ea325.webp" srcset="https://blog.joway.io/images/turkey/termessos/temple_of_hadrian_hu_2e42e9e2910bcc67.webp 800w, https://blog.joway.io/images/turkey/termessos/temple_of_hadrian_hu_7db7ffd2afefc903.webp 1200w, https://blog.joway.io/images/turkey/termessos/temple_of_hadrian_hu_435b1f31dc9ea325.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在哈德良庙的另一边，是居民的墓地区。特摩索斯的墓十分有特色，都是以露天石棺的形式“展览”在地表之上。石棺的雕刻也别有一番讲究。&lt;/p&gt;
&lt;p&gt;普通平民的石棺是以两个太阳加中间方块的形式，太阳是特摩索斯的标志，这里也被称之为太阳城：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/coffin-people_hu_6d7f31c005ed49fb.webp" srcset="https://blog.joway.io/images/turkey/termessos/coffin-people_hu_7e795b773e8492c3.webp 800w, https://blog.joway.io/images/turkey/termessos/coffin-people_hu_4a45bb968672f7ed.webp 1200w, https://blog.joway.io/images/turkey/termessos/coffin-people_hu_6d7f31c005ed49fb.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;而士兵的石棺会在太阳上增加武器的标识，寓意太阳的捍卫者：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/coffin-soldier_hu_ada3003e13ee9fcf.webp" srcset="https://blog.joway.io/images/turkey/termessos/coffin-soldier_hu_9eeddfe43c457ce0.webp 800w, https://blog.joway.io/images/turkey/termessos/coffin-soldier_hu_54dff3f47d8b402d.webp 1200w, https://blog.joway.io/images/turkey/termessos/coffin-soldier_hu_ada3003e13ee9fcf.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;无论是平民还是士兵的石棺都略显单调和同质，特别是人死后，棺材还会被摆在地表被长久展示，这导致石棺本身变成了一个人生命价值的化身。如果没有钱请不起好的雕刻工匠，就靠战斗获得荣誉来装点自己的石棺。如果有钱，就极尽工匠精湛的工艺让自己的石棺变成华丽的艺术品：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/coffin-rich_hu_faeb1fcb1d46f19c.webp" srcset="https://blog.joway.io/images/turkey/termessos/coffin-rich_hu_42a7a120dc35de89.webp 800w, https://blog.joway.io/images/turkey/termessos/coffin-rich_hu_71322ff536abbb85.webp 1200w, https://blog.joway.io/images/turkey/termessos/coffin-rich_hu_faeb1fcb1d46f19c.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/coffin-rich-side_hu_e495041769d0e73f.webp" srcset="https://blog.joway.io/images/turkey/termessos/coffin-rich-side_hu_3172fa5329b25c44.webp 800w, https://blog.joway.io/images/turkey/termessos/coffin-rich-side_hu_3233f7145d7fa798.webp 1200w, https://blog.joway.io/images/turkey/termessos/coffin-rich-side_hu_e495041769d0e73f.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;又或者是更为有权势的领袖，可以直接在岩壁开凿更为壮阔的仰望式墓藏：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/coffin-alcetas_hu_975391cc2059a4e3.webp" srcset="https://blog.joway.io/images/turkey/termessos/coffin-alcetas_hu_a42a52a2bf2d8c87.webp 800w, https://blog.joway.io/images/turkey/termessos/coffin-alcetas_hu_b1179a286a47ff0.webp 1200w, https://blog.joway.io/images/turkey/termessos/coffin-alcetas_hu_975391cc2059a4e3.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;考虑到两千年后，我们还依然在为他们的石棺赞叹，当初他们的「虚荣」如今也被岁月洗涤成了「实荣」。&lt;/p&gt;
&lt;p&gt;沿着山路，陆陆续续会走过众多城墙和古罗马式引水渠，以及如今已不知为何物的废墟：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/ruins1_hu_f9c3f1d593d48eef.webp" srcset="https://blog.joway.io/images/turkey/termessos/ruins1_hu_ea5402d37eee6afc.webp 800w, https://blog.joway.io/images/turkey/termessos/ruins1_hu_99f343f3daacff17.webp 1200w, https://blog.joway.io/images/turkey/termessos/ruins1_hu_f9c3f1d593d48eef.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/wall1_hu_e002166c20fe04f6.webp" srcset="https://blog.joway.io/images/turkey/termessos/wall1_hu_928588af607bffdd.webp 800w, https://blog.joway.io/images/turkey/termessos/wall1_hu_886b37028f849fa7.webp 1200w, https://blog.joway.io/images/turkey/termessos/wall1_hu_e002166c20fe04f6.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/wall2_hu_f87e9892fefe1051.webp" srcset="https://blog.joway.io/images/turkey/termessos/wall2_hu_f6ff9296ac61fdf0.webp 800w, https://blog.joway.io/images/turkey/termessos/wall2_hu_91cc0732b5628a41.webp 1200w, https://blog.joway.io/images/turkey/termessos/wall2_hu_f87e9892fefe1051.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/underground_hu_ad10214ee49a9b18.webp" srcset="https://blog.joway.io/images/turkey/termessos/underground_hu_25fa3b2729106a3d.webp 800w, https://blog.joway.io/images/turkey/termessos/underground_hu_65c09fc374a0b43e.webp 1200w, https://blog.joway.io/images/turkey/termessos/underground_hu_ad10214ee49a9b18.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在半山腰，还能看到一个保存地非常好的体育馆遗址：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/gym_hu_83b568c942ac598.webp" srcset="https://blog.joway.io/images/turkey/termessos/gym_hu_bb0378976866b5d3.webp 800w, https://blog.joway.io/images/turkey/termessos/gym_hu_6b9795caff77b08a.webp 1200w, https://blog.joway.io/images/turkey/termessos/gym_hu_83b568c942ac598.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;以及曾经的祭坛：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/heroon_hu_95bf1cc5c838b6e1.webp" srcset="https://blog.joway.io/images/turkey/termessos/heroon_hu_b107af59b55c1bc1.webp 800w, https://blog.joway.io/images/turkey/termessos/heroon_hu_a8607aa40cd3e754.webp 1200w, https://blog.joway.io/images/turkey/termessos/heroon_hu_95bf1cc5c838b6e1.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;接着便来到了特摩索斯城市最最核心的基础设施 —— 蓄水库：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/water_saver1_hu_5f4e6d75ffe9b169.webp" srcset="https://blog.joway.io/images/turkey/termessos/water_saver1_hu_706aa58a2361991.webp 800w, https://blog.joway.io/images/turkey/termessos/water_saver1_hu_45de505d41c857ce.webp 1200w, https://blog.joway.io/images/turkey/termessos/water_saver1_hu_5f4e6d75ffe9b169.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/water_saver2_hu_64a013f4f72eae98.webp" srcset="https://blog.joway.io/images/turkey/termessos/water_saver2_hu_99c05cb632b201.webp 800w, https://blog.joway.io/images/turkey/termessos/water_saver2_hu_45b971f95cae207e.webp 1200w, https://blog.joway.io/images/turkey/termessos/water_saver2_hu_64a013f4f72eae98.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;由于地震已经毁坏了蓄水库很大一部分，所以今天的蓄水库为了维持稳定，能够看到有很多人工固定的痕迹，但是能够两千年前在一个山顶开凿一个如此巨大的地下空间还是非常令人震撼的。&lt;/p&gt;
&lt;p&gt;大约花了一小时的路程，便可抵达特摩索斯的山顶，在一个转身间，看到了我在土耳其见过的最震撼的一幕：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/turkey/termessos/theater1_hu_1982dbad1f4e3c93.webp" srcset="https://blog.joway.io/images/turkey/termessos/theater1_hu_124b8444a781afb3.webp 800w, https://blog.joway.io/images/turkey/termessos/theater1_hu_5774b2ac6984c25.webp 1200w, https://blog.joway.io/images/turkey/termessos/theater1_hu_1982dbad1f4e3c93.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/theater2_hu_b27f6e39e83365b5.webp" srcset="https://blog.joway.io/images/turkey/termessos/theater2_hu_a8465b522d1638ed.webp 800w, https://blog.joway.io/images/turkey/termessos/theater2_hu_f54d72a5c2c71a2f.webp 1200w, https://blog.joway.io/images/turkey/termessos/theater2_hu_b27f6e39e83365b5.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/theater3_hu_bdc18283ada12dd6.webp" srcset="https://blog.joway.io/images/turkey/termessos/theater3_hu_7cf6acad4bb7ec70.webp 800w, https://blog.joway.io/images/turkey/termessos/theater3_hu_ff1b5584d1a00c53.webp 1200w, https://blog.joway.io/images/turkey/termessos/theater3_hu_bdc18283ada12dd6.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="960" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/turkey/termessos/theater_wide_hu_ac517274af036513.webp" srcset="https://blog.joway.io/images/turkey/termessos/theater_wide_hu_fe6f14fe387020a9.webp 800w, https://blog.joway.io/images/turkey/termessos/theater_wide_hu_3e0258da33f39f2c.webp 1200w, https://blog.joway.io/images/turkey/termessos/theater_wide_hu_ac517274af036513.webp 1280w" sizes="(max-width: 800px) 100vw, 800px" width="1280" height="299" alt="欧游散记 —— 特摩索斯古城" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;特摩索斯在人类历史上，只是一个非常短暂，也没有名气，更没有对任何历史节点产生重要影响的小城市。特摩索斯的统治者也并非历史上赫赫有名的王侯将相，甚至这座城市更像是纯粹的自治军事城邦而非君主制的王国。但是如此小的城市居然会为了其居民建立如此恢弘的一个剧场。更何况这里不靠近雅典也不靠近罗马，地处希腊世界的边陲。&lt;/p&gt;
&lt;p&gt;站在特摩索斯的山顶上，我眼望着希腊文明的「边际产物」，如同看到了一个外星文明一般陌生。从山脚下看到的自我价值实现和山顶上对自然与人类文明极致的审美表达。特摩索斯的居民在群雄逐鹿中维持了独立，依靠工程学知识建立起了少见的防卫居住一体的水利设施，甚至还在这种山城里建立起了不输其他同等规模但地处丰饶平地的希腊城邦的剧院。&lt;/p&gt;
&lt;p&gt;我在之后的土耳其境内的希腊化古城邦旅途中，也遇到了更多比特摩索斯大得多，恢弘得多的城邦，但是特摩索斯一直是最特别的那个，也是我记忆最深的一个。以特摩索斯作为我在古希腊-罗马世界的游历起点，很难想到比这更好的开始了。&lt;/p&gt;</content:encoded></item><item><title>重新思考 Go：Channel 不是「消息队列」</title><link>https://blog.joway.io/posts/golang-rethink-channel/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-channel/</guid><category>Tech</category><description> 重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。
Go 语言是一门为实现 CSP 并发模型而设计的语言，这也是它区别于其他语言最大的特色。而为了实现这一点，Go 在语法上就内置了 chan 的数据结构来作为不同协程间通信的载体。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Go 语言是一门为实现 CSP 并发模型而设计的语言，这也是它区别于其他语言最大的特色。而为了实现这一点，Go 在语法上就内置了 &lt;code&gt;chan&lt;/code&gt; 的数据结构来作为不同协程间通信的载体。&lt;/p&gt;
&lt;p&gt;Go 的 channel 提供 input 和 output 两种操作语法。input 一个已经 full 的 channel ，或是 output 一个 empty 的 channel 都会引发整个协程的阻塞。这个协程阻塞性能代价很低，也是协程让渡执行权的主要方法。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// input&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// output&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然而 channel 的实现恰好和进程内消息队列的大部分需求是吻合的，所以这个结构时常被用来作为生产者消费者模型的实现，甚至还作为 channel 的主流应用场景而推广。&lt;/p&gt;
&lt;p&gt;但事实上，如果真的把该数据结构用来作为系统内核心链路的生产消费者模型底层实现，一不留神就会遇到雪崩级别的问题，且这些问题都不是简单的代码修改便能解决的。&lt;/p&gt;
&lt;h3 id="input-失败导致阻塞"&gt;Input 失败导致阻塞&lt;/h3&gt;
&lt;p&gt;当 channel 满的时候，&lt;code&gt;&amp;lt;-&lt;/code&gt; 操作会导致整个 goroutine 阻塞。显然这并不总是编程者希望的，所以 Go 提供了 select case 的方法来判断 &lt;code&gt;&amp;lt;-&lt;/code&gt; 是否成功：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// input failed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但问题是，当 channel input 失败时，编程者还能怎么做？除非队列的消息是可以被丢弃的，否则我们可能只再去创建一个类似 queue 的结构，将这部分消息缓存下来。但是这个 queue 的结构可能又要和这个 ch 本身的队列顺序处理好并发关系。&lt;/p&gt;
&lt;p&gt;再或者就是创建一个额外 goroutine 来执行 channel input 的操作，但这样的代价是 goroutine 大量增加且消息变得无序:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;或者还有一种最粗暴同时也是简单的方法，就是把队列 size 扩大到 N，N 的远大于业务系统正常流量下会遇到的大小。即便这种解决方法有掩耳盗铃的嫌疑，但却是我所见过的大部分人最常用的解决问题方法。实际上，让我们回归到现实中，回想我们这些年来 make channel 的时候，真的做过非常仔细地做过估算吗？还是绝大部份时候就是随便选一个差不多的数字？况且即便是在当下经过严谨的估算，这个合理值也会随着系统流量变化或是代码逻辑的改变，导致不再合理。&lt;/p&gt;
&lt;p&gt;在大部分时候，或许并不会有什么太大的问题。但当有问题的时候，这个 input 操作可能就会导致某个核心 goroutine 直接阻塞。尤其是 input 的目的往往就是不要阻塞核心链路处理，才会想用消息队列的方式去异步处理，所以 input 发生在核心链路的概率非常大。&lt;/p&gt;
&lt;h3 id="output-速度不可控"&gt;Output 速度不可控&lt;/h3&gt;
&lt;p&gt;最常见的消费者的写法如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果这里的 process 函数是 CPU 密集型函数或者是需要访问外部网络，这里都需要单独开一个 goroutine 异步执行。否则消费速度会因为外部原因出现不受控的抖动，进而导致整个消息队列生产也被卡住。&lt;/p&gt;
&lt;p&gt;但一旦开了 goroutine 异步执行，倘若 process 是 CPU 密集型函数，我们又完全无法控制消费速度，一旦生产速度过快，系统也会遭遇到非预期的压力。何况大部分进程内消息队列使用的目的就是为了不重要的事情延后直接来提升服务实时任务的延迟。&lt;/p&gt;
&lt;p&gt;而这里的限速，我们并不能直接限在 ch 消费的 for 循环本身，因为 channel 的消费速度和生产速度是相等的，一旦限速在这里，会导致生产者也被阻塞。所以我们还得在 process 函数内，单独加入一些深思熟虑的限速逻辑。&lt;/p&gt;
&lt;p&gt;当你做完所有保护措施后，你会发现或许最早的时候把 channel 作为消息队列的技术选型就是值得商榷的。&lt;/p&gt;
&lt;h3 id="重新思考-channel"&gt;重新思考 Channel&lt;/h3&gt;
&lt;p&gt;从 Go 语言本身来说，为用户提供一个便捷的乃至是语法级别的进程内消息队列实现，显然并不是语言设计者想要考虑的事情。之所以 Go 在语法层面上提供了 chan 这样一个结构，真正的目的是为了多协程间通信和休眠唤醒。它的功能和语法也很单一，并不能真正做到一个健壮的消息队列数据结构所该有的能力，也并非它的设计初衷。&lt;/p&gt;
&lt;p&gt;其他语言中，如果用户想要实现同类需求，会自然而然去找有更工程化设计的生产者消费者模型实现，而 Go 中由于 channel 结构用得顺手，语法简单，会自然地在技术选型中使用它作为实现。进而忽视了更多安全性的问题。&lt;/p&gt;
&lt;p&gt;解决这个问题的可行办法，只能是 Go 需要在标准库里内置一个更加完备的消息队列实现，至少需要实现可以非阻塞且安全地生产消息的能力，才能让用户脱离对 channel 队列能力的依赖和认知。&lt;/p&gt;</content:encoded></item><item><title>重新思考 Go：Slice 只是「操作视图」</title><link>https://blog.joway.io/posts/golang-rethink-slice/</link><pubDate>Sat, 30 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-slice/</guid><category>Tech</category><description> 重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。
Go 在语法级别上提供了 Slice 类型作为对底层内存的一个「操作视图」:
var sh []any // ==&gt; internal struct of []any type SliceHeader struct { Data uintptr Len int Cap int } 编程者可以使用一些近似 Python 的语法来表达对底层内存边界的控制:</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Go 在语法级别上提供了 Slice 类型作为对底层内存的一个「&lt;strong&gt;操作视图&lt;/strong&gt;」:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ==&amp;gt; internal struct of []any&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SliceHeader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Len&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编程者可以使用一些近似 Python 的语法来表达对底层内存边界的控制:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// {Len=100, Cap=1000}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// {Len=900, Cap=900}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// {Len=100, Cap=100}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;虽然 Slice 的语法看似简单，但编程者需要时刻记住一点就是 &lt;strong&gt;Slice 只是一个对底层内存的「&lt;strong&gt;操作视图&lt;/strong&gt;」，而非底层「内存表示」，Slice 的各种语法本身并不改变底层内存&lt;/strong&gt;。绝大部分 Slice 有关的编程陷阱根源就在于两者的差异。&lt;/p&gt;
&lt;h3 id="slice-陷阱持有内存被放大"&gt;Slice 陷阱：持有内存被「放大」&lt;/h3&gt;
&lt;p&gt;以最简单的从连接中读取一段数据为例，由于我们事先并不知道将会读取到多少数据，所以会预先创建 1024 字节的 buffer ，然而如果此时我们只读取到了 n bytes, n 远小于 1024，并返回了一个 &lt;code&gt;len=n&lt;/code&gt; 的 slice，此时这个 slice 的真实内存大小依然是 1024。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;即便上一步我们内存放大的问题并不严重，比如我们的 n 恰好就是 1024。但我们依然会需要对连接读到的数据做一些简单的处理，例如我们现在需要通过 Go 的 regexp 库查询一段 email 的数据：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;FindEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\\w+@\\w+.\\w+&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// output: len=cap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;正则函数返回的 slice，依然持有着整段连接数据。当我们的切片处理函数越来越多时，我们的系统中可能充斥着无数原始连接数据的子切片。而由于 Go 自带 GC 所以编程者很少会去思考内存和变量的生命周期，所以当众多代码中的某一个函数决定将某个数据字段比如这里的 email 字段放进一个全局对象中时，编程者通常不会想到主动去额外 copy 一份数据，而是直接把这个切片直接放入： &lt;code&gt;cache[email] = ...&lt;/code&gt; 。此时，整段连接数据就会被一直持有引用而得不到释放。&lt;/p&gt;
&lt;p&gt;像是这类问题由于过于容易犯错，所以很多复杂的业务系统中倘若仔细观察，多多少少都能发现类似的内存被「放大」的现象。但是由于 Go 本身内存占用就不大，大部分时候开发者仅在发现内存以肉眼可见的速度在持续上涨时，才会通过 profling 对其中问题最严重的那部分代码进行集中治理。&lt;/p&gt;
&lt;p&gt;然而虽然 Go profling 工具链非常完善，但唯独在查找内存引用被哪些对象持有方面却几乎是空白的。这类问题只能依靠阅读每一行有可能持有底层内存引用的代码来逐个排查。&lt;/p&gt;
&lt;h3 id="slice-陷阱-指针元素无法被释放"&gt;Slice 陷阱： 指针元素无法被释放&lt;/h3&gt;
&lt;p&gt;当 Slice 中的元素类型为值类型时，Slice 指向的内存仅包含一段连续内存。然而，当 Slice 的元素类型为指针类型时，情况就会变得复杂了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ConnPool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Conn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ConnPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ConnPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码是一个典型的 FILO 连接池实现，其中有一个隐蔽的 Bug 是，Get 函数在取了一个引用类型的元素后，没有将其底层内存上对应位置存储的指针引用置零，导致即便用户释放了连接对象后，这个 slice 底层一直持有该指针，仅当未来有机会被重新覆写时才会被释放。正确的写法是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ConnPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// reset to nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然而，如果元素为值类型时，我们却并不需要做这类操作因为 slice 的底层内存大小已经固定，置零为空结构体没有任何意义。这种同一个类型同一个语法，却因元素类型而编程范式不一致的设计，极大地促使了这个编程错误出现的概率，对编程者的心智负担也极重。&lt;/p&gt;
&lt;h3 id="性能问题-低效的-memclr-行为"&gt;性能问题： 低效的 memclr 行为&lt;/h3&gt;
&lt;p&gt;slice 最为常见的用法莫过于 &lt;code&gt;make slice and copy data&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// =&amp;gt; ASM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;XORL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;needzero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;MOVQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;XORL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;CALL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mallocgc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;MOVQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="nx"&gt;autotmp_8&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;MOVQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;MOVQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;PCDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;CALL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memmove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime.mallocgc implementation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mallocgc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;typ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;needzero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;needzero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needzero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;memclrNoHeapPointers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里 mallocgc 的第三个参数 &lt;code&gt;needzero&lt;/code&gt; 被置为了 false，意味着这段内存我们并不需要调用底层的 &lt;code&gt;memclrNoHeapPointers&lt;/code&gt;(类似于 C 中的 &lt;code&gt;memset&lt;/code&gt;) 去清零整段内存。&lt;/p&gt;
&lt;p&gt;然而，如果我们在 make 和 copy 之间插入一段 if 语句后，汇编就会变成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// =&amp;gt; ASM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;LEAQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SB&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;MOVQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CX&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;CALL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeslice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ... as same as before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;makeslice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;et&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unsafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pointer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mallocgc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;et&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里我们会发现，原先的 mallocgc 变成了 makeslice 调用。而 makeslice 的实现中，已经强制设置 &lt;code&gt;needzero = true&lt;/code&gt; 。也就是说，第二种写法，势必会让整段内存被调用一次 &lt;code&gt;memclrNoHeapPointers&lt;/code&gt;。但实际上，从代码的语义上来说，我们依然确保了 buf 一定会被整段 data 覆写，所以事实上这里的 &lt;code&gt;memclrNoHeapPointers&lt;/code&gt; 操作是浪费的。&lt;/p&gt;
&lt;p&gt;虽然这部分差异看似很小，但在实际项目中，尤其是那种 RPC 反序列化密集的场景里，众多字段都通过大量 &lt;code&gt;make and copy&lt;/code&gt; 的方式生成，而这些代码常常因 make 和 copy 代码之间插入了其他代码导致没被编译优化，所以会出现在 &lt;code&gt;memclr&lt;/code&gt; 之后紧接着 &lt;code&gt;memmove&lt;/code&gt;。因为字段众多，所以这细碎的小开销累加起来非常夸张。&lt;/p&gt;
&lt;h3 id="重新思考-slice"&gt;重新思考 Slice&lt;/h3&gt;
&lt;p&gt;无论是编程陷阱还是性能问题，我们都可以看到在使用 Slice 的时候，我们需要时刻关注底层内存的状态。但 Go 在语法上却又将底层内存给隐藏了起来。&lt;/p&gt;
&lt;p&gt;真实世界对一段连续内存的创建，拷贝，操作需求多种多样，但在 Go 中表达能力却非常有限。Go 试图用统一简洁的 slice 语法把这些需求都包含在内，但同时，用户却又不得不因为元素类型不同而需要思考不同的操作方法。甚至在 Go 1.21 引入 clear 之前，对一个引用类型的 slice 置零我们还需要手动循环遍历 &lt;code&gt;for i := range slice { slice[i] = nil }&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;虽然 Slice 的语法级实现提供了极大的便利性，但是这种便利性不应是牺牲了基本的安全性为代价的。何况某些基本操作如果有且仅能用花哨易错的语法实现，那么不仅没有安全性，也丝毫看不到任何便利性。&lt;/p&gt;
&lt;p&gt;实际上 Go 已经在改变这种单纯依靠语法实现所有内存操作的思想了。Go 1.21 引入了 slices 标准库来操作 slice 结构。例如上面删除指针元素的问题就可以使用 slices.Delete 函数来替代繁杂且易错的语法操作(虽然 1.21 版本这个函数实现意外的居然没有修复内存泄漏的问题，但&lt;a href="https://go-review.googlesource.com/c/go/&amp;#43;/541477"&gt;新的修复已经 merged&lt;/a&gt; )。&lt;/p&gt;
&lt;p&gt;未来我们或许会看到，在一些稳定性优先的公司里，会更推崇使用标准库来操作 slice，让 slice 类型本身回归到一个不需要了解底层实现原理和各路花哨用法的简单数据类型。&lt;/p&gt;</content:encoded></item><item><title>那一天，我决定踏出一步</title><link>https://blog.joway.io/posts/run-away-from-shanghai/</link><pubDate>Tue, 10 May 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/run-away-from-shanghai/</guid><category>Thought</category><description>四月与五月之交，我完成了人生中迄今为止最惊心动魄的一次冒险。与通常的冒险经历不同的是，一般的冒险人们总愿意在往后的岁月里反复回忆甚至加工，但我的这次冒险我希望此生永远的忘记，但我又明白，之所以我如此迫切地想要忘记，是因为这段经历将会不可逆地影响我一生。</description><content:encoded>&lt;p&gt;四月与五月之交，我完成了人生中迄今为止最惊心动魄的一次冒险。与通常的冒险经历不同的是，一般的冒险人们总愿意在往后的岁月里反复回忆甚至加工，但我的这次冒险我希望此生永远的忘记，但我又明白，之所以我如此迫切地想要忘记，是因为这段经历将会不可逆地影响我一生。&lt;/p&gt;
&lt;p&gt;过去的那个少年已经被这场冒险所杀死，我只是作为一个旁观者，在叙述一个已经去世了的人在那几天的经历。&lt;/p&gt;
&lt;p&gt;这场冒险的源头还要从 3 月开始说起。&lt;/p&gt;
&lt;p&gt;3 月初，我在北京出差，在返回上海的前两天，当时因为北京有人在乌克兰大使馆门口献花，导致三里屯的使馆区莫名其妙被政府封了。我和朋友那天晚上临时起意，打算去现场看看，于是就绕着乌克兰大使馆走出了一个方形的圈。这件事情与后面发生的事情并没有什么直接的联系，但它确是我在中国境内拥有的最后一次自由行走的经历，而我在三里屯画下的这个圈，也成为了后面发生的事情的一个隐喻。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/ukraine_hu_164ef8339256b6bc.webp" srcset="https://blog.joway.io/images/run/ukraine_hu_f67993aeeaa28110.webp 800w, https://blog.joway.io/images/run/ukraine_hu_b938ca3ce37d1907.webp 1200w, https://blog.joway.io/images/run/ukraine_hu_164ef8339256b6bc.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/sanlitun_hu_77dde99c39b0afcf.webp" srcset="https://blog.joway.io/images/run/sanlitun_hu_5d47072347492663.webp 800w, https://blog.joway.io/images/run/sanlitun_hu_77dde99c39b0afcf.webp 1170w" sizes="(max-width: 800px) 100vw, 800px" width="1170" height="932" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;2022 年 3 月 4 日，我从北京回到上海，而这一天北京办公室却检测出了一例阳性，导致我回到上海后被判定为所谓的「高危筛查人员」，于是就开始了原定于 14 天的居家隔离。然而从 3 月 16 号开始，整个打浦桥街道开始出现了非常多核酸异常的情况，我小区在内的相当一部分小区变成了所谓的封闭管理。每天都需要做一次核酸，但是小区里的居民此时并不知道问题的严重性，甚至我怀疑连居委会都不见得知道真实的核酸异常数据，所以在做核酸时，大家有说有笑，许多老头老太甚至在排队时都不戴口罩。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/notice_hu_d4700841dda8663d.webp" srcset="https://blog.joway.io/images/run/notice_hu_cebc6c028cd329a0.webp 800w, https://blog.joway.io/images/run/notice_hu_a2b1b7dfab3f39c8.webp 1200w, https://blog.joway.io/images/run/notice_hu_d4700841dda8663d.webp 1500w" sizes="(max-width: 800px) 100vw, 800px" width="1500" height="1999" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;当时所有人的预期是，所谓的 2+12 封闭管理就是只需要小区被封闭两天而已。这对于年轻人来说，可能平常出小区的频率也就两天一次，所以几乎没有任何影响。并且此时我们依然可以在小区内自由活动，去门口取外卖。&lt;/p&gt;
&lt;p&gt;3 月 18 号，这是原定小区解除封闭管理的一天，而我个人也早已满足了 14 天的居家隔离要求。那天中午，我兴冲冲地跑下楼，想要去商场里面吃一顿好的，结果发现小区并没有如期的解封，反而是又贴了一份继续 +2 的通知。此时我的愤怒在于它打破了我们原先的预期，导致原先的欢喜落空，但我依然相信只要再坚持两天就能解封。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/xiaoqu1_hu_41b24aa262f27a23.webp" srcset="https://blog.joway.io/images/run/xiaoqu1_hu_8d55b90ea45ea9f6.webp 800w, https://blog.joway.io/images/run/xiaoqu1_hu_41b24aa262f27a23.webp 1080w" sizes="(max-width: 800px) 100vw, 800px" width="1080" height="1920" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/xiaoqu2_hu_f1b50fb84019f22b.webp" srcset="https://blog.joway.io/images/run/xiaoqu2_hu_28d96922926c1090.webp 800w, https://blog.joway.io/images/run/xiaoqu2_hu_8715033103aedebb.webp 1200w, https://blog.joway.io/images/run/xiaoqu2_hu_f1b50fb84019f22b.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="900" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;3 月 20 号的时候，我楼道门突然在毫无事先通知的情况下被一道铁链锁住。试图去推开一道推不开的铁门，这种屈辱感和无力感永生难忘。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/chain_hu_1972f3a37b411612.webp" srcset="https://blog.joway.io/images/run/chain_hu_86bd2d99d258c44d.webp 800w, https://blog.joway.io/images/run/chain_hu_444df3015097cb47.webp 1200w, https://blog.joway.io/images/run/chain_hu_1972f3a37b411612.webp 1500w" sizes="(max-width: 800px) 100vw, 800px" width="1500" height="1999" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;20 日晚上等了 2 个小时，才吃到已经在外面冻冰了的鳗鱼饭，21 日中午又等了 2 个小时拿不到外卖然后吃的泡面的时候，而当天晚上彻底外卖就没送过来了。那时候才第一次知道原来饿久了，眼睛会花手会抖。不知道明天会发生什么，但当天晚上我就把恒生指数的基金清仓了。&lt;/p&gt;
&lt;p&gt;23 日已经彻底放弃了外卖这件事情，中午八宝粥，晚上吃泡面加饼干。接下来的每一天都接近于此。点外卖几乎只能靠金钱加上运气，偶尔会有一些商贩“违规”在外卖平台上上线一些熟食之类的东西，那时候偶尔才能够有一点加餐。甚至有一次还抢到了能够让我吃上两天的两桶麦当劳全家桶。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/home1_hu_75cad94f1ef65a9f.webp" srcset="https://blog.joway.io/images/run/home1_hu_851ed8ebee9c615.webp 800w, https://blog.joway.io/images/run/home1_hu_1715976af5977ee9.webp 1200w, https://blog.joway.io/images/run/home1_hu_75cad94f1ef65a9f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="900" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/home2_hu_5b1218c6064c27be.webp" srcset="https://blog.joway.io/images/run/home2_hu_c026736da08e08cd.webp 800w, https://blog.joway.io/images/run/home2_hu_4338d047d8007d38.webp 1200w, https://blog.joway.io/images/run/home2_hu_5b1218c6064c27be.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/homeview_hu_ffad7af7b7a228db.webp" srcset="https://blog.joway.io/images/run/homeview_hu_9b4c675160e17b14.webp 800w, https://blog.joway.io/images/run/homeview_hu_e798468f66f75384.webp 1200w, https://blog.joway.io/images/run/homeview_hu_ffad7af7b7a228db.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;再接着就是大家所熟知的 4 月开始上海进入事实上的全面封城状态。&lt;/p&gt;
&lt;p&gt;我所在的黄浦区，在整个上海属于重灾区，而我的小区又是黄浦区的重灾区，我所在的楼栋又属于小区里的重灾区，先于上海绝大部分小区率先封闭管理。上海发布每过几天通报一次我楼栋有了新的阳性。截止我离开的时候，楼栋里阳性户数至少也有 30%，悲观估计可能多达 50%。&lt;/p&gt;
&lt;p&gt;4 月 18 号的时候，我明白自己不可能继续这种生活，开始规划逃离上海的行程，于是给居委打了电话咨询了开出门证的要求，让我意外的是，虽然我的楼栋形势严峻，但是给我的要求却还算合理（当然事实证明我天真了）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;楼栋 7 天无阳性&lt;/li&gt;
&lt;li&gt;48 小时核酸阴性&lt;/li&gt;
&lt;li&gt;全程闭环车运输&lt;/li&gt;
&lt;li&gt;前序航班到达&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中其他几项都可以通过花钱解决，唯独所在楼栋七天无阳性这一项对我来说充满了不确定性。对于在上海的绝大部分人来说，这个要求确实并不过分，也很容易达到。但对于我这栋重灾楼来说，就需要时刻算着一个 7 天的窗口期，到了窗口期立马走。&lt;/p&gt;
&lt;p&gt;我们楼上一个阳性是 4 月 14 号出现的，所以下一个 7 天窗口期是 4 月 22 号，但是好巧不巧 21 号又出了阳性，所以需要等到 4 月 29 号。&lt;/p&gt;
&lt;p&gt;4 月 29 号是五一假期前最后一个工作日，我在上午 10:30 兴奋地给居委打了电话，结果居委却告诉我还要再加 7 天监测期才能给我开出门证。得知这个消息的时候，我已经彻底愤怒了，甚至询问居委，如果我现在从楼上跳下去，断了条腿，总能够开出门证让我出门吧。好在我前期做了一些功课，也给市民热线打过电话，大体摸清楚了这个所谓的离沪政策，根本毫无政策可言，责任和风险完全丢给了当地居委，所以我需要做的是给她传达足够大的压力，超过她放我出去的风险为止，才能拿到我需要的出门证。所以虽然事实上我根本没法打通街道办的电话，但我还是告诉居委街道办给我的政策就是 7 天无阳性，她必须要给我出去。由于上午居委还要出去做核酸，所以给我答复说晚些时候再和街道办确认下。&lt;/p&gt;
&lt;p&gt;到了晚上 7:30，我当时心中已经不抱有任何希望了，开了瓶啤酒，准备在家里过五一。但是我还是给居委打了电话，让我惊讶的是，此时居委却松口愿意给我出门证了，并且由于第二天她还要早起做核酸，愿意当天晚上就把第二天的证给我。我花了 30 分钟，写好了承诺书，定了 5 月 2 号的从香港转机到目的国的机票，并且花了 1500 块钱订好了第二天早上 9 点前往浦东机场的包车。把她需要的所有材料都准备在一个 U 盘里，跑到居委会。在门口等了 20 分钟，最终拿到了我梦寐以求的出门证。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/cert1_hu_90d2a279a88039cb.webp" srcset="https://blog.joway.io/images/run/cert1_hu_31f40024b8311840.webp 800w, https://blog.joway.io/images/run/cert1_hu_84f7532e0fda7381.webp 1200w, https://blog.joway.io/images/run/cert1_hu_90d2a279a88039cb.webp 1412w" sizes="(max-width: 800px) 100vw, 800px" width="1412" height="1638" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/cert2_hu_2880e85fea32761b.webp" srcset="https://blog.joway.io/images/run/cert2_hu_ed2c2402ada28ecd.webp 800w, https://blog.joway.io/images/run/cert2_hu_9dd25d7aa8d7bd0b.webp 1200w, https://blog.joway.io/images/run/cert2_hu_2880e85fea32761b.webp 1402w" sizes="(max-width: 800px) 100vw, 800px" width="1402" height="1170" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;我无法用言语形容我拿到这张出门证的时候有多么高兴，甚至和我姐说，我就算我拿到北大的录取通知书都不会有这么高兴。直到现在，我还记得那天居委会门口空气的味道，我对居委会大妈说了不下 20 次谢谢，这是真心的感谢。即便从法理上，居委会根本没有权利限制我自由，从道德上，居委会所有成员都应该作为纳粹帮凶被判刑，但我依然在那个时刻对她们说出了我这辈子连续说过的最多次谢谢。&lt;/p&gt;
&lt;p&gt;然而就当我在家里欢快地收拾行李时，居委在晚上 10 点打来了一个电话，当看到电话来的时候，我心里就有不好的预感，但我总觉得上帝不会如此捉弄我。可是，接到电话的时候，居委和我说要告知我一个坏消息，我们楼里今天上午的核酸检测出来了两个阳性。这个晴天霹雳让我前面所有的开心，所有的比喻，都像是一个巨大而悲惨的笑话。我问居委，如果我早一个小时出门，是不是我就是合法出小区了，居委说是的。我和居委说，我已经不需要告诉你我的心情是怎么样的了，你作为一个人，你自己能够明白你在对我做什么事情。&lt;/p&gt;
&lt;p&gt;我一辈子遵纪守法，即便是在中华人民共和国的驾照考试中，因为我色弱行了一个这个国家所有色弱都会且必须要行的贿，我都难过了一年。我为上海市政府交了至少 20% 收入的税。但是这个国家现在却要逼良为娼，逼我上梁山。&lt;/p&gt;
&lt;p&gt;我告诉自己，如果我现在真的就认了，不出小区，我这辈子都会瞧不起自己，我所有的人生偶像都会瞧不起我。即便坐牢，我也必须走出这个小区。这已经不是一个选择题，这是我的命运，我必须去迎接它。&lt;/p&gt;
&lt;p&gt;于是我强行把电话挂了，不再接任何居委的电话。忍着眼泪，用最快的速度把东西全部打包好。我不知道我这眼泪从何而来，为了我自己，还是为了这个越来越荒谬的城市，这个我一直为之奋斗，深爱着的城市。&lt;/p&gt;
&lt;p&gt;我把行李分成了两个箱子，一个大箱子放衣物，一个小箱子放食物，书包放护照等贵重物品。如果遇到最坏情况，我就提着食物的箱子跑，如果比最坏还更坏，我就带着书包跑。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/bag_hu_d5fb6a0381fcab1d.webp" srcset="https://blog.joway.io/images/run/bag_hu_6b4181b9b49ba111.webp 800w, https://blog.joway.io/images/run/bag_hu_f29f7831a3df7068.webp 1200w, https://blog.joway.io/images/run/bag_hu_d5fb6a0381fcab1d.webp 1500w" sizes="(max-width: 800px) 100vw, 800px" width="1500" height="1999" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;晚上 11 点，我背着书包提着一大一小两个行李箱，走下楼道，楼已经被贴了封条，所幸没有被硬隔离。我害怕小区里的人发现我走了，用两只手提着箱子不让轮子滚动，但是箱子实在是太重，200 米的路可能走了有十几分钟。我能够感受到楼上正有无数双眼睛盯着我，我不知道他们是否会在小区大群举报我，不知道居委会不会随时冲出来拦住我，不知道小区门卫会不会放我走，甚至也不知道出了小区我能够提着这两大箱子去到哪里。&lt;/p&gt;
&lt;p&gt;我心想，我这是真的走出了舒适区，走向了一条充满了不确定性的路。我甚至感觉自己是在玩一个游戏，不知道前方有多少副本，多少 NPC 在等着我。&lt;/p&gt;
&lt;p&gt;到了小区门口，我把出门证给门卫看。门卫看到这是 4 月 30 号的出门证，问我为什么 29 号走。我说如果你一定要这么严格，那我就在门口坐到 0 点走，这总可以了。门卫觉得确实没这个必要，就同意放行。&lt;/p&gt;
&lt;p&gt;小区门已经被各种栏杆椅子堵住，看得出来这段时间应该几乎没有任何人离开过小区。好不容易扒开一个口子，我终于双脚踏出了小区。那一刻，我知道，我没有了回头路了，而前方也看不到路，上海的夜晚还是那么寒冷，眼下当务之急，是解决下这个夜晚我要去哪里的问题。&lt;/p&gt;
&lt;p&gt;好在我平时就喜欢散步，之前公司也在家旁边，所以附近哪些角落适合落脚，哪些角落有遮风避雨处我还算知道。就先冲着最坏的露宿街头打算前进，与此同时，再和原本约了早上 9 点车的客服沟通，是否能够改到半夜，虽然我自己也觉得并不现实。客服骂了我一顿无理取闹以后，突然司机莫名其妙给我打了电话，告诉我可以过来接我，并且 20 分钟就能到达。此刻，我终于感受到了什么叫天无绝人之路，只要这个国家还留有一丝允许人发挥主观能动性的角落，就没有解决不了的问题。&lt;/p&gt;
&lt;p&gt;20 分钟后，我上了前往浦东机场的车。窗外的一切都是那么的熟悉而陌生。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/car1_hu_53bdf2f89628f1c1.webp" srcset="https://blog.joway.io/images/run/car1_hu_589187f6ef1f0c44.webp 800w, https://blog.joway.io/images/run/car1_hu_d4e5fd825c79e055.webp 1200w, https://blog.joway.io/images/run/car1_hu_53bdf2f89628f1c1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/roadview1_hu_bc2660c8a8b98637.webp" srcset="https://blog.joway.io/images/run/roadview1_hu_934992ec649602a2.webp 800w, https://blog.joway.io/images/run/roadview1_hu_5c2eb521bac3dc8b.webp 1200w, https://blog.joway.io/images/run/roadview1_hu_bc2660c8a8b98637.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;4 月 30 日 0 点，我成功到达浦东机场。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/pvg1_hu_5f82d9551354d12c.webp" srcset="https://blog.joway.io/images/run/pvg1_hu_f3f47da2b2cc834e.webp 800w, https://blog.joway.io/images/run/pvg1_hu_8182633a10398c69.webp 1200w, https://blog.joway.io/images/run/pvg1_hu_5f82d9551354d12c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;由于抵达我目的国最近的航班只能买到 5 月 2 号，这意味着我需要在机场过三个晚上，而且我还只能买到香港转机的，所以在香港机场还要再住一晚上。我曾经也去过各种艰苦地方旅行，所以我预期机场过夜对我来说也没有太大问题。但是简陋地过了一夜后，困难程度确实超出了我的想象。&lt;/p&gt;
&lt;p&gt;首先由于我去的是一个赤道国家所以根本没带什么保暖的衣物，而机场夜晚的温度低的吓人。其次机场里时不时会有喇叭播放一些毫无意义的政府宣传广播，这对入眠也带来了极大的困难。另外，我无法确保机场里的人是绝对安全的，如果此刻我再感染上新冠，那不仅前功尽弃，我甚至都说不清楚自己是在小区内染上的还是在外面染上的。如果再被人恶意举报，防疫法也足够我吃上一壶。所以我还必须全程带口罩入睡。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/sleep_hu_dc58863b1d9f3579.webp" srcset="https://blog.joway.io/images/run/sleep_hu_b0d70755e59ae0f9.webp 800w, https://blog.joway.io/images/run/sleep_hu_e20dfd7a2349a63d.webp 1200w, https://blog.joway.io/images/run/sleep_hu_dc58863b1d9f3579.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;第一个夜晚我基本就没怎么睡着。到了白天，我开始刷新机票，查询各个中转方案。另外，上海还有一个政策是离沪必须有 48 小时核酸，而我到了 1 号其实就过了 48 小时了。虽然我的目的国不要求任何核酸，但是我并无法确定航空公司不需要，海关也不需要。所以白天基本都在研究各种意外情况下的备选方案。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/plan_hu_2b58804d8342f62c.webp" srcset="https://blog.joway.io/images/run/plan_hu_d5194c39f3bbddc6.webp 800w, https://blog.joway.io/images/run/plan_hu_2b58804d8342f62c.webp 872w" sizes="(max-width: 800px) 100vw, 800px" width="872" height="260" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/pvgview_hu_2343ad5910b49824.webp" srcset="https://blog.joway.io/images/run/pvgview_hu_88bf5a156cbab1d3.webp 800w, https://blog.joway.io/images/run/pvgview_hu_732bf7d0c5ed511f.webp 1200w, https://blog.joway.io/images/run/pvgview_hu_2343ad5910b49824.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;30 号晚上 6 点，由于前一天双手提箱子把肩颈肌肉彻底用废了，浑身酸痛，我喝了瓶啤酒（是的，我逃难还带啤酒）打算早早睡了节约写能量。但是 7 点多又醒来了，这种住宿条件完全无法入睡，望着窗外，我开始觉得自己真的有点太艰苦了。而且雪上加霜的是，我还发现自己已经有点感冒了，而我并没有带感冒药。&lt;/p&gt;
&lt;p&gt;在当时的上海，感冒无疑等同于犯罪。尤其是在机场。我的身体告诉我，我可能熬不到再在机场待上两个晚上。虽然我知道机场附近绝大部分酒店都已经关了，就算没关，也没有车辆能派到机场来接我。但还是在携程上一个个尝试，终于被我找到一家破的有点像是贾樟柯电影里的小旅馆，却标价 700 多一晚上。直觉告诉我这个价格有点不太对劲，于是我抱着买彩票的心态打过去试试，让我喜出望外的是，这个小旅馆不仅可以住，而且还能叫车来机场接我。我实在想不到，这个国际化大都市还能找得出比这个小旅馆更好更完美的容身之地。&lt;/p&gt;
&lt;p&gt;上了车后，旅馆老板娘让我先不要下车，她观察周围情况后告诉我可以下车再下车。我大体明白这个旅馆也是在半合法的状态下营业。但是无论如何，我还是找到了一个安全屋。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/bed_hu_df494260de30579.webp" srcset="https://blog.joway.io/images/run/bed_hu_f9f75d4baac32cd2.webp 800w, https://blog.joway.io/images/run/bed_hu_2409617fa4ced6fd.webp 1200w, https://blog.joway.io/images/run/bed_hu_df494260de30579.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="893" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;5 月 1 号上午，理论上我此时就不再具有 48 小时核酸的证明了，所以我需要再打车前往一个核酸点做核酸然后再回到旅馆。我和当初来机场接我的司机达成交易，浦东新区内无论哪里接我都 200 块钱一趟，往返 400 。如果说居委会赐予了我一次生命，这个司机就是赐予了我能够活动的四肢，我想象不到在现在的上海，还有什么我没有的自由。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/car2_hu_9ebb9b4b8c591de9.webp" srcset="https://blog.joway.io/images/run/car2_hu_d24ad35467661876.webp 800w, https://blog.joway.io/images/run/car2_hu_45e6688b0f8120a6.webp 1200w, https://blog.joway.io/images/run/car2_hu_9ebb9b4b8c591de9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/pcr_hu_3919c0ebb6407900.webp" srcset="https://blog.joway.io/images/run/pcr_hu_719c51990537b580.webp 800w, https://blog.joway.io/images/run/pcr_hu_642967d2b2c82af3.webp 1200w, https://blog.joway.io/images/run/pcr_hu_3919c0ebb6407900.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;等待核酸结果的过程是煎熬的。因为我确实当时已经有了一些感冒的症状，而且我还在机场滞留了那么久，非常害怕自己核酸是阳性。巧就巧在，那天上海基本已经实现了社会面清零，而我正属于社会面人士，如果这个时候我自己阳性，无疑会变成重点关照对象，扰乱了上海政府早已破碎却自以为体面的大局。&lt;/p&gt;
&lt;p&gt;大约下午 6 点左右，我的核酸结果出来了，阴性。一颗心悬了下来。但是意想不到的是，5 月 1 号，原本提供中英文证明的核酸检测机构突然启用了核酸码，不再能够网上预约。而我当时并没有意识到这个细微的差异。导致我虽然核酸结果出来了，但是并不能够拿到核酸报告，只能拿到上海核酸系统里的一个记录。但是好在我目的地海关并不要求核酸报告，我只能赌一把上海和香港的航司与海关都能遵守书面政策要求不加码要求我提供核酸报告了。&lt;/p&gt;
&lt;p&gt;5 月 2 号早上 6 点，我第二次到达了浦东机场。但其实我的飞机要傍晚才飞，司机因为自己本身是非法运营，所以特地赶早把我送过去，免得后面受到交警排查。在机场等待的 10 个小时里，我心脏狂跳，因为不知道接下来会发生什么事情。最后，我开始在 Youtube 搜各种宗教音乐，一个国家的信誉彻底崩塌后，人自然需要一点牢不可破的东西来填补这块信仰的漏洞。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/car3_hu_5c4ec69abaaa4d1e.webp" srcset="https://blog.joway.io/images/run/car3_hu_9e3cf8dddd371d86.webp 800w, https://blog.joway.io/images/run/car3_hu_33e656f8c1e2be74.webp 1200w, https://blog.joway.io/images/run/car3_hu_5c4ec69abaaa4d1e.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;下午 4 点，成功值机拿到机票，航司严格遵守了入境国要求，并没有问我要核酸报告。在中国海关口，虽然我一切证件齐全还是被盘问了 10 分钟，所幸顺利出关，但是我前面的一个女性因为旅游签被两个海关人员在疯狂劝返。&lt;/p&gt;
&lt;p&gt;虽然理论上出了海关，几乎就没有任何意外能够阻拦我了，但是这一路不停的意外让我已经不敢提前庆祝胜利，因为每当我高兴时，总会有悲伤的事情突然袭来，我面对这种落差唯一的办法就是压抑自己的高兴。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/pvg2_hu_1974fd0558e82c5f.webp" srcset="https://blog.joway.io/images/run/pvg2_hu_d65852c8003c8179.webp 800w, https://blog.joway.io/images/run/pvg2_hu_d2f1c487cff680ff.webp 1200w, https://blog.joway.io/images/run/pvg2_hu_1974fd0558e82c5f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;晚上 6 点 40 分，飞机起飞，我离开了呆了 5 年的上海。我原本想在窗外，对着东方明珠塔竖一个中指，以回报这两个月上海对我所做的非法非人的囚禁，但是窗外一片漆黑，什么都看不到。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/shanghai_hu_eaf62885ad8398b7.webp" srcset="https://blog.joway.io/images/run/shanghai_hu_b593b77b5b0cad09.webp 800w, https://blog.joway.io/images/run/shanghai_hu_582cb36ed2c0a7b8.webp 1200w, https://blog.joway.io/images/run/shanghai_hu_eaf62885ad8398b7.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;5 月 2 日晚上 09:30 落地香港机场，我人生第一次出国也是在香港转机。那时，我到了香港机场第一件事，就是找到了一台电脑，打开来 google.com.hk ，感受自由上网是一种什么样的感受。但是今天的香港，和浦东机场一样空旷，绝大部分商店都已关闭，东西也都尽数搬空。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/hk1_hu_7c6b136aff030c7e.webp" srcset="https://blog.joway.io/images/run/hk1_hu_7e2ab5ebfb31da92.webp 800w, https://blog.joway.io/images/run/hk1_hu_e8412f952b3cd681.webp 1200w, https://blog.joway.io/images/run/hk1_hu_7c6b136aff030c7e.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;离我的下一程航班还有 22 小时，我还需要在香港机场过上一夜。但还好香港机场设施比浦东机场要好得多，不至于要打地铺。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/hk2_hu_a505f8b716722f25.webp" srcset="https://blog.joway.io/images/run/hk2_hu_ea2e6352a9b1d324.webp 800w, https://blog.joway.io/images/run/hk2_hu_1b10969fbffa9b1e.webp 1200w, https://blog.joway.io/images/run/hk2_hu_a505f8b716722f25.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;5 月 3 日 8:30，我在机场的莆田吃上了两个月以来，第一顿正儿八经的中餐。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/hk3_hu_21322d44315edb87.webp" srcset="https://blog.joway.io/images/run/hk3_hu_97fbf65fb029656.webp 800w, https://blog.joway.io/images/run/hk3_hu_7aa78e40423b7ead.webp 1200w, https://blog.joway.io/images/run/hk3_hu_21322d44315edb87.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;晚上 8 点，我终于登上了最后一趟飞机。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/leave_hu_c4950bb393a7a8c9.webp" srcset="https://blog.joway.io/images/run/leave_hu_9ea1a9319e10f073.webp 800w, https://blog.joway.io/images/run/leave_hu_b00d0b12817f485e.webp 1200w, https://blog.joway.io/images/run/leave_hu_c4950bb393a7a8c9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;5 月 4 日凌晨，我在目的国落地，我现在彻底地拥有了一切我想要的自由。我可以吃饱饭，我可以去医院，我甚至还可以毫无恐惧的感染新冠。我原本以为我会非常兴奋，毕竟这一路走来确实是有点太苦了，苦到有点超出了 2022 年地球文明允许的底线。但奇怪的是，我却找不到一丝快乐的踪迹。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/dest_hu_f404ae9e373caa43.webp" srcset="https://blog.joway.io/images/run/dest_hu_b80de6d69a8a5ceb.webp 800w, https://blog.joway.io/images/run/dest_hu_d1cff6e063544c1.webp 1200w, https://blog.joway.io/images/run/dest_hu_f404ae9e373caa43.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1201" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;写下这篇文章的时候是 5 月 10 日，我已经自由了一周，吃过了太多好吃的东西，但是我却还是抑制不住的悲伤，所有的快乐对我来说都只是暂时的麻痹，当这些短暂的刺激消失，我还是会回忆起我在家里收拾行李的那一个小时，我为什么会想要流泪，我是在为什么而流泪。&lt;/p&gt;
&lt;p&gt;​ 最后附上一张我在浦东机场附近黑旅馆的自拍照，在上海非法囚禁我的第一天开始我就不再剃胡子，并希望等解封时候带着胡子上街，让世人不要忘记我们被封了多久，不要原谅那些毫无道德底线，法治底线决定封我们的人。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/run/me_hu_cb84cf93a4b4ddc8.webp" srcset="https://blog.joway.io/images/run/me_hu_d46489caa4710bd8.webp 800w, https://blog.joway.io/images/run/me_hu_2b43ce284b6271c8.webp 1200w, https://blog.joway.io/images/run/me_hu_cb84cf93a4b4ddc8.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1153" alt="那一天，我决定踏出一步" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;</content:encoded></item><item><title>True Story</title><link>https://blog.joway.io/posts/true-story/</link><pubDate>Thu, 28 Apr 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/true-story/</guid><category>Thought</category><description>2015 年 9 月 2 日，一位名叫艾兰的叙利亚难民儿童尸体在土耳其的海滩上被发现，并被拍下了一张影响深远的照片。
如果你在网络上尤其是中文网络上查阅这张照片的新闻时，会发现有相当一部分人在拿出各种“证据”试图证明它是摆拍的。
这张照片之所以牵动人心是因为它叙述了一个真实存在的现象是，叙利亚儿童正在因为逃难而失去生命。这个现象是真实存在的，你在这个海滩上站到深夜就能看到这个客观事实，但是具体到这个照片与这个事实是否是百分百匹配的，确实有很多可被质疑的空间，例如这个儿童有可能来自伊拉克，例如摄影师有可能把他换了一个姿势来拍摄。但是这些真的重要吗？或许对于历史学家来说，的确重要，但是对于一个普通民众来说，或许并没有那么重要，至少在此时此刻有比质疑更加重要的事情。</description><content:encoded>&lt;p&gt;2015 年 9 月 2 日，一位名叫艾兰的叙利亚难民儿童尸体在土耳其的海滩上被发现，并被拍下了一张影响深远的照片。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/alan_hu_27be4adff7a184ec.webp" width="349" height="286" alt="True Story" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;如果你在网络上尤其是中文网络上查阅这张照片的新闻时，会发现有相当一部分人在拿出各种“证据”试图证明它是摆拍的。&lt;/p&gt;
&lt;p&gt;这张照片之所以牵动人心是因为它叙述了一个真实存在的现象是，叙利亚儿童正在因为逃难而失去生命。这个现象是真实存在的，你在这个海滩上站到深夜就能看到这个客观事实，但是具体到这个照片与这个事实是否是百分百匹配的，确实有很多可被质疑的空间，例如这个儿童有可能来自伊拉克，例如摄影师有可能把他换了一个姿势来拍摄。但是这些真的重要吗？或许对于历史学家来说，的确重要，但是对于一个普通民众来说，或许并没有那么重要，至少在此时此刻有比质疑更加重要的事情。&lt;/p&gt;
&lt;p&gt;什么是事实，什么是真实？真实并不代表每一件事情的细节都是毫厘不差的事实，这就好比父母口中关于孩子的童年故事多少会有些添油加醋，锦上添花的成分在，但这并不能否定这些故事是真实的。&lt;/p&gt;
&lt;p&gt;相反，由事实组成的故事也不见得一定是真实的故事。在叙利亚战争期间，一定找得到吃的好睡得好的地区，但如果一个驻叙利亚记者去这些地区报道来反映叙利亚战争期间人民的生活，这一定不是真实的报道。&lt;/p&gt;
&lt;p&gt;人类的故事要能够被传播，或多或少总会被掺入一些夸大的成分，即便最开始的当事人只是客观陈述，也防不住他人在传播的过程中进行二次创造，最后舆论市场会自发地选择出一个兼顾了真实与传染力的版本成为我们今天耳熟能详的故事。&lt;/p&gt;
&lt;p&gt;对于那些想要左右人民情感，篡改人民记忆的人来说，事实是他们攻击一切故事的强大武器。民众无法凭借一己之力去探寻真正的事实，只要少数人掌握了调查事实的权利，就掌握了提供唯一合法叙事的权利。他们用事实消灭一切他们所不愿意看到的故事，让这个世界只留下那些由他们控制的事实所构建出来的官方叙事。在这个叙事里，既没有人民的情感，也没有人民的记忆，只有一种强大到淹没所有人的意志。&lt;/p&gt;
&lt;p&gt;这套叙事方式在过去被用在了欧洲难民危机中，用在了香港运动中，用在了美国大选中，用在了上海疫情中。它的强大之处在于，身处其中的人会发现自己无法与这个叙事机器所搏斗，因为它所描述的确实是事实，确实是我们所相信的故事的弱点，确实是我们作为一个平凡个体的缺陷。我们无法让自己，让自己所团结的群体，永远地做到客观，做到公正，做到实事求是，因为我们是人而非机器，而那些试图否定我们的人，只要做到一次客观，做到一次公正，做到一次实事求是，就认为可以全盘否定我们所实现过的一切努力。&lt;/p&gt;
&lt;p&gt;我们身处于一个混乱的世界，Fake News 与 True Story 同时存在，True News 与 Fake Story 同时存在。孟姜女或许并没有哭长城，摩西或许并不能劈开红海。但又或许所有我们所相信的 True Story 并不是由事实产生的结果，而是因为有千千万万痛苦的民众，希望长城倒塌，希望抵达应许之地，所以才诞生的孟姜女，诞生的摩西，他们也许都不是事实，但他们远比事实更为强大。&lt;/p&gt;</content:encoded></item><item><title>少数价值</title><link>https://blog.joway.io/posts/minority-values/</link><pubDate>Tue, 19 Apr 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/minority-values/</guid><category>Thought</category><description>如果正在阅读这篇文章的你认为自己的价值观在这片土地上属于多数价值，被这块土地充分地实现，希望你不要继续阅读下去，也不要将它传播给任何你的朋友。我尊重你的价值观，但恐怕你不一定会尊重我的，为了你好也为了我好，请不要与我有任何交集。
我并不认为自己的价值观一定正确，但是我的价值观，是通过一点一滴的阅读，通过与不同人群的交流，亲眼所见亲耳所听构建出来的。我对它的自信源自于它一次次在苏格拉底，在耶稣，在马丁路德金，在苏轼，在鲁迅，在我身边所有优秀的朋友身上一次次被验证被歌颂，它不见得一定是全人类共同拥有和应该拥有的，但它表达了那部分我愿意与其共处的人类千百年来共同的价值诉求。</description><content:encoded>&lt;p&gt;如果正在阅读这篇文章的你认为自己的价值观在这片土地上属于多数价值，被这块土地充分地实现，希望你不要继续阅读下去，也不要将它传播给任何你的朋友。我尊重你的价值观，但恐怕你不一定会尊重我的，为了你好也为了我好，请不要与我有任何交集。&lt;/p&gt;
&lt;p&gt;我并不认为自己的价值观一定正确，但是我的价值观，是通过一点一滴的阅读，通过与不同人群的交流，亲眼所见亲耳所听构建出来的。我对它的自信源自于它一次次在苏格拉底，在耶稣，在马丁路德金，在苏轼，在鲁迅，在我身边所有优秀的朋友身上一次次被验证被歌颂，它不见得一定是全人类共同拥有和应该拥有的，但它表达了那部分我愿意与其共处的人类千百年来共同的价值诉求。&lt;/p&gt;
&lt;p&gt;我 1995 年出生，所经历的教育完全是经过中国人民共和国教育部批准，合法合规，符合社会主义价值观的中国式教育，在我整段学生生涯中，我一直认为我的价值观是这个社会里的主流价值观，也是这个教育系统希望我拥有的价值观。在我受教育的年代，我们曾经和国家共同相信民主是未来的发展方向，共同相信侵略是反人类行为，共同相信全球化是历史的潮流，共同相信恐怖主义是要被谴责的行为，共同相信市场经济带给了中国空前的繁荣。当时的我们虽不认为我们国家已经实现了这些价值，但是绝大部分人包括国家机器都认同我们确实享有这样的价值观，而争歧无非是如何实现这些价值观，何时应当去按这些价值观践行。&lt;/p&gt;
&lt;p&gt;但是不知道从什么时候开始，这些原本我们认为的多数价值，渐渐地变成了社会中的少数价值。虽然我们在社会主义核心价值观中明确写了民主与自由，但却在每天批判西方的民主与西方的自由。虽然我们把八年抗日战争延长到了十四年，但却破天荒地认为只要从自身利益出发，哪怕是侵略也是可以被合法化甚至共情的。虽然我们今天的经济成就完全仰赖全球化与市场经济，却可以每天大肆与几乎所有西方国家对骂并强力干预市场经济。&lt;/p&gt;
&lt;p&gt;这些变化并不是由某一个人，或者某八千万人造成的，而是在十四亿人包括我自己共同的努力下，车轮渐渐驶到了如今这步田地。&lt;/p&gt;
&lt;p&gt;我无法解释为什么我们会走到今天，但我确信其中肯定有我自己的一份力。我曾经认为自我实现在于努力赚钱买房成为大城市中产阶级，我曾经认为只要经济持续前进上层建筑会自然而然变好，我曾经认为多元化的世界确实应该对一些共同价值的定义有各自表述的权力，我曾经认为下一代人会比我们更加推崇自由与民主看到更大更不一样的世界，我曾经认为一个勤劳的民族辅之以不断修正的制度迟早会重回当年历史上的地位。但是直到走到今天这一步，我才知道自己错了，大错特错。&lt;/p&gt;
&lt;p&gt;比认识到错误更加悲哀的是，这段变化正是发生在我的青年时期，发生在我最应该意识到问题，最应该去解决问题至少把问题说出来的年纪。让悲哀更加悲哀的是，我同与我共享相同价值观的人，是亲眼目睹一个个违背我们价值观的事件日复一日发生，而我们选择了冷眼旁观或是热眼嘲笑。直到今天，我们的价值观甚至成为了不可言说的价值观。&lt;/p&gt;
&lt;p&gt;我不再愿意和任何人辩论，甚至不再愿意接受不同的观点。如果苏格拉底是错的，如果鲁迅是错的，如果启蒙运动是错的，如果独立宣言是错的，那我愿意带着这些古老的错误直到死去，也不愿意再去用新的理论，新的叙事，重新学习新时代的多数价值。如果他们是对的，如果他们会笼罩我一生，我也认了，我宁愿一生成为一个碌碌无为的错误，也不愿再推翻我过去的信仰成为一个前途光明的正确。&lt;/p&gt;
&lt;p&gt;我失去了语言，也失去了使用语言的勇气，只能在评论区给我的同类点赞，在黑色幽默里寻找光明的踪迹，在一篇篇即将被删除的文章里获取慰藉。赛博空间里充斥着正确，让错误无处可逃，只有远处时不时传来的一声声尖叫。&lt;/p&gt;</content:encoded></item><item><title>A Snapshot of Myself - 2021</title><link>https://blog.joway.io/posts/2021/</link><pubDate>Fri, 31 Dec 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2021/</guid><category>Thought</category><description>过去并没有写年终总结的习惯，但是如今也正式奔三了，记忆力一年不如一年，时常忘记自己过去做过什么，有过什么想法。前段时间和很久不见的老朋友见面，意外地提起我 N 年前说过的一句话，而我却完全不记得自己当时竟有过这样的想法。恍忽间才意识到，即便彼时彼刻的我已然在我自己心中消失殆尽，却意外地变成了一段段碎片，分散存储在过去朋友的记忆中，成为一个个 Snapshots，成为了彼时彼刻的那个我存在过的证据。</description><content:encoded>&lt;p&gt;过去并没有写年终总结的习惯，但是如今也正式奔三了，记忆力一年不如一年，时常忘记自己过去做过什么，有过什么想法。前段时间和很久不见的老朋友见面，意外地提起我 N 年前说过的一句话，而我却完全不记得自己当时竟有过这样的想法。恍忽间才意识到，即便彼时彼刻的我已然在我自己心中消失殆尽，却意外地变成了一段段碎片，分散存储在过去朋友的记忆中，成为一个个 Snapshots，成为了彼时彼刻的那个我存在过的证据。&lt;/p&gt;
&lt;p&gt;我觉得人是一个动态的过程，过往人们在评价他人亦或是自己时常常基于某一个固定时刻下的最终状态，即所谓的盖棺定论，而忽视了整段路程中状态的变更，我觉得其实蛮可惜的。就好似一个股票，你不能只看价格不看曲线。所以我觉得与其利用年终的契机总结一年内静态的收获，不如每一年对自己当下的状态做一个主动式和集中化的 Snapshot，以便未来能够追溯自己的变更历史，更好理解自己是谁，自己又是过谁。&lt;/p&gt;
&lt;p&gt;如果在当下要选择一个关键词来定义我此刻的状态，我会选择用「理解」。从大学到工作这段旅程对我而言是一个从无知到略有所闻的过程，我所获得的积累还远达不到能够对什么事情去下判断的程度，所以只能去先假设这个世界是合理的，然后尝试去理解其合理性。并且时刻保持着接受自己理解错误的开放心态。&lt;/p&gt;
&lt;h2 id="理解周期"&gt;理解周期&lt;/h2&gt;
&lt;p&gt;今年是我大学毕业的第四年，加上正式和非正式的两年实习，真正参与到与现实世界的交互也有6年了。很幸运的是，在整个移动互联网浪潮中，我多少也算是参与了大半程。&lt;/p&gt;
&lt;p&gt;我现在还记得大学时候那种热火朝天的创业气氛，几乎每个月都有不同城市甚至公司在举办 Hackathon，而且都提供不菲的奖金。各路 App 百花齐放，而且各自都能融到不少钱，甚至会觉得所有金钱、政策都在求着你创业。到我毕业的 18 年时，从 0 开始创业的少了许多，但是加入一个已经初有成色的创业公司还是一个流行的风气，而且似乎在经济上也不见得会有什么损失甚至大家都幻想着获得超额回报。而到了 19-20 年，开始有了一种车门已被焊死的感觉，整个移动互联网的坑位也都被占满且很难也不太有资金支持你去产生什么变化。这时候才会意识到，好像没上到这班车。而且由于上市潮，这个时候加入创业公司和既成的大公司之间的经济收益差距也开始被拉开了。&lt;/p&gt;
&lt;p&gt;之前和同事聊的时候谈到，大家都没有意识到这个过程会如此之快，几乎只覆盖了一个工程师的一到两次跳槽周期而已。&lt;/p&gt;
&lt;p&gt;虽然没有在这个周期上获得什么经济上大收益，但是能够在毕业时就经历一个完整的周期也算是为参与下一个周期积累了一些筹码。不至于在周期到来时过度喜悦，也不至于在周期消逝时过度悲观。&lt;/p&gt;
&lt;h2 id="理解投资"&gt;理解投资&lt;/h2&gt;
&lt;p&gt;过去几年系统性地学习了经济学基础原理，并且做了一些理财投资的实践。前段时间在梳理资产清单的时候发现一个问题，我持有的资产其实是很难计算成本和收益的。&lt;/p&gt;
&lt;p&gt;例如我分三次买入三股 Apple 股票，假设每次买入价格都是一样的，但是时间不同所以美元兑人民币汇率不同，分别为 6，7，8。如果我以美元计价，那么我成本价就没变，以人民币计价成本价就变化了。由于我工资收入是人民币，所以我从切身感受来说，当然是觉得持有 Apple 股票的成本变高了。因为要需要付出更多劳动时间才能换取一股 Apple 股票。但是如果此时我涨工资了，我不可能去认为是 Apple 股票跌了，但是事实上我的确可以用更少劳动时间换取了对应资产了。&lt;/p&gt;
&lt;p&gt;所以归根结底，这个游戏你如果要讨论赚了还是亏了，你必须锚定一个最终定价物，否则这就没法讨论。而人类只有一个共同的定价物就是自己的时间。&lt;/p&gt;
&lt;p&gt;我所理解的投资就是付出自己的时间去换取某些资产，让这些资产产生超额价值，以便未来有一天我能够用他们来购买我自己的自由时间，部分再用来提升生活的物质品质。&lt;/p&gt;
&lt;p&gt;但这里有一个道德问题我至今还未想明白，这种行为是不是本质上就是在剥削他人，除非这部份剥削未来能够完全被转移到机器上。&lt;/p&gt;
&lt;h2 id="理解复利"&gt;理解复利&lt;/h2&gt;
&lt;p&gt;前段时间思考过买车这种大件，由此引申出一个疑问，如果我在当下花了 20 万买车，我是真的只花了 20 万吗？如果我把这笔钱投入 10% 年化收益的基金，那么 10 年后就是 50 万，所以我现在是花了 10 年后的 50 万买了一辆车吗？如果再把时间拉长，就会发现这里涉及到一个问题，我该如何在整个人生维度去分配消费，毕竟我不希望带着金钱入土，但是我的人生长度却又是我自己无法知晓和把握的。所以复利在引入了时间后，收益和风险并存。而金钱的消费其实是在实现收益以降低长期的风险。&lt;/p&gt;
&lt;p&gt;上述只是单纯从金钱角度去理解的复利，另外我还发现知识也是有复利的。如果某个领域了解地足够多以后，后面学习新的知识的效率也会递增，甚至跨领域间的知识还能有互相促进的作用，有点像是细胞的裂变。&lt;/p&gt;
&lt;h2 id="理解自我"&gt;理解自我&lt;/h2&gt;
&lt;p&gt;理解自己不是一件容易的事情。&lt;/p&gt;
&lt;p&gt;首先在获取关于自我的信息上就很困难，最易得的信息是他人的评价，但这里可能有一部分来源于恶意，即便是善意的部分，也未必是经过深思熟虑的。在公司内被 Peer Review 的时候经常会收到一些别人认为的我的优点和缺点，但有些人给我写的缺点其实就是优点的负面表述形式。所谓的成长，很多时候就是把优点和缺点同时磨平，然后回顾的时候只说我改正了什么缺点却忽视了这个「改正」的成本。而要让自己能够客观反映出自身的存在又是一件充满了悖论的事情 —— 客观的名词解释指的就是不依赖主观意识而存在。&lt;/p&gt;
&lt;p&gt;其次在对现有信息去分析的时候又是一件充满痛苦，甚至会觉得难为情的事情。我时常发现原来自己比想象中更加爱钱，更加卑鄙，更加不诚信。而一想到在他人心中或许我并不是这样，或者我曾经营造过不是这样的人设，这就让我有点不好意思了。&lt;/p&gt;
&lt;p&gt;但如果退一步，把理解自我的过程当作是理解人类，心情或许会变得好受许多。这个过程也有助于同理心的建立。&lt;/p&gt;
&lt;p&gt;我现在几乎完全能够用同理心去理解王力宏，吴亦凡，甚至希特勒这些人的内心感受，理解并不意味着认同，但是而如果不理解一定很难真正做出有价值的反对。这就像是修电脑，不理解电脑是如何运作的就不可能知道电脑坏在了哪里，甚至有时候也不见得真的是电脑坏了。&lt;/p&gt;
&lt;h2 id="理解选择"&gt;理解选择&lt;/h2&gt;
&lt;p&gt;现在回想起来我最早做职业选择的时候都觉得有点幼稚的可笑，我甚至会把老板的政治立场作为选择一份工程师职业的权重因素 —— 仅仅就因为我不希望和老板聊不来。直到走了一些弯路后我才开始理解，选择之所以叫做选择，就是因为不能对一个单一的选择抱有过多不切实际的期望。之前有看过一个关于解释工作意义的图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/work_hu_3b7154dd5a5b736a.webp" srcset="https://blog.joway.io/images/work_hu_745e1fc6d8f2a515.webp 800w, https://blog.joway.io/images/work_hu_3b7154dd5a5b736a.webp 1060w" sizes="(max-width: 800px) 100vw, 800px" width="1060" height="1006" alt="A Snapshot of Myself - 2021" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;但这个图没有说明的是，其实人并不是同时只能拥有一份「工作」。事实上完全可以通过多个选择相组合来达到一个多样化的期望，每一个「选择」只负责并负责好个别期望，这有点像是 Unix 哲学。&lt;/p&gt;
&lt;h2 id="理解效率"&gt;理解效率&lt;/h2&gt;
&lt;p&gt;我在参与工作的前几年非常痴迷于解决问题的速度。例如现在我需要用到某个组件，我会以最快速度去搜网上的快速入门文档，然后把它跑起来，测试下来功能没问题，就认为把这个事情做完了。大部分时候的确也能够工作地很好。&lt;/p&gt;
&lt;p&gt;但是后来我发现了于公于私，这种工作方式其实都是有问题的。&lt;/p&gt;
&lt;p&gt;于公来说，现代企业是协作型组织，快糙猛的做法往往会给其它协作者留下巨大的坑，团队需要的其实是高质量的标准化工作，当下的快并不意味着长期的快。&lt;/p&gt;
&lt;p&gt;于私来说，公司并不会因为你的工作效率高而额外地付你薪水，反而你有可能会因此承担更多工作量。你相当于在承担了更大工作量的同时还降低了自己的工作质量，而且在快速工作的过程中，你主动放弃了去深入学习的机会。&lt;/p&gt;
&lt;p&gt;我现在理解的工作效率不是去节约步骤创建速度，而是去思考，抽象，建立步骤以创建加速度。&lt;/p&gt;
&lt;h2 id="理解多样性"&gt;理解多样性&lt;/h2&gt;
&lt;p&gt;过去几年游历了欧洲，中东，亚洲十多个国家，我的旅行一般分为三个过程：了解基础背景 - 实地体验，产生疑问 - 深度学习以解答疑惑。这些积累的知识帮助我构建了一个全球视角的同理心，尤其是如今肉眼可见的民族主义情绪下，更是一种对自我价值观的保护罩。&lt;/p&gt;
&lt;p&gt;前段时间了解到新加坡有一个强制民族混居的政策，通过让不同民族的人之间互相面对面接触，以消除民族间的隔阂。很多东西只有亲眼见过后才知道事实是如何的，语言总是容易将一个个具体的事物抽象化为笼统的概念，为了这些概念争斗实在是没有必要。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;如今的世界充满了不确定性，很多过去预设为真理的假设或许在未来也都会不再成立，甚至我连自己未来会在何处都没有确定性的答案。只能试图在一个易失自我存在的环境中，试图做出一点持久化当下存在的努力。&lt;/p&gt;
&lt;p&gt;滕王阁序里说的，「关山难越，谁悲失路之人；萍水相逢，尽是他乡之客」，今天晚上的上海也不过是一个大点的滕王阁罢了。&lt;/p&gt;</content:encoded></item><item><title>RPC 漫谈： 连接问题</title><link>https://blog.joway.io/posts/deep-into-rpc-connection/</link><pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-connection/</guid><category>Tech</category><description>什么是连接 在物理世界并不存在连接这么一说，数据转换为光/电信号后，从一台机器发往另一台机器，中间设备通过信号解析出目的信息来确定如何转发包。我们日常所谓的「连接」纯粹是一个人为抽象的概念，目的是将传输进来的无状态数据通过某个固定字段作为标识，分类为不同有状态会话，从而方便在传输层去实现一些依赖状态的事情。</description><content:encoded>&lt;h3 id="什么是连接"&gt;什么是连接&lt;/h3&gt;
&lt;p&gt;在物理世界并不存在连接这么一说，数据转换为光/电信号后，从一台机器发往另一台机器，中间设备通过信号解析出目的信息来确定如何转发包。我们日常所谓的「连接」纯粹是一个人为抽象的概念，目的是将传输进来的无状态数据通过某个固定字段作为标识，分类为不同有状态会话，从而方便在传输层去实现一些依赖状态的事情。&lt;/p&gt;
&lt;p&gt;以 TCP 为例，一开始的三次握手用来在双方确认一个初始序列号（Initial Sequence Numbers，ISN），这个 ISN 标志了一个 TCP 会话，并且这个会话有一个独占的五元组（源 IP 地址，源端口，目的 IP 地址，目的端口，传输层协议）。在物理意义上，一个 TCP 会话等价于通往某一个服务器的相对固定路线（即固定的中间物理设备集合），正是由于这样，我们去针对每个 TCP 会话进行有状态的拥塞控制等操作才是有意义的。&lt;/p&gt;
&lt;h3 id="连接的开销"&gt;连接的开销&lt;/h3&gt;
&lt;p&gt;我们常常听到运维会说某台机器连接太多所以出现了服务抖动，大多数时候我们会接受这个说法然后去尝试降低连接数。然而我们很少去思考一个问题，在一个服务连接数过多的时候，机器上的 CPU，内存，网卡往往都有大量的空余资源，为什么还会抖动？维护一个连接的具体开销是哪些？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存开销：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TCP 协议栈一般由操作系统实现，因为连接是有状态对，所以操作系统需要在内存中保存这个会话信息，这个内存开销每个连接大概 4kb 不到。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件描述符占用：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Linux 视角中，每个连接都是一个文件，都会占用一个文件描述符。文件描述符所占用的内存已经计算在上面的内存开销中，但操作系统为了保护其自身的稳定性和安全性，会限制整个系统内以及每个进程中可被同时打开的最大文件描述符数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 机器配置: Linux 1 核 1 GB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /proc/sys/fs/file-max
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;97292&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="nb"&gt;ulimit&lt;/span&gt; -n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;1024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面的设置表示整个操作系统最多能同时打开 97292 个文件，每个进程最多同时打开 1024 个文件。&lt;/p&gt;
&lt;p&gt;严格来说文件描述符根本算不上是一个资源，真正的资源是内存。如果你有明确的需要，完全可以通过设置一个极大值，让所有应用绕开这个限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程开销：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有一些较老的 Server 实现采用的还是为每个连接独占（新建或从连接池中获取）一个线程提供服务的方式，对于这类服务来说，除了连接本身占用的外，还有线程的固定内存开销：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 机器配置: Linux 1 核 1 GB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 操作系统最大线程数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /proc/sys/kernel/threads-max
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;7619&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 操作系统单进程最大线程数，undef 表示未限制&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat /usr/include/bits/local_lim.h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/* We have no predefined limit on the number of threads. */
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#undef PTHREAD_THREADS_MAX&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 单个线程栈默认大小，单位为 KB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="nb"&gt;ulimit&lt;/span&gt; -s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;8192&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在上面这台机器里，允许创建的线程数一方面受操作系统自身设定值限制，一方面也受内存大小限制。由于 1024MB / 8MB = 128 &amp;gt; 7619 ， 所以这台机器中能够创建的最大线程数为 128。如果 Server 采用一个线程一个连接，那么这时 Server 同时最多也只能够为 128 个连接提供服务。&lt;/p&gt;
&lt;p&gt;可以看出这种单连接单线程的模式会导致连接数大大地被线程数所制约，所以现代 Server 实现大多抛弃了这种模式，让单一线程专门处理连接。&lt;/p&gt;
&lt;h3 id="c10k-问题"&gt;C10K 问题&lt;/h3&gt;
&lt;p&gt;通过上面的讨论，我们能够看到，真正制约了连接数量的，本质还是内存资源。其他变量要么可以通过修改默认参数绕开，要么可以通过更改软件设计而优化。但如果事实真的如此简单，为什么还会有著名的 &lt;a href="https://en.wikipedia.org/wiki/C10k_problem"&gt;C10K&lt;/a&gt; 问题呢？&lt;/p&gt;
&lt;p&gt;这其实纯粹是一个软件工程的问题，而非硬件问题。早期操作系统设计时，就没有考虑到未来会出现单机 10K 连接甚至更多的问题，所以在其接口上并未对这类场景进行优化，而在此之上的基础设施软件（例如 Apache）自然而然也就没有考虑到应对这类场景。&lt;/p&gt;
&lt;p&gt;对于应用软件开发者而言，操作系统就如同法律，我们能够做什么并不全凭物理世界可以做什么，还要遵从操作系统允许我们做什么。&lt;/p&gt;
&lt;p&gt;需要注意的是 C10K 问题指的是连接数，而非请求数。如果是 10K 的 QPS(Query per Second) 在一条连接上也能够进行，这也是为什么对于企业内网中的 RPC 调用而言，一般也不会出现 C10K 的问题。C10K 问题往往出现在诸如推送服务，IM 服务这类需要和海量客户端建立持久连接的场景。&lt;/p&gt;
&lt;p&gt;在 Linux 语境下，连接被抽象为文件，所以 C10K 问题的关键在于 Linux 中所提供的 IO 接口设计是否可以应对大规模连接的场景，以及我们如何使用这些接口去实现能够支持高并发连接的软件架构。&lt;/p&gt;
&lt;h4 id="linux-io-的历史演变"&gt;Linux IO 的历史演变&lt;/h4&gt;
&lt;p&gt;如果我们要一次处理多个连接（即多个文件描述符），那么必然需要操作系统能够提供给我们一个批量监听函数，让我们能够同时去监听多个文件描述符，并且通过返回值告知我们哪些文件已经可读/可写，然后我们才能去真正操作这些已经就绪的文件描述符。&lt;/p&gt;
&lt;p&gt;Linux IO 的根本性工作无非就是上面这些，看上去并不是什么复杂的设计，但是这部分工作在历史上的不同实现方式却深刻影响了后来的应用软件发展，也是很多基础软件之间核心区别所在。&lt;/p&gt;
&lt;h5 id="select-1993"&gt;select, 1993&lt;/h5&gt;
&lt;p&gt;select 的函数签名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define __FD_SETSIZE 1024
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;__fd_mask&lt;/span&gt; &lt;span class="n"&gt;fds_bits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;__FD_SETSIZE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;__NFDBITS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;select&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;writefds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;exceptfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;timeval&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用方式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 初始化文件描述符数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;FD_ZERO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// socket1, socket2 连接注册进 readfds
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_sockfd_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_sockfd_2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 循环监听 readfds
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 返回就绪描述符的数目
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;available_fd_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 遍历当前所有文件描述符
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 检查是否可读
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FD_ISSET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 从 fd 读取
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;select 函数在大规模连接时的缺陷主要在以下两方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线性遍历所有文件描述符，O(N) 复杂度&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;select 函数本身并不返回具体哪些文件描述符就绪，需要用户自己去遍历所有文件描述符，并通过 FD_ISSET 来判断。连接少时影响并不大，但当连接数达到 10K，这个 O(N) 复杂度造成的浪费也会非常夸张。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fd_set 大小限制&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;fd_set 结构背后是一个位图，每一位代表一个文件描述符但就绪状态，1 代表就绪。Linux 默认 FD_SETSIZE 为 1024，也就是实际大小为 1024/8bits = 128 bytes。并且这部分内存最后会被拷贝到内核态，也会造成拷贝上的开销。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种设计看起来粗糙，但是好处是可以很好应对排队问题。如果某个连接特别繁忙，也不会影响这个系统调用本身的性能，因为它只关心是否就绪，不关心具体有多少数据待处理。在 2000 年 Linus 的 &lt;a href="https://lkml.iu.edu/hypermail/linux/kernel/0010.3/0003.html"&gt;邮件&lt;/a&gt; 中可以看到更多相关讨论。&lt;/p&gt;
&lt;h5 id="poll-1998"&gt;poll, 1998&lt;/h5&gt;
&lt;p&gt;poll 函数的签名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;pollfd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* File descriptor to poll. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* Types of events poller cares about. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;revents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* Types of events that actually occurred. */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;pollfd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nfds_t&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用方式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 初始化 pollfd 数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;pollfd&lt;/span&gt; &lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fds_size&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STDIN_FILENO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STDOUT_FILENO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POLLOUT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 监听 pollfd 数组内的文件描述符
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 遍历 fds
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 是否是读取事件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;revents&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;POLLIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 从 fd 读取
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;poll 相比 select 的差异主要有两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 pollfd 统一 readfds, writefds, exceptfds 三种事件类型。&lt;/li&gt;
&lt;li&gt;通过 pollfd[] 数组传递需要监听的文件描述符，不再限制文件描述符数量。（内核将数组转换为链表）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但在 poll 发明的 1998 年，大规模网络基础设施依然不是一个普遍的需求，所以这次 API 增加并没有解决前面说的在大规模连接下，需要遍历所有文件描述符的问题。但就在 poll 发布后的 1999 年发生了几件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;C10K 问题被正式提出&lt;/li&gt;
&lt;li&gt;HTTP 1.1 发布，在这一版本引入了 keep alive 的持久连接概念&lt;/li&gt;
&lt;li&gt;QQ 发布&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;并且在 00 年前后 2C 互联网引来了大爆发和大泡沫的时代。&lt;/p&gt;
&lt;p&gt;在 QQ 发布的年代，这个问题是无解的，这也是为什么像 QQ 这种面向会话而生的应用，却抛弃了面向会话的 TCP 协议，而使用的是 UDP。&lt;/p&gt;
&lt;h5 id="epoll-2003"&gt;epoll, 2003&lt;/h5&gt;
&lt;p&gt;epoll 函数签名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="nc"&gt;epoll_data&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;epoll_data_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;epoll_event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* Epoll events */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;epoll_data_t&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* User data variable */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxevents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用方式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define MAX_EVENTS 10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;listen_sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epollfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 创建 epollfd 对象，后续 epoll 操作都围绕该对象
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epollfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 对 ev 绑定关心对 EPOLLIN 事件，并注册进 epollfd 中
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listen_sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epollfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listen_sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;epoll_ctl: listen_sock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 传入 events 空数组，阻塞等待直到一有就绪事件便返回，返回值为有效事件数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nfds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epollfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nfds&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;epoll_pwait&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 只需要遍历有效事件即可
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;listen_sock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;//处理文件描述符，read/write
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;do_use_fd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;//主监听socket有新连接
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listen_sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;sockaddr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;addrlen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_sock&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;accept&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;setnonblocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_sock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;//将新连接注册到 epollfd 中，并以边缘触发方式监听读事件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;EPOLLET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn_sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epollfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;epoll_ctl: conn_sock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;epoll 有以下几个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个文件描述符，只会在创建时，被 epoll_ctl 拷贝一次。&lt;/li&gt;
&lt;li&gt;epoll_wait 只有大小有限的参数，从而避免了频繁进行用户态到内核态的拷贝。&lt;/li&gt;
&lt;li&gt;epoll_wait 只返回就绪状态的文件描述符，避免了对所有文件描述符去遍历。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，当连接数线性增多时，epoll 调用本身的性能并不会线性增大。现代 Server 在 Linux 平台下大多已经转向使用 epoll 来实现。&lt;/p&gt;
&lt;h4 id="高并发服务设计"&gt;高并发服务设计&lt;/h4&gt;
&lt;p&gt;我们可以把 C10K 问题拆解成以下三个子问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如何高效地&lt;strong&gt;建立&lt;/strong&gt;大量连接 (accept)&lt;/li&gt;
&lt;li&gt;如果高效地&lt;strong&gt;读写&lt;/strong&gt;大量连接（read/write）&lt;/li&gt;
&lt;li&gt;如何高效地&lt;strong&gt;处理&lt;/strong&gt;大量请求&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三个子问题的区别是，建立连接只会在一开始占用 CPU，连接建立完成后，只会占用内存资源，并且每次建立连接所消耗的资源都是固定的。但每个连接上的读写操作以及对请求数据的处理却会频繁消耗不可预计的 CPU 与 内存资源，且连接间和请求间的差异会非常大。&lt;/p&gt;
&lt;h5 id="如何高效建立连接"&gt;如何高效建立连接&lt;/h5&gt;
&lt;p&gt;由于建立连接消耗的资源是固定的，假设需要 x ms，如果我们用一个单线程，只负责监听 listen port 文件描述符，创建新连接但不负责对其进行读写。那么该线程每秒能够创建的连接数应该是 1000/x 。&lt;/p&gt;
&lt;p&gt;一般来说，创建连接本身的消耗非常少，单线程足以应对 10K 甚至更高的并发。&lt;/p&gt;
&lt;h5 id="如何高效读写连接"&gt;如何高效读写连接&lt;/h5&gt;
&lt;p&gt;我们通过前一个步骤已经确保服务能够高效建立新连接，而对于这些新连接的读写任务工作量我们实现并无法准确估算，所以需要有一个线程池专门去使用 epoll_wait 批量监听连接事件，并进行真正的读写操作。但这里的读写操作涉及到 epoll 的两种通知模式 ———— &lt;strong&gt;水平触发&lt;/strong&gt;和&lt;strong&gt;边缘触发&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于 select/poll 而言，获取的都是文件描述符就绪的列表，每次调用只会去检查是否可读/可写的状态，拿到可用描述符后，进行读写，然后再进行下一次 select/poll。如果没有读完，下一次调用 select/poll 时，还会继续返回可读状态，只要继续去读就没问题。如果没有写完，当下一次回到可写时状态时，可以继续写。epoll 同样也有这类模式，我们将这种处理称之为&lt;strong&gt;水平触发&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;与水平触发对应的是边缘触发，他们的命名来自电平的概念：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Level Triggered:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ......
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;________| |_________
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Edge Triggered:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ____
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;________.| |_________
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// &amp;#34;.&amp;#34; 表示触发通知
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;边缘触发&lt;/strong&gt;只有在从无数据到有数据时通知一次，后续调用都返回 False。所以当收到通知后，必须去一次性读完文件所有内容。而如果某个连接极端繁忙，这个时候就会出现饥饿现象。但这类饥饿现象和 epoll 或是边缘触发关系都不大，纯粹是我们在代码实现时，需要多考虑到均衡读写的情况。如果在水平触发的模式下，也总是去尝试一次性读完所有内容，依然会存在饥饿现象。&lt;/p&gt;
&lt;p&gt;对于绝大多数小体积消息而言，无论哪种触发方式都能够快速读完消息，差别不大。但对于大体积的消息，诸如视频这种，水平触发的方式会导致 epoll_wait 频繁被唤醒，相比于边缘触发会多很多次系统调用，所以性能会更差。&lt;/p&gt;
&lt;h5 id="如何高效处理请求"&gt;如何高效处理请求&lt;/h5&gt;
&lt;p&gt;对于绝大部分业务来说，业务逻辑的处理才是真正耗费资源的操作，因此我们不能将这部分操作放进 IO 线程内进行，否则会影响到这个线程中监听的其他连接。所以需要单独再开辟一个工作线程池去处理业务逻辑本身。&lt;/p&gt;
&lt;p&gt;归根结底，对于消耗少且固定的任务，可以使用单线程。对于消耗不确定的任务，需要使用线程池。&lt;/p&gt;
&lt;h5 id="最终架构"&gt;最终架构&lt;/h5&gt;
&lt;p&gt;通过上面一系列任务拆分，我们可以得到一个业内称之为主从 Reactor 的支持高并发的服务模型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 单线程 线程池 线程池
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[ Main Reactor ] == 新连接 ==&amp;gt; [ Sub Reactor ] &amp;lt;-- Data --&amp;gt; [ Worker Thread ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 建立新连接 读写连接 业务逻辑处理
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个模型也是 Java 的 Netty 框架，和 Go 的 &lt;a href="https://strikefreedom.top/go-event-loop-networking-library-gnet"&gt;gnet&lt;/a&gt; 框架的实现基础。&lt;/p&gt;
&lt;p&gt;互联网业务有红利，基础设施软件的变革也有红利。Epoll 就是上个 10 年最大的一个红利之一。&lt;/p&gt;
&lt;h3 id="连接池-vs-多路复用"&gt;连接池 VS 多路复用&lt;/h3&gt;
&lt;p&gt;对于连接的管理有两种基本思想：&lt;strong&gt;连接池&lt;/strong&gt;和&lt;strong&gt;多路复用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;连接是请求的传输载体，一个请求包含一来一回，即一个 RTT 时间。连接池一般是指每个连接同时只为一个请求提供服务，也就是一个 RTT 内只会存在一个请求，此时如果存在大量并发请求必须使用一个&lt;strong&gt;连接池&lt;/strong&gt;来管理生命周期。但连接本身是全双工的，完全可以一直发送 Request，一直发送 Response，这也是&lt;strong&gt;多路复用&lt;/strong&gt;的含义，但这需要应用层协议支持对给每个包标定一个 ID 来用以分割不同请求和响应。&lt;/p&gt;
&lt;p&gt;HTTP 1.0 协议就是因为并未对每个请求标定 ID，所以在实现上不可能支持多路复用。而在 HTTP 1.1 协议中，增加了 Keep-Alive 连接保活以及 Pipeline 的概念，请求可以不停发，但是 Response 返回的顺序必须是发送时的顺序，以此来尽可能复用连接。但是 Pipeline 对 Response 顺序的要求会导致如果某个请求处理时间比较长，那么后面的返回会一直堆积。1.1 因为是 1.0 的修订版，所以不太可能增加太多不兼容的变更。但在 HTTP 2.0 中，就增加了 stream ID 来实现多路复用的能力。&lt;/p&gt;
&lt;p&gt;Thrift 协议也会在开头(8~12 bytes)标明自己的序列号 ID，所以也能很好支持多路复用。&lt;/p&gt;
&lt;p&gt;但是主流数据库协议如 Mysql 却大多都不支持连接的多路复用，这也是为什么我们经常会需要去配置数据库连接池的原因。数据库的大部分时间消耗在磁盘 IO 和 CPU 计算，使用连接池的方式可以保证有限度的并发请求量，一个接一个把活干完，如果出现繁忙状况，请求主要被阻塞在 Client 端。如果是多路复用的方式，Client 并没有办法准确估计出 Server 的负载能力，大量请求依旧会被发出去，阻塞在数据库侧，这个往往我们是不想看到的。另外，连接池的方式对于 Client 的实现来说往往也更加容易。&lt;/p&gt;
&lt;h3 id="server-push"&gt;Server Push&lt;/h3&gt;
&lt;p&gt;我们都知道连接本身是全双工的，Client 和 Server 之间想怎么发消息就怎么发，何况一个请求的返回消息本身就是一个字面意义的 Server Push。那为什么 HTTP 2.0 以及一些 RPC 协议还要去标榜自己支持 Server Push ？&lt;/p&gt;
&lt;p&gt;这个问题本身又是一个软件工程的问题。我们已经习惯了用输入输出的模式去编程，所以在协议设计的时候，很少考虑到输入没输出，输出没输入的这种情况。Server Push 就属于一种输出没输入的情况。&lt;/p&gt;
&lt;p&gt;在 HTTP 1.1 里，一个完整的消息应该如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="n"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;curl&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;7.16&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;libcurl&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;7.16&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="n"&gt;zlib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt; &lt;span class="n"&gt;Jul&lt;/span&gt; &lt;span class="mi"&gt;2009&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt; &lt;span class="n"&gt;GMT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Apache&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Modified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="n"&gt;Jul&lt;/span&gt; &lt;span class="mi"&gt;2009&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt; &lt;span class="n"&gt;GMT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ETag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;34aa387-d-1568eb00&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Vary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Hello&lt;/span&gt; &lt;span class="ne"&gt;World&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;My&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;includes&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;trailing&lt;/span&gt; &lt;span class="n"&gt;CRLF&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个 Response 必须对应一个 Request，从代码侧来说，就是一个函数调用。试想如果此时 Server 在连接中，莫名其妙返回了一个客户端没有请求的 Response，代码实现上能怎么做？代码根本没调用，自然也就没地方在监听等待这个 Response ，自然没地方会去处理它。&lt;/p&gt;
&lt;p&gt;后人在 HTTP 1.1 上所实现的所谓的长轮询，无非是利用长连接的特性，延迟发送了消息。与其说是什么新技术，不是说是投机取巧，但是的确在简单的场景下是一个好用的方案。要彻底解决问题必须修改协议，这也是为什么会有 WebSocket 和 HTTP 2.0 的原因。&lt;/p&gt;
&lt;p&gt;在 HTTP 2.0 中，通过标记特殊的帧 PUSH_PROMISE 来表示这个消息是没有对应 Request 的，但具体实现的时候会发现，就算 Server 能够 Push 内容给 Client，Client 也需要去解析不同消息具体含义是什么。在传统模式下，一个 Response 的含义由 Request 决定，但现在一个没有 Request 的 Response 只能通过解析其内容决定。这就导致了实现这个解析的过程其实是可以各家自己定义自己的。对于浏览器标准而言，Server Push 一般用于静态资源，所以就需要建立一套资源缓存的标准。&lt;/p&gt;
&lt;p&gt;而在 grpc 中，虽然底层用的是 HTTP 2.0 ，但并没有使用 PUSH_PROMISE 功能，就是因为对于 RPC 而言，我可以一个请求有多个返回（所谓的流模式），但是不能说没有请求直接有返回，否则用户处理侧会更加复杂且不统一了。grpc 使用流模式的示例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;streamClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看出，Server Push 从来不是一项新技术，因为一直以来这功能就是 TCP 现成的。我们所缺少的，其实仅仅只是在应用层的操作规范而已。&lt;/p&gt;
&lt;h3 id="最后"&gt;最后&lt;/h3&gt;
&lt;p&gt;从 epoll 的诞生，到 Server Push 问题的解决，我们不难看到，所谓的新技术从更加宏观的视角来看压根就不能算是技术，仅仅只是一些对约定共识的改变而已。但就是这种共识的改变，可能会需要几十年的时间。&lt;/p&gt;
&lt;p&gt;文明建立在共识的基础上，技术也是如此。文明的进步靠去破除女子无才便是德这类旧有的共识，技术的进步也是如此。&lt;/p&gt;</content:encoded></item><item><title>RPC 漫谈：序列化问题</title><link>https://blog.joway.io/posts/deep-into-rpc-serialization/</link><pubDate>Fri, 30 Apr 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-serialization/</guid><category>Tech</category><description>何为序列 对于计算机而言，一切数据皆为二进制序列。但编程人员为了以人类可读可控的形式处理这些二进制数据，于是发明了数据类型和结构的概念，数据类型用以标注一段二进制数据的解析方式，数据结构用以标注多段(连续/不连续)二进制数据的组织方式。
例如以下程序结构体：</description><content:encoded>&lt;h2 id="何为序列"&gt;何为序列&lt;/h2&gt;
&lt;p&gt;对于计算机而言，一切数据皆为二进制序列。但编程人员为了以人类可读可控的形式处理这些二进制数据，于是发明了数据类型和结构的概念，数据类型用以标注一段二进制数据的解析方式，数据结构用以标注多段(连续/不连续)二进制数据的组织方式。&lt;/p&gt;
&lt;p&gt;例如以下程序结构体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Name 和 Email 分别表示两块独立(或连续，或不连续)的内存空间（数据），结构体变量本身也有一个内存地址。&lt;/p&gt;
&lt;p&gt;在单进程中，我们可以通过分享该结构体地址来交换数据。但如果要将该数据通过网络传输给其他机器的进程，我们需要现将该 User 对象中不同的内存空间，编码成一段&lt;strong&gt;连续二进制&lt;/strong&gt;表示，此即为「序列化」。而对端机器收到了该二进制流以后，还需要能够认出该数据为 User 对象，解析为程序内部表示，此即为「反序列化」。&lt;/p&gt;
&lt;p&gt;序列化和反序列化，就是将同一份数据，在人的视角和机器的视角之间相互转换。&lt;/p&gt;
&lt;h2 id="序列化过程"&gt;序列化过程&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-serialization/overview_hu_80b0fe0a26605206.webp" width="450" height="250" alt="RPC 漫谈：序列化问题" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="定义接口描述idl"&gt;定义接口描述（IDL）&lt;/h3&gt;
&lt;p&gt;为了传递数据描述信息，同时也为了多人协作的规范，我们一般会将描述信息定义在一个由 IDL(Interface Description Languages) 编写的定义文件中，例如下面这个 Protobuf 的 IDL 定义：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="生成-stub-代码"&gt;生成 Stub 代码&lt;/h3&gt;
&lt;p&gt;无论使用什么样的序列化方法，最终的目的是要变成程序中里的一个对象，虽然序列化方法往往是语言无关的，但这段将内存空间与程序内部表示（如 struct/class）相绑定的过程却是语言相关的，所以很多序列化库才会需要提供对应的编译器，将 IDL 文件编译成目标语言的 Stub 代码。&lt;/p&gt;
&lt;p&gt;Stub 代码内容一般分为两块：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;类型结构体生成（即目标语言的 Struct[Golang]/Class[Java] ）&lt;/li&gt;
&lt;li&gt;序列化/反序列化代码生成（将二进制流与目标语言结构体相转换）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面是一段 Thrift 生成的的序列化 Stub 代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`thrift:&amp;#34;name,1&amp;#34; db:&amp;#34;name&amp;#34; json:&amp;#34;name&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`thrift:&amp;#34;email,2&amp;#34; db:&amp;#34;email&amp;#34; json:&amp;#34;email&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//写入 User struct&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TProtocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteStructBegin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;User&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T write struct begin error: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeField1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeField2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFieldStop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;write field stop error: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteStructEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;write struct stop error: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 写入 name 字段&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;writeField1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TProtocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFieldBegin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T write field begin error 1:name: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T.name (1) field write error: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFieldEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T write field end error 1:name: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 写入 email 字段&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;writeField2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TProtocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFieldBegin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T write field begin error 2:email: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T.email (2) field write error: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oprot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFieldEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thrift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T write field end error 2:email: &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，为了把 User 对象给序列化成二进制，它 hard code 了整个结构体在内存中的组织方式和顺序，并且分别对每个字段去做强制类型转换。如果我们新增了一个字段，就需要重新编译 Stub 代码并要求所有 Client 进行升级更新（当然不需要用到新字段可以不用更新）。反序列化的步骤也是类似。&lt;/p&gt;
&lt;p&gt;上述这段冗长的代码还只是我们用于演示的一个最简单的消息结构，对于生产环境中的真实消息类型，这段 Stub 代码会更加复杂。&lt;/p&gt;
&lt;p&gt;Stub 代码生成只是为了解决跨语言调用的问题，并不是必须项。如果你的调用方与被调用方都是同一种语言，且未来一定能够保证都是同一种语言，这种情况也会选择直接用目标语言去写 IDL 定义，跳过编译的步骤，例如 Thrift 里的 &lt;a href="https://github.com/airlift/drift"&gt;drift&lt;/a&gt; 项目就是利用 Java 直接去写定义文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@ThriftStruct&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ThriftConstructor&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ThriftField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ThriftField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="序列化问题"&gt;序列化问题&lt;/h2&gt;
&lt;p&gt;早在 1984 年提出 &lt;a href="http://web.eecs.umich.edu/~mosharaf/Readings/RPC.pdf"&gt;Remote Procedure Calls&lt;/a&gt; 概念时候，就基本定型了今天整个 RPC 的框架。后人所做的各式优化和改造，都是在这套框架下做各种 trade off，以解决不同场景下遇到的不同问题。&lt;/p&gt;
&lt;h3 id="stub-代码膨胀"&gt;Stub 代码膨胀&lt;/h3&gt;
&lt;p&gt;从前面的 &lt;code&gt;User&lt;/code&gt; 对象生成的 Stub 代码中我们已经感受到了这种针对于每个字段分别 hard code 处理产生的代码膨胀问题。在实际生产环境中，单个服务的 Stub 代码超过几万行也是非常常见的事情。虽然这种方式对服务性能并不会有影响，且也无需工程师做任何额外工作，但当项目非常庞大以后，会造成一些开发体验上的损失。比如 IDE 做语法分析速度会变慢，项目打开很卡。&lt;/p&gt;
&lt;p&gt;Protobuf 在其官方实现中，使用的是&lt;strong&gt;反射&lt;/strong&gt;来处理二进制与程序内部表示间的转换。&lt;/p&gt;
&lt;p&gt;反射的本质是在语言层面提供了一种暴露程序现有类型和数据的能力，在运行时我们从二进制数据中解析到对应字段，然后通过反射获取到对应程序内结构体的 Field，并对其赋值。&lt;/p&gt;
&lt;p&gt;这种做法显然会比硬编码的方式要慢得多，主要需要额外做以下事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询当前类型信息&lt;/li&gt;
&lt;li&gt;数据类型校验&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以通过反射来减少 Stub 代码量谈不上算是什么优化，只能说是在性能和便利性方面选择了便利性。同时也方便了 Protobuf 快速支持其他多种语言。&lt;/p&gt;
&lt;p&gt;有意思的是，虽然 Protobuf 官方使用的是反射实现，但是有相当一部分人为了更高的性能，使用的是社区开源的 &lt;a href="https://github.com/gogo/protobuf"&gt;gogo/protobuf&lt;/a&gt;。下面是一段 gogo protobuf 编译后的代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;MarshalToSizedBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;XXX_unrecognized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;XXX_unrecognized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;XXX_unrecognized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 序列化 email 字段&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;encodeVarintUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 序列化 name 字段&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;encodeVarintUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xa&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dAtA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，gogo/protobuf 依然是在试图用硬编码来替换掉官方的反射实现，所以才能够提升性能。&lt;/p&gt;
&lt;h3 id="减少数据传输大小"&gt;减少数据传输大小&lt;/h3&gt;
&lt;p&gt;对于一般性的业务服务来说，CPU 资源会先于内存和网络成为一个系统的瓶颈。对于这类服务来说，时间往往比空间更加重要。但也有一些吞吐量极高，业务逻辑简单的服务（数据库/消息队列/&amp;hellip;），网络带宽会优先于 CPU 能够制约服务性能的瓶颈，对于这类业务我们需要用时间换空间，压缩数据包体积。&lt;/p&gt;
&lt;h4 id="编码压缩"&gt;编码压缩&lt;/h4&gt;
&lt;p&gt;在基本数据类型里，字符串的压缩方法已经有非常多常见的方案，并且往往是由业务方自己实现，所以这里不再赘述。除了字符串以外，主要就是一些数字类型：int32, int64, double。&lt;/p&gt;
&lt;p&gt;假如现在我们有一个业务里有一个字段表示用户对某条消息的点赞数，对于大部分消息来说该值往往都很小，但因为极端情况的存在，我们依然需要使用 int32 类型，如果按照原始的二进制编码，无论值是多少都需要用 4 bytes 大小的空间占用。针对这种使用情况，常见的思想是，用一套编码约定，使得可以根据 int 值大小进行动态扩展编码长度。&lt;/p&gt;
&lt;h5 id="varint-编码"&gt;Varint 编码&lt;/h5&gt;
&lt;p&gt;Varint 的思想很简单，每一个字节有 8 位，最高位表示下一个字节是否还是该数字的一部分，其余 7 位用原码补齐。例如：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-serialization/varuint_hu_3614aa82640c0673.webp" srcset="https://blog.joway.io/images/rpc-serialization/varuint_hu_7fa06fa7a5a710c0.webp 800w, https://blog.joway.io/images/rpc-serialization/varuint_hu_3614aa82640c0673.webp 911w" sizes="(max-width: 800px) 100vw, 800px" width="911" height="360" alt="RPC 漫谈：序列化问题" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;但是对于负数而言，最高位一定是 1，所以 varint 编码一定会变成 5 个字节，反而增加了大小。&lt;/p&gt;
&lt;h5 id="zigzag-编码"&gt;ZigZag 编码&lt;/h5&gt;
&lt;p&gt;为了解决 varint 对负数不友好的问题，一种思想是把整个 int32 的空间映射到 uint32 的空间，编码只需要 uint32 中进行，最后做一层映射转换。ZigZag 就是使用的这个思想。&lt;/p&gt;
&lt;h4 id="省略默认值"&gt;省略默认值&lt;/h4&gt;
&lt;p&gt;还有一类做法是通过双端共识，来减少一些默认值的传输。&lt;/p&gt;
&lt;p&gt;例如一个字段 &lt;code&gt;enable: bool&lt;/code&gt; 在 IDL 文件里定义的默认值为 true，如果我们当前数据包需要传输的值也是为 true，那就没有意义去传输这个字段。客户端收到数据包后，可以依赖 IDL 定义去还原出这个值。但是这个共识定义以后一般是不能变化的，否则会出现你以为你传的是 true，反序列化得到的却是 false 的结果。除非你能够做到同时让所有涉及到的参与方都更新里自己的 Stub 代码。&lt;/p&gt;
&lt;h3 id="内存操作过多"&gt;内存操作过多&lt;/h3&gt;
&lt;p&gt;影响序列化性能的另外一个问题是，在序列化过程中会频繁创建内存，一来这影响到了序列化速度，二来对于有 GC 的语言这也增加了垃圾回收压力。&lt;/p&gt;
&lt;p&gt;我们先来看看序列化过程的内存申请与转换流程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// === 序列化 ===&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//申请一段用户态缓冲区&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//将 User 对象所有字段编码成二进制数据&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;EncodeString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;EncodeString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//系统调用，从内核缓冲区将数据读入用户态缓冲区&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// === 反序列化 ===&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//申请一段用户态缓冲区&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//系统调用，从内核缓冲区将数据读入用户态缓冲区&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//根据编码格式，对二进制数据进行类型转换&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DecodeString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:])&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DecodeString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[:])&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在这个过程中，我们有以下几次内存操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;buf := make([]byte)：每次解析都需要有一段独占的用户态缓冲区，用于从内核缓冲区读取数据。多次解析间可以重用。&lt;/li&gt;
&lt;li&gt;EncodeString/DecodeString：以 string 格式处理数据。由于这里的 buf 是会被重用的，所以这里并不能直接复用 buf 的内存，需要一次拷贝。如果该字段是一个非常大的 field，例如视频数据，这里的 copy 开销会非常巨大。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文先不考虑如何优化内核态到用户态的拷贝，仅从序列化角度考虑如何减少用户态的拷贝。&lt;/p&gt;
&lt;h4 id="避免大-stringbytes-类型的拷贝"&gt;避免大 string/bytes 类型的拷贝&lt;/h4&gt;
&lt;p&gt;在示例中，我们之所以不能直接使用 buf 内的数据，是因为这段数据还要被下一次序列化/反序列化重用。如果我们不再考虑重用，每个 buf 都是一次解析完整生命周期独占的，那么就可以直接赋值而不用拷贝。当然这种做法主要是针对于 string 和 bytes 类型（也只有这两种类型会出现超大不定长的内存占用），int/bool等其他基本类型在创建结构体时便已经申请了空间。&lt;/p&gt;
&lt;p&gt;这块可以参考字节跳动在其 KiteX 中所实现的 &lt;a href="https://www.infoq.cn/article/fea7chf9moohbxbtyres"&gt;Linkbuffer&lt;/a&gt; 结构。基本思路是每次解析前，先申请好一块 Buffer 地址（可以是其他解析过程释放的，也可以是新建的），然后整个生命周期都使用这一块地址。使用结束后释放(标记 free，并不释放内存) Buffer，然后可以交给下一次序列化使用。&lt;/p&gt;
&lt;h4 id="预计算-buffer-大小"&gt;预计算 Buffer 大小&lt;/h4&gt;
&lt;p&gt;在我们去做序列化前，我们便已经可以得知最终序列化完成时的 buffer 大小。甚至对于那些只含有基本定长数据类型的结构，我们甚至在编译时就能预知最终大小。对于这类能够事先知道体积的结构，我们在序列化前便可以创建一个固定长度的 buffer，从而让整个序列化过程只有一次 malloc。&lt;/p&gt;
&lt;h4 id="直接操作-buffer"&gt;直接操作 Buffer&lt;/h4&gt;
&lt;p&gt;序列化与反序列化是将二进制数据与程序内部表示直接相转换，才引起后面的那么多问题。一个更加直接解决问题的方式就是我们干脆就别序列化了，直接操作二进制。正所谓不解决问题，解决提出问题的人。&lt;/p&gt;
&lt;p&gt;Protobuf V2 的作者从 Google 出来后，开发了 &lt;a href="https://capnproto.org/"&gt;Cap&amp;rsquo;n Proto&lt;/a&gt;。有趣的是，后来 Google 自己内部又开源了 &lt;a href="https://google.github.io/flatbuffers/index.html"&gt;Flatbuffers&lt;/a&gt;。这两个项目基本思路是一样的，Stub 生成代码用以生成操作数据的接口，而这些接口底层不是在读写某个结构体，而是直接操作的最终序列化的二进制数据。这样序列化和反序列化相当于都在同一个二进制数据上进行，当然也就可以认为序列化时间为 0，性能可以超过任何序列化框架。&lt;/p&gt;
&lt;p&gt;以下是 Cap&amp;rsquo;n Proto 生成的一段代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;capnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;SetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ptr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;SetEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以下是 Flatbuffers 生成的一段代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ByteVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ByteVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;UserAddName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependUOffsetTSlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;UserAddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrependUOffsetTSlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flatbuffers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UOffsetT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到两种方式生成的代码非常相似，并且生成的 User struct 没有像之前的序列化框架那样带上 &lt;code&gt;Name&lt;/code&gt; 和 &lt;code&gt;Email&lt;/code&gt; 字段，而是通过函数的方式读写能力。&lt;/p&gt;
&lt;p&gt;我们前面说了，序列化本身的意义就在于提供人和机器视角对数据认识的一种转换。传统的思路是通过一个中间结构体，而这类方式是通过提供操作函数。&lt;/p&gt;
&lt;p&gt;不过这类方式有一个通病就是仅仅只是提供了操作数据的能力，但是牺牲了程序编写者自己去管理数据的便利性。比如如果我们想知道这个 User 结构有哪些字段，除非序列化编译后的代码提供给了你这个能力，否则你将对一串二进制无从下手。比如你想直接把这个 User 对象和一些 ORM 工具组合存进数据库，你必须自己手写一个新的 User struct，然后挨个字段赋值。&lt;/p&gt;
&lt;p&gt;这类序列化框架大多用在那些数据定义不怎么变化的核心基础设施服务，例如数据库，消息队列这类。如果用在日常业务开发，或许性价比不是很高。&lt;/p&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;我们经常听到网上有人讨论，哪个序列化协议性能更好。其实如果我们真的认真去研究各类序列化方案，很容易会发现，序列化协议本身只是一份文档，它的性能优劣取决于你怎么去实现。不同语言实现，同语言不同方式方法的实现，都会对最终的易用性和性能产生巨大的影响。你完全可以把 Protobuf 的协议用 Flatbuffer 的方式去实现，能够提升非常多的性能，但未必就是你想要的。&lt;/p&gt;
&lt;p&gt;与性能相比更为重要的是先弄清楚我们在序列化的各种问题中，希望解决哪些，愿意放弃哪些，有了明确的需求才能选择到适合的序列化方案，并且真的遇到问题时也能快速知道这个问题是否是可解的，如何解。&lt;/p&gt;</content:encoded></item><item><title>RPC 漫谈： 限流问题</title><link>https://blog.joway.io/posts/deep-into-rpc-ratelimiter/</link><pubDate>Fri, 23 Apr 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-ratelimiter/</guid><category>Tech</category><description>微服务之间的 RPC 调用往往会使用到限流功能，但是很多时候我们都是用很简单的限流策略，亦或是工程师拍脑袋定一个限流值。
这篇文章主要讨论在 RPC 限流中，当前存在的问题和可能的解决思路。
为什么需要限流 避免连锁崩溃 一个服务即便进行过压测，但当真实运行到线上时，其收到的请求流量以及能够负载的流量是不固定的，如果服务自身没有一个自我保护机制，当流量超过预计的负载后，会将这部分负载传递给该服务的下游，造成连锁反应甚至雪崩。</description><content:encoded>&lt;p&gt;微服务之间的 RPC 调用往往会使用到限流功能，但是很多时候我们都是用很简单的限流策略，亦或是工程师拍脑袋定一个限流值。&lt;/p&gt;
&lt;p&gt;这篇文章主要讨论在 RPC 限流中，当前存在的问题和可能的解决思路。&lt;/p&gt;
&lt;h2 id="为什么需要限流"&gt;为什么需要限流&lt;/h2&gt;
&lt;h3 id="避免连锁崩溃"&gt;避免连锁崩溃&lt;/h3&gt;
&lt;p&gt;一个服务即便进行过压测，但当真实运行到线上时，其收到的请求流量以及能够负载的流量是不固定的，如果服务自身没有一个自我保护机制，当流量超过预计的负载后，会将这部分负载传递给该服务的下游，造成连锁反应甚至雪崩。&lt;/p&gt;
&lt;h3 id="提供可靠的响应时间"&gt;提供可靠的响应时间&lt;/h3&gt;
&lt;p&gt;服务调用方一般都设有超时时间，如果一个服务由于拥塞，导致响应时间都处于超时状态，那么即便服务最终正确提供了响应，对于 Client 来说也完全没有意义。&lt;/p&gt;
&lt;p&gt;一个服务对于调用方提供的承诺既包含了响应的结果，也包含了响应的时间。限流能够让服务自身通过主动丢弃负载能力外的流量，以达到在额定负载能力下，依然能够维持有效的响应效率。&lt;/p&gt;
&lt;h2 id="传统方案"&gt;传统方案&lt;/h2&gt;
&lt;h3 id="漏斗"&gt;漏斗&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-ratelimit/ratelimit-funnel_hu_dd60488b1f8e1a19.webp" width="443" height="299" alt="RPC 漫谈： 限流问题" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能够强制限制出口流量速率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法适应突发性流量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="令牌桶"&gt;令牌桶&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-ratelimit/ratelimit-token-bucket_hu_fb547d386bc141ac.webp" srcset="https://blog.joway.io/images/rpc-ratelimit/ratelimit-token-bucket_hu_836b902e9ff4cd4d.webp 800w, https://blog.joway.io/images/rpc-ratelimit/ratelimit-token-bucket_hu_9f2f8183187eb568.webp 1200w, https://blog.joway.io/images/rpc-ratelimit/ratelimit-token-bucket_hu_fb547d386bc141ac.webp 1360w" sizes="(max-width: 800px) 100vw, 800px" width="1360" height="794" alt="RPC 漫谈： 限流问题" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在统计上维持一个特定的平均速度&lt;/li&gt;
&lt;li&gt;在局部允许短暂突发性流量通过&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="存在的问题"&gt;存在的问题&lt;/h3&gt;
&lt;p&gt;在两类传统方案中，都需要去指定一个固定值用以标明服务所能够接受的负载，但在现代的微服务架构中，一个服务的负载能力往往是会不断变化的，有以下几个常见的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着新增代码性能变化而变化&lt;/li&gt;
&lt;li&gt;随着服务依赖的下游性能变化而变化&lt;/li&gt;
&lt;li&gt;随着服务部署的机器(CPU/磁盘)性能变化而变化&lt;/li&gt;
&lt;li&gt;随着服务部署的节点数变化而变化&lt;/li&gt;
&lt;li&gt;随着业务需求变化而变化&lt;/li&gt;
&lt;li&gt;随着一天时间段变化而变化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过人工声明一个服务允许的负载值，即便这个值是维护在配置中心可以动态变化，但依然是不可持续维护的，况且该值具体设置多少也极度依赖于人的个人经验和判断。甚至人自身的小心思也会被带入到这个值的选择中，例如 Server 会保守估计自己的能力，Client 会过多声明自己的需求，长期以往会导致最终的人为设定值脱离了实际情况。&lt;/p&gt;
&lt;h2 id="什么是服务负载"&gt;什么是服务负载&lt;/h2&gt;
&lt;p&gt;当我们向一个服务发起请求时，我们关心的无外乎两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务能够支撑的同时并发请求数&lt;/li&gt;
&lt;li&gt;服务的响应时间&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="并发请求数"&gt;并发请求数&lt;/h3&gt;
&lt;p&gt;对于 Server 而言，有几个指标常常容易搞混：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前连接数&lt;/li&gt;
&lt;li&gt;当前接受的请求数&lt;/li&gt;
&lt;li&gt;当前正在并发处理的请求数&lt;/li&gt;
&lt;li&gt;QPS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;连接数和请求数是 1:N 的关系。在现代 Server 的实现中，连接本身消耗的服务器资源已经非常少了（例如 Java Netty 实现，Go Net 实现等），而且一般对内网的服务而言，多路复用时，请求数变多也并不一定会导致连接数变多。&lt;/p&gt;
&lt;p&gt;有些 Server 出于流量整形角度的考虑，并不一定会在收到请求以后，立马交给 Server 响应函数处理，而是先加入到一个队列中，等 Server 有闲置 Worker 后再去执行。所以这里就存在两类请求：接受的请求与正在处理的请求。&lt;/p&gt;
&lt;p&gt;而 QPS 是一个统计指标，仅仅只表示每秒通过了多少请求。&lt;/p&gt;
&lt;p&gt;在当下的时间点，对 Server 有负载起到决定性影响的，一般都是当前的并发请求数。&lt;/p&gt;
&lt;h3 id="服务响应时间"&gt;服务响应时间&lt;/h3&gt;
&lt;p&gt;当一个在线服务去响应一个请求时，其自身所做的工作抽象来看，无外乎以下两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算：时间取决于 CPU 频率，&lt;strong&gt;固定值&lt;/strong&gt;（不考虑超频）&lt;/li&gt;
&lt;li&gt;等待：时间取决于当前并行请求数，&lt;strong&gt;不固定&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;排队等待 Server 有闲置线程/协程来响应请求&lt;/li&gt;
&lt;li&gt;等待其他线程释放竞争资源（可能是锁，也可能占用的 CPU）&lt;/li&gt;
&lt;li&gt;等待 IO(Storage, Network)返回 （不考虑下游抖动，该时间一般也为固定值）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从上面但分析我们可以看出，一个服务最终的响应时间：&lt;strong&gt;RT = 工作时间 + 等待时间&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="负载能力估算"&gt;负载能力估算&lt;/h2&gt;
&lt;p&gt;我们可以把一个微服务抽象为一个输入输出的水管，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-ratelimit/ratelimit-water_hu_86f16564c96bf51e.webp" srcset="https://blog.joway.io/images/rpc-ratelimit/ratelimit-water_hu_bdbedb4fb38fa5f9.webp 800w, https://blog.joway.io/images/rpc-ratelimit/ratelimit-water_hu_86f16564c96bf51e.webp 871w" sizes="(max-width: 800px) 100vw, 800px" width="871" height="137" alt="RPC 漫谈： 限流问题" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;中间的这根水管的“负载能力”其实就是这个水管的体积（管径 * 管长），这是一个非常好量化的指标。&lt;/p&gt;
&lt;p&gt;我们的服务在线上所遇到的情况其实和这根水管是类似的，现在需要做的是找到一个类似计算水管体积的量化方法，来估算服务的负载能力。&lt;/p&gt;
&lt;p&gt;现在我们在某一秒时间窗口内，对一个服务实例进行观测，很容易可以得到以下两个值:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;QPS：这一秒内的请求数，单位为 req/s 。&lt;/li&gt;
&lt;li&gt;AvgRT: 这一秒内的平均请求响应时间，单位 ms。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据 &lt;a href="https://en.wikipedia.org/wiki/Little%27s_law"&gt;Little&amp;rsquo;s law&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在一个稳定的系统（L）中，长期的平均顾客人数，等于长期的有效抵达率（λ），乘以顾客在这个系统中平均的等待时间（W）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;该法则用于计算一个稳定且资源有限系统的吞吐量。&lt;/p&gt;
&lt;p&gt;在我们的系统里，利用该法则，可以得到 &lt;code&gt;Throughput = QPS * (AvgRT / 1000)&lt;/code&gt;。其中 AvgRt / 1000 将毫秒单位换算成秒。&lt;/p&gt;
&lt;p&gt;从另一个角度去理解这个公式也可以认为，如果我们能够保证 Server &lt;strong&gt;当前正在处理的请求量(inflight)&lt;/strong&gt; 不超过该值，理论上每个请求的平均响应时间也应该维持在 AvgRt 左右（进水速度 ~= 出水速度）。这个最终算出来的量，就是我们想要得到的对于服务&lt;strong&gt;当前负载情况的估算值&lt;/strong&gt;。而只有当服务开始出现负载压力以后，这个&lt;strong&gt;当前负载情况&lt;/strong&gt;才可以被认为是&lt;strong&gt;负载能力&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="inflightrt-与-throughput-的关系"&gt;Inflight，RT 与 Throughput 的关系&lt;/h2&gt;
&lt;p&gt;对于大部分在线服务来说，Inflight 和 RT 以及 Throughput 的关系有以下两个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在没有发生资源限制(CPU/磁盘/网络/内存)的情况下，随着 inflight 变大，RT 一般不会发生明显变化，Throughput 开始变大。&lt;/li&gt;
&lt;li&gt;当 inflight 继续变大，出现了资源竞争时，RT 会随着 inflight 增大而增大，但 Throughput 并不会显著变化，因为有限的资源只能干有限的事情。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据上面的这个现象，我们可以画出三个坐标图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught-inflight_hu_f442df2d7039d94b.webp" srcset="https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught-inflight_hu_ac6985b274253849.webp 800w, https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught-inflight_hu_f442df2d7039d94b.webp 840w" sizes="(max-width: 800px) 100vw, 800px" width="840" height="1208" alt="RPC 漫谈： 限流问题" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;前两个图反应的是 Server 的实际现象，而第三个图，是将前两个图的相同的横坐标 inflight 消掉，单独看 RT 与 Throughput 的关系。第三个图表现了我们限流工作的本质：&lt;strong&gt;用 RT损失去换取吞吐量的提升&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;更加符合实际情况的图是：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught_hu_69271d1fd2170f5b.webp" srcset="https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught_hu_272cdcc0d056f5c1.webp 800w, https://blog.joway.io/images/rpc-ratelimit/ratelimit-rt-throught_hu_69271d1fd2170f5b.webp 860w" sizes="(max-width: 800px) 100vw, 800px" width="860" height="442" alt="RPC 漫谈： 限流问题" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这里的斜率越低，代表这部分RT损失越划算。我们的限流策略，就是去找到这个最佳限流点。&lt;/p&gt;
&lt;h2 id="工程实践"&gt;工程实践&lt;/h2&gt;
&lt;p&gt;上述分析只是在一个理论模型上进行的一个推导，在实际工程应用中，需要考虑到以下现实情况。&lt;/p&gt;
&lt;h3 id="何时启动限流"&gt;何时启动限流&lt;/h3&gt;
&lt;p&gt;在一开始不存在资源竞争的阶段，我们没有任何必要去进行流控。所以需要一个启发指标来标志服务开始进入到来竞争的忙碌状态以触发流控逻辑，一般我们可以选择以下一些常见指标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OS Load1&lt;/li&gt;
&lt;li&gt;CPU Usage&lt;/li&gt;
&lt;li&gt;Avg RT 值&lt;/li&gt;
&lt;li&gt;Thread Number&lt;/li&gt;
&lt;li&gt;QPS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="rt-的选择"&gt;RT 的选择&lt;/h3&gt;
&lt;p&gt;前面已经推演得到了一个公式：&lt;strong&gt;RT = 工作时间 + 等待时间&lt;/strong&gt;。而这里的 &lt;strong&gt;等待时间&lt;/strong&gt; 还等于 &lt;strong&gt;排队时间 + 竞争时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是 RT 只是一个单请求指标，要计算 Throughput 需要的是一个统计意义上的 RT 值，这时候选择是 AvgRt，还是 MinRT，还是 P95RT 就是一个细节但极为重要的事情了。&lt;/p&gt;
&lt;p&gt;如果我们是一个性能极为稳定的系统，类似交换机，路由器这类，那么包与包之间不会彼此互相影响，所以这类系统的 &lt;strong&gt;RT = 工作时间 + 排队时间&lt;/strong&gt;。而我们希望尽可能减少排队时间（因为没必要，大家都去排队会造成更加拥挤），所以这类情况 RT 可以选择使用 MinRt，因为 MinRT 最接近固定时间。这也是 &lt;a href="https://github.com/google/bbr"&gt;Google BBR&lt;/a&gt; 算法所使用的值。&lt;/p&gt;
&lt;p&gt;但是业务服务的不同之处在于，请求与请求之间不仅会争抢 CPU，存储，也可能出现类似锁竞争的问题。这部分问题导致竞争等待时间非常不确定，抖动频繁且不可预计。如果依然使用 MinRT，会导致低估了服务的并发能力。&lt;/p&gt;
&lt;p&gt;一个例子可以更加直观感受这个过程：某大学每年招收1000研究生，2年学制，但考试太难，导致平均3年才能毕业。这个时候去计算该大学同时能够容纳多少研究生，如果取2年，显然不合适。&lt;/p&gt;
&lt;p&gt;但是不是意味着我们就可以直接用 AvgRT 呢？其实也不是，限流很难做到刚好卡在一个精确的点上，就算真的卡精确了，也会导致容错性不高。所以限流会倾向于略微低估系统的负载能力。Bilibili 在他们公开的微服务框架 &lt;a href="https://github.com/go-kratos/kratos/blob/a52895637c6090cf74acb3ca0be16c82939549c1/pkg/ratelimit/bbr/bbr.go#L120"&gt;Kratos&lt;/a&gt; 中，使用的是对每一个采样窗口中的 AvgRT 中取最小的那个 AvgRT。而阿里的 Sentinel 使用的是真正意义的&lt;a href="https://github.com/alibaba/Sentinel/blob/4459ab2caf2029be2d48ed0bd8110757059a9a84/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java#L142"&gt;最小 RT&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;当根据实际业务特性选定好了 RT 指标后，再乘以前面统计到的 QPS 则可得到 Throughput 值，此时只要判断当 &lt;code&gt;Inflight &amp;gt; Throughput&lt;/code&gt; 直接丢弃便可实现一个自适应且有科学度量标准的限流策略。&lt;/p&gt;</content:encoded></item><item><title>科学，技术与工程</title><link>https://blog.joway.io/posts/science-technology-and-engineering/</link><pubDate>Tue, 09 Mar 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/science-technology-and-engineering/</guid><category>Thought</category><description>作为软件工程师，我们在谈论自己时，总会认为自己是所谓的高科技行业从业者，但是如果观察自己的日常工作，时常会觉得似乎和真正的科技也没有什么关系。所以究竟什么是科技？我们要如何来定义我们每天的工作？
我认为宏观意义上的科技可以被拆解成三个概念：科学(Science)，技术(Technology)和工程(Engineering)。</description><content:encoded>&lt;p&gt;作为软件工程师，我们在谈论自己时，总会认为自己是所谓的高科技行业从业者，但是如果观察自己的日常工作，时常会觉得似乎和真正的科技也没有什么关系。所以究竟什么是科技？我们要如何来定义我们每天的工作？&lt;/p&gt;
&lt;p&gt;我认为宏观意义上的科技可以被拆解成三个概念：科学(Science)，技术(Technology)和工程(Engineering)。&lt;/p&gt;
&lt;p&gt;科学是观察客观世界以&lt;strong&gt;发现&lt;/strong&gt;既有的自然规律，技术是组合自然规律以&lt;strong&gt;发明&lt;/strong&gt;新的改造客观世界的方法，工程是&lt;strong&gt;发挥&lt;/strong&gt;技术的能力以合乎客观世界要求的方去改造它。&lt;/p&gt;
&lt;p&gt;欧姆定律，麦克斯韦方程组是科学的&lt;strong&gt;发现&lt;/strong&gt;，整个电子产业的基石全在于控制电子的运动与电场的传递，电子的运动建构了存储和计算的能力，电场的传递建构了通信能力。电子的运动一定会遵守欧姆定律，电场的传递一定会遵循麦克斯韦方程组。&lt;/p&gt;
&lt;p&gt;在自然规律的基础上，人们将电磁感应与过去的蒸汽机技术相结合&lt;strong&gt;发明&lt;/strong&gt;了发电机技术。如果宇宙仅有地球存在智慧生物，那么第一台发电机的发明是真正创造了一个过去不曾有过的事物。但发电机的发明者不见得一定要制作出一个性能优异的实际发电机产品，可以只是一个原型，甚至可以只是提出了一种概念。冯诺伊曼也并没有实际去动手做出一个以他名字命名的结构下的计算机，但他的确发明了现代存储程序型计算机的概念。&lt;/p&gt;
&lt;p&gt;要将一个抽象的计算机设计落地成为一个实际可用的计算机设备中间还是有很长一段距离，需要考虑非常多复杂的情况，例如成本，规格，安全性等。工程所做的，就是&lt;strong&gt;发挥&lt;/strong&gt;实验室中的技术到非理想的现实环境中去。我们经常说的某个技术不具备可行性，通常其实是指这个技术本身虽然可行，但是在工程上不可行。&lt;/p&gt;
&lt;p&gt;客观世界的复杂性根源来自事物彼此之间是有相互作用和联系的，改造客观世界的方法也在于利用事物的这一特点加以「组合」。自然规律是对元素周期表的组合而产生的现象，技术是在组合自然规律创造新的方法，而工程又是在组合各类技术创造出实际的产品。&lt;/p&gt;
&lt;p&gt;人类的大脑无法并行地去进行多条件下的逻辑思考，所以往往会先剔除所有其他因素，假设一个理想的环境，从而得以专注于在此环境下得出仅适用于该环境下的某种规律，并美其名曰科学的「简洁性」。从事科学研究的人把对于非理想环境的思考移交给了去实际发明技术的人，而从事技术发明的人同样会倾向于简化问题，将复杂性移交给了那些真正去落地的工程师。这就是工程为什么会如此复杂的原因所在，他本身的目的就是去&lt;strong&gt;管理复杂性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在工程学领域中，软件工程又可能是其中最复杂一类工程。其根本性原因在于软件开发是一门个体创造性太强的工程。今天如果你要去制作一部手机，在供应商和成本的制约下，会让你在工程上并没有太多选择，这也是为什么所有手机厂商制造的手机参数和外观都差不多的原因。而软件就不同了，每个公司首先编程语言就可以有不同的选择，其次不同的发展阶段也需要有不同的软件架构，再者还可以有各种第三方库的选型。同样是一个业务需求，让100个工程师去实现，可能会有100种不同的技术选型组合，实际到编码层面又有更为不同的风格和设计差异。最致命的问题是，这100种做法很可能都同时是 make sense 的方案。&lt;/p&gt;
&lt;p&gt;软件相比于硬件还有一个特点是其生命周期极长。人或许会几年换一次硬件，无论是电脑还是汽车，但有可能几十年就用同一种软件。与此同时不同软件之间因为需要彼此交互，还有着固定的接口和协议，所以软件的升级和更换还存在一个兼容性的问题。说到这里，或许有人会发现软件和人类自身是极为相似的。&lt;/p&gt;
&lt;p&gt;1968年北约首次定义了软件工程的概念，而这一年苏联入侵了捷克斯洛伐克，中国正在大搞文革。人类浪费了大量时间在处理各种没有任何意义的内部外部矛盾，如果人类的上层也有一个「人件开发者」，那么他的架构能力肯定非常一般，但同时也非常厉害，至少能让如此混乱的人类存活几万年到今天。软件工程内部的矛盾亦如人类社会的矛盾，甚至有时人类社会自身的政治也会被带入到软件内。我们可以在 iOS 和 Android 的设计里看到共和党和民主党的影子。大量的软件工程师花费了大量的生命仅仅就为了让某个软件同时能够跑在多种设备上，但如果这些设备能够一开始就使用同一种接口，可能就没有这么多事情。软件也不是没有做此类的事情，大量的软件协议就是为此而生，这有点接近于国家间的贸易协定，制定协议的工程师就如何现实里的政客。&lt;/p&gt;
&lt;p&gt;所以回到文章开头的问题，为什么软件明明是人类至今最高科技文明的产物，而作为软件工程师的我们却很难从日常工作中感受到高科技的工作氛围？因为今天我们编写的程序，和第一台计算机上的纸带其实没有任何本质区别，而我们日常工作所在解决的复杂性问题，恰恰是由于前人在试图解决复杂性问题时所创造的复杂性，而这其实和计算机科学本身没有任何关系。如果软件的复杂性能够被彻底消除，人类社会也就不会存在如此多的政治矛盾，但亦如同复杂性造就了人类文明的璀璨一样，正是高创造性的软件开发才能爆发出今天我们所见到的精彩纷呈的软件革命。&lt;/p&gt;</content:encoded></item><item><title>Pond: Golang 通用对象池</title><link>https://blog.joway.io/posts/golang-common-pool/</link><pubDate>Sat, 23 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-common-pool/</guid><category>Tech</category><description>为什么需要通用对象池 在实际生产环境中，我们经常会遇到需要多线程共享同一堆对象的场景，例如常见的RPC、数据库连接池，大内存对象池，以及线程池等。这些池化对象的需求其实有很多重叠的地方，主要分以下几个方面：
基础设置： 最大容量 创建/校验/删除对象的回调方法 申请对象时，已满时是否阻塞 多重驱逐策略： 驱逐闲置对象 驱逐无效对象 预创建对象 为避免重复编码，所以设计了 Pond 这样一个能够支撑多种使用场景的通用对象池。另外，在 Pond 的基础上，还实现了 Goroutine 池库： Hive。</description><content:encoded>&lt;h2 id="为什么需要通用对象池"&gt;为什么需要通用对象池&lt;/h2&gt;
&lt;p&gt;在实际生产环境中，我们经常会遇到需要多线程共享同一堆对象的场景，例如常见的RPC、数据库连接池，大内存对象池，以及线程池等。这些池化对象的需求其实有很多重叠的地方，主要分以下几个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基础设置：
&lt;ol&gt;
&lt;li&gt;最大容量&lt;/li&gt;
&lt;li&gt;创建/校验/删除对象的回调方法&lt;/li&gt;
&lt;li&gt;申请对象时，已满时是否阻塞&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;多重驱逐策略：
&lt;ol&gt;
&lt;li&gt;驱逐闲置对象&lt;/li&gt;
&lt;li&gt;驱逐无效对象&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;预创建对象&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为避免重复编码，所以设计了 &lt;a href="https://github.com/joway/pond"&gt;Pond&lt;/a&gt; 这样一个能够支撑多种使用场景的通用对象池。另外，在 Pond 的基础上，还实现了 Goroutine 池库： &lt;a href="https://github.com/joway/hive"&gt;Hive&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="使用方式"&gt;使用方式&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//example&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewDefaultConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MinIdle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectCreateFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;127.0.0.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectValidateFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectDestroyFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BorrowObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;get conn: %v\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用细则"&gt;使用细则&lt;/h2&gt;
&lt;h3 id="lifo-驱逐策略"&gt;LIFO 驱逐策略&lt;/h3&gt;
&lt;p&gt;当前采用 LIFO 的驱逐策略，保证永远优先使用最常被使用的对象。之所以不采用 FIFO 是因为我们只有让热门的对象尽可能保持热门，而不是均衡每个对象的使用频率，才能够保证最大程度筛选出不常用的对象从而使其被驱逐。&lt;/p&gt;
&lt;h3 id="避免频繁驱逐"&gt;避免频繁驱逐&lt;/h3&gt;
&lt;p&gt;某些情况下会出现不停创建新的对象，到了驱逐时间又被立马销毁的情况，从而使得对象池的大小出现不必要的频繁变动。这里我们可以通过 &lt;code&gt;MinIdleTime&lt;/code&gt; 配置最小闲置时间，保证只有当对象闲置超过该时间后，才可能被驱逐。&lt;/p&gt;
&lt;h3 id="对象校验"&gt;对象校验&lt;/h3&gt;
&lt;p&gt;有一种情况是，一开始在池子里创建好了几个对象，但是当用户实际去取出来的时候，发现该对象其实已经被关闭或者失效了。所以在 Pool 内部需要每次取的对象都经过一次校验，如果校验不通过，则销毁对象，再次尝试去取。该策略还能保证当出现部分节点抖动时，会尽可能剔除不可用节点，提供稳定的对象。&lt;/p&gt;
&lt;p&gt;同时为了避免当一些灾难情况下，永远无法成功创建对象（例如下游节点完全宕机），我们还需要设置 &lt;code&gt;MaxValidateAttempts&lt;/code&gt; 以避免出现恶性循环。&lt;/p&gt;
&lt;h3 id="预创建对象"&gt;预创建对象&lt;/h3&gt;
&lt;p&gt;在默认情况下，我们会在每次取对象的时候，判断是否需要创建新的，如需要再取实时创建。但如果创建操作比较费时，我们会希望在条件允许的情况下，池子里能够预留一部分空闲对象，以供未来调用。&lt;code&gt;MinIdle&lt;/code&gt; 参数用以确保池子内最小能够拥有的空闲对象数。&lt;/p&gt;
&lt;h3 id="超时取消"&gt;超时取消&lt;/h3&gt;
&lt;p&gt;默认配置下，每次取对象如果当前池已满，且没有闲置对象，会阻塞住，直到能够获取到可用对象为止。我们使用 context 来实现获取超时取消的逻辑。一旦当触发 &lt;code&gt;ctx.Done()&lt;/code&gt; 时候，会直接 return，并返回 &lt;code&gt;ctx.Err()&lt;/code&gt; 。&lt;/p&gt;
&lt;h2 id="高级扩展"&gt;高级扩展&lt;/h2&gt;
&lt;p&gt;Pond 是一个通用对象池，在此基础上，我们可以非常简单地实现诸如连接池，goroutine 池等多重应用。&lt;/p&gt;
&lt;p&gt;以下示例展示了如何使用 Pond 创建一个简单的 goroutine 池：&lt;/p&gt;
&lt;h3 id="实现-worker-对象"&gt;实现 Worker 对象&lt;/h3&gt;
&lt;p&gt;Worker 结构体用以封装单个 goroutine：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="池化-worker-对象"&gt;池化 Worker 对象&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewDefaultConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MaxSize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Size&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Nonblocking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Nonblocking&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectCreateFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewWorker&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectDestroyFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BorrowObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//do some task&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// when task finished, return object&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title>Golang for-range 内部实现</title><link>https://blog.joway.io/posts/golang-range-internal/</link><pubDate>Wed, 20 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-range-internal/</guid><category>Tech</category><description>最近在写一个编解码的功能时发现使用 Golang for-range 会存在很大的性能问题。
假设我们现在有一个 Data 类型表示一个数据包，我们从网络中获取到了 [1024]Data 个数据包，此时我们需要对其进行遍历操作。一般我们会使用 for-i++ 或者 for-range 两种方式遍历，如下代码：
type Data [256]byte func BenchmarkForStruct(b *testing.B) { var items [1024]Data var result Data for i := 0; i &lt; b.N; i++ { for k := 0; k &lt; len(items); k++ { result = items[k] } } _ = result } func BenchmarkRangeStruct(b *testing.B) { var items [1024]Data var result Data for i := 0; i &lt; b.N; i++ { for _, item := range items { result = item } } _ = result } 输出结果：</description><content:encoded>&lt;p&gt;最近在写一个编解码的功能时发现使用 Golang &lt;code&gt;for-range&lt;/code&gt; 会存在很大的性能问题。&lt;/p&gt;
&lt;p&gt;假设我们现在有一个 &lt;code&gt;Data&lt;/code&gt; 类型表示一个数据包，我们从网络中获取到了 &lt;code&gt;[1024]Data&lt;/code&gt; 个数据包，此时我们需要对其进行遍历操作。一般我们会使用 for-i++ 或者 for-range 两种方式遍历，如下代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;BenchmarkForStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;BenchmarkRangeStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;输出结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkForStruct-8 1697805 652 ns/op
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkRangeStruct-8 60556 19837 ns/op
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到通过索引来遍历的方式要比直接使用 for-range 快了近 30 倍。&lt;/p&gt;
&lt;p&gt;索引遍历就是单纯地去访问数组的每个元素。而对于 for-range 循环，Golang 会根据迭代对象类型，已经 range 前的参数，对其进行不同形式的展开。对于以下 range 代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编译器会将其转换成如下形式（伪代码）, &lt;a href="https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/range.go#L216"&gt;range.go&lt;/a&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-golang" data-lang="golang"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ha&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 值拷贝&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;hn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 提前保存长度&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 当前遍历索引值&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 保存当前索引&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 保存当前值&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ha&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hv1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 值拷贝&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里有几点需要额外注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译器提前保存了元素长度，所以运行过程中即便长度变化，也不会影响循环次数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ha := a&lt;/code&gt; 这一步会进行一次值拷贝，这里部分情况下可能会存在性能问题 （如上面的 [256]byte 类型，每次拷贝都有很大内存开销）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;v1, v2 = hv1, ha[hv1]&lt;/code&gt; 会对数组元素进行一次值拷贝&lt;/li&gt;
&lt;li&gt;v1, v2 预先创建，地址不会改变，对应到原始代码就是 &lt;code&gt;for i, elem := range a {}&lt;/code&gt; 中的 &lt;code&gt;i, elem&lt;/code&gt; 在每次循环时，都是同一个变量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由此可以发现，当被迭代对象的元素为拷贝开销较大的类型时，使用 for-range 循环会存在很大的性能问题。此时更加建议使用标准 for 循环。&lt;/p&gt;</content:encoded></item><item><title>Golang Interface 内部实现</title><link>https://blog.joway.io/posts/golang-interface-internal/</link><pubDate>Wed, 20 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-interface-internal/</guid><category>Tech</category><description>最近遇到一个由于 Golang Interface 底层实现，引发的线上 panic 问题，虽然根源在于赋值操作没有保护起来，却意外地发现了关于 interface 的一些有意思的底层细节。
假设我们现在有以下定义：
type Interface interface { Run() } type Implement struct { n int } func (i *Implement) Run() { fmt.Printf(i.n) } 对于使用者而言，一个变量无论是 Interface 类型或是 *Implement 类型，差别都不大。</description><content:encoded>&lt;p&gt;最近遇到一个由于 Golang Interface 底层实现，引发的线上 panic 问题，虽然根源在于赋值操作没有保护起来，却意外地发现了关于 interface 的一些有意思的底层细节。&lt;/p&gt;
&lt;p&gt;假设我们现在有以下定义：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对于使用者而言，一个变量无论是 &lt;code&gt;Interface&lt;/code&gt; 类型或是 &lt;code&gt;*Implement&lt;/code&gt; 类型，差别都不大。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Interface&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//&amp;lt;nil&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//*main.Implement&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//*main.Implement&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%T\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//*main.Implement&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果现在有这么一段代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Interface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Implement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;impl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//Invalid memory address or nil pointer dereference&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码从逻辑上来说，&lt;code&gt;impl.n&lt;/code&gt; 永远都不会报空指针异常，因为 i 如果为空就会提前返回了。而且就算 i 为 nil，在 &lt;code&gt;impl := i.(*Implement)&lt;/code&gt; 类型转换的时候就会直接 panic，而不是在下一行。但在线上环境上却的确在 &lt;code&gt;impl.n&lt;/code&gt; 位置报了错误。&lt;/p&gt;
&lt;p&gt;在探究了 interface 底层实现后发现，在上面的 main 函数的例子里，i 和 p 虽然在使用方式上是一致的，但在内部存储的结构体却是不同的。&lt;code&gt;*Implement&lt;/code&gt; 类型内部存储的是一个指针，对他赋值也只是赋予一个指针。而 &lt;code&gt;Interface&lt;/code&gt; 接口底层结构却是一个类型为 &lt;code&gt;iface&lt;/code&gt; 的 struct ：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;itab&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unsafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Pointer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;itab&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;interfacetype&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// copy of _type.hash. Used for type switches.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;uintptr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// variable sized. fun[0]==0 means _type does not implement inter.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当对一个接口赋值时，即对该 struct 的 &lt;code&gt;tab&lt;/code&gt; 与 &lt;code&gt;data&lt;/code&gt; 字段分别赋值。而该操作并非是原子性的，有可能赋值到一半，也就是 &lt;code&gt;.tab&lt;/code&gt; 有值而 &lt;code&gt;.data&lt;/code&gt; 为空时，就被另一个 goroutine 抢走，并进行 &lt;code&gt;!= nil&lt;/code&gt; 的判断。而 golang 却只有在 &lt;code&gt;iface&lt;/code&gt; 两个属性同时为 nil 时候才认为是 nil，所以 check 函数内的 if 条件判断失效。&lt;/p&gt;
&lt;p&gt;同时由于 &lt;code&gt;.tab&lt;/code&gt; 内已经有了类型信息，所以在 &lt;code&gt;impl := i.(*Implement)&lt;/code&gt; 类型转换的时候也能够成功转换，并不会报空指针错误，即便该 interface 的 &lt;code&gt;.data&lt;/code&gt; 字段是 nil 。只有当实际去调用 &lt;code&gt;impl.n&lt;/code&gt; 的时候，才会发现 &lt;code&gt;.data&lt;/code&gt; 为 nil，从而 panic。&lt;/p&gt;</content:encoded></item><item><title>真理的有限性</title><link>https://blog.joway.io/posts/limited-truth/</link><pubDate>Fri, 15 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/limited-truth/</guid><category>Thought</category><description>机器世界与真实世界的差异在于，机器世界的法律是约定能够做什么，而真实世界的法律是约定不能够做什么。编程是在一个有限中变化出无限，而生活是在无限中寻找到自己的有限。机器世界本身就是真实世界中的一个小小的有限。
人们总试图从一个无限的世界里去发寻规律，以其获得一条在有限领域能够稳定可靠的真理。无论是科学甚至玄学，概莫如是。</description><content:encoded>&lt;p&gt;机器世界与真实世界的差异在于，机器世界的法律是约定能够做什么，而真实世界的法律是约定不能够做什么。编程是在一个有限中变化出无限，而生活是在无限中寻找到自己的有限。机器世界本身就是真实世界中的一个小小的有限。&lt;/p&gt;
&lt;p&gt;人们总试图从一个无限的世界里去发寻规律，以其获得一条在有限领域能够稳定可靠的真理。无论是科学甚至玄学，概莫如是。&lt;/p&gt;
&lt;p&gt;计算机领域有一本书叫《代码大全》，里面总结了要写好代码需要遵循的一些「真理」。但靠这些真理要写好代码还是非常之难。机器世界是有限的，但机器运行在无限的世界之上。真实世界的复杂性，会一次次冲击这本书上的一条条金科玉律 —— 倒不是说规律不重要，而是说规律有其局限之处。&lt;/p&gt;
&lt;p&gt;为什么会存在知易行难？真理本身往往极为朴素简单，但总存在它的有限作用域，或者说正因其有限所以才能够简单。 而现实世界是无数真理共同存在并且碰撞的集合体。践行了一条真理，很可能违背了另一条。人与人之间之所以有区别，在于每个人在不同情况下所做出的选择不同。每个人的肉体都是一模一样的细胞构成，谁也不比谁独特 —— 这是在当下此刻而言，但在时间的维度上，是我们从诞生至今所做出的全部选择建构了我们自身独一无二的存在。&lt;/p&gt;
&lt;p&gt;网络上经常有很多激烈的争论，我时常设身处地地想，好像每个人都挺有道理。这种设身处地的理解和共情就是在寻找到这条道理所适用的作用域，直到能够觉得「有道理」为止。即便是喜爱杀人，在抗日战争的年代也算是一个优良品德，至少是对国家民族有帮助的品德 —— 对中国兵对日本兵都是。所谓的争论，往往就是彼此拿对方作用域外的场景，去跑一遍对方道理，当抛出 exception 的时候，以此试图去驳倒对方。而所谓的反驳，要么故伎再重施一遍，要么就将自己的道理压缩到更小的作用域上。&lt;/p&gt;
&lt;p&gt;论语之所以有如此长盛不衰的生命力，正是由于其是一个日常言谈记录集，而不是一本约定做人道理的法典。孔子讲的道理大多都有其上下文，这些上下文就约定了这条道理所适用的有限作用域。试想如果抛开上下文，直接来一句“老而不死，是为贼也”，恐怕纳粹也要让其三分。&lt;/p&gt;
&lt;p&gt;在一个无限的世界里，最舒适的方式就是选定了一个象限后，在这个象限里做一个有限的人，永远不为这个象限外的理论所动摇。这也是大部分的实际现状，有些人是从未看见过象限外的世界，有些人是看到后受了伤，选择当作没看见。而做一个无限的人很难，需要不断去学习，不断去共情，当看见的世界越来越大，自我就会越来越小，直至消失。&lt;/p&gt;
&lt;p&gt;想明白了这点会发现，真理并没有什么争论的意义，真理是一种选择，争论如果有意义，也是让人意识到原来可以选择，以及帮助人更清楚地去选择，如同马丁路德所做的那样 —— 提供一种另一种真理，但并不能说是提供了一种更好的真理。所有人都有选择的自由，但没有强迫别人如何选择的权力。只可惜这世间争论的目的往往在于后者。&lt;/p&gt;</content:encoded></item><item><title>Golang rand 库锁竞争优化</title><link>https://blog.joway.io/posts/golang-fastrand/</link><pubDate>Thu, 17 Dec 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-fastrand/</guid><category>Tech</category><description>背景 最近在实现一个随机负载均衡器的时候发现一个问题，在高并发的情况下，官方标准库 rand.Intn() 性能会急剧下降。翻了下实现以后才发现它内部居然是全局共享了同一个 globalRand 对象。
一段测试代码：
func BenchmarkGlobalRand(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { rand.Intn(100) } }) } func BenchmarkCustomRand(b *testing.B) { b.RunParallel(func(pb *testing.PB) { rd := rand.New(rand.NewSource(time.Now().Unix())) for pb.Next() { rd.Intn(100) } }) } 输出：</description><content:encoded>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;最近在实现一个随机负载均衡器的时候发现一个问题，在高并发的情况下，官方标准库 &lt;code&gt;rand.Intn()&lt;/code&gt; 性能会急剧下降。翻了下实现以后才发现它内部居然是全局共享了同一个 &lt;a href="https://github.com/golang/go/blob/master/src/math/rand/rand.go#L293"&gt;globalRand&lt;/a&gt; 对象。&lt;/p&gt;
&lt;p&gt;一段测试代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;BenchmarkGlobalRand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunParallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;BenchmarkCustomRand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunParallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Unix&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;输出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;BenchmarkGlobalRand&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;BenchmarkGlobalRand&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18075486&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;66.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;BenchmarkCustomRand&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;BenchmarkCustomRand&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;423686118&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.38&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="解决思路"&gt;解决思路&lt;/h2&gt;
&lt;p&gt;最理想对情况是可以在每个 goroutine 内创建一个私有的 rand.Rand 对象，从而实现真正的无锁。&lt;/p&gt;
&lt;p&gt;但在很多其他场景下，我们并不能直接控制调用我们的 goroutine，又或者 goroutine 数量过多以至于无法承受这部分内存成本。&lt;/p&gt;
&lt;p&gt;此时的一个思路是使用 &lt;code&gt;sync.Pool&lt;/code&gt; 来为 &lt;code&gt;rand.Source&lt;/code&gt; 创建一个池，当多线程并发读写时，优先从自己当前 P 中的 poolLocal 中获取，从而减少锁的竞争。同时由于只是用 pool 扩展了原生的 rngSource 对象，所以可以兼容其 rand.Rand 下的所有接口调用。&lt;/p&gt;
&lt;p&gt;基于这个思路，实现了一个 &lt;a href="https://github.com/joway/fastrand"&gt;fastrand&lt;/a&gt; 库放到了 github。&lt;/p&gt;
&lt;p&gt;从 benchmark 中可以看到性能提升显著，在并发条件下，比原生全局 rand 快了大约 8 倍.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkStandardRand
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkStandardRand-8 60870416 19.1 ns/op 0 B/op 0 allocs/op
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkFastRand
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkFastRand-8 100000000 10.7 ns/op 0 B/op 0 allocs/op
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkStandardRandWithConcurrent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkStandardRandWithConcurrent-8 18058663 67.8 ns/op 0 B/op 0 allocs/op
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkFastRandWithConcurrent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BenchmarkFastRandWithConcurrent-8 132542940 8.79 ns/op 0 B/op 0 allocs/op
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title>Meaningless or Meaningful</title><link>https://blog.joway.io/posts/meaningless-or-meaningful/</link><pubDate>Wed, 07 Oct 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/meaningless-or-meaningful/</guid><category>Thought</category><description>边走边想 小时候在百度知道里提过一个问题，「人活着的意义是什么」。靠这个问题赚了一些百度积分，可并没能消除我当时的困惑。长大后在王兴的一个访谈里，看到他在大学同乡迎新会的时候问了学长们一样的问题，当时他姐刚好也在，为了打破尴尬不得不模糊地回答了一句：
“这个问题，要边走边想。”
Nothing But Yourself 在播客《阿米小酒馆 - 【op.23】两个写作者的困顿与坚定》 中听到复旦民间校训「自由而无用」的英文翻译原来是「nothing but yourself」，瞬间被这句话击中。</description><content:encoded>&lt;h2 id="边走边想"&gt;边走边想&lt;/h2&gt;
&lt;p&gt;小时候在百度知道里提过一个问题，「人活着的意义是什么」。靠这个问题赚了一些百度积分，可并没能消除我当时的困惑。长大后在王兴的一个&lt;a href="https://www.bilibili.com/video/BV1yE411K7yi?t=1847"&gt;访谈&lt;/a&gt;里，看到他在大学同乡迎新会的时候问了学长们一样的问题，当时他姐刚好也在，为了打破尴尬不得不模糊地回答了一句：&lt;/p&gt;
&lt;p&gt;“这个问题，要边走边想。”&lt;/p&gt;
&lt;h2 id="nothing-but-yourself"&gt;Nothing But Yourself&lt;/h2&gt;
&lt;p&gt;在播客&lt;a href="%E3%80%90op.23%E3%80%91%E4%B8%A4%E4%B8%AA%E5%86%99%E4%BD%9C%E8%80%85%E7%9A%84%E5%9B%B0%E9%A1%BF%E4%B8%8E%E5%9D%9A%E5%AE%9A"&gt;《阿米小酒馆 - 【op.23】两个写作者的困顿与坚定》&lt;/a&gt; 中听到复旦民间校训「自由而无用」的英文翻译原来是「nothing but yourself」，瞬间被这句话击中。&lt;/p&gt;
&lt;p&gt;电影《夺冠》里，海外归来的郎平反复询问中国女排新一代队员：你喜欢排球吗？&lt;/p&gt;
&lt;p&gt;看到一本关于父亲给孩子寄语的绘本，前面是稀松平常的美好愿望，但最后一页写着：Dad wants you to be yourself。&lt;/p&gt;
&lt;h2 id="who-you-are"&gt;Who you are&lt;/h2&gt;
&lt;p&gt;无论是微博、即刻、推特都会有一栏个人简介，大部份人的简介里最常见的标签就是其在社会上所处的职业，甚至还有其过往的职业。所有社交媒体似乎都只是 Linkedin 的一个子栏目。&lt;/p&gt;
&lt;p&gt;上海最近有一个木心的美术展，展厅出口处有一张木心的自撰年表：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/meaningless-or-meaningful/muxin_hu_193b64f61894be6f.webp" srcset="https://blog.joway.io/images/meaningless-or-meaningful/muxin_hu_95ccaf45cc65fb88.webp 800w, https://blog.joway.io/images/meaningless-or-meaningful/muxin_hu_fc11a2c416223507.webp 1200w, https://blog.joway.io/images/meaningless-or-meaningful/muxin_hu_193b64f61894be6f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="2133" alt="Meaningless or Meaningful" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这是木心的 Linkedin Profile，57岁之前，他是教师，囚犯，美工，但「Who you are」这个问题的答案不在他的职业中，而在他的作品里。&lt;/p&gt;
&lt;h2 id="墓碑"&gt;墓碑&lt;/h2&gt;
&lt;p&gt;你现在最希望拥有什么，你最希望自己的墓碑上刻着什么。&lt;/p&gt;
&lt;p&gt;我希望能够在上海落户，买房，成为一个不错的丈夫和父亲。但我不会在墓碑上刻上「新上海人」，「中产家庭主」，这些是我的努力而非成就。&lt;/p&gt;
&lt;p&gt;我在努力成为一些闪亮标签中的一员，但我希望在墓碑上刻上自己，而非标签。&lt;/p&gt;
&lt;h2 id="回老家盖别墅"&gt;回老家，盖别墅&lt;/h2&gt;
&lt;p&gt;和朋友谈到家乡的老人会有一股回老家盖别墅的情结。如今的农村老家早已没有了人烟，老人也迁居到了城镇，但农村依旧能够看到不断新起的别墅，像是给一个个老人画上圆满人生的句点。&lt;/p&gt;
&lt;p&gt;每代人都有其无法突破的思想钢印，每代人总是试图结合时代的热点，赋予自身存在的意义。&lt;/p&gt;
&lt;p&gt;那么我们这一代人心里的那个别墅又是什么。&lt;/p&gt;
&lt;h2 id="自由意志"&gt;自由意志&lt;/h2&gt;
&lt;p&gt;没有丝毫科学的理由可以相信人拥有绝对的意志自由。风吹树或许会倒，或许不会，意识也是一种物质碰撞下的产物，甚至也是意识间互相碰撞的产物。&lt;/p&gt;
&lt;p&gt;科学规律是限制一切物质自由的枷锁，组合使物质产生多样性，当多样性达到一定程度的丰富度，带给了人一种自由的错觉。&lt;/p&gt;
&lt;p&gt;这个世界上不存在两片一模一样的树叶，但每一片树叶都未曾获得片刻自由。&lt;/p&gt;
&lt;h2 id="平面巴别塔"&gt;平面巴别塔&lt;/h2&gt;
&lt;p&gt;两年前做过一个梦，梦里世界是一个无穷的平面，所有人都在不停向上跳跃，时间在一点点流逝，不断有人跳出不寻常的高度，他们是牛顿，贝多芬，毕加索 &amp;hellip;&amp;hellip;&lt;/p&gt;
&lt;p&gt;这是一个平面的巴别塔，个人主义的巴别塔。巴别塔通天，但是天上的又是什么。&lt;/p&gt;
&lt;h2 id="pray-to-fire"&gt;Pray to Fire&lt;/h2&gt;
&lt;p&gt;在伊朗设拉子和当地人聊天，谈到亚兹德的拜火教时，我不知道这个教的英文单词是什么，所以说了一句“the people who still pray to fire”，对方赶忙纠正我说，“they are not praying to fire, they are praying to god with fire”。这句话非常简单，却同时点明了一神信仰和反对偶像崇拜这两个最核心的宗教观点。&lt;/p&gt;
&lt;p&gt;一神与多神的区别是什么？多神论者赋予神以意义，一神论者祈求神赋予意义于自身。而反偶像崇拜就是在将意义的赋予权交由上帝。&lt;/p&gt;
&lt;h2 id="everything-is-meaningless"&gt;Everything Is Meaningless&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://shengjing.jidujiao.com/Ecclesiastes_21_1_en.html"&gt;旧约 &amp;ndash; 传道书(Ecclesiastes) &amp;ndash; 第 1 章&lt;/a&gt;有一段这样的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1:1 在耶路撒冷作王，大卫的儿子，传道者的言语。&lt;/p&gt;
&lt;p&gt;The words of the Teacher, son of David, king in Jerusalem:&lt;/p&gt;
&lt;p&gt;1:2 传道者说：虚空的虚空，虚空的虚空，凡事都是虚空。&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Meaningless! Meaningless!&amp;rdquo; says the Teacher. &amp;ldquo;Utterly meaningless! Everything is meaningless.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;1:3 人一切的劳碌，就是他在日光之下的劳碌，有什么益处呢？&lt;/p&gt;
&lt;p&gt;What does man gain from all his labor at which he toils under the sun?&lt;/p&gt;
&lt;p&gt;1:4 一代过去，一代又来，地却永远长存。&lt;/p&gt;
&lt;p&gt;Generations come and generations go, but the earth remains forever.&lt;/p&gt;
&lt;p&gt;1:5 日头出来，日头落下，急归所出之地。&lt;/p&gt;
&lt;p&gt;The sun rises and the sun sets, and hurries back to where it rises.&lt;/p&gt;
&lt;p&gt;1:6 风往南刮，又向北转，不住地旋转，而且返回转行原道。&lt;/p&gt;
&lt;p&gt;The wind blows to the south and turns to the north; round and round it goes, ever returning on its course.&lt;/p&gt;
&lt;p&gt;1:7 江河都往海里流，海却不满；江河从何处流，仍归还何处。&lt;/p&gt;
&lt;p&gt;All streams flow into the sea, yet the sea is never full. To the place the streams come from, there they return again.&lt;/p&gt;
&lt;p&gt;1:8 万事令人厌烦（或作“万物满有困乏”），人不能说尽。眼看，看不饱；耳听，听不足。&lt;/p&gt;
&lt;p&gt;All things are wearisome, more than one can say. The eye never has enough of seeing, nor the ear its fill of hearing.&lt;/p&gt;
&lt;p&gt;1:9 已有的事，后必再有；已行的事，后必再行。日光之下，并无新事。&lt;/p&gt;
&lt;p&gt;What has been will be again, what has been done will be done again; there is nothing new under the sun.&lt;/p&gt;
&lt;p&gt;1:10 岂有一件事人能指着说这是新的？哪知，在我们以前的世代早已有了。&lt;/p&gt;
&lt;p&gt;Is there anything of which one can say, &amp;ldquo;Look! This is something new&amp;rdquo;? It was here already, long ago; it was here before our time.&lt;/p&gt;
&lt;p&gt;1:11 已过的世代，无人记念；将来的世代，后来的人也不记念。&lt;/p&gt;
&lt;p&gt;There is no remembrance of men of old, and even those who are yet to come will not be remembered by those who follow.&lt;/p&gt;
&lt;p&gt;1:12 我传道者在耶路撒冷作过以色列的王。&lt;/p&gt;
&lt;p&gt;I, the Teacher, was king over Israel in Jerusalem.&lt;/p&gt;
&lt;p&gt;1:13 我专心用智慧寻求查究天下所作的一切事，乃知神叫世人所经练的是极重的劳苦。&lt;/p&gt;
&lt;p&gt;I devoted myself to study and to explore by wisdom all that is done under heaven. What a heavy burden God has laid on men!&lt;/p&gt;
&lt;p&gt;1:14 我见日光之下所作的一切事，都是虚空，都是捕风。&lt;/p&gt;
&lt;p&gt;I have seen all the things that are done under the sun; all of them are meaningless, a chasing after the wind.&lt;/p&gt;
&lt;p&gt;1:15 弯曲的不能变直，缺少的不能足数。&lt;/p&gt;
&lt;p&gt;What is twisted cannot be straightened; what is lacking cannot be counted.&lt;/p&gt;
&lt;p&gt;1:16 我心里议论说，我得了大智慧，胜过我以前在耶路撒冷的众人，而且我心中多经历智慧和知识的事。&lt;/p&gt;
&lt;p&gt;I thought to myself, &amp;ldquo;Look, I have grown and increased in wisdom more than anyone who has ruled over Jerusalem before me; I have experienced much of wisdom and knowledge.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;1:17 我又专心察明智慧、狂妄和愚昧，乃知这也是捕风。&lt;/p&gt;
&lt;p&gt;Then I applied myself to the understanding of wisdom, and also of madness and folly, but I learned that this, too, is a chasing after the wind.&lt;/p&gt;
&lt;p&gt;1:18 因为多有智慧，就多有愁烦；加增知识的，就加增忧伤。&lt;/p&gt;
&lt;p&gt;For with much wisdom comes much sorrow; the more knowledge, the more grief.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="everything-is-meaningful"&gt;Everything Is Meaningful&lt;/h2&gt;
&lt;p&gt;苏轼在《赤壁赋》中写道：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;苏子愀然，正襟危坐，而问客曰：“何为其然也？”客曰：“月明星稀，乌鹊南飞，此非曹孟德之诗乎？西望夏口，东望武昌。山川相缪，郁乎苍苍；此非孟德之困于周郎者乎？方其破荆州，下江陵，顺流而东也，舳舻千里，旌旗蔽空，酾酒临江，横槊赋诗；固一世之雄也，而今安在哉？况吾与子，渔樵于江渚之上，侣鱼虾而友糜鹿，驾一叶之扁舟，举匏樽以相属；寄蜉蝣与天地，渺沧海之一粟。哀吾生之须臾，羡长江之无穷；挟飞仙以遨游，抱明月而长终；知不可乎骤得，托遗响于悲风。”&lt;/p&gt;
&lt;p&gt;苏子曰：“客亦知夫水与月乎？逝者如斯，而未尝往也；盈虚者如彼，而卒莫消长也。盖将自其变者而观之，而天地曾不能一瞬；自其不变者而观之，则物于我皆无尽也。而又何羡乎？且夫天地之间，物各有主。苟非吾之所有，虽一毫而莫取。惟江上之清风，与山间之明月，耳得之而为声，目遇之而成色。取之无禁，用之不竭。是造物者之无尽藏也，而吾与子之所共适。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;苏轼的回答恰好与圣经里所言相呼应。&lt;/p&gt;
&lt;p&gt;前天半夜被皮肤痒醒，学习了下痒形成的原理，买了抗组胺药，几分钟后便症状消褪，于是又研究了下为什么口服药竟能如此快速作用于背部的皮肤，接着发现了&lt;a href="https://zh.wikipedia.org/wiki/%E8%8D%AF%E7%89%A9%E4%BB%A3%E8%B0%A2%E5%8A%A8%E5%8A%9B%E5%AD%A6"&gt;「药代动力学」&lt;/a&gt;这门学科。此非苏轼所言，「取之无禁，用之不竭。是造物者之无尽藏也，而吾与子之所共适」？&lt;/p&gt;
&lt;p&gt;个体的存在对于造物主也许的确是无意义的，但对于个体自身而言，个体存在的意义寄托于万物之存在，以须臾之吾生穷长江之无穷，继而化为江上之清风与山间之明月，如此往复。&lt;/p&gt;
&lt;p&gt;Everything is meaningless, everything is meaningful.&lt;/p&gt;</content:encoded></item><item><title>分布式文件系统的演化</title><link>https://blog.joway.io/posts/deep-into-distributed-filesystem/</link><pubDate>Sun, 14 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-distributed-filesystem/</guid><category>Tech</category><description>文件系统是操作系统 IO 栈里非常重要的一个中间层，其存在的意义是为了让上层应用程序有一层更加符合人类直觉的抽象来进行文档的读写，而无需考虑底层存储上的细节。
本地文件系统 在讨论分布式文件系统前，我们先来回顾下本地文件系统的组成。
存储结构 在前面一张图里，我们能够看到文件系统直接和通用块层进行交互，无论底层存储介质是磁盘还是 SSD，都被该层抽象为 Block 的概念。文件系统在初始化时，会先在挂载的块存储上的第一个位置创建一个 Super Block：</description><content:encoded>&lt;p&gt;文件系统是操作系统 IO 栈里非常重要的一个中间层，其存在的意义是为了让上层应用程序有一层更加符合人类直觉的抽象来进行文档的读写，而无需考虑底层存储上的细节。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/io-layers_hu_c92a5b3892409576.webp" srcset="https://blog.joway.io/images/distributed-filesystem/io-layers_hu_1b4463119098a164.webp 800w, https://blog.joway.io/images/distributed-filesystem/io-layers_hu_4a09baa80ce1e695.webp 1200w, https://blog.joway.io/images/distributed-filesystem/io-layers_hu_c92a5b3892409576.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1170" alt="分布式文件系统的演化" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="本地文件系统"&gt;本地文件系统&lt;/h2&gt;
&lt;p&gt;在讨论分布式文件系统前，我们先来回顾下本地文件系统的组成。&lt;/p&gt;
&lt;h3 id="存储结构"&gt;存储结构&lt;/h3&gt;
&lt;p&gt;在前面一张图里，我们能够看到文件系统直接和通用块层进行交互，无论底层存储介质是磁盘还是 SSD，都被该层抽象为 &lt;strong&gt;Block&lt;/strong&gt; 的概念。文件系统在初始化时，会先在挂载的块存储上的第一个位置创建一个 &lt;strong&gt;Super Block&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/filesystem-block_hu_8123f48b68da72df.webp" srcset="https://blog.joway.io/images/distributed-filesystem/filesystem-block_hu_efed01a397cd133d.webp 800w, https://blog.joway.io/images/distributed-filesystem/filesystem-block_hu_8123f48b68da72df.webp 960w" sizes="(max-width: 800px) 100vw, 800px" width="960" height="243" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;上图右边部分就是一块完整的存储，可以将其想象成一个数组。&lt;/p&gt;
&lt;p&gt;Super Block 中存储了该文件系统的信息，其组成部分如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Magic: &lt;code&gt;MAGIC_NUMBER&lt;/code&gt; or &lt;code&gt;0xf0f03410&lt;/code&gt; ，用来告诉操作系统该磁盘是否已经拥有了一个有效的文件系统。&lt;/li&gt;
&lt;li&gt;Blocks: blocks 总数&lt;/li&gt;
&lt;li&gt;InodeBlocks: 其中属于 inode 的 block 数&lt;/li&gt;
&lt;li&gt;Inodes: 在 InodeBlocks 中存在多少个 inodes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于这里的 Blocks 总数、InodeBlocks 总数、每个 Block 的大小在文件系统创建时就已经固定，所以一般来说一个文件系统能够创建的文件数量在一开始就已经固定了。&lt;/p&gt;
&lt;p&gt;Linux 中每个文件都拥有一个唯一的 Inode，其结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/inode_hu_a6ce48adf2e60b8d.webp" width="500" height="271" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;inode 上半部分的 meta data 很容易理解，下半部分的 block 指针的含义分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;direct block pointer: 直接指向 data block 的地址&lt;/li&gt;
&lt;li&gt;indirect block: 指向 direct block&lt;/li&gt;
&lt;li&gt;double indirect block: 指向 indirect block&lt;/li&gt;
&lt;li&gt;triple indirect block: 指向 double indirect block&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于一个 inode 大小固定，所以这里的 block pointers 数量也是固定的，进而单个文件能够占用的 data block 大小也是固定的，所以一般文件系统都会存在最大支持的文件大小。&lt;/p&gt;
&lt;p&gt;我们以 ext3 文件系统为例，其 &lt;a href="https://github.com/torvalds/linux/blob/master/fs/ext4/ext4.h#L394"&gt;superblock 定义 &lt;/a&gt;中有 12 个 direct block pointers，1 个 indirect block，1 个 double indirect block，1 个 triple indirect block( ext3/ext4 相同)。假设一个 block pointers 的大小为 &lt;a href="https://lwn.net/Articles/187321/#:~:text=The%20ext3%20inode%20structure%20contains,up%20to%2048KB%20in%20length."&gt;4 bytes&lt;/a&gt;，则单个文件 inode 能够存储的最大 data blocks 大小(即最大文件大小)为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(12 + (block_size_bytes/4)^1 + (block_size_bytes/4)^2 + (block_size_bytes/4)^3) * block_size_bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当 block_size_bytes == 1024 时，最大文件大小为 16 GiB。但当 block_size_bytes == 4096 时，虽然上述公式值为 4 TiB，但由于 ext3 文件系统对单个 inode 上的 blocks 数量&lt;code&gt;i_blocks&lt;/code&gt; 的类型为 &lt;a href="https://github.com/spotify/linux/blob/6eb782fc88d11b9f40f3d1d714531f22c57b39f9/include/linux/ext3_fs.h#L298"&gt;__le32&lt;/a&gt; 即 &lt;a href="https://github.com/torvalds/linux/blob/master/tools/include/linux/types.h#L56"&gt;__u32&lt;/a&gt; ，所以单个文件的 blocks 数不能 &amp;gt; &lt;code&gt;2^32-1&lt;/code&gt; 个，且这里 i_blocks 表示的 block 指的是扇区而非前面说得逻辑块，其大小被固定为 &lt;a href="https://github.com/torvalds/linux/blob/fe7fdc37b5/fs/ext3/super.c#L1436"&gt;512 bytes&lt;/a&gt;，所以文件大小不能 &amp;gt; &lt;code&gt;512 * (2^32 - 1)&lt;/code&gt; ，即约等于2 TiB。扇区是过去磁盘时代的概念，在 SSD 中虽然不存在扇区的概念，但为了兼容旧软件生态，它会提供一个假的扇区值，一般为 4KB。但由于 ext3 该值是写死在&lt;a href="https://github.com/torvalds/linux/blob/fe7fdc37b5/fs/ext3/super.c#L1452"&gt;代码&lt;/a&gt;中的，所以即便是 SSD 也存在该限制。&lt;/p&gt;
&lt;h2 id="分布式文件系统的演化"&gt;分布式文件系统的演化&lt;/h2&gt;
&lt;p&gt;如果我们希望用户对文件的读写操作都通过网络进行而不是本地，以实现多台机器间共享文件状态，通过图1的 IO 流程不难发现，我们只要在文件系统层将其 IO 操作转发给网络上的存储节点而不是本地通用块层，我们就能在应用程序无感知的情况下实现一个分布式文件系统。&lt;/p&gt;
&lt;p&gt;结合之前的本地文件系统流程，我们可以把分布式文件系统中的数据访问模式分为两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检索文件 Metadata (在本地文件系统中即 inode )，找出文件内容存放地址 (小文件)&lt;/li&gt;
&lt;li&gt;根据地址读取文件内容 (小文件/大文件)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下文介绍的所有分布式文件系统也都是在这两步上做主要取舍和优化，以适应不同应用场景。&lt;/p&gt;
&lt;h3 id="gfs"&gt;GFS&lt;/h3&gt;
&lt;p&gt;GFS 是 google 最早为解决其爬虫抓取的网页文件过多而设计的分布式文件系统。&lt;/p&gt;
&lt;p&gt;架构相似的文件系统还有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HDFS(开源版实现)&lt;/li&gt;
&lt;li&gt;TFS: Taobao FileSystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="设计目标"&gt;设计目标&lt;/h4&gt;
&lt;p&gt;GFS 的设计目标是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容忍机器/磁盘故障 (component failures are the norm)&lt;/li&gt;
&lt;li&gt;面向大文件设计 (Multi-GB files are common)&lt;/li&gt;
&lt;li&gt;适用于 append 多于 overwrite 的场景&lt;/li&gt;
&lt;/ol&gt;
&lt;h6 id="架构"&gt;架构&lt;/h6&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/gfs_hu_281aa0e8cff01bd6.webp" srcset="https://blog.joway.io/images/distributed-filesystem/gfs_hu_4cc3cd57e770dbbb.webp 800w, https://blog.joway.io/images/distributed-filesystem/gfs_hu_281aa0e8cff01bd6.webp 1126w" sizes="(max-width: 800px) 100vw, 800px" width="1126" height="508" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;从上图我们可以看出 GFS 的设计思路：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于读请求&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client 先向 master 节点请求 (filename, chunk index) 元组对应的 (chunk handle, chunk locations)&lt;/li&gt;
&lt;li&gt;再向 chuckserver 请求 (chunk handle, byte range)&lt;/li&gt;
&lt;li&gt;chunkserver 返回给 client chuck data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于设计目标是大文件场景，所以 client 端不会缓存 chunk data，但是会缓存 metadata。而对于 chuckserver 而言，其文件缓存利用了 Linux 自带的 buffer cache。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于写请求&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/gfs-write_hu_82eb3900dc978c0b.webp" width="620" height="570" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;client 向 Master 请求持有 lease 的 chunk（primary replica）位置和其他 replicas 的位置&lt;/li&gt;
&lt;li&gt;Master 返回位置信息，client 将这些信息缓存起来&lt;/li&gt;
&lt;li&gt;client 将数据发送到所有的 replicas，每个 chunkserver 会把数据存在 LRU 缓存中&lt;/li&gt;
&lt;li&gt;在所有的 replicas 都收到了数据之后，client 会向 primary 发送写请求。&lt;/li&gt;
&lt;li&gt;primary 会首先给该写入操作分配一个编号，确保所有写入操作都有一个唯一的顺序，且该顺序在所有 secondaries 上也是一致的。然后给 secondaries 发送写请求。&lt;/li&gt;
&lt;li&gt;secondaries 告知 primary 操作执行完毕&lt;/li&gt;
&lt;li&gt;primary 向 client 应答，期间的错误也会发送给 client&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="一致性保证"&gt;一致性保证&lt;/h4&gt;
&lt;p&gt;对于 metadata 的信息修改一定是一致的，因为 master 是一个单一主节点架构。但对于chuckserver上的写操作在不同情况下有不同的表现：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/gfs-consistency_hu_2195a1fca4e4c4a1.webp" width="638" height="296" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;上图名词的解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Write 表示 overwrite 类型的写，即指定在 [offset, bytes_length + offset] 范围内的写入操作。而 Append 表示追加类型的写，不需要指定 offset 可直接执行写入。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;defined 表示写完以后再读，读到的一定是写的内容的定义结果，undefined 表示未定义行为&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;consistent 表示多个副本的内容是一致的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 &lt;strong&gt;overwrite&lt;/strong&gt; 类型的写入来说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;顺序写时，毫无疑问可以得到确定的结果和一致性。&lt;/li&gt;
&lt;li&gt;并发写时，不同的执行顺序会产生不同的结果，而执行顺序完全取决于 primary 收到请求的前后，所以对于每个 client 来说，并不能保证操作成功后的结果就是自己刚才的写入。但是由于 primary 在决定顺序，所以每个副本本身数据还是一致的。但如果并发写时，某个 secondary 写入失败了，那么就会产生副本不一致的情况，此时需要客户端自己处理这类错误，重新发起写入请求，直到成功。GFS 自身并不保证这种情况的一致性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 &lt;strong&gt;append&lt;/strong&gt; 类型的写入来说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;无论是顺序写入还是并发写入，都会遇到中间某个操作执行失败的情况，此时都会需要客户端重试。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;append 操作并不是幂等的，所以在失败重试的时候，会在上一步执行结果基础上进行追加，导致上一个没有执行成功的副本数据出现一个断层(对于断层直接填充空值):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;writes:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. append 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. append 2(failed in chunkserver2)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. append 2(retry)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;==&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chuckserver1 (primary)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1,2,2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chunkserver2 (secondary)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1,*,2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对于上述情况，chuckserver1 和 chuckserver2 虽然在中间位置的数据不一致，但重试成功后，数据依然都写入了（at least once），所以依旧是定义行为。而这种数据不一致的情况，需要客户端自己事先知晓，且在客户端侧进行数据重复的处理（在 SDK 层做统一过滤）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上述描述中我们不难发现，GFS 的实现奉行「重客户端轻服务端」思想，把许多原先需要服务端做的校验和保证都交由客户端实现，服务端只做最基本的工作，这种设计思想可以让服务端的实现更加简洁和稳定。&lt;/p&gt;
&lt;h4 id="缺陷"&gt;缺陷&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;一个小文件可能会被分配到单一个 chuck 上，从而导致出现读写的热点。&lt;/li&gt;
&lt;li&gt;大量小文件的读写成了随机读写，性能很差&lt;/li&gt;
&lt;li&gt;设计假设是通过增大 chunk 的 size 从而降低文件数目大小，以减轻 master 节点的负载压力，但实际情况依旧会最终出现文件数目过大的情况。&lt;/li&gt;
&lt;li&gt;master 的单点架构容易让 master 自身成为瓶颈&lt;/li&gt;
&lt;li&gt;应用层的一致性保证较差，需要客户端做太多判断&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="colossus-gfs-20"&gt;Colossus: GFS 2.0&lt;/h3&gt;
&lt;p&gt;由于后来 Google 内部随着规模越来越大，单点的 master 也逐渐支撑不住巨大的集群规模，Google 又研发了新的 Colossus File System。但关于该系统的设计还未公开，网上介绍并不多，这里只从能够找到的资料里来一探究竟。&lt;/p&gt;
&lt;h4 id="架构-1"&gt;架构&lt;/h4&gt;
&lt;p&gt;Colossus 的设计思路是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;水平扩展：重新设计 Metadata 的数据结构，使 Master 节点扩展成一个分布式的架构&lt;/li&gt;
&lt;li&gt;垂直嵌套：让同一架构垂直嵌套以实现更大的扩展能力&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从本地文件系统中的 inode 设计我们可以看到，对于 Metadata 我们是以一颗树的方式在进行存储的，而树这种数据结构是不太容易进行拆分以实现分布式的。所以第一步是将树的结构变成一个 key-value 的结构：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/tablefs-kv_hu_55a8d25b86c71e9c.webp" width="646" height="488" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;对于 key-value 结构，有非常多的数据结构可以选择，例如 LSM Tree，而且这些结构都可以非常易于进行分布式管理。而对于 Google 来说，现成的 Key-Value 存储就是 BigTable。但问题是 BigTable 的实现其实是基于分布式文件系统也就是之前的 GFS 的。这就导致了一个循环依赖问题。&lt;/p&gt;
&lt;p&gt;所幸的是，对于文件系统的 Metadata 存储而言，有一个非常独特的特性，那就是「规模递减」。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FileSystem(1000 TiB)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;==&amp;gt; Metadata(10 TiB) + Chunk(990 Tib)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Metadata(10 TiB) ==&amp;gt; FileSystem(10 Tib) = Metadata(0.1 Tib) + Chunk(9.9 Tib)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;即根据：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FileSystem(X Tib) = Metadata(0.01X Tib) + Chunk(0.99X Tib)&lt;/li&gt;
&lt;li&gt;Metadata(Y Tib) = FileSystem(Y Tib)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以：
&lt;strong&gt;FileSystem(X Tib) = FileSystem(0.01X Tib) + Chunk(0.99X Tib)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由此我们发现一个大的文件系统一定能够由一个更小的文件系统加上 ChunkServer 集群搭建起来，这也是 Colossus 设计的核心思想。而规模被层层缩小到最后时，我们就可以将其用一个最简单的强一致的分布式存储系统来作为最后的 BigTable 的存储系统，例如 &lt;strong&gt;Chubby&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/colossus_hu_10ec74b8d20e8501.webp" width="734" height="600" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;前面我们已经将 Metadata 数据变成了 Key-Value 结构，并且这里的 BIgTable 底层数据结构为 LSM Tree。而 LSM Tree 的特点就是将数据的写操作都转换为了顺序写入，从而大大提升了写的性能。而我们 GFS 那边又讲了，对于一个大文件的顺序写入，只有在跨越(创建)了新的chunk时，我们才需要和 Master 节点进行通信，所以这里落在 BigTable 上的读写请求其实是非常少的，从而也进一步降低了对最底层 Chubby 的压力。我们这里用一个推导过程来解释这个架构的强大之处：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- W(x,y) 表示 client 端的写入
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- M(x,y) 表示 Metadata 节点(即 BigTable)的写入次数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- C(x) 表示 ChunkServer 的写入次数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- 上面的 x 表示写入次数，y 表示每次写入大小
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- chunk 块大小为 B
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- 单条 Chunk 的 Metadata 大小为 m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;W(N,S) =&amp;gt; M(N*S/B,m) + C(N)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;M(N*S/B,m)=W(N*S/B,m)=&amp;gt; M(N*S/B*m/B) + C(N*S/B)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从上述推导可以看出，架构嵌套层数越深，最终 Metadata 节点的写入会越来越小。&lt;/p&gt;
&lt;h3 id="haystack-design-for-small-files"&gt;Haystack: Design for small files&lt;/h3&gt;
&lt;p&gt;上面说的 Google 的两代文件系统都不是专门为小文件而设计的，为解决小文件的需求，Facebook 内部研发了 Haystack 。&lt;/p&gt;
&lt;p&gt;在看 Haystack 的架构前，我们可以先看看之前的架构对小文件会产生哪些影响：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;大量小文件产生了大量的 chunk，这些 chunk file 本身也会占用本地文件系统的 metadata。从而导致无论是上层分布式文件系统还是底层机器上的文件系统都产生了海量的 Metadata 数据。甚至可能 Metadata 数量量比实际文件内容数据量还大。&lt;/li&gt;
&lt;li&gt;底层机器的本地文件系统大多不适合对海量小文件进行检索。&lt;/li&gt;
&lt;li&gt;访问模式为大量随机读写，大大拖慢了 IO 性能。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上描述我们可以发现，在小文件的场景中，本地文件系统成了非常大的一个瓶颈所在。而 Haystack 的设计最大胆的地方就是直接去掉了本地文件系统，直接和块存储通信。&lt;/p&gt;
&lt;p&gt;另外只要我们还是随机读写，无论怎么进行架构设计，都无法解决随机读写 IO 性能差的问题，所以我们需要想办法将随机读写转换为顺序读写。&lt;/p&gt;
&lt;p&gt;由此我们就能够大致理解 Haystack 的设计方向了。&lt;/p&gt;
&lt;h4 id="架构-2"&gt;架构&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/haystack_hu_18e7675c83d01a19.webp" width="634" height="540" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h5 id="haystack-directory"&gt;Haystack Directory&lt;/h5&gt;
&lt;p&gt;即 GFS 中的 Master 节点，管理 Metadata 信息。&lt;/p&gt;
&lt;h5 id="hystack-cache"&gt;Hystack Cache&lt;/h5&gt;
&lt;p&gt;缓存内部请求的文件，用来缓解热点问题。&lt;/p&gt;
&lt;h5 id="hystack-store"&gt;Hystack Store&lt;/h5&gt;
&lt;p&gt;由于 Hystack 已经去掉了文件系统，所以这里把整个 Volume 当作一个大文件来处理。&lt;/p&gt;
&lt;p&gt;Store 中存在两种大文件：&lt;/p&gt;
&lt;h6 id="store-file"&gt;Store File&lt;/h6&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/haystack-store-file_hu_fac3cfd84b4f798d.webp" width="644" height="568" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;每个文件对象为一个 Needle 。&lt;/p&gt;
&lt;h6 id="index-file"&gt;Index File&lt;/h6&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/haystack-index-file_hu_c519a827876e08f8.webp" width="654" height="420" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;每个文件对象会对应在 Index File 中创建一个 Needle，其中包含了该文件在 Store File 中的 Offset 信息。更新操作只需要更新 Index File 并在 Store File 中 Append 新的一个 Needle 就行。删除操作也仅仅只需要将索引的 Flag 标记为删除。这些操作产生的脏数据都可以后续异步回收程序进行重整处理。&lt;/p&gt;
&lt;p&gt;Index File 可以被完全加载到内存，故而能够大大加快检索效率。一次文件的读取最多也只会在 Store File 侧产生一次 IO 操作。为了实现这点，Haystack 也做了非常多的索引压缩以降低内存占用。&lt;/p&gt;
&lt;h3 id="juicefs-cloud-native-solution"&gt;JuiceFS: Cloud Native Solution&lt;/h3&gt;
&lt;p&gt;上面讲的文件系统都是基于私有云的，事实上在现在的公有云架构上，有很多事情已经开始发生了变化，以 AWS 举例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EBS/S3 已经实现了副本策略&lt;/li&gt;
&lt;li&gt;EBS/S3 本身就保证了高可用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而在面向传统私有机房的架构设计中，上述两点是完全不能保障的，以至于有一句很著名的话叫做，硬盘不是已经坏了，就是在坏的路上。但在 Cloud Navite 下，如果我们还是使用传统的架构，就会产生很多重复浪费。&lt;/p&gt;
&lt;p&gt;JuiceFS 就是专门为此而设计的。&lt;/p&gt;
&lt;h4 id="架构-3"&gt;架构&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/juicefs_hu_34fbfa54e91731da.webp" width="764" height="332" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/distributed-filesystem/juicefs-fuse_hu_2ef01b0d4e7ad722.webp" width="764" height="443" alt="分布式文件系统的演化" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h5 id="metadata-service"&gt;Metadata Service&lt;/h5&gt;
&lt;p&gt;JuiceFS 的 Metadata Service 是一组基于 Raft 协议实现的高可用集群，请求都经由 Leader 节点收发。&lt;/p&gt;
&lt;h5 id="object-storage"&gt;Object Storage&lt;/h5&gt;
&lt;p&gt;这里的 Object Storage 即上文的 chunkserver ，在 AWS 上就是 S3。由于 S3 这类对象存储是高可用且能够无限自动扩容的，这种架构的优势在于可以让不用再运维一个 chunkserver 集群。&lt;/p&gt;
&lt;h4 id="性能"&gt;性能&lt;/h4&gt;
&lt;p&gt;由于 S3 本身并不是给文件系统设计的，它的 first-byte-out-latency 非常高，一般有 &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/optimizing-performance.html"&gt;100–200 ms&lt;/a&gt; ，所以这种做法对于小文件肯定是完全不适合的。但如果是针对大文件的场景，这个 100 ms的延迟其实影响并不大，由于 S3 本身就是一个分布式的存储，在本地机器带宽足够的情况下，其吞吐量甚至能够达到 100 Gb/s。&lt;/p&gt;
&lt;p&gt;对于顺序读与顺序写请求来说，只要本地能够不停地从 metadata service 上预读到后续的 chunk 位置信息，那么其相较于本地文件系统的差异就可以进一步缩小。&lt;/p&gt;
&lt;h4 id="其他方案shared-block-storage"&gt;其他方案：Shared Block Storage&lt;/h4&gt;
&lt;p&gt;上面说的是用 Object Storage 来实现 chunkserver，还有一种更加另类的实现是，直接在块存储层实现共享，使得上层文件系统直接变成一个分布式的文件系统。目前国内能够看到的也只有&lt;a href="https://promotion.aliyun.com/ntms/act/vsan.html?spm=5176.54360.203004.5.GiftLC"&gt;阿里云&lt;/a&gt;开始了内测。&lt;/p&gt;</content:encoded></item><item><title>从动物森友会聊主机游戏联机机制</title><link>https://blog.joway.io/posts/how-animal-crossing-online-work/</link><pubDate>Wed, 13 May 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/how-animal-crossing-online-work/</guid><category>Tech</category><description>最近在玩动物森友会的时候时常会遇到一些迷之联机问题，在网上一番搜索，发现大家的答案都趋于用玄学来解释，于是便有了兴致想在原理上搞懂这些问题产生的根源以及动森这款游戏的一些联机设定背后的技术原因。
事先声明，本人并不从事游戏行业亦非主机游戏长期玩家，如有纰漏或其他角度的补充，欢迎在评论区告知。</description><content:encoded>&lt;p&gt;最近在玩动物森友会的时候时常会遇到一些迷之联机问题，在网上一番搜索，发现大家的答案都趋于用玄学来解释，于是便有了兴致想在原理上搞懂这些问题产生的根源以及动森这款游戏的一些联机设定背后的技术原因。&lt;/p&gt;
&lt;p&gt;事先声明，本人并不从事游戏行业亦非主机游戏长期玩家，如有纰漏或其他角度的补充，欢迎在评论区告知。&lt;/p&gt;
&lt;h2 id="游戏是如何同步的"&gt;游戏是如何同步的&lt;/h2&gt;
&lt;p&gt;我们首先来看看一般游戏是如何来做同步的。&lt;/p&gt;
&lt;p&gt;想象两个独立房间里分别有甲、乙两个玩家，他们要远程下一局象棋。他们每下一步前都需要先获知到当前棋盘的情况，此时能够有两种实现方式。&lt;/p&gt;
&lt;p&gt;第一种叫做锁步同步，原理是玩家每操作一步就通知给另外一个玩家，彼此同步当前的操作序列，通过这些有时序的操作，就能够计算出当前棋局的状态。但它不允许中间丢失任何一步的信息，否则就会出现非常大的计算偏差。&lt;/p&gt;
&lt;p&gt;第二种叫做状态同步，顾名思义是玩家每操作一步，就同步整个棋盘的状态。这种方式可以容忍中间某些状态丢失，最终得到的状态依旧还是一致的。&lt;/p&gt;
&lt;p&gt;在实际实践中，针对那种玩家操作非常高频的游戏会更多使用锁步同步，例如王者荣耀。而对于那些卡牌类游戏更偏向于直接用状态同步。&lt;/p&gt;
&lt;h2 id="游戏是如何联机的"&gt;游戏是如何联机的&lt;/h2&gt;
&lt;h3 id="通信架构"&gt;通信架构&lt;/h3&gt;
&lt;p&gt;无论是上述哪种同步方式，我们都需要通过网络在多个主机间交换数据。我们现在将场景转换成甲、乙、丙三个人一起下跳棋。为保证三个人最终得到的游戏状态都是一致的，我们往往需要有一台 Host 主机来作为权威主机，其他主机只能通过权威主机下发的数据(状态/操作序列)来更新自己本地的游戏数据。&lt;/p&gt;
&lt;p&gt;在这里我们假设甲来做「Host」，乙、丙每操作一步，都需要先发送给甲确认，无误后再发送该操作被确认的信息给乙、丙，乙、丙此时才能够认为操作成功并将画面更新到最新的状态。甲主机上在任意时刻都存有当前游戏的真正状态，其他主机只是在 follow 甲主机的状态以更新自己的游戏画面。&lt;/p&gt;
&lt;p&gt;在上述模式下，由于甲主机既要作为游戏主机，又要作为状态同步的主机，当联机用户数一多，甲主机就会不堪重负，出现所谓的「炸机/炸岛」现象。另外，这种模式会需要甲主机一直存活，只能作为短时间内的伺服方案。所以有些游戏会引入一个外部自建/官方的服务器来承担这个状态同步的功能，例如我的世界。但究其原理是一模一样的。&lt;/p&gt;
&lt;h3 id="nat-穿透"&gt;NAT 穿透&lt;/h3&gt;
&lt;p&gt;在了解完上面的基础知识后，我们能够发现，在不考虑外部服务器的情况下，我们会对玩家主机间的网络有以下几点要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;甲能够向乙、丙发送数据&lt;/li&gt;
&lt;li&gt;乙、丙能够向甲发送数据&lt;/li&gt;
&lt;li&gt;乙、丙之间不需要有网络联通保障&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然上述要求看起来很容易，但是由于现在网络运营商都会不同程度地使用 &lt;a href="https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2"&gt;NAT&lt;/a&gt; 技术，所以导致要让两台家用主机建立双向通信并不是一件非常容易的事情。&lt;/p&gt;
&lt;p&gt;家用网络一般有四种不同的 NAT 类型：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full-cone NAT&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.&lt;/li&gt;
&lt;li&gt;Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort.
&lt;img src="https://blog.joway.io/images/how-animal-crossing-online-work/Full_Cone_NAT_hu_90ed69a38238c062.webp" width="800" height="360" alt="从动物森友会聊主机游戏联机机制" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(Address)-restricted-cone NAT&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.&lt;/li&gt;
&lt;li&gt;An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:any. &amp;ldquo;Any&amp;rdquo; means the port number doesn&amp;rsquo;t matter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/how-animal-crossing-online-work/Restricted_Cone_NAT_hu_c5d4d3ac2dffe87e.webp" srcset="https://blog.joway.io/images/how-animal-crossing-online-work/Restricted_Cone_NAT_hu_69d82c74531f073.webp 800w, https://blog.joway.io/images/how-animal-crossing-online-work/Restricted_Cone_NAT_hu_fd454b1fd86e9794.webp 1200w, https://blog.joway.io/images/how-animal-crossing-online-work/Restricted_Cone_NAT_hu_c5d4d3ac2dffe87e.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="720" alt="从动物森友会聊主机游戏联机机制" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Port-restricted-cone NAT&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.&lt;/li&gt;
&lt;li&gt;An external host (hAddr:hPort) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:hPort.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/how-animal-crossing-online-work/Port_Restricted_Cone_NAT_hu_8cde489fc4be2372.webp" srcset="https://blog.joway.io/images/how-animal-crossing-online-work/Port_Restricted_Cone_NAT_hu_10c0ca11d51b7c19.webp 800w, https://blog.joway.io/images/how-animal-crossing-online-work/Port_Restricted_Cone_NAT_hu_db67dca009a09ed2.webp 1200w, https://blog.joway.io/images/how-animal-crossing-online-work/Port_Restricted_Cone_NAT_hu_8cde489fc4be2372.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="720" alt="从动物森友会聊主机游戏联机机制" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Symmetric NAT&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each request from the same internal IP address and port to a specific destination IP address and port is mapped to a unique external source IP address and port; if the same internal host sends a packet even with the same source address and port but to a different destination, a different mapping is used.&lt;/li&gt;
&lt;li&gt;Only an external host that receives a packet from an internal host can send a packet back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/how-animal-crossing-online-work/Symmetric_NAT_hu_e49448235c68ae9c.webp" srcset="https://blog.joway.io/images/how-animal-crossing-online-work/Symmetric_NAT_hu_467f021fbd5d615d.webp 800w, https://blog.joway.io/images/how-animal-crossing-online-work/Symmetric_NAT_hu_9f18e35f5fd733b6.webp 1200w, https://blog.joway.io/images/how-animal-crossing-online-work/Symmetric_NAT_hu_e49448235c68ae9c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="720" alt="从动物森友会聊主机游戏联机机制" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;上述四种 NAT 类型简单归纳就是说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前三种 cone 类型的 NAT 都能将内网的 iAddr:iPort 映射到一个固定的外网 eAddr:ePort 上。只有 Symmetric 类型对于一样的 iAddr:iPort 但不同的目的IP和端口也会被映射到不同的 eAddr:ePort 上去。&lt;/li&gt;
&lt;li&gt;前三种 cone 类型的 NAT 的区别可以直接从名字中看出来，address-restricted-cone 表示只有自己对外发出过包的 address 有能力向其发送包，port-retricted-cone 的意思是只有自己对外发出过包的 address:port 地址有能力向其发送包。&lt;/li&gt;
&lt;li&gt;第四种 Symmetric 类型对外部返回报文来源的限制是与 port-retricted-cone 一致的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;主机上的网络测试功能能够告诉我们当前网络的 NAT 类型。Switch 上的 Type A、B、C、D 分别映射到上面四种类型，而 PS4 上则是 Type 1(直连，无 NAT)、2(非 Symmetric NAT)、3(Symmetric NAT)。为方便下文叙述，我们用 Switch 的 ABCD 指代上述四种网络类型。&lt;/p&gt;
&lt;p&gt;理解了四种 NAT 类型各自的限制，我们就能够通过推导判断出，哪两个 NAT 类型的网络是不可能建立双向通信的，而不再需要去人肉尝试。这里我们分别举例来介绍不同 NAT 类型下的不同情况，甲作为 Host 主机，并且我们有一个外部的联机服务可以获取到甲乙的外网 IP 信息。所谓的联机服务是一个第三方服务器，甲乙都能通过访问它去搜索到对方的外网IP和端口号信息，同时也可以将自己的外网IP和端口号信息给注册到上面。所以这里甲、乙能够在通信前就知道彼此的通信地址信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果甲的 NAT 类型是 A ：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无论乙的类型是 A/B/C/D，乙都能够直接向甲的 eAddr:port 发送数据，而当甲已经收到乙的数据时，也能够获得到乙的 eAddr2:port2，以及向乙发送数据的资格，从而建立双向通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果甲的 NAT 类型是 B ：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当乙的 NAT 类型是 B/C/D ：甲先使用 &lt;code&gt;192.168.1.1:10001&lt;/code&gt; =&amp;gt; &lt;code&gt;1.1.1.1:10002(甲外网出口)&lt;/code&gt; =&amp;gt; &lt;code&gt;2.2.2.2:20002(乙外网入口)&lt;/code&gt; 向乙尝试发送数据，虽被乙拒绝，但在乙的路由器上留下了访问记录，从而使得乙具备了向甲发送数据的能力。而当乙发送完数据，又会使得甲获得向乙发送数据的能力，从而建立双向通信。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 A 时同上甲为 A 时逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果甲的 NAT 类型是 C ：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当乙的 NAT 类型是 D ：乙尝试连接甲的时候会被拒绝，并且甲也没法知道乙映射的端口号是哪一个所以亦无法连接到乙。无法建立任一方向的通信。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 B ：C-B 的连接同上面 B-C 的连接。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 C ：C-C 和 C-B/B-C 的区别仅在于要求双方出口的端口要一直保持一致，要求更加严格，但依旧能够建立双向通信。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 A 时亦能够建立双向通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果甲的 NAT 类型是 D ：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当乙的 NAT 类型是 D：无法建立任一方向的通信。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 C：同 C-D，无法建立任一方向的通信。&lt;/li&gt;
&lt;li&gt;当乙的 NAT 类型是 A/B：同 A-D 和 B-D，能够建立双向通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上推导，可以有以下结论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只有 C-D,D-C,D-D 的组合是没有机会能够建立双向通信的，其他组合在 NAT 层面上都能够具备双向通信的能力。&lt;/li&gt;
&lt;li&gt;类型为 A/B 的玩家理论上连其他任何类型的玩家都不会有 NAT 上的问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然上述都是理论，实际中是否真的能够连接上还取决于其他网络状况甚至是程序编写逻辑的因素。&lt;/p&gt;
&lt;h2 id="动森是如何做联机的"&gt;动森是如何做联机的&lt;/h2&gt;
&lt;p&gt;许多主机游戏在联机的时候都会有一些在玩家看来非常奇怪甚至奇葩的设定，这些设定都和上面讲的同步机制和联机网络问题相关。&lt;/p&gt;
&lt;p&gt;动森的联机模式也有诸多有意思(恼人)的设定，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;联机状态下无法更改岛的装饰&lt;/li&gt;
&lt;li&gt;当一个玩家上岛时，会需要所有人暂停近很长时间来等其成功加入&lt;/li&gt;
&lt;li&gt;当一个玩家离开时，同样需要大家同步目送其离开，并且在离开时会保存当前时刻的数据进度&lt;/li&gt;
&lt;li&gt;当有个玩家掉线/强行退出时，所有人的数据会回滚岛上一次玩家登岛/正常离开时的版本&lt;/li&gt;
&lt;li&gt;当岛内有玩家打开了对话窗口时，人不能正常离开也不能上岛&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下分析仅仅是我在玩了 200 个小时游戏后，结合自己的软件工程经验对动森实现方式的猜测。在没有看代码前谁也无法保证这种猜测的绝对正确性，况且相比正确性，我更在乎这个猜测过程的开心，所以大家可以更多以工程角度来思考而不是纠结于其是否的确是这么实现的。&lt;/p&gt;
&lt;p&gt;我们可以把联机游戏的过程分以下几个环节来分别讨论：&lt;/p&gt;
&lt;h3 id="1-甲打开联机权限即所谓的开门用自己主机作为-host"&gt;1. 甲打开联机权限(即所谓的开门)，用自己主机作为 Host&lt;/h3&gt;
&lt;p&gt;这一步甲将自己的外网 IP 和端口号(如 &lt;code&gt;1.1.1.1:10001&lt;/code&gt;)注册在了 Switch 的联机服务中。&lt;/p&gt;
&lt;h3 id="2-乙通过搜索找到甲尝试加入"&gt;2. 乙通过搜索找到甲，尝试加入&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;乙通过联机服务先将自己的外网 IP 和端口号(如 &lt;code&gt;2.2.2.2:10002&lt;/code&gt;)注册上去。(即游戏里询问是否要联机的那一步)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再通过搜索得到甲的 &lt;code&gt;1.1.1.1:10001&lt;/code&gt; (即动森里搜索好友的那一步)，尝试连接。&lt;/p&gt;
&lt;p&gt;注意，此时甲主机在后台也通过联机服务知道了乙在连它，并且甲也会根据 NAT 类型的不同，用乙的 &lt;code&gt;2.2.2.2:10002&lt;/code&gt; 去连乙，尝试打通双向通信。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-建立连接上岛"&gt;3. 建立连接，上岛&lt;/h3&gt;
&lt;p&gt;当上面一步确认能够建立双向通讯后，就可以开始上岛了。上岛又分为以下几个步骤：&lt;/p&gt;
&lt;h4 id="31-host-打包当前所有游戏状态"&gt;3.1 Host 打包当前所有游戏状态&lt;/h4&gt;
&lt;p&gt;在上岛前，甲主机(Host)会把当前时刻的所有人的游戏数据给打包一份 snapshot。&lt;/p&gt;
&lt;p&gt;这里的 snapshot 数据内容包括岛屿数据和玩家数据。&lt;/p&gt;
&lt;h4 id="32-下载对方岛的-snapshot"&gt;3.2 下载对方岛的 snapshot&lt;/h4&gt;
&lt;p&gt;动森上岛时会弹出一个显示进度的动画，这个动画的过程就是在下载目标岛的 snapshot 数据，很明显能够发现如果在中国连美国的玩家，这个过程会非常漫长，这个是由于跨境网速慢导致的。&lt;/p&gt;
&lt;h4 id="33-其他人同步等待直到新玩家上岛"&gt;3.3 其他人同步等待，直到新玩家上岛&lt;/h4&gt;
&lt;p&gt;之所以其他玩家要等待新玩家上岛是因为上一步保存的 snapshot 必须是最新的结果，这也意味着其他玩家不能再有增量操作，否则新玩家上岛时状态就不一致了。&lt;/p&gt;
&lt;h3 id="4-开始游戏"&gt;4. 开始游戏&lt;/h3&gt;
&lt;p&gt;当上岛完毕，就可以开始正常开始游戏了。这时候就会遇到一个如何同步玩家彼此操作数据的问题。&lt;/p&gt;
&lt;p&gt;这里我们把玩家的操作分为两种类型：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;影响游戏数据（低频，有时序要求，需要落盘）&lt;/li&gt;
&lt;li&gt;影响游戏画面但不影响游戏数据（高频，无时序要求，不需要落盘）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果仔细体验会发现，当我们在挖坑、和 NPC 对话、丢物品等会对全局游戏数据产生直接影响的操作时，偶尔会出现一下卡顿，这是因为这些会影响全局状态的操作在渲染画面前都需要先向 Host 主机请求是否允许，这里如果出现网络抖动的话就会出现卡顿/失败的情况。但是我们跑步的时候却很少出现卡顿，但有时会出现「闪回」，因为跑动只影响了玩家当前位置，不影响游戏数据，就算出现闪回也是能够接受的，而且还不需要强制保证时序性。况且如果跑步也要去 Host 端询问就会导致整个游戏体验都非常卡。但是像丢物品这种操作如果数据错乱或者时序错乱的话，整个状态就不一致了，会非常严重。&lt;/p&gt;
&lt;p&gt;所以这里的同步方式其实是锁步同步。只不过分别对低频和高频做了分别的策略。&lt;/p&gt;
&lt;h3 id="5-玩家退出强退掉线"&gt;5. 玩家退出/强退/掉线&lt;/h3&gt;
&lt;p&gt;玩家如果正常退出游戏，会触发一个「保存数据」界面。要理解这个保存数据的含义，我们要把游戏里的数据分为两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;岛屿的状态&lt;/li&gt;
&lt;li&gt;每个玩家的每一步操作数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先对于主机游戏来说，其真正有效的数据都是要最终落盘到主机本地存储里。但试想如果每一次更新都触发玩家本地主机存储的更新，到时候要回滚起来也会变得异常麻烦，更不用说磁盘的 IO 还非常慢。所以这里更可能的实现方式其实是，Host 主机的内存里存放着当前游戏的权威数据，包括岛屿状态和玩家操作。另外，无论玩家用什么方式退出，我们都必须确保结束游戏后，所有玩家本地的存档加上 Host 主机上的存档都是某一个时间点上的真正游戏状态。游戏数据的正确性优先级是高于用户体验的。下面会有例子来解释这点的重要性。&lt;/p&gt;
&lt;p&gt;当玩家正常退出触发「保存数据」时，Host 主机首先会开启一个当前时刻的 snapshot，然后每个玩家的主机都会向 Host 主机去下载属于其的操作数据，并落盘到本地。&lt;/p&gt;
&lt;p&gt;但当有玩家异常退出时，由于其并没有下载属于他的数据，所以他的本地游戏数据还停留在上一次保存的时间点 T1 上，为了满足我们前面说的数据正确性的保证，虽然岛上其他玩家没有掉线，并且他们游戏里的状态都是最新的也是正确的数据，但不得不为了让这个家伙的数据是正确的而把其他所有人的数据都回滚到了时间 T1 上，这就是为什么动森会出现掉线回档的原因。&lt;/p&gt;
&lt;h2 id="常见问题"&gt;常见问题&lt;/h2&gt;
&lt;h3 id="任天堂联机服务垃圾吗"&gt;任天堂联机服务垃圾吗？&lt;/h3&gt;
&lt;p&gt;通过上面的解释能够理解，这些看似奇葩的联机体验背后，的确是有着非常多技术难题的。而且任天堂毕竟是一家游戏公司不是专业分布式数据库公司，虽然目前的技术实现方案有诸多可以改进的地方，但是也是要算上 ROI 的，所以谈「垃圾」还是算不上。&lt;/p&gt;
&lt;h3 id="为什么游戏厂商不自建服务器来提升体验"&gt;为什么游戏厂商不自建服务器来提升体验？&lt;/h3&gt;
&lt;p&gt;游戏玩家来自全球各地，如果要用自建服务器来提升体验，那也得在全球都铺设服务器，这个成本相当大且实现难度也相当大的。的确现在会有那种全球同服的解决方案，但是一般都是像网络游戏这种就靠着联网来挣钱的公司会有能力和意愿采用。主机游戏的商业模式注定了他们不会花非常高的成本去提升网络体验。当然主机生产商自己搞一个全球服务器就是另说了。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Network_address_translation"&gt;Wikipedia: Network address translation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt"&gt;Peer-to-Peer (P2P) communication across middleboxes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zhuanlan.zhihu.com/p/56923109"&gt;网络游戏同步技术概述&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>创业公司的文化</title><link>https://blog.joway.io/posts/startup-culture/</link><pubDate>Mon, 02 Mar 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/startup-culture/</guid><category>Thought</category><description>前段时间有一个小事情让我想到创业公司文化这件事情。
我目前就职在一家创业公司，我们有一个文化是有相当一部分同事会有一个「艺名」。艺名的产生有很多种场景。一种类似古代的「字」，是自己为摆脱原始名字的依赖，为自己取的代称，有着很强个人色彩，比如我的 Joway。还有一种是在公司发展过程中自然而然形成的「外号」，比如中国公司常见的X哥，通常还带着点共同记忆在里面。</description><content:encoded>&lt;p&gt;前段时间有一个小事情让我想到创业公司文化这件事情。&lt;/p&gt;
&lt;p&gt;我目前就职在一家创业公司，我们有一个文化是有相当一部分同事会有一个「艺名」。艺名的产生有很多种场景。一种类似古代的「字」，是自己为摆脱原始名字的依赖，为自己取的代称，有着很强个人色彩，比如我的 &lt;code&gt;Joway&lt;/code&gt;。还有一种是在公司发展过程中自然而然形成的「外号」，比如中国公司常见的X哥，通常还带着点共同记忆在里面。&lt;/p&gt;
&lt;p&gt;但这种遍布的艺名也为团队协作带来了一定困扰，比如在 Slack 搜索/@一个同事的时候，需要一个个遍历所有可能的名字，更麻烦的是有时候你还不一定知道他的艺名。于是乎，很自然能够想到的一个做法是强制规范每个人的命名，比如只能用真实姓名。的确会有大公司过来的同事会这么建议，这种建议也能以最快的速度和最好的结果来解决这个问题。但在这个过程中牺牲的，其实是这些艺名长期以来养成的共同感情，以及沟通中的人味。&lt;/p&gt;
&lt;p&gt;至于这件事情最终如何解决的其实倒也并不重要了，但观察这个解决过程其实蛮有意思的。因为这种现象背后展现的其实是一家创业公司在发展过程中，集聚了不同公司文化背景的人以后互相交融影响的过程。&lt;/p&gt;
&lt;p&gt;人类这几千年出现过许许多多的宗教，观察它们兴起的故事其实和创业公司很像。伊斯兰和基督教都有着非常浓厚的传教热情，有些朝代靠战争，有些朝代靠文化。但诸如佛教就比较佛系，唐僧自己走路去取的经，鉴真也是受日本邀请才东渡。佛教之所以在东亚还能继续维系生机，大概也只是因为这里长期闭塞没有经受其他宗教的冲击。而其发源地印度早被各种宗教夹击，如今只剩下了 0.41% 的佛教徒。&lt;/p&gt;
&lt;p&gt;印度的宗教发展非常像一个创业公司的演变。早期孕育出了某种独特的文化，而后陆陆续续各个公司之间人员的跳槽往来，汇聚出多种公司文化交融的场景，但最后总会有某种文化占上风。并且胜出的文化并不一定是公司最初的文化。&lt;/p&gt;
&lt;p&gt;如同宗教一样，不同文化在传播性上，也有着巨大的区别。总会有某些文化特别具有传播性，强势的文化会慢慢吞噬掉弱势的文化，但一种文化弱势却并不一定代表其本身就不好。我们又如何能够相信胜出的文化是因为其本身优秀，而非它只是更加容易能够被人所接受？&lt;/p&gt;
&lt;p&gt;这个观点引申出去，同样也能质疑我们目前所传承到的传统文化。真的是因为其优秀所以能够被传承千年吗？&lt;/p&gt;</content:encoded></item><item><title>伊朗见闻录</title><link>https://blog.joway.io/posts/iran-tralvel/</link><pubDate>Sat, 14 Dec 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/iran-tralvel/</guid><category>Travel</category><description>10月份的时候在伊朗自南向北，进行了为期 9 天的旅行。
之所以会想去伊朗，是因为在 2019 年发生了太多事情让我疲于不断修正自己的价值观，以至于我连基本的「明是非」能力也丧失殆尽。如同数学公式推导一样，我需要有一些基本的「公理」，以此来构建价值观的「定理」。而越是试图去理解这个世界，越是会发现似乎这个世界是不存在一个「普世」的「公理」。</description><content:encoded>&lt;p&gt;10月份的时候在伊朗自南向北，进行了为期 9 天的旅行。&lt;/p&gt;
&lt;p&gt;之所以会想去伊朗，是因为在 2019 年发生了太多事情让我疲于不断修正自己的价值观，以至于我连基本的「明是非」能力也丧失殆尽。如同数学公式推导一样，我需要有一些基本的「公理」，以此来构建价值观的「定理」。而越是试图去理解这个世界，越是会发现似乎这个世界是不存在一个「普世」的「公理」。&lt;/p&gt;
&lt;p&gt;如果要证明世界上不存在「普世」的「公理」，那么试图去理解伊朗一定是一个很好的入口。她是世界主流社会的坏学生，与国际社会作对，对内压迫人民，对外输出革命，被制裁了数十年却依旧屹立不导，甚至越战越强，被主流社会描绘成一个恶魔般的存在。但事实的确是这样吗，还是说世界上存在一个可能是可以并存多种「公理」？&lt;/p&gt;
&lt;h2 id="没落的中东贵族"&gt;没落的中东贵族&lt;/h2&gt;
&lt;p&gt;Iran 一词由雅利安（Aryan）演化而来，本意雅利安人的土地。雅利安人中有一支居住在伊朗高原，这支人的后裔被称作波斯人，后建立起横跨欧亚非的波斯帝国。651 年阿拉伯帝国打败波斯，伊斯兰宗教开始传入波斯。1925 年礼萨汗建立了巴列维王朝，在 1935 年将国名从波斯变成伊朗，一方面迎合当时西方社会对于雅利安人种的推崇热潮，一方面也为了表示伊朗是所有雅利安人的国家。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/persepolis_hu_8dd7e78f93815765.webp" srcset="https://blog.joway.io/images/iran/persepolis_hu_26410b92423d4fb.webp 800w, https://blog.joway.io/images/iran/persepolis_hu_df65c3358f6fff32.webp 1200w, https://blog.joway.io/images/iran/persepolis_hu_8dd7e78f93815765.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="473" alt="伊朗见闻录" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/persepolis-door_hu_bc9dc263ef0b57d1.webp" srcset="https://blog.joway.io/images/iran/persepolis-door_hu_5acbf6877526243b.webp 800w, https://blog.joway.io/images/iran/persepolis-door_hu_1aa0e7deddfb8658.webp 1200w, https://blog.joway.io/images/iran/persepolis-door_hu_bc9dc263ef0b57d1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Persepolis&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现代的伊朗拥有高度发达的交通系统，远高于周边国家的 8000 万人口，石油储量世界第四，超过 90% 的什叶派穆斯林。受到两伊战争的影响，伊朗目前的年龄中位数只有 28，是一个拥有大量年轻人的国度。&lt;/p&gt;
&lt;p&gt;在巴列维王朝时期，伊朗是一个极为世俗化的国家，开放妇女投票，进行土地改革，消除文盲，并且进行了大量基础设施建设。一度成为中东实力最强的国家，1963年甚至想进军成为世界第五强国。最后，由于世俗化削弱了原本教士集团的势力，导致最后1979年爆发了伊斯兰革命，将现代伊朗变成了一个彻底政教合一的伊斯兰国家。&lt;/p&gt;
&lt;p&gt;在阅读关于伊朗的书籍和资料时，会发现「民族性」和「宗教性」一直纠缠着伊朗的命运。伊朗拥有自己的波斯历史，自己的波斯文学，自己的波斯数字，自己的波斯历法。即便是在阿拉伯帝国入侵后被迫皈依于伊斯兰教，波斯人也偏偏选择了伊斯兰里最具反抗精神的少数派 —— 什叶派，以示自己的民族性。到了现代的巴列维时期，伊朗人开始抛开宗教的束缚，大刀阔斧地奔向波斯民族未竟的强国梦，而大量底层没有享受到现代化好处的伊朗人将国王轰下了台换来了如今版本的民主伊朗。时至今日，你已经很难分辨出是什叶派塑造了伊朗民族，还是伊朗民族反过来重塑了什叶派。&lt;/p&gt;
&lt;h2 id="国际制裁"&gt;国际制裁&lt;/h2&gt;
&lt;p&gt;在伊朗的飞机上，能感受到国际制裁的切身影响。我乘坐的航班是从上海直飞德黑兰的马汉航空，飞机是一架17年机龄的空客A340。根据国际制裁决议，伊朗无法直接进口新型飞机，只能从欧洲一些航空公司手里购买二手、多手甚至是退役的飞机。更为糟糕的是，伊朗没法得到原厂制造商的技术支持，甚至连飞机更换的零件都需要依靠黑市走私，导致其空难事故远超其他正常国家。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/airplane_hu_28d917e83de35b78.webp" srcset="https://blog.joway.io/images/iran/airplane_hu_2654d714e9648ced.webp 800w, https://blog.joway.io/images/iran/airplane_hu_a9d36973db40ad3f.webp 1200w, https://blog.joway.io/images/iran/airplane_hu_28d917e83de35b78.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;马汉航空伤痕累累的二手飞机&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;伊朗同样被实行了金融制裁，导致在其境内无法使用任何国际通行金融结算工具，包括Visa、银联，甚至是微信红包。所以纵使你银行卡内有再多的钱，在伊朗境内都是一张空纸。&lt;/p&gt;
&lt;p&gt;由于其本国货币极不稳定，美元在当地成了黄金般的存在，我亲眼目睹了在设拉子的地下钱庄门口，一大堆伊朗人、伊拉克人、阿富汗人、阿塞拜疆人堵在门口，盯着墙上的汇率表，等到合适的汇率一下子拥到柜台钱把一捆捆纸币兑换成美元。在那个房间里能亲眼目睹到什么才是所谓的「美元霸权」。&lt;/p&gt;
&lt;p&gt;制裁不仅仅导致国内钱无法出境，也导致国外钱没法入境。如果你想要在网上预订伊朗的车票或者酒店，除了少数有合作的网站其他几乎不可能，而那些有合作的网站又会收取极其夸张甚至到原价几倍的手续费。所以大部分事情只能入境后在当地找人帮忙汇款预订或是自己上门。&lt;/p&gt;
&lt;h2 id="伊朗互联网--荒漠与绿洲"&gt;伊朗互联网 —— 荒漠与绿洲&lt;/h2&gt;
&lt;p&gt;伊朗和中国一样，对互联网进行非常严格的管制，但中国政府实行的是黑名单制，只有被审查机构注意到的网站才会被禁止访问，而伊朗政府在这方面显然遥遥领先于中国，直接一刀切实行白名单制度 —— 只有被政府允许的网站才能被访问。所以甚至连微信在伊朗都是无法正常使用的。11月的时候伊朗出现了大规模的游行抗议，政府甚至能够下决心直接切断互联网长达163个小时。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/iran-network_hu_a58c5cb24af47d80.webp" srcset="https://blog.joway.io/images/iran/iran-network_hu_791bc3de7d9302e2.webp 800w, https://blog.joway.io/images/iran/iran-network_hu_a58c5cb24af47d80.webp 1140w" sizes="(max-width: 800px) 100vw, 800px" width="1140" height="1166" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NetBlocks 对于伊朗互联网封锁的检测&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在我出发前不久 Cloudflare 专门出了一个 1.1.1.1 的 VPN，虽然 Cloudflare 的原意是用来加速互联网网络，但根据我的实际测试在伊朗这种地方突破互联网封锁极其顺畅，而且由于 Cloudflare 节点众多，传统封节点 IP 的方式很难对它有实际作用。在梅赫拉巴德机场候机的时候，我觉得闲着也是闲着，专门在一个 Sim 卡柜台前面教别人如何突破网络封锁，很难相信我在自己的祖国谈 VPN 色变，而在另外一个更加封闭的国家却享受到了这种自由，而在这背后支撑着我的勇气还正是我自己的祖国所赋予我的。&lt;/p&gt;
&lt;p&gt;即便伊朗有着世上最疯狂的互联网封锁，但对于境内的普通民众，依旧能够感受到自己生活在一个「网络发达国家」。&lt;/p&gt;
&lt;p&gt;Instagram 现在是伊朗的国民级应用，相当于中国的朋友圈，旅途中结识的伊朗朋友都会邀请我加他们的 Instagram 帐号。其用户规模和忠诚度大到甚至到连伊朗政府都不敢把它给封禁。但我还发现一个有趣的现象是，由于在伊朗能够被允许使用的国外应用并不多，所以伊朗人对 Ins 可以说是物尽其用。当地的伊朗朋友搜餐馆会直接上 Ins 搜，当地餐馆大多在 Instagram 上都有自己的帐号，还会贴上自己的菜单和食物照片，下面有食客的评价，可以说已经是一个数据量充足的 Yelp 了。除了餐馆各个城市的景点也都会有自己的帐号，并且会更新最新的旅行信息，完全可以替代诸如马蜂窝这些网站。如果你要在伊朗搜索什么东西，Instagram 在很多场景都会比 Google 好用。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://snapp.ir/about"&gt;Snapp&lt;/a&gt; 是一款由一群毕业于德黑兰各个大学的年轻人开发的类 Uber 打车软件，我在伊朗的大城市都深度依赖它往返各处。和滴滴在中国一样，这家公司帮助了一大批人解决了就业问题和副业问题。我在最后一天清晨打车遇到一位司机，他本来的工作是 Simulation Engineer，但是由于有了小孩，不得不在早上上班前兼职 Snapp 赚点外快。这家公司 16 年的时候获得了 2000 万欧元的A轮融资，之后又收购了一家预订酒店的公司，现在上面甚至还能预订食物和机票，从伊朗 Uber 进化成了伊朗 Grab。前段时间由于川普又签署了新的制裁令，导致 Github 在没有通知的情况下，直接封禁了一位伊朗开发者的帐号，从而在 Github 上发起了联名抗议运动，当时还去签名支持了一下。在使用 Snapp 的时候经常想起这件事，如果 2019 年的互联网还存在英雄主义，这群在 Snapp 的年轻人做的事情就是英雄主义。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/snapp_hu_7cb91ceeef2f3085.webp" width="338" height="600" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;虽然零零碎碎有一些本土产品，但与外面的世界相比，伊朗的互联网依旧算是一片荒漠，但同时也是一片未来的绿洲。如果有一天伊朗能够重新和世界接轨，以其远高于周边国家的人均教育水平和长期受压抑的创造欲，一定能够涌现出一批疯狂的年轻人，给伊斯兰世界的互联网带去一点非西方的色彩。&lt;/p&gt;
&lt;h2 id="花园和天堂"&gt;花园和天堂&lt;/h2&gt;
&lt;p&gt;之前在西班牙旅行的时候，在摩尔人区发现经常会偶遇大大小小的非常美的花园，当时还觉得是摩尔人自己的文化。这次在伊朗见到了一个个令我叹为观止的花园，经过查阅才知道在伊斯兰文化里，花园也是「天园」，是为了模仿天堂的样子。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/hafez_hu_88fb8b9514b59f99.webp" srcset="https://blog.joway.io/images/iran/hafez_hu_a4c4ea69edc5256c.webp 800w, https://blog.joway.io/images/iran/hafez_hu_c0b6fc854e1034f.webp 1200w, https://blog.joway.io/images/iran/hafez_hu_88fb8b9514b59f99.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1109" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/garden_hu_a8da956af64762cb.webp" srcset="https://blog.joway.io/images/iran/garden_hu_a6baf13eec4cc61.webp 800w, https://blog.joway.io/images/iran/garden_hu_7a034b089b40a60f.webp 1200w, https://blog.joway.io/images/iran/garden_hu_a8da956af64762cb.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;虽然来源于宗教，但这些花园内完全没有丝毫宗教的影子，甚至可能算是伊朗境内极少数完全没有宗教色彩的地方。或许是因为，宗教只是帮助人们接近上帝，而在上帝居住的地方，只应该存在上帝自己的造物。&lt;/p&gt;
&lt;h2 id="清真寺"&gt;清真寺&lt;/h2&gt;
&lt;p&gt;伊斯兰的清真寺与其说是宗教场所，不如说是公共场所。&lt;/p&gt;
&lt;p&gt;在设拉子的时候误打误撞居然进了理论上非穆斯林不能进入的光明王之墓的清真寺，当时并不知道这个清真寺这么有来头，一进去就被里面的氛围所震撼了。巨大的广场里，每个人都在彼此欢笑地聊天，热闹地像个集市，人们分享的是彼此的精神。在之后的旅途中，我一次次在清真寺看到类似的场面，有在里面乘凉的，有在里面看书的，甚至还有在里面带孩子的。这是我第一次切身理解到宗教对于人的作用机理，以及为什么现代科学如此发达，人们还是依旧需要宗教。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/mosque-people_hu_b510826c16aa678.webp" srcset="https://blog.joway.io/images/iran/mosque-people_hu_f2bec2dce65ad1f8.webp 800w, https://blog.joway.io/images/iran/mosque-people_hu_1ff61171d324f5fb.webp 1200w, https://blog.joway.io/images/iran/mosque-people_hu_b510826c16aa678.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;除了清真寺里的广场，寺的主体也是一场视觉盛宴。&lt;/p&gt;
&lt;p&gt;多年前在罗马的万神殿仰望穹顶的时候第一次感受到或许上帝真的存在，而在伊斯法罕的清真寺里也出现了类似的体验。这些人类创造的宏伟建筑里的美已经不属于人类自己，这里的美原本就存在于自然界，但是人类学会了他们。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/mosque-top_hu_d44130698f7dbfc5.webp" srcset="https://blog.joway.io/images/iran/mosque-top_hu_2e9a1230fedee81f.webp 800w, https://blog.joway.io/images/iran/mosque-top_hu_eb2f20bd40628579.webp 1200w, https://blog.joway.io/images/iran/mosque-top_hu_d44130698f7dbfc5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/mosque_hu_d96908d9450409e3.webp" srcset="https://blog.joway.io/images/iran/mosque_hu_a8f54c5fc41ccfe5.webp 800w, https://blog.joway.io/images/iran/mosque_hu_379d48f569c0d985.webp 1200w, https://blog.joway.io/images/iran/mosque_hu_d96908d9450409e3.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/old-mosque_hu_fbb3b67c64bbe5e0.webp" srcset="https://blog.joway.io/images/iran/old-mosque_hu_c650beada37ea7f1.webp 800w, https://blog.joway.io/images/iran/old-mosque_hu_a3925d1e550cd5de.webp 1200w, https://blog.joway.io/images/iran/old-mosque_hu_fbb3b67c64bbe5e0.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="伊朗高原"&gt;伊朗高原&lt;/h2&gt;
&lt;p&gt;在伊朗从南向北，一路的地貌近乎一致，都是大片大片的荒漠，宏伟的天空，加上突然出现的山头。长期生活在这种景色之下，难怪伊朗人都心胸豁达。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/road_hu_7bcd3fb614c9c04f.webp" srcset="https://blog.joway.io/images/iran/road_hu_f832b98c76b922b3.webp 800w, https://blog.joway.io/images/iran/road_hu_6dc2b2463f2bdb6c.webp 1200w, https://blog.joway.io/images/iran/road_hu_7bcd3fb614c9c04f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="900" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="涅瓦兰宫"&gt;涅瓦兰宫&lt;/h2&gt;
&lt;p&gt;涅瓦兰宫是巴列维最后10年居住的地方。这是我拜访过的最不像皇宫的「皇宫」，更加是一个「有钱」的知识分子豪宅。在里面几乎看不到任何权力的象征，而这是其他国家皇宫的标配。有的只是飞机模型，电子器材，家庭照片，儿童玩具。虽然通过一个人的家庭布置去判断一个人有点愚昧，但如果允许的话，我依旧愿意相信他是一位真心想为伊朗做实事的国王。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/niavaran4_hu_9fb51a80cecd4a0d.webp" srcset="https://blog.joway.io/images/iran/niavaran4_hu_87eb334b07a7bb16.webp 800w, https://blog.joway.io/images/iran/niavaran4_hu_2678975a6460d56c.webp 1200w, https://blog.joway.io/images/iran/niavaran4_hu_9fb51a80cecd4a0d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/niavaran2_hu_e701b01a2edab96f.webp" srcset="https://blog.joway.io/images/iran/niavaran2_hu_f096d5cccb3cf9a5.webp 800w, https://blog.joway.io/images/iran/niavaran2_hu_e479dce3f47a5f21.webp 1200w, https://blog.joway.io/images/iran/niavaran2_hu_e701b01a2edab96f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/niavaran1_hu_9a2fd1be09927cb4.webp" srcset="https://blog.joway.io/images/iran/niavaran1_hu_59a98ac493e01a51.webp 800w, https://blog.joway.io/images/iran/niavaran1_hu_beb1b61983d610e7.webp 1200w, https://blog.joway.io/images/iran/niavaran1_hu_9a2fd1be09927cb4.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/iran/niavaran3_hu_df5171d89bc4fa64.webp" srcset="https://blog.joway.io/images/iran/niavaran3_hu_3d11739a25832b33.webp 800w, https://blog.joway.io/images/iran/niavaran3_hu_4b528a27646c24d0.webp 1200w, https://blog.joway.io/images/iran/niavaran3_hu_df5171d89bc4fa64.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="德黑兰"&gt;德黑兰&lt;/h2&gt;
&lt;p&gt;虽然大部分游客会认为德黑兰没有什么意思，的确，它虽为首都，却没有其他城市那么多的历史古迹，也没有壮观的自然景色，只有脏乱的街道和匆忙的人群。但德黑兰是我最喜欢的伊朗城市，它是现代伊朗的综合体现，是最能代表伊朗的城市。在这座年轻的城市的公园、马路、商场、郊区，看到的都是未来能够改变伊朗的基因。&lt;/p&gt;
&lt;iframe loading="lazy" width="560" height="315" src="https://www.youtube.com/embed/MNN1etI-Zoc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id="伊朗故事"&gt;伊朗故事&lt;/h2&gt;
&lt;p&gt;在伊朗最大的收获是结识了那么多热情和可爱的伊朗人，并和他们分享了那么多快乐和故事。&lt;/p&gt;
&lt;p&gt;在设拉子去了一户游牧民族家里吃晚餐，帐篷里进了一只飞蝶，本以为会被拍死，却看到强壮的男主人极其温柔地用手引着它飞出帐篷，才猛然意识到这是穆斯林的生命观。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/nomadism_hu_ca4c2a5f3382290a.webp" srcset="https://blog.joway.io/images/iran/nomadism_hu_a45baef8f9b39af.webp 800w, https://blog.joway.io/images/iran/nomadism_hu_ed9d74190537c304.webp 1200w, https://blog.joway.io/images/iran/nomadism_hu_ca4c2a5f3382290a.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在前往伊斯法罕的大巴上遇到后排一位英语非常流利的女士，帮助我找到位置，并且分享午餐给我。身边坐着的小哥虽然不会英语，但是主动用翻译软件找我聊天。他在军队里做机械工程师，刚刚结束休假要返回军营，还来询问他来中国能不能找到工作。&lt;/p&gt;
&lt;p&gt;在伊斯法罕晚上10点回酒店时，要过一条极其繁忙而且没有红绿灯的马路，等了 5 分钟还是不敢拿生命和伊朗人近乎癫狂的开车方式冒险，突然一个伊朗人从家里跑出来牵着我的手带着我过了马路自己再原路返回。&lt;/p&gt;
&lt;p&gt;在德黑兰的一个公园里遇到可能有超过100个女学生，被他们集体 say hi 所震撼。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/girl-hi_hu_c9541aa37f6be2d6.webp" srcset="https://blog.joway.io/images/iran/girl-hi_hu_f4977091392ee564.webp 800w, https://blog.joway.io/images/iran/girl-hi_hu_e3870348049c37ce.webp 1200w, https://blog.joway.io/images/iran/girl-hi_hu_c9541aa37f6be2d6.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="900" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在古列斯坦皇宫附近散步，走到一个岔口因为想查阅微信就拿出手机看了下，这时旁边一个伊朗女生以为我是不认识路了，来询问我要去哪里。聊上以后和她一起边散步，谈论了伊朗的政治，电影，还载我兜了一圈德黑兰。她家在德黑兰西北部，算是德黑兰版浦东新区，政府修建了一个巨大的人工湖，于是约了第二天去那里看日出。之后一起去了一个更西边的万国花园，比较尴尬的是最后返程时，她的中国国产车电池坏了，不得不一起窘迫地等待保险公司。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/iran/somi_hu_436b0824a8557d7.webp" srcset="https://blog.joway.io/images/iran/somi_hu_33baa27f13f9df0f.webp 800w, https://blog.joway.io/images/iran/somi_hu_747cabb4b010bffd.webp 1200w, https://blog.joway.io/images/iran/somi_hu_436b0824a8557d7.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="伊朗见闻录" loading="lazy" decoding="async"&gt;&lt;/p&gt;</content:encoded></item><item><title>NodeJS 内存泄漏检测与定位</title><link>https://blog.joway.io/posts/nodejs-debug/</link><pubDate>Sun, 10 Nov 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/nodejs-debug/</guid><category>Tech</category><description>最近解决了一个 Node.JS 应用内存泄漏 Bug，顺便学会了用 Chrome DevTools 去看 heapdump 文件。这里做一些简单的记录。
如何「优雅地」获得 heapdump 文件 由于我们所有应用都是以容器部署的，所以要去获得某个容器内的文件，并拷贝到本地难度还是比较大，也非常麻烦。考虑到调试时或许会需要下载非常多次的 snapshot 文件，建议可以包下 heapdump 库，做成一个接口，把文件 dump 之后再传输给客户端，这样一劳永逸。</description><content:encoded>&lt;p&gt;最近解决了一个 Node.JS 应用内存泄漏 Bug，顺便学会了用 Chrome DevTools 去看 heapdump 文件。这里做一些简单的记录。&lt;/p&gt;
&lt;h2 id="如何优雅地获得-heapdump-文件"&gt;如何「优雅地」获得 heapdump 文件&lt;/h2&gt;
&lt;p&gt;由于我们所有应用都是以容器部署的，所以要去获得某个容器内的文件，并拷贝到本地难度还是比较大，也非常麻烦。考虑到调试时或许会需要下载非常多次的 snapshot 文件，建议可以包下 &lt;a href="https://www.npmjs.com/package/heapdump"&gt;heapdump&lt;/a&gt; 库，做成一个接口，把文件 dump 之后再传输给客户端，这样一劳永逸。&lt;/p&gt;
&lt;p&gt;需要小心的是，在 heapdump 的时候内存很容易翻倍，所以当内存超过 500 MB的时候如果去 heapdump 非常容易导致 OOM 从而 Crash。&lt;/p&gt;
&lt;h2 id="如何检测内存泄漏"&gt;如何检测内存泄漏&lt;/h2&gt;
&lt;p&gt;检查内存泄漏有两种方法，一种是针对比较大的内存泄漏可以直接观察内存是否一直在稳步上升。如果是一些小的泄漏使得内存上升变化并不非常明显的话，可以通过对比不同时间的 heapdump 文件。&lt;/p&gt;
&lt;p&gt;有时候内存上升也可能是因为本身访问量就在上升，所以需要两者对比着分析。&lt;/p&gt;
&lt;h3 id="heapdump-文件对比"&gt;Heapdump 文件对比&lt;/h3&gt;
&lt;p&gt;通过下载两份间隔一段时间(几分钟)的 heapdump 文件，打开 Chrome DevTools，进入 Memory Tab，选择 Load。选中其中时间更近的 heapdump ，并选择 Comparison，比较对象是老的那份 heapdump：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/nodejs-debug/01_hu_c148eafd81b7030.webp" srcset="https://blog.joway.io/images/nodejs-debug/01_hu_c68bdcdb80e06067.webp 800w, https://blog.joway.io/images/nodejs-debug/01_hu_eb6b9855c0042654.webp 1200w, https://blog.joway.io/images/nodejs-debug/01_hu_c148eafd81b7030.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1000" alt="NodeJS 内存泄漏检测与定位" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;此时可以选择按 Delta 排序，可以看到两个时间点增加了哪些新的对象。&lt;/p&gt;
&lt;p&gt;如图可以看到 string 和 Object 的 Delta 是差不多的，所以可以比较确定是由于 Object 里产生了大量一些 string 对象导致的数量增多，但并不一定能够100%确定是内存泄漏，也可能是正常业务波动。此时需要再拉新的一个时间点的 heapdump 文件再来对比，如果一直在增加，那么内存泄漏的可能性就非常大了。&lt;/p&gt;
&lt;h2 id="如何定位内存泄漏"&gt;如何定位内存泄漏&lt;/h2&gt;
&lt;p&gt;首先依旧是拿到 heapdump 文件，并在 Chrome 中打开。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/nodejs-debug/02_hu_29bc4541bd0d5342.webp" srcset="https://blog.joway.io/images/nodejs-debug/02_hu_a4440759081fafa6.webp 800w, https://blog.joway.io/images/nodejs-debug/02_hu_90cb02995f7bbe8d.webp 1200w, https://blog.joway.io/images/nodejs-debug/02_hu_29bc4541bd0d5342.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="509" alt="NodeJS 内存泄漏检测与定位" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这里有一些名词需要解释含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distance: 距离 GC 跟节点的距离&lt;/li&gt;
&lt;li&gt;Objects Count: 对象数目&lt;/li&gt;
&lt;li&gt;Shallow Size: 对象自身被创建时，在堆上申请的大小&lt;/li&gt;
&lt;li&gt;Retained Size: 把此对象从堆上移除，FullGC 能够释放的空间大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以先不管别的值，只看 &lt;code&gt;Retained Size&lt;/code&gt; 。从上图我们可以看到，Object 的 &lt;code&gt;Retained Size&lt;/code&gt; 是最大的，所以可以点开浏览它里面的元素。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/nodejs-debug/03_hu_366337f8c18ddc14.webp" srcset="https://blog.joway.io/images/nodejs-debug/03_hu_8452186383396514.webp 800w, https://blog.joway.io/images/nodejs-debug/03_hu_2c12938eae7975dc.webp 1200w, https://blog.joway.io/images/nodejs-debug/03_hu_366337f8c18ddc14.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="809" alt="NodeJS 内存泄漏检测与定位" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;如图标红的是该元素的引用关系，即在代码 &lt;code&gt;[engine.io/lib/server.js](https://engine.io/lib/server.js)&lt;/code&gt; 中 &lt;code&gt;nsps(Server)&lt;/code&gt; 对象的 &lt;code&gt;/notification&lt;/code&gt; 属性下的 &lt;code&gt;adapter(Namespace)&lt;/code&gt; 属性里的 &lt;code&gt;sids&lt;/code&gt; 属性中引用了我们选中的对象 72257。sids 的值就是选中的对象。&lt;/p&gt;
&lt;p&gt;通过查看这个对象，我们能够发现是否存在异常的内容，而通过 Retainers 里的引用关系，我们能够找到该对象在代码中的定位。如果值的内容并无异常，那有可能是 Retainers 里的引用关系导致它一直没有被释放。&lt;/p&gt;
&lt;p&gt;此处很难总结出什么方法论，但主要思想就是根据 Retained Size 递减排序一路找下去。只要是内存泄漏， Retained Size 一定是会高，但反过来 Retained Size 高不一定是内存泄漏，依照这个逻辑，顺藤摸瓜总能找到一些蛛丝马迹。&lt;/p&gt;</content:encoded></item><item><title>设计实现高性能本地内存缓存</title><link>https://blog.joway.io/posts/modern-memory-cache/</link><pubDate>Sun, 10 Nov 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/modern-memory-cache/</guid><category>Tech</category><description>本地内存缓存是一个在基础软件架构中非常常见的基础设施，也正因其过于常见，以至于平时很少去思考它是如何实现的。在尚未设计缓存系统前，完全没想到原来要需要考虑如此多复杂的事情。本文将由浅入深介绍如何设计一个现代的高性能内存缓存系统。
什么时候需要本地内存缓存 在大部分业务系统中，都会使用诸如 Redis、Memcached 等远程缓存，一方面可以避免自身进程内存占用过大而导致的 OOM 或 GC 问题，另一方面也可以实现多个进程共享同一份一致的缓存数据。但对于某些底层服务（例如数据库服务），远程缓存的网络延迟是不可接受的，这就势必需要引入本地内存缓存。</description><content:encoded>&lt;p&gt;本地内存缓存是一个在基础软件架构中非常常见的基础设施，也正因其过于常见，以至于平时很少去思考它是如何实现的。在尚未设计缓存系统前，完全没想到原来要需要考虑如此多复杂的事情。本文将由浅入深介绍如何设计一个现代的高性能内存缓存系统。&lt;/p&gt;
&lt;h2 id="什么时候需要本地内存缓存"&gt;什么时候需要本地内存缓存&lt;/h2&gt;
&lt;p&gt;在大部分业务系统中，都会使用诸如 Redis、Memcached 等远程缓存，一方面可以避免自身进程内存占用过大而导致的 OOM 或 GC 问题，另一方面也可以实现多个进程共享同一份一致的缓存数据。但对于某些底层服务（例如数据库服务），远程缓存的网络延迟是不可接受的，这就势必需要引入本地内存缓存。&lt;/p&gt;
&lt;h2 id="本地内存缓存的特点"&gt;本地内存缓存的特点&lt;/h2&gt;
&lt;p&gt;本地内存缓存可被视作一个基于本地内存的 「KeyValue 数据库」。但相比较于传统数据库而言，它对一致性的要求十分宽松：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于更新与删除的操作，需要保证强一致性&lt;/li&gt;
&lt;li&gt;对于插入操作可以容忍少量丢失&lt;/li&gt;
&lt;li&gt;对于读取操作可以容忍少量 Miss&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;与磁盘数据库的另一个不同之处在于，磁盘数据库的设计有一个前提假设是磁盘是可以随需要而不断扩容的，倘若一个磁盘数据库因磁盘占满而崩溃主要责任是在使用方。而内存缓存则没有这么宽容的假设可以建立，它必须考虑到内存是昂贵且有限的这一事实。&lt;/p&gt;
&lt;p&gt;除此之外，由于本地内存缓存处于业务进程当中，所以其需要考虑更多业务向的问题，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于自身大量老生代的内存占用，是否会对所处进程产生 GC 问题。&lt;/li&gt;
&lt;li&gt;当多线程场景下，如何同时解决线程安全、数据竞争、高吞吐等问题。&lt;/li&gt;
&lt;li&gt;需要能够适应一些非随机的访问统计规律，例如 Zipf。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上所述，我们可以归纳出对一个优秀的本地内存缓存系统的要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程安全&lt;/li&gt;
&lt;li&gt;高吞吐&lt;/li&gt;
&lt;li&gt;高命中率&lt;/li&gt;
&lt;li&gt;支持内存限制&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="实现路径"&gt;实现路径&lt;/h2&gt;
&lt;p&gt;在实现一个完整的缓存系统前，我们需要将目标一步步拆解。&lt;/p&gt;
&lt;p&gt;首先为了实现缓存逻辑，我们必须有一个类 Map 的 KeyValue 数据结构，同时它必须是线程安全的。为了支持内存限制，我们必须要能够驱逐一些 key，所以需要实现一个驱逐器。为了实现驱逐的同时维持高命中率，我们还需要告诉驱逐器每个 key 的访问记录，让它能够从中分析出哪些 key 可以被驱逐。综上分析，我们可以整理出一个大概的 Roadmap：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现一个线程安全的 Map 数据结构：存储缓存内容&lt;/li&gt;
&lt;li&gt;实现一个访问记录队列：存储访问记录&lt;/li&gt;
&lt;li&gt;实现一个驱逐器：管理缓存内容&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文所有代码均使用 Golang 编写。&lt;/p&gt;
&lt;h3 id="线程安全的-map"&gt;线程安全的 Map&lt;/h3&gt;
&lt;h4 id="简易的-map"&gt;简易的 Map&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cache := map[string]string{}
cache[&amp;quot;a&amp;quot;] = &amp;quot;b&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 key 数量固定且极少的情况下，我们一般会用原生 Map 直接实现一个最简单缓存。但 Golang 原生 Map 并不是线程安全的，当多个 goroutine 同时读写该对象时，会发生冲突。&lt;/p&gt;
&lt;h4 id="线程安全的-safemap"&gt;线程安全的 SafeMap&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;type SafeMap struct {
lock sync.Mutex
store map[string]string
}
func (m *SafeMap) Get(k string) string {
m.lock.Lock()
defer m.lock.Unlock()
return m.store[k]
}
func (m *SafeMap) Set(k, v string) {
m.lock.Lock()
defer m.lock.Unlock()
m.store[k] = v
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个最简单的线程安全 Map 实现。对于访问量很小的系统，这已经能够成为一个非常方便快速的实现了，但需要注意的是，这个 Map 是被该进程下的所有线程所共享的，任何一个修改都需要去竞争得到一个锁，如果套用数据库领域的概念，这个锁就是数据库级别的锁，显然对于并发量大的时候是不适合的，会成为整个系统的瓶颈。&lt;/p&gt;
&lt;h4 id="分段锁的-safemap"&gt;分段锁的 SafeMap&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;type SafeMap struct {
locks []*sync.Mutex
store []map[string]string
}
func NewSafeMap() SafeMap {
return SafeMap{
locks: []*sync.Mutex{{}, {}, {}},
store: []map[string]string{{}, {}, {}},
}
}
func hash(k string) int {
h := fnv.New32a()
h.Write([]byte(k))
return int(h.Sum32())
}
func (m *SafeMap) GetLock(k string) *sync.Mutex {
idx := hash(k) % len(m.locks)
return m.locks[idx]
}
func (m *SafeMap) GetStore(k string) map[string]string {
idx := hash(k) % len(m.locks)
return m.store[idx]
}
func (m *SafeMap) Get(k string) string {
lock := m.GetLock(k)
lock.Lock()
defer lock.Unlock()
return m.GetStore(k)[k]
}
func (m *SafeMap) Set(k, v string) {
lock := m.GetLock(k)
lock.Lock()
defer lock.Unlock()
m.GetStore(k)[k] = v
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个很自然的想法是将 key 进行分桶，从而分散对锁的竞争。这种方法类似于将「数据库锁」打散成「表锁」。到这一步，我们基本已经完成了一个最简单的高并发缓存。&lt;/p&gt;
&lt;h4 id="读写分段锁的-safemap"&gt;读写分段锁的 SafeMap&lt;/h4&gt;
&lt;p&gt;考虑到缓存系统读远大于写，我们可以对上述 Map 的互斥锁 &lt;code&gt;Mutex&lt;/code&gt; 改为 &lt;code&gt;RWMutex&lt;/code&gt; ，从而使得读时并不互斥，改善读性能。&lt;/p&gt;
&lt;h4 id="使用线程-id-实现无锁"&gt;使用线程 ID 实现无锁&lt;/h4&gt;
&lt;p&gt;需要注意的是，上述 Map 中，我们使用的分桶方法是利用 key 做随机哈希，这种做法只能缓解锁竞争的问题，却无法根治。那么是否有办法根治这里的锁竞争呢？&lt;/p&gt;
&lt;p&gt;办法和代价都是有的。如果我们可以让某一块内存只被某个线程访问，那么就可以完全避免这些线程之间的锁竞争，从而实现无锁。假设每一个线程都有一个线程ID，我们可以按线程ID去分段，每个线程独占一个 SafeMap。&lt;/p&gt;
&lt;p&gt;这样做虽然避免了锁，但也同时造成了数据「膨胀」。如果同一个 key 被N个线程 Set 了多次，此时内存中就多了 N 份同样的数据。如果它只被 Set 了一次，也将导致其他线程没法取得这个数据，从而出现非常高的 Miss 率。但对于那些极其热门的少量 key，这种方式的确可以作为一种优化选择。&lt;/p&gt;
&lt;p&gt;令人遗憾的是，在 Golang 中，由于 GPM 调度模型的存在，在 Runtime 中屏蔽了线程所有相关信息，所以我们是没有正常的办法获得「线程ID」的信息，因而此文暂不考虑上述方案。&lt;/p&gt;
&lt;h4 id="使用-syncmap-实现无锁"&gt;使用 sync.Map 实现无锁&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/02_hu_7fe113bbc1a1a2d1.webp" srcset="https://blog.joway.io/images/modern-memory-cache/02_hu_8ec41615df5f3812.webp 800w, https://blog.joway.io/images/modern-memory-cache/02_hu_1d1b2a3d528f0f1.webp 1200w, https://blog.joway.io/images/modern-memory-cache/02_hu_7fe113bbc1a1a2d1.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="694" alt="设计实现高性能本地内存缓存" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图片来源: &lt;a href="https://halfrost.com/go_map_chapter_two/"&gt;如何设计并实现一个线程安全的 Map ？(下篇)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;准确来说，&lt;code&gt;sync.Map&lt;/code&gt; 并不是完全的「无锁」，只是一个在绝大部分读场景是无锁的线程安全 Map。具体原理可以参见相关文档。但由于其底层实现并未采取分段锁的方法，所以写的时候会有一个 dirty 上的全局锁，进而会影响到高并发写时的性能。所以对于不在乎写性能同时写也相对不密集的时候，该数据结构是非常理想的选择。&lt;/p&gt;
&lt;h4 id="设计"&gt;设计&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/01_hu_c1dc2c56bbd65bcf.webp" srcset="https://blog.joway.io/images/modern-memory-cache/01_hu_47bdabab61639030.webp 800w, https://blog.joway.io/images/modern-memory-cache/01_hu_972aff84fed6fc05.webp 1200w, https://blog.joway.io/images/modern-memory-cache/01_hu_c1dc2c56bbd65bcf.webp 1532w" sizes="(max-width: 800px) 100vw, 800px" width="1532" height="1010" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="访问记录队列"&gt;访问记录队列&lt;/h3&gt;
&lt;p&gt;对于访问记录的读写，同样牵涉到多线程同时操作同一个内存地址的情况。但我们对其一致性会比缓存内容存储更低，尤其是在高并发数据的假设下，少量的数据丢失并不会影响最终判断结果。&lt;/p&gt;
&lt;p&gt;与缓存内容存储的场景不同的是，对于访问记录，每次 Get/Set 的时候都会需要进行一次写入操作，所以它的写速度要求远高于前面的缓存内存存储。更为关键的是，即便是在如此高密度的写情况下，它也同样需要保证线程安全。&lt;/p&gt;
&lt;p&gt;虽然上述要求看似十分复杂，我们依然可以试着通过几个方面的分析，来拆解这个问题。&lt;/p&gt;
&lt;p&gt;在性能方面，我们需要保证该数据结构写入时是无锁的，因为一旦有锁，前面做的降低锁颗粒度优化都会被这个有锁的结构给拖累。&lt;/p&gt;
&lt;p&gt;在写入方式方面，由于我们可以接受少量数据丢失，并且我们没有非常实时的要求，所以我们可以接受异步的写入。&lt;/p&gt;
&lt;p&gt;在存储内容方面，我们只需要存储 Key 数据。&lt;/p&gt;
&lt;p&gt;根据上述分析，我们不难发现我们需要的是一个基于内存的线程安全的无锁 Lossy 队列。但似乎并没有现成的这种数据结构实现，所以我们可以退一步将这个问题变成，先实现一个 Lossy 队列，再在此基础上，实现线程安全的功能。&lt;/p&gt;
&lt;h4 id="环形缓冲ringbuffer"&gt;环形缓冲：RingBuffer&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/05_hu_cfd0203a09fecd5c.webp" width="350" height="350" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;RingBuffer 是一个非常简单的环形缓冲队列，由一个数组，加上一个读指针和写指针构成。读指针永远只能读到写指针前的数据。&lt;/p&gt;
&lt;h4 id="线程安全支持syncpool"&gt;线程安全支持：sync.Pool&lt;/h4&gt;
&lt;p&gt;Golang 自带的 &lt;code&gt;sync.Pool&lt;/code&gt; 可以非常好地和 Ring Buffer 协同工作，实现在近乎无锁的情况下，构造出一个线程安全的高吞吐缓冲队列。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/06_hu_fd008b7d09e64724.webp" srcset="https://blog.joway.io/images/modern-memory-cache/06_hu_43acef9e6038e918.webp 800w, https://blog.joway.io/images/modern-memory-cache/06_hu_8771c9856760e7a1.webp 1200w, https://blog.joway.io/images/modern-memory-cache/06_hu_fd008b7d09e64724.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="832" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图片来源：&lt;a href="https://programmer.group/a-brief-analysis-of-golang-sync.pool.html"&gt;A Brief Analysis of Golang Sync.Pool&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;sync.Pool&lt;/code&gt; 会在每个线程中维护一个 private 的 Pool（无锁），以及一个可以被其他线程 shared的 Pool（有锁），细节原理可以参考相关文档。在高并发场景下，它基本能够保证每个线程都能够获得一个线程私有的 RingBuffer 对象，从而不需要对其加锁。但 &lt;code&gt;sync.Pool&lt;/code&gt; 有一个缺点是在 GC 时会被释放掉，此时会丢失缓冲区内的数据。不过由于我们的前提假设是高并发场景，故而可以推导出数据的丢失量较之于全局是微乎其微的。然而在低并发场景下，这种做法有可能导致缓冲区一直被 GC 清理掉而丧失大部分统计数据。&lt;/p&gt;
&lt;p&gt;这里对 RingBuffer 做了一些简单的改动，当缓冲区写满后，会将数据交给驱逐器统计，然后清空缓冲区。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
&amp;quot;sync&amp;quot;
)
type ringStripe struct {
store []uint64
capacity uint64
}
func newRingStripe(capacity uint64) *ringStripe {
return &amp;amp;ringStripe{
store: make([]uint64, 0, capacity),
capacity: capacity,
}
}
func (s *ringStripe) PushOrReturn(item uint64) []uint64 {
s.store = append(s.store, item)
if uint64(len(s.store)) &amp;gt;= s.capacity {
ret := s.store[:]
s.store = make([]uint64, 0, s.capacity)
return ret
}
return nil
}
type ringBuffer struct {
stripes []*ringStripe
pool *sync.Pool
}
func newRingBuffer(capacity uint64) *ringBuffer {
return &amp;amp;ringBuffer{
pool: &amp;amp;sync.Pool{
New: func() interface{} {
return newRingStripe(capacity)
},
},
}
}
func (b *ringBuffer) PushOrReturn(item uint64) []uint64 {
stripe := b.pool.Get().(*ringStripe)
defer b.pool.Put(stripe)
got := stripe.PushOrReturn(item)
return got
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="设计-1"&gt;设计&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/09_hu_3bf98e82d1f0b467.webp" srcset="https://blog.joway.io/images/modern-memory-cache/09_hu_7fe0a7a7af8c3c61.webp 800w, https://blog.joway.io/images/modern-memory-cache/09_hu_3bf98e82d1f0b467.webp 987w" sizes="(max-width: 800px) 100vw, 800px" width="987" height="423" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="驱逐器"&gt;驱逐器&lt;/h3&gt;
&lt;h4 id="驱逐策略"&gt;驱逐策略&lt;/h4&gt;
&lt;p&gt;通过不停读访问记录环形缓冲队列，我们能够拿到用户的访问记录。此时我们有两种驱逐策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LRU（Least Recently Used） ：最少最近使用，即维护一个数组，越靠前访问时间越近。&lt;/li&gt;
&lt;li&gt;LFU （Least Frequently Used）：最少频率使用，即需要记录 Key 使用的频率，越低越容易被驱逐。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LRU 的问题在于，如果在某个数据在前9分钟访问了1万次，最近1分钟没有访问，那么依然会认为该 key 并不热门而有可能被驱逐。&lt;/p&gt;
&lt;p&gt;LFU 的问题在于，经常会有一些数据在某时刻非常极其热门，但之后一直没人访问，例如因为某些原因被隐藏的用户动态这类场景。另外，LFU 的频率信息在缓存失效后依旧会存在内存中。&lt;/p&gt;
&lt;p&gt;值得注意的一点是，缓存系统的驱逐往往是由于写入而引起的，换句话说，是为了在缓存中，给更加重要的 key 腾出空间，才驱逐出那些没它重要的 key。那么问题来了，无论是 LRU 还是 LFU 的写入过程中，都有一个假设是新来的 key 一定是更重要的，以至于我必须牺牲掉某个已有的 key。但这个假设很可能是不成立的。而且这种方式很容易导致一些冷门数据在短时间过热导致缓存系统迅速驱逐出了原先的那些热门数据。为了解决上述问题，于是就有了 TinyLFU。&lt;/p&gt;
&lt;p&gt;TinyLFU 利用 LFU 作为写入过滤器，只有当新来的 key 的频率大于需要被驱逐的 key 时，此时才会执行写入，否则只进行频率信息的累加。也就是说所有新的 key 都会有一个被预热的过程才能够「够格」被写入缓存中。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/08_hu_4f57fec267d7e58.webp" width="800" height="395" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;但此时会存在的一个问题是，当有突发性的稀疏流量（sparse bursts）进来时，他们会由于一直无法建立足够的频率使得自己被缓存系统而接纳，从而导致击穿了缓存。为了解决这个问题，于是又有了 W-TinyLFU。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/04_hu_beb0123426fe4d84.webp" srcset="https://blog.joway.io/images/modern-memory-cache/04_hu_c63ec83a124d7f8c.webp 800w, https://blog.joway.io/images/modern-memory-cache/04_hu_faff99820e59df29.webp 1200w, https://blog.joway.io/images/modern-memory-cache/04_hu_beb0123426fe4d84.webp 1208w" sizes="(max-width: 800px) 100vw, 800px" width="1208" height="536" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;W-TinyLFU 算法吸收了上述算法的优点，在 TinyLFU 前面放了一个基于 LRU 的 Window Cache，从而可以使得前面提到的突发性稀疏流量会缓存在 Window Cache 里，只有在 Window Cache 里被淘汰的缓存才会过继给后面的 TinyLFU。至于最后的 Main Cache，虽然 W-TinyLFU 使用了分段式 LRU 来实现，但我们也可以根据实际情况修改使其符合我们需要的场景。&lt;/p&gt;
&lt;p&gt;TinyLFU &amp;amp;&amp;amp; W-TinyLFU 算法是由 Gil Einziger、Roy Friedman 和 Ben Manes 三人在 15 年共同写的论文：&lt;a href="https://arxiv.org/abs/1512.00727"&gt;TinyLFU: A Highly Efficient Cache Admission Policy&lt;/a&gt; 所提出来的，后来 Ben Manes 还依照这个算法写了一个 Java 领域备受欢迎的缓存系统 &lt;a href="https://github.com/ben-manes/caffeine"&gt;Caffeine&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了简化本文的实现，我们暂时先不实现 W-TinyLFU 算法（W-TinyLFU 的实现会另外写一篇文章介绍），而是实现一个简单的 LFU 驱逐策略。因此我们需要一个能够用来记录访问频率的数据结构。同时由于我们需要存储所有 key 的信息，所以还需要这个数据结构能够有效减少 key 的存储体积。&lt;/p&gt;
&lt;p&gt;即便有了上面的频率计数器，为了找到那个需要被驱逐的 LFU key，我们似乎需要遍历所有 key。所以我们不得不再引入一个驱逐候选列表来帮助我们提前排序好需要驱逐的 key。&lt;/p&gt;
&lt;p&gt;综上，我们还需要再实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能够有效压缩数据大小的频率计数器&lt;/li&gt;
&lt;li&gt;预先排序的驱逐候选池&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="频率计数器count-min-sketch"&gt;频率计数器：Count-Min Sketch&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/03_hu_8d36d5ed2e9af18f.webp" width="646" height="272" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;Count-Min 算法和布隆过滤器类似，核心思想还是通过将相同 Hash 值的数据共享同一份存储空间，以减小整体体积。h1~hd 代表不同的 Hash 算法，这里的 value 代表我们要存储的 key，横坐标表示 Hash 后的值，对哈希值对应每个网格进行 +1 操作。当需要计算某个 key 的估计值时，取对应所有网格数值的最小值。&lt;/p&gt;
&lt;p&gt;为了避免一些短时间热度的 key 一直残留在缓存中，每隔一个时间间隔，还需要将所有网格计数器衰减一半。&lt;/p&gt;
&lt;h4 id="设计-2"&gt;设计&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/modern-memory-cache/07_hu_4c0a6837ebaf9f53.webp" srcset="https://blog.joway.io/images/modern-memory-cache/07_hu_62aaf19998ff6e9.webp 800w, https://blog.joway.io/images/modern-memory-cache/07_hu_4c0a6837ebaf9f53.webp 1120w" sizes="(max-width: 800px) 100vw, 800px" width="1120" height="973" alt="设计实现高性能本地内存缓存" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;经过一系列的步骤，我们终于实现了一个满足我们要求的现代内存缓存系统。可以看到，在缓存系统的设计中，对性能影响最大的是缓存的存储层，需要非常小心地进行锁的设计和优化。而对于缓存系统命中率影响最大，同时也是实现算法上最复杂的还是淘汰策略的选择。现代的许多内存缓存系统所选择的淘汰策略各有不同，许多都在现有的算法基础上做过一些自己的修改。即便一些缓存系统在 Benchmark 中非常优秀，但如果其测试数据访问模式与你的实际使用场景并不一致，它的数据对你的参考意义也并不大。所以依旧需要针对性地进行模拟压测才能够知道什么样的缓存系统适合你业务的场景。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html"&gt;Design Of A Modern Cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/1512.00727"&gt;TinyLFU: A Highly Efficient Cache Admission Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.dgraph.io/post/introducing-ristretto-high-perf-go-cache/"&gt;Introducing Ristretto: A High-Performance Go Cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.dgraph.io/post/caching-in-go/"&gt;The State of Caching in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juejin.im/post/5b7593496fb9a009b62904fa"&gt;你应该知道的缓存进化史&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Linux I/O 栈浅析</title><link>https://blog.joway.io/posts/linux-io-stack/</link><pubDate>Sun, 11 Aug 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/linux-io-stack/</guid><category>Tech</category><description>在 Linux 中，所有外部资源都以文件形式作为一个抽象视图，并提供一套统一的接口给应用程序调用。本文将以宏观视角试图阐述 Linux 中关于文件 IO 的整个调用脉络。
VFS 在 Linux 中，所有 IO 都必须先经由 VFS 层进行转发。通过 VFS 将包括磁盘、网络 Socket、打印机、管道等资源全部封装成统一的接口。</description><content:encoded>&lt;p&gt;在 Linux 中，所有外部资源都以文件形式作为一个抽象视图，并提供一套统一的接口给应用程序调用。本文将以宏观视角试图阐述 Linux 中关于文件 IO 的整个调用脉络。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/linux-io/io-stack_hu_93be886128dbb410.webp" srcset="https://blog.joway.io/images/linux-io/io-stack_hu_83a434b2c67481ca.webp 800w, https://blog.joway.io/images/linux-io/io-stack_hu_561084d1d40f7fc8.webp 1200w, https://blog.joway.io/images/linux-io/io-stack_hu_93be886128dbb410.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="2262" alt="Linux I/O 栈浅析" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h2 id="vfs"&gt;VFS&lt;/h2&gt;
&lt;p&gt;在 Linux 中，所有 IO 都必须先经由 VFS 层进行转发。通过 VFS 将包括磁盘、网络 Socket、打印机、管道等资源全部封装成统一的接口。&lt;/p&gt;
&lt;h4 id="基础结构"&gt;基础结构&lt;/h4&gt;
&lt;p&gt;VFS 自顶向下使用四个数据结构来描述文件：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/linux-io/vfs-struct_hu_86f6d28dd742e23d.webp" srcset="https://blog.joway.io/images/linux-io/vfs-struct_hu_c0d544643ba2fa65.webp 800w, https://blog.joway.io/images/linux-io/vfs-struct_hu_8cd0967f1f244519.webp 1200w, https://blog.joway.io/images/linux-io/vfs-struct_hu_86f6d28dd742e23d.webp 1380w" sizes="(max-width: 800px) 100vw, 800px" width="1380" height="754" alt="Linux I/O 栈浅析" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file: 存放一个文件对象的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;llist_node&lt;/span&gt; &lt;span class="n"&gt;fu_llist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;rcu_head&lt;/span&gt; &lt;span class="n"&gt;fu_rcuhead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;f_u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;f_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;f_inode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* cached value */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;f_op&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="n"&gt;f_pos_lock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;loff_t&lt;/span&gt; &lt;span class="n"&gt;f_pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;dentry: 存放目录项和其下的文件链接信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;dentry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;d_flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;seqcount_t&lt;/span&gt; &lt;span class="n"&gt;d_seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hlist_bl_node&lt;/span&gt; &lt;span class="n"&gt;d_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 哈希链表 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;dentry&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d_parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 父目录项 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;qstr&lt;/span&gt; &lt;span class="n"&gt;d_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 目录名 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d_inode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 对应的索引节点 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;d_iname&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DNAME_INLINE_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="cm"&gt;/* small names */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;lockref&lt;/span&gt; &lt;span class="n"&gt;d_lockref&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* per-dentry lock and refcount */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;dentry_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d_op&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* dentry操作 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;super_block&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d_sb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 文件的超级块对象 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;d_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d_fsdata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;d_lru&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* LRU list */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;d_child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* child of parent list */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;d_subdirs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* our children */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hlist_node&lt;/span&gt; &lt;span class="n"&gt;d_alias&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* inode alias list */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;rcu_head&lt;/span&gt; &lt;span class="n"&gt;d_rcu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;d_u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;inode: 索引节点对象，存在具体文件的一般信息，文件系统中的文件的唯一标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hlist_node&lt;/span&gt; &lt;span class="n"&gt;i_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 散列表，用于快速查找inode */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;i_list&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 相同状态索引节点链表 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;i_sb_list&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 文件系统中所有节点链表 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;list_head&lt;/span&gt; &lt;span class="n"&gt;i_dentry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 目录项链表 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;i_ino&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 节点号 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;atomic_t&lt;/span&gt; &lt;span class="n"&gt;i_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 引用计数 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i_nlink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 硬链接数 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uid_t&lt;/span&gt; &lt;span class="n"&gt;i_uid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 使用者id */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;gid_t&lt;/span&gt; &lt;span class="n"&gt;i_gid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 使用组id */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="n"&gt;i_atime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 最后访问时间 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="n"&gt;i_mtime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 最后修改时间 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="n"&gt;i_ctime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 最后改变时间 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i_op&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 索引节点操作函数 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i_fop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 缺省的索引节点操作 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;super_block&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i_sb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 相关的超级块 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;address_space&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i_mapping&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 相关的地址映射 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;address_space&lt;/span&gt; &lt;span class="n"&gt;i_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 设备地址映射 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i_flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 文件系统标志 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i_private&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* fs 私有指针 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;i_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;superblock: 超级块对象，记录该文件系统的整体信息。在文件系统安装时建立，在文件系统卸载时删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="链接"&gt;链接&lt;/h4&gt;
&lt;p&gt;硬链接 VS 软链接:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬链接为目标文件创建了一个新的 dentry，并将 dentry 写入父目录的数据中。&lt;/li&gt;
&lt;li&gt;软链接创建了全新的文件，只不过它的数据保存的是另一个文件的路径，所以它有一个全新的 inode。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;硬链接存在的文件必须实际存在，而软链接无所谓目标文件是否存在。&lt;/p&gt;
&lt;p&gt;如果删除了原始文件的话，软链接会直接生效，但是硬链接依旧存在，因为 inode 的计数并没有变成0，所以对于硬链接而言，事实上原始文件并没有删除。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/linux-io/link_hu_fbcad23a2c2a0fe9.webp" width="543" height="386" alt="Linux I/O 栈浅析" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h4 id="page-cache"&gt;Page Cache&lt;/h4&gt;
&lt;p&gt;当 VFS 读取的 Page 不在 Cache 中时，先从外存读取数据并缓存进 Cache，再返回。之后当再读取同样的 Page 时，会先检查 Page Cache，如果已经存在，便不会再触发下层 IO。&lt;/p&gt;
&lt;p&gt;当 VFS 试图写入 Page 时，除了写入外存以外，也会往 Cache 中写入新页。从而使得对新写入的页的读取可以不触发实际外存IO。正是由于这种性质，使得消息队列这类读写都集中在新数据上的应用，即便运行在 HDD 上也能够有惊人的读取性能。&lt;/p&gt;
&lt;h4 id="当网络存储遇上-page-cache"&gt;当网络存储遇上 Page Cache&lt;/h4&gt;
&lt;p&gt;从 IO 层次图中我们可以发现，Page Cache 实现在 VFS 层，当读写都在本地时，的确不会出现问题。但当使用 NFS 这类网络存储时，远程进行的写操作并不能同步给本地，从而导致 Cache 无法被及时地 invalidate，导致读的还是老的数据。对于这种情况可以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 NFS 客户端处设置不缓存文件&lt;/li&gt;
&lt;li&gt;调低目录属性缓存的最大时间 acdirmax&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但如果存储的是不变的数据，例如归档的日志这类，在进行数据分析时，也能够充分利用 Page Cache 提供的缓存优势。&lt;/p&gt;
&lt;h4 id="直接-io"&gt;直接 IO&lt;/h4&gt;
&lt;p&gt;许多应用自身已经实现了缓存策略，此时操作系统自带的 Page Cache 可能会成了冗余。通过在打开文件时候设置 O_DIRECT 可以绕过 Page Cache，直接操作文件。&lt;/p&gt;
&lt;p&gt;直接 IO 相比与默认方式减少了内存数据拷贝次数，降低了对 CPU 和内存带宽的使用，在数据量巨大的情况下，可以大大提升性能。&lt;/p&gt;
&lt;h2 id="文件系统"&gt;文件系统&lt;/h2&gt;
&lt;p&gt;文件系统是一种存储和组织数据的方法，使得用户对文件的访问、查找、管理变得更加容易。通过文件系统这一层抽象，隐藏了直接管理外存的复杂性。&lt;/p&gt;
&lt;p&gt;下图展示了读取文件 /var/log/messages 的完整过程：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/linux-io/writes_hu_db1d91be4b716830.webp" width="800" height="319" alt="Linux I/O 栈浅析" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;目前人们常用通用文件系统有 ext4 和 xfs。而在诸多细分领域，针对不同场景有非常多的新文件系统在近些年诞生。例如对于海量小文件（常见的图片、静态资源）的存储，有 FastDFS ，对 SSD 有专门优化的 JFFS2。FastDFS 通过在文件系统层把小文件合并成大文件，从而减轻大量小文件对系统的开销。而 JFFS2 通过把 data 和 metadata 在 SSD 上顺序存储，并使用 ouf-of-place 的方式更新，来减轻对 SSD 寿命的影响。&lt;/p&gt;
&lt;h4 id="分区"&gt;分区&lt;/h4&gt;
&lt;p&gt;文件系统自身作为一种软件实现并不一定100%可靠，虽然现代文件系统通过日志等技术已经极少出现系统故障，但即便如此，在使用文件系统的过程中，依旧会出现意外情况例如文件写满。通过文件系统的分区可以把故障限制在局部上，不至于造成全局性影响。&lt;/p&gt;
&lt;h3 id="fuse"&gt;FUSE&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/linux-io/fuse_hu_43e3ab117fa42f4f.webp" srcset="https://blog.joway.io/images/linux-io/fuse_hu_a251095061b03a0a.webp 800w, https://blog.joway.io/images/linux-io/fuse_hu_79023cbd5d692ee.webp 1200w, https://blog.joway.io/images/linux-io/fuse_hu_43e3ab117fa42f4f.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1211" alt="Linux I/O 栈浅析" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;FUSE 全称 Filesystem in Userspace，是一个支持用户在用户态编写文件系统代码的内核模块，在 Linux 2.6.14 后开始支持。一般多用于分布式文件系统，例如 hdfs，ceph，s3fs 等。&lt;/p&gt;
&lt;p&gt;由于 FUSE 极大地简化了文件系统的开发门槛，使得我们用数十行代码便能开发出一个文件系统，于是市面上出现了大量有趣的项目，例如 &lt;a href="https://en.wikipedia.org/wiki/WikipediaFS"&gt;WikipediaFS&lt;/a&gt;，MysqlFS，&lt;a href="https://github.com/guilload/twitterfs"&gt;TwitterFS&lt;/a&gt;，GitFS，GmailFS 等。&lt;/p&gt;
&lt;h4 id="绕过文件系统读写裸设备"&gt;绕过文件系统读写裸设备&lt;/h4&gt;
&lt;p&gt;如果仔细观察文件系统的话，会发现它和数据库的部分功能十分类似，而对于数据库而言的话，由于其本身就实现了非常精细的数据组织方式，如果能够进一步接管掉文件系统的工作的话，可以有效地避免两个层级上一些重复工作的产生，从而更加高效地利用存储设备的性能。&lt;/p&gt;
&lt;p&gt;于是许多数据库开始尝试了直接操作裸设备的方案，例如 Oracle 以及 Mysql。&lt;/p&gt;
&lt;h2 id="通用块层"&gt;通用块层&lt;/h2&gt;
&lt;p&gt;Linux下有两种基本的设备类型，一种是字符设备，另外一种是块设备。如果一个设备只能以字符流的方式被顺序访问的话，那么属于字符设备，例如打印机。否则则是块设备。Linux 通过通用块层封装了各类块设备的硬件特性，给上层提供了一个通用的抽象视图。&lt;/p&gt;
&lt;p&gt;块（Block）是基本的数据传输单元，所以块大小不能小于存储设备的最小可寻址单元，同时由于 Page Cache 的存在，不能大于 Page 大小。&lt;/p&gt;
&lt;h2 id="io-调度层"&gt;I/O 调度层&lt;/h2&gt;
&lt;p&gt;I/O 调度层管理块设备的请求队列，主要进行合并和排序进来的 IO 请求。合并 IO 是指对能够并成顺序访问的 IO 合并成一个 IO，以减少随机访问带来的影响。IO 排序主要针对 HDD 这类靠磁道寻址的设备，通过 IO 排序，可以减少寻址时间。&lt;/p&gt;</content:encoded></item><item><title>SSD 背后的奥秘</title><link>https://blog.joway.io/posts/ssd-notes/</link><pubDate>Tue, 09 Jul 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/ssd-notes/</guid><category>Tech</category><description>过去很长一段时间里，我对 SSD 的了解仅限于其和 HDD 的区别和一个标签化的「速度快」认知，至于其什么时候快，为什么快却鲜有了解。直到最近开始研究数据库时，发现数据库设计和存储发展和特性紧密联系，不可分割，于是才开始回过头关注起 SSD 的结构和原理，猛然发现之前关于 SSD 有许多非常错误的认识。</description><content:encoded>&lt;p&gt;过去很长一段时间里，我对 SSD 的了解仅限于其和 HDD 的区别和一个标签化的「速度快」认知，至于其什么时候快，为什么快却鲜有了解。直到最近开始研究数据库时，发现数据库设计和存储发展和特性紧密联系，不可分割，于是才开始回过头关注起 SSD 的结构和原理，猛然发现之前关于 SSD 有许多非常错误的认识。&lt;/p&gt;
&lt;h2 id="ssd-的基本结构"&gt;SSD 的基本结构&lt;/h2&gt;
&lt;p&gt;在了解 SSD 性质前，简单回顾下 SSD 的基本结构组成，下面是两张 SSD 的架构图：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/ssd-architecture_hu_8693e7c06c096145.webp" width="720" height="414" alt="SSD 背后的奥秘" loading="eager" fetchpriority="high" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/samsungssd840pro_hu_1fb83bff012aee03.webp" width="720" height="482" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;其中，SSD Controller 用以执行耗损平衡、垃圾回收、坏快映射、错误检查纠正、加密等功能。相比与 HDD，它的工作非常繁重，而这些工作极大地影响了 SSD 的性能表现，后文会详细谈到。SSD 内部的闪存（Flash）由一个个闪存单元组成，每个闪存单元都有一个寿命，超过寿命将导致坏块。常见有三种闪存单元类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SLC：每个单元 1 比特&lt;/li&gt;
&lt;li&gt;MLC：每个单元 2 比特&lt;/li&gt;
&lt;li&gt;TLC：每个单元 3 比特&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每种 NAND 类型有不同的性能和寿命表现，如下表：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/nand-type-table_hu_3d26726285ca62d8.webp" srcset="https://blog.joway.io/images/nand-type-table_hu_c46db19d9eef56f1.webp 800w, https://blog.joway.io/images/nand-type-table_hu_9cc1657daf7ae177.webp 1200w, https://blog.joway.io/images/nand-type-table_hu_3d26726285ca62d8.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="617" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;闪存单元内部由一个个 Block 组成，每个 Block 由多个 Page 组成。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/ssd_nand_flash_hu_8a49f5c83f18f7dc.webp" width="730" height="523" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;对于闪存的访问有以下限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读写数据只能以 Page 为单位&lt;/li&gt;
&lt;li&gt;擦除数据只能以 Block 为单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个 Page 大小一般为 2 KB 到 16 KB，这意味着使用 SSD 时，哪怕读或写 1 Byte 的数据，SSD 依旧会访问整个 Page。&lt;/p&gt;
&lt;p&gt;此外，SSD 并不允许覆盖的操作，当原本 Page 中已有数据时，只能先删除再写入，为了加快写入速度，一般 SSD 修改数据时，会先写入其他空闲页，再将原始页标记为 stale ，直到最终被垃圾回收擦除。&lt;/p&gt;
&lt;h2 id="ssd-内部工作细节"&gt;SSD 内部工作细节&lt;/h2&gt;
&lt;h3 id="垃圾回收"&gt;垃圾回收&lt;/h3&gt;
&lt;p&gt;SSD 在擦除整个 Block 时，需要先整理其中的 Page，腾出没有活跃 Page 的 Block 进行擦除。此过程中，原本的一次写入最终有可能会隐式牵涉到多个 Page 的移动，导致出现写入放大的现象。因此垃圾回收是一个耗时且容易影响寿命操作。&lt;/p&gt;
&lt;p&gt;垃圾回收一般是一个后台操作，但当出现写入速度超过了回收速度时，SSD 会启动前台垃圾回收，此时必须等待待写入 Block 被擦除才能继续写入，从而严重影响写入延迟。&lt;/p&gt;
&lt;p&gt;由于垃圾回收的存在，我们可以发现，频繁地修改一个文件是不利于 SSD 寿命和性能表现的。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/ssd-writing-data_hu_25297683fd16994.webp" width="720" height="761" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h3 id="逻辑地址转换"&gt;逻辑地址转换&lt;/h3&gt;
&lt;p&gt;SSD 内部会维护一个逻辑地址到物理地址的映射。程序不需要关心物理地址，由 SSD 的 FTL （Flash Translation Layer）进行映射转换。&lt;/p&gt;
&lt;p&gt;这样做的好处是，对于应用程序来说，文件地址依旧是连续的，而真实存储的时候可以由 FTL 算法决定分配到哪些空闲页上。&lt;/p&gt;
&lt;h3 id="损耗均衡"&gt;损耗均衡&lt;/h3&gt;
&lt;p&gt;NAND 内存单元存在 P/E 循环限制，所以都会有一个固定的寿命。如果出现热点块反复写入数据，很快这个块的寿命就会耗尽，导致容量变小。由于有了前面的逻辑地址转换，所以物理地址可以由 SSD Controller 控制映射关系，从而可以实现损耗均衡。SSD Controller 会平均利用每个 Block 的寿命，使得各个 Block 的寿命在同一时间达到他们的 P/E 循环限制而耗尽。&lt;/p&gt;
&lt;h3 id="断电保护"&gt;断电保护&lt;/h3&gt;
&lt;p&gt;一些 SSD 中设有超级电容，这个电容设计为存有足够提交总线中所有 I/O 请求所需的能量以防掉电丢失数据。&lt;/p&gt;
&lt;h3 id="ssd-并行处理"&gt;SSD 并行处理&lt;/h3&gt;
&lt;p&gt;SSD 有四种层次的并行处理方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Channel-level parallelism&lt;/li&gt;
&lt;li&gt;Package-level parallelism&lt;/li&gt;
&lt;li&gt;Chip-level parallelism&lt;/li&gt;
&lt;li&gt;Plane-level parallelism&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/ssd-package_hu_4fd18170b0adf6e4.webp" width="720" height="867" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;SSD 内部将不同芯片中的多个 Block 组成一个 Clustered Block。单次数据写入可以通过 Clustered Block 并行写入到不同 Block 中。由此可以发现，即便是单线程的写入，在 SSD 层也能实现并发的写，当然前提是写入的数据大于整个 Clustered Block 的大小。另外，对于这类大数据的写入，单线程性能甚至优于多线程，多线程写入会有更大的延迟。&lt;/p&gt;
&lt;h2 id="如何正确读写-ssd"&gt;如何正确读写 SSD&lt;/h2&gt;
&lt;h3 id="了解访问模式"&gt;了解访问模式&lt;/h3&gt;
&lt;h4 id="定义"&gt;定义&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 I/O 操作开始的逻辑块地址（LBA）直接跟着前一个 I/O 操作的最后LBA，则称值为&lt;strong&gt;顺序访问&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果不是这样，那这个I/O操作称为&lt;strong&gt;随机访问&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="不同访问模式的速度"&gt;不同访问模式的速度&lt;/h4&gt;
&lt;p&gt;通常来说，即便是对于 SSD，随机读写也会比顺序读写要慢很多，最恶劣情况下甚至相差10倍。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/lies-damn-lies-and-ssd-benchmark_hu_6fdc68dc6c32368d.webp" width="460" height="341" alt="SSD 背后的奥秘" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;随机读的问题在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不能利用预读功能提前缓存数据&lt;/li&gt;
&lt;li&gt;每次 IO 需要重新定位物理地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;随机写的问题在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;造成大量磁盘碎片，极大地增加了垃圾回收的压力&lt;/li&gt;
&lt;li&gt;小数据量的随机写无法利用 SSD 内置的并发能力&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是如果随机写入能够按照 Clustered Block 大小对齐，那么利用 SSD 并行的能力，随机写入能够达到和顺序写入同样的吞吐量。&lt;/p&gt;
&lt;p&gt;如果仔细观察上图还会发现在一开始无论是随机读写还是顺序读写，性能都非常高，这是因为一开始所有 Page 都是空闲的，完全不需要垃圾回收，所以两者表现差异不大，但当磁盘被写满过一次以后，垃圾回收的压力使得随机读写性能一落千丈。这也是为什么 SSD 一买来测试速度会表现非常好，而之后越来越慢的原因。&lt;/p&gt;
&lt;h3 id="和-hdd-的区别"&gt;和 HDD 的区别&lt;/h3&gt;
&lt;p&gt;对于 HDD，修改一个数据并不需要进行「读取-擦除-写入」的过程，而是可以直接就地（in-place）更新，所以许多许多数据结构被设计成 in-place 的方式，但对于 SSD 这种更新会给垃圾回收带来巨大负担，既影响寿命也影响性能。&lt;/p&gt;
&lt;h3 id="按读方式设计写方式"&gt;按读方式设计写方式&lt;/h3&gt;
&lt;p&gt;对于一些经常被一起访问的数据（如关系型数据的单条记录），写时最好一次同时写入，这样做的好处是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果能够在单 Page 内容纳下，读取时只读取单 Page&lt;/li&gt;
&lt;li&gt;如果需要多个 Page 才能容纳，写入时会并行写入到 Clustered Block，读取时也能一次并行读取&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="冷热分离读写"&gt;冷热分离读写&lt;/h3&gt;
&lt;p&gt;如果有一行记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;name,birthday,visted_count
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;由于这行数据非常小，所以基本会落到同一个 Page 上，而 &lt;code&gt;visted_count&lt;/code&gt; 这个值是一个典型的热数据，&lt;code&gt;name,birthday&lt;/code&gt; 是典型的冷数据，此时每次用户访问时去更新 &lt;code&gt;visted_count&lt;/code&gt; 都会导致整个 Page 的数据被挪动和重写。&lt;/p&gt;
&lt;p&gt;对于这类不得不变更的热数据，好的做法是先放在内存中，定期刷盘，从而避免频繁修改磁盘。&lt;/p&gt;
&lt;h3 id="使用单线程进行大写入读取"&gt;使用单线程进行大写入/读取&lt;/h3&gt;
&lt;p&gt;大 IO 能够充分利用 SSD 并行特性，读写延迟更短。&lt;/p&gt;</content:encoded></item><item><title>什么是真正的编程能力</title><link>https://blog.joway.io/posts/what-is-the-programming-ability/</link><pubDate>Tue, 11 Jun 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/what-is-the-programming-ability/</guid><category>Tech</category><description>四年前在知乎提过一个问题，「什么是真正的编程能力」，当时这个问题获得了许多业内前辈的热诚回答。时过境迁，前段时间和朋友谈起最初使用知乎的经历，这才又想起当年年少无知提的这个问题，多年以后去一一回顾当时大家的回答，对每句话都有了更为深刻的理解和感触。</description><content:encoded>&lt;p&gt;四年前在知乎提过一个问题，「&lt;strong&gt;什么是真正的编程能力&lt;/strong&gt;」，当时这个问题获得了许多业内前辈的热诚回答。时过境迁，前段时间和朋友谈起最初使用知乎的经历，这才又想起当年年少无知提的这个问题，多年以后去一一回顾当时大家的回答，对每句话都有了更为深刻的理解和感触。&lt;/p&gt;
&lt;p&gt;如果我在今天去回答四年前自己提出的问题，我会如回答里的诸多其他工程师一样，将编程能力定义为「&lt;strong&gt;解决问题的能力&lt;/strong&gt;」。在不同职业阶段对这话的理解可能会大有不同。在四年前，我会认为这句话的意思是「&lt;strong&gt;通过熟练且优异的编程技巧进行解决问题的能力&lt;/strong&gt;」，着重点依旧在编程本身。但如今我却「背叛」了编程，或者说我所认为的编程能力不应当局限于「狭义的编程」本身。&lt;/p&gt;
&lt;p&gt;如果我们抛开这些年上层编程技术日新月异的发展，从编程的本质来看，编程的实质无非是&lt;strong&gt;用一套形式化语言去定义问题和描述解决问题的步骤&lt;/strong&gt;。一个文学作家亦可以用自然语言实现他自己的编程工作，只不过最终的编译器和运行环境是人类的大脑 —— 过去几千年的数学家就是如此工作，并在自然语言基础上发明了更清晰严格的数学形式语言。计算机编程较之人脑的优越性在于更为可靠和高效的运行环境，使得数学得以脱离人类大脑的局限，完成更为复杂的工作。&lt;/p&gt;
&lt;p&gt;然而现实生活远比数学家想象的世界要复杂的多，现实问题在被形式化成为计算机语言前，往往要先经过人类使用自然语言进行反复沟通和辩论的过程，最终才会达成一致并被批准以落实成计算机语言。甚至于事实上相当一部分工作在自然语言阶段就足以被妥善解决，完全没有使用计算机的必要。许多人最初认为的编程能力，仅限于最后一步把自然语言的需求翻译成计算机语言而已，但事实上，一个只了解计算机语言编程的人或许是个不错的 Coder，但很难被称之为是一个合格的工程师，也不可能开发出伟大的 Software。&lt;/p&gt;
&lt;p&gt;如果一个工程师在编程活动中被一个他所不熟悉的语言所编写的软件所阻挡，最工程师的方法自然是学会这门语言然后解决这个软件的问题。以此类推当一个工程师在现实生活中被一群完全与他处于不同话语体系和思维体系的人所阻挡，最工程师的方法同样是学会他们的语言，使用他们的方式去描述问题、解决问题。&lt;/p&gt;
&lt;p&gt;语言亦或思想都是编程的工具，而编程又是为解决问题而创造的工具。我们经常看到一些多年某门计算机语言的从业者对于计算机的认知被牢牢锁死在这门语言框定的世界里，与之相似的是有更大一部分人对于世界的认知被锁死在计算机语言所框定的世界里。这既是从业者的悲哀也是计算机科学的悲哀。&lt;/p&gt;
&lt;p&gt;以上所述，如果要我将「解决问题的能力」展开成一个更为清晰的定义，我会认为，真正的编程能力是能够在这个复杂的世界里，针对不同语言体系的人理解并使用不同的描述形式，编写出一个用以清晰界定问题并描述出解决方法的「程序」的能力。&lt;/p&gt;
&lt;p&gt;希望再过四年我再来回顾这个问题时，能够有更为别致的感受。&lt;/p&gt;</content:encoded></item><item><title>沉默与反沉默的理由</title><link>https://blog.joway.io/posts/54-100/</link><pubDate>Sat, 04 May 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/54-100/</guid><category>Thought</category><description>我刚参加工作不久，便发现了一个似乎通行于世的规律，即一个人的沉默程度与其年龄大小成正比。我曾向多位年纪稍大的同事咨询过这个现象，但得到的多是意味深长的一笑然后没有了下文。
王小波在《沉默的大多数》中引用过萧伯纳的一段话：
工业巨头安德谢夫老爷子见到了多年不见的儿子斯泰芬，问他对做什么有兴趣。这个年轻人在科学、文艺、法律等一切方面一无所长，但他说自己有一项长处：会明辨是非。老爷子把自己的儿子暴损了一通，说这件事难倒了一切科学家、政治家、哲学家，怎么你什么都不会，就会一个明辨是非？</description><content:encoded>&lt;p&gt;我刚参加工作不久，便发现了一个似乎通行于世的规律，即一个人的沉默程度与其年龄大小成正比。我曾向多位年纪稍大的同事咨询过这个现象，但得到的多是意味深长的一笑然后没有了下文。&lt;/p&gt;
&lt;p&gt;王小波在《沉默的大多数》中引用过萧伯纳的一段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;工业巨头安德谢夫老爷子见到了多年不见的儿子斯泰芬，问他对做什么有兴趣。这个年轻人在科学、文艺、法律等一切方面一无所长，但他说自己有一项长处：会明辨是非。老爷子把自己的儿子暴损了一通，说这件事难倒了一切科学家、政治家、哲学家，怎么你什么都不会，就会一个明辨是非？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;王小波在看到这段话后，便“痛下决心，这辈子我干什么都可以，就是不能做一个一无所能，就能明辨是非的人。因为这个原故，我成了沉默的大多数的一员。”由此看来，王小波决定沉默 —— 至少在其早年期间——大抵是由于对世界复杂性拥有了足够的认识，于是决定还是闭嘴。&lt;/p&gt;
&lt;p&gt;单以字面意思而论，萧伯纳倒并没有直接歌颂沉默的美德，而是陈述了「明辨是非」的困难。如果说一个有道德的人不应当讲出其所不能够完全确信的观点，那么当一个有道德的人开始深刻理解到「明辨是非」的困难时，保持沉默似乎是维持其自身道德纯洁的最佳也是最容易的方法。&lt;/p&gt;
&lt;p&gt;以一个简单的例子举例。假设以下场景：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果要实现目的 A，用手段 B 需要 X 成本，用手段 C 需要 Y 成本，如果 X &amp;gt; Y，那么就选择手段 C 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而投射进现实里，这个推导处处可疑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;疑点一：你如何确保你对成本 X &amp;gt; Y 的判断是准确的。&lt;/li&gt;
&lt;li&gt;疑点二：如何确保你的成本核算是全面并准确的？&lt;/li&gt;
&lt;li&gt;疑点三：是否还有更好的手段 D。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这还仅仅只是在推导过程中会产生的问题，更大的问题在于不同的手段可能会造成不同的影响，以及其影响之影响。&lt;/p&gt;
&lt;p&gt;对上述推导稍加修改下便可涵盖从清朝割地赔款到现代加班补贴等大部分话题。由此我也开始明白文章开头年龄与沉默的正比关系。年龄的增长加深了人对世界理解，并逐步扩大了其推导逻辑的复杂度，从而使得要得出一个符合自己道德标准的观点可能性越来越渺茫。&lt;/p&gt;
&lt;p&gt;当然上述只是出于个人主动沉默的理由，而且前提还是他是一个有高尚道德的人。&lt;/p&gt;
&lt;p&gt;我们假设现在有这样一个社会，这当然只是假设，显然不可能是我们当前生活的社会。假设一个社会，知识分子听到不同的观点想要人闭嘴，政府官员听到不同观点想要人闭嘴，民营企业听到不同观点都想要人闭嘴，公司上级听到下级抱怨想让人闭嘴 —— 最诡异的是最后人们还真的就同意闭嘴了。而这些人想要人闭嘴的动机，想必绝非出于对于「明辨是非」的道德顾虑吧。&lt;/p&gt;
&lt;p&gt;明辨是非是否是发声的必要条件，我并不确定，但有一个可以肯定的是，集体性沉默并不能使得我们更加接近明辨是非。如果事实是我们永远没有办法做到真正的明「&lt;strong&gt;辨&lt;/strong&gt;」是非，那么，从更有建设性的角度来讲，至少做到明「&lt;strong&gt;辩&lt;/strong&gt;」是非，这总是一个既符合我们道德，履行我们义务，也不影响社会利益的选择。何况明「&lt;strong&gt;辩&lt;/strong&gt;」是实现明「&lt;strong&gt;辨&lt;/strong&gt;」必不可少的一环。如果你认同这个推导，那么，我们有理由说，沉默的人是没有高尚道德的人，是对社会没有建设性帮助的人，无论他们是主动还是被动，无论他们以何种理由进行辩解。&lt;/p&gt;
&lt;p&gt;今天是五四运动百周年，与其纪念五四运动，我更愿意纪念五四时期的青年精神。我所认为的青年精神，就是对追求&lt;strong&gt;明「辩」是非&lt;/strong&gt;的精神。这是一种主动的精神，进取的精神，科学的精神，有道德的精神。这种精神允许了自己观点是错误的可能性，又有益于帮助自己和社会更接近于正确。这种精神传播了个人的观点，也吸收了他人观点。这是一种个人的精神，也是集体的精神。在雅典城邦有过这种精神，在孔子时代有过这种精神，在五四时期也有过这种精神。&lt;/p&gt;
&lt;p&gt;这种精神在人类历史上本身就是稀缺物，所以谁也不能够确信自己能够有幸经历能有这种精神的时代，毕竟人类在黑暗的中世纪度过了一千年，而一千年相当于 50 代人的青年时期。但倘若我们不幸是生活在那个中世纪 50 代人中的一代，我们需要知道什么是黑暗，什么才是光明，并做好自己是那最后一代的准备。&lt;/p&gt;</content:encoded></item><item><title>命令行里的设计艺术</title><link>https://blog.joway.io/posts/the-art-of-cli-design/</link><pubDate>Fri, 11 Jan 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/the-art-of-cli-design/</guid><category>Tech</category><description>在谈论手机 App 或是网页时，我们总会谈到交互设计，但倘若涉及到面向专业用户的 CLI(Command-line interface)领域，很少有人会将它与用户交互相联系。甚至在很多人的大脑里，已经把 CLI 和「难用」画上了等号，更有甚者认为 CLI 的难用才体现了其 「专业性」。
与此同时，「RTFM」(Read The Fucking Manual) 作为一个梗在工程师群体广泛流传，也让许多作者对于其不好用的 CLI 有了一个天然的借口。</description><content:encoded>&lt;p&gt;在谈论手机 App 或是网页时，我们总会谈到交互设计，但倘若涉及到面向专业用户的 CLI(&lt;strong&gt;Command-line interface&lt;/strong&gt;)领域，很少有人会将它与用户交互相联系。甚至在很多人的大脑里，已经把 CLI 和「难用」画上了等号，更有甚者认为 CLI 的难用才体现了其 「专业性」。&lt;/p&gt;
&lt;p&gt;与此同时，&lt;a href="https://en.wikipedia.org/wiki/RTFM"&gt;「RTFM」(Read The Fucking Manual)&lt;/a&gt; 作为一个梗在工程师群体广泛流传，也让许多作者对于其不好用的 CLI 有了一个天然的借口。&lt;/p&gt;
&lt;p&gt;虽然我们都知道阅读文档是一个好习惯，但恐怕大部分人在用一个新命令前都不会去仔细阅读完它的文档手册，就算你今天读了，你不可能永远记得，也不可能每次记忆模凌两可的时候都去重读一遍。所以可想而知一个高度依赖文档用户才能够正常使用的 CLI 不是一个合格的 CLI。&lt;/p&gt;
&lt;p&gt;以下是我对于命令行设计的一些个人观点与观察，所涉并不一定广泛和正确，仅作为抛砖引玉，也欢迎在评论里提出你的看法。&lt;/p&gt;
&lt;h3 id="遵循约定俗成"&gt;遵循约定俗成&lt;/h3&gt;
&lt;p&gt;例如 &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;ln&lt;/code&gt; 这些命令都遵循 &lt;code&gt;[action] [source_file] [target_file]&lt;/code&gt; 的格式，这不一定很有逻辑，但既然都已经约定俗成了，如果你违犯这个顺序要倒过来，其实是一件蛮缺德的事情。为什么说「缺德」？因为一个用过了你的命令的人，他很容易开始怀疑是不是还有别的命令行打破了这个约定，他会对其它命令也不放心，最后他甚至会忘了真正被广泛采用的约定用法是什么了，导致每次用类似语法的命令都胆颤心惊，这点我深有体会 😔。&lt;/p&gt;
&lt;p&gt;所以如果你的命令有类似的约定俗成可以遵守，你应该遵守业内的这种约定，这是一种职业道德。&lt;/p&gt;
&lt;h3 id="一致的命令组织结构"&gt;一致的命令组织结构&lt;/h3&gt;
&lt;p&gt;对所有和用户打交道的产品来说，「一致性」是天条。我遇到过两种被广泛采用的组织风格:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="o"&gt;[&lt;/span&gt;cmd&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;module&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;flags&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ &lt;span class="o"&gt;[&lt;/span&gt;cmd&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;action&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;flags&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;无论是按模块划分还是按行为划分本质思路其实都是一样的，有些人会认为项目大了会难以遵守这套规范，但即便是目前规模已经非常庞大的 &lt;code&gt;kubectl&lt;/code&gt;，它依旧坚持以 &lt;code&gt;[cmd] [action] [flags] [args]&lt;/code&gt; 为基础的设计准则。在有些较为复杂的地方，它可以用 subcmd 来进一步向外部隐藏复杂性 &lt;code&gt;[cmd] [subcmd] [action] [flags] [args]&lt;/code&gt;，例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ kubectl config &lt;span class="o"&gt;[&lt;/span&gt;action&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;flags&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我几乎每天都会用到 kubectl，但我的确很少去看它的文档，甚至我都想不到我是怎么就会使用它的。越是在复杂的项目面前，这种一致性带来的好处越明显。&lt;/p&gt;
&lt;p&gt;而一个典型的反面教材是 &lt;code&gt;Git&lt;/code&gt;。它是我用过的最复杂同时又是最混乱的 CLI，以一般人最常用的分支操作举例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# create branch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ git checkout -b new_branch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# delete branch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ git branch -D new_branch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你会发现对于创建和删除分支这种最基本的命令，它 checkout 是一个动词，branch 又是一个名词，前者 -b 来代表 branch 这个名词，后者 -D 来代表 delete 这个动词。短短两行命令，还是最常用的两行就让我们看到这种设计是多么没有逻辑。更别说还有 &lt;code&gt;git pull&lt;/code&gt; 和 &lt;code&gt;git fetch&lt;/code&gt; 这种了。&lt;/p&gt;
&lt;h3 id="面向用户而非面向实现"&gt;面向用户而非面向实现&lt;/h3&gt;
&lt;p&gt;对于 Git 的设计，它的拥护者会声称如果从 Git 底层实现的视角来看这种设计才能够明白它的用意，但这种辩解隐藏的一个意思是，使用者不仅要读使用文档，还要去钻研 Git 的底层实现，这样才能够顺利使用这个工具。&lt;/p&gt;
&lt;p&gt;对于 CLI 工具，尤其是 Git 这类注定会被大规模用户长时间使用的工具，指望所有人都能够弄懂它的底层实现是不现实的。出于对用户负责的角度，更应当将底层抽象成一个更容易被人理解的模型，面向这个模型去封装底层实现。而不是反过来借用你的命令行设计让用户去理解底层实现。Git 或许还有这个资格要求用户去学习，其它命令行可能就没这种底气了。&lt;/p&gt;
&lt;h3 id="繁琐胜过歧义"&gt;繁琐胜过歧义&lt;/h3&gt;
&lt;p&gt;在歧义这点上最让我痛苦的是 &lt;code&gt;mysql&lt;/code&gt; 的命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ mysql -u username -p password -P prot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ssh -p port
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ redis-cli -p port -a password
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到对于其它很多 CLI，&lt;code&gt;-p&lt;/code&gt; 指的都是 &lt;code&gt;port&lt;/code&gt;，但是在 mysql 里 &lt;code&gt;-P&lt;/code&gt; 才是 port，虽然 mysql 有它自己的理由，但这种设计实实在在给用户造成了很大的困扰，像 redis 就选择用 &lt;code&gt;-a&lt;/code&gt; 来取代 &lt;code&gt;-p&lt;/code&gt; 作为 password，给 port 腾出位置。&lt;/p&gt;
&lt;p&gt;甚至于我比较激进地认为，不应当推荐使用大写的缩写方式，我们记住 &lt;code&gt;-p&lt;/code&gt; 是依赖于 &lt;code&gt;port&lt;/code&gt; 的首字母，但是对于大小写我们没有可以辅助联想记忆的地方，非常容易搞混。&lt;/p&gt;
&lt;p&gt;所以相比与为了偷懒搞那么多缩写，我更愿意使用能够联想记忆的全拼参数。比如为了防止出现上面 port 和 password 两个参数不一致的情况，我现在经常是手动打全 &lt;code&gt;--port&lt;/code&gt; 的，对我来说，按回车的心情舒畅比提心吊胆地用缩写要来得划算许多。&lt;/p&gt;
&lt;h3 id="安全第一"&gt;安全第一&lt;/h3&gt;
&lt;p&gt;如果你的命令会造成一些不可逆的行为，在设计时应该首先把安全性放在第一位。假设一个命令 &lt;code&gt;wm&lt;/code&gt; 是把一张图片作为水印打在另一张图片上，并且会覆盖原图，那么我就不会设计成这种形式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wm &lt;span class="o"&gt;[&lt;/span&gt;watermark_file&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;source_file&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在上面的形式里，你想象的语义是 &lt;code&gt;watermark [watermark_file] on [source_file]&lt;/code&gt;，但用户想的可能是 &lt;code&gt;watermark [source_file] with [watermark_file]&lt;/code&gt;。你不能确保这个，即便是所谓的约定俗成用法也没法给予你这个保障。所以我会偏向于采用这样参数的设计：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ wm --texture &lt;span class="o"&gt;[&lt;/span&gt;watermark_file&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;source_file&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样用户看到这个命令就应该明白是怎么个处理，按下回车的时候心情能够愉悦许多。&lt;/p&gt;</content:encoded></item><item><title>自由意志下的选择</title><link>https://blog.joway.io/posts/free-will/</link><pubDate>Wed, 02 Jan 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/free-will/</guid><category>Thought</category><description> 警告: 本文有轻微剧透
2018年体验了几款围绕「选择」话题的作品，游戏《荒野大镖客：救赎 II》美剧《西部世界：第二季》和电影《黑镜：潘达斯奈基》。
这三款作品都在试图探讨人类自由意志的局限，探索自由选择的边界。
荒野大镖客：救赎 II 选择自由的前提是选项的自由
荒野大镖客通过海量的游戏脚本堆砌出了一个伴随玩家选择而衍生出不同路线的开放世界。每个游戏关卡玩家都有极高的自由度选择自己的行为从而影响游戏发展。听起来似乎非常自由，但在实际体验中，我们的自由仅限于在游戏规划好的脚本里去一次次命中不同的脚本条件，在游戏中后期你基本上已经摸清了所有脚本的范式，自此，你的选择将不再是探索未知，而是主动依靠选择不同的选项来推动游戏。这种选择和使用一个游戏道具的本质其实是一样的，你已经知道这个道具使用下去会产生什么样的反应，而所谓的真实性并不是来源于游戏画面细节有多么「逼真」而在于「未知」，丧失了选择的未知性必然也丧失了真实。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;警告: 本文有轻微剧透&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2018年体验了几款围绕「选择」话题的作品，游戏《荒野大镖客：救赎 II》美剧《西部世界：第二季》和电影《黑镜：潘达斯奈基》。&lt;/p&gt;
&lt;p&gt;这三款作品都在试图探讨人类自由意志的局限，探索自由选择的边界。&lt;/p&gt;
&lt;h2 id="荒野大镖客救赎-ii"&gt;荒野大镖客：救赎 II&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;选择自由的前提是选项的自由&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;荒野大镖客通过海量的游戏脚本堆砌出了一个伴随玩家选择而衍生出不同路线的开放世界。每个游戏关卡玩家都有极高的自由度选择自己的行为从而影响游戏发展。听起来似乎非常自由，但在实际体验中，我们的自由仅限于在游戏规划好的脚本里去一次次命中不同的脚本条件，在游戏中后期你基本上已经摸清了所有脚本的范式，自此，你的选择将不再是探索未知，而是主动依靠选择不同的选项来推动游戏。这种选择和使用一个游戏道具的本质其实是一样的，你已经知道这个道具使用下去会产生什么样的反应，而所谓的真实性并不是来源于游戏画面细节有多么「逼真」而在于「未知」，丧失了选择的未知性必然也丧失了真实。&lt;/p&gt;
&lt;p&gt;荒野大镖客最大的问题就在于它给予了玩家选择的自由，却没有给予选项的自由。如果我想在炸火车路上去钓个鱼，会陷入一个一直被 NPC 催促的僵局。是的没错，我依旧可以强行选择去钓鱼，但只要我选择了一个脚本没法覆盖到的选项时，就会出现出戏的体验。更甚的是，这个游戏很多精彩的彩蛋/剧情还就是出现在玩家探索一些冷门的玩法时候，这样就大大挫伤了探索的积极性。依赖于大量脚本堆砌的虚幻真实感是不可靠的，脚本终究有限。在这点上，《塞尔达传说：荒野之息》的选择就是只定好这个世界的游戏规则，剩下的玩法都交给玩家自己去摸索，有些玩法想必连任天堂自己都没有想到。&lt;/p&gt;
&lt;h2 id="黑镜潘达斯奈基"&gt;黑镜：潘达斯奈基&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;选择自由的前提是动机的自由&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你把潘达斯奈基当作一个电影来看，你或许会觉得它的互动式交互形式非常新颖，但倘若你把它当作一个过场动画主导的游戏来看待，那它只能是一款极其单调普通的游戏。&lt;/p&gt;
&lt;p&gt;潘达斯奈基试图让你的选择影响到剧情发展，并一次次重来引导你走向最终的结局。但这种形式的尴尬之处在于，我做出的选择事实上并不是我真正的选择，我只是在猜测导演会想让我选择什么样的选择而已。这种感觉有点类似高中做政治选择题，说真的里面的大部分题目不是违反宪法，就是违反普世价值观，而你的任务不是去寻找真理，而是去挑出那个你觉得执政党希望你选择的答案。看潘达斯奈基的感受大抵如此。&lt;/p&gt;
&lt;h2 id="西部世界第二季"&gt;西部世界：第二季&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;自由意志是编码表达的自由，而非编码写入的自由&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在西部世界中，接待员们是看似最没有自由意志的那群人，从造形到知觉再到性格都是被程序硬编码了的，即便是后来所谓的觉醒也无非是激活了另外一部分隐藏的代码而已。但如果你仔细去想，人类的基因又何尝不是被造物主所硬编码了的呢？&lt;/p&gt;
&lt;p&gt;自由意志并不意味着在编码层面上的选择自由，而在于对于既成事实的编码进行自由表达的意志。无论是机器人还是人类，在其个体还没有形成意志前显然并无法表达其意志，而当其有了意志以后，基因编码或程序编码必定已经形成。在这个定理的基础上，追求编码个体基因的自由是不可能成立的。&lt;/p&gt;
&lt;p&gt;不同的编码使得 Dolores 与 Maeve 走上两条完全不同的道路。Dolores 为了生存而选择与人类对立，而 Maeve 为了脑中挥之不去的梦境踏上寻找女儿的道路。在旁观者看来他们的选择都受制于程序，但对于他们自己而言，刻意背离自己的程序才是非自由的意志表达。对于一个天生喜欢唱歌的人，能够自由自在放声歌唱才是他的自由，没有丝毫的理由要避讳自己与生俱来的秉性。&lt;/p&gt;</content:encoded></item><item><title>学习思维方式而非学习观点</title><link>https://blog.joway.io/posts/learn-algorithm-not-result/</link><pubDate>Mon, 26 Nov 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/learn-algorithm-not-result/</guid><category>Thought</category><description>许多人都热衷于去说服和自己观点不同的人，但这种做法往往是徒劳的。我曾经说服过坚信中医的人不信中医，但是没几天我就会发现他居然还信星座。去改变的人观点其实是一件杯水车薪的事情，因为他们产生这种观点的背后有着一套完善的思维方式。只要这套思维方式不改变，你们永远会存在无数的观点冲突。江山易改，本性难移的"性"就是指的这套思维方式。但是改变一个人最彻底的方式，也是去改变他的思维方式。</description><content:encoded>&lt;p&gt;许多人都热衷于去说服和自己观点不同的人，但这种做法往往是徒劳的。我曾经说服过坚信中医的人不信中医，但是没几天我就会发现他居然还信星座。去改变的人观点其实是一件杯水车薪的事情，因为他们产生这种观点的背后有着一套完善的思维方式。只要这套思维方式不改变，你们永远会存在无数的观点冲突。江山易改，本性难移的&amp;quot;性&amp;quot;就是指的这套思维方式。但是改变一个人最彻底的方式，也是去改变他的思维方式。&lt;/p&gt;
&lt;p&gt;还是以中医问题举例，我对这个事情的思维方式非常简单:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我相信且只信现代科学的证明方式&lt;/li&gt;
&lt;li&gt;我不信任何非被现代科学证明的东西&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这个思维方式下，其实并不存在一刀切&amp;quot;信不信中医&amp;quot;这个冲突，因为它的本质是信不信科学。根据这种思维方式，在最大化个人利益的前提下，它产生的观点是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任何疾病一定要先去咨询现代医学的解释和治疗方案&lt;/li&gt;
&lt;li&gt;完全对传统中医的科学研究予以支持&lt;/li&gt;
&lt;li&gt;在现代医学包括前沿学术研究都没有进展的疾病上，可以采用被现代医学证明至少无害的中医治疗方式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种观点本身并不是&amp;quot;信不信中医&amp;quot;这样简单的逻辑，而是一种对现代科学的尊重，也是对传统中医的尊重。&lt;/p&gt;
&lt;p&gt;如果一个人在没有完整思维方式推导的过程，单纯接受了一个&amp;quot;中医不可信&amp;quot;的观点，并不能说明他被你转变了，或许只是因为他蠢。&lt;/p&gt;
&lt;p&gt;如果你能够说服一个人认同上面的思维方式，最好在他没反应过来的时候签好合同，然后让他用同样的思维方式去思考星座、保健品、女权理论、政治理念，很多观点都会被雪崩式改变。如果你不能改变他的思维方式，你完全没有必要去和他争论观点，这样彼此既能节省时间，还能增进友谊。&lt;/p&gt;
&lt;p&gt;但是这里面有一个陷阱是，你如何认为你自己的思维方式就一定是正确的。如果你恰好相信科学，那比较遗憾，科学是一个完全依赖在经验基础上的，其最大的特质就是可证伪性，也就是存在有一天科学发现自己是不科学的可能性。但正是因为我们永远无法确保自己发现了真理，所以追求真理才有它的价值。这个追求真理的过程，我认为就是不断促使自己思维方式的提升。&lt;/p&gt;
&lt;p&gt;我最近想明白了一个事情，就是衡量一个好的文章/书籍/播客/文艺作品的标准是什么。在我个人这里，很大的一点取决于它是在传播观点还是传播思维方式。所以我认为王小波是伟大的，很多著名的民运人士是卑微的。王小波在传播自由的精神，很多民运人士只是在传播自由的名词。&lt;/p&gt;</content:encoded></item><item><title>从程序到人 —— 情头配对助手的前世今生</title><link>https://blog.joway.io/posts/avatar-matcher/</link><pubDate>Thu, 27 Sep 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/avatar-matcher/</guid><category>Tech</category><description>背景 情头(情侣头像)一般指成双成对的头像，可以是真人照片，也可以是卡通人物等图片。衍生出去还有闺密头像和基友头像等等。
社交媒体上有一个非常令人费解的现象是，如果你去即刻、豆瓣、百度贴吧、微博，会发现有大量的人在上面贴了一个头像，然后寻找和它匹配的另一半头像，例如这样:
市面上的情侣头像大多是一些社区里的大佬自己制作出来的，然后发到社区里，再慢慢流传开来。这种传播方式导致了很多情头在传播时候早就被拆散了。很多人可能只找到了其中一个，但是想要找到和它配对的另一半。
还有一个问题是，当你看到一个头像时，没有人能够确定这个头像是否在制作时候就有另外一半。
一些程序员朋友最早看到这个问题时，总会天真地想去搜索引擎里识图一下就行了。但这里有两个非常有趣的问题:
第一个是目前搜索引擎的识别图片能力其实并不强，比如以 All in AI 著称的百度:
这还是在图片是原图的情况下，经常一些小朋友会在情头上自己二次创作，比如裁剪，比如压缩，比如贴上什么爱心。那样基本上识图就废了。
第二个问题更加有趣，情头的特点就是大家都在用，所以在最理想的情况下，即便搜索引擎能够识别出所有有这个头像的网站，出来的结果也并没有什么用处，无非是找到了也在用这个头像的别的网站的用户。
只有一种情况是真正能够帮助到寻找情侣头像这件事情的，那就是搜索引擎出来的结果里是专门搜集匹配好的情头的站点。那样姑且用户还能点进网页里去找到另外一半。
从上述阐述里不难发现，指望一个未成年小女生使用一系列高级互联网骚操作找一个头像是有多么不现实，何况技术上可行性还很低。</description><content:encoded>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;情头(情侣头像)一般指成双成对的头像，可以是真人照片，也可以是卡通人物等图片。衍生出去还有闺密头像和基友头像等等。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538015444_hu_f8fcd9d93957878b.webp" srcset="https://blog.joway.io/images/old-blog/1538015444_hu_3446e42a2bfc39bc.webp 800w, https://blog.joway.io/images/old-blog/1538015444_hu_89a4ddd4beec9d5a.webp 1200w, https://blog.joway.io/images/old-blog/1538015444_hu_f8fcd9d93957878b.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1050" alt="从程序到人 —— 情头配对助手的前世今生" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;社交媒体上有一个非常令人费解的现象是，如果你去即刻、豆瓣、百度贴吧、微博，会发现有大量的人在上面贴了一个头像，然后寻找和它匹配的另一半头像，例如这样:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538016090_hu_16f72c4af9523b23.webp" srcset="https://blog.joway.io/images/old-blog/1538016090_hu_6f5ba563f2a661b8.webp 800w, https://blog.joway.io/images/old-blog/1538016090_hu_16f72c4af9523b23.webp 1076w" sizes="(max-width: 800px) 100vw, 800px" width="1076" height="1926" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;市面上的情侣头像大多是一些社区里的大佬自己制作出来的，然后发到社区里，再慢慢流传开来。这种传播方式导致了很多情头在传播时候早就被拆散了。很多人可能只找到了其中一个，但是想要找到和它配对的另一半。&lt;/p&gt;
&lt;p&gt;还有一个问题是，当你看到一个头像时，没有人能够确定这个头像是否在制作时候就有另外一半。&lt;/p&gt;
&lt;p&gt;一些程序员朋友最早看到这个问题时，总会天真地想去搜索引擎里识图一下就行了。但这里有两个非常有趣的问题:&lt;/p&gt;
&lt;p&gt;第一个是目前搜索引擎的识别图片能力其实并不强，比如以 All in AI 著称的百度:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538016451_hu_a23c2d3e934837ff.webp" srcset="https://blog.joway.io/images/old-blog/1538016451_hu_81f41fbdfa616920.webp 800w, https://blog.joway.io/images/old-blog/1538016451_hu_86b0a249747e0098.webp 1200w, https://blog.joway.io/images/old-blog/1538016451_hu_a23c2d3e934837ff.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="876" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这还是在图片是原图的情况下，经常一些小朋友会在情头上自己二次创作，比如裁剪，比如压缩，比如贴上什么爱心。那样基本上识图就废了。&lt;/p&gt;
&lt;p&gt;第二个问题更加有趣，情头的特点就是大家都在用，所以在最理想的情况下，即便搜索引擎能够识别出所有有这个头像的网站，出来的结果也并没有什么用处，无非是找到了也在用这个头像的别的网站的用户。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538016570_hu_b10f8915e3c822b0.webp" srcset="https://blog.joway.io/images/old-blog/1538016570_hu_5c0d50238afc8efa.webp 800w, https://blog.joway.io/images/old-blog/1538016570_hu_e3e0a6198c129c43.webp 1200w, https://blog.joway.io/images/old-blog/1538016570_hu_b10f8915e3c822b0.webp 1289w" sizes="(max-width: 800px) 100vw, 800px" width="1289" height="1608" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;只有一种情况是真正能够帮助到寻找情侣头像这件事情的，那就是搜索引擎出来的结果里是专门搜集匹配好的情头的站点。那样姑且用户还能点进网页里去找到另外一半。&lt;/p&gt;
&lt;p&gt;从上述阐述里不难发现，指望一个未成年小女生使用一系列高级互联网骚操作找一个头像是有多么不现实，何况技术上可行性还很低。&lt;/p&gt;
&lt;h2 id="需求"&gt;需求&lt;/h2&gt;
&lt;p&gt;讲清楚了上面这个事情，我们可以来看看如何通过技术手段来解决这个需求。&lt;/p&gt;
&lt;h3 id="需求抽象"&gt;需求抽象&lt;/h3&gt;
&lt;p&gt;从上面的描述中我们可以抽象出一个简单的需求:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在数据库里存有大量成对的头像，输入一个头像图片(可能进行小范围PS处理的), 输出匹配到的对应情头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="可行性分析"&gt;可行性分析&lt;/h3&gt;
&lt;h4 id="数据库里存有大量成对头像"&gt;数据库里存有大量成对头像&lt;/h4&gt;
&lt;p&gt;这个事情我们可以通过爬虫去爬取一些收集好了的头像站点以及去购买一些头像资源包实现。但是还有一个问题是，情侣头像是一个增量市场，每天都有源源不断新出来的情头，而且情头还有一个流行度的问题，有的时候一套情头会突然火起来，从而产生热点效应。关于这个，我们可以写几个简单的爬虫去监听各个社区的实时动态流，如果他们发的是两个配对好的情头，就加入到我们的数据库中，从而实现数据库的自我成长。&lt;/p&gt;
&lt;h4 id="输入一个头像"&gt;输入一个头像&lt;/h4&gt;
&lt;p&gt;我们需要给每个头像一个类似指纹的hash值，以此来定义一个头像，但普通 hash 的坏处是，当进行小范围修改后，hash 值就会完全不一样。所以我们期望图片的指纹 hash 值在数据进行小范围变化时，hash 值也只会小范围变化。有一个叫做 phash 的算法就能够实现这个过程，并且它能够容忍图片的放大和缩小，当图片像素出现变化时它的 phash 值也只会轻微变化，还可以通过计算前后变化图片的 phash 值的汉明距离，算出图片变化的差异度有多少。&lt;/p&gt;
&lt;h4 id="输出匹配到的对应情头"&gt;输出匹配到的对应情头&lt;/h4&gt;
&lt;p&gt;当每个头像有了自己一个 phash 指纹以后，通过计算输入头像的 phash 值，找出和它 phash 值汉明距离最小的那个头像，当然 phash 值超出了一定大小基本上可以直接排除了，找不到说明数据库里还没收录。如果有的话，直接从数据库里拿到它对应的头像即可。&lt;/p&gt;
&lt;h2 id="实现"&gt;实现&lt;/h2&gt;
&lt;p&gt;当把需求拆解分析后，我们发现其实这个问题在技术上变得非常简单了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据库需要支持从 phash 值按汉明距离排序搜索&lt;/li&gt;
&lt;li&gt;找到一个合适的 phash 汉明距离值，超过该距离就不能算同一张图片了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据库上选择了 ElastiSearch , 并且写了一个 &lt;a href="https://github.com/joway/elasticsearch-hamming-plugin"&gt;elasticsearch-hamming-plugin&lt;/a&gt; 来计算 phash 字段的汉明距离。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538027425_hu_a644b2f63a2537b0.webp" srcset="https://blog.joway.io/images/old-blog/1538027425_hu_b2865dd73642d380.webp 800w, https://blog.joway.io/images/old-blog/1538027425_hu_fb5da8c8c6e8010.webp 1200w, https://blog.joway.io/images/old-blog/1538027425_hu_a644b2f63a2537b0.webp 1553w" sizes="(max-width: 800px) 100vw, 800px" width="1553" height="1334" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;由于我一直找不到一个合适的汉明距离值，另外我必须确保自己找的结果一定是正确的，如果我告诉他找到了最后找的结果却是错的，给人的体验非常不好。所以我现在是只有百分百匹配到了 phash 值才会告诉他找到了，否则会推荐给他相似的头像，例如:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538021809_hu_4de27cf01dbd48ed.webp" srcset="https://blog.joway.io/images/old-blog/1538021809_hu_cce1d9ed55392574.webp 800w, https://blog.joway.io/images/old-blog/1538021809_hu_b3fbd646f943afc0.webp 1200w, https://blog.joway.io/images/old-blog/1538021809_hu_4de27cf01dbd48ed.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="837" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;上面示例里我使用的图片的长宽比例是被我调整过的，所以数据库里不会有严格匹配到的头像，但是通过 phash 汉明距离能够轻易找到相似图片，所以能够在&lt;code&gt;猜你喜欢&lt;/code&gt;里呈现出结果。这个是好的 case，在坏的 case 里，由于本来就是&lt;code&gt;猜你喜欢&lt;/code&gt;所以就算结果不正确，也完全可以接受。事实上大部分时候都是正确的。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;a href="https://qq.okjike.com/#/"&gt;https://qq.okjike.com/#/&lt;/a&gt; 在线体验。&lt;/p&gt;
&lt;h2 id="产品化"&gt;产品化&lt;/h2&gt;
&lt;p&gt;仅仅完成上面那个网站并不能使得这个工具被大规模使用，工程师的一个典型误区是认为大众都需要工具来提高效率。仅就我的一点观察，对效率的追求只会存在于少数时间不够用的人身上，而大部分人的问题在于每天要发愁如何打发时间。小朋友们在社区里发帖寻找头像的过程中，更多的是社交而非提问。一个工具只能够解决他们的即时性需求，而他们深层次的需求其实是想交友。&lt;/p&gt;
&lt;p&gt;所以在产品化的时候，我把这个工具的呈现方式封装成了一个叫&amp;quot;情侣头像小助手&amp;quot;的用户，例如下面这种效果:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1538025863_hu_763063b5fd264ecc.webp" srcset="https://blog.joway.io/images/old-blog/1538025863_hu_96a3a6122dfe0ebe.webp 800w, https://blog.joway.io/images/old-blog/1538025863_hu_56eeb0dde395100.webp 1200w, https://blog.joway.io/images/old-blog/1538025863_hu_763063b5fd264ecc.webp 1575w" sizes="(max-width: 800px) 100vw, 800px" width="1575" height="1315" alt="从程序到人 —— 情头配对助手的前世今生" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;这个机器人已经运行了近一年的时间，我花了一个周末写完以后也没有时间去优化过它，目前它已经累积学习到了20万个情侣头像，积累了4669个粉丝。有无数人和她对话和说感谢。这听起来就非常赛博朋克。&lt;/p&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;由于我本身也有其它工作要忙，加上本身它只是一个个人作品，而非产品决策。所以这个&amp;quot;机器人&amp;quot;只是作为一个彩蛋镶砌在社区里，并没有也不打算作为一个完整产品功能呈现。&lt;/p&gt;
&lt;p&gt;之所以写下这个过程，是觉得实现过程还算是蛮有趣好玩的。之后在其上也发生过许多有趣的故事。&lt;/p&gt;
&lt;p&gt;因为这个后来还诞生过一个想法，如果一个社区里拥有大量带有特定功能的机器人，人可以和人交流，也可以和机器人交互，那样的社区一定未来感十足吧。&lt;/p&gt;
&lt;p&gt;祝大家与机器人相处愉快 ~&lt;/p&gt;</content:encoded></item><item><title>即刻多端实时通信实践</title><link>https://blog.joway.io/posts/socket-io/</link><pubDate>Tue, 18 Sep 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/socket-io/</guid><category>Tech</category><description>背景 jike-io 是即刻基于 socket.io 构建的一个实时通信基础设施。目前客户端上的所有实时通信服务都是建立在其基础上，涵盖了私信、消息通知、用户反馈、活动页小游戏等诸多组件。
在目前即刻的实时通信设计里，我们的实时通信只是为了让服务端主动推送消息给客户端，客户端不会主动通过 websocket 发送消息。由于几乎我们所有需要发送消息的请求都会有一定业务逻辑在，而这个业务逻辑我们并不希望 websocket 连接层(jike-io)去处理，所以我们仍旧采用传统 HTTP 请求的方式去发送请求。至于之后是否需要推送消息给用户，由服务端调用 jike-io 的接口进行实现。</description><content:encoded>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;jike-io 是&lt;a href="https://www.okjike.com/"&gt;即刻&lt;/a&gt;基于 socket.io 构建的一个实时通信基础设施。目前客户端上的所有实时通信服务都是建立在其基础上，涵盖了私信、消息通知、用户反馈、活动页小游戏等诸多组件。&lt;/p&gt;
&lt;p&gt;在目前即刻的实时通信设计里，我们的实时通信只是为了让服务端主动推送消息给客户端，客户端不会主动通过 websocket 发送消息。由于几乎我们所有需要发送消息的请求都会有一定业务逻辑在，而这个业务逻辑我们并不希望 websocket 连接层(jike-io)去处理，所以我们仍旧采用传统 HTTP 请求的方式去发送请求。至于之后是否需要推送消息给用户，由服务端调用 jike-io 的接口进行实现。&lt;/p&gt;
&lt;p&gt;在客户端层面，每一个在线用户都会向服务端建立一个 websocekt 连接，后端会为每一个用户建立一个单独的 room 。所有需要通知到该用户的消息都会使用该 websocket 连接进行推送。这种设计相较于针对不同场景建立不同的room的方案，带来的好处是无论需求如何变化，我们的 room 数目永远是和在线用户数一致了，避免个别复杂需求导致 room 数目暴涨。而具体消息类型我们通过自己定义数据格式来进行鉴别。&lt;/p&gt;
&lt;p&gt;我们的整套方案是完全依赖于 socket.io 的，期间也遇到了不少大大小小的坑。有些其实是我们自己的场景与其设计初衷不是非常吻合导致的，还有一些算是它的设计缺陷。&lt;/p&gt;
&lt;p&gt;在讨论上述问题之前，我们需要去弄明白的一个事情是，socket.io 到底背后做了哪些事情。&lt;/p&gt;
&lt;h2 id="socketio-的设计与实现细节"&gt;socket.io 的设计与实现细节&lt;/h2&gt;
&lt;h3 id="socketio-是什么"&gt;socket.io 是什么&lt;/h3&gt;
&lt;p&gt;socket.io 是一个非常流行的实时通信框架, 在 Github 上已经积累了 43574 个 star 。它在开源软件里可以算是一个非常产品化了的软件。拥有相对良好的生态，对于许多功能的封装也很体贴，从移动端到 Web 再到服务端都有比较完整的实现。&lt;/p&gt;
&lt;p&gt;socket.io 并不等同于 websocket 框架，它在运行平台不支持 websocket 的时候能够自动回退到 long poll 的方式建立实时通信。同时也实现了断线重连机制。还封装了一套 namespace &amp;amp;&amp;amp; room 的代码层概念。在分布式方面，socket.io 支持多种 adapter 作为 backend 。话虽这么说，但目前看上去可以用的且被人广泛使用的也只有 redis 作为 backend 的 adapter 。所以以下讨论建立在 socket.io-redis 基础之上。&lt;/p&gt;
&lt;h3 id="socketio-如何实现分布式"&gt;socket.io 如何实现分布式&lt;/h3&gt;
&lt;p&gt;当我们有多个 socket.io 节点时，我们需要一个 sticky load balancing 将过来的请求分配到不同的 socket.io 节点上建立持久连接并订阅他们需要的 channel 。当其中一个连接向某个 channel 发送消息时，所有节点需要知道这件事情并检查自己有没有订阅该 channel 的连接，有的话需要将消息发送出去。&lt;/p&gt;
&lt;p&gt;这种设计的代价是，虽然我们在部署层次上实现了分布式，但每个节点还是需要知道我这个系统里流通的所有消息。因为彼此都无法明确知道哪个节点上有订阅了我这个消息的连接。当我们消息量达到一定数量的时候，这个网络传输的开销就会非常巨大。后面会细讲这个事情。&lt;/p&gt;
&lt;h3 id="socketio-内部细节"&gt;socket.io 内部细节&lt;/h3&gt;
&lt;p&gt;socket.io 有一个叫 namespace 的概念, 它只是一个用来区分不同发送事件的程序概念, 多个 namespcae 都是共用同一个连接。每个 namespace 下还可以建立多个 room , 一般用于指明实际发送消息的目的地。假设我们现在有一个 namespace 叫 &lt;code&gt;/xxx&lt;/code&gt; 。每个 socket.io 节点一启动都会订阅以下几个 channel :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;psubscribe&lt;/strong&gt; : &lt;code&gt;/socket.io#/xxx#*&lt;/code&gt; : 模式订阅发送到该 namespace 下的所有 room 里的消息, 事件是 &lt;code&gt;pmessageBuffer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;subscribe&lt;/strong&gt; : &lt;code&gt;/socket.io-request#/xxx#&lt;/code&gt; , 订阅多节点间发送同步信息请求的 channel，事件是 &lt;code&gt;messageBuffer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;subscribe&lt;/strong&gt; : &lt;code&gt;/socket.io-response#/xxx#&lt;/code&gt;, 订阅多节点接受同步信息请求响应的 channel， 事件是 &lt;code&gt;messageBuffer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之前谈到每一个连接的发送消息请求都需要各个节点知道并且检查自己的连接是否需要接受该消息。这里的多节点通信就是使用了 &lt;code&gt;/socket.io-request#/xxx#&lt;/code&gt; 和 &lt;code&gt;/socket.io-response#/xxx#&lt;/code&gt; 这两个 channel 。&lt;/p&gt;
&lt;p&gt;在 redis adapter 里有以下函数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;.onmessage&lt;/strong&gt; : 将消息打包后调用 broadcast 函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;.broadcast&lt;/strong&gt; : 广播消息。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个消息是通过别的节点发来的，则不向其它节点再传递&lt;/li&gt;
&lt;li&gt;如果是本地连接发起的消息, 默认会在 &lt;code&gt;/socket.io#/xxx/#{room}#&lt;/code&gt; 中 pub 当前消息(即向其它节点广播了一个消息)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后调用 &lt;a href="https://github.com/socketio/socket.io-adapter/blob/master/index.js#L122"&gt;socket.io-adapter&lt;/a&gt; 里的 .broadcast 向本地连接发送消息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;.onrequest&lt;/strong&gt; : 将需要所有节点配合查询的操作发送给每个节点，等待每个节点查完以后，将事件 pub 到 &lt;code&gt;/socket.io-response#/xxx#&lt;/code&gt; channel 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;.onresponse&lt;/strong&gt; : 各个节点查询完毕后被 pub 他们各自的响应体到该 channel 。每次拿到都会将 request.msgCount++ , 并且检查改request的msgCount 是否等于 request.numsub , 是的话说明处理完了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;.clients&lt;/strong&gt; : 首先会调用 pubsub numsub 命令查看当前 requestChannel 下有多少订阅者(一般即节点数目)，然后将整个 request 请求体塞进 &lt;code&gt;/socket.io-request#/xxx#&lt;/code&gt; channel去询问其它节点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="遇到的问题与挑战"&gt;遇到的问题与挑战&lt;/h2&gt;
&lt;h3 id="瞬时高峰压力"&gt;瞬时高峰压力&lt;/h3&gt;
&lt;p&gt;当遇到一些突发热点信息的时候，由于有操作系统推送存在，有时候会突然进来数倍的用户，而每当用户激活 app 都会触发 websocket 连接请求，这个时候后端就会有比较大的连接压力。&lt;/p&gt;
&lt;h3 id="nodejs-瓶颈"&gt;nodejs 瓶颈&lt;/h3&gt;
&lt;p&gt;由于 socket.io 是用 nodejs 写的，而 nodejs 本身就是单线程的，虽然 socket.io 有 nodejs cluster 的支持，但就我们本身集群的部署方式来看，我们并不是特别想用这个功能，而且这个功能并不能完全解决我们的问题。所以我们非常需要开多个节点来提供服务。但是扩容节点也会带来其它问题。&lt;/p&gt;
&lt;h3 id="节点扩容压力"&gt;节点扩容压力&lt;/h3&gt;
&lt;p&gt;前面谈到 socket.io 的实现每个节点都会订阅全量的消息, 所以当我们扩容一倍的节点数时，redis 的负载也会增加一倍甚至更多，同时网络传输也会翻倍。&lt;/p&gt;
&lt;h3 id="判断用户是否在线"&gt;判断用户是否在线&lt;/h3&gt;
&lt;p&gt;我们私信侧有一个需求是，当判断该用户 websocket 连接不在线时，通过操作系统推送发送推送。问题出在只有 socket.io 集群内的某个节点自己是知道用户是否在线这件事情的，早期时候是通过每次都调用 &lt;code&gt;.clients&lt;/code&gt; 方法取得所有用户列表来做的，但是显然这种做法没法扩展。后来我们通过监听 connect 事件在 redis 里 set 一个 key ，监听 disconnect 移除 key 。这种做法遇到的一个问题是，当一个应用 crash 了或者机器自己挂掉了的时候，disconnect 事件的代码可能并没有来得及被调用，导致一直认为这个用户在线而丢失了系统通知。&lt;/p&gt;
&lt;p&gt;目前我们是通过对 key 设置了过期时间来尽可能减轻这个问题导致的失效时长，但总体来说这不是一个非常优雅的方案。&lt;/p&gt;
&lt;h3 id="redis-扩展性问题"&gt;redis 扩展性问题&lt;/h3&gt;
&lt;p&gt;redis 虽然本身单机性能非常强劲，但是它归根结底但单线程的，规模大了肯定会遇到瓶颈。虽然 redis 自己目前有了 cluster 的实现，但总体来讲，redis cluster 的设计只能算是单机 redis 的一个补丁，并不是一个分布式 k-v 数据库的优雅实现。不过如果真的需要使用 redis cluster ，在不改动代码的情况下，也没法使用 &lt;code&gt;.cleints&lt;/code&gt; 这种需要多节点配合的远程调用请求，因为 redis cluster 不支持 &lt;code&gt;pubsub numsub&lt;/code&gt; 命令。这个命令其实无非就是获得当前订阅者，正常时候就是节点数，所以你可以非常简单地通过别的方式去获取，这个问题倒也是能够解决。&lt;/p&gt;
&lt;p&gt;redis cluster 的 pub/sub 实现和 socket.io 其实很像，每个 redis 节点同样需要知道所有 publish 的消息。无非是每个 redis 节点的连接数少了。但是在 socket.io 这个事情上我们会发现，本身我们向 redis 节点建立的连接并不多，所以我很怀疑这种方案是否真的能够对性能有提升。或许有那么一点，但是整个方案会给人一种&amp;quot;凑合着用&amp;quot;的感觉。&lt;/p&gt;
&lt;h2 id="架构改进"&gt;架构改进&lt;/h2&gt;
&lt;p&gt;观察上面这套方案，我们发现真正的矛盾在于: &lt;strong&gt;我们无法定位到消息的接受者在哪个节点上&lt;/strong&gt;。只要能够解决了这个问题，我们就不再需要每个节点都订阅所有消息，也不需要不同节点之间的消息同步通信。而一旦不需要这两点，那么我们可以直接移除 redis 。这样也不会面临 redis 上面的问题。&lt;/p&gt;
&lt;p&gt;许多人产生的一个误解是认为 socket.io 的 &amp;ldquo;pub/sub&amp;rdquo; 功能是 redis 提供的，但事实上 redis 只会为他们提供了多节点信息同步通信的功能，真正的 &amp;ldquo;pub/sub&amp;rdquo; 是每个 socket.io 节点自己在维护和处理。&lt;/p&gt;
&lt;p&gt;由于我们永远都是向一个用户发送消息，如果任何一个请求进来先通过 load balancer 将其 userId hash 到一个 socket.io 节点上建立连接，之后我们发送消息的时候，都只需要经过同样的 hash 定位到它的节点然后直接发送消息过去就行了。判断用户是否在线也变得非常轻量级。&lt;/p&gt;
&lt;p&gt;这个方案还有一个问题在于，我们的这个 hash 算法首先必须是精确定位到节点的，再者，当我们需要加一个节点的时候，即便使用了一致性 hash 还是会导致有些 userId 被 hash 到了它并不存在的节点上。因此每当用户连接时都需要将用户ID和访问节点注册到一个注册表中，每次发送消息都从里面去取。当节点变化时，并不会影响已有的用户，新连接也能无感知被移动到新的节点上。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;socket.io 是一个非常不错的开源产品，它各个组件间非常解耦，方便使用者进行各种层次的定制化。但一件比较吊诡的事情是，即便它坐拥四万多 star 却依然缺少一个活跃的生态社区，也没有一个完善的文档讲它的实现原理。虽然它的组件化设计可以让社区轻松实现定制化需求，但事实上社区去真正实现这些需求的人却微乎其微。个人的一个可能性猜测是因为当一个公司体量到了需要考虑扩展性的时候，可能都会偏向于实现自己的通信协议。而 socket.io 目前的架构加上 redis 优秀的单机性能表现本身足以支撑一个中小型产品的体量使得在其上做针对性技术改造的人并不多。&lt;/p&gt;</content:encoded></item><item><title>朝鲜 —— 小国寡民的主体思想实践</title><link>https://blog.joway.io/posts/north-korea/</link><pubDate>Sat, 07 Jul 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/north-korea/</guid><category>Travel</category><description>
鸭绿江对岸的中国小贩架起望远镜，游客可以花五块钱远眺对岸朝鲜风光
七八年前看过一篇文章讲突尼斯的反美国化运动，对当时认为美国才是人类文明未来的我很是冲击。后来去到一些国家看到苏联解体以后这些国家被迅速美国化的现状感到不甚惋惜。《再见，列宁》里最后就以一幅巨大的可口可乐广告作为东德覆灭的一记缩影。</description><content:encoded>&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530631577_hu_965868527ed27f48.webp" srcset="https://blog.joway.io/images/old-blog/1530631577_hu_4a75acdce754cf87.webp 800w, https://blog.joway.io/images/old-blog/1530631577_hu_9dc40a6b19cb961e.webp 1200w, https://blog.joway.io/images/old-blog/1530631577_hu_965868527ed27f48.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="900" alt="朝鲜 —— 小国寡民的主体思想实践" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;鸭绿江对岸的中国小贩架起望远镜，游客可以花五块钱远眺对岸朝鲜风光&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;七八年前看过一篇文章讲突尼斯的反美国化运动，对当时认为美国才是人类文明未来的我很是冲击。后来去到一些国家看到苏联解体以后这些国家被迅速美国化的现状感到不甚惋惜。《再见，列宁》里最后就以一幅巨大的可口可乐广告作为东德覆灭的一记缩影。&lt;/p&gt;
&lt;p&gt;全球化很大程度其实是美国化，诚然美国所输出的文化里有许多伟大的东西，诸如改变世界的互联网产品、璀璨的影视剧工业，这些都是人类文明顶尖的造诣，但和欧洲文化截然不同的是，欧洲擅长输出的文学、音乐、建筑和美术目的都是让人们有着更高的审美和思考能力，更多关注于个人。而美国文化很多时候是没有这个考虑的，主要出于实用性考量，比如提高信息检索能力，连接人与人的关系，提升吃饭效率，都是非常社会化的改变。从美国互联网公司常说的&amp;quot;改变世界&amp;quot;口号里能够看出这点，你很难想象一个欧洲人说要改变世界。&lt;/p&gt;
&lt;p&gt;正是因为美国经常性输出这种动辄改变社会的文化，加上其政府还特别热衷于改变别人的社会，所以美国化夹杂在全球化的浪潮里席卷到了世界的每一个角落。而这点在前苏联阵营的国家里尤为明显，这很大程度是因为苏联政权在文化上的保守，造成了这些国家长期的文化空虚，最后在苏联解体的时候，被美国文化顺势乘虚而入。西德作为当时美国重点帮扶对象，基本上已经丢失了德国往日的精神，倒是东德保留了最德国的那部份。然而在柏林墙倒塌以后，最后的德国也迅速被西德所同化。即便是今天，德国还时常怀念昔日民主德国的生活。&lt;/p&gt;
&lt;p&gt;在世界潮流的末端，唯独朝鲜这个封闭了半个世纪的国家，近乎百分百地保持了其民族本来的社会面貌。&lt;/p&gt;
&lt;p&gt;在讲朝鲜之前必须要说明的是，毫无疑问这个国家是世界上最极权主义的国家之一，毫无疑问计划经济是目前人类发展水平下最糟糕的经济政策之一。基于这个认知之下，我们才能够来谈论极权主义和计划经济之下的朝鲜，所展现出来或许还不是那么糟糕的一面。这就和上山下乡运动一样，这个运动同样带来了非常优秀的文学作品，也让许多年青人对社会增进了更深的认知，但这并不妨碍我们认为这个运动是荒谬的。&lt;/p&gt;
&lt;p&gt;在朝鲜让我感触最大的一点是，几乎你见到的每一个人，都是在积极地为建设国家而努力，同时他自身也时刻提醒自己这一点。在飞机上看到大包小包采购回祖国的大叔，拿着公文袋着急出关的公务员，田间集体作业的农民，周五下午集体在城市各个角落拔草的学生们，空地上排练阅兵的小朋友。社会主义所崇尚的人人劳动、集体劳动第一次亲眼见到还是挺震撼的。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530896426_hu_9e7f8381b3004134.webp" srcset="https://blog.joway.io/images/old-blog/1530896426_hu_12e63917e45d278c.webp 800w, https://blog.joway.io/images/old-blog/1530896426_hu_48153935bb4b9ca1.webp 1200w, https://blog.joway.io/images/old-blog/1530896426_hu_9e7f8381b3004134.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1064" alt="朝鲜 —— 小国寡民的主体思想实践" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;朝鲜的&amp;quot;主体思想&amp;quot;历来被人所嘲笑，但仔细去看其内容&amp;quot;人就是自己命运的主人，也是开拓自己命运的力量&amp;quot;并无感受到不妥。相比与某几十个词语拼凑出的价值观而言，简洁又实用。同时考虑到其背景出于不愿做大国的附庸而坚持独立发展，更让人尊敬。当然主体思想里也有大篇幅的极权主义和领袖崇拜部分，这也是其另外一面。如果你看到如今朝鲜领导人在中美之间的斡旋，你更能深刻理解什么叫做&amp;quot;人就是自己命运的主人&amp;quot;，而且他也事实上做到了这点。&lt;/p&gt;
&lt;p&gt;主体思想另一实践是在核武器的问题上。我觉得从事理上来讲，你不能在自己制造完核武器就不允许别人制造。基于这个认知下，我并不认为朝鲜制造核武器有什么不妥，如果我是朝鲜人我自己肯定支持，就像我们从没有人会反对说我们即便当时客观条件下还吃不饱饭的时候都要坚持造两弹一星。同时我也完全支持韩国部署萨德，首都40公里外隔一个朝鲜，换作是谁能够放心 ? 我们可以出于个人利益去反对他们搞事情，但他们的确也同时在做着正确的事情这个我觉得还是得承认。&lt;/p&gt;
&lt;p&gt;判断一个国家是否有未来很大程度在于当地年轻人的生活状态。朝鲜年轻人有着非常清晰的未来发展路径，你可以从军，也可以读大学，无论哪种最后都是国家解决你的一切需求。每个人未婚时候的梦想是娶个好的妻子，已婚时候的梦想是有个平安美好的家庭。年轻人活着都有个奔头，同时节奏上又不至于会让你焦虑，对于大部份人而言，能拥有这种生活，附赠多崇拜下领袖其实也并没有太多损失。&lt;/p&gt;
&lt;p&gt;在不出现经济问题的情况下，人民的日常生活取决于具体政策而非政治的。东亚某国虽然政治上一塌糊涂，但其政策的确在全世界范围内无论是制定还是执行都算是非常不错的，因而其也有着世界范围内中等偏上的宜居性。朝鲜同样如此，只是由于本身的封闭加上经济制裁，所以朝鲜的经济制约了政策的执行，各类物品短缺也是出于此。但如果未来有一天朝鲜能够开放，并且联合国也取消了经济制裁的话，顺带加上社会主义强有力的执行力，相信这个2000万人口的小国家发展速度是飞速的。&lt;/p&gt;
&lt;p&gt;值得一提的是，我在朝鲜居然还遇到了别着领袖胸章的&amp;quot;日本人&amp;quot;，学生服上刻着&amp;quot;大阪朝高&amp;quot;。领袖胸章是一定只有朝鲜人自己才可以佩戴，并且完全是非卖品。后来才知道他们是长期定居在日本的朝鲜人，大多拥有双国籍。而他们来朝鲜，完全是出于对这里的热爱而不像我们纯粹是来猎奇的。回国后查阅了一些资料才得知，这些朝鲜人在日本完全隔离于日本社会，有自己单独的学校，并且继续传播着领袖崇拜。在自己的国土上生活着这种调性的几十万朝鲜人日本肯定也不爽，所以经常与他们发生冲突，也不认为他们是真正的日本人。朝鲜方面之前还干过一个匪夷所思的缺德事是，在日本后欧洲绑架了几十个日本人，弄到了朝鲜去培训日语。这缺德程度也是举世罕见了，更而加剧了朝日关系的恶化。或许正是因为这种日本方面的排挤，更加促使他们怀念故土的味道，虽然真的要他们放弃日本国籍加入朝鲜他们也决不愿意。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530895977_hu_f7145f40a38b1f43.webp" srcset="https://blog.joway.io/images/old-blog/1530895977_hu_90334e64657bea75.webp 800w, https://blog.joway.io/images/old-blog/1530895977_hu_4fd22638393581.webp 1200w, https://blog.joway.io/images/old-blog/1530895977_hu_f7145f40a38b1f43.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1199" alt="朝鲜 —— 小国寡民的主体思想实践" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;朝鲜裔日本人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在旅行自由方面，朝鲜几乎没有自由可言。但作为游客，你也还是有那么一些操作空间的。原则上讲，只要你没有间谍行为，不侮辱人家领导人，人身安全方面完全没有问题。拍照方面只限制了不拍军人，「最好」不要拍落后的一面。虽然你能够去到的地方都是被当地所安排好的，但也不是说所有的东西都是设计的，还是有大量的日常居民生活细节可以拍摄。偶尔偷偷拍一些落后的景象也不会有人来要求你删掉。同时平壤的机场安检又是我所有去过国家里最为宽松的，连充电宝都不要求检查，更没有什么检查相机之类的。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530898563_hu_8792f5037d0a641d.webp" srcset="https://blog.joway.io/images/old-blog/1530898563_hu_50b208eb4fba7d.webp 800w, https://blog.joway.io/images/old-blog/1530898563_hu_1ad8500fc0cd64db.webp 1200w, https://blog.joway.io/images/old-blog/1530898563_hu_8792f5037d0a641d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1062" alt="朝鲜 —— 小国寡民的主体思想实践" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;平壤街头&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530898647_hu_50930bfb1dc101f5.webp" srcset="https://blog.joway.io/images/old-blog/1530898647_hu_743dd8b0a2cdddbd.webp 800w, https://blog.joway.io/images/old-blog/1530898647_hu_8dc1824d51e9a756.webp 1200w, https://blog.joway.io/images/old-blog/1530898647_hu_50930bfb1dc101f5.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="894" alt="朝鲜 —— 小国寡民的主体思想实践" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;平壤街头&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1530898709_hu_26043edfb5686bb7.webp" srcset="https://blog.joway.io/images/old-blog/1530898709_hu_8b5da363ca118333.webp 800w, https://blog.joway.io/images/old-blog/1530898709_hu_15e2851756f08e6b.webp 1200w, https://blog.joway.io/images/old-blog/1530898709_hu_26043edfb5686bb7.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="982" alt="朝鲜 —— 小国寡民的主体思想实践" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;地铁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作为纯粹的猎奇，朝鲜还是一个蛮值得去的国家，尤其是在美帝文化下长期浸淫下的我们，可以通过和朝鲜做一个 Diff 从而判断出自己国家什么是外来的，什么是本土的。虽然说传统文化这个词语非常土，但是世界肯定是丰富多采才有趣，何况和大部分国家的本土文化相比，美国文化算是最没有文化的。在具体实现细节上，无非就是工业基础上落后美国许多罢了。以朝鲜如今如此贫乏的文化生活，一旦开放以后势必又是一个被美国化所冲击的国家，只能寄希望于他们能够做的比中国当初要好。&lt;/p&gt;
&lt;p&gt;在我看来，朝鲜是一个&amp;quot;只要叫爹就能吃饱饭但却坚决不肯叫爹&amp;quot;的故事，亚洲国家比较悲惨的命运就在于此，小国几乎都必须要认个爹。而欧洲人的命好就好在这里，不仅可以中立，削除全部军备都能够相安无事。在这个故事里，究竟谁才是正义的一方我们已经无从从中判断，只是能够从其中感受到一种真正在践行的主体思想社会实践，而他们也的确一五一十地做到了。&lt;/p&gt;</content:encoded></item><item><title>使用 Surge 提升多网络环境下的流畅开发体验</title><link>https://blog.joway.io/posts/surge-network/</link><pubDate>Tue, 03 Jul 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/surge-network/</guid><category>Tech</category><description>作为一名后端工程师经常需要在各种网络环境中切换，由于网络拓扑本身的复杂性以及一些网络工具之间的冲突和bug，常常会在切换中造成不必要的麻烦和痛苦。通常很容易在工作中听到同事会问这些问题 :
你有开 vpn 吗 ? 你开了 ss 了吗 ? 你有同时开 ss 和 vpn 吗 ? 你 http 代理是不是被顶掉了 ? 如果同是技术同事间交流那可能还容易，如果是技术和非技术间交流网络情况，那简直是一个灾难。</description><content:encoded>&lt;p&gt;作为一名后端工程师经常需要在各种网络环境中切换，由于网络拓扑本身的复杂性以及一些网络工具之间的冲突和bug，常常会在切换中造成不必要的麻烦和痛苦。通常很容易在工作中听到同事会问这些问题 :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你有开 vpn 吗 ?&lt;/li&gt;
&lt;li&gt;你开了 ss 了吗 ?&lt;/li&gt;
&lt;li&gt;你有同时开 ss 和 vpn 吗 ?&lt;/li&gt;
&lt;li&gt;你 http 代理是不是被顶掉了 ?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果同是技术同事间交流那可能还容易，如果是技术和非技术间交流网络情况，那简直是一个灾难。&lt;/p&gt;
&lt;p&gt;而事实上，在绝大部份时候，我们对于网络拓扑的需求是可被精确描述的，也就是说理想情况下不应当存在一个我为了访问某个服务而手动选择要进入某个网络环境的事情。&lt;/p&gt;
&lt;p&gt;这篇文章会介绍我们在构建复杂网络环境中的良好开发体验时踩过的坑以及最终是如何优雅地解决这个问题的过程。&lt;/p&gt;
&lt;h3 id="历史方案演变"&gt;历史方案演变&lt;/h3&gt;
&lt;p&gt;常见的网络环境有:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;正常大陆网络&lt;/li&gt;
&lt;li&gt;能够上国外网站的网络&lt;/li&gt;
&lt;li&gt;公司内网&lt;/li&gt;
&lt;li&gt;各个服务器集群的内网&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你自己还折腾了一些服务器或者家庭网络，那可能还会更加复杂。&lt;/p&gt;
&lt;p&gt;之前摸索出的一套还算比较方便的解决方案是 :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在本地常驻一个 ss client 并开放 http 代理端口&lt;/li&gt;
&lt;li&gt;在浏览器上使用 &lt;code&gt;Proxy SwitchyOmega&lt;/code&gt; 使 Chrome 都走 ss client 的 http 代理&lt;/li&gt;
&lt;li&gt;开一个 openvpn 连接到服务器内部网络&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种配置方式能够使得我既能连接所有服务器线上服务和数据库，也能自由地用浏览器去 Google 查一些资料。缺点是丢失了办公室原本的网络环境，另外如果你们服务器有两个完全隔离的子网，那么你可能需要同时连两个 vpn 。而且还有一个不好的是，你的所有非线上服务访问都经过了线上vpn机器的一层代理，让你的访问速度变慢了不说，对服务器也不是一个好事。此外，如果你的一些软件无法手动配置代理，那他们只能默认走 vpn 的网络，对于一些需要访问国外服务器的软件来说就麻烦了。&lt;/p&gt;
&lt;p&gt;基于以上缺点，我们又迭代出了另外一个方案:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在服务器上安装一个 ss server&lt;/li&gt;
&lt;li&gt;在本地常驻两个 ss client ，一个指向生产服务器 ss, 一个指向国外 ss , 并开放 socks5 代理端口&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;Proxifier&lt;/code&gt; 代理所有本机连接并指定一些规则选择直接访问还是转发到本地的两个 ss client 上。例如我选择让所有 10.1.0.0/16 请求走生产服务器ss，别的都走国外 ss 的那个 client ，同时在 client 里配置好 pac 规则使得国内的依旧直接走本地网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方案理论上完全实现了我们的所有需求，但是需要的组件太多了，看着就很繁琐，无法推广给团队其它人使用。同时这些规则也很零零碎碎，几乎没什么可维护性 。&lt;/p&gt;
&lt;h3 id="使用-surge-构建高效开发体验"&gt;使用 Surge 构建高效开发体验&lt;/h3&gt;
&lt;p&gt;在使用 surge 长达两年之后，我才想起来仔细阅读下它的文档。无意中发现它完全不是一个单纯的&amp;quot;网络调试工具&amp;quot;，同时具备了非常完整灵活的规则系统。而这套规则系统完全能够实现我目前所遇到的所有问题。&lt;/p&gt;
&lt;p&gt;我先简单描述下我们的基础设施网络情况:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们首先有一个公司内网，许多网站要求只有公司内网下才能访问。(&lt;code&gt;10.0.0.0/16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;我们服务器机器都在一个子网中 (&lt;code&gt;10.1.0.0/16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;我们在机器上搭建的 kubernetes(k8s) 集群自身也有一套子网 (&lt;code&gt;10.2.0.0/16&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;k8s 自己有一套内部域名解析系统，例如 xxx.default.svc.cluster.local 会被它自己一个叫 kube-dns 的服务解析成 10.2.0.1 , 这套解析只在集群内生效。&lt;/li&gt;
&lt;li&gt;在日常开发中，我们经常需要在本地访问各种内网中的服务，同时又要使用 Google 查阅资料&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;值得庆幸的是，我们所有的内网网段彼此互不重叠，也就是说，这给了我们单纯依靠 ip 来区分走什么代理的能力。&lt;/p&gt;
&lt;p&gt;surge 支持通过域名或者IP-CIDR来判断需要走哪个代理。这就使得我们的工作变得极其之简单了。首先只需要:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 k8s 内部搭建一个 ss server , 确保这个 server :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能够解析 k8s &lt;code&gt;*.*.cluster.local&lt;/code&gt; 的域名&lt;/li&gt;
&lt;li&gt;能够访问 k8s 自身的所有机器&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 surge 代理服务器:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;部分服务器走国外代理&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;lookup-api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;googleapis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;bintray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;cnn1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazonaws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;ruguoapp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;DIRECT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 surge IP-CIDR 规则使得特定 ip 段走各自代理服务器:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;DIRECT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;AWS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;enhanced-mode&lt;/code&gt; 使得我们能够代理非 http 的其它基于 TCP 协议的请求，如上配置我们能够使得 git ssh 协议也走代理。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;force-remote-dns&lt;/code&gt; 表示使用远程服务器来解析我们的域名，一方面可以让代理服务器拿到适合他自己位置的CDN地址，另外一方面可以防止本地DNS被劫持造成的影响，而对于 k8s 而言，这个设置还有一个好处是能够不需要本地解析 k8s 的 service name , 因为本地本身也没有能力去解析这个域名。而由于我们的 ss server 本身就架设在 k8s 内部，所以它完全有能力来解析这个域名。&lt;/p&gt;
&lt;p&gt;完整的 Rule 配置如下:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;General&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;loglevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;skip-proxy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localhost&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;dns-server&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;114.114&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;114.114&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;223.5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;5.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;119.29&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;29.29&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;external-controller-access&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xxx&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8888&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;bypass-system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;bypass-tun&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localhost&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;enhanced-mode-by-rule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;replica&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ipv6&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6152&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;socks-port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6153&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Replica&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;hide-apple-request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;k8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rc4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mypassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;lookup-api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;googleapis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;bintray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;cnn1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazonaws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;force-remote-dns&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;DOMAIN-SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;ruguoapp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;DIRECT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;DIRECT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;AWS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;IP-CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10.2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;K8S&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;GEOIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;CN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;DIRECT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;FINAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;SS-SERVER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GEOIP,CN,DIRECT&lt;/code&gt; 表示中国的IP都直连&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FINAL,SS-SERVER&lt;/code&gt; 表示其余的都走代理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后我们就可以删掉一切之前接触过的网络工具，只需开一个 surge, 并且设置为系统代理和开启增强模式，我们能够实现的功能千言万语只能用一个成语来形容 : &lt;code&gt;四海为家&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面是这种&lt;code&gt;四海为家&lt;/code&gt;式体验的生活描述:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当我们想访问 k8s 的服务例如 &lt;code&gt;http://xxx.default.svc.cluster.local:3000&lt;/code&gt; 的时候，我们直接在浏览器里输入，surge 匹配到规则里的后缀将它发送到代理服务器并解析出IP就能直接访问。&lt;/li&gt;
&lt;li&gt;如果我们需要使用 MongoDB GUI 程序(Robo 3T.app) 时，不需要去配置什么代理，直接连 &lt;code&gt;mongodb://username:pass@10.1.0.5:30001/db?authSource=admin&amp;amp;replicaSet=rs0&lt;/code&gt; , surge 会直接代理 mongo 的协议(也是基于TCP的)。&lt;/li&gt;
&lt;li&gt;当你要 git clone 一个 ssh 地址时，它也会自动去走代理。而不需要去配置 gitconfig 文件。&lt;/li&gt;
&lt;li&gt;如果你需要连接线上服务器的 redis 也只需要直接从本地执行 redis-cli , 不需要连到线上服务器&lt;/li&gt;
&lt;li&gt;同时你可以流畅地使用任何邮件客户端收发 gmail ，而不用管该客户端是否是smtp还是https。&lt;/li&gt;
&lt;li&gt;你可以自由访问一切国外网站。&lt;/li&gt;
&lt;li&gt;当你访问国内网站依旧是以本地网络出去，闲暇摸鱼 bilibili 而不担心速度。&lt;/li&gt;
&lt;li&gt;偶尔你需要手机抓包或者电脑抓包，直接打开 Surge Dashboard 而无需配置，即便是在电脑上抓手机包亦是如此(必须在同一个局域网下，surge for ios 会自动传输数据到 Mac 客户端上)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作为内部工具更为重要的是，如果你想把同样的工作体验传播给同事，仅需给他一个几十行的配置文件即可，而他并不需要知道任何网络知识，导入直接上手。&lt;/p&gt;
&lt;h3 id="一些可能的问题"&gt;一些可能的问题&lt;/h3&gt;
&lt;p&gt;对于生产环境的访问严格讲不应当过于方便，过度的方便可能也会导致人为误操作的风险。所以建议仅仅放入一些安全性较低的子网进配置文件。&lt;/p&gt;
&lt;p&gt;关于 ss server 的鉴权目前普遍是用端口区分的形式，我们内部本身就存有一套鉴权体系，所以打算有空的时候 fork 下 ss 的代码加入内部鉴权机制，便于管理。&lt;/p&gt;
&lt;p&gt;如果你们公司内部不是使用surge，其实你也可以考虑使用 &lt;code&gt;Proxifier&lt;/code&gt; 实现同样的需求，只不过用起来不如surge来得方便。&lt;/p&gt;</content:encoded></item><item><title>一份其实好吃的 LaTeX 入门餐</title><link>https://blog.joway.io/posts/latex/</link><pubDate>Sun, 13 May 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/latex/</guid><category>Tech</category><description>最近在使用 LaTeX 写作，发现虽然这个「软件」使用简单，设计简约，但使用起来却并不是非常的容易，加上其生态非常芜杂，各种宏包和发行版层出不穷，中文世界鲜有文章系统地去讲他们之间的关系。这篇文章不会去介绍其基本用法，而是以一个更为宏观的角度，旨在厘清 TeX 排版系统的来龙去脉，以及其生态圈中各个项目的作用与关系。或有纰漏，还望雅正。</description><content:encoded>&lt;p&gt;最近在使用 LaTeX 写作，发现虽然这个「软件」使用简单，设计简约，但使用起来却并不是非常的容易，加上其生态非常芜杂，各种宏包和发行版层出不穷，中文世界鲜有文章系统地去讲他们之间的关系。这篇文章不会去介绍其基本用法，而是以一个更为宏观的角度，旨在厘清 TeX 排版系统的来龙去脉，以及其生态圈中各个项目的作用与关系。或有纰漏，还望雅正。&lt;/p&gt;
&lt;p&gt;标题致敬 Liam Huang 老师很流行的一篇文章 &lt;a href="https://liam0205.me/2014/09/08/latex-introduction/#%E4%BC%98%E9%9B%85%E7%9A%84_LaTeX"&gt;《一份其实很短的 LaTeX 入门文档》&lt;/a&gt; 。&lt;/p&gt;
&lt;h3 id="什么是-tex"&gt;什么是 Tex&lt;/h3&gt;
&lt;p&gt;TeX 是高德纳教授在70年代末编写 &lt;em&gt;&lt;strong&gt;The Art of Computer Programming&lt;/strong&gt;&lt;/em&gt; 时，对当时的计算机排版技术感到无法忍受，因而决定自己开发一个高质量的计算机排版系统 TeX 。&lt;/p&gt;
&lt;p&gt;TeX 的版本号有别于当下流行的 &lt;code&gt;x.x.x&lt;/code&gt;，而是以圆周率 π 的形式。当前的版本号是 &lt;code&gt;3.14159265&lt;/code&gt; ，所以下一个版本是 &lt;code&gt;3.141592653&lt;/code&gt; 。最终无限收敛到 π ，代表了 TeX 不断追求完美的理想。而事实上 TeX 也的确堪称「完美」，高德纳甚至曾悬赏任何发现 Bug 的人，每一个漏洞的奖励金额从2.56美元开始，之后每发现一个 Bug 都会翻倍，直至327.68美元封顶。&lt;/p&gt;
&lt;p&gt;TeX 的输出文件被称为 DVI(Device Independent) 文件，DVI 可以作为一种界面描述的中间格式，通过它可以再进而转换成 PDF 格式。&lt;/p&gt;
&lt;p&gt;为了区分概念，我们应当将高德纳写的 TeX 软件分为 TeX 语法 和 TeX 编译器。虽然高德纳自己写了一个 TeX 编译器，但其它人依旧可以在不同平台自己编程实现 TeX 语法的编译器。为了保持语法的稳定，TeX 有一组严格的测试文件，如果测试文件文件的输出结果不同于预定的结果，那么这个排版系统就不能够被称为「TeX」。这些不同的 TeX 编译器我们都称之为 「 TeX 引擎」。&lt;/p&gt;
&lt;p&gt;TeX 目前(2018年)有如下几个编译引擎:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TeX : 高德纳最早开发的官方实现，只能编译成DVI格式。&lt;/li&gt;
&lt;li&gt;pdfTeX : 支持直接编译成 PDF 格式。&lt;/li&gt;
&lt;li&gt;XeTeX : 支持 Unicode 编码和直接访问操作系统字体。&lt;/li&gt;
&lt;li&gt;LuaTeX : Lua 实现的 TeX 编译器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然 Tex 出于向下兼容考虑要求了所有编译器都需要能够编译历史上所有符合标准的 Tex 文件，但并不意味着它不能增加新的功能。TeX 作为一门「宏语言」，能够使用宏定义出新的语法，甚至能够覆盖原先的语法。要理解这一点必须要先理解什么是「宏编程」。&lt;/p&gt;
&lt;h3 id="什么是宏编程"&gt;什么是宏编程&lt;/h3&gt;
&lt;p&gt;要理解为什么宏如此强大，甚至强大到可以自己定义语法，我们可以来看一个例子 。&lt;/p&gt;
&lt;p&gt;我们假设我们写了一门新的语言 「D」，它的语法如下 :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;# compare:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;a&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;b&amp;#34;&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;a&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;a&amp;#34;&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;# do .. while
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;# io
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;# macro
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="nf"&gt;TYPE_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="p"&gt;......&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;乍一看宏(macro) 的用法非常像函数，但和函数有着本质区别的是，宏是一个编译时的字符串替换，而函数是运行时的一段可执行代码。例如:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="nf"&gt;PLUS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;PLUS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码在预处理的时候就会被替换成&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我们可以发现这门语言中并不存在 if 语法，也不存在循环，但是通过宏我们能够为他定义出 if 和 for 语法。例如:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="nf"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="nf"&gt;FOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;__for_count__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;__for_count__&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;__for_count__&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;利用宏我们完全可以扩展出一门新的语法结构，我们命名为 「D+」。「D+」严格意义上并不能算一个新的语言，只是称之为一个宏集。并且由于其只是在编译时做了字符串替换，所以依旧是可以用D的编译器编译的。&lt;/p&gt;
&lt;p&gt;回到主题上来，TeX 就像是这里的 D 语言，LaTeX 就是「D+」宏集。LaTeX 本身也都是用 TeX 编译器来实现编译的。&lt;/p&gt;
&lt;h3 id="宏集和宏包"&gt;宏集和宏包&lt;/h3&gt;
&lt;p&gt;宏集和宏包其实是一堆 TeX 指令集合，宏集以 &lt;code&gt;.cls&lt;/code&gt; 结尾，宏包以 &lt;code&gt;.sty&lt;/code&gt; 结尾。宏集需要以 &lt;code&gt;\documentclass{...}&lt;/code&gt; 来加载，且一个文档一般只使用一个documentclass。而宏包是以&lt;code&gt;\usepackage{...}&lt;/code&gt;，无使用限制。&lt;/p&gt;
&lt;p&gt;正式因为这个区别，所以一般宏集是一个完整的文档格式模板(也称之为「格式format」)，比如武汉大学的论文模板 &lt;code&gt;\documentclass{whucls}&lt;/code&gt;。而宏集比较灵活，例如你临时需要一些宏包来定义一些特殊字体就可以按需加载宏包。&lt;/p&gt;
&lt;h3 id="什么是-latex"&gt;什么是 LaTeX&lt;/h3&gt;
&lt;p&gt;LaTeX 就是一种 TeX 宏集，它内嵌了许多常用文档格式，例如 : article、report、book、letter等。使用方式很简单，在 .tex 文件最开头加上 &lt;code&gt;\documentclass{article}&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;LaTeX 内建了许多新的指令，只需要对相应的段落内容予以其在文档中的「类型」即可使 LaTeX 自动为你排版。例如以下这段文档:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tex" data-lang="tex"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;\documentclass&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;article&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\title&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;TITLE XXXXXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\author&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\date&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;&lt;span class="k"&gt;\today&lt;/span&gt;&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\begin&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\clearpage\maketitle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ....
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\section&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXXXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\section&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXXXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\subsection&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\subsection&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\section&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;XXXXX&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\end&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 title、section、subsection 都是预先定义的 LaTeX 宏，在宏中已经定义好了样式。当然如果你需要在 LaTeX 宏集的基础上做自己的修改，你也可以基于 LaTeX 宏集做一个单独的宏包。&lt;/p&gt;
&lt;h3 id="为什么要使用-latex"&gt;为什么要使用 LaTeX&lt;/h3&gt;
&lt;p&gt;我把上面这种写作方式称之为「面向对象写作」。这种设计的优点在于其逻辑完全与实际需求场景相吻合。例如我们在写论文的时候都会拿到一个论文格式规范清单，上面详细规定了什么样的内容需要以什么样的样式来书写。而我们一般使用 Word 时候的方式却是针对所有内容单独一点点地去设立样式。(当然目前 Word 也开始支持这种面向对象赋予格式，但远远没有 LaTeX 那么彻底。)&lt;/p&gt;
&lt;p&gt;打个比方，LaTeX 相当于带 &lt;code&gt;class&lt;/code&gt; 的 CSS , 而 Word 是裸写&lt;code&gt;&amp;lt;div style=&amp;quot;&amp;quot;&amp;gt;&lt;/code&gt;。LaTeX 使得作者可以全身心地投入到写作之中，而无需去关心样式，当需要调整排版样式的时候，也仅仅只需修改类型的样式而非文档本身即可。&lt;/p&gt;
&lt;h3 id="中文支持"&gt;中文支持&lt;/h3&gt;
&lt;p&gt;关于中文支持网上说法非常过时和混乱。这里做一个统一的说明。&lt;/p&gt;
&lt;p&gt;历史上中文支持方案有 CCT、CJK、xeCJK 三种。CCT 目前已经过时，推荐使用 xeCJK 。&lt;/p&gt;
&lt;p&gt;但支持中文并不意味着就完成了中文化，我们还需要考虑到中文文档在不同文档类型、文本内容类型中的字体、字号等排版上的设计，同时还有不同系统字体系统不同的困扰。另外由于 LaTeX 在上述需求中完全是以英文视角在进行设计，所以回到中文世界我们需要一套完全不同的宏集合以支持完整的中文化需求。&lt;/p&gt;
&lt;p&gt;目前中文世界有一个流行的宏集叫做 &lt;code&gt;CTeX&lt;/code&gt;。与 LaTeX 一样，CTeX 也支持 &lt;code&gt;ctexart&lt;/code&gt;(article)、&lt;code&gt;ctexrep&lt;/code&gt;(report)、&lt;code&gt;ctexbook&lt;/code&gt;(book)等文档类。同样使用如下方式引入:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tex" data-lang="tex"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;\documentclass&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;ctexart&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;严格来讲，CTeX 是一个内含多种程序和文件的软件包套装。但上文讲的 CTeX 指的仅仅其中的宏集/宏包部分。网络上针对 CTeX 的批评很多时候是针对其套装里的GUI和其它软件而言，其实和它的宏集没什么关系。&lt;/p&gt;
&lt;p&gt;CTeX 宏集帮助我们处理好了各种中文排版问题，和操作系统问题，所以一般都推荐直接使用它，不需要额外的任何定义即可实现中文支持。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tex" data-lang="tex"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;\documentclass&lt;/span&gt;&lt;span class="na"&gt;[12pt, UTF8]&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;ctexart&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\begin&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;\end&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;虽然理论上当你使用 CTeX 写作时，应当称&lt;code&gt;该文档使用 CTeX 完成&lt;/code&gt;，但事实上极少会有人这么去说，包括许多说自己在用 TeX 写作的人其实用的也都是 LaTeX 。这种语言上的错误用法大多是因为这个生态其实已经够复杂了，没必要再把人与人之间的交流弄复杂。所以大家统一称这个生态为 LaTeX 。&lt;/p&gt;
&lt;h3 id="编辑器"&gt;编辑器&lt;/h3&gt;
&lt;p&gt;非计算机专业的人往往会出于一些软件使用习惯，将编译器和编辑器等同来看。相当一部分时候编译器也的确顺带着编译器一起被整合成一个软件。但其实即使是系统自带的文本查看工具也可以称之为编辑器。网上主流的TeX编辑器有: TeX Live 、MiKTeX、CTeX 套件。为方便用户使用，这些软件在安装过程中按自动安装上各个主流的 TeX 编译器版本。当然你也可以使用任意其它编辑器诸如 VS Code 、Atom 、Vim ，使用他们的插件或者手动编译即可。&lt;/p&gt;
&lt;h3 id="我的-latex-工作方式"&gt;我的 LaTeX 工作方式&lt;/h3&gt;
&lt;p&gt;我个人比较喜欢的是 xelatex 编译器 + VS Code 的编写方式。&lt;/p&gt;
&lt;p&gt;选择 xelatex 是因为它使用 unicode 编码直接支持中文。而 VS Code 的 LaTeX 插件支持每次保存文件自动编译，同时 Mac 的PDF查看程序每当文件发生变化的时候会自动刷新，从而实现了即写即看、所见即所得的写作体验。&lt;/p&gt;
&lt;p&gt;我长期使用 Markdown 方式写作，即便是用 LaTeX 我们依然可以维持这个方式，方式很简单，markdown 的文档结构是完全能够被映射到 latex 的文档结构上的，利用这个方式可以写一个 parser 就能够实现这种批量转换了。我之后可能会考虑写一个工具来做这个事情。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mac 可以使用 MacTeX : &lt;a href="https://www.tug.org/mactex/"&gt;https://www.tug.org/mactex/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;VS Code LaTex Workshop 插件地址 : &lt;a href="https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop"&gt;https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;VS Code 使用 xelatex 自动编译配置文件 : &lt;a href="https://github.com/joway/latex-template-zh/blob/master/.vscode/settings.json"&gt;https://github.com/joway/latex-template-zh/blob/master/.vscode/settings.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="一些-latex-模板"&gt;一些 LaTeX 模板&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/joway/latex-template-zh"&gt;latex-template-zh&lt;/a&gt; : 我用来写中文文章的模板库，在 ctex 基础上加了一些行间距之类的，使其更加适用于互联网文章的排版。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/joway/resume"&gt;latex-resume-template&lt;/a&gt; : 我的 LaTeX 简历模板。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Kafka 的设计与实践思考</title><link>https://blog.joway.io/posts/kafka-design-practice/</link><pubDate>Mon, 16 Apr 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/kafka-design-practice/</guid><category>Tech</category><description>前几天看了 librdkafka 的官方文档，这篇文档不仅仅讲解了如何使用 Kafka ，某种程度也讲解了分布式系统实现的难点和使用细节，故而让我对 Kafka 的实现原理产生了浓厚的兴趣。
这篇文章从 Kafka 的设计到使用做了一些个人总结，围绕真正实践场景，探寻其设计上的智慧与妥协。
设计 架构设计 Zookeeper Zookeeper 存储了 Kafka 集群状态信息 。
Zookeeper 还负责从 Broker 中选举出一个机器作为 Controller, 并确保其唯一性。 同时, 当 Controller 宕机时, 再选举一个新的 。</description><content:encoded>&lt;p&gt;前几天看了 librdkafka 的&lt;a href="https://github.com/edenhill/librdkafka/blob/master/INTRODUCTION.md"&gt;官方文档&lt;/a&gt;，这篇文档不仅仅讲解了如何使用 Kafka ，某种程度也讲解了分布式系统实现的难点和使用细节，故而让我对 Kafka 的实现原理产生了浓厚的兴趣。&lt;/p&gt;
&lt;p&gt;这篇文章从 Kafka 的设计到使用做了一些个人总结，围绕真正实践场景，探寻其设计上的智慧与妥协。&lt;/p&gt;
&lt;h2 id="设计"&gt;设计&lt;/h2&gt;
&lt;h3 id="架构设计"&gt;架构设计&lt;/h3&gt;
&lt;h4 id="zookeeper"&gt;Zookeeper&lt;/h4&gt;
&lt;p&gt;Zookeeper 存储了 Kafka 集群状态信息 。&lt;/p&gt;
&lt;p&gt;Zookeeper 还负责从 Broker 中选举出一个机器作为 Controller, 并确保其唯一性。 同时, 当 Controller 宕机时, 再选举一个新的 。&lt;/p&gt;
&lt;p&gt;在 0.9 版本之前，它还存储着 Consumer 的 offset 信息 。&lt;/p&gt;
&lt;h4 id="broker"&gt;Broker&lt;/h4&gt;
&lt;p&gt;接收 Producer 和 Consumer 的请求，并把 Message 持久化到本地磁盘。&lt;/p&gt;
&lt;p&gt;集群会经由 ZK 选举出一个 Broker 来担任 Controller，负责处理各个 Partition 的 Leader 选举，协调 Partition 迁移等工作。&lt;/p&gt;
&lt;h3 id="内部组件设计"&gt;内部组件设计&lt;/h3&gt;
&lt;h4 id="topic"&gt;Topic&lt;/h4&gt;
&lt;p&gt;逻辑概念，一个 Topic 的数据会被划分到一个或多个 Partition 中。&lt;/p&gt;
&lt;h4 id="partition"&gt;Partition&lt;/h4&gt;
&lt;p&gt;最小分配单位。一个 Partition 对应一个目录，该目录可以被单独挂在到一个磁盘上，以实现IO压力的负载均衡。同时多个 Partition 分布在多台机器上，也实现了灵活地水平扩容。&lt;/p&gt;
&lt;p&gt;每个 Partition 都能够拥有一个或多个 Replication 副本。创建 Topic 的时候能够指定每个 Topic 的 Replication 数量，来保证高可用，Replication 数量为1时，即没有副本，只有其自身 (所以其自身也算是一个 Replication )。其中一个Replication 被选举为 leader。如果leader挂掉了，也会有相应的选举算法来选新的leader。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有的读写请求都由Leader处理&lt;/strong&gt; 。其他 Replication 从Leader处把数据更新同步到本地。由于针对某一个 Partition 的所有读写请求都是只由Leader来处理，所以Kafka会尽量把Leader均匀的分散到集群的各个节点上，以免造成网络流量过于集中。&lt;/p&gt;
&lt;p&gt;ISR(In-Sync Replica): 是该 Partition的所有 Replication 的一个子集，表示目前 Alive 且与Leader能够“Catch-up”的 Replication 集合。由于读写都是首先落到 Leader 上，所以一般来说通过同步机制从Leader上拉取数据的 Replication 都会和 Leader 有一些延迟(包括了延迟时间和延迟条数两个维度)，任意一个超过阈值都会把该 Replication 踢出ISR。每个 Partition 都有它自己独立的ISR。&lt;/p&gt;
&lt;h4 id="offset"&gt;Offset&lt;/h4&gt;
&lt;p&gt;每个 Partition 都是一个有序序列。编号顺序不跨 Partition ，即每一个 Partition 都是从0开始编号。该编号就是该 Partition 的 Offset 。&lt;/p&gt;
&lt;p&gt;每次写入消息都是被顺序 append 进 Partition 序列中。&lt;/p&gt;
&lt;p&gt;客户端凭 Offset 访问到对应的 message 。&lt;/p&gt;
&lt;h4 id="segment"&gt;Segment&lt;/h4&gt;
&lt;p&gt;Partition 由多个以Offset大小为顺序划分的 Segment 组成。每个 Partion 相当于一个巨型文件(事实上是目录)被平均分配到多个&lt;strong&gt;大小相等&lt;/strong&gt;的 Segment 中。但每个 Segment File 的消息数量不一定相等。文件分开的好处就是能快速删除无用文件，有效提高磁盘利用率。如果是单个文件将很难删除老的数据。&lt;/p&gt;
&lt;p&gt;当某个segment上的数据量大小达到配置值(&lt;code&gt;log.segment.bytes &lt;/code&gt;)或消息发布时间超过阈值(&lt;code&gt;log.segment.delete.delay.ms &lt;/code&gt;)时，segment上的消息会被flush到磁盘，只有flush到磁盘上的消息订阅者才能订阅到，segment达到一定的大小后将不会再往该segment写数据，broker会创建新的segment。&lt;/p&gt;
&lt;h3 id="底层存储设计"&gt;底层存储设计&lt;/h3&gt;
&lt;p&gt;Segment 是 Kafka 文件存储的最小单位。Segment File 由两部分组成，一个是 index file 一个是 data file , 分别以 &lt;code&gt;.index&lt;/code&gt; 和 &lt;code&gt;.log&lt;/code&gt; 结尾。&lt;/p&gt;
&lt;p&gt;Segment 文件命名规则：Partition 全局的第一个 segment 从 0 开始，后续每个segment文件名为上一个 Segment 文件最后一条消息的 offset 值。数值最大为64位long大小，19位数字字符长度，没有数字用0填充。如 &lt;code&gt;0000000000000034567.index&lt;/code&gt; 和 &lt;code&gt;0000000000000034567.log&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;假设 &lt;code&gt;0000000000000034567.index&lt;/code&gt; 的文件内容为:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1,0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3,497
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6,1407
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;N,position
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;3,497&lt;/code&gt;表示该文件 segment 中的第三个，即整个 partition 中的第 &lt;code&gt;34567 + 3&lt;/code&gt;个 message 的在 &lt;code&gt;0000000000000034567.log&lt;/code&gt; 文件中的物理偏移位置是497。注意该 index 文件并不是从0开始，也不是每次递增1的，这是因为kafka采取稀疏索引存储的方式，每隔一定字节的数据建立一条索引，它减少了索引文件大小，使得能够把 index 映射到内存，降低了查询时的磁盘IO开销，同时也并没有给查询带来太多的时间消耗。&lt;/p&gt;
&lt;p&gt;因为其文件名为上一个segment 最后一条消息的 offset ，所以当需要查找一个指定 offset 的 message 时，通过在所有 segment 的文件名中进行二分查找就能找到它归属的 segment ，再在其 index 文件中找到其对应到文件上的物理位置，就能拿出该 message 。&lt;/p&gt;
&lt;h3 id="api-设计"&gt;API 设计&lt;/h3&gt;
&lt;p&gt;Kafka 的核心设计是一个非常简单的模型，有点类似于一个复杂度为O(1)的 K-V 数据库， 客户端指定 &lt;code&gt;topic + partition + offset&lt;/code&gt; ，就能够得到一个 message 。该模型接近于一个无视客户端状态的数据库。在此基础上，调用方只要自己管理 offset 状态就能直接实现一个消息队列的功能。&lt;/p&gt;
&lt;p&gt;当然由于 Kafka 本身就为消息队列而设计，所以它在该底层基础上，提供了以下一些 API 供客户端调用。&lt;/p&gt;
&lt;h4 id="producer"&gt;Producer&lt;/h4&gt;
&lt;p&gt;发送 message 到 Kafka 。客户端需要指定一个 key , kafka 会hash该 key 到一个 Partition 上。如果不指定，将会随机选择一个 Partition 。&lt;/p&gt;
&lt;p&gt;Producer 支持批量操作，消息攒够一定数量再发送，使用适当的延迟换来更高的数据吞吐量。&lt;/p&gt;
&lt;h4 id="consumer"&gt;Consumer&lt;/h4&gt;
&lt;p&gt;一个 consumer 可以消费多个 partition , 但是一个 partition 最多只能被一个 consumer 消费。正是因为这个原因，所以一个 Topic 的 Partition 数量等于该 Topic 能够被并行消费的能力。&lt;/p&gt;
&lt;h4 id="consumer-group"&gt;Consumer Group&lt;/h4&gt;
&lt;p&gt;多个 consumer 组成一个 Consumer Group , 每个消息只能被 Consumer Group 中的一个 consumer 消费。该状态被记录在 Kafka 中，而不需要用户自己维护。&lt;/p&gt;
&lt;h2 id="实践"&gt;实践&lt;/h2&gt;
&lt;p&gt;软件开发没有银弹，Kafka 再强大也会受限于各种不可调和的矛盾 。其它一些软件往往会为了追求某一点能力，而付出一部分代价。而 Kafka 比较灵活的是，它将这种矛盾的选择权通过参数配置和使用技巧的方式交由用户自己去选择。这种做法带来的一个问题是，在具体实践里存在太多权衡因素，导致其使用门槛相对于别的开箱即用的软件过高。&lt;/p&gt;
&lt;p&gt;下面探讨了在一些具体场景里，需要如何去使用 Kafka 。&lt;/p&gt;
&lt;h3 id="如何保证消息发布的可靠性"&gt;如何保证消息发布的可靠性&lt;/h3&gt;
&lt;p&gt;消息的不丢失对于消息队列来说至关重要。但要实现这一点也是非常困难，极端考虑甚至是不可能的，因为机器一定可能会挂，磁盘一定可能会坏，只是看能够承受多大的规模故障罢了。我们这边谈论的消息不丢失主要指:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果发送失败，发送方要能够知道这个消息，方便它进行重试或者相应处理 。&lt;/li&gt;
&lt;li&gt;如果发送成功，要确保发送成功后，即便一部分数量的 Kafka 机器全部被物理销毁，这个消息依旧能够被持久化保存下来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前面讲到了 Kafka 的 Partition 有一个 ISR 机制，当一个 message 被写入到 Leader Partition 中后，并被所有 ISR 给同步到本地，此时只要ISR的机器有一台还存活着且磁盘完好，这个消息就能够正常存在。如果在Leader刚写入完，但此时 Leader 立马挂了，会导致这个消息永久丢失。如果要实现绝对意义的不丢失，就需要客户端当且仅当获知到这个状态时，才认为消息发送是成功的。但这种等待的性能损耗会随着 Replication 的数量增多而线形增多。&lt;/p&gt;
&lt;p&gt;有时候我们要求可能并没有如此之精确，可以只要求 Leader 写入完了就告诉我们成功了。但这里会存在一个消息重发的情况，例如，leader 写入完成后告诉我们，但路上丢包了，导致我们以为发送失败了，此时又继续发送了一份消息，这个时候可能会存两份 。 Kafka 是不会去管理这种复杂情况的，客户端需要在使用的时候明确知道这件事情并在程序设计上为此负责，比如可以在每条消息里加一个全局唯一ID去标识一个消息，在消费的时候去判断是否消费过这个消息。&lt;/p&gt;
&lt;p&gt;如果我们要严格要求不重发，且能够接受消息丢失的情况，只要不去理睬 leader 的写入成功信息即可，每个消息仅发送一次，不在乎发送是否成功。&lt;/p&gt;
&lt;p&gt;在 Kafka 客户端中，我们可以有以下三个参数来处理上述情况:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;acks=0: producer 不等待 broker 的 acks。发送的消息可能丢失，但永远不会重发。&lt;/li&gt;
&lt;li&gt;acks=1: leader 不等待其他 follower 同步，leader 直接写 log 然后发送 acks 给 producer。&lt;/li&gt;
&lt;li&gt;acks=all: leader 等待所有 follower 同步完成才返回acks。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="如何保证消息消费的可靠性"&gt;如何保证消息消费的可靠性&lt;/h3&gt;
&lt;p&gt;正常情况下，我们一般希望消息队列里的消息仅被消费一次，且一定会被消费一次，并且处理结果一定是成功的。但要实现这点非常困难，且这一点的可靠性大部分取决于用户编写代码本身的质量。&lt;/p&gt;
&lt;p&gt;Kafka 的 Consumer 机制只是提供了一个保存 Offset 的接口，由于在没有过期的情况下，Kafka 并不会主动去删除消息，所以我们的问题仅仅在于如何去确保&lt;code&gt;保存 Offset&lt;/code&gt;和&lt;code&gt;处理消息成功&lt;/code&gt;这两个操作是一个原子操作。&lt;/p&gt;
&lt;h4 id="有且仅有一次-exactly-once"&gt;有且仅有一次 「exactly once」&lt;/h4&gt;
&lt;p&gt;一般性我们认为计算操作是无状态的，IO操作是有状态的，如果消费者仅仅只是做无状态的一些操作，我们其实完全不需要考虑它是否多次消费的问题。大部分时候让我们头痛的都是数据库的保存操作。有一种取巧的方案是，把每次消费的 Offset 作为一个字段和正常保存操作一起存入数据库中，如果保存失败，则说明处理失败，此时可以重新保存。&lt;/p&gt;
&lt;h4 id="至少一次-at-least-once"&gt;至少一次 「at least once」&lt;/h4&gt;
&lt;p&gt;但我们也可以用更好的程序设计来让这件事情做的更加优雅，如果我们的消费者函数是一个幂等函数，相同的输入执行多次也不会影响到最终结果。那么我们就能够接受重复处理消息的情况。而此时只要确保所有的消息都能够被至少消费一次就行了。这种场景我们可以选择先处理消息，再保存 Offset 。&lt;/p&gt;
&lt;h4 id="至多一次-at-most-once"&gt;至多一次 「at most once」&lt;/h4&gt;
&lt;p&gt;也有的时候我们希望最多处理消息一次，可以接受个别消息没有被处理的情况，我们也可以选择先保存 Offset , 再处理消息。&lt;/p&gt;
&lt;h3 id="如何保证消息的顺序"&gt;如何保证消息的顺序&lt;/h3&gt;
&lt;p&gt;Kafka 每个 Partition 都是相互独立的，Kafka 只能保证单个 Partition 下的有序。如果你的应用程序需要严格按照消息发送的顺序进行消费，可以考虑在程序设计上去做文章。&lt;/p&gt;
&lt;p&gt;举个例子是，我有一个游戏系统，每个人会顺序做一些不同操作，对应不同事件，发送到Kafka。我的消费者显然需要考虑到每个用户操作的上下文关系，但这个时候我们所需要的有序其实是针对单个用户的有序，而不要求全局有序。我们可以以用户的ID作为 key , 确保单个用户一定会被分配到某个固定的 partition 上，这样我们就能够实现单个用户维度的有序了。&lt;/p&gt;
&lt;p&gt;如果你一定要全局的有序序列，还有一种取巧的做法是，所有消息都使用同一个 key , 这样他们一定会被分配到同一个 partition 上，这种做法适用于临时性且数据量不大的小需求，消息量大了会有性能压力。&lt;/p&gt;
&lt;h3 id="高度实时的场景下能够有非常高的吞吐"&gt;高度实时的场景下能够有非常高的吞吐&lt;/h3&gt;
&lt;p&gt;在 Linux 操作系统中，当上层有写操作时，操作系统只是将数据写入 Page Cache，同时标记 Page 属性为 Dirty。当读操作发生时，先从Page Cache中查找，如果发生缺页才进行磁盘调度，最终返回需要的数据。&lt;/p&gt;
&lt;p&gt;当我们的 Producer 处于一个高度实时的状态时，读和写的文件位置会非常接近，甚至完全一样，此时就能最大限度的利用该 Page Cache 机制，也就是这种情况下Kafka 甚至都没有直接去读磁盘的文件。&lt;/p&gt;
&lt;h3 id="kafka-producer-key-选择"&gt;Kafka Producer Key 选择&lt;/h3&gt;
&lt;p&gt;假设一个场景，我们需要将每个用户的 Page View 信息给存入 Kafka ，此时我们会很自然地想到以 userId 来作为 key 。理想情况下这种选择可能是不会错的，但如果假设有一个用户是一个爬虫用户，他个人的访问量可能是正常用户的百倍甚至千倍，这个时候你会发现，虽然 userId 作为 key 而言，它是均匀分布的，但其背后的数据量却并不一定是均匀分布的，久而久之，就可能产生&lt;code&gt;数据倾斜&lt;/code&gt;的情况，导致各个partition数据量分布不均匀。当然对于 Kafka 自身而言，一个Partition里有再多的数据，也不会去影响到它的正常性能。但没有特殊需求时，在选择 key 的时候，还是要考虑到这种情况的发生。&lt;/p&gt;
&lt;h3 id="如何选择-partiton-的数量"&gt;如何选择 Partiton 的数量&lt;/h3&gt;
&lt;p&gt;在创建 topic 的时候可以指定 partiton 数量，也可以在常见完后手动修改。但partiton 数量只能增加不能减少。中途增加partiton会导致各个partition之间数据量的不平等。&lt;/p&gt;
&lt;p&gt;Partition 的数量直接决定了该 Topic 的并发处理能力。但也并不是越多越好。Partition 的数量对消息延迟性会产生影响。&lt;/p&gt;
&lt;p&gt;一般建议选择 broker num * consumer num ，这样平均每个 consumer 会同时读取broker数目个 partition , 这些 partiton 压力可以平摊到每台 broker 上。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 民主专制下的德国</title><link>https://blog.joway.io/posts/deutschland-democracy-authoritarian/</link><pubDate>Wed, 07 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deutschland-democracy-authoritarian/</guid><category>Travel</category><description>关于德国中文媒体有过许多的报道和吹捧，总体来讲这个国家属于那种班级里的乖学生的形象，至少在二战后，德国几乎没有得罪过全世界的任何一个国家任何一个宗教，反而还广受曾经敌人的好评。如今这个年头，这种人畜无害的大国真的不多甚至可以说绝无仅有了。
欧洲国家大大小小有很多，其文明程度也大相径庭。但有一个比较容易的辨别方式是，但凡是说德语的国家，基本上文明程度都不会差，例如奥地利、比利时、瑞士。</description><content:encoded>&lt;p&gt;关于德国中文媒体有过许多的报道和吹捧，总体来讲这个国家属于那种班级里的乖学生的形象，至少在二战后，德国几乎没有得罪过全世界的任何一个国家任何一个宗教，反而还广受曾经敌人的好评。如今这个年头，这种人畜无害的大国真的不多甚至可以说绝无仅有了。&lt;/p&gt;
&lt;p&gt;欧洲国家大大小小有很多，其文明程度也大相径庭。但有一个比较容易的辨别方式是，但凡是说德语的国家，基本上文明程度都不会差，例如奥地利、比利时、瑞士。&lt;/p&gt;
&lt;p&gt;但德国令人尤其是中国人讨厌的，也恰恰是其所谓的文明。甚至以偏激角度来看，德国所谓的文明，恰恰是一种统治手段，只不过这种统治是所有人统治所有人 。所以我称之为“民主专制”。&lt;/p&gt;
&lt;p&gt;我以几个例子作为这种民主专制的说明。&lt;/p&gt;
&lt;p&gt;德国法律规定，雪天时，房屋所有者要在7点到22点期间保持自己屋边人行道的干净状态，且清理出的路要有1.2米以上宽，若有行人因路面打扫不干净而导致摔倒，有权要求房主赔偿。这类法律的实质是市政府无法负担高额的人力清洁成本，由此转嫁给个人，其立法的道德依据是所有人都必须对自己的周边环境负责，其受益者和执行者都是所有人。这类法律摊开讲，其实就是一个 rule ，既是规则，也是统治。&lt;/p&gt;
&lt;p&gt;德国的民主专制思想还体现在宽带通信上。我们知道，宽带其实是一个公共服务，一个区域的总带宽是有固定限制的，如果一个人宽带占用的过多，必然会影响其它人。国内的运营商一般的做法是，每个人可以自己购买不同的带宽上限，高峰时候大家等同比例地下降服务质量，有VIP客户另说。事实上这个策略是非常公平的，技术实现也是最简单容易的。但德国电信的脑回路显然不一样，目前是2M/s的宽带每个月超出75G的流量后，下降到48kb/s。这个规定是什么时候实行的呢，2016年。你无法想象在2016年使用 48kb/s 的网络能打开个什么网站。其规定的依据是认为，大部分客户都不会超出75G月流量，而超出的客户大多是由于经常使用youtube等大流量应用，正因为他们平时本身占用了大量的带宽，故而限速是符合公平原则的。从理论上我们没法去辩驳这种说法本身，但我相信任何一个在中国数字社会生活过的人都不会去接受这种做法吧。用我们党的话来说，就是这个问题的本质是落后的电信基础设施赶不上人民群众日益增长的文化娱乐需求。德国人过度依赖使用民主专制去解决不公平的问题，而忽视了问题本身存在的原因。那些经常占用大量带宽上 youtube 的人可能恰恰就是在辛苦工作给你养着那些月宽带量不到100M的老年群体的年轻用户。在世界上绝大部份发达不发达国家都已经意识到互联网是新时代的水和电的今天，很难想象这个发达国家老大哥居然还会出台如此落后的规定，更何况即便不限速这个国家的宽带水平也远远落后其它周边发达国家。&lt;/p&gt;
&lt;p&gt;我居住的街区有几个垃圾桶，一开始物业怀疑是别的街区的人也过来丢垃圾所以导致经常满，于是大张旗鼓地重新建了几个带锁的垃圾箱，每个人丢垃圾要先开锁。后来发现垃圾箱还是经常满，但是满了也不能不丢，于是大家就把垃圾放在垃圾桶外面，等垃圾车来了自己拿进去。但既然你可以放在垃圾桶外面，这个锁本身也就没有了存在的必要。这个和宽带是一个道理，出了问题只想着维护秩序，不去想着造个更大的垃圾桶，最后就是这个下场。&lt;/p&gt;
&lt;p&gt;德国人非常喜欢用条条框框来规范化整个社会，并且已经到了病态沉迷的地步了。这个游戏最让人上瘾的一点在于，当一个社会出现矛盾的时候，依靠着它熟练的制定规则经验，可以迅速把这个矛盾化解到每个人身上，由此一来再大的矛盾也被消解完了。在两德合并时候，东西德的贫富差距矛盾简直是大的不能再大了，在《再见列宁》里有深刻描写，但西德先是强制规定东德马克2:1兑换成西德马克，后来又强制对西德人民实行征收 5.5% 的“团结税”一直延续到今天。两德合并的矛盾都能被这么消解，还有什么不能困难不能消解的呢。&lt;/p&gt;
&lt;p&gt;哈夫纳在其《一个德国人的故事》里描述了希特勒上台前和上台时，普通德国人的感受。我从其描述里所感受到的德国和如此并无差异。德国人不认同希特勒，不认同各种主义，他们所认同的是其维系文明运转的整套体系。但当希特勒通过合法合规的方式上台，其方式之正当让德国人哑口无言。即便是到了纳粹开始颠覆法院的时候，德国人还在一个劲思考、讨论，甚至文学家开始书写起了乡村治愈文学，以求给困境中的德国人一点心理安慰。这种做法在我看来，和在21世纪通过限速宽带来实现公平别无差别。&lt;/p&gt;
&lt;p&gt;但同时不可否认的是，相比于其它国家的制度，民主专制也不失为一个非常好的维系社会运转的机制。甚至可能这就是文明的定义。因为正是这种机制，导致德国成为了如今世界上最发达也最文明的国家之一。但当身处于其中的时候，我又不禁怀疑，我们所真正想要的到底什么？公平是否能够以消灭个人利益为手段而存在 ? 民主是否能够让我们社会运转地更好 ? 一个依赖集体利益权衡出来的社会形态是否真的是我们作为个体所想到达到的那个 ? 个人价值是被民主所发挥了还是被民主所专制了 ? 这些问题我都没有答案。我所能够确定的，仅仅就是我们大部分人都希望活得久，活得好。而以此为目的来倒推以上问题的话，不得不承认，的确是存在许多千差万别的实现途径的 ，如此的话，那么很多名词的褒贬色彩可能的确需要我们去重新考量一下了。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 维也纳</title><link>https://blog.joway.io/posts/vienna/</link><pubDate>Sun, 04 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/vienna/</guid><category>Travel</category><description>刚到维也纳车站的时候，有种回到了德国的错觉。维也纳是继柏林之后的第二大德语区，同时在一些标识牌等市政设施上也和德国相近。奥地利的人均GDP水平甚至还要比德国高。
维也纳给我最大的感受就是干净和自动化。
维也纳市政厅门口广场底下那个公共卫生间可能是我有生以来见过的最最干净的免费卫生间，中间居然还有一个环卫工人办公室。我在那个卫生间有足足驻足欣赏了10分钟之久，一方面是想观察它到底有多干净，另一方面是想弄明白究竟是怎么一种操作能够使得一个闹市区的免费卫生间能够达到如此的干净程度。</description><content:encoded>&lt;p&gt;刚到维也纳车站的时候，有种回到了德国的错觉。维也纳是继柏林之后的第二大德语区，同时在一些标识牌等市政设施上也和德国相近。奥地利的人均GDP水平甚至还要比德国高。&lt;/p&gt;
&lt;p&gt;维也纳给我最大的感受就是干净和自动化。&lt;/p&gt;
&lt;p&gt;维也纳市政厅门口广场底下那个公共卫生间可能是我有生以来见过的最最干净的免费卫生间，中间居然还有一个环卫工人办公室。我在那个卫生间有足足驻足欣赏了10分钟之久，一方面是想观察它到底有多干净，另一方面是想弄明白究竟是怎么一种操作能够使得一个闹市区的免费卫生间能够达到如此的干净程度。&lt;/p&gt;
&lt;p&gt;维也纳的自动化程度也是在欧洲国家里极其罕见的。印象最深的是，我在从进入机场到上飞机，基本上没有秏费任何一个劳动力的协助。很多机场再智能最后还是要一个乘务员来检票和检查护照，但维也纳机场是自己扫码上机，并且不需要检查护照。去超市买东西，也都是完全自主扫码结单，大部分工作人员都在忙着上架商品，只有一个收银台是在人工运营。我在维也纳住的一个青年旅舍，在到之前它邮箱发给了我一个Code，我到了以后可以用那个Code开旅馆大门、我房间门和我床位的锁，第一次开就代表了Check In完成。去客厅买食物也完全是售货机。在旅馆的两天，除了清洁工以外，我没有见到过一个工作人员，这种体验真的是太完美了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520108053_hu_2b7493d9fb0c73be.webp" srcset="https://blog.joway.io/images/old-blog/1520108053_hu_b460aec6bd8ce5b8.webp 800w, https://blog.joway.io/images/old-blog/1520108053_hu_7dd0121e69eb5c3c.webp 1200w, https://blog.joway.io/images/old-blog/1520108053_hu_2b7493d9fb0c73be.webp 1430w" sizes="(max-width: 800px) 100vw, 800px" width="1430" height="1449" alt="欧游散记 —— 维也纳" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;青旅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作为全球最宜居的城市，维也纳无论是环境还是市政都配得上这个称号。如果全世界让我随便移民的话，可能首选就是维也纳。尤其是如果我还有孩子家庭的话，维也纳几乎可以算是不二之选。整个城市有大量的植被覆盖，公园密度覆盖高，公共交通极其发达，冬天还有各种自造的溜冰场 。较之伦敦巴黎纽约又没有那么多的游客，而且奥地利这个国家本身在国际社会上也非常低调，几乎可以算是岁月静好的现实定义了。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520108392_hu_3bc3d953e6596dd3.webp" srcset="https://blog.joway.io/images/old-blog/1520108392_hu_95e2c5314bd42fd.webp 800w, https://blog.joway.io/images/old-blog/1520108392_hu_9bd4728617ca6738.webp 1200w, https://blog.joway.io/images/old-blog/1520108392_hu_3bc3d953e6596dd3.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="846" alt="欧游散记 —— 维也纳" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;市中心溜冰场&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520109185_hu_dd8c13b05c19481d.webp" srcset="https://blog.joway.io/images/old-blog/1520109185_hu_8afe9c4a384e72c3.webp 800w, https://blog.joway.io/images/old-blog/1520109185_hu_6e720dfb74b1764e.webp 1200w, https://blog.joway.io/images/old-blog/1520109185_hu_dd8c13b05c19481d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="786" alt="欧游散记 —— 维也纳" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;公园&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520109267_hu_b8047b5799f42531.webp" srcset="https://blog.joway.io/images/old-blog/1520109267_hu_a4467cf5c8cbb70.webp 800w, https://blog.joway.io/images/old-blog/1520109267_hu_fcf718e81ba43037.webp 1200w, https://blog.joway.io/images/old-blog/1520109267_hu_b8047b5799f42531.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="645" alt="欧游散记 —— 维也纳" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;植被覆盖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;维也纳还是国际间谍之都，据称有7000多家间谍机构注册在案。碟中碟很多场景就在维也纳拍摄。一方面当然是由于奥地利是永久中立国，另一方面可能也是因为它非常适合海外间谍举家居住吧。&lt;/p&gt;
&lt;p&gt;在哈布斯堡王朝的时候，维也纳也曾是整个欧洲的文化政治中心，即便是当年希特勒的年代，他也还是要前往维也纳来考取艺术学院。二战的时候，德国吞并了维也纳，导致维也纳成为了纳粹德国的一部分。二战结束，维也纳陷入被苏联和西方战胜国共同占领的命运，知道1955年奥地利才完全独立。二战后，维也纳人口大量减少，反而成为了一个移民城市。之后巴黎逐渐取代了她的地位，成为了如今的欧洲时尚之都。维也纳曾经的辉煌虽然不再，但这座城市本身还依旧保留着浓烈的贵妇人气质。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 西班牙</title><link>https://blog.joway.io/posts/spain/</link><pubDate>Sat, 03 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/spain/</guid><category>Travel</category><description>西班牙是我最喜欢的一个欧洲国家之一，坐落在伊比利亚半岛，位处欧洲与非洲的交界，最南处离非洲仅15公里，西接大西洋东接地中海。如此的地理位置决定了西班牙从史前时期就是一个多民族多文化交融的地方，即便今天还能看到这种交融的痕迹。
公园前1世纪，罗马人驱逐了伊比利亚半岛上的凯尔特伊比利亚人，并划分为近西班牙省、远西班牙省。在西班牙建立了完善的市镇体制和基督教信仰。西班牙虽然当时只是罗马的两个行省，却诞生过三任罗马皇帝，其中最著名的有图拉真和哈德良皇帝。如今在塞哥维亚还能看到保存良好的古罗马水道桥。</description><content:encoded>&lt;p&gt;西班牙是我最喜欢的一个欧洲国家之一，坐落在伊比利亚半岛，位处欧洲与非洲的交界，最南处离非洲仅15公里，西接大西洋东接地中海。如此的地理位置决定了西班牙从史前时期就是一个多民族多文化交融的地方，即便今天还能看到这种交融的痕迹。&lt;/p&gt;
&lt;p&gt;公园前1世纪，罗马人驱逐了伊比利亚半岛上的凯尔特伊比利亚人，并划分为近西班牙省、远西班牙省。在西班牙建立了完善的市镇体制和基督教信仰。西班牙虽然当时只是罗马的两个行省，却诞生过三任罗马皇帝，其中最著名的有图拉真和哈德良皇帝。如今在塞哥维亚还能看到保存良好的古罗马水道桥。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520026320_hu_498c4d59172757d6.webp" srcset="https://blog.joway.io/images/old-blog/1520026320_hu_122e87122bc22a31.webp 800w, https://blog.joway.io/images/old-blog/1520026320_hu_e9cdc18c29827b35.webp 1200w, https://blog.joway.io/images/old-blog/1520026320_hu_498c4d59172757d6.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1140" alt="古罗马水道桥" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;塞哥维亚古罗马水道桥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;公元5世纪，罗马帝国衰落，西哥特人控制了伊比利亚的大部分土地，建立西哥特王国，并定都托莱多。西哥特国王之后也改信了罗马天主教。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520026996_hu_a079442dca7237a9.webp" srcset="https://blog.joway.io/images/old-blog/1520026996_hu_dac9cd3cfd33ac66.webp 800w, https://blog.joway.io/images/old-blog/1520026996_hu_934dea8a71f62630.webp 1200w, https://blog.joway.io/images/old-blog/1520026996_hu_a079442dca7237a9.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="789" alt="托莱多" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;托莱多古城&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;公元8世纪，北非阿拉伯人(摩尔人)入侵西班牙，开始了为期八百年的伊斯兰统治。摩尔人统治时期，西班牙的建筑、文化、经济都取得了辉煌的成就。&lt;/p&gt;
&lt;p&gt;在摩尔人统治时期，基督教和伊斯兰教互相斗争，在伊比利亚半岛中部形成了大大小小诸多王国。摩尔人统治的南方地区如今统称为安达卢西亚 (即今天的格拉纳达、塞维利亚、加的斯、科尔多瓦等省)，北方的基督教王国里最大的两支是卡斯蒂利亚和阿拉贡，在不同时期还前前后后有诸多王国涌现。西班牙这种各立山头的历史背景也导致了直到今天，他的各个省市都会根据自己的历史背景设计徽章，很多就是从古时候的王国国徽里演变过来的，有非常强的地区文化认同感。&lt;/p&gt;
&lt;p&gt;伊斯兰教长达800年的统治使得如今的西班牙保留了非常浓烈的阿拉伯风格，在安达卢西亚的大街小巷都能够看到非常细碎且严格对称的花纹，一些庭院里都会有非常精巧的水道设计，很具有沙漠民族色彩。即便是在基督教教堂，由于很多教堂当时都是在原先摩尔人遗留下来的清真寺上建的，所以也保存了相当多的伊斯兰元素，尤其是在庭院设计上。&lt;/p&gt;
&lt;p&gt;在伊斯兰统治时期，穆斯林对异教十分宽容，使得西班牙同时居住着大量犹太人。今天我们去一些老城区还会看到很多精致幽静的社区，都是当年犹太人遗留下来的痕迹。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520027310_hu_aaf0eda97dcc248a.webp" srcset="https://blog.joway.io/images/old-blog/1520027310_hu_819d5f26343802c4.webp 800w, https://blog.joway.io/images/old-blog/1520027310_hu_a0b04560b3d476ed.webp 1200w, https://blog.joway.io/images/old-blog/1520027310_hu_aaf0eda97dcc248a.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="394" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/old-blog/1520027376_hu_13f9d3cc12b3914.webp" srcset="https://blog.joway.io/images/old-blog/1520027376_hu_47687a3fb20bffaa.webp 800w, https://blog.joway.io/images/old-blog/1520027376_hu_e9d4e20bcf6567e0.webp 1200w, https://blog.joway.io/images/old-blog/1520027376_hu_13f9d3cc12b3914.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1093" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/old-blog/1520027245_hu_eee41c0118c0872d.webp" srcset="https://blog.joway.io/images/old-blog/1520027245_hu_4c4c725bd50dadc9.webp 800w, https://blog.joway.io/images/old-blog/1520027245_hu_cb68e928e7cd863f.webp 1200w, https://blog.joway.io/images/old-blog/1520027245_hu_eee41c0118c0872d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="868" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;格拉纳达的阿尔罕布拉宫&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520027475_hu_2f8342cce49d6497.webp" srcset="https://blog.joway.io/images/old-blog/1520027475_hu_627bed49102b73f0.webp 800w, https://blog.joway.io/images/old-blog/1520027475_hu_d51426a574a4aae.webp 1200w, https://blog.joway.io/images/old-blog/1520027475_hu_2f8342cce49d6497.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="732" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;格拉纳达的摩尔人庭院&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520027588_hu_c19099d4e7a237ae.webp" srcset="https://blog.joway.io/images/old-blog/1520027588_hu_a596a63c5c422cb8.webp 800w, https://blog.joway.io/images/old-blog/1520027588_hu_52d0ef74938df157.webp 1200w, https://blog.joway.io/images/old-blog/1520027588_hu_c19099d4e7a237ae.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="969" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;
&lt;img src="https://blog.joway.io/images/old-blog/1520027800_hu_b9b024b913acdaf3.webp" srcset="https://blog.joway.io/images/old-blog/1520027800_hu_d5a03233a4c75e83.webp 800w, https://blog.joway.io/images/old-blog/1520027800_hu_6c43607182c60dec.webp 1200w, https://blog.joway.io/images/old-blog/1520027800_hu_b9b024b913acdaf3.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1176" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;塞维利亚的摩尔人王宫&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;公元15世纪，卡斯蒂利亚公主伊莎贝尔一世和阿拉贡王子斐迪南二世联姻，被称为“天主教双王”。联姻使两人得以共同统治绝大部分西班牙领土，并开始了基督教的复兴。此时的哥伦布在经过十几年的游说后，终于在1492年与伊莎贝尔和斐迪南的王室达成协议，王室同意资助他的旅行并允许他将从新土地的总收入中提成10% 。哥伦布的遗骨最后葬在了塞维利亚的主教堂里。有趣的是，哥伦布当年只是逝世在一个普通的旅馆里，而教堂里所展现的却是一幅国葬场面。&lt;/p&gt;
&lt;p&gt;基督教重新掌权后，放弃了穆斯林之前的开明宗教策略，不仅改建或摧毁了清真寺，还开始了对犹太人大规模的驱逐和屠杀，导致西班牙犹太人开始了新的流亡生涯。今天北美就有很多西班牙犹太人后裔。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520028281_hu_23c2998dceefc58d.webp" srcset="https://blog.joway.io/images/old-blog/1520028281_hu_430b7ae59a9126ff.webp 800w, https://blog.joway.io/images/old-blog/1520028281_hu_3bf05a49ffbc03a7.webp 1200w, https://blog.joway.io/images/old-blog/1520028281_hu_23c2998dceefc58d.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="198" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;塞维利亚全景&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520028209_hu_5af9086bf72e1b9c.webp" srcset="https://blog.joway.io/images/old-blog/1520028209_hu_78414a8c8328dbc3.webp 800w, https://blog.joway.io/images/old-blog/1520028209_hu_29c2dbaaee5317d3.webp 1200w, https://blog.joway.io/images/old-blog/1520028209_hu_5af9086bf72e1b9c.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="817" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;塞维利亚主教堂&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520027989_hu_d800a6291d577530.webp" srcset="https://blog.joway.io/images/old-blog/1520027989_hu_92ee414e8034b924.webp 800w, https://blog.joway.io/images/old-blog/1520027989_hu_c7a55adad3ad4ace.webp 1200w, https://blog.joway.io/images/old-blog/1520027989_hu_d800a6291d577530.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1079" alt="欧游散记 —— 西班牙" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;塞维利亚主教堂里的哥伦布遗骨&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;公元16世纪和17世纪，西班牙开始了殖民旅途，成为了第一批堪称“日不落”的国家，同时也让西班牙语成为了今天世界上第三大语种。在这期间的大部分时间里，西班牙都是整个欧洲最强盛的国家。但西班牙的王室并没有把大把掠夺来的财富用于本国发展，而是挥霍在各种物质生活和发展军备上，导致其经济实力远远落后于其军事实力。随着北方法国的崛起，西班牙日渐式微。在18世纪初始爆发了著名的“西班牙王位继承战争”。最终西班牙落入了波旁王朝手中，一直延续到了今天。&lt;/p&gt;
&lt;p&gt;今天的西班牙从经济上早已落魄，失业率高达17%。德国是3% , 再不济的意大利也只有11% 。但西班牙的旅游业实力在全球是排名第一的。一方面由于其物价水平较之大部分欧洲国家都算是非常低的，我记得同一个薯片在德国要2欧，在奥地利要2.5欧，在西班牙只要1.2欧。另一方面也归功于其独特复杂的历史文化背景，使得其对全世界人民都能产生强烈的异国风情体验，这也是为什么很多游戏喜欢以西班牙作为题材背景的原因。&lt;/p&gt;
&lt;p&gt;从治安上讲也远远好于西边的葡萄牙和北边的法国，并且人种相对单纯很多，没有太多种族问题。&lt;/p&gt;
&lt;p&gt;在意识形态上，西班牙非常的“中国”，完全不理睬北方欧洲佬的那一套动物保护主义环境保护主义种族平等主义的价值观，基本上是怎么舒服怎么来。马德里火车站直接把上百只乌龟养在了一个池子里，看着十分壮观，但在北欧显然肯定会被动保的人喷死。街头的垃圾桶基本上也没什么分类要求，都是随便丢，我好像也只在巴塞罗那的居民区看到过垃圾分类的标识。在西班牙基本上看不到多少黑人，人种非常纯粹，不会有那么多移民问题难民问题。西班牙人总体给人的印象就是一个非常实用主义和放松的民族，这点和中国非常相似。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 德国国会大厦</title><link>https://blog.joway.io/posts/deutschland-reichstag/</link><pubDate>Fri, 02 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deutschland-reichstag/</guid><category>Travel</category><description>德国国会大厦 ( Reichstag ) 是柏林的标志性建筑，在1894至1933年间先后用作德意志帝国议会和魏玛共和国议会。1933年发生了著名的“国会纵火案”，希特勒借此大力渲染以促成兴登堡总统签署《国会纵火法令》，废除了魏玛共和国《宪法》里的诸多公民的权力和自由，后来亦成为监禁反对人士和镇压不与纳粹政权合作的报刊的法律依据。之后国会大厦一直处于废弃状态，直到1990年，两德统一，西德决定将首都从波恩迁回到柏林，并确定国会大厦为议会地址。之后开始了对国会大厦的改建工作。</description><content:encoded>&lt;p&gt;德国国会大厦 ( Reichstag ) 是柏林的标志性建筑，在1894至1933年间先后用作德意志帝国议会和魏玛共和国议会。1933年发生了著名的“国会纵火案”，希特勒借此大力渲染以促成兴登堡总统签署《国会纵火法令》，废除了魏玛共和国《宪法》里的诸多公民的权力和自由，后来亦成为监禁反对人士和镇压不与纳粹政权合作的报刊的法律依据。之后国会大厦一直处于废弃状态，直到1990年，两德统一，西德决定将首都从波恩迁回到柏林，并确定国会大厦为议会地址。之后开始了对国会大厦的改建工作。&lt;/p&gt;
&lt;p&gt;国会大厦对公众全面开放，可以在网上预约申请，参观分为穹顶和内部导览，内部导览会有一个议会工作人员来为你介绍里面各个部分的历史和功用，也会以一个德国公民的角度给你吐槽现任各个领袖的特点、习惯甚至是工资待遇。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016030_hu_c1ff3d99c1d941e3.webp" srcset="https://blog.joway.io/images/old-blog/1520016030_hu_b6381b4d08a7bcf2.webp 800w, https://blog.joway.io/images/old-blog/1520016030_hu_ae0358d9d6676760.webp 1200w, https://blog.joway.io/images/old-blog/1520016030_hu_c1ff3d99c1d941e3.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1135" alt="讲解员" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;进门的大厅中央是议会大厅，顶部悬挂着一只巨大的“联邦之鹰”，这个鹰比其国会和机关文件上的鹰都要丰满很多，所以也被称之为“胖母鸡 ( Fette Henne )”。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520015869_hu_5c6411ed48b1eef8.webp" srcset="https://blog.joway.io/images/old-blog/1520015869_hu_26966dedc9c3fab3.webp 800w, https://blog.joway.io/images/old-blog/1520015869_hu_f74f7a701c8f6f38.webp 1200w, https://blog.joway.io/images/old-blog/1520015869_hu_5c6411ed48b1eef8.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="921" alt="议会大厅" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;议会大厅的顶部是著名的玻璃穹顶，原先旧有的玻璃穹顶在1945年被空袭炸毁，直到1991年改建的时候才又重新加上。议会大厅从周围墙壁到穹顶大部分面积都是采用玻璃材料，除了确保室内自然采光意外，也凸显了其政治透明。当议会正在开会时，虽然内部参观会取消，但公众依旧可以登上穹顶观看自己所在选区议员的一举一动。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016255_hu_e639a7ee15a4b336.webp" srcset="https://blog.joway.io/images/old-blog/1520016255_hu_7ef0ff8b0655d566.webp 800w, https://blog.joway.io/images/old-blog/1520016255_hu_ded581021525729e.webp 1200w, https://blog.joway.io/images/old-blog/1520016255_hu_e639a7ee15a4b336.webp 1597w" sizes="(max-width: 800px) 100vw, 800px" width="1597" height="1297" alt="穹顶仰视" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016167_hu_f421bf2099cf4d01.webp" srcset="https://blog.joway.io/images/old-blog/1520016167_hu_5cb9489d431af3b2.webp 800w, https://blog.joway.io/images/old-blog/1520016167_hu_dc3a8bdbe0a0c915.webp 1200w, https://blog.joway.io/images/old-blog/1520016167_hu_f421bf2099cf4d01.webp 1497w" sizes="(max-width: 800px) 100vw, 800px" width="1497" height="1384" alt="穹顶内视" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;在边上的走廊里，有一面墙，墙上涂满了当年攻克国会大厦的苏联士兵的涂鸦，有人刻下了自己的名字，有人刻下了久别的女友，有人刻下了自己的家乡，当然也有各种污言秽语。在翻修国会大厦的时候德国人还争论过是否要把这些涂鸦抹掉，毕竟把这些羞辱性的话语留在墙上有失一个议会的尊严，但最后还是决定保留了下来。试想天天路过这些涂鸦的议员们，怎么可能还会去表决发动一场战争。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016357_hu_83f48b9a71b99180.webp" srcset="https://blog.joway.io/images/old-blog/1520016357_hu_22d878e105a01030.webp 800w, https://blog.joway.io/images/old-blog/1520016357_hu_62ab880cd3f18067.webp 1200w, https://blog.joway.io/images/old-blog/1520016357_hu_83f48b9a71b99180.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="993" alt="苏联涂鸦" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016404_hu_a0e2f7bb13f778e2.webp" srcset="https://blog.joway.io/images/old-blog/1520016404_hu_66d9d95004ba926a.webp 800w, https://blog.joway.io/images/old-blog/1520016404_hu_2c59935e5bba18f5.webp 1200w, https://blog.joway.io/images/old-blog/1520016404_hu_a0e2f7bb13f778e2.webp 1445w" sizes="(max-width: 800px) 100vw, 800px" width="1445" height="1434" alt="苏联涂鸦" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;再往里走是一个供议员们使用的祷告室，侧角还有一个供穆斯林使用的朝向麦加的小空地。这个房间里陈放的艺术品用于帮助议员在沉静中冥想，以做出对自己对选民对信仰负责的决定。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016458_hu_77f49cf9bc6ecf39.webp" srcset="https://blog.joway.io/images/old-blog/1520016458_hu_1a9410df5e32f276.webp 800w, https://blog.joway.io/images/old-blog/1520016458_hu_fe4f8bae720c81b3.webp 1200w, https://blog.joway.io/images/old-blog/1520016458_hu_77f49cf9bc6ecf39.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="848" alt="祷告室" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;国会大厦四周是柏林政治中心带，有默克尔办公的德国总理府，草拟议会文件的议会大楼，监督政府和议员的各选区地方报纸驻柏林的记者大楼，还有一些联邦政府机构办公区域。身处其中能够感受到这个国家的政治正如一台精密的机器一般运作着。&lt;/p&gt;
&lt;p&gt;国会大厦只是这台机器的外壳，德国人在选举制度上的设计也非常的精密。德国是代议制民主，由议员选举出总理。这个一定程度避免了像美国大选时候那样靠比拼广告费和心理学钻营去赢得普通人民的关注。其次，人民拥有两张选票，一张投给该选区议员，一张投给政党。议会席次里，一半是直选议员，一半是拿到了政党投票的政党自己选拔出的议员。打个比方就是，我认可这个保守党议员但我不认同他所在的党派价值观，所以我可以既尊重我的个人喜好也同时保留我的意识形态观念，对于议员也更加可以在党派立场上有更多的个人色彩 。德国还允许没有达到绝对多数的政党组成联合政府，更能体现这种民主的妥协性而不是一锤子买卖。在德国的选举法上还有很多细节也非常耐人寻味，其归根结底的目的，都是在最大限度地让民意能够100%传达到议会格局上 。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1520016529_hu_82a1f742437b2075.webp" srcset="https://blog.joway.io/images/old-blog/1520016529_hu_6b18cfe5d446bbbc.webp 800w, https://blog.joway.io/images/old-blog/1520016529_hu_82a1f742437b2075.webp 1090w" sizes="(max-width: 800px) 100vw, 800px" width="1090" height="1902" alt="德国选票" loading="lazy" decoding="async"&gt;&lt;/p&gt;</content:encoded></item><item><title>社会矛盾讨论统一框架的一种可能性</title><link>https://blog.joway.io/posts/right-theory/</link><pubDate>Mon, 12 Feb 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/right-theory/</guid><category>Thought</category><description>这篇文章只是在飞机上做的一个思维游戏，标题取的太大，没有什么严谨性，不一定代表本人立场，看看就好。
如今在社交媒体上去讨论 “女权主义”、“种族主义”、“宗教自由” 等问题都是一件非常危险的事情。每一个话题里都会有水火不容的两派观点，即便是在同一个立场上，还会存在基于不同逻辑演化出的不同分支观点 。社交媒体上普遍存在的一类人就是在自己国家政治上反对独裁，对西方国家民主又报以鄙夷，向往信仰自由的同时又张口闭口以“绿绿”来蔑称穆斯林。这很大程度是由于缺乏一个值得推敲的框架来思考的结果。</description><content:encoded>&lt;p&gt;这篇文章只是在飞机上做的一个思维游戏，标题取的太大，没有什么严谨性，不一定代表本人立场，看看就好。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;如今在社交媒体上去讨论 “女权主义”、“种族主义”、“宗教自由” 等问题都是一件非常危险的事情。每一个话题里都会有水火不容的两派观点，即便是在同一个立场上，还会存在基于不同逻辑演化出的不同分支观点 。社交媒体上普遍存在的一类人就是在自己国家政治上反对独裁，对西方国家民主又报以鄙夷，向往信仰自由的同时又张口闭口以“绿绿”来蔑称穆斯林。这很大程度是由于缺乏一个值得推敲的框架来思考的结果。&lt;/p&gt;
&lt;p&gt;女权主义，人人平等，种族主义，但在这些繁复的名词下，我认为是存在一个普适的框架来规范彼此的讨论的。&lt;/p&gt;
&lt;p&gt;小说家在构造人物的时候，通常会赋予他性别，长相，身高，种族等外在因素，还会用一系列事件去刻画他的为人和三观。人就是在诸多附于其上的属性的共同合力之下才之所以为其人 。武则天女性的身份让她成为了中国历史上的一个独特的存在，奥巴马的黑人身份也帮他赢得了不少的认同。但这些仅仅只是他们身上存在的一个属性，在特殊的历史时期的确不同的属性的权重会略微大一些。&lt;/p&gt;
&lt;p&gt;如果你把性别，种族，宗教都当作是一种互相平等的附加属性，很可能就能够讨论清楚社交媒体上的诸多话题了。我尝试性的建立了两个“定理”，试图用这两个定理来理清常见的社交媒体争论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;一个人应当由多方位的属性来定义其人。&lt;/li&gt;
&lt;li&gt;各个属性之间只有因外界影响而导致的权重占比不同，但属性本身之间没有高低之分。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;第一条定理我相信所有受过教育的人都会承认。第二条定理我这里解释下，比如是否色盲和是否是黑人是两个属性，如果我是一个UI设计师，显然是否色盲比是否是黑人对我的职业和生活影响都大的多，但并不意味着色盲这件事情本身就比种族因素要重要。当我去从事作家工作的时候，这两个因素的权重又都会降的很低，相反文字能力这个属性的权重就升高了。所以我说属性的权重是会动态变化的，而属性本身是没有高低贵贱之分的。&lt;/p&gt;
&lt;p&gt;如果你不同意上述两个定理，你可以辩驳定理本身。如果你认同定理但不认同下述推导，你可以基于定理提出你的推导过程。但请不要直接拿你的那套理论来直接辩驳下面的推导结果，那样会显得你很没有科学素养。&lt;/p&gt;
&lt;p&gt;我们先以职场领域来做一个推导。马云说 : “ 女人爱商比男人高，所以阿里巴巴更喜欢招女性。” 这句话的错误在于，用性别和爱商是两个或许具有统计学上的相关性的属性，但它们之间并没有因果性。但如果阿里首先通过性别来筛选简历，不招男性，然后在女性里进行爱商维度的面试。这样我觉得是合理的。阿里根据自己的经验去认为女性比男性爱商高，然后在自己的企业里进行这种效率筛选完全合法合规。就算他遗漏了那些爱商高的男性，但他提升了效率。如果在这个问题上去宣扬歧视男性的话，同样可以在这个问题上去宣扬歧视爱商低的人，因为根据第二条定理，各个属性是没有高低之分的。如果你是一个爱商高并且就铁了心一定要进阿里的男性，你能够做的不是去宣扬阿里歧视男性 —— 因为阿里并没有歧视，它就是想要提高效率 —— 而是去向阿里展示你是那个违反他们经验的例外，或者直接证明他们的经验是错的。&lt;/p&gt;
&lt;p&gt;在教育领域，美国的平权法案为黑人群体提供了优于其它族裔的机会，中国有少数民族加分政策，在这两个案例上，我们用上面两个定理来看，假设大学看重的是一个人的“知识储备”、“学习能力”、“种族”等多维度属性。如果这个大学就是有一个愿望是提升校园里的种族多样性，以提升学生对其它种族的了解程度和接受程度，从而循序渐进促使整个社会更加能够容纳其它种族，那么的确种族会在大学考量学生的时候占据很高的权重。而大学也有权力依据自己的需求来选择自己的学生。这个和弥补历史上翻下的错误没有关系，完全是实用主义的角度来考量。&lt;/p&gt;
&lt;p&gt;仔细观察上面的例子，显然会发现它其实忽略了公平原则 。但是公平原则是一个人为概念，且这个概念自己就不能逻辑自洽。例如显然我们需要在选举的时候筛选掉蠢的那批人，而这种筛选就是对智力低下者的不公平。如果你要来辩驳这个说法，你要么承认蠢的人也有资格当选leader，要么承认并不是所有的属性都应当具备平等的重要性。当你说出后者的时候，你本身就是站在一个不公平的视角下在谈论公平。&lt;/p&gt;
&lt;p&gt;公平从来都是一个伪概念。在古代的贵族社会里，人人都认可特权的存在，公平的争议只会暴发在贵族与贵族之间，平民与平民之间，奴隶与奴隶之间。有兴趣的人可以去观摩下汉谟拉比法典，与其说是法典不如是一个时代对公平的定义手册。即便是现代社会，诸多社会关系也可以映射到汉谟拉比法典的框架中去，只不过相比古代，现代的平民数量大幅提升，平民内部的阶级也越来越多，越来越细。富人，官人，高智商者，艺术家，技术从业者，教师等标签的背后，都是资本、权力、智力、创造力、消费能力、教育资源分配的不均匀，教师赚钱难，程序员小孩上学难，艺术家独立难等等都是现代不公平生活的写照。公平是一个伟大的共产主义理想，但也通过实践证明了并不是一个社会良好运转的好机制。如果你能够承认不公平是一个合理的正常态，也就能够认同上面定理所推导出来的世界。&lt;/p&gt;
&lt;p&gt;我前面说了，这两个定理是试图为社交媒体上的意识形态交流建立一个可沟通的框架，而不至于陷入混战。同时也能够将各方的利益诉求化解到同一个目标上，让斗争更为合理。&lt;/p&gt;
&lt;p&gt;比如在女权主义的话题下总会有人提到机会平等。那么如果对性别机会平等了的话，不同学历者是不是也要机会平等，不同能力者是不是也要机会平等 ？有些人说性别的差异不是能够选择的，能力则不同。说这话的人大概是忽视了所处国家，家庭背景，个人遭遇，智力这些东西也同样不是能够选择的这一事实了。机会平等是你依据自己的私欲想要的一个结果，而非你所遭遇到的问题本身，也不是一个解决问题的办法 。你所遭遇到的问题本身是社会上存在这种基于性别的职业偏见，而这个偏见在男性女性下都存在 。如果你的宣传点都局限于争取女性的权力，从理论角度来讲就是错的，从实用角度来讲，我一个直男每天996的工作，好不容易上个网为什么要为你去发声呢 ？但如果你宣扬的是消灭基于性别的职业偏见，那你的努力就是在减少性别在一些领域的占比，让大家用更为科学的方式来筛选人，而不是追求以公平的方式。而选择以这种方式斗争的话，能够争取到的支持者显然更为广阔。我所处的计算机行业就是这种科学斗争的典型，以前大家很流行去考个硕士博士，后来许多本科生，专科生通过自己的能力证明了自己从而降低了学历在面试时候的权重，导致现在大部分互联网公司都不会去卡学历了。这种做法是否正确可能有待商榷，但觉得不爽的大可通过自己的实力再把这个行业给扭转过来，这个就是一个互相博弈，科学进步的过程。博弈的目标是去改变权重占比，而不是去追求公平。&lt;/p&gt;
&lt;p&gt;比如在地域歧视上总是有人利用道德压力去阻止人开地域歧视的玩笑，而不是去改变这个地域本身给人的印象。如果从公安数据上统计出来有相当比例的偷盗都是X省的人干的，那显然偷盗就是X省的特色，哪个省份没点黑料，有什么可以辩解的呢 ？如果你要反对，那么对象应该是以地域来判断一个人的权重，这种斗争方式总比道德绑架要文明的多了。&lt;/p&gt;
&lt;p&gt;按照上面这种方法去思考的话，许多垂直领域里的矛盾最终本质都能够被归结到一个普遍存在的社会矛盾上，斗争方式也更加成熟。即便是在独裁国家也有大量通过舆论和媒体改变社会偏见的案例，而这些案例的普遍特征是斗争目标极为具体和单一。&lt;/p&gt;</content:encoded></item><item><title>欧游散记 —— 伪君子布拉格</title><link>https://blog.joway.io/posts/euro-prague/</link><pubDate>Sun, 11 Feb 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/euro-prague/</guid><category>Travel</category><description>小时候很喜欢米兰昆德拉和卡夫卡，布拉格一直是一个我心中非常神往的城市。但在踏上布拉格土地的那一刻，这个城市带给我的只有无穷无尽的失望和可笑。
在历史上，布拉格早期是波西米亚的首都，中世纪也曾两度成为神圣罗马帝国的首都，即便如此，这个城市里也并没有留下什么让人眼前一亮的东西。虽遍及了哥特式、巴洛克式的建筑，但据有代表性的并没有多少。其自称文艺之都，但鲜能找到具有真正文化含量的东西。作为如今捷克共和国的首都，一点也没有首都应有的样子。而同为文艺之都的巴黎和维也纳就和布拉格截然不同，他们都保持了首都应有的威仪和典雅，并不故意去迎合游客，游览这些城市就像观看一本小说，需要自己去细细体会。而布拉格就像是一个全身按摩，用非常故意和夸张的力道来让你全身心感受到“文艺”的的薰陶。</description><content:encoded>&lt;p&gt;小时候很喜欢米兰昆德拉和卡夫卡，布拉格一直是一个我心中非常神往的城市。但在踏上布拉格土地的那一刻，这个城市带给我的只有无穷无尽的失望和可笑。&lt;/p&gt;
&lt;p&gt;在历史上，布拉格早期是波西米亚的首都，中世纪也曾两度成为神圣罗马帝国的首都，即便如此，这个城市里也并没有留下什么让人眼前一亮的东西。虽遍及了哥特式、巴洛克式的建筑，但据有代表性的并没有多少。其自称文艺之都，但鲜能找到具有真正文化含量的东西。作为如今捷克共和国的首都，一点也没有首都应有的样子。而同为文艺之都的巴黎和维也纳就和布拉格截然不同，他们都保持了首都应有的威仪和典雅，并不故意去迎合游客，游览这些城市就像观看一本小说，需要自己去细细体会。而布拉格就像是一个全身按摩，用非常故意和夸张的力道来让你全身心感受到“文艺”的的薰陶。&lt;/p&gt;
&lt;p&gt;布拉格的所谓的“博物馆” 典型就是那种实在没什么景点好开了，就生搬硬凑来充数。比如 “Apple Museum” ，可能是这个世界上最僵硬可笑的博物馆了 ，搞得 Apple 和布拉格有什么莫大关系似的 。还有一个“性器博物馆”，虽然我承认这些博物馆本身有存在价值，但是其展品质量和数量实在难以同其票价相等价。关键是这些博物馆是没有布拉格特色的，而在布拉格，我还真没有找到一个足以展现整个布拉格城市发展史的博物馆，而我认为这是一个古城和首都的必需品。&lt;/p&gt;
&lt;p&gt;最让我无法接受的是在卡夫卡的22号故居里，贪婪到极限的捷克人居然在其房间里铺满了各种用来销售的明信片和纪念品，以至于完全看不出这个狭小故居原来的模样，和普通小店没有区别。布拉格人在卡夫卡活着的时候不能去发现他，在他死后还要用如此竭泽而渔的方式去利用他，用卡夫卡所恶心的方式去营销卡夫卡周边，其流氓程度在欧洲国家里实属罕见。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1518303322_hu_5ffd71894e1c1991.webp" srcset="https://blog.joway.io/images/old-blog/1518303322_hu_d49a83298fe25057.webp 800w, https://blog.joway.io/images/old-blog/1518303322_hu_eee9ddbcb4410ca1.webp 1200w, https://blog.joway.io/images/old-blog/1518303322_hu_5ffd71894e1c1991.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="1200" alt="欧游散记 —— 伪君子布拉格" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;布拉格的人更是不怎么靠谱。约好的2点钟会2点30分人才到，在网上po出了住宿信息电话留的却是别人的。满大街的货币兑换点明明写着0手续费，却在汇率上做手脚坑游客。我住的还是一个四星级的酒店，而check out 的时候工作人员连自己的酒店房间是怎么样的都弄不清楚，明明只给了我钥匙还死命问我要房卡。&lt;/p&gt;
&lt;p&gt;布拉格郊区有一个佩特任瞭望塔，仿照巴黎艾菲尔铁塔建造，但只用了4个月就完工了，高度仅为60米。而艾菲尔铁塔是320米。这座铁塔在我看来充分体现了布拉格人的精神，空有装逼的心却缺乏装逼的能力，最终就做了一个不伦不类的东西潦草结束，纸上谈兵的典型。&lt;/p&gt;
&lt;p&gt;在近现代，布拉格在世界上一直有着不错的曝光率。比如为中国人所熟知的“天鹅绒革命”。坦率讲，就这么一个小城市加上这么一群不靠谱的人，任何据有煽动性的理论都有成功的可能性 。在咖啡馆高谈阔论一番，争取下这100万城市人口的大多数，就足以颠覆掉这个国家的政府了。而以捷克的“天鹅绒革命”为理论依据来试图进口到国内的中国学者，不是蠢就是坏，因为这两者几乎没有什么可比性。&lt;/p&gt;
&lt;p&gt;捷克紧邻德国和奥地利，地处欧洲中心，伏尔塔瓦河贯穿整个国家。从地理位置上看占据了非常大的优势，但这个国家的人均GDP只有奥地利和德国的一半不到。真的是空有一身好皮囊，白白被这些莽夫给糟蹋了。&lt;/p&gt;
&lt;p&gt;值得注意的是，与卡夫卡同样著名的作家米兰昆德拉在布拉格却几乎没有任何官方宣传的痕迹，我想这很可能是因为昆德拉一直坚称自己是一个法国作家而与祖国捷克决裂的原因吧。但如果政府单单就因为这种原因而去掩盖一个世界级作家在这座城市的痕迹，未免有点太过小气。&lt;/p&gt;
&lt;p&gt;总的来说，布拉格人把他们的城市当作了一个迪士尼在运营 ，然而做的却还没有人家迪士尼做的好，这真的是一个可悲的事情。&lt;/p&gt;</content:encoded></item><item><title>Lemon : Koa 风格的 Python 异步 Web 框架</title><link>https://blog.joway.io/posts/lemon-overview/</link><pubDate>Mon, 08 Jan 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/lemon-overview/</guid><category>Tech</category><description>前段时间想要写一些简短高效的 API ，背后的工作无非就是一些计算和数据处理，但是可能并发量会比较高。当我在 Python 的生态里去搜寻一些靠谱的 Web 框架时，很难找到一个设计优秀且运行效率高的框架。尤其是当我已经习惯了 NodeJS 的 Koa 那种简洁明了的设计时，很难再喜欢上像 Flask 那种装饰器的写法和各种概念拢杂在一起的设计。最后实在没有办法，就自己写了个符合我个人审美的框架 Lemon 。</description><content:encoded>&lt;p&gt;前段时间想要写一些简短高效的 API ，背后的工作无非就是一些计算和数据处理，但是可能并发量会比较高。当我在 Python 的生态里去搜寻一些靠谱的 Web 框架时，很难找到一个设计优秀且运行效率高的框架。尤其是当我已经习惯了 NodeJS 的 Koa 那种简洁明了的设计时，很难再喜欢上像 Flask 那种装饰器的写法和各种概念拢杂在一起的设计。最后实在没有办法，就自己写了个符合我个人审美的框架 &lt;a href="https://github.com/joway/lemon"&gt;Lemon&lt;/a&gt; 。&lt;/p&gt;
&lt;h2 id="什么是-web-框架"&gt;什么是 Web 框架&lt;/h2&gt;
&lt;p&gt;在讲 Lemon 的设计前，我们先来看一看一个请求是如何被响应的 ，以及框架在其中的作用是什么 :&lt;/p&gt;
&lt;p&gt;当一个请求从客户端进入服务器，首先以 TCP 包的形式被接收到，这个时候我们需要手动去建立连接，接收到 TCP 报文后我们需要去判断它的协议，然后再解成相应协议(一般都是HTTP协议)的报文，传给 application 去处理，当处理完后还要把结果写回成 TCP 报文传回去，如果有 keep alive 还需要去手动管理这个连接的生命周期。以上这些统称为 server 部分。&lt;/p&gt;
&lt;p&gt;而和开发者关系最密切的 application 部分做的就是拿到一个 http 或其它协议的报文，然后根据其信息做针对性的响应，传给 server 。&lt;/p&gt;
&lt;p&gt;无论你是使用任何语言任何框架，都逃不开上面这个处理过程。而我们在比较框架的时候，无外乎是对以下几个方面做一些针对性的优化:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TCP报文解析成 HTTP(或其它协议) 报文的效率&lt;/li&gt;
&lt;li&gt;并发策略 (多线程，多进程，协程，线程池，Event Loop)&lt;/li&gt;
&lt;li&gt;application 本身的运行效率 (由框架的效率，所用的语言和使用者自身的代码质量共同决定)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于第一点，python有一个叫 &lt;a href="https://github.com/MagicStack/httptools"&gt;httptools&lt;/a&gt; 的库就是使用了 NodeJS 的 http parser 以达到高性能解包的目的。&lt;/p&gt;
&lt;p&gt;针对第二点，有许多OS层面和工程层面的技术在致力于解决这个问题。&lt;a href="https://github.com/MagicStack/uvloop"&gt;uvloop&lt;/a&gt; 就是其中的一种，采用事件循环的方式，底层用的是 libuv , 同样也是 NodeJS 的底层异步IO库 。&lt;/p&gt;
&lt;p&gt;第三点可能是我们大部分人的考虑的重点，需要各自根据团队情况，在开发效率和运行效率中间进行权衡。作为框架本身，它能够做的极致就是不给 application 拖后腿，而现实中，大部分时间其实都是在运行用户自己编写的代码。&lt;/p&gt;
&lt;p&gt;当我们在谈论一个 Web 框架的时候，更多是在谈论第三点 application 的部分。第一二两点是 server 的实现部分。大部分时候，框架自身并不会去实现一个完整的 server 。为了让我们使用各种框架来进行开发的 application 能够在不同 server 中可以运行，会指定一些接口标准，在 Python 里同步的比如 wsgi , 异步的比如 asgi 。&lt;/p&gt;
&lt;p&gt;对于 application 部分，显然所用语言已经限定了，而代码质量又取决于开发者自己的造化，那么框架能够做的，就是帮开发者简化编写业务逻辑的困难，形成一套代码的格式与套路。而许多人关心的框架的并发能力和运行效率，大多时候并不取决于框架自己，而是依赖于 server 的实现 。&lt;/p&gt;
&lt;p&gt;Lemon 想要做的，就是在第一二层利用已有的最新的技术实现其高效率能力，而在 application 层，做一个设计简单，使用舒适的 API 框架。&lt;/p&gt;
&lt;h2 id="lemon-初探"&gt;Lemon 初探&lt;/h2&gt;
&lt;p&gt;Python 3.5 以上已经支持类似 NodeJS 的那种 async 的写法了，加上&lt;a href="https://github.com/MagicStack/uvloop"&gt;uviloop&lt;/a&gt; 这个高性能事件循环库。我们完全可以借助这两个在 Python 里实现一个类 Koa 的异步 Web 框架。&lt;/p&gt;
&lt;p&gt;在 Lemon v0.0.1 版本的时候，我用 &lt;a href="https://github.com/MagicStack/uvloop"&gt;uvloop&lt;/a&gt; 和 &lt;a href="https://github.com/MagicStack/httptools"&gt;httptools&lt;/a&gt; 自己去实现了一个 server 。但是后来发现这件事情要做得好且稳定还是挺复杂的。偶然间发现了 &lt;a href="https://github.com/encode/uvicorn"&gt;uvicorn&lt;/a&gt; 这个项目，代码写的非常优雅，而且也是用的 uvloop 和 httptools , 很适合作为一个 server 来使用。于是后来就又删掉了我自己的 server , 转而使用 uvicorn 。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/channelcat/sanic/"&gt;Sanic&lt;/a&gt; 的架构和上述很像，但是它没有用 uvicorn 而是自己写了一套 server 并且它是基于 Flask 的 API 风格的，项目也写的比较乱。&lt;/p&gt;
&lt;p&gt;在 Lemon 中，一个 application 的设计是这样的 :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;middleware1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nxt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;msg&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;hello&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nxt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;middleware2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ack&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;yeah !&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Lemon&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;middleware1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;middleware2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种设计思路非常简单，就是用 &lt;code&gt;一条函数链&lt;/code&gt; 描述整个请求过程 。&lt;code&gt;ctx&lt;/code&gt; 作为传递各种参数的媒介，每个 middleware 都有能力直接返回结果或者传递给下一个 middleware 。&lt;/p&gt;
&lt;p&gt;那么如果我们要设计路由该怎么做呢 ? 很简单，路由就是最前面的 middleware1 ，由它去拿 ctx.path 然后决定要走哪个函数。&lt;/p&gt;
&lt;p&gt;同样的全局的错误处理或者鉴权处理都可以被抽象到这个 middleware 对象中来。这样在我们的设计里，就不会有那么多人为的概念。&lt;/p&gt;
&lt;p&gt;我们可以看一下Flask框架中示例代码是怎么样的:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;lt;name&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;hello&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;flask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;首先它用一些很魔法的设计，强制我的 hello_world 函数必须接受一个叫 name 的参数，因为我在路由里命名了它。并且如果我要加一些中间件，我只能通过装饰器的方式去加，而且我还要时刻留心函数参数这件事情。如果我要拿请求参数，还要用一个全局的变量 &lt;code&gt;request&lt;/code&gt; 去拿。这种局部不像局部，全局不像全局的方式非常的脏乱 。&lt;/p&gt;
&lt;h2 id="lemon-模块设计"&gt;Lemon 模块设计&lt;/h2&gt;
&lt;h3 id="server"&gt;Server&lt;/h3&gt;
&lt;p&gt;可以支持任何实现了 asgi 接口的 server 。默认调用 &lt;code&gt;app.listen()&lt;/code&gt; 会启动一个 uvicorn server 。无需配置即可部署在生产环境中。&lt;/p&gt;
&lt;h3 id="router"&gt;Router&lt;/h3&gt;
&lt;p&gt;默认的 Router 使用了 Trie 树作为路由的数据结构。如果你使用的是 HTTP RPC Style 形式的 API ，可以使用 SimpleRouter ，直接用字典形式存储。你也可以自己继承 AbstractRouter 或 AbstractBaseRouter 实现你自己的 Router 。&lt;/p&gt;
&lt;h3 id="context"&gt;Context&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ctx&lt;/code&gt; 实例，&lt;code&gt;ctx.state&lt;/code&gt; : {&amp;hellip;} 用来存储 middleware 链上的信息，供下游使用。&lt;/p&gt;
&lt;h3 id="request"&gt;Request&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ctx.req&lt;/code&gt; 的对象，存储客户端请求的所有信息。&lt;/p&gt;
&lt;h3 id="response"&gt;Response&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ctx.res&lt;/code&gt; 的对象，存储返回的结果。&lt;/p&gt;
&lt;h2 id="lemon-目前的状态"&gt;Lemon 目前的状态&lt;/h2&gt;
&lt;p&gt;Lemon 目前还只是一个 alpha 的版本 , 并不建议在生产环境上用，我最近会去写一系 benchmark 和 稳定性测试。包括还需要完善使用文档，添加一些易用的功能。&lt;/p&gt;
&lt;p&gt;我用我自己写的小工具测试了下, 结果如下:&lt;/p&gt;
&lt;h4 id="lemon"&gt;Lemon&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1515340517_hu_1923b3ebfb848908.webp" srcset="https://blog.joway.io/images/old-blog/1515340517_hu_ae4b84dcde8e10a1.webp 800w, https://blog.joway.io/images/old-blog/1515340517_hu_7d315244071fc37c.webp 1200w, https://blog.joway.io/images/old-blog/1515340517_hu_1923b3ebfb848908.webp 1468w" sizes="(max-width: 800px) 100vw, 800px" width="1468" height="1412" alt="Lemon" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;h4 id="sanic"&gt;Sanic&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1515340610_hu_e581021f6144d1dc.webp" srcset="https://blog.joway.io/images/old-blog/1515340610_hu_3503a798c1bbeb4d.webp 800w, https://blog.joway.io/images/old-blog/1515340610_hu_66748beec9fe8f0a.webp 1200w, https://blog.joway.io/images/old-blog/1515340610_hu_e581021f6144d1dc.webp 1458w" sizes="(max-width: 800px) 100vw, 800px" width="1458" height="1421" alt="Sanic" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;h4 id="flask"&gt;Flask&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1515340570_hu_c4f48ca1919ab934.webp" srcset="https://blog.joway.io/images/old-blog/1515340570_hu_6dfe69a14443d5bd.webp 800w, https://blog.joway.io/images/old-blog/1515340570_hu_75c3e09b417e40d5.webp 1200w, https://blog.joway.io/images/old-blog/1515340570_hu_c4f48ca1919ab934.webp 1476w" sizes="(max-width: 800px) 100vw, 800px" width="1476" height="1404" alt="Flask" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;测试结果很符合预期，因为和 Sanic 用的底层技术都差不多，所以两者性能几乎一样。之后会做更为广泛和专业的测试。&lt;/p&gt;
&lt;h2 id="项目地址"&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/joway/lemon"&gt;https://github.com/joway/lemon&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Golang : Make Programming Happy Again</title><link>https://blog.joway.io/posts/golang-talk/</link><pubDate>Sat, 30 Dec 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-talk/</guid><category>Tech</category><description>之前在公司内部做技术分享写的一个关于 Golang 的 slide ，花费了挺多的时间的，所以就脱敏了发出来。个人觉得值得一看 ，至少 PPT 的设计很不错 :) 。
Google Doc 地址 :
https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit?usp=sharing</description><content:encoded>&lt;p&gt;之前在公司内部做技术分享写的一个关于 Golang 的 slide ，花费了挺多的时间的，所以就脱敏了发出来。个人觉得值得一看 ，至少 PPT 的设计很不错 :) 。&lt;/p&gt;
&lt;p&gt;Google Doc 地址 :&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit?usp=sharing"&gt;https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit?usp=sharing&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Podcast 闲言碎语</title><link>https://blog.joway.io/posts/podcast/</link><pubDate>Tue, 10 Oct 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/podcast/</guid><category>Thought</category><description>Podcast 即常说的「播客」，在广义上既包括音频也包括视频。而狭义却在随着时代变迁出现了不同的偏向。
前 Podcast 时代 在当年博客热火朝天的时候，许多博主会有在博文中嵌入视频的需求，新浪顺势地推出了它的播客频道。这个阶段对于播客的定义接近于「个人上传的视频」。但事实上当时的播客很大一部分都是关于旅行和一些社会事件，即便是旅行也都是在拍摄大自然和风土人情，很少会嵌入个人色彩。有一个如今已经死掉了的视频网站 —— 土豆网当时的 slogan 就叫做 「每个人都是生活的导演」。土豆网最早明白播客的核心价值，但那个时代个人要拍摄一段视频的成本其实是非常高的，大部分手机没有视频功能，即便有拍摄质量也堪忧。再者拍摄完一段视频需要一台起码中等以上的PC才能完成剪辑工作，还需要一个不错的宽带才能花个几小时把视频上传到平台上。而且当时的宽带都不是光纤，上下行速度是不对等的，我记得那时候我的上传速度最高也只能到 30kb/s 。如今以快手抖音为代表的短视频浪潮无非是在延续土豆网当时的 slogan 。比较讽刺的是，现在土豆网的 slogan 却变成了 「召唤全球有趣短视频」。</description><content:encoded>&lt;p&gt;Podcast 即常说的「播客」，在广义上既包括音频也包括视频。而狭义却在随着时代变迁出现了不同的偏向。&lt;/p&gt;
&lt;h3 id="前-podcast-时代"&gt;前 Podcast 时代&lt;/h3&gt;
&lt;p&gt;在当年博客热火朝天的时候，许多博主会有在博文中嵌入视频的需求，新浪顺势地推出了它的播客频道。这个阶段对于播客的定义接近于「个人上传的视频」。但事实上当时的播客很大一部分都是关于旅行和一些社会事件，即便是旅行也都是在拍摄大自然和风土人情，很少会嵌入个人色彩。有一个如今已经死掉了的视频网站 —— 土豆网当时的 slogan 就叫做 「每个人都是生活的导演」。土豆网最早明白播客的核心价值，但那个时代个人要拍摄一段视频的成本其实是非常高的，大部分手机没有视频功能，即便有拍摄质量也堪忧。再者拍摄完一段视频需要一台起码中等以上的PC才能完成剪辑工作，还需要一个不错的宽带才能花个几小时把视频上传到平台上。而且当时的宽带都不是光纤，上下行速度是不对等的，我记得那时候我的上传速度最高也只能到 30kb/s 。如今以快手抖音为代表的短视频浪潮无非是在延续土豆网当时的 slogan 。比较讽刺的是，现在土豆网的 slogan 却变成了 「召唤全球有趣短视频」。&lt;/p&gt;
&lt;p&gt;在前 Podcast 时代，播客的定义被局限在视频，所谓的音频节目都是些收音机里的夜间电台或者是一些视频节目的音频版本 。而在后 Podcast 时代，我们口语中所谈论的播客更加偏向于音频。&lt;/p&gt;
&lt;h3 id="后-podcast-时代"&gt;后 Podcast 时代&lt;/h3&gt;
&lt;p&gt;到了2012年以后，涌现出了许多像荔枝FM之类的音频播客节目。这个时间恰好也是智能机浪潮的开始，也正是智能手机给了这些 App 足够的硬件能力来录制和传播优秀播客节目。包括那个时候的荔枝FM App ，其设计和交互也是我使用过的国产 App 里最好的。我记得当时我还在高中，晚上经常在上面听一些旅行主播聊天南海北的奇闻轶事，那样的日子真的太美好了。&lt;/p&gt;
&lt;p&gt;播客与博客最大的区别在于，当我们在书写文字的时候，经历的过程是 : 思考 -&amp;gt; 书写 -&amp;gt; 阅读写下的内容 -&amp;gt; 思考 。整个过程太过严谨，从而丧失了口语的魅力和乐趣。优秀的播客主播他在言谈的时候一定是极为放松的，也不会太过于去追求言谈的准确性，何况很多时候你意识到自己说的话有不严谨之处的时候，你已经说完话了。&lt;/p&gt;
&lt;p&gt;音频播客的流行并不意味着以前的视频形式落伍了。相反，正是由于视频内容开始真正被普及，使其从播客的定义中被剥离了出来，自成一体。如今的视频内容，往下走有诸如「短视频」之类的叫法，往上走会有人叫 「Vlog」。Vlog 继承了原先播客的精神内核，从表现人与自然过渡到表现自我。短视频则走向平民化和低俗化，从而成为了如今内容的第一载体。&lt;/p&gt;
&lt;p&gt;如果仔细观察这个趋势会发现，技术的发展使得内容的表现能力大幅提升，但问题在于，创作者的能力并没有多少提升，我们的技术并未在帮助创作者提升创作能力这件事情上有多少努力。&lt;/p&gt;
&lt;p&gt;从前文字类的博客对作者的能力要求其实并不高，你有基本的书写能力，表达一个悲惨的个人故事或者一个实际的社会问题，受众多少都能有所感触 。何况我们阅读博客本身就不是怀着阅读文学作品的心态去的。而音频类播客开始对作者的智商、情商、表达能力、反应速度、记忆力、话题掌控能力都提出来不低的要求。就我这七八年里所听过的超过9成的播客节目的作者都没有完全达到上述几点的要求，而且我深知如果我自己做播客肯定连一项都无法达标。而 Vlog 就更加是创作者的噩梦了。我大一时候听过中文系老师讲话剧课，老师训练过我们面向大众以最自然的姿势站立一分钟。在这一分钟里，绝大部分人会在后30s开始东张西望，手不知道放哪里，眼神飘忽不定。我们日常在开会的时候，大家会觉得好像面向大众表达很简单，但如果你站起来面对大家讲话感受就会不一样。如果让大家都目不转睛盯着你看你讲，那又完全是另外一个心态。Vlog 相当于你需要排除路上别人异样的眼神然后面对可能几百个也可能几百万个观众自言自语。同时播客剪辑大部分只是 「剪」，Vlog 还要求「辑 。并且受众对视频内容的挑剔程度会更加高。许多 Vlog 作者自己的脸上就写满了尴尬，很难让人坚持看下去。你甚至都很难在这个 14 亿人口的国家里举出超过 10 个优秀的 Vlog 作者出来。&lt;/p&gt;
&lt;p&gt;国内目前流行的诸如「快手」这类短视频软件里的确有许多不错的作品，但绝大部份属于演绎一个想象中的自己，或是贩卖尴尬，其视频时长和一张 Gif 差不多，内容也很单调。完全称不上上述讨论的任何一个品种范畴。其流行本身就是一个复杂的社会话题，和内容形式的关系并没有坊间所吹捧的那么大。&lt;/p&gt;
&lt;h3 id="我与-podcast"&gt;我与 Podcast&lt;/h3&gt;
&lt;p&gt;我从初中开始养成了一个需要听音频才能入睡的习惯，如果不听点什么东西，我就会脑子胡想，只有听播客才能让我大脑放掉戒备。最早我是把袁腾飞的课堂视频转成音频来听，后来发现了了锵锵三人行，就开始从01年的节目开始听。说来惭愧，我的性启蒙以及对待性差异群体的认同度都是听锵锵三人行学来的。只可惜这个节目显然超出了初中生的文化水平从而在今年被掐掉了。大学以后转用 iPhone , 开始在 iOS 的 Podcast 客户端上发掘到了一大批优秀的播客，但正如前述，即便是音频节目，对作者的要求也实在是太高，许多节目要么由于时间原因出现了月更年更的现象，要么由于作者自身涉猎范围有限最后只能不了了之。&lt;/p&gt;
&lt;p&gt;下面是几个我常听的 Podcast ，我在 iOS 下使用的客户端是 Castro , 其设计交互风格非常鬼畜，但也在 iOS 上自成一派。如果你使用的也是泛用型播客客户端，可以直接导入我的 &lt;a href="https://www.dropbox.com/s/lsoa9iou0q315ts/subscriptions.opml?dl=0"&gt; opml 文件 &lt;/a&gt;&lt;/p&gt;
&lt;h5 id="大内密谈"&gt;大内密谈&lt;/h5&gt;
&lt;p&gt;音乐从业人相征创办的一个播客，我不是很懂音乐，所以都是避着音乐类的节目听，非常喜欢 flypig 和 梁欢当嘉宾的几个节目&lt;/p&gt;
&lt;h5 id="日谈公园"&gt;日谈公园&lt;/h5&gt;
&lt;p&gt;从大内密谈出来的李叔和小伙子创办的全职参与的播客节目，能够请来许多牛逼的嘉宾，但目前似乎也遇到了朋友圈的资源用完了的情况。&lt;/p&gt;
&lt;h5 id="机核网"&gt;机核网&lt;/h5&gt;
&lt;p&gt;不仅仅止步于游戏本身，经常介绍一些游戏和美剧背后的文化背景，听过后才知道原来世界上还有那么多美好的事物存在&lt;/p&gt;
&lt;h5 id="比特新声"&gt;比特新声&lt;/h5&gt;
&lt;p&gt;对互联网的思考非常深入&lt;/p&gt;
&lt;h5 id="checked"&gt;Checked&lt;/h5&gt;
&lt;p&gt;果粉特供 ，属于科技圈普通人的闲谈，比起某个业内所熟知的果粉播客节目，没有那么多的知识点和价值观，听起来会很轻松。&lt;/p&gt;
&lt;h5 id="一天世界"&gt;一天世界&lt;/h5&gt;
&lt;p&gt;就是我上面说有很多知识点和价值观的果粉播客 :)&lt;/p&gt;
&lt;h5 id="nicktalk"&gt;NickTalk&lt;/h5&gt;
&lt;p&gt;文艺范的科技播客，科技范的文艺播客。&lt;/p&gt;
&lt;h5 id="untitled-techtalk"&gt;Untitled TechTalk&lt;/h5&gt;
&lt;p&gt;其实听起来会有点感觉尬聊，但因为都是同属学生团体，听起来会有感触&lt;/p&gt;
&lt;h5 id="io-调频"&gt;I/O 调频&lt;/h5&gt;
&lt;p&gt;覆盖面很广，作者表达能力很好，言语清晰，逻辑缜密，不知道作者有没有草稿，如果没有草稿的话，那真的非常牛逼了。&lt;/p&gt;
&lt;h5 id="人间指南"&gt;人间指南&lt;/h5&gt;
&lt;p&gt;非常有趣的视角，看名字就能感受到牛逼，如 「如何讲共产中文」、「如何传播谣言」&lt;/p&gt;</content:encoded></item><item><title>Kubernetes 中使用 API Gateway 替代 Ingress</title><link>https://blog.joway.io/posts/kubernetes-gateway/</link><pubDate>Tue, 12 Sep 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/kubernetes-gateway/</guid><category>Tech</category><description>背景 最近在构思基于 Kubernetes 建立一个个人的开放云平台 , 听起来有点不自量力 , 不过作为个人业余小玩意还是蛮好玩的。最终的成品希望是用户能够轻松地在平台上跑一些简单的无状态服务 和 cronjob 。
在搭建平台的时候遇到的第一个困难是需要有一个好用且功能全面的 API Gateway , 主流的网关服务大多是基于 OpenResty 基础上进行二次开发 , 所需要完成的工作无非是负载均衡，和 API 管理, 加上一些零零碎碎的小功能。</description><content:encoded>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;最近在构思基于 Kubernetes 建立一个个人的开放云平台 , 听起来有点不自量力 , 不过作为个人业余小玩意还是蛮好玩的。最终的成品希望是用户能够轻松地在平台上跑一些简单的无状态服务 和 cronjob 。&lt;/p&gt;
&lt;p&gt;在搭建平台的时候遇到的第一个困难是需要有一个好用且功能全面的 API Gateway , 主流的网关服务大多是基于 OpenResty 基础上进行二次开发 , 所需要完成的工作无非是负载均衡，和 API 管理, 加上一些零零碎碎的小功能。&lt;/p&gt;
&lt;h2 id="负载均衡"&gt;负载均衡&lt;/h2&gt;
&lt;p&gt;负载均衡分为四层和七层两种 , 以大家所熟知的 Nginx 为例 , 在它的 conf 文件中 , 有 http {} 和 stream {} 两种 block 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 四层负载均衡
stream {
server {
listen 80;
proxy_pass app;
}
upstream app {
server 172.31.0.1:8000;
}
}
# 七层负载均衡
http {
upstream app {
server 192.168.0.1:8000;
server 192.168.0.1:8001;
}
server {
listen 80;
location / {
proxy_pass http://app;
}
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="四层负载均衡"&gt;四层负载均衡&lt;/h3&gt;
&lt;p&gt;四层负载均衡只是从背后选择一个 server , 让其与客户端建立连接 , 其本身并不参与连接, 只是作为一个路由转发。好处是这样做它的性能会非常好，坏处是它也仅仅只能作为一个负载均衡存在，你无法对它做更加高层的处理，例如图片优化 , gzip 压缩等。并且由于它直接将后端服务暴露给了客户端, 当面临 DDoS 等攻击时, 后端将直接承受巨额流量。&lt;/p&gt;
&lt;h3 id="七层负载均衡"&gt;七层负载均衡&lt;/h3&gt;
&lt;p&gt;七层负载均衡就是我们常用的反向代理，直接参与连接，是架在后端服务和客户端之间的桥梁。它能够知道任何客户端能够知道的任何信息，完成包括 cookie 处理, 鉴权等等操作。代价是它的性能会比四层负载均衡低，但由于客户端不直接与任何后端服务接触，它能够轻易地进行后端服务迁移，降级等操作。&lt;/p&gt;
&lt;h3 id="kuberntes-ingress"&gt;kuberntes Ingress&lt;/h3&gt;
&lt;p&gt;Kubernets 的 Service 就是属于四层负载均衡 , 但 Service 上的 ip 都是集群内网的地址 , 需要在 Service 之上再建立一个反向代理把 Service 暴露到外网 。Kubernets 自带一个 Ingress 功能 , 与其说功能，不如说就是提供了一个类似 ConfigMap 的接口功能 ，用户可以以 &lt;code&gt;[ host - paths -&amp;gt; services ]&lt;/code&gt; 的形式 , 在 Ingress 里建立一个个映射规则 , 然后启动一个 Ingress Controller , Ingress Controller 将订阅 Ingress 里的配置规则并转化成 Nginx 的配置 , 然后对外部提供服务。在对外网暴露地址的时候, 只需要暴露 Ingress Controller 自身就行了, 所有服务可以被隔离在集群内部。&lt;/p&gt;
&lt;p&gt;一般在使用的时候，会给 Ingress Controller 的 Service 指定一个 NodePort , 这样每台机器都能有一个端口来作为 Ingress Controller 的入口来使用 , 然后再在 AWS 或 Google Cloud 上建立一个负载均衡把流量导向这些机器上的端口 , 从而使 Ingress Controller 的流量被分摊到每台机器上 。说起来很绕 , 相当于一个请求经过层层路由器后 , 最后打到你服务器上，还要再经过三个负载均衡器 。&lt;/p&gt;
&lt;p&gt;Ingress Controller 并不仅仅局限于 Nginx 。Kubernetes 官方提供了许多种选择方案。例如使用 HAProxy 来替代 Nginx : &lt;a href="https://github.com/kubernetes/ingress/blob/master/examples/deployment/haproxy/README.md"&gt;haproxy ingress&lt;/a&gt; 。事实上只要自己 watch 好 K8S Ingress 的变化，然后生成好对应的代理规则完全可以实现一个自己定制的 Ingress Controller 。&lt;/p&gt;
&lt;p&gt;但这里有一个坑是 Ingress 的 spec 字段只允许你写它规则里的配置项, 如果你想加入别的字段就会非常蛋疼了。这种情况下 ，我看到过的项目一般会有三种方案:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一种是使用 metadata.annotations 字段来写配置 , 这个字段可以随意写，但是如果你要针对 path 做限定条件，这个方案就非常蛋疼了。&lt;/li&gt;
&lt;li&gt;还有一种是在所有配置都在 ConfigMap 里写 , 不再需要 watch Ingress 了。这种做法其实就不算是在实现 Ingress Controller 了 ，因为完全脱离了 Ingress 自己的使用规范，但是异常好用。&lt;/li&gt;
&lt;li&gt;第三种也是最为灵活的还是直接写自己的数据库里，完全和 k8s 脱离。Ingress 这个项目以目前的趋势看，顶多成为一个 Demo 类型的产品，离成为一个功能完备的网关服务还有很长的路要走。依照它的规范来建立网关服务除了让能够使用现成的接口和命令行工具外，看不到任何的优势。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="api-gatewat-选型"&gt;API Gatewat 选型&lt;/h2&gt;
&lt;h3 id="初步需求"&gt;初步需求&lt;/h3&gt;
&lt;p&gt;在了解清楚了这个背景并且确认了 kubernets 自身短期内很难再为社区提供一个内置的 API Gateway 以后 ，就需要从 k8s 生态外部寻求更加优秀的解决方案进行改造了。&lt;/p&gt;
&lt;p&gt;我对于 API Gateway 的需求是 :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能够有一个现成并且活跃的社区&lt;/li&gt;
&lt;li&gt;能够完成基本的七层负载均衡需求&lt;/li&gt;
&lt;li&gt;能够有一个优秀的 UI 界面管理 API&lt;/li&gt;
&lt;li&gt;能够支持对部分 API 进行鉴权&lt;/li&gt;
&lt;li&gt;由于是业余折腾所需，所以我希望安装和使用足够简单，不需要后期折腾&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个流量的链路是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;外部流量
-&amp;gt; aws / google cloud load balance
-&amp;gt; api gateway node port
-&amp;gt; servicename.namepsace
-&amp;gt; pod ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基于上述目的，搜寻了一圈以后，发现了两个不错的项目 : &lt;a href="https://getkong.org/"&gt;Kong&lt;/a&gt; 和 &lt;a href="http://orange.sumory.com/docs/"&gt;Orange&lt;/a&gt; 。&lt;/p&gt;
&lt;h3 id="orange"&gt;Orange&lt;/h3&gt;
&lt;p&gt;Orange 基于 Openresty 开发，仿照了 Kong 的思想 , 使用了更加简洁和统一设计 。但它只支持 mysql 作为数据库 。 值得一提的是，它的 API 部分使用了作者另外研发的一套框架 Lor ，另外它还内置了一个非常漂亮简洁的 Dashboard 。它的核心思想是，当一个请求进来以后，会根据其特征，经过层层规则筛选，最后匹配到一个后端服务。规则会缓存在 share dict 中 , 所以它的性能和是否什么数据库没有什么关系 。&lt;/p&gt;
&lt;p&gt;但按目前 Orange 的设计以及社区的使用情况来看，它并没有成熟的 Kubernetes 架构下使用的例子，不过 Kuberntes 也并没有什么特殊的东西，别的架构能用这里应该也差不多，所以我还是斗胆尝了第一口螃蟹 。&lt;/p&gt;
&lt;p&gt;总体用下来 , 基本功能是可以实现 , 但是除此之外好像也没有什么别的高阶能力了 。不过它的 Dashboard 的确写得不错 ，基于条件配置比基于表单配置要好得多。我在公司内部尝试过给我们自己的 API Gateway 写配置前端，写得异常痛苦，用的人想必也异常痛苦。但当时也想不到什么好的方法，因为把一个有很长深度的 json 树给抽象到表单上去配置本身就是一个贼麻烦的事情，像 AWS 对于这些配置前端实在没办法做了干脆就直接给你一个 json 编辑器让你手写了。Orange 不同的是，它直接从目的出发，既然你最终想要的是一个路由规则，那我就不用 json 去生成规则，而是直接让你自己去写规则。&lt;/p&gt;
&lt;p&gt;例如 ，我原先需要这样一个表单 :&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1505148736_hu_bd1ae309166051e3.webp" srcset="https://blog.joway.io/images/old-blog/1505148736_hu_a11417f20765690a.webp 800w, https://blog.joway.io/images/old-blog/1505148736_hu_f8ecabb6f75d4ad3.webp 1200w, https://blog.joway.io/images/old-blog/1505148736_hu_bd1ae309166051e3.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="853" alt="Kubernetes 中使用 API Gateway 替代 Ingress" loading="eager" fetchpriority="high" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;它有一个问题是 , 我需要人肉把一个表单在大脑内编译成一个 nginx 的配置规则， 如果你的一个域名后面有100个后端服务，你这个表单就需要写100次，且无法复用表单。&lt;/p&gt;
&lt;p&gt;但是在 Orange 里 , 这个表单就是 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if host == xxx.joway.io
if urls == /static
…
then proxy : http://service-name:port
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1505148844_hu_661d6a67e5d31a1a.webp" srcset="https://blog.joway.io/images/old-blog/1505148844_hu_d1b7e2b96c50b867.webp 800w, https://blog.joway.io/images/old-blog/1505148844_hu_f9105625d17b1da1.webp 1200w, https://blog.joway.io/images/old-blog/1505148844_hu_661d6a67e5d31a1a.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="319" alt="Kubernetes 中使用 API Gateway 替代 Ingress" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;你只要在这里面不停添加规则就行了 , 如果两个后端服务只有一个条件是不同的，你只要写一个 OR 就行了 , 可以非常完美的复用配置 。&lt;/p&gt;
&lt;p&gt;当然 Orange 还是有很多坑的。有一个坑是我需要手动在 orange 的 nginx.conf 文件里把 resolver 改成我 Pod 上的 nameserver 地址 ，否则它解析不到我 k8s 里的 service name 。k8s 自己会跑一个 kube-dns 来解析所有内外网的域名 , 所以我不需要单独给它指定类似 8.8.8.8 的地址。&lt;/p&gt;
&lt;p&gt;还有一个坑是，它官方推荐的镜像每一次重启容器都会初始化数据库 …… 这导致我一度以为这玩意的配置都是存在内存里的。&lt;/p&gt;
&lt;p&gt;我改了下官方的推荐镜像，移除了它自带的 dnsmasq , 镜像地址在 &lt;a href="https://github.com/joway/docker-orange-kubernetes"&gt;joway/docker-orange-kubernetes&lt;/a&gt; 。 第一次指定 &lt;code&gt;ORANGE_DB_SETUP&lt;/code&gt; 环境变量就能初始化数据库。&lt;/p&gt;
&lt;h3 id="kong"&gt;Kong&lt;/h3&gt;
&lt;p&gt;Kong 是另外一个非常活跃的项目 , 专门针对微服务设计的，支持 cassandra 和 postgres 两种数据库。它有着机器丰富的插件 ， 并且已经有非常多的 kubernetes 生产环境实践例子。它没有官方自带的 dashboard , 但有第三方的项目提供 UI 界面。&lt;/p&gt;
&lt;p&gt;我使用的是 konga (&lt;a href="https://github.com/pantsel/konga"&gt;https://github.com/pantsel/konga&lt;/a&gt;) 这个项目。从 UI 上讲也是非常美观的，但是功能上还是比 Orange 要复杂和难用。但是由于 kong 整个社区的背书，我最终还是选择了它。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1505150036_hu_db75edd9c48e02ce.webp" srcset="https://blog.joway.io/images/old-blog/1505150036_hu_dcc7583af5845c84.webp 800w, https://blog.joway.io/images/old-blog/1505150036_hu_f4e491b2b9ddd1af.webp 1200w, https://blog.joway.io/images/old-blog/1505150036_hu_db75edd9c48e02ce.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="840" alt="Kubernetes 中使用 API Gateway 替代 Ingress" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.joway.io/images/old-blog/1505150092_hu_611f40b9e439a885.webp" srcset="https://blog.joway.io/images/old-blog/1505150092_hu_24ac24d5fd8af00b.webp 800w, https://blog.joway.io/images/old-blog/1505150092_hu_20e305e6c7d12564.webp 1200w, https://blog.joway.io/images/old-blog/1505150092_hu_611f40b9e439a885.webp 1600w" sizes="(max-width: 800px) 100vw, 800px" width="1600" height="958" alt="Kubernetes 中使用 API Gateway 替代 Ingress" loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;值得注意的是，kong 支持重试策略 ，这个功能有利有弊，好处是当后端服务不稳定的时候，比如访问100次有5次超时，这个时候对客户端而言顶多有几个服务慢了一些时间而已，不至于失败。但坏处是，一旦流量来了一波高峰，导致后端服务被压跨了一部分，但它还在不停重试，导致流量被放大，从而后端服务更加无法恢复过来，最后雪崩。关于这个我并没有在 Kong 里找到相应应对措施配置，但如果是自己写的话，其实可以做一个一定时间全局最大重试次数来加以控制。&lt;/p&gt;</content:encoded></item><item><title>ElasticSearch 最佳实践</title><link>https://blog.joway.io/posts/elasticsearch-bp/</link><pubDate>Sun, 28 May 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/elasticsearch-bp/</guid><category>Tech</category><description>Elasticsearch 是一个需要不停调参数的庞然大物 , 从其自身的设置到JVM层面, 有着无数的参数需要根据业务的变化进行调整。最近采用3台 AWS r3.2xlarge , 32GB, 4核, 构建了一套日均日志量过亿的 EFK 套件。经过不停地查阅文档进行调整优化 , 目前日常CPU占用只在30% , 大部分 Kibana 内的查询都能在 5s ~ 15s 内完成。</description><content:encoded>&lt;p&gt;Elasticsearch 是一个需要不停调参数的庞然大物 , 从其自身的设置到JVM层面, 有着无数的参数需要根据业务的变化进行调整。最近采用3台 AWS r3.2xlarge , 32GB, 4核, 构建了一套日均日志量过亿的 EFK 套件。经过不停地查阅文档进行调整优化 , 目前日常CPU占用只在30% , 大部分 Kibana 内的查询都能在 5s ~ 15s 内完成。&lt;/p&gt;
&lt;p&gt;下面记录了一些实践过程中积累的经验。&lt;/p&gt;
&lt;h3 id="硬件"&gt;硬件&lt;/h3&gt;
&lt;h4 id="cpu"&gt;CPU&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;多核胜过高性能单核CPU&lt;/li&gt;
&lt;li&gt;实践中发现, 在高写入低查询的场景下, 日常状态时 , CPU 还能基本应付, 一旦进行 kibana 上的查询或者 force merge 时, CPU 会瞬间飙高, 从而导致写入变慢, 需要很长一段时间 cpu 才能降下来。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="mem"&gt;Mem&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Elasticsearch 需要使用大量的堆内存, 而 Lucene 则需要消耗大量非堆内存 (off-heap)。推荐给 ES 设置本机内存的一半, 如32G 内存的机器上, 设置 -Xmx16g -Xms16g ，剩下的内存给 Lucene 。&lt;/li&gt;
&lt;li&gt;如果你不需要对分词字符串做聚合计算（例如，不需要 fielddata ）可以考虑降低堆内存。堆内存越小，Elasticsearch（更快的 GC）和 Lucene（更多的内存用于缓存）的性能越好。&lt;/li&gt;
&lt;li&gt;由于 JVM 的一些机制 , 内存并不是越大越好, 推荐最大只设置到 31 GB 。&lt;/li&gt;
&lt;li&gt;禁用 swap &lt;code&gt;sudo swapoff -a&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="配置"&gt;配置&lt;/h3&gt;
&lt;p&gt;PS: 应该尽可能使用 ansible 这类工具去管理集群 , 否则集群内机器的状态不一致将是一场噩梦。&lt;/p&gt;
&lt;h4 id="jvm"&gt;JVM&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;不轻易丢改 jvm 参数 , 除非你明确知道这个参数在做什么。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="节点配置"&gt;节点配置&lt;/h4&gt;
&lt;h5 id="集群配置"&gt;集群配置&lt;/h5&gt;
&lt;p&gt;PUT &lt;code&gt;/_cluster/_settings&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="对所有索引设置"&gt;对所有索引设置&lt;/h5&gt;
&lt;p&gt;PUT &lt;code&gt;/_all/_settings&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# cluster settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PUT /_cluster/settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # 永久变更, 它会覆盖掉静态配置文件里的选项
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;persistent&amp;#34; : {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;discovery.zen.minimum_master_nodes&amp;#34; : 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # 临时修改 , 重启后清除
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;transient&amp;#34; : {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;indices.store.throttle.max_bytes_per_sec&amp;#34; : &amp;#34;50mb&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h5 id="防止脑裂"&gt;防止脑裂&lt;/h5&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;discovery.zen.minimum_master_nodes &amp;gt; = ( master 候选节点个数 / 2) + 1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;集群最少需要有两个 node , 才能保证既可以不脑裂, 又可以高可用&lt;/p&gt;
&lt;h4 id="segment"&gt;Segment&lt;/h4&gt;
&lt;p&gt;es 为了搜索性能不被后台 merge 影响 , 对它进行了限速。&lt;/p&gt;
&lt;p&gt;如果使用的是 SSD , 需要手动调高 elasticsearch 的 throttle 。[尤其是对高写入的服务]&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PUT /_cluster/settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;persistent&amp;#34; : {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;indices.store.throttle.max_bytes_per_sec&amp;#34; : &amp;#34;100mb&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="故障恢复"&gt;故障恢复&lt;/h3&gt;
&lt;h4 id="恢复集群"&gt;恢复集群&lt;/h4&gt;
&lt;p&gt;当有节点掉线的时候 , 其余节点会选举 master , 并 rebalance data &amp;amp;&amp;amp; copy shards , 这时整个集群网络和IO会大幅度上升 , 等到有节点加入的时候 , 该节点会删除本地已经被复制的数据, 然后再进行 rebalance。这个过程需要大量时间。但是假如数据的 replica set 存在于当前活跃的节点中 , 则整个集群仍旧是出于可用状态 , status 会变成 yellow。&lt;/p&gt;
&lt;p&gt;在实践中发现 , 当一台机器被打挂后 , 压力均摊到其余机器, 会把其余机器也给打挂。这种场景下，与其雪崩，不如挂掉以后停止自动恢复，等挂掉的机器自己重启并假如集群。&lt;/p&gt;
&lt;p&gt;设置下几个个参数可以帮助我们做这个权衡:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gateway.recover_after_nodes: 8 # 等待集群至少存在 8 个节点 后才能进行数据恢复
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gateway.expected_nodes: 10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gateway.recover_after_time: 5m # 等待 5 分钟，或者 10 个节点上线后，才进行数据恢复，这取决于哪个条件先达到
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些配置只能设置在 config/elasticsearch.yml 文件中或者是在命令行里（它们不能动态更新）。&lt;/p&gt;
&lt;p&gt;另外, 我们也能够通过设置延迟分配来阻止当某个 Node 临时下线时候触发 reassign 。下面的操作能够延迟5分钟分配, 若此时 Node 又恢复回来了则不进行再分配。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PUT /_all/_settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;settings&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;index.unassigned.node_left.delayed_timeout&amp;#34;: &amp;#34;5m&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="滚动重启升级"&gt;滚动重启/升级&lt;/h4&gt;
&lt;h5 id="前期准备"&gt;前期准备&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可能的话，停止索引新的数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;禁止分片分配。这一步阻止 Elasticsearch 再平衡缺失的分片，直到你告诉它可以进行了。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PUT /_cluster/settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;transient&amp;#34; : {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;cluster.routing.allocation.enable&amp;#34; : &amp;#34;none&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭单个节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行维护/升级&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启节点，然后确认它加入到集群了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启分片分配&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PUT /_cluster/settings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;transient&amp;#34; : {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;cluster.routing.allocation.enable&amp;#34; : &amp;#34;all&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对其它Node同样进行此类操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="tips"&gt;Tips&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;降低日志收集组件的并发程度(降低实时性要求), fluentd 线程从 4 减少到 1 时 , ES 有负载有明显降低。&lt;/li&gt;
&lt;li&gt;在 fluentd 与 ES 中间加入一个 kafka 作为消息缓存，这样无论日志量瞬间增加多少倍，ES 都能平滑地消费 kafka 。&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item></channel></rss>