테스트배경

aurora3 (mysql8) 부터는 드디어 컬럼추가할 때, 컬럼 드랍할 때 수행 즉시 완료된다 ! (다른 DDL, index 추가 등은 아직 아닙니다..!)

기존에는 aurora (mysql 5.7) 에서는 inplace 방식으로 진행되기 때문에 테이블 사이즈에 따라 한참 걸려서 큰 테이블에 대한 컬럼 추가 작업은 어려움이 있었는데 이젠 instant로 바로 반영된다.

기존 inplace 방식을 간단히 살펴보고, instant 는 어떻게 수행되도록 변경되었는지를 확인해본다

algorithm=inplace

 컬럼 추가 기준으로 봤을 때 (인덱스 추가는 조금 다름)

  1. inplace는 DDL이 반영된 임시테이블을 하나 만들고  
  2. 데이터를 원본테이블에서 읽어서 부어주고,
  3. DDL반영 중 들어온 변경사항들을 마저 반영해준 뒤,
  4. 메타데이터 락을 획득하여 DML의 추가유입을 막고
  5. 임시테이블과 원본테이블을 바꿔치기 하는 식으로 진행 됩니다. 

DBA가 트래픽이 많은시간, 배치잡이 도는 시간을 피해서 DDL을 수행하려는 이유가 바로 2,3,4번에 있습니다.
inplace 의 대략적인 프로세스를 살펴보자면 아래와 같습니다.
(2,3 번 과정은 너무 복잡해서 다 이해하지 못하여 생략,,,)

--------------------------- 1번
- mysql_alter_table
    - open_and_process_table
        - MDL_context::acquire_lock
        - open_table_get_mdl_lock
  - create_table_impl
    - rea_create_base_table
      - dd::create_table
  - ha_innobase::check_if_supported_inplace_alter:  COPY 방식인지, INPLACE 방식인지 체크
  - mysql_inplace_alter_table
    - MDL_context::upgrade_shared_lock:   다른 DDL이 또 들어오지 않도록 metadata lock을 건다, DML은 가능한 metadata lock 

    - THD_STAGE_INFO(thd, stage_alter_inplace_prepare): prepare 단계로 진입
    - handler::ha_prepare_inplace_alter_table:
      - ha_innobase::prepare_inplace_alter_table
        - ha_innobase::prepare_inplace_alter_table_impl
            - row_create_table_for_mysql: 임시 테이블 생성

--------------------------- 2번
    - THD_STAGE_INFO(thd, stage_alter_inplace)
    - handler::ha_inplace_alter_table
      - ha_innobase::inplace_alter_table
        - ha_innobase::inplace_alter_table_impl
        ------------ 아래 부터는 Primary key 를 읽으면서 기존 데이터 복사 진행
          - row_merge_build_indexes:   
            - stage->begin_phase_read_pk: 
            - begin_phase_read_pk:
            - row_merge_read_clustered_index: 
              - merge_buf[i] = row_merge_buf_create(index[i]): 각 인덱스마다 buffer 생성, innodb_sort_buffer_size로 읽기
              - row_merge_insert_index_tuples(): 
                - stage->begin_phase_insert: srv_stage_alter_table_insert 단계로 진입
            - end_phase_read_pk: 

--------------------------- 3번
            - m_stage->begin_phase_flush
            - row_merge_write_redo
            - row_log_apply
              - stage->begin_phase_log_index
              - row_log_apply_ops: row log 적용
            - if (error == DB_SUCCESS && ctx->online && ctx->need_rebuild())
              - row_log_table_apply
                - stage->begin_phase_log_table

--------------------------- 4번,5번
    - wait_while_table_is_used
      - MDL_context::upgrade_shared_lock
        - MDL_context::acquire_lock : 커밋 전 MDL_EXCLUSIVE 잠금으로 업그레이드 대기
    - THD_STAGE_INFO(thd, stage_alter_inplace_commit): DDL ccomit 
    - handler::ha_commit_inplace_alter_table
      - ha_innobase::commit_inplace_alter_table
        - commit_inplace_alter_table_impl
 - log_ddl->write_drop_log: commit 완료 후 log drop (3번에서 쓰던 로그 같음)
    - dd_commit_inplace_alter_table: data dictionary 업데이트

그동안 DBA들이 DDL 처리할 때 개발자 분들과 얘기하던 내용들이 조금은 이해되시길 바라며

DBA들의 이야기를 위 내용과 매칭해보면,,,, 

  • 이 테이블은 사이즈가 커서 좀 오래 걸릴 것 같아요 ->  2번에서 오래걸려서 그렇습니다
  • 작업하는 동안 쓰기가 많으면 DDL이 좀 더 오래 걸릴 수 있습니다 ->  3번에서 처리할 양이 많아져서 그렇습니다
  • 테이블 작업 마지막에 metadata lock을 걸 때 잠시 테이블 락이 있을 수 있습니다 ->  4번,5번에서 메타데이터락을 잡습니다

metadata lock 관련해서 부연설명을 하자면 ,,,,
위에서 metadata lock을 처음에 한번, 마지막에 한번 잡는데 종류와 동작 방식이 다릅니다.

  • 첫번째 metadata lock은 MDL_SHARED_UPGRADABLE 입니다. 이것은 작업 테이블에 다른 DDL이 들어오지 않도록만 할 뿐 DML,select 는 가능합니다 
table->mdl_ticket->downgrade_lock(MDL_SHARED_UPGRADABLE);
(gdb) p *mdl_ticket
$3 = {<MDL_wait_for_subgraph> = {_vptr.MDL_wait_for_subgraph = 0x82fe7e0 <vtable for MDL_ticket+16>, static DEADLOCK_WEIGHT_CO = 0, static DEADLOCK_WEIGHT_DML = 25,
    static DEADLOCK_WEIGHT_ULL = 50, static DEADLOCK_WEIGHT_DDL = 100}, next_in_context = 0xfffe54118710, prev_in_context = 0xfffe54118898, next_in_lock = 0x0,
  prev_in_lock = 0xfffe540fe998, m_type = MDL_SHARED_UPGRADABLE, m_duration = MDL_TRANSACTION, m_ctx = 0xfffe54001130, m_lock = 0xfffe540fe780, m_is_fast_path = false,
  m_hton_notified = false, m_psi = 0xffff87a41100}
  • 마지막에 잡는 metadata lock은 EXCLUSIVE입니다. 모든 쿼리를 block 하는데 그동안 재빨리 바꿔치기를 하기 위함으로, DDL 마지막에 쿼리가 실패할 수 있다고 말하게 되는 범인입니다
#1  0x0000000003a080c0 in MDL_context::upgrade_shared_lock (this=0xfffe54001130, mdl_ticket=0xfffe54106170, new_type=MDL_EXCLUSIVE, lock_wait_timeout=60)
    at /mysql_source/mysql-8.0.32/sql/mdl.cc:3756
        mdl_new_lock_request = {type = MDL_EXCLUSIVE, duration = MDL_TRANSACTION, next_in_list = 0x0, prev_in_list = 0x0, ticket = 0x0, key = {m_length = 19, m_db_name_length = 4,
            m_object_name_length = 12, m_ptr = "\004test\000instant_test", '\000' <repeats 368 times>, static m_namespace_to_wait_state_name = {{m_key = 102,
MDL_context::upgrade_shared_lock (this=0xfffe54001130, mdl_ticket=0xfffe54106170, new_type=MDL_EXCLUSIVE, lock_wait_timeout=60) at /mysql_source/mysql-8.0.32/sql/mdl.cc:3758
3758	  is_new_ticket = !has_lock(mdl_svp, mdl_new_lock_request.ticket);

여기까지 기존에 활용되던 inplace 방식의 DDL을 살펴보았습니다.

사실 inplace의 의미를 곰곰이 생각해보면 Inplace 라면 테이블 교체없이 이뤄져야 하는 거 아니냐 싶은데요

위 과정을 좀 더 자세하게 살펴보면, 아래에서 볼 수 있듯이 기존 테이블인 0xfffe58ab2ce0 가 old_table이 되고 altered_table인 0xfffe58ac87c0 가 new_table이 되는 것을 볼 수 있습니다.

altered table 인 0xfffe58ac87c0 은 ‘test.#sql-1285_7’ 라는 이름으로 생성된 것 또한 확인할 수 있습니다.  

17404	    if (use_inplace) {
17405	      if (mysql_inplace_alter_table(thd, *schema, *new_schema, old_table_def,
17406	                                    table_def, table_list, table, altered_table,
17407	                                    &ha_alter_info, inplace_supported,
17408	                                    &alter_ctx, columns, fk_key_info,
17409	                                    fk_key_count, &fk_invalidator)) {
(gdb) p table
$2 = (TABLE *) 0xfffe58ab2ce0
(gdb) p altered_table
$3 = (TABLE *) 0xfffe58ac87c0      ======> 기존 테이블과 altered table이 다름

altered_table = 0xfffe58ac87c0
use_inplace = true
inplace_supported = HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE

#0  row_create_table_for_mysql (table=@0xfffe58ac7f08: 0xfffe5817a128, compression=0x0, create_info=0xffff8714ec68, trx=0xffff8d7b4ff8, heap=0x0)
    at /mysql_source/mysql-8.0.32/storage/innobase/row/row0mysql.cc:2768
#1  0x0000000004af20c4 in prepare_inplace_alter_table_dict<dd::Table> (ha_alter_info=0xffff8714c280, altered_table=0xfffe58ac87c0,
    old_table=0xfffe58ab2ce0, old_dd_tab=0xfffe58ab42e0, new_dd_tab=0xfffe5817ac60, table_name=0xfffe58154a75 "instant_test", flags=33, flags2=16,
    fts_doc_id_col=18446744073709551615, add_fts_doc_id=false, add_fts_doc_id_idx=false)
    at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:4802
#2  0x0000000004b03d88 in ha_innobase::prepare_inplace_alter_table_impl<dd::Table> (this=0xfffe58abf560, altered_table=0xfffe58ac87c0,
    ha_alter_info=0xffff8714c280, old_dd_tab=0xfffe58ab42e0, new_dd_tab=0xfffe5817ac60)
    at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:6047
#3  0x0000000004ade7e4 in ha_innobase::prepare_inplace_alter_table (this=0xfffe58abf560, altered_table=0xfffe58ac87c0, ha_alter_info=0xffff8714c280,
    old_dd_tab=0xfffe58ab42e0, new_dd_tab=0xfffe5817ac60) at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:1446
#4  0x00000000037bc924 in handler::ha_prepare_inplace_alter_table (this=0xfffe58abf560, altered_table=0xfffe58ac87c0, ha_alter_info=0xffff8714c280,
    old_table_def=0xfffe58ab42e0, new_table_def=0xfffe5817ac60) at /mysql_source/mysql-8.0.32/sql/handler.cc:4846
#5  0x00000000034e14bc in mysql_inplace_alter_table (thd=0xfffe58000da0, schema=..., new_schema=..., table_def=0xfffe58ab42e0,
    altered_table_def=0xfffe5817ac60, table_list=0xfffe58ac1a88, table=0xfffe58ab2ce0, altered_table=0xfffe58ac87c0, ha_alter_info=0xffff8714c280,
    inplace_supported=HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE, alter_ctx=0xffff8714d170, columns=std::set with 0 elements,
    fk_key_info=0xfffe58ac7350, fk_key_count=0, fk_invalidator=0xffff8714d0a8) at /mysql_source/mysql-8.0.32/sql/sql_table.cc:13408

T@7: dd_table_share.cc: | | | | | | | <open_table_def
T@7:       table.cc:  2880: | | | | | | | >open_table_from_share
T@7:       table.cc:  2881: | | | | | | | | enter: name: 'test.#sql-1285_7'  form: 0xfffe58ac87c0     ======> 위에서 봤던 altered_table , 여기로 데이터를 복사한뒤 마지막에 바꿔치기를 한다

그리고 좀 더 확실하게 보기 위해 이 테이블에 대해 또 inplace ddl을 하면,,,,

위에선 altered_table 이자 new table이었던 0xfffe58ac87c0 테이블이 이제는 old_table이 된것을 볼 수 있습니다  

(gdb) p table
$5 = (TABLE *) 0xfffe58ac87c0
(gdb) p altered_table
$6 = (TABLE *) 0xfffe58a9ecd0
(gdb) bt
#0  mysql_inplace_alter_table (thd=0xfffe58000da0, schema=..., new_schema=..., table_def=0xfffe58ae3780, altered_table_def=0xfffe58ac9430,
    table_list=0xfffe58c5d3e8, table=0xfffe58ac87c0, altered_table=0xfffe58a9ecd0, ha_alter_info=0xffff8714c280,
    inplace_supported=HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE, alter_ctx=0xffff8714d170, columns=std::set with 0 elements,
    fk_key_info=0xfffe58c601d8, fk_key_count=0, fk_invalidator=0xffff8714d0a8) at /mysql_source/mysql-8.0.32/sql/sql_table.cc:13208


#0  row_create_table_for_mysql (table=@0xfffe58c3c018: 0xfffe5817aa18, compression=0x0, create_info=0xffff8714ec68, trx=0xffff8d7b4ff8, heap=0x0)
    at /mysql_source/mysql-8.0.32/storage/innobase/row/row0mysql.cc:2734
#1  0x0000000004af20c4 in prepare_inplace_alter_table_dict<dd::Table> (ha_alter_info=0xffff8714c280, altered_table=0xfffe58a9ecd0,
    old_table=0xfffe58ac87c0, old_dd_tab=0xfffe58ae3780, new_dd_tab=0xfffe58ac9430, table_name=0xfffe58154a75 "instant_test", flags=33, flags2=16,
    fts_doc_id_col=18446744073709551615, add_fts_doc_id=false, add_fts_doc_id_idx=false)
    at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:4802
#2  0x0000000004b03d88 in ha_innobase::prepare_inplace_alter_table_impl<dd::Table> (this=0xfffe58c60dd0, altered_table=0xfffe58a9ecd0,
    ha_alter_info=0xffff8714c280, old_dd_tab=0xfffe58ae3780, new_dd_tab=0xfffe58ac9430)
    at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:6047
#3  0x0000000004ade7e4 in ha_innobase::prepare_inplace_alter_table (this=0xfffe58c60dd0, altered_table=0xfffe58a9ecd0, ha_alter_info=0xffff8714c280,
    old_dd_tab=0xfffe58ae3780, new_dd_tab=0xfffe58ac9430) at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:1446
#4  0x00000000037bc924 in handler::ha_prepare_inplace_alter_table (this=0xfffe58c60dd0, altered_table=0xfffe58a9ecd0, ha_alter_info=0xffff8714c280,
    old_table_def=0xfffe58ae3780, new_table_def=0xfffe58ac9430) at /mysql_source/mysql-8.0.32/sql/handler.cc:4846
#5  0x00000000034e14bc in mysql_inplace_alter_table (thd=0xfffe58000da0, schema=..., new_schema=..., table_def=0xfffe58ae3780,
    altered_table_def=0xfffe58ac9430, table_list=0xfffe58c5d3e8, table=0xfffe58ac87c0, altered_table=0xfffe58a9ecd0, ha_alter_info=0xffff8714c280,
    inplace_supported=HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE, alter_ctx=0xffff8714d170, columns=std::set with 0 elements,
    fk_key_info=0xfffe58c601d8, fk_key_count=0, fk_invalidator=0xffff8714d0a8) at /mysql_source/mysql-8.0.32/sql/sql_table.cc:13408
#6  0x00000000034ec82c in mysql_alter_table (thd=0xfffe58000da0, new_db=0xfffe58c5da38 "test", new_name=0x0, create_info=0xffff8714ec68,
    table_list=0xfffe58c5d3e8, alter_info=0xffff8714eb00) at /mysql_source/mysql-8.0.32/sql/sql_table.cc:17405

T@7: dd_table_share.cc: | | | | | | | <open_table_def
T@7:       table.cc:  2880: | | | | | | | >open_table_from_share
T@7:       table.cc:  2881: | | | | | | | | enter: name: 'test.#sql-1285_7'  form: 0xfffe58a9ecd0

algorithm=instant

instant 방식은 훨씬 간단합니다. (방식은 간단한데, 디버깅은 더 어려운 것 같습니다)

inplace 처럼 복잡한 과정 없이 INSTANT로 실행될 수 있는 DDL인지 확인 후 메타데이터 변경만 하기 때문에 기존데이터 복사 등의 과정이 없습니다.

(간단하지만 metadata 쪽 처리 관련해서 뭔가 큰것이 있는 것 같은데 내용이 너무 많아 생략했습니다)

--------------------------- 1번
mysql_alter_table
    open_and_process_table
        open_table
           MDL_context::acquire_lock 
           open_table_get_mdl_lock
    check_if_supported_inplace_alter : 
    mysql_inplace_alter_table
        THD_STAGE_INFO(thd, stage_alter_inplace_prepare)
        handler::ha_prepare_inplace_alter_table 
          ha_innobase::prepare_inplace_alter_table
            ha_innobase::prepare_inplace_alter_table_impl 
              if (...is_instant(ha_alter_info)) : instant algorithm이라면 exit 
        THD_STAGE_INFO(thd, stage_alter_inplace)
        handler::ha_inplace_alter_table 
          ha_innobase::inplace_alter_table
            ha_innobase::inplace_alter_table_impl
              if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info)) :   instant algorithm이라면 exit  

--------------------------- 2번
        wait_while_table_is_used 
          MDL_context::upgrade_shared_lock
            MDL_context::acquire_lock 
        THD_STAGE_INFO(thd, stage_alter_inplace_commit)
        handler::ha_commit_inplace_alter_table 
          ha_innobase::commit_inplace_alter_table
            commit_inplace_alter_table_impl
              if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||is_instant(ha_alter_info))   :  instant algorithm이라면 exit  
            dd_commit_inplace_instant // instance ddl 
              switch (type) // 
                dd_copy_private(*new_dd_tab, *old_dd_tab); 
                dd_commit_instant_table 
                  dd_copy_table_columns
                  dd_add_instant_columns 
                  dd_update_v_cols

inplace 방식과 비교하면 훨씬 간단해진 것을 확인할 수 있습니다.

특히 기존 데이터 복제, DDL 중 들어온 로그에 대해 sync하는 부분들이 모두 없어졌습니다.

아래처럼 instant 로 풀릴 수 있는 DDL인지를 확인한 뒤 

1028	  Instant_Type instant_type = innobase_support_instant(
1029	      ha_alter_info, m_prebuilt->table, this->table, altered_table);
1030
1031	  ha_alter_info->handler_trivial_ctx =
1032	      instant_type_to_int(Instant_Type::INSTANT_IMPOSSIBLE);
1033
1034	  if (!dict_table_is_partition(m_prebuilt->table)) {
1035	    switch (instant_type) {
1036	      case Instant_Type::INSTANT_IMPOSSIBLE:


(gdb) p alter_info->requested_algorithm
$2 = Alter_info::ALTER_TABLE_ALGORITHM_INSTANT

(gdb) p  instant_type
$30 = INSTANT_ADD_DROP_COLUMN

중간 중간 아래와 같은 로직을 거치면서
inplace 에서는 해야할 처리들(기존 데이터 복제, DDL 중 들어온 데이터 싱크) 을 안하고 바로 ok_exit 해버리면서 과정이 훨씬 간소화되었습니다  

template < typename Table>
 bool ha_innobase::inplace_alter_table_impl(TABLE *altered_table,
                                           Alter_inplace_info *ha_alter_info,
                                           const Table *old_dd_tab,
                                           표 *new_dd_tab) {
  // ... 
  if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) ||
      is_instant (ha_alter_info)) {
  ok_exit :
    DEBUG_SYNC(m_user_thd, "innodb_after_inplace_alter_table" );
    DBUG_RETURN ( false );
  }

다만 한가지 꼭 유의할 점은,  위 1번 과정 중  check_if_supported_inplace_alter를 자세하게 확인해보면

#1  0x0000000004add930 in ha_innobase::check_if_supported_inplace_alter (this=0xfffe3ca9f260, altered_table=0xfffe3cb124c0,
    ha_alter_info=0xffff682c8280) at /mysql_source/mysql-8.0.32/storage/innobase/handler/handler0alter.cc:1053
        _db_trace = {m_stack_frame = {
            func = 0x614fbbd "mysql_alter_table(THD*, const char*, const char*, HA_CREATE_INFO*, Table_ref*, Alter_info*)", func_len = 17,
            file = 0x614ae90 "/mysql_source/mysql-8.0.32/sql/sql_table.cc", level = 2147483655, prev = 0xffff682ca168}}
        __PRETTY_FUNCTION__ = "virtual enum_alter_inplace_result ha_innobase::check_if_supported_inplace_alter(TABLE*, Alter_inplace_info*)"
        old_encryption = 0xfffe3c117990 "N"
        new_encryption = 0xfffe3cb153b0 "N"
        instant_type = INSTANT_ADD_DROP_COLUMN
        add_drop_v_cols = false
        online = false
        cf_it = {<base_list_iterator> = {list = 0x48f0bac <native_rw_unlock(native_rw_lock_t*)+20>, el = 0xffff682cabe0, prev = 0xffff682c80e0,
            current = 0xffff682c80a0}, <No data fields>}
.
.
.

          /* INSTANT can't be done any more. Fall back to INPLACE. */
          break;
        } else if (!is_valid_row_version(
                       m_prebuilt->table->current_row_version + 1)) {
          ut_ad(is_valid_row_version(m_prebuilt->table->current_row_version));
          if (ha_alter_info->alter_info->requested_algorithm ==
              Alter_info::ALTER_TABLE_ALGORITHM_INSTANT) {
            my_error(ER_INNODB_MAX_ROW_VERSION, MYF(0),
                     m_prebuilt->table->name.m_name);
            return HA_ALTER_ERROR;
          }


is_valid_row_version 를 체크한다는 것인데
/* INSTANT can’t be done any more. Fall back to INPLACE. */ 구문이 얘기하는 것처럼
instant ddl 은 한 테이블에 대해 최대 64번까지 수행될 수 있고,
64번을 초과한 경우엔  위에서 본 것 처럼 INSTANT로 수행할 수 없습니다.

그리고 이 ROW_VERSION 64번을 체크하는 부분이 아래의 is_valid_row_version 함수입니다.

#0  is_valid_row_version (version=65 'A') at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0mem.h:429

	static inline bool is_valid_row_version(const uint8_t version) {
	  /* NOTE : 0 is also a valid row versions for rows which are inserted after
	  upgrading from earlier INSTANT implemenation */
	  if (version <= MAX_ROW_VERSION) {
	    return true;
	  }

	  return false;

(gdb) p version
$1 = 65

(gdb) p MAX_ROW_VERSION
$1 = 64 '@'

(gdb) s
433	  return false;





mysql> ALTER TABLE instant_test ADD COLUMN t64 VARCHAR(10), ALGORITHM=INSTANT;
Query OK, 0 rows affected (0.22 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT NAME, TOTAL_ROW_VERSIONS
    -> FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/instant_test';
+-------------------+--------------------+
| NAME              | TOTAL_ROW_VERSIONS |
+-------------------+--------------------+
| test/instant_test |                 64 |
+-------------------+--------------------+
1 row in set (0.03 sec)

mysql> ALTER TABLE instant_test ADD COLUMN t65 VARCHAR(10), ALGORITHM=INSTANT;
ERROR 4092 (HY000): Maximum row versions reached for table test/instant_test. No more columns can be added or dropped instantly. Please use COPY/INPLACE.

위와 같이 64번까지 instant ddl 을 한 뒤 ,65 번째 DDL에서는 실패하게 됩니다

단, 이때는 일부러 rebuild를 유발하는 algorithm=inplace  DDL을 수행하면 ROW_VERSION 이 초기화가 되어 다시 INSTANT DDL을 64번까지 수행할 수 있습니다

mysql> alter table instant_test add column t65 varchar(10),algorithm=inplace;
Query OK, 0 rows affected (0.22 sec)
Records: 0  Duplicates: 0  Warnings: 0


mysql> SELECT NAME, TOTAL_ROW_VERSIONS   FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE 'test/instant_test';
+-------------------+--------------------+
| NAME              | TOTAL_ROW_VERSIONS |
+-------------------+--------------------+
| test/instant_test |                  0 |
+-------------------+--------------------+
1 row in set (0.19 sec)

초기화 된 이후 또 instant ddl 을 수행해보면, 이때는 is_valid_row_version 을 통과한 것을 확인할  수 있습니다 

mysql> alter table instant_test add column t66 varchar(10),algorithm=instant;


Breakpoint 1, is_valid_row_version (version=1 '\001') at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0mem.h:429
429	  if (version <= MAX_ROW_VERSION) {

(gdb) p version
$1 = 1 '\001'

(gdb) s
430	    return true;

보너스 

handler0alter.cc 를 읽다보면 instant ddl 에 대해서 더 많이 알 수 있는데요
잘 모르지만 눈치껏 코드를 쭉 보면서 번외로  재밌는 내용들을 몇가지 가져와봤습니다.

  • 현재 algorithm=instant로 지원하는 DDL
  enum class INSTANT_OPERATION {
    COLUMN_RENAME_ONLY,           /*!< Only column RENAME */
    VIRTUAL_ADD_DROP_ONLY,        /*!< Only virtual column ADD AND DROP */
    VIRTUAL_ADD_DROP_WITH_RENAME, /*!< Virtual column ADD/DROP with RENAME */
    INSTANT_ADD,  /*< INSTANT ADD possibly with virtual column ADD and
                     column RENAME */
    INSTANT_DROP, /*|< INSTANT DROP possibly with virtual column ADD/DROP and
                    column RENAME */
    NONE
  };
  • 무서운 fall back 로직  (중요!)
    • instant 가 안되면 바로 inplace로 풀리도록 Fallback 로직이 있어서 주의해야합니다
    • 이러한 inplace fallback을 원하지 않고 DDL이 instant 알고리즘으로 풀릴 수 없다면 바로 실패하길 원한다면 항상 DDL구문 뒤에 ,algorithm = ? 을 붙여주는 것이 건강에 이롭습니다
    • ex) alter table tb_test add column tt int , algorithm=instant;
    • 참고로 inplace가 안되는 것은 COPY로 fallback을 하는 로직이 있습니다. 
        /* INSTANT can't be done any more. Fall back to INPLACE. */
        break;
      } else if (!Instant_ddl_impl<dd::Table>::is_instant_add_drop_possible(
                     ha_alter_info, table, altered_table, m_prebuilt->table)) {
        if (ha_alter_info->alter_info->requested_algorithm ==
            Alter_info::ALTER_TABLE_ALGORITHM_INSTANT) {
          /* Return error if either max possible row size already crosses max
          permissible row size or may cross it after add. */
          my_error(ER_INNODB_INSTANT_ADD_DROP_NOT_SUPPORTED_MAX_SIZE, MYF(0));
          return HA_ALTER_ERROR;
        }

  • 일별 파티셔닝하는 테이블은 instant ddl 도 조심해서 수행해야합니다  (중요!)
    • 아래와 같이 테이블의 파티션 여부를 체크하는데 아무리 instant여도 파티션이 많으면 이 부분에서 또 많은 시간을 소모합니다.
    • 경험상 파티션이 천개 이상 쯤 되면 아래부분에서 몇초~수십초는 소요하는데 그 과정에서 쿼리가 모두 metadata lock으로 block되어 실패하게 됩니다.
 dict_table_is_partition (table=0xfffe7ca72178) at /mysql_source/mysql-8.0.32/storage/innobase/include/dict0dict.ic:1383
1383	  return (dict_name::is_partition(table->name.m_name));
(gdb)
dict_name::is_partition (dict_name="test/instant_test") at /mysql_source/mysql-8.0.32/storage/innobase/dict/dict0dd.cc:7284
7284	  return check_partition(dict_name, false, position);
dict_name::check_partition (dict_name="test/instant_test", sub_part=false, position=@0xffff9834afd8: 281473235333136) at /mysql_source/mysql-8.0.32/storage/innobase/dict/dict0dd.cc:7200
7200	  position = dict_name.find(part_sep);
dict_name::check_partition (dict_name="test/instant_test", sub_part=false, position=@0xffff9834afd8: 18446744073709551615) at /mysql_source/mysql-8.0.32/storage/innobase/dict/dict0dd.cc:7210
7210	  position = dict_name.find(alt_sep);


  • Fulltext index 를 사용하는 테이블은 instant DDL이 안됩니다!
    • 사실 instant 뿐만 아니라 inplace도 안됩니다 
    • 이유는 fulltext index를 사용하는 테이블들은 FTS_DOC_ID 라는 히든 필드를 사용하는 것으로 보이는데, 이 동작 관련해서 아직 inplace, instasnt algorithm을 지원하지 않는것으로 보입니다. (뇌피셜)
static bool innobase_fulltext_exist(const TABLE *table) {
  for (uint i = 0; i < table->s->keys; i++) {
    if (table->key_info[i].flags & HA_FULLTEXT) {
      return (true);
    }
  }

  return (false);
}

      if (key->flags & HA_FULLTEXT) {
        assert(!(key->flags & HA_KEYFLAG_MASK &
                 ~(HA_FULLTEXT | HA_PACK_KEY | HA_GENERATED_KEY |
                   HA_BINARY_PACK_KEY)));
        ha_alter_info->unsupported_reason =
            innobase_get_err_msg(ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FTS);

# 정리 

  • mysql8 (aurora 3 ) 버전 들어오면서 DDL을 처리할 때 기존의 inplace 방식이 아닌 instant alogrithm이 추가 되었다
  • instasnt 는 기존 inplace 와 달리 데이터 복제, 반영 중 들어온 로그 반영의 과정 없이 metadata 만 수정해버려서 엄청 빠르다 
  • 단 이렇게 엄청 좋은 instsant 도 아직은 컬럼 추가 쪽 밖에 지원하지 않고
  • 한 테이블에 대해 최대 64번만 지원된다는 제약이 있으며 ( table rebuild로 초기화 가능) 
  • 파티션 테이블, 풀텍스트 인덱스를 사용하면 주의해야 한다
  • 혹여나 작업이 잘못되어 instant 가  inplace 로 fallback 되지 않도록 alter table … , algorithm= ? ,lock=none 구문을 생활화하자