书名:HBase入门与实践
ISBN:978-7-115-49383-5
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 彭 旭
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书以精练的语言介绍HBase的基础知识,让初学者能够快速上手使用HBase,对HBase的核心思想(如数据读取、数据备份等)和HBase架构(如LSM树、WAL)有深入的分析,并且让有经验的HBase开发人员也能够循序渐进地深入理解HBase源码,以便更好地去调试和解决线上遇到的各种问题。本书更加专注于HBase在线实时系统的调优,使HBase集群响应延迟更低。本书结合企业必备的“用户行为分析系统”,让读者能够快速上手的同时,也不乏企业HBase实际应用场景,理论不脱离实际,真正做到从入门到精通。
本书适合有一定Java基础的程序员作为HBase入门教程,HBase运维人员可以将本书作为参考手册来部署和监控HBase,正在将HBase应用到在线生产环境中的软件开发人员也可以参考本书来调优HBase在线集群性能。
本书循序渐进地介绍了HBase从入门到企业实践调优,深入浅出地阐述了HBase架构与实现原理,以企业必备的用户行为系统为实践场景,用通俗易懂的语言带领读者走进HBase世界。作者在魅族云和大数据充分实践了书中架构,并实际带来了质量和效率的提升,同时也降低了云端运营成本。
——李柯辰,前魅族平台事业部总经理,现卓轩科技CEO
彭旭是一个优秀的程序员、架构师。本书在介绍HBase基础知识的同时融入了他在魅族云服务团队将存储系统迁移到HBase的经验和教训,此外还在HBase源码和架构研究上有涉猎,对初学者、Java相关开发人员和HBase运维人员等都是一本不错的参考书。
——何伟,前魅族Flyme技术委员会主席,现卓轩科技CTO
作者将多年HBase的实践经验与心得体会沉淀为本书,既有经典的案例分析,也有抽丝剥茧的源码分析,对于大数据行业从业者,非常值得一读。
——张发恩,前百度云技术委员会主席,创新工场人工智能工程院首席架构师,创新奇智CTO
作者是一个长期战斗在一线的典型程序员,本书所写的内容均贴近现实应用场景,用线上真实的案例来描述如何构建、优化HBase生成环境,事半功倍,不管是初学者还是入门者都可以从这些案例中吸取经验。
——唐进,前百度云产品委员会主席,现爱乐奇CTO
与拖沓烦冗的英文著作相比,本书篇幅适中,但是以精简干练的语言完整地描述了Hbase从初学到熟练应用所需学习的所有HBase知识点。
——马杜,华云数据执行总裁
HBase是一个历久弥坚的分布式列式存储系统,相关图书出版时间均距今已久。本书以当前稳定版本HBase为基础,着重介绍了HBase在线实时系统的调优等,很适合正在将HBase应用到在线生产环境的开发人员和运维人员阅读。2010年下半年当当网在纽交所上市之前,我受当时CTO的委托加强当时的防盗刷/刷单系统,本质上是要升级其中的高速计数器产品模块,该模块持久化部分的技术选型就是HBase。这是当当网技术第一次在生产环境中应用Hadoop和HBase。在这个过程中,最让人记忆深刻的不是技术难度,而是当时的资料很有限,HBase的技术迭代又很快,网上大量文档在不同程度上有过时的问题,与其在网站上找过时的资料,不如直接读代码。因此,本书全面介绍了当前稳定版本的HBase,真是为HBase相关开发人员提供了很大的便利。
——傅强,前当当网技术副总裁,现九枝兰合伙人
HBase是Apache下的项目,它是一个高可用、高性能、可伸缩和面向列的分布式存储系统,能实现对海量数据的高性能的读写。本书作者是前阿里天猫的工程师,有着丰富的开发经验,相信这本书一定能让读者的HBase理论与实践水平更上一层楼。
——杨开振,《深入浅出Spring Boot 2.x》《深入浅出MyBatis技术原理与实战》
《Java EE互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)
和Redis实现》作者
一个有很大价值的开源项目既需要活跃的代码贡献者为其快速迭代铺路,也需要有优质的入门资料为广大初学者打下良好的基础。要学习HBase,英文版的Apache HBase Book仍然是最佳的参考手册,而对英文不是特别好的读者来说,可以选择的资料就是《HBase权威指南》或者是一些技术作者的专栏博客。本书对入门者来说是很好的中文读物。书中配有丰富的案例和插图,让读者可以较为轻松地理解HBase的常用场景和用法。希望本书对广大的HBase爱好者有所裨益。
——胡争,小米HBase工程师,HBase Committer
我几年前在维护淘宝的HBase集群时,时常和业务方交流如何基于HBase的架构来设计一个可靠和高性能的业务系统,深感对HBase内部原理的了解能极大改善业务系统的性能、稳定性以及成本。本书以当前HBase最新的稳定版本为基础,十分精准地抽离出了开发者需要重点关注的特点,并辅以几个典型的场景加以分析,堪称业务系统设计的手边助理。此外,作者拥有丰富的一线经验,因此本书具有很高的帮助开发者迅速上手生产系统的安装、部署以及API使用的实践价值。
——邓明鉴,前阿里巴巴高级专家,现ZStack首席架构师
人工智能作为当前最热门的技术,其根本上离不开大数据的支持。如果把人工智能比喻成一个神经网络,那么数据则是在这个神经网络中用来传递信息的化学物质,没有信息传递的神经网络显然不名一文,因此大数据扮演着人工智能基石的角色。Hadoop生态系统的HDFS和MapReduce分别为大数据提供了存储和分析处理能力,但是对在线实时的数据存取则无能为力,而HBase作为Apache顶级项目,弥补了Hadoop的这一缺陷,满足了在线实时系统低延时的需求。目前HBase在各大互联网公司几乎都有应用,前景广阔。
本书先介绍HBase基础知识,再带领读者深入研读HBase源码,从数据读取之Scan流程、HBase架构(如LSM树和WAL),到构建线上实时低延迟系统的调优,结合企业必备的“用户行为分析系统”,让读者能够快速上手HBase并了解HBase实现原理,同时让读者通过分析源码来了解HBase的设计思想,期望读者能够真正做到从入门到精通。
本书各章的主要内容概括如下。
作为一个典型的内敛型程序员,我从来没有过要写书、当作者的念头,也一直觉得作家是一个很神秘的职业。由于个人性格问题,我对任何事情的描述都是直来直往,描述问题经常是直奔主题,很少会去解释背景与前因后果。机缘巧合下我遇到了几个做教育以及写书的朋友,一番纠结后兴起了写书的念头,正好最近一直在研究HBase,加上已经到了三十而立的年龄,宝宝也将要出生,觉得自己也需要做点什么,一方面给自己留点回忆,另一方面等宝宝长大后也算是给她的一个礼物。开始写的时候进度很慢,感觉无法坚持下去,但是慢慢地写着写着竟然越来越精神,越来越投入,愿本书能够帮到你!
感谢我的父母对我个人和家庭无私的付出与无微不至的照顾,感谢我的妻子黄晶对我的包容与支持!感谢珠海市魅族科技有限公司、珠海市卓轩科技有限公司提供的平台与实践经历促成了本书!感谢李柯辰先生与何伟先生对我和本书的鼎力支持与帮助!感谢我生命中遇到的每一个人,愿大家越来越好!祝我的宝贝彭语桐开心快乐成长!
由于作者水平有限,在编写过程中,难免出现错误或者不准确的地方,但是凡是涉及实例代码之处,我都实际运行过程序,从而保证程序的准确性。如果读者在阅读过程中发现有错误之处,敬请指正,联系方式如下:
微博17051158029
微信17051158029
邮箱17051158029@163.com
彭旭
2018年10月
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供如下资源:
本书源代码;
书中彩图文件。
要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
HBase的数据模型与传统数据库相比更加灵活,使用之前无须预先定义一个所谓的表模式(Schema),同一个表中不同行数据可以包含不同的列,而且HBase对列的数量并没有限制。当然如果一行包括太多的列,就会对性能产生负面影响。HBase很适合存储不确定列、不确定大小的半结构化数据。
HBase是一个键值(key-value)型数据库。HBase数据行可以类比成一个多重映射(map),通过多重的键(key)一层层递进可以定位一个值(value)。因为HBase数据行列值可以是空白的(这些空白列是不占用存储空间的),所以HBase存储的数据是稀疏的。
下面解释一下与HBase逻辑模型相关的名词。
(1)表(table):类似于关系型数据库中的表,即数据行的集合。表名用字符串表示,一个表可以包含一个或者多个分区(region)。
(2)行键(row key):用来标识表中唯一的一行数据,以字节数组形式存储,类似于关系型数据库中表的主键(不同的是从底层存储来说,行键其实并不能唯一标识一行数据,因为HBase数据行可以有多个版本。但是,一般在不指定版本或者数据时间戳的情况下,用行键可以获取到当前最新生效的这行数据,因此从用户视图来说,默认情况下行键能够标识唯一一行数据),同时行键也是HBase表中最直接最高效的索引,表中数据按行键的字典序排序。
(3)列族(column family):HBase是一个列式存储数据库,所谓列式就是根据列族存储,每个列族一个存储仓库(Store),每个Store有多个存储文件(StoreFile)用来存储实际数据。
(4)列限定符(column qualifier):每个列族可以有任意个列限定符用来标识不同的列,这个列也类似于关系型数据库表的一列,与关系型数据库不同的是列无须在表创建时指定,可以在需要使用时动态加入。
(5)单元格(cell):单元格由行键、列族、列限定符、时间戳、类型(Put
、Delete
等用来标识数据是有效还是删除状态)唯一决定,是HBase数据的存储单元,以字节码的形式存储。
(6)版本(version):HBase数据写入后是不会被修改的,数据的Put
等操作在写入预写入日志(Write-Ahead-Log,WAL)(类似于Oracle Redo Log)后,会先写入内存仓库(MemStore),同时在内存中按行键排序,等到合适的时候会将MemStore中的数据刷新到磁盘的StoreFile文件。因为数据已经排序,所以只需顺序写入磁盘,这样的顺序写入对磁盘来说效率很高。由于数据不会被修改,因此带来的问题就是数据会有多个版本,这些数据都会有一个时间戳用来标识数据的写入时间。
(7)分区(region):当传统数据库表的数据量过大时,我们通常会考虑对表做分库分表。例如,淘宝的订单系统可以按买家ID与按卖家ID分别分库分表。同样HBase中分区也是一个类似的概念,分区是集群中高可用、动态扩展、负载均衡的最小单元,一个表可以分为任意个分区并且均衡分布在集群中的每台机器上,分区按行键分片,可以在创建表的时候预先分片,也可以在之后需要的时候调用HBase shell命令行或者API动态分片。
接下来以用户行为管理系统为例,假设现在需要用HBase来存储电商系统的用户行为数据,表s_behavior
用来存储这些行为数据,两个列族pc
和ph
分别存储电脑端与手机端的行为数据,列v
用来存储用户浏览记录,列o
用来存储用户的下单记录,图3-1描述了这个表的HBase逻辑视图。
图3-1 HBase逻辑视图
HBase按行键的字典序存储数据行,其数据存储层级可以用如下的一个Java中的Map
结构类比:
Map<RowKey,Map<Column Family,Map<Column Qualifier,Map<Timestamp,Value>>>>
如下代码可以用来定位到某个键值对(或者说单元格):
map.get("12345_1516592489001_1").get("pc").get("v").get("1516592489001");
代码清单3-1的JSON
字符串同样近似地描述了这个多维的Map
。
代码清单3-1 HBase逻辑视图类比JSON
{
"12345_1516592489001_1": {
"pc": {
"pc:v": {
"1516592489000": "1001",
"1516592489001": "1002"
},
"pc:o": {
"1516592489000": "1001"
}
},
"ph": {
"ph:v": {
"1516592489001": "1002"
}
}
},
"12345_1516592490000_2": {
"ph": {
"ph:v": {
"1516592490000": "1004"
}
}
}
}
HBase是一个列式存储数据库,数据按列族聚簇存储在存储文件(StoreFile)中,空白的列单元格不会被存储,图3-2描述了HBase表的物理存储模型。
(1)HBase中表按照行键的范围被划分为不同的分区(Region),各个分区由分区服务器负责管理并提供数据读写服务,HBase主节点进程(HMaster)负责分区的分配以及在集群中的迁移。
(2)一个分区同时有且仅由一个分区服务器提供服务。当分区增长到配置的大小后,如果开启了自动拆分(也可以手动拆分或者建表时预先拆分),则分区服务器会负责将这个分区拆分成两个。每个分区都有一个唯一的分区名,格式是“<表名,startRowKey
,创建时间>”。一个分区下的每个列族都会有一个存储仓库(Store),因此一个表有几个列族,那么每个分区就会有几个存储仓库。
(3)每个Store(存储仓库)有且仅有一个MemStore(内存仓库),但是可以有多个存储文件。当分区服务器处理写入请求时,数据的变更操作在写入WAL后,会先写入MemStore,同时在内存中按行键排序。当MemStore到达配置的大小或者集群中所有MemStore使用的总内存达到配置的阈值百分比时,MemStore会刷新为一个StoreFile(存储文件)到磁盘,存储文件只会顺序写入,不支持修改。
(4)数据块(block)是HBase中数据读取的最小单元,StoreFile由数据块组成,可以在建表时按列族指定表数据的数据块大小。如果开启了HBase的数据压缩功能,数据在写入StoreFile之前会按数据块进行压缩,读取时同样对数据块解压后再放入缓存。理想情况下,每次读取数据的大小都是指定的数据块大小的倍数,这样可以避免一些无效的IO,效率最高。
图3-2 HBase物理视图
图3-3描述了上面提到的表和分区等各模块分别由HBase的哪些进程负责管理。
图3-3 HBase模块交互图
虽然HBase是一个“无模式”的数据库,但是当架构一个系统时,仍然得考虑为了满足系统功能需求应该设计几个表、每个表存储什么类型的数据以及如何优化对数据的查询。很多时候应该在设计的时候就考虑优化,而不是等系统开发完成或者数据量已经庞大到无法动弹的时候再来考虑优化。传统的关系型数据库在设计表的同时需要考虑如何给表添加索引,类似地,HBase在设计表的同时应该考虑如何设计行键以提高查询效率。行键在HBase中充当表的一级索引角色,并且HBase本身没有提供二级索引的机制,因此对行键的设计优化对实时查询尤为重要。
MySQL等关系型数据库在写数据的同时需要随机写磁盘来构建索引,而HBase系统架构则通过写内存、顺序写磁盘来提升写性能(磁盘顺序写性能比随机写性能高很多),但这是以牺牲读性能为代价的。因为内存中的数据最终会刷新为StoreFile,所以最后会有很多个StoreFile来存储数据,读取数据时也就需要读取多个StoreFile来查找到所需要的数据。同样也有许多方法可以用来提升读性能,例如通过写多份数据来优化来自不同维度的数据查询,如电商订单的卖家维度与买家维度数据,也可以通过牺牲一些业务可用性来提升性能,如客户端缓存,禁写WAL可以使得写速度更快,但是可能会导致一些数据的丢失。
下面以用户行为日志系统为例来设计其HBase存储方案,假设我们的系统仍然聚焦在电商业务,数据类型、数据特征与数据分析需求如下所示。
HBase数据按照行键字典序自然排序,这对扫描(Scan
)操作是一个优化。行键也是HBase最有效的索引,与MySQL之类的传统关系型数据库支持多字段的索引不同的是,HBase不支持二级索引,因此MySQL通过多重、多列索引支持的复杂数据库查询操作对HBase来说可能是个灾难。当然HBase也支持对列的条件过滤(参见6.4节),但是因为需要读取存储文件做字符或者字节对比,所以效率很低,对性能影响很大。
下面列出了一些HBase行键设计的原则。
下面从统计需求来看用户行为日志系统表s_behavior
的行键设计。
[用户ID]_[时间戳]
”。在某些极端并发情况下,例如,用户浏览器同时打开多个商品,那么这些浏览记录时间戳可能相同。考虑到唯一性原则,可以在行键最后添加一个序列号来实现,因此最后得到的行键格式为“[用户ID]_[时间戳]_[序列号]
”。用户浏览的商品ID、订单ID等可以作为列值数据存储。[商品ID]_[数据类型]_[时间戳]_[序列号]
”,用户ID等可作为列值数据存储。当然一个完整的用户日志系统还要考虑很多其他需求,但是到目前为止的两个需求已经足够麻烦了,因为两个需求得到的行键完全没有共同点。下面分析如何在保持性能基本稳定的同时满足以上两个需求。
回到用户行为管理系统,为了满足两个查询需求,可以考虑按用户维度构建行键存储数据,按商品维度建立二级索引满足商品维度统计需求,如表5-1所示。
表5-1 用户行为日志管理系统二级索引设计
行键 |
列族:列浏览记录 (cf:v) |
列族:列下单记录 (cf:o) |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
||
|
||
|
||
|
表5-1前4行为数据部分,后4行为索引部分,下面解释每行数据的含义。
12345
在时间戳1510720956000
浏览了商品1001
,同时下单了商品1001
。12345
在时间戳1510721056000
浏览了商品1002
。12346
在时间戳1510721086000
浏览了商品1001
。12346
在时间戳1510721096000
下单了商品1001
。下面看看如何使用二级索引满足系统的两个统计需求。
12345
从2017年12月1日到2018年1月1日之间的商品浏览记录,只需指定查询的行键区间[12345_1512086400000, 12345_1514764800000)
。idx1
。假设需要统计商品1001
在2018年 1 月 1 日这天的转化率,需要扫描的行健区间为[12345_idx1_1001_0_1514764800000, 12345_idx1_1001_1_1514851200000)
,并且只需扫描所有的索引行键。拿到所有的索引行键后循环一遍,根据数据类型判断是浏览记录还是下单记录,对浏览记录和下单记录,分别求和即可得到转化率。注意,这里的场景比较特殊,通常情况下还会需要根据索引行键得到数据行键,再根据数据行键去查询数据,因此索引行键的最后是数据行键。生产环境中用户ID的生成规则通常是一个递增的数字(用户管理系统一般都基于关系型数据库,用户ID一般使用MySQL等数据库提供的自增主键)。以用户行为日志表为例,假设将该表分为10个分区,分区行键范围分别是[0, 1), [1, 2), [2, 3), … [8, 9), [9,),用户行为日志管理系统用户ID使用的递增数字计数器已经到了12346
,接下来的新注册用户ID会是12347, 12348, …, 12366, …, 12398,而新注册的用户一般会比较活跃,会产生比较多的行为日志数据。这些数据的行键以用户ID开始,即行键第一个字符为1,因此这些数据会落在分区[1,2),显然接下来分区[1,2)会承受很大的读写压力,继而引起负责该分区的分区服务器负载升高,使得该分区服务器上的其他分区响应延长,最终可能会影响集群整体性能。同样一些使用时间戳作为行键的设计也会引起同样的问题,因此行键的设计需要规避这些问题,使得数据均匀、平衡地分布在集群的每台分区服务器。
下面是一些常见的避免热点区间的方法。
(1)加盐。在行键前面添加随机数字或者字母,使得数据随机分配到不同的分区,这种方式弊端显而易见。如果需要使用GET
请求再次获取某行数据,则需要在插入时保存一个原始业务行键与添加的随机数的映射关系,或者使用某种散列函数计算原始业务行键的散列值,然后将该散列值作为最终行键的前缀。使用这种行键设计的应用通常只是用来做一些分析统计,因此一般实时在线系统不建议使用该行键设计方式。
(2)反转补齐。将用户ID反转(如12345
反转为54321
)可以将变化最多的部分放到行键前面,这样数据的写入也能够顺序地流入各个分区而使得集群负载比较均衡。反转补齐是避免热点区间常用的方法。因为用户ID一般都使用关系型数据库的自增主键,长度最长一般为20个数字,所以为了使得行键保持定长以方便排序以及可以从行键反推出用户ID,通常会将用户ID反转后在末尾加0补齐20个数字。类似地,如果使用时间戳作为行键的一部分,则可以使用“Long.MAX_VALUE-
时间戳”,这样最新时间戳的数据行键值较小,数据行能够排在数据存储文件前列,代码清单5-1实现了这两种行键设计的处理方式。
代码清单5-1 行键反转补齐
1. package com.mt.hbase.chpt5;
2.
3. public class RowKeyUtil {
4.
5. /**
6. * 补齐20位,再反转
7. *
8. * @param userId
9. * @return
10. */
11. public String formatUserId(long userId) {
12. String str = String.format("%0" + 20 + "d", userId);
13. StringBuilder sb = new StringBuilder(str);
14. return sb.reverse().toString();
15. }
16.
17. /**
18. * Long.MAX_VALUE-lastupdate得到的值再补齐20位
19. *
20. * @param lastupdate, eg: "1479024369000"
21. * @return
22. */
23. public String formatLastUpdate(long lastupdate) {
24. if(lastupdate < 0){
25. lastupdate = 0;
26. }
27. long diff = Long.MAX_VALUE - lastupdate;
28. return String.format("%0" + 20 + "d", diff);
29. }
30.
31. public static void main(String [] args){
32.
33. RowKeyUtil rowKeyUtil = new RowKeyUtil();
34. // 下行输出结果为54321000000000000000
35. System.out.println(rowKeyUtil.formatUserId(12345L));
36.
37. long time = System.currentTimeMillis();
38. // 运行时下行输出结果为09223370520503124434
39. System.out.println(rowKeyUtil.formatLastUpdate(time));
40.
41. }
42.
43.
44. }
用户行为日志表s_behavior
的每行代表用户的一条浏览或者下单记录,每行最多包含两列,分别代表浏览或者下单记录。每当用户有新的浏览记录或者下单记录时,就在表中新增一行记录,最后这个表的数据会看起来“高高瘦瘦”的,称之为高表。与高表相反的还有另外一种设计,假设仍然以用户ID为查询维度,同样需要把同一个用户的数据聚簇地存储在一起。如果以用户ID为行键,则用户的每条浏览记录或者下单记录为数据行的一列,每当用户有新的浏览记录或者下单记录时,就更新表中以用户ID为行键的这行数据,对这行数据增加一列,最后这个表的数据会看起来“矮矮胖胖”的,称之为宽表。下面定义了高表与宽表。
表5-2给出了HBase中高表与宽表的优劣对比。
表5-2 高表与宽表的优劣对比
查询性能 |
负载均衡 |
元数据 |
事务支持 |
|
---|---|---|---|---|
高表 |
√ |
√ |
||
宽表 |
√ |
√ |
hbase:meta
)更大,可能会给HBase集群主节点(HMaster)与HBase客户端带来更大的内存压力(客户端会缓存元数据)。回到用户行为日志表s_behavior
的设计,到底是应该选用高表设计还是宽表设计呢?显然行键设计章节最后得出的表结构是一个高表。
2017年11月9日,微信团队发布了《2017年微信数据报告》,报告显示2017年9月日均登录用户超9亿,日发送次数达380亿,朋友圈视频发布次数达6800万,相信很多人都会对这样一个庞然大物架构的设计很感兴趣。
由于微信的体量以及爆发增长的特性,因此分布式可弹性伸缩、多机房负载均衡、容灾是系统架构与设计必须考虑的要素。HBase就是一个很合适的存储系统,下面来一起看看如何使用HBase作为朋友圈存储系统的设计,同样从需求开始分析如何实现。
朋友圈的核心是每个用户各自拥有的一个自己发布的相册和一个用户关注的好友的动态,称之为时间线TimeLine。为了实现这两个功能,存储系统的设计需要考虑如下需求。
第一、二个需求可以归结为一个用户关系表的构建,该用户关系表会有两种访问模式,第一个是查询哪些用户关注了LiLei,第二个是查询LiLei关注了哪些用户。第一个想到的设计可能就是从主语出发,使用用户ID作为行键,每列存储一个该用户关注人的用户ID,显然这就是一个之前提到的宽表的设计。假设该表命名为t_following
,表数据格式如图5-1所示。
图5-1 用户关系表-宽表
图5-1的两个表都包含一个列族,列族名均为cf
,t_user
表列族cf
包含一列,列限定符名称为n
,而t_following
表受益于HBase的无模式,列可以在使用时动态定义,那么现在有个新问题是假设LiLei(用户ID为12345
)新关注了一个用户Kate(12350
),HBase客户端在向t_following
表写数据的时候需要知道写入的列限定符名称,那么此时需要定义一个规则来命名列限定符,可以使用如下两种解决方案。
12350
),作为列限定符。该方法的优点是客户端在插入新关注用户时无须调用服务端获取列限定符,取消关注时也无须查出整行数据,而只需直接将用户ID作为操作的列限定符使用。表t_following
事实上只满足了LiLei关注了哪些用户的需求,一个简单的GET
请求即可知道LiLei关注了哪些用户。如果需要知道哪些用户关注了LiLei,需要使用同样的模式新建表t_followed
用来存储哪些用户关注了LiLei。
现在已经满足了需求定义的第一个需求和第二个需求的前半部分(哪些用户关注了LiLei),第三个需求似乎也容易满足。虽然一个GET
请求即可知道LiLei关注了哪些用户或者哪些用户关注了LiLei,但是这个GET
请求需要读取出整行数据,然后遍历这行数据所有的列来得到LiLei有没有关注Lucy,才能知道LiLei能不能看到Lucy对他们的共同关注对象HanMeiMei朋友圈的评论。如果LiLei关注了成百上千个朋友,那么注定LiLei的朋友圈体验没那么优雅。那么,有没有更加高效的解决方案呢?凡事都有两面性,既然我们使用宽表来设计存储架构获得了成功,那么使用高表设计又会有什么样的效果呢?
使用高表来存储用户关注列表的关键点是将列转行,可以使用“用户ID+被关注人用户ID”作为行键。在朋友圈消息或者评论上会显示用户的昵称,因此为了提高性能,可以把用户昵称作为单元值存储,这是一个反范式的设计,违反了数据库设计的第三范式,会导致数据的冗余。假如用户更新了昵称,那么除了更新用户表t_user
之外,用户关系表t_following
也需要更新,一般情况下用户更新昵称周期比较长,而且更新昵称后允许延迟展示,因此可以采用一些延迟更新之类的策略来维持数据的一致性(这种最终一致性,在过程中可能存在不一致性,也称为弱一致性)。考虑到各影响因素,我们预期用户更新昵称周期比较长,通过冗余用户昵称的代价可以提升一部分读性能,因此这里的反范式设计是一个更好的解决方案,再来看看高表设计下的数据格式,如图5-2所示。
同样表t_following
事实上只满足了LiLei关注了哪些用户的需求,一个Get
请求即可以知道LiLei是否关注了HanMeiMei,并且该Get
请求只需查询出一个单元值,查询的数据量大大小于宽表的设计,一个区间扫描即可知道LiLei关注了哪些用户。
如果需要知道哪些用户关注了LiLei,需要使用同样的模式新建表t_followed
用来存储哪些用户关注了LiLei。
还有一种设计是为这种关系加一个类型,例如,0
代表关注,1
代表被关注,行键12345_0_12346
表示LiLei关注Lily,行键12346_1_12345
表示LiLei被Lily关注,这样就可以合并t_following
表与t_followed
表,如图5-3所示。
图5-2 用户关系表:高表
图5-3 用户关系表-合并关注与被关注
注意,这里行键的设计是把关系类型作为行键的第二个因子。如果把关系类型作为行键的第一个因子,那么用户关注的数据与用户被关注的数据会聚簇在一起,分别作为不同的分区分布在分区服务器。如果用户关注的数据使用比较频繁,那么负责这部分数据的分区服务器会比较繁忙,这就造成了整个集群的负载不均衡。
在线上生产环境中的大部分情况都会使用高表的设计,因为相对来说高表性能都会优于宽表,但是由于HBase只支持行级事务,如果某些业务要求使用事务来保证数据的强一致性,那么此时高表就可能不是一个更好的选择了。
到这里朋友圈的两个核心功能个人相册与时间线仍未实现,下面先看一看朋友圈的业务流程。
(1)HanMeiMei发布一张朋友圈图片,该图片会上传到HanMeiMei最近的CDN缓存服务器,上传成功后返回一个图片引用地址。
(2)HanMeiMei的微信客户端将朋友圈内容以及图片引用地址发布到微信服务器自己的相册。
(3)发布完成后如果LiLei刷新自己的朋友圈时间线,由于LiLei关注了HanMeiMei,因此LiLei会看到HanMeiMei刚刚发布的朋友圈。
个人相册的存储表比较简单,用户发布朋友圈也只需要插入一条数据到用户相册表,行键使用用户ID加时间戳,列族cf
包含两列,列t
用来存储用户朋友圈的文字内容,列p
用来存储用户朋友圈的图片CDN缓存地址,如图5-4所示。
图5-4 用户朋友圈相册表
注意到为了可读性,这里的行键设计并未做优化,为了更好地实现负载均衡以及数据的读取,对行键的用户ID可以做反转补齐,时间戳部分可以用Long.MAX_VALUE-timestamp
后补齐,这样中间的连接符号“-
”可以省略。
到这里由于用户的朋友圈消息已经存储下来了,用户的时间线可以根据t_following
和t_album
两张表计算得出。如果LiLei关注了500个用户,那么LiLei刷新朋友圈要扫描出这500个用户的朋友圈消息,然后按时间倒序排序,选取前N条展示到LiLei的朋友圈,这样的效率无疑是无法接受的,而且这500个用户的数据可能分布在上百台服务器,其中某些请求有很大的概率会失败,最后LiLei完全没法愉快地刷朋友圈了。
为了提高朋友圈用户体验,读性能必须能够保证。为了提高读性能,可以将读取的工作提前到写之后,类似于HDFS写多个数据副本,这里也可以使用写扩散的模型来提高读的效率,工作流程如下。
(1)HanMeiMei发布一张朋友圈图片,该图片会上传到HanMeiMei最近的CDN缓存服务器,上传成功后返回一个图片引用地址。
(2)HanMeiMei的微信客户端将朋友圈内容以及图片引用地址发布到微信服务器自己的相册。
(3)查找t_followed
表找到哪些用户关注了HanMeiMei,将HanMeiMei发布的朋友圈内容以及图片插入到这些用户的TimeLine。
(4)LiLei刷新自己的朋友圈,直接从LiLei的时间线表查询出数据展示。
HBase的写性能很高,读性能相对低,时间线的设计使用写扩散模型,通过写多份冗余数据来提升读性能,因此朋友圈的发布是一个比较重的动作,但是其实对用户是无感知的,做到了对业务、用户体验无损的情况下提升性能。最后用户的时间线表设计如图5-5所示,采用高表设计,行键为“用户ID+时间戳+发布者ID”,同样为了可读性,行键并未使用反转补齐等策略做负载均衡,列族cf
包含3列,列t
用来存储用户朋友圈的文字内容,列p
用来存储用户朋友圈的图片CDN缓存地址,列u
用来存储发布者的个人基本信息(包括昵称和头像),这样用户刷新朋友圈时就无须多一次用户信息的查找。细心的读者可能会提出疑问,如果用户在同一时间发布多条朋友圈,那么行键就重复了?确实如此,但是显然同一时间,同一个用户基本不可能发布超过一条朋友圈,因此这里不做考虑。
图5-5 用户时间线表设计