一、缘起

CICD的思想目前对于每一个正规的软件开发团队基本都是必填项,那么一般来讲Jenkins的应用自然沦为了刚需。

Jenkins目前在单台Vm虚机上基于docker容器化部署,当Jenkins用了一段时间以后,发现每天的Jenkins 构建次数日益剧增,逐渐出现了Jenkins访问速度慢,卡顿,甚至直接终止服务响应的情况。由于底层是基于Vm,那么申请了一些物理资源,暂时解决了问题。

随后,随着几个项目组的构建需求频繁增长,每天Jenkins的构建次数会超过500次,此时显然原有部署结构已经不够支撑了。Jenkins服务各种卡死,无响应白屏频频发生。不过出现这个问题,也是意料之内,但是就是相对棘手了些。

由于现有服务器资源相对有限,直接开几台高配置Vm,可能会比较简单粗暴的解决问题,但成本相对较大,也不利于资源利用。因为Jenkins工作日时间也分忙时闲时。

二、解决思路
1. 痛点梳理
  • 构建任务高峰期,Jenkins服务频发不可用状态
  • 服务虚机资源有限,不能随意调用空闲资源
  • Jenkins 服务器宕机后需要人工手动重启
2. 思路分析
A. 传统Master-Slave模式

增加3台虚机,在现有Jenkins服务上创建多个Slave,绑定新增的这几台虚机上,通过Master-Slave的形式来尝试解决问题

  • 优势

    • 通过增加新的vm来解决问题,问题理论上可以得到解决
    • 时间的技术壁垒较低,时间较短
  • 劣势

    • 需要新申请虚机资源,对于资源调度分配上会有一定风险
    • 3台Slave在Jenkins构建闲时也会占用资源,导致资源浪费
    • 每个Slave的职能会根据业务需求随时调整,Slave的环境配置后期维护成本较高
    • 当Jenkins构建任务较多,Slave分配不均的时候,也会存在排队现象
B. 基于K8s动态Slave模式

基于K8s集群,Jenkins Master 接到构建任务后会动态在集群中的一个工作节点上拉起一个Jenkins Slave Pod来干活儿,活儿干完后可及时释放Pod。

  • 优势

    • 基于云原生现有K8s集群来解决问题,充分的利用现有资源,无需再申请新虚机
    • Slave可在构建任务来之时动态创建,工作结束后自动销毁,释放资源
    • 可通过K8s原生来管理Jenkins的调度策略,防止Slave调度分配不均匀
    • 通过云原生Chart来管理Jenkins配置,后期比较利于维护、扩展
    • Jenkins 小概率意外宕机场景,通过K8s的机制可以自愈
  • 劣势

    • 增加了系统复杂度

    • 有一定技术壁垒

    • 实现需要时间

通过团队共识,果断采用基于K8s动态Slave模式,以云原生的思路解决问题,这样不但可以解决Jenkins的服务资源紧缺现象,还能充分的利用K8s集群中的工作节点资源,云原生的形式也利于后期扩展和维护。计划先采用一个小型K8s集群(1m3n)来尝试解决问题。

三、Jenkins工作原理简述
1. 启动加载

Jenkins 启动时,会读取本地持久化目录数据,加载到内存。启动后,我们对Jenkins的上层用户操作,实际上都是对内存中数据状态的修改,Jenkins会异步刷盘,数据持久化。这样设计可应对大规模客户端操作请求,运行起来会更加节约I/O,性能更快。

2. 主从工作通信机制

基于Remoting通信框架(https://github.com/jenkinsci/remoting),通过TCP (JNLP protocols) 实现长连接序列化传输,master来向其对应注册的slave发送指令,slave异步处理后返回。

  • Master

在这里插入图片描述

  • Slave
    在这里插入图片描述

  • 参考资料

https://cloud.tencent.com/developer/article/1773184?from=article.detail.1773183

四、Dynamic Slave 工作原理
1. 概念定义

基于K8s集群,Jenkins Master 接到构建任务后会动态拉起一个Jenkins Slave Pod来干活儿,活儿干完后可及时释放Pod。

在这里插入图片描述

2. 设计优势
  • 动态伸缩

    合理的使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。

  • 服务高可用

    当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。

  • 扩展性好

    当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。

五、Docker in Docker
1. 设计思路

我们都知道,基于k8s拉起的服务,实际上都是以Pod形式存在的,而Pod是个容器组,最终服务实际是以Pod内的Container来支撑运行的。那么针对Slave的应用场景,Container应该如何设计?

为了不入侵宿主机Node上的docker服务,还要在Node上的Pod中使用docker服务,那么目前最佳的思路是要引入Docker in Docker技术来完成。

2. Docker in Docker

我们先来看下标准的dind使用方式,也就是Slave的内部设计

在这里插入图片描述

A. 容器职能

上图中,我们看到,Slave内部引入了3个容器,分别是JNLP,docker daemon和 docker build ,先来介绍下这3个容器的职能

  • JNLP container

    负责与Jenkins Master建立TCP通信,接收l来自Master发送过来的所有构建指令

  • Docker Daemon container

    docker的工作原理简单说是通过C/S模式进行的,而docker daemon就是server端。

    docker服务启动时,本地环境的client会和server端建立一个tcp连接,默认端口号是2375(如果使用TLS的话,则为2376),当我们运行一个docker指令时,这条指令会被发送到docker daemon,然后再返回相关信息。

  • Docker Build container

    这里才是我们真正用到的build server,我们构建项目用到的各种环境(java、go、php …)都可以把环境的基础信息放到这里面来,然后docker build 来构建。

B. Dind设计优势
  • Pod网络 - 可以直接使用pod网络,通过pod的 ip可以直接访问到内部容器暴露的服务端口。

  • 服务可用性 - 由于是docker daemon,那么容器内部会建立一个守护进程,经过实际测试,用原来比较丑陋的『 kill 1』的命令,是无法终止它的,那么就天然保证了docker服务的可用性。

  • Pod清理方式 - Pod的清理交给k8s管理,如果特定场景下需要自清理,那么emptyDir是个不错的选择。

  • 资源调度和使用情况 - Cpu和内存申请交给Pod, 可通过k8s机制来管理container的内存和cpu使用。

六、总结

经过一番折腾,上线后完全Cover住目前项目组的构建场景,构建速度明显加快,同时最大化资源利用。总体来讲,基于K8s + dind 来玩Slave体验还是不错的,后续有时间我会继续更新本帖,把实现过程中的一些具体操作呈现给大家。当然,如果您有问题,也请在评论区给我留言。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐