聊聊我在美团搞微服务架构的一些血泪经验

Maven下载中
2026-07-16 13:54
阅读 668

本文约3700字,阅读大概需要15分钟。适合有一定Java基础,想入门微服务架构的同学。


写在前面

现在是晚上11点半,北京望京这边下着小雨。我刚把儿子哄睡着,轻手轻脚地关上卧室门,坐到书房里打开电脑。

先做个自我介绍吧,我是老王,美团外卖的一名Java开发,算下来今年已经是第四年了。这四年里,我从一个只会写CRUD的小白,慢慢成长为能独立负责一个微服务模块的"老油条"。平时除了写代码,最大的爱好就是参加各种技术分享会,不管是公司内部的还是外部的,只要时间允许我都尽量去。毕竟在这个行业,不学习真的会被淘汰。

最近远程办公,在家撸代码的时间反而更多了。白天跟团队对需求、写代码、review,晚上孩子睡了就自己捣鼓一些新技术。最近一直在研究AI相关的东西,比如大模型的应用、Agent框架之类的,后面有机会再跟大家聊聊这块。

今天这篇文章,想跟大家聊聊Spring Cloud微服务。为啥突然想写这个?说来话长。

事情是这样的

去年双11大促前,我们组接了个新项目,要做一个商家营销工具平台。说实话,当时我们组的技术栈还是比较传统的,单体Spring Boot应用,数据库用的是MySQL,缓存用的Redis,消息队列用的RocketMQ。平时日活几十万的时候,这套架构跑得还挺稳的。

但是这次不一样了。

产品经理(此处没有针对任何PM的意思,求生欲上线)提了一堆需求,什么商家自助配置活动、实时数据看板、智能推荐、多端适配...我听完就一个感觉:这要是还搞单体应用,上线那天就是事故现场。

当时我们tech lead老张在周会上拍了板:"这个项目必须上微服务,用Spring Cloud全家桶。"

好家伙,我当时心里就咯噔一下。Spring Cloud我知道,但真正在生产环境从零搭一套,还真没干过。之前都是在别人搭好的框架上写业务代码,这次要自己从零开始...

"老王,你来负责基础架构搭建这块。"老张看向我。

"啊?我?"

"你之前不是老参加技术分享会嘛,这块你熟。"

行吧,被架到这儿了,硬着头皮也得干。

从零开始的第一步:服务拆分

搞微服务,第一件事就是服务拆分。这玩意儿说起来简单,做起来全是坑。

我们一开始犯了一个经典错误:拆得太细了。恨不得每个功能点都拆成一个服务,什么用户服务、订单服务、商品服务、优惠券服务、活动服务、消息服务、日志服务...拆出来十几个。

结果呢?开发效率直接暴跌。一个稍微复杂点的业务流程,要调四五个服务,联调的时候各种网络问题、数据一致性问题,搞得大家怨声载道。

后来我们复盘了一下,重新调整了拆分策略。核心原则就几条:

  1. 按业务域拆分,而不是按功能拆分。比如商家域、用户域、交易域、营销域,每个域是一个大的服务边界。
  2. 高内聚低耦合。域内的功能尽量放在一起,域间通过标准接口通信。
  3. 考虑团队结构。康威定律不是说着玩的,服务划分要跟团队结构匹配。

最终我们拆成了下面这几个核心服务:

服务名 职责 技术选型 数据库
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集成得很好。主要做了这几件事:

  1. 接口级限流:每个核心接口都配了QPS限流规则,防止某个接口被打爆影响整个服务。
  2. 服务级熔断:下游服务异常率超过阈值时自动熔断,快速失败,避免雪崩。
  3. 热点参数限流:针对某些特殊参数(比如某个爆款商品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");
        }
    }
}

数据库设计的一些思考

微服务架构下,数据库设计跟单体应用有很大不同。核心原则就是:每个服务拥有自己独立的数据库,服务间不共享数据库

说起来容易,做起来难。比如商家服务和营销服务,都需要用到商家的一些基础信息。怎么办?

我们的方案是:

  1. 数据冗余:营销服务里冗余一份商家基础信息(商家ID、名称、logo等),通过消息队列同步更新。
  2. 接口查询:非核心场景,直接通过Feign调商家服务获取。
  3. 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做链路追踪。三件套配齐了,基本上线上出了问题能快速定位。

一些踩坑总结

写了这么多,最后总结几个我们踩过的坑,希望能帮大家少走弯路:

  1. 服务不要拆太细。刚开始微服务化的团队,很容易犯这个错误。宁可粗一点,后面再拆分,也别一上来就搞几十个服务。

  2. 分布式事务要慎重。能用最终一致性解决的,就别上强一致性方案。Seata的AT模式虽然好用,但性能损耗不小。我们大部分场景用的是基于消息队列的最终一致性方案。

  3. 配置管理要规范。别什么配置都往Nacos里塞,要有分类、有命名规范、有变更审批流程。

  4. 日志规范要统一。微服务环境下,日志是排查问题的命根子。统一日志格式、统一日志级别、统一TraceId透传。

  5. 灰度发布必须有。微服务多了之后,全量发布的风险太大。我们用的是基于流量的灰度方案,通过Gateway层做流量染色和路由。

写在最后

搞微服务这一年多,最大的感受就是:没有银弹。Spring Cloud提供了一堆好用的工具,但怎么用、用在哪,还是需要结合自己的业务场景来思考。

不要为了微服务而微服务。如果你的业务单体应用就能搞定,团队也就三五个人,那真的没必要上微服务。引入微服务带来的复杂度,可能会让你得不偿失。

好了,今天就聊到这。儿子好像醒了,我得去看看。

对了,最近在学习AI相关的技术,发现了一些很有意思的东西。比如用AutoGen这种多Agent框架来辅助代码review,效果还挺不错的。还有Antigravity这个概念也挺有意思的,虽然跟Python那个彩蛋不是一回事,但在架构设计里确实有些"反重力"的思路值得借鉴。

代码人生,且行且珍惜吧。我们下次分享会再见。


如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题可以在评论区留言,我看到都会回复的。

作者:美团外卖Java开发老王,一个热爱技术分享的程序员。

评论 0

最热最新
暂无评论
Maven下载中Lv.1
0
影响力
0
文章
0
粉丝