May 2013

[MySQL 5.6] Innodb 后台线程之 dict stats 线程 及如何计算索引统计信息

前言   在5.6中,引入的一个新参数innodb_stats_auto_recalc用于控制是否进行自动统计信息计算。当表上的记录修改超过10%时,就会对统计信息重新计算;这只对在建表时打开了innodb_stats_persistent或者指定了建表选项STATS_PERSISTEND=1生效,采样page的个数通过参数innodb_stats_persistent_sample_pages来控制(实际读取的page数会大于该值)。 在函数dict_stats_is_persistent_enabled中可以看到,如果没有明确为ON/OFF的话(物理升级?),直接由innodb_stats_persistent来确定 我们也可以根据表的具体负载情况,通过alter语句为表设置是否打开或关闭STATS_PERSISTEND,因为统计信息的重计算,是一个开销很大的过程。 在MySQL 5.6中,完成统计信息自动计算的是一个独立线程,用于重新计算表或索引的统计信息.在一个loop中,等待事件dict_stats_event,或者每隔10秒被无条件唤醒。实际工作函数为dict_stats_process_entry_from_recalc_pool() 新对象:recalc_pool   1.recalc_pool   在内存中维持了这样一个vector,其定义如下:  57 typedef std::vector<table_id_t> recalc_pool_t;  58 static recalc_pool_t            recalc_pool; 所有需要被重新计算的表会加入到recalc_pool中,recalc_pool初始化大小为128,随后如果需要再被扩大。recalc_pool通过recalc_pool_mutex来进行互斥操作 2.将表加入到recalc_pool中 dict_stats_recalc_pool_add: 遍历recalc_pool,查看表的table->id是否已经存在,如果存在,直接返回,否则将该表的id加入到recalc_pool中,并发送dict_stats_event事件 在做完DML后,会去调用函数row_update_statistics_if_needed判断是否需要更新统计信息,有以下集中情况: 1)如果表明确指定了STATS_PERSISTEND或者打开了innodb_stats_persistent      (1)表上明确指定了stats_auto_recalc或者innodb_stats_auto_recalc为TRUE,并且修改的记录数大于总记录数的10分之一时,调用dict_stats_recalc_pool_add将表放到recalc_pool中,并将修改计数器table->stat_modified_counter重置为0.      (2)不满足(1),直接return,不进行下面的判断 2)这时候,在关闭了PERSISTENT STATS的情况下,当发现修改的记录超过6.25%时,更新统计信息dict_stats_update(table, DICT_STATS_RECALC_TRANSIENT)->dict_stats_update_transient 3.dict stat线程如何处理 dict stat线程接受到dict_stats_event事件后,调用工作函数dict_stats_process_entry_from_recalc_pool() 主要做以下几件事情: 1)从recalc_pool中拿第一个table id 2)持有dict->mutex锁 3)根据表id获取表对象(dict_table_open_on_id(table_id, TRUE, FALSE));如果表被drop掉了,这里返回的是NULL,不做任何处理 4)将表标记为table->stats_bg_flag = BG_STAT_IN_PROGRESS; 这样就无法去DROP 该表了。 5)现在可以安全的释放dict_sys->mutex 6)如果该表在10秒内 已经计算过一次,那么就把该表重新放到recalc_pool尾部,不做任何处理。否则: 调用dict_stats_update(table, […]

[MySQL 5.6] Innodb后台线程之master线程

在MySQL 5.6中,master线程的工作已经被大大减轻,类似purge, page clean都分配给独立的后台线程来进行。那么现在master线程还需要干啥活儿呢。以下就是本文需要介绍的部分 简单的看看代码,函数入口不变,依旧是srv_master_thread,但相对5.5的代码,这里已经非常非常精简了。 概括的说,master线程干这么几件事儿: A. 每sleep 1秒钟 检查最近1秒内是否有活跃事件,这是一个全局计数器,在几个地方会被递增(通过函数srv_inc_activity_count): 1.srv_active_wake_master_thread—> 实际上在5.6里这里只剩下计数器的功能了,因为除非是以force recovery 启动,或者被shutdown了,否则不会进入suspend状态 2.srv_wake_master_thread—> 在DROP/TRUNCATE TABLE时会调用到,实际上 ,在5.6的代码里,master线程大多是在sleep的状态,并不需要去做wake up的动作(report了一个bug:http://bugs.mysql.com/bug.php?id=69270) 3.row_undo_step->事务回滚时 例如事务提交/prepare时(innobase_commit->srv_active_wake_master_thread) ,事务回滚时,一个简单的查询都会引起计数器增加,从而让Master线程判定现在系统正忙. A.1如果认定现在系统正忙,则调用函数srv_master_do_active_tasks,做如下工作: 1.log_free_check 总会检查redo中是否有足够的空间,以确定是否做flush或者做checkpoint,通常情况下,用户线程在写redo日志之前也会无条件调用该函数。 这里会先在无加锁的情况下,检查log_sys->check_flush_or_checkpoint是否为TRUE,如果为TRUE,则调用log_check_margins(),否则直接返回。 check_flush_or_checkpoint在函数log_close中被设置,backtrace如下:       mtr_commit->mtr_log_reserve_and_write->log_close() 在每次将一个mtr日志写到buffer后,总会调用log_close()函数,注意,该函数是持有log_sys->mutex锁的 有以下几种情况会去设置check_flush_or_checkpoint为TRUE: log_sys->buf_free > log_sys->max_buf_free log_sys->lsn-buf_pool_get_oldest_modification() >log_sys->max_modified_age_sync log_sys->lsn-log_sys->last_checkpoint_lsn > log_sys->max_checkpoint_age_async TODO: mtr的组织,如何提交,以及redo 日志在内存中的控制 log_free_check会调用log_check_margins做两件事: 1)调用log_flush_margin:首先确认log->buf_free 是否大于 log->max_buf_free,如果是,则需要将日志写到文件,到当前lsn(log_write_up_to(lsn, LOG_NO_WAIT, FALSE))。如果已经有别的线程在干这活儿,则啥也不干,返回 2)调用log_checkpoint_margin,判断是否达到redo的同步刷脏点,或者异步/同步checkpoint点,决定是否刷脏(log_preflush_pool_modified_pages)及做checkpoint(log_checkpoint) 3)如果log_sys->check_flush_or_checkpoint依然为TRUE,则回到1)继续。 2.ibuf_contract_in_background(0, FALSE); 做ibuf merge, 正常情况下,每次处理innodb_io_capacity*0.05个page, […]

Innodb:如何计算异步/同步刷脏及checkpoint的临界范围

本文主要是记录Innodb在初始化日志子系统时,如何计算异步/同步刷脏或checkpoint的临界范围 代码分析基于MySQL 5.6.11 /////////////////////////////////////////////////////////////////////////////////////////////// 相关配置为: innodb_log_buffer_size=200M innodb_log_file_size=1000M innodb_log_files_in_group=4 innodb_flush_log_at_trx_commit = 1 innodb_thread_concurrency = 64   刷脏和做checkpoint的临界条件在系统启动初始化日志系统时即被确定了,backtrace 如下: innobase_start_or_create_for_mysql      ->log_group_init 2151                 log_group_init(0, i, srv_log_file_size * UNIV_PAGE_SIZE, 2152                                SRV_LOG_SPACE_FIRST_ID, 2153                                SRV_LOG_SPACE_FIRST_ID + 1);                    ->log_calc_max_ages                        log_calc_max_ages:   […]

[MySQL 5.6] double write buffer的几个关键函数

一个double write buffer 有2MB, 共128个page,在MySQL 5.6中, 默认有120个page用于批量刷新(如 LRU Flush 或者FLUSH LIST FLUSH),剩下的8个Page用于单个page的flush。 在DEBUG版本下,120是可以通过参数innodb_doublewrite_batch_size来配置的,好吧。我已经不安分的把DEBUG宏给去掉了。 全局对象buf_dblwr, 对应结构体为buf_dblwr_t: ib_mutex_t  mutex 互斥量 ulint block1 第一个doubewrite 块(64个page)的page no ulint block2 第二个double write 块的page no ulint first_free 在write_buf中第一个空闲的位置 ulint s_reserved  为单个page刷新预留的slot数,当flush类型为BUF_FLUSH_SINGLE_PAGE时,会进入到函数buf_dblwr_write_single_page来写一个Page  ulint b_reserved  为batch flush 预留的slot数 ibool*  in_use 用于标记一个slot是否被使用,只用于single page flush  ibool batch_running  当设置为TRUE时,表明有一次batch flush正在进行 byte*  write_buf double write buffer在内存的缓存,以UNIV_PAGE_SIZE对其 byte*  […]

[MySQL 5.6] page cleaner线程的效率问题

最近在测试5.6.11的写性能,当我完成一项测试,关闭workload,习惯性设置innodb_max_dirty_pages_pct为0,然后等待脏页刷完再shutdown。   发现存在刷脏的抖动:   time      flushed    Innodb_data_written 11:49:57  3692        117.3m 11:49:58  1125        33.5m| 11:49:59  4187        134.6m 11:50:00  1241         35.0m 11:50:01  4076        127.4m   从pstack的采样结果来看,page cleaner线程频繁出现这样的backtrace。 buf_flush_page_cleaner_thread->page_cleaner_sleep_if_needed->os_thread_sleep 加了两个计数来监控,也发现page cleaner线程平均sleep时间过长(750,000 ms 左右)。 那么为什么会出现波动呢? 检查发现,机器上有一个heartbeat脚本每隔两秒钟更新一条记录。这会导致如下条件成立: 2385                 if (srv_check_activity(last_activity) 2386                     […]

[MySQL 5.6] GTID内部实现、运维变化及存在的bug

由于之前没太多深入关注gtid,这里给自己补补课,本文是我看文档和代码的整理记录。 本文的主要目的是记下跟gtid相关的backtrace,用于以后的问题排查。另外也会讨论目前在MySQL5.6.11版本中存在的bug。 本文讨论的内容包括 一.主库上的gtid产生及记录 二.备库如何使用GTID复制 三.主备运维的变化 四.MySQL5.6.11存在的bug 前言:什么是GTID 什么是GTID呢, 简而言之,就是全局事务ID(global transaction identifier ),最初由google实现,官方MySQL在5.6才加入该功能,本文的起因在于5.6引入一大堆的gtid相关变量,深感困惑。 去年年中的时候,也写过一片简短的博客,大致介绍了下gtid是什么,http://mysqllover.com/?p=87 。本文也不打算太多文字的介绍,因为网络上已经有大量的类似文章。 GTID的格式类似于: 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1 这是在我的一台服务器上生成的gtid记录,它在binlog中表现的事件类型就是: GTID_LOG_EVENT:用于表示随后的事务的GTID 另外还有两种类型的GTID事件: ANONYMOUS_GTID_LOG_EVENT :匿名GTID事件类型(暂且不论) PREVIOUS_GTIDS_LOG_EVENT: 用于表示当前binlog文件之前已经执行过的GTID集合,记录在Binlog文件头,例如: # at 120 #130502 23:23:27 server id 119821  end_log_pos 231 CRC32 0x4f33bb48     Previous-GTIDs # 10a27632-a909-11e2-8bc7-0010184e9e08:1, # 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1-1129 GTID字符串,用“:”分开,前面表示这个服务器的server_uuid,这是一个128位的随机字符串,在第一次启动时生成(函数generate_server_uuid),对应的variables是只读变量server_uuid。 它能以极高的概率保证全局唯一性,并存到文件DATA/auto.cnf中。因此要注意保护这个文件不要被删除或修改,不然就麻烦了。 第二部分是一个自增的事务ID号,事务id号+server_uuid来唯一标示一个事务。 除了单独的GTID外,还有一个GTID SET的概念。一个GTID SET的表示类似于: 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1-31 GTID_EXECUTED和GTID_PURGED是典型的GTID SET类型变量;在一个复制拓扑中,GTID_EXECUTED 可能包含好几组数据,例如: mysql> show global variables like […]