MySQL 요청 처리 흐름: 클라이언트 연결부터 결과 반환까지
MySQL에서 클라이언트 연결, 인증, SQL 파싱, 최적화, 실행, 결과 반환까지의 내부 요청 처리 흐름을 운영 관점에서 정리한다.
MySQL 요청 처리 흐름: 클라이언트 연결부터 결과 반환까지
MySQL 운영에서 “쿼리가 느리다”는 증상은 단일 원인으로 설명되지 않는다. 클라이언트가 서버에 접속하는 순간부터 결과를 돌려받기까지는 네트워크 연결, 인증, 세션 생성, SQL 파싱, 권한 확인, optimizer의 실행 계획 선택, storage engine 호출, 잠금 대기, redo/undo 처리, 결과 전송이라는 여러 단계가 이어진다. 어느 단계에서 병목이 발생했는지 구분하지 못하면 인덱스만 추가하거나 파라미터만 조정하는 식의 단편적인 대응으로 흐르기 쉽다.
이 글은 MySQL 요청 처리 경로를 서버 내부 구조와 운영 진단 관점에서 정리한다. 목표는 개별 SQL 튜닝 전에 “요청이 서버 안에서 어떤 경로를 지나가는가”를 머릿속에 그릴 수 있게 하는 것이다. 이후 InnoDB, optimizer, locking, replication, observability 주제를 이해할 때도 이 흐름은 기본 지도가 된다.
1. 전체 흐름 요약
MySQL 서버가 하나의 SQL 요청을 처리하는 흐름은 대략 다음과 같다.
Client
-> TCP/Unix socket 연결
-> handshake 및 인증
-> session/thread 할당
-> SQL 수신
-> parser: 문법 분석 및 parse tree 생성
-> resolver/preprocessor: 객체명, 컬럼, 권한, 타입 확인
-> optimizer: 실행 계획 선택
-> executor: plan 실행
-> storage engine API 호출(InnoDB 등)
-> lock/latch/IO/log 처리
-> row 또는 OK/error packet 생성
-> client로 결과 반환
운영자는 이 흐름을 크게 네 영역으로 나누어 볼 수 있다.
| 영역 | 주요 구성 요소 | 대표 증상 |
|---|---|---|
| 연결/세션 | listener, connection thread, authentication, thread cache | 연결 지연, Too many connections, 인증 실패 |
| SQL 계층 | parser, resolver, optimizer, executor | CPU 증가, 실행 계획 변화, 임시 테이블, 정렬 비용 |
| Storage engine 계층 | InnoDB buffer pool, B-tree, transaction, lock, redo/undo | row lock wait, IO wait, purge 지연, checkpoint 압박 |
| 반환/관측 | result packet, network write, Performance Schema, slow log | 결과 전송 지연, 대량 fetch, 네트워크 병목 |
MySQL의 특징은 SQL 계층과 storage engine 계층이 분리되어 있다는 점이다. SQL 계층은 문법, 권한, optimizer, executor를 담당하고, 실제 데이터 접근과 트랜잭션 처리는 InnoDB 같은 storage engine이 담당한다. 따라서 SELECT 하나도 optimizer 관점의 문제와 InnoDB 관점의 문제를 분리해 보아야 한다.
2. 연결 수립: socket, handshake, 인증
클라이언트가 MySQL에 접속하면 서버는 먼저 TCP socket 또는 Unix domain socket 연결을 수락한다. 이후 MySQL protocol handshake를 통해 서버 버전, connection id, 인증 플러그인, capability flag 등을 교환한다. 클라이언트는 계정 정보와 인증 응답을 보내고, 서버는 mysql.user 계정 정보와 host matching 규칙을 기준으로 인증을 수행한다.
이 단계의 운영상 핵심은 연결 비용과 동시 연결 수다. 애플리케이션이 connection pool을 사용하지 않고 요청마다 새 연결을 만들면 handshake, TLS, 인증, thread 준비 비용이 반복된다. 짧은 SQL만 실행하는 서비스에서는 실제 쿼리 실행보다 연결 생성 비용이 더 크게 보일 수 있다.
확인할 대표 변수는 다음과 같다.
SHOW GLOBAL STATUS LIKE 'Connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Aborted_connects';
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'thread_cache_size';
SHOW GLOBAL STATUS LIKE 'Threads_created';
실행 결과(MySQL 8.0.46):
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections | 11 |
+---------------+-------+
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 1 |
+-------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_running | 2 |
+-----------------+-------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Aborted_connects | 0 |
+------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 30 |
+-----------------+-------+
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 8 |
+-------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_created | 1 |
+-----------------+-------+
해석 기준은 다음과 같다.
Connections가 초당 빠르게 증가하면 애플리케이션 pool 재사용 여부를 먼저 확인한다.Threads_connected는 현재 열린 세션 수이고,Threads_running은 실제 실행 중인 thread 수에 가깝다. 연결은 많지만 running이 낮다면 idle connection이 많을 수 있다.Threads_created가 지속 증가하면 thread cache가 부족하거나 연결 churn이 심할 수 있다.Aborted_connects증가는 인증 실패, 네트워크 끊김, 잘못된 client 설정, host matching 문제를 의심한다.
MySQL Community Server에서는 전통적으로 connection당 thread 모델이 기본이다. 즉, 각 클라이언트 세션은 서버 thread 하나에 연결되어 요청을 처리한다. thread cache가 있더라도 동시 실행이 많아지면 context switching과 memory 사용량이 증가한다. Aurora MySQL도 클라이언트 접속과 세션 관리는 MySQL 호환 계층을 통해 이루어지지만, proxy 계층, failover, endpoint 동작, 보안 그룹, TLS 설정 등 AWS 네트워크 구성의 영향도 함께 받는다.
3. 세션 상태와 요청 수신
인증이 완료되면 MySQL은 세션을 만든다. 세션에는 character set, collation, autocommit, transaction isolation level, SQL mode, time zone, temporary table, prepared statement, user variables 같은 상태가 포함된다. 같은 SQL이라도 세션 상태가 다르면 동작이 달라질 수 있다.
예를 들어 autocommit=0인 세션에서 단순 조회 후 커밋하지 않고 방치하면 InnoDB의 read view가 오래 유지되어 purge가 지연될 수 있다. sql_mode가 다르면 잘못된 날짜나 truncation 처리도 달라진다. 운영 중 “특정 애플리케이션 인스턴스에서만 재현되는 문제”는 SQL 텍스트보다 세션 상태 차이가 원인일 때가 있다.
현재 세션 상태는 다음처럼 확인할 수 있다.
SELECT
@@autocommit AS autocommit,
@@transaction_isolation AS tx_isolation,
@@sql_mode AS sql_mode,
@@character_set_client AS charset_client,
@@collation_connection AS collation_connection,
@@time_zone AS time_zone;
실행 결과(MySQL 8.0.46):
+------------+-----------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+-----------+
| autocommit | tx_isolation | sql_mode | charset_client | collation_connection | time_zone |
+------------+-----------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+-----------+
| 1 | REPEATABLE-READ | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION | latin1 | latin1_swedish_ci | SYSTEM |
+------------+-----------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+-----------+
연결 pool을 사용하는 애플리케이션에서는 세션 재사용이 장점이지만, 반대로 이전 요청의 세션 상태가 다음 요청에 남는 문제가 생길 수 있다. 따라서 pool 반환 시 세션 초기화 정책이 중요하다. MySQL Connector/J, HikariCP, ProxySQL, RDS Proxy 등을 사용할 때는 connection reset, session variable 초기화, transaction 정리 동작을 확인해야 한다.
4. Parser와 resolver: SQL을 실행 가능한 형태로 바꾸는 단계
클라이언트가 SQL packet을 보내면 MySQL SQL 계층은 먼저 parser를 통해 문법을 분석한다. parser는 SQL 문자열을 token으로 나누고 parse tree를 만든다. 이 단계에서 문법 오류가 있으면 storage engine까지 가지 않고 즉시 error packet이 반환된다.
문법 분석 후에는 resolver 또는 preprocessor 단계에서 다음 작업이 수행된다.
- database, table, column 이름을 실제 객체로 해석한다.
- alias, wildcard, function, expression을 정리한다.
- 사용자 권한을 확인한다.
- view, stored routine, trigger, generated column 등 관련 객체를 확인한다.
- 데이터 타입과 collation 변환 가능성을 검토한다.
이 단계의 비용은 일반적으로 실행 비용보다 작지만, 매우 복잡한 SQL, 다수의 view 중첩, 많은 partition, 거대한 IN (...) list, 동적 SQL 남발에서는 무시하기 어렵다. 또한 metadata lock과도 연결된다. SQL이 테이블 정의를 참조하는 동안 DDL은 metadata lock을 기다릴 수 있고, 반대로 진행 중인 DDL 때문에 일반 SQL이 대기할 수도 있다.
metadata lock 대기는 다음과 같이 확인한다.
SELECT
OBJECT_TYPE,
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_DURATION,
LOCK_STATUS,
OWNER_THREAD_ID
FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
ORDER BY OBJECT_SCHEMA, OBJECT_NAME, LOCK_STATUS;
Waiting for table metadata lock 상태가 보이면 단순히 쿼리가 느린 것이 아니라 DDL, 장기 트랜잭션, 잠금 순서 문제를 함께 보아야 한다.
5. Optimizer: 가능한 경로 중 실행 계획 선택
resolver 이후 MySQL optimizer는 SQL을 실행할 방법을 선택한다. optimizer는 어떤 테이블부터 읽을지, 어떤 index를 사용할지, join 순서를 어떻게 할지, range scan을 할지 full scan을 할지, filesort나 temporary table이 필요한지 등을 결정한다.
optimizer의 결정은 통계 정보와 비용 모델에 의존한다. InnoDB 테이블 통계, index cardinality, histogram, 조건식의 선택도, row estimate, join buffer 비용 등이 영향을 준다. 따라서 데이터 분포가 바뀌거나 통계가 오래되면 같은 SQL의 실행 계획이 바뀔 수 있다.
기본 진단은 EXPLAIN과 EXPLAIN ANALYZE다.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
status VARCHAR(20) NOT NULL,
KEY idx_orders_status_created (status, created_at),
KEY idx_orders_created_at (created_at),
CONSTRAINT fk_orders_customers_flow
FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB;
INSERT INTO customers (id, email) VALUES
(100, 'kim@example.com'),
(101, 'lee@example.com');
INSERT INTO orders (id, customer_id, created_at, status) VALUES
(1, 100, '2026-05-13 09:00:00', 'PAID'),
(2, 100, '2026-05-13 10:00:00', 'READY'),
(3, 101, '2026-05-14 11:00:00', 'PAID');
EXPLAIN FORMAT=TREE
SELECT o.id, o.created_at, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-05-01'
AND o.status = 'PAID'
ORDER BY o.created_at DESC
LIMIT 50;
EXPLAIN ANALYZE
SELECT o.id, o.created_at, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-05-01'
AND o.status = 'PAID'
ORDER BY o.created_at DESC
LIMIT 50;
EXPLAIN은 예상 계획이고, EXPLAIN ANALYZE는 실제 실행하면서 측정한 row 수와 시간 정보를 보여준다. 운영 환경에서 EXPLAIN ANALYZE는 실제 SQL을 실행하므로, 변경 SQL이나 무거운 조회에는 주의해서 사용해야 한다.
optimizer 단계에서 흔한 오해는 “index가 있으면 반드시 사용된다”는 것이다. MySQL은 index를 사용할 수 있어도 비용이 더 높다고 판단하면 full table scan을 선택할 수 있다. 작은 테이블, 낮은 선택도, 잘못된 통계, 함수로 감싼 컬럼, collation 변환, 암묵적 타입 변환은 index 사용을 방해하거나 비효율적으로 만든다.
-- 인덱스를 방해할 수 있는 예: 컬럼에 함수 적용
SELECT * FROM orders WHERE DATE(created_at) = '2026-05-13';
-- 범위 조건으로 바꾸면 인덱스 사용 가능성이 높아진다
SELECT *
FROM orders
WHERE created_at >= '2026-05-13 00:00:00'
AND created_at < '2026-05-14 00:00:00';
실행 결과(MySQL 8.0.46):
+----+-------------+---------------------+--------+
| id | customer_id | created_at | status |
+----+-------------+---------------------+--------+
| 1 | 100 | 2026-05-13 09:00:00 | PAID |
| 2 | 100 | 2026-05-13 10:00:00 | READY |
+----+-------------+---------------------+--------+
+----+-------------+---------------------+--------+
| id | customer_id | created_at | status |
+----+-------------+---------------------+--------+
| 1 | 100 | 2026-05-13 09:00:00 | PAID |
| 2 | 100 | 2026-05-13 10:00:00 | READY |
+----+-------------+---------------------+--------+
Aurora MySQL에서도 optimizer 계층은 MySQL 호환 동작을 따른다. 다만 storage 계층이 분산 저장 구조이므로 물리 IO 지연, buffer cache warm-up, replica lag, failover 직후 cache 상태 같은 운영 요소가 체감 성능에 영향을 줄 수 있다. 실행 계획 문제와 Aurora storage/replica 상태 문제를 분리해서 보는 습관이 필요하다.
6. Executor와 storage engine 호출
optimizer가 계획을 선택하면 executor는 그 계획을 실제로 수행한다. executor는 SQL 계층에 있으며, row를 가져오거나 수정하기 위해 storage engine handler API를 호출한다. InnoDB는 이 요청을 받아 clustered index, secondary index, buffer pool, transaction, lock manager, redo/undo subsystem을 사용한다.
예를 들어 secondary index로 orders(status, created_at)를 검색한 뒤 SELECT *를 수행하면 흐름은 다음과 비슷하다.
- executor가 InnoDB에 secondary index range scan을 요청한다.
- InnoDB는 buffer pool에서 index page를 찾고, 없으면 storage에서 읽어온다.
- secondary index leaf에서 primary key 값을 찾는다.
- 필요한 컬럼이 secondary index에 없으면 clustered index로 다시 접근한다.
- transaction isolation에 맞게 visibility를 확인한다.
- row lock이 필요하면 lock manager를 통해 잠금을 잡거나 대기한다.
- executor는 row를 받아 WHERE, projection, join, sort, aggregation 처리를 이어간다.
이 흐름에서 비용은 단순히 “읽은 row 수”만이 아니다. buffer pool hit 여부, random IO, latch 경합, undo record 접근, row lock wait, temporary table spill, network 전송량이 모두 관여한다.
스토리지 계층의 기본 상태는 다음 쿼리로 확인할 수 있다.
SHOW ENGINE INNODB STATUS\G
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
SHOW GLOBAL STATUS LIKE 'Innodb_rows_%';
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
Performance Schema를 사용하면 현재 실행 중인 statement와 wait event를 더 구조적으로 볼 수 있다.
SELECT
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
t.PROCESSLIST_DB,
t.PROCESSLIST_STATE,
es.EVENT_NAME,
es.TIMER_WAIT / 1000000000000 AS seconds,
LEFT(es.SQL_TEXT, 200) AS sql_text
FROM performance_schema.threads t
JOIN performance_schema.events_statements_current es
ON es.THREAD_ID = t.THREAD_ID
WHERE t.PROCESSLIST_ID IS NOT NULL
ORDER BY seconds DESC;
실행 결과(MySQL 8.0.46):
+----------------+------------------+------------------+-----------------+-------------------+----------------------+---------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PROCESSLIST_ID | PROCESSLIST_USER | PROCESSLIST_HOST | PROCESSLIST_DB | PROCESSLIST_STATE | EVENT_NAME | seconds | sql_text |
+----------------+------------------+------------------+-----------------+-------------------+----------------------+---------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 17 | root | localhost | mysql_tech_note | executing | statement/sql/select | 0.0039 | SELECT
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
t.PROC
... <truncated 454 chars>
대기 이벤트까지 보려면 다음과 같이 현재 wait를 조회한다.
SELECT
t.PROCESSLIST_ID,
ew.EVENT_NAME,
ew.OBJECT_SCHEMA,
ew.OBJECT_NAME,
ew.INDEX_NAME,
ew.TIMER_WAIT / 1000000000000 AS seconds_waited
FROM performance_schema.events_waits_current ew
JOIN performance_schema.threads t ON t.THREAD_ID = ew.THREAD_ID
WHERE t.PROCESSLIST_ID IS NOT NULL
ORDER BY seconds_waited DESC;
7. 트랜잭션, 잠금, 로그가 요청 처리에 끼치는 영향
SELECT와 UPDATE는 executor 관점에서는 모두 plan 실행이지만, InnoDB 내부에서는 트랜잭션과 잠금 처리의 차이가 크다. UPDATE, DELETE, INSERT는 undo log를 만들고 redo log를 기록하며, 필요한 row lock을 획득한다. SELECT ... FOR UPDATE 역시 읽기처럼 보이지만 row lock을 잡는 locking read다.
autocommit=1이면 각 statement가 하나의 트랜잭션으로 처리된다. 반대로 명시적 트랜잭션에서는 여러 statement가 같은 transaction context 안에서 실행되고, commit 또는 rollback 전까지 잠금과 read view가 유지될 수 있다.
잠금 대기는 요청 처리 흐름을 멈추게 하는 대표적인 원인이다. 다음 쿼리는 MySQL 8.0에서 Performance Schema를 이용해 InnoDB transaction과 lock wait를 보는 출발점이다.
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r
ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
JOIN information_schema.innodb_trx b
ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID;
잠긴 객체와 인덱스까지 함께 보려면 performance_schema.data_locks, performance_schema.data_lock_waits를 조인한다.
SELECT
dlw.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
dlw.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,
dl.OBJECT_SCHEMA,
dl.OBJECT_NAME,
dl.INDEX_NAME,
dl.LOCK_TYPE,
dl.LOCK_MODE,
dl.LOCK_STATUS
FROM performance_schema.data_lock_waits dlw
JOIN performance_schema.data_locks dl
ON dl.ENGINE_LOCK_ID = dlw.REQUESTING_ENGINE_LOCK_ID;
로그 처리도 중요하다. commit 시점에 redo log flush 정책(innodb_flush_log_at_trx_commit)과 binary log sync 정책(sync_binlog)은 지연 시간과 내구성의 균형을 결정한다. Aurora MySQL은 redo log를 로컬 디스크에 전통적으로 기록하는 구조와 다르게 분산 storage service에 로그 레코드를 전송하는 아키텍처를 사용한다. 운영자는 Aurora에서 commit latency가 증가할 때 DB instance CPU만 볼 것이 아니라 storage latency, replica 상태, failover 직후 상태, CloudWatch 지표와 Performance Insights wait를 함께 확인해야 한다.
8. 결과 생성과 클라이언트 반환
executor가 row를 만들면 MySQL server는 이를 protocol packet으로 인코딩해 client에 반환한다. 결과가 작으면 이 단계는 거의 눈에 띄지 않는다. 하지만 대량 결과를 애플리케이션이 천천히 fetch하거나 네트워크가 느리면 서버 thread는 결과 전송 때문에 오래 점유될 수 있다.
운영에서 흔한 패턴은 다음과 같다.
- SQL 실행 자체는 빠른데 client fetch가 느려 전체 응답 시간이 길다.
SELECT *와 큰TEXT/BLOB컬럼 때문에 network payload가 커진다.- 애플리케이션이 pagination 없이 수십만 row를 가져온다.
mysqldump, 배치 조회, 리포트 쿼리가 운영 트래픽과 같은 네트워크/CPU를 사용한다.
대량 결과 전송 문제는 DB 내부 wait보다 애플리케이션 관측 지표에서 먼저 보일 수 있다. MySQL 쪽에서는 Bytes_sent, Bytes_received, Threads_running, statement digest별 rows sent 등을 함께 본다.
SHOW GLOBAL STATUS LIKE 'Bytes_sent';
SHOW GLOBAL STATUS LIKE 'Bytes_received';
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_ROWS_SENT,
SUM_ROWS_EXAMINED,
ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000000000, 4) AS avg_seconds
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_ROWS_SENT DESC
LIMIT 10;
Rows_examined가 높고 Rows_sent가 낮으면 필터링 효율이나 인덱스를 의심한다. Rows_sent 자체가 매우 높으면 애플리케이션이 필요한 만큼만 가져오는지, keyset pagination이 가능한지, 요약 테이블이나 비동기 리포트로 분리할 수 있는지 검토한다.
9. 요청 흐름을 기준으로 한 장애 해석
MySQL 장애 대응에서는 증상을 단계별로 분해해야 한다.
연결 단계 문제
대표 증상은 Too many connections, 접속 timeout, 인증 실패, connection 폭증이다. 이때는 SQL 튜닝보다 connection pool, max_connections, thread cache, 네트워크, 계정 host pattern을 먼저 본다.
SHOW PROCESSLIST;
SHOW GLOBAL STATUS WHERE Variable_name IN
('Connections','Threads_connected','Threads_running','Aborted_connects','Max_used_connections');
실행 결과(MySQL 8.0.46):
+----+-----------------+-----------+-----------------+---------+------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+-----------------+---------+------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 6 | Waiting on empty queue | NULL |
| 22 | root | localhost | mysql_tech_note | Query | 0 | init | SHOW PROCESSLIST |
+----+-----------------+-----------+-----------------+---------+------+------------------------+------------------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Aborted_connects | 0 |
| Connections | 22 |
| Max_used_connections | 1 |
| Threads_connected | 1 |
| Threads_running | 2 |
+----------------------+-------+
Max_used_connections가 max_connections에 근접했다면 단순히 값을 올리기 전에 idle connection, pool 크기, 애플리케이션 인스턴스 수, transaction 누수를 확인해야 한다. 값을 올리면 일시적으로 접속 실패는 줄어도 DB가 더 많은 동시 작업을 감당하지 못해 전체 지연이 악화될 수 있다.
Optimizer/실행 계획 문제
대표 증상은 특정 SQL만 갑자기 느려지는 것이다. 통계 변경, 데이터 분포 변화, 배포로 인한 조건식 변경, index 추가/삭제, MySQL 버전 변경이 원인이 될 수 있다.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
created_at DATETIME NOT NULL,
status VARCHAR(20) NOT NULL,
KEY idx_orders_status_created (status, created_at)
) ENGINE=InnoDB;
INSERT INTO orders (id, customer_id, created_at, status) VALUES
(1, 100, '2026-05-13 09:00:00', 'PAID'),
(2, 100, '2026-05-14 10:00:00', 'READY');
EXPLAIN FORMAT=JSON
SELECT id, customer_id, created_at
FROM orders
WHERE status = 'PAID'
ORDER BY created_at DESC
LIMIT 10;
SHOW INDEX FROM orders;
ANALYZE TABLE orders;
ANALYZE TABLE은 통계를 갱신하지만 운영 중에는 metadata lock과 IO 영향을 고려해야 한다. 계획 고정을 위해 optimizer hint를 사용할 수도 있으나, 장기적으로는 적절한 인덱스와 안정적인 쿼리 형태가 우선이다.
InnoDB 잠금/트랜잭션 문제
대표 증상은 Waiting for row lock, deadlock, purge 지연, history list 증가다. 이 경우 느린 SQL 하나만 볼 것이 아니라 blocking transaction을 찾아야 한다.
SELECT
trx_id,
trx_state,
trx_started,
trx_mysql_thread_id,
trx_rows_locked,
trx_rows_modified,
LEFT(trx_query, 200) AS trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
장기 트랜잭션은 row lock뿐 아니라 undo 정리와 MVCC에도 영향을 준다. 특히 read-only로 보이는 세션도 REPEATABLE READ에서 read view를 오래 유지하면 purge를 지연시킬 수 있다.
IO와 buffer pool 문제
대표 증상은 전반적인 쿼리 지연, checkpoint 압박, buffer pool miss 증가다. InnoDB는 가능한 한 buffer pool에서 page를 읽지만, working set이 buffer pool보다 크거나 갑작스러운 full scan이 반복되면 디스크 또는 Aurora storage 접근이 증가한다.
SELECT
VARIABLE_NAME,
VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN (
'Innodb_buffer_pool_read_requests',
'Innodb_buffer_pool_reads',
'Innodb_data_reads',
'Innodb_data_writes',
'Innodb_log_waits'
);
실행 결과(MySQL 8.0.46):
+----------------------------------+----------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+----------------------------------+----------------+
| Innodb_buffer_pool_read_requests | 20236 |
| Innodb_buffer_pool_reads | 997 |
| Innodb_data_reads | 1021 |
| Innodb_data_writes | 599 |
| Innodb_log_waits | 0 |
+----------------------------------+----------------+
buffer pool hit ratio만으로 결론을 내리면 위험하다. 짧은 시간 구간의 지표, SQL digest, read/write latency, storage wait를 함께 보아야 한다.
10. Aurora MySQL에서의 운영 차이
Aurora MySQL은 MySQL protocol과 상당한 호환성을 제공하지만, 요청 처리 흐름의 하단부, 특히 storage와 고가용성 영역에서 전통적인 단일 서버 MySQL과 다르다.
운영 관점의 차이는 다음과 같다.
- Compute instance와 distributed storage가 분리되어 있다.
- redo log 처리와 storage replication 방식이 Aurora 고유 구조를 따른다.
- reader instance는 같은 cluster volume을 읽지만 각 instance의 buffer cache 상태는 다를 수 있다.
- failover 후 writer endpoint가 새 instance를 가리키며, connection 재수립과 DNS/cache 동작이 중요하다.
- Performance Insights와 CloudWatch 지표가 병목 위치를 파악하는 데 중요한 보조 자료가 된다.
따라서 Aurora에서 “쿼리가 느리다”는 보고를 받으면 다음을 함께 확인한다.
1. SQL plan이 바뀌었는가?
2. writer와 reader 중 어느 endpoint에서 발생하는가?
3. 특정 reader만 느린가, cluster 전체가 느린가?
4. failover, scale event, parameter 변경 직후인가?
5. Performance Insights의 top wait event는 CPU, lock, IO, network 중 어디에 가까운가?
6. 애플리케이션 connection pool이 failover 후 정상적으로 재연결했는가?
Aurora라고 해서 SQL 계층의 기본 원리가 달라지는 것은 아니다. parser, optimizer, executor, InnoDB 호환 계층의 이해는 그대로 필요하다. 다만 storage latency와 failover, endpoint, replica 관측을 MySQL 내부 진단과 함께 해석해야 한다.
11. 실무 진단 순서
요청 처리 흐름을 기준으로 한 기본 진단 순서는 다음과 같다.
1. 접속 가능한가?
- health check, connection timeout, 인증 실패, max_connections 확인
2. 연결은 되지만 전체가 느린가?
- Threads_running, CPU, IO, wait event, DB instance 지표 확인
3. 특정 SQL만 느린가?
- slow log, Performance Schema digest, EXPLAIN, rows examined/sent 확인
4. 대기 중인가 실행 중인가?
- processlist state, metadata lock, row lock, wait event 확인
5. 실행 계획 문제인가 storage 문제인가?
- EXPLAIN ANALYZE, buffer pool, IO, temporary table, sort 지표 확인
6. 결과 전송 문제인가?
- Rows_sent, Bytes_sent, client fetch 방식, pagination 확인
7. 최근 변경이 있었는가?
- 배포, schema 변경, index 변경, 통계 갱신, parameter 변경, failover 확인
현장에서 바로 사용할 수 있는 최소 점검 SQL은 다음과 같다.
-- 현재 세션/쿼리 상태
SHOW FULL PROCESSLIST;
-- 전체 연결과 실행 thread
SHOW GLOBAL STATUS WHERE Variable_name IN
('Threads_connected','Threads_running','Connections','Max_used_connections','Aborted_connects');
-- statement digest 상위 쿼리
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT,
ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_sec,
ROUND(AVG_TIMER_WAIT / 1000000000000, 4) AS avg_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 오래 열린 트랜잭션
SELECT
trx_id,
trx_state,
trx_started,
trx_mysql_thread_id,
trx_rows_locked,
trx_rows_modified,
LEFT(trx_query, 200) AS query_sample
FROM information_schema.innodb_trx
ORDER BY trx_started
LIMIT 20;
12. 흔한 오해와 주의 사항
첫째, SHOW PROCESSLIST만으로는 병목 원인을 확정할 수 없다. State는 힌트일 뿐이고, 순간 스냅샷이다. 짧은 지연이 반복되는 문제는 Performance Schema summary, slow log, 애플리케이션 tracing을 함께 보아야 한다.
둘째, Threads_connected가 높다고 항상 나쁜 것은 아니다. connection pool이 충분히 재사용되고 있고 Threads_running이 낮다면 idle connection일 수 있다. 반대로 connected는 낮아도 running이 CPU core 수를 지속적으로 크게 초과하면 active workload가 과도할 수 있다.
셋째, EXPLAIN 결과가 좋아 보여도 실제 실행은 느릴 수 있다. row estimate가 틀렸거나, lock wait가 있거나, buffer pool miss가 많거나, result set이 너무 클 수 있다. 가능하면 안전한 환경에서 EXPLAIN ANALYZE와 실제 관측 지표를 비교한다.
넷째, 모든 문제를 파라미터로 해결하려고 해서는 안 된다. max_connections, buffer pool, log 관련 설정은 중요하지만, 잘못된 쿼리 패턴과 transaction 설계를 가릴 수는 없다. 요청 처리 흐름의 어느 단계가 병목인지 먼저 특정한 뒤 조정해야 한다.
다섯째, 운영 진단 SQL 자체도 부하가 될 수 있다. 매우 큰 Performance Schema summary나 metadata lock 조회를 과도하게 반복하지 말고, 필요한 컬럼과 조건을 제한한다. 장애 중에는 진단이 추가 부하를 만들지 않도록 주의한다.
13. 운영 체크리스트
MySQL 요청 처리 흐름을 기준으로 다음 항목을 정기적으로 점검한다.
-
max_connections -
Threads_connected,Threads_running,Connections,Threads_created - 주요 SQL은
EXPLAIN -
Rows_examined대비Rows_sent
결론
MySQL 요청은 단순히 SQL 문자열이 storage에서 결과를 읽어오는 과정이 아니다. 연결과 인증, 세션 상태, SQL 계층의 분석과 최적화, InnoDB의 page 접근과 트랜잭션 처리, 결과 packet 전송이 이어지는 복합 경로다. 운영자는 이 경로를 기준으로 증상을 분해해야 한다. 연결 문제인지, 실행 계획 문제인지, 잠금 대기인지, IO 문제인지, 결과 전송 문제인지 구분할 수 있어야 튜닝과 장애 대응이 정확해진다.
다음 주제들에서는 이 요청 처리 흐름을 더 세분화해 MySQL 서버 프로세스와 thread 모델, InnoDB buffer pool, redo/undo, optimizer와 index 선택, transaction isolation과 locking을 각각 깊게 다룰 수 있다. 오늘의 핵심은 모든 진단을 “요청이 지금 어느 단계에서 시간을 쓰고 있는가”라는 질문에서 시작하는 것이다.