本文是公司的命题作文,应该会发布在我们组的技术博客中。
HBase是一个采用LSM结构的、支持多版本的数据库。与一些数据库解决并发读写问题的mvcc不同(当然HBase也有mvcc),这里的多版本是指在语义层面支持向同一个rowkey+column
family+column
qualifier中写入多个timestamp不同的value,是暴露给用户的语义层面的多版本,这也是遵循了BigTable的设计。不过HBase的timestamp不仅可以由服务器根据写入时的时间戳生成,也可以由用户指定,因此会额外多一些需要考虑的问题,一些问题至今仍没有很好的解决,本文将描述这些问题,供对HBase感兴趣的人参考,并将在不久的将来在社区的新版中修复这些问题。
时间戳是目前HBase中作为判断同一列数据“版本”新旧的唯一标准,读取数据的时候可以指定读取多少个版本,会返回从新到旧的X个最新版本数据给客户端。同时在建表的配置中每个CF可以配置保留多少个版本的数据,超过这个配置后会在compaction时清理掉最旧的若干个版本。因此,如果一个用户按先后顺序分别对同一列写了时间戳为5、4、3、2、1的五个Put,同时保留最新的3个版本,那么读到的会是5、4、3,哪怕这三个其实是最先写入的。
同时因为HBase是一个LSM结构的数据库,在这种结构中,会把删除操作转化为一个特殊的写入,叫delete
marker或tombstone,因此Delete和Put在写入时的性能是完全一样的。而在读取时,如果读到了某个type为Delete的数据,就会根据这个Delete对象设置的语义来屏蔽掉对应的Put,以保证客户端看不到被删掉的数据。也就是说刚执行完删除操作后,数据占用空间不仅不会变小,反而会变大。只有在执行Minor
Compaction时,才会清掉所读取的HFile中被Delete删除掉的Put,释放空间;在执行Major
Compaction时,才会确保删除掉所有被删除的Put,并删除Delete。
目前HBase的Delete支持的语义比较多(JavaDoc见此):不考虑多版本问题的话,可以说HBase支持删掉某行、或某个cf的所有列(cq)、或某列(cf+cq)的数据;如果考虑多版本,则只能说HBase支持删除行/cf/列下的某个特定时间戳的版本,或时间戳小于等于这个值的所有版本。即使用户没有设置时间戳,系统也会用一个隐含的当前时间戳来作为这个Delete的时间戳,并按照“时间戳小于等于这个值的所有版本”来执行。Put单纯考虑时间戳而不考虑写入顺序,可以比较方便的补一个旧版的数据。但是如果Delete也只考虑时间戳不考虑操作的顺序,就会带来一些比较违反直觉的问题:
1,如果写了一个比较大的时间戳的Put,大于当前时间,再想通过文档中描述的“Delete all versions of
”的功能来删,那么这个Put是删不掉的。所谓的删除所有版本,隐含着的语义是删除时间戳小于等于当前时间的所有版本,因为如果client没有指定一个update操作的时间戳的话,会用系统当前时间戳。
2,如果为了删掉第一条里的Put,手动指定了一个比较大时间戳的Delete去删,那么就意味着以后再写入数据如果用默认的系统时间作为时间戳,就读不到了,因为这些数据的时间戳都小于那个Delete的时间戳从而被屏蔽掉。但是一旦后台执行了Major
Compaction,把Delete清理掉,再用当前时间作为时间戳写数据,就能读到了。而Major
Compaction对用户是透明的,对用户来说意味着行为不一致。
3,如果先后写入了时间戳为1、2、3三个版本的数据,同时表的设置为保留最新的两个版本,则会读到3、2,读不到1。如果在写完3之后,通过删除指定版本的接口删掉2这个数据,那么能读到3和1,因为这两个是目前的最大版本。但是如果在删除之前执行了一次Major
Compaction,时间戳为1的Put会被清理,(因为有2和3两个比他新的版本),这个时候如果再删2,那么就只能读到3了。也是Major
Compaction对用户不透明。
4,如果在同一毫秒内对一个列进行操作,多版本会无效(因为多版本只跟时间戳有关,并且时间戳精度是毫秒),并且Delete会覆盖Put(可以理解为这是人为定义的顺序),哪怕实际上是前半毫秒先Delete后半毫秒再Put,也会导致读不到数据。这个问题虽然可以靠提高时间戳的精度解决,比如用纳秒,但是并不解决本质问题。
上述4个问题导致HBase的删除和多版本的语义比较奇怪,目前或者说HBase诞生以来之所以这样做,一个是逻辑简单、性能比较好,一个是可以保证HBase所有的操作对于相同的row+cf+cq+ts的操作是幂等的,若干个操作互换顺序、反复重复操作,最终的状态是一致的。但是由于后台的compaction的存在,这个一致性是打了折扣的;并且不依赖操作的先后顺序之后很多事情会违反用户的直觉。因此最近我们也在讨论着手去修改这个问题。
在语义上,我们希望HBase能实现删除语义的“顺序一致性”,即删除与写入操作的顺序也会影响读取的结果,更符合用户的直观感受。而Put的操作依然通过时间戳来定义新旧,与之前保持一致。先进行的删除操作无论其时间戳设为多少,都不会影响后续的Put操作,如果Delete删除的时间戳为MaxLong,则相当于暂时清空所有版本,后续仍旧可以写入,而非相当于永久屏蔽所有版本导致这个行或cf无法写入任何数据。实现该语义后,额外带来的好处是当用户想“删除这列的最新的版本”时,不需要先读这个列获取最新时间戳再写入这个时间戳的Delete,而是直接写入一个特殊的delete
marker,根据其写入的顺序来决定他屏蔽的版本,以提高删除时的性能。
同时,对于多版本的写入,无论Major
Compaction是否发生,一旦当前列有超过MaxVersions个版本之后,旧的数据都应该被立刻“逻辑删除”,永远不可见,而非像目前一样一旦删了某个新版本,那些之前被新版冲掉的数据如果没有被compaction清理掉就又可见了。
当然,修改这两个语义之后,也意味着在读取的时候不能直接按时间戳排序,也要根据mvcc来判断操作的先后顺序,通过更复杂的逻辑来保证这两个语义,因此性能可能会有所影响。所以这个语义会是可配置、默认关闭的,以实现语义和性能上的兼容。我们会在完善细节上的设计之后着手修改社区上的版本,与社区一同改善HBase的功能、让HBase更加强大。