Erlo

openGauss数据库源码解析系列文章——公共组件源码解析(下)

2021-05-26 10:30:06 发布   278 浏览  
页面报错/反馈
收藏 1 赞

上一篇openGauss数据库源码解析系列文章——公共组件源码解析(上)中,从系统表、数据库初始化、多线程架构及线程池技术四个方面对公共组件的源代码实现进行介绍。本篇接着从内存管理、多维监控、模拟信号机制展开介绍。

五、内存管理

数据库在运行过程中涉及许多对象,这些对象具有不同的生命周期,有些处理需要频繁分配内存。如一个SQL语句,在解析时需要对词法单元和语法单元分配内存,在执行过程中需要对执行状态分配内存。在事务结束时,如果不是prepare语句,那么SQL语句的执行计划内存和执行过程的状态内存都需要释放。如果是prepare语句,那么执行计划需要保存到缓冲池中,执行过程的状态内存释放即可。为了保证内存分配的高效和避免内存泄漏,openGauss设计开发了自己的内存管理,代码实现在“openGauss-serversrccommonbackendutilsmmgr”目录。

openGauss在内存管理上采用了上下文的概念,即具有同样生命周期或者属于同一个上下文语义的内存放到一个MemoryContext管理,MemoryContext的结构代码如下(结构成员参照注释):

typedef struct MemoryContextData* MemoryContext;
typedef struct MemoryContextData {
    NodeTag type;                  /* 上下文类别*/
    MemoryContextMethods* methods; /* 虛函数表*/
    MemoryContext parent;       /* 父上下文。顶级上下文为 NULL*/
    MemoryContext firstchild;      /* 子上下文的链表头*/
    MemoryContext prevchild;       /* 前向子上下文 */
    MemoryContext nextchild;       /* 后向子上下文 */
    char* name;                    /* 上下文名称,方便调试 */
    pthread_rwlock_t lock;         /*上下文共享时的并发控制锁 */
    bool is_shared;                /* 上下文是否在多个线程共享 */
    bool isReset;            /* isReset为true时,表示复位后没有内存空间用于分配*/
    int level;                     /* 上下文层次级别*/
    uint64 session_id;             /* 上下文属于的会话ID */
    ThreadId thread_id;            /* 上下文属于的线程ID */
} MemoryContextData;

虛函数表就是具体的内存管理操作函数指针,具体定义代码如下(函数功能参照注释):

typedef struct MemoryContextMethods {
/*在上下文中分配内存*/
    void* (*alloc)(MemoryContext context, Size align, Size size, const char* file, int line);
    /* 释放pointer 内存到上下文中*/
void (*free_p)(MemoryContext context, void* pointer);
/*在上下文中重新分配内存*/
void* (*realloc)(MemoryContext context, void* pointer, Size align, Size size, const char* file, int line);
    void (*init)(MemoryContext context);    /*上下文初始化*/
    void (*reset)(MemoryContext context);   /*上下文复位*/
    void (*delete_context)(MemoryContext context);   /*删除上下文 */
    Size (*get_chunk_space)(MemoryContext context, void* pointer); /*获取上下文块大小 */
    bool (*is_empty)(MemoryContext context);  /*上下文是否为空*/
    void (*stats)(MemoryContext context, int level); /*上下文信息统计*/
#ifdef MEMORY_CONTEXT_CHECKING
    void (*check)(MemoryContext context); /*上下文异常检查*/
#endif
} MemoryContextMethods;

这些回调函数指针初始化是在AllocSetContextSetMethods函数中调用AllocSetMethodDefinition函数完成的。

AllocSetMethodDefinition函数的实现代码如下:

template 
void AlignMemoryAllocator::AllocSetMethodDefinition(MemoryContextMethods* method)
{
    method->alloc = &AlignMemoryAllocator::AllocSetAlloc;
    method->free_p = &AlignMemoryAllocator::AllocSetFree;
    method->realloc = &AlignMemoryAllocator::AllocSetRealloc;
    method->init = &AlignMemoryAllocator::AllocSetInit;
    method->reset = &AlignMemoryAllocator::AllocSetReset;
    method->delete_context = &AlignMemoryAllocator::AllocSetDelete;
    method->get_chunk_space = &AlignMemoryAllocator::AllocSetGetChunkSpace;
    method->is_empty = &AlignMemoryAllocator::AllocSetIsEmpty;
    method->stats = &AlignMemoryAllocator::AllocSetStats;
#ifdef MEMORY_CONTEXT_CHECKING
    method->check = &AlignMemoryAllocator::AllocSetCheck;
#endif
}

可以看到,这些实际操作内存管理的函数为AlignMemoryAllocator类中的AllocSetAlloc函数、AllocSetFree函数、AllocSetRealloc函数、AllocSetInit函数、AllocSetReset函数、AllocSetDelete函数、AllocSetGetChunkSpace函数、AllocSetIsEmpty函数、AllocSetStats函数和AllocSetCheck函数。在这些处理函数中,涉及的结构体代码如下:

typedef AllocSetContext* AllocSet;
typedef struct AllocSetContext {
    MemoryContextData header; /*内存上下文,存储空间是在这个内存上下文中分配的 */
     AllocBlock blocks;  /* AllocSetContext所管理内存块的块链表头 */
    AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* 空闲块链表*/
    /*这个上下文的分配参数 */
    Size initBlockSize;   /* 初始块大小*/
    Size maxBlockSize;    /* 最大块大小 */
    Size nextBlockSize;   /* 下一个分配的块大小 */
    Size allocChunkLimit; /* 块大小上限*/
    AllocBlock keeper;    /* 在复位时,保存的块 */
    Size totalSpace;      /* 这个上下文分配的总空间 */
    Size freeSpace;       /* 这个上下文总的空闲空间 */
    Size maxSpaceSize;   /* 最大内存空间 */
    MemoryTrack track;   /* 跟踪内存分配信息 */
} AllocSetContext;
AllocBlock定义如下:
typedef struct AllocBlockData* AllocBlock;
typedef struct AllocBlockData {
    AllocSet aset;   /* 哪个AllocSetContext 拥有此块,AllocBlockData 归属AllocSetContext管理*/
    AllocBlock prev; /* 在块链表中的前向指针 */
    AllocBlock next; /* 在块链表中的后向指针 */
    char* freeptr;   /* 这个块空闲空间的起始地址 */
    char* endptr;    /* 这个块空间的结束地址*/
    Size allocSize;  /* 分配的大小*/
#ifdef MEMORY_CONTEXT_CHECKING
    uint64 magicNum; /* 魔鬼数字值,用于内存校验。当前代码固定填写为DADA */
#endif
} AllocBlockData;
typedef struct AllocChunkData* AllocChunk; /* AllocChunk 内存前面部分是一个AllocBlock结构*/
typedef struct AllocChunkData {
       void* aset; /* 拥有这个chunk的AllocSetContext,如果空闲,则为空闲列表链接*/
       Size size; /* chunk中的使用空间 */
#ifdef MEMORY_CONTEXT_CHECKING
       Size requested_size; /* 实际请求大小,在空闲块中时为0 */
       const char* file; /* palloc/palloc0调用时的文件名称 */
       int line;         /* palloc/palloc0 调用时的行号*/
       uint32 prenum;    /* 前向魔鬼数字*/
#endif
} AllocChunkData;

从前面的数据结构可以看出,核心数据结构为AllocSetContext,这个数据结构有3个成员“MemoryContextData header;”、“AllocBlock blocks;”和“AllocChunk freelist[ALLOCSET_NUM_FREELISTS];”。这3个成员把内存管理分为3个层次。

(1) MemoryContext管理上下文之间的父子关系,设置MemoryContext的内存管理函数。 (2) AllocBlock blocks把所有内存块通过双链表链接起来。 (3) 具体的内存单元chunk。内存单元chunk是从内存块AllocBlock内部分配的,内存块和内存单元chunk的转换关系为:“AllocChunk chunk = (AllocChunk)(((char*)block) + ALLOC_BLOCKHDRSZ);”和“AllocBlock block = (AllocBlock)(((char*)chunk) - ALLOC_BLOCKHDRSZ);”。 内存单元chunk经过转换得到最终的用户指针,内存单元chunk和用户指针的转换关系为:((AllocPointer)(((char*)(chk)) + ALLOC_CHUNKHDRSZ))和((AllocChunk)(((char*)(ptr)) - ALLOC_CHUNKHDRSZ))。数据结构的基本关系如图1所示。

在这里插入图片描述

图1 数据结构的基本关系

下面先看第1层MemoryContext的实现,主要实现在mcxt.cpp文件中,如表1所示。

表1 MemoryContext的实现函数

函数 功能介绍
ChooseRootContext 在线程池机制下,上下文有3个类别,即实例级别、会话级别、线程级别。这个函数根据tag类型和parent返回相应类别的根上下文
MemoryContextCreate 首先根据root是否为空确定是从父MemoryContext分配内存还是从操作系统调用malloc分配内存,然后对分配的MemoryContext进行初始化,如果存在父MemoryContext,则挂到父MemoryContext上
MemoryContextDelete 先删除这个MemoryContext的子节点,把这个MemoryContext的父节点置为空,回调AllocSetDelete方法释放分配的对象,最后释放上下文本身
MemoryContextIsEmpty 先看看当前上下文是否有子节点,然后回调is_empty检查当前上下文是否为空
MemoryContextReset 先看看当前上下文是否有子节点,如果有子节点,则遍历子节点,递归调用MemoryContextReset进行复位。最后回调reset复位当前上下文
MemoryContextSetParent 如果上下文有父节点,则从父上下文解除当前上下文。然后把上下文挂到新的父上下文
GetMemoryChunkSpace 当前指针pointer前移STANDARDCHUNKHEADERSIZE找到标准StandardChunkHeader位置,然后根据块属于上下文的回调AllocSetGetChunkSpace获取块空间大小
MemoryContextStats 先回调AllocSetStats统计当前上下文信息,然后遍历子节点,递归调用MemoryContextStatsInternal统计上下文信息
MemoryContextAllocDebug 检查分配内存大小(size)是否小于MaxAllocSize,回调AllocSetAlloc分配内存
pfree 根据当前指针pointer偏移STANDARDCHUNKHEADERSIZE找到标准头,根据头部StandardChunkHeader找到归属的上下文,回调AllocSetFree释放内存

再看第2层AllocSet的实现,主要实现在aset.cpp文件中,如表2所示。

表2 AllocSet的实现函数 |函数 | 功能介绍 | |--|--| |AllocSetFreeIndex | 根据请求的size(内存大小)计算应该在那个空闲块链表freechunk中分配内存 | |set_sentinel | 设置哨兵0x7E,用于踩内存检查 | | sentinel_ok |内存检查是否正常 | | MemoryContextControlSet |根据白名单设置上下文的maxSpaceSize限制大小 | |AllocSetContextCreate | 根据contextType类型,调用不同的AllocSetContextCreate分配器分配MemoryContext| |AllocSetMethodDefinition | 设置MemoryContext回调处理方法 | |AllocSetContextSetMethods | 设置不同上下文类型的分配器 | | AllocSetContextCreate | 创建一个具体的MemoryContext。根据类型,确定内存保护函数。调用MemoryContextCreate函数创建一个AllocSetContext,设置maxSpaceSize大小,设置回调方法,设置初始块大小、下一个块大小、最大块大小。根据分配的最大块大小设置allocChunkLimit。如果上下文最值超过了“ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ”,则分配一个AllocBlock;设置AllocBlock的上下文,空闲起始地址(freeptr)为跳过头部管理占用空间后剩余的空间,末尾地址(endptr)为块结束地址,分配大小(allocSize)为分配的块大小,魔鬼数字(magicNum)为0xDADADADADADADADA,context的总空间(totalSpace)加上这次分配的块大小,上下文的空闲空间(freeSpace)加上这个块的空闲空间,块的前向和后向指针为空,AllocSetContext的第一个块(blocks)指向这个块,保留块(keeper)指向这个块。返回AllocSetContext| |AllocSetInit | 特定AllocSetContext初始化函数,当前为空,没有使用| |AllocSetReset | 根据上下文类型选择保护函数,把空闲链表(freelist)置空,遍历内存块,如果是保留块,则对这块内存重新初始化。如果不是,则释放掉。根据是否有保留块重新初始化上下文| | AllocSetDelete | 如果上下文没有内存块,则直接返回。根据上下文类型选择保护函数,获取内存块首地址,把保留块和内存块头置空。遍历所有内存块,释放掉。把上下文的空闲空间(freeSpace)和总空间(totalSpace)置为0 | |AllocSetAlloc | 根据上下文类型选择保护函数,如果申请内存大小超过了内存块大小上限,则直接调用OS(操作系统)接口分配一个内存块,初始化这个内存块。把内存块转换为一个内存单元(chunk),对这个内存单元chunk进行初始化。把这个内存块挂到上下文上,返回内存单元chunk指针。如果申请内存大小没有超过内存块大小上限,根据内存大小映射的空闲链表(freelist),看看相应空闲链表中是否有相应大小的内存单元(chunk),如果有空闲的内存单元(chunk),则分配一个内存单元(chunk)返回。如果空闲链表(freelist)没有空闲的chunk,看看当前的块(blocks)是否有足够的内存,如果没有足够的内存,则根据块剩余的内存大小放到相应的空闲链表(freelist)中。分配一个新的块,对这个新的块进行初始化,把块挂到上下文上,从这个块上分配一个内存单元(chunk)返回。如果当前的块有足够的内存,则从当前块上分配一个内存单元(chunk)返回 | | AllocSetFree | 根据类型,确定内存保护函数。如果释放内存单元大小超过了内存块大小上限,把这个内存单元转换为AllocBlock,把内存块从上下文中解除,把内存块变量置空,释放内存块、如果内存单元大小小于内存块上限,则根据内存单元大小映射的空闲链表(freelist),把释放的内存单元挂到上下文的空闲链表(freelist)上 | | AllocSetRealloc | 根据类型,确定内存保护函数。如果原来的内存单元大小能够满足请求的大小,则重新赋值新的内存检查信息后直接返回当前的内存单元、如果旧的内存单元大小超过了内存块的上限,则调用OS重分配内存接口重新分配一个内存块,对新的内存块初始化,把新的内存块转换为内存单元AllocChunk返回、如果旧的内存单元大小没有超过内存块的上限,则根据新的大小调用AllocSetAlloc分配一个新的内存单元,把旧的内存单元值复制到新的内存单元,调用AllocSetFree释放原来的内存单元 | | AllocSetGetChunkSpace | 返回内存单元的大小,包括头部占用的空间 | | AllocSetIsEmpty | 检查是否复位(isReset),如果是,则返回true;否则返回false | | AllocSetStats | 显示上下文的内存消耗信息,打印到标准stderr输出上 |

六、多维监控

数据库是企业的关键组件,数据库的性能直接决定了很多业务的吞吐量。为了简化数据库维护人员的调优,openGauss对数据库运行进行了多维度的监控,并且开发了一些维护特性,比如WDR(wordload dignostic report,工作负荷诊断报告)性能诊断报告、慢SQL诊断、session性能诊断、系统KPI(key performance indicator,关键性能指标)辅助诊断等,帮助维护人员对数据库的性能进行诊断。这些监控项都以视图的方式对外呈现,集中在DBE_PERF模式下。WDR Snapshot除了自身快照的元数据,其他数据表来源也是DBE_PERF schema下的视图。WDR Snapshot数据表命名原则:snap_{源数据表},根据这个关系可以找到snap表所对应的原表。对这些视图的解释参照openGauss的官网(https://opengauss.org)中《开发者指南》手册的“DBE_PERF schema”章节。 性能视图的源代码在“openGauss-serversrccommonbackendcatalogperformance_views.sql”文件中(网址为:https://gitee.com/opengauss/openGauss-server/blob/master/src/common/backend/catalog/performance_views.sql,安装后会复制到安装路径的“/share/postgresql/performance_views.sql”下)。在数据库初始化阶段由initdb读取这个文件在数据库系统中创建相应的视图。这些视图遵循了openGauss通用视图的实现逻辑,即视图来自函数的封装,这些函数可能是内置函数,也可能是存储函数。OS运行的性能视图“dbe_perf.get_global_os_runtime”的相关代码如下:

CREATE OR REPLACE FUNCTION dbe_perf.get_global_os_runtime
  (OUT node_name name, OUT id integer, OUT name text, OUT value numeric, OUT comments text, OUT cumulative boolean)
RETURNS setof record
AS $$
DECLARE
  row_data dbe_perf.os_runtime%rowtype;
query_str := 'SELECT * FROM dbe_perf.os_runtime';
      FOR row_data IN EXECUTE(query_str) LOOP
   ......
    END LOOP;
    return;
  END; $$
LANGUAGE 'plpgsql' NOT FENCED;
CREATE VIEW dbe_perf.global_os_runtime AS
  SELECT DISTINCT * FROM dbe_perf.get_global_os_runtime();

global_os_runtime视图来自存储函数get_global_os_runtime的封装,在存储函数内访问“dbe_perf.os_runtime”视图、os_runtime视图的SQL语句为“CREATE VIEW dbe_perf.os_runtime AS SELECT * FROM pv_os_run_info();”。pv_os_run_info是内置函数,而内置函数负责读取数据库系统的监控指标,pv_os_run_info函数的相关代码如下:

Datum pv_os_run_info(PG_FUNCTION_ARGS)
{
    FuncCallContext* func_ctx = NULL;
    /* 判断是不是第一次调用 */
    if (SRF_IS_FIRSTCALL()) {
        MemoryContext old_context;
        TupleDesc tup_desc;
        /* 创建函数上下文 */
        func_ctx = SRF_FIRSTCALL_INIT();
        /*
         * 切换内存上下文到多次调用上下文
         */
        old_context = MemoryContextSwitchTo(func_ctx->multi_call_memory_ctx);
        /* 创建一个包含5列的元组描述模板*/
        tup_desc = CreateTemplateTupleDesc(5, false);
        TupleDescInitEntry(tup_desc, (AttrNumber)1, "id", INT4OID, -1, 0);
        TupleDescInitEntry(tup_desc, (AttrNumber)2, "name", TEXTOID, -1, 0);
        TupleDescInitEntry(tup_desc, (AttrNumber)3, "value", NUMERICOID, -1, 0);
        TupleDescInitEntry(tup_desc, (AttrNumber)4, "comments", TEXTOID, -1, 0);
        TupleDescInitEntry(tup_desc, (AttrNumber)5, "cumulative", BOOLOID, -1, 0);
 
        /* 填充元组描述模板 */
        func_ctx->tuple_desc = BlessTupleDesc(tup_desc);
        /* 收集系统信息 */
        getCpuNums();
        getCpuTimes();
        getVmStat();
        getTotalMem();
        getOSRunLoad();
        (void)MemoryContextSwitchTo(old_context);
    }
 
    /*设置函数的上下文,每次函数调用都需要*/
    func_ctx = SRF_PERCALL_SETUP();
    while (func_ctx->call_cntr 
登录查看全部

参与评论

评论留言

还没有评论留言,赶紧来抢楼吧~~

手机查看

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认