HBase 体系之 Write-ahead-Log

本文翻译自: http://www.larsgeorge.com/2010/01/hbase-architecture-101-write-ahead-log.html

      什么是Write-ahead-Log呢?在之前的博文中已经讲过了HBase的基本存储架构。其中提到的一个部分就是Write-ahead-Log,或者简称WAL。这边博文将详细的讲述hlog的工作流程。

      

      WAL是故障发生时进行恢复的重要工具。这和MYSQL中的BIN log很相似,都是记录所有的数据操作。这种策略是非常重要的以防内存发生一些异常,这可以使服务器发生故障的时候根据log恢复到他发生刚故障之前的那个时刻。这也意味着数据写入的时候如果WAL写入失败了,这次数据操作也会被判定为失败的。

      让我们在高级视图上看一下HBase是怎么实现这个功能的。首先客户端初始化一个更改数据的动作,这现在被叫 put(Put),delete(Delete) 和 incrementColumnvalue()(这里有是也简称为incr),每一种改动都被包装进KeyValue对象利用远程调用发送到Reginoserver对应这次改动的Region(s)中,数据在RegionServer中首先被写入WAL,然后被写入MemStore。

      最后当Memstore达到一定的大小或者到达指定的时刻之后,数据被异步的持久化到文件系统上。在这之前数据是存储在内存中的,在这段时间里面如果regionserver崩溃了,内存的数据就没有了,但是我们有WAL,就可以恢复数据。

      HLog

      实现WAL的类叫做HLog,在之前的博文中已经介绍过了每个RegionServer上面只有一个HLog的实例。当一个HRgion初始化的时候HLog会传入构造方法对HRgion进行初始化。
      HLog的核心方法是append(),它在内部也被叫做doWrite(),这两个方法的作用就像它们的名字一样。这里还有一件要注意的事情就是在执行数据改动操作的时候有一个选项,setWriteToWAL(boolean) 。如果你将这个方法的值设置为false,那么在数据改动的时候将没有WAL写入,这个选项的默认值是true,一般情况下你一定会需要WAL的。但是如果你说你要运行大型的MapReduce任务,想要随时返回结果,获得好的性能的不需要关注数据的丢失,你可以选择不使用WAL。
      HLog另外一个重要的特性就是追踪改变。这是用一个 “sequence number” 来实现的。它使用一个AtomicLong来保证线程安全,它的大小要么是0,要么是最大的那个持久到文件系统的seq number。所以当region 打开它的存储文件的时候,它会读出Hfile最大的seq number,如果这个数字比HLog最大的那个seq number还要大,就把当前的HLog的seq设置为它,所以打开完所有的存储文件之后,HLog的seq就反映了持久化的数据最后的地方,等一会儿会看到这个设计的作用。

      上面的图片展示了三个不同的Region,他们每一个都包含了不同的rowkey范围的数据,和上面描述的一致,这三个Region共享同一个HLog实例。这意味着当三个Region中有数据写入的时候,他们写入HLog的顺序是不可预测的。在下面会解释这一点。
      当前的WAL使用hadoop的Sequence File进行存储,它存储的是键值对记录。WAL的value 是客户端要写入数据的键值对,key则是HLogKey实例。如果你可以会议起上一篇关于存储的博文http://www.larsgeorge.com/2009/10/hbase-architecture-101-storage.html 中所讲的内容,在这里keyValue 不只由 row,column family,qualifier ,timestamp,和value,还有“key type”。这里的key type代表着一次 “put”或者是“delete”。
      我们还没有提到的就是KeyValue属于谁,也就是Region和table name。他们都是被存储在HLogKey里面的,还有上面提到的sequence number也是被存储在这里面的。每次记录seq number都会增长,这样可以保持操作写入的顺序,最后还记录了 写入时间。

      LogFlusher

      上面提到当数据到达RegionServer的时候KeyValue实例被(选择性的)写入WAL,然后会被写入Sequence File。看起来这不重要,但并不是这样。在java中有一个基类叫做Stream。一般情况下文件系统会使用缓冲来提高性能因为系统在批量写入数据的时候会更快。如果将上述的记录利用分开的IO流写这样会使吞吐量非常的糟糕。但是在这里造成的差异就是本来WAL是要写入文件系统的但是实际上它却是在内存中,为了减轻这个问题需要一层缓冲,这里提供了一个Logflusher类。这里调用HLog.optionalSync(),他会检查配置中hbase.regionserver.optionallogflushinterval(默认是设置为10秒) 是否被改变了,还有是否这种情况调用了HLog.sync(),另外一个调用这个同步方法的地方是HLog.doWrite()。

        LogRoller

      很明显对于log的写入大小限制是有意义的。当我们想要确认一个log是持久化到了文件系统上去了的,这是LogRoller实现的。这是hbase-site.xml文件里面的hbase.regionserver.logroll.period参数来确定的,默认设置是1小时。所以每60分钟log被关闭并且重新写入到一个新的log中。一段时间之后这里聚集了一堆log文件。HLog.rollWriter()方法被调用的时候会滚动产生一个新的日志文件用来存储新得log,rollWriter里面还调用了cleanOldLogs来刷新还在内存里面的log。它会检查写入存储文件的最大seq number,因为在那个number之前的所以文件都已经持久化到文件系统了。检查旧的log文件,如果某个log文件里面所有的seq number都小于刚才查出来的那个seq number,说明这个log中的所有操作都已经持久化到文件系统上了,这个log就可以删除了。
      另外一个控制log滚动的参数是hbase.regionserver.hlog.blocksize 和 hbase.regionserver.logroll.mutilplier,默认设置是达到SequenceFile的blocksize的95%的时候对日志进行滚动,一般这个大小是64M,所以要么是logs达到一定的大小,要么是logs经历了一定的时间,他才会被滚动。

        Replay

      一旦一个regionserver启动了,它开启一个region的时候会检查这里是否还有剩下的日志,如果有就将这些日志重放,使用Store.doRestructionLog(),重放日志就是简单的将日志里面的操作写入到Memstore,当Memstore的数据被刷新到文件系统的时候,日志重放就完成了。
     这些老的日志通常是由前一个region server崩溃留下的,当master发现有region server崩溃的时候,它会将这个regionserver的日志根据所属的region进行拆分,然后放到各自Region的目录下面,然后上面提到的重放机制会检测到日志进行恢复。值得注意的是只有被拆分的log才能被恢复,拆分日志使用的是HLog.splitLog()。老的log会被读入内存然后被写入各自所属的region。

        Issues

        上面提到所有的改动都被写入到Regionserver的一个HLog中,那么疑问来了?为什么不给每一个region都写入一个Hlog呢。
用BigTable里面的话来说就是这会造成大量的磁盘寻址。
      HBase遵循的规则类似,就像上面解释的那样,这会滚动出很多的日志直到数据操作被写入文件系统,这样这些log就会被删除,如果想单独的每个region都分开写log文件,迟早会造成一些问题。
      目前为止看起来是没有问题,但是重申一次,它会在事情变糟糕的时候发生问题。在平常,将所有的数据改动都安全的持久化到文件系统,这很好。但是如果在region server崩溃的时候需要分割日志文件,但是这时候日志文件是存储在各个region下的,这里也并没有索引能够找到这些日志文件,所以这时候master无法找到日志文件。
      另外一个问题就是数据安全。你想依赖这个系统在不管什么新奇的场景下来保全你的数据。如果想要尽量的使数据的安全性高你可以把log flush的时间调整得尽量的低。当数据流被写入到这个系统之后,是否它已经被写入到磁盘了呢。
      到了这个时候我们已经非常的清楚HLog就是用来保证数据的安全性的了。因此一个log应该要保持打开长达一个小时(甚至更长如果有这样的配置)。当数据到达的时候一个新的key/value被写入Sequence File然后被flush到磁盘。但是这并不是Hadoop的工作方式。它提供了一个API,这个API可以打开一个文件,写入大量的数据,然后马上关闭,然后留下一个不变的文件留给其他人访问。而且只有文件被关闭的时候他对其他人才是可见并且可读的。如果一个进程在写入数据的死掉了,这次的操作会被认定是失败的。还有一个需求就是允许能够读到server在崩溃那个时刻的log或者说越近越好。
      append在HDFS中通常很有用但是在HBase中却不是,HBase中使用hflush().他所做的是将一起的数据写入磁盘当log被写入的时候,这是为了使server崩溃之后我们可以安全的读到崩溃的最后一刻的数据。

        Append/Sync

      即使有hflush()我们还是有一个问题,就是频繁的调用会导致系统变慢。改进的方法就是实现 "Group Commit ",它会批量的刷新纪录。.META. 表每次改动都会使用sync,用户表可以选择配置。

        Distributed Log Splitting

      当region需要被重新部署的时候,这是后分裂log是一个问题。有一个方法就是在zookeeper上面存储一个被改动过的region的列表,这样没有被改动的region就可以马上被部署。这有那些被改动的region需要被等待直到logs 被split。
      剩下的问题就是如何split log更快了。下面是BigTable在论文里面提出的解决的方案:

      为了不重复的读取log,首先将提交的log进行排序,按照(table,row name,long seq number) ,这样就可以按顺序读出记录分配到各个server上去。




https://blog.csdn.net/u010916254/article/details/48025445

猜你喜欢

转载自blog.csdn.net/varyall/article/details/80451897