Как найти утечку памяти?

В программе есть горутина, которая запускается раз в 5 минут и кладем в БД новые данные.
В основной программе работает телеграмм бот.
Если отключить горутину, то утечки нет.
Возможно дело в коннекте с БД: связь один раз открывается в файле main.go, а дальше везде передается объект БД, который не закрывается - программа работает вечно и код не доходит до выполнения строчки закрытия соединения.
//index.go
func main(){
        db, err := sql.Open("sqlite3", "test.db")
	go dbWorker(db) //вызов горутины из-за которой течет память
	defer db.Close()
        u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates:= bot.GetUpdatesChan(u)

	for update := range updates {
		if update.Message != nil { // If we got a message
			mes=sendReply(update.Message, bot, db)
			bot.Send(mes)
		}
	}
}

Код анонимизирован (имена переменных и полей в БД ненастоящие):
func f(rows *sql.Rows, db *sql.DB){
	var unworkingDeviceInfos []DeviceInfo
	for rows.Next() {
		var current DeviceInfo
		if err := rows.Scan(&current.c,&current.DeviceType,&current.User); err != nil {
			fmt.Println("Error while scanning...")
		}
		unworkingDeviceInfos = append(unworkingDeviceInfos, current)
	}
	fmt.Println("Count of unworkingDevices: ", len(unworkingDeviceInfos))
	for i:=0; i<len(unworkingDeviceInfos); i++ {
		current:=unworkingDeviceInfos[i]
		device := NewDevice(current.c, current.DeviceLogin, current.DevicePassword, current.DeviceType)
		rawJSON, err:=device.getStats()
		if err==nil{
			duration,err:=getDeviceDowntime(db, current.c)
			if err!=nil{
				log.Println("getDeviceDowntime raise error %v", err)
			}
			sendWakeupMessage(current.c)
			if duration<=650{
				info, err:=parseStats(rawJSON, current.DeviceType)
				query,err:=db.Prepare("INSERT INTO reports(a,b,c,d,e,f,g,h,x,y,z) VALUES(?,?,?,?,?,?,?,?,?,?,?)")
				if err!=nil{
					fmt.Println("Error with inserting")
				}
				query.Exec(time.Now(),current.User,current.c,RESTARTING,info.c, info.f, info.d, info.e, info.x, info.y,0)
			}
			query,err:=db.Prepare("INSERT INTO reports(a,user,c,d,e,f,g,h,x,y,z) VALUES(?,?,?,?,?,?,?,?,?,?,?)")
			if err!=nil{
				fmt.Println("Error with inserting")
			}
			f:=0
			query.Exec(time.Now(),current.User,current.c,FAIL,NORMAL, f, 0, 0, "", "",duration)
			queryStr:=fmt.Sprintf("UPDATE devices SET d=0,reported=0 WHERE c='%s'",current.c)
			db.Exec(queryStr)
		}
	}
}

func g(rows *sql.Rows, db *sql.DB){
	var workingDeviceInfos []DeviceInfo
	for rows.Next() {
		var current DeviceInfo
		if err := rows.Scan(&current.c,&current.DeviceType,&current.User, &current.Reported); err != nil {
			fmt.Println("Error while scanning...")
		}
		workingDeviceInfos = append(workingDeviceInfos, current)
	}
	for i:=0; i<len(workingDeviceInfos); i++ {
		var current DeviceInfo
		var Device Device
		var err error
		current=workingDeviceInfos[i]
		Device = NewDevice(current.c, current.DeviceLogin, current.DevicePassword, current.DeviceType)
		var rawJSON []byte
		rawJSON, err=device.getStats()
		var info UniversalStatsType
		if err!=nil{
			log.Println("Possible dead device: ", current.c)
			if (current.Reported==0){
				sendAlert(current.c, current.User)
			}
			date:=time.Now().Unix()
			query:=fmt.Sprintf("UPDATE devices SET d=%d,failDate=%d,reported=1 WHERE c='%s'",RESTARTING,date,current.c)
			
			db.Exec(query)
		} else {
			fmt.Println("Good Device")
			info, err=parseStats(rawJSON, current.DeviceType)
			if err!=nil{
				log.Println("Error while parsing stats!")
				continue
			}
			//Записываем
			query,err:=db.Prepare("INSERT INTO reports(date,user,c,d,e,f,rt,e,x,y,z) VALUES(?,?,?,?,?,?,?,?,?,?,?)")
			if err!=nil{
				fmt.Println("Error with inserting")
			}
			query.Exec(time.Now(),current.User,current.c,info.d,info.s, info.f, info.i, info.x, info.y, info.z,0)
		}
	}
}

func dbWorker(db *sql.DB)error{
	fmt.Println("Starting dbWorker...")
	for {
		if (!globalConfig.workerIsEnabled){
			time.Sleep(time.Minute)
			continue
		}
		rows, err := db.Query("SELECT a,b,c FROM table WHERE param=?",value)
		if err != nil {
			fmt.Println("Error 1 in dbWorker", err)
			return err
		}
		f(rows, db)
		rows.Close()
		rows, err = db.Query("SELECT a,b,c,d FROM table WHERE param!=?",value)
		if err != nil {
			fmt.Println("Error 2 in dbWorker", err)
			return err
		}
		g(rows, db)
		rows.Close()
		time.Sleep(5*time.Minute)
	}
}
  • Вопрос задан
  • 194 просмотра
Пригласить эксперта
Ответы на вопрос 3
В го автоматическое управление памятью, поэтому утечек памяти в классическом смысле быть не может (если не использовать пакет unsafe). Но могут быть утечки горутин (когда вы запускаете горутины, но они не завершаются, а блокируются на каком-то io или мьютексе/канале/...)

Такое легко ищется с помощью pprof. Добавьте в свою программу веб-интерфейс pprof-а

Для этого запустите любой http-сервер из стандартной библиотеки и добавьте импорт import _ "net/http/pprof"

Например так:
import (
  "net/http"
  _ "net/http/pprof"
)

...

func main() {
  ...
  http.ListenAndServe("localhost:8080", nil)
}


После этого при запуске программы у вас должен открываться веб-интерфейс pprof-а по адресу 127.0.0.1:8080/debug/pprof

Дождитесь, когда накопятся утечки и откройте страницу 127.0.0.1:8080/debug/pprof/goroutine?debug=1

На ней будет список всех работающих горутин и их количество. Найдите группу с самым большим количеством, она и утекает. По стеку посмотрите, где горутина блокируется, тогда поймете, почему они накапливаются.
Ответ написан
jcmvbkbc
@jcmvbkbc
"I'm here to consult you" © Dogbert
Как найти утечку памяти?

Стандартный совет: попробовать собирать с опцией -fsanitize=address либо запускать под valgrind.
Ответ написан
Комментировать
@falconandy
Про pprof еще почитайте что-то типа https://www.nylas.com/blog/finding-memory-leak-in-..., https://www.freecodecamp.org/news/how-i-investigat...

1. Потребление памяти постоянно растет или постепенно стабилизируется? Для тестирования попробуйте кардинально уменьшить sleep-ы до секунд вместо минут, чтобы было проще/быстрее исследовать.
2. Попробуйте заменять какую-то логику на заглушки (например, тело функции device.getStats(), работу с БД) и проверять, как изменяется потребление памяти.
3. db.Prepare в цикле выглядят бесполезными.
4. Возможно стоит использовать переиспользуемый буфер (пул буферов) в функции device.getStats(), чтобы постоянно не создавать новые при вызове ioutil.ReadAll().
5. return []byte{}, err не является идиоматичным - используйте return nil, err
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы