본문 바로가기

MySQL/R&D

MySQL Large table drop issue 분석 (in source code)

1. 배경

DB 를 운영을 하다보면 Table Drop 을 진행할 일이 많다.

 

DB 에 저장된 데이터는 최후의 보루이기때문에(물론 백업본이 있기는하지만), 보통은 테이블 renaming 을 통해 사용되지않는 것을 확인 후 추후 Drop 을 진행한다.

 

InnoDB 의 Table Drop 은 file_per_table=1 일 경우, 물리적인 Table Space 자체를 없애는 것이기때문에 DB 입장에서는 테이블의 크기가 크더라도 시간이 오래 걸릴만한 작업은 아니다.

 

물론, Adaptive Hash Index 같은 것들이 메모리 상에 존재할 경우에는 해당 Hash map 을 해제해야하기때문에 시간이 오래 걸릴 수는 있다.

하지만 일반적인 경우에서 InnoDB 는 OS 에 unlink call 을 하고 InnoDB 는 다시 자기 할 일을 하면 되기때문이다.

 

그런데 사용되지않는 큰 테이블의 Drop 을 진행하다가 연결 되어있던 서비스 Connection 들이 Locking 에 빠진 듯한 현상을 경험한 적이 있다. (show processlist 상 connection 들의 status 는 "Opening table" 였음)

 

해당 글은 MySQL 5.7 에서 왜 저런 현상이 발생한 것인지? 에 대한 내용이다. 

 

 

2. 확인 사항

1)

대부분의 커넥션들은 "Opening tables" 라는 상태 메세지를 보여주었는데, 단순히 "Opening tables" 상태 메세지가 보여지는 구간에 진입했을 때 뮤텍스가 부족해서 해당 메세지가 출력된 것인지?

 

혹은 실제로 "drop table" 이 실행되는 동안 "open_files_limit" 의 범위를 초과해서 "Opening tables" 상태에서 대기를 하는 것인지 확인해보고자 한다.

 

2)

InnoDB 의 Table Drop 작업이 atomic 하게 처리된다면,

drop 도중 crash 가 발생하거나, drop 도중 세션이 끊기거나 하는 등의 현상에서 drop 이 취소가 된다면, rollback 이 될 수도 있다.

이 사항을 확인해보고자 한다.

 

또한 truncate 의 경우 drop 과 비교해서 "LRU scan과 Adaptive hash index 페이지 제거 작업" 이라는 추가적인 작업이 있기때문에, 메모리의 페이지 제거 작업들 이 어느정도 영향을 미치는지 확인해보고자 한다.

 

3)

MySQL 8.0 에서도 drop 시 동일한 상황이 발생하는지 확인한다.

 

4)
MySQL 5.7 과 MySQL 8.0 의 달라진 부분의 소스코드를 비교한다.

 

 

 

 

3. Test

1) Drop 도중 Mutex 변화량

MySQL 버전 : MySQL Community 5.7.17

> drop table t_user_single_product; 
Query OK, 0 rows affected (15.29 sec)

 

* Drop 이전 수치

 

* Drop 이후 수치

 

-> Mutex 의 변화량을 보면 drop 하는 동안은 큰 변화량이 없다가 drop 이 풀리는 시점에 순간적으로 올라가는 것을 볼 수 있다.

 

prometheus 에서 일정 주기로 메타 정보를 수집하는데, drop 하는 동안 수집을 위한 Connection 은 "Checking Permission" 으로 대기를 한다.

[(none) 15:11:42] > show processlist; 
+-------+-------------+-----------------+---------------------+
| Id | User | Host | db | Command | Time | State | Info | 
+-------+-------------+-----------------+---------------------+
| 10314 | system user | NULL
| 10315 | system user | insert into t_user_single_product (cancel_dt, create_dt, last_invoice_id, package_pid, read_end_dt, ... |
| 15673 | prouser | 127.0.0.1:58044 | NULL | Query | 11 | checking permission | SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ifnull(ENGINE, 'NONE') as ENGIN |
| 15675 | prouser | 127.0.0.1:58050 | NULL | Query | 9 | checking permission | SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ifnull(ENGINE, 'NONE') as ENGIN |
...
...
...
+-------+-------------+-----------------+---------------------+

이를 보면 drop 작업이 실행되는 동안 "checking permission" 단계에서 대기하다가, drop 이 끝난 후 이미 높아져있던 뮤텍스 관련 수치를 수집해서 위와 같은 형태 (drop 이 끝나자마자 치솟는 형태) 가 나타났을 것이다.

따라서 분명 drop 하는 동안에 mutex 수치 변화가 있었을테지만, 확인할 길이 없다.

 

 

2) DROP 동안 mysql 서버가 열고 있는 file count (lsof)

실제 Drop 하는 동안 Connection 의 state 는 "Opening table" 였기때문에, 실제 open file limit 에 걸렸지는 않을까 싶어 (정말 혹시나..) 

drop 이 진행되는 동안 mysqld 가 열고 있는 open file count 를 확인했다.

$ vi uzi.sh

#!/bin/bash
while true
do:
  sudo -i lsof | grep mysqld | wc -l >> count.log
done:

drop 하는 동안 "uzi.sh" 라는 스크립트를 실행했다.

 

$ cat count.log

// drop 시작 전
9661
9661

// drop 시작
9809
10028
...

...

12875
12875

// drop 완료
9815
9815

실제 count 를 수집하며 lsof 의 리스트를 확인해보니,

drop 작업으로 인해 올라간 것은 아니고 fd 오픈이 많아졌다.

게다가 늘어난 fd 들은 대부분 prometheus 에서 수집을 위해 요청한 것이었다.

 

 

3) DROP 시 Atomic 처리에 대한 테스트

MySQL 버전 : MySQL Community 5.7.17

 

a. table drop 도중 mysql 서버 다운

1. MySQL > drop table t_user_single_product;


2. kill -9 `pidof mysqld`; kill -9 `pid of mysqld_safe` (drop 작업 도중 서버 강제 shutdown)

 

3. mysqld start

 

$ vi mysql.err

...
...
2019-02-20T18:02:30.986738+09:00 0 [Note] InnoDB: Completed initialization of buffer pool tcmalloc: large alloc 1682784256 bytes == 0x134ea4000 @ 2019-02-20T18:02:32.584097+09:00 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). 2019-02-20T18:02:32.597762+09:00 0 [Note] InnoDB: Opened 2 undo tablespaces 2019-02-20T18:02:32.597796+09:00 0 [Note] InnoDB: 2 undo tablespaces made active 2019-02-20T18:02:32.598102+09:00 0 [Note] InnoDB: Highest supported file format is Barracuda.
tcmalloc: large alloc 1682784256 bytes == 0x134ea4000 @ 2019-02-20T18:02:32.794862+09:00 0 [Note] InnoDB: Log scan progressed past the checkpoint lsn 7064849646784
2019-02-20T18:02:33.069191+09:00 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 7064854889472
...
...
2019-02-20T18:04:18.035018+09:00 0 [Note] InnoDB: Doing recovery: scanned up to log sequence number 7065896779741
2019-02-20T18:04:18.036034+09:00 0 [Note] InnoDB: Database was not shutdown normally! 2019-02-20T18:04:18.036045+09:00 0 [Note] InnoDB: Starting crash recovery. 2019-02-20T18:04:23.552518+09:00 0 [Note] InnoDB: 1 transaction(s) which must be rolled back or cleaned up in total 1 row operations to undo
2019-02-20T18:04:23.552568+09:00 0 [Note] InnoDB: Trx id counter is 8802986496 2019-02-20T18:04:23.552582+09:00 0 [Note] InnoDB: Starting an apply batch of log records to the database...
InnoDB: Progress in percent: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
tcmalloc: large alloc 1682784256 bytes == 0x134ea4000 @ 2019-02-20T18:05:01.354894+09:00 0 [Note] InnoDB: Apply batch completed 2019-02-20T18:05:01.354938+09:00 0 [Note] InnoDB: Last MySQL binlog file position 0 24993868, file name mysql-bin.000047
2019-02-20T18:05:01.767257+09:00 0 [Note] InnoDB: Rolling back trx with id 8802986101, 1 rows to undo
2019-02-20T18:05:01.767429+09:00 0 [Note] InnoDB: Rollback of trx with id 8802986101 completed
...
...

 

4. table 확인

> select uid from t1 limit 5;
+--------+
| uid |
+--------+
| 7126 |
| 7127 |
| 85961 |
| 345773 |
| 538848 |
+--------+

 

-> 서버가 다시 재시작하면서 crash recovery 를 했고, drop 중이던 테이블은 정상적으로 rollback 되었다.

 

b. drop 도중 실행한 세션에서 인터럽트 시그널 (ctrl + c)

MySQL > drop table t_user_single_product;
^C^C -- query aborted
^C^C -- query aborted
Query OK, 0 rows affected (10.69 sec)

drop 하는 세션에서 drop 도중에 인터럽트는 작동하지않는다.

 

 

c. drop 도중 실행한 세션 강제 종료

1. 세션1 [OS 상 pid = 118950] )  MySQL > drop table t_user_single_product;

 

2. 세션2) $ kill -9 118950

 

3. 세션3) MySQL > show processlist;


+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| Id | User | Host | db | Command | Time | State | Info
+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| 7 | test | localhost | boracay_production4 | Query | 12 | checking permissions | drop table t1 |
| 11 | test | localhost | NULL | Query | 0 | starting | show processlist |
+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+

4. 테이블 확인
MySQL > select uid from t_user_single_product limit 5;
ERROR 1146 (42S02): Table 'boracay_production6.t_user_single_product' doesn't exist

OS 상의 세션은 죽었지만, MySQL 상의 "drop command" 는 여전히 남아있어서 계속 DROP 이 실행된다.

-> 결국 테이블은 DROP

 

 

d. drop 을 실행한 커넥션 kill

1. 세션1) MySQL > drop table t_user_single_product;

2. 세션2) MySQL > show processlist;

+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| Id | User | Host | db | Command | Time | State | Info

+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| 11 | test | localhost | boracay_production6 | Query | 1 | checking permissions | drop table t1 |
| 12 | test | localhost | NULL | Query | 0 | starting | show processlist |

+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+

 

3. 세션2) MySQL > kill 11;
Query OK, 0 rows affected (0.00 sec)

4. "3번" 작업에서 "세션1)" 을 kill 시켰기때문에, connection 이 즉시 끊긴다. 
MySQL > drop table t_user_single_product;
ERROR 2013 (HY000): Lost connection to MySQL server during query

 

5. drop 을 실행한 connection 은 끊겼지만, drop command 는 여전히 남아있다.
MySQL > show processlist;
+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| Id | User | Host | db | Command | Time | State
+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+
| 11 | test | localhost | boracay_production6 | Killed | 12 | checking permissions | drop table t1 |
| 12 | test | localhost | NULL | Query | 0 | starting | show processlist |
+----+-------------+-----------------+---------------------+---------+------ +----------------------------------+----------------------------------+

6. 테이블 확인
MySQL > select uid from t_user_single_product limit 5;
ERROR 1146 (42S02): Table 'boracay_production6.t_user_single_product' doesn't exist

DROP 을 실행한 MySQL Connection 에 kill 시그널은 전달 되었고 "Command" 상태도 "Killed" 로 바뀌었지만,

-> 결국 테이블은 drop 처리 됨.

 

 

4) MySQL TRUNCATE table

$ free -mt
          total used free shared buff/cache available
Mem: 128397 51454 67283 57 9660 76288
Swap: 10239 0 10239
Total:138637 51454 77523

MySQL > truncate t_user_single_product;
Query OK, 0 rows affected (15.19 sec)


참고로 drop 시에는 10.69 sec 가 걸렸다.

 

5) MySQL 8.0 에서의 Table DROP / TRUNCATE

a. DROP TABLE

[boracay_production8 17:35:37] > \! df -h
                              Filesystem Size Used Avail Use%
Mounted on /dev/nvme0n1p5 5.5T 584G 5.0T 11% /data

[boracay_production8 17:35:39] > drop table t1;
Query OK, 0 rows affected (0.00 sec)

[boracay_production8 17:35:44] > \! df -h
                              Filesystem Size Used Avail Use% 
Mounted on /dev/nvme0n1p5 5.5T 161G 5.4T 3% /data

-> 아주 금방 DROP 이 완료된다.

b. TRUNCATE TABLE

[boracay_production2 17:44:40] > truncate table t_slidestand;
Query OK, 0 rows affected (0.00 sec)

-> DROP 과 동일하게 바로 완료된다.

 

 

6) Review

사전에 확인하고자하는 것들은 모두 확인했다.

일부 확인 자체가 불가능한 부분들도 있었고, MySQL 5.7 의 테이블에 대한 Atomic 처리에 대해서도 확인했다.

 

허나 MySQL 8.0 에서는 DROP 과 TRUNCATE 모두 금방 완료가 되었다.

 

manual 상으로는 5.7 과 8.0 모두 atomic DDL 을 하고 있다고 나와있는데 5.7, 8.0 의 처리가 각각 다르니,

source code 상으로는 어떤 부분이 다를지 확인해보면 되겠다.

 

 

4. MySQL 5.7 vs MySQL 8.0 소스코드 비교

1) MySQL 5.7 delete_table function

* 8.0과 다른 로직이 포함된 소스 코드 라인
*********************************************************************** 
************* 이 부분이 8.0 에는 없지만 5.7에서는 수행하는 부분 ***************** 
****************** 롤백 세그먼트를 언두영역에 allocate 함 ******************** 
***********************************************************************

for (iter = parent_trx->mod_tables.begin(); iter != parent_trx->mod_tables.end(); ++iter) 
{
  dict_table_t* table_to_drop = *iter;
  if (strcmp(norm_name, table_to_drop->name.m_name) == 0) { // erase 함수는 innodbase/include/trx0types.h 파일에 정의
    // ***
    // private:
    // typedef std::vector<trx_rseg_t*, ut_allocator<trx_rseg_t*> > // trx_rsegs_t; // trx_rseg_t=롤백 세그먼트
    // ***
    
    // 결국 이 라인에서 롤백 세그먼트를 메모리에 allocator
    parent_trx->mod_tables.erase(table_to_drop);
    break; 
  }
} 
*********************************************************************** 
*********************************************************************** 
***********************************************************************

 

* 전체 코드

더보기

int ha_innobase::delete_table( {

dberr_t THD* char

err;
thd = ha_thd();

norm_name[FN_REFLEN];

DBUG_ENTER("ha_innobase::delete_table");

DBUG_EXECUTE_IF( "test_normalize_table_name_low", test_normalize_table_name_low();

); DBUG_EXECUTE_IF(

"test_ut_format_name",

test_ut_format_name(); );

/* Strangely, MySQL passes the table name without the '.frm' extension, in contrast to ::create */

// 입력받은 테이블의 네임을 normalize 한 테이블 네이밍으로 norm_name 에 컨버팅 normalize_table_name(norm_name, name);

// 세션 id 할당
innodb_session_t*& priv = thd_to_innodb_session(thd);

// 드랍할 테이블을 룩업
dict_table_t* handler = priv->lookup_table_handler(norm_name);

if (handler != NULL) {

// 테이블의 첫 노드 스캔 ( = primary 의 첫 노드)
// 8.0 과 똑같은 형태로 release 함.
for (dict_index_t* index = UT_LIST_GET_FIRST(handler->indexes);

index != NULL && index->last_ins_cur;
index = UT_LIST_GET_NEXT(indexes, index)) { /* last_ins_cur and last_sel_cur are allocated

together ,therfore checking only last_ins_cur before releasing mtr */

index->last_ins_cur->release();

index->last_sel_cur->release(); }

} else if (srv_read_only_mode
|| srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN) {

DBUG_RETURN(HA_ERR_TABLE_READONLY); }

trx_t* parent_trx = check_trx_exists(thd); TrxInInnoDB trx_in_innodb(parent_trx);

// 주어진 트랜잭션에 의해 수정된 테이블을 저장하는데 사용되는 유형

// 테이블 객체는 수정된 트랜잭션이 실행되는 동안은 테이블 객체가 파기되면 안된다는 것을 알고 있기때문 에, 메모리에 테이블 오브젝트에 대한 포인터를 저장

trx_mod_tables_t::const_iterator iter;

// trx_t::mod_tables = 트랜잭션에 의해 변경된 테이블 리스트 // iter 값이 끝날때까지 iter 을 증가시키면서, for 문 수행

*********************************************************************** ************* 이 부분이 8.0 에는 없지만 5.7에서는 수행하는 부분 ***************** ****************** 롤백 세그먼트를 언두영역에 allocate 함 ******************** ***********************************************************************

for (iter = parent_trx->mod_tables.begin(); iter != parent_trx->mod_tables.end();

++iter) {

dict_table_t* table_to_drop = *iter;

if (strcmp(norm_name, table_to_drop->name.m_name) == 0) { // erase 함수는 innodbase/include/trx0types.h 파일에 정의
// ***
// private:

// typedef std::vector<trx_rseg_t*, ut_allocator<trx_rseg_t*> > // trx_rsegs_t; // trx_rseg_t=롤백 세그먼트
// ***
// 결국 이 라인에서 롤백 세그먼트를 메모리에 allocator parent_trx->mod_tables.erase(table_to_drop);

break; }

} *********************************************************************** *********************************************************************** ***********************************************************************

trx_t* trx = innobase_trx_allocate(thd);

ulint name_len = strlen(name);

ut_a(name_len < 1000);

/* Either the transaction is already flagged as a locking transaction or it hasn't been started yet. */

// 작업을 위해 lock 대기 ut_a(!trx_is_started(trx) || trx->will_lock > 0);

/* We are doing a DDL operation. */ // lock 획득
++trx->will_lock;

/* Drop the table in InnoDB */

// 실제 drop table 진행
err = row_drop_table_for_mysql(

norm_name, // 테이블 네임
trx, // 할당받은 innodb 트랜잭션
thd_sql_command(thd) == SQLCOM_DROP_DB, // SQLCOM_DROP_DB 실행 true, handler);

// 테이블을 찾을 수 없다는 말은? 해당 테이블은 파티션 테이블이다. // concat(table name, '#') 한 리스트들을 삭제
if (err == DB_TABLE_NOT_FOUND) {

uint len = (uint) strlen(norm_name); ulint num_partitions;
ut_a(len < FN_REFLEN); norm_name[len] = '#';

norm_name[len + 1] = 0;
err = row_drop_database_for_mysql(norm_name, trx,

&num_partitions); norm_name[len] = 0;

if (num_partitions == 0
&& !row_is_mysql_tmp_table_name(norm_name)) {

table_name_t tbl_name; tbl_name.m_name = norm_name; ib::error() << "Table " << tbl_name <<

" does not exist in the InnoDB"
" internal data dictionary though MySQL is" " trying to drop it. Have you copied the .frm" " file of the table to the MySQL database"
" directory from another database? "
<< TROUBLESHOOTING_MSG;

}
if (num_partitions == 0) {

err = DB_TABLE_NOT_FOUND; }

}

if (err == DB_TABLE_NOT_FOUND
&& innobase_get_lower_case_table_names() == 1) {

#ifdef _WIN32
char* is_part = strstr(norm_name, "#p#");

#else
char* is_part = strstr(norm_name, "#P#");

#endif /* _WIN32 */

if (is_part != NULL) {
char par_case_name[FN_REFLEN];

#ifndef _WIN32
/* Check for the table using lower

#else

case name, including the partition separator "P" */ strcpy(par_case_name, norm_name); innobase_casedn_str(par_case_name);

/* On Windows platfrom, check
whether there exists table name in
system table whose name is
not being normalized to lower case */ create_table_info_t::normalize_table_name_low(

par_case_name, name, FALSE); #endif /* _WIN32 */

err = row_drop_table_for_mysql(
par_case_name, trx,
thd_sql_command(thd) == SQLCOM_DROP_DB, true, handler);

} }

================= 테이블 drop 완료 ===============

if (handler == NULL) {
ut_ad(!srv_read_only_mode);
/* Flush the log to reduce probability that the .frm files and

the InnoDB data dictionary get out-of-sync if the user runs with innodb_flush_log_at_trx_commit = 0 */

log_buffer_flush_to_disk(); } else if (err == DB_SUCCESS) {

// 드랍한 테이블을 핸들러 리스트에서 제거

priv->unregister_table_handler(norm_name); }

// 트랜잭션 커밋 innobase_commit_low(trx);

// 트랜잭션 릴리즈 trx_free_for_mysql(trx);

DBUG_RETURN(convert_error_code_to_mysql(err, 0, NULL)); }

 

2) MySQL 8..0 delete_table function

* 5.7과 다른 로직이 포함된 소스 코드 라인

*********************************************************************** 
************************* 8.0에서 추가된 부분 ***************************** 
******** Data Dictionary 에 접근하여 해당 테이블에 DDL 작업이 없으면 ************ 
********************** LRU 가 아닌 목록으로 이동시킨다. ********************** 
*************** InnoDB의 테이블 이름이 DD 객체와 일치하지 않을 때 *************** 
**************** DD 객체를 통해 나중에 테이블을 열지 못하게한다. *****************
***********************************************************************
// DD 테이블이 null 이 아니고 && DD 테이블이 persistent 해야 if 문 실행 

if (dd_tab != nullptr && dd_tab->is_persistent()) {
  dict_table_t *tab;
  dd::cache::Dictionary_client *client = dd::get_dd_client(thd); 
  dd::cache::Dictionary_client::Auto_releaser releaser(client);
  // global DD 객체 기반의 innodb inMemory table metadata 를 int err 에 넣음 
  int err = dd_table_open_on_dd_obj(
  		client, // data dictionary client 
        dd_tab->table(), // DD table object 
        (!dd_table_is_partitioned(dd_tab->table())
		? nullptr : reinterpret_cast<const dd::Partition *>(dd_tab)), // DD partition OR subpartition 
        norm_name, // table Name
		tab, // Innodb Table
		thd); // Thread THD
        
// err = 0 은 SUCCESS CODE 
  if (err == 0 && tab != nullptr) {
  // can_be_evicted = 이 테이블이 메모리에 보관 될 것으로 예상되면 - TRUE 
  // 이 때 이 테이블은 FK 관계가 있 거나 DDL 중일 수도 있음.
  if (tab->can_be_evicted && dd_table_is_partitioned(dd_tab->table())) { 
  	// dictionary 시스템에 뮤텍스를 얻고 진입 
    mutex_enter(&dict_sys->mutex);
	// DDL 작업이 없으면 "LRU 가 아닌 목록" 으로 이동 
    dict_table_ddl_acquire(tab);
	mutex_exit(&dict_sys->mutex); }
    
  // 해당 MySQL 서버가 file_per_table 인지 아닌지? 
  file_per_table = dict_table_is_file_per_table(tab); 
  // DD table close
  dd_table_close(tab, thd, nullptr, false);
} }
***********************************************************************
***********************************************************************
***********************************************************************

 

* 전체 코드

더보기

int ha_innobase::delete_table(const char *name, const dd::Table *table_def) { if (table_def != NULL &&

dict_sys_t::is_dd_table_id(table_def->se_private_id())) { my_error(ER_NOT_ALLOWED_COMMAND, MYF(0)); return (HA_ERR_UNSUPPORTED);

}

// 핸들러 쓰레드를 get() 하고, 해당 쓰레드가 정상적인지 check_trx_exists() THD *thd = ha_thd();
trx_t *trx = check_trx_exists(thd);

// 데이터딕셔너리의 table_def 가 null 이 아닌지 && persistent 한지 확인 if (table_def != nullptr && table_def->is_persistent()) {

// mysql 2pc 코디네이터가 DML 을 하는 MySQL 핸들러에게 innodb transaction 을 할당하는 함수 innobase_register_trx(ht, thd, trx);

}

// 트랜잭션을 할당받은 thread 가 drop 을 수행하도록 function call

return (innobase_basic_ddl::delete_impl(thd, name, table_def)); }

int innobase_basic_ddl::delete_impl(THD *thd, const char *name, const Table *dd_tab) {

dberr_t error = DB_SUCCESS; char norm_name[FN_REFLEN];

DBUG_EXECUTE_IF("test_normalize_table_name_low", test_normalize_table_name_low(););

DBUG_EXECUTE_IF("test_ut_format_name", test_ut_format_name(););

/* Strangely, MySQL passes the table name without the '.frm' extension, in contrast to ::create */

// 입력받은 테이블의 네임을 normalize 한 테이블 네이밍으로 norm_name 에 컨버팅 normalize_table_name(norm_name, name);

// 스레드의 세션 아이디 할당
innodb_session_t *&priv = thd_to_innodb_session(thd);

// 드랍할 테이블 룩업
dict_table_t *handler = priv->lookup_table_handler(norm_name);

if (handler != NULL) {

// dict_index_t = 인덱스를 위한 데이터 스트럭쳐
// 테이블의 첫 노드 스캔 (= primary 의 첫 노드)
for (dict_index_t *index = UT_LIST_GET_FIRST(handler->indexes);

index != NULL && index->last_ins_cur;

index = UT_LIST_GET_NEXT(indexes, index)) { /* last_ins_cur and last_sel_cur are allocated together,therfore only checking last_ins_cur before releasing mtr */ index->last_ins_cur->release(); index->last_sel_cur->release();

}
} else if (srv_read_only_mode ||

srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN) { return (HA_ERR_TABLE_READONLY);

}

trx_t *trx = check_trx_exists(thd);

TrxInInnoDB trx_in_innodb(trx);

ulint name_len = strlen(name);

ut_a(name_len < 1000);

// 트랜잭션 시작을 기다리거나, lock 획득을 기다림 ut_a(!trx_is_started(trx) || trx->will_lock > 0);

/* We are doing a DDL operation. */ // lock 확득 will_Lock + 1 ++trx->will_lock;

bool file_per_table = false;

*********************************************************************** ************************* 8.0에서 추가된 부분 ***************************** ******** Data Dictionary 에 접근하여 해당 테이블에 DDL 작업이 없으면 ************ ********************** LRU 가 아닌 목록으로 이동시킨다. ********************** *************** InnoDB의 테이블 이름이 DD 객체와 일치하지 않을 때 *************** **************** DD 객체를 통해 나중에 테이블을 열지 못하게한다. ***************** ***********************************************************************

// DD 테이블이 null 이 아니고 && DD 테이블이 persistent 해야 if 문 실행 if (dd_tab != nullptr && dd_tab->is_persistent()) {

dict_table_t *tab;

dd::cache::Dictionary_client *client = dd::get_dd_client(thd); dd::cache::Dictionary_client::Auto_releaser releaser(client);

// global DD 객체 기반의 innodb inMemory table metadata 를 int err 에 넣음 int err = dd_table_open_on_dd_obj(

client, // data dictionary client dd_tab->table(), // DD table object (!dd_table_is_partitioned(dd_tab->table())

? nullptr

: reinterpret_cast<const dd::Partition *>(dd_tab)), // DD partition OR subpartition norm_name, // table Name
tab, // Innodb Table
thd); // Thread THD

// err = 0 은 SUCCESS CODE if (err == 0 && tab != nullptr) {

// can_be_evicted = 이 테이블이 메모리에 보관 될 것으로 예상되면 - TRUE // 이 때 이 테이블은 FK 관계 가 있거나 DDL 중일 수도 있음.

if (tab->can_be_evicted && dd_table_is_partitioned(dd_tab->table())) { // dictionary 시스템에 뮤텍스를 얻고 진입 mutex_enter(&dict_sys->mutex);
// DDL 작업이 없으면 "LRU 가 아닌 목록" 으로 이동 dict_table_ddl_acquire(tab);

mutex_exit(&dict_sys->mutex); }

// 해당 MySQL 서버가 file_per_table 인지 아닌지? file_per_table = dict_table_is_file_per_table(tab); // DD table close
dd_table_close(tab, thd, nullptr, false);

} }

*********************************************************************** *********************************************************************** ***********************************************************************

// 실제 Drop 하는 funbction call
// handler 에는 drop 할 테이블이 룩업 되어있음.
// 데이터 딕셔너리가 다른 트랜잭션에 의해 이미 잠겨져 있지 않다면 트랜잭션은 커밋됨. // 만약, 잠겨있다면 데이터 딕셔너리가 잠긴 상태로 유지
error = row_drop_table_for_mysql(norm_name, trx, true, handler);

========================= 테이블 drop commit =======================

// handler 가 nullptr 이 아니고 && 위 row_drop_table_for_mysql 문이 성공했다면 if 문 진입 if (handler != nullptr && error == DB_SUCCESS) {

// 드랍한 테이블을 핸들러 리스트에서 제거

priv->unregister_table_handler(norm_name); }

// DD 에서 drop 된 테이블 정보 제거
if (error == DB_SUCCESS && file_per_table) {

dd::Object_id dd_space_id = dd_first_index(dd_tab)->tablespace_id(); dd::cache::Dictionary_client *client = dd::get_dd_client(thd); dd::cache::Dictionary_client::Auto_releaser releaser(client);

if (dd_drop_tablespace(client, thd, dd_space_id) != 0) { error = DB_ERROR;

} }

return (convert_error_code_to_mysql(error, 0, NULL));

}

int convert_error_code_to_mysql(
dberr_t error, /*!< in: InnoDB error code */
ulint flags, /*!< in: InnoDB table flags, or 0 */
THD *thd) /*!< in: user thread handle or NULL */

{
switch (error) {

case DB_SUCCESS: return (0);

case ... ...

...

...

...

} }

 

3) MySQL 8.0 DROP TABLE 이 빠른 이유

소스코드에서도 확인했듯이, MySQL 5.7 에서는 atomic 처리를 위해 drop 시 rollback segment 를 undo 영역에 할당하게 된다.

 

이 과정에서 실제 테이블 스페이스를 구성하고 있는 모든 extent 들의 메타 정보를 읽어서 메모리에 올리기때문에,

공유 자원을 사용하는 MySQL 에서는 많은 부하를 일으키게 된다.

(테이블스페이스의 크기가 크면 클수록 더 많은 부하를 가짐)

 

MySQL 8.0 도 atomic 한 처리를 지원하지만, 지원하는 방식이 다르다.
테이블의 메타정보들을 이미 "Data Dictionary (DD)" 에서 관리하고 있기때문에, 실제 물리적인 테이블스페이스에서 읽어올 필요가 없다.

다만 내부적으로, atomic 한 처리를 위해 innodb_ddl_log 테이블에 롤백을 위한 4단계의 사전 작업이 이루어진다.

 

  1. Prepare : mysql.innodb_ddl_log 라는 시스템 테이블에 "roll forward+roll back" 을 위한 DDL operation 을 정의

  2. Perform : 실제 DDL operation 수행 (ex. create table / drop table ... )

  3. Commit : Data Dictionary 를 갱신하고 Data Dictionary Transaction 을 Commit

  4. Post-DDL : 1번에서 정의한 DDL operation 을 실행하고 삭제. Post-DDL 단계에서 DROP TABLE / TRUNCATE TABLE 같이 테이블을 rollback 할 필

    요성이 있는 DDL 들을 지원하는 "mysql.innodb_dynamic_metadata" 의 동적 메타데이터를 삭제한다.

 

※ innodb_ddl_log / innodb_dynamic_metadata 의 스키마 (debug mode 로 컴파일 후 debug mode 를 통해 조회 가능)

> SHOW CREATE TABLE mysql.innodb_ddl_log
| innodb_ddl_log | CREATE TABLE `innodb_ddl_log` (

`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `thread_id` bigint(20) unsigned NOT NULL,
`type` int(10) unsigned NOT NULL,
`space_id` int(10) unsigned DEFAULT NULL,

`page_no` int(10) unsigned DEFAULT NULL,
`index_id` bigint(20) unsigned DEFAULT NULL,
`table_id` bigint(20) unsigned DEFAULT NULL,
`old_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `new_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`),

KEY `thread_id` (`thread_id`)
) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB AUTO_INCREMENT=96 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC |

> SHOW CREATE TABLE mysql.innodb_dynamic_metadata
| innodb_dynamic_metadata | CREATE TABLE `innodb_dynamic_metadata` (

`table_id` bigint(20) unsigned NOT NULL, `version` bigint(20) unsigned NOT NULL, `metadata` blob NOT NULL,
PRIMARY KEY (`table_id`)

) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC |

 

5. 정리

테스트 결과에서 보았듯이 MySQL 5.7 기준에서 atomic 하게 처리되는 로직은 정상적으로 작동한다. (확인된 케이스는 drop 도중 서버 강제 shutdown 시)

 

하지만 drop table 이 진행되고 있다면 서버를 강제 shutdown 하지 않는 이상 drop 작업의 rollback 은 어렵다.

 

서버에 트래픽이 많은 상황에서 drop table 을 한다면 다수의 커넥션들이 막혀있는 현상을 겪을 수 있는데, 이럴 경우 테스트 의 결과처럼 서버를 강제로 shutdown 시키지 않는 한 작업을 취소할 수 없다.

 

따라서 큰 테이블 drop 이나 truncate 시에는 민감한 서비스의 경우 트래픽을 고려하여 진행해야한다.

 

반면에 MySQL 8.0 에서는 "Data Dictionary" 로 DROP/TRUNCATE 의 atomic 처리를 지원하기때문에, MySQL 5.7 에서 발생하는 문제들은 발생하지않는다.