以下是边看代码边记录的,从磁盘读取一个压缩Page到buffer pool的的全过程,以函数buf_page_get_gen作为入口
buf_page_get_gen
1.根据space和offset来计算请求的page是否已经读到了buffer pool中
fold = buf_page_address_fold(space, offset);
block = (buf_block_t*) buf_page_hash_get_low(
buf_pool, space, offset, fold);
判断:如果这个block所对应表正在被drop掉(使用lazy drop table),则调用buf_LRU_free_block((buf_page_t*)block, TRUE, TRUE)将其从LRU中删除
block->page.space_was_being_deleted在函数buf_LRU_mark_space_was_deleted内被设置
2.if (block && buf_pool_watch_is_sentinel(buf_pool, &block->page))
则将block设置为NULL。
buffer pool的watch[BUF_POOL_WATCH_SIZE];成员暂不清楚其用途,似乎用在insert buffer上,回头专门研究下
是在MySQL5.5.5引入
一个普通读的mode值为BUF_GET,因此以下也会忽略掉不必要的代码流程
3.当page不在bp时,则从磁盘读取该page。
if (buf_read_page(space, zip_size, offset, trx)) {
buf_read_ahead_random(space, zip_size, offset,
ibuf_inside(mtr), trx);
另外还会顺便做一下预读
buf_read_page是读取page的接口函数,其调用栈如下:
a.获取 tablespace_version = fil_space_get_version(space),用于随后判断该space是否正在被删除或者tablespace被DISCARD/IMPORT
b.调用如下函数:
407 count = buf_read_page_low(&err, TRUE, BUF_READ_ANY_PAGE, space,
408 zip_size, FALSE,
409 tablespace_version, offset, trx);
buf_read_page_low
–>bpage = buf_page_init_for_read(err, mode, space, zip_size, unzip,
tablespace_version, offset);
|–>计算page hash中的对应hash值fold = buf_page_address_fold(space, offset);
|–>加buf_pool->LRU_list_mutex和buf_pool->page_hash_latch的x锁
|–>从page hash中读取对应Page,如果已经在bp中,并且该tablespace正在被删除,则删掉该page( buf_LRU_free_block(watch_page, TRUE, TRUE);)
|–>fil_tablespace_deleted_or_being_deleted_in_mem检查tablespace是否正在被删除
|–>data = buf_buddy_alloc(buf_pool, zip_size, &lru, TRUE); 为压缩Page分配内存
|–>分配Page描述符(buf_page_t)及初始化其字段信息
|–>将其插入到page hash中。
HASH_INSERT(buf_page_t, hash, buf_pool->page_hash, fold,
bpage);
|–>插入到LRU LIST的old段buf_LRU_add_block(bpage, TRUE/* to old blocks */);
|–>buf_page_set_io_fix(bpage, BUF_IO_READ);设置bpage->io_fix为BUF_IO_READ,表示将从磁盘读取Page
–> 如果bpage为NULL,则返回;这里触发的bug#43948, Perocna已经移植patch,官方还没修复
–>调用*err = _fil_io(OS_FILE_READ | wake_later,
sync, space, zip_size, offset, 0, zip_size,
bpage->zip.data, bpage, trx);
|–>fil_mutex_enter_and_prepare_for_io(space_id);
|–>mutex_enter(&fil_system->mutex); 加fil_system的互斥锁
|–>如果是系统表空间或者日志文件,直接返回,这些文件总是一直打开的
|–>如果fil_system->n_open < fil_system->max_n_open,即当前打开的innodb文件数小于最大允许数目(由参数innodb_open_files控制),直接返回
|–>如果当前表空间正在被rename(space->stop_ios为true),则retry,直到rename完成
|–>如果文件已经打开,返回
|–>尝试关闭一些文件(fil_try_to_close_file_in_LRU),以保证打开的文件数少于innodb_open_files
|–>依旧大于innodb_open_files,则唤醒IO线程(os_aio_simulated_wake_handler_threads,使用srv_use_native_aio不许要通知)
|–>fil_flush_file_spaces刷新表空间,然后再retry
|–>fil_node_prepare_for_io //如果文件没打开,则将其打开(fil_node_open_file),并且node->n_pending++
|–>mutex_exit(&fil_system->mutex);
|–>根据zip_size计算读文件的偏移量(offset_high,offset_low)
|–>异步读文件
ret = os_aio(type, mode | wake_later, node->name, node->handle, buf,
offset_low, offset_high, len, node, message, space_id, trx);
|–>当mode == OS_AIO_SYNC时,fil_node_complete_io(node, fil_system, type);更新fil_node_t
–>sync为true,则buf_page_io_complete(bpage);
|–>获取bpage->zip.data中记录的space id(偏移量FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID)和page no(偏移量FIL_PAGE_OFFSET),跟bpage中对应字段进行对比
|–>函数buf_page_is_corrupted用于检测page是否损坏
|–>对于非压缩Page,检查存储在Page头和尾部的日志序列号(LSN)是否相同
|–>检查Page中的LSN是否大于当前的LSN
|–>当innodb_checksums打开时,计算checksum并和记录在page中的checksum(偏移量FIL_PAGE_SPACE_OR_CHKSUM)做比较
|–>对于压缩表,调用page_zip_calc_checksum来计算checksum,
|–>对于非压缩表:
a.老版本的innodb:buf_calc_page_old_checksum
b.当前默认:buf_calc_page_new_checksum
c.开启innodb_fast_checksum:buf_calc_page_new_checksum_32
|–>buf_page_set_io_fix(bpage, BUF_IO_NONE);
c.如果有必要的话,刷buffer pool的LRU LIST,调用buf_flush_free_margin(buf_pool, TRUE);
buf_flush_free_margin 用于刷新LRU LIST的脏页,调用栈如下:
|–>n_to_flush = buf_flush_LRU_recommendation(buf_pool)//计算flush的page数以保证有足够可替换的block
|–>n_flushed = buf_flush_LRU(buf_pool, n_to_flush)
|–>buf_flush_wait_batch_end
这部分代码后面单独再分析
4.调用预读函数buf_read_ahead_random,如果设置了innodb_random_read_ahead会进行预读
5.goto loop && block = (buf_block_t*) buf_page_hash_get_low(buf_pool, space, offset, fold);
6.switch (buf_block_get_state(block))
case BUF_BLOCK_ZIP_PAGE:
case BUF_BLOCK_ZIP_DIRTY:
—>如果当前bpage是buffer-fixed or I/O-fixed,则sleep一段时间,goto loop
—>block = buf_LRU_get_free_block(buf_pool);从buffer pool的free list(或者lru尾)获取一个空闲block
a.free+lru的长度<buf_pool->curr_size / 20,直接断言错误,lock heaps或者adpative hash index占用太多的buffer pool
(关于Innodb内存分配管理,后续分析的点)
b.free+lru<buf_pool->curr_size / 3,打印warning
c.block = buf_LRU_get_free_only(buf_pool); 从free list上找一个空闲block
d.如果free list上没有空闲的,则调用buf_LRU_search_and_free_block从LRU上获取
|–>首先尝试unzip__lru上释放一个Uncompressed page,调用函数buf_LRU_free_from_unzip_LRU_list
|–>判断是否可以从unzip_lru上释放block(buf_LRU_evict_from_unzip_LRU)
|–>从unzip_lru上释放一个block,调用freed = buf_LRU_free_block(&block->page, FALSE, have_LRU_mutex);第二个参数为false表示不释放压缩Page.
|–>如果没有的话,再调用buf_LRU_free_from_common_LRU_list从LRU上释放一个clean page
e.找不到空闲blcok,尝试刷LRU LIST buf_flush_free_margin(buf_pool, TRUE) 并将其移动到free list上(buf_LRU_try_free_flushed_blocks)
—>检查
a.刚刚读入的page是否被修改了,如果被修改了,则释放(buf_LRU_block_free_non_file_page)并重新loop2.
b.该page是否是buffer-fix 或 io-fix,是的话,则goto wait_until_unfixed
—>将compressed page memcpy到刚刚分配的block中,调用函数buf_relocate(bpage, &block->page);
a.memcpy(dpage, bpage, sizeof *dpage);
b.将bpage从LRU移除,并重新插入dpage
c.将bpage从Page hash中移除,并重新插入dpage
—>插入到unzip_lru头部:
buf_unzip_LRU_add_block(block, FALSE);
buf_block_set_io_fix(block, BUF_IO_READ);
—>释放bpage(已经memcpy到block->page中),buf_page_free_descriptor(bpage);
—>解压文件
success = buf_zip_decompress(block, FALSE); //这里我们把第二个参数调整为FALSE,表示在解压时不做checksum验证
—>调用ibuf_merge_or_delete_for_page执行该page上所有缓存的操作,并删除对应的insert buffer记录
然后设置io_fix:buf_block_set_io_fix(block, BUF_IO_NONE);
7.buf_page_set_accessed_make_young
8.第一次读取page(access_time=0),执行一次buf_read_ahead_linear
遗留的问题:
1.innodb如何进行预读,可以看到有两次预读,需要了解下(一次random read ahead–buf_read_ahead_random, linear read ahead–buf_read_ahead_linear)
2.buf_pool->watch数组的作用不甚明了
3.从磁盘读取Page后顺带刷脏页的过程buf_flush_free_margin
4.从磁盘读取page后,对insert buffer的操作ibuf_merge_or_delete_for_page