|
|
爱吹牛的稀饭 · 如何列举Bucket的所有文件、指定前缀的文 ...· 4 周前 · |
|
|
踢足球的黑框眼镜 · post重定向之后如何看post的respo ...· 1 年前 · |
|
|
有情有义的馒头 · python使用os和shutil模块进行文 ...· 2 年前 · |
|
|
文武双全的鸵鸟 · C语言字符串详解-腾讯云开发者社区-腾讯云· 2 年前 · |
|
|
捣蛋的铁链 · Spring ...· 2 年前 · |
|
|
干练的稀饭 · datatable - 聂欢 - 博客园· 2 年前 · |
Druid 是一个实时分析型的数据库,用于大规模实时数据导入、快速查询分析的场景,包括网站访问点击流分析、网络性能监控分析、应用性能指标存储与分析、供应链分析、广告分析等。 Druid 的核心集成了数据仓库、时序数据库、日志搜索系统的设计,主要包含如下特性: 列式存储:Druid 使用列存方式组织数据,访问时可按需加载访问到的列,支持快速的扫描和聚合计算能力;同时数据按列式存储,能极大的提升数据的压缩率。 分布式可扩展:Druid 集群可扩展至上百台服务器,可以高并发出力读写请求,提供每秒百万级的数据导入,以及亚秒级的查询延时。 支持实时及批量导入:Druid 支持实时或批量方式导入数据,非常方便点支持从 Kafka、Hadoop 等数据源导入数据。 高可用&负载均衡:Druid 集群支持在线的增加、移除服务节点,集群会进行自动的负载均衡,当有节点故障时,Druid 通过也可通过多副本高可用的方式自动 Failover。 云原生架构:Druid 将数据存储在外部 Deep Storage(例如 云存储、HDFS 等),即使 Druid 服务节点故障,也不影响数据的可靠性。 索引加速:Druid 通过位图方式自动对数据建索引,支持快速的索引过滤。 时间分区:Druid 会先将数据按时间分区,也可根据其他方式进一步分区,基于时间范围的查询只会访问对应时间范围内地数据。 预聚合:Druid 支持在导入数据时对数据进行提前的聚合分析,例如sum、count、min、max等,作为数据的元数据存储,当实际访问时,可直接访问预聚合好的数据。 SQL 支持:Druid 同时支持 SQL、HTTP 方式访问,表达能力强,灵活方便。 Druid 数据模型 Coordinator 负责集群的协调及数据高可用 Overlord 控制集群数据导入任务的分配 Broker 处理客户端查询请求 Router 是可选的路由组件 Historical 负责可查询数据的存储 MiddleMangager 负责数据的导入 Druid 的各个组件可以随意部署,但根据组件的职能,会分成三类,每一类组件建议在服务器上混部。 Master Servers:运行集群的 Coordinator 与 Overlord 控制类的组件。 Query Servers:运行集群查询类组件,包括 Broker、Router Data Servers:运行集群数据导入、存储相关组件,包括 Middle Managers、Histricals Druid 本身不存储数据,数据的存储依赖于外部的组件,数据的存储(Deep Storage)依赖外部的存储,例如 AWS S3、阿里云 OSS、HDFS 等分布式存储,云数据存储依赖 MySQL、PostgreSQL 等数据库;依赖 Zookeeper 实现服务发现、Leader 选举等功能。 Deep Storage Druid 本身不存储数据,而将数据存储到外部的 Deep Storage,由 Deep Storage 保证数据的可靠存储,例如 AWS S3、阿里云 OSS、HDFS 等分布式存储。 Druid 的数据会按数据顺序组织,并按时间维度对数据进行分区存储,一段时间范围的数据会存储到一起,组成一个 Segment。数据在 Segment 里会按列存方式进行压缩存储,并对 Dimension 数据建立索引。 Segment 结构 Druid 的所有数据都包含时间戳列,还包含多个 Dimensions 以及 Metrics 列,其中 Dimension 列可支持快速过滤、聚合,Druid 在存储 Dimension 列时,会进行压缩存储,并通过位图方式建索引,每一列的数据包含 Dictionary:存储列值到 整型 ID 的映射 Column Data:根据 1产生的一系列的整型 ID,进行压缩存储 Inverted Index(Bitmaps):针对 Column 里每个不同的 value,会建一个位图倒排索引 比如 Page 列的存储,包含 "Justin Bieber", "Ke$ha" 两个取值,该列对应的存储类似如下三个部分 1: Dictionary that encodes column values "Justin Bieber": 0, "Ke$ha": 1 2: Column data 3: Bitmaps - one for each unique value of the column value="Justin Bieber": [1,1,0,0] value="Ke$ha": [0,0,1,1] 当某一段时间范围内地数据量很大时,在将数据存储为 Segments 时,可以采用 sharding 策略,比如按文件大小切分 Segments、或根据指定的 Dimension 进行 Hash 分到多个 Segments,在检索的时候,能进一步减少需要查询的数据。 Druid 支持从 Kafka、Hadoop 里导入数据,数据导入以 Task 方式进行,Overlord 负责导入任务的分配,Middle Manager 负责实际的数据导入,数据会先写到 Middle Manager 的内存,积累到一定大小或时间窗口后,数据会组织为 Segment 写到 Deep Storage,并将 Segment 的元数据写入到 Metadata Storage。 Coordinator 会周期性的检测 Metadata Storage,当发现新的 Segment 产生时,会将 Segment 根据负载情况分给其中的部分 Historical(根据副本数) 节点管理,Historical 节点接管 Segment 的管理,这部分 Segment 即可用于查询。 Broker 接收数据的查询请求,根据 Metadata 的信息,计算出查询关联的 Middle Managers、Historicals 节点,并将请求发送到对应的节点, Middle Managers、Historicals 根据查询的时间范围,找出所有可能包含查询数据的 Segments,并从中过滤出满足条件的数据,Broker 负责将查询结果进行汇总返回给客户端。 Druid 与传统数据库通过读写 API 写入数据的方式不同,通过 Pull 方式拉取数据,对接常用的 Kafka、HDFS等大数据生态数据源。 借助外部可靠的 Deep Storage 和 Meatadata store 来实现数据、元数据的存储,将 Druid 从数据存储的高可靠管理中解放,让各个组件的实现都非常轻量; Druid 的实现高度模块化,每个模块有独立的职能,但因为组件非常多,在部署管理上稍微有些复杂。 通过列式存储以及位图索引,极大的降低存储成本,并支持高效的数据过滤查询。 通过时间分区策略,对事件型、时序类型场景非常友好,能快速根据查询时间范围降低扫描的数据量。
Apache IoTDB 是专为物联网时序数据打造的数据库,提供数据采集、存储、分析的功能。IoTDB 提供端云一体化的解决方案,在云端,提供高性能的数据读写以及丰富的查询能力,针对物联网场景定制高效的目录组织结构,并与 Apache Hadoop、Spark、Flink 等大数据系统无缝打通;在边缘端,提供轻量化的 TsFile 管理能力,端上的数据写到本地 TsFile,并提供一定的基础查询能力,同时支持将 TsFile 数据同步到云端。 TsFile TsFile 是为物联网设备时序数据存储定制的文件格式,整体以树状目录结构组织,一个 TsFile 里可存储多个设备的数据,每个设备包含多个 measurment(指标)。如下图,TsFile 里包含两个设备数据,标识分别为 d1、d2;每个设备包含 s1、s2、s3 三个监测指标。 TsFile 整体是一个多级映射表,TsFileMetaData ==> TimeSeriesMetadata ==> ChunkMetadata ==> Chunk。 TsFileMetadata 描述整个 TsFile ,包含格式版本信息, MetadataIndexNode 的位置,总的 chunk 数等元数据信息。 MetadataIndexNode 包含多个 TimeSeriesMetadata ,每个 TimeSeriesMetadata 指向一个设备的元数据信息 ChunkMetadata 列表; ChunkMetadata 指向 ChunkHeader 位置,并对应最终的 Chunk Data。 IoTDB 内置查询引擎负责所有用户命令的解析、生成计划、交给对应的执行器、返回结果集。IoTDB 通过查询引擎提供了 JDBC 访问 API,简单易用。 IoTDB> CREATE TIMESERIES root.ln.wf01.wt01.status WITH DATATYPE=BOOLEAN, ENCODING=PLAIN IoTDB> CREATE TIMESERIES root.ln.wf01.wt01.temperature WITH DATATYPE=FLOAT, ENCODING=RLE IoTDB> INSERT INTO root.ln.wf01.wt01(timestamp,status) values(100,true); IoTDB> INSERT INTO root.ln.wf01.wt01(timestamp,status,temperature) values(200,false,20.71) IoTDB> SELECT status FROM root.ln.wf01.wt01 +-----------------------+------------------------+ | Time|root.ln.wf01.wt01.status| +-----------------------+------------------------+ |1970-01-01T08:00:00.100| true| |1970-01-01T08:00:00.200| false| +-----------------------+------------------------+ Total line number = 2 元数据管理 IoTDB 的元数据模型采用树状结构组织,一个实例包含多个 Storage Group (类似于 Namespace、Database 的概念),一个 Storage Group 里包含多个 Device ,每个 Device 包含多个 Measurement , Measurement 对应的时间序列数据最终存储在 TsFile Chunk 里。另外,为了方便数据过期,每个 Stroage Group 的数据会以时间范围的形式切分存储,默认以周为单位,使用不同的目录存储。 // Storage Group 分区存储结构 -- sequence ---- [存储组名1] ------ [时间分区ID1] -------- xxxx.tsfile -------- xxxx.resource ------ [时间分区ID2] ---- [存储组名2] -- unsequence IoTDB 存储引擎基于 LSM Tree 结构设计,写入的数据先记录 WAL,再写到内存 memtable,在后台逐步刷到磁盘 TsFile;磁盘上的 TsFile 通过一定的规则进行 Compaction,保证查询效率。 IoTDB 支持在边缘侧、云端部署,通常在边缘侧采集的数据有同步到远端进一步分析处理的需求;IoTDB 提供了同步工具,支持将端/设备上的 TsFile 数据往云端同步。 IoTDB 支持与现有的大数据处理系统,包括 Hive、Spark 等无缝连通,IoTDB 提供了 hive-tsfile 、 spark-tsfile 、 spark-iotdb 等连接器,让 Hive、Spark 能直接访问 tsfile 格式的数据,以及访问 IoTDB 的数据。 针对物联网模型做了定制化,提供 JDBC 访问方式,支持边云一体化部署。 存储使用 Hadoop File system,并提供多种 connector,与现有大数据生态无缝打通。 开放的 TsFile 存储格式,设备模型简单易理解。 IoTDB TsFile 的结构,目前仅有 java 版本,资源占用方面对边缘轻量级设备不友好,限制了其在端/设备侧的应用。 云端版本目前仅有单节点版本,无法满足海量设备数据接入云端的需求。 存储上支持使用 HDFS 或 本地盘,通过使用 HDFS 来存储可保证存储层高可用,但计算层没有进一步的高可用保障。
MongoDB 从3.6版本开始支持了 Change Stream 能力(4.0、4.2 版本在能力上做了很多增强),用于订阅 MongoDB 内部的修改操作,change stream 可用于 MongoDB 之间的增量数据迁移、同步,也可以将 MongoDB 的增量订阅应用到其他的关联系统;比如电商场景里,MongoDB 里存储新的订单信息,业务需要根据新增的订单信息去通知库存管理系统发货。 Change Stream 与 Tailing Oplog 对比 在 change stream 功能之前,如果要获取 MongoDB 增量的修改,可以通过不断 tailing oplog 的方式来 拉取增量的 oplog ,然后针对拉取到的 oplog 集合,来过滤满足条件的 oplog。这种方式也能满足绝大部分场景的需求,但存在如下的不足。 使用门槛较高,用户需要针对 oplog 集合,打开特殊选项的的 tailable cursor ("tailable": true, "awaitData" : true)。 用户需要自己管理增量续传,当拉取应用 crash 时,用户需要记录上一条拉取oplog的 ts、h 等字段,在下一次先定位到指定 oplog 再继续拉取。 结果过滤必须在拉取侧完成,但只需要订阅部分 oplog 时,比如针对某个 DB、某个 Collection、或某种类型的操作,必须要把左右的 oplog 拉取到再进行过滤。 对于 update 操作,oplog 只包含操作的部分内容,比如 {$set: {x: 1}} ,而应用经常需要获取到完整的文档内容。 不支持 Sharded Cluster 的订阅,用户必须针对每个 shard 进行 tailing oplog,并且这个过程中不能有 moveChunk 操作,否则结果可能乱序。 MongoDB Change Stream 解决了 Tailing oplog 存在的不足 简单易用,提供统一的 Change Stream API,一次 API 调用,即可从 MongoDB Server 侧获取增量修改。 统一的进度管理,通过 resume token 来标识拉取位置,只需在 API 调用时,带上上次结果的 resume token,即可从上次的位置接着订阅。 支持对结果在 Server 端进行 pipeline 过滤,减少网络传输,支持针对 DB、Collection、OperationType 等维度进行结果过滤。 支持 fullDocument: "updateLookup" 选项,对于 update,返回当时对应文档的完整内容。 支持 Sharded Cluster 的修改订阅,相同的 API 请求发到 mongos ,即可获取集群维度全局有序的修改。 Change Stream 实战 以 Mongo shell 为例,使用 Change Stream 非常简单,mongo shell 封装了针对整个实例、DB、Collection 级别的订阅操作。 db.getMongo().watch() 订阅整个实例的修改 db.watch() 订阅指定DB的修改 db.collection.watch() 订阅指定Collection的修改 新建连接1发起订阅操作 mytest:PRIMARY>db.coll.watch([], {maxAwaitTimeMS: 60000}) 最多阻塞等待 1分钟 新建连接2写入新数据 mytest:PRIMARY> db.coll.insert({x: 100}) WriteResult({ "nInserted" : 1 }) mytest:PRIMARY> db.coll.insert({x: 101}) WriteResult({ "nInserted" : 1 }) mytest:PRIMARY> db.coll.insert({x: 102}) WriteResult({ "nInserted" : 1 }) 连接1上收到 Change Stream 更新 mytest:PRIMARY> db.watch([], {maxAwaitTimeMS: 60000}) { "_id" : { "_data" : "825E0D5E35000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E353BE5C36D695042C90004" }, "operationType" : "insert", "clusterTime" : Timestamp(1577934389, 1), "fullDocument" : { "_id" : ObjectId("5e0d5e353be5c36d695042c9"), "x" : 100 }, "ns" : { "db" : "test", "coll" : "coll" }, "documentKey" : { "_id" : ObjectId("5e0d5e353be5c36d695042c9") } } { "_id" : { "_data" : "825E0D5E37000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E373BE5C36D695042CA0004" }, "operationType" : "insert", "clusterTime" : Timestamp(1577934391, 1), "fullDocument" : { "_id" : ObjectId("5e0d5e373be5c36d695042ca"), "x" : 101 }, "ns" : { "db" : "test", "coll" : "coll" }, "documentKey" : { "_id" : ObjectId("5e0d5e373be5c36d695042ca") } } { "_id" : { "_data" : "825E0D5E39000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E393BE5C36D695042CB0004" }, "operationType" : "insert", "clusterTime" : Timestamp(1577934393, 1), "fullDocument" : { "_id" : ObjectId("5e0d5e393be5c36d695042cb"), "x" : 102 }, "ns" : { "db" : "test", "coll" : "coll" }, "documentKey" : { "_id" : ObjectId("5e0d5e393be5c36d695042cb") } } 上述 ChangeStream 结果里,_id 字段的内容即为 resume token,标识着 oplog 的某个位置,如果想从某个位置继续订阅,在 watch 时,通过 resumeAfter 指定即可。比如每个应用订阅了上述3条修改,但只有第一条已经成功消费了,下次订阅时指定第一条的 resume token 即可再次订阅到接下来的2条。 mytest:PRIMARY> db.coll.watch([], {maxAwaitTimeMS: 60000, resumeAfter: { "_data" : "825E0D5E35000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E353BE5C36D695042C90004" }}) { "_id" : { "_data" : "825E0D5E37000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E373BE5C36D695042CA0004" }, "operationType" : "insert", "clusterTime" : Timestamp(1577934391, 1), "fullDocument" : { "_id" : ObjectId("5e0d5e373be5c36d695042ca"), "x" : 101 }, "ns" : { "db" : "test", "coll" : "coll" }, "documentKey" : { "_id" : ObjectId("5e0d5e373be5c36d695042ca") } } { "_id" : { "_data" : "825E0D5E39000000012B022C0100296E5A1004EA4E00977BCC482FB44DEED9A3C2999946645F696400645E0D5E393BE5C36D695042CB0004" }, "operationType" : "insert", "clusterTime" : Timestamp(1577934393, 1), "fullDocument" : { "_id" : ObjectId("5e0d5e393be5c36d695042cb"), "x" : 102 }, "ns" : { "db" : "test", "coll" : "coll" }, "documentKey" : { "_id" : ObjectId("5e0d5e393be5c36d695042cb") } } Change Stream 内部实现 watch() wrapper db.watch() 实际上是一个 API wrapper,实际上 Change Stream 在 MongoDB 内部实际上是一个 aggregation 命令,只是加了一个特殊的 $changestream 阶段,在发起 change stream 订阅操作后,可通过 db.currentOp() 看到对应的 aggregation/getMore 操作的详细参数。 "op" : "getmore", "ns" : "test.coll", "command" : { "getMore" : NumberLong("233479991942333714"), "collection" : "coll", "maxTimeMS" : 50000, "lsid" : { "id" : UUID("e4fffa71-e168-4527-be61-f0918849d107") "planSummary" : "COLLSCAN", "cursor" : { "cursorId" : NumberLong("233479991942333714"), "createdDate" : ISODate("2019-12-31T06:35:52.479Z"), "lastAccessDate" : ISODate("2019-12-31T06:36:09.988Z"), "nDocsReturned" : NumberLong(1), "nBatchesReturned" : NumberLong(1), "noCursorTimeout" : false, "tailable" : true, "awaitData" : true, "originatingCommand" : { "aggregate" : "coll", "pipeline" : [ "$changeStream" : { "fullDocument" : "default" "cursor" : { "lsid" : { "id" : UUID("e4fffa71-e168-4527-be61-f0918849d107") "$clusterTime" : { "clusterTime" : Timestamp(1577774144, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) "$db" : "test" "operationUsingCursorId" : NumberLong(7019500) "numYields" : 2, "locks" : { resume token resume token 用来描述一个订阅点,本质上是 oplog 信息的一个封装,包含 clusterTime、uuid、documentKey等信息,当订阅 API 带上 resume token 时,MongoDB Server 会将 token 转换为对应的信息,并定位到 oplog 起点继续订阅操作。 struct ResumeTokenData { Timestamp clusterTime; int version = 0; size_t applyOpsIndex = 0; Value documentKey; boost::optional<UUID> uuid; ResumeTokenData 结构里包含 version 信息,在 4.0.7 以前的版本,version 均为0; 4.0.7 引入了一种新的 resume token 格式,version 为 1; 另外在 3.6 版本里,Resume Token 的编码与 4.0 也有所不同;所以在版本升级后,有可能出现不同版本 token 无法识别的问题,所以尽量要让 MongoDB Server 所有组件(Replica Set 各个成员,ConfigServer、Mongos)都保持相同的内核版本。 更详细的信息,参考 https://docs.mongodb.com/manual/reference/method/Mongo.watch/#resumability updateLookup Change Stream 支持针对 update 操作,获取当前的文档完整内容,而不是仅更新操作本身,比如 mytest:PRIMARY> db.coll.find({_id: 101}) { "_id" : 101, "name" : "jack", "age" : 18 } mytest:PRIMARY> db.coll.update({_id: 101}, {$set: {age: 20}}) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) 上面的 update 操作,默认情况下,change stream 会收到 {_id: 101}, {$set: {age: 20} 的内容,而并不会包含这个文档其他未更新字段的信息;而加上 fullDocument: "updateLookup" 选项后,Change Stream 会根据文档 _id 去查找文档当前的内容并返回。 需要注意的是,updateLookup 选项只能保证最终一致性,比如针对上述文档,如果连续更新100次,update 的 change stream 并不会按顺序收到中间每一次的更新,因为每次都是去查找文档当前的内容,而当前的内容可能已经被后续的修改覆盖。 Sharded cluster Change Stream 支持针对 sharded cluster 进行订阅,会保证全局有序的返回结果;为了达到全局有序这个目标,mongos 需要从每个 shard 都返回订阅结果按时间戳进行排序合并返回。 在极端情况下,如果某些 shard 写入量很少或者没有写入,change stream 的返回延时会受到影响,因为需要等到所有 shard 都返回订阅结果;默认情况下,mongod server 每10s会产生一条 Noop 的特殊oplog,这个机制会间接驱动 sharded cluster 在写入量不高的情况下也能持续运转下去。 由于需要全局排序,在 sharded cluster 写入量很高时,Change Stream 的性能很可能跟不上;如果对性能要求非常高,可以考虑关闭 Balancer,在每个 shard 上各自建立 Change Stream。 Change Stream Manual Change Streams Production Recommendations Tailable Cursors/
MongoDB 使用 BI Connector 来支持 BI 组件直接使用 SQL 或 ODBC 数据源方式直接访问 MongoDB,在早期 MongoDB 直接使用 Postgresql FDW 来实现 SQL 到 MQL 的转换,后来实现更加轻量级的 mongosqld 来支持 BI 工具的连接。 安装 BI Connector 参考 Install BI Connector wget https://info-mongodb-com.s3.amazonaws.com/mongodb-bi/v2/mongodb-bi-linux-x86_64-rhel70-v2.12.0.tgz $tar xvf mongodb-bi-linux-x86_64-rhel70-v2.12.0.tgz mongodb-bi-linux-x86_64-rhel70-v2.12.0/LICENSE mongodb-bi-linux-x86_64-rhel70-v2.12.0/README mongodb-bi-linux-x86_64-rhel70-v2.12.0/THIRD-PARTY-NOTICES mongodb-bi-linux-x86_64-rhel70-v2.12.0/example-mongosqld-config.yml mongodb-bi-linux-x86_64-rhel70-v2.12.0/bin/mongosqld mongodb-bi-linux-x86_64-rhel70-v2.12.0/bin/mongodrdl mongodb-bi-linux-x86_64-rhel70-v2.12.0/bin/mongotranslate mongosqld 接受 SQL 查询,并将请求发到 MongoDB Server,是 BI Connector 的核心 mongodrdl 工具生成数据库 schema 信息,用于服务 BI SQL 查询 mongotranslate 工具将 SQL 查询转换为 MongoDB Aggregation Pipeline 启动 mongosqld 参考 Lauch BI Connector mongodb-bi-linux-x86_64-rhel70-v2.12.0/bin/mongosqld --addr 127.0.0.1:3307 --mongo-uri 127.0.0.1:9555 --addr 指定 mongosqld 监听的地址 --mongo-uri 指定连接的 MongoDB Server 地址 默认情况下,mongosqld 自动会分析目标 MongoDB Server 里数据的 Schema,并缓存在内存,我们也可以直接在启动时指定 schema 影射关系。schema 也可以直接 mongodrdl 工具来生成,指定集合,可以将集合里的字段 shema 信息导出。 $./bin/mongodrdl --uri=mongodb://127.0.0.1:9555/test -c coll01 schema: - db: test tables: - table: coll01 collection: coll01 pipeline: [] columns: - Name: _id MongoType: float64 SqlName: _id SqlType: float - Name: qty MongoType: float64 SqlName: qty SqlType: float - Name: type MongoType: string SqlName: type SqlType: varchar 使用 MySQL 客户端连接 mongosqld mongosqld 可直接支持 MySQL 客户端访问,还可以通过 Excel、Access、Tableau等BI工具连接 mysql --protocol=tcp --port=3307 mysql> use test Database changed mysql> show tables; +----------------+ | Tables_in_test | +----------------+ | coll | | coll01 | | coll02 | | inventory | | myCollection | | yourCollection | +----------------+ 6 rows in set (0.00 sec) mysql> select * from coll01; +------+------+--------+ | _id | qty | type | +------+------+--------+ | 1 | 5 | apple | | 2 | 10 | orange | | 3 | 15 | banana | +------+------+--------+ 3 rows in set (0.00 sec) // 对照 MongoDB 数据库里的原始数据 mongo --port mymongo:PRIMARY> use test switched to db test mymongo:PRIMARY> show tables; coll01 coll02 inventory myCollection yourCollection mymongo:PRIMARY> db.coll01.find() { "_id" : 1, "type" : "apple", "qty" : 5 } { "_id" : 2, "type" : "orange", "qty" : 10 } { "_id" : 3, "type" : "banana", "qty" : 15 } SQL 转 Aggregation 比如要将针对 test.coll01 的 SQL 查询转换为 MongoDB Aggregation Pipeline,需要先通过 mongodrdl 分析 schema,然后使用 mongotranslate 工具来转换 // 导出分析的 shema 文件 $./bin/mongodrdl --uri=mongodb://127.0.0.1:9555/test -c coll01 > coll01.schema // SQL 转换为 Aggregation $./bin/mongotranslate --query "select * from test.coll01" --schema coll01.schema {"$project": {"test_DOT_coll01_DOT__id": "$_id","test_DOT_coll01_DOT_qty": "$qty","test_DOT_coll01_DOT_type": "$type","_id": NumberInt("0")}}, 高性能,官方号称 100x faster,因为可以全内存运行,性能提升肯定是很明显的 简单易用,支持 Java、Python、Scala、SQL 等多种语言,使得构建分析应用非常简单 统一构建 ,支持多种数据源,通过 Spark RDD 屏蔽底层数据差异,同一个分析应用可运行于不同的数据源; 应用场景广泛,能同时支持批处理以及流式处理 MongoDB Spark Connector 为官方推出,用于适配 Spark 操作 MongoDB 数据;本文以 Python 为例,介绍 MongoDB Spark Connector 的使用,帮助你基于 MongoDB 构建第一个分析应用。 准备 MongoDB 环境 安装 MongoDB 参考 Install MongoDB Community Edition on Linux mkdir mongodata mongod --dbpath mongodata --port 9555 准备 Spark python 环境 参考 PySpark - Quick Guide 下载 Spark cd /home/mongo-spark wget http://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-2.4.4/spark-2.4.4-bin-hadoop2.7.tgz tar zxvf spark-2.4.4-bin-hadoop2.7.tgz 设置 Spark 环境变量 export SPARK_HOME=/home/mongo-spark/spark-2.4.4-bin-hadoop2.7 export PATH=$PATH:/home/mongo-spark/spark-2.4.4-bin-hadoop2.7/bin export PYTHONPATH=$SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-0.10.4-src.zip:$PYTHONPATH export PATH=$SPARK_HOME/python:$PATH 运行 Spark RDD 示例 # count.py from pyspark import SparkContext sc = SparkContext("local", "count app") words = sc.parallelize ( ["scala", "java", "hadoop", "spark", "akka", "spark vs hadoop", "pyspark", "pyspark and spark"] counts = words.count() $SPARK_HOME/bin/spark-submit count.py Number of elements in RDD → 8 如果上述程序运行成功,说明 Spark python 环境准备成功,还可以测试 Spark 的其他 RDD 操作,比如 collector、filter、map、reduce、join 等,更多示例参考 PySpark - Quick Guide Spark 操作 MongoDB 数据 参考 Spark Connector Python Guide 准备测试数据 test.coll01 插入3条测试数据,test.coll02 未空 mongo --port 9555 > db.coll01.find() { "_id" : 1, "type" : "apple", "qty" : 5 } { "_id" : 2, "type" : "orange", "qty" : 10 } { "_id" : 3, "type" : "banana", "qty" : 15 } > db.coll02.find() 准备操作脚本,将输入集合的数据按条件进行过滤,写到输出集合 # mongo-spark-test.py from pyspark.sql import SparkSession # Create Spark Session spark = SparkSession \ .builder \ .appName("myApp") \ .config("spark.mongodb.input.uri", "mongodb://127.0.0.1:9555/test.coll01") \ .config("spark.mongodb.output.uri", "mongodb://127.0.0.1:9555/test.coll") \ .getOrCreate() # Read from MongoDB df = spark.read.format("mongo").load() df.show() # Filter and Write df.filter(df['qty'] >= 10).write.format("mongo").mode("append").save() # Use SQL # df.createOrReplaceTempView("temp") # some_fruit = spark.sql("SELECT type, qty FROM temp WHERE type LIKE '%e%'") # some_fruit.show() $SPARK_HOME/bin/spark-submit --packages org.mongodb.spark:mongo-spark-connector_2.11:2.4.1 mongo-spark-test.py mongo --port 9555 > db.coll02.find() { "_id" : 2, "qty" : 10, "type" : "orange" } { "_id" : 3, "qty" : 15, "type" : "banana" }
Redis 混合存储实例是阿里云自主研发的兼容Redis协议和特性的云数据库产品,混合存储实例突破 Redis 数据必须全部存储到内存的限制,使用磁盘存储全量数据,并将热数据缓存到内存,实现访问性能与存储成本的完美平衡。 架构及特性 混合存储兼容绝大多数 Redis 命令,与原生 Redis 相比,如下命令不支持或受限制;不支持的主要原因是考虑到性能,如业务中有使用到,请提交工单。 Keys(键) List(链表) Scripting(Lua脚本) RENAME LINSERT SCRIPT 不支持LOAD和DEBUG子命令 RENAMENX 选型指南 - 规格 选择混合存储实例时,需要选择合适的【内存配置 + 磁盘配置】;磁盘决定能存储的数据总量,内存决定能存储的热数据总量,实例生产时会根据存储的规格配置选择合适的CPU资源配置,目前暂不支持自定义CPU核数。 比如【64GB内存 + 256GB磁盘】实例,意思是实例最多能存储 256GB 的数据(以KV存储引擎的物理文件总大小为准),其中 64GB 数据可以缓存在内存。 内存选型建议:Redis 混合存储为保证最大程度的兼容 redis 原生访问协议,要求所有的key必须常驻内存,value 可以根据冷热读来自动决定存储在内存还是磁盘,所以内存空间必须要足以存储所有的key、以及对应的元信息。 key数量 推荐内存规格(越大性能越好) 小于 2000万 64GB、32GB、16GB 2000万 ~ 5000万 64GB、32GB 5000万 ~ 1亿 128GB、64GB、32GB 大于 1亿 128GB、64GB 磁盘选型建议:因 Redis 数据存储到 KV 存储引擎,每个key都会额外元数据信息,存储空间占用会有一定的放大,建议在磁盘空间选择上,留有适当余量,按实际存储需求的 1.2 - 1.5倍预估。 案例1:用户A 使用 Redis Cluster 存储了 100GB 的数据,总的访问QPS不到2W,其中80%的数据都很少访问到。用户A 可以使用 【32GB内存 + 128GB磁盘】 混合存储实例,节省了近 70GB 的内存存储,存储成本下降50%+。 案例2:用户B 在IDC自建 Pika/SSDB 实例,解决Redis存储成本高的问题,存储了约 400GB 的数据,其中活跃访问的在10%左右,集群运维负担很重,想迁移至云数据库;用户B 可以使用 【64GB内存 + 512GB磁盘】混合存储实例,来保证免运维的同时,服务质量不下降。 Redis 混合存储的性能与内存磁盘配比,以及业务的访问高度相关;根据规格配置及业务访问模式的不同,简单 set/get 的性能可在几千到数万之间波动。最好情况所有的访问都内存命中,性能与 Redis 内存版基本一致;最差情况所有的访问都需要从磁盘读取。 测试场景:2000w key,value大小为1KB,25%的热key能存储在内存,get 请求测试数据如下 内存版(100%数据在内存) 混合存储版(25%数据在内存) 12.3(万) 高斯分布80%的概率访问20%的key 高斯分布99%的概率访问1%的key 视频直播类 视频直播类业务往往存在大量热点数据,大部分的请求都来自于热门的直播间。使用 Redis 混合存储型实例,内存中保留热门直播间的数据,不活跃的直播间数据被自动存储到磁盘上,可以达到对有限内存的最佳利用效果。 电商类应用有大量的商品数据,新上架的商品会被频繁访问,而较老的商品访问热度不高;使用 Redis 混合存储型实例,可以轻松突破内存容量限制,将大量的商品数据存储到磁盘,在正常业务请求中,活跃的商品数据会逐步缓存在内存中,以最低的成本满足业务需求。 在线教育类 在线教育类的场景,有大量的课程、题库、师生交流信息等数据,通常只有热门课程、最新题库题库会被频繁访问; 使用 Redis 混合存储型,将大量的课程信息存储到磁盘,活跃的课程、题库信息会换入到内存并常驻内存,保证高频访问数据的性能,实现性能与存储成本的平衡。 其他数据访问有明显冷热特性,对性能要求不高的场景均可使用Redis混合存储来降低存储成本。 磁盘还有剩余空间,但内存先满了,导致写入报错 OOM error 内存规格太小,导致内存空间不足以容纳所有key及其元数据信息,建议在控制台升级实例规格即可,增大实例内存。 key对应的value比较小,混合存储对于比较小的value(比如小于20byte),不会触发换出换出到磁盘,因为小的value换出到磁盘,在内存里还是会存储一些meta信息,最终导致换出到磁盘并不能腾出内存空间;这个问题混合存储内核在持续优化,尽量适应更多的应用场景。
SERVER-17397: Dropping a Database or Collection in a Sharded Cluster may not fully succeed 是 MongoDB 里老大难的问题,库或集合删除操作如果没有完全执行成功,再新建相同名字的集合,可能导致读到老版本数据的问题。 集合分片原理 MongoDB sharding 分片原理参考 MongoDB Sharded cluster架构原理 总的来说,当用户对集合执行开启分片之后,集合分片的元数据会保存在 config server 的 config 集合里 config.collections 记录集合分片的元数据,根据哪个 shardKey 分片,集合是否已经被删除等元数据 config.chunks,记录各个 chunk(shardKey的某一段范围)对应的 shard 信息,用于路由请求 各个 shard 里存储集合实际的数据 删除分片集合流程 删除所有 shard 里的对应的数据 删除 config.chunks 这个集合相关的chunk信息 修改 config.collections,标记集合已经删除 注:3.2+都是按上述流程操作,删除 Database 过程类似,还需要再额外操作 config.databases 集合,但本质上存在的问题类似 上述动作需要操作 config server 以及 所有的 shard,如果中间有步骤失败(一些很老的版本,并不是按照上述步骤执行,而且执行过程中可能没有严格检查返回的错误码,即使返回成功实际上内部可能执行失败),最终导致集合的部分数据仍然残留,没有完全清理干净。 如果这个集合名字重新被使用,再次调用 shardCollection 产生新的分片元数据,可能导致 在 shard 上的一些残留数据可能被读取到,而这些数据实际上应该被删除了 mongos 没有成功更新路由信息,最终可能出现多个 mongos 看到的数据视图也不一致,有的 mongos 能读到数据,有的读不到(通过 `flushRouterConfig 命令可以强制刷新路由信息可解决) MongoDB sharding 删除集合/数据库涉及到多个节点进行操作,这些动作无法做到原子性,可能导致一个集合最终处于某种中间状态;复用该集合可能导致一写数据一致性问题。 使用 MongoDB 3.2+ 以上版本,大部分case,只要没有异常,删除集合动作都能正常完成的,复用集合名字问题一般问题也不大,但无法完全避免问题。 建议 Sharding 环境下,namespace 名字一旦被删除,不要再次复用 在需要复用 Namespace 的情况下,如果要确保不会有数据问题,每次可以按 drop collection workaround 确保相关数据被正确清理,并且路由信息被更新。
MongoDB oplog (类似于 MySQL binlog) 记录数据库的所有修改操作,除了用于主备同步;oplog 还能玩出很多花样,比如 全量备份 + 增量备份所有的 oplog,就能实现 MongoDB 恢复到任意时间点的功能 通过 oplog,除了实现到备节点的同步,也可以额外再往单独的集群同步数据(甚至是异构的数据库),实现容灾、多活等场景,比如阿里云开源的 MongoShake 就能实现基于 oplog 的增量同步。 MongoDB 3.6+ 版本对 oplog 进行了抽象,提供了 Change Stream 的接口,实际上就是能不断订阅数据库的修改,基于这些修改可以触发一些自定义的事件。 ...... 总的来说,MongoDB 可以通过 oplog 来跟生态对接,来实现数据的同步、迁移、恢复等能力。而在构建这些能力的时候,有一个通用的需求,就是工具或者应用需要有不断拉取 oplog 的能力;这个过程通常是 根据上次拉取的位点构建一个 cursor 不断迭代 cursor 获取新的 oplog 那么问题来了,由于 MongoDB oplog 本身没有索引的,每次定位 oplog 的起点都需要进行全表扫描么? oplog 的实现细节 { "ts" : Timestamp(1563950955, 2), "t" : NumberLong(1), "h" : NumberLong("-5936505825938726695"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.903Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28740"), "x" : 0 } } { "ts" : Timestamp(1563950955, 3), "t" : NumberLong(1), "h" : NumberLong("-1206874032147642463"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.903Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28741"), "x" : 1 } } { "ts" : Timestamp(1563950955, 4), "t" : NumberLong(1), "h" : NumberLong("1059466947856398068"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.913Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28742"), "x" : 2 } } 上面是 MongoDB oplog 的示例,oplog MongoDB 也是一个集合,但与普通集合不一样 oplog 是一个 capped collection,但超过配置大小后,就会删除最老插入的数据 oplog 集合没有 id 字段,ts 可以作为 oplog 的唯一标识; oplog 集合的数据本身是按 ts 顺序组织的 oplog 没有任何索引字段,通常要找到某条 oplog 要走全表扫描 我们在拉取 oplog 时,第一次从头开始拉取,然后每次拉取使用完,会记录最后一条 oplog 的ts字段;如果应用发生重启,这时需要根据上次拉取的 ts 字段,先找到拉取的起点,然后继续遍历。 oplogHack 优化 注:以下实现针对 WiredTiger 存储引擎,需要 MongoDB 3.0+ 版本才能支持 如果 MongoDB 底层使用的是 WiredTiger 存储引擎,在存储 oplog 时,实际上做过优化。MongoDB 会将 ts 字段作为 key,oplog 的内容作为 value,将key-value 存储到 WiredTiger 引擎里,WiredTiger 默认配置使用 btree 存储,所以 oplog 的数据在 WT 里实际上也是按 ts 字段顺序存储的,既然是顺序存储,那就有二分查找优化的空间。 MongoDB find 命令提供了一个选项,专门用于优化 oplog 定位。 大致意思是,如果你find的集合是oplog,查找条件是针对 ts 字段的 gte、gt、eq ,那么 MongoDB 字段会进行优化,通过二分查找快速定位到起点; 备节点同步拉取oplog时,实际上就带了这个选项,这样备节点每次重启,都能根据上次同步的位点,快速找到同步起点,然后持续保持同步。 oplogHack 实现 由于咨询问题的同学对内部实现感兴趣,这里简单的把重点列出来,要深刻理解,还是得深入撸细节。 // src/monogo/db/query/get_executor.cpp StatusWith<unique_ptr<PlanExecutor>> getExecutorFind(OperationContext* txn, Collection* collection, const NamespaceString& nss, unique_ptr<CanonicalQuery> canonicalQuery, PlanExecutor::YieldPolicy yieldPolicy) { // 构建 find 执行计划时,如果发现有 oplogReplay 选项,则走优化路径 if (NULL != collection && canonicalQuery->getQueryRequest().isOplogReplay()) { return getOplogStartHack(txn, collection, std::move(canonicalQuery)); return getExecutor( txn, collection, std::move(canonicalQuery), PlanExecutor::YIELD_AUTO, options); StatusWith<unique_ptr<PlanExecutor>> getOplogStartHack(OperationContext* txn, Collection* collection, unique_ptr<CanonicalQuery> cq) { // See if the RecordStore supports the oplogStartHack // 如果底层引擎支持(WT支持,mmapv1不支持),根据查询的ts,找到 startLoc const BSONElement tsElem = extractOplogTsOptime(tsExpr); if (tsElem.type() == bsonTimestamp) { StatusWith<RecordId> goal = oploghack::keyForOptime(tsElem.timestamp()); if (goal.isOK()) { // 最终调用 src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp::oplogStartHack startLoc = collection->getRecordStore()->oplogStartHack(txn, goal.getValue()); // Build our collection scan... // 构建全表扫描参数时,带上 startLoc,真正执行是会快速定位到这个点 CollectionScanParams params; params.collection = collection; params.start = *startLoc; params.direction = CollectionScanParams::FORWARD; params.tailable = cq->getQueryRequest().isTailable(); db.collection.remove({}, {multi: true}),逐个文档从 btree 里删除,最后所有文档被删除,但文件物理空间不会被回收 db.collection.drop() 删除集合的物理文件,空间立即被回收 总的来说,remove 会产生逻辑的空闲空间,这些空间能立即用于写入新数据,但文件占用的总物理空间不会立即回收;通常只要持续在写入数据,有物理空间碎片问题并不大,不需要去 compact 集合,有的场景,remove 了大量的数据后,后续的写入可能并不多,这时如果想回收空间,就需要显式的调用 compact。 compact 命令对读写的影响 compact 一个集合,会加集合所在DB的互斥写锁,会导致该DB上所有的读写请求都阻塞;因为 compact 执行的时间可能很长,跟集合的数据量相关,所以强烈建议在业务低峰期执行,避免影响业务。 compact 具体做了什么? Compact 动作最终由存储引擎 WiredTiger 完成,WiredTiger 在执行 compact 时,会不断将集合文件后面的数据往前面空闲的空间写,然后逐步 truancate 文件回收物理空间。每一轮 compact 前,WT 都会先检查是否符合 comapact 条件。 前面80%的空间里,是否有20%的空闲空间,用于写入文件后面20%的数据,或者 前面90%的空间里,是否有10%的空闲空间,用于写入文件后面10%的数据 如果上面都不满足,说明执行compact肯定无法回收10%的物理空间,此时 compact 就回退出。所以有时候遇到对一个大集合进行 compact,compact立马就返回ok,集合的物理空间也没有变化,就是因为 WiredTiger 认为这个集合没有 compact 的必要。 如何预估compact能回收多少空间? The amount of empty space available for reuse by WiredTiger is reflected in the output of db.collection.stats() under the heading wiredTiger.block-manager.file bytes available for reuse. mymongo:PRIMARY> db.coll.stats().wiredTiger["block-manager"]["file bytes available for reuse"] 5033984 执行 compact 执行前请确保你已经读懂了上面的内容,知道compact命令的原理、影响 // compact somedb.somecollection use somedb db.runCommnd({compact: "somecollection"}) // compact oplog,在副本集primary上执行需要加 force 选项 use local db.runCommnd({compact: "somecollection", force: true}) MongoDB compact command
mongos x 2、shard x 3 测试1:集合不开启分片,批量 insert 导入数据,每个 batch 100 个文档 测试2:集合开启分片,随机生成 shardKey,chunk 已提前 split 好,能确保写入均分到3个shard 测试1:单个 shard cpu 跑满,insert qps 在 6w 左右 测试2:3个 shard cpu 跑满,insert qps 在 7w 左右(平均每个分片2.4w左右) 注:两个测试里,mongos 都不是瓶颈,能力足够 从测试结果看,每个shard都承担 1/3 的负载,的确达到横向扩张的目的,但为啥分片之后,单个shard的能力就下降了呢?如果是这样,sharding的扩展能力如何体现? 这里核心的问题在于 batch insert 在 mongos 和 mongod 上处理行为的差别 导入数据时,一次 insert 一条数据,和一次 insert 100 条数据,性能差距是很大的;首先减少了client、server 端之间的网络交互;同时 server 可以将 batch insert 放到一个事务里,降低开销; mongos 在收到 batch insert 时,因为一个 batch 里的数据需要根据 shardKey 分布到不同的shard,所以一个 batch 实际上需要被拆开的;这里 mongos 也做了优化,会尽量将连续的分布在一个shard上的文档做 batch 发到后端 shard。 在集合不开启分片的情况,mongos 收到的 batch 肯定是转发给 primary shard,所以转发过去还是一整个 batch 操作; 而在集合开启分片的情况下,因为用户测试时,shardKey 是随机生成的,基本上整个 batch 被打散成单条操作,逐个往后端 shard 上发送,请求到后端 shard 基本已经完全没有合并了。 所以在上述测试中,不分片的单个 shard 6w qps、与分片后每个 shard 2.4w qps,实际上就是请求是否 batch 执行的差别。 对应用的影响 从上面的分析可以看出,batch 往分片的集合写入时,因为无法预知数据应该分散到哪个分片,实际上往后端 shard 写入时,会失去 batch 的效果,但这个批量导入一般发生在数据导入阶段,影响比较小。
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 MongoDB World 2019 上发布新版本 MongoDB 4.2 Beta,包含多项数据库新特性,本文尝试从技术角度解读。 Full Text Search MongoDB 4.2 之前,全文搜索(Full Text Search)的能力是靠 Text Index 来支持的,在 MongoDB-4.2 里,MongoDB 直接与 Lucene 等引擎整合,在 Atlas 服务里提供全文建索的能力。 MongoDB FTS 原理 用户可以在 Atlas 上,对集合开启全文索引,后台会开起 Lucene 索引引擎(索引引擎、查询引擎均可配置),对存量数据建立索引。 对于开启全文建索的集合,新写入到 MongoDB 的数据, 后台的服务会通过 Change Stream 的方式订阅,并更新到 Lucene 索引引擎里。 索引的查询直接以 MongoDB Query 的方式提供,Mongod 收到请求会把请求转发到 Lucene 引擎,收到建索结果后回复给客户端。 Full Text Search 示例 下面是一个 Full Text Search 使用的简单示例,整个使用体验非常简单,除了需要在 Atlas 控制台上建索引,其他跟正常使用 MongoDB 毫无差别,随着这块能力的完善,能覆盖很多 Elastic Search 的场景。 Step1: 准备数据 MongoDB Enterprise > db.fruit.find() { "_id" : 1, "type" : "apple", "description" : "Apples come in several varieties, including Fuji, Granny Smith, and Honeycrisp." } { "_id" : 2, "type" : "banana", "description" : "Bananas are usually sold in bunches of five or six." } Step2: Atlas 上创建 FTS 索引 Step3: 使用 MongoDB 客户端做搜索,支持 Wildcard、Prefix 等多种搜索能力 // 简单查询 db.fruit.aggregate([ $searchBeta: { "term": { "query": "Smith", "path": "description" { "_id" : 1, "type" : "apple", "description" : "Apples come in several varieties, including Fuji, Granny Smith, and Honeycrisp." } // Wildcard 查询 db.fruit.aggregate([ $searchBeta: { "term": { "query": "s*l*", "path": "description", "wildcard": true { "_id" : 1, "type" : "apple", "description" : "Apples come in several varieties, including Fuji, Granny Smith, and Honeycrisp." } { "_id" : 2, "type" : "banana", "description" : "Bananas are usually sold in bunches of five or six." } Distributed Transaction MongoDB 4.0 支持副本集事务,极大的丰富了应用场景;4.0 的事务存在最大修改 16MB、事务执行时间不能过长的限制,在 4.2 支持分布式事务的这些问题都解决了。分布式事务的支持也意味用户修改分片key的内容成为可能,因为修改分片key的内容,可能会导致key要迁移到其他shard,而在4.2之前,无法保证这个迁移动作(目标上新写、源上删掉)的原子性,而借助分布式事务,这个问题也就迎刃而解。 4.2 支持的分布式事务是硬核技术,目前具备这个能力的开源数据库本身也不多,MongoDB 采用二阶段提交的方式(细节以后再分析),实现在多个 Shard 间发生的修改,要么同时发生,要么都不发生,保证事务的 ACID 特性。 在使用上,4.2 的分布式事务跟 4.0 副本集事务使用方式完全一样,用户无需关心后端数据如何分布。 High Availablity MongoDB 在保证数据库服务可用性方面持续努力,在 4.0 提供了 Retryable Write 功能,在新的 4.2 版本,MongoDB 增加了 Retryable Read 功能,对于一些临时的网络问题,用户无需自己实现重试逻辑,MongoDB 会自动重试处理,保证用户业务的连续性。 Improved Query Language MongoDB 4.2 在查询语言的表达能力上进一步增强,update、aggregation、index 等方面都有巨大的提升,具体细节等 4.2 正式版文档发出可以详细了解。 Update 能力增强 4.2 之前,Update 操作基本上都是用确定的值更新某个字段,在新版本里,Update 能根据文档现有的字段内容来生成新的更新内容,如下的实例,根据文档 pay、tax 字段,加起来生成一个 total 字段;这个在 4.2 之前,用户需要先读取文档内容,获取 pay、tax 字段得到结果,然后调用 Update 设置新的字段。类似的特性还有很多,基本上 Aggregation 里能表达的更新操作,4.2 的 Update 命令都能支持。 db.orders.find() { "_id" : 1, "pay" : 100, "tax" : 17 } // 这个操作发布会PPT上有写,但实际连 4.2 测试并不能工作,等正式版出来再看看 db.orders.update( {_id: 1}, { "$set": { "total": { "$sum": ["$pay", "$tax"] } 分析能力增强 Aggregation 方面,MongoDB 也做了大量的改进,来更好的支持业务分析场景;比如增加 $merge 操作符,能不断的将增量分析结果与原来的结果进行汇总(老的版本只支持 $out,把当次分析结果写到某个集合)。 Index 能力增强(Wildcard Index) 使用 MongoDB 时,经常会遇到一些场景,某个字段包含很多个属性,很多属性都可能需要用于查询,现在的解决方案时,针对每个属性,必须提前知道它的访问行为,建立必要的索引;MongoDB 4.2 引入 Wildcard Index,可以针对一系列的字段自动建索引,满足丰富的查询需求。 如下面的例子所示,书籍的 attribute 字段里包含很多熟悉,包括颜色、大小等信息,如果经常需要根据属性查找,可以针对 attribute 字段建立 Wildcard index。 db.books.find() { "_id" : ObjectId("5d0c5d931eefdf585ae9ca95"), "type" : "book", "title" : "The Red Book", "attributes" : { "color" : "red", "size" : "large", "inside" : { "bookmark" : 1, "postitnote" : 2 }, "outside" : { "dustcover" : "worn" } } } { "_id" : ObjectId("5d0c5d9e1eefdf585ae9ca96"), "type" : "book", "title" : "The Blue Book", "attributes" : { "color" : "blue", "size" : "small", "inside" : { "map" : 1 }, "outside" : { "librarystamp" : "Local Library" } } } { "_id" : ObjectId("5d0c5dac1eefdf585ae9ca97"), "type" : "book", "title" : "The Green Book", "attributes" : { "color" : "green", "size" : "small", "inside" : { "map" : 1, "bookmark" : 2 }, "outside" : { "librarystamp" : "Faraway Library", "dustcover" : "good" } } } // 没有索引的时候,根据颜色属性查找,走全表扫描 db.books.find({"attributes.color": "green"}).explain() "queryPlanner" : { "queryHash" : "528C4C03", "planCacheKey" : "528C4C03", "winningPlan" : { "stage" : "COLLSCAN", // 针对 attributes 字段所有的子字段建立 Wildcard 索引,针对 color、size 等的查询就都可以走索引 db.books.createIndex({ "attributes.$**": 1 }); db.books.find({"attributes.color": "green"}).explain() "queryPlanner" : { "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", db.books.find({"attributes.size": "small"}).explain() "queryPlanner" : { "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", Field Level Encrytion MongoDB 除了支持 SSL、TDE 等安全机制,在 4.2 引入「字段级加密」的支持,实现对用户JSON文档的Value 进行自动加密。整个过程在 Driver 层完成,传输、存储到服务端的文档Value都是密文,MongoDB 4.2 Drvier 支持丰富的加密策略,可以针对集合、字段维度开启加密,加密过程对开发者完全透明。 MongoDB and Kubernetes Kubernetes 是工业级的容器编排管理平台,可以使用 Kubernetes 管理 MongoDB 集群的整个生命周期,但随着业务部署环境越来越复杂多样化,有的可能是私有云部署、有的是公有云的部署,使得集群的管理难度也越来越高。 在新版本 MongoDB Atlas(公有云), MongoDB Cloud Manager(私有云企业版管理) 都集成了 Kubernetes operators 的支持,使得用户可以使用 Kubernetes 统一管理 MongoDB 资源。 MongoDB Chart MongoDB Chart 在去年的 MongoDB World 已经介绍过了,今年有做了多方面的增强,算得上是一个功能比较完备的 BI 分析工具了。有了 Charts,MongoDB 也无需支持 SQL 来去对接 BI 工具了。 Charts 在使用上还是有一定学习成本的,不是特别直观,需要配合教程,了解下运作原理,才能得到想要的图,比如这个例子里,针对电影集合,Released 的年份做了聚合分析,得到分布图。 MongoDB Realm MongoDB 在4月份的时候收购了 Realm,一个为移动端开发而设计的新型数据库。MongoDB 去年发布了 MongoDB Mobile 来应对移动端的数据存储需求,在收购 Realm 后,二者会进行深度整合,Real Core 里会借助MongoDB提供的能力,增加非结构化数据存储到能力,比如 JSON、Dict、Set,让 Realm 变得更强大,同时发挥 Realm 在移动端生态以及 MongoDB 数据库存储的优势。 Atlas Data Lake (Beta) 在新版本 Atlas 服务里,提供了 Atlas Data Lake,能直接通过 MongoDB API 访问存储在 AWS S3 (未来支持 Azure、Google 的存储服务)里的数据。 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
MongoDB 提供 currentOp 命令,列出当前正在执行的查询操作,并提供 killOp 命令,用于中止一些耗时比较长,影响线上业务的操作,作为一种应急手段。 下图是一个 currentOp 命令的输出项之一,用户在获取到 opid 后,调用 killOp() 并没有把这个请求干掉。 为什么 opid 是负数? opid 在 mongod 里是一个 uint32 类型的整数,当你从 mongo shell 里看到 opid 为负数时,说明你的 mongod 已经成功执行超过21(INT32_MAX)次请求了,相当牛逼。 MongoDB 客户端与server是通过 BSON 来交换数据的,而在 bson 标准里,是没有 uint32 类型的,所以 opid 最终是以 int32 传递给客户端的,shell 拿到这个opid,当这个值超过 INT32_MAX 时,打印出来就是负数了。 负数的 opid 会 kill 会不掉么? MongoDB 3.2.5 之前的确是有这个 bug,没有考虑到负数的情况,在 SERVER-23066 里已经修复了,阿里云上3.2、3.4、4.0 的最新版本均已修复这个问题。 修复的代码也很简单,就是接收到负数opid时,将其转换为 uint32 类型,详见 SERVER-23066 Make killOp accept negative opid mongod 既然已经修复了,负数 opid 还是 kill 不掉? 此时 killOp 不成功,已经跟 opid 是否是负数没有关系了,本来在 MongoDB 的设计里,也不是所有操作都能被 kill 的。 killOp 的原理,为什么 killOp 能干掉请求? MongoDB 一个用户连接,后端对应一个线程,本身一个请求开始后,会有一个线程一直执行,直到结束。能被 killOp 杀掉的请求,是因为请求在执行过程中会检测,是否收到了 kill 信号,如果收到了,就走结束请求的逻辑。所以 killOp 的作用也只是给对应的操作一个 kill 信号标志而已。 SomeCommand::Run() for (someCondition) { doSomeThing(); if (killOpReceived) { // SomeCommand 主动检测了 killOp 的信号,才能被 kill 掉 break; 什么样的操作需要被 kill 掉? 运行逻辑很简单、开销很低的命令无需捕获 killOp 信号,这种操作 kill 掉也没什么意义,解决不了根本问题。而复杂命令,比如 find、update、createIndex、aggregation 等操作,可能持续遍历很多条记录,才一定需要具备被 kill 的能力。MongoDB 会在执行这些命令的执行逻辑里加入检查是否收到 kill 命令的逻辑。 加了 killOp 检测逻辑的命令,就一定能立马被 kill? 不一定,一个操作比如 createIndex,会分为很多步骤,命令解析、加锁、执行具体命令逻辑、释放锁、回包等,只有命令执行到具体执行逻辑里时,killOp 才会生效,如果一个操作还没有成功加上锁,本身每占用什么资源,而且对应的线程也没有执行,killOp 是不会生效的。 query 操作为什么会加写锁? 正常只读的请求、如 find、listIndexes 都是不会加写锁,但当 MongoDB 开启 profiling 的时候,请求执行超过一定阈值(默认100ms)的请求,会记录到 db.system.profile capped colleciton 里,写这个集合就需要加意向写锁(w),同时对于 capped collection 的写入,会有一个特殊的 METADATA 互斥写锁(W),有兴趣的研究代码,关键字列在下面. const ResourceId resourceCappedInFlightForOtherDb = ResourceId(RESOURCE_METADATA, ResourceId::SINGLETON_CAPPED_IN_FLIGHT_OTHER_DB); Lock::ResourceLock cappedInsertLockForLocalDb( txn->lockState(), resourceCappedInFlightForLocalDb, MODE_X);
为什么我的 MongoDB 使用了 XX GB 内存? 一个机器上部署多个 Mongod 实例/进程,WiredTiger cache 应该如何配置? MongoDB 是否应该使用 SWAP 空间来降低内存压力? MongoDB 内存用在哪? Mongod 进程启动后,除了跟普通进程一样,加载 binary、依赖的各种library 到内存,其作为一个DBMS,还需要负责客户端连接管理,请求处理,数据库元数据、存储引擎等很多工作,这些工作都涉及内存的分配与释放,默认情况下,MongoDB 使用 Google tcmalloc 作为内存分配器,内存占用的大头主要是「存储引擎」与 「客户端连接及请求的处理」。 存储引擎 Cache MongoDB 3.2 及以后,默认使用 WiredTiger 存储引擎,可通过 cacheSizeGB 选项配置 WiredTiger 引擎使用内存的上限,一般建议配置在系统可用内存的60%左右(默认配置)。 举个例子,如果 cacheSizeGB 配置为 10GB,可以认为 WiredTiger 引擎通过tcmalloc分配的内存总量不会超过10GB。为了控制内存的使用,WiredTiger 在内存使用接近一定阈值就会开始做淘汰,避免内存使用满了阻塞用户请求。 目前有4个可配置的参数来支持 wiredtiger 存储引擎的 eviction 策略(一般不需要修改),其含义是: eviction_target 当 cache used 超过 eviction_target,后台evict线程开始淘汰 CLEAN PAGE eviction_trigger 当 cache used 超过 eviction_trigger,用户线程也开始淘汰 CLEAN PAGE eviction_dirty_target 当 cache dirty 超过 eviction_dirty_target,后台evict线程开始淘汰 DIRTY PAGE eviction_dirty_trigger 当 cache dirty 超过 eviction_dirty_trigger, 用户线程也开始淘汰 DIRTY PAGE 在这个规则下,一个正常运行的 MongoDB 实例,cache used 一般会在 0.8 * cacheSizeGB 及以下,偶尔超出问题不大;如果出现 used>=95% 或者 dirty>=20%,并一直持续,说明内存淘汰压力很大,用户的请求线程会阻塞参与page淘汰,请求延时就会增加,这时可以考虑「扩大内存」或者 「换更快的磁盘提升IO能力」。 TCP 连接及请求处理 MongoDB Driver 会跟 mongod 进程建立 tcp 连接,并在连接上发送数据库请求,接受应答,tcp 协议栈除了为连接维护socket元数据为,每个连接会有一个read buffer及write buffer,用户收发网络包,buffer的大小通过如下sysctl系统参数配置,分别是buffer的最小值、默认值以及最大值,详细解读可以google。 net.ipv4.tcp_wmem = 8192 65536 16777216 net.ipv4.tcp_rmem = 8192 87380 16777216 redhat7(redhat6上并没有导出这么详细的信息) 上通过 ss -m 可以查看每个连接的buffer的信息,如下是一个示例,读写 buffer 分别占了 2357478bytes、2626560bytes,即均在2MB左右;500个类似的连接就会占用掉 1GB 的内存;buffer 占到多大,取决于连接上发送/应答的数据包的大小、网络质量等,如果请求应答包都很小,这个buffer也不会涨到很大;如果包比较大,这个buffer就更容易涨的很大。 tcp ESTAB 0 0 127.0.0.1:51601 127.0.0.1:personal-agent skmem:(r0,rb2357478,t0,tb2626560,f0,w0,o0,bl0) 除了协议栈上的内存开销,针对每个连接,Mongod 会起一个单独的线程,专门负责处理这条连接上的请求,mongod 为处理连接请求的线程配置了最大1MB的线程栈,通常实际使用在几十KB左右,通过 proc 文件系统看到这些线程栈的实际开销。 除了处理请求的线程,mongod 还有一系列的后台线程,比如主备同步、定期刷新 Journal、TTL、evict 等线程,默认每个线程最大ulimit -s(一般10MB)的线程栈,由于这批线程数量比较固定,占的内存也比较可控。 # cat /proc/$pid/smaps 7f563a6b2000-7f563b0b2000 rw-p 00000000 00:00 0 Size: 10240 kB Rss: 12 kB Pss: 12 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 12 kB Referenced: 12 kB Anonymous: 12 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB 线程在处理请求时,需要分配临时buffer存储接受到的数据包,为请求建立上下文(OperationContext),存储中间的处理结果(如排序、aggration等)以及最终的应答结果等。 当有大量请求并发时,可能会观察到 mongod 使用内存上涨,等请求降下来后又慢慢释放的行为,这个主要是 tcmalloc 内存管理策略导致的,tcmalloc 为性能考虑,每个线程会有自己的 local free page cache,还有 central free page cache;内存申请时,按 local thread free page cache ==> central free page cache 查找可用内存,找不到可用内存时才会从堆上申请;当释放内存时,也会归还到 cache 里,tcmalloc 后台慢慢再归还给 OS, 默认情况下,tcmalloc 最多会 cache min(1GB,1/8 * system_memory) 的内存, 通过 setParameter.tcmallocMaxTotalThreadCacheBytesParameter 参数可以配置这个值,不过一般不建议修改,尽量在访问层面做调优) tcmalloc cache的管理策略,MongoDB 层暴露了几个参数来调整,一般不需要调整,如果能清楚的理解tcmalloc原理及参数含义,可做针对性的调优;MongoDB tcmalloc 的内存状态可以通过 db.serverStatus().tcmalloc 查看,具体含义可以看 tcmalloc 的文档。重点可以关注下 total_free_bytes ,这个值告诉你有多少内存是 tcmalloc 自己缓存着,没有归还给 OS 的。 mymongo:PRIMARY&gt; db.serverStatus().tcmalloc "generic" : { "current_allocated_bytes" : NumberLong("2545084352"), "heap_size" : NumberLong("2687029248") "tcmalloc" : { "pageheap_free_bytes" : 34529280, "pageheap_unmapped_bytes" : 21135360, "max_total_thread_cache_bytes" : NumberLong(1073741824), "current_total_thread_cache_bytes" : 1057800, "total_free_bytes" : 86280256, "central_cache_free_bytes" : 84363448, "transfer_cache_free_bytes" : 859008, "thread_cache_free_bytes" : 1057800, "aggressive_memory_decommit" : 0, 如何控制内存使用? 合理配置 WiredTiger cacheSizeGB 如果一个机器上只部署 Mongod,mongod 可以使用所有可用内存,则是用默认配置即可。 如果机器上多个mongod混部,或者mongod跟其他的一些进程一起部署,则需要根据分给mongod的内存配额来配置 cacheSizeGB,按配额的60%左右配置即可。 控制并发连接数 TCP连接对 mongod 的内存开销上面已经详细分析了,很多同学对并发有一定误解,认为「并发连接数越高,数据库的QPS就越高」,实际上在大部分数据库的网络模型里,连接数过高都会使得后端内存压力变大、上下文切换开销变大,从而导致性能下降。 MongoDB driver 在连接 mongod 时,会维护一个连接池(通常默认100),当有大量的客户端同时访问同一个mongod时,就需要考虑减小每个客户端连接池的大小。mongod 可以通过配置 net.maxIncomingConnections 配置项来限制最大的并发连接数量,防止数据库压力过载。 是否应该配置 SWAP 官方文档上的建议如下,意思是配置一下swap,避免mongod因为内存使用太多而OOM。 For the WiredTiger storage engine, given sufficient memory pressure, WiredTiger may store data in swap space. Assign swap space for your systems. Allocating swap space can avoid issues with memory contention and can prevent the OOM Killer on Linux systems from killing mongod. 开启 SWAP 与否各有优劣,SWAP开启,在内存压力大的时候,会利用SWAP磁盘空间来缓解内存压力,此时整个数据库服务会变慢,但具体变慢到什么程度是不可控的。不开启SWAP,当整体内存超过机器内存上线时就会触发OOM killer把进程干掉,实际上是在告诉你,可能需要扩展一下内存资源或是优化对数据库的访问了。 是否开启SWAP,实际上是在「好死」与「赖活着」的选择,个人觉得,对于一些重要的业务场景来说,首先应该为数据库规划足够的内存,当内存不足时,「及时调整扩容」比「不可控的慢」更好。 尽量减少内存排序的场景,内存排序一般需要更多的临时内存 主备节点配置差距不要过大,备节点会维护一个buffer(默认最大256MB)用于存储拉取到oplog,后台从buffer里取oplog不断重放,当备同步慢的时候,这个buffer会持续使用最大内存。 控制集合及索引的数量,减少databse管理元数据的内存开销;集合、索引太多,元数据内存开销是一方面的影响,更多的会影响启动加载的效率、以及运行时的性能。
上个月底 MongoDB Wolrd 宣布发布 MongoDB 4.0, 支持复制集多文档事务,阿里云数据库团队 研发工程师第一时间对事务功能的时间进行了源码分析,解析事务实现机制。 MongoDB 4.0 引入的事务功能,支持多文档ACID特性,例如使用 mongo shell 进行事务操作 > s = db.getMongo().startSession() session { "id" : UUID("3bf55e90-5e88-44aa-a59e-a30f777f1d89") } > s.startTransaction() > session.getDatabase("mytest").coll01.insert({x: 1, y: 1}) WriteResult({ "nInserted" : 1 }) > session.getDatabase("mytest").coll02.insert({x: 1, y: 1}) WriteResult({ "nInserted" : 1 }) > s.commitTransaction() (或者 s.abortTransaction()回滚事务) 支持 MongoDB 4.0 的其他语言 Driver 也封装了事务相关接口,用户需要创建一个 Session,然后在 Session 上开启事务,提交事务。例如 python 版本 with client.start_session() as s: s.start_transaction() collection_one.insert_one(doc_one, session=s) collection_two.insert_one(doc_two, session=s) s.commit_transaction() java 版本 try (ClientSession clientSession = client.startSession()) { clientSession.startTransaction(); collection.insertOne(clientSession, docOne); collection.insertOne(clientSession, docTwo); clientSession.commitTransaction(); Session Session 是 MongoDB 3.6 版本引入的概念,引入这个特性主要就是为实现多文档事务做准备。Session 本质上就是一个「上下文」。 在以前的版本,MongoDB 只管理单个操作的上下文,mongod 服务进程接收到一个请求,为该请求创建一个上下文 (源码里对应 OperationContext),然后在服务整个请求的过程中一直使用这个上下文,内容包括,请求耗时统计、请求占用的锁资源、请求使用的存储快照等信息。有了 Session 之后,就可以让多个请求共享一个上下文,让多个请求产生关联,从而有能力支持多文档事务。 每个 Session 包含一个唯一的标识 lsid,在 4.0 版本里,用户的每个请求可以指定额外的扩展字段,主要包括: lsid: 请求所在 Session 的 ID, 也称 logic session id txnNmuber: 请求对应的事务号,事务号在一个 Session 内必须单调递增 stmtIds: 对应请求里每个操作(以insert为例,一个insert命令可以插入多个文档)操作ID 实际上,用户在使用事务时,是不需要理解这些细节,MongoDB Driver 会自动处理,Driver 在创建 Session 时分配 lsid,接下来这个 Session 里的所以操作,Driver 会自动为这些操作加上 lsid,如果是事务操作,会自动带上 txnNumber。 值得一提的是,Session lsid 可以通过调用 startSession 命令让 server 端分配,也可以客户端自己分配,这样可以节省一次网络开销;而事务的标识,MongoDB 并没有提供一个单独的 startTransaction的命令,txnNumber 都是直接由 Driver 来分配的,Driver 只需保证一个 Session 内,txnNumber 是递增的,server 端收到新的事务请求时,会主动的开始一个新事务。 MongoDB 在 startSession 时,可以指定一系列的选项,用于控制 Session 的访问行为,主要包括: causalConsistency: 是否提供 causal consistency 的语义,如果设置为true,不论从哪个节点读取,MongoDB 会保证 "read your own write" 的语义。参考 causal consistency readConcern:参考 MongoDB readConcern 原理解析 writeConcern:参考 MongoDB writeConcern 原理解析 readPreference: 设置读取时选取节点的规则,参考 read preference retryWrites:如果设置为true,在复制集场景下,MongoDB 会自动重试发生重新选举的场景; 参考retryable write Atomic 针对多文档的事务操作,MongoDB 提供 "All or nothing" 的原子语义保证。 Consistency 太难解释了,还有抛弃 Consistency 特性的数据库? Isolation MongoDB 提供 snapshot 隔离级别,在事务开始创建一个 WiredTiger snapshot,然后在整个事务过程中使用这个快照提供事务读。 Durability 事务使用 WriteConcern {j: ture} 时,MongoDB 一定会保证事务日志提交才返回,即使发生 crash,MongoDB 也能根据事务日志来恢复;而如果没有指定 {j: true} 级别,即使事务提交成功了,在 crash recovery 之后,事务的也可能被回滚掉。 事务与复制 复制集配置下,MongoDB 整个事务在提交时,会记录一条 oplog(oplog 是一个普通的文档,所以目前版本里事务的修改加起来不能超过文档大小 16MB的限制),包含事务里所有的操作,备节点拉取oplog,并在本地重放事务操作。 事务 oplog 示例,包含事务操作的 lsid,txnNumber,以及事务内所有的操作日志(applyOps字段) "ts" : Timestamp(1530696933, 1), "t" : NumberLong(1), "h" : NumberLong("4217817601701821530"), "v" : 2, "op" : "c", "ns" : "admin.$cmd", "wall" : ISODate("2018-07-04T09:35:33.549Z"), "lsid" : { "id" : UUID("e675c046-d70b-44c2-ad8d-3f34f2019a7e"), "uid" : BinData(0,"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") }, "txnNumber" : NumberLong(0), "stmtId" : 0, "prevOpTime" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "o" : { "applyOps" : [ { "op" : "i", "ns" : "test.coll2", "ui" : UUID("a49ccd80-6cfc-4896-9740-c5bff41e7cce"), "o" : { "_id" : ObjectId("5b3c94d4624d615ede6097ae"), "x" : 20000 } }, { "op" : "i", "ns" : "test.coll3", "ui" : UUID("31d7ae62-fe78-44f5-ba06-595ae3b871fc"), "o" : { "_id" : ObjectId("5b3c94d9624d615ede6097af"), "x" : 20000 } } ] } } 整个重放过程如下: 获取当前 Batch (后台不断拉取 oplog 放入 Batch) 设置 OplogTruncateAfterPoint 时间戳为 Batch里第一条 oplog 时间戳 (存储在 local.replset.oplogTruncateAfterPoint 集合) 写入 Batch 里所有的 oplog 到 local.oplog.rs 集合,根据 oplog 条数,如果数量较多,会并发写入加速 清理 OplogTruncateAfterPoint, 标识 oplog 完全成功写入;如果在本步骤完成前 crash,重启恢复时,发现 oplogTruncateAfterPoint 被设置,会将 oplog 截短到该时间戳,以恢复到一致的状态点。 将 oplog 划分到到多个线程并发重放,为了提升并发效率,事务产生的 oplog 包含的所有修改操作,跟一条普通单条操作的 oplog 一样,会据文档ID划分到多个线程。 更新 ApplyThrough 时间戳为 Batch 里最后一条 oplog 时间戳,标识下一次重启后,从该位置重新同步,如果本步骤之前失败,重启恢复时,会从 ApplyThrough 上一次的值(上一个 Batch 最后一条 oplog)拉取 oplog。 更新 oplog 可见时间戳,如果有其他节点从该备节点同步,此时就能读到这部分新写入的 oplog 更新本地 Snapshot(时间戳),新的写入将对用户可见。 事务与存储引擎 事务时序统一 WiredTiger 很早就支持事务,在 3.x 版本里,MongoDB 就通过 WiredTiger 事务,来保证一条修改操作,对数据、索引、oplog 三者修改的原子性。但实际上 MongoDB 经过多个版本的迭代,才提供了事务接口,核心难点就是时序问题。 MongoDB 通过 oplog 时间戳来标识全局顺序,而 WiredTiger 通过内部的事务ID来标识全局顺序,在实现上,2者没有任何关联。这就导致在并发情况下, MongoDB 看到的事务提交顺序与 WiredTiger 看到的事务提交顺序不一致。 为解决这个问题,WiredTier 3.0 引入事务时间戳(transaction timestamp)机制,应用程序可以通过 WT_SESSION::timestamp_transaction 接口显式的给 WiredTiger 事务分配 commit timestmap,然后就可以实现指定时间戳读(read "as of" a timestamp)。有了 read "as of" a timestamp 特性后,在重放 oplog 时,备节点上的读就不会再跟重放 oplog 有冲突了,不会因重放 oplog 而阻塞读请求,这是4.0版本一个巨大的提升。 * __wt_txn_visible -- * Can the current transaction see the given ID / timestamp? static inline bool __wt_txn_visible( WT_SESSION_IMPL *session, uint64_t id, const wt_timestamp_t *timestamp) if (!__txn_visible_id(session, id)) return (false); /* Transactions read their writes, regardless of timestamps. */ if (F_ISSET(&session->txn, WT_TXN_HAS_ID) && id == session->txn.id) return (true); #ifdef HAVE_TIMESTAMPS WT_TXN *txn = &session->txn; /* Timestamp check. */ if (!F_ISSET(txn, WT_TXN_HAS_TS_READ) || timestamp == NULL) return (true); return (__wt_timestamp_cmp(timestamp, &txn->read_timestamp) <= 0); #else WT_UNUSED(timestamp); return (true); #endif 从上面的代码可以看到,再引入事务时间戳之后,在可见性判断时,还会额外检查时间戳,上层读取时指定了时间戳读,则只能看到该时间戳以前的数据。而 MongoDB 在提交事务时,会将 oplog 时间戳跟事务关联,从而达到 MongoDB Server 层时序与 WiredTiger 层时序一致的目的。 事务对 cache 的影响 WiredTiger(WT) 事务会打开一个快照,而快照的存在的 WiredTiger cache evict 是有影响的。一个 WT page 上,有N个版本的修改,如果这些修改没有全局可见(参考 __wt_txn_visible_all),这个 page 是不能 evict 的(参考 __wt_page_can_evict)。 在 3.x 版本里,一个写请求对数据、索引、oplog的修改会放到一个 WT 事务里,事务的提交由 MongoDB 自己控制,MongoDB 会尽可能快的提交事务,完成写清求;但 4.0 引入事务之后,事务的提交由应用程序控制,可能出现一个事务修改很多,并且很长时间不提交,这会给 WT cache evict 造成很大的影响,如果大量内存无法 evict,最终就会进入 cache stuck 状态。 为了尽量减小 WT cache 压力,MongoDB 4.0 事务功能有一些限制,但事务资源占用超过一定阈值时,会自动 abort 来释放资源。规则包括 事务的生命周期不能超过 transactionLifetimeLimitSeconds (默认60s),该配置可在线修改 事务修改的文档数不能超过 1000 ,不可修改 事务修改产生的 oplog 不能超过 16mb,这个主要是 MongoDB 文档大小的限制, oplog 也是一个普通的文档,也必须遵守这个约束。 Read as of a timestamp 与 oldest timestamp Read as of a timestamp 依赖 WiredTiger 在内存里维护多版本,每个版本跟一个时间戳关联,只要 MongoDB 层可能需要读的版本,引擎层就必须维护这个版本的资源,如果保留的版本太多,也会对 WT cache 产生很大的压力。 WiredTiger 提供设置 oldest timestamp 的功能,允许由 MongoDB 来设置该时间戳,含义是Read as of a timestamp 不会提供更小的时间戳来进行一致性读,也就是说,WiredTiger 无需维护 oldest timestamp 之前的所有历史版本。MongoDB 层需要频繁(及时)更新 oldest timestamp,避免让 WT cache 压力太大。 引擎层 Rollback 与 stable timestamp 在 3.x 版本里,MongoDB 复制集的回滚动作是在 Server 层面完成,但节点需要回滚时,会根据要回滚的 oplog 不断应用相反的操作,或从回滚源上读取最新的版本,整个回滚操作效率很低。 4.0 版本实现了存储引擎层的回滚机制,当复制集节点需要回滚时,直接调用 WiredTiger 接口,将数据回滚到某个稳定版本(实际上就是一个 Checkpoint),这个稳定版本则依赖于 stable timestamp。WiredTiger 会确保 stable timestamp 之后的数据不会写到 Checkpoint里,MongoDB 根据复制集的同步状态,当数据已经同步到大多数节点时(Majority commited),会更新 stable timestamp,因为这些数据已经提交到大多数节点了,一定不会发生 ROLLBACK,这个时间戳之前的数据就都可以写到 Checkpoint 里了。 MongoDB 需要确保频繁(及时)的更新 stable timestamp,否则影响 WT Checkpoint 行为,导致很多内存无法释放。 分布式事务 MongoDB 4.0 支持副本集多文档事务,并计划在 4.2 版本支持分片集群事务功能。下图是从 MongoDB 3.0 引入 WiredTiger 到 4.0 支持多文档事务的功能迭代图,可以发现一盘大棋即将上线,敬请期待。
MongoDB 因其灵活的文档模型、可扩展分布式设计广受开发者喜爱,在此基础上,MongoDB 4.0 推出了更强大的功能支持,目前4.0第一个RC版本已经发布,本文将介绍 MongoDB 4.0 核心的一些新特性。 多文档事务(Multi-Document ACID Transaction) 结合 MongoDB 文档模型内嵌数组、文档的支持,目前的单文档事务能满足绝大部分开发者的需求。为了让 MongoDB 能适应更多的应用场景,让开发变得更简单,MongoDB 4.0 将支持复制集内部跨一或多个集合的多文档事务,保证针对多个文档的更新的原子性。而在未来的 MongoDB 4.2 版本,还会支持分片集群的分布式事务。 MongoDB 的事务接口非常简单,开发者只需要将「需要保证原子性的更新序列」放到一个 session 的 开始事务 与提交事务之间即可。 如下是 Python API 使用事务的例子 with client.start_session() as s: s.start_transaction(): collection.insert_one(doc1, session=s) collection.insert_one(doc2, session=s) except: s.abort_transaction() raise s.commit_transaction() 如下是 Java API 使用事务的例子 try (ClientSession clientSession = client.startSession()) { clientSession.startTransaction(); try { collection.insertOne(clientSession, docOne); collection.insertOne(clientSession, docTwo); clientSession.commitTransaction(); } catch (Exception e) { clientSession.abortTransaction(); 事务是 MongoDB 开发团队经过3年多努力的结果,从3.0版本引入 WiredTiger 、到3.2版本支持 ReadConcern、3.6 支持 Causal Consistency 等很多工作都是在为事务功能做准备,最终在4.0版本将整个事务的API提供给用户,帮助用户更好的构建应用。 聚合类型转换( Aggregation Pipeline Type Conversions) 灵活的文档模型是 MongoDB 相比传统关系型数据库的一大优势,应用开发者无需为存储的数据预先定义结构(或者模式),这使得开发者能快速的应对开发需求的迭代;在灵活的同时,MongoDB 还提供了 schema validation 功能,使得开发者可以根据需要定义文档数据的模型。 MongoDB 的文档允许用户灵活的写入各种类型的数据字段,这给消费数据带了一定的复杂性,比如一些数据分析的场景,应用通常希望某个字段的数据拥有统一的类型,以方便数据处理。 MongoDB 4.0 引入了新的聚合操作符 $convert, 允许用户在 aggregation pipeline 里将文档的字段转换成统一的类型输出,使得数据消费端,比如 MongoDB BI 工具、Spark Connectors 以及其他 ETL 工具能更简单的处理 MongoDB 数据。 非阻塞的备节点读(Non-Blocking Secondary Reads) 为了确保备节点上的读与主节点保持相同的因果一致性语义,MongoDB 备节点在批量应用 oplog 的时候会阻塞读请求,这使得在高写入负载下,备节点上读的平均延时通常比主节点更高。 借助事务功能中 storage engine timestamps and snapshots 的实现,引擎层可以很容易的实现「指定时间戳快照读取的功能」,使得备节点上的读请求无需阻塞等待就能读到一致时间点的数据。这个特性将极大的提升 MongoDB 读扩展的能力。 迁移速度提升40%(40% Faster Data Migrations) 应用在不断演进过程中,其负载特性也在不断发生变化,这就要求数据库具备扩展的能力,及时适应应用的负载变化。MongoDB 分片集群支持实时的添加、移除shard 节点,并能在各个 shard 之间自动迁移数据来均衡负载。 MongoDB 4.0 支持在迁移数据的过程中,并发的读取(源端)和写入(目标端),使得迁移的性能提升了约 40%, 使得新添加的节点能更快的承载业务压力,让分片集群发挥最佳效果。 扩展修改订阅(Extensions to Change Streams) MongoDB 3.6 推出了修改订阅( Change Streams)的功能,使得用户能实时的获取数据的修改,同时通过 Change Streams 还能很方便的实现多数据中心跨复制集的数据同步。MongoDB 4.0 进一步扩展 Change Streams 功能,可以实现分片集群维度的修改订阅。 开始体验 MongoDB 4.0 RC 猛击这里体验MongoDB 4.0 RC 查看Release Notes for MongoDB 4.0 Release Candidate
上周末花了几个小时刷完《SQL反模式》这本书,书里介绍了数据库应用开发者最长遇到的一些问题,虽然这本书面向的读者是使用数据库的应用开发者,但它对数据库管理员、数据库开发者同样会有启发,强烈推荐阅读。本书涉及的问题包括但不限于 如何存储多值属性? 如何使用关系模型表达树结构? 如何建立主键规范? 如何支持可变的属性/字段? 如何从表中随机选择一行? 如何实现文本查询的需求? 如何存储文件类型数据? 如何限定列的有效值? 如何表达精准浮点数?10.如何写出安全(难以 SQL 注入)的 SQL 语句? 针对上面的问题在实际的开发场景中都经常遇到,作者介绍了一系列「反模式」的设计思路,我发现很多都是开发过程中很容易犯的错误。以「如何存储多值属性」为例,最直观的反模式设计思路就是「复用原来字段,格式化的逗号分割列表」,如果需求比较局限,联系人数量不会无限制扩展,针对联系人字段也不会经常有查询、聚合需求,这种方法的确成本很低。 但实际上这种方法缺点很多,比如(1)针对这个字段的查询,基本都得使用正则表达式,而正则表达式没有确定的规范,不同的数据库支持都不一样,导致写出的 SQL 也不具备通用性。(2) 针对该字段里属性的查询无法使用索引 (3)列长度有限制,属性数量扩展有上限 ... 而这些随着应用需求的不断变化,可能对系统产生非常大的影响,扩展起来非常麻烦。 从数据库开发者的角度看,对于这么多可能误用的场景,我们需要思考数据库服务本身能做什么工作来简化、或者规避问题,比如 很多新的存储引擎都具备了 SAMPLE 随机取样的能力,方便用户随机获取记录,避免了用户 阿里云数据库上,专门有针对防 SQL 防注入的检查,减小了用户犯错造成的影响 对于属性可扩展的需求,SQL 可能并不是最佳需求,一些基于 KV、Document 的接口的数据库,如 Redis、MongoDB,可能是更加的选择。 对于文件存储的需求,存储在数据库节点同机器上文件系统上有很多问题, 但量大的时候,存储在数据库里也无法满足需求;目前比较常用的方案时,文件存储到专门的分布式文件系统里,数据库里存储对象的标识名。
之前写过一篇MongoDB 无法启动,如何恢复数据的文章,介绍了几种从无法启动的 MongoDB 节点恢复数据的方法,主要包括: 如果配置了副本集多节点,则从其他节点恢复(强烈建议重要的数据至少要存2份) 从最近的备份集恢复,一般重要的生产数据,需要对数据进行持续的全量/增量备份 repair 模式恢复,如果元数据本身有问题,repair 模式也是无法工作的; 通过 WiredTiger 自带工具分析,对元数据损坏的情况也使用,能尽可能多的恢复数据。 通过分析 BSON 数据来提取恢复数据,但这个只对没有压缩的 mmapv1 引擎有效,默认 WiredTiger 会开启 snappy 压缩,无法通过分析 BSON 来提取出数据。 其中方法1-3比较简单,第4种方法对 WiredTiger 引擎的原理不了解,可能完全无从下手,本文将详细介绍如何通过 WiredTiger 的工具来提取有效数据。 每个 MongoDB 的集合/索引都对应一个 WiredTiger Table; 集合名跟 Table 名的映射关系保存在元数据里,只有通过这部分元数据,才能获得这个映射关系。 集合的数据对应 collecton-uniqueid-hash.wt 文件 索引的数据对应 index-uniqueid-hash.wt MongoDB 所有集合的元数据,都存在一个特殊的 WiredTiger Table 里,名字叫 _mdb_catalog;这里可以看出,MongoDB 的元数据,对 WiredTiger 来说其实也只是普通数据。 只要单个表的数据有效,通过 WiredTiger 的 wt工具可以将其提取出来。默认 MongoDB 的源码里不会编译出 wt 工具,可以自行下载 WiredTiger 源码编译,接下来的介绍假设你已经编译出了 wt 工具。 恢复单个集合 假设你的数据库只有很少的集合,根据集合的大小,你可能很容易判断出集合名跟 WiredTiger 文件名的对应关系,比如 somedb.collection ===> somedb/collection-10--6822964274931136278 (如果没有指定 directoryPerDB 的选项,则没有 somedb/前缀 只要这个这个集合的数据还是完整的,没有被损坏,我们就可以通过如下步骤来恢复 Step1: 通过 wt 工具将集合数据 dump 到文件 ./wt -v -h some_db_home -C "extensions=[./ext/compressors/snappy/.libs/libwiredtiger_snappy.so]" -R dump -f collection.dump somedb/collection-10--6822964274931136278 此时,这个集合的数据就已经导出到 collection.dump 文件了,如果这一步出错,说明这个文件 WiredTiger 已经无法解析了,则无法恢复了。 Step2: 在新的实例上创建一个临时集合 mkdir some_dest_db_home mongod --dbpath some_dest_db_home --port some_port mongo --port some_port > use somedb > db.createCollection("collection") // 创建临时集合 > db.collection.stats().uri // 查看该集合对应的 WiredTiger 表名 假设临时集合创建后,其在新的临时实例上集合名与 WiredTiger 表名对应关系如下 somedb.collection ===> somedb/collection-2--6822964274931136278 (如果没有指定 directoryPerDB 的选项,则没有 somedb/前缀 Step3: 将 dump 出的数据 load 到临时集合 停掉临时实例,然后将 Step1 里 dump 的数据 load 到临时集合 ./wt -v -h some_dest_db_home -C "extensions=[./ext/compressors/snappy/.libs/libwiredtiger_snappy.so]" -R load -f collection.dump -r somedb/collection-2--6822964274931136278 这时在目标实例上,访问 somedb.collection 时,访问的数据就是从「已经损坏的源实例」上 somedb.collection 的数据,只不过这个实例的 id 索引、统计元数据等是无法匹配的,但这并不影响全表扫描的数据访问。 Step4:mongodump/mongorestore 临时集合,修正数据统计信息、索引信息 通过 mongodump 从目标实例将 somedb.collection 备份出来,mongodump 只会触发对集合数据的顺序访问。 然后通过 mongorestore 重新导入,restore 后的数据即为恢复的目标数据。 恢复大量集合 上面介绍了如何恢复单个集合,如果损坏的 MongoDB 里有大量的集合,一个个按上面的流程恢复要搞到猴年马月了;要想自动化,需要解决的关键问题就是如果确定 MongoDB 集合名 与 WiredTiger 表名的映射关系,这里只需要稍加修改 MongoDB 源码,可以让 损坏的mongod 在 repair 模式下把映射关系输出 diff --git a/src/mongo/db/storage/kv/kv_database_catalog_entry.cpp b/src/mongo/db/storage/kv/kv_database_catalog_entry.cpp index 91afa40026..523cb1fa95 100644 --- a/src/mongo/db/storage/kv/kv_database_catalog_entry.cpp +++ b/src/mongo/db/storage/kv/kv_database_catalog_entry.cpp @@ -260,6 +260,8 @@ void KVDatabaseCatalogEntry::initCollection(OperationContext* opCtx, const std::string ident = _engine->getCatalog()->getCollectionIdent(ns); + log() << "metadata mapping " << ns << " " << ident; RecordStore* rs; if (forRepair) { // Using a NULL rs since we don't want to open this record store before it has been Step1: 获取损坏实例的MongoDB 集合名 与 WiredTiger 表名的映射关系,例如 somedb.collection1 collection-2--4775156767705741267 somedb.collection2 collection-4--4775156767705741267 Step2:根据上述映射关系,将集合数据逐个 dump 写个 shell 脚本很容易做到自动化,导出的文件名可以跟集合名保持一致,用于区分 Step3:创建目标实例,并预先创建好所有的集合 写个 js 脚本,通过 mongo shell 批量创建 Step4: 获取目标实例的MongoDB 集合名 与 WiredTiger 表名的映射关系,例如 somedb.collection1 collection-6--4335156767705741253 somedb.collection2 collection-8--4335156767705741253 Step5:将 dump 数据逐个 load 到 目标实例 跟 Step2 类似,需要写个 shell 脚本 Step6: 对整个目标实例(或部分 DB)进行 mongodump/mongorestore 修正 id 索引、统计信息等 祝你成功 ^_^ ^_^ ^_^
本文内容来自团队内部的技术分享,主要介绍 InnoDB 内部实现原理,基于官方文档以及网上的一些 InnoDB 的 PPT 介绍,从个人视角讲述对 InnoDB 的理解,文中的配图均来自互联网,通过对应的链接可以扩展阅读。 PS: 对数据库内核感兴趣的同学,可以联系我私聊,阿里云数据库团队需要你。
There is a great new feature in the release note of MongoDB 3.5.12. Faster In-place Updates in WiredTiger This work brings improvements to in-place update workloads for users running the WiredTiger engine, especially for updates to large documents. Some workloads may see a reduction of up to 7x in disk utilization (from 24 MB/s to 3 MB/s) as well as a 20% improvement in throughput. I thought wiredtiger has impeletementd the delta page feature introduced in the bw-tree paper, that is, writing pages that are deltas from previously written pages. But after I read the source code, I found it's a totally diffirent idea, in-place update only impacted the in-meomry and journal format, the on disk layout of data is not changed. I will explain the core of the in-place update implementation. MongoDB introduced mutable bson to descirbe document update as incremental(delta) update. Mutable BSON provides classes to facilitate the manipulation of existing BSON objects or the construction of new BSON objects from scratch in an incremental fashion. Suppose you have a very large document, see 1MB _id: ObjectId("59097118be4a61d87415cd15"), name: "ZhangYoudong", birthday: "xxxx", fightvalue: 100, xxx: .... // many other fields If the fightvalue is changed from 100 to 101, you can use a DamageEvent to describe the update, it just tells you the offset、size、content(kept in another array) of the change. struct DamageEvent { typedef uint32_t OffsetSizeType; // Offset of source data (in some buffer held elsewhere). OffsetSizeType sourceOffset; // Offset of target data (in some buffer held elsewhere). OffsetSizeType targetOffset; // Size of the damage region. size_t size; So if you have many small changes for a document, you will have DamageEvent array, MongoDB add a new storage interface to support inserting DamageEvent array (DamageVector). bool WiredTigerRecordStore::updateWithDamagesSupported() const { return true; StatusWith<RecordData> WiredTigerRecordStore::updateWithDamages( OperationContext* opCtx, const RecordId& id, const RecordData& oldRec, const char* damageSource, const mutablebson::DamageVector& damages) { WiredTiger added a new update type called WT_UPDATE_MODIFIED to support MongoDB, when a WT_UPDATE_MODIFIED update happened, wiredTiger first logged a change list which is transformed from DamageVector into journal, then kept the change list in memory associated with the original record. When the record is read, wiredTiger will first read the original record, then apply every operation in change list, returned the final record to the client. So the core for in-place update: WiredTiger support delta update in memory and journal, so the IO of writing journal will be greatly reduced for large document. WiredTiger's data layout is kept unchanged, so the IO of writing data is not changed.
最初的写入流程,继承自 leveldb,多个 写线程组成一个 group, leader 负责 group 的 WAL 及 memtable 的提交,提交完后唤醒所有的 follwer,向上层返回。 支持 allow_concurrent_memtable_write 选项,在1的基础上,leader 提交完 WAL 后,group 里所有线程并发写 memtable。原理如下图所示,这个改进在 sync=0的时候,有3倍写入性能提升,在 sync=1时,有2倍性能提升,参考Concurrent inserts and the RocksDB memtable 支持 enable_pipelined_write 选项,在2的基础上,引入流水线,第一个 group 的 WAL 提交后,在执行 memtable 写入时,下一个 group 同时开启,已到达 Pipeline 写入的效果。
MongoDB 3.4 社区版于2016年年底正式发布,目前已经历10次的小版本迭代,在经过长时间的内部场景测试后,阿里云数据库团队正式支持 MongoDB 3.4,让用户直接在云上享受稳定的数据库服务。 MongoDB 3.4 的主要功能改进参考这里,简单总结一下就是: 更快的主备同步,参考 MongoDB 3.4 复制集全量同步改进 更高效的Sharding集群,参考 MongoDB 升级3.4对均衡的影响 更强大的功能,如 Readonly View、Collation、Decimal type等 更丰富的aggregation操作,如$bucket、$graghLookup One more thing 阿里云数据库 MongoDB 3.4 版本里,除了上述官方社区版本的特性外,我们还正式支持了 Mongorocks 引擎,一款基于RocksDB 实现的 MongoDB 存储引擎。 MongoDB 当前默认的 Wiredtiger 引擎非常优秀,相比 MongoDB 早期的 mmapv1 存续引擎性能上有非常大的提升,而且支持数据压缩,存储成本更低。 Wiredtiger 基于 btree 结构组织数据,在一些极端场景下,因为 Cache eviction 及写入放大的问题,可能导致 Write hang,细节可以到 MongoDB jira 上了解相关的issue,针对这些问题 MongoDB 官方团队一直在优化,我们也看到 Wiredtiger 稳定性在不断提升;而 RocksDB 是基于 LSM tree 结构组织数据,其针对写入做了优化,将随机写入转换成了顺序写入,能保证持续高效的数据写入。 如下是使用 sysbench 进行的一个简单的 insert 测试,insert 的集合默认带一个二级索引,在刚开始 Wiredtiger 的写入性能远超 RocksDB,而随着数据量越来越大,WT的写入能力开始下降,而 RocksDB 的写入一直比较稳定。 更多 Wiredtiger、Mongorocks 的对比可以参考 Facebook 大神在 Percona Live 上的技术分享。 除了 RocksDB,MongoDB 云数据库还支持 TerarkDB 引擎,借助 TerarkDB 的全局压缩技术,在提高压缩率的同时,能大幅提高随机查询的性能。 阿里云数据库MongoDB版功能一览 欢迎大家来体验宇宙最强的 MongoDB 云数据库服务
线上某业务,频繁出现IOPS 使用率100%的(每秒4000IOPS)现象,每次持续接近1个小时,从慢请求的日志发现是一个 getMore 请求耗时1个小时,导致IOPS高;深入调查之后,最终发现竟是一个索引选择的问题。 2017-11-01T15:04:17.498+0800 I COMMAND [conn5735095] command db.mycoll command: getMore { getMore: 215174255789, collection: "mycoll" } cursorid:215174255789 keyUpdates:0 writeConflicts:0 numYields:161127 nreturned:8419 reslen:4194961 locks:{ Global: { acquireCount: { r: 322256 } }, Database: { acquireCount: { r: 161128 } }, Collection: { acquireCount: { r: 161128 } } } protocol:op_command 3651743ms 业务每个整点开始,会把过去1小时的数据同步到另一个数据源,查询时会按 _id 排序,2个主要查询条件如下,先执行find命令,然后遍历cursor,读取所有满足条件的文档。 * created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" } * sort: {_id: 1} 业务数据的特性 每条数据插入时都带上 created_at 字段,时间为当前时间戳,并建立了 {created_at: -1} 的索引 _id 字段为用户自定义(并非mongodb默认的ObjectId),取值较随机,无规律 整个集合非常大,总文档数超过1亿条 MongoDB的find、getMore特性 find命令,会返回第一批满足条件的batch(默认101条记录)以及一个cursor getMore 根据find返回的cursor继续遍历,每次遍历默认返回不超过4MB的数据 索引的选择 方案1:使用 created_at 索引 整个执行路径为 通过 created_at 索引,快速定位到符合条件的文档 读出所有的满足 created_at 查询条件的文档 对所有的文档根据 _id 字段进行排序 如下是走这个索引的2条典型日志,可以看出 符合 created_at 条件的文档大概有7w+,全部排序后,返回前101条,总共耗时约600ms; 接下来 getMore,因为结果要按_id排序,getMore 还是得继续把所有符合条件的读出来排序,并跳过第一次的101条,返回下一批给客户端。 2017-11-01T14:02:31.861+0800 I COMMAND [conn5737355] command db.mycoll command: find { find: "mycoll", filter: { created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" } }, projection: { $sortKey: { $meta: "sortKey" } }, sort: { _id: 1 }, limit: 104000, shardVersion: [ Timestamp 5139000|7, ObjectId('590d9048c628ebe143f76863') ] } planSummary: IXSCAN { created_at: -1.0 } cursorid:215494987197 keysExamined:71017 docsExamined:71017 hasSortStage:1 keyUpdates:0 writeConflicts:0 numYields:557 nreturned:101 reslen:48458 locks:{ Global: { acquireCount: { r: 1116 } }, Database: { acquireCount: { r: 558 } }, Collection: { acquireCount: { r: 558 } } } protocol:op_command 598ms 2017-11-01T14:02:32.036+0800 I COMMAND [conn5737355] command db.mycoll command: getMore { getMore: 215494987197, collection: "mycoll" } cursorid:215494987197 keyUpdates:0 writeConflicts:0 numYields:66 nreturned:8510 reslen:4194703 locks:{ Global: { acquireCount: { r: 134 } }, Database: { acquireCount: { r: 67 } }, Collection: { acquireCount: { r: 67 } } } protocol:op_command 120ms 方案2:使用 _id 索引 整个执行路径为 根据 _id 索引,扫描所有的记录 (按_id索引的顺序扫描,对应的文档的created_at是随机的,无规律) 把满足 created_at 条件的文档返回,第一次find,要找到101个符合条件的文档返回 如下是走这个索引的2条典型日志,可以看出 第一次扫描了17w,才找到101条符合条件的记录,耗时46s 第二次要累计近4MB符合条件的文档(8419条)才返回,需要全表扫描更多的文档,最终耗时1个小时,由于全表扫描对cache非常不友好,所以一直是要从磁盘读取,所以导致大量的IO。 2017-11-01T14:03:25.648+0800 I COMMAND [conn5735095] command db.mycoll command: find { find: "mycoll", filter: { created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" } }, projection: { $sortKey: { $meta: "sortKey" } }, sort: { _id: 1 }, limit: 75000, shardVersion: [ Timestamp 5139000|7, ObjectId('590d9048c628ebe143f76863') ] } planSummary: IXSCAN { _id: 1 } cursorid:215174255789 keysExamined:173483 docsExamined:173483 fromMultiPlanner:1 replanned:1 keyUpdates:0 writeConflicts:0 numYields:2942 nreturned:101 reslen:50467 locks:{ Global: { acquireCount: { r: 5886 } }, Database: { acquireCount: { r: 2943 } }, Collection: { acquireCount: { r: 2943 } } } protocol:op_command 46232ms 2017-11-01T15:04:17.498+0800 I COMMAND [conn5735095] command db.mycoll command: getMore { getMore: 215174255789, collection: "mycoll" } cursorid:215174255789 keyUpdates:0 writeConflicts:0 numYields:161127 nreturned:8419 reslen:4194961 locks:{ Global: { acquireCount: { r: 322256 } }, Database: { acquireCount: { r: 161128 } }, Collection: { acquireCount: { r: 161128 } } } protocol:op_command 3651743ms IOPS高是因为选择的索引不是最优,那为什么MongoDB没有选择最优的索引来执行这个任务呢? 从日志可以看出,绝大部分情况,MongoDB 都是走的 created_at 索引 上述case,那个索引更优,其实是跟数据的分布情况相关的 如果满足 created_at 查询条件的文档特别多,那么对大量的文档排序的开销也是很大的 如果 created_at 字段分布非常离散(如本案例中的数据),则全表扫描找出符合条件的文档开销更大 MongoDB 的索引是基于采样代价模型,一个索引对采样的数据集更优,并不意味着其对整个数据集也最优 MongoDB 一个查询第一次执行时,如果有多个执行计划,会根据模型选出最优的,并缓存起来,以提升效率 当 MongoDB 发生集合创建/删除索引时,会将缓存的执行计划清空掉,并重新选择 MongoDB 在执行的过程中,也会根据执行计划的表现,比如一个执行计划,很多次迭代都没遇到符合条件的文档,就会考虑这个执行计划是否最优了,会触发重新构建执行计划的逻辑(具体触发的策略还没有详细研究,后续再分享),比如方案2里的find查询,执行计划里包含了 {replanned: 1} 说明是重新构建了执行计划;当它发现这个执行计划实际执行起来效果更差时,最终还是会会到更优的执行计划上。 最懂数据的还是业务自身,对于查询优化器搞不定的case,可以通过在查询时加 hint,自己指定的索引来构建执行计划。
线上某 MongoDB 复制集实例(包含 Primary、Secondary、Hidden 3个节点 ),Primary 节点突然 IOPS 很高,调查后发现,其中 Hidden 处于 RECOVERING 状态,同时 Priamry 上持续有一全表扫描 oplog 的操作,正是这个 oplog 的 COLLSCAN 导致IO很高。 2017-10-23T17:48:01.845+0800 I COMMAND [conn8766752] query local.oplog.rs query: { ts: { $gte: Timestamp 1505624058000|95, $lte: Timestamp 1505624058000|95 } } planSummary: COLLSCAN cursorid:20808023597 ntoreturn:0 ntoskip:0 keysExamined:0 docsExamined:44669401 keyUpdates:0 writeConflicts:0 numYields:353599 nreturned:0 reslen:20 locks:{ Global: { acquireCount: { r: 707200 } }, Database: { acquireount: { r: 353600 }, acquireWaitCount: { r: 15 }, timeAcquiringMicros: { r: 3667 } }, oplog: { acquireCount: { r: 353600 } } } 935646ms 上述问题,初步一看有2个疑问 Hidden 上最新的 oplog 在 Primary 节点上是存在的,为什么 Hidden 会一直处于 RECOVERING 状态无法恢复? 同步拉取 oplog 时,会走 oplogHack 的路径,即快速根据oplog上次同步的位点定位到指点位置,这里会走一个二分查找,而不是COLLSCAN,然后从这个位点不断的tail oplog。既然有了这个优化,为什么会出现扫描所有的记录? 接下里将结合 MongoDB 同步的细节实现来分析下上述问题产生的原因。 备如何选择同步源? MongoDB 复制集使用 oplog 来做主备同步,主将操作日志写入 oplog 集合,备从 oplog 集合不断拉取并重放,来保持主备间数据一致。MongoDB 里的 oplog 特殊集合拥有如下特性: 每条 oplog 都包含时间戳,按插入顺序递增,如果底层使用的KV存储引擎,这个时间戳将作为 oplog 在KV引擎里存储的key,可以理解为 oplog 在底层存储就是按时间戳顺序存储的,在底层能快速根据ts找位置。 oplog 集合没有索引,它一般的使用模式是,备根据自己已经同步的时间戳,来定位到一个位置,然后从这个位置不断 tail query oplog。针对这种应用模式,对于 local.oplog.rs.find({ts: {$gte: lastFetechOplogTs}}) 这样的请求,会有特殊的oplogStartHack 的优化,先根据gte的查询条件在底层引擎快速找到起始位置,然后从该位置继续 COLLSCAN。 oplog 是一个 capped collection,即固定大小集合(默认为磁盘大小5%),当集合满了时,会将最老插入的数据删除。 选择同步源,条件1:备上最新的oplog时间戳 >= 同步源上最旧的oplog时间戳 备在选择同步源时,会根据 oplog 作为依据,如果自己最新的oplog,比同步源上最老的 oplog 还有旧,比如 secondaryNewest < PrimaryOldest,则不能选择 Primary 作为同步源,因为oplog不能衔接上。如上图,Secondary1 可以选择 Primary 作为同步源,Secondary2 不能选择 Primary作为同步源,但可以选择 Secondary1 作为同步源。 如果所有节点都不满足上述条件,即认为找不到同步源,则节点会一直处于 RECOVERING 状态,并会打印 too stale to catch up -- entering maintenance mode 之类的日志,此时这个节点就只能重新全量同步了(向该节点发送 resync 命令即可)。 选择同步源,条件2:如果minvalid处于不一致状态,则minvalid里的时间戳在同步源上必须存在 local.replset.minvalid(后简称minvalid)是 MongoDB 里的一个特殊集合,用于存储节点同步的一致时间点,在备重放oplog、回滚数据的时候都会用到,正常情况下,这个集合里包含一个ts字段,跟最新的oplog时间戳一致,即 { ts: lastOplogTimestamp }。 当备拉取到一批 oplog 后,假设第一条和最后一条 oplog 的时间戳分别为 firstOplogTimestamp、lastOplogTimestamp,则备在重放之前,会先把 minvalid 更新为 { ts: lastOplogTimestamp, begin: firstOplogTimestamp},加了begin字段后就说明,当前处于一个不一致的状态,等一批 oplog 全部重放完,备将 oplog 写到本地,然后更新 minvalid 为{ ts: lastOplogTimestamp},此时又达到一致的状态。 节点在ROLLBACK时,会将 minvalid 先更新为{ ts: lastOplogTimestampInSyncSource, begin: rollbackCommonPoint},标记为不一致的状态,直到继续同步后才会恢复为一致的状态。比如 主节点 A B C F G H 备节点1 A B C F G 备节点2 A B C D E 备节点就需要回滚到 CommonPoint C,如果根据主来回滚,则minvalid会被更新为 { ts: H, begin:C}` 在选择同步源时,如果 minvalid 里包含 begin 字段,则说明它上次处于一个不一致的状态,它必须先确认 ts 字段对应的时间戳(命名为 requiredOptime)在同步源上是否存在,主要目的是: 重放时,如果重放过程异常结束,重新去同步时,必须要找包含上次异常退出时oplog范围的节点来同步 ROLLBACK后选择同步源,必须选择包含ROLLBACK时参考节点对应的oplog范围的节点来同步;如上例,备节点2回滚时,它的参考节点包含了H,则在接下来选择同步源上,同步源一定要包含H才行。 为了确认 requireOptime 是否存在,备会发一个 ts: {$gte: requiredOptime, $lte: requiredOptime} 的请求来确认,这个请求会走到 oplogStartHack的路径,先走一次二分查找,如果能找到(绝大部分情况),皆大欢喜,如果找不到,就会引发一次 oplog 集合的全表扫描,如果oplog集合很大,这个开销非常大,而且会冲掉内存中的cache数据。 oplogStartHack 的本质 通过上面的分析发现,如果 requiredOptime 在同步源上不存在,会引发同步源上的一次oplog全表扫描,这个主要跟oplog hack的实现机制相关。 对于oplog的查找操作,如果其包含一个 ts: {$gte: beginTimestamp} 的条件,则 MongoDB 会走 oplogStartHack 的优化,先从引擎层获取到第一个满足查询条件的RecordId,然后把RecordId作为表扫描的参数。 如果底层引擎查找到了对应的点,oplogStartHack优化有效 如果底层引擎没有没有找到对应的点,RecordId会被设置为空值,对接下来的全表扫描不会有任何帮助。(注:个人认为,这里作为一个优化,应该将RecordId设置为Max,让接下里的全表扫描不发生。) if (查询满足oplogStartHack的条件) { startLoc = collection->getRecordStore()->oplogStartHack(txn, goal.getValue()); // 1. 将起始值传到底层引擎,通过二分查找找到起始值对应的RecordId // Build our collection scan... CollectionScanParams params; params.collection = collection; params.start = *startLoc; // 2. 将起始RecordId作为表扫描的参数 params.direction = CollectionScanParams::FORWARD; params.tailable = cq->getParsed().isTailable(); 结合上述分析,当一致时间点对应的oplog在同步源上找不到时,会在同步源上触发一次oplog的全表扫描。当主备之间频繁的切换(比如线上的这个实例因为写入负载调大,主备角色切换过很多次),会导致多次ROLLBACK发生,最后出现备上minvalid里的一致时间点在同步源上找不到,引发了oplog的全表扫描;即使发生全表扫描,因为不包含minvalid的oplog,备也不能选择这个节点当同步源,最后就是一直找不到同步源,处于RECOVERING状态无法恢复,然后不断重试,不断触发主上的oplog全表扫描,恶性循环。 如何避免上述问题? 上述问题一般很难遇到,而且只有oplog集合大的时候影响才会很恶劣。 终极方法还是从代码上修复,我们已经在阿里云MongoDB云数据库里修复这个问题,并会向官方提一个PR,在上述的场景不产生全表扫描,而是返回找不到记录。
MongoDB的用户在遇到性能问题时,经常会关注到 serverStatus.globalLock 指标,但对指标的含义不是很明确,本文会深入解释下 globalLock 指标的含义。 PRIMARY> db.serverStatus().globalLock "totalTime" : NumberLong("7069085891000"), "currentQueue" : { "total" : 0, "readers" : 0, "writers" : 0 "activeClients" : { "total" : 23, "readers" : 0, "writers" : 0 大家可以先看下官方文档 对globalLock的解释 (使用MongoDB遇到问题都请第一时间去查阅官方文档) ,如果中间分析部分的内容读起来有困难,可直接调至最后的总结部分。 globalLock A document that reports on the database’s lock state. Generally, the locks document provides more detailed data on lock uses. globalLock.totalTime The time, in microseconds, since the database last started and created the globalLock. This is roughly equivalent to total server uptime. globalLock.currentQueue A document that provides information concerning the number of operations queued because of a lock. globalLock.currentQueue.total The total number of operations queued waiting for the lock (i.e., the sum of globalLock.currentQueue.readers and globalLock.currentQueue.writers). A consistently small queue, particularly of shorter operations, should cause no concern. The globalLock.activeClients readers and writers information provides contenxt for this data. globalLock.currentQueue.readers The number of operations that are currently queued and waiting for the read lock. A consistently small read-queue, particularly of shorter operations, should cause no concern. globalLock.currentQueue.writers The number of operations that are currently queued and waiting for the write lock. A consistently small write-queue, particularly of shorter operations, is no cause for concern. globalLock.activeClients A document that provides information about the number of connected clients and the read and write operations performed by these clients. Use this data to provide context for the globalLock.currentQueue data. globalLock.activeClients.total The total number of active client connections to the database (i.e., the sum of globalLock.activeClients.readers and globalLock.activeClients.writers). globalLock.activeClients.readers The number of the active client connections performing read operations. globalLock.activeClients.writers The number of active client connections performing write operations. Client锁的状态 enum ClientState { // 枚举常量,标识Client的当前状态 kInactive, kActiveReader, kActiveWriter, kQueuedReader, kQueuedWriter }; Mongod上每个连接会对应一个Client对象,Client里包含当前锁的状态,初始为 kInactive,根据请求及并发状况的不同,会进入到其他的状态,核心逻辑在 lockGlobalBegin 里实现。 template <bool IsForMMAPV1> LockResult LockerImpl<IsForMMAPV1>::lockGlobalBegin(LockMode mode) { dassert(isLocked() == (_modeForTicket != MODE_NONE)); if (_modeForTicket == MODE_NONE) { const bool reader = isSharedLockMode(mode); auto holder = ticketHolders[mode]; if (holder) { _clientState.store(reader ? kQueuedReader : kQueuedWriter); holder->waitForTicket(); _clientState.store(reader ? kActiveReader : kActiveWriter); _modeForTicket = mode; const LockResult result = lockBegin(resourceIdGlobal, mode); if (result == LOCK_OK) return LOCK_OK; // Currently, deadlock detection does not happen inline with lock acquisition so the only // unsuccessful result that the lock manager would return is LOCK_WAITING. invariant(result == LOCK_WAITING); return result; 而 serverStatus.globalLock 其实根据这个锁的状态进行导出 2018-03-13 update 获取 Client 状态时,已经获取到 ticket 的 Reader/Writer 如果在等锁,也会认为是 Queued 状态,这个之前忽略了。 template <bool IsForMMAPV1> Locker::ClientState LockerImpl<IsForMMAPV1>::getClientState() const { auto state = _clientState.load(); if (state == kActiveReader && hasLockPending()) state = kQueuedReader; if (state == kActiveWriter && hasLockPending()) state = kQueuedWriter; return state; ret.append("totalTime", (long long)(1000 * (curTimeMillis64() - _started))); BSONObjBuilder currentQueueBuilder(ret.subobjStart("currentQueue")); currentQueueBuilder.append("total", clientStatusCounts[Locker::kQueuedReader] + clientStatusCounts[Locker::kQueuedWriter]); currentQueueBuilder.append("readers", clientStatusCounts[Locker::kQueuedReader]); currentQueueBuilder.append("writers", clientStatusCounts[Locker::kQueuedWriter]); currentQueueBuilder.done(); BSONObjBuilder activeClientsBuilder(ret.subobjStart("activeClients")); activeClientsBuilder.append("total", clientStatusCounts.sum()); activeClientsBuilder.append("readers", clientStatusCounts[Locker::kActiveReader]); activeClientsBuilder.append("writers", clientStatusCounts[Locker::kActiveWriter]); activeClientsBuilder.done(); globalLock.totalTime = 进程启动后经历的时间 globalLock.currentQueue.total = 下面2者之和 globalLock.currentQueue.readers = kQueuedReader 状态Client总数 globalLock.currentQueue.writers = kQueuedWriter 状态Client总数 globalLock.activerClients.totol = 下面2者之和 + 系统内部的一些Client(比如同步线程) globalLock.activerClients.readers = kActiveReader 状态Client总数 globalLock.activerClients.writers = kActiveWriter 状态Client总数 详解 globalLock 状态转换 为了方便后续介绍,先科普一下MongoDB的层次锁模型 * Lock modes. * Compatibility Matrix * Granted mode * ---------------.--------------------------------------------------------. * Requested Mode | MODE_NONE MODE_IS MODE_IX MODE_S MODE_X | * MODE_IS | + + + + - | * MODE_IX | + + + - - | * MODE_S | + + - + - | * MODE_X | + - - - - | MongoDB 加锁时,有四种模式【MODE_IS、MODE_IX、MODE_S、MODE_X】,MODE_S, MODE_X 很容易理解,分别是互斥读锁、互斥写锁,MODE_IS、MODE_IX是为了实现层次锁模型引入的,称为意向读锁、意向写锁,锁之间的竞争情况如上图所示。 MongoDB在加锁时,是一个层次性的管理方式,从 globalLock ==> DBLock ==> CollecitonLock ... ,比如我们都知道MongoDB wiredtiger是文档级别锁,那么读写并发时,加锁就类似如下 1. globalLock (这一层只关注是读还是写,不关注具体是什么LOCK) 2. DBLock MODE_IX 3. Colleciotn MODE_IX 4. pass request to wiredtiger 1. globalLock MODE_IS (这一层只关注是读还是写,不关注具体是什么LOCK) 2. DBLock MODE_IS 3. Colleciton MODE_IS 4. pass request to wiredtiger 根据上图的竞争情况,IS和IX是无需竞争的,所以读写请求可以在没有竞争的情况下,同时传到wiredtiger引擎去处理。 再举个栗子,如果一个前台建索引的操作跟一个读请求并发了 前台建索引操作 1. globalLock MODE_IX (这一层只关注是读还是写,不关注具体是什么LOCK) 2. DBLock MODE_X 3. pass to wiredtiger 1. globalLock MODE_IS (这一层只关注是读还是写,不关注具体是什么LOCK) 2. DBLock MODE_IS 3. Colleciton MODE_IS 4. pass request to wiredtiger 根据竞争表,MODE_X和MODE_IS是要竞争的,这也就是为什么前台建索引的过程中读是被阻塞的。 我们今天介绍的 globalLock 对应上述的第一步,在globalLock这一层,只关心是读锁、还是写锁,不关心是互斥锁还是意向锁,所以 globalLock 这一层是不存在竞争的。那么 globalLock 里的几个指标到底意味着什么? 从上述的代码可以发现,globalLockBegin里(基本所有的数据库读写请求都要走这个路径)决定了globalLock的状态转换,核心逻辑如下 template <bool IsForMMAPV1> LockResult LockerImpl<IsForMMAPV1>::lockGlobalBegin(LockMode mode) { const bool reader = isSharedLockMode(mode); auto holder = ticketHolders[mode]; if (holder) { _clientState.store(reader ? kQueuedReader : kQueuedWriter); holder->waitForTicket(); _clientState.store(reader ? kActiveReader : kActiveWriter); const LockResult result = lockBegin(resourceIdGlobal, mode); if (result == LOCK_OK) return LOCK_OK; 上述代码里,如果holder不为空,Client会先进去kQueuedReader或kQueuedWriter状态,然后获取一个ticket,获取到后转换为kActiveReader或kActiveWriter状态。这里的ticket是什么东西? 这里的ticket是引擎可以设置的一个限制。正常情况下,如果没有锁竞争,所有的读写请求都会被pass到引擎层,这样就有个问题,你请求到了引擎层面,还是得排队执行,而且不同引擎处理能力肯定也不同,于是引擎层就可以通过设置这个ticket,来限制一下传到引擎层面的最大并发数。比如 wiredtiger设置了读写ticket均为128,也就是说wiredtiger引擎层最多支持128的读写并发(这个值经过测试是非常合理的经验值,无需修改)。 mmapv1引擎并没有设置ticket的限制,也就是说用mmapv1引擎时,globalLock的currentQueue会一直是0. globalLock完成后,client就进入了kActiveReader或kActiveWriter中的一种状态,这个就对应了globalLock.activerClients字段里的指标,接下来才开始lockBegin,加DB、Collection等层次锁,更底层的锁竞争会间接影响到globalLock。 serverStatus.globalLock 或者 mongostat (qr|qw ar|aw指标)能查看mongod globalLock的各个指标情况。 Wiredtiger限制传递到引擎层面的最大读写并发数均为128(合理的经验值,通常无需调整),如果超过这个阈值,排队的请求就会体现在globalLock.currentQueue.readers/writers里。 如果globalLock.currentQueue.readers/writers个值长时间都不为0(此时globalLock.activeClients.readers/writers肯定是持续接近或等于128的),说明你的系统并发太高(或者有长时间占用互斥锁的请求比如前台建索引),可以通过优化单个请求的处理时间(比如建索引来减少COLLSCAN或SORT),或升级后端资源(内存、磁盘IO能力、CPU)来优化。 globalLock.activeClients.readers/writers 持续不为0(但没达到128,此时currentQueue为空),并且你觉得请求处理已经很慢了,这时也可以考虑2中提到的优化方法。
今天接到一个用户反馈的问题,sharding集群,使用wiredtiger引擎,某个DB下集合全部用的hash分片,show dbs 发现其中一个shard里该DB的大小,跟其他的集合差别很大,其他基本在60G左右,而这个shard在200G左右? 由于这个DB下有大量的集合及索引,一眼也看不出问题,写了个脚本分析了一下,得到如下结论 somedb 下所有集合都是hash分片,并且chunk的分布是比较均匀的 show dbs 反应的是集合及索引对应的物理文件大小 集合的数据在各个shard上逻辑总大小是接近的,只有shard0占用的物理空间比其他大很多 从shard0上能找到大量 moveChunk 的记录,猜测应该是集合的数据在没有开启分片的情况下写到shard0了,然后开启分片后,从shard0迁移到其他shard了,跟用户确认的确有一批集合是最开始没有分片。 所以这个问题就转换成了,为什么复制集里集合的逻辑空间与物理空间不一致?即collection stat 里 size 与 storageSize 的区别。 mymongo:PRIMARY> db.coll.stats() "ns" : "test.coll", "size" : 30526664, "count" : 500808, "avgObjSize" : 33, "storageSize" : 19521536, "capped" : false, 逻辑存储空间与物理存储空间有差距的主要原因 存储引擎存储时,需要记录一些额外的元数据信息,这会导致物理空间总和比逻辑空间略大 存储引擎可能支持数据压缩,逻辑的数据块存储到磁盘时,经过压缩可能比逻辑数据小很多了(具体要看数据的特性,极端情况下压缩后数据变大也是有可能的) 引擎对删除空间的处理,很多存储引擎在删除数据时,考虑到效率,都不会立即去挪动数据回收删除的存储空间,这样可能导致删除很多文档后,逻辑空间变小,但物理空间并没有变小。如下图所示,灰色的文档删除表示被删除。删除的空间产生很多存储碎片,这些碎片空间不会立即被回收,但有新文档写入时,可以立即被复用。 而上述case里,集合数据先分到一个shard,然后启用分片后,迁移一部分到其他shard,就是一个典型的产生大量存储碎片的例子。存储碎片对服务通常影响不大,但如果因为空间不够用了需要回收,如何去强制的回收这些碎片空间? 数据清理掉重新加入复制集同步数据,或者直接执行resync命令 (确保有还有其他的数据备份) 对集合调用 compact 命令 2017-08-03 15:42:04 update 关于 compact操作,有同学问道,问题链接 mongdb中由于删除了大量的数据,但是没有释放磁盘空间给系统,想通过compact命令来释放磁盘空间;但是对compact命令有几个疑问 compact命令在WiredTiger引擎上是库级别锁还是collection级别锁? 执行compact命令需要多大的空余磁盘空间呢 compact 加的是DB级别的互斥写锁,同一个DB上的读写都会被阻塞 compact基本不需要额外的空间,wiredtiger compact的原理是将数据不断往前面的空洞挪动,并不需要把数据存储到临时的位置(额外的存储空间)。 resync命令 compact命令 云数据库MongoDB版
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 MongoDB Sharding 关于 MongoDB sharding 的原理,如果不了解请先参考 关于MongoDB Sharding,你应该知道的 MongoDB Sharded cluster架构原理 注:本文的内容基于 mongoDB 3.2 版本。 Primary shard 使用 MongoDB sharding 后,数据会以 chunk 为单位(默认64MB)根据 shardKey 分散到后端1或多个 shard 上。 每个 database 会有一个 primary shard,在数据库创建时分配 database 下启用分片(即调用 shardCollection 命令)的集合,刚开始会生成一个[minKey, maxKey] 的 chunk,该 chunk 初始会存储在 primary shard 上,然后随着数据的写入,不断的发生 chunk 分裂及迁移,整个过程如下图所示。 database 下没有启用分片的集合,其所有数据都会存储到 primary shard 何时触发 chunk 分裂? mongos 上有个 sharding.autoSplit 的配置项,可用于控制是否自动触发 chunk 分裂,默认是开启的。如无专业人士指导,强烈建议不要关闭 autoSplit,更好的方式是使用「预分片」的方式来提前分裂,后面会详细介绍。 mongoDB 的自动 chunk 分裂只会发生在 mongos 写入数据时,当写入的数据超过一定量时,就会触发 chunk 的分裂,具体规则如下。 int ChunkManager::getCurrentDesiredChunkSize() const { // split faster in early chunks helps spread out an initial load better const int minChunkSize = 1 << 20; // 1 MBytes int splitThreshold = Chunk::MaxChunkSize; // default 64MB int nc = numChunks(); if (nc <= 1) { return 1024; } else if (nc < 3) { return minChunkSize / 2; } else if (nc < 10) { splitThreshold = max(splitThreshold / 4, minChunkSize); } else if (nc < 20) { splitThreshold = max(splitThreshold / 2, minChunkSize); return splitThreshold; bool Chunk::splitIfShould(OperationContext* txn, long dataWritten) const { dassert(ShouldAutoSplit); LastError::Disabled d(&LastError::get(cc())); try { _dataWritten += dataWritten; int splitThreshold = getManager()->getCurrentDesiredChunkSize(); if (_minIsInf() || _maxIsInf()) { splitThreshold = (int)((double)splitThreshold * .9); if (_dataWritten < splitThreshold / ChunkManager::SplitHeuristics::splitTestFactor) return false; if (!getManager()->_splitHeuristics._splitTickets.tryAcquire()) { LOG(1) << "won't auto split because not enough tickets: " << getManager()->getns(); return false; ...... chunkSize 为默认64MB是,分裂阈值如下 集合 chunk 数量 1024B [1, 3) 0.5MB [3, 10) [10, 20) [20, max) 写入数据时,当 chunk 上写入的数据量,超过分裂阈值时,就会触发 chunk 的分裂,chunk 分裂后,当出现各个 shard 上 chunk 分布不均衡时,就会触发 chunk 迁移。 何时触发 chunk 迁移? 默认情况下,MongoDB 会开启 balancer,在各个 shard 间迁移 chunk 来让各个 shard 间负载均衡。用户也可以手动的调用 moveChunk 命令在 shard 之间迁移数据。 Balancer 在工作时,会根据shard tag、集合的 chunk 数量、shard 间 chunk 数量差值 来决定是否需要迁移。 (1)根据 shard tag 迁移 MongoBD sharding 支持 shard tag 特性,用户可以给 shard 打上标签,然后给集合的某个range 打上标签,mongoDB 会通过 balancer 的数据迁移来保证「拥有 tag 的 range 会分配到具有相同 tag 的 shard 上」。 (2)根据 shard 间 chunk 数量迁移 int threshold = 8; if (balancedLastTime || distribution.totalChunks() < 20) threshold = 2; else if (distribution.totalChunks() < 80) threshold = 4; 集合 chunk 数量 [1, 20) [20, 80) [80, max) 针对所有启用分片的集合,如果 「拥有最多数量 chunk 的 shard」 与 「拥有最少数量 chunk 的 shard」 的差值超过某个阈值,就会触发 chunk 迁移; 有了这个机制,当用户调用 addShard 添加新的 shard,或者各个 shard 上数据写入不均衡时,balancer 就会自动来均衡数据。 (3)removeShard 触发迁移 还有一种情况会触发迁移,当用户调用 removeShard 命令从集群里移除shard时,Balancer 也会自动将这个 shard 负责的 chunk 迁移到其他节点,因 removeShard 过程比较复杂,这里先不做介绍,后续专门分析下 removeShard 的实现。 chunkSize 对分裂及迁移的影响 MongoDB 默认的 chunkSize 为64MB,如无特殊需求,建议保持默认值;chunkSize 会直接影响到 chunk 分裂、迁移的行为。 chunkSize 越小,chunk 分裂及迁移越多,数据分布越均衡;反之,chunkSize 越大,chunk 分裂及迁移会更少,但可能导致数据分布不均。 chunkSize 太小,容易出现 jumbo chunk(即shardKey 的某个取值出现频率很高,这些文档只能放到一个 chunk 里,无法再分裂)而无法迁移;chunkSize 越大,则可能出现 chunk 内文档数太多(chunk 内文档数不能超过 250000 )而无法迁移。 chunk 自动分裂只会在数据写入时触发,所以如果将 chunkSize 改小,系统需要一定的时间来将 chunk 分裂到指定的大小。 chunk 只会分裂,不会合并,所以即使将 chunkSize 改大,现有的 chunk 数量不会减少,但 chunk 大小会随着写入不断增长,直到达到目标大小。 如何减小分裂及迁移的影响? mongoDB sharding 运行过程中,自动的 chunk 分裂及迁移如果对服务产生了影响,可以考虑一下如下措施。 (1)预分片提前分裂 在使用 shardCollection 对集合进行分片时,如果使用 hash 分片,可以对集合进行「预分片」,直接创建出指定数量的 chunk,并打散分布到后端的各个 shard。 指定 numInitialChunks 参数在 shardCollection 指定初始化的分片数量,该值不能超过 8192。 Optional. Specifies the number of chunks to create initially when sharding an empty collection with a hashed shard key. MongoDB will then create and balance chunks across the cluster. The numInitialChunks must be less than 8192 per shard. If the collection is not empty, numInitialChunks has no effect. 如果使用 range 分片,因为 shardKey 的取值不确定,预分片意义不大,很容易出现部分 chunk 为空的情况,所以 range 分片只支持 hash 分片。 (2)合理配置 balancer monogDB 的 balancer 能支持非常灵活的配置策略)来适应各种需求 Balancer 能动态的开启、关闭 Blancer 能针对指定的集合来开启、关闭 Balancer 支持配置时间窗口,只在制定的时间段内进行迁移 Aliyun MongoDB sharding Manage Sharded Cluster Balancer shardCollection command Migration Thresholds shard tag 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
MongoDB上周五在北京DTCC分享了「32 Tips to Boost MongoDB Performance」,本文是分享的PPT以及重要内容的注解。 注解:本次分享主要「自底向上」的介绍提升 MongoDB 服务性能需要注意的问题,从硬件、操作系统、服务端一直到应用端,前面3个层次的建议主要面向DBA及运维人员,而最上层的应用开发建议主要面向开发者。 注解:了解一个数据库性能时,我们可能会从硬件、软件提供商、或技术同行那里获取到一些数据,但性能数据跟硬件配置、测试方法、环境、请求类型、数据集等都有很大的关联,在自己的环境里表现如何,建议通过benchmark实测一下,目前常用的mongoDB benchmark有 YCSB 以及 sysbench。 注解: 硬件选型方面,在不差钱的前提下肯定是越牛逼越好;对于大部分数据库应用来说,瓶颈可能最先出现在IO上,所以从机械硬盘到SSD的硬件提升通常是效果最明显的。 注解:数据库随机访问的模式较多,建议关闭THP、NUMA、readahead等特性,不排除这些特性可能在某些特定场景上能有性能提升,如果要开启请一定先做下对比测试。 注解:wiredtiger引擎在锁粒度、数据压缩上的支持远超mmapv1,从mmapv1升级到wiredtiger引擎,通常会带来存储成本的降低,以及性能的提升。 注解:生产环境建议一定使用3节点的MongoDB复制集,如果是写(尤其是更新、删除)密集型的应用,可以考虑讲oplog设置更大点(默认为磁盘空间5%)。 注解:mongoDB sharding 能实现数据库的水平扩展,但其相比复制集运维管理上更加复杂,建议只有在真正需要(扩展写入能力、扩展存储容量、降低当个分片故障时的影响)的时候才考虑使用sharding。 注解:选择shard key时,主要考虑key的「离散度」以及「频率」,离散度越高越好,能更好的分散数据;频率越低越好,避免出现热点;实际选择时,要结合查询需求来确定,最满足业务需求的才是最好的。 注解:sharding默认会自动在shard间进行数据迁移,如果迁移对线上访问有性能冲击,可以设置迁移窗口期,比如只在凌晨「1:00 - 6:00」来做数据迁移。 注解:慢请求对定位性能问题非常有帮助,建议线上业务都开启,并设置合理的阈值,默认为100ms。注解:监控对任何线上业务都必不可少,监控的信息能让你充分了解线上服务的运行状态。 注解:很多场景下,数据备份是最后一根救命稻草,有备无患,建议数据库一定做好备份。 注解: MongoDB Driver:使用正确的姿势连接复制集注解: MongoDB Driver:使用正确的姿势连接分片集群 注解:阿里云-MongoDB云数据库,了解详情 MongoDB 猛击这里下载PDF版本
最近好几个社区用户咨询,错误的执行了 dropDatabse 把数据库误删除了(或 dropCollection 误删集合),有什么方法能恢复数据?本文主要介绍几种可能有效的恢复方案。 方案1:通过备份集恢复 如果对 MongoDB 做了全量备份 + 增量备份,那么可以通过备份集及来恢复数据。备份可以是多种形式,比如 通过 mongodump 等工具,对数据库产生的逻辑备份 拷贝 dbpath 目录产生的物理备份 文件系统、卷管理等产生的快照等 从这里其实也可以看出一个问题,就是「部署了多节点的复制集,为什么还需要做数据备份?」;遇到误删数据库这种问题,dropDatabase 命令也会同步到所有的备节点,导致所有节点的数据都被删除。 方案2:通过 oplog 恢复 如果部署的是 MongoDB 复制集,这时还有一线希望,可以通过 oplog 来尽可能的恢复数据;MongoDB 复制集的每一条修改操作都会记录一条 oplog,所以当数据库被误删后,可以通过重放现有的oplog来「尽可能的恢复数据」。前不久遇到的一个用户,运气非常好,数据库是最近才创建的,所有的操作都还保留在oplog里,所以用户通过oplog把所有误删的数据都找回了。 通过 oplog 恢复数据的流程非常简单,只需要把oplog集合通过mongodump导出,然后通过mongorestore 的 oplogReplay 模式重放一下。 Step1: 导出 oplog 集合 mongodump -d local -c oplog.rs -d -o backupdir Step2: 拷贝oplog集合的数据 mkdir new_backupdir cp backupdir/local/oplog.rs.bson new_backupdir/oplog.bson Step3: 重放oplog mongorestore --oplogReplay new_backupdir 方案3:通过分析数据文件恢复 MongoDB 以 bson 的格式存储数据,所以只要 dropDatbase 或 dropCollection 后ß,对应的物理数据没有从磁盘删除,就有希望恢复,但从 MongoDB 引擎的特性看,能恢复的可能性较小。 mmapv1 wiredTiger dropDatabase 数据文件立即会被删除 数据文件立即会被删除 dropCollection 不会立即从磁盘删除,空间会被复用 数据文件立即会被删除 从上表的描述可以看出,如果使用 mmapv1 存储引擎,dropCollection 是不会立即删除数据文件的,这种情况下,可通过分析数据文件李的bson文档来恢复数据;而其他场景的误删,数据文件会立即从磁盘删除,无法通过这种方法恢复。 最后,强烈建议大家在使用 MonogDB 数据库存储重要数据时,一定要部署复制集,并做数据备份。通常2类用户不做数据备份 没爱过;使用 MongoDB 存储不重要的数据,丢了也无所谓;(但即使是这样,实际数据被误删时,用户还是想尽可能的恢复数据,而不是丢了「无所谓」) 爱过,但伤得不够深;使用 MongoDB 存储了重要的数据,但从未出过问题,于是抱着侥幸心理不对数据进行备份。不要等待受伤了再做备份,有备无患。 之前做过一个 MongoDB 数据备份的技术分享,介绍了阿里云 MongoDB 云数据库的备份恢复方案,能实现MongoDB复制集、MongoDB Sharding 恢复到任意时间点,有兴趣的同学可以参考下,MongoDB秒级备份恢复(SDCC上海站数据库核心技术与应用实战峰会分享PPT) MongoDB Backup Methods MongoDB 云数据库 MongoDB Sharding
本文是我前同事付秋雷最近遇到到一个关于MongoDB执行计划选择的问题,非常有意思,在探索源码之后,他将整个问题搞明白并整理分享出来。付秋雷(他的博客)曾是 Tair(阿里内部用得非常广泛的KV存储系统)的核心开发成员,目前就职于蘑菇街。 苏先生反馈线上某条查询很慢(10+ seconds),语句相当于 db.myColl.find({app:"my_app",requestTime:{$gte:1492502247000,$lt:1492588800000}}).sort({_id:-1}).limit(1) myColl这个collection中的记录内容类似于: { "_id" : ObjectId("58fd895359cb8757d493ce60"), "app" : "my_app", "eventId" : 141761066, "requestTime" : NumberLong("1493010771753"), "scene" : "scene01" } { "_id" : ObjectId("58fd895359cb8757d493ce52"), "app" : "my_app", "eventId" : 141761052, "requestTime" : NumberLong("1493010771528"), "scene" : "scene02" } { "_id" : ObjectId("58fd895359cb8757d493ce36"), "app" : "my_app", "eventId" : 141761024, "requestTime" : NumberLong("1493010771348"), "scene" : "scene03" } { "_id" : ObjectId("58fd895359cb8757d493ce31"), "app" : "my_app", "eventId" : 141761019, "requestTime" : NumberLong("1493010771303"), "scene" : "scene01" } { "_id" : ObjectId("58fd895359cb8757d493ce2d"), "app" : "my_app", "eventId" : 141761015, "requestTime" : NumberLong("1493010771257"), "scene" : "scene01" } { "_id" : ObjectId("58fd895259cb8757d493ce10"), "app" : "my_app", "eventId" : 141760986, "requestTime" : NumberLong("1493010770866"), "scene" : "scene01" } { "_id" : ObjectId("58fd895259cb8757d493ce09"), "app" : "my_app", "eventId" : 141760979, "requestTime" : NumberLong("1493010770757"), "scene" : "scene01" } { "_id" : ObjectId("58fd895259cb8757d493ce02"), "app" : "my_app", "eventId" : 141760972, "requestTime" : NumberLong("1493010770614"), "scene" : "scene03" } { "_id" : ObjectId("58fd895259cb8757d493cdf1"), "app" : "my_app", "eventId" : 141760957, "requestTime" : NumberLong("1493010770342"), "scene" : "scene02" } { "_id" : ObjectId("58fd895259cb8757d493cde6"), "app" : "my_app", "eventId" : 141760946, "requestTime" : NumberLong("1493010770258"), "scene" : "scene01" } 相关的索引有: "v" : 1, "key" : { "_id" : 1 "name" : "_id_", "ns" : "myDatabase.myColl" "v" : 1, "key" : { "responseTime" : -1 "name" : "idx_responseTime_-1", "ns" : "myDatabase.myColl" "v" : 1, "key" : { "app" : 1, "scene" : 1, "eventId" : -1, "requestTime" : -1 "name" : "idx_app_1_scene_1_eventId_-1_requestTime_-1", "ns" : "myDatabase.myColl" 慢查询就是在myColl中查找符合[1492502247000, 1492588800000)这个时间范围的所有记录,以下描述中称这条查询为bad query。 如果去掉$lt:1492588800000这个约束条件,查找[1492502247000, +∞)这个时间范围,就会很快(milliseconds)。 db.myColl.find({app:"my_app",requestTime:{$gte:1492502247000}}).sort({_id:-1}).limit(1) 以下描述中称这条查询为good query。 问题来了: [问题A] 这两条查询都是走的什么索引呢?导致执行时间相差如此之大 [问题B] 如果两条查询选取的索引不同,为什么会有这个不同呢,这两条查询长得还是挺像的 [问题C] 如果bad query选取和good query一样的索引,是否还会有一样的问题呢 这两条查询都是走的什么索引呢?导致执行时间相差如此之大 和Mysql一样,Mongodb也提供了explain语句,可以获取query语句的查询计划(queryPlanner)、以及执行过程中的统计信息(executionStats)。 违和发散:Cassandra中也是有类似的功能,Hbase中目前是没有看到的。 在mongo shell中的使用方法是在query语句后面加上.explain('executionStats'),对于上面的good query,对应的explain语句为: db.myColl.find({app:"my_app",requestTime:{$gte:1492502247000}}).sort({_id:-1}).limit(1).explain('executionStats') good query的explain语句的执行结果如下,无关细节用...省略: "queryPlanner" : { "plannerVersion" : 1, "namespace" : "myDatabase.myColl", "indexFilterSet" : false, "parsedQuery" : ... "winningPlan" : { "stage" : "LIMIT", "limitAmount" : 1, "inputStage" : { "stage" : "FETCH", "filter" : ..., "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "_id" : 1 "indexName" : "_id_", "direction" : "backward", "indexBounds" : { "_id" : [ "[MaxKey, MinKey]" "rejectedPlans" : ..., "executionStats" : { "executionSuccess" : true, "nReturned" : 1, "executionTimeMillis" : 0, "totalKeysExamined" : 8, "totalDocsExamined" : 8, "executionStages" : { "stage" : "LIMIT", "inputStage" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "direction" : "backward", "indexBounds" : { "_id" : [ "[MaxKey, MinKey]" "keysExamined" : 8, "serverInfo" : ..., "ok" : 1 结果分为四部分:queryPlanner、executionStats、serverInfo、ok,仅关注queryPlanner、executionStats这两部分。 executionStats就是执行queryPlanner.winningPlan这个计划时的统计信息,可以从indexBounds看到good query在索引扫描(IXSCAN)阶段,使用的索引是_id主键索引。从IXSCAN这个阶段的keysExamined统计可以解释为什么good query执行的这么快,只扫描了8条数据。 同样使用explain语句看看bad query使用的是什么索引: "queryPlanner" : { "winningPlan" : { "stage" : "SORT", "inputStage" : { "stage" : "SORT_KEY_GENERATOR", "inputStage" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "app" : 1, "scene" : 1, "eventId" : -1, "requestTime" : -1 "indexName" : "idx_app_1_scene_1_eventId_-1_requestTime_-1", "direction" : "forward", "indexBounds" : { "app" : [ "[\"my_app\", \"my_app\"]" "scene" : [ "[MinKey, MaxKey]" "eventId" : [ "[MaxKey, MinKey]" "requestTime" : [ "(1492588800000.0, 1492502247000.0]" "rejectedPlans" : ..., "executionStats" : { "executionSuccess" : true, "nReturned" : 1, "executionTimeMillis" : 56414, "totalKeysExamined" : 3124535, "totalDocsExamined" : 275157, "executionStages" : { "stage" : "SORT", "inputStage" : { "stage" : "SORT_KEY_GENERATOR", "inputStage" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "direction" : "forward", "indexBounds" : { "app" : [ "[\"my_app\", \"my_app\"]" "scene" : [ "[MinKey, MaxKey]" "eventId" : [ "[MaxKey, MinKey]" "requestTime" : [ "(1492588800000.0, 1492502247000.0]" "keysExamined" : 3124535, "serverInfo" : ..., "ok" : 1 可以看到bad query使用的索引是一个复合索引(Compound Indexes),确实和good query使用的索引不一样。同样,从IXSCAN这个阶段的keysExamined统计可以看到扫描了3124535条数据,所以执行时间会很长。 如果两条查询选取的索引不同,为什么会有这个不同呢,这两条查询长得还是挺像的 Mongodb是如何为查询选取认为合适的索引的呢? 粗略来说,会先选几个候选的查询计划,然后会为这些查询计划按照某个规则来打分,分数最高的查询计划就是合适的查询计划,这个查询计划里面使用的索引就是认为合适的索引。 好,粗略地说完了,现在细致一点说(还是那句话:没有代码的解释都是耍流氓,以下所有的代码都是基于mongodb-3.2.10)。 先看一个栈: mongo::PlanRanker::scoreTree mongo::PlanRanker::pickBestPlan mongo::MultiPlanStage::pickBestPlan mongo::PlanExecutor::pickBestPlan mongo::PlanExecutor::make mongo::PlanExecutor::make mongo::getExecutor mongo::getExecutorFind mongo::FindCmd::explain 这是使用lldb来调试mongod时,在mongo::PlanRanker::scoreTree(代码位于src/mongo/db/query/plan_ranker.cpp)处设置断点打印出来的栈。 scoreTree里面就是计算每个查询计划的得分的: // We start all scores at 1. Our "no plan selected" score is 0 and we want all plans to // be greater than that. double baseScore = 1; // How many "units of work" did the plan perform. Each call to work(...) // counts as one unit. size_t workUnits = stats->common.works; // How much did a plan produce? // Range: [0, 1] double productivity = static_cast<double>(stats->common.advanced) / static_cast<double>(workUnits); double tieBreakers = noFetchBonus + noSortBonus + noIxisectBonus; double score = baseScore + productivity + tieBreakers; scoreTree并没有执行查询,只是根据已有的PlanStageStats* stats来进行计算。那么,是什么时候执行查询来获取查询计划的PlanStageStats* stats的呢? 在mongo::MultiPlanStage::pickBestPlan(代码位于src/mongo/db/exec/multi_plan.cpp)中,会调用workAllPlans来执行所有的查询计划,最多会调用numWorks次: size_t numWorks = getTrialPeriodWorks(getOpCtx(), _collection); size_t numResults = getTrialPeriodNumToReturn(*_query); // Work the plans, stopping when a plan hits EOF or returns some // fixed number of results. for (size_t ix = 0; ix < numWorks; ++ix) { bool moreToDo = workAllPlans(numResults, yieldPolicy); if (!moreToDo) { break; 如果bad query选取和good query一样的索引,是否还会有一样的问题呢 Mongodb查询时,可以借助于hint命令强制选取某一条索引来进行查询,比如上述的bad query加上.hint({_id:1}),就可以强制使用主键索引: db.myColl.find({app:"my_app",requestTime:{$gte:1492502247000,$lt:1492588800000}}).sort({_id:-1}).limit(1).hint({_id:1}) 然而,即使是这样,查询还是很慢,依然加上.explain('executionStats')看一下执行情况,解答问题A时已经对explain的结果做了些解释,所以这次着重看IXSCAN阶段的keysExamined: "executionStages" : { "stage" : "LIMIT", "inputStage" : { "stage" : "FETCH", "filter" : { "$and" : [ "app" : { "$eq" : "my_app" "requestTime" : { "$lt" : 1492588800000 "requestTime" : { "$gte" : 1492502247000 "nReturned" : 1, "inputStage" : { "stage" : "IXSCAN", "nReturned" : 32862524, "keysExamined" : 32862524, 扫描了32862524条记录,依然很慢。这个现象比较好解释了,从executionStats.executionStages可以看到,加了hint的查询经历了LIMIT => FETCH => IXSCAN 这几个阶段,IXSCAN这个阶段返回了32862524条记录,被FETCH阶段过滤只剩下一条,所以有32862523条无效扫描,为什么会有这么多无效扫描呢? 这个和业务逻辑是相关的,requestTime时间戳是随时间增长的,主键_id也可以认为随时间增长的,所以按照主键索引倒序来,最开始被扫描的是最新的记录,最新的记录是满足"requestTime" : {"$gte" : 1492502247000}这个条件的,所以good query只需要满足"app" : {"$eq" : "my_app"}就会很快返回; 然而bad query的约束条件"requestTime" : {"$gte" : 1492502247000, "$lt" : 1492588800000}中的"$lt" : 1492588800000是无法被满足的,必须要把所有比1492588800000这个时间戳新的记录都扫描完了之后才会返回。 苏先生提出了完美的解决方案:不使用_id来排序,而是使用request_time来进行排序。这样就会使用"requestTime" : -1这条索引,只需要进行"app" : {"$eq" : "my_app"}的过滤,也是milliseconds时间内完成查询。 搭建有效的线下调试环境是重现、解决问题的重要手段,例如之前重现zk问题时使用salt快速搭建本地集群 维护开源产品不了解源码,或者没有找到看的有效入口,是很被动的,缺少定位解决问题的根本手段 https://docs.mongodb.com/manual/ http://www.cnblogs.com/xjk15082/archive/2011/09/18/2180792.html https://lldb.llvm.org/lldb-gdb.html https://github.com/mongodb/mongo/wiki/Build-Mongodb-From-Source 感谢林青大神在排查过程中提供的关键帮助。
经常有用户咨询「MongoDB CPU 利用率很高,都快跑满了」,应该怎么办? 遇到这个问题,99.9999% 的可能性是「用户使用上不合理导致」,本文主要介绍从应用的角度如何排查 MongoDB CPU 利用率高的问题 Step1: 分析数据库正在执行的请求 用户可以通过 Mongo Shell 连接,并执行 db.currentOp() 命令,能看到数据库当前正在执行的操作,如下是该命令的一个输出示例,标识一个正在执行的操作。重点关注几个字段 client:请求是由哪个客户端发起的? opid:操作的opid,有需要的话,可以通过 db.killOp(opid) 直接干掉的操作 secs_running/microsecs_running: 这个值重点关注,代表请求运行的时间,如果这个值特别大,就得注意了,看看请求是否合理 query/ns: 这个能看出是对哪个集合正在执行什么操作 lock*:还有一些跟锁相关的参数,需要了解可以看官网文档,本文不做详细介绍 db.currentOp 文档在这里,多看官网文档 "desc" : "conn632530", "threadId" : "140298196924160", "connectionId" : 632530, "client" : "11.192.159.236:57052", "active" : true, "opid" : 1008837885, "secs_running" : 0, "microsecs_running" : NumberLong(70), "op" : "update", "ns" : "mygame.players", "query" : { "uid" : NumberLong(31577677) "numYields" : 0, "locks" : { "Global" : "w", "Database" : "w", "Collection" : "w" 这里先要明确一下,通过 db.currentOp() 查看正在执行的操作,目的到底是什么? 并不是说我们要将正在执行的操作都列出来,然后通过 killOp 逐个干掉;这一步的目的是要看一下,是否有「意料之外」的耗时请求正在执行。 比如你的业务平时 CPU 利用率不高,运维管理人员连到数据库执行了一些需要全表扫描的操作,然后突然 CPU 利用率飙高,导致你的业务响应很慢,那么就要重点关注下那些执行时间很长的操作。 一旦找到罪魁祸首,拿到对应请求的 opid,执行 db.killOp(opid) 将对应的请求干掉。 如果你的应用一上线,cpu利用率就很高,而且一直持续,通过 db.currentOp 的结果也没发现什么异常请求,可以进入到 Step2 进行更深入的分析。 Step2:分析数据库慢请求 MongoDB 支持 profiling 功能,将请求的执行情况记录到同DB下的 system.profile 集合里,profiling 有3种模式 profiling 设置文档在这里,多看官网文档 关闭 profiling 针对所有请求开启 profiling,将所有请求的执行都记录到 system.profile 集合 针对慢请求 profiling,将超过一定阈值的请求,记录到system.profile 集合 默认请求下,MongoDB 的 profiling 功能是关闭,生产环境建议开启,慢请求阈值可根据需要定制,如不确定,直接使用默认值100ms。 operationProfiling: mode: slowOp slowOpThresholdMs: 100 基于上述配置,MongoDB 会将超过 100ms 的请求记录到对应DB 的 system.profile 集合里,system.profile 默认是一个最多占用 1MB 空间的 capped collection。 查看最近3条 慢请求,{$natrual: -1} 代表按插入数序逆序 db.system.profile.find().sort({$natrual: -1}).limit(3) 在开启了慢请求 profiling 的情况下(MongoDB 云数据库是默认开启慢请求 profiling的),我们对慢请求的内容进行分析,来找出可优化的点,常见的包括。 profiling的结果输出含义在这里,多看官网文档 CPU杀手1:全表扫描 全集合(表)扫描 COLLSCAN,当一个查询(或更新、删除)请求需要全表扫描时,是非常耗CPU资源的,所以当你在 system.profile 集合 或者 日志文件发现 COLLSCAN 关键字时,就得注意了,很可能就是这些查询吃掉了你的 CPU 资源;确认一下,如果这种请求比较频繁,最好是针对查询的字段建立索引来优化。 一个查询扫描了多少文档,可查看 system.profile 里的 docsExamined 的值,该值越大,请求CPU开销越大。 关键字:COLLSCAN、 docsExamined CPU杀手2:不合理的索引 有的时候,请求即使查询走了索引,执行也很慢,通常是因为索引建立不太合理(或者是匹配的结果本身就很多,这样即使走索引,请求开销也不会优化很多)。 如下所示,假设某个集合的数据,x字段的取值很少(假设只有1、2),而y字段的取值很丰富。 { x: 1, y: 1 } { x: 1, y: 2 } { x: 1, y: 3 } ...... { x: 1, y: 100000} { x: 2, y: 1 } { x: 2, y: 2 } { x: 2, y: 3 } ...... { x: 1, y: 100000} 要服务 {x: 1: y: 2} 这样的查询 db.createIndex( {x: 1} ) 效果不好,因为x相同取值太多 db.createIndex( {x: 1, y: 1} ) 效果不好,因为x相同取值太多 db.createIndex( {y: 1 } ) 效果好,因为y相同取值很少 db.createIndex( {y: 1, x: 1 } ) 效果好,因为y相同取值少 至于{y: 1} 与 {y: 1, x: 1} 的区别,可参考MongoDB索引原理 及 复合索引官方文档 自行理解。 一个走索引的查询,扫描了多少条索引,可查看 system.profile 里的 keysExamined 字段,该值越大,CPU 开销越大。 关键字:IXSCAN、keysExamined CPU杀手3:大量数据排序 当查询请求里包含排序的时候,如果排序无法通过索引满足,MongoDB 会在内存李结果进行排序,而排序这个动作本身是非常耗 CPU 资源的,优化的方法仍然是建立索引,对经常需要排序的字段,建立索引。 当你在 system.profile 集合 或者 日志文件发现 SORT 关键字时,就可以考虑通过索引来优化排序。当请求包含排序阶段时, system.profile 里的 hasSortStage 字段会为 true。 关键字:SORT、hasSortStage 其他还有诸如建索引,aggregationv等操作也可能非常耗 CPU 资源,但本质上也是上述几种场景;建索引需要全表扫描,而vaggeregation 也是遍历、查询、更新、排序等动作的组合。 Step3: 服务能力评估 经过上述2步,你发现整个数据库的查询非常合理,所有的请求都是高效的走了索引,基本没有优化的空间了,那么很可能是你机器的服务能力已经达到上限了,应该升级配置了(或者通过 sharding 扩展)。 当然最好的情况时,提前对 MongoDB 进行测试,了解在你的场景下,对应的服务能力上限,以便及时扩容、升级,而不是到 CPU 资源用满,业务已经完全撑不住的时候才去做评估。
近日有 MongoDB 用户遇到一个问题,使用 Wiredtiger 存储引擎的 MongoDB 无法启动,咨询我数据能否恢复回来,能恢复多少是多少 ... 问题出现的场景据用户描述是「mongod磁盘写满了,导致进程 crash」,尝试重新启动,结果 wiredtiger 报错,错误信息类似如下,类似的问题 mongodb jira 上也有人提过,可以参考 SERVER-26924,说明此时 MongoDB 数据文件已经损坏。 2017-03-28T22:06:05.315-0500 W - [initandlisten] Detected unclean shutdown - /data/mongodb/mongod.lock is not empty. 2017-03-28T22:06:05.315-0500 W STORAGE [initandlisten] Recovering data from the last clean checkpoint. 2017-03-28T22:06:05.324-0500 I STORAGE [initandlisten] wiredtiger_open config: create,cache_size=13G,session_max=20000,eviction=(threads_max=4),statistics=(fast),log=(enabled=true,archive=true ,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0), 2017-03-28T22:06:05.725-0500 E STORAGE [initandlisten] WiredTiger (0) [1454119565:724960][1745:0x7f2ac9534bc0], file:WiredTiger.wt, cursor.next: read checksum error for 4096B block at offset 6 799360: block header checksum of 1769173605 doesn't match expected checksum of 4176084783 2017-03-28T22:06:05.725-0500 E STORAGE [initandlisten] WiredTiger (0) [1454119565:725067][1745:0x7f2ac9534bc0], file:WiredTiger.wt, cursor.next: WiredTiger.wt: encountered an illegal file form at or internal value 2017-03-28T22:06:05.725-0500 E STORAGE [initandlisten] WiredTiger (-31804) [1454119565:725088][1745:0x7f2ac9534bc0], file:WiredTiger.wt, cursor.next: the process must exit and restart: WT_PANI C: WiredTiger library panic 2017-03-28T22:06:05.725-0500 I - [initandlisten] Fatal Assertion 28558 MongoDB 3.2及以后的版本已经很少会出现这样的问题,至少从我接触 MongoDB 到现在还没实际遇到过这个问题,不过既然问题已经发生,我们来看看遇到这种情况应该怎么恢复数据? 如何恢复 MongoDB 数据? 第一招: 从复制集其他节点同步数据 MongoDB 通过复制集能保证高可靠的数据存储,通常生产环境建议使用「3节点复制集」,这样即使其中一个节点崩溃了无法启动,我们可以直接将其数据清掉,重新启动后,以全新的 Secondary 节点加入复制集,它会自动的同步数据,这样也就达到了恢复数据的目的。 然而不幸的是,该用户的 MongoDB 实例 只部署了一个节点 ... 我只能呵呵了 ... 第二招:从最近的一个备份集恢复数据 有的时候可能出现一些极端的case,比如遇到自然灾害,复制集所有节点都挂了(或者像上面的用户这样,你的复制集只部署一个节点...),这时第一招就没法用了。 此时,如果靠谱的你刚好对数据做了备份,此时就排上用场了,比如你每天对 MongoDB 做一次全量备份,那么你就可以把数据恢复到最近一天的数据了;如果你更靠谱的还对数据做了增量本分,能恢复的数据就更多了。 但是可想而知,这个用户既然能部署一个「只有单个节点的复制集」,肯定也不会想到去对数据库进行备份了 ... 第三招: repair 模式启动 MongoDB 当 MongoDB 无法启动时,通常是因为数据文件出现了不一致,mongod 支持以 repair 的模式启动,mongod 会尽可能的尝试自己去修复数据的不一致状态,修复过程中尽可能多的保留有效的数据。 但 repair 也不是对所有的场景都有效,repair 会先加载 MongoDB 所有的集合信息,然后针对每个集合来 repair,如果存储元数据的数据文件损坏,repair 也是没法工作的。 mongod --repair // 用户尝试按这种方式启动,仍然报相同的错误 第四招:使用 wireditger 工具恢复 以上3招都不行,我的第一想法就是通过 wiredtiger 的 salvage 功能去尽可能的恢复数据(salvage 可翻译为数据打捞,即针对一个wt的数据文件,尽可能多的从中提取有效的数据),本来是想写个工具来做这个事情。不过调研了一下发现 1. repair 模式启动,实现时也是调用的 wiredtiger 的 salvage 接口实现。 2. wireditger 自带的一个命令行工具 wt,包含了 salvage 的功能。 3. 找到一篇使用 wt 工具恢复 MongoDB 数据的文章,写的非常赞。 网友总结的使用 wiredtiger 工具 wt 恢复数据的方法原理很简单,就是通过恢复 wiredtiger 数据文件来恢复MongoDB数据,我实验了一下,的确可行,而且原文的步骤介绍已经非常详细,这里就不再赘述。需要注意的是 MongoDB 3.2 最新版本已经是了 wiredtiger 2.8,所以编译 wt 工具时,可以下载 2.8 版本的 wiredtiger 源代码。 MongoDB 默认会对集合数据进行 snappy 压缩,所以一定要确保 snappy 正确安装,在执行 wt 工具时,通过扩展的形式加载 snappy lib,否则运行时会报错。 如果需要恢复的集合很多,本文的方法效率是很低的。 第五招:从文件里提取bson文档来恢复 MongoDB json格式的文档,最终是以 BSON (Binary json) 格式持久化存储。 假设我们有个工具叫 bsonextract(有兴趣的同学可以尝试实现下贡献到社区里,直接调 BSON 的接口,实现起来不难),它能从一个数据文件里分析并提取出所有 BSON 格式的内容,那么我们也就达到了恢复数据的目的。 分析时,一段数据满足2个条件,我们即可认为是一个合法的 MongoDB 文档 这段数据是一个合法的 BSON 文档 包含一个 id 字段 (oplog 集合不包含 id 字段,但通常也没有去恢复 oplog 的必要) 上面这个方法不仅只能恢复 wiredtiger 的数据,对 MongoDB 所有存储引擎都有效。 最后,issue SERVER-19815 里介绍了 MongoDB 一直在优化 MongoDB ,让它能在 repair 模式里自动处理各种数据文件损坏(或部分丢失)的场景,目标就是万一遇到数据集损坏的场景,repair都能自动修复掉。 下面是 repair 以后能自动处理的一些场景及处理方法 Database files missing An entry for a file will exist in the catalogue, but on disk file is gone Will be impossible to recover from, remove the entry from the catalogue Warn the user strongly about this (Error message) Database files corrupted An entry for a file will exist in the catalogue, but on disk file is unable to be opened Attempt to rename the collection with WiredTiger to a new table that has some mention of it being corrupted in the name Re-create the same collection with the same name (in order to continue repair) Warn the user strongly about this problem, the creation of the new collection Index files missing An entry will exist in the catalogue, but on disk file is gone Build the index as part of repair Index files corrupted An entry will exist in the catalogue, but on disk file is unable to be opened Drop, then rebuild the index as part of repair MongoDB catalogue metadata may be out of alignment with the WT files on disk When something is missing on disk, then this should be resolved by the changes above When something is missing from the catalogue metadata but exists as a wt table on disk we have no recourse. We would need a user accessible function to import If the WiredTiger metadata is corrupt, then the database is corrupt
MongoDB journal 与 oplog,谁先写入?最近经常被人问到,本文主要科普一下 MongoDB 里 oplog 以及 journal 这两个概念。 journal journal 是 MongoDB 存储引擎层的概念,目前 MongoDB主要支持 mmapv1、wiredtiger、mongorocks 等存储引擎,都支持配置journal。 MongoDB 所有的数据写入、读取最终都是调存储引擎层的接口来存储、读取数据,journal 是存储引擎存储数据时的一种辅助机制。 以wiredtiger 为例,如果不配置 journal,写入 wiredtiger 的数据,并不会立即持久化存储;而是每分钟会做一次全量的checkpoint(storage.syncPeriodSecs配置项,默认为1分钟),将所有的数据持久化。如果中间出现宕机,那么数据只能恢复到最近的一次checkpoint,这样最多可能丢掉1分钟的数据。 所以建议「一定要开启journal」,开启 journal 后,每次写入会记录一条操作日志(通过journal可以重新构造出写入的数据)。这样即使出现宕机,启动时 Wiredtiger 会先将数据恢复到最近的一次checkpoint的点,然后重放后续的 journal 操作日志来恢复数据。 MongoDB 里的 journal 行为 主要由2个参数控制,storage.journal.enabled 决定是否开启journal,storage.journal.commitInternalMs 决定 journal 刷盘的间隔,默认为100ms,用户也可以通过写入时指定 writeConcern 为 {j: ture} 来每次写入时都确保 journal 刷盘。 oplog oplog 是 MongoDB 主从复制层面的一个概念,通过 oplog 来实现复制集节点间数据同步,客户端将数据写入到 Primary,Primary 写入数据后会记录一条 oplog,Secondary 从 Primary(或其他 Secondary )拉取 oplog 并重放,来确保复制集里每个节点存储相同的数据。 oplog 在 MongoDB 里是一个普通的 capped collection,对于存储引擎来说,oplog只是一部分普通的数据而已。 MongoDB 的一次写入 MongoDB 复制集里写入一个文档时,需要修改如下数据 将文档数据写入对应的集合 更新集合的所有索引信息 写入一条oplog用于同步 上面3个修改操作,需要确保要么都成功,要么都失败,不能出现部分成功的情况,否则 如果数据写入成功,但索引写入失败,那么会出现某个数据,通过全表扫描能读取到,但通过索引就无法读取 如果数据、索引都写入成功,但 oplog 写入不成功,那么写入操作就不能正常的同步到备节点,出现主备数据不一致的情况 MongoDB 在写入数据时,会将上述3个操作放到一个 wiredtiger 的事务里,确保「原子性」。 beginTransaction(); writeDataToColleciton(); writeCollectionIndex(); writeOplog(); commitTransaction(); wiredtiger 提交事务时,会将所有修改操作应用,并将上述3个操作写入到一条 journal 操作日志里;后台会周期性的checkpoint,将修改持久化,并移除无用的journal。 从数据布局看,oplog 与 journal 的关系 谁先写入?? oplog 与 journal 是 MongoDB 里不同层次的概念,放在一起比先后本身是不合理的。 oplog 在 MongoDB 里是一个普通的集合,所以 oplog 的写入与普通集合的写入并无区别。 一次写入,会对应数据、索引,oplog的修改,而这3个修改,会对应一条journal操作日志。
Mongos 到 Shard请求管理 Mongos 是 MongoDB 分片集群的访问入口,Mongos 收到 Client 访问请求,会根据从 Config Server 获取的路由表将请求转发到后端对应的 Shard 上。 MongoDB-3.2 版本里,Mongos 到 Shard 的请求由一组 TaskExecutor 来执行,TaskExecutor 可以简单理解为一个任务调度器,当Mongos 需要向 Shard 发送请求时,会将调用 TaskExecutor::scheduleRemoteCommand 将请求扔给调度器,然后等待任务执行完成。 关于 TaskExecutor Mongos 会根据请求的类型来选择 TaskExecutor,写请求为了保证顺序,每次都会选择一个特定的 TaskExecutor 来执行任务。对于读请求 Mongos 会采用 RoundRobin 的方式从一组TaskExecutor 中来选择一个执行(默认会初始化CPU核数个 TaskExecutor)。 TaskExecutor 包含2个重要的组成部分,负责调度逻辑的的 NetworkInterfaceThreadPool, 以及负责实际IO操作的 NetworkInterfaceASIO,使用了 boost::ASIO,将所有IO操作都异步化,它包含一个连接池(ConnectionPool),用于管理 Mongos 到 Shard 的网络连接。 当 Mongos 需要向 Shard 发请求时,就会从连接池里获取一个新的网络连接,当没有空闲的网络连接时,则会创建新的网络连接,所以当客户端到 Mongos 并发请求很多时,Mongos 到 后端 Shard 的网络连接也会很多。 关于连接池 ConnectionPool 针对每 个Shard 机器维护一个连接池,这个连接池包含4个小的池子,用于管理连接的生命周期。 processingPool: 正在建立的连接 readyPool:已经建立并且可用的连接 checkoutPool: 正在使用的连接 droppedProcessingPool:失败的连接,需要释放 连接池管理规则 连接池的总连接会控制在[minConnections, maxConnections]之间,默认为1和无穷大 当需要新建连接时,会发起一个新建连接的异步请求,并把请求放到 processingPool 当连接建立成功后,会把请求转移到readyPool ,readyPool 里的连接可以直接用于服务新的请求 服务某个请求时会从 readyPool 里取出连接后,会将连接转移到 checkOutPool,标识为正在使用 连接使用完后,会归还到 readyPool; 当遇到请求失败 或 一个网络连接空闲超过1分钟时,会释放连接 Mongos 里 TaskExecutor 的个数默认为机器的 CPU 核数,也可以在启动时指定;如果一个机器上部署多个 MongoDB 进程,最好调整该值,可以一定程度上降低到后端 Shard 的连接数量。修改 TaskExecutor 的方法如下 1. 启动命令行指定 mongos --setParameter taskExecutorPoolSize=16 2. 配置文件指定 setParameter: taskExecutorPoolSize: 16 如果 Client 访问 Mongos 的并发特别高,修改 TaskExecutor 也无法有效的控制 Mongos 到 Shard 的连接数,因为一旦没有了空闲的连接,就会创建新的。目前 Mongos 到 Shard 最大连接数还不支持配置,如果确实有需要,可以修改源码。 src/mongo/executor/connection_pool.h - size_t maxConnections = std::numeric_limits<size_t>::max(); + size_t maxConnections = 10000;
MongoDB杭州用户交流会于2017年3月12日下午在阿里巴巴西溪园区举行,吸引了来自全国各地的近300名用户参与,千寻位置、妈妈帮、阿里云等公司的5位技术专家分享了MongoDB 的运维管理及使用经验,干货满满。 用户会进行过程中我已经在中文社区微信总群、二群里做了实时的图文直播,这里再做一个重点内容汇总,错过现场的同学可以学习一下,完整的PPT、以及视频云栖社区的同学正在整理中,敬请期待。 首先,来自千寻位置的肖应军同学分享了其统一监控平台使用 MongoDB 的实践经验。 千寻的统一监控平台包含数据采集、分发、存储、报表、监控等多个模块,其中「存储」和「报表」的模块大量使用了mongoDB,分别解决数据存储和数据分析的问题。 在数据存储方面,监控数据拥有固有的特性,比如监控的指标不固定,可能临时增加;数据写入的频率比较固定,不会有大的波峰/谷流量出现;读取的并发量比较低,但一次返回的数据量比较大,同时随着数据不断的累计,存储量会越来越大。而mongoDB能很好的解决上述需求 mongoDB 无 schema 的特性,使得数据结构扩展起来非常方便 mongoDB 高性能以及数据压缩的特性完全能慢满足数据存储的需求 mongoDB 的TTL索引的特性能自动的删除过期的数据,确保存储容量不会无限膨胀 千寻的报表模块经历了2个阶段的发展,第一阶段分析需求比较简单,直接使用 mongoDB 的aggregation、mapReduce做数据分析来完成;而随着业务方越来越多,报表的维度越来越细,开始使用spark(通过mongoDB spark connector)、阿里云EMR等产品配合mongoDB做数据分析,效率更高,并且能满足复杂查询分析的需求。 最后,千寻的同学分析了使用 mongoDB 过程中积累的经验 生产环境推荐「1主2次」的配置,保证服务高可用、数据高可靠 (注:要保证高可用,除了后端要多节点,还要正确的使用mongoDB driver,以正确的方式连接复制集) 慢查询导致长时间锁库(注:3.x版本wiredtiger引入行级锁后,这个问题应该已经不存在) 写入压力大可能导致整个库慢 (注:尤其是备库的读会受影响,参考MongoDB Secondary 延时高(同步锁)问题分析,但数据库压力太大说明资源已经不足了,应该扩容了) 建索引时,尽量指定{background: true}选项,后台建索引,避免锁库影响业务。 mongoshell能直接执行js脚本,能极大的方便集群管理 使用TTL索引时,索引的字段必须为时间戳字段(注:官方文档有详细介绍) 写入时指定需要的writeConcern级别,推荐{w: 1} (注:3.x的版本里{w: 1}是默认的writeConcern级别,是可靠性与性能的折中选择) 自建mongoDB 全部迁移 到 mongoDB云数据库服务,极大的降低了运维管理成本。(注:作为 mongoDB 云数据库的开发者,能得到客户的肯定,感到灰常开心),下面是个广告链接,不感兴趣的请直接跳过 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 接下来阿里云的技术专家明俨深度解析了mongoDB sharding 备份相关的技术。 mongoDB sharding 解决了写入能力、存储容量扩展的问题,引入了 mongos 用于请求路由,引入 config server 存储sharding 集群的元数据,整个架构相比复制集更加复杂。 sharding 的备份因为「外部修改」以及「内部数据迁移」的影响,使得针对 sharding 集群的备份很难对应都一个确定的时间点。 传统的解决方案是整个集群停止写操作(注:停写的方式包括业务停写,或对secondary调用fsyncLock,或将secondary节点移除),然后对所有shard、config server的数据进行备份,这样的确能回复到一个确定的时间点,但代价很大。 阿里云 MongoDB 数据库针对sharding备份的解决方案是 每个 shard 通过 「定期全量备份 + 持续抓取oplog」,具备恢复到任意时间点的能力(时间点精确到秒级别) 通过分析config server的迁移操作记录,恢复时避开「可能影响数据不一致的时间区间」(通常很短)。 妈妈帮的技术专家胡兴邦介绍了5年来使用 mongoDB 的经验,妈妈帮从2012年就开始全线使用 mongoDB,从2.2(看版本就知道是资深用户)的版本一路升级到3.2(目前都已升级到3.2的最新版本)。 妈妈帮使用mongoDB一路发展过来,使用的架构也不断演进,主要经历了4个阶段 最早的master-slave架构 (注:新接触mongoDB的用户可能不知道这是个啥东西,这是mongoDB早期支持的一种部署模式,跟MySQL的主从架构类似,目前已被复制集替代,不建议使用) 业务增长后,使用多组 master-slave 的 mongoDB 多组的mongoDB master-slave 升级为 多组复制集 多组复制集 + mongoDB sharding 妈妈帮最初选择 mongoDB 主要基于其灵活的文档模型,以及天生可扩展的架构,在业务发展的早期能保证业务快速迭代开发,在业务快速发展之后,还能横向扩展。 在遇到事务方面的需求时(注:mongoDB目前无法支持多文档事务,官方有计划支持),妈妈帮使用了最简单的方式来应对,即「后台定时修正不一致的数据」,其他的备选方案,例如使用消息队列、二阶段提交方式从方案上更加成熟,但实现复杂度更高。 在sharding方面,妈妈帮也积累了不少经验,建议用户在使用sharding时,一定要注意shardKey的选择,并给出了一些建议。 能满足业务场景查询需求,尽量保证大部分query条件都由shard key,这样请求只用分发到后端单个shard就能满足,性能更高 尽量避免单个shard出现热点 (注:需要正确理解hash分片 和 range分片 2种方式的优劣,做出最适合自己业务的选择) 避免shard key的取值过少,导致单个chunk很大(jumbo chunk)而无法自动迁移 多阅读官方文档,sharding-shard-key 阿里云资深研发工程师果实介绍阿里云 MongoDB 云数据库高可用的主题,介绍mongoDB云数据库如何实现自动的故障检测及故障转移。 阿里云数据库 MongoDB 版 是由3个节点组成的高可用复制集(目前也已支持sharding形态),3个分别为Primary、Secondary 和 Hidden,其中Priamry、Secondary节点提供给用户读写,Hidden节点对用户不可见,主要用于实例备份以及保证实例高可用。 Hidden节点平时只同步Primary上写入的数据,并不对外提供服务,实例的全量及增量备份会在Hidden上进行,做到不影响用户的业务。 同时,后端管控服务会不断的模拟用户访问行为来探测实例可用性,当发现实例有节点故障时 如果 Hidden 节点故障(不可恢复的故障,如果机器没挂,会尝试先重启启动),后端管控会从资源池里选择一个新的节点,以Hidden的身份加入复制集,替换原来的Hidden,这个过程对用户的服务无影响。 如果 Secondary 节点故障,会自动将 Hidden 节点切换为 Secondary,保证用户访问 Secondary 节点不受影响。此时变成了1的状态,按1的方式继续故障转移处理。 如果 Primary 节点故障,这时复制集会自动选出新的Primary,此时复制集里缺一个Secondary,变成了2的状态,按2的方式继续故障转移处理。 如果出现2台及以上节点故障,根据 MongoDB 多数派的选举原则,是无法选出Primary的,这时实例会进入只读状态,需要人工介入恢复,但这种场景极少出现。(注:这里也可以reconfig一下,让复制集变成单节点运行继续服务读写,但考虑到用户数据的可靠性,目前并没有使用这个方案) 除了故障时的处理,对于计划中的机器维修、下线,则需要对机器上所有的实例,先将该节点切换为Hidden角色,然后针对所有的Hidden节点按上述1的流程处理,用新的节点替换,当节点上没有任何实例数据时,就可以安全下线了。 最后出场的是徐雷老师,徐雷老师是《MongoDB实战》第2版的译者,徐雷老师的分享风趣幽默,不仅讲到MongoDB,还分享了很多架构设计方面的经验,由于当时有事掉线了,没有获取到精髓,等PPT出来大家可以好好学习一下。 在分享里徐老师也提到 MongoDB 目前在国内外各大企业里都有着广泛的应用,充分说明 MongoDB 是一门值得深入投资的技术。 最后,预告一下,MongoDB 中文社区今年还会继续在全国各大城市举行 MongoDB 用户的技术交流会,有强大的社区做后盾,用户们可以更放心的使用 MongoDB;而且 MongoDB 本身官方文档已经非常全面了,绝大多数的问题都能从官方文档找到答案,建议大家多看官方文档,用好 MongoDB,为你的业务创造最大价值。 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
MongoDB 3.4 支持了 Collation特性,官方文档对这个特性的解释是 Collation allows users to specify language-specific rules for string comparison, such as rules for lettercase and accent marks. 简而言之,Collation特性允许MongoDB的用户根据不同的语言定制排序规则,举个例子,一个存储中国用户信息的集合。 db.createCollection("person") db.person.insert({name: "张三"}) db.person.insert({name: "李四"}) db.person.insert({name: "王五"}) db.person.insert({name: "马六"}) db.person.insert({name: "张七"}) 默认情况下,名字字段会被当做一个普通的二机制字符串来对比,按照name字段排序的结果如下 mongo-9554:PRIMARY> db.person.find().sort({name: 1}) { "_id" : ObjectId("586b98980cec8d86881cffac"), "name" : "张七" } { "_id" : ObjectId("586b98980cec8d86881cffa8"), "name" : "张三" } { "_id" : ObjectId("586b98980cec8d86881cffa9"), "name" : "李四" } { "_id" : ObjectId("586b98980cec8d86881cffaa"), "name" : "王五" } { "_id" : ObjectId("586b98980cec8d86881cffab"), "name" : "马六" } 而对于中文名字,通常有按拼音顺序排序的需求,这时就可以通过collation来搞定 db.createCollection("person", {collation: {locale: "zh"}}) db.person.insert({name: "张三"}) db.person.insert({name: "李四"}) db.person.insert({name: "王五"}) db.person.insert({name: "马六"}) db.person.insert({name: "张七"}) 此时再按name字段排序,则会按照locale指定的中文规则来排序 mongo-9554:PRIMARY> db.person.find().sort({name: 1}) { "_id" : ObjectId("586b995d0cec8d86881cffae"), "name" : "李四" } { "_id" : ObjectId("586b995d0cec8d86881cffb0"), "name" : "马六" } { "_id" : ObjectId("586b995d0cec8d86881cffaf"), "name" : "王五" } { "_id" : ObjectId("586b995d0cec8d86881cffb1"), "name" : "张七" } { "_id" : ObjectId("586b995d0cec8d86881cffad"), "name" : "张三" } MongoDB 3.4里,基本所有设计字符串字段排序的命令,都支持指定collation,比如「创建集合、创建索引、find」等;上述例子里在createCollection的时候指定了collation,则该集合里所有字符串默认都会按指定的collation来排序,如果只想针对某一个字段来指定collation,可以该字段创建指定collation的索引,例如 db.person.createIndex({name: 1}, {collation: {locale: "zh"}}) 注意:如果是从3.2版本升级到3.4的,需要先执行如下命令才能使用collation特性 db.adminCommand( { setFeatureCompatibilityVersion: "3.4" } )
MongoDB分片集群(Sharded Cluster)通过将数据分散存储到多个分片(Shard)上,来实现高可扩展性。实现分片集群时,MongoDB 引入 Config Server 来存储集群的元数据,引入 mongos 作为应用访问的入口,mongos 从 Config Server 读取路由信息,并将请求路由到后端对应的 Shard 上。 使用分片集群时你需要知道的 用户访问 mongos 跟访问单个 mongod 类似 所有 mongos 是对等关系,用户访问分片集群可通过任意一个或多个mongos mongos 本身是无状态的,可任意扩展,集群的服务能力为『Shard服务能力之和』与『mongos服务能力之和』的最小值。 访问分片集群时,最好将应用负载均匀的分散到多个 mongos 上 正确连接分片集群的姿势 要正确连接复制集,需要先了解下MongoDB的Connection String URI,所有官方的driver都支持以 Connection String 的方式来连接 MongoDB 分片集群。 下面就是Connection String包含的主要内容 mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] mongodb:// 前缀,代表这是一个Connection String username:password@ 如果启用了鉴权,需要指定用户密码 hostX:portX 多个 mongos 的地址列表 /database 鉴权时,用户帐号所属的数据库 ?options 指定额外的连接选项 以连接阿里云数据库MongoDB版为例,当你购买阿里云MongoDB分片集群后,就会在控制台上看到各个mongos的地址信息。 为了方便用户使用,控制台上也生成了连接复制集的Connection String及通过Mongo Shell连接的命令。 例如通过java来连接,更多的DEMO MongoClientURI connectionString = new MongoClientURI("mongodb://:****@s-m5e80a9241323604.mongodb.rds.aliyuncs.com:3717,s-m5e053215007f404.mongodb.rds.aliyuncs.com:3717/admin"); // ****替换为root密码 MongoClient client = new MongoClient(connectionString); MongoDatabase database = client.getDatabase("mydb"); MongoCollection<Document> collection = database.getCollection("mycoll"); 通过上述方式连接分片集群时,客户端会自动将请求分散到多个mongos 上,以实现负载均衡;同时,当URI 里 mongos 数量在2个及以上时,当有mongos故障时,客户端能自动进行 failover,将请求都分散到状态正常的 mongos 上。 当 Mongos 数量很多时,还可以按应用来将 mongos 进行分组,比如有2个应用A、B、有4个mongos,可以让应用A 访问 mongos 1-2(URI里只指定mongos 1-2 的地址), 应用B 来访问 mongos 3-4(URI里只指定mongos 3-4 的地址),根据这种方法来实现应用间的访问隔离(应用访问的mongos彼此隔离,但后端 Shard 仍然是共享的)。 总而言之,在访问分片集群时,请务必确保 MongoDB URI 里包含2个及以上的mongos地址,来实现负载均衡及高可用。 常用连接参数 如何实现读写分离? 在options里添加readPreference=secondaryPreferred即可实现,读请求优先到Secondary节点,从而实现读写分离的功能,更多读选项参考Read preferences 如何限制连接数? 在options里添加maxPoolSize=xx即可将客户端连接池限制在xx以内。 如何保证数据写入到大多数节点后才返回? 在options里添加w= majority即可保证写请求成功写入大多数节点才向客户端确认,更多写选项参考Write Concern MongoDB
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 工欲善其事,必先利其器,我们在使用数据库时,通常需要各种工具的支持来提高效率;很多新用户在刚接触 MongoDB 时,遇到的问题是『不知道有哪些现成的工具可以使用』,本系列文章将主要介绍 MongoDB 生态在工具、driver、可视化管理等方面的支持情况。 MongoDB 生态 - 可视化管理工具 MongoDB 生态 - 客户端 Driver 支持 MongoDB 生态 - 官方命令行工具 本文主要介绍社区里贡献的贡献的一些开源工具,这些都是从 MongoDB tools 里精选的github start比较多的开源项目。 shell 提升工具 mongo-hacker mongo-hacker 主要是通过 ~/.mongorc.js 文件给 mongo shell 实现额外的扩展功能,比如配色输出、扩展一些API、简化aggregation语法等,提升了mongo shell的可读性、易用性,不过由于长时间未更新,部分功能在最新的版本上已经不可用了,经过测试,我最关注的配色输出是没问题的。 schema分析工具 variety variety 是一款 MongoDB 的 schema 分析工具。 比如针对如下 users 集合 db.users.insert({name: "Tom", bio: "A nice guy.", pets: ["monkey", "fish"], someWeirdLegacyKey: "I like Ike!"}); db.users.insert({name: "Dick", bio: "I swordfight.", birthday: new Date("1974/03/14")}); db.users.insert({name: "Harry", pets: "egret", birthday: new Date("1984/03/14")}); db.users.insert({name: "Geneviève", bio: "Ça va?"}); db.users.insert({name: "Jim", someBinData: new BinData(2,"1234")}); $ mongo test --eval "var collection = 'users'" variety.js +------------------------------------------------------------------+ | key | types | occurrences | percents | | ------------------ | ------------ | ----------- | -------- | | _id | ObjectId | 5 | 100.0 | | name | String | 5 | 100.0 | | bio | String | 3 | 60.0 | | birthday | Date | 2 | 40.0 | | pets | Array(1),String(1) | 2 | 40.0 | | someBinData | BinData-old | 1 | 20.0 | | someWeirdLegacyKey | String | 1 | 20.0 | +------------------------------------------------------------------+ Restful接口 Eve eve 是基于python开发的开源 REST API 框架,借助它可以快速方便的开发Web服务,eve后端的数据库支持 MongoDB 以及关系型数据库。 $ curl -i http://127.0.0.1:5000/people/obama HTTP/1.0 200 OK Etag: 28995829ee85d69c4c18d597a0f68ae606a266cc Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT Cache-Control: 'max-age=10,must-revalidate' Expires: 10 "firstname": "barack", "lastname": "obama", "_id": "50acfba938345b0978fccad7" "updated": "Wed, 21 Nov 2012 16:04:56 GMT", "created": "Wed, 21 Nov 2012 16:04:56 GMT", "_links": { "self": {"href": "people/50acfba938345b0978fccad7", "title": "person"}, "parent": {"href": "/", "title": "home"}, "collection": {"href": "people", "title": "people"} 与 eve 功能类似的工具还有 Kule、RESTHeart、Crest。 索引优化工具 dex dex 是 MongoDB 开发的索引优化工具,能根据查询日志来优化索引,但比较遗憾的是这个工具只支持2.6及以下的MongoDB; 这个项目做的工作非常有意义,有兴趣的同学可以fork这个项目,增加对最新版本 MongoDB 的支持。 对象关系映射 mongoengine mongoengine 能很方便的实现 python 对象到 MongoDB 文档之间的映射。 from mongoengine import * connect('mydb') ''' Blog基类 class BlogPost(Document): title = StringField(required=True, max_length=200) posted = DateTimeField(default=datetime.datetime.utcnow) tags = ListField(StringField(max_length=50)) meta = {'allow_inheritance': True} ''' 文本Blog派生类 class TextPost(BlogPost): content = StringField(required=True) ''' 链接Blog派生类 class LinkPost(BlogPost): url = StringField(required=True) # Create a text-based post >>> post1 = TextPost(title='Using MongoEngine', content='See the tutorial') >>> post1.tags = ['mongodb', 'mongoengine'] >>> post1.save() # Create a link-based post >>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine') >>> post2.tags = ['mongoengine', 'documentation'] >>> post2.save() # Iterate over all posts using the BlogPost superclass >>> for post in BlogPost.objects: ... print '===', post.title, '===' ... if isinstance(post, TextPost): ... print post.content ... elif isinstance(post, LinkPost): ... print 'Link:', post.url ... print # Count all blog posts and its subtypes >>> BlogPost.objects.count() >>> TextPost.objects.count() >>> LinkPost.objects.count() 其他语言也有类似的工具,例如 nodejs 版本的 mongoose java 版本的 spring data mongodb Ruby 版本的 MongoMapper、 PHP 版本的 doctrine-odm 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 Mongo shell 是 MongoDB 的命令行管理工具,功能非常强大,最近社区很多人咨询的一些问题,比如 命令行看 json 格式比较吃力? 如何确定Secondary节点同步是否跟上? 怎么查看DB、集合使用了多少空间? 能否在shell 脚本里调用Mongo shell 怎么执行 MongoDB 命令,比如创建集合、索引? ...... 上述问题都可以通过 Mongo shell 来解决,而且Mongo shell能做的远不止这些。 为了方便关系型数据库的的用户切换到 MongoDB 上能快速上手,mongo shell里做了一些语法上的兼容(最终还是通过调用 MongoDB 的命令实现的 ),例如 show dbs 列出所有DB use dbname 切换当前DB show tables 或 show collections 列出当前DB的所有表/集合 show users 列出当前DB的所有用户 show profile 列出当前DB的所有慢查询 show logs 列出运行日志 MongoDB的所有请求都以命令的形式发出,支持的命令列表参考Database Commands 基本所有的driver都会实现一个通用的执行命令的接口,然后再封装出一些常用的接口(比如常用的CRUD操作),mongo shell 通过 runCommand 接口来实现执行命令,例如执行 serverStatus 命令 * db.runCommand( { serverStatus: 1} ) mongo shell也对很对很多常用的命令进行了封装,让用户使用起来更简单。 常见的封装接口包括 * db.serverStatus() 查看mongod运行状态信息 * db.stats() 查看db元数据 * db.collection.stats() 查看集合元数据 * db.collection.insert() / update / remove / find 对集合增删改查 * db.collection.createIndex() 创建索引 * db.collection.dropIndex() 删除索引 * db.dropDatabase() 删除DB * db.printReplicationInfo() * db.printSlaveReplicationInfo() 查看复制集同步信息 * rs.status() 查看复制集当前状态 * rs.conf() 查看复制集配置 * rs.initiate() 初始化复制集 * rs.reconfig() 重新配置复制集 * rs.add() / rs.remove() 增加/删除复制集节点 * sh.enableSharding() 对DB启用分片 * sh.shardCollection() 对集合进行分片 * sh.status() 查看sharding状态信息 * ... 文档格式化输出 很多同学在使用 mongo shell时,觉得文档输出后可读性差,比如 mongo-9555:PRIMARY> db.collection1.find() // 对集合调用find时,默认输出前20个文档 { "_id" : ObjectId("587ed6ce098a4da78d508468"), "name" : "jack", "age" : 18, "sex" : "male", "hobbies" : [ "football", "basketball" ], "contact" : { "phone" : "10000123456", "address" : "hangzhou", "zipcode" : "31000" } } 实际上,mongo shell 可以对cursor的输出进行格式化(pretty)输出,JSON的文档会被格式化输出,可读性很强 mongo-9555:PRIMARY> db.collection1.find().pretty() "_id" : ObjectId("587ed6ce098a4da78d508468"), "name" : "jack", "age" : 18, "sex" : "male", "hobbies" : [ "football", "basketball" "contact" : { "phone" : "10000123456", "address" : "hangzhou", "zipcode" : "31000" mongo shell 里还可以通过 printjson 来格式化输出任意json对象,比如 mongo-9555:PRIMARY> printjson({ "_id" : ObjectId("587ed6ce098a4da78d508468"), "name" : "jack", "age" : 18, "sex" : "male", "hobbies" : [ "football", "basketball" ], "contact" : { "phone" : "10000123456", "address" : "hangzhou", "zipcode" : "310000000" } }) "_id" : ObjectId("587ed6ce098a4da78d508468"), "name" : "jack", "age" : 18, "sex" : "male", "hobbies" : [ "football", "basketball" "contact" : { "phone" : "10000123456", "address" : "hangzhou", "zipcode" : "31000" shell脚本调用 mongo shell 除了支持交互式的调用方式,还能支持执行完一个或一批操作后自动退出,这样就能很方便的在shell 脚本里调用 mongo shell,比如获取 MongoDB 各个命令备调用的次数。 $ mongo --host localhost:27017 --eval "printjson( db.serverStatus().opcounters )" MongoDB shell version: 3.0.5 connecting to: localhost:27017/test "insert" : 2, "query" : 13, "update" : 0, "delete" : 0, "getmore" : 74191, "command" : 104198 如果要一次执行很多个 MongoDB 的操作,可以将操作写到文件里,然后使用 mongo shell 批量执行 $cat test.js db = db.getSiblingDB("mydb") // 脚本里切换db的方式,相当于use mydb for (var i = 0; i < 100; i++) { db.collection.insert( {x: i} ) printjson( {db.collection.count()} ) $ mongo --host localhost:27017 test.js MongoDB shell version: 3.0.5 connecting to: localhost:27017/test mongo shell 还提供『启动时执行脚本』的机制,类似与linux shell里的启动新的shell时,执行~/.bashrc等文件的机制。 只要将脚本写入 ~/.mongorc.js 文件里, mongo shell 启动时,就会先执行这个脚本,例如 $cat .mongorc.js print("Welcome, ZhangYoudong"); 然后每次登录mongo shell时,这个文件的js脚本就会被执行 $ mongo --host localhost:27017 MongoDB shell version: 3.0.5 connecting to: localhost:27017/test Welcome, ZhangYoudong man 手册 上述的命令,并不需要去记忆,跟使用 linux shell 一样,需要用的时候看下 help 信息 * help * db.help() * rs.help() * sh.help() * db.collection.find().help() * help misc 除了上述功能,mongo shell 还提供了命令补全、命令历史等很多实用的功能,只要习惯了使用mongo shell,根本无需再使用图形界面来管理 MongoDB;当然为了方便更多用户,阿里云 MongoDB 云数据库 不仅支持通过mongo shell 及 其他第三方图形管理工具访问,还附带一个DMS的数据库管理系统,供用户免费使用。 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 最近 MongoDB “赎金事件”闹得沸沸扬扬,不少公网上裸奔的 MongoDB 中招,有兴趣的同学可以看下耗子叔写的从 MONGODB “赎金事件” 看安全问题,中招的主要原因还是因为用户的安全意识比较薄弱,部署的 MongoDB 完全没有任何安全防护,可以通过公网访问,并且没有开启鉴权。 MongoDB 官方文档在安全方面做了很多总结,出了一个MongoDB Security Checklist,但从这次中招的规模来看,大部分用户并没有认真看过这个文档,这次事件也刚好是一个数据库安全科普的好机会,希望所有的数据库用户都要重视自己的数据安全。 MongoDB 安全 Checklist,用户可以做的安全措施包括 开启鉴权 Enable Auth,并给所有需要访问MongoDB数据库的用户配置合适的权限(权限最小化原则) 限制网络访问,只允许从安全的网络环境访问MongoDB,可以使用系统防火墙,或 MongoDB 自身的配置项bindIp来限制访问来源 开启访问审计,记录所有的用户访问行为,万一出问题时有据可查 限制MongoDB进程的权限,尽量创建单独的用户来管理MongoDB进程,不要用root帐号启动 访问链路加密、存储的数据加密,不过绝大部分场景还不需要这么高的安全级别,实在需要也可以配置上,会对性能有一定影响 为了方便用户快速进行合理的配置,给大家分享一个阿里云数据库MongoDB版的配置模板(为了简化,稍有改动)。 数据组织,一个mongod进程对应一个工作目录,包含data、etc、logs 3个目录,分别存储数据、配置、以及运行日志 |-- $mymongo |-- data -- *** |-- etc -- keyfile -- mongod.conf |-- logs -- mongod.pid -- mongod.log mongod.conf内容 (将$mymongo替换成你的工作目录) systemLog: destination: file logAppend: true logRotate: rename path: $mymongo/logs/mongod.log timeStampFormat: iso8601-local traceAllExceptions: false verbosity: 0 processManagement: fork: true pidFilePath: $mymongo/logs/mongod.pid #bindIp: 127.0.0.1 port: 3001 http: enabled: false maxIncomingConnections: 1000 unixDomainSocket: enabled: false operationProfiling: mode: slowOp slowOpThresholdMs: 100 security: authorization: enabled keyFile: $mymongo/etc/keyfile javascriptEnabled: false replication: oplogSizeMB: 5120 replSetName: myreplset storage: dbPath: $mymongo/data directoryPerDB: true syncPeriodSecs: 60 engine: wiredTiger journal: enabled: true commitIntervalMs: 100 wiredTiger: engineConfig: cacheSizeGB: 4 基于上述模板,用户可以根据自己的实际情况稍加修改,主要关注如下参数 systemLog.verbosity 建议设置为0,如想记录更多debug信息,可修改该值为1-5,越大日志越详细 net.bindIp 监听的ip地址列表 默认监听所有的ip,如果有多块网卡,可以选择性的绑定,以限制不可信的网络访问 net.port 默认27017,根据需要定制 net.maxIncomingConnections 最大连接数 根据需要配置,保证系统最大文件句柄数大于该值(ulimit -n) operationProfiling.slowOpThresholdMs 慢请求阈值 如无特殊需求,建议使用默认的100ms,超过该值的请求会记录到对应db的system.profile集合里 replication.replSetName 复制集名字 强烈建议部署复制集提供服务,名字随便定制 replication.oplogSizeMB oplog大小 默认为磁盘空间5%,无特殊需求建议保持默认值 security.authorization 是否开启鉴权 强烈建议开启 security.keyFile 复制集内部鉴权的keyfile路径 复制集要开启鉴权,必须配置keyfile,用于复制集成员间的鉴权 security.javascriptEnabled 是否支持server端js,比如$where、mapreduce需要server端js的支持 如无必要,建议关闭 storage.directoryPerDB 每个db一个单独的目录存储 强烈建议,以充分发挥文件系统优势 storage.engine 强烈建议使用wiredtiger,低成本 + 高性能 storage.wiredTiger.engineConfig.engineConfig wireditger cache大小 默认 max(1, 0.6 * RAM) storage.journal.enabled 是否开启journal 强烈建议开启 storage.journal.commitIntervalMs journal 刷盘间隔 默认100ms,建议保持默认值 上述参数,对于不确定含义或拿不准的参数可以直接使用默认值 (配置项行首加 # 即可注释掉配置)。 使用上述模板启动 MongoDB 服务后,用户可以先通过localhost/127.0.0.1来连接 MongoDB,建立第一个管理用户,接下来就可以用管理用户登录做进一步的操作。 要想让MongoDB发挥最佳的效果,了解其各项配置含义来优化配置是非常有必要的,如果想完全免去数据库运维的烦恼,专注于业务开发,则可直接使用阿里云数据库MongoDB版,主要特性包括: 完全兼容MongoDB 3.2版本,开启鉴权、保证数据库安全 3节点复制集,保证数据高可靠、服务高可用,即将推出分片集群(MongoDB Sharded Cluster) 提供全量、增量备份功能,即使数据被误删,也能恢复到任意时间点 提供数据库操作审计功能,用户的所有操作都记录审计日志,有据可查 支持VPC环境,支持用户白名单功能 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
电商业务一个基本的功能模块就是存储品类丰富的商品信息,各种商品特性、参数各异,MongoDB 灵活的文档模型非常适合于这类业务,本文主要介绍如何使用 MongoDB 来存储商品分类信息,内容翻译自User case - Product Catalog 关系型数据库解决方案 上述问题使用传统的关系型数据库也可以解决,比如以下几种方案 针对不同商品,创建不同的表 比如音乐专辑、电影这2种商品,有一部分共同的属性,但也有很多自身特有的属性,可以创建2个不同的表,拥有不同的schema。 CREATE TABLE `product_audio_album` ( `sku` char(8) NOT NULL, `artist` varchar(255) DEFAULT NULL, `genre_0` varchar(255) DEFAULT NULL, `genre_1` varchar(255) DEFAULT NULL, PRIMARY KEY(`sku`)) CREATE TABLE `product_film` ( `sku` char(8) NOT NULL, `title` varchar(255) DEFAULT NULL, `rating` char(8) DEFAULT NULL, PRIMARY KEY(`sku`)) 这种做法的主要问题在于 针对每个新的商品分类,都需要创建新的表 应用程序开发者必须显式的将请求分发到对应的表上来查询,一次查询多种商品实现起来比较麻烦 所有商品存储到单张表 CREATE TABLE `product` ( `sku` char(8) NOT NULL, `artist` varchar(255) DEFAULT NULL, `genre_0` varchar(255) DEFAULT NULL, `genre_1` varchar(255) DEFAULT NULL, `title` varchar(255) DEFAULT NULL, `rating` char(8) DEFAULT NULL, PRIMARY KEY(`sku`)) 将所有的商品存储到一张表,这张表包含所有商品需要的属性,不同的商品根据需要设置不同的属性,这种方法使得商品查询比较简单,并且允许一个查询跨多种商品,但缺点是浪费的空间比较多。 提取公共属性,多表继承 CREATE TABLE `product` ( `sku` char(8) NOT NULL, `title` varchar(255) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, `price`, ... PRIMARY KEY(`sku`)) CREATE TABLE `product_audio_album` ( `sku` char(8) NOT NULL, `artist` varchar(255) DEFAULT NULL, `genre_0` varchar(255) DEFAULT NULL, `genre_1` varchar(255) DEFAULT NULL, PRIMARY KEY(`sku`), FOREIGN KEY(`sku`) REFERENCES `product`(`sku`)) CREATE TABLE `product_film` ( `sku` char(8) NOT NULL, `title` varchar(255) DEFAULT NULL, `rating` char(8) DEFAULT NULL, PRIMARY KEY(`sku`), FOREIGN KEY(`sku`) REFERENCES `product`(`sku`)) 上述方案将所有商品公共的属性提取出来,将公共属性存储到一张表里,每种商品根据自身的需要创建新的表,新表里只存储该商品特有的信息。 Entity Attribute Values 形式存储 所有的数据按照<商品SKU, 属性、值> 的3元组的形式存储,这个方案实际上是把关系型数据库当KV存储使用,模型简单,但应对复杂的查询不是很方便。 Entity Attribute Values sku_00e8da9b Audio Album sku_00e8da9b title A Love Supreme sku_00e8da9b sku_00e8da9b artist John Coltrane sku_00e8da9b genre sku_00e8da9b genre General MongoDB 解决方案 MognoDB 与关系型数据库不同,其无schema,文档内容可以非常灵活的定制,能很好的使用上述商品分类存储的需求; 将商品信息存储在一个集合里,集合里不同的商品可以自定义文档内容。 比如一个音乐专辑可以类似如下的文档结构 sku: "00e8da9b", type: "Audio Album", title: "A Love Supreme", description: "by John Coltrane", asin: "B0000A118M", shipping: { weight: 6, dimensions: { width: 10, height: 10, depth: 1 pricing: { list: 1200, retail: 1100, savings: 100, pct_savings: 8 details: { title: "A Love Supreme [Original Recording Reissued]", artist: "John Coltrane", genre: [ "Jazz", "General" ], tracks: [ "A Love Supreme Part I: Acknowledgement", "A Love Supreme Part II - Resolution", "A Love Supreme, Part III: Pursuance", "A Love Supreme, Part IV-Psalm" 而一部电影则可以存储为 sku: "00e8da9d", type: "Film", asin: "B000P0J0AQ", shipping: { ... }, pricing: { ... }, details: { title: "The Matrix", director: [ "Andy Wachowski", "Larry Wachowski" ], writer: [ "Andy Wachowski", "Larry Wachowski" ], aspect_ratio: "1.66:1" 所有商品都拥有一些共同的基本信息,特定的商品可以根据需要扩展独有的内容,非常方便; 基于上述模型,MongoDB 也能很好的服务各类查询。 查询某个演员参演的所有电影,并按发型日志排序 db.products.find({'type': 'Film', 'details.actor': 'Keanu Reeves'}).sort({'details.issue_date', -1}) 上述查询也可以通过建立索引来加速 db.products.createIndex({ type: 1, 'details.actor': 1, 'details.issue_date': -1 }) 查询标题里包含特定信息的所有电影 db.products.find({ 'type': 'Film', 'title': {'$regex': '.*hacker.*', '$options':'i'}}).sort({'details.issue_date', -1}) 可建立如下索引来加速查询 db.products.createIndex({ type: 1, details.issue_date: -1, title: 1 }) 当单个节点无法满足海量商品信息存储的需求时,就需要使用MongoDB sharding来扩展,假定大量的查询都是都会基于商品类型,那么就可以使用商品类型字段来进行分片。 db.shardCollection('products', { key: {type: 1} }) 分片时,尽量使用复合的索引字段,这样能满足更多的查询需求,比如基于商品类型之后,还会经常根据商品的风格标签来查询,则可以把商品的标签字段作为第二分片key。 db.shardCollection('products', { key: {type: 1, 'details.genre': 1} }) 如果某种类型的商品,拥有相同标签的特别多,则会出现jumbo chunk的问题,导致无法迁移,可以进一步的优化分片key,以避免这种情况。 db.shardCollection('products', { key: {type: 1, 'details.genre': 1, sku: 1} }) 加入第3分片key之后,即使类型、风格标签都相同,但其sku信息肯定不同,就肯定不会出现超大的chunk。
线上运行的服务会产生大量的运行及访问日志,日志里会包含一些错误、警告、及用户行为等信息,通常服务会以文本的形式记录日志信息,这样可读性强,方便于日常定位问题,但当产生大量的日志之后,要想从大量日志里挖掘出有价值的内容,则需要对数据进行进一步的存储和分析。 本文以存储 web 服务的访问日志为例,介绍如何使用 MongoDB 来存储、分析日志数据,让日志数据发挥最大的价值,本文的内容同样使用其他的日志存储型应用。 一个典型的web服务器的访问日志类似如下,包含访问来源、用户、访问的资源地址、访问结果、用户使用的系统及浏览器类型等 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "[http://www.example.com/start.html](http://www.example.com/start.html)" "Mozilla/4.08 [en](Win98; I ;Nav)" 最简单存储这些日志的方法是,将每行日志存储在一个单独的文档里,每行日志在MongoDB里类似 _id: ObjectId('4f442120eb03305789000000'), line: '127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "[http://www.example.com/start.html](http://www.example.com/start.html)" "Mozilla/4.08 [en](Win98; I ;Nav)"' 上述模式虽然能解决日志存储的问题,但使得这些数据分析起来比较麻烦,因为文本分析并不是MongoDB所擅长的,更好的办法时,在把一行日志存储到MongoDB的文档里前,先提取出各个字段的值,如下所示,上述的日志被转换为一个包含很多个字段的文档。 _id: ObjectId('4f442120eb03305789000000'), host: "127.0.0.1", logname: null, user: 'frank', time: ISODate("2000-10-10T20:55:36Z"), path: "/apache_pb.gif", request: "GET /apache_pb.gif HTTP/1.0", status: 200, response_size: 2326, referrer: "[http://www.example.com/start.html](http://www.example.com/start.html)", user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)" 同时,在这个过程中,如果你觉得有些字段对数据分析没有任何帮助,则可以直接过滤掉,以减少存储上的消耗,比如 数据分析不会关心user信息、request、status信息,这几个字段没必要存储 ObjectId里本身包含了时间信息,没必要再单独存储一个time字段 (当然带上time也有好处,time更能代表请求产生的时间,而且查询语句写起来更方便,尽量选择存储空间占用小的数据类型) 基于上述考虑,一行日志最终存储的内容可能类似如下 _id: ObjectId('4f442120eb03305789000000'), host: "127.0.0.1", time: ISODate("2000-10-10T20:55:36Z"), path: "/apache_pb.gif", referer: "[http://www.example.com/start.html](http://www.example.com/start.html)", user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)" 日志存储服务需要能同时支持大量的日志写入,用户可以定制 writeConcern 来控制日志写入能力,猛击这里详细了解writeConcern db.events.insert({ host: "127.0.0.1", time: ISODate("2000-10-10T20:55:36Z"), path: "/apache_pb.gif", referer: "[http://www.example.com/start.html](http://www.example.com/start.html)", user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)" 如果要想达到最高的写入吞吐,可以指定 writeConcern 为 {w: 0} 而如果日志的重要性比较高(比如需要用日志来作为计费凭证),则可以使用更安全的writeConcern级别,比如 {w: 1} 或 {w: "majority"} 同时,为了达到最优的写入效率,用户还可以考虑批量的写入方式,一次网络请求写入多条日志。 db.events.insert([doc1, doc2, ...]) 当日志按上述方式存储到 MongoDB 后,就可以满足各种查询需求 查询所有访问 /apache_pb.gif 的请求 q_events = db.events.find({'path': '/apache_pb.gif'}) 如果这种查询非常频繁,可以针对path字段建立索引,以高效的服务这类查询 db.events.createIndex({path: 1}) 查询某一天的所有请求 q_events = db.events.find({'time': { '$gte': ISODate("2016-12-19T00:00:00.00Z"),'$lt': ISODate("2016-12-20T00:00:00.00Z")}}) 通过对time字段建立索引,可加速这类查询 db.events.createIndex({time: 1}) 查询某台主机一段时间内的所有请求 q_events = db.events.find({ 'host': '127.0.0.1', 'time': {'$gte': ISODate("2016-12-19T00:00:00.00Z"),'$lt': ISODate("2016-12-20T00:00:00.00Z" } 通过对host、time建立复合索引可以加速这类查询 db.events.createIndex({host: 1, time: 1}) 同样,用户还可以使用MongoDB的aggregation、mapreduce框架来做一些更复杂的查询分析,在使用时应该尽量建立合理的索引以提升查询效率。 当写日志的服务节点越来越多时,日志存储的服务需要保证可扩展的日志写入能力以及海量的日志存储能力,这时就需要使用MongoDB sharding来扩展,将日志数据分散存储到多个shard,关键的问题就是shard key的选择。 按时间戳字段分片 一种简单的方式是使用时间戳来进行分片(如ObjectId类型的_id,或者time字段),这种分片方式存在如下问题 因为时间戳一直顺序增长的特性,新的写入都会分到同一个shard,并不能扩展日志写入能力 很多日志查询是针对最新的数据,而最新的数据通常只分散在部分shard上,这样导致查询也只会落到部分shard 按随机字段分片 按照_id字段来进行hash分片,能将数据以及写入都均匀都分散到各个shard,写入能力会随shard数量线性增长,但该方案的问题时,数据分散毫无规律,所有的范围查询(数据分析经常需要用到)都需要在所有的shard上进行查找然后合并查询结果,影响查询效率。 按均匀分布的key分片 假设上述场景里 path 字段的分布是比较均匀的,而且很多查询都是按path维度去划分的,那么可以考虑按照path字段对日志数据进行分片,好处是 写请求会被均分到各个shard 针对path的查询请求会集中落到某个(或多个)shard,查询效率高 不足的地方是 如果某个path访问特别多,会导致单个chunk特别大,只能存储到单个shard,容易出现访问热点 如果path的取值很少,也会导致数据不能很好的分布到各个shard 当然上述不足的地方也有办法改进,方法是给分片key里引入一个额外的因子,比如原来的shard key是 {path: 1},引入额外的因子后变成 {path: 1, ssk: 1} 其中ssk可以是一个随机值,比如_id的hash值,或是时间戳,这样相同的path还是根据时间排序的 这样做的效果是分片key的取值分布丰富,并且不会出现单个值特别多的情况。 上述几种分片方式各有优劣,用户可以根据实际需求来选择方案。 应对数据增长 分片的方案能提供海量的数据存储支持,但随着数据越来越多,存储的成本会不断的上升,而通常很多日志数据有个特性,日志数据的价值随时间递减,比如1年前、甚至3个月前的历史数据完全没有分析价值,这部分可以不用存储,以降低存储成本,而在MongoDB里有很多方法支持这一需求。 TTL 索引 MongoDB 的TTL索引 可以支持文档在一定时间之后自动过期删除,例如上述日志time字段代表了请求产生的时间,针对该字段建立一个TTL索引,则文档会在30小时后自动被删除。 db.events.createIndex( { time: 1 }, { expireAfterSeconds: 108000 } ) TTL 索引目前是后台单线程来定期(默认60s一次)去删除已过期的文档,如果写入很多,导致积累了大量待过期的文档,则会导致文档过期一直跟不上而一直占用着存储空间。 使用Capped集合 如果对日志保存的时间没有特别严格的要求,只是在总的存储空间上有限制,则可以考虑使用capped collection来存储日志数据,指定一个最大的存储空间或文档数量,当达到阈值时,MongoDB会自动删除capped collection里最老的文档。 db.createCollection("event", {capped: true, size: 104857600000} 定期按集合或DB归档 比如每到月底就将events集合进行重命名,名字里带上当前的月份,然后创建新的events集合用于写入,比如2016年的日志最终会被存储在如下12个集合里 events-201601 events-201602 events-201603 events-201604 events-201612 当需要清理历史数据时,直接将对应的集合删除掉 db["events-201601"].drop() db["events-201602"].drop() 不足到时候,如果要查询多个月份的数据,查询的语句会稍微复杂些,需要从多个集合里查询结果来合并 TTL index Storing Log Data MongoDB write concern 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
3.2版本复制集同步的过程参考MongoDB 复制集同步原理解析 在 3.4 版本里 MongoDB 对复制集同步的全量同步阶段做了2个改进 在拷贝数据的时候同时建立所有的索引,在之前的版本里,拷贝数据时会先建立_id索引,其余的索引在数据拷贝完之后集中建立 在拷贝数据的同时,会把同步源上新产生的oplog拉取到本地local数据库的临时集合存储着,等数据全量拷贝完,直接读取本地临时集合的oplog来应用,提升了追增量的效率,同时也避免了同步源上oplog不足导致无法同步的问题。 上图描述了这2个改进的效果,实测了『10GB的数据,包含64个集合,每个集合包含2个索引字段,文档平均1KB』,3.4版本的全量同步相比3.2版本性能约有20%的提升,如果数据集很大,并且在同步的过程中有写入,提升的效果会更明显,并且彻底解决了因同步源oplog不足而导致全量同步失败的问题。 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。 据不完全统计,目前还有很多同学在生产环境使用着 MongoDB 2.x 版本的服务,偶尔也会听到一些抱怨,但有些抱怨其实很没道理,因为抱怨的问题在最新版本的MongoDB里已经解决了,你缺的只是一次版本升级。 1. 更安全的数据库 3.x 版本默认WriteConcern 为{w:1},2.x 较早的版本为 {w: 0} 3.x 默认使用更安全的 SCRAM-SHA-1 算法鉴权,代替了2.x 版本默认的 MONGODB-CR 3.x 支持加密引擎对存储的数据进行加密 2. 更高的服务性能 mmapv1 wiredtiger DB级别锁 集合级别锁 文档级别锁 如果你使用2.x存在高并发时的性能问题,那么升级到3.x后,问题会得到极大的改善。 3. 更低的存储成本 mmapv1 wiredtiger 不支持数据压缩 不支持数据压缩 支持snappy、zlib等压缩 很多用户从 2.x 升级到 3.x + wiredtiger 后,惊奇的发现,数据量居然变小了很多,比如原来100G的数据,升级后只有30G了,这是因为wiredtiger默认使用snappy压缩,存储成本通常只有mmapv1的10%-30%左右。 4. 更快的复制 3.x 在增量同步数据时,拉取oplog和重放oplog完全流水线化,效率更高 3.4 对全量同步做了改进 在拷贝数据的时候,同时建立所有的索引(以前版本只有_id索引是在同步数据时建立的) 拷贝数据的阶段,secondary 不断拉取新的 oplog,同步效率更高,同时避免了出现oplog不足无法同步的问题。 5. 更简单、高效的分片集群 MongoDB 3.2 开始,分片集群的Config Server 也是一个复制集,之前的版本则是多个独立的mongod节点,维护起来更简单。 MongoDB 3.4 开始,分片集群的迁移由Config server负责,并支持同时发起迁移任务,迁移效率更高。 3.x 版本里还增加了其他一些很给力的功能特性,比如 部分索引,可以让索引占用的空间更小 文档校验,灵活的文档模型下 Collation,支持本地化语言排序 只读视图,让复杂的查询写起来的更简单 更强大的aggregation支持 ...... 升级步骤建议 因为2.x 到 3.x 改动很多,在升级的时候,必须先升级到3.0版本,步骤参考Upgrade MongoDB to 3.0,然后从3.0再往更高的版本升级。 虽然通过上述方式,MongoDB能做到不停机的从2.x升级到3.x,但强烈建议升级的时候,使用更保险的方式。 建立新的3.x复制集 mongodump 2.x 复制集的数据 mongorestore 到3.x复制集 等待3.x服务稳定,将2.x复制集下线 版本使用建议 (2016-12-23版) 强烈建议升级到3.2 建议升级到3.2 强烈建议使用 云数据库 MongoDB 版 基于飞天分布式系统和高性能存储,提供三节点副本集的高可用架构,容灾切换,故障迁移完全透明化。并提供专业的数据库在线扩容、备份回滚、性能优化等解决方案。
mongorocks 是基于著名的开源KV数据库RocksDB)实现的一个MongoDB存储引擎,借助rocksdb的优秀特性,mongorocks能很好的支持一些高并发随机写入、读取的应用场景。 MongoDB 与 mongorocks 的关系 mongodb 支持多种引擎,目前官方已经支持了mmapv1、wiredtiger、in-Memory等,而mongorocks则是第三方实现的存储引擎之一(对应上图红框的位置)。 MongoDB KV存储引擎模型 MongoDB 从 3.0 版本 开始,引入了存储引擎的概念,并开放了 StorageEngine 的API 接口,为了方便KV存储引擎接入作为 MongoDB 的存储引擎,MongoDB 又封装出一个 KVEngine 的API接口,比如官方的 wiredtiger 存储引擎就是实现了 KVEngine 的接口,本文介绍的 mongorocks 也是实现了KVEngine的接口。 KVEngine 主要需要支持如下接口 创建/删除集合 MongoDB 使用 KVEngine 时,将所有集合的元数据会存储到一个特殊的 _mdb_catalog 的集合里,创建、删除集合时,其实就是往这个特殊集合里添加、删除元数据。 _mdb_catalog 特殊的集合不需要支持索引,只需要能遍历读取集合数据即可,MongoDB在启动时,会遍历该集合,来加载所有集合的元数据信息。 数据存储及索引 插入新文档时,MongoDB 会调用底层KV引擎存储文档内容,并生成一个 RecordId 的作为文档的位置信息标识,通过 RecordId 就能在底层KV引擎读取到文档的内容。 如果插入的集合包含索引(MongoDB的集合默认会有_id索引),针对每项索引,还会往底层KV引擎插入一个新的 key-value,key 是索引的字段内容,value 为插入文档时生成的 RecordId,这样就能快速根据索引找到文档的位置信息。 如上图所示,集合包含{_id: 1}, {name: 1} 2个索引 用户插入文档时,底层引擎将文档内容存储,返回对应的位置信息,即 RecordId1 集合包含2个索引 插入 {_id: ObjectId1} ==> RecordId1 的索引 插入 {name: "rose"} ==> RecordId1 的索引 有了上述的数据,在根据_id访问时文档时 (根据其他索引字段类似) 根据文档的 _id 字段从底层KV引擎读取 RecordId 根据 RecordId 从底层KV引擎读取文档内容 mongorock 存储管理 mongorocks 存储数据时,每个key都会包含一个32位整型前缀,实际存储时将整型转换为big endian格式存储。 所有的元数据的前缀都是0000 每个集合、以及集合的每个索引都包含不同的前缀,集合及索引与前缀的关系存储在 0000metadata-*为前缀的key里 _mdb_catalog 在mongorocks也是一个普通的集合,有单独的前缀 创建集合、写数据 创建集合或索引时,mongrocks会为其分配一个前缀,并将对应关系持久化,比如创建集合 bar(默认会创建_id字段的索引),mongorocks 会给集合和索引各分配一个前缀,如上图所示的 0002, 0003,并将对应关系持久化。 接下来往bar集合里写的所有数据,都会带上 0002 前缀; 往其_id索引里写的数据都会带上前缀 0003; 写索引时,有个比较有意思的设计,重点介绍下 (其他的key-value引擎,如wiredtiger也使用类似的机制) MongoDB 支持复合索引,比如db.createIndex({a: 1, b, -1, c, 1}),这个索引要先按a字段升序、a相同的按b字段降序.. 依此类推,但KV引擎并没有这么强大的接口,如何实现对这种复合索引的支持呢? MongoDB针对每个索引,会有一个位图来描述索引各个字段的排序方向,比如插入如下2条索引时 (key的部分会转换为BSON格式插入到底层) {a: 100, b: 200, c: 300} == > RecordId1 {a: 100, b: 300, c: 400} ==> RecordId2 插入到底层 RocksDB,第1条记录会排在第2条记录前面,但我们建立的索引是 {a: 1, b, -1, c, 1},按这个索引,第2条记录应该排在前面才对,否则索引顺序就是错误的。 mongorocks 在存储索引数据时,会根据索引的排序位图,如果方向是逆序(如b: -1),会把key的内容里将b字段对应的bit全部取反,这样在 RocksDB 里第2条记录就会排在第1条前面。 根据_id来查找集合数据时,其他访问方式类似 根据集合的名字,在元数据里找到集合的前缀 0002 及其_id索引对应的前缀 0003 根据 0003 + 文档id 生成文档_id索引的key,并根据key读取出文档的RecordId 根据 0002 + RecordId 生成存储文档内容的key,并根据key读取出文档的内容 将集合的元数据从_mdb_catalog移除 将集合及其索引与前缀的对应关系都删除掉 将第2步里删除的前缀加入到待删除列表,并通知 RocksDB 把该前缀开头的所有key通过compact来删除掉(通过定制CompactionFilter来实现,这个compact过程是异步做的,所以集合删了,会看到底层的数据量不会立马降下来),同时持久化一条0000droppedprefix-被删除前缀的记录,这样是防止compact被删除前缀的过程中宕机,重启后被删除前缀的key不会被会收掉,直到待删除前缀所有的key都被回收时,最终会把0000droppedprefix-被删除前缀的记录删除掉。 文档原子性 MongoDB 写入文档时,包含如下步骤 插入文档到集合 更新集合所有的索引 记录oplog(如果是复制集模式运行) MongoDB 保证单文档的原子性,上述3个步骤必须全部成功应用或者全部不应用,mongorocks 借助 RocksDB 的 WriteBatch 接口来保证,将上述3个操作放到一个WriteBatch中,最后一次提交,RocksDB 层面会保证 WriteBatch 操作的原子性。 特殊的oplog 在MongoDB里,oplog是一个特殊的 capped collection(可以理解为环形存储区域),超过配置的大小后,会将最老的数据删除掉,如下是2个oplog的例子,mongorocks在存储oplog时,会以oplog集合前缀 + oplog的ts字段作为key来存储,这样在RocksDB,oplog的数据都是按ts字段的顺序来排序的。 0008:ts_to_uint64 ==> { "ts" : Timestamp(1481860966, 1), "t" : NumberLong(71), "h" : NumberLong("-6964295105894894386"), "v" : 2, "op" : "i", "ns" : "test.tt", "o" : { "_id" : ObjectId("58536766d38c0573d2ff5b90"), "x" : 2000 } } 0008:ts_to_uint64 ==> { "ts" : Timestamp(1481860960, 1), "t" : NumberLong(71), "h" : NumberLong("3883981042971627762"), "v" : 2, "op" : "i", "ns" : "test.tt", "o" : { "_id" : ObjectId("58536760d38c0573d2ff5b8f"), "x" : 1000 } } capped collection 当集合超出capped集合最大值时,就会逐个遍历最先写入的数据来删除,直到空间降到阈值以下。 mongorocks 为了提升回收oplog的效率,做了一个小的优化。 针对oplog集合,插入的每一个文档,除了插入数据本身,还会往一个特殊的集合(该集合的前缀为oplog集合的前缀加1)里插入一个相同的key,value为文档大小。比如 0008:ts_to_uint64 ==> { "ts" : Timestamp(1481860966, 1), "t" : NumberLong(71), "h" : NumberLong("-6964295105894894386"), "v" : 2, "op" : "i", "ns" : "test.tt", "o" : { "_id" : ObjectId("58536766d38c0573d2ff5b90"), "x" : 2000 } } 0009:ts_to_uint64 ==> 88 (假设88为上面这个文档的大小) 有了这个信息,在删除oplog最老的数据时,就可以先遍历包含oplog文档大小信息的集合,获取被删除文档的大小,而不用把整个oplog的key-value都读取出来,然后统计大小。个人觉得这个优化当oplog文档大小比较大效果会比较好,文档小的时候并不一定能有效。 集合大小元数据管理 MongoDB 针对collection的count()接口,如果是全量的count,默认是O(1)的时间复杂度,但结果不保证准确。mongorocks 为了兼容该特性,也将每个集合的『大小及文档数』也单独的存储起来。 比如集合foo的大小、文档数分别对应2个key 0000datasize-foo ==> 14000 (0000是metadata的前缀) 0000numrecords-foo ==> 100 上面2个key,当集合里有增删改查时,默认并不是每次都更新,而是累计到一定的次数或大小时更新,后台也会周期性的去更新所有集合对应的这2个key。 mongorocks 也支持每次操作都将 datasize、numrecords 的更新进行持久化存储,配置storage.rocksdb.crashSafeCounters参数为true即可,但这样会对写入的性能有影响。 借助 RocksDB 本身的特性,mongorocks能很方便的支持对数据进行物理备份,执行下面的命令,就会将产生一份快照数据,并将对应的数据集都软链接到/var/lib/mongodb/backup/1下,直接拷贝该目录备份即可。 db.adminCommand({setParameter:1, rocksdbBackup: "/var/lib/mongodb/backup/1"}) 总体来说,MongoDB 存储引擎需要的功能,mongorocks 都实现了,但因为 RocksDB 本身的机制,还有一些缺陷,比如 集合的数据删除后,存储空间并不是立即回收,RocksDB 要通过后台压缩来逐步回收空间 mongorcks 对 oplog 空间的删除机制是在用户请求路径里进行的,这样可能导致写入的延迟上升,应像 wiredtiger 这样当 oplog 空间超出时,后台线程来回收。 RocksDB 缺乏批量日志提交的机制,无法将多次并发的写log进行合并,来提升效率。
MongoDB 3.2.9 版本在 wiredtiger 上做了很多改进,但不幸的时,这个版本引入了一个新的 bug,持续大量 insert/update 场景,有一定的可能导致 wiredtiger 进入 deadlock,MongoDB 官方迅速的在3.2.10里修复了该问题,该版本在 wiredtiger 内存使用上也做了控制,尽量避免了因为内存碎片导致 wiredtiger 内存使用远超出 cacheSizeGB 配置的问题,目前 MongoDB 3.2.10+ 的版本已经非常稳定。 MongoDB 目前有4个可配置的参数来支持 wiredtiger 存储引擎的 eviction 策略调优,其含义是: eviction_target 当 cache used 超过 eviction_target,后台evict线程开始淘汰 CLEAN PAGE eviction_trigger 当 cache used 超过 eviction_trigger,用户线程也开始淘汰 CLEAN PAGE eviction_dirty_target 当 cache dirty 超过 eviction_dirty_target,后台evict线程开始淘汰 DIRTY PAGE eviction_dirty_trigger 当 cache dirty 超过 eviction_dirty_trigger, 用户线程也开始淘汰 DIRTY PAGE 上述默认值是在3.2.10版本里调整的,如果你正在使用 MongoDB 3.0/3.2,遇到了 wiredtiger 相关问题(绝大部分场景遇不到),可以先升级到3.2的最新版本。 在此基础上(使用3.2.10+),如果通过 mongostat 发现 used、dirty 持续超出eviction_trigger、eviction_dirty_trigger,这时用户的请求线程也会去干 evict的事情(开销大),会导致请求延时上升,这时基本可以判定,mongodb 已经存在资源不足的问题,即用户读写『从磁盘上读取的数据的速率』 远远 超出了 『mongodb 将数据从内存淘汰出去速率』,可以做的优化包括: 增强 IO 能力 SATA 盘升级到 SSD 将 wiredtiger 的数据和 journal 分到不同的盘上 扩充机器内存,加大 wiredtiger cache cache 越大,越能平衡上述2个速率的差距 eviction 参数调优 降低eviction_target 或 eviction_dirty_target,让evict 尽早将数据从 wiredtiger 的 cache 刷到操作系统的 page cache,以便提早刷盘。 db.runCommand({setParameter: 1, wiredTigerEngineRuntimeConfig: "eviction_dirty_target=5,eviction_target=80"}) 接下来分析一下 MongoDB 3.2.9 bug 产生的原因,想了解源码的往下看 当用户请求打开wiredtiger cursor 的时候,会检查是否需要 进行 cache 淘汰,当 『cache 使用百分比超出 eviction_trigger』 或者 『cache 脏页百分比超过 eviction_dirty_triger』,用户请求线程就会进入到 cache 淘汰逻辑,执行__wt_cache_eviction_worker static inline bool __wt_eviction_needed(WT_SESSION_IMPL *session) { bytes_inuse = __wt_cache_bytes_inuse(cache); bytes_max = conn->cache_size + 1; // Avoid division by zero pct_full = (u_int)((100 * bytes_inuse) / bytes_max); if (pct_full > cache->eviction_trigger) return true; if (__wt_cache_dirty_inuse(cache) > (cache->eviction_dirty_trigger * bytes_max) / 100) return (true); return false; 用户线程执行 __wt_cache_eviction_worker 会持续的检查 __wt_eviction_needed 条件是否满足,不需要 evict 时,用户线程就会继续响应请求;如果需要evict,就会从 evict queue 里取 page 进行淘汰,当 evict queue 为空时,用户线程 wait 一段时间继续重复上述逻辑。 __wt_cache_eviction_worker(WT_SESSION_IMPL *session, bool busy, u_int pct_full) { for (;;) { /* See if eviction is still needed. */ if (!__wt_eviction_needed(session, busy, &pct_full) || (pct_full < 100 && cache->pages_evict > init_evict_count + max_pages_evicted)) return (0); /* Evict a page. */ switch (ret = __evict_page(session, false)) { case 0: if (busy) return (0); /* FALLTHROUGH */ case EBUSY: break; case WT_NOTFOUND: /* Allow the queue to re-populate before retrying. */ __wt_cond_wait( session, conn->evict_threads.wait_cond, 10000); cache->app_waits++; break; 后台的 evict server 线程会遍历 wiredtiger 的 btree 页,将满足条件的的 page 加入到 evict queue 并进行淘汰,每一轮都会通过 __evict_update_work 更新当前的工作状态信息,并告知调用者是否还需要继续执行 evict。 // 这个就是执行上述表格中描述的逻辑 // WT_CACHE_EVICT_CLEAN 标记代表后台线程需要淘汰 CLEAN PAGE // WT_CACHE_EVICT_CLEAN_HARD 代表用户线程也需要去淘汰 CLEAN PAGE // DIRTY* 参数类似 static bool __evict_update_work(WT_SESSION_IMPL *session) bytes_max = conn->cache_size + 1; bytes_inuse = __wt_cache_bytes_inuse(cache); if (bytes_inuse > (cache->eviction_target * bytes_max) / 100) F_SET(cache, WT_CACHE_EVICT_CLEAN); if (__wt_eviction_clean_needed(session, NULL)) F_SET(cache, WT_CACHE_EVICT_CLEAN_HARD); dirty_inuse = __wt_cache_dirty_leaf_inuse(cache); if (dirty_inuse > (cache->eviction_dirty_target * bytes_max) / 100) F_SET(cache, WT_CACHE_EVICT_DIRTY); if (__wt_eviction_dirty_needed(session, NULL)) F_SET(cache, WT_CACHE_EVICT_DIRTY_HARD); return (F_ISSET(cache, WT_CACHE_EVICT_ALL | WT_CACHE_EVICT_URGENT)); __evict_update_work 最后通过 F_ISSET(cache, WT_CACHE_EVICT) 来判断是否要继续 evict #define WT_CACHE_EVICT_ALL (WT_CACHE_EVICT_CLEAN | WT_CACHE_EVICT_DIRTY) 这样可能会出现一种情况,eviction_trigger 或 eviction_dirty_trigger 触发了,这时后台线程是需要继续进行 evict 的,但eviction_target、eviction_ditry_target都不满足,导致上述判断条件返回 false,后台线程不继续干活,这样就不会有新的 page 加入到 evict queue,而上述用户线程还在继续等待 evict,一直不会返回,这样就会导致请求 hang 。 修复上述问题的主要代码如下:github commit 主要修改逻辑是,当 used 超过eviction_trigger时,同时也设置WT_CACHE_EVICT_CLEAN标记(DIRTY 类似),这样确保有用户线程在等时,evict 一定会进行。 static bool __evict_update_work(WT_SESSION_IMPL *session) bytes_max = conn->cache_size + 1; bytes_inuse = __wt_cache_bytes_inuse(cache); if (bytes_inuse > (cache->eviction_target * bytes_max) / 100) F_SET(cache, WT_CACHE_EVICT_CLEAN); if (__wt_eviction_clean_needed(session, NULL)) - F_SET(cache, WT_CACHE_EVICT_CLEAN_HARD); + F_SET(cache, WT_CACHE_EVICT_CLEAN | WT_CACHE_EVICT_CLEAN_HARD); dirty_inuse = __wt_cache_dirty_leaf_inuse(cache); if (dirty_inuse > (cache->eviction_dirty_target * bytes_max) / 100) F_SET(cache, WT_CACHE_EVICT_DIRTY); if (__wt_eviction_dirty_needed(session, NULL)) - F_SET(cache, WT_CACHE_EVICT_DIRTY_HARD); + F_SET(cache, WT_CACHE_EVICT_DIRTY | WT_CACHE_EVICT_DIRTY_HARD); return (F_ISSET(cache, WT_CACHE_EVICT_ALL | WT_CACHE_EVICT_URGENT)); 高性能,官方号称 100x faster,因为可以全内存运行,性能提升肯定是很明显的 简单易用,支持 Java、Python、Scala、SQL 等多种语言,使得构建分析应用非常简单 统一构建 ,支持多种数据源,通过 Spark RDD 屏蔽... MongoDB oplog (类似于 MySQL binlog) 记录数据库的所有修改操作,除了用于主备同步;oplog 还能玩出很多花样,比如 全量备份 + 增量备份所有的 oplog,就能实现 MongoDB 恢复到任意时间点的功能 通过 oplog,除了实现到备节点的同步,也可以额外再往单独的集群同步数据(甚至是异构的数据库),实现容灾、多活等场景,比如阿里云开源的 MongoShake 就能实现基于 oplog 的增量同步。
授人以渔,胜过授人以鱼,使用aggregate
你好,非常感谢你关注我的文章
以正确方式连接复制集来保证高可用是指(以写操作为例说明)
当你后端的复制集有成员故障时,可能会选出新的primary,这时driver会自动检测到后端节点宕机时,会获取到最新的副本集状态,然后向新的primary写入,这样应用程序就能继续写入。
但如果使用driver时,仅仅只是『正确连接副本集』是远远不够的,应用程序该做的基本逻辑还是要有的,不然也是无法保证高可用。
按你描述的场景,正确连接副本集后,如果你做了上述1、2的工作,是完全可以保证服务高可用,数据高可靠的。
driver连接副本集时,连接地址里指定所有成员的信息,当副本集有成员宕机时,driver会自动进行failover,连接到新的primary。
driver连接分片集群时,连接地址里指定多个mongos的信息,当有mongos宕机时,driver会自动连接可用的其他mongos。
可能是你的查询条件并没有匹配的文档,而你有设置了第3个参数upsert为true,当update没找到匹配条件的文档时,会将新的文档insert到集合。
嵌套数组目前mongodb支持比较弱,暂不能满足你的需求;你的文档结构比较复杂,应该重新再review下你的设计方案是否有可以改进的地方。
db.collection.update( {name: "join"}, { $set: {name: "name john"} }, {multi: true} )
目前阿里云MongoDB、redis已支持vpc,你在控制台购买实例的时候就能选择将实例加入到某个vpc网络里(如果还没有专有网络,需要先创建)
补充一下楼上的,索引通常会常驻内存,但如果实例配置的内存不足,索引也是需要先从硬盘加载的;
如果需要对用户、时间2个维度分别进行查询,则需要针对2个维度分别建立索引。索引的原理参考: https://yq.aliyun.com/articles/33726?spm=0.0.0.0.PsBXc4&msgid=12861
2.6版本支持了$position操作符,可以实现这个功能
https://docs.mongodb.org/manual/reference/operator/update/push/
针对uid字段建立索引,遍历[10, 9, 1, 12...]列表,逐个根据uid查询文档,使用in的查询效率很低的,无法使用索引,而且每次比较都要遍历数组。
OperationFailure: could not find host matching read preference { mode: "primary", tags: [ {} ] } for set rs1
请确认下rs1这个副本集当前的primary是否正常
备份本质上是『遍历所有的数据库,所有的集合,将文档导出』,这些用mongodb的java driver都能实现的。官方已经有比较完备的工具支持,如mongodump, mongorestore, mongooplog等工具,没必要再用java重复造轮子