Go中cipher.AEAD.Seal()加密大文件内存占用翻倍问题咨询
我使用Go语言的ChaCha20-Poly1305实现加密数据,但在加密大文件时,内存占用超出预期。我理解Go的AEAD密码实现需要将全部数据载入内存以生成哈希,但内存占用达到了明文的两倍。
以下是复现问题的代码(实际场景中key和nonce不应为空):
package main import ( "os" "fmt" "runtime" "golang.org/x/crypto/chacha20poly1305" ) func main() { showMemUsage("START") plaintext := make([]byte, 4 * 1024 * 1024 * 1024) // 4 GiB showMemUsage("STAGE 1") key := make([]byte, chacha20poly1305.KeySize) if cipher, err := chacha20poly1305.New(key); err == nil { showMemUsage("STAGE 2") nonce := make([]byte, chacha20poly1305.NonceSize) cipher.Seal(plaintext[:0], nonce, plaintext, nil) } showMemUsage("END") } func showMemUsage(tag string) { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Fprintf(os.Stdout, "[%s] Alloc = %v MiB, TotalAlloc = %v MiB\n", tag, m.Alloc / 1024 / 1024, m.TotalAlloc / 1024 / 1024) }
根据crypto/cipher/gcm.go的源码注释(同样适用于ChaCha20-Poly1305):
// To reuse plaintext's storage for the encrypted output, use plaintext[:0]
// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext.
Seal(dst, nonce, plaintext, additionalData []byte) []byte
我尝试复用内存,但调用Seal()后,加密4GiB数据始终占用8GiB内存,运行结果如下:
[START] Alloc = 0 MiB, TotalAlloc = 0 MiB [STAGE 1] Alloc = 4096 MiB, TotalAlloc = 4096 MiB [STAGE 2] Alloc = 4096 MiB, TotalAlloc = 4096 MiB [END] Alloc = 8192 MiB, TotalAlloc = 8192 MiB
问题根源
问题出在明文slice的容量不足以容纳加密后的数据:
ChaCha20-Poly1305加密后的数据会在明文基础上追加16字节的Poly1305校验标签。你的代码中plaintext是用make([]byte, 4*1024*1024*1024)创建的,这意味着它的长度和容量都是4GiB,没有多余空间存储标签。
当调用Seal(plaintext[:0], nonce, plaintext, nil)时,内部的grow函数会检查目标slice的容量是否足够容纳加密后的数据(明文长度+16字节标签):
len(dst)是0,需要的总长度是4GiB + 16- 而
dst的容量是4GiB,不足以容纳额外的16字节标签 - 因此
grow函数会分配一块新的内存来存储加密后的数据,导致内存占用翻倍(原4GiB明文 + 新分配的4GiB+16字节密文)
解决方案
创建明文slice时,预留出足够容纳Poly1305标签的容量,修改plaintext的创建代码:
plaintext := make([]byte, 4*1024*1024*1024, 4*1024*1024*1024 + chacha20poly1305.TagSize)
这样plaintext[:0]的容量是4GiB + 16,满足加密后数据的存储需求,grow函数会直接在原有内存上扩展slice长度,不会分配新内存。修改后的内存占用会保持在4GiB左右(仅额外占用标签的16字节)。
补充说明
即使你不需要保留明文,也需要注意:如果明文slice的容量刚好等于明文长度,加密时必然会触发新内存分配。只有当目标slice的容量大于等于明文长度+标签长度时,才能实现真正的原地内存复用。
内容的提问来源于stack exchange,提问作者chrixm

