作者:卢文双 资深数据库内核研发

序言

以前对 MySQL 测试框架 MTR 的应用,次要集中于 SQL 正确性验证。近期因为工作须要,深刻理解了 MTR 的方方面面,发现 MTR 的能力不仅限于此,还反对单元测试、压力测试、代码覆盖率测试、内存谬误检测、线程竞争与死锁等性能,因而,本着分享的精力,将其总结成一个系列。

次要内容如下:

  • 入门篇:工作机制、编译装置、参数、指令示例、举荐用法、增加 case、常见问题、异样调试
  • 进阶篇:高阶用法,包含单元测试、压力测试、代码覆盖率测试、内存谬误检测、线程竞争与死锁
  • 源码篇:剖析 MTR 的源码
  • 语法篇:单元测试、压力测试、mysqltest 语法、异样调试

因为集体程度无限,所述不免有谬误之处,望雅正。

本文是第三篇源码篇。

本文首发于 2023-06-05 22:03:44

MTR 系列基于 MySQL 8.0.29 版本,如有例外,会特地阐明。

简介

首先回顾一下MySQL 测试框架次要蕴含的组件:

  • mysql-test-run.pl :perl 脚本,简称 mtr,是 MySQL 最罕用的测试工具,负责管制流程,包含启停、辨认执行哪些用例、创立文件夹、收集后果等等,次要作用是验证 SQL 语句在各种场景下是否返回正确的后果。
  • mysqltest :C++二进制程序,负责执行测试用例,包含读文件、解析特定语法、执行用例。用例的非凡语法(比方,--source--replace_column等)都在command_namesenum_commands两个枚举构造体中。
  • mysql_client_test :C++二进制程序,用于测试 MySQL 客户端 API(mysqltest 无奈用于测试 API)。
  • mysql-stress-test.pl :perl 脚本,用于 MySQL Server 的压力测试。
  • 反对 gcov/gprof 代码覆盖率测试工具。

除此之外,还提供了单元测试工具,以便为存储引擎和插件创立独自的单元测试程序。

各个组件的地位如下:

源码地位装置目录地位
mysql-test/mysql-test-run.plmysql-test/mysql-test-run.pl
client/mysqltest.ccbin/mysqltest
testclients/mysql_client_test.ccbin/mysql_client_test
mysql-test/mysql-stress-test.plmysql-test/mysql-stress-test.pl

源码剖析

基本原理

简要回顾一下 MTR 的基本原理:

SQL 正确性:比照(diff)冀望输入和理论输入,若完全一致,则测试通过;反之,测试失败。

高级用法:以下工具都须要在编译时启用对应的选项。

  • valgrind:mtr 依据传参拼接 valgrind 指令的形式来测试。
  • ASAN:蕴含编译器插桩模块,还有一个运行时的库用来替换 malloc 函数。插桩模块次要用在栈内存上,而运行时库次要用在堆内存上。
  • MSAN:外围是编译插桩,同时还有一个运行时库,用来在启动时,将低地址内存设置为不可读,而后映射为影子内存。
  • UBSAN:在编译时对可疑操作进行插桩,以捕捉程序运行时的未定义行为。同时,还有一个额定的运行时库。
  • gcov/gprof:mtr 依据传参拼接相干指令来测试。
  • 单元测试:通过 mtr 调用 mysqltest,再调用 xx-t 等生成的二进制文件。

更多内容请参考本系列「(一)入门篇」及「(二)进阶篇」。

整体流程图

mysql-test-run.pl

时序图

MTR 框架时序图如下所示:

框架流程

如上图所示,mysql-test-run.pl框架运行流程如下:

1、初始化(Initialization)

  • 确定用例执行范畴(collect_test_cases),包含运行哪些 suite,skip 哪些用例,在本阶段依据disabled.def文件、--skip-xxx命令(比方skip-rpl)等确定执行用例。将所有用例组织到一个大的内存构造中(my @tests_listmy @tests),包含用例启动参数,用例。
  • 同时,初始化数据库(initialize_servers()->mysql_install_db())。前面运行用例启动数据库时,不须要每次初始化,只需从这里的目录中拷贝启动。

2、运行用例(run test)

主线程依据参数--parallel(默认是 1)启动一个或者多个用例执行线程(run_worker()),各线程有本人独立的 client port,data dir 等。

启动的run_worker与主线程之间是 server-client 模式,主线程是 server,run_worker()是 client。

  • 主线程与run_worker是一问一答模式,主线程向run_worker发送运行用例的文件门路、配置文件参数等各种参数信息,run_worker向主线程返回运行后果,直到所有在 collection 中的用例都运行结束,主线程 close 各run_worker,进行收尾工作。
  • 主线程先读取各run_worker返回值,对上一个用例进行收尾工作。之后,读取 collection 中的用例,通过本地 socket 发送到run_worker线程,run_worker线程接管到主线程命令,运行本次用例执行函数run_testcase(),而 run_testcase()次要负责 3 件事:启动 mysqld、启动并监控 mysqltest,解决执行后果

    • 启动 mysqld: 依据参数启动一个或者多个 mysqld(start_servers()),在start_servers大多数状况下会拷贝主线程初始化后的目录到run_worker的目录,作为新实例的启动目录,用 shell 命令启动数据库。
    • 启动并监控 mysqltest:用例在mysqltest中执行(会逐行扫描 *.test 文件中的 SQL 或指令并于 MySQL 中执行),run_worker线程会监控mysqltest的运行状态,监测其是否运行超时或者运行完结。
    • 解决执行后果:mysqltest执行完结会留下执行日志,框架依据执行日志判断执行是否通过,如果没通过是否须要重试等

代码

本大节所波及代码都来自于mysql-test-run.pl文件,但因为该文件内容过多,此处只截取要害流程代码。

援用模块
require "lib/mtr_gcov.pl";require "lib/mtr_gprof.pl";require "lib/mtr_io.pl";require "lib/mtr_lock_order.pl";require "lib/mtr_misc.pl";require "lib/mtr_process.pl";
主流程
# BEGIN 是 Perl 语言的标记,用于程序体“运行前”执行的代码逻辑,会在所有代码(包含main函数)执行前执行BEGIN {  # Check that mysql-test-run.pl is started from mysql-test/  unless (-f "mysql-test-run.pl") {    print "**** ERROR **** ",      "You must start mysql-test-run from the mysql-test/ directory\n";    exit(1);  }  # Check that lib exist  unless (-d "lib/") {    print "**** ERROR **** ", "Could not find the lib/ directory \n";    exit(1);  }}# END 是 Perl 语言的标记,用于程序体“运行后”执行的代码逻辑,会在所有代码执行后执行END {    my $current_id = $$;    if ($parent_pid && $current_id == $parent_pid) {        remove_redundant_thread_id_file_locations();  clean_unique_id_dir();    }    if (defined $opt_tmpdir_pid and $opt_tmpdir_pid == $$) {    if (!$opt_start_exit) {      # Remove the tempdir this process has created      mtr_verbose("Removing tmpdir $opt_tmpdir");      rmtree($opt_tmpdir);    } else {      mtr_warning(          "tmpdir $opt_tmpdir should be removed after the server has finished");    }  }}select(STDERR);$| = 1;    # Automatically flush STDERR - output should be in sync with STDOUTselect(STDOUT);$| = 1;    # Automatically flush STDOUTmain();
main 函数
sub main {  # Default, verbosity on  report_option('verbose', 0);  # This is needed for test log evaluation in "gen-build-status-page"  # in all cases where the calling tool does not log the commands directly  # before it executes them, like "make test-force-pl" in RPM builds.  mtr_report("Logging: $0 ", join(" ", @ARGV));  command_line_setup(); # 剖析命令行参数  # Create a directory to store build thread id files  create_unique_id_dir();  $build_thread_id_file = "$build_thread_id_dir/" . $$ . "_unique_ids.log";  open(FH, ">>", $build_thread_id_file) or    die "Can't open file $build_thread_id_file: $!";  print FH "# Unique id file paths\n";  close(FH);  # --help will not reach here, so now it's safe to assume we have binaries  My::SafeProcess::find_bin($bindir, $path_client_bindir);  $secondary_engine_support =    ($secondary_engine_support and find_secondary_engine($bindir)) ? 1 : 0;  if ($secondary_engine_support) {    check_secondary_engine_features(using_extern());    # Append secondary engine test suite to list of default suites if found.    add_secondary_engine_suite();  }  if ($opt_gcov) { # 是否启用了 -gcov 参数    gcov_prepare($basedir); # 删除之前生成的临时文件,比方 *.gcov、*.da、*.gcda  }  if ($opt_lock_order) { # 是否启用了 --lock-order=bool 参数    lock_order_prepare($bindir); # 创立 lock_order 目录  }  ######################################  # 依据参数,收集须要测试的 suites  ######################################  # Collect test cases from a file and put them into '@opt_cases'.  if ($opt_do_test_list) { # 对应选项 --do-test-list=FILE ,各个测试 case 按行宰割,如需正文则增加 # 号    collect_test_cases_from_list(\@opt_cases, $opt_do_test_list, \$opt_ctest);  }  my $suite_set;  if ($opt_suites) { # 是否通过 --suites 参数指定了要运行的 suites 汇合    # Collect suite set if passed through the MTR command line    if ($opt_suites =~ /^default$/i) {      $suite_set = 0;    } elsif ($opt_suites =~ /^non[-]default$/i) {      $suite_set = 1;    } elsif ($opt_suites =~ /^all$/i) {      $suite_set = 2;    }  } else {    # Use all suites(suite set 2) in case the suite set isn't explicitly    # specified and :-    # a) A PREFIX or REGEX is specified using the --do-suite option    # b) Test cases are passed on the command line    # c) The --do-test or --do-test-list options are used    #    # If none of the above are used, use the default suite set (i.e.,    # suite set 0)    $suite_set = ($opt_do_suite or                    @opt_cases  or                    $::do_test  or                    $opt_do_test_list    ) ? 2 : 0;  }  # Ignore the suite set parameter in case a list of suites is explicitly  # given  if (defined $suite_set) {    mtr_print(         "Using '" . ("default", "non-default", "all")[$suite_set] . "' suites")      if @opt_cases;    if ($suite_set == 0) {      # Run default set of suites      $opt_suites = $DEFAULT_SUITES;    } else {      # Include the main suite by default when suite set is 'all'      # since it does not have a directory structure like:      # mysql-test/<suite_name>/[<t>,<r>,<include>]      $opt_suites = ($suite_set == 2) ? "main" : "";      # Scan all sub-directories for available test suites.      # The variable $opt_suites is updated by get_all_suites()      find(\&get_all_suites, "$glob_mysql_test_dir");      find({ wanted => \&get_all_suites, follow => 1 }, "$basedir/internal")        if (-d "$basedir/internal");      if ($suite_set == 1) {        # Run only with non-default suites        for my $suite (split(",", $DEFAULT_SUITES)) {          for ("$suite", "i_$suite") {            remove_suite_from_list($_);          }        }      }      # Remove cluster test suites if ndb cluster is not enabled      if (not $ndbcluster_enabled) {        for my $suite (split(",", $opt_suites)) {          next if not $suite =~ /ndb/;          remove_suite_from_list($suite);        }      }      # Remove secondary engine test suites if not supported      if (defined $::secondary_engine and not $secondary_engine_support) {        for my $suite (split(",", $opt_suites)) {          next if not $suite =~ /$::secondary_engine/;          remove_suite_from_list($suite);        }      }    }  }  my $mtr_suites = $opt_suites;  # Skip suites which don't match the --do-suite filter  if ($opt_do_suite) {    my $opt_do_suite_reg = init_pattern($opt_do_suite, "--do-suite");    for my $suite (split(",", $opt_suites)) {      if ($opt_do_suite_reg and not $suite =~ /$opt_do_suite_reg/) {        remove_suite_from_list($suite);      }    }    # Removing ',' at the end of $opt_suites if exists    $opt_suites =~ s/,$//;  }  if ($opt_skip_sys_schema) {    remove_suite_from_list("sysschema");  }  if ($opt_suites) {    # Remove extended suite if the original suite is already in    # the suite list    for my $suite (split(",", $opt_suites)) {      if ($suite =~ /^i_(.*)/) {        my $orig_suite = $1;        if ($opt_suites =~ /,$orig_suite,/ or            $opt_suites =~ /^$orig_suite,/ or            $opt_suites =~ /^$orig_suite$/ or            $opt_suites =~ /,$orig_suite$/) {          remove_suite_from_list($suite);        }      }    }    # Finally, filter out duplicate suite names if present,    # i.e., using `--suite=ab,ab mytest` should not end up    # running ab.mytest twice.    my %unique_suites = map { $_ => 1 } split(",", $opt_suites);    $opt_suites = join(",", sort keys %unique_suites);    if (@opt_cases) {      mtr_verbose("Using suite(s): $opt_suites");    } else {      mtr_report("Using suite(s): $opt_suites");    }  } else {    if ($opt_do_suite) {      mtr_error("The PREFIX/REGEX '$opt_do_suite' doesn't match any of " .                "'$mtr_suites' suite(s)");    }  }  ##############################################  # 决定并发数量:  # 1. 如果设置了 --parallel 参数,则依据参数来决定;  # 2. 反之,依据 CPU 外围数来决定;  ##############################################  # Environment variable to hold number of CPUs  my $sys_info = My::SysInfo->new();  $ENV{NUMBER_OF_CPUS} = $sys_info->num_cpus();  if ($opt_parallel eq "auto") {    # Try to find a suitable value for number of workers    $opt_parallel = $ENV{NUMBER_OF_CPUS};    if (defined $ENV{MTR_MAX_PARALLEL}) {      my $max_par = $ENV{MTR_MAX_PARALLEL};      $opt_parallel = $max_par if ($opt_parallel > $max_par);    }    $opt_parallel = 1 if ($opt_parallel < 1);  }  init_timers(); # 字面意义,初始化 timer  ##############################################  # 之前收集了 test suites,当初收集 test cases  ##############################################  mtr_report("Collecting tests");  my $tests = collect_test_cases($opt_reorder, $opt_suites,                                 \@opt_cases,  $opt_skip_test_list);  mark_time_used('collect');  # A copy of the tests list, that will not be modified even after the tests  # are executed.  my @tests_list = @{$tests};  check_secondary_engine_option($tests) if $secondary_engine_support;  if ($opt_report_features) {    # Put "report features" as the first test to run. No result file,    # prints the output on console.    my $tinfo = My::Test->new(master_opt    => [],                              name          => 'report_features',                              path          => 'include/report-features.test',                              shortname     => 'report_features',                              slave_opt     => [],                              template_path => "include/default_my.cnf",);    unshift(@$tests, $tinfo);  }  my $num_tests = @$tests;  if ($num_tests == 0) {    mtr_report("No tests found, terminating");    exit(0);  }  ##############################################  # 初始化测试所需的 servers  # 1. kill 掉之前运行 mtr 残留的过程  # 2. 通过 mysqld --initialize [options] 创立测试所用 database  ##############################################  initialize_servers();  ##############################################  # 以并行形式运行单元测试  ##############################################  # Run unit tests in parallel with the same number of workers as  # specified to MTR.  $ctest_parallel = $opt_parallel;  # Limit parallel workers to the number of regular tests to avoid  # idle workers.  $opt_parallel = $num_tests if $opt_parallel > $num_tests;  $ENV{MTR_PARALLEL} = $opt_parallel;  mtr_report("Using parallel: $opt_parallel");  my $is_option_mysqlx_port_set = $opt_mysqlx_baseport ne "auto";  if ($opt_parallel > 1) {    if ($opt_start_exit || $opt_stress || $is_option_mysqlx_port_set) {      mtr_warning("Parallel cannot be used neither with --start-and-exit nor",                  "--stress nor --mysqlx_port.\nSetting parallel value to 1.");      $opt_parallel = 1;    }  }  $num_tests_for_report = $num_tests;  # Shutdown report is one extra test created to report  # any failures or crashes during shutdown.  $num_tests_for_report = $num_tests_for_report + 1;  # When either --valgrind or --sanitize option is enabled, a dummy  # test is created.  if ($opt_valgrind_mysqld or $opt_sanitize) {    $num_tests_for_report = $num_tests_for_report + 1;  }  # Please note, that disk_usage() will print a space to separate its  # information from the preceding string, if the disk usage report is  # enabled. Otherwise an empty string is returned.  my $disk_usage = disk_usage();  if ($disk_usage) {    mtr_report(sprintf("Disk usage of vardir in MB:%s", $disk_usage));  }  # Create server socket on any free port  my $server = new IO::Socket::INET(Listen    => $opt_parallel,                                    LocalAddr => 'localhost',                                    Proto     => 'tcp',);  mtr_error("Could not create testcase server port: $!") unless $server;  my $server_port = $server->sockport();  if ($opt_resfile) {    resfile_init("$opt_vardir/mtr-results.txt");    print_global_resfile();  }  if ($secondary_engine_support) {    secondary_engine_offload_count_report_init();    # Create virtual environment    create_virtual_env($bindir);  }  if ($opt_summary_report) {    mtr_summary_file_init($opt_summary_report);  }  if ($opt_xml_report) {    mtr_xml_init($opt_xml_report);  }  # Read definitions from include/plugin.defs  read_plugin_defs("include/plugin.defs", 0); # Also read from plugin.defs files in internal and internal/cloud if they exist  my @plugin_defs = ("$basedir/internal/mysql-test/include/plugin.defs",                     "$basedir/internal/cloud/mysql-test/include/plugin.defs");  for my $plugin_def (@plugin_defs) {    read_plugin_defs($plugin_def) if -e $plugin_def;  }  # Simplify reference to semisync plugins  $ENV{'SEMISYNC_PLUGIN_OPT'} = $ENV{'SEMISYNC_SOURCE_PLUGIN_OPT'};  if (IS_WINDOWS) {    $ENV{'PLUGIN_SUFFIX'} = "dll";  } else {    $ENV{'PLUGIN_SUFFIX'} = "so";  }  if ($group_replication) {    $ports_per_thread = $ports_per_thread + 10;  }  if ($secondary_engine_support) {    # Reserve 10 extra ports per worker process    $ports_per_thread = $ports_per_thread + 10;  }  create_manifest_file();  # Create child processes  my %children;  $parent_pid = $$;  for my $child_num (1 .. $opt_parallel) {    my $child_pid = My::SafeProcess::Base::_safe_fork();    if ($child_pid == 0) {      $server = undef;    # Close the server port in child      $tests  = {};       # Don't need the tests list in child      # Use subdir of var and tmp unless only one worker      if ($opt_parallel > 1) {        set_vardir("$opt_vardir/$child_num");        $opt_tmpdir = "$opt_tmpdir/$child_num";      }      init_timers();      run_worker($server_port, $child_num); ###### 外围函数      exit(1);    }    $children{$child_pid} = 1;  }  mtr_print_header($opt_parallel > 1);  mark_time_used('init');  # 这里的 server 指的是 mtr 的主控制循环,而不是 mysql server 。  # 该函数的次要作用是定期(每秒)唤醒一次,查看来自 worker 的新音讯并解决,  # 当 test cases 执行完或超时时,该函数会退出。  my $completed = run_test_server($server, $tests, $opt_parallel);  exit(0) if $opt_start_exit;  ##############################################  # 为退出测试做收尾工作  ##############################################  # Send Ctrl-C to any children still running  kill("INT", keys(%children));  if (!IS_WINDOWS) {    # Wait for children to exit    foreach my $pid (keys %children) {      my $ret_pid = waitpid($pid, 0);      if ($ret_pid != $pid) {        mtr_report("Unknown process $ret_pid exited");      } else {        delete $children{$ret_pid};      }    }  }  # Remove config files for components  read_plugin_defs("include/plugin.defs", 1);  for my $plugin_def (@plugin_defs) {    read_plugin_defs($plugin_def, 1) if -e $plugin_def;  }  remove_manifest_file();  if (not $completed) {    mtr_error("Test suite aborted");  }  if (@$completed != $num_tests) {    # Not all tests completed, failure    mtr_report();    mtr_report("Only ", int(@$completed), " of $num_tests completed.");    foreach (@tests_list) {      $_->{key} = "$_" unless defined $_->{key};    }    my %is_completed_map = map { $_->{key} => 1 } @$completed;    my @not_completed;    foreach (@tests_list) {      if (!exists $is_completed_map{$_->{key}}) {        push (@not_completed, $_->{name});      }    }    if (int(@not_completed) <= 100) {      mtr_error("Not all tests completed:", join(" ", @not_completed));    } else {      mtr_error("Not all tests completed:", join(" ", @not_completed[0...49]), "... and", int(@not_completed)-50, "more");    }  }  mark_time_used('init');  push @$completed, run_ctest() if $opt_ctest;  # Create minimalistic "test" for the reporting failures at shutdown  my $tinfo = My::Test->new(name      => 'shutdown_report',                            shortname => 'shutdown_report',);  # Set dummy worker id to align report with normal tests  $tinfo->{worker} = 0 if $opt_parallel > 1;  if ($shutdown_report) {    $tinfo->{result}   = 'MTR_RES_FAILED';    $tinfo->{comment}  = "Mysqld reported failures at shutdown, see above";    $tinfo->{failures} = 1;  } else {    $tinfo->{result} = 'MTR_RES_PASSED';  }  mtr_report_test($tinfo);  report_option('prev_report_length', 0);  push @$completed, $tinfo;  if ($opt_valgrind_mysqld or $opt_sanitize) {    # Create minimalistic "test" for the reporting    my $tinfo = My::Test->new(      name      => $opt_valgrind_mysqld ? 'valgrind_report' : 'sanitize_report',      shortname => $opt_valgrind_mysqld ? 'valgrind_report' : 'sanitize_report',    );    # Set dummy worker id to align report with normal tests    $tinfo->{worker} = 0 if $opt_parallel > 1;    if ($valgrind_reports) {      $tinfo->{result} = 'MTR_RES_FAILED';      if ($opt_valgrind_mysqld) {        $tinfo->{comment} = "Valgrind reported failures at shutdown, see above";      } else {        $tinfo->{comment} =          "Sanitizer reported failures at shutdown, see above";      }      $tinfo->{failures} = 1;    } else {      $tinfo->{result} = 'MTR_RES_PASSED';    }    mtr_report_test($tinfo);    report_option('prev_report_length', 0);    push @$completed, $tinfo;  }  if ($opt_quiet) {    my $last_test = $completed->[-1];    mtr_report() if !$last_test->is_failed();  }  mtr_print_line();  if ($opt_gcov) {    gcov_collect($bindir, $opt_gcov_exe, $opt_gcov_msg, $opt_gcov_err);  }  if ($ctest_report) {    print "$ctest_report\n";    mtr_print_line();  }  # Cleanup the build thread id files  remove_redundant_thread_id_file_locations();  clean_unique_id_dir();  # Cleanup the secondary engine environment  if ($secondary_engine_support) {    clean_virtual_env();  }  print_total_times($opt_parallel) if $opt_report_times;  mtr_report_stats("Completed", $completed);  remove_vardir_subs() if $opt_clean_vardir;  exit(0);}
run_worker 函数
# This is the main loop for the worker thread (which, as mentioned, is# actually a separate process except on Windows).## Its main loop reads messages from the main thread, which are either# 'TESTCASE' with details on a test to run (also read with# My::Test::read_test()) or 'BYE' which will make the worker clean up# and send a 'SPENT' message. If running with valgrind, it also looks# for valgrind reports and sends 'VALGREP' if any were found.sub run_worker ($) {  my ($server_port, $thread_num) = @_;  $SIG{INT} = sub { exit(1); };  # Connect to server  my $server = new IO::Socket::INET(PeerAddr => 'localhost',                                    PeerPort => $server_port,                                    Proto    => 'tcp');  mtr_error("Could not connect to server at port $server_port: $!")    unless $server;  # Set worker name  report_option('name', "worker[$thread_num]");  # Set different ports per thread  set_build_thread_ports($thread_num);  # Turn off verbosity in workers, unless explicitly specified  report_option('verbose', undef) if ($opt_verbose == 0);  environment_setup();  # Read hello from server which it will send when shared  # resources have been setup  my $hello = <$server>;  setup_vardir(); # 创立 var 目录(默认,若指定 --vardir,则以参数为准),用于寄存测试过程中产生的文件。  check_running_as_root(); # 查看是否以 root 运行,若是,则无需查看文件权限了。  if (using_extern()) {    create_config_file_for_extern(%opts_extern);  }  # Ask server for first test  print $server "START\n";  mark_time_used('init');  while (my $line = <$server>) {    chomp($line);    if ($line eq 'TESTCASE') {      my $test = My::Test::read_test($server);      # Clear comment and logfile, to avoid reusing them from previous test      delete($test->{'comment'});      delete($test->{'logfile'});      # A sanity check. Should this happen often we need to look at it.      if (defined $test->{reserved} && $test->{reserved} != $thread_num) {        my $tres = $test->{reserved};        my $name = $test->{name};        mtr_warning("Test $name reserved for w$tres picked up by w$thread_num");      }      $test->{worker} = $thread_num if $opt_parallel > 1;      run_testcase($test); # 运行测试用例(test case),返回 0 示意执行胜利,非 0 示意失败。      |-- do_before_run_mysqltest($tinfo);      |-- start_mysqltest($tinfo);      |-- while 循环定期判断 mysqltest 的执行状态并做后续解决      # Stop the secondary engine servers if started.      stop_secondary_engine_servers() if $test->{'secondary-engine'};      $ENV{'SECONDARY_ENGINE_TEST'} = 0;      # Send it back, now with results set      $test->write_test($server, 'TESTRESULT');      mark_time_used('restart');    } elsif ($line eq 'BYE') { # 收到 BYE 指令      mtr_report("Server said BYE");      my $ret = stop_all_servers($opt_shutdown_timeout); # 敞开所有 server      if (defined $ret and $ret != 0) {        shutdown_exit_reports();        $shutdown_report = 1;      }      print $server "SRV_CRASH\n" if $shutdown_report;      mark_time_used('restart');      my $valgrind_reports = 0;      if ($opt_valgrind_mysqld or $opt_sanitize) {        $valgrind_reports = valgrind_exit_reports() if not $shutdown_report;        print $server "VALGREP\n" if $valgrind_reports;      }      if ($opt_gprof) { # 如果指定了 -gprof ,则应用 gprof 解析 gcov 生成的后果文件 gmon.out        gprof_collect(find_mysqld($basedir), keys %gprof_dirs);      }      mark_time_used('admin');      print_times_used($server, $thread_num);      exit($valgrind_reports);    } else {      mtr_error("Could not understand server, '$line'");    }  }  stop_all_servers();  exit(1);}

mysql-stress-test.pl

该文件会被 mysql-test-run.pl调用,但压力测试的命令或 SQL 须要自行编写。

指令用法

perl mysql-stress-test.pl--stress-suite-basedir=/opt/qa/mysql-test-extra-5.0/mysql-test--stress-basedir=/opt/qa/test--server-logs-dir=/opt/qa/logs--test-count=20--stress-tests-file=innodb-tests.txt--stress-init-file=innodb-init.txt--threads=5--suite=funcs_1--mysqltest=/opt/mysql/mysql-5.0/client/mysqltest--server-user=root--server-database=test--cleanup

代码流程

该文件代码比较简单,次要步骤为:

  1. 解析参数,查看文件、门路的合法性;
  2. PREPARATION STAGE :筹备测试所需文件;
  3. INITIALIZATION STAGE :读取--stress-init-file指定的文件来初始化 stress database ;
  4. STRESS TEST RUNNING STAGE :依据参数--threads指定的数量创立线程(若不指定,默认是 1)。每个线程都执行test_loop函数来运行压力测试。

    1. 调用 test_init 函数初始化 session 变量。
    2. 调用test_execute 函数来执行测试(测试后果.result的文件名是随机生成的)。

mysqltest.cc

工作原理

执行框架次要集中在mysqltest.cc中,mysqltest读取用例文件(*.test),依据预约义的命令(比方--source--replace_column, shutdown_server等)执行相应的操作。

依据mysql-test-run.pl 文件中的run_worker 函数传入的运行参数(args)取得用例文件门路等信息,而后读取文件逐行执行语句,语句分为两种:

  • 一种是能够间接执行的 SQL 语句
  • 一种是管制语句,管制语句用来管制 mysqlclient 的非凡行为,比方shutdown mysqld等,这些命令预约义在command_names中。

详见系列文章第四篇「MySQL 测试框架 MTR 系列教程(四):语法篇」。

调用栈

通过 main() 函数能够清晰的看到 mysqltest.cc 的整体流程,次要分为几步:

  1. 初始化或筹备工作;
  2. while 循环读取 command 并解决,每个 command 的处理函数又可能是一个循环解析字符串并执行的过程;
  3. 剖析执行后果。
main|-- init_signal_handling|-- parse_args(argc, argv);|-- mysql_server_init|-- // 关上或创立 result log|-- mysql_init(&con->mysql)|-- safe_connect(&con->mysql, con->name, opt_host, opt_user, opt_pass, opt_db,               opt_port, unix_sock) // Connect to a server doing several retries if needed|-- ssl_client_check_post_connect_ssl_setup(          &con->mysql, [](const char *err) { die("%s", err); })|-- mysql_query_wrapper(        &con->mysql, "SET optimizer_switch='hypergraph_optimizer=on';")|-- // 其余筹备工作    ...|-- while (!read_command(&command) && !abort_flag) {     // 解析 command type     ...     if (ok_to_do) {     // 依据 command->type 做对应解决     switch (command->type) {        case Q_CONNECT:          do_connect(command); // 创立新连贯          break;        case Q_CONNECTION:          select_connection(command);          break;        case Q_DISCONNECT:        case Q_DIRTY_CLOSE:          do_close_connection(command);          break;        case Q_ENABLE_QUERY_LOG:          set_property(command, P_QUERY, false); // 通过设置某个属性值为 0或1,达到敞开/启用的目标          break;        case Q_DISABLE_QUERY_LOG:          set_property(command, P_QUERY, true);          break;        case Q_ENABLE_ABORT_ON_ERROR:          set_property(command, P_ABORT, true);          break;        case Q_DISABLE_ABORT_ON_ERROR:          set_property(command, P_ABORT, false);          break;        case Q_ENABLE_RESULT_LOG:          set_property(command, P_RESULT, false);          break;        case Q_DISABLE_RESULT_LOG:          set_property(command, P_RESULT, true);          break;        case Q_ENABLE_CONNECT_LOG:          set_property(command, P_CONNECT, false);          break;        case Q_DISABLE_CONNECT_LOG:          set_property(command, P_CONNECT, true);          break;        case Q_ENABLE_WARNINGS:          do_enable_warnings(command);          break;        case Q_DISABLE_WARNINGS:          do_disable_warnings(command);          break;        case Q_ENABLE_INFO:          set_property(command, P_INFO, false);          break;        case Q_DISABLE_INFO:          set_property(command, P_INFO, true);          break;        case Q_ENABLE_SESSION_TRACK_INFO:          set_property(command, P_SESSION_TRACK, true);          break;        case Q_DISABLE_SESSION_TRACK_INFO:          set_property(command, P_SESSION_TRACK, false);          break;        case Q_ENABLE_METADATA:          set_property(command, P_META, true);          break;        case Q_DISABLE_METADATA:          set_property(command, P_META, false);          break;        case Q_SOURCE:          do_source(command);          break;        case Q_SLEEP:          do_sleep(command);          break;        case Q_WAIT_FOR_SLAVE_TO_STOP:          do_wait_for_slave_to_stop(command);          break;        case Q_INC:          do_modify_var(command, DO_INC);          break;        case Q_DEC:          do_modify_var(command, DO_DEC);          break;        case Q_ECHO:          do_echo(command);          command_executed++;          break;        case Q_REMOVE_FILE:          do_remove_file(command);          break;        case Q_REMOVE_FILES_WILDCARD:          do_remove_files_wildcard(command);          break;        case Q_COPY_FILES_WILDCARD:          do_copy_files_wildcard(command);          break;        case Q_MKDIR:          do_mkdir(command);          break;        case Q_RMDIR:          do_rmdir(command, false);          break;        case Q_FORCE_RMDIR:          do_rmdir(command, true);          break;        case Q_FORCE_CPDIR:          do_force_cpdir(command);          break;        case Q_LIST_FILES:          do_list_files(command);          break;        case Q_LIST_FILES_WRITE_FILE:          do_list_files_write_file_command(command, false);          break;        case Q_LIST_FILES_APPEND_FILE:          do_list_files_write_file_command(command, true);          break;        case Q_FILE_EXIST:          do_file_exist(command);          break;        case Q_WRITE_FILE:          do_write_file(command);          break;        case Q_APPEND_FILE:          do_append_file(command);          break;        case Q_DIFF_FILES:          do_diff_files(command);          break;        case Q_SEND_QUIT:          do_send_quit(command);          break;        case Q_CHANGE_USER:          do_change_user(command);          break;        case Q_CAT_FILE:          do_cat_file(command);          break;        case Q_COPY_FILE:          do_copy_file(command);          break;        case Q_MOVE_FILE:          do_move_file(command);          break;        case Q_CHMOD_FILE:          do_chmod_file(command);          break;        case Q_PERL:          do_perl(command);          break;        case Q_RESULT_FORMAT_VERSION:          do_result_format_version(command);          break;        case Q_DELIMITER:          do_delimiter(command);          break;        case Q_DISPLAY_VERTICAL_RESULTS:          display_result_vertically = true;          break;        case Q_DISPLAY_HORIZONTAL_RESULTS:          display_result_vertically = false;          break;        case Q_SORTED_RESULT:          /*            Turn on sorting of result set, will be reset after next            command          */          display_result_sorted = true;          start_sort_column = 0;          break;        case Q_PARTIALLY_SORTED_RESULT:          /*            Turn on sorting of result set, will be reset after next            command          */          display_result_sorted = true;          start_sort_column = atoi(command->first_argument);          command->last_argument = command->end;          break;        case Q_LOWERCASE:          /*            Turn on lowercasing of result, will be reset after next            command          */          display_result_lower = true;          break;        case Q_SKIP_IF_HYPERGRAPH:          /*            Skip the next query if running with --hypergraph; will be reset            after next command.           */          skip_if_hypergraph = true;          break;        case Q_LET:          do_let(command);          break;        case Q_EXPR:          do_expr(command);          break;        case Q_EVAL:        case Q_QUERY_VERTICAL:        case Q_QUERY_HORIZONTAL:          if (command->query == command->query_buf) {            /* Skip the first part of command, i.e query_xxx */            command->query = command->first_argument;            command->first_word_len = 0;          }          [[fallthrough]];        case Q_QUERY:        case Q_REAP: {          bool old_display_result_vertically = display_result_vertically;          /* Default is full query, both reap and send  */          int flags = QUERY_REAP_FLAG | QUERY_SEND_FLAG;          if (q_send_flag) {            // Last command was an empty 'send' or 'send_eval'            flags = QUERY_SEND_FLAG;            if (q_send_flag == 2)              // Last command was an empty 'send_eval' command. Set the command              // type to Q_SEND_EVAL so that the variable gets replaced with its              // value before executing.              command->type = Q_SEND_EVAL;            q_send_flag = 0;          } else if (command->type == Q_REAP) {            flags = QUERY_REAP_FLAG;          }          /* Check for special property for this query */          display_result_vertically |= (command->type == Q_QUERY_VERTICAL);          /*            We run EXPLAIN _before_ the query. If query is UPDATE/DELETE is            matters: a DELETE may delete rows, and then EXPLAIN DELETE will            usually terminate quickly with "no matching rows". To make it more            interesting, EXPLAIN is now first.          */          if (explain_protocol_enabled)            run_explain(cur_con, command, flags, false);          if (json_explain_protocol_enabled)            run_explain(cur_con, command, flags, true);          if (*output_file) {            strmake(command->output_file, output_file, sizeof(output_file) - 1);            *output_file = 0;          }          run_query(cur_con, command, flags); // 执行查问          display_opt_trace(cur_con, command, flags);          command_executed++;          command->last_argument = command->end;          /* Restore settings */          display_result_vertically = old_display_result_vertically;          break;        }        case Q_SEND:        case Q_SEND_EVAL:          if (!*command->first_argument) {            // This is a 'send' or 'send_eval' command without arguments, it            // indicates that _next_ query should be send only.            if (command->type == Q_SEND)              q_send_flag = 1;            else if (command->type == Q_SEND_EVAL)              q_send_flag = 2;            break;          }          /* Remove "send" if this is first iteration */          if (command->query == command->query_buf)            command->query = command->first_argument;          /*            run_query() can execute a query partially, depending on the flags.            QUERY_SEND_FLAG flag without QUERY_REAP_FLAG tells it to just send            the query and read the result some time later when reap instruction            is given on this connection.          */          run_query(cur_con, command, QUERY_SEND_FLAG); // 执行查问          command_executed++;          command->last_argument = command->end;          break;        case Q_ERROR:          do_error(command);          break;        case Q_REPLACE:          do_get_replace(command);          break;        case Q_REPLACE_REGEX:          do_get_replace_regex(command);          break;        case Q_REPLACE_COLUMN:          do_get_replace_column(command);          break;        case Q_REPLACE_NUMERIC_ROUND:          do_get_replace_numeric_round(command);          break;        case Q_SAVE_MASTER_POS:          do_save_master_pos();          break;        case Q_SYNC_WITH_MASTER:          do_sync_with_master(command);          break;        case Q_SYNC_SLAVE_WITH_MASTER: {          do_save_master_pos();          if (*command->first_argument)            select_connection(command);          else            select_connection_name("slave");          do_sync_with_master2(command, 0);          break;        }        case Q_COMMENT: {          command->last_argument = command->end;          /* Don't output comments in v1 */          if (opt_result_format_version == 1) break;          /* Don't output comments if query logging is off */          if (disable_query_log) break;          /* Write comment's with two starting #'s to result file */          const char *p = command->query;          if (p && *p == '#' && *(p + 1) == '#') {            dynstr_append_mem(&ds_res, command->query, command->query_len);            dynstr_append(&ds_res, "\n");          }          break;        }        case Q_EMPTY_LINE:          /* Don't output newline in v1 */          if (opt_result_format_version == 1) break;          /* Don't output newline if query logging is off */          if (disable_query_log) break;          dynstr_append(&ds_res, "\n");          break;        case Q_PING:          handle_command_error(command, mysql_ping(&cur_con->mysql)); // 执行 ping 指令          break;        case Q_RESET_CONNECTION:          do_reset_connection();          global_attrs->clear();          break;        case Q_QUERY_ATTRIBUTES:          do_query_attributes(command);          break;        case Q_SEND_SHUTDOWN:          if (opt_offload_count_file) {            // Save the value of secondary engine execution status            // before shutting down the server.            if (secondary_engine->offload_count(&cur_con->mysql, "after"))              cleanup_and_exit(1);          }          handle_command_error(              command, mysql_query_wrapper(&cur_con->mysql, "shutdown"));          break;        case Q_SHUTDOWN_SERVER:          do_shutdown_server(command);          break;        case Q_EXEC:        case Q_EXECW:          do_exec(command, false);          command_executed++;          break;        case Q_EXEC_BACKGROUND:          do_exec(command, true); // 执行 shell 命令          command_executed++;          break;        case Q_START_TIMER:          /* Overwrite possible earlier start of timer */          timer_start = timer_now();          break;        case Q_END_TIMER:          /* End timer before ending mysqltest */          timer_output();          break;        case Q_CHARACTER_SET:          do_set_charset(command);          break;        case Q_DISABLE_PS_PROTOCOL:          set_property(command, P_PS, false);          /* Close any open statements */          close_statements();          break;        case Q_ENABLE_PS_PROTOCOL:          set_property(command, P_PS, ps_protocol);          break;        case Q_DISABLE_RECONNECT:          set_reconnect(&cur_con->mysql, 0);          break;        case Q_ENABLE_RECONNECT:          set_reconnect(&cur_con->mysql, 1);          enable_async_client = false;          /* Close any open statements - no reconnect, need new prepare */          close_statements();          break;        case Q_ENABLE_ASYNC_CLIENT:          set_property(command, P_ASYNC, true);          break;        case Q_DISABLE_ASYNC_CLIENT:          set_property(command, P_ASYNC, false);          break;        case Q_DISABLE_TESTCASE:          if (testcase_disabled == 0)            do_disable_testcase(command);          else            die("Test case is already disabled.");          break;        case Q_ENABLE_TESTCASE:          // Ensure we don't get testcase_disabled < 0 as this would          // accidentally disable code we don't want to have disabled.          if (testcase_disabled == 1)            testcase_disabled = false;          else            die("Test case is already enabled.");          break;        case Q_DIE:          /* Abort test with error code and error message */          die("%s", command->first_argument);          break;        case Q_EXIT:          /* Stop processing any more commands */          abort_flag = true;          break;        case Q_SKIP: {          DYNAMIC_STRING ds_skip_msg;          init_dynamic_string(&ds_skip_msg, nullptr, command->query_len);          // Evaluate the skip message          do_eval(&ds_skip_msg, command->first_argument, command->end, false);          char skip_msg[FN_REFLEN];          strmake(skip_msg, ds_skip_msg.str, FN_REFLEN - 1);          dynstr_free(&ds_skip_msg);          if (!no_skip) {            // --no-skip option is disabled, skip the test case            abort_not_supported_test("%s", skip_msg);          } else {            const char *path = cur_file->file_name;            const char *fn = get_filename_from_path(path);            // Check if the file is in excluded list            if (excluded_string && strstr(excluded_string, fn)) {              // File is present in excluded list, skip the test case              abort_not_supported_test("%s", skip_msg);            } else {              // File is not present in excluded list, ignore the skip              // and continue running the test case              command->last_argument = command->end;              skip_ignored = true;  // Mark as noskip pass or fail.            }          }        } break;        case Q_OUTPUT: {          static DYNAMIC_STRING ds_to_file;          const struct command_arg output_file_args[] = {              {"to_file", ARG_STRING, true, &ds_to_file, "Output filename"}};          check_command_args(command, command->first_argument, output_file_args,                             1, ' ');          strmake(output_file, ds_to_file.str, FN_REFLEN);          dynstr_free(&ds_to_file);          break;        }        default:          processed = 0;          break;      }    }    if (!processed) {      current_line_inc = 0;      switch (command->type) {        case Q_WHILE: // 解决 while 代码块          do_block(cmd_while, command); // 该函数内是一个循环          break;        case Q_IF: // 解决 if 代码块          do_block(cmd_if, command);          break;        case Q_ASSERT:          do_block(cmd_assert, command);          break;        case Q_END_BLOCK:          do_done(command);          break;        default:          current_line_inc = 1;          break;      }    } else      check_eol_junk(command->last_argument);    if (command_executed != last_command_executed || command->used_replace) {      /*        As soon as any command has been executed,        the replace structures should be cleared      */      free_all_replace();      /* Also reset "sorted_result", "lowercase" and "skip_if_hypergraph"*/      display_result_sorted = false;      display_result_lower = false;      skip_if_hypergraph = false;    }    last_command_executed = command_executed;    parser.current_line += current_line_inc;    if (opt_mark_progress) mark_progress(&progress_file, parser.current_line);    // Write result from command to log file immediately.    flush_ds_res();  } // while-end|-- // ================= 查看后果 ================= /*    The whole test has been executed _sucessfully_.    Time to compare result or save it to record file.    The entire output from test is in the log file  */  if (log_file.bytes_written()) {    if (result_file_name) {      /* A result file has been specified */      if (record) {        /* Recording */        /* save a copy of the log to result file */        if (my_copy(log_file.file_name(), result_file_name, MYF(0)) != 0)          die("Failed to copy '%s' to '%s', errno: %d", log_file.file_name(),              result_file_name, errno);      } else {        /* Check that the output from test is equal to result file */        check_result();      }    }  } else {    /* Empty output is an error *unless* we also have an empty result file */    if (!result_file_name || record ||        compare_files(log_file.file_name(), result_file_name)) {      die("The test didn't produce any output");    } else {      empty_result = true; /* Meaning empty was expected */    }  }  if (!command_executed && result_file_name && !empty_result)    die("No queries executed but non-empty result file found!");  verbose_msg("Test has succeeded!");  timer_output();  /* Yes, if we got this far the test has suceeded! Sakila smiles */  cleanup_and_exit(0);  return 0; /* Keep compiler happy too */}

参考

MySQL: The MySQL Test Framework

浅析 mysql-test 框架 - 腾讯云开发者社区-腾讯云 (tencent.com)")


欢送关注我的微信公众号【数据库内核】:分享支流开源数据库和存储引擎相干技术。

题目网址
GitHubhttps://dbkernel.github.io
知乎https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault)https://segmentfault.com/u/dbkernel
掘金https://juejin.im/user/5e9d3ed251882538083fed1f/posts
CSDNhttps://blog.csdn.net/dbkernel
博客园(cnblogs)https://www.cnblogs.com/dbkernel