본문 바로가기
프로그래밍/Go

[golang] api 메모리 누수 처리하기

by 동네로봇 2020. 10. 18.

go 서비스가 돌고 있는 linux 서버에서 어느 순간부터 too many open files 에러가 발생했다.

openfiles 값을 늘려주었지만 소용이 없었고, 오로지 서비스를 재시작해야지만 정상 동작하였다. 

하지만 시간이 지나면 다시 동일한 에러가 반복되어 나타났다. 

 

컴파일 혹은 동작 중 문제가 없었기 때문에 어떠한 에러로그도 쌓이지 않았고, 또한 서버에서 어떤 파일이 열려 있다는 것인지 또는 어떤 프로세스에 의해서 열리는 것인지 알 방도가 없었기 때문에 원인을 찾는 데 애를 먹었다. 

 

몇 주 동안 헤매고 있었을 때 문제를 해결할 수 있도록 해준 블로그들이다. (그저.. 빛... ㅠ)

블로그1

블로그2

 

결론부터 말하면 go 서버에서 API를 요청했을 때,

response 처리를 제대로 해주지 않아 메모리 누수가 발생했던 것이었다.

 

HTTP connection 이 끊어지지 않고 계속 유지되어 서비스를 사용할 수록 커넥션이 증가하고, 서버에서 열 수 있는 소켓 연결 개수를 넘어가는 순간 too many open files 에러가 발생했던 것이었다. 

 

 


블로그1에 나와있는 메모리 누수 예방 방법은 총 4단계이다.

 

1단계 : http.Client 커스터마이징

2단계 : 닫히지 않은 응답 바디에서의 메모리 누수 방지

3단계 : Go 채널의 타임아웃 제어

4단계 : 컨텍스트를 활용한 타임아웃 제어

 

이미 채널을 통한 고루틴 제어는 활용하고 있었기에, 1단계와 2단계를 적용한 것 만으로 문제는 해결되었다. 

 

1. http client를 직접 커스터마이징 하기

go 에서 제공하는 http 라이브러리를 사용하여 api 요청을 하는 경우, 디폴트 값을 사용하지 말고 직접 커스터마이징하여 사용하는 것이 위험을 줄일 수 있다. 

 

go doc 을 보면 transport 와 client 의 기본 설정 값을 확인할 수 있다. 

golang.org/src/net/http/transport.go

golang.org/src/net/http/client.go

 

http 연결을 재사용하지 않고 요청이 있을 때마다 연결을 하도록 transport 의 DisabelKeepAlives 값을 참으로 설정하였다. 

	// set tls enable
	tr := &http.Transport{
    		DisableKeepAlives: true;
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	cli := &http.Client{
		Transport: tr,
		Timeout:   time.Second * 10,
	}

 

 

연결을 재사용하는 경우 Dial, MaxIdleConns 값 등을 필요에 따라 설정해주면 된다.

 

2. HTTP 응답 바디 닫기

client.Do로 요청을 보내면 response를 받는데, 이때 이 response Body에 아무런 내용이 없다고 하더라도 반드시 읽은 후 바디를 닫아주어야 한다. 그렇지 않으면 서버에서 연결이 끝났다는 것을 인식하지 못한다. 서버 소켓 연결을 유지하는 것을 확인하고 싶다면 서버에서 다음 명령어를 통해 확인할 수 있다.

 

netstat -nat | awk '{print $6}' | sort | uniq -c | sort -n

여기서 연결 유지 상태인 established 의 개수가 시간이 지남에 따라 계속 증가한다면, go 에서 응답 바디가 닫히지 않고 있다는 뜻이다. 

 

response body를 닫는 코드에 대해서는 블로그2 에서 자세히 설명하고 있다.

바디를 읽고 닫아주기 위해서 다음과 같은 코드를 추가하였다. 

resp, err := client.Do(req)
if resp != nil {
	defer resp.Body.Close()
}

if err != nil {
	fmt.Println(err)
	return
}

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

fmt.Println(string(body))

 

만약 body 값을 사용하지 않는다면 그냥 읽기만 하도록 _ 로 값을 받는다던지 하면 된다. 마지막에는 무조건 body를 닫도록 한다.

 

이 두가지 처리를 통해서 too many open files 에러를 해결할 수 있었다.