Overview

slow log可帮助DBA定位可能存在问题的SQ语句,从而进行SQL语句层面的优化。slow log可以记录到文件或者mysql.slow_log表上,目前大部分情况下采用后者。mysql.slow_log采用CSV引擎进行存取。本文将结合两者阐述mysql记录slow log以及CSV引擎本身的相关实现细节。

Recording slow log

  1. 写入一条slow log的函数调用栈:

```

  1. dispatch_command-->
  2. log_slow_statement-->
  3. log_slow_do-->
  4. Query_logger::slow_log_write-->
  5. Log_to_csv_event_handler::log_slow-->
  6. handler::ha_write_row-->
  7. ha_tina::write_row ```
  8. 记录low log发生在sql语句执行完成后,而一条sql语句是否被记录取决于四方面:

```

  1. if (thd->enable_slow_log && opt_slow_log) {
  2. bool warn_no_index =
  3. ((thd->server_status &
  4. (SERVER_QUERY_NO_INDEX_USED | SERVER_QUERY_NO_GOOD_INDEX_USED)) &&
  5. opt_log_queries_not_using_indexes &&
  6. !(sql_command_flags[thd->lex->sql_command] & CF_STATUS_COMMAND));
  7. bool log_this_query =
  8. ((thd->server_status & SERVER_QUERY_WAS_SLOW) || warn_no_index) &&
  9. (thd->get_examined_row_count() >=
  10. thd->variables.min_examined_row_limit);
  11. bool suppress_logging = log_throttle_qni.log(thd, warn_no_index);

  12. if (!suppress_logging && log_this_query) DBUG_RETURN(true);

  13. } ```

```

    • 配置是否打开了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 logslow loghandler设置的接口(如activate_log_handler)、记录本次连接产生的慢查询日志次数等 4.Query_loggerLog_event_handlerLog_to_file_event_handlerLog_to_csv_event_handler中封装了日志handler的接口。Log_event_handler的派生类有Log_to_file_event_handlerLog_to_csv_event_handler,允许日志输出到文件或(和)表,可由--log_ouput指定 5. 由于THD->TEX中未有打开slow logtable,所以在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.htmlCSV尝试维护准确的记录数,其方法:

  • 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_initrnd_nextrnd_endrnd_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

updatedelete会改动数据文件,其中update操作会先将原纪录delete,再插入新的数据。 updatedelete操作在执行之前,需要执行rnd_next扫描表,找到所关联的row updatedelete操作依赖于:


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中存储了当前所有被标记为deleterow
  • tina_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是一款相当简单的引擎,没有索引,仅支持全表扫描,读写都简单地调用标准函数readwrite,当主机crash掉后内核缓冲区里的数据会丢失,仅适用于如slow log这种对于可靠性、性能要求并不高的场景。

Reference

原文:http://mysql.taobao.org/monthly/2019/07/08/