引言:
在上一篇文章中我们一直在说微服务的好,可是我们也说了微服务不是银弹,它也有坏的地方,那究竟坏在哪儿,今天我们一起来看看。
微服务带来的挑战
到这里,如果你以为微服务就是"把大系统拆成小系统,一切变美好",那就大错特错了。微服务解决了一类问题,但引入了另一类更棘手的问题。有人说"微服务是分布式系统带来的复杂性",这句话说得非常精准。
下面我们来逐一剖析这些挑战。
服务发现的难题
在单体架构中,服务调用是方法调用------A模块调用B模块,直接import就行,B在内存里等着呢。
在微服务架构中,A服务要调用B服务,首先要知道B在哪里。但是B可能部署在10台不同的机器上,而且这些机器可能动态变化------扩容了、缩容了、某个实例挂了又被重启了。
问题来了:A服务怎么知道B服务的所有实例地址?可能你会说可以使用httpclient直接发请求,确实毛病,但是如果这个地址变了呢?我们就需要手动修改代码,这肯定不行。
这就引入了服务发现的需求。
解决方案通常有两种模式:
客户端发现模式:A服务直接从注册中心(比如Eureka、Consul、Nacos)拉取B服务的实例列表,然后自己选择调用哪个。
服务端发现模式:A服务把请求发给一个负载均衡器(比如Nginx、Kubernetes Service),由负载均衡器去查询注册中心并转发请求。
但无论哪种模式,都引入了一个新的组件------服务注册中心。这个组件本身必须高可用,如果注册中心挂了,整个系统可能就瘫痪了。这相当于把原来"不存在"的问题变成了一个需要专门维护的系统组件。
网络通信不可靠
单体架构中,方法调用是在同一个进程内,几乎是瞬时完成,而且不会失败(除非进程崩溃)。
微服务中,调用变成了跨网络的远程调用。网络是天生不可靠的:
网络延迟可能比想象的大
网络可能丢包
对端服务可能暂时不可用
对端服务可能处理超时
这些在分布式系统中被称为部分故障。部分故障是最棘手的,因为整个系统没有完全挂,但某些节点出了问题,你无法确定问题是暂时的还是永久的。
解决方案:引入了各种容错模式:
超时控制:每次远程调用必须设置超时时间,不能无限等待
重试机制:遇到可恢复的错误,自动重试(但要小心幂等性)
断路器模式:当某个服务错误率超过阈值,自动"熔断",快速返回降级响应,避免连锁故障
舱壁隔离:不同服务的线程池隔离,一个服务的故障不会耗尽所有资源
降级:当依赖的服务不可用时,提供备选方案(比如返回缓存数据)
这些容错逻辑在单体架构中几乎不需要考虑,但在微服务中成了必修课。
分布式数据管理
这可能是我认为微服务中最难的问题。
在单体架构中,事务是简单的------多个表的更新在同一个数据库事务中完成,要么全部成功,要么全部回滚,ACID特性由数据库保证。
在微服务中,订单服务和库存服务使用各自的数据库。一个下单操作需要:
-
订单服务创建订单(在自己的数据库里)
-
库存服务扣减库存(在自己的数据库里)
-
支付服务发起支付(调用第三方)
这三个操作跨三个服务、跨三个数据库,如果第2步失败了,第1步怎么回滚?如果第3步超时了但实际支付成功了,怎么保证数据一致?
传统数据库事务已经无能为力了。
这就引入了分布式系统中的一个经典问题:分布式事务。
解决方案路径一:强一致性
使用两阶段提交(2PC) 或三阶段提交(3PC),或者使用支持分布式事务的框架(如Seata的AT模式)。这些方案试图在分布式环境下模拟ACID事务。
但代价是什么?性能急剧下降、可用性降低(因为要锁定资源)、实现复杂。
解决方案路径二:最终一致性
这是微服务社区更推崇的方向------放弃强一致性,接受最终一致性。
核心思想是:不要求数据在任何时刻都一致,但保证在某一时刻之后数据会达成一致。
具体实现方式有:
MQ事务:订单服务创建订单后,发送一个"订单已创建"半消息,如果本地事务成功,那消费者就可以消费这条消息,如果失败那直接当没收到这条消息,rocketmq就可以做到。
消息队列+本地事务表:订单服务在本地事务中同时写入业务数据和待发送消息,然后异步发送消息。库存服务消费消息进行处理。
Saga模式:将长事务拆分为一系列本地事务,每个步骤都有对应的补偿操作。如果某一步失败,执行所有已成功步骤的补偿操作。
最终一致性带来了显著的复杂性:需要处理消息的可靠投递、幂等消费、重试和死信等。这也是使用消息队列必须解决的问题,但从实践来看,这是微服务架构下处理数据一致性的主流方式。
分布式事务的CAP困局
说到分布式数据,必须要提CAP定理。这个定理是分布式系统的"基本法则":
C(Consistency,一致性):所有节点在同一时刻看到相同的数据
A(Availability,可用性):每个请求都能收到响应(但不保证数据最新)
P(Partition tolerance,分区容忍性):系统中部分节点之间通信失败时,系统仍能正常工作
CAP定理的核心结论是:在分布式系统中,当网络分区发生时,你必须在一致性和可用性之间二选一。
对微服务系统来说,网络分区是必然会发生的(网络不可能100%可靠),所以你必须在C和A之间做权衡。
-
选择CP(一致性和分区容忍):当分区发生时,系统拒绝服务以保证数据一致。ZooKeeper、Eureka的某些模式属于这类。
-
选择AP(可用性和分区容忍):当分区发生时,系统继续提供服务,但可能返回旧数据。Cassandra、Riak属于这类。
而更实际的场景是,对于不同的业务,我们可以做不同的选择:
支付、库存扣减等对一致性要求高的场景,倾向于CP
商品浏览、评论查询等对可用性要求高的场景,倾向于AP
这就是微服务架构带来的又一个复杂性维度------你需要在不同服务中做出不同的权衡,而不能再像单体数据库那样简单依赖ACID,而是需要我们自己设计好方案。
服务调用的依赖链与级联故障
单体架构中,调用链是线性的:A→B→C,如果C挂了,A和B能感知到错误,但影响范围可控。
微服务中,调用链可能非常深:服务A调用B,B调用C,C调用D,D调用E......任何一个服务出问题,上游所有服务都会受到影响。
更可怕的是级联故障------服务D响应变慢,导致服务C的线程池被占满,C变得不可用,进而导致B的线程池被占满,最终整个链路雪崩。
这就是前面提到的断路器、舱壁隔离、超时和限流等机制要解决的问题。
另外,复杂的调用链也给问题定位带来了巨大挑战。一个请求失败,可能是链路上任何一个服务出问题,也可能是网络问题,也可能是某个服务的依赖第三方出了问题。你需要能够在成千上万的调用中快速定位故障节点。
这催生了分布式链路追踪 (如Jaeger、Zipkin,skywalking)和可观测性体系(日志、指标、追踪三大支柱)的蓬勃发展。
运维的复杂性
部署一个微服务很简单,但部署50个甚至200个微服务呢?
每个服务有自己的配置、有自己的依赖环境、需要监控自己的健康状态、需要收集自己的日志......
在Kubernetes等容器编排技术成熟之前,运维微服务是一件极其痛苦的事情。即便有了K8s,你依然需要处理:
配置管理:不同环境(开发、测试、生产)、不同服务的配置如何管理?
日志聚合:一个请求跨越多个服务,日志散落在不同地方,如何关联?
监控告警:几十上百个服务的CPU、内存、QPS、错误率如何统一监控?
发布策略:灰度发布、金丝雀发布、蓝绿部署等如何实施?
这些催生了一整套云原生技术生态:Kubernetes、Prometheus、Grafana、ELK、Istio等等。
服务边界划分的难题
这是个更加"软性"但同样致命的问题:如何正确拆分微服务?
如果拆得太粗,还是像单体,发挥不出微服务的优势。
如果拆得太细,服务之间高频通信,网络开销巨大,系统变得脆弱复杂。
拆分边界如果设计错误,会导致:
频繁的跨服务调用(性能差)
分布式事务泛滥(数据一致性难)
改一个功能需要改多个服务(发布耦合)
领域驱动设计(DDD)中的限界上下文 概念,是解决这个问题的重要工具。简单来说,就是找出业务中的天然边界------哪些功能经常一起变化,哪些数据有强一致性要求,哪些功能是独立的业务能力------把这些作为拆分的依据。
但这没有标准答案,需要架构师对业务有深刻理解,并且在演进过程中不断调整。
微服务与分布式系统的关系
到这里,我们其实已经触及了微服务和分布式系统的深层次关系。
微服务本质上是分布式系统的一种具体实现形态。
分布式系统是一个更宽泛的概念------只要多个计算机通过网络协作完成共同目标,就是分布式系统。Hadoop集群是分布式系统,数据库集群也是分布式系统。
而微服务是一种面向业务能力的分布式系统,它强调的是:
-
每个节点(服务)拥有独立的业务语义
-
每个节点可以独立演进
-
节点之间通过业务API通信,而不是底层RPC框架
换句话说,微服务 = 面向业务的分布式架构 + 一系列工程实践。
理解这一点很重要,因为它意味着:所有分布式系统的经典问题,微服务都有;而微服务还额外带来了业务边界划分、团队组织架构等更深层的问题。
反过来看,学习微服务的过程,本质上就是在学习如何在分布式环境下构建和管理一个业务系统。这也是为什么微服务的学习曲线如此陡峭------你不仅要学业务设计,还要学网络、容错、数据一致性、运维等一整套分布式系统知识。
微服务技术生态全景图
为了让你对微服务有一个全局视野,我整理一下当前微服务技术生态的主要组成部分:
| 领域 | 解决的问题 | 代表技术 |
|---|---|---|
| 服务注册与发现 | 服务实例的动态感知 | Nacos、Consul、Eureka、ZooKeeper |
| API网关 | 统一入口、路由、认证、限流 | Spring Cloud Gateway、Kong、Nginx |
| 配置管理 | 分布式配置的动态更新 | Apollo、Nacos Config、Spring Cloud Config |
| 远程调用 | 服务间通信 | Dubbo、gRPC、OpenFeign |
| 负载均衡 | 客户端侧的负载策略 | Ribbon、Spring Cloud LoadBalancer |
| 断路器/容错 | 故障隔离和降级 | Resilience4j、Hystrix、Sentinel |
| 分布式事务 | 跨服务数据一致性 | Seata、TCC、Saga |
| 消息队列 | 异步解耦、最终一致性 | RocketMQ、Kafka、RabbitMQ |
| 分布式链路追踪 | 请求链路跟踪 | Jaeger、Zipkin、SkyWalking |
| 可观测性 | 指标监控和告警 | Prometheus+Grafana、Micrometer |
| 日志聚合 | 集中日志管理 | ELK Stack、Loki |
| 容器编排 | 微服务的部署和管理 | Kubernetes、Docker Swarm |
| 服务网格 | 基础设施层下沉 | Istio、Linkerd |
| CI/CD | 持续集成和持续部署 | Jenkins、GitLab CI、ArgoCD |
这不是一个简单的技术列表,而是一个相互关联的生态系统。每项技术都在解决前面提到的某个具体挑战。
总结与思考
写到这里,我想做个总结。
微服务不是凭空产生的新概念,它是对单体架构困境的回应。它通过将系统拆分为多个可独立部署的服务,换取了灵活性、可扩展性和团队效率,但代价是必须面对分布式系统的全部复杂性。
理解微服务的核心脉络可以用一句话贯穿:从单体到微服务,本质上是把"整体复杂性"拆解成了"个体简单性 + 连接复杂性"。
个体的简单性:每个服务代码量小、职责清晰、容易理解
连接的复杂性:服务之间如何发现、通信、容错、保证数据一致
微服务成功与否,关键在于是否有一套成熟的基础设施和团队能力来支撑这个"连接复杂性"。
对于学习路径,我自己也在摸索当中不过ai给我的建议是:
-
先理解问题,再理解方案。不要一上来就学Spring Cloud或者Kubernetes,先把"为什么要这么设计"想清楚。
-
从一个小而完整的项目入手。实际编码会让你对"服务间调用"、"容错处理"、"分布式事务"有切身的体会,那种踩坑的体验是看文档无法替代的。
-
带着问题去学技术。比如学到断路器,先想想"如果没有断路器,系统会怎样?"再去了解Hystrix或Sentinel的实现原理。
-
保持演进思维。架构不是一次性设计出来的,而是随着业务发展不断演进的。不要追求一开始就拆出完美的微服务,允许自己从单体起步,逐步拆分,是更务实的做法。
下一篇,我会结合我的微服务项目,深入到代码和架构层面,把今天这些概念一个个落地------看看服务注册发现是怎么配的,分布式事务是怎么处理的,链路追踪是怎么做的。到那个时候,这些抽象的概念就会变得具体起来。
同时后续我还会写学习从零搭建一个agent的学习过程和收获,欢迎关注我与我一起探讨。