You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

753 lines
22 KiB
Markdown

## 数据模型设计
### 1. 模型设计基础
- 数据模型设计的元素
- 实体
- 描述业务的主要数据集合
- 谁, 什么, 何时, 何地, 为何, 如何
- 属性
- 描述实体里面的单个信息
- 关系
- 描述实体与实体之间的数据规则
- 结构规则: 1-N, N-1, N-N
- 引用规则: 电话号码不能单独存在
- 传统模型设计
- ![传统模型设计.png](pic/传统模型设计.png)
- 架构师要产出 LDM 给开发者
- 逻辑模型
- 实体, 属性, 函数名称, 实体间关系
- 开发者: 第三范式下的物理模型
- 数据在库里尽量不可能存在冗余
### 2. JSON 文档模型的设计特点
- MongoDB 文档模型设计的三个误区
- 不需要模型设计
- MongoDB 应用用一个超级大文档来组织所有数据
- MongoDB 不支持关联或者事务
- 关于 JSON 文档模型设计
- 文档模型设计处于是物理模型设计阶段 (PDM)
- JSON 文档模型通过内嵌数组或引用字段来表示关系
- 文档模型设计不遵从第三范式, 允许冗余
- 流程: 概念建模 -> 逻辑建模 -> 文档模型 / 关系模型
- 为什么人们都说 MongoDB 是无模式
- 严格来说, MongoDB 同样需要概念/逻辑建模
- 文档建模设计的物理层结构可以和逻辑层类似
- MongoDB 无模式由来:
- **可以省略物理建模的具体过程**
- 概念模型 -> 逻辑模型 -(复用)-> 物理模型
- JSON 模型和逻辑模型对比
- ![JSON 模型和逻辑模型对比.png](pic/JSON%20模型和逻辑模型对比.png)
- **文档模型的设计原则: 性能和易用**
- 关系模型 vs 文档模型
- ![关系模型 vs 文档模型.png](pic/关系模型%20vs%20文档模型.png)
### 3. 文档模型三步走
- MongoDB 文档模型设计三步曲 - 总览
- ![MongoDB文档模型设计三步曲.png](pic/MongoDB文档模型设计三步曲.png)
- **第一步: 建立基础文档模型**
- 根据概念模型或者业务需求推导出逻辑模型 - 找到对象
- 列出实体之间的关系(及基数) - 明确关系
- 套用逻辑设计原则来决定内嵌方式 - 进行建模
- 完成基础模型构建
- 业务需求及逻辑模型 -(逻辑导向)-> 基础建模 -> 集合/字段/基础形状
- 举例
- 找到对象:
- Contacts
- Groups
- Address
- Portraits
- 明确关系
- 一个联系人有一个头像(1-1)
- 一个联系人可以有多个地址(1-N)
- 一个联系人可以属于多个组, 一个组可以有多个联系人(N-N)
- 表示:
- Groups(name) <-[N-N]-> Contacts (name,company,title) <-[1-N]-> Addresses(type,street,city,state,zip_code)
- Contacts(...) <-[1-1]-> Portraits(mine_type,date)
- 1-1 关系建模: portraits
- 基本原则: **一对一关系以内嵌为主, 作为子文档形式或者直接在顶级不涉及到数据冗余**
- 例外情况: **如果内嵌后导致文档大小超过16MB**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
}
```
- 1-N 关系建模: Addresses
- 基本原则: **一对多关系同样以内嵌为主, 用数组来表示一对多不涉及到数据冗余**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
- Groups(name) <-[N-N]->
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
}
```
- N-N 关系建模: 内嵌数组模式
- 基本原则: **不需要映射, 一般用内嵌数组来表示一对多, 通过冗余来实现 N-N**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- **第二步: 根据读写工况细化**
- 技术需求, 读写比例, 方式及数量 -(技术导向)-> 工况细化 -> [引用及关联]
- 考虑的问题
- 最频繁的数据查询模式
- 最常用的查询参数
- 最频繁的数据写入模式
- 读写操作的比例
- 数据量的大小
- 基于内嵌的文档模型
- 根据业务需求
- 使用引用来避免性能瓶颈
- 使用冗余来优化访问性能
- 举例
- 联系人管理应用分组需求
- 用于客户营销
- 有千万级联系人
- 需要频繁变动分组的信息, 如增加分组及修改名称及描述以及营销状态
- 一个分组可以有百万级联系人
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- 一个分组(groups)的改动意味着百万级的DB操作
- 解决方案: Group 使用单独的集合
- 类似于关系型设计
- 用 id 或者唯一键关联
- 使用 $lookup 来提供一次查询多表的能力(类似关联)
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Groups
group_id
name
```
- 使用 group_id 进行关联
- 此时查询语句
```spring-mongodb-json
db.contacts.aggregate([
{
$lookup:{
from: "groups",
localField: "group_ids",
foreignField: "group_id",
as: "groups"
}
}])
```
- aggregate 的 $lookup 来实现关联查询
- 例子2: 联系人的头像: 引用模式
- 头像使用高保真, 大小在 5MB - 10MB
- 头像一旦上传, 一个月不可变更
- 基础信息查询(不含头像) 和 头像查询的比例为 9 1
- 建议: 使用引用方式, 把头像数据放到另外一个集合, 可以显著提升 90% 的查询效率
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Contact_Portrait: {
_id: 123,
mimetype: xxx,
data: xxxx
}
```
- 什么时候该使用引用方式?
- 内嵌文档太大, 数 MB 或者超过 16MB
- 内嵌文档或数组元素会频繁修改
- 内嵌数组元素会持续增长并且没有封顶
- MongoDB 引用设计的限制
- MongoDB 对使用引用的集合之间并无主外键检查
- MongoDB 使用聚合框架的 $lookup 来模仿关联查询
- $lookup 只支持 left outer join
- $lookup 的关联目标(from) 不能是分片表
- **第三步: 套用设计模式**
- 文档模式: 无范式, 无思维定式, 充分发挥想象力
- 设计模式: 实战过屡试不爽的设计技巧, 快速应用
- 举例: 一个 loT 场景的分桶设计模式, 可以帮助把存储空间降低10倍并且查询效率提升数十倍
- 经验和学习 -(模式导向)-> 套用设计模式 -> 优化的模型
- 问题: 物联网场景下的海量数据处理 - 飞机监控数据
```json
{
"_id" : "20160101050000:CA2790",
"icao": "CA2790",
"callsign": "CA2790",
"ts": ISODate("2016-01-01T05:00:00.000+0000"),
"events": {
"a": 31418,
"b" : 173,
"s" :91,
"v" : 80
}
}
```
- 实际问题: 520亿条, 10TB - 海量数据
- 10万架飞机
- 1年的数据
- 每分钟1条
- ![飞机-海量数据.png](pic/飞机-海量数据.png)
- 52.6B = 100000 * 365 * 24 * 60
- 6364GB = 100000 * 365 * 24 * 60 * 130
- 4503GB = 100000 * 365 * 24 * 60 * 92
- 解决方案: 分桶设计
```json
{
" id" :"20160101050000:WG9943",
"icao":"WG9943",
"ts":ISODate("2016-01-01T05:00:00.000+0000"),
"events":[
{
"a":24293, "b":319, "p":[41,70], "s":56,
"t": ISODate("2016-01-01T05:00:00.000+0000")
},
{
"a":33663, "b":134, "p":[-38, -30], "s":385,
"t": ISODate("2016-01-01T05:00:01.000+0000")
}
]
}
```
- 60 events == 1小时的数据
- 一个文档: 一架飞机一个小时的数据
- ![飞机-海量数据-分桶.png](pic/飞机-海量数据-分桶.png)
- 可视化表现24小时的飞行数据
- 1440次读
- 模式小结:
- 场景
- 时序数据
- 物联网
- 智慧城市
- 智慧交通
- 痛点
- 数据点采频繁, 数据量太多
- 设计模式的方案及优点
- 利用文档内嵌数组, 将一个时间段的数据聚合到一个文档里
- 大量减少文档数量
- 大量减少索引占用空间
### 4. 设计模式集锦
- 问题: 大文档, 很多字段, 很多索引
```json
{
title: "Dunkirk",
release USA:"2017/07/23",
release UK:"2017/08/01",
release France:"2017/08/01",
release Festival San Jose:"2017/07/22"
}
```
- 需要很多索引
```json
{release_UsA:1}
{release_UK:1}
{release_France:1}
{release_Festival_San_Jose:1 }
```
- 电影的上映时间
- 解决方法: 列转行
```json
{
title: "Dunkirk",
releases:[
{country:"USA",date:"2017/7/23"},
{country:"Uk",date:"2017/08/01”}
]
}
```
- 转换后: 字段数变少了
- 创建索引: db.movies.createIndex({"releases.country":1, "releases.date":1})
- 模式小结:
- 场景: 产品属性多, 多语言(多国家属性)
- 痛点: 文档中有很多类似的字段, 会用于组合查询搜索, 需要创建很多索引
- 设计模式方案及优点: 转化为数组, 一个索引解决所有查询问题
- 问题: 模型灵活了, 如何管理文档的不同版本
```json
"id":ObiectId("5de26f197edd62c5d388babb"),
"name":"TJ",
"Tapdata""company":
```
- v1.0
```json
"id":obiectId("5de26f197edd62c5d388babb")
"name":"TJ"
"company":"Tapdata"
"wechat":"titang826"
"schema_version": "2.0"
```
- v2.0
- 解决方案schema_version 使用这个字段进行区分标识
- 模式小结:版本字段
- 场景: 任何有版本衍生的数据库
- 痛点: 文档模型格式多, 无法知道其合理性, 升级时候需要更新太多文档
- 设计模式及优点: 增加一个版本号字段, 快速过滤掉不需要升级的文档, 升级时候对不同的文档做不同的升级
- 问题: 统计网页点击流量
- 每访问一个页面就会产生一次数据库计数更新操作
- 统计数字准确性并不十分重要
- 解决方案: 用近似计算
- 每隔 10(X)次写一次
- Increment by 10(X)
- {$inc:{views:1}}
- if random(0,9) == 0 increment by 10
- 模式小结: 近似计算
- 场景:
- 网页计数
- 各种结果不需要准确的排名
- 痛点:
- 写入太频繁, 消耗系统资源
- 设计模式方案及优点
- 间隔写入, 每隔10次或者100次
- 大量减少写入需求
- 问题: 业绩排名, 游戏排名, 商品统计等精确统计
- 热销榜: 某个商品今天卖了多少, 这个星期卖了多少, 这个月卖了多少?
- 电影排行: 观影者, 场次统计
- 传统解决方案: 通过聚合计算
- 痛点: 消耗资源多, 聚合计算时间长
- 解决方案: 用预聚合字段
```json
{
product:"Bike"
sku:“abc123456”
quantitiy:20394,
daily_sales: 40,
weekly sales:302,
monthly_sales:1419
}
```
```spring-mongodb-json
db.inventory.update({_id:123},{
$inc:{
quantity:-1,
daily_sales: 1,
weekly_sales:1,
monthly_sales: 1
}
})
```
- 模式小结: 预聚合
- 场景: 准确排名, 排行榜
- 痛点: 统计计算耗时, 计算时间长
- 设计模式方案及优点: 模型中直接增加统计字段, 每次更新数据时候同时更新统计值
## 分片和集群 - [运维]
### 1. 分片集群机制及原理
- MongoDB 常见的部署架构
- ![MongoDB常见的部署架构.png](pic/MongoDB常见的部署架构.png)
- 为什么要使用分片集群?
- 数据容量日益增大, 访问性能日渐降低, 怎么破?
- 新品上线异常火爆, 如何支撑更多的并发用户?
- 单库已有 10TB 数据, 恢复需要 1-2 天, 如何加速?
- 地理分布数据
- 分片如何解决?
- 银行交易单表内10亿笔资料超负荷运转
- 交易号 0 - 1,000,000,000
- 思路:
- 交易号: 0 - 500,000,000 -> mongod[分片1]
- 交易号: 500,000,000 - 1,000,000,000 -> mongod[分片2]
- 还可以再细分[最多1024片]
- 分片集群剖析 - 路由节点 mongos
- ![分片节点剖析.png](pic/分片节点剖析.png)
- 分片集群剖析 - 配置节点 mongod
- ![配置节点mongod.png](pic/配置节点mongod.png)
- 分片集群剖析 - 数据节点 mongod
- ![数据节点mongod.png](pic/数据节点mongod.png)
- MongoDB 分片集群特点
- 应用全透明, 无特殊处理
- 数据自动均衡
- 动态扩容, 无须下线
- 提供三种分片方式
- 分片集群数据分布方式
- 基于范围
- ![分片集群数据分布方式-基于范围.png](pic/分片集群数据分布方式-基于范围.png)
- 基于 Hash
- ![分片集群数据分布方式-基于Hash.png](pic/分片集群数据分布方式-基于Hash.png)
- 基于 zone/tag
- ![分片集群数据分布方式-基于zone.png](pic/分片集群数据分布方式-基于zone.png)
- 小结:
- 分片集群可以有效解决性能瓶颈及系统扩容问题
- 分片消耗较多, 管理复杂, 尽量不要分片
- 详细了解学习后再进行分片
### 2. 分片集群的设计
- 如何用好分片集群
- 合理的架构
- 是否需要分片?
- 需要多少分片?
- 数据的分布规则
- 正确的姿势
- 选择需要分片的表
- 选择正确的片键
- 使用合适的均衡策略
- 足够的资源
- CPU
- RAM
- 存储
- 合理的架构 - 分片大小
- 分片的基本标准
- 关于数据: 数据量不超过 3TB, 尽可能保持在 2TB一个片;
- 关于索引: 常用索引必须容纳进内存
- 按照以上标准初步确定分片后, 还需要考虑业务压力, 随着压力增大, CPU, RAM, 磁盘中的任何一项出现瓶颈时, 都可以通过添加更多分片来解决。
- 合理的架构 - 需要多少个分片
- A = 所需存储总量/单服务器可挂载容量 8TB/2TB = 4
- B = 工作集大小/单服务器内存容量 400GB/ (256G*0.6) = 3
- C = 并发量总数/ (单服务器并发量*0.7) 30000/(9000*0.7) = 6 [额外开销]
- 分片数量 = max(A, B, C)=6
- 合理的架构 - 其他需求
- 考虑分片的分布:
- 是否需要跨机房分布分片?
- 是否需要容灾?
- 高可用的要求如何?
- 正确的姿势
- ![正确的姿势-概念图.png](pic/正确的姿势-概念图.png)
- 各种概念由小到大
- 片键 shard key: 文档中的一个字段
- 文档 doc: 包含 shard key 的一行数据
- 块 Chunk: 包含 n个文档
- 分片 Shard: 包含 n个 chunk
- 集群 Cluster: 包含 n个 分片
- 正确的姿势 - 选择合适的片键
- 影响片键效率的主要因素
- 取值基数(Cardinality)
- 取值分布
- 分散写, 集中读
- 被尽可能的业务场景用到
- 避免单调递增或递减的片键
- 正确的姿势 - 选择基数大的片键
- 对于小基数的片键:
- 因为备选值有限, 那么块的总数量就有限
- 随着数据增多, 块的大小会越来越大
- 太大的块, 会导致水平扩展时移动块会非常困难
- 例如: 存储一个高中的师生数据, 以年龄(假设年龄范围为15-65岁)作为片键, 那么:
- 15 <= 年龄 <= 65, 且只为整数
- 最多只会有 51个 chunk
- 结论: 取值基数要大
- 正确的姿势 - 选择分布均匀的片键
- 对于分布不均匀的片键
- 造成某些块的数据量急剧增大
- 这些块压力随之增大
- 数据均衡以chunk为单位, 所以系统无能为力
- 例如: 存储一个学校的师生数据, 以年龄(假设年龄范围为15-65岁)作为片键,那么:
- 15 <= 年龄 <= 65, 且只为整数
- 大部分的年龄范围为 15-18岁(学生)
- 15, 16, 17, 18 四个chunk的数据量, 访问压力远大于其他chunk
- 结论: 取值分布应尽可能均匀
- 正确的姿势 - 定向性好
- 考虑:
- 4个分片的集群, 你希望读某条特定的数据
- 如果你用片键作为条件查询, mongos 可以直接定位到具体的分片
- 如果你不用片键, mongos 需要把查询发到4个分片
- 等最后一个分片响应, mongos 才能响应应用端
- 结论: 对主要查询要具有定向能力
- 比如用组合片键来解决这个问题
- 片键: {user_id: 1, time: 1}
- 足够的资源
- mongos 与 config 通常消耗很少的资源, 可以选择低规格虚拟机
- 资源的重点在于 shard 服务器
- 需要足以容纳热数据索引的内存
- 正确创建索引后CPU通常不会成为瓶颈, 除非涉及非常多的计算
- 磁盘尽量选用SSD
- 最后, 实际测试是最好的检验, 来看你的资源配置是否完备
- 即使项目初期已经具备了足够的资源, 仍然需要考虑在合适的时候扩展
- 建议监控各项资源使用情况, 无论哪一项达到 60% 以上, 则开始考虑扩展, 因为:
- 扩展需要新的资源, 申请新资源需要时间
- 扩展后数据需要均衡, 均衡需要时间, 应保证新数据入库速度慢于均衡速度
- 均衡需要资源, 如果资源即将或已经耗尽, 均衡会很低效的
### 3. 分片集群的搭建及扩容 - 实验
- 实验目标及流程
- 目标: 学习如何搭建一个2个分片集群
- 环境: 3台Linux 虚拟机, 4Core 8GB
- 步骤
- 配置域名解析
- 准备分片目录
- 创建第一个分片复制集并初始化
- 创建config复制集并初始化
- 初始化分片集群, 加入第一个分片
- 创建分片表
- 加入第二个分片
- 实验架构
- testdemo01
- Shard1{Primary 27010} Shard2{Primary 27011} Config1 27019 mongos 27017
- testdemo02
- Secondary 27010 Secondary 27011 Config2 27019
- testdemo03
- Secondary 27010 Secondary 27011 Config3 27019
- ![实验架构.png](pic/实验架构.png)
- 1 - **配置域名解析**
- 在三台虚拟机上分别执行以下3条命令, 注意替换实际IP地址
- echo "192.168.1.1 testdemo01 member1.example.com member2.example.com" >> /etc/hosts
- echo "192.168.1.2 testdemo02 member3.example.com member4.example.com" >> /etc/hosts
- echo "192.168.1.3 testdemo03 member5.example.com member6.example.com" >> /etc/hosts
- 2 - 准备分片目录
- 在各服务器上创建数据目录, 我们使用 /data, 按自己的需求修改为其他目录:
- 在 member1 / member3 / member5 上执行以下命令:
- mkdir -p /data/shard1/
- mkdir -p /data/config/
- 在 member2 / member4 / member6 上执行以下命令:
- mkdir -p /data/shard2/
- mkdir -p /data/mongos/
- 3 - 创建第一个分片用的复制集
- 在 member1 / member3 / member5 上执行以下命令:
- mongod --bind_ip 0.0.0.0 --replSet shard1 --dbpath /data/shard1 --logpath /data/shard1/mongod.log --port 27010 --fork --shardsvr --wiredTigerCacheSizeGB 1
- 检查是否成功运行: mongo localhost:27010
- 4 - 初始化第一个分片复制集
- mongo --host member1.example.com:27010
```spring-mongodb-json
rs.initiate({
_id: "shard1",
"members": [
{
"_id": 0,
"host": "member1.example.com:27010"
},
{
"_id": 1,
"host": "member3.example.com:27010"
},
{
"_id": 2,
"host": "member5.example.com:27010"
},
]
})
```
- 查看结果: rs.status()
- 5 - 创建 config server 复制集
- 在member1 / member3 / member5 上执行以下命令
- mongod --bind_ip 0.0.0.0 --replSet config --dbpath /data/config --logpath /data/config/mongod.log --port 27019 --fork --configsvr --wiredTigerCacheSizeGB 1
- 6 - 初始化 config server 复制集
- mongo --host member1.example.com:27019
```spring-mongodb-json
rs.initiate({
_id: "config",
"members": [
{
"_id": 0,
"host": "member1.example.com:27019"
},
{
"_id": 1,
"host": "member3.example.com:27019"
},
{
"_id": 2,
"host": "member5.example.com:27019"
},
]
})
```
- 查看结果: rs.status()
- 7 - 在第一台机器上搭建 mongos[也可以直接在别的机器上进行运行, 一般至少需要2个来做高可用]
- mongos --bind_ip 0.0.0.0 --logpath /data/mongos/mongos.log --port 27017 --fork --configdb config/member1.example.com:27019,member3.example.com:27019,member5.example.com:27019
- 连接到 mongos, 添加分片
- mongo --host member1.example.com:27017
- mongos>
- sh.addShard("shard1/member1.example.com:27010,member3.example.com:27010,member5.example.com:27010");
- 查看运行结果: sh.status()
- 8 - 创建分片表
- 连接到 mongos, 创建分片集合
- mongo --host memeber1.example.com:27017
- mongos> sh.status()
- mongos> sh.enableSharding("foo"); 指定库名
- mongos> sh.shardCollection("foo.bar", {_id:'hashed'}); 指定集合和分片键
- mongos> sh.status();
- 插入测试数据
- use foo
- for(var i=0;i < 10000; i++){ db.bar.insert({i:i});}
- 查看结果: sh.status()
- 9 - 创建第二个分片的复制集 - 扩容
- 在 member2 / member4 / member6 上执行以下命令
- mongod --bind_ip 0.0.0.0 --replSet shard2 --dbpath /data/shard2 --logpath /data/shard2/mongod.log --port 27011 --fork --shardsvr --wiredTigerCacheSizeGB 1
- 10 - 初始化第二个分片复制集 - 扩容
- mongo --host member2.example.com:27011
```spring-mongodb-json
rs.initiate({
_id: "shard2",
"members": [
{
"_id": 0,
"host": "member2.example.com:27011"
},
{
"_id": 1,
"host": "member4.example.com:27011"
},
{
"_id": 2,
"host": "member6.example.com:27011"
},
]
})
```
- 查看结果: rs.status()
- 11 - 加入第二个分片 - 扩容
- 连接到 mongos, 添加分片
- mongo --host member1.example.com:27017
- mongos>
- sh.addShard("shard2/member2.example.com:27011,member4.example.com:27011,member6.example.com:27011");
- 查看运行结果: sh.status()
### 4.