MySQL 서버 아키텍처: SQL 계층, 스토리지 엔진, 플러그인 구조의 역할 분리
MySQL 서버가 SQL 계층과 스토리지 엔진을 분리해 요청을 처리하는 구조와 운영상 의미를 정리한다.
MySQL 서버 아키텍처: SQL 계층, 스토리지 엔진, 플러그인 구조의 역할 분리
MySQL을 운영하다 보면 같은 SQL이라도 어떤 테이블에서는 빠르게 끝나고, 어떤 테이블에서는 예측하기 어려운 잠금 대기나 디스크 I/O를 만든다. SELECT, INSERT, UPDATE라는 문법만 보면 단순해 보이지만, 실제 서버 내부에서는 연결 관리, 파싱, 권한 확인, 최적화, 실행, 잠금, 트랜잭션 로그 기록, 스토리지 엔진 호출, 결과 반환이 단계별로 일어난다. 이 흐름을 이해하지 못하면 성능 문제를 모두 “쿼리가 느리다” 또는 “DB가 느리다”라는 한 문장으로 뭉개게 된다.
MySQL 아키텍처를 공부할 때 가장 먼저 잡아야 할 축은 SQL 계층과 스토리지 엔진 계층의 분리다. MySQL 서버는 SQL을 해석하고 실행 계획을 세우는 공통 계층을 가지고 있고, 실제 데이터를 저장하고 읽는 책임은 InnoDB 같은 스토리지 엔진에 위임한다. 이 구조 때문에 MySQL은 여러 엔진을 플러그인처럼 붙일 수 있었고, 동시에 운영자는 “이 문제가 SQL 계층의 문제인가, InnoDB 내부 문제인가, 파일 시스템이나 스토리지 문제인가”를 구분해서 봐야 한다.
이 글은 앞으로 이어질 InnoDB, 메모리 구조, 로그, 트랜잭션, 잠금, 옵티마이저, 복제, Aurora MySQL 글의 출발점이다. 여기서는 MySQL 서버를 하나의 큰 프로세스로만 보지 않고, 요청이 어떤 계층을 지나며 어떤 책임이 분리되는지 운영 관점에서 정리한다.
1. MySQL 서버를 구성하는 큰 계층
MySQL 서버는 단순히 mysqld 프로세스 하나로 실행되지만, 내부 역할은 크게 다음 계층으로 나눌 수 있다.
| 계층 | 주요 역할 | 운영자가 주로 보는 증상 |
|---|---|---|
| 연결/세션 계층 | 클라이언트 접속, 인증, 세션 변수, 스레드 할당 | 접속 폭주, Too many connections, 인증 실패, idle connection 증가 |
| SQL 계층 | SQL 파싱, 권한 확인, 옵티마이저, 실행기, 내장 함수 | 실행 계획 오류, 임시 테이블, filesort, CPU 사용량 증가 |
| 스토리지 엔진 계층 | 데이터/인덱스 접근, 트랜잭션, 잠금, redo/undo, 버퍼 캐시 | row lock wait, deadlock, redo pressure, buffer pool miss |
| 로그/복제 계층 | binlog 기록, relay log, replication event 처리 | 복제 지연, binlog 폭증, PITR 가능 범위 |
| 플러그인/확장 계층 | 인증, audit, fulltext parser, storage engine 등 확장 | 플러그인 의존성, 버전 호환성, 보안 정책 차이 |
| 운영체제/스토리지 계층 | 파일 시스템, 디스크 I/O, 네트워크, 메모리, CPU | fsync 지연, IOPS 한계, swap, 네트워크 timeout |
이 구분은 단순한 이론이 아니다. 장애 대응에서 매우 중요하다. 예를 들어 SELECT가 느릴 때 그 원인은 다음 중 하나일 수 있다.
- SQL 계층에서 잘못된 실행 계획을 선택했다.
- InnoDB가 필요한 페이지를 buffer pool에서 찾지 못하고 디스크에서 읽고 있다.
- 다른 트랜잭션이 gap lock 또는 record lock을 잡고 있어 대기 중이다.
- 임시 테이블이 메모리를 넘어서 디스크로 내려갔다.
- Aurora MySQL 환경에서는 스토리지 계층의 분산 I/O latency 또는 reader lag가 관여한다.
- 애플리케이션 connection pool이 과도한 동시 요청을 밀어 넣어 DB 내부 큐잉을 만들고 있다.
따라서 MySQL 운영의 첫 번째 원칙은 “SQL 문장만 보지 말고, 그 SQL이 어느 계층에서 병목을 만드는지 분해해서 본다”는 것이다.
2. 요청 처리 흐름: 클라이언트에서 스토리지 엔진까지
하나의 쿼리가 들어왔을 때 MySQL 서버는 대략 다음 순서로 움직인다.
- 클라이언트가 TCP 또는 Unix socket으로 접속한다.
- 서버는 handshake를 수행하고 인증 플러그인을 통해 계정을 확인한다.
- 접속이 승인되면 세션이 생성되고, 해당 세션을 처리할 스레드가 배정된다.
- 클라이언트가 SQL을 전송한다.
- SQL 계층이 문장을 파싱하고 문법 오류를 확인한다.
- 권한과 객체 존재 여부를 확인한다.
- 옵티마이저가 가능한 실행 계획을 평가한다.
- 실행기가 선택된 계획에 따라 스토리지 엔진 API를 호출한다.
- InnoDB 같은 엔진이 인덱스와 데이터 페이지를 읽거나 변경한다.
- 필요하면 redo log, undo log, binlog, lock, MVCC 처리가 함께 일어난다.
- 결과를 클라이언트에 반환한다.
이를 간단히 그리면 다음과 같다.
Client
|
| SQL 요청
v
MySQL Connection / Session Layer
|
| 인증, 세션 변수, 스레드 배정
v
SQL Layer
|-- Parser
|-- Privilege Check
|-- Optimizer
|-- Executor
|
| Handler API 호출
v
Storage Engine Layer
|-- InnoDB
|-- MEMORY
|-- MyISAM 등
|
| 파일/메모리/로그 접근
v
OS / Filesystem / Storage
여기서 핵심은 SQL 계층이 직접 디스크 파일을 뒤지는 것이 아니라는 점이다. SQL 계층은 “어떤 테이블에서 어떤 조건으로 어떤 인덱스를 이용해 읽어야 하는가”를 결정하고, 실제 레코드 탐색과 변경은 스토리지 엔진에 요청한다. MySQL 내부에서는 이 경계가 handler API로 추상화되어 있다.
3. SQL 계층의 책임
SQL 계층은 MySQL의 공통 두뇌에 가깝다. InnoDB를 쓰든 MEMORY 엔진을 쓰든, 사용자는 SQL 문법을 통해 서버에 요청한다. SQL 계층은 다음 일을 담당한다.
3.1 파싱과 구문 분석
SQL 문장은 먼저 토큰화되고 파싱된다. 이 단계에서 문법 오류가 있으면 스토리지 엔진까지 내려가지 않는다.
예를 들어 SELECT 절에는 반환할 표현식이 반드시 있어야 한다. 다음처럼 조회할 컬럼을 명시해야 파서가 문장을 정상적으로 해석한다.
SELECT id
FROM (SELECT 1 AS id) AS sample_user
WHERE id = 1;
실행 결과(MySQL 8.0.46):
+----+
| id |
+----+
| 1 |
+----+
반면 문법은 맞지만 컬럼이 없거나 권한이 없는 경우에는 이후의 이름 해석 또는 권한 확인 단계에서 오류가 반환된다.
3.2 권한 확인과 객체 해석
MySQL은 사용자가 접근하려는 데이터베이스, 테이블, 컬럼, 루틴에 대한 권한을 확인한다. 이때 계정은 단순히 사용자명만이 아니라 user@host 조합으로 판단된다.
운영 중 권한 문제를 확인할 때는 다음 쿼리가 기본이다.
CREATE USER IF NOT EXISTS 'app_user'@'%'
IDENTIFIED BY 'app_user_password_8_0!';
GRANT SELECT ON mysql_tech_note.* TO 'app_user'@'%';
SELECT user, host, plugin, account_locked
FROM mysql.user
WHERE user = 'app_user'
ORDER BY user, host;
SHOW GRANTS FOR 'app_user'@'%';
실행 결과(MySQL 8.0.46):
+----------+------+-----------------------+----------------+
| user | host | plugin | account_locked |
+----------+------+-----------------------+----------------+
| app_user | % | caching_sha2_password | N |
+----------+------+-----------------------+----------------+
+-------------------------------------------------------+
| Grants for app_user@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`%` |
| GRANT SELECT ON `mysql_tech_note`.* TO `app_user`@`%` |
+-------------------------------------------------------+
권한 확인은 보안뿐 아니라 장애 대응에도 중요하다. 배포 후 특정 기능만 실패하는 경우 실제 원인이 테이블 권한 누락, view definer 문제, routine execute 권한 누락인 경우가 많다.
3.3 옵티마이저
옵티마이저는 SQL을 어떤 순서와 방법으로 실행할지 결정한다. 조인 순서, 사용할 인덱스, range scan 여부, 임시 테이블 사용 여부, filesort 여부 등이 여기서 결정된다.
대표적인 진단 도구는 EXPLAIN이다.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
name VARCHAR(100) 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_created_at (created_at),
KEY idx_orders_customer_created (customer_id, created_at),
CONSTRAINT fk_orders_customers
FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB;
INSERT INTO customers (id, name) VALUES
(100, 'Kim'),
(101, 'Lee');
INSERT INTO orders (id, customer_id, created_at, status) VALUES
(1, 100, '2026-05-02 10:00:00', 'PAID'),
(2, 100, '2026-05-03 11:00:00', 'READY'),
(3, 101, '2026-01-10 09:00:00', 'PAID');
EXPLAIN
SELECT o.id, o.created_at, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 50;
MySQL 8.0에서는 실제 실행 시간과 추정치의 차이를 보기 위해 EXPLAIN ANALYZE도 사용할 수 있다.
EXPLAIN ANALYZE
SELECT o.id, o.created_at, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 50;
옵티마이저 문제는 SQL 계층의 문제처럼 보이지만, 실제 판단에는 스토리지 엔진이 제공하는 통계 정보가 영향을 준다. 예를 들어 InnoDB의 인덱스 cardinality가 부정확하면 옵티마이저가 잘못된 인덱스를 고를 수 있다. 즉 SQL 계층과 엔진 계층은 분리되어 있지만 완전히 독립적이지는 않다.
3.4 실행기
실행기는 옵티마이저가 선택한 실행 계획을 실제로 수행한다. 이 과정에서 스토리지 엔진에 “인덱스를 열어라”, “다음 레코드를 읽어라”, “이 레코드를 갱신하라” 같은 요청을 보낸다. 사용자는 SQL을 한 문장으로 보지만, 내부 실행기는 매우 많은 row access 요청을 반복할 수 있다.
운영자가 자주 보는 지표 중 Handler_read% 계열 상태 변수는 이 계층의 흔적을 보여준다.
SHOW GLOBAL STATUS LIKE 'Handler_read%';
실행 결과(MySQL 8.0.46):
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 43 |
| Handler_read_key | 2098 |
| Handler_read_last | 0 |
| Handler_read_next | 4818 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 195 |
+-----------------------+-------+
대략적으로 다음처럼 해석할 수 있다.
| 지표 | 의미 | 주의할 점 |
|---|---|---|
Handler_read_key |
인덱스 키 기반으로 레코드를 읽은 횟수 | 높다고 무조건 나쁘지는 않다 |
Handler_read_next |
인덱스 순서대로 다음 레코드를 읽은 횟수 | range scan, index scan과 관련 |
Handler_read_rnd_next |
테이블 또는 임시 테이블을 순차적으로 읽은 횟수 | full scan, filesort 후 접근 가능성 |
이 지표만으로 결론을 내리면 안 되지만, 쿼리 패턴 변화와 함께 보면 SQL 계층의 접근 방식이 바뀌었는지 감지할 수 있다.
4. 스토리지 엔진 계층의 책임
스토리지 엔진은 실제 데이터 저장과 접근을 담당한다. MySQL이 여러 스토리지 엔진을 지원한다는 것은 같은 SQL 계층 아래에 서로 다른 저장 방식을 붙일 수 있다는 뜻이다.
현대 운영 환경에서는 대부분 InnoDB가 기본이다. InnoDB는 다음 기능을 제공한다.
- 트랜잭션과 ACID 보장
- row-level locking
- MVCC 기반 consistent read
- crash recovery
- redo log와 undo log
- buffer pool 기반 데이터 페이지 캐시
- clustered index 구조
- foreign key 지원
반면 MEMORY 엔진은 메모리 기반 임시성 데이터에 적합하고, MyISAM은 과거에는 많이 쓰였지만 트랜잭션과 crash recovery 측면에서 현대 운영 DB의 기본 선택으로는 부적절하다.
현재 서버의 테이블 엔진 분포는 다음 쿼리로 확인할 수 있다.
SELECT engine, COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY engine
ORDER BY table_count DESC;
실행 결과(MySQL 8.0.46):
+--------+-------------+
| ENGINE | table_count |
+--------+-------------+
| InnoDB | 2 |
+--------+-------------+
특정 데이터베이스의 테이블별 엔진을 보려면 다음처럼 확인한다.
SELECT table_schema, table_name, engine, table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY table_name;
실행 결과(MySQL 8.0.46):
+-----------------+------------+--------+------------+
| TABLE_SCHEMA | TABLE_NAME | ENGINE | TABLE_ROWS |
+-----------------+------------+--------+------------+
| mysql_tech_note | customers | InnoDB | 2 |
| mysql_tech_note | orders | InnoDB | 3 |
+-----------------+------------+--------+------------+
운영 환경에서 의도치 않게 MyISAM, MEMORY, CSV 같은 엔진 테이블이 섞여 있다면 반드시 목적을 확인해야 한다. 특히 백업, 복구, 잠금, crash recovery 특성이 InnoDB와 다르기 때문에 장애 상황에서 예상과 다른 동작을 만들 수 있다.
5. Handler API와 계층 분리의 의미
MySQL의 SQL 계층은 특정 스토리지 엔진의 내부 구현을 모두 알지 못한다. 대신 handler API라는 추상화된 인터페이스를 통해 엔진과 통신한다. 이 구조는 다음과 같은 장점을 만든다.
- SQL 계층은 여러 스토리지 엔진을 공통 문법으로 다룰 수 있다.
- 스토리지 엔진은 자체적인 저장 구조와 잠금 방식을 가질 수 있다.
- 플러그인 형태로 엔진을 추가하거나 교체할 수 있다.
- 옵티마이저는 엔진이 제공하는 통계와 비용 정보를 바탕으로 계획을 세운다.
하지만 단점도 있다. 운영자가 SQL만 보고 모든 내부 동작을 추론하기 어렵다. 예를 들어 SELECT ... FOR UPDATE는 SQL 문장으로는 하나지만, InnoDB에서는 current read를 수행하고 row lock 또는 gap lock을 잡을 수 있다. 같은 SQL이라도 격리 수준, 인덱스 유무, 조건식 형태에 따라 잠금 범위가 달라진다.
다음 예를 보자.
START TRANSACTION;
SELECT *
FROM orders
WHERE customer_id = 100
AND created_at >= '2026-05-01'
FOR UPDATE;
ROLLBACK;
실행 결과(MySQL 8.0.46):
+----+-------------+---------------------+--------+
| id | customer_id | created_at | status |
+----+-------------+---------------------+--------+
| 1 | 100 | 2026-05-02 10:00:00 | PAID |
| 2 | 100 | 2026-05-03 11:00:00 | READY |
+----+-------------+---------------------+--------+
이 쿼리가 어떤 범위를 잠그는지는 SQL 계층만으로 결정되지 않는다. InnoDB가 어떤 인덱스를 사용하느냐, 조건이 range scan이냐, 격리 수준이 REPEATABLE READ냐 READ COMMITTED냐에 따라 달라질 수 있다. 따라서 잠금 문제를 분석할 때는 EXPLAIN과 InnoDB lock wait 정보를 함께 봐야 한다.
EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 100
AND created_at >= '2026-05-01'
FOR UPDATE;
실행 결과(MySQL 8.0.46):
+----+-------------+--------+------------+-------+---------------------------------------------------+-----------------------+---------+------+------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------------------------------------------+-----------------------+---------+------+------+----------+------------------------------------+
| 1 | SIMPLE | orders | NULL | range | idx_orders_created_at,idx_orders_customer_created | idx_orders_created_at | 5 | NULL | 2 | 50.00 | Using index condition; Using where |
+----+-------------+--------+------------+-------+---------------------------------------------------+-----------------------+---------+------+------+----------+------------------------------------+
... <truncated 205 chars>
MySQL 8.0에서는 Performance Schema를 통해 현재 데이터 잠금도 확인할 수 있다.
SELECT *
FROM performance_schema.data_locks\G
SELECT *
FROM performance_schema.data_lock_waits\G
6. 플러그인 구조: 확장성과 운영 리스크
MySQL은 스토리지 엔진뿐 아니라 인증, fulltext parser, audit, keyring, clone, replication 관련 기능 등 여러 부분을 플러그인 형태로 확장할 수 있다.
현재 설치된 플러그인은 다음 쿼리로 확인한다.
SELECT plugin_name, plugin_version, plugin_status, plugin_type, plugin_library
FROM information_schema.plugins
ORDER BY plugin_type, plugin_name;
또는 다음 명령도 사용할 수 있다.
SHOW PLUGINS;
플러그인 구조는 유연하지만 운영 리스크도 있다.
- 업그레이드 시 플러그인 호환성이 문제가 될 수 있다.
- 인증 플러그인 변경은 애플리케이션 드라이버 호환성 문제를 만든다.
- audit/keyring 계열 플러그인은 보안 정책과 백업/복구 절차에 영향을 준다.
- 스토리지 엔진 플러그인은 데이터 파일 형식과 복구 가능성에 직접 영향을 준다.
예를 들어 MySQL 8.0에서 기본 인증 플러그인이 caching_sha2_password로 바뀌면서 오래된 클라이언트 드라이버가 접속하지 못하는 문제가 자주 발생했다. 이 문제는 SQL 계층이나 InnoDB 문제가 아니라 인증 플러그인과 클라이언트 프로토콜 호환성 문제다.
계정별 인증 플러그인은 다음처럼 확인한다.
SELECT user, host, plugin
FROM mysql.user
ORDER BY user, host;
실행 결과(MySQL 8.0.46):
+------------------+-----------+-----------------------+
| user | host | plugin |
+------------------+-----------+-----------------------+
| app_user | % | caching_sha2_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session | localhost | caching_sha2_password |
| mysql.sys | localhost | caching_sha2_password |
| root | % | caching_sha2_password |
| root | localhost | caching_sha2_password |
+------------------+-----------+-----------------------+
운영에서는 플러그인을 “설치되어 있으니 된다”가 아니라 “업그레이드, 백업, 복구, 클라이언트 호환성, 장애 시 대체 절차까지 검증되었는가”라는 기준으로 봐야 한다.
7. InnoDB가 사실상 표준이 된 이유
MySQL이 여러 엔진을 지원한다고 해도, 대부분의 운영 시스템에서는 InnoDB가 표준이다. 이유는 명확하다. 서비스 데이터베이스에는 트랜잭션, 동시성, 장애 복구가 필요하기 때문이다.
InnoDB는 SQL 계층 아래에서 다음을 책임진다.
- 데이터 페이지를 buffer pool에 캐시한다.
- clustered index 기준으로 레코드를 저장한다.
- redo log로 crash recovery를 보장한다.
- undo log로 rollback과 MVCC를 지원한다.
- row lock과 next-key lock으로 동시성을 제어한다.
- checkpoint와 page flush로 메모리의 dirty page를 디스크에 반영한다.
이 구조 때문에 InnoDB 문제는 단순히 “디스크가 느리다”로 끝나지 않는다. buffer pool hit ratio, redo log pressure, flush list length, history list length, lock wait, purge 지연 같은 내부 상태를 함께 봐야 한다.
대표적인 InnoDB 상태 확인 명령은 다음이다.
SHOW ENGINE INNODB STATUS\G
출력은 길지만, 초기에 특히 볼 영역은 다음과 같다.
| 영역 | 확인할 내용 |
|---|---|
TRANSACTIONS |
긴 트랜잭션, lock wait, history list length |
LATEST DETECTED DEADLOCK |
최근 deadlock 원인 |
BUFFER POOL AND MEMORY |
buffer pool 사용량, dirty page, read/write 상태 |
ROW OPERATIONS |
background thread와 row operation 상태 |
LOG |
log sequence number, flushed up to, checkpoint 관련 정보 |
앞으로의 InnoDB 내부 구조 글에서는 이 항목들을 하나씩 풀어갈 것이다. 지금 단계에서는 SQL 계층의 실행기가 InnoDB에 요청을 보내고, InnoDB가 자체적인 메모리·로그·잠금 체계를 통해 요청을 수행한다는 큰 그림을 잡으면 된다.
8. 운영 관점에서 계층을 나누어 진단하는 방법
실제 장애나 성능 문제를 볼 때는 다음 순서로 계층을 나누어 접근하는 것이 좋다.
8.1 연결 계층 확인
먼저 접속이 몰렸는지 확인한다.
SHOW GLOBAL STATUS LIKE 'Threads%';
SHOW GLOBAL VARIABLES LIKE 'max_connections';
SELECT user, host, db, command, time, state, info
FROM information_schema.processlist
ORDER BY time DESC
LIMIT 20;
실행 결과(MySQL 8.0.46):
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
+-------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 30 |
+-----------------+-------+
+-----------------+-----------+-----------------+---------+------+------------------------+-------------------------------------------------------------------------------------------------------------------+
| user | host | db | command | time | state | info |
+-----------------+-----------+-----------------+---------+------+------------------------+-------------------------------------------------------------------------------------------------------------------+
| event_scheduler | localhost | NULL | Daemon | 6 | Waiting on empty queue | NULL
... <truncated 586 chars>
Threads_connected가 평소보다 높고 max_connections에 가까우면 SQL이나 InnoDB보다 앞단의 connection pool, 트래픽, idle session 관리 문제가 먼저일 수 있다.
8.2 SQL 계층 확인
느린 쿼리와 실행 계획을 본다.
EXPLAIN FORMAT=JSON
SELECT id, customer_id, created_at
FROM orders
WHERE customer_id = 100
AND created_at >= '2026-05-01';
운영 서버에서는 Performance Schema나 sys schema를 활용해 digest 기준으로 쿼리 패턴을 본다.
SELECT digest_text,
count_star,
ROUND(sum_timer_wait / 1000000000000, 2) AS total_sec,
ROUND(avg_timer_wait / 1000000000000, 6) AS avg_sec,
sum_rows_examined,
sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;
rows_examined가 지나치게 크고 rows_sent가 작다면 인덱스 설계나 조건식의 sargability를 의심해야 한다.
8.3 스토리지 엔진 계층 확인
InnoDB 잠금, 트랜잭션, 버퍼 상태를 확인한다.
SHOW ENGINE INNODB STATUS\G
SELECT *
FROM information_schema.innodb_trx\G
MySQL 8.0에서는 Performance Schema의 data lock 테이블도 중요하다.
SELECT engine_transaction_id, object_schema, object_name, index_name,
lock_type, lock_mode, lock_status, lock_data
FROM performance_schema.data_locks
ORDER BY object_schema, object_name, index_name;
8.4 운영체제와 스토리지 확인
DB 내부 지표가 I/O 병목을 가리킨다면 OS 레벨도 확인해야 한다.
iostat -x 1
vmstat 1
pidstat -d -p $(pidof mysqld) 1
클라우드 환경에서는 로컬 명령만으로 충분하지 않다. RDS/Aurora에서는 CloudWatch, Performance Insights, Enhanced Monitoring이 보여주는 지표와 함께 봐야 한다.
9. Aurora MySQL에서는 무엇이 달라지는가
Aurora MySQL은 MySQL 호환 SQL 계층을 제공하지만, 스토리지 아키텍처는 Community MySQL과 크게 다르다. 일반 MySQL/InnoDB는 인스턴스가 로컬 또는 EBS 같은 블록 스토리지에 데이터 파일과 redo log를 관리하는 모델에 가깝다. Aurora는 분산 스토리지 계층을 사용하고, 여러 AZ에 복제된 스토리지 볼륨을 기반으로 동작한다.
운영 관점에서 중요한 차이는 다음과 같다.
| 항목 | Community MySQL/InnoDB | Aurora MySQL |
|---|---|---|
| 스토리지 | 인스턴스가 데이터 파일과 로그를 직접 관리 | 분산 스토리지 계층이 핵심 역할 |
| 복제 | binlog/replica 기반 구성이 일반적 | reader는 shared storage 기반, writer 변경 전파 모델이 다름 |
| 백업 | physical/logical backup, snapshot, binlog 조합 | continuous backup, cluster snapshot, PITR 제공 |
| 장애 조치 | orchestrator, VIP, ProxySQL, DNS 등 직접 설계 | writer failover와 cluster endpoint 제공 |
| I/O 비용 | 스토리지 장비/EBS 성능과 직접 연관 | Aurora I/O 과금과 스토리지 요청 패턴 중요 |
그렇다고 SQL 계층의 지식이 무의미해지는 것은 아니다. Aurora MySQL에서도 옵티마이저, 인덱스, 트랜잭션, 잠금, 실행 계획, connection 관리의 기본은 여전히 MySQL이다. 다만 InnoDB의 물리 로그/파일 관리와 장애 복구를 해석할 때 Aurora 특유의 분산 스토리지 구조를 고려해야 한다.
예를 들어 같은 느린 쿼리라도 Community MySQL에서는 로컬 buffer pool miss와 EBS latency를 먼저 의심할 수 있고, Aurora에서는 Performance Insights의 wait event, storage read latency, reader endpoint 사용 방식, writer/reader 간 read-after-write 요구사항을 함께 봐야 한다.
Aurora에서 기본적으로 확인할 지표는 다음 범주다.
- DatabaseConnections
- CPUUtilization
- FreeableMemory
- BufferCacheHitRatio
- Deadlocks
- DMLLatency, SelectLatency
- VolumeReadIOPs, VolumeWriteIOPs
- AuroraReplicaLag
- Performance Insights의 wait event
즉 Aurora를 쓰더라도 MySQL 계층 구조를 알아야 하고, 그 위에 Aurora가 바꾼 스토리지·복제·백업 모델을 추가로 얹어 이해해야 한다.
10. 자주 발생하는 오해
오해 1. MySQL은 하나의 엔진이므로 모든 테이블이 같은 방식으로 동작한다
그렇지 않다. MySQL은 SQL 계층과 스토리지 엔진 계층이 분리되어 있다. 대부분 InnoDB를 쓰지만, 실제 운영 환경에는 MEMORY, MyISAM, FEDERATED, CSV, 또는 특수 플러그인이 섞여 있을 수 있다. 엔진이 다르면 잠금, 복구, 트랜잭션 특성이 달라진다.
오해 2. EXPLAIN만 보면 모든 병목을 알 수 있다
EXPLAIN은 SQL 계층의 실행 계획을 이해하는 데 중요하지만, 실제 실행 중 lock wait, buffer pool miss, redo log flush, 디스크 fsync 지연까지 모두 보여주지는 않는다. EXPLAIN ANALYZE, Performance Schema, InnoDB status, OS 지표를 함께 봐야 한다.
오해 3. InnoDB 설정만 튜닝하면 성능 문제가 해결된다
InnoDB 설정은 중요하지만, 잘못된 쿼리 패턴이나 connection pool 폭주를 설정값으로 해결할 수는 없다. innodb_buffer_pool_size를 늘려도 full scan이 반복되는 SQL 구조나 과도한 동시 요청은 여전히 문제를 만든다.
오해 4. Aurora MySQL은 관리형이므로 MySQL 내부 구조를 몰라도 된다
Aurora가 백업, failover, 스토리지 복제를 많이 대신해주지만, SQL 실행 계획, 인덱스, 잠금, 트랜잭션, connection 관리는 여전히 사용자의 설계와 운영 책임이다. 관리형 서비스는 운영 부담을 줄여주지만, 데이터베이스 원리를 없애주지는 않는다.
11. 실무 점검 쿼리 모음
첫 아키텍처 점검에서는 다음 쿼리들을 기본 세트로 사용할 수 있다.
11.1 서버 버전과 기본 정보
SELECT VERSION() AS mysql_version;
SHOW VARIABLES LIKE 'version%';
SHOW VARIABLES LIKE 'default_storage_engine';
SHOW VARIABLES LIKE 'sql_mode';
SHOW VARIABLES LIKE 'transaction_isolation';
실행 결과(MySQL 8.0.46):
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
+-------------------------+------------------------------+
| Variable_name | Value |
+-------------------------+------------------------------+
| version | 8.0.46 |
| version_comment | MySQL Community Server - GPL |
| version_compile_machine | x86_64 |
| version_compile_os | Linux |
| version_compile_zlib | 1.3.2 |
+-------------------------+------------------------------+
+------------------------+--------+
| Variable_name | Value |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+
+---------------+-----------------------------------------------------------------------------------------------------------------------+
| Variable_name | Value |
+---------------+----------------------------------------------------------------------------------------------------------
... <truncated 510 chars>
11.2 스토리지 엔진 분포
SELECT engine, COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY engine
ORDER BY table_count DESC;
실행 결과(MySQL 8.0.46):
+--------+-------------+
| ENGINE | table_count |
+--------+-------------+
| InnoDB | 2 |
+--------+-------------+
11.3 큰 테이블과 엔진 확인
SELECT table_schema,
table_name,
engine,
table_rows,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
ORDER BY data_length + index_length DESC
LIMIT 20;
실행 결과(MySQL 8.0.46):
+-----------------+------------+--------+------------+---------+
| TABLE_SCHEMA | TABLE_NAME | ENGINE | TABLE_ROWS | size_mb |
+-----------------+------------+--------+------------+---------+
| mysql_tech_note | orders | InnoDB | 3 | 0.0 |
| mysql_tech_note | customers | InnoDB | 2 | 0.0 |
+-----------------+------------+--------+------------+---------+
11.4 접속과 스레드 상태
SHOW GLOBAL STATUS LIKE 'Threads%';
SHOW GLOBAL STATUS LIKE 'Connections';
SHOW GLOBAL STATUS LIKE 'Aborted_connects';
SELECT command, state, COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY command, state
ORDER BY cnt DESC;
실행 결과(MySQL 8.0.46):
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
+-------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Connections | 33 |
+---------------+-------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Aborted_connects | 0 |
+------------------+-------+
+---------+------------------------+-----+
| command | state | cnt |
+---------+------------------------+-----+
| Query | executing | 1 |
| Daemon | Waiting on empty queue | 1 |
+---------+------------------------+-----+
11.5 쿼리 digest 상위 항목
SELECT digest_text,
count_star,
ROUND(sum_timer_wait / 1000000000000, 2) AS total_sec,
ROUND(avg_timer_wait / 1000000000000, 6) AS avg_sec,
sum_rows_examined,
sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text IS NOT NULL
ORDER BY sum_timer_wait DESC
LIMIT 10;
11.6 InnoDB 상태
SHOW ENGINE INNODB STATUS\G
SELECT trx_id,
trx_state,
trx_started,
trx_wait_started,
trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
이 쿼리들은 문제를 바로 해결하는 만능 도구가 아니다. 하지만 “지금 병목이 연결 계층인가, SQL 실행 계획인가, InnoDB 트랜잭션/잠금인가, 스토리지 I/O인가”를 가르는 출발점이 된다.
12. 운영 체크리스트
MySQL 서버 아키텍처를 기준으로 운영 환경을 점검할 때는 다음 질문을 던져야 한다.
- 애플리케이션 connection pool의 최대 연결 수가 MySQL
max_connections -
EXPLAIN과EXPLAIN ANALYZE
13. 정리
MySQL 서버 아키텍처의 핵심은 SQL 계층과 스토리지 엔진 계층의 분리다. SQL 계층은 접속, 파싱, 권한 확인, 최적화, 실행을 담당하고, 스토리지 엔진 계층은 실제 데이터 접근, 트랜잭션, 잠금, 로그, 복구를 담당한다. 이 구조는 MySQL의 유연성을 만들었지만, 동시에 운영자가 문제를 계층별로 나누어 진단해야 한다는 요구도 만든다.
앞으로 InnoDB buffer pool, redo log, undo log, MVCC, optimizer, replication, backup, Aurora MySQL을 깊게 다룰 때도 이 기본 구분은 계속 반복된다. 어떤 지표가 SQL 계층의 신호인지, 어떤 지표가 InnoDB 내부 상태인지, 어떤 현상이 운영체제나 Aurora 스토리지 계층에서 오는지 구분할 수 있어야 한다.
좋은 MySQL 운영은 설정값 몇 개를 외우는 일이 아니다. 요청이 서버 내부에서 어떤 경로로 흐르고, 각 계층이 어떤 책임을 가지며, 문제가 생겼을 때 어느 계층을 먼저 의심해야 하는지를 이해하는 일이다. 이 첫 장은 그 전체 지도의 출발점이다.