Я пишу приложение на голанге. Он генерирует структуру данных в памяти для записи базы данных по запросу. Часто, когда запись запрашивается, она запрашивается несколько раз, поэтому я хочу кэшировать запись в памяти, чтобы избежать нескольких вызовов к базе данных (в основном из-за задержки).
Возможно ли, чтобы этот кеш в памяти динамически расширялся в памяти до тех пор, пока мы не достигнем нехватки памяти (т. е. сбоя malloc), а затем освободим часть кеша?
Кэширование в Redis или аналогичном устройстве усложнит развертывание. Если это единственный вариант, я бы предпочел просто указать статический размер кэша во время выполнения.
Полагаю, я не против использования C.malloc
, но я не знаю, как это взаимодействует с управлением памятью Go (если я выделяю кусок памяти, а затем среда выполнения go выделяет этот кусок для стека горутины или чего-то еще сверху). кучи, то я не смогу освободить свою память для ОС, пока не будет освобождено все, что находится сверху). Кроме того, я пока компилирую без cgo, и было бы неплохо продолжать это делать.
Я надеюсь, что в пакете debug
или runtime
есть что-то, что может намекать на то, что система испытывает нехватку памяти, поэтому я могу динамически изменять размер своего кэша и хранить программу на чистом Go.
Любая помощь или понимание очень ценятся.
@Volker Как бы вы использовали sync.Pool в качестве кеша в памяти? Насколько я понимаю, sync.Pool можно использовать для хранения списка выделенных структур нужного типа, поэтому вам не придется постоянно выделять память. Но у него (похоже) нет механизма «Получить конкретный экземпляр структуры, содержащей нужные мне данные».
Ой, извини, ты прав, конечно.
вы можете попробовать такой кольцевой буфер github.com/smallnest/ringbuffer
@Volker Итак, я не слышал о sync.Pool, спасибо, что рассказали мне об этом. Это было бы странно, но возможно. Если бы я выделил несколько блоков памяти и добавил их указатели в пул. Поскольку я каждый раз получал случайный указатель, я получал много промахов в кеше, но приятно, что сборщик мусора освобождает их по мере необходимости. Вероятно, я бы не стал обращаться к этой проблеме. Но я рад, что вы сообщили мне об этом, чтобы я мог использовать его для рабочих потоков или подключений к БД, что было бы большим подспорьем.
@code_monk Мне нравится хороший кольцевой буфер. Однако не похоже, что он может динамически расширяться и сжиматься в ответ на нехватку памяти.
В этом ответе есть решение для выделения памяти во время выполнения.
Вот отправная точка для параллельного безопасного кеша с использованием этого кода:
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
}
Потрясающий. Я думаю, что это работоспособное решение. Спасибо.
Одним из простых вариантов является использование группового кэша. Хотя его предполагаемая цель — предоставить кэш в памяти, который используется совместно кластером приложений, он может прекрасно работать и автономно.
Следующий код настраивает кэш группового кэша без каких-либо узлов (т.е. только ваш собственный экземпляр вашего приложения) с одной «группой» элементов, ограниченной 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, чем все, что я мог бы создать, но я надеюсь, что у меня будет такой, который сможет динамически расширяться для заполнения доступной памяти и сжиматься в ответ на нехватку памяти, чтобы прекрасно сосуществовать с другими процессами.
Вы рассматривали или даже пробовали sync.Pool? Если нет: почему бы и нет? Если да: что мешает использовать sync.Pool?