Утечка памяти Golang в отношении горутин

У меня есть программа Go, которая работает непрерывно и полностью полагается на горутины + 1 поток manager. Основной поток просто вызывает горутины и в противном случае спит.

Есть утечка памяти. Программа использует все больше и больше памяти, пока не истощит все 16 ГБ ОЗУ + 32 ГБ SWAP, и тогда каждая горутина паникует. На самом деле панику вызывает память ОС, обычно паника fork/exec ./anotherapp: cannot allocate memory, когда я пытаюсь выполнить anotherapp.

Когда это произойдет, все рабочие потоки перестанут работать, будут восстановлены и перезапущены. Таким образом, каждая горутина будет паниковать, восстанавливаться и перезапускаться... и в этот момент использование памяти не уменьшится, оно останется на уровне 48 ГБ, хотя сейчас практически ничего не выделено. Это означает, что все горутины всегда будут паниковать, поскольку памяти никогда не бывает достаточно, пока весь исполняемый файл не будет уничтожен и полностью перезапущен.

Всего это около 50 000 строк, но фактическая проблемная область выглядит следующим образом:

type queue struct {
    identifier string
    type bool
}

func main() {

    // Set number of gorountines that can be run
    var xthreads int32 = 10
    var usedthreads int32
    runtime.GOMAXPROCS(14)
    ready := make(chan *queue, 5)

    // Start the manager goroutine, which prepared identifiers in the background ready for processing, always with 5 waiting to go
    go manager(ready)

    // Start creating goroutines to process as they are ready
    for obj := range ready { // loops through "ready" channel and waits when there is nothing

        // This section uses atomic instead of a blocking channel in an earlier attempt to stop the memory leak, but it didn't work
        for atomic.LoadInt32(&usedthreads) >= xthreads {
            time.Sleep(time.Second)
        }
        debug.FreeOSMemory() // Try to clean up the memory, also did not stop the leak
        atomic.AddInt32(&usedthreads, 1) // Mark goroutine as started

        // Unleak obj, probably unnecessary, but just to be safe
        copy := new(queue)
        copy.identifier = unleak.String(obj.identifier) // unleak is a 3rd party package that makes a copy of the string
        copy.type = obj.type
        go runit(copy, &usedthreads) // Start the processing thread

    }

    fmt.Println(`END`) // This should never happen as the channels are never closed
}

func manager(ready chan *queue) {
    // This thread communicates with another server and fills the "ready" channel
}

// This is the goroutine
func runit(obj *queue, threadcount *int32) {
    defer func() {
        if r := recover(); r != nil {
            // Panicked
            erstring := fmt.Sprint(r)
            reportFatal(obj.identifier, erstring)
        } else {
            // Completed successfully
            reportDone(obj.identifier)
        }
        atomic.AddInt32(threadcount, -1) // Mark goroutine as finished
    }()
    do(obj) // This function does the actual processing
}

Насколько я вижу, когда функция do (последняя строка) завершается либо по завершению, либо по панике, функция runit затем завершается, что полностью завершает горутину, что означает, что вся память из этой горутины теперь должна быть свободна. . Вот что происходит сейчас. Что происходит, так это то, что это приложение просто использует все больше и больше и больше памяти, пока оно не перестанет функционировать, все runit горутины паникуют, но память не уменьшается.

Профилирование ничего подозрительного не выявляет. Утечка, похоже, выходит за рамки профилировщика.


person Alasdair    schedule 04.02.2015    source источник
comment
Я бы попробовал код, который вы разместили, без кода, который вы не опубликовали. Используйте функцию manager(), которая просто бесконечно генерирует ввод, и функцию do(), которая ничего не делает (пустая функция). Посмотрите, есть ли у вас все еще утечка памяти. Если нет, то, очевидно, утечка находится в коде, который вы не опубликовали, и в этом случае мы ничего не можем сделать в текущем состоянии вопроса.   -  person icza    schedule 04.02.2015
comment
Вы где-нибудь используете unsafe или C? Последняя версия Go? Я бы попробовал запустить его с GODEBUG=gctrace=1, чтобы проверить, что происходит со сборщиком мусора.   -  person siritinga    schedule 04.02.2015
comment
Я не знаю, задумано это или нет, но нет гарантии, что этот код будет использовать максимум 10 горутин. Если вы хотите ограничить количество рабочих до 10, выполните это. Код выше имеет имя поля type, которое не будет компилироваться. Можете ли вы показать реальный код?   -  person Cerise Limón    schedule 04.02.2015
comment
Может быть, каким-то образом происходит утечка горутин: что-то никогда не выходит. Есть старые заявления из списков рассылки о том, что стеки горутин никогда не возвращаются в систему. goroutineCount, _ := runtime.GoroutineProfile(nil) может быстро сказать вам, сколько работает.   -  person twotwotwo    schedule 05.02.2015
comment
Комментарий ThunderCat точен. Код, который вы пытаетесь использовать для ограничения количества рабочих потоков, выглядит низкоуровневым и откровенно подверженным ошибкам. Скорее всего, вы захотите воспользоваться подходом из: talks.golang.org/2012/waza. слайд № 41, где вы ограничиваете количество рабочих горутин, которые получают данные из общего канала работы.   -  person dyoo    schedule 05.02.2015
comment
Спасибо, я перешел с атомарного на фиксированное количество горутин, которые используются повторно. На самом деле это иронично, потому что я избегал этого, специально думая, что это увеличит использование памяти. Через пару дней можно будет увидеть результаты. Тем не менее это должно представлять собой ошибку в компиляторе, не так ли? (Предполагая, что это исправляет это.)   -  person Alasdair    schedule 05.02.2015
comment
Так что теперь проблема усугубляется тем, что горутины используются повторно, а не уничтожаются. Таким образом, это указывает на утечку памяти, не связанную с горутинами, но это должен быть пакет, использующий C или небезопасный. Я буду исследовать все такие пакеты.   -  person Alasdair    schedule 06.02.2015


Ответы (1)


Рассмотрите возможность инвертирования шаблона, см. здесь или ниже....

package main

import (
    "log"
    "math/rand"
    "sync"
    "time"
)

// I do work
func worker(id int, work chan int) {
    for i := range work {
        // Work simulation
        log.Printf("Worker %d, sleeping for %d seconds\n", id, i)
        time.Sleep(time.Duration(rand.Intn(i)) * time.Second)
    }
}

// Return some fake work
func getWork() int {
    return rand.Intn(2) + 1
}

func main() {
    wg := new(sync.WaitGroup)
    work := make(chan int)

    // run 10 workers
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            worker(i, work)
            wg.Done()
        }(i)
    }

    // main "thread"
    for i := 0; i < 100; i++ {
        work <- getWork()
    }

    // signal there is no more work to be done
    close(work)

    // Wait for the workers to exit
    wg.Wait()
}
person freeformz    schedule 08.04.2015