开发者学堂课程【HBase 入门与实战:基于 HBase 快速构架海量订单存储系统】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/808/detail/13896
基于 HBase 快速构架海量订单存储系统
内容介绍:
一、原理:数据分布策略
二、物流详情管理系统设计
一、原理:数据分布策略
1、扩展性与数据分布问题
为什么会有数据分布问题?
目标:
·线性扩展:系统可以靠加盘/加机器进行线性扩容,加多少得多少
·负载均衡:扩缩容后,依然能达到负载均衡
·可靠性:多副本
在过去都是用单机的数据库系统或者用大型机,随着整个行业的发展,数据越来越多,单机或者大型机已经存不下,需要一个多机或者多盘的方式来管理更多的数据,这里面有两种扩展方式,一种是 scale up,一种 scale out,scale up 是提高硬件的配置,比如有 1T 的盘变成 4T 的盘,8T 的盘,未来可能是更大的盘,但这种方式很容易遇到硬件的瓶颈,单盘不可能做到很大,除非技术有里程碑式的突破,现在业界比较常用的方式是 scale out 方式,一块盘不够用多块盘,一台机器可能连十几块盘,甚至几十块盘,不够的话弄更多的机器,如果更多的机器连成一个集群不够弄更多的机房,可以很快很容易地扩展到一个非常大的规模。
所以这就是实现线性扩展的方式,系统可以通过加盘加机器来扩充存储容量,同时也可以通过这种方式来扩展计算能力,是加多少就有多少。
另外还需要系统具有负载均衡的能力,当进行扩容或者缩容之后,整个系统的数据与它的请求,在系统之间是一个均匀的负载情况。
最后可以通过这个分部来解决可靠性的问题,常见的多副本问题,但是副本不在今天的主题范围内,聚焦在前面两个目标。
2、HBase 的顺序分布策略
(1)HBase 的数据分布策略:顺序分布
·唯一主键,rowkey = user_id+ts
·一个表按 rowkey 被拆分成多个 region,最少1个
·region 边界构成"路由表",请求据此进行路由
·region 所在的 RS 可以手工分配,默认随机分配
·region 边界可修改,可以 split/merge·region 边界可能是不完整的 rowkey:如2
·具有相同前缀的 row 可能分到不同的 region,如 3
·HBase 没有 partition key
目前有两种比较常用的方案,一种是顺序分布, HBase 采用方式,一种是一致性哈希的服务,像单独的 DB 产品的分布,先看一下 HBase 顺序分布。这个例子是有一个表,有两个主键,一个 user_id
有一个时间戳以及在时间戳上面 user 在哪个位置。这个数据是伪造的,对于任何一个用户来讲,user_id 加上时间戳构成了唯一的主键,比如 RowKey 是 user_id 时间戳。对于 HBase 的数据库来讲,先抛开数据分布本身,数据自己的存储是按照 use_id 顺序往下排的,对于 use_id 相同的时候按照 ts 顺序来排的,对于 HBase Region
分布来说可能把这个表按照整个rock的分布拆成三个 Region ,
比如可能1到300这个地方拆一下,然后300到500这个地方拆一下,产生了三个 region ,在 HBase 侧面在管理的时候,Region边界并不是一三百或者二五十是这样的东西,可能是一个虚拟的rowkey ,比如右边小表第一个 region 开始的时候是负无穷,也是任何一个小于二的 rowkey 都是分在第一个位置里面,rowkey 2 在整个表里面是不存在的,是一个虚拟的编辑,只是用来做参与比较的,所有比二大的 use_id 包括二自己都会分到第二个位置里面。对于等于三的数据出现的情况是 use_id 等于三的数据被分到了两个region 里面,里面的一个 region 编辑其实是把 region 的前缀打散了,第三个 region 的时候其实300到600然后到正无穷大,把通过 region 的边界,把整个 rock 的值域全部覆盖到了,是从负无穷到正无穷,中间比如二和三六百这些点,其实是可以人工指定的。这个表把它称为一个路由表,在 HBase 里面my ta 表,就是 HBase 冒号 myta 这个表,所有基于对 HBase 的请求,都会先查这个表找到要请求的 region 然后根据 region 所在的机器,把请求放到机器里面去,路由表在客户端也有相应的缓存来提高定位的速度,对于一个 region 能分到哪个机器在建表的时候是随机分布的。可以保证表里面的 region 能够在整个集群范围内获得比较好的随机分布。
同时因为有路由表的存在,所以可以通过人工接入的方式来改变 region 所在的机器,有时候可以把后面的三个 info 改掉,好处是 region 分布可以非常的灵活。
比如当出现热点的时候,可以手动把它挪到另外一个空闲的机器里面,可以单独扩一台机器进来,把位置挪过去,可以很容易很从容地来处理现场突发的一些状况,也就是 region 可以通过人工干预来做分配,同时 region 编辑也是可以修改的,比如可以把 region 按照一两百 rock 再拆成两个,可以再拆再拆,同时可以对它们进行合并,比如 region 太多的时候可以把它合掉。
注意一点 HBase 没有 partition key 的概念,对于一致性哈希的分布来讲都会认为 user_id 列能是一个分区列,或者它比较适合作为一个分区键,但是对于 HBase 是不看分区键的,因为 HBase 的 region 分布及请求路由全部都是基于整个或者基于完整的 rock 来进行的,比如会继续三四百的一个整个的 rock 来对它进行路由和拆分。
所以 HBase 是没有 partition key 的概念,或者可以认为 partition key 在 HBase 里面是个逻辑,并不是一个物理概念,比如认为 user_id 是 partition key 的时候具有相同前缀的 user_id 比如一可能逻辑上一定会在一起,但是它在物理上可能不一定分在一起。
比如三内容,可能分到两个 region 里去,但是逻辑上它们一定是挨在一起的,比如三五百读完之后读到下一行数据,一定是三六百。
(2)顺序分布策略分析
理想情况下,或我们希望做到线性扩展:
·每个机器服务的 region 数量大致相等
·单表 region 数>机器数
负载均衡:
·每个 region 的数据量大致相同
·每个 region 产生的负载大致相同(吞吐)
·region 分裂/合并后依然满足上述约束
业务视角的HBase表的设计目标:
数据和请求,在 region 之间均匀/随机分布
负载均衡进行拓展从理想的情况来看,希望 HBase 的每一个region 都差不多一样大,就是它们的数据量搭配是相同的,同时表的所有的 region 期待它能在整个机器里面均匀的分布,也是每台机器服务的位置数量大概要相同,这样的话可以期待每台机器或者它的吞吐应该是大致相同的,同时希望在 region 发生分裂或者合并以后,以及机器发生扩容或者缩容以后,整个集群仍然处于一个负载均衡的状态,是一个最里理想的状态。事实上大部分的时候系统会处于一种近似负载均衡的状态,如果业务有一些突发情况,比如大簇很可能会打破这种状态,需要人工介入。为了保证系统大部分时候都处于一个负载均衡,需要对整个系统做很好的设计,设计的目标是希望数据和请求在 region 之间进行随机的均匀分布。
(3)顺序分布的常见问题
用户视角:
数据倾斜:user_id 一般都是从1开始分配,单调递增,新用户数据都集中在表尾的 region
访问倾斜:user_id 越大越热,user_id 越小越冷。随着业务发展,表尾 region 的负载会显著高于表头 region
超大用户:个别用户的数据量和请求可能显著大于其他用户,导致局部热点
HBase 顺序分布的策略有一些缺陷叫tread off。列举了几点比如数据清洁,从现在这个例子来看案例user_id 场景特别容易出现数据清洁。
user_id 往往是从1开始分配的,不是随机数。是12345678由小到大递增的分布。对于顺序分布的表来说,当产生一些新的userid 的时候往往产生在表尾位置,大的 user_id 都是产生在表尾,对于一个会跑很久的系统来说,小的 user_id 都比较冷,而大的user_id 都比较热,也会产生访问的情景表尾会比表头更热,对于数据来讲,表尾比表头数据更多,这是一个因为 user_id 分布本身的顺序性导致的数据倾斜以及访问倾斜,如果有一些 user_id 特别大,比如像 user_id 等于三这样的数据,有三行是整个表里面最大的也是三行,如果 user_id 特别多,比如占了整个表数据分布的50%以上,就属于一个超大的用户,超大用户对于整个数据分布来讲,也会产生一个倾斜,这种数据分布都需要特别的处理,比如能够把它识别出来单独放到一张表里,或者对它再次进行二次的拆分策略,来解决局部的热点问题。
二、物流详情管理系统设计
1、需求定义
电商物流订单,每个订单会经历多轮中转最后达到用户手中。每一次中转会产生一个事件,比如已揽收、装车、到达xx中转站、派送中、已签收。构建一个系统记录全网所有物流订单的状态变化,为用户提供订变更记录的查询服务。
功能需求(业务场景抽象):
存储所有物流订单的状态变化信息。
查询指定物流订单。
所有状态变化数据,按时间倒序排列。
最近一条状态数据
非功能需求:
·海量订单数据(上百亿),系统容量/吞吐可线性扩展
·高并发,高性能
对物流订单详情来说,当下单之后可以看这个订单的派送情况,比如是不是已经发货了,到哪了,运输当中可能经历过一系列的这个特点,最后会派送签收,它会有一个多次的变更的状态,物流订单详情的存储系统就要记录全网所有物流订单的状态变化,把每一行的信息记下来,而且全网所有的订单全部都填了,同时还要提供订单的变更信息的查询,比如打开手淘要看订单到哪儿了,就是这样的查询的服务,需要能查一个订单所有的变化状态或者查订单最近一条的状态,目前有三个需求,但是是跟物流详情业务实际的需求是差很多的,但是为了方便理解把它抽象成只有三个需求,除了功能上的这个需求之外,还有一些其他的非功能需求,比如全网的数据量其实是非常大的,有上百亿的订单量,整个系统的吞吐容量能够进行扩容,同时在这么大的数据体量下面,系统要能够保证极高的并发写入以及高并发的查询,而且写入和查询都要能够巨量速度很快。
2、schema 设计
(1)第一版表设计
思路:根据查询需求反推表设计
rowkey = orderld + ts(秒)
value:该订单在该时间戳的变更详情
一个物流订单的一次状态变化作为一行,通过 orderld 前缀可以读到该订单的所有数据
假设 orderld 加上时间戳后作为主键,然后跟时间戳上面 ID 的状态变化,也就是根据查询来反推表格设计,得到一个很直接的设计。这个设计有个特点是一个订单的每一行每一次状态变化代表里面是做一行来存储的,这个叫做高表的设计,还有另外一种设计是一个订单的所有变化存在一行里面, detail 本身可能每一列它有不同的列名,就是用做它的列名,然后它的 value 是这个的一个状态。
对于这个 detail 只有一列就只有这一个信息要存的话,这种宽表的设计是可以的,事实上还需要存一些其他的信息,作为举例把其他的例都隐掉了,但事实上作为一个生产性能来讲,需要能够扩展其他的例子,比如这一次人员是谁,是不是有一些其他的信息需要记录,都需要扩展到这一行的后面当中,所以这里面采用高档的设计,也是生产中实际使用的设计。
(2)第一版表设计:数据分布解析
物流订单 ID 不具有全局随机性,容易导致数据倾斜或局部热点。当全表有数百亿订单时,微小的不均衡都会被放大为热点,阻碍系统的线性扩展
·解决方案:让 Orderld 具有全局随机性,加 Hash rowkey=hash +orderld+ts
·hash=md5(orderld).substr(8) (4-8均可,按需选择)
假设新增了两行数据啊,一个是111订单的事件记录以及333新订单的记录,可以看到从这个图上看不出来什么,但是当仔细分析这个系统的时候,发现这个订单的案例其实不具有全局随机性,也就是它跟之前 user_id 有差不多的问题, user_id 一个比较明显的顺序分布,但是 Orderid 因为有很多的物流公司,每家物流公司的订单的规则都不太一样,大致可以看到物流的订单其实有比较明显的前缀关系,比如像申通都是有固定前缀的,可以得到一个结论是物流订单不具有全局随机性的,也就是如果直接把订单存在这很可能会导致数据倾斜或者局部的热点,尤其是在当整个体量非常大的时候微小的数据分布将会放大阻碍系统的扩展为了解决目录订单本身分布不均匀的问题,需要对它做一个处理让这种不均匀的数据能够变得均匀,比较直接的方案给它加一个 Hash 。比如物流订单本身算 MD5 或者算一个 Hash 取结果的前四位或者前八位拼到前面 rowkey ,Hash 加上 orderld 加上时间戳作为新的 rowkey 。
(3)第二版表设计:orderld 加 Hash
任何订单的写入,都会落在一个随机的位置,以此来确保全表数据分布的均匀性
这样对于任何一个订单来讲在整个表里面,因为前面拼了 Hash 。整个数据的分布取决于 Hash 值在整个 region 里面的位置,而不是取决于 orderld 的,因为 Hash 本身具有随机性整个,订单的这个数据也是具有随机性的,注意一点是上 Hash 的时候只给 orderld 来算 Hash ,不参与 Hash 计算的,因为期望同一个订单的所有变化数据要排在一起,查的时候能够一把查出来,如果也参与了计算,意味着同一个订单的不同时间测得数据在生成的 Hash 可能是不一样的,可能会散布到整个不同的地方,没有办法去捞到订单的所有数据。因为没有办法预测一个订单可能会有多少的事件变化,也不知道这个事情到底是什么就没法查了,所以说只能给 orderld 自己上话题,通过加 Hash 的方式就很好地解决了订单本身分布的问题
(4)第二版表设计:查询
查询一个订单的所有状态数据:
scan'events.{STARTROW=>'00aa111'STOPROW=>00aa112'}(注意前缀扫描的实现方式)
读出来后在业务侧按时间倒排
查询一个订单的最近一条记录:
select*from events where hash='00aaand orderld='111 order by ts desc limit 1;
功能缺失:HBase 不支持 SQL 查询(可使用Apache Phoenix)
低效:假设支持了,上述 select 会查该订单的所有数据,再返回最后一条,浪费服务端的查询能力
解决方案:ts 在存储的时候,应按 DESC 排序
查询系统要支持两种产品,一种是支持查所有的,一种视察最近的。查询所有的对于 SQL 本身角度来讲,要做一次前缀扫描比如有orderld 之后算出来 Hash 值,基于 Hash1和 orderld 来做前缀查询,对于前缀查询写 scan 的时候要特别注意 StopRow 是通过加一的方式来实现的,也是这里面其实是122,这样能把111这个orderld 全部的数据都查出来。
如果需要查完之后做一个按时间戳倒排,按照现在的设计来讲是需要在客户端做倒排的。另外对于查询订单的最近一条记录写成 SQL 也是需要做倒排,对于当前的表设计需要把orderld 所有的数据都查出来取最后一条,这样就有一个问题是只想要最后一条记录,但是这个查询会把 orderld 的所有数据都查出来,是比较低效的。
如果是用 mysql 来做表设计,在建表的时候很自然的就会把 ts 设一个 DESC 的排序过程,对于 HBase 来讲天然是不支持 DESC 排序的。
(5)终版表设计:ts desc
rowkey = hash +orderld +ts DESC
·hash=md5(orderld)substr(8)(4-8均可,按需选择)
·ts DESC =Long.MAX VALUE-ts(秒级时间戳可用int存储,此时用Integer.MAX VALUE
查询某个订单的最近一条记录:
scan'events{STARTROW=>'00aa111'CACHING=> 1LIMIT=> 1)
比如可以在业务侧自己实现倒序的排序,这种方法是通过 long.MAX_VALUE-ts 来存储可以实现倒序排列,在数学上是可以证明通过计算机的补码设计可以来证明,通过 long- 的方式。
这样当要查一个订单的最近一条记录的收直接读一行数据就可以。而且当去扫一个 order 的所有数据的时候,得到的数据也天然是按时间做倒排,业务不用再做一次排序。这样表设计可以很好地解决写的分布、查询的两个问题。
2、设计考量
分布:rowkey 的随机分布非常重要,如果原始的业务字段随机性较差,可以添加 hash 前缀
·hash 通常取 md5/murmurhash 的前4-8个字节
·加 hash 的副作用:不支持跨 orderld 的范围查询(业务上不需要)
·hash 的替代方案:reverse,如 1112345 经 reverse 是 5432111:不增加 rowkey 长度,适用于有公共前缀但末尾有良好离散度的数据,如时间戳字段
·值类型的 DESC 组织:MAX-原始值,适用于整形类型,如 short,int,long
·浮点数的 desc 实现:按 bit 进行 reverse
查询效率分析:
·服务端扫描的数据量==结果集的数据量,查询没有浪费,最优
·如果读的多,返回的少,则查询有浪费
成本
·数据压缩:文本类数据通常有较好的压缩比,一般可提供
·TTL:物流订单不需要永久保留,结合业务设置合理的 TTL
·冷热分离:云 HBase 提供业务透明的冷热分离能力,自动将冷数据迁移到低成本介质中
整个设计里面关键的点,首先数据的分布 rock 的随机分布非常重要,尤其是大表上面特别的重要,如果原始的业务字段的实际性比较差,比如 user_id 或者 orderid 甚至还有比如像直播的业务,直播间的一些 ID,这些 ID 往往都是有一些特定生成的规则,本身是不具备比较好的随机性,同时又具有一个比较好的颗粒度,可以通过加 Hash 去解决,颗粒度是指不会去涉及到跨订单的查询,比如不会做一个 orderid 大于什么的查询。
因为 orderid 本身就是一个独立单位,这也是加 Hash 之后的副作用,因为原本比如 orderid 2 在数据上可能是挨在一起的,但是加 Hash 之后一会儿就不挨在一起,这也是加 Hash 的副作用,不能够在对加 Hash 的业务字段作为查询。但是其实在业务上也不需要这么做的,加 Hash 有一个替代的方案就是做 reverse 比如1112345经 reverse 是5432111,这种方式不会增加 rowkey 的长度,因为加 Hash 总是多了几个字节的 rowkey ,变长了以后存储的成分虽然比较小,但是当体量很大之后,也是有成本,如果一个业务的字段的末尾是具有比较好的散度的数据,比如时间戳字段是比较好的离散的数据,所以可以通过 reverse 的方式来做一个随机的采列,但对业务数据有依赖,得去做仔细的选择。
另外做倒排往往会通过 max 来做倒排,对于浮点数没有办法做减法。所以可以用 reverse 的方式来做,跟个 max 减差不多,从查询的角度来看,关注的是一个表设计的查询效率怎么样,怎么去度量,就是看查询扫的数据量以及得到结果集的大小,如果扫十行只返回一行,九行都浪费掉了,这个查询是非常低效的,如果说扫十行返回十行,这个产品完全没有浪费,认为它是最优的,对于现在做那个表设计来讲,其实是没有浪费的,如果未来做设计可以通过这种方式来做整个查询,从成本上来看物流订单本身是一个海量的存储,包括订单、日志还有一些数据都是海量存储,对这种海量数据的存储用现代化提供的三种压缩的策略,第一个是数据压缩有字典的压缩,还有各种压缩的优化,一般能够提供一个将近十倍左右的压缩比,除了压缩之外还有 TTL 过期的数据或者可以指定一个过去的周期来把过去的数据自动删掉,就像物流形成这样的数据,不需要对它进行永久保留的,因为它跟业务订单不一样,因为它不涉及钱的问题,比如淘宝买的东西,这个订单可能需要用物流信息,这个单完结之后,可能它的价值就非常的低了,可以结合业务的场景来设计一个比较合理的 TTL,对于物流场景还有一个明显的冷热分离的特性,是大部分都查最近生成的物流的订单的数据,老的数据几乎不查,这样老数据问题是老数据还要留一段时间,所以需要一个冷热分离的能力,冷数据放到一个低价格的高密处境。云 HBase 会提供一个对业务透明的冷热分离能力。业务主要做表数变更就可以,数据自动迁移对成本是非常有好处的。








