Nebula北京Meetup总结

Nebula在京东北辰组织了他们的第二次Meetup,公司CEO叶小萌是蚂蚁金服图数据库Geobase的负责人。两位技术总监:陈恒和侯凤林,陈恒之前是HBase committer,现主要负责Nebula的存储层设计,类似TiKV。侯凤林(dutor)应该是负责query engine,类似TiDB。 架构是典型的存储计算分离,计算层设计是类SQL,添加go语法来实现边跳转,管道来简化嵌套子查询。storage层是kv结构,按照key做静态hash分区(暂不支持动态分区),hash值是根据顶点计算(解释图场景几乎没有按照点做order by的需求),也就是同一个顶点的所有点和出入边都存同一个分区,出入边遍历友好。但是,对求A到B之间的top 10权重边这种需求,查询不友好,需要单独加索引。 每个分区三副本组成一个raft group来实现复制,rocksdb引擎。单节点可能有很多分区,而每个分区都需要一个线程来负责推raft log,他们实现了一个共享线程池来负责推raft log,从而控制线程数。每个group内可设leaner角色,就是异步复制到其他系统,用于离线场景等。meta server跟data server设计类似,唯一区别是meta server负责meta相关的kv请求,data server负责图算子的kv请求。 目前处于alpha版本,下半年会发布可用于生产环境的release版本。我理解金融反欺诈反洗钱领域对图数据库的需求很强烈,理由如下: 首先蚂蚁金服是国内最开始自研图数据库的厂商,后团队出来创业后,最先找到的合作对象是京东数科,京东有强烈需求,星云有技术和经验,二者是极佳的互补。未来的核心服务对象,我猜仍然会是银行,金融企业,保险行业,证券这些不缺钱的大金主。除BAT外的第二梯队和第三梯队的互联网公司会陆续参与到他们的社区来,开发,测试,修bug,提高口碑和知名度,甚至成为行业图数据库领域的标准产品(例如查询语言一旦成为国内标准,用户上了船,就很难下船了)。 暂时不支持事务,他们未来打算用实用的pecolator分布式事务协议,有缺点(最大问题在于时间戳服务限制了集群的更大拓展性)但简单实用,因为基于rocksdb很容易通过单行事务实现多行事务,同时实现了去中心化的锁冲突检测。 开源的方式很棒,选择的方向是风口,团队技术靠谱,又有风投加持,未来可期。 最后,个人一点理解: infra既是个技术活,又是个产品活。对用户来说,产品经理的作用仍更胜于工程师,希望他们能打磨出一款精致的infra产品。

June 29, 2019 · Zheng Hu

Further GC optimization for HBase3.x: Reading HFileBlock into offheap directly

In HBASE-21879, we redesigned the offheap read path: read the HFileBlock from HDFS to pooled offheap ByteBuffers directly, while before HBASE-21879 we just read the HFileBlock to heap which would still lead to high GC pressure. After few months of development and testing, all subtasks have been resovled now except the HBASE-21946 (It depends on HDFS-14483 and our HDFS teams are working on this, we expect the HDFS-14483 to be included in hadoop 2....

June 23, 2019 · Zheng Hu

从HBase offheap到Netty的内存管理

HBase的offheap现状 HBase作为一款流行的分布式NoSQL数据库,被各个公司大量应用,其中有很多业务场景,例如信息流和广告业务,对访问的吞吐和延迟要求都非常高。HBase2.0为了尽最大可能避免Java GC对其造成的性能影响,已经对读写两条核心路径做了offheap化,也就是对象的申请都直接向JVM offheap申请,而offheap分出来的内存都是不会被JVM GC的,需要用户自己显式地释放。在写路径上,客户端发过来的请求包都会被分配到offheap的内存区域,直到数据成功写入WAL日志和Memstore,其中维护Memstore的ConcurrentSkipListSet其实也不是直接存Cell数据,而是存Cell的引用,真实的内存数据被编码在MSLAB的多个Chunk内,这样比较便于管理offheap内存。类似地,在读路径上,先尝试去读BucketCache,Cache未命中时则去HFile中读对应的Block,这其中占用内存最多的BucketCache就放在offheap上,拿到Block后编码成Cell发送给用户,整个过程基本上都不涉及heap内对象申请。 但是在小米内部最近的性能测试结果中发现,100% Get的场景受Young GC的影响仍然比较严重,在HBASE-21879贴的两幅图中,可以非常明显的观察到Get操作的p999延迟跟G1 Young GC的耗时基本相同,都在100ms左右。按理说,在HBASE-11425之后,应该是所有的内存分配都是在offheap的,heap内应该几乎没有内存申请。但是,在仔细梳理代码后,发现从HFile中读Block的过程仍然是先拷贝到堆内去的,一直到BucketCache的WriterThread异步地把Block刷新到Offheap,堆内的DataBlock才释放。而磁盘型压测试验中,由于数据量大,Cache命中率并不高(~70%),所以会有大量的Block读取走磁盘IO,于是Heap内产生大量的年轻代对象,最终导致Young区GC压力上升。 消除Young GC直接的思路就是从HFile读DataBlock的时候,直接往Offheap上读。之前留下这个坑,主要是HDFS不支持ByteBuffer的Pread接口,当然后面开了HDFS-3246在跟进这个事情。但后面发现的一个问题就是:Rpc路径上读出来的DataBlock,进了BucketCache之后其实是先放到一个叫做RamCache的临时Map中,而且Block一旦进了这个Map就可以被其他的RPC给命中,所以当前RPC退出后并不能直接就把之前读出来的DataBlock给释放了,必须考虑RamCache是否也释放了。于是,就需要一种机制来跟踪一块内存是否同时不再被所有RPC路径和RamCache引用,只有在都不引用的情况下,才能释放内存。自然而言的想到用reference Count机制来跟踪ByteBuffer,后面发现其实Netty已经较完整地实现了这个东西,于是看了一下Netty的内存管理机制。 Netty内存管理概述 Netty作为一个高性能的基础框架,为了保证GC对性能的影响降到最低,做了大量的offheap化。而offheap的内存是程序员自己申请和释放,忘记释放或者提前释放都会造成内存泄露问题,所以一个好的内存管理器很重要。首先,什么样的内存分配器,才算一个是一个“好”的内存分配器: 高并发且线程安全。一般一个进程共享一个全局的内存分配器,得保证多线程并发申请释放既高效又不出问题。 高效的申请和释放内存,这个不用多说。 方便跟踪分配出去内存的生命周期和定位内存泄露问题。 高效的内存利用率。有些内存分配器分配到一定程度,虽然还空闲大量内存碎片,但却再也没法分出一个稍微大一点的内存来。所以需要通过更精细化的管理,实现更高的内存利用率。 尽量保证同一个对象在物理内存上存储的连续性。例如分配器当前已经无法分配出一块完整连续的70MB内存来,有些分配器可能会通过多个内存碎片拼接出一块70MB的内存,但其实合适的算法设计,可以保证更高的连续性,从而实现更高的内存访问效率。 为了优化多线程竞争申请内存带来额外开销,Netty的PooledByteBufAllocator默认为每个处理器初始化了一个内存池,多个线程通过Hash选择某个特定的内存池。这样即使是多处理器并发处理的情况下,每个处理器基本上能使用各自独立的内存池,从而缓解竞争导致的同步等待开销。 Netty的内存管理设计的比较精细。首先,将内存划分成一个个16MB的Chunk,每个Chunk又由2048个8KB的Page组成。这里需要提一下,对每一次内存申请,都将二进制对齐,例如需要申请150B的内存,则实际待申请的内存其实是256B,而且一个Page在未进Cache前(后续会讲到Cache)都只能被一次申请占用,也就是说一个Page内申请了256B的内存后,后面的请求也将不会在这个Page中申请,而是去找其他完全空闲的Page。有人可能会疑问,那这样岂不是内存利用率超低?因为一个8KB的Page被分配了256B之后,就再也分配了。其实不是,因为后面进了Cache后,还是可以分配出31个256B的ByteBuffer的。 多个Chunk又可以组成一个ChunkList,再根据Chunk内存占用比例(Chunk使用内存/16MB * 100%)划分成不同等级的ChunkList。例如,下图中根据内存使用比例不同,分成了6个不同等级的ChunkList,其中q050内的Chunk都是占用比例在[50,100)这个区间内。随着内存的不断分配,q050内的某个Chunk占用比例可能等于100,则该Chunk被挪到q075这个ChunkList中。因为内存一直在申请和释放,上面那个Chunk可能因某些对象释放后,导致内存占用比小于75,则又会被放回到q050这个ChunkList中;当然也有可能某次分配后,内存占用比例再次到达100,则会被挪到q100内。这样设计的一个好处在于,可以尽量让申请请求落在比较空闲的Chunk上,从而提高了内存分配的效率。 仍以上述为例,某对象A申请了150B内存,二进制对齐后实际申请了256B的内存。对象A释放后,对应申请的Page也就释放,Netty为了提高内存的使用效率,会把这些Page放到对应的Cache中,对象A申请的Page是按照256B来划分的,所以直接按上图所示,进入了一个叫做TinySubPagesCaches的缓冲池。这个缓冲池实际上是由多个队列组成,每个队列内代表Page划分的不同尺寸,例如queue->32B,表示这个队列中,缓存的都是按照32B来划分的Page,一旦有32B的申请请求,就直接去这个队列找 未占满的Page。这里,可以发现,队列中的同一个Page可以被多次申请,只是他们申请的内存大小都一样,这也就不存在之前说的内存占用率低的问题,反而占用率会比较高。 当然,Cache又按照Page内部划分量(称之为elemSizeOfPage,也就是一个Page内会划分成8KB/elemSizeOfPage个相等大小的小块)分成3个不同类型的Cache。对那些小于512B的申请请求,将尝试去TinySubPagesCaches中申请;对那些小于8KB的申请请求,将尝试去SmallSubPagesDirectCaches中申请;对那些小于16MB的申请请求,将尝试去NormalDirectCaches中申请。若对应的Cache中,不存在能用的内存,则直接去下面的6个ChunkList中找Chunk申请,当然这些Chunk有可能都被申请满了,那么只能向Offheap直接申请一个Chunk来满足需求了。 Chunk内部分配的连续性(cache coherence) 上文基本理清了Chunk之上内存申请的原理,总体来看,Netty的内存分配还是做的非常精细的,从算法上看,无论是 申请/释放效率 还是 内存利用率 都比较有保障。这里简单阐述一下Chunk内部如何分配内存。 一个问题就是:如果要在一个Chunk内申请32KB的内存,那么Chunk应该怎么分配Page才比较高效,同时用户的内存访问效率比较高? 一个简单的思路就是,把16MB的Chunk划分成2048个8KB的Page,然后用一个队列来维护这些Page。如果一个Page被用户申请,则从队列中出队;Page被用户释放,则重新入队。这样内存的分配和释放效率都非常高,都是O(1)的复杂度。但问题是,一个32KB对象会被分散在4个不连续的Page,用户的内存访问效率会受到影响。 Netty的Chunk内分配算法,则兼顾了 申请/释放效率 和 用户内存访问效率。提高用户内存访问效率的一种方式就是,无论用户申请多大的内存量,都让它落在一块连续的物理内存上,这种特性我们称之为 Cache coherence。 来看一下Netty的算法设计: 首先,16MB的Chunk分成2048个8KB的Page,这2048个Page正好可以组成一颗完全二叉树(类似堆数据结构),这颗完全二叉树可以用一个int[] map来维护。例如,map[1]就表示root,map[2]就表示root的左儿子,map[3]就表示root的右儿子,依次类推,map[2048]是第一个叶子节点,map[2049]是第二个叶子节点…,map[4095]是最后一个叶子节点。这2048个叶子节点,正好依次对应2048个Page。 这棵树的特点就是,任何一颗子树的所有Page都是在物理内存上连续的。所以,申请32KB的物理内存连续的操作,可以转变成找一颗正好有4个Page空闲的子树,这样就解决了用户内存访问效率的问题,保证了Cache Coherence特性。 但如何解决分配和释放的效率的问题呢? 思路其实不是特别难,但是Netty中用各种二进制优化之后,显的不那么容易理解。所以,我画了一副图。其本质就是,完全二叉树的每个节点id都维护一个map[id]值,这个值表示以id为根的子树上,按照层次遍历,第一个完全空闲子树对应根节点的深度。例如在step.3图中,id=2,层次遍历碰到的第一颗完全空闲子树是id=5为根的子树,它的深度为2,所以map[2]=2。 理解了map[id]这个概念之后,再看图其实就没有那么难理解了。图中画的是在一个64KB的chunk(由8个page组成,对应树最底层的8个叶子节点)上,依次分配8KB、32KB、16KB的维护流程。可以发现,无论是申请内存,还是释放内存,操作的复杂度都是log(N),N代表节点的个数。而在Netty中,N=2048,所以申请、释放内存的复杂度都可以认为是常数级别的。 通过上述算法,Netty同时保证了Chunk内部分配/申请多个Pages的高效和用户内存访问的高效。 引用计数和内存泄露检查 上文提到,HBase的ByteBuf也尝试采用引用计数来跟踪一块内存的生命周期,被引用一次则其refCount++,取消引用则refCount– ,一旦refCount=0则认为内存可以回收到内存池。思路很简单,只是需要考虑下线程安全的问题。 但事实上,即使有了引用计数,可能还是容易碰到忘记显式refCount– 的操作,Netty提供了一个叫做ResourceLeakDetector的跟踪器。在Enable状态下,任何分出去的ByteBuf都会进入这个跟踪器中,回收ByteBuf时则从跟踪器中删除。一旦发现某个时间点跟踪器内的ByteBuff总数太大,则认为存在内存泄露。开启这个功能必然会对性能有所影响,所以生产环境下都不开这个功能,只有在怀疑有内存泄露问题时开启用来定位问题用。 总结 Netty的内存管理其实做的很精细,对HBase的Offheap化设计有不少启发。目前HBase的内存分配器至少有3种: Rpc路径上offheap内存分配器。实现较为简单,以定长64KB为单位分配Page给对象,发现Offheap池无法分出来,则直接去Heap申请。 Memstore的MSLAB内存分配器,核心思路跟RPC内存分配器相差不大。应该可以合二为一。 BucketCache上的BucketAllocator。 就第1点和第2点而言,我觉得今后尝试改成用Netty的PooledByteBufAllocator应该问题不大,毕竟Netty在多核并发/内存利用率以及CacheCoherence上都做了不少优化。由于BucketCache既可以存内存,又可以存SSD磁盘,甚至HDD磁盘。所以BucketAllocator做了更高程度的抽象,维护的都是一个(offset,len)这样的二元组,Netty现有的接口并不能满足需求,所以估计暂时只能维持现状。 可以预期的是,HBase2.0性能必定是朝更好方向发展的,尤其是GC对P999的影响会越来越小。 参考资料 https://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919/ https://netty.io/wiki/reference-counted-objects.html

February 23, 2019 · Zheng Hu

HBaseConWest2018演讲 - HBase Practice In XiaoMi

HBaseConWest2018于6.18日在美国加州圣何塞举办,本次会议由Hortonworks承办。每年去美国硅谷参加HBaseConWest已经算是小米HBase团队的惯例了,一方面小米团队在HBase社区的影响力有目共睹,目前已经培养了7位HBase Committer,其中有2位HBase PMC;另外一方面,小米内部也很乐意对外去分享公司一年所做的工作,相当于把一年的工作(包括内部的实践以及社区贡献)做一个年度总结分享给大家。 所以,2018年我们也很积极的提交了演讲议题(HBase Practice In XiaoMi),并花了很多精力整理总结,内部还做过3次英文试讲。但遗憾的是,今年中美关系比较紧张,美国签证没有如期办下来。按照组内历年的经验,一般提前一个月左右办理签证,能很顺利办下来。今年我们在5.14日去大使馆面试申请签证,被要求填写补充材料,在5.16拿到承办方的visa letter并提交补充材料之后,一直到现在签证尚未发放。本想没办法去现场的话,就只能把我们这个议题提交到8.17日的HBaseConAsia去讲。写邮件跟组委会沟通,组委会之前把我们talk的优先级放的比较高,也比较喜欢我们演讲内容,所以后面就想让我们做一个远程分享。为了以防万一设备异常之类的,就先让我们准备一个视频,有任何异常的话,直接放视频也不慌。于是,我们就录了一个,发现视频效果还行(主要是可以做剪辑,哈哈),就跟组委会说,现场干脆直接用视频好了,有任何疑问的话,远程答疑就好。 于是,最后在HBaseConWest2018上看到的就是以下PPT和视频了。演讲内容主要分两部分,第一部分小米内部实践,由我的同事田竞云来分享,第二部分复制功能改进,由我来分享。 PPT Video 总体来说,没有机会去HBaseConWest2018现场分享这个事情,个人还是挺遗憾的。之前Hortonworks的Ted Yu和Pinterest的TianYing获知我们要去美国分享,都很积极的约了我们聚会,最后也只能取消。原定的去美国一些其他行程,也只得取消。有一点值得欣慰的是,在组委会和我们的共同努力下,总算是有机会把小米过去一年做的一些工作整理并呈现给大家,包括美国HBase社区的朋友们。感谢组委会和社区,也感谢铎神和小豪在试讲中提出的很多宝贵建议。

June 18, 2018 · Zheng Hu

成为HBase Committer

我于10月20号,接受Apache HBase社区邀请,成为HBase Committer。 由于我在小米就是专门负责维护内部HBase分支以及线上集群,再加上之前小米已经有6位HBase Committer,其中一位PMC(项目委员会成员), 所以在这样的环境之下,成为Committer其实是一件特别顺理成章的事情,并没有特别值得骄傲的地方。相比一个在公司做HBase方向但是公司缺乏HBase Committer的同学来说,成为HBase Committer需要付出更多的时间和努力。 下面来谈谈我对社区的一些观察: 首先HBase的PMC成员大部分都是极其活跃的,活跃到什么程度呢,就是一年365天,基本上每天都在为社区贡献,甚至度假的时候只要能连上网,他们也在不遗余力的回复JIRA和邮件。 当然对大部分PMC而言,为HBase社区贡献并推动社区的进步,跟他们所在公司的目标是一致的,但从日活跃时长以及一年的活跃天数来看,他们相比普通的敬业码农,却都称得上是不折不扣的工作狂。 Apache相关社区具有提升机制,例如当一个Contributor提及的代码超过一定量时,就会有PMC成员推荐这位Contributor去当Committer,当一个Committer的贡献达到一定程度的时候,又会被PMC推荐加入项目管理委员会,也就是PMC。同时,Committer和PMC在业界都是能得到广泛认可的,无论从个人职业层面,还是从项目发展方面,这都是一个很好的机制。而Github上的大部分开源项目可能都没有类似的提升机制,所以一个Contributor可能贡献了很多代码,但还是Contributor,这很可能会打消积极性。 HBase社区的代码贡献者来自全球的各个地方,有的人在美国,有的人在印度,有的人在中国。各位贡献者分布在不同的时区内,所以跟进一个问题,可能是今天中国人说了一句话,等到明天美国人才能回句话,接着印度人又提了一些意见,最后中国人觉得不错可以做就开始做了。整个任务的跟进时间可能特别长,所以,做社区的事情一定要有长期跟进的准备。有一些同学跟我聊过,说在公司里面跟进社区问题会不会很耽误时间,其实具体每一天来说,并不需要花特别多的时间用来跟进社区,我工作时间一般还是好好在公司搬砖,下班后会花一些时间用来写社区代码之类的,反正社区跟进也比较慢。 公司和社区关系。小米相对来说比较开放一点,公司管理层面非常鼓励员工积极参与到开源社区,HBase就不用多说,除了HBase之外,贵米也自主研发并开源的一些项目,例如分布式KV存储Pegasus,业界知名的监控系统Falcon等等。这其实是一个好现象,一方面能为公司塑造较好的技术品牌,另外一方面,开源能激发码农的工作积极性,因为码农有一个很好的平台和世界上该领域最优秀的程序员们一起合作,精神层面能到极大的满足。相对应的问题就是社区做的这些工作是否对公司有用,如果是fix bug,必然有用。如果是用不上的新功能,这个确实没有必要花太多精力,因为你觉得用不上的,别人也会觉得用不上。所以,修复Bug加上开发有用的Feature,才是正道。

October 22, 2017 · Zheng Hu

HBase Region Balance实践

HBase是一种支持自动负载均衡的分布式KV数据库,在开启balance的开关(balance_switch)后,HBase的HMaster进程会自动根据 指定策略 挑选出一些Region,并将这些Region分配给负载比较低的RegionServer上。官方目前支持两种挑选Region的策略,一种叫做DefaultLoadBalancer,另一种叫做StochasticLoadBalancer,这两种策略后面会具体讲到。由于HBase的所有数据(包括HLog/Meta/HStoreFile等)都是写入到HDFS文件系统中的, 因此HBase的Region移动其实非常轻量级。在做Region移动的时候,保持这个Region对应的HDFS文件位置不变,只需要将Region的Meta数据分配到相关的RegionServer即可,整个Region移动的过程取决于RegionClose以及RegionOpen的耗时,这个时间一般都很短。 本文来讲讲hbase的balance实现。 Balance的流程 首先通过LoadBalancer找出所有需要移动的region plan,一个region plan包括region/原始RegionServer/目的RegionServer三个属性。 unassign region , 将region从原来的RegionServer上解除绑定; assign region ,将region绑定到目标RegionServer上; 其中, unassign region的具体流程为: create zk closing node . 该节点在/unassigned路径下, 包含(znode状态,region名字,原始RS名,payload)这些数据。 hmaster 调用rpc服务关闭region server。region-close的流程大致为先获取region的writeLock , 然后flush memstore, 再并发关闭该region下的所有的store file文件(注意一个region有多个store,每个store又有多个store file , 所以可以实现并发close store file) 。最后释放region的writeLock. 设置zk closing node的znode状态为closed. assgin region的具体流程为: 获取到对应的Region Plan. HMaster调用rpc服务去Region Plan对应的RegionServer上open region. 这里会先更新/unassigned节点为opening. 然后并发Load HStore,再更行zk/ROOT/META表信息,这里是为了client下次能获取到正确的路由信息, 最后更新region状态为OPEN. DefaultLoadBalancer策略 这种策略能够保证每个RS的regions个数基本上都相等,确切来说,假设一共有n个RS,第i个RS有Ai个region,记average=sigma(Ai)/n , 那么这种策略能够保证所有的RS的region个数都在[floor(average), ceil(average)]之间。这种策略的实现简单,应用广泛。 但是,这种策略考虑的因素比较单一, 没有考虑到每台region server的读写qps/负载压力等等,这样就可能导致出现一种情况:虽然每个region server的regions都非常接近,但是90%的请求还是落在了一台RS上,因为这台RS上的region全部都是热点数据,这样还是没有达到负载均衡的目的。 但我觉得balance的首要目的是保证数据均衡,如果在数据均衡的情况下,负载还是集中,这时候就要考虑下rowKey的选择是否有问题了。因此, 我个人还是比较推荐采用DefaultLoadBalancer的。 StochasticLoadBalancer策略 StochasticLoadBalancer 这种策略真的是非常复杂,简单来讲,是一种综合权衡一下6个因素的均衡策略: 每台RegionServer读请求数(ReadRequestCostFunction) 每台RegionServer写请求数(WriteRequestCostFunction) 每台RegionServer的Region个数(RegionCountSkewCostFunction) 移动代价(MoveCostFunction) 数据locality(TableSkewCostFunction) 每张表占据RegionServer中region个数上限(LocalityCostFunction) 对于cluster的每一种region分布, 采用6个因素加权的方式算出一个代价值,这个代价值就用来评估当前region分布是否均衡,越均衡则代价值越低。然后通过成千上万次随机迭代来找到一组RegionMove的序列,使得最终的代价值严格递减。 得到的这一组RegionMove就是HMaster最终执行的region迁移方案。...

June 28, 2017 · Zheng Hu

HBaseCon West 2017 Session解读

HBaseCon West 2017的PPT解读如下: 1. HBase at Xiaomi 由小米的杨哲和张洸濠合作分享,两位是2016年新晋升的HBase Committer (ps: 小米目前总共产生了8位HBase Committer,其中2位HBase PMC,解决了数百个issue). 分享的一些亮点主要有: 1. 0.94升级到0.98集群的一些经验。 2. 小米内部HBase使用g1gc的一些经验。 3. 2016年小米对社区做的一些开发和改进,包括但不限于顺序推送复制日志/优化Scan操作/开发异步客户端功能以及相关测试结果,等等。 2. Apache HBase at DiDi (by Kang Yuan) 主要分享了HBase在滴滴的一些实践经验,目前滴滴的HBase是基于0.98.21版本,然后将rsgroup这个功能迁移到了自己的分支,用来做业务隔离。另外,PPT中也提到通过将地理位置坐标进行GeoHash转换成一维byte存放到HBase中,可以解决查询一个点周边坐标的问题。 3. Accordion: HBase Breathes with In-Memory Compaction (From Yahoo) 有了InMemory-Compaction功能之后,HBase支持将Memstore直接Flush成一个ImmutableSegment,这个ImmutableSegment其实是一块内存空间,多次Memstore的Flush操作会导致产生多个ImmutableSegment,特定条件下,多个ImmtableSegment会进行In-Memory的Compaction,也就是多个ImmutableSegment完全在内存中合并成为一个大的ImmutableSegment(其中BASIC类型的InMemoryCompaction会保留所有数据,EAGER类型的InMemoryCompaction会清理冗余版本数据)。最终,这个大的ImmutableSegment还是要Flush到磁盘的,然后接着触发做磁盘上的Compaction操作。 按照设计文档以及PPT的说明,InMemory-Compaction有以下好处: 由于InMemoryCompaction会在内存中进行compaction, 不用频繁的Flush Memstore到Disk(Flush次数太多会造成storefile个数增长, storefile的增长会导致读性能严重下降), 从而可以降低读操作延迟。 ImmtableSegment今后可能会和HFile的layout保持一致,这样Flush的速度将大幅提升。 对于行数据频繁更新的场景,InMemory-Compaction可以采用EAGER方式在内存中就清理掉冗余版本数据,节省了这部分数据落盘的代价。 最后,PPT测试数据也确实说明使用InMemoryCompaction后,写吞吐有较大幅度提升,读延迟有较大幅度下降。 ps. In-memory Compaction由stack等6位成员共同完成(将在HBase2.0的release版本发布),这其中有两位美女工程师(PPT中的照片证明颜值确实很高),现在都已经是HBase的Committer了。 另外,In-memory compaction详细设计文档请参考:https://issues.apache.org/jira/browse/HBASE-13408 4. Efficient and portable data processing with Apache Beam and HBase (By Google) 这个演讲更多是来HBaseCon宣传下Apache Beam这个项目。 Apache Beam这个项目始于2016年2月份,近1年多的时间内就收到了来自全球178个贡献者的8600+提交,主要是希望提供一个统一的API用来同时处理Batch任务和Streaming任务,他的API后端可以接Apex/Flink/Spark/GoogleCloudDataFlow等服务,同时提供Java和Python的客户端SDK。这个东西就好比JDBC一样,提供了一个统一的借口,后端可以连接MySQL/Oracle/Postgresql/SQLServer等关系型数据库。我没理解错的话,这个东西应该是可以用来在HBase/MongoDB/HDFS/Cassandra/Kafka/BigTable/Spanner/Elasticsearch/GridFS/Hive/AMQP等(超过20种通用的存储服务)各种服务间实现数据transform。...

June 28, 2017 · Zheng Hu

HBase回放Hlog顺序不一致的问题

在HBase的主从复制集群中, 如下图左所示,Region-Server-X以及Region-Server-Y是master集群中的两个Region-Server。正常情况下, 对Region-A的写入会在Region-Server-X上append log 到Hlog-X,然后Region-Server-X会异步地将该部分Hlog批量地应用(apply)到slave-cluster中。 此时,若Region-Server-X发生了宕机,那么Region-A会被Region-Server-Y托管,之后业务开始写Region-A导致append log到Hlog-Y日志, 同时 Region-Server-Y会新开一个线程去执行Hlog-X的replay任务,这样就会出现Region-A的Hlog-X以及Hlog-Y同时写入slave-cluster的情况出现。也就是说, Region-A的Hlog在slave cluster中的回放顺序错乱。 另外,Region Move也会产生类似的问题,即两个Region Server并发回放Hlog导致回放顺序错乱。 当Region-A的Hlog日志回放顺序错乱时,会导致主从集群最终数据不一致的问题:在master cluster上,先执行put操作,然后执行delete操作(delete操作是为了删除put的记录),在slave cluster中回放的顺序可能变成先执行delete 操作, 再执行put操作。如果在delete操作之后put操作之前,slave cluster的region-server 做了一次major compaction, 那么会导致出现put的数据没有被删除的情况。 另外,日志回放数据错乱还可能会导致slave-cluster数据处于一个从未在maser-cluster上出现过的状态。 该问题现在依然存在于hbase的系统中,社区也对此进行过多次讨论,但现在依然没有解决。下面简单阐述小米HBase团队提出的一种解决思路: 把Region-A 从Region-Server-X上move 到Region-Server-Y,让Region-Server-Y托管Region-A。此时Region-A可读可写,但Region-A产生的Hlog-Y并不会立刻推送到slave-cluster上; 某个Region-Server将Hlog-X中的Region-A的日志推送到slave-cluster; 等待Hlog-X中Region-A的日志全部回放到slave-cluster之后,Region-Server-Y开始推送HLog-Y日志到slave-cluster上。 这里,还需要考虑一种比较极端的情况。如下图所示,有X , Y , Z 三个Region-Server, 其中X负责A/B/C 3个region, Y负责D/E 2 个region, Z 负责F 1个region。若此时X发生crash, A/B 2个region被迁往Z,C这个region被迁往Y,之后Y开始接收Region-C的读写请求,在C写了一小段数据之后,Y这个Region-Server又发生了宕机,于是D/E/C 3个region全部都被迁移到Z。 对于Region-C而言,为了保证Hlog推送到slave-cluster严格有序,必须先推送Hlog-X中的日志,然后再推送Hlog-Y中的日志,最后推送Hlog-Z的日志。对于Region-Server宕机的这种情况而言,由于宕机的Hlog并不会增长,因此依次回放Hlog-X/Hlog-Y中所有的日志即可;但对于Region迁移这种情况(仍以上图为例,假设Region-C从X迁移到Y),Region-Sever-X的Hlog-X会不断增长,因此在Region-Server-Y托管Region-C时需要记录下Region-C在Hlog-X中的MaxSequenceId,当回放的HLog-X的seqId>=MaxSequenceId时,就可以开始回放Hlog-Y的日志了。 因此,为了统一处理,无论region failover 还是region move,需要做以下记录: Y从X上托管Region-C时,记录Hlog-X的MaxSequenceId到HBase的meta表中; 在Z从Y上托管Region-C时,记录Hlog-Y的MaxSequenceId到HBase的meta表中。 …… 极端情况下,一个Region在多个Region-Server之间做多次迁移,会形成一条MaxSequenceId链表,为了保证改Region的Hlog回放顺序严格一致,必须依序回放各个Region-Server对应的Hlog,直到该段Hlog回放到对应的MaxSequenceId,接着回放下一段hlog。这样就能保证HLog在Region Move/Region Server Failover时,Region的回放顺序严格一致了。 目前该方案的设计方案已经发到社区了,小米HBASE团队也将对此进行修复。对该问题的更多相关讨论可以参考HBASE-9465。 总结 本文介绍了一种保证Hlog回放顺序严格一致的方案,可以解决master-cluster和slave-cluster数据不一致的问题。该方案可能会导致region在迁移过程中master-cluster和slave-cluster复制延迟增大(新方案必须严格等待上一段hlog回放,才能回放下一段hlog),整个过程相当于牺牲了主备集群replication的及时性,换来主从集群间数据最终一致性。 参考资料 https://issues.apache.org/jira/browse/HBASE-9465 https://issues.apache.org/jira/browse/ACCUMULO-2931 https://issues.apache.org/jira/browse/HBASE-10275 https://hbase.apache.org/0.94/replication.html

June 13, 2016 · Zheng Hu

TokuDB的多版本并发控制(MVCC)

本文讲讲TokuDB事务的隔离性,在源码实现中,比较复杂。为了便于描述,本文只对最关键的内容进行描述,对细节的东西略过。 背景介绍 在传统的关系型数据库(例如Oracle, MySQL, SQLServer)中,事务可以说是研发和讨论最核心内容。而事务最核心的性质就是ACID。 其中 A表示事务的原子性,也就是组成事务的所有子任务只有两种结果: 要么随着事务的提交,所有子任务都成功执行;要么随着事务的回滚,所有子任务都撤销。 C表示一致性,也就是无论事务提交或者回滚,都不能破坏数据的一致性约束,这些一致性约束包括键值唯一约束、键值关联关系约束等。I表示隔离性,隔离性一般是针对多个并发事务而言的,也就是在同一个时间点,t1事务和t2事务读取的数据应该是隔离的,这两个事务就好像进了同一酒店的两间房间一样,各自在各自的房间里面活动,他们相互之间并不能看到各自在干嘛。D表示持久性,这个性质保证了一个事务一旦承诺用户成功提交,那么即便是后继数据库进程crash或者操作系统crash,只要磁盘数据没坏,那么下次启动数据库后,这个事务的执行结果仍然可以读取到。 TokuDB目前完全支持事务的ACID。 从实现上看, 由于TokuDB采用的分形树作为索引,而InnoDB采用B+树作为索引结构,因而TokuDB在事务的实现上和InnoDB有很大不同。 本文主要讲讲TokuDB的事务隔离性的实现,也就是常提到的多版本并发控制(MVCC)。在InnoDB中, 设计了redo和undo两种日志,redo存放页的物理修改日志,用来保证事务的持久性; undo存放事务的逻辑修改日志,它实际存放了一条记录在多个并发事务下的多个版本,用来实现事务的隔离性(MVCC)和回滚操作。 由于TokuDB的分形树采用消息传递的方式来做增删改更新操作,一条消息就是事务对该记录修改的一个版本,因此,在TokuDB源码实现中,并没有额外的undo-log的概念和实现,取而代之的是一条记录多条消息的管理机制。虽然一条记录多条消息的方式可以实现事务的MVCC,却无法解决事务回滚的问题,因此TokuDB额外设计了tokudb.rollback这个日志文件来做帮助实现事务回滚。 TokuDB的事务表示 在tokudb中, 在用户执行的一个事务,具体到存储引擎层面会被拆开成许多个小事务(这种小事务记为txn)。 例如用户执行这样一个事务: begin; insert into hello set id = 1, value = '1'; commit; 对应到TokuDB存储引擎的redo-log中的记录为: xbegin 'b': lsn=236599 xid=15,0 parentxid=0,0 crc=29e4d0a1 len=53 xbegin 'b': lsn=236600 xid=15,1 parentxid=15,0 crc=282cb1a1 len=53 enq_insert 'I': lsn=236601 filenum=13 xid=15,1 key={...} value={...} crc=a42128e5 len=58 xcommit 'C': lsn=236602 xid=15,1 crc=ec9bba3d len=37 xprepare 'P': lsn=236603 xid=15,0 xa_xid={...} crc=db091de4 len=67 xcommit 'C': lsn=236604 xid=15,0 crc=ec997b3d len=37 对应的事务树如下图所示:...

December 13, 2015 · Zheng Hu

TokuDB的索引结构:分形树的实现

本文从工程实现角度解析TokuDB的索引结构--分形树。 详细描述了ft-index的磁盘存储结构,ft-index如何实现Point-Query, Range-Query, Insert/Delete/Update操作, 并在描述过程中,试图从各个角度和InnoDB的B+树做详细对比。 分形树简介 分形树是一种写优化的磁盘索引数据结构。 在一般情况下, 分形树的写操作(Insert/Update/Delete)性能比较好,同时它还能保证读操作近似于B+树的读性能。据Percona公司测试结果显示, TokuDB分形树的写性能优于InnoDB的B+树), 读性能略低于B+树。 类似的索引结构还有LSM-Tree, 但是LSM-Tree的写性能远优于读性能。 工业界实现分形树最重要的产品就是Tokutek公司开发的ft-index(Fractal Tree Index)键值对存储引擎。这个项目自2007年开始研发,一直到2013年开源,代码目前托管在Github上。开源协议采用 GNU General Public License授权。 Tokutek公司为了充分发挥ft-index存储引擎的威力,基于K-V存储引擎之上,实现了MySQL存储引擎插件提供所有API接口,用来作为MySQL的存储引擎, 这个项目称之为TokuDB, 同时还实现了MongoDB存储引擎的API接口,这个项目称之为TokuMX。在2015年4月14日, Percona公司宣布收购Tokutek公司, ft-index/TokuDB/TokuMX这一系列产品被纳入Percona公司的麾下。自此, Percona公司宣称自己成为第一家同时提供MySQL和MongoDB软件及解决方案的技术厂商。 本文主要讨论的是TokuDB的ft-index。 ft-index相比B+树的几个重要特点有: 从理论复杂度和测试性能两个角度上看, ft-index的Insert/Delete/Update操作性能优于B+树。 但是读操作性能低于B+树。 ft-index采用更大的索引页和数据页(ft-index默认为4M, InnoDB默认为16K), 这使得ft-index的数据页和索引页的压缩比更高。也就是说,在打开索引页和数据页压缩的情况下,插入等量的数据, ft-index占用的存储空间更少。 ft-index支持在线修改DDL (Hot Schema Change)。 简单来讲,就是在做DDL操作的同时(例如添加索引),用户依然可以执行写入操作, 这个特点是ft-index树形结构天然支持的。 由于篇幅限制,本文并不对Hot Schema Change的实现做具体描述。 此外, ft-index还支持事务(ACID)以及事务的MVCC(Multiple Version Cocurrency Control 多版本并发控制), 支持崩溃恢复。 正因为上述特点, Percona公司宣称TokuDB一方面带给客户极大的性能提升, 另一方面还降低了客户的存储使用成本。 ft-index的磁盘存储结构 ft-index的索引结构图如下(在这里为了方便描述和理解,我对ft-index的二进制存储做了一定程度简化和抽象, 具体的二进制存储格式可以参考我的博客): 在下图中, 灰色区域表示ft-index分形树的一个页,绿色区域表示一个键值,两格绿色区域之间表示一个儿子指针。 BlockNum表示儿子指针指向的页的偏移量。Fanout表示分形树的扇出,也就是儿子指针的个数。 NodeSize表示一个页占用的字节数。NonLeafNode表示当前页是一个非叶子节点,LeafNode表示当前页是一个叶子节点,叶子节点是最底层的存放Key-value键值对的节点, 非叶子节点不存放value。 Heigth表示树的高度, 根节点的高度为3, 根节点下一层节点的高度为2, 最底层叶子节点的高度为1。Depth表示树的深度,根节点的深度为0, 根节点的下一层节点深度为1。 分形树的树形结构非常类似于B+树, 它的树形结构由若干个节点组成(我们称之为Node或者Block,在InnoDB中,我们称之为Page或者页)。 每个节点由一组有序的键值组成。假设一个节点的键值序列为[3, 8], 那么这个键值将(-00, +00)整个区间划分为(-00, 3), [3, 8), [8, +00) 这样3个区间, 每一个区间就对应着一个儿子指针(Child指针)。 在B+树中, Child指针一般指向一个页, 而在分形树中,每一个Child指针除了需要指向一个Node的地址(BlockNum)之外,还会带有一个Message Buffer (msg_buffer), 这个Message Buffer 是一个先进先出(FIFO)的队列,用来存放Insert/Delete/Update/HotSchemaChange这样的更新操作。...

November 25, 2015 · Zheng Hu