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    阅读(188) 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,基本和实际情况相符。

HBase最佳实践-集群规划

August 21st, 2016    阅读(0) Comments off

HBase自身具有极好的扩展性,也因此,构建扩展集群是它的天生强项之一。在实际线上应用中很多业务都运行在一个集群上,业务之间共享集群硬件、软件资源。那问题来了,一个集群上面到底应该运行哪些业务可以最大程度上利用系统的软硬件资源?另外,对于一个给定业务来说,应该如何规划集群的硬件容量才能使得资源不浪费?最后,一个给定的RegionServer上到底部署多少Region比较合适?想必这些问题都曾经困惑过很多HBaser,那本文将结合前人的分享以及笔者的经验简单的对这三个问题分别进行解析,抛砖引玉,希望大家能够针对这几个话题进行深入的交流!

集群业务规划

一般而言,一个HBase集群上很少只跑一个业务,大多数情况都是多个业务共享集群,实际上就是共享系统软硬件资源。这里通常涉及两大问题,其一是业务之间资源隔离问题,就是将各个业务在逻辑上隔离开来,互相不受影响,这个问题产生于业务共享场景下一旦某一业务一段时间内流量猛增必然会因为过度消耗系统资源而影响其他业务;其二就是共享情况下如何使得系统资源利用率最高,理想情况下当然希望集群中所有软硬件资源都得到最大程度利用。前者本次并不讨论,后期会开’专场’讨论,本节主要就后者进行探讨。

使得集群系统资源最大化利用,那首先要看业务对系统资源的需求情况。经过对线上业务的梳理,通常可将这些业务分为如下几类:

1. 硬盘容量敏感型业务:这类业务对读写延迟以及吞吐量都没有很大的要求,唯一的需要就是硬盘容量。比如大多数离线读写分析业务,上层应用一般每隔一段时间批量写入大量数据,然后读取也是定期批量读取大量数据。特点:离线写、离线读,需求硬盘容量

2. 带宽敏感型业务:这类业务大多数写入吞吐量很大,但对读取吞吐量没有什么要求。比如日志实时存储业务,上层应用通过kafka将海量日志实时传输过来,要求能够实时写入,而读取场景一般是离线分析或者在上次业务遇到异常的时候对日志进行检索。特点:在线写、离线读,需求带宽

3. IO敏感型业务:相比前面两类业务来说,IO敏感型业务一般都是较为核心的业务。这类业务对读写延迟要求较高,尤其对于读取延迟通常在100ms以内,部分业务可能要求更高。比如在线消息存储系统、历史订单系统、实时推荐系统等。特点:在(离)线写、在线读,需求内存、高IOPS介质

(而对于CPU资源,HBase本身就是CPU敏感型系统,主要用于数据块的压缩/解压缩,所有业务都对CPU有共同的需求)

一个集群想要资源利用率最大化,一个思路就是各个业务之间‘扬长避短’,合理搭配,各取所需。实际上就是上述几种类型的业务能够混合分布,建议不要将同一种类型的业务太多分布在同一个集群。因此一个集群理论上资源利用率比较高效的配置为:硬盘敏感型业务 + 带宽敏感型业务 + IO敏感型业务。

另外,集群业务规划的时候除了考虑资源使用率最大化这个问题之外,还需要考虑实际运维的需求。建议将核心业务和非核心业务分布在同一个集群,强烈建议不要将太多核心业务同时分布在同一个集群。这主要有两方面的考虑:

1. 一方面是因为‘一山不容二虎’,核心业务共享资源必然会产生竞争,一旦出现竞争无论哪个业务’落败’都不是我们愿意看到的;

2. 另一方面在特殊场景下方便运维童鞋进行降级处理,比如类似于淘宝双十一这类大促活动,某个核心业务预期会有很大的流量涌入,为了保证核心业务的平稳,在资源共享的情况下只能牺牲其他非核心业务,在和非核心业务方充分交流沟通的基础上限制这些业务的资源使用,在流量极限的时候甚至可以直接停掉这些非核心业务。试想,如果是很多核心业务共享集群的话,哪个核心业务愿意轻易让路?

那有些同学就说了:如果按照你这样设计,那岂不是会产生很多小集群。的确,这种设计会产生很多小集群,相信如果没有资源隔离的话,小集群是没法避免的。有些使用’rsgroup’进行业务资源隔离的集群会做的很大,大集群通过隔离会将业务独立分布到很多独立的RS上,这样实际上就产生了很多逻辑上的小集群,那么,这些小集群同样适用上面提出的规划思路。

集群容量规划

每个季度公司都会要求采购新机器,一般情况下机器的规格(硬盘总容量、内存大小、CPU规格)都是固定的。假如现在一台RegionServer的硬盘规格是3.6T * 12,总内存大小为128G,从理论上来说这样的配置是否会有资源浪费?如果有的话是硬盘浪费还是内存浪费?那合理的硬盘/内存搭配应该是什么样?和哪些影响因素有关?

这里需要提出一个’Disk / Java Heap Ratio’的概念,意思是说一台RegionServer上1bytes的Java内存大小需要搭配多大的硬盘大小最合理。在给出合理的解释在前,先把结果给出来:

Disk Size / Java Heap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2

按照默认配置,RegionSize = 10G,对应参数为hbase.hregion.max.filesize;MemstoreSize = 128M,对应参数为hbase.hregion.memstore.flush.size;ReplicationFactor = 3,对应参数为dfs.replication;HeapFractionForMemstore = 0.4,对应参数为hbase.regionserver.global.memstore.lowerLimit;

计算为:10G / 128M * 3 * 0.4 * 2 = 192,意思是说RegionServer上1bytes的Java内存大小需要搭配192bytes的硬盘大小最合理,再回到之前给出的问题,128G的内存总大小,拿出96G作为Java内存用于RegionServer,那对应需要搭配96G * 192 = 18T硬盘容量,而实际采购机器配置的是36T,说明在默认配置条件下会有几乎一半硬盘被浪费。

计算公式是如何’冒’出来的?

再回过头来看看那个计算公式是怎么’冒’出来的,其实很简单,只需要从硬盘容量纬度和Java Heap纬度两方面计算Region个数,再令两者相等就可以推导出来,如下:

硬盘容量纬度下Region个数:Disk Size / (RegionSize *ReplicationFactor) 

Java Heap纬度下Region个数:Java Heap * HeapFractionForMemstore / (MemstoreSize / 2 ) 

Disk Size / (RegionSize *ReplicationFactor)  = Java Heap * HeapFractionForMemstore / (MemstoreSize / 2 ) 

=> Disk Size / Java Heap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2

这样的公式有什么具体意义?

1. 最直观的意义就是判断在当前给定配置下是否会有资源浪费,内存资源和硬盘资源是否匹配。

2. 那反过来,如果已经给定了硬件资源,比如硬件采购部已经采购了当前机器内存128G,分配给Java Heap为96G,而硬盘是40T,很显然两者是不匹配的,那能不能通过修改HBase配置来使得两者匹配?当然可以,可以通过增大RegionSize或者减少MemstoreSize来实现,比如将默认的RegionSize由10G增大到20G,此时Disk Size / Java Heap = 384,96G * 384 = 36T,基本就可以使得硬盘和内存达到匹配。

3. 另外,如果给定配置下内存硬盘不匹配,那实际场景下内存’浪费’好呢还是硬盘’浪费’好?答案是内存’浪费’好,比如采购的机器Java Heap可以分配到126G,而总硬盘容量只有18T,默认配置下必然是Java Heap有浪费,但是可以通过修改HBase配置将多余的内存资源分配给HBase读缓存BlockCache,这样就可以保证Java Heap并没有实际浪费。

另外,还有这些资源需要注意…

带宽资源:因为HBase在大量scan以及高吞吐量写入的时候特别耗费网络带宽资源,强烈建议HBase集群部署在万兆交换机机房,单台机器最好也是万兆网卡+bond。如果特殊情况交换机是千兆网卡,一定要保证所有的RegionServer机器部署在同一个交换机下,跨交换机会导致写入延迟很大,严重影响业务写入性能。

 CPU资源:HBase是一个CPU敏感型业务,无论数据写入读取,都会因为大量的压缩解压操作,特别耗费计算资源。因此对于HBase来说,CPU越多越好。

参考:

http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html

Region规划

Region规划主要涉及到两个方面:Region个数规划以及单Region大小规划,这两个方面并不独立,而是相互关联的,大Region对应的Region个数少,小Region对应的Region个数多。Region规划相信是很多HBase运维同学比较关心的问题,一个给定规格的RegionServer上运行多少Region比较合适,在刚开始接触HBase的时候,这个问题也一直困扰着笔者。在实际应用中,Region太多或者太少都有一定的利弊:

优点     缺点

大量小Region

1. 更加有利于集群之间负载分布

2. 有利于高效平稳的Compaction,这是因为小Region中HFile相对较小,Compaction代价小,详情可见:Stripe Compaction

1. 最直接的影响:在某台RegionServer异常宕机或者重启的情况下大量小Region重分配以及迁移是一个很耗时的操作,一般一个Region迁移需要1.5s~2.5s左右,Region个数越多,迁移时间越长。直接导致failover时间很长。

2. 大量小Region有可能会产生更加频繁的flush,产生很多小文件,进而引起不必要的Compaction。特殊场景下,一旦Region数超过一个阈值,将会导致整个RegionServer级别的flush,严重阻塞用户读写。

3. RegionServer管理维护开销很大

少量大Region

1. 有利于RegionServer的快速重启以及宕机恢复

2. 可以减少总的RCP数量

3. 有利于产生更少的、更大的flush

1. Compaction效果很差,会引起较大的数据写入抖动,稳定性较差

2. 不利于集群之间负载均衡

可以看出来,在HBase当前工作模式下,Region太多或者太少都不是一件太好的事情,在实际线上环境需要选择一个折中点。官方文档给出的一个推荐范围在20~200之间,而单个Region大小控制在10G~30G,比较符合实际情况。

然而,HBase并不能直接配置一台RegionServer上的Region数,Region数最直接取决于RegionSize的大小配置hbase.hregion.max.filesize,HBase认为,一旦某个Region的大小大于配置值,就会进行分裂。

hbase.hregion.max.filesize默认为10G,如果一台RegionServer预期运行100个Region,那单台RegionServer上数据量预估值就为:10G * 100 * 3 = 3T。反过来想,如果一台RegionServer上想存储12T数据量,那按照单Region为10G计算,就会分裂出400个Region,很显然不合理。此时就需要调整参数hbase.hregion.max.filesize,将此值适度调大,调整为20G或者30G。而实际上当下单台物理机所能配置的硬盘越来越大,比如36T已经很普遍,如果想把所有容量都用来存储数据,依然假设一台RegionServer上分布100个Region,那么每个Region的大小将会达到可怕的120G,一旦执行Compaction将会是一个灾难。

可见,对于当下的HBase,如果想让HBase工作的更加平稳(Region个数控制在20~200之间,单Region大小控制在10G~30G之间),最多可以存储的数据量差不多为200 * 30G * 3= 18T。如果存储的数据量超过18T,必然会引起或多或少的性能问题。所以说,从Region规模这个角度讲,当前单台RegionServer能够合理利用起来的硬盘容量上限基本为18T。

然而随着硬件成本的不断下降,单台RegionServer可以轻松配置40T+的硬盘容量,如果按照上述说法,越来越多的硬盘其实只是’镜中月,水中花’。社区也意识到了这样的问题,在当前Region的概念下提出了Sub-Region的概念,可以简单理解为将当前的Region切分为很多逻辑上小的Sub-Region。Region还是以前的Region,只是所有之前以Region为单位进行的Compaction将会以更小的Sub-Region粒度执行。这样,单Region就可以配置的很大,比如50G、100G,此时单台RegionServer上也就可以存储更多的数据。个人认为Sub-Region功能将会是HBase开发的一个重点。

总结

本文结合HBase相关理论知识以及笔者的实际经验,对HBase集群规划中最常见的三个问题 - 业务规划、容量规划以及Region规划做了简单的解析,希望给大家一些启发和思考。线上集群规划是一个经验积累的过程,相信每个HBase运维同学或多或少都会碰到一些坑,也肯定会有自己的思考和见解,希望大家能够更多的在评论区或者邮件交流,谢谢!

Categories: HBase, region规划, 集群规划 Tags:

InnoDB透明页压缩与稀疏文件

August 18th, 2016    阅读(86) No comments

MySQL 5.7中包括了很多让人耳目一新的新特性,其中就包括了InnoDB Transparent Page Compression,姑且称之为InnoDB透明页压缩。其实透明页压缩这个东西,早就关注过,其用到了sparse file和hole punching技术,但一直没能将这两种技术跟InnoDB压缩联系起来。最近花了点时间了解了下。

熟悉InnoDB的同学都知道,InnoDB从MySQL 5.1版本开始就支持压缩,提供zlib压缩算法,是记录压缩(record compress),曾大概看过InnoDB这部分相关的源码,逻辑比较复杂,如果对InnoDB page的组织结构不了解,相信很难看出个所以然,该压缩是页感知的(page aware),即需要知道页里面记录是怎么保存的。与之相反,MySQL 5.7最新支持的压缩是页透明的(page transparent),当然,页首尾的元数据是不压缩的,不关心这个页里面保存的是什么内容,可以理解为页/块压缩(page/block compress,本文将块和页混用)。

假设有个16KB的InnoDB页P1,通过块压缩为11KB,如果表空间使用的文件系统在mkfs时指定block size为4KB,那么只需要使用3个文件块来保存11KB的数据,节省1个文件块即4KB的空间。那么是不是说InnoDB下个页P2的数据直接从所节省的这4KB开始写入吗,答案是否定的。

InnoDB透明页压缩不会改变表文件的结构,我们可以理解为每页都占据了文件中4个块的大小,页压缩后的最终大小不会影响每个页在表文件中的起始偏移位置。即第k个页的数据,还是从表文件第4*k个块开始写入。问题来了,为什么不呢,因为压缩页经过修改后,再次压缩后的大小是不可知的,可能本来压缩后的大小为11KB,再次压缩就变成15KB了,那么仍需要4K文件块来保存,如果文件第4*n+3个块已经被写入了P2的数据,P1再次压缩后多出来4K数据就没地方放了。

从上段描述来看,不管P1被压缩成什么熊样,P2仍然需要从表文件的第4*n+4个偏移块开始写入数据,这种压缩并没有改变文件逻辑大小。虽然压缩后,IO是小了,但4KB的IO相比16KB的IO并不能带来多大的性能提升。然并卵!

怎样才能节省被压缩后释放的空间呢,这就需要用到文件系统/操作系统内核层面的技术 – sparse file,简单来说,sparse file是这样的文件, file 1大小是12KB,但是其实只占用首尾2个文件块共8KB的磁盘空间,中间4KB由于没有真实数据,并未分配磁盘空间,或者本来已经分配了,但又被回收了,像是中间被挖了个洞(punch hole)。这被挖的4KB,可以被文件系统用来分配给其他文件保存数据。如果中间4KB的数据被用户填上了呢,没事,文件系统分配一个新的空闲快给file 1即可。关于sparse file更详细的介绍参见参考文献。当然这可能会导致数据库IO不连续。

通过上面的描述,相信很容易就能够将sparse file技术应用到InnoDB透明页压缩上。不再赘述,只放一张图。

为什么InnoDB要另辟蹊径,采用新的压缩方案,不再原来的压缩实现上进行优化呢,可能有以下两点原因:

首先,原有的记录级压缩,代码实现复杂的,需要基于不同的页类型采用不同的处理方式,需要熟悉InnoDB的索引和页结构,代码封装性较差,添加新的压缩算法或进行性能优化提升较费劲,所以一直仅支持zlib。在这个基础上进行优化提高较困难。这个观点得到MySQL官方的验证,详见参考文献中的官方描述。

其次,相对于原来的记录级压缩,新方案更加灵活,因为压缩算法是保持在InnoDB页的元数据中,理论上可以做到同个表中不同页采用了不同的压缩算法,比如根据不同页类型来决定是否压缩,采用某种压缩算法(当然目前MySQL官方还没这么做)。现实中,也会存在同个表包括多种压缩算法的场景,因为用户可以动态修改压缩算法(也可以启动和关闭压缩),而动态修改并不是说把已经压缩的页马上使用新的压缩算法重新压一次,而是对新产生或更新的页起作用,这就会导致有些页是不压缩的,有些页是采用zlib,有些采用lz4。吐槽下,为什么InnoDB还不支持snappy或quicklz呢。

参考文献:

http://dev.mysql.com/worklog/task/?id=7696

 http://mysqlserverteam.com/innodb-transparent-page-compression/

http://mysqlserverteam.com/innodb-transparent-pageio-compression/

https://wiki.archlinux.org/index.php/Sparse_file

Categories: 数据库管理 Tags:

Kudu:支持快速分析的新型Hadoop存储系统

August 11th, 2016    阅读(118) No comments

KuduCloudera开源的新型列式存储系统,是Apache Hadoop生态圈的新成员之一(incubating),专门为了对快速变化的数据进行快速的分析,填补了以往Hadoop存储层的空缺。本文主要对Kudu的动机、背景,以及架构进行简单介绍。

背景——功能上的空白

        Hadoop生态系统有很多组件,每一个组件有不同的功能。在现实场景中,用户往往需要同时部署很多Hadoop工具来解决同一个问题,这种架构称为混合架构 (hybrid architecture)比如,用户需要利用Hbase的快速插入、快读random access的特性来导入数据,HBase也允许用户对数据进行修改,HBase对于大量小规模查询也非常迅速。同时,用户使用HDFS/Parquet + Impala/Hive来对超大的数据集进行查询分析,对于这类场景, Parquet这种列式存储文件格式具有极大的优势。

        很多公司都成功地部署了HDFS/Parquet + HBase混合架构,然而这种架构较为复杂,而且在维护上也十分困难。首先,用户用FlumeKafka等数据Ingest工具将数据导入HBase,用户可能在HBase上对数据做一些修改。然后每隔一段时间(每天或每周)将数据从Hbase中导入到Parquet文件,作为一个新的partition放在HDFS上,最后使用Impala等计算引擎进行查询,生成最终报表。

        这样一条工具链繁琐而复杂,而且还存在很多问题,比如:

  • Ÿ  如何处理某一过程出现失败?
  • Ÿ  HBase将数据导出到文件,多久的频率比较合适?
  • Ÿ  当生成最终报表时,最近的数据并无法体现在最终查询结果上。
  • Ÿ  维护集群时,如何保证关键任务不失败?
  • Ÿ  Parquetimmutable,因此当HBase中删改某些历史数据时,往往需要人工干预进行同步。

        这时候,用户就希望能够有一种优雅的存储解决方案,来应付不同类型的工作流,并保持高性能的计算能力。Cloudera很早就意识到这个问题,在2012年就开始计划开发Kudu这个存储系统,终于在2015年发布并开源出来。Kudu是对HDFSHBase功能上的补充,能提供快速的分析和实时计算能力,并且充分利用CPUI/O资源,支持数据原地修改,支持简单的、可扩展的数据模型。

背景——新的硬件设备

        RAM的技术发展非常快,它变得越来越便宜,容量也越来越大。Cloudera的客户数据显示,他们的客户所部署的服务器,2012年每个节点仅有32GB RAM,现如今增长到每个节点有128GB256GB RAM。存储设备上更新也非常快,在很多普通服务器中部署SSD也是屡见不鲜。HBaseHDFS、以及其他的Hadoop工具都在不断自我完善,从而适应硬件上的升级换代。然而,从根本上,HDFS基于03GFSHBase基于05BigTable,在当时系统瓶颈主要取决于底层磁盘速度。当磁盘速度较慢时,CPU利用率不足的根本原因是磁盘速度导致的瓶颈,当磁盘速度提高了之后,CPU利用率提高,这时候CPU往往成为系统的瓶颈。HBaseHDFS由于年代久远,已经很难从基本架构上进行修改,而Kudu是基于全新的设计,因此可以更充分地利用RAMI/O资源,并优化CPU利用率。我们可以理解为,Kudu相比与以往的系统,CPU使用降低了,I/O的使用提高了,RAM的利用更充分了。

 

简介

        Kudu设计之初,是为了解决一下问题:

  • Ÿ  对数据扫描(scan)和随机访问(random access)同时具有高性能,简化用户复杂的混合架构
  • Ÿ  CPU效率,使用户购买的先进处理器的的花费得到最大回报
  • Ÿ  IO性能,充分利用先进存储介质
  • Ÿ  支持数据的原地更新,避免额外的数据处理、数据移动
  • Ÿ  支持跨数据中心replication

        Kudu的很多特性跟HBase很像,它支持索引键的查询和修改。Cloudera曾经想过基于Hbase进行修改,然而结论是对HBase的改动非常大,Kudu的数据模型和磁盘存储都与Hbase不同。HBase本身成功的适用于大量的其它场景,因此修改HBase很可能吃力不讨好。最后Cloudera决定开发一个全新的存储系统。

        Kudu的定位是提供”fast analytics on fast data”,也就是在快速更新的数据上进行快速的查询。它定位OLAP和少量的OLTP工作流,如果有大量的random accesses,官方建议还是使用HBase最为合适。

架构与设计

1.基本框架

        Kudu是用于存储结构化(structured)的表(Table)。表有预定义的带类型的列(Columns),每张表有一个主键(primary key)。主键带有唯一性(uniqueness)限制,可作为索引用来支持快速的random access

类似于BigTableKudu的表是由很多数据子集构成的,表被水平拆分成多个Tablets. Kudu用以每个tablet为一个单元来实现数据的durabilityTablet有多个副本,同时在多个节点上进行持久化。

        Kudu有两种类型的组件,Master ServerTablet ServerMaster负责管理元数据。这些元数据包括talbet的基本信息,位置信息。Master还作为负载均衡服务器,监听Tablet Server的健康状态。对于副本数过低的TabletMaster会在起replication任务来提高其副本数。Master的所有信息都在内存中cache,因此速度非常快。每次查询都在百毫秒级别。Kudu支持多个Master,不过只有一个active Master,其余只是作为灾备,不提供服务。

        Tablet Server上存了10~100Tablets,每个Tablet3(或5)个副本存放在不同的Tablet Server上,每个Tablet同时只有一个leader副本,这个副本对用户提供修改操作,然后将修改结果同步给followerFollower只提供读服务,不提供修改服务。副本之间使用raft协议来实现High Availability,当leader所在的节点发生故障时,followers会重新选举leader。根据官方的数据,其MTTR约为5秒,对client端几乎没有影响。Raft协议的另一个作用是实现ConsistencyClientleader的修改操作,需要同步到N/2+1个节点上,该操作才算成功。

        Kudu采用了类似log-structured存储系统的方式,增删改操作都放在内存中的buffer,然后才merge到持久化的列式存储中。Kudu还是用了WALs来对内存中的buffer进行灾备。

2.列式存储

        持久化的列式存储存储,与HBase完全不同,而是使用了类似Parquet的方式,同一个列在磁盘上是作为一个连续的块进行存放的。例如,图中左边是twitter保存推文的一张表,而图中的右边表示了表在磁盘中的的存储方式,也就是将同一个列放在一起存放。这样做的第一个好处是,对于一些聚合和join语句,我们可以尽可能地减少磁盘的访问。例如,我们要用户名为newsycbot

的推文数量,使用查询语句:

SELECT COUNT(*) FROM tweets WHERE user_name = ‘newsycbot’;

        我们只需要查询User_name这个block即可。同一个列的数据是集中的,而且是相同格式的,Kudu可以对数据进行编码,例如字典编码,行长编码,bitshuffle等。通过这种方式可以很大的减少数据在磁盘上的大小,提高吞吐率。除此之外,用户可以选择使用通用的压缩格式对数据进行压缩,如LZ4, gzip, bzip2。这是可选的,用户可以根据业务场景,在数据大小和CPU效率上进行权衡。这一部分的实现上,Kudu很大部分借鉴了Parquet的代码。

        HBase支持snappy存储,然而因为它的LSM的数据存储方式,使得它很难对数据进行特殊编码,这也是Kudu声称具有很快的scan速度的一个很重要的原因。不过,因为列式编码后的数据很难再进行修改,因此当这写数据写入磁盘后,是不可变的,这部分数据称之为base数据。KuduMVCC(多版本并发控制)来实现数据的删改功能。更新、删除操作需要记录到特殊的数据结构里,保存在内存中的DeltaMemStore或磁盘上的DeltaFIle里面。DeltaMemStoreB-Tree实现的,因此速度快,而且可修改。磁盘上的DeltaFIle是二进制的列式的块,和base数据一样都是不可修改的。因此当数据频繁删改的时候,磁盘上会有大量的DeltaFiles文件,Kudu借鉴了Hbase的方式,会定期对这些文件进行合并。

3.对外接口

        Kudu提供C++JAVA API,可以进行单条或批量的数据读写,schema的创建修改。除此之外,Kudu还将与hadoop生态圈的其它工具进行整合。目前,kudu beta版本对Impala支持较为完善,支持用Impala进行创建表、删改数据等大部分操作。Kudu还实现了KuduTableInputFormatKuduTableOutputFormat,从而支持Mapreduce的读写操作。同时支持数据的locality。目前对spark的支持还不够完善,spark只能进行数据的读操作。

使用案例——小米

小米是Hbase的重度用户,他们每天有约50亿条用户记录。小米目前使用的也是HDFS + HBase这样的混合架构。可见该流水线相对比较复杂,其数据存储分为SequenceFileHbaseParquet

在使用Kudu以后,Kudu作为统一的数据仓库,可以同时支持离线分析和实时交互分析。

性能测试

1. parquet的比较

        图是官方给出的用ImpalaTPC-H的测试,对比ParquetKudu的计算速度。从图中我们可以发现,Kudu的速度和parquet的速度差距不大,甚至有些Queryparquet还快。然而,由于这些数据都是在内存缓存过的,因此该测试结果不具备参考价值。

2.Hbase的比较

        图是官方给出的另一组测试结果,从图中我们可以看出,在scanrange查询上,kuduparquetHBase快很多,而random access则比HBase稍慢。然而数据集只有60亿行数据,所以很可能这些数据也是可以全部缓存在内存的。对于从内存查询,除了random accessHBase慢之外,kudu的速度基本要优于HBase

3.超大数据集的查询性能

        Kudu的定位不是in-memory database。因为它希望HDFS/Parquet这种存储,因此大量的数据都是存储在磁盘上。如果我们想要拿它代替HDFS/Parquet + HBase,那么超大数据集的查询性能就至关重要,这也是Kudu的最初目的。然而,官方没有给出这方面的相关数据。由于条件限制,网易暂时未能完成该测试。下一步,我们将计划搭建10Kudu + Impala服务器,并用tpc-ds生成超大数据,来完成该对比测验。

Categories: 大数据 Tags:

HBase最佳实践-CMS GC调优

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

HBase发展到当下,对其进行的各种优化从未停止,而GC优化更是其中的重中之重。从0.94版本提出MemStoreLAB策略、Memstore Chuck Pool策略对写缓存Memstore进行优化开始,到0.96版本提出BucketCache以及堆外内存方案对读缓存BlockCache进行优化,再到后续2.0版本宣称会引入更多堆外内存,可见HBase会将堆外内存的使用作为优化GC的一个战略方向。然而无论引入多少堆外内存,都无法避免读写全路径使用JVM内存,就拿BucketCache中offheap模式来讲,即使HBase数据块是缓存在堆外内存的,但是在读取的时候还是会首先将堆外内存中的block加载到JVM内存中,再返回给用户。可见,无论使用多少堆外内存,对JVM内存的使用终究是绕不过去,既然绕不过去,就还是需要落脚于GC本身,对GC本身进行优化。本文就将会介绍HBase应用场景下CMS GC策略的调优技巧,后续还会针对另一业界开始使用的GC策略-G1GC策略在HBase应用场景下进行调优介绍。

CMS GC工作原理

如果看官已经对CMS GC工作原理比较熟悉,完全可以跳过本节内容,直接进入下节。如果看官还对CMS GC不是很了解,可以参考笔者之前的另一篇文章《HBase GC的前生今生-身世篇》,文中对JVM的内存结构以及CMS GC进行了相当详细的介绍。为了下文介绍方便,在此还是对其中的一些重要知识点进行提炼:

1. 整个JVM内存由Young区、Tenured区和Perm区三部分组成,其中Young区又分为一个Eden区和两个Survivor区

2. 整个对象生命周期简要说明(一定要烂熟于心,下文会一直用到):

(1)Young区:一个对象初始化之后,首先会进入Eden区,当Eden区满之后会触发一次Minor GC,Minor GC会检查Eden区所有对象是否依旧存活(是否有其他对象引用),如果存活,会将其从Eden区拷贝到Survivor区,并将这些存活对象的age加一,而死亡的对象会被作为垃圾回收。此时Eden区又空闲出来,等新对象填充,填充满之后再会触发Minor GC,如此往复。需要注意的是,每执行一次Minor GC,存活对象的age就会加一。

(2)Tenured区:一旦存活对象的age超多一定阈值就会晋升到Tenured区,因此可以理解为Tenured区一般存放长寿对象。很显然,随着时间流逝,Tenured区也会被填充满,此时就会触发CMS GC(old gc),这种GC相对比较复杂,由5个步骤组成,详见参考文章。

3. 无论是Minor GC还是CMS GC,都会’Stop-The-World’,即停止用户的一切线程,只留下gc线程回收垃圾对象。其中Minor GC的STW时间主要耗费在复制阶段,CMS GC的STW时间主要耗费在标示垃圾对象阶段。

GC调优目标

上节简单介绍了Java虚拟机的内存结构以及Java GC的基本知识,接下来会在此基础上介绍HBase集群中GC的几种参数调优技巧。在介绍具体的调优技巧之前,有必要先来看看GC调优的最终目标和基本原则:

1. 平均Minor GC时间尽可能短。因为整个Minor GC都处于STW,因此短时间Minor GC会使用户读写更加平稳,延迟可控。

2. CMS GC次数越少越好。时间越短越好。一方面是因为一次CMS GC一般都会引起至少秒级的应用暂停,对用户读写影响较大;另一方面频繁的CMS GC会产生大量的内存碎片,严重的时候会引起Full GC,导致RegionServer宕机。

下面对参数的调优技巧都谨遵以上原则,尤其对于HBase这类延迟敏感性项目而言,在尽量避免严重影响用户读写的情况下使得GC更加平稳、暂停时间更短!

CMS GC优化技巧

本节会针对HBase这一应用场景对JVM的各种GC参数进行分析,主要分三个阶段进行。第一阶段会介绍适用于所有场景下的GC参数配置,这些参数不需要太多解释读者就可以轻松理解;第二阶段和第三阶段分别就两组参数进行调优讲解,这两组参数一般会根据不同的应用场景进行设置才能使得GC效果最好,鉴于这两组参数的复杂性,我们会通过理论+实验的方式一一进行说明;

阶段一:默认推荐配置

在介绍具体的调优技巧之前,先来看看CMS GC涉及到的所有相关参数及其对应的意义,下面是最常见的参数:

-Xmx -Xms -Xmn -Xss -XX:MaxPermSize= M -XX:+SurvivorRatio=S  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled -XX:+MaxTenuringThreshold=N -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=C -XX:-DisableExplicitGC

通过上文对各个GC参数的说明,可以轻松得出第一阶段推荐的参数设置如下,这样的设置基本适用于所有的场景:

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled  -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75% -XX:-DisableExplicitGC


调优预准备

上文通过解释各个GC参数意义给出了基本的推荐设置,同时也提到几个对性能影响重大的参数:Xmn、SurvivorRatio以及MaxTenuringThreshold,下面会通过理论推理+实验验证的方式对这几个参数在HBase系统的设置进行调优。在深入介绍调优技巧之前,需要额外针对三个相关部分预先做下讲解,这样可以更好地理解下文的实验数据分析。这三部分分别是:测试环境+测试基本条件,GC日志解释,HBase场景内存分析;

测试环境

首先就下文中实验测试的硬件拓扑、软件配置以及相关测试数据情况进行说明:


需要强调的是HBase全部配置为BucketCache模式,而不是LruBlockCache。使用了大量堆外内存作为读缓存,在很大程度上优化了GC,如下图:


上图是在两种缓存策略下GC表现情况,可见BucketCache模式比LruBlockCache模式GC表现好很多,强烈建议线上配置BucketCache模式。可能很多童鞋都测试过这两种模式下的GC、吞吐量、读写延迟等指标,看到测试结果都会很疑惑,BucketCache模式下的各项性能指标都比LruBlockCache差了好多,笔者也疑惑过,后来才明白:测试肯定是在基本全内存场景下进行的,这种情况下确实会是如此。读者可以想想为什么会如此,实在不明白可以参考之前一篇博文《BlockCache方案性能对比测试报告》。但是话又说回来,在大数据场景下又有多少业务会是全内存操作呢?

GC日志分析

介绍完实验基本条件后,再对GC日志进行简单的解释,方便下文对日志进行分析。需要注意只有在添加参数-XX:+PrintTenuringDistribution才能打印对应日志,强烈建议线上集群开启该参数,日志片段如下:

2016-07-26T10:37:16.933+0800: 227753.150: [GC2016-07-26T10:37:16.933+0800: 227753.150: [ParNew
Desired survivor size 268435456 bytes, new threshold 5 (max 15)
- age   1:   57523184 bytes,   57523184 total
- age   2:   80236520 bytes,  137759704 total
- age   3:   73226496 bytes,  210986200 total
- age   4:   50318392 bytes,  261304592 total
- age   5:   63166384 bytes,  324470976 total
- age   6:        240 bytes,  324471216 total
: 1268903K->305311K(1572864K), 0.0840620 secs] 26598675K->25635082K(66584576K), 0.0844700 secs] [Times: user=1.82 sys=0.08, real=0.08 secs]

上述日志片段分三部分进行解释:

第一部分:基本信息区,主要有两点需要重点关注,其一是Desired survivor size 268435456 bytes,表示Survivor区大小为256M;其二是new threshold 5 (max 15),表示对象晋级老生代的最大阈值为15,但是因为Survivor区太小导致age大于5的对象会直接溢出晋级老生代(也有可能是阈值设置太大)。

第二部分:不同age对象分布区,第一列表示该Young区共分布有age在1~6的对象;第二列表示所在age含有的对象集所占内存大小,比如age为2的所有对象总大小为80236520 bytes;第三列表示小于对应age的所有对象占用内存的累加值,比如age2对应第二列137759704 total表示age为1和age为2的所有对象总大小;

第三部分:内存回收信息区,第一列表示Young区的内存回收情况,1268903K->305311K表示Young区回收前内存为1268903K,回收后变为305311K;第二列表示Jvm Heap的内存回收情况,26598675K->25635082K(66584576K) 表示当前Jvm总分配内存为66584576K,回收前对象占用内存为26598675K,回收后对象占用内存为25635082K;第三列表示回收时间,其中real表示本次gc所消耗的STW时间,即用户业务暂停时间。

HBase场景内存分析

通常来讲,每种应用都会有自己的内存对象特性,分类来说无非就两种:一种是短寿对象(指存活对象较短的对象,比如临时变量等)居多工程,比如大多数纯HTTP请求处理工程,短寿对象可能占到所有对象的70%左右;另一种是长寿对象(指存活对象较长的对象,比如TTL设置较长的缓存对象)居多工程,比如类似于HBase、Spark等这类大内存工程。具体以HBase为例,来看看具体的内存对象:

1. RPC请求对象,比如Request对象和Response对象,一般这些对象会随着短连接RPC的销毁而消亡,这些对象可以认为是短寿对象;

2. Memstore对象,HBase中Memstore中对象一般会持续存活较长时间,用户写入数据到Memstore中之后对象就一直存在,直至Memstore写满之后flush到HDFS。一般在写入QPS较高的情况下写满memstore也通常需要一个小时左右,可见Memstore对象肯定是长寿对象。另外,Memstore对象默认比较大,2M大小。

3. BlockCache对象,和Memstore对象一样,BlockCache对象一般也会在内存存活较长时间,属于长寿对象。这种对象默认64K大小。

因此可以看出,HBase系统属于长寿对象居多的工程,因此GC的时候只需要将RPC这类短寿对象在Young区淘汰掉就可以达到最好的GC效果。

阶段二:NewParSize调优

理论分析

NewParSize表示young区大小,而young区大小直接决定minor gc的频率。minor gc频率一方面决定单次minor gc的时间长短,gc越频繁,gc时间就越短;一方面决定对象晋升到老年代的量,gc越频繁,晋升到老年代的对象量就越大。解释起来就是:

1. 增大young区大小,minor gc频率降低,单次gc时间会较长(young区设置更大,一次gc就需要复制更多对象,耗时必然比较长),业务读写操作延迟抖动较大。反之,业务读写操作延迟抖动较小,比较平稳。

2. 减小young区大小,minor gc频率增快,但会加快晋升到老年代的对象总量(每gc一次,对象age就会加一,当age超过阈值就会晋升到老年代,因此gc频率越高,age就增加越快),潜在增加old gc风险。

因此设置NewParSize需要进行一定的平衡,不能设置太大,也不能设置太小。

实验结果

实验条件:分为独立对照试验,三台RegionServer分别设置Xmn为512m、2g、5g,Xmn越大,分配的Young区越大;SurvivorRatio和MaxTenuringThreshold取默认值;

实验结果曲线图:


结果分析

1. 图一是Xmn不同场景下总体的GC耗时曲线图,其中横坐标表示GC次数,纵坐标表示GC耗时(STW),单位ms。需要特别说明的是,这3条曲线是在相同时间段统计的,也就是说在这段时间内Xmn为512m的情况下GC次数最多,而相应的Xmn为5的情况下GC次数最少。

2. 图一整体上来看绿线尖峰很多而且很高,表示CMS GC较频繁,但绿线主体部分处于红线与蓝线之下,表示平均Minor GC耗时更短;蓝线GC次数最少,尖峰也比较突出,另外Minor GC相比红线和绿线耗时更长;红线的Minor GC耗时介于蓝线和绿线之间,尖峰比较平稳,表示CMS GC相对比较短暂;因此总体来看,红线代表的Xmn为2的场景下CMS GC更加合理,平均Minor GC相对不高,而相比之下,另外两种场景都有特别明显的缺陷,Xmn=2是一个最优的选择;图一只能直观上看出这么多,更加精确结果需要接着看图二和图三。

3. 图二主要统计Minor GC的主要指标:总GC次数以及平均单次Minor GC耗时。两者来看,更关注后者,因为后者决定了业务读写的延迟以及稳定度;由图中可以看出,Xmn512m的平均单次Minor GC耗时最少,其次是Xmn2g,最差是Xmn5g,达到了130ms左右,意味着在其Minor GC过程中所有业务读写延迟至少为130ms;这个也很好理解,Young区越小,Minor GC频率越高,单次Minor GC需要复制的对象数就越少,耗时越少;

4. 图三主要统计CMS GC(老年代GC)的主要指标:CMS GC次数以及平均单次老年代GC耗时(只算STW耗时);由图中可以看出,Xmn2g无论是GC次数还是GC耗时都更加优秀,相比之下Xmn512m就是最差的选择;解释起来也很简单,因为Young区设置太小,Minor GC频率高,对象age增加很快,很多对象就有可能因为age超过阈值(默认6)晋升到老年代,相对而言会更有可能引入大量短寿对象晋升老年代。而短寿对象相对而言会比较小,比如request、response等,大量小对象一旦进入老年代,就会导致CMS GC的时候需要标注更多对象,必然比较耗时;

实验结论

可见,测试结果基本和理论分析一致,Xmn设置过小会导致CMS GC性能较差,而设置过大会导致Minor GC性能较差,因此建议在JVM Heap为64g以上的情况下设置Xmn在1~3g之间,在32g之下设置为512m~1g;具体最好经过简单的线上调试;需要特别强调的是,笔者在很多场合都看到很多HBase线上集群会把Xmn设置的很大,比如有些集群Xmx为48g,Xmn为10g,查看日志发现GC性能极差:单次Minor GC基本都在300ms~500ms之间,CMS GC更是很多超过1s。在此强烈建议,将Xmn调大对GC(无论Minor GC还是CMS GC)没有任何好处,不要设置太大。

阶段三:增大Survivor区大小(减小SurvivorRatio) & 增大MaxTenuringThreshold

理论分析

上文讲过,一次Minor GC会将存活对象从Eden区(以及survivor from区)复制到Survivor区(to区),因此增大Survivor区可以容纳更多的存活对象。这样就会防止因为Survivor区太小导致很对存活对象还没有达到MaxTenuringThreshold阈值就直接进入老生代,潜在增大old gc的触发频率;但是Survivor区设置太大也会有一定的问题,Survivor设置较大会使得对象可以在Young区’待’的时间很长,但是对于一些长寿对象较多的场景下(比如HBase),大量长寿对象长时间待在Young区做很多’无谓’的复制,一定程度上增加Minor GC开销。

另外,增加MaxTenuringThreshold相当于提高了进入老年代的门槛,可以有效限制进入老年代的对象数。和Survivor设置相似,调整MaxTenuringThreshold也需要做一个取舍,设置太小会增加CMS GC的触发频率以及耗时,而设置太大则会在长寿对象较多场景下增加Minor GC开销。一般情况下,默认MaxTenuringThreshold=15已经相对比较大,不需要做任何调整。

实验结果

实验条件:分为独立对照试验,三台RegionServer分别设置SurvivorRatio为2、8、15,SurvivorRatio越大,Survivor区大小越小;MaxTenuringThreshold取默认值;其他:-Xmx64g,-Xmn2g;

实验结果曲线:

结果分析

1. 图一是SurvivorRatio在三种不同场景下对应的GC性能曲线图,大体可以看出蓝线Minor GC次数最多,绿线尖峰太多,即CMS GC性能最差;具体细节再来看图二和图三。

2. 图二主要统计Minor GC主要指标:平均单次Minor GC耗时三者基本相当,SurvivorRatio:2场景下稍微较高,这是因为SurvivorRatio=2对应的Survivor区较大,可以使得对象在Young区’待’的时间很长,在HBase这种长寿对象较多的情况下,可能会增加一些无谓的‘复制’开销(下文会通过日志分析详细解释)。另外,SurvivorRatio=2场景下Minor GC频率也比较高,可能的原因是因为在总Young大小确定的情况下,Survivor越大,Eden自然越小,Minor GC频率就会增大。可见,SurvivorRatio=2场景下Minor GC性能相对稍微较差。

3. 图三主要统计CMS GC主要指标:三者CMS GC次数基本相当,SurvivorRatio=2场景下单次CMS GC耗时最少,相比SurvivorRatio=8的场景耗时减少30%左右,性能最好;而相比之下SurvivorRatio=15场景下耗时最长,性能相当差;这是因为SurvivorRatio=2场景下存活对象可以长时间待在Young区,可以得到充分的淘汰,晋升到老生代的短寿小对象会比较少,因而CMS GC性能较好;相比SurvivorRatio=15会因为Survivor区设置太小,很多短寿小对象因为得不到充分的淘汰就会‘溢出’到老生代,导致CMS性能很差。

实验结论

可见,测试结果基本和理论分析也基本一致,对于Minor GC来说,SurvivorRatio设置对其影响不是很大。而对于CMS GC来说,将SurvivorRatio设置过大简直就是灾难,性能极其差。而和默认值SurvivorRatio=8相比,将SurvivorRatio调大有利于短寿小对象更充分地淘汰,因此建议将SurvivorRatio=2

CMS调优结论

1. 缓存模式采用BucketCache策略Offheap模式

2. 对于大内存(大于64G),采用如下配置:

-Xmx64g -Xms64g -Xmn2g -Xss256k -XX:MaxPermSize=256m -XX:+SurvivorRatio=2  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
-XX:+CMSParallelRemarkEnabled -XX:+MaxTenuringThreshold=15 -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly        
-XX:CMSInitiatingOccupancyFraction=75 -XX:-DisableExplicitGC

其中Xmn可以随着Java分配堆内存增大而适度增大,但是不能大于4g,取值范围在1~3g范围;SurvivorRatio一般建议选择为2;MaxTenuringThreshold设置为15;

3 对于小内存(小于64G),只需要将上述配置中Xmn改为512m-1g即可

总结

本文首先比较系统的介绍了CMS GC的相关知识,之后分三个阶段层层推进对HBase集群中相关重要参数的调优进行了详细说明,尤其后面两阶段通过理论推理以及实验验证的方式对两组核心参数进行了针对性调整,最终得出一个较为完整的CMS GC参数配置。读者可以参考该参数配置对集群进行调整,再通过日志查看调整效果~

Categories: HBase Tags:

ceph恢复优化

August 1st, 2016    阅读(216) 1 comment
1.背景
在对ceph块存储进行性能测试时发现,当有osd重启或者存储机重启时,I/O性能会急剧下降,尤其在随机写的负载下,下降幅度达到90%,并且会持续一段时候才慢慢恢复到正常水平。一开始我们也尝试将恢复相关的参数调低(osd_recovery_max_active=1、osd_recovery_max_chunk=131072、osd_max_backfills=1),以及调整正常I/O和恢复I/O的优先级(osd_recovery_op_priority=10、osd_client_op_priority=63),但是测试结果来看,这些参数的调整没有多大的效果,随机写负载下性能下降仍然很大。
2.原因分析
在《ceph基于pglog的一致性协议》一文中分析了ceph的一致性协议,从中我们得知在osd重启、存储机重启等场景下基于pglog的恢复的时候,在peering的时候会根据pglog来构建出missing列表,然后在恢复时根据missing列表逐个进行恢复,恢复的粒度是整个对象大小(默认4MB,有可能有的对象不足4MB,就按对象大小),即使只修改了一个4KB,也需要将4MB的对象拷贝过来,这样100个io就会达到400MB的带宽,对网络及磁盘产生较大的影响。当写io命中正在修复的对象时,也是先修复原来4MB的对象,即需要将4MB的数据通过网络拷贝过来,延迟就会增加很多,然后再写入数据,对于随机写的场景尤其严重,基本都是命中的情况,带来相当大的延迟,从而iops下降(下降幅度80%~90%)。这个影响时间取决于上层的业务量和osd停服的时间。
origin-recover
3.恢复优化
因为pglog里每条记录里只是记了操作及版本等信息,并没有记录这次操作是修改哪部分数据,所以优化的办法就是在pglog里记录每次操作修改的区间,记为[offset,len](实现时pglog结构里引入dirty_extents来表示),正常写I/O处理时,会写pglog,对应的是一个MODIFY操作,在这条记录里记上修改的区间。
当一个osd故障重启后,进行peering的时候,合并pglog来构建missing列表时就可以将同一个对象的多次修改的[offset,len]求一个并集,得出故障期间总的修改区间,然后在恢复的时候就能够根据这个范围来恢复这部分增量数据,从而大幅度减少了恢复时的网络和磁盘带宽,以及正常I/O命中恢复对象的等待时间,从而大幅降低对正常I/O的性能影响(尤其是对于随机写I/O的场景)。
recover-optimize
针对我们使用的块存储的应用场景,为了减小实现的复杂度,引入一个标记can_recover_partial来表示是否可以进行部分恢复,默认是false,当进行写I/O的处理时记录pglog里的[offset,len],并且标记can_recover_partial为true,然后恢复时就可以根据这个标记只恢复对象内的增量数据。对于无法判断是否可以进行部分优化时,就回退到原有的恢复逻辑去恢复整个对象,需要考虑几种情况,包括:truncate、omap、clone、EC等。
truncate
truncate的操作对应的就是截断对象,可以类比于文件系统里的ftruncate的操作(可以将文件截断,或者将文件扩大)。原生ceph的pglog没有TRUNCATE的操作码,它里面的truncate操作都是MODIFY操作,如果一个对象先被修改了,然后又被truncate了,那么在恢复这个对象的时候仍然是按照这整个对象来恢复,有可能因为truncate这个对象变小了(小于4MB),这个不用管, 直接将这个对象读出来拷贝过去就行了。
但是我们要在pglog里加入[offset, len]来表示修改范围,那么和truncate混在一起后,就需要做区分,为了简单起见,当碰到truncate操作时,将can_recover_partial设置成false,这样truncate的处理就跟原来一样,在我们的使用场景下,trucate操作不常见。
omap
omap即objectmap,用来记录对象的扩展属性,因为文件系统的xatrr属性数量及长度都有限制,超过了就需要放到omap中。设置omap属性时,对应到pglog里也是一个MODIFY操作,也就是说omap在多副本间的一 致性也是由pglog来保证的。在恢复的时候合并pglog来构建missing对象时,是不区分属性还是数据,都认为是对这个对象的修改,在恢复的时候都是先恢复恢复属性(xatrr和omap),然后再恢复数据。这样的话,即使一个对象在pglog里只设置了属性,在恢复的时候会连数据一起恢复过来,也即是多了一次数据的拷贝。在块存储的场景下,这个代价是可以接受的,因此不必针对omap做特殊处理。
clone
这里的clone指的clone对象操作,就是做了快照之后对原卷进行写入时触发的cow操作。每次clone操作会在pglog里记录一条CLONE的记录,然后在filestore里会根据这个CLONE操作进行clone_range的处理,也就是从原来的head对象(表示卷的数据对象),拷贝数据到新生成的snap对象(即快照的对象)。在peering的时候合并pglog时,没有区分MODIFY和CLONE的,都会构建丢失对象放到missing列表里,也就是说这个missing列表里有可能既包含head对象,也包含snap对象。
在《ceph rbd快照原理解析》里详细介绍了快照的实现及故障恢复的处理,引入恢复优化后,对于snap对象还是按照原来的逻辑处理,而对于head对象就要做不同处理,按照对象的修改区间来进行恢复。
EC
块存储下都是采用多副本的策略,而不会用到纠删码(EC),一般是ceph对象存储里才用到纠删码,因此我们不考虑这种情况,遇到纠删码相关的就就按照原有的逻辑进行恢复。
4.优化效果
下面的测试结果对比了优化前和优化后的性能(主要针对随机读写的场景),从结果上来看,效果显著:
1)减少了重启osd过程中对集群正常存储I/O性能影响,从优化前的I/O性能下降90%到优化后I/O性能只下降10%;
2)缩短重启恢复所需要的时间,重启单个osd的恢复时间从10分钟减少到40秒左右;
before-recover-optimize
after-recover-optimize
Categories: ceph, 分布式, 存储 Tags:

Spark 2.0技术预览

July 27th, 2016    阅读(140) No comments
Spark 2.0预览版已出,本文将带你浏览下这个2.0到底有啥流逼的地方。
在正式发布之前,你可以
  1. github下载自己编译 https://github.com/apache/spark
  2. 官网最下方有个很小的连接
  3. https://databricks.com/try-databricks 可以创建预览版集群
本次大版本更新包含三个主题,Easier,Faster,Smarter。
Easier
首先我们看看Easier。Easier的方面主要集中在SQL和流处理方面。
犹如C++编译器会标榜自己对C++标准的支持程度,标准SQL是每个SQL On Hadoop系统都会拿来宣传的看点(如果的确做得不错)。在Spark 2.0中,SQL标准支持得到很大强化。
进一步解释这部分之前,需要先说一下Spark SQL的Parser。Spark SQL有一个原生的Parser通过SQLContext暴露。这个Parser在1.x时代是个很原始的作品,例如在Spark 1.6时,Spark SQL原生Parser可以通过55/99的TPC-DS测试(一个数据分析场景的SQL引擎Benchmark测试),将近一半的测试由于SQL标准兼容问题无法通过。
而Spark SQL内嵌了一个Hive Parser,基本重用了Hive相关的大量代码,这个接口使用HiveContext暴露。大多数用户由于种种兼容问题和功能原因会选择Hive Parser。这样的内嵌模式,对SparkSQL来说带来了不必要的Hive耦合,这部分代码很难维护也不容易增加新的特有功能。
在2.x时代,由于Parser的重写,标准兼容问题得到解决,Spark将逐渐切换到SQLContext。更具体的,在2.0里,99个TPCDS全都能跑通了。
主要增加的支持是:
  1. rollup和cube
  2. intersect和except
  3. select/where/having中使用子查询,例如 select * from t where a = (select max(b) from t2)
  4. 窗口函数支持
  5. IN、EXIST谓词支持子查询 where col in (select …)这样
  6. natural join (根据字段名自动匹配join)
这些对于习惯HiveContext的用户来说可能不是一个值得大书特书的东西,但是这个标志着Spark和Hive会逐渐解耦,甚至完全独立,而SparkSQL的功能演进将会更自由。
Easier主题的第二个更重要的部分是API大一统。Spark API演进文档
由于Spark的快速迭代和演进,Spark暴露的API体系越来越多,很多是实现类似功能的,有些是由于原有API不方便又要向下兼容而另开了新坑,这些都将在2.0进行整合。
这部分整合包含了:
  1. HiveContext,SQLContext以及SparkContext将整合到SparkSession中。SparkSession将成为大入口,包含Dataset生成,Catalog支持(包含Spark本身的Catalog和Hive Catalog支持),配置相关接口和集群环境相关接口。
  2. Dataset和Dataframe整合:Dataframe将被整合进Dataset API。
  3. Streaming API和Batch整合:同一套API用于Streaming和Batch。在此之前只能用流API加原生的RDD API进行流计算。RDD API是Spark最早的原生API,抽象程度较低,也没有Schema。这部分也是Smarter的主要内容,之后会做更多论述。
  4. Java API和Scala API整合:在此之前Java有自己的JavaRDD接口。现在都将使用Dataset API。
这里需要大概介绍一下RDD,Dataset和Dataframe。
RDD原生API是Spark最早的API体系,它类型安全,操作有序,但是是非常底层的API。它的代码类似如下:
val data = sc.textFile("/Users/ilovesoup/test.data")
val scoreRDD = res0.map{case Array(dept, score) => (dept, score.toInt)}
scoreRDD.reduceByKey(_ + _)
.filter{ case (dept, score) => score > 200 }.collect()
.foreach(x => println(x._1 + "," + x._2))
Dataframe API是在Row集合上的操作,而Row可以理解成类似数据库的记录行,将数据切分成多个列,每个列有自己的名字。所以DataFrame的操作更类似数据库API,是将原生RDD API的抽象和打包。更主要的是,DataFrame由于接近SQL的API模型,将完全享受Catalyst优化器(Catalyst是SparkSQL的优化器,类似数据库引擎的执行计划优化模块),而原生的RDD由于过于自由的API,则无法享受这一优化。考虑C++和汇编,由于C++是高级语言,比汇编有更多限定,因此编译器可以更多进行优化,这里是类似的道理。
DataFrame的坏处是,动态类型。因为解析的时候Schema还没有注入,无法完整静态类型检查,因此无法做到类型安全。
DataFrame的代码是这样的:
val df = scoreRDD.toDF("dept", "score")
df.groupBy($"dept").agg(sum($"score"))
.filter($"sum(score)" > 200).limit(100).collect()
.foreach(row => println(row(0).asInstanceOf[String] 
                    + "," + row(1).asInstanceOf[Long]))
而DataSet API则是类型安全的类数据库API。DataSet支持lambda API(比如x => x + 1这样),有类型支持并享受Catalyst优化。又由于它也有Schema支持,因此存储的时候能根据类型序列化到Native内存上(而非Java堆空间),无须使用Java Object,因此速度更快更省内存。
这里附一份官网的图
它的代码看起来这样:
val counts = words 
    .groupBy(_.toLowerCase)
    .count()
对比RDD原生操作:
val counts = words
    .groupBy(_.toLowerCase)
    .map(w => (w._1, w._2.size))
在2.0中,DataSet和DataFrame进行了整合,DataFrame将成为DataSet[Row]的同义词,底层使用同一套API。这套API将作为Structured Streaming统一API。
由于编译器类型检查在R和Python中无效,因此Dataset只用于Scala和Java。
API分为Typed和Untyped
case class Score(dept: String, score: Int)
val ds = df.as[Score]
// 官网的预览版还不能执行这个,可能需要去官云上跑
import org.apache.spark.sql.expressions.scala.typed
ds.groupByKey(_.dept).agg(typed.sum(_.score))

Untyped

// 这里用列名引用,类型丢失,就像Dataframe中

ds.groupBy("dept").agg(sum("score")).collect()
 
Faster
第二个大主题是Faster(更快)。其中最主要的一个变更是WholeStage CodeGen。
SparkSQL会将一个Plan拆分成不同Operator的组合。例如从官网上摘抄的例子:
也许你知道Spark 1.x时代,对于某些SQL Operator,Spark会进行独立的代码生成,每个Operator的代码段互相之间并不直接关联,这样做实现看起来比较优雅和简洁,生成的代码也有一定的封装性,看起来妥妥滴软件工程设计,但是,慢。
2.0新增的一个很重要优化是就是在这块做文章。新的代码生成器在可能的情况下将把不同的Operator放在一起生成一个大块的处理函数。这样的好处是,去掉了冗余的operator之间的数据传递(这个可能是性能开销的大头),并且一个大的代码段不跨越函数边界,将更优可能被JVM JIT优化。终极目标是,生成的代码和手写代码有近似的速度。
看一下下面这个例子:
select id + 1 from students where id <> 0;
上面的Plan的计算部分将会有两个Operator:id+1对应Projection Operator,id <> 0对应Filter Operator。新的Spark代码生成可以将这两个部分生成一个大的代码块进行计算。

这里一些看起来奇怪的标志位是用来做NULL判断的。这点是CodeGen比较麻烦的地方,因为为了速度,CodeGen会将Row中的数据读取到Primitive的Java变量中,而Primitive类型是无法接受空值的。但是SQL语义中空值是可以参与计算的,要保持这一语义,必须做一些奇奇怪怪的标志位对空值做处理。另外由于代码是跟着AST进行生成的,因此id<>0将产生两步骤计算:先计算id=0然后取反。

while (inputadapter_input.hasNext()) { // 从一组Row中不断获取
    InternalRow inputadapter_row = (InternalRow) inputadapter_input.next();
   // row的字段名被转换成对应的下表,这里是判断某个字段是否为空;
   // 因为null在SQL中是可以和其他primitive一起计算而不会抛出异常的,因此这里需要特殊处理
    boolean inputadapter_isNull = inputadapter_row.isNullAt(0); 
   // 为字段0的primitive Java变量赋值
    int inputadapter_value = inputadapter_isNull ? -1 : (inputadapter_row.getInt(0));
    if (!(!(inputadapter_isNull))) continue; // Filter部分
    boolean filter_value3 = false; // 阶段filter结果标志初始化
    filter_value3 = inputadapter_value == 0; // 判断 id = 0
    boolean filter_value2 = false; // 最终filter标志
    filter_value2 = !(filter_value3); // 计算 !(id = 0)
    if (!filter_value2) continue; // 如果filter不符合则跳过当前row

    filter_numOutputRows.add(1); // 计数器

    int project_value = -1; // Projection值初始化
    project_value = inputadapter_value + 1; // 计算id + 1
    project_rowWriter.write(0, project_value); // 写出结果
    append(project_result); // 添加到输出buffer
    if (shouldStop()) return;
}
再看聚合操作。新的聚合代码生成看起来更像一个手写聚合的代码框架,一个大循环迭代一大批数据。
select sum(id + 888) from students where id <> 999;
class GeneratedIterator {
    private long agg_bufValue;
    private void agg_doAggregateWithoutKey() {
        while (inputadapter_input.hasNext()) {
...         // 上面省略了filter逻辑,和之前的例子类似
            agg_value9 = inputadapter_value + 888;
...         // v7 = v9 中间代码为了对应各种空值判断 
            agg_value2 = agg_value3 + agg_value7;
...         // v1 = v2,产生理由同上,agg_bufValue是类成员
            // 大循环结束之后将产生局部sum结果
            agg_bufValue = agg_value1; 
        }
    }
}
除了WholeStage CodeGen之外,向量化是另一个大的优化点。所谓向量化是说,一次返回一组列的值在一次函数调用中一起处理,这样能减小虚函数调用次数并可以做SIMD加速(一个指令多个数据同时处理,CPU级优化)。这个优化和Wholestage codegen并不共存。用在例如Parquet Reader的解码部分。
Smarter
最后是Smarter主题(更智能)。这里主要是说之前提到的Batch和Streaming API统一。
2.0新的Streaming API超越了之前独立Streaming框架的范畴,与Batch处理合二为一,让同一套API框架得以作用于流计算和批处理。其中的哲学是:简单的方式对流数据进行计算,而不用考虑这其实是一个流。
这包含了:
  1. Dataset API能描述Streaming,而不再用Dstream
  2. 直接对Streaming进行Ad-hoc SQL查询(SQL运行时可变)
  3. 直接对Streaming进行机器学习(2.1)
  4. 用Catalyst优化器对流计算进行优化
  5. 统一的模型将统一享受未来的Tungsten优化(包括刚才提到的Codegen等)
上面两个是JIRA和设计文档,到2016-5-27为止Ticket本身还没关闭。
这个文章提出了一个模型:Repeated Queries (RQ)
这个模型尝试让Streaming上的Query犹如在静态表上查询一样,并尝试将静态查询的概念一一映射到流查询上。
逻辑上来说,streaming是一个append-only表,数据根据系统处理时间到达(这个时间是表的一个字段)。而查询就是在这个表上进行的。
用户创建基于处理时间的触发条件(支持ASAP,到达就处理)
用户定义输出模式,可以是delta(只输出增删),append模式(只增不减),或者Update模式(在线修正,比如更新数据库),或者快照(每次全量)。
相对于很流行的Storm和老版本的DStream来说,RQ模式更灵活也更方便。
Storm API过于底层,难于开发。本身又难以支持基于事件本身时间(相对于事件到达时间)的处理模型。底层处理模型是来一条处理一条(除非外加Trident)。
Dstream也使用系统处理时间,也难于支持事件时间。而底层使用micro-batch模型。并且现有API相对DataSet API也偏底层,开发不灵活。
新的RQ模型支持处理时间和事件时间,并且和底层处理模型无关,还可以支持micro-batch之外的其他处理。另外比较重要的是RQ享受Catalyst查询优化(任何DataSet API入口的程序都可以享受Catalyst优化甚至其他大多数为SQL进行的优化)。
这里贴几个文档附带的范例:
1. ETL-输入小写规整
2. 数据库和流同步
好了,上面就是Spark 2.0的一些新东西,更多东西只能亲手尝试才能体会了。

HBase Compaction的前生今世-改造之路

July 25th, 2016    阅读(0) Comments off

上一篇文章主要基于工作流程对compaction进行了介绍,同时说明了compaction的核心作用是通过合并大量小文件为一个大文件来减少hfile的总数量,进而保证读延迟的稳定。合并文件首先是读出所有小文件的KVs,再写入同一个大文件,这个过程会带来严重的IO压力和带宽压力,对整个系统的读请求和写请求带来不同程度的影响。

因此HBase对于compaction的设计总是会追求一个平衡点,一方面需要保证compaction的基本效果,另一方面又不会带来严重的IO压力。然而,并没有一种设计策略能够适用于所有应用场景或所有数据集。在意识到这样的问题之后,HBase就希望能够提供一种机制可以在不同业务场景下针对不同设计策略进行测试,另一方面也可以让用户针对自己的业务场景选择合适的compaction策略。因此,在0.96版本中HBase对架构进行了一定的调整,一方面提供了Compaction插件接口,用户只需要实现这些特定的接口,就可以根据自己的应用场景以及数据集定制特定的compaction策略。另一方面,0.96版本之后Compaction可以支持table/cf粒度的策略设置,使得用户可以根据应用场景为不同表/列族选择不同的compaction策略,比如:
alter ’table1’ , CONFIGURATION => {‘hbase.store.engine.class’ => ‘org.apache.hadoop.hbase.regionserver.StripStoreEngine’, … } 

上述两方面的调整为compaction的改进和优化提供了最基本的保障,同时提出了一个非常重要的理念:compaction到底选择什么样的策略需要根据不同的业务场景、不同数据集特征进行确定。那接下来就根据不同的应用场景介绍几种不同的compaction策略。

在介绍具体的compaction策略之前,还是有必要对优化compaction的共性特征进行提取,总结起来有如下几个方面:

1. 减少参与compaction的文件数:这个很好理解,实现起来却比较麻烦,首先需要将文件根据rowkey、version或其他属性进行分割,再根据这些属性挑选部分重要的文件参与合并;另一方面,尽量不要合并那些大文件,减少参与合并的文件数。
2. 不要合并那些不需要合并的文件:比如OpenTSDB应用场景下的老数据,这些数据基本不会查询到,因此不进行合并也不会影响查询性能

3. 小region更有利于compaction:大region会生成大量文件,不利于compaction;相反,小region只会生成少量文件,这些文件合并不会引起很大的IO放大

接下来就介绍几个典型的compaction策略以及其适应的应用场景:

FIFO Compaction(HBASE-14468)

FIFO Compaction策略主要参考了rocksdb的实现,它会选择那些过期的数据文件,即该文件内所有数据都已经过期。因此,对应业务的列族必须设置TTL,否则肯定不适合该策略。需要注意的是,该策略只做这么一件事情:收集所有已经过期的文件并删除。这样的应用场景主要包括:

1. 大量短时间存储的原始数据,比如推荐业务,上层业务只需要最近时间内用户的行为特征,利用这些行为特征进行聚合为用户进行推荐。再比如Nginx日志,用户只需要存储最近几天的日志,方便查询某个用户最近一段时间的操作行为等等

2. 所有数据能够全部加载到block cache(RAM/SSD),假如HBase有1T大小的SSD作为block cache,理论上就完全不需要做合并,因为所有读操作都是内存操作。
因为FIFO Compaction只是收集所有过期的数据文件并删除,并没有真正执行重写(几个小文件合并成大文件),因此不会消耗任何CPU和IO资源,也不会从block cache中淘汰任何热点数据。所以,无论对于读还是写,该策略都会提升吞吐量、降低延迟。
开启FIFO Compaction(表设置&列族设置)
HTableDescriptor desc = new HTableDescriptor(tableName);
    desc.setConfiguration(DefaultStoreEngine.DEFAULT_COMPACTION_POLICY_CLASS_KEY, 
      FIFOCompactionPolicy.class.getName());
HColumnDescriptor desc = new HColumnDescriptor(family);
    desc.setConfiguration(DefaultStoreEngine.DEFAULT_COMPACTION_POLICY_CLASS_KEY, 
      FIFOCompactionPolicy.class.getName());

Tire-Based Compaction(HBASE-7055)(HBASE-14477)

之前所讲到的所有‘文件选取策略’实际上都不够灵活,基本上没有考虑到热点数据的情况。然而现实业务中,有很大比例的业务都存在明显的热点数据,而其中最常见的情况是:最近写入到的数据总是最有可能被访问到,而老数据被访问到的频率就相对比较低。按照之前的文件选择策略,并没有对新文件和老文件进行一定的‘区别对待’,每次compaction都有可能会有很多老文件参与合并,这必然会影响compaction效率,却对降低读延迟没有太大的帮助。

针对这种情况,HBase社区借鉴Facebook HBase分支的解决方案,引入了Tire-Based Compaction。这种方案会根据候选文件的新老程度将其分为多个不同的等级,每个等级都有对应等级的参数,比如参数Compation Ratio,表示该等级文件选择时的选择几率,Ratio越大,该等级的文件越有可能被选中参与Compaction。而等级数、每个等级参数都可以通过CF属性在线更新。

可见,Tire-Based Compaction方案通过引入时间等级和Compaction Ratio等概念,使得Compaction更加灵活,不同业务场景只需要调整参数就可以达到更好的Compaction效率。目前HBase计划在2.0.0版本发布基于时间划分等级的实现方式-Date Tierd Compaction Policy,后续我们也重点基于该方案进行介绍。

该方案的具体实现思路,HBase更多地参考了Cassendra的实现方案:基于时间窗的时间概念。如下图所示,时间窗的大小可以进行配置,其中参数base_time_seconds代表初始化时间窗的大小,默认为1h,表示最近一小时内flush的文件数据都会落入这个时间窗内,所有想读到最近一小时数据请求只需要读取这个时间窗内的文件即可。后面的时间窗窗口会越来越大,另一个参数max_age_days表示比其更老的文件不会参与compaction。

1
312737.png

上图所示,时间窗随着时间推移朝右移动,图一中没有任何时间窗包含4个(可以通过参数min_thresold配置)文件,因此compaction不会被触发。随着时间推移来到图二所示状态,此时就有一个时间窗包含了4个HFile文件,compaction就会被触发,这四个文件就会被合并为一个大文件。

对比上文说到的分级策略以及Compaction Ratio参数,Cassendra的实现方案中通过设置多个时间窗来实现分级,时间窗的窗口大小类似于Compaction Ratio参数的作用,可以通过调整时间窗的大小来调整不同时间窗文件选择的优先级,比如可以将最右边的时间窗窗口调大,那新文件被选择参与Compaction的概率就会大大增加。然而,这个方案里面并没有类似于当前HBase中的Major Compaction策略来实现过期文件清理的功能,只能借助于TTL来主动清理过期的文件,比如这个文件中所有数据都过期了,就可以将这个文件清理掉。

因此,我们可以总结得到使用Date Tierd Compaction Policy需要遵守的原则:
1. 特别适合使用的场景:时间序列数据,默认使用TTL删除。类似于“获取最近一小时/三小时/一天”场景,同时不会执行delete操作。最典型的例子就是基于Open-TSDB的监控系统,如下图所示:

2

372163.png
2. 比较适合的应用场景:时间序列数据,但是会有全局数据的更新操作以及少部分的删除操作。
3. 不适合的应用场景:非时间序列数据,或者大量的更新数据更新操作和删除操作。

Stripe Compaction (HBASE-7667)

通常情况下,major compaction都是无法绕过的,很多业务都会执行delete/update操作,并设置TTL和Version,这样就需要通过执行major compaction清理被删除的数据以及过期版本数据、过期TTL数据。然而,接触过HBase的童鞋都知道,major compaction是一个特别昂贵的操作,会消耗大量系统资源,而且执行一次可能会持续几个小时,严重影响业务应用。因此,一般线上都会选择关闭major compaction自动触发,而是选择在业务低峰期的时候手动触发。为了彻底消除major compaction所带来的影响,hbase社区提出了strip compaction方案。

其实,解决major compaction的最直接办法是减少region的大小,最好整个集群都是由很多小region组成,这样参与compaction的文件总大小就必然不会太大。可是,region设置小会导致region数量很多,这一方面会导致hbase管理region的开销很大,另一方面,region过多也要求hbase能够分配出来更多的内存作为memstore使用,否则有可能导致整个regionserver级别的flush,进而引起长时间的写阻塞。因此单纯地通过将region大小设置过小并不能本质解决问题。

Level Compaction

此时,社区开发者将目光转向了leveldb的compaction策略:level compaction。level compaction设计思路是将store中的所有数据划分为很多层,每一层都会有一部分数据,如下图所示:

3

663348.png
1. 数据组织形式不再按照时间前后进行组织,而是按照KeyRange进行组织,每个KeyRange中会包含多个文件,这些文件所有数据的Key必须分布在同一个范围。比如Key分布在Key0~KeyN之间的所有数据都会落在第一个KeyRange区间的文件中,Key分布在KeyN+1~KeyT之间的所有数据会分布在第二个区间的文件中,以此类推。
2. 整个数据体系会被划分为很多层,最上层(Level 0)表示最新数据,最下层(Level 6)表示最旧数据。每一层都由大量KeyRange块组成(Level 0除外),KeyRange之间没有Key重合。而且层数越大,对应层的每个KeyRange块大小越大,下层KeyRange块大小是上一层大小的10倍。图中range颜色越深,对应的range块越大。
3. 数据从Memstore中flush之后,会首先落入Level 0,此时落入Level 0的数据可能包含所有可能的Key。此时如果需要执行compaction,只需要将Level 0中的KV一个一个读出来,然后按照Key的分布分别插入Level 1中对应KeyRange块的文件中,如果此时刚好Level 1中的某个KeyRange块大小超过了一定阈值,就会继续往下一层合并。
4. level compaction依然会有major compaction的概念,发生major compaction只需要将部分Range块内的文件执行合并就可以,而不需要合并整个region内的数据文件。
可见,这种compaction在合并的过程中,从上到下只需要部分文件参与,而不需要对所有文件执行compaction操作。另外,level compaction还有另外一个好处,对于很多‘只读最近写入数据’的业务来说,大部分读请求都会落到level 0,这样可以使用SSD作为上层level存储介质,进一步优化读。然而,这种compaction因为level层数太多导致compaction的次数明显增多,经过测试,发现这种compaction并没有对IO利用率有任何提升。

Stripe Compaction 实现

虽然原生的level compaction并不适用于HBase,但是这种compaction的思想却激发了HBaser的灵感,再结合之前提到的小region策略,就形成了本节的主角-stripe compaction。同level compaction相同,stripe compaction会将整个store中的文件按照Key划分为多个Range,在这里称为stripe,stripe的数量可以通过参数设定,相邻的stripe之间key不会重合。实际上在概念上来看这个stripe类似于sub-region的概念,即将一个大region切分成了很多小的sub-region。

随着数据写入,memstore执行flush之后形成hfile,这些hfile并不会马上写入对应的stripe,而是放到一个称为L0的地方,用户可以配置L0可以放置hfile的数量。一旦L0放置的文件数超过设定值,系统就会将这些hfile写入对应的stripe:首先读出hfile的KVs,再根据KV的key定位到具体的stripe,将该KV插入对应stripe的文件中即可,如下图所示。之前说过stripe就是一个个小的region,所以在stripe内部,依然会像正常region一样执行minor compaction和major compaction,可以预想到,stripe内部的major compaction并不会太多消耗系统资源。另外,数据读取也很简单,系统可以根据对应的Key查找到对应的stripe,然后在stripe内部执行查找,因为stripe内数据量相对很小,所以也会一定程度上提升数据查找性能。
4
884122.png
官方对stripe compaction进行了测试,给出的测试结果如下:
5
532361.png
上图主要测定了在不同的stripe数量以及不同的L0数量下的读写延迟对比情况,参考对照组可以看出,基本上任何配置下的读响应延迟都有所降低,而写响应延迟却有所升高。
6
882584.png
上图是默认配置和12-stripes配置下读写稳定性测试,其中两条蓝线分别表示默认情况下的读写延迟曲线,而两条红线表示strips情况下读写延迟曲线,可以明显看出来,无论读还是写,12-stripes配置下的稳定性都明显好于默认配置,不会出现明显的卡顿现象。
到此为止,我们能够看出来stripe compaction设计上的高明之处,同时通过实验数据也可以明显看出其在读写稳定性上的卓越表现。然而,和任何一种compaction机制一样,stripe compaction也有它特别擅长的业务场景,也有它并不擅长的业务场景。下面是两种stripe compaction比较擅长的业务场景:
1. 大Region。小region没有必要切分为stripes,一旦切分,反而会带来额外的管理开销。一般默认如果region大小小于2G,就不适合使用stripe compaction。
2. RowKey具有统一格式,stripe compaction要求所有数据按照Key进行切分,切分为多个stripe。如果rowkey不具有统一格式的话,无法进行切分。

上述几种策略都是根据不同的业务场景设置对应的文件选择策略,核心都是减少参与compaction的文件数,缩短整个compaction执行的时间,间接降低compaction的IO放大效应,减少对业务读写的延迟影响。然而,如果不对Compaction执行阶段的读写吞吐量进行限制的话也会引起短时间大量系统资源消耗,影响用户业务延迟。HBase社区也意识到了这个问题,也提出了一定的应对策略:

Limit Compaction Speed

该优化方案通过感知Compaction的压力情况自动调节系统的Compaction吞吐量,在压力大的时候降低合并吞吐量,压力小的时候增加合并吞吐量。基本原理为:
1. 在正常情况下,用户需要设置吞吐量下限参数“hbase.hstore.compaction.throughput.lower.bound”(默认10MB/sec) 和上限参数“hbase.hstore.compaction.throughput.higher.bound”(默认20MB/sec),而hbase实际会工作在吞吐量为lower + (higer – lower) * ratio的情况下,其中ratio是一个取值范围在0到1的小数,它由当前store中待参与compation的file数量决定,数量越多,ratio越小,反之越大。
2. 如果当前store中hfile的数量太多,并且超过了参数blockingFileCount,此时所有写请求就会阻塞等待compaction完成,这种场景下上述限制会自动失效。


截至目前,我们一直都在关注Compaction带来的IO放大效应,然而在某些情况下Compaction还会因为大量消耗带宽资源从而严重影响其他业务。为什么Compaction会大量消耗带宽资源呢?主要有两点原因:

1. 正常请求下,compaction尤其是major compaction会将大量数据文件合并为一个大HFile,读出所有数据文件的KVs,然后重新排序之后写入另一个新建的文件。如果待合并文件都在本地,那么读就是本地读,不会出现垮网络的情况。但是因为数据文件都是三副本,因此写的时候就会垮网络执行,必然会消耗带宽资源。
2. 原因1的前提是所有待合并文件都在本地的情况,那在有些场景下待合并文件有可能并不全在本地,即本地化率没有达到100%,比如执行过balance之后就会有很多文件并不在本地。这种情况下读文件的时候就会垮网络读,如果是major compaction,必然也会大量消耗带宽资源。
可以看出来,垮网络读是可以通过一定优化避免的,而垮网络写却是不可能避免的。因此优化Compaction带宽消耗,一方面需要提升本地化率(一个优化专题,在此不详细说明),减少垮网络读;另一方面,虽然垮网络写不可避免,但也可以通过控制手段使得资源消耗控制在一个限定范围,HBase在这方面也参考fb也做了一些工作:

Compaction BandWidth Limit

原理其实和Limit Compaction Speed思路基本一致,它主要涉及两个参数:compactBwLimit和numOfFilesDisableCompactLimit,作用分别如下:
1. compactBwLimit:一次compaction的最大带宽使用量,如果compaction所使用的带宽高于该值,就会强制令其sleep一段时间
2. numOfFilesDisableCompactLimit:很显然,在写请求非常大的情况下,限制compaction带宽的使用量必然会导致HFile堆积,进而会影响到读请求响应延时。因此该值意义就很明显,一旦store中hfile数量超过该设定值,带宽限制就会失效。

写在最后

Compaction对于HBase的读写性能至关重要,但是它本身也会引起比较严重的写放大,本文基于此介绍了官方社区对Compaction进行的多种优化方案。希望大家在看完这些优化方案之后可以更好地理解Compaction!
Categories: compaction, HBase Tags:

搜索意图识别浅析

July 20th, 2016    阅读(109) No comments

对于搜索引擎来讲,很多情况下只需要用户在搜索输入框内输入所需要查询的内容就可以了,其余的事情就全部交给搜索引擎去处理。理想的情况下,搜索引擎会优先返回用户想要的结果。理想很丰满,但总会存在一些骨感的现实,用户通过搜索无法找到最想要的结果。如果应用中压根不存在用户搜索的内容,倒还可以理解。反之的话,就是一个大写的尴尬。本文主要谈论和解决的是令人尴尬的问题。

 

为什么会搜索不到

1、不同的用户对同一种诉求的表达往往是有差别的,往往会存在一种比较常见的现象,用户输入的query并不能清晰准确的表达需求。

2、搜索系统对用户query的理解能力较弱,无法挖掘出用户的真实需求。

3、召回结果集的排序不合理,可能用户需求的内容被排在后面而未曝光。

以上几点大概是用户无法找到需求内容的主要原因,本文主要讨论的是前两点,主要是想解决如何更好的理解用户的需求并进行准确的召回,进而对第三点所涉及到的排序起到积极作用。

用户作为一个使用主体,其知识水平和表达能力会有差异,当不同用户想搜索同一个商品时所输入的query会存在差别,具体如下所示:

Alt pic

可见,对于同一个商品往往会对应不同的query,相对精确的有“蔓越莓胶囊欧洲”、“blackmore蔓越莓”;品牌优先的有“blackMores”;功效优先的有“女士痛经”,”泌尿系统感染”;输入错误的有”蔓越梅”,输入别名的有”圣洁莓”;输入较模糊的有“妇科”,“炎症”。所以说用户的输入一般会存在表达差异,词汇差异,需求明确性差异等。

要想解决这些问题就需要通过用户输入的query来获取用户的真实需求,本文把对用户输入的理解称为QueryParser,包含:query切分(分词),query意图识别,query改写(query扩展/query纠错/query删除等),接下来本文主要针对query意图识别和query改写结合在考拉海淘搜索中的具体应用来和大家聊聊。

 

1.query意图识别

本文主要针对垂直搜索进行介绍,不同的垂直引擎中的query会有自己的特点。像去哪儿网的日志中肯定有很多“城市a到城市b的机票”这种pattern的query,而电商网站中肯定大部分是“产品/品牌/型号/款式/价格”等类型数据的组合,音乐类应用中大部分应该是艺人和歌曲名相关的query。相比通用搜索而言,垂直搜索可能更针对性的挖掘用户的意图。

1.1意图识别的难点

1、输入不规范,前文中已有介绍,不同的用户对同一诉求的表达是存在差异性的。

2、多意图,查询词为:”水”,是矿泉水,还是女生用的化妆水。

3、数据冷启动。当用户行为数据较少时,很难获取准确的意图。

4、没有固定的评价标准。pv,ipv,ctr,cvr这种可以量化的指标是对搜索系统总体的评价,具体到用户意图的预测上并没有标准的量化指标。

1.2 意图识别的方法

1.2.1 词表穷举法

这种方法最简单暴力,通过词表直接匹配的方式来获取查询意图,同时,也可以加入比较简单并且查询模式较为集中的类别。

  • 查询词:德国[addr] 爱他美[brand] 奶粉[product] 三段[attr]
  • 查询模式:[brand]+[product];[product]+[attr];[brand]+[product]+[attr]

当然查询模式是可以做成无序的。这种意图识别的方式实现较为简单,能够较准确的解决高频词。由于query一般是满足20/80定律,20%的query占据搜索80%的流量。但是,80%得长尾query是无法通过这种方式来解决的,也就是说这种方式在识别意图的召回可能只占20%。同时,需要人工参与较多,很难自动化实现。

1.2.2 规则解析法

这种方法比较适用于查询非常符合规则的类别,通过规则解析的方式来获取查询的意图。比如:

  • 北京到上海今天的机票价格,可以转换为[地点]到[地点][日期][汽车票/机票/火车票]价格。
  • 1吨等于多少公斤,可以转换为[数字][计量单位]等于[数字][计量单位]。

这种靠规则进行意图识别的方式对规则性较强的query有较好的识别精度,能够较好的提取准确信息。但是,在发现和制定规则的过程也需要较多的人工参与。

1.2.3 机器学习方法

意图识别其实可以看做是一个分类问题,针对于垂直产品的特点,定义不同的查询意图类别。可以统计出每种意图类别下面的常用词,对于考拉海淘而言,可以统计出类目词,产品词,品牌词,型号词,季节时间词,促销词等等。对于用户输入的query,根据统计分类模型计算出每一个意图的概率,最终给出查询的意图。 但是,机器学习的方法的实现较为复杂,主要是数据获取和更新较困难,数据的标注也需要较准确才能训练出较好地模型。

 

2. query意图识别在考拉海淘中的应用

考拉海淘是一个电商类的产品,目前其搜索意图相对单一为产品购买。本文主要讨论考拉海淘中用到的query改写,类目相关,命名实体识别和Term Weight等内容。 考拉的搜索系统有大量的用户访问,我们希望通过对用户query的意图分析来提高搜索体验,目前,考拉系统的架构包含下图所示的几个部分:Alt pic

2.1 实体词识别

通过对日志分析,将用户常用的搜索词分为以下四类:地址(澳洲),品牌词(爱他美),产品词(奶粉),属性词(三段)。当用户输入query时,如果能准确的识别每个实体词,就能去索引里面精确匹配对应的字段,从而提高召回的准确率,在排序中也可以用到实体词进行优化。 举一个栗子:有一个商品的标题是”AYAM BRAND 雄鸡标 辣椒金枪鱼“,它的类目是“冷面/熟食/方便菜 其他熟食”。当用户搜“辣鸡面”的时候,通过单字逻辑召回这款商品。通过实体识别会得到这个商品的产品词是“金枪鱼”,而query要搜的产品词是“面”。这样就可以判断出其实这是一个误召回,进而可以将这个商品进行过滤或者是排序的时候放到较后的位置。

我们的实体词识别模型是通过crf来进行训练的,语料是用户搜索的真实query,用一个相对准确的词典(品牌词/产品词/属性词/地址词)去标注语料。具体的标注预料如下所示:

  • 爱 B-brand 他 I-brand 美 I-brand 奶 B-product 粉 I-product 三 B-attr 段 I-attr

训练出的模型对于地址,品牌词,产品词的识别准确率平均95%左右,英文属性词的识别准确率还有待提高,crf模型还有一个比较好的地方是具有一定的泛化能力。另外,模型的训练是使用考拉平台上的商品数据,所以对非考拉平台的产品和品牌识别的准确率也不理想。但是,最重要的是识别本平台已有的实体,尽可能准确的向用户展示最准确的商品搜索结果。

2.2 query改写

query改写包括:query纠错,query扩展,query删除,query转换。本文主要讨论在考拉中常用的query扩展,query删除和query转换。

2.2.1 query扩展

搜索召回依赖索引数据,商品数据依赖于编辑运营的录入,数据的完整性很难得到保障,也就是说很难从各个角度来描述这个商品。

还是用例子说明,一个商品的标题是“Fisher-Price 费雪 碎花儿童学步鞋”,由于用户输入的差异性存在,会有用户搜索”婴儿鞋”,”宝宝鞋”。很明显这个学步鞋恰恰用户所需的商品,但是因为数据的不完整性而无法被召回。这就是前文提到的有商品却无法展示给用户,这是最不希望遇到的情况。这时候就需要用到query扩展,我们会维护一个同义词扩展表,当用户输入一个query的时候,会进行同义词扩展,从而尽可能召回所有与用户相关的商品。

2.2.2 query删除

query删除一般的应用场景是在当用户输入query过多时导致无法正常召回,可以通过丢词的方式来筛选用户的query,从而召回与query最相关的商品。

依旧用例子说明,当用户的query为”卡乐比水果麦片”时,由于这款商品可能被下架,或者商品种类较少,通过query删除,可以把原query改写为“水果麦片”,进而可以召回其他品牌的水果麦片。query删除是需要用到实体识别的,因为要决定query中的哪些数据被删除才能对用户原意图造成的影响最小。像”卡乐比水果麦片”,通过意图识别得到”卡乐比“是品牌,”水果麦片“是产品,显然用户更需要的是水果麦片,而不是“卡乐比”其他类型的麦片。

2.2.3 query转换

会存在这样一种情况,确实没有商品是满足用户的明确需求。 比如,用户搜索”祖马龙”,考拉海淘并没有这款商品。也无法通过query同义词扩展和query删除来对原query进行处理。通过session数据可以发现,用户搜索“祖马龙”后会伴随着“香水”这个query出现,利用用户行为数据是可以挖掘出“祖马龙”和”香水”这两个query是相关的。当用户搜索”祖马龙”而无法召回时,是可以把query转换为”香水”来尽可能满足用户的需求。

2.3 类目相关

当用户搜索“Adidas”的时候,是想要搜索“运动鞋”,还是“衣服”,又或者是“沐浴露”。当然,你可能说不同的用户有不同的需求,这就涉及到个性化搜索的内容了,暂时不在本文的讨论范围内。如果用户行为数据足够多,直接使用统计分析就可以找到query对应的类目相关程度。当然,统计算法也是机器学习的一种。但是,仍有一部分问题是需要机器学习算法来完成的。

通过对用户行为数据的挖掘,发现“Adidas”对应的类目相关性排序为:运动鞋>衣服>沐浴露。当用户搜索“Adidas”的时候,会按照类目相关性的顺序,将运动鞋排在最前面。当然,考虑到多样性,排序时会通过类目打散将衣服和沐浴露适当的掺杂在运动鞋中。

query的类目相关性是通过用户行为数据进行挖掘的,一些长尾的类目虽然与query相关,由于马太效应却无法被挖掘。比如query“面膜”所挖掘出的相关性类目为“男士面膜”/“女士面膜”/“面膜粉”等,而“孕妇面膜”这个类目却一直处于不相关的状态。其实,“男士面膜”/”女士面膜”/”面膜粉”/“孕妇面膜”在”面膜”这个维度都是相关的,我们通过虚拟类目的做法来解决这种长尾问题。离线将这四个类目归一为一个虚拟类目,当用户的query落在虚拟类目中的大部分类目时,认为这个query与虚拟类目包含的其他类目也具有相关性。

2.4 Term Weight

中文自然语言处理的第一步就是分词,分词的结果中,每个词的重要性显然应该时候区别的。Term Weight就是为了给这些词不同的打分,根据分值就可以判断出核心词,进而可以应用到不同的场景。比如,有一个商品的标题为“碗装保温饭盒套装”,通过Term Weight可以得到核心词为“饭盒”。当用户搜”碗”召回这个商品的时候,是可以根据term weight来进行排序降权的。

通过以上几点可以看出,query意图识别在一个搜索系统中是必不可少的,可以说query意图识别的精确程度高低决定着一次搜索质量的优劣。

Categories: Uncategorized, 大数据, 检索 Tags: