学习openGuass的第五天

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

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;

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

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 <bool enable_memoryprotect, bool is_shared, bool is_tracked>
void AlignMemoryAllocator::AllocSetMethodDefinition(MemoryContextMethods* method)
{
    method->alloc = &AlignMemoryAllocator::AllocSetAlloc<enable_memoryprotect, is_shared, is_tracked>;
    method->free_p = &AlignMemoryAllocator::AllocSetFree<enable_memoryprotect, is_shared, is_tracked>;
    method->realloc = &AlignMemoryAllocator::AllocSetRealloc<enable_memoryprotect, is_shared, is_tracked>;
    method->init = &AlignMemoryAllocator::AllocSetInit;
    method->reset = &AlignMemoryAllocator::AllocSetReset<enable_memoryprotect, is_shared, is_tracked>;
    method->delete_context = &AlignMemoryAllocator::AllocSetDelete<enable_memoryprotect, is_shared, is_tracked>;
    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;

六、多维监控
数据库是企业的关键组件,数据库的性能直接决定了很多业务的吞吐量。为了简化数据库维护人员的调优,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-server\src\common\backend\catalog\performance_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 < TOTAL_OS_RUN_INFO_TYPES) {
        /* 填充所有元组每个字段的值 */
        Datum values[5];
        bool nulls[5] = {false};
        HeapTuple tuple = NULL;
        errno_t rc = 0;
        rc = memset_s(values, sizeof(values), 0, sizeof(values));
        securec_check(rc, "\0", "\0");
        rc = memset_s(nulls, sizeof(nulls), 0, sizeof(nulls));
        securec_check(rc, "\0", "\0");
        if (!u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].got) {
            ereport(DEBUG3,
                (errmsg("the %s stat has not got on this plate.",
                   u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].name)));
            func_ctx->call_cntr++;
            continue;
        }
        values[0] = Int32GetDatum(func_ctx->call_cntr);
        values[1] = CStringGetTextDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].name);
      values[2] = u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].getDatum(
            u_sess->stat_cxt.osStatDataArray[func_ctx->call_cntr]);
    values[3] = CStringGetTextDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].comments);
        values[4] = BoolGetDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].cumulative);

        tuple = heap_form_tuple(func_ctx->tuple_desc, values, nulls);
        SRF_RETURN_NEXT(func_ctx, HeapTupleGetDatum(tuple));
    }
    /* 填充结束,返回结果 */
    SRF_RETURN_DONE(func_ctx);
}

pv_os_run_info函数可以分为3段:

(1)调用CreateTemplateTupleDesc函数和TupleDescInitEntry函数定义元组描述信息。
(2)调用getCpuNums函数、getCpuTimes函数、getVmStat函数、getTotalMem函数、getOSRunLoad函数收集系统信息。
(3)把收集的u_sess信息填充到元组数据中,最后返回给调用者。openGauss提供了实现返回结果的通用SQL函数的实现步骤和方法,它们是SRF_IS_FIRSTCALL、SRF_PERCALL_SETUP、SRF_RETURN_NEXT和SRF_RETURN_DONE。从代码可以看出,pv_os_run_info的实现流程也是遵循openGauss通用的SQL函数实现方法。
系统指标的收集来自读取系统信息,对数据库系统中一些模块进行打点(打点就是按照规格采集指定数据,用以记录系统运行的一些关键点)。很多打点集中在两个方面: 事务执行次数和执行时间。从而推断最大时间、最小时间、平均时间。这些比较分散,代码逻辑相对简单,这里不再进行介绍。只需要根据内置函数读取的变量查看这些变量赋值的地方就可以追踪具体的实现位置。

openGauss数据库主要维护特性的实现代码在“openGauss-server\src\gausskernel\cbb\instruments”目录中,比如WDR、SQL百分位计算,这里不再进行介绍。

性能统计对openGauss的正常运行也会带来一定的性能损耗,所以这些特性都有开关控制。具体说明如下。

(1)等待事件信息实时收集功能的开关为enable_instr_track_wait。
(2)Unique SQL信息实时收集功能的开关为enable_instr_unique_sql、enable_instr_rt_percentile。
(3)数据库监控快照功能的开关为enable_wdr_snapshot。
其他功能也都有相应的GUC参数进行调节,根据平常使用的需要,可以打开具体维护项查看系统的运行情况。

七、模拟信号机制
信号是Linux进程/线程之间的一种通信机制,向一个进程发送信号的系统函数是kill,向一个线程发送信号的系统函数是pthread_kill。在openGauss中既有gs_ctl向openGauss进程发送的进程间信号,也有openGauss进程中线程间的信号。

信号是一种有限的资源,OS提供的信号有SIGINT、SIGQUIT、SIGTERM、SIGALRM、SIGPIPE、SIGFPE、SIGUSR1、SIGUSR2、SIGCHLD、SIGTTIN、SIGTTOU、SIGXFSZ等。这些信号一般都是系统专用的,每个信号都有专门的用途,比如SIGALRM是系统定时器的通知信号。留给应用的信号主要是SIGUSR1、SIGUSR2。

在系统信号有限的情况下,为了在openGauss中表达不同的丰富的通信语义,openGauss额外增加了新的变量表示具体的语义。openGauss是多线程架构,在同一个进程内如果不同的线程注册了不同的处理函数,则后者会覆盖前者的信号处理。为了不同线程能够注册不同的处理函数,需要自己管理信号对应的注册函数。为了解决这些问题,openGauss实现了信号的模拟机制。信号模拟的基本原理是每个线程注册管理自己的信号处理函数,信号枚举值仍然使用系统的信号值,线程使用自己的变量记录信号和回调函数对应关系。线程之间发送信号时,先设置变量为具体的信号值,然后使用系统调用pthread_kill发送信号,线程收到通知后再根据额外的变量表示的具体信号值,回调对应的信号处理函数。

信号处理涉及的数据结构代码如下。每个线程有一个GsSignalSlot结构,保存了线程ID、线程名称和GsSignal结构,而GsSignal结构保存了每个信号对应的处理函数数组和每个线程相关的信号池。而信号池struct SignalPool包括了使用的信号列表和空闲的信号列表,当一个模拟信号到达时,找一个空闲信号GsNode,然后放到使用的列表中。GsNode中存放了信号值结构GsSndSignal sig_data。在GsSndSignal结构中保存了发送的信号具体值和发送的线程ID。当需要设置一些额外检查信息时,设置GsSignalCheck内容。相关代码如下。

typedef struct GsSignalSlot {
    ThreadId thread_id;
    char* thread_name;
    GsSignal* gssignal;
} GsSignalSlot;
typedef struct GsSignal {
    gs_sigfunc handlerList[GS_SIGNAL_COUNT];
    sigset_t masksignal;
    SignalPool sig_pool;
    volatile unsigned int bitmapSigProtectFun;
} GsSignal;
typedef struct SignalPool {
    GsNode* free_head; /* 空闲信号列表头部 */
    GsNode* free_tail; /* 空闲信号列表尾部 */
    GsNode* used_head; /* 使用信号列表头部*/
    GsNode* used_tail; /*使用信号列表尾部*/
    int pool_size;     /* 数组列表大小*/
    pthread_mutex_t sigpool_lock;
} SignalPool;
typedef struct GsNode {
    GsSndSignal sig_data;
    struct GsNode* next;
} GsNode;
typedef struct GsSndSignal {
    unsigned int signo;  /* 需要处理的信号*/
    gs_thread_t thread;  /* 发送信号的线程ID */
    GsSignalCheck check; /* 信号发送线程需要检查的信息 */
} GsSndSignal;
typedef struct GsSignalCheck {
    GsSignalCheckType check_type;
    uint64 debug_query_id;
    uint64 session_id;
} GsSignalCheck;
上一篇:墨天轮openGauss免费在线SQL测试云主机


下一篇:openGauss编译安装过程