持续集成工具踩坑记录:那些年我用 Jenkins、GitLab CI 和 GitHub Actions 掉过的坑
引言:持续集成不是“拿来即用”,而是“修修补补”

作为一名工作了五年的开发工具工程师,我的日常工作就是围绕 CI/CD(持续集成与持续交付)展开的。从最开始的 Jenkins 单机部署到 GitLab CI 的容器化改造,再到如今 GitHub Actions 在公司多项目中的全面落地,我经历了 CI 工具的多次迭代和演进。
说真的,CI 工具本身不难用,但真正要在实际业务中落地,却处处是坑。有时候一个环境配置的差异就能让你调试一整天,更别说构建不稳定、权限问题、资源竞争这些让人头疼的事情。今天就借这篇文章,想和大家聊聊我在使用这些常见 CI 工具时踩过的那些坑,以及我是如何一步步把这些“地雷”拆除的。
一、第一次上手 Jenkins:初见惊艳,再见崩溃

项目背景
那是我入职不久后接手的一个中型 Java 项目,代码量在几十万行左右。当时的 CI 环境几乎为零,我们团队决定引入 Jenkins 来实现自动化构建和部署。
听起来挺简单对吧?结果……你懂的。
遇到的问题
插件冲突导致无法升级 一开始我们选用了 Jenkins 社区推荐的一系列插件,比如 Pipeline、Email Extension、HTML Report 等。后来随着业务复杂度上升,我们需要引入 Kubernetes 插件来实现动态 agent 扩展,这时候升级 Jenkins 就出问题了——某些旧插件已经不再维护,导致新版本的 Jenkins 启动失败。
Pipeline 脚本编写不规范 因为刚接触,脚本写得非常随意,甚至直接在 Jenkins Web 页面上写 pipeline,结果出现分支判断错误、环境变量误用等问题。有一次上线前自动构建失败,居然没人发现,最后只能回滚处理。
节点挂掉之后没有自动恢复 我们用的是传统的 Jenkins Master/Slave 架构,其中一个 Slave 节点运行在一台虚拟机上。结果那天网络波动,节点失联,Jenkins 没有自动切换,任务一直卡着没人管,直到第二天早上才发现。
权限管理混乱 初期我们为了方便所有人提交、触发构建和发布流程,开了很多权限。结果后来有人误删了一个 job 的配置,还搞坏了一个测试环境的部署流水线。
解决方案
这些问题并不是一次性解决的,都是通过一个个“血泪教训”逐步优化过来的:
标准化 Jenkinsfile 并纳入 Git 管理 把所有的构建逻辑统一写入
Jenkinsfile中,并放在对应项目的 repo 里进行版本控制。这样不仅提高了可维护性,也避免了页面修改带来的不一致。建立统一的共享库(Shared Library) 提取公共函数封装成 Groovy 模块,比如通用的
send_notification()、deploy_to_staging()等,让多个项目复用同一套逻辑,减少重复劳动。采用 Docker 容器化部署 Jenkins 使用 Docker Compose 部署 Jenkins 主节点,同时用 Kubernetes 插件管理动态 Agent,大大提升了扩展性和稳定性。
定期清理插件,只保留必要的功能 渐渐明白了一个道理:“插件不在多,在稳。”我们清掉了不常用或存在兼容性问题的插件,只保留关键插件,减少了后期维护成本。
权限分层控制 + 变更审批流程 设置基于角色的权限控制(Role-based Strategy),普通开发者只能提交代码和查看日志,而构建和发布的操作必须由管理员触发或经过审批流程。
最终效果
虽然 Jenkins 学习曲线陡峭,但它确实很适合早期 CI 能力还不成熟的团队入门。在我们改进了一段时间后,自动化构建的成功率从最初的 70% 提高到了 98% 以上,平均构建时间也缩短了将近一半。
一个小感悟:Jenkins 像是一个老派的手工匠人,功能全,自由度高,但你得自己搭台子、调工具、修毛病。想要用得好,必须花时间去磨合。
二、迁移到 GitLab CI:爽快一时,维护不易
项目背景
随着公司业务的发展,我们的部分项目逐渐从私有 GitLab 迁移过去。于是顺其自然地开始试用 GitLab CI。
相比 Jenkins,GitLab CI 最大的优势在于“无缝集成”,不需要额外搭建复杂的服务器结构,而且 YML 配置方式看起来也很简洁。初期体验非常好,尤其是 GitLab Pages 的静态网站托管能力,简直是前端同学的福音。
遇到的问题
YAML 表达式太灵活,容易出错 GitLab CI 的
.gitlab-ci.yml文件支持模板、变量嵌套、Job 继承等特性,看起来很强大,但实际上很容易写错。有一次因为某个extends写错了层级,整个流水线跑完后才发现部署目标被覆盖到了生产环境……并行执行资源争抢严重 我们的一个项目需要并发执行 20 多个测试 Job,但由于 GitLab Runner 默认使用的是共享 executor(通常是 Shell Executor),所有 Job 实际上还是串行跑的。这导致大量任务堆积,构建排队时间越来越长。
Runner 管理复杂 初期为了方便,我们在每台机器上都注册了一个 Shared Runner。后来人员流动,很多旧的 Runner 没及时注销,导致系统识别混乱,有时候任务跑到不相干的机器上去了。
缓存策略不合理导致构建慢 GitLab CI 支持缓存依赖目录(如 Maven 的 .m2、Node.js 的 node_modules),但我们一开始没有合理配置,缓存路径匹配不上,反而每次都在重新下载依赖,效率很低。
没有内置的通知机制 GitLab 本身没有像 Jenkins 那样灵活的通知插件,只能靠
after_script自己发消息,或者写一堆条件判断来决定是否发送通知。
解决方案
规范化 CI 配置模版 我们建立了公司级的
.gitlab-ci.tpl.yaml模板文件,里面定义了统一的 stages、cache、retry 等配置。各个项目只需继承模板,再填写自定义参数即可,确保格式统一、逻辑清晰。使用 Kubernetes Executor 动态扩容 改用 GitLab Runner 的 Kubernetes Executor,并结合我们自己的 Kubernetes 集群,每个 Job 启动独立的 Pod,互不影响。这样既能保证资源隔离,又能充分利用集群算力。
设置标签 + 限制访问范围 每个项目组使用专属 Runner 标签,比如
java,nodejs,android,并通过 Tag 分流任务。同时也设置了只有特定用户才能注册 Runner,防止滥用。精细化缓存策略 根据不同语言生态设计不同的 cache key:
cache: key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" paths: - .m2/repository/ - node_modules/有效提升缓存命中率,加快了构建速度。
封装通知组件 在 CI 流程中加入一个名为
notify.sh的统一脚本,接收当前状态、任务名、链接等参数,通过企业微信或钉钉机器人推送消息。这样无需每个项目单独配置,提高可维护性。
效果总结
迁移到 GitLab CI 后,我们的 CI 部署变得轻量又高效。尤其是在小型项目和前后端融合团队中,这种“开箱即用”的特性特别受欢迎。配合 Kubernetes,我们的构建并发能力提升了 3~5 倍,缓存优化后,平均构建时间也减少了 40%。
不过,也暴露出一些问题,比如大项目配置复杂时,YAML 层叠嵌套会导致可读性下降,还有权限体系不如 Jenkins 灵活。
三、转向 GitHub Actions:现代化虽好,陷阱也不少
项目背景
去年我们几个核心项目迁移到 GitHub 上,自然而然也开始尝试使用 GitHub Actions。说实话,GitHub Actions 的体验真是“丝滑”了不少,它的市场生态、UI 设计、文档体验都非常友好。
但别高兴得太早……
遇到的问题
默认超时时间过短 GitHub Actions 默认每个 Job 的运行时长不超过 6 小时。我们有一个大数据训练任务,原本在本地跑了 8 小时没问题,但在 Actions 上就中断了。后来才知道要手动指定
timeout-minutes参数。Actions Runner 资源受限 使用 GitHub 托管的 Linux VM(ubuntu-latest)时,内存不足,尤其在 Node.js 多工程打包时频繁 OOM(Out of Memory)。临时解决方案是拆分构建步骤,但终究治标不治本。
缓存失效频繁 GitHub 提供了
actions/cache用于依赖缓存,但它的缓存键更新策略比较严格。有一次我们不小心改了缓存路径,导致缓存全部失效,Build 时间暴涨。敏感信息泄露风险 我们曾误把一个密钥作为环境变量写在 workflow 中,忘了使用
${{ secrets.SECRET_KEY }}语法,结果 GitHub 自动检测出安全问题,给我们发了警告邮件 🫢。自建 Runner 管理复杂 当我们想接入私有 Kubernetes 环境时,部署 GitHub Runner 成了一项不小的挑战。官方文档虽然详细,但实际部署过程中仍有不少细节需要注意,比如权限绑定、镜像拉取、token 定期更新等。
解决方案
明确设置超时时间 对于耗时较长的任务,加上以下配置:
jobs: train-model: runs-on: ubuntu-latest timeout-minutes: 100升级运行环境 / 自建 Runner 对于资源密集型任务,要么选择更高配的 runner(如 Mac 或自建机器),要么将任务分阶段执行,必要时借助缓存中间产物。
优化缓存键策略 使用稳定的缓存键结构,例如:
steps: - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}保证内容变更时才更换缓存 Key,避免无意义的缓存重建。
加强密钥管理和代码审查 所有密钥必须走 GitHub Secrets 系统管理,且 PR 合并前强制进行 Action 配置审查。我们还引入了第三方工具,用于扫描 workflows 是否有潜在的安全隐患。
部署自建 Runner + 监控告警 使用 Helm Chart 快速部署自建 Runner 到 K8s 集群中,并添加健康检查 + 日志采集。一旦 Runner 出现异常,自动触发 Slack 通知提醒运维介入。
效果总结
GitHub Actions 最大的优势是“生态强、上手快、体验好”。对于中小型开源项目、DevOps 团队快速迭代的场景,它是首选。但在面对大规模项目、资源密集型任务时,它也暴露出一些局限性,比如性能瓶颈和成本控制的问题。
我们目前的做法是在公开项目中广泛使用 GitHub Actions,而在内部私有项目中则继续使用 GitLab CI 或 Jenkins,根据实际情况进行灵活选择。
总结一下:不同 CI 工具有什么特点?
| 工具 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Jenkins | 插件丰富、高度定制化 | 界面老、学习成本高 | 老系统迁移、大型企业 |
| GitLab CI | 集成好、结构清晰 | 管理分散、权限较弱 | 私有化部署项目 |
| GitHub Actions | 生态强大、易用性强 | 免费资源有限、自定义成本高 | 开源项目、协作性强的团队 |
经验分享:关于 CI 工具,我想给新手的几点建议
不要盲目追求“最新最潮” 有些同学一上来就要用 Argo Workflows 或 Tekton,觉得才是高端大气。其实不然,先理解清楚你们的 CI 需求和现状,再决定技术栈。
尽早做 CI 配置的标准化 不管是 Jenkinsfile、.gitlab-ci.yml 还是 GitHub Workflow,都应该统一格式、命名规范、模块化设计,否则后面维护起来会哭死。
重视缓存和性能优化 CI 构建慢,往往是因为重复下载依赖。善用缓存、并行构建、增量分析这些手段,可以节省大量时间。
不要忽视监控和报警机制 CI 环境出了问题没人知道?那你就是在“裸奔”。建议搭配 Prometheus + Grafana 或者 ELK,实时观测构建成功率、耗时趋势、资源占用等指标。
学会“抽象通用逻辑” 把常见的构建步骤(编译、测试、打包、部署)抽象成函数或 Action 模块,提高复用率,也能降低新人学习门槛。
安全永远不能忽视 密钥泄漏、恶意脚本注入、未授权触发构建等风险都要提前防范,别等“爆雷”了才后悔没做安全加固。
结尾语:CI 是 DevOps 的起点,而不是终点
一路走来,我深知 CI 并不是一个“装好了就万事大吉”的东西,而是一个需要持续优化、反复打磨的过程。它就像我们写的代码一样,也需要不断重构、测试和迭代。
我希望这篇“踩坑记录”能帮助你在选择和使用 CI 工具的过程中少走弯路。如果文中有任何疑问或补充,欢迎留言交流。如果你也在使用这些工具,不妨分享下你的经验和故事,我们一起成长,一起进步 😊。
文章首发于个人博客,转载请联系作者。

评论 0