聊聊我在美团搞微服务架构的一些血泪经验
本文约3700字,阅读大概需要15分钟。适合有一定Java基础,想入门微服务架构的同学。
写在前面
现在是晚上11点半,北京望京这边下着小雨。我刚把儿子哄睡着,轻手轻脚地关上卧室门,坐到书房里打开电脑。
先做个自我介绍吧,我是老王,美团外卖的一名Java开发,算下来今年已经是第四年了。这四年里,我从一个只会写CRUD的小白,慢慢成长为能独立负责一个微服务模块的"老油条"。平时除了写代码,最大的爱好就是参加各种技术分享会,不管是公司内部的还是外部的,只要时间允许我都尽量去。毕竟在这个行业,不学习真的会被淘汰。
最近远程办公,在家撸代码的时间反而更多了。白天跟团队对需求、写代码、review,晚上孩子睡了就自己捣鼓一些新技术。最近一直在研究AI相关的东西,比如大模型的应用、Agent框架之类的,后面有机会再跟大家聊聊这块。
今天这篇文章,想跟大家聊聊Spring Cloud微服务。为啥突然想写这个?说来话长。
事情是这样的
去年双11大促前,我们组接了个新项目,要做一个商家营销工具平台。说实话,当时我们组的技术栈还是比较传统的,单体Spring Boot应用,数据库用的是MySQL,缓存用的Redis,消息队列用的RocketMQ。平时日活几十万的时候,这套架构跑得还挺稳的。
但是这次不一样了。
产品经理(此处没有针对任何PM的意思,求生欲上线)提了一堆需求,什么商家自助配置活动、实时数据看板、智能推荐、多端适配...我听完就一个感觉:这要是还搞单体应用,上线那天就是事故现场。
当时我们tech lead老张在周会上拍了板:"这个项目必须上微服务,用Spring Cloud全家桶。"
好家伙,我当时心里就咯噔一下。Spring Cloud我知道,但真正在生产环境从零搭一套,还真没干过。之前都是在别人搭好的框架上写业务代码,这次要自己从零开始...
"老王,你来负责基础架构搭建这块。"老张看向我。
"啊?我?"
"你之前不是老参加技术分享会嘛,这块你熟。"
行吧,被架到这儿了,硬着头皮也得干。
从零开始的第一步:服务拆分
搞微服务,第一件事就是服务拆分。这玩意儿说起来简单,做起来全是坑。
我们一开始犯了一个经典错误:拆得太细了。恨不得每个功能点都拆成一个服务,什么用户服务、订单服务、商品服务、优惠券服务、活动服务、消息服务、日志服务...拆出来十几个。
结果呢?开发效率直接暴跌。一个稍微复杂点的业务流程,要调四五个服务,联调的时候各种网络问题、数据一致性问题,搞得大家怨声载道。
后来我们复盘了一下,重新调整了拆分策略。核心原则就几条:
- 按业务域拆分,而不是按功能拆分。比如商家域、用户域、交易域、营销域,每个域是一个大的服务边界。
- 高内聚低耦合。域内的功能尽量放在一起,域间通过标准接口通信。
- 考虑团队结构。康威定律不是说着玩的,服务划分要跟团队结构匹配。
最终我们拆成了下面这几个核心服务:
| 服务名 | 职责 | 技术选型 | 数据库 |
|---|---|---|---|
| merchant-gateway | API网关,统一入口 | Spring Cloud Gateway | 无 |
| merchant-auth | 认证授权服务 | Spring Security + OAuth2 | MySQL |
| merchant-core | 商家核心业务服务 | Spring Boot | MySQL |
| marketing-engine | 营销活动引擎 | Spring Boot | MySQL + Redis |
| data-dashboard | 数据看板服务 | Spring Boot | ClickHouse |
| message-center | 消息中心 | Spring Boot | MongoDB |
注册中心选型:Nacos真香
注册中心这块,我们纠结了一阵子。Eureka?Consul?Zookeeper?Nacos?
Eureka在Spring Cloud 2.x之后就不怎么更新了,Pass。Consul需要额外部署,运维成本高。Zookeeper...这玩意儿在CP场景下表现好,但我们更需要的是AP。
最后选了Nacos,原因很简单:
- 阿里开源的,国内社区活跃,文档友好
- 同时支持服务注册发现和配置管理,一个组件搞定两件事
- 支持AP和CP模式切换
- 跟Spring Cloud Alibaba生态无缝集成
部署的时候我们搞了个三节点的Nacos集群,用MySQL做持久化存储。这里有个坑要提醒大家:Nacos集群模式下,一定要配置好MySQL数据源,不然重启之后服务注册信息全丢了。别问我怎么知道的,问就是上周五晚上加班时踩的坑,当时真的想砸电脑。
# application.yml 关键配置
spring:
cloud:
nacos:
discovery:
server-addr: nacos1:8848,nacos2:8848,nacos3:8848
namespace: merchant-prod
group: MERCHANT_GROUP
config:
server-addr: nacos1:8848,nacos2:8848,nacos3:8848
namespace: merchant-prod
group: MERCHANT_GROUP
file-extension: yaml
服务间通信:Feign还是gRPC?
微服务之间怎么通信,这也是个经典问题。
我们项目里大部分场景用的是OpenFeign,毕竟声明式调用太方便了,写个接口加个注解就完事。但是有个性能敏感的场景——数据看板服务从营销引擎拉取实时数据,QPS峰值能到上万,Feign的HTTP调用延迟扛不住。
这块我们换成了gRPC,性能直接提升了一个量级。Protobuf序列化比JSON小很多,HTTP/2的多路复用也不是盖的。
| 对比项 | OpenFeign (HTTP/JSON) | gRPC (HTTP/2/Protobuf) |
|---|---|---|
| 平均延迟 | 15-30ms | 2-5ms |
| 序列化大小 | 较大 | 较小(约1/3) |
| 开发效率 | 高(声明式) | 中等(需要写proto) |
| 调试友好度 | 高(直接看JSON) | 低(二进制) |
| 适用场景 | 一般业务调用 | 高频、低延迟场景 |
这里分享一个gRPC的小经验:proto文件一定要做好版本管理。我们用了一个独立的Git仓库来管理所有proto文件,通过CI/CD自动生成各语言的SDK。不然等proto文件满天飞的时候,你就知道什么叫痛了。
配置中心:动态配置真的救命
Nacos的配置管理功能,在我们项目里发挥了巨大作用。
举个真实的例子:去年双11当天晚上8点,流量高峰来了,我们发现营销引擎的一个限流阈值设得太保守了,好多正常请求被拒。这时候要是改代码重新发布,黄花菜都凉了。好在配置是动态的,我在Nacos控制台改了个参数,几秒钟全集群生效,问题立马解决。
那一刻,真的觉得配置中心这玩意儿太重要了。
我们项目的配置管理策略是这样的:
merchant-core/
├── application.yaml # 默认配置
├── application-dev.yaml # 开发环境
├── application-test.yaml # 测试环境
├── application-prod.yaml # 生产环境
└── application-prod-gray.yaml # 灰度环境
在Nacos里,我们用namespace区分环境,group区分应用,dataId区分具体配置文件。这样管理起来很清晰。
还有个最佳实践:敏感配置(比如数据库密码、API Key)不要直接写在Nacos里,要走加密或者对接公司的密钥管理服务。我们之前有个实习生把生产数据库密码明文写在配置里,虽然只有内网能访问,但还是被安全团队扫描出来了,挨了一顿批。
熔断限流:Sentinel不能少
做C端产品,尤其是我们外卖这种高并发场景,熔断限流是必须的。
我们用的是Sentinel,阿里出品,跟Spring Cloud Alibaba集成得很好。主要做了这几件事:
- 接口级限流:每个核心接口都配了QPS限流规则,防止某个接口被打爆影响整个服务。
- 服务级熔断:下游服务异常率超过阈值时自动熔断,快速失败,避免雪崩。
- 热点参数限流:针对某些特殊参数(比如某个爆款商品ID)做细粒度限流。
@RestController
@RequestMapping("/api/activity")
public class ActivityController {
@GetMapping("/detail/{id}")
@SentinelResource(value = "activityDetail",
blockHandler = "activityDetailBlock")
public Result<ActivityDetail> getActivityDetail(@PathVariable Long id) {
return activityService.getDetail(id);
}
// 限流降级处理方法
public Result<ActivityDetail> activityDetailBlock(Long id, BlockException ex) {
log.warn("活动详情接口被限流, activityId={}", id);
return Result.fail("系统繁忙,请稍后再试");
}
}
这里有个坑:Sentinel的Dashboard一定要单独部署,而且生产环境要配置好鉴权。我们之前Dashboard直接裸奔,被运维小哥发现了,又是一顿教育。
链路追踪:出了问题得能查
微服务多了之后,一个请求可能经过四五个服务,出了问题怎么排查?靠日志一个个翻?那得翻到什么时候。
我们接入了SkyWalking做全链路追踪。这玩意儿是真的好用,请求在哪个服务耗时多少、哪个SQL慢、哪个下游调用超时,一目了然。
接入也很简单,Java Agent的方式,对代码零侵入:
# 启动参数加上SkyWalking Agent
java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=merchant-core \
-Dskywalking.collector.backend_service=skywalking-oap:11800 \
-jar merchant-core.jar
有个小技巧:TraceId一定要透传到日志里。这样在ELK里搜日志的时候,直接搜TraceId就能把整个调用链的日志都捞出来。我们是在MDC里做的:
@Component
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
String traceId = Tracer.Context.getTraceId();
MDC.put("traceId", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
数据库设计的一些思考
微服务架构下,数据库设计跟单体应用有很大不同。核心原则就是:每个服务拥有自己独立的数据库,服务间不共享数据库。
说起来容易,做起来难。比如商家服务和营销服务,都需要用到商家的一些基础信息。怎么办?
我们的方案是:
- 数据冗余:营销服务里冗余一份商家基础信息(商家ID、名称、logo等),通过消息队列同步更新。
- 接口查询:非核心场景,直接通过Feign调商家服务获取。
- CQRS:读写分离场景,用Canal监听MySQL binlog,同步到Elasticsearch,提供查询能力。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 数据冗余 | 查询快,不依赖其他服务 | 数据一致性维护成本高 | 核心链路、高频查询 |
| 接口查询 | 数据实时准确 | 增加调用链路,影响性能 | 非核心场景、低频查询 |
| CQRS | 查询灵活,性能好 | 架构复杂,有延迟 | 复杂查询、报表场景 |
分库分表这块,我们用的是ShardingSphere。商家数据按商家ID做分片键,分了16个库,每个库64张表。为什么这么分?因为我们的业务场景里,大部分查询都是带商家ID的,这样能保证同一个商家的数据在同一个库,避免跨库查询。
部署和运维
最后聊聊部署。我们用K8s来管理所有的微服务,配合Helm Chart做标准化部署。
每个服务的Dockerfile都差不多,分享一个我们优化过的模板:
# 构建阶段
FROM maven:3.8-openjdk-11 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
# 运行阶段
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# JVM参数优化
ENV JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
几个要点:
- 多阶段构建,减小镜像体积
- 依赖预下载,加速后续构建
- JVM参数根据容器资源限制调整,别直接抄默认的
- 一定要开HeapDump,OOM的时候能救命
说到运维,不得不提监控。我们用Prometheus + Grafana做指标监控,ELK做日志收集分析,SkyWalking做链路追踪。三件套配齐了,基本上线上出了问题能快速定位。
一些踩坑总结
写了这么多,最后总结几个我们踩过的坑,希望能帮大家少走弯路:
服务不要拆太细。刚开始微服务化的团队,很容易犯这个错误。宁可粗一点,后面再拆分,也别一上来就搞几十个服务。
分布式事务要慎重。能用最终一致性解决的,就别上强一致性方案。Seata的AT模式虽然好用,但性能损耗不小。我们大部分场景用的是基于消息队列的最终一致性方案。
配置管理要规范。别什么配置都往Nacos里塞,要有分类、有命名规范、有变更审批流程。
日志规范要统一。微服务环境下,日志是排查问题的命根子。统一日志格式、统一日志级别、统一TraceId透传。
灰度发布必须有。微服务多了之后,全量发布的风险太大。我们用的是基于流量的灰度方案,通过Gateway层做流量染色和路由。
写在最后
搞微服务这一年多,最大的感受就是:没有银弹。Spring Cloud提供了一堆好用的工具,但怎么用、用在哪,还是需要结合自己的业务场景来思考。
不要为了微服务而微服务。如果你的业务单体应用就能搞定,团队也就三五个人,那真的没必要上微服务。引入微服务带来的复杂度,可能会让你得不偿失。
好了,今天就聊到这。儿子好像醒了,我得去看看。
对了,最近在学习AI相关的技术,发现了一些很有意思的东西。比如用AutoGen这种多Agent框架来辅助代码review,效果还挺不错的。还有Antigravity这个概念也挺有意思的,虽然跟Python那个彩蛋不是一回事,但在架构设计里确实有些"反重力"的思路值得借鉴。
代码人生,且行且珍惜吧。我们下次分享会再见。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题可以在评论区留言,我看到都会回复的。
作者:美团外卖Java开发老王,一个热爱技术分享的程序员。


评论 0