Flink——Exactly-Once

Flink——Exactly-Once,第1张

Apache Flink是目前市场最受关注的流计算处理引擎,相较于Spark Streaming的依托Spark Core实现的微批处理模型,Flink是一个纯粹的流处理引擎,其基于 *** 作符的连续流模型,可以达到微秒级别的延迟。

Flink实现了流批一体化模式,实现按照事件处理和无序处理两种形式,基于内存计算。强大高效的反压机制和内存管理,基于轻量级分布式快照checkpoint机制,从而自动实现了Exactly-Once一致性语义。

1 数据源端

支持可靠的数据源(如kafka), 数据可重读

Apache Flink内置FlinkKafkaConsumer010类,不依赖于 kafka 内置的消费组offset管理,在内部自行记录和维护 consumer 的offset。

2 Flink消费端

轻量级快照机制: 一致性checkpoint检查点

Flink采用了一种轻量级快照机制(检查点checkpoint)来保障Exactly-Once的一致性语义。所谓的一致检查点,即在某个时间点上所有任务状态的一份拷贝(快照)。该时间点是所有任务刚好处理完一个相同数据的时间。

间隔时间自动执行分布式一致性检查点(Checkpoints)程序,异步插入barrier检查点分界线,内存状态自动存储为cp进程文件。保证数据Exactly Oncey精确一次处理。

(1) 从source(Input)端开始,JobManager会向每个source(Input)发送检查点barrier消息,启动检查点。在保证所有的source(Input)数据都处理完成后,Flink开始保存具体的一致性检查点checkpoints,并在过程中启用barrier检查点分界线。

(2) 接收数据和barrier消息,两个过程异步进行。在所有的source(Input)数据都处理完成后,开始将自己的检查点(checkpoints)保存到状态后(StateBackend)中,并通知JobManager将Barrier分发到下游

(3) barrier向下游传递时,会进行barrier对齐确认。待barrier都到齐后才进行checkpoints检查点保存。

(4) 重复以上 *** 作,直到整个流程完成。

3 输出端

与上文Spark的输出端Exactly-Once一致性上实现类似,除了目标源需要满足一定条件以外,Flink内置的二阶段提交机制也变相实现了事务一致性。支持幂等写入、事务写入机制(二阶段提交)

这一块和上文Spark的幂写入特性内容一致,即相同Key/ID 更新写入,数据不变。借助支持主键唯一性约束的存储系统,实现幂等性写入数据,此处将不再继续赘述。

Flink在处理完source端数据接收和operator算子计算过程,待过程中所有的checkpoint都完成后,准备发送数据到sink端,此时启动事务。其中存在两种方式: (1) WAL预写日志: 将计算结果先写入到日志缓存(状态后端/WAL)中,等checkpoint确认完成后一次性写入到sink。(2) 二阶段提交: 对于每个checkpoint创建事务,先预提交数据到sink中,然后等所有的checkpoint全部完成后再真正提交请求到sink, 并把状态改为已确认。

整体思想: 为checkpoint创建事务,等到所有的checkpoint全部真正的完成后,才把计算结果写入到sink中。

title: GFS 小结

tags:

categories:

comments: true

date: 2017-06-12 17:00:00

提到分布式系统,有一个无法绕开的话题—— Google 三驾马车。本文就 GFS 概括介绍。

与传统的分布式系统相比,在大方向上,GFS 同样追求高性能、高可靠性、高可用性,同时 Google 基于自身的生产环境、技术环境,有一些特殊的设计思路。

GFS 架构比较简单,一个 GFS 集群一般由一个 master 、多个 chunkserver 和多个 clients 组成,在 GFS 中,所有文件被切分成若干个 chunk,并且每个 chunk 拥有唯一不变的标识(在 chunk 创建时,由 master 负责分配),所有 chunk 都实际存储在 chunkserver 的磁盘上。为了容灾,每个 chunk 都会被复制到多个 chunkserver。

系统架构如下:

在整个集群中,为了简化设计,master 是单节点,它管理着所有文件系统的所有 metadata:命名空间、访问控制信息、文件和 chunk 的映射关系、chunk 的存储位置。同时 master 还管理系统范围内的各种活动:chunk 创建、复制、迁移、回收,chunk lease 等等,是系统中最核心的部分,后面会继续进一步描述 master 是如何工作的。

Chunkserver 真正存储着所有 chunk,chunkserver 依托于 linux 文件系统,所以它本身不需要缓存文件数据,直接利用 linux 系统的数据缓存,简化了设计。

Master 是整个 GFS 的核心,这里重点介绍下 master 的存储以及工作。

所有的元数据都存储在 Master 的内存中,以保证 Master 的性能。大部分元数据同时会以变更记录的形式保存到 *** 作日志中, *** 作日志会在本地磁盘中持久化同时被复制到其他的 Master 上(虽然是 single master,但是会有备份节点备份 Master 的相关数据,比如 *** 作日志、checkpoint 文件,以保证可靠性)。

Master 会在后台周期性的扫描所保存的状态信息,因为全部在内存中,所以效率非常高。通过这种周期性的扫描,master 实现 chunk 回收、chunkserver 宕机时 chunk 的复制、以及迁移 chunk ,实现 chunkserver 的负载均衡。

但是, chunk 的位置信息不会被持久化,而是在每次 master 启动时(以及启动后定期执行),或有 chunkserver 加入时,master 会轮训所有 chunkserver 获取所有的 chunk 信息然后保存在内存中。这种方式简化了 master 和 chunkserver 的数据同步,当然数据定期轮训的缺点就是实时性稍差。

*** 作日式是元数据唯一的持久化记录,它还定义了并发 *** 作的执行顺序的逻辑时间线,所以 *** 作日志的完整性得到保证,才能保证 GFS 的可靠性,否则会丢失文件或者 client 的 *** 作。因此 *** 作日志会被复制到多台备份节点,而且,只有 master 把 *** 作日志持久化到本地并且复制到远程之后,才会响应客户端的请求,保证数据不丢失。

随着时间的增长, *** 作日志会越来越大,当日止增长到一定量时,master 会将所有的系统状态做一次 checkpoint(可以理解为持久化某一个时间点的全部状态数据),后续的 *** 作变更会写入到新的日志文件,这样在重启或灾难恢复时,master 只需要加载最新的 checkpoint 文件到内存,然后重新执行最新的一部分 *** 作日志即可。(这也是比较通用的一种灾备方法,定期做 checkpoint,然后重新记录 *** 作日志,恢复时基于 checkpoint + operation log)

Checkpoint 文件以压缩 B- 树的结构存储,能直接映射到内存,无需额外解析,大幅提升了速度。同时创建 checkpoint 时,master 会启动独立的线程,不会阻塞正在进行的 *** 作。

Master 节点执行所有的命名空间管理、chunk管理以及负责垃圾回收。

Master 在 *** 作命名空间是基于锁实现的,在 *** 作对应的文件或目录时,会给对应的文件/目录加读锁以及读写锁,eg:对于一个 /home/usr/zhaif 的 *** 作,会依次给父目录 /home,/home/usr 加读锁,读锁可以防止正在读取得文件、父目录被删除、改名,同时给 /home/usr/zhaif 加读锁或写锁(根据 *** 作类型),当对 *** 作目标的 *** 作是修改类 *** 作时,会加写锁,保证并发场景下互斥写。

上文提到,master 会负责 chunk 副本的存储位置,即存储在哪些 chunkserver 上,master 会最大化的保证数据可靠性,同时最大化利用网络带宽。

在创建一个 chunk 时,master 选择存储空副本的初始位置时,会考虑一下几点:

除了管理 chunk 副本的存储位置,master 会在 chunk 有效副本数小于指定数量时重新复制 chunk 副本,以保证数据可靠性。

最后,Master 会定期对所有副本负载均衡,检查当前副本分布情况,然后移动副本位置以更搞笑的利用硬盘空间和负载。

GFS 的文件删除不会立刻回收物理空间,而是惰性的(现如今,惰性回收在存储系统中是一种比较常见的策略,比如 redis 回收过期数据,分配的内存空间)。这种回收机制使系统更简单、更可靠、更高效。

当一个文件被删除时,master 只是将文件改名,标记为已删除。Master 会对命名空间做定期扫描,会删除一定时间前标记删除的文件,同时删除其在命名空间中的记录以及相关元数据,此时一个文件才被真正的删除。

Master 在常规定期扫描的过程中会发现一些孤儿 chunk,即不被任何文件包含的 chunk,然后删除他们的元数据。Chunkserver 在和 master 定期交互时,汇报了其所有的 chunk 信息,master 会告知其不存在的 chunk,chunkserver 得知后会删除这些 chunk 副本。

这种惰性删除的主要问题是空间利用率,尤其的在存储空间紧缺时。所以 GFS 也提供了通过显示的再删除一次已经删除的文件来加速空间回收,另外也允许用户根据需要对不同的目录设置不同的回收策略,eg:指定用些目录的删除策略为即时删除,而不是惰性删除。

Master 的写 *** 作是基于 lease 机制(后文介绍),当 master 每次分配 lease 时都会增加对应的 chunk 的版本号,然后所用最新的副本,通过版本号区分当前的和过期的副本。

GFS 在设计是采用 client 和 API 协同设计的思路,所以在读写过程中 client 也不单纯是发读请求或写请求,还包括其他一些 *** 作。

Client 不通过 master 节点读写文件,而是从 master 那获取读写 *** 作的需要联系的 chunkserver,为了避免频率的和 master 联系,client 会缓存 从 master 获取的 metadata,后续 *** 作直接和 chunkserver 沟通实现读写。一次简单的读流程如下:

相较于读 *** 作,写实现更为复杂一些。所有的写入 *** 作会在所有 chunk 的副本上执行,GFS 采用 lease 机制来保证多个 chunk 副本之间变更顺序一致。

Master 会选择一个副本分配 lease,拥有这个 lease 的 chunk 被称为 primary,其他副本则是 secondary。Primary 会将对 chunk 的 *** 作序列化,然后其他 secondary 按也这个序列执行修改,从而保证所有副本变更一致。

Lease 有效期初始为 60s,primary chunk 在完成修改 *** 作后可以申请延长 lease 有效期,同样的 master 在一些情况下可以提起取消 lease。Master 和 chunkserver 之间会有定期的心跳监测,传递心跳信息是可以带上这些 lease 的验证请求或者批准信息。Lease 机制极大的简化的 master 的负担,将写 *** 作保证数据一致性的工作分担给 chunkserver,使得 master 变得很轻量。

下图是一次写 *** 作的流程:

GFS 将写 *** 作拆分为数据流(对应3)和控制流(对应4),数据流以 Pipline 的方式推送到所有副本。

GFS 同时提供了一个种原子的写入 *** 作——记录追加。相比普通的写入 *** 作,追加只需指定要写入的数据,不需要提供偏移量(即要写入的位置)。GFS 会保证追加 *** 作至少一次原子性写入。记录追加的控制流程同上文描述基本相同,却别在于 primary 会检测此次追加后 chunk 是否超过最大值,如果达到最大值,primary 会先将当前 chunk 填充满,然后同步给 secondary 同样 *** 作,然后回复 client 要求其对下一个 chunk 重新执行追加 *** 作。

原子记录追加 *** 作在避免了使用一个分布式锁带来的开销,对于多 producer,单 consumer的场景以及合并多个来源文件的场景很契合。

GFS 是一个分布式系统,为了更好的 AP,一定程度上降低了对 C 的要求,其一致性模型是比较宽松。下图是变更后文件状态,其中:

从上文的写入数据流程可以发现,串行的写数据secondary 和 primary *** 作顺序是一直的,如果成功,则一定是 defined,如果失败,则不一致,比如 primary 写成功了,而有一个 secondary 写失败。同样的道理,在并行场景下,写失败会不一致,但是成功的话只能保证一致,因为并发 *** 作可能会导致一个文件 region 内包含来自多个 client 的写 *** 作,所以是 undefined

记录追加 *** 作是原子的,GFS对于此 *** 作能保证的是 至少一次成功 语义,所以有可能会在某个副本上发生多次追加,但是 GFS 返回给 client 的 offset 都是 defined region 的起点,如果这期间在某个副本的 *** 作被重复追加了,此时它的 offset 会比其他大,后续的 *** 作对所有副本都会从这个最大的 offset 开始追加,或者被追加到其他 chunk 上,因此对于记录追加 *** 作而言,如果执行成功,文件 region 状态是定义的但会有部分不一致。

GFS 通过 Checksum 叫校验数据是否损坏,比如因为宕机丢失了一些修改 *** 作而导致失效,此时 master 会标记失效,不在返回给 client 失效的副本位置信息,并尽快回收。 对于已经被 client 缓存的失效副本信息,当 client 访问这个失效副本时,一个失效副本会返回提前结束的 chunk,从而 client 能得知重新联系 master 获取最新的位置信息。

另外,正如上文所述, master 也会和 chunkserver 通过心跳来检测宕机,并校验数据有效性,在发现问题后会尽快恢复。

GFS 通过快速恢复和复制保证整个集群的高可用性,无论 master 还是 chunkserver 都可以在数秒内重启并恢复状态。

Chunk 会被复制到不同的机架上的不同 chunkserver,当某台 chunkserver 失效或者其上的 chunk 已损坏时,master 会继续复制已有的副本,保证每个 chunk 的可用性。

Master 服务器的状态会被复制,它所有的 *** 作日志、checkpoint 文件都会被复制到多台机器,对 master 服务器的状态的任何 *** 作都要等 *** 作日志被复制到备份节点后本机磁盘后才会被提交生效。所以 Master 宕机后,重启后不会有任何数据丢失,如果无法重启或磁盘故障,则可以选择拥有全部 *** 作日志的备份节点启动一个新的 master 进程。由此可以保证 master 的可靠性。

同时,还存在一些 shadow master ,在 master 宕机时能可以提供 read-only 服务,但要比 master 慢一些(通常不到 1s),它们通过读取 *** 作日志副本的并顺序执行方式保证其和 master 以相同的方式变更。同样的,shadow master 也会和 chunkserver 定期交互检测 chunkserver状态、拉取数据。

GFS的诞生来源于google日益增长的数据量的处理需求,它是一个可扩展的分布式文件系统,用于大型分布式数据密集型应用,在廉价的通用硬件上运行时提供容错机制,并且可以为大量客户端提供较高的聚合性能。

它的设计由当前和预期的应用负载(当时的)和技术环境驱动,与以前的文件系统的假设有着明显不同,因此gfs在设计上有几个不同的points:

当前已部署多个集群用于不同目的,最大的拥有1000多个存储节点,超过300TB的存储服务,并且有数百个客户端连续不断地高负载请求。

前面提到一些对应用负载和技术环境的观察,现在更详细地进行阐述:

虽然GFS不能提供像POSIX标准的API,但它提供一个相似的文件系统接口。文件在目录中按层次结构组织,并以路径名作为标识。支持create、delete、open、close、read and write files。

gfs支持快照和record append *** 作。快照以低代价创建文件副本或者目录树,record append支持多个客户端并发地写文件,保证每个独立客户端append的原子性。

一个gfs集群包含一个master和多个chunkservers,chunkserver被多个客户端访问,如图1所示。每一个都是普通linux机器上运行的用户态服务进程。资源允许的情况下,客户端可以和chunkserver部署在同一台机器上。

文件被划分为固定大小的块。每个chunk由一个独一无二的64位大小的chunk handle所标识,chunk handle在chunk被创建时由master分配。每个chunk的副本分布在多个机器上,系统默认为三副本模式,用户也可以为不同namespace的文件指定不同级别的副本。

master包含文件系统的所有元信息。包含namespace、访问控制权限信息、文件到chunks的映射、当前chunks的位置信息。也控制着全局的活动,像chunk租约管理、gc、chunk迁移等。master通过心跳的方式与每个chunkserver交流来发送它的指令和收集状态。

客户端与master的交互涉及元信息 *** 作,所有数据 *** 作直接与chunkserver交互。gfs不提供POSIX标准API,因此不需要挂接到linux的vnode层。

客户端和chunkserver都不缓存文件数据。大多数应用传输大文件,客户端缓存收益很低。chunks作为本地的文件存储,linux系统有自己的buffer cache,chunkserver不需要再增加缓存。

单master简化了系统的设计,但是会有单点的瓶颈问题,这是必须要解决的。客户端不会从master读写数据文件,客户端请求master它需要的交互的chunkserver信息,并且将其缓存一段时间,后续的 *** 作直接与chunkservers交互。

客户端会发送请求给离它最近的一个副本。实际上,客户端通常会向master请求多个chunk的信息,以减少未来与maser交互的代价。

chunk size定为64MB,相比普通的文件系统的block size更大。每个chunk副本以linux文件的形式存在chunkserver上,仅根据需要来扩展。使用lazy space allocation的方式避免空间浪费。

large chunk size有以下几个优点:

但是large chunk size with lazy space allocation也有其缺点:单个文件可能包含很少数量的chunks,或许只有一个,当许多客户端访问相同文件时这些chunks成为热点。但由于目标应用大多是顺序的读多个large chunk文件,热点并不是主要的问题。

然而GFS第一次用于批处理队列系统时确实出现了热点问题,数百个客户端同时访问一个单chunk文件,存储这个文件的几个chunkserver超负荷运转,当时通过错开应用的启动时间避免了这个问题,一个潜在、长期的解决方法是允许客户端从其它客户端读取数据。

master保存三种类型的元数据:

所有元数据都保存在内存中 。对于元数据的内存 *** 作是很快的,后台任务周期巡检整个状态也是比较简单高效的。周期巡检用于实现chunk gc、在chunkserver故障时重新构造副本、chunk迁移以平衡多个chunkserver的负载和disk usage。

虽然系统的容量受master内存大小的限制,但这并不是一个严重的问题,64MB的chunk只需要不到64byte大小的元信息,如果一定需要更大的文件系统,那么增加内存的代价相比为可靠性、性能和灵活性等付出的代价是较小的。

前两种类型的元数据通过写日志来保证持久化,并且会复制日志到远程机器上。master不需要将chunks的位置信息持久化,而是在master启动和新的chunkserver加入集群时向每个chunkserver询问它的位置信息,之后通过心跳信息监控chunk位置变更信息。chunkserver作为最后一关是确切知道自己本地有没有哪些chunk的,因此维护一个一致性的视图是没有必要的。

operation log 包含元数据的变更记录, 它是GFS的核心 ,它不仅仅是唯一的元数据持久化记录,也表明了并发 *** 作的逻辑时间线。文件、chunks和它们的版本都是由逻辑时间线唯一标识。元数据变更记录在持久化之前对客户端是不可见的,而且日志被复制到多个远程的机器,只有相应的记录在本地和远程都持久化到硬盘了才可以回复客户端。master使用批处理log的方式提高系统的吞吐。

master通过回放日志来恢复文件系统的状态,为提高恢复速度需要保持log量足够小。当log增长超过特定大小时,master会checkpoint它的状态,以加速恢复提高可用性。构建checkpoint可能需要花费一段时间,因此master以一种不delay后续变化的方式来组织内部状态,先switch到一个新的日志文件,使用独立的线程创建checkpoint,新的checkpoint包含了所有switch之前的变化。几百万个文件的集群在一分钟内可以完成,完成后将同时被写入本地和远程。恢复只需要最新的checkpoint和之后的日志文件,旧的checkpoints和日志文件可以完全删除。

GFS使用一个宽松的一致性模型,这种模型可以很好地支持分布式应用程序,而且实现起来简单有效。

file namesapce变化(例如文件创建)是原子的,使用namespace锁。

master的operation log定义了这些 *** 作的全局顺序。

数据变化后文件region的状态取决于变化的类型,是否成功、失败或者是并发的。Table1做了总结。如果所有客户端都能看到相同的数据,无论它们读的是哪个副本,则这个file region是一致的。

数据变化有两种:writes或者record appends。write是指从应用指定offset处开始写数据,record append指即使存在并发冲突,数据也要被原子地append到文件至少一次,但offset是由GFS选定。

GFS保证在一系列成功的mutations后,file region是defined,通过下面两点来保证:

过期的副本将不会再涉及到任何mutation,master也不会将其位置信息回应给客户端,不久后将会被gc。但客户端缓存的信息可能包含过期的副本,缓存失效存在一个时间窗口,文件再次打开也会清除该文件的所有chunk信息。由于大多数文件是append-only,过期的副本通常返回的是过早的结尾而不是过期的数据。

介绍客户端、master和chunkserver之间如何交互来实现数据变化、原子追加写和快照的。

使用租约的方式维护多个副本间一致的mutation order。master授权租约给副本中的一个,称之为primary。primary为chunk的mutaions选择一个顺序,所有副本都按照这个顺序apply。

租约机制最小化了master的管理overhead。租约初始的超时时间是60s,如果chunk一直在变化过程中,primary可以申请续租。这些授权和续租请求由master和chunkserver之间的心跳信息携带。master也可以尝试撤销租约,即使它与primary失去了联系,也可以等租约过期后安全地授权给另外一个副本。

在Figure2中,跟随着写入控制流展示了处理过程:

如果一个写请求比较大或者超出了chunk边界,GFS客户端将它拆为多个写 *** 作,但是多个 *** 作可能与其它客户端并发交叉写入,因此共享的fie region最终可能包含多个不同客户端的碎片,这会造成 一致性模型 中所描述的file region处于consistent but undefined状态。

数据以pipline的机制在chunkserver链上线性传输,而控制流是从客户端到primary再到所有的其它副本。分离数据流和控制流可以更高效地使用网络。可以带来以下好处:

GFS提供原子的append operaton叫作 record append 。传统的write中,客户端指定offset,并发写相同region时不是serializable,最终region可能包含多个客户端的碎片数据。而对于record append,客户端仅指定数据,GFS保证至少一次成功的原子append,offset由GFS选定,与Unix的O_APPEND模式相似。

多个客户端并发 *** 作相同文件是比较重的。如果处理传统的write,客户端需要额外复杂和昂贵的同步逻辑,像分布式锁。而record append仅需要primary增加一点额外的逻辑:primary检查是否并发append数据的chunk会超出max size,如果会超出则将chunk填充到max size,并且告诉所有二级副本同样 *** 作,然后回应客户端指出这个 *** 作应该选择另一个chunk重试;大多数情况下记录是在max size内的,primary将数据append到自己的副本,并告诉所有二级副本按照确切的offset写数据,最后回应给客户端。

如果中间出现错误,客户端重试,相同chunk的副本可能包含不同的数据,可能包含相同的记录或者一部分相同,GFS不保证bytewise identical,仅仅保证数据至少有一次被成功地原子写入。从report success逻辑可以容易得出,数据必须是在某个chunk的所有副本上以相同的offset写入。在此之后,所有副本都与记录end一样长,即使后面不同的副本成为primary,任何将来的记录也将分配到更高的offset或者不同的chunk。根据上述的一致性保证,成功的record append的region是defined和一致的,而中间的region是不一致的(undefined)。GFS的应用可以处理这种不一致的region(272)。

snapshot *** 作拷贝一份文件或者目录树,几乎是实时的,同时最大程度减少对正在进行中的mutation的干扰。

像AFS一样,使用标准的COW技术实现snapshot。当master接收到一个snapshot请求,首先将所有涉及到chunks的租约撤销,这保证了这些chunks后续的write将会先请求master查找租约持有者,master会创建一个新的副本来回应。

租约被撤销或者过期后,master将这个 *** 作记录日志到disk。新创建的snapshot引用元数据相同的chunks。

当snapshot *** 作完成后,客户端第一次要写chunk C,发送请求给master查询持有租约者,master察觉到chunk C的引用大于1,则让每个含有当前chunk副本的chunkserver创建一个新的chunk叫作C',所有创建都使用本地的副本,相比100Mb的网络本地速度大约是三倍速度。master授权租约给新的chunk C'中的一个并且回复给客户端,之后正常地写chunk。整个过程对客户端是透明的。

master执行所有的namespace *** 作。另外,它管理整个系统的chunk副本:

接下来,详细探讨这些细节。

许多master *** 作可能花费较长一段时间,比如snapshot *** 作需要撤销相关的所有chunks的租约。因此为了不delay其它master *** 作,在namesapce的regions上使用locks来确保串行化。

GFS没有按目录列出该目录中所有文件的结构,也不支持文件和目录的别名(unix中的硬链和软链)。GFS将完整的路径名到元数据的映射表作为它的逻辑namespace。使用前缀压缩,这个表可以有效保存在内存中。namespace tree中的每个节点都有一个关联的读写锁。

每个master *** 作在运行前都会获取一组锁。如果涉及到/d1/d2//dn/leaf,它将获取目录名称/d1、/d1/d2、、/d1/d2//dn上的读锁,完整路径/d1/d2//dn/leaf的读锁或者写锁。leaf可以是文件或者目录。

创建文件不需要对父级目录加锁,因为没有"目录"的概念不会修改它,而加读锁是防止它被删除、重命名或者snapshot。这种锁机制的好处是允许相同目录下并发的mutations。

一个GFS集群通常具有分布在多个机架上的数百个chunkserver,这些chunkserver也会被相同或者不同机架的数百个客户端访问。不同机架上的两台计算机之间的通信可能会跨越一个或者多个网络交换机。另外进出机架的带宽可能小于机架内所有计算机的总带宽。多级分布式对如何分发数据以实现可伸缩性、可靠性和可用性提出了独特的挑战。

副本放置策略有两个目的:最大化数据可靠性和可用性,最大化网络带宽利用率。不仅要在多台机器上放置,还要在多个racks上,即使整个racks损坏也可以确保部分副本保持可用。也可以利用多个racks的总带宽。

chunk副本创建有三个原因:

当master创建新的chunk时,根据几个因素考虑如何放置新的副本:

当chunk可用副本的数量低于用户指定时,master会重新复制。可能发生在几种情况:

需要重新复制的chunk根据以下几个因素确定优先级:

master限制集群和每一个chunkserver内的活跃的clone数量,另外chunkserver通过限制其对源chunkserver的读请求来限制在每个clone *** 作上花费的带宽。

master会定期重新平衡副本:检查当前副本的分布,迁移副本以获得更好的磁盘空间利用率和负载平衡。同样通过此过程,master逐渐填充一个新的chunkserver。另外,master通常更倾向于移除具有低磁盘利用率chunkservers上的副本,以平衡空间使用。

当文件被删除时,master记录日志,但不会立即回收资源,而是将文件重命名为包含删除时间戳标记的隐藏名称。如果这些文件存在时间超过三天(时间可配置),master巡检时会将其删除。在此之前,仍然可以用特殊名称来读取文件,并且可以重命名为正常名称来取消删除。当从namesapce中删除隐藏文件时,其内存元数据将被删除,这有效切断了所有chunk的连接,在对chunk namespace的扫描中,master识别出孤立的chunk并清除元数据。在心跳信息中,每个chunkserver报告其拥有的chunks子集,而master将回应不在存在于master元数据中的所有的chunk的标识。chunkserver可以自由删除此类chunk的副本。

这种gc机制相比立即删除有以下几个优点:

这种机制主要的缺点是当存储空间紧张时,延迟有时会影响用户的使用,重复创建和删除临时文件的应用可能无法立即重用存储。如果删除的文件再次被明确删除,GFS将通过加快存储回收来解决这些问题。还允许用户将不同的复制和回收策略应用于不同的namespace的不同部分中。

如果一个chunkserver故障或者chunk丢失了mutations,这个chunk副本可能是过期的。对于每个chunk,master都维护了一个chunk版本号。

当master授权租约给一个chunk时,这个chunk的版本号增加1,如果一个副本当前不可用了,则其版本号将不会领先。当chunkserver重新启动并报告其chunks集合和相关联的版本号时,master将检测到该chunkserver上具有过期的副本。如果master看到的版本号大于它记录的版本号,则认为在授权租约时失败了,因此将较高的版本号更新。

master在常规gc中删除旧的副本。另一个保护措施,在master回应客户端哪个chunk持有租约或者clone *** 作中chunkserver从另一个chunkserver读取chunk时会包含chunk的最新版本号。客户端或者chunkserver在执行 *** 作时会验证版本号。

这个系统最大的挑战之一是处理经常故障的组件。组件的质量和数量造成的问题会超出预期,组件故障可能造成系统不可能,甚至数据错误。接下来讨论GFS如何应对这些挑战,还有系统如何诊断不可避免问题。

使用两个简单有效的方式保证系统的高可用:快速恢复和复制。

master和chunkserver的恢复都是秒级别的。

master维护每个chunk的副本数量,当chunkserver下线或者checksum检测出错误副本时,master会通过已有副本来复制。尽管复制提供了很好的解决方式,但仍在探索其它形式的跨服务器冗余方案,例如奇偶校验或者纠删码,以适应不断增长的只读存储需求。在非常松耦合的系统中实现这些更复杂的冗余方案更具有挑战性。

master的 *** 作日志和checkpoint会被复制到多台机器上,状态的变化只有在本地和所有副本上都持久化以后才可以commit。master进程负责所有的mutations以及后台任务,当它宕机时可以很快重启,如果机器或者磁盘故障,GFS的外部监控将使用日志在其它节点重启新的master进程。在master宕机时,master的备节点只提供只读服务,它们不与master保持强一致,可能会落后于master,通常在1/4秒内。它们保证了那些不介意读到过期数据的应用的高可用读。类似于chunk的primary机制,master的备按照相同的序列应用日志。与master一样,在启动时从每个chunkserver拉取chunks的位置信息,与它们频繁交换握手消息来监控其状态。

每个chunkserver使用checksum来检测存储数据的损坏。数据损坏的chunk可以通过其它的副本来恢复,但是通过副本间比较来检验数据是不切实际的。正常的副本也不是完全一样的,如前文所讲,原子的append并不能保证完全一样的副本。因此每个chunkserver会维护自己的checksum。

每个chunk分为多个64kb的blocks,每个block包含一个32位的checksum,与其它元数据一样,checksum保存在内存中,依靠log持久化,与用户数据分离。

对于读,chunkserver在返回数据给请求者前先检测checksum,确保不会将出错的数据传输给其它chunkservers或者客户端。如果数据是坏的,chunkserver将错误返回给请求者并报告给master,请求者将会去读其它副本, master将会根据其它副本重新克隆一份。当新的副本创建以后,master指示chunkserver将错误的副本删除。checksum的计算不涉及I/O,对读的影响比较小,客户端通常尝试使用对齐block边界读来减少overhead。

为append写是做了checksum计算上的优化的,因为append写是主要的负载(相比于overwrite)。GFS只增量地更新最后部分block的checksum,为新的block的计算新的checksum。这样即使block已经损坏,新的checksum将与存储的数据不会匹配,下次读时将会与正常一样被检测出来。

如果一个写请求要写一个chunk中已存在的region,必要要先检验region的第一个和最后一个block的checksum,然后再重写,最后计算新的checksums。因为第一个和最后一个block可能含有不被重写的内容,如果这部分数据是损坏的,则新的checksum将包含错误的数据。

在idle时,checkserver可以扫描并检查不活跃的chunks,可以检测到冷chunks的错误,一旦错误被检测到,master可以创建一个新的副本。

GFS在设计上与传统文件系统有很多不同,这些点是基于对当时应用负载和技术环境的观察所重新设计,将组件故障看作平常的事件而非异常,为大文件的读取和追加写做优化,扩展和放宽了标准的文件系统接口以改善整个系统。通过监控、复制以及快速恢复能力提供容错能力,使用checksum机制来校验数据的正确性。通过将控制流和数据流分离,数据直接在chunkservers、客户端之间传输,为许多并发的各种任务的读取和写入提供了高吞吐量。大chunk size和租约机制使得master的 *** 作足够轻量化,使得这样一个简单中心化的master不会成为瓶颈。

GFS成功地满足了google的存储需求,作为研究、开发和数据处理的存储平台广泛地应用于google内部。

在B站的业务场景中,存在很多种不同模型的数据,有些数据关系比较复杂像:账号、稿件信息。有些数据关系比较简单,只需要简单的kv模型即可满足。此外,又存在某些读写吞吐比较高的业务场景,该场景早期的解决方案是通过MySQL来进行数据的持久化存储,同时通过redis来提升访问的速度与吞吐。但是这种模式带来了两个问题,其一是存储与缓存一致性的问题,该问题在B站通过canal异步更新缓存的方式得以解决,其二则是开发的复杂度,对于这样一套存储系统,每个业务都需要额外维护一个任务脚本来消费canal数据进行缓存数据的更新。基于这种场景,业务需要的其实是一个介于Redis与MySQL之间的提供持久化高性能的kv存储。此外对象存储的元数据,对数据的一致性、可靠性与扩展性有着很高的要求。

基于此背景,我们对自研KV的定位从一开始就是构建一个高可靠、高可用、高性能、高拓展的系统。对于存储系统,核心是保证数据的可靠性,当数据不可靠时提供再高的可用性也是没用的。可靠性的一个核心因素就是数据的多副本容灾,通过raft一致性协议保证多副本数据的一致性。

分布式系统,如何对数据进行分片放置,业界通常有两种做法,一是基于hash进行分区,二是基于range进行分区,两种方式各有优缺点。hash分区,可以有效防止热点问题,但是由于key是hash以后放置的,无法保证key的全局有序。range分区,由于相邻的数据都放在一起,因此可以保证数据的有序,但是同时也可能带来写入热点的问题。基于B站的业务场景,我们同时支持了range分区和hash分区,业务接入的时候可以根据业务特性进行选择。大部分场景,并不需要全局有序,所以默认推荐hash分区的接入方式,比如观看记录、用户动态这些场景,只需要保证同一个用户维度的数据有序即可,同一个用户维度的数据可以通过hashtag的方式保证局部有序。

整个系统核心分为三个组件:

Metaserver用户集群元信息的管理,包括对kv节点的健康监测、故障转移以及负载均衡。

Node为kv数据存储节点,用于实际存储kv数据,每个Node上保存数据的一个副本,不同Node之间的分片副本通过raft保证数据的一致性,并选出主节点对外提供读写,业务也可以根据对数据一致性的需求指定是否允许读从节点,在对数据一致性要求不高的场景时,通过设置允许读从节点可以提高可用性以及降低长尾。

Client模块为用户访问入口,对外提供了两种接入方式,一种是通过proxy模式的方式进行接入,另一种是通过原生的SDK直接访问,proxy本身也是封装自c++的原生SDK。SDK从Metaserver获取表的元数据分布信息,根据元数据信息决定将用户请求具体发送到哪个对应的Node节点。同时为了保证高可用,SDK还实现了重试机制以及backoff请求。

集群的拓扑结构包含了几个概念,分别是Pool、Zone、Node、Table、Shard 与Replica。

基于不同的业务场景,我们同时支持了range分区和hash分区。对于range场景,随着用户数据的增长,需要对分区数据进行分裂迁移。对于hash分区的场景,使用上通常会根据业务的数据量做几倍的冗余预估,然后创建合适的分片数。但是即便是几倍的冗余预估,由于业务发展速度的不可预测,也很容易出现实际使用远超预估的场景,从而导致单个数据分片过大。

之所以不在一开始就创建足够的分片数有两个原因:其一,由于每一个replica都包含一个独立的engine,过多的分片会导致数据文件过多,同时对于批量写入场景存在一定的写扇出放大。其二,每一个shard都是一组raftgroup,过多的raft心跳会对服务造成额外的开销,这一点后续我们会考虑基于节点做心跳合并优化减少集群心跳数。

为了满足业务的需求场景,我们同时支持了range和hash两种模式下的分裂。两种模式分裂流程类似,下面以hash为例进行说明。

hash模式下的分裂为直接根据当前分片数进行倍增。分裂的流程主要涉及三个模块的交互。

metaserver

分裂时,metaserver会根据当前分片数计算出目标分片数,并且下发创建replica指令到对应的Node节点,同时更新shard分布信息,唯一不同的是,处于分裂中的shard状态为splitting。该状态用于client流量请求路由识别。当Node完成数据分裂以后上报metaserver,metaserver更新shard状态为normal从而完成分裂。

Node

node收到分裂请求以后,会根据需要分裂的分片id在原地拉起创建一个新的分片。然后对旧分片的数据进行checkpoint,同时记录旧分片checkpoint对应的logid。新分片创建完成后,会直接从旧分片的checkpoint进行open,然后在异步复制logid之后的数据保证数据的一致性。新分片加载完checkpoint后,原来的旧分片会向raftgroup提交一条分裂完成日志,该日志处理流程与普通raft日志一致。分裂完成后上报分裂状态到metaserver,同时旧分片开始拒绝不再属于自己分片的数据写入,client收到分片错误以后会请求metaserver更新shard分布。

完成分裂以后的两个分片拥有的两倍冗余数据,这些数据会在engine compaction的时候根据compaction_filter过滤进行删除。

Client

用户请求时,根据hash(key) % shard_cnt 获取目标分片。表分裂期间,该shard_cnt表示分裂完成后的最终分片数。以上图3分片的分裂为例:

hash(key) = 4, 分裂前shard_cnt为3,因此该请求会被发送到shard1 分裂期间,由于shard_cnt变为6,因此目标分片应该是shard4, 但是由于shard4为splitting,因此client会重新计算分片从而将请求继续发送给shard1 等到最终分裂完成后,shard4状态变更为Normal,请求才会被发送到shard4

分裂期间,如果Node返回分片信息错误,那么client会请求metaserver更新分片分布信息。

类似于MySQL的binlog,我们基于raftlog日志实现了kv的binlog 业务可以根据binlog进行实时的事件流订阅,同时为了满足事件流回溯的需求,我们还对binlog数据进行冷备。通过将binlog冷备到对象存储,满足了部分场景需要回溯较长事件记录的需求。

直接复用raftlog作为用户行为的binlog,可以减少binlog产生的额外写放大,唯一需要处理的是过滤raft本身的配置变更信息。learner通过实时监听不断拉取分片产生的binlog到本地并解析。根据learner配置信息决定将数据同步到对应的下游。同时binlog数据还会被异步备份到对象存储,当业务需要回溯较长时间的事件流的时候,可以直接指定位置从S3拉取历史binlog进行解析。

基于上述提到的binlog能力,我们还基于此实现了kv的多活。learner模块会实时将用户写入的数据同步到跨数据中心的其他kv集群。对于跨数据中心部署的业务,业务可以选择就近的kv集群进行读取访问,降低访问延时。

kv的多活分为读多活和写多活。对于读多活,机房A的写入会被异步复制到机房B,机房B的服务可以直接读取本机房的数据,该模式下只有机房A的kv可以写入。对于写多活,kv在机房A B 都能同时提供写入并且进行双向同步,但是为了保证数据的一致性,需要业务上做数据的单元化写入,保证两个机房不会同时修改同一条记录。通过将用户划分单元,提供了写多活的能力。通过对binlog数据打标,解决了双向同步时候的数据回环问题。

对于用户画像和特征引擎等场景,需要将离线生成的大量数据快速导入KV存储系统提供用户读取访问。传统的写入方式是根据生成的数据记录一条条写入kv存储,这样带来两个问题。其一,大批量写入会对kv造成额外的负载与写入带宽放大造成浪费。其次,由于写入量巨大,每次导入需要花费较长的时间。为了减少写入放大以及导入提速,我们支持了bulk load的能力。离线平台只需要根据kv的存储格式离线生成对应的SST文件,然后上传到对象存储服务。kv直接从对象存储拉取SST文件到本地,然后直接加载SST文件即可对外提供读服务。bulk load的另外一个好处是可以直接在生成SST后离线进行compaction,将compaction的负载offload到离线的同时也降低了空间的放大。

由于LSM tree的写入特性,数据需要被不断的compaction到更底层的level。在compaction时,如果该key还有效,那么会被写入到更底层的level里,如果该key已经被删除,那么会判断当前level是否是最底层的,一条被删除的key,会被标记为删除,直到被compaction到最底层level的时候才会被真正删除。compaction的时候会带来额外的写放大,尤其当value比较大的时候,会造成巨大的带宽浪费。为了降低写放大,我们参考了Bitcask实现了kv分离的存储引擎sparrowdb

sparrowdb 介绍

用户写入的时候,value通过append only的方式写入data文件,然后更新索引信息,索引的value包含实际数据所在的data文件id,value大小以及position信息,同时data文件也会包含索引信息。与原始的bitcask实现不一样的是,我们将索引信息保存在 rocksdb。

更新写入的时候,只需要更新对应的索引即可。compaction的时候,只需将索引写入底层的level,而无需进行data的拷贝写入。对于已经失效的data,通过后台线程进行检查,当发现data文件里的索引与rocksdb保存的索引不一致的时候,说明该data已经被删除或更新,数据可以被回收淘汰。

使用kv存储分离降低了写放大的问题,但是由于kv分离存储,会导致读的时候多了一次io,读请求需要先根据key读到索引信息,再根据索引信息去对应的文件读取data数据。为了降低读访问的开销,我们针对value比较小的数据进行了inline,只有当value超过一定阈值的时候才会被分离存储到data文件。通过inline以及kv分离获取读性能与写放大之间的平衡。

在分布式系统中,负载均衡是绕不过去的问题。一个好的负载均衡策略可以防止机器资源的空闲浪费。同时通过负载均衡,可以防止流量倾斜导致部分节点负载过高从而影响请求质量。对于存储系统,负载均衡不仅涉及到磁盘的空间,也涉及到机器的内存、cpu、磁盘io等。同时由于使用raft进行主从选主,保证主节点尽可能的打散也是均衡需要考虑的问题。

副本均衡

由于设计上我们会尽量保证每个副本的大小尽量相等,因此对于空间的负载其实可以等价为每块磁盘的副本数。创建副本时,会从可用的zone中寻找包含副本数最少的节点进行创建。同时考虑到不同业务类型的副本读写吞吐可能不一样导致CPU负载不一致,在挑选副本的时候会进一步检查当前节点的负载情况,如果当前节点负载超过阈值,则跳过该节点继续选择其他合适的节点。目前基于最少副本数以及负载校验基本可以做到集群内部的节点负载均衡。

当出现负载倾斜时,则从负载较高的节点选择副本进行迁出,从集群中寻找负载最低的节点作为待迁入节点。当出现节点故障下线以及新机器资源加入的时候,也是基于均值计算待迁出以及迁入节点进行均衡。

主从均衡

虽然通过最少副本数策略保证了节点副本数的均衡,但是由于raft选主的性质,可能出现主节点都集中在部分少数节点的情况。由于只有主节点对外提供写入,主节点的倾斜也会导致负载的不均衡。为了保证主节点的均衡,Node节点会定期向metaserver上报当前节点上副本的主从信息。

主从均衡基于表维度进行 *** 作。metaserver会根据表在Node的分布信息进行副本数的计算。主副本的数量基于最朴素简单的数学期望进行计算: 主副本期望值 = 节点副本数 / 分片副本数。下面为一个简单的例子:

假设表a包含10个shard,每个shard 3个replica。在节点A、B、C、D的分布为 10、5、6、9 那么A、B、C、D的主副本数期望值应该为 3、1、2、3 如果节点数实际的主副本数少于期望值,那么被放入待迁入区,如果大于期望值,那么被放入待迁出区。同时通过添加误差值来避免频繁的迁入迁出。只要节点的实际主副本数处于 [x-δx,x+δx] 则表示主副本数处于稳定期间,x、δx 分别表示期望值和误差值。

需要注意的是,当对raft进行主从切换的时候,从节点需要追上所有已提交的日志以后才能成功选为主,如果有节点落后的时候进行主从切换,那么可能导致由于追数据产生的一段时间无主的情况。因此在做主从切换的时候必须要检查主从的日志复制状态,当存在慢节点的时候禁止进行切换。

37 故障检测&修复

一个小概率的事件,随着规模的变大,也会变成大概率的事件。分布式系统下,随着集群规模的变大,机器的故障将变得愈发频繁。因此如何对故障进行自动检测容灾修复也是分布式系统的核心问题。故障的容灾主要通过多副本raft来保证,那么如何进行故障的自动发现与修复呢。

健康监测

metaserver会定期向node节点发送心跳检查node的健康状态,如果node出现故障不可达,那么metaserver会将node标记为故障状态并剔除,同时将node上原来的replica迁移到其他健康的节点。

为了防止部分node和metaserver之间部分网络隔离的情况下node节点被误剔除,我们添加了心跳转发的功能。上图中三个node节点对于客户端都是正常的,但是node3由于网络隔离与metaserver不可达了,如果metaserver此时直接剔除node3会造成节点无必要的剔除 *** 作。通过node2转发心跳探测node3的状态避免了误剔除 *** 作。

除了对节点的状态进行检测外,node节点本身还会检查磁盘信息并进行上报,当出现磁盘异常时上报异常磁盘信息并进行踢盘。磁盘的异常主要通过dmesg日志进行采集分析。

故障修复

当出现磁盘节点故障时,需要将原有故障设备的replica迁移到其他健康节点,metaserver根据负载均衡策略选择合适的node并创建新replica, 新创建的replica会被加入原有shard的raft group并从leader复制快照数据,复制完快照以后成功加入raft group完成故障replica的修复。

故障的修复主要涉及快照的复制。每一个replica会定期创建快照删除旧的raftlog,快照信息为完整的rocksdb checkpoint。通过快照进行修复时,只需要拷贝checkpoint下的所有文件即可。通过直接拷贝文件可以大幅减少快照修复的时间。需要注意的是快照拷贝也需要进行io限速,防止文件拷贝影响在线io

过期数据淘汰

在很多业务场景中,业务的数据只需要存储一段时间,过期后数据即可以自动删除清理,为了支持这个功能,我们通过在value上添加额外的ttl信息,并在compaction的时候通过compaction_filter进行过期数据的淘汰。level之间的容量呈指数增长,因此rocksdb越底层能容纳越多的数据,随着时间的推移,很多数据都会被移动到底层,但是由于底层的容量比较大,很难触发compaction,这就导致很多已经过期的数据没法被及时淘汰从而导致了空间放大。与此同时,大量的过期数据也会对scan的性能造成影响。这个问题可以通过设置periodic_compaction_seconds 来解决,通过设置周期性的compaction来触发过期数据的回收。

scan慢查询

除了上面提到的存在大批过期数据的时候可能导致的scan慢查询,如果业务存在大批量的删除,也可能导致scan的时候出现慢查询。因为delete对于rocksdb本质也是一条append *** 作,delete写入会被添加删除标记,只有等到该记录被compaction移动到最底层后该标记才会被真正删除。带来的一个问题是如果用户scan的数据区间刚好存在大量的delete标记,那么iterator需要迭代过滤这些标记直到找到有效数据从而导致慢查询。该问题可以通过添加 CompactOnDeletionCollector 来解决。当memtable flush或者sst compaction的时候,collector会统计当前key被删除的比例,通过设置合理的 deletion_trigger ,当发现被delete的key数量超过阈值的时候主动触发compaction。

delay compaction

通过设置 CompactOnDeletionCollector 解决了delete导致的慢查询问题。但是对于某些业务场景,却会到来严重的写放大。当L0被compaction到L1时候,由于阈值超过deletion_trigger ,会导致L1被添加到compaction队列,由于业务的数据特性,L1和L2存在大量重叠的数据区间,导致每次L1的compaction会同时带上大量的L2文件造成巨大的写放大。为了解决这个问题,我们对这种特性的业务数据禁用了CompactOnDeletionCollector 。通过设置表级别参数来控制表级别的compaction策略。后续会考虑优化delete trigger的时机,通过只在指定层级触发来避免大量的io放大。

compaction限速

由于rocksdb的compaction会造成大量的io读写,如果不对compaction的io进行限速,那么很可能影响到在线的写入。但是限速具体配置多少比较合适其实很难确定,配置大了影响在线业务,配置小了又会导致低峰期带宽浪费。基于此rocksdb 在59以后为 NewGenericRateLimiter 添加了 auto_tuned 参数,可以根据当前负载自适应调整限速。需要注意的是,该函数还有一个参数 RateLimiter::Mode 用来限制 *** 作类型,默认值为 kWritesOnly,通常情况该模式不会有问题,但是如果业务存在大量被删除的数据,只限制写可能会导致compaction的时候造成大量的读io。

关闭WAL

由于raft log本身已经可以保证数据的可靠性,因此写入rocksdb的时候可以关闭wal减少磁盘io,节点重启的时候根据rocksdb里保存的last_apply_id从raft log进行状态机回放即可。

降副本容灾

对于三副本的raft group,单副本故障并不会影响服务的可用性,即使是主节点故障了剩余的两个节点也会快速选出主并对外提供读写服务。但是考虑到极端情况,假设同时出现两个副本故障呢? 这时只剩一个副本无法完成选主服务将完全不可用。根据墨菲定律,可能发生的一定会发生。服务的可用性一方面是稳定提供服务的能力,另一方面是故障时快速恢复的能力。那么假设出现这种故障的时候我们应该如何快速恢复服务的可用呢。

如果通过创建新的副本进行修复,新副本需要等到完成快照拷贝以后才能加入raft group进行选举,期间服务还是不可用的。那么我们可以通过强制将分片降为单副本模式,此时剩余的单个健康副本可以独自完成选主,后续再通过变更副本数的方式进行修复。

RaftLog 聚合提交

对于写入吞吐非常高的场景,可以通过牺牲一定的延时来提升写入吞吐,通过log聚合来减少请求放大。对于SSD盘,每一次写入都是4k刷盘,value比较小的时候会造成磁盘带宽的浪费。我们设置了每5ms或者每聚合4k进行批量提交。该参数可以根据业务场景进行动态配置修改。

异步刷盘

有些对于数据一致性要求不是非常高的场景,服务故障的时候允许部分数据丢失。对于该场景,可以关闭fsync通过 *** 作系统进行异步刷盘。但是如果写入吞吐非常高导致page cache的大小超过了 vmdiry_ratio ,那么即便不是fsync也会导致io等待,该场景往往会导致io抖动。为了避免内核pdflush大量刷盘造成的io抖动,我们支持对raftlog进行异步刷盘。

透明多级存储,和缓存结合,自动冷热分离,通过将冷数据自动搬迁到kv降低内存使用成本。

新硬件场景接入,使用SPDK 进行IO提速,使用PMEM进行访问加速。

参考文献

[1] Bitcask A Log-Structured Hash Table for Fast Key/Value Data

[2] Lethe: A Tunable Delete-Aware LSM Engine

以上就是关于Flink——Exactly-Once全部的内容,包括:Flink——Exactly-Once、GFS 小结、GFS论文笔记等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/10129072.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-05-05
下一篇2023-05-05

发表评论

登录后才能评论

评论列表(0条)

    保存