grep

Backend

MySQL InnoDB Log에 대한 이해 - (1)

christy.seo, sun.j카카오

2025년 8월 11일

원문에서 보기 ↗

개요

MySQL의 기본 스토리지 엔진인 InnoDB는 트랜잭션의 무결성과 영속성을 보장하기 위해 Redo Log를 생성하여 관리합니다. Redo Log는 WAL(Write-Ahead Log)의 일종으로, WAL의 핵심기능을 제공합니다

다음과 같은 기능을 가지고 있어야 WAL이라고 할 수 있습니다.

WAL로서 Redo Log는 InnoDB 스토리지 엔진의 트랜잭션의 영속성을 보장하기 위한 핵심적인 메커니즘이기 때문에 Redo Log 관리 방식을 이해하고 있는 것은 중요합니다. Redo Log 동작 방식을 이해하면 전반적인 InnoDB 스토리지 엔진을 이해할 수 있습니다.

이 글에서는 먼저 Redo Log가 어떤 목적으로 DBMS에서 사용되는지 간단히 알아보고 MySQL Ver. 5.7의 동작 방식을 상세히 설명하려고 합니다. 그리고, 2편에서는 MySQL Ver. 8.0에서 Redo Log 개선 사항을 이해해 보도록 하겠습니다.

1. Redo Log 란 무엇인가?

Redo Log는 앞서 개요에서 소개해 드린 것처럼 데이터의 영속성을 보장하기 위한 메커니즘으로 InnoDB 스토리지 엔진이 사용하는 WAL을 말합니다. 즉, 데이터 변경 사항을 디스크로 안전하게 기록하는 과정에서 시스템 장애 시 유실 방지를 하기 위한 로그입니다.

1.1 Redo Log 기본 요소

Redo Log는 기본적으로 다음과 같이 Log Buffer와 Log File을 가지고 동작합니다.

1.1.1 Redo Log Buffer

Redo Log Buffer는 Redo Log 작성 전에 변경정보가 먼저 저장되는 메모리 영역입니다. 기본 크기는 16MB이지만, innodb_log_buffer_size 시스템 변수를 사용하여 크기를 변경할 수 있습니다. Log Buffer의 내용은 시스템 변수의 설정값에 따라 결정된 Flush 주기에 따라 Redo Log File에 작성됩니다.

1.1.2 Redo Log File

Redo Log File은 데이터 변경 정보를 저장하는 디스크에 생성되는 파일을 말합니다. 여기에 저장된 정보를 기반으로 InnoDB 스토리지 엔진은 Crash Recovery를 진행합니다. Redo Log File은 기본적으로 2개가 생성되지만, innodb_log_files_in_group 시스템 변수를 통해 생성되는 파일 갯수를 변경할 수 있습니다. 또한, innodb_log_file_size 시스템 변수를 통해 파일의 크기 조정도 가능합니다.

1.2 Redo Log 기본 동작 방식

Redo Log의 기본 동작 방식에 대해 먼저 간단히 알아보겠습니다.

1.1.1 트랜잭션 수행 시 Redo Log 동작 방식

먼저 기본적으로 트랜잭션 수행 예제를 통해 Redo Log 전반적인 작업 흐름을 살펴보고자 합니다.

동작 방식을 순서대로 설명하면 다음과 같습니다.

  1. DML 쿼리가 수행되어 Page내용이 변경되면, Buffer Pool에 Page가 복사되고 그 영역에서 변경 작업이 진행됩니다.
  2. 1번에서 진행된 작업 내용은 MTR 단위로 세션별 메모리 영역에 기록됩니다.
  3. MTR에 변경 내용이 모두 기록되면 MTR commit()이 수행되며 Redo Log Buffer 으로 이동하게 됩니다. 그리고, 변경된 페이지들을 Buffer Pool의 Flush List에 추가합니다.
  4. Flush 이벤트 발생 시 또는 Buffer Pool에 대한 Checkpoint 발생 시 Redo Log Buffer에 있는 내용이 Redo Log File로 저장됩니다. (Checkpoint 발생 시 디스크에 Page내용이 쓰여지기 전에 Redo Log Buffer의 내용이 먼저 쓰이게 됩니다. )
  5. Redo Log File 저장 후 Buffer Pool에 있는 Flush List에 있는 Page 정보가 Disk의 데이터 파일로 내려써지게 됩니다.

위 논리적인 동작방식은 모든 InnoDB 버전에서 동일하게 진행됩니다.

1.1.2 Redo Log Buffer Flush 주기 설정

앞서 이야기 한대로 Redo Log Buffer는 어떤 기준에 다다르거나, 이벤트 발생 시 Redo Log Buffer의 변경 내용을 Flush 합니다. Flush 작업의 주기는 MySQL 성능에 영향을 주는 아주 중요한 요소 입니다. 모든 Flush 유발 이벤트를 다 제어할 수는 없지만, 트랜잭션 처리와 관련하여 innodb_flush_log_at_trx_commit 시스템 변수를 사용하면 DBE(Database Engineer)가 Flush 주기를 제어할 수 있습니다.

이 시스템 변수의 값은 실행되는 트랜잭션의 안정성과 DB 성능에 크게 영향을 주기 때문에 신중히 고민하여 결정해야 합니다. 안정성이 중요할 경우 1로 설정하여 커밋 시 마다 디스크에 작업 내용이 Flush 될 수 있게 해야 하고, 성능이 중요하다면, 0 또는 2를 선택하여 성능을 높이도록 해야 합니다.

innodb_flush_log_at_trx_commit 시스템 변수는 MySQL 내구성과 성능 사이의 균형을 맞추는데 중요한 변수이니 따로 꼭 상세히 공부해 보기를 추천합니다. 여기서는 이 시스템 변수에 관한 내용은 간단히만 정리하고 넘어가도록 하겠습니다.

여기서는 Redo Log에 대한 기본 개념 및 동작 방식에 대해 알아보았습니다. 이제는 상세 내부 동작 방식을 알아보도록 하겠습니다. 상세 동작 방식을 이해하기 위해 먼저 MTR이라고 하는 mini Transaction에 대해 이해해야 합니다. 이제 mini Transaction에 대해 알아봅니다.

2. MTR (mini-Transaction)

MTR은 Redo Log가 내부 작업을 할 때 사용하는 가장 작은 작업 단위를 말합니다. 즉, 단일 Page 또는 소수의 Page에 대한 변경 작업을 Redo Log에 기록하는 가장 작은 단위입니다.

2.1 MTR 특징

MTR은 다음과 같은 특징을 가지고 있습니다.

Redo Log 작성 시 MTR 구조를 사용함으로써 다음과 같은 효과를 얻을 수 있습니다.

2.2 MTR 구조체

MySQL 소스 안에서 MTR은 mtr_t 구조체를 통해 구현됩니다. mtr_t 구조체 구성 요소 중 간단히 정리하면 다음과 같다.

struct mtr_t {
struct Impl { 
/** mtr이 잠금을 사용하기 위한 용도  **/
mtr_buf_t m_memo;

	/** mtr에서 사용하는 mini Transaction 로그 **/
mtr_buf_t m_log;

/** 현재 mtr 로그 모드 **/
mtr_log_t m_log_mode; 

	/** MTR 생명주기 정보 **/
mtr_stat_t m_state;

/** mtr 로그에 기록된 페이지 초기 로그 레코드 수 **/
ib_uint32_t	m_n_log_recs;

	/** 더티 페이지가 생성되고 디스크를 플러시가 필요한 경우 true **/
bool m_made_dirty; 

	/** 버퍼 페이지를 변경한 경우 true **/
bool m_modifications; 

	/** ibuf의 데이터가 변경된 경우 true **/
bool m_inside_ibuf;

 	/** 현재 mtr에 의해 수정된 테이블스페이스 **/
fil_space_t*  m_user_space;
/** 현재 mtr에 의해 수정된 Undo 테이블스페이스 **/
fil_space_t*  m_undo_space;  
/** 현재 mtr에 의해 수정된 System 테이블스페이스 **/
fil_space_t*  m_sys_space;

	/** Flush Observer **/
	/** m_log_moderedo를 쓰지 않겠다는 뜻일 때 더티 페이지의 플러시 여부는 이 파라미터
(인덱스 생성) 로 판단한다. **/
FlushObserver* m_flush_observer; 

	/** 현재 자신의 mini-Transaction **/
mtr_t*  m_mtr;
}

2.2.1 MTR 구조체 추가 설명

MTR 구조체 내부의 여러 변수 중 몇 개만 좀 더 알아보도록 하겠습니다.

[m_memo]

MTR 내부에서 잠금 이용할 때 사용되는 변수입니다.

[m_log]

MTR 로그를 저장하여 관리하는 변수입니다.

[m_log_mode]

현재 관리하는 MTR Log의 모드를 보여주는 변수로 다음과 같은 값을 가질 수 있습니다.

[m_state]

MTR의 생명주기 상태 정보를 가진 변수입니다. 다음과 같은 4가지 상태로 표현됩니다.

MTR 구조체는 각 세션에서 트랜잭션을 수행할 때, 각 세션이 할당받아 사용하는 세션 메모리 영역의 Heap 영역에 할당됩니다. MTR이 필요한 상황에서 구조체를 호출하는 방식으로 생성되는데 초기에는 64byte로 받아서 생성됩니다. 그리고, 필요한 만큼 Linked List로 연결하여 사용합니다.

trx_undo_assign_udno 함수 소스 상의 예제를 보면 다음과 같이 사용하는 것을 볼 수 있습니다.


trx_undo_assign_undo(
/*=================*/
  trx_t*    trx,    /*!< in: transaction */
  trx_undo_ptr_t* undo_ptr, /*!< in: assign undo log from
          referred rollback segment. */
  ulint   type)   /*!< in: TRX_UNDO_INSERT or
          TRX_UNDO_UPDATE */
{
  trx_rseg_t* rseg;
  trx_undo_t* undo;
  mtr_t   mtr;     /** <------ MTR 선언 부분   **/
  dberr_t   err = DB_SUCCESS;

  ut_ad(trx);

  mtr_start(&mtr);  /** <------ MTR 초기화    **/
  if (&trx->rsegs.m_noredo == undo_ptr) {
    mtr.set_log_mode(MTR_LOG_NO_REDO);;
  } else {
    ut_ad(&trx->rsegs.m_redo == undo_ptr);
  }

  if (trx_sys_is_noredo_rseg_slot(rseg->id)) {
    mtr.set_log_mode(MTR_LOG_NO_REDO);;
    ut_ad(undo_ptr == &trx->rsegs.m_noredo);
  } else {
    ut_ad(undo_ptr == &trx->rsegs.m_redo);
  }

  mutex_enter(&rseg->mutex);

  DBUG_EXECUTE_IF(
    "ib_create_table_fail_too_many_trx",
    err = DB_TOO_MANY_CONCURRENT_TRXS;
    goto func_exit;
  );

  undo = trx_undo_reuse_cached(trx, rseg, type, trx->id, trx->xid,&mtr); /**  <------ MTR 하위 함수로 전달하여 사용 **/
  if (undo == NULL) {
    err = trx_undo_create(trx, rseg, type, trx->id, trx->xid, &undo, &mtr);                                                    /** <------ MTR 하위 함수로 전달하여 사용 **/
    if (err != DB_SUCCESS) {

      goto func_exit;
    }
  }

  if (type == TRX_UNDO_INSERT) {
    UT_LIST_ADD_FIRST(rseg->insert_undo_list, undo);
    ut_ad(undo_ptr->insert_undo == NULL);
    undo_ptr->insert_undo = undo;
  } else {
    UT_LIST_ADD_FIRST(rseg->update_undo_list, undo);
    ut_ad(undo_ptr->update_undo == NULL);
    undo_ptr->update_undo = undo;
  }

  if (trx_get_dict_operation(trx) != TRX_DICT_OP_NONE) {
    trx_undo_mark_as_dict_operation(trx, undo, &mtr); /** <------ MTR 하위 함수로 전달하여 사용 **/
  }

func_exit:
  mutex_exit(&(rseg->mutex));
  mtr_commit(&mtr);  /** <------ MTR 저장완료 후 Commit 진행  **/
  
  return(err);
}

2.2.3 MTR 생애 주기

MTR은 트랜잭션이 시작하면 트랜잭션이 수행되면서 발생하는 데이터 변경 내역을 저장하기 위해 생성됩니다. 이때 저장하는 정보는 실제 Page의 내용뿐 아니라 Undo Tablespace에 생성되어 관리하는 페이지 정보도 같이 저장됩니다.

MTR은 크게 다음과 같은 순서로 생애 주기가 진행됩니다.

각 MTR을 기준으로 생애 주기를 함수를 기준으로 설명하면 다음과 같이 요약할 수 있습니다.

주기동작 내용
start()생성된 MTR에 대한 초기화 작업이 이루어지는 단계
commit()MTR 객체 내용을 Redo Log Buffer로 전달할지 말지 검토한 뒤, Case에 맞게 해당 작업을 수행하는 단계
release_resource()모든 리소스를 반환하고 종료하는 단계

그림으로 표현하면 다음과 같이 간단히 도식화 해볼 수 있습니다.

위 그림은 각 단계별로 MTR이 가져가는 상태값과 모드 정보를 같이 보여주고 있습니다. MTR 생애 주기에 따른 모드와 상태값을 같이 확인하시면서 보시면 생애 주기에 따른 동작 방식을 이해하기 쉬우실 겁니다.

2.2.3.1 MTR Commit

MTR 생애 주기 중 Commit 작업 부분을 좀 더 상세히 알아보고자 합니다. 전반적인 Flow는 다음과 같이 도식화할 수 있습니다.

Commit 함수가 호출 되면 먼저 해당 작업 내용을 Redo Log Buffer로 넘길지 취소 시킬 지 확인하는 단계를 거치게 됩니다. 이 때 Commit 작업이 취소되지 않는다면 execute() 함수를 호출하여 다음과 같은 작업을 수행합니다.

3. 트랜잭션 기반의 MTR 동작 방식에 대한 이해

이제 부터는 트랜잭션 기반으로 MTR 내부 동작 방식을 이해해 보도록 하겠습니다.

3.1 트랜잭션 수행 시 MTR 동작방식

UPDATE 쿼리 하나를 수행하는 트랜잭션을 기준으로 트랜잭션 작업에 대한 작업 내용이 어떻게 저장되는지 확인해 보도록 하겠습니다.

위 그림을 통해 확인할 수 있듯이 쿼리가 수행되면, MTR은 크게 3가지 MTR을 생성합니다.

  1. Buffer Pool에 저장되는 Page 변경 내용을 담는 MTR (초록색 MTR)
  2. Undo TableSpace 영역에 저장되는 Page 변경 내용을 담는 MTR (주황색 MTR)
  3. 기타 커밋 작업과 관련된 정보를 저장하는 MTR (노랑색 MTR)

[참고]

위 그림에서는 데이터 종류에 따라 3가지 색으로 MTR을 표현했습니다.

트랜잭션이 시작하고 DML 쿼리를 수행할 때, 먼저 세션의 메모리 공간에 MTR을 생성하고 mtr.start()를 수행하여 초기화를 진행합니다. 그 뒤 실제 쿼리 수행 후 발생하는 변경 정보들을 각각의 MTR에 저장합니다. 이 때 발생하는 변경 정보 중 Undo와 관련된 정보는 Undo용 MTR에 저장하고, 일반 Page용 변경 정보는 일반 데이터 변경용 MTR에 저장합니다. 그리고 추가적으로 발생하는 커밋용 정보들은 Commit용 MTR에 저장합니다. 이때, 작업량이 많아서 MTR이 더 필요하게 되면 추가 생성하여 기존 MTR에 링크드 리스트로 연결하여 사용합니다.

쿼리 수행이 완료되면, mtr.commit()이 호출 됩니다. mtr.commit()이 호출 되면, mtr에 기록된 정보들을 Redo Log Buffer로 전달하게 됩니다. 그리고 해당 변경 작업으로 인해 flush 되어야 하는 dirty page들은 buffer pool flush List에 추가됩니다.

그리고, 작업이 다 완료된 MTR은 release_resource()를 호출하여 할당받은 리소스를 모두 해제하고 종료합니다.

번외1. 트랜잭션 완료 후 Buffer Pool Flush List 처리 방식

트랜잭션 작업이 완료되고 나면, Buffer Pool Flush List에 추가된 Page들에 대한 Flush 작업이 발생합니다. 여기서는 트랜잭션 작업의 마지막 단계라고 할 수 있는 Dirty Page에 대한 Flush가 어떻게 동작하는지 간단히 설명하고자 합니다.

Flush List로 관리되고 있는 Dirty Page들은 여러 기준에 따라 Flush 작업이 진행됩니다. Flush 작업은 그림에서 확인할 수 있듯이 동기 방식과 비동기 방식이 모두 동작합니다. 비동기 방식으로 동작하는 함수인 buf_flush_write_block_now()와 동기 방식으로 동작하는 buf_dblwr_flush_buffered_writes() 함수가 호출되어 Flush가 진행됩니다만, 마지막에 데이터 파일에 작성할 때는 file_io() 함수를 호출하여 진행합니다.

번외2. 트랜잭션 롤백 시 동작 방식

트랜잭션 롤백이 수행될 때 다음과 같은 방식으로 롤백이 진행됩니다.

트랜잭션 수행 시 각각 쿼리마다 MTR이 생성되고 commit()함수가 수행되어 해당 내용이 Redo Log에 저장됩니다. 그래서, 트랜잭션을 롤백할 때도 해당 변경 내용이 다시 되돌아가기 위해 Redo Log로 저장되어야 합니다. 그 부분을 유념하여 그림으로 프로세스를 확인하시면 좋을 것 같습니다.

4. MySQL Ver. 5.7 Redo Log 동작 방식

앞에서는 Redo Log 내부 동작 방식을 이해하기 위한 기초 지식을 정리해 보았습니다. 이제부터는 그 지식을 기반으로 Ver. 5.7의 동작 방식을 알아보도록 하겠습니다.

4.1 Mutex 기반의 동작 방식

MySQL Ver. 5.7의 Redo Log 동작 방식은 간단히 이야기하여 Mutex 기반으로 동작한다라고 설명할 수 있습니다. 각각의 작업 단계에서 작업의 안정성을 보장하기 위해 MySQL Ver. 5.7은 Mutex 사용합니다. 그래서, MTR 내부 동작 방식도 다음과 같이 진행됩니다.

트랜잭션 수행 시 MTR이 생성되어 작성되고 나서 내부적으로 MTR commit()이 호출되면 먼저 로그 버퍼 쓰기를 위한 Redo Log Buffer의 Mutex를 확보합니다. Mutex를 확보하고 나면 MTR 정보를 Redo Log Buffer에 작성합니다. 작성을 완료하면 확보한 Mutex를 해제하여 반환합니다. 그리로, 변경된 페이지 정보를 Buffer Pool의 Flush List에 추가하고, MTR 커밋을 위한 마무리로 관련된 리소스를 해제하는 작업을 진행합니다.

4.2 트랜잭션 기반의 Redo Log 동작 방식 설명

간단히 함수로 확인했던 Redo Log 동작 방식을 실제 트랜잭션을 수행하는 예제를 통해 좀 더 상세히 알아보도록 하겠습니다.

위 그림에서 보시는 바와 같이 전체적인 트랜잭션의 작업은 크게 3단계로 나누어 볼 수 있습니다.

  1. MTR에 저장된 변경 내용을 Redo Log Buffer로 전달합니다.
  2. Dirty Page 를 Buffer Pool의 Flush List에 추가합니다.
  3. Redo Log Buffer에 저장된 변경 정보를 Redo Log File에 작성합니다.

그림을 통해 확인할 수 있지만, 각각의 단계에서 리소스에 경합 관리가 필요한 부분은 Mutex를 통한 리소스 확보 후 작업이 진행됨을 확인할 수 있습니다. 위 단계 중 Log Buffer 작업 부분인 1번 부분에 대한 것만 좀 더 상세히 알아보도록 하겠습니다.

4.2.1 MTR 정보 Log Buffer로 전달하기

각각의 세션에 수행되는 트랜잭션이 어떤 단계로 Global Redo Log Buffer에 작성되는지 좀 더 자세히 살펴 보도록 하겠습니다.

사용자 쿼리가 수행되면 작업이 진행되면서 발생하는 변경 정보를 MTR 객체를 만들어서 저장합니다. 이때 먼저 MTR 객체는 세션에 할당한 메모리 영역에 MTR정보를 저장합니다. 그리고 쿼리 작업이 다 끝나고 나면 mtr.commit()이 수행됩니다. 세션에 저장한 MTR 정보들은 mtr.commit()이 수행되면 Global Redo Log Buffer로 전달되는데요. 이 때 다른 세션들의 작업 내용 저장과 충돌 되는 것을 막기 위해 로그 버퍼 영역의 log_sys_mutex를 먼저 확보합니다. 그 후에 MTR 정보를 Global Redo Log Buffer에 저장하게 됩니다.

이와 같은 방법으로 각 세션에서 수행되는 트랜잭션의 정보는 Global Redo Log Buffer에 작성됩니다. 그리고, Buffer에 작성된 내용은 앞에 Chapter 1.1.2에 설명한 주기에 따라 디스크의 Redo Log File에 작성됩니다.

결론

이 문서에서는 InnoDB Redo Log 동작 방식을 이해하기 위해 필요한 기초 정보 및 MTR에 대해 설명하였습니다. 그리고, MySQL Ver. 5.7에서 세션별로 관리되는 MTR 정보들이 어떻게 Global Redo Log Buffer에 전달되는지 그 로직을 간단히 정리해 보았습니다. 다음 2편 에서는 1편에서 전달드린 내용을 기반으로 MySQL Ver. 8.0에서 어떻게 성능 개선을 이루었는지 살펴 보도록 하겠습니다.

InnoDB Redo Log 동작 방식에 대한 이해가 어려우신 분들에게 많은 도움이 되었으면 좋겠습니다.

관련 글


참고