<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>cczywyc</title>
  <icon>https://www.gravatar.com/avatar/fd6207bf884e683a10f4dedae91093ac</icon>
  <subtitle>一个爱折腾的人</subtitle>
  <link href="https://cczywyc.com/atom.xml" rel="self"/>
  
  <link href="https://cczywyc.com/"/>
  <updated>2026-03-23T07:34:30.689Z</updated>
  <id>https://cczywyc.com/</id>
  
  <author>
    <name>cczywyc</name>
    <email>cczywyc@qq.com</email>
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>再聊 Chatbot/Workflow/RAG/Agent</title>
    <link href="https://cczywyc.com/2026/03/16/%E5%86%8D%E8%81%8AChatbot-Workflow-RAG-Agent/"/>
    <id>https://cczywyc.com/2026/03/16/%E5%86%8D%E8%81%8AChatbot-Workflow-RAG-Agent/</id>
    <published>2026-03-16T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>为什么又重新聊这几个概念？</p><p>最近在系统性学习 AI Agent 的构建和落地，喂给了 GPT 一批资料，让它给我整理了一份详细的 roadmap，第一步便是应该先弄清楚这几个概念，GPT 建议我输出一份文档，于是便有了这个文章（但其实这篇文章也是我列好大纲让 AI 写的）。</p><h2 id="一、为什么要区分这四个概念"><a href="#一、为什么要区分这四个概念" class="headerlink" title="一、为什么要区分这四个概念"></a>一、为什么要区分这四个概念</h2><p>学 AI Agent 的第一步不是写代码，而是搞清楚边界。很多项目失败不是因为技术不行，而是用错了模式——该用 Workflow 的地方强上 Agent，该用 Agent 的地方只做了 RAG。理解这四个概念的区别和关系，是后续所有工程决策的基础。</p><hr><h2 id="二、一句话定义"><a href="#二、一句话定义" class="headerlink" title="二、一句话定义"></a>二、一句话定义</h2><ul><li><strong>Chatbot</strong>：基于 LLM 的对话系统，用户问一句答一句，不主动行动，不调用外部工具。</li><li><strong>Workflow</strong>：预定义的固定流程，每一步做什么、调什么工具、输出什么格式都是开发者写死的。</li><li><strong>RAG（Retrieval-Augmented Generation）</strong>：在生成回答前，先从外部知识库检索相关内容注入上下文，让模型基于检索结果回答。</li><li><strong>Agent</strong>：具备自主决策循环的系统，能根据当前状态动态选择下一步行动，包括调用工具、检索信息、追问用户，直到任务完成。</li></ul><hr><h2 id="三、用一个场景看区别"><a href="#三、用一个场景看区别" class="headerlink" title="三、用一个场景看区别"></a>三、用一个场景看区别</h2><p><strong>任务：”帮我了解 LangGraph 适不适合我们的项目”</strong></p><table><thead><tr><th>系统</th><th>它会怎么做</th><th>问题在哪</th></tr></thead><tbody><tr><td>Chatbot</td><td>根据训练数据介绍 LangGraph 的基本信息，说完就结束</td><td>不了解”你的项目”，信息可能过时，不会主动补充</td></tr><tr><td>Workflow</td><td>按预设管道：搜索文档 → 提取特性 → 生成表格</td><td>如果搜索结果差，不会回头换策略，带着差结果继续跑</td></tr><tr><td>RAG</td><td>从知识库检索 LangGraph 相关段落，基于检索结果回答</td><td>知识库没有的内容就答不上，不会主动获取新信息</td></tr><tr><td>Agent</td><td>先分析问题 → 搜索文档 → 发现不够 → 搜替代方案对比 → 追问项目需求 → 给出建议</td><td>最灵活，但行为不完全可预测，调试成本高</td></tr></tbody></table><hr><h2 id="四、六个维度深度对比"><a href="#四、六个维度深度对比" class="headerlink" title="四、六个维度深度对比"></a>四、六个维度深度对比</h2><h3 id="4-1-决策权归属"><a href="#4-1-决策权归属" class="headerlink" title="4.1 决策权归属"></a>4.1 决策权归属</h3><p>这是最根本的区别。</p><ul><li>Chatbot &#x2F; RAG：决策权在<strong>用户</strong>手里，用户不问它就不动。</li><li>Workflow：决策权在<strong>开发者</strong>手里，流程提前设计好。</li><li>Agent：决策权在<strong>系统自身</strong>，根据当前状态自主判断下一步。</li></ul><h3 id="4-2-处理意外的能力"><a href="#4-2-处理意外的能力" class="headerlink" title="4.2 处理意外的能力"></a>4.2 处理意外的能力</h3><ul><li>Workflow：遇到意外就出问题，只认识预设路径。</li><li>Chatbot：遇到意外只能说”我不确定”。</li><li>RAG：知识库里没有相关内容时，检索结果变成噪声，回答质量断崖式下跌。</li><li>Agent：可以换策略——搜不到就换关键词，工具报错就试另一个，信息不够就主动追问。</li></ul><h3 id="4-3-工具使用方式"><a href="#4-3-工具使用方式" class="headerlink" title="4.3 工具使用方式"></a>4.3 工具使用方式</h3><ul><li>Chatbot：通常不调用工具。</li><li>Workflow：按预设顺序调用固定工具。</li><li>RAG：只调用”检索”这一种工具。</li><li>Agent：动态选择调用什么工具、什么时候调、调几次，可以组合多个工具。</li></ul><h3 id="4-4-上下文来源"><a href="#4-4-上下文来源" class="headerlink" title="4.4 上下文来源"></a>4.4 上下文来源</h3><ul><li>Chatbot：训练数据 + 当前对话。</li><li>RAG：训练数据 + 检索到的外部文档片段。</li><li>Workflow：管道中上一步传递的数据。</li><li>Agent：综合所有来源（训练知识、检索结果、工具返回值、对话历史、长期记忆），并自己决定什么时候需要获取更多上下文。</li></ul><h3 id="4-5-可控性与风险"><a href="#4-5-可控性与风险" class="headerlink" title="4.5 可控性与风险"></a>4.5 可控性与风险</h3><table><thead><tr><th>维度</th><th>Chatbot</th><th>Workflow</th><th>RAG</th><th>Agent</th></tr></thead><tbody><tr><td>可预测性</td><td>中</td><td>高</td><td>中</td><td>低</td></tr><tr><td>调试难度</td><td>低</td><td>低</td><td>中</td><td>高</td></tr><tr><td>失败模式</td><td>幻觉、过时</td><td>流程卡死</td><td>召回不准、引用错位</td><td>决策失误、工具误用、循环不收敛</td></tr><tr><td>适合生产环境</td><td>简单场景可直接上</td><td>最稳、最适合</td><td>需要持续优化检索质量</td><td>需要 guardrails + eval 才能上线</td></tr></tbody></table><h3 id="4-6-适用场景"><a href="#4-6-适用场景" class="headerlink" title="4.6 适用场景"></a>4.6 适用场景</h3><p><strong>用 Chatbot 就够：</strong> FAQ、简单信息查询、闲聊。任务单一，不需要外部数据和多步推理。</p><p><strong>用 Workflow 更好：</strong> 流程确定、步骤固定。例如定时报表生成、工单自动分类、数据管道处理。路径清晰就别用 Agent，Workflow 更稳更快。</p><p><strong>用 RAG 更好：</strong> 基于特定知识库的问答，且问答模式相对固定。例如企业内部文档问答、产品手册查询、客服知识库。</p><p><strong>必须用 Agent：</strong> 任务路径不确定，需要多步推理和动态决策。例如线上问题诊断、技术方案调研、需要跨多个数据源才能完成的复杂任务。</p><hr><h2 id="五、它们不是互斥的，而是可嵌套的"><a href="#五、它们不是互斥的，而是可嵌套的" class="headerlink" title="五、它们不是互斥的，而是可嵌套的"></a>五、它们不是互斥的，而是可嵌套的</h2><p>一个常见误区是把这四个概念当成互斥的选项。实际上它们是不同层次的能力，可以互相嵌套：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Agent（最上层编排者）</span><br><span class="line">├── 调用 RAG 模块获取知识</span><br><span class="line">├── 调用固定 Workflow 处理子任务</span><br><span class="line">├── 底层对话能力 = Chatbot</span><br><span class="line">└── 通过工具与外部世界交互</span><br></pre></td></tr></table></figure><p>一个成熟的 Agent 系统的运行过程可能是：用户提问 → Agent 判断需要查资料 → 调用 RAG 检索 → 发现不够 → 调用搜索工具补充 → 按 Workflow 格式整理结果 → 用对话能力生成自然语言回复。</p><hr><h2 id="六、关键判断：什么时候该用-Agent"><a href="#六、关键判断：什么时候该用-Agent" class="headerlink" title="六、关键判断：什么时候该用 Agent"></a>六、关键判断：什么时候该用 Agent</h2><p>在决定是否使用 Agent 之前，先问三个问题：</p><ol><li><strong>任务路径是否确定？</strong> 如果每次执行的步骤都一样，用 Workflow。</li><li><strong>是否需要动态使用多个工具？</strong> 如果只需要检索，RAG 就够。</li><li><strong>是否需要系统自主做出决策？</strong> 如果人类可以轻松预定义所有分支，不需要 Agent。</li></ol><p>一个简单的原则：<strong>能用简单方案解决的，就不要用复杂方案。</strong> Agent 的灵活性是有代价的——更难调试、更难预测、更难保证质量。只有当任务的复杂度确实需要自主决策时，Agent 才是正确的选择。</p><hr><h2 id="七、三个核心认知"><a href="#七、三个核心认知" class="headerlink" title="七、三个核心认知"></a>七、三个核心认知</h2><ol><li><p><strong>Agent 的”行动能力”来自工具。</strong> LLM 只能生成文本，是工具让它能搜索、能调 API、能读写文件。工具设计的质量直接决定 Agent 的能力上限。</p></li><li><p><strong>光有 LLM 不等于有 Agent。</strong> LLM 是被动的输入-输出系统。Agent 需要一个持续运行的控制循环：感知 → 决策 → 行动 → 观察结果 → 再决策。LLM 是 Agent 的一个组件，不是 Agent 本身。</p></li><li><p><strong>复杂度是有成本的。</strong> 不是所有问题都需要 Agent。选择 Chatbot &#x2F; Workflow &#x2F; RAG &#x2F; Agent 的依据不是”哪个更高级”，而是”任务的不确定性有多高”。</p></li></ol><hr><h2 id="附：引用"><a href="#附：引用" class="headerlink" title="附：引用"></a>附：引用</h2><ul><li><a href="https://github.com/microsoft/ai-agents-for-beginners">Microsoft AI Agents for Beginners</a>: Agent 基础概念与组成结构</li><li><a href="https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/">OpenAI Building Agents</a> 概览：产品与工程视角的 Agent 设计</li><li><a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic Building Effective Agents</a>：Agent 工程实践与复杂度控制建议</li></ul>]]></content>
    
    
    <summary type="html">几种AI系统的概念</summary>
    
    
    
    <category term="AI文章合集" scheme="https://cczywyc.com/categories/AI%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="AI" scheme="https://cczywyc.com/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>RAG（下）</title>
    <link href="https://cczywyc.com/2025/05/29/RAG_Code/"/>
    <id>https://cczywyc.com/2025/05/29/RAG_Code/</id>
    <published>2025-05-29T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://cczywyc.com/2025/05/25/RAG/">上一篇文章</a>是关于 RAG 的一些基本介绍，这篇主要是根据上篇的流程来实现一个简易的 RAG 系统，进一步加深对 RAG 工作流程的理解。本文所列举的示例代码非常简单，完全是按照上篇文章中所划分的三个阶段来的，但是包含了 RAG 系统所有的必要步骤。</p><h2 id="数据源加载与分块"><a href="#数据源加载与分块" class="headerlink" title="数据源加载与分块"></a>数据源加载与分块</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">List</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">split_into_chunks</span>(<span class="params">doc_file: <span class="built_in">str</span></span>) -&gt; <span class="type">List</span>[<span class="built_in">str</span>]:</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(doc_file, <span class="string">&#x27;r&#x27;</span>) <span class="keyword">as</span> file:</span><br><span class="line">        content = file.read()</span><br><span class="line">    <span class="keyword">return</span> [chunk <span class="keyword">for</span> chunk <span class="keyword">in</span> content.split(<span class="string">&quot;\n\n&quot;</span>)]</span><br><span class="line"></span><br><span class="line">chunks = split_into_chunks(<span class="string">&quot;doc.md&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i, chunk <span class="keyword">in</span> <span class="built_in">enumerate</span>(chunks):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;i&#125;</span>] <span class="subst">&#123;chunk&#125;</span>\n&quot;</span>)</span><br></pre></td></tr></table></figure><p>我们上篇说到，分块的方式有很多种，这里随便选取一种，按照换行符来给数据源分块。</p><h2 id="文本的向量化"><a href="#文本的向量化" class="headerlink" title="文本的向量化"></a>文本的向量化</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> sentence_transformers <span class="keyword">import</span> SentenceTransformer</span><br><span class="line"></span><br><span class="line">embedding_model = SentenceTransformer(<span class="string">&quot;shibing624/text2vec-base-chinese&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">embed_chunk</span>(<span class="params">chunk: <span class="built_in">str</span></span>) -&gt; <span class="type">List</span>[<span class="built_in">float</span>]:</span><br><span class="line">    embedding = embedding_model.encode(chunk, normalize_embeddings=<span class="literal">True</span>)</span><br><span class="line">    <span class="keyword">return</span> embedding.tolist()</span><br><span class="line"></span><br><span class="line">embedding = embed_chunk(<span class="string">&quot;测试内容&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(embedding))</span><br><span class="line"><span class="built_in">print</span>(embedding)</span><br></pre></td></tr></table></figure><ol><li><p>代码首先从 sentence-transformers 库导入 SentenceTransformer 类，并加载一个预训练好的中文文本向量化模型 shibing624&#x2F;text2vec-base-chinese。这个模型擅长将中文文本转换成能表达其语义的数字向量。</p></li><li><p>embed_chunk 函数接收一个文本块，使用加载的模型将其编码（encode）成一个向量。normalize_embeddings&#x3D;True 参数会将向量进行归一化，这有助于后续的相似度计算。</p></li><li><p>最后测试打印一下向量化，运行可以看到将测试内容转化成高维（本例是 768）向量。</p></li></ol><p>下面便是调用向量化方法，将第一步加载分块的数据源全部向量化。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">embeddings = [embed_chunk(chunk) <span class="keyword">for</span> chunk <span class="keyword">in</span> chunks]</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(embeddings))</span><br><span class="line"><span class="built_in">print</span>(embeddings[<span class="number">0</span>])</span><br></pre></td></tr></table></figure><h2 id="索引与存储"><a href="#索引与存储" class="headerlink" title="索引与存储"></a>索引与存储</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> chromadb</span><br><span class="line"></span><br><span class="line">chromadb_client = chromadb.EphemeralClient()</span><br><span class="line">chromadb_collection = chromadb_client.get_or_create_collection(name=<span class="string">&quot;default&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">save_embeddings</span>(<span class="params">chunks: <span class="type">List</span>[<span class="built_in">str</span>], embeddings: <span class="type">List</span>[<span class="type">List</span>[<span class="built_in">float</span>]]</span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">    <span class="keyword">for</span> i, (chunk, embedding) <span class="keyword">in</span> <span class="built_in">enumerate</span>(<span class="built_in">zip</span>(chunks, embeddings)):</span><br><span class="line">        chromadb_collection.add(</span><br><span class="line">            documents=[chunk],</span><br><span class="line">            embeddings=[embedding],</span><br><span class="line">            ids=[<span class="built_in">str</span>(i)]</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">save_embeddings(chunks, embeddings)</span><br></pre></td></tr></table></figure><ol><li>示例中使用的向量数据库是 chromadb</li><li>为了演示方便，chromadb.EphemeralClient() 创建了一个临时的、在内存中运行的向量数据库实例。</li><li>get_or_create_collection 创建了一个名为 “default” 的集合（类似于数据库中的表），用于存放我们的数据。</li><li>save_embeddings 函数遍历文本块和它们对应的向量，并将它们成对地添加到 chromadb_collection 中。每个条目都包含三部分：<ul><li>documents: 原始的文本内容</li><li>embeddings: 文本对应的向量</li><li>ids: 每个条目的唯一标识符，这里使用其在列表中的索引</li></ul></li></ol><h2 id="检索与召回"><a href="#检索与召回" class="headerlink" title="检索与召回"></a>检索与召回</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">retrieve</span>(<span class="params">query: <span class="built_in">str</span>, top_k: <span class="built_in">int</span></span>) -&gt; <span class="type">List</span>[<span class="built_in">str</span>]:</span><br><span class="line">    query_embedding = embed_chunk(query)</span><br><span class="line">    results = chromadb_collection.query(</span><br><span class="line">        query_embeddings=[query_embedding],</span><br><span class="line">        n_results=top_k</span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">return</span> results[<span class="string">&#x27;documents&#x27;</span>][<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line">query = <span class="string">&quot;哆啦A梦使用的3个秘密道具分别是什么？&quot;</span></span><br><span class="line">retrieved_chunks = retrieve(query, <span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i, chunk <span class="keyword">in</span> <span class="built_in">enumerate</span>(retrieved_chunks):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;i&#125;</span>] <span class="subst">&#123;chunk&#125;</span>\n&quot;</span>)</span><br></pre></td></tr></table></figure><ol><li><p>retrieve 函数接收一个用户问题 query 和一个整数 top_k（代表需要检索的数量，这里是 5）。</p></li><li><p>它首先将用户问题也转换成一个向量（使用与之前完全相同的模型）。</p></li><li><p>然后，它使用 chromadb_collection.query 方法，用问题的向量去数据库中搜索最相似的 top_k 个向量。</p></li><li><p>数据库返回最相似的条目，函数从中提取出原始的文本文档并返回。</p></li><li><p>代码最后用一个具体问题 “哆啦A梦使用的 3 个秘密道具分别是什么？” 来测试检索功能，并打印出找到的前 5 个最相关的文本块。</p></li></ol><h2 id="重排"><a href="#重排" class="headerlink" title="重排"></a>重排</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> sentence_transformers <span class="keyword">import</span> CrossEncoder</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">rerank</span>(<span class="params">query: <span class="built_in">str</span>, retrieved_chunks: <span class="type">List</span>[<span class="built_in">str</span>], top_k: <span class="built_in">int</span></span>) -&gt; <span class="type">List</span>[<span class="built_in">str</span>]:</span><br><span class="line">    cross_encoder = CrossEncoder(<span class="string">&#x27;cross-encoder/mmarco-mMiniLMv2-L12-H384-v1&#x27;</span>)</span><br><span class="line">    pairs = [(query, chunk) <span class="keyword">for</span> chunk <span class="keyword">in</span> retrieved_chunks]</span><br><span class="line">    scores = cross_encoder.predict(pairs)</span><br><span class="line"></span><br><span class="line">    scored_chunks = <span class="built_in">list</span>(<span class="built_in">zip</span>(retrieved_chunks, scores))</span><br><span class="line">    scored_chunks.sort(key=<span class="keyword">lambda</span> x: x[<span class="number">1</span>], reverse=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> [chunk <span class="keyword">for</span> chunk, _ <span class="keyword">in</span> scored_chunks][:top_k]</span><br><span class="line"></span><br><span class="line">reranked_chunks = rerank(query, retrieved_chunks, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i, chunk <span class="keyword">in</span> <span class="built_in">enumerate</span>(reranked_chunks):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[<span class="subst">&#123;i&#125;</span>] <span class="subst">&#123;chunk&#125;</span>\n&quot;</span>)</span><br></pre></td></tr></table></figure><ol><li><p>此处引入了一个 CrossEncoder 模型。与 SentenceTransformer（将单个句子转为向量）不同，CrossEncoder 直接比较一对文本（在这里是 (query, chunk)），并输出一个它们之间的相关性得分。</p></li><li><p>rerank 函数将用户问题与上一步检索到的每个文本块配对。</p></li><li><p>cross_encoder.predict 为每一对计算一个精确的相关性分数。</p></li><li><p>代码根据这个分数从高到低对检索到的文本块进行重新排序，并返回排序后最靠前的 top_k 个。</p></li></ol><p>上篇文章我们讲了一下重排的作用和目的，这里再次重述一下：重排可以显著提高 RAG 系统的质量，在召回阶段召回的结果可能会包含一些不那么相关的片段，重排序则使用一个更复杂、更精确的模型（CrossEncoder）对初步结果进行精细打磨，确保最终提供给语言模型的上下文质量最高、相关性最强。</p><h2 id="生成"><a href="#生成" class="headerlink" title="生成"></a>生成</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line"><span class="keyword">from</span> google <span class="keyword">import</span> genai</span><br><span class="line"></span><br><span class="line">load_dotenv()</span><br><span class="line">google_client = genai.Client()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">generate</span>(<span class="params">query: <span class="built_in">str</span>, chunks: <span class="type">List</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    prompt = <span class="string">f&quot;&quot;&quot;你是一位知识助手，请根据用户的问题和下列片段生成准确的回答。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">用户问题: <span class="subst">&#123;query&#125;</span></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">相关片段:</span></span><br><span class="line"><span class="string"><span class="subst">&#123;<span class="string">&quot;\n\n&quot;</span>.join(chunks)&#125;</span></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">请基于上述内容作答，不要编造信息。&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;<span class="subst">&#123;prompt&#125;</span>\n\n---\n&quot;</span>)</span><br><span class="line"></span><br><span class="line">    response = google_client.models.generate_content(</span><br><span class="line">        model=<span class="string">&quot;gemini-2.5-flash&quot;</span>,</span><br><span class="line">        contents=prompt</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> response.text</span><br><span class="line"></span><br><span class="line">answer = generate(query, reranked_chunks)</span><br><span class="line"><span class="built_in">print</span>(answer)</span><br></pre></td></tr></table></figure><p>最后一步就比较好理解了，将问题和重排后的相关片段作为上下文丢给大模型，然后调用 LLM 大模型接口生成答案，代码也比较简洁易懂，这里就不做描述。</p><p>以上就利用 python + langchain 实现了一个非常简易的 RAG 系统，主要是针对上一篇文章中三个阶段的代码实现，详细的代码运行结果和数据源文本见<a href="https://github.com/cczywyc/rag-py-eg">本项目地址</a>，运行前项目跟目录下创建一个 .env 文件，将 gemini 的 api key 填进去即可，像下面这样。</p><figure class="highlight tex"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GEMINI<span class="built_in">_</span>API<span class="built_in">_</span>KEY=xxxxxxxxxxxxx&lt;替换成你的API<span class="built_in">_</span>KEY&gt;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">一个建议的RAG系统的代码实现</summary>
    
    
    
    <category term="AI文章合集" scheme="https://cczywyc.com/categories/AI%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="AI" scheme="https://cczywyc.com/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>RAG（上）</title>
    <link href="https://cczywyc.com/2025/05/25/RAG/"/>
    <id>https://cczywyc.com/2025/05/25/RAG/</id>
    <published>2025-05-25T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<h1 id="基础原理"><a href="#基础原理" class="headerlink" title="基础原理"></a>基础原理</h1><p>RAG（Retrieval-Augmented Generation）中文叫做检索增强生成，它是一种将外部知识库与大语言模型（LLM）相结合的技术。其核心思想是在大模型生成回答之前，先检索文档将检索到内容作为上下文输入给模型，从而提高回答的准确性和时效性。</p><p>在一个典型的 RAG 系统中，首先会对外部文档进行预处理并向量化存储形成知识库，当用户提出问题，系统会将问题转化成向量，并且计算出向量相似度，在知识库中找出与用户问题相近的片段，然后将用户问题和相似片段一起丢给大模型（prompt），大模型在此基础上生成答案。</p><p>总结上述过程，大致可以分为三个阶段：</p><ol><li>准备阶段：分片、索引</li><li>检索阶段：召回、重排</li><li>生成阶段：生成</li></ol><h1 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h1><p>通过上述的介绍不难发现，RAG 适合用做智能客服或者企业内部知识库等私有化数据的场景。一般来说，传统的 LLM 通常都有上下文的限制（即使 Gemini 有百万的上下文），这就导致大模型在工作时始终有上下文局限，我们不可能把大量的私有数据一下子全部喂给大模型，而 RAG 通过分片、向量化存储和向量相似性等手段解决了 LLM 根本局限性。</p><p>总结一下，可以归纳为三大核心优势：</p><ol><li>获取最新信息：RAG通过连接到实时更新的数据源（如新闻网站、社交媒体流或企业内部的动态数据库），克服了LLM的知识时效性问题。这使得RAG系统能够提供反映最新情况的答案，这在金融、新闻和客户服务等快速变化的领域至关重要。</li><li>增加事实性并减少幻觉：RAG通过“事实接地”（Factual Grounding）来提高输出的准确性。它要求LLM的回答必须以检索到的外部文档为依据，而不是仅凭其内部的参数化知识。这种机制显著降低了模型产生“幻觉”的风险，并允许系统在生成答案时提供引文或来源链接，从而让用户可以验证信息的准确性。</li><li>实现专业领域知识：RAG使得通用的LLM能够在不经过昂贵和耗时的重新训练或微调的情况下，应用于高度专业的领域。无论是医疗、法律还是制造业，企业都可以通过构建包含其专有文档、手册和数据的知识库，来创建一个能够理解并回答特定领域问题的“专家系统”。</li></ol><p>下面来说一下 RAG 的工作流程。</p><h1 id="工作流程"><a href="#工作流程" class="headerlink" title="工作流程"></a>工作流程</h1><h2 id="第一阶段：分片与索引"><a href="#第一阶段：分片与索引" class="headerlink" title="第一阶段：分片与索引"></a>第一阶段：分片与索引</h2><ol><li><p>数据分片（Text Splitting）</p><p> 上面说到，LLM 存在上下文的局限，往往无法一次性处理长数据源，因此需要将数据源（文档、链接内容等）切分成多个片段。需要说明的是，分片方式的选择应当是灵活的，它应该是具备“上下文感知”（Context-aware）能力，尽可能沿着段落、章节、标题等自然边界进行分割，以保持每个块内部的语义连贯性，这对于确保检索的精准度和生成答案的质量至关重要。</p></li><li><p>嵌入（Embedding）</p><p> 然后，每个文本块都被送入一个嵌入模型（Embedding Model）中，转换成一个高维的数值向量，即“嵌入向量”。这个向量是文本块语义的数学表示，在向量空间中，意思相近的文本块，其对应的向量在空间中的距离也更近。嵌入模型的选择是一个关键的架构决策，因为它直接决定了后续语义搜索的质量和效果。</p></li><li><p>索引与存储（Indexing &amp; Storage)</p><p> 上一步通过 Embedding 将片段文本转换为向量后，将片段文本连同片段向量存入向量数据库中的过程叫做索引。</p></li></ol><h2 id="第二阶段：召回与重排"><a href="#第二阶段：召回与重排" class="headerlink" title="第二阶段：召回与重排"></a>第二阶段：召回与重排</h2><p>第二阶段主要包含查询转换、向量化搜索和重新排名等过程，我们一个个概念说。</p><ol><li><p>召回</p><p> 召回就是搜索与用户问题相关的片段过程，大致可以描述如下：<br> 用户输入问题 -&gt; Embedding（Embedding 下文有专门说到） -&gt; 转换为向量 -&gt; 向量数据库 -&gt; 查询与用户问题相近的片段 -&gt; 召回结果</p><p> 需要注意的是，向量数据库查询与用户问题相近的片段这一过程，我们叫做向量相似度计算。向量相似度计算方式有很多种，目前比较常见的有余弦相似度、欧式距离、点积等。</p></li><li><p>重排</p><p> 重排做的事情就是重新排序，它做的事情跟召回类似，对召回结果中的相似片段进行二次评估，从而找到跟用户问题更为相似和接近的片段作为重排结果，提升回答的准确性。跟召回不同的是，重排阶段使用了一个更小、更专业的重排模型，它在提升准确性的同时也带来了更大的开销和成本，因此重排阶段不能跟召回混在一起。</p></li></ol><h2 id="第三阶段：生成"><a href="#第三阶段：生成" class="headerlink" title="第三阶段：生成"></a>第三阶段：生成</h2><p>最后这个阶段比较好理解，生成最终答案。</p><p>在第二阶段得到重排结果后，系统将经过重新排名后的、最相关的文档块内容，与用户的原始查询组合在一起，形成一个全新的、内容丰富的“增强提示”（Augmented Prompt）。这是一个关键的提示工程（Prompt Engineering）步骤。提示语会明确地指示LLM，将提供的文档块作为其回答的唯一事实依据。</p><p>这个增强提示被发送给LLM。模型被要求在严格遵循所提供上下文的基础上，综合信息，并以流畅、连贯的自然语言生成对用户原始问题的回答。这种“有约束的生成”是RAG的核心机制，它将LLM的创造力引导到基于事实的轨道上，从而确保了答案的准确性和可靠性。</p><p>最后，生成的答案会呈现给用户。一个设计良好的RAG系统通常会附上答案所依据的来源信息，例如源文档的名称、链接或具体段落的引用 。这种透明度不仅增强了用户的信任感，也为需要审计和验证的专业场景提供了必要支持。</p><h1 id="RAG-系统的核心组件说明"><a href="#RAG-系统的核心组件说明" class="headerlink" title="RAG 系统的核心组件说明"></a>RAG 系统的核心组件说明</h1><p>上面说完了 RAG 大致的工作流程，下面来说一下 RAG 系统中常见的核心组件。</p><h2 id="文本拆分器"><a href="#文本拆分器" class="headerlink" title="文本拆分器"></a>文本拆分器</h2><p>上面说到了 RAG 系统的流程第一阶段就需要对数据源进行分片，分片的方式有多种，例如可以对数据源按字数分片、按段落分片、按章节分片和按页码分片等。这其中需要我们的 RAG 能够具备分片的能力，例如常见的 LangChain 就提供了多种文本拆分器，可以满足对数据源进行不同分片方式的场景。</p><h2 id="Embedding"><a href="#Embedding" class="headerlink" title="Embedding"></a>Embedding</h2><p>Embedding 说简单点就是将文本转换为向量的过程。</p><p>我以一个二维向量为例，假设 “某产品的工作电压是 210V～250V 之间” 这个文本片段对应的向量是 [1,2]，“某产品的最佳工作电压是 220V” 这个文本片段对应的向量是 [1,1]，“某产品保修 3 年” 这个文本片段对应的向量是 [-3,-1]，容易看到，前两个文本片段包含的内容比较接近，转化为向量后，向量也是十分接近的，而第三个文本片段和前两个内容差距比较大，转化为向量后差别也比较大，这正是 Embedding 的意义，含义相近的文本在做 Embedding 之后，向量也是相近的。</p><p>现在假设用户的问题是 ”某产品应该在多大的电压下工作“，在对用户问题做 Embedding 之后得到一个向量，我们就可以根据向量相似度找到与之相近的向量文本片段，从而将用户问题和相近的文本片段一起丢给大模型，大模型就可以根据这些信息生成最终的答案。</p><h2 id="向量数据库"><a href="#向量数据库" class="headerlink" title="向量数据库"></a>向量数据库</h2><p>向量数据库是 RAG 系统的基石头，它负责存储和检索嵌入向量，常见的主流的向量数据库有 Pinecone 和 ChromaDB 等。</p>]]></content>
    
    
    <summary type="html">RAG 的基本概念和流程</summary>
    
    
    
    <category term="AI文章合集" scheme="https://cczywyc.com/categories/AI%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="AI" scheme="https://cczywyc.com/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>聊聊MCP</title>
    <link href="https://cczywyc.com/2025/04/26/MCP/"/>
    <id>https://cczywyc.com/2025/04/26/MCP/</id>
    <published>2025-04-26T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>模型上下文协议（MCP）是一个开放协议，用于标准化应用程序向大语言模型提供上下文的方式，由 Anthropic 于 2024 年 11 月推出。在 MCP 出现之前，将 AI 模型连接到各种数据源通常需要为每个源开发定制化的集成方案，导致系统碎片化、难以扩展且开发成本高昂，MCP 则通过提供一个通用的、标准化的协议来解决这个问题。</p><h2 id="MCP-架构"><a href="#MCP-架构" class="headerlink" title="MCP 架构"></a>MCP 架构</h2><p>MCP 的核心架构遵循客户端-服务器模型，主要包含三个组件：</p><ol><li><strong>MCP 主机（MCP Host）</strong>：指的是希望通过 MCP 利用外部数据或工具的应用程序。例如 AI 助手（如 Claude Desktop）、集成开发环境（IDE）、或其他 AI 工具。MCP Host 负责管理 MCP 客户端实例，并控制权限和安全策略。</li><li><strong>MCP 客户端（MCP Client）</strong>：嵌入在主机应用程序中，是实现 MCP 协议客户端逻辑的部分。它负责与一个或多个 MCP 建立并维护安全、一对一的连接，管理通信、状态和能力协商。</li><li><strong>MCP 服务器（MCP Server）</strong>：是实现协议服务器端逻辑的程序。它连接到具体的数据源（本地文件、数据库等）或远程服务（API），并通过 MCP 协议暴露其能力。MCP Server 可以是本地进程，也可以是远程服务。</li></ol><p><img src="https://img.cczywyc.com/MCP_core_components.webp"></p><h3 id="MCP-Server-暴露的核心能力"><a href="#MCP-Server-暴露的核心能力" class="headerlink" title="MCP Server 暴露的核心能力"></a>MCP Server 暴露的核心能力</h3><p>MCP Server 主要向客户端暴露三种类型的能力，是 AI 模型能够更深入地理解上下文并执行任务：</p><ol><li>Resources：提供 AI 模型可以读取的类文件数据或上下文信息。这可以包括文件内容、数据库记录、API 响应、系统信息或应用程序状态等 。例如，文件系统服务器可以暴露本地文档作为资源，数据库服务器可以暴露查询结果作为资源。AI 模型可以将这些资源加载到其上下文中，以获取执行任务所需的信息。</li><li>Tools：定义 AI 模型可以（在用户批准后）调用的函数或操作 。这些工具使 AI 能够执行计算、访问外部 API、查询数据库、操作文件或与外部系统交互 。例如，GitHub 服务器可能提供 <code>create_issue</code> 或 <code>list_repositories</code> 等工具 。工具是实现“代理行为”（Agentic Behavior）的关键，让 AI 从信息处理者转变为任务执行者。</li><li>Prompts：提供预定义的、可重用的提示模板或工作流，通常可以通过斜杠命令或菜单选项触发，以帮助用户或 AI 模型完成特定任务 。这有助于引导交互、结构化请求，并确保一致性。</li></ol><p>通过以上三种能力的组合，MCP Server 极大地扩展了 AI 模型的功能边界，使其能够更有效地与现实世界的数据和系统进行交互。</p><h2 id="MCP-解决了哪些问题"><a href="#MCP-解决了哪些问题" class="headerlink" title="MCP 解决了哪些问题"></a>MCP 解决了哪些问题</h2><p>MCP 通过标准化协议解决了 AI 工具在实际应用中面临的两大核心问题：数据接入的局限性和任务执行的缺失。</p><p>在 MCP 出现之前，通用大语言模型（LLM）的训练数据是有限的，它只能来自公开的数据源。在实际使用场景下，我们往往需要使用多个不同的数据源作为 LLM 的上下文语料输入，例如：在 AI 工具里，我需要同时使用本地文件的一些资料和高德地图以及 github 的资源，在这种场景下，一般都需要单独定制开发；再比如，我需要 AI 工具帮我在本地 terminal 执行一些命令时，往往需要借助系统提供的 api 来完成。</p><p>那么，再有了 MCP 以后，上述场景变成了什么样子的呢？现在可以使用不同的 MCP Server，用一个标准的、通用的协议来完成上述事情。比如，我可以配置本地 file 和 terminal 的 MCP，github 和 高德也都有官方提供的 MCP Server，我们只需要进行一些简单的配置，就可以通过自然语言来描述我们的需求，AI 工具（也叫 MCP Host）会自动选择合适的 MCP Client 和 对应的 MCP Server 通信，来完成各种复杂的任务。</p><p><img src="https://img.cczywyc.com/MCP_before_and_after.png"></p><p>总得来说，MCP 通过以下方式解决了上述问题：</p><ul><li>提供标准化的交互方式：MCP 为 AI 工具提供了一种标准化的方法来发现和调用外部系统中的操作。它充当了AI“理解”与“执行”之间的桥梁，使AI不仅能回答问题，更能主动完成任务。</li><li>简化集成开发：通过标准化的协议，MCP大大降低了开发者为AI构建与关键工具安全集成的难度和时间成本。</li><li>提高 AI 工具间的可替换性：由于采用了统一标准，相似类型的应用（如不同的云存储服务）在MCP服务器实现上会很相似，使得在它们之间切换变得非常简单，可能只需要修改几行代码，而无需重写整个集成。</li></ul><h2 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h2><h3 id="通信协议：JSON-RPC-2-0"><a href="#通信协议：JSON-RPC-2-0" class="headerlink" title="通信协议：JSON-RPC 2.0"></a>通信协议：JSON-RPC 2.0</h3><p>MCP 的核心通信机制建立在 JSON-RPC 2.0 协议之上。这是一种轻量级的远程过程调用（RPC）协议，使用 JSON 作为数据格式。选择 JSON-RPC 2.0 提供了几个优势：</p><ul><li>标准化：它是一个成熟且广泛理解的标准，简化了不同语言和平台之间的互操作性。</li><li>轻量级：JSON 格式简洁，易于解析，开销较低。</li><li>支持多种消息类型：JSON-RPC 2.0 定义了请求（Request）、响应（Response，包括成功结果 Result 和错误 Error）以及通知（Notification）等消息类型，满足了 MCP 所需的双向、有状态通信需求。</li><li>状态管理：MCP 利用 JSON-RPC 建立有状态的连接，支持能力协商、进度跟踪、取消操作和错误报告等高级功能。</li></ul><p>所有通过 MCP 传输层发送的消息（无论是请求、响应还是通知）都遵循 JSON-RPC 2.0 的规范进行编码和解码。</p><h3 id="传输层：STDIO-与-SSE"><a href="#传输层：STDIO-与-SSE" class="headerlink" title="传输层：STDIO 与 SSE"></a>传输层：STDIO 与 SSE</h3><p>MCP 协议本身是与传输层无关的，但它定义了两种标准的传输机制，用于在客户端和服务器之间实际传输 JSON-RPC 消息：</p><ol><li>STDIO (Standard Input&#x2F;Output):<ul><li>机制：在这种模式下，MCP 客户端（通常在主机应用内）将 MCP 服务器作为一个子进程启动。服务器通过其标准输入（stdin）读取来自客户端的 JSON-RPC 消息，并将响应或通知写入其标准输出（stdout）。</li><li>适用场景：这是本地集成的理想选择，即客户端和服务器在同一台机器上运行时。它设置简单，通信效率高，适用于进程间通信。官方建议客户端尽可能支持 stdio。许多 SDK 提供内置的 STDIO 传输实现。</li><li>由于通信限制在本地进程间，通常被认为相对安全，但仍需注意服务器进程的权限管理。</li></ul></li><li>Streamable HTTP (with SSE - Server-Sent Events):<ul><li>机制：这种模式下，服务器作为一个独立的进程运行（可能在远程机器上），能够处理多个客户端连接。客户端通过标准的 HTTP POST 请求向服务器发送 JSON-RPC 消息。服务器可以使用 Server-Sent Events (SSE) 技术通过一个持久的 HTTP 连接向客户端流式发送响应、通知或服务器发起的请求。客户端也可以通过 HTTP GET 请求启动 SSE 流，以便服务器可以主动向客户端发送消息。</li><li>适用场景：适用于需要网络通信的场景，例如连接到远程服务器或基于 Web 的应用。它利用了广泛使用的 HTTP 协议，易于穿透防火墙。微软 Copilot Studio 目前仅支持 SSE 传输。</li><li>安全：由于涉及网络通信，安全性变得尤为重要。必须使用 TLS 加密传输。服务器需要实施严格的认证和授权机制，并验证请求来源（例如，检查 Origin 头）以防止 DNS 重绑定等攻击。实现者需要仔细处理连接管理、错误恢复（SSE 支持通过 Last-Event-ID 头进行断线重连）和潜在的拒绝服务攻击。</li></ul></li></ol><h3 id="工作流程"><a href="#工作流程" class="headerlink" title="工作流程"></a>工作流程</h3><p>当用户与支持 MCP 功能的 AI 工具交互时，会进行一系列的通信过程。</p><h4 id="协议握手"><a href="#协议握手" class="headerlink" title="协议握手"></a>协议握手</h4><ol><li>连接初始化（Initial connection）：交互开始，MCP Host 通过 MCP Client 连接到已经配置好的每个 MCP Server；</li><li>能力发现（Capability discovery）：MCP Host 会询问每个 MCP Server：“你可以提供什么能力？”，每个 MCP Server 会响应自身可以提供的功能、可以调用的工具等；</li><li>登记注册（Registration）：MCP Host 登记注册每个 MCP Server 的能力，以便在后续交互过程中决定请求哪个 MCP Server。</li></ol><p><img src="https://img.cczywyc.com/MCP_protocol_handshake.png"></p><p>下面以询问 “杭州今天天气怎么样？”为例来介绍整个流程。</p><ol><li>需求识别：MCP Host 分析识别问题，识别到此任务需要获取外部数据源的实时信息；</li><li>工具或资源选择：MCP Host 确定需要使用 MCP 来完成此任务；</li><li>权限请求：MCP Client 确认是否有访问外部数据源的权限；</li><li>请求过程：权限一旦得到确认，MCP Host 便调用相应的 MCP Client 使用标准的协议格式向对应的 MCP Server 发送请求；</li><li>外部处理：MCP Server 收到来自 MCP Client 的请求，开始调用外部资源进行处理，如：查询天气服务、房访问文件系统或访问数据库；</li><li>返回结果：MCP Server 将处理后的结果以标准化格式返回给 MCP Client；</li><li>上下文整合：MCP Host 将返回的结果结合上下文环境进行结果整合；</li><li>生成响应：MCP Host 根据整合后的上下文生成最终的结果返回给用户。</li></ol><h2 id="配置与开发"><a href="#配置与开发" class="headerlink" title="配置与开发"></a>配置与开发</h2><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>MCP 的使用配置也相对简单。</p><p>首先需要选用支持 MCP 功能的 MCP Host，如 Claude Desktop、cursor、cline、trae 等，将 MCP Server 集成到 MCP Host 中，配置的核心就是告诉 MCP Host 如何启动和与 MCP Server 通信。</p><ul><li><p>通用配置元素：配置通常涉及以下信息：</p><ul><li>命令（Command）：启动服务器进程的可执行文件或命令（例如 <code>npx</code>, <code>python</code>, <code>docker</code>）</li><li>参数（Args）：传递给启动命令的参数列表（例如脚本路径、服务器选项、要暴露的目录等）</li><li>环境变量（Env）：为服务器进程设置的环境变量（例如 API 密钥、访问令牌、数据库连接字符串等）</li></ul></li><li><p>Claude Desktop 配置示例（clause_desktop_config.json）：</p><p>Clause Desktop 使用一个 JSON 文件（通常位于 <code>~/Clause/clause_desktop_config.json</code>）来配置 MCP Server，其基本结构如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;mcpServers&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;server-identifier-1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;command_to_run_server_1&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;arg1&quot;</span><span class="punctuation">,</span> <span class="string">&quot;arg2&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;env&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;API_KEY&quot;</span><span class="punctuation">:</span> <span class="string">&quot;your_api_key_here&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;server-identifier-2&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;docker&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;env&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;GITHUB_TOKEN&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&lt;YOUR_GITHUB_TOKEN&gt;&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>mcpServers</code> 是包含所有 MCP Server 配置的顶层对象</li><li><code>server-identifier-1</code>，<code>server-identifier-2</code> 是为每个 MCP Server 指定的唯一标识符（例如：filesystem，github，minecraft）</li></ul></li></ul><p>备注：上述只是介绍了使用 Claude Desktop 配置 MCP Server 的示例，其他的工具比如 cursor、trae 等现在都提供了比较简单的配置方式和交互路径，没有什么难度。</p><h3 id="开发"><a href="#开发" class="headerlink" title="开发"></a>开发</h3><p>我们可以根据实际场景开发自己的 MCP Client 和 MCP Server，为了简化整个开发流程，<a href="https://modelcontextprotocol.io/introduction">官方</a>和社区提供了多种语言的 SDK 和框架，这些 SDK 封装了协议的复杂性，提供了更高级别的抽象。</p><ul><li>官方主要 SDK：<ul><li>TypeScript&#x2F;JavaScript</li><li>Python</li><li>Java&#x2F;Kotlin（包括对 Spring 的支持）</li><li>Go</li><li>Swift</li></ul></li><li>SDK 特性：<ul><li>核心 MCP Client 和 MCP Server 实现</li><li>内置的传输层实现（如 STDIO 和 SSE），通常无需外部 Web 框架即可使用</li><li>对同步和异步编程范式的支持</li><li>用于定义和处理能力的辅助类和函数</li><li>协议版本协商、列表变更通知等协议功能的实现</li></ul></li></ul><h2 id="比较与总结"><a href="#比较与总结" class="headerlink" title="比较与总结"></a>比较与总结</h2><p>MCP 提供了一种独特的 AI 集成方法，与其他现有技术既有重叠也有区别，最后再来简单总结一下 MCP 与其他集成工具的区别：</p><ul><li><p>MCP vs 传统 API 调用：</p><ul><li>范围：传统 API 是通用的软件接口；MCP 专为 AI 与外部世界交互而设计，侧重于以标准化方式提供上下文（资源）、动作（工具）和引导（提示）</li><li>抽象层级：直接调用 API 需要 AI 或其宿主应用处理认证、请求格式化、错误处理等细节。MCP Server 则封装了这些细节，为 AI 提供了一个更高层次、更一致的接口</li><li>集成复杂度：MCP 旨在解决 N×M 的集成难题，通过标准化接口减少所需的连接数量</li></ul></li><li><p>MCP vs RAG（Retrieval-Augmented Generation）：</p><ul><li>交互模式：RAG 主要是一种被动的信息检索机制，通过从知识库（通常是向量数据库）中查找相关文本片段并注入提示词来增强 LLM 的上下文 。MCP 则支持主动交互，AI 可以通过“工具”执行实时查询、调用 API 或执行操作，而不仅仅是检索静态文本</li><li>数据时效性：RAG 依赖于预先索引的数据，可能存在时效性问题。MCP 可以直接访问实时数据源</li><li>计算开销：RAG 通常需要计算密集型的嵌入生成和向量搜索。MCP 的直接访问模式可以避免这部分开销</li><li>关系：MCP 和 RAG 并非完全互斥，可以互补。例如，一个 MCP Server 可以实现一个“工具”，该工具的功能是执行向量数据库查询，从而将 RAG 的能力整合到 MCP 框架下</li></ul><p><strong>备注：我本人对 RAG 也十分感兴趣，后面肯定会单独研究一下 RAG。</strong></p></li><li><p>MCP vs 框架工具（如 LangChain Tools）：</p><ul><li>标准化层面：LangChain 等框架提供了一个开发者面向的标准（例如，其 Tool 类接口），用于在代码层面集成工具。MCP 则提供了一个模型（或运行时 Agent）面向的标准，允许 AI Agent 在运行时发现和使用任何符合 MCP 规范的服务器暴露的工具，即使这些工具在编写 Agent 代码时是未知的</li><li>互补性： 这两者是高度互补的。LangChain 已经提供了适配器，可以将 MCP Server 暴露的工具视为 LangChain 工具来使用 。开发者可以使用 LangChain 来构建 Agent 的逻辑流程，同时利用 MCP 作为与外部工具交互的标准化接口</li></ul></li></ul><p>结来说，MCP 的核心优势在于其开放性、标准化和对主动交互（工具执行）的强调，旨在创建一个更统一、更灵活的 AI 集成生态系统。然而，它的成功依赖于是否被广泛的采纳，并且它将集成的复杂性从直接 API 调用转移到了实现和维护符合协议标准的服务器上。</p><p>（全文完）</p><h2 id="Refrence"><a href="#Refrence" class="headerlink" title="Refrence"></a>Refrence</h2><ol><li><a href="https://modelcontextprotocol.io/introduction">https://modelcontextprotocol.io/introduction</a> – MCP 官方文档</li></ol>]]></content>
    
    
    <summary type="html">关于MCP入门和使用的文章</summary>
    
    
    
    <category term="AI文章合集" scheme="https://cczywyc.com/categories/AI%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="AI" scheme="https://cczywyc.com/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>Paxos</title>
    <link href="https://cczywyc.com/2024/10/02/Paxos/"/>
    <id>https://cczywyc.com/2024/10/02/Paxos/</id>
    <published>2024-10-02T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Paxos-背景和介绍"><a href="#Paxos-背景和介绍" class="headerlink" title="Paxos 背景和介绍"></a>Paxos 背景和介绍</h1><p>进入正文之前先来简单说一下几个概念和 Paxos 的由来。</p><h3 id="分布式一致性问题"><a href="#分布式一致性问题" class="headerlink" title="分布式一致性问题"></a>分布式一致性问题</h3><p>在分布式计算领域，一个核心且基础的挑战是如何让一组独立的计算机（或称为节点、进程）在可能发生故障（如节点崩溃、网络消息丢失或延迟）的环境下，就某个值或状态达成一致 。这个问题被称为共识（Consensus）。达成共识的能力对于构建可靠的分布式系统至关重要，这些系统需要在多个副本之间维护数据一致性，例如分布式数据库、分布式锁服务、以及实现状态机复制（State Machine Replication）等场景 。状态机复制是一种强大的技术，它允许将任何单机上的确定性算法转化为一个容错的、分布式的实现，确保即使部分副本发生故障，整个系统对外也能表现得像一个永不宕机的单体服务 。</p><p>一个健壮的共识算法通常需要满足几个关键属性：</p><ol><li>有效性（Validity）：最终被选定的值必须是某个进程实际提议过的值。</li><li>一致性（Agreement&#x2F;Consistency）：所有做出决定的正常进程必须对同一个值达成一致 。这是共识最核心的保证。</li><li>完整性（Integrity）：每个进程最多只能决定一个值。</li><li>可终止性（Termination&#x2F;Liveness）：所有正常的进程最终都能做出决定。</li><li>容错性（Fault Tolerance）：算法必须能够在一定数量的进程失败或网络异常的情况下继续运行并达成共识。</li></ol><p>分布式一致性问题可以简单描述为：如何确保分布式系统中的多个节点对某个值达成一致，即使在部分节点故障或网络不可靠的情况下。</p><h3 id="Paxos-的诞生"><a href="#Paxos-的诞生" class="headerlink" title="Paxos 的诞生"></a>Paxos 的诞生</h3><p>Paxos 算法由 Leslie Lamport 于 1989 年首次提出，并在 1998 年发表论文《The Part-Time Parliament》。由于这篇论文使用了一个古希腊 Paxos 岛上的议会作为比喻，晦涩难懂，直到 2001 年 Lamport 又发表了《Paxos Made Simple》，才使这个算法被更广泛地理解和应用。</p><p>Paxos 算法解决的核心问题是：在一个可能发生节点宕机或网络异常的分布式系统中，如何就某个值达成一致，使得系统满足一致性（所有节点最终达成相同的决定）和安全性（一旦某个值被选定，就不会被改变）。</p><p>Paxos 算法之所以如此重要，是因为它为分布式系统中的一个核心问题提供了第一个经过严格证明的、可在异步网络模型下容忍非拜占庭故障（non-Byzantine failures，即节点只会崩溃或丢失消息，不会恶意发送错误信息）的解决方案。</p><ul><li>保证一致性（Safety）：Paxos 最核心的贡献在于它提供了一种机制，确保即使在节点崩溃、消息丢失、延迟或乱序的情况下，系统中的所有节点最终也能就某个值达成唯一的、一致的决定 。它保证了“只选出最多一个值”（Agreement）和“被选出的值必须是提议过的值”（Validity）这两个关键的安全属性。</li><li>容错性（Fault Tolerance）：Paxos 算法通过其核心的“法定人数”（Quorum）机制，能够容忍少数节点（通常是少于半数）的失效。只要大多数节点（一个 Quorum）能够正常工作并相互通信，系统就能继续就新的值达成共识 。</li><li>分布式系统的基石：Paxos 不仅仅是一个理论算法，它为构建实际的、高可用的分布式系统奠定了基础。许多关键的工业级系统，如 Google 的 Chubby 锁服务、微软的 Autopilot 集群管理系统，以及亚马逊、IBM 等公司的许多内部系统，都使用了 Paxos 或其变种来实现核心的协调和一致性保证 。同时，Paxos 的思想也深刻影响了后续共识算法（如 Raft ）的设计。</li></ul><p>Paxos 的出现，标志着分布式系统理论的一个重大突破，它成功地将严谨的理论证明与解决实际工程挑战（如构建容错数据库、文件系统等）的需求结合起来。它证明了在充满不确定性的分布式环境中，实现可靠的一致性是可能的，为现代大规模分布式计算系统的发展铺平了道路。</p><h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>Paxos 算法在现代分布式系统中有广泛应用：</p><ol><li>分布式数据库 ：如 Google 的 Spanner、Cockroach DB 等</li><li>分布式锁服务 ：如 Google 的 Chubby</li><li>配置管理 ：如 ZooKeeper（虽然 ZooKeeper 使用的是 Paxos 的变种 ZAB 协议）</li><li>共识系统 ：如区块链中的共识机制</li></ol><h1 id="Paxos-算法原理和详细实现过程"><a href="#Paxos-算法原理和详细实现过程" class="headerlink" title="Paxos 算法原理和详细实现过程"></a>Paxos 算法原理和详细实现过程</h1><h3 id="Paxos-中的角色"><a href="#Paxos-中的角色" class="headerlink" title="Paxos 中的角色"></a>Paxos 中的角色</h3><p>Paxos 算法中定义了三种角色：</p><ol><li>Proposer（提议者） ：提出提案，提案包括提案编号和提案的值。</li><li>Acceptor（接受者） ：接收提案并决定是否接受。</li><li>Learner（学习者） ：不参与决议，只学习被批准的提案。</li></ol><p>在实际实现中，一个节点可以同时扮演多个角色。这种角色的分离，特别是 Acceptor 作为核心状态维护者的设计，为 Paxos 提供了灵活性。例如，Proposer 的选举（通常为了提高效率而选出一个 Leader）可以独立于核心共识逻辑进行，而 Learner 的实现方式也可以多样化。然而，Acceptor 的正确性和状态持久性是保证整个协议安全性的基石。任何对 Acceptor 状态的访问和修改都需要严格的并发控制和持久化保证。</p><h3 id="算法流程"><a href="#算法流程" class="headerlink" title="算法流程"></a>算法流程</h3><p>在深入之前，有必要再强调一下提案编号（Proposal Number）的关键作用：</p><ul><li>唯一性与全序性：每个 Proposer 发起的每一次提议尝试都必须使用一个比它之前使用过的任何编号都大的、全局唯一的编号 。这通常通过组合一个单调递增的计数器（或高精度时间戳）和 Proposer 自身的唯一标识符来实现。</li><li>建立优先级：提议编号的大小决定了提议的优先级。Acceptor 只会响应（Promise）或接受（Accept）编号大于等于其已承诺的最高编号的提议。这确保了协议的进展总是朝着更新（编号更大）的提议方向进行。</li><li>保证一致性：提议编号是 Paxos 安全性的核心。通过要求 Proposer 在第二阶段选择的值必须基于第一阶段从多数 Acceptor 处了解到的、具有最高编号的已接受值，Paxos 巧妙地利用提议编号的顺序来确保一旦某个值被选定，后续更高编号的提议也只能选定相同的值。</li></ul><p>Paxos 算法分为两个阶段：</p><h4 id="阶段一：准备（Prepare）与承诺（Promise）"><a href="#阶段一：准备（Prepare）与承诺（Promise）" class="headerlink" title="阶段一：准备（Prepare）与承诺（Promise）"></a>阶段一：准备（Prepare）与承诺（Promise）</h4><p>这个阶段的目标是让一个 Proposer 获得“领导权”（即一个被多数 Acceptor 承诺接受的提议编号），并发现是否存在任何可能已经被选定的值。</p><ul><li>（1a）Prepare 消息：<ul><li>Proposer 想要提议一个值（比如 v），它首选选择一个新的、唯一的、比它之前用过的都大的提议编号 <code>n</code></li><li>Proposer 向一个 <strong>法定人数（Quorum）</strong> 的 Acceptor 发送 <code>Prepare(n)</code> 消息。</li></ul></li><li>（1b）Promise 响应：<ul><li>当一个 Acceptor 收到 <code>Prepare(n)</code> 消息时，它会比较 <code>n</code> 和它已经记录的已响应过的最高 Prepare 请求编号 <code>max_prepare</code>。</li><li>情况 1：<code>n</code> 大于该 Acceptor 已经响应过的所有 Prepare 请求的编号，则该 Acceptor 必须：<ol><li>将 max_prepare 更新为 <code>n</code>，并持久化这个状态。</li><li>向 Proposer 返回一个 <code>Promise(n, prev_accepted_n, prev_accepted_v)</code> 消息。<ul><li><code>prev_accepted_n</code> 是这个 Acceptor 之前接受过的提议中编号最高的那个提议编号（如果没有接受过任何提议，则为空或者置为特殊值，如 0 或 -1）。</li><li><code>prev_accepted_v</code> 是对应 <code>prec_accepted_n</code> 的值（如果 <code>prev_accepted_n</code> 为空，则此值也为空）。</li></ul></li><li>这个 <code>Promise</code> 响应意味着 Acceptor 承诺：它未来不会再接受任何编号小于 <code>n</code> 的提议，也不会再响应任何编号小于 <code>n</code> 的 Prepare 请求。</li></ol></li><li>情况 2：<code>n</code> 小于 max_prepare，这表示收到了一个过时的 Prepare 请求（可能来自网络延迟，或者有其他的 Proposer）已经发起了更高编号的提议，Acceptor 可以选择：<ol><li>直接忽略这个 <code>Prepare(n)</code> 消息。</li><li>或者可以优化一下，回复一个 <code>Nack(n, max_prepare)</code> 拒绝的消息，告知 Proposer 它的提议编号太旧了，让 Proposer 可以更快放弃或重试。</li></ol></li></ul></li></ul><h4 id="阶段二：接受（Accept）与已接受（Accepted）"><a href="#阶段二：接受（Accept）与已接受（Accepted）" class="headerlink" title="阶段二：接受（Accept）与已接受（Accepted）"></a>阶段二：接受（Accept）与已接受（Accepted）</h4><p>如果 Proposer 成功从多数 Acceptor 那里获得了对提议编号 <code>n</code> 的 Promise，它就可以进入第二阶段，尝试让一个具体的值被接受。</p><ul><li>（2a）Accept 消息<ul><li>Proposer 收集来自 Quorum 的 <code>Promise(n, prev_n, prev_v)</code> 响应。</li><li>关键决策：选择要提议的值 <code>v_proposal</code> 。<ol><li>Proposer 检查所有收到的 <code>Promise</code> 响应中携带的 <code>(prev_n, prev_v)</code>。</li><li>如果<strong>存在</strong>任何 <code>prev_v</code> 不为空的响应（即至少有一个 Acceptor 之前接受过某个值），Proposer <strong>必须</strong>选择所有报告的 <code>(prev_n, prev_v)</code> 中具有<strong>最高</strong> <code>prev_n</code> 的那个 <code>prev_v</code> 作为它本次要提议的值 <code>v_proposal</code>。</li><li>如果所有收到的 <code>Promise</code> 响应中的 <code>prev_v</code> 都为空（即这个 Quorum 中的 Acceptor 之前都没有接受过任何值），那么 Proposer <strong>可以自由选择</strong>它最初想要提议的值 <code>v_original</code> 作为 <code>v_proposal</code>。</li></ol></li><li>一旦确定了 <code>v_&#123;proposal&#125;</code>，Proposer 就向之前回复了 <code>Promise</code> 的那个 Quorum（或者另一个 Quorum，通常是同一个）发送 <code>Accept(n, v_proposal)</code> 消息。</li></ul></li><li>（2b）Accepted 响应：<ul><li>当一个 Acceptor 收到 <code>Accept(n, v)</code> 消息时，它会检查 <code>n</code> 是否<strong>大于或等于</strong>它记录的 <code>max_prepare</code>。<strong>注意</strong>：这里允许等于 <code>n</code>，因为 Acceptor 在阶段 1b 已经承诺了不接受小于 <code>n</code> 的提议，但接受等于 <code>n</code> 的提议是允许的，并且是协议正确运行所必需的。</li><li>情况 1：<code>n</code> &gt; <code>max_prepare</code>。Acceptor 接受这个提议：<ol><li>记录下接受的提议编号 <code>accepted_proposal_number = n</code> 和接受的值 <code>accepted_value = v</code>（<strong>并持久化这些状态</strong>）。</li><li>向 Proposer 和所有 Learner 发送 <code>Accepted(n, v)</code> 消息。</li></ol></li><li>情况 2：<code>n</code> &lt; <code>max_prepare</code>。这表示 Acceptor 在回复 <code>Promise(n)</code> 之后，又收到了一个更高编号的 <code>Prepare(n&#39;)</code> 并回复了 <code>Promise(n&#39;)</code>。根据承诺，它不能再接受编号为 <code>n</code> 的提议了。因此，它会忽略这个 <code>Accept(n, v)</code> 消息。</li></ul></li></ul><p><strong>共识达成</strong>：当一个值 <code>v</code> 被一个 Quorum（多数）的 Acceptor 接受（即它们都发送了 <code>Accepted(n, v)</code> 消息，对于某个特定的 <code>n</code>），那么这个值 <code>v</code> 就被 Paxos 协议**选定（chosen）**了。Learner 通过收集这些 <code>Accepted</code> 消息来得知结果。</p><p>这个两阶段过程的核心机制在于：<strong>阶段一的 Prepare&#x2F;Promise 交互强制任何想要成为“领导者”（获得批准的 Proposer）的进程，必须先去了解并尊重可能已经发生的“历史”（即被 Quorum 接受的值），然后才能在阶段二提出自己的（可能被修正过的）主张。</strong> 如果 Quorum 中的 Acceptor 报告了之前接受的值，新的 Proposer 就失去了自由选择值的权利，必须延续那个已被接受（或趋向于被接受）的值。正是这种机制，保证了即使有多个 Proposer 并发尝试，或者 Proposer 在过程中失败，最终也只会有一个值被稳定地选定。</p><h3 id="算法特性"><a href="#算法特性" class="headerlink" title="算法特性"></a>算法特性</h3><p>法定人数（Quorum）的概念：保证多数一致</p><p>Quorum 在 Paxos 中扮演着至关重要的角色，它是保证算法安全性和容错性的数学基础。</p><ul><li><strong>定义</strong>：一个 Quorum 是 Acceptor 集合的一个子集，其大小通常定义为<strong>严格超过半数</strong>。在一个包含 <code>N</code> 个 Acceptor 的系统中，Quorum 的大小通常是 ⌊N&#x2F;2⌋+1。例如，在有 5 个 Acceptor 的系统中，Quorum 大小是 3；在有 3 个 Acceptor 的系统中，Quorum 大小是 2 。</li><li><strong>Quorum 交集属性</strong>：Quorum 的核心特性是<strong>任意两个 Quorum 必须至少有一个共同的成员</strong> 。这是集合论的一个简单结果（如果两个子集都包含超过半数的元素，它们的交集不可能是空的）。</li><li><strong>保证安全性（Safety）</strong>：Paxos 利用 Quorum 交集属性来确保一致性。<ul><li>在阶段一，Proposer 必须从一个 Quorum 的 Acceptor 处获得 Promise。</li><li>在阶段二，Proposer 必须将 Accept 请求发送给一个 Quorum 的 Acceptor，并且一个值只有被一个 Quorum 的 Acceptor 接受才算被选定。</li><li>假设一个值 <code>v</code> 在提议 <code>m</code> 中被一个 Quorum <code>Q_1</code> 接受了。现在，如果另一个 Proposer 发起了更高编号的提议 <code>n &gt; m</code>，它必须在阶段一从某个 Quorum <code>Q_2</code> 获得 Promise。由于 <code>Q_1</code> 和 <code>Q_2</code> 必然有交集，Proposer <code>n</code> 在阶段一必定会联系到至少一个接受了 <code>(m, v)</code> 的 Acceptor。这个 Acceptor 会在 <code>Promise</code> 响应中报告 <code>(m, v)</code>。根据阶段二的规则，Proposer <code>n</code> 必须选择具有最高编号的已接受值，因此它也只能提议值 <code>v</code>（或者是在 <code>m</code> 和 <code>n</code> 之间被选定的某个值，通过归纳法可知也必须是 <code>v</code>）。这样就保证了不会有与 <code>v</code> 不同的值在更高的提议编号中被选定。</li></ul></li><li><strong>提供容错性（Fault Tolerance）</strong>：Quorum 机制使得 Paxos 能够容忍少数 Acceptor 的失败。在一个有 <code>N = 2F + 1</code> 个 Acceptor 的系统中，只要至少有 <code>F + 1</code> 个 Acceptor（即一个 Quorum）能够正常工作并相互通信，系统就能继续运行 Prepare 和 Accept 阶段，从而达成共识 。系统可以容忍最多 <code>F</code> 个 Acceptor 同时发生故障。</li><li><strong>动态 Quorum</strong>：虽然 Basic Paxos 通常使用静态的、基于多数的 Quorum，但在更高级的系统中，有时会考虑动态调整 Quorum 配置（例如，增减节点）。但这本身就是一个需要通过共识来安全完成的操作，以避免在配置变更期间引入不一致性。</li></ul><h3 id="算法难点与挑战"><a href="#算法难点与挑战" class="headerlink" title="算法难点与挑战"></a>算法难点与挑战</h3><ol><li>活锁问题 ：多个 Proposer 可能会不断地相互抢占，导致没有提案能被选定。解决方法是引入随机延迟或选举一个主 Proposer。</li><li>恢复问题 ：节点故障恢复后如何获取最新状态。</li><li>多轮决议 ：实际系统中需要连续对多个值达成一致，需要引入实例编号。</li></ol><h1 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h1><p>下面是一个基于 Golang 版本的 Paxos 算法的简易实现。</p><h2 id="基础数据结构定义"><a href="#基础数据结构定义" class="headerlink" title="基础数据结构定义"></a>基础数据结构定义</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ProposalID 表示提案编号</span></span><br><span class="line"><span class="keyword">type</span> ProposalID <span class="keyword">struct</span> &#123;</span><br><span class="line">    Number <span class="type">int</span></span><br><span class="line">    NodeID <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Value 表示提案的值</span></span><br><span class="line"><span class="keyword">type</span> Value <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// PrepareRequest 表示准备请求</span></span><br><span class="line"><span class="keyword">type</span> PrepareRequest <span class="keyword">struct</span> &#123;</span><br><span class="line">    ProposalID ProposalID</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// PrepareResponse 表示准备响应</span></span><br><span class="line"><span class="keyword">type</span> PrepareResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">    ProposalID    ProposalID</span><br><span class="line">    AcceptedID    *ProposalID</span><br><span class="line">    AcceptedValue Value</span><br><span class="line">    Ok            <span class="type">bool</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// AcceptRequest 表示接受请求</span></span><br><span class="line"><span class="keyword">type</span> AcceptRequest <span class="keyword">struct</span> &#123;</span><br><span class="line">    ProposalID ProposalID</span><br><span class="line">    Value      Value</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// AcceptResponse 表示接受响应</span></span><br><span class="line"><span class="keyword">type</span> AcceptResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">    ProposalID ProposalID</span><br><span class="line">    Ok         <span class="type">bool</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Acceptor-实现"><a href="#Acceptor-实现" class="headerlink" title="Acceptor 实现"></a>Acceptor 实现</h2><p>Acceptor 是 Paxos 算法中的关键角色，负责接收和响应 Proposer 的请求：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> paxos</span><br><span class="line"></span><br><span class="line"><span class="comment">// Acceptor represents a node that can accept proposals in the Paxos protocol</span></span><br><span class="line"><span class="keyword">type</span> acceptor <span class="keyword">struct</span> &#123;</span><br><span class="line">    promisedID    *ProposalID</span><br><span class="line">    acceptedID    *ProposalID</span><br><span class="line">    acceptedValue Value</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewAcceptor creates a new Acceptor instance</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewAcceptor</span><span class="params">()</span></span> Acceptor &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;acceptor&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HandlePrepare processes a prepare request from a proposer</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *acceptor)</span></span> HandlePrepare(request PrepareRequest) PrepareResponse &#123;</span><br><span class="line">    <span class="comment">// If we haven&#x27;t promised anyone yet, or this proposal is higher than our promise</span></span><br><span class="line">    <span class="keyword">if</span> a.promisedID == <span class="literal">nil</span> || (request.ProposalID.Number &gt; a.promisedID.Number) &#123;</span><br><span class="line">        a.promisedID = &amp;request.ProposalID</span><br><span class="line">        <span class="keyword">return</span> PrepareResponse&#123;</span><br><span class="line">            ProposalID:    request.ProposalID,</span><br><span class="line">            AcceptedID:    a.acceptedID,</span><br><span class="line">            AcceptedValue: a.acceptedValue,</span><br><span class="line">            Ok:           <span class="literal">true</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Reject if the proposal number is less than what we&#x27;ve promised</span></span><br><span class="line">    <span class="keyword">return</span> PrepareResponse&#123;</span><br><span class="line">        ProposalID: request.ProposalID,</span><br><span class="line">        Ok:         <span class="literal">false</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HandleAccept processes an accept request from a proposer</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *acceptor)</span></span> HandleAccept(request AcceptRequest) AcceptResponse &#123;</span><br><span class="line">    <span class="comment">// Accept only if the proposal ID is greater than or equal to what we&#x27;ve promised</span></span><br><span class="line">    <span class="keyword">if</span> a.promisedID == <span class="literal">nil</span> || request.ProposalID.Number &gt;= a.promisedID.Number &#123;</span><br><span class="line">        a.promisedID = &amp;request.ProposalID</span><br><span class="line">        a.acceptedID = &amp;request.ProposalID</span><br><span class="line">        a.acceptedValue = request.Value</span><br><span class="line">        <span class="keyword">return</span> AcceptResponse&#123;</span><br><span class="line">            ProposalID: request.ProposalID,</span><br><span class="line">            Ok:         <span class="literal">true</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Reject if the proposal number is less than what we&#x27;ve promised</span></span><br><span class="line">    <span class="keyword">return</span> AcceptResponse&#123;</span><br><span class="line">        ProposalID: request.ProposalID,</span><br><span class="line">        Ok:         <span class="literal">false</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面的代码中，<code>promisedId</code> 是 Acceptor 承诺过的最高提案编号，<code>acceptedId</code> 和 <code>acceptedValue</code> 分别是已接受的提案编号和值。</p><h2 id="Proposer-实现"><a href="#Proposer-实现" class="headerlink" title="Proposer 实现"></a>Proposer 实现</h2><p>Proposer 负责发起提案并协商整个一致性过程：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> paxos</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&quot;</span></span><br><span class="line">    <span class="string">&quot;time&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Proposer represents a node that proposes values in the Paxos protocol</span></span><br><span class="line"><span class="keyword">type</span> proposer <span class="keyword">struct</span> &#123;</span><br><span class="line">    nodeID        <span class="type">string</span></span><br><span class="line">    proposalNum   <span class="type">int</span></span><br><span class="line">    acceptors     []Acceptor</span><br><span class="line">    quorumSize    <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewProposer creates a new Proposer instance</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewProposer</span><span class="params">(nodeID <span class="type">string</span>, acceptors []Acceptor)</span></span> Proposer &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;proposer&#123;</span><br><span class="line">        nodeID:      nodeID,</span><br><span class="line">        proposalNum: <span class="number">0</span>,</span><br><span class="line">        acceptors:   acceptors,</span><br><span class="line">        quorumSize:  <span class="built_in">len</span>(acceptors)/<span class="number">2</span> + <span class="number">1</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Propose attempts to get a value accepted by a majority of acceptors</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *proposer)</span></span> Propose(value Value) (<span class="type">bool</span>, Value) &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="comment">// Generate a new proposal ID</span></span><br><span class="line">        p.proposalNum++</span><br><span class="line">        proposalID := ProposalID&#123;</span><br><span class="line">            Number: p.proposalNum,</span><br><span class="line">            NodeID: p.nodeID,</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// Phase 1: Prepare</span></span><br><span class="line">        prepareResponses := p.sendPrepareRequests(proposalID)</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// Check if we got a majority of OKs</span></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(prepareResponses) &lt; p.quorumSize &#123;</span><br><span class="line">            <span class="comment">// Wait a bit before retrying to avoid live-lock</span></span><br><span class="line">            time.Sleep(time.Duration(rand.Intn(<span class="number">100</span>)) * time.Millisecond)</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// If any acceptor already accepted a value, we must use the value</span></span><br><span class="line">        <span class="comment">// with the highest proposal number</span></span><br><span class="line">        highestAcceptedID := p.findHighestAcceptedProposal(prepareResponses)</span><br><span class="line">        valueToPropose := value</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> highestAcceptedID != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> _, resp := <span class="keyword">range</span> prepareResponses &#123;</span><br><span class="line">                <span class="keyword">if</span> resp.AcceptedID != <span class="literal">nil</span> &amp;&amp; </span><br><span class="line">                   resp.AcceptedID.Number == highestAcceptedID.Number &amp;&amp;</span><br><span class="line">                   resp.AcceptedID.NodeID == highestAcceptedID.NodeID &#123;</span><br><span class="line">                    valueToPropose = resp.AcceptedValue</span><br><span class="line">                    <span class="keyword">break</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// Phase 2: Accept</span></span><br><span class="line">        acceptResponses := p.sendAcceptRequests(proposalID, valueToPropose)</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// Check if we got a majority of OKs</span></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(acceptResponses) &gt;= p.quorumSize &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>, valueToPropose</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// Wait a bit before retrying to avoid live-lock</span></span><br><span class="line">        time.Sleep(time.Duration(rand.Intn(<span class="number">100</span>)) * time.Millisecond)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// sendPrepareRequests sends prepare requests to all acceptors</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *proposer)</span></span> sendPrepareRequests(proposalID ProposalID) []PrepareResponse &#123;</span><br><span class="line">    <span class="keyword">var</span> okResponses []PrepareResponse</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> _, acceptor := <span class="keyword">range</span> p.acceptors &#123;</span><br><span class="line">        resp := acceptor.HandlePrepare(PrepareRequest&#123;ProposalID: proposalID&#125;)</span><br><span class="line">        <span class="keyword">if</span> resp.Ok &#123;</span><br><span class="line">            okResponses = <span class="built_in">append</span>(okResponses, resp)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> okResponses</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// sendAcceptRequests sends accept requests to all acceptors</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *proposer)</span></span> sendAcceptRequests(proposalID ProposalID, value Value) []AcceptResponse &#123;</span><br><span class="line">    <span class="keyword">var</span> okResponses []AcceptResponse</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> _, acceptor := <span class="keyword">range</span> p.acceptors &#123;</span><br><span class="line">        resp := acceptor.HandleAccept(AcceptRequest&#123;</span><br><span class="line">            ProposalID: proposalID,</span><br><span class="line">            Value:      value,</span><br><span class="line">        &#125;)</span><br><span class="line">        <span class="keyword">if</span> resp.Ok &#123;</span><br><span class="line">            okResponses = <span class="built_in">append</span>(okResponses, resp)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> okResponses</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// findHighestAcceptedProposal finds the highest proposal ID among accepted proposals</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *proposer)</span></span> findHighestAcceptedProposal(responses []PrepareResponse) *ProposalID &#123;</span><br><span class="line">    <span class="keyword">var</span> highest *ProposalID</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> _, resp := <span class="keyword">range</span> responses &#123;</span><br><span class="line">        <span class="keyword">if</span> resp.AcceptedID != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> highest == <span class="literal">nil</span> || </span><br><span class="line">               resp.AcceptedID.Number &gt; highest.Number ||</span><br><span class="line">               (resp.AcceptedID.Number == highest.Number &amp;&amp; </span><br><span class="line">                resp.AcceptedID.NodeID &gt; highest.NodeID) &#123;</span><br><span class="line">                highest = resp.AcceptedID</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> highest</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Learner-实现"><a href="#Learner-实现" class="headerlink" title="Learner 实现"></a>Learner 实现</h2><p>Learner 不参与提案，它只学习被批准的提案。Learner 接收来自 Acceptor 的消息，当收到来自多数（一个 Quorum）Acceptor 的关于同一个提议（相同编号和值）的 Accepted 消息时，Learner 就知道这个值已经被选定，随之可以将选定的值应用于本地状态或通知客户端。</p><p>注意：Learner 的失败通常不影响 Paxos 协议达成共识的过程，但可能会影响最终结果被知晓的范围。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Learner represents a node that learns the agreed-upon value in the Paxos protocol</span></span><br><span class="line"><span class="keyword">type</span> learner <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu            sync.RWMutex</span><br><span class="line">    learnedValue  Value</span><br><span class="line">    learnedID     *ProposalID</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewLearner creates a new Learner instance</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewLearner</span><span class="params">()</span></span> Learner &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;learner&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Learn records a value that has been accepted by a quorum of acceptors</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l *learner)</span></span> Learn(proposalID ProposalID, value Value) &#123;</span><br><span class="line">    l.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> l.mu.Unlock()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Only learn if this is a newer proposal than what we&#x27;ve already learned</span></span><br><span class="line">    <span class="keyword">if</span> l.learnedID == <span class="literal">nil</span> || proposalID.Number &gt; l.learnedID.Number &#123;</span><br><span class="line">        l.learnedValue = value</span><br><span class="line">        l.learnedID = &amp;proposalID</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetLearnedValue returns the most recently learned value</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l *learner)</span></span> GetLearnedValue() Value &#123;</span><br><span class="line">    l.mu.RLock()</span><br><span class="line">    <span class="keyword">defer</span> l.mu.RUnlock()</span><br><span class="line">    <span class="keyword">return</span> l.learnedValue</span><br></pre></td></tr></table></figure><h1 id="总结一下"><a href="#总结一下" class="headerlink" title="总结一下"></a>总结一下</h1><p>Paxos 算法是分布式计算领域的一项里程碑式的成就，它为在不可靠网络和节点故障环境中实现数据一致性这一核心挑战提供了第一个经过严格证明的解决方案。通过其精巧的两阶段协议和 Quorum 机制，Paxos 保证了即使在部分失败的情况下，系统也能就某个值达成唯一的共识，这对于构建可靠的分布式数据库、配置管理系统和状态机复制至关重要。</p><p>然而，Paxos 的强大功能也伴随着其众所周知的复杂性。其原始论文《The Part-Time Parliament》独特的叙事风格，以及算法本身在并发和故障场景下的微妙交互，使得 Paxos 的理解和正确实现都相当困难。正如本文在实现部分所强调的，状态管理（尤其是 Acceptor 状态的持久化）和并发控制是实现 Paxos 时需要特别注意的关键点，任何疏忽都可能破坏其安全性保证。</p><p>Basic Paxos 在理论上优先保证安全性，但牺牲了活性的保证，这导致在实践中几乎必须引入 Leader 选举机制来避免活锁并提高性能。这种对 Leader 的依赖，以及 Basic Paxos 处理一系列决策时的效率问题（每轮决策都需要完整的两阶段），催生了 Paxos 的多种变种：</p><ul><li>Multi-Paxos：通过选举稳定 Leader 并允许 Leader 跳过 Prepare 阶段来优化连续决策的效率，是实践中最常用的形式。</li><li>Fast Paxos：旨在减少达成共识所需的网络延迟，允许在某些条件下（无冲突时）跳过 Prepare 阶段，用一轮消息完成共识，但可能增加冲突处理的复杂性。</li><li>Cheap Paxos：着眼于减少维持系统容错性所需的活跃 Acceptor 数量，通过使用备份 Acceptor 来降低常规操作的资源消耗。</li></ul><p>这些变种的存在本身就说明了原始 Paxos 设计中固有的权衡（例如，简单性 vs 效率，理论保证 vs 实践性能）。此外，对 Paxos 理解难度的普遍认同，也直接推动了旨在提供类似一致性保证但更易于理解和实现的替代算法的开发，其中最著名的就是 Raft。Raft 通过更明确地定义 Leader 角色、状态转换和日志复制机制，降低了工程师理解和实施共识算法的门槛。（后面我可能会做一下 6.824 中 Raft 的 lab，到时候可能会专门写一篇 Raft 的文章，先挖个坑吧）</p><p>总而言之，Paxos 算法以其开创性的理论贡献和在构建健壮分布式系统中的实际应用价值，奠定了其在计算机科学中的重要地位。尽管其复杂性促使了变种和替代方案的发展，但理解 Paxos 的核心原理——角色、阶段、Quorum 和安全保证——对于任何深入研究或构建分布式系统的工程师和研究人员来说，仍然是不可或缺的基础。</p><p>(全文完)</p><h2 id="Refrence"><a href="#Refrence" class="headerlink" title="Refrence"></a>Refrence</h2><ol><li>Lamport, L. (1998). The Part-Time Parliament. ACM Transactions on Computer Systems (TOCS), 16(2), 133-169.</li><li>Lamport, L. (2001). Paxos Made Simple. ACM SIGACT News (Distributed Computing Column), 32(4), 51-58.</li><li>Fischer, M. J., Lynch, N. A., &amp; Paterson, M. S. (1985). Impossibility of distributed consensus with one faulty process. Journal of the ACM (JACM), 32(2), 374-382.</li><li>Ongaro, D., &amp; Ousterhout, J. (2014). In Search of an Understandable Consensus Algorithm. 2014 USENIX Annual Technical Conference (USENIX ATC 14), 305-319.  (Raft 论文，作为 Paxos 的重要对比和后续发展)</li><li>Schneider, F. B. (1990). Implementing fault-tolerant services using the state machine approach: A tutorial. ACM Computing Surveys (CSUR), 22(4), 299-319.  (关于状态机复制的重要背景文献)</li><li><a href="https://time.geekbang.org/column/article/201700">https://time.geekbang.org/column/article/201700</a>, <a href="https://time.geekbang.org/column/article/202772">https://time.geekbang.org/column/article/202772</a> (极客时间 - 分布式协议与算法实战)</li></ol>]]></content>
    
    
    <summary type="html">分布式共识算法-paxos</summary>
    
    
    
    <category term="分布式系统文章合集" scheme="https://cczywyc.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="MIT-6.824" scheme="https://cczywyc.com/tags/MIT-6-824/"/>
    
    <category term="分布式系统" scheme="https://cczywyc.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>KV Server</title>
    <link href="https://cczywyc.com/2024/09/17/KV-server/"/>
    <id>https://cczywyc.com/2024/09/17/KV-server/</id>
    <published>2024-09-17T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>MIT 6.824 课程的第二个 lab 是实现一个 key&#x2F;value server，这里是官网的 <a href="http://nil.csail.mit.edu/6.5840/2024/labs/lab-kvsrv.html">实验说明</a>。本篇文章是关于 key&#x2F;value server 实验设计思路及实现过程。</p><h1 id="实验概述"><a href="#实验概述" class="headerlink" title="实验概述"></a>实验概述</h1><p>本实验要求实现一个单机版的 kv server，支持 <code>Put</code>、<code>Append</code> 和 <code>Get</code> 三种操作。该服务保证所有的操作都必须是线性化（linearizable）的，确保操作顺序符合实时顺序，并且能够处理网络故障（例如消息丢失），保证操作只执行一次。实验目标是能够满足不同客户端以及不可靠网络等场景，确保服务器在并发和故障情况下都能够正常工作。</p><h1 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h1><ul><li>线性化：通过使用 Go 的互斥锁（mutex）保护共享数据，确保并发访问时操作顺序一致。</li><li>网络故障处理：客户端为每个请求分配唯一序列号（sequence number），服务器通过客户端唯一标识（Client ID）和请求序列号检测重复请求，确保幂等性。</li><li>操作定义：<ul><li>Put(key, value): 将键 <code>key</code> 的值设置为 <code>value</code>，覆盖原有值。需确保线程安全，并记录序列号以防重复。</li><li>Append(key, arg): 向键对应的值增加内容，并返回旧值，若键不存在则视为对空字符串的追加。</li><li>Get(key): 获取键的值，若键不存在返回空字符串。由于 Get 是只读操作，重复执行不会影响状态，无需特别处理序列号。</li></ul></li></ul><h1 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h1><h2 id="核心数据结构设计"><a href="#核心数据结构设计" class="headerlink" title="核心数据结构设计"></a>核心数据结构设计</h2><h3 id="键值对存储"><a href="#键值对存储" class="headerlink" title="键值对存储"></a>键值对存储</h3><p>键值对存储核心的数据结构选择 <code>map</code>，并配合互斥锁（<code>sync.Mutex</code>）保证并发安全</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> KVServer <span class="keyword">struct</span> &#123;</span><br><span class="line">mu         sync.Mutex</span><br><span class="line">store      <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span></span><br><span class="line">clientSeqs <span class="keyword">map</span>[<span class="type">int64</span>]<span class="type">int</span> <span class="comment">// to track the latest sequence number for each client</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>store: 存储键值对</li><li>clientSeqs：记录每个客户端已经处理过的序列号，用于检测重复请求。</li></ul><h3 id="操作原子性"><a href="#操作原子性" class="headerlink" title="操作原子性"></a>操作原子性</h3><p>通过互斥锁保护共享资源的访问：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Get fetches the current value for the key.</span></span><br><span class="line"><span class="comment">// A Get for a non-existing key should return an empty string.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(kv *KVServer)</span></span> Get(args *GetArgs, reply *GetReply) &#123;</span><br><span class="line">kv.mu.Lock()</span><br><span class="line"><span class="keyword">defer</span> kv.mu.Unlock()</span><br><span class="line">  <span class="comment">// 处理 Get 逻辑</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>每个操作在处理前先获取锁，确保同一时刻只有一个协程修改数据。</p><h3 id="处理重复请求"><a href="#处理重复请求" class="headerlink" title="处理重复请求"></a>处理重复请求</h3><p>为每个客户端分配唯一 <code>ClientId</code>，每次请求携带递增的 Seq。服务器通过 <code>clientSeqs</code> 记录已处理的请求：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> args.SeqNumber &lt;= kv.clientSeqs[args.ClientId] &#123;</span><br><span class="line"><span class="comment">// Repeat the request and return the historical value</span></span><br><span class="line">reply.PreviousValue = kv.store[args.Key]</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">......</span><br><span class="line"></span><br><span class="line"><span class="comment">// update the request sequence number for the client</span></span><br><span class="line">kv.clientSeqs[args.ClientId] = args.SeqNumber</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="关键步骤代码"><a href="#关键步骤代码" class="headerlink" title="关键步骤代码"></a>关键步骤代码</h2><h3 id="Get"><a href="#Get" class="headerlink" title="Get"></a>Get</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// client.go</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ck *Clerk)</span></span> Get(key <span class="type">string</span>) <span class="type">string</span> &#123;</span><br><span class="line">args := GetArgs&#123;</span><br><span class="line">Key:       key,</span><br><span class="line">ClientId:  ck.clientId,</span><br><span class="line">SeqNumber: ck.seqNumber,</span><br><span class="line">&#125;</span><br><span class="line">reply := GetReply&#123;&#125;</span><br><span class="line">ck.seqNumber++</span><br><span class="line"></span><br><span class="line"><span class="comment">// send the Get RPC request</span></span><br><span class="line">ok := ck.server.Call(<span class="string">&quot;KVServer.Get&quot;</span>, &amp;args, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> ok &#123;</span><br><span class="line"><span class="keyword">return</span> reply.Value</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// server.go</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Get fetches the current value for the key.</span></span><br><span class="line"><span class="comment">// A Get for a non-existing key should return an empty string.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(kv *KVServer)</span></span> Get(args *GetArgs, reply *GetReply) &#123;</span><br><span class="line">kv.mu.Lock()</span><br><span class="line"><span class="keyword">defer</span> kv.mu.Unlock()</span><br><span class="line"></span><br><span class="line">value, exists := kv.store[args.Key]</span><br><span class="line"><span class="keyword">if</span> !exists &#123;</span><br><span class="line">reply.Value = <span class="string">&quot;&quot;</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">reply.Value = value</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Put-Append"><a href="#Put-Append" class="headerlink" title="Put &amp;&amp; Append"></a>Put &amp;&amp; Append</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// client.go</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ck *Clerk)</span></span> PutAppend(key <span class="type">string</span>, value <span class="type">string</span>, op <span class="type">string</span>) <span class="type">string</span> &#123;</span><br><span class="line">args := PutAppendArgs&#123;</span><br><span class="line">Key:       key,</span><br><span class="line">Value:     value,</span><br><span class="line">Op:        op,</span><br><span class="line">ClientId:  ck.clientId,</span><br><span class="line">SeqNumber: ck.seqNumber,</span><br><span class="line">&#125;</span><br><span class="line">reply := PutAppendReply&#123;&#125;</span><br><span class="line">ck.seqNumber++</span><br><span class="line"></span><br><span class="line"><span class="comment">// send the RPC request</span></span><br><span class="line">ok := ck.server.Call(<span class="string">&quot;KVServer.&quot;</span>+op, &amp;args, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> ok &#123;</span><br><span class="line"><span class="keyword">return</span> reply.PreviousValue</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ck *Clerk)</span></span> Put(key <span class="type">string</span>, value <span class="type">string</span>) &#123;</span><br><span class="line">ck.PutAppend(key, value, <span class="string">&quot;Put&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Append value to key&#x27;s value and return that value</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ck *Clerk)</span></span> Append(key <span class="type">string</span>, value <span class="type">string</span>) <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ck.PutAppend(key, value, <span class="string">&quot;Append&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// server.go</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Put installs or replaces the value for a particular key in the map.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(kv *KVServer)</span></span> Put(args *PutAppendArgs, reply *PutAppendReply) &#123;</span><br><span class="line">kv.mu.Lock()</span><br><span class="line"><span class="keyword">defer</span> kv.mu.Unlock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> args.SeqNumber &lt;= kv.clientSeqs[args.ClientId] &#123;</span><br><span class="line"><span class="comment">// Repeat the request and return the historical value</span></span><br><span class="line">reply.PreviousValue = kv.store[args.Key]</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">kv.store[args.Key] = args.Value</span><br><span class="line">reply.PreviousValue = kv.store[args.Key]</span><br><span class="line"></span><br><span class="line"><span class="comment">// update the request sequence number for the client</span></span><br><span class="line">kv.clientSeqs[args.ClientId] = args.SeqNumber</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Append appends arg to key&#x27;s value and returns the old value.</span></span><br><span class="line"><span class="comment">// An Append to a non-existing key should act as if the existing value were a zero-length string.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(kv *KVServer)</span></span> Append(args *PutAppendArgs, reply *PutAppendReply) &#123;</span><br><span class="line">kv.mu.Lock()</span><br><span class="line"><span class="keyword">defer</span> kv.mu.Unlock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> args.SeqNumber &lt;= kv.clientSeqs[args.ClientId] &#123;</span><br><span class="line">reply.PreviousValue = kv.store[args.Key]</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// find the key if exist</span></span><br><span class="line">value, exists := kv.store[args.Key]</span><br><span class="line"><span class="keyword">if</span> !exists &#123;</span><br><span class="line"><span class="comment">// a zero-length string</span></span><br><span class="line">value = <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// append the value</span></span><br><span class="line">kv.store[args.Key] = value + args.Value</span><br><span class="line">reply.PreviousValue = value</span><br><span class="line"></span><br><span class="line"><span class="comment">// update the request sequence number for the client</span></span><br><span class="line">kv.clientSeqs[args.ClientId] = args.SeqNumber</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本实验是一个比较简单的分布式系统的入门实验。它初步提出了分布式系统中的数据一致性问题以及容错处理，在实现过程中需要考虑线性化、客户端的重复请求和网络故障处理等场景，实现较为简单，因此本文篇幅也相对较短。完整代码见本项目 <a href="https://github.com/cczywyc/mit-6.5840/tree/main/src/kvsrv">Github 仓库</a>。</p><h1 id="Refrence"><a href="#Refrence" class="headerlink" title="Refrence"></a>Refrence</h1><ol><li><a href="http://nil.csail.mit.edu/6.5840/2024/labs/lab-kvsrv.html">http://nil.csail.mit.edu/6.5840/2024/labs/lab-kvsrv.html</a></li></ol>]]></content>
    
    
    <summary type="html">MIT-6.824实验二详解</summary>
    
    
    
    <category term="分布式系统文章合集" scheme="https://cczywyc.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="MIT-6.824" scheme="https://cczywyc.com/tags/MIT-6-824/"/>
    
    <category term="分布式系统" scheme="https://cczywyc.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>MapReduce</title>
    <link href="https://cczywyc.com/2024/08/25/MapReduce/"/>
    <id>https://cczywyc.com/2024/08/25/MapReduce/</id>
    <published>2024-08-25T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>MapReduce 是一种处理大量数据的编程模型以及实现，它使用 Map 和 Reduce 两个函数对于海量数据计算做了一次抽象，用于在集群上使用分布式算法处理和生成大数据集。MapReduce 最先由 Google 在 2004 年提出，并且在内部得到了大量的实践，简单说来，MapReduce 模型由一个 Map 函数处理一个基于 key&#x2F;value pair 的数据集合，输出中间的基于 key&#x2F;value pair 的数据集合，然后再由一个 Reduce 函数用来合并所有的具有相同中间 key 值的中间 value 值。</p><p>仔细分析发现，现实世界中有很多满足上述处理模型的例子，关于 MapReduce 的产生背景和详细的介绍本文不再详细叙述，强烈建议阅读 Google 的<a href="https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf">论文</a>，一定会对 MapReduce 模型有更进一步的理解。</p><p>说起分布式系统，大家应该都知道 MIT 有一个明星课程 6.824，它是专门讲解分布式系统的，这个课程还设置了一系列非常有难度的实验来检验你对课程的掌握。为了进一步加深我对分布式系统的理解，前段时间我便开始完成 2024 年春季 6.824 的 5 个实验 lab（备注：6.824 以前是 4 个实验 lab，从 2024 年春季课程开始，拆分成了 5 个，但是 lab 的大体内容都是一样的，并且现在课程改名为 6.5840，其实都是一样的），本篇文章就是 MapReduce 实验的实现详解文章，以此记录实现步骤和思考过程。</p><p>6.824 历年所有的课程安排、实验 lab 和示例代码都可以在 <a href="https://pdos.csail.mit.edu/6.824/">课程官网</a> 找到。</p><h1 id="实验前准备"><a href="#实验前准备" class="headerlink" title="实验前准备"></a>实验前准备</h1><p>关于实验前的准备，这里我以 2024 春季课程为例。</p><ol><li>本实验是基于 Go 语言来完成的，首先需要安装 Go 的开发环境；</li><li>clone 实验代码，本实验运行的 main 函数已经编写好，我们只需要自己实现 Coordinator 和 Worker 以及 RPC 模块，代码仓库地址和实验标准也可以在 <a href="https://pdos.csail.mit.edu/6.824/labs/lab-mr.html">官网</a> 找到。</li></ol><h1 id="实验详解"><a href="#实验详解" class="headerlink" title="实验详解"></a>实验详解</h1><p>本实验中我们需要实现一个分布式的 MapReduce 来完成统计多个文件中每个单词出现的次数多任务。这个分布式的 MapReduce 是由一个 Coordinator 进程 和多个 Worker 进程共同完成任务。我们要完成这个任务分为两步，第一步是执行 Map 任务，第二步是执行 Reduce 任务。</p><h2 id="执行概括"><a href="#执行概括" class="headerlink" title="执行概括"></a>执行概括</h2><p>通过将 Map 调用的输入数据自动分割为 M 个数据片段的集合，Map 调用被分布到多台机器上执行。输入的数据片段能够在不同的机器上并行处理。使用分区函数将 Map 调用产生的中间 key 值分成 R 个不同分区（例如本实验中的 hash(key) mod R），Reduce 调用也被分布到多台机器上执行。分区数量（R）和分区函数由用户指定。在本实验中，R 从主函数中当作参数传入，为 10。下面是 MapReduce 执行概览图。</p><p><img src="https://img.cczywyc.com/map-reduce/mapReduce_execution_overview.png"></p><p>在 MapReduce 模型中，有一个特殊的程序叫 Coordinator（也叫做 Master），其他的程序都是 Worker 程序，由 Coordinator 分配任务给 Worker。待分配的任务被分成了两类，一类是 Map 任务，一类是 Reduce 任务，Coordinator 先将 Map 任务分配给空闲的 Worker，并记录 Worker 进程和 Map 任务的相关信息和执行结果，等所有的 Map 任务都执行结束后开始分配 Reduce 任务给空闲的 Worker，直至所有的任务结束，程序退出。</p><h2 id="程序数据结构设计"><a href="#程序数据结构设计" class="headerlink" title="程序数据结构设计"></a>程序数据结构设计</h2><h3 id="Coordinator"><a href="#Coordinator" class="headerlink" title="Coordinator"></a>Coordinator</h3><p>Coordinator 进程只有一个，它负责任务的调度，并且记录每一个任务的状态（等待调度、执行中或已完成），同时 Cootdinator 还应该记录每个 Worker 机器（进程）的标识。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Phase <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">MapPhase Phase = <span class="literal">iota</span> + <span class="number">1</span></span><br><span class="line">ReducePhase</span><br><span class="line">Done</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> TaskType <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">MapTask    TaskType = <span class="literal">iota</span> + <span class="number">1</span></span><br><span class="line">ReduceTask          <span class="comment">// reduce task</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> TaskStatus <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">Waiting TaskStatus = <span class="literal">iota</span> + <span class="number">1</span></span><br><span class="line">Running</span><br><span class="line">Finished</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">Id       <span class="type">int</span>        <span class="comment">// the map task or reduce task id</span></span><br><span class="line">WorkName <span class="type">string</span>     <span class="comment">// the worker name</span></span><br><span class="line">TaskType TaskType   <span class="comment">// the task type, map or reduce</span></span><br><span class="line">Status   TaskStatus <span class="comment">// the task state</span></span><br><span class="line">Input    []<span class="type">string</span>   <span class="comment">// task input files, map task is only one input file</span></span><br><span class="line">Output   []<span class="type">string</span>   <span class="comment">// task output files, reduce task is only one output file</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> BaseInfo <span class="keyword">struct</span> &#123;</span><br><span class="line">nReduce   <span class="type">int</span> <span class="comment">// the total number of reduce tasks</span></span><br><span class="line">taskMap   <span class="keyword">map</span>[TaskType][]*Task</span><br><span class="line">workerMap <span class="keyword">map</span>[<span class="type">string</span>]*WorkInfo</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> WorkInfo <span class="keyword">struct</span> &#123;</span><br><span class="line">name           <span class="type">string</span></span><br><span class="line">lastOnlineTime time.Time</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Coordinator <span class="keyword">struct</span> &#123;</span><br><span class="line">phase    Phase</span><br><span class="line">baseInfo *BaseInfo</span><br><span class="line">timer    <span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line">mutex    sync.Mutex</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Worker"><a href="#Worker" class="headerlink" title="Worker"></a>Worker</h3><p>Worker 进程有一个或者多个，在真实的 MapReduce 场景中，Worker 进程往往分布在不同的机器上执行以提高任务执行的效率，在本实验中所有的 Worker 进程都在一个机器上完成，可以通过起多个 Worker 进程来模拟，不过我的程序依然是按照多机器来设计的。</p><p>Worker 进程负责具体执行任务，它通过 RPC 和 Coordinator 通信。在本实验中，空闲的Worker 进程不断地向 Coordinator 发送请求获取任务，直至 MapReduce 程序退出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> WorkerS <span class="keyword">struct</span> &#123;</span><br><span class="line">name    <span class="type">string</span></span><br><span class="line">mapF    <span class="function"><span class="keyword">func</span><span class="params">(<span class="type">string</span>, <span class="type">string</span>)</span></span> []KeyValue</span><br><span class="line">reduceF <span class="function"><span class="keyword">func</span><span class="params">(<span class="type">string</span>, []<span class="type">string</span>)</span></span> <span class="type">string</span></span><br><span class="line">workDir <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// KeyValue is the key/value pire of the map functions</span></span><br><span class="line"><span class="keyword">type</span> KeyValue <span class="keyword">struct</span> &#123;</span><br><span class="line">Key   <span class="type">string</span></span><br><span class="line">Value <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ByKey is for sorting by key</span></span><br><span class="line"><span class="keyword">type</span> ByKey []KeyValue</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a ByKey)</span></span> Len() <span class="type">int</span>           &#123; <span class="keyword">return</span> <span class="built_in">len</span>(a) &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a ByKey)</span></span> Swap(i, j <span class="type">int</span>)      &#123; a[i], a[j] = a[j], a[i] &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a ByKey)</span></span> Less(i, j <span class="type">int</span>) <span class="type">bool</span> &#123; <span class="keyword">return</span> a[i].Key &lt; a[j].Key &#125;</span><br></pre></td></tr></table></figure><h3 id="RPC-设计"><a href="#RPC-设计" class="headerlink" title="RPC 设计"></a>RPC 设计</h3><p>前面说到，Coordinator 和 Worker 通过 RPC 通信，官方给出的 RPC 代码中使用了 Unix domain socket 建立连接，这里需要了解一下 Unix domain socket。</p><p>Unix domain socket 或者 IPC socket 是一种终端，可以使同一台操作系统上的两个或多个进程数据通信。与管道相比，Unix domain sockets 既可以使用字节流也可以使用数据队列，而管道只能使用字节流。Unix domain socket 的接口和Internet socket 很像，区别在于它不使用网络底层协议来通信，而是使用系统文件的地址来作为自己的身份。它可以被系统进程引用，因此两个进程可以同时打开一个 Unix domain sockets 来进行通信，不过这种通信方式是发生在系统内核里而不会在网络里传播。关于这种通信方式，下面是一个用 golang 实现的简易的服务端和客户端通信的 demo。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// server.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;io&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line">    <span class="string">&quot;net&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> file <span class="type">string</span> = <span class="string">&quot;test.sock&quot;</span> <span class="comment">//用于 unix domain socket 的文件</span></span><br><span class="line">start:</span><br><span class="line">    lis, err := net.Listen(<span class="string">&quot;unix&quot;</span>, file) <span class="comment">//开始监听</span></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;                      <span class="comment">//如果监听失败，一般是文件已存在，需要删除它</span></span><br><span class="line">            log.Println(<span class="string">&quot;UNIX Domain Socket 创 建失败，正在尝试重新创建 -&gt; &quot;</span>, err)</span><br><span class="line">            err = os.Remove(file)</span><br><span class="line">            <span class="keyword">if</span> err != <span class="literal">nil</span> &#123; <span class="comment">//如果删除文件失败 ，要么是权限问题，要么是之前监听不成功，不管是什么 都应该退出程序，不然后面 goto 就死循环了</span></span><br><span class="line">                    log.Fatalln(<span class="string">&quot;删除 sock 文件失败！程序退出 -&gt; &quot;</span>, err)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">goto</span> start <span class="comment">//删除文件后重新执行一次创建</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123; <span class="comment">//监听成功会直接执行本分支</span></span><br><span class="line">            fmt.Println(<span class="string">&quot;创建 UNIX Domain Socket 成功&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">defer</span> lis.Close() <span class="comment">//虽然本次操作不会执行， 不过还是加上比较好</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">            conn, err := lis.Accept() <span class="comment">//开始接 受数据</span></span><br><span class="line">            <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">                    log.Println(<span class="string">&quot;请求接收错误 -&gt; &quot;</span>, err)</span><br><span class="line">                    <span class="keyword">continue</span> <span class="comment">//一个连接错误，不会影响整体的稳定性，忽略就好</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">go</span> handle(conn) <span class="comment">//开始处理数据</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handle</span><span class="params">(conn net.Conn)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> conn.Close()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">            io.Copy(conn, conn) <span class="comment">//把发送的数据 转发回去</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// client.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;bufio&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line">    <span class="string">&quot;net&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    file := <span class="string">&quot;test.sock&quot;</span></span><br><span class="line">    conn, err := net.Dial(<span class="string">&quot;unix&quot;</span>, file) <span class="comment">//发起 请求</span></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            log.Fatal(err) <span class="comment">//如果发生错误，直接退出程序，因为请求失败所以不需要 close</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> conn.Close() <span class="comment">//习惯性的写上</span></span><br><span class="line"></span><br><span class="line">    input := bufio.NewScanner(os.Stdin) <span class="comment">//创建 一个读取输入的处理器</span></span><br><span class="line">    reader := bufio.NewReader(conn)     <span class="comment">//创建 一个读取网络的处理器</span></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">            fmt.Print(<span class="string">&quot;请输入需要发送的数据: &quot;</span>)       <span class="comment">//打印提示</span></span><br><span class="line">            input.Scan()                    <span class="comment">// 读取终端输入</span></span><br><span class="line">            data := input.Text()            <span class="comment">// 提取输入内容</span></span><br><span class="line">            conn.Write([]<span class="type">byte</span>(data + <span class="string">&quot;\n&quot;</span>)) <span class="comment">// 将输入的内容发送出去，需要将 string 转 byte 加 \n  作为读取的分割符</span></span><br><span class="line"></span><br><span class="line">            msg, err := reader.ReadString(<span class="string">&#x27;\n&#x27;</span>) <span class="comment">//读取对端的数据</span></span><br><span class="line">            <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">                    log.Println(err)</span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Println(msg) <span class="comment">//打印接收的消息</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们再来看官方给的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// server start a thread that listens for RPCs from worker.go</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Coordinator)</span></span> server() &#123;</span><br><span class="line">rpc.Register(c)</span><br><span class="line">rpc.HandleHTTP()</span><br><span class="line"><span class="comment">//l, e := net.Listen(&quot;tcp&quot;, &quot;:1234&quot;)</span></span><br><span class="line">sockname := coordinatorSock()</span><br><span class="line">os.Remove(sockname)</span><br><span class="line">l, e := net.Listen(<span class="string">&quot;unix&quot;</span>, sockname)</span><br><span class="line"><span class="keyword">if</span> e != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;listen error:&quot;</span>, e)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">go</span> http.Serve(l, <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// call send an RPC request to the coordinator, wait for the response.</span></span><br><span class="line"><span class="comment">// usually returns true, returns false if something goes wrong.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">call</span><span class="params">(rpcname <span class="type">string</span>, args <span class="keyword">interface</span>&#123;&#125;, reply <span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line"><span class="comment">// c, err := rpc.DialHTTP(&quot;tcp&quot;, &quot;127.0.0.1&quot;+&quot;:1234&quot;)</span></span><br><span class="line">sockname := coordinatorSock()</span><br><span class="line">c, err := rpc.DialHTTP(<span class="string">&quot;unix&quot;</span>, sockname)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;dialing:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> c.Close()</span><br><span class="line"></span><br><span class="line">err = c.Call(rpcname, args, reply)</span><br><span class="line"><span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Println(err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Cook up a unique-ish UNIX-domain socket name</span></span><br><span class="line"><span class="comment">// in /var/tmp, for the coordinator.</span></span><br><span class="line"><span class="comment">// Can&#x27;t use the current directory since</span></span><br><span class="line"><span class="comment">// Athena AFS doesn&#x27;t support UNIX-domain sockets.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">coordinatorSock</span><span class="params">()</span></span> <span class="type">string</span> &#123;</span><br><span class="line">s := <span class="string">&quot;/var/tmp/5840-mr-&quot;</span></span><br><span class="line">s += strconv.Itoa(os.Getuid())</span><br><span class="line"><span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到正是基于上述的通信方式。</p><h2 id="任务执行"><a href="#任务执行" class="headerlink" title="任务执行"></a>任务执行</h2><p>接着我们再来谈任务的执行。Worker 进程向 Coordinator 发送请求获取任务，Coordinator 找到空闲的任务，将其返回给 Worker，Worker 根据任务的类型和任务的参数执行任务，待任务结束后，Worker 告知 Coordinator，Coordinator 修改任务状态，继续分配下一个任务。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// worker.go</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Worker is called by main/mrworker.go</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Worker</span><span class="params">(mapf <span class="keyword">func</span>(<span class="type">string</span>, <span class="type">string</span>)</span></span> []KeyValue, reducef <span class="function"><span class="keyword">func</span><span class="params">(<span class="type">string</span>, []<span class="type">string</span>)</span></span> <span class="type">string</span>) &#123;</span><br><span class="line"><span class="comment">// get the current workspace path</span></span><br><span class="line">workDir, _ := os.Getwd()</span><br><span class="line">w := WorkerS&#123;</span><br><span class="line">name:    <span class="string">&quot;worker_&quot;</span> + strconv.Itoa(rand.Intn(<span class="number">100000</span>)),</span><br><span class="line">mapF:    mapf,</span><br><span class="line">reduceF: reducef,</span><br><span class="line">workDir: workDir,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// send the RPC to the coordinator for asking the task in a loop</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">reply := callGetTask(w.name)</span><br><span class="line"><span class="keyword">if</span> reply.Task == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="comment">// can not get the task, maybe all map tasks or all reduce task are running but not be finished</span></span><br><span class="line"><span class="comment">// waiting to the next phase</span></span><br><span class="line">time.Sleep(time.Second)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker: Receive the task: %v \n&quot;</span>, reply)</span><br><span class="line"><span class="keyword">var</span> err <span class="type">error</span></span><br><span class="line"><span class="keyword">switch</span> reply.Task.TaskType &#123;</span><br><span class="line"><span class="keyword">case</span> MapTask:</span><br><span class="line">err = w.doMap(reply)</span><br><span class="line"><span class="keyword">case</span> ReduceTask:</span><br><span class="line">err = w.doReduce(reply)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line"><span class="comment">// worker exit</span></span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker name: %s exit.\n&quot;</span>, w.name)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">callTaskDone(reply.Task)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// callGetTask send RPC request to coordinator for asking task</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">callGetTask</span><span class="params">(workName <span class="type">string</span>)</span></span> *GetTaskReply &#123;</span><br><span class="line">args := GetTaskArgs&#123;</span><br><span class="line">WorkerName: workName,</span><br><span class="line">&#125;</span><br><span class="line">reply := GetTaskReply&#123;&#125;</span><br><span class="line">ok := call(<span class="string">&quot;Coordinator.GetTask&quot;</span>, &amp;args, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: Coordinator.GetTask failed!\n&quot;</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> &amp;reply</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">callTaskDone</span><span class="params">(task *Task)</span></span> &#123;</span><br><span class="line">args := TaskDoneArgs&#123;</span><br><span class="line">Task: task,</span><br><span class="line">&#125;</span><br><span class="line">reply := TaskDoneReply&#123;&#125;</span><br><span class="line">ok := call(<span class="string">&quot;Coordinator.TaskDone&quot;</span>, &amp;args, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: Coordinator.TaskDone failed!\n&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先是 Map 任务。它负责读取初始文件，根据文件内容生成一系列的临时中间文件，这些临时的中间文件内容是一系列的 key&#x2F;value pair，其中 key 是单词，value 是次数，也就是 1。Map 任务的数量应当根据输入文件的多少适当拆分，本实验中为简单起见，约定一个文件一个 Map 任务，因此一共是 8 个 Map 任务。所有的 Map 任务执行成功后开始 Reduce 阶段，因此 MapReduce 程序需要标记当前处于 Map 阶段还是 Reduce 阶段，只有 Map 阶段结束，才会进入 Reduce 阶段。</p><h3 id="Map-任务"><a href="#Map-任务" class="headerlink" title="Map 任务"></a>Map 任务</h3><p>以本实验为例，Map 任务负责读取一个原始的文件的内容，调用 map 函数（注意 map 函数以插件的方式加载，本实验中不需要我们实现）生成 [word-1] 这种格式的临时文件。前面说到，为了方便起见，一个文件代表一个 Map 任务，每个 Map 任务生成的临时文件数量和分区数量 R 保持一致。关于 Map 产生的临时文件分配及命名规则，后面会单独描述，下面是 Worker 节点处理 Map 任务的逻辑。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// doMap execute the map task</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(w *WorkerS)</span></span> doMap(reply *GetTaskReply) <span class="type">error</span> &#123;</span><br><span class="line">task := reply.Task</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(task.Input) == <span class="number">0</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: task number %d: No input!\n&quot;</span>, task.Id)</span><br><span class="line"><span class="keyword">return</span> errors.New(<span class="string">&quot;map task no input&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker name: %s start execute number: %d map task \n&quot;</span>, w.name, task.Id)</span><br><span class="line"></span><br><span class="line">fileName := task.Input[<span class="number">0</span>]</span><br><span class="line">inputBytes, err := os.ReadFile(fileName)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: read map task input file error: %v \n&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// kv2ReduceMap: key: reduce index, value: key/value list. split the same key into reduce</span></span><br><span class="line">kv2ReduceMap := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">int</span>][]KeyValue, reply.NReduce)</span><br><span class="line"><span class="keyword">var</span> output []<span class="type">string</span></span><br><span class="line">outputFileNameFunc := <span class="function"><span class="keyword">func</span><span class="params">(idxReduce <span class="type">int</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;mr-%d-%d-temp-&quot;</span>, task.Id, idxReduce)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// call the map function</span></span><br><span class="line">kva := w.mapF(fileName, <span class="type">string</span>(inputBytes))</span><br><span class="line"><span class="keyword">for</span> _, kv := <span class="keyword">range</span> kva &#123;</span><br><span class="line">idxReduce := ihash(kv.Key) % reply.NReduce</span><br><span class="line">kv2ReduceMap[idxReduce] = <span class="built_in">append</span>(kv2ReduceMap[idxReduce], kv)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> idxReduce, item := <span class="keyword">range</span> kv2ReduceMap &#123;</span><br><span class="line"><span class="comment">// write to the temp file</span></span><br><span class="line">oFile, _ := os.CreateTemp(w.workDir, outputFileNameFunc(idxReduce))</span><br><span class="line">encoder := json.NewEncoder(oFile)</span><br><span class="line"><span class="keyword">for</span> _, kv := <span class="keyword">range</span> item &#123;</span><br><span class="line">err := encoder.Encode(kv)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: write map task output file error: %v \n&quot;</span>, err)</span><br><span class="line">_ = oFile.Close()</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// rename</span></span><br><span class="line">index := strings.Index(oFile.Name(), <span class="string">&quot;-temp&quot;</span>)</span><br><span class="line">_ = os.Rename(oFile.Name(), oFile.Name()[:index])</span><br><span class="line">output = <span class="built_in">append</span>(output, oFile.Name())</span><br><span class="line">_ = oFile.Close()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">task.Output = output</span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker name: %s finished the map task number: %d \n&quot;</span>, w.name, task.Id)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Reduce-任务"><a href="#Reduce-任务" class="headerlink" title="Reduce 任务"></a>Reduce 任务</h3><p>Reduce 任务发生在 Map 任务全部执行结束以后，负责执行 Reduce 操作。它处理 Map 任务产生的中间文件，调用 Reduce 函数输出结果到最终的文件中。以本实验为例，Reduce 任务找到其对应编号（具体规则下面会说）的临时文件，统计单词出现的次数，并将最终的结果输出到文件中，下面是 Worker 节点处理 Reduce 任务的逻辑。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// doReduce execute the reduce task</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(w *WorkerS)</span></span> doReduce(reply *GetTaskReply) <span class="type">error</span> &#123;</span><br><span class="line">task := reply.Task</span><br><span class="line"><span class="keyword">var</span> kva ByKey</span><br><span class="line"><span class="keyword">for</span> _, fileName := <span class="keyword">range</span> task.Input &#123;</span><br><span class="line">open, _ := os.Open(fileName)</span><br><span class="line">decoder := json.NewDecoder(open)</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line"><span class="keyword">var</span> kv KeyValue</span><br><span class="line"><span class="keyword">if</span> err := decoder.Decode(&amp;kv); err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Printf(<span class="string">&quot;[Error]: read reduce task input file error: %v \n&quot;</span>, err)</span><br><span class="line">_ = open.Close()</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">kva = <span class="built_in">append</span>(kva, kv)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// sort</span></span><br><span class="line">sort.Sort(kva)</span><br><span class="line"></span><br><span class="line"><span class="comment">// write to a temp file</span></span><br><span class="line">tempFile := fmt.Sprintf(<span class="string">&quot;mr-out-%d-temp-&quot;</span>, task.Id)</span><br><span class="line">oFile, _ := os.CreateTemp(w.workDir, tempFile)</span><br><span class="line"></span><br><span class="line">i := <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> i &lt; <span class="built_in">len</span>(kva) &#123;</span><br><span class="line">j := i + <span class="number">1</span></span><br><span class="line"><span class="keyword">for</span> j &lt; <span class="built_in">len</span>(kva) &amp;&amp; kva[j].Key == kva[i].Key &#123;</span><br><span class="line">j++</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> values []<span class="type">string</span></span><br><span class="line"><span class="keyword">for</span> k := i; k &lt; j; k++ &#123;</span><br><span class="line">values = <span class="built_in">append</span>(values, kva[k].Value)</span><br><span class="line">&#125;</span><br><span class="line">result := w.reduceF(kva[i].Key, values)</span><br><span class="line">_, _ = fmt.Fprintf(oFile, <span class="string">&quot;%v %v\n&quot;</span>, kva[j].Key, result)</span><br><span class="line"></span><br><span class="line">i = j</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// rename the reduce task output</span></span><br><span class="line">index := strings.Index(oFile.Name(), <span class="string">&quot;-temp&quot;</span>)</span><br><span class="line">_ = os.Rename(oFile.Name(), oFile.Name()[:index])</span><br><span class="line">_ = oFile.Close()</span><br><span class="line"></span><br><span class="line">task.Output = []<span class="type">string</span>&#123;oFile.Name()&#125;</span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker name: %s finished the reduce task number: %d \n&quot;</span>, w.name, task.Id)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="产生的文件"><a href="#产生的文件" class="headerlink" title="产生的文件"></a>产生的文件</h2><p>Map 任务和 Reduce 任务产生的文件这里单独来说。在 MIT 实验里对产生的文件名作了具体的要求：</p><ol><li>A reasonable naming convention for intermediate files is <code>mr-X-Y</code>, where X is the Map task number, and Y is the reduce task number.</li><li>The worker implementation should put the output of the X’th reduce task in the file <code>mr-out-X</code>.</li></ol><p>前面我们知道，每个 Map 都有对应的任务 ID，每一个原始文件对应一个 Map 任务，根据实验要求，每个 Map 分别输出 R 个临时文件，这里的 R 就是分区数量，即 Reduce 任务的数量。在 Map 任务中，对每个 key 作 hash，并与分区数量 R 取余，从而将不同的 key 散列到不同的 Reduce 任务上；Reduce 任务根据自身任务 ID 和散列的文件 ID 找到它需要处理的临时文件，这样就实现了相同的 key 必定由同一个 Reduce 处理的效果，从而确保了统计的准确性。最后每个 Reduce 根据其自身编号输出对应的文件。</p><p><img src="https://img.cczywyc.com/map-reduce/MapReduce_map_file.png"></p><p>假设一共有 4 个文本需要处理，对应 4 个 Map 任务，分区数量 R 是 9。那么按照规则编号为 1 的 Map 任务生成的中间临时文件便是 mr-1-1、mr-1-2、mr-1-3 …… mr-1-9，同样地，2 号 Map 任务生成的中间临时文件是 mr-2-1、mr-2-2、mr-2-3 …… mr-2-9。</p><p><img src="https://img.cczywyc.com/map-reduce/MapReduce_reduce_file.png"></p><p>可以看到，Reduce 阶段，编号为 1 的 Reduce 任务需要处理 Y &#x3D; 1 的临时文件，其他 Reduce 也是如此。</p><p>以上我们就把 Map 任务和 Reduce 产生的文件处理规则说清楚了。</p><h2 id="容错"><a href="#容错" class="headerlink" title="容错"></a>容错</h2><p>最后来说一下错误处理。</p><h3 id="Master-故障"><a href="#Master-故障" class="headerlink" title="Master 故障"></a>Master 故障</h3><p>论文中提到，处理 Master 失败一个简单的解决办法就是让 Master 节点周期性的将其数据结构写入磁盘，即形成一个检查点，如果 Master 节点异常，则可以从最后一个检查点重新启动 Master 进程，从而完成故障恢复。实验原因，本次并未实现 Master 节点的故障恢复。</p><h3 id="Worker-故障"><a href="#Worker-故障" class="headerlink" title="Worker 故障"></a>Worker 故障</h3><p>处理 Worker 节点故障可以单独起一个线程，让 Master 节点周期性地 ping 每个 Worker 节点，如果在一个约定的时间范围内 Master 没有收到 Worker 节点的心跳信息，则将其标记为失效。所有由这个失效的 worker 完成的 Map 任务被重设为初始状态，重新分配给其他的 Worker 节点。同样的，Worker 失效时正在运行的 Map 或 Reduce 任务也将被重置为空闲状态，重新等待分配执行。</p><p>当 Worker 故障时，由于已经完成的 Map 任务的输出存储在这台机器上，Map 任务的输出已经不可访问了，因此必须重新执行。而已经完成的 Reduce 任务的输出存储在全局文件系统上（例如 GFS），因此不需要再次执行。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// workerTimer create a timer that checks the worker online status every 1 second</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Coordinator)</span></span> workerTimer() &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">ticker := time.NewTicker(time.Second)</span><br><span class="line"><span class="keyword">defer</span> ticker.Stop()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> <span class="keyword">range</span> ticker.C &#123;</span><br><span class="line"><span class="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> &lt;-c.timer:</span><br><span class="line">log.Printf(<span class="string">&quot;[Info]: Worker timer exit. \n]&quot;</span>)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">c.mutex.Lock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, workInfo := <span class="keyword">range</span> c.baseInfo.workerMap &#123;</span><br><span class="line"><span class="keyword">if</span> time.Now().Sub(workInfo.lastOnlineTime) &lt;= <span class="number">10</span> &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// According to the MapReduce paper, in a real distributed system,</span></span><br><span class="line"><span class="comment">// since the intermediate files output by the Map task are stored on their respective worker nodes,</span></span><br><span class="line"><span class="comment">// when the worker is offline and cannot communicate, all map tasks executed by this worker,</span></span><br><span class="line"><span class="comment">// whether completed or not, should be reset to the initial state and reallocated to other workers,</span></span><br><span class="line"><span class="comment">// while the files output by the reduce task are stored on the global file system (GFS),</span></span><br><span class="line"><span class="comment">// and only unfinished tasks need to be reset and reallocated.</span></span><br><span class="line"><span class="keyword">if</span> c.phase == MapPhase &#123;</span><br><span class="line">mapTasks := c.baseInfo.taskMap[MapTask]</span><br><span class="line"><span class="keyword">for</span> _, task := <span class="keyword">range</span> mapTasks &#123;</span><br><span class="line"><span class="keyword">if</span> task.WorkName == workInfo.name &#123;</span><br><span class="line">task.Status = Waiting</span><br><span class="line">task.WorkName = <span class="string">&quot;&quot;</span></span><br><span class="line">task.Output = []<span class="type">string</span>&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> c.phase == ReducePhase &#123;</span><br><span class="line">reduceTasks := c.baseInfo.taskMap[ReduceTask]</span><br><span class="line"><span class="keyword">for</span> _, task := <span class="keyword">range</span> reduceTasks &#123;</span><br><span class="line"><span class="keyword">if</span> task.WorkName == workInfo.name &amp;&amp; task.Status == Running &#123;</span><br><span class="line">task.Status = Waiting</span><br><span class="line">task.WorkName = <span class="string">&quot;&quot;</span></span><br><span class="line">task.Output = []<span class="type">string</span>&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">delete</span>(c.baseInfo.workerMap, workInfo.name)</span><br><span class="line">&#125;</span><br><span class="line">c.mutex.Unlock()</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>注：本实验完整代码见我的 <a href="https://github.com/cczywyc/mit-6.5840">Github 仓库</a>。</p><p>(全文完)</p><h1 id="Refrence"><a href="#Refrence" class="headerlink" title="Refrence"></a>Refrence</h1><p><a href="http://nil.csail.mit.edu/6.5840/2024/labs/lab-mr.html">http://nil.csail.mit.edu/6.5840/2024/labs/lab-mr.html</a></p><p><a href="https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf">https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf</a></p>]]></content>
    
    
    <summary type="html">MIT-6.824实验一详解</summary>
    
    
    
    <category term="分布式系统文章合集" scheme="https://cczywyc.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="MIT-6.824" scheme="https://cczywyc.com/tags/MIT-6-824/"/>
    
    <category term="分布式系统" scheme="https://cczywyc.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>基于 WebRTC 实现一个文件传输工具（二）- 信令服务器的实现</title>
    <link href="https://cczywyc.com/2024/02/20/%E5%9F%BA%E4%BA%8EWebRTC%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%B7%A5%E5%85%B7%EF%BC%88%E4%BA%8C%EF%BC%89-%20%E4%BF%A1%E4%BB%A4%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    <id>https://cczywyc.com/2024/02/20/%E5%9F%BA%E4%BA%8EWebRTC%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%B7%A5%E5%85%B7%EF%BC%88%E4%BA%8C%EF%BC%89-%20%E4%BF%A1%E4%BB%A4%E6%9C%8D%E5%8A%A1%E5%99%A8/</id>
    <published>2024-02-20T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>上篇文章，详细介绍了 WebRTC 技术。我们知道，WebRTC 标准中并没有规定信令服务器的实现，它把这部分的实现交给了程序开发者本身，一方面基于 WebRTC 的应用形态和技术栈可能是多种多样的，这样不但保持了技术中立性，还最大限度的确保了技术的灵活性和开放性；另外一方面，协议的设计是极其复杂的，要兼顾的场景很多，这样也保证了技术标准的简洁性。因此，只要符合 WebRTC 的通信流程和架构设计理念，信令服务器的实现方案应该是非常灵活的。</p><p>本文是此系列文章的第二篇，介绍 WebRTC 中信令服务器的设计。</p><h1 id="信令介绍"><a href="#信令介绍" class="headerlink" title="信令介绍"></a>信令介绍</h1><p>在 WebRTC 的技术栈里，详细定义了一整套的规范，例如它的接口设计，使用 SDP 进行媒体协商、使用 ICE 收集网络地址并进行联通性检测等，这些规范使得客户端与客户端之间的数据交换变得非常容易和方便，但是我们发现 WebRTC 并没有对客户端建立点对点连接之前的过程定义规范，“空缺”的这部分，其实就是信令服务器在发挥作用的地方。</p><p>概括来说，WebRTC 信令是设备通信和交换点对点连接所需的信息的方式，包括交换应用程序之间的网络信息、协商媒体格式并处理通信会话的各个方面，因此在 WebRTC 的世界中，信令格外重要。</p><h1 id="信令服务器作用"><a href="#信令服务器作用" class="headerlink" title="信令服务器作用"></a>信令服务器作用</h1><p>上面介绍了 WebRTC 中的信令，我们知道，WebRTC 的核心是点对点通信，在客户端连接建立之前，我们需要一个公共的服务器来实现上述信息的交换剂，这便是信令服务器，具体说来，一个信令服务器应该具备以下作用：</p><ul><li>交换通信双方的网络信息：<ul><li>最常见的是交换通信双方的 IP 和端口；</li><li>两个 WebRTC 客户端之间会尽可能选择 P2P 进行通信。同一个局域网内，P2P 传输很容易实现；不同的局域网，需要借助网络穿透等技术手段实现 P2P 传输，若遇到网络穿透失败或者无法穿透的情况，则需要借助中转服务器实现数据传输。</li></ul></li><li>交换通信双方的媒体信息<ul><li>媒体信息用 SDP 表示，SDP 包含浏览器中的 RTP 媒体栈配置媒体会话所需要的全部信息，包括媒体类型(音频、视频、数据)，所用的编解码器(Oplus、G.711 等)、用于编解码器的各个参数或设置、以及有关宽带的信息；</li><li>信令通道也需要交换用于 SRTP 的密钥材料；</li><li>总结一下，SDP 可以简单理解为：媒体类型的编码是什么、是否支持对应的媒体类型和编码器、编码方式是什么等。</li></ul></li><li>业务层的功能管理<ul><li>创建会话、加入已存在的会话、退出会话等；</li><li>会话生命周期的管理和用户的管理等；</li><li>客户端的身份认证。</li></ul></li></ul><p>因为我打算使用 WebRTC 技术实现一个文件传输的工具，因此本文更多讨论的是在文件传输工具的 WebRTC 实现下信令服务器的设计与实现。</p><h1 id="WebRTC-信令工作流程"><a href="#WebRTC-信令工作流程" class="headerlink" title="WebRTC 信令工作流程"></a>WebRTC 信令工作流程</h1><p>一个 WebRTC 系统信令的工作流程图大概如下：</p><p><img src="https://img.cczywyc.com/signaling_workflow.png"></p><ul><li>客户端 A 和客户端 B 通过 WebSocket 等协议请求信令服务器；</li><li>客户端 A 与信令服务器之间的通信称为 offer-answer 机制，是 WebRTC 等一部分；</li><li>客户端 A 和客户端 B 可以视为两个对等点，对等点之间的连接是为了在设备之间建立通信，在这之前对等点之间必须通过 WebRTC 信令服务器进行连接前的通信。</li></ul><h1 id="架构设计"><a href="#架构设计" class="headerlink" title="架构设计"></a>架构设计</h1><p>介绍了信令服务器的作用后，我设计的文件传输工具信令服务器大体架构是这样：<img src="https://img.cczywyc.com/hyper-transfer-signaling.png"></p><p>架构整体结构不难，主要在于信令服务器对会话、用户等的管理，下面将详细说一下信令服务器这部分的管理功能。</p><h1 id="管理模块"><a href="#管理模块" class="headerlink" title="管理模块"></a>管理模块</h1><h2 id="用户和房间管理"><a href="#用户和房间管理" class="headerlink" title="用户和房间管理"></a>用户和房间管理</h2><p>信令服务器能够对用户进行管理。两个需要建立点对点连接的用户，要能够找到彼此，需要借助房间的概念，即两个用户加入同一个房间，在同一个房间的用户可以交换彼此的连接信息，从而建立彼此的点对点连接。</p><p>在传输过程中，用户和房间的连接信息由信令服务器维护。再进一步扩展，信令服务器应当支撑多个 WebRTC 传输环境，即多个房间，且房间之间互不影响；每个房间参与的人数也应该不受限制，信令服务器应当支持多个用户可以进入同一个房间，可以选择对房间内特定的用户传输文件或者对房间内所有的用户传输文件。</p><p><img src="https://img.cczywyc.com/signaling-user_room.png"></p><h2 id="会话管理"><a href="#会话管理" class="headerlink" title="会话管理"></a>会话管理</h2><ul><li>信令服务器支持创建一个新的会话，允许客户端通过信令服务器交换剂网络信息和媒体信息；</li><li>信令服务器还应当管理现有会话，允许新的用户加入现有的会话；</li><li>不同的会话之前应该互不影响、相互隔离。</li></ul><h2 id="信令传输方式与协议"><a href="#信令传输方式与协议" class="headerlink" title="信令传输方式与协议"></a>信令传输方式与协议</h2><p>信令传输协议比较灵活，理论上可以选择多种通信协议。但是实际使用时，我们一般选择 TCP 或者基于 TCP 的 HTTP&#x2F;HTTPS、WS&#x2F;WSS 等应用层协议作为信令服务器的通信协议。</p><h3 id="HTTP-传输"><a href="#HTTP-传输" class="headerlink" title="HTTP 传输"></a>HTTP 传输</h3><ul><li>浏览器可以发起新的 HTTP 请求，以便向服务器发送信令信息并从中接受信令消息；</li><li>可以通过轮询或者 get 请求来从服务器获取信令信息</li></ul><p>不难看出，通过 HTTP 协议的方式有非常明显的弊端，当房间新加入用户时，信令服务器没办法主动通知房间中已经存在的用户，即传统的 HTTP 协议没办法从服务端主动向客户端推送数据。</p><p>当然，这种弊端也是可以通过技术手段解决的，例如升级 HTTP2 或者客户端不断轮询请求信令服务器。</p><h3 id="WebSocket-传输"><a href="#WebSocket-传输" class="headerlink" title="WebSocket 传输"></a>WebSocket 传输</h3><ul><li>WebSocket 允许浏览器开通一个与服务器的双向连接，客户端不仅能从信令服务器请求数据，信令服务器还能主动向客户端推送数据；</li><li>现在主流的浏览器都支持 WebSocket，但某些 Web 代理和防火墙并不完全支持 WebSocket，可能会导致问题，尤其在身份验证方面。</li></ul><h2 id="NAT-穿透"><a href="#NAT-穿透" class="headerlink" title="NAT 穿透"></a>NAT 穿透</h2><p>本篇的最后来说一下客户端建立对等连接时的网络问题。</p><p>在一个 WebRTC 系统中，一个比较理想的情况是在客户端之间直接通过获取操作系统和网卡的 IP 地址建立点对点通信，我们一般称之为客户端之间处在同一个的内网中，但是实际中的场景往往复杂的多。目前我们日常使用的大部分设备都处在 NAT 设备后面，这就导致客户端没办法通过读取各自网卡的 IP 信息建立连接，在这种情况下，就需要借助 NAT 穿透技术。</p><p>关于 NAT 穿透技术，在本系列第一篇文章中已经提到可以借助 STUN&#x2F;TURN 等方案实现 NAT 穿透，这里不再阐述，并且本篇文章主要关注 WebRTC 信令服务器的设计和实现，NAT 穿透技术的工作原理和工作流程将在本系列第三篇文章中详细展开说明。</p><p>这里先看一下 NAT 设备下信令服务器的通信过程：</p><p><img src="https://img.cczywyc.com/webrtc_nat_connect.png"></p><ol><li>前面已经说过，客户端通信前需要先建立对信令服务器的连接；</li><li>NAT 设备的场景，交换剂信息前，客户端还需要与 STUN&#x2F;TURN 建立连接，这样做的目的是通过 STUN&#x2F;TURN 服务器获得各自的外网 IP 地址（子网和公网）、端口和 NAT 结构（IP 地址和端口对我们称之为 ICE Candidate）。连接后通信双方就可以通过信令服务器彼此交换信息了；</li><li>当获得彼此信息后，就可以尝试 NAT 穿透，客户端之间建立对等的 P2P 连接。</li></ol><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><p><a href="https://sheerbittech.medium.com/webrtc-signaling-server-everything-you-need-to-know-569f6b5c7317">https://sheerbittech.medium.com/webrtc-signaling-server-everything-you-need-to-know-569f6b5c7317</a></p><p><a href="https://juejin.cn/post/7171089420911640613">https://juejin.cn/post/7171089420911640613</a></p>]]></content>
    
    
    <summary type="html">WebRTC介绍</summary>
    
    
    
    <category term="hyper-transfer 文章合集" scheme="https://cczywyc.com/categories/hyper-transfer-%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="Golang" scheme="https://cczywyc.com/tags/Golang/"/>
    
    <category term="WebRTC" scheme="https://cczywyc.com/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>基于 WebRTC 实现一个文件传输工具（一）- WebRTC 详解</title>
    <link href="https://cczywyc.com/2024/02/05/%E5%9F%BA%E4%BA%8EWebRTC%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%B7%A5%E5%85%B7%EF%BC%88%E4%B8%80%EF%BC%89-%20WebRTC%E8%AF%A6%E8%A7%A3/"/>
    <id>https://cczywyc.com/2024/02/05/%E5%9F%BA%E4%BA%8EWebRTC%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%B7%A5%E5%85%B7%EF%BC%88%E4%B8%80%EF%BC%89-%20WebRTC%E8%AF%A6%E8%A7%A3/</id>
    <published>2024-02-05T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>如题，最近在折腾一个文件传输工具，用于在多个不同设备间方便快速地传输文件。我的要求也很直接，这个文件传输工具，最好不需要繁琐的安装和配置过程，能够做到开箱即用，同时它还需要兼容我的 PC 设备和多个移动设备，经过一番调研之后，我决定把技术方案定为了 WebRTC。</p><p>本篇文章是此系列文章的第一篇，详细介绍 WebRTC 技术。</p><h1 id="WebRTC-概述"><a href="#WebRTC-概述" class="headerlink" title="WebRTC 概述"></a>WebRTC 概述</h1><p>WebRTC（Web Real-Time Communication）是一个开源项目，旨在通过简单的应用程序接口（API）实现浏览器和移动应用之间的实时通信（RTC）。它允许网页浏览器进行语音通话、视频聊天和点对点文件分享，而无需用户安装特殊的插件或第三方软件。</p><p>简单来说，WebRTC 项目主要思想是定义 WebRTC API，该 API 允许浏览器安全地访问设备上的输入外设，如麦克风和网络摄像头，以点对点的方式与远程设备共享或交换媒体数据以及实时数据。借助 WebRTC，多个不同设备可以在一个平台上流畅地、安全地共享语音、视频和实时数据。</p><p>WebRTC 是 Google 在 2011 开源的项目，并将其纳入 Chrome 浏览器的开发计划，经过多年持续的发展和壮大，它已经成为浏览器间实时通信的主流技术之一，现在，几乎所有主流的浏览器都支持 WebRTC 标准，并且在各个领域得到了广泛的应用和推广。有关 WebRTC 的历史可以参考<a href="https://princiya777.wordpress.com/2017/08/06/webrtc-a-detailed-history/">这篇文章</a>。</p><p>WebRTC 是实时通信的未来，它受欢迎的原因有很多，我总结了 WebRTC 以下几个特点：</p><ul><li>WebRTC 是一种无需插件的现代实时通信技术。它不需要任何额外的插件或应用程序来进行音频、视频流和数据共享，它使用 JavaScript、应用程序编程接口（API）和 HTML5 将通信技术嵌入到浏览器中。像 Google Hangouts、Whatsapp、Facebook Messenger、ZOOM 团队通信、Zendesk 客户支持、Skype for Web 等产品都集成了 WebRTC。</li><li>不同设备的浏览器能够以对等的方式直接与其他浏览器交换实时媒体。</li><li>WebRTC 提供了比其他流媒体更高的安全性，并且无需借助第三方软件。</li><li>它是免费开源的，并且在全球范围内应用，这正是推动此项技术发展的动力。</li><li>WebRTC 支持多种编程语言的客户端 API。WebRTC 标准受到多种编程语言和框架的支持，例如：JavaScript、Java(Android)、NodeJs Rest、NodeJs WebSocket、Go、PHP、Python、React Native、REST、Ruby、Swift(IOS)。</li></ul><h1 id="核心组件和架构设计"><a href="#核心组件和架构设计" class="headerlink" title="核心组件和架构设计"></a>核心组件和架构设计</h1><h2 id="三个核心组件"><a href="#三个核心组件" class="headerlink" title="三个核心组件"></a>三个核心组件</h2><p>WebRTC 是通过浏览器进行的实时通信，通过标准化的 WebRTC API 与 Web 浏览器进行工作和通信，因此 WebRTC API 必须提供一系列的实用工具，其中包括一些类似于连接管理（以点对点方式）、编解码功能协商、选择和控制、媒体控制、安全和防火墙等。WebRTC 的功能大体可以分为以下三个组件。</p><h3 id="MediaStream"><a href="#MediaStream" class="headerlink" title="MediaStream"></a>MediaStream</h3><p>媒体捕获和流处理组件，MediaStream 允许浏览器访问设备的摄像头和麦克风，并捕获音视频流。</p><p>这是实现实时通信的第一步：获取用户想要分享的数据。在这个阶段下，可能会发生以下情况：</p><ol><li>用户期望的流数据（音频&#x2F;视频&#x2F;其他数据）和即将要建立的通信模式被捕获；</li><li>即将分享的本地媒体流允许浏览器访问流设备，如摄像头、麦克风等；</li><li>允许浏览器捕获媒体。</li></ol><p>以上操作均是通过 getUserMedia() 方法获取访问权限，具体 API 操作参考<a href="https://webrtc.github.io/samples/">这篇文章</a>。</p><h3 id="RTCPeerConnection"><a href="#RTCPeerConnection" class="headerlink" title="RTCPeerConnection"></a>RTCPeerConnection</h3><p>网络通信组件，它是 WebRTC 中最核心的组件，负责在浏览器之间建立、维护和管理直接的音视频或数据通信连接。</p><p>实时通信的第二步：确定了通信流之后，下一步就是将不同的设备连接起来，这涉及到网络通信组件，通信双方通过 STUN 和 TURN 服务允许发送者和接受者之间建立点对点连接。具体连接过程将在后面详细说明。</p><p>此阶段具体 API 操作参考<a href="https://webrtc.github.io/samples/">这篇文章</a>。</p><h3 id="RTCDataChannel"><a href="#RTCDataChannel" class="headerlink" title="RTCDataChannel"></a>RTCDataChannel</h3><p>数据通信组件，允许网页应用直接在用户浏览器之间建立安全的双向数据通道，用于传输任何类型的数据。</p><p>实时通信的最后一步：数据通信和交换，发挥作用的是数据通信组件，具体来说就是不同设备通过浏览器直接点对点交换数据。具体发生在当第一次在一个实例化的 PeerConnection 对象上调用 CreateDataChannel() 方法时。</p><p>此阶段具体 API 操作参考<a href="https://webrtc.github.io/samples/">这篇文章</a>。</p><h2 id="WebRTC-架构设计"><a href="#WebRTC-架构设计" class="headerlink" title="WebRTC 架构设计"></a>WebRTC 架构设计</h2><p>以下是源自<a href="https://webrtc.github.io/webrtc-org/architecture/">官网</a>的 WebRTC 结构设计图<img src="https://img.cczywyc.com/webrtc_architecture.png"></p><p>可以看出，WebRTC 的设计相当复杂，如果你是浏览器开发者，你应该关注 WebRTC C++ API 和可以使用的捕获&#x2F;渲染钩子；如果你是 Web 程序开发者，你应该更多关注 Web API 部分。</p><h1 id="关键技术"><a href="#关键技术" class="headerlink" title="关键技术"></a>关键技术</h1><h2 id="信令"><a href="#信令" class="headerlink" title="信令"></a>信令</h2><p>前面说到 WebRTC 是一种点对点的实时通信技术，不同设备的客户端浏览器可以直接通过 WebRTC 提供的 Web API 建立对等点对点连接，信息的交换与分享不需要借助中间服务器。但是在建立连接之前，客户端之间是如何知晓彼此的连接信息呢？多个客户端之间的实时通信会话又是如何维护的呢？这就需要借助信令服务器了。</p><p>简单说来，信令服务器就是在连接建立之前负责在通信双方传递信令数据来建立和维护实时通信会话，信令数据包括但不限于传递会话初始化信息、客户端信息、媒体元信息以及网络配置信息等。客户端通过信令服务器交换和协商上述信息，从而建立点对点的连接，需要说明的是，连接建立以后，客户端之间的实时通信和数据交换是不通过信令服务器的，它仅仅是在会话建立前发挥作用，以及维护多个会话状态等。</p><p>WebRTC 客户端实现实时通信的第一步就是连接信令服务器，需要特别说明的是，WebRTC 标准自身不规定任何特定的信令协议，开发者可以选择 WebSocket、SIP 或者 HTTPS 等其他协议实现会话控制、媒体元数据交换、网络配置信息交换等功能，所以信令服务器在 WebRTC 技术框架中是极其重要且实现方式多种多样的，保证了技术的灵活性和开放性。</p><h2 id="NAT-穿透"><a href="#NAT-穿透" class="headerlink" title="NAT 穿透"></a>NAT 穿透</h2><p>上面说到，WebRTC 客户端之间建立连接是通过信令服务器来交换网络连接信息的，这一般涉及客户端的 ip 地址和端口信息，这里涉及到两种情况：</p><h3 id="WebRTC-客户端处在同一个局域网内"><a href="#WebRTC-客户端处在同一个局域网内" class="headerlink" title="WebRTC 客户端处在同一个局域网内"></a>WebRTC 客户端处在同一个局域网内</h3><p>这种情况比较好处理，只需要通过信令服务器交换客户端彼此的局域网地址，通过 WebRTC 提供的 Web API 即可建立连接，实现点对点通信。</p><h3 id="WebRTC-客户端不在同一个局域网内"><a href="#WebRTC-客户端不在同一个局域网内" class="headerlink" title="WebRTC 客户端不在同一个局域网内"></a>WebRTC 客户端不在同一个局域网内</h3><p>这种情况更符合大多数的情况，因为现如今互联网上大多数设备都在分配私有 IP 地址的 NAT 后面，并且一般限制了来自外部的访问，这种情况下因为不在同一个局域网内，直接交换客户端的私有 IP 地址就行不通了，这就需要 NAT 穿透技术。</p><p>因此，NAT 穿透技术在 WebRTC 技术体系内是必要且至关重要的。</p><h3 id="WebRTC-使用多种技术来实现-NAT-穿透"><a href="#WebRTC-使用多种技术来实现-NAT-穿透" class="headerlink" title="WebRTC 使用多种技术来实现 NAT 穿透"></a>WebRTC 使用多种技术来实现 NAT 穿透</h3><h4 id="STUN（Session-Traversal-Utilities-for-NAT）"><a href="#STUN（Session-Traversal-Utilities-for-NAT）" class="headerlink" title="STUN（Session Traversal Utilities for NAT）"></a>STUN（Session Traversal Utilities for NAT）</h4><ul><li>STUN 是一种协议，是基于 UDP 的完整的穿透 NAT 的解决方案，属于打洞技术，它允许 NAT 后面的客户端发现其公共 IP 地址以及它们所在的 NAT 类型</li><li>客户端向 STUN 服务器发送请求，STUN 服务器以客户端的公网 IP 地址和端口号响应</li><li>如果两个对等连接的客户端都位于允许直接连接的 NAT 类型后面，则该信息将用于建立直接连接</li></ul><h4 id="TURN（Traversal-Using-Relays-around-NAT）"><a href="#TURN（Traversal-Using-Relays-around-NAT）" class="headerlink" title="TURN（Traversal Using Relays around NAT）"></a>TURN（Traversal Using Relays around NAT）</h4><ul><li>TURN 是一种数据传输协议，通过 Relay 方式穿越 NAT，TURN 主要用在 STUN 无法穿透的场景下，例如一方或者双方位于对称 NAT 后的情况</li><li>TURN 服务器在对等点之间中继流量，有效绕过 NAT</li><li>此方法会引入额外的延迟和带宽成本，因为所有流量必须经过 TURN 服务器</li></ul><h4 id="ICE（Interactive-Connectivity-Establishment）"><a href="#ICE（Interactive-Connectivity-Establishment）" class="headerlink" title="ICE（Interactive Connectivity Establishment）"></a>ICE（Interactive Connectivity Establishment）</h4><ul><li>ICE 是一个结合 STUN 和 TURN 建立连接的框架</li><li>ICE 尝试多种方法来找到连接的最佳路径，它使用 STUN 进行打洞，若失败，则使用 TURN 进行中转</li></ul><h1 id="通信过程"><a href="#通信过程" class="headerlink" title="通信过程"></a>通信过程</h1><h2 id="局域网通信"><a href="#局域网通信" class="headerlink" title="局域网通信"></a>局域网通信</h2><p>WebRTC 客户端都位于同一个局域网内，这种情况属于比较简单的一种通信模式，借助信令服务器，客户端连接信令服务器时，会携带客户端在局域网内的 IP 地址和端口信息，此信息可用于直接建立点对点连接。</p><p>通信模型如下：<img src="https://img.cczywyc.com/simple_webrtc_connect.png"></p><h2 id="广域网通信"><a href="#广域网通信" class="headerlink" title="广域网通信"></a>广域网通信</h2><p>这种模式涉及的场景就复杂了，在这种通信模式下，不同的客户端处于不同的网络环境，客户端前面的 NAT 也有不同的部署方式，所以存在不同类型的 NAT，而穿透不同类型的 NAT 所使用的方式和技术成本是不一样的，在本系列的第三篇文章里，我会专门详细介绍 NAT 穿透技术，这里不作展开。</p><p>通信模型如下：<img src="https://img.cczywyc.com/how_webrtc_works.png"></p><h1 id="安全性"><a href="#安全性" class="headerlink" title="安全性"></a>安全性</h1><p>WebRTC 的安全性这里拿出来单独说。</p><p>一直以来，人们都希望自己的通信和交流处在安全且私密的环境中，安全性方面，WebRTC 也做了大量的考虑和设计。</p><ul><li>加密：所有 WebRTC 组件（包括信令）都必须加密</li><li>安全协议：WebRTC 强制使用安全的实时传输协议（SRTP）来传输音视频数据，在数据通道上使用数据加密传输协议（DTLS）确保通信安全</li><li>浏览器保证：当使用 API 获取设备的摄像头、麦克风权限时，浏览器会有明确的提示，且必须经过用户同意后才能开启</li></ul><p>附图：WebRTC 的协议栈和加密通信协议示意图</p><p><img src="https://img.cczywyc.com/webrtc_protocol_stack.png"></p><p><img src="https://img.cczywyc.com/webrtc_security_protocol.png"></p><h1 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h1><p>现如今，WebRTC 的应用场景极为丰富，包括但不限于：</p><ul><li>视频会议和语音通话：提供低延迟、高质量的实时视频和音频通信体验</li><li>实时数据共享：支持文本、文件、屏幕共享等多种形式的实时数据共享</li><li>直播和点播：支持实时直播与点播服务，为用户提供丰富的媒体体验</li><li>远程协作和教育：在线教育、远程会议和协作工具等应用中提供实时互动能力</li></ul><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><p><a href="https://www.geeksforgeeks.org/introduction-to-webrtc/">https://www.geeksforgeeks.org/introduction-to-webrtc/</a></p><p><a href="https://web.dev/articles/webrtc-basics?hl=zh-cn">https://web.dev/articles/webrtc-basics?hl=zh-cn</a></p><p><a href="https://webrtc.github.io/samples/">https://webrtc.github.io/samples/</a></p><p><a href="https://bloggeek.me/how-webrtc-works/">https://bloggeek.me/how-webrtc-works/</a></p><p><a href="https://medium.com/callstatsio/what-are-stun-and-turn-in-computer-networking-95d5130597c7">https://medium.com/callstatsio/what-are-stun-and-turn-in-computer-networking-95d5130597c7</a></p><p><a href="https://medium.com/dvt-engineering/introduction-to-webrtc-cad0c6900b8e">https://medium.com/dvt-engineering/introduction-to-webrtc-cad0c6900b8e</a></p><p><a href="https://blog.jianchihu.net/webrtc-av-transport-basis-nat-traversal.html">https://blog.jianchihu.net/webrtc-av-transport-basis-nat-traversal.html</a></p><p><a href="https://webrtc-security.github.io/">https://webrtc-security.github.io/</a></p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">WebRTC介绍</summary>
    
    
    
    <category term="hyper-transfer 文章合集" scheme="https://cczywyc.com/categories/hyper-transfer-%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="WebRTC" scheme="https://cczywyc.com/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>2023 年终总结</title>
    <link href="https://cczywyc.com/2023/12/24/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93-2023/"/>
    <id>https://cczywyc.com/2023/12/24/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93-2023/</id>
    <published>2023-12-24T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>临近年末，最近陆陆续续看到不少人公开了自己的年终总结，我也跟个风，总结一下即将过去的我的 2023 年。</p><p>一开始我在总结过去的一年的时候，发现并没有太多值得总结的事情，出于博客网站的需要，我决定还是开个好头，写下了这篇文章。需要特别说明的是，这是一篇流水账式的文章，并没有特别的干货，写的真的很烂。</p><p>既然说到这里，先来说一下我的<a href="https://cczywyc.com/">个人网站</a>，2023 年是我正式开始写博客的第一年，正如我在<a href="https://cczywyc.com/2023/01/10/%E5%85%A8%E6%96%B0%E5%8D%9A%E5%AE%A2%E7%AB%99%E7%82%B9/">这篇文章</a>中所说，以前我总是记录的很杂、很随意，我希望通过写博客的方式让自己养成记录的好习惯，并且借此得以规范我的记录，希望这是一个好的开始。截止到现在，整个 2023 年我一共正式写了 12 篇文章，算很少的了，其中有不少时间还花在个人网站的建设上，一开始我使用的是 hugo 来构建我的网站，苦于我使用的主题文档缺少，加上 hugo 可自定义性不高，在 12 月份的时候我把个人网站改为了 hexo 构建，并且使用了全新的主题，这套主题文档很详细，可配置性很高，比较适合作为我构建个人网站的选择，最后也就是你现在看到的这个样子。</p><p>去年 12 月，我跟妹子摇中了一个不错的盘，也算正式上车，成为房奴大队的一员，今年 1 月，随着房子封顶，我们正式开始了还贷的日子，一开始多少会有些不适应，索性我跟妹子的收入加起来还算可观，目前在房贷的压力下，我们的生活质量并没有受到太大的影响。我们的运气算不错，房子预计会提前交付，大概是在 24 年的 6 月份，再过半年我们就正式结束 3 年多的杭漂，对此心里还是有不小的期待。交房后的半年预计会很忙，所以明年下半年大部分的时间都在操心房子装修的事情，到时候关于装修的过程，我预计也会水几篇文章来记录新房折腾的过程，这里先挖个坑。</p><p>生活上，这一年似乎过的比较平淡，没有什么特别值得纪念的事情，印象中这一年倒是去了不少地方。上半年去了两趟上海，熟悉的外滩、热闹的步行街、拥堵的街道、熙熙攘攘的人群，还有不少杭州吃不到的美食，总体来说，上海这座城市给我留下的印象还是不错的，我喜欢这座城市带给我的高级感，但是我并不喜欢它拥堵的交通。</p><p>10 月国庆节，去了一趟妹子家，双方父母见了面，婚期也被提上了日程。</p><p>年底，跟大学室友在庐山聚了一次，这是 1220 宿舍从毕业旅行之后再一次相聚旅游，3 天时间，很快也很快乐。我是一个重感情的人，我喜欢跟朋友一起玩的感觉，几个要好的朋友从不同的城市相聚，大家述说着自己的生活，倾听着彼此的经历，有吐槽、有吹牛，也有对未来生活的向往…… 这种美好的感觉是能够治愈心灵的，所以在旅行结束，我发了这样一条朋友圈：</p><p><img src="https://img.cczywyc.com/nianzhongzongjie_2023_1.jpg"></p><p>旅游确实可以放松心情，在大自然面前，它可以接纳你的一切情绪，不论是你带着喜悦的心情，或者是忧愁的情感，都会得到心灵的净化，从而收获内心的平静，这也是我一直想要追求的。</p><p>工作上，6 月，我换了一份工作，离开了工作了 3 年的地方，刚开始及其的不适应，陌生的环境、陌生的同事，这种感觉持续了将近 1 个月，我并没有刻意调整自己的这种情绪，后来应该是麻木了，这种感觉渐渐消失了。实话说，面对工作，我是及其焦虑的，新工作是 CURD 的工作，这并不是我想要的工作，更不是我喜欢的方向，需要说明的是，我并不是一个眼高手低的人，但是我很清楚自己想要什么，我很清楚我理想中的工作应该是做什么样的内容和方向。这种理想和现实的差距让我不知所措，我陷入了长久了焦虑中，越焦虑，这种失落的情绪就越是明显和深刻。幸运的是，我对自己还算有一个清醒的认识，焦虑的情绪并没有太久的占据上风，我知道现阶段的我还不够好，不管怎么样，还是要做好当下的工作，也算是暂时地与自己“和解”了。</p><p>技术上，感觉也没有什么好总结的，过去的 2023 年，我在技术上确实是一事无成，想参与的开源迟迟没有进展，我感兴趣的中间件和云原生领域也没有按计划开始，对于计算机内功的修炼也只是进行了皮毛，想做的项目也只是建了一个仓库…… 我去翻了一下年中换工作时<a href="https://cczywyc.com/2023/07/12/%E6%8D%A2%E5%B7%A5%E4%BD%9C%E4%BA%86/">文章</a>里面给自己制定的计划，当时制定的计划都还历历在目，大致总结了一下：</p><ol><li>操作系统 CSAPP 这本书只看了第一章的内容，书上的实验也没有做，做的很烂。</li><li>计算机网络方面看的较多，算是做的比较好的地方。</li><li>分布式系统方面，《mit 6.824》已经开始看了，目前也开始接触了 MapReduce 的论文。</li><li>打算和两个好友做一个 RSS 订阅的应用，目前项目前期的工作已经准备的差不多了，年末的时候有小伙伴去闭关开发了，计划明年启动开发。</li></ol><p>总结下来发现，年中给自己制定的目标完成的情况太差，但让我高兴的是，我终于在今年迈出了这一步，我觉得这是一个好的开始，期待在 2024 年我可以做的更好。</p><p>下半年还看了几本书，有两本技术人写的，也有技术无关的，关于阅读这一部分，本站 <a href="https://cczywyc.com/book/">阅读专栏</a>会有详细的总结。阅读能让我收获极大的内心平静，这也是我逐渐养成阅读的原因，每当我焦虑时、或者迷茫时，我会去读书，可能在某个时刻，突然就在书中找到答案了，希望你可以和我一样，在书中找到属于自己的天地。</p><p>可能是因为年底看完了耗子叔（陈皓）的遗作《左耳听风》，这是一本新书，和耗子书极客时间专栏同名，书里面也有很多耗子书博客中的内容。写到这里，我不禁怀念起耗子叔，他应该是我最喜欢的技术人了，率真的性格、及其鲜明的技术观点、对年轻人的鼓励、终身学习的态度、谦逊的性格，这些都深深打动了我，今年有一段时间，我几乎每天下班都泡在耗子叔的<a href="https://coolshell.cn/">网站</a>中，也几乎看完了他在极客时间上的专栏，还没来得及跟他交流一番，他就离开了我们，这里放上耗子叔生前的座右铭–芝兰生于深谷，不以无人而不芳；君子修身养德，不以穷困而改志。永远缅怀耗子叔！</p><p>我以前并不是一个爱给自己制定计划的人，因为我觉得对我来说，制定计划只是装装样子，大概率也不会完成。今年这一年我的观念变化真的很大，我发现经过这半年的探索，自己好像很享受完成既定计划的感觉，这种成就感也会反推我去完成更多的事情，总结的最后，我也大致列出了 2024 年要完成事情：</p><ol><li>继续看 CSAPP 这本书，完成前 6 章的内容和课后实验，同时完成 《mit 6.828》的内容；</li><li>完成 《mit 6.824》分布式系统剩下的课程和课后的 lab；</li><li>和伙伴完成 go-my-rss 第一版订阅功能；</li><li>有时间看一下《mit 6.829》；</li><li>完成新房装修，折腾一下软路由和新家网络；</li><li>出去旅游，暂定新疆、重庆、东北、香港、韩国；</li><li>至少看 5 本书技术之外的书；</li><li>练习口语。</li></ol><p>就先列举这些吧，给自己一个大致的计划方向，每天提升一点点。</p><p>上面就是我 2023 年终总结的全部内容，草草梳理完了自己的 2023 年，希望这是一个全新的开始，也期待在 2024 年终总结中自己可以交出更好的答卷。</p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">梳理我的 2023</summary>
    
    
    
    <category term="年终总结合集" scheme="https://cczywyc.com/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93%E5%90%88%E9%9B%86/"/>
    
    
    <category term="年终总结" scheme="https://cczywyc.com/tags/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>GitHub 数据库升级过程</title>
    <link href="https://cczywyc.com/2023/12/10/GitHub%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8D%87%E7%BA%A7/"/>
    <id>https://cczywyc.com/2023/12/10/GitHub%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8D%87%E7%BA%A7/</id>
    <published>2023-12-10T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>最近 GitHub 官方博客更新了一篇文章，讲述了将 GitHub 网站依赖的 1200+ 台 MySQL 主机升级到 8.0 版本的过程。</p><p>先前，Oracle 官方宣布，在 2023 年 10 月 21 日之后，MySQL 5.7 将达到其生命周期的终点，这意味着 Oracle 将不再为 MySQL 5.7 提供官方更新、错误修复或安全补丁，正是在这一背景下，才有了 GitHub 此次的升级。</p><p>根据 GitHub 官方介绍，GitHub 使用 MySQL 来存储大量关系数据，因此对于数据库这种底层的基础设施的升级绝非易事，为了升级到 MySQL 8.0，他们规划、测试和升级本身总共花费了一年多的时间，并且需要 GitHub 内部多个团队的协作。因此对于 GitHub 此次这种大规模的底层数据库的升级，我本身是抱有极大的兴趣，也想了解一下如此大规模的 MySQL 的集群采用了什么样的升级方案，所以我去阅读了 <a href="https://github.blog/2023-12-07-upgrading-github-com-to-mysql-8-0/">GitHub 官方博客</a>，并且将原文翻译如下，以下是原文翻译全部内容。</p><hr><p>15 年前，GitHub 开始于一个使用 Ruby on Rails（cczywyc 注：一个基于 Ruby 语言的 web 开发框架）框架构建的应用，使用单体的 MySQL 数据库，从那时起，Github 逐渐改进 MySQL 结构，以满足平台的扩展和弹性需求，这其中包括构建高可用、实现自动化测试和数据分区的要求。如今，MySQL 仍热是 GitHub 基础设施的核心部分，并且也是我们选择关系型数据库的核心部分。</p><p>下面就是我们将我们的 1200 多个 MySQL 主机升级到 8.0 版本的过程。在不影响我们的服务水平目标（SLO）的前提下升级 MySQL 版本可谓是一项不小的成就，在这个过程中，升级规划、测试和升级本身就花费了我们一年多的时间，并且还依赖于 GitHub 内部多个团队的协作。</p><h2 id="升级的动机"><a href="#升级的动机" class="headerlink" title="升级的动机"></a>升级的动机</h2><p>为什么我们要升级到 MySQL8.0 版本呢？随着 MySQL 5.7 临近生命周期的尾声，我们将我们 MySQL 集群升级到了下一个主要的版本 – MySQL8.0。我们不仅希望使用这个能够获得最新的安全补丁、错误修复和性能增强功能的 MySQL 版本，我们还希望能够使用到 8.0 版本中的一些新功能，这包括 Instant DDLs、不可见索引和压缩二进制日志等。</p><h2 id="GitHub-的-MySQL-的基础设施"><a href="#GitHub-的-MySQL-的基础设施" class="headerlink" title="GitHub 的 MySQL 的基础设施"></a>GitHub 的 MySQL 的基础设施</h2><p>在我们深入了解升级过程之前，让我们先来全面了解一下我们的 MySQL 基础设施：</p><ul><li>我们的 MySQL 集群包括 1200 多台主机，它们包括在数据中心的 Azure 虚拟机和裸金属主机。</li><li>我们数据库存储了超过 300 TB 的数据，并且在 50 多个数据库集群中每秒处理 550 万次的查询。</li><li>每个集群都带有主集群加副本集群的配置，用来实现高可用性。</li><li>我们做到了数据分区。我们利用水平和垂直分片来扩展 MySQL 集群，针对特定的领域，我们有特定的 MySQL 集群来存储这些特定领域的数据，同时我们还为大区域提供了水平分片的 Vitess 集群，这些区域的增长超出了单-主 MySQL 集群的规模。</li><li>我们拥有一个庞大的工具生态系统，包括 Percona Toolkit、gh-ost、orchestrator、freno 以及内部自动化，用于运营我们的平台。</li></ul><p>上面所有的这些组合成一个多样化且复杂的部署，需要在保持我们的 SLOs 的同时进行升级。</p><h2 id="准备阶段"><a href="#准备阶段" class="headerlink" title="准备阶段"></a>准备阶段</h2><p>作为 GitHub 主要的数据存储，我们对可用性设定了很高的标准。由于我们的数据库集群的规模和 MySQL 基础设施的关键性，我们对升级过程确定了一下的要求：</p><ul><li>我们必须在遵守我们的服务水平目标（SLO）和服务水平协议（SLA）的同时升级每个 MySQL 数据库。</li><li>我们无法在测试和验证阶段考虑所有的故障模式，因此，为了保持在服务水平目标（SLO）范围内，（在发生升级故障时）我们需要能够在不中断服务的情况下可以回滚到先前的 MySQL 5.7 版本。</li><li>我们的 MySQL 集群负载非常多样化，为了降低风险，我们需要对每个数据库集群进行原子升级，并在其他重大变更周围安排计划，这意味着升级过程会很长，因此我们从一开始就预见我们需要能够维持运行混合版本的环境。</li></ul><p>升级准备工作开始于 2022 年 7 月，甚至在升级任何一个生产数据库之前，我们就设定了几个关键里程碑。</p><h3 id="准备升级的基础设施"><a href="#准备升级的基础设施" class="headerlink" title="准备升级的基础设施"></a>准备升级的基础设施</h3><p>我们需要确定 MySQL 8.0 的适当默认值，并进行一些基准性能测试。因为我们需要操作两个版本的 MySQL，我们的工具和自动化需要能够处理混合版本，并且能够识别在 5.7 和 8.0 之间的新的、不同的或已弃用的语法。</p><h3 id="确保应用兼容性"><a href="#确保应用兼容性" class="headerlink" title="确保应用兼容性"></a>确保应用兼容性</h3><p>我们将 MySQL 8.0 添加到所有使用 MySQL 的应用程序的持续集成（CI）中。我们在 CI 中并行运行 MySQL 5.7 和 8.0，以确保在长时间的升级过程中不会出现回归。我们检测到 CI 中的各种错误和不兼容性，帮助我们删除任何不受支持的配置或功能，并转义任何新的保留关键字。</p><p>为帮助应用程序开发人员过渡到 MySQL 8.0，我们还在 GitHub Codespacees 中启动了选择 MySQL 8.0 预构建容器的选项，以便进行调试，并为额外的预生产测试提供了 MySQL 8.0 开发集群。</p><h3 id="沟通和透明度"><a href="#沟通和透明度" class="headerlink" title="沟通和透明度"></a>沟通和透明度</h3><p>我们使用 GitHub Projects 创建了一个 滚动日历，以便在内部进行升级计划的沟通和跟踪。我们创建了 issue 模版，用于跟踪应用团队和数据库团队的 checklist，以协调升级工作。</p><p><img src="https://github.blog/wp-content/uploads/2023/12/image2-1.png?w=1797" alt="MySQL 8.0 升级计划跟踪项目板"></p><h2 id="升级计划"><a href="#升级计划" class="headerlink" title="升级计划"></a>升级计划</h2><p>为了满足我们的可用性标准，我们采用了一种渐进式升级策略，允许在整个过程中设置检查点和回滚。</p><h3 id="步骤一：滚动副本升级"><a href="#步骤一：滚动副本升级" class="headerlink" title="步骤一：滚动副本升级"></a>步骤一：滚动副本升级</h3><p>我们首先升级了单个副本，并在其离线状态下进行监控，以确保基本功能稳定。然后我们启用了生产流量，并继续监控查询延迟、系统指标和应用程序指标。接着，我们逐步将 8.0 副本上线，直到升级了整个数据中心，然后遍历其他数据中心。同时，我们还保留了足够的 5.7 版本的副本以便进行回滚，但我们禁用了它们的生产流量，开始通过 8.0 服务器提供所有的读取流量。</p><p><img src="https://github.blog/wp-content/uploads/2023/12/image4.png?w=1972"></p><h3 id="步骤二：更新复制拓扑"><a href="#步骤二：更新复制拓扑" class="headerlink" title="步骤二：更新复制拓扑"></a>步骤二：更新复制拓扑</h3><p>在所有的只读流量通过 8.0 副本提供服务后，我们调整了复制拓扑如下：</p><ul><li>配置一个 8.0 的主备候选节点，直接在当前的 5.7 主节点下进行复制。</li><li>在那个 8.0 副本的下游创建了两个复制链：<ul><li>一组仅包含 5.7 版本的副本（不提供流量，但已准备好以防回滚）</li><li>一组仅包含 8.0 版本的副本（服务流量）</li></ul></li><li>拓扑结构在这个状态下只持续了很短的时间（最多几个小时），然后我们就进入了下一步</li></ul><p><img src="https://github.blog/wp-content/uploads/2023/12/image3-1.png?w=928&resize=928,752"></p><h3 id="步骤三：将-MySQL-主机升级为主节点"><a href="#步骤三：将-MySQL-主机升级为主节点" class="headerlink" title="步骤三：将 MySQL 主机升级为主节点"></a>步骤三：将 MySQL 主机升级为主节点</h3><p>我们选择不直接在主数据库主机上进行升级。相反，我们将通过 Orchestrator 执行的优雅故障切换将一个 MySQL 8.0 副本升级为主数据库。在那时，复制拓扑结构包括一个8.0 主数据库，连接到它的有两个复制链：一个用于回滚的离线的 5.7 副本集和一个用于服务的 8.0 副本集。</p><p>Orchestrator 还能将 5.7 版本的数据库主机作为潜在的故障切换候选者列入黑名单，以防止在计划外的故障切换时发生意外回滚。</p><p><img src="https://github.blog/wp-content/uploads/2023/12/image1-1.png?w=1982"></p><h3 id="步骤四-升级内部实例类型"><a href="#步骤四-升级内部实例类型" class="headerlink" title="步骤四: 升级内部实例类型"></a>步骤四: 升级内部实例类型</h3><p>我们在内部还拥有用于备份或非生产工作负载的辅助服务器，我们对这些服务器也进行了升级以确保一致性。</p><h3 id="步骤五：清理"><a href="#步骤五：清理" class="headerlink" title="步骤五：清理"></a>步骤五：清理</h3><p>一旦我们确认这些集群无需再回滚，并已成功升级至 8.0 版本，我们就移除 5.7 版本的服务器。然后验证包括至少完成一轮完整的 24 小时流量循环，以确保在流量高峰期间没有问题。</p><h2 id="回滚的能力"><a href="#回滚的能力" class="headerlink" title="回滚的能力"></a>回滚的能力</h2><p>保持我们升级策略的一个核心部分就是能够回滚到之前的 MySQL 5.7 版本的能力。对于只读副本，我们确保保持足够的 5.7 副本在线以提供生产流量负载，并且如果 8.0 副本运行不良时，通过禁用这些 8.0 副本来启动回滚操作；对于主节点来说，为了在不丢失数据或中断服务的情况下进行回滚，我们就需要能够在 8.0 和 5.7 之间保持双向的数据复制。</p><p>MySQL 数据复制支持从一个版本复制到下一个更高的版本，但并不明确支持相反过程（MySQL 复制的兼容性）。当我们在演示集群上测试将 8.0 主机升级为主节点时，我们发现所有的 5.7 副本的复制都中断了。下面是我们需要解决几个问题：</p><ol><li>在 MySQL 中，utf8mb4 是默认的字符集，并使用更现代的 utf8mb4_0900_ai_ci 排序规则作为默认值，之前的 MySQL 5.7 版本支持 utf8mb4_unicode_520_ci 排序规则，但不支持最新版本的 Unicode utf8mb4_0900_ai_ci。</li><li>MySQL 8.0 引入了角色管理的特性，但这个特效在 MySQL 5.7 中并不存在。当将 8.0 实例提升为集群中的主节点时，我们遇到了问题，我们的配置管理会将某些权限集扩展为包含角色语句并执行它们，这会导致 5.7 副本的下游复制中断，我们是通过在升级窗口期间临时调整受影响用户的定义权限从而解决了这个问题。</li></ol><p>为了解决字符排序规则的不兼容性，我们必须将默认字符编码设置为 utf8，排序规则设置为 utf8_unicode_ci。</p><p>对于 GitHub.com 的整体架构，我们的 Rails 配置确保字符排序规则一致，并且使得标准化客户端配置到数据库变得更容易。因此，我们对于我们最关键的应用程序能够保持双向复制具有很强的信心。</p><h2 id="挑战"><a href="#挑战" class="headerlink" title="挑战"></a>挑战</h2><p>在我们的测试、准备和升级过程中，我们遇到了一些技术挑战。</p><h3 id="关于-Vitess"><a href="#关于-Vitess" class="headerlink" title="关于 Vitess"></a>关于 Vitess</h3><p>我们使用 Vitess 对关系型数据进行水平分片。总体而言，升级我们的 Vitess 集群与升级 MySQL 集群并无太大不同。我们已经在 CI 中运行 Vitess，因此我们能够验证查询的兼容性。在我们分片集群的升级策略中，我们一次升级一个分片。VTgate，即 Vitess 代理层，会广播 MySQL 的版本信息，而一些客户端行为取决于这个版本信息。例如，一个应用程序使用了一个禁用了 5.7 服务器查询缓存的 Java 客户端，因为在 8.0 中移除了查询缓存，对于它们来说，这会生成阻塞错误。因此，一旦给定 keyspace 的单个 MySQL 主机完成升级，我们必须确保我们也更新了 VTgate 的设置以广播 8.0 的信息。</p><h3 id="复制延迟"><a href="#复制延迟" class="headerlink" title="复制延迟"></a>复制延迟</h3><p>我们使用读取副本来扩展我们的读取可用性，GitHub.com 需要低的复制延迟，以提供最新的数据。</p><p>在我们的测试早期，我们遇到了一个 MySQL 的复制错误，它在 8.0.28 版本中被修复：</p><blockquote><p>Replication: If a replica server with the system variable replica_preserve_commit_order &#x3D; 1 set was used under intensive load for a long period, the instance could run out of commit order sequence tickets. Incorrect behavior after the maximum value was exceeded caused the applier to hang and the applier worker threads to wait indefinitely on the commit order queue. The commit order sequence ticket generator now wraps around correctly. Thanks to Zhai Weixiang for the contribution. (Bug #32891221, Bug #103636)</p></blockquote><p>我们恰好符合触发此错误的所有条件：</p><ul><li>我们使用 replica_preserve_commit_order，因为我们使用基于 GTID 的复制。</li><li>我们的许多集群在很长一段时间内承受着高强度的负载，尤其是我们最关键的集群。大部分集群都有很高的写入压力。</li></ul><p>由于此错误已在上游修复，我们只需要确保我们部署的 MySQL 版本高于 8.0.28 版本即可。</p><p>我们还观察到，在 MySQL 8.0 中，导致复制延迟的大量写入问题变得更加严重。这使得我们更加重视避免大量的写入突发情况。在 GitHub，我们使用 freno 来根据复制延迟来控制写入工作负载。</p><h3 id="查询在-CI-中通过但是在生产环境失败"><a href="#查询在-CI-中通过但是在生产环境失败" class="headerlink" title="查询在 CI 中通过但是在生产环境失败"></a>查询在 CI 中通过但是在生产环境失败</h3><p>我们知道在生产环境中不可避免地会出现问题，因此我们采用了逐步升级副本的策略。我们遇到了一些在 CI 中通过的查询，在生产环境中遇到真实工作负载时却失败的情况。其中一个显著的问题是，在具有大型 WHERE IN 子句的查询中，MySQL 会崩溃。我们有一些包含数万个值的大型 WHERE IN 查询。在这些情况下，我们需要在继续升级过程之前重写这些查询。查询抽样有助于跟踪和检测这些问题。在 GitHub，我们使用 Solarwinds DPM（VividCortex），即一个 SaaS 数据库性能监控工具，用于查询可观察性。</p><h2 id="经验教训和收获"><a href="#经验教训和收获" class="headerlink" title="经验教训和收获"></a>经验教训和收获</h2><p>经过测试、性能调优和解决已识别的问题，我们的整个升级过程持续了一年多，其中涉及到 GitHub 的多个团队的工程师。最终我们将我们的整个集群升级到了 MySQL 8.0，包括用户 GitHub.com 的演练集群、生产集群以及支持我们内部工具的实例。此次升级凸显了我们可观察性平台、测试计划和回滚能力的重要性。测试和逐步推出策略使我们能够及早发现问题，并减少主要升级中遇到新故障模式的可能性。</p><p>尽管有逐步推出的策略，我们仍然需要在每一步都具备回滚的能力，并且我们需要能够观察以识别何时需要回滚的信号。使回滚变得最具挑战的方面是保持从新的 8.0 主服务器到 5.7 副本的反向复制。我们发现 Trilogy 客户端库的一致性为我们提供了更可预测的连接行为，并让我们有信心，主 Rails 单体的连接不会破坏反向复制。</p><p>然而，对于我们的一些 MySQL 集群，连接来自不同框架&#x2F;语言的多个不同客户端，我们发现反向复制在几个小时内就会中断，缩短了回滚机会的时间窗口。幸运的是，这些情况很少见，我们没有出现在我们需要回滚之前复制中断的情况。但对我们来说，这是一个教训，即具有已知且被充分理解的客户端连接配置是有益的。它强调了制定准则和框架以确保在这些配置中保持一致性的价值。</p><p>我们之前对数据进行分区的努力得到了回报——它使我们能够针对不同的数据领域进行更有针对性的升级。这一点非常重要，因为一个失败的查询会阻塞整个集群的升级，而不同的工作负载分区使我们能够逐步进行升级，并减少在过程中遇到的未知风险的影响范围。这种权衡的结果是我们的 MySQL 集群规模也随之增长。</p><p>GitHub 上次升级 MySQL 版本时，我们有五个数据库集群，现在我们有 50 个以上的集群。为了成功升级，我们不得不投资于监测、工具和管理整个群集的流程。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>此次 MySQL 升级只是我们必须执行的例行维护中的一种类型——对于我们在集群上运行的任何软件，都必须具备升级路径。作为升级项目的一部分，我们开发了新的流程和操作能力，成功完成了 MySQL 版本的升级。然而，在升级过程中仍然有太多需要手动干预的步骤，我们希望减少完成未来 MySQL 升级所需的工作量和时间。</p><p>我们预计随着 GitHub.com 的增长，我们的集群将继续扩大，并且我们有将数据进一步分区的目标，这将随着时间增加我们的 MySQL 集群数量。在操作任务和自愈能力方面进行自动化建设可以帮助我们未来扩展 MySQL 运营能力。我们相信，投资可靠的集群管理和自动化将使我们能够扩展 GitHub，并跟上所需的维护工作，提供更可预测和弹性的系统。</p><p>此次升级的经验为我们的 MySQL 自动化奠定了基础，并为将来的升级铺平了道路，使其更加高效完成，但仍然保持同样的关注和安全水平。</p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">[转载翻译]GitHub.com 数据库升级到 MySQL8.0 的过程与经验</summary>
    
    
    
    <category term="外文优质文章合集" scheme="https://cczywyc.com/categories/%E5%A4%96%E6%96%87%E4%BC%98%E8%B4%A8%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="优质外文翻译" scheme="https://cczywyc.com/tags/%E4%BC%98%E8%B4%A8%E5%A4%96%E6%96%87%E7%BF%BB%E8%AF%91/"/>
    
  </entry>
  
  <entry>
    <title>交换机和网络虚拟化</title>
    <link href="https://cczywyc.com/2023/11/21/%E4%BA%A4%E6%8D%A2%E6%9C%BA%E5%92%8C%E7%BD%91%E7%BB%9C%E8%99%9A%E6%8B%9F%E5%8C%96/"/>
    <id>https://cczywyc.com/2023/11/21/%E4%BA%A4%E6%8D%A2%E6%9C%BA%E5%92%8C%E7%BD%91%E7%BB%9C%E8%99%9A%E6%8B%9F%E5%8C%96/</id>
    <published>2023-11-21T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>最近在公司负责几个二层网络、三层网络相关的项目，项目推进的过程中也掌握了一些交换机和网络虚拟化相关的知识。在之前我接触到的可能更多是三层、四层网络相关的知识，刚好借着这次项目经验总结梳理一下二层网络设备和云计算中网络虚拟化相关的知识。</p><h1 id="交换机概述"><a href="#交换机概述" class="headerlink" title="交换机概述"></a>交换机概述</h1><h2 id="什么是交换机"><a href="#什么是交换机" class="headerlink" title="什么是交换机"></a>什么是交换机</h2><p>交换机是一种网络硬件设备，用于连接多台设备（如计算机、服务器、打印机等），使它们能够在一个局域网（LAN）内通过数据包进行通信，现如今，交换机是包括互联网在内的现代 IT 网络的基本组成部分之一，它解决了设备间通信的问题。</p><p>交换机接受一个设备的数据包，并且将数据包发送到指定的设备上去，从而实现设备间高效通信。交换机是如何知道它收到的数据包应该发往哪个设备呢？其实是通过 MAC 地址来实现的，交换机收到的网络报文头里包含了源 MAC 地址和目的 MAC 地址，然后交换机在它的地址表中找到对应的网络设备，从而将数据包发往指定的设备。关于交换机具体的工作原理，将在下面具体介绍。</p><h2 id="交换机类型"><a href="#交换机类型" class="headerlink" title="交换机类型"></a>交换机类型</h2><p>从网络模型上来看，我们说的交换机一般都是指的工作在二层网络模型上面的交换机，我们称为二层交换机，它通过检查数据帧的目标 MAC 地址，将数据帧发送到正确的目标设备，从而实现局域网内的数据通信，并通过构建 MAC 地址表来实现数据的快速转发和广播域的隔离。当然，也存在三层交换机，它具备路由功能，可以根据目标 IP 地址来进行数据包的转发，它能够根据网络层的信息进行智能的数据包转发，提高网络的性能和效率；同时，三层交换机具备执行路由的功能，它可以执行静态路由和动态路由，具有更快的切换速度，甚至比传统的路由器更快，三层交换机可以执行 IP 地址和子网划分来路由数据包，同时处理 VLAN 内通信以及不同 VLAN 之间的数据包路由。</p><p>下面是二层交换机和三层交换机的特点：</p><table><thead><tr><th align="center">二层交换机</th><th align="center">三层交换机</th></tr></thead><tbody><tr><td align="center">在 OSI 模型的第二层（数据链路层）上工作</td><td align="center">在 OSI 模型的第三层（网络层）上工作</td></tr><tr><td align="center">根据 MAC 地址将数据包发送到目标设备</td><td align="center">根据 ip 地址路由数据包</td></tr><tr><td align="center">只能基于 MAC 地址工作</td><td align="center">可以执行 2 层和 3 层交换机的功能</td></tr><tr><td align="center">用于减少本地网络上的流量</td><td align="center">主要用于实现虚拟局域网（VLAN）</td></tr><tr><td align="center">不查看数据包的第 3 层部分，因此速度相当快</td><td align="center">在将数据包发送到目的地之前需要花费时间来检查数据包</td></tr><tr><td align="center">单广播域</td><td align="center">多个广播域</td></tr><tr><td align="center">只能在网络内通信</td><td align="center">可以在网络内和网络外通信</td></tr></tbody></table><p>通常二层交换机有如下使用场景：</p><ol><li>办公室局域网：在办公室环境中，二层交换机用于连接各个工作站、打印机和其他网络设备，实现内部局域网的通信和数据交换。</li><li>数据中心网络：在大型数据中心中，二层交换机用于连接服务器、存储设备和其他网络设备，提供高带宽、低延迟的数据交换和通信。</li><li>无线接入网络：在无线网络中，二层交换机通常用于连接无线接入点（Access Point），将无线设备连接到有线网络，并提供无线设备之间的数据转发和通信。</li></ol><p>三层交换机有如下使用场景：</p><ol><li>跨子网通信：在大型企业网络中，三层交换机用于实现不同子网之间的通信，通过路由功能将数据包从一个子网转发到另一个子网，实现跨网络的数据交换。</li><li>虚拟化环境：在虚拟化环境中，三层交换机用于连接虚拟机和物理服务器，提供虚拟机之间和虚拟机与物理服务器之间的高性能数据交换和通信。</li><li>多租户网络：在云计算环境或多租户网络中，三层交换机用于隔离不同租户的网络流量，提供安全的数据隔离和路由功能，确保不同租户之间的数据互不干扰。</li></ol><h2 id="交换机的连接方式"><a href="#交换机的连接方式" class="headerlink" title="交换机的连接方式"></a>交换机的连接方式</h2><p>根据网络的需求和拓扑结构的不同，交换机有多种不同的连接方式。</p><ol><li>直连连接（Point-to-Point）：两个交换机通过一根直接的物理电缆连接，适用于小型网络或相邻设备之间的连接。</li><li>级联连接（Daisy-Chaining）：多个交换机按顺序连接在一起，形成链状结构，用于扩展网络规模，但链的长度可能影响性能。</li><li>聚合连接（Link Aggregation）：将多个物理链路组合成一个逻辑上的连接，增加带宽、提高冗余。例如，MALG 就是一种聚合连接的方式。</li><li>星型连接（Star Topology）：交换机直连到一个中心设备，通常是网络中心交换机或者路由器，提供简单的管理和良好的冗余性。</li><li>环形连接（Ring Topology）：交换机按环形连接，提供冗余路径，但当其中一个连接中断时可能影响到整个环。</li><li>堆叠连接（Stacking）：多个交换机通过特殊的堆叠接口或电缆连接，形成逻辑上的单一设备，用于增加端口数量和提高性能。</li><li>交叉连接（Cross-Connect）：直接连接两个交换机，通常在大型数据中心或企业网络中使用，提高网络性能和减少延迟。</li></ol><h2 id="堆叠和-MLAG-比较"><a href="#堆叠和-MLAG-比较" class="headerlink" title="堆叠和 MLAG 比较"></a>堆叠和 MLAG 比较</h2><p>由于我们业务中，这两种形态比较多，这里着重介绍一下交换机堆叠和 MLAG 两种工作方式。</p><h3 id="堆叠"><a href="#堆叠" class="headerlink" title="堆叠"></a>堆叠</h3><p>交换机堆叠是通过将多个交换机连接在一起，形成一个逻辑上的单一设备，主要是用于扩展交换机端口数量和提高带宽，提高系统整体性能。这一组连接在物理上通过专用的堆叠接口或堆叠电缆连接。在逻辑上，这些堆叠的交换机被视为一个整体，共享一个管理 IP 地址、配置和操作状态。通过堆叠，可以统一管理整个堆叠，简化网络管理。</p><p>堆叠是提升系统的健壮性、提升性能的手段。</p><p><img src="https://img.cczywyc.com/switch_stacking.jpg"></p><h3 id="MLAG"><a href="#MLAG" class="headerlink" title="MLAG"></a>MLAG</h3><p>MLAG（MUlti-chassis Link Aggregation）是一种通过多个设备形成链路聚合组（LAG）以实现冗余的方法，MLAG 是通过对等链路连接两个交换机，并形成一个链路聚合组，以作为逻辑上的单一设备，可以使用该机制将更多的交换机添加到链路聚合组中。目前 MLAG 已经成为一种广泛应用的链路聚合方法，MLAG 拓扑结构可以在很大程度上扩展网络能力，它在 LAG 提供的传统链路级冗余基础上增加了节点级冗余，提高系统可靠性，并简化管理，在这个系统中，当其中一个交换机失效时，系统仍然可以工作。</p><p>MALG 是提升系统高可用性的手段。</p><p><img src="https://img.cczywyc.com/switch_mlag.jpg"></p><h3 id="MLAG-的高可用实现"><a href="#MLAG-的高可用实现" class="headerlink" title="MLAG 的高可用实现"></a>MLAG 的高可用实现</h3><p>总的说来，交换机 MLAG 技术通过多种手段确保了整个系统的可靠性和故障恢复能力，主要表现为以下几个关键方面：</p><ol><li>对等链路（Peer-Link）：MLAG 中的两个交换机通过对等链路进行连接。对等链路是一条专用链路，用于传输 MALG 协议消息和同步数据。通过对等链路，两个交换机之间可以实现实时的状态同步和故障检测。</li><li>逻辑单元：MLAG 中的多个交换机被视为一个逻辑单元，它们共享相同的 MAC 地址和配置信息。这意味着对外部设备而言，MALG 表现为一个单一的逻辑交换机，提供冗余和负载均衡。</li><li>心跳检测：MLAG 中的交换机通过对等链路进行心跳检测，以确保彼此的可用性。如果一台交换机无法检测到对等链路上的心跳信号，它将认为对方交换机发生故障，并接管系统对外提供服务。</li><li>数据同步：MLAG 中的交换机通过对等链路同步数据，以确保数据的一致性。当一台交换机接管另一台交换机的功能时，它会继续处理之前交换机处理的数据流，以实现无缝的故障转移。</li><li>网络拓扑：MLAG 的高可用性还取决于网络拓扑的设计。为了实现冗余和故障恢复，MLAG 中的交换机应该通过不同的物理路径连接到其他设备，以避免单点故障。</li></ol><p>这里再单独说一下 MLAG 中数据同步的问题。我们知道，在一个分布式系统中，数据的一致性是及其重要的，同时在一个分布式系统中实现数据的一致性也是及其复杂的。同样的，在交换机 MLAG 机制中，数据一致性也是必要的，它主要是通过对等链路实现数据一致性的。</p><p>这种数据一致性主要包括 MAC 地址同步和数据转发。每个交换机维护了一个共享的 MAC 地址表，用于转发数据包到指定的目标设备，当一个交换机学习到新的 MAC 地址时，它会通过对等链路将该信息发送给另外一个交换机，以确保两个交换机的 MAC 地址表保持一致。MLAG 交换机对外表现为一个逻辑单元，当这个逻辑单元收到数据包时，MLAG 交换机可以共同处理数据流量，并确保数据包按相同的规则进行转发。</p><h1 id="交换机的工作原理"><a href="#交换机的工作原理" class="headerlink" title="交换机的工作原理"></a>交换机的工作原理</h1><p>交换机的工作原理基于数据链路层的功能，我们知道数据包在网络上进行传输时，都是一个完整的数据包，这里说的完整的数据包指的是它应当包含网络模型中每一层的结构，一个不完整的数据包是无法在网络上传输的。</p><p>上面说到网络上传输的都是一个完整的数据包，每一层的数据包都有其特定的报文格式，一般来说一个即将发送的数据，最开始由上层协议组装，然后下层的数据包都是在上一层的数据包的基础上加上自己特有的数据头，这样数据经过层层组装，最终成为一个完整的包，最终发出去，并且每一层的数据包是在不同的地方进行组装和处理的，这里以 MAC 头和 IP 头为例简单描述一下：</p><p><img src="https://img.cczywyc.com/network_header.png"></p><p>比如说上图中的 MAC 头部分，就是由交换机组装的，交换机将数据组装成数据帧，其中包含了目标设备的 MAC 地址和源设备的 MAC 地址。搞明白了数据包在各层的组装方式以后，这里我用一个非常常用的 ping 的例子说一下交换机在数据包传输过程中的工作原理：</p><p><img src="https://img.cczywyc.com/ping_detail.png"></p><p>假设在一个简单的局域网内，192.168.1.1 的机器去 ping 192.168.1.2 的机器（ping 使用的 ICMP 协议不是本文讨论的范围，不作说明，只重点说明交换机的作用），首先这两台机器的 ip 地址在同一个网段内，它们是可以 ping 通的。</p><p>这里上来就会遇到第一个问题，上面我们说到数据要在网络上传输，数据包必须是完整的，但是很明显 192.168.1.1 这个机器只知道要 ping 的机器的 ip 地址，也就是192.168.1.2，它并不是对方的 MAC 地址是多少，应该怎么办呢？答案就是 ARP 协议（ARP 不在本文讨论的范围，不作说明）。</p><p>我们知道了机器 192.168.1.1 是通过发送 ARP 报文知道了机器 192.168.1.2 的 MAC 地址，然后组装好一个完整的数据包，数据包在交换机处理时，这时交换机会做一个事情，那就是 MAC 地址学习。数据包中包含了源设备（机器 192.168.1.1）的 MAC 地址，交换机会在地址表中查找是否有这个端口和源设备 MAC 地址的对应关系，如果没有，它会进行 MAC 地址学习，在地址表中新增该端口和源设备 MAC 地址的对应关系，或者更新该地址表。</p><p>接着，交换机会在地址表中查找目标设备 MAC 地址的记录，如果在地址表中找到了目标设备 MAC 地址对应的端口，就直接把数据包发往对应的端口。那么这里又来了新的问题，如果没有在地址表中找到呢？很容易想到，那就是广播，交换机会广播数据包到除了源设备端口以外的其他所有端口，数据包广播出去以后，目标设备在收到数据包后，会根据 MAC 地址判断这个数据包是否是发给自己的，如果是，它会发送一个响应。一旦目标设备响应，交换机会学习并记录新的 MAC 地址和端口的对应关系，新增地址表。</p><p>经过几轮的广播洪泛，就可以确保在交换机地址表中建立正确的 MAC 地址与端口的映射关系，从而提高网络转发的效率和准确性。需要注意的是，频繁的广播洪泛可能导致网络中的一些性能问题，因此交换机通常会采用一些优化策略，如 Aging Time（老化时间）来控制地址表中映射的生命周期，防止不必要的广播洪泛。</p><h1 id="VLAN"><a href="#VLAN" class="headerlink" title="VLAN"></a>VLAN</h1><p>上面说了交换机的工作原理，现在我们来思考两个问题：</p><p>广播问题：根据交换机的工作原理，很容易想到，随着网络内设备的不断增多，整个系统内广播包一大堆，性能势必就会下降。</p><p>安全问题：广播包每个设备都可以收到，试想如果局域网内每个设备都可以收到广播包，简单抓包就可以知道很多信息，会带来数据安全的问题。</p><p>尤其是现在云的时代，云计算设施和大型数据中心内，上述问题会尤为突出，为了解决这些问题，就需要做好隔离。网络隔离包括了传统意义上的物理隔离和虚拟隔离。</p><h4 id="物理隔离"><a href="#物理隔离" class="headerlink" title="物理隔离"></a>物理隔离</h4><p>先说物理隔离，每个需要隔离的逻辑单元都使用单独的交换机，配置单独的子网，可以解决上述问题，但是想象一下在大型企业或者大型数据中心里面，这种全面的物理隔离势必会增加大量的成本，所以目前这种手段并不是首选。</p><h4 id="虚拟隔离"><a href="#虚拟隔离" class="headerlink" title="虚拟隔离"></a>虚拟隔离</h4><p>还有一种方式就是虚拟隔离，这就是我们常用的 VLAN，全称叫做虚拟局域网。使用 VLAN，一个交换机上会连属于多个局域网的机器，交换机是根据 VLAN ID 来区分不同的机器所属的局域网的。具体的实现方式也很好理解，只需要在原来的数据包的二层头上加一个 TAG，里面有一个 VLAN ID，一共 12 位，最多可以划分 4096 个 VLAN。</p><p><img src="https://img.cczywyc.com/vlan.png"></p><p>如果我们买的交换机是支持 VLAN 的，当交换机把二层的头取下来的时候，就能够识别这个 VLAN ID，这样只有相同 VLAN 的包，才会相互转发，不同 VLAN的包，是看不到的，这样广播和安全的问题就都能解决了。</p><blockquote><p>交换机的 VLAN ID 是 12 位的，这是因为 IEEE 802.1Q 标准规定了 VLAN 标签的格式和长度。在 VLAN 标签中，有 16 位用于 VLAN 标识符（VLAN Identifier，简称 VID）。这 16 位中，有 12 位用于表示 VLAN 的编号，可以用来标识不同的 VLAN。</p><p>为什么 VLAN ID 只有 12 位呢？这是因为在 VLAN 标签中，还有其他的字段需要占用一部分位数。其中，2 位用于标识标签协议识别符（Tag Protocol Identifier，简称 TPID），用于识别帧是否为 IEEE 802.1Q 标签帧。另外，还有 1 位用于标识丢弃优先级（Drop Eligible Indicator，简称 DEI），用于指示在拥塞情况下是否可以丢弃该帧。最后，还有 1 位用于标识优先级代码点（Priority Code Point，简称 PCP），用于指定帧的优先级。</p><p>因此，剩下的 12 位就用于表示 VLAN 的编号，这样可以支持最多 4096 个不同的 VLAN。其中，编号为 0 和 4095 的 VLAN 是保留的，不能用于实际的 VLAN。所以实际可用的 VLAN 编号范围是从 1 到 4094。</p></blockquote><h1 id="GRE-VXLAN"><a href="#GRE-VXLAN" class="headerlink" title="GRE &amp; VXLAN"></a>GRE &amp; VXLAN</h1><p>上面说了 VLAN 这种虚拟局域网的实现，我们知道由于 VLAN ID 只有 12 位，因此它只能支持最多 4096 个不同的 VLAN，最初设计的时候看起来是够了，但是现如今在大型的数据中心或者云计算平台等地方显然是不够用的，该如何解决这个问题呢？</p><ul><li>一种方式是修改协议：显然是不现实、不可能的，已经形成标准的协议并且在千千万万的设备上都按照这个协议来跑的程序，不可能去做协议层面的修改。</li><li>另外一种方式就是扩展：在原来包格式的基础上扩展出一个头，里面包含足够用于区分租户的 ID，外层包的格式和原来的保持一样。</li></ul><p>那么机遇上面扩展的思路，就有了 Overlay 网络，这是一种基于物理网络的虚拟化网络实现。而 Overlay 网络的两个著名的实现有 GRE 和 VXLAN。</p><h2 id="GRE"><a href="#GRE" class="headerlink" title="GRE"></a>GRE</h2><p>先说 GRE，GRE 全称 Generic Routing Encapsulation，它是一种 IP-over-IP 的隧道技术。它将 IP 包封装在 GRE 包里，外面加上 IP 头，在隧道的一端封装数据包，并在通路上进行传输，到另外一端的时候解包。你可以认为 Tunnel 是一个虚拟的、点对点的连接。</p><p><img src="https://img.cczywyc.com/gre_format.webp"></p><p>上图可以看到，在 GRE 包头中，前 32 位是一定会有的，后面的都是可选的。包头里面的 key 字段，是一个 32 位的字段，里面存放的就是用于区分用户的 Tunnel ID，32 位足够了。</p><p>下面的格式类型是专门用于网络虚拟化的 GRE 包头格式，称为 NVGRE，它的网络号是 24 位，也完全够用了。</p><p>使用 GRE 隧道，我以下图的场景为例，说明一下数据包是如何传输的：</p><p><img src="https://img.cczywyc.com/gre_conn.webp"></p><p>这里面有两个网段，两个路由器，中间要通过 GRE 隧道进行通信，当隧道建立以后，会多出两个 Tunnel 端口，用于封包、解包。</p><ol><li>主机 A 在左边的网络，IP 地址为 192.168.1.102，它想要访问主机 B，主机 B 在右边的网络，IP 地址为 192.168.2.115。于是发送一个包，源地址为 192.168.1.102，目标地址为 192.168.2.115。因为要跨网段访问，于是根据默认的 default 路由表规则，要发给默认的网关 192.168.1.1，也即左边的路由器。</li><li>根据路由表，从左边的路由器，去 192.168.2.0&#x2F;24 这个网段，应该走一条 GRE 的隧道，从隧道一端的网卡 Tunnel0 进入隧道。</li><li>在 Tunnel 隧道的端点进行包的封装，在内部的 IP 头之外加上 GRE 头。对于 NVGRE 来讲，是在 MAC 头之外加上 GRE 头，然后加上外部的 IP 地址，也即路由器的外网 IP 地址。源 IP 地址为 172.17.10.10，目标 IP 地址为 172.16.11.10，然后从 E1 的物理网卡发送到公共网络里。</li><li>在公共网络里面，沿着路由器一跳一跳地走，全部都按照外部的公网 IP 地址进行。</li><li>当网络包到达对端路由器的时候，也要到达对端的 Tunnel0，然后开始解封装，将外层的 IP 头取下来，然后根据里面的网络包，根据路由表，从 E3 口转发出去到达服务器 B。</li></ol><h3 id="GRE-的问题"><a href="#GRE-的问题" class="headerlink" title="GRE 的问题"></a>GRE 的问题</h3><p>从上面我们知道，GRE 通过隧道的方式，很好地解决了 VLAN ID 不足的问题，但是 GRE 本身还是存在不少问题。</p><ul><li>Tunnel 数量问题。GRE 是一种点对点隧道，如果有三个网络，就需要在每两个网络之间建立一个隧道。如果网络数目增多，这样隧道的数目会呈指数性增长。</li><li>GRE 不支持组播，因此一个网络中的一个虚机发出一个广播帧后，GRE 会将其广播到所有与该节点有隧道连接的节点。</li><li>当前有很多防火墙和三层网络设备无法解析 GRE，因此它们无法对 GRE 封装包做合适的过滤和负载均衡。</li></ul><h2 id="VXLAN"><a href="#VXLAN" class="headerlink" title="VXLAN"></a>VXLAN</h2><p>VXLAN 全称叫做虚拟可扩展局域网，也是一种网络虚拟化技术标准，它允许单个物理网络被多个不同的组织或租户共享，而任何一个租户都无法看到其他任何租户的网络流量。</p><p>从技术实现上来看，VXLAN 通过在 4 层 UDP 数据包中封装 2 层 以太网帧，使其能够创建跨越 3 层 物理网络的虚拟化 2 层子网。每个分段的子网都由 VXLAN 网络标识符（VNI）唯一标识。在 VXLAN 网络实现中，执行数据包的封装和解包的实体叫做 VTEP（VXLAN Tunnel Endpoint），VTEP 可以是独立的网络设备，例如物理路由器或者交换机，也可以是部署在服务器上的虚拟交换机。VTEP 将以太网帧封装为 VXLAN 数据包，然后通过 IP 或其他 3 层网络发送到目标 VTEP，在那里进行解封并转发到目标服务器。为了支持无法独立作为 VTEP 运行的设备，例如裸金属服务器，一些硬件 VTEP 可以封装和解封数据包。VTEP 可以驻留在 Hypervisor 主机中，例如基于内核的虚拟机（KVM），以直接支持虚拟化工作负载。这种类型的 VTEP 被称为软件 VTEP。</p><p>从报文格式上来看，VXLAN 和 GRE 不同，VXLAN 是从二层外面就套了一个 VXLAN 的头，这里面包含的 VXLAN ID 为 24 位，非常够用了。在 VXLAN 头外面还封装了 UDP、IP，以及外层的 MAC 头。</p><p><img src="https://img.cczywyc.com/vxlan_format.webp"></p><p>和 GRE 端到端的隧道不同，VXLAN 不是点对点的，而是支持通过组播来定位目标机器的，不一定是这一端发出，另外一个端接收。当一个 VTEP 启动的时候，它们都需要通过 IGMP 协议加入到一个组播组，就像加入到一个微信群一样，所有发送到这个“微信群”里面的消息，大家都能收到，而当每个物理机上的虚拟机启动之后，VTEP 就知道，有一个新的 VM 上线了，它归我管。</p><p>下面我以一个云平台中的例子，说明这个通信过程。</p><p><img src="https://img.cczywyc.com/VTEP_example.webp"></p><p>如图，假设虚机 1、2、3 属于云中同一业务的机器，因此给他们划分了相同的 VXLAN ID，在机器上，我们分别可以知道它们的 IP 地址，于是可以在虚拟机 1 上 ping 虚拟机 2。这个时候虚拟机 1 并不知道虚拟机 2 的 MAC 地址，因此这个包是发不出去的，所以要先发送 ARP 广播。</p><p><img src="https://img.cczywyc.com/VTEP_ARP.webp"></p><p>ARP 请求到达 VTEP1 的时候，VTEP1 知道要访问一台“不归自己管”的机器，上面提到它不是加了一个组播组吗，于是就类似于在微信群里发送 @all 消息一样，问一下虚拟机 2 归睡管，于是 VTEP1 将 ARP 请求封装在 VXLAN 里面，组播出去。由于同一个组播组，VTEP2 和 VTEP3 当然都收到了消息，因此都会解开 VXLAN 包看，里面是一个 ARP。</p><p>VTEP3 在本地广播半天，没人回；同样地，VTEP2 也在本地广播，虚拟机 2 回了，于是 VTEP2 便知道了虚拟机 2 归它管，也知道了虚拟机 2 的 MAC 地址，同时，VTEP2 也学会了虚拟机 1 归 VTEP1 管，以后要找虚拟机 1，去找 VTEP1 就可以了。</p><p><img src="https://img.cczywyc.com/VTEP_ARP_revive.webp"></p><p>VTEP2 将 ARP 的回复封装在 VXLAN 里面，这次不用组播了，直接发回给 VTEP1。VTEP1 解开 VXLAN 的包，发现是 ARP 的回复，于是发给虚拟机 1。通过这次通信，VTEP1 也学到了，虚拟机 2 归 VTEP2 管，以后找虚拟机 2，去找 VTEP2 就可以了。虚拟机 1 的 ARP 得到了回复，知道了虚拟机 2 的 MAC 地址，于是就可以发送包了。</p><p><img src="https://img.cczywyc.com/VTEP_send.webp"></p><p>虚拟机 1 发给虚拟机 2 的包到达 VTEP1，它当然记得刚才学的东西，要找虚拟机 2，就去 VTEP2，于是将包封装在 VXLAN 里面，外层加上 VTEP1 和 VTEP2 的 IP 地址，发送出去。网络包到达 VTEP2 之后，VTEP2 解开 VXLAN 封装，将包转发给虚拟机 2。虚拟机 2 回复的包，到达 VTEP2 的时候，它当然也记得刚才学的东西，要找虚拟机 1，就去 VTEP1，于是将包封装在 VXLAN 里面，外层加上 VTEP1 和 VTEP2 的 IP 地址，也发送出去。网络包到达 VTEP1 之后，VTEP1 解开 VXLAN 封装，将包转发给虚拟机 1。</p><p>完成整个通信流程。</p><h3 id="VXLAN-和-EVPN"><a href="#VXLAN-和-EVPN" class="headerlink" title="VXLAN 和 EVPN"></a>VXLAN 和 EVPN</h3><p>EVPN（Ethernet VPN）是一种以太网虚拟专用网络技术，它提供了一种灵活、可扩展的解决方案，用于在广域网（WAN）中扩展以太网服务。EVPN 通过使用 BGP（边界网关协议）作为控制平面，将以太网的 2 层和 3 层连接扩展到跨越多个站点的网络中。当 VXLAN 与 EVPN 结合使用时，操作员可以在支持该标准并且属于同一 3 层网络的任何物理网络交换机上创建虚拟网络。例如，可以从交换机 A 获取一个端口，从交换机 B 获取两个端口，再从交换机 C 获取另外一个端口，然后构建一个虚拟网络，对所有连接的设备来说，它看起来就像是一个单一的物理网络，参与此虚拟网络的设备将无法看到其他 VXLAN 或底层网络的流量。下面是 VXLAN 和 EVPN 结合使用的一些主要优势：</p><ol><li>灵活的网络扩展：VXLAN 与 EVPN 结合使用可以在 3 层 IP 或 MPLS 网络上扩展 2 层网络，这意味着，我们可以在支持 EVPN 的任何物理网络交换机上创建虚拟网络，而不受物理布局和地理距离的限制，这使得网络扩展变得更加灵活和容易。</li><li>简化的网络管理：VXLAN 与 EVPN 结合使用可以将虚拟网络与物理基础设施分离，从而简化了网络管理。操作员可以在支持 EVPN 的交换机上创建虚拟网络，而无需关心底层物理网络的细节。这使得网络配置和管理变得更加简单和高效。</li><li>安全的网络分段：VXLAN 与 EVPN 结合使用可以实现安全的网络分段。每个虚拟网络都可以作为一个独立的分段，不同的虚拟网络之间的流量是隔离的。这样可以确保不同租户之间的流量互不干扰，提高了网络的安全性。</li><li>高性能和高扩展性：VXLAN 与 EVPN 结合使用可以提供高性能和可扩展性的网络解决方案。EVPN 使用 BGP（Border Gateway Protocol）作为控制平面，可以实现灵活的路由和转发。VXLAN 提供了灵活的数据平面封装和解封装，可以在大规模网络中实现高性能的数据传输。</li><li>多站点互联：VXLAN 与 EVPN 结合使用可以实现多站点互联。通过在不同数据中心的交换机上创建虚拟网络，并使用 EVPN 进行路由和转发，可以实现跨数据中心的网络互联。这使得数据中心之间的数据传输更加灵活和高效。</li></ol><p><img src="https://img.cczywyc.com/evpn-vxlan_diagram.png"></p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><p><a href="https://www.geeksforgeeks.org/difference-between-layer-2-and-layer-3-switches/">https://www.geeksforgeeks.org/difference-between-layer-2-and-layer-3-switches/</a></p><p><a href="https://documentation.meraki.com/MS/Layer_3_Switching/Comparing_Layer_3_and_Layer_2_Switches">https://documentation.meraki.com/MS/Layer_3_Switching/Comparing_Layer_3_and_Layer_2_Switches</a></p><p><a href="https://www.qsfptek.com/qt-news/switch-stacking-vs-mlag.html">https://www.qsfptek.com/qt-news/switch-stacking-vs-mlag.html</a></p><p><a href="https://www.practicalnetworking.net/stand-alone/vlans/">https://www.practicalnetworking.net/stand-alone/vlans/</a></p><p><a href="https://www.practicalnetworking.net/stand-alone/routing-between-vlans/">https://www.practicalnetworking.net/stand-alone/routing-between-vlans/</a></p><p><a href="https://time.geekbang.org/column/intro/100007101?tab=catalog">https://time.geekbang.org/column/intro/100007101?tab=catalog</a></p><p><a href="https://www.techtarget.com/searchnetworking/definition/virtual-LAN">https://www.techtarget.com/searchnetworking/definition/virtual-LAN</a></p><p><a href="https://www.n-able.com/blog/what-are-vlans">https://www.n-able.com/blog/what-are-vlans</a></p><p><a href="https://www.solarwinds.com/resources/it-glossary/vlan">https://www.solarwinds.com/resources/it-glossary/vlan</a></p><p><a href="https://en.wikipedia.org/wiki/Virtual_Extensible_LAN">https://en.wikipedia.org/wiki/Virtual_Extensible_LAN</a></p><p><a href="https://www.juniper.net/us/en/research-topics/what-is-vxlan.html">https://www.juniper.net/us/en/research-topics/what-is-vxlan.html</a></p><p><a href="https://www.juniper.net/us/en/research-topics/what-is-evpn-vxlan.html">https://www.juniper.net/us/en/research-topics/what-is-evpn-vxlan.html</a></p><p><a href="https://support.huawei.com/enterprise/en/doc/EDOC1100168670">https://support.huawei.com/enterprise/en/doc/EDOC1100168670</a></p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">介绍关于交换机入门和云计算中网络虚拟化相关的知识</summary>
    
    
    
    <category term="网络协议文章合集" scheme="https://cczywyc.com/categories/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="网络协议" scheme="https://cczywyc.com/tags/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/"/>
    
  </entry>
  
  <entry>
    <title>go feed 介绍</title>
    <link href="https://cczywyc.com/2023/11/11/go_parse_feed/"/>
    <id>https://cczywyc.com/2023/11/11/go_parse_feed/</id>
    <published>2023-11-11T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>上篇文章记录了我在服务器上搭建了自己的 RSS 服务，经过一段时间的体验使用，我发现 RSS 这种信息获取的方式的确很适合我。在深入使用的过程中，也逐渐总结了一些 tt-rss 的优缺点，先说我认为好的方面：</p><ul><li>丰富的主题</li><li>完善的插件系统</li><li>支持独立部署</li><li>功能齐全</li></ul><p>同时我也认为它有不好的地方（个人主观观点）：</p><ul><li>PHP 驱动实现，稍有笨重，不够”现代化“</li><li>虽然有较为丰富的主题，但整体 UI 风格不是特别喜欢</li><li>用户系统不够完善，可玩性不高</li><li>不支持除 RSS 订阅以外的形态，例如：网页、文章阅读清单功能</li></ul><p>基于上述，我决定自己开始探索这种类 RSS 阅读器的产品形态，自己做一个类 RSS 阅读器。</p><p>目前，大部分的博客网站都是支持 RSS 的，不支持 RSS 的网站，也有 RSS Hub 这样的产品帮助我们解决问题。RSS 文件订阅类型一般有 rss、atom、json类型，文件格式为 XML 和 JSON。网站将网站的内容信息整合到一个 RSS 文件中，这个文件我们一般称为 feed，也叫信源。在 RSS 文件里面具体包含了网站的标题、描述、作者信息、文章列表等信息，并且随着网站内容更新，RSS 文件也会随之更新，因此 RSS 阅读器通过定时解析 RSS 文件，就可以知道我们关注的网站有更新，实现了信息聚合。因此，最重要的过程就变成了两部分：</p><ol><li>生成 RSS 文件，搞定信息来源</li><li>解析 RSS 文件，搞定信息展示</li></ol><p>对于第 1 点来说，除了网站自身会提供 RSS 文件之外，也有诸如 RSS Hub 这样的项目，来帮助我们实现生成网站的 RSS 文件。第二部分，就是实现一个 RSS 阅读器的关键。这个过程本质不难，就是解析 RSS 文件，它们往往基于 XML 格式或者 JSON 格式，所以理论上我们只要解析到 XML 的 item 或者解析到 JSON 的节点，就能获取 RSS 文件的内容，从而获取网站的内容。</p><h1 id="RSS-内容解析"><a href="#RSS-内容解析" class="headerlink" title="RSS 内容解析"></a>RSS 内容解析</h1><p>在这之前，我们要搞清楚 RSS 文件都包含哪些内容，哪些内容是我们需要的，可以使用标准库来解析 XML 或者 JSON，当然 go 生态里面也有不少库已经帮我们做了这部分功能，这里我选择使用 <a href="https://github.com/mmcdole/gofeed">gofeed</a> 来解析 RSS 文件。</p><p>前面说到 RSS 文件有几种不同的类型，其中 rss 和 atom 类型都是基于 XML 格式的。例如 <a href="https://feeds.feedburner.com/ruanyifeng">阮一峰的网络日志</a> 就是 rss 类型的，<a href="https://blog.codingnow.com/atom.xml">云风</a> 的博客就是 atom 类型的，通过例子也比较容易看出两种类型的区别。</p><p>gofeed 解析 RSS 文件，用法也比较简单，下面是几种常见的用法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// parse feed from a rss URL</span></span><br><span class="line">fp := gofeed.NewParser()</span><br><span class="line">feed, _ := fp.ParseURL(<span class="string">&quot;http://feeds.twit.tv/twit.xml&quot;</span>)</span><br><span class="line">fmt.Println(feed)</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// parse feed from String(XML or JSON)</span></span><br><span class="line">feedData := <span class="string">`&lt;rss version=&quot;2.0&quot;&gt;</span></span><br><span class="line"><span class="string">&lt;channel&gt;</span></span><br><span class="line"><span class="string">&lt;title&gt;Sample Feed&lt;/title&gt;</span></span><br><span class="line"><span class="string">&lt;/channel&gt;</span></span><br><span class="line"><span class="string">&lt;/rss&gt;`</span></span><br><span class="line">fp := gofeed.NewParser()</span><br><span class="line">feed, _ := fp.ParseString(feedData)</span><br><span class="line">fmt.Println(feed)</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// parse feed from file(io.Reader)</span></span><br><span class="line">file, _ := os.Open(<span class="string">&quot;/path/to/a/file.xml&quot;</span>)</span><br><span class="line"><span class="keyword">defer</span> file.Close()</span><br><span class="line">fp := gofeed.NewParser()</span><br><span class="line">feed, _ := fp.Parse(file)</span><br><span class="line">fmt.Println(feed)</span><br></pre></td></tr></table></figure><p>上面代码示例可以看到，我们最终解析到的都是一个 feed 对象，那么这个 feed 结构体具体包含哪些内容呢，不同的 RSS 文件类型有什么区别呢，下面结合 gofeed 结构体中的定义具体说明。</p><h2 id="rss-类型"><a href="#rss-类型" class="headerlink" title="rss 类型"></a>rss 类型</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Feed is an RSS Feed</span></span><br><span class="line"><span class="keyword">type</span> Feed <span class="keyword">struct</span> &#123;</span><br><span class="line">Title               <span class="type">string</span>                   <span class="string">`json:&quot;title,omitempty&quot;`</span></span><br><span class="line">Link                <span class="type">string</span>                   <span class="string">`json:&quot;link,omitempty&quot;`</span></span><br><span class="line">Links               []<span class="type">string</span>                 <span class="string">`json:&quot;links,omitempty&quot;`</span></span><br><span class="line">Description         <span class="type">string</span>                   <span class="string">`json:&quot;description,omitempty&quot;`</span></span><br><span class="line">Language            <span class="type">string</span>                   <span class="string">`json:&quot;language,omitempty&quot;`</span></span><br><span class="line">Copyright           <span class="type">string</span>                   <span class="string">`json:&quot;copyright,omitempty&quot;`</span></span><br><span class="line">ManagingEditor      <span class="type">string</span>                   <span class="string">`json:&quot;managingEditor,omitempty&quot;`</span></span><br><span class="line">WebMaster           <span class="type">string</span>                   <span class="string">`json:&quot;webMaster,omitempty&quot;`</span></span><br><span class="line">PubDate             <span class="type">string</span>                   <span class="string">`json:&quot;pubDate,omitempty&quot;`</span></span><br><span class="line">PubDateParsed       *time.Time               <span class="string">`json:&quot;pubDateParsed,omitempty&quot;`</span></span><br><span class="line">LastBuildDate       <span class="type">string</span>                   <span class="string">`json:&quot;lastBuildDate,omitempty&quot;`</span></span><br><span class="line">LastBuildDateParsed *time.Time               <span class="string">`json:&quot;lastBuildDateParsed,omitempty&quot;`</span></span><br><span class="line">Categories          []*Category              <span class="string">`json:&quot;categories,omitempty&quot;`</span></span><br><span class="line">Generator           <span class="type">string</span>                   <span class="string">`json:&quot;generator,omitempty&quot;`</span></span><br><span class="line">Docs                <span class="type">string</span>                   <span class="string">`json:&quot;docs,omitempty&quot;`</span></span><br><span class="line">TTL                 <span class="type">string</span>                   <span class="string">`json:&quot;ttl,omitempty&quot;`</span></span><br><span class="line">Image               *Image                   <span class="string">`json:&quot;image,omitempty&quot;`</span></span><br><span class="line">Rating              <span class="type">string</span>                   <span class="string">`json:&quot;rating,omitempty&quot;`</span></span><br><span class="line">SkipHours           []<span class="type">string</span>                 <span class="string">`json:&quot;skipHours,omitempty&quot;`</span></span><br><span class="line">SkipDays            []<span class="type">string</span>                 <span class="string">`json:&quot;skipDays,omitempty&quot;`</span></span><br><span class="line">Cloud               *Cloud                   <span class="string">`json:&quot;cloud,omitempty&quot;`</span></span><br><span class="line">TextInput           *TextInput               <span class="string">`json:&quot;textInput,omitempty&quot;`</span></span><br><span class="line">DublinCoreExt       *ext.DublinCoreExtension <span class="string">`json:&quot;dcExt,omitempty&quot;`</span></span><br><span class="line">ITunesExt           *ext.ITunesFeedExtension <span class="string">`json:&quot;itunesExt,omitempty&quot;`</span></span><br><span class="line">Extensions          ext.Extensions           <span class="string">`json:&quot;extensions,omitempty&quot;`</span></span><br><span class="line">Items               []*Item                  <span class="string">`json:&quot;items&quot;`</span></span><br><span class="line">Version             <span class="type">string</span>                   <span class="string">`json:&quot;version&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的结构体属性较多，我们只需要关注几个即可</p><ul><li><p>Title：通常是网站的标题</p></li><li><p>Link：网站的 url 地址</p></li><li><p>Links：网站的链接集合，通常包含网站的 url，网站订阅链接的 URL 等</p></li><li><p>Description：网站的描述</p></li><li><p>Items：注意看，这也是一个结构体，它就是我们解析的网站的内容集合，我们 RSS 阅读器就是要解析这部分内容，并把它每一个节点内容展示出来。</p><p>  点开 Items，它的结构属性如下：</p>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Item is an RSS Item</span></span><br><span class="line"><span class="keyword">type</span> Item <span class="keyword">struct</span> &#123;</span><br><span class="line">Title         <span class="type">string</span>                   <span class="string">`json:&quot;title,omitempty&quot;`</span></span><br><span class="line">Link          <span class="type">string</span>                   <span class="string">`json:&quot;link,omitempty&quot;`</span></span><br><span class="line">Links         []<span class="type">string</span>                 <span class="string">`json:&quot;links,omitempty&quot;`</span></span><br><span class="line">Description   <span class="type">string</span>                   <span class="string">`json:&quot;description,omitempty&quot;`</span></span><br><span class="line">Content       <span class="type">string</span>                   <span class="string">`json:&quot;content,omitempty&quot;`</span></span><br><span class="line">Author        <span class="type">string</span>                   <span class="string">`json:&quot;author,omitempty&quot;`</span></span><br><span class="line">Categories    []*Category              <span class="string">`json:&quot;categories,omitempty&quot;`</span></span><br><span class="line">Comments      <span class="type">string</span>                   <span class="string">`json:&quot;comments,omitempty&quot;`</span></span><br><span class="line">Enclosure     *Enclosure               <span class="string">`json:&quot;enclosure,omitempty&quot;`</span></span><br><span class="line">Enclosures    []*Enclosure             <span class="string">`json:&quot;enclosures,omitempty&quot;`</span></span><br><span class="line">GUID          *GUID                    <span class="string">`json:&quot;guid,omitempty&quot;`</span></span><br><span class="line">PubDate       <span class="type">string</span>                   <span class="string">`json:&quot;pubDate,omitempty&quot;`</span></span><br><span class="line">PubDateParsed *time.Time               <span class="string">`json:&quot;pubDateParsed,omitempty&quot;`</span></span><br><span class="line">Source        *Source                  <span class="string">`json:&quot;source,omitempty&quot;`</span></span><br><span class="line">DublinCoreExt *ext.DublinCoreExtension <span class="string">`json:&quot;dcExt,omitempty&quot;`</span></span><br><span class="line">ITunesExt     *ext.ITunesItemExtension <span class="string">`json:&quot;itunesExt,omitempty&quot;`</span></span><br><span class="line">Extensions    ext.Extensions           <span class="string">`json:&quot;extensions,omitempty&quot;`</span></span><br><span class="line">Custom        <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>        <span class="string">`json:&quot;custom,omitempty&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>  这一部分，还是挑几个重要的属性说：</p><ul><li>Title：这里的 Title 代表的是文章标题</li><li>Link：文章的 url 地址</li><li>Description：文章的概要描述</li><li>Content：文章的完整内容</li></ul><p>  值得一提的是，不同的 RSS 服务提供方，这里面的字段会有稍微差别，比如说，有些网站提供的 RSS 订阅就只有 Description，没有 Content 属性（或者 Content 属性跟 Description 内容一样，并没有显示文章完整内容），所以你会看到在 RSS 阅读器上，有的订阅是可以完整显示内容的，有的只是显示了摘要，需要你点击到原网站才能看到完整的内容。比如 <a href="https://tech.meituan.com/">美团技术团队</a> 的 <a href="">RSS 文件</a>，它的 Content 就不完整，你在 tt-rss 里面看到的就是这个样子 <img src="https://img.cczywyc.com/tech-meituan.png"></p><p>  关于这一部分不能解析到完整内容的 RSS 文件，我将在后面单独说。</p><h2 id="atom-类型"><a href="#atom-类型" class="headerlink" title="atom 类型"></a>atom 类型</h2>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Feed is an Atom Feed</span></span><br><span class="line"><span class="keyword">type</span> Feed <span class="keyword">struct</span> &#123;</span><br><span class="line">Title         <span class="type">string</span>         <span class="string">`json:&quot;title,omitempty&quot;`</span></span><br><span class="line">ID            <span class="type">string</span>         <span class="string">`json:&quot;id,omitempty&quot;`</span></span><br><span class="line">Updated       <span class="type">string</span>         <span class="string">`json:&quot;updated,omitempty&quot;`</span></span><br><span class="line">UpdatedParsed *time.Time     <span class="string">`json:&quot;updatedParsed,omitempty&quot;`</span></span><br><span class="line">Subtitle      <span class="type">string</span>         <span class="string">`json:&quot;subtitle,omitempty&quot;`</span></span><br><span class="line">Links         []*Link        <span class="string">`json:&quot;links,omitempty&quot;`</span></span><br><span class="line">Language      <span class="type">string</span>         <span class="string">`json:&quot;language,omitempty&quot;`</span></span><br><span class="line">Generator     *Generator     <span class="string">`json:&quot;generator,omitempty&quot;`</span></span><br><span class="line">Icon          <span class="type">string</span>         <span class="string">`json:&quot;icon,omitempty&quot;`</span></span><br><span class="line">Logo          <span class="type">string</span>         <span class="string">`json:&quot;logo,omitempty&quot;`</span></span><br><span class="line">Rights        <span class="type">string</span>         <span class="string">`json:&quot;rights,omitempty&quot;`</span></span><br><span class="line">Contributors  []*Person      <span class="string">`json:&quot;contributors,omitempty&quot;`</span></span><br><span class="line">Authors       []*Person      <span class="string">`json:&quot;authors,omitempty&quot;`</span></span><br><span class="line">Categories    []*Category    <span class="string">`json:&quot;categories,omitempty&quot;`</span></span><br><span class="line">Entries       []*Entry       <span class="string">`json:&quot;entries&quot;`</span></span><br><span class="line">Extensions    ext.Extensions <span class="string">`json:&quot;extensions,omitempty&quot;`</span></span><br><span class="line">Version       <span class="type">string</span>         <span class="string">`json:&quot;version&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>  这是 atom 类型的结构体属性，大部分字段跟上面的 rss 类型差不多，不同的就是网站内容部分这里是 Entry，点开 Entry 属性，结构体属性如下：</p>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Entry is an Atom Entry</span></span><br><span class="line"><span class="keyword">type</span> Entry <span class="keyword">struct</span> &#123;</span><br><span class="line">Title           <span class="type">string</span>         <span class="string">`json:&quot;title,omitempty&quot;`</span></span><br><span class="line">ID              <span class="type">string</span>         <span class="string">`json:&quot;id,omitempty&quot;`</span></span><br><span class="line">Updated         <span class="type">string</span>         <span class="string">`json:&quot;updated,omitempty&quot;`</span></span><br><span class="line">UpdatedParsed   *time.Time     <span class="string">`json:&quot;updatedParsed,omitempty&quot;`</span></span><br><span class="line">Summary         <span class="type">string</span>         <span class="string">`json:&quot;summary,omitempty&quot;`</span></span><br><span class="line">Authors         []*Person      <span class="string">`json:&quot;authors,omitempty&quot;`</span></span><br><span class="line">Contributors    []*Person      <span class="string">`json:&quot;contributors,omitempty&quot;`</span></span><br><span class="line">Categories      []*Category    <span class="string">`json:&quot;categories,omitempty&quot;`</span></span><br><span class="line">Links           []*Link        <span class="string">`json:&quot;links,omitempty&quot;`</span></span><br><span class="line">Rights          <span class="type">string</span>         <span class="string">`json:&quot;rights,omitempty&quot;`</span></span><br><span class="line">Published       <span class="type">string</span>         <span class="string">`json:&quot;published,omitempty&quot;`</span></span><br><span class="line">PublishedParsed *time.Time     <span class="string">`json:&quot;publishedParsed,omitempty&quot;`</span></span><br><span class="line">Source          *Source        <span class="string">`json:&quot;source,omitempty&quot;`</span></span><br><span class="line">Content         *Content       <span class="string">`json:&quot;content,omitempty&quot;`</span></span><br><span class="line">Extensions      ext.Extensions <span class="string">`json:&quot;extensions,omitempty&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>  这种 atom 类型下，Summary 表示的就是网站概要总结，Content 表示的就是网站的完整内容。</p><h2 id="json-类型"><a href="#json-类型" class="headerlink" title="json 类型"></a>json 类型</h2><p>  Json 类型跟字段属性跟上面两种类型差别不大，由于平时本人见的也不多，这里就不贴代码了，具体的代码见 <a href="https://github.com/mmcdole/gofeed/blob/master/json/feed.go">gofeed 仓库</a>。</p><h2 id="gofeed-支持"><a href="#gofeed-支持" class="headerlink" title="gofeed 支持"></a>gofeed 支持</h2><p>  说完上述三种类型的 RSS 订阅文件，再来看一下 gofeed 的支持。gofeed 对这三种类型的 RSS 订阅文件也都有直接的支持。</p>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// RSS Feed</span></span><br><span class="line"></span><br><span class="line">feedData := <span class="string">`&lt;rss version=&quot;2.0&quot;&gt;</span></span><br><span class="line"><span class="string">&lt;channel&gt;</span></span><br><span class="line"><span class="string">&lt;webMaster&gt;example@site.com (Example Name)&lt;/webMaster&gt;</span></span><br><span class="line"><span class="string">&lt;/channel&gt;</span></span><br><span class="line"><span class="string">&lt;/rss&gt;`</span></span><br><span class="line">fp := rss.Parser&#123;&#125;</span><br><span class="line">rssFeed, _ := fp.Parse(strings.NewReader(feedData))</span><br><span class="line">fmt.Println(rssFeed.WebMaster)</span><br></pre></td></tr></table></figure>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Atom Feed</span></span><br><span class="line"></span><br><span class="line">feedData := <span class="string">`&lt;feed xmlns=&quot;http://www.w3.org/2005/Atom&quot;&gt;</span></span><br><span class="line"><span class="string">&lt;subtitle&gt;Example Atom&lt;/subtitle&gt;</span></span><br><span class="line"><span class="string">&lt;/feed&gt;`</span></span><br><span class="line">fp := atom.Parser&#123;&#125;</span><br><span class="line">atomFeed, _ := fp.Parse(strings.NewReader(feedData))</span><br><span class="line">fmt.Println(atomFeed.Subtitle)</span><br></pre></td></tr></table></figure>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JSON Feed</span></span><br><span class="line"></span><br><span class="line">feedData := <span class="string">`&#123;&quot;version&quot;:&quot;1.0&quot;, &quot;home_page_url&quot;: &quot;https://daringfireball.net&quot;&#125;`</span></span><br><span class="line">fp := json.Parser&#123;&#125;</span><br><span class="line">jsonFeed, _ := fp.Parse(strings.NewReader(feedData))</span><br><span class="line">fmt.Println(jsonFeed.HomePageURL)</span><br></pre></td></tr></table></figure><p>  这些用法在 gofeed 官方仓库都有对应说明</p><h1 id="no-Content-的场景"><a href="#no-Content-的场景" class="headerlink" title="no Content 的场景"></a>no Content 的场景</h1><p>  上面说到，有些订阅的 RSS 文件，并没有网站的完整内容，对于这种场景，应该如何解析呢？</p><p>  普遍的做法是类似 tt-rss 这样，在没有 Content 的情况下，解析 Description 显示，只显示内容摘要，可以通过点击标题进原地址看完整内容。在这种情况下，如果我想解析到网站完整的内容并且直接在 RSS 阅读器中展示应该怎么办呢？</p><ol><li>容易想到，一种方式可以通过文章的原文链接，爬取原文网页上的内容。这种通过爬虫的方式，及其不稳定，主要涉及到不同网站样式布局会有不同的表现，需要针对不同的网站的 RSS 文件做单独适配。</li><li>另外一种，我有考虑过在 RSS 阅读器的内容显示区域，直接调用浏览器的能力根据原文地址链接直接渲染原网页。</li></ol><p>  上面两种方式是我一开始就想到的方式，但是在我看来，这两种方式实现起来并不优雅，似乎不是一个好的解决方案，所以这两种方式我现阶段并没有去做验证和调研。在写这篇文章的时候，我突然想到现在 AI 大模型这么火，这个场景是不是能接入大模型实现的更优雅一点呢？比如说，对这种无法解析完整文章内容的场景，我可以借助原文的地址链接让 AI 大模型总结提取文章内容（互联网接入功能），我知道的目前 Microsoft Bing Chat 是提供互联网接入功能的，尝试了一下是可以读取原文完整内容的。</p><p>  最后，对于这种利用 AI 大模型的做法，我接下来应该会详细做一个验证，到时候可能会单独写一篇文章讲述整个验证过程，证明其是否具有可行性。</p><p>  （本文完）</p></li></ul>]]></content>
    
    
    <summary type="html">如何使用 golang 精确解析 rss 订阅</summary>
    
    
    
    <category term="Golang 技术文章合集" scheme="https://cczywyc.com/categories/Golang-%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="Golang" scheme="https://cczywyc.com/tags/Golang/"/>
    
    <category term="RSS" scheme="https://cczywyc.com/tags/RSS/"/>
    
  </entry>
  
  <entry>
    <title>搭建了自己的 RSS 服务</title>
    <link href="https://cczywyc.com/2023/10/26/%E6%90%AD%E5%BB%BA%20RSS%20%E6%9C%8D%E5%8A%A1/"/>
    <id>https://cczywyc.com/2023/10/26/%E6%90%AD%E5%BB%BA%20RSS%20%E6%9C%8D%E5%8A%A1/</id>
    <published>2023-10-26T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>先来简单说一下什么是 RSS 吧，RSS 全称叫做 Really Simple Syndication，中文叫做简易信息聚合，也叫聚合内容，它是一种消息来源格式规范，用以聚合多个网站更新的内容并自动通知网站订阅者。使用 RSS 后，网站订阅者便无需手动查看网站是否有新的内容，同时 RSS 可将多个网站更新的内容进行整合，以摘要的形式呈现，有助于订阅者快速获取重要信息，并选择性地点阅查看。说的简单点就是它可以把各种信息源整合起来，我只需要在一个 RSS 服务里面就可以获取到来自各种不同渠道的订阅更新，现在我们看到个很多技术博客和网站都提供了 RSS 订阅链接，细心的话你应该在很多地方就见过这个东西。</p><p>说来奇怪，RSS 本应是上个时代的产物，近些年缺被越来越多的人追捧，在目前这个信息爆炸的时代，各种信息流充满着我们的屏幕，不同的信息平台也有着不同的推送方式，导致信息获取破碎，无法筛选自己真正感兴趣的信息。如果你跟我有差不多同样的想法，那么，搭建一个自己的 RSS 服务就显的很有必要了。</p><p>我的信息获取来源，一方面是一些微信公众号，还有一些技术博客和刊栏，导致我的信息获取非常的分散不集中，于是便萌生了搭建一个自己的 RSS 服务的想法，在进行了一番调研之后，我的其中一个很重要的要求就是要能独立部署，所以在抛弃了众多优秀的 RSS 阅读器之后，<a href="https://tt-rss.org/">tt-rss</a> 似乎成为了我最佳的选择，那么说干就干。</p><h1 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h1><p>要搭建一个自己的 RSS 服务，首先需要一个主机，可以是物理机，当然也可以是云主机，这里我选择一个 Ubuntu 服务器来搭建，使用云服务器也是现在更容易选择的便捷方式。具体需要准备的如下：</p><ul><li>云服务器：Ubuntu 22.04 LTS</li><li>域名：xxx.xxx</li><li>SSL 证书</li></ul><p>需要说明的是，域名和 SSL 证书是必要的，一方面我们尽量不要使用 ip 访问我们的网站，另外为了安全起见，应当使用 HTTPS 访问我们的 RSS 服务。基本上每一个云服务厂商都提供了域名和 SSL 证书的申请，域名首单购买超级便宜，SSL 证书则基本都有免费的测试证书，对于搭建个人的 RSS 服务来说应该是够用了。域名和 SSL 的配置各大云服务厂商都有很详细的配置说明，这里就不作展开。</p><h1 id="服务搭建"><a href="#服务搭建" class="headerlink" title="服务搭建"></a>服务搭建</h1><p>这里我选择的是 tt-rss，由于它依赖于 postgres 数据库和其他的服务，可以按照官网的方式搭建，社区也有人提供了 docker-compose 的方式，可以全部把需要的服务都以 docker 容器的方式启动。这里我强烈推荐 <a href="https://ttrss.henry.wang/#about">Awesome TTRSS</a> 来快速搭建 RSS 服务。</p><h4 id="安装-docker-和-docker-compose"><a href="#安装-docker-和-docker-compose" class="headerlink" title="安装 docker 和 docker-compose"></a>安装 docker 和 docker-compose</h4><p>docker 的安装有很多种方式，首选的当然是官网的教程，<a href="https://docs.docker.com/engine/install/ubuntu/">docker 官网</a>针对于不同的操作系统都提供了很详细的安装方式。比如以 Ubuntu 为例，官网就写出了很详细的教程：</p><p>设置 docker 源</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># set up docker&#x27;s apt repository</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Add Docker&#x27;s official GPG key:</span></span><br><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install ca-certificates curl gnupg</span><br><span class="line"><span class="built_in">sudo</span> install -m 0755 -d /etc/apt/keyrings</span><br><span class="line">curl -fsSL https://download.docker.com/linux/ubuntu/gpg | <span class="built_in">sudo</span> gpg --dearmor -o /etc/apt/keyrings/docker.gpg</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> a+r /etc/apt/keyrings/docker.gpg</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add the repository to Apt sources:</span></span><br><span class="line"><span class="built_in">echo</span> \</span><br><span class="line">  <span class="string">&quot;deb [arch=&quot;</span>$(dpkg --print-architecture)<span class="string">&quot; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \</span></span><br><span class="line"><span class="string">  &quot;</span>$(. /etc/os-release &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$VERSION_CODENAME</span>&quot;</span>)<span class="string">&quot; stable&quot;</span> | \</span><br><span class="line">  <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/docker.list &gt; /dev/null</span><br><span class="line"><span class="built_in">sudo</span> apt-get update</span><br></pre></td></tr></table></figure><p>安装 docker</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># install docker packages</span></span><br><span class="line"><span class="built_in">sudo</span> apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span><br></pre></td></tr></table></figure><p>安装 docker-compose</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># install docker-compose</span></span><br><span class="line"><span class="built_in">sudo</span> apt-get install docker-compose</span><br></pre></td></tr></table></figure><p>检查是否安装成功</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># check docker</span></span><br><span class="line">docker -v</span><br><span class="line">Docker version 24.0.7, build afdd53b</span><br><span class="line"></span><br><span class="line"><span class="comment"># check docker-compose</span></span><br><span class="line">docker-compose -v</span><br><span class="line">docker-compose version 1.29.2, build unknown</span><br></pre></td></tr></table></figure><p>当出现上面的回显，说明 docker 和 docker-compose 安装成功</p><h4 id="安装-tt-rss-相关服务"><a href="#安装-tt-rss-相关服务" class="headerlink" title="安装 tt-rss 相关服务"></a>安装 tt-rss 相关服务</h4><p>去 AweSome-TTRSS <a href="https://github.com/HenryQW/Awesome-TTRSS">github 仓库</a> 下载 <a href="https://github.com/HenryQW/Awesome-TTRSS/blob/main/docker-compose.yml">docker-compose.yml</a>，示例如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&quot;3&quot;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">service.rss:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wangqiru/ttrss:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">ttrss</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">181</span><span class="string">:80</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SELF_URL_PATH=https://your_domain/</span> <span class="comment"># please change to your own domain</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DB_PASS=ttrss</span> <span class="comment"># use the same password defined in `database.postgres`</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">PUID=1000</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">PGID=1000</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">feed-icons:/var/www/feed-icons/</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">public_access</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">service_only</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">database_only</span></span><br><span class="line">    <span class="attr">stdin_open:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">tty:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">service.mercury:</span> <span class="comment"># set Mercury Parser API endpoint to `service.mercury:3000` on TTRSS plugin setting page</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wangqiru/mercury-parser-api:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">mercury</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">public_access</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">service_only</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">service.opencc:</span> <span class="comment"># set OpenCC API endpoint to `service.opencc:3000` on TTRSS plugin setting page</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wangqiru/opencc-api-server:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">opencc</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">NODE_ENV=production</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">service_only</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">database.postgres:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:13-alpine</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">postgres</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=ttrss</span> <span class="comment"># feel free to change the password</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">~/postgres/data/:/var/lib/postgresql/data</span> <span class="comment"># persist postgres data to ~/postgres/data/ on the host</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">database_only</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># utility.watchtower:</span></span><br><span class="line">  <span class="comment">#   container_name: watchtower</span></span><br><span class="line">  <span class="comment">#   image: containrrr/watchtower:latest</span></span><br><span class="line">  <span class="comment">#   volumes:</span></span><br><span class="line">  <span class="comment">#     - /var/run/docker.sock:/var/run/docker.sock</span></span><br><span class="line">  <span class="comment">#   environment:</span></span><br><span class="line">  <span class="comment">#     - WATCHTOWER_CLEANUP=true</span></span><br><span class="line">  <span class="comment">#     - WATCHTOWER_POLL_INTERVAL=86400</span></span><br><span class="line">  <span class="comment">#   restart: always</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">feed-icons:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line">  <span class="attr">public_access:</span> <span class="comment"># Provide the access for ttrss UI</span></span><br><span class="line">  <span class="attr">service_only:</span> <span class="comment"># Provide the communication network between services only</span></span><br><span class="line">    <span class="attr">internal:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">database_only:</span> <span class="comment"># Provide the communication between ttrss and database only</span></span><br><span class="line">    <span class="attr">internal:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>其中第 7 行，是 ttrss 服务端口，这里默认是 181 端口映射到容器里面的 80 端口，映射出来的端口可以自行修改；第 9 行就是 ttrss 的访问地址，这里设置成你的域名；最后是第 10 行和第 44 行，设置 pg 的密码，两个地方成一样的就行。</p><p>利用 docker-compose 启动容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># at your docker-compose.yml dir</span></span><br><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><p>执行完成后，检查容器是否启动成功：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># chekc docker containers</span></span><br><span class="line">docker ps</span><br><span class="line"></span><br><span class="line">CONTAINER ID   IMAGE                                COMMAND                  CREATED        STATUS        PORTS                                   NAMES</span><br><span class="line">9d7a30db8f54   postgres:13-alpine                   <span class="string">&quot;docker-entrypoint.s…&quot;</span>   44 hours ago   Up 44 hours                                           postgres</span><br><span class="line">30f0ca736138   wangqiru/mercury-parser-api:latest   <span class="string">&quot;dumb-init -- npm ru…&quot;</span>   44 hours ago   Up 44 hours   3000/tcp                                mercury</span><br><span class="line">918e35b76206   wangqiru/opencc-api-server:latest    <span class="string">&quot;docker-entrypoint.s…&quot;</span>   44 hours ago   Up 44 hours                                           opencc</span><br><span class="line">aff0d1127aac   wangqiru/ttrss:latest                <span class="string">&quot;sh /docker-entrypoi…&quot;</span>   44 hours ago   Up 44 hours   0.0.0.0:181-&gt;80/tcp, :::181-&gt;80/tcp   ttrss</span><br></pre></td></tr></table></figure><p>当看到有 4 个容器时，则启动成功</p><h4 id="设置-HTTPS-访问"><a href="#设置-HTTPS-访问" class="headerlink" title="设置 HTTPS 访问"></a>设置 HTTPS 访问</h4><p>在服务器上安装 nginx，以 Ubuntu 为例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># install nginx</span></span><br><span class="line"><span class="built_in">sudo</span> apt install nginx</span><br></pre></td></tr></table></figure><p>编辑 nginx.conf：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/nginx/nginx.conf</span></span><br><span class="line"></span><br><span class="line">user root;</span><br><span class="line">worker_processes auto;</span><br><span class="line">pid /run/nginx.pid;</span><br><span class="line">include /etc/nginx/modules-enabled/*.conf;</span><br><span class="line"></span><br><span class="line">events &#123;</span><br><span class="line">worker_connections 768;</span><br><span class="line"><span class="comment"># multi_accept on;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line"></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"><span class="comment"># Basic Settings</span></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"></span><br><span class="line">sendfile on;</span><br><span class="line">tcp_nopush on;</span><br><span class="line">types_hash_max_size 2048;</span><br><span class="line"><span class="comment"># server_tokens off;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># server_names_hash_bucket_size 64;</span></span><br><span class="line"><span class="comment"># server_name_in_redirect off;</span></span><br><span class="line"></span><br><span class="line">include /etc/nginx/mime.types;</span><br><span class="line">default_type application/octet-stream;</span><br><span class="line"></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"><span class="comment"># SSL Settings</span></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"></span><br><span class="line">ssl_session_timeout 5m;</span><br><span class="line">ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;</span><br><span class="line">ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; <span class="comment"># Dropping SSLv3, ref: POODLE</span></span><br><span class="line">ssl_prefer_server_ciphers on;</span><br><span class="line"></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"><span class="comment"># Logging Settings</span></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"></span><br><span class="line">access_log /var/log/nginx/access.log;</span><br><span class="line">error_log /var/log/nginx/error.log;</span><br><span class="line"></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"><span class="comment"># Gzip Settings</span></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"></span><br><span class="line">gzip on;</span><br><span class="line"></span><br><span class="line"><span class="comment"># gzip_vary on;</span></span><br><span class="line"><span class="comment"># gzip_proxied any;</span></span><br><span class="line"><span class="comment"># gzip_comp_level 6;</span></span><br><span class="line"><span class="comment"># gzip_buffers 16 8k;</span></span><br><span class="line"><span class="comment"># gzip_http_version 1.1;</span></span><br><span class="line"><span class="comment"># gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"><span class="comment"># Virtual Host Configs</span></span><br><span class="line"><span class="comment">##</span></span><br><span class="line"></span><br><span class="line">include /etc/nginx/conf.d/*.conf;</span><br><span class="line">include /etc/nginx/sites-enabled/*;</span><br><span class="line"></span><br><span class="line">upstream rss &#123;</span><br><span class="line">server 127.0.0.1:181;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">listen 443 ssl;</span><br><span class="line">server_name your_domain.com;</span><br><span class="line"></span><br><span class="line">ssl_certificate crt_file_path;</span><br><span class="line">ssl_certificate_key key_file_path;</span><br><span class="line"></span><br><span class="line">location / &#123;</span><br><span class="line">      proxy_redirect off;</span><br><span class="line">proxy_pass http://rss;</span><br><span class="line"></span><br><span class="line">      proxy_set_header  Host                <span class="variable">$http_host</span>;</span><br><span class="line">      proxy_set_header  X-Real-IP           <span class="variable">$remote_addr</span>;</span><br><span class="line">      proxy_set_header  X-Forwarded-Ssl     on;</span><br><span class="line">      proxy_set_header  X-Forwarded-For     <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">      proxy_set_header  X-Forwarded-Proto   <span class="variable">$scheme</span>;</span><br><span class="line">      proxy_set_header  X-Frame-Options     SAMEORIGIN;</span><br><span class="line"></span><br><span class="line">      client_max_body_size        100m;</span><br><span class="line">      client_body_buffer_size     128k;</span><br><span class="line"></span><br><span class="line">      proxy_buffer_size           4k;</span><br><span class="line">      proxy_buffers               4 32k;</span><br><span class="line">      proxy_busy_buffers_size     64k;</span><br><span class="line">      proxy_temp_file_write_size  64k;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">listen 80;</span><br><span class="line">    server_name  your_domain.com;</span><br><span class="line">    <span class="built_in">return</span> 301 https://your_domain.com<span class="variable">$request_uri</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面 nginx 的配置大家应该都能看懂，第 72 行配置成你的域名，同理在第 100 行也配置成你的域名，http 的访问重定向到 https；74、75 行 配置 ssl 证书路径；66-68 行配置 nginx 代理的地址，因为这里 tt-rss 服务和 nginx 安装在同一台机器，所以 配置成 127.0.0.1:181，端口就是在上面 docker-compose.yml 文件里面配置的服务映射端口。</p><p>最后就是重启 nginx 服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># restart nginx service</span></span><br><span class="line">systemctl restart nginx</span><br></pre></td></tr></table></figure><p>最后在浏览器访问 <a href="https://your_domain/">https://your_domain</a> 正常情况下就能看到如下页面，默认用户名和密码是 admin&#x2F;passoword，登陆成功后在偏好设置里面可以更改管理员密码</p><p><img src="https://img.cczywyc.com/tt-rss.png"></p><p>(本文完)</p>]]></content>
    
    
    <summary type="html">在服务器上部署自己的 RSS 服务</summary>
    
    
    
    <category term="网站环境搭建教程合集" scheme="https://cczywyc.com/categories/%E7%BD%91%E7%AB%99%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%E6%95%99%E7%A8%8B%E5%90%88%E9%9B%86/"/>
    
    
    <category term="工具资源" scheme="https://cczywyc.com/tags/%E5%B7%A5%E5%85%B7%E8%B5%84%E6%BA%90/"/>
    
    <category term="折腾" scheme="https://cczywyc.com/tags/%E6%8A%98%E8%85%BE/"/>
    
  </entry>
  
  <entry>
    <title>聊聊 Java21 新特性</title>
    <link href="https://cczywyc.com/2023/09/29/Java21_feature/"/>
    <id>https://cczywyc.com/2023/09/29/Java21_feature/</id>
    <published>2023-09-29T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>2023 年 09 年 20 日，Java21 正式发布，这是最新的 LTS 版本，官方预计会对此版本提供至少 8 年的维护。这一次 JDK 的升级是否会引起 Java8 项目升级暂不清楚，但是此版本带来了诸如虚拟线程等重量级功能更新，可能会导致 Java21 版本将会成为未来几年最经典的 JDK 版本，就像曾经的 Java8 一样。</p><h1 id="新功能介绍"><a href="#新功能介绍" class="headerlink" title="新功能介绍"></a>新功能介绍</h1><p>此次 Java21 版本一共带来了 15 个新功能更新，具体可以见官方的<a href="https://openjdk.org/projects/jdk/21/">发布页面</a>，一眼望去，其中最重磅的更新摸过于 JEP444，也就是我们所说的虚拟线程，一般我们在其他语言（比如 golang）里面叫做协程。除此之外，还有诸如分代 ZGC、Switch 模式匹配等实用功能的更新，以下是 Java21 版本所有的新功能：</p><table><thead><tr><th align="center">提案</th><th align="center">标题</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center"><a href="https://openjdk.org/jeps/430">JEP430</a></td><td align="center">String Templates (Preview)</td><td align="center">字符串模版。Java 语言现有字符串文本和文本块增加功能，其他常用语言均已提供相应的功能用法</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/431">JEP431</a></td><td align="center">Sequenced Collections</td><td align="center">序列集合。该 JEP 提议引入”一个新的接口族，用于表示集合的概念，这些集合的元素按照预定义的序列或顺序排列，它们是作为集合的结构属性”。这一提案的动机是由于集合框架中缺乏预定义的顺序和统一的操作集。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/439">JEP439</a></td><td align="center">Generational ZGC</td><td align="center">分代 ZGC。通过扩展 Z 垃圾收集器（ZGC）来维护年轻对象和年老对象的独立生成，从而提高应用程序性能。这将使 ZGC 能够更频繁地收集年轻对象 – 这些对象往往英年早逝。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/440">JEP440</a></td><td align="center">Record Patterns</td><td align="center">记录模式。使用记录模式增强 Java 语言，以解构记录值。可以嵌套记录模式和类型模式，以实现功能强大、声明性和可组合形式的数据导航和处理。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/441">JEP441</a></td><td align="center">Pattern Matching for switch</td><td align="center">switch 增强。通过将模式匹配扩展到 switch语句，可以针对多个模式测试表达式，每个模式都有一个特定的操作，从而可以简洁、安全地表达复杂的面相数据的查询。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/442">JEP442</a></td><td align="center">Foreign Function &amp; Memory API (Third Preview)</td><td align="center">外部函数和内存 API。Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过有效地调用外部函数（即 JVM 外部的代码），并通过安全地访问外部内存（即不受 JVM 管理的内存），API 使 Java 程序能够调用本机库并处理本机数据，而不会出现 JNI 的脆弱性和危险性。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/443">JEP443</a></td><td align="center">Unnamed Patterns and Variables (Preview)</td><td align="center">未命名模式和变量。未命名模式匹配记录组件而不说明组件的名称或类型，未命名变量可以初始化但不使用。两者都用下划线字符_表示。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/444">JEP444</a></td><td align="center">Virtual threads</td><td align="center">虚拟线程。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/445">JEP445</a></td><td align="center">Unnamed classed and Instance Main Methods (Preview)</td><td align="center">未命名类和实例主方法。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/446">JEP446</a></td><td align="center">Scoped Values (Preview)</td><td align="center">作用域值。引入作用域值，这些值可以在不使用方法参数的情况下安全有效地共享给方法。它们优先于线程化局部变量，尤其是在使用大量虚拟线程时。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/448">JEP448</a></td><td align="center">Vector API (Sixth Incubator)</td><td align="center">向量计算 API，该功能还在孵化阶段。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/449">JEP449</a></td><td align="center">Deprecate the Windows 32-bit x86 Port for Removal</td><td align="center">弃用 Windows 32 位 x86 移植，并打算在将来的版本中将其删除。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/451">JEP451</a></td><td align="center">Prepare to Disallow the Dynamic Loading of Agents</td><td align="center">准备禁止动态加载代理。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/452">JEP452</a></td><td align="center">Key Encapsulation Methanism API</td><td align="center">密钥封装机制 API。一种用于密钥封装机制的 API，这是一种实用公钥加密来保护对称密钥的加密技术。</td></tr><tr><td align="center"><a href="https://openjdk.org/jeps/453">JEP453</a></td><td align="center">Structured Concurrency (Preview)</td><td align="center">结构化并发。结构化并发将在不同线程中运行的相关任务组视为单个工作单元，从而简化错误处理和消除，提高可靠性，并增强可观察性。</td></tr></tbody></table><p>以上就是此次 Java 21 带来的全部新功能更新，本文将针对大家关注比较多的几个功能作详细介绍。</p><h2 id="分代-ZGC"><a href="#分代-ZGC" class="headerlink" title="分代 ZGC"></a>分代 ZGC</h2><p>ZGC 最初是在 <a href="https://openjdk.org/jeps/333">JEP333</a> 中提出的，全称叫做可扩展的低延迟垃圾收集器，最开始是在 Java11 上作为实验性的功能提供。随后在 <a href="https://openjdk.org/jeps/377">JEP377</a> 中确定在 Java15 版本中正式生产环境可用。</p><p>ZGC (The Z Garbage Collector) 以低延迟著称，在设计之初，它的设计目标包括：</p><ul><li>停顿时间不超过 10ms；</li><li>停顿时间不会随着堆大小，或者活跃对象的大小而增加；</li><li>支持 8MB ～ 4TB 级别的堆（在 Java13 版本已经最大支持 16TB）</li></ul><h3 id="CMS-和-G1-回顾"><a href="#CMS-和-G1-回顾" class="headerlink" title="CMS 和 G1 回顾"></a>CMS 和 G1 回顾</h3><p>从设计目标我们可以看出，ZGC 适用于大内存低延迟服务的内存管理和回收。在正式说 ZGC 之前，我们先来说说常用的垃圾收集器遇到的典型问题 – GC 的停顿时间。GC 的停顿时间指垃圾回收期间 STW（Stop The World），当 STW 时，所有应用线程停止活动，等待 GC 停顿结束。</p><p>这里我把 CMS 和 G1 垃圾收集器加入对比，首先，CMS 新生代的 Young GC、G1 和 ZGC 都基于标记-复制算法，但是算法在不同的垃圾收集器下就表现出了巨大的性能差异。这里我以 G1 为例，分析一下 G1 垃圾收集器在混合回收阶段的耗时过程，需要说明的是 G1 的新生代回收和 CMS 的新生代回收均采用的是标记-复制算法，其过程全程 STW 这里不做讨论。G1 的混合回收过程可以分为标记阶段、清理阶段和复制阶段。</p><h4 id="标记阶段停顿分析"><a href="#标记阶段停顿分析" class="headerlink" title="标记阶段停顿分析"></a>标记阶段停顿分析</h4><ul><li>初始标记阶段：这个阶段是从 GC Root 出发标记，该阶段是 STW 的，由于 GC Root 数量不多，通过该阶段耗时非常短。</li><li>并发标记阶段：并发标记阶段是指从 GC Root 开始对堆中对象进行可达性分析，找出存活对象。该阶段通常耗时较长，但由于该阶段不是 STW 的，我们一般不太关心该阶段的耗时。</li><li>再标记阶段：重新标记那些在并发阶段发生变化的对象，该阶段是 STW 的。</li></ul><h4 id="清理阶段停顿分析"><a href="#清理阶段停顿分析" class="headerlink" title="清理阶段停顿分析"></a>清理阶段停顿分析</h4><ul><li>清理阶段清点出有存活对象的分区和没有存活对象的分区，该阶段不会清理垃圾对象，也不会执行存活对象的复制，该阶段是 STW 的。</li></ul><h4 id="复制阶段停顿分析"><a href="#复制阶段停顿分析" class="headerlink" title="复制阶段停顿分析"></a>复制阶段停顿分析</h4><ul><li>复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是 STW 的，其中内存分配通常耗时非常短，但对象成员变量的复制耗时有可能较长，这是因为复制耗时与存活对象数量与对象复杂度成正比，对象越复杂，复制耗时越长。</li></ul><p>通过上面的回顾可以看出，四个 STW 过程中，初始标记因为只有标记 GC Root，耗时较短。再标记因为对象较少，耗时也较短。清理阶段因为内存分区数量少，耗时也较短。转移阶段要处理所有存活的对象，耗时会较长。因此 G1 停顿时间的瓶颈主要是转移阶段 STW，关于转移阶段不能和并发标记阶段一样并发执行呢？<strong>主要是 G1 未能解决转移过程中准确定位对象地址的问题</strong>。</p><h3 id="ZGC-原理"><a href="#ZGC-原理" class="headerlink" title="ZGC 原理"></a>ZGC 原理</h3><p>与 CMS 和 G1 类似，ZGC 也是采用标记-复制算法，不同的是，ZGC 对垃圾回收的过程做了重大改进：ZGC 在标记、转移和重定位阶段几乎都是并发的，这是 ZGC 实现停顿时间小于 10ms 的关键原因。ZGC 垃圾回收的周期如下图所示：</p><p><img src="https://static001.infoq.cn/resource/image/c8/bf/c8yyb76e534124219874ed233707cabf.png"></p><p>前面说到，G1 的转移阶段是完全 STW 的，并且停顿时间随着活跃对象的增加而增加，ZGC 与之不同，整个 ZGC 只有三个阶段是 STW 的，分别是初始标记阶段、再标记阶段、初始转移阶段，再标记阶段 STW 的时间很短，最多 1ms，超过 1ms 则再次进入并发标记阶段。最后再说转移阶段，前面说到，G1 垃圾收集器，未能解决在转移阶段过程中准确定位对象地址的问题，所以它的 GC 耗时会随着存活对象的增加而增加。与 G1 不同的是，<strong>ZGC 是通过着色指针和读屏障技术解决转移过程中准确定位对象的问题，实现了并发转移</strong>，至于着色指针和读屏障技术下文会稍加说明。</p><h3 id="Java21-中的-ZGC-改进"><a href="#Java21-中的-ZGC-改进" class="headerlink" title="Java21 中的 ZGC 改进"></a>Java21 中的 ZGC 改进</h3><p>回顾完了 ZGC 的大致过程和特点，最后再来说本次 Java21 版本中对 ZGC 的改进。首先在 Java21 中默认不开启分代 ZGC，要开启分代 ZGC，需要使用命令行参数：-XX:+ZGenerational</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -XX:+UseZGC -XX:+ZGenerational ...</span><br></pre></td></tr></table></figure><p>需要特别说明的是，按照官方的说法，在后面的版本中，分代 ZGC 将会作为默认的选项开启，届时 -XX:+ZGenerational 将作为不使用分代 ZGC 的开关，直到最后，不使用分代 ZGC 选项将会被移除。</p><p>本次改动，分代 ZGC 将堆分成了两个逻辑代，年轻代用于最近分配的对象，老年代用于长期存在的对象。年轻代和老年代的垃圾回收都是独立进行的，因此 ZGC 可以更专注于收集收益较大的年轻代对象。和不开启分代 ZGC 一样，分代 ZGC 所有的垃圾回收工作都是和应用线程并发的执行的。由于 ZGC 读取和修改对象是和应用线程并发进行的，因此必须确保垃圾收集器和应用线程提供一致的对象视图，ZGC 通过着色指针、加载屏障和存储屏障来做到这一点的。</p><ul><li>着色指针是指一种在堆中的对象指针，它与对象的内存地址联系在一起，包含了编码对象的已知状态的元数据，这个元数据描述了对象是否存活、对象的地址是否正确等信息。ZGC 始终使用 64 位对象指针，因此它可以容纳多达数 TB 堆的元数据位和对象地址。当一个对象中的字段指向另外一个对象时，ZGC 会使用着色指针来实现这种指向。</li><li>加载屏障是指 ZGC 注入在应用程序中的代码片段，只要应用程序读取到了引用自另外对象的一个对象字段，加载屏障就会解释存储在字段中的着色指针中的元数据，并且在应用程序使用引用对象之前采取一些措施。需要说明的是，之前不开启分代的 ZGC 也是通过着色指针和加载屏障来实现上述过程的，只不过分代 ZGC 针对上述过程做了相应优化。</li><li>另外一点，分代 ZGC 还是通过存储屏障来跟踪一代对象到另外一代对象的引用的。存储屏障也是 ZGC 注入到应用程序中的代码片段，只要应用程序将引用存储到对象字段中。分代 ZGC 为着色指针添加了新的元数据位，这样存储屏障就能确定正在写入的字段是否已被记录为可能包含跨代指针。着色指针使分代 ZGC 的存储屏障比传统 ZGC 的存储屏障更加高效。</li></ul><p>增加存储屏障后，分代 ZGC 可以将标记可达对象的工作从加载屏障转移到存储屏障。也就是说，存储屏障可以使用着色指针中的元数据位来有效地确定在存储之前字段所引用的对象是否需要被标记。将标记移出加载屏障后，可以更容易地对其进行优化，这一点是很重要的，因为加载屏障的执行效率往往高于存储屏障。现在当加载屏障解释着色指针时，如果对象被重新定位，它只需要更新对象地址，并更新元数据，以表明已知地址是正确的。后续的加载屏障将解释此元数据，而不再检查对象是否已被重新定位。</p><p>分代 ZGC 在着色指针中使用不同的标记和重定位元数据位集，因此可以独立收集分代数据。</p><h2 id="虚拟线程"><a href="#虚拟线程" class="headerlink" title="虚拟线程"></a>虚拟线程</h2><p>虚拟线程这个词想必大家都不陌生，我们或多或少在其他语言（例如 golang 或者 python）都听说过协程的概念，这里的协程就是我要说的虚拟线程。</p><p>虚拟线程最初是 Java19 中在提案 <a href="https://openjdk.org/jeps/425">JEP425</a> 中以预览功能提出，后面在 Java20 中在提案 <a href="https://openjdk.org/jeps/436">JEP436</a> 中做了改进，仍然是以预览版的形式出现。终于在 Java21 版本，该特性作为正式可用的功能跟我们见面。</p><p>虚拟线程是一种在 JVM 中实现的轻量级的线程，也叫用户线程，它不受操作系统管理和调度，被 JVM 管理。和传统的线程相比，虚拟线程创建和销毁的速度更快，开销更小，可以被大量的创建，因此更加适合轻量的多任务场景。</p><p><img src="https://belief-driven-design.com/images/2023-10-05-java-virtual-threads-scheduler.webp#center"></p><h3 id="虚拟线程的几种创建方式"><a href="#虚拟线程的几种创建方式" class="headerlink" title="虚拟线程的几种创建方式"></a>虚拟线程的几种创建方式</h3><ol><li>静态构造器</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">var</span> <span class="variable">virtual</span> <span class="operator">=</span> Thread.ofVirtual();</span><br><span class="line">virtual.name(<span class="string">&quot;my-virtual-thread&quot;</span>).start(() -&gt; &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Thread.startVirtualThread(() -&gt; &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;hello virtual thread&quot;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ol start="2"><li>Executors</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">var</span> <span class="variable">executor</span> <span class="operator">=</span> Executors.newVirtualThreadPerTaskExecutor();</span><br><span class="line">executor.submit(() -&gt; &#123;</span><br><span class="line">System.out.println(<span class="string">&quot;hello virtual thread&quot;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ol start="3"><li>线程工厂</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ThreadFactory</span> <span class="variable">virtualThreadFactory</span> <span class="operator">=</span> Thread.ofVirtual().name(<span class="string">&quot;my-virtual-thread&quot;</span>).factory();</span><br><span class="line">virtualThreadFactory.newThread(() -&gt; &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;hello virtual thread&quot;</span>);</span><br><span class="line">&#125;).start();</span><br></pre></td></tr></table></figure><h2 id="Switch-表达式"><a href="#Switch-表达式" class="headerlink" title="Switch 表达式"></a>Switch 表达式</h2><p>最后，Java21 新特性里面，我还想说一下 switch 表达式。最近几年，几乎每个 jdk 的新版本都能看到对 switch 表达式的改进，在以往的 switch 语句中，对于 case 中的类型匹配限制还是很多的，switch 表达式在 Java14 开始趋于稳定，在 Java17 中 switch 的模式匹配首次以预览版的新式出现（见 <a href="https://openjdk.org/jeps/406">JEP406</a>），在 Java18、Java19、Java20 多个版本中又进行了更新和功能完善，如今在 Java21 中以正式功能特性发布。</p><p>现在假设我有一个 HashMap&lt;String, Object&gt;，这个 map 的 value 我可能存放有多个不同类型的变量，在以前，我们需要使用 if 语句对类型进行判断，现在借助 switch 语句的模式匹配，便可以简化这种写法，具体例子看下面的代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        Map&lt;String, Object&gt; data = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        data.put(<span class="string">&quot;key&quot;</span>, <span class="string">&quot;cczywyc&quot;</span>);</span><br><span class="line">        <span class="comment">// data.put(&quot;key&quot;, 56);</span></span><br><span class="line">        <span class="comment">// data.put(&quot;key&quot;, 56.12);</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// old code</span></span><br><span class="line">        <span class="keyword">if</span> (data.get(<span class="string">&quot;key&quot;</span>) <span class="keyword">instanceof</span> String s) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;this is string type:&quot;</span> + s);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (data.get(<span class="string">&quot;key&quot;</span>) <span class="keyword">instanceof</span> Integer s) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;this is integer type:&quot;</span> + s);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (data.get(<span class="string">&quot;key&quot;</span>) <span class="keyword">instanceof</span> Double s) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;this is double type:&quot;</span> + s);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// new code</span></span><br><span class="line">        <span class="keyword">switch</span> (data.get(<span class="string">&quot;key&quot;</span>)) &#123;</span><br><span class="line">            <span class="keyword">case</span> String s -&gt; System.out.println(<span class="string">&quot;this is string type:&quot;</span> + s);</span><br><span class="line">            <span class="keyword">case</span> Integer s -&gt; System.out.println(<span class="string">&quot;this is integer type:&quot;</span> + s);</span><br><span class="line">            <span class="keyword">case</span> Double s -&gt; System.out.println(<span class="string">&quot;this is double type:&quot;</span> + s);</span><br><span class="line">            <span class="keyword">default</span> -&gt; System.out.println(<span class="string">&quot;there is no type be found&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>相信看了上面的示例代码，便很清晰的看到新特性的用法，这里对 switch 语句的新特性不作过多介绍。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>毫无疑问，Java21 作为一个 LTS 版本，带来了期待已久的虚拟线程新特性，我预测这个版本在 Java 发展的历史上会是非常重要的一个版本，至于有多少公司愿意升级，还需要打一个大大的问号，至少目前在我看来，新版 JDK 的升级还需要观望，目前国内大部分公司还是清一色 JDK8，暂时可能升不太动，但是我作为一个技术人员，看到新版 JDK 这些新特性，还是异常兴奋的，尤其是虚拟线程，期待后面在 Java 并发编程领域有更多不一样的玩法，让我们拭目以待。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><p>Java21 Releas: <a href="https://openjdk.org/projects/jdk/21/">https://openjdk.org/projects/jdk/21/</a></p><p>JEP439 Generational ZGC: <a href="https://openjdk.org/jeps/439">https://openjdk.org/jeps/439</a></p><p>JEP444 Virtual Thread: <a href="https://openjdk.org/jeps/444">https://openjdk.org/jeps/444</a></p><p>JEP441 Pattern Matching for switch in Java21: <a href="https://openjdk.org/jeps/441">https://openjdk.org/jeps/441</a></p><p>JEP406 Pattern Matching for switch in Java17: <a href="https://openjdk.org/jeps/406">https://openjdk.org/jeps/406</a></p><p>(全文完)</p>]]></content>
    
    
    <summary type="html">Java21 新功能介绍</summary>
    
    
    
    <category term="Java 技术文章合集" scheme="https://cczywyc.com/categories/Java-%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="Java" scheme="https://cczywyc.com/tags/Java/"/>
    
    <category term="JDK 新特性" scheme="https://cczywyc.com/tags/JDK-%E6%96%B0%E7%89%B9%E6%80%A7/"/>
    
  </entry>
  
  <entry>
    <title>换工作了</title>
    <link href="https://cczywyc.com/2023/07/12/%E6%8D%A2%E5%B7%A5%E4%BD%9C%E4%BA%86/"/>
    <id>https://cczywyc.com/2023/07/12/%E6%8D%A2%E5%B7%A5%E4%BD%9C%E4%BA%86/</id>
    <published>2023-07-12T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>经过两个多月断断续续的面试，工作终于是在 6 月份敲定了，不管结果如何，好在终于做出了选择，综合考虑，选择了一家了还算不错的公司，正式开启了职业生涯的第二阶段。其实这篇文章 6 月份就应该写的，之所以拖到现在一方面是刚去新环境需要一个时间过渡，另外一方面则是一直没制定好下一个阶段的计划。</p><p>按照习惯，先总结一下过去两个多月吧，找工作是一个及其痛苦的过程，两个多月的时间里，我要随时保持面试状态，学习也是以八股和面试为主，面试本身也极大的消耗了我的精力。我清楚的记得，过去两个月经常失眠，夜晚思绪乱飞，常常因为理想和现实差距过大而焦虑，要达到理想中的目标还需要付出太多。上周有天晚上跟妹子聊到这个问题，她跟我说到每个有成就的人可能都会经历从基础的物质享受到实现自身价值的阶段，这其中伴随着理想的实现，对普通人来说，美好的事物往往都是从煎熬中获得的。老实说，在现在这样的环境下我不一定完全认同这些观点，但是我觉得为什么不再坚持一下呢？就是这样伴随着焦虑和自我怀疑结束了毕业以来的第一次社招，我成功换了一家公司工作。可能对其他人来说，这不是一个多么好的 offer，但是这是我现阶段最好的选择，我很感激这一切。</p><p>现在已经入职了一个月，新工作已经步入正轨，心态也调整了过来，终于有时间来制定下一个阶段的计划。</p><p>5 月份，技术圈发生了一件不小的事儿，有天突然看到左耳朵耗子（陈皓）去世的消息，老实说，在这之前我跟他并没有在社交媒体上有过互动，只是经常会看到他在推上发文，他是一个很有态度的人，也很敢于输出自己的观点并热衷于跟人讨论，这些都深深吸引了我。我曾经有段时间花了大量的时间阅读了他的 <a href="https://coolshell.cn/">博客文章</a> ，也慢慢养成了一些技术人的好习惯。最近又把《左耳听风》翻出来看了一遍，其中《程序员练级攻略》是重点阅读的，不得不说有时候突破认知比盲目努力更重要，与其乱学一气，倒不如梳理一下知识结构。看了专栏，计划和目标更清晰了，的确有一种相见恨晚的感觉，后悔自己为什么没有早点看到这些，并且专栏每个阶段看都会有不一样的感受，我想这也是一种缅怀大师的方式吧。</p><p>《程序员练级攻略》中提到的系统知识和基础是下半年重点要看的，目前自身的基础还存在很多漏洞，还是希望能够通过系统化的训练来查漏补缺。</p><ol><li>操作系统方面，主要就是 CSAPP 这本书，预计下半年看完第一部分前 6 章的内容，并且完成书上的练习。</li><li>网络方面，今年下半年就不打算看《TPC&#x2F;IP 详解》了，主要把《Unix 网络编程》卷 1 中跟内核网络处理相关的内容看一下，不一定能看完，具体进展如何还是看年底总结吧。</li><li>编程语言方面，由于我目前工作中使用的主要语言是 Java，所以私底下我可能就不会花太多的时间在 Java上了，初步的计划是把极客时间上《手写 mini spring》这个专栏跟完，进一步熟悉 Spring 框架的设计；更多的精力还是会放在 Go 上，把《深入理解分布式系统》书中有关共识算法的内容看一遍，结合《mit 6.824》内容，使用 Go 实现 Raft 算法，《mit 6.824》的几个 lab 应该就没时间做了，毕竟难度很大，这一部分计划放在明年了；最近 Rust 热度也不小，我应该是没时间开展 Rust 的入门学习了，先挖个坑，明年开始入 Rust 的坑，嘿嘿。</li><li>应该还会跟朋友合作做个小工具，具体成果到时候单独写出来吧。</li></ol><p>好吧，前面的总结和下半年的计划就是这样了，虽然还有很多想要学习的东西，但是目前列出的这些已经不少了，对我来说是一个不小的挑战，努力去完成吧，相信自己可以做到。</p><p>路还很长，希望自己可以不负青春、不负这个时代，朝着心中所想加油去冲！</p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">关于换工作的想法和接下来的打算</summary>
    
    
    
    <category term="一些碎碎念" scheme="https://cczywyc.com/categories/%E4%B8%80%E4%BA%9B%E7%A2%8E%E7%A2%8E%E5%BF%B5/"/>
    
    
    <category term="杂记" scheme="https://cczywyc.com/tags/%E6%9D%82%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>详解 SSH</title>
    <link href="https://cczywyc.com/2023/07/09/%E8%AF%A6%E8%A7%A3%20SSH/"/>
    <id>https://cczywyc.com/2023/07/09/%E8%AF%A6%E8%A7%A3%20SSH/</id>
    <published>2023-07-09T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.690Z</updated>
    
    <content type="html"><![CDATA[<h1 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h1><p>SSH 全称 The Secure Shell protocol，它是应用层上的一种加密的网络传输协议，可在不安全的网络中为网络服务提供安全的传输环境，它是通过在网络中创建安全隧道来实现 SSH 客户端与服务器端之间的连接。SSH 协议最常见的用途是远程登录系统，人们通常使用 SSH 来传输命令行界面和远程执行命令，因此可以说它是我们使用最频繁的网络协议之一。</p><p>在设计上，SSH 是 telnet 和非安全 shell 的替代品，Telnet 和 Berkeley rlogin、rsh、rexec 等协议采用明文传输，使用不可靠的密码，容易遭到监听、嗅探和中间人攻击。SSH 旨在保证非安全网络环境（例如互联网）中信息加密完整可靠。</p><h2 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h2><p>SSH 协议先后定义在多个RFC文档中，以下列出了 SSH 协议相关的一些重要的 RFC。</p><table><thead><tr><th align="center">RFC</th><th align="center">介绍</th><th align="center">时间</th></tr></thead><tbody><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4250.txt">RFC 4250</a></td><td align="center">协议号码</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4251.txt">RFC 4251</a></td><td align="center">协议架构</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4252.txt">RFC 4252</a></td><td align="center">身份验证</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4253.txt">RFC 4253</a></td><td align="center">SSH 传输层协议</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4254.txt">RFC 4254</a></td><td align="center">SSH 连接协议</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4255.txt">RFC 4255</a></td><td align="center">使用 DNS 发布 SSH 密钥指纹</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4256.txt">RFC 4256</a></td><td align="center">SSH 的通用消息交换身份验证</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4335.txt">RFC 4335</a></td><td align="center">SSH 会话通道中断扩展</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4344.txt">RFC 4344</a></td><td align="center">SSH 传输层加密模式</td><td align="center">2006 年 01 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4716.txt">RFC 4716</a></td><td align="center">SSH 公钥文件格式</td><td align="center">2006 年 11 月</td></tr><tr><td align="center"><a href="https://www.rfc-editor.org/rfc/rfc4819.txt">RFC 4819</a></td><td align="center">公钥子系统</td><td align="center">2007 年 03 月</td></tr></tbody></table><h2 id="应用"><a href="#应用" class="headerlink" title="应用"></a>应用</h2><p>SSH 协议除了最常见的登录远程系统外，还有多种其他的用途，归纳起来可以分为以下几类：</p><ul><li>远程登录系统（SSH Clien）</li><li>安全的文件传输（SFTP）</li><li>远程设备管理和账号控制</li><li>SSH 隧道</li><li>端口转发</li><li>X11 连接</li><li>安全的身份验证</li></ul><h1 id="历史"><a href="#历史" class="headerlink" title="历史"></a>历史</h1><h2 id="1-x-版本"><a href="#1-x-版本" class="headerlink" title="1.x 版本"></a>1.x 版本</h2><p>芬兰赫尔辛基理工大学的塔图·于勒宁发现自己学校存在嗅探密码的网络攻击，便于 1995 年编写了一套保护信息传输的程序，并称其为 “secure shell”，简称 SSH，设计目标是取代先前的 rlogin、Telnet、FTP和 rsh 等安全性不足的协议。1995 年 7 月，于勒宁以免费软件的形式将其发布。程序很快流行起来，截至 1995 年底，SSH 的用户数已经达到两万，遍布五十个国家。<br>​1995 年 12 月，于勒宁创立了 SSH 通信安全公司来继续开发和销售 SSH。SSH 的早期版本用到了很多自由软件，例如 GNU libgmp，但后来由 SSH 公司发布的版本逐渐变成了专有软件。<br>​截至2000年，已经有两百万用户使用SSH。</p><h2 id="OpenSSH-和-OSSH"><a href="#OpenSSH-和-OSSH" class="headerlink" title="OpenSSH 和 OSSH"></a>OpenSSH 和 OSSH</h2><p>1999 年，开发者们希望使用自由版本的 SSH，于是重新使用较旧的 1.2.12 版本，这也是最后一个采用开放源代码许可的版本。随后瑞典程序员 Björn Grönvall 基于这个版本开发了 OSSH。不久之后，OpenBSD 的开发者又在 Grönvall 版本的基础上进行了大量修改，形成了 OpenSSH，并于 OpenBSD 2.6 一起发行。从该版本开始，OpenSSH 又逐渐移植到了其他操作系统上面。</p><p>截至 2005 年，OpenSSH 是唯一一种最流行的 SSH 实现，而且成为了大量操作系统的默认组件，而 OSSH 已经过时。OpenSSH 仍在维护，而且已经支持 SSH-2 协议。从 7.6 版开始，OpenSSH 不再支持 SSH-1 协议。</p><h2 id="2-x-版本"><a href="#2-x-版本" class="headerlink" title="2.x 版本"></a>2.x 版本</h2><p>2006 年，SSH-2 协议成为了新的标准。与 SSH-1 相比，SSH-2 进行了一系列功能改进并增强了安全性，例如基于迪菲-赫尔曼密钥交换的加密和基于消息认证码的完整性检查。SSH-2 还支持通过单个 SSH 连接任意数量的 shell 会话。SSH-2 协议与 SSH-1 不兼容，由于更加流行，一些实现（例如 lsh 和 Dropbear）只支持 SSH-2 协议。</p><h2 id="1-99-版"><a href="#1-99-版" class="headerlink" title="1.99 版"></a>1.99 版</h2><p>RFC 4253 规定支持 2.0 及以前版本 SSH 的 SSH 服务器应将其原始版本标为 “1.99”。“1.99” 并不是实际的软件版本号，而是为了表示向下兼容。</p><h1 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h1><h2 id="基本架构"><a href="#基本架构" class="headerlink" title="基本架构"></a>基本架构</h2><p>SSH 协议框架中最主要的部分是三个协议：</p><ol><li>传输层协议（The Transport Layer Protocol）：传输层协议提供服务器认证，数据机密性，信息完整性等的支持。</li><li>用户认证协议（The User Authentication Protocol）：用户认证协议为服务器提供客户端的身份鉴别。</li><li>连接协议（The Connection Protocol）：连接协议将加密的信息隧道复用成若干个逻辑通道，提供给更高层的应用协议使用。</li></ol><p>他们的关系如下图：</p><p><img src="https://phoenixnap.com/kb/wp-content/uploads/2021/09/layers-of-ssh-protocol-for-what-is-ssh-pnap.png"></p><p>如果放在整个网络模型中，大概就是以下这个样子：</p><p><img src="https://img.cczywyc.com/SSH/ssh-transport-layer-protocol.png"></p><p>同时还有为许多高层的网络安全应用协议提供扩展的支持。各种高层应用协议可以相对地独立于 SSH 基本体系之外，并依靠这个基本框架，通过连接协议使用 SSH 的安全机制。</p><h2 id="工作过程"><a href="#工作过程" class="headerlink" title="工作过程"></a>工作过程</h2><p>SSH 协议的实现是以 C&#x2F;S 架构的模式工作的，也就是说 SSH 会话连接的建立都是通过 SSH 客户端连接到 SSH 服务端。SSH 客户端主导连接设置的过程，由 SSH 客户端发起 SSH 会话连接，并使用公钥验证 SSH 服务端的身份。在后续阶段 SSH 协议使用对称加密和散列算法来确保客户端和服务端之间数据交换的安全性和完整性。</p><p>下图大致画出了 SSH 协议工作过程：</p><p>​<img src="https://www.ssh.com/hs-fs/hubfs/SSH_Client_Server.png?width=1112&name=SSH_Client_Server.png"></p><p>流程包括以下三个阶段，分别对应着前文 SSH 基础架构中的三个协议过程：</p><p><img src="https://img2020.cnblogs.com/blog/1001313/202012/1001313-20201220124203651-1040613159.png"></p><h2 id="传输层协议"><a href="#传输层协议" class="headerlink" title="传输层协议"></a>传输层协议</h2><h3 id="握手过程"><a href="#握手过程" class="headerlink" title="握手过程"></a>握手过程</h3><p><img src="https://img.cczywyc.com/SSH/ssh-handshake-steps.png"></p><ol><li>TCP Connection：一旦 TCP 握手完成建立连接，客户端和服务端之间交换 ID 字符串。</li><li>Algorithm Negotiation：算法协商，双方支持的密钥交换算法和压缩算法列表，用于在客户端和服务端之间协商出双方共同支持的算法。</li><li>Key Exchange：密钥交换，服务器将向客户端进行身份验证，并生成服务器消息身份验证和密钥。</li><li>End of key Exchange：协商交换完成，这将表明客户端和服务端双方后续将以协商的算法进行加解密和压缩。</li></ol><h2 id="用户认证协议"><a href="#用户认证协议" class="headerlink" title="用户认证协议"></a>用户认证协议</h2><p>用户认证协议发生在传输层协议完成之后，客户端将包括认证方式、用户名等信息的消息通过会话密钥加密发送给服务端，用于验证客户端身份，目前支持一下三种的客户端身份认证：</p><ul><li>密码验证：如果认证方式为密码认证，则客户端会发送密码消息用于验证客户端身份，该消息受传输层协议加密保护。知道帐号和密码，就可以登录到远程主机，并且所有传输的数据都会被 SSH 传输层协议加密。但是，可能会有别的服务器在冒充真正的服务器，但只要客户端校验主机公钥，在服务器私钥不泄露的前提下就能避免被“中间人”攻击。</li><li>密钥验证：如果认证方式为密钥验证，则客户端发送包含由客户端私钥签名的客户端公钥。当服务器收到此消息时，它会检查提供的密钥是否可以接受身份验证，如果可以，它会检查签名是否正确。具体说来，客户端必须为自己创建一对密钥，并把公钥放在需要访问的服务器上。客户端软件会向服务器发出请求，请求用你的私钥进行安全验证并发送使用私钥对会话 ID 等信息的签名。服务器收到请求之后，先在你在该服务器的用户根目录下寻找你的公钥，然后把它和你发送过来的公钥进行比较，并用公钥检验签名是否正确。如果两个密钥一致，且签名正确，服务器就认为用户登录成功。</li><li>主机验证：身份验证是在客户端的主机上执行的，而不是客户端本身。因此，支持多个客户端的主机将为其所有客户端提供身份验证。此方法的工作原理是让客户端发送使用客户端主机的私钥创建的签名。因此，SSH 服务器不是直接验证用户的身份，而是验证客户端主机的身份。</li></ul><blockquote><p>补充：在服务端看来，SSH 也提供安全验证</p><ul><li>服务器将自己的公钥分发给相关的客户端，并将密钥交换过程中的公开信息与协商密钥的哈希值的签名发送给客户端，客户端将获取的服务器公钥计算指纹并与其他安全信道获得的公钥指纹相比对并验证主机签名。</li><li>存在一个密钥认证中心，所有提供服务的主机都将自己的公钥提交给认证中心，公钥认证中心给服务端颁发证书，而任何作为客户端的主机则只要保存一份认证中心的公钥就可以了。在这种模式下，服务器会发送认证中心提供给主机的证书与主机对密钥交换过程中公开信息的签名。客户端只需要验证证书的有效性并验证签名。</li></ul></blockquote><h2 id="连接协议"><a href="#连接协议" class="headerlink" title="连接协议"></a>连接协议</h2><p>连接协议发生在连接建立的最后一个阶段，它允许将加密的信息隧道复用成若干个逻辑通道，提供给更高层的应用协议使用，同时还有为许多高层的网络安全应用协议提供扩展的支持，各种高层应用协议可以相对地独立于 SSH 协议体系之外，并依靠这个基本框架，通过连接协议使用 SSH 的安全机制。例如：</p><ol><li>session：程序的远程执行。该程序可能是一个shell、一个应用程序，如文件传输或电子邮件，一个系统命令或一些内置子系统。一旦会话通道被打开，后续请求将用于启动远程程序。</li><li>X11：这指的是X Window系统，一个计算机软件系统和网络协议，为联网计算机提供图形用户界面（GUI）。X允许应用程序在网络服务器上运行，但显示在桌面机器上。</li><li>远程端口转发和本地端口转发。</li><li>文件传输。</li></ol><h1 id="SSH-协议常见应用场景解析"><a href="#SSH-协议常见应用场景解析" class="headerlink" title="SSH 协议常见应用场景解析"></a>SSH 协议常见应用场景解析</h1><p>上文提到，SSH 协议除了最常见的登录远程系统外，还有多种其他的用途，下面主要讲解 SSH 协议两种其他常见的使用场景。</p><h2 id="SFTP"><a href="#SFTP" class="headerlink" title="SFTP"></a>SFTP</h2><p>说到 SFTP（default port is 21） 就不得不提 FTP，前面说到，使用 SSH 协议的远程登录就是为零代替不安全的 telnet  的，同样的，SFTP 也就是为了代替不安全的 FTP 的。</p><p>FTP 客户端是一个软件应用程序，允许登录运行 FTP 服务器软件的计算机。一旦用户连接到服务器并使用密码进行身份验证，用户就可以从服务器计算机传输文件和文件夹。客户端功能嵌入在大多数常见的网络浏览器中，但也可以作为大多数常见操作系统的单独专用软件应用程序使用。FTP 协议未加密，并透明地传输密码和数据。工具广泛可用于从网络中捕获用户名和密码。这被称为密码嗅探，在 20 世纪 90 年代中期就已经是一种常见的攻击。</p><p>SFTP 协议和 SSH 几乎在所有新应用程序中都取代了 FTP。FTP只能与不支持 SFTP 的遗留系统一起使用。使用 FTP时，通常不可能在受监管的行业中实现合规性。使用 SFTP 替代品，也很容易实现文件传输自动化。SFTP 支持使用加密密钥、SSH 密钥进行身份验证。这使得自动化变得非常容易。SFTP 协议还可以跨防火墙和网络地址转换（NAT）自动工作，无需任何特殊配置。</p><h2 id="SSH-Tunneling"><a href="#SSH-Tunneling" class="headerlink" title="SSH Tunneling"></a>SSH Tunneling</h2><h3 id="什么是-SSH-Tunneling"><a href="#什么是-SSH-Tunneling" class="headerlink" title="什么是 SSH Tunneling"></a>什么是 SSH Tunneling</h3><p>SSH 隧道是一种通过加密的 SSH 连接传输任意网络数据的方法。它可用于将加密添加到以及停止维护的应用程序中。它还可用于实现 VPN（虚拟专用网络）和跨防火墙访问内联网服务。</p><p>SSH 是在不受信任的网络上进行安全远程登录和文件传输的标准。它还提供了一种使用端口转发保护任何给定应用程序的数据流量的方法，基本上通过 SSH 隧道转发任何 TCP&#x2F;IP 端口，这意味着应用程序数据流量被引导到加密的 SSH 连接内流动，因此在传输过程中不会被窃听或拦截。SSH 隧道可以为原生不支持加密的已经停止维护的应用程序添加网络安全。</p><p><img src="https://www.ssh.com/hubfs/Imported_Blog_Media/Securing_applications_with_ssh_tunneling___port_forwarding-2.png"></p><p>上图显示了 SSH 隧道的简化概述。通过不受信任的网络在 SSH 客户端和 SSH 服务器之间建立安全连接。此 SSH 连接是加密的，保护机密性和完整性，并对通信方进行身份验证。</p><p>应用程序使用 SSH 连接连接到应用程序服务器。启用隧道后，应用程序会连接到 SSH 客户端监听的本地主机上的端口。然后，SSH 客户端通过其加密隧道将应用程序转发到服务器。然后，服务器连接到实际的应用程序服务器-通常与SSH 服务器在同一台机器或同一数据中心。因此，应用程序通信是安全的，而无需修改应用程序或最终用户工作流程。</p><h3 id="如何使用-SSH-Tunneling"><a href="#如何使用-SSH-Tunneling" class="headerlink" title="如何使用 SSH Tunneling"></a>如何使用 SSH Tunneling</h3><p>通常来说，SSH Tunneling 缺点是，任何能够登录服务器的用户都可以启用端口转发，这被内部 IT 人员广泛利用，在云中登录他们的家庭机器或服务器，将端口从服务器转发回企业内部网到他们的工作机器或合适的服务器。黑客和恶意软件同样可以用它来将后门留在内部网络中，它还可用于通过多个允许不受控制的隧道的设备反弹攻击来隐藏攻击者的踪迹。</p><p>这里是一个 SSH Tunneling 的 <a href="https://www.ssh.com/academy/ssh/tunneling-example">使用配置</a>，SSH Tunneling 通常与 SSH 密钥和公钥身份验证一起使用，以实现过程的完全自动化。</p><h3 id="SSH-Tunneling-对企业的好处"><a href="#SSH-Tunneling-对企业的好处" class="headerlink" title="SSH Tunneling 对企业的好处"></a>SSH Tunneling 对企业的好处</h3><p>SSH 隧道在许多使用大型机系统作为应用程序后端的公司环境中被广泛使用。在这些环境中，应用程序本身对安全性的原生支持可能非常有限。通过使用隧道，无需修改应用程序即可实现对 SOX、HIPAA、PCI-DSS 和其他标准的合规性。</p><p>在许多情况下，企业里的这些应用程序和应用程序服务器更改的代价非常大，甚至某些程序的源代码可能不可用，供应商可能不再存在，产品可能没有支持，或者开发团队可能不再存在。添加安全包装器，如 SSH 隧道，为此类应用程序增加了安全性提供了一种具有成本效益和实用的方法。例如，为了安全起效，整个全国范围内的 ATM 网络都使用隧道运行。</p><h1 id="SSH-协议报文分析"><a href="#SSH-协议报文分析" class="headerlink" title="SSH 协议报文分析"></a>SSH 协议报文分析</h1><p>最后，我们对 SSH 协议的报文进行具体的分析，在分析具体的报文之前，先来明确两个概念：</p><ul><li>会话密钥 key：key 是通过客户端和服务端之间通过诸如 D-H 算法协商出来的，是在 SSH 会话数据传输过程中对称加密使用的密钥。</li><li>公钥 pub key：pub key 称为服务主机密钥 server_host_key，用于 SSH-TRANS 传输协议进行服务器验证，说简单点就是在身份验证阶段客户端去验证服务器用的。</li></ul><h2 id="报文分析"><a href="#报文分析" class="headerlink" title="报文分析"></a>报文分析</h2><p>下面以下图中的例子来分析 SSH 协议的报文</p><p><img src="https://img.cczywyc.com/SSH/ssh_pcap.pic.jpg"></p><h3 id="TCP-三次握手"><a href="#TCP-三次握手" class="headerlink" title="TCP 三次握手"></a>TCP 三次握手</h3><p>上图中，1 - 3 报文是 TCP 的三次握手，因为比较基础，也不属于本篇文章讨论的范畴，因此对这一部分不做分析。</p><h3 id="SSH-协议版本协商"><a href="#SSH-协议版本协商" class="headerlink" title="SSH 协议版本协商"></a>SSH 协议版本协商</h3><p>上图中 4 和 6 即是 SSH 协议版本协商过程</p><table><thead><tr><th align="center">No</th><th align="center">描述</th><th align="center">解释</th></tr></thead><tbody><tr><td align="center">6</td><td align="center">SSH 协议版本协商</td><td align="center">服务器将自己的 SSH 协议版本发送到客户端，格式为：SSH-protoversion（版本号）-softwareversion（自定义） SP（空格一个，可选） comments（注释，可选） CR（回车） LF（换行）</td></tr><tr><td align="center">4</td><td align="center">SSH 协议版本协商</td><td align="center">客户端将自己的 SSH 协议版本发送到服务器，格式为：SSH-protoversion（版本号）-softwareversion（自定义） SP（空格一个，可选） comments（注释，可选） CR（回车） LF（换行）</td></tr></tbody></table><p>这一步其实很简单，总结下来就是发送了一个格式为 SSH-protoversion-softwareversion SP comments CR LF 的字节流。</p><h3 id="SSH-协议算法协商"><a href="#SSH-协议算法协商" class="headerlink" title="SSH 协议算法协商"></a>SSH 协议算法协商</h3><p>上图中的 8 - 16 就是 SSH 协议算法协商过程，该过程从客户端和服务器相互发出Key Exchange Init请求开始，主要是告诉对方自己支持的相关加密算法列表、MAC算法列表等。最后协商成功后，将会生成对称加密的会话密钥 key 和一个会话 ID。</p><p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/9/27/1661af155463ee56~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp"></p><blockquote><p>需要注意的是，这里生成的是后续用于对称加密的会话密钥 key，不要跟公钥弄混淆了，至于两者的区别，在本小节一开头就已经做了解释，公钥是给客户端验证服务器身份用的。</p></blockquote><p>与此同时，在这一步公钥会从服务器传送到客户端，完成密钥交换的过程。</p><p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/9/27/1661af59cc725838~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp"></p><p>再来说一下会话密钥，这里的会话密钥是通过 D - H 算法计算出来的，不会在网络上传输，其破解的难度取决于离散对数的破解难度，几乎不会被破解。</p><p>下面将具体分析一下 Key Exchange Init 报文包</p><p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/9/27/1661b026636be5f9~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp"></p><table><thead><tr><th align="center">关键字</th><th align="center">解释</th></tr></thead><tbody><tr><td align="center">kex_algorithms</td><td align="center">密钥交换算法，里面即包含使用的 D - H 算法，用于生成会话密钥</td></tr><tr><td align="center">server_host_key_algorithms</td><td align="center">服务器主机密钥算法，可以采用 ssh-rsa，ssh-dss，ecdsa-sha2-nistp256，有公钥和私钥的说法，公钥即我们上面讲到的 pub key。关于私钥的概念，可以参考 <a href="https://blakesmith.me/2010/02/08/understanding-public-key-private-key-concepts.html">understanding public key private key concepts</a></td></tr><tr><td align="center">encryption_algorithms_client_to_server</td><td align="center">对称加密算法，常用的有 aes128-cbc，3des-cbc</td></tr><tr><td align="center">mac_algorithms_client_to_server</td><td align="center">MAC 算法，主要用于保证数据完整性</td></tr><tr><td align="center">compression_algorithms_client_to_server</td><td align="center">压缩算法</td></tr></tbody></table><h3 id="认证阶段"><a href="#认证阶段" class="headerlink" title="认证阶段"></a>认证阶段</h3><p>最后，上图中的 18 - 20 就是认证阶段</p><ol><li><p>基于账号和口令的验证方式</p><p> 客户端将自己的用户名 + 密码用上面生成的会话密钥 key 进行加密之后传送到服务器端进行验证，服务器端验证通过，则响应成功，否则在进行有限次（推荐是20次）重新认证。注意，用户名和密码是采用上面算法协商阶段生成的会话密钥 key 进行加密的，包括后面的连接会话阶段所传送的数据都是，不要认为是采用服务器的 pub key 加密的。</p></li><li><p>基于公钥和私钥的验证方式</p><p> 这种方式也称为免密登录。简单地说，就是客户端自己生成公钥私钥（通常采用 ssh-keygen 程序生成），然后将公钥以某种方式（通常是手动添加）保存到服务器 ~&#x2F;.ssh&#x2F;authorized_keys 文件中，以后服务器都会接受客户端传过来的经过会话密钥加密过的公钥，然后解密得到公钥之后和本地的公钥签名是否一样，如果是，则允许登录。</p></li></ol><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://en.wikipedia.org/wiki/Secure_Shell">https://en.wikipedia.org/wiki/Secure_Shell</a></p><p><a href="https://www.ssh.com/academy/ssh">https://www.ssh.com/academy/ssh</a></p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">SSH 协议详解及其应用</summary>
    
    
    
    <category term="网络协议文章合集" scheme="https://cczywyc.com/categories/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="网络协议" scheme="https://cczywyc.com/tags/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/"/>
    
  </entry>
  
  <entry>
    <title>流量控制（一）：从限流算法说起</title>
    <link href="https://cczywyc.com/2023/04/20/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E4%BB%8E%E9%99%90%E6%B5%81%E7%AE%97%E6%B3%95%E8%AF%B4%E8%B5%B7/"/>
    <id>https://cczywyc.com/2023/04/20/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E4%BB%8E%E9%99%90%E6%B5%81%E7%AE%97%E6%B3%95%E8%AF%B4%E8%B5%B7/</id>
    <published>2023-04-20T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.690Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>关于这篇文章起因，最近计划看一下《Effective Java》，然后我了解到此书的作者同样也是 Google Guava 库的作者，作为一个练习2年的 Java 的程序员，还没有研究过 Guava，属实有点说不过去，借此机会研究了一下 Google Guava。</p><p>先简单说一下 <a href="https://en.wikipedia.org/wiki/Google_Guava">Google Guava</a> 库，它不仅仅是 JDK 的升级库，诸如包含集合（collections）、缓存（caching）、并发库（concurrency libraries）、原生类型支持（primitives support）、字符串处理（string processing）、I&#x2F;O 库等，还是《Effective Java》这本书中那些优秀经验的实践代表，两者应该结合起来阅读学习。在 Guava 中有两个实现我很感兴趣，一个是它实现的 Bloom Filter（布隆过滤器），另外一个就是本篇文章即将介绍的 RateLimiter（限流器）。</p><p>我们平时在开发高并发的分布式系统时，会借助许多手段来满足系统的并发要求，我总结下来大致分为下面几个方面：</p><ul><li>缓存：通过缓存系统可以减少对底层数据存储系统的频繁访问，从而有效地提高系统的访问能力。从前端的浏览器，到网络，在到后端的服务，底层的数据库、文件系统、硬盘和CPU，全都都有设计缓存，这是提高快速访问能力的有效手段。</li><li>负载均衡：通过负载均衡可以将请求分摊到多个服务器上，避免单点故障和资源瓶颈，它是水平扩展的关键技术。</li><li>异步调用：异步调用的一个主要的技术手段就是消息队列，通过消息队列实现异步任务处理、系统解耦、削峰填谷等。</li><li>流量控制：通过限制系统等请求量和并发量，防止系统过载。常见的流量控制的手段包括限流、熔断和降级。</li></ul><h1 id="常见的限流实现方式"><a href="#常见的限流实现方式" class="headerlink" title="常见的限流实现方式"></a>常见的限流实现方式</h1><h2 id="计数器算法"><a href="#计数器算法" class="headerlink" title="计数器算法"></a>计数器算法</h2><p>这是最基本、最粗暴的算法。一般来说，该算法会会维护一个计数器，当处理请求时，就对计数器做加一操作，当你请求处理完时，就对计数器做减一操作，如果计数器达到某个数量（预先设定的阀值），则按照限流的规则处理，防止系统过载。</p><h2 id="采用队列的算法"><a href="#采用队列的算法" class="headerlink" title="采用队列的算法"></a>采用队列的算法</h2><p>这个算法有点像 FIFO，请求有快有慢，所有的请求都放入队列中，消费端以固定的速率从消息队列中取出请求消费，从而控制整个系统的处理流量，以下就是这个算法的示意图：</p><p><img src="https://static001.geekbang.org/resource/image/c8/3d/c8f774f88ab8a4b72378971263c0393d.png?wh=860*175"></p><p>当然，我们可以在上面这个算法的基础上，延伸出其他更为高阶的用法，例如利用优先级队列实现一个具有优先级的流量控制算法。例如针对不同的请求，处理时先处理优先级高的队列，等优先级高的队列处理完了再处理优先级低的队列；再比如，还可以设置不同的权重，根据队列的权重处理来先后处理不同的请求。</p><h2 id="漏桶（Leaky-Bucket）算法"><a href="#漏桶（Leaky-Bucket）算法" class="headerlink" title="漏桶（Leaky Bucket）算法"></a>漏桶（Leaky Bucket）算法</h2><p>关于漏桶，wikipedia上有关于它的<a href="https://en.wikipedia.org/wiki/Leaky_bucket">词条</a>。</p><p><img src="https://media.geeksforgeeks.org/wp-content/uploads/leakyTap-1.png"></p><p>漏桶算法，就像一个漏斗一样，如果把进入漏斗的水比做进入的流量，那么从漏斗中漏出的水就是系统处理的流量。进入漏斗中的流量可快可慢，但是经过漏斗，流量都会以一个固定的速率流出，如果遇到突发流量，超过了漏斗的最大容量，则会丢弃流量，通过这种方式，可以保护系统，防止系统被突发的大流量冲垮。</p><p>关于漏桶算法的实现，一个比较常见的实现方式就是利用 FIFO 队列。把数据包放入 FIFO 队列中，如果流量都以固定大小的数据包组成，则在该进程的时钟每次 tick 时，从队列中移除该固定长度的数据用于消费；如果流量是以可变长度大小的数据包组成，则每次对队列中的数据进行消费时依赖于字节或者比特的数量。</p><p>下面就是一个处理可变长度数据包流量的算法：</p><ol><li>在时钟tick时初始化一个计数器n</li><li>重复下面的操作直到n小于队列头部数据包的数据大小<ol><li>从队列头部弹出一个数据包，计做P</li><li>将数据包P发出</li><li>将计数器n减少数据包P的长度</li></ol></li><li>重置计数器n，重复步骤1</li></ol><p><img src="http://img.cczywyc.com/LeakyBucket_veriable-length_packet.png"></p><p>代码大致逻辑如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * leaky bucket, variable-length packets</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> cczyWyc</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VariableLenPacketDemo</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">storage</span> <span class="operator">=</span> <span class="number">0</span>; <span class="comment">// stored packet in bucket</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">bucketSize</span> <span class="operator">=</span> <span class="number">10</span>; <span class="comment">// the capacity of the bucket</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">inputPktSize</span> <span class="operator">=</span> <span class="number">4</span>; <span class="comment">// inputs packet into the bucket at a time</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">outPktSize</span> <span class="operator">=</span> <span class="number">1</span>; <span class="comment">// outputs packet from the bucket at a time</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">numberQueries</span> <span class="operator">=</span> <span class="number">4</span>; <span class="comment">// total number of times bucket content is checked</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; numberQueries; i++) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">leftSize</span> <span class="operator">=</span> bucketSize - storage;</span><br><span class="line">            <span class="keyword">if</span> (inputPktSize &lt;= leftSize) &#123;</span><br><span class="line">                <span class="comment">// add packet into the bucket</span></span><br><span class="line">                storage += inputPktSize;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;packet loss = &quot;</span> + inputPktSize);</span><br><span class="line">            &#125;</span><br><span class="line">            System.out.println(<span class="string">&quot;buff size = &quot;</span> + storage + <span class="string">&quot; out of bucket size = &quot;</span> + bucketSize);</span><br><span class="line">            storage -= outPktSize;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关于漏桶算法，以下是一个完整的算法demo</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LeakyBucket</span> &#123;</span><br><span class="line">    <span class="comment">/** bucket size */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> bucketSize;</span><br><span class="line">    <span class="comment">/** leakRate */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> leakRate;</span><br><span class="line">    <span class="comment">/** current water in bucket */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> currentPacket;</span><br><span class="line">    <span class="comment">/** bucket, use queue */</span></span><br><span class="line">    <span class="keyword">private</span> Queue&lt;Packet&gt; queue;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">LeakyBucket</span><span class="params">(<span class="type">int</span> bucketSize, <span class="type">int</span> leakRate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.bucketSize = bucketSize;</span><br><span class="line">        <span class="built_in">this</span>.leakRate = leakRate;</span><br><span class="line">        <span class="built_in">this</span>.currentPacket = <span class="number">0</span>;</span><br><span class="line">        <span class="built_in">this</span>.queue = <span class="keyword">new</span> <span class="title class_">ArrayDeque</span>&lt;&gt;();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * add packet into the bucket</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> packetSize packet size</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> true/false</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">addPacket</span><span class="params">(<span class="type">int</span> packetSize)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">this</span>.currentPacket + packetSize &lt;= <span class="built_in">this</span>.bucketSize) &#123;</span><br><span class="line">            <span class="built_in">this</span>.queue.add(<span class="keyword">new</span> <span class="title class_">Packet</span>(packetSize, System.currentTimeMillis()));</span><br><span class="line">            <span class="built_in">this</span>.currentPacket += packetSize;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * leak the packet</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">leak</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (!<span class="built_in">this</span>.queue.isEmpty()) &#123;</span><br><span class="line">            <span class="type">Packet</span> <span class="variable">packet</span> <span class="operator">=</span> <span class="built_in">this</span>.queue.peek();</span><br><span class="line">            <span class="type">long</span> <span class="variable">timeElapsed</span> <span class="operator">=</span> System.currentTimeMillis() - packet.getArrivalTime();</span><br><span class="line">            <span class="type">int</span> <span class="variable">leakPacket</span> <span class="operator">=</span> (<span class="type">int</span>) (timeElapsed * leakRate / <span class="number">1000</span>);</span><br><span class="line">            <span class="keyword">if</span> (leakPacket &gt;= packet.getPktSize()) &#123;</span><br><span class="line">                <span class="built_in">this</span>.queue.poll();</span><br><span class="line">                <span class="built_in">this</span>.currentPacket -= packet.getPktSize();</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Packet</span> &#123;</span><br><span class="line">    <span class="comment">/** packet size */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> pktSize;</span><br><span class="line">    <span class="comment">/** packet arrival time */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> arrivalTime;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Packet</span><span class="params">(<span class="type">int</span> pktSize, <span class="type">long</span> arrivalTime)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.pktSize = pktSize;</span><br><span class="line">        <span class="built_in">this</span>.arrivalTime = arrivalTime;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">getArrivalTime</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> arrivalTime;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getPktSize</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> pktSize;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">bucketSize</span> <span class="operator">=</span> <span class="number">100</span>;</span><br><span class="line">        <span class="type">int</span> <span class="variable">leakRate</span> <span class="operator">=</span> <span class="number">10</span>;</span><br><span class="line">        <span class="type">LeakyBucket</span> <span class="variable">leakyBucket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LeakyBucket</span>(bucketSize, leakRate);</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span>[] packets = &#123;<span class="number">20</span>, <span class="number">30</span>, <span class="number">10</span>, <span class="number">50</span>, <span class="number">40</span>&#125;;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> packetSize : packets) &#123;</span><br><span class="line">            <span class="keyword">if</span> (leakyBucket.addPacket(packetSize)) &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;packet length &quot;</span> + packetSize + <span class="string">&quot; has been add the leaky bucket&quot;</span>);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;leaky bucket full, packet length &quot;</span> + packetSize + <span class="string">&quot; has been dropped&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            leakyBucket.leak();</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="令牌桶（Token-Bucket）算法"><a href="#令牌桶（Token-Bucket）算法" class="headerlink" title="令牌桶（Token Bucket）算法"></a>令牌桶（Token Bucket）算法</h1><p>关于令牌桶算法，可以参考维基百科的词条：<a href="https://en.wikipedia.org/wiki/Leaky_bucket">令牌桶</a>。令牌桶算法是有一个中间代理人，以固定的速率往一个桶（Bucket）内放入令牌，只有拿到令牌的请求才能进行消费。和漏桶算法都以一个固定的速率消费不同，令牌桶算法在流量不是很大时放桶内放入令牌，在流量大时，可以以较快的速率消费（只要桶内的令牌足够）。</p><p><img src="https://static001.geekbang.org/resource/image/99/f0/996b8d60ed90c470ce839f8826e375f0.png?wh=808*481"></p><p>以下是一个令牌桶算法实现：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> TokenBucket <span class="keyword">struct</span> &#123;</span><br><span class="line">rate               <span class="type">int64</span></span><br><span class="line">maxTokens          <span class="type">int64</span></span><br><span class="line">currentTokens      <span class="type">int64</span></span><br><span class="line">lastRefillTmestamp time.Time</span><br><span class="line">mutex              sync.Mutex</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewTokenBucket return a new token bucket</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewTokenBucket</span><span class="params">(Rate <span class="type">int64</span>, MaxTokens <span class="type">int64</span>)</span></span> *TokenBucket &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;TokenBucket&#123;</span><br><span class="line">rate:               Rate,</span><br><span class="line">maxTokens:          MaxTokens,</span><br><span class="line">lastRefillTmestamp: time.Now(),</span><br><span class="line">currentTokens:      MaxTokens,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// refill add token into bucket</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tb *TokenBucket)</span></span> refill() &#123;</span><br><span class="line">now := time.Now()</span><br><span class="line">end := time.Since(tb.lastRefillTmestamp)</span><br><span class="line">tokensTobeAdded := (end.Nanoseconds() * tb.rate) / <span class="number">1000000000</span></span><br><span class="line">tb.currentTokens = <span class="type">int64</span>(math.Min(<span class="type">float64</span>(tb.currentTokens+tokensTobeAdded), <span class="type">float64</span>(tb.maxTokens)))</span><br><span class="line">tb.lastRefillTmestamp = now</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// RequestAllowed if the requests are allowed. true:allowed, false:reject</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tb *TokenBucket)</span></span> RequestAllowed(tokens <span class="type">int64</span>) <span class="type">bool</span> &#123;</span><br><span class="line">tb.mutex.Lock()</span><br><span class="line"><span class="keyword">defer</span> tb.mutex.Unlock()</span><br><span class="line">tb.refill()</span><br><span class="line"><span class="keyword">if</span> tb.currentTokens &gt;= tokens &#123;</span><br><span class="line">tb.currentTokens = tb.currentTokens - tokens</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="应用"><a href="#应用" class="headerlink" title="应用"></a>应用</h1><p>在许多的开源框架和工具中，都实现了 Token Bucket（令牌桶）算法，例如一个比较常见的例子就是 Google Guava实现的 RateLimiter。RateLimiter 是一个限流工具，基于令牌桶算法实现，可以控制某个时间段内控制请求的数量。</p><p>从使用上来看，Google Guava 的限流器使用非常简单，只需要创建一个 RateLimiter 对象，并调用它的 acquire 方法即可。acquire 方法会阻塞调用线程，自到获取令牌为止。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 每秒钟只允许通过10个请求</span></span><br><span class="line"><span class="type">RateLimiter</span> <span class="variable">limiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">10.0</span>);</span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="comment">// 获取令牌</span></span><br><span class="line">    limiter.acquire();</span><br><span class="line">    <span class="comment">// 处理请求</span></span><br><span class="line">    handleRequest();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在下面的例子中，我创建了一个流速为 2 个请求&#x2F;秒的限速器，从直观上来看，这里的流速指的是每秒最多允许 2 个请求通过限流器，在 Guava 中，这里其实是一种匀速的概念，2 个请求&#x2F;秒等价于1一个请求&#x2F;500毫秒，在向线程池提交任务之前，调用 acquire() 方法就能起到限流的作用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//限流器流速：2个请求/秒</span></span><br><span class="line"><span class="type">RateLimiter</span> <span class="variable">limiter</span> <span class="operator">=</span> </span><br><span class="line">  RateLimiter.create(<span class="number">2.0</span>);</span><br><span class="line"><span class="comment">//执行任务的线程池</span></span><br><span class="line"><span class="type">ExecutorService</span> <span class="variable">es</span> <span class="operator">=</span> Executors</span><br><span class="line">  .newFixedThreadPool(<span class="number">1</span>);</span><br><span class="line"><span class="comment">//记录上一次执行时间</span></span><br><span class="line">prev = System.nanoTime();</span><br><span class="line"><span class="comment">//测试执行20次</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i=<span class="number">0</span>; i&lt;<span class="number">20</span>; i++)&#123;</span><br><span class="line">  <span class="comment">//限流器限流</span></span><br><span class="line">  limiter.acquire();</span><br><span class="line">  <span class="comment">//提交任务异步执行</span></span><br><span class="line">  es.execute(()-&gt;&#123;</span><br><span class="line">    <span class="type">long</span> cur=System.nanoTime();</span><br><span class="line">    <span class="comment">//打印时间间隔：毫秒</span></span><br><span class="line">    System.out.println(</span><br><span class="line">      (cur-prev)/<span class="number">1000_000</span>);</span><br><span class="line">    prev = cur;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">输出结果：</span><br><span class="line">...</span><br><span class="line"><span class="number">500</span></span><br><span class="line"><span class="number">499</span></span><br><span class="line"><span class="number">499</span></span><br><span class="line"><span class="number">500</span></span><br><span class="line"><span class="number">499</span></span><br></pre></td></tr></table></figure><h2 id="Google-Guava-RateLimiter实现原理分析"><a href="#Google-Guava-RateLimiter实现原理分析" class="headerlink" title="Google Guava RateLimiter实现原理分析"></a>Google Guava RateLimiter实现原理分析</h2><p>Guava RateLimiter 使用的是令牌桶算法，在分析原理之前，我们再来描述一下令牌桶算法：</p><ol><li>令牌以固定的速率添加到令牌桶中，假设限流的速率是 r&#x2F; 秒，则令牌每 1&#x2F;r 秒会添加一个；</li><li>假设令牌桶的容量是 b，如果令牌桶已满，则新的令牌会丢弃；</li><li>请求能够通过限流器的前提是令牌桶中有令牌。</li></ol><p>上面我们解释了流速 r，它其实是一种匀速的概念，那么令牌桶的容量 b 该怎么理解呢？b 其实是 burst 的缩写，意义是限流器允许的最大突发流量。比如 b&#x3D;10，并且桶中的令牌已满，此时限流器允许 10 个请求同时通过限流器，当然只是突发流量而已，这 10 个请求会带走 10 个令牌，所以后续的流量只能按照速率 r 通过限流器。</p><p>以下涉及到源码分析，本文使用的 Guava 版本是截止目前最新的31.1-jre版本。</p><p>前面讲到，RateLimiter的使用非常简单，它有两个静态方法用来实例化，实例化以后，我们只需要关心 acquire 就行了，甚至都没有 release 操作。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 实例化的两种方式：</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> RateLimiter <span class="title function_">create</span><span class="params">(<span class="type">double</span> permitsPerSecond)</span>&#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> RateLimiter <span class="title function_">create</span><span class="params">(<span class="type">double</span> permitsPerSecond, <span class="type">long</span> warmupPeriod, TimeUnit unit)</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">double</span> <span class="title function_">acquire</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="type">double</span> <span class="title function_">acquire</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>)</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>)</span> &#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">long</span> timeout, TimeUnit unit)</span> &#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>, <span class="type">long</span> timeout, TimeUnit unit)</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">double</span> <span class="title function_">getRate</span><span class="params">()</span> &#123;&#125;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">setRate</span><span class="params">(<span class="type">double</span> permitsPerSecond)</span> &#123;&#125;</span><br></pre></td></tr></table></figure><blockquote><p>我们知道，Java 并发包中提供了 Semaphore，它也能够对资源访问进行控制，不同的是，Semaphore 控制的是并发的数量，而 RateLimiter 是用来控制资源访问的速率（rate）的，它强调的是控制速率。</p></blockquote><p>可以看到，create 方法指定一个 permitsPerSecond 参数，代表每秒钟产生多少个 permits，这就是速率。RateLimiter允许预占未来的令牌，比如，每秒钟产生5个 permits，我们可以单次请求 100 个 permits，这样，紧接着的下一个请求需要等待大概 20 秒才能获取到 permits。</p><h3 id="SmoothRateLimiter介绍"><a href="#SmoothRateLimiter介绍" class="headerlink" title="SmoothRateLimiter介绍"></a>SmoothRateLimiter介绍</h3><p>通过源码可以看到，RateLimiter只有一个子类，就是抽象类 SmoothRateLimiter，SmoothRateLimiter 有两个实现类，其实就是对应了限流器的两种模式，这里先介绍一下中间的抽象类 SmoothRateLimiter，然后后面分别介绍它的两个实现类（对应的两种模式）。</p><p><img src="http://img.cczywyc.com/RateLimiter%E7%B1%BB%E5%9B%BE.png"></p><p>RateLimiter 作为抽象类，它只有两个属性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> SleepingStopwatch stopwatch;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> Object mutexDoNotUseDirectly;</span><br></pre></td></tr></table></figure><p>其中，stopwatch非常重要，它是一个底层计时器，用它来“计时”，RateLimiter 把实例化的时间设置为 0 值，后续都是取相对时间，用微秒表示。</p><p>mutexDoNotUseDirectly 用来做锁，RateLimiter 依赖于 synchronized 来控制并发，所以我们其实可以看到，它的各个属性都没没有用 volatile 修饰。</p><p>具体看抽象类 SmoothRateLimiter 源码，可以看到它有如下属性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 当前还有多少 permits 没有被使用，被存下来的 permits 数量</span></span><br><span class="line"><span class="type">double</span> storedPermits;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 最大允许缓存的 permits 数量，也就是 storedPermits 能达到的最大值</span></span><br><span class="line"><span class="type">double</span> maxPermits;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 每隔多少时间产生一个 permit，</span></span><br><span class="line"><span class="comment">// 比如我们构造方法中设置每秒 5 个，也就是每隔 200ms 一个，这里单位是微秒，也就是 200,000</span></span><br><span class="line"><span class="type">double</span> stableIntervalMicros;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 下一次可以获取 permits 的时间，这个时间是相对 RateLimiter 的构造时间的，是一个相对时间，可以理解为时间戳</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">long</span> <span class="variable">nextFreeTicketMicros</span> <span class="operator">=</span> <span class="number">0L</span>; </span><br></pre></td></tr></table></figure><p>nextFreeTicketMicros 是一个很重要的属性。我们每一次获取 permits 的时候，先拿 storedPermits 的值，因为它是当前存下来的 permits，如果当前存的 permits 够，storedPermits 减去请求消耗的 permits 就可以了，如果不够，那么就需要将这个 nextFreeTicketMicros 的时间往前推，表示预占了接下来多少时间的 permits。那么如果在下一个请求到来的时候，如果还没有到 nextFreeTicketMicros 这个时间点，需要 sleep 到这个时间点再返回，当然也需要再将这个时间往前推。</p><blockquote><p>这里可能有个疑问：因为时间是一直在往前走的，应该要一直往池中添加 permits，所以 storedPermits 的值需要不断的往上添加，难道需要另外开启一个线程来单独完成这个事情吗？其实不是的，只需要在关键的操作中同步一下，然后重新计算 permits 就好了。</p></blockquote><h3 id="SmoothBursty-分析"><a href="#SmoothBursty-分析" class="headerlink" title="SmoothBursty 分析"></a>SmoothBursty 分析</h3><p>查看源码，RateLimiter 有一个公有的静态 create 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> RateLimiter <span class="title function_">create</span><span class="params">(<span class="type">double</span> permitsPerSecond)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>参与 permitsPerSecond 表示每秒钟可以产生多少个 permits，其中调用了如下方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> RateLimiter <span class="title function_">create</span><span class="params">(<span class="type">double</span> permitsPerSecond, SleepingStopwatch stopwatch)</span> &#123;</span><br><span class="line">    <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SmoothBursty</span>(stopwatch, <span class="number">1.0</span> <span class="comment">/* maxBurstSeconds */</span>);</span><br><span class="line">    rateLimiter.setRate(permitsPerSecond);</span><br><span class="line">    <span class="keyword">return</span> rateLimiter;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里实例化的是 SmoothBursty 的实例，顺着代码点进去，发现它只有一个属性 maxBurstSeconds，在实例化的时候，指定了 maxBurstSeconds 为 1.0，也就是说，最多会缓存 1 秒钟，也就是 （1.0 * permitsPerSecond）这么多个 permits 到池中。</p><blockquote><p>这个 1.0 秒，还关系到 storedPermits 和 maxPermits：</p><p>0 &lt;&#x3D; storedPermits &lt;&#x3D; maxPermits &#x3D; permitsPerSecond</p></blockquote><p>继续往下看 setRate 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">setRate</span><span class="params">(<span class="type">double</span> permitsPerSecond)</span> &#123;</span><br><span class="line">  checkArgument(</span><br><span class="line">      permitsPerSecond &gt; <span class="number">0.0</span> &amp;&amp; !Double.isNaN(permitsPerSecond), <span class="string">&quot;rate must be positive&quot;</span>);</span><br><span class="line">  <span class="keyword">synchronized</span> (mutex()) &#123;</span><br><span class="line">    doSetRate(permitsPerSecond, stopwatch.readMicros());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>setRate 是一个 public 方法，可以用它来调整速率。继续看初始化的过程，这里使用了 synchronized 来控制并发。往下跟踪可以看到子类抽象类 SmoothRateLimiter 中重写了这个方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">doSetRate</span><span class="params">(<span class="type">double</span> permitsPerSecond, <span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">    <span class="comment">// 同步</span></span><br><span class="line">    resync(nowMicros);</span><br><span class="line">    <span class="comment">// 计算属性 stableIntervalMicros</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">stableIntervalMicros</span> <span class="operator">=</span> SECONDS.toMicros(<span class="number">1L</span>) / permitsPerSecond;</span><br><span class="line">    <span class="built_in">this</span>.stableIntervalMicros = stableIntervalMicros;</span><br><span class="line">    doSetRate(permitsPerSecond, stableIntervalMicros);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Resend 方法用来调整 storesPermits 和 nextFreeTicketMicros。这就是我在上面说的，不需要单独开启一个线程增加 storedPermits 的值，在关键的步骤节点，需要先更新一下 storedPermits 到正确的值。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">resync</span><span class="params">(<span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">  <span class="comment">// 如果 nextFreeTicket 已经过掉了，想象一下很长时间都没有再次调用 limiter.acquire() 的场景</span></span><br><span class="line">  <span class="comment">// 需要将 nextFreeTicket 设置为当前时间，重新计算 storedPermits</span></span><br><span class="line">  <span class="keyword">if</span> (nowMicros &gt; nextFreeTicketMicros) &#123;</span><br><span class="line">    <span class="type">double</span> <span class="variable">newPermits</span> <span class="operator">=</span> (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();</span><br><span class="line">    storedPermits = min(maxPermits, storedPermits + newPermits);</span><br><span class="line">    nextFreeTicketMicros = nowMicros;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>上述的源码中，coolDownIntervalMicros() 这个方法可以先不用关注，可以看到，在子类 SmoothBursty 类中的实现是直接返回了 stableIntervalMicros 的值，也就是我们说的，每产生一个 Permits 的时间长度。</p><p>看到这里不难发现，在 resync 的时候，此时的 stableIntervalMicros 其实并没有设置，是在下面的 doSetRate 的实现中设置的，也就是说这里发生了一次除 0 的操作，得到的结果其实是无穷大。而此时 maxPermits 还是 0，不过这里没有多大关系。</p></blockquote><p>再回到前面的 doSetRate 方法，可以看到 resync 以后，会走到下面的 doSetRate逻辑，这里子类 SmoothBursty 的实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">doSetRate</span><span class="params">(<span class="type">double</span> permitsPerSecond, <span class="type">double</span> stableIntervalMicros)</span> &#123;</span><br><span class="line">  <span class="type">double</span> <span class="variable">oldMaxPermits</span> <span class="operator">=</span> <span class="built_in">this</span>.maxPermits;</span><br><span class="line">  <span class="comment">// 这里计算了，maxPermits 为 1 秒产生的 permits</span></span><br><span class="line">  maxPermits = maxBurstSeconds * permitsPerSecond;</span><br><span class="line">  <span class="keyword">if</span> (oldMaxPermits == Double.POSITIVE_INFINITY) &#123;</span><br><span class="line">    <span class="comment">// if we don&#x27;t special-case this, we would get storedPermits == NaN, below</span></span><br><span class="line">    storedPermits = maxPermits;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 因为 storedPermits 的值域变化了，需要等比例缩放</span></span><br><span class="line">    storedPermits =</span><br><span class="line">        (oldMaxPermits == <span class="number">0.0</span>)</span><br><span class="line">            ? <span class="number">0.0</span> <span class="comment">// initial state</span></span><br><span class="line">            : storedPermits * maxPermits / oldMaxPermits;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面就是实例化 SmoothBursty 的一个过程，接下来再来分析 acquire 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@CanIgnoreReturnValue</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">double</span> <span class="title function_">acquire</span><span class="params">()</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> acquire(<span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@CanIgnoreReturnValue</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">double</span> <span class="title function_">acquire</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>)</span> &#123;</span><br><span class="line">  <span class="comment">// 预约，如果当前不能直接获取到 permits，需要等待</span></span><br><span class="line">  <span class="comment">// 返回值代表需要 sleep 多久</span></span><br><span class="line">  <span class="type">long</span> <span class="variable">microsToWait</span> <span class="operator">=</span> reserve(<span class="keyword">permits</span>);</span><br><span class="line">  <span class="comment">// sleep</span></span><br><span class="line">  stopwatch.sleepMicrosUninterruptibly(microsToWait);</span><br><span class="line">  <span class="comment">// 返回 sleep 的时长</span></span><br><span class="line">  <span class="keyword">return</span> <span class="number">1.0</span> * microsToWait / SECONDS.toMicros(<span class="number">1L</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>继续看 reserve 方法：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">final long reserve(int permits) &#123;</span><br><span class="line">  checkPermits(permits);</span><br><span class="line">  synchronized (mutex()) &#123;</span><br><span class="line">    return reserveAndGetWaitLength(permits, stopwatch.readMicros());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">final long reserveAndGetWaitLength(int permits, long nowMicros) &#123;</span><br><span class="line">  // 返回 nextFreeTicketMicros</span><br><span class="line">  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);</span><br><span class="line">  // 计算时长</span><br><span class="line">  return max(momentAvailable - nowMicros, 0);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>顺着代码继续往里看：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">long</span> <span class="title function_">reserveEarliestAvailable</span><span class="params">(<span class="type">int</span> requiredPermits, <span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">  <span class="comment">// 这里做一次同步，更新 storedPermits 和 nextFreeTicketMicros (如果需要)</span></span><br><span class="line">  resync(nowMicros);</span><br><span class="line">  <span class="comment">// 返回值就是 nextFreeTicketMicros，注意刚刚已经做了 resync 了，此时它是最新的正确的值</span></span><br><span class="line">  <span class="type">long</span> <span class="variable">returnValue</span> <span class="operator">=</span> nextFreeTicketMicros;</span><br><span class="line">  <span class="comment">// storedPermits 中可以使用多少个 permits</span></span><br><span class="line">  <span class="type">double</span> <span class="variable">storedPermitsToSpend</span> <span class="operator">=</span> min(requiredPermits, <span class="built_in">this</span>.storedPermits);</span><br><span class="line">  <span class="comment">// storedPermits 中不够的部分</span></span><br><span class="line">  <span class="type">double</span> <span class="variable">freshPermits</span> <span class="operator">=</span> requiredPermits - storedPermitsToSpend;</span><br><span class="line">  <span class="comment">// 为了这个不够的部分，需要等待多久时间</span></span><br><span class="line">  <span class="type">long</span> <span class="variable">waitMicros</span> <span class="operator">=</span></span><br><span class="line">      storedPermitsToWaitTime(<span class="built_in">this</span>.storedPermits, storedPermitsToSpend) <span class="comment">// 这部分固定返回 0</span></span><br><span class="line">          + (<span class="type">long</span>) (freshPermits * stableIntervalMicros);</span><br><span class="line">  <span class="comment">// 将 nextFreeTicketMicros 往前推</span></span><br><span class="line">  <span class="built_in">this</span>.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);</span><br><span class="line">  <span class="comment">// storedPermits 减去被拿走的部分</span></span><br><span class="line">  <span class="built_in">this</span>.storedPermits -= storedPermitsToSpend;</span><br><span class="line">  <span class="keyword">return</span> returnValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到获取 permits 的时候，其实是获取了两部分，一部分来自于存量 storedPermits，存量不够的话，另一部分来自于预占未来的 freshPermits。这里有一个关键点，可以看到上面方法的返回值是 nextFreeTicketMicros 的旧值，因为只要到这个时间节点，就说明在当次 acquire 可以成功返回了，而不管 sroredPermits 够不够，如果不够，会将 nextFreeTicketMicros 往前推一定的时间，预占了一定的量。</p><h3 id="SmoothWarmingUp-分析"><a href="#SmoothWarmingUp-分析" class="headerlink" title="SmoothWarmingUp 分析"></a>SmoothWarmingUp 分析</h3><p>下面再来看 Guava 限流器的另外一种模式。SmoothWarmingUp 适用于资源需要预热的场景，比如某个接口的业务需要使用到数据库连接，由于连接需要预热才能进入到最佳状态，如果我们的系统长时间处于低负载或零负载状态，连接池中的连接慢慢释放掉了，此时我们认为连接池是冷的。假设业务在稳定状态下，正常可以提供最大 1000 QPS 的访问，但是如果连接池是冷的，我们就不能让系统达到 1000 的QPS，要限制住突发流量，因为这会拖垮后端数据库，从而拖垮整个系统，应该有一个预热的过程。这里我先贴一个 Guava 源码中 SmoothWarmingUp 的注释：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">   * This implements the following function where coldInterval = coldFactor * stableInterval.</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * &lt;pre&gt;</span></span><br><span class="line"><span class="comment">   *          ^ throttling</span></span><br><span class="line"><span class="comment">   *          |</span></span><br><span class="line"><span class="comment">   *    cold  +                  /</span></span><br><span class="line"><span class="comment">   * interval |                 /.</span></span><br><span class="line"><span class="comment">   *          |                / .</span></span><br><span class="line"><span class="comment">   *          |               /  .   ← &quot;warmup period&quot; is the area of the trapezoid between</span></span><br><span class="line"><span class="comment">   *          |              /   .     thresholdPermits and maxPermits</span></span><br><span class="line"><span class="comment">   *          |             /    .</span></span><br><span class="line"><span class="comment">   *          |            /     .</span></span><br><span class="line"><span class="comment">   *          |           /      .</span></span><br><span class="line"><span class="comment">   *   stable +----------/  WARM .</span></span><br><span class="line"><span class="comment">   * interval |          .   UP  .</span></span><br><span class="line"><span class="comment">   *          |          . PERIOD.</span></span><br><span class="line"><span class="comment">   *          |          .       .</span></span><br><span class="line"><span class="comment">   *        0 +----------+-------+--------------→ storedPermits</span></span><br><span class="line"><span class="comment">   *          0 thresholdPermits maxPermits</span></span><br><span class="line"><span class="comment">   * &lt;/pre&gt;</span></span><br><span class="line"><span class="comment">   */</span></span><br></pre></td></tr></table></figure><p>可以看到 X 轴代表 storedPermits 的数量，Y 轴代表获取一个 permits需要的时间。如果系统处于低负载的状态，storedPermits 会一直增加，当请求来的时候，我们要从 storedPermits 中取 permits，在这种模式下，如果 storedPermits越大，代表系统越冷，则获取 permits 需要的时间就越多。这里回顾一下上面介绍的 SmoothBursty 模式，它从 storedPermtis 中获取 permits 是不需要等待时间的，而 SmoothWarmingUp 恰恰相反。这里大致总结一下 SmoothBursty 和 SmoothWarmingUp 的区别：</p><ol><li><strong>SmoothBursty 初始化的时候令牌池中的令牌数量为 0，而 SmoothWarmingUp 初始化的时候令牌池数量为 maxPermits。</strong></li><li><strong>SmoothBursty 从令牌池中获取令牌不需要等待，而 SmoothWarmingUp 从令牌池中获取令牌需要等待一段时间，该时间长短和令牌池中的令牌数量有关系</strong></li></ol><p>这里我在网上找到了一个更详细的图对此预热过程进行说明：</p><p><img src="https://pic4.zhimg.com/80/v2-571a4dc8cf72049f183781d4b924b537_1440w.webp"></p><p>上图中 slope 表示绿色实线的斜率，其计算方式如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slope = (stableIntervalMicros * coldFactor - stableIntervalMicros) / (maxPermits - thresholdPermits)</span><br></pre></td></tr></table></figure><p>由于横坐标表示令牌桶中令牌使用，纵坐标表示从令牌桶中获取一个令牌需要的时间，则红色实线对应的矩形面积、绿色实线对应的梯形面积都代表时间，因此预热时间 warmupPeriodMicros的定义如下（梯形面积）：从满状态的令牌桶中取出（maxPermits - thresholdPermits）个令牌所需花费的时间。预热时间是我们在构造的时候指定的，完成预热后，我们能进入到一个稳定的速率中（stableInterval）。有一个关键的点是从 thresholdPermits 到 0 的时间，是从 maxPermits 到 thresholdPermits 时间到一半，也就是梯形面积是长方形面积的 2 倍，至于具体原因，可以自行论证。下面就是根据构造参数计算出 thresholdPermits 和 maxPermits 的值。</p><p>梯形面积为 warmupPeriod，而长方形面积为 stableInterval * thresholdPermits，即：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">warmupPeriod = <span class="number">2</span> * stableInterval * thresholdPermits</span><br></pre></td></tr></table></figure><p>由此可以得出 thresholdPermits 的值：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">thresholdPermits = <span class="number">0.5</span> * warmupPeriod / stableInterval</span><br></pre></td></tr></table></figure><p>然后根据梯形面积公式：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">warmupPeriod = <span class="number">0.5</span> * (stableInterval + coldInterval) * (maxPermits - thresholdPermits)</span><br></pre></td></tr></table></figure><p>可以得出 maxPermits 为：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maxPermits = thresholdPermits + <span class="number">2.0</span> * warmupPeriod / (stableInterval + coldInterval)</span><br></pre></td></tr></table></figure><p>这样我们就算出了 thresholdPermits 和 maxPermits 的值。</p><p>下面再来看一下冷却时间间隔，它指的是 sroredPermits 中每个 permits 的增长速度，为了达到从 0 到 maxPermits 花费 warmupPeriodMicros 的时间，在源码中将其定义为：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="type">double</span> <span class="title function_">coolDownIntervalMicros</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> warmupPeriodMicros / maxPermits;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它在 resync 中有使用：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">resync</span><span class="params">(<span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (nowMicros &gt; nextFreeTicketMicros) &#123;</span><br><span class="line">    <span class="comment">// coolDownIntervalMicros 在这里使用</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">newPermits</span> <span class="operator">=</span> (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();</span><br><span class="line">    storedPermits = min(maxPermits, storedPermits + newPermits);</span><br><span class="line">    nextFreeTicketMicros = nowMicros;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接着来看它的 doSetRate 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">doSetRate</span><span class="params">(<span class="type">double</span> permitsPerSecond, <span class="type">double</span> stableIntervalMicros)</span> &#123;</span><br><span class="line">    <span class="type">double</span> <span class="variable">oldMaxPermits</span> <span class="operator">=</span> maxPermits;</span><br><span class="line">    <span class="comment">// coldFactor 是固定的 3</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">coldIntervalMicros</span> <span class="operator">=</span> stableIntervalMicros * coldFactor;</span><br><span class="line">    <span class="comment">// 这个公式上面已经解释了</span></span><br><span class="line">    thresholdPermits = <span class="number">0.5</span> * warmupPeriodMicros / stableIntervalMicros;</span><br><span class="line">    <span class="comment">// 这个公式上面也已经解释了</span></span><br><span class="line">    maxPermits =</span><br><span class="line">        thresholdPermits + <span class="number">2.0</span> * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);</span><br><span class="line">    <span class="comment">// 计算那条斜线的斜率。上面解释了</span></span><br><span class="line">    slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);</span><br><span class="line">    <span class="keyword">if</span> (oldMaxPermits == Double.POSITIVE_INFINITY) &#123;</span><br><span class="line">        <span class="comment">// if we don&#x27;t special-case this, we would get storedPermits == NaN, below</span></span><br><span class="line">        storedPermits = <span class="number">0.0</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        storedPermits =</span><br><span class="line">            (oldMaxPermits == <span class="number">0.0</span>)</span><br><span class="line">                ? maxPermits <span class="comment">// initial state is cold</span></span><br><span class="line">                : storedPermits * maxPermits / oldMaxPermits;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后再来回顾一下下面的代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line">  <span class="keyword">final</span> <span class="type">long</span> <span class="title function_">reserveEarliestAvailable</span><span class="params">(<span class="type">int</span> requiredPermits, <span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">    resync(nowMicros);</span><br><span class="line">    <span class="type">long</span> <span class="variable">returnValue</span> <span class="operator">=</span> nextFreeTicketMicros;</span><br><span class="line">    <span class="type">double</span> <span class="variable">storedPermitsToSpend</span> <span class="operator">=</span> min(requiredPermits, <span class="built_in">this</span>.storedPermits);</span><br><span class="line">    <span class="type">double</span> <span class="variable">freshPermits</span> <span class="operator">=</span> requiredPermits - storedPermitsToSpend;</span><br><span class="line">    <span class="type">long</span> <span class="variable">waitMicros</span> <span class="operator">=</span></span><br><span class="line">        storedPermitsToWaitTime(<span class="built_in">this</span>.storedPermits, storedPermitsToSpend)</span><br><span class="line">            + (<span class="type">long</span>) (freshPermits * stableIntervalMicros);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);</span><br><span class="line">    <span class="built_in">this</span>.storedPermits -= storedPermitsToSpend;</span><br><span class="line">    <span class="keyword">return</span> returnValue;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>这段代码上面解释 acquire 的时候已经解释过了，它是 acquire 的核心，waitMicros 由两部分组成，一部分是从 storedPermits 中获取花费的时间，一部分是等待 freshPermits 产生花费的时间。在 SmoothBursty 的实现中，从 storedPermits 中获取 permits 直接返回 0，不需要等待。而在 SmoothWarmingUp 的实现中，由于需要预热，所以从 storedPermits 中取 permits 需要花费一定的时间，其实就是要计算下图中，阴影部分的面积。</p><p><img src="http://img.cczywyc.com/SmoothWarmingUp.png"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="type">long</span> <span class="title function_">storedPermitsToWaitTime</span><span class="params">(<span class="type">double</span> storedPermits, <span class="type">double</span> permitsToTake)</span> &#123;</span><br><span class="line">  <span class="type">double</span> <span class="variable">availablePermitsAboveThreshold</span> <span class="operator">=</span> storedPermits - thresholdPermits;</span><br><span class="line">  <span class="type">long</span> <span class="variable">micros</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">  <span class="comment">// 如果右边梯形部分有 permits，那么先从右边部分获取permits，计算梯形部分的阴影部分的面积</span></span><br><span class="line">  <span class="keyword">if</span> (availablePermitsAboveThreshold &gt; <span class="number">0.0</span>) &#123;</span><br><span class="line">    <span class="comment">// 从右边部分获取的 permits 数量</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">permitsAboveThresholdToTake</span> <span class="operator">=</span> min(availablePermitsAboveThreshold, permitsToTake);</span><br><span class="line">    <span class="comment">// 梯形面积公式：(上底+下底)*高/2</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">length</span> <span class="operator">=</span></span><br><span class="line">        permitsToTime(availablePermitsAboveThreshold)</span><br><span class="line">            + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);</span><br><span class="line">    micros = (<span class="type">long</span>) (permitsAboveThresholdToTake * length / <span class="number">2.0</span>);</span><br><span class="line">    permitsToTake -= permitsAboveThresholdToTake;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 加上 长方形部分的阴影面积</span></span><br><span class="line">  micros += (<span class="type">long</span>) (stableIntervalMicros * permitsToTake);</span><br><span class="line">  <span class="keyword">return</span> micros;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对于给定的 x 值，计算 y 值</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">double</span> <span class="title function_">permitsToTime</span><span class="params">(<span class="type">double</span> <span class="keyword">permits</span>)</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> stableIntervalMicros + <span class="keyword">permits</span> * slope;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>到这里，SmoothWarmingUp 基本上说完了。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>按照我写文章的惯例，对这篇文章做一个小结。本文首先从限流算法说起，介绍了两种常见的限流算法，分别是 Leaky Bucket Algorithm（漏桶算法） 和 Token Bucket Algorithm（令牌桶算法）；接着重点以 Token Bucket Algorithm 为例，讲了 Token Bucket Algorithm 的具体实现，即 Google Guava RateLimiter。Guava RateLimiter 有两种模式，应对突发流量的 SmoothBursty 和 可以预热的 SmoothWarmingUp。SmoothBursty 获取 permits 不需要等待，可以现在令牌桶中存入 permits，需要注意的是，这里 permits 的增长是一种匀速的状态，当有突发流量时，一次性可以从令牌桶获取多个 permits；而 SmoothWarmingUp 获取 permits需要一个预热的状态，这样设计的作用是，如果系统可以承受最大的 QPS 是1000，如果系统是冷的，让系统立即达到 1000 QPS 会拖垮系统，因此这个预热的过程可以有效的保护系统，具体表现为从令牌桶中获取 permits 等待的时间会随着令牌被消耗逐渐缩短，直至一个稳定的时间。</p><blockquote><p>注：本篇是流量控制的第一篇，后面可能会单独写一篇文章聊一下 TCP 中滑动窗口实现的流量控制。</p></blockquote><p>（全文完）</p>]]></content>
    
    
    <summary type="html">常见的限流算法详解及其应用</summary>
    
    
    
    <category term="Java 技术文章合集" scheme="https://cczywyc.com/categories/Java-%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="Java" scheme="https://cczywyc.com/tags/Java/"/>
    
    <category term="流量控制" scheme="https://cczywyc.com/tags/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>技术人员如何提高</title>
    <link href="https://cczywyc.com/2023/04/07/%E6%8A%80%E6%9C%AF%E4%BA%BA%E5%91%98%E5%A6%82%E4%BD%95%E6%8F%90%E9%AB%98/"/>
    <id>https://cczywyc.com/2023/04/07/%E6%8A%80%E6%9C%AF%E4%BA%BA%E5%91%98%E5%A6%82%E4%BD%95%E6%8F%90%E9%AB%98/</id>
    <published>2023-04-07T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<p>又是一年的校招季，最近有几个学弟学妹过来询问我关于校招的许多事情，诸如校招到现在没有offer怎么办、校招该如何准备等等一些这样的问题。我向来不喜欢好为人师，主要是我觉得我的阅历还远远不够，给别人提供不了什么有价值的建议，但是诸如此类的问题倒是确实引起了我的思考，那就是我们技术人员到底该如何提高自己呢？</p><p>带着这样的思考，我阅读了不少大牛的博客、帖子，试图从他们身上看到一个技术大牛的发展之路，有<a href="https://coolshell.cn/">耗子叔</a>出品的练级攻略和一系列的学习方法论，也有<a href="https://blog.codingnow.com/">云风</a>一路发展的记录，这些都让我有了不少的思考，于是你看到了这篇文章。需要特别说明的是，我本身没有特别丰富的人生经验和阅历，此文也仅代表我的一些阅读思考，并且这篇文章讨论的更多是技术人员在技术层面上的提升，如果你希望在这篇文章寻找捷径或者想在非技术层面上和我交流，那么这篇文章可能不适合你，就没有必要读了。</p><p>我将从以下四个方面来表达我的观点。</p><h2 id="关于学习态度"><a href="#关于学习态度" class="headerlink" title="关于学习态度"></a>关于学习态度</h2><p>我经常看到有人说：今天开始我一定要好好学习，这个月我要学完xxxx，一般这样的口号喊出来以后，没过两天就好像泄气了，再也没有再听他说关于学习的事情，是的，曾经定下的宏伟目标，99%的人都没有坚持下来；我还看到过这样的情况，前两天才学完的知识，到今天又忘了，于是乎干脆不学了。诸如此类的例子还有很多，那么如何会产生这样的现象呢？</p><p>我认为第一个问题产生的原因主要在于目标不明确，也就是说你在学习前没有一个清晰的目标，这种情况下可能一时兴起学习几天，等那股劲儿过去便没有下文了。我认为长期坚持学习一定不能只靠心中的那股劲儿，需要有一个具体而清晰的目标，例如说我要在一个月内读完一本书，或者我要在一个星期内学会某项技术并且在我的项目中应用它，这样以来，你坚持学习的动力便不再只是靠着心中的那股劲儿了，你有了具体而清晰的目标，并且这个目标对你来说是能够在付出时间的情况下可以实现的，于是乎你就有了长期坚持学习的动力来源，并且以此不断的获取正反馈，没错，当你学会某项技能时，心中的成就感和满足感是会给你带来积极的反馈。</p><p>关于学习态度，这里我还想阐明主动学习和被动学习的观念。上面第二个例子，我们经常遇到说学完一个知识过几天就会忘掉的情况，其实我认为这主要是我们大多数时间的学习是被动。1946年美国学者埃德加·戴尔提出了学习金字塔的理论，这个理论将人的学习分为主动学习和被动学习两个层次：</p><ul><li>被动学习：如听讲、阅读、试听、演示，学习内容的平均留存率为5%、10%、20%和30%。</li><li>主动学习：如通过讨论、实践、教授给他人，会将原来被动学习的内容留存率从5%提升到50%、75%和90%。</li></ul><p><img src="https://p0.itc.cn/images01/20210330/a9e1d2e705de4d75aa14905bf8368f35.jpeg" alt="学习金字塔"></p><p>所以通过这个理论我们知道，我们应该更多的做一些主动学习，并且主动跟他人交流和分享，这样才能提高知识的留存率，最终转化为自己的知识。</p><p>关于学习态度，我要表达的就是这些了，需要再强调的就是学习是一个应当长期坚持的事情，我指的不仅仅是我们专业领域的学习，而是泛指整个人生的学习，我们应该端正学习的态度和转换对学习的认知，制定一个清晰的目标，并且尽可能做到主动学习。没错，你不需要特别的努力，也远远没到拼天赋的地步，你只需要做到这些并且长期坚持下去，你就能超过98%以上的大多数人，因为我们周围绝大多数的人都坚持不下去。</p><h2 id="去哪儿学"><a href="#去哪儿学" class="headerlink" title="去哪儿学"></a>去哪儿学</h2><p>接着我们再来讨论去哪儿学习的问题。</p><p>毫无疑问，我们身处一个快餐文化的时代，大多数人心态都比较浮躁，总想着急于求成，所以你总是能看到各种“速成”培训班或者是“7天精通xxxx”这样的书籍，这种质量参差不齐甚至错误百出的学习资料，正是抓住了我们的这种心理，开始在各种市场上泛滥，而那些真正的质量好、需要花大量的时间去慢慢消化的书籍等反倒无人问津，这是一个很怪的现象。</p><p>与此同时，现如今大多数人的信息渠道都被微信公众号、知乎、微博、抖音、B站占据着，这些信息渠道中有营养的知识少之又少。我们所处的时代，网络很发达，随便动动手指，我们就能获取到巨大的信息量，所以我们不再面临过去那样无东西可学的状态，反而是有太多的东西可以学，那么就需要我们对这些信息进行过滤，筛选出来有价值的信息学习，这就是涉及到我们去哪儿学、去哪里筛选信息的问题。</p><p>我认为，就计算机领域而言，其诞生于西方世界，并且计算机世界的基本规则基本都是西方制定的，所以我们应该深入到信息的源头去，去看官方文档、去看英文文档，这样我们获取的就是的未经别人咀嚼的第一手资料，应当减少摄入经过别人消化理解后的二手资料，因为别人的理解未必比你好，你可能有更好的见解，在英文阅读有障碍的前提下，应当尽可能阅读英文文档或者英文书籍的中译版，并且不断地提高英语阅读能力。</p><p>你看，还是回到上个章节的结尾，在这种情况下，你不需要特别努力，你只需要静下来看好书，阅读原版英文资料，你就能超过绝大多数人。</p><h2 id="学什么"><a href="#学什么" class="headerlink" title="学什么"></a>学什么</h2><p>最后我想谈论的是技术人员学哪些知识的问题。</p><p>在谈论这个问题之前，我想表达的是我们应该抓住技术的本质。纵观技术发展的几次大变革，无外乎是以下几个阶段：</p><ol><li>1990年-2000年，这个阶段是MB时代，是搜狐、网易、新浪、雅虎这些门户网站的时代，这个时代是一些互联网提供商整合一些咨询发布到网上。</li><li>2000年-2010年，这个阶段是GB时代，上网开始变得简单了，人们可以在互联网上上传照片，听音乐，被称为多媒体时代。</li><li>2010年-2020年，这个阶段是TB时代，也就是我们常说的移动互联网的时代，在这个时代，智能手机成了流量的载体和信息的入口，被称为数字化时代。</li><li>2022年open AI的chatGPT横空出世，我把它叫做人工智能的时代……</li></ol><p>但是你看，不管我们所处时代信息化如何变革发展，技术本质的东西变化了没有？其实是没有变的，操作系统原理还是那样，只不过硬件变好了，网络协议还是之前制定的，只不过有一些修改和优化，其本质还是没变的。所以抓住技术本质，我们要学的就是这些原理性的东西，才能以不变应万变，更重要的是，有了这些原理性的知识做支撑，不管出现再多种类的技术栈，我们都能快速上手。</p><p>另外一方面，我来说说编程语言的问题，我始终认为，一个高手不能只仅限于某个特定的语言，语言只是解决问题的工具，要抱有开放的心态，去掉对语言的偏见。那么，你可能就有另外一个问题了，现如今有这么多语言，我需要着重于哪些呢？我认为我们要学的无外乎就是及其成熟的工业级语言和具有巨大前景的热门语言，它们应当具备这样的特点：</p><ol><li>社区活跃，能解决许多场景的问题</li><li>在具体的领域有杀手级的应用</li></ol><p>我举个例子，C&#x2F;C++一直都被认为是最接近操作系统的高级语言，并且历史悠久，经过了几个时代的检验；再比如说Java，应该是当今世界最成熟的工业级语言，在web开发等诸多领域，不仅有Spring全家桶这样的超级杀手级别框架，而且社区活跃，你在开发中几乎所有的场景在这里都能找到对应的解决方案；再来说Golang，社区里有docker和k8s这样杀手级的应用，golang事实上已经成为云原生领域的标准语言。</p><p>所以，至少C你应当是一定要学的，对于做偏后端领域的来说，Java是你一定要学的，Golang也应该学习，最近Rust的兴起，你也应该关注。对于前端领域这里就不做推荐了，筛选标准同上。</p><p>好了，再学完上述原理性的知识和编程语言以后，你就已经是一个不错的技术人员了，下面就要根据自己的兴趣爱好具体在某一个或者某几个领域深耕了，比如说常见web后端开发、前端开发、分布式系统开发、数据库内核研发、架构师、软件测试、云原生等领域，我想你有了上面的基础和方法论，继续坚持下去，一定会成为某个领域的技术大牛，这一点，也是我正在努力的方向，希望我们可以一起提高进步。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>最后习惯性的来个总结，这篇文章主要结合我自身的思考讨论了技术人员如何提高的问题，我分为了三个方面，首先我们应当端庄学习态度，改变认知，这是为什么学的问题；接着我们需要找到优质的信息渠道，尽可能阅读第一手的资料，这是去哪里学的问题；最后我们需要抓住技术的本质，从复杂繁多的技术中抽离出那些不变的东西出来，这是学哪些内容的问题。</p><p>当然本篇文章属于输出本人观点的文章，你可以不同意我的观点，或者对于我的观点有什么更好的看法，欢迎与我交流。</p><p>（全文完）</p>]]></content>
    
    
    <summary type="html">近期阅读的思考</summary>
    
    
    
    <category term="阅读总结合集" scheme="https://cczywyc.com/categories/%E9%98%85%E8%AF%BB%E6%80%BB%E7%BB%93%E5%90%88%E9%9B%86/"/>
    
    
    <category term="杂记" scheme="https://cczywyc.com/tags/%E6%9D%82%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Reactor模型</title>
    <link href="https://cczywyc.com/2023/03/11/Reactor%E6%A8%A1%E5%9E%8B/"/>
    <id>https://cczywyc.com/2023/03/11/Reactor%E6%A8%A1%E5%9E%8B/</id>
    <published>2023-03-11T00:00:00.000Z</published>
    <updated>2026-03-23T07:34:30.689Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><h2 id="C10K-问题"><a href="#C10K-问题" class="headerlink" title="C10K 问题"></a>C10K 问题</h2><p><a href="http://www.kegel.com/c10k.html">C10K</a> 问题是由Dan Kegel在1999年提出的。C代表并发连接，指的是在一个单机网络服务器能够同时1万个并发请求。在过去很长一段时间，这个目标一度是很难实现的，所以也就产生了C10K的问题，但是随着网络技术的发展，C10早已经被解决，现如今已经不再是一个具有挑战性的问题了。</p><p>上面提到的C10K问题，是在32位Linux2.2内核的机器上遇到的问题，在当时由于机器的内存、网卡和网络带宽等硬件的限制，人们发现很难突破这个问题。随着电子工业的发展，根据摩尔定律，计算机的处理能力，每隔一段时间都会翻倍， 计算机的处理能力已经渐渐不再是瓶颈。与此同时，随着网络技术的发展，网络连接的IO模型和架构模式也在不断的发展，在现如今分布式集群的环境下，当时的问题虽然早已经不复存在，但是弄清楚背后的原理，能够帮助我们写出高性能的程序。</p><h1 id="IO-模型的演进"><a href="#IO-模型的演进" class="headerlink" title="IO 模型的演进"></a>IO 模型的演进</h1><p>随着操作系统内核的发展，推动软件技术的进步，结合编程语言的支持，诞生了许多种不同的IO模型，大致概括为一下五类：</p><p><img src="http://img.cczywyc.com/reactor/1.png"></p><h2 id="阻塞式-IO"><a href="#阻塞式-IO" class="headerlink" title="阻塞式 IO"></a>阻塞式 IO</h2><p>阻塞式IO也就是一对一建立连接，一个客户端和一个服务端建立连接，在服务端返回数据之前，客户端会阻塞，直到有数据返回，对应的调用过程如下图</p><p><img src="http://img.cczywyc.com/reactor/2.png"></p><p>这种模式的IO连接，操作系统会给每一个连接分配一个线程（进程），在有数据返回之前，调用线程都会阻塞，如果连接数变多，就会频繁创建、销毁线程，给操作系统带来巨大的开销，除此之外，在数据发送过程中，数据要在操作系统内核态和用户态之前切换，又增加了操作系统的开销</p><p><img src="http://img.cczywyc.com/reactor/3.png"></p><h2 id="非阻塞式-IO"><a href="#非阻塞式-IO" class="headerlink" title="非阻塞式 IO"></a>非阻塞式 IO</h2><p>上述阻塞式IO缺点明显，支持不了很多的连接，那么这时就会产生一个问题，一个网络服务如何服务更多的用户？</p><p>阻塞式IO在上述的基础上发展了一点儿，在用户态发生一次系统调用后，内核会立即返回，在用户态第一个阶段不是阻塞的，而是会不断的轮询内核数据是否准备好，第二个阶段数据在发生上下文切换的过程中，仍然是阻塞的</p><p><img src="http://img.cczywyc.com/reactor/4.png"></p><p>与此同时，我们可以看到，这种IO模型依然需要频繁的上下文切换，增大系统的开销</p><p><img src="http://img.cczywyc.com/reactor/5.png"></p><h2 id="IO-多路复用"><a href="#IO-多路复用" class="headerlink" title="IO 多路复用"></a>IO 多路复用</h2><p>前面两个IO模型，本质上都是一个连接请求分配一个进程（线程），有没有一个办法让一个进程维护多个Socket状态呢？这就是IO多路复用技术，也是目前使用最广泛的IO模型技术。</p><p>IO多路复用本质上和非阻塞式IO一样，不同的是它利用了select&#x2F;poll、epoll这样的操作系统内核支持的特性，来完成更高并发数连接的支持，从模型本质上它仍然是同步非阻塞式IO。</p><p><img src="http://img.cczywyc.com/reactor/6.png"></p><h3 id="select-poll"><a href="#select-poll" class="headerlink" title="select&#x2F;poll"></a>select&#x2F;poll</h3><p>select 实现多路复用的方式是，将已连接的 Socket 都放到一个文件描述符集合，然后调用 select 函数将文件描述符集合拷贝到内核里，让内核来检查是否有网络事件产生，检查的方式很粗暴，就是通过遍历文件描述符集合的方式，当检查到有事件产生后，将此 Socket 标记为可读或可写， 接着再把整个文件描述符集合拷贝回用户态里，然后用户态还需要再通过遍历的方法找到可读或可写的 Socket，然后再对其处理。</p><p>所以，对于 select 这种方式，需要进行 2 次「遍历」文件描述符集合，一次是在内核态里，一个次是在用户态里 ，而且还会发生 2 次「拷贝」文件描述符集合，先从用户空间传入内核空间，由内核修改后，再传出到用户空间中。</p><p>select 使用固定长度的 BitsMap，表示文件描述符集合，而且所支持的文件描述符的个数是有限制的，在 Linux 系统中，由内核中的 FD_SETSIZE 限制， 默认最大值为 1024，只能监听 0~1023 的文件描述符。</p><p>poll 不再用 BitsMap 来存储所关注的文件描述符，取而代之用动态数组，以链表形式来组织，突破了 select 的文件描述符个数限制，当然还会受到系统文件描述符限制。</p><p>但是 poll 和 select 并没有太大的本质区别，都是使用「线性结构」存储进程关注的 Socket 集合，因此都需要遍历文件描述符集合来找到可读或可写的 Socket，时间复杂度为 O(n)，而且也需要在用户态与内核态之间拷贝文件描述符集合，这种方式随着并发数上来，性能的损耗会呈指数级增长。</p><p><img src="http://img.cczywyc.com/reactor/7.png"></p><h3 id="epoll"><a href="#epoll" class="headerlink" title="epoll"></a>epoll</h3><p>epoll 通过两个方面，很好解决了 select&#x2F;poll 的问题。</p><p>第一点，epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述符，把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里，红黑树是个高效的数据结构，增删查一般时间复杂度是 O(logn)，通过对这棵黑红树进行操作，这样就不需要像 select&#x2F;poll 每次操作时都传入整个 socket 集合，只需要传入一个待检测的 socket，减少了内核和用户空间大量的数据拷贝和内存分配。</p><p>第二点， epoll 使用事件驱动的机制，内核里维护了一个链表来记录就绪事件，当某个 socket 有事件发生时，通过回调函数内核会将其加入到这个就绪事件列表中，当用户调用 epoll_wait() 函数时，只会返回有事件发生的文件描述符的个数，不需要像 select&#x2F;poll 那样轮询扫描整个 socket 集合，大大提高了检测的效率。从下图你可以看到 epoll 相关的接口作用：</p><p><img src="http://img.cczywyc.com/reactor/8.png"></p><p>epoll 的方式即使监听的 Socket 数量越多的时候，效率不会大幅度降低，能够同时监听的 Socket 的数目也非常的多了，上限就为系统定义的进程打开的最大文件描述符个数。因而，epoll 被称为解决 C10K 问题的利器。epoll 支持两种事件触发模式，分别是边缘触发（edge-triggered，ET）和水平触发（level-triggered，LT）</p><ul><li>边缘触发：使用边缘触发模式时，当被监控的 Socket 描述符上有可读事件发生时，服务器端只会从 epoll_wait 中苏醒一次，即使进程没有调用 read 函数从内核读取数据，也依然只苏醒一次，因此我们程序要保证一次性将内核缓冲区的数据读取完；</li><li>水平触发：使用水平触发模式时，当被监控的 Socket 上有可读事件发生时，服务器端不断地从 epoll_wait 中苏醒，直到内核缓冲区数据被 read 函数读完才结束，目的是告诉我们有数据需要读取。</li></ul><p>select&#x2F;poll 只有水平触发模式，epoll 默认的触发模式是水平触发，但是可以根据应用场景设置为边缘触发模式。</p><h2 id="信号驱动-IO"><a href="#信号驱动-IO" class="headerlink" title="信号驱动 IO"></a>信号驱动 IO</h2><p><img src="http://img.cczywyc.com/reactor/9.png"></p><h2 id="异步-IO"><a href="#异步-IO" class="headerlink" title="异步 IO"></a>异步 IO</h2><p>前面四种IO模型从同步异步角度来看，都是同步IO，那么有没有真正的异步IO模型呢？答案是有的，这需要操作系统内核的支持</p><p><img src="http://img.cczywyc.com/reactor/10.png"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上列出了五种IO模型，其中IO多路复用代表的有Reactor模型，异步IO代表的有Proactor模型，今天这篇文章讨论的主要是Reactor模型</p><h1 id="Reactor-IO-模型"><a href="#Reactor-IO-模型" class="headerlink" title="Reactor IO 模型"></a>Reactor IO 模型</h1><p>上述IO模型着重写了IO多路复用技术，在工业软件领域，IO多路复用技术有非常多成熟的应用，例如Nginx、Redis、Netty等。前面说了，从模型本质上来看，IO多路复用是同步非阻塞式IO，对于非阻塞式IO，不同的语言也有不同的实现，例如Java在JDK1.4引入了NIO，其就是非阻塞式IO（Netty就是基于Java NIO实现）</p><p>说了这么多IO多路复用技术，但是我们实际应用还是一个叫做Reactor模型的东西。Reactor模型是一帮大佬们结合面向对象的思想，在IO多路复用上做了一层封装，并且取了一个很牛逼的名字——Reactor模型。目前Reactor模型有三种方案，分别是：</p><ul><li>单Reactor单线程&#x2F;单进程</li><li>单Reactor多线程&#x2F;多进程</li><li>主从Reactor多线程&#x2F;多进程</li></ul><p>以上三种方案都有对应的应用，至于方案中具体选用是线程还是进程，这个跟实现的程序语言有关，一般来说，Java语言使用的是线程，例如Netty；C语言则使用的是线程或者进程都可以，例如Nginx使用的是进程，Memcache使用的是线程。</p><h2 id="单-Reactor-单线程-单进程"><a href="#单-Reactor-单线程-单进程" class="headerlink" title="单 Reactor 单线程&#x2F;单进程"></a>单 Reactor 单线程&#x2F;单进程</h2><p>一般来说，C 语言实现的是「单 Reactor 单进程」的方案，因为 C 语编写完的程序，运行后就是一个独立的进程，不需要在进程中再创建线程。而 Java 语言实现的是「单 Reactor 单线程」的方案，因为 Java 程序是跑在 Java 虚拟机这个进程上面的，虚拟机中有很多线程，我们写的 Java 程序只是其中的一个线程而已。以C语言进程为例，下面是它的方案示意图：</p><p><img src="http://img.cczywyc.com/reactor/11.webp"></p><p>可以看到进程里有Reactor、Acceptor和Handler三个对象：</p><ul><li>Reactor：监听和分发事件</li><li>Acceptor：获取连接</li><li>Handler：业务处理</li></ul><p>上图中select、accept、read和send是系统调用函数，dispatch和业务处理是需要我们完成的逻辑处理，其中dispatch是事件分发操作</p><p>下面是这个方案的流程：</p><ul><li>Reactor 对象通过 select （IO 多路复用接口） 监听事件，收到事件后通过 dispatch 进行分发，具体分发给 Acceptor 对象还是 Handler 对象，还要看收到的事件类型；</li><li>如果是连接建立的事件，则交由 Acceptor 对象进行处理，Acceptor 对象会通过 accept 方法 获取连接，并创建一个 Handler 对象来处理后续的响应事件；</li><li>如果不是连接建立事件， 则交由当前连接对应的 Handler 对象来进行响应；</li><li>Handler 对象通过 read -&gt; 业务处理 -&gt; send 的流程来完成完整的业务流程。</li></ul><p>这个方案因为全部的工作都在同一个进程里面进行，不需要考虑进程间通信，所以实现起来较为简单，但是这个方案存在2个明显缺点：</p><ol><li>因为只有一个进程，无法充分利用多核CPU的能力</li><li>Handler 对象在业务处理时，整个进程是无法处理其他连接的事件的，如果业务处理耗时比较长，那么就造成响应的延迟；</li></ol><p>所以，单 Reactor 单进程的方案不适用计算机密集型的场景，只适用于业务处理非常快速的场景。Redis 是由 C 语言实现的，它采用的正是「单 Reactor 单进程」的方案，因为 Redis 业务处理主要是在内存中完成，操作的速度是很快的，性能瓶颈不在 CPU 上，所以 Redis 对于命令的处理是单进程的方案。</p><h2 id="单-Reactor-多线程-多进程"><a href="#单-Reactor-多线程-多进程" class="headerlink" title="单 Reactor 多线程&#x2F;多进程"></a>单 Reactor 多线程&#x2F;多进程</h2><p>如果要克服「单 Reactor 单线程 &#x2F; 进程」方案的缺点，那么就需要引入多线程 &#x2F; 多进程，这样就产生了单 Reactor 多线程 &#x2F; 多进程的方案。下图是它的方案示意图：</p><p><img src="http://img.cczywyc.com/reactor/12.png"></p><p>下面是这个方案的流程：</p><ul><li>Reactor 对象通过 select （IO 多路复用接口） 监听事件，收到事件后通过 dispatch 进行分发，具体分发给 Acceptor 对象还是 Handler 对象，还要看收到的事件类型；</li><li>如果是连接建立的事件，则交由 Acceptor 对象进行处理，Acceptor 对象会通过 accept 方法 获取连接，并创建一个 Handler 对象来处理后续的响应事件；</li><li>如果不是连接建立事件， 则交由当前连接对应的 Handler 对象来进行响应；</li></ul><p>上面这三个步骤和单Reactor单线程&#x2F;单进程方案是一样的，下面就开始不一样了，</p><ul><li>Handler 对象不再负责业务处理，只负责数据的接收和发送，Handler 对象通过 read 读取到数据后，会将数据发给子线程里的 Processor 对象进行业务处理；</li><li>子线程里的 Processor 对象就进行业务处理，处理完后，将结果发给主线程中的 Handler 对象，接着由 Handler 通过 send 方法将响应结果发送给 client；</li></ul><p>单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能，那既然引入多线程，那么自然就带来了多线程竞争资源的问题。例如，子线程完成业务处理后，要把结果传递给主线程的 Reactor 进行发送，这里涉及共享数据的竞争。要避免多线程由于竞争共享资源而导致数据错乱的问题，就需要在操作共享资源前加上互斥锁，以保证任意时间里只有一个线程在操作共享资源，待该线程操作完释放互斥锁后，其他线程才有机会操作共享数据。</p><p>再来看单 Reactor 多进程的方案：</p><p>事实上，单 Reactor 多进程相比单 Reactor 多线程实现起来很麻烦，主要因为要考虑子进程 &lt;-&gt; 父进程的双向通信，并且父进程还得知道子进程要将数据发送给哪个客户端。而多线程间可以共享数据，虽然要额外考虑并发问题，但是这远比进程间通信的复杂度低得多，因此实际应用中也看不到单 Reactor 多进程的模式。</p><p>另外，「单 Reactor」的模式还有个问题，因为一个 Reactor 对象承担所有事件的监听和响应，而且只在主线程中运行，在面对瞬间高并发的场景时，容易成为性能的瓶颈的地方。</p><h2 id="主从-Reactor-多线程-多进程"><a href="#主从-Reactor-多线程-多进程" class="headerlink" title="主从 Reactor 多线程&#x2F;多进程"></a>主从 Reactor 多线程&#x2F;多进程</h2><p>直接来看方案示意图：</p><p><img src="http://img.cczywyc.com/reactor/13.png"></p><p>下面是此方案的流程：</p><ul><li>主线程中的 MainReactor 对象通过 select 监控连接建立事件，收到事件后通过 Acceptor 对象中的 accept 获取连接，将新的连接分配给某个子线程；</li><li>子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听，并创建一个 Handler 用于处理连接的响应事件；</li><li>如果有新的事件发生时，SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应；</li><li>Handler 对象通过 read -&gt; 业务处理 -&gt; send 的流程来完成完整的业务流程。</li></ul><p>这种方案虽然看起来多了一步，但是实现起来却比单Reactor多线程&#x2F;多进程要简单的多，原因如下：</p><ul><li>主线程和子线程分工明确，主线程只负责接收新连接，子线程负责完成后续的业务处理；</li><li>主线程和子线程的交互很简单，主线程只需要把新连接传给子线程，子线程无须返回数据，直接就可以在子线程将处理结果发送给客户端。</li></ul><p>其中Netty和Memcache采用的都是主从Reactor多线程的方案，Nginx采用的是主从Reactor多进程的方案。</p>]]></content>
    
    
    <summary type="html">网络编程IO模型及应用实现</summary>
    
    
    
    <category term="网络协议文章合集" scheme="https://cczywyc.com/categories/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E6%96%87%E7%AB%A0%E5%90%88%E9%9B%86/"/>
    
    
    <category term="IO 模型" scheme="https://cczywyc.com/tags/IO-%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="网络编程" scheme="https://cczywyc.com/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    
  </entry>
  
</feed>
