APP下载

面向工业大资料的物件储存技术实践

消息来源:baojiabao.com 作者: 发布时间:2024-05-17

报价宝综合消息面向工业大资料的物件储存技术实践

来源:昆仑资料K2Data

什么是物件储存

物件储存一般是指以“物件”(object)为资料组织形式的储存服务。引用Wikipedia上给出的定义如下:

Object storage (also known as object-based storage[1]) is a computer data storage architecture that manages data as objects, as opposed to other storage architectures like file systems which manage data as a file hierarchy, and block storage which manages data as blocks within sectors and tracks

从这段描述中其实可以看出,物件储存其实并不适合主动的定义方式,而是通过与其它典型储存型别作比较,突出其自身特点。物件储存的单位称为物件,与档案系统里的档案接近,但物件储存不一定要支援档案系统里树状的目录结构。真实的物件储存服务包括Facebook储存的图片储存和Spotify歌曲库等。

物件储存定义并不限定资料模型,但在实现中大家一般采用键 + 资料 + 元资料的结构来描述一个物件(如上图所示),其中:

物件键:用于唯一确定一个物件的id;资料:物件的资料本身,一般为一个档案,例如一张图片、一个视讯档案等等;元资料(metadata):描述物件的一组结构化资料,称为元资料,一般以key-value的形式储存;

所以,物件储存也可以看做一种key-value形式,其中key对应物件的键,value对应一个档案和元资料。另外,一般物件储存还有桶(bucket)的概念,主要用于方便物件管理,在一个桶里物件键是唯一的,但不同的桶之间物件键一般是可以重复的。

物件储存现状分析

本文讨论面向工业大资料的物件储存技术实践,但物件储存本身并不是一个新颖的概念,几乎所有主流公有云服务供应商都有自己的物件储存服务。下面我们简要介绍一些常见物件储存服务的功能和特点。商业物件储存服务中一般包含较多的定价和安全策略,这些不是本文考虑的重点问题。本文更关注工业大资料应用场景对物件储存在功能上的需求,以及满足这些需求的所依赖的技术要素。

AWS S3

AWS是Amazon的公有云服务,S3是其物件储存服务。每个 Amazon S3 物件都有键、资料和元资料,与概念介绍一致。在S3,键表述为一种类似档案系统目录的层级结构,例如:

Photos/family/2019/a.jpgDocs/work/b.doc

这看起来像Unix档案系统里的路径,但有一点区别,即它是以桶的名称开始(例如上面的Photos和Docs是桶的名称),而不是Unix档案系统里的“/"根目录。层级结构方便使用者以类似档案系统的方式组织物件资料,使用者甚至可以建立”资料夹“。当然,这些资料夹只是互动上的设计,而实际上键只是一个有层级结构的字串。

元资料包含两部分,通用资讯自定义内容。通用资讯由系统自动生成的,比如物件建立日期和档案大小等等;自定义内容由使用者建立,以key-value形式储存。使用者可以按键获取物件档案和元资料。S3还允许使用者用标签(tag)来标记物件,标签也是以key-value形式储存的,通过界面可以获得某个物件对应的标签列表。标签的目的是方便使用者将物件分类,但S3并不支援按照标签来筛选物件资料。但利用AWS检索服务,可以对档名和元资料自动构建索引,在里的文章有详细介绍。

对于一些结构化(或半结构化)资料,例如CSV、JSON或Parquet格式档案,S3支援称为S3 Select功能。S3 Select使使用者可以使用SQL来获取物件内容,例如:

SELECT s._1, s._2 FROM S3Object s WHERE s._3 > 100

对应从CSV格式的物件S3Object里提取前两列,同时需要满足第三列大于100的条件。

S3是AWS的核心服务之一,其推出年代较早,对后续的物件储存服务产生了深远的影响。国内的阿里云OSS和腾讯云COS与S3的功能比较接近;Openstack Swift是一种开源的物件储存服务,其功能上也类似S3。在资料一致性方面,S3、Swift和COS支援最终一致性;OSS支援强一致性。

Azure Blob Storage

Azure Blob是微软提供公有云的物件储存方案。Azure Blob采用与AWS S3相似的资料模型,而在应用场景上有一些细化。Azure Blob支援三种储存场景:

Block blobs:用于管理随机访问的物件资料,读写单位一般是整个物件档案;Append blobs:对档案追加(append)操作有所优化,适合类似日志储存;Page blobs:对档案内容随机访问有所优化,可用于虚拟机器的虚拟硬盘档案;

Azure Blob除了对档案访问场景进行了细化,还支援Azure Search建立物件资料索引(Azure Search是微软公有云服务推出的通用索引服务)。利用Azure Search可以查询储存在Azure Blob里的档案内容,根据查询条件访问这些资料。目前支援索引的档案格式包括PDF、Office文件、文字档案、JSON、CSV等等。如果被索引的资料发生变化,Azure Search支援增量索引。所以相比AWS S3,Azure Blob支援更灵活的访问方式。Azure Blob支援资料强一致性。

Google Cloud Storage

Google Cloud Storage是Google推出的物件储存服务,它的资料模型与AWS S3类似,没有Azure Blob对资料储存的细分场景。与Azure Blob类似的是Google Cloud Storage支援使用BigQuery建立索引和查询,主要支援一些结构化资料型别,如CSV、JSON、Avro等等。Google Cloud Storage支援资料最终一致性。

现状小结

物件储存服务是公有云平台提供的一种标准储存方案,一般都按照物件键 + 资料 + 元资料的资料模型,但在实现细节上稍有不同。在资料访问上,Azure Blob和Google Cloud Storage除了支援按键访问,还支援与索引服务配合使用——建立索引后查询物件资料内容或元资料。大多数物件储存方案中,物件资料作为一个整体进行存取操作,而在Azure Blob下还支援物件内资料读写操作(即Append blobs和Page blobs)。在一致性方面,大部分物件储存支援最终一致性,而Azure Blob和阿里OSS支援强一致性。

物件储存设计

工业场景下物件储存的需求

工业场景下,物件资料的资料来源一般是装置,这与面向终端使用者的互联网应用不一样。例如在社交网络应用中,我们可能利用物件储存来储存使用者上传的照片,照片的键一般与使用者id系结,物件资料读写从键的分布来看一般是随机的,大部分操作都只访问一个特定id对应的资料(例如读取某个使用者的照片时间线),跨多个id访问的情况比较少。但在工业场景下,我们比较少单独考虑某个特定装置的资料,更多是获取一段时间内的满足一定条件的所有物件资料供资料分析使用。

考虑一个具体的场景,假设我们记录了10000台风机产生的故障档案,而我们常常希望通过分析回答:

不同型号的风机发生故障的频率是否相同?同一型号不同批次生产的风机发生故障的频率?某一型号的风机随着安装时间增长其故障率的变化情况?不同地理区域的风机故障情况有何特征?不同故障型别的分布情况?

通过物件键可以将物件资料组织成类似档案系统的层级结构,但仍然难以方便地筛选出用于分析的物件资料(如回答上述问题)。在对现状的讨论中,Azure Blob和Google Cloud Storage除了键以外还提供索引服务,但是索引服务是独立于物件储存的,索引与资料之间的延迟并没有保证。另外,一些功能在国内并不支援,例如AWS Search。综合多方面考虑,我们决定设计和实现面向工业场景的物件储存服务。

资料模型 = 元资料 + 物件资料

首先我们考虑一种更适合检索的物件资料模型,可以描述成下图

在一般物件资料模型中元资料只起到补充说明的作用,但在我们的物件资料模型里:

元模型=物件键+补充说明+通用资讯+资料指标

这是我们设计的物件储存与其它物件储存最大的区别。元资料不仅包括了物件键,还包括使用者自定义内容,可以根据自定义内容筛选符合条件的物件资料。

具体来说,一个物件的元资料包含多个列(column):其中一些列称为id列(id-column),它们的组合对应物件键自定义内容由使用者定义一些与物件资料相关的资讯;通用资讯列记录了物件资料的统计资讯,例如物件的大小和建立时间等。以储存风机产生的故障档案为例,它的元资料可能如下所示:

其中:

物件键 = 风场id + 风机id自定义内容 = 记录时间 + 型号 + 经纬度 + 错误码通用资讯 = 大小 + 建立时间

资料指标用于记录物件资料的储存位置,如果物件资料储存在某种档案系统,例如HDFS,那么指标的形式可以是hdfs://nameservice1:port/path/to/data,所以理论上物件资料可以储存在任意系统之中。

上面的表格看起来像一般的关系资料,列的资料型别可以是字串、整型、浮点数或日期。一个与关系资料不太一样的地方是物件元资料的schema可能经常发生变化。虽然关系模型也允许修改资料的schema,但在资料量较大的情况下变更schema的代价非常大,所以更适合储存元资料的是一些NoSQL储存引擎,例如Elasticsearch。

在这样的资料模型下,一般的资料消费方式是先根据某种条件筛选物件的元资料,待得到一个列表后再根据资料指标读取对应的物件资料。例如在上面的例子里,我们可以根据风场id和风机id找到一台风机产生的所有资料;或者找到某种特定故障型别的所有资料。另一种重要的使用方式是只消费物件的元资料,例如我们可以统计过去一个月内发生的不同故障的histogram。这比起只能是根据已知的资料键来读取物件资料的访问方式要灵活得多。

系统架构

我们在本文给出一种基于Elasticsearch和HDFS的参考实现,其系统架构如下图所示。使用者的读写请求经过Load balancer分发到某台具体的REST服务器;在REST服务里实现物件储存的增删查改操作逻辑;各项操作逻辑基于对底层各种服务的组合呼叫。底层的三种储存的作用分别是:

MySQL用于储存物件元资料的schema定义;Elasticsearch用于储存和检索物件的元资料;HDFS用于储存物件资料;

在这样的架构下物件的元资料和物件资料都以多备份的形式储存并支援HA(High Availability),而REST服务也支援HA,所以整个物件储存服务支援HA,并且均可以根据负载情况水平扩充套件。

使用者的一般使用场景是:

1. 资料建模:考虑物件的元资料定义,这里特指元资料中的物件键和自定义内容的设计,例如在前面的例子里使用者设计的物件键(风场id、风机id)和自定义内容(记录时间、型号、经纬度、错误码)。每一种物件资料记做一个物件型别(Object Class),物件储存服务同时管理多个物件型别;

2. 资料写入:使用者呼叫REST界面写入物件,每一次写入包括两个部分,即元资料和物件资料。当且仅当元资料和物件资料都完成写入之后一次写入操作才向用户返回成功;

3. 资料变更和删除:使用者可以修改物件内容,例如更新元资料中某个字段的值,或者更新物件资料。在发起更新请求之前使用者需要提供物件键来唯一确定被修改的物件,这也意味着物件键本身是不能被修改的。物件的删除可以看做是更新的一种特殊场景;

4. 资料检索和读取:使用者可以按照物件元资料的列对某个物件型别进行检索,得到一组符合条件的物件元资料列表。由于元资料中包含指向物件资料的指标,所以后续可以读取对应的物件资料;

至此我们清楚了物件储存服务的使用过程,下面深入讨论一些技术要点。

技术要点1:原子性写操作

在上一节提出的物件储存服务中,一个物件既包括物件资料,还包括元资料。在实现物件的写操作时,需要保证这两部分一致,也就是说物件资料和元资料对使用者来说应该是一体的,或者说写操作是原子性的,体现在:

写入或删除一个物件的过程中,不应该存在某个时刻使使用者只能读到该物件的物件资料或元资料;更新一个物件的过程中,不应该存在某个时刻物件的元资料与物件资料不一致,例如元资料是新的但物件资料是旧的,反之亦然;

为了达到上述目标,物件的写入过程如下图所示:

可分为两个步骤,首先从物件储存服务外部复制物件资料到物件储存内部,然后将元资料复制到物件储存服务。表面上看,在上图示记为1的时刻会发生不一致,即物件储存中只有物件资料而没有元资料,但考虑到我们读取资料时总是先查询元资料,所以在此刻使用者是看不到这份物件资料的。无论元资料还是物件资料,我们都是复制一份到物件储存内部,而不应该使用转移操作;另外,复制到物件储存内部的物件资料不再保留原档名,而是改为一个随机生成的档名来防止冲突。

上面的描述里隐含了一个假设——元资料从外部复制到内部的过程本身是原子性的,即不存在一个时刻使使用者能看到尚未写完的元资料。在我们的参考设计里,物件的元资料储存在Elasticsearch,可以保证写入一条记录的原子性,而其实大部分数据库服务都支援至少单条记录操作的原子性。注意物件资料的复制是不需要满足原子性的要求的,所以一般的档案系统都可以用于储存物件资料。

下面我们考虑一下删除的过程,如下图所示:

删除与写入一样分为两个步骤,首先将待删除物件的元资料标记删除,即对外部使用者不可见,但仍储存在Elasticsearch里,然后再将物件资料和元资料物理删除。标记删除的目的在下一节关于冲突处理时展开。一个实现细节是在上图中物理删除物件时(标记2),应该先删除物件资料,后删除元资料,避免在系统故障时出现物件资料无法被清理的情况。删除操作也是符合原子性要求的,这与写入操作是相同的道理。

更新一个物件可以分两种情况:

只更新物件的元资料中自定义内容;更新物件资料,这通常意味着元资料也会发生变化,例如资料大小统计资讯;

对于前一种情况,原子化的更新操作只依赖于Elasticsearch支援原子化更新,所以不需要额外注意。后一种情况类似物件写入的过程,需要三步:

步骤1:把新的物件资料复制到物件储存内部;步骤2:更新物件元资料内容,并把元资料里的资料指标指向新的物件资料(这个步骤本身是原子性的);步骤3:删除原来的物件资料来清理空间;

与写入的原理类似,后一种更新操作也是原子化的,但这里存在一个技术细节——如何确保我们能删除旧的物件资料?步骤2和3之间是不能互换的,否则会存在某个时刻使用者可以查询到物件的元资料但却找不到物件资料。因此,如果在步骤2之后系统出现故障,我们如何能知道旧的物件资料在哪儿?注意这时候资料指标已经指向了新的物件资料。为了解决这个问题,在步骤2中,我们需要把旧的资料指标储存下来(例如写入写前日志),即使系统发生故障,在服务恢复后再清理掉旧的物件资料。

本节讨论了原子性写操作实现的技术细节,每种写操作都分为多个步骤,所以在实现时需要写前日志来保证操作的事务性。在系统发生故障后的服务重启时,可以根据写前日志来处理尚未完成的事务。

注:我们的参考实现基于Elasticsearch,在Elasticsearch里可以支援在更新一条记录时执行一个指令码,而整个执行过程也是原子性的。基于这个特性,可以在物件元资料里的资料指标发生改变时将旧的指标记录到元资料本身的一个数组里,从而免去了依赖写前日志来记录的麻烦。

技术要点2:读写冲突处理

读写冲突发生在同时写一个物件,或者同时发生一读一写的情况。我们回顾一下写资料的过程,总是先处理物件资料,然后处理元资料;读资料的过程则是先读取元资料,再获取物件资料。所以物件的读写冲突只会发生在元资料相关的操作上。例如,在写入两个物件键相同的物件的过程中,真正可能发生冲突的步骤是两边同时向Elasticsearch写入键相同的记录(Elasticsearch里也有键的概念,在实现时我们将Elasticsearch里的键设定为物件键的字串)。Elasticsearch内部使用MVCC(Multi-versioned Concurrency Control)的冲突处理模式,在同时写入两条键相同的记录时,其中一方会失败。因此,如果在写入两个物件键相同的物件时恰好发生元资料写入冲突,那么其中一方会发生失败。

一读一写的情况会稍微复杂一些,不能完全由Elasticsearch来解决。这里的复杂性主要来自读物件的过程被分为两个阶段,即先获取元资料,再读取物件资料。在现实中这两个阶段之间可能相隔一段较长的时间,例如我们先根据某个查询条件获得一组物件元资料列表,然后我们在一些平行计算框架(例如Mapreduce或Spark)里消费物件资料。假如在得到元资料之后,消费物件资料之前,我们修改了物件资料会发生什么问题?例如,在消费资料之前我们已经把物件资料更新了。合理的做法是使用者根据已经获得的元资料仍然可以读取更新前的物件资料,而不会读到新的物件资料(因为会不一致)。另外,也不能因为物件资料已经被更新就返回错误,否则对于那些一次性消费大批物件资料的应用场景,可能出现频繁的失败。在上一节提到了原子性写操作,无论对于更新还是删除,我们总是先做标记,延迟一段时间之后再物理删除物件资料。这段时间应该足够长,确保大部分更新前的读取操作已经完成;同时,系统需要维护一个程序按照设定的时间来延迟删除已被标记的物件资料。

技术要点3:一致性讨论

从前面关于原子性操作的讨论中,我们可以看到物件储存服务的一致性其实是由其元资料储存的一致性决定的。也就是说,如果我们能做到元资料储存强一致性,那么物件服务就是强一致性的。我们使用Elasticsearch来储存元资料,所以这里需要讨论Elasticsearch的一致性问题。Elasticsearch的一致性一直存在模糊(可以参考https://www.elastic.co/guide/en/elasticsearch/resiliency/current/index.html),随着版本不断升级,开发团队在试图减少一些已知问题。

从一般情况来讲Elasticsearch应该属于最终一致(Eventual consistency),但通过一些调整可以实现“接近”强一致性的行为。我们对Elasticsearch的配置调整包括:

优先从primary节点读取资料;写入操作设定处于active的节点数量不低于n/2,其中n表示Elasticsearch丛集的大小;丛集active节点数如果低于n/2则停止对外提供服务;在读取资料前强制执行refresh操作,确保读取发生之前的写入操作已经对外可见;

如果希望深入理解上述配置的含义需要读者对Elasticsearch有比较多的了解,但简而言之,我们希望每次写入操作都能覆盖到丛集里大部分节点,而每次读取则有些选择从Leader节点(一般是首先被写入的节点)。这样虽然无法保证强一致性,但确保了在大部分情况下,Elasticsearch对外的表现接近强一致性,即我们读到的资料总是最新写入的。

根据我们前面对架构的讨论,Elasticsearch并非唯一可选择的物件元资料储存。我们选择Elasticsearch是看重其强大的检索能力,但如果对一致性有非常严格的要求,也可以选择其它储存方式。

注:我们早期基于MySQL实现过元资料储存,其面临的最大问题是schema修改带来的巨大开销,以及在执行一些聚合操作时耗时过大。

技术要点4:档案合并

物件储存服务往往需要储存大量的物件资料,而这些资料会以档案的形式储存在底层的档案系统中。如果存在大量小档案,可能造成档案系统效率降低。例如,在HDFS中,每个block大小通常在64M(或128M),一个block对应一个inode,即HDFS Namenode内存中的一条记录。即使档案再小,在HDFS中仍然会占用一个inode,从而大量的小档案会带给Namenode内存压力。如果我们能把小档案合并成大档案,可以减少物件档案对inode的占用,从而缓解内存压力,这就是档案合并的出发点。如果底层档案系统不是HDFS而是Linux本地档案系统,其inode数量也是有一定上限的,也会有相应的问题。

一种档案合并的思路是将物件按照时间顺序划分为多个区间,每个区间内所有物件的物件资料档案合并为一个大档案。每个物件的元资料都包含其建立时间,这个时间是在建立该物件时由系统自动生成的。后续对该物件的更新操作不会改变物件的建立时间,这个性质非常重要。假设我们设定区间的大小为1小时,那么按时间划分的区间是:

..., (8:00,9:00],(9:00,10:00],...

以区间(8:00,9:00]为例,建立时间落入这个区间的所有物件的物件资料会被合并到一个大档案,而其元资料里的指标会指向大档案里的一部分,具体来说包括:

大档案的档名;物件资料在大档案里的offset;物件资料的length;

根据上述三个资讯,我们可以从大档案里读取对应的物件资料。档案合并是在物件已经写入物件服务之后发生的,例如上面例子里的(8:00,9:00]区间内的资料合并一定发生在9:00之后,并且档案合并操作必须对使用者是透明的。换句话说,在档案合并的过程中,使用者应该不会感知到底层物件资料正在被合并,而合并操作也不会影响使用者的读写操作。为此,合并的步骤包括:

步骤1:把待合并的小档案合并为一个大档案;步骤2:依次更新被合并的所有物件的元资料,使指标指向大档案里的一部分;步骤3:删除已被合并的小档案来清理空间;

对于任意一个物件来说,其操作过程类似物件的更新操作,虽然步骤2里物件元资料更新无法支援批量更新(即把更新包含在一个原子性事务中),但在任意时刻对外界使用者来说,他看到的都是最新的物件资料。如果在合并过程中发生写操作冲突,沿用前面讨论过多冲突处理方式,其中一方发生错误——假设合并涉及100个物件档案,而其中1个由于写冲突失败了,剩余99个成功,那么合并后原来的100个小档案会变成1个大档案(包含100个物件资料)加1个小档案(包含由于冲突导致合并失败的1个物件资料)。可以看到,由于冲突造成了一定的资料冗余,但在正常使用情况下冲突的概率非常小,所以少量的资料冗余是可以容忍的。档案合并的过程如下图所示:

档案合并还会带来另一个数据冗余问题。如果在档案合并发生之后,其中一部分物件资料发生了更新,原本要被删除的物件资料现在已经成为大档案的一部分,而要从大档案里移出其中一部分,相当于把其中未被更新的物件的资料重新写一遍,同时更新对应的资料指标。可见由于档案合并,更新已被合并的物件资料代价较大。

实际上,在工业场景下,大部分情况都是物件写入,而发生更新的场景很少(这与互联网应用场景下的物件储存不同),所以在更新比例极少的情况下,我们可以容忍大档案里少量资料已经失效但仍然保留带来的冗余开销。

注:如果现实情况下更新比较频繁,可以采取一定策略来优化合并档案的删除操作。例如,我们可以先统计某个大档案里有多少内容已经失效了,当且仅当失效比例较高的时候才真正执行删除操作。

总结

本文介绍了一种面向工业大资料的物件储存服务设计实践。经过场景分析,我们发现工业场景下物件储存的需求与互联网场景下的情况是不一样的,尤其对于物件的检索提出了更高的要求。为了满足这种要求,我们在资料模型设计中强化了元资料的角色,改变了物件资料的消费方式,提出了新的物件储存服务系统架构,结合Elasticsearch + HDFS的参考实现详细讨论了其中的技术要点,希望对从事面向工业大资料的物件储存服务的设计和开发人员提供一定的参考。

— 完 —

关注清华-青岛资料科学研究院官方微信公众平台“THU资料派”及姊妹号“资料派THU”获取更多讲座福利及优质内容。

2019-10-18 13:53:00

相关文章