본문 바로가기
Work/개발 노트

[Go언어] API 서버간 내부 통신 시 too many open files 문제

by ★용호★ 2020. 12. 5.

다수의 API 서버를 실행 중이고, 각 서버간에는 주기적으로 상호 통신을 실행하고 있다. 이 때 일정 시간이 경과하면 아래와 같은 오류가 발생하면서 먹통이 되는 현상이 발생하였다.

dial tcp: lookup 127.0.0.1: too many open files

 

먼저 메시지 내용 처럼 현재 얼마만큼의 파일이 Open 되었는지 확인해보기 위해 아래와 같이 lsof 명령을 사용하였다.

lsof | wc -l


문제가 발생한 대상 API 서버에서 too many open files 발생 시 file open 수는 17141개 였는데 재시작 후 6994로 감소하였다. 이로 인해 프로세스 실행 후 문제가 발생한 시점까지 대략 10147개의 file open이 추가된 것으로 추정되는 상황이다. (다른 프로세스로 인해 Open된 file도 존재하겠지만 증감폭이 미미하기 때문에 무시하였다)

문제가 발생한 어플리케이션에서는 네트워크 통신이 많았기 때문에 네트워크 Connection에 대해 모니터링하기 위해 조금 더 세분화해서 IPv4에 해당하는 네트워크 connection open 수 아래와 같이 lsof 명령을 실행하여 확인해보았다.

lsof -i 4


실행 결과 확실히 대상 어플리케이션에서 Open된 네트워크 connection 수가 대다수였고, watch 명령을 사용하여 모니터링한 결과 1분 간격으로 3~4개의 file open이 추가되는 것을 확인하였다.

watch -n1 lsof -i 4

 


1분 간격이라는 특정 주기를 가지고 있다는 것에 초점을 맞춰서 원인을 분석한 결과 해당 API 서버에서 다른 API 서버로 Request를 전달하는 부분에서 정상적으로 Connection이 close 되지 않는 것을 발견하였다.

HTTP 프로토콜은 Stateless하여 Request 후 Response를 받으면 연결을 끊지만, 재연결 시 Handshake에 들어가는 비용을 최소화하기 위해 연결을 유지하도록 설정할 수도 있다. Golang에서 http 패키지 사용 시 네트워크 연결을 계속 유지하는 것이 기본 설정이지만 HTTP 헤더에 Connection: close를 명시적으로 선언하던가 http 설정에 연결 유지 설정을 비활성화 하는 것으로 close를 시킬 수 있다. 즉,  Connection이 Close 되지 않으면 File Descriptor가 부족해질 수 있다.

 

req, err := http.NewRequest("GET","http://example.com",nil)
req.Close = true
// 또는 req.Header.Add("Connection", "close")
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}

 

동일한 HTTP 서버에 많은 요청을 보내는 경우에는 재연결을 위해 네트워크 연결을 유지하는 것이 좋지만 트래픽이 그렇게 많지 않다면 Response 직 후에 연결을 Close 하는 것이 더 나을 수 있다.

연결 유지를 사용할 경우 해당 Response 객체의 Body 내용을 모두 읽어서 재사용할 수 있는 상태로 만들어주어야 하기 때문에 아래와 같이 구현을 해주어야 한다.

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    fmt.Println(err)
    return
}

 

대부분의 경우 Reponse로 받은 데이터를 사용하기 위해 위와 같이 Body를 모두 읽어서 사용하지만 그렇지 않은 경우도 있을 수 있기 때문에 아래와 같이 Body 내용을 버리도록 구현할 수도 있다.

_, err = io.Copy(ioutil.Discard, resp.Body)
if err != nil {
    return "", resp, err
}

 

Body를 읽는 작업 조차도 리소스를 소모하는 작업이기 때문에 Body 크기가 큰 경우에는 오히려 새 연결을 만드는 것이 더 나을 수도 있다.

 

여기서 연결 유지를 사용하는 경우 Connection이 어차피 재사용 될 것이라고 생각했기 때문에 같은 클라이언트로부터의 연결 요청에 Connection이 계속 증가되는 것에 의문을 갖게 되었다. 그래서 코드를 조금 더 자세히 살펴보니 매 요청마다 http Client를 새로 생성해서 사용했기 때문에 대상 서버로 연결 요청 시 매번 다른 Port로 요청을하게 되면서 서로 다른 Client가 되었던 것이었다.

 

c := &http.Client{Transport: &http.Transport{
    TLSClientConfig: &tls.Config{
        InsecureSkipVerify: true,
    },
    DisableKeepAlives: false,
}}

 

테스트를 위해 http Client 사용 부분을 Singleton으로 변경하고 나서 다시 확인해보니 하나의 Connection을 재사용하여 더이상 Connection 수가 증가하지 않는 것을 확인할 수 있었고, 이로 인해 File Descriptor 수도 증가하지 않아 기존 문제를 해결할 수 있었다.

 

결국 문제되었던 부분은 Connection을 종료할 조건이 없기 때문에 연결이 끊어지지 않고 유지되었고, 서로 다른 HTTP Client로 요청했기 때문에 재사용도 되지 않아 계속해서 Connection이 증가하던 것이었다. 매번 HTTP Client를 새로 생성하려면 Connection이 종료될 수 있도록 Timeout 설정을 하거나 위에서 언급했던 것 처럼 연결유지를 사용하지 않는 것으로 이 문제를 해결할 수 있다. 

 

참고

 

댓글