
一、我们手动发布的那段黑暗时光2019年我在一家中型互联网公司做后端。那个时候每次发版都是一场噩梦。周四晚上全组人留守等着发布。发布步骤是这样的开发人员在本地打War包传到服务器运维人员登录服务器备份旧包停掉Tomcat替换War包启动Tomcat检查日志如果有问题手动回滚听起来原始吧但这确确实实就是我们当时的操作。更要命的是因为是手动操作经常出现测试环境好好的代码到生产就出问题因为配置文件不一样回滚的时候发现备份的包版本搞混了凌晨2点发布所有人都困得不行操作失误有一次我们的一个同事困了把生产数据库的配置覆盖成了测试数据库的配置导致生产环境连不上数据库。凌晨3点紧急回滚全组人折腾到早上6点才恢复正常。那次之后我们痛定思痛开始搭建CI/CD流水线。二、Git Flow分支策略规范是协作的基础在搭建流水线之前首先要规范Git分支策略。我们采用了Git Flow的简化版master生产分支 ↑ | merge | release/1.0.0发布分支 ↑ | merge | develop开发分支 ↑ | merge | feature/xxx功能分支从develop切出 hotfix/xxx热修复分支从master切出我们的分支策略规则feature分支从develop切出开发完成后合并回develop命名规范feature/功能描述-开发者姓名release分支在发版前从develop切出测试通过后合并到master和develophotfix分支从master切出修复后同时合并到master和develop所有合并必须通过Merge Request禁止直接push到受保护分支Merge Request必须经过至少1人Code Review才能合并踩坑记录1分支命名混乱最开始大家分支命名随心所欲有人叫new-feature有人叫功能新增还有人直接用中文。导致merge request列表完全无法理解每个分支在做什么。解决制定了严格的命名规范并用GitLab的Protected Branch规则强制执行。三、Jenkinsfile多环境构建有了分支规范后开始搭建Jenkins流水线。我们的技术栈是Java Maven Spring Boot。3.1 Jenkinsfile核心配置pipeline{agent any// 定义环境变量environment{REGISTRYregistry.example.comAPP_NAMEorder-serviceDOCKER_IMAGE${REGISTRY}/${APP_NAME}:${BUILD_NUMBER}}stages{// 第一阶段代码检出stage(Checkout){steps{checkout scm script{env.GIT_COMMIT_SHORTsh(script:git rev-parse --short HEAD,returnStdout:true).trim()}}}// 第二阶段代码检查stage(Code Check){stages{stage(SonarQube Scan){steps{withSonarQubeEnv(SonarQube){sh mvn clean verify sonar:sonar \ -Dsonar.projectKey${APP_NAME} \ -Dsonar.projectVersion${BUILD_NUMBER} }timeout(time:5,unit:MINUTES){waitForQualityGate(true)}}}stage(Checkstyle){steps{shmvn checkstyle:check}}}}// 第三阶段单元测试stage(Unit Test){steps{shmvn clean test -Dspring.profiles.activetest}post{always{junittarget/surefire-reports/*.xmljacoco execPattern:target/jacoco.exec}}}// 第四阶段构建Docker镜像stage(Build Push Image){steps{script{// 构建镜像sh docker build -t${DOCKER_IMAGE}\ -t${APP_NAME}:${GIT_COMMIT_SHORT}. // 推送到镜像仓库sh docker push${DOCKER_IMAGE}docker push${APP_NAME}:${GIT_COMMIT_SHORT}// 清理本地镜像shdocker rmi${DOCKER_IMAGE}|| true}}}// 第五阶段部署到测试环境stage(Deploy to Test){steps{script{deployEnvtestnamespacetest-ns// 更新K8s Deploymentsh kubectl set image deployment/${APP_NAME}\${APP_NAME}${DOCKER_IMAGE}\ -n${namespace}// 等待滚动更新完成sh kubectl rollout status deployment/${APP_NAME}\ -n${namespace}\ --timeout300s }}}// 第六阶段集成测试stage(Integration Test){steps{sh newman run postman/collection.json \ --environment postman/test.env.json \ --reporters cli,junit \ --reporter-junit-export results/test-results.xml }post{always{junitresults/test-results.xml}}}// 第七阶段部署到预发环境stage(Deploy to Staging){when{branchrelease/*}steps{script{deployEnvstagingnamespacestaging-nssh kubectl set image deployment/${APP_NAME}\${APP_NAME}${DOCKER_IMAGE}\ -n${namespace}// 预发环境需要人工确认timeout(time:2,unit:HOURS){input message:确认在预发环境测试通过,submitter:dev-lead,qa}}}}// 第八阶段部署到生产环境stage(Deploy to Production){when{branchmaster}steps{script{deployEnvprodnamespaceprod-ns// 生产发布前自动备份sh kubectl exec statefulset/mysql -n${namespace}-- \ mysqldump -A backup-${BUILD_NUMBER}.sql sh kubectl set image deployment/${APP_NAME}\${APP_NAME}${DOCKER_IMAGE}\ -n${namespace}sh kubectl rollout status deployment/${APP_NAME}\ -n${namespace}\ --timeout600s }}post{success{// 发布成功发送通知dingTalk(robot:devops-robot,type:LINK,title:✅ ${APP_NAME} 发布成功,text:构建 #${BUILD_NUMBER} 已成功部署到生产环境,messageUrl:${BUILD_URL})}failure{// 发布失败自动回滚script{echo发布失败开始自动回滚...sh kubectl rollout undo deployment/${APP_NAME}\ -n${namespace}}dingTalk(robot:devops-robot,type:TEXT,text: ${APP_NAME} 发布失败已自动回滚)}}}}}3.2 Dockerfile配置# 多阶段构建优化镜像大小 FROM maven:3.8-openjdk-8 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-slim WORKDIR /app # 安全创建非root用户 RUN groupadd -r appgroup useradd -r -g appgroup appuser # 复制构建产物 COPY --frombuilder /build/target/*.jar app.jar # 安全降低容器运行权限 USER appuser # 健康检查 HEALTHCHECK --interval30s --timeout3s --start-period60s \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT [java, -XX:UseG1GC, -Xms512m, -Xmx1024m, -jar, app.jar]四、踩坑实录那些年我们踩过的DevOps大坑坑1生产环境配置覆盖测试配置有一次我们在Jenkinsfile里使用了变量${ENV}来指定环境但这个变量在测试环境有值在生产环境因为安全设置没有值。结果生产环境启动时用的是本地默认配置数据库连接池配置过小导致大量请求超时。解决所有环境变量必须在流水线中显式声明并在启动前打印脱敏后确认stage(Deploy){steps{script{echo部署环境${ENV}echo数据库地址${DB_HOST.substring(0,8)}***// 脱敏日志echoRedis地址${REDIS_HOST.substring(0,8)}***}}}坑2SonarQube误报导致无效回滚我们的SonarQube配置了一个质量门限Bug数量 0 就阻止合并。结果开发人员为了绕过这个检查在代码里写了大量的//NOSONAR注释SonarQube报告看起来很干净但实际上是自欺欺人。更夸张的是有一次SonarQube扫描出现误报它认为一段代码有内存泄漏实际上是一个对象池导致发布被阻止我们不得不回滚了已经测试通过的代码。解决调整SonarQube质量门限区分Blocker/Critical和Minor/Warning建立误报申诉流程误报需要在SonarQube中标记并说明原因定期Review//NOSONAR注释确保不是用来掩盖真实问题坑3Pipeline超时设置错误有一次我们的集成测试跑了2小时还没跑完Jenkins设置了30分钟超时超时后Jenkins杀掉了测试进程。但实际上测试本身没问题只是数据量太大。更坑的是我们配置的自动回滚在测试超时时也会触发结果导致已经部署好的代码被回滚了。解决根据测试类型设置不同的超时时间单元测试5分钟集成测试30分钟端到端测试2小时超时后的自动回滚逻辑需要排除测试阶段的失败坑4Docker镜像标签冲突有一次构建服务器磁盘满了构建失败了。运维人员清理磁盘后重新构建这次构建成功了。但问题是新的构建用的是和之前相同的latest标签而生产环境正在运行的是上一次构建的容器。两个容器的镜像ID不同但标签相同导致回滚时回滚到了错误版本。解决永远使用BUILD_NUMBERGIT_COMMIT_SHORT作为镜像标签禁止使用latest// 禁止docker build-t${APP_NAME}:latest.// 正确docker build-t${APP_NAME}:${BUILD_NUMBER}-${GIT_COMMIT_SHORT}.五、业务场景某电商团队从手动发布到全自动CI/CD的演进第一阶段纯手动发布耗时4小时/次2019年初这个电商团队50人左右完全靠手动发布开发人员本地打包上传到服务器运维人员SSH登录部署测试人员手动测试如果有问题手动回滚问题发布一次需要4-6小时一个月内发生了3次发布事故开发人员不敢发布积累了大量代码测试人员抱怨重复劳动第二阶段半自动化耗时1.5小时/次2019年中引入了Jenkins但只是自动构建和部署到测试环境生产环境仍然手动Jenkins自动构建 → 自动部署到测试环境测试人员在测试环境手动测试测试通过后运维人员手动部署到生产改善测试环境发布从4小时缩短到20分钟测试人员可以随时触发测试环境构建但生产发布仍然是瓶颈第三阶段全自动CI/CD耗时20分钟/次2019年底完成了完整的CI/CD流水线代码合并到develop→ 自动构建 单元测试 部署到测试环境代码合并到release/*→ 自动构建 集成测试 部署到预发环境release/*合并到master→ 自动构建 部署到生产环境蓝绿部署关键改进引入了蓝绿部署两套环境来回切换发布时间窗口从4小时缩短到20分钟引入了自动回滚任何一步失败自动回滚到上一个稳定版本引入了金丝雀发布先放5%流量观察无问题再全量成果发布频率从每月1次提升到每周2次发布事故从每月3次降到每季度不到1次开发人员对发布不再恐惧六、总结与思考DevOps流水线建设的关键要点规范先行在搭建流水线之前先规范Git分支策略、代码审查流程、发布流程小步快跑不要一开始就追求完美先实现基础的CI再逐步添加CD能力自动化一切重复的事情一定要自动化人工操作必然出错可观测性构建过程、部署过程必须有完整的日志和监控出问题能快速定位安全第一敏感信息不要写在代码里使用Vault或KMS管理密钥容错设计任何自动化步骤都要考虑失败情况要有回滚预案血的教训永远不要相信手动操作小心一点就没问题。人是会犯错的而自动化不会。给你的思考题你们团队的发布流程是怎样的有哪些可以自动化的环节如果现在系统出现故障你能在几分钟内回滚到上一个稳定版本个人观点仅供参考