LabHub
はじめる
배우기 러닝패스 코스

リアルタイム通信 — WebSocket・gRPC ストリーミング・WebRTC

コネクションプールが枯れると速いリクエストも並ぶ

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

커넥션 풀이 마르면 서버는 한가한데 클라이언트의 모든 요청이 줄을 서고, 원인은 대개 풀 크기가 아니라 타임아웃과 폐기 규칙입니다.

왜 이게 필요했나

HTTP 클라이언트, DB 드라이버, gRPC 채널은 모두 안에 풀을 가지고 있습니다. 연결을 만드는 비용 — TCP 핸드셰이크, TLS 핸드셰이크, 인증 — 을 요청마다 치르지 않으려는 것입니다. 풀은 보통 조용히 잘 돌다가 어느 날 한꺼번에 무너집니다. 그리고 무너지는 모습이 원인과 멀리 떨어져 있습니다. 서버 CPU 는 한가하고, 서버 쪽 지연 지표도 멀쩡한데, 클라이언트의 요청만 수 초씩 걸리거나 "풀에서 연결을 얻지 못했다" 는 오류가 쏟아집니다.

리틀의 법칙이 이 현상을 한 줄로 설명합니다. 동시에 필요한 연결 수는 초당 요청 수에 요청 하나의 지연을 곱한 값입니다. 초당 200개를 50ms 에 처리하면 연결 10개면 됩니다. 뒤의 서비스 하나가 느려져 지연이 5초가 되면 같은 부하에 연결 1,000개가 필요해집니다. 풀 상한이 10이면 나머지는 줄을 섭니다. 풀이 고갈된 것은 결과이고, 원인은 지연입니다.

어떻게 동작하나

풀을 이루는 숫자는 다섯 가지이고, 각각 막는 사고가 다릅니다.

설정 막는 것 없으면
최대 크기 서버에 연결을 무한정 여는 것 장애 때 클라이언트가 서버를 두들긴다
획득 대기 시간 빈자리를 끝없이 기다리는 것 호출 스레드가 전부 풀 앞에 멈춘다
요청 타임아웃 느린 호출이 자리를 무한정 차지하는 것 느린 호출 몇 개가 풀 전체를 붙잡는다
유휴 한도 상대가 이미 닫은 연결을 다시 쓰는 것 한동안 조용하다가 첫 요청이 재설정으로 실패한다
최대 수명 오래된 연결이 한 서버에 쏠리는 것 서버를 늘려도 부하가 옮겨 가지 않는다

획득 대기 시간과 요청 타임아웃은 자주 헷갈립니다. 획득 대기 시간은 기다리는 쪽을 구해 줄 뿐, 자리를 차지한 쪽을 끝내지 못합니다. 5초 걸리는 호출 네 개가 크기 4 인 풀을 채우면, 획득 대기 시간이 1.5초인 빠른 호출은 전부 실패합니다. 느린 호출에 요청 타임아웃이 걸려 있어야 자리가 돌아옵니다.

더 조용한 함정은 타임아웃 난 연결을 풀에 돌려놓는 것입니다. 타임아웃은 기다리기를 그만뒀다는 뜻이지 서버가 응답을 안 보낸다는 뜻이 아닙니다. 늦은 응답은 같은 소켓으로 도착하고, 그 소켓을 다음에 빌린 요청은 남의 응답을 자기 것으로 읽습니다. HTTP/1.1 처럼 요청과 응답이 순서로만 짝지어지는 프로토콜에서는 이것이 데이터가 뒤섞이는 사고가 됩니다. 규칙은 하나입니다. 상태를 모르는 연결은 버립니다.

유휴 한도는 상대의 유휴 한도보다 짧아야 합니다. 서버와 로드밸런서는 조용한 연결을 먼저 끊습니다. Node.js 의 HTTP 서버는 keepAliveTimeout 기본값이 5초라서, 유휴 한도가 그보다 긴 클라이언트 풀은 6초 쉰 연결을 꺼내 쓰다가 연결 재설정을 받습니다. AWS 의 Application Load Balancer 는 유휴 한도 기본값이 60초입니다.

최대 수명은 부하 분산과 관련이 있습니다. 풀은 한번 만든 연결을 오래 쓰려고 하므로, 서버를 늘려도 기존 연결은 원래 서버에 머뭅니다. 연결에 수명을 두고 다 쓰면 닫아 새로 만들게 하면, 새 연결이 새 서버로 가면서 부하가 서서히 고르게 퍼집니다. 모든 연결의 수명이 같은 순간에 끝나지 않도록 수명에도 약간의 무작위성을 섞습니다.

현장에서 만나는 모습

가장 흔한 장면은 외부 API 하나가 느려지자 전혀 상관없는 API 까지 느려지는 것입니다. 둘이 같은 HTTP 클라이언트, 같은 풀을 쓰고 있었고, 느린 쪽이 풀을 다 차지했습니다. 목적지마다 풀을 나누거나(벌크헤드), 적어도 요청 타임아웃을 목적지마다 따로 두는 것이 처방입니다.

HTTP/2 와 gRPC 클라이언트에서는 풀의 모양이 조금 다릅니다. 연결 하나가 스트림 여럿을 싣기 때문에 "빌리는 것" 은 연결이 아니라 스트림이고, 상한은 상대가 알린 동시 스트림 수(SETTINGS_MAX_CONCURRENT_STREAMS)가 정합니다. 그 한도에 닿으면 새 호출은 클라이언트 안에서 줄을 서고, 이것도 서버 지표에는 보이지 않습니다. 연결 하나로 버티려다 한도에 막히는 서비스는 연결을 몇 개 더 여는 것으로 풀기도 합니다. 결국 같은 질문 — 무엇이 몇 개까지 동시에 가능하고, 넘치면 어디서 기다리는가 — 을 층마다 다시 묻게 됩니다.

관측도 풀에서 해야 합니다. 서버 쪽 지표에는 줄이 보이지 않으므로, 풀의 사용 중·쉬는 중·기다리는 중 연결 수와 획득 대기 시간, 획득 실패 횟수를 클라이언트가 직접 내보내야 합니다. 이 숫자가 없으면 장애 때 "서버는 멀쩡하다" 는 말만 오갑니다. 특히 획득 대기 시간은 요청 지연 안에 섞여 보고되기 쉬우므로 따로 떼어 재야, 느려진 것이 서버인지 풀 앞의 줄인지를 가를 수 있습니다.

다음 실습에서 할 것

상한·대기 시간·폐기 규칙·지표·유휴 한도를 가진 풀을 표준 라이브러리로 만듭니다. 타임아웃 난 소켓을 돌려놓으면 다음 요청이 지난 응답을 읽는 장면을 재현하고, 5초 걸리는 호출 넷이 풀을 말리는 장면과 0.5초 타임아웃 하나로 살아나는 장면을 같은 측정기로 잽니다.