容器化Node Exporter对主机磁盘使用率的监控问题

容器化Node Exporter对主机磁盘使用率的监控问题

Node Exporter是prometheus社区开发的节点监控工具。在Prometheus生态中,如果一个组件被命名为Exporter,那么从狭义上来说,可以将它理解为一个适配器,用于将某些应用原生的非Prometheus格式的监控指标转换为符合Prometheus标准的指标,供Prometheus Server抓取,从而能够将该应用完美地融入Prometheus生态体系。一般来说,在Linux系统下,操作系统的状态信息会以虚拟文件系统的形式在/proc/sys两个目录之下。因此,Node Exporter的职责就是按需从/proc/sys读取指标(本质上是读取相应文件的内容,例如内存相关的信息存放在/proc/meminfo中),然后进行格式转换。

Node Exporter的工作原理非常简单,如果以可执行文件的方式直接部署在宿主机上通常也不会发生太大的问题。但是随着云原生浪潮的到来,再加上Prometheus和Kubernetes原生的亲和性,以Prometheus为中心构建Kubernetes集群的监控系统显然是我们的不二之选。因此作为Prometheus生态重要的组成部分,Node Exporter的容器化也是不可避免的,并且一般它会以DaemonSet的形式运行在集群的每个节点之上。

已知Docker容器本质上只是一个普通进程,但是通过Namespace和CGroup等内核机制为其构建了一个逻辑上的隔离环境,从而允许其有独立的网络空间,挂载点视图等等。由于Node Exporter的任务是监控宿主机的运行状态,隔离的运行环境反而会对其运行造成干扰,因此它需要尽量与宿主机贡献Namespace。一般与Node Exporter运行相关的并且我们能够配置的是Network和PID这两个Namespace。所以DaemonSet的相关配置如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
...
spec:
template:
  spec:
    hostNetworktrue
    hostPIDtrue

已知Node Exporter主要通过读取/proc/sys来获取监控指标,虽然已经通过上述方法共享了Network Namespace以及PID Namespace,但是容器和宿主机的/proc以及/sys中的内容仍然不是完全相同的。因此,最好的方法其实是将宿主机中的这两个目录挂载到容器中,再让容器中的Node Exporter进程读取从这两个挂载目录中获取宿主机的运行信息,配置如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
...
spec:
template:
  spec:
    containers:
    - args:
      - --path.procfs=/host/proc
      - --path.sysfs=/host/sys
      volumeMounts:
      - mountPath: /host/proc
        name: proc
        readOnlytrue
      - mountPath:m /host/sys
        name: sys
        readOnlytrue
      volumes:
      - name: proc
        hostPath:
          path: /proc
      - name: sys
        hostPath:
          path: /sys

通过上述配置,在容器中运行的Node Exporter的确能够正确获取绝大多数的所在宿主机的运行指标。但是节点磁盘的容量数据除外。将常用的df命令的输出结果和Node Exporter暴露的指标对比之后可以发现,Node Exporter暴露的指标不但少了若干个挂载磁盘的信息,而且同样对于挂载在根目录的磁盘,两者的数据也并不一致。下面我们通过Node Exporter采集相关指标的实现原理来分析造成上述现象的原因。

当我们在谈论磁盘的使用率时谈论的实际上是挂载在某个目录的磁盘分区的使用率。一个磁盘分区会由对应的文件系统进行管理,通过该文件系统我们就能获取到该分区的使用情况。已知自从引入了Mount Namespace之后,挂载点信息已经从系统全局级别的信息变为了进程级别的信息。显然对于宿主机的磁盘使用率的监控,我们应该访问的是根Mount Namespace的挂载信息而1号进程肯定是位于根Namespace内的。因此Node Exporter会首先通过读取/proc/1/mounts获取根Mount Namespace的挂载点信息:

root@yzz:/dev# cat /proc/1/mounts
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0
udev /dev devtmpfs rw,nosuid,relatime,size=4066220k,nr_inodes=1016555,mode=755 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0
tmpfs /run tmpfs rw,nosuid,noexec,relatime,size=817468k,mode=755 0
/dev/vda1 / ext4 rw,relatime,errors=remount-ro,data=ordered 0
securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0
tmpfs /dev/shm tmpfs rw,nosuid,nodev 0

每一行都是对一个挂载点的描述,从左往右各列依次为设备名,挂载点,文件系统的类型等等,Node Exporter再依次对每行的挂载点执行statfs系统调用,获取挂载在该路径的文件系统的相关信息,从中我们可以获取到对应设备的总的存储容量以及当前可用的存储容量,从而分析出磁盘的使用率。

通过上述Node Exporter对磁盘使用率指标获取原理的分析,我们可以知道,之所以在容器中运行的Node Exporter无法获取到正确的信息是因为容器的根目录和宿主机的根目录并不一致。因此Node Exporter对相对于容器根目录的挂载点执行statfs系统调用是无法获取到正确信息的。对于这个问题,解决方法是将宿主机的根目录以Bind Mount的形式(且Propagation Type为Slave,从而宿主机的挂载点在容器中也可见)挂载到容器中并让Node Exporter基于该路径寻找对应磁盘的挂载点,具体配置如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
...
spec:
template:
  spec:
    containers:
    - args:
      - --path.procfs=/host/proc
      - --path.sysfs=/host/sys
      - --path.rootfs=/host/root
      volumeMounts:
      ...
      - mountPath: /host/root
        name: root
      volumes:
      ...
      - name: root
        hostPath:
          path: /

此时,对于宿主机根目录挂载磁盘信息的访问,Node Exporter将不再直接访问容器的根目录,而是会访问宿主机根目录在容器中的挂载点,即/host/root,获取得到。

综上,对于Node Exporter这种较为特殊的应用,容器化的作用带来的仅仅是打包部署的方便,而容器的隔离性实际上还会对应用的正常运行造成阻碍。但是通过本文的案例,我们也可以看到,相比于虚拟机,容器的隔离机制是更为灵活的,通过共享Namespace以及HostPath形式的Volume挂载,最终能够让对宿主机环境有较大依赖的程序依然能够正常地在容器中运行。

 

posted on 2020-02-24 15:34  姚灯灯!  阅读(794)  评论(0编辑  收藏

导航