1 为什么要拆分?

先看一段对话。

从上面对话可以看出拆分的理由:

1)  应用间耦合严重。系统内各个应用之间不通,同样一个功能在各个应用中都有实现,后果就是改一处功能,需要同时改系统中的所有应用。这种情况多存在于历史较长的系统,因各种原因,系统内的各个应用都形成了自己的业务小闭环;

2)  业务扩展性差。数据模型从设计之初就只支持某一类的业务,来了新类型的业务后又得重新写代码实现,结果就是项目延期,大大影响业务的接入速度;

3)  代码老旧,难以维护。各种随意的if else、写死逻辑散落在应用的各个角落,处处是坑,开发维护起来战战兢兢;

4) 系统扩展性差。系统支撑现有业务已是颤颤巍巍,不论是应用还是DB都已经无法承受业务快速发展带来的压力;

5)  新坑越挖越多,恶性循环。不改变的话,最终的结果就是把系统做死了。

2 拆前准备什么?

2.1 多维度把握业务复杂度

一个老生常谈的问题,系统与业务的关系?

我们最期望的理想情况是第一种关系(车辆与人),业务觉得不合适,可以马上换一辆新的。但现实的情况是更像心脏起搏器与人之间的关系,不是说换就能换。一个系统接的业务越多,耦合越紧密。如果在没有真正把握住业务复杂度之前贸然行动,最终的结局就是把心脏带飞。

如何把握住业务复杂度?需要多维度的思考、实践。

一个是技术层面,通过与pd以及开发的讨论,熟悉现有各个应用的领域模型,以及优缺点,这种讨论只能让人有个大概,更多的细节如代码、架构等需要通过做需求、改造、优化这些实践来掌握。

各个应用熟悉之后,需要从系统层面来构思,我们想打造平台型的产品,那么最重要也是最难的一点就是功能集中管控,打破各个应用的业务小闭环,统一收拢,这个决心更多的是开发、产品、业务方、各个团队之间达成的共识,可以参考《微服务(Microservice)那点事》一文,“按照业务或者客户需求组织资源”。

此外也要与业务方保持功能沟通、计划沟通,确保应用拆分出来后符合使用需求、扩展需求,获取他们的支持。

2.2 定义边界,原则:高内聚,低耦合,单一职责!

业务复杂度把握后,需要开始定义各个应用的服务边界。怎么才算是好的边界?像葫芦娃兄弟一样的应用就是好的!

举个例子,葫芦娃兄弟(应用)间的技能是相互独立的,遵循单一职责原则,比如水娃只能喷水,火娃只会喷火,隐形娃不会喷水喷火但能隐身。更为关键的是,葫芦娃兄弟最终可以合体为金刚葫芦娃,即这些应用虽然功能彼此独立,但又相互打通,最后合体在一起就成了我们的平台。

这里很多人会有疑惑,拆分粒度怎么控制?很难有一个明确的结论,只能说是结合业务场景、目标、进度的一个折中。但总体的原则是先从一个大的服务边界开始,不要太细,因为随着架构、业务的演进,应用自然而然会再次拆分,让正确的事情自然发生才最合理。

2.3 确定拆分后的应用目标

一旦系统的宏观应用拆分图出来后,就要落实到某一具体的应用拆分上了。

首先要确定的就是某一应用拆分后的目标。拆分优化是没有底的,可能越做越深,越做越没结果,继而又影响自己和团队的士气。比如说可以定这期的目标就是将db、应用分拆出去,数据模型的重新设计可以在第二期。

2.4 确定当前要拆分应用的架构状态、代码情况、依赖状况,并推演可能的各种异常。

动手前的思考成本远远低于动手后遇到问题的解决成本。应用拆分最怕的是中途说“他*的,这块不能动,原来当时这样设计是有原因的,得想别的路子!”这时的压力可想而知,整个节奏不符合预期后,很可能会接二连三遇到同样的问题,这时不仅同事们士气下降,自己也会丧失信心,继而可能导致拆分失败。

2.5 给自己留个锦囊,“有备无患”。

锦囊就四个字“有备无患”,可以贴在桌面或者手机上。在以后具体实施过程中,多思考下“方案是否有多种可以选择?复杂问题能否拆解?实际操作时是否有预案?”,应用拆分在具体实践过程中比拼得就是细致二字,多一份方案,多一份预案,不仅能提升成功概率,更给自己信心。

2.6 放松心情,缓解压力

收拾下心情,开干!

3 实践

3.1 db拆分实践

DB拆分在整个应用拆分环节里最复杂,分为垂直拆分和水平拆分两种场景,我们都遇到了。垂直拆分是将库里的各个表拆分到合适的数据库中。比如一个库中既有消息表,又有人员组织结构表,那么将这两个表拆分到独立的数据库中更合适。

水平拆分:以消息表为例好了,单表突破了千万行记录,查询效率较低,这时候就要将其分库分表。

3.1.1 主键id接入全局id发生器

DB拆分的第一件事情就是使用全局id发生器来生成各个表的主键id。为什么?

举个例子,假如我们有一张表,两个字段id和token,id是自增主键生成,要以token维度来分库分表,这时继续使用自增主键会出现问题。

正向迁移扩容中,通过自增的主键,到了新的分库分表里一定是唯一的,但是,我们要考虑迁移失败的场景,如下图所示,新的表里假设已经插入了一条新的记录,主键id也是2,这个时候假设开始回滚,需要将两张表的数据合并成一张表(逆向回流),就会产生主键冲突!

因此在迁移之前,先要用全局唯一id发生器生成的id来替代主键自增id。这里有几种全局唯一id生成方法可以选择。

1)snowflake:https://github.com/twitter/snowflake;(非全局递增)

2) mysql新建一张表用来专门生成全局唯一id(利用auto_increment功能)(全局递增);

3)有人说只有一张表怎么保证高可用?那两张表好了(在两个不同db),一张表产生奇数,一张表产生偶数。或者是n张表,每张表的负责的步长区间不同(非全局递增)

4)……

我们使用的是阿里巴巴内部的tddl-sequence(mysql+内存),保证全局唯一但非递增,在使用上遇到一些坑:

1)对按主键id排序的sql要提前改造。因为id已经不保证递增,可能会出现乱序场景,这时候可以改造为按gmt_create排序;

2)报主键冲突问题。这里往往是代码改造不彻底或者改错造成的,比如忘记给某一insert sql的id添加#{},导致继续使用自增,从而造成冲突;

3.1.2 建新表&迁移数据&binlog同步

1)  新表字符集建议是utf8mb4,支持表情符。新表建好后索引不要漏掉,否则可能会导致慢sql!从经验来看索引被漏掉时有发生,建议事先列计划的时候将这些要点记下,后面逐条检查;

2)  使用全量同步工具或者自己写job来进行全量迁移;全量数据迁移务必要在业务低