微服务架构把一个庞大的系统拆成了几十甚至上百个独立的小服务。每个服务由不同的团队开发,部署在不同的机器上,独立演进、独立发布。
但有一个问题始终绕不开:服务之间需要互相调用。订单服务要调用用户服务查信息,支付服务要调用订单服务改状态,优惠券服务可能要同时调用订单服务和用户服务。
这就引出了微服务架构中最核心的挑战之一:服务间的依赖如何管理?依赖管理不好,轻则调用失败,重则整个系统雪崩。
本文从服务依赖的几种形态、依赖管理的具体手段、以及常见问题的应对方案三个维度,梳理服务间依赖管理的完整思路。
一、服务依赖的几种形态
服务之间的依赖关系不是扁平的,理解不同形态有助于针对性管理。
强依赖
服务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访问数据,而不是直连数据库。
四、总结
服务间的依赖管理是微服务架构中最复杂的课题之一,其核心可以归纳为几个关键原则:
设超时是为了不干等,做重试是为应对瞬时故障,加熔断是为避免反复调用已经挂了的东西,做降级是为提供兜底响应,舱壁隔离是为了让故障被控制在小范围内,幂等是为了应对重试带来的副作用。
这些手段组合起来,核心目标只有一个:当一个服务出问题时,问题被控制在局部,不会像多米诺骨牌一样推倒整个系统。这就是服务间依赖管理最本质的价值所在。