在信息爆炸的大数据时代,如何以更低成原本解决海量数据的存储问题,已成为企业大数据业务中的重要一环。UCloud自研的新一代对象存储服务US3,在过去一段时间,针对大数据业务场景推出了计算存储分离和大数据备份解决方案。数据库
这背后的主要缘由包括:缓存
一、因为网络技术的高速发展,使得网络传输性能再也不是大数据场景下高吞吐业务需求的瓶颈;
二、Hadoop技术栈中的HDFS存储解决方案运维复杂且成本高昂;
三、云平台基于海量存储资源池构建的对象存储服务US3具有按需使用、操做简单、可靠稳定、价格便宜的优点,是替换HDFS的最佳存储方案选择。所以,为了让用户可以更加方便的在Hadoop场景下,使用US3实现计算存储分离和大数据备份解决方案,US3自研了US3Hadoop适配器、US3Vmds、US3Distcp三个组件。网络
本文主要介绍US3Hadoop适配器在研发设计过程当中的一些思路和问题解决。架构
整体设计思路并发
Hadoop生态里对存储的操做基本上都是经过一个通用的文件系统基类FileSystem来进行的。US3Hadoop适配器(简称:适配器)是经过US3FileSystem实现该基类来操做US3。相似于HDFS实现的DistributedFileSystem和基于AWS S3协议实现的S3AFileSystem。适配器直接把IO和索引都请求发给US3,架构以下图所示:app
这里的索引操做主要是不涉及读写数据的API,如: HEADFile, ListObjects, Rename, DeleteFile, Copy(用于修改metadata);IO操做的API,如GetFile,PutFile(小于4M文件)已经分片上传相关的4个API: InitiateMultipartUpload,UploadPart,FinishMultipartUpload,AbortMultipartUpload。US3有了这些API后,怎么跟FileSystem的成员方法能对应起来,能够看下FileSystem须要重写哪些方法。结合实际需求和参考DistributedFileSystem、S3AFileSystem的实现,咱们肯定了须要重写的主要方法:initialize、create、rename、getFileStatus、open、listStatus、mkdirs、setOwner、setPermission、setReplication、setWorkingDirectory、getWorkingDirectory、getSchem、getUri、getDefaultBlockSize、delete。同时对一些难以模拟的方法,重写为异常不支持,如Append成员方法。运维
其实从上面FileSystem的成员方法说明来看,其语义和单机文件系统引擎的接口语义相似,基本上也是以目录树结构来组织管理文件系统。US3提供的ListObjects API恰好也提供了目录树拉取的一种方式,当Hadoop调用listStatus方法时,就能够经过ListObjects循环拉取到当前目录(前缀)下全部子成员从而返回对应的结果。tcp
设置文件所属用户/组,操做权限等相关操做则利用了US3的元数据功能,把这些信息都映射到文件的KV元数据对上。写入文件流则会优先缓存在内存中最多4MB数据,再根据后续的操做来决定采用PutFile仍是分片上传的API来实现。oop
读取文件流则经过GetFile返回流实例来读取期待的数据。虽然这些方法实现看上去很直白,可是潜在着不少值得优化的地方。性能
getFileStatus的时空博弈
经过分析FileSystem的调用状况,能够知道索引操做在大数据场景中占比达70%以上,而getFileStatus在索引操做重占比最高,因此有必要对其进行优化。那优化点在哪里呢?
首先由于US3中的“目录”(对象存储是KV存储,所谓目录只是模拟而已)是以‘/’结尾的Key,而FileSystem的对文件的操做是经过Path结构进行,该结构的路径都不会以‘/’结尾,因此经过Path拿到的Key去US3中进行HeadFile时,有可能因为该Key在US3中是目录,HeadFile就会返回404, 必须经过第二次用“Key/”去US3中Head才能确认。若是这个Key目录仍是不存在,就会致使getFileStatus时延大大增长了。
所以US3适配在建立目录时作了如下两件事:1.向US3写入mime-type为“file/path”, 文件名为“Key”的空文件;2.向US3写入mime-type为“application/x-director”, 文件名为“Key/”的空文件;
而普通文件mime-type为“application/octet-stream”。这样在getFileStatus中经过一次HeadFile API就判断当前Key究竟是文件仍是目录,同时当该目录下为空时,也能在US3控制台展示出该目录。并且因为Hadoop场景写入的主要是大文件,增长一次空文件索引的写入耗时在ms级别,时延基本可忽略。
此外,getFileStatus在Hadoop的使用中具有明显的“时空局部性”特征,在具体的FileSystem实例中最近被getFileStatus操做的Key,在短期会被屡次操做。利用这个特色,US3FileSystem在实现过程当中,getFileStatus获得对应的结果在FileStatus返回以前,会把有3s生命周期的FileStatus插入到Cache中。那后续3秒内对该Key的操做就会复用Cache中该Key的FileStatus信息,固然delete操做会在US3中删除完Key后,直接把Cache中的有效FileStatus标记为有3s生命周期的404 Cache,或者直接插入一个有3s生命周期的404 Cache。若是是rename,会复用源的Cache来构造目的Key的Cache,并删除源,这样就能减小大量跟US3的交互。Cache命中(us级别)会减小getFileStatus上百倍的时延。
固然这会引入必定的一致性问题,但仅限于在多个Job并发时至少有一个存在“写”的状况,如delete和rename的状况下,若是仅仅只有读,那么无影响。不过大数据场景基本属于后者。
ListObjects一致性问题
US3的ListObjects接口跟其余对象存储方案相似,目前都只能作到最终一致性(不过US3后续将推出强一致性保证的ListObjects接口),所以其余对象存储实现的适配器也都会存在写入一个文件,而后当即调用listStatus时会偶尔出现这个文件不存在的状况。其余对象存储方案有时会经过引入一个中间件服务(通常是数据库),当写入一个文件会向这个中间件写入这个文件索引,当listStatus时会跟中间件的索引信息进行合并,这样确实缓解了这种状况,进一步提升了一致性。
但还不够,好比写入对象存储成功,但写入中间件时程序奔溃了,这样就致使不一致的问题,又回到了最终一致性的问题。
US3Hadoop适配器的实现相对更加简单有效,不须要借助额外的服务,能提供索引操做级别的Read-Your-Writes一致性,而该一致性级别在Hadoop大部分场景基本等同于强一致性。US3Hadoop适配器不像S3AFileSystem的实现,在create或者rename、delete后立马返回,而是在内部调用ListObjects接口作了一次“对帐”,直到“对帐”结果符合预期则返回。
固然这里也是有优化空间的,好比delete一个目录时,对应会把这个目录下全部文件先拉出来,而后依次调用DeleteFile API去删除,若是每次DeleteFile API删除都“对帐”一次,那么整个时延会翻倍。US3Hadoop适配器的作法是只对最后一次索引操做进行“对帐”,这是因为索引的oplog是按时序同步到列表服务中,若是最后一条索引“对帐”成功,那么前面的oplog必定在列表服务中写入成功。
Rename的深度定制
前面提到的rename也是US3的一个重要优化点,其余对象存储方案的实现通常经过Copy的接口会先把文件复制一遍,而后再删除源文件。能够看出若是rename的文件很大,那么rename的整个过程势必致使时延很高。
US3根据该场景的需求,专门开发了Rename的API接口,所以US3Hadoop适配器实现rename的语义相对比较轻量,并且时延保持在ms级别。
保证read高效稳定
读是大数据场景的高频操做,因此US3Hadoop适配器的读取流实现,不是对http响应的body简单封装,而是考虑了多方面的优化。例如,对读取流的优化,经过加入预读Buffer,减小网络IO系统调用频率,下降read操做的等待时延,特别是大批量顺序读的IO提高效果明显。
另外,FileSystem的读取流具备seek接口,也就是须要支持随机读,这里又分两种场景:
一、seek到已读流位置的前置位置,那么做为Underlay Stream的Http响应的body流就要做废关闭掉,须要从新发起一个从seek的位置开始分片下载的GetFile API,得到其Http响应的body流来做为新的Underlay Stream。可是实际测试过程当中发现,不少seek操做事后不必定会进行read操做,有可能直接关闭,或者seek回到已读取流位置的后置位置,因此在seek发生时,US3Hadoop适配器的实现是只作seek位置标记,在read的时候根据实际状况对Underlay Stream作延迟关闭打开处理。此外若是seek的位置还在Buffer中,也不会从新打开Underlay Stream,而是经过修改Buffer的消费偏移。
二、随机读的另外一种场景就是,seek到已读流位置的后置位置。这里一样跟前面同样采用延迟流打开,可是在肯定要作真实的seek操做时,不必定会经过关闭老的Underlay Stream,从新在目标位置打开新的Underlay Stream来实现。由于当前已读的位置跟seek的后置位置可能距离很近,假设只有100KB距离,说不定这段距离彻底在预读Buffer的范围中,这时也能够经过修改Buffer的消费偏移来实现。
事实上US3Hadoop适配器确实也是这么作的,不过目前的规则是seek的后置位置到当前已读流位置的距离小于等于预读Buffer剩余空间加上16K的和,则直接经过修改预读Buffer的消费偏移和消费Underlay Stream中的数据来定位到seek的后置位置上。之因此还加了16K是考虑到TCP接收缓存中的数据。固然后续肯定从一个ready的Underlay Stream中消费N字节数据的时间成本大体等于从新发起一个GetFile API并在准备传输该Http响应body以前的时间成本,也会考虑把这N字节的因素计入偏移计算过程当中。
最后流的优化还要考虑到Underlay Stream异常的状况,好比HBase场景长时间持有打开的流,却因为其余操做致使长时间没有操做该流,那么US3可能会主动关闭释放Underlay Stream对应的TCP链接,后续对在Underlay Stream上的操做就会报TCP RST的异常。为了提供可用性,US3Hadoop适配器的实现是在已经读取位置点上进行Underlay Stream的从新打开。
写在最后
US3Hadoop适配器的实如今借鉴开源方案下,进一步优化了相关核心问题点,提高了Hadoop访问US3的可靠性与稳定性,并在多个客户案例中发挥着打通Hadoop与US3的重要桥梁做用,帮助用户提高大数据场景下的存储读写效率。
但US3Haoop适配器还存在不少可提高的空间,相比于HDFS,索引、IO的时延还有差距,原子性保障上也相对比较弱,这些也是咱们接下来要思考解决的问题。目前推出的US3Vmds解决了索引时延的大部分问题,使得经过US3Hadoop适配器操做US3的性能获得大幅提高,并在部分场景接近原生HDFS的性能。具体数据能够参考官方文档(https://docs.ucloud.cn/ufile/...
将来,US3产品会不断改进优化大数据场景下的存储解决方案,在下降大数据存储成本的同时,进一步提高用户在大数据场景下的US3使用体验。