ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从单体到微服务:可扩展性架构设计的性能演进之路

从单体到微服务:可扩展性架构设计的性能演进之路 1. 单体架构的性能天花板可扩展性问题的真正起点聊可扩展性架构设计我从来不建议直接拍脑袋上微服务而是先得把单体架构为什么撑不住这件事彻底想明白。很多时候团队一上来就说我们系统慢、扩展不动了要微服务但实际问下来连瓶颈在哪都没定位清楚。架构演进不是追新是为了解决具体问题。1.1 单体架构的三种扩展方式与各自上限单体应用在早期阶段其实是最舒服的形态代码集中、部署简单、调试方便我见过太多从几个人的小项目一路长到几十人团队还在跑单体的例子。单体的扩展方式就那三板斧垂直扩展Scale Up加CPU、加内存、换更强的物理机或云主机规格。优点是无脑缺点是成本曲线极其陡峭而且单机硬件存在物理上限8核不够上32核32核不够上64核但总有一个规格是云厂商不提供的。水平扩展Scale Out在应用服务器前面挂负载均衡多启几个实例分摊流量。这是单体架构最容易实现的横向扩容但它只能解决无状态应用层的流量压力数据库层面的瓶颈一点忙都帮不上。读写分离与缓存MySQL主从复制、Redis缓存热点数据这是在不动架构前提下最常用的性能优化手段。这三种方式组合起来可以把一个单体系统的支撑能力提升一到两个数量级。一个典型的业务系统初期单机部署能扛几百QPS做了负载均衡加缓存之后能到几千QPS再做读写分离和分库分表能到上万QPS。到了这个阶段你就会撞上一堵实打实的墙。1.2 单体架构卡在哪一层的性能瓶颈以我自己经历过的一个电商类项目为例巅峰期单体应用部署了16台8核16G的ECS数据库是一主两从的MySQL集群Redis扛热点商品缓存。这套架构在平时能稳定支撑日均百万级请求量但每逢大促流量翻倍问题就开始冒头应用实例还是那个单体所有功能模块用户、订单、商品、库存、支付回调都打在同一个进程里任何模块的代码变更都要整个应用发版发版期间所有功能一起抖动。数据库连接池最先被击穿16个应用实例满负荷运行每个实例的数据库连接池如果配成50瞬间就有800个连接同时打到主库。MySQL默认的最大连接数也就151左右DBA把max_connections调到2000才堪堪够用但主库的CPU和IO早就告警了。慢查询会无差别拖垮全局报表模块的一条复杂SQL在高峰期跑出5秒的执行时间直接把连接池占满前台用户的登录请求也跟着一起排队超时。单体架构下慢的地方和快的地方之间根本没有隔离机制一个卡点就是全局卡点。很多团队就是在这个节骨眼上开始喊微服务的。但我想先把结论放在前面微服务拆分本身就是一场高投入的架构升级它解决了单体架构的横向扩展问题但也会引入分布式环境下的复杂性。真正成熟的演进路径不是从单体一步跳到微服务而是先把单体内部的结构理清楚再逐步把边界清晰、性能瓶颈集中的模块抽离出去。1.3 单体架构体检表拆分前必须先回答的四个问题在动手拆分之前我习惯先拉一份清单把单体现状摸清楚。这四件事没做到位后面所有拆分决策都可能走偏瓶颈的分布图哪个模块耗CPU、哪个模块耗IO、哪个模块调用最频繁用APM工具比如SkyWalking、Arthas做一轮全链路采样拿到真实的调用频率和耗时数据而不是靠猜。数据域的耦合度哪些表被多个模块读写哪些SQL跨了多个业务域做join这是决定服务边界的关键依据。如果一张核心表被八个模块引用拆服务之前必须先做数据归属的梳理。接口调用的拓扑单体内部的类调用虽然有一定的耦合但毕竟还是同一个进程内的方法调用一旦拆成服务就会变成网络调用。网络请求的延迟、超时、重试、序列化开销全部要重新评估。团队的结构与发布节奏一个模块的改动频率是不是明显高于其他模块团队里是否有清晰的owner能对拆分后的服务负责微服务的开发和运维成本是持续的不是拆完就结束了。2. 微服务架构的拆分粒度边界划分是性能演进的第一关键决策很多团队拆微服务第一个动作就是打开IDE凭感觉把原来的业务模块目录一个个变成独立的工程。这样做出来的不是微服务只是把一个单体的崩溃问题放大成几十个微服务的编排问题。服务边界该画在哪必须回到数据和业务域的本质上思考。2.1 按业务能力而非技术功能拆分微服务拆分最经典的参考是领域驱动设计DDD中的限界上下文Bounded Context。通俗地讲就是把业务能力当成边界每个服务只对一块清晰的业务领域全权负责。一个电商系统很自然地被拆分成用户服务负责账号、注册、登录、收货地址等用户侧能力商品服务负责商品信息的维护、上下架、类目与属性库存服务负责仓储库存的扣减与回补订单服务负责订单创建、状态流转、订单查询支付服务负责支付通道对接、支付状态回传物流服务负责发货、轨迹、签收但请注意这个划分维度不是唯一的答案。如果你们的是一个强运营后台系统可能要把商品管理拆成商品前台展示服务和商品后台管理服务如果订单领域非常复杂订单服务和订单查询服务都可以拆开。拆分粒度没有放之四海而皆准的标准判断标准就一条这个服务是否拥有独立的数据边界与独立的故障隔离能力。2.2 拆分后的性能收益预期管理拆微服务这件事最容易被误解的是服务拆了性能就会变好。必须说句大实话微服务化本身不会带来单个接口的延迟下降甚至初期性能还会变差。因为在单体里调用一个方法可能只需要0.1毫秒拆成服务后同样的调用变成了网络RPC走一遍序列化、传输、反序列化至少增加2到5毫秒。那微服务带来的性能收益在哪在横向扩展能力。单体架构下你没法单独给订单查询这个模块扩容——要么整体扩应用实例要么不扩。微服务化之后订单查询服务独立部署可以单独从2个实例扩到20个实例其他服务完全不受影响。在典型业务中查询类请求的流量往往是写入类请求的10到50倍这种按需扩容的能力才是性能演进真正的价值所在。我拆过一个真实案例用户中心的读接口占了全站28%的请求量而它的写入接口只占3%。单体架构下为这一个读接口连带着用户模块、关联的账号体系全部要加机器。拆出来之后读接口的实例数单独扩到12个写入服务保持2个实例整体的机器成本反而下降了将近35%接口P99延迟从原来的280毫秒降到了75毫秒。这就是边界划分带来的可扩展性红利。2.3 从大单体到微服务的过渡路径绞杀者模式如果你现在守着的是一个几十万行代码的巨石系统直接停下来重构是不现实的。业界最成熟的做法是绞杀者模式Strangler Pattern在继续运行单体系统的同时把新功能和被反复改动的模块按业务边界抽离成新服务通过网关或者反向代理把对应流量逐步切到新服务上旧的单体代码则一点点减少最终被绞杀到只剩下最稳定的核心或者彻底废弃。这个模式的落地要点有三个流量切分要可灰度新服务上线初期先把5%的流量切过去观察错误率和延迟稳定后逐步提高比例。切流量的工具可以是网关层的按用户百分比路由也可以是按IP段路由。新旧并行期要做数据同步拆出去的服务需要自己的数据库但旧系统还在读写同一份数据。通常的做法是从旧库同步数据到新库或者让新服务和旧服务共用一段数据访问层在边界完全清晰后再做数据迁移。每个拆出去的模块都要能独立回滚尤其是数据库结构变更要设计成向前兼容的确保新服务出问题后能随时切回旧逻辑不至于因为拆了服务还导致整个系统不可用。我在多次迁移中验证了一个规律以每2到4周拆出一个服务的节奏推进整个过程大约需要6到12个月。如果团队期望一次性把单体重写成几十个微服务大概率会死在中间态的泥潭里。3. 微服务架构下的数据拆分与通信选型性能的关键变量服务拆了数据库不拆等于白拆。数据的归属权和访问方式是微服务架构中最容易出问题、也最影响性能的一部分。3.1 数据库按服务独立分库后的数据一致性如何解每个微服务应该拥有自己独立的数据库或至少独立的表空间其他服务不能直接访问它的数据表只能通过服务接口获取数据。这个原则带来的最直接问题是原本一条SQL就能完成的跨多表查询现在要变成多次RPC调用聚合。举个例子在单体时代查一份用户的订单列表可能是订单表join用户表一条SQL就搞定了。拆分后订单服务返回订单记录用户服务返回用户信息接口层要把两者聚合起来。如果为了减少调用次数可以让订单服务在写入时冗余一份用户名、用户头像等快照字段——这是一种以空间换性能的做法也是电商系统的常态。至于数据一致性微服务领域的分寸感很重要强一致场景比如库存扣减、支付扣款尽量通过同步RPC调用 事务补偿来保证最终一致或者引入分布式事务框架Seata等做分布式事务管理。弱一致场景比如订单状态变更后的短信通知、积分累计直接交给消息队列异步处理性能更好实现也更简单。从性能演进的角度看我强烈建议不要所有服务之间都用同步调用的方式。没必要。大量非实时的业务动作完全可以用消息队列削峰填谷既降低接口延迟又给系统增加了缓冲层。比如下单创建订单后后续的库存锁扣、优惠券核销、通知发送全部走MQ异步处理下单接口本身的耗时能下降一半以上。3.2 服务间通信选型HTTP与RPC框架的对比服务间通信方案当前主流就是两类基于HTTP的API调用比如Spring Cloud OpenFeign RestTemplate以及高性能RPC框架比如Dubbo、gRPC。我没有绝对立场但根据场景选择的原则很明确维度HTTP/OpenFeignDubbo/gRPC开发调试成本低直接浏览器或Postman调试高需要额外的工具和序列化协议理解通信性能偏高延迟基于HTTP文本协议低延迟二进制协议性能提升20%~50%跨语言支持好任何语言都能调用HTTPgRPC支持跨语言Dubbo则偏Java生态服务治理能力依赖Spring Cloud全家桶Nacos、Sentinel等Dubbo自带注册中心、负载均衡、容错机制适用场景对外API、跨团队协作、异构系统对接内部高并发调用、低延迟敏感的链路实际项目中我见到的架构大多是混合的对外的网关层走HTTP内部核心链路的高频调用走RPC尤其Java技术栈而事件通知和异步解耦走MQ。还是那句话没有最好的技术只有最合适的组合。这里要特别提醒一个性能陷阱服务间调用链过长。一个请求进来经过了网关、用户服务、订单服务、库存服务、支付服务、物流服务六跳每跳增加2到5毫秒P99延迟很容易从单体的100毫秒膨胀到300毫秒以上。控制微服务性能的关键动作之一就是把同步链路尽量控制在3跳以内超过3跳的考虑用聚合服务/查询服务做BFF层来合并多次调用或者把部分数据下沉到调用方做本地缓存。3.3 网关层的性能设计入口限流没那么简单微服务架构通常都会引入API网关如Spring Cloud Gateway、Kong、APISIX统一处理鉴权、路由、限流、日志。网关的性能设计直接影响全站吞吐网关必须无状态网关实例随时可以横向扩容所以任何会话状态都不能存本地。限流不能只做单机版单机限流Guava RateLimiter在单实例下好用但在多实例部署时会出现每台机器各自限流总流量远超预期的情况。要支持集群限流用Redis Lua脚本或Sentinel的集群流控能力。网关只做转发与通用逻辑不要在网关里写具体业务逻辑否则网关将成为新的性能瓶颈和修改绊脚石。鉴权、灰度路由这类通用横切逻辑放网关业务参数校验和组装放服务内部。4. 拆完之后的性能治理那些单体时代不存在的问题微服务拆分完成后业务代码的开发效率确实提升了但性能问题反而变得比以前更隐蔽、更难排查。下面这些坑我几乎在每一个微服务项目里都遇到过。4.1 分布式链路追踪没有它故障定位就是大海捞针单体时代查问题翻日志直接搜请求ID就行。微服务化之后一次用户请求会横跨多个服务的多台机器日志分散在几十个实例上。没有链路追踪排查一个慢请求可能要挨个服务翻十几份日志效率极低。我的建议是尽早部署全链路追踪系统。Java技术栈可以选SkyWalking它支持无侵入的agent接入对业务代码基本零改动也可以用Zipkin或者Jaeger配合Spring Cloud Sleuth/Micrometer Tracing。关键要做的配置每个服务都要生成全局唯一的traceId并在所有出站请求的Header中传递日志打印时带上traceId便于按链路筛选运维侧把日志采集接入Elasticsearch配合Kibana按traceId检索全链路日志有了链路追踪哪些环节慢、哪个服务出现了超时重试、哪个数据库查询耗时长一眼就能定位。这在性能调优阶段的价值远比在架构设计阶段想象得大。4.2 分布式事务与数据最终一致性性能与正确性的天平微服务拆分后原来在一个数据库事务里完成的多表操作被拆到不同服务的数据域里。经典的分布式事务解决方案有2PC两阶段提交强一致但性能较差、TCCTry-Confirm-Cancel性能较好但实现复杂以及最务实的事务消息方案。在实际电商下单场景中我更推荐本地消息表 消息队列的最终一致性方案在订单服务中业务操作和数据落地在同一个本地事务里同时插入一条消息记录然后通过一个定时任务或MQ producer把消息发到消息队列下游服务消费消息完成自己的业务动作。这种方案的性能损耗最小而且可靠性足够支撑99.9%的业务场景。值得注意的一点是不要因为微服务引入了分布式事务问题就把所有跨服务操作都套上分布式事务框架。分布式事务框架通常有较大的性能开销能通过业务设计规避的就规避。比如库存扣减与其用分布式事务强一致锁库存不如用Redis预扣减 异步对账兜底性能和一致性兼顾。4.3 配置中心与注册中心高可用必须摆在第一位微服务架构里注册中心Nacos、Consul、Eureka和配置中心Nacos、Apollo是基础依赖。这两者的稳定性决定了整个微服务集群的稳定性它们在性能演进中的地位必须提高。我在一个项目中踩过大坑注册中心是单节点部署的Nacos一次发布过程中Nacos短暂不可用导致所有服务的心跳续约失败服务间调用大面积报错全网服务中断了好几分钟。自那以后我对中间件的高可用部署格外执着注册中心和配置中心必须集群部署至少3个节点部署在不同物理机或可用区配置变更要支持灰度发布先推送到一台实例验证再全量推送服务消费端要设置合理的缓存与容错策略注册中心短暂不可用时消费端应能继续使用本地缓存的服务列表而不是直接报错这些细节在流量小的时候看不出问题一旦大促流量上来任何一个基础组件抖动都会被放大成全局性能事故。4.4 弹性伸缩与容量规划从加机器到算着加机器微服务的横向扩展能力要用得好还得有容量估算能力。不能等CPU被打满了再去扩容那是被动救火。我的常规做法是压测找基线每个核心服务上线前用压测工具如JMeter、wrk、Locust找到单个实例的QPS上限和P99延迟。留出安全余量生产环境的单实例目标负载压在压测上限的50%~60%给突发流量留缓冲。配置弹性伸缩策略云上环境可以直接用容器服务K8s的HPAHorizontal Pod Autoscaler按CPU使用率、QPS或自定义指标自动扩缩容。把最小实例数和最大实例数设好让系统在流量高峰期自动扩容、低谷自动缩容。兜底限流降级容量规划再精准也总有预估不到的情况。每个核心接口必须有Sentinel或Hystrix级别的限流、熔断、降级方案保证系统在极限流量下仍然能保住核心链路可用。我记得之前做过一次大促容量评估预估峰值流量16000 QPS按单实例800 QPS的平峰承载能力计算预留了22个实例16000 QPS需要至少25个实例再考虑1.5倍的故障冗余和安全余量实际大促当天峰值到了17500 QPS系统扛住了但P99延迟比平时高了约60毫秒属于可接受的范围。这个弹性伸缩的能力在单体架构下根本做不到——你总不能把整站扩到40台机器去伺候一个模块的流量。5. 团队与运维成本微服务性能演进的另一面技术上的演进做完了还有一个很多人后知后觉的坑微服务的长期维护成本。这部分的组织和运维变化比代码本身的影响更深远。5.1 微服务拆分后的运维复杂度不可回避单体应用一台服务器或少则几台服务器就能跑微服务化以后几十个服务、上百个实例人工运维完全不可能。这个过程必须配套的基建包括容器化与编排Docker打包 Kubernetes部署让每个服务独立发布、独立扩缩容。少数项目可能觉得K8s太重了那至少也要用Docker Compose或轻量级PaaS平台管理服务编排。CI/CD流水线每个服务独立构建、独立测试、独立部署。流水线里必须包含自动化测试环节我见过太多因为改了一个服务结果把另一个服务搞挂的案例没有自动化测试的微服务架构就是在裸奔。统一的可观测性三件套日志ELK/Loki、指标Prometheus Grafana、链路追踪SkyWalking/Jaeger缺一不可。尤其是告警规则要按服务维度分别配置比如订单服务的错误率超过1%就告警、支付服务的P99延迟超过200毫秒就告警。这些基建投入是一次性的但它们决定了微服务架构能不能长期健康地活下去。5.2 团队结构要跟着服务边界调整微服务领域有个著名的康威定律系统的架构往往反映了生产它的组织的沟通结构。如果你拆了服务但团队还是按前中后台的老方式组织服务边界和职责边界就会错位开发效率反而下降。正确的做法是用服务owner制来组织团队每个服务或每几个内聚的服务有一支全职能小队包含开发、测试、运维能力这支小队对自己负责的服务拥有完全的决策权和发布权。服务之间的协作通过明确的接口契约进行而不是靠跨团队的流程审批。这两年流行的平台工程和领域团队概念本质上都是在解决微服务架构下的组织效率问题。5.3 我的总结演进的核心始终是问题驱动我经历过单体系统的痛苦也体会过微服务带来的扩展性红利而且这些年在实践中积累了不少值得反复咀嚼的经验。如果要我把这套架构演进中的心得浓缩成几句话我会说架构设计的出发点一定是具体业务问题和演化趋势而不是听起来先进的技术标签。拆微服务之前先去做容量评估和链路分析确认瓶颈到底在应用层还是在数据层拆分过程中用绞杀者模式渐进式推进绝不搞推翻重来的大重构拆分完成后把可观测性和自动化运维作为一等公民而不是等出了事故再补课。可扩展性架构设计不是一锤子买卖它是一个持续演进的过程只要业务还在增长架构就没有最终形态。最后分享一个我自己坚持的小原则每次架构调整都要留下可量化的性能基线数据。改之前是什么水平改之后提升了多少要有对比才有说服力。这些数据不仅是向管理层证明架构演进价值的依据更是下次性能优化时最宝贵的起点。没有数据的架构升级基本等于碰运气。
返回列表