Eisen's Blog

© 2024. All rights reserved.

在 Kubernetes 中使用最新的 LXCFS

2020 September-21

在 k8s 中启动的 pod 下的 container 虽然在资源上做了隔离,但是这种隔离对用户通常是不可见的,具体的表现就是使用 top cat /proc/cpuinfo 或者 free -m 等这样的命令的时候看到的依然是宿主机的资源。这种行为在 openbayes 这种为外部用户分配资源的场景会有很强的迷惑性,用户不知道自己到底有多少资源,因为从里面看到的资源和在外面的声明的是不一样的。为了达到容器内外的一致性,我们引入了 lxcfs。

工作原理

lxcfs 官方的定义里提到它是一个用户空间的文件系统。它主要提供了一系列文件可以覆盖 /proc 用以覆盖系统的这些目录。也就是说,如果想要让容器里看到的资源是已经做了隔离之后的资源,其实就是将系统默认的一些文件通过 mount 的方式进行覆盖。当容器中进程读取相应文件内容时,lxcfs 会从容器对应的 cgroup 中读取正确的资源限制。

lxcfs 的方案调研

目前通过搜索引擎可以找到一篇文章介绍了如何在 k8s 中通过 daemonset 安装 lxcfs 以及如何通过 k8s 的 admission-webhook 为 k8s 里的容器默认做这个隔离的绑定,介绍的很清楚,但有一些小问题:

  1. 其使用的 lxcfs 有点老了(3.0.4),直接拿过来用会发现其实 /cpu/online 这个没有解决,会导致在 python 下 os.cpu_count() 依然是错误的,需要升级 lxcfs 才能解决这个问题
  2. 通过 hooks 的方式有点像是 meta programming 太随意的修改了 k8s 容器的流程,这可能在后期带来维护的问题,并且我们只有对用户的容器才有做隔离的需求,所以不如就是在容器启动的时候直接处理就好

因此最终的方案如下:

  1. 依然使用 daemonset 方式安装和启动 lxcfs,但是对其脚本做了升级改造
  2. 在模板中增加绑定即可,不实用 hook 的方式

安装

https://github.com/denverdino/lxcfs-admission-webhook/tree/master/lxcfs-image 的两个文件做了修改:

Dockerfile:

FROM ubuntu:18.04
RUN apt update -y
RUN apt-get --purge remove lxcfs
RUN apt install -y wget git libtool m4 autotools-dev automake pkg-config build-essential libfuse-dev libcurl4-openssl-dev libxml2-dev mime-support
ENV LXCFS_VERSION 4.0.5

RUN wget https://github.com/lxc/lxcfs/archive/lxcfs-$LXCFS_VERSION.tar.gz && \
    mkdir /lxcfs && tar xzvf lxcfs-$LXCFS_VERSION.tar.gz -C /lxcfs --strip-components=1 && \
    cd /lxcfs  && ./bootstrap.sh && ./configure && make

COPY start.sh /
CMD ["/start.sh"]

主要修改了以下几个方面:

  1. 基础镜像用了 ubuntu 当然相应的依赖也做了对应的修改
  2. 从 github 下载了 4.0.5 版本的 lxcfs 并按照新的编译流程生成执行文件和库,其中在 make 之后会在目录下生成 src/lxcfs src/.libs/liblxcfs.so src/liblxcfs.la 三个文件
  3. 没做 build 阶段,因为产出的镜像并不是很大,就算了...要求更高的同学可以继续修改哦

start.sh:

#!/bin/bash

# Cleanup
nsenter -m/proc/1/ns/mnt fusermount -u /var/lib/lxcfs 2> /dev/null || true
nsenter -m/proc/1/ns/mnt [ -L /etc/mtab ] || \
        sed -i "/^lxcfs \/var\/lib\/lxcfs fuse.lxcfs/d" /etc/mtab

# Prepare
mkdir -p /usr/local/lib/lxcfs /var/lib/lxcfs

# Update lxcfs
cp -f /lxcfs/src/lxcfs /usr/local/bin/lxcfs
cp -f /lxcfs/src/.libs/liblxcfs.so /usr/local/lib/lxcfs/liblxcfs.so
cp -f /lxcfs/src/liblxcfs.la /usr/local/lib/lxcfs/liblxcfs.la


# Mount
exec nsenter -m/proc/1/ns/mnt /usr/local/bin/lxcfs /var/lib/lxcfs/ --enable-cfs -l

主要修改如下:

  1. src/lxcfs src/.libs/liblxcfs.so src/liblxcfs.la 这三个输出文件,按照编辑的目标文件做对应的修改
  2. 启动的之后增加了两个参数 --enable-cfs -l 不然 cpu/online 依然不起作用

Daemonset 就没做什么修改了,仅仅是更改了镜像而已。

绑定

绑定这里提到了是在 openbayes 的构建流程中做了修改:

volumes:
  - name: lxcfs-proc-cpuinfo
    hostPath:
      path: /var/lib/lxcfs/proc/cpuinfo
      type: File
  - name: system-cpu-online
    hostPath:
      path: /var/lib/lxcfs/sys/devices/system/cpu/online
      type: File
  - name: lxcfs-proc-diskstats
    hostPath:
      path: /var/lib/lxcfs/proc/diskstats
      type: File
  - name: lxcfs-proc-meminfo
    hostPath:
      path: /var/lib/lxcfs/proc/meminfo
      type: File 
  - name: lxcfs-proc-stat
    hostPath:
      path: /var/lib/lxcfs/proc/stat
      type: File    
  - name: lxcfs-proc-swaps
    hostPath:
      path: /var/lib/lxcfs/proc/swaps
      type: File
  - name: lxcfs-proc-uptime
    hostPath:
      path: /var/lib/lxcfs/proc/uptime
      type: File
  ...

测试

如下所示,在一个分配了 4 个 cpu 20g 内存的容器里以下命令已经可以显示实际分配的资源数目了:

1. top

2020 09 21 14 44 44

2. free

2020 09 21 14 45 18

3. os.cpu_count

2020 09 21 14 45 37


初步尝试 Github Actions

2020 July-18

多次尝试补上多年不写的博客,这次一定要成功...这里介绍刚刚切换到 github action 的原因以及最开始遇到的一些问题和解决方案。后面如果有什么新的进展就补充一篇进阶吧。

为啥尝试 github actions

先讲讲我使用 CI 的一些历史,看看为啥一路走来走到了这里。

最最初什么都没有的时候自然就是 gocdjenkins 的统治时期,那个时候我基本就是用 CI 自己基本没配置过。那个时候的 CI 特别难用,没有现在的一些重要功能。每次跑 CI 都像是在重新 provision 一个环境似的,由于我自己没什么经验就带过吧。

之后(17 年)尝试过 drone ci文章 中做了介绍,提到了 pipeline as code原生支持 docker 以及 简单易用的插件扩展 这些功能,不过当时 droneci 并不是一个类似于 saas 的产品,而是需要自己部署的,不是那么开箱即用,但我也觉得很方便了。几年之后越来越多的 CI 在这些方面都支持的越来越好了。在 openbayes 项目之初(18 年初),出于集中力量办正事的考虑,就直接使用了 circleci(之前在 travisci 和它之间做过纠结,但考虑到其 pipelineascode 更友好就采用它了)。可能这不是一个最优的策略,但既然都 pipeline as code 了,其实切换成本并不大,处于有问题立即跑路的想法就这么用了。后续来看问题不大,

我们背后的技术栈还是比较散的,按照语言区分的话基本有这么几个:

  • golang
  • python
  • java
  • ruby
  • js / typescript

cicleci 支持 docker 环境构建,其实很容易的就覆盖了。然后几乎所有的项目的产出物都是 docker 镜像,只要和 hub.docker.com 的访问速度不错一切都好说。可惜后面出现了两个事情叠加起来让我们不得不考虑更换 CI 了。

首先是在 19 年 10 月份 CircleCI 的计价策略发生了一些变化,见 Plans for optimal performance: why CircleCI is changing our pricing model,其实现在回想起来我都不记得之前老的计价策略是什么了。这个新的计价策略基本就是人头数 + 运行时长。

如果是单纯的这个变化也感觉可以接受,可后面我们就发现 CircleCI 跑的速度大幅下降了...甚至会时不时的卡死在那里...这就让人比较烦躁了...让人觉得只是在变相的多收取费用呀...而且动不动就卡一个小时这多影响开发效率呀。那个时候 github actions 也已经存在了一阵子了,11 月份我们就开始尝试把最频繁提交的项目做迁移了。

当然其实还有另外一个想法吧,众所周知,GitLab 项目是自带 CI/CD 的,把代码仓库和 CI/CD 的流水线放在一起还算是挺自然的一个想法吧。毕竟在这个场景下,代码才算是核心资产,CI/CD 作为附属品保证了代码的可用性/可信性,算是 GitOps 的一个环节,放在同一个 vendor 下进行管理也算是自然吧。

同样是 Pipeline as code 切换成本基本就是一个会配置 CI 的同学半个小时的工作量。但在切换后发现了一个问题:github 的环境访问国内的一些镜像仓库的访问速度比较慢。一个大的仓库 1500+ TestCase 跑下来只用了 5 分钟,提交个镜像却花了 15 分钟。做了些研究之后发现 Github 下支持了 self hosted runners ,研究了下,感觉是非常好的思路。

使用 Self Hosted

顾名思义,self hosted runners 就是支持用户用自己的环境(而不是 github 的自己的机器)去跑 ci 的任务。总结一下它的好处有这么几点,特别解决我们的痛点。

自由配置网络和硬件

前面提到,我们 CI/CD 的产出物基本就是 docker 镜像,考虑到容错和各地网络访问速度,我们会将同一份镜像提交到多个镜像仓库。比如我们至少会在 ucloud 和 hub.docker 两个地方存放镜像。那么,CI/CD 的环境最好可以同时对这两个环境的网络访问速度还算可以。我就选择了一个香港节点,直接测试后发现两边提交镜像的速度都可以接受,就把它作为 self hosted runner 了。并且由于自己的机器就那么一到两台,docker build 的缓存还可以自然的使用起来,CI 跑起来更快了。

在国内的同学都了解目前 hub.docker 几乎要不可用了...多个镜像仓库来回倒腾不知道花了多少时间了。甚至连 Github 的访问如果不开个什么代理都举步维艰。对网络环境的考量确实是个很无聊但是又非常务实的需求。

效果自然是还不错,从原来的十几分钟降到了几分钟,快了不少。

方便的安装和配置

Github 的这个功能跑起来非常容易,开一个虚拟机然后按照 Github 上面的提示直接跑下面的脚本就行了。

2020 07 18 15 50 01

安装好之后就能看到这个 runner 了:

2020 07 18 14 31 29

多说一句,在最开始 Github 只支持单个仓库的 self-hosted runner 感觉比较浪费,不过我们使用的时候(就是发现 Github 自己的 CI 环境网络越来越不合适的时候)已经支持在组织级别增加了,可以在这里看到。

其他优点

其他的就是如下了:

  1. 免费,除了 host 本身的云计算费用外其他就不花钱了
  2. 单个任务的执行上限更长,具体见这里的解释,这个限制对大部分场景来说意义不大,不过我们自己的一些场景还是挺需要解除这个限制的

吹完它之后说它的一些问题。

Self Hosted 遇到的一些问题

Provision 的坑

上文提到从 circleci 往 github hosted github action 迁移很快的,但是从 Github Hosted 往 self-hosted 迁移后会发现有各种诡异的依赖报错,原因很简单,就是人家的机器上有的东西你没有装好。

那人家咋装的呢?在 virtual-environments 有详细的安装脚本。所以如果想要自己的 self-hosted 和 github hosted 跑的一样就需要用这里面的脚本全部跑一遍才行...听起来有点麻烦,不过其实也可以自己灵活点...既然自己的 pipeline 自己都知道有啥依赖,不如缺啥补啥...比如我发现前端的 CI 报错没有 node 那就跑个 nodejs,然后发现没有 phantomjs 都就跑一下这个脚本。

Job 的限制

前面说了解除限制方面的有点,但是也有副作用,在 https://github.community/t/do-jobs-in-self-hosted-runners-run-in-parallel/17776/2 有一个解释。我直接贴在这里吧,因为这个问题挺是个问题的。

Jobs should run in parallel for both hosted runner and self-hosted runner. But for self-hosted runner, there are some comments:

  • One self-hosted runner can only run one job at a time, when no available runners are idle, the subsequent jobs will be in queueing until available runners are idle.
  • If have enough available self-hosted runners for a workflow run, each job will be assigned a self-hosted runner, and the jobs will run in parallel.
  • When multiple workflows are running, if the workflows are more than the available self-hosted runners, each workflow will be assigned only one self-hosted runner.

Thanks.

总结起来就是你要是只有一个 self-hosted 的节点,那抱歉什么都是按顺序来。如果有多个节点,那么什么都好说。


记一次 nvidia docker 错误追查

2019 January-17

最近搞装机的事情很是烦躁,非常理解做运维的同学的辛苦了。尤其是当出现一些不知所云的错误的时候,真的是头都炸了。而且如果不能保持冷静还可能把原先做的工作因为一两个失误毁于一旦。

从现在开始尝试频繁的记录在运维的过程中遇到的各种各样的错误。这次是记录 dgx1 上 nvidia-docker2 异常,导致 nvidia-device-plugin 无法启动。

dgx1 加入 kubernetes 集群之后发现其 nvidia-device-plugin 启动报错 RunContainerErrork describe pod 发现错误信息:

Error: failed to start container "nvidia-device-plugin-ctr": Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \"process_linux.go:385: running prestart hook 0 caused \\\"error running hook: exit status 1, stdout: , stderr: exec command: [/usr/bin/nvidia-container-cli --load-kmods configure --ldconfig=@/sbin/ldconfig.real --device=all --utility --pid=11077 /var/lib/docker/overlay2/510a6de5ed82decf7421a392e5274b4fe47e8d0cd3610175c3550f1d26c91376/merged]\\\\nnvidia-container-cli: initialization error: driver error: failed to process request\\\\n\\\"\"": unknown

说是驱动有问题,第一个想到的就是因为将早先的 nvidia-384 驱动更新到了 nvidia-410 可能有问题,再重启之后没有作用,于是尝试通过 apt 重新安装 nvidia-410

$ add-apt-repository ppa:graphics-drivers/ppa
$ apt update
$ apt install nvidia-410

重启后依然发现类似问题,再去搜索发现 https://zhuanlan.zhihu.com/p/37519492 和我遇到的问题类似,通过命令 nvidia-container-cli -k -d /dev/tty info 得到具体的报错:

E0117 08:51:20.843706 12905 driver.c:197] could not start driver service: load library failed: libnvidia-fatbinaryloader.so.384.145: cannot open shared object file: no such file or directory

384 这个驱动版本我明明已经删了,为什么还要找这个库呢?是不是因为新的 410 安装的不全呢?再往后看,提到

安装驱动的时候会自动安装这个 libcuda1-384 包的,估计是什么历史遗留问题,或者是 purge 又 install 把包的依赖关系搞坏了,因此现在需要重新安装。

立即想到我的 410 是不是也没有安装 libcuda1-410 呢?赶紧 apt search libcuda 发现果然有这么个依赖,apt install libcuda1-410 赶紧安装,再次跑 nvidia-container-cli -k -d /dev/tty info 就一切正常了。