背景介绍
(一)需求情景
老板提出一个紧急需求,下周交付:
·我有非常多的文件需要存储,以图片和短视频为主,以后还会有更多;
·每一个图片或者视频都会有很多的标签信息,我需要通过标签能快速的找到我想要的文件,要求操作流程简单,不需要其它多余操作;
·需要尽量节省资源。
和目标客户交流:
·小王最近接收了一个图片模型训练的项目,需要线上批量拉取某种标签的图片进行模型训练,比如美食、房屋,图片大小为20K~10M;
·小李是短视频推荐系统的研发,需要一个可以存储和通过标签检索短视频的服务,短视频文件大小为200k ~ 200MB,RT不敏感,吞吐量要求TPS 100+,QPS 200+,保存数据2个月以上。
(二)需求分析 用户到底想要什么?
图片、视频统一存储检索系统,提供图片、视频文件的存储、检索服务(需要注意的是我们往往会给图片或者视频打上很多的标签,比如:美食、房屋等等,这里提及的检索是根据标签进行检索),数据大多都是小文件(80%大小在数MB以内),数据体量大,增长快速等特点。
根据以上信息,我们得出以下功能需求:
1)图片或者视频数据作为单独的对象存储到一个大容器中;
2)应用可以通过标签查询来获取每个单独的对象数据(图片或者视频);
3)支持海量数据,高性能,可扩展,高可用。
基于以上需求,我们如何设计一个图片视频统一存储检索系统?
图片视频统一存储检索系统设计
(一)技术选型
在技术选型方面,我们最终选择HBase。
HBase的优势:
1)HBase 可以很好地支持非结构化的数据存储,且基于HDFS保证数据的可靠性;
2)HBase 吞吐量非常高,足够支撑业务;
3)HBase 基于Hadoop集群,不需要重新维护其他数据存储服务。
对于我们的需求,HBase有哪些不足之处:
1)HBase 对小文件(小于10MB)支持很好,但是对于较大的文件无法满足;
2)由于HBase的设计,HBase会发生Compact和Split操作,文件存储会频繁的触发此类的操作导致写放大等不良的影响;
3)HBase不适合复杂的检索操作,功能上可能会有限制。
针对以上问题,我们该如何来弥补这些不足呢?
大文件存储方案
方案一:将大文件分片存储在HBase中,例如100M的文件,我们可以以每片1M分成100个,然后存储在HBase的表里面。但是这个方案对后期的一些实现,比如查询、整合,可能需要一些比较复杂的设计来保证整个流程是可行的。
方案二:将大文件直接存储到HDFS中,HBase中只存储图片或者视频的元数据以及标签信息。这个方案既避免把一个文件拆分成多个,而且可以充分利用底层的HDFS来保证整体数据的可靠性。
基于第二个方案,我们设计了如下技术架构图。
如上图所示,用户直接将数据写入到统一文件服务,然后统一文件服务进行判断,如果它 一个大于10M的数据,那么直接将数据存储在HDFS当中,而其它的一些比如文件的Meta信息和标签信息,则统一存储在HBase当中。
如果是小于10M的一些文件,那么我们可以直接将文件存储在HBase当中。
第二个问题是复查查询支持:
1)HBase 本身根据字典排查,我们可以将一些较为固定的标签设计在 Rowkey中,以便满足我们常规查询的需要;
2)如果系统需要支持更加灵活的检索功能,我们也可以引入Phoenix 来构建二级索引或者ElasticSearch来满足我们的检索功能。
(二)需求分析
我们选择上方的方案二来构建统一存储检索系统,整体架构图如下所示。
该项目是一个Spring Boot的项目,主要提供Rest API和SDK两种接口,支持以下功能:
1)Bucket的创建,删除,查询等接口;
2)上传文件,查询文件内容以及基本的信息;
3)根据标签信息查询获取文件信息以及文件的具体内容。
在数据存储上,每一个 Bucket 对应 HBase 的一张表,对于小文件(我们将所有小于2MB的文件定义为小文件,而大于2M的文件者称为较大文件)直接存储在HBase的表中,而大于2MB文件我们存储在HDFS中,并且将文件的Meta信息存储在HBase的表中。
额外说明:
1)在下面的实践项目中,我们只是简单的实现其中的部分功能,针对于服务本身的高可用和SDK等功能并不加以实现。
2)对于检索功能而言也只是实现一些简单的检索功能。
图片视频统一存储检索系统编码实践
(一)准备工作
接下来我们看一下应该做哪些前期工作。
如上所示,首先需要购买HBase增强版的服务,然后在用户的控制台详情里面去获取HBase的一些链接信息。
接着我们需要配置集群,开通公网,获取连接配置,还有本机一些白名单的东西,最后是开发环境的配置。
(二)HBase 表结构设计
说明:
1)为了能够支持检索功能,我们在RowKey的设计上采用 BucketName+标签+key 的方式,通过这种方式,可以使用HBase前缀扫描的方式找到在一个特定bucket下标签为tag_1的数据。
2)在数据存储上,我们存储了文件名,文件大小,文件创建时间和过期时间,还有它Bucket的名称。Content主要分为两块,第一个是追对小文件,会直接将数据存储在HBase当中,而针对于大文件,在Content里面是文件存在HDFS的路径。
3)虽然这种设计可以满足我们检索的功能,但是这种设计只能应用在一些标签相对比较固定的场景,而且会存在数据的热点问题。
4)该设计对于删除操作不友好,因为我们在RowKey前面加了一些其他信息,所以在删除的时候,我们需要先获取完整 的信息包括标签信息,单考虑到删除操作并不多,所以该设计在一定程度上还是可以满足我们的需求。
(三)项目目录介绍
这个项目主要有以下几个目录。
其中,common目录包含:
1)公用层,包括工具类和基础的bean类;
2)Controller层,对外提供的API接口;
3)Service层,核心处理逻辑,包括对HBase和HDFS操作的封装类等。
(四)Service 层
接下来我们看一下当用户上传一个文件的时候,服务端这边的处理流程。
服务端收到图片或者是视频文件的时候,首先会在HBase中存储Meta信息, 接着判断文件的大小是否超过长度限制: 1)如果长度没有超过限制,那么我们会将文件的内容直接存储在HBase表 中的fc:_content 2)如果长度超过了限制,那么我们先将文件写入到HDFS的特定的文件夹中,接着将文件的路径存储在fc:_content 当数据存储之后,用户如果要通过某一个标签进行查询,具体流程如下。
当服务端接收到查询请求,会根据容器名称和标签进行模糊查询, 获取文件的Mate信息,接着根据文件的长度进行判断:
1)如果长度小于存放HBase的阈值,说明文件是存储在HBase中的,直接从HBase中获取,整合数据之后返回给用户。
2)如果长度大于存储HBase的阈值,说明文件是存储在HDFS中,需要根据cf:content中存储的文件路径从HDFS中获取文件信息,组装完成之后返回给用户。
(五)效果演示
创建容器
查询容器
上传带有标签的图片(小图片)
上传一个50MB的大文件(代替视频文件)
根据标签查询文件
总结
虽然我们基本已经完成了老板交待的任务,项目也正常上线,但是从设计上来说,我们依旧遗留两个问题:
1)RowKey的热点问题,因为我们是将容器名称和标签作为前缀,那么当存储的文件达到一定程度之后势必会导致热点,影响HBase的稳定性。
2)无法支持复杂的查询,并且由于用于查询条件的标签是开始就固定在RowKey当中的,所以之后如果想扩展标签的维度势必会非常困难。
那么针对上面的两个问题,我们应该如何解决呢?
其实上面的两个问题,都是由于我们将RowKey作为查询的依据而产生的问题,那么我们是否可以将查询所使用到的字段不存储在RowKey上,这样我们就可以使用一个随机的RowKey避免热点问题,答案是肯定的。
下面介绍两种比较常见的开源方案。
方案一:使用HBase + Phoenix 的方案,创建容器名称和标签作为二级索引,查询通过Phoenix对二级索引进行加速。通过这种方式,我们可以支持更多字段的查询,而且后期在扩展的时候,我们只要添加更多的二级索引,就可以通过其他标签进行查询。
方案二:使用HBase + ElasticSearch 方案,索引信息通过 hbase-indexer 服务将标签同步到ES 中,查询的时候先查询ES,通过标签获取HBase明细表里的RowKey信息,再从HBase中获取明细数据。通过这种方式,我们可以支持更多更复杂的查询条件。
是否有更好的方案?
阿里巴巴Lindorm多模引擎集成了宽表,时序,搜索还有文件系统,完全可以支持这种统一存储,而且通过它的Solr的功能,可以为我们提供一些更好的复杂查询来满足各种需求。