做后端开发这几年,经历了好几次系统架构的演进,从单体应用到分布式,从分布式到微服务,每一次架构演进,都踩了不少坑,熬了不少夜,也积累了很多经验。今天就来聊聊架构演进之路踩过的那些坑,那些让我熬夜的问题,从单体到分布式、从分布式到微服务、从同步到异步、从单机到集群,每一次架构演进都遇到了哪些问题,是怎么解决的,有什么经验教训,希望能给正在做架构演进的朋友一些参考,避免踩同样的坑。
一、单体应用时代:简单但是隐患重重
最开始的时候,我们的系统是一个单体应用,所有的功能都在一个项目里,前端、后端、数据库,都在一起,部署的时候就是一个war包,扔到Tomcat里就能跑。那时候用户量不大,功能也不多,单体应用完全够用,开发简单,部署简单,调试方便,小团队用单体应用开发效率很高。
但是随着用户量的增长,功能的增加,单体应用的问题慢慢暴露出来了。
第一个问题是代码越来越臃肿,越来越难维护。所有功能都在一个项目里,代码量越来越大,从几万行到几十万行,模块之间耦合严重,改一个地方可能影响好几个地方,经常是改了A功能,B功能出bug了,改了B功能,C功能又出问题了。而且新人接手项目,要看几十万行代码,根本看不过来,上手很慢。
第二个问题是部署越来越麻烦,越来越慢。代码量大了,编译打包的时间越来越长,从几分钟到十几分钟,部署一次要等很久。而且,只要有一个功能有改动,就要重新部署整个应用,部署的时候还要停机,用户体验不好。如果部署出了问题,回滚也很麻烦,整个应用都要回滚。
第三个问题是扩展性差,无法按需扩展。单体应用只能整体扩展,不能按需扩展,比如某个功能压力大,需要多部署几台,但是只能把整个应用多部署几台,其他不需要扩展的功能也跟着多部署,浪费资源。而且,不同的功能对资源的需求不一样,有的需要CPU多,有的需要内存多,有的需要IO多,单体应用只能统一配置,无法按需配置资源。
第四个问题是技术栈受限,无法引入新技术。单体应用只能用一种技术栈,比如用Java就只能用Java,想用Python或者Go做某个功能,就很麻烦,无法根据不同的功能选择最合适的技术栈。而且,技术升级也很麻烦,比如想升级框架版本,整个应用都要改,风险很大。
这些问题,在用户量不大、功能不多的时候,还能忍受,但是随着用户量和功能的增长,这些问题越来越严重,最后到了不得不改的地步。于是,我们开始了第一次架构演进,从单体应用到分布式应用。
二、第一次演进:从单体到分布式,踩了这些坑
第一次架构演进,我们把单体应用拆分成了几个分布式的服务,按照业务领域拆分,比如用户服务、商品服务、订单服务、支付服务等,每个服务独立部署,服务之间通过RPC或者HTTP调用。
这次演进,解决了单体应用的很多问题,比如代码拆分了,每个服务代码量小了,好维护了;部署独立了,改一个服务只需要部署这个服务,不用停机了;扩展性好了,哪个服务压力大就扩展哪个服务,按需扩展;技术栈也灵活了,不同的服务可以用不同的技术栈。
但是,分布式也带来了很多新的问题,踩了很多坑,熬了很多夜。
坑一:分布式事务问题
这是分布式最大的坑,也是最让我头疼的问题。单体应用的时候,事务很简单,用数据库的本地事务就行,ACID都能保证。但是分布式之后,一个业务操作可能要调用多个服务,比如下单,要调用订单服务创建订单,调用库存服务扣减库存,调用支付服务扣款,这三个操作要么都成功,要么都失败,这就是分布式事务。
一开始,我们对分布式事务的复杂性估计不足,以为简单用一下就可以了,结果上线之后出了大问题。有一次,订单创建成功了,库存扣减成功了,但是支付扣款失败了,结果订单状态是已支付,但是钱没扣,用户白嫖了商品,造成了资损。还有一次,订单创建成功了,但是库存扣减失败了,结果超卖了,库存只有100件,结果卖了120件,用户投诉,只能退款赔偿。
这些问题,都是分布式事务没做好导致的,每次出问题都要熬夜排查,手动修复数据,很痛苦。后来,我们研究了各种分布式事务方案,比如两阶段提交(2PC)、三阶段提交(3PC)、TCC(Try-Confirm-Cancel)、本地消息表、事务消息、Saga模式等,根据不同的业务场景,选择了不同的方案。
对于强一致性要求高的场景,比如支付,我们用了TCC模式;对于最终一致性可以接受的场景,比如订单状态更新,我们用了本地消息表+消息队列的方案;对于长事务,我们用了Saga模式。经过一番折腾,终于把分布式事务的问题解决了,但是也踩了很多坑,熬了很多夜。
经验教训:分布式事务是分布式系统中最复杂的问题之一,一定要重视,在架构设计的时候就要考虑清楚,根据业务场景选择合适的方案,不要等上线出了问题才临时抱佛脚。而且,分布式事务没有银弹,没有一种方案能解决所有问题,要根据不同的业务场景选择不同的方案,甚至同一个业务的不同环节,用不同的方案。
坑二:服务调用的超时、重试、幂等问题
分布式系统中,服务之间通过网络调用,网络是不可靠的,会有延迟、丢包、超时等问题,所以服务调用的超时、重试、幂等问题非常重要,我们在这上面也踩了很多坑。
一开始,我们的服务调用没有设置超时时间,结果有一次,某个服务挂了,调用它的服务一直等,线程都被占满了,导致整个服务都不可用,引发了雪崩效应,好几个服务都挂了,排查了半天才发现是这个问题。后来,我们给所有的服务调用都设置了合理的超时时间,避免无限等待。
然后是重试问题,设置了超时之后,调用超时了怎么办?一开始我们简单地重试,结果又出问题了。有一次,某个服务响应慢,调用超时了,我们重试了三次,结果那个服务本来就慢,重试之后压力更大了,直接被打挂了,引发了更严重的问题。而且,重试还可能导致重复调用,比如第一次调用其实成功了,只是响应超时了,客户端没收到响应,就重试,结果服务端执行了两次,造成了重复扣款、重复下单等问题。
后来,我们优化了重试策略,不是所有错误都重试,只对网络异常、服务不可用等临时性错误重试,对业务错误不重试;而且重试要有次数限制和退避策略,比如重试3次,每次间隔时间递增,避免连续重试把服务打挂;更重要的是,所有的写操作都要做幂等,保证重复调用不会产生副作用。
幂等问题也是一个大坑,一开始我们很多接口都没做幂等,结果因为重试、网络重发、用户重复点击等原因,经常出现重复数据,比如重复下单、重复扣款、重复发短信,每次出问题都要手动删数据、退款,很麻烦。后来,我们给所有的写接口都加了幂等,用唯一请求ID或者业务唯一键来保证幂等,重复的请求直接返回第一次的结果,不重复执行,才解决了这个问题。
经验教训:分布式系统中,服务调用的超时、重试、幂等是三个最基础也是最重要的问题,一定要在设计的时候就考虑到,每个服务调用都要设置合理的超时时间,重试策略要合理,所有写操作都要做幂等,不然上线之后一定会出问题。
坑三:服务发现和负载均衡问题
分布式系统中,服务多了,服务的地址怎么管理?服务之间怎么发现对方?请求怎么负载均衡到不同的服务实例?这些都是服务发现和负载均衡的问题,我们在这上面也踩了坑。
一开始,我们用的是硬编码的方式,把服务的地址写在配置文件里,服务地址变了就改配置,重启服务。但是服务多了之后,这种方式根本没法维护,服务实例上下线、地址变更,都要手动改配置,很容易出错,而且服务挂了也不能自动摘除,请求还会转发到挂掉的服务上,导致错误。
后来,我们引入了服务注册中心,比如ZooKeeper、Consul、Eureka等,服务启动的时候注册到注册中心,服务下线的时候注销,服务消费者从注册中心获取服务地址列表,然后做负载均衡。这样,服务的上下线都能自动感知,不用手动改配置了,方便了很多。
但是,服务注册中心也带来了新的问题,比如注册中心挂了怎么办?服务注册信息不一致怎么办?心跳超时导致服务被误摘除怎么办?负载均衡策略怎么选?这些问题都需要考虑。我们就遇到过一次,注册中心出了问题,服务注册信息乱了,请求都转发到了错误的服务上,导致大面积错误,排查了很久才发现是注册中心的问题。
后来,我们做了很多优化,比如注册中心集群部署,保证高可用;服务消费者本地缓存服务地址列表,注册中心挂了还能用本地缓存继续工作;心跳超时时间设置合理,避免网络抖动导致服务被误摘除;负载均衡策略根据业务场景选择,轮询、随机、加权、一致性哈希等,不同场景用不同的策略。
经验教训:服务发现和负载均衡是分布式系统的基础设施,一定要选择成熟稳定的方案,做好高可用,不要自己造轮子。而且,要考虑各种异常情况,比如注册中心挂了、网络分区、服务误摘除等,做好容错和降级,保证系统的稳定性。
坑四:分布式系统的监控和排查问题
单体应用的时候,监控和排查问题很简单,看应用日志、看服务器状态、看数据库状态,基本就能定位问题。但是分布式系统之后,服务多了,调用链长了,一个请求可能要经过好几个服务,出了问题,排查起来非常困难,不知道是哪个服务出的问题,也不知道请求在哪一步慢了、失败了。
我们就遇到过很多次,用户反馈某个功能慢或者报错,但是我们看每个服务的日志,都没发现明显的错误,因为错误可能发生在服务调用的过程中,比如网络超时、序列化失败、负载均衡错误等,这些问题在单个服务的日志里看不到。每次排查这种问题,都要翻好几个服务的日志,对着时间戳一点点对,非常耗时,经常熬夜排查。
后来,我们引入了分布式链路追踪系统,比如Zipkin、Jaeger、SkyWalking等,给每个请求生成一个唯一的traceId,在服务调用的过程中传递,把整个调用链的信息都收集起来,包括每个服务的耗时、状态、错误信息等。这样,出了问题,只要拿到traceId,就能看到整个调用链,快速定位是哪个服务出的问题,哪一步慢了,哪一步失败了,排查效率大大提升。
除了链路追踪,我们还完善了监控系统,包括服务的CPU、内存、IO、网络等基础监控,服务的QPS、响应时间、错误率等业务监控,服务调用的成功率、耗时、超时率等调用监控,还有告警系统,有异常马上告警,不用等用户反馈才发现问题。
经验教训:分布式系统的监控和排查非常重要,一定要在架构演进的同时,完善监控和链路追踪,不然系统出了问题根本没法排查。不要等系统上线了、出问题了才想起来做监控,那时候就晚了。监控和链路追踪是分布式系统的基础设施,一定要提前做好。
三、第二次演进:从分布式到微服务,又踩了这些坑
分布式应用跑了一段时间,解决了单体应用的很多问题,但是随着业务的发展,分布式应用也出现了新的问题,比如服务粒度还是太粗,一个服务里还是有很多功能,耦合还是比较严重;服务的部署和发布还是不够灵活,大团队协作的时候还是会互相影响;技术栈还是不够灵活等。于是,我们又开始了第二次架构演进,从分布式到微服务。
微服务的核心思想是把服务拆得更细,每个微服务只负责一个单一的业务功能,独立开发、独立部署、独立扩展,团队也按照微服务来划分,每个团队负责一个或几个微服务,端到端负责。
微服务确实带来了很多好处,比如服务粒度更细,耦合更低,更容易维护;部署更灵活,每个微服务可以独立部署发布,互不影响;团队协作更高效,每个团队负责自己的微服务,不用和其他团队协调;技术栈更灵活,每个微服务可以用最合适的技术栈。
但是,微服务也带来了更多的问题,踩了更多的坑,熬了更多的夜。
坑一:服务拆分过度,导致服务数量爆炸,运维成本剧增
微服务最大的坑就是服务拆分过度,很多人以为微服务就是拆得越细越好,结果一个业务拆出了几十个甚至上百个微服务,服务数量爆炸,运维成本剧增。
我们一开始也犯了这个错误,把服务拆得很细,结果拆出了五六十个微服务,每个微服务都要独立部署、独立监控、独立运维,运维成本一下子就上去了。原来几个运维就能搞定的系统,现在需要专门的运维团队来维护这么多服务,而且经常出问题,不是这个服务挂了,就是那个服务启动失败,每天都在处理各种运维问题,疲于奔命。
而且,服务太多了,服务之间的调用关系也变得非常复杂,一个请求可能要经过十几个服务,出了问题排查起来更困难,性能也更差,因为网络调用的开销太大了。我们就遇到过,一个简单的查询请求,经过了十几个服务,每个服务都有网络开销和序列化开销,响应时间从原来的几十毫秒变成了几百毫秒,用户体验很差。
后来,我们重新审视了服务拆分,合并了一些拆分过度的微服务,把相关的功能合并到一个服务里,最后把五六十个微服务合并成了二十多个,运维成本降下来了,性能也提升了,而且维护性也更好了。
经验教训:微服务不是拆得越细越好,要根据业务领域和团队情况合理拆分,粒度要适中,太粗了达不到微服务的效果,太细了运维成本太高,性能也差。一般来说,一个微服务应该能被一个小团队(2-5人)完全负责,业务上是内聚的,有明确的边界。不要为了微服务而微服务,盲目拆分,那样只会带来更多的问题。
坑二:微服务的数据一致性和数据查询问题
微服务架构下,每个微服务都有自己的数据库,数据是分散的,这就带来了两个问题:数据一致性和跨服务数据查询。
数据一致性的问题,在分布式的时候就有,微服务之后更严重了,因为服务更多了,业务操作涉及的服务更多了,分布式事务更复杂了。这个我们在分布式的时候已经踩过坑了,有了一些经验,微服务之后继续沿用之前的方案,根据业务场景选择合适的分布式事务方案,倒是没出太大的问题,但是还是要持续注意。
跨服务数据查询是一个新的大坑,微服务架构下,数据分散在不同的服务的数据库里,但是很多业务场景需要跨服务查询数据,比如查询订单列表,需要同时展示订单信息、商品信息、用户信息,这些数据分别在订单服务、商品服务、用户服务里,怎么查询?
一开始,我们用的是服务间调用的方式,订单服务查询订单列表,然后调用商品服务查询商品信息,调用用户服务查询用户信息,然后组装起来返回。这种方式简单,但是问题很多,一是性能差,多次服务调用,网络开销大;二是耦合重,订单服务要依赖商品服务和用户服务,商品服务或者用户服务挂了,订单服务也受影响;三是复杂查询很难做,比如分页、排序、筛选,跨服务做这些非常麻烦,性能也很差。
后来,我们引入了CQRS(命令查询职责分离)模式,把写操作和读操作分开,写操作还是在各个微服务里,读操作专门做一个查询服务,把各个微服务的数据同步到查询服务的数据库里(比如Elasticsearch、ClickHouse等),查询的时候直接查查询服务,不用跨服务调用。这样,查询性能大大提升,也解耦了,复杂查询也能做了。
但是,CQRS也带来了新的问题,比如数据同步的延迟、数据一致性的保证、同步失败的处理等,我们也踩了一些坑,比如数据同步不及时,用户看到的数据是旧的;同步失败导致数据丢失等。后来,我们用了事件驱动的方式,微服务写操作成功后发事件,查询服务消费事件更新数据,并且做了重试和补偿机制,保证数据最终一致,才解决了这些问题。
经验教训:微服务架构下,数据查询是一个大问题,特别是复杂查询,一定要提前考虑好查询方案,不要等上线了才发现查询性能很差,做不了复杂查询。CQRS是一个常用的方案,但是也有它的复杂度,要根据业务场景选择,不是所有场景都需要CQRS,简单的跨服务查询用服务调用也可以,不要过度设计。
坑三:微服务的部署和发布问题
微服务数量多了,部署和发布也成了一个大问题,几十个微服务,每个都要独立部署发布,手动部署根本忙不过来,而且容易出错。我们一开始就是手动部署,每次发布都要熬夜,几十个服务一个个部署,还要验证,经常部署到半夜,而且经常出问题,不是这个服务部署失败,就是那个服务启动报错,很痛苦。
后来,我们做了CI/CD(持续集成/持续部署),用Jenkins、GitLab CI等工具,代码提交之后自动构建、自动测试、自动部署,不用手动操作了,大大提升了发布效率,也减少了人为错误。而且,我们还引入了容器技术(Docker)和容器编排工具(Kubernetes),把微服务打包成容器镜像,用Kubernetes来管理容器的部署、扩缩容、自愈等,运维效率大大提升。
发布策略方面,我们也做了优化,从原来的停机发布,变成了滚动发布、蓝绿发布、金丝雀发布等,发布的时候不用停机,用户无感知,而且出了问题可以快速回滚,风险大大降低。
经验教训:微服务架构下,CI/CD和容器化是必备的基础设施,没有这些,几十个微服务的部署和发布根本没法做,运维成本会非常高。一定要在微服务架构演进的同时,建设好CI/CD和容器化基础设施,不然会被运维问题拖垮。
坑四:微服务的团队协作和沟通问题
微服务不仅仅是技术架构的变化,也是组织架构的变化,康威定律说,系统架构和组织架构是对应的,有什么样的组织架构,就有什么样的系统架构。微服务架构下,团队也要按照微服务来划分,每个团队负责一个或几个微服务,端到端负责,这样才能发挥微服务的优势。
但是,我们一开始没有调整组织架构,还是原来的团队结构,前端团队、后端团队、运维团队、测试团队,结果微服务的优势完全发挥不出来,反而带来了更多的沟通问题。一个微服务的需求,要前端、后端、运维、测试多个团队协作,沟通成本很高,而且互相推诿,出了问题不知道是谁的责任,发布的时候也要多个团队协调,效率很低。
后来,我们按照微服务调整了组织架构,成立了几个跨职能的小团队,每个团队有前端、后端、测试、运维,负责一个或几个相关的微服务,端到端负责,从需求到开发到测试到部署到运维,都是这个团队负责。这样,团队内部沟通成本低,效率高,责任明确,微服务的优势才真正发挥出来。
经验教训:微服务不仅仅是技术问题,也是组织和管理问题,做微服务架构演进的同时,一定要调整组织架构,按照微服务来划分跨职能的小团队,端到端负责,不然微服务的优势发挥不出来,反而会带来更多的沟通和协作问题。
四、架构演进的一些经验和教训
经历了这几次架构演进,踩了这么多坑,熬了这么多夜,我也总结了一些经验和教训,分享给大家。
1. 架构演进要循序渐进,不要过度设计,不要盲目追新
架构演进是一个循序渐进的过程,要根据业务的发展和实际的问题,一步步来,不要一开始就搞最复杂的架构,不要为了微服务而微服务,为了分布式而分布式。单体应用能解决的问题,就不要搞分布式;分布式能解决的问题,就不要搞微服务。架构是为业务服务的,不是为了炫技,适合当前业务的架构就是最好的架构。
而且,不要盲目追新,什么技术火就用什么,新技术不一定适合你的业务,而且新技术往往不成熟,坑很多,用了之后可能会踩很多坑,得不偿失。用成熟稳定的技术,解决实际的问题,才是正道。
2. 架构演进要同时建设基础设施,不要只改业务架构
每次架构演进,不仅仅是业务架构的变化,还需要配套的基础设施,比如分布式事务、服务发现、配置中心、链路追踪、监控告警、CI/CD、容器化等等,这些基础设施一定要和业务架构同时建设,不要等业务架构改完了,出了问题才想起来做基础设施,那时候就晚了。
很多团队做架构演进,只关注业务架构的拆分,不关注基础设施的建设,结果分布式/微服务上线之后,各种问题都来了,事务问题、服务调用问题、监控问题、部署问题,疲于奔命,最后发现还不如原来的单体应用。所以,基础设施一定要提前建设好,这是架构演进成功的保障。
3. 架构演进要考虑团队的能力,不要超出团队的承受范围
架构越复杂,对团队的能力要求越高,分布式、微服务这些架构,需要团队有很强的技术能力和运维能力,不然根本hold不住,只会踩更多的坑,出更多的问题。
很多团队,技术能力一般,运维能力不足,却盲目上微服务,结果搞得一团糟,系统还不如单体应用稳定,开发效率也不如单体应用高。所以,架构演进一定要考虑团队的能力,选择团队能hold住的架构,不要盲目追求先进的架构,超出团队的承受范围,只会适得其反。
4. 架构演进要有容错和降级机制,保证系统的稳定性
分布式和微服务架构下,故障是常态,网络会超时,服务会挂,数据库会出问题,所以一定要有容错和降级机制,保证系统在部分故障的情况下还能正常运行,至少是核心功能正常运行。
容错机制包括超时、重试、熔断、降级、限流、隔离等,比如某个服务挂了,熔断器打开,直接返回降级结果,不要让故障蔓延到其他服务,引发雪崩效应;比如流量太大了,限流,保护系统不被打垮;比如非核心功能出问题了,降级,保证核心功能正常。这些容错和降级机制,是分布式系统稳定性的保障,一定要做好。
5. 架构演进是持续的,没有一劳永逸的架构
最后,要认识到,架构演进是持续的,没有一劳永逸的架构,今天合适的架构,明天可能就不合适了,业务在发展,技术在进步,架构也要跟着演进。所以,不要以为做了一次架构演进就万事大吉了,要持续关注架构的健康度,根据业务的发展和技术的进步,适时调整和优化架构,让架构始终适合业务的发展。
而且,架构演进不是一次性的大爆炸式的重构,那样风险太大,应该是持续的、渐进式的优化,每次只改一小部分,风险可控,持续优化,慢慢把架构调整到合适的状态。
五、写在最后
架构演进之路踩坑记:那些让我熬夜的问题。
以上就是我这几年经历的架构演进,以及踩过的那些坑,从单体到分布式,从分布式到微服务,每一次演进都解决了之前的问题,但是也带来了新的问题,踩了新的坑,熬了新的夜。但是,每一次踩坑,每一次熬夜,都让我对系统架构有了更深的理解,让我成长了很多。
架构演进是一条没有终点的路,没有完美的架构,只有适合当前业务的架构,也没有一劳永逸的架构,只有持续的演进和优化。重要的是,根据业务的实际情况,选择合适的架构,循序渐进地演进,同时建设好配套的基础设施,考虑好团队的能力,做好容错和降级,保证系统的稳定性,这样才能让架构真正为业务服务,而不是成为业务的负担。
希望我的这些踩坑经历和经验教训,能给正在做架构演进的朋友一些参考,少踩一些坑,少熬一些夜,让架构演进的路走得更顺一些。
最后用一句话结尾:"架构是演化出来的,不是设计出来的;适合的才是最好的,稳定的才是最优的。"愿我们都能在架构演进的路上,少踩坑,少熬夜,做出稳定、高效、适合业务的好架构。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录