Showing Posts From
슬로우
- 03 Dec, 2025
MySQL 슬로우 쿼리 로그와의 밤샘 전쟁
MySQL 슬로우 쿼리 로그와의 밤샘 전쟁 새벽 3시의 슬랙 알림 진동이 울렸다. 아내가 뒤척였다. 폰을 집어들었다. 슬랙이다. 프로덕션 알림 채널. [CRITICAL] Slow Query Detected Response Time: 8.3s Endpoint: /api/users/activity8초. 망했다. 잠옷 바람으로 거실로 나왔다. 노트북을 켰다. 부팅되는 동안 물을 마셨다. 손이 떨렸다. 커피 때문인지 떨림 때문인지 모르겠다. VPN 연결. 프로덕션 DB 접속. 슬로우 쿼리 로그를 열었다. SELECT u.*, ua.last_active, ua.page_views FROM users u LEFT JOIN user_activities ua ON u.id = ua.user_id WHERE u.created_at >= '2024-01-01' AND u.status = 'active' ORDER BY ua.last_active DESC LIMIT 100;평범해 보였다. 그게 더 문제다.EXPLAIN은 거짓말을 하지 않는다 EXPLAIN을 돌렸다. +----+-------------+-------+------+---------------+------+---------+------+--------+----------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+--------+----------+ | 1 | SIMPLE | u | ALL | NULL | NULL | NULL | NULL | 450000 | Using... | | 1 | SIMPLE | ua | ALL | NULL | NULL | NULL | NULL | 890000 | Using... | +----+-------------+-------+------+---------------+------+---------+------+--------+----------+type이 ALL. 둘 다. 45만 로우와 89만 로우를 풀스캔. 카르테시안 곱. 400억 건을 뒤진다는 소리다. "씨발." 소리가 나왔다. 작게. user_activities 테이블을 확인했다. 인덱스가 하나도 없다. user_id에도. last_active에도. 누가 만든 건지 git blame을 돌렸다. 6개월 전. 커밋 메시지 "Add user activity tracking". 커밋한 사람은 나. "아 씨." 이번엔 크게 나왔다. 6개월 전엔 데이터가 얼마 없었다. 몇천 건. 그때는 문제없었다. 근데 지금은 89만 건. 매일 3천 건씩 쌓인다. 프로덕션에서 터지기 전까진 아무도 몰랐다. 스테이징에 데이터가 없으니까.인덱스를 추가할 수 없는 이유들 인덱스 추가하면 된다. 간단하다. 근데 프로덕션에서 ALTER TABLE은 간단하지 않다. 89만 건 테이블에 인덱스 추가하면 락이 걸린다. InnoDB라도 시간이 걸린다. 10분? 20분? 그 동안 서비스는 멈춘다. 새벽 3시에 배포는 못 한다. 승인이 필요하다. CTO 결재. 근데 CTO는 자고 있다. 당연히. 옵션을 생각했다.아침까지 기다린다 - 유저들은 8초 쿼리를 계속 겪는다 지금 긴급 배포 - CTO 깨워서 승인받기 임시방편으로 쿼리 수정 - 근본 해결 아님2번은 무섭다. 3번은 찝찝하다. 1번은 책임이 무섭다. 슬랙을 켰다. CTO에게 DM. "프로덕션 슬로우 쿼리 발생했습니다. 인덱스 추가 필요합니다. 긴급 배포 승인 부탁드립니다." 읽음 표시가 안 뜬다. 10분을 기다렸다. 20분을 기다렸다. 답이 없다.임시방편이라는 이름의 선택 기다릴 수 없었다. 쿼리를 수정하기로 했다. 인덱스 없이 빠르게 만드는 방법. -- 기존 WHERE u.created_at >= '2024-01-01'-- 수정 WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)범위를 줄였다. 1년치가 아니라 90일치만. -- ORDER BY 제거 -- LIMIT 100만 남김정렬을 포기했다. 어차피 프론트에서 다시 정렬한다. 코드를 수정했다. 커밋. 푸시. 젠킨스에서 빌드. 배포는 못 한다. 승인이 없으니까. 근데 DB 쿼리는 바로 테스트할 수 있다. 프로덕션 DB에 직접. EXPLAIN SELECT u.*, ua.last_active FROM users u LEFT JOIN user_activities ua ON u.id = ua.user_id WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY) AND u.status = 'active' LIMIT 100;0.3초. 됐다. 근데 이건 임시방편이다. 근본 해결이 아니다. 데이터는 계속 쌓인다. 90일 범위도 언젠가 느려진다. 새벽 4시. CTO한테 전화했다. "여보세요?" 목소리가 잠에 절어있다. "죄송합니다. 프로덕션 이슈입니다." 상황을 설명했다. 쿼리 수정으로 임시 해결했다고. 인덱스 추가가 필요하다고. "아침에 회의하자. 6시까지는 자야겠다." 끊겼다. 나는 못 잔다. 아침 9시의 포스트모텀 출근했다. 4시간 잤다. CTO실에 들어갔다. 기획팀장도 있다. 왜 기획팀장까지. "상황 설명해봐." 화면을 공유했다. EXPLAIN 결과. 슬로우 쿼리 로그. 응답시간 그래프. "인덱스가 없었어요. user_activities 테이블에." "왜 없었는데?" "제가... 6개월 전에 만들 때 안 넣었습니다." "왜?" "데이터가 적어서 문제 될 줄 몰랐습니다." 침묵. 기획팀장이 물었다. "유저들 피해는?" "어젯밤 3시부터 8시까지. 약 5시간. 해당 API를 쓰는 유저들이 8초씩 기다렸습니다." "몇 명?" "로그 확인 결과 430명 정도." "우리 서비스 쓸 때 8초씩 기다렸다는 거네?" "...네." 또 침묵. CTO가 말했다. "인덱스 추가 언제 할 건데?" "오늘 점심시간에 하겠습니다. 유저 적을 때." "락 걸리는 시간은?" "15분 정도 예상합니다." "공지는?" "11시 50분에 올리겠습니다." "좋아. 근데 다음부터는 테이블 설계할 때 인덱스 미리 생각해." "네." 회의가 끝났다. 자리로 돌아왔다. 후배가 물었다. "형 괜찮아요?" "응." 괜찮을 리 없다. 점심시간의 배포 11시 50분. 공지를 올렸다. "점심시간 동안 DB 작업으로 서비스 일시 중단됩니다. 12:00 ~ 12:20 예정" 슬랙에 난리가 났다. "갑자기요?" "고객사 미팅 있는데" "점심시간이라 괜찮을 거예요" "아 진짜" 무시했다. 할 건 해야 한다. 12시 정각. 프로덕션 DB 접속. ALTER TABLE user_activities ADD INDEX idx_user_id (user_id), ADD INDEX idx_last_active (last_active);엔터를 눌렀다. 쿼리가 실행됐다. 진행률은 안 보인다. 그냥 기다려야 한다. 1분. 2분. 5분. 슬랙 알림이 울렸다. "아직 안 돼요?" "언제 돼요?" 10분. 12분. "Query OK, 890000 rows affected (13 min 47 sec)" 됐다. 서비스를 켰다. API 테스트. 응답시간 0.2초. 공지를 올렸다. "작업 완료. 서비스 정상화" 아무도 반응 안 했다. 다들 점심 먹으러 갔다. 나도 밥 먹으러 갔다. 김치찌개집. 사장님이 물었다. "오늘 안색이 안 좋네?" "어제 못 잤어요." "야근?" "비슷한 거요." 밥을 먹었다. 맛이 없었다. 배는 고픈데 넘어가지가 않았다. 그 후 일주일 인덱스 추가 후 일주일. 모니터링했다. 슬로우 쿼리 0건. 평균 응답시간 0.18초. 완벽하다. 근데 찝찝하다. user_activities 말고 다른 테이블은? 확인했다. orders 테이블 - 인덱스 3개뿐. user_id, created_at, status. payments 테이블 - 인덱스 2개. order_id, created_at. notifications 테이블 - 인덱스 1개. user_id만. 전부 시한폭탄이다. 데이터 쌓이면 터진다. 금요일 오후. 팀 회의에서 말했다. "모든 테이블 인덱스 점검이 필요합니다." 팀장이 물었다. "얼마나 걸려?" "일주일?" "다음주는 스프린트 마감인데." "그럼 그 다음 주에." "그때는 신규 기능 개발 들어가는데." 결국 없던 일이 됐다. 인덱스 점검은 우선순위에서 밀렸다. 또. 터지기 전까진 아무도 신경 안 쓴다. 나도 마찬가지다. 일이 많으니까. 다음번엔 어느 테이블이 터질까. orders? payments? 모르겠다. 터지면 그때 고치지 뭐. 새벽 3시에 또 깰 것 같다는 생각을 했다. 배운 것과 못 배우는 것 이번 일로 배운 거.테이블 만들 때 인덱스부터 생각하기 데이터 적을 때 빠르다고 안심하지 말기 스테이징에도 프로덕션급 데이터 넣어두기 슬로우 쿼리 알림은 진동 말고 무음으로근데 못 배우는 것도 있다. 기술 부채는 계속 쌓인다. 치울 시간은 없다. 우선순위는 항상 밀린다. 터지기 전까진 아무도 신경 안 쓴다. 개발자도. 기획자도. CTO도. 터지면 그제야 급하게 고친다. 새벽에. 혼자서. 그러고 일주일 지나면 다들 잊는다. 나만 기억한다. 다음번엔 어디가 터질지. 이게 7년차 개발자의 일상이다. 화려하지 않다. 그냥 버티는 거다. 터지면 고치고. 또 터지면 또 고치고. 가끔 생각한다. 이직해야 하나. 근데 다른 회사도 똑같을 거다. 기술 부채는 어디에나 있다. 레거시 코드도. 밤샘 배포도. 그냥 이렇게 사는 거다. 커피 한 잔 더 마셨다. 네 번째다. 오늘도 야근이다.다음번 슬로우 쿼리는 언제 터질까. 폰은 무음으로 해뒀다.