我观察到eBPF LRU散列图(BPF_MAP_TYPE_LRU_HASH)中的元素被错误地逐出.在下面的代码中,我插入到大小为8的LRU散列映射中,并每秒打印其内容:

package main

import (
    "fmt"
    "github.com/cilium/ebpf"
    "log"
    "time"
)

func main() {
    spec := ebpf.MapSpec{
        Name:       "test_map",
        Type:       ebpf.LRUHash,
        KeySize:    4,
        ValueSize:  8,
        MaxEntries: 8,
    }

    hashMap, err := ebpf.NewMap(&spec)
    if err != nil {
        log.Fatalln("Could not create map:", err)
    }

    var insertKey uint32

    for range time.Tick(time.Second) {
        err = hashMap.Update(insertKey, uint64(insertKey), ebpf.UpdateAny)
        if err != nil {
            log.Printf("Update failed. insertKey=%d|value=%d|err=%s", insertKey, insertKey, err)
        }

        var key uint32
        var value uint64
        count := 0
        elementsStr := ""

        iter := hashMap.Iterate()

        for iter.Next(&key, &value) {
            elementsStr += fmt.Sprintf("(%d, %d) ", key, value)
            count++
        }

        log.Printf("Total elements: %d, elements: %s", count, elementsStr)

        insertKey++
    }
}

当我运行上面的程序时,我看到以下内容:

2023/03/29 17:32:29 Total elements: 1, elements: (0, 0) 
2023/03/29 17:32:30 Total elements: 2, elements: (1, 1) (0, 0) 
2023/03/29 17:32:31 Total elements: 3, elements: (1, 1) (0, 0) (2, 2) 
2023/03/29 17:32:32 Total elements: 3, elements: (3, 3) (0, 0) (2, 2) 
...

由于映射有八个条目,我预计第四行将显示四个值,但它只显示三个值,因为条目(1, 1)已被逐出.

如果我将max_entries更改为1024,我注意到这个问题是在插入第200个元素之后发生的,但有时它会在插入第200个元素之后发生.这是不一致的.

这个问题不仅限于从用户空间创建/插入 map ,因为我在创建 map 并插入 map 的XDP程序中发现了这个问题;上面的问题重现了我在实际程序中观察到的问题.在我的实际程序中,也有1024个条目,我注意到在插入16元素后发生了这个问题.

我在运行Linux内核5.16.7的生产服务器上对此进行了测试.

我在Linux VM上进行了测试,并将内核升级到了6.2.8,我观察到驱逐策略有所不同.例如,当max_entries是8时,我观察到:

2023/03/29 20:38:02 Total elements: 1, elements: (0, 0)
2023/03/29 20:38:03 Total elements: 2, elements: (0, 0) (1, 1)
2023/03/29 20:38:04 Total elements: 3, elements: (0, 0) (2, 2) (1, 1)
2023/03/29 20:38:05 Total elements: 4, elements: (0, 0) (2, 2) (1, 1) (3, 3)
2023/03/29 20:38:06 Total elements: 5, elements: (4, 4) (0, 0) (2, 2) (1, 1) (3, 3)
2023/03/29 20:38:07 Total elements: 6, elements: (4, 4) (0, 0) (2, 2) (1, 1) (5, 5) (3, 3)
2023/03/29 20:38:08 Total elements: 7, elements: (4, 4) (0, 0) (2, 2) (1, 1) (6, 6) (5, 5) (3, 3)
2023/03/29 20:38:09 Total elements: 8, elements: (7, 7) (4, 4) (0, 0) (2, 2) (1, 1) (6, 6) (5, 5) (3, 3)
2023/03/29 20:38:10 Total elements: 1, elements: (8, 8)
...

max_entries是1024时,我注意到在添加1025个元素后,总共有897个元素.我无法在我们的生产服务器上使用内核6.2.8进行测试.

推荐答案

LRU散列图并不保证有exactly个最大项数,其实现显然是为了在远远超过8个项的情况下提供良好的性能.快速浏览一下代码,我看到的是:

  1. LRU分为两个部分,一个是"活动列表",另一个是"非活动列表",其中一个任务根据元素最近是否被访问而定期将元素从一个移动到另一个.它不是真正的LRU(项目不会在每次访问时都被移到头部).

  2. 当映射已满,并且需要逐出某些项以插入新项时,代码将一次从非活动列表中逐出up to 128个项;只有当非活动列表为空时,它才会从活动列表中逐出单个项.

  3. 还有一个每个CPU的"本地空闲列表",其中有一个等待填充数据的已分配项;当该列表为空时,它会try 从全局空闲列表中拉出,如果该列表为空,则转到驱逐路径.本地免费列表的目标大小为4项.

因此,6.2.8中的行为看起来简单而一致:假设您的所有密钥都在"非活动列表"上(对于扫描类型的访问模式来说并不太奇怪,或者可能只是因为它们都还没有机会得到提升),然后它们都被丢弃了.我不太清楚5.16,但它可能与本地空闲列表和从同一个CPU运行的所有更新有关.

基本上,我认为数据类型并不是以您使用它的方式来使用的,错误存在于您的预期中.如果您不同意,我认为您必须向内核开发人员提出.

Go相关问答推荐

语法-for循环中的initit陈述是否允许分配?

Zitadel示例Go Webapp加密密钥

[0]Func()as";请勿比较哨兵类型

Golang测试容器,无法使网络正常工作

如何描述OpenAPI规范中围棋的数据类型.JSON?

如何在S汇编器中更高效地将全局数据加载到霓虹灯寄存器?

JWT 如何解析声明有效性和错误?

go aws-lambda 与 terraform 中的 exec 格式错误

Golang 网络应用程序安全性:您是否应该判断输入是否为有效的 utf-8?

使用 Grafana alert 在几分钟内重复alert

确保 Go 1.20 编译时的严格可比性?

从动态输入中提取字符串,其中部分字符串可能不存在

在 Gorm 的 AfterFind() 钩子中获取智能 Select struct 的值

此代码如何生成内存对齐切片?

Go 中 SDL Surface 的 OpenGL 纹理

函数调用中的类型参数panic

如何在 Windows 上使用 cgo 为 386 arch 构建 lib?

如何优雅地映射到 Go 中返回可变长度数组的方法?

如何使用golang操作很长的字符串以避免内存不足

如何从字符串中删除多个换行符`\n`但只保留一个?