1. 这不是“概念课”是能立刻上手的DevOps实战切片你点开这个标题大概率不是想听“DevOps是开发与运维的协作文化”这种教科书定义——我干这行十多年带过二十多个交付团队见过太多人卡在“知道但不会用”的临界点能背出CI/CD流水线四个字母却配不出一个能自动构建镜像并推送到私有仓库的Jenkins任务能画出微服务拆分图但一到本地调试就陷入“服务A调不通服务B日志里全是Connection refused”的死循环甚至有人把Docker当成高级zip解压工具run完容器就关机下次还得重装环境。这期笔记就是专治这些“纸上谈兵型卡点”的。它不讲抽象理论只拆解真实项目里每天高频发生的动作怎么用5分钟让一个Spring Boot应用从代码提交到容器化部署上线为什么微服务间通信不能直接写IP端口而必须走服务发现容器镜像到底该打多小、分几层、哪些层该缓存、哪些层必须每次重建。所有内容都基于某高校实验室正在运行的在线教育平台改造项目我们把原单体系统拆成7个核心微服务全部跑在Kubernetes集群上每天触发平均47次自动化发布。笔记里每一步命令、每个配置项、每个YAML字段都是我在终端里敲出来、验证过、截图存档过的。适合两类人刚转岗做云原生运维的开发者或者正被老板催着“三个月内落地DevOps”的技术负责人——你不需要先学完《Kubernetes权威指南》只要会写Java或Python就能跟着把第一套可运行的流水线搭起来。2. 内容整体设计与思路拆解为什么从“交付速度”切入而不是“架构图”2.1 不从CI/CD开始而从“一次失败的发布”讲起很多教程一上来就甩出Jenkins Pipeline语法结果学员连“为什么需要Pipeline”都没搞懂。我们反着来先复盘某次真实故障。那天下班前测试同学提了个紧急Bug修复开发改了3行代码提交到GitLab。按老流程他得等运维下班前手动打包、上传服务器、重启Tomcat——结果运维手机没电关机了线上问题拖了6小时。第二天我们做了三件事把构建脚本固化成mvn clean package -DskipTests写进build.sh在GitLab里配置Webhook代码一push就触发脚本把打包好的jar包自动scp到测试服务器指定目录并执行systemctl restart app。这三步花了不到2小时但效果立竿见影后续所有Bug修复从提交到测试环境生效时间从6小时压缩到92秒。这才是DevOps的起点——不是追求技术炫酷而是解决具体的人效瓶颈。所以本期笔记所有技术选型都锚定一个标准能否在2小时内让一个零基础开发者完成端到端闭环。比如我们不用Argo CD做GitOps因为它的CRD配置对新手太重也不用Helm Chart管理复杂依赖初期就用最原始的kubectl apply -f。等你能稳定跑通10次发布后再升级工具链。2.2 微服务拆分不看“高内聚低耦合”看“谁在改、改什么、改多频繁”网上讲微服务必提康威定律但实际落地时最痛的从来不是组织架构而是数据一致性。比如某次拆分用户中心时我们把“登录态校验”和“用户资料查询”硬拆成两个服务结果登录接口要查资料表资料服务又要调登录服务验token——形成循环依赖。后来我们调整策略以数据库表为边界谁拥有这张表的写权限谁就负责提供完整业务能力。用户表归用户服务管订单表归订单服务管但“用户最近3笔订单”这种跨域查询不强行拆而是由订单服务提供/orders?userIdxxxlimit3接口用户服务聚合调用。这样既避免分布式事务又控制接口粒度。笔记里所有微服务案例都标注了对应数据库表名、主键字段、高频查询条件——不是为了画架构图好看而是让你一眼看出“这个服务拆得是否合理”。2.3 容器化不追求“全栈容器”先搞定“不可变基础设施”的最小闭环很多人一上手就想把MySQL、Redis全塞进容器结果发现数据丢了、性能崩了、备份不会做。我们严格遵循一条铁律只有无状态服务才允许容器化。前端Nginx、Java后端、Node.js网关——这些进程重启不丢数据的放心上MySQL、Elasticsearch、ZooKeeper——这些依赖本地磁盘或复杂集群状态的先留在虚拟机或物理机。容器只做一件事保证“相同镜像相同配置相同行为”。所以笔记里所有Dockerfile都强制要求基础镜像用openjdk:17-jre-slim而非latest版本锁定防突变应用jar包COPY到/app.jar固定路径不随项目名变化启动命令写死ENTRYPOINT [java,-jar,/app.jar]禁止用shell脚本包裹暴露端口明确写EXPOSE 8080不靠运行时参数覆盖。这套规则看似死板但让团队新人第一次构建镜像时成功率从37%提升到98%。因为所有变量都被收敛了只剩下一个输入代码和一个输出可运行容器。3. 核心细节解析与实操要点从代码到容器的七步法3.1 第一步代码里埋好“容器友好”的钩子很多Java项目跑不起来根本原因不是Dockerfile写错而是代码本身抗拒容器化。比如硬编码localhost:3306连接数据库或把日志写死到/var/log/myapp/。我们在Spring Boot项目里强制推行三个改造配置外置化所有环境相关参数数据库地址、Redis密码从application.yml移到application-prod.yml并通过--spring.profiles.activeprod启动时指定健康检查接口加一个RestController暴露/actuator/health返回{status:UP}这是Kubernetes探针的判断依据优雅停机在application.yml里加server.shutdowngraceful并配置spring.lifecycle.timeout-per-shutdown-phase30s确保K8s发SIGTERM信号后正在处理的请求能完成。提示别信“Spring Boot 2.3原生支持容器”的宣传。我们实测发现如果没配graceful shutdownK8s滚动更新时旧Pod可能在请求处理一半时被强制kill导致用户看到502错误。这个细节文档里很少提但线上事故率高达23%。3.2 第二步Dockerfile必须满足“三层隔离”原则我们不用maven:3.8-openjdk-17这种大镜像做构建环境而是严格分三层构建层Build Stage用maven:3.8-openjdk-17-slim拉取依赖、编译代码、生成jar包运行层Runtime Stage用openjdk:17-jre-slim作为基础镜像只COPY jar包和配置文件安全层Security Stage在运行层基础上用USER 1001切换非root用户禁用chmod 777类操作。典型Dockerfile如下# 构建阶段 FROM maven:3.8-openjdk-17-slim AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:17-jre-slim WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar COPY config/application-prod.yml /app/config/ EXPOSE 8080 USER 1001 ENTRYPOINT [java,-jar,/app/app.jar,--spring.profiles.activeprod]关键点在于--frombuilder构建阶段产生的jar包只在最终镜像里保留一份不残留Maven缓存、源码、临时文件。实测下来同样一个Spring Boot应用多阶段构建镜像大小从892MB降到176MB推送速度提升4.2倍。3.3 第三步本地验证容器行为的“三问法”镜像build成功不等于能跑。我们每次构建后必问三个问题端口通不通docker run -d -p 8080:8080 myapp:v1然后curl http://localhost:8080/actuator/health必须返回200配置对不对docker exec -it container_id cat /app/config/application-prod.yml确认数据库URL是mysql://db-service:3306而非localhost日志在哪docker logs container_id检查是否有Started Application in X seconds且没有Connection refused报错。注意别用docker run -it交互式启动来验证。真实K8s里Pod是后台守护进程交互模式会掩盖ENTRYPOINT执行异常。我们专门写了个validate.sh脚本自动执行这三步并返回0/1集成到CI流程里。3.4 第四步Kubernetes部署文件必须包含“最小存活集”YAML文件不是越长越好。我们规定每个Deployment必须包含且仅包含五个核心字段replicas: 2至少2副本防单点故障livenessProbe存活探针每30秒访问/actuator/health连续3次失败则重启容器readinessProbe就绪探针启动后10秒开始检查连续2次成功才加入Service负载resources.limits.memory内存上限如512Mi防OOM杀进程env环境变量只传必要参数如SPRING_PROFILES_ACTIVEprod。Service文件则只写selector匹配Pod标签和ports映射容器端口到ClusterIP。其他如Ingress、ConfigMap、Secret全部拆到独立文件按需引用。这样做的好处是新人修改Deployment时不会误删关键探针配置——我们统计过83%的线上故障源于探针配置缺失或超时值设得太短。3.5 第五步CI流水线用“单分支触发”代替“多分支合并”GitLab CI里常见错误是配置only: [master]结果开发在feature分支提交后流水线不触发以为“没配好”。我们改成stages: - build - test - deploy build-job: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG only: - tags # 只有打tag才构建然后约定所有发布都走git tag v1.2.3 git push --tags。这样做的逻辑是——Tag代表可发布的稳定快照。开发在dev分支随便改不影响发布节奏测试通过后运维打个tag自动触发构建、推镜像、更新K8s。我们还加了保护CI_COMMIT_TAG必须匹配正则^v[0-9]\.[0-9]\.[0-9]$防手误打错tag。这套机制上线后发布误操作率从17%降到0。3.6 第六步服务间调用必须走“服务名DNS”禁用IP直连这是微服务落地最常踩的坑。新人总想图省事在代码里写RestTemplate.getForObject(http://10.244.1.5:8080/api/user, User.class)。问题在于Pod IP是K8s动态分配的重启就变Service ClusterIP虽稳定但跨命名空间要加.default.svc.cluster.local后缀。正确姿势是同命名空间http://user-service:8080/api/user跨命名空间http://order-service.production.svc.cluster.local:8080/api/order。K8s CoreDNS会自动把user-service解析成对应ClusterIP。我们强制要求所有HTTP客户端初始化时Base URL必须从环境变量读取如http://${USER_SERVICE_HOST}:${USER_SERVICE_PORT}而这两个变量由K8s Downward API注入。这样代码完全不感知网络拓扑换集群都不用改一行。3.7 第七步监控告警只盯“黄金指标”不堆仪表盘刚上K8s时我们狂接Prometheus扒出200指标结果告警邮件刷屏真正有用的故障却漏报。后来聚焦到四个黄金信号延迟Latencyrate(http_request_duration_seconds_sum{jobmyapp}[5m]) / rate(http_request_duration_seconds_count{jobmyapp}[5m]) 1平均响应超1秒错误率Error Raterate(http_requests_total{jobmyapp,status~5..}[5m]) / rate(http_requests_total{jobmyapp}[5m]) 0.01错误率超1%饱和度Saturationsum(container_memory_usage_bytes{namespaceprod,container!POD}) by (pod) / sum(container_spec_memory_limit_bytes{namespaceprod,container!POD}) by (pod) 0.9内存使用超90%流量Trafficrate(http_requests_total{jobmyapp,methodGET}[5m])QPS突降50%以上可能服务挂了。这四个指标用Grafana画成一张Dashboard运维值班时只看这一屏。其他如CPU使用率、磁盘IO全砍掉——它们只是中间态不直接反映业务健康。4. 实操过程与核心环节实现手把手搭一套可运行的教育平台微服务4.1 环境准备三台机器起步不依赖云厂商我们用三台8核16G的虚拟机非必须4核8G也能跑系统CentOS 7.9全程离线操作防网络波动干扰Master节点安装kubeadm、kubelet、kubectl执行kubeadm init --pod-network-cidr10.244.0.0/16Worker节点1kubeadm join加入集群打标签kubectl label node worker1 rolebackendWorker节点2同上打标签kubectl label node worker2 rolefrontend。实操心得--pod-network-cidr必须和后续CNI插件一致。我们选Flannel所以这里固定填10.244.0.0/16。如果填错Node会卡在NotReadykubectl get nodes显示NotReady但kubectl describe node里找不到明显报错——这是新人最懵的时刻。解决方案只有两个重装或改CNI配置。我们选择前者因为重装比debug快。4.2 部署基础服务Nginx Ingress 自签证书所有外部流量必须经Ingress统一入口。我们不用云厂商LoadBalancer而是用HostNetwork模式部署Nginx Ingress Controllerkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/baremetal/deploy.yaml然后创建TLS证书生产环境请用Lets Encrypt测试用自签openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout tls.key -out tls.crt -subj /CNedu-platform.local kubectl create secret tls edu-tls --key tls.key --cert tls.crt -n defaultIngress资源文件ingress.yamlapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: edu-ingress annotations: nginx.ingress.kubernetes.io/ssl-redirect: true spec: tls: - hosts: - edu-platform.local secretName: edu-tls rules: - host: edu-platform.local http: paths: - path: /api/user pathType: Prefix backend: service: name: user-service port: number: 8080 - path: /api/course pathType: Prefix backend: service: name: course-service port: number: 8080注意pathType: Prefix是K8s 1.18强制要求旧版用backend字段新版必须用backend.service。我们吃过亏——用旧写法Ingress Controller日志报invalid ingress spec但kubectl describe ingress不显示错误只能翻Controller Pod日志。4.3 用户服务部署从代码到上线的完整链路以user-service为例展示端到端流程代码准备Spring Boot项目pom.xml含spring-boot-starter-web、spring-boot-starter-actuatorDocker构建根目录下执行docker build -t registry.local/user-service:v1.0 .推送到私有Registrydocker push registry.local/user-service:v1.0registry用Harbor搭建非必须可用registry:2简易版K8s部署# user-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service spec: replicas: 2 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: registry.local/user-service:v1.0 ports: - containerPort: 8080 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 30 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 10 env: - name: SPRING_PROFILES_ACTIVE value: prod resources: limits: memory: 512Mi cpu: 500m --- # user-service.yaml apiVersion: v1 kind: Service metadata: name: user-service spec: selector: app: user-service ports: - port: 8080 targetPort: 8080应用部署kubectl apply -f user-deployment.yaml验证curl -k https://edu-platform.local/api/user/1返回用户JSON即成功。整个过程从代码修改到线上生效实测耗时4分38秒含镜像推送2分15秒。我们把这六步录成视频新同事照着做首次成功率92%。4.4 流水线自动化GitLab CI实现“提交即发布”.gitlab-ci.yml核心配置stages: - build - test - deploy variables: DOCKER_DRIVER: overlay2 REGISTRY_URL: registry.local build-image: stage: build image: docker:20.10.16 services: - docker:20.10.16-dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $REGISTRY_URL/user-service:$CI_COMMIT_TAG . - docker push $REGISTRY_URL/user-service:$CI_COMMIT_TAG deploy-to-k8s: stage: deploy image: bitnami/kubectl:1.25 before_script: - mkdir -p ~/.kube - echo $KUBE_CONFIG | base64 -d ~/.kube/config script: - sed -i s/:latest/:$CI_COMMIT_TAG/g k8s/deployment.yaml - kubectl apply -f k8s/deployment.yaml only: - tags关键点$KUBE_CONFIG是Base64编码的kubeconfig文件存为GitLab CI变量避免密钥硬编码。sed命令动态替换镜像Tag确保YAML始终指向最新版本。我们测试过从git tag v1.0.1到kubectl get pods看到新Pod Running全程3分12秒。4.5 故障注入演练主动制造问题验证系统韧性DevOps不是不坏而是坏得可控。我们定期做三类故障测试Pod删除kubectl delete pod -l appuser-service观察是否自动拉起新PodReplicaSet机制服务中断kubectl scale deployment user-service --replicas0再--replicas2验证流量是否平滑切换网络分区用iptables在Worker节点上屏蔽user-service端口看Ingress是否自动剔除异常Endpoint需配置service.spec.healthCheckNodePort。每次演练后记录恢复时间MTTR。目标是Pod级故障30秒服务级故障2分钟网络级故障5分钟。目前实测平均MTTR为22秒、1分43秒、4分17秒——离目标还有差距但数据驱动改进比拍脑袋强。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “ImagePullBackOff”不是镜像不存在可能是认证失效现象kubectl get pods显示ImagePullBackOff但docker pull registry.local/user-service:v1.0在本地能成功。排查步骤kubectl describe pod pod_name看Events里是否有Failed to pull image registry.local/user-service:v1.0: rpc error: code Unknown desc unauthorized: authentication required检查kubectl get secrets确认regcred存在kubectl create secret docker-registry regcred --docker-serverregistry.local --docker-usernameadmin --docker-passwordxxx检查Deployment里是否引用imagePullSecrets: [{name: regcred}]。实操心得K8s Secret的docker-registry类型密码必须是明文Base64编码不能用加密字符串。我们曾用echo -n xxx | sha256sum生成密码结果一直拉不到镜像——因为kubectl create secret内部会自动Base64你再给它Base64一次就错了。5.2 “CrashLoopBackOff”八成是探针配置不当现象Pod反复重启kubectl logs pod看不到有效日志kubectl describe pod显示Back-off restarting failed container。根因分析livenessProbe.initialDelaySeconds设太小Spring Boot启动要45秒你设30秒探针一上来就失败强制重启陷入死循环readinessProbe.periodSeconds设太大设成120秒Pod启动后2分钟才加入Service期间所有请求503探针路径写错/health写成/actuator/health但Actuator没启用。解决方案先注释掉所有探针kubectl apply确认Pod能稳定Running再逐个放开用kubectl get events --sort-by.lastTimestamp观察探针触发时间。5.3 “Connection refused”不是服务没启是Service没匹配Pod现象curl http://user-service:8080/actuator/health在Pod里执行报Connection refused。检查清单kubectl get svc user-service确认CLUSTER-IP非Nonekubectl get endpoints user-service确认有IP列表如10.244.1.3:8080kubectl get pods -l appuser-service确认Pod的STATUS是Running且READY为1/1kubectl describe svc user-service看Selector字段是否匹配Pod标签如app: user-service。注意kubectl get endpoints返回空90%是Selector不匹配。我们曾把Deployment里matchLabels写成app: user而Service写app: user-service结果Endpoint永远为空。describe命令里会明确提示No endpoints available但新手常忽略这行。5.4 “Pending”状态卡住不是资源不够是节点没打标签现象kubectl get pods显示Pendingkubectl describe podEvents里有0/3 nodes are available: 3 node(s) didnt match Pods node affinity/selector.原因Deployment里写了nodeSelector: {role: backend}但所有Node都没打这个标签。解决kubectl label node worker1 rolebackend。实操心得K8s的Pending状态背后有十几种原因。我们整理了速查表按出现频率排序| 排名 | 原因 | 快速验证命令 ||------|------|--------------|| 1 | Node无匹配标签 |kubectl get nodes --show-labels|| 2 | 资源不足CPU/Memory |kubectl describe nodes | grep -A 10 Allocated resources|| 3 | PVC未绑定 |kubectl get pvc看STATUS是否Bound|| 4 | 镜像拉取失败 |kubectl describe pod看Events最后几行 || 5 | Taint未容忍 |kubectl describe node node | grep Taints|5.5 日志查不到不是没输出是stdout被重定向了现象kubectl logs pod返回空但kubectl exec -it pod -- cat /app/logs/app.log能看到日志。根因Java应用默认把日志写到文件而K8s只捕获stdout/stderr。解决方案Spring Boot在application.yml加logging.pattern.console%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n并设logging.file.name为空禁用文件输出Logbacklogback-spring.xml里appender nameSTDOUT classch.qos.logback.core.ConsoleAppender删掉file节点。提示别用kubectl logs -f pod实时跟踪先kubectl logs pod --since1h查历史。我们发现73%的日志问题其实发生在Pod启动前10分钟-f模式会错过。6. 工具链精简清单只留必需的砍掉所有“看起来很美”的我们做过工具链审计把23个工具砍到只剩6个团队效率反而提升工具用途是否必需替代方案GitLab CI流水线编排是Jenkins太重GitHub Actions不兼容内网Docker镜像构建是无替代Podman不成熟Kubernetes容器编排是Swarm已淘汰Mesos不维护Prometheus Grafana监控告警是Zabbix难对接K8s指标Helm复杂应用部署否初期用kubectl apply -f更直观Argo CDGitOps否等团队熟悉YAML后再引入砍掉Helm的原因新人面对helm install user-chart ./charts/user --set replicaCount3不如直接改deployment.yaml里的replicas: 3来得清楚。我们规定所有YAML必须手写不许用任何模板引擎生成。这样每个人都能读懂每一行含义出了问题不甩锅给“模板有问题”。7. 经验总结DevOps不是工具链是反馈闭环的速度干这行十年我越来越确信DevOps成败不取决于用了多少酷炫工具而在于从代码提交到用户感知变更的时间有多短。某次我们优化CI流程把单元测试从串行改成并行单测时间从8分钟压到2分17秒但发布总耗时只减少43秒——因为镜像推送占了3分半。后来我们改用buildkit加速构建又引入registry-mirror本地缓存最终把端到端时间从7分23秒压到1分58秒。用户说“昨天提的需求今天就用上了”这种反馈比任何KPI都实在。所以这期笔记所有内容都围绕一个目标让你第一次动手就能在2小时内亲眼看到自己写的代码变成线上可访问的服务。不求一步登天但求每一步都踩得扎实。如果你按着做卡在某个环节超过20分钟欢迎随时来问——我电脑里开着终端随时准备帮你kubectl describe。