blob: 07aa4690655f980ec410e35335d1fbaa977c7b0a [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pegasus | Replica Server 的设计</title>
<link rel="stylesheet" href="/zh/assets/css/app.css">
<link rel="shortcut icon" href="/zh/assets/images/favicon.ico">
<link rel="stylesheet" href="/zh/assets/css/utilities.min.css">
<link rel="stylesheet" href="/zh/assets/css/docsearch.v3.css">
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/all.min.js"></script>
<script src="/assets/js/docsearch.v3.js"></script>
<!-- Begin Jekyll SEO tag v2.8.0 -->
<title>Replica Server 的设计 | Pegasus</title>
<meta name="generator" content="Jekyll v4.3.3" />
<meta property="og:title" content="Replica Server 的设计" />
<meta name="author" content="Pegasus" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="在 Pegasus 的架构中,ReplicaServer负责数据的读写请求。我们在这篇文章中详细讨论它的内部机制。" />
<meta property="og:description" content="在 Pegasus 的架构中,ReplicaServer负责数据的读写请求。我们在这篇文章中详细讨论它的内部机制。" />
<meta property="og:site_name" content="Pegasus" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2017-11-21T00:00:00+00:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="Replica Server 的设计" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Pegasus"},"dateModified":"2017-11-21T00:00:00+00:00","datePublished":"2017-11-21T00:00:00+00:00","description":"在 Pegasus 的架构中,ReplicaServer负责数据的读写请求。我们在这篇文章中详细讨论它的内部机制。","headline":"Replica Server 的设计","mainEntityOfPage":{"@type":"WebPage","@id":"/2017/11/21/replica-server-design.html"},"url":"/2017/11/21/replica-server-design.html"}</script>
<!-- End Jekyll SEO tag -->
</head>
<body>
<nav class="navbar is-info">
<div class="container">
<!--container will be unwrapped when it's in docs-->
<div class="navbar-brand">
<a href="/zh/" class="navbar-item ">
<!-- Pegasus Icon -->
<img src="/assets/images/pegasus.svg">
</a>
<div class="navbar-item">
<a href="/zh/docs" class="button is-primary is-outlined is-inverted">
<span class="icon"><i class="fas fa-book"></i></span>
<span>Docs</span>
</a>
</div>
<div class="navbar-item is-hidden-desktop">
<!--A simple language switch button that only supports zh and en.-->
<!--IF its language is zh, then switches to en.-->
<!--If you don't want a url to be relativized, you can add a space explicitly into the href to
prevents a url from being relativized by polyglot.-->
<a class="button is-primary is-outlined is-inverted" href=" /2017/11/21/replica-server-design.html"><strong>En</strong></a>
</div>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
<!-- Appears in mobile mode only -->
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navMenu">
<div class="navbar-end">
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable ">
<a href=""
class="navbar-link ">
<span class="icon" style="margin-right: .25em">
<i class="fas fa-users"></i>
</span>
<span>
ASF
</span>
</a>
<div class="navbar-dropdown">
<a href="https://www.apache.org/"
class="navbar-item ">
Foundation
</a>
<a href="https://www.apache.org/licenses/"
class="navbar-item ">
License
</a>
<a href="https://www.apache.org/events/current-event.html"
class="navbar-item ">
Events
</a>
<a href="https://www.apache.org/foundation/sponsorship.html"
class="navbar-item ">
Sponsorship
</a>
<a href="https://www.apache.org/security/"
class="navbar-item ">
Security
</a>
<a href="https://privacy.apache.org/policies/privacy-policy-public.html"
class="navbar-item ">
Privacy
</a>
<a href="https://www.apache.org/foundation/thanks.html"
class="navbar-item ">
Thanks
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable ">
<a href="/zh/community"
class="navbar-link ">
<span class="icon" style="margin-right: .25em">
<i class="fas fa-user-plus"></i>
</span>
<span>
开源社区
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/community/#contact-us"
class="navbar-item ">
联系我们
</a>
<a href="/zh/community/#contribution"
class="navbar-item ">
参与贡献
</a>
<a href="https://cwiki.apache.org/confluence/display/PEGASUS/Coding+guides"
class="navbar-item ">
编码指引
</a>
<a href="https://github.com/apache/incubator-pegasus/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Fbug"
class="navbar-item ">
Bug 追踪
</a>
<a href="https://cwiki.apache.org/confluence/display/INCUBATOR/PegasusProposal"
class="navbar-item ">
Apache 提案
</a>
</div>
</div>
<a href="/zh/blogs"
class="navbar-item ">
<span class="icon" style="margin-right: .25em">
<i class="fas fa-rss"></i>
</span>
<span>Blog</span>
</a>
<a href="/zh/docs/downloads"
class="navbar-item ">
<span class="icon" style="margin-right: .25em">
<i class="fas fa-fire"></i>
</span>
<span>版本发布</span>
</a>
</div>
<div class="navbar-item is-hidden-mobile">
<!--A simple language switch button that only supports zh and en.-->
<!--IF its language is zh, then switches to en.-->
<!--If you don't want a url to be relativized, you can add a space explicitly into the href to
prevents a url from being relativized by polyglot.-->
<a class="button is-primary is-outlined is-inverted" href=" /2017/11/21/replica-server-design.html"><strong>En</strong></a>
</div>
</div>
</div>
</nav>
<section class="section">
<div class="container">
<div class="columns is-multiline">
<div class="column is-one-fourth">
</div>
<div class="column is-half">
<div class="content">
<ul class="blog-post-meta">
<li class="blog-post-meta-item">
<span class="icon" style="margin-right: .25em">
<i class="far fa-calendar-check" aria-hidden="true"></i>
</span>
November 21, 2017
</li>
<li class="blog-post-meta-item">
<span class="icon" style="margin-right: .25em">
<i class="fas fa-edit" aria-hidden="true"></i>
</span>
Pegasus
</li>
</ul>
<p>在 Pegasus 的架构中,ReplicaServer负责数据的读写请求。我们在这篇文章中详细讨论它的内部机制。</p>
<hr />
<h3 id="读写流程">读写流程</h3>
<p>ReplicaServer由一个个的Replica的组成,每个Replica表示一个数据分片的Primary或者Secondary。真正的读写流程,则是由这些Replica来完成的。</p>
<p>前面说过,当客户端有一个写请求时,会根据MetaServer的记录查询到分片对应的ReplicaServer。具体来说,客户端需要的其实是分片Primary所在的ReplicaServer。当获取到这一信息后,客户端会构造一条请求发送给ReplicaServer。请求除数据本身外,最主要的就包含了分片的编号,在Pegasus里,这个编号叫<strong>Gpid</strong>(global partition id)。</p>
<p>ReplicaServer在收到写请求时,会检查自己是不是能对请求做响应,如果可以的话,相应的写请求会进入写流程。具体的写流程不再赘述,大体过程就是先prepare再commit的两阶段提交。</p>
<p>可能导致ReplicaServer不能响应写请求的原因有:</p>
<ol>
<li>ReplicaServer无法向MetaServer持续汇报心跳,自动下线</li>
<li>Replica在IO上发生了一些无法恢复的异常故障,自动下线</li>
<li>MetaServer将Replica的Primary进行了迁移</li>
<li>Primary在和MetaServer进行group成员变更的操作,拒绝写</li>
<li>当前Secondary个数太少,Replica出于安全性考虑拒绝写</li>
<li>出于流控考虑而拒绝写</li>
</ol>
<p>这些类型的问题,Pegasus都会以错误码的形式返回给客户端。根据不同的错误类型,客户端可以选择合适的处理策略:</p>
<ul>
<li>无脑重试</li>
<li>降低发送频率</li>
<li>重新向MetaServer请求路由信息</li>
<li>放弃</li>
</ul>
<p>在目前Pegasus提供的客户端中,对这些错误都做了合适的处理。</p>
<p>读流程比写流程简单些,直接由Primary进行读请求的响应。</p>
<p>除此之外,Pegasus还提供了两种scan的,允许用户对写入的数据进行遍历:</p>
<ul>
<li>HashScan: 可以对同一个HashKey下的所有(SorkKey, Value)序对进行扫描,扫描结果按SortKey排序输出。该操作在对应Primary上完成。</li>
<li>table全局scan: 可以对一个表中的所有数据进行遍历。该操作在实现上会获取一个表中所有的Partition,然后逐个对Primary进行HashScan。</li>
</ul>
<h3 id="读写一致性模型">读写一致性模型</h3>
<ol>
<li>
<p>read-your-write consistency</p>
<p>假如一个写请求已经成功返回,那么后续的读一定可以读出来。</p>
</li>
<li>
<p>无external consistency</p>
<p>两个先后发起的写请求,并不保证前面那个一定比后面那个先成功。</p>
</li>
<li>
<p>无snapshot consistency</p>
<p>scan请求到的数据是不遵守因果律的,有可能后写进去的数据先扫描出来。之所以这样,是因为Pegasus在实现scan的时候并没有打snapshot。Pegasus在后续上可以跟进。</p>
</li>
</ol>
<h3 id="sharedlog和privatelog">SharedLog和PrivateLog</h3>
<p>前面介绍过,Pegasus在实现上追随了RSM(Replicated state machine)的模板:所有的写请求先写入到WAL(write ahead log),然后再提交到存储引擎。在多Replica并存的存储系统中,WAL的处理是一个比较棘手的问题,因为每一个Replica都需要写WAL。如果Replica较多的话,这意味着对磁盘的随机写。一般来讲,我们是希望避免磁盘的随机写的。</p>
<p>对于这类问题,一般的解决办法是多个Replica合写一个WAL,例如HBase就采取了这种做法。但这种做法所带来的劣势是对Replica的迁移重建工作非常的不友好。就Pegasus的架构来看,合写WAL意味着添加PotentialSecondary的时候会有易错且速度慢的log split操作。</p>
<p>Kudu在应对此类问题上提供了另外一个思路:无视这个问题,每个Replica各写一份WAL。之所以能这么做,我们认为出发点主要在于写请求是不会直接落盘,而是进操作系统的buffer cache的。有了一层buffer cache, 这意味着HDD的随机写可以得到一定程度的抑制;对于SSD,其写放大的问题也可以得到规避。但在这种做法下,如果开启写文件的立即落盘(fsync/O_DIRECT),整个写请求会有比较严重的性能损耗。</p>
<p>Pegasus在这里采取了另外一种做法:</p>
<ol>
<li>所有的写请求先合着写一个WAL,叫做<strong>SharedLog</strong>;</li>
<li>同时,对于每个Replica, 所有的请求都有一个内存cache, 然后以批量的方式写各自的WAL,叫做<strong>PrivateLog</strong></li>
<li>在进程重启的时候,PrivateLog缺失的部分可以在重放SharedLog时补全;</li>
<li>添加PotentialSecondary时,直接使用PrivateLog。</li>
</ol>
<h3 id="要不要立即落盘">要不要立即落盘</h3>
<p>要不要立即落盘也是个很有趣的问题,需要纠结的点如下:</p>
<ul>
<li>对于多副本的系统而言,只写OS缓存并不特别糟糕,因为单机断电的数据丢失并不会造成数据的真正丢失</li>
<li>对于单机房部署的集群,整机房的断电+不立即落盘可能会导致部分数据的丢失。为了应对这种问题,可以立即落盘或者加备用电池。</li>
<li>对于两地三机房部署的集群,所有机房全部不可用的可能性非常低,所以就算不立即落盘,一般问题也不大。</li>
</ul>
<p>Pegasus当前在写WAL上并没有采用即时落盘的方式,主要是性能和安全上的一种权衡。后续这一点可以作为一个配置项供用户选择。</p>
<h3 id="存储引擎">存储引擎</h3>
<p>Pegasus选择<a href="https://github.com/facebook/rocksdb">rocksdb</a>作为了单个Replica的存储引擎。在rocksdb的使用上,有三点需要说明一下:</p>
<ul>
<li>我们关闭掉了rocksdb的WAL。</li>
<li>PacificA对每条写请求都编了SequenceID, rocksdb对写请求也有内部的SequenceID。我们对二者做了融合,来支持我们自定义的checkpoint的生成。</li>
<li>我们给rocksdb添加了一些compaction filter以支持Pegasus的语义:例如某个value的TTL。</li>
</ul>
<p>和很多一致性协议的实现一样,Pegasus中PacificA的实现也是和存储引擎解耦的。如果后面有对其他存储引擎的需求,Pegasus也可能会引入。</p>
<h3 id="是否共享存储引擎">是否共享存储引擎</h3>
<p>在实现ReplicaServer上,另一个值得强调的点是“多个Replica共享一个存储引擎实例,还是每个Replica使用一个存储引擎实例”。主要的考虑点如下:</p>
<ol>
<li>共享存储引擎实例,意味着存储引擎是并发写的。如果存储引擎对并发写优化的不是很好,很有可能会成为性能瓶颈。</li>
<li>共享存储引擎不利于向replica group中添加新的成员。</li>
<li>如果一个存储引擎有自己的WAL,那么不共享存储引擎很有可能会造成磁盘的随机写。</li>
<li>一般在存储引擎的实现中,都会有单独的compaction过程。不共享存储引擎,并且存储引擎数太多的话,可能会导致过多的线程开销,各自在compaction时也可能引发随机写。</li>
</ol>
<p>Pegasus目前各个Replica是不共享存储引擎的。我们关掉rocksdb的WAL一方面的考虑也是为了避免3。</p>
<h3 id="replica的状态转换">Replica的状态转换</h3>
<p>在Pegasus中,一个Replica有如下几种状态:</p>
<ul>
<li>Primary</li>
<li>Secondary</li>
<li>PotentialSecondary(learner):当group中新添加一个成员时,在它补全完数据成为Secondary之前的状态</li>
<li>Inactive:和MetaServer断开连接时候的状态,或者在向MetaServer请求修改group的PartitionConfiguration时的状态</li>
<li>Error:当Replica发生IO或者逻辑错误时候的状态</li>
</ul>
<p>这几个状态的转换图不再展开,这里简述下状态转换的一些原则:</p>
<ul>
<li>Primary负责管理一个group中所有成员的状态。当Primary和Secondary或者Learner通信失败时,会采取措施将其移除。Secondary或者Learner从来不去尝试推翻一个Primary,推翻并选举新的Primary时MetaServer的责任。</li>
<li>当管理者决定触发状态变化时,<strong>当事人</strong>不会立即得到通知。例如,MetaServer因为探活失败要移除旧Primary时,不会通知旧Primary“我要移除你”;同理,当Primary因为通信失败要移除一个Secondary或者Learner时,也不会通知对应的Secondary或者Learner。这么做的原因也很好理解,这些动作之所以会发生,是因为网络不通,此时和<strong>当事人</strong>做通知是没有意义的。当事人在和决策者或者MetaServer恢复通信后,会根据对方的状态做响应变化。</li>
</ul>
<p>下面以Primary移除一个Secondary为例来阐述上述原则:</p>
<ul>
<li>Primary向Secondary发送prepare消息失败时,准备移除该Secondary</li>
<li>Primary会进入一个拒绝写的状态</li>
<li>开始把移除掉Secondary新的PartitionConfiguration发送给MetaServer</li>
<li>MetaServer在把新PartitionConfiguration持久化后会回复Primary成功</li>
<li>Primary把新的PartitionConfiguration修改到本地,并恢复到响应写的状态</li>
</ul>
<h3 id="添加learner">添加Learner</h3>
<p>添加Learner是整个一致性协议部分中最复杂的一个环节,这里概述以下其过程:</p>
<ul>
<li>MetaServer向Primary发起add_secondary的提议,把一个新的Replica添加到某台机器上。这一过程不会修改PartitionConfiguration。</li>
<li>Primary<strong>定期</strong>向对应机器发起添加Learner的邀请</li>
<li>Leaner在收到Primary的邀请后,开始向Primary拷贝数据。整个拷贝数据的过程比较复杂,要根据Learner当前的数据量决定是拷贝Primary的数据库镜像、PrivateLog、还是内存中对写请求的缓存。</li>
<li>Leaner在拷贝到Primary的全部数据后,会通知Primary拷贝完成</li>
<li>Primary向MetaServer发起修改PartitionConfiguration的请求。请求期间同样拒绝写,并且仍旧是MetaServer持久化完成后Primary才会修改本地视图。</li>
</ul>
<h3 id="replicaserver的bootstrap">ReplicaServer的bootstrap</h3>
<p>当一个ReplicaServer的进程启动时,它会加载自己的所有replica,并且重放所有的WAL。这些replica会被设置为inactive,是不会向外界提供读写服务的。</p>
<p>等加载完成后,ReplicaServer会启动FD模块连接MetaServer。连接成功后会向MetaServer查询自己服务的replica列表,并和自己加载的replica列表相比较并做相应调整:</p>
<ul>
<li>如果本地多出了一部分replica, replica server会将其关闭</li>
<li>如果MetaServer多出了一部分replica,请求MetaServer将其移除</li>
<li>如果MetaServer和本地都有,按MetaServer所标记的角色进行服务</li>
</ul>
<p>ReplicaServer向MetaServer查询replica列表并做本地调整的这一过程叫<strong>ConfigSync</strong>。这一过程并不仅限于bootstrap时候会有,而是在集群运行过程中会定期发生的一个任务。</p>
</div>
<div class="tags">
</div>
</div>
<div class="column is-one-fourth is-hidden-mobile" style="padding-left: 3rem">
<p class="menu-label">
<span class="icon">
<i class="fa fa-bars" aria-hidden="true"></i>
</span>
本页导航
</p>
<ul class="menu-list">
<li><a href="#读写流程">读写流程</a></li>
<li><a href="#读写一致性模型">读写一致性模型</a></li>
<li><a href="#sharedlog和privatelog">SharedLog和PrivateLog</a></li>
<li><a href="#要不要立即落盘">要不要立即落盘</a></li>
<li><a href="#存储引擎">存储引擎</a></li>
<li><a href="#是否共享存储引擎">是否共享存储引擎</a></li>
<li><a href="#replica的状态转换">Replica的状态转换</a></li>
<li><a href="#添加learner">添加Learner</a></li>
<li><a href="#replicaserver的bootstrap">ReplicaServer的bootstrap</a></li>
</ul>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="content is-small has-text-centered">
<div style="margin-bottom: 20px;">
<a href="http://incubator.apache.org">
<img src="/assets/images/egg-logo.png"
width="15%"
alt="Apache Incubator"/>
</a>
</div>
Copyright &copy; 2023 <a href="http://www.apache.org">The Apache Software Foundation</a>.
Licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version
2.0</a>.
<br><br>
Apache Pegasus is an effort undergoing incubation at The Apache Software Foundation (ASF),
sponsored by the Apache Incubator. Incubation is required of all newly accepted projects
until a further review indicates that the infrastructure, communications, and decision making process
have stabilized in a manner consistent with other successful ASF projects. While incubation status is
not necessarily a reflection of the completeness or stability of the code, it does indicate that the
project has yet to be fully endorsed by the ASF.
<br><br>
Apache Pegasus, Pegasus, Apache, the Apache feather logo, and the Apache Pegasus project logo are either
registered trademarks or trademarks of The Apache Software Foundation in the United States and other
countries.
</div>
</div>
</footer>
<script src="/assets/js/app.js" type="text/javascript"></script>
</body>
</html>