본문 바로가기
삵
MariaDB

MariaDB가 메모리를 먹다 OOM으로 죽을 때

열열린기술자·2026년 10월 14일·조회 0

운영 중인 MariaDB가 어느 날 로그도 없이 사라지고, 다시 시작하면 또 멀쩡히 돈다. 메모리 설정을 올렸다가 이런 일을 겪은 DBA가 꽤 많다. mysqld를 죽인 건 DB 자신이 아니라 커널의 OOM killer인 경우가 대부분이다. 예전에 버퍼 풀만 키우면 빨라진다는 글을 보고 물리 메모리의 80%를 버퍼 풀로 잡았다가, 접속이 몰리는 시간대에 서버가 통째로 흔들리는 걸 본 적이 있다.

결론부터 말하자면, MariaDB/MySQL의 실제 메모리 상한은 innodb_buffer_pool_size 같은 전역 버퍼 하나로 끝나지 않는다. 전역 버퍼에 더해 연결 하나가 늘 때마다 할당될 수 있는 연결당 버퍼(sort_buffer, join_buffer, read_buffer 등)를 함께 계산해야 진짜 상한이 나온다. 이 둘을 더한 값이 물리 메모리를 넘으면 OOM으로 죽는다.

1. 왜 설정값보다 더 먹는가

InnoDB 버퍼 풀은 테이블과 인덱스 데이터를 메모리에 올려두는 캐시다. 디스크 I/O를 줄이려고 존재하며, DB 서버 메모리에서 가장 큰 덩어리다. 이 크기는 innodb_buffer_pool_size로 고정되므로 예측이 쉽다.

문제는 연결당 버퍼다. 정렬(sort_buffer_size), 조인(join_buffer_size), 순차 읽기(read_buffer_size), 역순 읽기(read_rnd_buffer_size) 같은 버퍼는 세션마다, 필요할 때 할당된다. 접속이 10개일 때와 500개일 때 소비량이 완전히 다르다. 한 쿼리가 정렬과 여러 조인을 동시에 쓰면 한 세션이 이 버퍼들을 여러 벌 잡을 수도 있다.

그래서 평소엔 조용하다가 접속이 몰리는 시간에만 메모리가 치솟고, 커널이 가장 큰 프로세스인 mysqld를 골라 종료한다. 설정 파일만 보면 멀쩡해 보이는 이유가 여기 있다.

2. OOM으로 죽은 게 맞는지 먼저 확인

mysqld가 커널에 죽었는지부터 확인한다. 추측하지 말고 로그를 본다.

$ sudo dmesg -T | grep -i -E 'oom|killed process'
[Tue Oct 14 03:11:27 2026] Out of memory: Killed process 2417 (mysqld) total-vm:9873240kB, anon-rss:7412880kB, file-rss:0kB

이 줄이 보이면 OOM killer가 범인이다. MariaDB 에러 로그에는 보통 다음처럼 비정상 종료 뒤 재시작 흔적만 남는다.

$ sudo journalctl -u mariadb -n 20 --no-pager
mariadbd[2417]: ... [Note] InnoDB: Starting crash recovery.
systemd[1]: mariadb.service: Main process exited, code=killed, status=9/KILL
systemd[1]: mariadb.service: Failed with result 'signal'.

status=9/KILL은 SIGKILL로 강제 종료됐다는 뜻이다. DB 자체 에러가 아니라 외부에서 죽인 신호다.

3. 실제 메모리 상한 계산식

MariaDB가 최악의 경우 쓸 수 있는 메모리 상한은 다음과 같이 어림한다.

최대 메모리 ≈ 전역 버퍼
            + (연결당 버퍼 합 × max_connections)

전역 버퍼   = innodb_buffer_pool_size
            + innodb_log_buffer_size
            + key_buffer_size        (MyISAM 쓸 때)
            + query_cache_size       (쓸 때, MySQL 8.0엔 없음)

연결당 버퍼 = sort_buffer_size
            + join_buffer_size
            + read_buffer_size
            + read_rnd_buffer_size
            + thread_stack
            + net_buffer_length

연결당 버퍼는 세션이 해당 작업을 할 때만 잡히므로 모든 접속이 동시에 최대치를 쓰는 일은 드물다. 그래도 사이징은 최악을 기준으로 잡아야 안전하다. max_connections를 곱하는 이유가 이것이다.

4. 현재 값 조회

계산에 넣을 값을 실제 인스턴스에서 뽑는다.

MariaDB [(none)]> SELECT @@innodb_buffer_pool_size/1024/1024 AS buf_pool_mb,
    ->        @@max_connections AS max_conn;
+-------------+----------+
| buf_pool_mb | max_conn |
+-------------+----------+
|   6144.0000 |      500 |
+-------------+----------+
MariaDB [(none)]> SHOW VARIABLES WHERE Variable_name IN
    ->  ('sort_buffer_size','join_buffer_size','read_buffer_size',
    ->   'read_rnd_buffer_size','thread_stack','net_buffer_length',
    ->   'innodb_log_buffer_size','key_buffer_size');
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
| join_buffer_size       | 262144   |
| key_buffer_size        | 134217728|
| net_buffer_length      | 16384    |
| read_buffer_size       | 131072   |
| read_rnd_buffer_size   | 262144   |
| sort_buffer_size       | 262144   |
| thread_stack           | 299008   |
+------------------------+----------+

위 예시로 연결당 버퍼 합을 더하면 세션 하나당 대략 1.2MB 안팎이다. 여기에 max_connections=500을 곱하면 약 600MB. 버퍼 풀 6GB와 전역 버퍼를 더하면 최악의 경우 7GB에 육박한다. 물리 메모리가 8GB라면 OS와 다른 프로세스 몫이 거의 안 남는다. 이 상태가 바로 OOM 직전이다.

5. 지금 실제로 얼마나 쓰는지 본다

계산식은 상한이고, 현재 소비량은 performance_schema의 메모리 계측으로 본다. MariaDB에서는 performance_schema와 메모리 계측 설정이 켜져 있어야 값이 채워진다.

MariaDB [(none)]> SELECT event_name,
    ->        ROUND(current_number_of_bytes_used/1024/1024,1) AS mb
    -> FROM performance_schema.memory_summary_global_by_event_name
    -> ORDER BY current_number_of_bytes_used DESC
    -> LIMIT 8;

하부 테이블인 memory_summary_global_by_event_name에는 current_number_of_bytes_used 컬럼이 있다. 반면 MySQL의 sys 스키마 뷰를 쓸 때는 컬럼 이름이 다르다. sys.memory_global_by_current_bytes 뷰는 그 바이트 값을 current_alloc으로 노출하고, current_number_of_bytes_used라는 컬럼은 없다. 뷰에 대고 하부 컬럼명을 쓰면 다음 에러가 난다.

mysql> SELECT event_name, current_alloc
    -> FROM sys.memory_global_by_current_bytes
    -> ORDER BY current_number_of_bytes_used DESC LIMIT 8;
ERROR 1054 (42S22): Unknown column 'current_number_of_bytes_used' in 'order clause'

뷰를 쓸 때는 정렬도 뷰 컬럼으로 맞춘다. sys 뷰는 이미 current_alloc 내림차순으로 정렬돼 나오므로 보통 ORDER BY를 생략해도 된다.

mysql> SELECT event_name, current_alloc
    -> FROM sys.memory_global_by_current_bytes
    -> ORDER BY current_alloc DESC LIMIT 8;
+-------------------------------------+---------------+
| event_name                          | current_alloc |
+-------------------------------------+---------------+
| memory/innodb/buf_buf_pool          | 6.00 GiB      |
| memory/sql/TABLE_SHARE::mem_root    | 42.11 MiB     |
| memory/sql/THD::main_mem_root       | 18.74 MiB     |
| ...                                 | ...           |
+-------------------------------------+---------------+

이 결과로 버퍼 풀이 차지하는 몫과 세션이 들고 있는 몫을 눈으로 구분할 수 있다. 운영체제 레벨에서는 ps -o rss= -C mariadbd로 mysqld의 실제 상주 메모리(RSS)를 KB 단위로 같이 본다.

6. 안전하게 튜닝하는 순서

버퍼 풀은 적중률을 보며 조정한다

DB 전용 서버에서 버퍼 풀을 물리 메모리의 50~70% 선에서 출발하는 건 어디까지나 경험적 출발점이지 공식 수치가 아니다. MariaDB 공식 문서는 고정 비율 대신 버퍼 풀 적중률을 보라고 권한다. 구체적으로 innodb_buffer_pool_reads(디스크에서 읽은 횟수)의 증가분이 innodb_buffer_pool_read_requests(버퍼 풀에 요청한 횟수)의 증가분의 1% 미만이면 버퍼 풀이 충분하다고 본다.

MariaDB [(none)]> SHOW GLOBAL STATUS WHERE Variable_name IN
    ->  ('Innodb_buffer_pool_reads','Innodb_buffer_pool_read_requests');
+---------------------------------+-------------+
| Variable_name                   | Value       |
+---------------------------------+-------------+
| Innodb_buffer_pool_read_requests| 184523901   |
| Innodb_buffer_pool_reads        | 921044      |
+---------------------------------+-------------+

두 값의 비율이 1%를 밑돌면 버퍼 풀을 더 키울 이유가 약하다. 메모리가 빠듯하면 오히려 줄여 OOM 여유를 확보하는 게 낫다.

연결당 버퍼는 작게 유지한다

sort_buffer나 join_buffer를 전역에서 크게 잡는 건 가장 흔한 실수다. 이 값들은 접속 수만큼 곱해지므로 조금만 올려도 상한이 수 GB씩 뛴다. 전역 기본값은 작게 두고, 큰 정렬이 필요한 특정 세션에서만 SET SESSION sort_buffer_size=...로 올리는 방식이 안전하다.

max_connections는 실제 수요에 맞춘다

상한 계산에서 가장 큰 승수다. 애플리케이션 커넥션 풀이 실제로 쓰는 최대치보다 지나치게 크게 잡혀 있으면 그만큼 상한만 부풀린다. 피크 시간대 SHOW STATUS LIKE 'Max_used_connections'로 실제 최대 사용량을 확인하고 여유를 조금만 얹어 조정한다.

마지막 안전망을 둔다

systemd 환경이라면 서비스 유닛에 MemoryMax를 걸어 두면 mysqld가 선을 넘기 전에 커널이 아니라 cgroup 한도에서 먼저 눌린다. 다만 이건 근본 해결이 아니라 보호 장치다. 상한 계산을 먼저 맞추고 나서 보조로 쓴다.

7. 정리

MariaDB/MySQL의 메모리는 innodb_buffer_pool_size 한 줄로 끝나지 않는다. 전역 버퍼에 연결당 버퍼 곱하기 max_connections를 더한 값이 진짜 상한이고, 이 값이 물리 메모리를 넘으면 OOM killer가 mysqld를 죽인다. 먼저 dmesg로 OOM 여부를 확정하고, 계산식으로 상한을 잡은 뒤, 버퍼 풀은 적중률로, 연결당 버퍼는 작게, max_connections는 실수요로 맞추면 메모리 때문에 죽는 일은 사라진다.

자주 묻는 질문

innodb_buffer_pool_size를 물리 메모리의 몇 %로 잡아야 하나?

DB 전용 서버에서 50~70% 선을 출발점으로 삼는 경험적 관행이 있지만 공식 수치는 아니다. MariaDB 문서는 고정 비율 대신 버퍼 풀 적중률을 보라고 권한다. Innodb_buffer_pool_reads 증가분이 Innodb_buffer_pool_read_requests 증가분의 1% 미만이면 버퍼 풀이 충분한 것이므로 더 키울 필요가 약하다. 메모리가 빠듯하면 줄여 OOM 여유를 확보한다.

mysqld가 로그도 없이 사라졌다. 원인을 어떻게 확인하나?

sudo dmesg -T | grep -i oom로 커널 로그를 본다. 'Out of memory: Killed process ... (mysqld)' 줄이 있으면 OOM killer가 죽인 것이다. systemd에서는 journalctl -u mariadb에 status=9/KILL이 남는데, 이는 SIGKILL로 외부에서 강제 종료됐다는 뜻이지 DB 자체 에러가 아니다.

sys.memory_global_by_current_bytes에서 current_number_of_bytes_used로 정렬하면 왜 에러가 나나?

그 컬럼명은 하부 performance_schema.memory_summary_global_by_event_name 테이블의 것이고, sys 뷰에서는 current_alloc으로 노출된다. 뷰에 대고 하부 컬럼명을 쓰면 'Unknown column current_number_of_bytes_used' 에러가 난다. 뷰에서는 ORDER BY current_alloc으로 써야 하며, 이 뷰는 기본적으로 current_alloc 내림차순으로 나온다.

연결당 버퍼가 실제 메모리 상한에 어떻게 영향을 주나?

sort_buffer_size, join_buffer_size, read_buffer_size 등은 세션마다 필요할 때 할당된다. 따라서 이들의 합에 max_connections를 곱한 값이 최악의 경우 추가로 쓰일 수 있는 메모리다. 전역에서 이 값을 조금만 올려도 접속 수만큼 곱해져 상한이 수 GB씩 뛴다.

OOM을 막는 마지막 안전망이 있나?

systemd 유닛에 MemoryMax를 걸면 mysqld가 선을 넘기 전에 커널 OOM killer가 아니라 cgroup 한도에서 먼저 제어된다. 다만 근본 해결이 아니라 보호 장치이므로, 상한 계산을 먼저 맞춘 뒤 보조로 쓴다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.