现实中的路由规则,可能比你想象中复杂的多
文章转载自公众号:小姐姐味道
几乎每一个分布式系统,都会给用户提供自定义路由的功能。因为,仅通过range
、mod
、hash
等方法,很大概率已经满足不了用户的需求。下面以一个实际场景为例,说一下数据路由的思路。
文中聊的是数据路由,不是nginx之类的。
场景
某个大型toB
的应用,使用 MySQL 存储,单表数据量已达数亿,在结构变更、数据查询方面,已表现出明显的瓶颈,需要进行分库分表。
实施步骤
找到切分键
第一步就是找到切分的纬度。比如业务是按照时间纬度进行查询的,那么就把创建时间作为切分键。
此业务的切分键,是商户 id (类似于你在美团开店了,美团给你分配的唯一 id )。由于历史原因,这个 id 是用的数据库主键 id ,而且是自增的。业务具有以下特点:
- 业务操作是由某个商户发起的,每张表都有商户 id 字段
- 商户的数据不均衡,有的商户有几千万,有的可能只有十几条
- 存在部分 vip 商家,其数据量非常庞大
- 存储大量统计需求,所以无法分表,只能分库
- 存在遍历数据的可能,比如部分定时
切分需求一阶段
分库迫在眉睫。通过分析,部分 vip 商户,数据量巨大,把它单独转移到一个数据库中也不为过。
通过维护一个映射文件,来控制 vip 商户到数据存储流向。这时候,就需要自定义路由。
伪代码如下:
function viptable(id){
10 => "mysql-002"
101 => "mysql-003"
}
function router4vip(id){
aimDb = viptable(id)
if(aimDb) return aimDb
return "mysql-001"
}
商户为 10,数据将落向mysql-002
;商户为 101,将落向mysql-003
;数据默认使用mysql-001
存储。
另外,由于 id 是自动生成的自增字段,与路由存在一个先有鸡还是先有蛋的问题,所以将 id 字段修改为人工设值,延伸出另外一个配号系统,在此不多提。
切分需求二阶段
解决了 vip 商户的问题,接下来就需要解决mysql-001
的问题。随着业务的发展,落在默认库上的数据越来越多,很快又遇到了瓶颈。
想到的方法是,对其一分为二。mysql-001
的数据打散到两个库中。这个打散的规则,我们直接采用mod。
为什么不是一拆为三呢?主要是基于以下考虑,假设拆分后的 db 为:
mysql-001-1
mysql-001-2
这种情况下mysql-001
就变成了逻辑集群。当mysql-001-1
和mysql-001-2
也达到了瓶颈,那我们就可以对其继续进行拆分,依然是一拆为二,这时候,mod 4
就可以了,不会涉及复杂的数据迁移。
拆分后的db为:
mysql-001-1-1
mysql-001-1-2
mysql-001-2-1
mysql-001-2-2
到现在为止,我们采用了 vip 分库,mod 4
分库,伪代码如下:
...
function routertable(pivot){
0 => "mysql-001-1-1"
1 => "mysql-001-1-2"
2 => "mysql-001-2-1"
3 => "mysql-001-2-2"
}
function router4mod(id){
aimDb = router4vip(id)
if(aimDb) return aimDb
pivot = mod4(id)
return routertable(pivot)
}
到现在,我们已经分了六个库了。通过裂变的模式,有着较好的扩展性。
这样就可以高枕无忧了么?
切分需求三阶段
可惜的是,我们每次扩容,都是指数级别的。下一次,就是 mod 8
;而下下次,就是mod 16
。每次扩容,都会动一半的数据,wtf。
最后,决定在商户 id 的范围上做文章。
首先,做一个定长的商户 id ,比现有系统中的任何一个都长,主要考虑新的规则不会影响旧的路由规则。
然后,首先根据商户 id 的范围划分第一层虚拟集群,然后再根据 mod
划分第二层虚拟集群。我们的路由,现在是双层路由。
比如,我们把商户号定9位(java中int是10位),并做如下路由表:
100 000000 - 100 100000=> 虚拟集群1
100 100000 - 100 200000=> 虚拟集群2
...
前三位,用来分第一层虚拟集群,支持899个;后6位,代表范围,最大10万。每个范围下面,都会有自己的路由规则,有的可能 mod 2
,有的可能 mod 3
,有的可能再次 range
。
好,我们加入新的集群:
mysql-range0-0 代表号段在范围1中的偶数id
mysql-range0-1
伪代码如下:
...
function router4range(id){
if(id < 100000000){
return router4mod(id)
}else if
(id in [100000000-100100000]){
return
"mysql-range0-"+mod2(id)
}
}
到此为止,我们一共有8个库,其中两个是给 vip 用的,四个是遗留的路由算法,还有两个是给新的分库规则使用。
通过三次改进,我们的路由满足:
一、 当我们发现,当商户 id 增长到100 056400
,就达到瓶颈了,那么就可以新增一个新的范围,只需要改动一下路由表逻辑就ok了
二、 当某个范围内某个商户成长为 vip ,那我们就可以单独将其提取出来,增加新的 vip 库
三、 某个范围内数据热点严重,那么就可以 mod 4
进行扩容,并不影响范围外的数据
四、 商户 id 同时也有时间纬度的概念,可以针对某些旧商户进行归档清理
切分需求四阶段
系统想要预留另外一部分号段,用来提供一些测试账号,供客户试用。经历过前三轮的改造,我们可以很容易的对其进行规划。
End
为什么觉得redis-cluster
的slot
设计是个鸡肋呢,因为它把路由规则给定死了,要我去设计我肯定要放在驱动层。
某些架构师潇洒的来,潇洒的走,留下了不可磨灭的痕迹。为了兼容这些遗留系统的路由代码,分支会更加复杂,每一个公司都有一堆故事,无非是骂娘和被骂。稳定性重如山,路由代码可能是最重要的没技术含量的if else
。一动,都得死。
就问你怕不怕?
以上就是W3Cschool编程狮
关于现实中的路由规则,可能比你想象中复杂的多的相关介绍了,希望对大家有所帮助。