|
|
|
|
## 数据模型设计
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|