微服务不是「把单体拆开部署」就完事了,真正的成本在治理:服务怎么找到彼此、配置怎么下发、一个服务挂了怎么不拖垮一片。这篇按这个顺序整理。
一、整体结构
二、注册中心:Nacos
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 10.0.0.10:8848
namespace: prod
group: DEFAULT_GROUP
heart-beat-interval: 5000 # 心跳间隔
config:
server-addr: 10.0.0.10:8848
file-extension: yml
namespace: prod
refresh-enabled: true
配置动态刷新:类上加 @RefreshScope,或者用 @ConfigurationProperties + @EnableConfigurationProperties(推荐后者,不需要 @RefreshScope)。
踩过的坑:
- namespace 和 group 不一致是最常见的「服务找不到」原因 —— 两个服务在不同 namespace 下互不可见,控制台能看到实例但调不通。
- 临时实例 vs 持久实例:默认临时实例靠心跳,服务正常关闭会自动下线;持久实例需要手动下线,灰度发布时容易残留。
- Nacos 2.x 多了 gRPC 端口(9848),除了 8848 还要放行 9848/9849,否则客户端能连上但注册失败。这是升级 2.x 最常见的网络问题。
三、网关:Gateway
spring:
cloud:
gateway:
routes:
- id: order
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒令牌数
redis-rate-limiter.burstCapacity: 200
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
要点:
lb://走注册中心负载均衡;StripPrefix=1剥掉前缀。- 限流用 Redis 令牌桶(
RequestRateLimiter+spring-boot-starter-data-redis-reactive)。 - 跨域配置不要重复:网关配了 CORS,下游服务就别再配一次,否则响应头重复浏览器直接报错。
- 网关不要写业务逻辑,只做路由、鉴权、限流、日志。业务逻辑下沉到服务。
四、服务调用:OpenFeign
@FeignClient(name = "user-service", path = "/user",
fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/{id}")
Result<User> get(@PathVariable Long id);
}
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 2000 # ★ 连接超时一定要设,默认很长
readTimeout: 5000
loggerLevel: basic
Feign 的必配置项:
- 超时必须显式设置。默认连接超时 10s、读超时 60s,下游一慢就把上游线程全占住。
- 重试要谨慎:默认不重试,如果开了,务必确认下游接口幂等,否则重复下单。
- 日志级别按需开:
full会打印请求体和响应体,生产上别开(性能和隐私)。 - 熔断降级用
fallbackFactory:比fallback多拿到异常对象,能区分是超时还是业务异常。
五、熔断限流:Sentinel
| 能力 | 配置 | 说明 |
|---|---|---|
| 限流(QPS/线程数) | 控制台规则 | 超了直接失败(快速失败)或排队(匀速排队) |
| 熔断(慢调用比例) | RT + 比例阈值 | 下游变慢时自动断开,避免拖垮上游 |
| 熔断(异常比例/数) | 阈值 + 时间窗 | 连续报错就熔断 |
| 系统自适应 | CPU / Load / RT | 整机保护,最后一道防线 |
熔断的核心价值是快速失败 + 自动恢复:下游不可用时,与其让请求全部卡在超时上,不如立刻返回,留出资源给能成功的请求。
六、链路追踪
没有 TraceId 的微服务,排查问题就是灾难。做法:
- 网关生成
TraceId(或用 Micrometer Tracing / SkyWalking 自动埋点),放进 MDC。 - Feign 调用时用拦截器把
TraceId写到请求头传递。 - 异步/线程池场景用
TaskDecorator传递 MDC(前面 Spring Boot 那篇提过)。 - MQ 场景把
TraceId放进消息头,消费端取出写回 MDC。 - 日志格式里带
%X{traceId},ELK 里就能一次搜出整条链路。
// Feign 传递
@Bean
public RequestInterceptor traceInterceptor() {
return template -> template.header("X-Trace-Id", MDC.get("traceId"));
}
七、生产复盘:一次由慢 SQL 引发的雪崩
脱敏自某营销 SCRM 中台的一次事故。
现象:晚上 8 点营销活动开始,几分钟内多个服务接连告警,最后整个后台不可用。
链路还原(靠链路追踪看出来的):
- 活动触发人群筛选,一个慢 SQL(没走索引)让「人群服务」RT 从 50ms 涨到 5s。
- 上游「营销引擎」用 Feign 调用人群服务,超时 5s,线程池迅速被占满。
- 营销引擎不可用 → 网关大量请求堆积 → 网关线程池也满。
- 最后整个后台打不开 —— 一个 SQL 拖垮全站。
根因:没有任何一层做了资源隔离和快速失败。Feign 超时没设、线程池共用、没有熔断。
改造清单:
- 所有 Feign 调用统一设置超时(连接 2s / 读 5s),并按下游服务隔离线程池。
- 接入 Sentinel 熔断,慢调用比例超过 50% 自动断开。
- 慢 SQL 加索引 + 大人群走离线任务,不再实时查。
- 网关层加 QPS 限流,保护后端。
- 核心链路做降级预案:人群服务不可用时,退化成「全量人群 + 异步补偿」,而不是直接失败。
结论:微服务的稳定性 = 超时 + 隔离 + 熔断 + 降级 四件事。缺一件,故障就会沿着调用链一路放大。
八、拆分粒度的建议
- 按业务能力拆,不按技术分层拆。拆成「订单服务/用户服务」合理,拆成「controller 服务/dao 服务」是灾难。
- 先粗后细:拿不准就先放一起,等业务边界清楚了再拆。拆错的代价远大于晚拆。
- 共享库是隐形耦合:两个服务共用一个数据库,等于没拆。数据归属要清晰,跨服务只能通过接口。
- 分布式事务能避免就避免:优先用「本地事务 + MQ 最终一致」,实在不行再上 Seata。
一起聊聊