服务间的依赖管理:微服务的协作之道

微服务架构把一个庞大的系统拆成了几十甚至上百个独立的小服务。每个服务由不同的团队开发,部署在不同的机器上,独立演进、独立发布。

但有一个问题始终绕不开:服务之间需要互相调用。订单服务要调用用户服务查信息,支付服务要调用订单服务改状态,优惠券服务可能要同时调用订单服务和用户服务。

这就引出了微服务架构中最核心的挑战之一:服务间的依赖如何管理?依赖管理不好,轻则调用失败,重则整个系统雪崩。

本文从服务依赖的几种形态、依赖管理的具体手段、以及常见问题的应对方案三个维度,梳理服务间依赖管理的完整思路。


一、服务依赖的几种形态

服务之间的依赖关系不是扁平的,理解不同形态有助于针对性管理。

强依赖

服务A调用服务B,如果B不可用,A的核心功能就无法完成。比如下单接口必须调用库存服务扣减库存,库存服务挂了,订单就没法下。

强依赖意味着被调用方直接决定了调用方的可用性。应对策略是尽量压缩强依赖的数量,或者对被依赖的服务做高可用保障。

弱依赖

服务A调用服务B,但如果B不可用,A可以降级处理,核心功能不受影响。比如下单后发送短信通知,短信服务挂了,订单照样能下,只是用户收不到短信。

弱依赖适合用异步、熔断、降级等方式隔离故障。

循环依赖

服务A调用服务B,服务B又调用服务A。这在单体中是设计问题,在微服务中更是大忌。循环依赖会让服务之间紧紧耦合,部署和调试都变得复杂,也容易引发连锁故障。

解决循环依赖通常需要重新审视职责划分,或者引入消息队列解耦。

传递依赖

服务A调用B,B调用C,A间接依赖于C。这种隐性的依赖关系容易出问题------A的开发团队可能根本不知道C的存在,但C一旦出问题,A也会受影响。

应对传递依赖需要在调用链路中引入超时控制、熔断机制,避免底层故障向上传播。


二、依赖管理的核心手段

2.1 服务发现:别把地址写死

最基础的依赖管理问题是:服务A怎么知道服务B在哪台机器上?

传统做法是把B的地址写死在A的配置文件里。但B会扩容、会迁移、会有机器宕机,写死的地址很快就失效了。

服务发现解决的就是这个问题。B启动时向注册中心报告自己的地址,A调用B时去注册中心查询B的当前地址。这样B的机器变化对A完全透明。

注册中心还承担了健康检查的职责,定期检查B的各个实例是否活着,挂了就从列表中剔除。

2.2 负载均衡:别逮着一台调用

服务B通常部署了多个实例。A调用B时,应该选哪一个?每次都选第一个,第一台压力过大,其他的闲置。

负载均衡就是解决这个分配问题的。轮询、最少连接、IP哈希等算法各有适用场景。关键在于两点:一是不要有单点过热,二是当某个实例出问题时能自动避开。

2.3 超时控制:别傻等

网络调用最大的不确定性是时间。服务B可能正常返回,也可能慢得离谱。

如果不设置超时,A的线程会一直等着B响应。B卡住了,A的线程也跟着卡住,慢慢把A的线程池占满,导致A自己也处理不了新请求。

超时控制的本质是给每次调用设定一个等待上限。超过这个时间就当调用失败,释放线程资源,避免被拖垮。

2.4 重试机制:失败后自动重来

网络调用经常出现瞬时故障:网络抖了一下、服务B正在重启、机器负载瞬间飙高。这些故障往往几秒钟后自动恢复。

重试机制的作用就是在失败后自动再试一次或两次。但重试需要谨慎------如果故障不是瞬时的,重试只会加重系统负担。另外,写操作的重试要确保幂等,否则可能造成重复处理。

2.5 熔断器:别反复调用已经挂了的东西

熔断器的设计思路来自电路保护。当服务B连续失败次数达到阈值,熔断器就自动打开。后续所有调用直接失败,不再实际发起请求,省得浪费时间。过一段时间放少量请求试探,如果B恢复正常,就关闭熔断器;如果还失败,继续保持打开状态。

熔断的价值在于:不要在明知对方已经挂了的情况下,还一遍遍地浪费资源去尝试。

2.6 降级:事情办不成总得有兜底

降级是熔断后的后续动作。调用失败时,不能就这么把异常抛给用户。

常见的降级方案有:返回缓存数据(上次查询的结果)、返回默认值(比如库存充足)、返回简化版本(只显示核心信息)、直接提示稍后重试。

降级的核心思路是:虽然完整的功能做不了,但至少要体面地告知用户,而不是直接抛个500错误。

2.7 舱壁隔离:别让一个故障传染全部

舱壁隔离的名字来自轮船设计------船体被分成多个隔舱,一个舱进水不会影响其他舱。

在微服务中,舱壁隔离的意思是:调用不同下游服务应该使用不同的线程池。假设服务A同时调用服务B和服务C,如果B的线程池出了问题,至少C的线程池还能正常工作。

没有舱壁隔离的情况下,所有下游调用共用同一个线程池,一个下游出问题可能导致整个服务瘫痪。

2.8 幂等性:防止重复操作

网络调用常常有重试,重试则必然带来一个风险:同一个请求被处理了多次。比如支付接口重试了两次,用户被扣了两次钱。

幂等性的要求是:同一个操作执行一次和执行多次的效果相同。实现方式通常有两种:一是基于业务唯一标识(比如订单号),服务端根据标识判断这个请求是否处理过;二是基于状态机,只有处于特定状态时才处理。


三、常见问题与应对

服务A调用B,B调用C,C调用......链路越来越长

链路越长,出问题的概率越高,响应时间也越久。应对方法包括:控制调用深度不超过3层、能用并行调用的不用串行、非核心链路改成异步。

B升级了一个接口,A没来得及改,怎么办?

引入版本号机制是通用做法。常见的两种方案:一是在接口路径里带版本,A继续调用旧版本,A升级完再切到新版本;二是在请求参数中带版本号,服务端同时兼容新旧两个版本。

核心原则是:接口变更要做到向前兼容,给调用方留出升级时间。

多个服务共享同一个数据库,依赖变紧怎么办?

微服务的一个核心原则是每个服务拥有自己的数据库。共享数据库会让服务在数据层面紧紧耦合,改表结构要通知所有相关服务。

正确的方向是逐步拆分数据库,或者通过API访问数据,而不是直连数据库。


四、总结

服务间的依赖管理是微服务架构中最复杂的课题之一,其核心可以归纳为几个关键原则:

设超时是为了不干等,做重试是为应对瞬时故障,加熔断是为避免反复调用已经挂了的东西,做降级是为提供兜底响应,舱壁隔离是为了让故障被控制在小范围内,幂等是为了应对重试带来的副作用。

这些手段组合起来,核心目标只有一个:当一个服务出问题时,问题被控制在局部,不会像多米诺骨牌一样推倒整个系统。这就是服务间依赖管理最本质的价值所在。

相关推荐
invicinble2 小时前
Spring如何把bean注册到容器里
java·后端·spring
代码不加糖2 小时前
0基础搭建前后端分离项目:实现菜单与界面左右布局
java·前端·javascript·mysql·elementui·mybatis
希望永不加班2 小时前
SpringBoot 敏感数据脱敏(序列化层)
java·spring boot·后端·spring
希望永不加班2 小时前
SpringBoot 数据库索引优化:慢查询分析
java·数据库·spring boot·后端·spring
胡利光3 小时前
Harness Engineering 02|Repo Harness:让仓库对 Agent 可读
java·junit·单元测试
langsiming3 小时前
【无标题】
java·开发语言·数据库
weisian1513 小时前
Java并发编程--45-分布式一致性协议入门:Raft、Paxos与ZAB的核心思想
java·分布式·raft·paxos·zab
木井巳3 小时前
【递归算法】解数独
java·算法·leetcode·决策树·深度优先·剪枝
t***5443 小时前
如何在 Dev-C++ 中切换编译器
java·开发语言·c++