Overview
slow log可帮助DBA定位可能存在问题的SQ语句,从而进行SQL语句层面的优化。slow log可以记录到文件或者mysql.slow_log表上,目前大部分情况下采用后者。mysql.slow_log采用CSV引擎进行存取。本文将结合两者阐述mysql记录slow log以及CSV引擎本身的相关实现细节。
Recording slow log
- 写入一条
slow log的函数调用栈:
```
- dispatch_command-->
- log_slow_statement-->
- log_slow_do-->
- Query_logger::slow_log_write-->
- Log_to_csv_event_handler::log_slow-->
- handler::ha_write_row-->
- ha_tina::write_row ```
- 记录
low log发生在sql语句执行完成后,而一条sql语句是否被记录取决于四方面:
```
- if (thd->enable_slow_log && opt_slow_log) {
- bool warn_no_index =
- ((thd->server_status &
- (SERVER_QUERY_NO_INDEX_USED | SERVER_QUERY_NO_GOOD_INDEX_USED)) &&
- opt_log_queries_not_using_indexes &&
- !(sql_command_flags[thd->lex->sql_command] & CF_STATUS_COMMAND));
- bool log_this_query =
- ((thd->server_status & SERVER_QUERY_WAS_SLOW) || warn_no_index) &&
- (thd->get_examined_row_count() >=
- thd->variables.min_examined_row_limit);
-
bool suppress_logging = log_throttle_qni.log(thd, warn_no_index);
-
if (!suppress_logging && log_this_query) DBUG_RETURN(true);
- } ```
```
-
- 配置是否打开了
slow log功能,由参数opt_slow_log决定
- 配置是否打开了
-
- 查询语句执行的时间超过
long_query_time、并且检索的行数超过min_examined_row_limit
- 查询语句执行的时间超过
-
- 优化过程中发现无法使用索引、或者无高效索引的查询
-
- 对于无法使用索引或无高效索引的查询下,通过限流器
Slow_log_throttle,限制其(日志)产生的速度if (eligible && inc_log_count(*rate)) { /* Current query's logging should be suppressed. Add its execution time and lock time to totals for the current window. */ total_exec_time += (end_utime_of_query - thd->start_utime); total_lock_time += (thd->utime_after_lock - thd->start_utime); suppress_current = true; }
- 对于无法使用索引或无高效索引的查询下,通过限流器
``
3. 然后通过Query_logger,该类封装了日志(目前只有general log、slow log)handler设置的接口(如activate_log_handler)、记录本次连接产生的慢查询日志次数等
4.Query_logger在Log_event_handler,Log_to_file_event_handler,Log_to_csv_event_handler中封装了日志handler的接口。Log_event_handler的派生类有Log_to_file_event_handler、Log_to_csv_event_handler,允许日志输出到文件或(和)表,可由--log_ouput指定
5. 由于THD->TEX中未有打开slow log的table,所以在Log_to_csv_event_handler::log_slow中构造TABLE_LIST指定打开slow_log表,并最终调用ha_tina::write_row`
CSV Engine
CSV引擎可以将普通的CSV文件(逗号分隔的文件)作为MySQL的表处理。其主要代码在storage/csv/下。
TINA_SHARE
同一张表的多个handler之间共享数据一般会采用TABLE_SHARE类,但CSV引擎采用了自定义的TINA_SHARE类进行数据共享。
1. struct TINA_SHARE {
2. char *table_name;
3. char data_file_name[FN_REFLEN];
4. uint table_name_length, use_count;
6. bool is_log_table;
7. my_off_t saved_data_file_length;
8. mysql_mutex_t mutex;
9. THR_LOCK lock;
10. bool update_file_opened;
11. bool tina_write_opened;
12. File meta_file; /* Meta file we use */
13. File tina_write_filedes; /* File handler for readers */
14. bool crashed; /* Meta file is crashed */
15. ha_rows rows_recorded; /* Number of rows in tables */
16. uint data_file_version; /* Version of the data file used */
17. }
对于一张CSV表,同一时刻可有多个handler,但是只有一个TINA_SHARE实例。所有表的TINA_SHARE实例维护在tina_open_tables:
1. static unique_ptr<collation_unordered_multimap<string, TINA_SHARE *>> tina_open_tables;
TINA_SHARE实例(以下采用share简称)采用引用计数share->use_count自动完成资源的回收。
record count
MySQL中不同的存储引擎维护表的记录数的方法是不同的,如InnoDB中记录数只是个估计值,被用于优化器(https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html) CSV尝试维护准确的记录数,其方法:
handler::ha_statistics::stats::records:该值会随着write_row、以及delete_row等而变化,但由于该值属于handler的成员变量,而一张表可有多个handler,并且每次打开handler该值都会被清0,所以它对于记录数的统计是不可靠的。share->rows_recorded:由于每张表只有一个share,所以大部分情况下该参数能够准确反映表的记录数。但share->rows_recorded只会在share的引用计数清0、以及全表扫描后调用rnd_end()方法时持久化到元数据文件。如果在使用过程中,MySQL发生crash,元数据文件的对应值也是不准确的。
1. if (!--share->use_count) {
2. // ...
3. /* Write the meta file. Mark it as crashed if needed. */
4. (void)write_meta_file(share->meta_file, share->rows_recorded,
5. share->crashed ? true : false);
6. tina_open_tables->erase(share->table_name);
7. // ...
8. }
crash重启后,可以通过repair方法修正share->rows_recorded:a)如果数据文件为空,则share->rows_recorded被重置为0;b)如果数据文件每一行都能正确读取,则share->rows_recorded被设置为数据文件的行数;c)如果发现错误行,则将数据文件截断到最近正确的行,丢弃掉错误记录后所有数据,并将share->rows_recorded设置为已读取的记录数。
1. int ha_tina::repair(THD *thd, HA_CHECK_OPT *) {
2. if (!share->saved_data_file_length) // ...
3. if (rc == HA_ERR_END_OF_FILE) // ...
4. share->rows_recorded = rows_repaired; // ...
5. }
Table Scan
CSV没有索引,仅有全表扫描,涉及的方法有rnd_init、rnd_next、rnd_end,rnd_next中核心方法为find_current_row,该方法会从缓冲区中读入一行中各个字段的值。CSV的数据文件(后缀为.CSV)也很简单,其典型例子为:
1. "2019-07-31 07:11:30.173134","root[root] @ localhost []","838:59:59.000000","00:00:00.000332",4,4,"test",0,0,1,"select * from mysql.user",9
2. "2019-07-31 07:11:30.508952","root[root] @ localhost []","00:00:00.206624","00:00:00.002017",0,574,"mtr",0,0,1,"CALL mtr.check_warnings(@result)",10
3. "2019-07-31 07:11:30.644794","root[root] @ localhost []","00:00:00.006851","00:00:00.000716",1,655,"test",0,0,1,"SHOW VARIABLES LIKE 'debug'",11
Update Row
update、delete会改动数据文件,其中update操作会先将原纪录delete,再插入新的数据。 update、delete操作在执行之前,需要执行rnd_next扫描表,找到所关联的row update、delete操作依赖于:
1. struct tina_set {
2. my_off_t begin;
3. my_off_t end;
4. }
5. class ha_tina : public handler {
6. tina_set chain_buffer[DEFAULT_CHAIN_LENGTH];
7. tina_set *chain;
8. tina_set *chain_ptr;
9. }
chain_buffer中存储了当前所有被标记为delete的rowtina_set::begin指明该row在文件中的起点,tina_set::end为终点chain指向本次迭代扫描时的chain链的起点,chain_ptr指向chain链的尾部
每次执行update/delete,都会调用chain_append方法往chain链表尾部插入删除点。默认情况下,删除点tina_set会存放于预先分配的空间chain_buffer中。但当有大量删除点时,chain_append会调用realloc/malloc额外申请更大的空间
1. int ha_tina::chain_append() {
2. // 如果是连续的删除点,则合并
3. if (chain_ptr != chain && (chain_ptr - 1)->end == current_position)
5. // 如果空间不够,则申请内存,并将原数据拷贝到新空间(若采用malloc)
6. if ((size_t)(chain_ptr - chain) == (chain_size - 1)) {
7. chain_size += DEFAULT_CHAIN_LENGTH;
8. chain = (tina_set *)my_realloc(...)
9. // OR
10. *ptr = (tina_set *)my_malloc(...)
11. memcpy(ptr, chain, DEFAULT_CHAIN_LENGTH * sizeof(tina_set));
12. }
14. // 插入删除点
15. chain_ptr->begin = current_position;
16. chain_ptr->end = next_position;
17. chain_ptr++;
18. }
对于delete操作,chain_append操作已经足够。对于update操作,则仍需要打开一个临时文件(后缀为.CSN),将更新后的数据插入到临时文件中:
1. int ha_tina::update_row(const uchar *, uchar *new_data) {
2. if (open_update_temp_file_if_needed()) goto err;
3. if (mysql_file_write(update_temp_file, (uchar *)buffer.ptr(),
4. size, MYF(MY_WME | MY_NABP)))
5. got err;
6. }
当全表扫描结束后,则在rnd_end中将原数据文件未有被标记为delete的记录插入到临时文件中。最后,删除原文件,并将临时文件重命名为数据文件:
1. int ha_tina::rnd_end() {
2. while ((file_buffer_start != (my_off_t)-1))
3. {
4. mysql_file_write(update_temp_file, ...);
5. if (in_hole) {
6. // skip hole
7. }
8. }
10. mysql_file_rename(...)
11. }
更新的记录会被放入数据文件头,原记录顺序会被打乱。
尽管server层会上表锁,但在update过程发生前,可能有其它handler已经打开。对于后者持有的数据文件描述符已经不再有效。share->data_file_version用于标识数据文件的版本,当handler发现local_data_file_version落后于share->data_file_version,则会重新打开数据文件
1. int ha_tina::init_data_file() {
2. if (local_data_file_version != share->data_file_version) {
3. local_data_file_version = share->data_file_version;
4. if (mysql_file_close(data_file, MYF(0)) ||
5. (data_file = mysql_file_open(csv_key_file_data, share->data_file_name,
6. O_RDONLY, MYF(MY_WME))) == -1)
7. // ...
8. }
9. }
Summary
可以看到CSV是一款相当简单的引擎,没有索引,仅支持全表扫描,读写都简单地调用标准函数read、write,当主机crash掉后内核缓冲区里的数据会丢失,仅适用于如slow log这种对于可靠性、性能要求并不高的场景。
