2023. 8. 28. 20:02ㆍ데이터베이스
MySQL은 오픈소스 서비스입니다.
그래서 공식 홈페이지에 가면 쉽게 소스를 다운받을 수 있습니다. 다운받는 과정까지가 가장 쉽습니다
다운 받고 압축을 풀면, vscode로 열어서 코드를 확인할 수 있습니다.
'소스 설치' 를 할 것이 아니라면 어느 OS 에서든지 그냥 아무거나 받아서 봐도 됩니다.
소스를 다운받는 것에 큰 의미는 없고,
유능한 DBA가 되려면 언젠가 소스를 까볼 일이 생길 것만 같았습니다.
그래서 오늘은! 칼을 뽑은 김에
테이블을 삭제하는 프로세스가 내부적으로 어떻게 일어나는지 살펴보겠습니다.
참고로 이 글은 C/C++을 학부시절에만 만져봐서 뭐가 어떻게 흘러가는지 모를 사람에 의해 쓰여지고 있습니다.
가장 아래에 첨부된 소스코드 가이드가 있긴 한데, 워낙 프로젝트가 방대해서 도움이 잘 되지는 않습니다.
무작정 Ctrl + Shift + F로 코드를 찾아가보겠습니다.
DROP TABLE 관련해서 이리저리 찾아보던 중, storage/innobae/ddl 패키지에 ddl0ddl.cc 라는 파일이 있었습니다.
dberr_t drop_table(trx_t *trx, dict_table_t *table) noexcept {
ut_ad(!srv_read_only_mode);
/* There must be no open transactions on the table. */
ut_a(table->get_ref_count() == 0);
return row_drop_table_for_mysql(table->name.m_name, trx, false, nullptr);
}
그래도 주석이나 변수명이 명확해서 흐름이 어렴풋이 보이기는 합니다.
- read only mode 이면 중지
- 활성 트랜잭션 갯수가 0이 아니면 대기
- 모두 통과하면 row_drop_table_for_mysql() 함수를 호출한 결과를 return 합니다.
정체불명의 ut_ad와 ut_a의 코드를 살펴볼까요?
ut0dbg.h 라는 요상한 파일명을 가진 코드에 포함되어있습니다.
/** Abort execution if EXPR does not evaluate to nonzero.
@param EXPR assertion expression that should hold */
#define ut_a(EXPR) \
do { \
if (unlikely(false == (bool)(EXPR))) { \
ut_dbg_assertion_failed(#EXPR, __FILE__, __LINE__); \
} \
} while (0)
/** Abort execution. */
#define ut_error ut_dbg_assertion_failed(0, __FILE__, __LINE__)
#ifdef UNIV_DEBUG
/** Debug assertion. Does nothing unless UNIV_DEBUG is defined. */
#define ut_ad(EXPR) ut_a(EXPR)
/** Debug statement. Does nothing unless UNIV_DEBUG is defined. */
코드의 일부분입니다.
ut_a 함수는 EXPR로 들어온 조건이 만족되지 않으면 do while문을 계속 돌면서 대기하게 됩니다.
그래서 활성 트랜잭션이 0이 될때까지 drop table이 수행되지 않고 대기할 수 있었던 거네요.
ut_ad는... 저게 전부입니다. 잘 모르겠네요. ㅎㅎ 넘어갑니다.
대망의 row_drop_table_for_mysql 함수를 살펴봅시다. 약 400줄의 엄청난 함수입니다.
row0mysql.cc 라는 파일에 기록되어있습니다.
/** Drop a table for MySQL. If the data dictionary was not already locked
by the transaction, the transaction will be committed. Otherwise, the
data dictionary will remain locked.
@param[in] name Table name
@param[in] trx Transaction handle
@param[in] nonatomic Whether it is permitted to release
and reacquire dict_operation_lock
@param[in,out] handler Table handler or NULL
@return error code or DB_SUCCESS */
dberr_t row_drop_table_for_mysql(const char *name, trx_t *trx, bool nonatomic,
dict_table_t *handler) {
dberr_t err = DB_SUCCESS;
dict_table_t *table = nullptr;
char *filepath = nullptr;
bool locked_dictionary = false;
THD *thd = trx->mysql_thd;
dd::Table *table_def = nullptr;
bool file_per_table = false₩;
aux_name_vec_t aux_vec;
DBUG_TRACE;
DBUG_PRINT("row_drop_table_for_mysql", ("table: '%s'", name));
ut_a(name != nullptr);
/* Serialize data dictionary operations with dictionary mutex:
no deadlocks can occur then in these operations */
trx->op_info = "dropping table";
if (handler != nullptr && handler->is_intrinsic()) {
table = handler;
}
if (table == nullptr) {
if (trx->dict_operation_lock_mode != RW_X_LATCH) {
/* Prevent foreign key checks etc. while we are
dropping the table */
row_mysql_lock_data_dictionary(trx, UT_LOCATION_HERE);
locked_dictionary = true;
nonatomic = true;
}
ut_ad(dict_sys_mutex_own());
ut_ad(rw_lock_own(dict_operation_lock, RW_LOCK_X));
table = dict_table_check_if_in_cache_low(name);
/* If it's called from server, then it should exist in cache */
if (table == nullptr) {
/* MDL should already be held by server */
int error = 0;
table = dd_table_open_on_name(
thd, nullptr, name, true,
DICT_ERR_IGNORE_INDEX_ROOT | DICT_ERR_IGNORE_CORRUPT, &error);
if (table == nullptr && error == HA_ERR_GENERIC) {
err = DB_ERROR;
goto funct_exit;
}
} else {
table->acquire();
}
/* Need to exclusive lock all AUX tables for drop table */
if (table && table->fts) {
dict_sys_mutex_exit();
err = fts_lock_all_aux_tables(thd, table);
dict_sys_mutex_enter();
if (err != DB_SUCCESS) {
dd_table_close(table, nullptr, nullptr, true);
goto funct_exit;
}
}
} else {
table->acquire();
ut_ad(table->is_intrinsic());
}
if (!table) {
err = DB_TABLE_NOT_FOUND;
goto funct_exit;
}
file_per_table = dict_table_is_file_per_table(table);
/* Acquire MDL on SDI table of tablespace. This is to prevent
concurrent DROP while purge is happening on SDI table */
if (file_per_table) {
MDL_ticket *sdi_mdl = nullptr;
dict_sys_mutex_exit();
err = dd_sdi_acquire_exclusive_mdl(thd, table->space, &sdi_mdl);
dict_sys_mutex_enter();
if (err != DB_SUCCESS) {
dd_table_close(table, nullptr, nullptr, true);
goto funct_exit;
}
}
/* This function is called recursively via fts_drop_tables(). */
if (!trx_is_started(trx)) {
if (!table->is_temporary()) {
trx_start_if_not_started(trx, true, UT_LOCATION_HERE);
} else {
trx_set_dict_operation(trx, TRX_DICT_OP_TABLE);
}
}
/* Turn on this drop bit before we could release the dictionary
latch */
table->to_be_dropped = true;
if (nonatomic) {
/* This trx did not acquire any locks on dictionary
table records yet. Thus it is safe to release and
reacquire the data dictionary latches. */
if (table->fts) {
ut_ad(!table->fts->add_wq);
row_mysql_unlock_data_dictionary(trx);
fts_optimize_remove_table(table);
row_mysql_lock_data_dictionary(trx, UT_LOCATION_HERE);
}
/* Do not bother to deal with persistent stats for temp
tables since we know temp tables do not use persistent
stats. */
if (!table->is_temporary()) {
dict_stats_wait_bg_to_stop_using_table(table, trx);
}
}
/* make sure background stats thread is not running on the table */
ut_ad(!(table->stats_bg_flag & BG_STAT_IN_PROGRESS));
if (!table->is_temporary() && !table->is_fts_aux()) {
if (srv_thread_is_active(srv_threads.m_dict_stats)) {
dict_stats_recalc_pool_del(table);
}
/* Remove stats for this table and all of its indexes from the
persistent storage if it exists and if there are stats for this
table in there. This function creates its own trx and commits
it. */
char errstr[1024];
err = dict_stats_drop_table(name, errstr, sizeof(errstr));
if (err != DB_SUCCESS) {
ib::warn(ER_IB_MSG_992) << errstr;
}
}
if (!table->is_intrinsic()) {
dict_table_prevent_eviction(table);
}
dd_table_close(table, thd, nullptr, true);
/* Check if the table is referenced by foreign key constraints from
some other table now happens on SQL-layer. */
DBUG_EXECUTE_IF("row_drop_table_add_to_background",
row_add_table_to_background_drop_list(table->name.m_name);
err = DB_SUCCESS; goto funct_exit;);
/* TODO: could we replace the counter n_foreign_key_checks_running
with lock checks on the table? Acquire here an exclusive lock on the
table, and rewrite lock0lock.cc and the lock wait in srv0srv.cc so that
they can cope with the table having been dropped here? Foreign key
checks take an IS or IX lock on the table. */
if (table->n_foreign_key_checks_running > 0) {
const char *save_tablename = table->name.m_name;
auto added = row_add_table_to_background_drop_list(save_tablename);
if (added) {
ib::info(ER_IB_MSG_993) << "You are trying to drop table " << table->name
<< " though there is a foreign key check"
" running on it. Adding the table to the"
" background drop queue.";
/* We return DB_SUCCESS to MySQL though the drop will
happen lazily later */
err = DB_SUCCESS;
} else {
/* The table is already in the background drop list */
err = DB_ERROR;
}
goto funct_exit;
}
/* Remove all locks that are on the table or its records, if there
are no references to the table but it has record locks, we release
the record locks unconditionally. One use case is:
CREATE TABLE t2 (PRIMARY KEY (a)) SELECT * FROM t1;
If after the user transaction has done the SELECT and there is a
problem in completing the CREATE TABLE operation, MySQL will drop
the table. InnoDB will create a new background transaction to do the
actual drop, the trx instance that is passed to this function. To
preserve existing behaviour we remove the locks but ideally we
shouldn't have to. There should never be record locks on a table
that is going to be dropped. */
if (table->get_ref_count() == 0) {
/* We don't take lock on intrinsic table so nothing to remove.*/
if (!table->is_intrinsic()) {
lock_remove_all_on_table(table, true);
}
ut_a(table->n_rec_locks.load() == 0);
} else if (table->get_ref_count() > 0 || table->n_rec_locks.load() > 0) {
ut_d(ut_error);
#ifndef UNIV_DEBUG
ut_ad(!table->is_intrinsic());
const auto added =
row_add_table_to_background_drop_list(table->name.m_name);
if (added) {
ib::info(ER_IB_MSG_994) << "MySQL is trying to drop table " << table->name
<< " though there are still open handles to"
" it. Adding the table to the background drop"
" queue.";
/* We return DB_SUCCESS to MySQL though the drop will
happen lazily later */
err = DB_SUCCESS;
} else {
/* The table is already in the background drop list */
err = DB_ERROR;
}
goto funct_exit;
#endif
}
/* The "to_be_dropped" marks table that is to be dropped, but
has not been dropped, instead, was put in the background drop
list due to being used by concurrent DML operations. Clear it
here since there are no longer any concurrent activities on it,
and it is free to be dropped */
table->to_be_dropped = false;
/* If we get this far then the table to be dropped must not have
any table or record locks on it. */
ut_a(table->is_intrinsic() || !lock_table_has_locks(table));
switch (trx_get_dict_operation(trx)) {
case TRX_DICT_OP_NONE:
trx_set_dict_operation(trx, TRX_DICT_OP_TABLE);
case TRX_DICT_OP_TABLE:
break;
case TRX_DICT_OP_INDEX:
/* If the transaction was previously flagged as
TRX_DICT_OP_INDEX, we should be dropping auxiliary
tables for full-text indexes or temp tables. */
ut_ad(strstr(table->name.m_name, "/fts_") != nullptr ||
strstr(table->name.m_name, TEMP_FILE_PREFIX_INNODB) != nullptr);
}
if (!table->is_temporary() && !file_per_table) {
dict_sys_mutex_exit();
for (dict_index_t *index = table->first_index();
err == DB_SUCCESS && index != nullptr; index = index->next()) {
err = log_ddl->write_free_tree_log(trx, index, true);
}
dict_sys_mutex_enter();
if (err != DB_SUCCESS) {
goto funct_exit;
}
}
/* Mark all indexes unavailable in the data dictionary cache
before starting to drop the table. */
for (dict_index_t *index = table->first_index(); index != nullptr;
index = index->next()) {
page_no_t page;
rw_lock_x_lock(dict_index_get_lock(index), UT_LOCATION_HERE);
page = index->page;
/* Mark the index unusable. */
index->page = FIL_NULL;
rw_lock_x_unlock(dict_index_get_lock(index));
if (table->is_temporary()) {
dict_drop_temporary_table_index(index, page);
}
}
err = DB_SUCCESS;
space_id_t space_id;
bool is_temp;
bool is_discarded;
bool shared_tablespace;
table_id_t table_id;
char *table_name;
space_id = table->space;
table_id = table->id;
is_discarded = dict_table_is_discarded(table);
is_temp = table->is_temporary();
shared_tablespace = DICT_TF_HAS_SHARED_SPACE(table->flags);
/* We do not allow temporary tables with a remote path. */
ut_a(!(is_temp && DICT_TF_HAS_DATA_DIR(table->flags)));
/* Make sure the data_dir_path is set if needed. */
dd_get_and_save_data_dir_path(table, table_def, true);
if (dict_table_has_fts_index(table) ||
DICT_TF2_FLAG_IS_SET(table, DICT_TF2_FTS_HAS_DOC_ID)) {
ut_ad(!is_temp);
err = row_drop_ancillary_fts_tables(table, &aux_vec, trx);
if (err != DB_SUCCESS) {
goto funct_exit;
}
}
/* Table space file name has been renamed in TRUNCATE. */
table_name = table->trunc_name.m_name;
if (table_name == nullptr) {
table_name = table->name.m_name;
} else {
table->trunc_name.m_name = nullptr;
}
/* Determine the tablespace filename before we drop
dict_table_t. Free this memory before returning. */
if (DICT_TF_HAS_DATA_DIR(table->flags)) {
auto dir = dict_table_get_datadir(table);
filepath = Fil_path::make(dir, table_name, IBD, true);
} else if (!shared_tablespace) {
filepath = Fil_path::make_ibd_from_table_name(table_name);
}
/* Free the dict_table_t object. */
err = row_drop_table_from_cache(table, trx);
if (err != DB_SUCCESS) {
ut_d(ut_error);
ut_o(goto funct_exit);
}
if (!is_temp) {
log_ddl->write_drop_log(trx, table_id);
}
/* Do not attempt to drop known-to-be-missing tablespaces,
nor system or shared general tablespaces. */
if (is_discarded || is_temp || shared_tablespace ||
fsp_is_system_or_temp_tablespace(space_id)) {
goto funct_exit;
}
ut_ad(file_per_table);
err = log_ddl->write_delete_space_log(trx, nullptr, space_id, filepath, true,
true);
funct_exit:
ut::free(filepath);
if (locked_dictionary) {
row_mysql_unlock_data_dictionary(trx);
}
trx->op_info = "";
trx->dict_operation = TRX_DICT_OP_NONE;
if (aux_vec.aux_name.size() > 0) {
if (trx->dict_operation_lock_mode == RW_X_LATCH) {
dict_sys_mutex_exit();
}
if (!fts_drop_dd_tables(&aux_vec, file_per_table)) {
err = DB_ERROR;
}
if (trx->dict_operation_lock_mode == RW_X_LATCH) {
dict_sys_mutex_enter();
}
fts_free_aux_names(&aux_vec);
}
return err;
}
분석할 엄두가 나질 않죠? ㅎ_ㅎ 차근차근 보면 됩니다...................되겠죠?
그나마 주석이 잘 달려있어서 감사하네요. 모르겠으니 주석만 모아서 봅시다
/** Drop a table for MySQL. If the data dictionary was not already locked
by the transaction, the transaction will be committed. Otherwise, the
data dictionary will remain locked.
@param[in] name Table name
@param[in] trx Transaction handle
@param[in] nonatomic Whether it is permitted to release
and reacquire dict_operation_lock
@param[in,out] handler Table handler or NULL
@return error code or DB_SUCCESS */
/* Serialize data dictionary operations with dictionary mutex:
no deadlocks can occur then in these operations */
/* Prevent foreign key checks etc. while we are
dropping the table */
/* If it's called from server, then it should exist in cache */
/* Need to exclusive lock all AUX tables for drop table */
/* Acquire MDL on SDI table of tablespace. This is to prevent
concurrent DROP while purge is happening on SDI table */
/* This function is called recursively via fts_drop_tables(). */
/* Turn on this drop bit before we could release the dictionary
latch */
/* This trx did not acquire any locks on dictionary
table records yet. Thus it is safe to release and
reacquire the data dictionary latches. */
/* Do not bother to deal with persistent stats for temp
tables since we know temp tables do not use persistent
stats. */
/* make sure background stats thread is not running on the table */
/* Remove stats for this table and all of its indexes from the
persistent storage if it exists and if there are stats for this
table in there. This function creates its own trx and commits
it. */
/* Check if the table is referenced by foreign key constraints from
some other table now happens on SQL-layer. */
/* TODO: could we replace the counter n_foreign_key_checks_running
with lock checks on the table? Acquire here an exclusive lock on the
table, and rewrite lock0lock.cc and the lock wait in srv0srv.cc so that
they can cope with the table having been dropped here? Foreign key
checks take an IS or IX lock on the table. */
/* We return DB_SUCCESS to MySQL though the drop will
happen lazily later */
/* The table is already in the background drop list */
/* Remove all locks that are on the table or its records, if there
are no references to the table but it has record locks, we release
the record locks unconditionally. One use case is:
CREATE TABLE t2 (PRIMARY KEY (a)) SELECT * FROM t1;
If after the user transaction has done the SELECT and there is a
problem in completing the CREATE TABLE operation, MySQL will drop
the table. InnoDB will create a new background transaction to do the
actual drop, the trx instance that is passed to this function. To
preserve existing behaviour we remove the locks but ideally we
shouldn't have to. There should never be record locks on a table
that is going to be dropped. */
/* We don't take lock on intrinsic table so nothing to remove.*/
/* The "to_be_dropped" marks table that is to be dropped, but
has not been dropped, instead, was put in the background drop
list due to being used by concurrent DML operations. Clear it
here since there are no longer any concurrent activities on it,
and it is free to be dropped */
/* If we get this far then the table to be dropped must not have
any table or record locks on it. */
/* If the transaction was previously flagged as
TRX_DICT_OP_INDEX, we should be dropping auxiliary
tables for full-text indexes or temp tables. */
/* Mark all indexes unavailable in the data dictionary cache
before starting to drop the table. */
/* Mark the index unusable. */
/* We do not allow temporary tables with a remote path. */
/* Make sure the data_dir_path is set if needed. */
/* Table space file name has been renamed in TRUNCATE. */
/* Determine the tablespace filename before we drop
dict_table_t. Free this memory before returning. */
/* Free the dict_table_t object. */
/* Do not attempt to drop known-to-be-missing tablespaces,
nor system or shared general tablespaces. */
주석만 봐도 대략 흐름이 눈에 들어옵니다. 역시 초대형 오픈소스 프로젝트 같습니다.
주석을 토대로 번역해가며 코드의 흐름을 'chatGPT와 협업해서' 정리해보면,
- 초기화단계: 변수들을 초기화하고, 트랜잭션 정보 등을 설정합니다. 트랜잭션에 대한 정보와 테이블 이름 등이 초기화됩니다.
- 테이블 핸들링 및 데이터 딕셔너리 엑세스: 함수가 수행될 때, 트랜잭션이 데이터 딕셔너리를 잠근 상태이거나 아닌 상태에 따라 분기됩니다. 잠긴 상태에서는 row_mysql_lock_data_dictionary 함수를 호출하여 데이터 딕셔너리를 잠그고, 데이터 딕셔너리의 레코드를 접근합니다. 그렇지 않은 경우, 딕셔너리 접근 관련 로직은 생략됩니다.
- 인덱스 접근 및 락 해제: 테이블에 연결된 인덱스들에 대한 락을 획득하고, 해당 인덱스들을 사용 불가능한 상태로 표시합니다. 이렇게 함으로써 뒷단에서 실제 삭제 작업을 수행하는 동안 인덱스가 수정되지 않도록 보장합니다.
- 백그라운드 작업 등록: 백그라운드에서 실행되는 스레드를 활용하여 인덱스와 관련된 백그라운드 작업을 등록합니다. 이 단계에서는 전체 텍스트 검색(Full-Text Search) 인덱스와 관련된 보조 테이블(auxiliary tables)의 삭제 작업을 처리하는 것으로 보입니다. fts_drop_dd_tables 함수가 호출됩니다.
- 테이블 삭제 준비 및 로깅: 삭제할 테이블의 정보를 활용하여 로깅 작업을 수행합니다. 테이블이 임시 테이블이 아니면서 파일 당 테이블(File-Per-Table) 옵션이 적용되어 있는 경우, 해당 테이블에 연결된 인덱스에 대한 로깅을 수행합니다.
- 테이블 캐시에서 삭제: 데이터 딕셔너리 캐시에서 테이블을 삭제합니다. row_drop_table_from_cache 함수가 호출됩니다. 이 단계에서 테이블에 대한 참조 관리와 관련된 작업도 수행됩니다.
- 테이블 스페이스 삭제: 마지막으로, 테이블 스페이스를 삭제하는 작업이 수행됩니다. 이 단계에서 인덱스 관련 파일이나 데이터 파일들을 삭제합니다.
테이블에 걸려있는 인덱스도 삭제되어야겠죠 ?!
대략 살펴보면
- 인덱스 접근 및 락 해제: dict_index_t 구조체에 있는 인덱스들에 락 할당 및 page 필드를 FIL_NULL로 할당해서 사용 불가능하게 만듭니다.
- 백그라운드 작업 등록: fts_drop_dd_tables 함수를 통해 전체 텍스트 검색 인덱스와 관련된 보조 테이블들을 백그라운드 삭제 작업 목록에 추가합니다.
일반적으로 개발팀에게 TABLE을 DROP해달라고 요청이 들어오면, 마음 편히 작업했던 기억이 있습니다.
아무리 큰 테이블이라도 수 초 내에 수행이 완료되기 때문이죠!
그 이유를 오늘 알았습니다.
DROP TABLE 문을 날린다고 해서 서버가 즉시 파일을 삭제하는 게 아니라,
최소한의 락만 잠깐 할당하고 실제 삭제 작업은 사용자 모르게 백그라운드에서 수행되기 때문이었네요.
역시 MySQL.... 멋지다....
MySQL 소스코드 분석 입문 성공적!
다음 목표는 소스코드에서 sql_mode 와 online ddl 간의 관련성 찾기입니다.
호호 재밌네요 개발자같다
https://dev.mysql.com/doc/dev/mysql-server/latest/
'데이터베이스' 카테고리의 다른 글
AWS DB 운영, ChatOps로 초극한 자동화 플랫폼 엔지니어링 (1) | 2024.06.01 |
---|---|
[MySQL 운영 및 !자동화!] Undo 로그 길이 모니터링 및 장기 실행 트랜잭션의 쿼리 조회하기 (0) | 2023.08.31 |
MySQL 8.1 간단하게 살펴보기 (1) | 2023.08.02 |
MySQL DBA DDL 실무 장애 경험 - (NULL -> NOT NULL) (0) | 2023.08.02 |
MySQL에서 DDL과 Metadata Lock, 장애와 자동화 (2) | 2023.06.21 |