字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~
第二十节:TOS 对象存储实战
概述
本节课程主要分为三个方面:
- 分布式存储选型对比: 对象存储的优势
- 对象存储的用法
- 对象存储面临的工程挑战和解法
课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。
课前
分布式存储选型对比: 对象存储的优势
- Structured data: 结构化数据
- Unstructured data:非结构化数据
- Relational Database: 关系型数据库
- NoSql Database:非关系型数据库
- Distributed stroage: 分布式存储
- Cloud Native Storage: 云原生存储
- Distributed File System: 分布式文件系统
- HDFS:当前最主流的开源分布式文件系统
- Object Storage: 对象存储
- Immutable data: 不可更改数据
- Mutable data: 可更改数据
- HTTP: 网络通信协议
- CDN: Content Delivery Network
对象存储的用法
- Data Model:存储系统的数据组织模型
- Posix File System Interface:Posix标准文件系统接口
- Directory/File: 目录/文件
- Resutful: 一种接口风格,通常基于HTTP实现
- Get/Head/Put/Delete: Http Method
- ListPrefix: 对象存储的Listprefix接口
对象存储面临的工程挑战和解法
- QPS:query per second
- IOPS:IO per second
- SLA:Service-Level-Agreement,衡量可用性的一个指标
- Bandwidth Intensive Application: 带宽型应用
- CPU Intensive Application: 带宽型应用
- Scalability: 可扩展性
- Availability:可用性
- Durability: 持久度
- Replication:复制
- Partition:分治
- 爆炸半径:故障影响业务的范围
- Hot Data: 热数据
- Warm Data: 温数据
- Cold Data: 冷数据
- EC: Erasure Coding
- Garbage Collection:垃圾回收
课中
引言
- 介绍一款短视频应用的整体架构
-
揭示短视频应用背后的视频/图片等静态内容的海量存储需求
- 计算单天/单月/单年的存储容量
-
分析视频/图片等静态内容存储需求的特点
- Immutable Data: 视频/图片是静态不可更改的数据
分布式存储选型对比: 对象存储的优势
回顾存储体系
之前课程介绍过如下存储体系分类:
- 单机存储
- 分布式存储
- 单机/分布式数据库
这里会重温下各类存储适用的场景和范围,并重点介绍分布式存储的分类:
- 分布式文件系统:当前业界开源代表是HDFS
- 对象存储: 本次课程介绍的TOS就是其中代表
HDFS vs 对象存储
-
是否Cloud Native Storage:
- 云原生存储开箱即用,极大解除了运维运营负担,生态体系依托云构建,丰富健全
- 对象存储是当前各大云厂商王牌存储产品
-
接入难易程度对比:
-
Data Model差异:
- HDFS:伪Posix File System Interface, Directory/File的数据组织形式
- 对象存储:扁平的逻辑命名空间, Bucket/Key的数据组织形式
- Bucket/Key数据组织形式优势:容易理解,使用心智负担小,贴合业务需求
-
使用接口差异:
- HDFS:Mkdirs/Create/Append/Delete/Get等文件接口
- 对象存储: GET/PUT/HEAD/DELETE等Restful HTTP接口
- HTTP接口的优势:开发简单,分享方便,可无缝接入CDN
-
-
其他对比:
- 可扩展性:对象存储可扩展性更强,支持无限容量
- 成本:对象存储成本更低
对象存储的用法
基本接口
- Restful风格简介:简单介绍Restful风格的形式和优点
- GET:获取对象内容
- PUT:下载对象内容
- DELETE:删除对象内容
- HEAD:获取对象元信息
- MultiPartUpload接口:针对大对象弱网环境上传的优化
这里会对基本的接口做一个简短的演示,让大家直观的了解对象存储的基本用法
Listprefix接口
- 接口功能:将扁平的逻辑命名空间,转化为人类易于理解的结构化逻辑命名空间
- CommonPrefix概念:将扁平的逻辑空间,通过分隔符,分割成类似目录的层次化命名空间
- 分页实现:通过页首和每页对象数量参数,实现分页
这里也会演示ListPrefix接口的基本用法
对象存储面临的工程挑战和解法
工程挑战
首先会梳理经典的一些业务场景:
- 海量容量场景: 业务持续产生大量数据,数据规模>>PB级别,存储容量和成本压力极大
- 海量QPS场景: 业务场景有高QPS读写请求,量级>>100K/s,并且时延要求极高,对底层存储IOPS压力极大
- 高可用性场景:业务对于SLA要求非常高,要求避免全局性不可用事件发生,但对于一致性要求比较低
其中带来的工程挑战有:
- 可扩展性:架构在存储容量/带宽吞吐/QPS等关键指标上,线性可扩展,能够承担业务在这些指标上的持续增长需求
- 持久度:数据存储成功后,需要能够抵抗单机/单机架/单机房等各种类型的故障而不丢失
- 可用性:系统不可用的时间在整体运行时间的占比需要尽可能小,系统不可用后需要具备快速恢复能力
- 性价比:在海量存储容量的情况下,需要尽力降低单位存储成本,以降低业务的成本支出
解法
Partition 分治提升可扩展性
思路:
- 将数据通过一定的Partition方法,散步到分布式系统中的不同的机器节点来计算/存储
Partition一般做法:
- Hash Partition:通过hash函数来做Partition的选取
- Range Partition: 通过range方式切分逻辑地址空间
带来的好处:
- 可扩展性好
- 爆炸半径低
Replication 多副本提升持久度
思路:
- 将数据拷贝多份来存储
Replication一般做法:
- 多副本:将数据拷贝成多个镜像的副本存储
- EC:使用Erasure Coding方法来构建冗余副本
带来的好处:
- 持久度高
- 吞吐能力也有提升
单元化最小化爆炸半径
思路:
- 将系统切分成多个垂直独立的单元,单元之间互相无影响
单元化一般做法:
- 去除系统单点依赖:系统中没有强依赖的单点
- 构建流量调度能力:流量可在单元之间灵活调度
带来的好处:
- 可用性高
- 运维更友好
镜像灾备应对极端情况
思路:
- 构建镜像的主备集群应对极端情况
单元化一般做法:
- 同构灾备:使用同构系统来做数据的镜像备份
- 异构灾备:使用异构系统来做数据的镜像备份
带来的好处:
- 极高的可用性
- 极高的可靠性
开源节流提升性价比
思路:
-
开源:
- 冷热分离,使用更低成本存储介质
-
节流:
- 通过更高比例EC降低单位存储逻辑冗余
- 提升垃圾回收效率,提高磁盘空间利用率
带来的好处:
- 更高的性价比
TOS 当前架构和展望
根据上面工程挑战和解法分析,简单总结并介绍TOS当前的架构,并展望后续TOS的发展方向。
课后思考题
- 对象存储适用于网页前端 js 文件存储吗?为什么?
- 对象存储使用 CDN 作为缓存,能够缓存哪些基本接口的结果呢?缓存刷新会使用到 HTTP 协议的何种机制呢?
- 对象存储 MultiPartUpload 接口,UploadID 如何保证全局唯一呢?
- 一个基于对象存储构建的网盘应用,应该如何用 Listprefix 接口实现个人文件的展示呢?
- 假设我们采取 Hash Partition 来完成对象存储元数据的存储,能够实现ListPrefix 接口么?如何实现呢?
- Replication 的一个副本损坏了,对系统会带来什么影响?应该如何修复呢?
- Erasure Coding 有哪些经典的算法?多机房之间的 EC 有何种解决方案呢?
- 镜像灾备如何保证主备集群之间的一致性呢?
课后大作业
实现一个对象存储客户端
作业要求:
- 在任意一个公有云中申请一个对象存储 Bucket
-
使用你熟悉的语言,实现一个对象存储命令行客户端,要求该客户端能够
a. 创建对象:超过 1GB 的对象使用 MultiUpload 上传,小于 1GB 的使用 Put 上传
b. 下载对象
c. 删除对象
d. 查看对象是否存在
e. 列举对象及 CommonPrefix
第二十一节:实操项目 - 老师手把手教
前置克隆项目地址:github.com/thuyy/ByteY… ,开课前1天完成项目运行,有问题可以记录一下,直播课跟着讲师节奏走,或者积极在弹幕区提问哈。
背景知识
存储&数据库
存储系统
- 块存储:存储软件栈里的底层系统,接口过于朴素
- 文件存储:日常使用最广泛的存储系统,接口十分友好,实现五花八门
- 对象存储:公有云上的王牌产品,immutable语义加持
- key-value存储:形式最灵活,存在大量的开源/黑盒产品
数据库系统
- 关系型数据库:基于关系和关系代数构建的,一般支持事务和SQL访问,使用体验友好的存储产品
- 非关系型数据库:结构灵活,访问方式灵活,针对不同场景有不同的针对性产品
分布式架构
- 数据分布策略:决定了数据怎么分布到集群里的多个物理节点,是否均匀,是否能做到高性能
- 数据复制协议:影响IO路径的性能、机器故障场景的处理方式
- 分布式事务算法:多个数据库节点协同保障一个事务的ACID特性的算法,通常基于2pc的思想设计
数据库结构
-
SQL引擎
- Parser:查询解析,生成语法树,并进行合法性校验。(词法分析、语法分析、语义分析)
- Optimizer:根据语法树选择最优执行路径。
- Executor:查询执行流程,真实的对数据进行处理。
-
事务引擎
- 实现事务的ACID。
-
存储引擎
- 存储数据、索引、日志。
项目要求
实现一个内存态数据库 ByteYoungDB,能够支持下面操作:
- Create/Drop Table, Create/Drop Index
- Insert、Delete、Update、Select
- 简单的等值匹配条件
where col = XXX
- 支持简单的事务Commit,Rollback
项目设计
项目分解
-
SQL引擎
- Parser:查询解析,生产语法树,并进行合法性校验。(词法分析、语法分析、语义分析)
- Optimizer:根据语法树选择最优执行路径。
- Executor:基于火山模型的查询执行流程。
-
事务引擎
- 事务提交和回滚机制设计。
-
存储引擎
- 数据结构设计
- 索引结构设计
项目搭建
使用大型C/C++项目中最常用的CMake工具。CMake是一种跨平台编译工具。CMake主要是编写CMakeLists.txt文件通过cmake命令将CMakeLists.txt文件转化为make所需要的Makefile文件,最后用make命令编译源码生成可执行程序或者库文件。
cmake_minimum_required(VERSION 3.8)
project(ByteYoungDB)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_CXX_FLAGS "-g -Wall -Werror -std=c++17")
set(CMAKE_CXX_FLAGS_DEBUG "-O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG ")
set(CMAKE_INSTALL_PREFIX "install")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin)
include_directories(${CMAKE_SOURCE_DIR}/sql-parser/include)
add_subdirectory(src/main)
add_subdirectory(src/sql-parser-test)
参考学习资料:
SQL引擎设计
Parser
SQL语言的解析非常的繁复,因此这里“偷懒”,去github上寻找了一个开源的SQL解析器。
输入是SQL语句,输出是语法树:
但sql-parser库只能提供词法分析和语法分析,生成查询树,不能进行语义分析,也就是合法性校验。因此我们将sql-parser库进行封装,增加语义分析功能:
class Parser {
public:
Parser();
~Parser();
bool parseStatement(std::string query);
SQLParserResult* getResult() { return result_; }
private:
bool checkStmtsMeta();
......
}
Optimizer
根据产生的查询树,生成对应的计划树。计划树由各个基础算子组成,针对本项目中要求的场景,构造了如下基础算子:
比如一条UPDATE查询,对应的计划树如下:
Executor
使用火山模型:
依赖计划树生成对应的执行树,每个Plan生成一个对应的Operator。
每个Operator调用next_.exec()
来调用下层Operator产生数据:
class BaseOperator {
public:
BaseOperator(Plan* plan, BaseOperator* next) : plan_(plan), next_(next) {}
~BaseOperator() {}
virtual bool exec() = 0;
Plan* plan_;
BaseOperator* next_;
};
事务引擎
在不考虑并发的情况下,以及数据无需落盘持久化的情况下,我们的事务引擎设计就变得比较简单。因此不需要实现复杂的MVCC机制,只需要能够实现事务的Commit和Rollback功能即可。
这里我们实现一个undo stack的机制,每次更新一行数据,就把这行数据老的版本push到undo stack中。如果事务回滚,那么就从undo stack中把老版本的数据逐个pop出来,恢复到原有的数据中去。
存储引擎
数据结构
因为我们是内存态的数据库,所以数据结构可以设计的比较简单。这里每次申请一批记录的内存,这样可以降低内存碎片化的问题,提高内存访问效率。然后将这批记录的内存放到FreeList中。当有数据插入时,从FreeList中获取一块内存用于写入,并放入DataList。当有数据删除时,将数据从DataList归还到FreeList中。
索引设计
因为这里只要求实现等值匹配,所以可以用最简单的hash索引。
项目库
代码仓
利用cloc工具进行代码统计:
编译
前置依赖:
brew install cmake
编译:
mkdir install
cd install
cmake ../
make
执行:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/Users/yangyang/Code/ByteYoungDB/sql-parser/lib
export DYLD_LIBRARY_PATH=/Users/yangyang/Code/ByteYoungDB/sql-parser/lib
运行
扩展演进
大家可以在当前项目的基础上继续演进,有如下几个方向:
- 实现B+Tree索引
- 实现count()、sum()、min()、max()等简单函数
- 实现group by操作
- 实现两表join操作
- 实现基于磁盘文件的存储引擎,以及数据的持久化