ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java工程师如何快速上手K8s与Jenkins?从容器化到CI/CD落地实践

Java工程师如何快速上手K8s与Jenkins?从容器化到CI/CD落地实践 1. 一个写Java的怎么就绕不开K8s和Jenkins了1.1 从本地能跑就行到线上必须稳定的转变我先说一下自己的背景。写Java后端写了六七年每天的工作几乎离不开Spring Boot、MyBatis、MySQL这些最熟悉的是mvn package打出一个带依赖的fat jar然后扔到测试服务器上用java -jar启动接着用ps看进程还在不在。那几年里我理所当然地认为部署这件事就该归运维管我只要能保证本地mvn test全绿、接口用Postman调通就算交付完成任务了。转折发生在一个对我来说有点尴尬的场景公司要做容器化改造测试环境从裸机迁到了K8s集群Jenkins被提上日程要求每个项目都要能自动构建、自动部署。当时我第一反应是我一个写Java的看K8s那堆Pod、Deployment、Service满脑子都是这跟我有什么关系。但现实很快打了脸——线上环境没人帮我手动停服务、扔jar包了我连自己写的Java服务为什么会起不来都要花一整天去查日志。后来我才意识到作为Java开发者已经不只是写接口这一个动作了。从代码提交、项目编译到镜像构建、容器编排、服务发布整条链路的任何一个环节出错最终背锅的都是开发。与其被动挨打不如主动把这套东西吃透。这篇文章里我想说的就是一个普通Java工程师怎么一步步把K8s和Jenkins从陌生的部署工具变成了日常工作的左膀右臂。如果你也正处在类似的转型期看到标题大概能明白我的心情。1.2 CI/CD不是新语言只是把Java工程师熟悉的构建流程自动化了很多人一听持续集成持续交付流水线就发怵觉得这是运维才需要掌握的高级概念。但换个角度想Java工程师从入门第一天在用Maven或者Gradle那其实就是一个标准化的构建流程。validate、compile、test、package、install每个阶段执行什么插件、依赖从哪里下载、产物输出到哪个目录这不就是一个小型的流水线吗Jenkins只不过是把这条流水线从单机搬到了一个更通用的调度平台上并且允许你按需串联更多步骤。K8s也同理。如果你懂Spring就应该知道Spring IoC容器管理Bean的生命周期K8s里的控制器Controller也在做类似的事情保证Pod副本数符合期望、滚动更新时先起新再删旧、负载均衡后端的Service配置变化后自动更新Endpoint。我把这些对应关系弄明白之后学习K8s的难度一下子降了好几个档次。后面我会专门写一节拿大家熟悉的Java概念去类比K8s里的核心对象。这一节想强调的核心观点是Java程序员转去看K8s和Jenkins不需要把自己当成一个空白的运维新手你应该把自己已有的构建经验、进程管理经验、配置管理经验迁移过去。我之前就是太给自己设限总觉得“这不是Java的活”等到真去啃的时候才发现这些工具离Java开发比想象中近得多。2. 先啃K8s给Java开发者的速通路线2.1 用Java概念类比核心对象立刻就不懵了K8s的文档喜欢用官方术语什么Pod、Deployment、Service、Ingress、ConfigMap、Secret初次接触的人很容易被名词淹没。我用Java里已有的概念对照了一遍发现很多对象是可以完美类推的这里直接做成一张表。K8s对象Java/Spring对应物一句话解释PodJVM进程实例一个Pod就是一个运行中的实例里面可以有一个或多个容器类比JVM里跑着Spring Boot应用DeploymentSpring容器/Bean配置它控制Pod的副本数量、镜像版本、更新策略类似描述IoC里某个Bean的scope和初始化时机Service网关/注册中心负载均衡入口给一组Pod提供稳定的访问入口类似Nginx反向代理也类似RPC服务发现里那个稳定的服务名ConfigMapapplication.yml把配置和应用程序解耦不把环境配置写死在镜像里SecretJasypt加密配置/数据库密码保存敏感信息比如数据库连接密码、密钥IngressSpring MVC的DispatcherServlet外部HTTP请求的统一入口按host/path路由到不同ServiceNamespaceMaven的groupId/模块分包资源隔离和分组管理可以类比不同业务模块、不同环境的jar包隔离这个类比可能不是100%精确但对初学者理解非常有用。我第一次看到Pod里有两个容器比如一个应用容器、一个Sidecar日志采集容器时想到的是“同一个进程内的多线程虽然共享内存但彼此独立运行”一下子就觉得没那么玄了。需要提醒的是类比归类比K8s里对象之间是有严格依赖关系的。Deployment负责管理PodService通过Label Selector去选PodIngress再转发到Service。这个依赖链条可以用一句话串起来外部请求先到IngressIngress路由到ServiceService负载均衡到PodPod里运行着你的Java容器而Deployment掌控整个过程里Pod的生死。2.2 一个Spring Boot服务在K8s上的最小部署清单我们不说理论直接给一个能跑通的最小方案。假设你有一个Spring Boot项目打包后叫demo.jarJDK用的是17你要把它部署到K8s。第一步先写一个Dockerfile。很多Java项目会直接用openjdk:17-jdk-slim这种镜像但这会让镜像体积偏大而且OpenJDK官方镜像维护不是很活跃。更稳的方式是用Eclipse Temurin的镜像不过这里为了讲解简单我还是用最熟悉的例子。FROM openjdk:17-jdk-slim LABEL maintaineryourname ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone WORKDIR /app COPY target/demo.jar app.jar EXPOSE 8080 ENTRYPOINT [java,-jar,app.jar]构建好镜像之后推到镜像仓库比如阿里云ACR或者Harbor。然后是部署文件一般公司里会分成deployment.yaml和service.yaml。apiVersion: apps/v1 kind: Deployment metadata: name: demo-deployment namespace: demo spec: replicas: 2 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: registry.example.com/java-demo/demo:1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 resources: requests: memory: 512Mi cpu: 250m limits: memory: 1Gi cpu: 1000mService文件就更简单了。apiVersion: v1 kind: Service metadata: name: demo-service namespace: demo spec: type: ClusterIP selector: app: demo ports: - port: 80 targetPort: 8080然后kubectl apply -f deployment.yaml -f service.yamlPod跑起来之后在集群内部通过demo-service:80就能访问到Java服务的8080端口。这就是一个Java应用在K8s上的最小部署。需要注意这个Service默认只在集群内部有效外部访问还需要Ingress或者NodePort。部署的时候最容易犯的一个错就是忘了命名空间。我一开始部署到default里后来服务多了之后环境非常混乱。建议从第一天开始就给每个环境建独立命名空间比如demo-dev、demo-test、demo-prod。2.3 让Java服务优雅上下线别让滚动更新变成“请求杀手”Java服务在K8s里和传统的Tomcat部署有一个很大的区别Pod会被频繁销毁和重建比如发布版本、扩容缩容、节点维护。如果你只是把Spring Boot应用原封不动丢进去不做任何配置大概率会在发布期间出现“连接被重置”、“报错Connection refused”这类问题。原因在于两个信号没有处理好。一个是容器停止时K8s默认向Java进程发送SIGTERM信号而Spring Boot默认行为是立即关闭不等请求处理完另一个是K8s判断Pod就绪的探针readinessProbe没有配好Service还在继续往正在停机的Pod转发流量。我的解决办法有三条。配置Spring Boot的优雅停机server.shutdowngraceful spring.lifecycle.timeout-per-shutdown-phase30s然后在Deployment的spec里加preStop钩子和终止等待时间spec: terminationGracePeriodSeconds: 60 containers: - name: demo lifecycle: preStop: exec: command: [sh, -c, sleep 10]preStop里sleep 10秒钟是为了让Pod先从Service的Endpoints里移除再真正停掉进程。如果你用过Nginx upstream这就像在Nginx里把某个后端标记为down之后等它处理完现存连接再关闭。K8s其实也在做类似的事只是需要你自己配置时机。这就是Java开发在K8s里最容易踩的坑也是面试里经常被问到的“优雅下线”问题。我建议无论在什么环境只要用K8s跑Java服务这三件套必须配齐。3. Jenkins自动部署从手动点按钮到全自动流水线3.1 先搞清楚Jenkins在你的场景里到底扮演什么角色很多人一提到Jenkins就想到那些五颜六色的仪表盘和插件列表以为它只是一个“可视化按钮工具”。其实Jenkins的核心价值在于它是一个构建调度的中枢你可以理解成一个用Groovy脚本驱动、可以按时间或Git事件触发、能在多台Agent上并行执行任务的“定时任务Plus”。从Java开发的角度看我们通常在本地跑mvn clean install然后手动上传jar包。Jenkins做的事很简单替你把这段命令放到了一个可以重复执行、可以审计、可以自动触发的地方。同时它还能把你原来手工做的上传服务器、执行启动脚本这些步骤通过Pipeline脚本固化成一条流水线。新版本的Jenkins有两个事情需要先处理一个汉化一个插件加速。Jenkins 2.541.3这种版本装好默认是英文界面虽然对日常使用没太大影响但团队里如果有不熟悉英文的新人建议在系统管理里安装“Localization: Chinese (Simplified)”插件然后设置语言。插件加速是因为Jenkins默认插件下载源在国外网络环境不好的时候经常装插件超时可以手动把升级站点URL换成国内镜像地址比如清华源的https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json这个在系统管理-插件管理-高级设置里配置。还有一点需要提一下Jenkins里有大量环境变量比如BUILD_NUMBER、WORKSPACE、JOB_NAME这些在你写Pipeline时特别有用可以用来做镜像tag、归档路径、邮件通知标题。官方有一份“Jenkins可用环境变量”列表虽然不一定全部记得住但BUILD_NUMBER、GIT_COMMIT、JOB_URL这几个最常用。3.2 用Pipeline脚本把Java项目的构建、推送、部署串起来在Jenkins里建一个流水线项目时我推荐直接用Pipeline脚本而不是选择“自由风格项目”。自由风格项目虽然界面配置方便但一旦步骤复杂所有逻辑都散落在页面上没法代码review也不方便复制到下一个项目。Pipeline脚本本质就是一份Jenkinsfile提交到Git仓库里和Java代码一起管理。这里给一个最基础的Java应用持续部署模板假设你已经把K8s集群的kubeconfig配到了Jenkins所在的环境里且Jenkins主机上装了kubectl。pipeline { agent any environment { DOCKER_REGISTRY registry.example.com PROJECT_NAME java-demo IMAGE_TAG ${BUILD_NUMBER}-${GIT_COMMIT?.take(7)} NAMESPACE demo } stages { stage(拉取代码) { steps { checkout scm } } stage(编译打包) { steps { sh mvn clean package -DskipTests } } stage(构建推送镜像) { steps { sh docker build -t ${DOCKER_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG} . docker push ${DOCKER_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG} } } stage(部署到K8s) { steps { sh kubectl set image deployment/demo-deployment demo${DOCKER_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG} -n ${NAMESPACE} kubectl rollout status deployment/demo-deployment -n ${NAMESPACE} } } } post { success { echo 部署成功 } failure { echo 部署失败 } } }这段脚本没有用任何插件只依赖Jenkins主机本身能执行shell命令。思路非常直观拉代码、打jar包、构建镜像、推镜像、更新K8s Deployment的镜像版本。IMAGE_TAG通过BUILD_NUMBER加上Git提交短哈希生成目的是让每次构建的镜像都可追溯这是生产环境里特别重要的一步。如果完全用新版的Kubernetes插件来部署还可以直接调用kubernetesDeploy步骤传入镜像名即可。但我觉得对于刚开始接触的人用kubectl set image更接近命令历史排查问题也更直接。等你把流程跑通了再往深度方向走去研究Jenkins的Kubernetes插件动态Agent、声明式Pipeline的高级写法。3.3 Jenkins容器里调用docker和kubectl总有一个绕不开的坑现在很多公司会把Jenkins也用容器方式跑这就带来了一个经典问题Jenkins容器里没有docker命令你怎么构建镜像我第一次遇到这个报错直接愣住了sh: docker: command not found后来折腾了很久才明白是容器环境的问题。最常见的解决方案有三种。第一种把宿主机上的docker.sock挂载进Jenkins容器也就是-v /var/run/docker.sock:/var/run/docker.sock。这种方式最直接Jenkins容器里的docker命令会直接操作宿主机的docker守护进程。但风险也很明显任何能执行Jenkins任务的人都等同于有了宿主机root权限。自己开发环境可以这么玩生产环境不推荐。第二种用Kaniko这种专门在容器里构建镜像的工具不依赖docker daemon会安全很多但配置学习成本高一些。第三种也是我目前用得最多的把Jenkins本身跑在K8s集群里然后利用Kubernetes插件的“动态Agent”功能让每次构建都临时拉起一个带kubectl和helm的Pod来执行任务。构建Agent用完就被销毁既实现了资源隔离也不用担心污染宿主机。Jenkins里使用kubectl核心就是准备一个能连接目标K8s集群的kubeconfig文件可以通过withKubeConfig插件或直接在环境变量里设置KUBECONFIG路径。注意集群地址、证书、token这些不要写死到Jenkinsfile里应该放到Jenkins的凭据Credentials管理里。4. 生产环境里那些让人头大的K8s和Jenkins故障4.1 Pod起不来镜像拉不下来到底该怎么查生产环境最常遇到的第一类问题就是Pod一直停在ImagePullBackOff状态。你运行kubectl get pods能看到的是一堆红色的错误状态但具体原因需要靠kubectl describe pod来查看。很多新手习惯直接看Pod日志但Pod没起来的时候根本没日志可看所以describe才是第一步。常见的几个原因有镜像仓库地址写错、镜像标签不存在、私有仓库未登录、镜像拉取凭据不对。Java服务如果用的是私有镜像仓库Deployment的spec.template.spec里要配置imagePullSecrets不然K8s根本没权限从你的Harbor或ACR里拉镜像。另外镜像tag如果总是用latest开发环境没问题但生产环境建议每次构建都带上唯一tag并且使用imagePullPolicy: IfNotPresent之外的策略否则会出现明明推了新镜像但节点上还跑着旧镜像的情况。还有一类问题是镜像能拉到但Pod启动后马上崩溃重启。这时还是要先看状态CrashLoopBackOff。如果是Java应用启动报错去看Pod日志如果是启动过程中内存不足被OOM杀死要重点检查下一节说的JVM内存配置。这里有一个我个人的习惯接到这类故障先kubectl describe pod看一下Events再kubectl logs看应用日志按这个顺序来永远不会跑偏。4.2 Service、Ingress、externalIPs服务访问不到的排查顺序另一个高频问题是服务部署好了但外部访问始终超时。这个问题牵涉到K8s的网络模型很多人一上来就怀疑Ingress配错了其实有可能是下面的Pod没挂上Service或者Service类型不对。我建议一种排查顺序先确认Pod正常且日志无报错再确认Service的Endpoints里有Pod的IP然后在集群内部用Service的DNS名字做curl测试最后才检查Ingress转发规则。这个顺序其实就是数据包路径客户端请求到IngressIngress到ServiceService到Pod。哪一环断了就排查哪一环。说到Service类型K8s里有ClusterIP、NodePort、LoadBalancer、ExternalName几种。ClusterIP只在集群内可达NodePort会在所有节点上开放一个端口LoadBalancer一般配合云厂商的负载均衡器使用。还有一个容易被忽略的externalIPs字段它可以给Service手动指定一个外部可达IP让集群外的流量直接通过这个IP访问Service。比如你有一台机器IP是10.0.0.5你想让外部访问某个服务可以这么写apiVersion: v1 kind: Service metadata: name: demo-service spec: selector: app: demo ports: - port: 80 targetPort: 8080 externalIPs: - 10.0.0.5这个在旧版K8s教程里经常被提及实际上外部流量直接发给这台物理机时请求会被转发到对应的Service。但它在现代集群中并不常用因为Ingress和LoadBalancer才是主流方案。面试和实际排障时把externalIPs和NodePort放到一起理解不会错。4.3 JVM在容器里的内存问题这和Java开发关系太近了K8s里跑Java服务最经典的一幕就是Pod突然死掉状态显示OOMKilled。开发一看应用日志没有OutOfMemoryError没有任何异常进程直接没了。这是因为K8s从cgroup层面限制了Pod可以使用的内存当Java进程使用的总内存超过这个上限时内核会直接杀掉进程。老版本的Java对容器内存的支持很差。JDK 8u131之前JVM默认是把宿主机物理内存当成本机内存来算的所以在容器里设置了-Xmx2g也没用JVM可能以为机器有64G内存然后不断扩张堆内存直到触发cgroup的限制被杀死。后来的JDK版本默认开启了UseContainerSupport但如果你还在用老JDK或者手动覆盖了JVM参数还是会出现问题。我的部署参数是这样配的ENTRYPOINT [java,-XX:MaxRAMPercentage75.0,-jar,app.jar]MaxRAMPercentage75意味着JVM最多使用容器内存上限的75%剩下的25%留给线程栈、Metaspace、JIT编译器这些容器看不见但确实需要的内存。同时Deployment里的resources.limits.memory要和这个比例配合好。比如限制1Gi内存JVM堆最大就是768Mi左右不要再把-Xmx直接写死成1G否则堆还没到顶峰容器就被K8s杀了。排查这类故障还有一个实用的技巧使用kubectl describe pod查看Last State那一栏如果显示OOMKilled: true基本就能确定是被杀而不是Java抛异常。然后再去看内存监控确认是堆内存还是非堆内存的问题。5. 进阶玩法Java生态里K8s和Jenkins还能怎么融合5.1 从“能用”到“好用”动态Agent、Operator、监控预警当你的Java项目数量多起来之后每个项目都复制粘贴一份Jenkinsfile会变得很痛苦。这时值得去做的第一件事就是让Jenkins Agent动态化。通过Kubernetes插件Jenkins可以在执行Pipeline时自动新建一个Pod作为Agent这个Pod里预装好Maven、JDK、kubectl、docker或Kaniko构建完自动删除。这样的好处很明显构建任务之间不会互相污染高峰期能自动拉起多个Agent并发执行空闲时不会白白占用资源。K8s本身也有一个和Java生态深度结合的点就是Operator模式。你可以把Operator理解成一个“不断检查期望状态和当前状态然后自动把当前状态调整到期望状态”的程序这和Spring里那些定时拉取数据做对账的Job很相似只不过它操作的对象是K8s集群内的资源。比如你希望集群里任意时刻都有3个Java服务实例当Pod挂了Operator会立刻帮你拉起新的这种自动化的扩展能力是纯手工运维做不到的。监控方面虽然这篇博文主要在说部署但生产环境的K8s没有Prometheus基本等于瞎子。部署Prometheus监控K8s集群重点盯几个指标Pod的CPU和内存使用率、重启次数、镜像仓库拉取耗时、Ingress请求成功率。Java应用本身还可以通过Spring Boot Actuator暴露Metrics让Prometheus来抓取指标。Jenkins也有对应的Prometheus插件直接把jenkins/adhoc这些接口数据暴露出来一旦构建成功率下降或平均构建时间变长告警马上能推出来。这里不用一开始就做得大而全但至少要把“Pod重启次数大于3”和“构建失败率超过20%”这两个告警配好。5.2 资源学习清单和进阶方向别只盯着“八股文”我把这个放最后是因为看到网上很多人问“K8s权威指南第五版pdf下载”这本书固然经典但如果你现在才开始学K8s我建议不要只抱着一本书啃。K8s社区变化太快一本书出版一年后部分内容可能就过时了。更好的学习路径是先看官方文档的Concepts部分再跟着官方教程做一遍minikube或者kind的本地集群最后再回到项目里实操。Java这边也一样面试常问的Java基础、Java面试题、并发编程、JVM调优其实大多都能在K8s和Jenkins场景里找到回响。比如你在Jenkins里配了一个并行构建不同stage同时跑这本质就是多线程并发的调度问题你在K8s里设置Deployment的滚动更新策略本质就是分布式系统里的发布一致性你处理容器中JVM内存问题本质就是JVM参数和Linux cgroup的交互。如果让我给一个更明确的进阶顺序我会建议按这几步走第一步把本地Java项目容器化能把镜像推上仓库第二步用K8s跑起来配合Ingress暴露HTTP服务第三步用Jenkins做持续集成实现代码提交后自动构建、自动部署第四步加入监控告警、环境隔离、灰度发布第五步去研究Operator、离线的制品管理、多集群容灾这些才是真正拉开差距的地方。至于那些“Java POI Word能生成图表吗”之类的问题其实也是同一个道理工具不是限制对工具链的理解才是。再说两句关于技术选型的心得。现在社区里有些声音觉得Jenkins老了应该用GitLab CI、GitHub Actions、Argo CD之类的替代品。这个判断要看场景。Jenkins的优势在于插件生态极其丰富私有化部署非常成熟在传统企业内部接受度极高。如果你在维护一个存量Java项目老团队都会用Jenkins你就不要硬推新东西如果是从零开始的新项目直接上云原生的CI/CD工具链当然更舒服。但无论如何K8s作为统一部署平台这个方向短期内不会变Java开发者提前掌握它对自己只有好处。我个人在实际操作中的体会是你不需要一开始就成为一个K8s和Jenkins的“专家”只需要在Java应用交付出问题的时候能快速定位到是哪一环出了问题是代码、是镜像、是Service配置还是Jenkins任务本身。这种能力远比背熟几张八股文面试题更有价值。等你有过几次半夜爬起来排查ImagePullBackOff或者CrashLoopBackOff的经历之后你自然就会明白K8s和Jenkins不是运营团队的专属工具而是Java应用生命周期里绕不开的一部分。最后再分享一个小技巧把所有和部署相关的脚本从Dockerfile到Jenkinsfile从deployment.yaml到Service配置全部放进Git仓库统一版本管理。这样一旦配合出现问题你能检查到每次改动的原因而不是靠“感觉这个配置以前能跑”去过日子。
返回列表