文中聊的是数据路由,不是nginx之类的。
几乎每一个分布式系统,都会给用户提供自定义路由的功能。因为,仅通过range、mod、hash等方法,很大概率已经满足不了用户的需求。下面以一个实际场景为例,说一下数据路由的思路。
某个大型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,有的可能 mod3,有的可能再次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同时也有时间纬度的概念,可以针对某些旧商户进行归档清理
系统想要预留另外一部分号段,用来提供一些测试账号,供客户试用。经历过前三轮的改造,我们可以很容易的对其进行规划。
为什么觉得redis-cluster的slot设计是个鸡肋呢,因为它把路由规则给定死了,要我去设计我肯定要放在驱动层。
某些架构师潇洒的来,潇洒的走,留下了不可磨灭的痕迹。为了兼容这些遗留系统的路由代码,分支会更加复杂,每一个公司都有一堆故事,无非是骂娘和被骂。稳定性重如山,路由代码可能是最重要的没技术含量的if else。一动,都得死。
就问你怕不怕?