ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Kubernetes从入门到精通】第87篇:微服务应用K8s化改造实战——从传统部署到云原生

【Kubernetes从入门到精通】第87篇:微服务应用K8s化改造实战——从传统部署到云原生 上一篇【第86篇】生产就绪检查清单——你的K8s集群真的可以上线吗下一篇【第88篇】多集群管理实战——从单集群到联邦集群的进化之路摘要前面86篇把K8s的组件、原理、运维都讲透了但很多同学还是犯愁道理我都懂可我那个跑了三年的Spring Boot应用到底怎么搬上去这篇就干一件实事——把一个传统微服务应用原封不动但不完全原样搬上K8s。你会看到Dockerfile怎么写才不笨重、配置怎么从硬编码变成ConfigMap、健康检查怎么从手写接口变成K8s探针、日志怎么从写文件变成标准输出、最后用一套DeploymentServiceIngressConfigMap把服务跑起来并用金丝雀发布灰度上线。这不是Hello World是真实项目组踩过坑的迁移清单。一、改造前的样子一个典型传统微服务先看看我们要迁移的对象。一个常见的Spring Boot微服务传统部署长这样【传统部署的三件套】 源码包 order-service.jar 配置文件 application.yml (数据库地址/端口/日志路径写死) 启动脚本 nohup java -jar order-service.jar 部署方式: 1. 登录服务器 2. scp jar包上去 3. 改 application.yml 4. kill 旧进程, 启动新进程 5. 祈祷别出事这种玩法在单机能跑但问题一堆配置散落在各台机器、扩缩容靠手工、回滚基本靠再发一版、一台机器挂了服务就瘫。要点K8s化改造的核心不是把jar塞进容器而是把环境差异和运维动作从人身上转移到声明式YAML里。配置外迁、探针接管健康、副本由控制器保证、路由由Ingress统一。二、第一步Dockerfile多阶段构建传统做法很多人直接FROM openjdk:8然后把jar拷进去镜像动辄600MB还把构建工具带进去了。正确姿势是多阶段构建# 阶段一构建编译环境最后扔掉 FROM maven:3.8-openjdk-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline # 提前拉依赖利用层缓存 COPY src ./src RUN mvn package -DskipTests -o # 阶段二运行只带运行期需要的东西 FROM eclipse-temurin:17-jre WORKDIR /app # 用非root用户跑安全对应第055篇SecurityContext RUN groupadd -r app useradd -r -g app app COPY --frombuild /app/target/order-service.jar ./app.jar USER app EXPOSE 8080 # 优雅退出交给K8s的SIGTERM对应第035篇 ENTRYPOINT [java, -jar, -XX:ExitOnOutOfMemoryError, ./app.jar]几个关键点多阶段构建工具和源码不进最终镜像体积砍掉一大半。层缓存先COPY pom.xml单独拉依赖改业务代码时不用重新下载整个依赖树。非root用USER app跑避免容器里是root第055篇讲过的SecurityContext思想在镜像层先落实。基础镜像固定别用latest用eclipse-temurin:17-jre这种带版本/ digest的可重现、可扫描。要点镜像越小调度越快、节点磁盘压力越小、CVE攻击面越小。docker images里那些800MB的java镜像基本都是没做多阶段构建的祖传镜像。构建并推到仓库# 构建注意加上构建上下文和标签dockerbuild-tregistry.example.com/order-service:1.4.2.# 推送dockerpush registry.example.com/order-service:1.4.2# 顺手扫个漏洞第059篇trivy image registry.example.com/order-service:1.4.2三、第二步配置外迁——干掉硬编码传统Spring Boot把数据库地址写死在application-prod.yml。上K8s后配置必须通过ConfigMap注入做到镜像不变、环境变。# configmap.yaml —— 把配置从代码里拔出来apiVersion:v1kind:ConfigMapmetadata:name:order-service-confignamespace:shopdata:application.yml:|server: port: 8080 spring: datasource: url: jdbc:mysql://mysql.shop.svc:3306/orders username: ${DB_USER} password: ${DB_PASSWORD} redis: host: redis.shop.svc management: endpoints: web: exposure: include: health,info,metrics # 给探针和监控用Secret里的敏感信息密码单独放不进ConfigMap第021/057篇apiVersion:v1kind:Secretmetadata:name:order-service-secretnamespace:shoptype:OpaquestringData:DB_USER:order_appDB_PASSWORD:S3cret#2026# 生产请用Vault/External Secrets启动时把ConfigMap挂成文件Spring Boot天然读application.yml# 在Deployment的容器里env:-name:DB_USERvalueFrom:secretKeyRef:name:order-service-secretkey:DB_USER-name:DB_PASSWORDvalueFrom:secretKeyRef:name:order-service-secretkey:DB_PASSWORDvolumeMounts:-name:configmountPath:/app/config# Spring Boot默认会读 config/application.ymlvolumes:-name:configconfigMap:name:order-service-config要点配置外迁后同一个镜像能在dev/test/prod跑只是挂不同的ConfigMap。这也让改配置不用重新打包回滚配置只是换一个ConfigMap版本配合Reloader还能热加载。四、第三步健康检查改造成探针传统应用常自己写个/health接口然后靠外部脚本去curl。K8s原生支持更精细的三种探针第034篇详细讲过直接接管这块livenessProbe:# 活不下去就重启httpGet:path:/actuator/health/livenessport:8080initialDelaySeconds:30# 给Spring Boot启动留时间periodSeconds:10failureThreshold:3readinessProbe:# 没就绪就不接流量httpGet:path:/actuator/health/readinessport:8080periodSeconds:5failureThreshold:2startupProbe:# 启动慢的保护伞老应用启动要2分钟httpGet:path:/actuator/health/livenessport:8080failureThreshold:30periodSeconds:5# 最多容忍 30*5150秒启动Spring Boot Actuator 自带health端点配合liveness/readiness分组Spring Boot 2.3刚好对应K8s语义。【探针分工别再混为一谈】 liveness → 我还活着吗? 失败杀掉重启 readiness → 我能接活吗? 失败摘流量(不杀) startup → 启动完了吗? 期间屏蔽前两个 典型惨案: 把 readiness 当 liveness 用 → 依赖的Redis抖一下, Pod被反复杀, 雪崩要点startupProbe是老应用上K8s的救星。很多祖传服务启动要一两分钟没有startupProbe时liveness会在启动期就误杀Pod陷入无限重启循环。五、第四步日志标准化——别再写文件传统应用把日志写到/var/log/order-service/app.log。容器里写文件有两个坑Pod一删日志没了多个副本日志分散。K8s的约定是标准输出(stdout/stderr)让日志采集层DaemonSet第076篇统一收。# 在Spring Boot的 logback-spring.xml 里appender nameCONSOLE classch.qos.logback.core.ConsoleAppenderencoderpattern%d{yyyy-MM-dd HH:mm:ss}[%thread]%-5level %logger{36}-%msg%n/pattern/encoder/appenderroot levelINFOappender-ref refCONSOLE /!--只打控制台--/root然后Deployment里一个emptyDir都不用挂日志盘日志自然进stdout被kubelet捕获# 看某个Pod的日志kubectl logs-nshop deploy/order-service--tail100# 多副本全看--prefix标副本名kubectl logs-nshop deploy/order-service--prefix-f# 再用PLG(第076篇)集中检索要点日志上云原生的第一原则——应用只管往stdout打落盘和轮转交给采集器和节点。这跟传统应用自己管日志文件是思维模式的切换。六、全套YAML把服务真正跑起来把上面所有片段拼成一个完整Deployment再补Service和IngressapiVersion:apps/v1kind:Deploymentmetadata:name:order-servicenamespace:shoplabels:app:order-servicetier:backendspec:replicas:3selector:matchLabels:app:order-servicestrategy:rollingUpdate:maxSurge:1maxUnavailable:0# 金丝雀/滚动时零中断template:metadata:labels:app:order-serviceversion:1.4.2spec:containers:-name:order-serviceimage:registry.example.com/order-service:1.4.2ports:-containerPort:8080resources:requests:cpu:200mmemory:512Milimits:cpu:1memory:1Gienv:-name:DB_USERvalueFrom:secretKeyRef:{name:order-service-secret,key:DB_USER}-name:DB_PASSWORDvalueFrom:secretKeyRef:{name:order-service-secret,key:DB_PASSWORD}volumeMounts:-{name:config,mountPath:/app/config}livenessProbe:httpGet:{path:/actuator/health/liveness,port:8080}initialDelaySeconds:30periodSeconds:10readinessProbe:httpGet:{path:/actuator/health/readiness,port:8080}periodSeconds:5startupProbe:httpGet:{path:/actuator/health/liveness,port:8080}failureThreshold:30periodSeconds:5volumes:-name:configconfigMap:name:order-service-config---apiVersion:v1kind:Servicemetadata:name:order-servicenamespace:shopspec:selector:app:order-serviceports:-port:80targetPort:8080---apiVersion:networking.k8s.io/v1kind:Ingressmetadata:name:order-servicenamespace:shopannotations:nginx.ingress.kubernetes.io/rewrite-target:/spec:ingressClassName:nginxrules:-host:shop.example.comhttp:paths:-path:/api/orderpathType:Prefixbackend:service:name:order-serviceport:number:80要点maxUnavailable: 0maxSurge: 1是零中断发布的黄金组合——滚动更新时先起新Pod再下旧Pod永远有副本在接流量。配合readinessProbe新Pod没就绪前不会进负载均衡。七、灰度上线金丝雀发布实战直接全量发布新版本太莽。用Service的selector天然支持金丝雀——给一小撮Pod打不同version标签【金丝雀思路(无需额外工具)】 流量 → Service(order-service) ├── 选择 apporder-service (含v1.4.2 和 v1.5.0) └── 但 v1.4.2 有 9个, v1.5.0 只有 1个 → 约10%流量落到新版本, 观察无异常再全量 更精细: 用 Ingress/Nginx 按 header 或权重切(第018/077篇)# 先发1个新版本副本(独立Deployment或同Deployment调标签)kubectlsetimage deploy/order-service order-serviceregistry.example.com/order-service:1.5.0# 但更稳的是用两个Deployment分别控副本数(1:9)# 金丝雀Deployment replicas:1, 稳定Deployment replicas:9# 两者label都带 apporder-service, 共享同一个Service# 观察指标(第075篇)kubectl get pods-nshop-lapporder-servicecurl-sshop.example.com/api/order/health确认新版本稳了再把稳定Deployment也升到1.5.0下线金丝雀Deployment。需要更高级的按权重/按用户灰度上Argo Rollouts或Istio第077/078篇。八、改造前后对比维度传统部署K8s化之后配置写死在yml散落各机ConfigMap/Secret注入镜像不变扩缩容手工加机器scpkubectl scale或HPA自动健康检查自己写脚本curlliveness/readiness/startup探针日志写本地文件stdout采集层统一收发布kill启动可能中断滚动更新零中断回滚再发一版kubectl rollout undo秒回灰度基本没有金丝雀/Service分流本篇小结把一个传统Spring Boot微服务搬上K8s本质是四件接管Dockerfile多阶段构建接管打包镜像小、可重现、ConfigMap/Secret接管配置镜像不变环境变、探针接管健康检查K8s自管生死、stdout接管日志采集层统一收。再靠DeploymentServiceIngress把它们串成一套声明式YAML最后用金丝雀发布灰度上线、kubectl rollout undo秒回滚。这不是把jar塞进容器那么简单而是把运维动作从人手搬进声明。一个服务改完扩缩容、回滚、灰度全都有了。下篇我们换个更大的视角当单个集群不够用多集群怎么管。上一篇【第86篇】生产就绪检查清单——你的K8s集群真的可以上线吗下一篇【第88篇】多集群管理实战——从单集群到联邦集群的进化之路
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进