{"id":"IL-0194","org":"Palark","date":"2023-03-23","docs":["https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions","https://kubernetes.io/docs/concepts/cluster-administration/flow-control/"],"title":{"en":"After a kube-apiserver restart, pod LIST requests from every pod of a Vector DaemonSet nearly tripled etcd memory and restarted all master nodes","ko":"kube-apiserver 재시작 뒤 Vector DaemonSet 의 파드 list 요청이 한꺼번에 몰려 etcd 메모리가 세 배 가까이 늘고 마스터 노드가 연쇄적으로 재시작했다."},"domain":["kubernetes"],"impact":{"scope":"A customer's production cluster: etcd and all master nodes restarted","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A field selector reduces what kube-apiserver returns, not what it reads from etcd: listing the pods of one node still reads every pod in the cluster. A DaemonSet whose pods all list at startup multiplies that by the node count each time kube-apiserver restarts. Where exact freshness is not needed, resourceVersion=0 lets kube-apiserver answer from its cache; where the client cannot set it, API Priority and Fairness can queue the requests so they reach etcd gradually.","ko":"etcd 는 필드 셀렉터와 레이블 셀렉터를 모르기 때문에, kube-apiserver 에는 가벼워 보이는, 필터를 건 list 요청도 etcd 에서는 그 종류의 객체 전체를 읽는 일이 된다. 데이터가 꼭 최신이어야 하는 컨트롤러가 아니라면 resourceVersion=0 으로 kube-apiserver 캐시에서 읽게 할 수 있다. 다만 kube-apiserver 가 여러 대면 인스턴스마다 다른 데이터를 받을 수 있다. 여러 파드가 한꺼번에 보내는 요청은 Priority and Fairness 로 대기열에 넣어 kube-apiserver 와 etcd 를 지킬 수 있다. 위험한 것은 파드 요청만이 아니어서 Cilium CNI 에서도 비슷한 문제가 나왔다."},"related":[],"sources":[{"url":"https://palark.com/blog/kubernetes-api-list-case-troubleshooting/","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"The DaemonSet was the Vector log collector, which uses pod data to add metadata to log lines. Each of its pods asked kube-apiserver for the pods on its own node with a field selector, a request that returns at most 110 pods. etcd, however, stores objects as plain key-value pairs under /registry/<kind>/<namespace>/<name> and knows nothing about field or label selectors, so kube-apiserver had to read every pod in the cluster from etcd (7130 in the logged request) and filter them itself. With the default resourceVersion behaviour (MostRecent), every such list goes to etcd. When all the collector pods started together after a kube-apiserver restart, these reads drove etcd memory up and brought the master nodes down.\n\nPalark first tried resourceVersion=0, which lets kube-apiserver answer from its cache: a test list of all pods took 117 ms instead of 337 ms. The collector is written in Rust, and the Kubernetes Rust library had no resource version setting, so this was not available right away; Palark later submitted a pull request to the library. To end the incident, Palark used API Priority and Fairness to queue the requests instead. A PriorityLevelConfiguration named limit-list-custom (type Limited, assuredConcurrencyShares 5, queuing with 16 queues, handSize 4, queueLengthLimit 50) and a FlowSchema that sends list and get requests on pods from the collector's service account to that level, distinguished by user, were applied. After that, etcd memory was mostly normal with some spikes left, and the incident's criticality was lowered.\n\nPalark found a similar problem with the Cilium CNI and handled it the same way.","ko":"고객사 운영 클러스터에서 kube-apiserver 를 재시작한 뒤 etcd 메모리가 거의 세 배로 늘었고 모든 마스터 노드가 연쇄적으로 재시작했다. 감사 로그로 찾은 원인은 로그에 파드 메타데이터를 덧붙이려고 파드 정보를 읽는 Vector 로그 수집기 DaemonSet 이었다. 이 파드들은 client-go 인포머의 기본 동작처럼 kube-apiserver 가 재시작하면 캐시를 다시 채우려고 list 요청을 보낸다. 파드 하나가 시작하면서 list 요청을 60번 보냈고 노드는 약 80개였다.\n\n요청은 /api/v1/pods?fieldSelector=spec.nodeName=$NODE_NAME 모양으로, 자기 노드에 있는 파드만 달라는 것이었다. 하지만 etcd 는 /registry/<kind>/<namespace>/<name> 꼴의 키로 값을 저장하는 단순한 키-값 저장소라서 필드 셀렉터나 레이블 셀렉터를 모른다. 그래서 kube-apiserver 는 파드 110개로 응답하려고 etcd 에서 파드 전부, 이 클러스터에서는 7130개를 읽어야 했다. resourceVersion 을 지정하지 않은 요청은 기본값인 MostRecent 로 처리돼 매번 etcd 까지 간다.\n\n요청에 resourceVersion=0 과 resourceVersionMatch=NotOlderThan 을 넣으면 kube-apiserver 가 자기 캐시에서 답해 응답 시간이 337 밀리초에서 117 밀리초로 줄었다. 하지만 이 애플리케이션은 Rust 로 짜여 있었고 쿠버네티스 Rust 라이브러리에는 resourceVersion 설정이 없었다. 대신 API Priority and Fairness 로 이 DaemonSet 서비스 어카운트의 파드 list 와 get 요청을 대기열에 줄 세웠다. 그 뒤 etcd 메모리 그래프는 스파이크가 일부 남았지만 대체로 정상이었고, 팀은 장애 등급을 낮췄다."},"trigger":{"en":"An ordinary restart of kube-apiserver, after which every pod of the log collector DaemonSet sent its list requests at the same time.","ko":"일상적인 kube-apiserver 재시작 뒤, Vector 로그 수집기 DaemonSet 의 파드 전부가 캐시를 다시 채우려고 한꺼번에 파드 list 요청을 보냈다."},"severity":"unknown","symptoms":["control-plane-unresponsive","database-overloaded"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"Memory graphs showed etcd memory on the master nodes almost tripling after a kube-apiserver restart; the audit logs then pointed to one DaemonSet's list requests.","ko":"kube-apiserver 재시작 직후 마스터 노드 전체 메모리와 etcd 메모리 그래프가 치솟고 마스터 노드가 연쇄적으로 재시작하면서 알았다. 원인이 된 DaemonSet 은 감사 로그를 보고 찾았다."},"components":["etcd","kube-apiserver","telemetry-agent"],"root_cause":"capacity","remediation":[{"text":{"en":"created a PriorityLevelConfiguration named limit-list-custom that queues requests (assuredConcurrencyShares 5, 16 queues, handSize 4, queueLengthLimit 50)","ko":"limit-list-custom 이라는 PriorityLevelConfiguration 을 만들어 요청 대기열을 설정했다. 값은 문서의 표준 설정을 그대로 썼다."}},{"text":{"en":"created a FlowSchema that routes list and get requests on pods from the log collector's service account to that priority level","ko":"같은 이름의 FlowSchema 를 만들어, 이 DaemonSet 서비스 어카운트가 보내는 파드 list 와 get 요청을 그 대기열로 보냈다."}}],"action_items":[{"text":{"en":"Submit a pull request adding resource version support to the Kubernetes Rust library","ko":"쿠버네티스 Rust 라이브러리에 resourceVersion 설정을 넣는 PR 을 냈다."}},{"text":{"en":"Open a pull request to the Vector log collector to address the startup list requests","ko":"Vector 로그 수집기 쪽에 이 문제를 고치는 PR 을 열었다."}},{"text":{"en":"Apply the same flow-control approach to a similar problem found with the Cilium CNI","ko":"Cilium CNI 에서도 비슷한 문제를 찾아 같은 방법으로 해결했다."}}],"contributing":["limit-unknown","default-config-unchanged"],"recurrence_of":null,"symptoms_text":{"en":"One evening, right after an ordinary restart of kube-apiserver in a customer's production cluster, etcd memory consumption rose almost threefold. The sudden growth led to a cascading restart of all the master nodes.\n\netcd logged slow reads over the whole pod key range, for example a warning \"apply request took too long\" with \"took\": \"130.768157ms\", \"expected-duration\": \"100ms\", a read-only range from \"/registry/pods/\" to \"/registry/pods0\" and \"range_response_count:7130 size:13313287\".\n\nThe audit logs pointed to one DaemonSet. Whenever kube-apiserver restarted, each of its pods sent new list requests to fill its caches, which is the default behaviour of client-go informers: 60 list requests per pod at startup, with about 80 nodes in the cluster. The requests had the form /api/v1/pods?fieldSelector=spec.nodeName=$NODE_NAME. Redeploying the pods one after another left no mark on the memory graphs; only restarting them all together did.","ko":"일상적인 kube-apiserver 재시작 직후 etcd 메모리 사용량이 거의 세 배로 치솟았고, 그 여파로 모든 마스터 노드가 연쇄적으로 재시작했다. 고객사의 운영 클러스터였다.\n\netcd 로그에는 \"apply request took too long\" 경고가 찍혔다. took 는 130.768157ms, expected-duration 은 100ms 였고, 요청은 key:\"/registry/pods/\" range_end:\"/registry/pods0\" 인 read-only range 였다. 응답은 range_response_count:7130 size:13313287 이었다. 노드 하나의 파드만 묻는 요청이라 110개를 넘지 않아야 했는데 etcd 는 파드 7130개를 읽고 있었다.\n\n감사 로그를 보니 DaemonSet 하나가 원인이었다. kube-apiserver 가 재시작하면 이 DaemonSet 의 파드들이 캐시를 채우려고 list 요청을 새로 보냈다. 파드 하나가 시작하면서 list 요청을 60번 보냈고 클러스터 노드는 약 80개였다. 파드를 하나씩 차례로 재배포할 때는 메모리 그래프에 변화가 없었고, 한꺼번에 재배포할 때만 메모리가 치솟았다."},"updated_at":"2026-11-10T00:00:40.425800Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0194","org_slug":"palark"}