高阶面试-存储系统的设计

概述

分类

  • 块存储 block storage
  • 文件存储 file storage
  • 对象存储 object storage

区别:

块存储

概述

位于最底层,块,是物理存储设备上数据存储的最小单位。硬盘(Hard Disk Drive,HDD)就属于块存储。常见的还有固态硬盘(SSD)、存储区域网络(SAN),操作系统和应用程序可以通过块级接口来访问这些数据。主要应用程序:数据库。

生产上,除了磁盘挂载,要么就是用云厂商提供的块存储,如AWS EBS,要么就是Ceph这种文件存储系统的块存储。

如何存储?

将数据分成固定大小的块(或扇区),每个块都有唯一的地址或编号标识。

假设有个硬盘,被分割为大小1MB的块,来存储数据。

硬盘初始化(10GB分为1w个1MB的块)-->数据写入(系统给数据分配一个或多个空闲的块)-->块分配(系统维护映射表记录块是否空闲)-->数据读取(应用程序请求包含数据的块的地址,系统检索后返回给应用程序)-->块管理(垃圾回收、压缩空间、快照、克隆、备份等)

scss 复制代码
物理磁盘 (Disk)
└── 分区 (Partition)
    ├── 主分区 (Primary Partition)
    └── 扩展分区 (Extended Partition)
        └── 逻辑分区 (Logical Partition)
            └── 物理卷 (Physical Volume)
                └── 卷组 (Volume Group)
                    └── 逻辑卷 (Logical Volume)
                        └── 文件系统 (Filesystem)
                            └── 块 (Block)
磁盘挂载示例
  1. 使用 fdisk 创建分区

    bash 复制代码
    [root@72agent ~]# fdisk /dev/xvde

    进入 fdisk 工具,开始对 /dev/xvde 设备进行分区操作。

  2. 创建新的分区

    • 输入 n 创建新分区

      bash 复制代码
      Command (m for help): n
    • 选择分区类型 p (主分区)

      bash 复制代码
      Select (default p): p
    • 设置分区编号,默认是 1

      bash 复制代码
      Partition number (1-4, default 1): 1
    • 设置起始扇区,默认是 2048

      bash 复制代码
      First sector (2048-629145599, default 2048): 
    • 设置结束扇区,默认是最大值

      bash 复制代码
      Last sector, +sectors or +size{K,M,G} (2048-629145599, default 629145599): 
  3. 更改分区类型

    • 输入 t 更改分区类型

      bash 复制代码
      Command (m for help): t
    • 选择分区 1

      bash 复制代码
      Selected partition 1
    • 输入类型代码 83 (Linux原生分区,可以格式化为 ext4、xfs 等 Linux 文件系统并用于普通的数据存储)

      bash 复制代码
      Hex code (type L to list all codes): 83
  4. 保存分区表并退出

    bash 复制代码
    Command (m for help): w
  5. 创建物理卷 (验证设备、写入LVM元数据包括物理卷标识符UUID、卷组信息、数据区描述、更新设备信息)

    bash 复制代码
    [root@72agent ~]# pvcreate /dev/xvde1
  6. 创建卷组(将一个或多个物理卷PV组合成一个卷组VG,会写入LVM卷组元数据,包括卷组名、物理卷列表、物理扩展块大小等)

    bash 复制代码
    [root@72agent ~]# vgcreate appvg /dev/xvde1
  7. 创建逻辑卷(将卷组中的物理存储空间组织成灵活易用的逻辑卷)

    bash 复制代码
    [root@72agent ~]# lvcreate -l 100%VG -n applv appvg
  8. 格式化逻辑卷为 XFS 文件系统(在逻辑卷上创建必要的文件系统结构如超级块、块组、inode表)

    bash 复制代码
    [root@72agent ~]# mkfs.xfs /dev/appvg/applv
  9. 创建挂载点(访问文件系统的路径,用于组织和管理文件)

    bash 复制代码
    [root@72agent ~]# mkdir /app
  10. 挂载逻辑卷到挂载点

    bash 复制代码
    [root@72agent ~]# mount /dev/appvg/applv /app/
  11. 添加挂载信息到 /etc/fstab,以便开机自动挂载

    bash 复制代码
    [root@72agent ~]# echo "/dev/mapper/appvg-applv /app xfs defaults 0 0" >> /etc/fstab
  12. 验证挂载情况

    bash 复制代码
    [root@72agent ~]# df -h

以上步骤依次执行,可以成功将 /dev/xvde 挂载到 /app 目录,并确保系统重启后自动挂载。请注意检查每个步骤的输出,以确保没有错误发生。

当然,也可以挂载逻辑卷

# 安装必要的工具(例如,使用LVM管理块存储)
sudo apt-get update
sudo apt-get install lvm2

# 使用fdisk或parted创建新的分区
sudo fdisk /dev/sdx
# 创建一个新的分区并退出

# 创建物理卷
sudo pvcreate /dev/sdx1

# 创建卷组
sudo vgcreate vg_myvolume /dev/sdx1

# 创建逻辑卷
sudo lvcreate -l 100%FREE -n lv_mydata vg_myvolume

# 格式化逻辑卷
sudo mkfs.ext4 /dev/vg_myvolume/lv_mydata

# 挂载逻辑卷
sudo mount /dev/vg_myvolume/lv_mydata /mnt/mydata

# 写入数据到挂载的卷
echo "Hello, block storage!" | sudo tee /mnt/mydata/hello.txt

# 查看数据
cat /mnt/mydata/hello.txt

# 卸载卷
sudo umount /mnt/mydata

如上,可写数据到挂载的卷

文件存储

在块存储的基础上,提供更高层次的抽象。最常见,相关协议如ftp、nfs、smb、scp、rsync等

文件存储的分类:

  • 基于磁盘的普通本地文件系统,如ext4、xfs等
  • 网络文件系统,如nfs
  • 分布式文件系统 如ceph、glusterFS等
读取 /home/user/document.txt 文件
  1. 查找目录 /home/user

    • 查找根目录 /,找到 home 目录的 inode。
    • 读取 home 目录的数据块,找到 user 目录的 inode。
    • 读取 user 目录的数据块,找到 document.txt 文件的 inode 编号。
  2. 读取 inode

    • 根据 document.txt 文件的 inode 编号,从 inode 表中读取 inode 元数据。
  3. 读取数据块

    • 读取 inode 中指向的数据块,获取文件的实际内容
      如果目录项和 inode 信息已经在内存中缓存,则可以减少磁盘访问次数

主要优化:

  • 缓存 目录项和 inode 信息放入内存
  • 预读 读取文件,系统会预读后续的数据块,提高顺序读性能
  • 写回 文件系统缓冲区会延迟将写操作的数据写入磁盘,减少磁盘写操作次数
  • 索引优化 ext4采用B+树的变体做目录索引,inode索引沿用unix系统的inode结构

对象存储

文件存储系统的特点,对文件访问,需要先访问元数据inode,再访问用户数据也就是存储文件的数据。整个过程涉及2-3次的磁盘访问,而互联网领域有大量的图片等存储需求,多次磁盘访问会显著降低性能;文件系统的方式,应用访问数据的整个访问路径较长,用户无法直接访问,必须经过nginx-应用(接口、权限、文件系统接口)-远程文件系统,而随着互联网应用的发展,有海量图片等资源,和文件存储系统不同,只需要一次存储多次访问,不需要文件锁、对文件内容的修改等,因此对象存储应运而生。如AWS S3、七牛云、腾讯云对象存储等。

如何设计对象存储

需求
mindmap root((需求)) 功能性需求 创建bucket bucket上传下载 bucket版本控制 列出bucket的对象 非功能性需求 大文件和很多小文件 一年数据量100PB 数据持久性6个9,服务可用性4个9

需求如上,假设20%小对象(小于1MB),60%中等对象(1MB-64MB),20%大对象(大于64MB)。计算得到对象总数大概0.68 billion个,一个对象的元数据1KB,那需要0.68TB空间存储元数据。

对象存储:metadata(ObjectName->ObjectId) dataStorage(objectId->Object)

分离元数据和对象数据,数据存储包含不可变数据,元数据存储包含可变数据

![[Pasted image 20240605212521.png]]

架构图

![[Pasted image 20240605213257.png]]

上传

![[Pasted image 20240605214007.png]]

对象必须在桶里面

  1. http put请求创建桶-->LB-->API-->IAM确保授权且有写权限-->metadata存储,db中创建bucket_info
  2. http put请求创建script.txt的对象-->LB-->API-->IAM-->将payload的对象数据发送到数据存储,返回对象uuid
  3. API调用metadataDB存储record,包含object_name、object_id(uuid)、bucket_id
下载

![[Pasted image 20240605231357.png]]

client-(GET /bucket-to-share/script.txt)->LB-->API-->IAM验证是否有读权限-->metadataDB检索uuid-->从数据存储中检索对象数据-->返回给client

数据存储服务

![[Pasted image 20240605233125.png]]

三个部分:

  • 数据路由 data routing service,提供restfulAPI访问数据节点集群,无状态服务,查询placement service获取最佳数据节点读写
  • 存储分布服务 placement service,负责将对象放置在不同的存储节点和数据中心,如下虚拟集群图,实现冗余存储和高可用 通过心跳监控所有数据节点。集群的话,使用paxos或raft协议构建5到7个节点的集群,保证服务的高可用。
  • 数据节点 data node,也叫复制组,通过将数据复制到多个数据节点确保可靠性和持久性。每个数据节点都运行一个数据服务守护进程,给存储分布服务发送心跳,包含数据节点管理多少个磁盘驱动器,每个驱动器存储多少数据。存储分布服务给数据节点分配ID,添加到虚拟集群映射中,并返回唯一id、虚拟集群map、去哪复制数据

![[Pasted image 20240605233813.png]]

流程:

API-对象数据->dataStorage

data routing service 生成对象的uuid,请求placement service存储

placement service检查虚拟集群map,返回主节点

data routing service将数据和uuid发给主节点

主节点保存并复制给两个副本节点(CAP的三种取舍),返回响应给data routing service

uuid返回给API

数据的管理

最简单:每个对象存储到单独的文件

缺点:很多小文件,性能受影响,1.浪费数据块,典型的块是4KB,对于小文件也是占用整个磁盘块;2.inode会太多,有耗尽inode的风险;3.操作系统对大量inode的处理不好

采用方案:在一个大文件中存储多个小对象

注意:读写文件的写入访问必须串行化。现代多核处理,为每个传入请求提供专用的读写文件

![[Pasted image 20240605235652.png]]

需要知道:

  • 包含小对象的数据文件
  • 对象在文件中的开始下标

需要object_mapping表,object_id、file_name、start_offset、object_size

可以部署单个大型集群支持所有数据节点,但没必要,因为映射数据在每个数据节点都是孤立的,不需要共享,每个数据节点部署一个简单的RDB如sqlite

更新后如下:

![[Pasted image 20240606105245.png]]

如何保证高可用

多数据中心复制

擦除编码(erasure coding) 创建奇偶校验,对应数学公式保证在最多4个节点宕机的情况下可以重建原始数据。假设i个节点每年0.81%的故障率,根据backblaze计算,擦出编码可实现11个9的高可用。缺点:极大的复杂了数据节点的设计。

校验和checksum,在每个对象的末尾附加校验和,在将文件标记为只读之前,在末尾添加整个文件的校验和,如下

![[Pasted image 20240606133808.png]]

metadata schema

需要支持3个查询:

  • find object_id by object_name
  • insert and delete object by object_name
  • list objects in a bucket sharing the same prefix

需要两个表

![[Pasted image 20240606134349.png]]

规模:假设100w客户,每个客户10个bucket,每个记录1kb,也就是需要 100w*10*1kb=10GB的存储空间

上规模最好不要单个数据库实例,分片扩展对象表

分片方案:

  • 按bucket_id,但bucket可能包含数十亿个对象,导致热点分片hotspot shard
  • 按object_id,无法快速执行1和2了
  • 按bucket_name和object_name组合的hash分片呢,前两个快速,但最后一个查询不好

最后一个怎么处理?
select * from object where bucket_id='123' and object_name like 'a/b/%'

元数据服务聚合每个分片的所有对象,再将结果返回给调用者。

分页有点复杂,单个的可以用offset和limit限制,但分片的话要跟踪每个分片的游标,每个分片偏移量也可能不一样。

解决方案:将列表数据放入一个由bucket_id分片的表,仅用来列出对象,简化实现。

版本控制

![[Pasted image 20240606140115.png]]

object_version,这个字段控制,用户删除特定版本的对象时,增加删除标记

优化大文件上传

![[Pasted image 20240606141500.png]]

client-调用InitiateMultipartUpload->object storage,返回唯一标识uploadId

client-UploadPart->object storage,返回etag,也就是该部分的md5校验和

全部上传完成,client-(uploadId、part No,ETags)->object storage

data store重新组装,返回成功消息

问题:

重新组装后,旧部件没用了,需要GC

  • 惰性对象回收
  • 孤儿数据,如一半上传的数据

具体回收过程:

  1. gc将对象从/data/b复制到/data/d 的列表,跳过object2和object5,因为他们删除标志是true
  2. 更新object_mapping表,更新object3的file_name和start_offset,通常是有大量的只读文件时才会压缩

![[Pasted image 20240606165718.png]]

背景

当时用的是moosefs,选型很简单,就是考虑支持 POSIX 接口,方便查看

文件系统 --》网络文件系统 --》分布式文件存储系统 --》S3等对象存储系统

ceph

架构

相关推荐
Neituijunsir29 分钟前
2024.06.28 校招 实习 内推 面经
c++·python·算法·面试·自动驾驶·汽车·求职招聘
测试界的世清1 小时前
2024最全软件测试面试八股文(答案+文档+视频讲解)
软件测试·面试·职场和发展
空青7262 小时前
AOP与IOC详解
java·服务器·分布式·后端·中间件·面试·架构
Neituijunsir4 小时前
2024.06.27 校招 实习 内推 面经
c++·算法·面试·车载系统·自动驾驶·汽车·求职招聘
醉颜凉4 小时前
多态的优点
java·面试·职场和发展·多态的优点·可替换性·可扩充性
窗边的anini5 小时前
【前端面经】MiniMax 面试
前端·面试·求职
modelsetget6 小时前
NIO为什么会导致CPU100%?
java·面试·nio
椰汁3315 小时前
随机文章生成器:Node.js与JSON数据的创意碰撞
javascript·后端·面试
cc的牛奶ovo16 小时前
还不会Promise吗?这篇文章让你再也不用被面试官拷打!
前端·javascript·面试
向阳逐梦16 小时前
使用getline()从文件中读取一行字符串
算法·面试·架构