OceanBase原理之内存管理

第1章 前言

1.1 多租户管理简介

OceanBase数据库中,应用了单集群多租户的设计,使得一个集群内能够创建多个彼此独立的租户。在OceanBase数据库,租户成为了资源分配的单位,同时还是数据库对象管理和资源管理的基础。

在某种程度上,租户可以被视为传统数据库中的"实例"的意思。各个租户之间保持完全的隔离状态。OceanBase禁止跨租户的数据访问,从而确保了用户的数据资产都能得到充分的保护,避免了被其他租户窃取的风险,这在数据安全层面至关重要。在资源使用方面,每个租户都享有其专属的资源配额,这种"独占"特性确保了资源的有效利用和租户间的公平竞争。总的来说,租户(tenant)既可以作为各类数据库对象的存储容器,又是CPU、内存、IO等资源的承载者。

租户按照职责范围的不同,分为系统租户、用户租户和 Meta 租户。系统租户即 sys 租户,是 OceanBase 数据库的系统内置租户;用户租户是由用户创建的租户,与通常所说的数据库管理系统相对应,可以被看作是一个数据库实例。Meta 租户是 OceanBase 数据库内部自管理的租户,专门管理租户的私有数据,将租户的私有数据和非私有数据彻底隔离开,杜绝了私有数据和非私有数据访问的一致性问题。每创建一个用户租户,系统就会创建一个对应的 Meta 租户,其生命期与用户租户保持一致。Meta 租户不能直接登录,它的信息会通过用户租户和系统租户进行访问。

租户按照兼容模式的不同,又分为 MySQL 租户和 Oracle 租户。使用不同类型的租户对应不同类型的语法和功能。在 OceanBase 数据库中,系统租户 sys 属于 MySQL 租户。

租户的可用物理资源以资源池的方式描述,资源池由分布在物理机上的若干资源单元组成,资源单元的可用物理资源通过资源配置指定,资源配置由用户创建。

  • 资源单元是一个容器。实际上,副本是存储在资源单元之中的,所以资源单元是副本的容器。资源单元描述了位于一个 Server 上的一组计算和存储资源(Memory、CPU 和 IO 等),一个租户在同一个 Server 上最多有一个资源单元,同时资源单元也是集群负载均衡的一个基本单位,在集群节点上下线,扩容、缩容时会动态调整资源单元在节点上的分布进而达到资源的使用均衡。
  • 资源配置是资源单元的配置信息,用来描述每个资源单元可用的 CPU、内存、存储空间和 IOPS 等。修改资源配置可以动态调整资源单元的规格,进而调整对应租户的资源。
  • 资源池由具有相同资源配置的若干个资源单元组成。一个租户拥有若干个资源池,这些资源池的集合描述了这个租户所能使用的所有资源,一个资源池只能属于一个租户。

1.2 多租户的资源隔离

OceanBase数据库实现了多租户的资源隔离,目前租户资源隔离粒度是内存、CPU和磁盘IO级别,不同的租户可以配置不同的内存、CPU和磁盘IO规格。

为了控制每个租户的内存资源占用,OceanBase数据库自研了一套内存分配器,主要包括ob_malloc、MemoryContext,它们在内存分配时会标记每块内存的归属,同时更新tenant、context、mod三个级别的内存统计信息,从而明确各租户的内存使用情况。

自研内存分配器,不仅实现了多租户的内存资源隔离,而且能针对性地设计内存诊断工具,排查OceanBase数据库中潜在的内存bug,例如ob-sanity、memory_leak、内存元数据dump等诊断工具。

第2章 内存分配器

2.1 ob_malloc

2.1.1 工作原理

OceanBase数据库自研了一套libc风格的接口函数ob_malloc/ob_free/ob_realloc,这套接口会根据tenant_id、ctx_id、label等属性动态申请大小为size的内存块,并且为内存块打上标记,确定归属。这不仅方便了多租户的资源管理,而且对诊断内存问题有很大帮助。

如图所示,ob_malloc会根据tenant_id、ctx_id索引到相应的ObTenantCtxAllocator,每一个ObTenantCtxAllocator都对应一组对象池ObjectSet,通过ObjectSet从内存池中动态申请出对象AObject。AObject由meta、data两部分组成,其中meta部分记录着这块内存的元数据,包括内存的归属、大小等信息,data部分是ob_malloc返回给调用者的数据区,地址为ptr,大小为size。

ob_free通过偏移运算求出即将释放的内存所对应的对象AObject,再将AObject放回内存池。

ob_realloc与libc的realloc不同,它不是在原有地址上扩容,而是先通过ob_malloc+memcpy将数据复制到另一块内存上,再调用ob_free释放原有内存。

inline void *ob_malloc(const int64_t nbyte, const ObMemAttr &attr = default_memattr);
inline void ob_free(void *ptr);
inline void *ob_realloc(void *ptr, const int64_t nbyte, const ObMemAttr &attr)

2.1.2 内存布局

OceanBase数据库的内存布局如图所示,其中AChunk是OceanBase与系统交互的最小内存单位,大小为2M的整数倍。2M大小的AChunk会被划分成多个8K大小的内存块,每个ABlock由相连的多个8K内存块组成。8K大小的ABlock会再次划分成多个AObject,每个AObejct的大小为8字节的整数倍。AChunk、ABlock、AObject都分为meta区和data区,meta区保存着内存块的元信息,data区为用户可使用的内存。

类名 成员变量
AChunk BlockSet *block_set_:AChunk所属的BlockSet uint64_t washed_blks_, washed_size_:被wash的ABlock数和内存大小 uint64_t alloc_bytes_:chunk_data的内存大小 AChunk *prev_, *next_:ObTenantCtxAllocator的空闲chunk链表head_chunk_或者BlockSet的using chunk 链表clist_ AChunk *prev2_, *next2_:ObTenantCtxAllocator的using chunk 链表using_list_head_ ASimpleBitSet<MAX_BLOCKS_CNT> blk_bs_:AChunk中全部ABlock的位置 ASimpleBitSet<MAX_BLOCKS_CNT> unused_blk_bs_:AChunk中未被使用的ABlock的位置
ABlock struct {uint8_t in_use_ : 1; uint8_t is_large_ : 1; uint8_t is_washed_ : 1;}:记录ABlock是否被使用、是否独占一个AChunk、是否被wash uint64_t alloc_bytes_:block_data的内存大小 uint32_t ablock_size_:大小一般为8K ObjectSet *object_set_:ABlock所属的ObejctSet int64_t mem_context_:所属MemoryContext的地址 ABlock *prev_, *next_:ABlock链表
AObject struct {uint16_t in_use_ : 1; uint16_t is_large_ : 1;}:记录AObject是否被使用、是否独占一个ABlock uint16_t nobjs_, nobjs_prev_, obj_offset_:标记AObejct的内存占用和在ABlock中的位置 uint32_t alloc_bytes_:实际可使用的内存大小 uint64_t tenant_id_; char label_[16]:记录内存归属 AObject *prev_、next_:AObject链表 char data_[0]:可使用内存的起始地址

2.1.3 内存池

ob_malloc会根据输入内存属性选择出合适的对象池ObjectSet,再通过ObjectSet从AObject/ABlock/AChunk组成的三级缓存结构的内存池中分配出数据区大小为size的对象AObject。

如图所示,ObjectSet会优先从自身持有的第一级缓存链表free_list中分配内存,free_list[cls]是缓存着大小为8*cls 字节的AObject链表。如果free_list中分不出期望size的缓存,则通过相应的BlockSet从第二级缓存链表block_list中分配出ABlock,从ABlock中切出期望size的AObject,并且将剩余内存放入free_list。同理,如果block_list中分不出期望size的缓存,则依次从BlockSet自身的chunk_free_list、ObTenantCtxAllocator的head_chunk、AChunkMgr的free_list等第三级缓存链表中分配出AChunk,从AChunk中切出期望size的ABlock,并且将剩余内存放入block_list。如果现有的三级缓存结构中分配不出期望size的缓存,则通过mmap系统调用向系统申请内存。

2.1.4 ObTenantCtxAllocator

为了更精细地管理租户的内存,OceanBase数据库把租户的内存划分为多个context,并且通过内存分配器ObTenantCtxAllocator来管理context的内存。

ObTenantCtxAllocator提供alloc/free/realloc接口,从对象池ObjectSet中申请和释放内存。

virtual void *alloc(const int64_t size, const ObMemAttr &attr);
virtual void* realloc(const void *ptr, const int64_t size, const ObMemAttr &attr);
virtual void free(void *ptr);

当对象池ObjectSet、BlockSet中分不出来内存时,会从head_chunk_中获取2M内存块,切割出所需分配的内存,剩余内存放入ObjectSet、BlockSet的缓存列表中,以供下次分配。

AChunk *alloc_chunk(const int64_t size, const ObMemAttr &attr);
void free_chunk(AChunk *chunk, const ObMemAttr &attr);
成员变量
ObTenantResourceMgrHandle resource_handle_:记录着context当前占用的内存和内存上限 uint64_t tenant_id_, ctx_id_:标示ObTenantCtxAllocator管理的内存所属的context bool has_deleted_:标记所属租户是否被删除 ObjectMgr obj_mgr_:内存分配器 int64_t idle_size_:限制context的空闲内存 AChunk head_chunk_, using_list_head_:属于context的2M内存块组成的空闲内存链表或using内存链表 int64_t chunk_cnt_:head_chunk_中2M内存块的数量 ObMutex chunk_freelist_mutex_, using_list_mutex_:访问head_chunk_、using_list_head_的读写锁 int64_t wash_related_chunks_:using_list_head_中被wash过的2M内存块数 int64_t washed_blocks_, washed_size_:using_list_head_中被wash过的ABlock数和内存大小

2.2 MemoryContext

OceanBase数据库采用了PostgreSQL的内存上下文管理机制MemoryContext,内存分配操作都在特定的memory_context中进行,memory_context释放时会回收其分配的全部内存,这样即使某些内存没有被任何指针指向或者忘记了释放,也可以通过释放memory_context来避免内存泄漏。

OceanBase数据库中的memory_context会组织成树状结构,可以更好地跟踪进程中memory_context的创建和使用情况,主要体现在两方面:

  • 分层:服务器程序运行期间,难免要根据不同的生命周期同时创建很多的memory_context对象,比如一个transaction下会有很多的query,一个query下会有很多的operator,显然一个operator结束后operator层的内存都可以释放了,一个query结束后query层的内存也可以释放了,等到transaction结束,transaction内存也可以释放,如果这些不同level的内存共享一个memory_context,那每层level想"一键清除"自己的内存就很困难,因为它需要业务代码自行记录分配出来的地址。如果为每层level创建一个memory_context,只要按需删除或者重置即可,这就是分层的好处。
  • 父子关系:不同层次之间用parent-child关系组织,一个parent可以有多child,一个child只能有一个parent,当父memory_context删除或者重置时,孩子及子孙memory_context会一一删除,对应到上面场景,如果transaction中某个query的memory_context忘记删除,那至少transaction结束时能靠parent-child关系溯源,将这次忘记删除的动作补齐。

2.2.1 MemoryContext接口

通用分配器有两种典型模型:

  • f类型分配(freeable):用户可以随时申请释放内存,释放的内存可以被复用回收,所有分配的内存必须在分配器结束或者重置时都释放完全,否则是bug行为。
  • p类型分配(permanent):用户只管申请内存,不需要并且也无法提前释放,所有内存在分配器结束或重置时释放。

OceanBase数据库的MemoryContext兼备f和p的能力,能同时提供两种接口以及相应的两种分配器对象:

// f类和p类内存分配的接口
void *allocf(const int64_t size, const ObMemAttr &attr=default_memattr);
void *allocp(const int64_t size, const ObMemAttr &attr=default_memattr);

// f类和p类内存分配器
ObIAllocator &get_malloc_allocator();
common::ObArenaAllocator &get_arena_allocator();

void free(void *ptr);

从上文描述可知,p类型内存不需要free,因此free接口只能传f类型分配出的内存, 否则将是未定义行为。

2.2.2 MemoryContext接口管理

OceanBase数据库通过CREATE_CONTEXT、DESTROY_CONTEXT、WITH_CONTEXT接口管理MemoryContext的生命周期,实现MemoryContext的创建、销毁以及切换。

// 创建上下文
int CREATE_CONTEXT(MemoryContext *&mem_context, const ContextParam ¶m);

// 销毁上下文
void DESTROY_CONTEXT(MemoryContext *mem_context);

// 切换上下文
WITH_CONTEXT(MemoryContext *mem_context) {
  ...
}

//临时创建、使用上下文
CREATE_WITH_TEMP_CONTEXT(const ContextParam ¶m) { 
  ...
}

其中,ContextParam包括了被创建的MemoryContext的基本属性:

  • int64_t properties_:以低5位标记ContextPropertyEnum中的属性

    enum ContextPropertyEnum
    {
    ADD_CHILD_THREAD_SAFE = 1 << 1, // 某些parent会并发添加孩子, 此属性控制节点管理的线程安全
    ALLOC_THREAD_SAFE = 1 << 2, // 某些context需要有线程安全的分配释放能力
    USE_TL_PAGE_OPTIONAL = 1 << 3, // 某些context的特殊优化,主要是SQL,可以将全局竞争只发生在chunk上
    RETURN_MALLOC_DEFAULT = 1 << 4 // 控制get_allocator接口返回的是p类型还是f类型的分配器
    };

  • ObMemAttr attr_:标记tenant_id, label, ctx_id信息,决定内存归属

  • int64_t page_size_:p类型分配器的page_size

  • uint32_t ablock_size_:f类型的分配器page_size

  • int parallel_:f类型分配器的并行度,当且仅当需要线程安全时生效

2.2.2.1 CREATE_CONTEXT

此接口根据输入参数ContextParam从指定的MemoryContext对象(父对象)上创建出一个新的MemoryContext对象(子对象),示例代码如下:

MemoryContext *context = nullptr;
int ret = ROOT_CONTEXT.CREATE_CONTEXT(context, ContextParam().set_label("MemDumpContext"));
PreAllocMemory *pre_mem = nullptr;
if (OB_FAIL(ret)) {
  LOG_WARN("create context failed", K(ret));
} else if (OB_ISNULL(pre_mem = (PreAllocMemory*)context->allocp(sizeof(PreAllocMemory)))) {
  ret = OB_ALLOCATE_MEMORY_FAILED;
  LOG_WARN("alloc mem failed", K(ret));
} else {}
  • ROOT_CONTEXT是MemoryContext树状结构的根结点,是所有MemoryContext的祖先,当且仅当进程退出时才会被释放。
  • ROOT_CONTEXT通过调用CREATE_CONTEXT接口创建出新的MemoryContext,并且成为新MemoryContext的父亲。
2.2.2.2 DESTROY_CONTEXT

此接口销毁输入参数MemoryContext分配的全部内存和MemoryContext自身,并且从树状结构中剔除。

2.2.2.3 WITH_CONTEXT

OceanBase数据库存在静态线程局部的上下文CurrentMemoryContext,WITH_CONTEXT利用guard机制完成CurrentMemoryContext的切换,在WITH_CONTEXT的作用域内CurrentMemoryContext切换成新的MemoryContext,离开作用域后恢复成之前的MemoryContext,示例代码如下:

WITH_CONTEXT(new_mem_context) {
  static const int64_t BUFLEN = 1 << 17;
  char *buf = (char *)CURRENT_CONTEXT.allocp(BUFLEN);
  if (OB_ISNULL(buf)) {
    ret = OB_ALLOCATE_MEMORY_FAILED;
    LIB_LOG(ERROR, "no memory", K(ret));
  } else {
    ...
  }
}
2.2.2.4 CREATE_WITH_TEMP_CONTEXT

有时用户只想在函数某一段代码创建一个临时的memory_context并切换,代码块结束就销毁,这种需求可以使用CREATE_WITH_TEMP_CONTEXT接口来完成,一个接口完成CREATE_CONTEXT、WITH_CONTEXT、DESTROY_CONTEXT三个动作,非常方便,输入参数为ContextParam, 使用语法与WITH_CONTEXT类似。

2.2.3. ObPageManager

创建MemoryContext时需要指定MemoryContext的基本属性ContextParam,当ContextParam.properties_设置了USE_TL_PAGE_OPTIONAL属性,则MemoryContext申请的内存都来自于数据结构为ObPageManager线程局部变量pm。

pm在线程启动时创建,在线程退出时销毁,通过pm申请内存可以避免ob_malloc的全局锁开销,从而提升内存分配的性能。每个线程都有一个pm,单个pm的缓存越大,浪费的内存越多。因此,有必要限制pm的缓存大小,默认情况下,单个pm最多缓存2个2M内存块,mini模式下pm没有缓存。

2.2.4 MemoryContext与ob_malloc的区别

  • MemoryContext可以快速可靠的释放内存,快速体现在不用遍历小对象,可靠体现在不会产生内存泄漏。
  • MemoryContext性能比ob_malloc更好,一次分配可以惠泽多次调用,并且没有锁竞争。
  • MemoryContext内存碎片相比ob_malloc更优,多次申请的内存更加集中,与外部长生命周期内存的隔离性更好。

第3章 基于OceanBase内存管理的诊断工具

3.1 ob-sanity

ob-sanity是个可以检查内存安全漏洞的observer编译选项,它在编译期进行指令插桩,在运行期检测observer代码中存在的内存错误,例如use-after-free、double-free、out-of-bounds。与ASAN相比,ob-sanity具备高性能、高扩展、高可控等优势,允许sanity版本的observer在各种压测环境下暴露潜在的内存问题。

3.1.1 指令插桩

ob-sanity利用llvm提供的pass能力,在编译阶段对observer代码进行修改,在所有内存访问指令load/store之前插桩权限检查代码,以达到运行期尽早发现内存安全漏洞的目标。为了提高性能,ob-sanity不检查栈变量和全局变量,一方面是在编译期不对栈变量和全局变量进行插桩,另一方面是将栈地址空间独立出来实现运行期skip栈变量。插桩代码如下:

插桩前:

char *ptr = ob_malloc(1);
*ptr++ = '\0';
*ptr = '\0';

插桩后:

char *ptr = ob_malloc(1);
if (is_poisoned(ptr, 1)) abort();  // 权限检查代码
*ptr++ = '\0';
if (is_poisoned(ptr, 1)) abort();
*ptr = '\0';

3.1.2 影子内存

ob-sanity通过1个字节的影子(shadow)内存来标记8个字节的常规(canonical)内存的访问状态,其中影子内存是指对内存检测工具不可见的内存,常规内存是指通过OceanBase内存管理机制申请、释放的内存。

权限检查代码需要通过canonical内存索引到shadow内存,读取shadow内存中记录的访问状态,判断对canonical内存的访问是否合法。shadow内存中记录的访问状态如下:

|------------------|----------|---------|--------------------|
| shadow内存(1字节) | 0xf0 | 0x00 | 0x$n(n=[1,7]) |
| canonical内存(8字节) | 8字节都不可访问 | 8字节都可访问 | 前n字节可访问,后8-n字节不可访问 |

为了提高索引效率,shadow内存与canonical内存的地址空间满足以下映射关系:

shadow_addr = canonical_addr >> 3;

3.1.3 库函数检查

类似memcpy、memset等操作内存的函数有很多并且被广泛使用,这些库函数是运行期动态链接的,无法编译插桩,通过hook实现对这类函数操作的内存区域的权限检查:

void *memset(void *s, int c, size_t n)
{
  static void *(*real_func)(void *, int, size_t)
    = (__typeof__(real_func)) dlsym(RTLD_NEXT, "memset");
  sanity_check_range(s, n);
  return real_func(s, c, n);
}

同时,gcc会对某些小内存操作进行优化,避免call指令,这显然会绕开sanity检查,利用-fno-builtin-function选项禁掉对相应函数的优化,比如禁掉memset优化:-fno-builtin-memset。

3.1.4 白名单

sanity版本的observer,只要发现内存安全问题就会触发coredump。对于已知但未修复的内存bug,频繁复现将会阻塞环境,如果能明确内存bug导致的影响是可控的,则可以配置白名单参数sanity_whitelist,使得白名单里出现的函数栈再次复现时将不会coredump。

  • 函数名获取:通过执行gdb的bt命令或者查询observer.log日志都可以获取函数名,推荐后者。

  • 更新白名单sanity_whitelist:

    alter system set sanity_whitelist = "func_1;func_2";

  • abort时内核会解析堆栈,并逐层跟白名单比较, 只要解析后的字符串包含白名单即跳过coredump。

3.2 内存泄漏诊断工具memory_leak

为了便于排查内存泄露问题,OceanBase 数据库实现了内存泄漏动态诊断机制,其工作原理是在每一次跟踪模块进行内存分配的时候,记录其申请内存的地址及相应的调用栈;在所申请内存被释放的时候,将该条记录清空。这样正常申请、释放内存的调用栈会被很快清空,而出现内存泄露的调用栈的记录将一直存在,最后我们按照调用栈进行分组,将每个不同的调用栈当前存在的申请记录进行累加,显示在虚拟表 __all_mem_leak_checker_info 中。换言之,累计次数多的调用栈很可能出现了内存泄露(当然也有可能是由于各种缓存而造成的,这个需要具体问题具体分析)。

3.2.1 使用说明

  • 启动memleak,跟踪指定模块内存。

    alter system set leak_mod_to_check='OB_COMMON_ARRAY';

    4.0及以上版本支持跟踪指定租户的指定模块内存

    alter system set leak_mod_to_check='OB_COMMON_ARRAY@tenant_id';

  • 查询跟踪模块的内存分配调用栈。

    select * from __all_virtual_mem_leak_checker_info order by alloc_count desc;

  • 一般来说存在泄露的调用栈计数都很大(而且越来越大),将这样的调用栈利用 addr2line 打印出,通过分析调用栈诊断内存是否泄漏。

    addr2line -pCfe bin/observer 0xe77df5 0xedf5a4 0xee14b0 0xee0923 1 0xeea558 0x4a05c3 0x1485cd9 0x1485223 0x1483c4b 0x1526f2a 0x1401359 0x1403075 0x140325a 0x1406416 0x14a51bb 0x14a5130 0x140x48f3aa7db8 0x14a4cc8 0x14a50f4 0x14a5cb1 0x130166e 0xd08bc9 0xd069d2 0xd01e97 0xeff033 0xefe6
    $oceanbase_root/src/lib/utility/utility.cpp:58
    $oceanbase_root/src/lib/../../src/lib/allocator/ob_mem_leak_checker.h:125
    $oceanbase_root/src/lib/allocator/ob_tc_malloc.cpp:399
    $oceanbase_root/src/lib/allocator/ob_tc_malloc.cpp:218
    $oceanbase_root/src/observer/../../src/lib/allocator/ob_malloc.h:38
    $oceanbase_root/src/lib/allocator/ob_malloc.cpp:121
    $oceanbase_root/src/lib/../../src/lib/allocator/ob_malloc.h:116
    $oceanbase_root/src/sql/../../src/lib/container/ob_array.h:295
    $oceanbase_root/src/sql/../../src/lib/container/ob_array.h:291
    $oceanbase_root/src/sql/../../src/lib/container/ob_array.h:468
    $oceanbase_root/src/sql/optimizer/ob_log_table_scan.cpp:85
    $oceanbase_root/src/sql/optimizer/ob_log_plan.cpp:1183
    $oceanbase_root/src/sql/optimizer/ob_log_plan.cpp:1484
    $oceanbase_root/src/sql/optimizer/ob_log_plan.cpp:1557
    $oceanbase_root/src/sql/optimizer/ob_log_plan.cpp:2092
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:467
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:455
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:890
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:378
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:450
    $oceanbase_root/src/sql/optimizer/ob_select_log_plan.cpp:568
    $oceanbase_root/src/sql/optimizer/ob_optimizer.cpp:23
    $oceanbase_root/src/sql/ob_sql.cpp:1160
    $oceanbase_root/src/sql/ob_sql.cpp:887
    $oceanbase_root/src/sql/ob_sql.cpp:152
    $oceanbase_root/src/observer/mysql/obmp_query.cpp:263

  • 启动memory_leak工具后执行性能会显著下降,问题定位后及时关闭memory_leak

    alter system set leak_mod_to_check='';

3.3 常态化memoryleak

上一节中介绍了OceanBase数据库通过memory_leak工具来诊断内存泄漏。启动memory_leak的时机一般是内存发生泄漏之后,如果memory_leak开启后内存不再泄漏,则无法跟踪到发生泄漏的模块,只能等待复现。因此,我们需要常态化的memleak,实时保存内存分配信息的采样,通过采样的结果分析可能发生内存泄漏的模块。

3.3.1 内存分配信息的采样

3.3.1.1 动态设置配置项,控制内存分配信息的采样比例
配置项 语义 范围 默认值
_min_malloc_sample_interval 相邻两次采样之间ob_malloc的最小次数 [1,10000] 16
_max_malloc_sample_interval 相邻两次采样之间ob_malloc的最大次数 [1,10000] 256

要求:

  • _min_malloc_sample_interval <= _max_malloc_sample_interval;
  • 当_min_malloc_sample_interval = 10000时,限流器零采样;
  • 当_max_malloc_sample_interval = 1时,限流器全采样。
3.3.1.2 累计内存触发采样

在控制内存信息采样比例的基础上,每累积 4M 内存,就会跟踪当前的内存分配信息。此机制可以保证单次分配内存大的模块,采样率更高,从而在被跟踪的object数一定的情况下,跟踪的内存大小更多。

当_min_malloc_sample_interval=16,_max_malloc_sample_interval=256时,各模块的采样率如下:

|--------|--------|-------|-------|------|-------|-------|------------|---------|
| 单次内存分配 | <16KB | 16KB | 32KB | 64KB | 128KB | 256KB | 256KB~16MB | >=16MB |
| 采样率 | 1/256 | 1/256 | 1/128 | 1/64 | 1/32 | 1/16 | 1/16 | 1 |

当_min_malloc_sample_interval=16,_max_malloc_sample_interval=64时,各模块的采样率如下:

|--------|---------|-------|-------|------------|---------|
| 单次内存分配 | <=64KB | 128KB | 256KB | 256KB~16MB | >=16MB |
| 采样率 | 1/64 | 1/32 | 1/16 | 1/16 | 1 |

3.3.2 虚拟表__all_virtual_malloc_sample_info

列名 类型 含义
svr_ip varchar:MAX_IP_ADDR_LENGTH IP地址
svr_port bigint(20) 端口号
tenant_id bigint(20) 租户ID
ctx_id bigint(20) CTX ID
mod_name varchar:OB_MAX_CHAR_LENGTH 模块名称
back_trace varchar:DEFAULT_BUF_LENGTH 内存分配的堆栈
ctx_name varchar:OB_MAX_CHAR_LENGTH CTX 名称
alloc_count bigint(20) 内存分配次数
alloc_bytes bigint(20) 内存分配的总大小

每10s对内存进行遍历,将被跟踪的obj统计到哈希表malloc_sample_map中,哈希表的主键ObMallocSampleKey包含元素:tenant_id、ctx_id、mod_name、backtrace,ObMallocSampleValue包含元素:alloc_count、alloc_bytes。

通过查询虚拟表获取哈希表中保存的全量数据:select * from __all_virtual_malloc_sample_info。

注:__all_virtual_malloc_sample_info里显示的数据是对内存分配行为的采样,对于不同的分配行为会有不一样的采样频率,实际的内存占用=alloc_bytes/采样频率,采样频率属于[1/_max_malloc_sample_interval, 1/_min_malloc_sample_interval]区间。

3.3.3 使用说明

常态化模式

默认配置项值为_min_malloc_sample_interval=16,_max_malloc_sample_interval=256,也可以自行设置配置项值,但要保证1<_min_malloc_sample_interval<=_max_malloc_sample_interval<10000。

select * from __all_virtual_malloc_sample_info where mod_name='glibc_malloc' order by alloc_size desc limit 10;
$addr2line -pCfe bin/observer 0x4209973 0x3d56e15 0x3d56d0a 0x41f6ec4 0xe2663a9 0xe266542 0x113e7f85 0xed77810 0xd28eaff 0xd28ea56 0x6f7fe47 0x41dfaec 0x7f80d3580445 0x41dc315
void* oceanbase::lib::ObTenantCtxAllocator::common_alloc<oceanbase::lib::ObjectMgr>(long, oceanbase::lib::ObMemAttr const&, oceanbase::lib::ObTenantCtxAllocator&, oceanbase::lib::ObjectMgr&) at /home/distcc/tmp/./deps/oblib/src/lib/alloc/ob_tenant_ctx_allocator.cpp:420 (discriminator 2)
oceanbase::lib::ObTenantCtxAllocator::alloc(long, oceanbase::lib::ObMemAttr const&) at /home/distcc/tmp/./deps/oblib/src/lib/alloc/ob_tenant_ctx_allocator.cpp:40
oceanbase::lib::ObMallocAllocator::alloc(long, oceanbase::lib::ObMemAttr const&) at /home/distcc/tmp/./deps/oblib/src/lib/alloc/ob_malloc_allocator.cpp:89
oceanbase::common::ob_malloc(long, oceanbase::lib::ObMemAttr const&) at /home/distcc/tmp/../../../deps/oblib/src/lib/allocator/ob_malloc.h:38
ob_malloc_retry(unsigned long) at /home/distcc/tmp/../../../../../deps/oblib/src/lib/alloc/malloc_hook.cpp:55
ob_malloc_hook(unsigned long, void const*) at /home/distcc/tmp/../../../../../deps/oblib/src/lib/alloc/malloc_hook.cpp:78
operator new(unsigned long) at ??:?
ObRWLock at /home/distcc/tmp/./deps/oblib/src/lib/lock/ob_rwlock.cpp:59
ObTenantMutilAllocatorMgr at /home/distcc/tmp/../../../src/share/allocator/ob_tenant_mutil_allocator_mgr.h:34 (discriminator 2)
oceanbase::common::ObTenantMutilAllocatorMgr::get_instance() at /home/distcc/tmp/./src/share/allocator/ob_tenant_mutil_allocator_mgr.cpp:218 (discriminator 4)
oceanbase::observer::ObServer::init(oceanbase::observer::ObServerOptions const&, oceanbase::common::ObPLogWriterCfg const&) at /home/distcc/tmp/../../../src/observer/ob_server.cpp:299 (discriminator 4)
main at /home/distcc/tmp/../../../src/observer/main.cpp:538
?? ??:0
_start at ??:?
全采样模式

跟踪全部的内存分配信息,在此模式下,OBServer 会出现很大程度的性能回退。

alter system set _min_malloc_sample_interval=1;
alter system set _max_malloc_sample_interval=1;
零采样模式

关闭常态化memoryleak。

alter system set _min_malloc_sample_interval=10000;
alter system set _max_malloc_sample_interval=10000;

3.4 内存元数据dump

OceanBase数据库有两种常用的获取内存统计信息的方法:查询与内存相关的虚拟表、查看MEMORY日志。然而,当observer不可用时,出现查表失败且MEMORY日志不再正常打印的情况,用户就需要dump内存元数据的手段获取内存统计信息。dump命令可以将所有内存的元数据写到磁盘文件memory_meta.

3.4.1 使用说明

  • 在OceanBase 数据库的部署目录新增 etc/dump.config 文件, 写入目标指令。

    输出全部内存的元数据

    echo 'dump chunk all' > etc/dump.config

    输出指定租户ID、context的内存元数据

    4.x版本

    echo 'dump chunk tenant_id=tenant_id,ctx_id=ctx_name' > etc/dump.config

    4.0之前版本

    echo 'dump chunk tenant_id=tenant_id,ctx_id=ctx_id' > etc/dump.config

  • 执行 kill -62 $pid,生成 log/memory_meta 文件

第4章 内存配置项和虚拟表

4.1 配置项

4.1.1 Capacity类配置项

observer总内存限制

读取observer内存限制时,会优先读取配置项 memory_limit,当 memory_limit 不为 0 时,表示observer的可用内存;否则,需要继续读取配置项memory_limit_percentage和物理机总内存,计算出observer的可用内存。

memory_limit决定了observer的内存规格,当内存规格为不大于64G的小规格时,OceanBase数据库的部署成本明显降低,但也因内存变小带来了稳定性风险。

## memory_limit [0M,物理机总内存*0.9) memory_limit_percentage [10,90]
## 设置 memory_limit 为 0M,通过 memory_limit_percentage 间接计算出observe 可用内存 == memory_limit_percentage * 物理内存
alter system set memory_limit='0M';
alter system set memory_limit_percentage=90;
## 设置 memory_limit == observer 可用内存
alter system set memory_limit='16G';
500租户的预留内存

设置配置项 system_memory 可以修改500租户的预留内存,system_memory 限制了500租户之外的所有租户的总内存为memory_limit - system_memory。

system_memory支持自适应功能,当设置system_memory为0时,系统会根据memory_limit的生效值给出system_memory的自适应值。

## system_memory (__easy_memory_limit, memory_limit]
## 设置system_memory大小为3G
alter system set system_memory='3G';
## 设置system_memory为自适应值
alter system set system_memory='0M'; 
隐藏sys租户的内存限制

设置配置项_hidden_sys_tenant_memory可以修改隐藏sys租户的内存限制。

_hidden_sys_tenant_memory支持自适应功能,当设置_hidden_sys_tenant_memory为0时,系统会根据system_memory的生效值给出_hidden_sys_tenant_memory的自适应值。

## _hidden_sys_tenant_memory [1G, system_memory]
## 设置_hidden_sys_tenant_memory为自适应值
alter system set _hidden_sys_tenant_memory='0M';
libeasy内存限制

对于mysql请求,分租户限制,每个租户限制为__easy_memory_limit,一旦某个租户发送或接受的mysql请求内存用量超出则断连接;对于rpc请求,分server限制,每个server限制为__easy_memory_limit,一旦某个server发送或接收的rpc请求内存用量超出则返回rpc失败(不断连接)。easy内存归属于500租户,要保证 __easy_memory_limit 小于 system_memory。用户还可以设置__easy_memory_reserved_percentage来预留部分libeasy可用内存。

## __easy_memory_limit [1G, system_memory)
## __easy_memory_reserved_percentage [0,100]       
alter system set __easy_memory_limit='512M';
alter system set __easy_memory_reserved_percentage=10;
底层模块的预留内存

预留内存memory_reserved是指observer总内存上限 memory_limit之外的内存。属性为OB_HIGH_ALLOC的基础模块,例如libeasy、MemoryLeak、DumpMemory 、TraceProcessor,在observer内存耗尽的时候,有机会从预留内存中申请到内存,从而保障这些基础模块的运行。

## memory_reserved [10M, 物理内存 - memory_limit)
alter system set memory_reserved='500M';
栈大小设置

线程栈的大小由stack_size设置,其缺省值为512K。stack_size越大,消耗的内存资源越多;stack_size越小,定位问题时可获得的信息越少。

## stack_size [512K,20M]
alter system set stack_size='512K';

线程栈消耗的总内存total_hold有两种计算方法:

  • 根据线程栈对应的内存模块CoStack,查询虚拟表__all_virtual_memory_info

    select sum(hold) from __all_virtual_memory_info where mod_name='CoStack';

  • shell命令获取observer线程数threads,total_hold = threads * stack_size

    cat /proc/85214/status |grep Threads

memory chunk 缓存大小

chunk_mgr有2M缓存,缓存数量太少,2M的申请性能会下降;因此可以通过memory_chunk_cache_size设置chunk的缓存数量。memory_chunk_cache_size缺省值为0,表示释放的2M内存块全部被缓存;缓存大小不为0时,缓存chunk数量=memory_chunk_cache_size/2M,即memory_chunk_cache_size小于2M时,不缓存2M内存块。当需要复现一些内存use-after-free问题时,将其设置为2M有机会加大复现概率。

## memory_chunk_cache_size [0M,)
alter system set memory_chunk_cache_size='10G';
查看memory_limit、system_memory、_hidden_sys_tenant_memory实际生效值
  • 在observer.log文件中查询关键词"update observer memory config"

    $grep 'update observer memory config' observer.log
    [2024-01-03 11:19:26.248812] INFO [SHARE.CONFIG] reload_config (ob_server_config.cpp:361) [54251][observer][T0][Y0-0000000000000000-0-0] [lt=22] update observer memory config success(memory_limit=8589934592, system_memory=1073741824, hidden_sys_memory=1073741824)

  • 查询视图GV$SYSSTAT

    obclient> select * from GV$SYSSTAT where NAME like '%effective%' and CON_ID=1;
    +--------+----------------+----------+------------+---------------------------------+-------+------------+------------+---------+
    | CON_ID | SVR_IP | SVR_PORT | STATISTIC# | NAME | CLASS | VALUE | VALUE_TYPE | STAT_ID |
    +--------+----------------+----------+------------+---------------------------------+-------+------------+------------+---------+
    | 1 | 100.88.144.165 | 58402 | 380 | effective observer memory limit | 64 | 8589934592 | SET_VALUE | 140014 |
    | 1 | 100.88.144.165 | 58402 | 381 | effective system memory | 64 | 1073741824 | SET_VALUE | 140015 |
    | 1 | 100.88.144.165 | 58402 | 382 | effective hidden sys memory | 64 | 1073741824 | SET_VALUE | 140016 |
    | 1 | 100.88.144.165 | 58404 | 380 | effective observer memory limit | 64 | 8589934592 | SET_VALUE | 140014 |
    | 1 | 100.88.144.165 | 58404 | 381 | effective system memory | 64 | 1073741824 | SET_VALUE | 140015 |
    | 1 | 100.88.144.165 | 58404 | 382 | effective hidden sys memory | 64 | 1073741824 | SET_VALUE | 140016 |

4.1.2 其它类型配置项

租户的MemStore内存限制

MemStore 指租户申请的内存资源中可供存放更新数据的内存空间,该内存空间的大小 = memory_size * memstore_limit_percentage,其中 memory_size 是在创建租户时指定的资源配置参数,限制租户在单个机器上可以使用的内存大小,memstore_limit_percentage 可通过 alter system 设置。

## memstore_limit_percentage (0,100)
alter system set memstore_limit_percentage=50;
触发转储的内存阈值

当租户的 MemStore 内存使用率达到 freeze_trigger_percentage 参数的值, 会触发转储。

当转储的次数已经达到了major_compact_trigger/minor_freeze_times 参数的值,会触发合并。

## freeze_trigger_percentage (0,100)
## major_compact_trigger [0,65535)
alter system set freeze_trigger_percentage=70;
alter system set major_compact_trigger=1;
内存泄漏跟踪

通过设置配置项leak_mod_to_check可以跟踪指定"mod、租户"内存分配的堆栈信息,帮助定位内存泄漏问题。具体内容见3.2节。

指定租户的指定ctx内存限制

一些模块的内存不受限制或者有内存泄漏,最终打爆了租户内存,影响稳定性。为了限制这些内存的使用,需要提供对这些内存进行限制的功能。

## 指定租户tenant_name下的ctx_name的可用内存为1000M
alter system set _ctx_memory_limit = 'ctx_name:1000' tenant = 'tenant_name';
## 取消ctx的内存限制
alter system set _ctx_memory_limit = '' tenant ='tenant_name';

4.2 虚拟表

4.2.1 __all_virtual_memory_info

功能:查询observer内部mod级别的内存统计信息

各列描述:

列名 类型 含义
tenant_id bigint(20) 租户ID
svr_ip varchar:MAX_IP_ADDR_LENGTH 副本所在机器的IP地址
svr_port bigint(20) 副本所在机器的端口号
ctx_id bigint(20) CTX ID
label varchar:OB_MAX_CHAR_LENGTH mod名称
ctx_name varchar:OB_MAX_CHAR_LENGTH CTX 名称
mod_name varchar:OB_MAX_CHAR_LENGTH mod名称
zone varchar:OB_MAX_CHAR_LENGTH 副本所属的zone
hold bigint(20) mod实际占用内存
used bigint(20) mod实际使用内存
count bigint(20) mod的内存分配次数

4.2.2 __all_virtual_tenant_ctx_memory_info

功能:查询observer内部context级别的内存统计信息

各列描述:

列名 类型 含义
tenant_id bigint(20) 租户ID
svr_ip varchar:MAX_IP_ADDR_LENGTH 副本所在机器的IP地址
svr_port bigint(20) 副本所在机器的端口号
ctx_id bigint(20) CTX ID
ctx_name varchar:OB_MAX_CHAR_LENGTH CTX 名称
hold bigint(20) CTX实际占用内存
used bigint(20) CTX实际使用内存
limit bigint(20) CTX的内存上限

4.2.3 __all_virtual_tenant_memory_info

功能:查询observer内部租户级别的内存统计信息

各列描述:

列名 类型 含义
tenant_id bigint(20) 租户ID
svr_ip varchar:MAX_IP_ADDR_LENGTH 副本所在机器的IP地址
svr_port bigint(20) 副本所在机器的端口号
hold bigint(20) 租户实际占用内存
limit bigint(20) 租户的内存上限

4.2.4 __all_virtual_malloc_sample_info

功能:查询各mod内存分配的堆栈信息,有助于定位内存泄漏

各列描述:

列名 类型 含义
svr_ip varchar:MAX_IP_ADDR_LENGTH IP地址
svr_port bigint(20) 端口号
tenant_id bigint(20) 租户ID
ctx_id bigint(20) CTX ID
mod_name varchar:OB_MAX_CHAR_LENGTH 模块名称
back_trace varchar:DEFAULT_BUF_LENGTH 内存分配的堆栈
ctx_name varchar:OB_MAX_CHAR_LENGTH CTX 名称
alloc_count bigint(20) 内存分配次数
alloc_bytes bigint(20) 内存分配的总大小

4.3 其它内存相关配置

4.3.1 配置sys租户资源

RootServer 会在集群自举过程中创建sys租户。在没有RootServer的机器上,ObServer会通过ObMultiTenant 创建隐藏sys 租户。

memory_size缺省值为实际生效的_hidden_sys_tenant_memory。

max_cpu 和 min_cpu的缺省值分别为server_cpu_quota_max,server_cpu_quota_min,默认值都为1。

此外,实体sys租户与普通租户一样有unit,是一个正常的租户,可以通过alter resource修改租户规格大小。

## 实体sys租户
alter resource unit sys_unit_config max_cpu=10,min_cpu=10,memory_size='2G';

4.3.2 内存碎片清理

内存碎片是由于频繁的申请、释放、拆分chunk形成的非连续空闲内存,内存碎片会导致系统内存不足时无法申请大块内存,进一步造成服务不可用。因此,OceanBase数据库设计了两种清理内存碎片的机制:

所属context、tenant或者observer占用的内存达到上限导致内存分配失败,observer会主动清理所属租户的全部内存碎片。

用户在系统租户下可以执行SQL指令主动清理整个observer的内存碎片:

alter system wash memory fragmentation;
相关推荐
OceanBase数据库官方博客1 天前
OceanBase 中常用的查询语句
sql·oceanbase·分布式数据库·查询语句
OceanBase数据库官方博客4 天前
如何解决JAVA程序通过obloader并发导数导致系统夯住的问题 | OceanBase 运维实践
java·运维·oceanbase·分布式数据库
OceanBase数据库官方博客4 天前
如何配置 Flink CDC 连接 OceanBase 实现数据实时同步
大数据·flink·oceanbase·分布式数据库
OceanBase数据库官方博客4 天前
如何实现主备租户的无缝切换 | OceanBase应用实践
oceanbase·分布式数据库·高可用
靖顺6 天前
【OceanBase 诊断调优】—— ocp上针对OB租户CPU消耗计算逻辑
oceanbase
一名数据库爱好者6 天前
OceanBase 闪回查询
数据库·oceanbase·dba
OceanBase数据库官方博客6 天前
ODC 如何精确呈现SQL耗时 | OceanBase 开发者工具解析
sql·oceanbase·分布式数据库·开发者·生态工具
一名数据库爱好者7 天前
OceanBase单表恢复(4.2.1.8)
adb·oceanbase
靖顺7 天前
【OceanBase 诊断调优】—— OceanBase 数据库统计信息被禁用,状态为 broken 的原因和解决方法
数据库·oceanbase
OceanBase数据库官方博客10 天前
如何在 Ubuntu 上 部署 OceanBase
ubuntu·oceanbase·分布式数据库·安装部署