从“大泥球”到“乐高积木”聊聊我们团队用Docker和K8s重构单体应用的踩坑实录三年前接手公司核心业务系统时那个用Spring Boot编写的庞然大物已经积累了超过50万行代码。每次发布新版本都像在走钢丝——测试环境跑得顺畅的生产环境必定崩溃开发本地能用的功能部署到服务器就报依赖缺失。更可怕的是某个业务模块的CPU飙高会导致整个系统瘫痪扩容只能整体横向复制资源利用率长期低于30%。这就是典型的大泥球架构Big Ball of Mud所有组件像被随意揉捏的泥团般紧密耦合任何修改都可能引发连锁反应。1. 为什么我们必须对单体应用动刀1.1 那些年我们吃过的苦头环境差异陷阱新同事入职第一周都在配环境某次发现生产环境的JDK小版本差异导致SSL握手失败部署噩梦发布需要78分钟其中45分钟在解决jar包冲突资源浪费促销期间支付模块需要10台服务器但其他模块只用得到其中3台的资源技术栈锁死想引入新框架时发现与老系统存在不可调和的依赖冲突关键指标对比重构前指标数值平均部署时间78分钟生产环境事故频率2.3次/周资源平均利用率28%新功能上线周期3-4周1.2 技术选型的十字路口我们评估了三种改造方案# 方案1模块化改造最低成本 $ git submodule add xxx # 尝试用Git子模块分离代码 # 方案2传统微服务Spring Cloud体系 $ spring init --dependenciesweb,cloud-config my-service # 方案3容器化微服务DockerK8s $ docker build -t myapp . kubectl apply -f deployment.yaml最终选择第三条路的核心考量环境一致性Docker镜像打包所有依赖渐进式改造可以逐个服务拆分弹性伸缩K8s的HPA能按需分配资源技术中立性每个服务可独立选择技术栈2. 第一块积木从单体拆出订单服务2.1 外科手术式的代码分离我们选择订单模块作为第一个拆分对象因为它业务边界清晰有独立的领域模型对外接口明确RESTful API约20个数据耦合度低仅需同步用户基础信息关键操作步骤在单体工程中建立order-service模块用JPA过滤器隔离订单相关表引入FeignClient处理服务间调用编写Dockerfile多阶段构建# 构建阶段 FROM maven:3.8-jdk-11 AS build COPY . /app RUN mvn -f /app/pom.xml clean package # 运行阶段 FROM openjdk:11-jre-slim COPY --frombuild /app/target/*.jar /app.jar ENTRYPOINT [java,-jar,/app.jar]2.2 踩坑记录数据库连接池风暴拆分后首次大促时订单服务突然雪崩。根本原因原单体共用150个连接池拆分后各服务默认创建100连接导致数据库连接数超过max_connections限制解决方案# application.yml优化配置 spring: datasource: hikari: maximum-pool-size: 20 # 根据实际压力测试调整 connection-timeout: 30003. 当容器数量突破临界点K8s登场3.1 从Docker Compose到Kubernetes当服务数量超过8个时出现以下问题手工管理容器启动顺序痛苦跨主机网络通信不稳定监控日志分散难以排查迁移过程关键步骤用Kompose转换docker-compose.yml设计合理的Namespace划分platform基础组件MySQL/Redis等business业务微服务monitoring监控体系配置资源限制与探针apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: order-service resources: limits: cpu: 2 memory: 2Gi requests: cpu: 0.5 memory: 512Mi livenessProbe: httpGet: path: /actuator/health port: 80803.2 网络拓扑的暗礁某次上线后服务出现间歇性超时最终定位到默认的kube-proxy使用iptables模式当Service超过1000个时规则匹配效率下降部分请求被随机丢弃优化方案# 切换为IPVS模式 $ kubectl edit configmap -n kube-system kube-proxy # 修改mode: ipvs4. 持续交付流水线的进化4.1 从Jenkins到GitOps旧部署流程开发提交代码 → 触发Jenkins任务构建Docker镜像 → 推送到私有仓库手动执行kubectl apply改进后的GitOps流程graph LR A[代码提交] -- B[CI构建镜像] B -- C[更新Helm Chart版本] C -- D[Argo CD自动同步]实际落地时需注意镜像tag必须包含commit hashHelm values分环境管理设置合理的同步策略如仅允许生产环境手动确认4.2 监控体系的升级原先的三件套Spring Boot ActuatorPrometheusGrafana在微服务场景下暴露出问题指标维度不足缺少K8s层面监控日志关联困难跨服务追踪链路过长新监控方案组件层级工具功能重点基础设施Node Exporter节点资源使用率容器cAdvisor容器CPU/内存服务MicrometerJVM/业务指标日志LokiELK分布式日志收集全链路Jaeger请求跨服务追踪5. 一年后的效果与反思关键指标对比重构后指标重构前重构后部署时间78分钟8分钟生产事故频率2.3次/周0.2次/周资源利用率28%65%扩容效率手动30分钟自动2分钟回头看这段历程有三点特别值得分享不要追求完美架构我们曾纠结于服务粒度后来发现按团队边界划分更实际技术债要可控初期允许部分服务共享数据库但必须明确迁移计划人的因素最关键建立了每周的故障复盘会和技术分享轮值制度最意外的收获是当把20多个服务通过K8s的Ingress统一暴露时原本复杂的Nginx配置变得异常简洁apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: rules: - http: paths: - path: /api/(.*) pathType: Prefix backend: service: name: gateway-service port: number: 8080
从“大泥球”到“乐高积木”:聊聊我们团队用Docker和K8s重构单体应用的踩坑实录
从“大泥球”到“乐高积木”聊聊我们团队用Docker和K8s重构单体应用的踩坑实录三年前接手公司核心业务系统时那个用Spring Boot编写的庞然大物已经积累了超过50万行代码。每次发布新版本都像在走钢丝——测试环境跑得顺畅的生产环境必定崩溃开发本地能用的功能部署到服务器就报依赖缺失。更可怕的是某个业务模块的CPU飙高会导致整个系统瘫痪扩容只能整体横向复制资源利用率长期低于30%。这就是典型的大泥球架构Big Ball of Mud所有组件像被随意揉捏的泥团般紧密耦合任何修改都可能引发连锁反应。1. 为什么我们必须对单体应用动刀1.1 那些年我们吃过的苦头环境差异陷阱新同事入职第一周都在配环境某次发现生产环境的JDK小版本差异导致SSL握手失败部署噩梦发布需要78分钟其中45分钟在解决jar包冲突资源浪费促销期间支付模块需要10台服务器但其他模块只用得到其中3台的资源技术栈锁死想引入新框架时发现与老系统存在不可调和的依赖冲突关键指标对比重构前指标数值平均部署时间78分钟生产环境事故频率2.3次/周资源平均利用率28%新功能上线周期3-4周1.2 技术选型的十字路口我们评估了三种改造方案# 方案1模块化改造最低成本 $ git submodule add xxx # 尝试用Git子模块分离代码 # 方案2传统微服务Spring Cloud体系 $ spring init --dependenciesweb,cloud-config my-service # 方案3容器化微服务DockerK8s $ docker build -t myapp . kubectl apply -f deployment.yaml最终选择第三条路的核心考量环境一致性Docker镜像打包所有依赖渐进式改造可以逐个服务拆分弹性伸缩K8s的HPA能按需分配资源技术中立性每个服务可独立选择技术栈2. 第一块积木从单体拆出订单服务2.1 外科手术式的代码分离我们选择订单模块作为第一个拆分对象因为它业务边界清晰有独立的领域模型对外接口明确RESTful API约20个数据耦合度低仅需同步用户基础信息关键操作步骤在单体工程中建立order-service模块用JPA过滤器隔离订单相关表引入FeignClient处理服务间调用编写Dockerfile多阶段构建# 构建阶段 FROM maven:3.8-jdk-11 AS build COPY . /app RUN mvn -f /app/pom.xml clean package # 运行阶段 FROM openjdk:11-jre-slim COPY --frombuild /app/target/*.jar /app.jar ENTRYPOINT [java,-jar,/app.jar]2.2 踩坑记录数据库连接池风暴拆分后首次大促时订单服务突然雪崩。根本原因原单体共用150个连接池拆分后各服务默认创建100连接导致数据库连接数超过max_connections限制解决方案# application.yml优化配置 spring: datasource: hikari: maximum-pool-size: 20 # 根据实际压力测试调整 connection-timeout: 30003. 当容器数量突破临界点K8s登场3.1 从Docker Compose到Kubernetes当服务数量超过8个时出现以下问题手工管理容器启动顺序痛苦跨主机网络通信不稳定监控日志分散难以排查迁移过程关键步骤用Kompose转换docker-compose.yml设计合理的Namespace划分platform基础组件MySQL/Redis等business业务微服务monitoring监控体系配置资源限制与探针apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: order-service resources: limits: cpu: 2 memory: 2Gi requests: cpu: 0.5 memory: 512Mi livenessProbe: httpGet: path: /actuator/health port: 80803.2 网络拓扑的暗礁某次上线后服务出现间歇性超时最终定位到默认的kube-proxy使用iptables模式当Service超过1000个时规则匹配效率下降部分请求被随机丢弃优化方案# 切换为IPVS模式 $ kubectl edit configmap -n kube-system kube-proxy # 修改mode: ipvs4. 持续交付流水线的进化4.1 从Jenkins到GitOps旧部署流程开发提交代码 → 触发Jenkins任务构建Docker镜像 → 推送到私有仓库手动执行kubectl apply改进后的GitOps流程graph LR A[代码提交] -- B[CI构建镜像] B -- C[更新Helm Chart版本] C -- D[Argo CD自动同步]实际落地时需注意镜像tag必须包含commit hashHelm values分环境管理设置合理的同步策略如仅允许生产环境手动确认4.2 监控体系的升级原先的三件套Spring Boot ActuatorPrometheusGrafana在微服务场景下暴露出问题指标维度不足缺少K8s层面监控日志关联困难跨服务追踪链路过长新监控方案组件层级工具功能重点基础设施Node Exporter节点资源使用率容器cAdvisor容器CPU/内存服务MicrometerJVM/业务指标日志LokiELK分布式日志收集全链路Jaeger请求跨服务追踪5. 一年后的效果与反思关键指标对比重构后指标重构前重构后部署时间78分钟8分钟生产事故频率2.3次/周0.2次/周资源利用率28%65%扩容效率手动30分钟自动2分钟回头看这段历程有三点特别值得分享不要追求完美架构我们曾纠结于服务粒度后来发现按团队边界划分更实际技术债要可控初期允许部分服务共享数据库但必须明确迁移计划人的因素最关键建立了每周的故障复盘会和技术分享轮值制度最意外的收获是当把20多个服务通过K8s的Ingress统一暴露时原本复杂的Nginx配置变得异常简洁apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: rules: - http: paths: - path: /api/(.*) pathType: Prefix backend: service: name: gateway-service port: number: 8080