分布式系统的常见问题
微服务之间通信常见问题 解决方案微服务通信主流两种方式同步调用HTTP/REST、gRPC、异步消息MQRocketMQ/RabbitMQ/Kafka两类通信各自的问题有重叠也有差异。一、网络层面最基础分布式固有问题网络抖动、超时、断连跨机器 / 跨机房调用网络不是永远稳定。调用超时是高频问题。问题服务 A 调用服务 BB 处理很慢或者网络卡住A 线程一直阻塞堆积请求线程耗尽雪崩。解决设置合理超时时间不要无限等待。网络延迟跨机房、跨地域调用RTT 增加同步链路变长接口整体耗时上升。方案本地缓存、接口聚合、减少链式调用。丢包、重试带来的幂等问题网络超时后客户端自动重试如果接口不幂等会重复创建订单、重复扣款。重点超时 ≠ 业务一定失败可能 B 已经执行成功只是响应丢了。解决接口设计成幂等接口唯一业务流水号。二、可靠性问题调用失败怎么办服务实例宕机、节点下线目标服务某个实例挂了如果负载均衡没有及时感知请求还打到故障实例 → 请求报错。解决注册中心Nacos/Eureka/Consul健康检查负载均衡摘除不健康节点。服务雪崩级联失败服务 B 变慢大量请求堆积在 A 的线程池A 线程耗尽A 自身也不可用并且继续向上传导整个链路瘫痪。对策熔断、降级、限流、舱壁隔离不同服务使用独立线程池Sentinel、Hystrix舱壁调用不同下游服务使用独立线程池防止一个下游拖垮全部线程。重试风暴下游短暂故障上游大量客户端同时不断重试瞬间流量放大压垮下游。解决指数退避 随机抖动 jitter不要固定间隔重试限制最大重试次数。三、接口契约 版本问题接口兼容性问题服务 B 升级接口删除字段、修改字段类型、修改入参上游 A 没同步直接报错。原则向后兼容新增字段不能删除旧字段字段类型不随便修改。工具OpenAPI/Swagger、gRPC proto 版本管理灰度发布。参数协议不一致、序列化问题JSON 序列化 / 反序列化、日期格式、BigDecimal 精度、空值处理、枚举不一致跨语言通信更容易踩坑。gRPC 使用 protobuf序列化体积更小类型强约束相比 REST 能减少这类问题。版本管理混乱多版本服务实例同时在线请求路由到错误版本。方案版本号放在请求头灰度路由、按标签路由。四、性能问题链式调用接口爆炸N1 调用A 调用 BB 再调用 CC 再调用 D链路很长。单次请求串联多次远程调用耗时叠加任何一环超时整体失败。经典反模式后端前端BFF 层缺失解决BFF 聚合接口并行调用CompletableFuture数据冗余减少远程查询。序列化 / 反序列化开销JSON 文本序列化开销大高频调用场景推荐 protobuf。连接开销短连接每次建立 TCP 握手性能差解决HTTP 连接池、gRPC 长连接。五、分布式事务 数据一致性异步 / 同步都会遇到同步调用部分成功部分失败A 调用 B 成功调用 C 失败B 的操作已经提交数据不一致。方案SAGA、TCC、本地消息表、事务消息根据业务选择最终一致性微服务很少用强一致 2PC。异步 MQ消息丢失、重复消费、消息顺序错乱、消息堆积消息丢失生产者没投递成功、Broker 宕机、消费者消费完还没执行业务就宕机重复消费网络超时重试投递 → 再次收到消息必须消费端做幂等顺序问题业务需要顺序时需要分区 / 单队列保证顺序但降低吞吐消息堆积消费速度跟不上生产MQ 磁盘打满六、可观测性问题调用链路难追踪一个请求跨多个服务出问题不知道在哪一环超时 / 报错。解决分布式链路追踪 SkyWalking / SleuthZipkin透传 traceId日志带上 traceId。日志分散各个服务日志独立排查问题需要去多个系统捞日志。统一日志平台 ELK。指标监控缺失看不到调用量、错误率、响应时间 P99不能提前发现问题。监控上报 Prometheus Grafana。七、安全问题服务之间未鉴权任意服务都可以调用内部接口网关只是对外鉴权服务内部接口之间没有身份校验。方案服务间令牌如 JWT、mTLS 双向 TLS 加密。敏感数据明文传输HTTP 明文数据被抓包。生产推荐 HTTPS/gRPC TLS。八、调用模式选型带来的坑选错通信方式是很多项目的根源问题同步HTTP/gRPC✅ 优点简单立刻拿到结果❌ 缺点强依赖下游可用性容易超时、雪崩阻塞线程适合短查询。异步 MQ✅ 解耦削峰下游故障不阻塞上游主流程❌ 缺点一致性复杂不能实时拿到返回结果要处理消息丢失 / 重复 / 堆积。经验查询类用同步通知、事件、异步任务优先 MQ下游异常导致 MQ 消息堆积上游如何感知 处理方案核心前提MQ 堆积本质是消费速度 生产速度。上游生产者本身正常发消息生产者默认是感知不到堆积的不能靠生产者自己发现。需要通过 MQ 服务侧指标、监控、告警来感知再做上游限流 / 保护。一、上游怎么感知消息堆积3 种感知途径1. MQ Broker 侧指标监控最主流MQRocketMQ / Kafka / RabbitMQ都会暴露核心指标接入 PrometheusGrafana队列消息堆积量未消费消息数消费延迟lag消费位点和生产位点差值Kafka 叫 consumer lagRocketMQ 有消费堆积条数消费 TPS、生产 TPS 对比生产 TPS 持续 消费 TPS堆积在持续上涨消费失败数、重试队列消息量下游消费报错大量消息进入重试队列也会造成堆积告警规则示例消费 lag 阈值比如 10 万 且 持续 5 分钟或者堆积量持续上涨触发告警。告警接收方运维 / 开发不是上游业务代码主动感知是监控系统感知2. 消费回执 / 回调机制业务层面感知可选适合业务事件场景下游消费完成后通过另一个 MQ/HTTP 回调通知上游 “处理完成”。上游维护待处理事件表记录消息和状态如果超过超时时间状态一直未更新判定下游消费异常、消息堆积 / 消费失败缺点增加业务复杂度不是通用方案适合重要业务事件不适合高吞吐普通消息。3. 生产者埋点 间接判断辅助手段不准生产者上报发送成功率。若 Broker 磁盘满、拒绝写入消息 → send 接口返回异常生产者立刻感知停止继续发消息⚠️ 注意普通堆积时 Broker 是可以正常接收消息的send 不会报错生产者完全无感。只有 Broker 资源耗尽才会拒绝生产这是堆积后期严重阶段。✅ 一句话总结感知消息堆积绝大多数场景上游业务代码无法实时自动感知依赖 MQ 的 lag 指标做监控告警是标准方案。二、感知到堆积后上游系统怎么处理分层策略原则保护上游核心业务不被拖垮同时防止堆积继续恶化区分两种情况① 下游短暂抖动很快恢复② 下游故障长时间不可用。1. 流量控制上游限流最常用当监控检测 lag 持续上涨对上游生产者做限流方式 1网关 / 上游接口限流降低生产消息的速率限制触发 MQ 发消息的请求量方式 2生产者本地令牌桶 / 漏桶控制单位时间发送消息数量目的减慢消息产生速度不让队列继续暴涨给下游留恢复时间注意不要直接阻断所有流量按业务分级非核心业务直接降流量核心业务保留少量配额。2. 业务降级暂停非核心事件投递业务分层核心事件下单、非核心事件日志、通知、统计。堆积告警触发上游停止发送非核心消息只保留核心业务消息投递。等消费 lag 回落再恢复非核心消息。3. 消息分流 / 路由高级故障期间非核心消息路由到备用队列后续低峰期再消费核心消息保留在主队列优先消费。4. 上游本地缓存 / 暂存谨慎使用上游收到业务请求本该发 MQ现在 MQ 堆积严重把事件持久化到上游本地 DB等 MQ 恢复后再后台补推消息。⚠️ 风险上游服务宕机会丢失本地暂存数据需要补偿任务适合对消息不丢要求高场景。❌ 禁止方案上游自动重试疯狂发消息会加剧堆积。5. 极端情况熔断停止生产消息下游长时间故障堆积持续飙升Broker 磁盘告警。上游触发熔断停止向 MQ 投递消息同时给前端返回友好提示。适用非强核心链路强核心业务不能直接断要本地持久化保存事件。三、配套下游侧处理上游配合联动感知堆积后除了控上游流量还要处理下游扩容消费者实例提升消费能力优先操作死信处理消费失败消息转入死信队列避免一直重试占用消费能力清理无效消息过期可丢弃的消息设置消息过期时间减少无效堆积四、容易踩坑的点❌ 指望生产者代码自动检测 lag生产者没有权限、也不应该轮询 MQ 查 lag高并发会压垮 Broker交给独立监控系统❌ 堆积了还不限流持续疯狂生产 → Broker 磁盘占满整个 MQ 集群不可写所有业务都受影响❌ 限流一刀切把核心业务也限制住❌ 消息无过期时间堆积消息永久保存在队列磁盘持续上涨分布式锁 重点关注问题分布式锁目的跨多个 JVM / 微服务保证同一时刻只有一个业务节点执行临界区代码。常见实现Redis、Zookeeper、数据库。下面是通用核心问题不分实现。1. 锁超时 业务执行时间 锁过期最经典坑场景节点 A 拿到锁设置过期时间 30s业务执行超过 30s锁自动过期释放节点 B 拿到同一把锁A 还在执行业务 →两个节点同时执行业务并发安全失效。解决思路续期机制看门狗 watch dogRedisson 的 watchdog定时检查业务没执行完就自动延长锁过期时间。不要单纯依靠锁过期来释放锁业务正常完成主动释放锁。评估业务最大执行时长设置合理 TTL长任务慎用简单 Redis 锁。注意看门狗也有上限如果服务宕机续期线程挂掉锁最终会自动释放防止死锁。2. 锁释放必须是【持有者才能释放】防止误删别人的锁坑A 拿到锁TTL 快到期A 业务阻塞锁过期B 拿到锁A 恢复执行执行del key把 B 的锁删掉。✅ 解决方案锁 value 存唯一标识UUID / 线程 ID释放锁时先判断 value 等于自己的标识再删除。Redis 不能两条命令分开执行判断 del 非原子要用Lua 脚本保证原子性。-- Redis释放锁lua脚本 if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end3. 死锁问题场景拿到锁的节点宕机没有执行释放锁代码 → 锁永远不释放其他节点永远拿不到锁。解决锁必须设置过期时间 TTL。ZK 临时节点天然自带会话断开节点自动删除不需要 TTL但 ZK 性能更低。4. 锁的可重入性同一个线程已经持有锁再次请求加锁是否能成功不可重入锁再次加锁直接阻塞 / 失败容易死锁。Redisson 支持可重入锁计数原生 Redis 自己实现简易锁很容易忽略可重入。业务如果有嵌套调用加锁逻辑必须支持可重入。5. 锁的公平性可选看业务非公平锁多个客户端抢锁谁抢到算谁可能出现饥饿某个节点一直抢不到性能高默认。公平锁按请求排队顺序获取锁Redis/ZK 都能实现性能差一些。大部分业务不需要公平锁。6. 集群模式下锁的安全性问题Redis 重点经典问题Redis 主从异步复制主节点拿到锁还没同步到从库主宕机从晋升为主。新主没有锁数据其他节点又拿到锁锁失效并发问题。普通 Redis 主从锁不保证强一致有安全漏洞。方案RedlockRedis 红锁多独立实例加锁过半成功才算拿到锁运维复杂性能差很多场景不推荐使用 ZK 做分布式锁ZAB 强一致无这个问题性能偏低Redis 单实例 业务接受极小概率风险很多业务选择这个比如后台任务7. 锁粒度问题性能锁粒度太大整个大业务加锁并发低吞吐量差。锁粒度太小锁太多维护复杂容易产生死锁。原则临界区代码尽量短只锁住必要操作。8. 锁等待、阻塞、自旋问题抢不到锁怎么处理自旋不断重试空转消耗 CPU高并发下 CPU 飙升要设置最大自旋次数 间隔。直接返回失败业务需要自己处理失败稍后重试 / 拒绝请求。阻塞等待ZK 可以 watch 监听Redis 原生没有阻塞。坑大量客户端同时自旋抢锁 → 惊群效应。9. 锁的可靠性区分「锁成功」和「业务成功」拿到锁 ≠ 业务一定执行成功。业务抛出异常一定要 finally 里释放锁⚠️ 注意释放锁 Lua 脚本也要执行防止异常导致锁不释放。10. 锁与事务的顺序非常容易踩坑❌ 错误先开数据库事务再获取分布式锁✅ 正确先获取分布式锁再开启 DB 事务事务提交后再释放分布式锁原因事务还没提交锁提前释放别的线程读到未提交数据脏读。11. 区分分布式锁 vs 本地锁本地锁 synchronized/Lock 只能锁 JVM 内多实例环境无效。分布式锁解决跨 JVM。三种实现方案优缺点对比Redis 分布式锁优点性能高实现简单缺点主从切换有锁失效风险需要处理 TTL、续期、Lua 释放Zookeeper 分布式锁优点强一致临时节点自动释放天然防死锁支持 watch不用关心 TTL缺点性能弱运维重网络抖动会频繁断会话释放锁数据库分布式锁乐观锁 / 悲观锁悲观锁select for updateDB 连接容易占用数据库压力大乐观锁版本号无锁等待适合读多写少缺点DB 压力大高并发容易失败。