小说网站开发实录,网站开发吧,有网站源码怎么做网站,一起做网店网站哪里进货的本文主要介绍innodb的内存管理#xff0c;涉及基础的内存分配结构、算法以及buffer pool的实现细节#xff0c;提及change buffer、自适应hash index和log buffer的基本概念和内存基本配比#xff0c;侧重点在内存的分配和管理方式。本文所述内容基于mysql8.0版本。基础内存…本文主要介绍innodb的内存管理涉及基础的内存分配结构、算法以及buffer pool的实现细节提及change buffer、自适应hash index和log buffer的基本概念和内存基本配比侧重点在内存的分配和管理方式。本文所述内容基于mysql8.0版本。基础内存分配在5.6以前的版本中innodb内部实现了除buffer pool外的额外内存池那个时期lib库中的分配器在性能和扩展性上表现比较差缺乏针对多核系统优化的内存分配器像linux下最通用的ptmalloc的前身是Doug Lea Malloc也是因为不支持多线程而被弃用了。所以innodb自己实现了内存分配器使用额外的内存池来响应那些原本要发给系统的内存请求用户可以通过设置参数innodb_use_sys_malloc 来选择使用innodb的分配器还是系统分配器使用innodb_additional_mem_pool_size参数设定额外内存池的大小。随着多核系统的发展一些分配器对内部实现进行了优化和扩展已经可以很好的支持多线程相较于innodb特定的内存分配器可以提供更好的性能和扩展性所以这个实现在5.6版本已经弃用5.7版本删除。本文所讨论的内容和涉及到的代码基于mysql8.0版本。innodb内部封装了基础分配释放方式malloc,free,calloc,new,delete等在开启pfs模式下封装内部加入了内存追踪信息使用者可传入对应的key值来记录某个event或者模块的内存分配信息这部分信息通过pfs内部表来对外展示以便分析内存泄漏、内存异常等问题。innodb内部也提供了对系统基础分配释放函数封装的allocator用于std::*容器内部的内存分配可以让这些容器内部的隐式分配走innodb内部封装的接口以便于内存信息的追踪。基础的malloc/calloc等封装后也会走allocator的接口。基础封装非UNIV_PFS_MEMORY编译模式
#define UT_NEW(expr, key) ::new (std::nothrow) expr
#define UT_NEW_NOKEY(expr) ::new (std::nothrow) expr
#define UT_DELETE(ptr) ::delete ptr
#define UT_DELETE_ARRAY(ptr) ::delete[] ptr
#define ut_malloc(n_bytes, key) ::malloc(n_bytes)
#define ut_zalloc(n_bytes, key) ::calloc(1, n_bytes)
#define ut_malloc_nokey(n_bytes) ::malloc(n_bytes)
...打开UNIV_PFS_MEMORY
#define UT_NEW(expr, key) ::new (ut_allocatorbyte(key).allocate(sizeof expr, NULL, key, false, false)) expr
#define ut_malloc(n_bytes, key) static_castvoid *(ut_allocatorbyte(key).allocate( n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, false, false))#define ut_zalloc(n_bytes, key) static_castvoid *(ut_allocatorbyte(key).allocate( n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, true, false))#define ut_malloc_nokey(n_bytes) static_castvoid *( ut_allocatorbyte(PSI_NOT_INSTRUMENTED) .allocate(n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, false, false))...
可以看到在非UNIV_PFS_MEMORY编译模式下直接调用系统的分配函数忽略传入的key而UNIV_PFS_MEMORY编译模式下使用ut_allocator分配下面有ut_allocator的介绍比较简单的封装。memory heap主要管理结构为mem_heap_t8.0中的实现比较简单内部维护block块的链表包含指向链表开始和尾部的指针可以快速找到链表头部和尾部的节点每个节点都是mem_heap_t的结构。mem_heap在创建的时候会初始一块内存作为第一个block大小可由使用者指定。mem_heap_alloc响应基本的内存分配请求先尝试从block中切分出满足请求大小的内存如果不能满足则创建一个新的block新的block size至少为上一个block的两倍(last block)直到达到规定的上限值新创建的block总是链到链表的尾部。mem_heap_t中记录了block的size和链表中block的总size以及分配模式(type)等信息基本结构如下图innodb在内部定义了三种分配block模式供选择:MEM_HEAP_DYNAMIC 使用malloc动态分配比较通用的分配模式MEM_HEAP_BUFFER size满足一定条件使用buffer pool中的内存块MEM_HEAP_BTR_SEARCH 保留额外的内存地址保存在free_block中 一般的使用模式为MEM_HEAP_DYNAMIC也可以使用b|c的模式在某些情况下使用free_block中的内存。mem heap链表中的block在最后统一free按照分配模式走不同的free路径。基本思想也是使用一次性分配大内存块再从大内存块中切分来响应小内存分配请求以避免多次调用malloc/free减少overhead。实现上比较简单内部只是将多个大小可能不一的内存块使用链表链起来大多场景只有一个block。没有内存归还、复用和合并等机制使用过程中不会将内存free会有一定程度的内存浪费但有效减少了内存碎片比较适用于短周期多次分配小内存的场景。基础allocatorut_allocatorinnodb内部提供ut_allocator用来为std::* 容器分配内存内部封装了基础的内存分配释放函数以便于内存追踪和统一管理。ut_allocator提供基本的allocate/deallocate的分配释放函数用于分配n个对象所需内存内部使用malloc/free。另外也提供了大内存块的分配开启LINUX_LARGE_PAGES时使用HugePage在服务器内存比较大的情况可以减少页表条目提高检索效率未开启时使用mmap/munmap内存映射的方式。更多细节见如下代码/** Allocator class for allocating memory from inside std::* containers. */
template class T
class ut_allocator {// 分配n个elements所需内存大小内部使用malloc/calloc分配// 不开启PFS_MEMORY分配n_elements * sizeof(T)大小的内存// 开启PFS_MEMORY多分配sizeof(ut_new_pfx_t)大小的内存用于信息统计pointer allocate(size_type n_elements, const_pointer hint NULL,PSI_memory_key key PSI_NOT_INSTRUMENTED,bool set_to_zero false, bool throw_on_error true);// 释放allocated()分配的内存内部使用free释放 void deallocate(pointer ptr, size_type n_elements 0)// 分配一块大内存如果开启了LINUX_LARGE_PAGES使用HugePage// 否则linux下使用mmappointer allocate_large(size_type n_elements, ut_new_pfx_t *pfx)// 对应allocate_largelinux下使用munmapvoid deallocate_large(pointer ptr, const ut_new_pfx_t *pfx)// 提供显示构造/析构funcvoid construct(pointer p, const T val) { new (p) T(val); }void destroy(pointer p) { p-~T(); }开启PFS_MEMORY场景下提供更多可用func:pointer reallocate(void *ptr, size_type n_elements, PSI_memory_key key);// allocateconstruct的封装, 分配n个单元的内存并构造相应对象实例pointer new_array(size_type n_elements, PSI_memory_key key);// 同上deallocatedestroy的封装void delete_array(T *ptr);
}mem_heap_allocatormem_heap_t的封装可以作为stl allocator来使用内部使用mem_heap_t的内存管理方式。/** A C wrapper class to the mem_heap_t routines, so that it can be used
as an STL allocator */
template typename T
class mem_heap_allocator {mem_heap_allocator(mem_heap_t *heap) : m_heap(heap) {}pointer allocate(size_type n, const_pointer hint 0) {return (reinterpret_castpointer(mem_heap_alloc(m_heap, n * sizeof(T))));}void deallocate(pointer p, size_type n) {}// 提供显示构造/析构funcvoid construct(pointer p, const T val) { new (p) T(val); }void destroy(pointer p) { p-~T(); }private:mem_heap_t *m_heap;
}buddyinnodb支持创建压缩页以减少数据占用的磁盘空间支持1K, 2K, 4K 、8K和16k大小的page。buddy allocator用于管理压缩page的内存空间提高内存使用率和性能。buffer pool中
UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX];/** Struct that is embedded in the free zip blocks */
struct buf_buddy_free_t {union {ulint size; /*! size of the block */byte bytes[FIL_PAGE_DATA];} stamp;buf_page_t bpage; /*! Embedded bpage descriptor */UT_LIST_NODE_T(buf_buddy_free_t) list;
};伙伴系统也是比较经典的内存分配算法也是linux内核用于解决外部碎片的一种手段。innodb的实现在算法上与buddy的基本实现并无什么区别所支持最小的内存块为1k(2^10)最大为16k每种内存块维护一个链表多种内存块链表组成了zip_free链表。分配入口在buf_buddy_alloc_low先尝试从zip_free[i]中获取所需大小的内存块如果当前链表中没有则尝试从更大的内存块链表中获取获取成功则进行切分一部分返回另一块放入对应free链表中实际上是buf_buddy_alloc_zip的一个递归调用只是传入的i不断增加。如果一直递归到16k的块都没法满足则从buffer pool中新申请一块大内存块并将其按照伙伴关系进行(比如现分配了16需要2k先切分8k,8k再将其中一个8k切分为4k,4k再将其中4k切分为2k,2k)切分直到满足分配请求。释放入口在buf_buddy_free_low为了避免碎片在释放的时候多做了一些事情。在释放一个内存块的时候没有直接放回对应链表中而是先查看其伙伴是不是free的如果是则进行合并再尝试对合并后的内存块进行合并。如果其伙伴是在USED的状态这里做了一次relocate操作将其内容拷贝到其它free的block块上再进行对它合并。这种做法有效减少了碎片的存在但拷贝这种操作也降低了性能。buffer poolbuffer pool是innodb主内存中一块区域用于缓存主表和索引中的数据读线程可以直接从buffer pool中读取相应数据从而避免io提升读取性能当一个页面需要修改时先在buffer pool中进行修改另有后台线程来负责刷脏页。一般在专用服务器中会将80%的内存分配给buffer pool使用。数据库启动时就会将内存分配给buffer pool不过内存有延迟分配的优化这部分内存在未真正使用前是没有进行物理映射的所以只会影响虚存大小。buffer pool的内存在运行期间不会收缩还给系统在数据库关闭时将这部分内存统一释放。可以设置多个buffer pool实例。buffer pool中使用chunk内存块来管理内存每个buffer pool实例包含一个或多个chunkchunks在buffer pool初始化时使用mmap分配并初始化为多个block。每个block地址相差UNIV_PAGE_SIZEUNIV_PAGE_SIZE一般是16kb这块内存包含了page相关控制信息和真正的数据page两部分之后将这些page加入free list中供使用。这里直接使用了mmap而非malloc是因为在glibc的ptmalloc内存分配器中大于MMAP_THRESHOLD阀值的内存请求也是使用mmapMMAP_THRESHOLD默认值是128kbuffer pool的配置大小一般会远大于128k。数据结构buffer pool进行内存管理的主要数据结构。buf_pool_t控制buffer pool的主结构内部包含多种逻辑链表以及相关锁信息、统计信息、hash table、lru和flush算法相关等信息。struct buf_pool_t {//锁相关 BufListMutex chunks_mutex; /*! protects (de)allocation of chunks*/BufListMutex LRU_list_mutex; /*! LRU list mutex */BufListMutex free_list_mutex; /*! free and withdraw list mutex */BufListMutex zip_free_mutex; /*! buddy allocator mutex */BufListMutex zip_hash_mutex; /*! zip_hash mutex */ib_mutex_t flush_state_mutex; /*! Flush state protection mutex */BufPoolZipMutex zip_mutex; /*! Zip mutex of this buffer */// index、各种size、数量统计ulint instance_no; /*! Array index of this buffer pool instance */ulint curr_pool_size; /*! Current pool size in bytes */ulint LRU_old_ratio; /*! Reserve this much of the buffer pool for old blocks */
#ifdef UNIV_DEBUGulint buddy_n_frames;
#endifut_allocatorunsigned char allocator; // 用于分配chunksvolatile ulint n_chunks; /*! number of buffer pool chunks */volatile ulint n_chunks_new; /*! new number of buffer pool chunks */buf_chunk_t *chunks; /*! buffer pool chunks */buf_chunk_t *chunks_old; ulint curr_size; /*! current pool size in pages */ulint old_size; /*! previous pool size in pages */// hash table 用于索引相关数据页page_no_t read_ahead_area; hash_table_t *page_hash; hash_table_t *page_hash_old; hash_table_t *zip_hash; // 统计信息相关ulint n_pend_reads; ulint n_pend_unzip; time_t last_printout_time;buf_buddy_stat_t buddy_stat[BUF_BUDDY_SIZES_MAX 1];buf_pool_stat_t stat; /*! current statistics */buf_pool_stat_t old_stat; /*! old statistics */// flush相关BufListMutex flush_list_mutex; FlushHp flush_hp; UT_LIST_BASE_NODE_T(buf_page_t) flush_list;ibool init_flush[BUF_FLUSH_N_TYPES];ulint n_flush[BUF_FLUSH_N_TYPES];os_event_t no_flush[BUF_FLUSH_N_TYPES];ib_rbt_t *flush_rbt; ulint freed_page_clock; ibool try_LRU_scan; lsn_t track_page_lsn; /* Pagge Tracking start LSN. */lsn_t max_lsn_io;UT_LIST_BASE_NODE_T(buf_page_t) free;UT_LIST_BASE_NODE_T(buf_page_t) withdraw;ulint withdraw_target; // lru 相关LRUHp lru_hp;LRUItr lru_scan_itr;LRUItr single_scan_itr;UT_LIST_BASE_NODE_T(buf_page_t) LRU;buf_page_t *LRU_old; ulint LRU_old_len; UT_LIST_BASE_NODE_T(buf_block_t) unzip_LRU;
#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUGUT_LIST_BASE_NODE_T(buf_page_t) zip_clean;
#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX];buf_page_t *watch;
};buf_page_t数据页的控制信息包含数据页的大部分信息page_id、size、引用计数(io,buf)access_time(用于lru调整)page_state以及压缩页的一些信息。class buf_page_t {public:page_id_t id;page_size_t size;uint32_t buf_fix_count;buf_io_fix io_fix;buf_page_state state;the flush_type. see buf_flush_t */unsigned flush_type : 2;unsigned buf_pool_index : 6;page_zip_des_t zip;
#ifndef UNIV_HOTBACKUPbuf_page_t *hash;
#endif /* !UNIV_HOTBACKUP */
#ifdef UNIV_DEBUGibool in_page_hash; /*! TRUE if in buf_pool-page_hash */ibool in_zip_hash; /*! TRUE if in buf_pool-zip_hash */
#endif /* UNIV_DEBUG */UT_LIST_NODE_T(buf_page_t) list;
#ifdef UNIV_DEBUGibool in_flush_list; ibool in_free_list;
#endif /* UNIV_DEBUG */FlushObserver *flush_observer; /*! flush observer */lsn_t newest_modification;lsn_t oldest_modification;UT_LIST_NODE_T(buf_page_t) LRU;
#ifdef UNIV_DEBUGibool in_LRU_list;
#endif /* UNIV_DEBUG */
#ifndef UNIV_HOTBACKUPunsigned old : 1; unsigned freed_page_clock : 31; unsigned access_time;
#ifdef UNIV_DEBUGibool file_page_was_freed;
#endif /* UNIV_DEBUG */
#endif /* !UNIV_HOTBACKUP */
};逻辑链表free list包含空闲的pages当需要从buffer pool中分配空闲块时从free list中摘取当free list为空时需要从LRU、unzip_LRU或者flush list中进行淘汰或刷脏以填充free list。buffer pool初始化时创建多个chunks划分的pages都加入free list中待使用。LRU list最重要的链表也是缓存占比最高的链表。buffer pool使用lru算法来管理缓存的数据页所有从磁盘读入的数据页都会先加入到lru list中也包含压缩的page。新的page默认加入到链表的3/8处(old list)等待下次读取并满足一定条件后再从old list加入到young list中以防止全表扫描污染lru list。需要淘汰时从链表的尾部进行evict。unzip_LRU list是LRU list的一个子集每个节点包含一个压缩的page和指向对应解压后的page的指针。flush list 已修改过还未写到磁盘的page list按修改时间排序带有最老的修改的page在链表的最尾部。当需要刷脏时从flush list的尾部开始遍历。zip_clean list包含从磁盘读入还未解压的pagepage一旦被解压就从zip_clean list中删除并加入到unzip_LRU list中。zip_free用于buddy allocator的空闲block listbuddy allcator是专门用于压缩的page(buf_page_t)和压缩的数据页的分配器。主要操作初始化buffer poolbuffer pool在db启动时调用buffer_pool_create进行初始化使用mmap创建配置大小的虚拟内存并划分为多个chunks其它字段基本使用calloc分配(memset)。buf_page_get_gen从buffer pool中读取page比较重要的操作。通过page_id获取对应的page如果buffer pool中有这个page则封装一些信息后返回如果没有则需要从磁盘中读入。读取模式分为以下7种NORMAL在page hash table中查找(hash_lock s模式)如果找到增加bufferfix cnt并释放hash_lock如果该page不在buffer pool中则以sync模式从磁盘中读取并加入对应的逻辑链表中判读是否需要线性预读。如果第一次访问buffer pool中的该page设置访问时间并判断是否需要线性预读。判断是否需要加入到young list中。SCAN如果page不在buffer pool中使用异步读取磁盘的模式不做随机预读和线性预读不设置访问时间不加入young list其它与normal一样。IF_IN_POOL只在buffer pool中查找page如果没有找到则返回NOT_FOUND。PEEK_IF_IN_POOL这种模式仅仅用来drop 自适应hash index跟IF_IN_POOL类似只是不加入young list不做线性预读。NO_LATCH读取并设置buffefix但是不加锁。IF_IN_POOL_OR_WATCH与IF_IN_POOL类似只在buffer pool中查找此page如果没有则设置watch。POSSIBLY_FREED与normal类似只是允许执行过程中page被释放。buf_LRU_get_free_block从buffer pool的free list中摘取一个空闲页如果free list为空移除lru链表尾部的block到free list中。这个函数主要是用于用户线程需要一个空闲block来读取数据页。具体操作如下如果free list不为空从中摘取并返回。否则转下面的操作。如果buffer pool设置了try_LRU_scan遍历lru链表尝试从尾部释放一个空闲块加入free list中。如果unzip_LRU链表不为空则先尝试从unzip_LRU链表中释放。如果没有找到再从lru链表中淘汰。如果没有找到则尝试从lru中flush dirty page并加入到free list中。没有找到设置scan_all重复上述过程与第一遍不同的地方在于需要scan整个lru链表。如果遍历了整个lru链表依然没有找到可以淘汰的block则sleep 10s等待page cleaner线程做一批淘汰或者刷脏。重复上述过程直到找到一个空闲block。超过20遍设置warn信息。page cleaner系统线程负责定期清理空闲页放入free list中和flush dirty page到磁盘上dirty page指那些已经被修改但是还未写到磁盘上的数据页。使用innodb_page_cleaners 参数设定page cleaner的线程数默认是4。通过特定参数控制dirty page所占buffer pool的空间比维持在一定水位下默认是10%。flush list上面章节提到过flush_list是包含dirty page并按修改时间有序的链表在刷脏时选择从链表的尾部进行遍历淘汰代码主体在buf_do_flush_list_batch中。这里不得不提的一个巧妙的操作叫作Hazard Pointerbuf_pool_t中的flush_hp将整体遍历复杂度由最差O(n*n)降到了O(n)。之所以复杂度最差会变为O(n*n)是由于flush list允许多个线程并发刷脏每次从链表尾部进行遍历使用异步io的方式刷盘在io完成后将page从链表中摘除每次提交异步io后从链表尾部再次扫描在刷盘速度比较慢的情况下可能每次都需要跳过之前已经flush过的page最差会退化为O(n*n)。flush_hp用于刷脏遍历flush list过程flush_list_mutex保护下修改在处理当前page之前将hazard pointer设置为下一个要遍历的buf_page_t的指针为线程指定下一个需要处理的page。当前page刷盘时会释放flush_list_mutex刷盘完成后重新获得锁处理flush_hp指向的page无论这中间发生过什么链表如何变动flush_hp总是被设置成为下一个有效的buf_page_t指针。所以复杂度总能保证为O(n)。刷脏的过程中也做了一些优化代码在buf_flush_page_and_try_neighbors中可以将当前page相邻的dirty page页也一起刷盘目的是将多个随机io转为顺序io减少overhead这在传统HHD的设备上比较有用在SSD上seek time已经不是显著的影响因素。可以使用参数innodb_flush_neighbors进行设置和关闭为0则关闭flush neighbors的优化默认值为1flush与当前page相同extent(1M)上的连续dirty page为2则flush与当前page相同extent上的dirty pageflush LRU listbuf_flush_LRU_list主要完成两件事将lru list尾部的可以移除的pages放入到free list中将lru list尾部的dirty page刷到磁盘。同一时刻只会有一个page cleaner线程对同一个LRU list操作。lru list遍历的深度由动态参数innodb_lru_scan_depth决定用于优化io敏感的场景默认值1024设置比默认值小的值可以适应大多数work load设置过大会影响性能尤其是在buffer pool足够大的情况。操作过程在LRU_list_mutex的保护下代码主体在buf_do_LRU_batch中。其中涉及到unzip_LRU list和LRU list中的淘汰unzip_LRU list不一定每次都会做淘汰操作衡量内存大小和负载情况只有在size超出buffer pool的1/10以及当前负载为io bound的情况才会做。代码主体在buf_free_from_unzip_LRU_list_batch中 将uncompressed page移除到free list中并不会将任何数据刷盘只是将解压缩的frames与压缩page分离。如果在上述操作后仍无法达到需要释放的page数量(遍历深度)则继续从lru list尾部进行遍历操作在buf_flush_LRU_list_batch中lru list的遍历同样使用了Hazard Pointerbuf_pool_t中的lru_hp。当前page如果是clear并且没有被io fixed和buffer fixed则从lru list中移除并加入free list中否则如果page是已经修改过的并且满足flush的条件则对其进行刷脏。小结innodb设定了buffer pool的总大小空闲page不够用时会将lru链表中可替换的页面移到free list中根据统计信息估计负载情况来决定淘汰的策略。所有的block在几种状态之间进行转换unzip_LRU、flush list设置一定的上限设置多个影响淘汰和刷脏策略的参数以达到不同负载不同buffer pool size下的性能和内存之间的平衡。change bufferchange buffer是用于缓存不在buffer pool中的二级索引页改动的数据结构insert、update或者delete这些DML操作会引起buffer的改变page被读入时与change buffer中的修改合并加入buffer pool中。引入buffer pool的目的主要减少随机io对于二级索引的更新经常是比较随机的当页面不在buffer pool中时将其对应的修改缓存在change buffer中可有效地减少磁盘的随机访问。可以通过参数 innodb_change_buffering 设置对应缓存的操作all, none, inserts, deletes, changes(insertsdeletes), purges。change buffer的内存也是buffer pool中的一部分可以通过参数innodb_change_buffer_max_size来设置内存占比默认25%最多50%。Adaptive Hash Indexinnodb的索引组织结构为btree当查询的时候会根据条件一直索引到叶子节点为了减少寻路的开销AHI使用索引键的前缀建立了一个哈希索引表在实现上就是多个个hash_tables(分片)。哈希索引是为那些频繁被访问的索引页而建立的可以理解为btree上的索引。看代码初始创建的数组大小为buf_pool_get_curr_size() / sizeof(void *) / 64其实是比较小的一块内存使用malloc分配。log bufferlog buffer是日志未写到磁盘的缓存大小由参数innodb_log_buffer_size指定一般来说这块内存都比较小默认是16M。在有大事务的场景下在事务未commited之前可以将redo日志数据一直缓存避免多次写磁盘可以将log buffer调大。参考资料https://dev.mysql.com/doc/refman/5.6/en/innodb-performance-use_sys_malloc.htmlhttps://dev.mysql.com/doc/refman/8.0/en/innodb-in-memory-structures.html