【技术积累】《MongoDB实战》笔记(1)

《MongoDB实战》笔记

第一章 为现代Web而生的数据库

特性

mongodb适合做水平扩展的数据库。

mongodb把文档组织成集合,无schema。

索引

mongodb的二级索引是B树实现。

每个集合最多可以创建64个索引,

副本集

mongodb通过副本集(replication set)的结构提供了复制功能。

副本集有一个主节点(primary node)和一个或多个从节点(secondary node)构成。主节点支持读写,从节点只读。而且副本集支持自动故障转移:如果主节点出了问题,集群会选一个从节点自动将它提升为主节点,在先前的主节点恢复之后,它就变成一个从节点。

journaling日志

mongodb中,用户可以选择写入语义,决定是否开启Journaling日志记录,控制写入速度与持久性的之间的平衡,Journaling日志是默认开启的,所有写操作都会被提交到一个只能追加的日志里。

fire-and-forget

mongodb默认是fire-and-forget,即写操作通过TCP套接字发送,不要求数据库应答。如果需要应该,需要开启特殊的安全模式。安全模式可配置,还可以用于阻塞操作,知道写操作被复制到特定数量的服务器。

自动分片

mongodb是基于范围的分片方式,自动分片(auto-sharding)。单个分片由一个副本集组成,每个副本集至少三个节点,两个携带数据的副本,就能保证自动恢复,没有单点失败。

副本集

通常副本集由两个副本组成,再加上一个部署在第三台服务器上的仲裁进程(arbiter process)。对于mongodb的自动分片架构而言,其组建包含配置为预先分片的副本集的mongod进程,以及特殊的元数据服务器,称为配置服务器(config server),另外还有单独名为mongos的路由服务器向适当的分片发送请求。

_id

所有文档都要有一个主键,存储在_id字段里,只要保证唯一性,也可以输入自定义_id.如果省略了_id,会自动插入一个mongo对象ID。

第二章 MongoDB Javascript Shell

针对性更新(targeted modification)

db.users.update({"favorites.movies":"sxxx"},{$addToSet:{"favorites.movies":"ssff"},false,true)

第四个参数为多项更新,如果为false,则更新(默认)只应用于查询选择器匹配的第一个文档。

创建索引

ensureIndex({num:1}),其中1表示升序,getIndexes()验证。

在查询语句后面紧跟.explain()可以看到查询计划。

db.stats

获取数据库与集合更底层的信息,db.stats()。

stats只是辅助方法,他封装了shell的命令调用方法,等同于

db.runCommand({dbstats:1}),往runCommand传递文档定义。

看下文档定义,执行去掉无括号的版本,db.runCommand。

第三章 使用MongoDB编程程序

mongo数据驱动

mongodb数据驱动三个功能:

  • 生成mongodb对象ID,即所有文档_id字段的默认值。
  • 将所有语言特定的文档表述和BSON(mongodb的二级制数据格式)互相转化。
  • 使用mongodb的网络协议通过TCP套接字与数据库通信。

对象ID

mongodb对象ID是全局唯一的标识符,不会重复。

它由12个字节构成,

4字节时间戳,3字节机器ID,2字节进程ID,3字节计数器

如:4c291856 238d3b 19b2 000001

可以看到对象ID包含了时间戳,从而提供对象创建时间(秒)

BSON数据类型

BSON规范包含了19种数据类型,如UTF-8字符串,32位和64位整数,双精度浮点数,布尔值,时间戳,UTC时间(datetime),针对模糊对象的大数据(opaque blob),部分语言支持的符号类型(symbol type)

BSON格式

文档转化为BSON:头部4字节表明文档的大小,接下来N个键值对,

每对都由一个表示其类型的字节开头,随后由null结尾的字符串表示键名,然后是被存储的值,最后是一个null字节表示文档结束。如图:

对象ID请使用BSON对象

如果要存储mongodb对象id,应该使用mongodb对象ID,

而不是字符串,除了遵循对象ID的存储惯例,BSON对象还能比字符串省一半以上的空间。

安全模式

mongodb安全模式写入时,驱动会在插入消息后追加一条getlaterror命令。它做了两件事,getlasterror命令需要与服务器做一次通信,它确保写操作已经送达服务器。第二,验证服务器在当前链接中没有抛出任何错误。

第四章 面向文档的数据

事务与原子性

mongodb不支持事务,但它支持多种原子更新操作,用于复杂文档。

数据库文件

mongodb创建数据库时候,会在磁盘上分配一组数据文件,所有集合,索引和数据库其他元数据都保存在这个文件里。数据文件都被放在启动mongod时指定的dbpath里,如未指定,则保存在/data/db里。

如图:

各文件说明:

mongod.lock文件,其中存储了服务器进程ID(不要删除,会影响异常恢复)。

数据库文件本身依赖所属的数据库命令,garden.ns是第一个生成的文件,ns后缀表示namespaces,数据库每个集合和索引都有自己的namespace,namespace的元数据都放在这个文件里。

默认情况下.ns文件固定在16MB,大约可以存24000个命令空间,也就是数据库索引和集合总数不能超过24000,通过---nssize配置。

看文件大小garden.0 64mb,garden.1 128mb,这些是mongoldb预先分配的数据文件,新数据文件大小是前一个的两倍,直达上限2G,通过---noprealloc和---small files配置。

空间使用

stats命令检查已使用空间和已分配空间。

如图:

其中fileSize字段表示数据分配文件空间的总和。也就是前面garden数据库两个文件之和。

dataSize是数据库BSON对象的实际大小,storageSize是dataSize加上集合增长预留的额外空间和未分配的已删除空间。

indexSize是索引大小的总和。

另:集合的每个数据文件里按照块分配文件,这些块称为区段(extent)。storageSize就是集合区段所分配空间的总和。

固定集合capped collection

原本只针对高性能日志场景设计的。大小固定,满了以后,后续插入会覆盖集合最先插入的文档。

与标准集合的区别:

  • 固定集合默认不为_id创建索引,插入更快,也可以自己构建。(不定义索引情况下,最好把固定集合用于顺序处理的数据结构,而非用于随机查询,为此,mongodb提供了一个特殊的排序操作符$natural,按照文档的插入顺序返回文档。

    db.user.actions.find().sort({"$natural":-1}))

  • 固定集合不能删除文档,不能执行任何增加文档大小的更新操作。

mongodb使用固定集合来完成复制,每个副本集的成员都会把所有的写操作记录到一个特殊的oplog.rs固定集合里。从节点顺序读取这个集合内容,然后应用到自己数据库内。

BSON数字类型

BSON只定义了三种数字类型,double,int,long。而javascript只支持一种数据类型Number,等价于IEEE的双精度浮点数。如果要存储为整数,需要NumberLong()或者NumberInt

db.numbers.save({n:NumberLong(5)})

BSON type

每种BSON类型有一个整数标识,

db.numbers.find({n:{$type:18}});

BSON缺少对小数的支持

BSON 日期与时间

javascript里月份是从零开始

new Date(2011,5,11)是2011年6月11号

文档大小限制

v2.0中BSON文档大小限制在16MB,不同版本输入db.isMaster查看maxBsonObjectSize字段。

大小限制的原因,一是防止设计深层嵌套,二是性能,在服务器端查询大文档,在讲结果发送给客户端前需要将文档复制到缓冲区里,同时驱动反序列化开销很大。

第五章 查询与聚合

find与findOne

find返回的是游标对象,findOne返回一个文档

分页

分页可以使用:skip,limit,sort,如:

products = db.products.find({'category_id':category['_id']}).skip((page_number-1)*12).limit(12).sort({helpful_votes:-1})

集合操作

in,all,$nin,接受数组参数

布尔操作符

ne,not,or,and,$exists

如:

db.products.find('details.manufacturer':'ACME',tags:{$ne:"gardening"})

or可以应用于条件选择器数组

db.products.find({$or:[{'details.color':'blue'},{'details.manufacturer':'ACME'}]})

数组下标表示

db.users.find({'address.0.state':'NY'})

其中0表示数组的第一个

$elemMatch

用于子文档的多个条件限制查询

db.users.find({address: {$elemMatch: {name: 'home',state: 'NY'}})

size

根据数组大小查询,$size,不走索引,只支持精确查找。推荐将数组大小作为一个字段放入集合中。

$where

可以手动编写javacript执行

db.reviews.find({$where: "function(){ return this.helpful_votes > 3;}"})

this指向当前文档,不走索引,性能低。

可以配合一般查询语句缩小遍历集合后使用,

正则表达式

使用了忽略大小写的选项则无法在查询中使用索引,就算在前缀匹配中也是如此。原生正则表达式例子:

db.reviews.find({user_id: ObjectId("xxxx"), text: /best|worst/i})

不能用原生,则用regex和options

投影

返回结果中去掉不需要的字段,如

db.users.find({}, {address:0, payment_method:0})

$slince

返回头多少条或者尾多少条信息,也可以接受两参数的形式,一个是跳过多少,一个是返回元素个数限制。

$skip

类似于offset ,最好省略skip,添加一个范围条件,提高性能。

min与max

不提供min(),max(),自己实现,按照某字段降序排序,再limit。

distinct

默认覆盖整个集合,返回字段的不同值集合,也可传入查询器。

group

group最少需要三个参数,第一个参数定义对key怎么分组,第二个参数是聚合的js函数,叫reduce函数,第三个参数为reduce函数初始文档。

results = db.views.group({key:{user_id:true}, initial = {review:0, vote:0.0},
    reduce = function(doc, aggregator){
        aggregator.views += 1.0;
        aggregator.votes += doc.votes;
    },
    finalize: function(doc){
        doc.average_votes = doc.votes /doc.reviews;
   }
});

distinct与group结果集合大小限制

distinct和group返回结果集合不能超过16MB,因为这两个命令是对特殊$cmd集合的查询。同时group不会处理多余10000个唯一键,不能满足的情况,使用map-reduce。

map-reduce

定义一个map函数,内部调用emit(),emit方法第一个参数是分组依据的key,第二个参数是包含执行reduce的文档。

map = function(){
	var shipping_moth = this.purchase_date.getMonth() + '-'+this.purchase_data.getFullYear();
	var items = 0;
	this.line_items.forEach(function(item){
		tmpItems += item.qunatity;
	});
	emit(shipping_month,{order_total:this.sub_total, item_total:0})
}
reduce = function(key,values){
	var tmpTotal = 0;
	var tmpItems = 0;
	tmpTotal += doc.order_total;
	tmpItems += doc.items_total;
	return ({total: tmpTotal, items:tmpItems)});
}

第六章 更新、原子操作与删除

原子性

所有发往核心服务器的更新都是原子的,以文档为单位进行隔离。说更新操作符是原子性的是因为他们能在不先查询的前提下完成更新。

findAndModify

db.orders.findAndModify({
	query:{}
	update:{}
})

默认情况下findAndModify返回更新前的文档,要返回更新后的文档,要制定{new:true}

多文档更新

db.products.update({},{$addToSet:{tags:'cheap''}}, false,true)

upsert

如果查询选择器匹配到文档则更新,无匹配则新增。一次只插入或更新一个文档。

$inc

递增,递减操作

$unset

能删除文档中特定的键,但作用在数组上,只是置为null,要删除删除,调用pull和pop.

addToSet和each

如果想在一个操作里向数组添加多个唯一的值,必须结合each操作符来使用addToSet

db.products.update({slug:'shovel'},{$addToSet: {'tags': {$each: ['tools','dirts']}})

$pop

删除最后添加的内容

$pull

与$pop类似,但更高级,可以明确制定删除哪个数组元素,而不是位置。

基于位置的更新

query = {_id: ObjectId("4c4d1476238d3b4dd5003981"),
'line_items.sku':"10027"}
update = {$set: {'line_items.$.quantity':5}}
db.orders.update(query,update)

在line_items.quantity字符串中表示位置操作符,如果查询选择器匹配到了这个文档,那么有10027这个sku的文档的下标就会替换位置操作符,从而正确更新文档。

mongodb锁策略

全局锁,已废弃,具体参考mongodb jira

写锁让步,可以使用$atomic参数控制隔离执行,不被暂停

更新性能

文档更新三种:

  • BSON大小不改变
  • BSON大小改变,头四个字节需要修改,或者增加字段
  • 重写文档,预分配磁盘放不下,需要移动,为降低这类开销,mongodb会根据每个集合的情况动态调整填充因子(padding factor),也就是如果一个集合会发生很多重新分配的情况,会自动增加填充因子。填充因子*插入文档的大小后,为要额外创建的空间。

第七章 索引与查询优化

$or与索引

or查询里,每个or查询子句,都能使用不同的索引,但每个子句本身只能使用一个索引。

tip

就算拥有正确索引,还是可能得不到快速的查询,索引和数据集无法完全加入内存,是要考虑的问题。

标准索引

索引是B树,集合存储为双向列表

mongodb的B树实现里,新节点会被分配8192字节,也就是说实际上每个节点能包含几百个键。请牢记,默认情况下,B节点内容有意维持在60%左右。

稀疏索引

当集合中大量文档都不包含被索引键。创建时指定{sparse:true}。在稀疏索引中,只会出现被索引键有值的情况。

声明索引要小心

构建会花很长时间,则无法中止。最明智的建议,将索引构建当成某类数据库迁移来看。

后台索引

不暂停数据库访问,在后台构建索引,对读操作主动让步。{background:true}

备份

如果想在备份中包含索引,需要直接备份mongodb的数据文件。

reIndex

reIndex,重建索引,占写锁,实例暂时无法使用。

慢查询

mongod服务器启动,---slowms 50会把筛选日志。

剖析器

use stocks
db.setProfilingLevel(2.50)

第一个参数,2表示读写,第二参数表示超过50ms都写日志。

查询优化器的三个规则

  • 避免scanAndOrder,使用索引排序
  • 通过有效索引约束来满足所有字段
  • 如果查询包含范围查询或者排序,对于选择的索引,其中最后用到的键需能满足该范围查询或者索引

查询计划

explain,传入true,输出查询计划

查询计划器的缓存

在发现了一个成功的计划之后,会记录下查询模式(query pattern)/nscanned的值以及索引说明,如

{pattern: {stock_symbol:'equality', close: 'bound'}, index:{stock_symbol:1}, scanned:894}

查询模式记录下每个键匹配的类型,你正请求对stock_symbol的精确匹配(相等),对close的范围匹配,就会使用这个索引。

查询计划期缓存过期

  • 对集合执行了100次写操作
  • 在集合上增加或删除了索引
  • 虽然使用了缓存的查询计划,但工作量大于预期,及nscanned超过缓存nsscanned的10倍

第八章 复制

两种复制风格

mongodb提供了两种复制风格:主从复制,副本集。都是主节点写,异步同步到从节点。

推荐使用副本集,因为支持自动故障转移,只有mongodb需要超过11个从节点,则需要主从复制。

虽然副本是冗余的,但副本不是备份的替代品。

副本集

副本集,至少三个节点,包括一个仲裁节点,选主。

启服务略

isMaster查看副本信息,rs.status()更详细信息,启动完成stateStr字段会从revocering到primary,secondary,arbiter。

测试选主

ctrl-c,kill -2或者连接上主节点,db.shutdownServer()

副本集的基础机制

副本集依赖于两个基础机制,oplog和心跳。

oplog是个固定集合,位于每个复制节点的local数据库里,记录所有对数据的变更。查看当前副本状态的基本信息,db.getReplicationInfo()

local库

replset.minvalid指定副本集成员的初始同步信息

system.replset保存了副本集配置文档

me和slaves实现写专注

system.indeses标准索引

副本同步

从节点从主节点赋值oplog,做三件事

  • 查看oplog最后一条的时间戳
  • 查询主节点oplog里所有大于此时间戳的记录
  • 将这些记录添加到自己库里

心跳检测

默认情况下,每个副本集成员每两秒ping一下其他所有成员。

如果没有多数节点,主节点会自动降为从节点。

回滚

当mongodb从节点升为主节点,会触发其他从节点回滚。略

在数据路径下rollback子目录保存了呗回滚的写操作,对每个回滚写操作集合,创建独立BSON文件,通过bsondump查询,mongorestore恢复。

重新配置副本集

无论何时,重新配置副本集导致重新选举新的主节点,那么所有客户端的链接都会被关闭,防止fire-and-forget风格的写操作。

配置文件

  • arbiterOnly:仲裁节点只存储配置数据。
  • priority,决定了选举的权重,设置为0,表示被动节点,永远不会被选为主节点,可以用于灾难恢复节点。
  • buildIndex:如果永远不会成为主节点,priority为0,可以设置。
  • slaveDelay:如果要设置大于0,务必保证priority为0.
    其他略

写关注

getlastError(w,timeout,j:true)

第一个参数为需要同步到服务器的数量,w可以等于"majority"。

第二参数为超时时间。

j表示强制同步到journaing日志。

读扩展

Mongo:: ReplSetConnection.new(['arete', 40000], ['arete',40001], :read => :secondary)

read的设置,读会从选择附近一个从节点读取。

副本扩展无法处理一致性读,需要将一致性读的部分抽取出来。

第九章 分片

分片集群

分片集群由分片,mongos路由器和配置服务器组成。

mongos路由器

通常运行于应用服务器相同的机器上,提供所有读写请求的统一系统视图。mongos进程是轻量级非持久化的。

配置服务器

持久化分片集群的元数据,包括集群配置,每个数据库集合特定范围数据的位置,一份变更记录,保证数据在分片之间迁移的历史。

mongos对配置服务器写入时候,使用二阶段提交。配置服务器最好三个以上,同时存在于不同机器实现冗余。

分片与块

分片是基于范围的,分片建(shared key)。

块(chunk),位于分片种的一段连续的分片范围,如分片的重点是块的拆分和迁移,拆分是对元数据的逻辑操作,迁移是均衡器(balancer)处理的物理操作。

分片索引

每个分片都维护了自己的索引,分片集合上的索引声明,会对所有分片起效。

分片集合只允许在_id和分片键上添加唯一索引。

分片键无法修改。

拓扑结构

运行mongodb两分片集群,一共要启动九个进程,每个副本集三个mongod,外加三个配置服务器。其中副本集是资源密集型,需要暂用独立的机器,仲裁节点不需要,配置服务器间不共用机器。

考虑到灾难恢复

监控

mongos上运行serverStatus和currentOp看到所有分片的聚合统计信息,或者查询config数据库,对于不均衡的块,进行split,movechunk。

增加分片

考虑向新分片移动的时间,预计每分钟100-200MB。在现有索引和工作集达到分片的90%就要开始计划。

移除分片

removeshard,会删除块,重新分配到其他分片上。

集合去分片

导出集合,再用不同名字将数据恢复到一个新的集合里。用mongodump连接mongos导出。

第十章 部署与管理

时钟

不同服务器见都是用NTP,网络时间,在linux上使用ntpd守护进程。

journaling日志

  • 会降低写操作性能
  • 不保证不丢失写操作,只保证恢复一致状态。每100ms将写缓冲同步到磁盘。

副本集验证

启动---keyfile指定密码文件,为至少6个base64字符集。

服务器监控

  • serverStatus
    输出页错误,B树访问率,打开连接数,总的插入,更新,查询和删除等。globalLock会显示所有锁的时间,currentQueue显示读写队列。mem部分是内存,理想状况下所有工作集都应该放到内存中。
  • top
    操作计数器,显示操作的平均耗时。
  • db.currentOp()
    返回正在运行的所有操作。要杀掉,db.killOp(opid)
  • mongostat
    显示实时活动视图,以固定时间查询服务器信息,显示统计数据的矩阵,从每秒插入数到常驻内存量,再到B树页丢失频率。
  • web控制台
    mongod的端口+1000为web控制台端口,一般为28017.

诊断工具

mongosniff,mongodump略

数据备份与恢复

两种方式

  • 一个是mongodump,mongorestore
  • 一个是基于原始数据的备份,大多数情况更快,但是要求锁定数据库,可以选择锁定从节点(可以保留全部索引)。

锁定数据库

use admin
db.rumCommand({fsync:1,lock:true})

此时数据库是写锁定的。

解锁:

db.$cmd.sys.unlock.findOne()

再查看db.currentOp是否解锁

压紧与修复

mongod ---repair

或者单个数据库

use cloud-docs

db.runCommand({repairDatabase:1})