
1. 先说清楚一个写Java的为什么要碰K8s和Jenkins“我一个写Java的怎么就开始玩K8s和Jenkins了”这句话是我去年年底在技术分享会上开场白的第一句话。当时台下坐着的十几个后端同事全都笑了因为大家都知道我在这之前是个连Docker都没认真用过的人日常工作是写Spring Boot接口、调JVM参数、背八股文偶尔研究一下AQS源码和StringBuilder的扩容逻辑。但形势比人强团队里的微服务从十几个拆到了六十多个测试环境的管理越来越乱每次发版少说两三个小时线上出问题回滚要靠人肉翻历史命令。老板在某次复盘会上直接拍板——后端组每个人必须能独立完成服务的发布和回滚。于是我这个以System.out.println为主要调试手段的传统Java选手被硬生生推进了Kubernetes和Jenkins的世界。这篇不是一本正经的官方文档翻译也不是K8s权威指南第五版PDF那样的系统教材就是一个Java后端程序员被现实毒打之后的真实迁移记录。我会从Java思维出发把“这玩意儿到底在干嘛”讲清楚再把集群怎么搭、Jenkins流水线怎么写、生产环境常见的故障怎么排查拆开讲细。适合看这篇文章的人写过一段时间Java后端、对容器化部署还处于“知道名字但没实操过”的状态、正在被要求接手发布和运维工作的同伴。如果你已经是运维老手可以跳过前两章直接看后面的踩坑合集。先说一个扎心的事实大部分Java面试题里不会考你怎么在K8s上发布一个服务但如果你连kubectl describe pod看到的CrashLoopBackOff都解释不了那你写出来的接口再优雅在用户眼里也只是一个“总是登不上”的报错链接。八股文保住的是面试K8s和Jenkins保住的是你服务的命。1.1 痛到不得不改的现状我们团队原来的发版流程说出来你可能不信全靠聊天软件通知A同学负责编译打包B同学负责把jar包传到指定机器C同学负责登录服务器执行nohup java -jar xxx.jar D同学负责盯着日志看到“Started Application in x seconds”才敢在群里说一声“好了”。这套流程在十几个服务的时候勉强能跑毕竟人脑还能记住谁部署在哪台机器上。但服务数量一多问题就炸了jar包文件名一潮汐端口冲突一潮汐某台机器内存爆了没人知道测试环境的数据被反复初始化开发们排着队抢同一台部署机。最难忘的是某次线上发版四个服务要同时更新操作顺序错了先停了B服务结果A服务的接口批量报错用户端App直接“网络异常”。那会儿大家的第一反应不是查日志而是先在群里问“谁动了服务器”。就是这种手工作坊式的流程让我彻底意识到部署链路的问题比业务代码的问题更可怕。代码写得再谨慎只要发布动作是人工的风险永远在那里。1.2 传统部署和容器化部署的本质区别很多人以为学K8s就是多学几个命令、多写几个YAML文件其实不是。真正要换的是思维模型。我画过一张对比表给我自己看也给我们团队的新人看维度传统服务器部署K8s Jenkins 部署发布单元jar包 依赖的环境镜像代码 运行时 系统依赖打包部署动作SSH上传 kill进程 启动声明期望状态由控制器自动收敛回滚方式找到旧包、重新启动换镜像tag、自动滚动回滚健康检查人盯着日志探针自动判断不健康自动重启/摘流量资源管理一服务器一jar靠人记按request/limit调度自动分配扩容缩容再开一台机器手动部署改replicas或者HPA自动扩缩这个表格里最关键的一点是倒数第二行。过去Java服务扩容本质上就是“再找一台机器把Java进程跑起来”。现在Pod这个概念相当于把“机器 操作系统 Java运行时 你的应用”打包成了一个标准盒子K8s负责决定盒子放在哪、开几个、挂了怎么处理。你不需要再关心IP是多少、进程在哪个机器上只描述“我要3个实例”其他的交给调度器。这就像你从“自己开手动挡面包车送货”变成了“给物流平台提交需求单三辆车、常温、按时出发”。平台怎么调度车、从哪个仓库装货不需要你操心。Java程序员以前是造车的现在更重要的能力是写对需求单。1.3 这篇文章的定位与个人学习路径说句实话Java后端转容器化这条路上最大的障碍不是语法是知识体系的映射问题。K8s文档写得很全但它是给运维视角的人写的术语密度太高什么DaemonSet、StatefulSet、Operator、CRD第一次看容易懵。而我作为Java开发者脑子里全是类、对象、接口、容器、Spring、配置文件这些概念所以我的做法很笨但很有效一个个把K8s的关键名词“翻译”成Java世界的对应物然后照着原来写接口的习惯去写YAML。这也是这篇文章的主线。先聊为什么再聊怎么做最后聊踩过哪些坑。我会把我用过的东西、踩过的坑、排查的思路尽量细致地写出来希望对正在被同样问题折磨的人有点帮助。2. 用Java思维啃下K8s核心概念2.1 一张Java类比表把K8s对象翻译成熟悉的话不懂术语就看不懂排障信息。这里先给一张我自己的笔记表如果你也是Java背景建议先把它过一遍再往下看K8s概念Java类比说明PodSpring容器里的Bean实例最小运行单元一个Pod里可以有多个容器类似一个Bean里注入多个组件共享网络和存储Deployment一个Controller类定义期望状态几个副本、用什么镜像、如何更新。它的职责相当于Spring里的动态代理保证实际状态向期望状态收敛ServiceRPC服务注册中心的调用地址给一组Pod提供稳定的访问入口和负载均衡Pod IP会变Service VIP不变ConfigMapapplication.yml存配置不加密Secret加密配置项存密码、tokenbase64只是编码不是加密NamespaceJava包的命名空间逻辑隔离不同项目各用一个空间PVC/PV数据库连接池/文件存储Pod要用存储就申请PVCPV是实际存储资源类似连接池申请连接IngressNginx反向代理配置外部HTTP流量入口按域名/路径转发到ServiceHPA线程池的动态扩容策略根据CPU/内存指标自动调整Pod副本数Node一台物理机/JVM实例承载Pod的服务器节点kubeletJVM运行时每个Node上都有的代理进程负责管理本机Pod的生命周期etcdZooKeeper集群状态存储所有配置和状态都保存在这里我在构建这个类比表的过程中最大顿悟是Deployment其实很像我们写Java的“接口和实现分离”——你定义好期望状态相当于接口契约K8s的控制器相当于框架机制负责不断检查实际状态并调谐reconcile到符合契约。跟Spring的IoC容器有点像你不用手动管理Bean的创建和依赖关系只用告诉容器“有这个类有这些依赖”容器帮你搞定剩下的事。有了这个基础你去看K8s的面试题比如“Deployment、StatefulSet和DaemonSet的区别”就很容易理解Deployment适合无状态服务像普通Java接口StatefulSet适合有身份的实例像集群里的节点、有独立存储的应用DaemonSet适合每个节点都必须跑一个的组件像日志采集器。2.2 第一个Java服务的YAML逐字段拆解我们直接看一个Spring Boot服务的Deployment YAML这是你后续所有工作里最常用的“模板文件”。我先把完整的版本贴出来再逐块拆apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: backend labels: app: order-service spec: replicas: 3 selector: matchLabels: app: order-service strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1 template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.internal/backend/order-service:1.4.2 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http env: - name: JAVA_OPTS value: -Xms512m -Xmx512m - name: SPRING_PROFILES_ACTIVE value: k8s resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 1Gi readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 2 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 2 terminationMessagePath: /dev/termination-log先看template下面那段这是Pod的模板。containers里第一个字段是name不是镜像名而是容器名和Java类里的字段名类似后续配Service选择器时会用到。image就是镜像仓库地址加tag所谓“发版”本质就是换这个tag。resources是一个新手容易忽略的重头戏。requests是你对这个Pod的最低资源保障相当于你告诉调度器“我要有0.5个CPU和512MB内存才肯运行”limits是上限相当于硬性天花板。Java开发在这里有个很经典的坑如果你只设了limits.memory: 1Gi但JVM启动参数里写的是-Xmx2gK8s会在Java进程使用内存超过容器上限时把它杀掉表现就是Pod反复重启日志里却看不到异常堆栈。所以Java的堆大小和容器的内存limit一定要对齐后面故障章节我会细说。readinessProbe和livenessProbe是两个健康探针很多初学的人分不清。简单说readiness是“我准备好接流量了吗”失败时Service不会把请求转发到这个Podliveness是“我还活着吗”失败时K8s会杀掉并重建容器。对Java服务来说Spring Boot的/actuator/health接口天生就是做这个的。初始延迟时间要按实际启动速度调整我见过很多服务启动要四十多秒但initialDelaySeconds只设了10结果一启动就被误杀了。strategy里滚动更新策略也很重要。maxUnavailable: 1表示更新时最多允许一个Pod不可用maxSurge: 1表示最多额外多创建一个Pod。这个参数对于高峰期的服务来说很关键如果不设置默认就会先杀完再建造成短暂没有可用实例用户就会遇到请求失败。2.3 集群搭建与运行时选型笔记讲完YAML再讲一下我们当时怎么搭集群的。首先是运行时选型。早年间大家普遍用Docker作为容器运行时后来K8s从1.24版本开始默认删除了对Dockershim的支持Docker底层的容器运行时也被containerd替代。现在生产环境最推荐的是containerd你不需要像以前那样写dockerfile然后命令里处处带docker而是通过crictl或者直接交给K8s处理。搭建过程我用的方式是kubeadm。大概流程如下以三台机器为准一台Master、两台Node先在所有机器上安装基础组件内核参数调优一下把swap关掉加载br_netfilter等模块。然后初始化Master节点kubeadm init --control-plane-endpointmaster.internal:6443 \ --pod-network-cidr10.244.0.0/16 \ --apiserver-advertise-address192.168.1.10这里--pod-network-cidr是给Pod网段用的后面网络插件要跟这个网段保持一致。初始化完成后会输出一段kubeadm join的命令复制到Node节点上执行节点就能加入集群。网络插件我选的是Flannel理由很简单部署简单适合中小规模集群。它的原理是给每个节点分配一个子网段通过VXLAN实现Pod之间的互通。如果你的集群规模大、对网络性能敏感再用Calico。这里提醒一句网络插件必须在集群初始化后尽早部署不然节点状态会一直是NotReady。镜像拉取这块国内环境的坑比较老生常谈。kubeadm init的时候会自动拉一些K8s组件镜像如果拉不下来需要先配好镜像加速或镜像源。操作方法是在containerd配置里设置registry.mirrors指向你常用的加速地址。这一步很关键跳过的话大概率倒在这一步我就是在这卡了半天。关于集群高可用生产环境至少要有三台Master加上外部负载均衡我们测试阶段一台Master也就够了。但要做好备份etcd的脚本这一点千万不要省。我后来把etcd快照备份加入了Jenkins的定时任务里才觉得自己不是纯粹裸奔。3. Jenkins自动部署落地从手动发布到一条流水线3.1 Jenkins和K8s的关系Master、Agent与动态Pod以前我对Jenkins的理解就是“一个自动打包工具”直到我真正上手才明白它更准确的身份是“CI/CD调度中心”。Jenkins里有两条核心脉络一条是Master/Agent架构Master负责任务调度、监控展示和权限管理Agent才是真正干活的工人另一条是流水线Pipeline把拉代码、编译、测试、打包、部署这些动作编成一套可重复执行的流程。刚开始用的时候我们的Agent还是固定的一台虚拟机上面装好JDK、Maven、Docker。那时候每次执行构建都感觉是“一台老爷车在运货”构建时间一长就焦虑。后来我们给Jenkins配了Kubernetes Cloud插件让Jenkins的Agent不是一台固定机器而是按需在K8s集群里动态创建一个Pod构建完就自动销毁。这个方案的好处非常明显构建任务多时多开几个Pod任务少时零资源占用跟我们Java线程池弹性的逻辑一模一样。你可能会问为什么不用GitLab CI或者其他云流水线一句话回答团队现状决定工具选型。我们仓库在GitLab、镜像在Harbor、集群已经搭好Jenkins作为中间的调度者插件生态成熟、文档多、老工程师熟悉迁移成本最低。工具之争永远是次要的解决发布可靠性的问题才是首要的。3.2 一条Java应用流水线逐段拆解直接上一个我们项目里拿得出手的代码片段这是一条完整但精简的声明式Pipeline发布的就是前面那个order-servicepipeline { agent any environment { REGISTRY registry.internal IMAGE_NAME backend/order-service NAMESPACE backend HARBOR_CRED harbor-credentials GIT_CRED gitlab-credentials } stages { stage(Checkout) { steps { git credentialsId: ${GIT_CRED}, url: http://gitlab.internal/backend/order-service.git } } stage(Build Test) { steps { sh mvn clean package -DskipTestsfalse } } stage(Build Push Image) { steps { script { docker.withRegistry(http://${REGISTRY}, ${HARBOR_CRED}) { def image docker.build(${IMAGE_NAME}:${BUILD_NUMBER}) image.push() } } } } stage(Deploy to K8s) { steps { sh kubectl set image deployment/order-service order-service${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER} -n ${NAMESPACE} --record } } stage(Smoke Test) { steps { sh curl -sf http://order-service.backend.svc.cluster.local:8080/actuator/health || exit 1 } } } }每个stage的解释Checkout不用多说就是拉代码。Build Test跑一遍mvn clean package -DskipTestsfalse这里我不建议跳过单测实时上毕竟有些坑就是测试阶段就能发现的等上了环境再爆雷代价更大。接下来是镜像构建。灵活用的docker.withRegistry是Jenkins的Docker插件提供的意思是在这段脚本执行期间docker命令自动带上这个registry的登录凭据。镜像tag这里用了${BUILD_NUMBER}也就是Jenkins构建序号。好处是能保证每次构建的镜像tag都是唯一的避免“同一个tag被覆盖”的混乱问题还能跟构建记录对应上。但生产环境如果用这套tag的可读性会差一些很多团队会用版本号commitId看团队习惯。最后的Deploy to K8s用了kubectl set image命令意思很直白把某个Deployment里名为order-service的容器的镜像改成新tag。用这个命令而不是kubectl apply -f deployment.yaml是因为它只更新镜像不会因为本地YAML和集群内YAML有偏差而误覆盖其他配置。这个区别值得写进团队规范里。Smoke Test是冒烟测试发布完成之后打一下健康检查接口确认服务真的活了才正式结束这条流水线。最初我们没加这一步结果有几次镜像tag写错了或者Harbor同步慢发布流程显示成功线上却还是旧版本在跑用户开始反馈问题才暴露出来。加了这一步之后就稳很多。3.3 容器内使用docker命令的三种解法用Jenkins动态Pod当Agent之后会遇到一个很现实的问题Pod里哪来的docker命令Pod是个隔离环境里面只有你需要的构建工具并没有operating system级别的全套二进制。这里有三条路线第一种把宿主机的/var/run/docker.sock挂载进Pod里。这种方案最简单Pod里的docker命令实际连的是宿主机的Docker Daemon构建出的镜像也在宿主机上。缺点是你失去了隔离性Pod里操作宿主机docker的能力相当于root这在那达工业级安全要求下是不太能接受的。第二种Docker in DockerDinD。在Pod里再跑一个Docker Daemon容器这样做隔离性稍好但嵌套一层Docker性能和稳定性都有损耗而且存储驱动、权限问题会让你额外掉很多头发。第三种是现在推荐的方向不用Docker Daemon而是用Kaniko这类工具直接在容器内无守护进程地构建镜像。Kaniko的实现原理是在用户空间逐层解包和执行构建命令最终生成镜像并推送到仓库整个过程不需要docker.sock也不需要DinD。我们后来改造的Pipeline里构建镜像那段就换成了KanikoJenkinsfile里调用方式类似cat EOF | /kaniko/executor \ --context/workspace \ --dockerfileDockerfile \ --destinationregistry.internal/backend/order-service:${BUILD_NUMBER} \ --context-sub-path. EOF这套方案在生产环境实际运行下来很稳构建速度不会比docker build慢太多但安全和隔离方面让人放心很多。如果你准备把Jenkins工作流彻底容器化建议直接用Kaniko的思路别再回头纠结docker.sock了。这里也补充一个东西Jenkins 2.541.3之后的版本在系统配置里可以直接配置Kubernetes Cloud填好集群地址、凭据、Pod模板后新建Job时agent部分就能指定kubernetes。人和岗直接对应很方便。如果你用的是旧一点的版本查一下对应版本号的配置路径大差不差。4. K8s生产环境常见故障排查实录4.1 先掌握排查顺序Events、Logs、Describe三板斧作为Java开发者遇到问题第一反应是看日志、打堆栈、找异常。这个直觉没有错但在K8s世界要先重新排序。因为很多问题在到达Java进程之前就已经被K8s拦截了。排障的正确顺序在我看来是这样第一步看Pod状态和事件。kubectl get pods -n backend看到有个Pod是CrashLoopBackOff接着kubectl describe pod order-service-xxxx看下面Events区域里写了什么。这个命令的信息密度极高Pending的原因、镜像拉取失败的消息、健康检查失败的次数、OOMKilled的判断全在这里面。相当于你的应用启动之前K8s已经帮你做了一轮体检。第二步看容器日志。kubectl logs -f pod名称 --previous当容器反复重启时拿--previous看上一次退出前的日志往往能捕捉到真正的错误堆栈。我们Java服务经常出现Spring Boot在启动过程中发现数据库连不上打印一堆异常然后进程退出这时候logs --previous是能看到完整的异常栈的。第三步如果网络相关进到容器内部或者用调试Pod去探测。kubectl exec -it pod名称 -- bash进入容器里看环境变量、配置文件、网络连接。但注意基础镜像里未必有bash所以最好的办法是用kubectl debug临时挂一个带工具的容器。为什么要先说工具链顺序因为很多人一上来就kubectl logs结果发现容器根本还没启动起来日志是空的就懵了。先看Events再决定看不看日志这个顺序能帮你省一个小时。4.2 Java服务在K8s里最容易踩的四个状态下面这张表是我把我们生产环境中踩过的坑浓缩出来的高频故障速查表每个都配了原因和排查思路。建议先收藏真遇到问题的时候翻出来对号入座。现象可能原因排查命令解决建议Pod一直PendingNode资源不足、污点容忍度不够kubectl describe pod看Eventskubectl top node看水位加Node或清理孤儿Pod检查污点/亲和性配置ImagePullBackOff镜像tag不存在、私有仓库认证失败、仓库地址不通kubectl describe pod里会直接写拉取报错检查tag是否正确确认imagePullSecret配置测试仓库连通性CrashLoopBackOffJava进程启动即失败、探针误判、依赖服务未就绪kubectl logs --previous看退出前日志describe看失败原因修复启动配置调整initialDelaySeconds确认下游依赖OOMKilled容器内存limit过小、JVM堆与容器限制不匹配kubectl describe pod看到OOMKilled事件调整limit-Xmx和容器memory对齐排查内存泄漏Service访问不通selector不匹配、targetPort选错、endpoints为空kubectl get endpoints 服务名kubectl get svc -o wide检查selector标签和Pod标签改名endpointsDNS解析失败CoreDNS故障、Pod里resolv.conf异常kubectl get pods -n kube-system看CoreDNS进入Pod内cat /etc/resolv.conf重启CoreDNS或检查网络插件配置探针失败导致重启readiness/liveness路径不对、启动时间不足kubectl describe看到Liveness probe failed用准确的健康路径调整initialDelaySeconds和periodSeconds重点说一下CrashLoopBackOff和OOMKilled这两个因为这俩对Java后端来说最具有迷惑性。CrashLoopBackOff的意思是“容器启动后崩了然后K8s反复拉起失败次数太多后进入退避状态”。Java进程不像某些原生程序启动慢、依赖多经常被误判。常见原因是数据库还没完全就绪你的服务启动到一半连不上库就抛异常退出或者你的liveness探针写的是/actuator/health但忘了引入spring-boot-starter-actuator压根没有这个接口探针从第一次就失败。还有一个经典的坑在我这里发生两个服务之间用hostname互相调用但K8s里不同Pod的hostname是Pod名不是你想象的域名结果服务启动时DNS解析失败起不来。OOMKilled这个更值得写一段。Java 8u191版本之前JVM默认不感知容器内存限制它会把宿主机内存当成可用内存来设置默认堆大小。在容器limit为1Gi的机器上JVM看着宿主机有64G内存默认堆就给你开个几十G出来一启动就持续吃内存吃满直接把容器撑爆K8s判定超限杀掉进程。后来Java 10默认开启UseContainerSupport但老版本JDK仍然要手动加-XX:MaxRAMPercentage这类参数。我们在实践中的做法是在部署YAML里明确写JVM参数不让它猜env: - name: JAVA_OPTS value: -XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage50.0 -Xss512kMaxRAMPercentage75.0的意思是让JVM最多使用容器可用内存的75%剩下的留给堆外内存、Metaspace和线程栈。配这个参数比直接写死-Xmx更适配容器环境的弹性资源分配。4.3 Service访问、探针与优雅上下线的那些坑Java程序员的直觉里访问一个服务就写IP加端口。K8s里的Service抽象解决了服务发现问题但它也有自己的坑。第一个是externalIPs的问题。热词里出现了“k8s externalips”我在这上面栽过。K8s里Service有几种暴露方式ClusterIP集群内访问、NodePort节点端口映射、LoadBalancer云负载均衡器。externalIPs是一种“手动指定外部可用IP”的方式相当于你不用LoadBalancer直接把某个IP绑到Service上让外部流量能进来。但我实践中发现如果你的网络环境下这个IP没有被正确路由到节点那Service的externalIPs配置了也不通。后来我在物理网络场景下直接用NodePort加节点上的防火墙放行端口反而更省事。所以记住一句话externalIPs能否用取决于你的底层网络能不能把这个IP路由到K8s节点而不是取决于配置本身对不对。第二个是优雅上下线。最开始我们的滚动更新总会在凌晨发版时产生一批用户报错。原因是旧Pod收到终止信号后立刻被杀掉但它还在处理请求新流量又因为endpoints更新有延迟还是会打过来。解决方案是让Java应用支持优雅停机Spring Boot 2.3以上版本可以配置server.shutdowngraceful同时K8s侧加preStop钩子在容器被杀前sleep一段时间让正在处理的请求完成然后再真正退出。相关YAML片段spec: containers: - name: order-service lifecycle: preStop: exec: command: [sh, -c, sleep 15]这个sleep时间我们在实践中取的是15秒刚好能让K8s把Pod从endpoints里摘掉同时让已有的请求有足够时间处理完。加上readiness探针每次流量切换用户侧的感觉会是“丝滑”。这套组合是我们线上发布从“提心吊胆”变成“习惯性操作”的关键。第三个坑是Pod IP变化。我们服务里有用到Redis和数据库连接池最初某些连接池配置的maxLifetime设得很少导致Pod一重建就全部重连数据源预热的那波请求超时特别多。后来把连接池参数调大并在应用启动时加了一个最小空闲连接初始化的逻辑问题才缓解。所以说K8s里的故障并不仅仅是部署配置问题Java应用自身也要为“随时可能被重建”这个现实做出调整。5. 进阶方向与Java程序员的容器认知升级5.1 从能跑到能扛Operator、Helm、监控把服务成功部署上去只是及格线。要做到不慌还得往前走几步。热词里提到的“k8s中operator案例”这类问题的本质就是K8s原生的Deployment、Service这些资源表达能力是有上限的。比如你部署的是一个带数据备份的中间件需要在特定阶段做备份、扩节点、主从切换原生K8s是不知道这些业务逻辑的。Operator的思想就是把这些运维经验“写成代码”封装成控制器在K8s里以一种自定义资源CRD的方式运行。对应到Java世界相当于你给框架写了一套自定义注解和一个解析器由框架帮你在合适时机执行这些逻辑。这个思路对于Java开发者来说非常友好因为Operator本身就是用Go写的但理解它不需要额外背景它就是一段“事件循环 调谐逻辑”的代码监听集群里的资源变化然后执行对应动作。我们后来想管理一套自研的定时任务平台就给那套平台写了个简单Operator自定义了TaskRunner这种CRD每提交一个TaskRunner控制器就帮忙拉起对应的Job。Helm也是绕不开的。它的价值相当于Maven的依赖管理和模板引擎你的YAML里可以有变量通过values.yaml注入一套模板渲染出dev、test、prod三套配置。避免同一个Deployment文件在三个环境里维护三个副本改配置时漏一处就上一个环境。Helm的helm upgrade --install --atomic命令还能让发布失败时自动回滚这一点在生产环境非常香。至于监控建议优先上Prometheus加Grafana。热词里也有“部署prometheus监控k8s”我不展开做完整教程只说一下关键点Prometheus通过一组exporter采集指标比如kube-state-metrics采集集群资源对象的状态node-exporter采集节点指标cadvisor采集容器指标。应用层面Spring Boot的Actuator如果配上micrometer-registry-prometheus就能把接口响应时间、QPS、JVM状态都暴露成Prometheus格式的指标Grafana里做漂亮面板。这一套刚配置完的感觉就是你终于不再靠“用户说卡”来感知服务健康而是看着Grafana曲线提前预判。关于“k8s调用gpu”如果你以后在K8s里跑模型推理服务那就需要节点上安装NVIDIA驱动、配置nvidia-device-plugin然后在Pod的resources.limits里声明nvidia.com/gpu: 1。这个我们目前生产还没真正用上但调研过一条可行的路线是有的需要的时候再单独开一篇写。5.2 Jenkins侧的效率优化汉化、加速、动态AgentJenkins本身功能强大但默认界面确实熬人。先解决汉化问题在插件管理里搜索“Localization: Chinese (Simplified)”插件并安装然后重启Jenkins界面就变成中文了。这里顺便说一句有段时间新建的Jenkins版本里这个插件可能不在默认源列表里那大概率是插件源需要替换成国内镜像。在Manage Jenkins - Tools - Update Site里把默认的更新中心地址换成对应的国内镜像地址插件能下载了速度也上来了。这就是热词里提到的“jenkins插件加速器”的实际操作。凭据管理这块千万别把Harbor密码或GitLab的token直接塞进Jenkinsfile。Jenkins里Credentials - System - Global credentials添加凭据然后在Pipeline里用credentialsId引用。这相当于你代码里的密文配置永远不落地而是由调度中心统一保管泄露风险小很多。动态Agent这块前面讲过了实际操作时注意两个细节一是Pod模板里的镜像要提前打好里面装着JDK、Maven、kubectl、Docker CLI或Kaniko不然每次构建都要现场下载工具二是并发构建数要控制不然K8s集群会被瞬间拉起的构建Pod打满。我见到过有人把agent { kubernetes { yamlFile agent-pod.yaml } }写得很大方结果一次大版本发布十几个构建同时触发workerNode直接资源耗尽。我个人的做法是配好全局并发数和资源request/limit给构建任务一个上限。顺带提一下热词里的“jenkins ai agent”。新版Jenkins开始尝试集成AI能力比如通过自然语言辅助诊断Pipeline失败原因、生成代码片段之类。我试用过一小段时间目前它对“日志里常见的编译错误、测试失败”能给出还挺像样的建议但复杂的K8s联调问题还是得靠人。这东西作为辅助可以别指望它救火。5.3 Java思维需要更新在哪几个点最后这节聊点认知层面的东西算不上教程算是我自己冲过一遍之后的体会。第一日志的语义变了。原来写Java日志是写到文件里出问题去服务器上tail -f某个.log。在K8s里正规做法是日志打到标准输出由底层运行时收集并转发。你用kubectl logs看到的就是容器的stdout日志文件本身不再需要你管理也不该被持久化到Pod内部。如果服务里有大量磁盘日志请尽早让它们输出到stdout。第二配置管理的思想变了。以前Java项目里配置散落在application.yml、Nacos、数据库表里很多时候是“改了配置重启服务生效”。K8s的ConfigMap和Secret配合Deployment滚动更新可以实现配置文件变更自动触发Pod滚动更新。但这里有一个隐性要求你的应用要支持配置热生效或者至少支持优雅重启。如果还是那种“配置文件改了必须人工进容器里改”那K8s的声明式管理就形同虚设。所以我建议Java服务尽量用Spring Cloud Config或Nacos把配置外置化集群只负责计算资源。第三对“故障”的忍耐力变了。原来我们会想尽办法避免进程挂掉因为挂了很麻烦——要ssh、要重启、要盯着日志。在K8s里Pod挂了本身就是一种正常事件K8s会把它重新拉起。所以你要关注的不是“为什么又挂了”而是“挂掉之后能不能自动恢复、恢复速度有多快、用户侧有没有感知”。这就是可观测性的意义也是为什么Prometheus监控指标那么重要。没有监控的时候你只能等用户喊有监控后你可以在用户喊之前就发现问题。第四八股文和实际运维之间需要补一张拼图。热词里“k8s面试题”确实火但说句实话很多面试题问的是“什么是Pod”“Service有哪几种类型”这种背诵题答出来了也不代表你会在生产环境排查事情。真正值钱的不是你背了多少概念而是当生产环境里有个Pod一直CrashLoopBackOff你能不能快速判断出是JVM参数问题、探针配置问题还是下游依赖问题。这个能力只有靠真实场景去练光啃PDF教材是练不出来的。我在实际接触K8s和Jenkins这半年多里最深的感受是这些工具的本质是把之前靠人肉执行、靠经验积累的操作变成了代码化、声明化、可评审、可回滚的工程流程。原来Java后端解决问题靠的是烂熟于心的业务逻辑和框架能力现在你还要多一份意识代码写完之后能不能稳定地跑在庞大的编排系统中、能不能在故障来临时快速定位这种能力在你作为后端工程师的成长路上跟写出优雅代码一样重要。最后分享一个一直很受用的小习惯每次在K8s上动一个Pod之前先kubectl get events --sort-by.metadata.creationTimestamp把集群里的事件按时间捋一遍而不是一上来就闷头改YAML。很多故障的根源一开始就写在Events里了。