记一次 nvidia docker 错误追查

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

从现在开始尝试频繁的记录在运维的过程中遇到的各种各样的错误。这次是记录 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 就一切正常了。

Helm vs Kustomize 如何选择 kubernetes 应用部署工具

记录为什么最终没有采用 helm 而是选择了 kustomize 作为 kubernetes 应用的部署工具。

前一阵子参加了在上海举行的 KubeCon,听了很多 session 收获还是挺多。(此处应再有一篇 blog 做做总结,不过介于最近半年来的 blog 效率实在不敢立任何 flag。)尤其是 habor 项目以及 helm 项目的介绍戳中的我的痛点。虽然这些项目我都听说过,也知道他们是做什么的,一个做 registry,一个做 kubernetes 包管理。但是因为没人在旁边不停的说,所以优先级一直被自己排的很低,这也是参加这种会议的意义之一吧:让你和你正在关注的技术的同行在一起聊聊,稍微的点拨和帮助可能就会解开你对某个东西的疑惑或顾虑。helm 的出镜率实在太高了,再加上我们本身有很多项目需要以一种更好的方式去部署到 kubernets,所以回来之后就决定去尝试一下。

使用各种项目管理之前的情况

首先说说之前的痛点。我们虽然不是个大公司,可是这代码也是越敲越多,服务也是越做越全。零零总总也有十几个项目要管理了。然后我们同样有多套部署环境:内网环境,预生产环境,生产环境。那么针对每一个环境几乎都要有一套 kubernetes 的 yaml 文件,但是各个仅仅是稍有不同。

然后我们自己的 ci 是将构建好的 docker 镜像放到 registry 里面。

那么,每次更新的镜像之后就是通过人手工去部署一下,绝大多数情况就是修改一下镜像的 tag,但是由于每个环境的 yaml 略有区别,那么如果我需要在不同环境切换的时候就需要来回修改这些 yaml 文件,一不小心写错了就只能怪自己手残。然而这种部署方式虽然在 kubernetes 之下就是改改 yaml 就好了,但是依然感觉很是原始。

希望有什么改善

仔细想想,自己的需求就是这么几个:

  1. 有一个统一的模板可以管理一个项目的 kubernetes 部署结构
  2. 有某种方式可以管理不同环境之间微小的差异
  3. 每次更新基本就是修改镜像的标签然后部署,那么有没有什么简单的办法实现之,而不是让我每次都去修改 yaml 文件

针对 helm 的调研

既然都说 helm 是 kubernetes 的包管理工具,那么我就先去尝试了一下 helm。

helm 诞生于一个名为 deis 的团队。早年在 mesos 大行其道之前有过自己的一个开源 paas 方案 deis,我当时在 tw 也做类似的事情,受其影响颇深。后来 kubernetes 崛起,deis 彻底放弃之前的项目,该做了一个基于 k8s 的 paas,并开源了 helm。然后 deis 团队最终被 ms 收购了并放弃了原来的 deis 项目。

简单的看了看,helm 给我一种大而无当的感觉:它真的是一个做包管理工具的,复杂的 go template 体系以及需要单独存放的 charts 让我感觉其更适合对标 ubuntu 的 apt 或者 macos 的 brew。它更像是对外提供一个复杂的可以依据各种配置信息生成适合于不同环境的软件发布包,而不是用于我们这种轻量级的部署配置管理的。所以我就放弃使用 helm 了。

针对 kustomize 的调研

在这个时候我想起来了在之前 github trending 看到的另外一个用户做 kubernetes 配置的工具 kustomize。简单的说,它就是一个简化 kubernetes yaml 编写的工具。它提供了两个重要的功能恰好满足了我的需求。

继承和 patch

kustomize 可以设置如下的层次:

├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    └── stg
        ├── ingress.yaml
        └── kustomization.yaml

其中 base 里保存各个环境所公有的配置:

base/kustomization.yaml:

resources:
- deployment.yaml
- service.yaml

然后在 overlays 中可以定义子环境:

overlays/stg/kustomization.yaml:

bases:
- ../../base

resources:
- ingress.yaml

可以看到 stg 下继承了 base 的配置,并且添加了 ingress.yaml 配置。同时,kustomize 不仅仅支持文件级别的 patch,还支持对一个文件某些字段的 patch 如下所示,replica_count.yaml 只包含了有关 replicas 的部分即可,在执行 kustomize build 之后就可以将这部分覆盖默认的配置。

edit 命令

kustomize 提供了一个命令行方法对镜像 tag 进行修改:

kustomize edit set imagetag xxx:94c269ec

如果图省事,可以这么做

export NEWTAG=94c269ec
kustomize edit set imagetag xxx:$NEWTAG

那么每次都去 ctrl-r 修改这个 export 然后再 ctrl-r 找到第二条命令执行一下就好了。虽然它还是修改了 kustomization.yaml 但是我觉得比打开编辑器改要舒服一些。

kustomize 额外加分项

轻量级

相对 helm,kustomize 依然保留了对 kubectl apply -f 命令的支持,仅仅作为一个命令行工具;不像 helm 还需要在 k8s 里面部署一个 tiller 可谓是非常的轻量级了。

对 secret 和 configmap 的支持

分别举例说明:

bases:
- ../../base

configMapGenerator:
- literals:
  - STORAGE.DATASETUPLOADURL=https://xxx/files/datasets
  - STORAGE.CODEUPLOADURL=https://xxx/files/codes
  - LIVELOG_PREFIX=https://xxx/jobs
  name: storage-server

resources:
- ingress.yaml

imageTags:
- name: xxx
  newTag: dc12c4d7
resources:
- deployment.yaml

secretGenerator:
- name: notification-service
  commands:
    SHORT_MESSAGE_API_KEY: "bash -c 'echo -n $SHORT_MESSAGE_API_KEY'"
    MG_API_KEY: "bash -c 'echo -n $MG_API_KEY'"
  type: Opaque

generatorOptions:
  disableNameSuffixHash: true

secretGeneratorconfigMapGenerator 可以以更灵活的方式生成 configmap 和 secret,相对来说更方便吧。然后注意看我 configMapGenerator 的例子,echo -n $xxx 是会有问题的,一定要使用 "bash -c 'echo -n $SHORT_MESSAGE_API_KEY'" 的命令哦。

参考

  1. kustomize
  2. helm

采用 ingress-nginx 将服务暴露到外部

记录在采用 ingress-nginx 暴露内部服务的过程

安装

ingress-nginx 是 ingress 的一个实现,目前它已经被放在 kubernetes 项目下面了,可见算是亲儿子了,可更新频率也非常高,再加上之前在别的环境用 nginx 的场景也很多,没想太多就觉得用它了。

在我安装 ingress-nginx 的时候,其最新的版本是 0.16.2。首先遵循文档先安装 mandatory.yaml

kubectl apply -f \
    https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.16.2/deploy/mandatory.yaml

这一步里面做了如下的事情:

  1. 创建 ingress-nginx namespace
  2. 部署默认的 backend
  3. 创建相应的 ConfigMap
  4. 创建 ServiceAccount 并授权
  5. 部署 nginx-ingress-controller

创建 service 暴露到集群外部

这也是一个神奇的操作,虽说 ingress 才是真正将服务暴露到外面的资源,但是实际上反而是一个 service 完成了最终将服务暴露出去的任务。这里我们可以有多种选择:

要么采用 NodePort 将 service 通过某一个特定的端口:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: ingress-nginx

要么采用 externalIPS 直接将 service 通过特定的 IP 暴露出去:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  externalIPs:
  - <external-ips>
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: ingress-nginx

这里我采用的是第二种,这样暴露出来的服务更干净。

然后测试一下看看是否工作:

curl http://<external-ip>

如果返回 404 说明已经链接到了默认的 backend 了。

暴露服务到外部

然后我们再创建一个 ingress 将我们的 java service 暴露到路径 /api 下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: openbayes-server-ing
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /api
        backend:
          serviceName: openbayes-server-svc
          servicePort: 80
kubectl apply -f ingress.yaml

然后再尝试一下 curl http://<external-ip>/api 看看是不是可以正常的访问这个 api。

采用 annotation 对特定服务做配置

默认的 nginx 配置未必适合我们的服务,访问 Nginx Configuration 可以看到 ingress-nginx 所提供的三种 nginx 配置方式。其中 ConfigMaps 可以实现对 nginx 默认配置的修改;而 ingress annotation 则可以实现对特定 ingress 进行配置。

比如我们的 /api 有上传文件的需求,而默认的请求尺寸最大为 1m 会导致文件上传报错 413,通过添加注解 nginx.ingress.kubernetes.io/proxy-body-size 可以指定请求大小限制:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: openbayes-server-ing
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "1024m"
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - http:
      paths:
      - path: /api
        backend:
          serviceName: openbayes-server-svc
          servicePort: 80

每次修改 ingress 后,nginx-ingress-controller 会默认更新 nginx.conf,立即生效。

在 ubuntu server 下使用代理

我们在自己的办公电脑上需要一些方式访问 google.com 等一些网站。在使用 ubuntu server 的时候同样也需要安装一些被 block 的依赖,因此对于很多 server 也需要做类似的配置。这里记录一下自己配置的过程。

前提条件

首先,这里所有的配置是建立在我已经有了一个 ss 的 server 的前提之下。

1. 安装 libev 版本

sudo add-apt-repository ppa:max-c-lv/shadowsocks-libev -y
sudo apt-get update
sudo apt-get install shadowsocks-libev -y

2. 配置 client

修改 /etc/shadowsocks-libev/config.json

{
    "server": "",
    "server_port": ,
    "local_address": "127.0.0.1",
    "local_port": 1080,
    "password": "",
    "timeout": 600,
    "method":"",
    "fast_open": true
}

重启服务

systemctl daemon-reload
systemctl restart shadowsocks-libev-local@config
systemctl enable shadowsocks-libev-local@config

至此,本地已经有了 sock5 的代理:localhost:1080。然而系统使用的代理大多是 http_proxy 我们需要另外一个工具 polipo

3. 配置 http https 代理

polipo 可以把 socks5 代理转化为 http 代理用。首先安装 polipo

sudo apt-get install polipo -y

然后修改配置文件 /etc/polipo/config

socksParentProxy = "localhost:1080"
socksProxyType = socks5

重启服务

systemctl enable polipo
systemctl restart polipo

然后可以在命令行下试试看了:

http_proxy=http://localhost:8123 curl www.google.com

参考

  1. UbuntuServer配置ShadowSocks代理

国内环境下更好的 docker 镜像获取

最近欠了好多的 blog,是在是有点忙,周末也被各种事情缠身。今天趁周日的最后半个小时抓紧记录下来一些调研的成果。

国内的网络环境大家都知道,一方面是不稳定的宽带,另一方面就是对一些国外网站访问的不可靠。今天集中记录一下自己在 docker 镜像处理方面的一些小技巧。

加速 docker pull

Docker 本身提供了一个叫做 docker registry mirror 的东西,就是为了减少重复的镜像下载所产生的额外带宽,在国内访问 docker 官方 image 极其缓慢的场景下这种需求尤为凸显。如果不设置相应的 mirror 国内服务器下载镜像真是举步维艰。

通过在 /etc/dockeer/daemon.json 做相应的配置就可以添加一个 registry mirror:

{
  "registry-mirrors": ["<your registry mirror url>"]
}

配置之后需要重启 docker。

$ sudo pkill -SIGHUP dockerd

docker-cn

Docker 中国 估计是 docker 为了在中国开展业务搞的子公司吧?不应该是什么山寨网站吧。其提供了加速国内 docker pull 的镜像地址 https://registry.docker-cn.com。按照上述的配置方式配置即可使用:

{
  "registry-mirrors": ["https://registry.docker-cn.com"]
}

然而很遗憾,这个速度并不理想,以我目前所在的网络环境,下行速度 4MB/s 拉取 ubuntu 镜像的速度大概也就是 400KB/s ~ 500KB/s。

daocloud

daocloud 作为一个做 caas 的公司为国内提供了号称免费的 mirror 构建服务。登录控制台就可以看到如图所示的位置的加速器按钮了,点进去就有相应的脚本了。

注意 虽然人家是好意给了一个 shell 脚本帮助修改 /etc/docker/daemon.json 的配置,然而如果你的 docker 不止有一个 runtime,比如你像我这样需要跑 nvidia docker 的 runtime,那么这个脚本会把你的配置搞砸…建议直接想我这样手动配置:

{
    "registry-mirrors": [""],
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}

其中将 `` 替换成 daocloud 提供的 mirror 即可。

配置之后重启 dockerd 感受下速度吧,同样的网络环境,基本是 2MB/s ~ 3MB/s。

拉取更难以获取的镜像

上述的 docker-cn 以及 daocloud 仅仅是支持官方 docker.io 镜像的加速,然而要知道当今世界 google 的 google cloud platform 做的是相当不错,google 旗下的 kubernetes 基本是当前 PaaS 的不二之选,其官方镜像域名 gcr.io 下有大量 docker.io 无法取代的重要资源。而这些资源早早的已成为了墙外之物。

做搬运工

为了获取 gcr.io 域名下的镜像,我们可以在境外创建一个 vps 然后通过蚂蚁搬家的方式一点一点挪过来:

  1. 把 gcr.io/image-name:tag pull 到 vps 上 docker pull gcr.io/image-name:tage
  2. 重新打标签到自己 docker.io 的账号下 docker tag gcr.io/image:tag <username>/image:tag
  3. 把新镜像推送到 docker.io docker push <username>/image:tag

当然你可以写一个脚本,把自己用得着的镜像一个个 push 到 docker.io 中。甚至有人会写一些类似于 webhook 的东西,当 gcr.io 一些特定项目的镜像更新后会自动触发相应的流程自动托运新的镜像。不过不论如何这样的坏处显而易见:这是一个体力活,虽然有一些加速的脚本但是我依然需要更新脚本,管理 webhook…我先前已经用这个方法搞了一堆这样的镜像了…都是眼泪…

google 一下发现这种方式在用 kubeadam 安装 kubernetes 的场景被很多人采用了。

docker proxy 配置

既然有了 vps 自然是可以直接搭建一个代理的,docker 本身是支持在 docker pull 使用代理的,那么配个代理不久解决问题了吗。具体怎么配置代理这里就不讲了,我只记录 docker 这边的配置:

首先 mkdir /etc/systemd/system/docker.service.d

然后创建 /etc/systemd/system/docker.service.d/http-proxy.conf,添加内容如下:

[Service]
Environment="HTTP_PROXY=http://user01:password@10.10.10.10:8080/"
Environment="HTTPS_PROXY=https://user01:password@10.10.10.10:8080/"
Environment="NO_PROXY=localhost,.docker.io,.docker.com,.daocloud.io"

当然要使用自己的 HTTP_PROXYHTTPS_PROXY,然后把不想使用代理的域名添加到 NO_PROXY,尤其是使用的镜像域名和 docker.io 应该考虑在内。

最后更新 systemctl 并重启服务

$ systemctl daemon-reload
$ systemctl restart docker

之后可以用以下镜像测试一下:

$ docker pull k8s.gcr.io/kube-scheduler-amd64:v1.10.2

注意

在查找有关 docker proxy 内容时会发现有两种搜索结果,一种是我上述讲的 docker pull 时采用代理的方法;另一种是如何在 docker container 中配置代理:Configure Docker to use a proxy server 这个迷惑性还是有的…不过第二种情况以后也可能会用得上。