设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

复制策略与复制的方式

2014-7-11 10:23| 发布者: joejoe0332| 查看: 4667| 评论: 0|原作者: Garfielt, 无若, 赵亮-碧海情天, yxrykds, Idiot_s_Sky, 明庭令|来自: 开源中国社区

摘要: 这个问题我已经想了几天了. 我试图想找出通用的复制解决方案能够应用到其他解决方案上去. 如果这个问题解决了,我们就能提供更多的功能组到更多的场景上.但在之前,我们要谈谈怎么去实现复制, 哪些类型的复制. 我们 ...
注意: 我写一些短篇的博客已经有些日子了. 我认为是时候改变下了.我不确定这篇博客会带来什么影响,但应该会是很大的影响.请让我知道你觉得更好的方法和理由。

  这个问题我已经想了几天了. 我试图想找出通用的复制解决方案能够应用到其他解决方案上去. 如果这个问题解决了,我们就能提供更多的功能组到更多的场景上.


  但在之前,我们要谈谈怎么去实现复制, 哪些类型的复制. 我们假设有一个单独的数据库 (没分片,在不同的节点). 普通情况下, 将有下面的选项:


  • 主/从 (Master/slaves)

  • 主/次 (Primary/secondaries)

  • 多重可写组合(Multi write partners)

  • 多重主(Multi master)


  上面的是我接下来博客要谈的内容.对于这些内容的目的,他们是完全不想关的。


  主/从模式是指这样一种情况,你只有一个主写节点和一个或多个从节点。这一模式的一大特点是你永远无法(至少在正常操作的情况下)对从节点做任何形式的更改。它们纯粹是用来读的,即使冒着损坏数据的风险切断它们与主节点的联系它们也不能变成可写的。


  这种方法的一个常见的例子是日志传送。我将在后面详细讨论它,但是你可以看看其他类似系统的文档,将一个从节点变更为可写是一个绝对不凡的经历。得有一个很好的理由。


  主/次级模式与主/从模式很类似,但在这种模式里我们可以选择一个次级节点成为主节点。只能有一个主服务器,但好处是允许有一种简单的方法来变更主节点。MongoDB就使用这样一个系统。


  多重可写组合系统允许任何节点接受写操作,并且它会留意将变更分发到其他节点。它也需要处理冲突,不像目前提到的其他选择。拥有使两个用户在多个节点同时写入相同值的能力。然而,多重可写组合通常会对同伴节点进行假设。例如,它们会在同步时比较,并有一个单独的协议用来将新的在线节点加入到常规复制组合中。


  多重主系统允许、接受并鼓励节点按需添加删除,它们假设会有写冲突,并有在运行的基础上解决冲突的需求。其他节点间没有相互同步的需求,它们通常“重新找主”到一个新的节点并开始复制它,这意味着你需要从一开始就复制所有数据。通常很希望到一个节点挂掉了,希望它在挂掉时已经完成所有变更,然后将它摘除。


  让我们看看每一个实际执行的细节,包括一些例子,希望这能让我说得更清楚。


日志泊运(Log Shipping)


  主/从通常是通过日志泊运来实现的。理解日志泊运的最简单方法是,主数据库会发送(很神奇,我们真的不太在乎这一点是怎样的)如何直接修改数据库文件的指令给从数据库。换句话说,从概念上讲,它会发送类似以下内容:


   1: writep(fd1, 1024, new[]{ 17,85,124,13,86}, 5);
   2: writep(fd1, 18432, new[]{ 12,95,34,83,76,32,59}, 7);b


  如你所想,这些是非常底层的修改。这个好处是非常易于捕捉和回放那些改变,而劣势是,你真地不能做任何其它事情。因为改变发生在堆栈的最底部,没有机会去运行任何各类的逻辑。我们只是写入到文件,如同主服务器所做的。


  这就是为什么允许从节点写很难的最关键原因。当它产生任何独立的写操作时,它便冒着主节点也在写的风险,这样会产生数据冲突。这就是为什么如果你想切换主从你必需做一系列事情。你必须处理完这些麻烦来保证你不会有两端都写的场景发生。


  一旦发生这种情况,你永远不不能再使两端同步了。这发生的几率很低。


  增加一个新节点,反过来,这很容易。请务必保留过程,做一个完整的数据库备份并将它移动到另一个节点。然后开始传递日志。因为只有它们在同一个点开始时一切才能安全实施。


  请注意,备份的这个版本在处理版本问题上很敏感。你不能在使用最底层存储的版本上做一丁点改变,因为那样可能会丢失一切。这个方法用于生成读复制很好用。事实上,这是大多数情况下使用的方法。


  理论上你甚至可以用它来做故障转移,因为如果主节点宕掉了,从节点可以接受写。问题是你如何处理从节点以为主节点宕掉了,而主节点以为一切都正常这种情形。这种情况下你可能让两端都可写,而这将导致不能合并地情况。


  理论上讲,因为它们有一个共同的根节点,你或许会觉得有一个引领者,并这样做了,但是这样会导致掉线服务器数据的丢失,或者你会没有可行方法取回的数据。这里我们记录的变化非常小,并且粒度太小以至于不允许你在提取变化信息方面做什么有用工作。

Oplog


  这实际上与日志传送方法非常类似,只是不发送底层的文件I/O操作,我们实际上发送的是更高层的命令。这就我们而言有相当多的好处。主服务器可以像如下一样发送日志:

set("users/1", {"name": "oren" });
set("users/2", {"name": "ayende" });
del("users/1");


  在次级节点上执行这套指令将导致次级上结果相同的状态。不像日志传送那样,这实际上需要次级服务器进行工作,所以相比应用已经计算过的文件更新这样的代价更昂贵。




  然而,这样做的好处是,你可以有一个更可读的日志。它也使把副服务器转为主服务器变得容易很多。最主要的是,如果你不傻的话:它们实际的操作完全是相同的一回事,但因为你是在协议层上工作,而不是文件级,所以你可以得到一些感兴趣的好处。


  让我们假设你有相同的“大脑分裂”问题,即主副服务器都认为它自己是主服务器。在用 Log Shipping 的情况下,我们无法调和这个分歧。而在用 Oplog 的情况下,我们却可以做到。这里的关键是,我们可以:


  • 对拒绝操作的服务器之一dump为可恢复状态。

  • 尝试应用两个服务器上的日志,希望它们不是同时工作在同一个文档上。


  这就是MongoDB采用的复制模式。并且它采取的处理这种冲突的第一种方法。事实上,这几乎是能够安全解决的唯一选择。当然,当两台服务器上更改相同对象是总是需要手动解决。而且最好是提前预防而不是认为“这样有时奏效”。


  你可以在这里看到一些MongoDB如何合并交叉写的讨论。事实上,如果继续使用相同的源数据,你可以在这里看到MongoDB内部的oplog :

   1: // Operations
   2:
   3: > use test
   4: switched to db test
   5: > db.foo.insert({x:1})
   6: > db.foo.update({x:1}, {$set : {y:1}})
   7: > db.foo.update({x:2}, {$set : {y:1}}, true)
   8: > db.foo.remove({x:1})
   9:
  10: // Op log view
  11:
  12: > use local
  13: switched to db local
  14: > db.oplog.rs.find()
  15: { "ts" : { "t" : 1286821527000, "i" : 1 }, "h" : NumberLong(0), "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } }
  16: { "ts" : { "t" : 1286821977000, "i" : 1 }, "h" : NumberLong("1722870850266333201"), "op" : "i", "ns" : "test.foo", "o" : { "_id" : ObjectId("4cb35859007cc1f4f9f7f85d"), "x" : 1 } }
  17: { "ts" : { "t" : 1286821984000, "i" : 1 }, "h" : NumberLong("1633487572904743924"), "op" : "u", "ns" : "test.foo", "o2" : { "_id" : ObjectId("4cb35859007cc1f4f9f7f85d") }, "o" : { "$set" : { "y" : 1 } } }
  18: { "ts" : { "t" : 1286821993000, "i" : 1 }, "h" : NumberLong("5491114356580488109"), "op" : "i", "ns" : "test.foo", "o" : { "_id" : ObjectId("4cb3586928ce78a2245fbd57"), "x" : 2, "y" : 1 } }
  19: { "ts" : { "t" : 1286821996000, "i" : 1 }, "h" : NumberLong("243223472855067144"), "op" : "d", "ns" : "test.foo", "b" : true, "o" : { "_id" : ObjectId("4cb35859007cc1f4f9f7f85d") } }


  你可以通过在oplog实体上的命令查看链。例如,上面命令的第7行被变成18行的一个插入。这样似乎也要做很多的工作来避免做任何计算工作,更倾向于用一个简单操作来解决问题。

比如,你有一个看起来像{counter:1}的文档,你在主节点上做了一个类似{$inc:{counter:1}}的更新,结果是{counter:2},而oplog将储存{$set:{counter:2}}。次级节点将这样复制而不是使用$inc。


  这是一个非常不错的特性,因为你可以多次操作这样的变更,但是返回的结果是一样的。但是这样会导致一个恶劣的结果,那就是你不能将多次的变更合并处理,当然,你可以采用更好的一些方式来处理这个问题,但是。。我并不喜欢这种方案。     

Multi write partners


  在这种模式下,我们存在一个服务器的集群,每一个服务器都很类似。所有的写操作都被处理并记录下来。当从源服务器复制到所有的目标服务器的时候,就会问目标服务器:你上次从我这儿操作了多少啦,这里就是从上次到现在的所有的变更哦。在这一点,我们可以从已经复制到所有的目标服务器的日志来看看。


  服务器宕机意味着相关的日志变化部分会在尺度上增加,直到同伴节点再次运行起来,或者我们从复制目标中移除这个服务器条目。


  到目前为止,这与你要组织 oplog 的方式非常相似。主要的不同是,组织需要记录的真实数据的方式。从 oplog 角度看,你准备向系统中写入发生的变化。并且,对之施行的唯一方式就是以它产生的相同顺序将其应用到 oplog 中。这会导致你只能一直拥有一个单主节点的系统。并且会引发在“大脑分裂”时数据丢失或需要手工合并的场景。


  就多重可写组合而言,我们要保持足够的上下文(通常是所有对象),以便给用户更好的选择来解决冲突。这也在非顺序的方式中给我们一种重新回放日志的选择。


  注意,无论如何,你都不能让一个新的服务器上线,如果你想让它一开始就运行良好的话。你必须从一个已知状态开始,通常是从一个已存在的数据库备份节点开始。像日志传送的那样,这个过程从本质上说,开始复制到(当前不存在的服务器上),当我们实际拥有新的服务器时,这将确保日志记录它们。在一个从服务器上备份和恢复数据库,然后,从源数据库那里配置接收复制。




  这里的复杂性是这样的,你需要处理的操作,你可能已经做过了。这就是为什么通常是搭配vector clocks使用,这样你就可以自动解决这些冲突。当你不能解决这些冲突时,就会陷入手动用户干预。


Multi Master


  多主系统(Multi master systems)非常类似于多重可写组合(Multi write partners),但是它们被设计成独立运作。常见的服务器可以是彼此间很少通信的。举例来说,一个可移动系统在一周中可能只能连上网络几个小时。同样地,我们不能限制复制操作的规模。实际上,常见的方法是快速把它复制给一个新的服务器。这意味着我们需要的复制,本质上可以从服务器历史上任何点,复制到一个新的服务器。


  只要你没有删除,它就会工作地很好。因为你需要保持跟踪,并且复制它们,所以那样做比较难。比如,RavenDB和CouchDB都是多主系统。冲突以同样的方式工作,我们使用一个向量时钟(vector clock)判断一个值是否相冲突。


Divergent writes


  我提到的这几次,但我没有完全解释。对于我而言,我们假设我们使用2个服务器(是的,我当然知道会议法定人数(quorums),等等,这个讨论暂不相关)在主/从模式下运行。


  在某一时刻,从服务器认为主服务器已经挂掉了就开始接管,并且主服务器不会通知,仍然会认为它是主服务器。在此期间,两个服务器都会接收到下面的写入信息:


Server AServer B
write users/1wrier users/2
write users/3write users/3
delete users/4delete users/5
delete users/6write users/6
write users/7delete all users
set all users to activewrite users/8


  那些操作发生之后,我们在两个服务器间恢复通讯,他们需要决定怎样去处理那些改变。

Getting down to business


  好的,已经很充分地讨论了那些术语的含义。让我们考虑使用它们的意义。在使用中,日志传送是迄今为止最简单的方法。好,假设你有一个日志机制,除了大多数数据库做的。它是一个严格地写入模型,并且绝对没有一个方法,或能处理发散地写入或能找出他们。日志传送的好处是它很容易得到这个结果而不需要关心实际包含的数据。我们直接在文件级别上工作,我们不关心数据是什么。问题是,我们几乎不能解决简单的冲突,类似写不同的对象。这是因为我们所有的改变,实际上是 在文件级别上工作。试图 合并改变来自多个日志文件可能会导致文件错误。站在好的一面,这可能是解决这个问题最有效的方式。


  Oplog是日志传输中的一个步骤,但它不是一个很大的操作。它不能解决分散写的问题。现在这是一个应用程序级别的协议。日志需要包含的信息具体到我们存储的实际数据类型。你需要编写显式的代码来处理这些问题。这很好,但它的所有操作序列也是有严格的。现在,你可以尝试合并不同的日志。然而,你还是需要担心冲突,说得更确切些,通常是没有数据本身,甚至也会帮你检测出冲突。


  多重可写组合(Multi write partners)意味着将其提升一个等级。他们跟踪版本历史(通常通过向量时钟)。在这里,情况更复杂,因为我们需要显式地决定如何处理冲突(自动解决或听从用户的决定),而且如何处理分布的更新。他们通常与某种形式的逻辑告诉你如何直接写。所以,所有写特定的数据块都进入首选的节点,去避免产生多个版本。数据需要包含一些信息,所以我们保持向量时钟的信息。一旦我们发送这些改变给我们的组合(partners),我们可以终止它们,保存在(磁盘)空间里。


  多重主(Multi master)意味着确保你的从机可能仅仅在偶然情况下才能看到另一个,而且在网络拓扑结构上不能有假设。它可以处理一个节点上发生的事件,从中获取一些数据拷贝,并且停掉一段时间(或是永远)。每一个节点都是完全独立的,并且它们之间应该是可以彼此合作的,但不是说它们之间就是互相需要的。这种方法的缺点是我们永远都需要保持记录一些信息。尤其是,我们需要保持对删除的记录,以确保我们能从远程的机器中获取这些信息。

What about set operations?


  有趣的是,这也许也是最难解决的问题。当你已经允许下面的操作发生时考虑这样一种情况:

Server AServer B
write users/7delete all users
set all users to activewrite users/8 (active: false)


  结果应该是什么呢?真的没有一个很好地答案去回答这个问题。应该将users/8设置为有效:真的吗?那users/7呢,应该被删除还是被保持?


  这将变得困难,因为你没有更好的选择。这里的难点实际上是冲突的判定。并没有一个恰当的方法来处理带有冲突的操作集。通常的解决方案是,将其翻译为产生的实际操作(delete users/1,user/2, users/3 – writer users/8, users/5),这样就可以了。基于集合的操作被翻译为实际发生的各个独立操作。在此基础上,我们能更容易的检测冲突。


  从操作上来说,日志传送是最容易处理的。你知道你想要获得什么,并且你可以处理它。Oplog 也很简单,你有个单主节点,它也能工作。多重主与多重可写组合需要你明确的留意冲突,选择合适的节点来减少冲突,等等。


  在实践中,至少在涉及 RavenDB 的情景中,能让服务器离线几周或几个月的能力似乎不常用到。常见的部署模型服务器的运行稳定的伙伴。有一些你可以对 multi write partners 做的优化,很难或不可能对 multi master 去做。


  在这一点上我目前个人的偏好是 日志泊运 或 multi write master 都可以。我认为他们中的任何一个都能相当简单的实施和支持操作。我想我会在我的下一篇文章中讨论使用其中一项对时间序列课题进行实际设计。


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部