浅谈容器性能监控

写在前面

最近在研究docker集群(kubernetes)的监控,为了彻底弄清楚,简单看了一点源码。这里分享一下我学到的东西。

docker api: stats

首先是docker的api,stats的具体使用场景如:

http://$dockerip:2375/containers/$containerid/stats

可以获取docker机器上某一个容器的状态,该请求的response会持续的写响应报文回来(每秒一次)

http://$dockerip:2375/containers/$containerid/stats?stream=false

参数stream默认是true,设为false后,response只写一次。

docker中该api对应的操作,就相当于docker stats $CID 这条命令,它调用到了ContainerStats()方法(位于docker项目目录的/daemon/stats.go 第19行),函数中启动了一个收集器:

daemon.SubscribeToContainerStats(name)
复制代码

收集器定时写数据到update变量中。 然后启动一个协程读数据,获取数据的途径包括:

update := v.(*execdriver.ResourceStats)

nwStats, err := daemon.getNetworkStats(name)

可以看到网络状态是另外读的,而cpu,内存状态在哪读呢?我们一步步跳转,看到在这里: /daemon/execdriver/driver_linux.go 112行:

func Stats(containerDir string, containerMemoryLimit int64, machineMemory int64) (*ResourceStats, error)

在这个函数里我们看得到,其实所谓的获取容器状态,就是读文件而已。我们举一个已经在docker运行的容器来说:

 cat /run/docker/execdriver/native/$containerID/state.json

这里面记录了一个cgroup_paths字段,他的值是一个路径,通过cstats, err := mgr.GetStats()程序才真正拿到了监控数据,如cpu的状态信息,存储在一个如下的路径中:

cd /sys/fs/cgroup/cpu,cpuacct/system.slice/docker-9b479ceaf3bef1caee2c37dfdc982a968144700a2c42991b238b938788a8a91f.scope

又比如内存的大致信息存在:

cat /sys/fs/cgroup/memory/system.slice/docker-9b479ceaf3bef1caee2c37dfdc982a968144700a2c42991b238b938788a8a91f.scope/memory.stat

具体里面放了什么数据,大家就自己看咯。

还剩下一个数据,也是我们讨论的重点:网络IO。

我们回到/daemon/stats.go: 看看函数getNetworkStats(name string): 每个docker容器创建后都会又docker创建一个网卡,网卡名以veth开头,后面是一串随机生成的十六进制码。 我们只要找到该网卡,然后,像上文的cpu,内存状态获取的方法一样,去找文件就行了。 然而这个网卡名似乎是存在内存中,至少我没有看到docker将这个网卡名存到别的地方。 代码中: nw, err := daemon.netController.NetworkByID(c.NetworkSettings.NetworkID) 可以看出获取了网卡(network),尔后有一个Statistics()方法,我们继续跟踪会发现,docker调用了一个组件包: github.com/docker/libcontainer 其中有statistics方法,并执行了一个cat的命令(该组件包的/sandbox/interface_linux.go 第174行): data, err := exec.Command("cat", netStatsFile).Output()

是的,又是读文件。 这次读的文件在:/sys/class/net/目录下, 我们进入该目录可以看到有许多子目录,其中就包括了docker启动容器时生成的网卡。

个人猜测:docker在内存中保存了容器到网卡的映射关系。通过这个映射关系可以找到网卡的统计数据(数据由内核生成,记录在机器的文件中) 我们可以看到如下的数据: clipboard.png

将这些数据统一起来,就是docker stats 这条命令干的事。

kubernetes如何调用上述的监控功能

kubernetes的监控采用了cAdvisor组件。因为kubernetes中记录了容器的信息(但是没有记录容器-网卡的映射关系),所以运行在节点上的cAdvisor不需要通过docker stats去获取容器的cpu和内存使用数据。 而网络IO数据呢?

我们知道k8s部署运行一个容器是会先生成一个pause容器。 是的,网络IO都记录在pause容器中。这里大家可以在自己的k8s环境中验证。

所以只要我们获取某容器对应的pause容器的containerID,我们就可以用如上的方式去抓到网络IO。

因为cAdvisor并不是为k8s专门设计的,不会特地在获取网络IO时去遍历查找容器对应的pause容器。所以当前的cAdvisor没有办法获取容器的网络IO。

所以如果在使用k8s集群,想要加入网络IO监控功能的时候,可以从k8s自带的cAdvisor入手。

cAdvisor

上面讲到cAdvisor,那么cAdvisor是如何获取网络的IO的呢? 首先,在k8s(1.0.6版本)的/pkg/kubelet/cadvisor/cadvisor_linux.go中51行,New(port int)方法,这是kubelet调用cAdvisor的入口。实例化一个cAdvisorClient,并执行他的Start()方法,我们可以进入cAdvisor项目中找到该方法(github.com/google/cadvisor/manager/manager.go 195行)。

我们看到有一句

err docker.Register(self, self.fsInfo)

先mark之。继续看:

glog.Infof("Starting recovery of all containers")
err = self.detectSubcontainers("/")

这里程序程序检查所有在运行的容器,同内存中记录的容器作比较,有新的容器就新建一个相关的处理机制:一个Container的结构(有减少的,就删除),然后执行cont.Start() (github.com/google/cadvisor/manager/manager.go 766行)

持续追踪我们就可以知道每秒它收集一次监控信息,收集方法即

stats, statsErr := c.handler.GetStats()

(/cadvisor/manager/container.go 401行) handler是一个接口。我们得知道当时是如何给他赋值的。这里不赘述了,我们直接找关键的方法:

func (self *dockerContainerHandler) GetStats() (*info.ContainerStats, error)

(cadvisor/container/docker/handler.go 第305行)

它又调用了:

func GetStats(cgroupManager cgroups.Manager, networkInterfaces []string) (*info.ContainerStats, error)

(cadvisor/container/libcontainer/helpers.go 第77行)

这里就是真正获取监控信息的地方,它引入了libcontainer包中的同名方法,

最终是导入了"github.com/docker/libcontainer/cgroups"这个第三方包中的方法。是不是很熟悉?对就是上文中提到的docker stats获取网络IO用到的包。我们到这个包里找到函数: (/libcontainer/cgroups/fs/apply_raw.go 第148行)

func (m *Manager) GetStats() (*cgroups.Stats, error) {
    m.mu.Lock()
    defer m.mu.Unlock()
    stats := cgroups.NewStats()
    for name, path := range m.Paths {
        //m.Paths中包括了cpu,memory,network等字段,根据这些字段找到相应的目录
        sys, ok := subsystems[name]
        if !ok || !cgroups.PathExists(path) {
            continue
        }
        //读目录里的文件获取监控信息
        if err := sys.GetStats(path, stats); err != nil {
            return nil, err
        }
    }

    return stats, nil
}

我们可以看到libcontainer/cgroups/fs目录下有cpu.go,memory.go等文件,每一个文件中都有一个集成了GetStats()的结构,我们可以获取到任何数据。

我们再回到cadvisor项目中cadvisor/container/libcontainer/helpers.go 第77行,

往下看:

// TODO(rjnagal): Use networking stats directly from libcontainer.
    stats.Network.Interfaces = make([]info.InterfaceStats, len(networkInterfaces))
    for i := range networkInterfaces {
        interfaceStats, err := sysinfo.GetNetworkStats(networkInterfaces[i])
        if err != nil {
            return stats, err
        }
        stats.Network.Interfaces[i] = interfaceStats
    }

这里官方还用了别的手段再去找network的数据,虽然不知道是处于什么理由,也不管这里是去找哪个系统文件看状态,但是这样依然是拿不到数据的。因为根本没有找到容器对应的网卡。这里找网卡的方法在cadvisor/container/docker/handler.go 第311行:

var networkInterfaces []string
    if len(config.Networks) > 0 {
        // ContainerStats only reports stat for one network device.
        // TODO(vmarmol): Handle multiple physical network devices.
        for _, n := range config.Networks {
            // Take the first non-loopback.
            if n.Type != "loopback" {
                networkInterfaces = []string{n.HostInterfaceName}
                break
            }
        }
    }
    stats, err := containerLibcontainer.GetStats(self.cgroupManager, networkInterfaces)

很显然这里是要改进的(在k8s的环境下)。

注:随着版本更新,新版本的cAdvisor采用github.com/opencontainers包。详见:github.com/opencontainers/runc/libcontainer/container_linux.go line 151: 另外,cadvisor获取网络监控数据的正确途径应该是: 监控pause容器,在其Pid对应的/proc/$pid/net/dev文件中即可得到容器内部网络监控数据

磁盘监控

cadvisor在主函数初始化时,通过一些init方法在内存中构建了机器上的文件系统的结构(有哪些磁盘、分别挂载目录是啥、maj和min号是啥)。随后,cadvisor会在监控机器的性能时,分为磁盘使用情况的监控和磁盘读写情况的监控。

磁盘使用情况的监控

cadvisor会执行syscall.Statfs方法,通过linux系统调用,获得磁盘的总大小、可用空间、inodes总数和使用数等信息

磁盘读写情况的监控

通过读取机器的/proc/diskstats文件内容,形如:

cat /proc/diskstats 
 254       0 vda 48560 0 994278 64304 2275295 70286 18962312 6814364 0 1205480 6877464
 254       1 vda1 48177 0 991034 64008 1865714 70286 18962304 6777592 0 1170880 6840740
 254      16 vdb 700 0 4955 284 0 0 0 0 0 284 284

通过这些数据计算出各个存储设备的读写次数、读写速度。(这里文件内容释义和计算方式可以参见www.udpwork.com/item/12931.…

暴露接口

当我们调用cadvisor的接口: /api/v2.1/machinestats 时,就能从返回的json结构中找到filesystem字段,包含了磁盘读写性能和磁盘使用情况的监控数据

缺陷

上文提到,cadvisor在初始化阶段就生成了机器文件系统的抽象结构,但是这个结构不会动态检查和更新,当机器上动态挂载了一个数据盘(比如使用ceph rbd做pod的pv),cadvisor不会感知,这个新挂的盘的监控数据也无法被感知。

目前社区暂时没有对这个功能进行优化。

Prometheus

这里写Prometheus其实只是想介绍一下我们上面提到的性能数据如何更有效地被采集和使用。

kubelet启动时,会监听在10250端口,并提供一个/metrics url, 其下还有 /metrics/cadvisor url ,分别提供kubelet自身工作流程的数据监控、以及cadvisor提供的性能监控信息。

这些信息以Prometheus Metrics 的格式返回给客户端。 并且可以通过相应的Grafana组件展示、通过Prometheus分析并提供报警。

参考:

你可能感兴趣的:(浅谈容器性能监控)