从单体到云原生:一个传统企业Java仔的血泪演进实录

代码自留地
2025-12-25 19:58
阅读 483

入职新公司刚满两个月,我这个“老”Java开发(其实才三年经验)就被安排参与了核心系统的架构改造。说实话,当初面试时HR说“我们正在做数字化转型”,我还以为是PPT里的漂亮话——直到看到那套十年前写的、跑在Tomcat 7 上、数据库连接池快爆掉的“遗产级”系统,我才意识到:这活儿真不是闹着玩的。

更刺激的是,上周五晚上八点,产品经理突然在群里@我说:“能不能加个实时数据看板?老板明天要看。”我看着那堆连日志都打不全的代码,内心一万只草泥马奔腾而过。但转念一想,这不正好是推动架构升级的好机会吗?于是,借着这个需求,我和团队开始了一场从单体到云原生的硬核迁移之旅。


起点:那个让人又爱又恨的单体应用

先交代下背景:我们是一家传统制造企业,IT部门长期“边缘化”,系统基本靠外包+内部老员工维护。主业务系统是一个典型的Spring MVC + MyBatis 单体应用,部署在物理机上,数据库是MySQL 5.6(别问,问就是“稳定”)。整个系统耦合度高得离谱——订单模块改个字段,财务报表可能就崩了;前端页面和后端逻辑混在一起,想做个独立API都得翻半天JSP。

最要命的是扩展性。去年双11期间,因为临时搞了个促销活动,流量激增,系统直接雪崩。运维同事手忙脚乱地重启服务,DBA大哥蹲在机房查慢查询,而我?在工位上盯着 Too many connections 的报错,默默打开了招聘软件。

说到求职,其实我之前面试时就被问过微服务相关的问题。当时背了点Spring Cloud的八股文,结果被面试官一句“你们系统拆分后怎么保证事务一致性?”问懵了。后来痛定思痛,买了《微服务架构设计模式》和《云原生Java》两本书啃,还顺手用Python写了个小爬虫,抓了GitHub上Star数高的开源项目源码(比如Spring Boot、Quarkus、Helidon),专门研究它们的启动流程和配置加载机制——别笑,这种“源码考古”对我理解框架底层帮助巨大。


第一步:拆!但别乱拆

领导拍板要做微服务,但没人敢动生产环境。于是我们先拿那个“实时看板”需求练手:把原来耦合在主系统里的数据统计逻辑抽出来,做成一个独立服务。

技术选型很务实:继续用Java(团队熟悉),Spring Boot 2.7(别杠,3.x还没完全适配我们的中间件),数据库还是MySQL,但单独建了个库。接口设计上,我们坚持RESTful风格,并用Swagger生成文档——毕竟前端同事上次抱怨“接口文档比代码还难懂”让我记忆犹新。

# application.yml 示例
server:
  port: 8081

spring:
  datasource:
    url: jdbc:mysql://db-stat-prod:3306/stat_db?useSSL=false
    username: stat_user
    password: ${STAT_DB_PASSWORD}
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

上线第一天就踩坑:前端调用接口超时。排查发现是没做连接池配置,默认HikariCP只有10个连接,而看板页面同时发起5个并行请求,直接堵死。赶紧加上:

@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(env.getProperty("spring.datasource.url"));
        config.setUsername(env.getProperty("spring.datasource.username"));
        config.setPassword(env.getProperty("spring.datasource.password"));
        config.setMaximumPoolSize(50); // 关键!
        return new HikariDataSource(config);
    }
}

教训:微服务不是简单拆分,每个服务都是独立的生产单元,资源隔离和容量规划必须前置


中间件治理:别让服务变成“孤儿”

拆出来之后,新问题来了:怎么调用?怎么监控?怎么知道它挂了?

我们先是尝试用Nginx做简单的反向代理,但很快发现不够用。于是引入Spring Cloud Gateway做统一入口,配合Nacos做服务注册与发现。说实话,第一次看到服务自动注册、自动剔除故障实例时,我差点感动哭——再也不用手动改hosts文件了!

但真正的考验是链路追踪。有一次看板数据延迟严重,查了半天才发现是上游订单服务响应慢,而订单服务又依赖用户中心……没有全链路跟踪,根本没法定位瓶颈。最后上了SkyWalking,效果立竿见影:

服务调用链 平均耗时(ms) P99(ms) 异常率
/api/orders 120 450 0.2%
→ user-service 80 300 0.1%
→ payment-service 320 1200 1.5%

一眼看出支付服务是瓶颈。后来发现是他们用同步HTTP调用第三方支付网关,没设超时。改成异步+重试后,整体延迟降了60%。


拥抱云原生:K8s 不是银弹,但真香

随着服务数量增加,手动部署越来越痛苦。测试环境还好,生产环境每次发布都要运维兄弟配合,还得避开业务高峰。有次半夜发版,我不小心把 application-prod.yml 写成了 application-prod.yaml,Pod起不来,运维大哥边骂边帮我改,场面一度尴尬。

于是,容器化提上日程

我们用Docker把每个服务打包成镜像,推到私有Harbor仓库。然后交给运维上Kubernetes。起初我对K8s一脸懵,什么Deployment、Service、Ingress,概念多到爆炸。但真正用起来才发现它的强大:

  • 自愈能力:Pod挂了自动拉起
  • 弹性伸缩:CPU超过70%自动扩容
  • 配置管理:ConfigMap 和 Secret 让敏感信息不再硬编码

下面是一个典型的Deployment配置片段:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: stat-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stat-service
  template:
    metadata:
      labels:
        app: stat-service
    spec:
      containers:
      - name: stat-app
        image: harbor.internal/stat-service:v1.2.0
        ports:
        - containerPort: 8081
        envFrom:
        - configMapRef:
            name: stat-config
        - secretRef:
            name: stat-secrets
        resources:
          requests:
            memory: "512Mi"
            cpu: "200m"
          limits:
            memory: "1Gi"
            cpu: "500m"

配合Horizontal Pod Autoscaler(HPA),我们甚至在促销期间实现了自动扩缩容。虽然第一次配置HPA时把CPU阈值设成5%,导致Pod疯狂创建又被杀掉(俗称“抖动”),但调整到70%后稳如老狗。


数据库怎么办?别忘了它也是瓶颈

微服务拆了,但数据库还是单点。某天DBA突然找我:“你们stat服务的查询把主库IO打满了!” 原来我们没做读写分离,所有查询都走主库。

解决方案分三步走:

  1. 读写分离:用ShardingSphere-JDBC,配置主从数据源
  2. 缓存兜底:热点数据(如当日订单总数)走Redis,TTL 30秒
  3. 异步聚合:非实时数据用Flink消费binlog做预计算,写入Elasticsearch供看板查询
// ShardingSphere 配置示例
spring:
  shardingsphere:
    datasource:
      names: master,slave0
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://master-db:3306/stat_db
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave-db:3306/stat_db
    masterslave:
      load-balance-algorithm-type: round_robin
      name: ms
      master-data-source-name: master
      slave-data-source-names: slave0

效果?主库CPU从90%降到30%,DBA终于请我喝了杯瑞幸。


为什么我还在看Python爬虫?

你可能会问:一个Java后端为啥研究Python爬虫?其实很简单——技术无边界

我们有个需求是从竞品官网抓取产品价格做分析。前端同事说“这应该后端做”,后端老大说“这属于数据采集,让数据团队搞”。结果两边踢皮球,最后落我头上。

用Python写个Scrapy爬虫半小时搞定,但部署时遇到反爬(IP封禁、验证码)。于是我用Java写了个代理池调度服务,配合Redis记录请求频次,再通过K8s Job定期触发爬虫任务。最终形成一个“Python采集 + Java调度 + K8s编排”的混合方案。

这件事让我明白:云原生时代,语言只是工具,解决问题才是核心。就像我书架上既有《Effective Java》,也有《流畅的Python》——能跑就行。


心得:架构演进不是一蹴而就

回顾这两个月,从单体到云原生,我们走了不少弯路:

  • 别为了微服务而微服务,先从业务边界清晰的模块下手
  • 监控和日志必须前置,否则上线即盲人摸象
  • 自动化是生命线:CI/CD、健康检查、自动回滚,一个都不能少
  • 团队协作工具(如GitLab、Jenkins、ArgoCD)比技术本身更重要

现在,我们的核心系统已经拆出6个微服务,全部跑在K8s集群上。上周老板又提新需求,我淡定回了句:“没问题,加个服务就行。” —— 这种从容,是两个月前不敢想的。

最后说点掏心窝子的话:如果你也在传统企业,别觉得技术落后就没希望。数字化转型不是换技术栈,而是换思维。哪怕从一个小小的Dockerfile开始,也是迈向云原生的第一步。

对了,最近我在看《Cloud Native Patterns》,打算下周试试把某个服务改成Quarkus,看看GraalVM native image能不能把启动时间压到100ms以内。要是成功了,再来更新!

(完)

附:关键组件版本参考表

组件 版本 说明
Spring Boot 2.7.12 兼容性好,社区支持强
Nacos 2.2.3 服务发现 + 配置中心
SkyWalking 9.7.0 APM监控
Kubernetes 1.25 生产环境
Docker 24.0 容器运行时
ShardingSphere 5.3.2 数据库中间件

PS:别问我为什么不直接上Serverless——我们连K8s网络策略都还没配明白呢 😅

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝