Как мне настроить освобождаемый кеш в Golang (т. е. я могу освободить память при обнаружении нехватки памяти)?

Я пишу приложение на голанге. Он генерирует структуру данных в памяти для записи базы данных по запросу. Часто, когда запись запрашивается, она запрашивается несколько раз, поэтому я хочу кэшировать запись в памяти, чтобы избежать нескольких вызовов к базе данных (в основном из-за задержки).

Возможно ли, чтобы этот кеш в памяти динамически расширялся в памяти до тех пор, пока мы не достигнем нехватки памяти (т. е. сбоя malloc), а затем освободим часть кеша?

Кэширование в Redis или аналогичном устройстве усложнит развертывание. Если это единственный вариант, я бы предпочел просто указать статический размер кэша во время выполнения.

Полагаю, я не против использования C.malloc, но я не знаю, как это взаимодействует с управлением памятью Go (если я выделяю кусок памяти, а затем среда выполнения go выделяет этот кусок для стека горутины или чего-то еще сверху). кучи, то я не смогу освободить свою память для ОС, пока не будет освобождено все, что находится сверху). Кроме того, я пока компилирую без cgo, и было бы неплохо продолжать это делать.

Я надеюсь, что в пакете debug или runtime есть что-то, что может намекать на то, что система испытывает нехватку памяти, поэтому я могу динамически изменять размер своего кэша и хранить программу на чистом Go.

Любая помощь или понимание очень ценятся.

Вы рассматривали или даже пробовали sync.Pool? Если нет: почему бы и нет? Если да: что мешает использовать sync.Pool?

Volker 20.08.2024 07:03

@Volker Как бы вы использовали sync.Pool в качестве кеша в памяти? Насколько я понимаю, sync.Pool можно использовать для хранения списка выделенных структур нужного типа, поэтому вам не придется постоянно выделять память. Но у него (похоже) нет механизма «Получить конкретный экземпляр структуры, содержащей нужные мне данные».

Jory Geerts 20.08.2024 10:50

Ой, извини, ты прав, конечно.

Volker 20.08.2024 11:24

вы можете попробовать такой кольцевой буфер github.com/smallnest/ringbuffer

code_monk 20.08.2024 17:58

@Volker Итак, я не слышал о sync.Pool, спасибо, что рассказали мне об этом. Это было бы странно, но возможно. Если бы я выделил несколько блоков памяти и добавил их указатели в пул. Поскольку я каждый раз получал случайный указатель, я получал много промахов в кеше, но приятно, что сборщик мусора освобождает их по мере необходимости. Вероятно, я бы не стал обращаться к этой проблеме. Но я рад, что вы сообщили мне об этом, чтобы я мог использовать его для рабочих потоков или подключений к БД, что было бы большим подспорьем.

TopherIsSwell 20.08.2024 18:16

@code_monk Мне нравится хороший кольцевой буфер. Однако не похоже, что он может динамически расширяться и сжиматься в ответ на нехватку памяти.

TopherIsSwell 20.08.2024 18:33
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
0
6
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В этом ответе есть решение для выделения памяти во время выполнения.

Вот отправная точка для параллельного безопасного кеша с использованием этого кода:

package main

import (
    "bufio"
    "os"
    "strconv"
    "strings"
    "sync"
)

type Memory struct {
    MemTotal     int
    MemFree      int
    MemAvailable int
}

type Cache[T any] struct {
    MinMemFree int // Min number of free bytes
    chunkSize  int // Number of key/value pairs removed from data when MinMemFree is reached
    mu         sync.Mutex
    data       map[string]T
    order      []string // Keeps track of the order of keys added to data
}

func NewCache[T any](minMemFree int, chunkSize int) *Cache[T] {
    return &Cache[T]{
        MinMemFree: minMemFree,
        chunkSize:  chunkSize,
        data:       make(map[string]T),
        order:      []string{},
    }
}

func (c *Cache[T]) Get(key string) T {
    c.mu.Lock()
    defer c.mu.Unlock()

    return c.data[key]
}

func (c *Cache[T]) Set(key string, value T) int {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.data[key] = value
    c.order = append(c.order, key)

    if c.minSizeReached() {
        return c.freeOldestChunk()
    }
    return 0
}

// Free oldest items in the cache, and return the number removed
func (c *Cache[T]) freeOldestChunk() int {
    count := 0
    for i := 1; i <= c.chunkSize; i++ {
        key := c.shiftOrder()
        if key == "" {
            break
        }
        delete(c.data, key)
        count++
    }
    return count
}

func (c *Cache[T]) shiftOrder() string {
    if len(c.order) == 0 {
        return ""
    }
    key := c.order[0]
    c.order = c.order[1:]
    return key
}

func (c *Cache[T]) minSizeReached() bool {
    return ReadMemoryStats().MemFree <= c.MinMemFree
}

func ReadMemoryStats() Memory {
    file, err := os.Open("/proc/meminfo")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    bufio.NewScanner(file)
    scanner := bufio.NewScanner(file)
    res := Memory{}
    for scanner.Scan() {
        key, value := parseLine(scanner.Text())
        switch key {
        case "MemTotal":
            res.MemTotal = value
        case "MemFree":
            res.MemFree = value
        case "MemAvailable":
            res.MemAvailable = value
        }
    }
    return res
}

func parseLine(raw string) (key string, value int) {
    text := strings.ReplaceAll(raw[:len(raw)-2], " ", "")
    keyValue := strings.Split(text, ":")
    return keyValue[0], toInt(keyValue[1])
}

func toInt(raw string) int {
    if raw == "" {
        return 0
    }
    res, err := strconv.Atoi(raw)
    if err != nil {
        panic(err)
    }
    return res
}

Потрясающий. Я думаю, что это работоспособное решение. Спасибо.

TopherIsSwell 20.08.2024 19:27

Одним из простых вариантов является использование группового кэша. Хотя его предполагаемая цель — предоставить кэш в памяти, который используется совместно кластером приложений, он может прекрасно работать и автономно.

Следующий код настраивает кэш группового кэша без каких-либо узлов (т.е. только ваш собственный экземпляр вашего приложения) с одной «группой» элементов, ограниченной 64 МБ памяти. Как только этот предел будет достигнут, внутренний кэш LRU удалит элемент.

func main() {
    maxCacheSize := int64(64 << 20)
    items := groupcache.NewGroup("your-items", maxCacheSize, groupcache.GetterFunc(func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
        // Do something expensive
        result, err := doExpensiveThing(key)
        if err != nil {
            return err
        }
        return dest.SetBytes(result)
    }))

    ctx := context.Background()
    var data string
    err := items.Get(ctx, "your-item-key", groupcache.StringSink(&data))
    if err != nil {
        log.Fatal(err)
    }
}

Поскольку групповой кэш предназначен для кэширования «блобов» и распределения их по кластеру, его «приемники» (структура, в которую вы помещаете свои данные) могут работать только со строками, фрагментами байтов и сообщениями protobuf. Поэтому, если вы хотите кэшировать некоторые результаты базы данных, вам необходимо сериализовать их во что-то вроде JSON.

Если вы хотите кэшировать разные «типы вещей», каждый со своим объемом максимальной памяти, вы можете создать несколько групп.

Еще один компромисс заключается в том, что невозможно удалить элементы и установить срок их действия. Если вам требуется что-то из этого, groupcache, вероятно, не для вас.

В этой части есть загвоздка: maxCacheSize := int64(64 << 20) Вероятно, это лучший кэш LRU, чем все, что я мог бы создать, но я надеюсь, что у меня будет такой, который сможет динамически расширяться для заполнения доступной памяти и сжиматься в ответ на нехватку памяти, чтобы прекрасно сосуществовать с другими процессами.

TopherIsSwell 20.08.2024 19:08

Другие вопросы по теме