분류
개발 · 8쪽
DB·백엔드에서 실제로 터지는 문제와 USB·CD 굽기 교과서를 모았다. 느린 쿼리, 락과 트랜잭션, 장애와 확장, 배포와 관측은 태그로 나눠 찾는다.
96편 · 글 80 · 강 16 · 마지막 글 2026.09.28
교과서굽기 16강
개발 글
96편 중 85–96
다른 상품을 샀는데 왜 서로 기다릴까, SELECT FOR UPDATE
재고를 잠그는 쿼리가 개발 DB 에선 멀쩡했다. 그런데 프로모션 날 주문 API 가 통째로 멈췄다. 서로 다른 상품을 산 사람끼리 줄을 서고 있었다.
DB 는 몇 초 만에 살아났는데 서비스는 왜 계속 죽어 있을까
DB 가 몇 초 느려졌을 뿐인데 서비스가 무너졌고, 원인이 사라진 뒤에도 회복이 안 됐다. 범인은 「실패하면 세 번 재시도」라는 지극히 상식적인 설정이었다.
100페이지만 가면 타임아웃, OFFSET 페이지네이션
목록 1페이지는 10ms 인데 100페이지쯤 가면 타임아웃이 난다. 그 깊이까지 넘기는 건 사람이 아니라 크롤러다. OFFSET 은 건너뛰는 척하면서 사실 다 읽고 있다.
운영에서만 느린 목록 API, 범인은 ORM N+1
목록 API 가 로컬에선 50ms 인데 운영에선 1.5초가 걸렸다. 느린 쿼리는 하나도 없었다. 범인은 쿼리 하나가 아니라 101번이라는 개수였다.
MySQL 의 utf8 은 UTF-8 이 아니다
닉네임에 이모지를 넣었더니 500 에러가 났다. 인코딩은 분명 utf8 로 맞췄는데도. MySQL 의 utf8 은 이름만 UTF-8 이고, 문자 하나에 3바이트까지만 담는다.
인덱스를 걸었는데 LIKE '%검색어%' 가 느리다
검색을 LIKE '%키워드%' 로 만들었더니 데이터가 늘면서 몇 초씩 걸렸다. 인덱스를 걸어 뒀는데도. 앞이 열린 패턴 앞에서는 B-tree 가 손쓸 방법이 없다.
한 번 눌렀는데 두 번 결제됐다면, 멱등성
사용자는 결제 버튼을 한 번 눌렀는데 청구는 두 번 나갔다. 응답을 못 받았다고 다시 보낸 게 원인이었다. 그런데 보낸 쪽은 처리가 됐는지 안 됐는지 알 길이 없다.
keep-alive 를 안 쓰면 악수만 하다 끝난다
상대 서비스는 5ms 만에 처리하는데 우리 쪽에서 재면 40ms 가 나왔다. 사라진 35ms 는 요청마다 연결을 새로 맺는 데 쓰이고 있었다. 설정은 맞았다. 쓰는 법이 틀렸다.
배포할 때마다 502 가 조금씩 난다
무중단 배포를 설정했는데 배포할 때마다 502 가 몇십 건씩 났다. 설정은 다 맞았다. 서버가 내려가는 순간과 트래픽이 끊기는 순간이 같지 않았을 뿐이다.
컬럼은 있는데 인덱스를 안 탄다면, 복합 인덱스 순서 문제
(status, created_at) 인덱스를 만들었는데 created_at 만으로 찾으면 인덱스를 안 탄다. 그래서 인덱스를 자꾸 더 만들게 된다. 순서 기준을 알면 오히려 개수가 준다.
적중률 95% 인데 정각마다 DB 가 튄다, 캐시 스탬피드
캐시 적중률은 95% 인데 정각마다 DB 부하가 순간적으로 치솟았다. 알림도 안 울릴 만큼 짧아서 더 고약했다. 같은 시각에 만든 캐시가 같은 시각에 한꺼번에 만료되고 있었다.
조건에 맞는 글이 없다. 브루니가 숨었나 보다.