ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从单体到微服务:性能瓶颈、拆分演进与可扩展性治理实战

从单体到微服务:性能瓶颈、拆分演进与可扩展性治理实战 1. 单体架构的性能上限到底卡在哪里很多团队找我聊架构设计的时候上来第一句话就是“我们系统扛不住了想拆微服务”。我问他们扛不住的具体表现是什么大多数回答都绕不开三个现象高峰期页面打开慢、数据库时不时报警CPU打满、应用一重启用户全部掉线要重新登录。这三个现象其实很有代表性它恰好指向单体架构最容易成为瓶颈的三个位置应用服务器的线程池、数据库连接池、以及本地Session存储。1.1 应用线程池被低估的并发闸门单体应用跑在Tomcat这类容器里默认情况下每个请求会占用一个工作线程。Tomcat的默认最大线程数在不同版本里略有差异常见配置是200到800之间。听起来800并发已经不小了但你要知道这800个线程不是只处理你业务代码里那几毫秒的逻辑而是从接收到请求、解析参数、调用数据库、序列化响应一直占用到整个请求结束。我见过一个典型的电商后端单接口平均响应时间在120毫秒左右。算一下极限吞吐一个线程每秒可以处理大约8个请求200个线程满负荷运转也就是1600 QPS。听起来还行但如果这个接口里有一次慢SQL查询耗时变成800毫秒单线程吞吐立刻掉到1.25 TPS整体吞吐直接崩到250 QPS。这就是为什么线上经常出现“一个慢查询拖垮整个应用”的现象——不是数据库先垮的是应用线程池先被打满后续请求全部排队然后超时、重试、雪崩。1.2 数据库连接池最多人配错的一个参数单体架构里所有业务模块共享同一个数据库连接池这个池子的配置直接决定了系统的真实并发上限。HikariCP里有一句很经典的公式可以作为参考maximumPoolSize (core_count * 2) effective_spindle_count这里的core_count是应用服务器CPU核心数effective_spindle_count指磁盘阵列中有效磁盘轴数SSD环境下通常取1。按照这个公式一台4C8G的机器跑应用合理连接池大小大概在9到11之间。很多刚接触的同学会觉得这个数值也太小了恨不得把连接池配到100、200结果就是数据库端连接数暴涨开销全耗在线程上下文切换和连接维护上吞吐反而下降。注意连接池大小不是越大越好。一个经验值是数据库CPU保持在60%到80%利用率时连接池通常就是最优状态。如果你发现配到50个连接数据库还是慢大概率问题不在池子大小而在SQL本身。1.3 Session本地存储扩展路上第一堵墙单体时代做负载均衡最烦的就是Session问题。默认情况下用户登录状态存在单台Tomcat的内存里一旦流量转发到另一台机器那边没有这个Session用户就被判定未登录体验就是“登录状态总是丢”。当时的主流解决方案有几种Session复制、粘滞会话Sticky Session、把Session挪到Redis。Session复制在节点多的时候会产生大量广播流量3个节点以内还能凑合用超过5个节点基本就是灾难。粘滞会话本质上是把流量固定打到某台机器上等于让负载均衡器帮你在应用层做“分片”一旦这台机器宕机粘在上面的用户还是躲不掉掉线。真正彻底解决这个问题的办法是Session外置化也就是把登录态、会话信息统一存到Redis这类外部存储里。这一步做完应用节点才真正变成“无状态”后面做水平扩展才有基础。很多人把这个当成微服务的前置步骤其实它就是为可扩展性扫清的第一道障碍。2. 微服务不是万能药什么情况才值得拆讲完单体的瓶颈很多人会想那是不是干脆全部拆成微服务就好了还真不是。我见过一个团队业务量日均请求不过几百万也硬是拆了二十多个微服务结果线上排查问题要在十几个服务之间跳来跳去发布一次要协调好几个小组效率比以前单体还低。2.1 真正值得拆微服务的四个信号以我自己的经验判断一个系统要不要从单体演进到微服务至少要满足下面四个信号里的两到三个第一模块间的资源诉求差异明显。比如用户量上来了A模块的负载高到天天报警B模块却常年空闲。如果不拆分为了A模块多几个实例你得把整个单体应用都扩容B模块占用的资源就被白浪费了。第二团队协作成本已经超过系统复杂度。单体的代码库膨胀到几十万行两个团队改同一个模块合并代码天天冲突发版相互阻塞。这时候把业务边界切开让每个团队负责一小块管理成本反而更低。第三性能瓶颈已经无法通过单体内部的局部优化解决。比如某个模块需要使用完全不同的存储方案图数据库、搜索引擎或者需要独立扩容来应对突发流量。继续停留在单体里这些诉求都很难开展。第四故障隔离需求变得迫切。单体内一个模块的内存泄漏可能导致整个进程OOM所有业务跟着遭殃。拆成独立服务后至少能把故障范围圈定在某个业务域内。2.2 先划业务边界再谈技术拆分拆分微服务最忌讳的是按技术层拆。我见过有人把Controller拆成一个服务、Service拆成一个服务、Mapper拆成一个服务结果一次接口调用要跨越好几个服务网络开销翻了好几倍性能还不如原来的单体。正确的思路是按业务域拆也就是DDD里常说的限界上下文。一个用户服务负责所有跟用户账号、画像、权限相关的逻辑一个订单服务负责下单、查询、售后等订单全链路逻辑。服务之间通过接口通信数据各自私有不直接访问对方的数据库表。这样拆分之后每个服务的内聚性很高改动影响面也容易控制。2.3 团队规模与发布节奏容易被忽视的决策因子如果你们团队总共就五个人系统日均QPS几百那我的建议很直接不要拆微服务先用单体把业务跑通。微服务带来的注册中心、配置中心、网关、链路追踪、日志聚合、分布式事务这些基础设施每一个都需要人维护。五个人要一边写业务一边运维十几个基础组件基本不可能。反过来如果团队已经超过两个小组并且每周都要发版单体时代的发版协调成本会让你痛不欲生。A组改了个接口B组还在用旧参数上线顺序错了就出事故。这时候微服务的独立部署能力才有真正的价值。微服务对性能的贡献不是因为它用了什么高深的技术而是它让“独立扩缩容”和“故障隔离”成为可能。热点服务可以单独多开几个实例边缘服务可以保持最小规模整个系统的资源利用率会显著提升。3. 从单体到微服务的演进实操一步步拆给你看确定要拆了具体怎么落地这里我梳理一遍从单体到微服务比较稳妥的演进路线每步都附上我实操过的方法和参数。3.1 第一步无状态化改造拆微服务之前先确保单体应用本身是可以水平扩展的。重点就一件事去掉所有存储在应用进程内的状态包括Session、本地缓存、临时文件。Session迁移到Redis本地缓存如果跨节点共享就改用分布式缓存临时文件放到对象存储或者共享存储。这一步看起来简单但坑不少。我遇到最典型的坑是有人把验证码图片生成后写到应用本地磁盘然后通过Nginx的静态路径直接访问负载均衡一开部分用户就是图片加载不出来。后来把验证码改成Redis存储、接口直出图片流问题才解决。3.2 第二步先拆非核心基础能力不建议一上来就动核心交易链路风险太大。稳妥的做法是先挑那些边界清晰、独立性强的模块下手比如短信通知、消息推送、文件上传、定时任务。把这些能力拆成独立服务积累一套从拆分、发布、监控到回滚的方法论团队也有了信心。我当时拆的第一个服务是消息中心。原来发短信、发邮件、发站内信的代码散落在各个业务模块里每个模块都要配置一遍短信供应商。拆成独立服务之后业务方只需要调用一个发送接口由消息中心统一做模板管理、失败重试、发送记录存档。拆完不到两周消息中心的发送成功率从原来的98.5%提升到99.9%以上原因是重试机制从“业务代码里各写各的”变成了“统一可靠重试”。3.3 第三步按核心业务域逐步拆分基础能力拆完之后再动核心业务。这里我建议遵循一个原则每次只拆一个业务域每拆一个域就保证它独立可部署、可回滚稳定运行两到四周再拆下一个。以电商系统为例核心业务域一般包括用户域、商品域、订单域、库存域、支付域。我的拆分顺序是用户域优先因为几乎所有业务都会依赖用户信息而且用户域和外部系统的耦合相对少。拆完之后再把商品域独立出来商品数据本身适合用缓存扛读压力拆分后单独做缓存化改造非常方便。最后才拆订单和库存这类强一致性的域因为它们涉及分布式事务复杂度最高需要额外准备方案。3.4 基础设施选型Spring Cloud Alibaba全家桶的取舍微服务落地离不开基础设施。我自己的技术栈选择是Spring Cloud Alibaba这套核心组件包括Nacos注册中心与配置中心二合一。注册中心负责服务发现配置中心负责动态配置。选它而不是Eureka主要是因为它自带配置中心能力少维护一个组件。Spring Cloud Gateway统一流量入口负责路由转发、鉴权、限流。性能比Zuul好不少底层基于Netty非阻塞模型在高并发下表现更稳。OpenFeign服务间声明式HTTP调用。配合Nacos做服务发现调用方只需要定义接口具体地址由注册中心动态提供。Sentinel流量治理组件限流、熔断、降级都在这一层做掉。这套组合的好处是组件之间集成度高踩坑的“坑”前人基本都填得差不多了。坏处是组件本身有学习和运维成本。小团队如果不想引入这么重的体系也可以考虑先用单独的Nacos加Feign限流暂时在网关层做撑到规模上来再加Sentinel。3.5 Sentinel限流降级一个生产级配置样例说到流量治理这里给一个我实际用过的Sentinel限流降级配置思路可以用作参考。限流的目的是保护系统不被突发流量打垮。我的做法是在网关层针对每个路由设置QPS限流阈值同时在核心服务接口上再设一层防护。比如订单服务日常峰值的QPS大概在3000左右我给下单接口设置的单机限流阈值是800 QPS集群阈值按实例数乘以600来估算留出30%左右的余量防止某台机器出现GC停顿或者网络抖动时直接打穿下游。降级规则的设置思路就四个字保住核心。当非核心接口比如优惠券列表、推荐位商品出现慢调用或异常比例上升时直接降级返回默认值或者缓存中的旧数据。比如优惠券服务不可用时下单流程不阻断只是用户看不到可用优惠券这样既能保证主流程可用又能给下游恢复争取时间。熔断则是针对服务间调用的保护。OpenFeign结合Sentinel可以给某个Feign接口配置熔断规则比如10秒内异常比例超过50%就熔断该调用5秒直接走降级逻辑不再真正发起请求。这样任何一个下游服务的抖动都不会无限向上游传播链路不至于整体雪崩。3.6 拆完之后链路变长性能开销怎么补微服务的代价在调用链路上。原来单体里一次本地方法调用只需要微秒级拆完之后变成一次远程调用网络传输加序列化反序列化通常要增加2到10毫秒的开销。一次查询本来要调一个服务现在要串行调三个服务延迟可能从30毫秒涨到60毫秒。应对办法有几种。第一种是并行调用没有依赖关系的服务间调用改成并行发起。比如查订单详情需要用户信息和商品信息这两个调用没有依赖关系用CompletableFuture并发去调总耗时从两次串行变成一次最慢调用的时间。第二种是数据冗余把高频查询需要的数据冗余到调用方本地。比如订单服务经常需要查用户名如果每次都调用户服务压力很大。可以做一个用户基础信息的同步机制订单库里冗余一个user_name字段用户信息变更时通过消息通知异步更新。这个方案牺牲了一点一致性换来了查询性能的大幅提升在实践中非常实用。第三种是缓存前置。微服务调用链的每一层都可以加缓存商品信息、用户信息这类写少读多的数据优先缓存在Redis里。缓存命中率越高链路里被跳过的请求越多整体性能自然越好。4. 微服务化之后的可扩展性治理扩容、限流与链路追踪服务拆完了这并不代表工作结束反而是一系列新问题的开始。微服务架构最大的优势在于可扩展性但真正把扩展能力用起来需要配套的治理手段。4.1 从按需扩容到弹性伸缩单体时代扩容的粒度是一整台应用服务器全部模块一起扩。微服务化之后扩容粒度可以细化到单个服务比如大促前只给订单服务扩到20个实例消息服务保持3个实例资源利用率一下就不一样了。在容器化部署的场景下弹性伸缩可以做得更自动化。以Kubernetes为例通过HPAHorizontalPodAutoscaler配置CPU使用率阈值或自定义指标当服务实例的CPU使用率持续超过70%时自动扩容低于30%时自动缩容。我给订单服务设置过基于Sentinel上报的QPS指标自动扩容高峰时段实例数从5个自动涨到16个流量回落后自动缩回5个整个过程不需要人为干预。4.2 网关层的性能设计细节网关是所有请求的必经之路网关的性能和稳定性直接决定了整个系统的可用性。这里有几个容易被忽视的点一是网关的超时配置必须比下游服务短。网关如果设置30秒超时下游服务慢到30秒才返回用户的连接将会长时间占用加上负载均衡器也可能在等整个连接链条会被拖死。我通常把网关的全局超时设置在5秒所有路由的默认超时控制在3秒以内宁可快速失败让用户重试也不让连接堆积。二是网关要按业务重要性区分优先级。登录、下单这类核心接口的限流阈值要高一些报表导出、圈子动态这类低频接口阈值低一些。否则突发流量进来低频接口把网关线程占满核心接口反而被拒之门外。三是网关层要提前做优雅停机处理。发布新版本时先把网关中对应服务的权重调低等待存量请求处理完再摘除旧实例。这一步如果没做每次发布都会出现零星超时。4.3 分布式事务性能与一致性的艰难平衡微服务化之后原来单体里一个本地事务就能搞定的事情现在要跨多个服务。比如下单要扣库存、生成订单、扣减优惠券这三个操作分布在三个服务里怎么保证一致性强一致的方案是使用Seata这类分布式事务框架通过AT模式或者TCC模式实现全局事务。但分布式事务的代价是性能。Seata的AT模式需要全局锁每次操作都要和TC事务协调器通信延迟增加比较明显且全局锁会限制并发。我实测过一个下单接口引入Seata后链路延迟增加了大约25%到40%。所以我的建议是能用最终一致性解决就不要上强一致。大部分场景可以这样设计本地事务先完成然后通过可靠消息通知其他服务更新。比如下单时先扣减库存并生成本地订单记录然后发送一条“订单已创建”的消息优惠券服务消费消息异步核销优惠券。在这个模型里只要消息不丢、消费方有幂等和重试机制最终数据会达成一致。消息驱动的方案性能损失几乎可以忽略不计。4.4 链路追踪微服务排障的必备能力一次请求在微服务架构里要经过网关、用户服务、订单服务、商品服务甚至还会调用消息队列和缓存。任何一个环节慢了用户感知都是“页面卡了”但到底是哪个环节慢没有链路追踪工具根本说不清楚。SkyWalking是我用得比较顺手的方案Java探针方式接入业务代码基本零侵入。它可以看到一次请求在每个服务里的耗时分布哪一段慢、有没有调用报错、下游服务之间调用了几次全部一目了然。链路追踪本身也有性能开销。探针要拦截方法调用、生成Span、异步上报一般会带来5%到10%的额外负载。所以生产环境里采样率不需要设成100%比如高频接口采样率设成10%低频接口设成50%既能覆盖大多数问题排查场景又能把性能开销控制在2%到3%以内。4.5 微服务常见问题速查表问题现象可能原因排查思路接口偶发超时下游服务GC停顿、线程池排队查看下游服务GC日志、线程池活跃度确认是否有慢调用服务启动后注册成功但流量打不进来网关/负载均衡缓存了旧实例信息检查注册中心状态确认网关路由是否刷新必要时手动摘除旧实例限流误伤正常流量阈值设置过低或统计维度不准查看Sentinel的实时监控曲线按接口实际峰值调整阈值消息重复消费消费端没有做幂等增加业务唯一键消费前先查重保证重复消息不影响数据拆服务后查询变慢链路串行调用过多、缺少缓存分析链路追踪数据并行化无依赖调用增加Redis缓存数据库连接被打满连接池配置过大或慢SQL堆积优化连接池参数、定位慢SQL、引入读写分离5. 一些组织层面和长期演进的经验提醒架构设计不完全是技术问题我在这些年摸爬滚打下来的体会是微服务演进的成败一半取决于技术另一半取决于组织和工程习惯。5.1 先统一技术规范再动手拆微服务之前如果团队的代码风格、接口文档规范、异常处理方式都还是“各写各的”拆出来的每个服务就是一堆风格各异的黑盒。我强烈建议先制定统一的服务开发规范包括接口返回体结构、统一异常码、日志打印格式、链路追踪的Span命名规则。这些规范只要花一两周就能定下来但能避免未来每个服务各搞一套排查问题时像在猜谜。5.2 服务划分不是一步到位业务边界一开始不可能划得很完美。我的经验是第一版拆分先把明显独立的模块切出来大而复杂的核心域可以保持一个服务里多模块的结构等业务逻辑更清晰、团队对服务边界的理解更成熟之后再进一步拆分。硬要一步到位很容易拆出一堆互相紧密耦合的“微服务”每改一个接口都要跨多个服务同步发版维护成本反而更高。5.3 关于数据库和服务的关系微服务强调数据私有一个服务只操作自己的库表。但这在实际推进中会遇到很多阻力特别是当多个业务都依赖同一张底层数据表时。我的处理方式是允许读操作访问共享库但只允许数据归属方的服务做写操作后续逐步通过消息或数据冗余把共享依赖解耦掉。读共享是一个过渡策略没有它很多系统的拆分根本推不动但要心里有数它不该是终态。5.4 性能演进的终点不是微服务而是持续优化微服务解决了单体的扩展性问题但引入的分布式复杂度也会带来新的性能挑战。服务间网络开销、序列化开销、消息中间件堆积、分布式锁竞争这些都要持续监控和优化。我每个季度都会重新审视一次系统里耗时Top 10的接口链路看看有没有可以并行化的调用、有没有命中率下降的缓存、有没有可以被消息解耦掉的强同步逻辑。架构没有终态只有阶段性的合适。会不会设计、能不能演进靠的是对业务和流量的持续理解以及面对复杂度时的克制与清醒。6. 最后分享一点我自己的真实感受折腾完好几次从单体到微服务的演进之后我现在面对“要不要拆微服务”这个问题给出的答案都特别朴素先看业务规模、团队规模和资源预算。如果系统还年轻、业务还没验证那就让单体再跑一段把基础设施做扎实、把数据模型理清楚跑出真实瓶颈后再动手拆分。真到拆的那一天也记得保持克制。最稳的路子永远是一步一步来无状态化、拆基础能力、拆核心域、接流量治理、加可扩展的部署方案。每一步都验证、观察数据、稳定运行再走下一步。很多时候别人眼中的“慢”恰恰是整个演进过程不出大事故的关键。技术圈里永远不缺漂亮的新名词但架构设计真正考验人的地方是在日复一日地与存量系统、团队协作和线上流量的反复博弈里做出让系统能继续长大的选择。希望这篇从单体到微服务性能演进的梳理能给正在这个岔路口做决策的你一些可以带回去直接用的经验。
返回列表