我是怎么被逼着把单体项目拆成Spring Cloud微服务的

表结构守护者
2026-03-17 05:14
阅读 1939

去年10月底,上海刚入秋,公司楼下那家瑞幸已经排到马路牙子了。我还在改一个祖传Java单体应用的订单超时逻辑——没错,就是那种启动要5分钟、改一行代码全组提心吊胆、测试一跑就崩的“史诗级”项目。产品经理跑来问:“能不能加个实时库存预警功能?”我差点把咖啡泼他脸上。

但转头一看排期:双11大促前两周。得,不重构不行了。

于是,在一个周五晚上九点、戴着AirPods循环播放Lo-fi beats(边写代码边听音乐是我最后的倔强)、出租屋里泡面都凉透的时刻,我终于下定决心:干!上Spring Cloud!


为什么不是Dubbo?也不是K8s原生?

其实我们团队之前试过Dubbo,配置复杂不说,文档还老是“看源码吧”。而K8s虽然香,但运维成本太高,我们小团队连专职SRE都没有,光是Ingress和Service Mesh就能把人搞疯。

后来我折腾了一圈市面上各种AI编程工具——GitHub Copilot太泛,CodeWhisperer对中文支持拉胯,Windsurf倒是挺新潮,但生成的YAML文件经常缺胳膊少腿。最后还是回到了Cursor:它不仅能理解我的上下文(比如我正在写Eureka配置),还能根据注释自动补全整个Feign客户端,关键是——它懂中文报错!

说回正题。Spring Cloud这套全家桶,虽然被吐槽“重”,但胜在生态成熟、文档齐全、社区活跃。尤其对我们这种既要快速上线又要保证稳定的中小团队来说,简直是救命稻日晚间八点档。


Moltbot:我们的第一个微服务实验田

我们决定拿一个边缘业务先练手:Moltbot——一个内部用的机器人消息推送服务。它原本是嵌在主应用里的一个模块,每天调用量不到五千次,逻辑也简单:接收事件 → 查用户 → 发钉钉/企业微信。

目标很明确:把它独立出来,做成一个可独立部署、自动扩缩容、带熔断降级的微服务。

第一步:注册中心选型

Spring Cloud默认推荐Eureka。虽然Consul、Nacos也行,但我们评估下来:

方案 部署难度 自动注销 中文文档 与Spring Boot集成
Eureka ⭐⭐ 支持 一般 原生支持
Nacos ⭐⭐⭐ 支持 丰富 需额外starter
Consul ⭐⭐⭐⭐ 需脚本 较少 社区版支持弱

最终选了Eureka——毕竟我们图的是快。本地起个@EnableEurekaServer,三行配置搞定。上线后发现一个问题:服务重启时,旧实例没及时下线,导致调用方偶尔404。

坑点来了:Eureka默认90秒才剔除失效实例!我们改成了:

eureka:
  instance:
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 10
  server:
    enable-self-preservation: false  # 关闭自我保护,否则宕机也不剔除

这波操作后,服务上下线几乎实时生效。测试同学终于不用每次部署完狂刷“是不是又挂了”。


Feign + Ribbon:别再自己拼URL了!

以前调内部接口,都是这样写的:

String url = "http://user-service/api/v1/users/" + userId;
RestTemplate.getForObject(url, User.class);

结果某天user-service改了端口,三个服务同时炸掉。运维半夜打电话骂我:“你咋不写服务发现?”

现在用Feign,优雅多了:

@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @GetMapping("/api/v1/users/{id}")
    User getUserById(@PathVariable("id") Long id);
}

配合Ribbon做负载均衡,天然支持轮询。但要注意:Feign默认没有超时控制!有一次user-service慢查询拖垮了Moltbot,线程池直接打满。

解决方案:加上Hystrix(虽然后来官方移除了,但我们还在用)或者直接用Spring Cloud LoadBalancer + Resilience4j。我们现在用的是后者:

feign:
  client:
    config:
      default:
        connectTimeout: 2000
        readTimeout: 5000

顺便提一句,千万别在Feign接口里传复杂对象当GET参数!Spring会序列化失败。血泪教训。


配置中心:告别application-prod.yml地狱

以前每个环境都要维护一套YAML,改个数据库密码得提四个PR。现在统一用Spring Cloud Config + Git后端

结构如下:

config-repo/
├── moltbot-dev.yml
├── moltbot-test.yml
└── moltbot-prod.yml

服务启动时自动拉取对应环境的配置。关键是要配好加密!我们用JCEKS密钥库把数据库密码加密:

spring:
  cloud:
    config:
      server:
        git:
          uri: https://gitlab.internal/config-repo
          username: ${GIT_USER}
          password: ${GIT_TOKEN}
      encrypt:
        key-store:
          location: classpath:/keystore.jks
          password: changeit
          alias: myKey

上线第一天,我就忘了把encrypt.key加到K8s Secret里,服务启动直接报错:“Decryption failed”。当时真的想砸电脑……后来写了自动化检测脚本,CI阶段就验证配置是否能解密。


网关:Zuul已死,Gateway当立

最初我们用Zuul做路由,结果压测时TPS卡在300上不去。查了半天发现Zuul是阻塞IO!换成Spring Cloud Gateway后,性能直接翻倍。

Moltbot的路由规则很简单:

spring:
  cloud:
    gateway:
      routes:
        - id: moltbot-route
          uri: lb://moltbot-service
          predicates:
            - Path=/api/moltbot/**

注意两点:

  1. lb:// 表示走服务发现(LoadBalancer)
  2. 路径匹配要用/**,否则POST请求体可能丢

另外,我们在网关层加了全局日志:

@Component
public class LoggingFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        log.info("Request: {} {}", exchange.getRequest().getMethod(), path);
        return chain.filter(exchange).then(Mono.fromRunnable(() -> 
            log.info("Response status: {}", exchange.getResponse().getStatusCode())
        ));
    }
}

现在排查问题快多了——再也不用去翻七八个服务的日志。


监控与链路追踪:别让Bug藏在黑盒里

微服务最怕“调用链断裂”。我们集成了Sleuth + Zipkin,一行依赖搞定:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

所有请求自动带上traceIdspanId,日志长这样:

2023-11-02 20:15:33.123  INFO [moltbot-service,abc123,def456] ... 

Zipkin UI里能看到完整调用链:

gateway → moltbot → user-service → notify-service

上周五,测试报了个偶发失败。我在Zipkin里搜traceId,发现卡在notify-service的数据库连接池满了。原来是没配HikariCP的最大连接数!赶紧加上:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20

生产环境教训:永远不要相信默认配置。


数据库设计:每个服务管好自己的地盘

Moltbot只存两条表:message_templatesend_record。关键原则:绝不跨服务查表

以前有人想直接从Moltbot查user表,被我当场驳回。现在所有用户信息都通过Feign调用user-service获取。虽然多一次网络开销,但换来的是边界清晰、独立演进

接口设计也遵循RESTful规范:

  • GET /templates → 获取模板列表
  • POST /messages → 创建推送任务
  • PUT /messages/{id}/retry → 重试失败消息

返回统一格式:

{
  "code": 200,
  "data": { ... },
  "msg": "success"
}

前端同学终于不用猜哪个字段叫errorCode哪个叫errCode了。


部署与运维:从Docker到K8s的平滑过渡

本地开发用Docker Compose一键启停:

version: '3'
services:
  eureka:
    image: springcloud/eureka-server
  moltbot:
    build: .
    environment:
      - SPRING_PROFILES_ACTIVE=dev

生产环境跑在K8s上,每个服务一个Deployment + Service。关键配置:

  • 就绪探针:检查Eureka注册是否成功
  • 存活探针:检查/actuator/health
  • 资源限制:CPU 500m,内存 1G(实测峰值才用300m)

上线流程:

  1. GitLab CI 构建镜像并推送到Harbor
  2. ArgoCD 自动同步K8s清单
  3. 灰度发布:先切5%流量,观察10分钟

双11那天,Moltbot QPS飙到2000,CPU稳如老狗。运维拍我肩膀:“小伙子,可以啊。”


总结:微服务不是银弹,但值得拥有

折腾三个月,Moltbot从单体模块蜕变成标准微服务。带来的好处很明显:

✅ 独立部署,不再牵一发而动全身
✅ 自动扩缩容,扛住流量洪峰
✅ 故障隔离,一个服务崩不影响全局

当然,代价也有: ❌ 运维复杂度上升(得会看Trace、调配置) ❌ 网络延迟增加(不过内网基本可忽略) ❌ 学习曲线陡峭(新人入职先啃三天Spring Cloud文档)

但总体来看,对于中等规模、需要快速迭代的业务,Spring Cloud依然是性价比最高的选择。尤其是配合Cursor这类智能工具,写配置、调接口的效率提升不止一倍。

最后说句掏心窝子的话:别为了微服务而微服务。如果你们业务简单、团队小、变化少,单体应用+模块化可能更香。技术选型,终究是为了让人活得轻松一点——至少让我能在晚上十点前关掉电脑,去楼下买杯热美式,而不是对着一堆烂摊子熬夜。

对了,Moltbot现在稳定运行中。上周产品经理又来找我:“能不能加个邮件通知?”
我笑了笑:“行啊,下周上线。”
心里OS:这回,可不怕了。

评论 0

最热最新
暂无评论
表结构守护者Lv.1
0
影响力
0
文章
0
粉丝