Incident Library
  • 원인
  • 구성 요소
  • 증상
  • 기여 요인
  • 사례

사례6건

6건 중 1~5번째

  1. 증상

    2025년 9월 4일, CoreDNS 파드 하나가 OOMKilled 됐다.

    촉발

    CoreDNS 파드 하나가 OOMKilled 됐다.

    원인
    구성 요소
    기여 요인
    교훈

    장애는 Istio, kube-proxy, conntrack 각각의 동작이 겹쳐 생겼다. Istio 는 DNS 캐시를 공격적으로 갱신하며 클라이언트 포트를 다시 썼고, kube-proxy 는 conntrack 항목을 정리하지 않았고, conntrack 의 UDP 기본 타임아웃은 2분이었다. kube-proxy 쪽 문제는 쿠버네티스 v1.32 에서 고쳐졌다. 발표자는 Istio 가 아주 복잡한 일은 단순하게 만들지만 아주 단순한 일은 복잡하게 만든다고 평했다. 발표자는 또 Cilium 을 소개했다. Cilium 은 iptables 체인을 차례로 훑는 대신 맵으로 Service 를 찾고, eBPF 데이터 경로로 kube-proxy 를 완전히 대체할 수 있다.

  2. 증상

    프로덕션 EKS 클러스터에서 원인을 바로 알기 어려운 서비스 에러가 간헐적으로 났다. RDS 나 Redis 같은 외부 서비스를 호출할 때 연결이 가끔 실패했다. DNS 요청이 몰릴 때는 도메인 조회 응답이 늦어지거나 조회가 아예 실패했다.

    촉발

    모든 CoreDNS 파드가 같은 노드에 배치되었고, 그 노드의 메모리 사용률이 급증해 메모리가 고갈됐다.

    원인
    구성 요소
    기여 요인
    감지

    글은 전면 장애를 어떻게 알아챘는지 적지 않았다. 그 전의 DNS 문제는 원인을 알기 어려운 서비스 에러가 간헐적으로 나면서 드러났다. 팀은 RDS 와 Redis 호출이 가끔 실패하는 현상을 추적하다가 DNS 이름 해석 실패가 근본 원인이라는 것을 확인했다.

    조치
    • CoreDNS 앞에서 DNS 질의를 받아 노드마다 캐시하는 NodeLocalDNS Cache 를 데몬셋으로 배포해 CoreDNS 부하를 줄였다.
    • CoreDNS 에 maxUnavailable: 1 인 PodDisruptionBudget 과 topology.kubernetes.io/zone 기준의 topologySpreadConstraints 를 적용해 파드가 한 곳에 몰리지 않게 했다.
    • Deployment 의 dnsConfig 로 ndots 값을 5 에서 2 로 낮춰 불필요한 DNS 조회를 줄였다.
    교훈

    쿠버네티스의 ndots:5 기본값은 점이 적은 외부 도메인을 조회할 때마다 질의를 여러 번 만든다. 이 부하는 트래픽이 큰 프로덕션 환경에서야 드러난다. CoreDNS 를 파드 2개의 기본 구성으로 두면 클러스터 전체의 단일 장애점이 된다. CoreDNS 파드는 노드와 가용 영역에 흩어 둔다. 별도 노드 그룹이나 Fargate 로 분리하면 다른 워크로드와 자원을 다투지 않는다. Affinity 와 PriorityClassName 으로 우선순위를 주면 더 안정적이다. CoreDNS 의 캐시와 bufsize 조정은 증상을 잠깐 줄였을 뿐이다. ndots 값은 여러 자료가 1이나 2를 권하지만 각 환경에 맞춰 정해야 한다.

  3. 증상

    어느 평범한 아침, 기술 지원 엔지니어가 전화로 고객 클러스터가 프로덕션 환경까지 포함해 사실상 동작하지 않는다고 알려 왔다. 클러스터에 접속해 보니 kube-apiserver 가 메모리를 전부 써 버린 상태였다.

    촉발

    200개가 넘는 노드에서 Cilium 파드가 쿠버네티스 API 에 LIST 요청을 동시에 대량으로 보냈다.

    원인
    구성 요소
    기여 요인
    감지

    기술 지원 엔지니어가 전화로 고객 클러스터가 사실상 동작하지 않는다고 알렸다. 요청을 보낸 쪽을 찾을 때는 Grafana 대시보드에서 API 요청 수가 급증한 것을 먼저 보았고, Prometheus 의 apiserver_request_total 을 인스턴스와 리소스별로 묶어 Cilium 네트워크 정책 쪽 LIST 요청을 찾았다. 감사 로그로 실제 요청도 확인했다.

    조치
    • 컨트롤 플레인 노드에 CPU 와 메모리를 두 차례에 걸쳐 더해 CPU 8개, 메모리 16 GB 에서 CPU 16개, 메모리 64 GB 로 늘렸고, 두 번째 증설 뒤 API 가 안정됐다.
    • 고객과 합의한 시험 시간에 cilium-agent 를 재시작해 API 서버가 하나씩 메모리를 다 쓰고 죽는 흐름을 확인했다.
    • d8-cni-cilium 서비스 어카운트의 cilium.io list 요청을 묶는 cilium-pods FlowSchema 와, 이 요청의 동시 처리 수를 제한하는 cilium-pods PriorityLevelConfiguration 을 만들어 클러스터에 배포했다.
    • cilium-agent 를 재시작해도 메모리 사용량이 크게 변하지 않는 것을 확인한 뒤 컨트롤 플레인 노드 자원을 원래 크기로 줄였다.
    후속 조치
    • API 요청을 제한해야 했던 일을 여러 번 겪은 뒤, API Priority and Fairness 설정을 Palark 쿠버네티스 플랫폼의 기본 구성으로 넣었다.
    교훈

    특정 클라이언트의 요청을 API Priority and Fairness 의 별도 우선순위 수준에 묶어 두면, 그 클라이언트가 요청을 쏟아내도 사용자 요청이 밀리지 않고 API 서버도 버틴다. API 서버 전체의 동시 처리 한도는 --max-requests-inflight 기본값 400 과 --max-mutating-requests-inflight 기본값 200 을 더한 값이다. API 서버는 이 한도를 각 우선순위 수준의 assuredConcurrencyShares 비율대로 나누므로, 한 수준의 값을 올리면 다른 수준의 한도는 줄어든다. 로그 보기나 exec 같은 오래 걸리는 요청과 WATCH 요청은 이 한도에 걸리지 않는다. 큐 개수, queueLengthLimit, handSize 는 요청이 너무 느려지지 않으면서 갑작스러운 폭증에도 API 서버가 넘치지 않도록 실험으로 정해야 한다. 지켜볼 지표로는 apiserver_flowcontrol_rejected_requests_total, apiserver_current_inqueue_requests, apiserver_flowcontrol_request_execution_seconds 가 있다. Palark 는 이런 일을 이번 말고도 여러 번 겪었다.

  4. 증상

    Grafana Cloud Logs 의 한 환경에서 Grafana Loki 의 쓰기 경로가 내려갔다. 롤아웃 중이던 distributor 파드가 OOM으로 죽었다가 다시 뜨기를 되풀이했다.

    촉발

    키-값 저장소에서 내장 memberlist 로 옮기는 마이그레이션의 1단계가 distributor 디플로이먼트와 서비스에 새 레이블을 붙였다. 서비스를 만드는 함수가 이 레이블을 셀렉터로도 복사했기 때문에, 롤아웃이 시작되자 distributor 서비스는 새 레이블을 단 새 파드만 가리켰다.

    원인
    구성 요소
    기여 요인
    감지

    처음에는 distributor 가 OOM으로 죽는 장애로 보였다. 첫 단서는 kubectl get pods 와 kube_deployment_status_replicas_updated 지표에서 distributor 의 10%만 재시작됐다는 점이었다. 팀은 PR 마다 Kubernetes 매니페스트 diff 를 만들어 PR 에 올리고 별도 레포에도 저장하는 자동화를 갖고 있었다. 이 diff 에서 distributor 서비스에 셀렉터가 추가되고 서비스가 다시 만들어진 것을 찾았다. 평소 배포에서는 서비스가 다시 만들어지지 않는다.

    조치
    • distributor 디플로이먼트의 레플리카를 늘렸다. 새로 뜬 파드도 새 레이블을 달고 있어서 부하가 더 많은 파드로 나뉘었고, OOM으로 죽는 파드가 줄면서 Kubernetes 가 옛 파드 교체를 이어 갔다.
    • 마이그레이션 단계의 변경을 되돌려 장애를 완전히 끝내고 원인을 조사할 시간을 벌었다.
    • service_ignored_labels:: [$._config.gossip_member_label] 을 설정하고 서비스 생성 함수 k.util.serviceFor 에 넘겨, memberlist 레이블이 서비스 셀렉터로 복사되지 않게 했다.
    후속 조치
    • 디플로이먼트나 스테이트풀셋의 레이블을 서비스 셀렉터로 복사하던 함수가 이번 같은 예외 상황도 처리하도록 고쳤다.
    교훈

    서비스 셀렉터를 디플로이먼트 레이블에서 자동으로 만들면, 롤아웃 중에 새 레이블이 붙는 순간 서비스가 새 파드만 가리킬 수 있다. 다른 환경에서 본 작은 오류를 롤아웃 때 흔한 잡음으로 넘기면 이런 신호를 놓친다. 환경마다 수평 확장과 메모리 한도 설정이 다르면 다른 환경에서 문제없던 변경이 특정 환경에서만 장애를 낸다. 원인을 빨리 찾는 데에는 Loki 와 Kubernetes 지표, PR 마다 남겨 둔 매니페스트 diff 가 도움이 됐다.

  5. 증상

    팀은 여러 서비스가 이상하게 동작한다는 보고를 받았다. 가끔 오류 페이지가 뜨고, 응답이 느리고, 타임아웃이 났다.

    촉발

    장애 직전에 팀원 한 명이 ElastAlert 에 새 알림 규칙을 명령줄로 배포했다. Cloudfront-* 인덱스에 일정 시간 새 문서가 들어오지 않으면 울리는 규칙이었다.

    원인
    구성 요소
    기여 요인
    감지

    여러 서비스에서 오류 페이지, 느린 응답, 타임아웃이 난다는 보고가 팀에 들어왔다. 알림은 하나도 울리지 않았다. 팀은 Prometheus 와 Grafana 가 안정적인 노드로 옮겨 간 뒤 파드 메모리 사용량 대시보드에서 원인을 찾았다.

    조치
    • NotReady 노드를 하나씩 종료해 오토스케일링 그룹이 새 노드를 띄우게 했다. 새 노드도 곧 NotReady 가 되어 이것만으로는 풀리지 않았다.
    • Prometheus 와 Grafana 가 안정적인 노드에 다시 스케줄된 뒤 파드 메모리 대시보드를 보고 ElastAlert 파드가 원인임을 찾아냈다.
    • 원인인 ElastAlert 문제를 고쳤다.
    후속 조치
    • CPU, 메모리, 디스크 공간 같은 자원 사용량에 알림 규칙을 정의한다.
    • 여러 출처를 함께 쓰는 알림 전략을 계속 연구한다. 지금은 Elasticsearch, 아직 설정하지 않은 Prometheus alertmanager, APM 용 New Relic 이 있다.
    • 지표를 클러스터 밖으로 내보내고 Grafana 도 클러스터 밖으로 옮기는 방안을 검토한다. 옮길 곳은 EC2 나 별도 쿠버네티스 클러스터가 될 수 있다.
    • 중요한 지표를 ELK 에서 시각화하거나 Prometheus 와 Grafana 의 가용성을 높인다. 지표 전략도 개선한다.
    • ElastAlert 의 자원 한도를 Helm values 로 지정한다.
    • 네임스페이스에 메모리 요청값의 기본값과 한도를 설정한다.
    • 고객 대면 워크로드를 빌드, 로깅, 모니터링 워크로드와 떼어 놓는다. 먼저 운영 클러스터 안의 전용 노드 그룹으로 옮기고, 경험에 따라 별도 도구 클러스터도 검토한다.
    • 모든 배포를 Jenkins 파이프라인 같은 자동화로 하고, 새 버전 배포에 GitOps 방식을 검토한다.
    • 배포 뒤 스모크 테스트 단계가 있는 파이프라인으로 배포한다.
    • 쿠버네티스와 클라우드 인프라 작업을 팀 스프린트에 넣어 모든 팀원이 경험 많은 팀원과 짝을 지어 맡게 하고, 주제별 심화 워크숍을 연다.
    교훈

    근본 원인 하나만 탓하는 포스트모템은 장애로 이어진 문제의 15% 정도만 다룬다. 이번에도 ElastAlert 를 고친 것은 일부일 뿐이었다. 알림이 없었고, 모니터링이 같은 클러스터 안에 있었고, Helm 차트의 자원 한도에 기본값이 없었다. 운영 도구와 고객 워크로드가 자원을 나눠 썼고, 손으로 한 배포를 팀이 몰랐고, 스모크 테스트가 없었고, 운영 지식이 일부에게만 있었다. 이렇게 배우려면 장애 건수나 해결 시간만 보지 않고 비난 없는 포스트모템을 허용하는 문화가 필요하다.

내 클러스터에도 있는지 확인하기

이 6건에서 가장 많이 나오는 구성 요소: 컨테이너 메모리 한도, CoreDNS. kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이런 틈을 점검합니다. 점검 항목의 근거는 이 라이브러리와 같은 공개 보고서입니다.

원인별3

소프트웨어 결함1건

  1. IL-0104 Istio 의 공격적인 DNS 캐시 갱신과 클라이언트 포트 재사용, kube-proxy 가 종료된 파드의 conntrack 항목을 정리하지 않는 문제, 2분짜리 UDP conntrack 기본 타임아웃이 겹쳐 CoreDNS 파드가 OOMKilled 됐다.

용량 한계3건

  1. IL-0027 CoreDNS 파드가 모두 한 노드에 몰린 채 그 노드의 메모리가 고갈되자 DNS 가 응답하지 못해 아이디어스 서비스 전체가 멈췄다
  2. IL-0195 200개가 넘는 노드의 고객 클러스터에서 Cilium 파드가 LIST 요청을 한꺼번에 쏟아내 kube-apiserver 가 메모리를 다 쓰고 죽기를 되풀이했다.
    Palark2023
  3. IL-0001 CoreDNS 파드가 메모리 한도 100Mi 에 걸려 OOM 으로 계속 죽으면서 쿠버네티스 클러스터 전체의 DNS 가 멈췄다

설정 변경2건

  1. IL-0191 Loki distributor 서비스에 레이블 셀렉터가 하나 더 들어가 롤아웃 중 새 파드 몇 개에만 쓰기 트래픽이 몰렸고, 그 파드들이 OOM으로 죽으면서 로그 쓰기 경로가 약 25분간 멈췄다.
  2. IL-0035 ElastAlert 에 새로 넣은 알림 규칙 때문에 ElastAlert 파드가 메모리를 끝없이 써서 노드가 잇달아 NotReady 가 되었고, 같은 클러스터의 도구와 Grafana, Prometheus 가 함께 흔들렸다.
    NU.nl2019