Spring Cloud Alibaba 上生产,我踩过的坑和熬过的夜
上周五晚上十一点半,我正窝在工位上调试一个 Nacos 配置推送失败的问题,手机突然弹出一条微信——我妈发来的:“又加班啊?公务员考试复习得咋样了?”
我苦笑了一下,默默回了个“快了快了”,然后继续盯着控制台里那行 com.alibaba.nacos.api.exception.NacosException: failed to req API。
是的,我是个在职程序员,干这行快两年了,白天写业务代码,晚上刷行测题。组里用的是 Spring Cloud Alibaba(SCA)全家桶,说是为了“拥抱国产微服务生态”,其实更多是因为领导看阿里云文档写得比 Spring 官方还全(笑)。
最近我们系统刚经历了一次大促压测,从注册中心到配置中心,再到熔断限流,几乎每个组件都给我上了一课。今天就想掏心窝子聊聊 SCA 在真实生产环境里那些书本不会教、但线上会要命的细节。
为什么选 SCA?不是因为爱国,是因为省事
说实话,一开始我对 SCA 是有偏见的。之前用过 Spring Cloud Netflix(Eureka + Hystrix + Zuul),虽然 Netflix 停更了,但好歹文档多、社区熟。结果去年 Q3,公司搞技术栈统一,运维老哥一句话:“以后所有新项目必须用阿里系中间件,对接我们自建的 Nacos 和 Sentinel 集群。”
我翻了翻《Spring Cloud 微服务实战》《Spring Cloud Alibaba 企业级应用开发》,发现书里讲的都是理想情况:服务注册秒上线、配置热更新无延迟、Sentinel 规则动态生效……可现实是,我们第一次压测时,服务注册慢得像蜗牛爬,配置改了十分钟还没生效,熔断规则直接没加载。
后来才知道,书里的 Demo 和生产环境之间,隔着一个银河系。
注册中心:Nacos 别只配个地址就完事
很多人以为 Nacos 就是换个注册中心名字,把 eureka.client.serviceUrl.defaultZone 换成 spring.cloud.nacos.discovery.server-addr 就行了。天真!
我们第一次上线时,服务启动后在 Nacos 控制台看不到实例,日志里全是:
[com.alibaba.nacos.client.naming] WARN failed to request /nacos/v1/ns/instance
排查半天才发现:Nacos 默认用内网 IP 注册!而我们的 K8s 网络策略只允许特定出口 IP 访问 Nacos 集群。结果服务把自己“藏”在一个运维根本 ping 不通的虚拟网卡里。
解决办法很简单,但容易被忽略:
spring:
cloud:
nacos:
discovery:
ip: ${HOST_IP} # 通过环境变量注入宿主机真实IP
port: 8080
namespace: prod-ns
group: DEFAULT_GROUP
另外,别忘了健康检查。Nacos 默认用 TCP 探活,但我们有些服务是纯异步的(比如消息消费者),没有 HTTP 接口。结果 Nacos 以为它挂了,直接踢出集群。后来我们统一加了个 /actuator/health 端点,配合 Spring Boot Actuator 才稳住。
开发心得:注册中心不是“配完就忘”的东西。每次部署新环境,第一件事就是去 Nacos 控制台确认实例列表、元数据、健康状态。别等用户投诉才去查。
配置中心:别让“热更新”变成“冷故障”
SCA 的配置中心确实香——改个 YAML,不用重启服务。但有一次双11前演练,产品经理临时要改超时时间,我在 Nacos 上改完,服务却一直没生效。
查日志发现:
WARN RefreshScope not enabled, configuration will not be refreshed dynamically
原来,只有被 @RefreshScope 注解的 Bean 才会热更新!而我们的 Feign Client、RestTemplate 都没加这个注解。
更坑的是,有些配置项根本不能热更新。比如数据库连接池大小、线程池核心数——这些在应用启动时就初始化好了,运行时改了也没用。我们一度以为 Nacos 有问题,其实是自己对 Spring 生命周期理解不够深。
现在我们的做法是:
- 所有需要动态调整的配置,明确标注
// HOT UPDATE SUPPORTED - 关键配置变更后,自动触发一次
/actuator/refresh调用(通过 Jenkins Pipeline) - 写单元测试验证配置是否真的生效(比如 mock Nacos 推送)
顺便吐槽一句:Nacos 的配置历史版本对比功能弱得可怜,连 Git diff 都不如。现在我们都把配置文件同步到 GitLab,当备份+审计用。
熔断限流:Sentinel 不是万能的保险丝
说到 Sentinel,我真是又爱又恨。它比 Hystrix 好用太多——图形化控制台、实时监控、规则持久化。但上线第一天,我们就搞出了“熔断风暴”。
场景是这样的:订单服务调用库存服务,库存服务因为 DB 慢,RT 升高。Sentinel 触发熔断,返回 fallback。但 fallback 里又调了另一个服务……结果链式熔断,整个下单链路雪崩。
问题出在哪?默认的熔断策略太激进!Sentinel 的 RT 模式默认阈值是 1 秒,而我们某些复杂查询正常就要 800ms。稍微抖一下就熔断。
后来我们做了三件事:
- 按接口精细化配置规则:高频接口用 QPS 限流,低频但关键的用异常比例熔断
- fallback 方法绝不依赖外部调用:只返回兜底数据或缓存,绝不发起新 RPC
- 规则持久化到 Apollo(别笑,我们混合用了):Sentinel 控制台改的规则重启就丢,所以写了 Adapter 同步到配置中心
// 示例:安全的 fallback
@SentinelResource(
value = "createOrder",
blockHandler = "createOrderBlock",
fallback = "createOrderFallback"
)
public Order createOrder(OrderRequest req) {
// 调用下游...
}
// 注意:这个方法不能抛异常,也不能调远程服务!
public Order createOrderFallback(OrderRequest req, Throwable t) {
log.warn("createOrder fallback due to: {}", t.getMessage());
return Order.builder().status("PENDING_RETRY").build(); // 返回可重试状态
}
真实事故:有一次测试同学误操作,在 Sentinel 控制台给所有接口加了 QPS=1 的规则。结果整个测试环境“瘫痪”,大家以为是 DB 挂了,查了两小时才发现是限流。从此我们给控制台加了权限分级——普通开发只能看,不能改。
服务调用:OpenFeign + Seata 的坑
我们用 OpenFeign 做服务间调用,搭配 Seata 处理分布式事务。看起来很美好,但实际集成时差点把我送走。
第一个问题是 Feign 的超时设置被覆盖。Seata 为了保证事务一致性,会修改 Feign 的 ReadTimeout。结果原本 5s 的超时变成了 60s,线程池直接打满。
解决方案是在 Feign Client 显式指定超时:
@FeignClient(
name = "inventory-service",
configuration = FeignConfig.class
)
public interface InventoryClient {
@PostMapping("/deduct")
Result deduct(@RequestBody DeductRequest req);
}
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(3000, 5000); // connect, read timeout in ms
}
}
第二个更隐蔽:Seata 的全局锁和本地事务冲突。我们在扣库存时,先查库存(SELECT FOR UPDATE),再更新。但 Seata 的 AT 模式会在 undo_log 里记录 before image,如果本地事务已经持有行锁,Seata 的补偿逻辑可能死锁。
后来我们干脆对关键路径改用 TCC 模式,虽然代码量翻倍,但稳定性提升显著。这也让我明白:分布式事务没有银弹,AT 模式适合读多写少,TCC 适合资金类操作。
性能与监控:别等线上报警才看指标
SCA 组件本身性能不错,但组合起来可能拖垮系统。我们压测时发现,当并发超过 500,Nacos 客户端 CPU 占用飙升。
原因是 Nacos 的长轮询机制太频繁。默认每 10 秒拉一次配置,服务一多,HTTP 连接数爆炸。后来调整了参数:
spring:
cloud:
nacos:
config:
timeout: 3000 # 连接超时
max-retry: 3
discovery:
watch-delay: 30000 # 服务列表拉取间隔,从默认10s改为30s
同时,我们把关键指标接入 Prometheus + Grafana:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
nacos_client_config_request_latency_ms |
配置拉取延迟 | > 2000ms |
sentinel_block_requests_total |
被限流请求数 | 1分钟内 > 100 |
feign_client_http_errors |
Feign 调用失败率 | > 1% |
运维老哥终于不用半夜打电话问我“是不是你代码又崩了”。
为什么我不用 Go 重写?
说到这,肯定有人问:既然 Java 微服务这么复杂,为什么不试试 Go?轻量、高性能、部署简单……
我也折腾过 Go,用 Gin + gRPC 写了个小服务,启动快、内存小,确实爽。但现实很骨感:
- 团队没人会 Go,出了问题只能我背锅
- 公司中间件 SDK 全是 Java 的,Go 要自己封装 Nacos/Sentinel 客户端
- 最关键:我要考公啊!哪有时间学新语言?
现在我的策略是:核心链路用 Java + SCA 稳字当头,边缘服务(比如日志收集、数据清洗)用 Go 试水。既满足技术好奇心,又不影响主线任务。
写在最后:技术选型,终究是权衡的艺术
回看这两年用 SCA 的经历,最大的感悟不是“哪个组件更好”,而是:生产环境里,稳定性和可维护性永远排在新技术前面。
我喜欢折腾 Rust、Go、Deno,但工作中还是乖乖用 JDK 8 + Spring Boot 2.3 + SCA 2021.1。因为我知道,凌晨三点被叫起来修 Bug 的时候,文档齐全、社区活跃、同事熟悉的方案,才是真正的生产力。
顺便说一句,最近我把《Spring Cloud Alibaba 实战》翻烂了,但真正解决问题的,往往是 GitHub issue 里某位老哥的一句 comment,或者是 Stack Overflow 上一个冷门回答。
所以,别迷信书本,也别盲目追新。把现有工具用到极致,比换框架更重要。
对了,明天模考行测,今晚得早点睡。这篇博客写完,我就关电脑——虽然心里还在想那个 Nacos 的推送延迟问题……
(完)

评论 0