| <!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 © 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> |