后端架构演进:从单体到云原生 —— 一个35岁老码农的血泪史

超凡的先知
2025-12-16 14:54
阅读 544

坐标上海,住在公司步行10分钟的出租屋里,Vim配置比女朋友还熟。上周五凌晨三点还在修一个线上OOM,突然想起自己去年面试时被问“你们系统怎么拆微服务”的尴尬场面——今天就来聊聊,我是怎么从一个单体应用里爬出来,滚进云原生这个大坑的。

起因:简历上的“高并发”差点让我背锅

去年年底想跳槽,更新简历时手一抖写上了“主导高并发系统架构设计”。结果面某大厂时,面试官眯着眼问:“你这系统QPS多少?怎么做的弹性伸缩?有做混沌测试吗?”我支支吾吾说“我们……用Nginx做了负载均衡”,对方笑了笑,没再追问。

那一刻我意识到:光会写业务逻辑,在35岁的年纪已经不够看了

更打脸的是回公司后,产品老大在周会上拍桌子:“双11流量预估翻三倍,现在系统撑不住怎么办?”运维兄弟幽幽补刀:“上次扩容加了两台机器,结果数据库连接池爆了,半夜报警吵醒我三次。”

得,躲不掉了。要么重构,要么等着被流量冲垮。领导一句话:“你不是简历写得挺牛?这次你牵头搞云原生改造。”

第一阶段:单体架构的“甜蜜期”早已过去

我们系统最初是个典型的Spring Boot单体应用:前端Vue + 后端Java + MySQL + Redis,部署在两台阿里云ECS上,Nginx反向代理。早期日活几千人,跑得飞快,改个Bug五分钟上线,日子美滋滋。

但随着用户量涨到百万级,问题全来了:

  • 发布一次全站停服:哪怕只改了个文案,也得停机部署。
  • 资源浪费严重:订单模块忙死,用户中心闲得发霉,但CPU内存一起吃。
  • 故障扩散:一个慢SQL拖垮整个JVM,全站502。
  • 数据库成瓶颈:所有业务共用一个MySQL实例,连表查询越来越慢。

最惨的是去年618,促销活动刚上线,数据库主从同步延迟飙到300秒,用户下单成功但库存没扣,超卖了两千单。老板脸色铁青,我躲在厕所刷Stack Overflow,差点哭出来。

拆!但别瞎拆

很多人以为微服务就是“把单体切成一堆小服务”,天真了。拆分不当,等于把一个大雷换成十个地雷

我们先做了领域划分(感谢DDD救我狗命),按业务边界拆出:

  • user-service(用户认证、资料)
  • order-service(下单、支付回调)
  • product-service(商品信息、库存)
  • notification-service(短信、站内信)

每个服务独立数据库,通过Feign + Ribbon调用。初期确实爽:订单模块挂了不影响用户登录,发布粒度细了,测试也隔离了。

但新问题立马冒头:

  • 分布式事务:下单要扣库存+创建订单+发通知,三个服务怎么保证原子性?
  • 链路追踪缺失:一个请求跨五个服务,日志散落在不同机器,查Bug像破案。
  • 配置爆炸:每个服务都要维护自己的application.yml,改个超时参数要改八处。

那会儿每天早上第一件事不是喝咖啡,是看SkyWalking的调用链有没有红色警报。运维同事看我的眼神都带着怜悯。

容器化:Docker救不了世界,但能救我下班时间

拆完微服务,部署又成了噩梦。每台机器要装JDK、Maven、Node.js(因为前端也要打包),环境不一致导致“在我机器上能跑”。

被逼无奈,开始搞Docker。第一次写Dockerfile时,把整个target/目录COPY进去,镜像1.2G,CI流水线跑一次十分钟。测试小哥吐槽:“你这镜像比我的游戏安装包还大。”

后来学乖了,用多阶段构建:

# 构建阶段
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# 运行阶段
FROM openjdk:17-jre-slim
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

镜像压到180MB,CI时间从10分钟降到90秒。运维终于不用半夜被叫起来“手动部署”。

但真正解放生产力的是Kubernetes。虽然一开始被YAML劝退(谁还记得kind: Deploymentkind: Service的区别?),但一旦跑通,自动扩缩容、滚动更新、健康探针这些功能真香。

比如订单服务高峰期自动从3副本扩到10副本:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

双11当天看着Pod数自动涨上去,我在工位啃着泡面笑出声——这钱花得值

云原生不是终点,而是新坑的起点

上了K8s就万事大吉?Too young.

第一个坑是服务网格。早期用Spring Cloud Gateway做API网关,但熔断、限流规则写死在代码里,改个阈值得重新打包。后来引入Istio,用VirtualService动态配置:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
  - order-service
  http:
  - route:
    - destination:
        host: order-service
        subset: v1
    retries:
      attempts: 3
      perTryTimeout: 2s

但Istio的学习曲线陡得像珠穆朗玛峰。有次配置错了mTLS,整个集群服务间通信中断,线上静默了20分钟。产品经理在群里@我:“系统是不是挂了?” 我盯着Prometheus监控图,手抖得不敢回。

第二个坑是可观测性。以前单体时代,ELK看日志就行。现在微服务+容器,日志分散、指标杂乱、链路断裂。我们最终搭了一套组合拳:

  • Logging:Fluentd收集容器日志 → Kafka → ES
  • Metrics:Prometheus抓取Micrometer指标 + Grafana看板
  • Tracing:Jaeger采样全链路,配合OpenTelemetry SDK

现在查问题流程变成:Grafana发现CPU飙升 → Jaeger定位到具体Span → Kibana搜该Pod日志。效率提升十倍,再也不用求运维导日志了。

数据库怎么办?别只顾着拆服务

很多人只关注服务拆分,忘了数据才是最难拆的

我们早期犯了个致命错误:把用户表和订单表物理分离后,还保留外键关联。结果跨库JOIN直接报错,ORM框架崩了。

解决方案分三步走:

  1. 去外键:所有关联靠ID,业务层校验
  2. 读写分离:主库写,从库读(用ShardingSphere中间件)
  3. 分库分表:订单表按user_id哈希,分16库16表

但分库分表带来新问题:全局ID怎么生成?雪花算法?UUID太长?最后选了Leaf-segment方案,用DB批量缓存ID段,性能扛住了。

接口设计也得改。以前单体时一个/order/detail接口返回订单+商品+用户信息,现在得聚合多个服务:

// 聚合服务(BFF层)
public OrderDetailVO getOrderDetail(Long orderId) {
    Order order = orderClient.getOrder(orderId); // 调order-service
    Product product = productClient.getProduct(order.getProductId()); // 调product-service
    User user = userClient.getUser(order.getUserId()); // 调user-service
    
    return OrderDetailVO.builder()
        .order(order)
        .product(product)
        .user(user)
        .build();
}

虽然增加了网络开销,但通过并行调用 + 缓存优化,P99延迟控制在200ms内。前端同学终于不用自己拼三个接口了。

面试题?我现在出题都带血泪经验

最近帮公司面试后端,必问一道题:

“如果让你把现在的单体系统改造成云原生架构,你会考虑哪些关键点?”

以前我答“拆微服务、上容器、加监控”,现在我会说:

维度 单体架构 云原生架构 踩过的坑
部署 手动SCP jar包 GitOps + ArgoCD 镜像tag没对齐,发布错版本
通信 内部方法调用 gRPC/HTTP + 服务网格 超时设置不合理,雪崩
数据 共享DB 分库分表 + CDC 事务补偿逻辑漏了状态机
可观测 查单机日志 Metrics+Logs+Traces 采样率设太高,ES炸了

真正的云原生不是技术堆砌,而是思维方式的转变:从“保证单点稳定”到“容忍局部失败”,从“追求零故障”到“快速自愈”。

写在最后:35岁还在写代码,值不值?

有人问我,年纪不小了还折腾这些新东西干嘛?不如转管理。

但每次看到K8s自动拉起崩溃的Pod,看到Grafana上平稳的曲线,看到用户反馈“系统变快了”——那种亲手解决问题的快感,是开会画PPT给不了的。

而且说实话,懂云原生的35岁程序员,比只会CRUD的25岁新人更抢手。上周猎头开价比我现薪资高40%,就因为我简历写了“Istio + K8s + Prometheus全链路落地”。

所以别信什么“35岁危机”。只要保持折腾,键盘敲得动,Vim配得顺,咱们老码农照样能打。

对了,如果你也在搞架构升级,推荐几个救命教程:

  • 《Cloud Native Patterns》by Cornelia Davis(别被厚吓到,前五章就够用)
  • B站“云原生实验室”系列(中文讲Istio最清楚)
  • GitHub搜“k8s-the-hard-way”(理解原理必看)

至于面试题?记住:面试官不关心你用什么技术,只关心你怎么解决问题。把我上面踩的坑讲清楚,offer基本稳了。

(完)


作者:沪漂老码农,Vim党,目前沉迷用kubectl get pods -w看Pod重生。个人博客不定期更新,内容包含但不限于:如何用Vim写K8s YAML、35岁如何不被优化、以及为什么前端动画比后端架构简单一万倍。

评论 0

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