五个解决方案让MongoDB拥有RDBMS的鲁棒性事务

事务问题

数据库支持数据块间的事务是有原因的。典型的场景是应用需要修改几个独立的比特时,如果只有一些而不是全部改变存储到了数据库,那么这就会出现不一致问题。因此ACID的概念是:

 

  • 原子性:所有的改变要么都做了,要么都没做
  • 一致性:数据保持一致性状态
  • 隔离性:其它用户看不到部分改变
  • 持久性:一旦向用户确认了事务,数据就处于安全的状态(通常存在硬盘上)

 

引入NoSQL数据库后,文档间ACID事务的支持通常就取消了。许多键/值存储仍有ACID,但它只适用于单个条目,取消ACID的主要原因是其可扩展限制。如果文档横跨几个服务器,事务将会很难实施以及性能。假设事务横跨数十个服务器,一些数据库是远程的,一些是不可靠的,想象下这会变的多难,多慢!

在单个文档等级上,MongoDB支持ACID。更准确的说,默认情况下是“ACI”,打开“j”WriteConcern选项后是ACID。Mongo有丰富的查询语言,横跨多个文档,因此人们一直在寻找多文档事务来使用他们的SQL代码。一个常见的办法是利用文档的性质:不需要很多行、很多关系,你可以将所有的东西嵌入到一个大文档中,Denormalization将带你回归事务。

这个技术解决了从一对一关系到一对多关系的很多事务问题。这也可能使应用更简单,数据库更快,所以这是双赢。不过当数据库必须分离时,该怎么办?

减少ACID

其实大部分应用都可以归结为:

 

  • 原子性:实际上你希望所有的改变都完成
  • 一致性:系统短时间不一致没关系,只要最终一致就行
  • 隔离性:缺乏隔离性导致暂时的不一致,这并不理想,但是当今线上服务时代,很多用户对此都习惯了(如用户支持:“它要花几秒传输”)。
  • 持久性:很重要,要支持。

 

这样问题就简化为鲁棒性、可扩性、最终一致性。

解决方案 1:字段同步

这种解决方案的使用场景最简单,最常见:文档间有些字段需要保持“同步”。例如,你有一个用户名为“John”的用户文档,文档代表John发表过的评论。如果用户可以更换用户名,那么这个改变需要发送给所有文档,即使进程中有应用错误或数据库错误。

为了实现这一目标,一个简单的办法是在主文档(这个情况下主文档是用户文档)中使用一个新字段(如“syncing”)。给“syncing”设置一个日期时间戳,记录用户文档的更新。

db.user.update({ _id: userId }, { $set:{ syncing: currentTime }, { rest of updates ... } })

然后应用会修改所有的评论文档。结束后,需要移除标识:

db.user.update({ _id: userId }, {$unset: { syncing: 1 } })

现在假设进程中出现了问题:有些评论使用的是旧用户名。不过这些地方仍然会保留标识,所以应用知道哪些进程需要重新进行。因此,你需要后台进程在指定的时间(如1小时)检查“syncing”文件是否有未完成的地方。索引应设为“sparse”,这样只有实际设置的文档需要被索引,索引量就会比较小。

db.user.ensureIndex({ syncing: 1 }, { sparse: true })

因此,系统通常可以保持事情在短时间内同步,在系统故障的情况下,时间周期为一个小时。如果时间不重要,当探测到“syncing”标志时,应用可以轻易修复文档。

解决方案2:作业队列

以上原理良好工作的前提是应用不需要很多内容,只依赖于通用进程(如:复制一个值)。一些事务需要执行特定变化,这些变化稍后很难识别。例如,用户文档包括一个朋友列表:

{ _id: userId, friends: [ userId1,userId2, ... ]}

现在A和B决定成为朋友:你需要把B添加到A的列表,也需要把A添加到B的列表。如果两者没有同时发生也没有关系(只要没有引发困扰)。针对这种情况和大多数事务问题的解决方案是使用作业队列,作业队列也存储在MongoDB。一个作业文档就像这样:

 

{ _id: jobId, ts: timeStamp, state: "TODO", type: "ADD_FRIEND", details: { users: [ userA, userB ]} }

 

或者是原始线程可以插入作业转发改变,或者是“worker”线程可以捡起工作。worker使用findAndModify()获取最原始的未加工的工作,findAndModify()是完全原子性的。操作中findAndModify()将工作标注为将被处理,同时也会表明worker name、当前时间以便于追踪。{ state: 1, ts: 1 } 上的索引使这些调用很迅速。

 

db.job.findAndModify({ query: { state: "TODO" }, sort: { ts: 1 }, update: { $set: { state: "PROCESSING", worker: { name: "worker1", ts: startTime } } } })

 

之后worker以一种幂等的方式对双方用户文档进行修改,这些改变能应用很多次,并且有同样的效果——这很重要!为了这个目的,我们只需要使用一个$addToSet。一种更通用的替代方式是在查询端添加一个测试,检测修改是否执行了。

db.user.update({ _id: userA }, {$addToSet: { friends: userB } })

最后一步是删除作业或标注作业完成。再保留一段时间作业是一种安全的方式,唯一的缺点是随着时间的流逝,先前的索引会变得越来越大,尽管你可以在指定域{ undone: 1 } 上使用稀疏索引,并且根据实际情况修改查询。

 

db.job.update({ _id: jobId }, { $set: { state: "DONE" } })

 

如果进程在某一时刻故障了,作业仍然会在队列中,并标注为处理中。后台进程停止一段时间后会将作业标注为需要再次处理,然后作业会重新从头开始。

解决方案:二阶段提交

二阶段提交是一个众所周知的解决方案,很多分布式系统都采用了这种解决方案。MongoDB简化了这种解决方案的实施,因为灵活的框架,我们可以将所有需要执行的数据全都放入文档中。我几年前就写过关于这种方法的文章,你可以去MongoDB Cookbook中查阅《 执行二阶段提交》(Perform Two Phase Commits)或者到MonoBD Manual中查阅《 执行二阶段提交》(Perform Two Phase Commits)。

解决方案4: Log Reconciliation

很多财务系统常用的解决方案是 log reconciliation。这种解决方案将事务写作简单的日志,这避免了复杂性和潜在的故障。然后从上次良好状态以来所有的变化推测当前账户的状态。在极端情况下,你可以清空账户,然后通过实施从第一天以来所有的变化重建账户……这听起来很恐怖,但是可行。账户文件需要一个“缓存”来提高速度,还需要一个seqId,seqId计算如下:

{ _id: accountId, cache: { balance:10000, seqId: 115 } }

执行事务时,一个典型的财务系统会给事务写一个条目,会给与事务有关的账户写一个“账户变化”条目。这个方法需要进一步的写保证,“作业队列”解决方案可以实现写保证,事务中所有的作业在所有账户更改写入前都会保持不变。不过有了MongoDB,我们可以写一个包括事务和账户更改的文档。这个文档应该嵌入tx集合,如下:

 

{ _id: ObjectId, ts: timestamp , proc: "UNCOMMITTED", state: "VALID", changes: [ { account: 1234, type: "withdraw", value: -100, seqId: 801, cachedBal: null }, { account: 2345, type: "deposit", value: 100, seqId: 203, cachedBal: null } ] }

 

几个重点:

 

  • 步骤:事务从“UNCOMMITTED” 状态开始,变为“COMMITTED”,此时涉及这些账户的所有先前事务也会变为 “COMMITTED” ,这表明这个事务也可以用作“anchor”来进行平衡计算。
  • 状态:状态可能是 “VALID”、“CANCELLED等。如果不是VALID,即使是“COMMITTED”,平衡计算也会忽略事务。
  • seqId:这是账户的独有的seqId,这个seqId给账户更改一个确定的顺序。
  • cachedBal:账户的缓存平衡。如果事务时“COMMITTED”状态,那么缓存平衡(如果设置了)是一个有效值。
  • 注意我们在 { changes.account: 1, changes.seqId: 1 }上使用一个独特的索引。reconciliation需要这个索引来提速,一个账户也不会有seqId副本。

 

关键是确保即使事务没有按顺序发生,缓存平衡也可以安全的计算/取消,还有就是事务状态可能改变。因此我们每个账户使用一个seqId,这确保了账户更改按确定的顺序发生,可以避免复杂的锁。在写事务前,应用首先通过简单地查询推断每个账户的下一个sqlId:

 

db.tx.find({ "changes.account": 1234 }, { "changes.$.seqId": 1 }).sort({ "changes.seqId": -1 }).limit(1)

 

然后每个sqlId都本地增长,然后写作事务的一部分。如果另一个线程也可能同时包括同样的seqId,独特的索引会确保写失败,线程会进行重试直到顺利完成任务。另一种方法是在账户集中保存一个当前seqId,然后用 findAndModify()获得下一个seqId,这通常会比较慢,除非你对账户有很多争用。注意如果因为某种原因事务没有写时,seqId可能会被跳过去,不过只有没有副本情况下才会成为。

下面我们谈谈reconciliation的基础。后台进程确保所有未提交的事务都会继续进行。只有所有账户的低seqId的事务都提交后一个事务才会被标注为提交。事务被标记为提交后就会变成不可变的。下面来谈谈好的方面:获得账户平衡。首先我们获得好的平衡,我们可以通过索引进行查询:

 

db.tx.find({ "changes.account": 1234, proc: "COMMITTED" }, { "changes.$": 1 }).sort({ "changes.seqId": -1 }).limit(1)

 

我们通过较大seqId的事务获得所有将发生的更改:

 

db.tx.find({ "changes.account": 1234, "changes.seqId": { $gt: lastGoodSeqId } }, { "changes.$": 1 }).sort({ "changes.seqId": 1 })

 

我们可以使用这些解决展示即将发生的损耗。如果我们只想简单的了解将来的平衡点在哪,我们可以让MongoDB收集所有变更展示总数:

 

db.tx.aggregate([{ $match: { "changes.account": 1234, "changes.seqId": { $gt: lastGoodSeqId }, state: "VALID" }},
{ $unwind: "changes" },
{ $match: { "account": 1234 }},
{ $group: { _id: "total", total: { $sum: "$value" } }}])

 

为了确保系统快速、计算量小,后台工作者要确保所有的事务都达到提交状态,平衡得到缓存。理想情况下一个事务是不可逆的,取而代之的是提交一个逆向事务来实施事务。不过只要所有的进一步事务状态和缓存都是正确设置的,取消是可行的。

解决方案5:版本控制

有时变得很复杂,以至于不能再JSON中表示,这些变更可能涉及很多有着复杂关系的文件(如树结构)。如果仅是部分变化(如破坏树)将会很混乱,这种情况下我们需要隔离。获取隔离性的一种方式是插入有着高版本号的新文档,取代对现有文档的更新。可以通过同日志和解同样的技术很容易、很安全的获得新版本号。通常{ itemId: 1, version: 1}上有一个独特的索引。

嵌入文档的应用从子文档开始,到主文档结束(如根节点)。当获取数据时,应用检查主文档的版本号,忽略高于版本号高于此版本号的文档。未完成的事务可以保持原状,可以忽略,可以清楚。

总结

综上所述,我们提供了在文档间实施鲁棒可扩展事物的五种解决方案:

 

  • 同步标志:最适用于仅从主文档复制数据的情况
  • 作业队列:比较通用,适用于95%的情况,大部分系统至少需要一个作业队列
  • 二阶段提交:这种技术确保每个实体都有为保持一致性状态所需的所有信息
  • Log Reconciliation:最鲁棒的技术,最适用于财务系统
  • 版本控制:提供了隔离性,适用于复杂的结构

 

此外,我们还提到了很多次MongoDB最终将支持真正的原子性和文档间的隔离事务。这已经作为分区的一部分了,但目前还只是内部的……只有文档在同一分区时这一特性才可能实现,否则我们将回到不可扩展的SQL世界。

谈百度网盘“涉黄”遭警告:网盘内容审核有哪几种方式?

北京时间8月11日消息,有关部门接到群众举报百度网盘部分账号存在淫秽色情问题,经调查这一情况确实属实,因此北京文化市场执法总队对百度下达整改通知。

值得一提的是,自从今年4月份有关部门开展扫黄打非、净网2014以及剑网行动之后,已经有不少产品受到影响或关闭。其中,最为大家熟悉的莫不过是快播,新浪爱问·共享资料也在5月5日主动关闭进行排查,时至今日仍未恢复,而360网盘也在活动之后宣布,为配合有关部门的净网行动,他们将对个人文件的分享内容进行人工审核……

看到这里问题就来了,人工审核无疑太累,而且不能上传后立马分享,有可能会影响到用户的分享积极性。那除了人工审核外,网盘内容的审核还有哪些方式?

网盘内容可能的审核方式:

1.  关键字搜索:这是最简单的审核方式,通过输入关键词,能将一些看起来明显是违法、涉黄的内容直接干掉。如果做好这一项,至少能将网盘分享的糟粕内容去掉一大半。

2.  建立身份档案:DNA是人类唯一的身份识别码,而文件的DNA无疑是MD5,如果网盘能建立涉黄文件的MD5数据库,用户上传后自动分析MD5是否合法,则能避免涉黄文件的重复分享。

3.  自动截取视频缩略图:估计百度这样的公司,应该能对色情图片进行自动识别了。所以可以自动截取一个视频的十来个地方的缩略图,用色情图片识别技术鉴别该视频是否违法。

当然有些用户,会更改文件名或者建立压缩包来逃避涉黄文件的打击,那有没有好的办法来解决这些问题?

4.  关注访问量高的文件:访问量高,基本是涉黄类分享内容的主要特征之一,如果能在人工审核中,对这些访问量突然蹿升的文件进行审查,相比又会毙掉一部分文件分享。

不过纵使有这些审核办法,但人工审核仍然是最有效以及不可避免的手段,否则也就不会出现鉴黄师这样的职业了。

Dev和Ops所面临的挑战

开发团队面对的挑战:

 

  • 延展性:我该如何进行系统架构,来保证在100台机器上的运作表现等同于仅1台机器上运作?
  • 性能表现:我该如何做才能确保系统表现与既定的服务级别协议相一致。
  • 性能测试:我该如何设计,才能使得开发者能更轻松地进行单元测试,同时与QA环节无缝接合?
  • 外延性:我该如何选择设计模式使得系统能满足不断变化的商业目标?
  • 问题诊断:怎么做才能快速进行问题定位,找出根本原因并进行有效处理?
  • 发布环节:怎么做才能快速进行程序的更新换代并最终成功发布?
  • 代码质量:该如何进行开发和测试把代码缺漏的影响降到最低?

 

营运团队面对的挑战:

 

  • 可靠性:所有应用都运作正常吗?应该采取何种战略把运行中断的影响降到最低?
  • 压力管理:如何合理分配资源来满足当前的营运负荷?又该如何动态变更环境配置来处理峰值满载情况?
  • 系统诊断:当多个虚拟机运行在同一机器上,一旦出现问题,该如何进行处理?
  • 监控:我需要时刻关注系统运作良好与否。
  • 成本管理:我们在尽量降低成本的同时还要保证营运质量。
  • 服务级别协议:对协议里面的各项指标,我们必须进行监控,管理和维护。

 

不难看出,开发部和营运部其实有着共同的目标:不断进行改良,使企业效益最大化。但诚如现实中的一个段子:当来自金星的开发部碰上来自火星的营运部,会经常发现大家都不在服务区,无法沟通。那么该如何做,才能使这两个企业的左膀右臂得以和谐共存?

程序延展性 — 这是每个开发者都盼望营运者所应该了解的

开发者心声:

我们需要花费好几个月甚至好几年的功夫才能开发出一个完整程序。我们费尽心思地去选择正确的设计模式,不厌其烦地优化自己的代码,尽最大努力保证质量。当我们再次回到代码行间中奋笔疾书前,衷心希望营运部能从以下几方面来好好对待我们的杰作。

首先,希望营运部能站在我们的角度来考虑问题。这次要谈的是程序延展性问题。

系统性能与延展性:恰如硬币的正反面

有时候人们会把性能和延展性混为一谈,但实际上两者是如正负极那样有所区别的。系统性能所关心的是:例如,程序的响应时间是多少?要花费多少CPU开销来进行一次请求应答?

另一方面,延展性所关心的是:当负载增加时,系统还能运作正常吗?比方说,经测量我们知道响应一次请求的时间是1s,延展性就需要关系当100或1000次请求发生时,这个并发响应时间是多少s。如果1000次的响应时间都接近1s,那么这个系统的延展性是良好的;但如果响应时间随着请求的递增而直线递增,那么这样的表现是……这样的经历,大家应该不会陌生吧。

要构架一个可扩展的程序不是件轻易的事情,但有几个核心原则能为成功之路做好铺垫。第一也是最重要的,尽最大努力保持程序的状态无关性,即程序不会在各个请求切换之间进行用户状态记录。当一个部件处于状态无关时,无论哪一个部件实例被调用,他们的作用都是相同的。这样的好处是不论在100台还是仅仅在1台机器上运行,无需复杂的设置,程序都能运作良好。这是个宏伟的目标,如果你能正确解构程序,你的程序或许就会呈现最大的状态无关性。

但是,用户状态有时候是需要被记录的。例如:当你登入一个网站时,网站需要记录你的信息来区分不同访客。一旦你的状态被记录,你随后的相关访问信息都会被一同记录下来。

在这种情况下,我们应该确保“粘性会话”在负载平衡器中被开启,意思是一旦会话被建立,所有及后的请求都应该附着到同一台机器中进行记录。这样做的好处是后续请求能够清楚知道用户的状态,他是谁和他正在做什么;反过来,倘若分而处之,那么必须把会话复写到不同服务器,才能让所有机器都知道用户的状态了。

然而,粘性有可能受制于系统弹性。假如负责收集请求的机器宕机了,那该怎么办?假如这需要劳烦用户再次登录,那对用户体验无疑是一次不小的打击。有些策略是能够帮助增强程序弹性的,不同策略对延展性的影响各有不同,有些影响是举足轻重的。这些策略包括:

 

  • 会话复制(主要的/次要的或者更多);
  • 数据库搜索;
  • 数据存储共享;
  • 富Cookies;
  • Terracotta服务器阵列;
  • 分布式缓存。

 

会话复制

会话复制是最常见的弹性措施。在这个模型中,当一个用户会话发生改变时,会话对象会被序列化,然后发送到一个或多个次服务器。一旦服务器出现宕机,负载平衡器会把当前负载重定向到次服务器。在一个简单的模型中,每个主服务器都会配备一个次服务器,这样便可以处理更多突发中断情况。但是一旦主次服务器同时中断,用户会话便也跟着丢失了。所以建议有条件的话,还是配备多个次服务器,虽然这可能会造成额外的工作量。

举例来说,当你把数据复制到五个服务器时,对于每一次变更,你都需要把会话序列化,然后交叉发送到这些服务器。这样便大大降低了系统延展性,因为需要额外的资源开销来管理复制机。在这个情况下,我们需要营运部注意这些故障转移规则,来协助进行必要服务器的管理。再者,我们不希望这些服务器是可变的(动态变更规模来满足负载要求),因为在一次精确的战略性服务器关闭措施中,需要确保会话数据不会被丢失。