数据库开发系列


文章目录

  • 数据库开发系列
  • 前言
  • 一、MongoDB存储引擎
  • 二、MongoDB 复制(副本集)
  • 三、为什么需要分片集群架构
  • 四、高可用分片集群架构(+复制集)
  • 总结

前言

数据库的演进
随着计算机的发展,越来越多的数据需要被处理,数据库是为处理数据而产生。从概念上来说,数据库是指以一定的方式存储到一起,能为多个用户共享,具有更可能小的冗余,与应用程序彼此独立的数据集合。从功能上来说,就是数据管理软件。

到了2000年随着互联网的发展,数据量呈现爆发式增长。海量数据的诞生,传统的关系型数据库在应对大规模,超大流量的时候就显得力不从心。借此,NoSQL数据库跟NewSQLl数据库就此登场。

NoSQL 全称 “not only sql”,所有非关系型的数据库都统称为NoSQL数据库,NoSQL数据库主要有四种类型,文档数据库(MongoDB为代表),键值数据库(Redis为代表),宽列存储(Hbase为代表)和图形数据库(Neo4J为代表)。

NewSQL数据库,提供了与NoSQL相同的扩展性,但仍属于关系型模型,还保留SQL作为查询语言,保证了ACID事务性。代表数据库有Spanner,CockroachDB。

数据库发展到今天,可以说是百花齐放,随着2020年阿里云数据库进入全球魔力四象限的Leader象限,这也是中国数据库40年来首次进入全球顶级数据库行列,标志着国产数据库开始崛起。

数据库索引技术
数据库是用来处理数据的,那高效的存储、检索数据,是数据库管理系统必备的技能。数据库中用于提高检索效率很重要的一项技术就是索引。

数据结构指的是“一组数据的存储结构”,算法指的是“操作数据的一组方法”。数据结构是为算法服务的,算法是要作用在特定的数据结构上的。数据库提高检索效率使用的是索引,数据库索引是一种树形数据结构,如MongoDB使用的是B-tree、MySQL使用的是B+tree。
参考https://blog.csdn.net/dbaxiaosa/article/details/127750957

MongoDB介绍
本文重点介绍MongoDB,它虽然诞生于NoSQL,但4.0版本之后,已经不仅仅是NoSQL了,可以与上文中任何一位NewSQL的代表PK。MongoDB是一个开源、高性能、无模式的文档数据库,旨在简化开发和扩展。


一、MongoDB存储引擎

存储引擎(Storage Engine)是MongoDB的核心组件,负责管理数据如何存储在硬盘(Disk)和内存(Memory)上。从MongoDB 3.2 版本开始,MongoDB 支持多数据存储引擎(Storage Engine),MongoDB支持的存储引擎有:WiredTiger,MMAPv1和In-Memory。

mongod 参数: –storageEngine wiredTiger | inMemory
如果参数值是wiredTiger,MongoDB使用的存储引擎是WiredTiger,将数据持久化存储在Disk Files中;
如果参数值是inMemory,MongoDB使用的存储引擎是In-Memory,将数据存储在内存中;
从MongoDB 3.2 版本开始,MongoDB默认的存储引擎是WiredTiger;

WiredTiger和MMAPv1都用于持久化存储数据,相对而言,WiredTiger比MMAPv1更新,功能更强大。
WiredTiger是在MongoDB3.0版本引入的,并且在MongoDB3.2版本开始成为MongoDB默认的存储引擎。相比较MMAPv1,WiredTiger功能更强大,而且具有更高的性能。3.0版本之前是采用的 MongoDB的MMAPv1存储引擎。WiredTiger提供文档级别(Document-Level)的并发控制,检查点(CheckPoint),数据压缩和本地数据加密( Native Encryption)等功能。
1、文档级别的并发控制
MongoDB在执行写操作时,WiredTiger 在文档级别进行并发控制,就是说,在同一时间,多个写操作能够修改同一个集合中的不同文档;当多个写操作修改同一个文档时,必须以序列化方式执行;这意味着,如果该文档正在被修改,其他写操作必须等待,直到在该文档上的写操作完成之后,其他写操作相互竞争,获胜的写操作在该文档上执行修改操作。
对于大多数读写操作,WiredTiger使用乐观并发控制(optimistic concurrency control),只在Global,database和Collection级别上使用意向锁(Intent Lock),如果WiredTiger检测到两个操作发生冲突时,导致MongoDB将其中一个操作重新执行,这个过程是系统自动完成的。
2、检查点
在Checkpoint操作开始时,WiredTiger提供指定时间点(point-in-time)的数据库快照(Snapshot),该Snapshot呈现的是内存中数据的一致性视图。当向Disk写入数据时,WiredTiger将Snapshot中的所有数据以一致性方式写入到数据文件(Disk Files)中。一旦Checkpoint创建成功,WiredTiger保证数据文件和内存数据是一致性的,因此,Checkpoint担当的是还原点(Recovery Point),Checkpoint操作能够缩短MongoDB从Journal日志文件还原数据的时间。
当WiredTiger创建Checkpoint时,MongoDB将数据刷新到数据文件(Disk Files)中,在默认情况下,WiredTiger创建Checkpoint的时间间隔是60s,或产生2GB的Journal文件。在WiredTiger创建新的Checkpoint期间,上一个Checkpoint仍然是有效的,这意味着,即使MongoDB在创建新的Checkpoint期间遭遇到错误而异常终止运行,只要重启,MongoDB就能从上一个有效的Checkpoint开始还原数据。
当MongoDB以原子方式更新WiredTiger的元数据表,使其引用新的Checkpoint时,表明新的Checkpoint创建成功,MongoDB将老的Checkpoint占用的Disk空间释放。使用WiredTiger 存储引擎,如果没有记录数据更新的日志,MongoDB只能还原到上一个Checkpoint;如果要还原在上一个Checkpoint之后执行的修改操作,必须使用Jounal日志文件。
3、预先记录日志
WiredTiger使用预写日志的机制,在数据更新时,先将数据更新写入到日志文件,然后在创建Checkpoint操作开始时,将日志文件中记录的操作,刷新到数据文件,就是说,通过预写日志和Checkpoint,将数据更新持久化到数据文件中,实现数据的一致性。WiredTiger 日志文件会持久化记录从上一次Checkpoint操作之后发生的所有数据更新,在MongoDB系统崩溃时,通过日志文件能够还原从上次Checkpoint操作之后发生的数据更新。

文件空间分配方式改进
MMAPv1存储引擎是在数据库级别分配文件的,将每个数据库中所有的集合和索引都混合存储在数据库文件中,即使删除了某个集合或索引,其占用的磁盘空间也很难及时自动回收。WiredTIger则在集合和索引级别分配文件,将每个数据库中所有的集合和索引都存储在单独的文件中,集合或索引删除后,其对应文件即可删除,磁盘空间回收方便。
WiredTiger的一些数据文件:
mongod.lock:用于防止多个进程连接同一个WiredTiger数据库
.wt文件:存储各个集合的数据,每个文件100MB
WiredTiger.wt:用于存储所有集合的元数据信息
WiredTiger.turtle:用于存储WiredTiger.wt的元数据信息
journal文件夹:用于存储日志文件(Write ahead log)

WiredTiger对内存的使用情况
wiredTiger对内存使用会分为两大部分,一部分是内部内存,另外一部分是文件系统的缓存。内部内存默认值有一个计算公式{ 50% of(RAM-1GB) ,or256MB },索引和集合的内存都被加载到内部内存,索引是被压缩的放在内部内存,集合则没有压缩。wiredTiger会通过文件系统缓存,自动使用其他所有的空闲内存,放在文件系统缓存里面的数据,与磁盘上的数据格式一致,可以有效减少磁盘I/O。

你需要分析是否对默认的内存做调优。一条比较好的原则就是wt的缓存足够大,能够缓存整个应用的工作集。
查看wt的缓存统计信息:
db.serverStatus().wiredTiger.cache
文档级别的并发控制
WiredTiger存储引擎使用文档级别锁,同一时刻多个写操作可以修改同一个集合中不同的文档,但不能修改同一个文档。这使得WiredTiger存储引擎的并发处理能力比MMAPv1更好。

通过检查点和预写日志实现数据持久化
按照MongoDB默认的配置,WiredTiger的写操作会先写入Cache(BTree结构),当Cache大小达到128KB时便将其持久化到预写日志文件(Write ahead log)。WiredTiger每60s或日志文件大小达到2GB时会做一次检查点Checkpoint,产生指定时间点的数据库快照(内存中数据的一致性视图),将快照中的所有数据以一致性方式持久化到数据文件中,保证数据文件和内存数据是一致的。Wiredtiger连接初始化时,首先将数据恢复至最新的快照状态,然后根据预写日志文件恢复数据,以保证存储可靠性。
Journal是一种预写式日志(write ahead log)机制,主要用来弥补CheckPoint机制的不足,如果开启了Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定Journal:true,写操作都会触发日志的持久化。一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据。

WiredTiger写入数据的流程:
1、应用向MongoDB写入数据(插入、修改或删除)
2、数据库从内部缓存中获取当前记录所在的业块,如果不存在则会从磁盘中加载(Buffer I/O)
3、WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变
4、如果开启了Journal日志,则在写数据的同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms后写入磁盘。
5、数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。

内存使用上限可配置
使用WiredTiger存储引擎时,MongoDB数据缓存分两部分:内部缓存和文件系统缓存。内部缓存大小可以使用–wiredTigerCacheSizeGB参数来设置,默认值为:1GB或RAM的60%到1GB之间,取两值中较大者。文件系统缓存大小则不固定,MongoDB自动使用系统空闲的内存,且数据在文件系统缓存中是压缩存储的。

数据压缩
使用WiredTiger存储引擎时,数据库的集合与索引、日志文件都是压缩存储的,节省了磁盘空间。WiredTiger默认情况下,集合数据使用块压缩算法,索引数据则使用前缀压缩算法。这使得数据占用磁盘空间少,读写速度快,花费I/O时间少。
集合中数据比较少时,压缩和无压缩的写性能相单,无压缩读性能反而比压缩读性能好。当集合数据很多时,压缩的读写性能则都要比无压缩的读写性能好。

二、MongoDB 复制(副本集)

MongoDB复制是将数据同步在多个服务器的过程。
复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。
复制还允许您从硬件故障和服务中断中恢复数据。
注:MongoDB复制是异步的,若配置了从Secondary读取数据,则需要业务接受数据延迟。

为什么需要复制” />

创建分片语法

//创建mongo实例mongod--bind_ip "0.0.0.0" --port "PORT" --dbpath "YOUR_DB_DATA_PATH" --logpath "YOUR_LOG_PATH" --replSet "REPLICA_SET_INSTANCE_NAME"--logappend --fork//启动一个新的副本集rs.initiate()//添加副本集rs.add(HOST_NAME:PORT)//查看副本集状态使用 rs.status() //查看副本集的配置rs.conf()

参数解析

--oplogSize 日志操作文件的大(默认磁盘剩余空间的5%--dbpath 数据文件路径--logpath日志文件路径--port端口号,默认是27017--replSet 复制集的名字,一个replica sets中的每个节点的这个参数都要用一个复制集名字,这里是test.--replSet test/这个后面跟的是其他standard节点的ip和端口--maxConns 最大连接数--fork 后台运行--logappend 日志文件循环使用,如果日志文件已满,那么新日志覆盖最久日志。

端口资源分配:本实例一主一从,节点数为偶数,因此需要仲裁节点
Primary:IP:27017
Secondary:IP2:27017
Arb:IP3:27017

1. 主机A创建复制集rs0(10.20.40.33)# mkdir /data/log# mkdir /data/db1# nohup mongod --port 27017 --dbpath=/data/db1 --logpath=/data/log/rs0-1.log --logappend --fork --shardsvr --replSet=rs0 &2. 主机B创建复制集rs0 (10.20.40.34)# mkdir /data/db2# nohup mongod --port 27017 --dbpath=/data/db2 --logpath=/data/log/rs0-2.log --logappend --fork --shardsvr --replSet=rs0 &3. 已经初始过的rs,可以新增复制集(10.20.40.3427017# mongo> rs.initiate({_id:"rs0",members:[{_id:0,host:"10.20.40.33:27017"}]})> rs.add("10.20.40.34:27017")4. 主机A登录,一下子初始化好复制集rs0配置(或者方式4等价)# mongo> rs.initiate({_id: 'rs0', members: [{_id: 0, host: '10.20.40.33:27017',priority :10}, {_id: 1, host: '10.20.40.33:27017',priority :1}]}) > rs.isMaster() > rs.conf()> rs.status()重新配置rs0,且带有优先级# mongo> config = {_id: 'rs0', members: [{_id: 0, host: '10.20.40.33:27017',priority :10}, {_id: 1, host: '10.20.40.33:27017',priority :1}]};> rs.reconfig(config); 5.移除分片rs.remove("10.20.40.34:27017")6.当节点数为偶数时,需要添加仲裁者节点rs.addArb("10.20.40.33:27017")7.当登录从节点时,需要执行如下命令,才可以看到同步主节点的消息use admindb.getMongo().setSecondaryOk()8.可以关闭节点db.shutdownServer()9.shell中添加节点,命令mongo --eval 'rs.reconfig(_id:"rs0",members:[{_id:0,host:'10.20.40.131:27017'},{_id:1,host:'10.20.40.145:27017'}])';

注意:
1、主节点进行读写,从节点默认不能读,需要设定后才可以,访问从节点后,执行db.getMongo().setSecondaryOk()
2、主节点登录后,use admin,db.shutdownServer(),exit后,发现从节点由secondary变为primary
3、故障转移后,主节点的IP地址发生变化。因此需要客户端程序来处理这种IP变化。

三、为什么需要分片集群架构

当业务遇到如下问题时,可以使用分片集群解决:
1、存储容量受单机限制,即磁盘资源遭遇瓶颈。
2、读写能力受单机限制,可能是CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。

关于负载均衡
MongoDB分片集群的自动负载均衡目前是由mongos的后台线程来做,并且每个集合同一时刻只能有一个迁移任务。负载均衡主要根据集合在各个shard上chunk的数量来决定的,相差超过一定阈值(和chunk总数量相关)就会触发chunk迁移。

负载均衡默认是开启的,为了避免chunk迁移影响到线上业务,可以通过设置迁移执行窗口,例如只允许凌晨02:00~06:00期间进行迁移。

use configdb.settings.update({ _id: "balancer" },{ $set: { activeWindow : { start : "02:00", stop : "06:00" } } },{ upsert: true })

四、高可用分片集群架构(+复制集)

一个 MongoDB分片集群由以下组件组成:
1、shard:每个分片包含分片数据的一个子集。每个分片都可以部署为副本集。
2、mongos:mongos充当查询路由器,在客户端应用程序和分片集群之间提供接口。
3、config servers:配置服务器存储集群的元数据和配置设置。从 MongoDB 3.4 开始,配置服务器必须部署为副本集 (CSRS)。
总之:shard、config servers组建部署副本集保障高可用,mongos是单点架构,可以部署多个节点保障高可用。

参照关系型数据库,我们来看看MongoDB中的对象。

部署测试(待完成):

端口资源分配
Shard Server 1:27020
Shard Server 2:27021
Shard Server 3:27022
Shard Server 4:27023
Config Server :27100
Route Process:40000

1. 创建Sharding复制集 rs0# mkdir /data/log# mkdir /data/db1# nohup mongod --port 27020 --dbpath=/data/db1 --logpath=/data/log/rs0-1.log --logappend --fork --shardsvr --replSet=rs0 &# mkdir /data/db2# nohup mongod --port 27021 --dbpath=/data/db2 --logpath=/data/log/rs0-2.log --logappend --fork --shardsvr --replSet=rs0 &1.1 复制集rs0配置# mongo localhost:27020 > rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'localhost:27020'}, {_id: 1, host: 'localhost:27021'}]}) > rs.isMaster() 2. 创建Sharding复制集 rs1# mkdir /data/db3# nohup mongod --port 27030 --dbpath=/data/db3 --logpath=/data/log/rs1-1.log --logappend --fork --shardsvr --replSet=rs1 &# mkdir /data/db4# nohup mongod --port 27031 --dbpath=/data/db4 --logpath=/data/log/rs1-2.log --logappend --fork --shardsvr --replSet=rs1 &2.1 查看主从关系# mongo localhost:27030> rs.initiate({_id: 'rs1', members: [{_id: 0, host: 'localhost:27030'}, {_id: 1, host: 'localhost:27031'}]})> rs.isMaster() 3. 创建Config复制集 conf# mkdir /data/conf1# nohup mongod --port 27100 --dbpath=/data/conf1 --logpath=/data/log/conf-1.log --logappend --fork --configsvr --replSet=conf &# mkdir /data/conf2# nohup mongod --port 27101 --dbpath=/data/conf2 --logpath=/data/log/conf-2.log --logappend --fork --configsvr --replSet=conf &3.1 复制集conf配置# mongo localhost:27100> rs.initiate({_id: 'conf', members: [{_id: 0, host: 'localhost:27100'}, {_id: 1, host: 'localhost:27101'}]})> rs.isMaster() 4. 创建Route即mongos,其依赖部署的configdbnohup mongos --port 40000 --configdb conf/localhost:27100,localhost:27101 --fork --logpath=/data/log/route.log --logappend & 4.1 设置分片# mongo localhost:40000> use admin> db.runCommand({ addshard: 'rs0/localhost:27020,localhost:27021'})> db.runCommand({ addshard: 'rs1/localhost:27030,localhost:27031'})> db.runCommand({ enablesharding: 'test'})> db.runCommand({ shardcollection: 'test.user', key: {name: 1}})

总结

通过本文的学习,应该对mongodb的分布式架构有了深刻了理解