背景
微服务的最基本基础,就是 CICD,CI 的交付产物是制品。制品有很多属性,比如环境、版本等。从这个抽象上来看,如果微服务是基于 K8s + docker 的话,那么,docker image 是制品的一种具象化的表现。同样,如果微服务并不基于 docker 和 K8s,那么,制品也可以是其他具体表现,比如 tar 包。
如果是基于 K8s + docker 的话,CI 最终交付的制品,应该是 docker 镜像。所以,如何构建镜像,就有的选了,比如:Jenkins、Gitlab CI、Drone、或者基于 K8s 的 Pod (init containers)的 Knative 等。个人认为,Gitlab CI 与 Gitlab 耦合太强,无法作为一个通用的构建中心独立出来。所以,Jenkins、Drone、Knative 等这几个系统,都可以按照团队喜好自建。但不论哪种方式,一个底线始终不变:必须能从原理层了解选型系统,并且,出了问题,最好可以从源码层定位和捕捉问题。
综合来看,Drone 既有 Pipeline 模型,又有插件化设计、基于 Golang,非常轻量,所以最终将其作为构建系统的核心底层设施。
Drone 是开源的,基于 Golang + Docker 的,也是目前 CNCF 云原生体系下的一个项目。
Drone 这套系统在我们公司,通过服务化交付运行近 3 年,非常稳定。由于使用 Drone 较早,目前使用的是 0.8.10 版本,后来准备使用 1.x 版本时,由于官方代码质量还严重不过关,有一堆问题,因此最终将版本,固定在了 1.x 之前的最后一个版本,也就是 0.8.10 。
然而,最近在排查某个问题的时候,发现 Drone 的 worker 节点上的组件进程 drone-agent 存在 socket 泄露问题。下面就是针对这个问题的一个排查过程。
提前剧透:最后的修复方式,是通过修改源码解决的,但是因为版本较早,截止目前,Drone 已经到了 v1.9.0 版本,之前版本,官方也不再管理和维护了。因此最终是通过直接修改 vendor 包的第三方库来解决。
另外,也顺便吐槽一下,Drone 官方维护者对 Drone 的权限收敛过大,对社区里的外部成员接纳程度不太高,在开源这个事情上的氛围差一些(这个问题也有人在社区论坛吐槽过)。
现存问题
问题
在查看 drone 的 worker 节点的健康状态时,发现其 worker 节点的工作组件 drone-agent 有大量链接,经排查,其数量达到了 18000+ 个,非常多。然而,查看其链接类型时,发现,几乎都是 unix domain socket 链接。
1 | [root@node002145 xxx] |
drone
drone 本身,是 CNCF 一个项目,主要是基于 docker 的 pipeline 引擎。可以替代 Jenkins、GitlabCI 执行构建过程,非常灵活。
而 drone 是 master-slave 架构,slave 节点是执行 pipeline 的节点,执行组件为 drone-agent。由于 drone 是基于 docker 的,所以,drone-agent 组件需要与其节点上的 dockerd 执行通信,告知 docker 要起什么容器、执行上面命令等等。
所以,这 18000+ 个 连接,其实便是 drone-agent 与 dockerd 的链接。那么,如何验证呢?其实很简单,我们上面执行 netstat -apn ,grep 的是 drone-agent 的进程。反过来,我们 grep dockerd 的进程,也是一样的 18000+。
至于为什么是 uninx domain socket ,而不是 TCP socket,是因为 drone-agent 与 dockerd 通信,走的就是 dockerd 服务默认的 unix domain socket server 。
这是 drone-agent 的启动方式:
1 | usr/bin/docker run --net=host --memory=10G --name=drone-agent -v /var/run/docker.sock:/var/run/docker.sock hub.xxx.com/paas/xxx/drone-agent:v1.0.2 |
可以看到,drone-agent 的启动,是要把主机上的 /var/run/docker.sock,挂载到 drone-agent 的容器内的。
这个问题,有什么影响?
由于操作系统中,进程能打开的文件句柄数量有限,而 TCP 链接本质还是一个 socket,也是要占用句柄数量的,虽然连接数有数量上限,且可通过 ulimit 配置,但是,只要有泄露问题,就还是有可能触发 “too many open files” 错误的。
排查
drone-agent 有 dockerd 之间的 socket 未释放,肯定是 drone-agent 未合理的关闭链接导致。
复现问题
先看一下,当前的 docker 数量,以及 drone-agent 的 unix domain socket 数量
1 | Every 0.3s: docker ps && netstat -apn |grep drone-agent|wc -l Mon Aug 10 14:52:11 2020 |
触发一次构建过程,再观察
1 | Every 0.3s: docker ps && netstat -apn |grep drone-agent|wc -l Mon Aug 10 14:55:11 2020 |
可以看到,一次构建触发,涨了 2 个 socket 链接,且不会释放
创建独立的 drone-agent 节点做测试
现在,构建系统是 1 个 drone-server + 2 个 drone-agent ,也就是 2 个worker 节点。之所以要创建一个独立的 drone-agent 节点,其目的是,作为我们的测试节点,且不影响现有的构建系统的征程工作。换句话说,我们要测试一些构建,这些构建,只在特定的 drone-agent 上跑。
这就要用到 Drone 的特性:节点选择特性:platform(可以理解为 K8s 体系的 NodeSelector 能力)。
①:首先,找一台机器,部署 drone-agent ,并给这个 drone-agent 指定 platform,由于 drone-agent 我们是 docker 启动的,故而,可参考如下 Dockerfile,构建一个新的 drone-agent 镜像
1 | FROM hub.xxx.com/paas/centos:7.5.1804 |
构建完之后,启动此镜像,这个 drone 的 agent 就成了一个特殊的 agent 了,其他构建,漂移不过来的。
之所以漂移不到这个 agent 节点,是因为,这个 agent 的节点,platform 指定的是 “kvm/amd64”,其他 agent 节点,默认的 platform 是 “linux/amd64” ,而最关键的是,一般的构建,默认其实指定了 platform 为 “linux/amd64”。
②:找一个需要构建的项目,配置 .drone.yml ,及 Dockerfile
1 | workspace: |
③:测试构建
为这个 gowebsocketdemo 配置了如上 .drone.yml 后,提交 tag 触发自动构建,总会漂移到这个 agent 节点上,不会漂移到其他 agent 节点。
代码层初步排查
万事具备,后边,我们调试 drone-agent ,只需要在之前的特殊节点起停 drone-agent 服务测试即可,不会影响到主业务。
从之前的分析看,drone-agent 在执行 pipeline 的时候,很短的时间,就起了 2 个 unix domain socket,它后边分步执行 pipeline 的时候,socket 数量并没有什么变化。这可能说明,unix domain socket 的建立,可能是在执行 pipeline 之前就已经开始了。为了验证这个问题,先梳理一下 drone-agent 执行 pipeline 的基本代码结构:
1 | // drone-agent 执行任务的逻辑 |
1 | // Run 方法逻辑,准备再正式执行前,打一个 120 sleep 的断点 |
改动之后,重新构建,部署 drone-agent。观察发现,在 sleep 动作发生时,drone-agent 的 unix socket 增加了 1 个,sleep 结束执行 pipeline ,unix socket 又增加了一个。
- sleep 之前,只有 r.engine.Setup 逻辑,所以,可以认为,执行 pipeline 前对 r.engine.Setup 的调用,一定有一个 unix socket 创建了但未释放。
- sleep 之后增加的 socket ,只能去 r.execAll 跟踪。
①:r.engine.Setup 里的 unix socket 追查
Setup 的逻辑其实很简单,就是创建 volume、network,所以,关于连接建立的逻辑,其实是在 e.client.VolumeCreate 之类的函数内
1 | // vendor/github.com/cncd/pipeline/pipeline/backend/docker/docker.go |
下面是 VolumeCreate 函数,这个函数,是在 docker 源码包中(vendor目录下)。
1 | // vendor/github.com/docker/docker/client/volume_create.go |
初步来看,docker client 源码中,提供了 ensureReaderClosed 方法,此方法内部如下:
1 | // vendor/github.com/docker/docker/client/request.go |
从这个层面来看,docker client 源码提供的 API,是处理了 resp.Body.Close 释放链接的方法了。如此一来,应该生效才对。
陷入两难境地:
- 为什么 docker client 的源码包的 ensureReaderClosed 不生效?
- 既然不生效,应该是使用姿势不对,一定有释放链接的方法?
个人认为,docker client 自己设计缺陷造成 socket 泄漏的可能性应该比较小(要不社区早炸了),所以问题的重心,应该放在 docker 源码包的上层调用上,也就是:github.com/cncd/pipeline 。也就是说,从可能性上来说:github.com/cncd/pipeline 使用了 github.com/docker/docker/client,但是其使用姿势有问题,所以链接未释放——这种可能性比较大。
缩小排查范围,修改源码做验证
我们排查问题,肯定要有一个基线,也就是排查 问题,先基本判定,哪种可能性大,哪种可能性小,否则,茫然排查,会花费大量精力,还不一定有结果。
目前来看,Drone 使用的源码包,可能出现 socket 泄漏问题的有 2 个:
- github.com/cncd/pipeline
- github.com/docker/docker/client
先基本判定,docker 的源码包出问题的可能性小,毕竟 docker 社区庞大,如果 docker/client 包存在问题,社区应该有大量帖子讨论这个问题,或者有相关的 issue 在 github repo 中。然而,看了一下,只有一个 dockerd socket 泄漏问题,浏览了一下具体描述,与我们的问题不太相符。
排查重心,先放到 github.com/cncd/pipeline 源码包。
走读 pipeline 源码包,其提供了一个 Engine API,我们需要对此 API 改造,让其提供 Close 方法,关闭与 dockerd 的链接
1 | // vendor/github.com/cncd/pipeline/pipeline/backend/backend.go |
①:我们为 pipeline Engine Interface,补充一个 Close 方法
1 | // vendor/github.com/cncd/pipeline/pipeline/backend/backend.go |
②:完善 pipeline Engine Interface 的实现
修改前:
1 | // vendor/github.com/cncd/pipeline/pipeline/backend/docker/docker.go |
修改后:
1 | // vendor/github.com/cncd/pipeline/pipeline/backend/docker/docker.go |
④:pipeline 源码包毕竟是底包,我们还得修改一下上层调用,所谓的上层,也就是 Drone 了。
1 | // Run starts the runtime and waits for it to complete. |
重新打包编译,部署测试。通过多次测试,发现 socket 泄漏问题解决了。每次构建结束,socket 都会释放。
考虑提PR
1、搜索相关 PR 是已存在
既然问题出在 github.com/cncd/pipeline 的源码包上,那么,可以考虑修复之后,提交一个 PR 过去。
但是,提 PR 之前,得先做搜索相关的 issue 和 PR,关键词:”leak”、”socket”、”close”
搜到一个还在 Open 的 PR:https://github.com/cncd/pipeline/pull/20
这个 PR 的大意是:作者 @cedk 提交了一个 PR,作者认为,docker client 应该主动关闭它使用的 socket ,但作者没有提到,为什么要关,以及它的负面影响是什么。然而,可惜的是,这个 PR 在 17 年提出来,一直没有被官方采纳。
2、重提 issue
由于 pipeline 未正常关闭 docker client socket,存在 socket 泄漏问题比较严重,可能触发进程打开最大连接数上限,导致后续 drone-agent 停止服务。因此,我打算提交一个 issue ,问一下 pipeline 包的官方维护者,为什么之前的这个 PR 没过。
参见:https://github.com/cncd/pipeline/issues/50
可惜的是,后来项目维护者说这个项目已经归档,不再维护了。
总结
其实,总结来说,Drone 存在的 unix domain socket 泄露问题,普遍存在于 v0.8.10 及以下版本。由于官方已不再维护 cncd/pipeline 项目,因此可以在项目里,修改 vendor 里的包来解决,不过这种方式并不优雅,也可以将 cncd/pipeline 这个包,提到项目级目录下,而不是放到 vendor 下。
至于如何修改,前面已经给了示例,也可以参考之前别人提的 PR:https://github.com/cncd/pipeline/pull/20 。