ssuperjun 님의 블로그
[장애 이력 자동 작성 도구 17] 최종 발표 자료 본문
프로젝트 기간: 3주(실제 소요 기간 11일)
역할: 프로젝트 기획, Redis 및 MySQL 장애 연출, 장애 수집-분석 스크립트 작성, 최종 산출물을 사내 메신저에 등록
협업 사항: 프로젝트 기획, input 및 output 선정 시 은빈 인턴님과 협업해 결정
[1]프로젝트 제목: 장애 이력 자동 작성 도구
[2]목적
- DB 담당자가 장애 알림을 받으면, 원활한 서비스를 위해 DB 정상화를 최우선으로 진행
- 추후 장애 이력 보고 시 장애 이력을 시간 순서대로 정리해 놓은 타임라인이 필요

한 장애가 발생했을 때 장애 이력 보고에 들어갈 항목: 23가지(장애 관리팀 & 장애 담당자)
=> 이 '장애 이력 자동 작성 도구'가 없다면, 장애가 발생한 DB의 로그를 일일이 확인해 타임라인을 정리해야 함
[3]실행 환경
배경: 로컬 맥북 -> hcon 서버 -> ca801(MySQL 마스터, Redis 슬레이브)/cb801(MySQL 슬레이브, Redis 마스터) 서버에 접속

실제 환경 유추

방법1: DB와 다른 망에서 스크립트 실행: 스크립트 실행 ip가 폐쇄망과 연결 가능해야 함(ACL 등록 필요)

방법2: DB와 같은 망에서 스크립트 실행: 폐쇄망 내 스크립트 실행 ip가 두레이 API ACL에 등록되어야 함
방법1 채택
폐쇄망의 ip를 두레이 API ACL에 등록하기 어렵다고 판단
구현 고려사항
- DB가 설치된 cb801에서 스크립트를 실행하면, 두레이 태스크 등록 없이 타임라인 출력
- hcon(DB가 설치된 서버 외부)에서 스크립트를 실행하면, 타임라인 출력 및 두레이 태스크 등록
[4]실행 흐름
- 담당자가 장애 알람 인지
- 담당자가 스크립트 실행(input = 장애가 발생한 서버 hostname)
- 해당되는 DB 서버에서 로그 수집
- Output 출력 및 두레이 태스크 등록
- DB 다운, 마스터-슬레이브 간 복제 중단 등 중요 알람이 뜨면 담당자는 '사후 보고용 타임라인 작성'이 필요하다고 판단 -> 정상화 후 스크립트 실행
- 스크립트는 정상화 이후 한번만 실행할 목적으로 만들어짐
- (장애 발생 때 한번 실행해놓은 뒤 장애 정상화 시점까지 백그라운드에서 장애 정상화 여부를 주기적으로 polling할 경우, 이미 장애가 발생한 서버에 부하를 줌)
- 물론 정상화 이전에 스크립트를 실행해도 타임라인이 생성되도록 만들었으며, 스크립트 재실행 권장 메시지를 출력함
장애 알람 중요도 구분
[Redis]
1. 직관적인 DOWN 증상(매우 중요): Redis down, 시스템 다운, Redis port 다운(접속 불가)
2. STOP 및 DOWN으로 이어지는 증상(중요): 디스크 error, reject된 커넥션, Missing Master, Cluster Flapping(클러스터 내의 특정 노드(서버)나 서비스가 정상과 비정상 상태를 아주 짧은 시간 동안 끊임없이 반복해서 오가는 현상)
3. 마스터-슬레이브 간 문제: 복제 중단
*아래 2가지 알람이 뜨면 담당자가 직접 판단해서 필요한 상황에만 자율적으로 스크립트 실행
1. 성능 저하(심각하면 DOWN으로 이어짐): CPU 사용률, Memory 사용률, 디스크 가용공간, 디스크 Inode 사용량, 디스크 사용률, 커넥션 사용량, 메모리 단편화율, Redis 메모리 사용률
2. 지금 당장의 장애는 아니지만, 장애 발생 시 무방비 상태(사후 분석 필수): Sentinel port 다운, Redis exporter 다운, Node exporter 다운
*exporter 다운: 프로메테우스가 해당 DB 상태(CPU, 메모리, 디스크 정보 등)를 가져가지 못함. 즉각적인 장애는 아니지만, 실제 장애 상황일 때 프로메테우스가 장애를 인지하지 못하는 상황이 벌어질 수 있음
장애는 아님
1. Sentinel Failover: 마스터에 장애가 발생. 그래도 failover가 잘 수행되면 괜찮음
2. 삭제된 keys: 데이터 삭제는 장애로 볼 수 없음. 다만 Redis가 꽉 차서 캐시가 지워지는 eviction 경우라면 문제가 될 수 있지만 이는 위의 Redis 메모리 사용률 등으로도 충분히 파악 가능
[MySQL]
시스템 다운, MySQL 재시작: 매우 중요
디스크 가용공간, 디스크 Inode 사용량, 디스크 error, 커넥션 사용량, DB 접속 불가: DB 서비스 먹통과 관련
복제 지연, 복제 중단: 마스터-슬레이브 간 장애
CPU 사용률, Memory 사용률, 디스크 사용률, 슬로우쿼리, InnoDB Buffer Pool hit ratio, Long 쿼리, Long 트랜잭션, 쿼리 지연: 성능 저하와 관련
MySQL exporter 다운, Node exporter 다운: 프로메테우스가 해당 DB 상태(CPU, 메모리, 디스크 정보 등)를 가져가지 못함. 즉각적인 장애는 아니지만, 실제 장애 상황일 때 프로메테우스가 장애를 인지하지 못하는 상황이 벌어질 수 있음
MMM 롤체인지: DB 서버의 역할이 바뀌었다 = 마스터가 죽었다는 뜻
MMM VIP ping check: MMM은 가상 IP를 DB 서버에 띄워서 애플리케이션이 접속하도록 하는데, 이 알람은 앱이 접속할 문(VIP)가 사라진 경우 발생(서비스 접속 불가)
MMM 모니터 Active: MySQL 서버들의 감시자인 MMM이 죽음. 즉각적인 장애는 아니지만, 마스터가 죽게 되면 failover(슬레이브 승격) 불가능
참고: MMM은 MySQL 서버들의 상태를 감시함. 마스터가 죽으면 슬레이브를 승격시키는 failover를 실행하고, 가상 IP(VIP)를 이동시켜 지속적인 서비스를 제공함
=> 담당자는 스크립트를 실행해야 할 주요 장애 상황 여부를 결정
[5]실행 테스트
Redis 실행 명령어
# 싱글 구성
python3 collect_redis_incident.py # 로컬 실행 (두레이 등록 생략)
# 원격 실행
python3 collect_redis_incident.py user@hostname --tunnel # 두레이 SSH 터널 경유
python3 collect_redis_incident.py user@hostname --no-dooray # 두레이 등록 생략
# Sentinel 구성
python3 collect_redis_incident.py user@hostname --sentinel /path/to/sentinel.log --tunnel
python3 collect_redis_incident.py user@hostname --sentinel --tunnel # conf의 log_path 사용
python3 collect_redis_incident.py user@hostname --sentinel --no-dooray
# 복제 구성 (마스터 + 슬레이브)
python3 collect_redis_incident.py user@master --slave user@slave --tunnel
python3 collect_redis_incident.py user@master --slave --tunnel # conf의 slave_host 사용
python3 collect_redis_incident.py user@master --slave --no-dooray
옵션:
--tunnel 두레이 API를 SSH 리버스 터널(localhost:19999)로 중계
(사전 준비: 맥북에서 ssh -N -R 19999:api.dooray.com:443 user@실행서버)
--no-dooray 두레이 태스크 등록 생략 (로컬 실행 시 자동 적용)
--sentinel Sentinel 로그 파일 기반 수집 모드
--slave 복제 구성 수집 모드 (슬레이브 호스트 지정 또는 conf 참조)
MySQL 실행 명령어
# 싱글 구성 (두레이 등록 생략)
python3 collect_mysql_incident.py irteamsu@infa-testsrv-ca801 --no-dooray
# 싱글 구성 (두레이 SSH 터널 사용)
python3 collect_mysql_incident.py irteamsu@infa-testsrv-ca801 --tunnel
# 복제 구성 (두레이 등록 생략)
python3 collect_mysql_incident.py irteamsu@infa-testsrv-ca801 --replica --no-dooray
# 복제 구성 (두레이 SSH 터널 사용)
python3 collect_mysql_incident.py irteamsu@infa-testsrv-ca801 --replica --tunnel
--slave 옵션(redis), --replica(mysql) 사용 이유: 마스터에서 슬레이브(레플리카)의 uuid 38자리까진 알아낼 수 있어도, ip주소나 hostname은 알아내지 못함
--sentinel 옵션(redis) 사용 이유: Sentinel 구축 및 장애 연출은 생략(서버 최소 3개 필요, ACL 등록 필요), redis_의도된_shutdown.txt 파일에 정적으로 기록된 로그 내용을 읽어 오는 형태로 구현
*두레이 태스크 등록 시, 로컬 맥북에서 실행
ssh -N -R 19999:api.dooray.com:443 bjpark00@hcon.nhnent.com
실행 결과
장애 복구 전 스크립트 실행

장애 복구 후 스크립트 실행(기존에 작성된 두레이 태스크 업데이트)


[6]실행 부가 설명
SSH 터널 (두레이 API 사용 시) 열어둔 이유: 두레이 API ACL에 로컬 맥북의 ip만 등록되어 있기 때문
context
- 로컬 맥북에서 hcon으로 ssh 접속이 가능하고, hcon에서 ca801/cb801로 ssh 접속이 가능한 상황
- 스크립트를 hcon에서 실행하면 코드 상에 localhost:19999로 요청하는 부분이 존재
요청이 두레이까지 흐르는 모습
hcon:19999 → (SSH 터널) → 맥북:임시포트 → api.dooray.com:443
SSH 터널이 수립될 때, 맥북이 내부적으로 소켓을 열어서 트래픽을 중계
터널 경유 방식 채택 이유
다른 아이디어: JSON 파일을 hcon 서버에서 output으로 만든 뒤, 이 JSON 파일을 로컬 맥북으로 옮겨 맥북에서 직접 두레이 태스크 등록
JSON 방식 단점: 관리해야 할 스크립트가 2개가 됨
[7]실행 가이드(환경별 config 변경사항)
슬레이브(레플리카) 서버 정보의 경우, 장애 난 마스터 DB 서버에 맞춰 conf 내용을 수정해야 함 -> --slave 옵션에 user@slave를 직접 입력하는 것으로 해결했지만 추후 고도화 필요
collect_redis_incident.conf 예시
[dooray]
# 두레이 개인 API 인증 토큰 (개인설정 > API > 개인 인증 토큰)
token = 생략
# 태스크를 등록할 프로젝트 ID
project_id = 생략
# 두레이 API base URL (민간: https://api.dooray.com / 공공: https://api.gov-dooray.com)
base_url = https://api.dooray.com
# SSH 리버스 터널 URL (맥북에서 ssh -N -R 19999:api.dooray.com:443 으로 터널 열기)
tunnel_url = https://localhost:19999
[redis]
# 모니터링할 systemd 서비스명
# - Redis 7.0 (systemd 등록): service = redis-server
# - Valkey (systemd 등록): service = valkey-{포트번호} 예) valkey-10379
# ※ 싱글/복제 구성의 journalctl 수집 및 [STARTED]/[STOPPED] 이벤트 감지에 사용됨
service = redis-server
# 정상화 판단 기준 (초): 마지막 재시작 후 이 시간 동안 kill 없으면 정상화로 판단
recovery_stable_secs = 30
[sentinel]
# Sentinel 로그 파일 경로 (--sentinel 옵션에서 경로 생략 시 사용)
log_path = /home1/irteam/redis_의도된_shutdown.log
[replication]
# 슬레이브 서버 접속 정보 (--slave 옵션에서 슬레이브 호스트 생략 시 사용)
slave_host = irteamsu@infa-testsrv-ca801
# 마스터 redis/valkey 로그 파일 경로
# - Redis 7.0: /var/log/redis/redis-server.log
# - Valkey: /home1/irteam/valkey_{포트번호}/valkey_{포트번호}.log 예) /home1/irteam/valkey_10379/valkey_10379.log
master_log_path = /var/log/redis/redis-server.log
# 슬레이브 redis/valkey 로그 파일 경로
# - Redis 7.0: /var/log/redis/redis-server.log
# - Valkey: /home1/irteam/valkey_{포트번호}/valkey_{포트번호}.log 예) /home1/irteam/valkey_10379/valkey_10379.log
slave_log_path = /var/log/redis/redis-server.log
[8]Input 선정 이유
Input(실행 명령어) 예시: python3 스크립트명 hostname(장애가 발생한 DB 서버)
*서버 내에서 직접 실행일 땐 hostname을 입력하지 않아도 동작하도록 구현
[이유]
- 핵심: 실행 명령어가 사용자(장애 담당자) 입장에서 복잡하지 않아야 함
- 아이디어1(알람 내용을 그대로 복사 붙여넣기) 반려: 실제론 알림을 복사 붙여넣기 하기 어려움, 복붙이 되더라도 오타 처리 및 알림 형식 파싱 복잡
- 아이디어2(장애 결과(ex. MySQL Not Reachable, Redis Port Down)를 input에 넣기) 반려: 사용자의 편의 저하에 비해 코드 성능이 그렇게 좋아지는 것은 아님(사후 보고용 리포트 작성이 목적이므로 신속하게 처리되지 않아도 됨)
- 아이디어3(input에 '로그를 수집하고 싶은 시간' 넣기) 반려: 장애가 중복 또는 자주 발생하지 않는다는 점을 고려하면 시간을 입력하는 건 사용자 입장에서도 번거로움, '시간'을 input으로 넣어서 과거 로그를 확인하는 기능 필요없음
- => 대신, 하루에 여러 장애가 발생했을 때, '이 로그가 어느 장애와 관련된 로그인지' 장애 묶음을 구분하도록 함
[9]Output 선정 이유
장애 이력 보고에 들어갈 항목 23가지 중
- 반드시 넣을 부분
- 장애해결 이후에 작성될 부분
- 장애관리팀 작성 영역이지만 output에 넣으면 좋을 부분
- 굳이 들어가야 되나 싶은 부분
- 구현 가능할지 모르겠는 부분
으로 항목을 구분한 뒤
- 4,5번 제외
- 담당자가 로그 확인 없이도 쉽게 작성할 부분 제외
- 핵심 항목만 남기기
최종 Output 형식 예시
장애 지속 시간 : 9분 48초 (발생 → 해결)
발생 시각 : 2025-03-15 13:19:12
해결 시각 : 2025-03-15 13:29:00
장애 발생 원인 : OOM Kill
장애 타임라인
13:19:12 [OOM] Feb 27 13:19:12 kernel: Out of memory: Kill process 1234
13:21:00 [OOM] Feb 27 13:21:00 kernel: oom-killer invoked
13:21:45 [OOM] Feb 27 13:21:45 kernel: Killed process 5678 (mysqld)
13:22:30 [OOM] Feb 27 13:22:30 kernel: Out of memory: Kill process 9012
13:28:30 [정상화] 2024-02-27T13:28:30.123456Z mysqld: ready for connections
13:29:00 [정상화] 2024-02-27T13:29:00.789012Z mysqld: restarted successfully
[10]장애 상황 연출
코드 구현을 위해, 장애 상황을 모두 연출 -> 로그 패턴 분석 -> 로그 수집 및 output 산출 스크립트 작성
DB의 장애 상황은 크게 5가지 상황 내에서 일어난다고 분류함(DB 다운 및 stop 등 장애에 영향을 미치는 요인 중 장애 연출이 가능한 요인 추림)
1. OOM killed(메모리 full)
2. 디스크 full 및 병목
3. CPU 병목
4. Too many connections
5. 내부 로직 segfault
1. Redis OOM 장애 시뮬레이션: https://ssuperjun.tistory.com/55
- => maxmemory 아주 작게 설정, 메모리 폭탄 투하 -> journalctl 로그에서 OOM kill 발생 확인
- 특이사항: systemd에 등록된 Redis는 oom-kill 시 자동 재시작 수행 - 메모리 폭탄 투하 코드가 계속 진행 중이면 ALIVE와 DEAD를 왔다갔다함(새 pid 생성)
- 시행착오: Swap 메모리 비활성화(메모리 limit을 초과하면 OS가 데이터를 RAM 대신 Swap 메모리로 밀어내는 문제 해결)
2. Redis 디스크 full 시뮬레이션: https://ssuperjun.tistory.com/64결론: 디스크 full 장애 감지 스킵 - 회사에선 Redis를 데이터 저장 용도로 사용하지 않음
- => Redis dir 경로를 용량이 작은 tmpfs(임시 데이터 저장 장소)로 바꾸고 dd 명령어로 실제 디스크를 모두 채우기
- 결론: 디스크 full 장애 감지 스킵 - 회사에선 Redis를 데이터 저장 용도로 사용하지 않음
3. Redis Too many connections 시뮬레이션: https://ssuperjun.tistory.com/65
- => maxclients를 낮게 설정(예: 5)한 뒤 연결을 폭주시키기
- Too many connections 감지 스킵 이유: Redis가 살아있을 때만 내부에서 확인 가능한 기록. Redis 재시작 시 휘발됨
- CPU 병목 장애 감지 스킵 이유: 이하 동일
4. Redis Segfault 장애 시뮬레이션: https://ssuperjun.tistory.com/66
=> SIGSEGV(kill -11) 직접 전송(gdb(프로세스에 붙어 상태 확인이나 강제 오류 발생 디버거)로 실제 segfault 유발 방식보다 간단, 로그 패턴은 동일하게 찍힐 것으로 예측)
5. Redis Sentinel 구조 로그 패턴 분석: https://ssuperjun.tistory.com/67
=> redis_의도된_shutdown.log 원본 로그 파일을 파싱하는 형태로 구현
[더 알아본 내용]
sdown과 odown에서 각각 s와 o의 의미
- sdown = Subjectively Down. 특정 Sentinel 하나가 마스터에 응답이 없다고 주관적으로 판단한 상태. 한 명의 Sentinel만 이상하다고 느끼는 단계
- odown = Objectively Down. quorum 수 이상의 Sentinel이 동의하여 객관적으로 다운으로 판단한 상태. 이 단계에서 페일오버가 트리거됨
[NEW_EPOCH]: Sentinel 클러스터 내 페일오버 시도가 시작될 때 epoch(판 번호)를 증가시키는 이벤트. 여러 Sentinel이 동시에 페일오버를 시도하는 것을 방지하기 위한 분산 합의 메커니즘으로, epoch가 높은 쪽이 우선권을 가짐
[CONFIG_UPDATE]: 다른 Sentinel이 페일오버를 완료하고 새 마스터 정보를 브로드캐스트했을 때, 이를 수신하여 자신의 설정을 업데이트했다는 이벤트. 즉 이 로그의 Sentinel은 페일오버를 직접 수행한 게 아니라 결과를 전달받은 것
[궁금증 해결]
"로그에서 failover보다 +sdown이 먼저 발생해야 하지 않나?"
로그에서 failover가 +sdown(센티널의 마스터 죽음 인지)보다 먼저 발생한 이유
- 이 로그는 이 Sentinel 노드(.39)의 시각으로 기록된 것임
- 페일오버는 다른 Sentinel(.38)이 수행했고, 이 Sentinel은 .38로부터 +config-update-from으로 결과를 전달받아 02:58:46에 마스터 전환을 인지
- 이후 구 마스터(.38)와 Sentinel(.38)이 새 마스터(.37) 기준으로 응답이 없어서 03:01:13과 03:01:35에 +sdown이 찍힘
=> 즉 페일오버 완료 후 구 마스터가 슬레이브로 재편입되는 과정에서 일시적으로 +sdown이 발생한 정상적인 흐름임
failover 전 Sentinel 구성 상황 정리
("+switch-master tfw 10.x.x.38 10379 10.x.x.37 10379" 로그 기반 추론)

여기서 .38의 마스터 DB가 다운되자 .38의 Sentinel이 페일오버를 수행해서 .37의 replica를 새 마스터로 승격시킴
페일오버 후에는 구 마스터였던 .38:10379가 새 마스터 .37의 슬레이브로 재편입을 시도하는 과정에서 +sdown/-sdown/+reboot이 찍히게 됨
Sentinel 구조의 장점(Replication에 비해)
Replication의 경우, master 장애로 인한 failover 시 replica에서 직접 master 연결 끊기 필요, 애플리케이션 연결 재설정도 필요
Sentinel 구조의 경우, 연결 재설정을 수동으로 할 필요 없음
6. Redis 복제 구조 장애 시뮬레이션: https://ssuperjun.tistory.com/68
- => cb801내 DB를 마스터로, ca801 내 DB를 슬레이브로 설정(마스터의 redis.conf를 수정)
- => 마스터 OOM Kill(maxmemory 아주 작게 설정, 메모리 폭탄 투하), 네트워크 차단 장애(마스터에서 슬레이브의 연결을 강제로 끊기) 연출
[궁금증 해결]
복제 구성에서 bgsave가 발생하는 이유
- save ""로 디스크 저장용 RDB는 비활성화했지만, 슬레이브 Full resync 시에는 마스터가 현재 데이터를 슬레이브에 전송하기 위해 별도로 BGSAVE를 수행함
- 이 bgsave는 save 설정과 무관하게 동작함
- 이번 로그에서 Starting BGSAVE for SYNC with target: replicas sockets라고 찍힌 것이 바로 이 경우에 해당함
- replicas sockets는 디스크에 파일을 쓰지 않고 RDB 데이터를 소켓으로 직접 슬레이브에 스트리밍하는 diskless replication 방식임
- 이 BGSAVE는 fork()로 자식 프로세스를 생성하는데, fork 순간 부모 프로세스의 메모리를 복사하므로 메모리 사용량이 일시적으로 급증해서 다시 OOM Kill이 발생함
- diskless replication을 비활성화(repl-diskless-sync no)하면 디스크에 RDB 파일을 먼저 쓴 후 전송하는데, 이 경우 fork 시 메모리 부담은 줄어들지만 디스크 I/O가 발생함. 어느 쪽이든 MemoryLimit이 낮으면 OOM Kill 반복은 피하기 어려움
MySQL 장애 연출: https://ssuperjun.tistory.com/72
각 장애마다 연출 → 로그 확인 → 패턴 확정 → 코드 반영 사이클을 반복
싱글 구성 (ca801)
| 순서 | 장애 유형 | 연출 방법 |
| 1 | 재시작 (정상) | mysqladmin shutdown 후 재시작 — 기준선 확보 |
| 2 | OOM Kill | stress-ng 또는 INSERT 루프로 메모리 고갈, vm.overcommit_memory 조정 |
| 3 | Too Many Connections | max_connections=5 설정 후 다수 연결 |
| 4 | 디스크 Full | fallocate로 디스크 채우기 |
| 5 | Segfault | kill -SIGSEGV {mysqld_pid} |
*Too many connections 장애만 스킵: error log에 흔적이 안남고, 이 장애는 DB 자체 문제보단 애플리케이션 레벨 문제라 판단
복제 구성 (ca801 마스터 → cb801 레플리카)
| 순서 | 장애 유형 | 연출 방법 |
| 6 | 복제 연결 단절 (마스터 재시작) | ca801에서 mysqld 재시작 |
| 7 | 복제 연결 단절 (네트워크 단절) | iptables로 ca801↔cb801 포트 차단/해제 |
| 8 | 마스터-레플리카 간 데이터 정합성 장애 유발 | 레플리카에서 직접 데이터를 수정한 뒤 마스터에서 동일 row를 건드려 충돌 유발 |
| 9 | 레플리카의 디스크 Full 장애 유발 | 마스터의 데이터를 레플리카가 동기화하는 과정에서 레플리카의 디스크 용량을 부족하게 설정 |
[11] Redis 구현 내용
11-1. 싱글 구성
수집할 DB: redis-server(7.0버전), valkey(7.2버전 이상)
수집할 로그: journalctl
journalctl 수집 이유
- OOM kill 등 장애를 감지하는 건 error log보다 journalctl이 간편(직접적인 oom-kill 메시지는 journalctl에 남지만 error log에는 남지 않음)
- dmesg(간단한 커널 로그)로 OOM kill 등의 장애를 감지할 수 있지만, journalctl보다 장애 발생 시각을 정확히 탐지하지 못함
주의사항
- Valkey는 systemd에 자동으로 등록되지 않음
- journalctl 수집 방식을 적용하기 위해, Valkey를 systemd에 등록함
- 실제 환경의 DB가 systemd에 등록되지 않은 경우를 고려해, MySQL 구현 시엔 journalctl이 아닌 mysql.err 로그를 수집함
redis-server(7.0버전)과 valkey의 journalctl 로그 형태가 동일한가?
다른 점
- 1. 프로세스 이름: Redis 7.0은 redis-server로 찍히지만, Valkey는 valkey-{포트 번호}로 찍힘
- 2. 메시지 키워드 형태
- Redis 7.0: master
Valkey: primary
그외 나머지 로그 형태는 Redis/Valkey 무관하게 동일
발생 시각 기준: 가장 최근 장애 묶음의 첫 번째 kill 또는 stop 이벤트 시각
- 장애 묶음 경계: 직전 [STARTED] → 다음 kill 또는 stop 간 gap이 30초 이상이면 새 묶음으로 판단
| 원인 | 감지 키워드 | 태그 |
| OOM Kill | Failed with result 'oom-kill' | [OOM_KILL] |
| Segfault | Failed with result 'core-dump' | [SEGFAULT] |
| Signal Kill | Failed with result 'signal' | [KILL_SIGNAL] |
| 비정상 종료 - {result값} (원인 수동 확인 필요) | Failed with result 'exit_code 또는 timeout 등' | [FAILED_EXIT_CODE], [FAILED_TIMEOUT] |
| 미확인 (로그 수동 확인 필요) | Failed with result 자체가 없는 경우(db 내부에서 강제 stop하는 경우는 여기에 해당) |
기타 이벤트 태그
| 태그 | 감지 키워드 |
| [STARTED] | Started {redis-server 또는 valkey-10379} |
| [STOPPED] | Stopped {redis-server 또는 valkey-10379} |
| [RESTART] | Scheduled restart job |
| [RESTART_THROTTLE] | Start request repeated too quickly |
정상화 판단: 마지막 [STARTED] 이후 30초 동안 kill 이벤트 없음
30초 선정 이유
- 로그에 완벽한 정상화를 판단할 수 있는 기록이 없음, 시간 gap으로 정상화를 감지하는 게 구현 난이도 측면에서 유리
- 전제: systemd는 Redis가 죽으면 자동 재시작을 진행함
- OOM Kill 직후 systemd가 redis를 재시작하는 데 걸리는 시간은 로그 상 1~3초
- 재시작 직후 메모리 채우기 스크립트(Insert 작업)가 남아있으면 보통 2~5초 내에 다시 kill됨
- => 장애 상황에서는 kill 이벤트 간격이 최대 10초 수준이므로, 넉넉잡아 30초 동안 kill이 없으면 정상화로 판단해도 무방
11-2. 복제 구성
수집 소스: 마스터 journalctl + 마스터/슬레이브 /var/log/redis/redis-server.log
11-2-1. 마스터 OOM Kill / Segfault / Signal Kill
발생 시각 기준: 가장 최근 장애 묶음의 첫 번째 kill 또는 stop 이벤트 시각
- 장애 묶음 경계: 연속된 kill/conn_lost 이벤트 중 직전 이벤트와의 gap이 30초 이상이면 새 묶음으로 판단
| 원인 | 감지 키워드 | 태그 |
| OOM Kill | Failed with result 'oom-kill' (마스터 journalctl) | [OOM_KILL] |
| Segfault | Failed with result 'core-dump' (마스터 journalctl) | [SEGFAULT] |
| Signal Kill | Failed with result 'signal' (마스터 journalctl) | [KILL_SIGNAL] |
슬레이브 측 태그
| 태그 | 감지 키워드 |
| [CONN_LOST] | Connection with master lost |
| [FULL_RESYNC] | Full resync from master |
| [SYNC_FAIL] | I/O error trying to sync with MASTER |
| [SYNC_OK] | MASTER <-> REPLICA sync: Finished with success |
| [PSYNC_OK] | Successful partial resynchronization |
마스터 측 태그
| 태그 | 감지 키워드 |
| [REPLICA_CONN] | Synchronization with replica .* succeeded |
정상화 판단: 마지막 [REPLICA_CONN] 또는 [SYNC_OK]/[PSYNC_OK] 이후 30초 동안 [CONN_LOST] 없음
11-2-2. 네트워크 단절
발생 시각 기준: 가장 최근 장애 묶음의 첫 번째 [CONN_LOST] 이벤트 시각
- 장애 묶음 경계: 연속된 [CONN_LOST] 이벤트 중 직전 이벤트와의 gap이 30초 이상이면 새 묶음으로 판단 (kill 이벤트 없음)
| 원인 | 감지 조건 | 태그 |
| 복제 연결 단절 | kill 이벤트 없이 Connection with master lost 반복 (슬레이브 로그) | [CONN_LOST] |
정상화 판단: 마지막 [PSYNC_OK] 또는 [SYNC_OK] 이후 30초 동안 [CONN_LOST] 없음
gap이 30초인 이유: 싱글 구성과 동일(OOM Kill 후 재시작까지 수초, BGSAVE(슬레이브 동기화를 위한 rdb 구성 위해 필요) fork 후 동기화까지 수초 수준이므로)
11-3. Sentinel 구성
수집 소스: Sentinel 로그 파일
발생 시각 기준: 장애 유형에 따라 다름
- Failover 발생 시: 직전 +new-epoch 또는 +odown 중 이른 시각 (없으면 직전 +sdown fallback)
- Failover 없이 +odown만 있을 시: 가장 최근 +odown 시각
- +sdown만 있을 시: 가장 최근 +sdown 시각
| 원인 | 감지 조건 |
| Sentinel Failover | +switch-master 이벤트 존재 |
| Sentinel Failover 실패 | +switch-master 없고 -failover-abort-* 존재 |
| Sentinel 장애 | 위 둘 다 없고 +sdown/+odown 존재 |
태그 정리
| 태그 | 감지 키워드 |
| [SDOWN] | +sdown |
| [SDOWN_RECOVER] | -sdown |
| [ODOWN] | +odown |
| [ODOWN_RECOVER] | -odown |
| [FAILOVER] | +switch-master |
| [FAILOVER_ABORT] | -failover-abort-* |
| [REBOOT] | +reboot |
| [NEW_EPOCH] | +new-epoch |
| [CONFIG_UPDATE] | +config-update-from |
정상화 판단: 마지막 복구 이벤트(-sdown/-odown)가 마지막 장애 이벤트 이후에 존재
=> -sdown/-odown은 센티널이 '마스터가 살아있음'을 보증해준다고 판단해 gap을 따로 설정하지 않음
공통 기능
| 기능 | 설명 |
| 두레이 태스크 자동 등록 | 장애 발생 시각 기준 중복 방지 (PUT 업데이트) |
| 두레이 태스크 상태 자동 설정 | 정상화 판단 시 완료, 진행 중이면 할일 |
| 민감정보 분리 | 두레이 토큰, 슬레이브 호스트 등 conf 파일로 관리 |
그외 코드 구현 시 고려한 부분
1. run 함수(명령어를 로컬 또는 ssh원격으로 실행하고 stdout 반환) 예외처리 로직 개선: 명령 타임아웃(30초) 초과, 명령 권한 오류, 빈 결과 시 예외처리
2. 날짜 경계 문제(23시->01시, 12월31일->1월1일) 해결
[12] MySQL 구현 내용
12-1. 로그 수집 위치
로그 경로: /home1/irteam/mysql/log/mysql.err (ca801/cb801 공통)
이유
재시작 시 휘발되지 않는 기록만을 수집하고자 함(SHOW ENGINE INNODB STATUS 등 런타임 통계는 스킵)
MySQL도 Valkey처럼 systemd로 관리되지 않고 직접 실행되어 journalctl 로그가 없음
12-2. 로그 수집 항목
싱글 구성 (ca801 단독)
| 장애 유형 | 수집 여부 | 수집 로그 | 스킵 사유 |
| OOM Kill / kill -9 | ✅ | ca801 error log | |
| Segfault | ✅ | ca801 error log | |
| 비정상 종료 (Aborting) | ✅ | ca801 error log | |
| 디스크 Full | ✅ | ca801 error log | 단, 디스크가 완전 소진되어 로그가 잘린 경우는 수집 불가 |
| Too Many Connections | ❌ | — | log_error_verbosity=2 기본값에서 error log 미기록 |
복제 구성 (ca801=마스터, cb801=레플리카)
| 장애 유형 | 수집 여부 | 수집 로그 | 스킵 사유 |
| 마스터 비정상 종료 (Aborting) | ✅ | ca801 + cb801 error log 통합 | |
| 마스터 OOM Kill / kill -9 | ✅ | ca801 + cb801 error log 통합 | |
| 마스터 디스크 Full | ✅ | ca801 + cb801 error log 통합 | |
| 복제 연결 단절 | ✅ | ca801 + cb801 error log 통합 | |
| 레플리카 디스크 Full | ✅ | ca801 + cb801 error log 통합 | |
| 데이터 정합성 충돌 (레플리카의 데이터를 마스터와 동기화하는 상황) |
❌ | — | error log 미기록 확인 (실제 연출로 확인) |
12-3. 장애 감지 키워드, 발생 시각 기준 정리
싱글 구성 (_SINGLE_EVENT_PATTERNS)
| 태그 | 감지 키워드 | 발생 시각 기준 |
| [STARTING] | mysqld_safe Starting mysqld daemon, [MY-010116] | — |
| [INNODB_READY] | [MY-013577] | — |
| [XA_RECOVERY] | [MY-010229] | — |
| [XA_RECOVERY_END] | [MY-010232] | — |
| [READY] | [MY-010931] | — |
| [SAFE_RESTARTED] | mysqld_safe mysqld restarted | — |
| [SAFE_ENDED] | mysqld_safe mysqld from pid file.*ended | — |
| [CRASH_DETECTED] | mysqld_safe Number of processes running now: 0 | fault_events 중 첫 번째 시각 |
| [ABORTING] | [MY-010119] | fault_events 중 첫 번째 시각 |
| [SEGFAULT] | handle_fatal_signal | fault_events 중 첫 번째 시각 |
| [DISK_FULL] | [MY-000035], [MY-011072] | fault_events 중 첫 번째 시각 |
발생 시각: {segfault, aborting, oom_or_crash, disk_full} 중 첫 번째 이벤트 시각
복제 구성 _REPL_EVENT_PATTERNS
| 태그 | 감지 키워드 | 서버 | 발생 시각 기준 |
| [CONN_LOST] | [MY-010557], [MY-010558] | replica | fault_events 중 첫 번째 시각 |
| [RECONNECTING] | [MY-010584] (I/O 스레드) | replica | fault_events 중 첫 번째 시각 |
| [CONNECTED] | [MY-010592] | replica | — |
| [REPL_START] | [MY-014001] | replica | — |
| [REPLICA_DISK_FULL] | [MY-012144], [MY-012639], [MY-012640], [MY-013132] | replica | fault_events 중 첫 번째 시각 |
| [STARTING] | [MY-010116] | master | — |
| [ABORTING] | [MY-010119] | master | fault_events 중 첫 번째 시각 |
| [CRASH_DETECTED] | mysqld_safe Number of processes running now: 0 | master | fault_events 중 첫 번째 시각 |
| [SAFE_RESTARTED] | mysqld_safe mysqld restarted | master | — |
| [READY] | [MY-010931] | master | — |
발생 시각: {oom_or_crash, aborting, conn_lost, reconnecting, replica_disk_full} 중 첫 번째 이벤트 시각
12-4. 태그 전체 목록 정리
| 태그 | 구성 | 설명 |
| [STARTING] | 싱글/복제 공통 | mysqld 기동 시작 |
| [INNODB_READY] | 싱글 | InnoDB 초기화 완료 |
| [XA_RECOVERY] | 싱글 | XA crash recovery 시작 (비정상 종료 간접 증거) |
| [XA_RECOVERY_END] | 싱글 | XA crash recovery 완료 |
| [READY] | 싱글/복제 공통 | mysqld 완전 기동 완료 |
| [SAFE_RESTARTED] | 싱글/복제 공통 | mysqld_safe가 mysqld 재시작 |
| [SAFE_ENDED] | 싱글 | mysqld_safe 정상 종료 |
| [CRASH_DETECTED] | 싱글/복제 공통 | mysqld 프로세스 소멸 감지 |
| [ABORTING] | 싱글/복제 공통 | mysqld 스스로 기록한 비정상 종료 |
| [SEGFAULT] | 싱글 | signal handler 스택 트레이스 감지 |
| [DISK_FULL] | 싱글 | 마스터 binlog 쓰기 실패 |
| [CONN_LOST] | 복제 | 레플리카 I/O 스레드 연결 끊김 |
| [RECONNECTING] | 복제 | 레플리카 I/O 스레드 재연결 시도 |
| [CONNECTED] | 복제 | 레플리카 I/O 스레드 재연결 성공 |
| [REPL_START] | 복제 | 복제 스레드 시작 |
| [REPLICA_DISK_FULL] | 복제 | 레플리카 InnoDB 파일 쓰기 실패 |
| [정상화] | 싱글/복제 공통 | 정상화 판단 기준 이벤트 |
12-5. 정상화 판단 기준 정리
싱글 구성
| 장애 유형 | 기준 이벤트 | Gap | 근거 |
| OOM Kill, Segfault, Aborting |
[MY-010931] ready for connections | +60초 | 커넥션 풀 재접속(~30초) + 초기 부하 안정화 |
| 디스크 Full | [MY-010931] ready for connections | +120초 | 디스크 공간 미확보 시 재시작 후 즉시 동일 장애 재발 가능. InnoDB 이전 실패 쓰기 복구 시간 + 운영자 디스크 정리 후 안정화 구간까지 커버 |
- ready for connections 이후 crash가 재발하면 정상화 판단 보류
- gap 60초 미경과 시 '장애 진행 중' 출력 후 재실행 안내
crash류의 장애 정상화 기준을 60초로 정한 이유
- ready for connections는 InnoDB 초기화, XA crash recovery, TLS 설정 완료 후 찍힘
- 일반적으로 커넥션 풀의 reconnect timeout은 30초
- 재접속 완료(30초) 후 쿼리(부하)가 실행되기까지 수 초가 더 필요
- => 30초+a = 여유롭게 60초가 지나면 정상화됐다고 판단
디스크 Full의 장애 정상화 기준을 120초로 정한 이유
- 디스크 Full이 해소되지 않으면 재시작 후 즉시 동일 장애 재발 가능
- mysqld 재시작 후 InnoDB가 이전에 실패한 쓰기 작업을 복구하는 시간 필요
- 운영자가 디스크 정리 후 재시작하는 수동 개입 시간 필요
- 60초는 커넥션 풀 재접속 기준이지만, 디스크 Full은 공간 확보 확인까지 포함해야 함
복제 구성
| 장애 유형 | 기준 이벤트 | Gap | 근거 |
| 연결 단절 계열 | [MY-010592] connected to source 또는 [MY-014001] repl_start | +120초 | replica_net_timeout(60초) 1사이클 + 여유 60초 |
| 레플리카 디스크 Full (InnoDB 파일 쓰기 실패) | 마지막 [REPLICA_DISK_FULL] 이후 fault 없음 + gap 120초 경과 | +120초 | SQL 스레드만 중단되어 I/O 스레드 재연결이 정상화 증거가 되지 않음. 수동으로 START REPLICA SQL_THREAD 후 안정화 구간까지 커버 |
| 레플리카 디스크 Full (relay-bin 쓰기 실패) | 마지막 [RELAY_DISK_FULL] 이후 fault 없음 + gap 720초 경과 | +720초 | [MY-000035]는 600초(10분)마다 반복 출력됨. 600초 초과 시 공간 확보로 자동 재개된 것으로 판단 + 여유 120초 |
- 기준 이벤트 이후 120초 또는 720초 이내에 fault가 재발하면 정상화 판단 보류
- gap 미경과 시 '장애 진행 중' 출력 후 재실행 안내
12-6. 장애 원인 판단 우선순위
싱글: [DISK_FULL] > [SEGFAULT] > [ABORTING] > [CRASH_DETECTED] > 미확인
복제: [REPLICA_DISK_FULL] > [ABORTING](master) > [CRASH_DETECTED](master) > [CONN_LOST]/[RECONNECTING] > 미확인
=> [DISK_FULL] > [SEGFAULT] > [ABORTING] > [CRASH_DETECTED] 태그들이 모두 존재하는 장애 묶음이라면 disk_full을 장애 원인으로 보겠다는 의미
- 우선순위가 높을수록 장애 원인이 구체적임
- 디스크 Full 시 signal 6(SIGABRT)으로 handle_fatal_signal도 함께 찍히므로 [DISK_FULL]을 [SEGFAULT]보다 우선 판단
- [ABORTING], [CRASH_DETECTED] 등으로 장애를 판단했을 땐 mysql.err 로그만으론 장애의 원인이 구체적으로 oom-kill인지 알 수 없으므로 journalctl이나 dmesg 확인 필요
[13] 고도화 필요 부분
문제
레플리카의 디스크 Full 장애(MySQL 9번 장애)를 일으킨 뒤 8분 후에 복구를 시도하면, '레플리카의 디스크 Full'에 대한 타임라인이 정리되지 않고, 복구 이후의 '복제 연결 단절' 타임라인만 정리됨
원인
복구 시 로그에 [CONN_LOST], [REPL_START]가 찍힘. 현재 로직에선 [CONN_LOST]도 하나의 장애로 감지
해결 과정
디스크 full 에러 메시지([MY-000035])가 10분에 한번씩 출력되는 것을 확인 -> 넉넉잡아 12분 gap으로 디스크 full 정상화를 판단하는 것으로 로직을 수정했으나, 동일한 문제 지속
담당자가 '복제 연결 단절' 원인을 보고 로그를 수동으로 직접 확인해보면 금방 '디스크 Full'이 원인임을 알 수 있을 거라 판단하고 시간관계상 로직 수정을 보류
해결 아이디어
새로운 로그 패턴이 나올 때마다 스크립트의 로직을 일일이 수정해야 하는 문제 존재
LLM을 이용한 로그 패턴 학습으로 장애 묶음을 구분하는 건 어떨까? - 학습 데이터 확보 문제 존재
[14] 고도화 아이디어
1. 가장 최근에 발생한 장애 로그 묶음 '하나만' 타임라인을 정리하는 현재 방식 대신, 타임라인이 정리되지 않은 '모든 장애'를 타임라인으로 정리
- 실무에서의 필요성 확인 필요
2. 장애 발생 직후 1차적으로 스크립트를 실행(원인 분석 및 휘발성 상태 정보 보존 목적) -> 1차 실행 때 수집한 정보를 사후 2차 실행에서 재활용하기
- 디스크 full, cpu 병목, too many connections, 복제 지연 정보들도 수집할 수 있음
- 1차 정보 저장을 어떻게 할지, 어떻게 재활용할지 더 고민 필요
3. 수집 대상 DB에 주기적 로그 수집 방법 => 폐기
현재 방식은 정상화가 끝난 이후 스크립트를 실행해 로그를 한번만 실행하는 걸 고려한 상황
- 주기적인 로그 수집 방식의 장점: 1.장애 알람 기능 구현, 2.수집에 kafka 사용, 3.로그 패턴 분석에 AI 활용 등 프로젝트 사이즈가 커짐
- 주기적인 로그 수집 방식의 단점: 장애 빈도 낮고, 주기적 폴링 및 명령은 장애 난 DB에 부하를 주므로 폐기
'인턴' 카테고리의 다른 글
| [장애 이력 자동 작성 도구] 발표 이후 고도화 - 장애 감지 로직 수정 (0) | 2026.03.19 |
|---|---|
| [스터디4] 개발자를 위한 레디스 (0) | 2026.03.18 |
| [장애 이력 자동 작성 도구 16] Redis, MySQL 추가 고민 (0) | 2026.03.12 |
| [장애 이력 자동 작성 도구 15] MySQL 시행착오 (0) | 2026.03.12 |
| [장애 이력 자동 작성 도구 14] MySQL 장애 연출 (0) | 2026.03.12 |