了解使用大量字符串的 Golang 内存管理



我正在为用 Go 编写的网站 Twitch.tv 开发聊天机器人。

该机器人的功能之一是积分系统,可以奖励观看特定流的用户。此数据存储在 SQLite3 数据库中。

为了获取观看者,机器人会进行 API 调用来抽搐并收集流的所有当前观看者。然后将这些查看器放入一段字符串中。

观众总数可以从一对夫妇到 20,000 或更多不等。

机器人的作用

  • 进行 API 调用
  • 将所有查看器存储在字符串切片中
  • 对于每个查看器,机器人会相应地迭代并添加点。
  • 机器人在下次迭代之前清除此切片

法典

type Viewers struct {
Chatters struct {
CurrentModerators []string `json:"moderators"`
CurrentViewers    []string `json:"viewers"`
} `json:"chatters"`
}    
func RunPoints(timer time.Duration, modifier int, conn net.Conn, channel string) {
database := InitializeDB() // Loads database through SQLite3 driver
var Points int
var allUsers []string
for range time.NewTicker(timer * time.Second).C {
currentUsers := GetViewers(conn, channel)
tx, err := database.Begin()
if err != nil {
fmt.Println("Error starting points transaction: ", err)
}
allUsers = append(allUsers, currentUsers.Chatters.CurrentViewers...)
allUsers = append(allUsers, currentUsers.Chatters.CurrentModerators...)
for _, v := range allUsers {
userCheck := UserInDB(database, v)
if userCheck == false {
statement, _ := tx.Prepare("INSERT INTO points (Username, Points) VALUES (?, ?)")
statement.Exec(v, 1)
} else {
err = tx.QueryRow("Select Points FROM points WHERE Username = ?", v).Scan(&Points)
if err != nil {
} else {
Points = Points + modifier
statement, _ := tx.Prepare("UPDATE points SET Points = ? WHERE username = ?")
statement.Exec(Points, v)
}
}
}
tx.Commit()
allUsers = allUsers[:0]
currentUsers = Viewers{} // Clear Viewer object struct
}

预期行为

当吸引成千上万的观众时,自然而然地,我希望系统资源会变得相当高。这可以将使用 3.0 MB RAM 的机器人转换为高达 20 MB+。当然,数以千计的元素会占用大量空间!

但是,发生了其他事情。

实际行为

每次调用 API 时,RAM 都会按预期增加。但是因为我清除了切片,我希望它会回落到"正常"的 3.0 MB 使用量。

但是,每个 API 调用的 RAM 使用量会增加,即使流的查看者总数减少,也不会下降。

因此,给定几个小时,机器人将轻松消耗 100 + MB 的内存,这对我来说似乎不对。


我在这里错过了什么?总的来说,我对编程和CS相当陌生,所以也许我正在尝试解决一些不是问题的东西。但这对我来说几乎听起来像是记忆泄漏。

我尝试通过 Golang 的运行时库强制垃圾回收并释放内存,但这并不能解决它。

要了解这里发生了什么,您需要了解切片的内部结构以及它发生了什么。您可能应该从 https://blog.golang.org/go-slices-usage-and-internals 开始

给出一个简短的答案:切片提供了底层数组一部分的视图,当您尝试截断切片时,您所做的只是减少对数组的视图,但底层数组不受影响,仍然占用同样多的内存。事实上,通过继续使用相同的阵列,您永远不会减少正在使用的内存量。

我鼓励您阅读其工作原理,但作为为什么没有释放实际内存的示例,请查看这个简单程序的输出,该程序演示了对切片的更改如何不会截断在引擎盖下分配的内存:https://play.golang.org/p/PLEZba8uD-L

重新切片切片时:

allUsers = allUsers[:0]

所有元素仍在后备数组中,无法收集。内存仍然被分配,这将在下一次运行中节省一些时间(它不必调整数组的大小,从而节省缓慢的分配(,这就是将其切为零长度而不仅仅是转储它的意义所在。

如果要将内存释放到 GC,则需要将其完全转储并每次创建一个新切片。这会更慢,但运行之间使用的内存更少。但是,这并不一定意味着您会看到进程使用的内存更少。GC 收集未使用的堆对象,然后最终可能会将该内存释放到操作系统,如果其他进程正在施加内存压力,操作系统最终可能会回收它。

最新更新