如何在Golang中为进程设置内存限制

11
我使用系统调用prlimit来设置进程的资源限制,它可以限制CPU时间,但在测试内存使用时,我遇到了问题。
package sandbox

import (
    "syscall"
    "unsafe"
)

func prLimit(pid int, limit uintptr, rlimit *syscall.Rlimit) error {
    _, _, errno := syscall.RawSyscall6(syscall.SYS_PRLIMIT64, uintptr(pid), limit, uintptr(unsafe.Pointer(rlimit)), 0, 0, 0)
    var err error
    if errno != 0 {
        err = errno
        return err
    } else {
        return nil
    }
}

并且这是我的测试。

func TestMemoryLimit(t *testing.T) {
    proc, err := os.StartProcess("test/memo", []string{"memo"}, &os.ProcAttr{})
    if err != nil {
        panic(err)
    }
    defer proc.Kill()
    var rlimit syscall.Rlimit
    rlimit.Cur = 10
    rlimit.Max = 10 + 1024
    prLimit(proc.Pid, syscall.RLIMIT_DATA, &rlimit)
    status, err := proc.Wait()
    if status.Success() {
        t.Fatal("memory test failed")
    }
}

这是备忘录:

package main

func main() {
    var a [10000][]int
    for i := 0; i < 1000; i++ {
        a[i] = make([]int, 1024)
    }
}

我申请了大量的内存,但只为这块内存设置了10个字节的大小,但无论如何它都不会发出段错误信号。

2个回答

12

RLIMIT_DATA 描述了进程数据段的最大大小。传统上,分配内存的程序通过调用 brk() 来扩大数据段以从操作系统中分配内存。

Go 不使用这种方法。相反,它使用 mmap() 系统调用的变量来请求地址空间中任何位置的内存区域。这比基于 brk() 的方法更加灵活,因为您可以使用 munmap() 释放任意内存区域,而基于 brk() 的方法只能释放数据段末尾的内存。

结果是,RLIMIT_DATA 在控制进程使用的内存量方面无效。请尝试使用 RLIMIT_AS,但请注意,此限制还包括您用于文件映射的地址空间,特别是在共享库的情况下。


如果我想要更多的通用性。那么C和其他编程语言呢? - ggaaooppeenngg
它只是以代码2退出,我如何追踪信号或区分其他退出代码? - ggaaooppeenngg
@ggaaooppeenngg RLIMIT_DATA 机制也适用于 C 程序。我不理解你的第二个问题。 - fuz
我使用ptrace来跟踪进程,在ptrace的循环中,我检查所启动进程的信号。如果我设置了RLIMIT_DATA,并且进程超过了限制,它将会发出段错误信号(SIGSEGV),而如果设置了RLIMIT_AS,则只会强制退出。我阅读了setrlimit的手册,它说它会返回ENOMEM,只有当堆栈超过限制时,它才会发出SIGSEGV信号。我该如何找到关于ENOMEM的定义,似乎我的测试以2或137的代码退出,但我不知道退出代码的含义。 - ggaaooppeenngg
1
@ggaaooppeenngg 显然,如果内存耗尽并且您没有捕获结果的 panic,Go 运行时将以退出状态 2 终止进程。当没有可用内存时,内存分配系统调用产生的错误状态为 ENOMEM(无内存错误)。 - fuz

1
有一个提案软内存限制,可能会在go 1.18之后发布。
这个选项有两种方式:一个是新的runtime/debug函数SetMemoryLimit和一个GOMEMLIMIT环境变量。总的来说,运行时将通过限制堆的大小并更积极地向底层平台返回内存来尝试维护此内存限制。这包括一种机制,以帮助缓解垃圾收集死循环。最后,通过设置GOGC=off,Go运行时将始终将堆增长到完整的内存限制。
这个新选项使应用程序能够更好地控制它们的资源经济。它赋予用户以下权力:
- 更好地利用他们已经拥有的内存, - 自信地减少他们的内存限制,因为Go将尊重它们, - 避免不支持的垃圾收集调优形式。
更新
此功能将在Go 1.19中发布。
运行时现在包括对软内存限制的支持。此内存限制包括Go堆和运行时管理的所有其他内存,但不包括二进制文件本身的映射、在其他语言中管理的内存以及操作系统代表Go程序持有的内存。
此限制可以通过runtime/debug.SetMemoryLimit或等效的GOMEMLIMIT环境变量进行管理。
该限制与runtime/debug.SetGCPercent/GOGC一起使用,并且即使GOGC=off也会被尊重,允许Go程序始终最大程度地利用其内存限制,在某些情况下提高资源效率。
以下是关于每个Go垃圾收集器指南中的内存限制使用建议:
虽然内存限制是一个强大的工具,而且Go运行时采取措施来减轻滥用的最坏行为,但仍然需要谨慎使用。以下是关于内存限制在哪些情况下最有用和适用,以及在哪些情况下可能会造成更多的伤害的建议。
  • 当您完全控制Go程序的执行环境,并且Go程序是访问某个资源集的唯一程序时(即某种内存预留,例如容器内存限制),请利用内存限制。

    一个很好的例子是将Web服务部署到具有固定可用内存量的容器中。

    在这种情况下,一个好的经验法则是留出额外的5-10%的余地,以考虑Go运行时不知道的内存来源。

  • 随时调整内存限制以适应不断变化的情况。

    一个很好的例子是cgo程序,其中C库需要暂时使用大量内存。

  • 如果Go程序可能与其他程序共享其有限内存,并且这些程序通常与Go程序解耦,请勿在内存限制下将GOGC设置为关闭。相反,保持内存限制,因为它可以帮助遏制不良瞬态行为,但对于平均情况,请将GOGC设置为较小的合理值。

    虽然试图为共同租户程序“保留”内存可能很诱人,但除非程序完全同步(例如,Go程序调用某个子进程并在其被调用者执行时阻塞),否则结果将不太可靠,因为两个程序都不可避免地需要更多内存。让Go程序在不需要时使用较少的内存将生成更可靠的结果。此建议也适用于超额承诺情况,在这种情况下,运行在一台机器上的容器的内存限制之和可能超过机器实际可用的物理内存。

  • 当部署到您无法控制的执行环境中时,请勿使用内存限制,特别是当程序的内存使用量与其输入成比例时。

    一个很好的例子是CLI工具或桌面应用程序。 当不清楚它可能会被提供什么样的输入或系统上可能有多少可用内存时,在程序中嵌入内存限制可能会导致令人困惑的崩溃和性能差。此外,高级终端用户可以随时设置内存限制。

  • 当程序已接近其环境的内存限制时,请勿设置内存限制以避免出现内存不足的情况。

    这实际上是将内存不足的风险替换为严重的应用程序减速的风险,即使Go尝试减轻抖动的努力,这通常也不是一个有利的交易。在这种情况下,要么增加环境的内存限制(然后可能设置内存限制),要么降低GOGC(提供比抑制抖动更清晰的权衡)将更有效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接