我前前后后折腾了大半年踩了无数坑才把公司那套从“代码提交到线上发布全靠手搓”的流程逐步升级成了以Docker、K8s、Jenkins为核心的云原生DevOps全链路体系。这套体系上线后部署效率提升得非常明显而且因为流程标准化了新同事上手也快了很多。这篇文章就结合我的实操经历把整个体系的构建思路、关键细节、以及那些文档里不会写的坑一次性说清楚。如果你正在打算搞这套东西或者已经在这条路上但遇到了瓶颈这篇文章应该能给你一些切实可用的参考。先说清楚这套体系到底解决了什么问题。在传统方式下开发把代码提交到Git仓库运维手动去服务器拉代码、打包、停服、部署偶尔还要半夜爬起来处理发布事故。环境不一致的问题、依赖冲突的问题、回滚困难的问题每一个都让人头疼。而云原生DevOps的核心思路就是把“环境”和“应用”都变成代码化的、可版本管理的资源通过自动化的流水线把构建、测试、部署这一整条链路串起来。Docker负责把应用连同它的运行环境一起打包成镜像解决环境一致性问题K8s负责这些镜像的编排调度和生命周期管理解决部署和扩缩容问题Jenkins则作为那个“总控台”把代码拉取、镜像构建、镜像推送、K8s滚动更新这一系列动作编排成一条自动化的流水线。这篇内容我会从最底层的Docker基础环境搭建开始讲然后到K8s集群的部署规划再到Jenkins流水线的设计最后会重点讲一下这套体系跑起来之后最常见的那些故障和排查思路。整个内容都是按照我实际落地的顺序来的你可以把它当成一份从0到1的实战笔记来看。1. 为什么偏偏是DockerK8sJenkins这三件套很多刚开始接触云原生的朋友会问市面上的容器和编排工具那么多为什么组合来组合去最终绕不开这三样。其实答案很简单这三者分别解决的是不同层面的问题而且恰好能严丝合缝地衔接在一起。Docker解决的是环境一致性和打包标准化的问题。在没有容器之前最经典的一句话就是“在我机器上是好的啊”。开发环境、测试环境、生产环境之间的系统版本、依赖库版本、配置差异导致同一个应用在A机器上跑得好好的到了B机器上就各种报错。Docker把应用二进制文件和它依赖的所有库、配置文件、环境变量打包成了一个只读的镜像然后用这个镜像去创建容器运行。所有环境跑的是同一个镜像环境差异这个老大难问题从根上被解掉了。K8s解决的是大规模容器的编排调度和管理的问题。当你的容器数量只有几个的时候手动docker run完全够用。但当容器数量上升到几十个上百个分布在好几台机器上时你需要知道容器跑在哪些机器上、哪些容器挂掉了需要重启、流量如何在不同副本间分发、高峰期怎么快速扩容。这些都是K8s的看家本领。它把一组机器抽象成一个资源池你只需要声明“我要跑3个副本”K8s自己会去找合适的机器把Pod拉起来。Jenkins解决的是整个链条的自动化串联的问题。代码从提交到上线中间经历拉代码、单元测试、镜像构建、镜像推送、更新K8s deployment等一系列操作。Jenkins Pipeline把这些操作编排成一段可以版本管理的脚本只要代码一提交到指定分支Pipeline自动触发一条龙跑完整个流程。它相当于这条自动化生产线的总控台。这三者单独拿出来各自都有替代品但组合在一起形成了一条从代码到容器的完整通路。Docker负责把代码变成镜像K8s负责把镜像变成服务Jenkins负责让这一切自动发生。理解了这一层依赖关系你再去看后文的实操内容思路会清晰很多。2. 从一台干净服务器开始的Docker基础环境搭建不管你是要在生产环境用还是先搭一套测试环境练手Docker的基础环境都必须搞扎实。这里我把自己在Linux环境下的安装过程和需要注意的点完整列出来。别小看这一步很多人后面出问题追根溯源都是Docker这一层就没装干净。2.1 操作系统准备与内核参数调整Docker对宿主机内核有要求官方推荐使用Linux内核3.10以上版本。CentOS 7.9、Ubuntu 20.04及以上版本的系统默认内核都能满足要求。我建议优先选用Ubuntu 20.04以上的版本因为后续K8s对内核模块和iptables版本的要求Ubuntu的兼容性明显比CentOS 7系列省心。当然CentOS 7.9也能跑但如果你还没选型我会优先推荐Ubuntu。装系统的时候有一个细节值得注意磁盘分区时尽量把 /var 目录单独分一个大区。因为Docker默认的数据目录就挂在 /var/lib/docker 下面镜像、容器层、卷数据全在这里。如果你把 / 和 /var 放在一起跑一阵子镜像多了很容易把根分区撑爆。我见过不少同事的服务器挂掉不是系统出问题就是Docker数据目录把磁盘塞满了。安装docker之前先确认一下系统内核网络转发功能是开启的# 永久开启内核转发 cat EOF | sudo tee /etc/sysctl.d/k8s.conf net.ipv4.ip_forward 1 net.bridge.bridge-nf-call-iptables 1 EOF # 立即生效 sudo sysctl -p /etc/sysctl.d/k8s.conf这个配置非常关键如果不开启后面容器和容器之间通信、容器访问外网都会出现各种莫名其妙的超时问题。这一步做完再开始正式安装Docker。2.2 Docker引擎安装与国内镜像源加速我在Ubuntu上习惯用官方脚本或者是直接添加官方GPG密钥来安装。这里用最稳妥的手动方式# 卸载可能存在的旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release # 添加Docker官方GPG密钥 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加软件源 echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后有一个国内网络环境下必须做的事——配置镜像加速器。Docker Hub在国内的访问速度很不稳定不配置加速器拉一个几十MB的镜像可能要等几分钟甚至更久一旦镜像体积到几百MB基本就没法用了。sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json -EOF { registry-mirrors: [ https://docker.m.daocloud.io ] } EOF sudo systemctl daemon-reload sudo systemctl restart docker关于镜像加速地址这个变化频率很快你可以在网上搜一下当前可用的加速器。不同加速器的稳定性和速度差别很大我建议是配置两三个万一某一个挂了还有备用的。另外提醒一句加速器只影响拉取镜像不影响你往自己的私有仓库推送镜像。配置完成后验证Docker是否正常工作sudo docker run hello-world sudo docker info看到Server Version那行输出版本号说明Docker引擎已经正常工作了。2.3 Docker常用命令速查与理解容器生命周期Docker的命令很多但对于做DevOps链路来说高频用到的就是镜像管理和容器生命周期管理这两类。这里把我平时最常用的命令整理成一个速查表命令作用备注docker pull 镜像名:标签拉取镜像到本地不写标签默认latestdocker images查看本地镜像列表-docker rmi 镜像ID删除镜像有容器引用时需先删容器docker ps -a查看所有容器-a参数能看到退出的容器docker run -d --name 容器名 -p 宿主机端口:容器端口 镜像名启动容器-d后台运行docker exec -it 容器ID bash进入容器排障时常用docker logs -f 容器ID查看容器日志-f表示实时跟读docker stop / docker rm停止/删除容器-f参数可强制删除运行中容器docker system prune清理悬空镜像和停止的容器慎用会删除未使用的资源有一个很容易犯的误区就是混淆了镜像和容器的关系。打个比方镜像是“类”容器是“实例”。同一个镜像可以启动多个容器每个容器都是独立运行的。修改容器里的文件不会影响镜像本身。所以当你改了容器里的配置来排查问题发现重启容器后配置又变回去了别慌这是正常的因为容器是基于镜像创建的。2.4 Docker数据卷和网络的核心配置生产环境使用Docker有两个问题必须有清醒的规划数据持久化和网络模型。数据卷用于解决容器删除后数据丢失的问题。Docker容器是无状态的容器一删容器内写入的文件立即消失。对于MySQL、Redis这类需要存数据的应用数据必须挂载到宿主机目录或者使用命名卷。启动MySQL容器时我习惯这样docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -v /data/mysql:/var/lib/mysql \ mysql:8.0-v参数把宿主机 /data/mysql 目录映射到容器内的数据目录这样即使容器损坏删掉数据也还在宿主机上。在K8s环境里这个思想演变成了PersistentVolumePV和PersistentVolumeClaimPVC抽象底层逻辑是相通的。网络方面Docker默认的bridge网络适合单机容器通信。但K8s集群里容器跨宿主机通信需要更复杂的网络方案比如Calico、Flannel等。这一块我们后面讲K8s的时候再展开你在单机测试阶段只要明白同一个宿主机上如果多个容器需要互相访问可以通过自定义bridge网络通信也可以通过宿主机端口映射访问。这已经是基本够用的网络知识了。3. K8s集群搭建前的硬件规划与架构设计思路K8s集群和Docker单机完全是两个量级的复杂度。常见错误是直接用一台机器既装Docker又装K8s单节点一开始测试没问题真正跑业务时才发现各种调度上的坑。我在搭建集群之前先把架构规划讲清楚。3.1 生产环境最小集群的节点角色划分K8s集群的基本角色分为Master节点控制平面和Worker节点工作节点。生产环境为了保证高可用Master节点至少要3台通过高可用组件如keepalived或云厂商的负载均衡对外提供稳定入口。但如果你搭建的是测试环境或者刚开始学习单Master节点的集群也能跑起来——只要你能接受管理节点挂了的时候整个集群不可调度这件事。我比较推荐的起步配置是1台Master 2台Worker或者3台Master 3台Worker。前者适合学习验证后者适合小规模生产。节点角色分配如下角色数量核心组件建议配置Master1~3kube-apiserver, kube-controller-manager, kube-scheduler, etcd4C8GSSDWorker2~3kubelet, kube-proxy, 容器运行时8C16G起步Master节点的核心是etcd。etcd是K8s的大脑存储了集群全部状态数据它的读写性能直接决定集群的响应速度。有条件的一定要把etcd数据目录放到专门的高性能SSD盘上。我踩过的一个坑是etcd数据目录和系统盘共用一块普通机械硬盘集群一跑起来Pod调度和状态同步慢得像蜗牛爬。3.2 如何规划镜像仓库和存放镜像的持久化存储K8s要拉取镜像来创建Pod镜像从哪里来是一个需要提前想清楚的问题。如果K8s节点都直接去Docker Hub拉镜像首先速度很难保证其次生产环境的镜像是不能随便用公共仓库的。所以搭建K8s之前先把私有镜像仓库规划好。常见的私有镜像仓库方案是Harbor它不仅支持镜像存储和权限控制还自带漏洞扫描和复制功能是国内使用最广泛的方案之一。另一种轻量级的方案是直接使用Docker自带的Registry镜像简单场景够用但缺少UI界面和权限管理。构建DevOps链路时我强烈建议第一步先把Harbor部署起来让它成为整个体系的镜像中转站# 下载Harbor离线安装包解压后修改harbor.yml wget https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz tar xvf harbor-offline-installer-v2.9.0.tgz cd harbor cp harbor.yml.tmpl harbor.yml vim harbor.ymlharbor.yml里有几个关键配置项hostname: 配置为你给Harbor规划的域名或IPharbor_admin_password: 管理员的初始密码安装完记得改不要一直用默认的Harbor12345data_volume: 镜像存储路径建议单独挂一个大磁盘如果只打算用HTTP访问记得把https相关配置注释掉配置完成后运行./install.sh然后通过浏览器访问http://你的服务器IP就能看到Web界面了。K8s节点从Harbor拉取私有镜像有两种方式。一是在每个节点上通过docker login登录Harbor这样Docker就会把凭证缓存到本地K8s经由CRI调用容器运行时拉镜像时自然用上了。二是通过K8s的Secret机制创建镜像拉取凭证kubectl create secret docker-registry regcred \ --docker-serveryour-harbor-server \ --docker-usernameadmin \ --docker-passwordyourpassword \ --docker-emailyourmailexample.com然后在Deployment的YAML里通过imagePullSecrets字段引用这个Secret。我后文在Jenkins流水线编排时应用部署这一步会用到这个Secret你先把概念理解清楚。3.3 节点内核参数与kubeadm初始化实战K8s集群的搭建我推荐使用kubeadm工具这是官方推荐的标准工具零散手动配置的步骤被大大简化。但从实践来看如果你在CentOS 7或者Ubuntu环境里按kubeadm流程走有几个环节特别容易出错我专门列出来。首先是关闭swap分区。K8s集群节点要求关闭swap因为kubelet的cgroup管理默认假设节点没有启用swapsudo swapoff -a sudo sed -i / swap / s/^/#/ /etc/fstab不关swap的话kubelet会直接启动失败错误日志会提示你“running with swap on is not supported, please disable swap”。这句话我太熟了当初在虚拟机里折腾的时候第一次初始化失败就是这个原因。然后是加载内核模块和设置系统参数cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter cat EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sudo sysctl --system这些和Docker安装时的内核参数类似但K8s的要求更严格一些br_netfilter模块必须加载否则iptables规则无法作用于bridge网络流量Service的流量转发会出问题。接下来就可以正式安装kubelet、kubeadm、kubectl这三个核心组件并初始化集群了。安装部分略过细节直接把初始化命令写出来sudo kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --image-repository registry.aliyuncs.com/google_containers \ --pod-network-cidr10.244.0.0/16 \ --kubernetes-versionv1.28.2这里有一个非常关键的参数--image-repository registry.aliyuncs.com/google_containers。因为K8s核心组件镜像默认是从registry.k8s.io拉取的这个地址在部分网络环境下访问速度很慢甚至直接失败。使用阿里云的镜像仓库加速可以快速拉取到所有K8s核心组件的镜像。初始化成功之后按提示执行配置kubectl的命令然后安装Pod网络插件。我用的比较多的是Calico它性能好并且支持NetworkPolicykubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml等kubectl get pods -n kube-system里所有Pod都变成Running状态再将Worker节点加入集群。Worker节点上执行kubeadm join命令这个命令在Master初始化时末尾会打印出来需要记下来。3.4 K8s的LNMP架构部署实验理解工作负载和Service的核心逻辑集群搭好之后别急着跑各种复杂业务先用一个经典的LNMP架构部署实验把K8s的核心概念串一遍。这个实验特别适合验证集群网络、存储和Pod调度是否正常工作而且部署过程中能直观理解K8s的几个核心对象Deployment、Service、ConfigMap、PersistentVolumeClaim。LNMP架构也就是Linux Nginx MySQL PHP的组合。在K8s里的部署思路是这样拆分的MySQL作为有状态服务需要持久化存储用StatefulSet或DeploymentPVC的方式部署并通过ClusterIP Service 对内暴露3306端口Nginx和PHP-FPM是典型的Web服务分别部署成DeploymentNginx通过ConfigMap配置反向代理到PHP-FPM的ServicePHP-FPM的代码文件需要挂载共享存储Nginx和PHP-FPM两个Pod同时挂载同一个PVC这样才能让Nginx找到PHP文件这里以部署MySQL为例YAML长这样apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-secret key: password volumeMounts: - name: mysql-data mountPath: /var/lib/mysql volumes: - name: mysql-data persistentVolumeClaim: claimName: mysql-pvc注意两个细节一个是通过secretKeyRef从Secret里读取MySQL的root密码不要明文写在YAML里另一个是通过PVC把MySQL数据持久化到集群存储中Pod被重新调度到其他节点后数据不丢。我这个实验在第一次跑的时候也遇到了Pod调度失败的情况原因是在PV还没创建好的时候PVC一直处于Pending状态Deployment对应的Pod就卡在ContainerCreating。所以说K8s的排障第一步永远是看状态、看事件kubectl describe pod能告诉你最直接的原因。4. Jenkins编排流水线打通提交代码到滚动上线的最后一公里整套体系中引入Jenkins的价值在于把所有手动操作变成一条可控、可追溯、可重复执行的流水线。你前面通过Docker解决了镜像的构建和交付通过K8s解决了应用的部署和运维而Jenkins负责把“构建-推送-部署”这几个环节串成一个整体。这一章我来讲实际流水线的设计思路以及那些环境变量和权限控制的细节。4.1 Jenkins的两种主要安装方式对比Jenkins的安装部署方式有几种我给你分析一下各自的适用场景。传统的方式是直接在服务器上安装一个独立的Java运行环境然后跑Jenkins war包这是最经典的玩法。优点是排障直观、符合大多数人的使用习惯缺点是和环境耦合较紧升级维护需要花时间。如果你已经折腾过若干次Jenkins部署这套方式应该很顺手。另一种方式是Docker方式运行Jenkins。官方镜像jenkins/jenkins:lts-jdk17可以直接跑。优点是隔离干净升级镜像即可升级Jenkins缺点是容器内外文件权限、Docker socket挂载这些细节需要特别注意。我个人的建议是如果打算做完整的DevOps链路推荐Docker方式运行Jenkins但需要把Docker socket挂载给Jenkins容器。什么意思就是让Jenkins容器能直接调用宿主机的Docker守护进程这样流水线里可以直接执行docker build、docker push这些命令而不需要在Jenkins容器内再套一层Docker也就是所谓的Docker in Docker这个方案弊病不少不建议用。Docker方式启动Jenkins的命令大致长这样docker run -d \ --name jenkins \ -p 8080:8080 \ -p 50000:50000 \ -v /var/jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /usr/bin/docker:/usr/bin/docker \ jenkins/jenkins:lts-jdk17这中间有两个极容易踩的坑。第一个容器内的用户是jenkinsUID 1000而挂载的宿主机目录如果权限设置不对Jenkins容器写不了/var/jenkins_home目录初始化就会失败。解决办法是宿主机上先创建好目录并授权mkdir -p /var/jenkins_home chown -R 1000:1000 /var/jenkins_home第二个挂载Docker socket之后Jenkins容器内执行docker命令实际上操作的是宿主机Docker这带来便利的同时也意味着——任何能创建Jenkins Job的人都相当于拥有了宿主机的root权限。权限分配上必须格外谨慎不能让每个普通开发都随便创建自由的Pipeline任务。4.2 第一次跑通流水线必须搞懂的Jenkins Pipeline语法Jenkins Pipeline有两种写法一种是Declarative Pipeline声明式一种是Scripted Pipeline脚本式。我更推荐声明式因为它结构清晰、强制定义了各个阶段而且配合Blue Ocean界面观看执行过程非常直观。一个典型的声明式Pipeline骨架是这样的pipeline { agent any environment { DOCKER_REGISTRY your-harbor-server.com DOCKER_CREDENTIALS credentials(harbor-admin) } stages { stage(Checkout) { steps { git branch: main, url: https://your-gitlab.com/your-project.git, credentialsId: gitlab-credentials } } stage(Build Image) { steps { script { docker.withRegistry(https://${DOCKER_REGISTRY}, harbor-admin) { def customImage docker.build(${DOCKER_REGISTRY}/library/myapp:${BUILD_NUMBER}) customImage.push() } } } } stage(Deploy to K8s) { steps { script { sh kubectl set image deployment/myapp \ myapp${DOCKER_REGISTRY}/library/myapp:${BUILD_NUMBER} -n production } } } } }这段代码里有几个关键点需要逐一解释。第一个是agent any。它表示这个Pipeline可以在任何一个可用的Jenkins执行器上运行。如果你有多个不同用途的节点比如一个专用于构建镜像的节点可以替换成agent { label docker-builder }。第二个是environment块里的DOCKER_REGISTRY。这个值建议不要直接写死在代码里而是通过Jenkins的配置或者环境变量统一管理。否则不同环境的Harbor地址变了你还得回头改代码不能忍。第三个是docker.withRegistry这个DSL语法。它能自动用给定的凭证harbor-admin登录Harbor完成镜像构建和推送。这里的credentialsId对应的是Jenkins里提前配置好的用户名密码凭证。在Jenkins管理后台的“凭据”页面添加一个“Username with password”的凭据ID填成harbor-admin即可。第四个是部署阶段的kubectl set image命令。这是最直观的一种“滚动更新”做法直接命令K8s把某个Deployment的镜像tag更新K8s会自动触发一次滚动更新逐步替换旧Pod。相比用kubectl apply -f去apply一个新的YAML文件set image更轻量适合快速更新。4.3 参数化构建同一个Job如何同时支持手动选择模块和其他触发方式实际工作中你会遇到一个很典型的需求同一个应用可能分为多个模块日常提交代码时只需要构建发生变更的那个模块而不是每次把整个项目重头构建一遍但有时候测试又希望手动指定构建哪个模块跑完整流程。这个需求在Jenkins里是通过参数化构建实现的。在Pipeline里定义一个Choice参数让用户在点击“立即构建”时选择模块pipeline { agent any parameters { choice(name: MODULE, choices: [all, user-service, order-service, gateway], description: 请选择要构建的模块) string(name: VERSION, defaultValue: v1.0.0, description: 指定镜像版本号) } stages { stage(Checkout) { steps { // 拉取代码 } } stage(Build Selected Module) { steps { script { switch (params.MODULE) { case all: sh ./build-all.sh break case user-service: sh ./build-module.sh user-service break default: sh ./build-module.sh ${params.MODULE} } } } } } }结合定时触发如果你希望每天凌晨都跑一遍全量构建不需要人工值班盯着就在Pipeline里增加一个triggers { cron(H 2 * * *) }当手动参数化构建和定时触发同时存在时Jenkins默认工作得很好手动点构建就走手动参数构建定时触发时参数使用默认值对于choice默认取第一个选项“all”。我之前也担心过这两者会不会冲突实测下来是不会的——定时触发的构建和使用默认参数手动触发构建本质上等价它们会各自生成一次独立的Build记录不会互相干扰。这里有一个我实际踩过的坑想提醒你。当初为了图省事把多个模块的全部代码放在同一个Git仓库里Pipeline里执行 git checkout 时拉取的是整个仓库。当代码仓库膨胀到几个G的时候每次构建光是拉代码就花好几分钟。对这种情况我后来在Pipeline里加入了sparse checkout的优化策略只拉取与本次构建模块相关的目录构建速度提升非常明显。4.4 Jenkins凭证管理和权限控制的细节Jenkins涉及Git凭证、Harbor凭证、K8s的kubeconfig凭证、云平台AK/SK凭证如果管理不当安全性会成为一个很大的隐患。我在这里提供一套简洁但有效的做法。所有凭证统一走Jenkins的“凭据”管理页面。Pipeline里通过credentials()或者withCredentials来引用。例如引用一个SSH私钥类型的凭证withCredentials([sshPrivateKey(credentialsId: gitlab-ssh-key, keyFileVariable: SSH_KEY)]) { sh GIT_SSH_COMMANDssh -i $SSH_KEY -o StrictHostKeyCheckingno git clone gityour-gitlab:yourproject.git }权限控制方面重点介绍如何配置只给某个用户某个Job的权限而不是把所有Jenkins的全部权限都放开。Jenkins插件Role-based Strategy能实现精细化的权限管控。大致做法是安装Role-based Authorization Strategy插件在“Manage Jenkins” - “Security”里把授权策略切换成“Role-Based Strategy”在“Manage and Assign Roles”下创建一个Project角色并用正则表达式指定该角色能访问哪些Job例如/^myapp-.*/表示可以操作所有myapp-前缀的Job把具体用户分配到这个角色名下这样我就能放心地让前端开发只拥有构建自己前端项目的权限无法触达后端部署任务而运维则持有全量管理权限。还有一点关于Jenkins可用环境变量的说明。在Pipeline里像${BUILD_NUMBER}、${JOB_NAME}、${WORKSPACE}这些内置环境变量非常常用。它们的含义分别是构建序号、任务名称、工作区路径。其中BUILD_NUMBER我特别常用因为它天然是单调递增的非常适合作为镜像的tag。每个镜像都用BUILD_NUMBER来标记后续排查问题时分分钟能通过镜像tag反查出是哪次构建产生的格外方便。4.5 Jenkins与GitLab同机部署的兼容性研判有朋友问过我Jenkins和GitLab能不能运行在同一台主机的Docker里这个问题我仔细验证过直接给结论可以前提是宿主机的资源给够。GitLab本身是一个比较吃内存的应用官方建议至少4GB内存才能流畅运转Jenkins作为持续集成服务构建任务多的时候也非常消耗CPU和内存。如果两台服务共用一台8G内存的机器我实测下来机器会比较吃力偶尔会出现构建卡顿或GitLab响应变慢。我的建议是测试环境8G内存起步、16G更稳生产环境单独部署。如果你只是为了本地体验或小团队内网用同机Docker部署完全可行分别映射两个端口GitLab 80/443Jenkins 8080互不冲突。有一点要注意两个容器不要同时挂在同一个加速器端口上启动时用docker run -p指定不同的宿主端口就可以了。5. 从Docker Compose平滑升级到K8s容器编排的演进之路很多团队早期的容器化方案都是从Docker Compose开始。Compose在单机上管理多个容器确实非常方便用一个docker-compose.yml文件就能把一组服务定义清楚然后一条命令docker-compose up -d启动全部服务。但当服务数量增长、需要跨多台宿主机部署时Compose就力不从心了。这需要顺理成章地演进到K8s。好消息是从Compose到K8s是有平滑过渡路线的。5.1 为什么Compose撑不住而K8s可以Compose的编排模型是“一台宿主机上的多个容器”它虽然能定义服务、网络、卷但无法解决跨节点的服务发现、自动扩缩容、故障自愈等分布式场景下的核心问题。而K8s天然把这些能力内建了。最直观的差异表现在扩缩容能力上。Compose想扩展某个服务的副本数需要手动改配置文件再加上--scale参数扩展出来的副本也都在这台机器上无法跨节点调度。K8s里只需要kubectl scale deployment myapp --replicas5自动在集群中挑选可用节点去分布这5个副本遇到有节点宕机K8s会把上面的Pod自动迁移到健康节点重新拉起。这就是云原生架构的“不可变基础设施”思想的落地副本是一种声明期望状态控制器负责去实现这个期望状态。5.2 让Compose时代的镜像无缝跑进K8s好消息是如果你已经有了标准的Docker镜像和Compose配置文件迁移到K8s的成本思想负担比想象中小很多。因为Compose里的服务定义和K8s里的Deployment、Service、ConfigMap在逻辑上一一对应。比如Compose里的一段服务定义services: web: image: nginx:1.25 ports: - 8080:80 environment: - ENVprod对应的K8s Deployment就是apiVersion: apps/v1 kind: Deployment metadata: name: web spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 env: - name: ENV value: prod需要注意的是Compose的ports映射是“宿主机端口:容器端口”K8s通常不推荐这种宿主机端口隔离模式而是建议用Service抽象。因为K8s集群里的Pod IP是随时变化的你不能指望外部直接通过某个固定IP和端口去访问它。正确方式是创建一个Service对象由K8s为Service分配一个稳定的虚拟IPClusterIP并在集群内部做负载均衡和转发apiVersion: v1 kind: Service metadata: name: web-service spec: selector: app: web ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP如果希望从集群外部访问把type改为NodePortK8s会在每个节点上选择一个端口范围默认30000-32767转发到Service。生产环境通常再用Ingress如Nginx Ingress Controller或Traefik统一管理外部访问入口基于域名做路由转发避免直接暴露一堆NodePort端口。从实践来看Compose迁移到K8s最容易出错的不是YAML写不写得出来而是你还没适应“Pod是临时动物”这个新的世界观。在Compose时代你习惯盯着某个固定容器看日志到K8s里Pod随时可能因为调度、重启、故障被销毁重建你需要尽快习惯地把注意力放到Deployment、Service、以及各种控制器的期望状态上而不是某个具体Pod本身。6. 这套体系跑起来之后那些高频故障和完整排查思路讲完构建链路最要紧的还是运维阶段的真实故障。这里我把这套体系上线这几个月以来遇到频率最高的几个问题以及我的完整排查思路整理出来这部分内容价值比较高建议你先收藏等出了类似问题再翻回来对照看看。6.1 K8s Pod卡在ContainerCreating到底卡在哪一步Pod一直处于ContainerCreating状态是新手和高频碰到最多的现象。排查的第一步不是盯着kubectl get pods干等而是用kubectl describe pod查看详细事件kubectl describe pod pod-name -n namespace事件里通常会出现几类不同的信息对应的原因和处理方式完全不同。一种常见的情况是镜像拉取失败事件里会有Failed to pull image或者ImagePullBackOff。这就要检查镜像名写没写错、镜像是否存在、拉取凭证是否配好。尤其用到私有Harbor时如果镜像名里带了Harbor的地址但Secret没创建或没在Deployment里引用事件里会出现x509: certificate signed by unknown authority或unauthorized: authentication required这就是凭证问题去查Secret的创建和引用是否正确。另一种常见情况是端口占用或硬盘资源不足事件里明确告诉你端口bind失败或者Failed to create pod sandbox。这种事件通常会伴随具体的报错信息比如no space left on device说明docker数据目录爆了。处理方式建议清理无用的镜像和容器docker system prune -a --volumes还有一种是PVC一直无法挂载事件里提示FailedMount。这种情况一般是PV没有绑定成功PVC处于Pending状态。用kubectl get pvc查看PVC状态如果确实是Pending再用kubectl describe pvc查看底层卷提供商返回的错误。排查思路切记一条铁律先看事件再看日志最后看网络。不要一上来就凭感觉重启所有东西。事件里往往藏着最直接的原因。6.2 Pod反复重启的CrashLoopBackOff日志与探针的配合诊断另一个高频问题就是Pod能创建成功但一直处于CrashLoopBackOff状态意思就是容器启动后立即崩溃被K8s反复重启进入退避循环。排查第一件事肯定是看日志kubectl logs pod-name -n namespace如果Pod在启动过程中就崩了日志可能很短只看到类似exec /bin/sh: exec format error之类的启动报错这说明镜像的启动脚本跟你声明的command参数不匹配或者说镜像构建时平台不对比如构建了arm64的镜像试图跑在amd64节点上。另一个容易被忽略的原因和健康检查探针有关。如果你的Deployment配置了livenessProbe存活探针并且探针的路径、端口配置错误导致K8s认为容器不健康也会主动重启容器甚至把它当成反复崩溃来重启。所以看到CrashLoopBackOff不要一心只怀疑应用本身要看日志里是否有探针失败的痕迹。使用kubectl describe pod查看Events列表里有没有Liveness probe failed或者Readiness probe failed。排查出来之后第一步不是改应用而是确认探针的路径是否正确、端口是否对应容器内的监听端口、超时时间是否太短导致误判。6.3 DNS解析时好时坏CoreDNS的配置是重点集群里经常有应用之间通过Service域名互相访问比如前端Pod通过http://backend-service访问后端服务。如果出现时好时坏的连接失败很大概率就是CoreDNS出了状况。CoreDNS是K8s集群内的DNS服务负责把Service名称解析成ClusterIP。排查DNS问题的流程是先确认CoreDNS Pod本身是否健康kubectl get pods -n kube-system | grep coredns如果CoreDNS的Pod一直处于Restic状态或者大量重启查看它的日志kubectl logs -n kube-system -l k8s-appkube-dns --tail100常用的定位方法是在故障应用所在的Pod里直接测试DNS解析kubectl exec -it pod-name -n namespace -- nslookup backend-service如果提示解析不到检查Service的名称和命名空间是否写对。跨命名空间的访问要用backend-service.production.svc.cluster.local这样的FQDN。还有一个小细节如果节点上的/etc/resolv.conf配置了特殊的本地DNS参数可能会影响Pod内DNS解析。K8s的Pod默认dnsPolicy是ClusterFirst意思是优先使用集群内的CoreDNS。如果你在Pod定义里设置了hostNetwork: trueDNS策略可能会继承宿主机的配置造成解析不规则。6.4 镜像拉取慢的应急预案即使配置了镜像加速器也难免遇到某些镜像就是拉不下来或者特别慢的情况。特别是在新节点加入集群时需要一次性拉取所有基础镜像那速度简直让人着急。我的补救思路有两条第一条提前预拉镜像。集群里跑的业务相对固定可以做一个离线镜像包把常用的基础镜像Nginx、MySQL、Redis、自研应用镜像提前在部署节点上手动docker pull一遍后续Pod调度到这些节点时就不用临时拉取。第二条给Containerd或Docker配置镜像仓库的代理加速。如果你的私有Harbor是内网访问的可以在K8s节点上配置HTTP代理让Docker拉取公共镜像时走稳定通道。这个方法配置项比较琐碎核心是在Docker的daemon.json里增加代理设置。这里有一个安全前提代理服务器的出口必须是合法合规的不能用任何不合规方式绕过网络管理的要求。6.5 多节点通信异常时的网络排查链路K8s集群节点间通信出现问题应用间的访问会大面积失败这种问题最让人压力大。排查通常从Service层面开始kubectl get svc -A kubectl get endpoints -Aendpoints的列表能告诉你Service后面实际关联的Pod IP。如果endpoints为空说明Service的selector没匹配到任何Pod检查Deployment的labels和Service的selector是否对得上。如果endpoints正常应用还是访问不通就要在故障Pod里直接测试ClusterIPkubectl exec -it pod-name -n namespace -- curl http://cluster-ip如果ClusterIP都不通大概率是网络插件问题了。Calico的Pod状态要先确认kubectl get pods -n kube-system | grep calicoCalico正常运行时有种经典问题是节点之间OpenStack或VPC安全组把BGP端口封了导致Calico网络建不起来。这种问题排查起来相当隐蔽延绵好几个小时。我的建议是任何时候做跨节点通信实验之前先确保自己的基础设施安全组和系统防火墙没有屏蔽节点之间的通讯端口比如Calico用的BGP 179端口。基础网络不通上层K8s怎么折腾都白搭。7. 结合业务规模做选型时的常见问题答疑最后这部分我总结了一些网友和同事问得最多的问题这些问题没有绝对唯一的答案但我会给出符合大多数场景的经验判断希望能帮你少走一些弯路。7.1 云原生AI运维优化与自动化交付项目怎么结合这套体系最近AI和大模型相关的项目比较多围绕K8s的AI运维优化也常常被问起。如果你做的自动化交付项目需要承载AI训练或推理任务那么这套体系依然可以沿用但需要在几个地方做针对性的加强。AI训练任务通常需要GPU资源。K8s集群需要提前安装Device Plugin组件如NVIDIA Device Plugin然后在Pod配置里声明resources.limits: nvidia.com/gpu: 1调度器才能识别GPU资源并分配。这部分我测试下来稳定性尚可但要注意GPU节点的驱动和容器运行时兼容性。另一个与AI更相关的是模型版本的管理。模型的版本和服务代码的版本一样需要被记录、被回滚。可以把模型文件同时打进镜像做成带版本号的纯镜像交付物或者把模型挂在对象存储中在部署时通过环境变量指定版本路径。这两种方式前者简化了部署流程后者更灵活且镜像更小。如果你倾向于频繁更新模型但代码很少改动我建议后者。7.2 Docker Desktop和Linux的Docker有什么区别很多朋友在Windows上学习Docker用的是Docker Desktop。这里我觉得有必要明确一个使用边界。Docker Desktop在Windows上依赖虚拟化技术如WSL2或Hyper-V运行Linux虚拟机在开发联调阶段完全可以正常使用。包括安装Nginx、MySQL、Redis这些开发依赖Docker Desktop完全胜任。但如果是部署到生产环境的K8s集群那肯定是以Linux服务器为主。如果你在Docker Desktop上测试K8s可以直接用它内置的Kubernetes功能或者配合kindKubernetes in Docker工具做单机测试效果和体验都很不错。但和学习生产级部署的区别要心里有数Docker Desktop屏蔽了很多网络、存储和系统底层的细节动手部署时还是建议在Linux虚拟机上从零搭建一遍这个过程才能真正掌握K8s的完整原理。7.3 Docker和K8s的区别一句话说透有不少刚入门的朋友分不清Docker和K8s的区别。我提供一个特别直白的类比Docker相当于集装箱制造厂它把货物打包成一个个标准集装箱容器镜像K8s相当于港口调度系统它负责把成千上万只集装箱正确分配到不同的货船节点上并且管理它们的启航、靠泊和冗余。简单说Docker容器化是基础K8s编排是平台。一个管打包一个管调度。两者联动才能构成完整的云原生基础设施底座。7.4 学习路线图和时间投入预期关于云原生学习路线我经常被问到“从零到熟练掌握这套体系需要多久”。我根据自己带过的新人团队的实际节奏给出一个保守但现实的预期基础阶段2-4周掌握Docker的镜像、容器、卷、网络、Compose的使用。这个阶段去B站等视频平台找几个完整的实战教程跟着把LNMP环境用Docker跑起来基本就入门了。不用贪多但一定要动手。进阶阶段4-6周掌握K8s常用对象Pod、Deployment、Service、Ingress、ConfigMap、Secret、PVC的用法独立部署一套单Master集群并成功部署一次LNMP架构。这个阶段重点理解Pod的调度逻辑和Service的流量转发机制。DevOps串联阶段2-4周安装Jenkins把Git代码提交到触发构建、推送镜像、更新K8s Deployment整条Pipeline跑通。体会参数化构建、凭证管理、多分支流水线这些功能的实际用途。整体下来两到三个月的业余时间投入能完成基本功。但如果你在过程中不断遇到一些奇怪的网络、存储问题那这些“意外”本身就是最宝贵的经验踩过的每一个坑都会加速你的成长。8. 再聊几个我来回折腾多次后沉淀下来的经验整套体系跑通到现在有几个经验是我想特别多强调几句的因为它们不是看官方文档能直接看出来的而是需要在实际操作中反复碰壁才能体会到。第一件事是关于镜像仓库的规划。我见过一些团队Harbor只要能用就开始用从来没有做过镜像清理策略。跑了半年仓库里堆满了不同tag的镜像占用好几个T的存储空间。我建议从第一天就启用Harbor的镜像清理规则保留最近几个版本比如每条tag只保留最近10个过期自动回收。存储省下来了排查版本问题也更清爽。第二件事是Jenkins的插件管理。我强烈建议不要为了图方便“建议安装”所有插件。插件太多版本冲突和启动缓慢的问题会扑面而来。只安装实际需要的插件典型的有Blue Ocean、Pipeline、Git、Kubernetes CLI、Role-based Strategy、Docker Pipeline这几个就够了。装得太多升级时维护成本呈指数增长。第三件事是日志收集的提前规划。K8s里的Pod是随时流动的容器一旦被销毁重建日志就随容器一起消失了。想排障的时候发现日志没了那种无力感我这辈子不想再经历。所以整套体系里一定要有一个日志接收端最轻量的方案是部署一套LokiPromtail或者ELK全家桶也行。Promtail收集每个节点上容器日志Loki存储和查询这套东西搭好之后感觉就像给系统加装了行车记录仪排查问题心里有底。第四件事是关于开发环境和生产环境的隔离。有不少小团队图省事开发和测试环境共用一套K8s集群用Namespace区分。我可以明确告诉你这会在资源隔离层面制造麻烦。有一些Pod在开发环境里疯狂吃资源很容易影响生产应用。有条件就让集群全隔离别在同一套集群里混跑不同环境。这些经验对我来说都是真金白银买来的分享出来希望你能少走几段弯路。整套DockerK8sJenkins的链路单看每一环都不难理解但把它们捏合成一个稳定的自动化体系确实需要大量实践。如果你正在搭建自己的体系遇到具体报错可以照着文章里的排查链路一步不落地走一遍大概率能自己找到答案。