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
  2. 记录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. }
    1. - 配置是否打开了`slow log`功能,由参数`opt_slow_log`决定
    2. - 查询语句执行的时间超过`long_query_time`、并且检索的行数超过`min_examined_row_limit`
    3. - 优化过程中发现无法使用索引、或者无高效索引的查询
    4. - 对于无法使用索引或无高效索引的查询下,通过限流器`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;
  5. bool is_log_table;
  6. my_off_t saved_data_file_length;
  7. mysql_mutex_t mutex;
  8. THR_LOCK lock;
  9. bool update_file_opened;
  10. bool tina_write_opened;
  11. File meta_file; /* Meta file we use */
  12. File tina_write_filedes; /* File handler for readers */
  13. bool crashed; /* Meta file is crashed */
  14. ha_rows rows_recorded; /* Number of rows in tables */
  15. uint data_file_version; /* Version of the data file used */
  16. }

对于一张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)
  4. // 如果空间不够,则申请内存,并将原数据拷贝到新空间(若采用malloc)
  5. if ((size_t)(chain_ptr - chain) == (chain_size - 1)) {
  6. chain_size += DEFAULT_CHAIN_LENGTH;
  7. chain = (tina_set *)my_realloc(...)
  8. // OR
  9. *ptr = (tina_set *)my_malloc(...)
  10. memcpy(ptr, chain, DEFAULT_CHAIN_LENGTH * sizeof(tina_set));
  11. }
  12. // 插入删除点
  13. chain_ptr->begin = current_position;
  14. chain_ptr->end = next_position;
  15. chain_ptr++;
  16. }

对于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. }
  9. mysql_file_rename(...)
  10. }

更新的记录会被放入数据文件头,原记录顺序会被打乱。

尽管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