Cgroups Memory子系统数据不正确的问题

工作中,需要对单个MySQL进程进行限制,并且对MySQL进程消耗的IO,CPU Time,Memory等进行统计,首先想到的就是使用Cgroups对进程进行限制,同时,Cgroups提供的一些接口,也可以非常方便地读取到进程消耗的IO,CPU Time, Memory等数据。

但是在实现自动化之后(使用了自己编写的一套管理程序来对MySQL进程进行管理),却发现了一个非常奇怪的问题:从Cgroups中读取的进程Memory消耗完全是一个不正确的值,使用ps命令查看到的MySQL进程内存大概是2G左右,可是通过读取memory子系统下memory.usage_in_bytes接口获得的数据大概只有几十KB,基本上是一个完全不相干的数值,这个就非常的奇怪了,虽然在官方的文档

For efficiency, as other kernel components, memory cgroup uses some optimization
to avoid unnecessary cacheline false sharing. usage_in_bytes is affected by the
method and doesn’t show ‘exact’ value of memory (and swap) usage, it’s a fuzz
value for efficient access. (Of course, when necessary, it’s synchronized.)
If you want to know more exact memory usage, you should use RSS+CACHE(+SWAP)
value in memory.stat(see 5.2).

也说明了这个数值不一定精确,而且是异步更新的,但是无论如何,差距如此之大肯定是有问题的。并且,在 memory.stat接口中获取的数据,也是不正确的数值。

这个问题还是很困扰的,如果无法找到原因,那这种统计方法就失效了,是个很大的问题。

于是开始考虑是不是使用方法的问题,由于我们一开始的逻辑,是等MySQL启动完成之后,读取MySQL的Pid文件获得进程的PID,再将这个PID填入到memory子系统的tasks接口中,很自然的,就会想到是不是只有在填入PID之后,Cgroups才会开始记录程序消耗的内存数据。

于是就做个实验验证一下:测试的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char* a = (char*)malloc(100 * 1024 * 1024);
    memset(a, 0, 100 * 1024 * 1024);
    printf("100M\n");
    getchar();

    char* b = (char*)malloc(100 * 1024 * 1024);
    memset(b, 0, 100 * 1024 * 1024);
    printf("200M\n");
    getchar();

    char* c = (char*)malloc(100 * 1024 * 1024);
    memset(c, 0, 100 * 1024 * 1024);
    printf("300M\n");
    getchar();
    
    char* d = (char*)malloc(100 * 1024 * 1024);
    memset(d, 0, 100 * 1024 * 1024);
    printf("400M\n");
    getchar();
    
    free(d);
    printf("300M\n");
    getchar();

    free(c);
    printf("200M\n");
    getchar();

    free(b);
    printf("100M\n");
    getchar();

    free(a);
    printf("0M\n");
    getchar();

    return 0;
}

程序比较简单,一步一步申请100M内存,直到400M,再一步步释放申请的内存。
编译运行,并将进程的PID加入到Cgroups中,如果Cgroups会统计程序开始申请的内存的话,那么memory.usage_in_bytes的数据应该会从100M递增到400M,然后再递减到0。

但是事实上,因为程序刚开始就会申请100M的内存,所以memory.usage_in_bytes的数据会从0M递增到300M,再递减到0。因为刚开始申请的那100M内存不会被Cgroups所记录。

最终,是通过实验的方式证明了Cgroups的一个猜想,下面还是要对Cgroups的代码进行一下分析,从实现上来证实这个假设。

那么怎么解决这个问题呢?幸好Cgroups会自动记录父进程的子进程的资源申请和释放的情况,所以这件事就好办了,利用一个shell脚本启动MySQL,并在shell脚本的一开始就获取脚本执行的PID,并把PID填入Cgroups的接口中,这样,由shell启动的MySQL会自动的在Cgroups的管理之下,那么对于资源的使用就是准确的数值了。