<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Charles的技术博客</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://yoursite.com/"/>
  <updated>2021-06-14T13:00:31.229Z</updated>
  <id>http://yoursite.com/</id>
  
  <author>
    <name>Charles0429</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>OceanBase源码阅读工具-Eclipse篇</title>
    <link href="http://yoursite.com/uncategorized/yuque/OceanBase%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%B7%A5%E5%85%B7-Eclipse%E7%AF%87/"/>
    <id>http://yoursite.com/uncategorized/yuque/OceanBase源码阅读工具-Eclipse篇/</id>
    <published>2021-06-14T11:05:24.000Z</published>
    <updated>2021-06-14T13:00:31.229Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>工欲善其事，必先利其器，OceanBase 社区版代码量较大，在学习 OceanBase 代码时，需要有一款较好的源码阅读工具，来帮助大家更好地理解代码的执行流程、接口的层次关系等等。根据我在 OceanBase 6 年来的开发和阅读代码体验，一款较好的源代码阅读工具需要如下的一些功能：</p><ul><li>按照符号搜索函数名、类名等</li><li>按照文本搜索文件名</li><li>文件内符号查找</li><li>按照文本搜索项目空间的字符串</li><li>查看类的继承关系</li><li>查看函数调用栈</li><li>查看某个变量的调用栈</li><li>跳转到某个函数的实现</li></ul><p>上面的功能是在平时的开发和源码阅读过程中使用较多的功能，根据我目前的使用经验，有两款工具基本符合上面的条件，第一款是 Eclipse C/C++版本，第二款是 CCLS，可以在 vim 等编辑器中使用。本文将主要介绍 Eclipse 下的这些功能。</p><h1 id="使用-Eclipse"><a href="#使用-Eclipse" class="headerlink" title="使用 Eclipse"></a>使用 Eclipse</h1><h2 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h2><p>可以从 Eclipse 官网下载到 Eclipse C/C++，下载完成后，打开里面的 Eclipse 应用即可使用</p><h2 id="导入-OceanBase-代码"><a href="#导入-OceanBase-代码" class="headerlink" title="导入 OceanBase 代码"></a>导入 OceanBase 代码</h2><p>从 Eclipse 的菜单栏 New-&gt;Makefile Project with Existing Code，导入 OceanBase 的源代码。项目生成后，Eclipse 会自动为代码构建索引，在普通的个人电脑上，索引的时间大约半个小时左右。</p><h2 id="使用-Eclipse-阅读-OceanBase-代码"><a href="#使用-Eclipse-阅读-OceanBase-代码" class="headerlink" title="使用 Eclipse 阅读 OceanBase 代码"></a>使用 Eclipse 阅读 OceanBase 代码</h2><h3 id="按照符号搜索函数名、类名等"><a href="#按照符号搜索函数名、类名等" class="headerlink" title="按照符号搜索函数名、类名等"></a>按照符号搜索函数名、类名等</h3><p>在开发和源码阅读中，有时候我们记得某个函数或类的名字，但忘记所在代码文件了，此时，按照符号搜索函数名、类名功能就非常方便了。在 Eclipse 中，通过 Navigate-&gt;Open Element 使用该功能。</p><h3 id="按照文本搜索文件名"><a href="#按照文本搜索文件名" class="headerlink" title="按照文本搜索文件名"></a>按照文本搜索文件名</h3><p>有时候，我们知道某个功能的代码实现在某个文件内，但忘记了具体的函数名了，此时，我们可以结合按照文本搜索文件名，以及在文件内符号查找来找到对应的代码。在 Eclipse 中，通过 Navigate-&gt;Open Resource 使用该功能。</p><h3 id="文件内符号查找"><a href="#文件内符号查找" class="headerlink" title="文件内符号查找"></a>文件内符号查找</h3><p>当我们定位到具体的代码文件后，如果需要在文件中查找对应的类、符号以及类内成员名等，可以使用该功能。在 Eclipse 中，打开某个代码文件后，通过右击鼠标-&gt;Quick Outline，在对应的搜索框里输入相应的符号即可查找。</p><h3 id="按照文本搜索项目空间的字符串"><a href="#按照文本搜索项目空间的字符串" class="headerlink" title="按照文本搜索项目空间的字符串"></a>按照文本搜索项目空间的字符串</h3><p>有时候我们需要按照文本搜索匹配的代码，例如，在 OceanBase 中，内存分配是按照模块命名的，如果我们想查找使用某个模块名分配内存的代码，那么我们可以搜索该模块名的字符串来匹配代码。在 Eclipse 中，可以通过 Search-&gt;File 打开对应的搜索框，在里面输入想要查找的字符串。</p><h3 id="查看类的继承关系"><a href="#查看类的继承关系" class="headerlink" title="查看类的继承关系"></a>查看类的继承关系</h3><p>在 C++代码中，可能会使用继承来组织代码，有时候需要查看类的继承关系来看不同场景下的实现。在 Eclipse 中，通过光标选中类名，然后调用 Navigate-&gt;Open Type Hierarchy 来查看类的继承关系。</p><h3 id="查看函数调用栈"><a href="#查看函数调用栈" class="headerlink" title="查看函数调用栈"></a>查看函数调用栈</h3><p>在排查问题时，我们可能会碰到某个函数运行处理的结果不符合预期的情况，此时，我们可能需要通过函数调用栈来跟踪函数执行路径，来判断哪条路径可能出问题了。在 Eclipse 中，光标移动到函数名上，Navigate-&gt;Open Call Hierarachy。根据我在 MAC 开发环境的使用经验，Eclipse 能够快速又较为准确地对 OceanBase 这样体量代码的函数查看调用栈，其他的 IDE 暂时还不能够同时满足快速和准确。</p><h3 id="查看某个变量的调用栈"><a href="#查看某个变量的调用栈" class="headerlink" title="查看某个变量的调用栈"></a>查看某个变量的调用栈</h3><p>有时候需要理解某个类内成员变量的作用，此时要查看其所有使用的地方。在 Eclipse 中，跟查看函数调用栈使用方式一样，可以查看某个变量的调用栈。</p><h3 id="跳转到某个函数的实现"><a href="#跳转到某个函数的实现" class="headerlink" title="跳转到某个函数的实现"></a>跳转到某个函数的实现</h3><p>在我们学习某个功能的实现时，通常需要自上而下跟踪代码路径下的实现，因此，需要有跳转到某个函数实现的能力。在 Eclipse 中，将光标移动到调用的函数上，然后通过 Navigate-&gt;Open Declaration 跟踪到具体的实现。有时候跟踪到具体实现后，又希望回到之前的代码处继续跟踪下面的流程，此时可以通过 Navigate-&gt;Back 来回到之前代码处。<br>本文提到的代码查找功能同样适用于任何其他 C/C++项目，如果有学习其他 C/C++代码的需求，也可以作为参考。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;工欲善其事，必先利其器，OceanBase 社区版代码量较大，在学习 OceanBase 代码时，需要有一款较好的源码阅读工具，来帮助大家更
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>OceanBase代码导览</title>
    <link href="http://yoursite.com/uncategorized/yuque/OceanBase%E4%BB%A3%E7%A0%81%E5%AF%BC%E8%A7%88/"/>
    <id>http://yoursite.com/uncategorized/yuque/OceanBase代码导览/</id>
    <published>2021-06-14T08:19:29.000Z</published>
    <updated>2021-06-14T13:00:31.341Z</updated>
    
    <content type="html"><![CDATA[<h1 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h1><p>OceanBase 是蚂蚁集团/阿里巴巴完全自主研发的一款分布式关系型数据库，在今年 6 月 1 号，OceanBase 开源了 3.1 版本的源代码，称为 OceanBase 社区版（下文简称 OceanBase）。OceanBase 是一款开源分布式 HTAP（Hybrid Transactional/Analytical Processing）数据库管理系统，具有原生分布式架构，支持金融级高可用、透明水平扩展、分布式事务、多租户和语法兼容等企业级特性。本文先简单地介绍 OceanBase 的架构，然后描述 OceanBase 的源代码的整体结构。</p><h1 id="OceanBase-社区版架构"><a href="#OceanBase-社区版架构" class="headerlink" title="OceanBase 社区版架构"></a>OceanBase 社区版架构</h1><h2 id="集群架构"><a href="#集群架构" class="headerlink" title="集群架构"></a>集群架构</h2><p>OceanBase 社区版的一个集群通常由多个 Zone 组成，一个 Zone 由一个或多个 ObServer 组成的，每个 ObServer 都具有计算和存储的功能。在 ObServer 中有一个较为特殊，负责总控服务的节点称为 RootService，负责管理集群的元数据和路由信息，其中，元数据是按照多版本方式管理的。OceanBase 按照分区的方式管理数据，一张表包含一个或多个分区，每个分区的数据会存储在多个 Zone 中，每个 Zone 都是一份完整的数据拷贝（副本）。每个分区的副本中会有一个 Leader 副本，负责处理该分区的读写请求。如下图所示，该 OceanBase 集群中有 3 个 Zone，每个 Zone 内有 3 个 ObServer，每个 Zone 内各自有一个 RootService 节点，每个分区在每个 Zone 里都有一个分区，总共 3 个副本，通过一致性算法 PAXOS 来保证三个副本数据的一致性。</p><p><img src="https://cdn.nlark.com/yuque/0/2021/jpeg/95060/1623571090436-5a4375d5-c801-4987-b1a6-2f6af24fc3f7.jpeg#height=484&id=BP97c&originHeight=484&originWidth=692&originalType=binary%E2%88%B6=1&size=0&status=done&style=none&width=692"></p><h2 id="ObServer-架构"><a href="#ObServer-架构" class="headerlink" title="ObServer 架构"></a>ObServer 架构</h2><p>在每个 ObServer 中都包含 SQL 引擎、存储引擎两个部分，其中 SQL 引擎负责 SQL 的解析和执行，存储引擎主要包括事务处理、分布式环境数据一致性处理以及基于 LSM-Tree 架构的数据存储。</p><h3 id="SQL-引擎"><a href="#SQL-引擎" class="headerlink" title="SQL 引擎"></a>SQL 引擎</h3><p>跟单机数据库类似，OceanBase 的 SQL 引擎也主要分为三个部分，包括解析器、优化器和执行器。当 SQL 引擎接受到了 SQL 请求后，经过语法解析、语义分析、查询重写、查询优化等一系列过程后，再由执行器来负责执行。所不同的是，在分布式数据库里，查询优化器会依据数据的分布信息生成分布式的执行计划。如果查询涉及的数据在多台服务器，需要走分布式计划，这是分布式数据库 SQL 引擎的一个重要特点，也是十分考验查询优化器能力的场景。OceanBase 数据库查询优化器做了很多优化，诸如算子下推、智能连接、分区裁剪等。如果 SQL 语句涉及的数据量很大，OceanBase 数据库的查询执行引擎也做了并行处理、任务拆分、动态分区、流水调度、任务裁剪、子任务结果合并、并发限制等优化技术。</p><h3 id="存储引擎"><a href="#存储引擎" class="headerlink" title="存储引擎"></a>存储引擎</h3><p>OceanBase 的存储引擎主要提供事务执行、多副本数据一致性处理和 LSM-Tree 存储引擎三方面能力。在事务执行方面，OceanBase 需要保证在分布式环境下事务的 ACID 特性；在多副本数据一致性处理方面，OceanBase 采用了 PAXOS 一致性算法来保证数据在正常以及容灾场景下多副本数据的一致性；在数据存储方面，OceanBase 提供了 LSM-Tree 架构的存储引擎，具有高压缩比等特点。</p><h1 id="OceanBase-源代码结构"><a href="#OceanBase-源代码结构" class="headerlink" title="OceanBase 源代码结构"></a>OceanBase 源代码结构</h1><p>OceanBase 源代码可以从<a href="https://github.com/oceanbase/oceanbase">OceanBase 开源项目链接</a>下载，所有内核相关的源代码都在 src 目录下，它下面主要有 rootserver、sql、storage、observer、election 和 clog 目录，下面将分别介绍这些子目录的代码结构。</p><h2 id="sql-目录"><a href="#sql-目录" class="headerlink" title="sql 目录"></a>sql 目录</h2><p>sql 里有 parser、resolver、rewrite、optimizer、executor、dtl、code_generator、engine、plan_cache、session 等。</p><ul><li>parser：SQL 语法解析</li><li>resolver：SQL 语义解析</li><li>rewrite：SQL 重写</li><li>optimizer：优化器</li><li>execuctor：执行器</li><li>dtl：数据传输层</li><li>code_generator：由逻辑执行计划生成物理执行计划</li><li>engine：各种物理算子的实现</li><li>plan_cache：物理执行计划缓存</li><li>session：数据库会话管理</li></ul><h2 id="storage-目录"><a href="#storage-目录" class="headerlink" title="storage 目录"></a>storage 目录</h2><p>storage 里有 blocksstable、compaction、gts、memtable、replayengine、transaction 以及 storage 目录本身的代码文件。</p><ul><li>blocksstable：包括存储格式、各种缓存等</li><li>compaction：LSM-Tree 存储中 compaction 相关的逻辑</li><li>gts：全局时间戳实现，主要用于提供全局事务版本号</li><li>memtable：LSM-Tree 引擎中 memtable 的实现</li><li>replayengine：redo 日志回放的实现</li><li>transaction：事务处理相关</li><li>storage 目录本身的代码：基于上述代码提供读写等服务</li></ul><h2 id="rootserver-目录"><a href="#rootserver-目录" class="headerlink" title="rootserver 目录"></a>rootserver 目录</h2><p>rootserver 目录包括 backup、restore、virtual_table 和 rootserver 目录本身的代码</p><ul><li>backup：备份中 RootService 管理部分</li><li>restore：恢复中 RootService 管理部分</li><li>virtual_table：RootService 相关的虚拟表，是 OceanBase 中将内存中信息通过数据库表格方式展示的手段</li><li>rootserver：包含了 Schema 元数据管理、路由信息管理、负载均衡、机器管理、资源规格管理等</li></ul><h2 id="observer-目录"><a href="#observer-目录" class="headerlink" title="observer 目录"></a>observer 目录</h2><p>observer 目录包括 omt、virtual_table、observer 目录本身的代码</p><ul><li>omt：ObServer 单机租户管理</li><li>virtual_table：ObServer 级别的虚拟表</li><li>observer 目录本身的代码：一些 ObServer 级别的服务，例如 ObServer 的启动，路由表的汇报等</li></ul><h2 id="election-目录"><a href="#election-目录" class="headerlink" title="election 目录"></a>election 目录</h2><p>包含了 PAXOS 选主相关的实现。</p><h2 id="clog-目录"><a href="#clog-目录" class="headerlink" title="clog 目录"></a>clog 目录</h2><p>包含了 PAXOS 相关的实现。</p><p>本文主要是对 OceanBase 代码做了概览，希望能帮助大家对 OceanBase 整体代码结构有大概的认识。后续还将结合 OceanBase 的设计、代码对各个模块相应的解读。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;headerlink&quot; title=&quot;介绍&quot;&gt;&lt;/a&gt;介绍&lt;/h1&gt;&lt;p&gt;OceanBase 是蚂蚁集团/阿里巴巴完全自主研发的一款分布式关系型数据库，在今年 6 月 1 号，OceanBase 开源了 3.1 版
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Google F1是如何做Schema变更的</title>
    <link href="http://yoursite.com/distributed/f1-online-schema-evolution/"/>
    <id>http://yoursite.com/distributed/f1-online-schema-evolution/</id>
    <published>2018-06-16T02:05:28.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>F1是Google自主研发的分布式数据库，采用计算与存储分离的架构，存储层采用Spanner作为分布式KV存储引擎，计算层则是F1团队研发的分布式SQL引擎，其整体架构如下图</p><p><img src="http://oserror.com/images/f1_arechitecture.png" alt="F1 Arechitecture"></p><p>存储层向SQL层(F1 Server)提供KV操作接口，而SQL层负责将用户请求的关系Schema数据转换成KV存储格式。在此架构下，F1有以下特点</p><ol><li>共享的存储层：所有的F1 Server共享存储数据，F1 Server不再做数据划分；</li><li>无状态的计算层：F1 Server层是无状态的，因此，同一个事务的不同语句是可能在不同的F1 Server执行的；</li><li>无全局的成员列表：由于F1 Server层是无状态的，因此，没有维护全局的F1 Server的成员列表；</li><li>关系性Schema：每个F1 Server都有一份关系型Schema的拷贝，F1 Server会将关系性请求映射成分布式KV存储能处理的请求；</li><li>大规模：F1 Server的规模通常是成百上千，其上运行的业务量较大。</li></ol><p>根据F1的上述特点，对其Schema变更需要有如下需求</p><ol><li>保持数据可用：由于集群规模大，Schema变更时需要保证数据是持续可用的，这意味者不能采用如加锁等简单粗暴但影响业务的变更方式；</li><li>异步的Schema变更：由于没有全局的成员列表，无法保证所有F1 Server同步地获取到同一个版本的Schema，因此，Schema变更时需要能够支持F1 Server异步的Schema变更；</li><li>对数据访问的性能影响较小：Schema变更属于后台任务，应该对前台的用户请求保持较小的性能影响。</li></ol><p>针对上述Schema变更需求，F1团队分析了异步的Schema变更可能导致的数据不一致的问题，提出了一种安全的Schema变更算法。本文将先简单介绍KV存储引擎的提供的接口，然后分析异步的Schema变更导致的问题，最后再描述F1的Schema变更算法以及其限制点。</p><h1 id="KV存储引擎"><a href="#KV存储引擎" class="headerlink" title="KV存储引擎"></a>KV存储引擎</h1><p>F1 Server所依赖的存储引擎需要提供三个操作接口：</p><ol><li>put：更新或者插入一条KV记录；</li><li>del：删除一条KV记录；</li><li>get：获取一条或多条KV记录，匹配提供的Key的前缀</li></ol><p>除此之外，还需要提供如下语义保证：</p><ol><li>Commit Timestamps：每个KV记录都会存储修改时间；</li><li>原子的批量读写：支持原子的执行多条get和put操作。</li></ol><p>由于F1对外提供的是关系型的Schema，因此，F1 Server负责将关系型Schema的数据转换成相应的KV记录。在关系型Schema中，最常用的Schema元素是表格，表格的Schema定义中一般会包含多个列定义，其中会在多列中选取其中的某一列或者几列来作为主键列，主键列能唯一的标识一行。很自然地，将表格的一行存储为KV记录时，会选取主键列的列值作为Key，F1并没有简单的采用将关系表格中的一行映射成一条KV记录的方法，而是将除主键列之外的每列都其映射成一条KV记录，规则如下</p><figure class="highlight plaintext"><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">Key:table_name.primary_key_column_values.non_primary_key_column_name</span><br><span class="line">Value:non_primary_key_column_value</span><br></pre></td></tr></table></figure><p>对于主键列，只需要用一个特殊的列名，标识其存在即可，F1使用了列名为exists，列值为null。</p><p>下面举一个例子来说明上述映射规则</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">create</span> <span class="keyword">table</span> Example</span><br><span class="line">(</span><br><span class="line">first_name <span class="type">varchar</span>(<span class="number">20</span>),</span><br><span class="line">    last_name <span class="type">varchar</span>(<span class="number">20</span>),</span><br><span class="line">    age <span class="type">int</span>,</span><br><span class="line">    phone_number <span class="type">varchar</span>(<span class="number">20</span>),</span><br><span class="line">    <span class="keyword">primary</span> key (first_name, last_name),</span><br><span class="line">);</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">first_name        last_name        age        phone_number</span><br><span class="line">John                Doe            24         555-123-4567</span><br><span class="line">Jane                Doe            35         555-456-7890</span><br></pre></td></tr></table></figure><p>按照上述规则转换成的KV记录为</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></pre></td><td class="code"><pre><span class="line">Key                            Value</span><br><span class="line">Example.John.Doe.exists        null</span><br><span class="line">Example.John.Doe.age           24</span><br><span class="line">Example.John.Doe.phone_number  555-123-4567</span><br><span class="line">Example.Jane.Doe.exists        null</span><br><span class="line">Example.Jane.Doe.age           35</span><br><span class="line">Example.Jane.Doe.phone_number  555-456-7890</span><br></pre></td></tr></table></figure><p>在关系型数据库中，除了数据表本身之外，还有索引表，F1中索引表的存储格式为</p><figure class="highlight plaintext"><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">Key:table_name.index_name.index_column_values.primary_column_values</span><br><span class="line">Value:null</span><br></pre></td></tr></table></figure><p>在索引表对应Key中存储主键列的值一方面是为了避免Key重复，另一方面是为了查询时能够做回表操作。接着上面的例子，假设在Example表格中，建了一个索引index_age，如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">create index index_age on Example(age);</span><br></pre></td></tr></table></figure><p>那么，索引表中的两行对应的KV记录如下</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></pre></td><td class="code"><pre><span class="line">Key                              Value</span><br><span class="line">Example.index_age.24.John.Doe     null</span><br><span class="line">Example.index_age.35.Jane.Doe     null</span><br></pre></td></tr></table></figure><p>除了将关系性Schema中的行映射成KV记录外，F1 Server还负责将关系操作映射对应的KV操作，通常的关系操作包括insert、delete、update和select。</p><ol><li>insert：插入会指定各列的值，根据Schema的行映射关系，生成对应的KV记录，将记录写入到KV存储中；</li><li>delete：先通过用户请求或者get接口确定要删除的所有Key，然后删除对应的KV记录，需要KV存储引起提供的get和del接口；</li><li>update：先通过用户请求或者get接口确定要更新的所有Key，如果是非主键列更新，则调用put接口，如果是主键列更新，则是del和put接口；</li><li>select：通过get接口查询到匹配的KV记录，并映射成关系型Schema中的行；</li></ol><h1 id="Schema变更带来的问题"><a href="#Schema变更带来的问题" class="headerlink" title="Schema变更带来的问题"></a>Schema变更带来的问题</h1><p>根据F1的架构特点，事务执行时，有可能出现不同的语句在不同的F1 Server执行的情况，那么不同的语句可能使用了不同版本的关系型Schema。为了设计和实现简单，F1只允许系统中同时出现两种不同版本的Schema，如下</p><p><img src="http://oserror.com/images/f1_schema_change.png" alt="F1 Schema change"></p><p>假设Schema版本S1 &lt; S2，且S2比S1多了一张索引表，有如下执行过程</p><ol><li>在S2版本的F1 Server上执行insert语句，由于S2版本包括索引表，因此会生成索引表相关的KV记录；</li><li>在S1版本的F1 Server上执行delete语句，且和1中insert语句使用相同的主键，由于S1版本不包括索引表，因此索引表相关的KV记录不会被删除；</li></ol><p>当上述事务执行完成后，索引表将会有多余的中间数据，导致数据表和索引表的数据不一致。</p><h1 id="F1-Schema变更算法"><a href="#F1-Schema变更算法" class="headerlink" title="F1 Schema变更算法"></a>F1 Schema变更算法</h1><p>F1 Schema变更算法要解决的问题是在系统中至多允许存在两个版本的Schema的前提下，保证数据库的表示(即所有的记录集合)在Schema变更时保持一致性。在讨论如何解决该问题之前，还需要定义清楚，何为数据库表示的一致性？在F1中，Schema的最终状态分为两种absent和public，一个数据库表示在某个版本的Schema S上是一致的，其需要满足如下条件：</p><ol><li>列值记录存在，则数据库表示中一定有包含它的行和表；</li><li>每行的public状态的列都有对应的列值；</li><li>所有的索引记录，其索引表在Schema S中一定存在；</li><li>所有public的索引包含其主表所有的行对应的索引记录；</li><li>所有的索引记录都会指向主表中合法的记录；</li><li>所有的public约束都会被严格遵守；</li><li>没有未知的值；</li></ol><p>如果数据库中包含了本不应该属于它的数据，此不一致称为orphan data anomaly，即违反了1,3,5,7约束项；如果数据库缺少了本应该属于它的数据，或者违反了一个public约束，此不一致称为integrity anomaly，即违反了2,4,6约束项。</p><p>假设OP(S)，代表任意的delete,update,insert或select在特定的Schema S中执行，任意一个正确实现的操作，应该保证该操作在Schema S中执行后，数据库表示是一致的，但是它不能保证按照Schema S版本执行之后，数据库表示还能在其他版本的Schema S’中也是一致的。</p><p>因此，Schema变更前后(S1-&gt;S2)的数据库表示一致性的，需要满足如下条件</p><ol><li>任意的OP(S1)需要在Schema版本S1应用后，数据库表示在Schema版本S2上是一致的；</li><li>任意的OP(S2)需要在Schema版本S2应用后，数据库表示在Schema坂本S1上是一致的；</li></ol><p>从前文的分析知道，直接将Schema元素从absent-&gt;public是可能导致数据库表示不一致的，因此，F1引入了Schema变更的中间状态，包括delete-only，write-only和write-only constraint。</p><ul><li>delete-only:一个delete-only状态的表，列和索引，不能被select，并且满足如下条件</li></ul><ol><li>对于表或列，只能被delete修改</li><li>对于索引，只能被delete和update修改</li></ol><ul><li><p>write-only：一个write-only的列或者索引，可以做insert，delete和update操作，但不能做select操作</p></li><li><p>write-only constraint：write-only constraint只能应用于新的insert，delete和update的数据，但对已有的数据不保证约束条件是满足的</p></li></ul><h2 id="添加-删除-Optional的Schema元素"><a href="#添加-删除-Optional的Schema元素" class="headerlink" title="添加/删除 Optional的Schema元素"></a>添加/删除 Optional的Schema元素</h2><p>从前面Schema变更的问题可以看出，Schema版本的回退导致在新版本插入的数据，回退到老版本时无法删除，因此，直观上来讲，对于添加一个Optional的Schema元素，可以在absent-&gt;public中间插入一个delete-only状态，即absent-&gt;delete-only-&gt;public，接下来将说明这样的变更过程是安全的。</p><p>首先来看absent-&gt;delete-only，由于两种状态都不会产生新的Schema元素的数据，因此，保证了不会出现orphan data问题，并且由于两种状态都是非public状态，因此，不会出现integrity问题。</p><p>接着来看delete-only-&gt;public，由于两种状态都能够删除需要删除的数据，因此，不会出现orphan data问题，且由于Schema元素是Optional的，因此，不会出现integrity问题。</p><p>删除的变更过程正好相反，不再描述，下文同。</p><h2 id="添加-删除-Required的Schema元素"><a href="#添加-删除-Required的Schema元素" class="headerlink" title="添加/删除 Required的Schema元素"></a>添加/删除 Required的Schema元素</h2><p>添加Required的Schema元素不能直接采用absent-&gt;delete-only-&gt;public的变更流程，因为，添加Required的Schema元素需要补充已有的数据，而delete-only状态是无法补充数据的，因此，需要在delete-only-&gt;public中添加一个write-only状态，即添加一个Required的Schema元素需要经过absent-&gt;delete-only-&gt;write-only-&gt;public的变更流程，接下来就来说明这个变更过程是能保证数据的一致性的。</p><p>absent-&gt;delete-only状态前面已描述过，这里不再赘述。对于delete-only-&gt;write-only状态转换，由于两种状态下都会删除新添加的Schema元素相关联的数据，因此不会出现orphan data问题，并且，由于两种Schema状态都是非public的，能保证不会出现integrity问题。</p><p>write-only-&gt;public的状态变化过程中，需要先保证对已有的数据完成补充，才能最终将状态变成public，由于两种状态都能删除新添加的Schema相关的数据，因此，不会出现orphan data问题，并且，由于两种Schema状态都会对新写入的数据做约束检查，新数据是能保证约束的，而对于原有的数据，需要后台任务在填充时做检查，如果都满足条件，才能将Schema元素变成public，否则，Schema变更应该失败。</p><h2 id="实现限制点"><a href="#实现限制点" class="headerlink" title="实现限制点"></a>实现限制点</h2><p>由于F1 Server只允许同时存在两个不同版本的Schema，因此，在实现时，需要通过Schema Lease来保证Schema的更新，如果一个F1 Server无法更新Lease，那么会自动退出。同时，F1 Server的写操作不能跨两个版本的Schema，因此，如果一个写操作是基于老版本Schema的，是无法成功的，需要重试。</p><p>另外，由于同时只允许存在两个不同版本的Schema，因此，1个Schema Lease只能做一个中间状态的变更，通常的不带数据不全的DDL操作，需要等待3-4个Schema Lease，而Schema Lease是分钟级别的，因此，一个不带数据不全的DDL变更需要等待分钟到数十分钟；如果带上数据补全，则根据原有数据量的大小，耗费的时间可能会很长。F1中采用把多个DDL变更的中间批量执行，来提高Schema变更的效率。</p><p>最后，博客推送的公众号欢迎大家关注<br><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://research.google.com/pubs/archive/41376.pdf">Online, Asynchronous Schema Change in F1</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;F1是Google自主研发的分布式数据库，采用计算与存储分离的架构，存储层采用Spanner作为分布式KV存储引擎，计算层则是F1团队研发的
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式数据库" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="Online DDL" scheme="http://yoursite.com/tags/Online-DDL/"/>
    
  </entry>
  
  <entry>
    <title>分布式系统中常见技术解决的问题是什么?</title>
    <link href="http://yoursite.com/distributed/system-principle/"/>
    <id>http://yoursite.com/distributed/system-principle/</id>
    <published>2018-03-21T00:09:08.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>在分布式系统中，经常会碰到的技术名词一般有Replication、Partition、Consensus、Transaction等等，这些技术在分布式系统设计中都是非常重要的，本文通过对分布式系统的Reliability、Scalability和Maintainability特性的讨论，描述这些技术解决的问题。</p><h1 id="Reliability"><a href="#Reliability" class="headerlink" title="Reliability"></a>Reliability</h1><p>Reliability，指的是在任何情况下，系统正常工作的能力。如果一个系统在发生任何异常时，都能正常的工作，那么系统是完全可靠的。现实中，异常种类很多，有的往往难以事先避免，因此，了解可能的异常并分析如何在异常发生时快速恢复是非常重要的。一般地，异常包括硬件异常，软件异常和人为异常。</p><h2 id="硬件异常"><a href="#硬件异常" class="headerlink" title="硬件异常"></a>硬件异常</h2><p>硬件异常种类很多，硬盘，电源等任意一个部件的损坏，都可能导致服务器不能正常的工作。通常这类异常难以避免，但是，我们可以通过一些技术手段来实现异常发生后的快速恢复，不管是从软件角度还是硬件角度，基本的解决思路都是冗余。从硬件角度来讲，我们可以通过单机冗余多份硬件，当其中某个硬件发生异常时，可以快速地用好的硬件替换掉故障的硬件，这种方式的硬件冗余对于数据中心级的故障是没有作用的；从软件角度来讲，我们可以通过多副本(Replication)来实现快速恢复，当某台服务器硬件异常时，可以在软件层面将流量导入到新的副本上(实际上也有硬件冗余，但这种方式更为灵活)，除了Replication之外，有时候为了减少单台服务器故障对所有用户的影响，可以对用户数据做Partition，单台服务器只存某一部分用户的数据，这样单机故障就只会影响一部分用户了。引入Replication后，如何保证多副本的数据的一致性又成了一个问题(Consensus)，Paxos和Raft算法就是为了解决这类问题。</p><h2 id="软件异常"><a href="#软件异常" class="headerlink" title="软件异常"></a>软件异常</h2><p>软件异常一般指的是系统的bug，这里面不仅包括自己写的系统的bug，也包括依赖的服务系统的bug。软件异常同样也是不能完全避免的，因此，在发生软件异常时，也需要有快速恢复的手段，通常有三种方法：</p><ol><li>通过调整软件已有的配置参数，规避问题</li><li>重启软件或者依赖的服务，消除异常状态</li><li>直接修复bug，并升级版本</li></ol><p>在没有发生致命性问题时，一般采用方法1或2来恢复，当发生的问题比较严重，并且没有已知的方法能绕过时，一般才使用方法3，方法3本身风险也是比较大的，因为修复bug的同时可能会产生新的bug。</p><h2 id="人为异常"><a href="#人为异常" class="headerlink" title="人为异常"></a>人为异常</h2><p>不管是软件本身，还是软件所运行的服务器，都是由人来管理的，但人是会犯错误的，有时候会执行错误的命令导致系统不能正常工作，其中比较致命的错误可能就是删掉某台服务器的数据了，在这种情况下为了能快速地恢复，通常也是采用Replication的思路，来避免问题。</p><h1 id="Scalability"><a href="#Scalability" class="headerlink" title="Scalability"></a>Scalability</h1><p>系统的工作负载通常不是一成不变的，当工作负载增加时，往往可以通过增加机器资源来保持性能不变，而需要增加机器数量的多少是由系统的扩展性来决定的，扩展性越好的系统，需要增加的机器资源越少。最完美的扩展性是线性扩展性，即工作负载扩大为原来N倍的时候，只需要加N倍的机器，就能够保持性能不变，最差的扩展性则是没有扩展性，即工作负载扩大为原来N倍时，即使加再多的机器，也无法保持性能和原来一样。</p><p>对于不同的系统，负载所代表的含义通常是不一样的，对于基础架构系统，通常每秒读和每秒写的次数，对于业务系统，通常有自己的指标，例如每秒交易创建的笔数。同样地，对于不同的系统，其使用的性能指标通常也是不相同的，对于批处理系统，通常强调的是吞吐量，即每秒完成的任务数量，而对于在线处理系统，通常强调的是响应时间。</p><p>在明确一个系统的工作负载指标和性能指标之后，我们才能讨论在该系统下如何实现扩展。扩展通常是两种思路，一是垂直扩展，即使用更好的机器替换现有的机器，二是水平扩展，即使用更多的机器。</p><p>对于垂直扩展，其优点是对业务是无影响的，缺点是更好的机器是很贵的，通常是一分钱一分货，而十分钱只能买到两分货，且现实中总有单机装不下的数据量，此时垂直扩展自然就无法实施了。</p><p>对于水平扩展，通常需要软件层面的配合，对于无状态的系统，通常只要在新加的机器上部署上需要扩展的系统，而对于有状态的系统，一般指的是存储系统，通常会将数据分成Partition（分区），这样新加的机器才能通过迁移Partition的方式，从老的机器上迁移数据以及对应的工作负载出来。水平扩展的优点是使用的都是相对廉价的服务器，能节约成本，但在软件层面需要做大量的工作，包括Partition的管理，迁移，负载均衡等等。</p><h1 id="Maintainability"><a href="#Maintainability" class="headerlink" title="Maintainability"></a>Maintainability</h1><p>可维护性的好坏决定了系统是否能够长久的发展，一个可维护性不好的系统，会给运维和开发人员带来很多不便。对于运维人员来讲，可维护性指的是系统是否支持常用的运维手段，良好的文档等等。而对于开发人员来讲，主要分为内核开发以及使用该系统的业务开发，对于业务开发，维护性指的是系统是否有良好的接口，方便业务使用，例如，Transaction就是底层系统提供给业务的一种接口，它保证了在一个事务中执行的语句具有ACID性质，从而业务只需要关注业务逻辑的开发，而不需要关心底层的具体实现；对于内核开发，维护性指的是系统的代码质量，主要包括代码的可阅读性和是否易于修改，主要和系统内核开发人员的代码设计能力相关。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>为了能达到较好的可靠性(Reliability)、可扩展性(Scalability)和可维护性(Maintainability)，分布式系统设计中通常会使用多副本(Replication)、数据分区(Partition)、一致性算法(Consensus)、事务(Transaction)等技术，理解它们要解决的问题，深入了解每种技术背后可能的实现方案，有助于评价某个系统的设计好坏，这对于多个竞品系统的选型和深入学习系统原理都是非常有必要的。</p><p>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li>Design Data Intensive Applications</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;在分布式系统中，经常会碰到的技术名词一般有Replication、Partition、Consensus、Transaction等等，这些技
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>MySQL online create index实现原理</title>
    <link href="http://yoursite.com/backend/mysql-online-create-index/"/>
    <id>http://yoursite.com/backend/mysql-online-create-index/</id>
    <published>2018-02-25T07:55:14.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>国内较多的互联网公司都是采用MySQL作为数据库系统，随着业务的发展，难免会碰到需要新建索引来优化某些SQL执行性能的情况。在MySQL实现online create index之前，新建索引意味着业务要停止写入，这是非常影响用户使用体验的，为此，MySQL引入了online create index，极大地减少了业务停写的时间，使得新建索引期间业务能够持续正常的工作。本文主要是对其实现原理的总结以及关键步骤的解释说明。</p><h1 id="MySQL-online-create-index原理"><a href="#MySQL-online-create-index原理" class="headerlink" title="MySQL online create index原理"></a>MySQL online create index原理</h1><p>在MySQL中表格至少需要设置一个主键，如果用户未指定主键的话，内部会自动生成一个。对于带主键的表格，MySQL会以聚集索引的方式实现，即表格的数据都是完整的存储在聚集索引上的。对于主键的变更，相当于对聚集索引进行变更，这个过程目前MySQL还是以停写的方式实现的，本文主要讨论的是新建二级索引的实现，为了方便描述，以一个例子来说明本文要讨论的场景。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create</span> <span class="keyword">table</span> t1(</span><br><span class="line">c1 <span class="type">int</span> <span class="keyword">primary</span> key,</span><br><span class="line">c2 <span class="type">int</span>,</span><br><span class="line">c3 <span class="type">int</span>,</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>刚开始业务中的SQL都是以主键c1来做查询的，后来随着业务的发展，可能出现了以c2做查询的SQL，此时，为了优化此类SQL的执行性能，需要在c2列上构建索引，即</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create</span> index index_c2 <span class="keyword">on</span> t1(c2);</span><br></pre></td></tr></table></figure><p>MySQL online create index主要分为两个阶段，第一阶段为从主表读取索引列并排序生成索引表的数据，称为基线数据；第二阶段为把新建索引阶段索引表的增量数据更新到第一阶段的基线数据上。具体来看，主要过程如下。</p><ol><li>用户执行create index</li><li>等待当前所有事务执行结束，但不影响新事务的开启；新开启的事务更新时会把新建索引的记录到增量数据，称为Row Log</li><li>开始构建索引，主要是从主表读出数据并排序</li><li>把新建索引表期间产生的增量数据更新到索引表中</li><li>构建的收尾工作</li></ol><p>接下来将略过不太重要的步骤1和步骤5，主要描述步骤2-4的详细实现。</p><h2 id="等事务结束"><a href="#等事务结束" class="headerlink" title="等事务结束"></a>等事务结束</h2><p>在执行create index语句之后，MySQL会先等待之前开启的事务先结束后，再真正开始索引的构建工作，这么做的原因是在执行<code>create index</code>之前开启的事务可能已经执行过某些更新SQL语句，这些SQL语句没有生成新建索引表的增量数据(Row Log)，如果不等待这部分事务结束，可能会出现基线数据中没有此部分数据，且Row Log中也没有此部分数据，最终该部分数据在索引表中不存在。</p><p>MySQL的等事务结束是通过MDL(Meta Data Lock)实现的，MDL会按序唤醒锁等待者，这样就能保证create index之前开启的事务一定执行完成了。</p><p>实际测试中，可以观察到当create index之前的事务一直没有结束时，create index语句会一直卡在<code>thd-&gt;mdl_context.upgrade_shared_lock</code>(sql_table.cc:7381)上。</p><h2 id="排序"><a href="#排序" class="headerlink" title="排序"></a>排序</h2><p>索引构建的第一阶段的工作是根据主表的数据，来构建索引表的数据。此过程总共有两个步骤，第一是读取主表中所需要的索引列数据；第二是将数据按照索引列排序。</p><p>其中读取主表数据和普通的全表扫描区别不大，而将数据按照索引列排序则是一个外部排序的过程。MySQL对外部排序实现较为简单，仅为最普通的单线程两路归并算法，优点是实现简单，占用内存资源少，缺点是性能较差。</p><h2 id="更新增量数据到构建好的索引表"><a href="#更新增量数据到构建好的索引表" class="headerlink" title="更新增量数据到构建好的索引表"></a>更新增量数据到构建好的索引表</h2><p>一般地，对于数据量较大的表格，构建索引的时间较长，通常是小时级别的，这期间往往会有新事务的提交，其中就可能包含对新建索引表的修改。因此，在索引基线数据构建好之后，还需要把构建期间的增量数据更新到索引表中，那么问题来了，在更新增量数据到索引表中会不断的有新事务修改数据，这样何时才能保证所有的修改都更新到索引表上呢？答案是加锁，粗暴一点的加锁方式是在整个增量数据更新到索引表期间停写，完成之后，再放开写入。但是，因为索引构建时间长，增量数据的数据量一般也较大，如果更新整个增量数据到索引表期间都停写的话，会较大地影响用户使用体验。因此，MySQL对加锁过程做了优化。</p><p>首先Row Log会被拆分为多个较小的Block，事务的更新会把数据写入到最后一个Block中，因此，普通的DML更新的时候会对最后一个Block加锁。同样的，在更新每个Block到索引表的时候，会先加锁，如果当前Block不是最后一个Block时，会把锁释放，如果是最后一个Block，则保持加锁状态，直到更新结束。因此，在更新Row Log到索引表期间，加锁的时间比较短，仅在最后一个Block更新到索引表时会持有锁一段时间。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>MySQL online create index的整体思路分为两步构建基线以及更新增量，构建基线时采用的归并算法比较简单，资源占用少，但性能会比较差；在更新增量时，采用将增量切分成更小的块，来减少停写的时间，是比较通用的方法。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://github.com/mysql/mysql-server/releases/tag/mysql-5.7.21">MySQL 5.7.21 source code</a></li><li><a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.html">MySQL online create index</a></li><li><a href="http://hedengcheng.com/?p=421">MySQL online add index</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;国内较多的互联网公司都是采用MySQL作为数据库系统，随着业务的发展，难免会碰到需要新建索引来优化某些SQL执行性能的情况。在MySQL实现
      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="数据库" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="MySQL" scheme="http://yoursite.com/tags/MySQL/"/>
    
      <category term="online create index" scheme="http://yoursite.com/tags/online-create-index/"/>
    
  </entry>
  
  <entry>
    <title>程序员需要知道的RAID基本原理</title>
    <link href="http://yoursite.com/backend/raid-principle/"/>
    <id>http://yoursite.com/backend/raid-principle/</id>
    <published>2017-02-18T12:23:10.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>RAID，全称为redundant array of independent disks，是目前商用服务器常见的磁盘管理技术。作为软件开发人员，需要了解各级RAID的特性，以便根据需求做出做合适的选择。本文总结了常见的RAID级别的特性，包括如下内容：</p><ul><li>RAID的基本作用</li><li>各级RAID的基本原理</li></ul><h1 id="RAID的基本作用"><a href="#RAID的基本作用" class="headerlink" title="RAID的基本作用"></a>RAID的基本作用</h1><p>RAID的一般有如下作用</p><ul><li>数据冗余</li><li>性能提升</li></ul><p>数据冗余是指把数据的校验信息存放在冗余的磁盘中，在某些磁盘数据损坏时，能从其他未损坏的磁盘中，重新构建数据。</p><p>性能提升是指RAID能把多块独立的磁盘组成磁盘阵列，通过把数据切成分片的方式，使得读/写数据能走多块磁盘，从而提升性能。</p><h1 id="各级RAID的基本原理"><a href="#各级RAID的基本原理" class="headerlink" title="各级RAID的基本原理"></a>各级RAID的基本原理</h1><p>根据RAID的冗余信息程度，切分数据的方式等不同，可以把RAID分成不同的级别，分别是</p><ul><li>RAID0</li><li>RAID1</li><li>RAID2</li><li>RAID3</li><li>RAID4</li><li>RAID5</li><li>RAID6</li></ul><p>接下来就讨论这些RAID级别的基本原理。</p><h2 id="RAID0的基本原理"><a href="#RAID0的基本原理" class="headerlink" title="RAID0的基本原理"></a>RAID0的基本原理</h2><p>RAID0设计的目标是为了提升读写性能，但并不带数据冗余信息。</p><p><img src="http://oserror.com/images/raid0.png" alt="RAID0"></p><p>如上图，RAID0会把数据切成块，分别存储在N个磁盘上。当读数据时，如果要读的数据块比较大，分布在多次磁盘上，那么能同时从多块盘读数据；当写数据时，如果要写的数据块比较大，分布式在多块磁盘上，那么同时能从多块盘写数据。</p><p>因为数据分布在多块盘上，当某块磁盘损坏时，整个RAID系统就不可用了。因此，N块盘的RAID0的特性如下：</p><ul><li>读性能最好情况下是单块盘的N倍</li><li>写性能最好情况下是单块盘的N倍</li><li>空间利用率为100%</li><li>不具有冗余信息，任何一块磁盘损坏，整个RAID不可用</li></ul><h2 id="RAID1的基本原理"><a href="#RAID1的基本原理" class="headerlink" title="RAID1的基本原理"></a>RAID1的基本原理</h2><p>RAID1的设计目标是为每份数据都提供一份或多份冗余数据，其结构如下：</p><p><img src="http://oserror.com/images/raid1.png" alt="RAID1"></p><p>如上图，RAID1中一个磁盘都有一个或多个冗余的镜像盘，所有磁盘的数据是一模一样的。RAID1读数据时，可以利用所有数据盘的带宽；写数据时，要同时写入数据盘和镜像盘，因此，需要等待最慢的磁盘写完成，写操作才完成，因此，写性能跟最慢的磁盘相当。N块盘的RAID1的特性如下：</p><ul><li>读性能最好情况下是原来的N倍</li><li>写性能跟最慢的磁盘相当</li><li>空间利用率1/N</li><li>N块盘，坏掉N-1块，RAID还能正常使用</li></ul><h2 id="RAID2的基本原理"><a href="#RAID2的基本原理" class="headerlink" title="RAID2的基本原理"></a>RAID2的基本原理</h2><p>RAID2的设计目标是在RAID0级别的基础上，加了海明纠错码。</p><p><img src="http://oserror.com/images/raid2.png" alt="RAID2"></p><p>如上图，前面四个盘是数据盘，后面三个盘是纠错码。RAID2读数据时，能同时使用多个数据盘的带宽；RAID2写数据时，除了写数据盘，还需要写校验盘，写性能会有下降。因此，N块盘的RAID2的特性如下：</p><ul><li>读性能不到原来的N倍，因为还有一部分是校验盘</li><li>写性能会有下降，因为每次都要写校验盘，受限于校验盘的数量</li><li>空间利用率小于100%，因为海明纠错码需要的冗余盘一般比数据盘的数量少</li><li>根据海明纠错码位数的不同，能容忍的坏盘数不同，具体信息可以参考<a href="https://en.wikipedia.org/wiki/Hamming_code">海明码</a></li></ul><h2 id="RAID3的基本原理"><a href="#RAID3的基本原理" class="headerlink" title="RAID3的基本原理"></a>RAID3的基本原理</h2><p>RAID3是把数据按照字节分别存在不同的磁盘中，并且最后一个磁盘提供纠错冗余，其结构如下：</p><p><img src="http://oserror.com/images/raid3.png" alt="RAID3"></p><p>如上图，由于按照字节切分数据，读数据时，一定会同时从多个盘读数据，可以利用所有数据盘的带宽；写数据时，也会利用所有磁盘的带宽，但所有的写校验数据都会在一个盘，因此，写性能主要受限于校验盘。N快盘的RAID3的特性如下：</p><ul><li>读性能是N-1倍，其中一块盘是校验盘</li><li>写性能受限于校验盘的写性能</li><li>空间利用率为(N-1)/N</li><li>坏掉一块盘，RAID还能正常工作</li></ul><h2 id="RAID4"><a href="#RAID4" class="headerlink" title="RAID4"></a>RAID4</h2><p>RAID4是把数据按照分块分别存在不同的磁盘中，并且最后一个磁盘提供纠错冗余，其结构如下：</p><p><img src="http://oserror.com/images/raid4.png" alt="RAID4"></p><p>如上图，读数据时，当数据分布在多块盘时，能够利用多块数据盘的带宽；写数据时，如果数据分布在多快盘时，能利用所有磁盘带宽，但写校验数据只能在一块盘上，因此，写性能主要受限于校验盘。N块盘的RAID4的特性如下：</p><ul><li>读性能是N-1倍，其中一块盘是校验盘</li><li>写性能受限于校验盘的写性能</li><li>空间利用率为(N-1)/N</li><li>坏掉一块盘，RAID还能正常工作</li></ul><h2 id="RAID5"><a href="#RAID5" class="headerlink" title="RAID5"></a>RAID5</h2><p>RAID5是把数据块按照分块分别存在不同的磁盘中，并且冗余信息也会分块分布在多块磁盘中，其结构如下：</p><p><img src="http://oserror.com/images/raid5.png" alt="RAID5"></p><p>如上图，读数据时，当数据分布在多块盘时，能够利用多块数据盘的带宽；写数据时，如果数据分布在多块盘时，能利用所有数据盘带宽，同时写校验数据也分散在多块盘上，但因为要额外写入校验数据，因此，写数据的性能略微有所下降。N块盘的RAID5的特性如下：</p><ul><li>读性能是N倍</li><li>写性能略微弱于RAID0</li><li>空间利用率为(N-1)/N</li><li>坏掉一块盘，RAID还能正常工作</li></ul><h2 id="RAID6"><a href="#RAID6" class="headerlink" title="RAID6"></a>RAID6</h2><p>RAID6是把数据块按照分块分别存在不同的磁盘中，并且冗余信息为两份奇偶校验码，分布在多块磁盘中，其结构如下：</p><p><img src="http://oserror.com/images/raid6.png" alt="RAID6"></p><p>如上图，读数据时，当数据分布在多块盘时，能够利用多块数据盘的带宽；写数据时，如果数据分布在多块盘时，能利用多块数据盘带宽，同时写校验数据也分散在多块盘中，但因为要额外写入两份校验数据，因此，写数据的性能要略微下降。N块盘的RAID6的特性如下：</p><ul><li>读性能是N倍</li><li>写性能略微弱于RAID0</li><li>空间利用率为(N-2)/N</li><li>坏掉两块盘，RAID还能正常工作</li></ul><h2 id="各级RAID的对比"><a href="#各级RAID的对比" class="headerlink" title="各级RAID的对比"></a>各级RAID的对比</h2><table><thead><tr><th>RAID级别</th><th>读性能</th><th>写性能</th><th>空间利用率</th><th>最大能容忍的坏盘数</th></tr></thead><tbody><tr><td>RAID0</td><td>单块盘的N倍</td><td>单块盘的N倍</td><td>100%</td><td>0</td></tr><tr><td>RAID1</td><td>单块盘的N倍</td><td>最慢磁盘的性能</td><td>1/N</td><td>N-1</td></tr><tr><td>RAID2</td><td>不到单块盘的N倍</td><td>单盘的写入速度 * 校验盘的数量</td><td>不到100%</td><td>取决于海明纠错码位数</td></tr><tr><td>RAID3</td><td>单块盘的N-1倍</td><td>校验盘的写入速度</td><td>(N-1)/N</td><td>1</td></tr><tr><td>RAID4</td><td>单块盘的N-1倍</td><td>校验盘的写入速度</td><td>(N-1)/N</td><td>1</td></tr><tr><td>RAID5</td><td>单块盘的N倍</td><td>略微弱于单块盘的N倍</td><td>(N-1)/N</td><td>1</td></tr><tr><td>RAID6</td><td>单块盘的N倍</td><td>略微弱于单块盘的N倍，差于RAID5</td><td>(N-2)/N</td><td>2</td></tr></tbody></table><p>一般地，RAID0容忍的坏盘数为0，风险太大，一般不常用；RAID1的信息冗余量很多，适合于对信息安全要求很高并且预算充足的场景；RAID2的控制器比较复杂，一般不常用；RAID3和RAID4由于其写入性能差，也不常用；RAID5由于读写性能、能容忍的坏盘数都比较均衡，因此，一般工业界经常使用的是RAID5；RAID6对于坏盘数容忍度较高，适合于对信息安全比较高的场景。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://zh.wikipedia.org/wiki/RAID">RAID</a></li><li><a href="https://en.wikipedia.org/wiki/Hamming_code">Hamming Code</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;RAID，全称为redundant array of independent disks，是目前商用服务器常见的磁盘管理技术。作为软件开发人
      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="RAID" scheme="http://yoursite.com/tags/RAID/"/>
    
      <category term="存储" scheme="http://yoursite.com/tags/%E5%AD%98%E5%82%A8/"/>
    
  </entry>
  
  <entry>
    <title>程序员需要知道的SSD基本原理</title>
    <link href="http://yoursite.com/backend/ssd-principle/"/>
    <id>http://yoursite.com/backend/ssd-principle/</id>
    <published>2017-02-12T02:22:14.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>SSD是目前商用服务器上非常流行的存储介质，因此，作为软件开发人员，需要了解的SSD基本原理，以便开发时能更好地发挥其优势，规避其劣势。本文总结了作为软件开发人员需要了解的SSD基本原理，全文组织结构如下：</p><ul><li>SSD的读写速度</li><li>SSD内部芯片的简单存取原理</li><li>SSD的读写特性</li><li>SSD的over-provisioning和garbage-collection</li><li>SSD的损耗均衡控制</li><li>SSD的写放大问题</li></ul><h1 id="SSD的读写速度"><a href="#SSD的读写速度" class="headerlink" title="SSD的读写速度"></a>SSD的读写速度</h1><p>首先，从软件开发人员作为SSD的用户角度来讲，首先需要了解的是SSD和普通HDD的性能对比，如下：</p><p>先来看顺序读和顺序写</p><p><img src="http://oserror.com/images/ssd_sequential_read.png" alt="Sequential Read"></p><p><img src="http://oserror.com/images/ssd_sequential_write.png" alt="Sequential Write"></p><p>其中，Seagate ST3000DM001是HDD，其他的都是SSD。从上述两图中可以看出，HDD的顺序读速度差不多为最慢的SSD的一半，顺序写稍微好点，但也比大部分慢一倍左右的速度。</p><p>再来看随机读和随机写</p><p><img src="http://oserror.com/images/ssd_random_read.png" alt="Random Read"></p><p><img src="http://oserror.com/images/ssd_random_write.png" alt="Random Write"></p><p>可以看出，HDD的随机读的性能是普通SSD的几十分之一，随机写性能更差。</p><p>因此，SSD的随机读和写性能要远远好于HDD，本文接下来的几个小节将会讨论为什么SSD的随机读写性能要远远高于HDD？</p><p>备注：本小节测试数据全部来自于<a href="http://www.pcgamer.com/hard-drive-vs-ssd-performance/2/">HDD VS SSD</a>。</p><h1 id="SSD内部芯片的简单存储原理"><a href="#SSD内部芯片的简单存储原理" class="headerlink" title="SSD内部芯片的简单存储原理"></a>SSD内部芯片的简单存储原理</h1><p>SSD内部一般使用NAND Flash来作为存储介质，其逻辑结构如下：</p><p><img src="http://oserror.com/images/ssd_nand_flash.png" alt="NAND Flash"></p><p>SSD中一般有多个NAND Flash，每个NAND Flash包含多个Block，每个Block包含多个Page。由于NAND的特性，其存取都必须以page为单位，即每次读写至少是一个page，通常地，每个page的大小为4k或者8k。另外，NAND还有一个特性是，其只能是读或写单个page，但不能覆盖写如某个page，必须先要清空里面的内容，再写入。由于清空内容的电压较高，必须是以block为单位。因此，没有空闲的page时，必须要找到没有有效内容的block，先擦写，然后再选择空闲的page写入。</p><p>在SSD中，一般会维护一个mapping table，维护逻辑地址到物理地址的映射。每次读写时，可以通过逻辑地址直接查表计算出物理地址，与传统的机械磁盘相比，省去了寻道时间和旋转时间。</p><h1 id="SSD读写特性"><a href="#SSD读写特性" class="headerlink" title="SSD读写特性"></a>SSD读写特性</h1><p>从NAND Flash的原理可以看出，其和HDD的主要区别为</p><ul><li>定位数据快：HDD需要经过寻道和旋转，才能定位到要读写的数据块，而SSD通过mapping table直接计算即可</li><li>读取速度块：HDD的速度取决于旋转速度，而SSD只需要加电压读取数据，一般而言，要快于HDD</li></ul><p>因此，在顺序读测试中，由于定位数据只需要一次，定位之后，则是大批量的读取数据的过程，此时，HDD和SSD的性能差距主要体现在读取速度上，HDD能到200M左右，而普通SSD是其两倍。</p><p>在随机读测试中，由于每次读都要先定位数据，然后再读取，HDD的定位数据的耗费时间很多，一般是几毫秒到十几毫秒，远远高于SSD的定位数据时间(一般0.1ms左右)，因此，随机读写测试主要体现在两者定位数据的速度上，此时，SSD的性能是要远远好于HDD的。</p><p>对于SSD的写操作，针对不同的情况，有不同的处理流程，主要是受到NAND Flash的如下特性限制</p><ul><li>NAND Flash每次写必须以page为单位，且只能写入空闲的page，不能覆盖写原先有内容的page</li><li>擦除数据时，由于电压较高，只能以block为单位擦除</li></ul><p>SSD的写分为新写入和更新两种，处理流程不同。</p><p>先看新写入的数据的流程，如下：</p><p><img src="http://oserror.com/images/ssd_new_write.png" alt="SSD New Write"></p><p>假设新写入了一个page，其流程如下：</p><ul><li>找到一个空闲page</li><li>把数据写入到空闲page中</li><li>更新mapping table</li></ul><p>而更新操作的流程如下：</p><p><img src="http://oserror.com/images/ssd_write_leave_idle.png" alt="SSD Leave Idle"></p><p>假设是更新了page G中的某些字节，流程如下：</p><ul><li>由于SSD不能覆盖写，因此，先找到一个空闲页H</li><li>读取page G中的数据到SSD内部的buffer中，把更新的字节更新到buffer</li><li>把buffer中的数据写入到H</li><li>更新mapping table中G页，置为无效页</li><li>更新mapping table中H页，添加映射关系</li></ul><p>可以看出，如果在更新操作比较多的情况下，会产生较多的无效页，类似于磁盘碎片，此时，需要SSD的over-provisioning和garbage-collection。</p><h1 id="SSD的over-provisioning和garbage-collection"><a href="#SSD的over-provisioning和garbage-collection" class="headerlink" title="SSD的over-provisioning和garbage-collection"></a>SSD的over-provisioning和garbage-collection</h1><p>over-provisioning是指SSD实际的存储空间比可写入的空间要大，比如，一块可用容量为120G的SSD，实际空间可能有128G。为什么需要over-provisioning呢？请看如下例子：</p><p><img src="http://oserror.com/images/ssd_over_provisioning.png" alt="SSD over-provisioning"></p><p>如上图所示，假设系统中就两个block，最终还剩下两个无效的page，此时，要写入一个新page，根据NAND原理，必须要先对两个无效的page擦除才能用于写入。此时，就需要用到SSD提供的额外空间，才能用garbage-collection方法整理出可用空间。</p><p><img src="http://oserror.com/images/ssd_garbage_collection.png" alt="garbage collection"></p><p>garbage collection的整理流程如上图所示</p><ul><li>首先，从over-provisoning的空间中，找到一个空闲的block</li><li>把Block0的ABCDEFH和Block1的A复制到空闲block</li><li>擦除Block 0</li><li>把Block1的BCDEFH复制到Block0，此时Block0就有两个空闲page了</li><li>擦除BLock1</li></ul><p>有空闲page之后，就可以按照正常的流程来写入了。</p><p>SSD的garbage-collection会带来两个问题：</p><ul><li>SSD的寿命减少，NAND Flash中每个原件都有擦写次数限制，超过一定擦写次数后，就只能读取不能写入了</li><li>写放大问题，即内部真正写入的数据量大于用户请求写入的数据量</li></ul><p>如果频繁的在某些block上做garbage-collection，会使得这些元件比其他部分更快到达擦写次数限制，因此，需要某个算法，能使得原件的擦写次数比较平均，这样才能延长SSD的寿命，这就需要下面要讨论的损耗均衡控制了。</p><h1 id="SSD损耗均衡控制"><a href="#SSD损耗均衡控制" class="headerlink" title="SSD损耗均衡控制"></a>SSD损耗均衡控制</h1><p>为了避免某些block被频繁的更新，而另外一些block非常的空闲，SSD控制器一般会记录各个block的写入次数，并且通过一定的算法，来达到每个block的写入都比较均衡。</p><p>以一个例子，说明损耗均衡控制的重要性：</p><p>假如一个NAND Flash总共有4096个block，每个block的擦写次数最大为10000。其中有3个文件，每个文件占用50个block，平均10分钟更新1个文件，假设没有均衡控制，那么只会3 * 50 + 50共200个block，则这个SSD的寿命如下：</p><p><img src="http://oserror.com/images/no_wear_leveling.png" alt="no wear leveling"></p><p>大约为278天。而如果是完美的损耗均衡控制，即4096个block都均衡地参与更新，则使用寿命如下：</p><p><img src="http://oserror.com/images/perfect_wear_leveling.png" alt="perfect wear leveling"></p><p>大约5689天。因此，设计一个好的损耗均衡控制算法是非常有必要的，主流的方法主要有两种：</p><ul><li>dynamic wear leveling</li><li>static wear leveling</li></ul><p>这里的dynamic和static是指的是数据的特性，如果数据频繁的更新，那么数据是dynamic的，如果数据写入后，不更新，那么是static的。</p><p>dynamic wear leveling的原理是记录每个block的擦写次数，每次写入数据时，找到被擦除次数最小的空block。</p><p>static wear leveling的原理分为两块：</p><ul><li>每次找到每擦除次数最小的可用block</li><li>当某个block的擦除次数小于阈值时，会将它与擦写次数较高的block的数据进行交换</li></ul><p>以一个例子来说明，两种擦写算法的不同点：</p><p>假如SSD中有25%的数据是dynamic的，另外75%的数据是static的。对于dynamic wear leveling方法，每次要找的是擦除了数据的block，而static的block里面是有数据的，因此，每次都只会在dynamic的block中，即最多会在25%的block中做均衡；对于static算法，每次找的block既可能是dynamic的，也可能是static的，因此，最多有可能在全部的block中做均衡。</p><p>相对而言，static算法能使得SSD的寿命更长，但也有其缺点：</p><ul><li>算法逻辑更复杂</li><li>在写入时，可能会移动数据，导致写放大，降低写入性能</li><li>更高的能耗</li></ul><h1 id="SSD写放大"><a href="#SSD写放大" class="headerlink" title="SSD写放大"></a>SSD写放大</h1><p>最后，我们分析一下SSD的写放大问题，一般由如下三个方面引起：</p><ul><li>SSD读写是以page为单位的，如果更新page中的部分数据，也需要写整个page</li><li>SSD的garbage collection中，会在block间移动数据</li><li>SSD的wear leveing中，可能也会在block间交换数据，导致写放大</li></ul><p>通常的，需要在其他方面和SSD的写放大之间做权衡，例如，可以减少garbage collection的频率来减少写放大问题；可以把SSD分成多个zone，每个zone使用不同的wear leveling方法等等。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>个人理解，使用SSD时，我们需要考虑如下情况：</p><ul><li>需要充分利用其随机读写快的特性</li><li>尽可能在软件层面更新小块数据，减轻SSD写放大问题</li><li>避免频繁的更新数据，减轻SSD写放大及寿命减少的问题，尽可能使用追加的方式写数据</li></ul><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://arstechnica.com/information-technology/2012/06/inside-the-ssd-revolution-how-solid-state-disks-really-work/5/">Solid-state revolution: in-depth on how SSDs really work</a></li><li><a href="http://www.pcgamer.com/hard-drive-vs-ssd-performance/2/">HDD VS SSD</a></li><li><a href="http://www.ssdfans.com/?p=131">SSD背后的秘密：SSD基本工作原理</a></li><li><a href="https://www.micron.com/~/media/.../nand-flash/tn2942_nand_wear_leveling.pdf">Wear Leveling Techniques</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;SSD是目前商用服务器上非常流行的存储介质，因此，作为软件开发人员，需要了解的SSD基本原理，以便开发时能更好地发挥其优势，规避其劣势。本文
      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="存储" scheme="http://yoursite.com/tags/%E5%AD%98%E5%82%A8/"/>
    
      <category term="SSD" scheme="http://yoursite.com/tags/SSD/"/>
    
  </entry>
  
  <entry>
    <title>事务隔离（二）：基于加锁方式的事务隔离原理</title>
    <link href="http://yoursite.com/backend/transaction-isolation-second/"/>
    <id>http://yoursite.com/backend/transaction-isolation-second/</id>
    <published>2017-02-12T02:20:39.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>前一篇文章中，介绍了ANSI SQL标准下的事务隔离级别及其扩展，这篇文章主要讨论了基于加锁的方式如何实现不同的事务隔离级别，全文的组织架构如下：</p><ul><li>ANSI SQL标准下的事务隔离级别及其扩展回顾</li><li>基于加锁方式的事务隔离原理</li></ul><h1 id="ANSI-SQL标准下的事务隔离级别及其扩展回顾"><a href="#ANSI-SQL标准下的事务隔离级别及其扩展回顾" class="headerlink" title="ANSI SQL标准下的事务隔离级别及其扩展回顾"></a>ANSI SQL标准下的事务隔离级别及其扩展回顾</h1><p>ANSI SQL标准下的事务隔离级别是基于禁止某些干扰现象而制定的，这些现象如下：</p><p>脏读P1</p><blockquote><p>W1(X)…R2(X)…A1…R2(X)</p></blockquote><p>不可重复读P2</p><blockquote><p>R1(X)…W2(X)…C2…R1(X)</p></blockquote><p>幻读P3</p><blockquote><p>R1(P)…W2(P)…C2…R1(P)</p></blockquote><p>针对三种现象，ANSI SQL标准设定了四种事务隔离级别，如下：</p><ol><li>Read Uncommitted：有可能发生P1，P2和P3</li><li>Read Committed：不可能发生P1，有可能发生P2和P3</li><li>Repeatable Read：不可能发生P1，P2，有可能发生P3</li><li>Serializable：不可能发生P1，P2和P3</li></ol><p>整个事务个隔离级别，与杜绝的现象的对应关系如下图：</p><p><img src="http://oserror.com/images/ansi_sql_isolation_levels.png" alt="ANSI SQL Isolation Levels"></p><p>由于ANSI SQL的标准存在以下限制：</p><ul><li>没有提及写操作的隔离性</li><li>ANSI SQL的标准比较老，对于采用多版本并发控制实现隔离性的级别不能够很好的描述</li></ul><p>即新干扰现象P0和P4，其中脏写P0如下</p><blockquote><p>W1(X)…W2(X)…A1</p></blockquote><p>写丢失P4如下</p><blockquote><p>R1(X)…R2(X)…W2(X)…C2…W1(X)…C1</p></blockquote><p>因此，引入了新的隔离级别，包括</p><ul><li>Cursor Stability</li><li>Snapshot</li></ul><p>具体的分析，请参照我的博文(<a href="http://oserror.com/backend/transaction-isolation-first/">事务隔离（一）：ANSI SQL事务隔离级别，限制及扩展</a>)。</p><h1 id="基于加锁方式的事务隔离原理"><a href="#基于加锁方式的事务隔离原理" class="headerlink" title="基于加锁方式的事务隔离原理"></a>基于加锁方式的事务隔离原理</h1><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p>锁有两种，即共享锁(Share Lock)和排他锁(Exclusive Lock)，对于不同事务加在同一个数据项上的锁，如果其中至少有一个是排他锁的话，那么事务是会冲突的，即其中一个事务必须等待。一般共享锁也称为读锁(Read Lock)，而排它锁也称为写锁(Write Lock)。</p><p>读写锁根据锁住的数据项不同，分为普通锁和谓词锁。谓词锁是指锁住满足某一查询条件的所有数据项，它不仅包括当前在数据库中满足条件的记录，也包括即将要插入，更新或删除到数据库并满足查询条件的数据项。对于不同事务加在查询条件下的谓词锁，在至少一个是写锁的情况，且两个谓词条件中包含重叠的数据项时，则两个事务是冲突的。</p><p>well-formed read(write)是指在read(write)一个数据项或者查询条件时，会先对数据项或者查询条件加read(write) lock。如果一个事务的所有读写都是well-formed，那称事务是well-formed。</p><p>two-phase read(write)是当有一个或多个read(write) lock被释放后，不能再加新的read(write) lock。如果一个事务在释放一个或多个锁后，不再加其他的锁，那么称该事务是two-phase的。</p><p>如果一把锁从加上，到事务结束(commit or abort)后才释放，则称此锁是long duration的，否则，称锁是short duration的。</p><p>多个并发执行的事务是serializability，指的是并发调度执行的结果等于这些事务某个串行执行的结果。例如，有三个并发执行的事务T1，T2和T3，如果其执行结果和其某个串行执行((T1,T2,T3),(T1,T3,T2),(T2,T1,T3),(T2,T3,T1),(T3,T1,T2),(T3,T2,T1))的结果相同。</p><p>如果一个事务T1持有一把锁的情况下，另一个事务T2申请一把冲突的锁，那么，事务T2只有等到事务T1释放这把锁之后，才能加上这把锁。</p><p>根据数据库的基础理论，采用well-formed two-phase locking方式调度事务的话，是能够保证serializability的。</p><p>在了解到锁的基本概念之后，接下来讨论，如何基于锁来实现各种不同的隔离级别。</p><h2 id="隔离级别如何实现"><a href="#隔离级别如何实现" class="headerlink" title="隔离级别如何实现"></a>隔离级别如何实现</h2><p>先来看所有的干扰现象：</p><p>脏写P0</p><blockquote><p>W1(X)…W2(X)…A1</p></blockquote><p>脏读P1</p><blockquote><p>W1(X)…R2(X)…A1…R2(X)</p></blockquote><p>不可重复读P2</p><blockquote><p>R1(X)…W2(X)…C2…R1(X)</p></blockquote><p>幻读P3</p><blockquote><p>R1(P)…W2(P)…C2…R1(P)</p></blockquote><p>写丢失P4</p><blockquote><p>R1(X)…R2(X)…W2(X)…C2…W1(X)…C1</p></blockquote><p>如果需要禁止P0，即禁止多个事务同时能修改一个数据项或谓词条件，则需要修改数据时，加写锁，并且是long duration的，此时，隔离级别满足Read Uncommitted。</p><p>如果需要禁止P1，即禁止读取到其他事务修改的中间状态的数据，在禁止P0的条件下，则需要，对读加锁，short duration的就能够满足条件，此时，隔离级别满足Read Committed。</p><p>如果需要禁止P4，即禁止事务读取并且修改某个数据项后，需要禁止其他事务再次修改，但如果只是读取的话，不影响，这里，需要一种特殊的锁，称为Cursor Lock，会对事务当前处理的行进行加锁，如果行记录被修改，那么锁会是long duration的，直到事务结束，如果，行未被修改，则锁会提前被释放，此时，隔离级别满足Curstor stability。</p><p>如果需要禁止P2，即要禁止到读到某个数据项后，该数据还可能被其他事务修改，因此，需要对读加锁，且一直加锁到事务结束，即long duration，此时，隔离级别满足Repeatable Read。</p><p>如果需要禁止P3，即要禁止读到某个谓词条件后，满足该谓词条件的数据还被其他事务修改，因此，需要对谓词条件加读锁，且是long duration的，此时，隔离级别满足SERIALIZABLE。</p><p>不同的加锁与事务隔离级别的对应关系如下：<br><img src="http://oserror.com/images/lock_isolation_levels.png" alt="Lock Isolation Levels"></p><p>表格中最后一项中，对于读写锁都是long duration的，即到事务结束才会释放锁，即事务过程中只有加锁阶段，没有解锁阶段，这种方式和普通的two-phase locking有什么区别呢？</p><h2 id="Two-phase-Locking"><a href="#Two-phase-Locking" class="headerlink" title="Two-phase Locking"></a>Two-phase Locking</h2><p>普通的two-phase locking包含两个阶段：</p><ul><li>Expanding phase：加锁阶段，此阶段只加锁，不释放锁</li><li>Shrinking phase：解锁阶段，此阶段只解锁，不加锁</li></ul><p>普通的two-phase locking可能会如下问题：</p><p>假设有两个事务T1，T2，它们的时序如下</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></pre></td><td class="code"><pre><span class="line">T1       T2</span><br><span class="line">R1(X)</span><br><span class="line">W1(X)</span><br><span class="line">         R2(X)</span><br><span class="line">         W2(X)</span><br><span class="line">A1</span><br></pre></td></tr></table></figure><p>由于事务T2读到的数据是事务T1修改的X，当事务T1回滚时，事务T2读到的数据就是脏数据，因此，需要对事务T2也进行回滚，如果存在T3也读到了T2修改的数据，那么T3也需要回滚，这样，会导致一系列的事务都需要回滚，称为Cascading Aborts。</p><p>而表格中的two-phase locking，只有加锁阶段，因此，不会存在上述问题。只有加锁阶段的two-phase locking，也称为strict two-phase locking。</p><p>由于two-phase locking采用的是加锁的方式，因此有可能会碰到经典的死锁问题，举个例子，如下：</p><p>假设有事务T1，T2，它们的加锁时序如下：</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></pre></td><td class="code"><pre><span class="line">T1       T2</span><br><span class="line">R1(X)   R2(Y)</span><br><span class="line">W1(Y)   W2(X)</span><br></pre></td></tr></table></figure><p>按照如上的时序，事务T1和T2处于等待互相释放锁的状态，即死锁。死锁问题会导致事务无法继续进行有效的工作，因此，必须要解决，常见的解决方案有：</p><ul><li>死锁检测并消除</li><li>锁等待一段时间阈值后，对事务进行回滚，并释放所有锁</li></ul><p>第一种方法，死锁检测并消除的方法是有一个单独的线程检测事务的锁等待图，如果图构成了一个环，那么，说明发生了死锁，此时，需要选择环中的一个事务进行回滚，并释放锁，使得其他事务能够继续运行下去。</p><p>第二种方法，锁等待一段时间阈值后，对事务进行回滚，并释放其所有锁，表明，每次发生死锁时，都会先回滚最早开始执行的事务，使得其他的事务能够继续运行下去。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf">A Critique of ANSI SQL Isolation Levels</a></li><li><a href="http://stackoverflow.com/questions/29722886/2pl-rigorous-vs-strict-model-is-there-any-benefit">2PL VS strict 2PL</a></li><li><a href="http://hssl.cs.jhu.edu/~randal/416/lectures/09.locking.pdf">Cascading Aborts jhu</a></li><li><a href="http://blog.163.com/li_hx/blog/static/183991413201610493233235?utm_source=tuicool&utm_medium=referral">Cascading Aborts blog</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;前一篇文章中，介绍了ANSI SQL标准下的事务隔离级别及其扩展，这篇文章主要讨论了基于加锁的方式如何实现不同的事务隔离级别，全文的组织架构
      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="数据库" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="事务隔离" scheme="http://yoursite.com/tags/%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB/"/>
    
  </entry>
  
  <entry>
    <title>事务隔离（一）：ANSI SQL事务隔离级别，限制及扩展</title>
    <link href="http://yoursite.com/backend/transaction-isolation-first/"/>
    <id>http://yoursite.com/backend/transaction-isolation-first/</id>
    <published>2016-12-10T07:52:31.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>一般的数据库教科书上都会介绍，事务有ACID四个特性，分别是atomicity, consistency, isolation和duriablity。本文主要讨论是事务的isolation特性，即隔离性。</p><p>谈到事务的隔离性，一般是指ANSI SQL标准下的四种隔离级别，即Read Uncommitted, Read Committed, Repeatable Read和Serialibility。但ANSI SQL的事务隔离级别的标准存在以下限制：</p><ul><li>没有提及写操作的隔离性</li><li>ANSI SQL的标准比较老，对于采用多版本并发控制实现隔离性的级别不能够很好的描述</li></ul><p>本文主要在分析ANSI SQL标准下的事务隔离级别之后，讨论其限制，以及扩展，全文的组织架构如下：</p><ul><li>ANSI SQL标准下的事务隔离级别</li><li>ANSI SQL标准的限制及其扩展</li></ul><p>本文收录在<a href="https://github.com/Charles0429/papers">papers项目</a>，papers项目旨在学习和总结分布式系统相关的论文。</p><h1 id="ANSI-SQL标准下的事务隔离级别"><a href="#ANSI-SQL标准下的事务隔离级别" class="headerlink" title="ANSI SQL标准下的事务隔离级别"></a>ANSI SQL标准下的事务隔离级别</h1><p>在数据库中，多个事务往往是并发执行的，事务之间可能会存在干扰，从而导致数据不正确的问题。为了保证事务之间执行不互相干扰，最简单的方案则是串行的执行一个个事务，但这会降低吞吐量。为此，ANSI SQL标准引入事务隔离级别，描述了并发事务的各种干扰级别，使得应用程序可以在吞吐量和正确性上做决策，不同的事务隔离级别保证不同程度的正确性，一般而言，事务隔离级别越低，吞吐量越高，正确性越低。</p><p>ANSI SQL的隔离级别主要是从解决应用程序出现的各种干扰现象中，而设计出来的，其隔离级别主要是为了解决以下三种现象：</p><ol><li>脏读 (P1)</li><li>不可重复读 (P2)</li><li>幻读 (P3)</li></ol><p>首先，来看脏读P1的发生的时序：</p><blockquote><p>事务T1写了数据X，事务T2读了数据X，事务T1回滚，此时，事务T2读取到的是脏数据</p></blockquote><p>其次，来看不可重复读P2发生的时序：</p><blockquote><p>事务T1读了数据X，事务T2写了数据X，事务T2提交，事务T1再次读数据X，两次读到的数据X不一样</p></blockquote><p>最后，来看幻读P3发生的时序</p><blockquote><p>事务T1读了满足X&gt;=m且X&lt;=n的数据，事务T2插入一条数据，满足条件X&gt;=m且x&lt;=n，事务T2提交，事务T1再次读满足X&gt;=m且X&lt;=n的数据，两次读到的数据不一样</p></blockquote><p>用稍微形式化的语言描述上述现象发生的时序，假设W1(X)表示事务T1修改了数据项X，而R2(X)，表示事务T2读了数据项X；W1(P)表示事务T1修改了满足谓词条件P的数据项，而R2(P)，表示事务T2读了满足谓词条件P的数据项。C1表示事务T1提交，A1表示事务T1回滚。</p><p>脏读P1</p><blockquote><p>W1(X)…R2(X)…A1…R2(X)</p></blockquote><p>不可重复读P2</p><blockquote><p>R1(X)…W2(X)…C2…R1(X)</p></blockquote><p>幻读P3</p><blockquote><p>R1(P)…W2(P)…C2…R1(P)</p></blockquote><p>针对三种现象，ANSI SQL标准设定了四种事务隔离级别，如下：</p><ol><li>Read Uncommitted：有可能发生P1，P2和P3</li><li>Read Committed：不可能发生P1，有可能发生P2和P3</li><li>Repeatable Read：不可能发生P1，P2，有可能发生P3</li><li>Serializable：不可能发生P1，P2和P3</li></ol><p>整个事务个隔离级别，与杜绝的现象的对应关系如下图：</p><p><img src="http://oserror.com/images/ansi_sql_isolation_levels.png" alt="ANSI SQL Isolation Levels"></p><p>值得一提的是，在Serializable下，不可能发生P1，P2和P3，但并不表明，不发生P1，P2和P3就一定是在Serializable。真正的在Serializable是指事务并发执行下得到的结果，与各个事务串行执行下的某个结果相同。</p><h1 id="ANSI-SQL标准的限制及其扩展"><a href="#ANSI-SQL标准的限制及其扩展" class="headerlink" title="ANSI SQL标准的限制及其扩展"></a>ANSI SQL标准的限制及其扩展</h1><p>如上文提到，ANSI SQL有如下限制：</p><ul><li>没有提及写操作的隔离性</li><li>ANSI SQL的标准比较老，对于采用多版本并发控制实现隔离性的级别不能够很好的描述</li></ul><p>对于第一点，没有提及写操作的隔离性，有如下现象</p><p>脏写P0</p><blockquote><p>事务T1修改X，事务T2修改X，事务T1回滚，此时不知回滚到什么值</p></blockquote><p>举个例子说明，假设事务T1修改X前，X=100，事务T1要把修改成10，事务T2要把X修改成20，如下：</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></pre></td><td class="code"><pre><span class="line">X=100</span><br><span class="line">W1X(10)</span><br><span class="line">W2X(20)</span><br><span class="line">A1</span><br></pre></td></tr></table></figure><p>事务T1回滚时，如果选择回滚到X=20，那么如果事务T2再回滚时，无法回滚到最原先的100；如果回滚到X=100，那么事务T2提交时，就无法知道写入的是X=20，在这种场景下，是无解的。</p><p>因此，对于脏写现象一定是要杜绝的，否则，无法保证事务能够正确的回滚。所以，对于所有的ANSI SQL标准下的隔离级别，需要增强到都满足P0不发生的级别。</p><p>对于写操作，还存在写丢失问题，发生在如下场景</p><p>写丢失P4</p><blockquote><p>事务T1读数据X，事务T2读数据X，事务T2修改数据X，事务T1修改数据X，最终写入的数据是脏数据</p></blockquote><p>举个例子，假如原先X=100，事务T1对X加5，事务T2对X减1，预期的结果应该是100+5-1=104，而如果在写丢失的情况下，则如下</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">T1         T2</span><br><span class="line">R(X=100)</span><br><span class="line">         R(X=100)</span><br><span class="line">         W(X=99)</span><br><span class="line">W(X=104)</span><br></pre></td></tr></table></figure><p>如上场景，最终写入到数据库时，X=104，这属于数据不一致，是应该杜绝的。</p><p>写丢失在现象在ANSI SQL的Read Committed级别可能发生，但是Repeatable Read级别不可能发生。为此，引入Cursor Stability隔离级别，保证不会发生P4。但是，P4不会在Repeatable级别发生，因为P2禁止了事务T1读X后，事务T2写X的场景。因此，Cursor Stability的隔离级别处于Read Committed和Repeatable Read之间。</p><p>现有的工业界产品，如MySQL等，大多都会采用MVCC等多版本并发控制来达到某种隔离性，而在ANSI SQL标准中，是没有考虑多版本的，因此，有必要讨论多版本并发控制下的隔离级别与现有的ANSI SQL标准下的隔离级别的不同。</p><p>先简单的介绍下一种多版本并发控制的思路，即Basic Time Ordering，其流程如下：</p><ol><li>事务T1开始时，先申请一个时间戳，记做Start-Timestamp</li><li>事务T1的读不会阻塞，因为，它会读其Start-Timestamp之前的版本，其他事务在Start-Timestamp之后的修改，对该事务是不可见的</li><li>事务T1本身的修改，包括更新，插入和删除，都会保存在事务的上下文中，方便事务本身重复读取修改过的数据</li><li>当事务T1要提交时，它会获取一个Commit-Timestamp，此Commit-Timestamp要保证比现有的所有其他事务的Start-Timestmap和Commit-Timestmap要大，如果存在其他事务Commit-Timestamp在事务T1的[Start-Timestamp, Commit-Timestamp]之内，且该事务修改了事务T1修改的数据，那么，事务T1会被终止，否则，事务提交</li></ol><p>可以看出，步骤4中保证了P4写丢失现象不会在Basic Time Ordering中发生，把Basic Time Ordering达到的隔离级别称为Snapshot级别。由于会读取Start-TimeStamp之前提交的记录，因此，Snapshot肯定是满足Read Committed。</p><p>接下来，要讨论的是Snapshot和Repeatable Read之间的关系：</p><p>Snapshot读取的是某个时间点前的快照，因此，也不会出现不可重复读的现象，所以，从这个角度来说Snapshot的隔离级别要大于Repeatable Read，但考虑以下场景：</p><blockquote><p>R1(X)…R2(Y)…W1(Y)…W2(X)…C1…C2</p></blockquote><p>这种场景在Repeatable Read级别是被禁止的，因为T2读了数据Y之后，T1修改Y并提交了，会导致不可重复读。而对于Snapshot是可能发生这种场景的，以X+Y要满足条件大于0为例，说明Snapshot下可能违反此约束，如下</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></pre></td><td class="code"><pre><span class="line">两个事务开始之前X=1,Y=2，时间戳为10000</span><br><span class="line">事务T1开始，申请时间戳为10001，R1(X)=1</span><br><span class="line">事务T2开始，申请时间戳为10003，R2(Y)=2</span><br><span class="line">事务T1，W1(Y)=0</span><br><span class="line">事务T2，W2(X)=0</span><br><span class="line">事务T1提交，发现X+Y=1+0，满足约束</span><br><span class="line">事务T2提交，发现X+Y=0+2，满足约束</span><br></pre></td></tr></table></figure><p>虽然事务T1和事务T2都认为满足约束，但是两个事务都执行完成后，约束不满足。</p><p>因此，在这个现象上，Snapshot的隔离级别要小于Repeatable Read。故，Snapshot的隔离级别既不大于Repeatable Read，也不小于Repeatable Read。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>事务隔离级别是比较有意思的话题，现阶段也有一些技术来实现各种隔离级别，接下来的一些文章会讨论实现事务隔离级别的技术，以及工业界产品中使用的技术，如下：</p><ul><li>Two Phase Locking</li><li>Basic Time Ordering</li><li>Multi-version Concurrentcy Control</li><li>Optimistic Concurrency Control</li><li>MySQL，Oracle，Spanner等产品是使用何种技术来做事务隔离的</li></ul><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf">A Critique of ANSI SQL Isolation Levels</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;一般的数据库教科书上都会介绍，事务有ACID四个特性，分别是atomicity, consistency, isolation和duriab
      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="数据库" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
      <category term="事务隔离级别" scheme="http://yoursite.com/tags/%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB/"/>
    
  </entry>
  
  <entry>
    <title>NewSQL究竟新在哪里？</title>
    <link href="http://yoursite.com/distributed/newsql/"/>
    <id>http://yoursite.com/distributed/newsql/</id>
    <published>2016-12-10T03:38:32.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<p>近几年来，数据库领域出现了一种新的关系数据库类型，称为NewSQL，例如，Google的Spanner，Amazon的Aurora等等，这些数据库相对于传统数据库来讲，区别在哪里？<a href="http://db.cs.cmu.edu/papers/2016/pavlo-newsql-sigmodrec2016.pdf">What’s Really New with NewSQL?</a>给了很好的总结，本篇文章主要是总结该论文的观点，最后会有一个简单的讨论部分，全文的组织结构如下：</p><ul><li>为什么需要NewSQL？</li><li>NewSQL的分类</li><li>NewSQL的技术挑战有哪些？</li><li>讨论</li></ul><p>本文收录在我的github中<a href="https://github.com/Charles0429/papers">papers项目</a>，papers项目旨在学习和总结分布式系统相关的论文。</p><h1 id="为什么需要NewSQL？"><a href="#为什么需要NewSQL？" class="headerlink" title="为什么需要NewSQL？"></a>为什么需要NewSQL？</h1><p>数据库的发展通常是随着业务需求的变化，在2000年左右，随着互联网的兴起，有许多同时在线的用户，这对数据库领域带来了非常大的挑战，数据库通常会成为瓶颈，所以，此时业务针对数据库的需求，主要体现在可扩展上面。</p><p>这时期数据库的扩展性，往往采用如下两种方案：</p><ol><li>垂直扩展：使用更好的硬件，来做数据库的服务器</li><li>水平扩展：采用中间件，做sharding的方式，即分库分表的方式</li></ol><p>垂直扩展中使用更好的硬件意味者成本高，并且更换硬件后，需要把数据从老的机器迁移到新的机器，中间可能需要停服务，因此往往采用水平扩展，例如，Google’s MySQL-based cluster。</p><p>采用中间件方式也有缺点，中间件一般要求轻量级，简单数据库操作可以搞定，但是，如果需要做分布式事务或者联表操作，会非常复杂，通常这些逻辑会放到应用层来做。</p><p>后续，NOSQL兴起，主要有几个原因：</p><ol><li>传统关系数据库更倾向于一致性，而在性能和可用性比较差</li><li>全功能的关系型数据库太重</li><li>关系模型对于简单的查询太重，不必要</li></ol><p>NOSQL以Google’s BigTable 和 Amazon’s Dynamo为代表，开源版对应为HBase和Cassandra。</p><p>NOSQL往往是不保证强一致性的，而对于一些应用来讲（例如金融服务），是需要强一致性和事务的，因此，如果它们基于NOSQL系统来开发的话，应用层需要些大量的逻辑来处理一致性和事务相关的问题。此时，业务需求是拥有可扩展性的基础上，能够支持强一致性。</p><p>因此，这里有几条路：</p><ol><li>性能更好的单个服务器来做数据库服务器</li><li>中间件层支持分布式事务</li></ol><p>使用更好的单个服务器的话，不满足业务需求的可扩展性。</p><p>使用中间件的话，会有如下问题，例如：</p><ol><li>中间件层往往是比较轻量级的，要实现一致性，必须在中间件层实现分布式事务，这点是非常困难的</li><li>中间件层本身的高可用很难保证</li></ol><p>上面两条路都不能很好的满足应用的需求，因此，NewSQL出现了。</p><p>首先来看NEWSQL的定义：针对OLTP的读写，提供与NOSQL相同的可扩展性和性能，同时能支持满足ACID特性的事务。即保持NOSQL的高可扩展和高性能，并且保持关系模型。</p><p>NEWSQL的优点：</p><ol><li>轻松的获得可扩展性</li><li>能够使用关系模型和事务，应用逻辑会简化很多</li></ol><p>注意，此篇论文中的NEWSQL偏向于OLTP型数据库，和一些OLAP类型的数据库不同，OLAP数据库更偏向于复杂的只读查询，查询时间往往很长。</p><p>而NEWSQL数据库的特性如下，针对其读写事务：</p><ol><li>执行时间短</li><li>一般只查询一小部分数据，通过使用索引来达到高效查询的目的</li><li>一般执行相同的命令，使用不同的输入参数</li></ol><h1 id="NewSQL的分类"><a href="#NewSQL的分类" class="headerlink" title="NewSQL的分类"></a>NewSQL的分类</h1><p>分三大类：</p><ol><li>从头开始，使用新架构的系统</li><li>中间件</li><li>DAAS，数据库即服务</li></ol><h2 id="New-Architectures"><a href="#New-Architectures" class="headerlink" title="New Architectures"></a>New Architectures</h2><p>采用新架构的NewSQL有如下特点：</p><ol><li>无共享存储</li><li>多节点的并发控制</li><li>基于多副本做高可用和容灾</li><li>流量控制</li><li>分布式查询处理</li></ol><p>优势：</p><ol><li>所有的部分都可以为分布式环境做优化，例如查询优化，通信协议优化。例如，所有的NEWSQL DBMS可以直接在节点间发送查询，而不是通过中心节点，例如中间件系统</li><li>本身负责数据分区，因此，可以把查询发送给有数据的分区，而不是把数据发送给查询。</li><li>拥有自身的存储，可以指定更复杂的多副本的方式</li></ol><p>缺点：</p><ol><li>懂该数据库的人少，缺少专业的运维</li></ol><p>代表产品：Spanner，CockroachDB</p><h2 id="Transparent-Sharding-Middleware"><a href="#Transparent-Sharding-Middleware" class="headerlink" title="Transparent Sharding Middleware"></a>Transparent Sharding Middleware</h2><p>中间件负责的事情如下：</p><ol><li>对查询请求做路由</li><li>分布式事务的协调者</li><li>数据分布，数据多副本控制，数据分区</li></ol><p>往往在各个数据库节点，需要装代理与中间件沟通，负责如下事情：</p><ol><li>在本地节点执行中间节点发来的情况，并且返回结果</li></ol><p>优点：</p><ol><li>应用通常不需要做变化</li></ol><p>缺点：</p><ol><li>各个节点还是运行传统数据库，即以磁盘为核心的数据库，对现有的大内存，多核服务器难以高效地利用</li><li>重复的查询计划和查询优化，在中间件做一次，在各个DBMS做一次</li></ol><p>备注：有研究表明，以磁盘为主要存储的传统DBMS，很难有效地利用非常多的核，以及更大的内存容量。</p><p>代表产品: MariaDB MaxScale, ScaleArc</p><h2 id="Database-as-a-Service"><a href="#Database-as-a-Service" class="headerlink" title="Database-as-a-Service"></a>Database-as-a-Service</h2><p>特点：</p><ol><li>用户可以按需使用</li><li>数据库本身可能使用云产品，例如云存储等，可以较容易的实现可扩展性</li></ol><p>代表产品：</p><ol><li>Amazon Aurora</li><li>ClearDB</li></ol><h1 id="NewSQL的技术挑战有哪些？"><a href="#NewSQL的技术挑战有哪些？" class="headerlink" title="NewSQL的技术挑战有哪些？"></a>NewSQL的技术挑战有哪些？</h1><h2 id="Main-Memory-Storage"><a href="#Main-Memory-Storage" class="headerlink" title="Main Memory Storage"></a>Main Memory Storage</h2><p>传统数据库都是以磁盘为存储中心的架构，读盘操作相对较慢，一般是内存中缓存页。</p><p>现在来讲，内存较便宜，容量大，能存储大量的数据。这些纯内存操作带来的好处是，读取和写入数据速度较快。</p><p>现有的大内存服务器，对数据库对内存的管理提出了新的要求，不再是像传统数据库那样，只是用来做页缓存，可以采用更高效地内存管理方式。</p><h2 id="Partitioning-Sharding"><a href="#Partitioning-Sharding" class="headerlink" title="Partitioning/Sharding"></a>Partitioning/Sharding</h2><p>数据分区一般以某几列做hash或者range分区。</p><p>特点：</p><ul><li>数据库需要能在多个分区执行SQL，并且合并数据结果的功能。</li><li>把同一个用户的数据可以放在一起，即使是不同数据表的数据，可以减少通信开销。</li><li>可以在线的添加或者删除机器。</li><li>可以在线的迁移或复制分区。</li></ul><h2 id="Concurrency-Control"><a href="#Concurrency-Control" class="headerlink" title="Concurrency Control"></a>Concurrency Control</h2><p>数据库通过Concurrency Control来提供ACID中的Atomicity和Isolation。</p><h3 id="Atomicity"><a href="#Atomicity" class="headerlink" title="Atomicity"></a>Atomicity</h3><p>分布式场景下，一般采用类2PC的协议，根据事务是否需要中心节点，分为以下两类：</p><ol><li>中心节点：单点，容量限制</li><li>非中心节点：需要时钟的同步</li></ol><p>关于时钟同步，不同数据库也有不同的做法，Spanner和CroachDB在时钟同步上的不同选择：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">But what makes Spanner differ- ent is that it uses hardware devices (e.g., GPS, atomic clocks) for high-precision clock synchronization. The DBMS uses these clocks to assign timestamps to transactions to enforce consistent views of its multi-version database over wide-area networks. CockroachDB also purports to provide the same kind of consistency for transactions across data centers as Span- ner but without the use of atomic clocks. They instead rely on a hybrid clock protocol that combines loosely synchronized hardware clocks and logical counters [41].</span><br></pre></td></tr></table></figure><h3 id="Isolation"><a href="#Isolation" class="headerlink" title="Isolation"></a>Isolation</h3><p>现有实现Isolation的技术主要包括：</p><ul><li>2PL：two phase locking</li><li>MVCC: Multiversion Concurrency Control</li><li>OCC: Optimistic Concurrency Control</li></ul><p>大部分的数据库还是在选择使用MVCC，例如CockroachDB；有些数据库使用2PL+MVCC，修改数据的时候，还是采用2PL，例如，InnoDB，Spanner</p><h2 id="Secondary-Indexes"><a href="#Secondary-Indexes" class="headerlink" title="Secondary Indexes"></a>Secondary Indexes</h2><p>一般有两种实现方式：局部索引VS全局索引</p><p>局部索引：</p><ol><li>每个partition有一部分索引数据，每次修改索引，只需要修改一个节点，但查找数据需要可能涉及多个节点</li></ol><p>全局索引：</p><ol><li>每个partition都有完整的索引数据，每次修改索引，都需要使用分布式事务，修改所有包含此索引副本的节点，查找数据只需要在一个节点</li></ol><h2 id="Replication"><a href="#Replication" class="headerlink" title="Replication"></a>Replication</h2><p>两个需要考虑的点：</p><ol><li>如何保证一致性：Paxos和2PC（跨Partition）</li><li>同步的方式：采用同步执行命令的方式，还是同步状态的方式</li></ol><h2 id="Crash-Recovery"><a href="#Crash-Recovery" class="headerlink" title="Crash Recovery"></a>Crash Recovery</h2><p>如何最小化宕机时间？</p><p>采用主备切换</p><p>如何优化新加机器恢复到同步的时间？</p><p>一般手段为做checkpoint</p><h1 id="讨论"><a href="#讨论" class="headerlink" title="讨论"></a>讨论</h1><p>可扩展性是NewSQL的一个非常重要的特点，对于中间件的方式，其上需要存路由信息，其本身的可扩展性比较难以解决，个人认为，其不应该算入NewSQL。</p><p>NewSQL的技术挑战除了上述提到的之外，还有如何实现多租户架构及租户之间的隔离，负载均衡等等问题。</p><p>从整篇论文中描述的内容可以看出，NewSQL中并没有开拓性的理论技术的创新，更多的是架构的创新，以及把现有的技术如何更好地适用于当今的服务器，适用于当前的分布式架构，使得这些技术有机的结合起来，形成高效率的整体，实现NewSQL高可用，可扩展，强一致性等需求。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="http://db.cs.cmu.edu/papers/2016/pavlo-newsql-sigmodrec2016.pdf">What’s Really New with NewSQL?</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;近几年来，数据库领域出现了一种新的关系数据库类型，称为NewSQL，例如，Google的Spanner，Amazon的Aurora等等，这些数据库相对于传统数据库来讲，区别在哪里？&lt;a href=&quot;http://db.cs.cmu.edu/papers/2016/pavlo
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="数据库" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
  </entry>
  
  <entry>
    <title>BloomFilter原理，实现及优化</title>
    <link href="http://yoursite.com/backend/bloomfilter/"/>
    <id>http://yoursite.com/backend/bloomfilter/</id>
    <published>2016-12-03T02:19:48.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>最近在做性能优化相关的事情，其中涉及到了BloomFilter，于是对BloomFilter总结了下，本文组织结构如下：</p><ul><li>BloomFilter的使用场景</li><li>BloomFilter的原理</li><li>BloomFilter的实现及优化</li></ul><h1 id="BloomFilter的使用场景"><a href="#BloomFilter的使用场景" class="headerlink" title="BloomFilter的使用场景"></a>BloomFilter的使用场景</h1><p>首先，简单来看下BloomFilter是做什么的？</p><blockquote><p>A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, that is used to test whether an element is a member of a set. False positive matches are possible, but false negatives are not, thus a Bloom filter has a 100% recall rate. In other words, a query returns either “possibly in set” or “definitely not in set”.</p></blockquote><p>上述描述引自维基百科，特点总结为如下：</p><ul><li>空间效率高的概率型数据结构，用来检查一个元素是否在一个集合中</li><li>对于一个元素检测是否存在的调用，BloomFilter会告诉调用者两个结果之一：可能存在或者一定不存在</li></ul><p>其次，为什么需要BloomFilter？</p><p>常用的数据结构，如hashmap，set，bit array都能用来测试一个元素是否存在于一个集合中，相对于这些数据结构，BloomFilter有什么方面的优势呢？</p><ul><li>对于hashmap，其本质上是一个指针数组，一个指针的开销是sizeof(void *)，在64bit的系统上是64个bit，如果采用开链法处理冲突的话，又需要额外的指针开销，而对于BloomFilter来讲，返回可能存在的情况中，如果允许有1%的错误率的话，每个元素大约需要10bit的存储空间，整个存储空间的开销大约是hashmap的15%左右（数据来自维基百科）</li><li>对于set，如果采用hashmap方式实现，情况同上；如果采用平衡树方式实现，一个节点需要一个指针存储数据的位置，两个指针指向其子节点，因此开销相对于hashmap来讲是更多的</li><li>对于bit array，对于某个元素是否存在，先对元素做hash，取模定位到具体的bit，如果该bit为1，则返回元素存在，如果该bit为0，则返回此元素不存在。可以看出，在返回元素存在的时候，也是会有误判的，如果要获得和BloomFilter相同的误判率，则需要比BloomFilter更大的存储空间</li></ul><p>当然，BloomFilter也有它的劣势，如下：</p><ul><li>相对于hashmap和set，BloomFilter在返回元素可能存在的情况中，有一定的误判率，这时候，调用者在误判的时候，会做一些不必要的工作，而对于hashmap和set，不会存在误判情况</li><li>对于bit array，BloomFilter在插入和查找元素是否存在时，需要做多次hash，而bit array只需要做一次hash，实际上，bit array可以看做是BloomFilter的一种特殊情况</li></ul><p>最后，以一个例子具体描述使用BloomFilter的场景，以及在此场景下，BloomFilter的优势和劣势。</p><p>一组元素存在于磁盘中，数据量特别大，应用程序希望在元素不存在的时候尽量不读磁盘，此时，可以在内存中构建这些磁盘数据的BloomFilter，对于一次读数据的情况，分为以下几种情况：</p><ol><li>请求的元素不在磁盘中，如果BloomFilter返回不存在，那么应用不需要走读盘逻辑，假设此概率为P1；如果BloomFilter返回可能存在，那么属于误判情况，假设此概率为P2</li><li>请求的元素在磁盘中，BloomFilter返回存在，假设此概率为P3</li></ol><p>如果使用hashmap或者set的数据结构，情况如下：</p><ol><li>请求的数据不在磁盘中，应用不走读盘逻辑，此概率为P1+P2</li><li>请求的元素在磁盘中，应用走读盘逻辑，此概率为P3</li></ol><p>假设应用不读盘逻辑的开销为C1，走读盘逻辑的开销为C2，那么，BloomFilter和hashmap的开销为</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">Cost(BloomFilter) = P1 * C1 + (P2 + P3) * C2</span><br><span class="line">Cost(HashMap) = (P1 + P2) * C1 + P3 * C2;</span><br><span class="line"></span><br><span class="line">Delta = Cost(BloomFilter) - Cost(HashMap)</span><br><span class="line">      = P2 * (C2 - C1)</span><br></pre></td></tr></table></figure><p>因此，BloomFilter相当于以增加<code>P2 * (C2 - C1)</code>的时间开销，来获得相对于hashmap而言更少的空间开销。</p><p>既然P2是影响BloomFilter性能开销的主要因素，那么BloomFilter设计时如何降低概率P2（即false positive probability）呢？，接下来的BloomFilter的原理将回答这个问题。</p><h1 id="BloomFilter的原理"><a href="#BloomFilter的原理" class="headerlink" title="BloomFilter的原理"></a>BloomFilter的原理</h1><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p>BloomFilter通常采用bit array实现，假设其bit总数为m，初始化时m个bit都被置成0。</p><p>BloomFilter中插入一个元素，会使用k个hash函数，来计算出k个在bit array中的位置，然后，将bit array中这些位置的bit都置为1。</p><p>以一个例子，来说明添加的过程，这里，假设m=19，k=2，如下：</p><p><img src="http://oserror.com/images/bloom_filter_insert.png" alt="bloomfilter insert"></p><p>如上图，插入了两个元素，X和Y，X的两次hash取模后的值分别为4,9，因此，4和9位被置成1；Y的两次hash取模后的值分别为14和19，因此，14和19位被置成1。</p><p>BloomFilter中查找一个元素，会使用和插入过程中相同的k个hash函数，取模后，取出每个bit对应的值，如果所有bit都为1，则返回元素可能存在，否则，返回元素不存在。</p><p>为什么bit全部为1时，是表示元素可能存在呢？</p><p>还是以上图的例子说明，如果要查找的元素是X，k个hash函数计算后，取出的bit都是1，此时，X本身也是存在的；假如，要查找另一个元素Z，其hash计算出来的位置为9,14，此时，BloomFilter认为此元素存在，但是，Z实际上是不存在的，此现象称为false positive。</p><p>最后，BloomFilter中不允许有删除操作，因为删除后，可能会造成原来存在的元素返回不存在，这个是不允许的，还是以一个例子说明：</p><p><img src="http://oserror.com/images/bloom_filter_delete.png" alt="bloomfilter delete"></p><p>上图中，刚开始时，有元素X，Y和Z，其hash的bit如图中所示，当删除X后，会把bit 4和9置成0，这同时会造成查询Z时，报不存在的问题，这对于BloomFilter来讲是不能容忍的，因为它要么返回绝对不存在，要么返回可能存在。</p><p>放到之前的磁盘读数据的例子来讲，如果删除了元素X，导致应用读取Z时也会返回记录不存在，这是不符合预期的。</p><p>BloomFilter中不允许删除的机制会导致其中的无效元素可能会越来越多，即实际已经在磁盘删除中的元素，但在bloomfilter中还认为可能存在，这会造成越来越多的false positive，在实际使用中，一般会废弃原来的BloomFilter，重新构建一个新的BloomFilter。</p><h2 id="参数如何取值"><a href="#参数如何取值" class="headerlink" title="参数如何取值"></a>参数如何取值</h2><p>在实际使用BloomFilter时，一般会关注false positive probability，因为这和额外开销相关。实际的使用中，期望能给定一个false positive probability和将要插入的元素数量，能计算出分配多少的存储空间较合适。</p><p>假设BloomFilter中元素总bit数量为m，插入的元素个数为n，hash函数的个数为k，false positive probability记做p，它们之间有如下关系（具体推导过程请参考维基百科）：</p><p>如果需要最小化false positive probability，则k的取值如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">k = m * ln2 / n;  公式一</span><br></pre></td></tr></table></figure><p>而p的取值，和m，n又有如下关系</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">m = - n * lnp / (ln2) ^ 2 公式二</span><br></pre></td></tr></table></figure><p>把公式一代入公式二，得出给定n和p，k的取值应该为</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">k = -lnp / ln2</span><br></pre></td></tr></table></figure><p>最后，也同样可以计算出m。</p><h1 id="BloomFilter实现及优化"><a href="#BloomFilter实现及优化" class="headerlink" title="BloomFilter实现及优化"></a>BloomFilter实现及优化</h1><h2 id="基本版实现"><a href="#基本版实现" class="headerlink" title="基本版实现"></a>基本版实现</h2><p>基础的数据结构如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">BloomFilter</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="built_in">BloomFilter</span>(<span class="keyword">const</span> <span class="keyword">int32_t</span> n, <span class="keyword">const</span> <span class="keyword">double</span> false_positive_p);</span><br><span class="line">  <span class="function"><span class="keyword">void</span> <span class="title">insert</span><span class="params">(<span class="keyword">const</span> T &amp;key)</span></span>;</span><br><span class="line">  <span class="function"><span class="keyword">bool</span> <span class="title">key_may_match</span><span class="params">(<span class="keyword">const</span> T &amp;key)</span></span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">  std::vector&lt;<span class="keyword">char</span>&gt; bits_;</span><br><span class="line">  <span class="keyword">int32_t</span> k_; </span><br><span class="line">  <span class="keyword">int32_t</span> m_; </span><br><span class="line">  <span class="keyword">int32_t</span> n_; </span><br><span class="line">  <span class="keyword">double</span> p_; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>其中bits_是用vector<char>模拟的bit array，其他对应于BloomFilter原理一节所说的几个参数。</p><p>整个BloomFilter包含三个操作：</p><ul><li>初始化：即上述代码中的构造函数</li><li>插入：即上述代码中的insert</li><li>判断是否存在：即上述代码中的key_may_match</li></ul><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>根据BloomFilter原理一节中的方法进行计算，代码如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line">BloomFilter&lt;T&gt;::<span class="built_in">BloomFilter</span>(<span class="keyword">const</span> <span class="keyword">int32_t</span> n, <span class="keyword">const</span> <span class="keyword">double</span> false_positive_p)</span><br><span class="line">  : <span class="built_in">bits_</span>(), <span class="built_in">k_</span>(<span class="number">0</span>), <span class="built_in">m_</span>(<span class="number">0</span>), <span class="built_in">n_</span>(n), <span class="built_in">p_</span>(false_positive_p)</span><br><span class="line">&#123;</span><br><span class="line">  k_ = <span class="keyword">static_cast</span>&lt;<span class="keyword">int32_t</span>&gt;(-std::<span class="built_in">log</span>(p_) / std::<span class="built_in">log</span>(<span class="number">2</span>));</span><br><span class="line">  m_ = <span class="keyword">static_cast</span>&lt;<span class="keyword">int32_t</span>&gt;(k_ * n * <span class="number">1.0</span> / std::<span class="built_in">log</span>(<span class="number">2</span>));</span><br><span class="line">  bits_.<span class="built_in">resize</span>((m_ + <span class="number">7</span>) / <span class="number">8</span>, <span class="number">0</span>); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里开始实现的时候犯了个低级的错误，一开始用的是<code>bits_.reserve</code>，导致BloomFilter的false positive probability非常高，原因是reserve方法只分配内存，并不进行初始化。</p><h3 id="插入"><a href="#插入" class="headerlink" title="插入"></a>插入</h3><p>即设置每个hash函数计算出来的bit为1，代码如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">void</span> BloomFilter&lt;T&gt;::<span class="built_in">insert</span>(<span class="keyword">const</span> T &amp;key)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">uint32_t</span> hash_val = <span class="number">0xbc9f1d34</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; k_; ++i) &#123;</span><br><span class="line">    hash_val = key.<span class="built_in">hash</span>(hash_val);</span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">uint32_t</span> bit_pos = hash_val % m_;</span><br><span class="line">    bits_[bit_pos/<span class="number">8</span>] |= <span class="number">1</span> &lt;&lt; (bit_pos % <span class="number">8</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="判断是否存在"><a href="#判断是否存在" class="headerlink" title="判断是否存在"></a>判断是否存在</h3><p>即计算每个hash函数对应的bit的值，如果全为1，则返回存在；否则，返回不存在。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">bool</span> BloomFilter&lt;T&gt;::<span class="built_in">key_may_match</span>(<span class="keyword">const</span> T &amp;key)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">uint32_t</span> hash_val = <span class="number">0xbc9f1d34</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; k_; ++i) &#123;</span><br><span class="line">    hash_val = key.<span class="built_in">hash</span>(hash_val);</span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">uint32_t</span> bit_pos = hash_val % m_;</span><br><span class="line">    <span class="keyword">if</span> ((bits_[bit_pos/<span class="number">8</span>] &amp; (<span class="number">1</span> &lt;&lt; (bit_pos % <span class="number">8</span>))) == <span class="number">0</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 class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>下面进行了一组测试，设置期望的false positive probability为0.1，模拟key从10000增长到100000的场景，观察真实的false positive probability的情况：</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></pre></td><td class="code"><pre><span class="line">key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1252</span><br><span class="line">key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1252</span><br><span class="line">key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.1257</span><br><span class="line">key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1211</span><br><span class="line">key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1277</span><br><span class="line">key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1263</span><br><span class="line">key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.126</span><br><span class="line">key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1219</span><br><span class="line">key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1265</span><br><span class="line">key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.1327</span><br></pre></td></tr></table></figure><p>由于实现的时候，会对k进行取整，根据取整后的结果(k=3)，计算出来的理论值是0.1250，可以，看出实际测出来的值和理论值差别不大。</p><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><p>前面实现的版本中，多次调用了hash_func函数，这对于计算比较长的字符串的hash的开销是比较大的，为了模拟这种场景，插入1000w行的数据，使用perf top来抓取其性能数据，结果如下：</p><p><img src="http://oserror.com/images/bloom_filter_hash_opt_before.png" alt="bloomfilter hash opt before"></p><p>如上图，除了生成数据的函数外，占用CPU最高的就属于hash_func了，占用了13%的CPU。</p><p>分析之前的代码可以知道，insert和key_may_match时，都会多次调用hash_func，这个开销是比较大的。</p><p>leveldb和维基百科中都有提到，根据之前的研究，可以采用两次hash的方式来替代上述的多次的计算，基本的思路如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">void</span> BloomFilter&lt;T&gt;::<span class="built_in">insert2</span>(<span class="keyword">const</span> T &amp;key)</span><br><span class="line">&#123; </span><br><span class="line">  <span class="keyword">uint32_t</span> hash_val = key.<span class="built_in">hash</span>(<span class="number">0xbc9f1d34</span>);</span><br><span class="line">  <span class="keyword">const</span> <span class="keyword">uint32_t</span> delta = (hash_val &gt;&gt; <span class="number">17</span>) | (hash_val &lt;&lt; <span class="number">15</span>);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; k_; ++i) &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="keyword">uint32_t</span> bit_pos = hash_val % m_;</span><br><span class="line">    bits_[bit_pos/<span class="number">8</span>] |= <span class="number">1</span> &lt;&lt; (bit_pos % <span class="number">8</span>);</span><br><span class="line">    hash_val += delta;</span><br><span class="line">  &#125;</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>即先用通常的hash函数计算一次，然后，使用移位操作计算一次，最后，k次计算的时候，不断累加两次的结果。</p><p>经过优化后，性能数据图如下：</p><p><img src="http://oserror.com/images/bloom_filter_hash_opt_after.png" alt="bloomfilter hash opt after"></p><p>和之前性能图对比发现，hash_func的CPU使用率已经减少到4%了。</p><p>对比完性能之后，我们还需要对比hash函数按照如此优化后，false positive probability的变化情况：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">before_opt</span><br><span class="line">key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1252</span><br><span class="line">key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1252</span><br><span class="line">key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.1257</span><br><span class="line">key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1211</span><br><span class="line">key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1277</span><br><span class="line">key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1263</span><br><span class="line">key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.126</span><br><span class="line">key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1219</span><br><span class="line">key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1265</span><br><span class="line">key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.1327</span><br><span class="line">after_opt</span><br><span class="line">key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1244</span><br><span class="line">key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1327</span><br><span class="line">key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.134</span><br><span class="line">key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1389</span><br><span class="line">key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1342</span><br><span class="line">key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1548</span><br><span class="line">key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.141</span><br><span class="line">key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1536</span><br><span class="line">key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1517</span><br><span class="line">key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.154</span><br></pre></td></tr></table></figure><p>优化后，最大的false positive probability增长了2%左右，这个可以增加k来弥补，因为，优化后的hash算法，在k增长时，带来的开销相对来讲不大。</p><p>备注，本节采用perf抓取性能数据图，命令如下</p><figure class="highlight plaintext"><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">sudo perf record -a --call-graph dwarf -p 9125 sleep 60</span><br><span class="line">sudo perf report -g graph</span><br></pre></td></tr></table></figure><p>本文的代码在<a href="https://github.com/Charles0429/toys/blob/master/bloomfilter/bloomfilter.cpp">bloomfilter.cpp</a>，使用文档在<a href="https://github.com/Charles0429/toys/tree/master/bloomfilter">ReadMe</a>。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://en.wikipedia.org/wiki/Bloom_filter">bloomfilter wikipage</a></li><li><a href="https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf">Less Hashing, Same Performance:Building a Better Bloom Filter</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;最近在做性能优化相关的事情，其中涉及到了BloomFilter，于是对BloomFilter总结了下，本文组织结构如下：&lt;/p&gt;
&lt;ul&gt;

      
    
    </summary>
    
      <category term="后台开发" scheme="http://yoursite.com/categories/backend/"/>
    
    
      <category term="BloomFilter" scheme="http://yoursite.com/tags/BloomFilter/"/>
    
      <category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>Paxos原理（一）：Basic Paxos</title>
    <link href="http://yoursite.com/distributed/paxos-principle-first/"/>
    <id>http://yoursite.com/distributed/paxos-principle-first/</id>
    <published>2016-11-08T12:46:13.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>Paxos算法由lamport大师提出，目标是解决分布式环境下数据一致性的问题。Paxos算法自发表以来以晦涩难懂著称，因此，其作者于2001年发表了一篇简化版的论文，<a href="http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf">Paxos Made Simple</a>。虽然这篇论文比前面的充满公式证明的论文容易理解，但是，如果对于Paxos算法本身要解决的问题不够理解的话，还是会很难理解该算法。Paxos原理系列文章的目标是在充分讨论Paxos要解决的问题的前提下，深入地分析和理解Paxos的原理。本文是此系列文章第一篇，主要内容如下：</p><ul><li>Paxos算法要解决的问题是什么？</li><li>Basic Paxos在整个Paxos算法中的地位及其原理</li></ul><p>本文收录在我的github中<a href="https://github.com/Charles0429/papers">papers项目</a>，papers项目旨在学习和总结分布式系统相关的论文。</p><h1 id="Paxos算法要解决的问题"><a href="#Paxos算法要解决的问题" class="headerlink" title="Paxos算法要解决的问题"></a>Paxos算法要解决的问题</h1><p>首先，来描述一下Paxos要解决的问题，分布式环境下数据一致性问题。</p><p>考虑下面的环境，如下图：</p><p><img src="http://oserror.com/images/distributed_consensus_problem.png" alt="Distrubted Consensus Problem"></p><p>在分布式环境中，为了保证服务的高可用，需要对数据做多个副本，一般是日志的方式来实现，即图中的log sequence，当某台机器宕机后，其上的请求可以自动的转到其他的Server上，同时会新找一台机器（为了保证副本数量足够），自动地把其他活着机器的日志同步过去，然后逐步回放到State Machine中去。如果Server 1,2,3中的日志是一致的话，可以保证这些Server回放到State Machine中的数据是一致的。那么问题来了，如何保证日志的一致性呢？这正是Paxos算法解决的问题，即如图中的Consensus Module所示，它们之间需要交互，保证日志中内容是完全一致的。</p><p>进一步来看日志中的内容，如下：</p><p><img src="http://oserror.com/images/distributed_log_sequence.png" alt="Log Sequence"></p><p>一个Log Sequence一般由多个Log Item组成，每个Log Item会包含一个Command，用于记录对应的客户端请求的命令，如图中的Add，Mov，Jmp和Set等等，每个Server会根据日志的内容和顺序，一个个的把命令回放到State Machine中。Paxos算法的目标就是为了保证每个Server上的Log Sequence中的Log Item中的Command和相对顺序完全一致，这样，在任意一台机器宕机之后，能保证可以快速地将服务切换到另外一台具有完全相同数据的Server上，从而达到高可用。</p><h1 id="Basic-Paxos"><a href="#Basic-Paxos" class="headerlink" title="Basic Paxos"></a>Basic Paxos</h1><h2 id="Basic-Paxos要解决的问题"><a href="#Basic-Paxos要解决的问题" class="headerlink" title="Basic Paxos要解决的问题"></a>Basic Paxos要解决的问题</h2><p>整个Paxos算法是为了解决整个Log Sequence一致性的问题，一般也称为Multi Paxos。而本小节要讨论的Basic Paxos是为了确定一个不变量的取值，放到上面的Log Sequence一致性上来讲，即为了确定某一个Log Item中的Command的取值，保证多个Server一旦确认该Log Item的Command之后，其值就不会再变化。</p><p>以一个例子描述确认Log Item的取值问题，如下：</p><p><img src="http://oserror.com/images/distributed_log_item_consensus.png" alt="Distributed Log Item Consensus"></p><p>如上图所示，每个Server从客户端接受到的请求可能不一样，例如，图中的三个Server分别接收到Add，Mov和Jmp等三个不同的请求，而对于当对于当前的Log Item来讲，只能存储一个请求，而为了保证Log的一致性，又必须要Log Item中存储的Command是一致的，因此，三个Server需要协调，最终确定此Log Item存储哪一个请求，这个确定的过程就是一轮Basic Paxos过程。</p><p>最后，以比较正式的方式来定义此问题：</p><p>假设一组进程能提出（propose）value，分布式一致性算法能保证最终只有一个value被选择（chosen）。如果没有value被提出，那么就没有value被选择。如果一个value被选择了，那么这组进程能学习（learn）到被选择的value。</p><p>总体看来，一致性算法的需求如下：</p><ul><li>被提出的value中，只有一个value被选择</li><li>进程在value被选择前，不应该能学习到该value</li></ul><p>根据上面的问题，算法中总共有三类角色，即proposers，acceptors和learners，实际的实现中，一个进程可能承担三种角色中的一个或多个。这些角色之间通过发送消息的方式来相互通信，并且是在非拜占庭场景下：</p><ul><li>角色的计算速度可能不同，甚至可能因为宕机而终止运行，随后又被重启。在没有记录之前已选择的value的情况下，之前选择的value会丢失，因此，需要记录之前已选择的value</li><li>消息可以时延很长，可以重复，可以丢失，但是其内容不能被篡改</li></ul><p>在了解了上述需求，问题定义及场景后，接下来一步步地推导出Basic Paxos的最终算法。</p><h2 id="如何解决问题？"><a href="#如何解决问题？" class="headerlink" title="如何解决问题？"></a>如何解决问题？</h2><p>整个问题分为两块：</p><ul><li>如何选择value？</li><li>如何学习value？</li></ul><h3 id="如何选择value？"><a href="#如何选择value？" class="headerlink" title="如何选择value？"></a>如何选择value？</h3><p>首先，来看一个最简单的方案，如下：</p><p><img src="http://oserror.com/images/consensus_one_acceptor.png" alt="Consensus one acceptor"></p><p>只有一个acceptor，这个acceptor只认第一个Proposer给它提出的value，例如，在上图中，如果Proposer1先把value提给acceptor，那么acceptor最终会选择该value，即Add。</p><p>此方案的优点是简单易懂，但当acceptor挂掉并无法恢复之后，被选择的value也跟着丢失了，不满足需求。</p><p>因此，接下来的方案中，只考虑多个acceptor的场景。</p><p>为了保证仅有一个value被选择，需要在多数派的Server接受该value时，才认为该value被选择。因为任意两个多数派的acceptor集合中，必然有一个acceptor是相同的。举个例子，如果不是必须多数派的话，可能出现的场景时，有前1/3的acceptor选择value1，中间1/3的acceptor选择value2，最后1/3的acceptor选择value3，这样就会导致不止一个value被选择，不符合要求。</p><p>因为消息是有可能丢失的，因此，当只有一个value被提出的时候，acceptor应该接受它，即</p><blockquote><p>P1. acceptor必须要接受它接受到的第一个value</p></blockquote><p>但这会导致如下问题：</p><p><img src="http://oserror.com/images/consensus_acceptor_p1_split_vote.png" alt="Paxos p1-split accept"></p><p>假设Proposer 1,2,3分别提出Add，Mov和Jmp，且Proposeri{i=1,2,3}首先提给Accepti{i=1,2,3}，这样会导致最终每个acceptor都接受（accept）了不同的值，最终没有value被选择。</p><p>P1和某个value只有被多数派的acceptor接受后的条件表明，每个acceptor需要能接受多个value，因此，需要通过某种方法来区分，这里采用假设每个proposer提出的value都被分配了一个id，id为自然数，每个(id,value)组合称为一个proposal（提案）。每个proposal的id都是不同的。此时，如果一个value被选择，会对应于一个或多个proposal被多数派的acceptor接受，且该proposal的value对应于被选择的value。</p><p>由于允许多个proposal被选择，因此，需要保证每个被acceptor接受的proposal的value都相同，故有如下推论</p><blockquote><p>P2. 如果一个proposal(id1, v1)被选择，那么，每个id大于id1的，被接受的proposal的value都等于v</p></blockquote><p>由于proposal被选择，至少需要一个acceptor接受，因此，可以由P2进一步地加强约束到</p><blockquote><p>P2a. 如果一个proposal(id1, v1)被选择，那么，每个id大于id1的proposal(id,value)，如果被任意一个acceptor接受的话，value=v1</p></blockquote><p>但P2a会存在如下问题：</p><p><img src="http://oserror.com/images/paxos_p2a_problem.png" alt="P2a problem"></p><p>考虑以上场景，Proposal1的(10, Add)proposal被三个acceptor接受，但是，Proposer1和acceptor4之间网络不联通，导致acceptor4一致为接受任何的proposal。此时，有一个新的proposal2加入，并且能和acceptor4联通，并且，其提出的proposal为(11, Jmp)，根据P1原则，acceptor4必须要接受第一个接收到的proposal，即(11, Jmp)，导致其和P2a冲突。</p><p>因此，进一步增强P2a的约束为</p><blockquote><p>P2b. 如果一个proposal(id1, v1)被选择，那么任意一个id大于id1的proposal的value等于v1</p></blockquote><p>根据P2b，如果一个proposal(m,v)被选择，那么对于n&gt;m的proposal的value也必须是v。假如当前最大的proposal的id为x-1，那么必定会存在一个多数派的acceptor组合C，使得每个acceptor接受的proposal都的id都属于[m,x-1]，且都拥有值v，并且，每个id属于[m,x-1]的proposal，如果其被任意的acceptor接受，其value必为v。</p><p>继续对P2b加强约束，由于任意的多数的集合S，至少包含C中的一个acceptor，而每个这样的集合中，id最大的proposal的value肯定是已经选择的value，因为，P2b保证了在有proposal(m,v)被选择后，其后id大于m的proposal的value肯定是v，因此，所有acceptor中只可能处于id小于m的proposal的value不等于v，所有id大于或或等于m的proposal其value必定是v。<br>从而，我们可以得出，任意一个proposal(x,v)，至少满足以下条件之一：</p><blockquote><p>P2c. 存在多数派的acceptor集合S，对于任意的proposal(x,v)，需要满足以下条件之一：</p><ol><li>S中的任意一个acceptor都没有接受过id小于x的proposal</li><li>S中的acceptor接受了id处于[0…x-1]的proposal，其中，v是当中id最大的proposal的value</li></ol></blockquote><p>因此，对于一个新提出的proposal，其必须要先学习到已经被或者将要被accept的id最大的value。要预测将要被accept的proposal是很困难的，但是，我们可以在acceptor中加限制，即，如果acceptor已经接受过(n,v)了，那么任何的id小于n的proposal都不会被接受，这样就能保证当前获取到的最大的id是正确的，举个例子说明：</p><p><img src="http://oserror.com/images/paxos_p2c_problem.png" alt="P2c problem"></p><p>上述例子发生的场景如下：</p><ol><li>Proposal2在Acceptor2还没有accept (1,Jmp)的时候，学习到当前最大的proposal的value是(0,Add)，因此，提出了Proposal2(2,Add)</li><li>Proposal3在Acceptor3接受了(1, Jmp)的时候，学习到当前最大id的Proposal的value是(1,Jmp)，因此，提出了Proposal3(3,Jmp)</li><li>由于网络或其他原因，Proposal3先到达Acceptor3，于是，acceptor3接受Proposal3，此时，多数派的已经达成，Jmp被选择</li></ol><p>而后，Proposal2达到acceptor3，如果acceptor3选择接受它，那么，会出现以下情况：</p><p><img src="http://oserror.com/images/paxos_p2c_problem1.png" alt="Paxos problem 1"></p><p>Proposal2覆盖了Acceptor3已经接受过的值，导致Add成为新的多数派而被选择，不符合要求。实际上，在Proposal3提出时，由于Proposal2并没有被接受，导致，Proposal3只能学习到(1,Jmp)，从这个角度来讲，Proposal2是属于Proposal3提出后被确认的，因此，需要在acceptor加以限制，不再接受比其接受过的proposal id小的proposal。</p><p>由上面的讨论，对于一个proposer需要经历如下两个步骤：</p><ul><li>对于一个proposer，选择一个新的proposer id，假设为n，然后发送请求到一些acceptor组合（保证大于多数派的数量），acceptor的回应保证如下：<ul><li>不再接受proposal id小于n的请求</li><li>已接受的最大的id的proposal的value</li></ul></li></ul><p>第一步称为Prepare</p><ul><li>如果proposer接收到多数派的acceptor的回应，那么，它可以提出该proposal，id为n，value为第一步的value，或者是如果第一步的获得的value为空的话，proposer则使用自己提出的value，然后把该proposal发给一些acceptor组合（保证大于多数派的数量）。</li></ul><p>第二步称为Accept</p><p>对于acceptor来讲，Prepare时，它都可以回应，但是，对于accept的时候，需要满足如下条件：</p><blockquote><p>P1a. 一个acceptor在它没有回应一个比n大的prepare请求时，其可以接受id为n的proposal的value</p></blockquote><p>值得注意的是，当一个acceptor已经回应了比n大的prepare请求时，就没必要回应小于或等于n的prepare请求了，因为后者肯定不会被accept了。因此，对于acceptor来讲，需要记录最大的prepare的proposal id，为了防止acceptor宕机后重启的情况，故最大的proposer的id需要被持久化存储。</p><p>用伪代码表达proposer的算法如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Prepare()</span><br><span class="line">&#123;</span><br><span class="line">    select a proposal id n;</span><br><span class="line">    send a prepare request with id n to a majority of acceptors</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Accept()</span><br><span class="line">&#123;</span><br><span class="line">    if get responce from majority &#123;</span><br><span class="line">        send proposal(n, v); // v is the hightest-numbered proposal&#x27;s value from reponses, or if any value if the responsed reported no proposals</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        abort()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用为代码表达acceptor的算法如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Prepare()</span><br><span class="line">&#123;</span><br><span class="line">    if receive prepare request id with n &gt; max_prepared_id recoreded &#123;</span><br><span class="line">        send(not accept proposal numbered less than n);</span><br><span class="line">        send(highest-numbered proposal it has accepted if exist)</span><br><span class="line">        max_prepared_id = id</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        do nothing;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Accept()</span><br><span class="line">&#123;</span><br><span class="line">    if max_prepared_id == request_accept_id &#123;</span><br><span class="line">        accept the proposal</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        do nothing</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="如何学习value？"><a href="#如何学习value？" class="headerlink" title="如何学习value？"></a>如何学习value？</h3><p>最简单粗暴的方案是，每当一个acceptor接受了新的proposal的时候，就广播给所有的learner，假设acceptor的数量为m，learner的数量为n，那么需要O(m*n)通信的开销。</p><p>为了减少通信开销，可以选出一个learner，负责接收acceptor的消息，然后再由它通知给其他的learner，这时需要O(m+n)。这个方法的缺点是如果这个learner宕机了，整个系统就无法正常工作了。改进的方案是，选择一组learner，假设数量为c，负责接受acceptor的消息，这些leaner负责通知其他的leaner，该方案的通信开销为c*o(m+n)，且可用性比较高，只要c个learner中没有全部宕机，系统就可以正常工作。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文分析和讨论了Paxos算法要解决的问题，即分布式系统中数据一致性的问题。为了实现数据一致性，需要保证各个副本的日志序列的一致性，而日志序列是由一个个的日志项组成的，Basic Paxos算法的目标是为了解决单个日志项的一致性。直观的来看，日志序列的一致性可以用多轮的Basic Paxos来达到，但是，往往出于性能，算法稳定性等原因的考虑，需要对多轮的Basic Paxos做优化，这就是接下来要讨论的Multi Paxos算法，敬请期待。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf">Paxos Made Simple</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;Paxos算法由lamport大师提出，目标是解决分布式环境下数据一致性的问题。Paxos算法自发表以来以晦涩难懂著称，因此，其作者于200
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Paxos" scheme="http://yoursite.com/tags/Paxos/"/>
    
  </entry>
  
  <entry>
    <title>golang实现Raft（一）：选主</title>
    <link href="http://yoursite.com/distributed/implement-raft-with-golang-first/"/>
    <id>http://yoursite.com/distributed/implement-raft-with-golang-first/</id>
    <published>2016-11-03T02:05:28.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h1><p>本文为golang实现Raft第一篇，主要描述了如何使用golang实现选主，文中的代码框架来自于MIT 6.824课程，包括rpc框架及测试用例。</p><h1 id="Raft选主"><a href="#Raft选主" class="headerlink" title="Raft选主"></a>Raft选主</h1><p>根据Raft论文，选主模块主要包括三大功能：</p><ul><li>candidate状态下的选主功能</li><li>leader状态下的心跳广播功能</li><li>follower状态下的确认功能</li></ul><h2 id="candidate状态下的选主功能"><a href="#candidate状态下的选主功能" class="headerlink" title="candidate状态下的选主功能"></a>candidate状态下的选主功能</h2><p>candidate状态下的选主功能需要关注两个方面：</p><ul><li>何时进入candidate状态，进行选主？</li><li>选主的逻辑是怎样的？</li></ul><p>首先，来讨论何时进入candidate状态，进行选主。</p><p>在一定时间内没有收到来自leader或者其他candidate的有效RPC时，将会触发选主。这里需要关注的是有效两个字，要么是leader发的有效的心跳信息，要么是candidate发的是有效的选主信息，即server本身确认这些信息是有效的后，才会重新更新超时时间，超时时间根据raft论文中推荐设置为[150ms,300ms]，并且每次是随机生成的值。</p><p>其次，来讨论选主的逻辑。</p><p>server首先会进行选主的初始化操作，即server会增加其term，把状态改成candidate，然后选举自己为主，并把选主的RPC并行地发送给集群中其他的server，根据返回的RPC的情况的不同，做不同的处理：</p><ul><li>该server被选为leader</li><li>其他的server选为leader</li><li>一段时间后，没有server被选为leader</li></ul><p>针对情况一，该server被选为leader,当前仅当在大多数的server投票给该server时。当其被选为主时，会立马发送心跳消息给其他的server，来表明其已经是leader，防止发生新的选举。</p><p>针对情况二，其他的server被选为leader，它会收到leader发送的心跳信息，此时，该server应该转为follower，然后退出选举。</p><p>针对情况三，一段时间后，没有server被选为leader，这种情况发生在没有server获得了大多数的server的投票情况下，此时，应该发起新一轮的选举。</p><h2 id="leader状态下的心跳广播功能"><a href="#leader状态下的心跳广播功能" class="headerlink" title="leader状态下的心跳广播功能"></a>leader状态下的心跳广播功能</h2><p>当某个server被选为leader后，需要广播心跳信息，表明其是leader，主要在以下两个场景触发：</p><ul><li>server刚当选为leader</li><li>server周期性的发送心跳消息，防止其他的server进入candidate选举状态</li></ul><p>leader广播心跳的逻辑为，如果广播的心跳信息得到了大多数的server的确认，那么更新leader自身的选举超时时间，防止发生重新选举。</p><h2 id="follower状态下的确认功能"><a href="#follower状态下的确认功能" class="headerlink" title="follower状态下的确认功能"></a>follower状态下的确认功能</h2><p>主要包括对candidate发的选举RPC以及leader发来的心跳RPC的确认功能。</p><p>对于选举RPC，假设candidate c发送选举RPC到该follower，由于follower每个term只能选举一个server，因此，只有当一个follower没有选举其他server的时候，并且选举RPC中的candidate c的term大于或等于follower的term时，才会返回选举当前candidate c为主，否则，则返回拒绝选举当前candidate c为主。</p><p>对于leader的心跳RPC，如果leader的心跳的term大于或等于follower的term，则认可该leader的心跳，否则，不认可该leader的心跳。</p><p>备注：本节所讨论的选举功能仅限于raft论文5.2，还未考虑选举过程中日志相关的信息以及选主过程中出现宕机等场景，此部分功能将在日志复制功能实现中再描述。</p><h2 id="选主实现"><a href="#选主实现" class="headerlink" title="选主实现"></a>选主实现</h2><h3 id="RPC"><a href="#RPC" class="headerlink" title="RPC"></a>RPC</h3><p>根据上述功能，需要以下的RPC：</p><ul><li>选举RPC</li><li>心跳RPC</li></ul><p>选举RPC包括的信息如下：</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">//       </span></span><br><span class="line"><span class="comment">// example RequestVote RPC arguments structure.</span></span><br><span class="line"><span class="comment">//       </span></span><br><span class="line"><span class="keyword">type</span> RequestVoteArgs <span class="keyword">struct</span> &#123;                                                                                                                                                           </span><br><span class="line">  <span class="comment">// Your data here.</span></span><br><span class="line">  Term         <span class="keyword">int</span></span><br><span class="line">  CandidateId  <span class="keyword">int</span> </span><br><span class="line">  LastLogIndex <span class="keyword">int</span> </span><br><span class="line">  LastLogTerm  <span class="keyword">int</span> </span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>其中，Term表示当前candidate的term，而candidateId表明当前candidate的ID，全局唯一。其他两个参数将在日志复制中功能完成后再使用，暂时先不讨论。</p><p>选举RPC的回复包含的信息如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> RequestVoteReply <span class="keyword">struct</span> &#123;</span><br><span class="line">  <span class="comment">// Your data here.</span></span><br><span class="line">  Term        <span class="keyword">int</span></span><br><span class="line">  VoteGranted <span class="keyword">bool</span></span><br><span class="line">&#125;  </span><br></pre></td></tr></table></figure><p>其中Term表示确认的server的term，如果candidate的term小于它，将会更新其term；VoteGranted表明回复的follower是否给其投票。</p><p>心跳RPC包含的信息如下：</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="keyword">type</span> AppendEntriesArgs <span class="keyword">struct</span> &#123;</span><br><span class="line">  Term     <span class="keyword">int</span></span><br><span class="line">  LeaderId <span class="keyword">int</span></span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>其中Term为leader的term，LeaderId为当前leader的ID，全局唯一。</p><p>心跳RPC的回复信息包括：</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="keyword">type</span> AppendEntriesReply <span class="keyword">struct</span> &#123;</span><br><span class="line">  Term    <span class="keyword">int</span></span><br><span class="line">  Success <span class="keyword">bool</span></span><br><span class="line">&#125;  </span><br></pre></td></tr></table></figure><p>包括回复的server的Term信息，以及是否认可该leader继续为主。</p><h3 id="candidate状态下的选主功能-1"><a href="#candidate状态下的选主功能-1" class="headerlink" title="candidate状态下的选主功能"></a>candidate状态下的选主功能</h3><p>根据前面描述，主要的逻辑为</p><ul><li>等待选举超时</li><li>增加term，置状态为follower，并且选举自己为leader</li><li>向其他的server并行地发送选举RPC，直到碰到上述描述的三种情况退出</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">election</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> result <span class="keyword">bool</span></span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    timeout := randomRange(<span class="number">150</span>, <span class="number">300</span>)</span><br><span class="line">    printTime()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;candidate=%d wait election timeout=%d\n&quot;</span>, rf.me, timeout)</span><br><span class="line">    rf.setMessageTime(milliseconds())</span><br><span class="line">    <span class="keyword">for</span> rf.lastMessageTime+timeout &gt; milliseconds() &#123;</span><br><span class="line">      <span class="keyword">select</span> &#123;</span><br><span class="line">      <span class="keyword">case</span> &lt;-time.After(time.Duration(timeout) * time.Millisecond):</span><br><span class="line">        printTime()</span><br><span class="line">        fmt.Printf(<span class="string">&quot;candidate=%d, lastMessageTime=%d, timeout=%d, plus=%d, now=%d\n&quot;</span>, rf.me, rf.lastMessageTime, timeout, rf.lastMessageTime+timeout, milliseconds())</span><br><span class="line">        <span class="keyword">if</span> rf.lastMessageTime+timeout &lt;= milliseconds() &#123;</span><br><span class="line">          <span class="keyword">break</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          rf.setMessageTime(milliseconds())</span><br><span class="line">          timeout = randomRange(<span class="number">150</span>, <span class="number">300</span>)</span><br><span class="line">          <span class="keyword">continue</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">    printTime()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;candidate=%d timeouted\n&quot;</span>, rf.me)</span><br><span class="line">    <span class="comment">// election till success</span></span><br><span class="line">    result = <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> !result &#123;</span><br><span class="line">      result = rf.election_one_round()</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>上述代码中，首先等待选举超时，超时后，会进入真正的选举逻辑<code>election_one_round</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="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">becomeCandidate</span><span class="params">()</span></span> &#123;                                                                                                                                                     </span><br><span class="line">  rf.state = <span class="number">1</span>   </span><br><span class="line">  rf.setTerm(rf.currentTerm + <span class="number">1</span>)</span><br><span class="line">  rf.votedFor = rf.me</span><br><span class="line">  rf.currentLeader = <span class="number">-1</span></span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>首先，进入candidate状态，增加其term，然后，选举自己。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(rf.peers); i++ &#123;</span><br><span class="line">   <span class="keyword">if</span> i != rf.me &#123;</span><br><span class="line">     <span class="keyword">var</span> args RequestVoteArgs</span><br><span class="line">     server := i</span><br><span class="line">     args.Term = rf.currentTerm</span><br><span class="line">     args.CandidateId = rf.me</span><br><span class="line">     <span class="keyword">var</span> reply RequestVoteReply</span><br><span class="line">     printTime()</span><br><span class="line">     fmt.Printf(<span class="string">&quot;candidate=%d send request vote to server=%d\n&quot;</span>, rf.me, i)</span><br><span class="line">     <span class="keyword">go</span> rf.sendRequestVoteAndTrigger(server, args, &amp;reply, rpcTimeout)</span><br><span class="line">   &#125;</span><br><span class="line"> &#125; </span><br><span class="line"> done = <span class="number">0</span></span><br><span class="line"> triggerHeartbeat = <span class="literal">false</span></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(rf.peers)<span class="number">-1</span>; i++ &#123;</span><br><span class="line">   printTime()</span><br><span class="line">   fmt.Printf(<span class="string">&quot;candidate=%d waiting for select for i=%d\n&quot;</span>, rf.me, i)</span><br><span class="line">   <span class="keyword">select</span> &#123;</span><br><span class="line">   <span class="keyword">case</span> ok := &lt;-rf.electCh:</span><br><span class="line">     <span class="keyword">if</span> ok &#123;</span><br><span class="line">       done++</span><br><span class="line">       success = done &gt;= <span class="built_in">len</span>(rf.peers)/<span class="number">2</span> || rf.currentLeader &gt; <span class="number">-1</span></span><br><span class="line">       success = success &amp;&amp; rf.votedFor == rf.me</span><br><span class="line">       <span class="keyword">if</span> success &amp;&amp; !triggerHeartbeat &#123;</span><br><span class="line">         triggerHeartbeat = <span class="literal">true</span></span><br><span class="line">         rf.mu.Lock()</span><br><span class="line">         rf.becomeLeader()</span><br><span class="line">         rf.mu.Unlock()</span><br><span class="line">         rf.heartbeat &lt;- <span class="literal">true</span></span><br><span class="line">         printTime()</span><br><span class="line">         fmt.Printf(<span class="string">&quot;candidate=%d becomes leader\n&quot;</span>, rf.currentLeader)</span><br><span class="line">       &#125;</span><br><span class="line">     &#125;</span><br><span class="line">   &#125;</span><br><span class="line">   printTime()</span><br><span class="line">   fmt.Printf(<span class="string">&quot;candidate=%d complete for select for i=%d\n&quot;</span>, rf.me, i)</span><br><span class="line"> &#125; </span><br></pre></td></tr></table></figure><p>接着，向除自己外的server发送选举RPC，等待server的回复，当成功返回数目到多数派时（包含自己在内），则宣布自己称为leader，即<code>becomeLeader</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">becomeLeader</span><span class="params">()</span></span> &#123;                                                                                                                                                        </span><br><span class="line">  rf.state = <span class="number">2</span>   </span><br><span class="line">  rf.currentLeader = rf.me</span><br><span class="line">&#125;            </span><br></pre></td></tr></table></figure><p>即，修改自身状态为leader。然后，给发送心跳的线程发送<code>rf.heartbeat &lt;-true</code>，通知心跳线程开始发心跳包。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (timeout+last &lt; milliseconds()) || (done &gt;= <span class="built_in">len</span>(rf.peers)/<span class="number">2</span> || rf.currentLeader &gt; <span class="number">-1</span>) &#123;</span><br><span class="line">  <span class="keyword">break</span>     </span><br><span class="line">&#125; <span class="keyword">else</span> &#123;    </span><br><span class="line">  <span class="keyword">select</span> &#123;  </span><br><span class="line">  <span class="keyword">case</span> &lt;-time.After(time.Duration(<span class="number">10</span>) * time.Millisecond):</span><br><span class="line">  &#125;         </span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>即，<code>timeout+last &lt; milliseconds()</code>达到超时时间；或者<code>done &gt;= len(rf.peers)/2</code>，server成为leader；或者<code>rf.currentLeader &gt; -1</code>，有其他server选为leader。</p><h3 id="leader状态下的广播心跳功能"><a href="#leader状态下的广播心跳功能" class="headerlink" title="leader状态下的广播心跳功能"></a>leader状态下的广播心跳功能</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><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="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">sendLeaderHeartBeat</span><span class="params">()</span></span> &#123;</span><br><span class="line">  timeout := <span class="number">20</span></span><br><span class="line">  <span class="keyword">for</span> &#123;   </span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-rf.heartbeat:</span><br><span class="line">      rf.sendAppendEntriesImpl()</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(time.Duration(timeout) * time.Millisecond):</span><br><span class="line">      rf.sendAppendEntriesImpl()</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>分为两个方面：</p><ul><li>第一个为刚当选为leader后，需要马上发送心跳信息，防止新的选举发生</li><li>第二个是leader周期性的发送心跳信息，来宣布自己为主</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">sendAppendEntriesImpl</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> rf.currentLeader == rf.me &#123;</span><br><span class="line">    <span class="keyword">var</span> args AppendEntriesArgs</span><br><span class="line">    <span class="keyword">var</span> success_count <span class="keyword">int</span></span><br><span class="line">    timeout := <span class="number">20</span></span><br><span class="line">    args.LeaderId = rf.me</span><br><span class="line">    args.Term = rf.currentTerm</span><br><span class="line">    printTime()  </span><br><span class="line">    fmt.Printf(<span class="string">&quot;broadcast heartbeat start\n&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(rf.peers); i++ &#123;</span><br><span class="line">      <span class="keyword">if</span> i != rf.me &#123;</span><br><span class="line">        <span class="keyword">var</span> reply AppendEntriesReply</span><br><span class="line">        printTime()</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Leader=%d send heartbeat to server=%d\n&quot;</span>, rf.me, i)</span><br><span class="line">        <span class="keyword">go</span> rf.sendHeartBeat(i, args, &amp;reply, timeout)</span><br><span class="line">      &#125;          </span><br><span class="line">    &#125;            </span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(rf.peers)<span class="number">-1</span>; i++ &#123;</span><br><span class="line">      <span class="keyword">select</span> &#123;   </span><br><span class="line">      <span class="keyword">case</span> ok := &lt;-rf.heartbeatRe:</span><br><span class="line">        <span class="keyword">if</span> ok &#123;  </span><br><span class="line">          success_count++</span><br><span class="line">          <span class="keyword">if</span> success_count &gt;= <span class="built_in">len</span>(rf.peers)/<span class="number">2</span> &#123;</span><br><span class="line">            rf.mu.Lock()</span><br><span class="line">            rf.setMessageTime(milliseconds())</span><br><span class="line">            rf.mu.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">    printTime()  </span><br><span class="line">    fmt.Printf(<span class="string">&quot;broadcast heartbeat end\n&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> success_count &lt; <span class="built_in">len</span>(rf.peers)/<span class="number">2</span> &#123;</span><br><span class="line">      rf.mu.Lock()</span><br><span class="line">      rf.currentLeader = <span class="number">-1</span></span><br><span class="line">      rf.mu.Unlock()</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>先是向集群中所有的其他server广播心跳，分为两种结果：</p><ul><li>收到了大多数server的确认，则更新leader的超时时间，防止重新进入选举状态</li><li>未收到大多数server的确认，则会退出发送心跳的逻辑，即置currentLeader = -1，此后，自然会有选举超时的server重新发起选举</li></ul><h3 id="follower状态下的确认功能-1"><a href="#follower状态下的确认功能-1" class="headerlink" title="follower状态下的确认功能"></a>follower状态下的确认功能</h3><p>包括对选举RPC的确认已经对心跳RPC的确认。</p><p>选举RPC的确认逻辑如下</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">RequestVote</span><span class="params">(args RequestVoteArgs, reply *RequestVoteReply)</span></span> &#123;</span><br><span class="line">  <span class="comment">// Your code here.</span></span><br><span class="line">  currentTerm, _ := rf.GetState()</span><br><span class="line">  <span class="keyword">if</span> args.Term &lt; currentTerm &#123;</span><br><span class="line">    reply.Term = currentTerm</span><br><span class="line">    reply.VoteGranted = <span class="literal">false</span></span><br><span class="line">    printTime() </span><br><span class="line">    fmt.Printf(<span class="string">&quot;candidate=%d term = %d smaller than server = %d, currentTerm = %d\n&quot;</span>, args.CandidateId, args.Term, rf.me, rf.currentTerm)</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="keyword">if</span> rf.votedFor != <span class="number">-1</span> &amp;&amp; args.Term &lt;= rf.currentTerm &#123;</span><br><span class="line">    reply.VoteGranted = <span class="literal">false</span></span><br><span class="line">    rf.mu.Lock()</span><br><span class="line">    rf.setTerm(max(args.Term, currentTerm))</span><br><span class="line">    reply.Term = rf.currentTerm</span><br><span class="line">    rf.mu.Unlock()</span><br><span class="line">    printTime() </span><br><span class="line">    fmt.Printf(<span class="string">&quot;rejected candidate=%d term = %d server = %d, currentTerm = %d, has_voted_for = %d\n&quot;</span>, args.CandidateId, args.Term, rf.me, rf.currentTerm, rf.votedFor)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;      </span><br><span class="line">    rf.mu.Lock()</span><br><span class="line">    rf.becomeFollower(max(args.Term, currentTerm), args.CandidateId)</span><br><span class="line">    rf.mu.Unlock()</span><br><span class="line">    reply.VoteGranted = <span class="literal">true</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;accepted server = %d voted_for candidate = %d\n&quot;</span>, rf.me, args.CandidateId)</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>如果当前server的term大于candidate的term，或者当前server已经选举过其他server为leader了，那么返回拒绝的RPC，否则，则返回成功的RPC，并置自身状态为follower。</p><p>心跳的RPC的逻辑如下</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rf *Raft)</span> <span class="title">AppendEntries</span><span class="params">(args AppendEntriesArgs, reply *AppendEntriesReply)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> args.Term &lt; rf.currentTerm &#123;</span><br><span class="line">    reply.Success = <span class="literal">false</span></span><br><span class="line">    reply.Term = rf.currentTerm</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;       </span><br><span class="line">    reply.Success = <span class="literal">true</span></span><br><span class="line">    reply.Term = rf.currentTerm</span><br><span class="line">    rf.mu.Lock() </span><br><span class="line">    rf.currentLeader = args.LeaderId</span><br><span class="line">    rf.votedFor = args.LeaderId</span><br><span class="line">    rf.state = <span class="number">0</span> </span><br><span class="line">    rf.setMessageTime(milliseconds())</span><br><span class="line">    printTime()  </span><br><span class="line">    fmt.Printf(<span class="string">&quot;server = %d learned that leader = %d\n&quot;</span>, rf.me, rf.currentLeader)</span><br><span class="line">    rf.mu.Unlock()</span><br><span class="line">  &#125;              </span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>如果follower的term大于leader的term，则返回拒绝的RPC，否则，返回成功的RPC。</p><p>本文中所有的代码都在<a href="https://github.com/Charles0429/toys/blob/master/6.824/src/raft/raft.go">Raft</a>。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf">In Search of an Understandable Consensus Algorithm(Extended Version)</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;headerlink&quot; title=&quot;介绍&quot;&gt;&lt;/a&gt;介绍&lt;/h1&gt;&lt;p&gt;本文为golang实现Raft第一篇，主要描述了如何使用golang实现选主，文中的代码框架来自于MIT 6.824课程，包括rpc框架及测
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="golang" scheme="http://yoursite.com/tags/golang/"/>
    
      <category term="raft" scheme="http://yoursite.com/tags/raft/"/>
    
  </entry>
  
  <entry>
    <title>自己动手写分布式KV存储引擎（三）：网络框架中的客户端实现原理</title>
    <link href="http://yoursite.com/distributed/implement-network-framework-client/"/>
    <id>http://yoursite.com/distributed/implement-network-framework-client/</id>
    <published>2016-10-30T09:15:46.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>自己动手写分布式KV存储引擎系列文章的目标是记录基于LevelDB(RockDB)构建一个分布式KV存储引擎实现过程，此系列文章对应的源码在<a href="https://github.com/Charles0429/dstore">DSTORE</a>。</p><p>本文主要分析了网络框架中客户端的实现原理，全文分为如下两部分</p><ul><li>客户端功能需求</li><li>客户端实现</li></ul><p>本系列的其他文章还包括：</p><ul><li><a href="http://oserror.com/distributed/implement-network-framework-using-C/">自己动手写分布式KV存储引擎（一）：设计和实现网络框架</a></li><li><a href="http://oserror.com/distributed/network-framework-timers/">自己动手写分布式KV存储引擎（二）：网络框架中的定时器原理和实现</a></li></ul><h1 id="客户端功能需求"><a href="#客户端功能需求" class="headerlink" title="客户端功能需求"></a>客户端功能需求</h1><p>在分布式系统中，一台服务器在与其他服务器交互的过程中，既扮演server端，也扮演client端，因此，网络框架中client端的实现也是至关重要的，一般地，网络框架中client端至少提供以下接口：</p><ul><li>connect: 连接其他服务器，因为是服务端程序，需要高性能，因此，此操作必须是非阻塞的</li><li>read: 读server端发来的数据</li><li>write： 往server端写数据</li></ul><h1 id="客户端实现"><a href="#客户端实现" class="headerlink" title="客户端实现"></a>客户端实现</h1><p>针对网络框架中的客户端需要的各种接口，本节讨论其功能实现。</p><h2 id="connect"><a href="#connect" class="headerlink" title="connect"></a>connect</h2><p>先来看看阻塞方式的connect，一般其用法如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> ret = <span class="built_in">connect</span>(fd, server_addr, server_len);</span><br><span class="line"><span class="built_in">send</span>(fd, data);</span><br><span class="line"><span class="built_in">recv</span>(fd, data);</span><br></pre></td></tr></table></figure><p>阻塞的connect函数调用如下，其TCP状态转换如下图：</p><p><img src="http://oserror.com/images/tcp_blocking_connect.png" alt="tcp block connect"></p><p>如上图所示，整个流程如下：</p><ol><li>client端调用阻塞式connect，操作系统会向server端发送SYN数据包，并且client端的TCP状态会变成SYNC_SENT</li><li>server端调用accept接受connect请求，首先设置其TCP状态为SYNC_RCVD，然后会发送对server端的SYN包的确认包</li><li>client端接收到server端的确认包，操作系统将TCP状态设置成ESTABLISHED，这时候阻塞的connect函数返回，并且，操作系统会向服务端发送确认包</li><li>服务端在收到客户端的确认包之后，操作系统将TCP连接状态设置成ESTABLISHED，接着，服务端的accept调用返回</li></ol><p>从上述调用流程可以看出，阻塞式的connect会等待收到服务端的确认包之后，才会返回，这中间的等待时间是一次网络包的往返时间，对于服务端程序来讲，阻塞在等待连接建立上是不能接受的，因此，必须采用非阻塞的connect。</p><p>非阻塞的connect的调用如下，其TCP状态转换如下图：</p><p><img src="http://oserror.com/images/tcp_nonblocking_connect.png" alt="tcp nonblocking accept"></p><p>如上图所示，整个流程如下：</p><ol><li>client端调用非阻塞的connect，操作系统会向server端发送SYN数据包，并且client端的TCP状态会变成SYNC_SENT，然后client端返回</li><li>server端调用accept接受connect请求，其首先设置其TCP状态为SYNC，发送对client端的SYN包的确认包，接着accept函数返回</li><li>client端收到server端的确认包之后，操作系统将其状态设置成ESTABLISHED，然后，向server端发送确认包</li><li>server端收到确认包之后，操作系统将其状态设置成ESTABLISHED</li></ol><p>对于非阻塞的connect，没有了等待server端回确认包的过程，但是，网络框架需要处理的是，连接真正建立的时候需要通知应用程序来处理。</p><p>根据linux mannual文档，说明如下</p><blockquote><p>EINPROGRESS<br>The  socket  is  nonblocking  and  the connection cannot be completed immediately.  It is possible to select(2) or poll(2) for completion by selecting the socket for<br>              writing.  After select(2) indicates writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine whether connect() completed  success‐<br>              fully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed here, explaining the reason for the failure).</p></blockquote><p>如上说明，非阻塞connect之后，如果返回值是EINPROGRESS，那么需要用select或者epoll监听可写事件，然后，使用getsockopt来获取是否有错误，如果没有错误，说明连接建立成功，否则，连接建立失败。当然，如果返回值是0，说明在非阻塞的connect返回时，连接已经建立成功，这时候，跟处理阻塞式的connect是一样的。</p><p>整个非阻塞connect的实现在<a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/tcp_client.cpp">tcp_client</a>，其中非阻塞connect调用是connect中完成。</p><h2 id="read"><a href="#read" class="headerlink" title="read"></a>read</h2><p>read的实现与server端的read一致，这里就不再赘述了。</p><h2 id="write"><a href="#write" class="headerlink" title="write"></a>write</h2><p>write的实现与server端的不同，需要根据不同情况来做不同的处理：</p><ul><li>如果连接是处于连接中的状态，那么收到可写事件时，需要通过getsockopt函数来检查连接是否正确建立，并设置连接为已连接状态</li><li>如果连接是已连接的状态，那么收到可写事件时，则会尝试调用write往内核buffer中写数据</li></ul><p>自己动手写分布式KV存储引擎的前三篇描述了如何设计和实现网络框架，接下来的文章将会关注如何基于网络框架设计和实现RPC库，敬请期待。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://book.douban.com/subject/1500149/">Unix网络编程</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;自己动手写分布式KV存储引擎系列文章的目标是记录基于LevelDB(RockDB)构建一个分布式KV存储引擎实现过程，此系列文章对应的源码在
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="网络编程" scheme="http://yoursite.com/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    
      <category term="C++" scheme="http://yoursite.com/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>自己动手写分布式KV存储引擎（二）：网络框架中的定时器原理和实现</title>
    <link href="http://yoursite.com/distributed/network-framework-timers/"/>
    <id>http://yoursite.com/distributed/network-framework-timers/</id>
    <published>2016-10-16T13:54:07.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>自己动手写分布式KV存储引擎系列文章的目标是记录基于LevelDB(RockDB)构建一个分布式KV存储引擎实现过程，此系列文章对应的源码在<a href="https://github.com/Charles0429/dstore">DSTORE</a>。</p><p>本文主要分析了网络框架中定时器的原理及实现，全文分为如下两部分</p><ul><li>定时器的功能设计</li><li>定时器的功能实现</li></ul><p>本系列的其他文章还包括：</p><ul><li><a href="http://oserror.com/distributed/implement-network-framework-using-C/">自己动手写分布式KV存储引擎（一）：设计和实现网络框架</a></li></ul><h1 id="定时器的功能设计"><a href="#定时器的功能设计" class="headerlink" title="定时器的功能设计"></a>定时器的功能设计</h1><p>本节主要分析了服务端程序对定时器的需求，定时器的算法选择。</p><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>在服务端编程中，很多地方会使用到定时器，例如：</p><ul><li>服务端定时发送心跳，证明自身在线</li><li>服务器检测到某些连接在一定时间内不活跃后，可以关闭其连接</li></ul><p>这些功能都是使用频率非常高的功能，其性能的好坏与定时器本身的性能好坏密不可分，因此，实现一个高性能的定时器是非常有必要的。接下来分析影响定时器性能最重要的因素：定时器的算法选择。</p><h2 id="定时器算法选择"><a href="#定时器算法选择" class="headerlink" title="定时器算法选择"></a>定时器算法选择</h2><p>在一个网络框架中，对定时器的操作主要包括：</p><ul><li>插入定时器，记做INSERT</li><li>更新定时器的超时时间，维持定时器超时时间按照从小到大的顺序，记做UPDATE</li><li>按照超时时间获取下一个定时器，记做GET</li><li>删除定时器，记做DELETE</li></ul><p>以维持心跳功能为例，讨论定时器功能采用何种算法才能获得高性能。</p><p>一个典型的连接之间维持心跳的事件发生序列如下：</p><ol><li>建立连接，设置定时器，并注册到网络框架中，即INSERT操作</li><li>网络框架在定时器到期时，需要从其维护的定时器池中取出来，调用相应的处理接口，然后，更新定时器的超时时间，维持定时器池中按照定时器超时时间，即GET+UPDATE操作</li><li>在链接关闭前，重复步骤2多次，记做次数为K</li><li>关闭连接，记做DELETE操作</li></ol><p>即从连接开始到连接关闭，整个发生的定时器事件为1 INSERT + K GET + K UPDATE + 1 DELETE。</p><h3 id="算法选择"><a href="#算法选择" class="headerlink" title="算法选择"></a>算法选择</h3><h4 id="普通链表"><a href="#普通链表" class="headerlink" title="普通链表"></a>普通链表</h4><ul><li>INSERT：插入到链表尾部，算法时间复杂度O(1)</li><li>UPDATE：就地更新，算法时间复杂度O(1)</li><li>GET : 需要遍历链表，获取超时时间最小的定时器，算法事件复杂度O(N)</li><li>DELETE : 采用双向链表的话，删除任意一个节点的时间复杂度为O(1)</li></ul><p>因此，采用普通链表，上述事件整体时间复杂度为O(1) + K * O(N) + K * O(1) + O(1)，即K * O(N)</p><h4 id="有序链表"><a href="#有序链表" class="headerlink" title="有序链表"></a>有序链表</h4><ul><li>INSERT : 插入到链表合适位置，保持有序性质，算法时间复杂度为O(N)</li><li>UPDATE : 更新超时时间后，需要把其插入到链表合适位置，保持有序性质，算法时间复杂度为O(N)</li><li>GET : 获取下一个超时时间最小的定时器，由于是排序链表，算法事件复杂度为O(1)</li><li>DELETE : 采用双向链表的话，删除任意一个节点的时间复杂度为O(1)</li></ul><p>因此，采用排序链表，上述事件整体时间复杂度为O(N) + K * O(N) + K * O(1) + O(1)，即(K+1) * O(N)</p><h4 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h4><ul><li>INSERT : 插入一个元素入堆，保持堆的性质，算法时间复杂度为O(lgN)</li><li>UPDATE : 更新超时时间后，需要维持堆的性质，算法时间复杂度为O(lgN)</li><li>GET : 由于每次更新和插入都保持了堆的性质，因此，此操作算法时间复杂度为O(1)</li><li>DELETE : 删除元素后，也需要保持堆的性质，算法时间复杂度为O(lgN)</li></ul><p>因此，采用堆，上述事件整体时间复杂度为O(lgN) + K * O(1) + K * O(lgN) + O(lgN)，即(K+2) * O(lgN)。</p><h4 id="排序树"><a href="#排序树" class="headerlink" title="排序树"></a>排序树</h4><ul><li>INSERT : 插入一个元素，保持排序树的性质，算法时间复杂度为O(lgN)</li><li>UPDATE : 更新超时事件后，需要维持排序树的性质，算法时间复杂度为O(lgN)</li><li>GET : 需要找到最左叶子节点，算法时间复杂度为O(lgN)</li><li>DELETE : 删除元素后，也需要保持排序树的性质，算法时间复杂度为O(lgN)</li></ul><p>因此，采用排序树，上述整体时间复杂度为O(lgN) + K * O(lgN) + K * O(lgN) + O(lgn)，即(2K+2) * O(lgN)。因为普通的排序树可能会导致不平衡，使得时间复杂度恶化到O(N)，因此，上述分析假定采用的是平衡树的方法，对于平衡树，其更新等操作的对应时间复杂度的常量因子往往是大于堆的。</p><p>考虑上述算法的时间复杂度，可以看出采用堆的算法时间复杂度最小，因此，本文中的定时器采用堆来实现。</p><h1 id="定时器的功能实现"><a href="#定时器的功能实现" class="headerlink" title="定时器的功能实现"></a>定时器的功能实现</h1><p>由于采用堆实现定时器，先来描述一下堆的实现原理。</p><h2 id="堆原理"><a href="#堆原理" class="headerlink" title="堆原理"></a>堆原理</h2><p>堆分为大根堆和小根堆，由于本文中的定时器按照超时时间升序排列，所以，以小根堆为例描述堆的基本原理。</p><p>首先来看堆的定义，引用自<a href="https://zh.wikipedia.org/wiki/%E5%A0%86_(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84">维基百科-堆</a></p><blockquote><p>n个元素序列{k1,k2…ki…kn},当且仅当满足下列关系时称之为堆：<br>(ki &lt;= k2i,ki &lt;= k2i+1)或者(ki &gt;= k2i,ki &gt;= k2i+1), (i = 1,2,3,4…n/2)</p></blockquote><p>前面括号中描述的即小根堆，即把堆看成一棵二叉树，则小根堆保证父节点的值要小于或等于子节点的值。</p><p>堆有以下特性：</p><ul><li>任意节点小于（或大于）它的所有子节点，最小元（或最大元）在堆的根上（堆序性）。</li><li>堆总是一棵完全树。即除了最底层，其他层的节点都被元素填满，且最底层尽可能地从左到右填入</li></ul><p>一般地，在堆上支持以下几种操作：</p><ul><li>build：将数组构造成堆</li><li>insert：向堆中插入一个元素</li><li>update：更新堆元素的值</li><li>get：获取堆顶元素的值</li><li>delete：删除堆中任意元素</li></ul><p>上述的操作都依赖于堆调整操作，分为向下调整和向上调整，调整的目的是为了在所调整的树路径上，维持堆的性质。</p><h3 id="向下调整"><a href="#向下调整" class="headerlink" title="向下调整"></a>向下调整</h3><p><img src="http://oserror.com/images/heap_down_adjust.png" alt="向下调整"></p><p>如上图所示，从节点值为8的节点开始调整，首先，找出8的子节点中，较小者，即3。然后，如果较小的子节点(3)，小于父节点(8)，那么，则将父子节点交换。最后，再从交换后的节点开始，直到到达叶子节点或者父节点比两个子节点都大。</p><h3 id="向上调整"><a href="#向上调整" class="headerlink" title="向上调整"></a>向上调整</h3><p><img src="http://oserror.com/images/heap_up_adjust.png" alt="向上调整"></p><p>如上图所示，从节点0开始调整，如果其父节点大于它，则互相交换，即途中的交换0和3；继续在交换后的位置开始，节点0和父节点1比较，父节点1大于子节点0，因此，继续交换，最终，得到调整后的堆。</p><p>堆支持的几种操作都能通过上述的向上调整和向下调整来实现。</p><h3 id="build"><a href="#build" class="headerlink" title="build"></a>build</h3><p>因为堆是一棵完全树，所以，一般采用数组的方式来实现它。假设数组为array，其长度为n，节点下标为(0…n-1)</p><p>从最后一个非叶子节点开始采用向下调整的方法，保证其下的子树维持堆性质；然后不断地循环此过程，直到达到下标为0的节点。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = n/<span class="number">2</span>; i &gt;=　<span class="number">0</span>; i--) &#123;</span><br><span class="line"><span class="built_in">down_heap</span>(array, i, n);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="insert"><a href="#insert" class="headerlink" title="insert"></a>insert</h3><p>把插入的元素放到数组最后，这样，它的插入可能会引起其所在父节点不满足堆性质，进行相应的调整，这和向上调整的过程是一致的，即</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">array.<span class="built_in">push_back</span>(element);</span><br><span class="line"><span class="built_in">up_heap</span>(array, element_index);</span><br></pre></td></tr></table></figure><h3 id="update"><a href="#update" class="headerlink" title="update"></a>update</h3><p>更新堆元素的值，分为两种情况：</p><ul><li>第一种是更新后，使得其比父节点小，那么，需要采用向上调整</li><li>第二种是更新后，使得其比子节点中的某个小，那么，需要采用向下调整</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">array[update_index] = new_value;</span><br><span class="line"><span class="keyword">if</span> (array[update_index] &lt; array[update_paraent_index]) &#123;</span><br><span class="line"><span class="built_in">up_heap</span>(array, update_index);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="built_in">down_heap</span>(array, update_index, array.<span class="built_in">size</span>());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="get"><a href="#get" class="headerlink" title="get"></a>get</h3><p>由堆的性质可以得出，堆顶元素是最小的，因此，只要返回数组的第一个元素。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> array[<span class="number">0</span>];</span><br></pre></td></tr></table></figure><h3 id="delete"><a href="#delete" class="headerlink" title="delete"></a>delete</h3><p>可以把该元素对应的值改成最小，然后，根据堆的特性，调整后其会到堆顶，然后，再将堆顶的元素和堆最末元素交换，重新调整堆。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">array[update_index] = <span class="number">-1</span>;</span><br><span class="line"><span class="built_in">up_heap</span>(array, update_index);</span><br><span class="line"><span class="built_in">swap</span>(array, <span class="number">0</span>, array.<span class="built_in">size</span>()<span class="number">-1</span>);</span><br><span class="line"><span class="built_in">down_heap</span>(array, <span class="number">0</span>, array.<span class="built_in">size</span>()<span class="number">-1</span>);</span><br></pre></td></tr></table></figure><h2 id="定时器实现"><a href="#定时器实现" class="headerlink" title="定时器实现"></a>定时器实现</h2><p>定时器支持的功能如下：</p><ul><li>插入定时器</li><li>删除定时器</li><li>获取所有超时的定时器</li><li>更新定时器的超时时间</li></ul><p>在实现定时器时，首先想到的是采用STL提供的接口，但查看文档后发现STL只支持从堆顶删除元素，不支持从堆的中间删除元素，因此，本文的定时器是在std::vector基础上，自己实现的堆。</p><h3 id="插入定时器"><a href="#插入定时器" class="headerlink" title="插入定时器"></a>插入定时器</h3><p>首先，先把定时器放到数组最末，然后，采用向上调整的方法使得堆性质保持，伪代码如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">EventLoop::push_timer_heap</span><span class="params">(Event *e)</span> </span></span><br><span class="line"><span class="function"></span>&#123;                   </span><br><span class="line">  e-&gt;index = timer_heap_.<span class="built_in">size</span>();</span><br><span class="line">  e-&gt;timeout += <span class="built_in">get_milliseconds</span>();</span><br><span class="line">  timer_heap_.<span class="built_in">push_back</span>(e);</span><br><span class="line">  <span class="built_in">adjust_timer_heap</span>(e-&gt;index, timer_heap_.<span class="built_in">size</span>(), timer_heap_);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="删除定时器"><a href="#删除定时器" class="headerlink" title="删除定时器"></a>删除定时器</h3><p>把相应的定时器的超时时间改成负数，然后采用向上调整的方法，把该定时器调整到堆顶；然后，交换堆顶和堆尾元素，再采用向下调整的方法调整第一个元素到倒数第二个元素之间的序列，使其维持堆性质，最后，删除掉最后一个元素，伪代码如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">EventLoop::pop_timer_heap</span><span class="params">(<span class="keyword">void</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123;                </span><br><span class="line">  <span class="built_in">swap_timer_event</span>(<span class="number">0</span>, timer_heap_.<span class="built_in">size</span>() - <span class="number">1</span>);</span><br><span class="line">  <span class="built_in">adjust_timer_heap</span>(<span class="number">0</span>, timer_heap_.<span class="built_in">size</span>() - <span class="number">1</span>, timer_heap_);</span><br><span class="line">  timer_heap_.<span class="built_in">pop_back</span>();</span><br><span class="line">&#125;                          </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">EventLoop::remove_timer_heap</span><span class="params">(<span class="keyword">const</span> Event *e)</span></span></span><br><span class="line"><span class="function"></span>&#123;                </span><br><span class="line">  timer_heap_[e-&gt;index]-&gt;timeout = <span class="number">-1</span>;</span><br><span class="line">  <span class="built_in">adjust_timer_heap</span>(e-&gt;index, timer_heap_.<span class="built_in">size</span>(), timer_heap_);</span><br><span class="line">  <span class="built_in">pop_timer_heap</span>();</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><h3 id="获取所有超时的定时器"><a href="#获取所有超时的定时器" class="headerlink" title="获取所有超时的定时器"></a>获取所有超时的定时器</h3><p>从堆顶获取元素，比较超时值和当前时间的关系，分为两种情况：</p><ol><li>如果超时值小于或等于当前时间，那么，说明定时器已经超时，调用它的处理函数，然后，采用pop_timer_heap将其弹出，并继续比较超时时间与当前时间的关系，如果满足小于或等于，则重复步骤1，否则跳转到步骤2</li><li>如果超时值大于当前时间，那么，说明最小的定时器超时时间还未到，则退出处理</li></ol><p>伪代码如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">EventLoop::process_timeout_events</span><span class="params">(<span class="keyword">void</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123; </span><br><span class="line">  <span class="keyword">const</span> <span class="keyword">int64_t</span> now = <span class="built_in">get_milliseconds</span>();</span><br><span class="line">  Event *e = <span class="built_in">top_timer_heap</span>();</span><br><span class="line">  <span class="keyword">while</span> (e != <span class="literal">nullptr</span> &amp;&amp; e-&gt;timeout &lt;= now) &#123;</span><br><span class="line">    e-&gt;<span class="built_in">timer_cb</span>(e-&gt;fd, Event::kEventTimer, e-&gt;args);</span><br><span class="line">    <span class="built_in">pop_timer_heap</span>();</span><br><span class="line">    e = <span class="built_in">top_timer_heap</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><h3 id="更新定时器超时时间"><a href="#更新定时器超时时间" class="headerlink" title="更新定时器超时时间"></a>更新定时器超时时间</h3><p>更新定时器的超时时间，分为两种情况：</p><ul><li>如果更新的值小于其父节点的值，那么采用向上调整</li><li>如果更新的值大于其父节点的值，那么采用向下调整</li></ul><p>为了能用定时器直接获得对应的堆数组的下标，每个定时器事件中都保存了其在堆数组的下标，避免了为了查询堆数组下标而需要遍历数组而带来的性能开销，伪代码如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">EventLoop::process_timeout_events</span><span class="params">(<span class="keyword">void</span>)</span></span></span><br><span class="line"><span class="function"></span>&#123; </span><br><span class="line">  <span class="keyword">const</span> <span class="keyword">int64_t</span> now = <span class="built_in">get_milliseconds</span>();</span><br><span class="line">  Event *e = <span class="built_in">top_timer_heap</span>();</span><br><span class="line">  <span class="keyword">while</span> (e != <span class="literal">nullptr</span> &amp;&amp; e-&gt;timeout &lt;= now) &#123;</span><br><span class="line">    e-&gt;<span class="built_in">timer_cb</span>(e-&gt;fd, Event::kEventTimer, e-&gt;args);</span><br><span class="line">    <span class="built_in">pop_timer_heap</span>();</span><br><span class="line">    e = <span class="built_in">top_timer_heap</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>上述所有的操作都是基于二叉堆来实现的，从堆的操作来看，其在数组中操作的元素相隔较远(父子节点的下标都是2倍关系)，因此，二叉堆对cache并不友好。在libev中采用四叉堆来缓解上述问题，据其代码中描述，四叉堆在5w+的定时器下，能获得5%左右的性能提升，原文如下</p><blockquote><p>at the moment we allow libev the luxury of two heaps,<br>a small-code-size 2-heap one and a ~1.5kb larger 4-heap<br>which is more cache-efficient.<br>the difference is about 5% with 50000+ watchers.</p></blockquote><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li>The Algorithm Design Manual</li><li>STL heap document</li><li><a href="https://zh.wikipedia.org/wiki/%E5%A0%86_(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84">维基百科-堆</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;引言&quot;&gt;&lt;a href=&quot;#引言&quot; class=&quot;headerlink&quot; title=&quot;引言&quot;&gt;&lt;/a&gt;引言&lt;/h1&gt;&lt;p&gt;自己动手写分布式KV存储引擎系列文章的目标是记录基于LevelDB(RockDB)构建一个分布式KV存储引擎实现过程，此系列文章对应的源码在
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
    
      <category term="高性能" scheme="http://yoursite.com/tags/%E9%AB%98%E6%80%A7%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>raft原理（三）：日志合并和客户端交互</title>
    <link href="http://yoursite.com/distributed/raft-principle-three/"/>
    <id>http://yoursite.com/distributed/raft-principle-three/</id>
    <published>2016-10-09T02:05:28.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h1><p>本文收录在<a href="https://github.com/Charles0429/papers">paper</a>项目中，papers项目旨在学习和总结分布式系统相关的论文；同时本文也是<a href="https://github.com/Charles0429/dstore">DSTORE</a>项目的必备知识，DSTORE的目标是自己动手实现一个分布式KV存储引擎。</p><p>本文为raft系列文章第三篇，本系列其他文章为</p><ul><li><a href="http://oserror.com/distributed/raft-principle-one/">raft原理（一）：选举和日志复制</a></li><li><a href="http://oserror.com/distributed/raft-principle-two/">raft原理（二）：安全性和集群成员变更</a></li><li>golang实现raft（一）：选举和日志同步</li><li>golang实现raft（二）：集群成员变更</li></ul><p>本文的组织结构如下</p><ul><li>Log compaction</li><li>Client interaction</li><li>Evaluation</li></ul><h1 id="Log-compaction"><a href="#Log-compaction" class="headerlink" title="Log compaction"></a>Log compaction</h1><p>Raft的日志会随着处理客户端请求数量的增多而不断增大，在实际系统中，日志不可能会无限地增长，原因如下：</p><ul><li>占用的存储空间随着日志增多而增加</li><li>日志越多，server当掉重启时需要回放的时间就越长</li></ul><p>因此，需要定期地清理日志，Raft采用最简单的快照方法。对系统当前做快照时，会把当前状态持久化到存储中，然后到快照点的日志项都可以被删除。</p><p><img src="http://oserror.com/images/raft_log_compaction.png" alt="raft log compaction"></p><p>Raft算法中每个server单独地做快照，即把当前状态机的状态写入到存储中（状态机中的状态都是已提交的log entry回放出来的）。除了状态机的状态外，Raft快照中还需要一些元数据信息，包括如下：</p><ul><li>快照中包含的最后一个log entry的index和term，记录这些信息的目的是为了使得<code>AppendEntries</code>RPC的一致性检查能通过，因为，在复制紧跟着快照后的log entry时，<code>AppendEntries</code> RPC带上需要复制的log entry前一个log entry的(index, iterm)，即快照的最后一个log entry的(index，term)，因此，快照中需要记录最后一个log entry的(index，term)</li><li>为了支持集群成员变更，快照中保存的元数据还会存储集群最新的配置信息。</li></ul><p>当server完成快照后，可以删除快照最后一个log entry及其之前所有的log entry，以及之前的快照。</p><p>虽然每个server是独立地做快照的，但是也有可能存在需要leader向follower发送整个快照的情况，例如，一个follower的日志处于leader的最近一次快照之前，恰好leader做完快照之后把其快照中的log entry都删除了，这时，leader就无法通过发送log entry来同步了，只能通过发送完整快照。</p><p>leader通过<code>InstallSnapshot</code> RPC来完成发送快照的功能，follower收到此RPC后，根据不同情况会有不同的处理：</p><p><strong>当follower中缺失快照中的日志时</strong></p><ul><li>follower会删除掉其上所有日志，并清空状态机</li></ul><p><strong>当follower中拥有快照中所有的日志时</strong></p><ul><li>follower会删掉快照所覆盖的log entry，但快照后所有日志都保留。备注：这里论文中没有提是否还是从leader接受快照，个人觉得follower可以自己做快照，并拒绝掉leader发快照的RPC请求</li></ul><p>对于Raft快照，关于性能需要考虑的点有：</p><ul><li>server何时做快照，太频繁地做快照会浪费磁盘I/O；太不频繁会导致server当掉后回放时间增加，可能的方案为当日志大小到一定空间时，开始快照。备注：如果所有server做快照的阈值空间都是一样的，那么快照点也不一定相同，因为，当server检测到日志超过大小，到其真正开始做快照中间还存在时间间隔，每个server的间隔可能不一样</li><li>写快照花费的时间很长，不能让其影响正常的操作。可以采用copy-on-write操作，例如linux的fork</li></ul><h1 id="Client-Interaction"><a href="#Client-Interaction" class="headerlink" title="Client Interaction"></a>Client Interaction</h1><p>Raft的client会把所有的请求发到leader上执行，在client刚启动时，会随机选择集群中的一个server</p><ul><li>如果选择的server是leader，那么client会把请求发到该server上</li><li>如果选择的server不是leader，该server会把leader的地址告诉给client，后续client会把请求发给该leader</li><li>如果此时没有leader，那么client会timeout，client会重试其他server，直到找到leader</li></ul><p>Raft的目标使得client是linerizable的，即每个操作几乎是瞬间的，在其调用到返回结果的某个时间点，执行其执行一次。由于需要client的请求正好执行一次，这就需要client的配合，当leader挂掉之后，client需要重试其请求，因为有可能leader挂掉之前请求还没有成功执行。但是，也有可能leader挂掉之前，client的请求已经执行完成了，这时候就需要新leader能识别出该请求已经执行过，并返回之前执行的结果。可以通过为client的每个请求分配唯一的编号，当leader检测到请求没有执行过时，则执行它；如果执行过，则返回之前的结果。</p><p>只读的请求可以不写log就能执行，但是它有可能返回过期的数据，有如下场景：</p><ul><li>老的leader挂掉了，但它自身还认为自己是leader，于是client发读请求到该server时，可能获得的是老数据</li></ul><p>Raft通过如下方法避免上述问题：</p><ul><li>leader需要自己知道哪些log entry是已经提交的，在正常情况下，leader一直是有已提交过的log entry的，但是，在leader刚当选的时候，需要当场获取，可以通过提交一个空的log entry来获取已提交过的log entry（备注：个人理解是为了避免commiting from previous leader那种情况）</li><li>leader在执行只读请求时，需要确定自己是否还是leader，通过和大多数的server发送heartbeat消息，来确定自己是leader，然后再决定是否执行该请求</li></ul><h1 id="Evaluation"><a href="#Evaluation" class="headerlink" title="Evaluation"></a>Evaluation</h1><p>Raft的性能测试配置如下：</p><ul><li>broadcast time = 15ms</li><li>5 servers</li><li>图中的时间范围是electionTimeout的随机范围</li></ul><p><img src="http://oserror.com/images/raft_performance.png" alt="performance"></p><p>从上图的第一幅图，可以看出：</p><ul><li>5ms的随机范围，可以将downtime减少到平均283ms</li><li>50ms的随机范围，可以将最坏的downtime减少到513ms</li></ul><p>从第二幅图，可以看出：</p><ul><li>可以通过减少electionTimeout来减少downtime</li><li>过小的electionTimeout可能会造成leader的心跳没有发给其他server前，其他server就开始选举了，造成不必要的leader的切换，一般建议范围为[150ms-300ms]</li></ul><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf">In Search of an Understandable Consensus Algorithm(Extended Version)</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Introduction&quot;&gt;&lt;a href=&quot;#Introduction&quot; class=&quot;headerlink&quot; title=&quot;Introduction&quot;&gt;&lt;/a&gt;Introduction&lt;/h1&gt;&lt;p&gt;本文收录在&lt;a href=&quot;https://github.c
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Raft" scheme="http://yoursite.com/tags/Raft/"/>
    
  </entry>
  
  <entry>
    <title>raft原理（二）：安全性和集群成员变更</title>
    <link href="http://yoursite.com/distributed/raft-principle-two/"/>
    <id>http://yoursite.com/distributed/raft-principle-two/</id>
    <published>2016-10-08T13:50:43.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h1><p>本文收录在<a href="https://github.com/Charles0429/papers">paper</a>项目中，papers项目旨在学习和总结分布式系统相关的论文；同时本文也是<a href="https://github.com/Charles0429/dstore">DSTORE</a>项目的必备知识，DSTORE的目标是自己动手实现一个分布式KV存储引擎。</p><p>本文为raft系列文章第二篇，本系列其他文章为</p><ul><li><a href="http://oserror.com/distributed/raft-principle-one/">raft原理（一）：选举和日志复制</a></li><li>raft原理（三）：日志合并及客户端交互</li><li>golang实现raft（一）：选举和日志同步</li><li>golang实现raft（二）：集群成员变更</li></ul><p>本文将继续讨论raft原理，包括raft的安全性和集群成员变更，全文组织结构如下</p><ul><li>Safety</li><li>Cluster Membership Changes</li></ul><h1 id="Safety"><a href="#Safety" class="headerlink" title="Safety"></a>Safety</h1><p>前面描述了raft是如何选主和复制日志的，但是没有讨论raft是如何保证所有server的状态机按照相同的顺序执行完全相同的指令的。本节将在server被选为主的限制进行补充，保证了任何term被选为leader都会包含前面所有提交过的log entry，具体地将会通过本节描述的一系列规则来阐述。</p><h2 id="Election-Restriction"><a href="#Election-Restriction" class="headerlink" title="Election Restriction"></a>Election Restriction</h2><p>在一些一致性算法中，即使一台server没有包含所有之前已提交的log entry，也能被选为主，这些算法需要把leader上缺失的日志从其他的server拷贝到leader上，这种方法会导致额外的复杂度。相对而言，raft使用一种更简单的方法，即它保证所有已提交的log entry都会在当前选举的leader上，因此，在raft算法中，日志只会从leader流向follower。</p><p>为了实现上述目标，raft在选举中会保证，一个candidate只有得到大多数的server的选票之后，才能被选为主。得到大多数的选票表明，选举它的server中至少有一个server是拥有所有已经提交的log entry的，而leader的日志至少和follower的一样新，这样就保证了leader肯定有所有已提交的log entry。</p><h2 id="Committing-entries-from-previous-terms"><a href="#Committing-entries-from-previous-terms" class="headerlink" title="Committing entries from previous terms"></a>Committing entries from previous terms</h2><p>从日志复制一节可以知道，<strong>在当前term</strong>，一个leader知道一个log entry在复制到大多数server后，其就可以被提交了。当一个leader在提交log entry之前宕机掉，后面选举出来的leader会复制该log entry，但是，一个leader不能立马对之前term的log entry是否复制到大多数server来判断其是否已被提交。</p><p><img src="http://oserror.com/images/raft_election_restriction.png" alt="raft_election_restriction"></p><p>如上图的例子，图（c）就发生了一个log entry虽然已经复制到大多数的server，但是仍然有可能被覆盖掉的可能，如图（d），整个发生的时序如下：</p><ul><li>图a中，S1被选为主，然后复制到log index为2的log entry到S2上</li><li>图b中，S1挂掉，然后S5获得了S3，S4和自身的选举，成为leader，然后，其从客户端收到了一个新的log entry(3)</li><li>图c中，S5挂掉，S1重新正常工作，又被选为主，继续复制log entry(2)，在log entry(2)被提交前，S1又挂掉</li><li>图d中，S5又重新被选为leader，然后，会把term 3的log entry覆盖到其他log index为2的log entry</li></ul><p>因此，在raft中，不会通过日志复制的个数来提交之前term的log entry，只有当前term的log entry才会通过日志副本的个数来判断，例如，图e中，如果S1在挂掉前把log entry(4)复制到了大多数的server后，就能保证之前的log entry（2）被提交了，之后S5也就不可能被选为leader了。</p><h2 id="Safety-argument"><a href="#Safety-argument" class="headerlink" title="Safety argument"></a>Safety argument</h2><p>本小节将证明已经被leader提交的log entry，在之后选举出的leader中也会存在。</p><p>以反证法来证明，假设Term T的leader T提交了一个log entry，但是此log entry没有在之后的某些term中，不妨设最小的Term U的leader U中不存在此log entry。证明如下：</p><p><img src="http://oserror.com/images/raft_safety_argument.png" alt="raft_safety_argument"></p><ol><li>提交的log entry在leader U被选为主之前已经不存在了，因为leader不会删除或覆盖自己之前的log entry；</li><li>leader T复制该log entry到大多数的server上，并且leader U获得了大多数server的选举，因此，至少有一个server（称为voter）同时复制了该log entry，并且选举U为leader，例如上图中的S3就是这样的server；</li><li>voter在选举leader U之前，从leader T获得了此log entry，因为U &gt; T，如果先选举leader U的话，则S1在term T发送给它的<code>AppendEntries</code>RPC会失败；</li><li>voter在选举leader U的时候，还存储着此log entry，因为，假设中U是最小的不存在此log entry的leader，且[T,U)之间的leader不会删除和覆盖自己的log entry且follower只会删除和leader冲突的log entry；</li><li>voter选举U为leader，说明U的日志至少是和voter一样新的，这点导致两点假设不成立；</li><li>第一，如果voter和U的最后的term相同，那么leader U则至少在最后的term的日志和voter的一样长，即包含了leader T提交的log entry；</li><li>第二，如果voter和U的最后的term不相同，那么U的必定大于voter的，则leader U必须包含term T的所有日志，因为U &gt; T；</li><li>因此，假设不成立，不可能存在term U，使得term U中的leader不包含之前leader已经提交过的log entry。</li></ol><h2 id="Follower-and-candidate-crashes"><a href="#Follower-and-candidate-crashes" class="headerlink" title="Follower and candidate crashes"></a>Follower and candidate crashes</h2><p>follower崩溃掉后，会按如下处理</p><ul><li>leader会不断给它发送选举和追加日志的RPC，直到成功</li><li>follower会忽略它已经处理过的追加日志的RPC</li></ul><h2 id="Time-and-availability"><a href="#Time-and-availability" class="headerlink" title="Time and availability"></a>Time and availability</h2><p>在raft中，election timeout的值需要满足如下条件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">broadcastTime &lt;&lt; electionTimeout &lt;&lt; MTBF</span><br></pre></td></tr></table></figure><p>其中broadcastTimeout是server并行发送给其他server RPC并收到回复的时间；electionTimeout是选举超时时间；MTBF是一台server两次故障的间隔时间。</p><p>electionTimeout要大于broadcastTimeout的原因是，防止follower因为还没收到leader的心跳，而重新选主。</p><p>electionTimeout要小于MTBF的原因是，防止选举时，能正常工作的server没有达到大多数。</p><p>对于boradcastTimeout，一般在[0.5ms,20ms]之间，而MTBF一般非常大，至少是按照月为单位。因此，一般electionTimeout一般选择范围为[10ms,500ms]。因此，当leader挂掉后，能在较短时间内重新选主。</p><h1 id="Cluster-Membership-Changes"><a href="#Cluster-Membership-Changes" class="headerlink" title="Cluster Membership Changes"></a>Cluster Membership Changes</h1><p>在集群server发生变化时，不能一次性的把所有的server配置信息从老的替换为新的，因为，每台server的替换进度是不一样的，可能会导致出现双主的情况，如下图：</p><p><img src="http://oserror.com/images/cluster_memship_wrong.png" alt="cluster_memship_wrong"></p><p>如上图，Server 1和Server 2可能以Cold配置选出一个主，而Server 3，Server 4和Server 5可能以Ｃnew选出另外一个主，导致出现双主。</p><p>raft使用两阶段的过程来完成上述转换：</p><ul><li>第一阶段，新老配置都存在，称为joint consensus</li><li>第二阶段，替换成新配置</li></ul><p><img src="http://oserror.com/images/raft_memship_right.png" alt="raft_memship_right"></p><ul><li>leader首先创建Cold,new的log entry，然后提交（保证大多数的old和大多数的new都接收到该log entry）；</li><li>leader创建Cnew的log entry，然后提交，保证大多数的new都接收到了该log entry。</li></ul><p>这个过程中，有几个问题需要考虑。</p><ul><li>新加入的server一开始没有存储任何的log entry，当它们加入到集群中，可能有很长一段时间在追加日志的过程中，导致配置变更的log entry一直无法提交</li></ul><p>Raft为此新增了一个阶段，此阶段新的server不作为选举的server，但是会从leader接受日志，当新加的server追上leader时，才开始做配置变更。</p><ul><li>原来的主可能不在新的配置中</li></ul><p>在这种场景下，原来的主在提交了Cnew log entry（计算日志副本个数时，不包含自己）后，会变成follower状态。</p><ul><li>移除的server可能会干扰新的集群</li></ul><p>移除的server不会受到新的leader的心跳，从而导致它们election timeout，然后重新开始选举，这会导致新的leader变成follower状态。Raft的解决方案是，当一台server接收到选举RPC时，如果此次接收到的时间跟leader发的心跳的时间间隔不超过最小的electionTimeout，则会拒绝掉此次选举。这个不会影响正常的选举过程，因为，每个server会在最小electionTimeout后发起选举，而可以避免老的server的干扰。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf">In Search of an Understandable Consensus Algorithm(Extended Version)</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Introduction&quot;&gt;&lt;a href=&quot;#Introduction&quot; class=&quot;headerlink&quot; title=&quot;Introduction&quot;&gt;&lt;/a&gt;Introduction&lt;/h1&gt;&lt;p&gt;本文收录在&lt;a href=&quot;https://github.c
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Raft" scheme="http://yoursite.com/tags/Raft/"/>
    
  </entry>
  
  <entry>
    <title>raft原理（一）：选主与日志复制</title>
    <link href="http://yoursite.com/distributed/raft-principle-one/"/>
    <id>http://yoursite.com/distributed/raft-principle-one/</id>
    <published>2016-10-07T14:27:27.000Z</published>
    <updated>2021-06-14T13:00:05.529Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h1><p>本文收录在<a href="https://github.com/Charles0429/papers">paper</a>项目中，papers项目旨在学习和总结分布式系统相关的论文；同时本文也是<a href="https://github.com/Charles0429/dstore">DSTORE</a>项目的必备知识，DSTORE的目标是自己动手实现一个分布式KV存储引擎。</p><p>本文为raft系列文章第一篇，本系列其他文章为</p><ul><li>raft原理（二）：安全性和集群成员变更</li><li>raft原理（三）：日志合并及客户端交互</li><li>golang实现raft（一）：选举和日志同步</li><li>golang实现raft（二）：集群成员变更</li></ul><p>本文介绍了分布式一致性算法raft的选主和日志复制原理，raft算法的主要目标是为了让分布式一致性算法更易理解和用于工程实践。</p><p>raft算法的主要特性为</p><ul><li>strong leader：raft算法使用的是strong leader方式，日志只能从leader同步到follower</li><li>leader election：使用随机定时器来选主</li><li>Membership changes：采用的是两阶段更新配置信息的方式</li></ul><p>本文的组织结构如下</p><ul><li>Replicated state machines</li><li>Strenthens and weaks of Paxos</li><li>Understandability</li><li>Raft basics</li><li>Raft leader election</li><li>Raft log replication</li></ul><h1 id="Replicated-State-Machine"><a href="#Replicated-State-Machine" class="headerlink" title="Replicated State Machine"></a>Replicated State Machine</h1><p><img src="http://oserror.com/images/replicate_state_machine.png" alt="replicate_state_machine"></p><p>Repicated State Machine一般分为三个部分：</p><ul><li>Log：记录一系列的指令</li><li>State Machine：把日志中提交的指令回放到状态机中</li><li>Consensus Module：分布式环境下，保证多机的日志是一致的，这样回放到状态机中的状态是一致的</li></ul><p>一致性算法作用于Consensus Module，一般有以下特性：</p><ul><li>safety：在非拜占庭问题下（网络延时，网络分区，丢包，重复发包以及包乱序等），结果是正确的</li><li>availability：在半数以上机器能正常工作时，则系统可用</li><li>timing-unindependent：不依赖于时钟来保证日志一致性，错误的时钟以及极端的消息时延最多会造成可用性问题</li></ul><h1 id="What’s-wrong-with-Paxos"><a href="#What’s-wrong-with-Paxos" class="headerlink" title="What’s wrong with Paxos?"></a>What’s wrong with Paxos?</h1><p>Paxos算法存在的主要问题为</p><ul><li>难以理解：对于大部分人来讲，难以理解Paxos论文</li><li>难以实现：Paxos算法在工程上实现难度高，论文中缺少必要的细节，并且整个Paxos的算法的思想决定了其不易于实现</li></ul><h1 id="Understandability"><a href="#Understandability" class="headerlink" title="Understandability"></a>Understandability</h1><p>鉴于Paxos难以理解和实现，raft的首要目标是使其易于理解，为此raft采用了以下设计思想来达到此目标：</p><ul><li>decomposition：把整个算法分为election，log replication，safety and membership changes</li><li>Simplify the state space：例如，日志不允许有空洞等等</li></ul><h1 id="Raft-basics"><a href="#Raft-basics" class="headerlink" title="Raft basics"></a>Raft basics</h1><p>Raft通过选出一个leader来简化日志副本的管理，例如，日志项(log entry)只允许从leader流向follower。</p><p>基于leader的方法，Raft算法可以分解成三个子问题：</p><ul><li>Leader election：原来的leader挂掉后，必须选出一个新的leader</li><li>Log replication：leader从客户端接收日志，并复制到整个集群中</li><li>Safety：如果有任意的server将日志项回放到状态机中了，那么其他的server只会回放相同的日志项</li></ul><h2 id="Raft-Server-States"><a href="#Raft-Server-States" class="headerlink" title="Raft Server States"></a>Raft Server States</h2><p>一个Raft集群通常有几台Raft Server组成，每个Server处于以下三种状态之一：</p><ul><li>leader：处理所有的客户端请求</li><li>follower：响应来自leader和candidate的请求</li><li>candidate：用于选主</li></ul><h2 id="Raft-Server-State-Transformation"><a href="#Raft-Server-State-Transformation" class="headerlink" title="Raft Server State Transformation"></a>Raft Server State Transformation</h2><p>Raft可能的状态变化如下图：</p><p><img src="http://oserror.com/images/raft_server_states.png" alt="raft_server_states"></p><p>Raft将时间分为多个term，term以连续的整数来标识，每个term以一次election开始，如果有server被选为leader，则该term的剩余时间该server都是leader。</p><p><img src="http://oserror.com/images/raft_terms.png" alt="raft_terms"></p><p>有些term里，可能并没有选出leader，这时候会开启一个新term来继续选主，如上图中的t3。</p><p>每个server都维护着一个当前term(current term)，有可能会存在某些server整个term都没参与的情况，这时候，在server通信的时候，会带上彼此的当前term信息，server会更新成它们之间的较大值。当leader或candidate发现它们的term属于老的值时，它们会转成follower状态。</p><p>Raft Server之间的通信通过RPC来进行，基础的raft算法只需要实现<code>RequestVote</code>和<code>AppendEntries</code>两个RPC。</p><h1 id="Raft-Leader-Election"><a href="#Raft-Leader-Election" class="headerlink" title="Raft Leader Election"></a>Raft Leader Election</h1><p>Raft使用心跳来触发选主，当server启动时，状态是follower。当server从leader或者candidate接收到合法的RPC时，它会保持在follower状态。leader会发送周期性的心跳来表明自己是leader。</p><p>当一个follower在election timeout时间内没有接收到通信，那么它会开始选主。</p><p>选主的步骤如下：</p><ul><li>增加current term</li><li>转成candidate状态</li><li>选自己为主，然后把选主RPC并行地发送给其他的server</li><li>candidate状态会继续保持，直到下述三种情况出现</li></ul><p>candidate会在下述三种情况下退出</p><ul><li>server本身成为leader</li><li>其他的server选为leader</li><li>一段时间后，没有server成为leader</li></ul><h2 id="server本身被选为leader"><a href="#server本身被选为leader" class="headerlink" title="server本身被选为leader"></a>server本身被选为leader</h2><p>当server得到集群中大多数的server的选举后，它会成为leader。对于每个server来讲，只能选举一台server为leader，从而使得大多数原则能确保只有一个candidate会被选成leader。</p><p>当candidate成为leader后，会发送心跳信息告诉其他server，从而防止新的选举。</p><h2 id="其他server选为leader"><a href="#其他server选为leader" class="headerlink" title="其他server选为leader"></a>其他server选为leader</h2><p>如果在等待选举期间，candidate接收到其他server要成为leader的RPC，分两种情况处理：</p><ul><li>如果leader的term大于或等于自身的term，那么改candidate会转成follower状态</li><li>如果leader的term小于自身的term，那么会拒绝该leader，并继续保持candidate状态</li></ul><h2 id="一段时间后，没有server成为leader"><a href="#一段时间后，没有server成为leader" class="headerlink" title="一段时间后，没有server成为leader"></a>一段时间后，没有server成为leader</h2><p>有可能，很多follower同时变成candidate，导致没有candidate能获得大多数的选举，从而导致无法选出主。当这个情况发生时，每个candidate会超时，然后重新发增加term，发起新一轮选举RPC。需要注意的是，如果没有特别处理，可能出导致无限地重复选主的情况。</p><p>Raft采用随机定时器的方法来避免上述情况，每个candidate选择一个时间间隔内的随机值，例如150-300ms，采用这种机制，一般只有一个server会进入candidate状态，然后获得大多数server的选举，最后成为主。每个candidate在收到leader的心跳信息后会重启定时器，从而避免在leader正常工作时，会发生选举的情况。</p><h1 id="Raft-Log-replication"><a href="#Raft-Log-replication" class="headerlink" title="Raft Log replication"></a>Raft Log replication</h1><p>当选出leader后，它会开始接受客户端请求，每个请求会带有一个指令，可以被回放到状态机中。leader把指令追加成一个log entry，然后通过<code>AppendEntries</code> RPC并行的发送给其他的server，当改entry被多数派server复制后，leader会把该entry回放到状态机中，然后把结果返回给客户端。</p><p>当follower宕机或者运行较慢时，leader会无限地重发<code>AppendEntries</code>给这些follower，直到所有的follower都复制了该log entry。</p><p><img src="http://oserror.com/images/raft_log_replication.png" alt="raft_log_replication"></p><p>log按照上图方式组织，每个log entry存储了指令和term信息，由leader指定。每个log entry有个数字索引(index)来表名其在log中的位置。</p><p>leader决定什么时候将一个log entry回放到状态机中是安全的，被回放的log entry称为committed，raft保证所有committed log entry会被持久化，并且最终会被回放到所有可工作的状态机中。</p><p>一个log在大多数的server已经复制它之后，则是committed（这个特指在leader的term里面的日志），在复制该log的同时，同时也会告诉已复制该log entry的follower，其之前的log entry也被提交了，follower则可以回放其之前的log entry。例如上图中的entry 7。leader会维护最大的committed的entry的index，当一个follower发现log entry已提交，则会将它回放到状态机中。</p><p>raft的log replication保证以下性质(Log Matching Property)：</p><ul><li>如果两个log entry有相同的index和term，那么它们存储相同的指令</li><li>如果两个log entry在两份不同的日志中，并且有相同的index和term，那么它们之前的log entry是完全相同的</li></ul><p>其中特性一通过以下保证：</p><ul><li>leader在一个特定的term和index下，只会创建一个log entry</li><li>log entry不会改变它们在日志中的位置</li></ul><p>特性二通过以下保证：</p><p><code>AppendEntries</code>会做log entry的一致性检查，当发送一个<code>AppendEntries</code>RPC时，leader会带上需要复制的log entry前一个log entry的(index, iterm)</p><ul><li>如果follower没有发现与它一样的log entry，那么它会拒绝接受新的log entry</li></ul><p>这样就能保证特性二得以满足。</p><p>在正常情况下，leader和follower会保持一致，一致性检查通常都会成功。但是，当leader崩溃后，可能会出现日志不一致的情况，通过一个例子来说明。</p><p><img src="http://oserror.com/images/raft_leader_follower_not_consistent.png" alt="raft_leader_follower_not_consistent"></p><p>如上图所示，raft的leader强制以自己的日志来复制不一致的日志，通过以下方法：</p><ul><li>找到leader和follower最后一个相同的log entry，然后删掉follower后面的日志，然后把该log entry之后的leader日志复制给follower</li></ul><p>上述方法是通过<code>AppendEntries</code>的一致性检查实现的，如下：</p><ul><li>leader为每个follower维护一个<em>nextIndex</em>，表明下一个将要发送给follower的log entry</li><li>当leader刚上任时，会把所有的<em>nextIndex</em>设置成其最后一个log entry的index加1，如上图，则是11</li><li>当follower的日志和leader不一致时，一致性检查会失败，那么会把<em>nextIndex</em>减1</li><li>最终<em>nextIndex</em>会是leader和follower相同log entry的index加1，这时候，再发送<code>AppendEntries</code>会成功，并且会把follower的所有之后不一致的日志删除掉</li></ul><p><strong>优化</strong></p><p>上述一次回退一个log entry的方法效率较低，在发生冲突时，可以让follower把冲突的term的第一个日志的index发回给leader，这样leader就可以一次过滤掉该term的所有log entry。</p><p>在正常情况下，log entry可以通过一轮RPC就能将日志复制到大多数的server，少数的慢follower不会影响性能。</p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf">In Search of an Understandable Consensus Algorithm(Extended Version)</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Introduction&quot;&gt;&lt;a href=&quot;#Introduction&quot; class=&quot;headerlink&quot; title=&quot;Introduction&quot;&gt;&lt;/a&gt;Introduction&lt;/h1&gt;&lt;p&gt;本文收录在&lt;a href=&quot;https://github.c
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Raft" scheme="http://yoursite.com/tags/Raft/"/>
    
  </entry>
  
  <entry>
    <title>自己动手写分布式KV存储引擎（一）：　设计和实现网络框架</title>
    <link href="http://yoursite.com/distributed/implement-network-framework-using-C/"/>
    <id>http://yoursite.com/distributed/implement-network-framework-using-C/</id>
    <published>2016-09-24T09:55:35.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h1><p>之前写过一篇博文，描述了本人学习分布式系统的思路(<a href="http://oserror.com/distributed/learning-distributed/">链接</a>)。自己动手写分布式KV存储引擎系列文章的目标是记录基于LevelDB(RockDB)构建一个分布式KV存储引擎实现过程，算是对之前学习思路的实践。初步设想，此系列文章会包含以下主题：</p><ul><li>如何设计和实现网络框架</li><li>如何设计和实现RPC库</li><li>分析LevelDB和RockDB的设计和实现原理</li><li>如何理解和实现raft/paxos算法</li><li>如何基于raft/paxos，构建强一致的分布式KV存储引擎</li><li>如何对分布式KV存储提供事务功能</li><li>如何对分布式KV存储系统优化性能</li><li>等等</li></ul><p>此系列文章对应的源码放在<a href="https://github.com/Charles0429/dstore">DSTORE</a>下。</p><p>本文为此系列第一篇文章，主要是关于如何设计和实现一个基本的网络框架，全文的组织结构如下：</p><ul><li>网络框架的要点</li><li>DSTORE网络框架的设计与实现</li></ul><h1 id="网络框架的要点"><a href="#网络框架的要点" class="headerlink" title="网络框架的要点"></a>网络框架的要点</h1><h2 id="使用TCP还是UDP"><a href="#使用TCP还是UDP" class="headerlink" title="使用TCP还是UDP?"></a>使用TCP还是UDP?</h2><p>由于TCP相对于UDP来讲，可靠性高很多，保证包的按序达到，这对于高可靠的存储系统来讲是十分必要的，因此，本文的网络框架将基于TCP来实现。</p><h2 id="操作系统的选择"><a href="#操作系统的选择" class="headerlink" title="操作系统的选择"></a>操作系统的选择</h2><p>由于目前Linux是服务端编程中主流的操作系统平台，因此，本文的网络框架将基于Linux平台，且为X86_64体系架构。</p><h2 id="Reactor-VS-Proactor"><a href="#Reactor-VS-Proactor" class="headerlink" title="Reactor VS Proactor"></a>Reactor VS Proactor</h2><p>一般Reactor模型基于I/O多路复用来实现，Linux平台提供select,epoll等接口，而Proactor模型一般基于异步I/O来实现，目前Linux系统对这块支持不太好，因此，本文的网络框架将基于Reactor来实现。</p><h2 id="线程模型"><a href="#线程模型" class="headerlink" title="线程模型"></a>线程模型</h2><p>两种常见的线程模型，一是IO线程和工作线程共用相同线程，二是IO线程和工作线程分开。</p><h3 id="I-O线程和工作线程共用"><a href="#I-O线程和工作线程共用" class="headerlink" title="I/O线程和工作线程共用"></a>I/O线程和工作线程共用</h3><p><img src="http://oserror.com/images/libeasy_thread_model_1.png" alt="thread model 1"></p><p>如上图，I/O线程和工作线程共用的线程模型中，实际上是没有专门的工作线程的，I/O线程不仅需要负责处理I/O，还需要真正地处理请求，计算结果。一般典型的处理流程为</p><ul><li>Process Read I/O: 处理读I/O</li><li>Process: 解析请求，计算结果</li><li>Process Write I/O： 处理写I/O，把计算结果返回给客户端</li></ul><p>这种线程模型的特点是</p><ul><li>处理流程相对简单，解析好请求后就能直接在同一线程处理，省去了线程切换的开销，非常适合Process耗费时间较小的请求</li><li>由于Process过程需要耗费时间，对于大任务，可能时间较长，会影响其他请求的处理</li></ul><h3 id="I-O线程和工作线程独立"><a href="#I-O线程和工作线程独立" class="headerlink" title="I/O线程和工作线程独立"></a>I/O线程和工作线程独立</h3><p><img src="http://oserror.com/images/libeasy_thread_model_2.png" alt="thread model 2"></p><p>如上图，在I/O线程和工作线程独立的线程模型中，有专门的工作线程来处理请求，计算结果，I/O线程仅仅需要做读写数据相关的操作。在这种线程模型下，整个流程为</p><ul><li>Process Read I/O:处理读数据，然后解析请求，生成任务，推送到工作线程的队列中，然后以异步事件方式通知工作线程处理</li><li>Process: 工作线程接收到异步事件后，从其工作队列中拿出任务，依次处理，处理完成后，生成结果，放到I/O线程的队列中，然后以异步事件方式通知I/O线程处理</li><li>Process Write I/O：I/O线程收到通知后，依次处理写数据请求</li></ul><p>这种线程模型的特点是</p><ul><li>I/O和计算分开处理，会引入线程切换开销，比较适合Process耗费时间长的任务请求</li><li>对于小任务请求不适合，大量时间耗费在线程切换开销</li></ul><p>对于存储系统，一般计算需求较小，因此采用第一种线程模型。</p><h2 id="I-O线程模型"><a href="#I-O线程模型" class="headerlink" title="I/O线程模型"></a>I/O线程模型</h2><p>选定好在I/O线程中处理任务之后，又需要确定I/O线程具体是如何分工的，一般有三种方式</p><ul><li>单线程做accept和I/O</li><li>单读一个线程accept,其他线程I/O</li><li>多线程，每个线程accept并且Ｉ/O</li></ul><p>主要从两个角度考虑这几个I/O模型的选择</p><ul><li>连接建立的频繁与否</li><li>I/O的吞吐量高与否</li></ul><p>对于第一种模型，比较适合连接建立不频繁的场景，在CPU使用不高的情况下，单线程也可以做到打满网络带宽</p><p>对于第二种模型，比较适合连接建立不频繁的场景，可以通过增加I/O线程的数量，来提升I/O的吞吐量</p><p>对于第三种模型，比较适合连接建立频繁的场景，可以通过增加线程的数量，来提升连接建立的速度和I/O的吞吐量</p><p>对于存储系统调用者来讲，一般会使用连接池，因此，存储系统一般不会频繁的建立连接；并且一般存储系统对I/O吞吐量需要较高，因此，选择第一种和第二种模型。本文中暂时采用第一种模型，如果在第一种模型不能提供足够的I/O带宽的情况下，考虑采用第二种模型。</p><h1 id="DSTORE网络框架设计与实现"><a href="#DSTORE网络框架设计与实现" class="headerlink" title="DSTORE网络框架设计与实现"></a>DSTORE网络框架设计与实现</h1><h2 id="网络框架需要处理的事件"><a href="#网络框架需要处理的事件" class="headerlink" title="网络框架需要处理的事件"></a>网络框架需要处理的事件</h2><p>在描述DSTORE网络框架设计之前，先分析网络框架需要处理的事件</p><h3 id="网络请求处理流程"><a href="#网络请求处理流程" class="headerlink" title="网络请求处理流程"></a>网络请求处理流程</h3><p><img src="http://oserror.com/images/network_packet_process.png" alt="handle request"></p><p>从上面的处理流程可以看出，对于Client和Server，它们需要关注的事情包括</p><ul><li>消息的解码</li><li>对消息的处理，生成响应</li><li>把响应结果根据格式编码</li></ul><p>对于网络框架层，需要关注的是</p><ul><li>读事件及读数据</li><li>写事件及写数据</li></ul><p>网络框架除了需要关注读写事件及读写数据外，还需要处理连接的建立和断开。</p><h3 id="网络连接处理流程"><a href="#网络连接处理流程" class="headerlink" title="网络连接处理流程"></a>网络连接处理流程</h3><p><img src="http://oserror.com/images/network_connect.png" alt="network connection flow"></p><p>网络连接处理流程和网络处理请求流程不太一样，在于Client和Server的处理与网络请求处理的流程不太一致，其流程如下</p><ol><li>Client发起网络框架提供的API connect来请求建立连接</li><li>Client端的网络框架记录此事件，并加入监听</li><li>Client端的操作系统把包发送给通过网卡发送到网络</li><li>Server端的操作系统读取网卡数据，通知网络框架</li><li>Server端网络框架调用accept建立连接，并调用Server的建立连接的接口</li><li>Server端的accept调用会产生一个回包</li><li>Client端的操作系统收到后，触发网络框架的事件</li><li>Client端网络框架通知Client连接已建立</li></ol><p>其中步骤5中accept返回后，其后半步骤与步骤6是并发的，并没有严格的顺序。</p><p>从上述流程可以看出，对于网络连接的建立，Server端和Client端处理调用网络框架的API之外，几乎不需要额外的处理。</p><p>而对于网络框架来讲，在Client和Server端的处理流程是不同的，分别是</p><ul><li>在Client端，网络框架需要调用操作系统提供的connect接口，并且，监听connect完成的事件</li><li>在Server端，网络框架需要调用操作系统提供的accept接口，并且，需要触发server的新建连接的接口(这个连接维护也可以在网络框架中处理)</li></ul><p>网络请求完成后，需要正确地关闭连接，其处理流程如下。</p><h3 id="网络连接关闭流程"><a href="#网络连接关闭流程" class="headerlink" title="网络连接关闭流程"></a>网络连接关闭流程</h3><p><img src="http://oserror.com/images/network_close.png" alt="network close"></p><p>如上图，对于Client端，需要处理的主要是调用网络框架的close API；对于Server端，则需要处理其上维护的连接结构体等等。</p><p>对于网络框架，需要处理的是</p><ul><li>Client端调用close</li><li>Server端需要监听close，然后触发Server端处理(也可以在网络框架中处理)</li></ul><h3 id="网络框架需要处理事件"><a href="#网络框架需要处理事件" class="headerlink" title="网络框架需要处理事件"></a>网络框架需要处理事件</h3><p>通过上面的分析，可以总结出网络框架应该处理以下事件</p><ul><li>处理连接的建立</li><li>处理连接的关闭</li><li>处理读事件和读数据</li><li>处理写事件和写数据</li></ul><p>备注：此文写作时，Client端的网络框架尚未实现。</p><h2 id="DSTORE网络框架设计"><a href="#DSTORE网络框架设计" class="headerlink" title="DSTORE网络框架设计"></a>DSTORE网络框架设计</h2><p>本网络框架的目标是使得Server端和Client端编程时，只需要以下事件</p><p><strong>Server端需要关注的事件</strong></p><ul><li>请求编码</li><li>请求解码</li><li>处理</li></ul><p><strong>Client端需要关注的事件</strong></p><ul><li>请求编码</li><li>请求解码</li><li>处理</li><li>主动调用close关闭连接</li></ul><p>其他的一律由网络框架部分来处理，网络框架的整体框架如下</p><p><img src="http://oserror.com/images/network_framework_architecture.png" alt="network framework architecture"></p><p>网框框架整体包含两部分：</p><ul><li>Reactor负责监听读写事件</li><li>Connection Management负责根据读写事件，来建立连接，关闭连接，读数据和写数据</li></ul><h3 id="Reactor"><a href="#Reactor" class="headerlink" title="Reactor"></a>Reactor</h3><p>一个reactor模式如下图：</p><p><img src="http://oserror.com/images/reactor_model.png" alt="reactor"></p><p>Reactor中组件包括Reactor，EventHandler，I/O multiplexing和Timer</p><ul><li>EventHandler是事件的接口，一般分为I/O事件、定时器事件等等</li><li>I/O multiplexing即I/O多路复用，linux中一般采用epoll接口</li><li>Timer是管理定时器的类，主要负责注册事件、获取超时事件列表等等，一般由网络框架开发者实现</li><li>Reactor中使用了I/O multiplexing和Timer，有EventHandler注册时，会调用相应的接口。Reactor的HandleEvents中需要先调用I/O multiplexing和Timer的接口，获取已就绪好的事件，最终调用每个EventHandler的HandleEvent接口来处理事件</li></ul><p>本文写作时，DSTORE的网络框架还没有实现定时器相关的功能。</p><h3 id="Connection-Management"><a href="#Connection-Management" class="headerlink" title="Connection Management"></a>Connection Management</h3><p>Connection Management主要需要处理如下</p><ul><li>on_read：读事件触发后，读取缓冲区的数据</li><li>on_write：缓冲区空闲后，写入应用所请求要写入的数据</li><li>on_connect：连接建立后，维护连接所必须的结构体和资源</li><li>on_close：连接关闭后，清理连接所必须的结构体和资源</li></ul><h2 id="DSTORE网络框架实现"><a href="#DSTORE网络框架实现" class="headerlink" title="DSTORE网络框架实现"></a>DSTORE网络框架实现</h2><p>本部分主要描述Reactor和Connection Management部分的实现。</p><h3 id="Reactor实现"><a href="#Reactor实现" class="headerlink" title="Reactor实现"></a>Reactor实现</h3><p><img src="http://oserror.com/images/reactor_implementation.png" alt="Reactor"></p><ul><li>EventLoop：对应于Reactor，调用epoll或select的接口</li><li>Event：每个文件描述符对应一个事件，read_cb处理读事件,write_cb处理写事件</li><li>EventPollAPI：I/O多路复用的接口，可以有epoll，select，poll和kqueue等多种实现，本文写作时只封装了epoll的接口</li></ul><p><strong>源码链接</strong></p><ul><li><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/event_loop.cpp">EventLoop and Event</a></li><li><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/epoll.cpp">Epoll</a></li></ul><h3 id="Connection-Management实现"><a href="#Connection-Management实现" class="headerlink" title="Connection Management实现"></a>Connection Management实现</h3><p><img src="http://oserror.com/images/connection_management_implementation.png" alt="connection management"></p><h4 id="TCPServer"><a href="#TCPServer" class="headerlink" title="TCPServer"></a>TCPServer</h4><p>TCPServer中维护了所有连接的hashmap，用来保存Client端和Server端所有建立的连接情况。</p><p>实现了连接管理中的四种功能：</p><ul><li>读数据</li><li>写数据</li><li>连接建立</li><li>连接关闭</li></ul><p>其中读数据和写数据依赖于EventLoop中每个Event的读事件和写事件的触发</p><p><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/tcp_server.cpp">源码</a></p><h4 id="TCPListener"><a href="#TCPListener" class="headerlink" title="TCPListener"></a>TCPListener</h4><p>TCPListener是用来处理accept相关的事件的，包括服务端socket从创建到listen的全过程，以及accept调用的支持。TCPListener调用accept之后，会触发TCPServer中的on_connect事件。</p><p><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/tcp_listener.cpp">源码</a></p><h4 id="Connection"><a href="#Connection" class="headerlink" title="Connection"></a>Connection</h4><p>Connection代表了Client与Server端的连接，每个连接上可能会收到客户端的多个请求，其使用链表来维护尚未处理的请求。</p><p><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/connection.cpp">源码</a></p><h4 id="Message"><a href="#Message" class="headerlink" title="Message"></a>Message</h4><p>Message代表来自Client的一个完整的消息，Server根据消息中的指定的操作，来进行相应的处理。</p><p><a href="https://github.com/Charles0429/dstore/blob/master/dstore/network/message.cpp">源码</a></p><h2 id="使用例子"><a href="#使用例子" class="headerlink" title="使用例子"></a>使用例子</h2><p>一个简单的使用例子请参照<a href="https://github.com/Charles0429/dstore/blob/master/dstore/test/simple_packet_test.cpp">simple_packet_test.cpp</a></p><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p><h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ul><li><a href="https://github.com/Charles0429/dstore">DSTORE源码</a></li><li><a href="http://oserror.com/backend/libev-analysis/">libev设计与实现</a></li><li><a href="http://oserror.com/backend/libeasy-server-side-framework/">libeasy实现原理</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;headerlink&quot; title=&quot;介绍&quot;&gt;&lt;/a&gt;介绍&lt;/h1&gt;&lt;p&gt;之前写过一篇博文，描述了本人学习分布式系统的思路(&lt;a href=&quot;http://oserror.com/distributed/learn
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="分布式" scheme="http://yoursite.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="网络编程" scheme="http://yoursite.com/tags/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/"/>
    
      <category term="C++" scheme="http://yoursite.com/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>gfs原理</title>
    <link href="http://yoursite.com/distributed/gfs/"/>
    <id>http://yoursite.com/distributed/gfs/</id>
    <published>2016-09-11T07:50:06.000Z</published>
    <updated>2021-06-14T13:00:05.525Z</updated>
    
    <content type="html"><![CDATA[<h1 id="1-Introduction"><a href="#1-Introduction" class="headerlink" title="1. Introduction"></a>1. Introduction</h1><p>本文是读GFS论文的总结，收录在我的github中<a href="https://github.com/Charles0429/papers">papers项目</a>，papers项目旨在学习和总结分布式系统相关的论文。</p><p>全文主要分为以下几方面：</p><ul><li>Design Motivation</li><li>Architecture</li><li>System Interactions</li><li>Master Operation</li><li>Fault Tolerance and Diagnose</li><li>Discussion</li></ul><h1 id="2-Design-Motivation"><a href="#2-Design-Motivation" class="headerlink" title="2. Design Motivation"></a>2. Design Motivation</h1><p>google对现有系统的运行状态以及应用系统进行总结，抽象出对文件系统的需求，主要分为以下几个方面。</p><ul><li>普通商用的机器硬件发生故障是常态</li><li>存储的问题普遍比较大，几个G的文件很常见</li><li>大部分的文件操作都是在追加数据，覆盖原来写入的数据的情况比较少见，随机写几乎不存在</li><li>读操作主要包括两种，large streaming read和small random read</li><li>为了应用使用方便，多客户端并行地追加同一个文件需要非常高效</li><li>带宽的重要性大于时延，目标应用是高速读大块数据的应用，对响应时间没有过多的需求</li></ul><h1 id="3-Architecture"><a href="#3-Architecture" class="headerlink" title="3. Architecture"></a>3. Architecture</h1><p>本部分讨论gfs的总体架构，以及在此架构上需要考虑的一些问题。</p><h2 id="3-1-Overview"><a href="#3-1-Overview" class="headerlink" title="3.1 Overview"></a>3.1 Overview</h2><p>GFS的整体架构如下图：</p><p><img src="http://oserror.com/images/gfs_architecture.png" alt="gfs architecture"></p><p>(图片来源：gfs论文)</p><p>GFS中有四类角色，分别是</p><ul><li>GFS chunkserver</li><li>GFS master</li><li>GFS client</li><li>Application</li></ul><h3 id="3-1-1-GFS-chunkserver"><a href="#3-1-1-GFS-chunkserver" class="headerlink" title="3.1.1 GFS chunkserver"></a>3.1.1 GFS chunkserver</h3><p>在GFS chunkserver中，文件都是分成固定大小的chunk来存储的，每个chunk通过全局唯一的64位的chunk handle来标识，chunk handle在chunk创建的时候由GFS master分配。GFS chunkserver把文件存储在本地磁盘中，读或写的时候需要指定文件名和字节范围，然后定位到对应的chunk。为了保证数据的可靠性，一个chunk一般会在多台GFS chunkserver上存储，默认为3份，但用户也可以根据自己的需要修改这个值。</p><h3 id="3-1-2-GFS-master"><a href="#3-1-2-GFS-master" class="headerlink" title="3.1.2 GFS master"></a>3.1.2 GFS master</h3><p>GFS master管理所有的元数据信息，包括namespaces，访问控制信息，文件到chunk的映射信息，以及chunk的地址信息（即chunk存放在哪台GFS chunkserver上）。</p><h3 id="3-1-3-GFS-client"><a href="#3-1-3-GFS-client" class="headerlink" title="3.1.3 GFS client"></a>3.1.3 GFS client</h3><p>GFS client是GFS应用端使用的API接口，client和GFS master交互来获取元数据信息，但是所有和数据相关的信息都是直接和GFS chunkserver来交互的。</p><h3 id="3-1-4-Application"><a href="#3-1-4-Application" class="headerlink" title="3.1.4 Application"></a>3.1.4 Application</h3><p>Application为使用gfs的应用，应用通过GFS client于gfs后端(GFS master和GFS chunkserver)打交道。</p><h2 id="3-2-Single-Master"><a href="#3-2-Single-Master" class="headerlink" title="3.2 Single Master"></a>3.2 Single Master</h2><p>GFS架构中只有单个GFS master，这种架构的好处是设计和实现简单，例如，实现负载均衡时可以利用master上存储的全局的信息来做决策。但是，在这种架构下，要避免的一个问题是，应用读和写请求时，要弱化GFS master的参与度，防止它成为整个系统架构中的瓶颈。</p><p>从一个请求的流程来讨论上面的问题。首先，应用把文件名和偏移量信息传递给GFS client，GFS client转换成(文件名，chunk index)信息传递给GFS master，GFS master把(chunk handle, chunk位置信息)返回给客户端，客户端会把这个信息缓存起来，这样，下次再读这个chunk的时候，就不需要去GFS master拉取chunk位置信息了。</p><p>另一方面，GFS支持在一个请求中同时读取多个chunk的位置信息，这样更进一步的减少了GFS client和GFS master的交互次数，避免GFS master成为整个系统的瓶颈。</p><h2 id="3-3-Chunk-Size"><a href="#3-3-Chunk-Size" class="headerlink" title="3.3 Chunk Size"></a>3.3 Chunk Size</h2><p>对于GFS来说，chunk size的默认大小是64MB，比一般文件系统的要大。</p><p><strong>优点</strong></p><ul><li>可以减少GFS client和GFS master的交互次数，chunk size比较大的时候，多次读可能是一块chunk的数据，这样，可以减少GFS client向GFS master请求chunk位置信息的请求次数。</li><li>对于同一个chunk，GFS client可以和GFS chunkserver之间保持持久连接，提升读的性能。</li><li>chunk size越大，chunk的metadata的总大小就越小，使得chunk相关的metadata可以存储在GFS master的内存中。</li></ul><p><strong>缺点</strong></p><ul><li>chunk size越大时，可能对部分文件来讲只有1个chunk，那么这个时候对该文件的读写就会落到一个GFS chunkserver上，成为热点。</li></ul><p>对于热点问题，google给出的解决方案是应用层避免高频地同时读写同一个chunk。还提出了一个可能的解决方案是，GFS client找其他的GFS client来读数据。</p><p>64MB应该是google得出的一个比较好的权衡优缺点的经验值。</p><h2 id="3-4-Metadata"><a href="#3-4-Metadata" class="headerlink" title="3.4 Metadata"></a>3.4 Metadata</h2><p>GFS master存储三种metadata，包括文件和chunk namespace，文件到chunk的映射以及chunk的位置信息。这些metadata都是存储在GFS master的内存中的。对于前两种metadata，还会通过记操作日志的方式持久化存储，操作日志会同步到包括GFS master在内的多台机器上。GFS master不持久化存储chunk的位置信息，每次GFS master重启或者有新的GFS chunkserver加入时，GFS master会要求对应GFS chunkserver把chunk的位置信息汇报给它。</p><h3 id="3-4-1-In-Memory-Data-Structures"><a href="#3-4-1-In-Memory-Data-Structures" class="headerlink" title="3.4.1 In-Memory Data Structures"></a>3.4.1 In-Memory Data Structures</h3><p>使用内存存储metadata的好处是读取metadata速度快，方便GFS master做一些全局扫描metadata相关信息的操作，例如负载均衡等。</p><p>但是，以内存存储的的话，需要考虑的是GFS master的内存空间大小是不是整个系统能存储的chunk数量的瓶颈所在。在GFS实际使用过程中，这一般不会成为限制所在，因为GFS中一个64MBchunk的metadata大小不超过64B，并且，对于大部分chunk来讲都是使用的全部的空间的，只有文件的最后一个chunk会存储在部分空间没有使用，因此，GFS master的内存空间在实际上很少会成为限制系统容量的因素。即使真的是现有的存储文件的chunk数量超过了GFS master内存空间大小的限制，也可以通过加内存的方式，来获取内存存储设计带来的性能、可靠性等多种好处。</p><h3 id="3-4-2-Chunk-Locations"><a href="#3-4-2-Chunk-Locations" class="headerlink" title="3.4.2 Chunk Locations"></a>3.4.2 Chunk Locations</h3><p>GFS master不持久化存储chunk位置信息的原因是，GFS chunkserver很容易出现宕机，重启等行为，这样GFS master在每次发生这些事件的时候，都要修改持久化存储里面的位置信息的数据。</p><h3 id="3-4-3-Operation-Log"><a href="#3-4-3-Operation-Log" class="headerlink" title="3.4.3 Operation Log"></a>3.4.3 Operation Log</h3><p><strong>operation log的作用</strong></p><ul><li>持久化存储metadata</li><li>它的存储顺序定义了并行的操作的最终的操作顺序</li></ul><p><strong>怎么存</strong></p><p>operation log会存储在GFS master和多台远程机器上，只有当operation log在GFS master和多台远程机器都写入成功后，GFS master才会向GFS client返回成功。为了减少operation log在多台机器落盘对吞吐量的影响，可以将一批的operation log形成一个请求，然后写入到GFS master和其他远程机器上。</p><p><strong>check point</strong></p><p>当operation log达到一定大小时，GFS master会做checkpoint，相当于把内存的B-Tree格式的信息dump到磁盘中。当master需要重启时，可以读最近一次的checkpoint，然后replay它之后的operation log，加快恢复的时间。</p><p>做checkpoint的时候，GFS master会先切换到新的operation log，然后开新线程做checkpoint，所以，对新来的请求是基本是不会有影响的。</p><h1 id="4-System-Interactions"><a href="#4-System-Interactions" class="headerlink" title="4. System Interactions"></a>4. System Interactions</h1><p>本部分讨论GFS的系统交互流程。</p><h2 id="4-1-Leases-and-Mutation-Order"><a href="#4-1-Leases-and-Mutation-Order" class="headerlink" title="4.1 Leases and Mutation Order"></a>4.1 Leases and Mutation Order</h2><p>GFS master对后续的数据流程是不做控制的，所以，需要一个机制来保证，所有副本是按照同样的操作顺序写入对应的数据的。GFS采用lease方式来解决这个问题，GFS对一个chunk会选择一个GFS chunkserver，发放lease，称作primary，由primary chunkserver来控制写入的顺序。</p><p>Lease的过期时间默认是60s，可以通过心跳信息来续时间，如果一个primary chunkserver是正常状态的话，这个时间一般是无限续下去的。当primary chunkserver和GFS master心跳断了后，GFS master也可以方便的把其他chunk副本所在的chunkserver设置成primary。</p><h3 id="4-1-1-Write-Control-and-Data-Flow"><a href="#4-1-1-Write-Control-and-Data-Flow" class="headerlink" title="4.1.1 Write Control and Data Flow"></a>4.1.1 Write Control and Data Flow</h3><p><img src="http://oserror.com/images/gfs_write_control.png" alt="GFS Write Control and Data Flow"></p><p>(图片来源：gfs论文)</p><ol><li>GFS client向GFS master请求拥有具有当前chunk的lease的chunkserver信息，以及chunk的其他副本所在的chunkserver的信息，如果当前chunk没有lease，GFS master会分配一个。</li><li>GFS master把primary chunkserver以及其他副本的chunkserver信息返回给client。client会缓存这些信息，只有当primary chunkserver连不上或者lease发生改变后，才需要再向GFS master获取对应的信息。</li><li>client把数据推送给所有包含此chunk的chunkserver，chunkserver收到后会先把数据放到内部的LRU buffer中，当数据被使用或者过期了，才删除掉。注意，这里没有将具体怎么来发送数据，会在下面的Data Flow讲。</li><li>当所有包含chunk副本的chunkserver都收到了数据，client会给primary发送一个写请求，包含之前写的数据的信息，primary会分配对应的序号给此次的写请求，这样可以保证从多个客户端的并发写请求会得到唯一的操作顺序，保证多个副本的写入数据的顺序是一致的。</li><li>primary转发写请求给所有其他的副本所在的chunkserver(Secondary replica)，操作顺序由primary指定。</li><li>Secondary replica写成功后会返回给primary replica。</li><li>Primary replica返回给client。任何副本发生任何错误都会返回给client。</li></ol><p>这里，写数据如果发生错误可能会产生不一致的情况，会在consistency model中讨论。</p><h2 id="4-2-Data-Flow"><a href="#4-2-Data-Flow" class="headerlink" title="4.2 Data Flow"></a>4.2 Data Flow</h2><p>4.1中第三步的Data Flow采用的是pipe line方式，目标是为了充分利用每台机器的网络带宽。假设一台机器总共有三个副本S1-S3。整个的Data Flow为：</p><ol><li>client选择离它最近的chunkserver S1，开始推送数据</li><li>当chunkserver S1收到数据后，它会立马转发到离它最近的chunkserver S2</li><li>chunkserver S2收到数据后，会立马转发给离它最近的chunkserver S3</li></ol><p>不断重复上述流程，直到所有的chunkserver都收到client的所有数据。</p><p>以上述方式来传送B字节数据到R个副本，并假设网络吞吐量为T，机器之间的时延为L，那么，整个数据的传输时间为B/T+RL。</p><h2 id="4-3-Atomic-Record-Appends"><a href="#4-3-Atomic-Record-Appends" class="headerlink" title="4.3 Atomic Record Appends"></a>4.3 Atomic Record Appends</h2><p>Append操作流程和写差不多，主要区别在以下</p><ul><li>client把数据推送到所有副本的最后一个chunk，然后发送写请求到primary</li><li>primary首先检查最后一个chunk的剩余空间是否可以满足当前写请求，如果可以，那么执行写流程，否则，它会把当前的chunk的剩余空间pad起来，然后告诉其他的副本也这么干，最后告诉client这个chunk满了，写入下个chunk。</li></ul><p>这里需要讨论的是，如果append操作在部分副本失败的情况下，会发生什么？</p><p>例如，写操作要追加到S1-S3，但是，仅仅是S1,S2成功了，S3失败了，GFS client会重试操作，假如第二次成功了，那么S1,S2写了两次，S3写了一次，目前的理解是GFS会先把失败的记录进行padding对齐到primary的记录，然后再继续append。</p><h2 id="4-4-Snapshot"><a href="#4-4-Snapshot" class="headerlink" title="4.4 Snapshot"></a>4.4 Snapshot</h2><p>Snapshot的整个流程如下：</p><ol><li>client向GFS master发送Snapshot请求</li><li>GFS master收到请求后，会回收所有这次Snapshot涉及到的chunk的lease</li><li>当所有回收的lease到期后，GFS master写入一条日志，记录这个信息。然后，GFS会在内存中复制一份snapshot涉及到的metadata</li></ol><p>当snapshot操作完成后，client写snapshot中涉及到的chunk C的流程如下：</p><ol><li>client向GFS master请求primary chunkserver和其他chunkserver</li><li>GFS master发现chunk C的引用计数超过1，即snapshot和本身。它会向所有有chunk C副本的chunkserver发送创建一个chunk C的拷贝请求，记作是chunk C’，这样，把最新数据写入到chunk C’即可。本质上是copy on write。</li></ol><h2 id="4-5-Consistency-Model"><a href="#4-5-Consistency-Model" class="headerlink" title="4.5 Consistency Model"></a>4.5 Consistency Model</h2><p><img src="http://oserror.com/images/consistency_model.png" alt="Consistency Model"></p><p>(图片来源：gfs论文)</p><p>GFS中consistent、defined的定义如下：</p><ul><li>consistent：所有的客户端都能看到一样的数据，不管它们从哪个副本读取</li><li>defined：当一个文件区域发生操作后，client可以看到刚刚操作的所有数据，那么说这次操作是defined。</li></ul><p>下面分析表格中出现的几种情况。</p><ol><li>Write(Serial Success)，单个写操作，并且返回成功，那么所有副本都写入了这次操作的数据，因此所有客户端都能看到这次写入的数据，所以，是defined。</li><li>Write(Concurrent Successes)，多个写操作，并且返回成功，由于多个客户端写请求发送给priamary后，由primary来决定写的操作顺序，但是，有可能多个写操作可能是有区域重叠的，这样，最终写完成的数据可能是多个写操作数据叠加在一起，所以这种情况是consistent和undefined。</li><li>Write(Failure)，写操作失败，则可能有的副本写入了数据，有的没有，所以是inconsistent。</li><li>Record Append(Serial Success and Concurrent Success)，由于Record Append可能包含重复数据，因此，是inconsistent，由于整个写入的数据都能看到，所以是defined。</li><li>Record Append(Failure)，可能部分副本append成功，部分副本append失败，所以，结果是inconsistent。</li></ol><p>GFS用version来标记一个chunkserver挂掉的期间，是否有client进行了write或者append操作。每进行一次write或者append，version会增加。</p><p>需要考虑的点是client会缓存chunk的位置信息，有可能其中某些chunkserver已经挂掉又起来了，这个时候chunkserver的数据可能是老的数据，读到的数据是会不一致的。读流程中，好像没有看到要带version信息来读的。这个论文中没看到避免的措施，目前还没有结果。</p><h3 id="4-5-1-Implications-for-Applications"><a href="#4-5-1-Implications-for-Applications" class="headerlink" title="4.5.1 Implications for Applications"></a>4.5.1 Implications for Applications</h3><p>应用层需要采用的机制：用append而不是write，做checkpoint，writing self-validating和self-identifying records。具体地，如下：</p><ol><li>应用的使用流程是append一个文件，到最终写完后，重命名文件</li><li>对文件做checkpoint，这样应用只需要关注上次checkpoint时的文件区域到最新文件区域的数据是否是consistent的，如果这期间发生不一致，可以重新做这些操作。</li><li>对于并行做append的操作，可能会出现重复的数据，GFS client提供去重的功能。</li></ol><h1 id="5-Master-Operation"><a href="#5-Master-Operation" class="headerlink" title="5. Master Operation"></a>5. Master Operation</h1><p>GFS master的功能包括，namespace Management， Replica Placement，Chunk Creation，Re-replication and Rebalancing以及Garbage Collection。</p><h2 id="5-1-Namespace-Management-and-Locking"><a href="#5-1-Namespace-Management-and-Locking" class="headerlink" title="5.1 Namespace Management and Locking"></a>5.1 Namespace Management and Locking</h2><p>每个master操作都需要获得一系列的锁。如果一个操作涉及到/d1/d2/…/dn/leaf，那么需要获得/d1,/d1/d2,/d1/d2/…/dn的读锁，然后，根据操作类型，获得/d1/d2/…/dn/leaf的读锁或者写锁，其中leaf可能是文件或者路径。</p><p>一个例子，当/home/user被快照到/save/user的时候，/home/user/foo的创建是被禁止的。</p><p>对于快照，需要获得/home和/save的读锁，/home/user和/save/user的写锁。对于创建操作，会获得/home,/home/user的读锁，然后/home/user/foo的写锁。其中，/home/user的锁产生冲突，/home/user/foo创建会被禁止。</p><p>这种加锁机制的好处是对于同一个目录下，可以并行的操作文件，例如，同一个目录下并行的创建文件。</p><h2 id="5-2-Replica-Placement"><a href="#5-2-Replica-Placement" class="headerlink" title="5.2 Replica Placement"></a>5.2 Replica Placement</h2><p>GFS的Replica Placement的两个目标：最大化数据可靠性和可用性，最大化网络带宽的使用率。因此，把每个chunk的副本分散在不同的机架上，这样一方面，可以抵御机架级的故障，另一方面，可以把读写数据的带宽分配在机架级，重复利用多个机架的带宽。</p><h2 id="5-3-Creation-Re-replication-Rebalancing"><a href="#5-3-Creation-Re-replication-Rebalancing" class="headerlink" title="5.3 Creation, Re-replication, Rebalancing"></a>5.3 Creation, Re-replication, Rebalancing</h2><h3 id="5-3-1-Chunk-Creation"><a href="#5-3-1-Chunk-Creation" class="headerlink" title="5.3.1 Chunk Creation"></a>5.3.1 Chunk Creation</h3><p>GFS在创建chunk的时候，选择chunkserver时考虑的因素包括：</p><ol><li>磁盘空间使用率低于平均值的chunkserver</li><li>限制每台chunkserver的最近的创建chunk的次数，因为创建chunk往往意味着后续需要写大量数据，所以，应该把写流量尽量均摊到每台chunkserver上</li><li>chunk的副本放在处于不同机架的chunkserver上</li></ol><h2 id="5-3-2-Chunk-Re-replication"><a href="#5-3-2-Chunk-Re-replication" class="headerlink" title="5.3.2 Chunk Re-replication"></a>5.3.2 Chunk Re-replication</h2><p>当一个chunk的副本数量少于预设定的数量时，需要做复制的操作，例如，chunkserver宕机，副本数据出错，磁盘损坏，或者设定的副本数量增加。</p><p>chunk的复制的优先级是按照下面的因素来确定的：</p><ol><li>丢失两个副本的chunk比丢失一个副本的chunk的复制认为优先级高</li><li>文件正在使用比文件已被删除的chunk的优先级高</li><li>阻塞了client进程的chunk的优先级高(这个靠什么方法得到?)</li></ol><p>chunk复制的时候，选择新chunkserver要考虑的点：</p><ol><li>磁盘使用率</li><li>单个chunkserver的复制个数限制</li><li>多个副本需要在多个机架</li><li>集群的复制个数限制</li><li>限制每个chunkserver的复制网络带宽，通过限制读流量的速率来限制</li></ol><h3 id="5-3-3-Rebalancing"><a href="#5-3-3-Rebalancing" class="headerlink" title="5.3.3 Rebalancing"></a>5.3.3 Rebalancing</h3><p>周期性地检查副本分布情况，然后调整到更好的磁盘使用情况和负载均衡。GFS master对于新加入的chunkserver，逐渐地迁移副本到上面，防止新chunkserver带宽打满。</p><h2 id="5-4-Garbage-Collection"><a href="#5-4-Garbage-Collection" class="headerlink" title="5.4 Garbage Collection"></a>5.4 Garbage Collection</h2><p>在GFS删除一个文件后，并不会马上就对文件物理删除，而是在后面的定期清理的过程中才真正的删除。</p><p>具体地，对于一个删除操作，GFS仅仅是写一条日志记录，然后把文件命名成一个对外部不可见的名称，这个名称会包含删除的时间戳。GFS master会定期的扫描，当这些文件存在超过3天后，这些文件会从namespace中删掉，并且内存的中metadata会被删除。</p><p>在对chunk namespace的定期扫描时，会扫描到这些文件已经被删除的chunk，然后会把metadata从磁盘中删除。</p><p>在与chunkserver的heartbeat的交互过程中，GFS master会把不在metadata中的chunk告诉chunkserver，然后chunkserver就可以删除这些chunk了。</p><p>采用这种方式删除的好处：</p><ol><li>利用心跳方式交互，在一次删除失败后，还可以通过下次心跳继续重试操作</li><li>删除操作和其他的全局扫描metadata的操作可以放到一起做</li></ol><p>坏处：</p><ol><li>有可能有的应用需要频繁的创建和删除文件，这种延期删除方式会导致磁盘使用率偏高，GFS提供的解决方案是，对一个文件调用删除操作两次，GFS会马上做物理删除操作，释放空间。</li></ol><h2 id="5-5-Stale-Replication-Detection"><a href="#5-5-Stale-Replication-Detection" class="headerlink" title="5.5 Stale Replication Detection"></a>5.5 Stale Replication Detection</h2><p>当一台chunkserver挂掉的时候，有新的写入操作到chunk副本，会导致chunkserve的数据不是最新的。</p><p>当master分配lease到一个chunk时，它会更新chunk version number，然后其他的副本都会更新该值。这个操作是在返回给客户端之前完成的，如果有一个chunkserver当前是宕机的，那么它的version number就不会增加。当chunkserver重启后，会汇报它的chunk以及version number，对于version number落后的chunk，master就认为这个chunk的数据是落后的。</p><p>GFS master会把落后的chunk当垃圾来清理掉，并且不会把落后的chunkserver的位置信息传给client。</p><p>备注：</p><p>１. GFS master把落后的chunk当作垃圾清理，那么，是否是走re-replication的逻辑来生成新的副本呢？没有，是走立即复制的逻辑。</p><h1 id="6-Fault-Tolerance-and-Diagnose"><a href="#6-Fault-Tolerance-and-Diagnose" class="headerlink" title="6. Fault Tolerance and Diagnose"></a>6. Fault Tolerance and Diagnose</h1><h2 id="6-1-High-Availability"><a href="#6-1-High-Availability" class="headerlink" title="6.1 High Availability"></a>6.1 High Availability</h2><p>为了实现高可用性，GFS在通过两方面来解决，一是fast recovery，二是replication</p><h3 id="6-1-1-Fast-Recovery"><a href="#6-1-1-Fast-Recovery" class="headerlink" title="6.1.1 Fast Recovery"></a>6.1.1 Fast Recovery</h3><p>master和chunkserver都被设计成都能在秒级别重启</p><h3 id="6-1-2-Chunk-Replications"><a href="#6-1-2-Chunk-Replications" class="headerlink" title="6.1.2 Chunk Replications"></a>6.1.2 Chunk Replications</h3><p>每个chunk在多个机架上有副本，副本数量由用户来指定。当chunkserver不可用时，GFS master会自动的复制副本，保证副本数量和用户指定的一致。</p><h3 id="6-1-3-Master-Replication"><a href="#6-1-3-Master-Replication" class="headerlink" title="6.1.3 Master Replication"></a>6.1.3 Master Replication</h3><p>master的operation log和checkpoint都会复制到多台机器上，要保证这些机器的写都成功了，才认为是成功。只有一台master在来做garbage collection等后台操作。当master挂掉后，它能在很多时间内重启；当master所在的机器挂掉后，监控会在其他具有operation log的机器上重启启动master。</p><p>新启动的master只提供读服务，因为可能在挂掉的一瞬间，有些日志记录到primary master上，而没有记录到secondary master上（这里GFS没有具体说同步的流程）。</p><h2 id="6-2-Data-Integrity"><a href="#6-2-Data-Integrity" class="headerlink" title="6.2 Data Integrity"></a>6.2 Data Integrity</h2><p>每个chunkserver都会通过checksum来验证数据是否损坏的。</p><p>每个chunk被分成多个64KB的block，每个block有32位的checksum，checksum在内存中和磁盘的log中都有记录。</p><p>对于读请求，chunkserver会检查读操作所涉及block的所有checksum值是否正确，如果有一个block的checksum不对，那么会报错给client和master。client这时会从其他副本读数据，而master会clone一个新副本，当新副本clone好后，master会删除掉这个checksum出错的副本。</p><h2 id="6-3-Diagnose-Tools"><a href="#6-3-Diagnose-Tools" class="headerlink" title="6.3 Diagnose Tools"></a>6.3 Diagnose Tools</h2><p>主要是通过log，包括重要事件的log(chunkserver上下线)，RPC请求，RPC响应等。</p><h1 id="7-Discussion"><a href="#7-Discussion" class="headerlink" title="7. Discussion"></a>7. Discussion</h1><p>本部分主要讨论大规模分布式系统一书上，列出的关于gfs的一些问题，具体如下。</p><h2 id="7-1-为什么存储三个副本？而不是两个或者四个？"><a href="#7-1-为什么存储三个副本？而不是两个或者四个？" class="headerlink" title="7.1 为什么存储三个副本？而不是两个或者四个？"></a>7.1 为什么存储三个副本？而不是两个或者四个？</h2><ul><li> 如果存储的是两个副本，挂掉一个副本后，系统的可用性会比较低，例如，如果另一个没有挂掉的副本出现网络问题等，整个系统就不可用了</li><li>如果存储的是四个副本，成本比较高</li></ul><h2 id="7-2-chunk的大小为何选择64MB？这个选择主要基于哪些考虑？"><a href="#7-2-chunk的大小为何选择64MB？这个选择主要基于哪些考虑？" class="headerlink" title="7.2 chunk的大小为何选择64MB？这个选择主要基于哪些考虑？"></a>7.2 chunk的大小为何选择64MB？这个选择主要基于哪些考虑？</h2><p><strong>优点</strong></p><ul><li>可以减少GFS client和GFS master的交互次数，chunk size比较大的时候，多次读可能是一块chunk的数据，这样，可以减少GFS client向GFS master请求chunk位置信息的请求次数。</li><li>对于同一个chunk，GFS client可以和GFS chunkserver之间保持持久连接，提升读的性能。</li><li>chunk size越大，chunk的metadata的总大小就越小，使得chunk相关的metadata可以存储在GFS master的内存中。</li></ul><p><strong>缺点</strong></p><ul><li>chunk size越大时，可能对部分文件来讲只有1个chunk，那么这个时候对该文件的读写就会落到一个GFS chunkserver上，成为热点。</li></ul><p>64MB应该是google得出的一个比较好的权衡优缺点的经验值。</p><h2 id="7-3-gfs主要支持追加，改写操作比较少，为什么这么设计？如何设计一个仅支持追加操作的文件系统来构建分布式表格系统bigtable？"><a href="#7-3-gfs主要支持追加，改写操作比较少，为什么这么设计？如何设计一个仅支持追加操作的文件系统来构建分布式表格系统bigtable？" class="headerlink" title="7.3 gfs主要支持追加，改写操作比较少，为什么这么设计？如何设计一个仅支持追加操作的文件系统来构建分布式表格系统bigtable？"></a>7.3 gfs主要支持追加，改写操作比较少，为什么这么设计？如何设计一个仅支持追加操作的文件系统来构建分布式表格系统bigtable？</h2><ul><li>因为追加多，改写少是google根据现有应用需求而确定的</li><li>bigtable的问题等读到bigtable论文再讨论</li></ul><h2 id="7-4-为什么要将数据流和控制流分开？如果不分开，如何实现追加流程？"><a href="#7-4-为什么要将数据流和控制流分开？如果不分开，如何实现追加流程？" class="headerlink" title="7.4 为什么要将数据流和控制流分开？如果不分开，如何实现追加流程？"></a>7.4 为什么要将数据流和控制流分开？如果不分开，如何实现追加流程？</h2><p>主要是为了更有效地利用网络带宽。把数据流分开，可以更好地优化数据流的网络带宽使用。</p><p>如果不分开，需要讨论下。</p><h2 id="7-5-gfs有时会出现重复记录或者padding记录，为什么？"><a href="#7-5-gfs有时会出现重复记录或者padding记录，为什么？" class="headerlink" title="7.5 gfs有时会出现重复记录或者padding记录，为什么？"></a>7.5 gfs有时会出现重复记录或者padding记录，为什么？</h2><p><strong>padding出现场景：</strong></p><ul><li>last chunk的剩余空间不满足当前写入量大小，需要把last chunk做padding，然后告诉客户端写入下一个chunk</li><li>append操作失败的时候，需要把之前写入失败的副本padding对齐到master</li></ul><p><strong>重复记录出现场景：</strong></p><ul><li>append操作部分副本成功，部分失败，然后告诉客户端重试，客户端会在成功的副本上再次append，这样就会有重复记录出现</li></ul><h2 id="7-6-lease是什么？在gfs中起到了什么作用？它与心跳有何区别？"><a href="#7-6-lease是什么？在gfs中起到了什么作用？它与心跳有何区别？" class="headerlink" title="7.6 lease是什么？在gfs中起到了什么作用？它与心跳有何区别？"></a>7.6 lease是什么？在gfs中起到了什么作用？它与心跳有何区别？</h2><p>lease是gfs master把控制写入顺序的权限下放给chunkserver的机制，以减少gfs master在读写流程中的参与度，防止其成为系统瓶颈。心跳是gfs master检测chunkserver是否可用的标志。</p><h2 id="7-7-gfs追加过程中如果出现备副本故障，如何处理？如果出现主副本故障，应该如何处理？"><a href="#7-7-gfs追加过程中如果出现备副本故障，如何处理？如果出现主副本故障，应该如何处理？" class="headerlink" title="7.7 gfs追加过程中如果出现备副本故障，如何处理？如果出现主副本故障，应该如何处理？"></a>7.7 gfs追加过程中如果出现备副本故障，如何处理？如果出现主副本故障，应该如何处理？</h2><ul><li>对于备副本故障，写入的时候会失败，然后primary会返回错误给client。按照一般的系统设计，client会重试一定次数，发现还是失败，这时候client会把情况告诉给gfs master，gfs master可以检测chunkserver的情况，然后把最新的chunkserver信息同步给client，client端再继续重试。</li></ul><ul><li>对于主副本故障，写入的时候会失败，client端应该是超时了。client端会继续重试一定次数，发现还是一直超时，那么把情况告诉给gfs master，gfs master发现primary挂掉，会重新grant lease到其他chunkserver，并把情况返回给client。</li></ul><h2 id="7-8-gfs-master需要存储哪些信息？master的数据结构如何设计？"><a href="#7-8-gfs-master需要存储哪些信息？master的数据结构如何设计？" class="headerlink" title="7.8 gfs master需要存储哪些信息？master的数据结构如何设计？"></a>7.8 gfs master需要存储哪些信息？master的数据结构如何设计？</h2><p>namespace、文件到chunk的映射以及chunk的位置信息</p><p>namespace采用的是B-Tree，对于名称采用前缀压缩的方法，节省空间；（文件名，chunk index）到chunk的映射，可以通过hashmap；chunk到chunk的位置信息，可以用multi_hashmap，因为是一对多的映射。</p><h2 id="7-9-假设服务一千万个文件，每个文件1GB，master中存储元数据大概占多少内存？"><a href="#7-9-假设服务一千万个文件，每个文件1GB，master中存储元数据大概占多少内存？" class="headerlink" title="7.9 假设服务一千万个文件，每个文件1GB，master中存储元数据大概占多少内存？"></a>7.9 假设服务一千万个文件，每个文件1GB，master中存储元数据大概占多少内存？</h2><p>1GB/64MB = 1024 / 64 = 16。总共需要16 * 10000000 * 64 B = 10GB</p><h2 id="7-10-master如何实现高可用性？"><a href="#7-10-master如何实现高可用性？" class="headerlink" title="7.10 master如何实现高可用性？"></a>7.10 master如何实现高可用性？</h2><ul><li>metadata中namespace，以及文件到chunk信息持久化，并存储到多台机器</li><li>对metadata的做checkpoint，保证重启后replay消耗时间比较短，checkpoint可以直接映射到内存使用，不用解析</li><li>在primary master发生故障的时候，并且无法重启时，会有外部监控将secondary master，并提供读服务。secondary master也会监控chunkserver的状态，然后把primary master的日志replay到内存中</li></ul><h2 id="7-11-负载的影响因素有哪些？如何计算一台机器的负载值？"><a href="#7-11-负载的影响因素有哪些？如何计算一台机器的负载值？" class="headerlink" title="7.11 负载的影响因素有哪些？如何计算一台机器的负载值？"></a>7.11 负载的影响因素有哪些？如何计算一台机器的负载值？</h2><p>主要是考虑CPU、内存、网络和I/O，但如何综合这些参数并计算还是得看具体的场景，每部分的权重随场景的不同而不同。</p><h2 id="7-12-master新建chunk时如何选择chunkserver？如果新机器上线，负载值特别低，如何避免其他chunkserver同时往这台机器上迁移chunk？"><a href="#7-12-master新建chunk时如何选择chunkserver？如果新机器上线，负载值特别低，如何避免其他chunkserver同时往这台机器上迁移chunk？" class="headerlink" title="7.12 master新建chunk时如何选择chunkserver？如果新机器上线，负载值特别低，如何避免其他chunkserver同时往这台机器上迁移chunk？"></a>7.12 master新建chunk时如何选择chunkserver？如果新机器上线，负载值特别低，如何避免其他chunkserver同时往这台机器上迁移chunk？</h2><p><strong>如何选择chunkserver</strong></p><ul><li>磁盘空间使用率低于平均值的chunkserver</li><li>限制每台chunkserver最近创建chunk的次数，因为创建chunk往往意味着后续需要写入大量数据，所以，应该把写流量均摊到每台chunkserver</li><li>chunk的副本放置于不同机架的chunkserver上</li></ul><p><strong>如何避免同时迁移</strong></p><p>通过限制单个chunkserver的clone操作的个数，以及clone使用的带宽来限制，即从源chunkserver度数据的频率做控制。</p><h2 id="7-13-如果chunkserver下线后过一会重新上线，gfs如何处理？"><a href="#7-13-如果chunkserver下线后过一会重新上线，gfs如何处理？" class="headerlink" title="7.13 如果chunkserver下线后过一会重新上线，gfs如何处理？"></a>7.13 如果chunkserver下线后过一会重新上线，gfs如何处理？</h2><p>因为是过一会，所以假设chunk re-replication还没有执行，那么在这期间，可能这台chunkserver上有些chunk的数据已经处于落后状态了，client读数据的时候或者chunkserver定期扫描的时候会把这些状态告诉给master，master告诉上线后的chunkserver从其他机器复制该chunk，然后master会把这个chunk当作是垃圾清理掉。</p><p>对于没有落后的chunk副本，可以直接用于使用。</p><h2 id="7-14-如何实现分布式文件系统的快照操作？"><a href="#7-14-如何实现分布式文件系统的快照操作？" class="headerlink" title="7.14 如何实现分布式文件系统的快照操作？"></a>7.14 如何实现分布式文件系统的快照操作？</h2><p>Snapshot的整个流程如下：</p><ol><li>client向GFS master发送Snapshot请求</li><li>GFS master收到请求后，会回收所有这次Snapshot涉及到的chunk的lease</li><li>当所有回收的lease到期后，GFS master写入一条日志，记录这个信息。然后，GFS会在内存中复制一份snapshot涉及到的metadata</li></ol><p>当snapshot操作完成后，client写snapshot中涉及到的chunk C的流程如下：</p><ol><li>client向GFS master请求primary chunkserver和其他chunkserver</li><li>GFS master发现chunk C的引用计数超过1，即snapshot和本身。它会向所有有chunk C副本的chunkserver发送创建一个chunk C的拷贝请求，记作是chunk C’，这样，把最新数据写入到chunk C’即可。本质上是copy on write。</li></ol><h2 id="7-15-chunkserver数据结构如何设计？"><a href="#7-15-chunkserver数据结构如何设计？" class="headerlink" title="7.15 chunkserver数据结构如何设计？"></a>7.15 chunkserver数据结构如何设计？</h2><p>chunkserver主要是存储64KB block的checksum信息，需要由chunk+offset，能够快速定位到checksum，可以用hashmap。</p><h2 id="7-16-磁盘可能出现位翻转错误，chunkserver如何应对？"><a href="#7-16-磁盘可能出现位翻转错误，chunkserver如何应对？" class="headerlink" title="7.16 磁盘可能出现位翻转错误，chunkserver如何应对？"></a>7.16 磁盘可能出现位翻转错误，chunkserver如何应对？</h2><p>利用checksum机制，分读和写两种情况来讨论：</p><ol><li>对于读，要检查所读的所有block的checksum值</li><li>对于写，分为append和write。对于append，不检查checksum，延迟到读的时候检查，因为append的时候，对于最后一个不完整的block计算checksum时候采用的是增量的计算，即使前面存在错误，也能在后来的读发现。对于overwrite，因为不能采用增量计算，要覆盖checksum，所以，必须要先检查只写入部分数据的checksum是否不一致，否则，数据错误会被隐藏。</li></ol><h2 id="7-17-chunkserver重启后可能有一些过期的chunk，master如何能够发现？"><a href="#7-17-chunkserver重启后可能有一些过期的chunk，master如何能够发现？" class="headerlink" title="7.17 chunkserver重启后可能有一些过期的chunk，master如何能够发现？"></a>7.17 chunkserver重启后可能有一些过期的chunk，master如何能够发现？</h2><p>chunkserver重启后，会汇报chunk及其version number，master根据version number来判断是否过期。如果过期了，那么会做以下操作：</p><ol><li>过期的chunk不参与数据读写流程</li><li>master会告诉chunkserver从其他的最新副本里拷贝一份数据</li><li>master将过期的chunk假如garbage collection中</li></ol><p>问题：如果chunkserver拷贝数据的过程过程中，之前拷贝的数据备份又发生了变化，然后分为两种情况讨论：</p><ol><li>如果期间lease没变，那么chunkserver不知道自己拷贝的数据是老的，应该会存在不一致的问题？</li><li>如果期间lease改变，那么chunkserver因为还不能提供读服务，那么version number应该不会递增，继续保持stable状态，然后再发起拷贝。</li></ol><p>PS:<br>本博客更新会在第一时间推送到微信公众号，欢迎大家关注。</p><p><img src="http://oserror.com/images/qcode_wechat.jpg" alt="qocde_wechat"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;1-Introduction&quot;&gt;&lt;a href=&quot;#1-Introduction&quot; class=&quot;headerlink&quot; title=&quot;1. Introduction&quot;&gt;&lt;/a&gt;1. Introduction&lt;/h1&gt;&lt;p&gt;本文是读GFS论文的总结，收录在我的gi
      
    
    </summary>
    
      <category term="分布式" scheme="http://yoursite.com/categories/distributed/"/>
    
    
      <category term="GFS" scheme="http://yoursite.com/tags/GFS/"/>
    
  </entry>
  
</feed>
