본문 바로가기

MySQL

MySQL Command Line Clinet에 Online DDL 모니터링 기능 추가

Online DDL 모니터링

MySQL 클라이언트에서 Online DDL로 인덱스를 추가하다보면 작업이 생각외로 꽤 오랜 시간동안(상대적이지만) 수행되는 경우가 많다. 서비스 운영 중에는 언제 끝날지 모르는 인덱스 추가 작업으로 인해 심장이 덜컥 내려앉을 때도 있다.

 

다행히 MySQL 서버에서는 Online DDL 진행 상황을 모니터링 할 수 있는 기능을 제공한다. 아래 명령을 별도의 세션에서 수행하면 Online DDL 작업이 어느 정도까지 진행되었는지 확인할 수 있다.

(ref. https://dev.mysql.com/doc/refman/8.0/en/monitor-alter-table-performance-schema.html)

 

mysql> SELECT EVENT_NAME, WORK_COMPLETED, WORK_ESTIMATED
       FROM performance_schema.events_stages_current;
+------------------------------------------------------+----------------+----------------+
| EVENT_NAME                                           | WORK_COMPLETED | WORK_ESTIMATED |
+------------------------------------------------------+----------------+----------------+
| stage/innodb/alter table (read PK and internal sort) |            280 |           1245 |
+------------------------------------------------------+----------------+----------------+
1 row in set (0.01 sec)

 

하지만 Online DDL 작업과 동시에 한 화면에서 모니터링도 할 수 있는 것은 아니기 때문에 항상 다른 세션으로 접속하여 모니터링 해야 한다. 보통 끝나겠거니 생각하는 경우가 많고, 매번 진행 상황을 모니터링 하지는 않기 때문에 필요한 기능이면서도 사실상 잘 사용하지 않는 기능이기도 하다.

그래서 이 좋은 기능을 Online DDL 작업과 함께 한 눈에 확인할 수 있도록 MySQL 클라이언트에 일부 기능을 추가해보았다. 소스 코드는 간단하기 때문에 본 글의 설명 또한 간단하게 설명하였다.

<sql-common/client.cc>

코드 수정 및 추가 파일

https://github.com/godtechwak/mysql-server-custom/blob/8.0/sql-common/client.cc

코드 수정 및 추가 라인

https://github.com/godtechwak/mysql-server-custom/blob/21f2b36840788a7053d64ff9614ccd04df3db804/sql-common/client.cc#L7968-L8056

 

Online DDL Progress 기능 추가

MySQL 클라이언트를 디버깅해보면 쿼리문을 수행할 때,

main() --> read_and_execute() --> readline() --> com_go() --> mysql_real_query_for_lazy() --> mysql_real_query() --> mysql_send_query() 순으로 함수가 실행되는 것을 확인할 수 있다.

Online DDL Progress 기능은 이 함수들 중에서 mysql_real_query() 함수가 정의되어 있는 sql-common/client.cc 파일에 추가해주면 된다.

아래 코드는 수정된 mysql_real_query() 함수이며, ALTER TABLE 명령이 수행될 때만 monitorDDLProgress() 함수를 실행한다.

 

int STDCALL mysql_real_query(MYSQL *mysql, const char *query, ulong length) {
  int retval;
  DBUG_TRACE;
  DBUG_PRINT("enter", ("handle: %p", mysql));
  DBUG_PRINT("query", ("Query = '%-.*s'", (int)length, query));
  DBUG_EXECUTE_IF("inject_ER_NET_READ_INTERRUPTED", {
    mysql->net.last_errno = ER_NET_READ_INTERRUPTED;
    DBUG_SET("-d,inject_ER_NET_READ_INTERRUPTED");
    return 1;
  });
  
  //by silver
  ConnInfoSub connInfoSub = {
    mysql->host,
    mysql->user,
    mysql->passwd,
    mysql->db,
    mysql->port,
    mysql->unix_socket,
    mysql->client_flag
  };

  //ALTER TABLE문을 수행하는 경우에만 체크 by silver
  if (strcasestr(query, "alter table")) {
    std::thread queryThread(monitorDDLProgress, connInfoSub);
    if (mysql_send_query(mysql, query, length)) return 1;
    queryThread.join();
  } else {
    if (mysql_send_query(mysql, query, length)) return 1;
  }
  
  retval = (int)(*mysql->methods->read_query_result)(mysql);
  mysql_extension_bind_free(MYSQL_EXTENSION_PTR(mysql));
  return retval;
}

 

아래는 monitorDDLProgress() 함수 정의이며, 이 함수는 Online DDL이 수행됨과 동시에 진행 상황을 MySQL 클라이언트에 출력한다.

 

//by silver
void monitorDDLProgress(const ConnInfoSub& connInfoSub) {
    MYSQL *mysql = mysql_init(NULL);   
    char ddl_percentage[10] = "";
    char old_ddl_percentage[10] = "";

    //시작 시간
    std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now();

    mysql_real_connect(mysql, 
                       connInfoSub.host, 
                       connInfoSub.user, 
                       connInfoSub.passwd,
                       connInfoSub.db, 
                       connInfoSub.port, 
                       connInfoSub.unix_socket,
                       connInfoSub.client_flag);

    while (true) {
        sleep(5); //DDL 수행을 보장하기 위해서 최소 5초간 대기한다.

        MYSQL_RES *result = nullptr;
        char cmd1[] = "SELECT ROUND(WORK_COMPLETED/WORK_ESTIMATED*100, 2) AS percentage FROM performance_schema.events_stages_current ORDER BY WORK_COMPLETED DESC LIMIT 1;";
        mysql_query(mysql, cmd1);
        result = mysql_use_result(mysql);
        MYSQL_ROW row = mysql_fetch_row(result);

        if (row == NULL) {
          puts("DDL is done...\n");
          mysql_free_result(result);
          mysql_thread_end(); //스레드 종료
          break;
        }

        strcpy(ddl_percentage, row[0]);

        std::chrono::duration<double> elapsed_time = std::chrono::steady_clock::now() - start_time;
        int total_seconds = static_cast<int>(elapsed_time.count());
        int milliseconds = static_cast<int>((elapsed_time.count() - total_seconds) * 1000);

        int minutes = total_seconds / 60;
        int seconds = total_seconds % 60;

        if (strcmp(ddl_percentage, old_ddl_percentage) != 0) {
          printf("Online DDL Progress: %s(%%) (%dmin %dsec %dms)\n", ddl_percentage, minutes, seconds, milliseconds);
        } else {
          printf("Online DDL Progress: 99.99(%%)..almost done (%dmin %dsec %dms)\n", minutes, seconds, milliseconds);
        }

        strcpy(old_ddl_percentage, ddl_percentage);
        mysql_free_result(result);
    }
    mysql_close(mysql);
}

 

인덱스 추가하여 Online DDL 진행 상황 모니터링

1. 약 1GB 크기의 테이블을 준비한다.

 

mysql> SELECT A.table_name, CASE WHEN LENGTH(A.index_data_size) > 3 AND LENGTH(A.index_data_size) < 7 THEN CONCAT(ROUND(A.index_data_size/1024, 2), '(KB)') WHEN LENGTH(A.index_data_size) > 6 AND LENGTH(A.index_data_size) < 10 THEN CONCAT(ROUND(A.index_data_size/1024/1024, 2), '(MB)') WHEN LENGTH(A.index_data_size) > 9 THEN CONCAT(ROUND(A.index_data_size/1024/1024/1024, 2), '(GB)') ELSE A.index_data_size END AS index_data_size, concat(case when 10 - char_length(repeat('+', (A.index_data_size/total_size*100)/10)) != 0 then concat(repeat('+', (A.index_data_size/total_size*100)/10), repeat('_', (10 - char_length(repeat('+', (A.index_data_size/total_size*100)/10))))) else repeat('+', (A.index_data_size/total_size*100)/10) end, '(', round((A.index_data_size/total_size*100), 1), '%)') as 'Percentage(%)', A.table_rows FROM( SELECT table_name, table_schema, SUM(index_length + data_length) AS index_data_size, MAX(table_rows) AS table_rows, (SELECT SUM(index_length + data_length) FROM information_schema.tables WHERE table_schema = 'test' ) AS total_size FROM information_schema.tables WHERE table_name = 'test' GROUP BY table_name ORDER BY index_data_size) AS A
+------------+-----------------+-------------------+------------+
| table_name | index_data_size | Percentage(%)     | table_rows |
+------------+-----------------+-------------------+------------+
| test       | 0.96(GB)        | +++++_____(54.7%) |   16045362 |
+------------+-----------------+-------------------+------------+

 

2. 인덱스를 추가한다.

 

mysql> ALTER TABLE test ADD INDEX ix_userid(user_id), ALGORITHM=inplace, LOCK=none;

 

3. Online DDL 진행 상황을 모니터링 한다.

 

 

 

추가적으로 필요한 기능이 있다면(sleep 5초를 변경한다던지, 출력 포맷을 변경한다던지) client.cc 파일의 관련 부분을 수정하여 사용하면 되겠다. 한 가지 주의해야할 점이 있다면, make 명령은 /mysql-server 디렉터리에서 수행하지 않고, /mysql-server/client 디렉터리에서 수행해야 한다. 만일 /mysql-server에서 make 명령을 수행하면 아래와 같은 오류를 맞이하게 될 것이다.

 

더보기

Consolidate compiler generated dependencies of target sql_main

[ 43%] Building CXX object sql/CMakeFiles/sql_main.dir/__/sql-common/client.cc.o

[ 43%] Linking CXX static library ../archive_output_directory/libsql_main.a

[ 50%] Built target sql_main

[ 53%] Built target innobase

[ 53%] Built target minchassis

[ 53%] Linking CXX shared library library_output_directory/libserver_unittest_library.dylib

Undefined symbols for architecture arm64:

"_mysql_query", referenced from:

monitorDDLProgress(ConnInfoSub const&) in libsql_main.a(client.cc.o)

"_mysql_thread_end", referenced from:

monitorDDLProgress(ConnInfoSub const&) in libsql_main.a(client.cc.o)

ld: symbol(s) not found for architecture arm64

clang: error: linker command failed with exit code 1 (use -v to see invocation)

make[2]: *** [library_output_directory/libserver_unittest_library.dylib] Error 1

make[1]: *** [CMakeFiles/server_unittest_library.dir/all] Error 2

make: *** [all] Error 2

 

mysql 클라이언트 프로그램은 실행 파일이 /mysql-server/client 디렉터리에서 만들어지기 때문에 빌드는 /mysql-server/client에서만 수행해주면 된다.