사용자들은 저수준 동시성 성능의 감소를 더 쉽게 인식하는 경향이 있지만, 고수준 동시성 성능의 향상은 종종 인지하기 더 어렵습니다. 따라서 저수준 동시성 성능을 유지하는 것은 매우 중요하며, 이는 사용자 경험과 업그레이드 의사에 직접적인 영향을 미칩니다 [1].
광범위한 사용자 피드백에 따르면, MySQL 8.0으로 업그레이드한 후 사용자는 일반적으로 성능 저하를 인식하고 있으며, 특히 배치 삽입 및 조인 작업에서 더욱 두드러집니다. 이러한 하향 추세는 MySQL의 더 높은 버전에서 더욱 명확해졌습니다. 또한 일부 MySQL 애호가와 테스트 담당자들은 업그레이드 후 여러 sysbench 테스트에서 성능 저하를 보고했습니다.
이러한 성능 문제를 피할 수 있을까요? 아니면 더 구체적으로, 성능 저하의 지속적인 추세를 어떻게 과학적으로 평가해야 할까요? 이러한 질문들은 중요하게 고려해야 할 사항입니다.
공식 팀이 계속해서 최적화를 진행하고 있지만, 성능의 점진적인 악화는 간과할 수 없습니다. 특정 시나리오에서는 개선이 있는 것처럼 보일 수 있지만, 이는 모든 시나리오에서 성능이 동일하게 최적화되었다는 것을 의미하지 않습니다. 게다가 특정 시나리오에서 성능을 최적화하는 것이 다른 영역의 성능 저하를 초래할 수도 있습니다.
MySQL 성능 저하의 근본 원인
일반적으로 더 많은 기능이 추가됨에 따라 코드베이스가 성장하고, 기능의 지속적인 확장으로 인해 성능을 제어하기가 점점 더 어려워집니다.
MySQL 개발자들은 성능 하락을 인식하지 못하는 경우가 많은데, 코드베이스에 추가되는 각 부분은 성능이 매우 조금씩 감소한다는 결과를 가져옵니다. 그러나 시간이 지남에 따라, 이러한 작은 하락이 누적되어 사용자들이 MySQL의 최신 버전에서 뚜렷한 성능 저하를 인식하게 됩니다.
예를 들어, 다음 그림은 MySQL 8.0.27과 비교하여 MySQL 8.0.40의 성능 하락을 보여주는 간단한 단일 조인 작업의 성능을 보여줍니다:
다음 그림은 단일 동시성 하에서 일괄 삽입 성능 테스트를 보여주며, MySQL 8.0.40의 성능 하락을 버전 5.7.44와 비교합니다:
위 두 그래프를 통해, 버전 8.0.40의 성능이 좋지 않다는 것을 알 수 있습니다.
이제 MySQL의 성능 저하의 근본 원인을 코드 수준에서 분석해 봅시다. 아래는 MySQL 8.0의 PT_insert_values_list::contextualize
함수입니다:
MySQL 5.7의 해당 PT_insert_values_list::contextualize
함수는 다음과 같습니다:
코드 비교를 통해, MySQL 8.0은 보다 우아한 코드를 가지고 있는 것으로 보이며, 진전이 있는 것처럼 보입니다.
그러나 안타깝게도, 많은 경우에는 이러한 코드 개선의 동기가 성능 저하로 이어지곤 합니다. MySQL 공식 팀은 이전의 List
데이터 구조를 deque
로 교체했는데, 이것이 점진적인 성능 저하의 원인 중 하나가 되었습니다. deque
문서를 살펴보겠습니다:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
위 설명에서 보듯이, 극단적인 경우에는 단일 요소를 유지하기 위해 전체 배열을 할당해야 하며, 이로 인해 메모리 효율성이 매우 낮아집니다. 예를 들어, 대량 삽입의 경우, 많은 수의 레코드를 삽입해야 하며, 공식 구현에서는 각 레코드를 별도의 덱(deque)에 저장합니다. 레코드 내용이 최소한일지라도 여전히 덱을 할당해야 합니다. MySQL 덱 구현은 빠른 조회를 지원하기 위해 각 덱에 1KB의 메모리를 할당합니다.
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
공식 구현은 인덱스 정보를 저장하기 위해 1KB의 메모리를 사용하며, 레코드 길이가 크지 않더라도 레코드 수가 많으면 메모리 접근 주소가 비연속적이 되어 캐시 친화성이 떨어질 수 있습니다. 이 설계는 캐시 친화성을 개선하기 위해 의도되었지만, 완전히 효과적이지는 않았습니다.
원래 구현은 메모리 풀을 통해 메모리를 할당하는 List 데이터 구조를 사용하여 어느 정도의 캐시 친화성을 제공했습니다. 랜덤 접근이 덜 효율적이지만, List 요소에 대한 순차 접근을 최적화하면 성능이 크게 향상됩니다.
MySQL 8.0으로 업그레이드하는 동안 사용자들은 배치 삽입 성능의 현저한 저하를 관찰했으며, 그 주요 원인 중 하나는 기본 데이터 구조의 상당한 변화였습니다.
또한, 공식 팀이 redo 로그 메커니즘을 개선하면서 MTR 커밋 작업 효율성이 감소했습니다. MySQL 5.7과 비교할 때, 추가된 코드는 개별 커밋의 성능을 상당히 저하시키지만, 전체 쓰기 처리량은 크게 개선되었습니다.
MySQL 5.7.44에서 MTR 커밋의 핵심 execute
작업을 살펴보겠습니다:
MySQL 8.0.40에서 MTR 커밋의 핵심 execute
작업을 검토해 보겠습니다.
비교하면 MySQL 8.0.40에서 MTR 커밋의 execute 작업은 더 복잡해졌고, 더 많은 단계가 포함되었음을 명확히 알 수 있습니다. 이 복잡성은 저 동시성 쓰기 성능의 감소의 주요 원인 중 하나입니다.
특히, m_impl->m_log.for_each_block(write_log)
와 log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn)
작업은 상당한 오버헤드가 있습니다. 이러한 변경 사항은 고 동시성 성능을 향상시키기 위해 이루어졌지만, 저 동시성 성능을 희생시킨 결과를 초래했습니다.
고 동시성 모드에서의 리두 로그 우선순위화로 인해 저 동시성 워크로드의 성능이 저하됩니다. innodb_log_writer_threads
의 도입은 저 동시성 성능 문제를 완화하기 위한 것이지만, 상기 함수들의 실행에는 영향을 미치지 않습니다. 이러한 작업들이 더 복잡해지고 빈번한 MTR 커밋이 필요하게 되었기 때문에 성능은 여전히 상당히 떨어졌습니다.
인스턴트 추가/제거 기능이 성능에 미치는 영향을 살펴보겠습니다. 아래는 MySQL 5.7의 rec_init_offsets_comp_ordinary
함수입니다.
MySQL 8.0.40의 rec_init_offsets_comp_ordinary
함수는 다음과 같습니다:
위의 코드에서 즉시 추가/삭제 열 기능이 도입됨에 따라 rec_init_offsets_comp_ordinary
함수가 눈에 띄게 복잡해졌고, 더 많은 함수 호출이 추가되었으며 캐시 최적화에 심각한 영향을 미치는 switch 문이 추가되었습니다. 이 함수는 자주 호출되므로 업데이트 인덱스, 배치 삽입 및 조인 성능에 직접적인 영향을 미쳐 주요 성능 저하를 초래합니다.
또한, MySQL 8.0의 성능 저하는 위에 국한되지 않으며, 전체 성능 저하에 기여하는 여러 다른 영역이 있으며, 특히 인라인 함수의 확장에 미치는 영향이 큽니다. 예를 들어, 다음 코드는 인라인 함수의 확장에 영향을 미칩니다:
우리의 테스트에 따르면 ib::fatal
문은 인라인 최적화에 심각한 방해를 줍니다. 자주 접근하는 함수의 경우, 인라인 최적화에 방해가 되는 문을 피하는 것이 좋습니다.
다음으로, 유사한 문제를 살펴보겠습니다. row_sel_store_mysql_field
함수는 자주 호출되며, row_sel_field_store_in_mysql_format
은 그 안에서 핫스팟 함수입니다. 구체적인 코드는 다음과 같습니다:
row_sel_field_store_in_mysql_format
함수는 궁극적으로 row_sel_field_store_in_mysql_format_func
를 호출합니다.
row_sel_field_store_in_mysql_format_func
함수는 ib::fatal
코드가 존재하여 인라인화될 수 없습니다.
초당 수천만 번 실행되는 비효율적인 함수는 조인 성능에 심각한 영향을 미칠 수 있습니다.
성능 하락의 이유를 계속 탐구해 봅시다. 다음의 공식적인 성능 최적화가 사실은 조인 성능 하락의 원인 중 하나입니다. 특정 쿼리를 개선할 수는 있지만, 여전히 보통의 조인 작업의 성능 저하 이유 중 일부입니다.
MySQL의 문제는 여기에 그치지 않습니다. 위의 분석에서 볼 수 있듯이, MySQL의 성능 하락은 이유 없이 발생하는 것이 아닙니다. 미세한 문제들의 연속이 쌓이면 사용자들이 경험하는 성능 저하로 이어질 수 있습니다. 그러나 이러한 문제들은 종종 식별하기 어려워, 해결하기 더 어렵게 만듭니다.
소위 ‘조기 최적화’는 모든 악의 근원이며, MySQL 개발에는 해당되지 않습니다. 데이터베이스 개발은 복잡한 과정이며, 시간이 흐름에 따른 성능을 등한시하는 것은 이후의 성능 향상을 상당히 어렵게 만듭니다.
MySQL 성능 하락 완화 솔루션
쓰기 성능 하락의 주된 이유는 MTR 커밋 문제, 순간적인 열 추가/제거, 그리고 여러 요소와 관련이 있습니다. 이들은 전통적인 방법으로 최적화하기 어렵습니다. 그러나 사용자들은 PGO 최적화를 통해 성능 하락을 보상할 수 있습니다. 적절한 전략을 통해, 성능은 일반적으로 안정적으로 유지될 수 있습니다.
배치 삽입 성능 저하에 대해, 우리의 오픈 소스 버전 [2]은 공식 deque를 개선된 리스트 구현으로 대체합니다. 이는 주로 메모리 효율성 문제를 해결하고 성능 저하를 부분적으로 완화할 수 있습니다. PGO 최적화를 오픈 소스 버전과 결합하면 배치 삽입 성능이 MySQL 5.7에 근접할 수 있습니다.
사용자는 또한 여러 스레드를 활용하여 동시 배치 처리를 수행할 수 있으며, 이는 개선된 redo 로그의 동시성을 최대한 활용하여 배치 삽입 성능을 크게 향상시킬 수 있습니다.
업데이트 인덱스 문제와 관련하여, 새로운 코드의 불가피한 추가로 인해 PGO 최적화가 이 문제를 완화하는 데 도움이 될 수 있습니다. 우리의 PGO 버전 [2]은 이 문제를 상당히 완화할 수 있습니다.
읽기 성능, 특히 조인 성능에 대해서는 인라인 문제 수정 및 기타 최적화를 포함하여 상당한 개선을 이루었습니다. PGO가 추가됨으로써 조인 성능이 공식 버전보다 30% 이상 증가할 수 있습니다.
우리는 저동시성 성능 최적화에 계속해서 시간 투자를 할 것입니다. 이 과정은 길지만 개선이 필요한 여러 영역이 포함되어 있습니다.
오픈 소스 버전은 테스트를 위해 제공되며, MySQL 성능을 개선하기 위한 노력이 지속될 것입니다.
참고문헌
[1] Bin Wang (2024). 소프트웨어 공학의 문제 해결 기술: MySQL을 개선하는 방법.
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis