【存储与数据库 学习资料(下)】字节跳动青训营 - 后端专场

4,114 阅读10分钟

字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第二十节:TOS 对象存储实战

概述

本节课程主要分为三个方面:

  1. 分布式存储选型对比: 对象存储的优势
  1. 对象存储的用法
  1. 对象存储面临的工程挑战和解法

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

分布式存储选型对比: 对象存储的优势

  • 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的发展方向。

课后思考题

  1. 对象存储适用于网页前端 js 文件存储吗?为什么?
  1. 对象存储使用 CDN 作为缓存,能够缓存哪些基本接口的结果呢?缓存刷新会使用到 HTTP 协议的何种机制呢?
  1. 对象存储 MultiPartUpload 接口,UploadID 如何保证全局唯一呢?
  1. 一个基于对象存储构建的网盘应用,应该如何用 Listprefix 接口实现个人文件的展示呢?
  1. 假设我们采取 Hash Partition 来完成对象存储元数据的存储,能够实现ListPrefix 接口么?如何实现呢?
  1. Replication 的一个副本损坏了,对系统会带来什么影响?应该如何修复呢?
  1. Erasure Coding 有哪些经典的算法?多机房之间的 EC 有何种解决方案呢?
  1. 镜像灾备如何保证主备集群之间的一致性呢?

课后大作业

实现一个对象存储客户端

作业要求:

  1. 在任意一个公有云中申请一个对象存储 Bucket
  1. 使用你熟悉的语言,实现一个对象存储命令行客户端,要求该客户端能够

    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)

参考学习资料:

《CMake Cookbook中文版》 - Bookset

SQL引擎设计

Parser

SQL语言的解析非常的繁复,因此这里“偷懒”,去github上寻找了一个开源的SQL解析器。

github.com/hyrise/sql-…

输入是SQL语句,输出是语法树:

image.png

但sql-parser库只能提供词法分析和语法分析,生成查询树,不能进行语义分析,也就是合法性校验。因此我们将sql-parser库进行封装,增加语义分析功能:

class Parser {
 public:
  Parser();
  ~Parser();

  bool parseStatement(std::string query);

  SQLParserResult* getResult() { return result_; }

 private:
  bool checkStmtsMeta();
  ......
}

Optimizer

根据产生的查询树,生成对应的计划树。计划树由各个基础算子组成,针对本项目中要求的场景,构造了如下基础算子:

image.png

比如一条UPDATE查询,对应的计划树如下:

image.png

Executor

使用火山模型:

依赖计划树生成对应的执行树,每个Plan生成一个对应的Operator。

image.png

每个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出来,恢复到原有的数据中去。

image.png

存储引擎

数据结构

因为我们是内存态的数据库,所以数据结构可以设计的比较简单。这里每次申请一批记录的内存,这样可以降低内存碎片化的问题,提高内存访问效率。然后将这批记录的内存放到FreeList中。当有数据插入时,从FreeList中获取一块内存用于写入,并放入DataList。当有数据删除时,将数据从DataList归还到FreeList中。

image.png

索引设计

因为这里只要求实现等值匹配,所以可以用最简单的hash索引。

项目库

代码仓

github.com/thuyy/ByteY…

利用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

运行

扩展演进

大家可以在当前项目的基础上继续演进,有如下几个方向:

  1. 实现B+Tree索引
  1. 实现count()、sum()、min()、max()等简单函数
  1. 实现group by操作
  1. 实现两表join操作
  1. 实现基于磁盘文件的存储引擎,以及数据的持久化