李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
工具
正文
cAdvisor工作原理
Leefs
2025-05-24 PM
2184℃
0条
[TOC] ### 一、容器监控方案介绍 虽然传统的物理机和虚拟机监控已经有了比较成熟的监控方案,但是容器的监控面临着更大的挑战,因为容器的行为和本质与传统的虚拟机是不一样的,总的来说,容器具有以下特性: 1. 容器是短期存活的,并且可以动态调度; 2. 容器的本质是进程,而不是一个完整操作系统; 3. 由于容器非常轻量,容器的创建和销毁也会比传统虚拟机更加频繁。 Docker 容器的监控方案有很多,除了 Docker 自带的`docker stats`命令,还有很多开源的解决方案,例如 `sysdig`、`cAdvisor`、`Prometheus` 等,都是非常优秀的监控工具。 下面首先来看下,不借助任何外部工具,如何用 Docker 自带的docker stats命令实现容器的监控。 #### docker stats 命令 使用 Docker 自带的docker stats命令可以很方便地看到主机上所有容器的 CPU、内存、网络 IO、磁盘 IO、PID 等资源的使用情况。 下面我们可以具体操作看看。 + 首先在主机上使用以下命令启动一个资源限制为 1 核 2G 的 nginx 容器: ```sh docker run --cpus=1 -m=2g --name=nginx -d nginx ``` + 容器启动后,可以使用docker stats命令查看容器的资源使用状态: ```sh docker stats nginx ``` 通过docker stats命令可以看到容器的运行状态如下: | CONTAINER | CPU % | MEM USAGE / LIMIT MEM % | NET I/O | BLOCK I/O | PIDS | | :----------- | :---- | :---------------------- | ------------- | :------------ | :--- | | f742a467b6d8 | 0.00% | 1.387 MiB / 2 GiB 0.07% | 656 B / 656 B | 0 B / 9.22 kB | 2 | 从容器的运行状态可以看出,docker stats命令确实可以获取并显示 Docker 容器运行状态。但是它的缺点也很明显,因为它只能获取本机数据,无法查看历史监控数据,没有可视化展示面板。 ### 二、cAdvisor简介 cAdvisor 是谷歌开源的一款通用的容器监控解决方案。cAdvisor 不仅可以采集机器上所有运行的容器信息,还提供了基础的查询界面和 HTTP 接口,更方便与外部系统结合。 所以,cAdvisor很快成了容器指标监控最常用组件,并且 Kubernetes 也集成了 cAdvisor 作为容器监控指标的默认工具。 cAdvisor(Container Advisor)是内嵌在kubelet中的轻量级监控代理,以Daemon形式运行在每个Worker节点上。 其监控体系采用三层架构: + **数据采集层**:通过Linux cgroups接口获取容器级指标 + **数据处理层**:实时聚合计算容器/节点资源使用率 + **数据暴露层**:通过REST API和Prometheus格式提供指标 官方地址: + dockerhub: https://hub.docker.com/r/google/cadvisor/ + github: https://github.com/google/cadvisor ### 三、cAdvisor安装和使用 #### 3.1 通过 Docker 容器运行 cAdvisor ```sh $ docker search cadvisor $ docker pull google/cadvisor $ docker run \ --volume=/:/rootfs:ro \ --volume=/var/run:/var/run:rw \ --volume=/sys:/sys:ro \ --volume=/var/lib/docker/:/var/lib/docker:ro \ --volume=/cgroup:/cgroup:ro \ --privileged=true \ --userns=host \ --publish=8080:8080 \ --detach=true \ --name=cadvisor \ google/cadvisor:latest $ docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --volume=/cgroup:/cgroup:ro --privileged=true --userns=host --publish=8080:8080 --detach=true --name=cadvisor google/cadvisor:latest ``` 运行 cadvisor 容器的 docker run 部分命令选项介绍: - `–detach=true` :指定容器在后台运行 - `–privileged=true`:设置为true之后,容器内的root才拥有真正的root权限,可以看到host上的设备,并且可以执行mount;否者容器内的root只是外部的一个普通用户权限 - `–volume=/cgroup:/cgroup:ro`:由于cadvisor需要通过socket访问docker守护进程,在CentOS和RHEL系统中需要这个选项。 打开浏览器,可以通过访问`http://localhost:8080`访问到 cAdvisor 的 Web 界面。 可以看到:CPU的使用率、内存使用率、网络吞吐量以及磁盘空间利用率,点击界面顶部 docker 可以进入查看某个docker容器的详细信息 cAdvisor 不仅可以监控容器的资源使用情况,还可以监控主机的资源使用情况。下面我们就先看下它是如何查看主机资源使用情况的。 > cAdvisor 监控容器具有以下特点: > > 1. 可以同时采集物理机和容器的状态; > 2. 可以展示监控历史数据。 #### 3.2 二进制可执行文件使用 cAdvisor cAdvisor 项目在他的 [release](https://github.com/google/cadvisor/releases) 页面发布了可执行的二进制,这些文件可以直接下载并执行 ```sh $ wget https://github.com/google/cadvisor/releases/download/v0.26.1/cadvisor $ chmod 755 cadvisor $ ./cadvisor ``` 默认情况下 cAdvisor 的网页端口为 8080,直接访问本地 [http://localhost:8080](http://localhost:8080/) 即可 #### 3.3 REST API cadvisor还提供远程调用的REST API,详情可以参考如下文档:https://github.com/google/cadvisor/blob/master/docs/api.md 另外,github上还有提供了一个用Go语言实现的调用REST API的客户端:https://github.com/google/cadvisor/tree/master/client #### 3.4 `cAdvisor`中监控指标 + **核心监控维度** | 指标名称 | 类型 | 含义 | | :------------------------------------- | :------ | :------------------------------------------- | | container_cpu_load_average_10s | gauge | 过去10秒容器CPU的平均负载 | | container_cpu_usage_seconds_total | counter | 容器在每个CPU内核上的累积占用时间 (单位:秒) | | container_cpu_system_seconds_total | counter | System CPU累积占用时间(单位:秒) | | container_cpu_user_seconds_total | counter | User CPU累积占用时间(单位:秒) | | container_fs_usage_bytes | gauge | 容器中文件系统的使用量(单位:字节) | | container_fs_limit_bytes | gauge | 容器可以使用的文件系统总量(单位:字节) | | container_fs_reads_bytes_total | counter | 容器累积读取数据的总量(单位:字节) | | container_fs_writes_bytes_total | counter | 容器累积写入数据的总量(单位:字节) | | container_memory_max_usage_bytes | gauge | 容器的最大内存使用量(单位:字节) | | container_memory_usage_bytes | gauge | 容器当前的内存使用量(单位:字节 | | container_spec_memory_limit_bytes | gauge | 容器的内存使用量限制 | | machine_memory_bytes | gauge | 当前主机的内存总量 | | container_network_receive_bytes_total | counter | 容器网络累积接收数据总量(单位:字节) | | container_network_transmit_bytes_total | counter | 容器网络累积传输数据总量(单位:字节) | + **指标采集原理** ```go // cAdvisor核心采集逻辑简化示意 func CollectMetrics() { for { stats := cgroups.GetStats("/sys/fs/cgroup") // 读取cgroups数据 metrics := calculateDerivedMetrics(stats) // 计算衍生指标 prometheus.Register(metrics) // 注册Prometheus指标 time.Sleep(10 * time.Second) // 采集间隔 } } ``` ### 四、监控原理 Docker 是基于 Namespace、Cgroups 和联合文件系统实现的。其中 **Cgroups 不仅可以用于容器资源的限制,还可以提供容器的资源使用率**。无论何种监控方案的实现,底层数据都来源于 Cgroups。 Cgroups 的工作目录为/sys/fs/cgroup,/sys/fs/cgroup目录下包含了 Cgroups 的所有内容。**Cgroups包含很多子系统,每一个目录代表一个子系统,可以用来对不同的资源进行限制**。例如对CPU、内存、PID、磁盘 IO等资源进行限制和监控。 **Docker 会在每一个 Cgroups 子系统下创建 docker 文件夹**。 > 监控系统是如何获取容器的内存限制的? + memory 子系统是Cgroups 众多子系统的一个,主要用来限制内存使用。 当在主机上使用以下命令启动一个资源限制为 1 核 2G 的 nginx 容器后: `docker run --name=nginx --cpus=1 -m=2g --name=nginx -d nginx` 通过命令行的输出可以得到容器的 ID,同时 Docker 会在/sys/fs/cgroup/memory/docker目录下以容器 ID 为名称创建对应的文件夹。 进入到容器ID命名的目录可以看到,容器 ID 的目录下有很多文件,其中 memory.limit_in_bytes 文件代表该容器内存限制大小,单位为 byte。 > 监控系统是如何获取容器的内存使用状态的? 内存使用情况存放在 memory.usage_in_bytes 文件里,通过 cat 命令可以查看当前内存的使用大小为 4259840 byte,约为 4 M。 > 监控系统是如何获取容器的网络监控数据来源? 网络的监控数据来源是从 `/proc/{PID}/net/dev` 目录下读取的,其中 PID 为容器在主机上的进程 ID。`/proc/27348/net/dev` 文件记录了该容器里每一个网卡的流量接收和发送情况,以及错误数、丢包数等信息。可见容器的网络监控数据都是定时从这里读取并展示的。 **容器的监控原理其实就是定时读取 Linux 主机上相关的文件并展示给用户。** ### 五、cAdvisor代码结构分析 + **数据采集结构图**  + **machineInfo** machineInfo的数据采集具体代码,主要是 `/machine/info.go -> Info()` ,在`new manager`的时候会去调用一次这个方法,主要是读取系统文件(具体文件见上面的“整体结构图”),将数据放入到m.MachineInfo中,后续在Start方法中,起一个协程,定时调用该方法,更新本地cache。 相关代码地址如下: ```go /machine/info.go /machine/machine.go /machine/operatingsystem_unix.go /machine/operatingsystem_windows.go ``` + **containerInfo** 整体的containerInfo的数据采集,由 /manager/manager.go -> Start() 开始,起整体流程图如下:  整体的流程的可以概括为两各部分: 1. 利用inotify去watch cgroupPath,监控该目录下的变更,拿到该目录下的增删改查事件,也就知道container的变更,从而动态更新cache中的数据 2. 定时check,cache中的m.containers和主动去拿获取目前存在的container,对整体做一个diff,从而更新cache中的数据 #### 5.1 创建Container 其代码路径为 `/manager/manager.go -> CreateContainer` , 具体代码如下,详细解析可以看代码中的注释(代码有做一些删减)。 ```go // Create a container. func (m *manager) createContainer(containerName string, watchSource watcher.ContainerWatchSource) error { m.containersLock.Lock() defer m.containersLock.Unlock() return m.createContainerLocked(containerName, watchSource) } func (m *manager) createContainerLocked(containerName string, watchSource watcher.ContainerWatchSource) error { namespacedName := namespacedContainerName{ Name: containerName, } // 查看该container是否以及存在,如果已存在,则直接return if _, ok := m.containers[namespacedName]; ok { return nil } // for (factories) 判断是否能创建handler,如果可以则创建handler。 // 该handler实现了 ContainerHandler的interface,里面有GetSpec()、GetStats()、ListContainers等方法 handler, accept, err := container.NewContainerHandler(containerName, watchSource, m.inHostNamespace) if err != nil { return err } if !accept { // ignoring this container. klog.V(4).Infof("ignoring container %q", containerName) return nil } logUsage := *logCadvisorUsage && containerName == m.cadvisorContainer // 创建 containerData struct{}结构体的对象 cont, err := newContainerData(containerName, m.memoryCache, handler, logUsage, collectorManager, m.maxHousekeepingInterval, m.allowDynamicHousekeeping, clock.RealClock{}) if err != nil { return err } ...... // 将该container及其所有的aliases,放入到m.containers中 m.containers[namespacedName] = cont for _, alias := range cont.info.Aliases { m.containers[namespacedContainerName{ Namespace: cont.info.Namespace, Name: alias, }] = cont } klog.V(3).Infof("Added container: %q (aliases: %v, namespace: %q)", containerName, cont.info.Aliases, cont.info.Namespace) ...... // 构建event,找到到合适的m.eventHandler.watchers的*[]watchers,放入到*[]watchers的EventChannel.channel中 newEvent := &info.Event{ ContainerName: contRef.Name, Timestamp: contSpec.CreationTime, EventType: info.EventContainerCreation, } err = m.eventHandler.AddEvent(newEvent) if err != nil { return err } // Start the container's housekeeping. // 开启一个housekeeping的协程,定时调用updateStats(),即更新cont的数据 return cont.Start() } ``` 其中 m.eventHandler.AddEvent(newEvent) ,其逻辑是找到到合适的 m.eventHandler.watchers 的[]watchers,再将newEvent分别放入到[]watchers中,其中根据条件匹配到合适的[]watchers逻辑如下: - watcher.request.SndTime< newEvent.timestamp< watcher.request.EndTime - newEvent.EventType在watcher.request.EventType中有 - newEvent.ContainerName的前缀是watcher.request.ContainerName #### 5.2 检测子容器 其代码路径为 `/manager/manager.go -> detectSubcontainers()` , 主要是拿到containerName=“/”下的所有container 和 m.containers做diff,获取新增的容器added 和 已删除的容器removed - Added:对于added的容器调用m.CreateContainer()(具体可参考:5.1 创建container) - Removed:对于removed的容器调用m.destroyContainer(),将该容器及其aliases在cache中的记录全部删除掉 其diff具体逻辑如下图:  #### 5.3 Watch 其代码路径为 `/manager/manager.go -> watchForNewContainers(quit chan error)` 用的是 k8s.io/utils/inotify 中的watch功能,即watch一个目录,从而拿到该目录下的所有变更。所以这里利用的是inotify来watch cgroupPath,从而watch到container的变更 - 调用m.containerWatchers中watch的start(),watch cgroupPaths中的变化,获取该目录变更event,并将得到的event,放入条件匹配的watch的EventChannel.channel中 - 调用 detectSubContainers(“/”) (具体可参考:5.2 检测子容器) - go func{}处理以上的到的event,对于add事件调用 m.CreateContainer() ,对于delete事件调用 m.destroyContainer() ,收到quit信号,则退出协程 #### 5.4 全局更新 其代码路径为 `/manager/manager.go -> globalHousekeeping(quit chan error)` ,主要是定时调用m.detectSubcontainers("/"),具体逻辑可参考`检测子容器`。间隔时间:globalHousekeepingInterval ```go func (m *manager) globalHousekeeping(quit chan error) { // longHousekeeping := min(100ms,*globalHousekeepingInterval / 2) longHousekeeping := 100 * time.Millisecond if *globalHousekeepingInterval/2 < longHousekeeping { longHousekeeping = *globalHousekeepingInterval / 2 } // 定时,间隔时间 *globalHousekeepingInterval ticker := time.NewTicker(*globalHousekeepingInterval) for { select { case t := <-ticker.C: start := time.Now() // Check for new containers. err := m.detectSubcontainers("/") if err != nil { klog.Errorf("Failed to detect containers: %s", err) } // housekeeping 耗时超过longHousekeeping,则打印一条日志 duration := time.Since(start) if duration >= longHousekeeping { klog.V(3).Infof("Global Housekeeping(%d) took %s", t.Unix(), duration) } case <-quit: // Quit if asked to do so. quit <- nil klog.Infof("Exiting global housekeeping thread") return } } } ``` *附参考文章链接* *https://www.cnblogs.com/vinsent/p/15830271.html* *https://www.kancloud.cn/cruzen/docker/3128436*
标签:
Kubernetes
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/2985.html
上一篇
Kubernetes之CSI介绍
下一篇
Prometheus简介
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
35
其它
25
GO
48
NLP
8
标签云
栈
稀疏数组
HDFS
Spark SQL
Map
持有对象
Kibana
Git
微服务
算法
Jenkins
Thymeleaf
CentOS
MySQL
Golang
二叉树
设计模式
Java工具类
Livy
链表
FastDFS
SpringCloud
国产数据库改造
正则表达式
工具
Spark
散列
BurpSuite
Flume
Http
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞