转载 分库分表流量倾斜问题的排查与解决

一、背景

2022年11.10号晚8点,月黑风高

各大电商公司正在等待着即将到来的大促...

而作为交易订单组的我们也不例外,此时我们在紧盯监控大盘,试图找到系统蛛丝马迹的问题,以便及时应对,如果这时候出了问题,那就关乎着团队的面子,关乎着今年的绩效,当然还关乎着今年的年终奖……,秃然,奇怪的现象发生了

注:本文是中大型互联网公司遇到的真实案例,其知识点的深度和广度拿来面试都足够,建议认真阅读并且熟悉涉及到的相关知识点。若有任何问题可在评论区指出~

二、现象

随着业务的发展,订单单库承载的压力越来越大,因此后续对数据库做了水平拆分,利用shardingjdbc的能力做了分库分表。

根据数据量增长预期进行预估,订单库总共分了16个库,每个库16个表,所以总共是256张表。简要的结构如下:

企业微信截图_6273ff17-5d3d-4344-98c2-584ea49fe085.png

理论上,每个分库的请求QPS相差不大才是符合预期的,毕竟当时做数据库拆分也就是为了均分流量,均分压力。但奇怪的现象是不同的分库访问QPS竟然有5倍之差!如下图所示:

image-20230317153046644.png

image-20230317153213156.png

3库的QPS达到了16K以上,而11库的QPS仅仅只有3K,发生了严重的流量倾斜现象,按这种趋势下去,分库分表的带来的收益会因为某些库的压力过大而大为降低

三、猜想

先来猜想下为什么会发生这种现象?

猜想一:下单大账户

由于订单分库分表策略是采用的买家ID进行路由的,也就是同一个买家的订单都会存储到一个分库里,同样的,同一个买家的请求也都会打到同一个分库上,所以有可能是某位超级大买家疯狂买买买?

image-20230317155149192.png 猜想二:订单分库分表算法不随机

第二种可能性是订单分库分表算法不够随机,导致大部分买家的订单数据不均匀的落到了某些分库中,发生了数据倾斜的现象

image-20230317155603680.png

四、猜想验证思路

猜想一验证思路

对于猜想一的验证,在当天(11.10)下单的用户中,group by uid,看每个uid创建的单数,即可判断出是否有下单大账户

4.1 猜想二验证思路

对新注册的用户,利用分库分表的算法计算其下的订单应该存储到哪个订单库,看存储是否是否均匀

这就需要了解订单的分库分表算法

4.1.1 订单的分库分表算法

在公司的用户体系中,用户ID是用long型来存储的。

订单分库分表的算法中,取用户ID的第49-56位(共8位bit位,低4位分库,高4位分表),然后与分库分表的个数减一相与(订单共分了16库16表),即可得到分库分表Index,代码如下所示:

分库算法:

 
public int getDbIndex(Long userId){
  return (userId >>> 8) & 15 + 1;
}

 

分表算法:

 
public int getTableIndex(Long userId){
  return (userId >>> 12) & 15 + 1;
}

 

这里的+1是因为&15后计算的值区间为[0-15],加1后为[1-16],我们定义的分库分表Index的范围也是[1-16],和算法相匹配

这里为什么采用&15的方式而不是%16的方式,是因为若num的值为2的幂的情况下,可以用&(num-1) 代替 % num,而位运算的性能高于取余运算,这个特性是参考了HashMap的源码,有兴趣可以看看java.util.HashMap#putVal函数

为了更清晰易懂,图示如下:

image-20230317171745222.png

再举个实际例子,若userId为:1526102172100467200

其经过getDbIndex分库算法计算的图示如下:

image-20230321140820979.png

如上介绍,订单的分库分表算法的生成逻辑是依赖用户ID的,那么现在问题来了,用户ID是如何生成的?是不是订单取的用户ID的49-56位散列不均匀,从而导致分库分表数据倾斜?下面介绍下用户ID的生成算法

4.1.2 用户ID生成算法

在分布式系统中,为了保证生成的ID是唯一的,有多种算法,我们采用的是雪花算法

image-20230317180020721.png

如上图所示,雪花算法是由时间戳(41位)+工作机器ID(10位)+ 序列号(12位)组成。其中工作机器ID的bit位数和序列号的bit位数可以根据业务需要自行分配

这样,将时间戳放在首位,一方面可以尽可能的防止生成重复ID,另外一方面可以保证生成的ID是趋势递增的,若使用的是innodb存储引擎,B+树作为主键索引的情况下,递增这个特性对数据插入是非常友好的

在我司,对雪花算法进行了一定的改造,在long型的64位中融入了业务编码,其构成如下:

64位ID = 1(占位)+ 41(毫秒)+8(业务编码)+5(机器ID)+9(累加位)

其中订单分库分表取的是用户ID的第49-56位,对应到上述的算法中,也就是取了2(业务编码)+ 5(机器ID) + 1(重复累加),如下图黄色部分,所以如果该部分的生成不够随机,那完全会导致订单数据分库分表的数据倾斜

image-20230318225310652.png

五、猜想验证

5.1 猜想一验证

如下图,统计了每个买家下单数,按倒序排列,可见,单用户下单数最多的是9单。根据这个结论,基本可以排除下单大账户的问题

image-20230317155954902.png

5.2 猜想二验证

经过测试,我们发现新创建的用户ID创建的订单都会分布在1,3,5这三个订单库,这表明分库分表算法取的因子位是非随机的

但是为什么分在了1,3,5库?

订单分库分表因子位取的是下图算法中标记黄色区域的8位,它的组成是2bit(业务规则)+ 5bit(机器ID)+ 1bit(累加位)

image-20230318225310652.png 在用户ID的生成中,业务编码的bit位都被填充为0,对于机器ID的bit位,采用了配置的方式,如下图,当前已经配置了三个机器,它们的机器ID分别为8、9和10。

image-20230319082818581.png 对于1bit的累加位,其位于最高位,由于毫秒内生成的ID数量有限,因此大多数情况下该位为0

那么8位因子位即可根据上述的推导计算出来,分别是00 01000 0,00 01001 0,00 01010 0,经分库算法计算后得1,3,5

和预期相符,至此真相大白

六、问题解决

解决这个问题,最好的方式就是让用户ID的49-56位生成得更随机一些

用户ID的生成算法改造如下:

 
long userId = (timestamp - idepoch) << 22 | machineId << 15 | busid << 7 | this.sequence;

 

即:

1-42 时间戳 共42位

43-49 机器ID位,共7位

50-57 业务ID位,共8位

58-64 累加位 ,共7位

其中订单ID取的49-56位全部采用了随机数生成算法进行生成

七、拓展

1、为什么取用户ID的49-56位作为订单分库分表的因子位?

实际上,我们在生成订单ID时也采用了雪花算法,并将用户ID的49-56位作为订单ID的业务bit位进行填充。这样,订单ID就融合了用户ID的信息,我们无需扫描全库全表,也无需建立映射表,就可以利用订单ID和用户ID来查询订单信息。

分库分表的最佳效率通常在需要路由到特定库和表的情况下得以体现,否则扫描256张表的性能会特别低。。。

2、为什么分了16个库16张表?

这个选择取决于业务的需求以及数据库机器的配置。举例来说,如果我们要支持未来5年的业务发展,当前每天的订单增长量为10万条,一年就是3650万条。根据数据库的配置,每张表最多容纳200万条数据,并且还要留有一定的缓冲,因此我们可以初步计算出需要进行分库分表的数量。

至于为什么选择16,因为16是2的幂,利用2的幂可以使用位运算代替取余运算,从而提高性能。这种特性也被广泛应用在类似HashMap等数据结构中。

3、雪花算法在实际生产上的使用体验如何,有遇到过线上问题吗?

在实际生产中,雪花算法的表现还是相对稳定的,尽管理论上存在时钟回拨等问题,但是这些问题的兜底方案已经得到了广泛的应用,因此实际使用体验还是比较良好的。

八、总结

本文介绍了分库分表流量倾斜的问题排查与解决思路

思路总结如下:

1、明确订单分库分表的路由算法,是通过取用户ID上的49-56位计算而来

2、用户ID的生成采用的是雪花算法。雪花算法是一种生成全局唯一ID的算法,通过在分布式系统中生成ID,避免了重复的问题。因为每个雪花都是独一无二的,因此称之为“雪花算法”。一般地,我们会对雪花算法进行一定的改造,以符合公司业务的需要,比如在我司就在64位中划分了8位作为业务bit位,由业务填充相关的信息。

3、为解决分布不均的问题,我们采用了更改用户ID生成算法的方式,将其49-56位更加随机、均匀化的方法。


作者:秃然的自我喔
链接:https://juejin.cn/post/7212135351732092987
来源:稀土掘金
posted @ 2023-06-25 17:26  重生之我是java程序员  阅读(209)  评论(0编辑  收藏  举报