EC2 OOM Kill — 새벽에 서버가 죽었다, 범인은 메모리였다 (2026)

새벽에 블로그에 들어갔더니 화면에 덩그러니 “Error establishing a database connection”이 떠 있었습니다. 블로그만 죽은 게 아니라 월급 계산기, 다른 웹 앱까지 전부 접속 불가. Cloudflare에서는 522 에러가 뜨고, AWS 콘솔을 열어보니 상태 검사가 2/3개 통과 — 인스턴스 연결성 검사 실패.
“해킹당했나?” 심장이 쿵 내려앉았는데, 결론부터 말하면 해킹이 아니라 메모리가 부족해서 서버가 자살한 겁니다. 정확히는 EC2 OOM Kill — Linux 커널이 메모리 부족 상황에서 MySQL을 강제로 죽인 것이었습니다.
EC2 OOM Kill이란 — 서버가 자기 살겠다고 MySQL을 죽이는 것
OOM Kill은 Out of Memory Kill의 약자입니다. Linux 커널이 “이러다 시스템 전체가 죽겠다” 싶을 때, 메모리를 가장 많이 쓰는 프로세스를 골라서 강제 종료하는 메커니즘입니다. 비행기가 추락하기 직전에 짐을 버리는 것과 비슷한데, 문제는 커널이 버리는 “짐”이 하필 MySQL이라는 겁니다.
제 t3.small 인스턴스는 RAM이 2GB입니다. 여기에 올라가 있는 것들을 보면:
- Apache — 웹 서버
- PHP — WordPress 돌리는 엔진
- MySQL — 데이터베이스 (기본 설정으로 약 500MB 차지)
- WordPress — 블로그
- 웹 앱 3개 — 월급 계산기 등
2GB에 이걸 다 올린 겁니다. 원룸에 다섯 명이 사는 것과 비슷합니다. 평소에는 어떻게든 돌아가는데, 손님(트래픽)이 좀 오면 발 디딜 틈이 없어지는 거죠. 그러면 커널이 “가장 큰 놈 나가!” 하면서 MySQL을 내쫓고, DB가 끊기니까 사이트 전체가 다운됩니다.
트래픽 증가 또는 봇 접속
→ Apache/PHP 프로세스 증가
→ 메모리 부족
→ Linux 커널: "MySQL 너 나가"
→ DB 연결 실패
→ "Error establishing a database connection"
→ 사이트 전체 다운
EC2 OOM Kill 확인하기 — 사인 규명
SSH 접속이 되면 가장 먼저 MySQL의 상태부터 확인합니다. 살아있는지, 죽었다 살아났는지, 몇 번 죽었는지.
MySQL 상태 확인
systemctl status mysql
제 경우 출력이 이랬습니다.
mysql.service - MySQL Community Server
Active: active (running) since Fri 2026-02-13 00:11:17 UTC; 49s ago
Memory: 495.7M (peak: 496.7M)
Feb 13 00:11:13 systemd[1]: mysql.service: A process of this unit has been killed by the OOM killer.
Feb 13 00:11:14 systemd[1]: mysql.service: Failed with result 'oom-kill'.
Feb 13 00:11:14 systemd[1]: mysql.service: Scheduled restart job, restart counter is at 3.
로그를 읽어보면 상황이 그려집니다.
- “killed by the OOM killer” — 커널이 MySQL을 처형했다는 사형 선고문
- “restart counter is at 3” — 오늘만 3번째 죽고 살아남. 고양이도 아니고…
- Memory: 495.7M — MySQL 혼자 500MB를 쓰고 있음. 2GB 중 1/4을 혼자 먹고 있었음
journalctl로 사망 기록 확인
journalctl -u mysql --since "today" | grep -i "start\|stop\|oom"
EC2 OOM Kill → 자동 재시작 → 또 OOM Kill → 또 재시작… 이 패턴이 반복되고 있었습니다. MySQL이 하루 종일 죽었다 살아나기를 반복하고 있었던 겁니다.
SSH도 안 될 때 — 손도 못 대는 상황
EC2 OOM Kill이 심해지면 SSH 접속 자체가 안 됩니다. 메모리가 바닥나서 SSH 세션 하나 열 여유가 없는 상태입니다. 환자를 살려야 하는데 병원 문이 잠긴 상황이죠.
이때는 AWS 콘솔에서 직접 전원을 만져야 합니다.
- AWS 콘솔 → EC2 → 인스턴스 페이지로 이동
- 해당 인스턴스 선택 → 인스턴스 상태 → 인스턴스 재부팅 시도
- 재부팅이 안 되면 “인스턴스 중지” 클릭
- 완전히 중지될 때까지 1~2분 대기 후 “인스턴스 시작” 클릭
여기서 진짜 중요한 거 하나. “중지”와 “종료”는 다릅니다. “종료”를 누르면 인스턴스가 삭제됩니다. 서버를 살리려다 서버를 지우는 참사가 벌어질 수 있습니다. 반드시 “중지”를 눌러야 합니다.
그리고 Elastic IP를 설정하지 않았다면, 중지 후 시작할 때마다 퍼블릭 IP가 바뀝니다. Cloudflare나 DNS에서 A 레코드를 새 IP로 바꿔줘야 합니다.
EC2 OOM Kill 1차 대응 — Swap 추가 (30초 안에 해야 함)
서버가 살아나면 진짜 30초 안에 Swap부터 추가해야 합니다. 농담이 아닙니다. MySQL이 다시 메모리를 500MB씩 잡아먹기 시작하면 또 죽습니다. 부팅 직후가 메모리에 여유가 있는 골든 타임입니다.
Swap이란
Swap은 디스크의 일부를 RAM처럼 사용하는 기술입니다. 실제 RAM보다 훨씬 느리지만, 메모리가 부족할 때 프로세스가 죽지 않고 버틸 수 있게 해줍니다.
비유하면 이렇습니다. 원룸(2GB RAM)이 꽉 찼을 때, 베란다 창고(디스크)에 짐을 빼놓는 겁니다. 창고까지 왔다갔다 하느라 느리긴 하지만, 적어도 짐이 넘쳐서 사람(MySQL)을 내쫓는 일은 없어집니다.
Swap 2GB 추가 (한 줄 복붙)
시간이 없으니 한 줄로 정리했습니다. 복사 → 붙여넣기 → 엔터.
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile && echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
이 명령어가 하는 일:
| 명령어 | 뭘 하는 건지 |
|---|---|
fallocate -l 2G /swapfile |
2GB짜리 빈 파일 생성 (베란다 창고 만들기) |
chmod 600 /swapfile |
root만 접근 가능하게 잠금 |
mkswap /swapfile |
이 파일을 Swap 용도로 포맷 |
swapon /swapfile |
Swap 켜기 |
echo ... >> /etc/fstab |
재부팅해도 자동으로 Swap 켜지게 등록 |
Swap 잘 들어갔는지 확인
free -m
Swap 항목에 약 2048MB가 표시되면 성공입니다. 이제 2GB RAM + 2GB Swap = 총 4GB의 메모리 공간이 확보됐습니다.
EC2 OOM Kill 2차 대응 — MySQL 다이어트
Swap으로 안전망을 깔았으니, 이제 근본 원인을 해결합니다. MySQL이 500MB씩 차지하는 게 문제였으니, MySQL의 메모리 사용량 자체를 줄여야 합니다.
MySQL 기본 설정은 대규모 서비스 기준으로 잡혀 있습니다. 개인 블로그 하나 돌리는데 500MB나 쓸 필요가 없습니다. 다이어트 시켜줍니다.
MySQL 설정 파일 수정
sudo tee -a /etc/mysql/mysql.conf.d/mysqld.cnf << 'EOF'
# Memory optimization for small instances
innodb_buffer_pool_size = 128M
innodb_log_buffer_size = 8M
key_buffer_size = 16M
max_connections = 30
EOF
각 설정이 뭘 하는 건지
| 설정 | 변경값 | 쉽게 말하면 |
|---|---|---|
innodb_buffer_pool_size |
128M | DB 캐시 크기. 블로그한테 128MB면 충분 |
innodb_log_buffer_size |
8M | 로그 버퍼. 대형 쇼핑몰이 아니니 8MB로 OK |
key_buffer_size |
16M | 인덱스 캐시. WordPress는 InnoDB라 낮춰도 됨 |
max_connections |
30 | 동시 접속 제한. 30명이면 개인 블로그에 충분 |
MySQL 재시작
sudo systemctl restart mysql
이 한 줄이 제일 떨리는 순간입니다. 설정을 잘못 건드렸으면 MySQL이 안 올라올 수도 있거든요. 에러 없이 재시작되면 성공입니다.
EC2 OOM Kill 대응 결과 — 하루 3번 죽던 서버가 안정화
Swap 추가 + MySQL 다이어트, 이 두 가지 조치 후 free -m으로 확인하면:
- Swap 2GB 잡혀 있음
- MySQL 메모리 사용량: 500MB → 약 200MB 이하로 감소
하루에 2~3번씩 죽던 서버가 조치 후 한 번도 다운되지 않았습니다. 500MB 먹던 MySQL이 200MB로 줄고, 설사 메모리가 부족해져도 Swap이 받쳐주니까 OOM Kill이 발생할 일이 없어진 겁니다.
t3.small WordPress 운영 생존 체크리스트
| 항목 | 권장 사항 |
|---|---|
| Swap | 최소 2GB 설정 필수. 안전벨트라고 생각하세요 |
| MySQL 메모리 | innodb_buffer_pool_size를 128MB 이하로. 기본값은 과함 |
| 캐시 플러그인 | WP Super Cache 또는 W3 Total Cache. PHP 부하를 확 줄여줌 |
| 모니터링 | free -m, systemctl status mysql 수시 확인 |
| Elastic IP | 할당해두면 IP 안 바뀜. 월 약 $3.6 |
마무리 — 서버는 죽어봐야 배운다
처음 "Error establishing a database connection"을 봤을 때는 막막했습니다. 서버 터미널 앞에서 식은땀을 흘리면서 구글링하던 그 새벽을 잊을 수가 없습니다. 하지만 원인을 하나씩 추적해보니 결국 메모리 관리 문제였습니다.
EC2 OOM Kill은 소규모 인스턴스를 운영하는 사람이라면 누구나 한 번쯤 겪는 통과의례입니다. Swap 설정과 MySQL 메모리 튜닝은 선택이 아니라 필수라는 걸 몸으로 배웠습니다.
그런데 여기서 끝이 아닙니다. "도대체 왜 갑자기 메모리가 부족해진 거지?"라는 의문이 남아있었는데, 2부에서 Apache 로그를 뒤져보니 feroxbuster라는 취약점 스캐너가 서버를 죽인 진짜 범인이었습니다. 2부에서는 공격 IP 차단, 스캐너 UA 일괄 차단, SSL 인증서 적용, GA4 연동까지 다룹니다.
Swap과 OOM Kill에 대한 더 자세한 설명은 Linux 커널 공식 메모리 관리 문서에서 확인할 수 있습니다.
서버 운영 관련 다른 글도 참고하세요.