标签 Unix 下的文章

一种基于内存映射文件的系统运行数据提取方法

这是我无意中想到的一个方法,估计这个方法已经不是什么新鲜的东西了,很可能在类似的问题场景中早已经被使用了。不过这里还是要说说我的思维过程。

近期在学习一些Linux性能查看和分析方面的工具,比如top、iostat、vmstat以及sar等。在学习过程中我发现这些工具有个共同的特点,那就是她们采集的Linux运行数据都是从/proc下的文件中实时获取并计算而得出的。众所周知,/proc是Linux内核维护的一个虚拟文件系统,他允许用户在Linux运行时查看内核运行数据(用户可以像查看普通文件一样查看/proc下的目录和文件),甚至是运行时实时改变内核设置。Linux实现/proc的细节不是这里要关注的,吸引我的是Linux的这种提取运行数据的设计。这个设计将Linux运行数据的产生实现细节与第三方性能采集工具间的耦合最大化地解开,这样一来/proc就像是一种Linux的基础服务,为用户提供一种实时的运行数据信息。而用户侧的运行数据查看工具也可以根据用户的需求自由定制,因此有了top、iostat、vmstat、iotop、sar等关注点不同的工具。

好了,说完/proc后,再来说说我们的产品。用户长期以来一直在抱怨我们的产品监控和维护方面手段太过单一,产品就像是一个黑盒,没有提供一种自我运行观察的能力,让客户看不清阿看不清,用户无法实时获取当前某个节点上的业务运行状况,无法采集到这些业务运行的实时基础数据,这的确是我们长期以来的短板(以前这块受重视度也的确不足)。虽然这两年我们在改善运维手段方面的投入已经加大,并收到一些显著的效果,但方案都是集中的,且相对重量级的,不那么敏捷灵活 – 在单节点上依旧无法简单地获取该节点的运行数据。

结合/proc的设计以及我们所遇到的问题,我有了一个大胆的想法:是否可以给我们的业务系统也加上一种类似Linux /proc这样的可提供基础运行数据的服务能力呢?于是就有了下面的解决方法。

Linux /proc下面的数据文件是Linux Kernel维护的,并允许用户层的进程实时查看和配置数据。而对于我们的产品而言,提供基础数据的产品实例与提取基础数据的第三方程序是两个独立的用户level的进程,显然我们需要找到一种让这两个进程实时通信、低耦合的且性能代价极低的方法。

我首先想到的是文件,这似乎和/proc的方式一样。你查看一下sysstat源码会发现,像iostat、sar等工具都是用fopen以"r"方式打开/proc/下的各种stat文件,匹配和读取指标项后再统计的。但在User层,两个无亲缘关系进程共同操作一个文件 – 一个读,一个写,the file position indicator是很难控制的,可能涉及文件锁(flock/fcntl),还要考虑使用的库函数是否是带缓冲的(fread/fgets都是带缓冲 的,不能用),写端需要及时fsync/fflush。总而言之,这么做是甚为自讨没趣的,会给两个程序的实现都带来很大的复杂性以及各种“坑”的。

那用named fifo如何呢?一但用named fifo,这两个进程就会产生启动依赖,如果一端没有启动,另一端会一直阻塞;而且通过fifo传递多种业务数据还可能存在打包和解包的过程,实现起来复杂的很。这显然是耦合十分严重的糟糕方案。

两个进程既要有共同的识别目标,就像/proc/cpuinfo这样的已知路径,一个进程还要能及时地得到另外一个进程运行时的数据,我们不妨尝试一下内存文件映射这个方案:运行数据提供的进程映射一个已知目标文件,比如perf/xxstat,然后在映射后的地址上创建和更新指标数据。比如我们建立一个整型数组,数组的每个元素都代表一种运行指标;而运行数据提取进程同样映射该文件,并在映射后获得数组中的各个元素值。下面是一个示例程序:

/* producer */
int
main()
{
    FILE *fp = NULL;

    errno = 0;
    fp = fopen(STAT_FILE, "w+");
    if (fp == NULL) {
        printf("can not create stat file , err = %d\n", errno);
        return -1;
    }

    errno = 0;
    long size = sysconf(_SC_PAGESIZE);
    if (ftruncate(fileno(fp), size) != 0) {
        printf("can not set stat file size, err = %d\n", errno);
        fclose(fp);
        return -1;
    }

    errno = 0;
    char *p = NULL;
    p = mmap(NULL, size, PROT_WRITE|PROT_READ, MAP_SHARED, fileno(fp), 0);
    if (p == MAP_FAILED) {
        printf("can not mmap file, error = %d\n", errno);
        fclose(fp);
        return -1;
    }

    errno = 0;
    if (fclose(fp) != 0) {
        printf("can not close file, error = %d\n", errno);
        return -1;
    }

    /* round up to 8 */
    while((int)p % 8 != 0) {
        p++;
    }

    long long *q = (long long*)p;
    q[0] = 1;
    q[1] = 1000;
    q[2] = 10000;
    q[3] = 100000;

    while(1) {
        q[0] += 1;
        q[1] += 10;
        q[2] += 100;
        q[3] += 1000;
        usleep(200);
    }

    return 0;
}

该producer程序首先尝试以"w+"方式打开xxstat文件,并设置文件的大小,然后调用mmap做内存文件映射,理论上来说mmap成功时返回的地址一定是按该平台下最严格内存系数对齐的地址,但这里为了安全起见,又做了一次内存地址的圆整。producer以映射的地址为首地址,建立了一个包含四个元素的、每个元素大小为8字节的整型数组,其中每个元素模拟一个运行指标。在while(1)循环中,producer模拟更新这四个指标数据。

下面是提取producer运行数据的例子程序,其映射过程与producer类似,这里就不贴出完整代码了,完整代码可在这里下载。

/* reader.c */

int
main()
{
    FILE *fp = NULL;
    … …

    char *p = NULL;
    p = mmap(NULL, size, PROT_READ,
             MAP_SHARED, fileno(fp), 0);
    if (p == MAP_FAILED) {
        printf("can not mmap file, error = %d\n", errno);
        fclose(fp);
        return -1;
    }

    … …

    long long *q = (long long*)p;

    while(1) {
        printf("%lld\t\t%lld\t\t%lld\t\t%lld\n", q[0], q[1], q[2], q[3]);
        sleep(1);
    }

    return 0;
}

在producer执行一段时间后,我们可以用reader去提取producer的实时运行数据了。

$ reader
2583        26820        268200        2682000
5793        58920        589200        5892000
9142        92410        924100        9241000
12431        125300        1253000        12530000
15586        156850        1568500        15685000
… …

需要注意的是两个进程映射的虽然是同一个文件,但各自进程空间映射的地址是不同的。如果在指标里存储地址数据,那另外一个进程在访问该地址时必然会出现问题。

在这个方案中,由于两个进程是读写同一块内存(虽然在各自进程空间的地址是不同的),因此数据是实时的。但由于两个进程间并没有任何同步机制,可能会产生误差,就好比一个进程中的两个线程对进程中某块地址空间一读一写这种情况一样。不过对于我们这种场景,这个问题是一般是可以被容忍和接受的,毕竟我们通过运行数据只是想了解一种运行趋势而已。如果producer中存在多个有亲缘关系的子进程或多线程要同时更新基础运行数据,那势必是要用锁或其他原子操作做数据操作的同步的。另外我们用的是内存映射具名的文件,OS会定期将数据刷到磁盘上,不过这个消耗对于小文件来说,对整体性能影响可忽略不计。

一旦业务系统具备了提供基础运行数据的能力,我们就可以根据我们的需求按照数据的格式打造我们所需要的各类数据提取和分析工具了。如果需要长期记录业务系统的运行情况,我们也可以实现类似sar这样的工具,以在后台定期对系统的运行数据进行记录,并提供历史查询等相关功能。

这种基于内存映射文件的方法还有一个好处,那就是我们可以用任何支持mmap调用的编程语言来实现数据提取工具,而不一定非得用C/C++这种原生适配Linux API的语言。

如果你觉得这种方案可行,那后续的重点就是基础运行数据的设计问题了。罗马不是一天建成的,/proc下的基础数据也不是一天就设计到位的。在基础数据设计这方面也是需要有很多考虑的,比如是文本还是二进制,用什么类型数据,还可能需要考虑一些数据对齐问题等。当然这就不是本文的重点了,就不细说了。

玩转top

相信很多人和我一样,top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了:敲入top命令,看看哪些进程占用 CPU较多,然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。

以前在监控工具使用方面总是浅尝辙止,并未做过多深入研究。近来愈来愈觉得有必要针对几种常用工具好好学习一下了。而top便首当其冲。top是一款 以查看进程(task)信息为中心的Linux系统性能监控工具,通过top我们可以查看到进程相关的cpu和内存占用相关的实时采样信息,因此 top尤其适合用于持续跟踪分析某些进程对系统cpu和内存的占用情况以及对系统负荷的影响。

入门

top的入门使用极其简单,就像前面所说的简单地的输入"top",我们就能看到top的输出了。

top – 06:35:47 up 7 min,  3 users,  load average: 1.00, 1.18, 0.67
Tasks: 189 total,   2 running, 186 sleeping,   0 stopped,   1 zombie
Cpu(s): 30.5%us,  7.6%sy,  0.0%ni, 60.5%id,  1.5%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   1534164k total,  1423392k used,   110772k free,    67328k buffers
Swap:   999420k total,      144k used,   999276k free,   576924k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                      
 1954 tonybai   20   0  316m  55m  26m S   26  3.7   0:36.53 compiz                                       
 2308 tonybai   20   0  499m  84m  39m S   13  5.6   1:07.63 chrome
… …

top的输出大致分为上下两个部分,上半部分输出到是系统的总体负荷信息,下半部分则是分进程列出进程的各种属性信息。

总体负荷信息由五行组成:

第一行:top – 06:35:47 up 7 min,  3 users,  load average: 1.00, 1.18, 0.67。
这行的输出与uptime命令是一样一样的,不信你可以单独执行一下uptime命令。我怀疑top就是直接调用uptime或使用uptime部分代码 得到的,毕竟它们都是procps(procps is the package that has a bunch of small useful utilities that give information about processes using the /proc filesystem.)工具集合的一员。这行输出了当前时间( 06:35:47)、自系统启动以来的累计时间(7 min),当前系统用户数(3 users),1分钟,5分钟以及15分钟的平均负荷( load average: 1.00, 1.18, 0.67)。

第二行:Tasks: 189 total,   2 running, 186 sleeping,   0 stopped,   1 zombie。
系统的进程信息汇总,包括总数以及处于各种状态的进程数量。

第三行:Cpu(s): 30.5%us,  7.6%sy,  0.0%ni, 60.5%id,  1.5%wa,  0.0%hi,  0.0%si,  0.0%st。
系统的CPU信息汇总,包括us(CPU用于运行用户空间进程的时间所占比例,不包括renice的用户进程)、sy(CPU用于运行内核进程的时间所占 比例)、ni(CPU用于运行用户空间被renice的进程的时间所占比例)、id(CPU空闲时间所占比例)、wa(CPU等待I/O完成时间所占用的 比例)、hi(处理硬件中断时间所占比例)、si(处理软中断时间所占比例)、st(虚拟机管理程序为其他task而从本虚拟机'偷取'的CPU时间所占 比例)。

第四行和第五行:
Mem:   1534164k total,  1423392k used,   110772k free,    67328k buffers
Swap:   999420k total,      144k used,   999276k free,   576924k cached

系统的内存以及交换区信息汇总,包括内存总量(mem total)、已使用内存(mem used)、空闲内存(mem free)以及交换区总量(swap total)、交换区使用量(swap used)、交换区空闲(swap free)。这里还有两个值buffers和cache,它们是内核使用的内存缓存,均是用于减少磁盘读取,提升系统性能的。buffers代表有多少内 存用于缓存磁盘数据块,目的是减少写磁盘次数;cache用于缓存从磁盘文件读取的数据,以减少读磁盘次数。

下半部分是进程属性信息展示区。默认情况输出的进程属性包括:
    PID(进程ID)
    USER(进程所有者的用户名)
    PR(进程的动态优先级)
    NI(Nice值,进程的base priority)
    VIRT (进程的虚拟内存用量,包括进程的二进制映像大小、数据区以及所有加载的共享库占用的size, = SWAP + RES)
    RES(进程使用的、未被换出的物理内存大小,= CODE + DATA)
    SHR(共享内存区域大小)
    S(进程状态)
    %CPU(上次刷新到现在运行该task的CPU时间所占百分比)
    %MEM(当前task所占用的内存百分比)
    TIME+  (自task启动后所使用的CPU时间累计)
    COMMAND (task对应的二进制程序名)

定制输出

top提供了强大的输出定制功能,无论是上半部分的系统整体负荷信息还是下半部分的进程属性信息展示都是可以根据使用的需求定制的。

整体负荷信息展示区的定制:
- 第一行展示/隐藏:通过点击键盘上的'l'键可以展示或隐藏第一行信息输出
- Task和CPU信息展示/隐藏:通过点击键盘上的't'键可以展示或隐藏Task和CPU行输出
- Mem和Swap信息展示/隐藏:通过点击键盘上的'm'键可以展示或隐藏Mem和Swap行输出

进程属性信息的显示定制:
默认情况下,我们可以看到top会显示进程的若干属性,包括PID、USER、PR、NI 、VIRT 、RES 、SHR、S、%CPU以及%MEM等。不过这些也仅仅是默认的而已,如果你不关住其中一些属性或关注其他一些属性,你完全可以自定义输出显示的进程属 性。点击键盘上的'f'键,top将为我们打开field选择页面:

Current Fields:  AEHIOQTWKNMbcdfgjplrsuvyzX  for window 1:Def
Toggle fields via field letter, type any other key to return

* A: PID        = Process Id                           0×00002000  PF_FREE_PAGES (2.5)
* E: USER       = User Name                            0×00008000  debug flag (2.5)
* H: PR         = Priority                             0×00024000  special threads (2.5)
… …

页面左侧列出了可选的所有进程属性。其中前面有*前缀的是当前已经选择的属性,比如PID。不过你可以通过点击PID对应的开关键'A'来取消对PID的 选择;同样你也可以点击未选择属性前面的开关键来选择对应的属性,比如敲击'p'来选择SWAP属性。定制完毕后回车回到top主页面,你就会看到你定制 后的结果了。

保存你的定制

如果你不想每次都在top启动后重新做定制操作,那就将你的定制保存到top的用户配置文件中。在定制后的top主页面上输入:'W',top会提示你:Wrote configuration to '/home/tonybai/.toprc,也就是说top会将你的定制保存在你的~/.toprc中。重启top看看,是否依旧是上次你定制后的结果呢^_^。

多视图

默认情况下top为我们打开了一个视图。不过top可不止支持一个视图。敲入'A'看看会发生什么?没错,你会看到上下分割的四副视图,另外在整个窗口的 左上角会出现反白的'1:Def',这是一个active视图的提示文字。反复输入'w',top会在各个视图间切换,左上角会在'1:Def'、 '2:Job'、'3:Mem'和'4:Usr'之间切换。‘1:Def'是默认视图,以CPU占用高低对task进行排序;'2:Job'这个视图看起 来比较陌生,里面展示的task多是些系统服务或内核线程;'3:Mem'视图则是以Mem占用高低对task进行排序;'4:Usr'视图则是按用户名 展示task。用'w'切换到某个视图后,可以输入'A'将该active视图放大为单视图铺满窗口。在多视图展示的情况下,还可以输入'-'来隐藏/展 示某种视图。另外这种多视图的配置也是可以保存在.toprc中的。

批处理模式

平时我们更多用的是在交互模式下运行的top,但交互模式下的数据无法记录下来,不便于事后分析,不过top的批处理模式可弥补这一不足。

执行top -b,即可让top以批处理模式运行。默认情况下top会不断重复执行,似乎批处理模式意义不大。不过我们可以限定批处理模式的运行间隔和运行次数,默认情况下top运行/更新间隔为3s,运行次数为无限制。我们可以通过一些命令行参数来设定这两个值,比如:

$> top -b  -d 1 -n 10

-d 用来设置更新间隔为1s;而-n 则设置批处理运行10次。

默认情况下top输出的task太多,我们可以通过指定相关进程或指定user来将关注面缩小,比如:

$> top -b -p 2500 -p 2501 -d 1 -n 10

这个命令只是会输出2500和2501这两个进程的相关信息。

$> top -b -u www-data -d 1 -n 10

这个命令只会输出www-data这个用户下的所有进程相关信息。

即便在批处理模式下,top依旧会输出整体负荷信息。这样一来对后续的数据后处理会带来些麻烦。一个好的方法是先定制top,再做批处理执行。比如先用 l,m,t把top的整体负荷信息都关闭掉,再定制好要关注的进程属性,保存到toprc中;之后再批处理运行top(可将输出结果重定向到某个数据文件 中),我们得到的数据就会比较规整,处理起来也十分方便了。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats