最近在负责一个遗留系统的改造项目,这个系统已经运行了五六年,代码混乱,架构老旧,性能差,经常出问题,随着业务增长,已经完全扛不住了,必须改造。但是遗留系统改造是一件非常棘手的事情,不能推倒重来,因为业务不能停,数据不能丢,用户不能受影响,只能在运行中改造,就像给飞行中的飞机换发动机,难度非常大。
经过几个月的调研、设计、开发、测试、上线,我们终于完成了第一阶段的改造,系统的性能和稳定性都有了很大的提升。今天就来分享一下这个遗留系统改造的架构设计经验,聊聊我们是怎么从一个老旧的单体系统,逐步改造成高可用高并发的分布式系统的,包括改造的原则、步骤、架构设计、技术选型、遇到的坑、以及最后的效果,希望能给正在做遗留系统改造的朋友一些参考。
一、原系统的问题和痛点
在说改造方案之前,先说说原系统有什么问题,为什么必须改造。
这个系统是一个电商类的业务系统,五六年前开发的,当时为了快速上线,用的是最传统的单体架构,所有的功能都在一个项目里,前端、后端、数据库都在一起,用的是PHP+MySQL,没有什么架构设计,怎么快怎么来。随着业务不断发展,功能越来越多,代码越来越臃肿,问题也越来越多。
1. 代码混乱,维护困难
这是最直接的问题,因为是快速开发的,没有规范,没有设计,代码写得非常混乱,一个函数几百行,一个文件几千行,到处都是复制粘贴,到处都是if else,注释很少,变量名乱七八糟,新人接手根本看不懂,改一个小功能都要研究好几天,还容易改出bug。
而且因为没有分层,没有模块化,业务逻辑、数据访问、页面渲染都混在一起,改一个地方,可能影响很多地方,牵一发而动全身,每次上线都心惊胆战,生怕出问题。
2. 性能差,扛不住高并发
原系统是单体架构,所有的功能都在一个应用里,数据库也是单库单表,随着用户量和数据量的增长,性能越来越差。高峰期的时候,接口响应很慢,经常超时,数据库连接数打满,CPU使用率100%,甚至会宕机,用户体验很差,经常收到投诉。
我们做过压测,原系统最多只能扛住500的并发,再高就不行了,但是业务高峰期的并发已经到了2000多,完全扛不住,必须改造。
3. 单点故障,可用性差
原系统是单点部署,应用只有一台服务器,数据库也只有一台,没有备份,没有容灾,一旦服务器出问题,或者数据库出问题,整个系统就挂了,而且恢复很慢,经常一停就是几个小时,对业务影响很大。
有一次数据库服务器的硬盘坏了,因为没有主从,没有备份,差点丢了数据,最后花了很大力气才恢复,停了大半天,损失很大,那次之后,公司就下定决心要改造系统。
4. 技术栈老旧,扩展困难
原系统用的技术栈很老旧,PHP版本还是5.3,MySQL是5.1,前端用的是原生的jQuery,没有用任何现代框架,很多新的技术和工具都用不了,想加个功能都很困难,开发效率很低。
而且因为是单体,想扩展某个功能的性能,只能整体扩容,不能单独扩容,比如订单模块压力大,但是其他模块压力小,也只能把整个应用多部署几台,很浪费资源,也不灵活。
5. 没有监控,出了问题不知道
原系统基本没有什么监控,只有服务器层面的CPU、内存监控,应用层面的监控、接口的监控、数据库的监控、业务的监控都没有,出了问题都是用户反馈了才知道,非常被动,而且排查问题也很困难,没有日志,没有链路追踪,不知道问题出在哪里。
这些问题加在一起,让原系统已经完全无法满足业务发展的需要了,必须进行改造,而且是刻不容缓。
二、改造的原则和目标
明确了问题之后,我们制定了改造的原则和目标,这些原则非常重要,是整个改造过程的指导思想,避免走弯路。
改造原则:
- 业务不能停,平滑过渡:这是最重要的原则,改造不能影响业务,不能停机,用户不能受影响,必须在运行中平滑过渡,就像给飞行中的飞机换发动机,不能让飞机掉下来。
- 逐步迭代,小步快跑:不能指望一下子把整个系统都改造完,那样风险太大,也不现实,要分阶段,分模块,逐步迭代,小步快跑,每一步都能验证,都能回滚,风险可控。
- 新旧并存,兼容过渡:改造过程中,新系统和旧系统要并存,通过路由、代理等方式,逐步把流量从旧系统切到新系统,出了问题可以随时切回去,保证安全。
- 数据优先,保证数据安全:数据是系统的核心,改造过程中,数据不能丢,不能错,数据迁移要保证一致性,要做好备份,做好校验,确保数据安全。
- 技术选型务实,不盲目追新:技术选型要务实,选择成熟、稳定、团队熟悉的技术,不要盲目追求新技术,不要为了用新技术而用新技术,适合的才是最好的,毕竟是生产系统,稳定是第一位的。
改造目标:
- 高可用:消除单点故障,系统可用性达到99.9%以上,单台服务器出问题不影响整体,数据库有主从,有备份,出问题能快速恢复。
- 高并发:系统能扛住5000以上的并发,接口平均响应时间在200ms以内,高峰期不宕机,用户体验流畅。
- 可扩展:系统架构要支持水平扩展,能根据业务需要灵活扩容,各个模块可以单独扩容,资源利用率高。
- 可维护:代码规范,结构清晰,模块化,分层,易于维护和开发,新人能快速上手,改功能不容易出bug。
- 可观测:有完善的监控、日志、告警、链路追踪,出了问题能快速发现,快速定位,快速解决。
三、整体架构设计
基于这些原则和目标,我们设计了新系统的整体架构,采用的是"前后端分离+分布式服务+读写分离+缓存"的架构,逐步从单体演进到分布式。
1. 整体分层
新系统整体分为几层:
- 接入层:用Nginx做反向代理和负载均衡,负责请求的接入、SSL终止、静态资源服务、流量分发,也做一些安全防护,比如限流、防刷、WAF等。
- 应用层:前后端分离,前端用Vue.js做SPA,后端是PHP的API服务,按照业务领域拆分成多个服务,比如用户服务、商品服务、订单服务、支付服务、搜索服务等,每个服务独立部署,独立扩展。
- 服务层:一些公共的基础服务,比如消息队列、缓存服务、文件存储服务、短信邮件服务、日志服务等,供各个业务服务调用。
- 数据层:MySQL做数据存储,做主从复制,读写分离,按业务分库;Redis做缓存,做分布式锁,做会话存储;Elasticsearch做全文检索;对象存储做图片和文件存储。
- 基础设施层:服务器用的是云服务器,有自动扩容,有负载均衡,有监控告警,有日志收集,有CI/CD流水线,支撑整个系统的运行。
2. 服务拆分
我们按照业务领域,把原有的单体系统拆分成了几个核心服务:
- 用户服务:负责用户的注册、登录、信息管理、地址管理、权限管理等。
- 商品服务:负责商品的管理、分类、库存、价格等。
- 订单服务:负责订单的创建、查询、状态管理、售后等。
- 支付服务:负责支付、退款、对账等。
- 搜索服务:负责商品的全文检索、筛选、排序等,用Elasticsearch实现。
- 内容服务:负责文章、公告、帮助中心等内容的管理。
每个服务都是独立的项目,独立部署,有自己的数据库,服务之间通过HTTP接口或者消息队列通信,这样每个服务可以独立开发、独立测试、独立部署、独立扩容,灵活性很高,也不会因为一个服务出问题影响整个系统。
当然,服务拆分不是一步到位的,我们是逐步拆的,先拆最核心、压力最大的模块,比如用户、商品、订单,其他的模块后面再慢慢拆,避免一下子拆太细,增加复杂度和运维成本。
3. 数据库架构
数据库是系统的核心,也是改造的重点,我们做了以下设计:
- 主从复制,读写分离:MySQL做一主多从,主库负责写,从库负责读,通过数据库中间件或者代码层面实现读写分离,减轻主库的压力,提高读性能。
- 按业务分库:把不同业务的数据放到不同的数据库里,比如用户库、商品库、订单库,避免单库太大,也避免业务之间互相影响,每个库可以独立优化,独立扩容。
- 大表分表:对于数据量很大的表,比如订单表、操作日志表,按照时间或者ID进行分表,比如订单表按月份分表,每个月一张表,避免单表数据量太大,查询慢。
- 索引优化:对所有的慢查询进行优化,添加合适的索引,删除没用的索引,优化SQL语句,避免全表扫描,避免慢查询。
- 数据备份:每天全量备份,每小时增量备份,备份文件存到对象存储,保留最近一个月的备份,定期做恢复演练,确保数据安全,出问题能快速恢复。
4. 缓存架构
缓存是提高性能的关键,我们用Redis做缓存,设计了多层缓存:
- 页面缓存:对于一些不经常变化的页面,比如商品详情页、文章详情页,做页面级的缓存,缓存整个页面的HTML,用户访问的时候直接返回缓存,不用渲染,性能非常高。
- 数据缓存:对于热点数据,比如商品信息、用户信息、分类信息,做数据缓存,缓存查询结果,减少数据库的查询压力。
- 会话缓存:用户的登录会话存在Redis里,分布式部署的时候,多个应用实例共享会话,用户不需要重新登录。
- 分布式锁:用Redis实现分布式锁,解决分布式环境下的并发问题,比如库存扣减、订单创建,避免超卖、重复创建。
缓存的更新策略,我们用的是"更新数据库+删除缓存"的方式,数据更新的时候,先更新数据库,然后删除对应的缓存,下次查询的时候再重新生成缓存,这样能保证缓存和数据库的一致性,也比较简单。同时设置了缓存过期时间,就算删除失败了,过期之后也会重新生成,避免脏数据一直存在。
为了防止缓存雪崩、缓存击穿、缓存穿透,我们也做了一些处理:
- 缓存雪崩:缓存的过期时间加随机值,避免大量缓存同时过期;热点数据永不过期;服务降级和限流。
- 缓存击穿:热点数据用互斥锁,缓存失效的时候,只有一个请求去查数据库生成缓存,其他请求等待,避免大量请求同时打到数据库。
- 缓存穿透:查询不存在的数据,也缓存一个空值,设置短的过期时间,避免每次都查数据库;用布隆过滤器过滤掉不存在的key。
5. 消息队列
我们引入了消息队列(用的是RabbitMQ),用来做异步处理、服务解耦、流量削峰:
- 异步处理:一些不需要同步返回的操作,比如发送短信、发送邮件、生成报表、记录日志,都放到消息队列里异步处理,提高接口的响应速度。
- 服务解耦:服务之间通过消息队列通信,降低耦合度,比如订单创建成功后,发一条消息到队列,库存服务、积分服务、通知服务都可以消费这条消息,做自己的处理,不需要订单服务一个个调用,扩展性更好。
- 流量削峰:高峰期的时候,请求量很大,直接打到数据库可能扛不住,就先把请求写到消息队列里,然后消费者按照自己的处理能力慢慢消费,把高峰的流量削平,保护系统不被冲垮。
消息队列的引入,大大提高了系统的吞吐量和可靠性,也让服务之间的耦合度降低了,扩展性更好。
6. 搜索架构
原系统的搜索是用MySQL的like查询,性能很差,数据量大了之后根本扛不住,我们引入了Elasticsearch做全文检索:
- 商品数据同步到Elasticsearch,支持全文检索、多条件筛选、排序、聚合等功能,搜索性能非常高,毫秒级返回。
- 数据同步用的是双写+定时补偿的方式,数据更新的时候,同时写MySQL和Elasticsearch,然后有一个定时任务,定期对比MySQL和Elasticsearch的数据,修复不一致的数据,保证数据的一致性。
- 搜索服务独立部署,独立扩容,和其他业务服务隔离,不会因为搜索的压力影响其他业务。
四、改造的步骤和过程
架构设计好了之后,我们是分步骤实施的,不是一下子全部换掉,而是逐步过渡,平滑迁移。
第一步:基础设施搭建
首先搭建新系统的基础设施,包括服务器、网络、负载均衡、数据库主从、Redis集群、消息队列、Elasticsearch、监控系统、日志系统、CI/CD流水线等,把基础的环境搭好,把基础的组件部署好,为后续的开发和迁移做好准备。
这一步很重要,基础设施是整个系统的基石,一定要搭好,做好高可用,做好监控,不然后面出问题会很麻烦。
第二步:新功能用新架构开发
基础设施搭好之后,我们规定,所有的新功能,都用新的架构开发,前后端分离,服务化,用新的技术栈,新的规范,不再往老系统里加新功能了。这样新功能就不会再增加老系统的复杂度,也能让团队逐步熟悉新的架构和技术栈,为后续的迁移积累经验。
同时,新系统和老系统是打通的,通过接口或者数据库共享,新系统可以调用老系统的数据和功能,老系统也可以调用新系统的接口,保证业务的完整性。
第三步:核心模块逐步迁移
新功能用新架构开发了一段时间,团队熟悉了新架构,基础设施也稳定了,我们就开始逐步把老系统的核心模块迁移到新架构。
迁移的顺序是按照模块的重要性和压力来的,先迁移压力最大、最核心的模块,比如用户模块、商品模块、搜索模块,这些模块迁移了,系统的性能和稳定性就能有很大的提升,然后再迁移其他的模块,比如订单、支付、内容等。
每个模块的迁移,都是先在新系统开发好,测试通过,然后通过Nginx的路由,把这个模块的流量逐步从老系统切到新系统,先切10%的流量,观察一段时间,没问题再切30%、50%、100%,全部切过去之后,老系统对应的模块就可以下线了。
这个过程中,数据是双写的,老系统和新系统都写,保证两边的数据一致,切流的时候,用户不会有感知,出了问题可以随时切回老系统,风险可控。
第四步:老系统下线
所有的模块都迁移到新系统之后,老系统就没有流量了,观察一段时间,确认没有问题,就可以把老系统下线了,服务器、数据库都可以释放掉,完成整个改造。
当然,老系统的数据要全部迁移到新系统,做好校验,确保数据没有丢,没有错,老系统的代码和数据库也要备份保留一段时间,万一有问题还能回滚。
整个改造过程,我们花了大概半年的时间,分了好几个阶段,每个阶段都有明确的目标和验收标准,逐步推进,风险可控,业务没有受到影响,用户也没有感知,非常平滑。
五、遇到的坑和解决方法
改造过程中,我们也遇到了很多坑,分享几个比较典型的,大家可以避避坑。
1. 数据一致性问题
这是最大的坑,迁移过程中,新老系统双写,很容易出现数据不一致的问题,比如老系统写成功了,新系统写失败了,或者反过来,两边的数据就不一样了,切流的时候就会出问题。
解决方法:
- 双写的时候,以老系统为主,老系统写成功了,再异步写新系统,新系统写失败了,记录日志,告警,人工处理或者定时补偿。
- 写一个数据校验的定时任务,每天凌晨对比新老系统的数据,找出不一致的数据,自动修复或者告警人工处理。
- 切流之前,做一次全量的数据校验和修复,确保两边的数据完全一致,再切流。
- 切流之后,继续双写一段时间,确认新系统稳定了,再停止老系统的写。
2. 分布式事务问题
服务拆分之后,一个业务操作可能涉及多个服务,比如创建订单,需要调用库存服务扣库存,调用用户服务扣积分,调用支付服务创建支付单,这就涉及到分布式事务的问题,怎么保证多个服务的数据一致性,是个难题。
我们的解决方案是,尽量避免分布式事务,通过业务设计,把需要强一致的操作放到同一个服务里,或者用最终一致性的方案,比如消息队列+本地消息表,保证最终一致。对于必须强一致的场景,用Seata这类分布式事务框架,但是尽量少用,因为分布式事务会增加系统的复杂度,降低性能。
3. 缓存一致性问题
缓存用多了,缓存和数据库的一致性就是个大问题,数据更新了,缓存没更新,用户就会看到旧数据,影响体验。
我们的解决方案是"更新数据库+删除缓存",而不是更新缓存,因为删除缓存更简单,更可靠,下次查询的时候再重新生成。同时设置缓存过期时间,就算删除失败了,过期之后也会重新生成。对于特别重要的数据,更新之后主动刷新缓存,或者用消息队列通知缓存更新。
4. 服务拆分太细,运维成本高
一开始我们差点犯了一个错误,就是想把服务拆得很细,什么都拆成独立的服务,后来发现,服务拆太细了,运维成本很高,部署、监控、排查问题都很麻烦,团队也扛不住。
后来我们调整了策略,服务拆分要适度,按照业务领域,把关系紧密、变更频率差不多的功能放到同一个服务里,不要拆太细,先粗粒度拆分,后面有需要了再细拆,毕竟我们团队不大,运维能力有限,适合的才是最好的。
5. 监控不到位,出了问题不知道
改造初期,监控没有跟上,新系统上线了,但是没有完善的监控,出了问题都是用户反馈了才知道,很被动。后来我们补了监控,从服务器、数据库、中间件,到应用接口、业务指标,都做了监控,配置了告警,出了问题马上就能发现,马上就能处理,系统的稳定性提升了很多。
监控真的非常重要,特别是分布式系统,没有监控,就像瞎子一样,出了问题根本不知道在哪里,一定要把监控做好,这是系统稳定运行的保障。
六、改造后的效果
经过几个月的改造,第一阶段完成之后,系统的效果非常明显:
1. 性能大幅提升
接口的平均响应时间从原来的800ms降到了150ms,95分位响应时间从2s降到了300ms,系统的并发承载能力从500提升到了8000,高峰期也很流畅,不会再出现超时、宕机的情况,用户体验好了很多。
2. 可用性大幅提升
消除了单点故障,应用多实例部署,负载均衡,数据库主从,有备份,有容灾,单台服务器出问题不影响整体,系统可用性从原来的99%提升到了99.95%,很少出故障,就算出了问题,也能快速恢复,对业务影响很小。
3. 开发效率提升
新系统代码规范,结构清晰,模块化,分层,新人上手快了很多,开发新功能的速度比原来快了一倍,而且bug少了很多,测试和上线的压力也小了很多。服务化之后,各个服务可以独立开发、独立部署,团队协作更顺畅,不会因为一个功能的改动影响整个系统的上线。
4. 可扩展性提升
系统支持水平扩展,哪个服务压力大,就给哪个服务加机器,不用整体扩容,资源利用率高了很多,扩容也很方便,几分钟就能加一台机器,应对业务增长和流量高峰更从容了。
5. 可观测性提升
有了完善的监控、日志、告警、链路追踪,出了问题能快速发现,快速定位,快速解决,运维效率提升了很多,原来排查一个问题要几个小时,现在几分钟就能定位到问题所在。
总的来说,这次改造是非常成功的,达到了预期的目标,系统的性能、稳定性、可维护性、可扩展性都有了很大的提升,为后续的业务发展打下了坚实的基础。
七、经验总结
最后,总结一下这次遗留系统改造的经验,给大家一些参考:
- 遗留系统改造,业务不能停是第一位的:一定要保证业务的连续性,平滑过渡,不能为了改造影响业务,更不能停机改造,要在运行中改造,逐步切换,风险可控。
- 不要推倒重来,要逐步迭代:推倒重来风险太大,也不现实,要分阶段,分模块,逐步迭代,小步快跑,每一步都能验证,都能回滚,这样风险才可控。
- 架构设计要适度,不要过度设计:不要一开始就追求完美的架构,不要拆太细的服务,不要用太多复杂的技术,要根据团队的能力和业务的需要,设计合适的架构,先解决最核心的问题,后面再逐步优化,过度设计只会增加复杂度和成本。
- 数据是核心,一定要保证数据安全:改造过程中,数据不能丢,不能错,数据迁移要做好校验,做好备份,做好双写,保证一致性,数据出了问题,就是大问题。
- 监控和运维要跟上:分布式系统比单体系统复杂得多,没有完善的监控和运维,根本玩不转,一定要把监控、日志、告警、链路追踪做好,这是系统稳定运行的保障。
- 团队的技术能力要跟上:新的架构和技术,需要团队去学习和掌握,改造之前要做好技术培训,让团队熟悉新的技术栈和架构,不然系统搭好了,团队不会用,也维护不好,反而会出更多问题。
- 技术选型要务实,不要盲目追新:生产系统,稳定是第一位的,技术选型要选择成熟、稳定、团队熟悉的技术,不要盲目追求新技术,不要为了用新技术而用新技术,适合的才是最好的。
遗留系统改造是一件非常有挑战的事情,但是只要方法对,步骤对,风险可控,是完全可以成功的。希望我们的经验能给大家一些参考,少走弯路,少踩坑。
八、写在最后
遗留系统改造架构设计:高可用高并发。
以上就是我们这次遗留系统改造的架构设计和经验分享,从原系统的问题,到改造的原则和目标,到整体架构设计,到改造的步骤,到遇到的坑,到最后的效果和经验总结,做了一个比较全面的分享。
遗留系统改造是每个技术团队都会遇到的问题,随着业务的发展,原来的系统总会变得老旧,跟不上业务的需要,这时候就需要改造。但是改造不是一件容易的事情,风险大,难度高,需要谨慎对待,做好规划,逐步推进,不能急于求成。
我们这次改造,也只是完成了第一阶段,后面还有很多工作要做,还有很多模块要迁移,还有很多优化要做,架构演进是一个持续的过程,没有终点,只有不断地优化,不断地适应业务的发展。
希望我们的经验能给正在做遗留系统改造的朋友一些参考,也欢迎大家交流自己的改造经验,一起学习,一起进步。
最后,用一句话结尾:"架构不是设计出来的,是演进出来的;好的架构,不是最完美的,而是最适合当前业务、能支撑未来发展的。"愿我们都能设计出适合自己业务的架构,支撑业务的发展,也在这个过程中不断提升自己的技术能力。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录