眼见不一定为实:调用链HBase倾斜修复
hello,大家好,我是小楼。
今天给大家分享一个关于HBase数据倾斜的排查案例,不懂调用链?不懂HBase?没关系,看完包懂~
背景
最近HBase负责人反馈HBase存储的调用链数据偶尔出现极其严重的倾斜情况,并且日常的倾斜情况也比较大,讲的通俗点就是出现了热点机器。
举个例子,有三台HBase机器存储调用链数据,其中大部分数据读写都在一台机器上,导致机器负载特别大,经常告警,这就是HBase倾斜,也叫热点现象。本文主要讲述了治理倾斜情况的过程,以及踩的几个坑。
知识铺垫
为什么会出现HBase倾斜的情况呢?既然是调用链数据HBase倾斜,那么首先简单介绍下几个调用链和HBase的背景知识。
全链路追踪
全链路追踪
可能是一个比较统一的叫法,平常最多的叫法叫调用链
,也可能有其他的叫法,不过说的都是同一个东西,本文全都用调用链
来指代。
调用链是分布式服务化场景下,跨应用的问题排查和性能分析的工具。
说的直白点,就是可以让你看到你的代码逻辑在哪个地方调用了什么东西,比如在serviceA的methodA的逻辑里,依次调用了redis、mysql、serviceB等,可以看到每个调用的耗时、报错、出入参、ip地址等信息,这就是调用链。
目前调用链有一个统一的标准,以前叫OpenTracing
,现在与其他的一些标准整合进了OpenTelemetry
,不过调用链的标准基本没变。
调用链标准的最核心的概念如下,只列出了一些最核心的元素,不代表全部:
- Span:调用链最基本的元素就是Span,一次 Dubbo Server 请求处理,一次 HTTP 客户端请求,乃至一次线程池异步调用都可以作为一个 Span。
- SpanID:一个Span的唯一标识,需要保证全局唯一
- TraceID:一条调用链的唯一标识,会在整个调用链路中传递
- ParentID:父 Span 的 SpanID。当存在 A -> B 这样的调用关系时,B Span 的 ParentID 是 A Span 的 SpanID。ParentID 用来构造整个调用链路的树形结构。每次发起新的请求时,都要把当前的 SpanID 作为 ParentID 传递给下一个 Span。
- Segment:Segment是特殊的Span,一般表示这是一个应用的边界 Span。如作为 Dubbo Server 的一次请求处理;作为 HTTP Server 的一次请求处理;作为 NSQ Consumer 的一次消息处理等。
- Trace:一条调用链就是一条Trace,Trace是一堆Span的集合,每一个Trace理论上来说是一颗树
下面用一张图来演示一次简单的三个服务间的Dubbo调用来展示调用链的数据是如何、何时产生的,以及各Span之间是通过什么关联起来的,用于深入理解上面的核心概念。
文字描述:外部请求调用了ServiceA.MethodA, SA.MA依次调用了SB.MB、Redis、MySQL, SB.MB调用了SC.MC, SC.MC内部只有计算逻辑。
注意:
- 图里Span内容只包含了一部分,不代表全部内容。
- 可能不同的调用链系统上报存储的方式不一样,有的是每个Segment上报一次,有的是每个Span上报一次,图中表示的是每个Span上报一次
HBase
网上关于HBase介绍的文章很多,这里不做详细的介绍,只是列出来一些基本的概念用于理解。
HBase是一个可以存储海量数据的数据库,既然是数据库,那么最基本的操作就是添加和查询
- RowKey
HBase基本的数据操作都是通过RowKey这个东西,RowKey是HBase的一个核心概念,如何设计Rowkey是使用HBase最关键的部分。
RowKey在HBase里的作用是什么?一个是数据的操作要通过rowkey,可以把rowkey理解为mysql的主键,有索引的作用,另一个是用来做负载均衡。Rowkey的数据格式是字节流,也就是byte数组,这个概念很重要。
什么是byte?就是一个8位字符,值在-128到127之间,所以即使你的rowkey不是那128个ascii码,也是可以存的,例如你的rowkey有三个字节,十进制表示分别是-56、-110、-27,发送到HBase也是可以存储的,不过你要展示出来给人看,可能就不太好展示这个RowKey了。
- Region
Region是HBase数据分片的基本单位,可以把Region理解为HBase的数据分片。
HBase是按什么来做分片的?如果你有搭建过HBase的话,并且看过HBase的web界面,可以看到Region部分有两个属性,Start Key和End Key。
这两个属性代表什么意思?举个例子,现在有两个Region,RegionA的StartKey和EndKey是00和01,RegionB的StartKey和EndKey是01和02,你要存两条数据,RowKey分别是0000ABC和0100DEF,第一条数据就会落到RegionA里,第二条数据就会落到RegionB里,简单来讲就是根据RowKey的前缀来决定这条RowKey落到哪个Region里,如果Rowkey匹配不到任何一个Region,那么会新建一个Region存储数据。
当Region的数据量到达某个阈值后,Region会自动分裂为两个Region,避免性能降低,HBase还有一个功能是预分区,比如在新建Table后,可以在Table里预先指定256个分区,StartKey和EndKey依次是00-01、01-02一直到FE-FF(前提是你的所有的RowKey的前缀都在00-FF区间内),预分区的好处是避免HBase最开始过多的自动分裂,因为分裂时数据是不可用的,过多的分裂会导致性能降低。
问题分析
介绍完了调用链和HBase的基本概念,这里介绍下我们调用链系统的存储架构,以及为什么会产生倾斜问题。
首先是调用链TraceID的设计,格式是 service_name-xx-yy-zz,也就是应用名+时间戳+IP+随机数。
调用链数据存储有两部分,一部分在ES,一部分在HBase,为什么不直接把原始数据存到ES里?因为ES机器比较贵,用的固态盘,为了节省成本。
ES里存储的是索引数据,也就是一些筛选条件,例如根据appName、startTime、耗时、是否有报错这些属性筛选调用链,这些可以用来筛选调用链的属性是存储在ES里的,并且为了节省空间,除了TraceID和SpanID这两个属性,其他属性的doc_value是关掉的,也就是只存了索引,没有存数据,因为要筛选出来TraceID和SpanID,然后根据这两个ID去HBase里取原始数据。
HBase里存储的是HBase的原始数据,除了TraceID和SpanID,因为这两个属性的数据在ES里已经有了。HBase里的每条数据是一个Span,每条数据的RowKey是xx-TraceID-SpanID,最开始的两个字符是TraceID做hash取前两位,为什么要做个hash?因为我们TraceID的开头是应用名,如果不加前面两位hash值的话,根据HBase存储数据的策略,前缀一样的会存储到一起,也就是同一个应用的Trace会存储到一起,那么流量大的应用Trace会很多,这样就会导致倾斜问题,加两位hash值可以让数据分散开,并且同一个TraceID的数据会存储到一起,可以一次性Scan出来。
既然RowKey的设计已经考虑到了倾斜问题,已经做了hash分散数据,那为什么日常会存在倾斜问题?而且偶尔会出现很严重的倾斜问题?原因是每个Trace的Span数量是不一样的,有的Trace可能就几个Span,有的Trace有几万个Span,还会出现一种极端情况,一个MQ消费者消费消息后又向好几个Topic里发送了消息,后续的消费者重复这样的操作,导致一条消息最终放大了几万甚至几十万倍,导致一个Trace里有几十万甚至几千万个Span,这只是其中一种场景,也可能业务开发做了什么骚操作,也会导致一个Trace包含的Span数量非常多,那么根据现在的存储架构,同一个Trace的数据会存储到一起,这就导致了倾斜问题。
方案设计
在定位到问题后,最直接的想法就是彻底打散RowKey,也就是把SpanID的MD5当作RowKey,因为SpanID是全局唯一的,所以MD5必然是彻底打散的,不过这样做有一个坏处,就是数据彻底打散后,要查出一整个Trace的话,就得一个Span一个Span去查,不像之前的RowKey设计可以一次性Scan出来。
为了知道这样查询性能有多慢,特意做了一次性能测试,结果如下:
span数量(个) | scan(ms) | search_es(ms) | gets(ms) | gets_parallel_batch100(ms) | gets_parallel_batch200(ms) | gets_parallel_batch300(ms) | gets_parallel_batch500(ms) |
---|---|---|---|---|---|---|---|
100 | 5 | 12 | 12+10 | ||||
265 | 10 | 20 | 20+25 | 20+10 | 20+15 | ||
336 | 10 | 20 | 20+28 | 20+10 | 20+15 | ||
562 | 10 | 25 | 25+45 | 25+15 | 25+15 | 25+23 | |
1759 | 30 | 57 | 57+130 | 57+38 | 57+40 | 57+45 | 57+45 |
2812 | 70 | 85 | 85+210 | 85+70 | 85+70 | 85+70 | 85+70 |
8000 | 170 | 210 | 210+700 | 210+180 | 210+180 | 210+180 | 210+200 |
之前的设计查询一整个Trace的步骤就是直接用TraceID去HBase里scan,不用查询ES,也就是第二列的耗时。
如果改成一个Span一个Span去查的话,查询步骤变成了两步,第一步先用TraceID从ES里查询出这个Trace所有的SpanID,然后再根据SpanID去HBase里批量gets,表格里的后5列就是两步查询的耗时,加号前面是查询ES的耗时,加号后面是HBase批量gets的耗时。第四列表示串行gets,后四列表示并行gets,并对不同batch的大小做了测试。
根据测试结果,串行gets的性能要比并行gets的性能低3-4倍,所以不考虑串行gets。并行batch的大小对性能影响不大,并且最终耗时相比只scan的耗时也就增大一倍,例如查询8000个Span,前后方案查询耗时对比为170ms:390ms,实际上用户感知不到,所以方案就定为用MD5彻底打散数据。
踩的坑
在开发完成后,在测试环境测试无误后就直接发了线上,由于最开始不太了解HBase的Region相关的概念,所以误以为RowKey改成MD5后倾斜情况会直接消失,就直接发布了HBase数据写入的服务,发布后HBase那边立刻出现了非常严重的倾斜情况,导致HBase写入超时,kafka堆积,赶紧回滚了,HBase负责人查看监控发现大部分数据写入到了一台机器上。
为什么会出现这种情况?测试环境为什么没有出现这个问题?
根据上面介绍的HBase的Region相关的概念,出现这种情况的原因可能是RowKey没有匹配到任何一个Region,所以数据写入到了新建的Region上,也就是一台机器上。
但是代码里写的明明就是MD5,并且在测试环境测试无误,之前的RowKey方案的前两位hash在00-FF之间,MD5的前缀肯定也在00-FF之间啊,按理说肯定可以匹配到一个Region的,为什么还会写到新的Region里?直接上代码
import org.apache.commons.codec.digest.DigestUtils;
// 用spanId的MD5值当作RowKey,写入到HBase里
public static byte[] rowKeyMD5(String spanId) {
// DigestUtils只是JDK加密包的封装,底层还是调用JDK本身的MD5加密
return DigestUtils.md5(spanId);
}
DigestUtils是org.apache.commons.codec.digest.DigestUtils包里带的,实际还是调用的JDK自带的MD5库,等同于如下的写法
import java.security.MessageDigest;
// MessageDigest是JDK自带的加密包,里面有MD5加密算法
MessageDigest.getInstance("md5").digest(spanId.getBytes(StandardCharsets.UTF_8));
调试一波,发现了问题,这里用一个简单的demo演示下,逻辑就是用md5加密"abc"这个字符串
一般我们看到的
加密后的MD5是16个或者32个0-F之间的字符,0-F的ASCII码是48-57和97-102,但是加密后的byte数组有的byte是负的,那加密出来的这16个byte是什么玩意?虽然继续看了MD5加密的源码,但是水平不足,看不懂加密原理。。。
看到加密后的byte数组应该就可以知道了为什么一发布就严重热点了,因为byte数组里面的东西根本不是正常的0-F之间的字符,虽然hbase的rowkey是只要是byte(-127~128)就行,但是现在MD5加密出的byte数组匹配不到原有的Region的StartKey和EndKey,全都写到新建的Region里了,那么我只需要把RowKey搞成MD5的16进制字符不就可以匹配到原有的Region了么?
那么Java怎么MD5加密出一般我们看到的那种16进制字符的呢?比较方便的写法是
import org.apache.commons.codec.binary.Hex;
Hex.encodeHex(DigestUtils.md5(str));
那么看下encodeHex里是怎么把md5byte数组转成十六进制字符串的
每个byte是8位,但是每个16进制字符,也就是0-F只需要四位bit就可以表示,所以一个byte可以表示两个16进制字符,也就是我们日常写的0xFF表示一个byte,上面的逻辑就是把一个byte的前四位和后四位分开,分别表示一个16进制字符,那么16个byte就可以拆成32个16进制字符,这就对上了,接下来看下encodeHex的输出
abc经过MD5加密后的16进制字符串是900150983cd24fb0d6963f7d28e17f72,我们按照encodeHex的逻辑来手动拆下byte看看对不对的上
首先看bs[0],也就是-112,用二进制表示就是10010000,注意,这是个补码,简单解释下原码和补码,计算机中的数值都是用二进制补码来存储的,正数的补码是它本身,也就是它的原码,负数的补码是它的原码除了符号位取反加1,详细的可以去看看计算机基础的书籍。
那么-112的原码就是11110000,补码就是10010000,拆成两部分也就是1001和0000,也就是9和0,跟16进制字符串的前两位,也就是90,对上了。
再拆下bs[1],也就是1,用二进制表示就是00000001,拆成两部分也就是0000和0001,也就是0和1,跟16进制字符串的三四位,也就是01,对上了
再拆下bs[2],也就是80,用二进制表示就是01010000,拆成两部分也就是0101和0000,也就是5和0,跟16进制字符串的五六位,也就是50,对上了
后面的同理,就不写了,看到这里我们就知道了那个16长度的byte数组到底是什么玩意,就是把每两个16进制字符合并成了一个byte
所以,我们经常以为或经常看到Java中的MD5每一位都是0-F的字符串是经过了encodeHex处理,但RowKey实际上用的是处理之前的byte[],它并不在0-F这个范围
改进
知道原因后,把RowKey的MD5改成十六进制字符,重新发布,果然没有出现严重热点问题,监控曲线跟之前一样,说明复用了已有的Region,日常倾斜情况需要跑一段时间才可以解决。
总结
- HBase的RowKey设计是使用HBase最最重要的地方
- 注意Java的MD5加密出来的东西不一定是你想要的
- 其实直接使用那个16长度的byte数组当作RowKey也可以,虽然基本不会复用已有的Region,不过要一点一点的灰度发布才可以
搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。
眼见不一定为实:调用链HBase倾斜修复的更多相关文章
- 多语言(Java、.NET、Node.js)混合架构下开源调用链追踪APM项目初步选型
1. 背景 我们的技术栈包括了Java..NET.Node.js等,并且采用了分布式的技术架构,系统性能管理.问题排查成本越来越高. 2. 基本诉求 针对我们的情况,这里列出了选型的主要条件,作为最终 ...
- APM调用链产品对比
APM调用链产品对比 随着企业经营规模的扩大,以及对内快速诊断效率和对外SLA(服务品质协议,service-level agreement)的追求,对于业务系统的掌控度的要求越来越高,主要体现在: ...
- dubbo+zipkin调用链监控
分布式环境下,对于线上出现问题往往比单体应用要复杂的多,原因是前端的一个请求可能对应后端多个系统的多个请求,错综复杂. 对于快速问题定位,我们一般希望是这样的: 从下到下关键节点的日志,入参,出差,异 ...
- Cat 客户端如何构建调用链消息树
场景 & 代码 Inner0 中的某方法调用了 Inner1,代码 Inner1的代码很简单, Cat通过一个线程本地变量来保存调用链的相关信息,其中核心的数据结构是消息树和操作栈.消息树用来 ...
- 使用docker-compose 一键部署你的分布式调用链跟踪框架skywalking
一旦你的程序docker化之后,你会遇到各种问题,比如原来采用的本地记日志的方式就不再方便了,虽然你可以挂载到宿主机,但你使用 --scale 的话,会导致 记录日志异常,所以最好的方式还是要做日志中 ...
- 调用链监控 CAT 之 URL埋点实践
URL监控埋点作用 一个http请求来了之后,会自动打点,能够记录每个url的访问情况,并将以此请求后续的调用链路串起来,可以在cat上查看logview 可以在cat Transaction及Eve ...
- 调用链Cat介绍
1. 调用链Cat 1.1. 调用链演进 1.2. 开源产品比较 1.3. 监控场景 1.4. cat的增值作用 1.5. cat典型报表 1.5.1. 应用报错大盘 1.5.2. 业务大盘 1.5. ...
- 谈谈iOS获取调用链
本文由云+社区发表 iOS开发过程中难免会遇到卡顿等性能问题或者死锁之类的问题,此时如果有调用堆栈将对解决问题很有帮助.那么在应用中如何来实时获取函数的调用堆栈呢?本文参考了网上的一些博文,讲述了使用 ...
- 扁平化promise调用链(译)
这是对Flattened Promise Chains的翻译,水平有限请见谅^ ^. Promises对于解决复杂异步请求与响应问题堪称伟大.AngularJS提供了$q和$http来实现它:还有很多 ...
随机推荐
- Redis 的同步机制了解么?
Redis 可以使用主从同步,从从同步.第一次同步时,主节点做一次 bgsave, 并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制 节点,复制节点接受完成后将 r ...
- Spring IoC 的实现机制?
Spring 中的 IoC 的实现原理就是工厂模式加反射机制. 示例: interface Fruit { public abstract void eat(); } class Apple impl ...
- Java 中如何格式化一个日期?如格式化为 ddMMyyyy 的形式?
Java 中,可以使用 SimpleDateFormat 类或者 joda-time 库来格式日期. DateFormat 类允许你使用多种流行的格式来格式化日期.参见答案中的示例代 码,代码中演示了 ...
- Mybatis入门程序(二)
1.实现需求 添加用户 更新用户 删除用户 2.添加用户 (1)映射文件User.xml(Mapper)中,配置添加用户的Statement <!-- 添加用户: parameterType:指 ...
- (stm32f103学习总结)—RTC独立定时器—实时时钟实验
一.STM32F1 RTC介绍 1.1 RTC简介 STM32 的实时时钟( RTC)是一个独立的定时器. STM32 的 RTC 模 块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的 ...
- 学习笔记 - Sass的安装与使用手册
最近因为工作需要,自学了Sass.现在将学习笔记整理在这里,供大家参考. 1. Sass的安装 Sass的编辑器安装方法有很多,大致能分为两种:应用程序(application)和命令行界面(comm ...
- 订单突破10000+,仅花1小时,APPx独家深入剖析背后的秘密!
拼多多:成立三年,获客三亿,月订单成交额达到恐怖的400亿,成功上市! 糕妈优选:营销活动推送1小时,订单超过10000+,商品成功刷屏朋友圈! 寻慢:一场活动净增7000+粉丝,付款转化率高达71% ...
- canvas系列教程07-canvas动画基础1
上面我们玩了一个图表,大家学好结构,然后在那个基础上去扩展各种图表,慢慢就可以形成自己的图表库了.也可以多看看一些国外的图表库简单的版本,分析分析,读代码对提高用处很大.我说了canvas两大主流用途 ...
- 前端react+redux+koa写的博客推荐
React-Node搭建的博客 曾经用的php+mysql+js写的博客,现在看来已经很low了,所以用目前最火的react+koa框架重构一下.先上地址吧:目前线上版本http://www.liuw ...
- jboss7学习3-jboss安装 访问(外网)添加用户
一.下载安装 1.下载地址: http://www.jboss.org/jbossas/downloads ,下载Certified Java EE 6 Full Profile版本. 2.解压 jb ...