mirror of
https://github.com/minio/minio.git
synced 2025-11-09 21:49:46 -05:00
caching: Optimize memory allocations. (#3405)
This change brings in changes at multiple places - Reuse buffers at almost all locations ranging from rpc, fs, xl, checksum etc. - Change caching behavior to disable itself under low memory conditions i.e < 8GB of RAM. - Only objects cached are of size 1/10th the size of the cache for example if 4GB is the cache size the maximum object size which will be cached is going to be 400MB. This change is an optimization to cache more objects rather than few larger objects. - If object cache is enabled default GC percent has been reduced to 20% in lieu with newly found behavior of GC. If the cache utilization reaches 75% of the maximum value GC percent is reduced to 10% to make GC more aggressive. - Do not use *bytes.Buffer* due to its growth requirements. For every allocation *bytes.Buffer* allocates an additional buffer for its internal purposes. This is undesirable for us, so implemented a new cappedWriter which is capped to a desired size, beyond this all writes rejected. Possible fix for #3403.
This commit is contained in:
50
pkg/objcache/capped-writer.go
Normal file
50
pkg/objcache/capped-writer.go
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package objcache implements in memory caching methods.
|
||||
package objcache
|
||||
|
||||
// Used for adding entry to the object cache.
|
||||
// Implements io.WriteCloser
|
||||
type cappedWriter struct {
|
||||
offset int64
|
||||
cap int64
|
||||
buffer []byte
|
||||
onClose func() error
|
||||
}
|
||||
|
||||
// Write implements a limited writer, returns error.
|
||||
// if the writes go beyond allocated size.
|
||||
func (c *cappedWriter) Write(b []byte) (n int, err error) {
|
||||
if c.offset+int64(len(b)) > c.cap {
|
||||
return 0, ErrExcessData
|
||||
}
|
||||
n = copy(c.buffer[int(c.offset):int(c.offset)+len(b)], b)
|
||||
c.offset = c.offset + int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Reset relinquishes the allocated underlying buffer.
|
||||
func (c *cappedWriter) Reset() {
|
||||
c.buffer = nil
|
||||
}
|
||||
|
||||
// On close, onClose() is called which checks if all object contents
|
||||
// have been written so that it can save the buffer to the cache.
|
||||
func (c cappedWriter) Close() (err error) {
|
||||
return c.onClose()
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -32,6 +33,13 @@ var NoExpiry = time.Duration(0)
|
||||
// DefaultExpiry represents default time duration value when individual entries will be expired.
|
||||
var DefaultExpiry = time.Duration(72 * time.Hour) // 72hrs.
|
||||
|
||||
// DefaultBufferRatio represents default ratio used to calculate the
|
||||
// individual cache entry buffer size.
|
||||
var DefaultBufferRatio = uint64(10)
|
||||
|
||||
// DefaultGCPercent represents default garbage collection target percentage.
|
||||
var DefaultGCPercent = 20
|
||||
|
||||
// buffer represents the in memory cache of a single entry.
|
||||
// buffer carries value of the data and last accessed time.
|
||||
type buffer struct {
|
||||
@@ -46,9 +54,16 @@ type Cache struct {
|
||||
// read/write requests for cache
|
||||
mutex sync.Mutex
|
||||
|
||||
// Once is used for resetting GC once after
|
||||
// peak cache usage.
|
||||
onceGC sync.Once
|
||||
|
||||
// maxSize is a total size for overall cache
|
||||
maxSize uint64
|
||||
|
||||
// maxCacheEntrySize is a total size per key buffer.
|
||||
maxCacheEntrySize uint64
|
||||
|
||||
// currentSize is a current size in memory
|
||||
currentSize uint64
|
||||
|
||||
@@ -68,27 +83,58 @@ type Cache struct {
|
||||
stopGC chan struct{}
|
||||
}
|
||||
|
||||
// New - Return a new cache with a given default expiry duration.
|
||||
// If the expiry duration is less than one (or NoExpiry),
|
||||
// the items in the cache never expire (by default), and must be deleted
|
||||
// manually.
|
||||
// New - Return a new cache with a given default expiry
|
||||
// duration. If the expiry duration is less than one
|
||||
// (or NoExpiry), the items in the cache never expire
|
||||
// (by default), and must be deleted manually.
|
||||
func New(maxSize uint64, expiry time.Duration) *Cache {
|
||||
if maxSize == 0 {
|
||||
panic("objcache: setting maximum cache size to zero is forbidden.")
|
||||
}
|
||||
C := &Cache{
|
||||
maxSize: maxSize,
|
||||
entries: make(map[string]*buffer),
|
||||
expiry: expiry,
|
||||
|
||||
// A garbage collection is triggered when the ratio
|
||||
// of freshly allocated data to live data remaining
|
||||
// after the previous collection reaches this percentage.
|
||||
//
|
||||
// - https://golang.org/pkg/runtime/debug/#SetGCPercent
|
||||
//
|
||||
// This means that by default GC is triggered after
|
||||
// we've allocated an extra amount of memory proportional
|
||||
// to the amount already in use.
|
||||
//
|
||||
// If gcpercent=100 and we're using 4M, we'll gc again
|
||||
// when we get to 8M.
|
||||
//
|
||||
// Set this value to 20% if caching is enabled.
|
||||
debug.SetGCPercent(DefaultGCPercent)
|
||||
|
||||
// Max cache entry size - indicates the
|
||||
// maximum buffer per key that can be held in
|
||||
// memory. Currently this value is 1/10th
|
||||
// the size of requested cache size.
|
||||
maxCacheEntrySize := func() uint64 {
|
||||
i := maxSize / DefaultBufferRatio
|
||||
if i == 0 {
|
||||
i = maxSize
|
||||
}
|
||||
return i
|
||||
}()
|
||||
c := &Cache{
|
||||
onceGC: sync.Once{},
|
||||
maxSize: maxSize,
|
||||
maxCacheEntrySize: maxCacheEntrySize,
|
||||
entries: make(map[string]*buffer),
|
||||
expiry: expiry,
|
||||
}
|
||||
// We have expiry start the janitor routine.
|
||||
if expiry > 0 {
|
||||
C.stopGC = make(chan struct{})
|
||||
// Initialize a new stop GC channel.
|
||||
c.stopGC = make(chan struct{})
|
||||
|
||||
// Start garbage collection routine to expire objects.
|
||||
C.startGC()
|
||||
c.StartGC()
|
||||
}
|
||||
return C
|
||||
return c
|
||||
}
|
||||
|
||||
// ErrKeyNotFoundInCache - key not found in cache.
|
||||
@@ -100,18 +146,6 @@ var ErrCacheFull = errors.New("Not enough space in cache")
|
||||
// ErrExcessData - excess data was attempted to be written on cache.
|
||||
var ErrExcessData = errors.New("Attempted excess write on cache")
|
||||
|
||||
// Used for adding entry to the object cache. Implements io.WriteCloser
|
||||
type cacheBuffer struct {
|
||||
*bytes.Buffer // Implements io.Writer
|
||||
onClose func() error
|
||||
}
|
||||
|
||||
// On close, onClose() is called which checks if all object contents
|
||||
// have been written so that it can save the buffer to the cache.
|
||||
func (c cacheBuffer) Close() (err error) {
|
||||
return c.onClose()
|
||||
}
|
||||
|
||||
// Create - validates if object size fits with in cache size limit and returns a io.WriteCloser
|
||||
// to which object contents can be written and finally Close()'d. During Close() we
|
||||
// checks if the amount of data written is equal to the size of the object, in which
|
||||
@@ -126,29 +160,46 @@ func (c *Cache) Create(key string, size int64) (w io.WriteCloser, err error) {
|
||||
}() // Do not crash the server.
|
||||
|
||||
valueLen := uint64(size)
|
||||
// Check if the size of the object is not bigger than the capacity of the cache.
|
||||
if c.maxSize > 0 && valueLen > c.maxSize {
|
||||
// Check if the size of the object is > 1/10th the size
|
||||
// of the cache, if yes then we ignore it.
|
||||
if valueLen > c.maxCacheEntrySize {
|
||||
return nil, ErrCacheFull
|
||||
}
|
||||
|
||||
// Will hold the object contents.
|
||||
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||
// Check if the incoming size is going to exceed
|
||||
// the effective cache size, if yes return error
|
||||
// instead.
|
||||
c.mutex.Lock()
|
||||
if c.currentSize+valueLen > c.maxSize {
|
||||
c.mutex.Unlock()
|
||||
return nil, ErrCacheFull
|
||||
}
|
||||
// Change GC percent if the current cache usage
|
||||
// is already 75% of the maximum allowed usage.
|
||||
if c.currentSize > (75 * c.maxSize / 100) {
|
||||
c.onceGC.Do(func() { debug.SetGCPercent(DefaultGCPercent - 10) })
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
cbuf := &cappedWriter{
|
||||
offset: 0,
|
||||
cap: size,
|
||||
buffer: make([]byte, size),
|
||||
}
|
||||
|
||||
// Function called on close which saves the object contents
|
||||
// to the object cache.
|
||||
onClose := func() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if size != int64(buf.Len()) {
|
||||
if size != cbuf.offset {
|
||||
cbuf.Reset() // Reset resets the buffer to be empty.
|
||||
// Full object not available hence do not save buf to object cache.
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
if c.maxSize > 0 && c.currentSize+valueLen > c.maxSize {
|
||||
return ErrExcessData
|
||||
}
|
||||
// Full object available in buf, save it to cache.
|
||||
c.entries[key] = &buffer{
|
||||
value: buf.Bytes(),
|
||||
value: cbuf.buffer,
|
||||
lastAccessed: time.Now().UTC(), // Save last accessed time.
|
||||
}
|
||||
// Account for the memory allocated above.
|
||||
@@ -156,12 +207,10 @@ func (c *Cache) Create(key string, size int64) (w io.WriteCloser, err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Object contents that is written - cacheBuffer.Write(data)
|
||||
// Object contents that is written - cappedWriter.Write(data)
|
||||
// will be accumulated in buf which implements io.Writer.
|
||||
return cacheBuffer{
|
||||
buf,
|
||||
onClose,
|
||||
}, nil
|
||||
cbuf.onClose = onClose
|
||||
return cbuf, nil
|
||||
}
|
||||
|
||||
// Open - open the in-memory file, returns an in memory read seeker.
|
||||
@@ -215,17 +264,17 @@ func (c *Cache) gc() {
|
||||
|
||||
// StopGC sends a message to the expiry routine to stop
|
||||
// expiring cached entries. NOTE: once this is called, cached
|
||||
// entries will not be expired if the consumer has called this.
|
||||
// entries will not be expired, be careful if you are using this.
|
||||
func (c *Cache) StopGC() {
|
||||
if c.stopGC != nil {
|
||||
c.stopGC <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// startGC starts running a routine ticking at expiry interval, on each interval
|
||||
// this routine does a sweep across the cache entries and garbage collects all the
|
||||
// expired entries.
|
||||
func (c *Cache) startGC() {
|
||||
// StartGC starts running a routine ticking at expiry interval,
|
||||
// on each interval this routine does a sweep across the cache
|
||||
// entries and garbage collects all the expired entries.
|
||||
func (c *Cache) StartGC() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
@@ -242,9 +291,10 @@ func (c *Cache) startGC() {
|
||||
|
||||
// Deletes a requested entry from the cache.
|
||||
func (c *Cache) delete(key string) {
|
||||
if buf, ok := c.entries[key]; ok {
|
||||
if _, ok := c.entries[key]; ok {
|
||||
deletedSize := uint64(len(c.entries[key].value))
|
||||
delete(c.entries, key)
|
||||
c.currentSize -= uint64(len(buf.value))
|
||||
c.currentSize -= deletedSize
|
||||
c.totalEvicted++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,12 @@ func TestObjCache(t *testing.T) {
|
||||
cacheSize: 5,
|
||||
closeErr: ErrExcessData,
|
||||
},
|
||||
// Validate error excess data during write.
|
||||
{
|
||||
expiry: NoExpiry,
|
||||
cacheSize: 2048,
|
||||
err: ErrExcessData,
|
||||
},
|
||||
}
|
||||
|
||||
// Test 1 validating Open failure.
|
||||
@@ -232,14 +238,30 @@ func TestObjCache(t *testing.T) {
|
||||
if err = w.Close(); err != nil {
|
||||
t.Errorf("Test case 7 expected to pass, failed instead %s", err)
|
||||
}
|
||||
w, err = cache.Create("test2", 1)
|
||||
if err != nil {
|
||||
_, err = cache.Create("test2", 1)
|
||||
if err != ErrCacheFull {
|
||||
t.Errorf("Test case 7 expected to pass, failed instead %s", err)
|
||||
}
|
||||
// Write '1' byte.
|
||||
w.Write([]byte("H"))
|
||||
if err = w.Close(); err != testCase.closeErr {
|
||||
t.Errorf("Test case 7 expected to fail, passed instead")
|
||||
|
||||
// Test 8 validates rejecting Writes which write excess data.
|
||||
testCase = testCases[7]
|
||||
cache = New(testCase.cacheSize, testCase.expiry)
|
||||
w, err = cache.Create("test1", 5)
|
||||
if err != nil {
|
||||
t.Errorf("Test case 8 expected to pass, failed instead %s", err)
|
||||
}
|
||||
// Write '5' bytes.
|
||||
n, err := w.Write([]byte("Hello"))
|
||||
if err != nil {
|
||||
t.Errorf("Test case 8 expected to pass, failed instead %s", err)
|
||||
}
|
||||
if n != 5 {
|
||||
t.Errorf("Test case 8 expected 5 bytes written, instead found %d", n)
|
||||
}
|
||||
// Write '1' more byte, should return error.
|
||||
n, err = w.Write([]byte("W"))
|
||||
if n == 0 && err != testCase.err {
|
||||
t.Errorf("Test case 8 expected to fail with ErrExcessData, but failed with %s instead", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user