사례3건
3건 중 1~3번째
- 증상
DNS 오류율과 서버 부하가 늘었다는 알림이 울렸다. 일부 AdGuard DNS 사용자는 DNS 조회가 느려지거나 실패했고, 타임아웃과 SERVFAIL 응답을 받았다.
- 촉발
AdGuard 는 11월 25일에 사용자 데이터 캐시 방식을 개선하려는 새 AdGuard DNS 버전을 배포했다. 버그 때문에 리졸버가 로컬 캐시 파일에 전체 사용자 데이터가 아니라 최근에 바뀐 일부만 저장했다. 다시 시도한 배포에서 많은 리졸버가 한꺼번에 전체 동기화를 시작했다.
- 원인
- 구성 요소
- 기여 요인
- 감지
15시 30분에서 16시 UTC 사이, 두 번째 배포를 시작한 뒤 DNS 오류율과 서버 부하가 늘었다는 알림이 울렸다.
- 조치
- 버그가 있는 버전을 모든 서버에서 동시에 롤백했다. 롤백은 17시 30분에서 18시 UTC 사이에 끝났다.
- 암스테르담의 트래픽 일부를 다른 유럽 거점으로 돌렸다. 암스테르담은 안정됐지만 프랑크푸르트와 런던의 부하가 늘었다.
- 프랑크푸르트와 런던에서 실제 연결 동시성과 파이프라인 깊이를 줄였다. 프랑크푸르트에서 파이프라인 깊이를 5에서 1로 줄이자 매우 빨리 회복됐다.
- 한도를 실제 하드웨어 용량에 맞게 조정했다.
- 전체 동기화가 더 일어나지 않도록 손상된 캐시 파일과 사용자 데이터 스냅샷을 고쳤다.
- 트래픽 분배를 정상으로 돌렸고 오류율이 평소 수준으로 내려왔다.
- 후속 조치
- 전체 사용자 데이터가 로컬 캐시 파일에 제대로 직렬화되고 그 파일에서 제대로 복원되는지 확인하는 자동 테스트를 넣는다. 캐시가 비었거나 잘린 것도 잡아내게 한다.
- 실제와 비슷한 부하에서 빈 캐시나 손상된 캐시로 리졸버를 시작하는 통합 테스트를 넣어, 시스템이 무리 없이 동작하고 비즈니스 로직 서비스에 과부하를 주지 않는지 확인한다.
- 거점마다 서버 하드웨어 사양을 하나로 맞춰 RAM 크기가 크게 다른 서버가 한 클러스터에 섞이지 않게 하거나, 하드웨어 종류마다 설정을 따로 관리한다.
- 표준 노드 종류마다 connlimit, 캐시 크기, 연결 한도를 다시 살펴 조정해 32 GiB 노드와 64 GiB 노드가 모두 안전한 가비지 컬렉션과 메모리 범위 안에서 돌게 한다.
- 최대 부하에서도 고루틴과 힙이 지나치게 늘지 않도록 파이프라이닝 깊이를 다시 따져 보고 필요한 곳은 낮춘다.
- 연결 수와 고루틴 수를 제한하는 방식을 다시 따져 보고, 자원 사용을 적절히 제한하면서도 연결이 끊긴 뒤에도 처리량이 충분히 나오게 한다.
- 모니터링이 CPU 와 전체 메모리뿐 아니라 Go 힙 크기, 가비지 컬렉션 시간, 초당 가비지 컬렉션 횟수, 클러스터별 요청 지연 분포도 보게 넓힌다.
- 상위 DNS 가 정상인데 SERVFAIL 비율과 요청 처리 시간이 계속 오르면 울리는 알림을 따로 둬, 내부 경합과 가비지 컬렉션 문제를 사용자가 겪기 전에 잡는다.
- DNS 리졸버의 배포와 롤백 절차를 다듬어 전체 서버의 큰 부분에서 캐시가 동시에 무효화되지 않게 한다.
- 암스테르담과 프랑크푸르트에서 본 동작을 바탕으로, 사고 중에 파이프라인 깊이, 연결 한도, connlimit 을 빨리 조정하는 플레이북을 문서로 만들고 연습한다.
- 교훈
노드마다 다르게 움직인 데에는 사용할 수 있는 RAM, Go 가비지 컬렉터, connlimit 설정이 서로 맞물린 것이 컸다. 메모리가 큰 서버에서 connlimit 을 너무 높게 잡으면 힙이 크게 자란 뒤에야 가비지 컬렉션이 강하게 돈다. 그러면 메모리는 남아도 CPU 시간이 가비지 컬렉션에 점점 더 쓰여, 지연이 늘고 처리량이 준다. AdGuard 는 이것을 메모리를 많이 쓰는 애플리케이션에서 connlimit 이 만드는 알려진 최악의 경우라고 했다. 메모리가 작은 서버는 캐시 크기와 연결 한도를 RAM 에 맞춰 줄이지 않아서 더 일찍 실패했다. 여기에 높은 파이프라이닝과 많은 재시도가 겹쳐 가비지 컬렉션 압박, 연결 포화, 클라이언트 재시도가 서로를 키우는 고리가 생겼다.
- 증상
2025-03-12 10:58 UTC 부터 백엔드 응답 시간이 높다는 알림이 들어왔다. 3월 12일과 13일 이틀 동안 mw-api-int, mw-parsoid, mw-jobrunner 에서 지연이 커지고 데이터베이스 오류가 늘었다. External Storage 클러스터의 부하와 연결 수도 크게 늘었다.
- 촉발
mw-api-int, mw-parsoid, mw-jobrunner 를 PHP 8.1 로 옮기는 예정된 Scap 배포가 장애를 일으켰다. 3월 12일 10:44 UTC 에 1126607[puppet] 과 1126650 [deployment-charts] 를, 3월 13일 11:15 UTC 에 1127476[puppet] 과 1127478[deployment-charts] 를 배포했다.
- 원인
- 구성 요소
- 기여 요인
- 감지
3월 12일 10:58 UTC 에 백엔드 응답 시간이 높다는 알림이 들어오기 시작했다. 처음 세운 가설은 특정 네트워크나 클라이언트의 많은 트래픽, 잘못 실행된 예약 작업, 자원을 많이 먹는 changeprop-jobqueue 작업이었다. 원인을 가리켰을 MediaWikiMemcachedHighErrorRate 알림은 logstash-prometheus 파이프라인이 밀려 빠지거나 곧바로 회복됐다. 장애 중에는 알림 78개가 울리고 있어서, 그 알림이 울렸더라도 부수 증상으로 보고 넘겼을 가능성이 컸다.
- 조치
- 3월 12일 13:19 UTC 에 cirrus-streaming-updater 의 SUP 병렬도를 낮추는 패치 1126988[deployment-charts] 를 배포했다.
- 13:33 에 1127000[deployment-charts] 로 changeprop-jobqueue 의 categoryMembershipChange 작업 동시성을 낮췄다.
- 13:44 에 1126607[puppet] 과 1126650 [deployment-charts] 를 되돌리고 Scap 을 다시 돌렸다.
- 3월 13일 11:34 UTC 에 changeprop-jobqueue 를 롤링 재시작했다.
- 11:43 에 mw-api-int 를 롤링 재시작했다.
- 11:44 에 1127500[deployment-charts] 로 changeprop-jobqueue 의 categorymembership 작업을 껐다.
- 11:46 에 1127476[puppet] 과 1127478[deployment-charts] 를 되돌렸다.
- 11:58 에 되돌린 내용을 반영하려고 mw-api-int 를 다시 배포했다.
- 12:07 에 mw-api-int 의 레플리카 수를 늘렸다.
- 12:28 에 되돌린 내용을 반영하려고 mw-jobrunner 를 다시 배포했다.
- 14:06 에 되돌린 내용을 반영하려고 mw-parsoid 를 다시 배포했다.
- 후속 조치
- 장애 때 channel:Memcached 로 보내는 메시지 양을 줄인다.
- Scap 에 helm 롤백 기능을 넣는다.
- -Dbuild_mw_container_image:False 로 실행할 때도 Scap 이 helmfile-defaults 를 갱신해야 하는지 검토한다.
- MediaWiki 이미지에서 envvars.inc 를 뺄지 검토한다.
- 서로 관련된 구성 요소를 한꺼번에 보여 주고 따라갈 링크를 담은 "Why" 대시보드를 만들지 검토한다.
- 장애 대응 조율을 개선한다. 누가 무엇을 맡았는지, 누가 대응에 들어오고 나갔는지 장애 조율자가 알 수 있어야 한다. 또 맥락 없는 의견과 대응과 관계없는 논의로 채널이 시끄러워지지 않아야 한다.
- 장애 중에 MediaWiki 개발자와 연락해 개발자의 관점을 바로 들을 수 있게 한다.
- 교훈
Wikimedia 는 기술적 원인보다, 3월 12일 장애 때부터 MediaWiki 배포 세 곳이 Memcached 클러스터에 닿지 못하고 있다는 것을 알아채지 못한 이유가 더 중요하다고 봤다. 알림에 연결된 대시보드는 무엇이 일어나는지만 보여 줬다. Wikimedia 는 서로 의존하거나 영향을 주는 구성 요소를 함께 보여 주는 대시보드가 있었다면 달랐을 것이라고 적었다. 예를 들어 SQLBlobStore 키는 External Storage 와 바로 이어져 있다. 원인을 가리킬 알림은 곧바로 회복됐다. 그 알림이 울렸더라도 알림 78개 사이에서는 부수 증상으로 봤을 수 있었다. 응답자가 많았지만 누가 무엇을 하는지, 누가 대응에 들어오고 나갔는지 분명하지 않아 장애 조율자가 조율하기 어려웠다. 외부 사용자 영향을 제대로 가늠하지 못해 실제보다 심각하게 여겼다. trace.wikimedia.org 도 장애 중에 쓰지 않았다.
- 증상
패키지 매출 랭킹 기능을 배포한 뒤 버그 픽스를 몇 차례 더 배포하고 유저를 재접속시키자 Database 알람이 울렸고, Database CPU 그래프가 100%에서 내려오지 않았다. Database 접속에 문제가 생기면서 유저들이 게임에 정상적으로 접속하거나 플레이할 수 없었다.
- 촉발
새 패키지 매출 랭킹 기능의 집계 쿼리가 들어간 배포 뒤 버그 픽스를 추가로 배포하고 유저를 재접속시키면서, 캐시가 빈 서버들이 이 쿼리를 한꺼번에 실행했다.
- 원인
- 구성 요소
- 기여 요인
- 감지
버그 픽스를 추가로 배포하고 유저를 재접속시킨 뒤 Database 알람이 울려 CPU 그래프를 보고 알았다.
- 조치
- 서버 점검을 걸었다.
- 문제가 된 패키지 매출 랭킹 기능을 켜고 끌 수 있는 설정을 빠르게 추가해 꺼 둔 뒤 서비스를 정상화했다.
- 판매 시작 시간을 공유하는 상품들의 product_id 조건을 하나로 묶고, 의미가 없는 현재 시각 조건을 지운 쿼리로 바꿔 바로 적용했다.
- 모든 서버가 함께 쓰는 캐시를 스케줄러 서버에 따로 두었다. 각 서버는 Database 대신 원격 저장소의 결과를 읽고, 요청은 큐에 넣는다. 스케줄러 서버는 주기적으로 마지막 요청으로 쿼리해 결과를 원격 저장소에 캐싱한다. 이렇게 해서 쿼리 실행 횟수를 줄였다.
- 쿼리 최적화와 캐시 사용 방식 개선을 적용한 뒤 패키지 매출 랭킹 기능을 다시 켰고, CPU가 문제 없이 유지되는 것을 확인했다.
- 교훈
무거운 쿼리와 분산 서버의 캐시 구조가 겹치면 예상하지 못한 문제가 생기므로, 문제가 될 만한 지점을 미리 파악해야 한다. 장애가 나면 원인 파악보다 서비스 정상화가 먼저다. 문제가 되는 기능을 끄고 켤 수 있는 안전장치를 미리 만들어 두면 서비스를 더 안정적으로 운영하는 데 도움이 된다. Production 환경의 online alter에는 Percona Toolkit 같은 유용한 툴을 잘 활용하는 것이 중요하다.
이 3건에서 가장 많이 나오는 구성 요소: 데이터베이스, 인메모리 캐시. kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이런 틈을 점검합니다. 점검 항목의 근거는 이 라이브러리와 같은 공개 보고서입니다.