从硬件狗到Go仔,我在Java微服务里翻车又爬起
去年双11前夜,我正戴着耳机听着Lo-fi beats敲着Go代码,突然被拉进一个紧急会议——“老张,你不是搞过嵌入式系统?现在这个Java微服务集群挂了,你来顶一下。”
我当时差点把咖啡喷出来:我转Go都快一年了,Spring Cloud Alibaba是啥玩意儿?
但没办法,谁让我是团队里“什么都略懂一点”的万金油呢?更扎心的是,这项目原本是另一个同事负责的,结果他跳槽去了字节,留下一坨没文档、没注释、配置全写死的“遗产”。产品还催着上线,测试那边已经开始甩锅:“后端接口又502了!”
于是,这个曾经在示波器前调SPI通信的硬件仔,被迫重新拾起Java,一头扎进了Spring Cloud Alibaba的深水区。今天这篇“血泪史”,就是想告诉后来者:别等线上崩了才去学,有些坑,踩一次就够了。
Nacos注册中心:你以为连上了,其实它把你拉黑了
第一个翻车点出在Nacos。本地跑得好好的,一上测试环境,服务死活注册不上去。日志里只有一行:
com.alibaba.nacos.api.exception.NacosException: failed to req API:/nacos/v1/ns/instance after all servers([nacos.prod.xxx.com:8848]) tried
我第一反应是网络不通。用telnet nacos.prod.xxx.com 8848一试,通的啊!再看防火墙规则、安全组……都没问题。最后发现,Nacos客户端默认用的是HTTP长连接 + 心跳机制,而我们的K8s Ingress层对长连接有超时限制(30秒),心跳包发不出去,服务就被自动剔除了。
解决方案很简单:要么改Ingress的keepalive配置,要么让客户端走短连接。我们选了后者,在bootstrap.yml里加了:
spring:
cloud:
nacos:
discovery:
# 强制使用短连接,避免被中间件掐断
ephemeral: true
# 显式指定命名空间,别用默认public
namespace: prod-ns-2023
经验:生产环境千万别用
public命名空间!我们早期图省事,所有环境混在一起,有一次测试服务注册到生产命名空间,直接把线上流量打歪了,半夜被call醒修bug,那叫一个酸爽。
Sentinel限流:配了等于没配?
第二个大坑是Sentinel。产品经理说:“大促期间要防刷,每个接口QPS不能超过100。” 我心想,Sentinel不就是干这个的吗?于是在控制台配了个流控规则,资源名填了/api/order/create,阈值100。
结果压测时,QPS飙到500,系统稳如老狗——根本没触发限流!
查了半天才发现:Sentinel默认只监控@SentinelResource注解标记的方法,或者通过SphU.entry()手动埋点的资源。而我们用的是Spring MVC的普通Controller,路径/api/order/create根本不会被自动识别为“资源”。
正确的做法有两种:
- 给Controller方法加
@SentinelResource(推荐):
@GetMapping("/order/create")
@SentinelResource(value = "createOrder", blockHandler = "handleBlock")
public ResponseEntity<Order> createOrder(@RequestParam String userId) {
// 业务逻辑
}
- 或者开启Web Servlet支持,在
application.yml中启用:
spring:
cloud:
sentinel:
filter:
enabled: true
web-context-unify: false # 关键!否则所有URL合并成一个资源
注意:
web-context-unify: false必须设为false,否则所有请求都会被归到同一个资源名下(比如/*),限流就失效了。
Seata分布式事务:别信“开箱即用”
最让我崩溃的是Seata。业务要求“下单+扣库存”必须原子性,领导拍板:“上Seata,阿里开源的,肯定稳。”
结果一集成,各种GlobalTransaction not active、branch register failed。数据库用的是MySQL 8.0,驱动版本不对;undo_log表没建;TC(Transaction Coordinator)和TM(Transaction Manager)网络不通……整整三天,我每天加班到凌晨,耳机里循环播放《The Nights》,感觉自己像个修水管的。
最终理清几个关键点:
| 问题 | 解决方案 |
|---|---|
| 数据库驱动不兼容 | 升级mysql-connector-java到8.0.28+ |
| undo_log表缺失 | 手动在每个业务库执行Seata提供的SQL脚本 |
| TC地址配置错误 | 在registry.conf中显式指定serverAddr = "seata-server:8091" |
| 事务超时太短 | @GlobalTransactional(timeoutMills = 60000) |
还有个隐藏坑:Seata的AT模式要求所有参与方都能回滚。我们有个老服务用的是MongoDB,根本不支持undo_log,结果整个分布式事务卡住。最后只能对那个服务做补偿逻辑,绕过Seata。
配置中心:别把密码写进Git!
说到配置,我得吐槽一句:见过太多人把application-prod.yml提交到Git仓库了!包括accessKey、数据库密码、Redis密钥……简直是安全灾难。
我们用Nacos Config做配置中心,但初期大家还是习惯本地写配置。后来我强制推行了一条CI规则:任何包含prod字样的YAML文件禁止合入主干。
正确的做法是:
- 所有敏感配置放在Nacos Config中
- 应用启动时通过
dataId+group拉取 - 本地只保留
bootstrap.yml,且只包含Nacos地址和namespace
# bootstrap.yml
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: nacos.prod.xxx.com:8848
namespace: prod-ns-2023
group: DEFAULT_GROUP
file-extension: yaml
这样,即使代码泄露,攻击者也拿不到真实配置。运维同学再也不用半夜打电话吼:“你们谁把数据库密码commit了?!”
资源治理:别让一个服务拖垮整个集群
最后说说“资源”这个词。在硬件时代,我天天跟RAM、Flash、CPU cycle打交道,知道每一字节都珍贵。可到了Java微服务世界,很多人仿佛忘了“资源”二字。
我们曾有一个服务,每次启动都要加载500MB的缓存数据到内存。结果K8s Pod内存限制设得太低(1G),频繁OOM Kill。日志里全是:
java.lang.OutOfMemoryError: Java heap space
解决思路很简单:按需加载 + 分页预热。但更深层的问题是:没人做容量规划。于是我和运维一起定了几条铁律:
- 每个服务必须声明
requests和limits - JVM堆内存不超过Pod内存的70%
- 禁止在启动时加载全量数据
- 关键接口必须有熔断和降级策略
现在,我们的服务在Prometheus里跑得明明白白,CPU、内存、GC次数一目了然。再也不用靠“重启大法”续命了。
写在最后:代码人生,就是不断填坑
从示波器到IDEA,从C到Go再到被迫重拾Java,我的“代码人生”就像一场不断打怪升级的游戏。Spring Cloud Alibaba确实强大,但它不是银弹,更不是玩具。生产环境里,一个配置错位,可能就是百万损失。
如果你也在用SCA,记住三件事:
- 别信默认配置,生产环境必须显式声明
- 监控先行,没Metrics的服务等于裸奔
- 文档比代码重要,别让你的继任者骂你祖宗
上周五晚上,我又一次戴着耳机敲代码。这次是Go,但桌角还放着一本《Spring Cloud Alibaba实战》。我知道,技术人的路没有尽头,只有下一个坑在等着我。
不过没关系,只要音乐还在响,键盘还在敲,我就还能写下去——毕竟,这才是真实的代码人生。

评论 0