当前位置: 首页 > 科技观察

发展-再见了,公司的“烂系统”

时间:2023-03-13 23:29:14 科技观察

图片来自Pexels为什么要拆分?先来看一段对话:从上面的对话可以看出分裂的原因:应用之间耦合严重。系统中的各个应用程序是断开的,每个应用程序中都实现相同的功能。后果就是改一个功能,系统中的所有应用都需要同时改。这种情况通常存在于历史悠久的系统中。由于种种原因,系统中的各个应用都形成了自己的业务小闭环。业务扩展性差。从设计之初,数据模型就只支持某一类业务。一个新的业务类型来了之后,必须重写代码来实现。导致项目延期,极大影响业务接入速度。代码陈旧,难以维护。各种乱七八糟的ifelse和硬编码的逻辑散落在应用的各个角落,处处是陷阱,让开发和维护瑟瑟发抖。系统扩展性差。支撑现有业务的系统已经在颤抖,无论是应用还是DB都无法承受业务快速发展带来的压力。新坑越挖越多,恶性循环。如果不改,最后的结果就是杀系统。拆机前要准备什么?业务复杂性的多维把握是一个老生常谈的问题。系统与业务的关系?我们最期待的理想情况是第一关系(车与人)。如果业务感觉不合适,可以立即更换新业务。的。但现实是,它更像是心脏起搏器与人的关系,而不仅仅是改变而已。连接到系统的业务越多,耦合就越紧密。如果在没有真正把握业务的复杂性之前就贸然行动,最后的结果就是夺走你的心。如何把握业务的复杂性?它需要多维的思考和实践。一是技术层面。通过与PD和开发的讨论,熟悉各种现有应用的领域模型,以及它们的优缺点。这种讨论只能给人一个大概的概念。更多细节如代码和架构需要通过需求来制定。、改造、优化这些做法要掌握。在熟悉了各个应用之后,我们需要从系统层面进行构思。我们要打造平台化的产品,那么最重要也是最难的一点就是功能的集中管控,打破各个应用的业务小闭环,统一起来。这个决心更多的是开发、产品、业务方、各个团队“根据业务或客户需求来组织资源”的共识。此外,还需要与业务方保持功能沟通和计划沟通,确保拆分后的应用满足使用需求和扩展需求,并获得他们的支持。定义边界,原则:高内聚、低耦合、单一职责掌握了业务复杂度后,就要开始定义各个应用的服务边界。什么是好的境界?像葫芦兄弟这样的应用就不错!比如葫芦兄弟的技能(应用)是相互独立的,遵循单一职责的原则。比如水宝宝只能喷水,火宝宝只能喷火,隐形宝宝不能喷水喷火但是可以隐身。更重要的是,葫芦兄弟最终可以组合成金刚葫芦,也就是这些应用的功能虽然相互独立,但是又相互联系,最后组合起来成为我们的平台。这里很多人会有疑惑,如何控制拆分粒度?很难下一个明确的结论,只能说是结合业务场景、目标、进度的折衷。但总的原则是从大的服务边界开始,不要太细,因为随着架构和业务的演进,应用自然会再次拆分,让正确的事情自然发生是最合理的。确定拆分后的应用目标系统的宏应用拆分图画好后,就要进行具体的应用拆分。首先要确定的是一个应用拆分后的目标。拆分优化无底线。可能会越来越深,越来越无果,影响自己和团队的士气。比如本期的目标可以定为分拆DB和应用,数据模型的重新设计可以在第二期。判断待拆分应用当前的架构状态,如代码状态、依赖状态等,并推断可能出现的异常。做之前思考的成本远低于做之后解决遇到的问题的成本。app拆分最怕的就是在app中间说,“他一个,这块不能动,原来当时设计是有原因的,得想想另一种方式!”此时的压力可想而知,很有可能你会接连遇到同样的问题。这时候,不仅同事士气会下降,自己也会失去信心,可能会导致拆分失败。给自己留个药包,“有备无患”的药包就是“有备无患”四个字,可以贴在桌面上,也可以贴在手机上。在以后的具体实施过程中,多思考“是否有多种方案可供选择?复杂的问题能否拆解?有实际操作的方案吗?”在具体实践过程中,应用拆分就是一个“一丝不苟”二字。多一份计划,多一份计划,不仅能增加成功的概率,还能给自己信心。放松,缓解压力,清理干净,然后开始吧!改造实践分库实践分库实践分库是整个应用分库过程中最复杂的。分为垂直拆分和水平拆分两种场景。我们都遇到过。垂直拆分是将库中的各个表拆分到合适的数据库中。比如一个图书馆既有留言表又有人员组织结构表,那么把这两张表拆分成一个独立的数据库就比较合适。横向拆分:以消息表为例。单表已超过千万行,查询效率低。这时候就必须分库和分表了。①主键id连接全局id生成器。分库首先要做的是使用全局id生成器生成每张表的主键id。为什么?比如我们有一张表,有id和token两个字段,id是通过自增主键生成的,我们想用token维度来划分数据库和表,那么继续使用自增主键会导致问题。前向迁移扩容时,自增主键在新建的分库分表中必须是唯一的。但是,我们需要考虑迁移失败的场景,如下图,假设在新表中插入了新的表项。记录,主键id也是2,此时假设开始回滚,需要将两张表的数据合并到一张表中(逆流),就会发生主键冲突!所以在迁移之前,必须使用全局唯一id生成器生成的id来代替主键自增id。这里有几种全局唯一的id生成方式可供选择:Snowflake:非全局自增。MySQL新建一张表专门生成一个全局唯一的id(使用auto_increment函数)(globalincrement)。有人说只有一张表如何保证高可用?两张表都准备好了(在两个不同的数据库中),一张表生成奇数,另一张表生成偶数。或者有n张表,每张表负责不同的步长范围(非全局增量)...我们使用阿里巴巴内部的tddl-sequence(MySQL+内存),保证全局唯一但非增量。使用中遇到一些坑:需要提前修改按主键id排序的SQL。因为id不再保证增加,可能会出现乱序的场景。这时候可以修改为通过gmt_create排序。报告主键冲突。这往往是由于代码改造不完整或纠错造成的,比如忘记在某个insertsql的id上加上#{},导致继续使用自增,导致冲突。②新建表&迁移数据&binlog同步新表的字符集推荐为utf8mb4,支持表情符号。新表创建后,千万不要遗漏索引,否则可能导致SQL变慢!根据经验,索引丢失的情况时有发生。建议提前规划计划时将这几点记下来,以后再一一核对。使用全量同步工具或者自己写作业进行全量迁移;全量数据迁移必须在业务非高峰期进行,并根据系统情况调整并发数。增量同步:全量迁移完成后,可以使用binlog增量同步工具跟踪数据。比如阿里内部使用经纬,其他公司可能有自己的增量系统,或者使用阿里的开源。cannal/otter:https://github.com/alibaba/canal?spm=5176.100239.blogcont11356.10.5eNr98https://github.com/alibaba/otter/wiki/QuickStart?spm=5176.100239.blogcont11356.21.UYMQ17增量同步开始时获取的binlog位置必须在全量迁移之前,否则会丢失数据。比如我中午12:00开始全量同步,13:00全量迁移完成,那么增量同步的binlog位置必须选择在12点之前。第一位会导致重复记录吗?不!线上的MySQLbinlog是row模式。比如一条delete语句删除了100条记录,那么binlog记录的不是delete的逻辑sql,而是100条binlogs记录。插入语句插入一条记录。如果主键冲突,则不会插入。③联表查询SQL改造现在主键已经连接了全局唯一id,新的数据库表和索引已经建立,数据也实时均衡了,现在可以开始切库了吗?不!考虑以下非常简单的连接表查询SQL,如果将表B拆分到另一个数据库,你应该如何处理这条SQL?毕竟不支持跨库联表查询!因此,在切库之前,需要将系统中上百个联表查询的SQL转换完成。怎么改造呢?业务回避:业务松耦合后,技术才能松耦合,从而避免联表SQL。但短期内不现实,需要时间沉淀。全局表:每个应用程序库中都有一个冗余表。缺点:没有拆分,很多场景不切实际,表结构改动麻烦。冗余字段:就像订单表一样,有冗余的商品id字段,但是我们需要的冗余字段太多,需要考虑字段变化后的数据更新问题。内存拼接:通过RPC调用获取另一个表的数据,然后进行内存拼接。适用于job类型的SQL,或者RPC查询较少的修改型SQL。不适合大数据量的实时查询SQL。假设10000个ID,分页RPC查询,每次查100个ID,耗时5ms,一共需要500ms,RT太高了。本地缓存另一张表的数据,适用于数据变化小,数据量大查询,对接口性能和稳定性要求高的SQL。④切库方案设计与实现(两种方案)以上步骤准备工作完成后,开始真正的切库环节。这里有两种方案,我们在不同的场景下使用。DBwritestop方案,如下图所示:优点:速度快,成本低。缺点:如果要回滚,必须联系DBA进行在线停写操作。风险很高,因为有可能在业务高峰期回滚。检查的地方只有一处,出问题概率高,回滚概率高。例如,如果你面临一个比较复杂的业务迁移,以下几种情况很可能会导致回滚:SQLjoin表查询的转换不完整。SQL联合表查询纠错&性能问题。索引遗漏会导致性能问题。字符集问题:另外在binlog的反向回流中很可能会出现字符集问题(utf8mb4转gbk),导致回流失败。这些binlog同步工具为了保证最终的强一致性,一旦某条记录回流失败就会卡在不同步状态,导致新旧表数据不同步,进而无法回滚!双写方案,如下图:Step2“打开双写开关,先写旧表A再写新表B”,此时一定要在写表的时候trycatchB表,并用非常清楚的标记异常类型,方便排除故障。步骤2双写持续一小段时间(比如半分钟)后,可以关闭binlog同步任务。优点:将复杂的任务分解成一系列可衡量的小任务,步步为营。在线服务不间断,轻松回滚。字符集问题影响不大。缺点:流程步骤多,周期长。双写导致RT增加。⑤switch一定要写好无论是什么库切图,switch都是必不可少的,这里switch的初始值一定要设置为null!如果随便设置一个默认值,比如“读取旧表A”,假设我们已经读取了新表B链接起来。这时候,应用程序重新启动。在应用启动的瞬间,可能不会推送最新的“读新表B”开关推送。这时候可能会使用默认值,会造成脏数据!拆分后如何保证一致性?以前很多表在一个数据库中,使用事务很方便。既然拆分出来了,怎么保证一致性呢?如下:分布式事务性能差,几乎不考虑。消息机制补偿(如何利用消息系统避免分布式事务?)定时任务补偿更多的是用于实现最终一致性,分为添加数据补偿和删除数据补偿两种。拆分后如何保证应用的稳定性?一句话:怀疑第三方,防备用户,做你自己!①怀疑第三方防御性编程,制定各种降级策略;比如缓存主备、推拉组合、本地缓存……遵循fail-fast原则,一定要设置超时时间,捕捉异常。强依赖变弱依赖,并发逻辑异步化:我们将某个核心应用的并发逻辑异步化后,响应时间几乎缩短了1/3,后面的中间件和其他应用都出现了抖动,而核心链一切正常。适当保护第三方并谨慎选择重试机制。②为用户做好准备,设计好接口,避免误用:遵循最少接口暴露原则:很多同学在搭建新的应用后,会暴露很多接口,这些接口因为没人用,缺乏维护,很容易挖坑将来。我听过不止一种对话,“你怎么用我的界面,当时随便写的,性能很差”。不要让用户做接口能做的事情:比如你只暴露一个getMsgById接口,别人要批量调用,可能直接用for循环RPC调用。如果您提供getMsgListByIdList接口,则不会发生这种情况。避免长时间运行的接口:特别是对于一些老系统,一个接口可能对应一个for循环selectDB的场景。…限流:按应用优先级进行流量控制:不仅限制总流量,而且对应用进行区分。例如,核心应用的配额必须高于非核心应用。业务能力控制:有时候不仅是系统层面的限制,业务层面也需要进行限制。例如,对于某些基于Saas的系统,“您的租户最多可供10,000人使用”。③做自己a)单一职责b)及时清理历史坑:例子:比如我们改造的时候,发现一个一年前留下的坑,去掉之后,整个集群的CPU占用率下降了1/3.c)运维SOP:说实话,如果线上出了问题,如果没有计划,不管你怎么处理,都会超时。曾经遇到过DB故障导致的脏数据问题,最后不得不硬着头皮写代码清理脏数据,但是时间很长,只能眼睁睁看着故障不断升级。经历了这件事后,我们立刻想象出了脏数据的各种场景,然后启动了三个作业来清理脏数据,以防止其他不可预知的故障场景产生脏数据,直接触发这三个清理作业,先恢复再检查。d)可预测的资源使用:应用程序的CPU、内存、网络和磁盘是众所周知的。定时匹配耗CPU、性能密集的作业优化、降级、离线(循环调用RPC或SQL)慢SQL优化、降级、限流Tair/Redis、DB调用应该是可预测的Example:Tair,DB例如:a某些界面类似秒杀功能,QPS很高(如下图)。请求先到Tair,如果没有找到,会返回到source。当请求激增时,甚至会触发Tair/Redis层缓存限流。另外,由于缓存一开始是没有数据的,请求会穿透到DB,从而破坏DB。这里的核心问题是Tair/Redis层的资源使用是不可预测的,因为它取决于接口的QPS,如何让请求可预测?如果我们再加一层本地缓存(例如Guava,超时时间设置为1秒),保证单机一个key只有一次回源请求,这样使用Tair/Redis资源可预测。假设有500个client,对于一个key,最多500个请求可以瞬间穿透到Tair/Redis,以此类推到DB。再举个例子:比如有500个client,那么瞬间最多有500次请求DB某个key。如果有10个键,那么最多可能有5,000个请求到数据库。正好这些SQL的RT有些高。如何保护DB的资源?可以通过定时程序不断的将数据从DB刷新到缓存中。这里把不可控的5000QPSDB访问改成了可控的个位数QPSDB访问。总结①准备好面对压力!②复杂的问题需要拆解成多个步骤,每一步都可以测试回滚!这是应用拆分过程中最宝贵的实践经验!③墨菲定律:你担心的事情会发生,而且很快就会发生,所以准备好你的SOP(标准化溶液)吧!一个周五,和群里的同事一起吃饭的时候,我们讨论了某个功能存在风险,约定下周解决。该功能在工作中失败。以前说小概率不可能发生,但是即使概率很小也是值得的,比如P=0.00001%。在互联网环境下,如果请求量足够大,小概率事件真的会发生。④借假修真这个词似乎有点玄乎。顾名思义,就是用一些东西来提升另一种能力。前者称为假,后者称为真。在任何一个单位,大规模拆除和改造核心系统的机会都很少,所以一旦承担起责任,就义无反顾地全力以赴!不要被过程的曲折所吓倒。这是真的。作者:战利军编辑:陶家龙来源:cnblogs.com/LBSer/p/6195309.html