MySQL多线程复制问题处理之Error_code: 1872

September 26th, 2016    阅读(0) Comments off

上周在生产环境上遇到一个问题,不敢独享,拿出来给小伙伴们做个简单的分享。

起因:由于IDC机房断电(估计又是哪里被挖掘机碰了下吧),导致所有服务器重启,影响到了其中的MySQL数据库。来看下这时数据库遇到的问题:
数据库版本:MySQL 5.7.10[……]

阅读全文

Categories: MySQL Tags:

Parquet与ORC:高性能列式存储格式

September 26th, 2016    阅读(27) No comments

背景

随着大数据时代的到来,越来越多的数据流向了Hadoop生态圈,同时对于能够快速的从TB甚至PB级别的数据中获取有价值的数据对于一个产品和公司来说更加重要,在Hadoop生态圈的快速发展过程中,涌现了一批开源的数据分析引擎,例如Hive、Spark SQL、Impala、Presto等,同时也产生了多个高性能的列式存储格式,例如RCFile、ORC、Parquet等,本文主要从实现的角度上对比分析ORC和Parquet两种典型的列存格式,并对它们做了相应的对比测试。

列式存储

由于OLAP查询的特点,列式存储可以提升其查询性能,但是它是如何做到的呢?这就要从列式存储的原理说起,从图1中可以看到,相对于关系数据库中通常使用的行式存储,在使用列式存储时每一列的所有元素都是顺序存储的。由此特点可以给查询带来如下的优化:

  • 查询的时候不需要扫描全部的数据,而只需要读取每次查询涉及的列,这样可以将I/O消耗降低N倍,另外可以保存每一列的统计信息(min、max、sum等),实现部分的谓词下推。
  • 由于每一列的成员都是同构的,可以针对不同的数据类型使用更高效的数据压缩算法,进一步减小I/O。
  • 由于每一列的成员的同构性,可以使用更加适合CPU pipeline的编码方式,减小CPU的缓存失效。

行式存储VS列式存储
图1 行式存储VS列式存储

嵌套数据格式

通常我们使用关系数据库存储结构化数据,而关系数据库支持的数据模型都是扁平式的,而遇到诸如List、Map和自定义Struct的时候就需要用户自己解析,但是在大数据环境下,数据的来源多种多样,例如埋点数据,很可能需要把程序中的某些对象内容作为输出的一部分,而每一个对象都可能是嵌套的,所以如果能够原生的支持这种数据,查询的时候就不需要额外的解析便能获得想要的结果。例如在Twitter,他们一个典型的日志对象(一条记录)有87个字段,其中嵌套了7层,如下图。

嵌套数据模型
图2 嵌套数据模型
随着嵌套格式的数据的需求日益增加,目前Hadoop生态圈中主流的查询引擎都支持更丰富的数据类型,例如Hive、SparkSQL、Impala等都原生的支持诸如struct、map、array这样的复杂数据类型,这样促使各种存储格式都需要支持嵌套数据格式。

Parquet存储格式

Apache Parquet是Hadoop生态圈中一种新型列式存储格式,它可以兼容Hadoop生态圈中大多数计算框架(Mapreduce、Spark等),被多种查询引擎支持(Hive、Impala、Drill等),并且它是语言和平台无关的。Parquet最初是由Twitter和Cloudera合作开发完成并开源,2015年5月从Apache的孵化器里毕业成为Apache顶级项目。

Parquet最初的灵感来自Google于2010年发表的Dremel论文,文中介绍了一种支持嵌套结构的存储格式,并且使用了列式存储的方式提升查询性能,在Dremel论文中还介绍了Google如何使用这种存储格式实现并行查询的,如果对此感兴趣可以参考论文和开源实现Drill。

数据模型

Parquet支持嵌套的数据模型,类似于Protocol Buffers,每一个数据模型的schema包含多个字段,每一个字段有三个属性:重复次数、数据类型和字段名,重复次数可以是以下三种:required(只出现1次),repeated(出现0次或多次),optional(出现0次或1次)。每一个字段的数据类型可以分成两种:group(复杂类型)和primitive(基本类型)。例如Dremel中提供的Document的schema示例,它的定义如下:

message Document {
  required int64 DocId;
  optional group Links {
    repeated int64 Backward;
    repeated int64 Forward; 
  }
  repeated group Name {
    repeated group Language {
      required string Code;
      optional string Country; 
     }
    optional string Url; 
  }
}

可以把这个Schema转换成树状结构,根节点可以理解为repeated类型,如图3。

Parquet的schema
图3 Parquet的schema结构
可以看出在Schema中所有的基本类型字段都是叶子节点,在这个Schema中一共存在6个叶子节点,如果把这样的Schema转换成扁平式的关系模型,就可以理解为该表包含六个列。Parquet中没有Map、Array这样的复杂数据结构,但是可以通过repeated和group组合来实现的。由于一条记录中某一列可能出现零次或者多次,需要标示出哪些列的值构成一条完整的记录。这是由Striping/Assembly算法实现的。

由于Parquet支持的数据模型比较松散,可能一条记录中存在比较深的嵌套关系,如果为每一条记录都维护一个类似的树状结可能会占用较大的存储空间,因此Dremel论文中提出了一种高效的对于嵌套数据格式的压缩算法:Striping/Assembly算法。它的原理是每一个记录中的每一个成员值有三部分组成:Value、Repetition level和Definition level。value记录了该成员的原始值,可以根据特定类型的压缩算法进行压缩,两个level值用于记录该值在整个记录中的位置。对于repeated类型的列,Repetition level值记录了当前值属于哪一条记录以及它处于该记录的什么位置;对于repeated和optional类型的列,可能一条记录中某一列是没有值的,假设我们不记录这样的值就会导致本该属于下一条记录的值被当做当前记录的一部分,从而造成数据的错误,因此对于这种情况需要一个占位符标示这种情况。

通过Striping/Assembly算法,parquet可以使用较少的存储空间表示复杂的嵌套格式,并且通常Repetition level和Definition level都是较小的整数值,可以通过RLE算法对其进行压缩,进一步降低存储空间。

文件结构

Parquet文件是以二进制方式存储的,是不可以直接读取和修改的,Parquet文件是自解析的,文件中包括该文件的数据和元数据。在HDFS文件系统和Parquet文件中存在如下几个概念:

  • HDFS块(Block):它是HDFS上的最小的副本单位,HDFS会把一个Block存储在本地的一个文件并且维护分散在不同的机器上的多个副本,通常情况下一个Block的大小为256M、512M等。
  • HDFS文件(File):一个HDFS的文件,包括数据和元数据,数据分散存储在多个Block中。
  • 行组(Row Group):按照行将数据物理上划分为多个单元,每一个行组包含一定的行数,在一个HDFS文件中至少存储一个行组,Parquet读写的时候会将整个行组缓存在内存中,所以如果每一个行组的大小是由内存大的小决定的。
  • 列块(Column Chunk):在一个行组中每一列保存在一个列块中,行组中的所有列连续的存储在这个行组文件中。不同的列块可能使用不同的算法进行压缩。
  • 页(Page):每一个列块划分为多个页,一个页是最小的编码的单位,在同一个列块的不同页可能使用不同的编码方式。

通常情况下,在存储Parquet数据的时候会按照HDFS的Block大小设置行组的大小,由于一般情况下每一个Mapper任务处理数据的最小单位是一个Block,这样可以把每一个行组由一个Mapper任务处理,增大任务执行并行度。Parquet文件的格式如下图所示。

Parquet文件结构
图4 Parquet文件结构
上图展示了一个Parquet文件的结构,一个文件中可以存储多个行组,文件的首位都是该文件的Magic Code,用于校验它是否是一个Parquet文件,Footer length存储了文件元数据的大小,通过该值和文件长度可以计算出元数据的偏移量,文件的元数据中包括每一个行组的元数据信息和当前文件的Schema信息。除了文件中每一个行组的元数据,每一页的开始都会存储该页的元数据,在Parquet中,有三种类型的页:数据页、字典页和索引页。数据页用于存储当前行组中该列的值,字典页存储该列值的编码字典,每一个列块中最多包含一个字典页,索引页用来存储当前行组下该列的索引,目前Parquet中还不支持索引页,但是在后面的版本中增加。

数据访问

说到列式存储的优势,Project下推是无疑最突出的,它意味着在获取表中原始数据时只需要扫描查询中需要的列,由于每一列的所有值都是连续存储的,避免扫描整个表文件内容。

在Parquet中原生就支持Project下推,执行查询的时候可以通过Configuration传递需要读取的列的信息,这些列必须是Schema的子集,Parquet每次会扫描一个Row Group的数据,然后一次性得将该Row Group里所有需要的列的Cloumn Chunk都读取到内存中,每次读取一个Row Group的数据能够大大降低随机读的次数,除此之外,Parquet在读取的时候会考虑列是否连续,如果某些需要的列是存储位置是连续的,那么一次读操作就可以把多个列的数据读取到内存。

在数据访问的过程中,Parquet还可以利用每一个row group生成的统计信息进行谓词下推,这部分信息包括该Column Chunk的最大值、最小值和空值个数。通过这些统计值和该列的过滤条件可以判断该Row Group是否需要扫描。另外Parquet未来还会增加诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推。

ORC文件格式

ORC文件格式是一种Hadoop生态圈中的列式存储格式,它的产生早在2013年初,最初产生自Apache Hive,用于降低Hadoop数据存储空间和加速Hive查询速度。和Parquet类似,它并不是一个单纯的列式存储格式,仍然是首先根据行组分割整个表,在每一个行组内进行按列存储。ORC文件是自描述的,它的元数据使用Protocol Buffers序列化,并且文件中的数据尽可能的压缩以降低存储空间的消耗,目前也被Spark SQL、Presto等查询引擎支持,但是Impala对于ORC目前没有支持,仍然使用Parquet作为主要的列式存储格式。2015年ORC项目被Apache项目基金会提升为Apache顶级项目。

数据模型

和Parquet不同,ORC原生是不支持嵌套数据格式的,而是通过对复杂数据类型特殊处理的方式实现嵌套格式的支持,例如对于如下的hive表:

CREATE TABLE `orcStructTable`(
  `name` string,
  `course` struct<course:string,score:int>,
  `score` map<string,int>,
  `work_locations` array<string>)

ORC格式会将其转换成如下的树状结构:

26265680

图5 ORC的schema结构
在ORC的结构中这个schema包含10个column,其中包含了复杂类型列和原始类型的列,前者包括LIST、STRUCT、MAP和UNION类型,后者包括BOOLEAN、整数、浮点数、字符串类型等,其中STRUCT的孩子节点包括它的成员变量,可能有多个孩子节点,MAP有两个孩子节点,分别为key和value,LIST包含一个孩子节点,类型为该LIST的成员类型,UNION一般不怎么用得到。每一个Schema树的根节点为一个Struct类型,所有的column按照树的中序遍历顺序编号。

ORC只需要存储schema树中叶子节点的值,而中间的非叶子节点只是做一层代理,它们只需要负责孩子节点值得读取,只有真正的叶子节点才会读取数据,然后交由父节点封装成对应的数据结构返回。

文件结构

和Parquet类似,ORC文件也是以二进制方式存储的,所以是不可以直接读取,ORC文件也是自解析的,它包含许多的元数据,这些元数据都是同构ProtoBuffer进行序列化的。ORC的文件结构入图6,其中涉及到如下的概念:

  • ORC文件:保存在文件系统上的普通二进制文件,一个ORC文件中可以包含多个stripe,每一个stripe包含多条记录,这些记录按照列进行独立存储,对应到Parquet中的row group的概念。
  • 文件级元数据:包括文件的描述信息PostScript、文件meta信息(包括整个文件的统计信息)、所有stripe的信息和文件schema信息。
  • stripe:一组行形成一个stripe,每次读取文件是以行组为单位的,一般为HDFS的块大小,保存了每一列的索引和数据。
  • stripe元数据:保存stripe的位置、每一个列的在该stripe的统计信息以及所有的stream类型和位置。
  • row group:索引的最小单位,一个stripe中包含多个row group,默认为10000个值组成。
  • stream:一个stream表示文件中一段有效的数据,包括索引和数据两类。索引stream保存每一个row group的位置和统计信息,数据stream包括多种类型的数据,具体需要哪几种是由该列类型和编码方式决定。

ORC文件结构
图6 ORC文件结构
在ORC文件中保存了三个层级的统计信息,分别为文件级别、stripe级别和row group级别的,他们都可以用来根据Search ARGuments(谓词下推条件)判断是否可以跳过某些数据,在统计信息中都包含成员数和是否有null值,并且对于不同类型的数据设置一些特定的统计信息。

数据访问

读取ORC文件是从尾部开始的,第一次读取16KB的大小,尽可能的将Postscript和Footer数据都读入内存。文件的最后一个字节保存着PostScript的长度,它的长度不会超过256字节,PostScript中保存着整个文件的元数据信息,它包括文件的压缩格式、文件内部每一个压缩块的最大长度(每次分配内存的大小)、Footer长度,以及一些版本信息。在Postscript和Footer之间存储着整个文件的统计信息(上图中未画出),这部分的统计信息包括每一个stripe中每一列的信息,主要统计成员数、最大值、最小值、是否有空值等。

接下来读取文件的Footer信息,它包含了每一个stripe的长度和偏移量,该文件的schema信息(将schema树按照schema中的编号保存在数组中)、整个文件的统计信息以及每一个row group的行数。

处理stripe时首先从Footer中获取每一个stripe的其实位置和长度、每一个stripe的Footer数据(元数据,记录了index和data的的长度),整个striper被分为index和data两部分,stripe内部是按照row group进行分块的(每一个row group中多少条记录在文件的Footer中存储),row group内部按列存储。每一个row group由多个stream保存数据和索引信息。每一个stream的数据会根据该列的类型使用特定的压缩算法保存。在ORC中存在如下几种stream类型:

  • PRESENT:每一个成员值在这个stream中保持一位(bit)用于标示该值是否为NULL,通过它可以只记录部位NULL的值
  • DATA:该列的中属于当前stripe的成员值。
  • LENGTH:每一个成员的长度,这个是针对string类型的列才有的。
  • DICTIONARY_DATA:对string类型数据编码之后字典的内容。
  • SECONDARY:存储Decimal、timestamp类型的小数或者纳秒数等。
  • ROW_INDEX:保存stripe中每一个row group的统计信息和每一个row group起始位置信息。

在初始化阶段获取全部的元数据之后,可以通过includes数组指定需要读取的列编号,它是一个boolean数组,如果不指定则读取全部的列,还可以通过传递SearchArgument参数指定过滤条件,根据元数据首先读取每一个stripe中的index信息,然后根据index中统计信息以及SearchArgument参数确定需要读取的row group编号,再根据includes数据决定需要从这些row group中读取的列,通过这两层的过滤需要读取的数据只是整个stripe多个小段的区间,然后ORC会尽可能合并多个离散的区间尽可能的减少I/O次数。然后再根据index中保存的下一个row group的位置信息调至该stripe中第一个需要读取的row group中。

由于ORC中使用了更加精确的索引信息,使得在读取数据时可以指定从任意一行开始读取,更细粒度的统计信息使得读取ORC文件跳过整个row group,ORC默认会对任何一块数据和索引信息使用ZLIB压缩,因此ORC文件占用的存储空间也更小,这点在后面的测试对比中也有所印证。

在新版本的ORC中也加入了对Bloom Filter的支持,它可以进一步提升谓词下推的效率,在Hive 1.2.0版本以后也加入了对此的支持。

性能测试

为了对比测试两种存储格式,我选择使用TPC-DS数据集并且对它进行改造以生成宽表、嵌套和多层嵌套的数据。使用最常用的Hive作为SQL引擎进行测试。

测试环境

  • Hadoop集群:物理测试集群,四台DataNode/NodeManager机器,每个机器32core+128GB,测试时使用整个集群的资源。
  • Hive:Hive 1.2.1版本,使用hiveserver2启动,本机MySql作为元数据库,jdbc方式提交查询SQL
  • 数据集:100GB TPC-DS数据集,选取其中的Store_Sales为事实表的模型作为测试数据
  • 查询SQL:选择TPC-DS中涉及到上述模型的10条SQL并对其进行改造。

测试场景和结果

整个测试设置了四种场景,每一种场景下对比测试数据占用的存储空间的大小和相同查询执行消耗的时间对比,除了场景一基于原始的TPC-DS数据集外,其余的数据都需要进行数据导入,同时对比这几个场景的数据导入时间。

场景一:一个事实表、多个维度表,复杂的join查询。

基于原始的TPC-DS数据集。

Store_Sales表记录数:287,997,024,表大小为:

  • 原始Text格式,未压缩 : 38.1 G
  • ORC格式,默认压缩(ZLIB),一共1800+个分区 : 11.5 G
  • Parquet格式,默认压缩(Snappy),一共1800+个分区 : 14.8 G

查询测试结果:

场景一结果

场景二:维度表和事实表join之后生成的宽表,只在一个表上做查询。

整个测试设置了四种场景,每一种场景下对比测试数据占用的存储空间的大小和相同查询执行消耗的时间对比,除了场景一基于原始的TPC-DS数据集外,其余的数据都需要进行数据导入,同时对比这几个场景的数据导入时间。选取数据模型中的store_sales, household_demographics, customer_address, date_dim, store表生成一个扁平式宽表(store_sales_wide_table),基于这个表执行查询,由于场景一种选择的query大多数不能完全match到这个宽表,所以对场景1中的SQL进行部分改造。

store_sales_wide_table表记录数:263,704,266,表大小为:

  • 原始Text格式,未压缩 : 149.0 G
  • ORC格式,默认压缩 : 10.6 G
  • PARQUET格式,默认压缩 : 12.5 G

查询测试结果:

场景二结果

场景三:复杂的数据结构组成的宽表,struct、list、map等(1层)

整个测试设置了四种场景,每一种场景下对比测试数据占用的存储空间的大小和相同查询执行消耗的时间对比,除了场景一基于原始的TPC-DS数据集外,其余的数据都需要进行数据导入,同时对比这几个场景的数据导入时间。在场景二的基础上,将维度表(除了store_sales表)转换成一个struct或者map对象,源store_sales表中的字段保持不变。生成有一层嵌套的新表(store_sales_wide_table_one_nested),使用的查询逻辑相同。

store_sales_wide_table_one_nested表记录数:263,704,266,表大小为:

  • 原始Text格式,未压缩 : 245.3 G
  • ORC格式,默认压缩 : 10.9 G 比store_sales表还小?
  • PARQUET格式,默认压缩 : 29.8 G

查询测试结果:

场景三结果

场景四:复杂的数据结构,多层嵌套。(3层)

整个测试设置了四种场景,每一种场景下对比测试数据占用的存储空间的大小和相同查询执行消耗的时间对比,除了场景一基于原始的TPC-DS数据集外,其余的数据都需要进行数据导入,同时对比这几个场景的数据导入时间。在场景三的基础上,将部分维度表的struct内的字段再转换成struct或者map对象,只存在struct中嵌套map的情况,最深的嵌套为三层。生成一个多层嵌套的新表(store_sales_wide_table_more_nested),使用的查询逻辑相同。

该场景中只涉及一个多层嵌套的宽表,没有任何分区字段,store_sales_wide_table_more_nested表记录数:263,704,266,表大小为:

  • 原始Text格式,未压缩 : 222.7 G
  • ORC格式,默认压缩 : 10.9 G 比store_sales表还小?
  • PARQUET格式,默认压缩 : 23.1 G 比一层嵌套表store_sales_wide_table_one_nested要小?

查询测试结果:

场景四结果

结果分析

从上述测试结果来看,星状模型对于数据分析场景并不是很合适,多个表的join会大大拖慢查询速度,并且不能很好的利用列式存储带来的性能提升,在使用宽表的情况下,列式存储的性能提升明显,ORC文件格式在存储空间上要远优于Text格式,较之于PARQUET格式有一倍的存储空间提升,在导数据(insert into table select 这样的方式)方面ORC格式也要优于PARQUET,在最终的查询性能上可以看到,无论是无嵌套的扁平式宽表,或是一层嵌套表,还是多层嵌套的宽表,两者的查询性能相差不多,较之于Text格式有2到3倍左右的提升。

另外,通过对比场景二和场景三的测试结果,可以发现扁平式的表结构要比嵌套式结构的查询性能有所提升,所以如果选择使用大宽表,则设计宽表的时候尽可能的将表设计的扁平化,减少嵌套数据。

通过这三种文件存储格式的测试对比,ORC文件存储格式无论是在空间存储、导数据速度还是查询速度上表现的都较好一些,并且ORC可以一定程度上支持ACID操作,社区的发展目前也是Hive中比较提倡使用的一种列式存储格式,另外,本次测试主要针对的是Hive引擎,所以不排除存在Hive与ORC的敏感度比PARQUET要高的可能性。

总结

本文主要从数据模型、文件格式和数据访问流程等几个方面详细介绍了Hadoop生态圈中的两种列式存储格式——Parquet和ORC,并通过大数据量的测试对两者的存储和查询性能进行了对比。对于大数据场景下的数据分析需求,使用这两种存储格式总会带来存储和性能上的提升,但是在实际使用时还需要针对实际的数据进行选择。另外由于不同开源产品可能对不同的存储格式有特定的优化,所以选择时还需要考虑查询引擎的因素。

Categories: Uncategorized Tags:

HBase最佳实践 – 多租户机制(上)

September 26th, 2016    阅读(0) Comments off

背景介绍

在HBase1.1.0发布之前,HBase同一集群上的用户、表都是平等的,没有优劣之分。这种’大同’社会看起来完美,实际上有很多问题。最棘手的主要有这么两个,其一是某些业务较其他业务重要,需要在资源有限的情况下优先保证核心重要业务的正常运行,其二是有些业务在某些场景下会时常’抽风’,QPS常常居高不下,严重消耗系统资源,导致其他业务无法正常运转。
这实际上是典型的多租户问题,社区针对这个问题提出了相应的应对措施,主要有如下三点:

(1)资源限制,主要针对用户、namespace以及表的QPS和请求大小进行限制,详见HBase-11598
(2)资源调度,主要针对任务进行优先级调度,通常会优先调度实时交互而且小的任务,而批量操作任务或者长时间操作任务(大scan)优先级相对较低,详见HBase-10993
(3)资源隔离,将不同表通过物理隔离的方式分布到不同的RegionServer上,详见HBase-6721

本文将会重点介绍HBase中的资源限制方案 – Quotas,主要对其使用方式、实现原理进行介绍,并对其实际效果通过实践进行验证。另外,本文还会对HBase的资源调度原理进行简单介绍,并对主要配置进行讲解。


资源限制-Quotas

Quotas使用条件

(1)HBase版本在1.1.0以上,或者低版本HBase应用了对应的Patch(HBase-11598)
(2)Quotas功能默认是关闭的,需要在配置文件hbase-site.xml中通过设置hbase.quota.enabled为true打开。设置完成之后,需要重启HMaster才能生效。

Quotas语句详解

hbase> set_quota TYPE => THROTTLE, THROTTLE_TYPE => READ, USER => 'u1', TABLE => 't2', LIMIT => '10req/sec'

(1)Quotas分别支持表级别以及用户级别资源限制,或者同时支持表级别和用户级别,如示例所示
(2)THROTTLE_TYPE可以取值READ / WRITE,分别对随机读和随机写进行限制
(3)LIMIT 可以从两个维度对资源进行限制,分别为req/time 和 size/time,前者限制单位时间内的请求数,后者限制单位时间内的请求数据量。需要指明的是time的单位可以是sec | min | hour | day,size的单位可以是B(bytes) | K | M | G | T | P,因此LIMIT可以表示为’1000req/min’或者’100G/day’,分别表示’限制1分钟的请求数在1000次以内’,’限制一台的数据量为100G’


常用Quotas语句

hbase> set_quota TYPE => THROTTLE, TABLE => 't1', LIMIT => '1000req/sec'
hbase> set_quota TYPE => THROTTLE, THROTTLE_TYPE => WRITE, USER => 'u1', LIMIT => '10M/sec'

注意事项

(1)set_quota命令执行的限制都是针对单个RegionServer来说的,并不是针对整个集群
(2)set_quota命令默认执行后并不会立刻生效,需要等待一段时间才会生效,等待时间默认为5min。可以通过参数 hbase.quota.refresh.period 进行设置,比如可以通过设置
hbase.quota.refresh.period = 60000将生效时间缩短为1min
(3)可以通过命令list_quotas查看当前所有执行的set_quota命令

Quotas – 实现原理

原理很简单,如果请求数超过设置的Quota数,就抛出异常!有同学会说也没在日志中看到任何异常嘛,这是因为这类异常日志级别是debug,而默认的日志输出级别为info,可以通过调整log4j来查看。但是这类异常实在太多,没有必要输出。

Quotas – 实践效果

了解了Quotas的使用方法以及基本原理,是不是很想试一试它的功效,笔者在测试环境做了如下的测试:

1. 测试硬件情况
集群规模
RS JVM内存配置
硬盘
HBase版本
YCSB版本
4台RegionServer
72G
12 * 3.6T
1.1.2
0.8.0

2. 测试环境新建两张表,分别称为A和B。两张表的数据构成都相同,10亿条数据,每条数据500Bytes,总大小500G左右。
3. 分别使用两个YCSB客户端分别对这两张表执行读写混合操作(读写比为1:1),再然后对B表不断执行set_quota操作,对该表QPS进行限制。再分别观察A表和B表的QPS以及读延迟变化情况。
4. 为了方便理解,下面测试结果中A表称为Unthrottle_Table,B表称为Throttle_Table。测试结果如下:


clipboard


通过测试基本可以看出,随着B表执行的QPS限制越来越严格,上图中Throttle_Table表对应的吞吐量(红色柱状图)越来越小,相应Unthrottle_Table表(紫色柱状图)对应的吞吐量却越来越大,这是因为B表执行QPS限制之后各种硬件资源就会更多地分配给A表。
总体来说,Quotas功能总体看来基本完成了资源限制的职能,达到了资源限制的目的。同时支持用户级别和表级别,另外同时支持请求大小和请求数量两个维度,基本涵盖了常见的资源限制维度;另外,易用性也是一大亮点,比较人性化,只需要在Shell界面上敲一行命令就可以搞定。


资源调度

在 0.99版本之前,HBase只提供了一种请求队列类型:FIFO队列,意为先到的请求会优先被处理,后到的请求需要等待之前的请求被处理完。这样的设计有一个致命的缺陷,就是在线交互式查询有可能会被离线大scan长时间阻塞,而从优先级的角度讲在线交互式查询无疑更加重要。

0.99版本之后,HBase将默认请求队列由FIFO类型改为了Deadline类型,用来解决上述缺陷。提起DeadLine队列,很多对Linux  IO调度算法比较了解的同学并不陌生,Linux IO常用调度算法主要有Noop、CFQ(Completely Fair Queuing)以及Deadline,其中Noop调度算法基本可以认为就是FIFO算法,因此同样存在上述弊端;而CFQ算法会按照IO请求的地址进行排序,这样处理的目的在于尽量少地减少磁盘移动,实际效果来看确实极大的提升了IO的吞吐率,但是相比Noop,部分IO请求有可能会一直排到队尾,存在饿死的情况。Deadline算法首先将读写IO队列进行了分离,而且读IO优先级要高于写IO优先级;除此之外,它还会为每一个IO请求设置一个时间戳,用以判断请求是否长时间没有得到处理,进而需要优先处理。需要知道的是,对于常见数据库环境来说(Oracle,MySQL等),Deadline算法总是最佳选择。

那HBase新增的Deadline算法和Linux IO中Deadline算法是否一样呢?答案是肯定的,至少两者实现思路基本是一致的。接下来主要结合HBase请求调度源码对Deadline算法进行深入分析。
Deadline算法基于Deadline类型队列实现,Deadline类型队列和FIFO类型队列不同,属于优先级队列,里面的元素会按照优先级进行排序,优先级高的排在队首,优先级低的排在队尾。很显然,Deadline算法目标是使得在线交互式查询请求优先级更高,而离线长scan请求优先级更低。除此之外还有一个通常不会被注意的目标:不能出现任何请求被饿死!在弄懂具体的实现机制前,需要首先搞清楚一个问题:如何量化一个scan的请求长短?

如何量化一个scan的请求长短:这个问题的理解需要对scan的流程有一个大体认识,一次scan请求并不会将所有数据查询返回,这一方面是因为在数据量大的场景下诸如带宽之类的系统资源会被严重消耗,另一方面也有可能会因为数据量大导致客户端OOM。因此HBase实际上将一次scan请求分为多次连续的next小请求执行,每次查询纪录数用户可以配置,默认为100条。这样假如一次scan查询总纪录数为1000,每次查询返回100条,就需要10次客户端到服务器端的next请求。看到这里,很多童鞋已经明白,可以通过当前RPC请求次数(即next RPC调用次数)粗略地衡量scan的长短,比如当前scanA的RPC请求次数为10,scanB的RPC请求次数为5,就可以认为scanA长于scanB,那理论上scanA的这次请求优先级就会低于scanB的这次请求。
HBase在具体实现中会为每一个请求设置一个deadline(时间期限),代表这个请求的处理期限,deadline越小,请求优先级越高。

2
626344.png

这个deadline参数是理解HBase资源调度的关键,它由两部分构成:后半部分的核心在于vtime,代表当前scan的next请求次数,可见vtime越大(scan越长),对应的deadline越大,优先级越低;因为设定get操作的vtime为0,因此同等条件下get操作优先级最高;可见,通过vtime就可以实现请求优先级功能。那对于长scan,会不会出现因为优先级太低长时间得不到处理饿死的情况呢?这就需要看看前半部分,timestamp表示请求点的绝对时间戳,设置绝对时间戳是为了保证该请求的deadline肯定早于5s(等式后面部分最大就是5s)之后所有请求的deadline,从而能够保证不会被饿死;


好吧,上面不是说Linux IO调度系统中Deadline算法还实现了读IO和写IO的分离,那HBase实现了么?当然,用户只需要通过简单的配置就不仅可以实现读请求和写请求的分离,还可以实现了scan请求的分离。

默认场景下,HBase只提供一个队列,所有请求都会进入该队列进行优先级排序。用户可以通过设置参数hbase.ipc.server.callqueue.handler.factor来设置多个队列,队列个数等于该参数 * handlercount,比如该参数设置为0.1,总的handlercount胃150,则会产生15个独立队列。
独立队列产生之后,可以通过参数 hbase.ipc.server.callqueue.read.ratio 来设置读写队列比例,比如设置0.6,则表示会有9个队列用于接收读请求,6个用于接收写请求;另外,可以通过参数 hbase.ipc.server.callqueue.scan.ratio 设置get和scan的队列比例,比如设置为0.1,表示1个队列用于scan请求,另外8个用于get请求;



本文主要介绍了HBase中多租户实现中的两个重要手段:资源限制以及资源调度,对其工作原理以及使用方法进行了解析。后续再针对资源隔离这个重头戏进行深入解析~

Categories: HBase Tags:

MySQL 5.7 OOM问题诊断——就是这么简单

September 22nd, 2016    阅读(0) Comments off

Inside君最近把金庸先生的笑傲江湖重看了三遍,感慨良多。很多工作、管理、生活、学习上的问题都能在其中一窥究竟,而那是年轻时所不能体会的一种感悟。比如下面风清扬的这段话:

风清扬又道:“单以武学而论,这些魔教长老们也不能说真正已窥上乘武学之门。他们不懂[……]

阅读全文

Categories: MySQL Tags:

视频云直播端网络QoS算法总结和技术展望

September 20th, 2016    阅读(72) No comments

一、概述

由于从直播端到RTMP服务器的网络情况复杂,尤其是在3G和带宽较差的Wifi环境下,网络丢包、抖动和延迟经常发生,导致直播推流不畅。RTMP基于TCP进行传输,TCP自身实现了网络拥塞下的处理,内部的机制较为复杂,而且对开发者不可见,开发者无法根据TCP协议的信息判断当时的网络情况,导致发送码率大于实际网络带宽,造成比较严重的网络拥塞。

由于TCP本身是面向连接、可靠地传输层协议,关于RTMP协议的网络QoS讨论较少,所以本文的QoS策略完全是通过不断实验和摸索设计开发的,如有问题欢迎指正。

二、视频云直播端QoS策略

由于上述原因,我们在研究解决直播端的网络拥塞问题时,根据实验结果进行分析,以发送一帧视频数据的时间为依据,判断当前网络拥塞情况。

2.1 策略依据的原理

如图1所示,TCP协议在发送数据时,首先将应用层(采用librtmp标准库)下发的数据缓存在sendbuffer中,然后应用层函数的调用返回,函数的调用是瞬间完成的。TCP协议何时从sendbuffer中取出数据进行发送是由TCP协议本身和当时的网络情况所决定的。如果网络中出现丢包和抖动,导致接收端接收数据超时,会激发发送端数据重传,重传机制本身挤占网络带宽,导致sendbuffer中的数据进一步发送失败,致使sendbuffer中的数据不断增多,达到上溢的警戒线,此时应用层函数下发数据到sendbuffer就不会瞬间完成,而是会等待sendbuffer中的数据低于警戒线,再将数据下发。

图1 TCP发送数据示意图

    因此,可以根据应用层函数写数据到sendbuffer的时间来判断网络的拥塞情况。

2.2 QoS算法1.0版本

2.2.1实现过程

QoS算法1.0版本具体的实现过程是:在网络带宽受限(采用路由器限制)的情况下,测试函数av_interleaved_write_frame的运行时间,如果时间大于阈值1,则认为网络很差,大幅度降低QoS level;如果时间小于阈值1,但是大于阈值2,则认为网络一般差,小幅度降低QoS level;如果连续一段时间,函数的运行时间都小于阈值2,则认为网络情况较好,可以考虑小幅度提高QoS level。

其中,函数av_interleaved_write_frame就是前述的应用层向TCP send buffer下发数据的函数。

算法基本流程如图2所示。

       图2 QoS算法1.0版本基本流程图

2.2.2 实验结果

测试环境:路由器500kbps限速的实验室环境和联通3G的实际环境下。

测试设备:魅族MX3手机,SDK版本:4.2.1。

测试结果如图3和图4所示。其中,横坐标是时间,单位是秒,每秒计算一次数据,并采样画图。纵坐标是码率,单位是kbps。

系列1代表视频实际发送码率,系列2代表音频实际发送码率,系列3代表总的发送码率,系列4代表QoS设置的视频发送码率。

图3 500kbps限速下的QoS算法实验结果

图4 联通3G下的QoS算法实验结果

    如图3所示,当出现网络拥塞时,视频和音频的发送码率都降为0,视频和音频的发送线程都被函数av_interleaved_write_frame挂住,直到该函数返回,QoS才开始发挥作用,进行发送码率的调节。此时,可以看到系列4曲线出现较大幅度的降低,表示设置的发送码率开始降低。。

然后,网络出现一段时间的平稳期,QoS算法开始向上调节网络的发送码率。

2.3 QoS算法1.1版本

在QoS 1.0版本基础上,通过引入微软的网络模拟工具network emulation windows toolkit,进行10分钟的相同视频源的测试,发现QoS 1.0算法的一些问题,并在此基础上进行优化。

2.3.1实验结果

测试环境:在Win7笔记本电脑上开wifi热点,使用network emulation windows toolkit对wifi对应的网卡进行模拟限制。

测试设备:Meizu MX3手机。

如表1所示,列出了测试的项目,包括丢包、带宽限制和延迟三种单独测试项,和模拟的3G真实环境。

    测试数据如图5、6所示,其中系列1是视频实际码率,系列2是设定的视频码率。

图5 random 15%丢包下的对比图

图6 200k带宽限制下对比图

      如图5和6所示,QoS 1.1相比QoS 1.0,实际视频码率(系列1)降低到0的次数明显减少,说明卡顿相应减少。

    主要原因是,针对QoS 1.0算法调节过于频繁,码率经常上探的问题,在QoS 1.1算法中增加QoS Level之间的barrier,防止频繁上探。

2.3.2实现过程

    QoS 1.1算法的实现基于以下假设:

(1)从高一级Level降到低一级Level越频繁,说明当前网络只支持低一级Level,应该减少从低一级Level上探的机会。避免卡顿。

(2)从高一级Level降到低一级Level,在高一级Level停留的时间越短,说明当前网络倾向于支持低一级Level。

(3)在高一级Level停留的越久,说明网络比较适合在高一级Level,此时应该削弱低一级Level到高一级Level的barrier。

(4)由于发送函数时间存在大小之分,决定了降低Level的强弱,强降低产生的Level间barrier应该高于弱降低产生的barrier。

(5)每两个QoS Level之间都存在barrier,在某一级Level上停留应该减弱下一级和下两级的barrier。

    算法流程图如图7所示。

     其中,菱形代表操作,长方形代表判断条件。writeFrameTime是写数据到sendbuffer的时间,barrier是上下两级码率设置之间的障碍,用来限制码率升级。

                            

                                                                    图7 QoS 1.1算法基本流程图

2.4 QoS算法1.2版本

    在QoS算法1.1版本对视频码率进行网络自适应调节的基础上,QoS算法1.2版本针对视频云直播端音视频线程进行了优化,使得在网络变差的情况下,音频推流得到一定程度的保障,改善播放端的音频播放体验。

2.4.1 视频云直播端线程背景介绍

    由于视频云直播端采用视频和音频两个独立线程执行采集、前处理、编码、打包发送操作,导致了在网络发生拥塞、数据发送不畅的情况下,音视频各自的采集和编码操作受阻,当网络恢复时,由于拥塞期间的数据没有被采集和编码,最终导致播放端出现卡顿。

    为了解决这一问题,对音频线程进行分离,分为音频采集、前处理、编码线程和打包发送线程。

2.4.2 视频云直播端线程交互流程

   以音频线程的调整为例,调整后的线程交互图如图8所示。

图8 视频云直播端线程交互图

  调整后的流程分为音频采集、前处理和编码线程,以及音频打包发送线程。两个线程之间通过一组循环buffer进行数据传递。

  如果网络发送出现拥塞,导致音频打包发送线程受阻,长时间没有从缓冲buffer中拷贝数据,最终导致缓冲buffer中没有空余位置可以接收新的编码数据,那么,音频编码后的数据将被丢弃。

2.4.3 测试效果

视频云直播端线程分离方案的测试效果如图9所示。

   测试环境:树莓派 带宽限制330kbps

   测试设备:华为Mate8手机

图9 视频云直播端线程分离方案测试结果(音频线程分离)

   如图7所示,音频线程经过分离,在卡顿点(红色圆圈)位置,音频发送数据在网络恢复后能够完全发送,弥补了网络卡顿时的数据发送障碍,不会导致播放端的卡顿。

实际测试中,采用音频线程分离策略,播放器的转圈明显减少,视频卡顿时,音频依然流畅,主观感受得到很大的提升。

三、视频云直播端QoS策略后期展望

    随着QoS 1.2版本的上线,视频云直播端在网络变差的时候会进行一定的处理,以尽可能保证视频和音频的流畅。

然而,由于QoS策略的判决依据仅仅来自于直播端本身,缺少来自网络的反馈,导致网络拥塞的判断出现滞后,影响播放端的体验。后期需要开发视频云的源站服务器,进行客户端到源站服务器之间的QoS保障,通过FEC、错误隐藏等方式提高QoS质量。

Categories: Uncategorized Tags:

HBase运维实践-聊聊RIT的那点事

September 8th, 2016    阅读(0) Comments off

相信长时间运维HBase集群的童鞋肯定都会对RIT(Region-In-Transition,很多参考资料误解为Region-In-Transaction,需要注意)有一种咬牙切齿的痛恨感,一旦Region处于长时间的RIT就会有些不知所措,至少以前的我就是这样过来的。正所谓“恐惧来源于未知”,不知所措意味着我们对RIT知之甚少,然而“凡事都有因果,万事皆有源头”,处于RIT状态的Region只是肉眼看到的一个结果,为什么会处于RIT状态才是问题探索的根本,也是解决问题的关键。本文就基于hbase 0.98.9版本对RIT的工作机制以及实现原理进行普及性的介绍,同时在此基础上通过真实案例讲解如何正确合理地处理处于RIT状态的Region。一方面希望大家能够更好的了解RIT机制,另一方面希望通过本文的学习之后可以不再’惧怕’RIT,正确认识处于RIT状态的Region。

Region-In-Trasition机制

从字面意思来看,Region-In-Transition说的是Region变迁机制,实际上是指在一次特定操作行为中Region状态的变迁,那这里就涉及这么几个问题:Region存在多少种状态?HBase有哪些操作会触发Region状态变迁?一次正常操作过程中Region状态变迁的完整流程是怎么样的?如果Region状态在变迁的过程中出现异常又会怎么样?

Region存在多少种状态?有哪些操作会触发状态变迁?

HBase在RegionState类中定义了Region的主要状态,主要有如下:



r1

上图中实际上定义了四种会触发Region状态变迁的操作以及操作对应的Region状态。其中特定操作行为通常包括assign、unassign、split以及merge等,而很多其他操作都可以拆成unassign和assign,比如move操作实际上是先unassign再assign;

Region状态迁移是如何发生的?

这个过程有点类似于状态机,也是通过事件驱动的。和Region状态一样,HBase还定义了很多事件(具体见EventType类)。此处以unassign过程为例说明事件是如何驱动状态变迁的,见下图:

%e8%ae%a9

上图所示是Region在close时的状态变迁图,其中红字部分就是发生的各种事件。可见,如果发生M_ZK_REGION_CLOSING事件,Region就会从OPEN状态迁移到PENDING_CLOSE状态,而发生RS_ZK_REGION_CLOSING事件,Region会从PENDING_CLOSE状态迁移到CLOSING状态,以此类推,发生RS_ZK_REGION_CLOSED事件,Region就会从CLOSING状态迁移到CLOSED状态。当然,除了这些事件之外,HBase还定义了很多其他事件,在此就不一一列举。截至到此,我们知道Region是一个有限状态机,那这个状态机是如何正常工作的,HMaster、RegionServer、Zookeeper又在状态机工作过程中扮演了什么角色,那就接着往下看~

一次正常操作过程中Region状态变迁的完整流程是怎么样的?

接下来本节以unassign操作为例对这个流程进行解析:

整个unassign操作是一个比较复杂的过程,涉及HMaster、RegionServer和Zookeeper三个组件:

1. HMaster负责维护Region在整个操作过程中的状态变化,起到一个枢纽的作用。它有两个重要的HashMap数据结构,分别为regionStates和regionsInTransition,前者用来存储整个集群中所有Region及其当时状态,而后者主要存储在变迁过程中的Region及其状态,后者是前者的一个子集,不包含OPEN状态的Regions;

2. RegionServer负责接收HMaster的指令执行具体unassign操作,实际上就是关闭region操作;

3. Zookeeper负责存储操作过程中的事件,它有一个路径为/hbase/region-in-transition的节点。一旦一个Region发生unssign操作,就会在这个节点下生成一个子节点,子节点的内容是一个“事件”经过序列化的字符串,并且Master会监听在这个子节点上,一旦发生任何事件,Master就会监听到并更新Region的状态。

下图是整个流程示意图:

r3

1. HMaster先执行事件M_ZK_REGION_CLOSING并更新RegionStates,将该Region的状态改为PENDING_CLOSE,并在regionsInTransition中插入一条记录;

2. 发送一条RPC命令给拥有该Region的RegionServer,责令其关闭该Region;

3. RegionServer接收到HMaster发送过来的命令之后,首先生成一个RS_ZK_REGION_CLOSING事件,更新到Zookeeper,Master监听到ZK节点变动之后更新regionStates,将该Region的状态改为CLOSING;

4. RegionServer执行真正的Region关闭操作:如果该Region正在执行flush或者compaction,等待操作完成;否则将该Region下的所有Memstore强制flush;

5. 完成之后生成事件RS_ZK_REGION_CLOSED,更新到Zookeeper,Master监听到ZK节点变动之后更新regionStates,将该Region的状态改为CLOSED;

到这里,基本上将unssign操作过程中涉及到的Region状态变迁解释清楚了,当然,其他诸如assign操作基本类似,在此不再赘述。这里其实还有一个问题,即关于HMaster上所有Region状态是否需要持久化的问题,刚开始接触这个问题的时候想想并不需要,这些处于RIT的状态信息完全可以通过Zookeeper上/region-in-transition的子节点信息构建出来。然而,在阅读HBase Book的相关章节时,看到如下信息:

于是就充满了疑惑,一方面Master更新hbase:meta是一个远程操作,代价相对很大;另一方面Region状态内存更新和远程更新保证一致性比较困难;再者,Zookeeper上已经有相应RIT信息,再持久化一份并没有太大意义。为了对其进行确认,就查阅跟踪了一下源码,发现是否持久化取决于一个参数:hbase.assignment.usezk,默认情况下该参数为true,表示使用zk情况下并不会对Region状态进行持久化(详见RegionStateStore类),可见HBase Book的那段说明存在问题,在此特别说明~

如果Region状态在变迁的过程中出现异常会怎么样?

再回顾unassign的整个过程就会发现一次完整操作涉及太多流程,任何异常都可能会导致Region处于较长时间的RIT状态,好在HBase针对常见的异常做了最基本的容错处理:

1. Master宕机重启:Master在宕机之后会丢失所有内存中的信息,也包括RIT信息以及Region状态信息,因此在重启之后会第一时间重建这些信息。重启之后会遍历Zookeeper上/hbase/regions-in-transition节点下的所有子节点,解析所有子节点对应的最后一个‘事件’,解析完成之后一方面借此重建全局的Region状态,另一方面根据状态机转移图对处于RIT状态的Region进行处理。比如如果发现当前Region的状态是PENDING_CLOSE,Master就会再次据此向RegionServer发送’关闭Region’的RPC命令。

2. 其他异常宕机:HBase会在后台开启一个线程定期检查内存中处于RIT中的Region,一旦这些Region处于RIT状态的时长超过一定的阈值(由参数hbase.master.assignment.timeoutmonitor.timeout定义,默认600000ms)就会重新执行unassign或者assign操作。比如如果当前Region的状态是PENDING_CLOSE,而且处于该状态的时间超过了600000ms,Master就会重新执行unassign操作,向RegionServer再次发送’关闭Region’的RPC命令。

可见,HBase提供了基本的重试机制,保证在一些短暂异常的情况下能够通过不断重试拉起那些处于RIT状态的Region,进而保证操作的完整性和状态的一致性。然而不幸的是,因为各种各样的原因,很多Region还是会掉入长时间的RIT状态,甚至是永久的RIT状态,必须人为干预才能解决,下面一节内容让我们看看都有哪些常见的场景会导致Region会处于永久RIT状态,以及遇到这类问题应该如何解决。

永久RIT状态案例分析

通过RIT机制的了解,其实可以发现处于RIT状态Region并不是什么怪物,大部分处于RIT状态的Region都是短暂的,即使在大多数短暂异常的情况下HBase也提供了重试机制保证Region能够很快恢复正常。然而在一些特别极端的场景下还是会发生一些异常导致部分Region掉入永久的RIT状态,进而会引起表读写阻塞甚至整个集群的读写阻塞。下面我们举两个相关的案例进行说明:

案例一:Compaction永久阻塞

现象:线上一个集群因为未知原因忽然就卡住了,读写完全进不来了;另外还有很多处于PENDING_CLOSE状态的Region。

分析:集群卡住常见原因无非两个,一是Memstore总消耗内存大小超过了上限进而触发RegionServer级别flush,此时系统会阻塞集群执行长时间flush操作;二是storefile数量过多超过设定的上限阈值(参见:hbase.hstore.blockingStoreFiles),此时系统会阻塞所有flush请求而执行compaction。

诊断:

(1)首先查看了各个RegionServer上的Memstore使用大小,并没有达到设定的upperLimit。

(2)再查看了一下所有RegionServer的storefile数量,瞬间石化了,store数为250的RegionServer上storefile数量竟然达到了1.5w+,很多单个store的storefile都超过了设定阈值100

(3)初步怀疑是因为storefile数量过多引起的,看到这么多storefile的第一反应是手动执行major_compaction,然而所有的compact命令好像都没有起任何作用

(4)无意中发现所有RegionServer的Compaction任务都是同一张表music_actions的,而且Compaction时间都基本持续了一两天。到此基本可以确认是因为表music_actions的Compaction任务长时间阻塞,占用了所有的Compaction线程资源,导致集群中所有其他表都无法执行Compaction任务,最后导致StoreFile大量堆积

(5)那为什么会存在PENDING_CLOSE状态的Region呢?经查看,这些处于PENDING_CLOSE状态的Region全部来自于表music_actions,进一步诊断确认是由于在执行graceful_stop过程中unassign时遇到Compaction长时间阻塞导致RegionServer无法执行Region关闭(参考上文unassign过程),因而掉入了永久RIT

解决方案:

(1)这个问题中RIT和集群卡住原因都在于music_actions这张表的Compaction阻塞,因此需要定位Compaction阻塞的具体原因。经过一段时间的定位初步怀疑是因为这张表的编码导致,anyway,具体原因不重要,因为一旦Compaction阻塞,好像是没办法通过正常命令解除这种阻塞的。临时有用的办法是增大集群的Compaction线程,以期望有更多空闲线程可以处理集群中其他Compaction任务,消化大量堆积的StoreFiles

(2)而永久性消灭这种Compaction阻塞只能先将这张表数据迁移出来,然后将这张表暴力删除。暴力删除就是先将HDFS对应文件删除,再将hbase:meta中该表对应的相关数据清除,最后重启整个集群即可。这张表删除之后使用hbck检查一致性之后,集群Compaction阻塞现象就消失了,集群就完全恢复正常。

案例二:HDFS文件异常

现象:线上集群很多RegionServer短时间内频频宕机,有几个Region处于FAILED_OPEN状态

分析诊断:

(1)查看系统监控以及RegionServer日志,确认RegionServer频繁宕机是因为大量CLOSE_WAIT状态的短连接导致。监控显示短时间内(4h)CLOSE_WAIT的数量从0增长到6w+。

(2)再查看RegionServer日志查看到如下日志:

2016-07-27 09:42:14,932 [RS_OPEN_REGION-inspur250.photo.163.org,60020,1469581282053-0] ERROR org.apache.hadoop.hbase.regionserver.handler.OpenRegionHandler - Failed open of region=news_user_actions,|u:cfcd208495d565ef66e7dff9f98764da
|1462799167|30671473410714402,1469522128310.3b3ae24c65fc5094bc2acfebaa7a56de., starting to roll back the global memstore size.
java.io.IOException: java.io.IOException: java.io.FileNotFoundException: File does not exist: /hbase/news_user_actions/b7b3faab86527b88a92f2a248a54d3dc/meta/0f47cda55fa44cf9aa2599079894aed6
2016-07-27 09:42:14,934 [RS_OPEN_REGION-inspur250.photo.163.org,60020,1469581282053-0] INFO  org.apache.hadoop.hbase.regionserver.handler.OpenRegionHandler - Opening of region {NAME => 'news_user_actions,|u:cfcd208495d565ef66e7dff9f9
8764da|1462799167|30671473410714402,1469522128310.3b3ae24c65fc5094bc2acfebaa7a56de.', STARTKEY => '|u:cfcd208495d565ef66e7dff9f98764da|1462799167|30671473410714402', ENDKEY => '|u:d0', ENCODED => 3b3ae24c65fc5094bc2acfebaa7a56de,} faile
d, marking as FAILED_OPEN in ZK

日志显示,Region ‘3b3ae24c65fc5094bc2acfebaa7a56de’打开失败,因此状态被设置为FAILED_OPEN,原因初步认为是FileNotFoundException导致,找不到的文件是Region ‘b7b3faab86527b88a92f2a248a54d3dc’ 下的一个文件,这两者之间有什么联系呢?

(3)使用hbck检查了一把,得到如下错误信息:

ERROR: Found lingering reference file hdfs://mycluster/hbase/news_user_actions/3b3ae24c65fc5094bc2acfebaa7a56de/meta/0f47cda55fa44cf9aa2599079894aed6.b7b3faab86527b88a92f2a248a54d3dc

看到这里就一下恍然大悟,从引用文件可以看出来,Region ‘3b3ae24c65fc5094bc2acfebaa7a56de’是‘ b7b3faab86527b88a92f2a248a54d3dc’的子Region,熟悉Split过程的童鞋就会知道,父Region分裂成两个子Region其实并没有涉及到数据文件的分裂,而是会在子Region的HDFS目录下生成一个指向父Region目录的引用文件,直到子Region执行Compaction操作才会将父Region的文件合并过来。

到这里,就可以理解为什么子Region会长时间处于FAILED_OPEN状态:因为子Region引用了父Region的文件,然而父Region的文件因为未知原因丢失了,所以子Region在打开的时候因为找不到引用文件因而会失败。而这种异常并不能通过简单的重试可以解决,所以会长时间掉入RIT状态。

(4)现在基本可以通过RegionServer日志和hbck日志确定Region处于FAILED_OPEN的原因是因为子Region所引用的父Region的文件丢失导致。那为什么会出现CLOSE_WAIT数量暴涨的问题呢?经确认是因为Region在打开的时候会读取Region对应HDFS相关文件,但因为引用文件丢失所以读取失败,读取失败之后系统会不断重试,每次重试都会同datanode建立短连接,这些短连接因为hbase的bug一直得不到合理处理就会引起CLOSEE_WAIT数量暴涨。

解决方案:删掉HDFS上所有检查出来的引用文件即可

案例分析

经过上面两个案例的讲解其实看出得出这么几点:

1. 永久性掉入RIT状态其实出现的概率并不高,都是在一些极端情况下才会出现。绝大部分RIT状态都是暂时的。

2. 一旦掉入永久性RIT状态,说明一定有根本性的问题原因,只有定位出这些问题才能彻底解决问题

3. 如果Region长时间处于PENDING_CLOSE或者CLOSING状态,一般是因为RegionServer在关闭Region的时候遇到了长时间Compaction任务或Flush任务,所以如果Region在做类似于Major_Compact的操作时尽量不要执行unassign操作,比如move操作、disable操作等;而如果Region长时间处于FAILED_OPEN状态,一般是因为HDFS文件出现异常所致,可以通过RegionServer日志以及hbck定位出来

写在文章最后

RIT在很多运维HBase的人看来是一个很神秘的东西,这是因为RIT很少出现,而一旦出现就很致命,运维起来往往不知所措。本文就希望能够打破这种神秘感,还原它的真实本性。文章第一部分通过层层递进的方式介绍了Region-In-Transition机制,第二部分通过生产环境的真实案例分析永久性RIT出现的场景以及应对的方案。希望大家能够更多的了解RIT,通过不断的运维实践最后再也不用惧怕它~~

Categories: HBase, region-in-transition, rit Tags:

redis3.2新功能–GEO地理位置命令介绍

September 7th, 2016    阅读(256) No comments

概述

redis3.2发布rc版本已经有一段时间了,估计RedisConf 2016左右,3.2版本就能release了。3.2版本中增加的最大功能就是对GEO(地理位置)的支持。说起redis的GEO特性,最大的贡献还是咱们中国人。redis作者在对3.2引进新特性的博客中介绍了为什么支持GEO。GEO hashing的api是在Ardb实现的,Ardb是github用户yinqiwen实现的基于redis协议实现的nosql系统,Ardb支持除了redis、还有LevelDB、RocksDB 、LMDB等kv引擎。其中Ardb实现了GEO hashing功能。从Ardb作者的用户名和标识的位置在深圳可以看出Ardb作者应该是咱中国人。Ardb是用c++写的。redis另一个开发者Matt Stancliff从Ardb提取GEO库,用C语言改写,整合进redis的一个自己的分支,并被redis作者接受,合并进了3.2版本。GEO目前提供以下6个命令。

  • 1、geoadd:增加某个地理位置的坐标。
  • 2、geopos:获取某个地理位置的坐标。
  • 3、geodist:获取两个地理位置的距离。
  • 4、georadius:根据给定地理位置坐标获取指定范围内的地理位置集合。
  • 5、georadiusbymember:根据给定地理位置获取指定范围内的地理位置集合。
  • 6、geohash:获取某个地理位置的geohash值。

地理位置的坐标是以WGS84为标准,WGS84,全称World Geodetic System 1984,是为GPS全球定位系统使用而建立的坐标系统。

GEO命令

下面来看看具体每个命令的用法。

geoadd

geoadd用来增加地理位置的坐标,可以批量添加地理位置,命令格式为:

GEOADD key longitude latitude member [longitude latitude member ...]

key标识一个地理位置的集合。longitude latitude member标识了一个地理位置的坐标。longitude是地理位置的经度,latitude是地理位置的纬度。member是该地理位置的名称。GEOADD可以批量给集合添加一批地理位置。

geopos

geopos可以获取地理位置的坐标,可以批量获取多个地理位置的坐标,命令格式为:

GEOPOS key member [member ...]

geodist

geodist用来获取两个地理位置的距离,命令格式为:

GEODIST key member1 member2 [m|km|ft|mi]

单位可以指定为以下四种类型:

  • m:米,距离单位默认为米,不传递该参数则单位为米。
  • km:公里。
  • mi:英里。
  • ft:英尺。

georadius

georadius可以根据给定地理位置坐标获取指定范围内的地理位置集合。命令格式为:

GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

longitude latitude标识了地理位置的坐标,radius表示范围距离,距离单位可以为m|km|ft|mi,还有一些可选参数:

  • WITHCOORD:传入WITHCOORD参数,则返回结果会带上匹配位置的经纬度。
  • WITHDIST:传入WITHDIST参数,则返回结果会带上匹配位置与给定地理位置的距离。
  • ASC|DESC:默认结果是未排序的,传入ASC为从近到远排序,传入DESC为从远到近排序。
  • WITHHASH:传入WITHHASH参数,则返回结果会带上匹配位置的hash值。
  • COUNT count:传入COUNT参数,可以返回指定数量的结果。

georadiusbymember

georadiusbymember可以根据给定地理位置获取指定范围内的地理位置集合。georadius命令传递的是坐标,georadiusbymember传递的是地理位置。georadius更为灵活,可以获取任何坐标点范围内的地理位置。但是大多数时候,只是想获取某个地理位置附近的其他地理位置,使用georadiusbymember则更为方便。georadiusbymember命令格式为(命令可选参数与georadius含义一样):

GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

geohash

geohash可以获取某个地理位置的geohash值。geohash是将二维的经纬度转换成字符串hash值的算法,后面会具体介绍geohash原理。可以批量获取多个地理位置的geohash值。命令格式为:

GEOHASH key member [member ...]

redis GEO实现

redis GEO实现主要包含了以下两项技术:

  • 1、使用geohash保存地理位置的坐标。
  • 2、使用有序集合(zset)保存地理位置的集合。

geohash

geohash的思想是将二维的经纬度转换成一维的字符串,geohash有以下三个特点:

  • 1、字符串越长,表示的范围越精确。编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右。
  • 2、字符串相似的表示距离相近,利用字符串的前缀匹配,可以查询附近的地理位置。这样就实现了快速查询某个坐标附近的地理位置。
  • 3、geohash计算的字符串,可以反向解码出原来的经纬度。

这三个特性让geohash特别适合表示二维hash值。这篇文章:GeoHash核心原理解析详细的介绍了geohash的原理,想要了解geohash实现的朋友可以参考这篇文章。

redis GEO命令实现

知道了redis使用有序集合(zset)保存地理位置数据(想了解redis有序集合的,可以参看这篇文章《有序集合对象》),以及geohash的特性,就很容易理解redis是如何实现redis GEO命令了。细心的读者可能发现,redis没有实现地理位置的删除命令。不过由于GEO数据保存在zset中,可以用zrem来删除某个地理位置。

  • geoadd命令增加地理位置的时候,会先计算地理位置坐标的geohash值,然后地理位置作为有序集合的member,geohash作为该member的score。然后使用zadd命令插入到有序集合。
  • geopos命令则先根据地理位置获取geohash值,然后decode得到地理位置的坐标。
  • geodist命令先根据两个地理位置各自得到坐标,然后计算两个坐标的距离。
  • georadius和georadiusbymember使用相同的实现,georadiusbymember多了一步把地理位置转换成对应的坐标。然后查找该坐标和周围对应8个坐标符合距离要求的地理位置。因为geohash得到的值其实是个格子,并不是点,这样通过计算周围对应8个坐标就能解决边缘问题。由于使用有序集合保存地理位置,在对地列位置基于范围查询,就相当于实现了zrange命令,内部的实现确实与zrange命令一致,只是geo有些特别的处理,比如获得的某个地理位置,还需要计算该地理位置是否符合给定的距离访问。
  • geohash则直接返回了地理位置的geohash值。

redis关于geohash使用了Ardb的geohash库geohash-int,redis使用的geohash编码长度为26位。可以精确到0.59m的精度。

总结

通过本文,拨开GEO身后的云雾,可以看出redis借助了有序集合(zset)和geohash,加上redis本身实现的命令框架,可以很容易的实现地理位置相关的命令。

Categories: Uncategorized Tags: , ,

Cassandra 故障探测原理–Accrual Failure Detector

September 1st, 2016    阅读(125) No comments

背景

众所周知,故障探测(failure detector)是分布式系统的基础模块,用于探测各种服务(数据库、缓存)、节点(主机、云主机、容器)、进程等服务的状态。在分布式环境下应用需要调整故障检测以适用于不同的QOS需求,而传统的故障探测算法只能提供bool结果对探测进行决断。传统的探测方法主要通过周期心跳Heartbeat和超时时间Timout来处理,当在固定timeout时间内没有收到心跳则断定该节点失效并进行相关逻辑处理。那问题出来了:这个timeout设置为多久呢?timeout跟heartbeat的关系如何?不同环境下网络等都可能不同(如局域网内通信、异地数据中心通信、跨机房),如何在不同环境下对timeout进行设置呢?心跳信号过短是不是会造成网络拥塞?

来来来,看如何解决上述问题:Accrual Failure Detector是日本的学着Naohiro Hayashibara等人提出的失败探测算法,国内暂时对该算法没有很好的中文定义,从其实现来看暂且定义为:累积型失败探测(对历史数据进行累积与分析),本文就是在对该算法的理解上并针对Cassnadra的实现进行了分析,如有纰漏之处,求指正。

Accrual failure detector(累积型失败探测)的创新在于:产生结果是被监测的节点或服务失效(crash)的置信度(the degree of confidence),置信度是随着时间变化的连续的值。累积型失败探测通过一个固定大小窗口(WS)存储收到心跳信号的间隔时间,通过这个窗口对心跳信号均值及方差进行分析,生成一个置信度。分布式应用可以根据自身的QOS需求定义适合自己的suspicion threshold(可信度阈值),定义一个较低的threshold会导致探测到一个real creash时间短,但是其正确性不高;定义一个较高的 threshold会导致特测到real crash的时间长,但是其正确性高。

Accrual Failure Detector应用举 例。在一个分布式系统中,有一个master server和多个worker server, master server需要把很多job分发到worker server上。很显然,master server需要探测worker server的状态。利用Accrual failuer detector,当某个worker server的置信度达到low threshold时,master server不向此worker server派发新的job;当置信度达到moderate threshold时,master server会把在此woker server上的job派发到其它的worker server;当置信度达到high threshold,master server会把释放关于此worker server的通信资源(比如关闭socket)。

failure detector基本概念

定义:q表示探测服务、p表示被探测服务,通过q来探测p的状态

■基本概念之一:Unreliable failure detectors

failure detectors是不可靠的,原因是crash server很难与 slow server区分。

■基本概念之二:Quality of service of failure detectors

      定义1 (Detection time TD):探测时间是指被检测服务p crash后直到q探测到P crash这段时间

       定义2 (Average mistake rate _M): 探测出错率

■探测方法一:Heartbeat failure detectors

    q监控p,p会定期的向q发送心跳,发送心跳的间隔记为△i。q在△to时间内没有收到新的心跳,则认为p已经crash。△tr定义为消息在网路上传输的时延。

第一个方案,把△to(Timeout 超时时间)设定为一个固定的值。缺点:当△to设置的过低时,crash很快会被检测到,但是结果的正确性不高。反之,crash被检测到时间长,但正确性高。

第二个方案,根据心跳的网络时延△tr (不同的网络环境下网络时延肯定不同,比如从杭州到帝都北京的网络时延肯定比中国到山姆大叔纽约的时延小)和 心跳发送间隔△i 来确定△to。缺点:需要依赖△i,因为不能保证发送心跳的规律性以及短间隔的时间导致不准确,同时操作系统的调度也会影响这个时间。

■探测方法二:Adaptive failure detectors

该方法能够自适应网络的变化,在心跳周期不变的情况下随着网络状况不断的变化,△to也会随着变化。Chen-FD提出一种算法,根据最近一段时间收到心跳的间隔来预测收到下个心跳的时间的边界值,△to根据这个预测时间计算一次(只会一次)。作者提供了2个版本的协议:基于同步时钟和基于异步时钟的。 Bertier-FD也提出一种算法基本与上相同,不同的是在Chen的算法的基础上增加了round-trip time(网络往返时间)的考虑。测试表明:Bertier-FD 算法比 Chen-FD算法 更快的检测到crash,但是其正确性降低了。

在上述两种探测方法中如何确定心跳发送间隔△i 一直都是焦点所在。 从常识来看,△i应该由应用自己的需求来确定。但是△i不能单纯的根据自己的需求来确定,很多学着认为其应该由下层的系统(网络,OS)来确定,如果△i远远小于△tr,故障特测时间由 △tr确定,减小△i不会减少故障特测时间,相反还会增加网络拥塞,从而增加△tr,进而增加故障探测时间;如果△i远远大于△tr,故障特测时间由△i 确定,增加△i会增大故障特测时间,且不会明显的减少网络负载。所以△i应该与平均△tr相当。

传统失败探测和accrual failure detector对比:

传统失败探测和累积型失败探测(accrual failure detector)进行比较,左边为传统型探测方法,所有的应用都依赖于一个解释器和探测器,这样就相当于针对不同的应用采用一刀切的方式决定其状态,而累积型失败探测(accrual failure detector)中解释器和探测器是分割的,每个应用可以实现自己的解释器,两者通过suspicion level进行连接,从而实现不同的应用可以设置不同的节点失败可疑级别实现自己的个性化策略。

Accrual failure detector原理:

server q 探测 p,输出值由如下函数表示,suspiction level of p 表示p crash的怀疑级别。

此函数必须满足如下属性:

1、如果p已经crash,值随着t趋近与无穷大。

2、如果p已经crash,存在一个时间段,此时间段后,其值是单调递增的。

3、如果p是alive,值有一个上限(upper bound)。

4、如果p是alive,存在一个时间段,此时间段后,其值为0。

Accrual failure detector实现

设计一个队列,存储收到心跳的间隔,队列的大小是一个固定值(称为window size),当收到新的心跳后,计算时间间隔并写入队列,删除队首的元素。μ表示队列中的时间间隔的平均值,σ表示标准差。其中μσ会随着心跳间隔队列的变化而动态变化。通过对这段固定窗口的历史心跳数据进行分析,实现累积型失败探测,该方法摆脱了△tr、△i、△to对算法的影响。具体公式如下公式(2)、(3)所示

Plater(t) 表示在大于时间t内收到心跳信号的概率,通过对心跳信号进行采集与分析,心跳信号是满足正态分布分布的。在一个较网络环境中收到心跳信号的概率满足正态分布的三个特征:

             1、集中性:正态曲线的高峰位于正中央,即均数所在的位置,在一个较稳定的网络环境下△i是有聚集性的

             2、对称性:正态曲线以均数为中心,左右对称,曲线两端永远不与横轴相交,△i满足该性质

             3、均匀变动性:正态曲线由均数所在处开始,分别向左右两侧逐渐均匀下降,△i满足该性质

Tlast表示最后一次收到心跳的时间;

tnow表示当前时间;

ϕ(tnow)公式二:表示当前时间某个节点失败怀疑级别,怀疑级别越高,表示当前节点越可能crash,所以当怀疑级别设置的太小的话会导致出错率提高。该函数目的就是通过把概率小数转化成整数级别,方便处理,把小数区间转化到整数区间。

Plater(t)公式三:直接积分会较麻烦,但通过数学公式可知,其可以转化为1-F(t)  其中F(t)(μ,σ)的正态函数


cassandra 中的故障探测

cassandara使用的此论文中的方法来进行故障探测。在cassandra中该实现主要在org.apache.cassandra.gms的FailureDetector类中,windows size设置为1000, threshold设为8,当φ值大于8时,则认为此节点crash。不过,计算φ值的算法有所改进:Plater(t) = e-t/μ     使该方法忽略了方差的变化,简化计算,否则需要每次计算方差。

FailureDetector中存储了每个节点的心跳数据:private Map<EndPoint, ArrivalWindow> arrivalSamples_ = new Hashtable<EndPoint, ArrivalWindow>(),通过hashtable保存每个节点的时间窗口,同时保证线程安全。

ArrivalWindow内部通过一个BoundedStatsDeque实现了容量大小为1000的队列,该类实现ArrayDeque<Double> deque。
 class ArrivalWindow
{
    private BoundedStatsDeque arrivalIntervals_;
    private int size_;
    ArrivalWindow(int size)
    {
        size_ = size;
        arrivalIntervals_ = new BoundedStatsDeque(size);
    }

//cassandra 自己的解释器,如果超过设置的 phiSuspectThreshold_则判断为节点down,在yaml.xml中可以设置怀疑级别阈值phi_convict_threashold(部分国内文章解释成时间设置,错误的)

 public void intepret(EndPoint ep)
    {
        ArrivalWindow hbWnd = arrivalSamples_.get(ep);
        if ( hbWnd == null )
        {
            return;
        }
        long now = System.currentTimeMillis();
        /* We need this so that we do not suspect a convict. */
        boolean isConvicted = false;
        double phi = hbWnd.phi(now);   //计算出节点down下去的可能级别
        logger_.trace(“PHI for ” + ep + ” : ” + phi);
        if (phi > phiSuspectThreshold_)
        {
            for ( IFailureDetectionEventListener listener : fdEvntListeners_ )
            {
                listener.suspect(ep);
            }
        }
    }

//计算Plater(t)

 double p(double t)
    {
        double mean = mean();
        double exponent = (-1)*(t)/mean;
        return 1 – ( 1 – Math.pow(Math.E, exponent) );
    }
//计算ϕ (tnow):
    double phi(long tnow)
    {
        int size = arrivalIntervals_.size();
        double log = 0d;
        if ( size > 0 )
        {
            double t = tnow – tLast_;
            double probability = p(t);
            log = (-1) * Math.log10( probability );
        }
        return log;
    }
//后续会继续研究cassandra的内部相关实现机制,敬请期待。
Categories: Uncategorized Tags:

Flask源码剖析

August 27th, 2016    阅读(0) Comments off

前言

本文将基于flask 0.1版本(git checkout 8605cc3)来分析flask的实现,试图理清flask中的一些概念,加深读者对flask的理解,提高对flask的认识。从而,在使用flask过程中,能够减少困惑,胸有成竹,遇bug而[……]

阅读全文

Categories: MySQL Tags:

HBase基准性能测试报告

August 25th, 2016    阅读(365) No comments

本次测试主要评估线上HBase的整体性能,量化当前HBase的性能指标,对各种场景下HBase性能表现进行评估,为业务应用提供参考。本篇文章主要介绍此次测试的基本条件,HBase在各种测试场景下的性能指标(主要包括单次请求平均延迟和系统吞吐量)以及对应的资源利用情况,并对各种测试结果进行分析。

测试环境

测试环境包括测试过程中HBase集群的拓扑结构、以及需要用到的硬件和软件资源,硬件资源包括:测试机器配置、网络状态等等,软件资源包括操作系统、HBase相关软件以及测试工具等。

集群拓扑结构

本次测试中,测试环境总共包含4台SA5212H2物理机作为数据存储。生成数据的YCSB程序与数据库并不运行在相同的物理集群。

单台机器主机硬件配置

软件版本信息

测试工具

YCSB全称Yahoo! Cloud Serving Benchmark,是Yahoo公司开发的专门用于NoSQL测试的基准测试工具。github地址:https://github.com/brianfrankcooper/YCSB YCSB支持各种不同的数据分布方式

1. Uniform:等概论随机选择记录

2. Zipfian:随机选择记录,存在热记录

3. Latest:近期写入的记录为热记录

测试场景

YCSB为HBase提供了多种场景下的测试,本次测试中,我们导入10亿条数据,并对如下场景进行测试:

YCSB并没有提供Increment相关的测试功能,但是部分业务有这方面的需求,因此对YCBS进行了改造,加入了Increment模块。需要注意的是,在测试Increment性能前需要导入1亿条数字进行测试。写入和查询的数据模拟目前线上记录的长度,具有以下特性:

HBase相关重要配置

hfile.block.cache.size:0.2
hbase.regionserver.global.memstore.upperLimit:0.45
jvm:-Xms48g -Xmx48g -Xmn4g -Xss256k -XX:PermSize=256m -XX:MaxPermSize=256m

jvm参数表示每台机器会分配48G内存作为Java的堆内存使用,hfile.block.cache.size参数表示HBase会为每台Region Server分配大小为9.6G(48 * 0.2)的内存作为读缓存使用。hbase.regionserver.global.memstore.upperLimit参数表示HBase会为每台Region Server最多分配大小为21.6G(48 * 0.45)的内存作为写缓存使用。

测试方法

上述测试场景中部分测试(插入测试、scan扫描查询等)对客户端带宽资源要求很高,单个客户端测试会因为客户端带宽耗尽而导致无法测出实际服务器集群读写性能,因此我们开启6个YCBS客户端并发进行测试,最终Throughput是6个客户端的总和,AverageLatency取6个客户端延迟的平均值。

单个YCSB测试都遵守标准测试流程,基本流程如下:

1. 在6个客户端服务器部署YCSB程序,向集群中load 10亿条数据

2. 按照预先定义的场景修改负载文件workload

3. 使用ycsb run方法执行测试,向集群写入读取数据

4. 进行数据操作时通过YCSB记录产生的统计数据,主要是吞吐量和平均延迟两个指标

5. 根据结果生成对应的图标

6. 针对不同场景,重复上述测试步骤

测试结果

单条记录插入

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;插入操作执行2千万次;插入请求分布遵从zipfian分布;

测试结果

资源使用情况

上图为单台RegionServer的带宽使用曲线图(资源使用情况中只列出和本次测试相关的资源曲线图,后面相关资源使用情况类似),本次测试线程为1000的情况下带宽基本维持在100M左右,对于百兆网卡来说基本上已经打满。

结果分析

1.     吞吐量曲线分析:线程数在10~500的情况下,随着线程数的增加,系统吞吐量会不断升高;之后线程数再增加,系统吞吐量基本上不再变化。结合图3带宽资源使用曲线图可以看出,当线程数增加到一定程度,系统带宽资源基本耗尽,系统吞吐量就不再会增加。可见,HBase写操作是一个带宽敏感型操作,当带宽资源bound后,写入吞吐量基本就会稳定。

2.     写入延迟曲线分析:随着线程数的不断增加,写入延迟也会不断增大。这是因为写入线程过多,导致CPU资源调度频繁,单个线程分配到的CPU资源会不断降低;另一方面由于线程之间可能会存在互斥操作导致线程阻塞;这些因素都会导致写入延迟不断增大。

建议

根据曲线显示,500线程以内的写入延迟并不大于10ms,而此时吞吐量基本最大,因此如果是单纯写入的话500线程写入会是一个比较合适的选择。

单纯查询

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;查询操作执行2千万次;查询请求分布遵从zipfian分布;

测试结果

资源使用情况

图5为线程数在1000时IO利用率曲线图,图中IO利用率基本保持在100%,说明IO资源已经达到使用上限。图6为线程数在1000时系统负载曲线图,图中load1曲线表示在最近一分钟内的平均负载,load5表示最近五分钟内的平均负载。最近5分钟的负责达到了50左右,对于32核系统来说,表示此时系统负载很高,已经远远超负荷运行。

结果分析

1.     吞吐量曲线分析:线程数在10~500的情况下,随着线程数的增加,系统吞吐量会不断升高;之后线程数再增加,系统吞吐量基本上不再变化。结合图5、图6系统资源使用曲线图可以看出,当线程数增加到一定程度,系统IO资源基本达到上限,系统负载也特别高。IO利用率达到100%是因为大量的读操作都需要从磁盘查找数据,系统负载很高是因为HBase需要对查找的数据进行解压缩操作,解压缩操作需要耗费大量CPU资源。这两个因素结合导致系统吞吐量就不再随着线程数增肌而增加。可见,HBase读操作是一个IO/CPU敏感型操作,当IO或者CPU资源bound后,读取吞吐量基本就会稳定不变。

2.     延迟曲线分析:随着线程数的不断增加,读取延迟也会不断增大。这是因为读取线程过多,导致CPU资源调度频繁,单个线程分配到的CPU资源会不断降低;另一方面由于线程之间可能会存在互斥操作导致线程阻塞;这些因素都会导致写入延迟不断增大。和写入延迟相比,读取延迟会更大,是因为读取涉及IO操作,IO本身就是一个耗时操作,导致延迟更高。

建议

根据曲线显示,500线程以内的读取延迟并不大于20ms,而此时吞吐量基本最大,因此如果是单纯读取的话500线程读取会是一个比较合适的选择。

Range扫描查询

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;scan操作执行一千两百万次,请求分布遵从zipfian分布; scan最大长度为100条记录, scan长度随机分布且遵从uniform分布;

测试结果

资源使用情况

图8为线程数在1000时IO利用率曲线图,图中IO利用率基本保持在100%,说明IO资源已经达到使用上限。图9为线程数在1000时带宽资源使用曲线图,图中带宽资源基本也已经达到上限。

结果分析

1.     吞吐量曲线分析:线程数在10~500的情况下,随着线程数的增加,系统吞吐量会不断升高;之后线程数再增加,系统吞吐量基本上不再变化。结合图8 、图9资源使用曲线图可以看出,当线程数增加到一定程度,系统IO资源基本达到上限,带宽也基本达到上限。IO利用率达到100%是因为大量的读操作都需要从磁盘查找数据,而带宽负载很高是因为每次scan操作最多可以获取50Kbyte数据,TPS太高会导致数据量很大,因而带宽负载很高。两者结合导致系统吞吐量就不再随着线程数增大会增大。可见,scan操作是一个IO/带宽敏感型操作,当IO或者带宽资源bound后,scan吞吐量基本就会稳定不变。

2.     延迟曲线分析:随着线程数的不断增加,读取延迟也会不断增大。这是因为读取线程过多,导致CPU资源调度频繁,单个线程分配到的CPU资源会不断降低;另一方面由于线程之间可能会存在互斥操作导致线程阻塞;这些因素都会导致写入延迟不断增大。和写入延迟以及单次随机查找相比,读取延迟会更大,是因为scan操作会涉及多次IO操作,IO本身就是一个耗时操作,因此会导致延迟更高。

建议

根据图表显示,用户可以根据业务实际情况选择100~500之间的线程数来执行scan操作。

查询插入平衡

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;查询插入操作共执行8千万次;查询请求分布遵从zipfian分布;

测试结果

资源使用情况

图11为线程数在1000时系统IO利用率曲线图,图中IO利用率基本保持在100%,说明IO资源已经达到使用上限。图12为线程数在1000时系统负载曲线图,图中显示CPU负载资源达到了40+,对于只有32核的系统来说,已经远远超负荷工作了。

结果分析

1.    吞吐量曲线分析:线程数在10~500的情况下,随着线程数的增加,系统吞吐量会不断升高;之后线程数再增加,系统吞吐量变化就比较缓慢。结合图11、图12系统资源使用曲线图可以看出,当线程数增加到一定程度,系统IO资源基本达到上限,带宽也基本达到上限。IO利用率达到100%是因为大量的读操作都需要从磁盘查找数据,而系统负载很高是因为大量读取操作需要进行解压缩操作,而且线程数很大本身就需要更多CPU资源。因此导致系统吞吐量就不再会增加。可见,查询插入平衡场景下,当IO或者CPU资源bound后,系统吞吐量基本就会稳定不变。

2.     延迟曲线分析:随着线程数的不断增加,读取延迟也会不断增大。这是因为读取线程过多,导致CPU资源调度频繁,单个线程分配到的CPU资源会不断降低;另一方面由于线程之间可能会存在互斥操作导致线程阻塞;这些因素都会导致写入延迟不断增大。图中读延迟大于写延迟是因为读取操作涉及到IO操作,比较耗时。

建议

根据图表显示,在查询插入平衡场景下用户可以根据业务实际情况选择100~500之间的线程数。

插入为主

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;查询插入操作共执行4千万次;查询请求分布遵从latest分布;

测试结果

资源使用情况

图15为线程数在1000时系统带宽使用曲线图,图中系统带宽资源基本到达上限,而总体IO利用率还比较低。

结果分析

1.    曲线分析:线程数在10~500的情况下,随着线程数的增加,系统吞吐量会不断升高;之后线程数再增加,系统吞吐量基本上不再变化。结合图14带宽资源使用曲线图可以看出,当线程数增加到一定程度,系统带宽资源基本耗尽,系统吞吐量就不再会增加。基本同单条记录插入场景相同。

2.    写入延迟曲线分析: 基本同单条记录插入场景。

建议

根据图表显示,插入为主的场景下用户可以根据业务实际情况选择500左右的线程数来执行。

查询为主

测试参数

总记录数为10亿,分为128个region,均匀分布在4台region server上;查询插入操作共执行4千万次;查询请求分布遵从zipfian分布;

测试结果

资源使用情况

图17为线程数在1000时IO利用率曲线图,图中IO利用率基本保持在100%,说明IO资源已经达到使用上限。

结果分析

基本分析见单纯查询一节,原理类似。

建议

根据图表显示,查询为主的场景下用户可以根据业务实际情况选择100~500之间的线程数来执行。

Increment自增

测试参数

1亿条数据,分成16个Region,分布在4台RegionServer上;操作次数为100万次;

测试结果

结果分析

1.    线程数增加,Increment操作的吞吐量会不断增加,线程数到达100个左右时,吞吐量会达到顶峰(23785 ops/sec),之后再增加线程数,吞吐量基本维持不变;

2.    随着线程数增加,Increment操作的平均延迟会不断增加。线程数在100以下,平均延时都在4ms以内;

建议

根据图表显示,查询为主的场景下用户可以根据业务实际情况选择100~500之间的线程数来执行。

测试结果总结

根据以上测试结果和资源利用情况可以得出如下几点:

1. 写性能:集群吞吐量最大可以达到70000+ ops/sec,延迟在几个毫秒左右。网络带宽是主要瓶颈,如果将千兆网卡换成万兆网卡,吞吐量还可以继续增加,甚至达到目前吞吐量的两倍。

2. 读性能:很多人对HBase的印象可能都是写性能很好、读性能很差,但实际上HBase的读性能远远超过大家的预期。集群吞吐量最大可以达到26000+,单台吞吐量可以达到8000+左右,延迟在几毫秒~20毫秒左右。IO和CPU是主要瓶颈。

3. Range 扫描性能:集群吞吐量最大可以达到14000左右,系统平均延迟在几毫秒~60毫秒之间(线程数越多,延迟越大);其中IO和网络带宽是主要瓶颈。

测试注意事项

1. 需要关注是否是全内存测试,全内存测试和非全内存测试结果相差会比较大。参考线上实际数据情况,本次测试采用非全内存读测试。是否是全内存读取决于总数据量大小、集群Jvm内存大小、Block Cache占比、访问分布是否是热点访问这四者,在JVM内存大小以及Block Cache占比不变的情况下,可以增大总数据量大小或者修改访问分布;

2. 测试客户端是否存在瓶颈。HBase测试某些场景特别耗费带宽资源,如果单个客户端进行测试很可能会因为客户端带宽被耗尽导致无法测出实际服务器集群性能。本次测试使用6个客户端并发进行测试。

3. 单条记录大小对测试的影响。单条记录设置太大,会导致并发插入操作占用大量带宽资源进而性能产生瓶颈。而设置太小,测试出来的TPS峰值会比较大,和线上实际数据不符。本次测试单条数据大小设置为50M,基本和实际情况相符。