最近在搭建DevOps流水线,踩了不少坑,也熬了不少夜,今天就来分享一下,那些让我熬夜的问题,以及,我是怎么解决的。

DevOps流水线,也就是CI/CD流水线,是现在互联网公司,必不可少的基础设施,能实现自动化构建,自动化测试,自动化部署,大大提升发布效率和质量。但是,搭建DevOps流水线,并不简单,会遇到各种各样的问题,每一个问题,都可能让你熬夜,让你崩溃。

今天这篇文章,就来分享一下,我在搭建DevOps流水线的过程中,遇到的那些坑,以及,我的解决方案和经验教训。

一、先说说背景

先简单说说背景。

我们团队,之前的发布方式,是手动发布,每次发布,都要打包,上传,部署,重启,验证,很麻烦,也很容易出错,而且,发布效率很低,一周才发布一次,很多功能,开发完了,要等很久,才能上线。

后来,团队决定,搭建DevOps流水线,实现自动化构建,自动化测试,自动化部署,提升发布效率和质量。这个任务,就交给了我。

我之前,也接触过一些CI/CD工具,比如Jenkins,GitLab CI,Travis CI等,但是,都是简单用用,没有从零开始,搭建过一套完整的流水线。所以,这次搭建,对我来说,也是一个挑战,踩了不少坑,也熬了不少夜。

我们最终,选择的是GitLab CI + Docker + Kubernetes的方案,代码托管在GitLab,用GitLab CI做流水线,用Docker做容器化,用Kubernetes做容器编排,部署到K8s集群里。这套方案,现在很流行,也很成熟,但是,搭建起来,还是有很多坑。

下面,就来分享一下,我遇到的那些坑,以及,我的解决方案。

二、坑一:环境不一致,本地能跑,流水线跑不了

第一个坑,就是环境不一致,本地能跑,流水线跑不了,这也是最常见的坑。

我们的项目,是Java项目,用Maven构建,本地开发的时候,用的是JDK 8,Maven 3.6,在本地,mvn package,能正常构建,没有问题。但是,在GitLab CI的流水线里,构建就失败了,报错,说找不到符号,或者,版本不兼容。

我查了很久,才发现,是因为,GitLab CI的Runner,用的是JDK 11,Maven 3.5,和本地的环境不一致,JDK 11,对一些旧的依赖,不兼容,所以,构建失败了。

这个问题,看起来简单,但是,我查了很久,因为,报错信息,很不明确,只是说,找不到符号,我以为,是代码的问题,或者,依赖的问题,查了很久,才发现,是环境的问题。

解决方案:

解决方案,就是,保证环境一致,流水线里的环境,和本地的环境,保持一致。具体来说,有几种方法:

  1. 用Docker镜像,指定具体的版本。在.gitlab-ci.yml里,指定构建用的Docker镜像,比如,用maven:3.6.0-jdk-8,这样,就能保证,JDK和Maven的版本,和本地一致,不会出现环境不一致的问题。这是最简单,也是最推荐的方法。
  1. 用Dockerfile,自己构建镜像。如果,官方的镜像,不能满足需求,可以自己写Dockerfile,构建自己的镜像,指定需要的环境,然后,推送到镜像仓库,流水线里,用自己的镜像。这样,更灵活,能定制自己需要的环境。
  1. 用版本管理工具,统一环境版本。在项目里,用版本管理工具,比如,jenv,管理JDK的版本,用mvnw,管理Maven的版本,这样,不管在什么环境,都能用统一的版本,避免环境不一致的问题。

后来,我们用的是第一种方法,在.gitlab-ci.yml里,指定maven:3.6.0-jdk-8镜像,问题就解决了,构建能正常通过了。而且,用Docker镜像,还能保证,每个构建的环境,都是干净的,一致的,不会因为,之前的构建,留下一些缓存,或者,一些文件,影响下一次的构建。

经验教训:

环境不一致,是最常见的坑,也是最容易忽略的坑。搭建流水线的时候,一定要保证,流水线的环境,和本地的环境,和生产环境,保持一致,这样,才能避免,"本地能跑,流水线跑不了","测试环境能跑,生产环境跑不了"的问题。

而且,用Docker容器化,是解决环境不一致问题的最好方法,把应用和依赖,都打包到Docker镜像里,这样,不管在什么环境,都能用同样的镜像,保证环境一致。

三、坑二:依赖下载慢,构建超时

第二个坑,就是依赖下载慢,构建超时。

我们的项目,用Maven管理依赖,很多依赖,都是从Maven中央仓库下载的,但是,Maven中央仓库,在国外,下载速度很慢,有时候,一个依赖,要下载好几分钟,甚至,下载失败,导致构建超时,失败。

刚开始,我们的流水线,构建一次,要十几分钟,甚至,二十几分钟,大部分时间,都在下载依赖,而且,经常因为,下载超时,构建失败,要重新构建,很浪费时间,也很影响效率。

这个问题,困扰了我很久,也熬了不少夜,试了很多方法,才解决。

解决方案:

解决方案,就是,配置国内的镜像源,加速依赖下载,并且,缓存依赖,避免重复下载。具体来说,有几种方法:

  1. 配置国内的Maven镜像源。在Maven的settings.xml里,配置国内的镜像源,比如,阿里云的Maven镜像,华为云的Maven镜像,腾讯云的Maven镜像等,这些国内的镜像,下载速度很快,能大大提升依赖下载的速度。
  1. 配置私有Maven仓库。如果公司有条件,可以搭建一个私有的Maven仓库,比如,Nexus,Artifactory等,把常用的依赖,缓存到私有仓库里,构建的时候,从私有仓库下载,速度更快,也更稳定。而且,私有仓库,还能存放公司自己的私有依赖,很方便。
  1. 缓存Maven的本地仓库。在GitLab CI里,可以缓存Maven的本地仓库,也就是,.m2目录,这样,第一次构建的时候,下载依赖,之后的构建,就不用重新下载了,直接用缓存的依赖,大大提升构建速度。GitLab CI的缓存,很容易配置,在.gitlab-ci.yml里,配置cache字段,指定要缓存的目录,就行。
  1. 把依赖打包到Docker镜像里。如果,依赖变化不大,可以把依赖,打包到构建用的Docker镜像里,这样,构建的时候,就不用下载依赖了,直接用镜像里的依赖,速度更快。但是,这种方法,不够灵活,如果依赖变化了,就要重新构建镜像,适合依赖比较稳定的项目。

后来,我们用的是,第一种和第三种方法,配置了阿里云的Maven镜像,并且,缓存了Maven的本地仓库,构建速度,大大提升,从原来的,十几分钟,二十几分钟,降到了,两三分钟,而且,很少因为,下载超时,构建失败了。

后来,我们又搭建了私有的Nexus仓库,把常用的依赖,都缓存到私有仓库里,速度更快,也更稳定,而且,还能存放公司自己的私有依赖,很方便。

经验教训:

依赖下载慢,是很多项目,都会遇到的问题,尤其是,项目依赖很多,或者,网络不好的时候,很容易导致构建超时,失败。搭建流水线的时候,一定要配置,国内的镜像源,或者,私有仓库,并且,缓存依赖,这样,才能大大提升构建速度,保证构建的稳定性。

而且,缓存,是提升流水线速度的重要手段,不仅要缓存依赖,还要缓存,构建的中间产物,比如,编译后的class文件,打包后的镜像层等,这样,能大大提升构建速度,减少构建时间。

四、坑三:Docker镜像构建慢,推送失败

第三个坑,就是Docker镜像构建慢,推送失败。

我们的项目,构建成功之后,要打包成Docker镜像,然后,推送到镜像仓库,再部署到K8s集群里。但是,Docker镜像构建,很慢,而且,经常推送失败,导致流水线失败。

刚开始,我们的Dockerfile,写得很简单,就是,从基础镜像开始,复制代码,构建,打包,但是,这样,每次构建,都要重新下载依赖,重新编译,重新打包,很慢,而且,Docker的构建缓存,没有用好,很多层,都没有缓存,每次都要重新构建。

而且,推送镜像的时候,因为,镜像仓库,在公网,或者,网络不好,经常推送失败,或者,推送很慢,导致流水线超时,失败。

解决方案:

解决方案,就是,优化Dockerfile,用好构建缓存,并且,配置镜像加速器,提升推送速度。具体来说,有几种方法:

  1. 优化Dockerfile,用好构建缓存。Docker的构建,是按层来的,每一条指令,就是一层,如果,这一层的内容,没有变化,就会用缓存,不用重新构建。所以,要把变化少的指令,放在前面,变化多的指令,放在后面,这样,能最大化地利用缓存。比如,先复制pom.xml,下载依赖,再复制代码,编译打包,这样,只要依赖不变,下载依赖这一层,就会用缓存,不用重新下载。
  1. 用多阶段构建,减小镜像体积。用Docker的多阶段构建,第一阶段,构建项目,打包,第二阶段,只复制打包后的产物,运行,这样,最终的镜像,里没有,构建工具,没有源码,没有依赖,体积很小,推送和拉取,都很快。而且,镜像小了,部署的时候,拉取镜像也快,启动也快。
  1. 配置Docker镜像加速器。如果,用的是Docker Hub,或者,其他国外的镜像仓库,拉取和推送,都很慢,可以配置,国内的镜像加速器,比如,阿里云的镜像加速器,腾讯云的镜像加速器等,能大大提升,拉取和推送的速度。
  1. 用私有镜像仓库。如果公司有条件,可以搭建一个私有的镜像仓库,比如,Harbor,Registry等,把镜像,推送到私有仓库,速度更快,也更稳定,而且,还能做镜像的权限管理,安全扫描等,很方便。
  1. 用Docker BuildKit,提升构建速度。Docker 18.09以上,支持BuildKit,是一个新的构建引擎,比传统的构建引擎,速度更快,效率更高,支持并行构建,缓存挂载等,能大大提升构建速度。可以在构建的时候,设置DOCKER_BUILDKIT=1,开启BuildKit。

后来,我们优化了Dockerfile,用了多阶段构建,并且,开启了BuildKit,镜像构建的速度,大大提升,从原来的,十几分钟,降到了,两三分钟,而且,镜像的体积,也小了很多,从原来的,几百MB,降到了,几十MB,推送和拉取,都快了很多。

而且,我们搭建了私有的Harbor镜像仓库,镜像都推送到私有仓库,速度更快,也更稳定,很少推送失败了。

经验教训:

Docker镜像构建,是流水线里,很重要的一环,也是很容易出问题的一环。一定要优化Dockerfile,用好构建缓存,用多阶段构建,减小镜像体积,并且,用私有镜像仓库,或者,镜像加速器,提升推送和拉取的速度,这样,才能保证,镜像构建的速度和稳定性。

而且,镜像的安全,也很重要,要定期扫描镜像的漏洞,及时更新基础镜像,避免,镜像里有安全漏洞,被黑客利用。

五、坑四:K8s部署失败,服务起不来

第四个坑,就是K8s部署失败,服务起不来。

镜像构建好,推送到镜像仓库之后,就要部署到K8s集群里了。但是,部署的时候,经常遇到,部署失败,服务起不来的问题,比如,Pod一直Pending,或者,CrashLoopBackOff,或者,服务启动了,但是,访问不了,等等。

这个问题,是最让人头疼的,因为,K8s的概念很多,配置也很多,出了问题,很难排查,经常要查很久,才能找到原因,很容易熬夜。

我遇到过,很多次,部署失败,服务起不来的问题,比如,镜像拉取失败,资源不足,端口配置错误,健康检查失败,配置文件错误,权限不足,等等,每一个问题,都让我头疼了很久。

解决方案:

解决方案,就是,学会排查K8s的问题,并且,做好配置管理,避免常见的错误。具体来说,有几种方法:

  1. 学会用kubectl命令,排查问题。K8s提供了很多命令,用来排查问题,比如,kubectl get pods,查看Pod的状态;kubectl describe pod,查看Pod的详细信息,包括事件,能看到,为什么Pod起不来;kubectl logs,查看Pod的日志,能看到,应用的报错信息;kubectl exec,进入Pod,查看内部的状态。这些命令,是排查K8s问题的基础,一定要熟练掌握。
  1. 做好配置管理,用Helm管理K8s配置。K8s的配置文件,很多,很复杂,很容易写错,可以用Helm,来管理K8s的配置,把配置,打包成Chart,模板化,参数化,这样,能减少配置错误,也方便管理和升级。而且,Helm有很多现成的Chart,可以直接用,不用自己写,很方便。
  1. 做好健康检查,合理配置liveness和readiness探针。K8s的健康检查,很重要,能保证,服务正常的时候,才把流量,转发过来,服务不正常的时候,重启服务。但是,健康检查的配置,不合理,也会导致,服务起不来,比如,初始延迟时间太短,服务还没启动好,就被判定为不健康,被重启了。所以,要合理配置,liveness和readiness探针的,初始延迟,超时时间,检查间隔等,保证,服务能正常启动,正常运行。
  1. 做好资源管理,合理配置requests和limits。K8s的资源管理,也很重要,要合理配置,CPU和内存的requests和limits,保证,服务有足够的资源,能正常运行,也不会,占用太多资源,影响其他服务。如果,资源配置不足,Pod会因为,资源不足,一直Pending,或者,被OOMKilled,起不来。如果,资源配置过多,会浪费资源,导致,集群的资源不够用。
  1. 做好权限管理,配置合适的ServiceAccount和RBAC。如果,服务需要,访问K8s的API,或者,访问其他的资源,需要配置,合适的ServiceAccount和RBAC权限,不然,会因为,权限不足,访问失败,服务起不来,或者,运行异常。
  1. 做好配置和密钥管理,用ConfigMap和Secret。应用的配置文件,和密钥,不要硬编码在镜像里,要用ConfigMap和Secret,来管理,这样,配置和密钥,和镜像分离,修改配置,不用重新构建镜像,更灵活,也更安全。但是,要注意,ConfigMap和Secret的挂载,配置正确,不然,服务会因为,找不到配置文件,起不来。

后来,我们用了Helm,管理K8s的配置,模板化,参数化,大大减少了配置错误,而且,我们整理了,K8s常见问题的排查手册,遇到问题,按照手册,一步步排查,很快就能找到原因,解决问题,很少因为,部署失败,熬夜了。

经验教训:

K8s部署,是流水线里,最容易出问题的一环,因为,K8s的概念多,配置复杂,出了问题,很难排查。所以,一定要,学会K8s的基本命令,学会排查问题,并且,做好配置管理,用Helm等工具,模板化配置,减少错误,还要,做好健康检查,资源管理,权限管理,配置管理等,保证,服务能正常部署,正常运行。

而且,最好,有一套,预发布环境,或者,测试环境,先在测试环境,部署验证,没问题了,再部署到生产环境,这样,能避免,把有问题的版本,部署到生产环境,影响线上业务。

六、坑五:流水线权限问题,安全隐患

第五个坑,就是流水线的权限问题,有安全隐患。

刚开始搭建流水线的时候,为了方便,我给了GitLab Runner,很大的权限,比如,能访问所有的项目,能推送所有的镜像,能部署到所有的K8s命名空间,甚至,给了集群的管理员权限。这样,用起来,确实很方便,不会因为,权限不足,流水线失败。

但是,后来,我们做安全审计的时候,发现,这样,有很大的安全隐患,如果,GitLab Runner被攻击了,或者,有恶意的用户,通过流水线,执行恶意的命令,就能,访问所有的项目,推送恶意的镜像,甚至,控制整个K8s集群,后果不堪设想。

这个问题,让我吓出了一身冷汗,赶紧,整改了流水线的权限,按照最小权限原则,给GitLab Runner,只给必要的权限,避免,权限过大,带来安全隐患。

解决方案:

解决方案,就是,按照最小权限原则,配置流水线的权限,并且,做好安全隔离和审计。具体来说,有几种方法:

  1. 按照最小权限原则,配置GitLab Runner的权限。给GitLab Runner,只给必要的权限,比如,只能访问,指定的项目,只能推送,指定的镜像仓库,只能部署到,指定的K8s命名空间,不要给,过大的权限,更不要给,集群的管理员权限。
  1. 用独立的Runner,做隔离。不同的项目,或者,不同的环境,用不同的GitLab Runner,做隔离,这样,就算,一个Runner被攻击了,也不会影响,其他的项目和环境。而且,可以给,不同的Runner,不同的权限,更灵活,更安全。
  1. 用受保护的分支和标签,限制部署权限。在GitLab里,配置受保护的分支和标签,只有,受保护的分支和标签,才能触发,部署到生产环境的流水线,并且,只有,有相应权限的用户,才能合并到受保护的分支,或者,打标签,这样,能避免,普通用户,随便部署到生产环境,保证生产环境的安全。
  1. 做好流水线的审计和日志。记录,每一次流水线的执行,包括,谁触发的,什么时候执行的,执行了什么操作,结果是什么,等等,做好审计和日志,这样,出了问题,能追溯,能找到原因,也能,发现异常的操作,及时处理。
  1. 做好镜像的安全扫描。在流水线里,加入,镜像安全扫描的步骤,构建好镜像之后,扫描镜像的漏洞,和恶意代码,如果,发现有高危漏洞,或者,恶意代码,就阻止流水线,不让,有问题的镜像,部署到生产环境,保证,镜像的安全。
  1. 做好密钥的管理,不要硬编码。流水线里,用到的密钥,比如,镜像仓库的密码,K8s的kubeconfig,数据库的密码等,不要硬编码在,.gitlab-ci.yml里,要用GitLab的CI/CD变量,或者,专门的密钥管理工具,比如,Vault,来管理,并且,配置变量的保护,和掩码,避免,密钥泄露。

后来,我们按照最小权限原则,重新配置了流水线的权限,给不同的项目,不同的环境,用不同的Runner,并且,配置了受保护的分支和标签,做好了审计和日志,加入了镜像安全扫描,密钥也都用CI/CD变量管理,大大提升了流水线的安全性,消除了安全隐患。

经验教训:

流水线的权限和安全,是很容易被忽略的,但是,也是很重要的,因为,流水线,有代码的访问权限,有镜像的推送权限,有部署的权限,如果,权限过大,或者,被攻击了,后果不堪设想。所以,搭建流水线的时候,一定要,按照最小权限原则,配置权限,做好安全隔离和审计,做好镜像安全扫描,做好密钥管理,保证流水线的安全。

而且,安全,不是一次性的,要定期,做安全审计,检查流水线的权限,配置,有没有安全隐患,及时整改,保证,流水线的安全。

七、坑六:流水线性能问题,排队等待时间长

第六个坑,就是流水线的性能问题,排队等待时间长。

刚开始,我们只有一个GitLab Runner,所有的项目,所有的流水线,都在这一个Runner上运行,后来,项目越来越多,流水线越来越多,一个Runner,忙不过来,经常,有很多流水线,在排队等待,要等很久,才能开始执行,严重影响了开发效率。

而且,有时候,有大的构建任务,占用了Runner,其他的小任务,也要排队等,很久才能执行,很不合理,也很影响效率。

这个问题,也困扰了我们很久,后来,我们做了一些优化,才解决。

解决方案:

解决方案,就是,横向扩展Runner,并且,做好任务调度和资源分配。具体来说,有几种方法:

  1. 横向扩展Runner,增加Runner的数量。最简单的方法,就是,增加Runner的数量,部署多个Runner,这样,能同时执行,多个流水线,减少排队等待时间。而且,可以把Runner,部署在不同的机器上,或者,不同的集群里,避免,单点故障,也能,提升整体的处理能力。
  1. 用标签,给Runner分类,做任务调度。给不同的Runner,打不同的标签,比如,build,test,deploy,docker,k8s等,然后,在流水线里,指定,任务用哪个标签的Runner,这样,能把不同的任务,调度到不同的Runner上,避免,大的构建任务,占用了Runner,影响其他的小任务。而且,还能给,不同的项目,不同的环境,用不同的Runner,做隔离,更安全,也更稳定。
  1. 用Docker Executor,或者,Kubernetes Executor,动态创建Runner。如果,用的是Docker Executor,或者,Kubernetes Executor,GitLab Runner,能动态创建,Docker容器,或者,K8s Pod,来执行任务,执行完了,就销毁,这样,能更灵活地,利用资源,也能,支持更多的并发任务,不用,预先部署很多Runner。而且,Kubernetes Executor,还能,根据任务的资源需求,动态分配资源,更高效,也更灵活。
  1. 优化流水线,减少执行时间。除了扩展Runner,还要,优化流水线本身,减少每个流水线的执行时间,这样,Runner的周转更快,能处理更多的任务。比如,用好缓存,减少构建时间;并行执行,独立的任务,减少整体的执行时间;只在必要的时候,执行,必要的步骤,比如,只有,代码变化了,才构建,只有,部署到生产环境的时候,才执行,安全扫描等。
  1. 配置流水线的优先级,和超时时间。给不同的流水线,配置不同的优先级,比如,生产环境的部署,优先级高,优先执行;测试环境的构建,优先级低,排队等。并且,配置合理的超时时间,避免,有问题的流水线,一直占用Runner,影响其他的流水线。

后来,我们部署了多个Runner,用标签分类,并且,用了Kubernetes Executor,动态创建Pod执行任务,还优化了流水线,减少了执行时间,配置了优先级和超时时间,排队等待的问题,大大缓解,开发效率,也大大提升。

经验教训:

流水线的性能,也是很重要的,如果,流水线经常排队,等待时间长,会严重影响开发效率。所以,搭建流水线的时候,一定要,做好性能规划,根据项目的数量,和流水线的频率,合理配置Runner的数量,和资源,并且,做好任务调度,和资源分配,用好缓存和并行,优化流水线的执行时间,保证,流水线的性能,满足开发的需求。

而且,要监控,流水线的性能,比如,排队时间,执行时间,成功率,失败率等,及时发现,性能瓶颈,及时优化,保证,流水线的稳定和高效。

八、写在最后

搭建DevOps流水线,确实,踩了不少坑,也熬了不少夜,但是,搭建好之后,带来的好处,也是很明显的,发布效率,大大提升,从原来的,一周发布一次,变成了,一天可以发布多次,而且,发布质量,也大大提升,很少因为,人为操作失误,导致发布故障,团队的开发效率,和产品的迭代速度,都大大提升。

而且,在踩坑的过程中,我也学到了很多,对DevOps,对CI/CD,对Docker,对K8s,都有了更深入的理解,也积累了很多实战经验,这些,都是很宝贵的财富。

今天,把这些踩坑的经历,分享出来,希望能给,正在搭建DevOps流水线的朋友,一些参考和帮助,让你们,少踩坑,少熬夜,少走弯路。

当然,我遇到的坑,只是,冰山一角,DevOps流水线,还有很多坑,需要我们,在实际的工作中,去发现,去解决,去总结。也欢迎大家,在评论区,分享你们遇到的坑,和解决方案,一起交流,一起进步。

最后,希望大家,都能搭建出,稳定,高效,安全的DevOps流水线,提升团队的开发效率,和产品的迭代速度,做出更好的产品。

愿我们,都能在踩坑中成长,在解决问题中进步,成为更好的工程师。