본문 바로가기

Golang

[Golang] Memory Leak 예방하기

728x90

http Request

response Body should be READ & CLOSED

Golang http Client.Do() 문서에 보면 아래와 같은 내용이 명시

If the returned error is nil, the Response will contain a non-nil Body which the user is expected to close. If the Body is not both read to EOF and closed, the Client's underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request.

  • response Body를 EOF까지 모두 읽고(ioutil.ReadAll()), 닫아야한다(response.Body.Close())
  • tcp커넥션 및 http client pool과 연관이 큼
resp, err := http.Get("<https://api.ipify.org?format=json>")
if resp != nil {
    // 일반적인 경우에는 err가 nil이 아니면 resp가 nil이지만
    // redirection 실패의 경우에는 resp가 nil이 아닐수도 있다고 함
    defer resp.Body.Close()
}

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

body, err := ioutil.ReadAll(resp.Body)
// _, err = io.Copy(ioutil.Discard, resp.Body) 만약 body를 사용하지 않는다면
if err != nil {
    fmt.Println(err)
    return
}

fmt.Println(string(body))
  • for문 안에서 http request를 보내는 경우
	for i := 0; i < 10; i++ {
		resp, err := http.Get("https://api.ipify.org?format=json")
		if err != nil {
			fmt.Println(err)
			return
		}
		body, err := ioutil.ReadAll(resp.Body)
		// body를 읽은 다음 바로 Close를 하여 커넥션 재사용이 가능하게
		// defer를 한다면 for문이 종료되기 전까지 close가 되지 않아 재사용이 안될 가능성 존재
		if resp != nil {
			resp.Body.Close()
		}
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println(string(body))
	}

Mongo Cursor

3가지 방식의 read가 존재

  • cursor.Next()
  • cursor.ReadAll()
  • cursor.TryNext() (tailable cursor)

기본적으로 Client가 데이터를 모두 소비하면 커서는 close 된다고 기술되어 있음. 하지만 cursor.Next() cursor.TryNext() 는 close가 되지 않아(individual, tailable), 명시적으로 cursor를 닫아주어야 함.

func (manager *Manager) decodeCursor(cursor *mongo.Cursor) ([]Document, error) {
	if cursor != nil {
		defer cursor.Close(context.TODO())
		res := make([]Document, 0, 2)
		for cursor.Next(context.TODO()) {
			var document Document
			err := cursor.Decode(&document)
			if err == nil {
				res = append(res, document)
			} else {
				return nil, err
			}
		}
		return res, nil
	}
	return nil, errors.New("failed to find result")
}

Go Channel Timeout Handling

chanSample := make(chan sampleChannel, 3)
wg := sync.WaitGroup{}

wg.Add(1)
go func() {
    defer wg.Done()
    chanSample <- u.getDataFromGoogle(id, anotherParam) // 함수의 예시
}()

wg.Add(1)
go func() {
    defer wg.Done()
    chanSample <- u.getDataFromFacebook(id, anotherParam)
}()

wg.Add(1)
go func() {
    defer wg.Done()
     chanSample <- u.getDataFromTwitter(id,anotherParam)
}()

wg.Wait()
close(chanSample)

위와 같이 3개의 API를 고루틴으로 실행하여, WaitGroup이 모든 프로세스가 끝날 때까지 대기하고 있기 때문에 모든 응답을 처리하고하기 위해서는 모든 API 호출이 끝날때까지 기다림

이 코드는 하나의 서비스가 죽었을 때 큰 문제를 일으킬 수 있다고 함. 죽은 서비스가 복구될때까지 계속 기다릴 것이기 때문

Goroutine Cancellation

if c== nil {
	c= context.Background()
}

ctx, cancel := context.WithTimeout(c, time.Second * 2)
defer cancel()

chanSample := make(chan sampleChannel, 3)
defer close(chanSample)

go func() {
	chanSample <- u.getDataFromGoogle(ctx, id, anotherParam) // 함수의 예시
}()

go func() {
	chanSample <- u.getDataFromFacebook(ctx, id, anotherParam)
}()

go func() {
	chanSample <- u.getDataFromTwitter(ctx, id,anotherParam)
}()

result := make([]*feed.Feed, 0)

for loop := 0; loop < 3; loop++ {
	select {
	case sampleItem := <-chanSample:
		if sampleItem.Err != nil {
			continue
		}
		if feedItem.Data == nil {
			continue
		}
		result = append(result,sampleItem.Data)
		// ============================================================
		// 일관성 없는 데이터를 방지하기 위해 컨텍스트가 타임아웃을 초과하는 경우
		// 유저에게 에러 메시지를 보낸다
		// ============================================================
	case <-ctx.Done(): // 컨텍스트가 타임아웃을 초과했다는 알림 시그널을 받음
		err := fmt.Errorf("Timeout to get sample id: %d. ", id)
		return result, err
	}
}

  • api에도 ctx 추가하여 timeout 설정하여 api request하는 gorutine도 취소될 수 있게 함.
func (u *usecase) getDataFromFacebook(ctx context.Context, id int64, param string) sampleChanel{
	req,err := http.NewRequest("GET", "<https://facebook.com>", nil)
	if err != nil {
	    return sampleChannel{
	        Err: err,
	    }
	}
	// ============================================================
	// 요청에 컨텍스트를 전달한다
	// 이 기능은 Go 1.7부터 사용할 수 있다
	// ============================================================
	if ctx != nil {
	    req = req.WithContext(ctx) // HTTP 호출 요청에 컨텍스트를 사용.
	}
}

Too Large Size of variable(?)

원래는 스택 메모리에 할당된 뒤 다른 곳에서 변수를 참조하는 일이 없기 때문에 바로 release되어 스택 메모리에서 점유가 해제

하지만 몇몇 경우에 스택이 아닌 힙에 데이터가 저장될 수 있다고 하는데(http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars), 이 경우에는 너무 큰 값을 선언하여 스택이 아닌 힙에 데이터가 저장되었고, 힙이 할당받은 메모리를 해제해주지 않아 생기는 문제 (https://umi0410.github.io/blog/golang/go-memory-leak-issue/)

func DoFloat(){
    var tmp [400000000]float64 // 3.2GB
    tmp[0] = 0 // tmp에 접근하지 않으면 unused variable이 되기 때문에 dummy한 access 작업 수행
}

	// tmp = nil 을 명시적으로 해주어 더이상 사용하지 않는다고 마킹 

참고 자료

[번역] Go API에서 메모리 누수 예방하기

개발 썰 - Go Memory Leak(메모리 누수) 관련 이슈

50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

Goroutine leak detector to help avoid Goroutine leaks.

728x90