浅述InnoDB与PostgreSQL的动态内存管理
原文链接:http://blog.inkernel.info/archives/59.html
我们都知道在服务器程序的编写当中,内存管理是非常重要的一块。一方面,直接使用malloc从程序的堆中申请内存会有性能问题,另一方面,直接向堆申请内存也可能在程序有bug的时候使得调试的时候相当棘手。更为重要的是,即使小心翼翼,杜绝了内存泄漏,但是长时间大量地分配小对象,也可能会产生内存碎片,使得原本连续的地址空间变得离散,那么以后分配大块内存的时候就可能分配不出来,导致程序奔溃,这是服务器程序非常大的隐患。
因此,大部分优秀的开源服务器程序都有自己的一套内存管理方法,以避免内存泄漏以及内存碎片,提高程序内存分配效率,便于检测内存越界读写等错误。在数据库服务器中,一条语句的执行需要分配很多内存,包括语法解析,查询优化,记录提取与转化等等,因此内存的管理也非常重要。本文主要讨论InnoDB与PostgreSQL中用于程序内部对象的动态内存的不同管理方法。(注:不涵盖数据库缓存的管理。数据库的缓存管理是另一个重要的部分,其中包括检查点,缓存页替换,页面预读等等,这里对此将不展开讨论,虽然InnoDB在通用内存池不足时也可能会使用一部分缓存池的内存。)
InnoDB的动态内存管理
InnoDB的内存管理当中使用内存堆(Memory Heap)的概念,一个内存堆是用链表组织的一群内存块, 相对比较简单。
在InnoDB当中有这样几种内存分配方式:
- MEM_HEAP_DYNAMIC: 动态分配
- MEM_HEAP_BUFFER: 缓存分配
- MEM_HEAP_BTR_SEARCH: 须和MEM_HEAP_BUFFER一起使用的标志位,供InnoDB的自适应哈希索引使用。
不同内存分配方式细节:
- MEM_HEAP_DYNAMIC
MEM_HEAP_DYNAMIC方式是最常用的分配方式。MEM_HEAP_DYNAMIC先从通用内存池(common memory pool)中分配,如果分配失败,则直接从操作系统中malloc。一次分配最少为64个字节(MEM_BLOCK_START_SIZE), 以后每次分配内存大小为上一次分配的内存块大小的两倍,直到达到上限MEM_BLOCK_STANDARD_SIZE之后每次继续分配的块大小就保持这个上限不变。当然,如果调用方一次申请的大小超过了这个上限,还是会按照申请的大小分配内存块返回给调用方。 通用内存池的内存管理使用伙伴算法(buddy algorithm), 这也是比较古老并且有效的动态内存管理方法。
- MEM_HEAP_BUFFER
MEM_HEAP_BUFFER方式从数据库的Buffer Pool中分配内存,由于数据库的Buffer Pool都是在系统启动初始化的时候就预分配好,因此这种分配方式的好处回收非常快。一次从Buffer Pool中分配的内存的最大大小(MEM_MAX_ALLOC_IN_BUF)受限于Buffer的页面大小,在InnoDB当中定义为Buffer页面大小(UNIV_PAGE_SIZE)减200个字节。
在debug版本的内存管理中,InnoDB还给分配的块增加了额外的头部和尾部, 头部包含块大小和一个魔数,尾部则包含和头部同样的魔数,这样,如果发生越界写错误,可以比较方便检测到。所有已经分配的堆还会组织成哈希表,以检测堆的重复释放。
PostgresSQL的动态内存管理
在PostgresSQL当中,内存管理主要使用内存分配上下文(Memory Context), 内存分配上下文的概念由Stonebraker提出, 后面又经过了多次改进。Memory Context跟InnoDB的Memory Heap有些相似的地方, 但更加复杂,使用起来也比较麻烦。Memory Context根据模拟数据库当中不同Storage的生命周期(比如事务级,语句级,记录级等)来动态管理内存。
它是一个分级的树状结构,有父亲结点, 也有子结点。当创建新的Memory Context的时候,可以将它指定为已经存在的某个Memory Context的子结点。所有的这些Memory Context不一定组成单棵树,也可能组成由多棵树构成的森林。Memory Context的一个优点是可以多次申请,一次回收,也可以只是进行重置,而不进行物理回收,这样已经分配的内存就可以重复使用,这种管理方式很适合数据库内部使用,在事务结束的时候可以统一对内存进行回收。在树状的结构中,当重置或回收一个结点对应的Memory Context时,也会重置或回收其子孙结点对应的Memory Context,这样就可以管理很多个Memory Context而不用担心会有内存泄漏。
PostgreSQL支持根据不同的使用场景自定义内存分配策略,只需要遵循相同的外部行为即可。这可以通过设置MemoryContextData结构中的MemoryContextMethods实现,MemoryContextMethods实际上是一组函数指针:
1 typedef struct MemoryContextMethodsData
2 {
3 Pointer (*alloc) (MemoryContext c, Size size);
4 void (*free_p) (Pointer chunk);
5 Pointer (*realloc) (Pointer chunk, Size newsize);
6 void (*reset) (MemoryContext c);
7 void (*delete) (MemoryContext c);
8 } MemoryContextMethodsData, *MemoryContextMethods;
为统一接口,所有的Memory Context分配的内存块都需要有相同的标准头部:
1 typedef struct StandardChunkHeader
2 {
3 MemoryContext mycontext; /* Link to owning context object */
4 Size size; /* Allocated size of chunk */
5 };
由于提供了这样的定制功能,因此Memory Context的特点还和其具体实现有关系。比如,首次分配的块大小可以是8k,以后每次分配都是上一次分配的块大小的两倍,但是针对一些小型对象的使用,可以减少初始块大小以节省内存。也可以总是至少预留一个块,除非该Memory Context被删除,否则该块只会被重置而不会被回收。
更多Memory Context的细节可参考PostgreSQL的内存管理模块下的REAME,里面有关于TopMemoryContext,CacheMemoryContext等已定义的Context的更详尽的说明。Memory Context的标准实现可参考AllocSetContext。
(本文链接: http://blog.inkernel.info/archives/59.html 转载请注明出处 @Ricky)
posted on 2012-02-29 10:59 Swimming Fish 阅读(816) 评论(0) 编辑 收藏 举报