• 简介
  • InnoDB 关闭过程
  • 参考

    简介

    通常有几种方式关闭 MySQL 服务器,常见有如下:A) 执行 mysqladmin shutdown 命令;B) 向服务器发送 SIGTERM、SIGQUIT 信号。

    mysqladmin

    首先,看下通过 mysqladmin shutdown 调用时的执行流程。
    static int execute_commands(MYSQL mysql,int argc, char *argv)
    {
    … …
    for (; argc > 0 ; argv++,argc—) {
    int option;
    bool log_warnings= true;
    switch (option= find_type(argv[0], &command_typelib, FIND_TYPE_BASIC)) {
    case ADMIN_SHUTDOWN:
    … …
    if(mysql_get_server_version(mysql) < 50709)
    resShutdown= mysql_shutdown(mysql, SHUTDOWN_DEFAULT);
    else
    resShutdown= mysql_query(mysql, “shutdown”);
    if(resShutdown) {
    my_printf_error(0, “shutdown failed; error: ‘%s’”, error_flags,
    mysql_error(mysql));
    return -1;
    }
    }
    }
    }
    如上,当版本小于 5.7.9 时,实际上会调用 mysql_shutdown() 函数,而在 5.7.9 版本之后(包括该版本),则会调用 mysql_query() 函数。
    在此,主要看下后者的处理。
    dispatch_command()
    | ← COM_SHUTDOWN:
    |-shutdown() ← 调用相同文件下的函数,关闭mysqld服务
    |-check_global_access()
    |-my_ok() ← 返回客户端,COM_QUERY<=>my_ok(),COM_SHUTDOWN<=>my_eof()
    |-general_log_print() ← 打印日志”Got shutdown command for”
    |-kill_mysql()
    |-pthread_kill() ← 向signal_thread发送SIGTERM信号
    也就是说,实际上还是通过用户线程向服务发送 SIGTERM 信号完成。

    sigterm

    如上,两种方式最终的实现都是通过发送 SIGTERM 信号完成的,那么我们接下来就看看 mysqld 服务是如何处理该信号量的。

    初始化

    其中部分信号,如 SIGABRT、SIGSEGV 等会在 my_init_signals() 函数中进行初始化信号处理函数,而 SIGTERM、SIGQUIT、SIGHUP 则会在单独的线程中进行处理,对应 signal_hand() 函数。
    mysqld_main()
    |-my_init_signals() ← 设置信号处理函数
    |-start_signal_handler() ← 创建一个单独的信号处理线程
    |-mysql_thread_create() ← 创建signal_hand()单独线程

    信号处理

    接下来看看 signal_hand() 函数中的处理方式。
    signal_hand()
    |-abort_loop=true ← 设置abort_loop变量,很多循环会检查该变量
    |-pthread_kill() ← 先主线程发送SIGUSR1信号
    |-close_connections() ← 关闭所有链接
    实际上,在主线程中对 SIGUSR1 信号并没有进行处理,应该只是一个线程切换;接下来,还是看看主线程的处理:
    mysqld_main()
    |-my_thread_join() ← 等待上述的signal_hand()处理线程
    |-clean_up()
    |-plugin_shutdown()
    |-plugin_deinitialize()
    |-ha_finalize_handlerton()
    |-hton->panic() ← 调用插件的关闭函数
    也就是会依次调用各个存储引擎的 panic() 函数,对应到 InnoDB 就是 innobase_end() 函数。

    InnoDB 关闭过程

    在关闭 MySQL 时,可以通过 innodb_fast_shutdown 参数控制存储引擎 InnoDB 的行为,该参数可以取 0、1、2,各个值的含义分别如下:
    0 关闭时 InnoDB 需要完成所有的 full purge 和 merge insert buffer 操作,这个过程会需要一定的时间,有时候可能会花上几个小时;通常在升级 InnoDB 时,需要设置为 0。
    1 默认值,关闭 InooDB 时不完成 full purge 和 merge insert buffer,但是会将缓冲池中的脏页写到磁盘中。
    2 意味着既不完成 full purge 和 merge insert buffer,同时也不将缓冲池中的脏页刷新到磁盘,但是会将 redo log 写入到磁盘;这种情况下,事务也不会丢失,但是下次启动时需要执行崩溃恢复。
    接着看看 InnoDB 关闭时的详细处理过程。

    源码解析

    如下是代码的处理过程,其中 logs_empty_and_mark_files_at_shutdown() 函数是主要的关闭处理函数,在系统关闭时同时执行 sharp checkpoint 操作。
    innobase_end()
    |-srv_fast_shutdown ← 根据参数innodb_fast_shutdown判断是否快速关闭
    |-hash_table_free() ← 释放innodb表占用的内存
    |-innobase_shutdown_for_mysql()
    | |-fts_optimize_shutdown()
    | |-dict_stats_shutdown()
    | |-logs_empty_and_mark_files_at_shutdown() ← 1. 将buffer pool落盘,并将LSN写入表空间,主要函数,后面均为资源清理
    | | |-ib::info() ← 1.0 打印Starting shutdown日志 <<<<<<
    | | |-srt_shutdown_state 设置变量,进入SRV_SHUTDOWN_CLEANUP状态
    | | |
    | | |-srv_any_background_threads_are_active() ← 1.1 等待后台线程关闭,没1min打印一次等待线程信息
    | | |
    | | |-trx_sys_any_active_transactions() ← 1.2 如果事务已经在prepare阶段了,则等待其处理完成
    | | |
    | | |-srv_get_active_thread_type() ← 1.3 等待worker、master、purge线程进入suspend状态
    | | |
    | | |-srt_shutdown_state 设置变量,进入SRV_SHUTDOWN_FLUSH_PHASE状态
    | | |-buf_page_cleaner_is_active ← 1.4 正常此时只剩下了page_cleaner线程刷Buffer Pool了,等待其完成
    | | |-buf_pool_check_no_pending_io() 检查IO操作是否都已经完成
    | | |
    | | | ← 1.5 如果srv_fast_shutdown变量值为2
    | | |-log_buffer_flush_to_disk() 将redo-log刷新到磁盘
    | | |-srv_any_background_threads_are_active()
    | | |-fil_close_all_files() 关闭文件,然后返回
    | | |
    | | |-log_make_checkpoint_at() ← 1.6 将最近LSN做checkpoint
    | | |-fil_flush_file_spaces() 将表空间文件和日志文件刷新到磁盘
    | | |-srt_shutdown_state 设置变量,进入SRV_SHUTDOWN_LAST_PHASE状态
    | | |-srv_get_active_thread_type() 再次确认所有服务已经关闭
    | | |
    | | |-fil_write_flushed_lsn() ← 1.7 将LSN写入到系统表空间的第一页中
    | | |-fil_close_all_files() 关闭所有文件
    | |
    | |-srv_conc_get_active_threads() ← 正常应该无活跃的线程,有则打印到日志
    | |
    | | ← 2. 接下来是一些资源的清理
    | |-srv_shutdown_all_bg_threads() 关闭InnoDB创建的后台线程
    | |-fclose() 关闭InnoDB打开的文件
    | |-dict_stats_thread_deinit()
    | | 释放mutexes,释放内存
    | |-os_thread_free() 释放线程相关的资源
    | |-sync_check_close() 释放同步相关资源
    |
    |-innobase_space_shutdown()
    实际上,Buffer Pool 中脏页刷到磁盘的操作是最耗时的,脏页越多需要 flush 的块也就越多,从而导致关闭时间变长;可以通过下面的命令来观察 Dirty Page 的数量:
    $ mysqladmin -uroot ext -i 1 | grep “Innodb_buffer_pool_pages_dirty”

    FAQs

  1. 如何查看 InnoDB 在等待那些线程?
    InnoDB 在关闭时,会每隔 100ms 检查一次后台线程是否已经全部退出,具体检查那些线程可以查看 srv_any_background_threads_are_active() 函数;等待超过了 1min ,则会打印日志 “Waiting for” 。
    2. 为什么要等待出于 prepare 阶段的事务?
    如果事务处于 prepare 阶段,说明已经在 InnoDB 执行了 commit 操作,也就是在存储引擎中认为已经提交,只需要服务端提交即可,显然,我们不希望丢失这部分数据。
    实际上,接下来的操作不会再与客户端进行交互,都是服务端的执行流程了,正常来说执行的时间会非常短。

    参考

    Reference Manual - The Server Shutdown Process