分布式存储系统中重删功能的设计

本文从“如何设计一个具有重删功能的存储系统”为引子, 不按知识点来简单罗列,而是以需要什么讲什么的方式来深入展开。 概念性的东西比较多,计划先作为引文,然后里面的有必要深究的知识点再单独整理文章发出。

什么是重删

一般有哪几种重删设计思路:

在线重删,各自优缺点

离线重删,各自优缺点

对于全闪的特性(磨损)重删设计方案的选择

无重删时的I/O路径是什么样的

数据存取位置的管理:元数据

有重删时I/O路径是什么样的

重删的原理:

管理方式的变更:需要增加一个哈希值-物理地址的元数据表来管理

由重删功能引发的逻辑地址-物理地址表项的变更: 一个逻辑地址现在可以对应多个物理地址

插入流程,非重复数据与重复数据,主要关注互斥

这里的垃圾回收是指什么,是通过什么来实现的

怎样的数据算是垃圾数据?即如何知道哪块数据占用的空间可以被释放?

若一个P对应的L都不存在,则表示没有L处的数据需要这一引用,则可以被释放。

垃圾回收是如何进行回收的?

与垃圾回收的互斥

查询流程

I/O栈管理

多层管理:转发层、卷层、池层、驱动层、物理层

I/O路径的生产者-消费者模型

线程模型

基本原则: 每个卷绑定到一个线程中,避免处理多个卷的IO请求时,需要频繁加锁去锁的耗费资源的操作。

每层的水龙头:I/O的静默管理

静默管理

为什么需要静默管理呢? 试想一个主机一直在接收请求,在整个I/O栈的每一层都可能有正在处理的请求,它们各自都依赖于一些配置信息有条不紊地进行处理。
突然发生了一个事件,这个事件需要更改各层的一些配置信息。毫无疑问,直接更改时不行的,只有当各层“暂停”处理请求后,没有正在处理的请求,才能安全地更改。
那么就需要一套机制,来实现以下几项功能:
1。监测事件的发生。这是典型的事件发布-订阅模式。事件发生时要能够通知各层,各层才能进行处理。事件相关的整理见博客事件驱动设计模式
2。各层在接收到事件后,尽快将本模块的流程中的请求清空,从而达到静默状态,从而允许整个系统尽快地进入静默状态。
为什么要尽快呢?因为在理想中我们希望这些事件不影响主机的I/O,即主机在这个时候还可以正常下发I/O。当然此时实际上各层并不具备处理I/O请求的能力。那么如何处理呢?这就需要一个队列来将请求暂存,对于上层来说请求可以下发,只不过没有回应。这个队列会在静默完成之后按照正常I/O请求的方式重新下发,下层再正常进行处理。
“尽快”如何实现?这就看各层的业务逻辑了。原则上就是将正在申请内存的请求释放掉,正在进行转发的请求释放掉,耗时的刷盘暂停掉。这些未完成的请求由事务模块刚开始便加入一个链表,保证在完成前不释放,所以此处的释放请求不会对数据的完整性造成影响。
3。各层静默处理结束后,此时请求追踪器计数应该是0。这时就可以切换配置信息。切换完成之后通知上层静默结束,然后会重新下发加入队列的请求以及主机的请求。
那么如何来追踪这些请求的完成情况呢?请看下文:

多层模型中请求完成情况的追踪

按照业务进行划分,每层都需要一些处理,处理完成后将请求发给下层,下层完成后将请求结果返回给上层。这是一种典型的分层设计思路。
显然我们需要一种机制来描述这些请求的返回情况。这是很有用的,我们可以通过这个设计来知道某层下发的请求是否已被完全返回。

如何追踪

元数据管理

常见的元数据的组织形式

元数据的本质实际上就是建立一种k-v的映射关系,支持增删改查的操作。

查询

插入

事务的引入

事务的设计:镜像/日志、线程调度

事务,单机系统的一致性与分布式系统的一致性

多线程调度的实现

删除

持久化(下刷)

集群及高可用

为什么需要集群,而不能单机?

这个问题从性能和可靠性两个角度考虑就很显然了。
从性能角度来讲,每个节点作为一个单机,无论配置有多高(更快的CPU、更大的内存、更快的硬盘等等),还是软件优化有多好(更好的多线程调度,等等),其硬件性能始终是有限的。
从可靠性角度来讲则更显然,单机若发生故障则没得玩了,集群则可以采取一些高可用的设计使得剩余节点或备份节点来接管故障节点,从而保证业务的连续性和数据的可靠性。

每个节点与集群的关系是什么?

基本思想是配置与业务分离,其中配置信息由所有节点共享,业务为每个节点独立。这样既可以实现集群中每个节点的差异化工作,还可以保证集群中每个节点的配置的统一性(无论是在正常业务还是在单点故障场景下)。

每个节点都有各自的独立的数据进行业务管理。对于整个集群需要维护一份公共的数据作为配置管理,该份公共数据需要实现每个节点的一致性。
显然每个节点是独立的硬件设备,具有独立的CPU、内存。因此需要一套机制来维护多个节点之间公共数据的同步。此处便涉及到了集群间节点的通信方法。

集群中的节点如何保证数据的一致性

从需求出发,不难想象我们需要实现这样的效果:业务端的某个数据需要发生更改,需要

公共数据与独立数据的交互

作为一个框架,我们需要设计一种通用的消息收发功能用于日后的开发。
通用地讲,我们需要能够从集群向某个节点发消息,也需要能够从某个节点向集群发消息,另外也需要节点与节点之间直接发消息。
基于以上三个基本功能,就可以按照具体业务设计不同的框架。

集群中是如何通信的

那么如何进行节点间通信呢?
我们知道进程间通信主要有管道、消息队列、共享内存、socket通信(还可用于主机-主机)等方式。

如何管理节点?

AB型架构:归属节点的设计

对于集群来讲,这个集群对外提供一个逻辑对象,但是集群内部还是每个节点各自在工作。在各层软件的设计中,我们需要指明对于该层来讲,其请求是使用哪(几)个节点来处理的。
AB型架构中,假设两个节点组成一个集群,对于一个上层请求来讲,都需要其中的一个节点来处理,另一个节点可以做点别的(例如镜像本节点,或什么都不做只做热备)。这样的话对于每个卷对象来说就需要一个归属节点来表示该请求要使用哪个节点来处理。
在代码实现过程中,我们可以想象出每个节点上都运行着相同的代码,那么如何来实现每个节点进行不同的工作呢?
可以这么实现:
每个节点都由一个全局唯一的编号来区分,在集群管理层中使用一个变量来保存每个节点的存在情况。这个变量可以是一个bitmap(因为一个系统设计时就已经固定了集群中最大节点的数目)。
以四节点集群系统为例,我们可以使用一个包含4个Bit的变量nodes来表示,

节点编号3210
存在标记0/10/10/10/1

举例说明:若集群中无节点,则该值中每个编号所对应的bit位标记都为0,nodes=0;
若集群中编号为0的节点存在,则该值中编号为0所对应的bit位标记为1,其他仍旧为0,nodes=1;
若集群中编号为0和1的节点都存在,则该值中编号为0和1所对应的bit位标记为1,其他仍旧为0,即nodes=3;
这样每个节点的存在与否都可以通过一个值来说明。以此为基础,我们可以衍生出来许多方法,例如维护一个当前已经稳定运行的在线节点的set,再通过硬件检测来维护一个集群中动态变化的节点的set,对两者按位与则可以知道是否有节点想要加入或退出,此时便可以通过一些自定义事件的回调机制处理一些逻辑,例如暂停当前存活节点各模块任务,准备接管等操作。对这些节点事件的处理便抽象出了节点的生命状态,而后才能考虑状态机的实现。这点将在下个小节说明。
将该配置信息在所有节点中同步,则每个节点都可以知道整个集群中的节点的存在状况,这样也就能知道哪个节点不在集群中了。

int node_set;

int add_node_to_node_set(node_set,node_id){
	return	node_set | (1 << node_id);
}
int remove_node_from_node_set(node_set,node_id){
    return node_set & ~(1<<node+id);
}
bool is_node_in_node_set(node_set,node_id){
	return ((node_set &(1<<node_id))!=0);
}

集群管理层集群管理层
节点A节点B
节点的生命状态与状态机

集群中多个节点的管理是个复杂的工作,因为要考虑许多东西:集群的建立,节点的加入、离开,其中节点的加入和离开又需考虑正常和异常的情况,这就又涉及到一些组合事件序列的处理。因此首先需要建立节点生命状态,然后才能基于其状态的转换建立其状态机的管理。
当然判断节点在集群中的可用性是个十分复杂的工作。由于分布式系统中的网络分区性,如果只是用心跳线检测来决定是否某个节点在不在线是不准确的,因为可能只是网络阻塞无法即时通信,这时候就需要一些其他的手段。
例如Redis中的Sentinel分为了检测主观下线状态和检测客观下线两个角度(【2】p234)来考虑。
1.主观下线:Sentinel还是以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的回复来判断实例是否在线。若一个实例在指定的时间内一直向Sentinel返回无效回复,则Sentinel会判断该实例为主观下线状态,修改该实例对应的属性标记为主观下线。
2.客观下线:由于主观下线的判断是“独裁的”,可能是不准确的,因此最好的办法还是听听其他Sentinel的意见。因此又引入一个客观下线的判断。其实现是基于一个quorum仲裁的方法,简单来讲,当认为某个主服务器已经进入下线状态的Sentinel个数超过了配置中的一个quorum值,那么就认为这个主服务器已经进入客观下线状态。
当然之后的故障接管就又涉及到了一个选举的过程,一般采用的是PAXOS算法,此处先不展开。

高可用

高可用的本质是什么?

数据的可靠性是基于以下几点来考虑的:
一:内存是易失性的,这意味着如果我们不做任何保护,当系统发生掉电故障后,内存中的数据就会丢失。
二:整个系统是多层结构,任何时候各层中都有可能有正在处理的请求,那么当系统发生任何软件、硬件故障的时候,如果我们不做处理,就会导致数据不完整或者丢失。

基于以上两点,实际上我们就可以想到高可用的本质就是能够时刻保证系统中内存中正在处理的数据的完整性。

高可用的实现

首先应该完成的是,当系统发生故障(无论是软件故障还是硬件故障,包括掉电故障),我们如何来保护当前内存数据不会丢失?

一方面我们可以想到使用新的NVME来通过硬件角度来实现,但是毕竟昂贵,我们还是需要考虑向下兼容的问题:如何来通过软件的方法来实现内存数据的保存?
我们知道,当这个软件发生软件故障的时候,整个进程就会崩溃,那么就无法依赖这个进程来完成。这样,我们就可以合理地想到使用另一个进程来作为这个进程的守护进程,专门来负责做一些主进程无法处理的事情。
此处便是将内存中的数据持久化到磁盘中。
那么此处就涉及到了内存管理:整个系统如何进行内存管理?内存如何申请、分配、释放?如何来表示我们需要将某些内存页刷到磁盘中?又是如何刷到磁盘中的?实现这个功能需要什么样的管理结构(例如刷写到磁盘中就需要考虑一致性校验,之后恢复到内存中是如何完成的?),另外可能还有一些优化,例如这个内存文件的压缩等。

内存管理

我们知道申请内存使用malloc,释放内存使用free。这种方式申请内存时是如何处理,有什么坏处以至于很多数据库组件如Redis,memCached,MariaDb都自己封装一层内存管理呢?他们又解决了什么问题?通过什么来解决的?
要明白以上几点就需要先从Linux内存管理说起。

Linux是如何进行内存管理的?

主要涉及到段页式内存管理方法。考虑以下问题:
为什么使用段页式来寻址?虚拟内存和物理内存如何建立映射关系?寻址范围如何确定?如何根据内存硬件容量的提升来增加寻址范围(多级页表的发展),如何来进行寻址(地址转换)?缺页中断怎么实现?什么是伙伴系统?为什么要发明伙伴系统?内存碎片是怎样产生的(空闲页的查找算法)?如何避免内存碎片过多?如何进行垃圾回收?malloc/free的时候发生了什么?
参考另一篇文章:Linux内存管理

为什么需要内存池的形式来自己管理?
如何实现一个内存池?Redis,memCached,MariaDb是怎么做的,相互之间有什么区别?改进了什么?

其他特性:快照

对于一个存储系统,我们需要一些其他的高级特性,例如快照功能。
快照是什么呢?实际上就是一个卷在某个时间的备份。要实现快照,就需要先理解数据的存储方式和元数据的管理方式。
要实现某时的备份,最简单的办法就是直接将整个卷中的所有数据进行复制。然而这样显然很不合理:每次进行快照的时候都将整个卷进行复制,严重影响性能,也严重浪费空间。
因此有了一些降低复制数据量的设计,主要的实现一般有两种:COW和ROW。

COW即Copy On Write,写时复制技术。这种方法

参考文献:
【1】 《MariaDB原理与实现》 张金鹏等
【2】 《Redis设计与实现》 黄建宏
【3】 《Unix核心编程》
【4】 《深入理解计算机系统》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值