MongoDB数据安全机制

在MongoDB发展的早期阶段,确实有一些数据持久化的问题没有处理好,特别是一些默认值的选定上。随着mongoDB的发展和社区的不断贡献,journal和replicaSet等功能也是越来越完善,到version 2.6之后,就已经能够使用在企业级的生产环境,也有大量的成功案例。尤其是version 3.2默认采用新的存储引擎WiredTiger,性能更是明显提升。

历史发展问题

在早期版本中,坊间有很多传说MongoDB会丢数据,也经常会听说mongoDB丢数据的案例。这其中有mongoDB早期版本的不完善的原因,也有不少是由于用户对mongoDB的安全机制的不了解导致。

Journal配置

在MongoDB 2.0之前,Journal没有被支持或者不是一个默认开的选项。在MySQL,PostgreSQL,Oracle等关系型数据库里都有一个Write Ahead Log(RedoLog)的机制用来解决因为系统掉电或者崩溃时导致内存数据丢失问题。MongoDB的journal就是实现这个目的的一种WAL日志。在MongoDB2.0之前,Journal没有被支持或者不是一个默认开的选项。所以当你进行写入操作时。在没有Journal的情况下,数据在写入内存之后即刻返回给应用程序。而数据刷盘动作则在后台由操作系统来进行。MongoDB会每隔60秒强制把数据刷到磁盘上。那么大家可以想象得到,如果这个时候发生了系统崩溃或者掉电,那么未刷盘的数据就会彻底丢失了。在大数据量的应用中,出现数据丢失的概率也就非常大了。自从2.0开始,MongoDB已经把Journal日志设为默认开启。

getLastError命令

Mongodb的写操作默认是没有任何返回值的,这减少了写操作的等待时间,同时也带来了安全隐患。不管有没有写入到磁盘或者有没有遇到网络错误错误,它都不会报错,客户端发送写命令之后就返回,不会等服务器返回操作的结果。 以java为例,举个例子:当我们为字段建立了一个唯一索引,针对这个字段我们插入两条相同的数据,不设置WriterConcern或者设置WriterConcern.NORMAL模式,这时候即便抛出异常,也不会得到任何错误。insert()函数在java中的返回值是WriteResult类,

WriteResult( CommandResult o , WriteConcern concern ){
        _lastErrorResult = o;
        _lastConcern = concern;
        _lazy = false;
        _port = null;
        _db = null;
    }

这个类实际上包装了getlastError的返回值,但是这时候WriteResult的_lastErrorResult属性实际上是空的。因为dup key错误是server error,只有在WriterConcern.SAFE或更高级别的模式下,才会得到server error。 MongoDB的读写操作是没有事务,也不完全有顺序的。在多线程模式下读写Mongodb的时候,如果这些读写操作是有逻辑顺序的,那么这时候也有必要调用getlasterror命令,用以确保上个操作执行完下个操作才能执行。在实际开发中,我们都是通过驱动程序操作MongoDB,而大多数驱动程序都是使用连接池去连接mongodb,这导致在多线程的程序中,很有可能采用的不是同一个连接。通过不同连接发送的操作命令,MongoDB服务器并不完全按照接收的顺序来执行。这就有可能导致前面插入的数据,后面查询不到的现象,尤其是高并发程序中。

数据库会为每一个MongoDB数据库连接创建一个队列,存放这个连接的请求。当客户端发送一个请求,会被放到队列的末尾。只有队列中的请求都执行完毕,后续的请求才会执行。所以从单个连接就可以了解整个数据库,并且它总是能读到自己写的东西。 注意,每个连接都有独立的队列,要是打开两个shell,就有两个数据库连接。在一个shell中执行插入,之后在另一个shell中进行查询不一定能得到插入的文档。然而,在同一个shell中,插入后再进行查询是一定能查到的。手动复现这个行为并不容易,但是在繁忙的服务器上,交错的插入/查找就显得稀松平常了。当开发者用一个线程插入数据,用另一个线程检查是否成功插入时,就会经常遇到这种问题。有那么一两秒钟时间,好像根本就没插入数据,但随后数据又突然冒出来。 使用Ruby、Python和Java驱动程序时要特别注意这种行为,因为这几个语言的驱动程序都使用了连接池。为了提高效率,这些驱动程序都和服务器建立了多个连接(一个连接池),并将请求分散到这些连接中去。好在它们都提供一些机制来确保一系列的请求都由一个连接来处理。MongoDBwiki(http;//dochub.mongodb.org/drivers/connections)上有不同语言连接池的详细信息。

当前现状

随着MongoDB的发展和开源社区的贡献,到当前3.2版本,数据安全的问题早已不复存在。Journal日志早在2.0版本就为默认开启,在大多数的语言驱动中,插入、更新和删除操作都会等待Server返回的确认信息。

By default, all write operations will wait for acknowledgment by the server, as the default write concern is WriteConcern.ACKNOWLEDGED.

那么在开发中,MongoDB如何来确保我们的数据是安全的呢?在google.groupuser上,mongo的开发者有一段这样的解释:

By default: Collection data (including oplog) is fsynced to disk every 60 seconds. Write operations are fsynced to journal file every 100 milliseconds. Note, oplog is available right away in memory for slaves to read. Oplog is a capped collection so a new oplog is never created, old data just rolls off. GetLastError with params: (no params) = return after data updated in memory. fsync: true: with --journal = wait for next fsync to journal file (up to 100 milliseconds); without --journal = force fsync of collection data to disk then return. w: 2 = wait for data to be updated in memory on at least two replicas.

我们可以看到:

  1. 如果打开journal,那么即使断电也只会丢失100ms的数据,这对大多数应用来说都可以容忍了。从1.9.2+,mongodb都会默认打开journal功能,以确保数据安全。而且journal的刷新时间是可以改变的,2-300ms的范围,使用 --journalCommitInterval 命令可以设置。
  2. Oplog和数据刷新到磁盘的时间是60s,对于复制来说,不用等到oplog刷新磁盘,在内存中就可以直接复制到Sencondary节点。

以MongoDB Java Driver中的GetLastError命令为例,我们从驱动程序接口开始分析MongoDB对数据安全的实现机制。

WriteProtocol.java  
    private BsonDocument createGetLastErrorCommandDocument() {
        BsonDocument command = new BsonDocument("getlasterror", new BsonInt32(1));
        command.putAll(writeConcern.asDocument());
        return command;
    }
    
    
WriteConcern.java
    /**
     * Gets this write concern as a document.
     *
     * @return The write concern as a BsonDocument, even if {@code w <= 0}
     */
    public BsonDocument asDocument() {
        BsonDocument document = new BsonDocument();

        addW(document);

        addWTimeout(document);
        addFSync(document);
        addJ(document);

        return document;
    }
    
    

所有的插入、更新和删除操作底层都会调用到createGetLastErrorCommandDocument方法得到getlasterror命令的Document,该document包含Object w, Integer wTimeoutMS, Boolean fsync, Boolean journal四个属性。所以通过分析Driver的接口实现,我们知道客户端就是通过这四个参数来调用MongoDB Server实现数据安全。 这里,我们先看一下WriteProtocol的构造函数。

WriteProtocol.java 

    // Private constructor for creating the "default" unacknowledged write concern.  Necessary because there already a no-args
    // constructor that means something else.
    private WriteConcern(final Object w, final Integer wTimeoutMS, final Boolean fsync, final Boolean journal) {
        if (w instanceof Integer) {
            isTrueArgument("w >= 0", ((Integer) w) >= 0);
        } else if (w != null) {
            isTrueArgument("w must be String or int", w instanceof String);
        }
        isTrueArgument("wtimeout >= 0", wTimeoutMS == null || wTimeoutMS >= 0);
        this.w = w;
        this.wTimeoutMS = wTimeoutMS;
        this.fsync = fsync;
        this.journal = journal;
    }

下面,我们就分析一下这四个参数分别表示什么。

  • w参数

When running with replication, this is the number of servers to replicate to before returning. A w value of 1 indicates the primary only. A w value of 2 includes the primary and at least one secondary, etc. In place of a number, you may also set w to majority to indicate that the command should wait until the latest write propagates to a majority of the voting replica set members. 在复制集环境中,w表示数据需要复制的服务器数量。1表示只写入primary Server就返回,2表示写入primary Server和至少一个secondary Server,其他数字同理。除了数字之外,w的值也可以被设置成“majority”,表示需要等到写操作被同步到大多数的Server之后才返回。

w参数表示的是在复制集环境中,最后一个写操作返回之前,需要等待写命令同步到几个Server(默认值为1)。w默认值为1,就兼顾了基本的数据安全和性能。
  1. w也可以被设置为0或者-1,表示每一个写入操作,MongoDB都不会返回一个是否成功的状态值。这个级别是写入性能最好但也是最不安全的级别。有不少时候MongoDB用来保存一些监控和程序日志数据,这个时候如果你有1、2条数据丢失,是不会对应用程序有什么影响的。这是version 2.2之前的默认设置,也是很多人觉得MongoDB数据不安全的主要原因。

  2. w=1的意思就是对每一个写入MongoDB的操作都会确认操作的完成状态,不管是成功还是失败。当然这个确认只是基于主节点的内存写入。这样,就可以侦测到重复主键,网络错误,系统故障,唯一索引或者是无效数据等大多数错误,这也是当前版本的默认设置。在这种情况下,出现因为系统故障掉电原因而导致的数据丢失只会是我们之前提到的日志没有及时写入磁盘的情况。如果你不能接受因为停电或系统崩溃而引起的可能的100ms的数据损失,那么你可以选用更安全的设置。

  3. w=2,3,4...,此时表示除了可以确保写操作写入了主节点的内存,还可以确保写操作被同步到了其他2-1,3-1,4-1个Server上。假如环境配置了3台Server,1主2从,w=2就可以保证数据被写入保证1台primary Server和1台secondary Server的内存。如果要是担心这时候primary Server不可用导致数据不一致,还可以设置w=3来确保数据被同步到所有的Server。

  4. 还有一种情况,假如Server数量不确定或者可能有变化,我们可以设置w值为“majority”。“majority” 指的是“大多数节点”,使用这个写安全级别,MongoDB只有在数据已经被复制到多数个节点的情况下才会向客户端返回确认。

    一般来说,MongoDB建议在集群中使用 {w: “majority”} 设置。在一个集群是健壮的部署的情况下(如:足够网络带宽,机器没有满负荷),这个可以满足绝大部分数据安全的要求,因为MongoDB的复制在正常情况下是毫秒级别的,往往在Journal刷盘之前已经复制到从节点了。 为什么要说绝大多数呢? 假如集群配置1主2从,w=3 数据还在primary Server内存,写操作的journal日志也在内存中,写操作也被同步到其中一台secondary Server。这时primary Server瞬间停电,数据没有入盘,journal日志也还没来得及入盘,剩下两台secondary Server一台有数据,一台无数据。此时之前的primary Server恢复可用,情况变成了两台server无数据,一台有数据,就导致了之前写入的数据被回退了。如果写入数据的时候,能够确保journal日志入盘,这种丢数据的情况就完全可以避免了。

  • wTimeoutMS

Specify a value in milliseconds to control how long to wait for write propagation to complete. If replication does not complete in the given timeframe, the getLastError command will return with an error status. 以毫秒为单位指定一个值,用来控制等待写传播来完成传播的时间。如果复制没有在给定的时间内完成,则GetLastError函数命令将错误状态返回。

等待超时时间就比较好理解,如果写操作在指定的时间内还没有执行完或同步完,则将返回错误的状态。wTimeoutMS的值要根据集群的Server数量和所处的网络环境来决定,wTimeoutMS值太高也有可能会导致连接数过高。
  • j参数

If true, wait for the next journal commit before returning, rather than waiting for a full disk flush. If mongod does not have journaling enabled, this option has no effect. If this option is enabled for a write operation, mongod will wait no more than 1/3 of the current commitIntervalMs before writing data to the journal. 如果参数j的值为true,则只要等到下一次journal日志提交(写磁盘)就会返回,而不需要等待一个完整的磁盘刷新。如果的mongod未启用日志记录,此选项没有任何效果。如果这个选项被用于写入操作,在最终写入到日志之前,mongod将最多等待commitIntervalMs(提交间隔)值的1/3。

使用这种方式意味着每一次的写操作会在MongoDB实实在在的把journal入盘以后才会返回。当然这并不意味着每一个写操作就等于一个IO。MongoDB并不会对每一个操作都立即入盘,而是会等最多30ms,把30ms内的写操作集中到一起,采用顺序追加的方式写入到盘里。在这30ms内客户端线程会处于等待状态。这样对于单个操作的总体响应时间将有所延长,但对于高并发的场景,综合下来平均吞吐能力和响应时间不会有太大的影响。特别是给journal部署一个对顺序写有优化并且IO带宽足够的专门存储系统的话,这个对性能的影响可以降到最低。
  • fsync

The primary use of fsync is to flush and lock the database for backups. The fsync operation blocks all other write operations for a while it runs. 主服务器使用fsync 来强制写入磁盘和备份时候的锁住整个数据库。 fsync运行时会阻塞数据库的其他写操作。

严格来说,FSYNC是一个管理员命令,它迫使所有的数据刷新到磁盘。你不应该在你的代码中使用它,至少不是经常使用,它常用于锁定备份数据库。MongoDB中的数据安全通过复制/分片/日志来实现,而不是强制写入,否则就违背了MongoDB的原始初衷了。在Java驱动程序包中的WriteConcern类,我基本上从来不使用WriteConcern.FSYNC_SAFE和WriteConcern.FSYNCED。

总的来说,对于数据安全,MongoDB2.6+ 就有了非常完善的机制,而且还很灵活。从不返回结果、确认主服务器写内存、确认主服务器写journal日志、确认同步到大多数服务器到强制写入磁盘,MongoDB数据安全级别逐渐提高。开发者可以根据不同的应用场景选择适合的安全级别,在数据安全和写操作的性能之间找到平衡。

最佳实践

根据以往踩过的坑和同行经验,总结出以下几点建议。

  1. 如果没有特殊要求,最低级别也要使用WriterConcern.SAFE,即w=1。
  2. 对于不重要的数据,比如log日志,可以使用WriterConcern.NONE或者WriterConcern.NORMAL,即w=-1或者w=0,省去等待网络的时间。
  3. 一般来说,在集群中使用WriterConcern.MAJORITY设置。在一个集群是健壮的部署的情况下(足够网络带宽,机器没有满负荷),这个可以满足绝大部分数据安全的要求。因为MongoDB的复制在正常情况下是毫秒级别的,往往在Journal刷盘之前已经复制到从节点了。如果你追求完美,那么可以再进一步使用 {w: “majority”,j:1} 。
  4. 对大量的不连续的数据写入,如果每次写入都调用getLastError会降低性能,因为等待网络的时间太长,这种情况下,可以每过N次调用一下getLastError。但是在Shard结构上,这种方式不一定确保之前的写入是成功的。
  5. 对连续的批量写入(batchs of write),要在批量写入结束的时候调用getlastError,这不仅能确保最后一次写入正确,而且也能确保所有的写入都能到达服务器。如果连续写入上万条记录而不调用getlastError,那么不能确保在同一个TCP socket里所有的写入都成功。这在并发的情况下可能就会有问题。避免这个并发问题,可以参考如何在一个链接(请求)里完成批量操作,MongoDB Java Driver并发
  6. 对数据安全要求非常高的的配置:j=true,w="majority",db.runCommand({getlasterror:1,j:true,w:'majority',wtimeout:10000})
  7. 大多数语言的驱动程序都可以在MongoOption中设置,MongoOption中的这些设置是全局的,对于单独的一个连接或一次操作,还可以分别设置。

传说中MongoDB 丢数据的事情,已经成为了历史。


参考资料

MongoDB writeConcern原理解析 MongoDB Doc MongoDB中文社区

转载于:https://my.oschina.net/u/1774673/blog/825091

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值