MySQL 프로세스와 스레드 모델: connection thread, background thread, thread cache
MySQL 서버의 connection thread, background thread, thread cache가 성능과 장애 대응에 미치는 영향을 운영 관점에서 정리한다.
1. 운영자가 스레드 모델을 이해해야 하는 이유
MySQL은 하나의 mysqld 프로세스 안에서 다수의 스레드를 사용해 클라이언트 연결, SQL 실행, 버퍼 관리, 로그 기록, 체크포인트, 복제, 이벤트 스케줄링 같은 작업을 동시에 처리한다. 운영 현장에서 “MySQL이 느리다”는 증상은 종종 쿼리 자체의 문제로만 보이지만, 실제로는 연결 폭증, 스레드 생성 비용, background thread 지연, InnoDB purge 적체, redo log flush 병목, 복제 SQL thread 지연처럼 스레드 모델과 직접 연결된 원인에서 출발하는 경우가 많다.
스레드는 단순한 구현 세부 사항이 아니다. MySQL에서 스레드는 다음 운영 판단의 기준이 된다.
- 동시 접속 수가 늘 때 CPU 사용률이 왜 선형적으로 증가하지 않는지 판단한다.
Threads_connected,Threads_running,Threads_created,Threads_cached지표를 보고 연결 풀과thread_cache_size를 조정한다.- InnoDB background thread가 밀릴 때 checkpoint, dirty page flush, purge lag, history list length가 왜 함께 악화되는지 이해한다.
- 장애 시
SHOW PROCESSLIST, Performance Schema, error log를 어떤 순서로 볼지 결정한다. - Aurora MySQL처럼 스토리지 계층이 분리된 환경에서 기존 MySQL의 I/O thread 해석을 그대로 적용해도 되는지 구분한다.
이 글은 MySQL 서버를 “하나의 프로세스와 여러 종류의 스레드가 협업하는 실행 엔진”으로 보고, connection thread, background thread, thread cache를 중심으로 운영자가 알아야 할 내부 동작과 진단 방법을 정리한다.
2. MySQL 서버 프로세스의 큰 구조
일반적인 Linux 환경에서 MySQL 서버는 mysqld라는 단일 OS 프로세스로 실행된다. 이 프로세스 내부에 SQL layer, storage engine layer, replication component, plugin, Performance Schema 등이 함께 로드된다. 사용자가 mysql 클라이언트, 애플리케이션 서버, 배치 작업에서 접속하면 MySQL은 해당 연결을 처리할 스레드를 할당하고, 그 스레드가 인증, 세션 변수 설정, SQL 파싱, 최적화, 실행, 결과 반환을 담당한다.
단순화하면 다음과 같은 흐름이다.
클라이언트 TCP/Unix socket 연결
-> mysqld listener
-> connection 수락
-> connection thread 할당 또는 생성
-> 인증 및 세션 초기화
-> SQL parse/optimize/execute
-> storage engine 호출
-> 결과 반환
-> 연결 유지 또는 종료
MySQL이 멀티프로세스 서버가 아니라 멀티스레드 서버라는 점은 중요하다. 하나의 프로세스 주소 공간 안에서 스레드들이 버퍼 풀, table cache, metadata lock 구조, Performance Schema 메모리, InnoDB 내부 자료구조를 공유한다. 공유 구조를 사용하기 때문에 캐시 효율은 좋지만, 동시에 latch, mutex, condition wait 같은 동기화 비용도 발생한다.
운영자는 이 구조를 다음처럼 해석해야 한다.
- CPU가 많다고 해서 모든 쿼리가 무제한 병렬 실행되는 것은 아니다. 쿼리 실행 경로의 latch, row lock, metadata lock, I/O, log flush에서 대기할 수 있다.
- 연결 수가 많다는 것과 실제 실행 중인 스레드가 많다는 것은 다르다.
Threads_connected는 연결된 세션 수이고,Threads_running은 현재 실행 또는 대기 중인 active thread에 가깝다. - thread cache는 “연결 수 제한을 늘리는 기능”이 아니라 “연결 종료 후 스레드 재사용으로 생성 비용을 줄이는 기능”이다.
- InnoDB background thread는 사용자 쿼리를 직접 실행하지 않지만, 이들이 지연되면 사용자 쿼리의 응답 시간과 쓰기 처리량이 악화된다.
3. Connection thread: 세션과 SQL 실행의 작업 단위
MySQL의 전통적인 연결 처리 모델은 “one thread per connection”에 가깝다. 클라이언트가 접속하면 MySQL은 해당 세션을 담당할 connection thread를 배정한다. 이 스레드는 클라이언트 요청을 기다리다가 SQL이 도착하면 parse, optimize, execute 경로를 수행하고 결과를 반환한다. 연결이 유지되는 동안 같은 세션 상태를 보존한다.
connection thread가 보존하는 대표적인 세션 상태는 다음과 같다.
- 현재 선택된 database
- transaction 상태와 isolation level
- session system variables
- temporary table
- prepared statement
- user variables
- character set, collation
- lock wait, metadata lock wait 상태
이 때문에 애플리케이션의 connection pool은 MySQL 입장에서 매우 중요한 부하 제어 장치가 된다. 연결을 짧게 만들고 버리는 패턴이 많으면 MySQL은 connection thread를 자주 만들고 회수해야 한다. 반대로 연결 풀 크기가 지나치게 크면 사용하지 않는 세션이 많이 남아 Threads_connected가 높아지고, 트랜잭션이 열린 채 방치되어 undo/purge와 lock 문제를 만들 수 있다.
3.1 연결 생성 비용
새 connection thread를 만드는 과정에는 다음 비용이 포함된다.
- OS thread 생성 또는 기존 thread cache에서 꺼내기
- 네트워크 연결 수락
- 인증과 권한 확인
- 세션 메모리 구조 초기화
- Performance Schema thread instrumentation 등록
- 세션 변수와 기본 상태 구성
개별 비용은 작아 보일 수 있지만, 초당 수백 또는 수천 건의 connect/disconnect가 반복되면 CPU와 mutex 경합이 커진다. 특히 짧은 요청마다 DB 연결을 새로 만드는 웹 애플리케이션은 쿼리 시간이 짧아도 MySQL 서버가 연결 처리에 시간을 소모하게 만든다.
운영 관점에서 Connections, Threads_created, Threads_cached, Threads_connected를 함께 봐야 하는 이유가 여기에 있다. Connections가 빠르게 증가하고 Threads_created도 같이 증가한다면 thread cache가 부족하거나 연결 재사용이 제대로 되지 않는 신호일 수 있다.
3.2 실행 중인 스레드와 대기 중인 스레드
SHOW PROCESSLIST에서 보이는 각 행은 클라이언트 세션 또는 내부 스레드의 현재 상태를 보여준다. Command, State, Time, Info를 보면 connection thread가 실제로 SQL을 실행 중인지, sleep 상태인지, lock을 기다리는지, 결과를 보내는지 추정할 수 있다.
SHOW FULL PROCESSLIST;
실행 결과(MySQL 8.0.46):
+----+-----------------+-----------+-----------------+---------+------+------------------------+-----------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+-----------------+---------+------+------------------------+-----------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 6 | Waiting on empty queue | NULL |
| 11 | root | localhost | mysql_tech_note | Query | 0 | init | SHOW FULL PROCESSLIST |
+----+-----------------+-----------+-----------------+---------+------+------------------------+-----------------------+
운영 중에는 다음 구분이 중요하다.
| 상태 | 의미 | 운영 해석 |
|---|---|---|
Sleep |
연결은 유지되지만 현재 요청 없음 | 연결 풀의 idle connection일 수 있다. 수가 과도하면 pool 크기와 timeout을 확인한다. |
Query |
SQL 실행 중 | State, 실행 시간, 쿼리 내용을 함께 본다. |
Locked 또는 lock 관련 state |
lock 대기 | row lock, metadata lock, table lock 가능성을 진단한다. |
Sending data |
결과 생성 또는 전송 단계 | 항상 네트워크 전송만 뜻하지 않는다. 실행 중 row 처리도 포함될 수 있다. |
Waiting for table metadata lock |
metadata lock 대기 | DDL, 장기 트랜잭션, open transaction을 확인한다. |
Threads_running이 높다는 것은 “접속자가 많다”보다 더 강한 부하 신호다. 많은 세션이 Sleep이면 연결 풀 크기 문제일 수 있지만, 많은 세션이 동시에 Query, lock wait, I/O wait 상태이면 서버의 실제 동시 작업량과 대기가 증가한 것이다.
4. Thread cache: 연결 폭증의 완충 장치
thread_cache_size는 클라이언트 연결이 종료되었을 때 connection thread를 즉시 제거하지 않고 캐시에 보관했다가 다음 연결에서 재사용할 수 있게 하는 설정이다. 이는 연결 생성과 파괴의 오버헤드를 줄이는 목적이다.
핵심 지표는 다음과 같다.
SHOW GLOBAL STATUS LIKE 'Threads_%';
SHOW GLOBAL STATUS LIKE 'Connections';
SHOW VARIABLES LIKE 'thread_cache_size';
실행 결과(MySQL 8.0.46):
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
+-------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections | 12 |
+---------------+-------+
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 8 |
+-------------------+-------+
예상 출력 형태는 다음과 같다.
Threads_cached 64
Threads_connected 180
Threads_created 1200
Threads_running 12
Connections 850000
이 값을 해석할 때는 절대값보다 증가율을 봐야 한다. MySQL이 장기간 실행된 서버에서 Threads_created가 1200이라는 숫자 자체는 높지 않을 수 있다. 반대로 짧은 시간 동안 Threads_created가 빠르게 증가하면 새 스레드 생성이 계속 발생하고 있다는 뜻이다.
4.1 Thread cache hit ratio 계산
운영자가 간단히 볼 수 있는 지표는 connection 대비 thread 생성 비율이다.
SHOW GLOBAL STATUS WHERE Variable_name IN
('Connections', 'Threads_created', 'Threads_cached', 'Threads_connected', 'Threads_running');
실행 결과(MySQL 8.0.46):
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Connections | 13 |
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
+-------------------+-------+
대략적인 계산은 다음과 같다.
thread_creation_ratio = Threads_created / Connections
이 비율이 매우 낮으면 대부분의 연결이 thread cache를 재사용하고 있다는 의미다. 비율이 높고 connect/disconnect가 많은 시스템이라면 다음을 확인한다.
- 애플리케이션 connection pool이 실제로 활성화되어 있는가?
- pool max size가 과도하게 작아 매 요청마다 새 연결을 만드는가?
- pool idle timeout이 너무 짧아 연결을 빨리 버리는가?
thread_cache_size가 workload에 비해 지나치게 작은가?- MySQL 서버가 재시작된 직후의 일시적 증가를 장기 문제로 오해하고 있지 않은가?
4.2 thread_cache_size 조정 기준
thread_cache_size는 무조건 크게 잡는 값이 아니다. 캐시된 스레드는 완전히 공짜가 아니며, 스레드 스택과 관련 구조가 메모리를 사용한다. 다만 현대 서버에서는 적정 수준의 thread cache가 연결 폭증 시 유용한 완충 장치가 된다.
일반적인 조정 절차는 다음과 같다.
- 평상시와 피크 시간대의
Connections증가율을 측정한다. - 같은 구간에서
Threads_created증가율을 측정한다. - connection pool 설정과 DB 접속 패턴을 먼저 확인한다.
- 연결 재사용이 정상인데도
Threads_created가 계속 증가하면thread_cache_size를 점진적으로 늘린다. - 조정 후
Threads_cached가 설정값 근처에서 안정되는지,Threads_created증가율이 낮아지는지 확인한다.
예시는 다음과 같다.
-- 현재 값 확인
SHOW VARIABLES LIKE 'thread_cache_size';
-- 동적 변경 가능 여부는 버전과 환경에 따라 확인한다.
SET GLOBAL thread_cache_size = 128;
실행 결과(MySQL 8.0.46):
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 8 |
+-------------------+-------+
영구 적용은 일반 MySQL에서는 설정 파일에 반영하고 재시작 또는 reload 정책에 맞춰 적용한다.
[mysqld]
thread_cache_size = 128
Aurora MySQL에서는 DB parameter group을 통해 조정한다. 동적 파라미터인지 정적 파라미터인지에 따라 즉시 적용 또는 재부팅 필요 여부가 달라진다. 또한 Aurora는 인스턴스 클래스별 메모리와 connection 한계가 다르므로, 단순히 온프레미스 MySQL의 값을 그대로 옮기기보다 CloudWatch, Performance Insights, DB parameter group의 적용 상태를 함께 확인해야 한다.
5. Background thread: 사용자가 보지 못하는 지속 작업
connection thread가 클라이언트 요청을 처리한다면, background thread는 MySQL과 InnoDB의 내부 유지 작업을 담당한다. 이들은 사용자 SQL을 직접 실행하지 않지만, 서버의 지속 성능과 복구 가능성을 유지한다.
대표적인 background thread 또는 내부 작업 범주는 다음과 같다.
- InnoDB page cleaner thread
- InnoDB purge thread
- InnoDB I/O thread
- InnoDB master thread 계열 작업
- redo log writer/flusher 관련 작업
- replication I/O thread, SQL/applier thread
- event scheduler thread
- Performance Schema, monitoring 관련 내부 thread
- shutdown, checkpoint, buffer flush 관련 작업
버전별로 thread 이름과 역할은 달라질 수 있다. MySQL 5.7, 8.0, Aurora MySQL의 내부 구현은 동일하지 않으므로 특정 thread 이름 하나에 의존하기보다 “어떤 내부 유지 작업이 지연되고 있는가”를 보는 것이 안전하다.
5.1 Page cleaner와 dirty page flush
InnoDB는 데이터를 수정할 때 buffer pool의 page를 먼저 변경하고, 해당 page는 dirty page가 된다. dirty page는 나중에 디스크 또는 스토리지 계층에 flush된다. page cleaner thread는 이 flush 작업을 조절해 checkpoint age가 과도하게 커지지 않도록 돕는다.
flush가 지연되면 다음 문제가 발생할 수 있다.
- checkpoint age 증가
- redo log 여유 감소
- 갑작스러운 aggressive flushing
- 쓰기 쿼리의 지연 증가
- crash recovery 시간 증가 가능성
일반 MySQL에서는 디스크 I/O 성능, innodb_io_capacity, innodb_io_capacity_max, redo log 크기, buffer pool dirty page 비율이 함께 영향을 준다. Aurora MySQL은 스토리지 계층이 분산되어 있고 redo/스토리지 처리 구조가 일반 MySQL과 다르므로 동일한 디스크 장치 지표만으로 flush 병목을 해석하면 부족하다. Aurora에서는 Performance Insights, CloudWatch의 write latency, commit latency, storage 관련 지표를 함께 봐야 한다.
5.2 Purge thread와 history list length
InnoDB MVCC는 트랜잭션 일관성을 위해 undo log를 사용한다. 오래된 read view가 남아 있으면 purge thread가 더 이상 필요 없는 undo record를 정리하지 못하고 history list가 길어질 수 있다. 장기 트랜잭션, 오래 열린 read-only transaction, 방치된 세션은 purge 지연의 흔한 원인이다.
운영자가 확인할 수 있는 대표 명령은 다음과 같다.
SHOW ENGINE INNODB STATUS\G
출력에서 History list length와 transaction 목록을 확인한다. MySQL 8.0에서는 Performance Schema를 통해 오래 열린 트랜잭션과 대기 이벤트를 더 구조적으로 볼 수 있다.
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
purge thread가 밀리면 단순히 내부 청소가 늦어지는 수준에서 끝나지 않는다. undo 공간 증가, buffer pool 압박, secondary index 정리 지연, DML 성능 저하로 이어질 수 있다. 따라서 “오래 열린 트랜잭션이 있는가”는 connection thread와 background thread를 함께 보는 대표적인 진단 항목이다.
5.3 Replication thread
복제 환경에서는 replication I/O thread와 SQL/applier thread도 중요한 내부 스레드다. 전통적인 replication에서는 source에서 binary log를 읽어 relay log에 저장하는 I/O thread, relay log 이벤트를 적용하는 SQL thread가 핵심이다. MySQL 8.0의 multi-threaded replication에서는 여러 worker thread가 병렬 적용을 수행할 수 있다.
진단 명령은 버전과 구성에 따라 다르지만, 기본적으로 다음을 확인한다.
SHOW REPLICA STATUS\G
-- 구버전에서는 SHOW SLAVE STATUS\G
MySQL 8.0에서는 Performance Schema replication table도 유용하다.
SELECT * FROM performance_schema.replication_connection_status\G
SELECT * FROM performance_schema.replication_applier_status_by_worker\G
Aurora MySQL에서는 read replica, cluster endpoint, replica lag 해석이 일반 MySQL 복제와 다를 수 있다. Aurora Replica는 공유 스토리지 기반 복제 특성을 가지므로 binary log 기반 외부 복제와 동일한 방식으로만 판단해서는 안 된다. 다만 외부 MySQL로의 binlog replication이나 cross-region 구성에서는 replication thread와 apply 지연을 여전히 중요하게 봐야 한다.
6. 진단에 자주 쓰는 관측 지표
스레드 모델을 운영에 활용하려면 단일 명령보다 여러 관측점을 조합해야 한다. 다음 쿼리는 현장에서 빠르게 서버의 연결/스레드 상태를 파악하는 출발점으로 사용할 수 있다.
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Connections',
'Max_used_connections',
'Threads_cached',
'Threads_connected',
'Threads_created',
'Threads_running',
'Aborted_connects',
'Connection_errors_max_connections'
);
실행 결과(MySQL 8.0.46):
+-----------------------------------+-------+
| Variable_name | Value |
+-----------------------------------+-------+
| Aborted_connects | 0 |
| Connection_errors_max_connections | 0 |
| Connections | 19 |
| Max_used_connections | 1 |
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
+-----------------------------------+-------+
max_connections와 실제 최대 사용량도 같이 확인한다.
SHOW VARIABLES WHERE Variable_name IN (
'max_connections',
'thread_cache_size',
'wait_timeout',
'interactive_timeout'
);
실행 결과(MySQL 8.0.46):
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| interactive_timeout | 28800 |
| max_connections | 30 |
| thread_cache_size | 128 |
| wait_timeout | 28800 |
+---------------------+-------+
현재 세션 상태 분포는 다음처럼 집계할 수 있다.
SELECT
COMMAND,
STATE,
COUNT(*) AS session_count,
MAX(TIME) AS max_time_seconds
FROM information_schema.PROCESSLIST
GROUP BY COMMAND, STATE
ORDER BY session_count DESC, max_time_seconds DESC;
실행 결과(MySQL 8.0.46):
+---------+------------------------+---------------+------------------+
| COMMAND | STATE | session_count | max_time_seconds |
+---------+------------------------+---------------+------------------+
| Daemon | Waiting on empty queue | 1 | 7 |
| Query | executing | 1 | 0 |
+---------+------------------------+---------------+------------------+
Performance Schema가 활성화되어 있다면 thread와 wait event를 더 자세히 볼 수 있다.
SELECT
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
t.PROCESSLIST_DB,
t.PROCESSLIST_COMMAND,
t.PROCESSLIST_TIME,
t.PROCESSLIST_STATE,
t.NAME AS thread_name,
t.TYPE AS thread_type
FROM performance_schema.threads AS t
WHERE t.PROCESSLIST_ID IS NOT NULL
ORDER BY t.PROCESSLIST_TIME DESC
LIMIT 30;
실행 결과(MySQL 8.0.46):
+----------------+------------------+------------------+-----------------+---------------------+------------------+------------------------+--------------------------------+-------------+
| PROCESSLIST_ID | PROCESSLIST_USER | PROCESSLIST_HOST | PROCESSLIST_DB | PROCESSLIST_COMMAND | PROCESSLIST_TIME | PROCESSLIST_STATE | thread_name | thread_type |
+----------------+------------------+------------------+-----------------+---------------------+------------------+------------------------+--------------------------------+-------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 7 | Waiting on empty queue | thread/sql/event_scheduler | FOREGROUND |
| 7 | NULL | NULL | NULL | Daemon | 7 | Suspending | thread/sql/compress_gtid_table | FOREGROUND |
| 22 | root | localhost | mysql_tech_note | Query | 0 | executing | thread/sql/one_connection | FOREGROUND |
+----------------+------------------+------------------+----------------
... <truncated 115 chars>
현재 대기 이벤트를 보려면 다음과 같은 형태로 접근한다.
SELECT
th.PROCESSLIST_ID,
th.PROCESSLIST_USER,
th.PROCESSLIST_HOST,
ew.EVENT_NAME,
ew.TIMER_WAIT,
th.PROCESSLIST_INFO
FROM performance_schema.events_waits_current AS ew
JOIN performance_schema.threads AS th
ON ew.THREAD_ID = th.THREAD_ID
WHERE th.PROCESSLIST_ID IS NOT NULL
ORDER BY ew.TIMER_WAIT DESC
LIMIT 20;
운영 환경에서는 Performance Schema 소비자가 꺼져 있을 수 있다. 성능 오버헤드와 관측 필요성을 고려해 필요한 consumer와 instrument를 점검해야 한다. Aurora MySQL에서는 Performance Insights가 이 정보를 더 접근하기 쉬운 형태로 제공할 수 있으므로, SQL 진단과 관리형 모니터링을 함께 사용하는 것이 좋다.
7. 대표 장애 패턴과 해석
7.1 연결 폭증으로 Threads_created가 급증하는 경우
증상은 다음과 같다.
Connections가 짧은 시간에 급격히 증가한다.Threads_created도 함께 증가한다.- 애플리케이션에서 DB 연결 시간이 증가한다.
- CPU 사용률이 상승하지만 실제 쿼리 처리량은 기대만큼 증가하지 않는다.
- error log에 connection 관련 오류가 보일 수 있다.
가능한 원인은 다음과 같다.
- 애플리케이션 connection pool 비활성화 또는 설정 오류
- pool idle timeout이 너무 짧음
- 서버리스/배치 작업이 동시에 많은 신규 연결 생성
thread_cache_size가 너무 작음max_connections근처까지 도달해 connection error 발생
이 상황에서 max_connections만 늘리는 것은 위험할 수 있다. 연결 수 한계를 늘리면 일시적으로 접속 실패는 줄어들 수 있지만, MySQL 내부 동시 실행과 메모리 사용량, lock 경합은 더 악화될 수 있다. 먼저 연결 재사용과 pool 상한을 바로잡고, 그 다음 thread cache와 connection limit을 조정해야 한다.
7.2 Threads_connected는 높지만 Threads_running은 낮은 경우
이 경우는 많은 세션이 연결만 유지하고 실제 작업은 거의 하지 않는 상태일 수 있다. 흔히 connection pool의 idle connection이 많거나, 여러 애플리케이션 인스턴스가 각자 큰 pool을 가진 경우에 발생한다.
점검할 항목은 다음과 같다.
- 애플리케이션 인스턴스 수 × pool max size가
max_connections보다 과도하지 않은가? - idle connection timeout이 운영 의도와 맞는가?
- 세션이 트랜잭션을 연 채
Sleep상태로 남아 있지 않은가? - 장애 조치 이후 오래된 연결이 정리되지 않았는가?
특히 Sleep 상태라도 transaction이 열려 있으면 purge와 lock에 영향을 줄 수 있다. 단순히 Sleep이 많다는 사실만으로 안전하다고 판단하면 안 된다.
7.3 Threads_running이 높고 CPU도 높은 경우
이 상황은 실제 active workload가 많거나, CPU-bound 쿼리 또는 latch 경합이 증가한 상태일 수 있다. 다음을 함께 확인한다.
- 상위 SQL digest
- 실행 계획 변화
- full scan 증가
- temporary table, filesort 증가
- row lock 또는 metadata lock wait
- InnoDB mutex/latch wait
- 애플리케이션 배포 후 동시 요청 증가
진단 쿼리 예시는 다음과 같다.
SELECT
DIGEST_TEXT,
COUNT_STAR,
ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000000000, 6) AS avg_seconds,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
Threads_running이 높을 때 무조건 DB 인스턴스 크기를 키우는 것보다, 어떤 SQL이 동시에 실행되고 어떤 wait가 병목인지 확인하는 것이 우선이다.
7.4 Background thread 지연으로 쓰기 성능이 흔들리는 경우
사용자 쿼리 수는 크게 늘지 않았는데 쓰기 지연이 증가한다면 background 작업 지연을 의심해야 한다. 예를 들어 dirty page flush가 밀리거나 purge가 지연되면 개별 DML이 더 많은 내부 비용을 부담하게 된다.
점검 항목은 다음과 같다.
SHOW ENGINE INNODB STATUS\G의 checkpoint, history list, pending I/O- redo log 관련 지표
- dirty page 비율
- 장기 트랜잭션 존재 여부
- 스토리지 latency
- Aurora의 경우 Performance Insights wait와 CloudWatch 지표
이 문제는 보통 한 가지 설정으로만 해결되지 않는다. 장기 트랜잭션 제거, write workload 완화, 인덱스/쿼리 개선, redo log와 flush 관련 파라미터 조정, 스토리지 성능 확인을 함께 진행해야 한다.
8. Aurora MySQL에서의 차이와 주의점
Aurora MySQL은 MySQL 호환 SQL layer를 제공하지만, 스토리지 구조와 복제/장애 조치 모델이 일반 MySQL과 다르다. 따라서 스레드 지표 해석도 일부 달라진다.
첫째, Aurora는 분산 스토리지 계층을 사용한다. 일반 MySQL에서 로컬 디스크 flush와 redo log 파일 쓰기로 해석하던 일부 현상은 Aurora에서는 스토리지 서비스와의 통신, quorum 기반 쓰기, cluster volume 상태와 연결된다. 사용자는 여전히 connection thread와 InnoDB 내부 작업을 보지만, 병목의 위치가 OS 디스크 장치가 아닐 수 있다.
둘째, Aurora Replica는 일반적인 MySQL asynchronous replica와 동일하지 않다. cluster volume을 공유하는 Aurora Replica의 지연은 binlog relay/apply thread만으로 설명되지 않는다. 반면 외부 복제나 binlog 기반 replication을 구성한 경우에는 MySQL replication thread 해석이 여전히 필요하다.
셋째, 파라미터 적용 방식이 다르다. thread_cache_size, max_connections, timeout, InnoDB 관련 설정은 DB parameter group 또는 cluster parameter group의 영향을 받는다. 동적 적용 가능 여부, pending reboot 상태, 인스턴스별 적용 상태를 확인해야 한다.
넷째, Performance Insights를 적극 활용해야 한다. Aurora에서는 SQL별 DB load, wait event, host 지표, CloudWatch 지표가 함께 제공된다. Threads_running이 높을 때 Performance Insights에서 CPU wait인지, lock wait인지, storage 관련 wait인지 확인하면 원인 범위를 빠르게 좁힐 수 있다.
9. 운영 체크리스트
9.1 평상시 기준선 수집
-
Threads_connected,Threads_running,Threads_created,Threads_cached - 피크 시간대의
Connections -
Max_used_connections가max_connections - 장기 트랜잭션과
Sleep
9.2 연결 폭증 대응
-
Connections와Threads_created -
thread_cache_size -
max_connections - 접속 실패가 있었다면
Aborted_connects,Connection_errors_max_connections
9.3 Background thread 지연 대응
-
SHOW ENGINE INNODB STATUS\G
9.4 장애 중 즉시 확인 순서
-
SHOW FULL PROCESSLIST -
SHOW GLOBAL STATUS LIKE 'Threads_%'
10. 흔한 오해와 주의사항
첫째, thread_cache_size는 동시 실행 성능을 직접 높이는 설정이 아니다. 이 값은 스레드 생성 비용을 줄이는 캐시다. 느린 쿼리, lock wait, I/O 병목을 해결하지 못한다.
둘째, Threads_connected가 높다고 항상 장애는 아니다. idle connection이 많을 수 있다. 하지만 idle connection이 열린 트랜잭션을 보유하고 있으면 purge와 lock에 악영향을 준다.
셋째, Threads_running이 낮다고 항상 정상은 아니다. background thread 지연, replication lag, checkpoint 문제처럼 사용자 thread 수와 별도로 진행되는 내부 문제가 있을 수 있다.
넷째, max_connections를 크게 늘리는 것은 마지막 수단에 가깝다. 연결 수가 많아지면 세션 메모리, thread stack, temporary table, sort buffer, join buffer 같은 메모리 사용 가능성이 함께 증가한다. 실제 메모리는 세션별 workload에 따라 달라지므로 단순 곱셈만으로 정확히 계산할 수는 없지만, 상한을 크게 여는 것은 장애 반경을 키울 수 있다.
다섯째, Aurora MySQL에서 일반 MySQL의 디스크 I/O 해석을 그대로 적용하면 오판할 수 있다. Aurora는 스토리지 계층이 분리되어 있으므로 DB load, wait event, cluster/instance 지표를 같이 봐야 한다.
여섯째, Performance Schema는 강력하지만 설정 상태에 따라 보이는 정보가 달라진다. 필요한 instrument가 꺼져 있으면 쿼리는 정상 실행되어도 관측 결과가 비어 있을 수 있다.
11. 결론
MySQL의 프로세스와 스레드 모델은 성능 문제를 해석하는 기본 언어다. connection thread는 세션과 SQL 실행을 담당하고, thread cache는 연결 생성 비용을 줄이며, background thread는 InnoDB와 복제, 로그, 정리 작업을 통해 서버의 지속 가능성을 유지한다. 운영자는 이 세 영역을 분리해서 보되, 장애 분석에서는 함께 연결해야 한다.
좋은 운영 기준은 단순하다. 연결 수와 실행 중인 스레드 수를 구분하고, thread cache를 연결 재사용의 보조 장치로 이해하며, background 작업 지연이 사용자 쿼리 지연으로 이어질 수 있음을 기억하는 것이다. 다음 주제들에서는 이러한 스레드 모델 위에서 MySQL의 메모리 구조, InnoDB buffer pool, redo/undo 로그, transaction과 lock이 어떻게 협력하는지 더 구체적으로 다룰 수 있다.