Coder Social home page Coder Social logo

freecache's Introduction

FreeCache - A cache library for Go with zero GC overhead and high concurrent performance.

Long lived objects in memory introduce expensive GC overhead, With FreeCache, you can cache unlimited number of objects in memory without increased latency and degraded throughput.

Build Status GoCover GoDoc

Features

  • Store hundreds of millions of entries
  • Zero GC overhead
  • High concurrent thread-safe access
  • Pure Go implementation
  • Expiration support
  • Nearly LRU algorithm
  • Strictly limited memory usage
  • Come with a toy server that supports a few basic Redis commands with pipeline
  • Iterator support

Performance

Here is the benchmark result compares to built-in map, Set performance is about 2x faster than built-in map, Get performance is about 1/2x slower than built-in map. Since it is single threaded benchmark, in multi-threaded environment, FreeCache should be many times faster than single lock protected built-in map.

BenchmarkCacheSet        3000000               446 ns/op
BenchmarkMapSet          2000000               861 ns/op
BenchmarkCacheGet        3000000               517 ns/op
BenchmarkMapGet         10000000               212 ns/op

Example Usage

// In bytes, where 1024 * 1024 represents a single Megabyte, and 100 * 1024*1024 represents 100 Megabytes.
cacheSize := 100 * 1024 * 1024
cache := freecache.NewCache(cacheSize)
debug.SetGCPercent(20)
key := []byte("abc")
val := []byte("def")
expire := 60 // expire in 60 seconds
cache.Set(key, val, expire)
got, err := cache.Get(key)
if err != nil {
    fmt.Println(err)
} else {
    fmt.Printf("%s\n", got)
}
affected := cache.Del(key)
fmt.Println("deleted key ", affected)
fmt.Println("entry count ", cache.EntryCount())

Notice

  • Memory is preallocated.
  • If you allocate large amount of memory, you may need to set debug.SetGCPercent() to a much lower percentage to get a normal GC frequency.
  • If you set a key to be expired in X seconds, e.g. using cache.Set(key, val, X), the effective cache duration will be within this range: (X-1, X] seconds. This is because that sub-second time at the moment will be ignored when calculating the the expiration: for example, if the current time is 8:15::01.800 (800 milliseconds passed since 8:15::01), the actual duration will be X-800ms.

How it is done

FreeCache avoids GC overhead by reducing the number of pointers. No matter how many entries stored in it, there are only 512 pointers. The data set is sharded into 256 segments by the hash value of the key. Each segment has only two pointers, one is the ring buffer that stores keys and values, the other one is the index slice which used to lookup for an entry. Each segment has its own lock, so it supports high concurrent access.

TODO

  • Support dump to file and load from file.
  • Support resize cache size at runtime.

License

The MIT License

freecache's People

Contributors

billyevans avatar bogcon avatar caldempsey avatar codergma avatar coocood avatar cooloppo avatar ct16k avatar debspencer avatar deckarep avatar docmerlin avatar extemporalgenome avatar gitsrc avatar hawkingrei avatar ichizok avatar jb0n avatar kmiku7 avatar laura-zelenku avatar negbie avatar oif avatar oldmantaiter avatar padjoo avatar pavel-main avatar pflanagan-cs avatar pheepi avatar qianbin avatar raulk avatar staon avatar stumble avatar testwill avatar xiehui3651 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

freecache's Issues

[Questions & Feedback] Update value without changin expiration

Hi!

Cool project, very fast.
I choose this instead of bigcache for one reason: bigcache hasn't expire for cache entry, only for entire cache.

Cons: not enough examples how to work with cache

Questions:

  1. How to update entry in cache and do not update expire time?
  2. How to see current expire time for entry?

Crash under stress test

Hi there,
I tried to use it with fasthttp, but it seems it crashes immediately after I use multiple clients, the code involved are

    

    cachedContent, _ = cache.Get(req)
    if len(cachedContent) != 0{
        ctx.Response.Header.Set("CacheHit", "frontendRAM")
        ctx.SetBody(cachedContent)
    }else{

        cachedContent = []byte("objContents")

        // save to memory
        _ = cache.Set(req, cachedContent, 0)
        ctx.Response.Header.Set("CacheHit", "chunkServerDecode")
    }

Hers is the stack trace

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x458ee6]

goroutine 40 [running]:
github.com/coocood/freecache.(*RingBuf).Write(0xc0bed84240, 0x0, 0xb, 0x0, 0xc, 0x0, 0x0)
    /home/ubuntu/src/github.com/coocood/freecache/ringbuf.go:89 +0x101
github.com/coocood/freecache.(*segment).set(0xc0bed84240, 0xc10bd3cdc8, 0xc, 0x18, 0x0, 0xb, 0x0, 0xe8a4600e8662d28, 0x0, 0xc10e2d5160, ...)
    /home/ubuntu/src/github.com/coocood/freecache/segment.go:144 +0x56b
github.com/coocood/freecache.(*Cache).Set(0xc0bed78000, 0xc10bd3cdc8, 0xc, 0x18, 0x0, 0xb, 0x0, 0x0, 0xc000010260, 0x0)
    /home/ubuntu/src/github.com/coocood/freecache/cache.go:53 +0x112
main.akamaiHandler(0xc10bd1f400)
    /home/ubuntu/src/frontend/main.go:189 +0x595
main.requestHandler(0xc10bd1f400)
    /home/ubuntu/src/frontend/main.go:87 +0x124
github.com/valyala/fasthttp.(*Server).serveConn(0xc10bcb6000, 0x725bc0, 0xc10bd0c020, 0x0, 0x0)
    /home/ubuntu/src/github.com/valyala/fasthttp/server.go:1932 +0x6f7
github.com/valyala/fasthttp.(*Server).serveConn-fm(0x725bc0, 0xc10bd0c020, 0x1, 0x0)
    /home/ubuntu/src/github.com/valyala/fasthttp/server.go:1538 +0x3e
github.com/valyala/fasthttp.(*workerPool).workerFunc(0xc10bcc2000, 0xc10bce0220)
    /home/ubuntu/src/github.com/valyala/fasthttp/workerpool.go:212 +0xc3
github.com/valyala/fasthttp.(*workerPool).getCh.func1(0xc10bcc2000, 0xc10bce0220, 0x665a00, 0xc10bce0220)
    /home/ubuntu/src/github.com/valyala/fasthttp/workerpool.go:184 +0x35
created by github.com/valyala/fasthttp.(*workerPool).getCh
    /home/ubuntu/src/github.com/valyala/fasthttp/workerpool.go:183 +0x119
exit status 2

Is the cache.TTL() method threadsfe?

A colleague of mine pointed out that the cache.TTL method doesn’t take a a lock when getting the TTL for a given key.

We haven’t yet done extended analysis but at first glance this doesn’t seem threadsafe and can lead to a dirty read if another goroutine mutates internal state concurrently.

Question about lookupByOff in delEntryPtr

Thanks for your repo.

I feel confused when I read the del func of segment. Why there is another lookupByOff in delEntryPtr while you have been got the idx by lookup. I have checked the codes carefully, but I still can't find out they can return the different values.

I want to know If there is other consideration for checking it again to assure safty.

Any reply would be appreciated.

Ringbuffer readAt get wrong result after resize

test ringbuffer code, looks like may get wrong value after resizing. and I'm not sure why ringbuffer need resize. if it's not a bug, would you mind explain the design of ringbuffer implementation in freecache?

code snippet below

    var d [6]byte                                                                                                                                                                                                                                                                                                                                                                                              
    rb := NewRingBuf(8, 0)                                                                                                                                                                              
    fmt.Println(string(rb.Dump()))                                                                                                                                                                      
                                                                                                                                                                                                        
    fmt.Println("write 123456  write-offset:", rb.End())                                                                                                                                                
    rb.Write([]byte("123456"))                                                                                                                                                                          
    fmt.Println("buffer: ",string(rb.Dump()))                                                                                                                                                           
    fmt.Println(rb.String())                                                                                                                                                                            
    fmt.Println("=========================================")                                                                                                                                            
                                                                                                                                                                                                        
    index := rb.End()                                                                                                                                                                                   
    fmt.Println("write abcdef  write-offset:", rb.End())                                                                                                                                                
    rb.Write([]byte("abcdef"))                                                                                                                                                                          
    fmt.Println("buffer: ", string(rb.Dump()))                                                                                                                                                          
    fmt.Println(rb.String())                                                                                                                                                                            
                                                                                                                                                                                                        
    fmt.Println("=========================================")                                                                                                                                            
                                                                                                                                                                                                        
    fmt.Println("after resize to 10")                                                                                                                                                                   
    rb.Resize(10)                                                                                                                                                                                       
    fmt.Println("buffer: ", string(rb.Dump()))                                                                                                                                                          
    fmt.Println(rb.String())                                                                                                                                                                            
    fmt.Println("=========================================")                                                                                                                                            
                                                                                                                                                                                                        
    fmt.Println("write AB")                                                                                                                                                                             
    rb.Write([]byte("AB"))                                                                                                                                                                              
    fmt.Println("buffer: ", string(rb.Dump()))                                                                                                                                                          
    fmt.Println(rb.String())                                                                                                                                                                            
    fmt.Println("=========================================")                                                                                                                                            
                                                                                                                                                                                                        
    fmt.Println("read abcdef by offset: ", index)                                                                                                                                                       
    rb.ReadAt(d[:], index)                                                                                                                                                                              
    fmt.Println(string(d[:])) 

result below

write 123456  write-offset: 0
buffer:  123456
[size:8, start:0, end:6, index:6]
=========================================
write abcdef  write-offset: 6
buffer:  cdef56ab
[size:8, start:4, end:12, index:4]
=========================================
after resize to 10
buffer:  56abcdef
[size:10, start:4, end:12, index:0]
=========================================
write AB
buffer:  ABabcdef
[size:10, start:4, end:14, index:2]
=========================================
read abcdef by offset:  6
cdef

Replace mutex into Rwmutex ?

The Freecache Get method implementation uses 256 of Mutex lock, can be modified to improve the use of Rwmutex lock? This improves concurrent read performance, and my use environment is that reading data is much larger than writing.

runtime error on i386

Hi,

before the latest commits freecache could be used on i386. That was possible because

atomic.AddInt64
atomic.LoadInt64

was only used to set and get the counters for the stats. So when none of the stats method were used it was able to use it on i386. But since we use atomic.AddInt64 inside the get and set (segment.go) method freecache isn't useable on i386 anymore.

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xb74ec87c]

goroutine 1 [running]:
sync/atomic.AddUint64(0x97636cb4, 0x1, 0x0, 0x1, 0x79eb3042)
/usr/lib/go/src/sync/atomic/asm_386.s:112 +0xc
github.com/coocood/freecache.(*segment).get(0x97636c88, 0x9bfd802a, 0x22e, 0x22e, 0x3042c69e, 0x65250c3f, 0x87234088, 0x0, 0xb7955b80, 0x9bfce000, ...)
/go/src/github.com/coocood/freecache/segment.go:201 +0x44a
github.com/coocood/freecache.(*Cache).Get(0x9760a000, 0x9bfd802a, 0x22e, 0x22e, 0x0, 0x97484188, 0x4, 0x9bfca000, 0x0)
/go/src/github.com/coocood/freecache/cache.go:56 +0xaa

GC settings

Hi. Thanks for the project. I have a question. I don't much understand managing GC. So, if I want to allocate 10 gb of RAM for cache (on a machine with 16 gb RAM), what value of debug.SetGCPercent() would be? And what will happen if I don't set GCPercent at all?

why Cache.locks not choice RWMutex??

	cache.locks[segId].Lock()  // 对于同一个Key读是堵塞的么?
	value, err = cache.segments[segId].get(key, hashVal)
	cache.locks[segId].Unlock()

a small question

I already know the free cache is fast than map.

but freecache store []byte in memory, and always our's data isn't []byte, so we should marshal or convert data to []byte..

so, my question is marshal time worth using freecache?

Use `interface{}` as value can reduce GC and save CPU

Because the value is only support []byte, we should serialize/unserialize our data each set/get operation, this will cost more CPU time. And the serialized/unserialized object will free which cause more GC. Why do we just use interface{} as value?

Cache is not respecting the provided size

I create a new cache with a size of 100MB and set the GC percentage as recommended in the readme to 20%.

cacheSize := 100 * 1024 * 1024 // 100MB
cache := freecache.NewCache(cacheSize)
debug.SetGCPercent(20)

Despite this, the cache grows like a memory leak beyond the 100MB I specified. After a few days of use, it is using ~1.2 GB, as indicated by profiling the heap. The machine has just 3 GB of memory, so this is significant.

      flat  flat%   sum%        cum   cum%
 1278.15MB 98.40% 98.40%  1278.15MB 98.40%  .../vendor/github.com/coocood/freecache.NewRingBuf /go/src/.../vendor/github.com/coocood/freecache/ringbuf.go (inline)
   13.55MB  1.04% 99.44%  1291.70MB 99.44% .../vendor/github.com/coocood/freecache.newSegment /go/src/.../vendor/github.com/coocood/freecache/segment.go (inline)

Tag

I use dep in my project and wanna use this repository but I don't see any version tag to use.
Could you please add a version tag?

AverageAccessTime()

First and foremost, thanks for building this. I'm currently using it for Static, and so far it's been awesome.

I'm a little confused about the AverageAccessTime(). My application is logging some stats about the cache for later analysis, and I'm not sure what the return value of this function represents. My initial guess was that it is the average time it takes to fetch an object from the cache, but the number seems way to high even for nanoseconds. I have tried consulting the code but I wasn't quite able to figure it out.

Sample log from my application:

2015/05/02 17:05:40 EntryCount: 0
2015/05/02 17:05:40 EvacuateCount: 0
2015/05/02 17:05:40 AverageAccessTime: 0
2015/05/02 17:05:40 HitCount: 0
2015/05/02 17:05:40 LookupCount: 0
2015/05/02 17:05:40 HitRate: 0.000000

2015/05/02 17:06:05 EntryCount: 86
2015/05/02 17:06:05 EvacuateCount: 0
2015/05/02 17:06:05 AverageAccessTime: 1430586343
2015/05/02 17:06:05 HitCount: 0
2015/05/02 17:06:05 LookupCount: 86
2015/05/02 17:06:05 HitRate: 0.000000

2015/05/02 17:06:15 EntryCount: 86
2015/05/02 17:06:15 EvacuateCount: 0
2015/05/02 17:06:15 AverageAccessTime: 1430586373
2015/05/02 17:06:15 HitCount: 516
2015/05/02 17:06:15 LookupCount: 602
2015/05/02 17:06:15 HitRate: 0.857143

2015/05/02 17:06:25 EntryCount: 86
2015/05/02 17:06:25 EvacuateCount: 0
2015/05/02 17:06:25 AverageAccessTime: 1430586379
2015/05/02 17:06:25 HitCount: 1032
2015/05/02 17:06:25 LookupCount: 1118
2015/05/02 17:06:25 HitRate: 0.923077

Add custom timer instead of time.Now() cloak time

I have my app using the cache library and several tests that check if records do not expire in a given timeout. These tests fail occasionally, because they run on a server sharing CPU with other services. I can never fix them, because the library depends on cloak time from time.Now(). The only ultimate solution here is to mock time. Do you think it would be possible to replace time.Now() with an interface representing current time? It would provide function Now(), but it could be custom and parameterized. Default implementation would still use time.Now().Unix(), but custom timer could be passed as a parameter. I have already written my own change.

By the way, it may also partly resolve issue #51.

Poor cache space utilization with "sequential" keys / multithreaded

inserting 1 million consecutive keys into 1Mb sized cache gives something like this
code (sorry, messy): https://gist.github.com/anton-povarov/90740565548bd5c0517c

entries: 4672, evacuated: 0, overwritten: 0
avg_value_len: 16
bytes per item: 224.43835616438355 - 16 - 16 = 192.43835616438355

so we could only insert 4672 keys into 1megabyte cache, which is ~224 bytes per item, subtracting 16 byte key len, 16 byte value len, we're left wih ~192 bytes of overhead per key (of which 24 are occupied by header).

My suspicion was that this is due to fnva hash failing to give well distributed values for these very regular keys. Which leads to poor segments utilization.

So i've changed the hashing function to md5 for a quick and dirty test, like this

func fnvaHash(data []byte) uint64 {
    sum := md5.Sum(data)
    return binary.LittleEndian.Uint64(sum[:8])

    // var hash uint64 = 14695981039346656037
    // for _, c := range data {
    //  hash ^= uint64(c)
    //  hash *= 1099511628211
    // }
    // return hash
}

which gives the following result

entries: 12409, evacuated: 0, overwritten: 0
avg_value_len: 43.480773
bytes per item: 84.50124909339995 - 43.480773 - 16 = 25.02047609339995

which seems much much better for basically the same code.

What i suggest is to find/implement a better hash function (like murmur3 or something), maybe this one: https://github.com/spaolacci/murmur3

EntryCount() not accurate when value is updated?

It seems EntryCount() doesn't show accurate number when a key's value is updated..?
Just by a brief look at segment.set(), looks like in certain cases, segment.delEntryPtr() doesn't match so segment.entryCount won't get subtracted but then when segment.insertEntryPtr() is called next, it increments segment.entryCount.

A very simple example:

func main() {
  cache := freecache.NewCache(0)
  var err error
  var entries = 50

  // Add entries
  for i := 0; i < entries; i++ {
    if err = cache.Set([]byte("entry_" + strconv.Itoa(i)), []byte("something"), -1); err != nil {
      fmt.Printf("ERROR: %d - %s\n", i, err)
    }
  }
  fmt.Printf("%d entries in freecache\n", cache.EntryCount())

  // Modify entries
  var key, value []byte
  var newvalue = []byte("some new value")
  for i := 0; i < entries; i++ {
    key = []byte("entry_" + strconv.Itoa(i))
    if err = cache.Set(key, newvalue, -1); err != nil {
      fmt.Printf("ERROR: %d - %s\n", i, err)
      continue
    }
    if value, err = cache.Get(key); err != nil {
      fmt.Printf("ERROR: %d - %s\n", i, err)
      continue
    }
    if !bytes.Equal(value, newvalue) {
      fmt.Printf("ERROR: %d %v != %v\n", i, value, newvalue)
    }
  }
  fmt.Printf("%d entries in freecache\n", cache.EntryCount())
}

The first EntryCount() shows 50 (expected) but the 2nd one shows 54.
The new value retrieved from cache.Get() seems accurate so I'm assuming this is just what EntryCount() returns is inaccurate.
If I call cache.Del() before calling cache.Set() in the 2nd loop, the 2nd EntryCount() shows 50.

What about mmap?

Using mmap as a backend for saving data to disk may be a good choice.

panic in processor: runtime error: slice bounds out of range

runtime/debug.Stack(0xcc477985d0, 0xa486a0, 0xe418e0)
/opt/go/src/runtime/debug/stack.go:24 +0xa7
git.apache.org/thrift.git/lib/go/thrift.(*TSimpleServer).processRequests.func1()
/src/git.apache.org/thrift.git/lib/go/thrift/simple_server.go:186 +0x5a
panic(0xa486a0, 0xe418e0)
/opt/go/src/runtime/panic.go:491 +0x283
github.com/coocood/freecache.(*RingBuf).EqualAt(0xc42028c0d8, 0xcc3850dad0, 0x28, 0x20, 0x347fffde, 0x7f47246617c0)
/src/github.com/coocood/freecache/ringbuf.go:147 +0x1de

ringbuf.Evacuate

if rb.index == len(rb.data) {

This line is aways false. Because:

  1. rb.index < readOff => rb.index+length <readEnd
  2. readEnd<=len(rb.data) => rb.index+length <len(rb.data)
  3. n<=length => rb.index+n < llen(rb.data)

Freecache description

We are working on an article about the current state of concurrent cache implementations in Go. We are planning to include freecache with following short description. Please let me know if my understanding is correct, or you would like to modify it.

FreeCache divides the cache into 256 segments. Each segment contains 256 slots
and a ring buffer to store the data. When a new key is added to the cache,
segment id is identified using the lower 8 bit of the hash of the key. Further,
slot is chosen using the LSB 9 to LSB 16 of the hash of the key. Dividing data into
slots helps in reducing the search space while looking for a key in the cache.

The data is then appended into the ring buffer and offset is stored into a sorted array.
If ring buffer doesn't have enough space, eviction is performed in the segment
from the beginning of the ring buffer using a modified LRU policy. An entry is
deleted from the ring buffer if the last access time for the entry is smaller
than the average access time of the segment. To find an entry in a cache on Get,
a binary search is performed in the sorted array in the corresponding slot.

Replace `time.Now` with lower precision time source

time.Now() will sometimes result in a full system call (this is the default on AWS EC2 instances). I think for an LRU you can probably get away with second precision. One option would be to have a background goroutine that called time.Now() every second and cached the result globally.

when to use debug.setGCPercent

I wonder when to use debug.setGCPercent, if my server is 16G, and the program use a little memory, then I want to use freecache to reduce the request for upstream server, if I use 4G
to pre-alloc the memory, should I use debug.setGCPercent to control the gc frequecy?

Clear Cache All Data Method

did not want to redefine *Cache Clear Cache All Data
cache := freecache.NewCache(cacheSize)
cache.Set(key, val, expire)
cache = freecache.NewCache(cacheSize)

Memory continuously consumed by (*segment).expand()?

Hi,

I have a long-running application which uses freecache together with a fairly large number (250,000) of goroutines. The application is running on a 16GB machine, and 8GB of that is given over to freecache caches. I have GOGC set to 50, and it seems (from checking the allocated object sizes using pprof) that garbage collection is working adequately.

The issue I have is that the amount of memory allocated by (*segment).expand() continuously increases over time. When the application has been running only a few minutes, the profile of top consumers as reported by go tool pprof (against an inuse_space heap profile) looks like this:

(pprof) top20
Showing nodes accounting for 8339.11MB, 99.25% of 8402.05MB total
Dropped 46 nodes (cum <= 42.01MB)
      flat  flat%   sum%        cum   cum%
    8192MB 97.50% 97.50%     8192MB 97.50%  github.com/coocood/freecache.NewRingBuf
   94.53MB  1.13% 98.63%    94.53MB  1.13%  runtime.malg
      31MB  0.37% 98.99%    68.76MB  0.82%  my-app/worker.(*Pool).worker
   19.08MB  0.23% 99.22%  8213.07MB 97.75%  my-app/worker.NewPool
    1.99MB 0.024% 99.24%  8193.99MB 97.52%  github.com/coocood/freecache.NewCache
    0.50MB 0.006% 99.25%  8213.57MB 97.76%  main.main
         0     0% 99.25%  8193.99MB 97.52%  my-app/foo.NewFoo
         0     0% 99.25%     8192MB 97.50%  github.com/coocood/freecache.newSegment
         0     0% 99.25%  8213.57MB 97.76%  runtime.main
         0     0% 99.25%    96.82MB  1.15%  runtime.mstart
         0     0% 99.25%    96.82MB  1.15%  runtime.newproc.func1
         0     0% 99.25%    96.82MB  1.15%  runtime.newproc1
         0     0% 99.25%    96.82MB  1.15%  runtime.systemstack

After a day or so of runtime, it looks like this:

(pprof) top20
Showing nodes accounting for 8969.93MB, 99.41% of 9023.42MB total
Dropped 42 nodes (cum <= 45.12MB)
Showing top 20 nodes out of 21
      flat  flat%   sum%        cum   cum%
    8192MB 90.79% 90.79%     8192MB 90.79%  github.com/coocood/freecache.NewRingBuf
  658.83MB  7.30% 98.09%   658.83MB  7.30%  github.com/coocood/freecache.(*segment).expand
   94.53MB  1.05% 99.13%    94.53MB  1.05%  runtime.malg
   19.08MB  0.21% 99.35%  8213.07MB 91.02%  my-app/worker.NewPool
       3MB 0.033% 99.38%   689.63MB  7.64%  my-app/worker.(*Pool).worker
    1.99MB 0.022% 99.40%  8193.99MB 90.81%  github.com/coocood/freecache.NewCache
    0.50MB 0.0055% 99.41%   686.13MB  7.60%  my-app/foo.(*Foo).foo
         0     0% 99.41%   686.63MB  7.61%  my-app/foo.(*Foo).FooBar
         0     0% 99.41%   511.37MB  5.67%  my-app/foo.(*Foo).setBaz
         0     0% 99.41%   137.63MB  1.53%  my-app/foo.(*Foo).setQuux
         0     0% 99.41%  8193.99MB 90.81%  my-app/foo.NewFoo
         0     0% 99.41%   658.83MB  7.30%  github.com/coocood/freecache.(*Cache).Set
         0     0% 99.41%   658.83MB  7.30%  github.com/coocood/freecache.(*segment).insertEntryPtr
         0     0% 99.41%   658.83MB  7.30%  github.com/coocood/freecache.(*segment).set
         0     0% 99.41%     8192MB 90.79%  github.com/coocood/freecache.newSegment
         0     0% 99.41%  8213.07MB 91.02%  main.main
         0     0% 99.41%  8213.07MB 91.02%  runtime.main
         0     0% 99.41%    96.82MB  1.07%  runtime.mstart
         0     0% 99.41%    96.82MB  1.07%  runtime.newproc.func1
         0     0% 99.41%    96.82MB  1.07%  runtime.newproc1

-- so an increase of several hundred MB in github.com/coocood/freecache.(*segment).expand.

After a few days, the app runs out of memory and the OOM killer ends it. I very strongly suspect that this is due to the growth in (*segment).expand, simply because I can't see anything else growing or leaking over time.

My question is: Is this memory growth expected? If so, is there any way to limit or bound it?

strange performance issue

为了准确的描述,抱歉使用中文发issus。

我为自己的代码做一个缓存组件,其中包括了freecache和redis。

在使用freecache进行页面缓存调优秀的时候遇到了很奇怪的性能问题。

在负载较低的情况下,freecache是远远超过redis的性能的。但是在页面较大时(),freecache无法使用所有的cpu,性能甚至不如调用redis接口的驱动。

具体测试如下

  • 测试机 ryzen 1700 32g内存 debian testing
  • 测试工具wrk -c 100 -t 32 -d 30
  • freecache代码版本:发本issus时最新go get的代码

对于大小为 1469811 bytes的页面
freecache的数据为:
Requests/sec: 27248.32
Transfer/sec: 383.06MB
cpu为230%左右,使用各种方法都无法提升

redis的数据为:
Requests/sec: 31695.28
Transfer/sec: 445.58MB
cpu占用一般为1000%左右,此时redis-server的cpu占用到了100%,所以无法更高。

而测试大小为20 bytes的页面(经过缓存的空json数组api返回值),则表现十分正常
freecache的数据为
Requests/sec: 193794.72
Transfer/sec: 51.03MB
cpu为1200%,此时wrk的cpu占用为300%左右,基本是cpu限制了效率。

redis为
Requests/sec: 54551.97
Transfer/sec: 14.46MB
程序cpu占用1000%左右

在我的intel i5笔记本上也一样遇到这个问题,区别是请求上线提升到了34000/s左右……

我试过了换用测试工具(ab,hey,wrk),测试大文件下载,测试静态小文件下载,跳过反序列化代码,调整页面缓存组建等方式,最后发现切换缓存驱动,推断可能是freecache里可能有我没有理解的原因造成了性能瓶颈。

我这边调用freecache的代码为

func (c *Cache) GetBytesValue(key string) ([]byte, error) {
	bytes, err := c.freecache.Get([]byte(key))
	if err == freecache.ErrNotFound {
		err = cache.ErrNotFound
	}
	return bytes, err
}

链接

调用redis的代码为

func (c *Cache) GetBytesValue(key string) ([]byte, error) {
	var bs []byte
	conn := c.Pool.Get()
	defer conn.Close()
	k := c.getKey(key)
	bs, err := redis.Bytes((conn.Do("GET", k)))
	if err == redis.ErrNil {
		return nil, cache.ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	if bs == nil {
		return nil, cache.ErrNotFound
	}
	return bs, err
}

链接

Keys are case insensitive

Why keys are case insensitive?
I'm storing random string as bytes to use as session hash.

Using it as session hash is dangerous

Incorrect ExpiredCount in cache

From

freecache/cache.go

Lines 163 to 164 in c5bd9d4

// ExpiredCount is a metric indicating the number of times an expire occurred.
func (cache *Cache) ExpiredCount() (count int64) {
, ExpiredCount is expected to increase once the expire happens. However, during the test, found the expiredCount doesn't get changed even if the expire happens.

Test code:

func TestLocalCacheExpiry(t *testing.T) {
	localCache := freecache.NewCache(10000)
	cacheKey := "testKey"
	// set cacheKey to expire after 1 second
	localCache.Set([]byte(cacheKey), []byte(""), 1)
	time.Sleep(100 * time.Millisecond)
	ttl, _ := localCache.TTL([]byte(cacheKey))
	assert.NotEqual(t, 0, int(ttl))
	expiredCount := localCache.ExpiredCount()
	assert.Equal(t, 0, int(expiredCount))
	_, err := localCache.Get([]byte(cacheKey))
	assert.Equal(t, nil, err)

	time.Sleep(1000 * time.Millisecond)

	ttl2, _ := localCache.TTL([]byte(cacheKey))
	// Since the time has passed over 1 second, the tt2 is expected to 0.
	assert.Equal(t, 0, int(ttl2))

	_, err2 := localCache.Get([]byte(cacheKey))
	// If not found, the error is expected.
	assert.NotEqual(t, nil, err2)

	expiredCount2 := localCache.ExpiredCount()
	// Since the cachekey become expired, and also it cannot be found in localCache, the expiredCount2 is expected to be 1.
	// However, the expiredCount2 is 0.
	assert.Equal(t,0, int(expiredCount2))
}

Possible performance enhancement

Thanks for building this! So great!

I have a question about the hashFunc and why you chose crypto/md5? I ran the benchmarks with a murmur3 hash and got a substantial improvement in performance:

Current hashFunc

BenchmarkCacheSet   2000000             690 ns/op
BenchmarkMapSet     2000000             828 ns/op
BenchmarkCacheGet   2000000             718 ns/op
BenchmarkMapGet     10000000            189 ns/op
BenchmarkHashFunc   5000000             253 ns/op    <- hashFunc

Murmur3 hashFunc

BenchmarkCacheSet   3000000             480 ns/op
BenchmarkMapSet     2000000             808 ns/op
BenchmarkCacheGet   3000000             494 ns/op
BenchmarkMapGet     10000000            195 ns/op
BenchmarkHashFunc   100000000           11.0 ns/op    <- murmur3

Just looking for an explanation of the existing hashFunc.

Set失败 没有反馈

package main

import (
_ "errors"
"fmt"
"runtime/debug"
_ "strconv"
"strings"

"github.com/coocood/freecache"

)

var kvMap map[string]string

func main() {

debug.SetGCPercent(10)
cache := freecache.NewCache(10240 * 1024)
kvMap = make(map[string]string)
for i := 0; i <= 10000; i++ {
	key := fmt.Sprintf("key%v", i)
	value := strings.Repeat(key, 1000)

	if _, ok := kvMap[key]; ok {
		continue
	}

	kvMap[key] = value

	keyBt := []byte(key)

	valueBt := []byte(value)

	err := cache.Set(keyBt, valueBt, 0)
	if err != nil {
		fmt.Println("set error:", err)
		fmt.Println("key len:", len(key))
		fmt.Println("keyBt len:", len(keyBt))
	}

}

bErr := false
errNum := 0

for k, v := range kvMap {
	keyBt := []byte(k)
	valueBt, err := cache.Get(keyBt)
	if err != nil {
		errNum++
		bErr = true
	} else {
		value := string(valueBt[:])
		if strings.Compare(v, value) != 0 {
			fmt.Println("value compare error")
			bErr = true
		}
	}

}

fmt.Println("bErr:", bErr, errNum)
fmt.Println("value compare end-----------------------------")

fmt.Printf("hit rate is %v, evacuates %v, entries %v, average time %v, expire count %v\n",
	cache.HitRate(), cache.EvacuateCount(), cache.EntryCount(), cache.AverageAccessTime(), cache.ExpiredCount())

cache.ResetStatistics()
fmt.Printf("hit rate is %v, evacuates %v, entries %v, average time %v, expire count %v\n",
	cache.HitRate(), cache.EvacuateCount(), cache.EntryCount(), cache.AverageAccessTime(), cache.ExpiredCount())

}

上面是我的测试代码,set数据到freecache里,但是get的时候就有很多的value是得不到的,应该是set失败了吧,但是没有error回馈啊

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.