暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

使用 /proc 分析进程的内存使用情况

新智锦绣 2025-04-07
94

点击蓝字关注我们


Lasso 已经提供了从 proc/meminfo 文件中获取的全局内存状态,但在某些情况下,我们需要分析特定进程(例如数据库后端)的内存使用和分配情况。为此,我们需要使用 proc 文件系统中的其他文件。本文意在说明可以使用哪些文件以及如何进行每个进程的内存使用分析。


使用的信息


/proc 文件系统包含有关进程的信息。这些信息存储在 proc/ 目录下,其中  是进程的 ID。在该目录下的文件中,我们可以使用 maps 文件。这是一个文本表示的进程内存分配情况。每一行表示连续的内存块,例如:

    00400000-00d68000 r-xp 00000000 fc:00 101577163                          /usr/edb/as16/bin/edb-postgres
    00f67000-00f89000 r--p 00967000 fc:00 101577163                          /usr/edb/as16/bin/edb-postgres
    00f89000-00fa6000 rw-p 00989000 fc:00 101577163                          /usr/edb/as16/bin/edb-postgres
    ...(snip)...
    01225000-012c9000 rw-p 00000000 00:00 0                                  [heap]
    ...(snip)...
    7f86327c6000-7f86327c7000 r--p 0002c000 fc:00 1337                       usr/lib64/ld-2.28.so
    7f86327c7000-7f86327c9000 rw-p 0002d000 fc:00 1337                       usr/lib64/ld-2.28.so
    7ffead5b5000-7ffead5d6000 rw-p 00000000 00:00 0                          [stack]
    7ffead5e6000-7ffead5e9000 r--p 00000000 00:00 0                          [vvar]
    7ffead5e9000-7ffead5eb000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

    每一行从左到右的字段描述如下:

    • 分配的地址范围

    • 权限

    • 偏移量

    • 设备

    • iNode

    • 路径

    没有路径的行通常是通过 mmap 分配的内存块。

    我们可以使用这些信息来分析每个进程的内存状态。


    Linux 和数据库中的内存分配


    在 Linux 中,我们通常使用 malloc() 和 free()(glibc 库中的函数)来分配和释放内存。在数据库引擎中,这些函数被包装为 palloc() 和 pfree(),它们提供了 MemoryContext 信息。当不再需要时,我们可以释放 MemoryContext 中分配的所有内存块,这样可以有效避免内存泄漏。

    另一方面,glibc 的 malloc() 以两种方式分配内存:

    • 扩展 heap 区域。当请求的大小小于某个阈值时会这样做。在 Ubuntu 22.04 的情况下,阈值为 128 KB。

    • 使用 mmap() 分配。当请求的大小等于或大于上述阈值时会这样做。

    free() 的行为取决于内存的分配方式:

    • 如果内存是分配给 heap 的,free() 不会实际将内存返回给操作系统。释放的内存块在 glibc 中被标记为free或available区域,可以用于未来的 malloc() 调用。这个区域会一直保留在 heap 中,直到进程终止。

    • 如果内存是通过 mmap() 分配的,free() 会释放该区域并将其返回给操作系统。

    因此,从操作系统角度来看的实际内存使用情况可能与应用程序的角度不同。


    收集 maps 文件


    当我们需要查看每个进程的内存使用情况时,需要分析 maps 文件。由于 proc 及其底层文件不是真实文件,我们无法直接将它们打包为 tarball。我们需要先将 maps 文件复制到普通文件中,然后再进行打包。

    以下是一个示例脚本,用于将 maps 文件作为普通文件收集:

      #!/bin/bash
      DEST=$1
      pwd=$(pwd)


      mkdir -p $DEST
      cd /proc
      for i in [0-9]*
      do
          cp $i/maps $pwd/$DEST/maps-$i
      done


      cd $pwd

      将上述内容保存为文件 backup_maps.sh。然后可以将 $PATH 目录归档为 tarball。


      将 maps 加载到表中


      单个进程的 maps 文件的总行数可能达到 300,000 行或更多。此外,手动分析如此多的行是不切实际的。以下是将这些行加载到数据库表中进行分析的方法。

      表结构可以如下:

        CREATE TABLE maps
        (
            test_label  text,   /* 测试用例的标签 */
            hostname    text,   /* 主机名 */
            pid         int,    /* 进程 ID */
            chunk_size  bigint/* 从地址范围计算的内存块大小 */
            start_addr  text,   /* 内存块的起始地址 */
            end_addr    text,   /* 内存块的结束地址 */
            perms       text,   /* 权限 */
            offset_val  text,   /* 偏移量 */
            dev         text,   /* 设备 */
            inode       text,   /* iNode */
            pathname    text    /* 模块的路径。当通过 mmap() 分配时没有路径。 */
                                /* 堆和栈分别表示为 [Heap] 和 [Stack] */
        );

        前三列定议不在 maps 文件中。我们需要从外部提供这些值。

        对于每个 maps 文件,可以使用以下 awk 脚本将内容转换为 SQL 语句,以便将其导入到 maps 数据库表中:

          #!/usr/bin/gawk
          # 调用顺序:
          #
          # gawk -f maps2table.awk test_label=xxx host_name=yyy pid=ppp <infile>
          #
          # 此脚本读取 proc/*/maps 文件,并将其转换为 SQL 语句,以导入到 "maps" 数据库表中。
          #
          BEGIN {
                  db_conn = "PGPASSWORD='xxx' psql -h myhost -U enterprisedb -d edb -c \"BEGIN;\"";
                  system(db_conn);
          }


          {
                  chunk_region = $1;
                  perms = $2;
                  offset = $3;
                  dev = $4;
                  inode = $5;
                  if (NF == 6)
                          pathname = "'" $6 "'";
                  else
                          pathname = "NULL";
                  split(chunk_region, aa, "-");
                  start_addr = aa[1];
                  end_addr = aa[2];
                  chunk_size = strtonum("0x" end_addr) - strtonum("0x" start_addr);


                  cmd= sprintf("PGPASSWORD='enterprisedb' psql -h myhost -U enterprisedb -d edb -c \"INSERT INTO maps VALUES ('%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %s);\"",
                          test_label, host_name, pid, chunk_size, start_addr, end_addr, perms, offset, dev, inode, pathname) ;
                  system(cmd);
          }
          END {
                  system("PGPASSWORD='xxx' psql -h myhost -U enterprisedb -d edb -c \"COMMIT;\"");
          }

          将上述内容保存为文件 maps2table.awk。

          然后可以使用以下脚本将 proc 中的所有 maps 文件加载到表中:

            #!/bin/bash
            id=$1
            host=$2
            for i in maps-*
            do
               gawk -v test_label=$id -v host_name=$host -v pid=${i##maps-} -f "./maps2table.awk" $i
            done

            将上述内容保存为文件 maps2table_all.awk。

            这里假设我们已经使用上述 backup_maps.sh 脚本复制了 maps 文件。每个 maps 文件的名称已更改为 maps-,其中  是该 maps 文件所属的进程 ID。


            加载到表中


            使用由 maps2table.awk 和 maps2table_all.awk 生成的 SQL 命令,可以将 maps 信息加载到数据库表中,如下所示:

              ./maps2table_all.awk  enterprise-db-1 myhost  


              分析数据


              一旦数据加载到表中,我们可以通过各种 SQL 语句以多种方式分析进程的内存使用情况。

              以下是一个示例,用于获取特定测量和服务器上运行 EPAS 的每个进程的 heap 和 stack 内存使用情况,并按分配的内存量排序:

                SELECT
                    test_label,
                    hostname,
                    pid,
                    lpad(to_char(sum(chunk_size), 'FM999,999,999,999'), 15AS total_heap_and_stack
                FROM
                   public.maps
                where
                    test_label = 'edb-1' and
                    pid IN
                    (
                        SELECT pid
                        FROM public.maps
                        WHERE
                            test_label = 'edb-1' 
                    ) AND
                    pathname = '[stack]' OR pathname = '[heap]'
                GROUP BY
                    test_label,
                    hostname,
                    pid
                   
                ORDER BY
                    sum(chunk_size) DESC,
                    test_label,
                    hostname,
                    pid

                结果如下:

                              test_label              | hostname |   pid   | total_heap_and_stack
                  -------------------------------------+----------+---------+----------------------
                   edb-1 |  myhost  | 1466247 |   7,298,633,728
                   edb-1 |  myhost  | 1466122 |   7,297,798,144
                   edb-1 |  myhost  | 1465744 |   7,297,544,192
                   edb-1 |  myhost  | 1463925 |   2,017,427,456
                   edb-1 |  myhost  | 1463923 |     880,758,784
                   edb-1 |  myhost  | 1463932 |     792,735,744
                   edb-1 |  myhost  | 1465797 |      57,286,656
                   edb-1 |  myhost  | 1464544 |      57,196,544
                   edb-1 |  myhost  | 1465036 |      56,877,056
                   edb-1 |  myhost  | 1465908 |      55,054,336
                   edb-1 |  myhost  | 1465041 |      54,763,520
                   edb-1 |  myhost  | 1464496 |      54,697,984
                   edb-1 |  myhost  | 1465827 |      54,673,408
                  ...(snip)...
                   edb-1 |  myhost  | 1463948 |       1,691,648
                   edb-1 |  myhost  | 1463947 |       1,650,688
                   edb-1 |  myhost  | 1463949 |       1,650,688
                   edb-1 |  myhost  | 1463917 |       1,462,272
                   edb-1 |  myhost  | 1463920 |       1,413,120
                   edb-1 |  myhost  | 1463921 |       1,413,120
                   edb-1 |  myhost  | 1463919 |       1,114,112
                   edb-1 |  myhost  |  225630 |         847,872
                   edb-1 |  myhost  | 1463916 |         847,872
                   edb-1 |  myhost  | 1463918 |         847,872
                   edb-1 |  myhost  |  225633 |         712,704
                  (1521 rows)


                  将结果与 pg_stat_activity 结合


                  我们可以将上述信息与实际的后端进程结合使用 Lasso。Lasso tarball 包含 postgresql/running_activity.out 文件,这是 pg_stat_activity 的内容,我们可以通过 PID 将其与内存消耗列表结合。


                  总结


                  通过使用 /proc//maps 文件,我们可以分析数据库后端内存使用的各个方面。由于典型生产数据库的数据量很大,因此将这些文件的内容存储在数据库表中以便进行各种分析是非常方便的。

                  结果可以与 Lasso 输出信息结合,以便进行进一步分析。


                  关于公司

                  感谢您关注新智锦绣科技(北京)有限公司!作为 Elastic 的 Elite 合作伙伴及 EnterpriseDB 在国内的唯一代理和服务合作伙伴,我们始终致力于技术创新和优质服务,帮助企业客户实现数据平台的高效构建与智能化管理。无论您是关注 Elastic 生态系统,还是需要 EnterpriseDB 的支持,我们都将为您提供专业的技术支持和量身定制的解决方案。


                  欢迎关注我们,获取更多技术资讯和数字化转型方案,共创美好未来!

                  Elastic 微信群

                  EDB 微信群


                  发现“分享”“赞”了吗,戳我看看吧


                  文章转载自新智锦绣,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论