PFS内存统计信息的聚合与准确性问题(四)

发布时间 2023-08-02 17:48:15作者: 吃饭端住碗

内存统计信息的聚合

内存统计信息的聚合总共有5个维度,也分别对应以下5张表,分别是:

  • MEMORY_SUMMARY_BY_ACCOUNT_BY_EVENT_NAME
  • MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME
  • MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME
  • MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME
  • MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME

其包括内存分配、释放、聚合的涉及的入口函数及架构如下:

可以看到最基础的维度为thread,然后再往父bucket中进行聚合。

聚合的“主要”逻辑堆栈如下:

|PSI_THREAD_CALL(delete_current_thread)
|--pfs_delete_current_thread_vc
|----aggregate_thread
|------aggregate_thread_memory
|--------aggregate_all_memory_with_reassign
|----------memory_full_aggregate_with_reassign
|--------aggregate_all_memory
|----------memory_full_aggregate

在线程退出时,会调用PSI_THREAD_CALL(delete_current_thread)接口进行该连接相关的内存统计信息的聚合,并释放pfs thread对象。

其核心逻辑函数为aggregate_thread_memory,代码如下:

void aggregate_thread_memory(bool alive, PFS_thread *thread,
                             PFS_account *safe_account, PFS_user *safe_user,
                             PFS_host *safe_host) {
  if (thread->read_instr_class_memory_stats() == nullptr) {
    return;
  }

  if (likely(safe_account != nullptr)) {
    /*
      Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME
      to MEMORY_SUMMARY_BY_ACCOUNT_BY_EVENT_NAME.
    */
    aggregate_all_memory_with_reassign(
        alive, thread->write_instr_class_memory_stats(),
        safe_account->write_instr_class_memory_stats(),
        global_instr_class_memory_array);

    return;
  }

  if ((safe_user != nullptr) && (safe_host != nullptr)) {
    /*
      Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to:
      -  MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME
      -  MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME
      in parallel.
    */
    aggregate_all_memory_with_reassign(
        alive, thread->write_instr_class_memory_stats(),
        safe_user->write_instr_class_memory_stats(),
        safe_host->write_instr_class_memory_stats(),
        global_instr_class_memory_array);
    return;
  }

  if (safe_user != nullptr) {
    /*
      Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to:
      -  MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME
      -  MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME
      in parallel.
    */
    aggregate_all_memory_with_reassign(
        alive, thread->write_instr_class_memory_stats(),
        safe_user->write_instr_class_memory_stats(),
        global_instr_class_memory_array, global_instr_class_memory_array);
    return;
  }

  if (safe_host != nullptr) {
    /*
      Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME
      to MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME, directly.
    */
    aggregate_all_memory_with_reassign(
        alive, thread->write_instr_class_memory_stats(),
        safe_host->write_instr_class_memory_stats(),
        global_instr_class_memory_array);
    return;
  }
  /*
    Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME
    to MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME.
  */
  aggregate_all_memory(alive, thread->write_instr_class_memory_stats(),
                       global_instr_class_memory_array);
}

根据代码可知,thread维度最为基础维度,通常情况下用户线程的内存统计信息都会从thread维度汇聚到account维度;MySQL后台线程或插件等会被聚合到global维度。所以这里我们主要关注account和global维度的具体聚合逻辑。

account维度的聚合实现主要通过aggregate_all_memory_with_reassign(bool alive,

                                        PFS_memory_safe_stat *from_array,

                                        PFS_memory_shared_stat *to_array,

                                        PFS_memory_shared_stat *global_array)函数实现,其中alive总是为false,from_array为thread维度的instruments内存统计信息数组指针,to_array为account维度的instruments内存统计信息数据指针,global_array为global维度的instruments内存统计信息数组指针。具体实现代码如下:

void aggregate_all_memory_with_reassign(bool alive,
                                        PFS_memory_safe_stat *from_array,
                                        PFS_memory_shared_stat *to_array,
                                        PFS_memory_shared_stat *global_array) {
  PFS_memory_safe_stat *from;
  PFS_memory_safe_stat *from_last;
  PFS_memory_shared_stat *to;

  from = from_array;
  from_last = from_array + memory_class_max;
  to = to_array;

  if (alive) {
    for (; from < from_last; from++, to++) {
      memory_partial_aggregate(from, to);
    }
  } else {
    PFS_memory_shared_stat *global;
    global = global_array;
    for (; from < from_last; from++, to++, global++) {
      memory_full_aggregate_with_reassign(from, to, global);
      from->reset();
    }
  }
}

在for循环中依次迭代from_array中的instrument内存统计信息往父bucket中进行聚合,其聚合实现代码如下:

void memory_full_aggregate_with_reassign(const PFS_memory_safe_stat *from,
                                         PFS_memory_shared_stat *stat,
                                         PFS_memory_shared_stat *global) {
  if (!from->m_used) {
    return;
  }

  stat->m_used = true;

  size_t alloc_count = from->m_alloc_count;
  size_t free_count = from->m_free_count;
  size_t alloc_size = from->m_alloc_size;
  size_t free_size = from->m_free_size;

  size_t net;
  size_t capacity;

  if (likely(alloc_count <= free_count)) {  
    /* Nominal path */
    stat->m_alloc_count += alloc_count;
    stat->m_free_count += free_count;
    stat->m_alloc_count_capacity += from->m_alloc_count_capacity;
    stat->m_free_count_capacity += from->m_free_count_capacity;
  } else {      
    global->m_used = true;

    stat->m_alloc_count += free_count; /* base */
    stat->m_free_count += free_count;

    /* Net memory contributed affected to the global bucket directly. */
    net = alloc_count - free_count;
    global->m_alloc_count += net;

    stat->m_alloc_count_capacity += from->m_alloc_count_capacity;

    size_t free_count_capacity;
    free_count_capacity = from->m_free_count_capacity;
    capacity = std::min(free_count_capacity, net);
    free_count_capacity -= capacity;

    /*
      Corresponding low watermark split between the parent and global
      bucket.
    */
    stat->m_free_count_capacity += free_count_capacity;
    global->m_free_count_capacity += capacity;
  }

  if (likely(alloc_size <= free_size)) {
    /* Nominal path. */
    stat->m_alloc_size += alloc_size;
    stat->m_free_size += free_size;
    stat->m_alloc_size_capacity += from->m_alloc_size_capacity;
    stat->m_free_size_capacity += from->m_free_size_capacity;
  } else {
    /* Global net alloc. */
    global->m_used = true;

    stat->m_alloc_size += free_size; /* base */
    stat->m_free_size += free_size;

    net = alloc_size - free_size;
    global->m_alloc_size += net;

    stat->m_alloc_size_capacity += from->m_alloc_size_capacity;

    size_t free_size_capacity;
    free_size_capacity = from->m_free_size_capacity;
    capacity = std::min(free_size_capacity, net);
    free_size_capacity -= capacity;

    stat->m_free_size_capacity += free_size_capacity;
    global->m_free_size_capacity += capacity;
  }
}

通过代码可知,通常情况下是直接将thread维度的统计信息属性值与account维度的统计信息属性值进行相加来进行聚合,即(alloc_count <= free_count)。

但还有一种情况,即父线程调用子线程进行一些工作,通常有两种场景,比如1.event调度,调度线程调用worker线程进行工作 2. main线程接受用户连接进行工作。这两种情况会产生内存分配与释放在同一线程的不均衡,比如内存的分配是在X线程进行的,但是内存的释放却是在Y线程进行的。这里的情况就对应函数中的另一半逻辑,即(alloc_count > free_count),此时可以理解为一个用户连接分配了一些global类型的内存(内存claim),在用户连接退出时,会将global类型的这部分内存直接聚合到global级别。

通过以上的逻辑分析,我们可以得出一个结论:内存统计信息是从thread维度往父bucket中进行聚合,其中通常会聚合到account维度和global维度,而在进行account维度的聚合时,通常情况下就是将thread维度的统计信息属性值直接与account维度的统计信息属性值相加,如果存在内存claim的情况,则会将额外分配的global类型的内存聚合到global级别。

从thread维度直接聚合到global维度的主要实现函数为memory_full_aggregate,具体实现代码如下:

void memory_full_aggregate(const PFS_memory_safe_stat *from,
                           PFS_memory_shared_stat *stat) {
  if (!from->m_used) {
    return;
  }
  stat->m_used = true;

  stat->m_alloc_count += from->m_alloc_count;
  stat->m_free_count += from->m_free_count;
  stat->m_alloc_size += from->m_alloc_size;
  stat->m_free_size += from->m_free_size;

  stat->m_alloc_count_capacity += from->m_alloc_count_capacity;
  stat->m_free_count_capacity += from->m_free_count_capacity;
  stat->m_alloc_size_capacity += from->m_alloc_size_capacity;
  stat->m_free_size_capacity += from->m_free_size_capacity;
}

这一部分的代码相对比较简单,直接就是将thread维度的统计信息属性值与global维度的统计信息属性值相加即可。

统计信息与OS内存使用不一致的问题

现象1:统计信息大于OS使用量

当前实例使用内存为1.4G

Buffer Pool设置为1G

相关内存统计信息为1G

此时将Buffer Pool调整为2G

如果此时分配的内存都被使用了,那么在OS上看到的实例使用的内存应该至少是大于2G的,但此时实际使用量却是1.6G。

此时产生的现象就是在OS层面观测到的实例实际使用的内容量与MySQL内存统计信息里看到的不一致。

问题产生的原因主要是Linux内存主要是通过虚拟内存文件映射的方式来管理的,当MySQL向Linux申请内存时,分配的都是虚拟内存,并没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,会发生缺页中断,操作系统再进行物理内存的分配,然后建立虚拟内存和物理内存之间的映射关系。

现象2:内存统计信息出现负值

在上一章节关于内存统计信息聚合的介绍中,我们看到在memory_full_aggregate_with_reassign函数中,内存聚合有两个路径,其中一个是(likely(alloc_size <= free_size)),从这个条件中我们可以看到alloc_size是有可能会小于free_size的,在PS表中,CURRENT_NUMBER_OF_BYTES_USED的值是通过alloc_size-free-size计算出来的,所以CURRENT_NUMBER_OF_BYTES_USED的值就也可能会出现负值。

而出现负值显然是不合理的,此种情况就属于是Bug缺陷了。在MariaDB的官方jira中,也找到了相关类似的issue:

https://jira.mariadb.org/browse/MDEV-23936,不过看记录已经过去三年了,官方也没有给出相关解释或解决方案。

还有一点,由于sys.memory_global_total表是performance_schema.memory_summary_global_by_event_name的视图,而total_allocated是通过sum(CURRENT_NUMBER_OF_BYTES_USED)得到的,那么既然CURRENT_NUMBER_OF_BYTES_USED的值是有可能会出现负值,所有total_allocated这个总计值就也不可信了。还有一点,我们之前讲过,内存统计信息的聚合是有几个不同的维度的,并不是所有的统计信息最后都会被聚合到Global级别,所有将sys.memory_global_total的total_allocated值作为内存分配量的统计值是一点不准确的。

现象3:存在未被统计的内存操作

在MySQL相关代码中,明确定义了只有这些文件中的内存分配会被PFS监控,这里这列出了一部分的MySQL代码文件,所以一定存在未被统计的内存。

/** List of filenames that allocate memory and are instrumented via PFS. */
static constexpr const char *auto_event_names[] = {
    /* Keep this list alphabetically sorted. */
    "api0api",
    "api0misc",
    "btr0btr",
    "btr0bulk",
    "btr0cur",
    "btr0pcur",
    "btr0sea",
    "btr0types",
    "buf",
    "buf0buddy",
    "buf0buf",
    "buf0checksum",
    "buf0dblwr",
    "buf0dump",
    "buf0flu",
    "buf0lru",
    "buf0rea",
    "buf0stats",
    "buf0types",
    "checksum",
    "crc32",
    "create",
    "data0data",
    "data0type",
    "data0types",
    "db0err",
    "dict",
    "dict0boot",
    "dict0crea",
    "dict0dd",
    "dict0dict",
    "dict0load",
    "dict0mem",
    "dict0priv",
    "dict0sdi",
    "dict0stats",
    "dict0stats_bg",
    "dict0types",
    "dyn0buf",
    "dyn0types",
    "eval0eval",
    "eval0proc",
    "fil0fil",
    "fil0types",
    "file",
    "fsp0file",
    "fsp0fsp",
    "fsp0space",
    "fsp0sysspace",
    "fsp0types",
    "fts0ast",
    "fts0blex",
    "fts0config",
    "fts0fts",
    "fts0opt",
    "fts0pars",
    "fts0plugin",
    "fts0priv",
    "fts0que",
    "fts0sql",
    "fts0tlex",
    "fts0tokenize",
    "fts0types",
    "fts0vlc",
    "fut0fut",
    "fut0lst",
    "gis0geo",
    "gis0rtree",
    "gis0sea",
    "gis0type",
    "ha0ha",
    "ha0storage",
    "ha_innodb",
    "ha_innopart",
    "ha_prototypes",
    "handler0alter",
    "hash0hash",
    "i_s",
    "ib0mutex",
    "ibuf0ibuf",
    "ibuf0types",
    "lexyy",
    "lob0lob",
    "lock0iter",
    "lock0lock",
    "lock0prdt",
    "lock0priv",
    "lock0types",
    "lock0wait",
    "log0log",
    "log0recv",
    "log0write",
    "mach0data",
    "mem",
    "mem0mem",
    "memory",
    "mtr0log",
    "mtr0mtr",
    "mtr0types",
    "os0atomic",
    "os0event",
    "os0file",
    "os0numa",
    "os0once",
    "os0proc",
    "os0thread",
    "page",
    "page0cur",
    "page0page",
    "page0size",
    "page0types",
    "page0zip",
    "pars0grm",
    "pars0lex",
    "pars0opt",
    "pars0pars",
    "pars0sym",
    "pars0types",
    "que0que",
    "que0types",
    "read0read",
    "read0types",
    "rec",
    "rem0cmp",
    "rem0rec",
    "rem0types",
    "row0ext",
    "row0ftsort",
    "row0import",
    "row0ins",
    "row0log",
    "row0merge",
    "row0mysql",
    "row0purge",
    "row0quiesce",
    "row0row",
    "row0sel",
    "row0types",
    "row0uins",
    "row0umod",
    "row0undo",
    "row0upd",
    "row0vers",
    "sess0sess",
    "srv0conc",
    "srv0mon",
    "srv0srv",
    "srv0start",
    "srv0tmp",
    "sync0arr",
    "sync0debug",
    "sync0policy",
    "sync0sharded_rw",
    "sync0rw",
    "sync0sync",
    "sync0types",
    "trx0i_s",
    "trx0purge",
    "trx0rec",
    "trx0roll",
    "trx0rseg",
    "trx0sys",
    "trx0trx",
    "trx0types",
    "trx0undo",
    "trx0xa",
    "usr0sess",
    "usr0types",
    "ut",
    "ut0byte",
    "ut0counter",
    "ut0crc32",
    "ut0dbg",
    "ut0link_buf",
    "ut0list",
    "ut0lock_free_hash",
    "ut0lst",
    "ut0mem",
    "ut0mutex",
    "ut0new",
    "ut0pool",
    "ut0rbt",
    "ut0rnd",
    "ut0sort",
    "ut0stage",
    "ut0ut",
    "ut0vec",
    "ut0wqueue",
    "zipdecompress",
};

我们找一个未被统计的案例,可以看到,在下图的堆栈中,在内存跟踪接口函数pfs_memory_alloc_vc中,在尝试进行内存分配跟踪时,由于找不到相关的Performance Schema Key,则函数直接return,不再进行后续的内存分配跟踪。(通过堆栈可以看到,该内存分配相关动作为后台master线程在进行主循环):