{"id":"IL-0191","org":"Grafana Labs","date":"2022-09-01","docs":[],"title":{"en":"A label selector accidentally added to the Loki distributor Service matched only the new pods of a rollout, which took all write traffic, were OOM-killed and stalled the rollout","ko":"Loki distributor 서비스에 레이블 셀렉터가 하나 더 들어가 롤아웃 중 새 파드 몇 개에만 쓰기 트래픽이 몰렸고, 그 파드들이 OOM으로 죽으면서 로그 쓰기 경로가 약 25분간 멈췄다."},"domain":["kubernetes","observability"],"impact":{"scope":"Write path of one Grafana Loki environment (a Grafana Cloud Logs cell)","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A Service whose selector is built by copying every label of a workload changes which pods it matches as soon as a new label is added. During a rollout it then matches only the new pods, which receive all the traffic before there are enough of them.\n\nA few errors during a rollout in other environments hid the problem, because those environments gave distributors more memory and could absorb the shift. Manifest diffs kept for every pull request made it possible to see that the Service selector had changed.","ko":"서비스 셀렉터를 디플로이먼트 레이블에서 자동으로 만들면, 롤아웃 중에 새 레이블이 붙는 순간 서비스가 새 파드만 가리킬 수 있다. 다른 환경에서 본 작은 오류를 롤아웃 때 흔한 잡음으로 넘기면 이런 신호를 놓친다. 환경마다 수평 확장과 메모리 한도 설정이 다르면 다른 환경에서 문제없던 변경이 특정 환경에서만 장애를 낸다. 원인을 빨리 찾는 데에는 Loki 와 Kubernetes 지표, PR 마다 남겨 둔 매니페스트 diff 가 도움이 됐다."},"related":[],"sources":[{"url":"https://grafana.com/blog/2022/08/31/how-adding-kubernetes-label-selectors-caused-an-outage-in-grafana-cloud-logs-and-how-we-resolved-it","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"Grafana Labs was migrating one Grafana Cloud Logs cluster so that the ring, the distributed hash table Loki components use to find each other, would live in embedded memberlist instead of a separately deployed key-value store, to remove that store as a single point of failure. Pods that join memberlist carry the label gossip_ring_member: 'true'. Services are generated with jsonnet and Grafana Tanka, and the library function that built a Service for a Deployment or StatefulSet copied every label of the workload into the Service selector. Another team had already flagged this and added an optional parameter to leave some labels out, but the author did not notice it and did not pass it, so the distributor Service ended up selecting only pods that had the new label.\n\nThe gateway, which authenticates write requests and finds the distributors through the Kubernetes API, could therefore only see the new pods once the rollout began. 100 percent of write traffic went to about 10 percent of the distributors. Those pods ran past their CPU and memory limits, were OOM-killed and failed their readiness checks, so Kubernetes did not move on to replace more old pods, and the same few pods kept taking the whole load. Because distributors are the entry point for data, the write path of that cell was down for about 25 minutes.\n\nRecovery started before the cause was found. Treating the outage as OOM crashes, the team scaled the distributor Deployment out. The extra replicas came up with the new label, joined the Service and spread the load; fewer pods crashed, more became healthy, and Kubernetes continued the rollout. The migration step was then reverted. The cause was found in the Kubernetes manifest diffs that automation posts on every pull request: the selector had been added to the distributor Service and the Service had been recreated, which a normal deploy does not do. The fix was to set service_ignored_labels:: [$._config.gossip_member_label] and pass it to the Service constructor k.util.serviceFor.\n\nThe step had not caused an outage elsewhere because this environment scaled its distributors horizontally much more than the others. Environments of similar size also gave distributors more memory, so the new pods survived the extra share of traffic long enough for Kubernetes to keep rolling the old pods.","ko":"Grafana Labs 는 Grafana Cloud Logs 클러스터 하나에서 Loki 의 링을 저장하던 별도 키-값 저장소를 내장 memberlist 로 바꾸는 중이었다. 링은 distributor 가 어느 ingester 에 데이터를 쓸지 정할 때 쓰는 분산 해시 테이블이다. 이 마이그레이션은 키-값 저장소라는 단일 장애점을 없애려는 작업이었다.\n\nGrafana Labs 는 jsonnet 과 Grafana Tanka 로 Kubernetes 배포를 관리한다. 디플로이먼트나 스테이트풀셋에 붙은 레이블을 모두 복사해 서비스 셀렉터로 만드는 함수가 있었다. 다른 팀이 이 문제를 이미 지적하고 일부 레이블을 빼는 선택 인자를 추가해 두었지만, 마이그레이션 코드를 쓴 엔지니어는 이를 알아채지 못하고 인자를 넘기지 않았다. 그래서 memberlist 용 레이블이 distributor 서비스의 셀렉터에도 들어갔다.\n\n1단계 변경을 롤아웃하자 distributor 서비스는 새 레이블이 붙은 파드만 가리켰다. gateway 는 인증을 확인한 뒤 쓰기 데이터를 distributor 로 넘기는 구성 요소다. gateway 는 Kubernetes API 로 distributor 를 찾기 때문에 롤아웃 초기에 뜬 새 파드만 보게 됐다. 쓰기 트래픽 100%가 distributor 의 10%에 몰렸고, 새 파드는 OOM으로 죽어 레디니스 체크를 통과하지 못했다. 롤아웃이 더 진행되지 않으니 같은 파드들이 계속 부하를 받았다.\n\n같은 단계가 다른 환경에서는 장애를 내지 않았다. 장애가 난 환경은 distributor 를 수평으로 훨씬 많이 늘려 운영했고, 비슷한 규모의 다른 환경은 distributor 메모리 한도도 함께 키워 두었다. 그래서 다른 환경의 새 파드는 메모리 한도에 이르기 전에 완전히 뜰 시간이 조금 더 있었고, 롤아웃이 계속 이어지며 부하가 퍼졌다. 이번 환경에서는 원인을 찾기 전에 distributor 를 늘려 복구를 시작했고, 마이그레이션 단계를 되돌려 약 25분 만에 장애를 끝냈다."},"trigger":{"en":"Step 1 of moving the Loki ring off a separately deployed key-value store onto embedded memberlist added a new label, gossip_ring_member: 'true', to the distributor pods and Service, together with a small config change, and the change required a rollout of the distributors.","ko":"키-값 저장소에서 내장 memberlist 로 옮기는 마이그레이션의 1단계가 distributor 디플로이먼트와 서비스에 새 레이블을 붙였다. 서비스를 만드는 함수가 이 레이블을 셀렉터로도 복사했기 때문에, 롤아웃이 시작되자 distributor 서비스는 새 레이블을 단 새 파드만 가리켰다."},"severity":"unknown","symptoms":["service-unavailable","oom-killed","uneven-load","readiness-probe-failures"],"timeline":{"detected_after":null,"resolved_after":"25m"},"date_kind":"published","detection":{"en":"The first clue was kubectl get pods and the kube_deployment_status_replicas_updated metric showing that only 16 of 140 distributor pods had been updated.","ko":"처음에는 distributor 가 OOM으로 죽는 장애로 보였다. 첫 단서는 kubectl get pods 와 kube_deployment_status_replicas_updated 지표에서 distributor 의 10%만 재시작됐다는 점이었다. 팀은 PR 마다 Kubernetes 매니페스트 diff 를 만들어 PR 에 올리고 별도 레포에도 저장하는 자동화를 갖고 있었다. 이 diff 에서 distributor 서비스에 셀렉터가 추가되고 서비스가 다시 만들어진 것을 찾았다. 평소 배포에서는 서비스가 다시 만들어지지 않는다."},"components":["config-templating","service-discovery","memory-limit"],"root_cause":"config-change","remediation":[{"text":{"en":"scaled the distributor Deployment out horizontally so the load spread over more pods with the new label and the rollout could continue","ko":"distributor 디플로이먼트의 레플리카를 늘렸다. 새로 뜬 파드도 새 레이블을 달고 있어서 부하가 더 많은 파드로 나뉘었고, OOM으로 죽는 파드가 줄면서 Kubernetes 가 옛 파드 교체를 이어 갔다."}},{"text":{"en":"reverted the migration step to end the outage","ko":"마이그레이션 단계의 변경을 되돌려 장애를 완전히 끝내고 원인을 조사할 시간을 벌었다."}},{"text":{"en":"set service_ignored_labels:: [$._config.gossip_member_label] and passed it to k.util.serviceFor so the label is no longer copied into the Service selector","ko":"service_ignored_labels:: [$._config.gossip_member_label] 을 설정하고 서비스 생성 함수 k.util.serviceFor 에 넘겨, memberlist 레이블이 서비스 셀렉터로 복사되지 않게 했다."}}],"action_items":[{"text":{"en":"Update the function that generates a Service from a Deployment or StatefulSet to handle edge cases, including this one","ko":"디플로이먼트나 스테이트풀셋의 레이블을 서비스 셀렉터로 복사하던 함수가 이번 같은 예외 상황도 처리하도록 고쳤다."}}],"contributing":["default-config-unchanged","early-signal-ignored","staging-not-representative"],"recurrence_of":null,"symptoms_text":{"en":"During a rollout of the Grafana Loki distributor Deployment in one Grafana Cloud Logs environment, the write path went down for about 25 minutes. The newly started distributor pods were OOM-killed over and over, never passed their health and readiness checks, and the rollout stopped making progress.\n\nkubectl get pods and the Kubernetes metric kube_deployment_status_replicas_updated showed that only 16 of 140 distributors, about 10 percent, were in the updated state for the whole outage. Counting the per-pod series of loki_request_duration_seconds_count whose rate on the write route was above zero gave the same answer: only 16 of 140 distributor replicas were receiving write requests.\n\nThe first suspicion was the data itself. Because log streams are sharded and replicated to subsets of pods, the team thought a single high-volume stream or a burst of new streams might have arrived just as the deploy started. The metrics for incoming write request rate and for active streams had not changed before the incident, so the suspicion moved to how requests were being routed to the distributors.\n\nThe same migration step had already been rolled out to other environments. It produced a few errors there, which the team took for the usual small number of errors during a rollout of many components that start at different times.","ko":"Grafana Cloud Logs 의 한 환경에서 Grafana Loki 의 쓰기 경로가 내려갔다. 롤아웃 중이던 distributor 파드가 OOM으로 죽었다가 다시 뜨기를 되풀이했다.\n\nkubectl get pods 와 지표 kube_deployment_status_replicas_updated 를 보니 distributor 140개 가운데 16개만 업데이트된 상태로 롤아웃이 멈춰 있었다. 새 파드는 CPU와 메모리 한도를 넘는 부하를 받아 OOM으로 죽었고, 헬스 체크와 레디니스 체크를 통과하지 못했다. Kubernetes 는 새 파드가 이 체크를 통과해야 다음 옛 파드를 교체하므로 롤아웃이 더 나아가지 않았고, 같은 파드들이 계속 부하를 받았다.\n\n팀은 처음에 특정 로그 스트림의 처리량이 갑자기 늘었거나 새 스트림이 한꺼번에 많이 생긴 시점에 마침 배포가 겹쳤다고 의심했다. 그러나 들어오는 쓰기 요청 비율과 활성 스트림 수 지표는 장애 직전에 변하지 않았다. 각 distributor 가 보고하는 loki_request_duration_seconds_count 에서 쓰기 경로 요청 비율이 0보다 큰 시계열을 세어 보니 140개 중 16개만 쓰기 요청을 받고 있었다. 그래서 요청이 distributor 로 가는 경로가 바뀌었다고 보고 확인에 나섰다.\n\n같은 마이그레이션 단계를 다른 환경에 배포했을 때도 오류가 몇 건 났다. 롤아웃 때는 구성 요소들이 서로 다른 시각에 뜨기 때문에 오류가 조금씩 나는 일이 흔해서, 팀은 이를 이상하게 여기지 않았다."},"updated_at":"2026-11-10T00:00:40.041141Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0191","org_slug":"grafana-labs"}