HDFS 设计的初衷是为了存储大文件(例如日志文件),面向批处理、顺序 I/O 的。然而架设在 HDFS 之上的 HBase 设计的初衷却是为了解决海量数据的随机读写的请求。把这两种设计初衷截然相反的组件怎么揉在一起的呢?这种分层的结构设计主要是为了使架构更清晰,HBase 层和 HDFS 层各司其职;但是却带来了潜在的性能下降。在很多业务场景中大家使用 HBase 抱怨最多的两个问题就是:Java GC 相关的问题和随机读写性能的问题。Facebook Messages(以下简称 FM 系统)系统可以说是 HBase 在 online storage 场景下的第一个案例(《 Apache Hadoop Goes Realtime at Facebook 》, SIGMOD 2011),最近他们在存储领域顶级会议 FAST2014 上发表了一篇论文《 Analysis of HDFS Under HBase: A Facebook Messages Case Study 》分析了他们在使用 HBase 中遇到的一些问题和解决方案,使用 HBase 做 online storage 的同学们可以参考下。
该论文首先讲了 Facebook 的分析方法包括 tracing/analysis/simulation,FM 系统的架构和文件与数据构成等,接下来开始分析 FM 系统在性能方面的一些问题,并提出了解决方案。
FM 系统的主要读写 I/O 负载
Figure 2 描述了每一层的 I/O 构成,解释了在 FM 系统对外请求中读占主导,但是由于 logging/compaction/replication/caching 导致写被严重放大。
- HBase 的设计是分层结构的,依次是 DB 逻辑层、FS 逻辑层、底层系统逻辑层。DB 逻辑层提供的对外使用的接口主要操作是 put() 和 get() 请求,这两个操作的数据都要写到 HDFS 上,其中读写比 99/1(Figure 2 中第一条)。
- 由于 DB 逻辑层内部为了保证数据的持久性会做 logging,为了读取的高效率会做 compaction,而且这两个操作都是写占主导的,所以把这两个操作(overheads)加上之后读写比为 79/21(Figure 2 中第二条)。
- 相当于调用 put() 操作向 HBase 写入的数据都是写入了两份:一份写入内存 Memstore 然后 flush 到 HFile/HDFS,另一份通过 logging 直接写 HLog/HDFS。Memstore 中积累一定量的数据才会写 HFile,这使得压缩比会比较高,而写 HLog 要求实时 append record 导致压缩比( HBASE-8155 )相对较低,导致写被放大 4 倍以上。 Compaction 操作就是读取小的 HFile 到内存 merge-sorting 成大的 HFile 然后输出,加速 HBase 读操作。Compaction 操作导致写被放大 17 倍以上,说明每部分数据平均被重复读写了 17 次,所以对于内容不变的大附件是不适合存储在 HBase 中的。由于读操作在 FM 业务中占主要比例,所以加速读操作对业务非常有帮助,所以 compaction 策略会比较激进。
HBase 的数据 reliable 是靠 HDFS 层保证的,即 HDFS 的三备份策略。那么也就是上述对 HDFS 的写操作都会被转化成三倍的 local file I/O 和两倍的网络 I/O。这样使得在本地磁盘 I/O 中衡量读写比变成了 55/45。
4. 然而由于对本地磁盘的读操作请求的数据会被本地 OS 的 cache 缓存,那么真正的读操作是由于 cache miss 引起的读操作的 I/O 量,这样使得读写比变成了 36/64,写被进一步放大。 另外 Figure 3 从 I/O 数据传输中真正业务需求的数据大小来看各个层次、各个操作引起的 I/O 变化。除了上面说的,还发现了整个系统最终存储在磁盘上有大量的 cold data(占 2/3),所以需要支持 hot/cold 数据分开存储。
总的来说,HBase stack 的 logging/compaction/replication/caching 会放大写 I/O,导致业务逻辑上读为主导的 HBase 系统在地层实际磁盘 I/O 中写占据了主导。
FM 系统的主要文件类型和大小
FM 系统的几种文件类型如 Table 2 所示,这个是纯业务的逻辑描述。在 HBase 的每个 RegionServer 上的每个 column family 对应一个或者多个 HFile 文件。FM 系统中有 8 个 column family,由于每个 column family 存储的数据的类型和大小不一样,使得每个 column family 的读写比是不一样的。而且很少数据是读写都会请求的,所以 cache all writes 可能作用不大(Figure 4)。
对于每个 column family 的文件,90% 是小于 15M 的。但是少量的特别大的文件会拉高 column family 的平均文件大小。例如 MessageMeta 这个 column family 的平均文件大小是 293M。从这些文件的生命周期来看,大部分 FM 的数据存储在 large,long-lived files,然而大部分文件却是 small, short-lived。这对 HDFS 的 NameNode 提出了很大的挑战,因为 HDFS 设计的初衷是为了存储少量、大文件准备的,所有的文件的元数据是存储在 NameNode 的内存中的,还有有 NameNode federation。
FM 系统的主要 I/O 访问类型
下面从 temporal locality, spatial locality, sequentiality 的角度来看。
73.7% 的数据只被读取了一次,但是 1.1% 的数据被读取了至少 64 次。也就是说只有少部分的数据被重复读取了。但是从触发 I/O 的角度,只有 19% 的读操作读取的是只被读取一次的数据,而大部分 I/O 是读取那些热数据。
在 HDFS 这一层,FM 读取数据没有表现出 sequentiality,也就是说明 high-bandwidth, high-latency 的机械磁盘不是服务读请求的理想存储介质。而且对数据的读取也没有表现出 spatial locality,也就是说 I/O 预读取也没啥作用。
解决方案
1. Flash/SSD 作为 cache 使用。
下面就考虑怎么架构能够加速这个系统了。目前 Facebook 的 HBase 系统每个 Node 挂 15 块 100MB/s 带宽、10ms 寻址时间的磁盘。Figure 9 表明:a) 增加磁盘块数有点用;b) 增加磁盘带宽没啥大用;c) 降低寻址时间非常有用。
由于少部分同样的数据会被经常读取,所以一个大的 cache 能够把 80% 左右的读取操作拦截而不用触发磁盘 I/O,而且只有这少部分的 hot data 需要被 cache。那么拿什么样的存储介质做 cache 呢?Figure 11 说明如果拿足够大的 Flash 做二级缓存,cache 命中率会明显提高,同时 cache 命中率跟内存大小关系并不大。
注:关于拿 Flash/SSD 做 cache,可以参考 HBase BucketBlockCache( HBASE-7404 )
我们知道大家比较关心 Flash/SSD 寿命的问题,在内存和 Flash 中 shuffling 数据能够使得最热的数据被交换到内存中,从而提升读性能,但是会降低 Flash 的寿命, 但是随着技术的发展这个问题带来的影响可能越来越小。
说完加速读的 cache,接着讨论了 Flash 作为写 buffer 是否会带来性能上的提升。由于 HDFS 写操作只要数据被 DataNode 成功接收到内存中就保证了持久性(因为三台 DataNode 同时存储,所以认为从 DataNode 的内存 flush 到磁盘的操作不会三个 DataNode 都失败),所以拿 Flash 做写 buffer 不会提高性能。虽然加写 buffer 会使后台的 compaction 操作降低他与前台服务的 I/O 争用,但是会增加很大复杂度,所以还是不用了。最后他们给出了结论就是拿 Flash 做写 buffer 没用。
然后他们还计算了,在这个存储栈中加入 Flash 做二级缓存不但能提升性能达 3 倍之多,而且只需要增加 5% 的成本,比加内存性价比高很多(怎么感觉有点像 SSD 的广告贴)。
2. 分层架构的缺点和改进方案
如 Figure 16 所示,一般分布式数据库系统分为三个层次:db layer/replication layer/local layer。这种分层架构的最大优点是简洁清晰,每层各司其职。例如 db layer 只需要处理 DB 相关的逻辑,底层的存储认为是 available 和 reliable 的。
HBase 是图中 a) 的架构,数据的冗余 replication 由 HDFS 来负责。但是这个带来一个问题就是例如 compaction 操作会读取多个三备份的小文件到内存 merge-sorting 成一个三备份的大文件,这个操作只能在其中的一个 RS/DN 上完成,那么从其他 RS/DN 上的数据读写都会带来网络传输 I/O。
图中 b) 的架构就是把 replication 层放到了 DB 层的上面,Facebook 举的例子是 Salus,不过我对这个东西不太熟悉。我认为 Cassandra 就是这个架构的。这个架构的缺点就是 DB 层需要处理底层文件系统的问题,还要保证和其他节点的 DB 层协调一致,太复杂了。
图中 c) 的架构是在 a 的基础上的一种改进,Spark 使用的就是这个架构。HBase 的 compaction 操作就可以简化成 join 和 sort 这样两个 RDD 变换。
Figure 17 展示了 local compaction 的原理,原来的网络 I/O 的一半转化成了本地磁盘读 I/O,而且可以利用读 cache 加速。我们都知道在数据密集型计算系统中网络交换机的 I/O 瓶颈非常大,例如 MapReduce Job 中 Data Shuffle 操作就是最耗时的操作,需要强大的网络 I/O 带宽。加州大学圣迭戈分校(UCSD) 和微软亚洲研究院(MSRA) 都曾经设计专门的数据中心网络拓扑来优化网络I/O 负载,相关研究成果在计算机网络顶级会议SIGCOMM 上发表了多篇论文,但是由于其对网络路由器的改动伤筋动骨,最后都没有成功推广开来。
Figure 19 展示了 combined logging 的原理。现在 HBase 的多个 RS 会向同一个 DataNode 发送写 log 请求,而目前 DataNode 端会把来自这三个 RS 的 log 分别写到不同的文件 / 块中,会导致该 DataNode 磁盘 seek 操作较多(不再是磁盘顺序 I/O,而是随机 I/O)。Combined logging 就是把来自不同 RS 的 log 写到同一个文件中,这样就把 DataNode 的随机 I/O 转化成了顺序 I/O。
作者简介
梁堰波,北京航空航天大学计算机硕士,美团网资深工程师,曾在法国电信、百度和 VMware 工作和实习过,这几年一直在折腾 Hadoop/HBase/Impala 和数据挖掘相关的东西,新浪微博 @DataScientist 。
评论