阿里巴巴LTR Qcon分享笔记

LTR, Leaning To Rank,是一种基于机器学习的Rank方法。

SENP : Search Engine Result Page 搜索引擎结果页面

宏观上看,分为了三类,分别是 PointWise,PaireWise,ListWise。

parameter of the classifier should be tuned to optimize the NDCG score on the cross validation set.
query full:SERPs returned in response to a query.
query less:SERPs teturned in response to the user click on some product category.

i.i.d :独立同分布independent and identically distributed

Personalized E-Commerce Search 个性化电子商务搜索

Predict relevance scores and re-rank products returned by an e- commerce search engine on the search engine result page

对搜索引擎结果页面中的item,预测相关性得分和re-rank

使用的数据

Search, browsing, and transaction histories for all users and specifically the user interacting
with the search engine in the current session

所有用户的搜索,浏览,交易历史,特别是在当前搜索引擎session中用户与系统交互的行为

Product properties and meta-data

商品特征和元数据

Data Using 使用的方法

  • Matchine Learning (e.g. RankSVM, LambdaMart)

  • Ranking Function(e.g. BM25, Cosine Similarity)

Theory理论 (PAC)

  • Generalization

  • Stability

Applications应用

  • Search 搜索
  • Recommender System 推荐系统
  • Question Answering 问答系统
  • Sentiment Analysis 情感分析,在电商领域,用户评论数据可以用情感分析模型,分析出用户对商品是否满意

Formulation LTR的Formulation

  • Machine Learning
    • Supervised learning with labeled data 使用标记数据进行监督学习,日志分析,点击量,点击停留时间等等
  • Ranking of objects by subject
    • Feature based ranking function 基于特征的排序方法
  • Approach
    • Traditional:BM25 (Probabilistic Model) 概率模型
    • New
      • Query and associated products form Group (Train Data) query的结果集称作group
      • Groups are i.i.d group之间是独立同分布
      • Features (query and product) in Group are not i.i.d group中的特征不是独立同分布
      • Model is a function of features 特征产生函数

Issues

  • Data Labeling 打标数据(训练集)
    • Relevance metric (Point) 跟CTR预估有点像,点了或者没点
    • Ordered pairs
    • Ordered list 对排列组合取最大概率的排列
  • Feature Extraction 特征提取 (非常非常重要)
    • Relevance (User/Query-Prod Feature) 用户的意图(历史行为或者当前的行为)和文档的属性有match
    • Semantic (User/Query-Prod Feature) 语义相关性 LDA,现在流行的是 deepLearning CNN
    • Importance (Prod Feature) doc本身的重要性,比如sku的各种重要属性,类似PageRank是网页本身重要的特种
  • Learning Method 学习方法
    • Model 模型选择,要结合业务,要最合适的,需要对数据的理解,业务的理解
    • Lose Function 损失函数,比如交叉熵。遇到不平滑或者不好求导的的情况,所以很多情况直接选择比较好求导的损失函数
    • Optimized Algorithms 优化算法,随机梯度下降
  • Evaluation Measure 评估测量
    • NDCG 当前排序结果和预期排序结果的比值
  • Machine Learning
    • Classification 分类
    • Regression 回归
    • Ordinal Classification/Regression
  • Ordinal Regression 序数回归

    • Pointwise
      • Transfer ranking to regression 转变ranking为回归问题
      • Ignore group info 忽略group信息
  • Learning to Rank

    • Pairwise
      • Transfer ranking to binary classification 转换ranking为二分类问题
    • Listwise
      • Straightforward represent learning 直接了当学习

Pointwise Model用的不多

  • McRank(2007)
  • Ordinal Liner Regression (1992)

PaireWise Model比较流行

  • RankSVM(2000)
    • Pairwise classification
  • IR SVM

    • Cost-sensitive Pairwise 排序结果A B C, A比C好很重要,C比A好很差劲

    • Using modified hinge loss

  • RankBoost (2003) GBDT
  • RankNet (2005) 神经网络
  • LambdaMart (2008) 最流行,既可以paireWise也可以Listwise

Listwise Model 目标是 :正确的那个排序结果的概率最大

  • Plackett-Luce Model
  • ListMLE (2008)
  • ListNet (2007)
    • Parameterized Plackett-Luce Model
  • AdaRank (2007)
  • PermuRank (2008)
  • SVM-Map (2007)

Optimize 优化

  • Direct Optimization 直接优化
    • AdaRank
    • SVM Map
  • Approximation 近似
    • Soft Rank
    • Lambda Rank
  • Learning Framework 学习框架
    • Data Representation 数据表示
    • Expected Risk 期望风险
    • Empirical Risk 经验风险
    • Generalization Analysis 概括分析
  • Evaluation 总结
    • Pairwise approach and Listwise approach perform better than Pointwise
      approach

Applications

  • Search
    • Re-Ranking 重排序
  • Recommender System
    • Collaborating Filter 协同过滤

PERSONALIZED E-COMMERCE SEARCH 个性化电商搜索

Pertinence 如何评价搜索的效果好坏

  • Log Analysis 通过分析日志分析来评价
  • Conversion in E-commerce 通过电商转化率评价,但是转化率的评价维度较多。比如通常意义上购买>加购>收藏,但是在双11前夕,用户有提前加购,收藏,等待双11零点下单的习惯,这个场景下,加购或者收藏的价值不低于购买。

Data 数据来源

  • User info 用户信息:基本信息,行为日志等
  • List of the terms that forms the query 检索词的term集合
  • Displayed items and their domains 展示的数据信息,在电商中指商品列表
  • Items on which the user clicked 用户点击了哪些item
  • Timing of all of these actions 用户行为的时间轴,比如分析留长等等,用户的行为都是用时间串联起来的
  • History Behaviors Day 28 to Day 30 近一个月历史的行为数据

Ensemble Model 混合模型

  • Boosting
  • Bagging
  • Stacking

Trap 需要注意的陷阱

  • Position Bias 偏见
    • 人对排在前面的有天然的好感,自然的从上往下读。当用户输入一个比较泛指的词时,说明他的意图不明确(自己都不知道想要什么),比如输入“裙子”,此刻搜索引擎给出的结果也只能是泛泛的,用户点击了某一个item,可能并不是他真正的意图,但至少在页面中,点击了的商品比没点的要好。
    • 线上的所有的日志和效果,都是由上一个模型产生的,上一个模型会引导用户的行为,导致产生了噪声数据,所以采集的数据并不是一个shuffle的结果。如果有条件,可以采用分桶实验

Clicks feedback

When to do personalize

  • Long Term 长期的偏好:比如一个用户每2个月买一次牙膏
  • Short Term 短期的偏好:基于当前会话session的

Past interaction timescales 历史交互时间轴数据

Search behaviors timescales 搜索行为时间轴数据

Learing from all repeated results

Features 特征

  • Aggregate features 聚合特征

  • Query features 检索特征

  • User click habits 用户点击习惯
    • Number of times the user clicked on the item in the past
  • Session features Session会话级别的特征
  • Non-Personalized Rank 非个性化Rank
    • Read linearly
    • Computed with infomation
  • Inhibiting/Promoting features
    • Query click entropy

Methodology

  • Classification will be used
    • Parameter of the classifier should be tuned optimize the NDCG score on the cross validation set.
  • Query Full
    • SERPs returned in response to a query
  • Query Less
    • SERPs returned in response to the user click

Ensemble Model 混合Model

A very powerful technique to increase accuracy on a variety of ML tasks

  • Boosting
  • Bagging

Ensemble Correlation

  • Voting
  • Weighing
  • Averaging
  • Rank averaging

Hbase系列1-Hbase适用场景

#HBase 使用场景

HBase 采用 LSM 架构,适合写多读少场景

##HBase 适用场景

  • 支持海量数据,需要 TB/PB 级别的在线服务(亿行以上)
  • 拥有良好扩展性,数据量增长速度快,对水平扩展能力有需求
  • 高性能读写,简单 kv 读写,响应延迟低
  • 写入很频繁,吞吐量大
  • 满足强一致性要求
  • 批量读取数据需求
  • schema 灵活多变
  • 无跨行跨表事务要求

##HBase 的限制

只有主索引(Rowkey),不支持表联接、聚合、order by 等高级查询

##案例分析

A. 统一日志案例

场景:写多读少,写入量巨大(日写入>37 亿),读取随机且访问较少。
设计:将 Rowkey 用 UUID 类似方式生成随机字段,转换成 Byte 数组,经过优化,截取为 4 个 Byte。既
随机,又很短,能满足 40 年存储。通过 HBase 组针对性的预分区,将数据区域划分到所有节点上,写入均
衡到每个节点,因此压力平均。读取的时候,因为随机,设置了 BLOOMFILTER,所以随机读性能提高。因
为读取少,所以对于读取的优化就没有写那么重要了。

B. 一次导入,多次读取

场景:晚上导入大量数据,白天提供用户访问,每次查询是带有客户 ID 的随机数据,数据量可以估算,每
条数据<100B。
设计:晚上导入大量数据,如果只有一台服务器提供服务,TPS 上不去,且容易导致节点宕机,因此需要写
入是均衡分布的。另外一方面,用户访问的时间和方式是比较随机的,数据又是随机的,因此将 Rowkey
随机分布有助于大量请求比较均衡在多个节点上。因为用户的访问更重要,因此将客户 ID 和相关业务类型
作为 Rowkey。如果写入数据是对用户顺序处理的,就会出现压力集中在某个节点上,导致宕机。于是将导
入 worker 设计为先在本地存储,然后分 10 个线程,将用户 Hash 成 10 组。每个线程读取一个用户的 300
条数据,批量写入 HBase,然后写下一个用户的 300 条数据,继续直到第 10 个用户,再返回写本组第一个
用户,直到所有数据写完。最终集群的表现比较平稳,且 TPS 很高。

C. 监控数据(京东云容器监控cap,京东大脑,jdh监控,bdp监控数据查询,ump,mjdos)

D. 海量存储(商家罗盘,供应商罗盘,Storm实时应用,搜索推荐)

Hbase系列4-rowkey设计技巧

#HBase Rowkey 设计

##一 引言

HBase Rowkey是唯一索引(Rowkey用来表示唯一一行记录),Rowkey设计的优劣直接影响读写性能。

HBase中的行是按照Rowkey的ASCII字典顺序进行全局排序的。

举例说明:假如有5个Rowkey:”012”, “0”, “123”, “234”, “3”,按ASCII字典排序后的结果为:”0”, “012”, “123”, “234”, “3”。(注:文末附常用ASCII码表)

Rowkey排序时会先比对两个Rowkey的第一个字节,如果相同,然后会比对第二个字节,依次类推… 对比到第X个字节时,已经超出了其中一个Rowkey的长度,短的Rowkey排在前面。

由于HBase是通过Rowkey查询的,一般Rowkey上都会存一些比较关键的检索信息,建议提前考虑数据具体需要如何查询,根据查询方式进行数据存储格式的设计,要避免做全表扫描,因为效率特别低,且会损耗集群性能。

##二 Rowkey设计原则

Rowkey设计应遵循以下原则:

###Rowkey唯一原则

Rowkey在设计上必须保证唯一性,Rowkey在HBase中只插入不更新(不做update操作)。HBase中所有操作都是“流式“操作,不会更改原来存储过的数据,只会进行append,并通过加上新的版本标识表示是最新数据(VERSION为1会覆盖,默认是1)。

###Rowkey的散列原则

Rowkey设计应散列,均匀的分布在各个HBase节点上
如果Rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。

Rowkey的第一部分如果是时间戳,会将造成所有新数据都在最后一个Region,造成访问热点。

热点:大量的client直接访问集群的一个或极少数个节点(访问可能是读、写或者其他操作)。大量访问会使热点Region所在的单个机器超出自身承受能力,引起性能下降(Full GC)甚至Region不可用,这也会影响同一个RegionServer上的其他Region,由于主机无法服务其他Region的请求。

通常有3种方式来解决热点问题

1、Reverse反转

针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。
反转Rowkey的例子通常以手机举例,可以将手机号反转后的字符串作为Rowkey,这样的就避免了以手机号那样比较固定开头(137x、15x等)导致热点问题。缺点是牺牲了Rowkey的有序性。

2、Salt加盐

Salting是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。

比如在一个有4个Region(注:以 [ ,a)、[a,b)、[b,c)、[c, )为Region起至)的HBase表中,
加Salt前的Rowkey:abc001、abc002、abc003

我们分别加上a、b、c前缀,加Salt后Rowkey为:a-abc001、b-abc002、c-abc003

可以看到,加盐前的Rowkey默认会在第2个region中,加盐后的Rowkey数据会分布在3个region中,理论上处理后的吞吐量应是之前的3倍。由于前缀是随机的,读这些数据时需要耗费更多的时间,所以Salt增加了写操作的吞吐量,不过缺点是同时增加了读操作的开销。

3、Hash散列或者MOD

用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。

确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用Get操作直接Get想要的行。

例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下

9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)

7006-abc002

95e6-abc003

若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,这种设计会使得分区之间更加均衡。

如果Rowkey是数字类型的,也可以考虑MOD方法(其他类型可以使用hashcode MOD),示意图:

###Rowkey长度原则

Rowkey设计建议定长,长度在10~100个字节,越短越好。
RowKey是一个二进制码流,可以是任意字符串,最大长度 64kb,实际应用中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。建议越短越好,不要超过16个字节,原因如下:

(1)数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光RowKey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;

(2)MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

(3)目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。

其他的如列族名、列名等属性名也是越短越好。value永远和它的key一起传输的。当具体的值在系统间传输时,它的RowKey、列名、时间戳也会一起传输。如果你的RowKey和列名和值相比较很大,那么你将会遇到一些有趣的问题。HFile中的索引最终占据了HBase分配的大量内存。

###Rowkey有序原则

充分利用Rowkey字典顺序排序特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。时间戳反转设计:一个常见的数据库处理问题是快速获取数据的最近版本,使用反转的时间戳作为Rowkey的一部分对这个问题十分有用,可以将Long.MAX_VALUE-timestamp追加到key的末尾,例如[key][reverse_timestamp]。

表中[key]的最新值可以通过scan [key]获得 [key]的第一条记录,因为HBase中rowkey是有序的,最新的[key]在任何更旧的[key]之前,所以第一条记录就是最新的。

这个技巧可以替代使用多版本数据,多版本数据会永久(很长时间)保存数据的所有版本。同时,这个技巧用一个scan操作就可以获得数据的所有版本。

Hbase的排序,是按照rowkey,columnKey(columnFamily + qualifier),timestamp 三维排序,且都是按照字典顺序。首先比较rowkey字典排序,同样rowkey的按columnKey字典排序,最后按timestamp最新排到最前。

###HBase Rowkey设计实战

在实际的设计中我们可能更多的是结合多种设计方法来实现Rowkey的最优化设计。

列1:设计订单状态表

使用Rowkey: reverse(order_id) + (Long.MAX_VALUE – timestamp)

设计优点:一、通过reverse订单号避免Region热点,二、可以按时间倒排显示。

列2:使用HBase作为事件(事件指的的终端在APP中发生的行为,比如登录、下单等等统称事件(event))的临时存储(HBase存储最近10分钟的热数据)

设计event事件的Rowkey为:两位随机数Salt + EventId + Date + Kafka的Offset

设计优点:
加盐的目的是为了增加查询的并发性,假如Salt的范围是0~n,那我们在查询的时候,可以将数据分为n个split同时做Scan操作。经过测试验证,增加并发度能够将整体的查询速度提升5~20倍以上。

随后的EventId和Date是用来做范围Scan使用的。在大部分的查询场景中,都指定了EventId,因此把EventId放在了第二个位置上,同时EventId的取值有几十个,通过Salt + EventId的方式可以保证不会形成热点。

把Date放在Rowkey的第三个位置上以实现按Date做Scan。这里可以考虑对Data进行反转(参考时间戳反转设计)

这样的Rowkey设计能够很好的支持如下查询场景:

1、只按照EventId查询

2、按照EventId和Date查询

非必要情况,一般不建议进行全表的Scan查询,全表Scan对性能的消耗很大。

###HBase的表设计

HBase表设计通常可以是宽表(Wide Table)模式,即一行包括很多列。同样的信息也可以用高表(Tall Table)形式存储,通常高表的性能比宽表要高出50%以上。所以推荐使用高表来完成表设计。

表设计时考虑HBase数据库的一些特性:

1、在HBase表中是通过Rowkey的字典序来进行数据排序的

2、所有存储在HBase表中的数据都是二进制的字节

3、原子性只在行内保证,HBase不支持跨行事务

4、列族(Column Family)在表创建之前就要定义好

5、列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加

##总结

在Rowkey设计时,请先考虑业务场景,比如是读比写多、还是读比写少。HBase本身是为写优化的,即便是这样,也可能会出现热点问题,设计时尽量避免热点。如果读场景比较多,除了考虑以上Rowkey设计原则外,还可以考虑和其他缓存数据库如Redis等结合。

##附录

常用ASCLL码

img

Hbase系列2-Hbase架构深度解析

原文:http://www.blogjava.net/DLevin/archive/2015/08/22/426877.html

HBase架构组成

HBase采用Master/Slave架构搭建集群,它隶属于Hadoop生态系统,由一下类型节点组成:HMaster节点、HRegionServer节点、ZooKeeper集群,而在底层,它将数据存储于HDFS中,因而涉及到HDFS的NameNode、DataNode等,总体结构如下:

img

其中HMaster节点用于:

  1. 管理HRegionServer,实现其负载均衡。
  2. 管理和分配HRegion,比如在HRegion split时分配新的HRegion;在HRegionServer退出时迁移其内的HRegion到其他HRegionServer上。
  3. 实现DDL操作(Data Definition Language,namespace和table的增删改,column familiy的增删改等)。
  4. 管理namespace和table的元数据(实际存储在HDFS上)。
  5. 权限控制(ACL)。

HRegionServer节点用于:

  1. 存放和管理本地HRegion。
  2. 读写HDFS,管理Table中的数据。
  3. Client直接通过HRegionServer读写数据(从HMaster中获取元数据,找到RowKey所在的HRegion/HRegionServer后)。

ZooKeeper集群是协调系统用于:

  1. 存放整个 HBase集群的元数据以及集群的状态信息。
  2. 实现HMaster主从节点的failover。

HBase Client通过RPC方式和HMaster、HRegionServer通信;一个HRegionServer可以存放1000个HRegion;底层Table数据存储于HDFS中,而HRegion所处理的数据尽量和数据所在的DataNode在一起,实现数据的本地化;数据本地化并不是总能实现,比如在HRegion移动(如因Split)时,需要等下一次Compact才能继续回到本地化。

这个架构图比较清晰的表达了HMaster和NameNode都支持多个热备份,使用ZooKeeper来做协调;ZooKeeper并不是云般神秘,它一般由三台机器组成一个集群,内部使用PAXOS算法支持三台Server中的一台宕机,也有使用五台机器的,此时则可以支持同时两台宕机,既少于半数的宕机,然而随着机器的增加,它的性能也会下降;RegionServer和DataNode一般会放在相同的Server上实现数据的本地化。

HRegion

HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion(这个数字怎么来的?没有从代码中看到限制,难道是出于经验?超过1000个会引起性能问题?

来回答这个问题:感觉这个1000的数字是从BigTable的论文中来的(5 Implementation节):Each tablet server manages a set of tablets(typically we have somewhere between ten to a thousand tablets per tablet server))。

img

HMaster

HMaster没有单点故障问题,可以启动多个HMaster,通过ZooKeeper的Master Election机制保证同时只有一个HMaster出于Active状态,其他的HMaster则处于热备份状态。一般情况下会启动两个HMaster,非Active的HMaster会定期的和Active HMaster通信以获取其最新状态,从而保证它是实时更新的,因而如果启动了多个HMaster反而增加了Active HMaster的负担。前文已经介绍过了HMaster的主要用于HRegion的分配和管理,DDL(Data Definition Language,既Table的新建、删除、修改等)的实现等,既它主要有两方面的职责:

  1. 协调HRegionServer
    1. 启动时HRegion的分配,以及负载均衡和修复时HRegion的重新分配。
    2. 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)。
  2. Admin职能
    1. 创建、删除、修改Table的定义。

img

ZooKeeper:协调者

ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等),并且会在它们宕机时通知给HMaster,从而HMaster可以实现HMaster之间的failover,或对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其他的HRegionServer)。ZooKeeper集群本身使用一致性协议(PAXOS协议)保证每个节点状态的一致性。

img

How The Components Work Together

ZooKeeper协调集群所有节点的共享信息,在HMaster和HRegionServer连接到ZooKeeper后创建Ephemeral节点,并使用Heartbeat机制维持这个节点的存活状态,如果某个Ephemeral节点实效,则HMaster会收到通知,并做相应的处理。

另外,HMaster通过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机。在第一个HMaster连接到ZooKeeper时会创建Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,如果当前Active的HMaster宕机,则该节点消失,因而其他HMaster得到通知,而将自身转换成Active的HMaster,在变为Active的HMaster之前,它会创建在/hbase/back-masters/下创建自己的Ephemeral节点。

img

HBase的第一次读写

在HBase 0.96以前,HBase有两个特殊的Table:-ROOT-和.META.(如BigTable中的设计),其中-ROOT- Table的位置存储在ZooKeeper,它存储了.META. Table的RegionInfo信息,并且它只能存在一个HRegion,而.META. Table则存储了用户Table的RegionInfo信息,它可以被切分成多个HRegion,因而对第一次访问用户Table时,首先从ZooKeeper中读取-ROOT- Table所在HRegionServer;然后从该HRegionServer中根据请求的TableName,RowKey读取.META. Table所在HRegionServer;最后从该HRegionServer中读取.META. Table的内容而获取此次请求需要访问的HRegion所在的位置,然后访问该HRegionSever获取请求的数据,这需要三次请求才能找到用户Table所在的位置,然后第四次请求开始获取真正的数据。当然为了提升性能,客户端会缓存-ROOT- Table位置以及-ROOT-/.META. Table的内容。如下图所示:

img

可是即使客户端有缓存,在初始阶段需要三次请求才能直到用户Table真正所在的位置也是性能低下的,而且真的有必要支持那么多的HRegion吗?或许对Google这样的公司来说是需要的,但是对一般的集群来说好像并没有这个必要。在BigTable的论文中说,每行METADATA存储1KB左右数据,中等大小的Tablet(HRegion)在128MB左右,3层位置的Schema设计可以支持2^34个Tablet(HRegion)。即使去掉-ROOT- Table,也还可以支持2^17(131072)个HRegion, 如果每个HRegion还是128MB,那就是16TB,这个貌似不够大,但是现在的HRegion的最大大小都会设置的比较大,比如我们设置了2GB,此时支持的大小则变成了4PB,对一般的集群来说已经够了,因而在HBase 0.96以后去掉了-ROOT- Table,只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了:

  1. 从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息。
  2. 从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息。
  3. 从查询到HRegionServer中读取Row。

从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。

img

hbase:meta表

hbase:meta表存储了所有用户HRegion的位置信息,它的RowKey:tableName,regionStartKey,regionId,replicaId等,它只有info列族,这个列族包含三个列,他们分别是:info:regioninfo列是RegionInfo的proto格式:regionId,tableName,startKey,endKey,offline,split,replicaId;info:server格式:HRegionServer对应的server:port;info:serverstartcode格式是HRegionServer的启动时间戳。

img

HRegionServer详解

HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成。

  1. WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。关于WAL可以参考Wikipedia的Write-Ahead Logging。顺便吐槽一句,英文版的维基百科竟然能毫无压力的正常访问了,这是某个GFW的疏忽还是以后的常态?
  2. BlockCache是一个读缓存,即“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻需要某个数据,那么有很大的概率在一下时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。这篇文章BlockCache101对on-heap和off-heap的BlockCache做了详细的比较。
  3. HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们分别属于不同的Table。HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。
    1. MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会 写入MemStore中,由MemStore根据一定的算法将数据Flush到地层HDFS文件中(HFile),通常每个HRegion中的每个 Column Family有一个自己的MemStore。
    2. HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。

img

虽然上面这张图展现的是最新的HRegionServer的架构(但是并不是那么的精确),但是我一直比较喜欢看以下这张图,即使它展现的应该是0.94以前的架构。

img

HRegionServer中数据写流程图解

当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。

img

写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。

img

MemStore Flush

MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示:

img

每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作,

需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。据说这是Column Family有个数限制的其中一个原因,估计是因为太多的Column Family一起Flush会引起性能问题?具体原因有待考证。

  1. 当一个HRegion中的所有MemStore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。
  2. 当全局MemStore的大小超过了hbase.regionserver.global.memstore.upperLimit的大小,默认40%的内存使用量。此时当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush顺序是MemStore大小的倒序(一个HRegion中所有MemStore总和作为该HRegion的MemStore的大小还是选取最大的MemStore作为参考?有待考证),直到总体的MemStore使用量低于hbase.regionserver.global.memstore.lowerLimit,默认38%的内存使用量。
  3. 当前HRegionServer中WAL的大小超过了hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs的数量,当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush使用时间顺序,最早的MemStore先Flush直到WAL的数量少于hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs。这里说这两个相乘的默认大小是2GB,查代码,hbase.regionserver.max.logs默认值是32,而hbase.regionserver.hlog.blocksize是HDFS的默认blocksize,32MB。但不管怎么样,因为这个大小超过限制引起的Flush不是一件好事,可能引起长时间的延迟,因而这篇文章给的建议:“Hint: keep hbase.regionserver.hlog.blocksize hbase.regionserver.maxlogs just a bit above hbase.regionserver.global.memstore.lowerLimit HBASE_HEAPSIZE.”。并且需要注意,这里给的描述是有错的(虽然它是官方的文档)。

在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就直到从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。

img

HFile格式

HBase的数据以KeyValue(Cell)的形式顺序的存储在HFile中,在MemStore的Flush过程中生成HFile,由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写,我们直到磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。

img

HFile参考BigTable的SSTable和Hadoop的TFile实现,从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。首先我们来看一下V1的格式:

img

V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成,其中Data Block是HBase的最小存储单元,在前文中提到的BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数和一系列的KeyValue(Cell)组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速监测这个Data Block的格式,防止数据的破坏。Data Block的大小可以在创建Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index纪录了每个Data块和Meta块的其实点、未压缩时大小、Key(起始RowKey?)等。Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。

HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

img

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue(Cell)的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组。

HFileV1版本的在实际使用过程中发现它占用内存多,并且Bloom File和Block Index会变的很大,而引起启动时间变长。其中每个HFile的Bloom Filter可以增长到100MB,这在查询时会引起性能问题,因为每次查询时需要加载并查询Bloom Filter,100MB的Bloom Filer会引起很大的延迟;另一个,Block Index在一个HRegionServer可能会增长到总共6GB,HRegionServer在启动时需要先加载所有这些Block Index,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本:

img

在这个版本中,Block Index和Bloom Filter添加到了Data Block中间,而这种设计同时也减少了写的内存使用量;另外,为了提升启动速度,在这个版本中还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析。

FileV3版本基本和V2版本相比,并没有太大的改变,它在KeyValue(Cell)层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。关于具体HFile格式演化介绍,可以参考这里。

对HFileV2格式具体分析,它是一个多层的类B+树索引,采用这种设计,可以实现查找不需要读取整个文件:

img

Data Block中的Cell都是升序排列,每个block都有它自己的Leaf-Index,每个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增加以后的读取性能。

参考:

https://www.mapr.com/blog/in-depth-look-hbase-architecture#.VdNSN6Yp3qx

http://jimbojw.com/wiki/index.php?title=Understanding_Hbase_and_BigTable

http://hbase.apache.org/book.html

http://www.searchtb.com/2011/01/understanding-hbase.html

http://research.google.com/archive/bigtable-osdi06.pdf

Hbase系列3-Hbase深入浅出

转自原文:https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-bigdata-hbase/index.html

HBase 在大数据生态圈中的位置

提到大数据的存储,大多数人首先联想到的是 Hadoop 和 Hadoop 中的 HDFS 模块。大家熟知的 Spark、以及 Hadoop 的 MapReduce,可以理解为一种计算框架。而 HDFS,我们可以认为是为计算框架服务的存储层。因此不管是 Spark 还是 MapReduce,都需要使用 HDFS 作为默认的持久化存储层。那么 HBase 又是什么,可以用在哪里,解决什么样的问题?简单地,我们可以认为 HBase 是一种类似于数据库的存储层,也就是说 HBase 适用于结构化的存储。并且 HBase 是一种列式的分布式数据库,是由当年的 Google 公布的 BigTable 的论文而生。不过这里也要注意 HBase 底层依旧依赖 HDFS 来作为其物理存储,这点类似于 Hive。

可能有的读者会好奇 HBase 于 Hive 的区别,我们简单的梳理一下 Hive 和 HBase 的应用场景:

Hive 适合用来对一段时间内的数据进行分析查询,例如,用来计算趋势或者网站的日志。Hive 不应该用来进行实时的查询(Hive 的设计目的,也不是支持实时的查询)。因为它需要很长时间才可以返回结果;HBase 则非常适合用来进行大数据的实时查询,例如 Facebook 用 HBase 进行消息和实时的分析。对于 Hive 和 HBase 的部署来说,也有一些区别,Hive 一般只要有 Hadoop 便可以工作。而 HBase 则还需要 Zookeeper 的帮助(Zookeeper,是一个用来进行分布式协调的服务,这些服务包括配置服务,维护元信息和命名空间服务)。再而,HBase 本身只提供了 Java 的 API 接口,并不直接支持 SQL 的语句查询,而 Hive 则可以直接使用 HQL(一种类 SQL 语言)。如果想要在 HBase 上使用 SQL,则需要联合使用 Apache Phonenix,或者联合使用 Hive 和 HBase。但是和上面提到的一样,如果集成使用 Hive 查询 HBase 的数据,则无法绕过 MapReduce,那么实时性还是有一定的损失。Phoenix 加 HBase 的组合则不经过 MapReduce 的框架,因此当使用 Phoneix 加 HBase 的组成,实时性上会优于 Hive 加 HBase 的组合,我们后续也会示例性介绍如何使用两者。最后我们再提下 Hive 和 HBase 所使用的存储层,默认情况下 Hive 和 HBase 的存储层都是 HDFS。但是 HBase 在一些特殊的情况下也可以直接使用本机的文件系统。例如 Ambari 中的 AMS 服务直接在本地文件系统上运行 HBase。

HBase 与传统关系数据库的区别

首先让我们了解下什么是 ACID。ACID 是指数据库事务正确执行的四个基本要素的缩写,其包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)以及持久性(Durability)。对于一个支持事务(Transaction)的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction Processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。下面,我们就简单的介绍下这 4 个特性的含义。

  • 原子性(Atomicity)是指一个事务要么全部执行,要么全部不执行。换句话说,一个事务不可能只执行了一半就停止了。比如一个事情分为两步完成才可以完成,那么这两步必须同时完成,要么一步也不执行,绝不会停留在某一个中间状态。如果事物执行过程中,发生错误,系统会将事物的状态回滚到最开始的状态。

  • 一致性(Consistency)是指事务的运行并不改变数据库中数据的一致性。也就是说,无论并发事务有多少个,但是必须保证数据从一个一致性的状态转换到另一个一致性的状态。例如有 a、b 两个账户,分别都是 10。当 a 增加 5 时,b 也会随着改变,总值 20 是不会改变的。

  • 隔离性(Isolation)是指两个以上的事务不会出现交错执行的状态。因为这样可能会导致数据不一致。如果有多个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。

  • 持久性(Durability)指事务执行成功以后,该事务对数据库所作的更改便是持久的保存在数据库之中,不会无缘无故的回滚。

在具体的介绍 HBase 之前,我们先简单对比下 HBase 与传统关系数据库的(RDBMS,全称为 Relational Database Management System)区别。如表 1 所示。

表 1. HBase 与 RDBMS 的区别

HBase RDBMS
硬件架构 类似于 Hadoop 的分布式集群,硬件成本低廉 传统的多核系统,硬件成本昂贵
容错性 由软件架构实现,由于由多个节点组成,所以不担心一点或几点宕机 一般需要额外硬件设备实现 HA 机制
数据库大小 PB GB、TB
数据排布方式 稀疏的、分布的多维的 Map 以行和列组织
数据类型 Bytes 丰富的数据类型
事物支持 ACID 只支持单个 Row 级别 全面的 ACID 支持,对 Row 和表
查询语言 只支持 Java API (除非与其他框架一起使用,如 Phoenix、Hive) SQL
索引 只支持 Row-key,除非与其他技术一起应用,如 Phoenix、Hive 支持
吞吐量 百万查询/每秒 数千查询/每秒

理解了上面的表格之后,我们在看看数据是如何在 HBase 以及 RDBMS 中排布的。首先,数据在 RDBMS 的排布大致如表 2。

表 2. 数据在 RDBMS 中的排布示例

ID 密码 时间戳
1 111 20160719
2 222 20160720

那么数据在 HBase 中的排布会是什么样子呢?如表 3 所示(这只是逻辑上的排布)。

表 3. 数据在 HBase 中的排布(逻辑上)

Row-Key Value(CF、Qualifier、Version)
1 info{‘姓’: ‘张’,’名’:’三’} pwd{‘密码’: ‘111’}
2 Info{‘姓’: ‘李’,’名’:’四’} pwd{‘密码’: ‘222’}

从上面示例表中,我们可以看出,在 HBase 中首先会有 Column Family 的概念,简称为 CF。CF 一般用于将相关的列(Column)组合起来。在物理上 HBase 其实是按 CF 存储的,只是按照 Row-key 将相关 CF 中的列关联起来。物理上的数据排布大致可以如表 4 所示。

表 4. 数据在 HBase 中的排布

Row-Key CF:Column-Key 时间戳 Cell Value
1 info:fn 123456789
1 info:ln 123456789
2 info:fn 123456789
2 info:ln 123456789

我们已经提到 HBase 是按照 CF 来存储数据的。在表 3 中,我们看到了两个 CF,分别是 info 和 pwd。info 存储着姓名相关列的数据,而 pwd 则是密码相关的数据。上表便是 info 这个 CF 存储在 Hbase 中的数据排布。Pwd 的数据排布是类似的。上表中的 fn 和 ln 称之为 Column-key 或者 Qulifimer。在 Hbase 中,Row-key 加上 CF 加上 Qulifier 再加上一个时间戳才可以定位到一个单元格数据(Hbase 中每个单元格默认有 3 个时间戳的版本数据)。初学者,在一开始接触这些概念是很容易混淆。其实不管是 CF 还是 Qulifier 都是客户定义出来的。也就是说在 HBase 中创建表格时,就需要指定表格的 CF、Row-key 以及 Qulifier。我们会在后续的介绍中,尝试指定这些相关的概念,以便加深理解。这里我们先通过下图理解下 HBase 中,逻辑上的数据排布与物理上的数据排布之间的关系。

图 1. Hbase 中逻辑上数据的排布与物理上排布的关联

img

从上图我们看到 Row1 到 Row5 的数据分布在两个 CF 中,并且每个 CF 对应一个 HFile。并且逻辑上每一行中的一个单元格数据,对应于 HFile 中的一行,然后当用户按照 Row-key 查询数据的时候,HBase 会遍历两个 HFile,通过相同的 Row-Key 标识,将相关的单元格组织成行返回,这样便有了逻辑上的行数据。讲解到这,我们就大致了解 HBase 中的数据排布格式,以及与 RDBMS 的一些区别。

对于 RDBMS 来说,一般都是以 SQL 作为为主要的访问方式。而 HBase 是一种”NoSQL”数据库。”NoSQL”是一个通用词表示该数据库并

是 RDBMS 。现在的市面上有许多种 NoSQL 数据库,如 BerkeleyDB 是本地 NoSQL 数据库的例子, HBase 则为大型分布式 NoSql 数据库。从技术上来说,Hbase 更像是”数据存储”而非”数据库”(HBase 和 HDFS 都属于大数据的存储层)。因此,HBase 缺少很多 RDBMS 特性,如列类型,二级索引,触发器和高级查询语言等。然而, HBase 也具有许多其他特征同时支持线性化和模块化扩充。最明显的方式,我们可以通过增加 Region Server 的数量扩展 HBase。并且 HBase 可以放在普通的服务器中,例如将集群从 5 个扩充到 10 个 Region Server 时,存储空间和处理容量都可以同时翻倍。当然 RDBMS 也能很好的扩充,但仅对一个点,尤其是对一个单独数据库服务器而言,为了更好的性能,往往需要特殊的硬件和存储设备(往往价格也非常昂贵)。

HBase 相关的模块以及 HBase 表格的特性

在这里,让我们了解下 HBase 都有哪些模块,以及大致的工作流程。前面我们提到过 HBase 也是构建于 HDFS 之上,这是正确的,但也不是完全正确。HBase 其实也支持直接在本地文件系统之上运行,不过这样的 HBase 只能运行在一台机器上,那么对于分布式大数据的环境是没有意义的(这也是所谓的 HBase 的单机模式)。一般只用于测试或者验证某一个 HBase 的功能,后面我们在详细的介绍 HBase 的几种运行模式。这里我们只要记得在分布式的生产环境中,HBase 需要运行在 HDFS 之上,以 HDFS 作为其基础的存储设施。HBase 上层提供了访问的数据的 Java API 层,供应用访问存储在 HBase 的数据。在 HBase 的集群中主要由 Master 和 Region Server 组成,以及 Zookeeper,具体模块如下图所示。

图 2. HBase 的相关模块

img

接下来,我们简单的一一介绍下 HBase 中相关模块的作用。

  • Master

    HBase Master 用于协调多个 Region Server,侦测各个 Region Server 之间的状态,并平衡 Region Server 之间的负载。HBase Master 还有一个职责就是负责分配 Region 给 Region Server。HBase 允许多个 Master 节点共存,但是这需要 Zookeeper 的帮助。不过当多个 Master 节点共存时,只有一个 Master 是提供服务的,其他的 Master 节点处于待命的状态。当正在工作的 Master 节点宕机时,其他的 Master 则会接管 HBase 的集群。

  • Region Server

    对于一个 Region Server 而言,其包括了多个 Region。Region Server 的作用只是管理表格,以及实现读写操作。Client 直接连接 Region Server,并通信获取 HBase 中的数据。对于 Region 而言,则是真实存放 HBase 数据的地方,也就说 Region 是 HBase 可用性和分布式的基本单位。如果当一个表格很大,并由多个 CF 组成时,那么表的数据将存放在多个 Region 之间,并且在每个 Region 中会关联多个存储的单元(Store)。

  • Zookeeper

    对于 HBase 而言,Zookeeper 的作用是至关重要的。首先 Zookeeper 是作为 HBase Master 的 HA 解决方案。也就是说,是 Zookeeper 保证了至少有一个 HBase Master 处于运行状态。并且 Zookeeper 负责 Region 和 Region Server 的注册。其实 Zookeeper 发展到目前为止,已经成为了分布式大数据框架中容错性的标准框架。不光是 HBase,几乎所有的分布式大数据相关的开源框架,都依赖于 Zookeeper 实现 HA。

一个完整分布式的 HBase 的工作原理示意图如下:

图 3. HBase 的工作原理

img

在上面的图中,我们需要注意几个我们之前没有提到的概念:Store、MemStore、StoreFile 以及 HFile。带着这几个新的概念,我们完整的梳理下整个 HBase 的工作流程。

首先我们需要知道 HBase 的集群是通过 Zookeeper 来进行机器之前的协调,也就是说 HBase Master 与 Region Server 之间的关系是依赖 Zookeeper 来维护。当一个 Client 需要访问 HBase 集群时,Client 需要先和 Zookeeper 来通信,然后才会找到对应的 Region Server。每一个 Region Server 管理着很多个 Region。对于 HBase 来说,Region 是 HBase 并行化的基本单元。因此,数据也都存储在 Region 中。这里我们需要特别注意,每一个 Region 都只存储一个 Column Family 的数据,并且是该 CF 中的一段(按 Row 的区间分成多个 Region)。Region 所能存储的数据大小是有上限的,当达到该上限时(Threshold),Region 会进行分裂,数据也会分裂到多个 Region 中,这样便可以提高数据的并行化,以及提高数据的容量。每个 Region 包含着多个 Store 对象。每个 Store 包含一个 MemStore,和一个或多个 HFile。MemStore 便是数据在内存中的实体,并且一般都是有序的。当数据向 Region 写入的时候,会先写入 MemStore。当 MemStore 中的数据需要向底层文件系统倾倒(Dump)时(例如 MemStore 中的数据体积到达 MemStore 配置的最大值),Store 便会创建 StoreFile,而 StoreFile 就是对 HFile 一层封装。所以 MemStore 中的数据会最终写入到 HFile 中,也就是磁盘 IO。由于 HBase 底层依靠 HDFS,因此 HFile 都存储在 HDFS 之中。这便是整个 HBase 工作的原理简述。

我们了解了 HBase 大致的工作原理,那么在 HBase 的工作过程中,如何保证数据的可靠性呢?带着这个问题,我们理解下 HLog 的作用。HBase 中的 HLog 机制是 WAL 的一种实现,而 WAL(一般翻译为预写日志)是事务机制中常见的一致性的实现方式。每个 Region Server 中都会有一个 HLog 的实例,Region Server 会将更新操作(如 Put,Delete)先记录到 WAL(也就是 HLog)中,然后将其写入到 Store 的 MemStore,最终 MemStore 会将数据写入到持久化的 HFile 中(MemStore 到达配置的内存阀值)。这样就保证了 HBase 的写的可靠性。如果没有 WAL,当 Region Server 宕掉的时候,MemStore 还没有写入到 HFile,或者 StoreFile 还没有保存,数据就会丢失。或许有的读者会担心 HFile 本身会不会丢失,这是由 HDFS 来保证的。在 HDFS 中的数据默认会有 3 份。因此这里并不考虑 HFile 本身的可靠性。

前面,我们很多次提到了 HFile,也就是 HBase 持久化的存储文件。也许有的读者还不能完全理解 HFile,这里我们便详细的看看 HFile 的结构,如下图。

图 4. HFile 的结构

img

从图中我们可以看到 HFile 由很多个数据块(Block)组成,并且有一个固定的结尾块。其中的数据块是由一个 Header 和多个 Key-Value 的键值对组成。在结尾的数据块中包含了数据相关的索引信息,系统也是通过结尾的索引信息找到 HFile 中的数据。HFile 中的数据块大小默认为 64KB。如果访问 HBase 数据库的场景多为有序的访问,那么建议将该值设置的大一些。如果场景多为随机访问,那么建议将该值设置的小一些。一般情况下,通过调整该值可以提高 HBase 的性能。

如果要用很短的一句话总结 HBase,我们可以认为 HBase 就是一个有序的多维 Map,其中每一个 Row-key 映射了许多数据,这些数据存储在 CF 中的 Column。我们可以用下图来表示这句话。

图 5. HBase 的数据映射关系

img

HBase 的使用建议

之前我介绍了很多 HBase 与 RDBMS 的区别,以及一些优势的地方。那么什么时候最需要 HBase,或者说 HBase 是否可以替代原有的 RDBMS?对于这个问题,我们必须时刻谨记——HBase 并不适合所有问题,其设计目标并不是替代 RDBMS,而是对 RDBMS 的一个重要补充,尤其是对大数据的场景。当需要考量 HBase 作为一个备选项时,我们需要进行如下的调研工作。

首先,要确信有足够多数据,如果有上亿或上千亿行数据,HBase 才会是一个很好的备选。其次,需要确信业务上可以不依赖 RDBMS 的额外特性,例如,列数据类型, 二级索引,SQL 查询语言等。再而,需要确保有足够硬件。且不说 HBase,一般情况下当 HDFS 的集群小于 5 个数据节点时,也干不好什么事情 (HDFS 默认会将每一个 Block 数据备份 3 分),还要加上一个 NameNode。

以下我给了一些使用 HBase 时候对表格设计的一些建议,读者也可以理解背后的含义。不过我并不希望这些建议成为使用 HBase 的教条,毕竟也有不尽合理的地方。首先,一个 HBase 数据库是否高效,很大程度会和 Row-Key 的设计有关。因此,如何设计 Row-key 是使用 HBase 时,一个非常重要的话题。随着数据访问方式的不同,Row-Key 的设计也会有所不同。不过概括起来的宗旨只有一个,那就是尽可能选择一个 Row-Key,可以使你的数据均匀的分布在集群中。这也很容易理解,因为 HBase 是一个分布式环境,Client 会访问不同 Region Server 获取数据。如果数据排布均匀在不同的多个节点,那么在批量的 Client 便可以从不同的 Region Server 上获取数据,而不是瓶颈在某一个节点,性能自然会有所提升。对于具体的建议我们一般有几条:

  1. 当客户端需要频繁的写一张表,随机的 RowKey 会获得更好的性能。
  2. 当客户端需要频繁的读一张表,有序的 RowKey 则会获得更好的性能。
  3. 对于时间连续的数据(例如 log),有序的 RowKey 会很方便查询一段时间的数据(Scan 操作)。

上面我们谈及了对 Row-Key 的设计,接着我们需要想想是否 Column Family 也会在不同的场景需要不同的设计方案呢。答案是肯定的,不过 CF 跟 Row-key 比较的话,确实也简单一些,但这并不意味着 CF 的设计就是一个琐碎的话题。在 RDBMS(传统关系数据库)系统中,我们知道如果当用户的信息分散在不同的表中,便需要根据一个 Key 进行 Join 操作。而在 HBase 中,我们需要设计 CF 来聚合用户所有相关信息。简单来说,就是需要将数据按类别(或者一个特性)聚合在一个或多个 CF 中。这样,便可以根据 CF 获取这类信息。上面,我们讲解过一个 Region 对应于一个 CF。那么设想,如果在一个表中定义了多个 CF 时,就必然会有多个 Region。当 Client 查询数据时,就不得不查询多个 Region。这样性能自然会有所下降,尤其当 Region 夸机器的时候。因此在大多数的情况下,一个表格不会超过 2 到 3 个 CF,而且很多情况下都是 1 个 CF 就足够了。

单例模式

单例模式

单例模式分为了饿汉式和懒汉式,总体来说懒汉式要优于饿汉式,饿汉式不管是否其他线程调用了getInstance,都在类加载阶段创建了实例。而懒汉式则只有在调用的时候,才实例化对象,更加节省系统资源。

饿汉式:

1
2
3
4
5
6
7
8
public class Singleton  {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}

public static Singleton getInstance() {
return INSTANCE;
}
}

懒汉式-双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 懒汉式-双重检查
*/
public class SingletonLazy {
private static SingletonLazy instance = null;

private SingletonLazy() {
}

public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}

懒汉式-内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 懒汉式-内部类
*/
public class SingletonLazy1 {


private SingletonLazy1() {
}

private static class InnerSingleton {
private final static SingletonLazy1 instance = new SingletonLazy1();
}


public static SingletonLazy1 getInstance() {

return InnerSingleton.instance;
}

public static void helloworld() {
System.out.println("hello lazy singleton.");
}
}

执行Main方法测试,从输出结果看,只有执行了SingletonLazy1.getInstance()方法,才开始加载内部类SingletonLazy1$InnerSingleton。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws InterruptedException{
SingletonLazy1.helloworld();
Object lock = new Object();
synchronized (lock) {
lock.wait(1000);
}
System.out.println("分割线---------------");
System.out.println(SingletonLazy1.getInstance());
}
}

输出结果:
[Loaded sun.nio.cs.US_ASCII$Decoder from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
分割线---------------
[Loaded sun.misc.VMSupport from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.wsy.learn.designmodel.SingletonLazy1$InnerSingleton from file:/Users/wangsiyuan1/workspace/springtest/target/classes/]
com.wsy.learn.designmodel.SingletonLazy1@27716f4

生产者消费者模式-Java

生产者消费者模式

生产者-消费者模式在服务端编程中,是一种很常见的设计模式,比如消息队列的实现,就是这种思想。本文就是用Java语言编写一个简单的生产者消费者例子,从而引出concurrent包下的阻塞队列和ReentrantLock一些玩法。

##基础知识

首先复习下基础知识,在Java中concurrent包下并发队列分为阻塞队列非阻塞队列,ConcurrentLinkedQueue是非阻塞队列,底层实现用了CAS。阻塞队列包括LinkedBlockingQueue,LinkedBlockingDeque,LinkedTransferQueue,ArrayBlockingQueue,阻塞队列底层是靠ReentrantLock实现。Condition包括await,signal,signalAll,Condition作为条件锁

我们知道Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。

而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。

但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

  • 1、线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。

  • 2、线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。

  • 3、锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
  • 4、线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。
  • 5、signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。

所以,发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。 signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

##生产者-消费者代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 用阻塞队列实现生产-消费者模式
*/
public class Productor {
static LinkedBlockingQueue<Integer> blockQueue = new LinkedBlockingQueue<>(10);

void provide() throws InterruptedException {
for (int i = 0; i < 10; i++) {
blockQueue.offer(i);
System.out.println("生产:" + i);
Thread.sleep(100);
}
}
}
1
2
3
4
5
6
7
8
9
10
/**
* 基于阻塞队列消费者
*/
public class Consumer {
void cusume() throws InterruptedException {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("消费:" + Productor.blockQueue.take());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void test0() {
//创建生产者
Productor productor = new Productor();
//创建消费者
Consumer consumer = new Consumer();
Thread cusumerThread = new Thread(() -> {
try {
consumer.cusume();
} catch (Exception e) {

}

});
cusumerThread.start();

try {
productor.provide();

cusumerThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

输出:
生产:0
消费:0
生产:1
消费:1
生产:2
消费:2
生产:3
消费:3
生产:4
消费:4
生产:5
消费:5
生产:6
消费:6
生产:7
消费:7
消费:8
生产:8
生产:9
消费:9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 自己实现的简单的阻塞队列
*/
public class MyBlockQueue<E> {
private LinkedList<E> queue = new LinkedList<>();
//第一种实现方式,采用了Object wait和notify的方式
private Object lock = new Object();
//第二种实现方法,采用了ReentrantLock获取Condition,通过condition await和signal方式实现
private ReentrantLock reentrantLock = new ReentrantLock();
Condition con = reentrantLock.newCondition();

void offer(E e) {
queue.offer(e);
synchronized (lock) {
lock.notifyAll();
}
}

E take() throws InterruptedException {
if (queue.size() == 0) {
synchronized (lock) {
lock.wait();
}
}
return queue.poll();
}

void offer1(E e) throws InterruptedException {

try {
reentrantLock.lockInterruptibly();
queue.offer(e);
con.signalAll();

} finally {
reentrantLock.unlock();
}
}

E take1() throws InterruptedException {
if (queue.size() == 0) {
try {
reentrantLock.lockInterruptibly();
con.await();

} finally {
reentrantLock.unlock();
}
}
return queue.poll();
}
}

Java线程基础知识

Java线程基础

如下图所示,Java中线程可分为NEW,RUNABLE,RUNING,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 共七个状态,一个状态是如何过渡到另一个状态图中标识的很清楚。

img

  • 初始状态(NEW)
    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

  • 就绪状态(RUNNABLE)
    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

    • 调用线程的start()方法,此线程进入就绪状态。
    • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  • 运行中状态(RUNNING)
    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

  • 阻塞状态(BLOCKED)
    阻塞状态是线程阻塞在进入synchronized关键字(当然也包括ReentrantLock)修饰的方法或代码块(获取锁)时的状态。

  • 等待(WAITING)
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  • 超时等待(TIMED_WAITING)
    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  • 终止状态(TERMINATED)
    当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

Java线程常用API

img

其中常用的方法为start(),yield(),sleep(),interrupt(),interrupted(),isInterrupted(),isAlive(),join(),

setDaemon(),setName(),setPriority(),其中stop方法和destroy方法,都是被废弃的方法在日常使用中不建议用。除了Thread类下的API,Object类下的wait(),notify(),notifyAll(),这三个方法也经常在多线程场景中出现。本文的目的,主要讲解的就是这些方法的使用和内部原理。

方法 方法说明 Exception
Thread.start 开启执行线程,由虚拟机负责调用run方法。(Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.) IllegalThreadStateException if the thread was already started.
Thread.yield 让出CPU,但是仅仅对同线程级别(Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilise a CPU)
Thread.sleep 使得正在执行的当前线程睡眠。敲黑板!但是不会让出任何锁的所有权( The thread does not lose ownership of any monitors.)这个特性很重要,也就是说在同步块中使用Thread.sleep要谨慎。 当线程中断会抛出InterruptedException异常,并同时清空中断标志位
Thread.interrupt 中断线程,但只是设置了中断标志位,此刻调用isInterrupted返回true。例子请参考下面的示例代码testInterrupt0方法。只会打印到0-9循环跳出 SecurityException if the current thread cannot modify this thread 请教这个异常什么时候会发生呢?
Thread.isInterrupted 查看线程是否处于中断状态.true为中断。调用之后不清除中断标志位。
Thread.interrupted 查看线程是否处于中断状态.true为中断。调用之后清除中断标志位。心细的同学已经发现和isInterrupted的区别了吧。
Thread.isAlive 线程是否存活,A thread is alive if it has been started and has not yet died.
Thread.join 等待线程死亡之后再执行。(Waits for this thread to die) 当线程中断会抛出InterruptedException异常,并同时清空中断标志位
Thread.setDaemon 设置为守护线程。任何非守护线程还在运行,守护线程就不会终止,最典型的守护线程是垃圾回收器的回收线程。 IllegalThreadStateException 当线程状态是alive的时候不能调用setDaemon
Thread.setName 设置线程的name
Thread.setPriority 设置线程的优先级。MIN_PRIORITY为1,MAX_PRIORITY为10,NORM_PRIORITY为5。
Object.wait 如果不指定timeout,则一直阻塞直到其他线程调用notify或者notifyAll。敲黑板!调用wait之后当前线程不在持有对象锁。
Object.notify 随机唤醒同一个对象monitor下的某个线程
Object.notifyAll 唤醒同一个对象monitor下的所有线程。查看testNotify示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void testInterrupt0()  Exception {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("loop" + i++);
if(i == 10) {
Thread.currentThread().interrupt();
}
}
//echo true
System.out.println(Thread.currentThread().isInterrupted());
//echo true
System.out.println(Thread.currentThread().interrupted());
//echo false
System.out.println(Thread.currentThread().isInterrupted());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThread extends Thread {
private Object lock;
private String name;

public MyThread(Object lock, String name) {
this.lock = lock;
this.name = name;
}

@Override
public void run() {
synchronized (lock) {
try {
System.out.println(name + " get lock,interrupt =" + Thread.currentThread().isInterrupted());

lock.wait();

//Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(name + " is interrupt. notify all interrupt =" + Thread.currentThread().isInterrupted());
lock.notifyAll();
}
System.out.println(name + ":notified...");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void testNotify() throws Exception {
MyThread t1 = new MyThread(lock, "t1");
MyThread t2 = new MyThread(lock, "t2");

Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);

thread1.start();
thread2.start();

Thread.sleep(1000);
long startTime = System.currentTimeMillis();
synchronized (lock) {
System.out.println("main get lock");
lock.notifyAll();
}

thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();

System.out.println("notify lock.time =" + (endTime - startTime));
}

testNotify执行结果

t1 get lock,interrupt =false
t2 get lock,interrupt =false
main get lock
t2:notified…
t1:notified…

Java对象刨根问底

如何创建一个对象

Java创建一个对象,最简单的方式就是

1
Object a = new Object();

可以,大家有没有想过,在JVM中,创建一个对象的流程是如何呢?其实具体流程入下图所示。

img

###分配内存

对象所需的内存大小,在类加载完成后便可以完全确认,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。那么如何划分,有两种方式:

  • 指针碰撞(Bump The Pointer):假设Java堆中的内存是绝对规整的,所有用过的内存都放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就是朝着空闲的那一边“挪动”与对象大小一样的距离。
  • 空闲列表(Free List):如果Java堆中的内存不规整,已使用的内存和未使用的内存相互交错,就没办法简单进行指针碰撞了,需要维护一个列表,记录哪些内存块是可用的,在分配的时候,找一块足够大的空间分配给对象,并更新列表的记录。

因此,在使用Serial,ParNew等带Compact过程的收集器时,系统采用的分配方式是指针碰撞法,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的是空闲列表法。

除了分配方法外,还需要考虑同步的问题。实际上虚拟机采用了CAS配上失败重试的方式保证更新的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),可以通过-XX:+UseTLAB参数来设定。

对象内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

HotSpot虚拟机对象头包括两部分内容:

  • 第一部分

用于存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分长度在32bit和64bit虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向
  • 第二部分

类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

对象访问定位

有两种方式,句柄访问直接指针访问。这两种访问方式各有优点,使用句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收移动对象是很常见的事情)时只会改变句柄中的实例数据指针,而reference本身不需要更新。使用直接指针访问方式最大好处就是速度更快,它节省一次指针定位的时间开销,由于对象访问在Java中非常频繁,这类开销积少成多,将是一个比较可观的提升。HotSpot就是采用直接指针法。

img

​ 通过句柄访问对象

img

​ 通过直接指针访问对象

java类加载内部流程

Java类加载机制的七个阶段

而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

##加载

下面是对于加载过程最为官方的描述。

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:

  • JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。

当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。

##准备(重点)

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

1
2
public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

1
public static int sector = 3;

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

1
public static final int number = 3;

之所以 static final 会直接被复制,而 static 变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 number 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

##初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

##使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

##卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

总结

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

引用

https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html