{"id":"IL-0386","org":"incident.io","date":"2023-09-26","docs":[],"title":{"en":"After a move to Google Cloud, Postgres connection timeouts and memcached i/o timeouts spiked on GKE nodes","ko":"Google Cloud 로 옮긴 뒤 GKE 노드에서 Postgres 연결 타임아웃과 memcached i/o timeout 이 간헐적으로 튀었다"},"domain":["kubernetes","gcp","database","networking"],"impact":{"scope":"A small share of requests hit Postgres connection timeouts and memcached i/o timeouts; at times health checks on an affected node failed","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Network timeouts are not always local to one request or pod: when they appear, the whole activity of the node, including other pods on it, needs to be checked. Staying within a provider's rate limits does not keep outbound calls from affecting a node, so outbound network calls need more sensible concurrency limits.","ko":"가설을 너무 빨리 좁히지 않는다. 이런 네트워크 타임아웃을 보면 같은 파드만이 아니라 같은 노드에서 도는 모든 것을 포함해 시스템 전체의 활동을 봐야 한다. 오류와 그 영향은 대개 요청 하나에 머물지만 늘 그렇지는 않다. 외부로 나가는 네트워크 호출의 동시성도 제한해야 한다. 이전에는 공급자의 rate limit 이 주된 제약이었지만, rate limit 안에 있어도 노드에 영향을 줄 수 있다."},"related":[],"sources":[{"url":"https://incident.io/blog/clouds-caches-and-connection-conundrums","kind":"blog","archived":"https://web.archive.org/web/20260518001502/https://incident.io/blog/clouds-caches-and-connection-conundrums"}],"summary":{"en":"incident.io runs a simple GKE setup, and the only relevant configurable behaviour of the cluster was that Dataplane V2 was turned on. After the move into Google Cloud, Postgres connection timeouts rose, and the memcached cache also had sporadic timeouts. The memcache client used a 100ms network timeout, so any dropped TCP packet was likely to end in a timeout, and it kept the default of 2 idle connections.\n\nThe team first doubled the Postgres maximum connection lifespan and made the connection pools static, which brought new connections down to about 1/s, and then raised MaxIdleConns from 2 to 20, which reduced the cache timeouts. Meanwhile an unrelated accidental database join had been spawning thousands of goroutines that called an external provider; when its fix landed, the database and cache errors nearly stopped. The remaining cache timeouts happened when a single request set a few hundred cache values at once while also generating many Google Cloud Storage URLs, and at those moments the node showed a spike in new connections and dropped packets.\n\nThe GKE documentation states that the Dataplane V2 agent, anetd, can consume significant CPU when many TCP connections are opened and closed rapidly on a node, and recommends keep-alives and connection pooling. incident.io found that this matched everything seen so far: it judged it likely that its own bursts of short-lived connections kept anetd on the node very busy for a couple of seconds at a time, and each fix that reduced new connections had helped, while the problem persisted, less often, whenever many external requests ran in parallel. The team decided not to turn off Dataplane V2, because that would require a new cluster and would only hide a pattern the application itself was causing, and instead planned to limit outbound network concurrency.","ko":"incident.io 는 인프라를 Google Cloud 로 모두 옮겼고, 옮긴 뒤 Postgres 연결 타임아웃이 평소보다 많이 나서 인시던트를 선언했다. 초당 새 연결이 수백 개씩 오르내리는 것을 보고 연결이 만료될 때의 경쟁 조건을 의심했다. 그래서 연결 최대 수명을 15m 에서 30m 로 늘리고 연결 풀을 고정 크기로 바꿨다. 새 연결은 초당 약 200개에서 약 1개로 줄었다. memcached 에서도 연결 타임아웃이 났고, MaxIdleConns 를 2 에서 20 으로 올리자 바로 나아졌지만 며칠 뒤 read i/o 타임아웃이 훨씬 많아졌다. 변수를 줄이려고 Google 이 운영하는 memcached 를 쿠버네티스 클러스터 안의 자체 memcached 로 옮겼지만 거의 달라지지 않았다.\n\n같은 때 실수로 들어간 데이터베이스 join 때문에 외부 제3자 서비스로 중복 네트워크 호출을 수천 번 보내던 문제가 따로 있었다. 이 문제를 고친 코드가 운영에 배포되자 데이터베이스 연결 문제와 캐시 문제가 거의 사라졌다. 남은 캐시 오류를 쫓아 영향받은 요청의 트레이스와 노드 로그를 보니, 요청 하나에서 캐시 set 수백 번과 Google Cloud Storage URL 생성 호출이 한꺼번에 일어났다. 그 노드에서는 새 연결 수와 드롭된 패킷이 함께 늘었고, 다른 요청인 헬스 체크까지 캐시에 닿지 못했다. 문제는 프로세스 하나가 아니라 노드 전체에 걸쳐 있었다.\n\n클러스터 설정에서 관련 있어 보이는 것은 Dataplane V2 를 켠 것 하나였다. GKE 문서는 노드에서 TCP 연결을 짧게 열고 닫는 일이 많으면 GKE Dataplane V2 에이전트 파드인 anetd 가 인스턴스마다 vCPU 2~3개까지 쓸 수 있다고 적고, HTTP 호출의 keep-alive 와 연결 풀링을 권한다. anetd 로그에는 눈에 띄는 것이 없었지만 CPU 가 꽤 뛰었다. incident.io 는 지금까지의 관찰과 조치가 이 설명과 맞는다고 봤다. 연결 풀을 고쳐 새 연결을 줄이자 데이터베이스와 캐시 타임아웃이 대부분 사라졌다. 상황을 키운 것은 잘못된 쿼리가 goroutine 수천 개를 띄워 외부 연결을 연 일이었다. 외부 요청을 병렬로 많이 보낼 때는 문제가 드물게나마 계속 났다. Dataplane V2 를 끄는 방안도 검토했지만 택하지 않았다. 끄는 것은 스스로 일으킨 문제를 덮는 일이었다. 끄려면 새 클러스터를 만들어 워크로드를 옮겨야 했다. Dataplane V2 는 앞으로 Autopilot 클러스터에서 끌 수 없는 기본값이 될 가능성이 컸고, incident.io 는 엄격한 네트워크 정책 같은 장점도 쓰고 싶었다."},"trigger":{"en":"After the move to Google Cloud, the application opened hundreds of new Postgres connections per second, and an accidental database join made it send thousands of duplicate calls to an external third party in parallel.","ko":"노드에서 짧게 열고 닫는 TCP 연결이 대량으로 생겼다. 연결 풀이 초당 수백 개씩 새 연결을 열었고, 실수로 들어간 데이터베이스 join 때문에 외부 제3자 서비스로 가는 중복 호출 수천 번이 한꺼번에 나갔다."},"severity":"unknown","symptoms":["intermittent-timeouts","packet-loss"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"An incident was declared when the number of Postgres connection timeouts rose above normal. The remaining cache timeouts were traced through the logs of the Kubernetes node that served an affected request, the request's trace and the node's metrics.","ko":"Postgres 연결 타임아웃이 평소보다 많이 나는 것을 보고 인시던트를 선언했다."},"components":["cni-plugin","database","cache"],"root_cause":"capacity","remediation":[{"text":{"en":"Doubled the maximum Postgres connection lifespan from 15m to 30m","ko":"Postgres 연결의 최대 수명을 15m 에서 30m 로 두 배 늘렸다."}},{"text":{"en":"Made the Postgres connection pools static so they always keep N connections open","ko":"연결 풀을 늘 N개의 연결을 열어 두는 고정 크기로 바꿨다."}},{"text":{"en":"Raised the memcache client's MaxIdleConns from 2 to 20","ko":"memcache 클라이언트의 MaxIdleConns 를 2 에서 20 으로 올렸다."}},{"text":{"en":"Moved temporarily from Google's hosted memcached to a memcached inside the Kubernetes cluster","ko":"변수를 줄이려고 Google 이 운영하는 memcached 대신 쿠버네티스 클러스터 안에서 직접 돌리는 memcached 로 옮겼다. 이 전환으로는 거의 달라지지 않았다."}},{"text":{"en":"Shipped the fix for the accidental database join that made thousands of duplicate external calls","ko":"실수로 들어간 데이터베이스 join 때문에 외부 제3자 서비스로 중복 호출을 수천 번 보내던 문제의 수정을 운영에 배포했다. 그 뒤 데이터베이스 연결 문제와 캐시 문제가 거의 사라졌다."}}],"action_items":[{"text":{"en":"Set more sensible concurrency limits for outbound network calls","ko":"외부로 나가는 네트워크 호출에 더 알맞은 동시성 한도를 둔다. 공급자의 rate limit 안에 있어도 노드에 영향을 줄 수 있기 때문이다."}}],"contributing":["default-config-unchanged","overlapping-failures"],"recurrence_of":null,"symptoms_text":{"en":"Shortly after incident.io moved its infrastructure fully into Google Cloud, the application saw a larger-than-normal number of Postgres connection timeouts, and new database connections per second were in the hundreds, spiking up and down. At the same time, the memcached instance used for transient data and faster API responses had sporadic connection timeouts.\n\nThe cache error read \"saving value to cache: setting cache value: read tcp 10.16.4.33:35586->10.67.0.5:11211: i/o timeout\". It appeared most often when caching the list of timeline events for an incident, and only for a tiny handful of requests among hundreds of thousands, with no consistent pattern in users, organizations, nodes or requests. On the Kubernetes node that served one failing request, a separate request also failed its health check because it could not reach the cache.\n\nThe errors changed as the team made fixes. Doubling the maximum Postgres connection lifespan from 15m to 30m and making the connection pools static cut new connections from about 200/s to about 1/s. Raising the memcache client's MaxIdleConns from 2 to 20 helped at once, but a few days later many more \"read i/o\" timeouts appeared. Moving from Google's hosted memcached to a memcached inside the Kubernetes cluster made little difference. When a fix for an unrelated bad database join, which had been making thousands of duplicate calls to an external third party, reached production, the database and cache errors nearly disappeared, but some cache errors remained.\n\nThe first theory was a race in which goroutines reused an expiring connection and then opened new connections en masse. The team also noticed that pooled memcache connections were never checked for health, but did not think that was what was happening. The trace of an affected request showed a few hundred cache set operations at the same moment, together with many calls to generate Google Cloud Storage URLs, with call durations rising from about 1 ms by a few orders of magnitude. Node metrics showed a spike in new connections and more dropped packets on that node, which pointed at the node as a whole rather than one process. The GKE documentation says the Dataplane V2 agent pods (anetd) can use two or three vCPUs when many TCP connections are opened and closed quickly on a node; the anetd logs showed nothing obvious, but its CPU use had jumped.","ko":"인프라를 Google Cloud 로 모두 옮긴 뒤 Postgres 연결 타임아웃이 평소보다 많이 나서 인시던트를 선언했다. 초당 새로 여는 연결 수가 수백 개 수준에서 크게 오르내렸다.\n\n일부 API 응답을 빠르게 하려고 쓰는 memcached 에서도 연결 타임아웃이 띄엄띄엄 났다. memcache 클라이언트의 기본 네트워크 타임아웃은 100ms 였고, memcache 서버까지는 보통 1ms 도 걸리지 않았다.\n\nmemcache 클라이언트의 MaxIdleConns 를 올리고 며칠이 지나자 \"read i/o\" 타임아웃이 훨씬 많이 보였다. 앱은 캐시에 값을 쓰다가 saving value to cache: setting cache value: read tcp 10.16.4.33:35586->10.67.0.5:11211: i/o timeout 오류를 냈다.\n\n이 오류는 사고 타임라인 이벤트 목록을 캐시에 넣을 때 가장 자주 났다. 수십만 건의 요청 가운데 몇 건에서만 났고, 사용자, 조직, 노드, 요청 어느 쪽으로도 일정한 패턴이 보이지 않았다.\n\n영향받은 요청 하나의 트레이스를 보니 캐시 set 이 수백 번 거의 동시에 일어났고, 같은 때 Google Cloud Storage 객체의 URL 을 만드는 호출도 아주 많았다. 캐시 호출 시간은 처음에 약 1ms 이하였다가 몇 자릿수만큼 늘어났고, 편차도 크게 벌어졌다.\n\n그 요청을 처리한 쿠버네티스 노드의 지표에서는 새로 여는 연결 수가 크게 튀었고, 같은 때 드롭된 패킷이 늘었다. 노드 로그에는 전혀 다른 요청인 헬스 체크까지 캐시에 닿지 못해 실패한 기록이 있었다.\n\n처음에는 연결이 만료될 때 여러 goroutine 이 같은 죽은 연결을 한꺼번에 다시 쓰려다 새 연결을 대량으로 여는 경쟁 조건을 의심했다. 연결 설정을 바꾸자 새 연결은 초당 약 200개에서 약 1개로 줄었지만, 캐시 오류는 완전히 사라지지 않았다.\n\n변수를 줄이려고 memcached 를 Google 이 운영하는 서비스에서 클러스터 안의 자체 memcached 로 옮겼지만 거의 달라지지 않았다. 그러다 외부 서비스로 중복 호출을 보내던 별개의 문제를 고친 코드가 운영에 배포되자 문제가 거의 사라졌다. incident.io 는 이것을 보고 GKE 나 GCP 네트워크 자체에는 문제가 없을 가능성이 크다고 봤다."},"updated_at":"2026-11-14T12:34:03.743516Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0386","org_slug":"incident-io"}