这是我参与「第四届青训营 」笔记创作活动的第 13 天!
设计一个简单的数据湖
1、文件结构
- 存入数据,按照 date 分区,schema:
- 每天都会写入新数据
- 需要使用列存格式
- 写入数据时
- 按照每条数据的 date 进行分区
- 额外使用 metadata 文件记录表信息,如下:
{
"schema": [
{
"name": "nuserID"
"type": "int"
},
{
"name": "date"
"type": "string"
},
{
"name": "event"
"type": "string"
},
{
"name": "phoneNumber"
"type": "string"
}
],
"partition_col": "date",
"location": "mytable/",
"partition": [
"2020-01-01",
"2020-01-02"
]
}
2、Time Travel
- 每次写入都生成一个新的元数据文件,记录变更
- 分区数据在 Update 时,不要删除旧数据,保证新旧共存
- 元数据中存储具体的文件路径,而不仅仅是分区文件夹
- 每一次写入操作,创建一个新的 json 文件,以递增版本号命名,记录本次新增/删除的文件
- 每当产生 N 个 json,做一次聚合,记录完整的分区文件信息
- 用 checkpoint 记录上次做聚合的版本号
{
...
"partition": [
"2020-01-01"{
"xxx.parquet",
"yyy.parquet"
}
"2020-01-02"{
"xxx.parquet",
"yyy.parquet"
}
]
}
3、Transaction
- Transaction 即事务
- ACID,是指数据库在写入或更新资料的过程中,为保证事务是正确可靠所必须具备的四个特性
- Atomicity:原子性-本次写入要么对用户可见,要么不可见(需要设计)。如:要么 A-10、B+10,要么不变。
- Consistency:一致性-落盘和输入一致(由计算引擎保证)。如:不可以 A-10、B+5。
- Isolation:事务隔离-正确解决读写冲突和写写冲突(需要设计)。如:A和C同时给B转10,B最终结果是+20.
- Durability:持久性-转账服务器重启,结果不变。(由储存引擎决定)。
(1)原子性
- 写入流程
- 写入 parquet 数据文件;
- 写入 json 元数据文件。
- 如何保证原子性(从用户可见性入手)
- 用户只会读取以版本号数字命名的 json 文件,每次都能读取到最大的版本号作为数据集的现状;
- 新的写入写完 parquet 后开始写 json 文件,使用 hash 值对 json 命名,如 xxx.json;
- 直到 json 文件内容写入完毕,利用 HDFS 的 RenameIfAbsent 能力将 xxx.json 替换为 000006.json 作为最新版本。
- 读写冲突已经被解决,how?
- 新的写入除非已经 commit,否则用户读不到;
- 用户正在读的分区,被另一个写入进行了更新,数据不会进行替换,而是共存。
(2)事务隔离
- Update 写入流程
- 从最新的版本中,获取需要 update 分区;
- 乐观锁先把该写入的文件全落盘,然后进行写 json 阶段;
- 分几种情况:
- 1.发现版本号和一开始没区别,直接写新的版本;
- 2.发现版本号增加了,看着新增的这些版本有没有更新我需要更新的分区:
- 没有,直接写新的版本;
- 有,两者都更新了同一分区,得重新 update。
4、Schema Evolution
- Add/Drop/Rename
- 重要:
- 用户并不直接读取 parquet 文件本身,而是通过数据湖接口读取,如 Datasetds=simpleDataLake.read(mytable).option(date=2020-01-01)
- 数据湖内部会读取应该读的 parquet,并在 schema 上做进一步处理
- ID 将 data 和 metadata 的列名做一一对应!
- 唯一确定的 ID、新增列赋予新 ID、删列 ID 不复用;
- 写入数据时,ID 也写入数据文件;
- 读取数据时,用 ID 做映射,如果:
- 1.Data 中没有,metadata 中有:ADD;
- 2.Data 中有,matadata 中没有,DROP;
- 3.Data 和 metadata 中都有同一 ID但是 name 不同:RENAME。
参考:htps://baike.baidu.com/item/ACID/10738