cubefs/client/blockcache/bcache/manage.go
leonrayang 3396accd80 feat(projectstructure): reorganize the module directory in the root to a suitable subdirectory
close:#22458839

Signed-off-by: leonrayang <chl696@sina.com>
2024-12-26 09:24:22 +08:00

747 lines
18 KiB
Go

// Copyright (C) 2020 Juicefs
// Modified work Copyright 2022 The CubeFS Authors.
//
// 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 bcache
import (
"bufio"
"bytes"
"container/list"
"crypto/md5"
"encoding/hex"
"fmt"
"hash/crc32"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/cubefs/cubefs/util/errors"
"github.com/cubefs/cubefs/util/log"
"github.com/cubefs/cubefs/util/stat"
)
const (
PathListSeparator = ";"
CacheConfSeparator = ":"
SpaceCheckInterval = 60 * time.Second
TmpFileCheckInterval = 20 * 60 * time.Second
FilePerm = 0o644
Basedir = "blocks"
)
type ReadCloser interface {
io.Reader
io.ReaderAt
io.Closer
}
type BcacheManager interface {
cache(key string, data []byte, direct bool)
read(key string, offset uint64, len uint32) (io.ReadCloser, error)
queryCachePath(key string, offset uint64, len uint32) (string, error)
load(key string) (ReadCloser, error)
erase(key string)
stats() (int64, int64)
}
func newBcacheManager(conf *bcacheConfig) BcacheManager {
log.LogInfof("init block cache: %s size:%d GB", conf.CacheDir, conf.BlockSize)
if conf.CacheDir == "" {
log.LogWarnf("no cache config,cacheDirs or size is empty!")
return nil
}
// todo cachedir reg match
cacheDirs := strings.Split(conf.CacheDir, PathListSeparator)
if len(cacheDirs) == 0 {
log.LogWarnf("no cache dir config!")
return nil
}
dirSizeMap := make(map[string]int64, len(cacheDirs))
for _, dir := range cacheDirs {
result := strings.Split(dir, CacheConfSeparator)
dirPath := result[0]
cacheSize, err := strconv.Atoi(result[1])
if dirPath == "" || err != nil {
log.LogWarnf("cache dir config error!")
return nil
}
dirSizeMap[dirPath] = int64(cacheSize)
conf.CacheSize = conf.CacheSize + int64(cacheSize)
}
bm := &bcacheManager{
bstore: make([]*DiskStore, len(cacheDirs)),
bcacheKeys: make(map[string]*list.Element),
lrulist: list.New(),
blockSize: conf.BlockSize,
pending: make(chan waitFlush, 1024),
}
index := 0
for cacheDir, cacheSize := range dirSizeMap {
disk := NewDiskStore(cacheDir, cacheSize, conf)
bm.bstore[index] = disk
go bm.reBuildCacheKeys(cacheDir, disk)
index++
}
go bm.spaceManager()
go bm.flush()
// go bm.scrub()
return bm
}
type cacheItem struct {
key string
size uint32
}
type keyPair struct {
key string
it *cacheItem
}
// key vid_inode_offset
type waitFlush struct {
Key string
Data []byte
}
type bcacheManager struct {
sync.RWMutex
bcacheKeys map[string]*list.Element
lrulist *list.List
bstore []*DiskStore
blockSize uint32
pending chan waitFlush
}
func encryptXOR(data []byte) {
for index, value := range data {
data[index] = value ^ byte(0xF)
}
}
func (bm *bcacheManager) queryCachePath(key string, offset uint64, len uint32) (path string, err error) {
bgTime := stat.BeginStat()
defer func() {
stat.EndStat("GetCache:GetCachePath", err, bgTime, 1)
}()
bm.Lock()
element, ok := bm.bcacheKeys[key]
bm.Unlock()
if ok {
item := element.Value.(*cacheItem)
path, err := bm.getCachePath(key)
if err != nil {
return "", err
}
bm.Lock()
bm.lrulist.MoveToBack(element)
bm.Unlock()
log.LogDebugf("Cache item found. key=%v offset =%v,len=%v size=%v, path=%v", key, offset, len, item.size, path)
return path, nil
}
log.LogDebugf("Cache item not found. key=%v offset =%v,len=%v", key, offset, len)
return "", os.ErrNotExist
}
func (bm *bcacheManager) getCachePath(key string) (string, error) {
if len(bm.bstore) == 0 {
return "", errors.New("no cache dir")
}
cachePath := bm.selectDiskKv(key).getPath(key)
return cachePath, nil
}
func (bm *bcacheManager) cache(key string, data []byte, direct bool) {
bgTime := stat.BeginStat()
defer func() {
stat.EndStat("Cache:Write", nil, bgTime, 1)
stat.StatBandWidth("Cache", uint32(len(data)))
}()
log.LogDebugf("TRACE cache. key(%v) len(%v) direct(%v)", key, len(data), direct)
if direct {
bm.cacheDirect(key, data)
return
}
select {
case bm.pending <- waitFlush{Key: key, Data: data}:
default:
log.LogDebugf("pending chan is full,skip memory. key =%v,len=%v bytes", key, len(data))
bm.cacheDirect(key, data)
}
}
func (bm *bcacheManager) cacheDirect(key string, data []byte) {
diskKv := bm.selectDiskKv(key)
if diskKv.flushKey(key, data) == nil {
bm.Lock()
item := &cacheItem{
key: key,
size: uint32(len(data)),
}
element := bm.lrulist.PushBack(item)
bm.bcacheKeys[key] = element
bm.Unlock()
}
}
func (bm *bcacheManager) read(key string, offset uint64, len uint32) (io.ReadCloser, error) {
var err error
bgTime := stat.BeginStat()
defer func() {
stat.EndStat("GetCache:Read", err, bgTime, 1)
if err == nil {
stat.StatBandWidth("GetCache:Read", len)
}
}()
metaBgTime := stat.BeginStat()
bm.Lock()
element, ok := bm.bcacheKeys[key]
bm.Unlock()
stat.EndStat("GetCache:Read:GetMeta", nil, metaBgTime, 1)
log.LogDebugf("Trace read. ok =%v", ok)
if ok {
item := element.Value.(*cacheItem)
f, err := bm.load(key)
if os.IsNotExist(err) {
bm.Lock()
delete(bm.bcacheKeys, key)
bm.Unlock()
d := bm.selectDiskKv(key)
atomic.AddInt64(&d.usedSize, -int64(item.size))
atomic.AddInt64(&d.usedCount, -1)
return nil, os.ErrNotExist
}
if err != nil {
return nil, err
}
defer f.Close()
size := item.size
log.LogDebugf("read. offset =%v,len=%v size=%v", offset, len, size)
if uint32(offset)+len > size {
len = size - uint32(offset)
}
dataBgTime := stat.BeginStat()
buf := make([]byte, len)
n, err := f.ReadAt(buf, int64(offset))
stat.EndStat("GetCache:Read:ReadData", err, dataBgTime, 1)
if err != nil {
return nil, err
} else {
// decrypt
encryptXOR(buf[:n])
return io.NopCloser(bytes.NewBuffer(buf[:n])), nil
}
} else {
err = os.ErrNotExist
}
return nil, err
}
func (bm *bcacheManager) load(key string) (ReadCloser, error) {
if len(bm.bstore) == 0 {
return nil, errors.New("no cache dir")
}
f, err := bm.selectDiskKv(key).load(key)
if err != nil {
return nil, err
}
bm.Lock()
defer bm.Unlock()
if element, ok := bm.bcacheKeys[key]; ok {
bm.lrulist.MoveToBack(element)
}
return f, err
}
func (bm *bcacheManager) erase(key string) {
if len(bm.bstore) == 0 {
return
}
err := bm.selectDiskKv(key).remove(key)
if err == nil {
bm.Lock()
defer bm.Unlock()
if element, ok := bm.bcacheKeys[key]; ok {
bm.lrulist.Remove(element)
}
delete(bm.bcacheKeys, key)
}
}
func (bm *bcacheManager) stats() (int64, int64) {
var usedCount, usedSize int64
for _, item := range bm.bstore {
usedSize += atomic.LoadInt64(&item.usedSize)
usedCount += atomic.LoadInt64(&item.usedCount)
}
return usedCount, usedSize
}
func (bm *bcacheManager) selectDiskKv(key string) *DiskStore {
return bm.bstore[hashKey(key)%uint32(len(bm.bstore))]
}
func (bm *bcacheManager) spaceManager() {
ticker := time.NewTicker(SpaceCheckInterval)
tmpTicker := time.NewTicker(TmpFileCheckInterval)
defer func() {
ticker.Stop()
tmpTicker.Stop()
}()
for {
select {
case <-ticker.C:
for _, store := range bm.bstore {
useRatio, files := store.diskUsageRatio()
log.LogDebugf("useRation(%v), files(%v)", useRatio, files)
if 1-useRatio < store.freeLimit || files > int64(store.limit) {
bm.freeSpace(store, 1-useRatio, files)
}
}
case <-tmpTicker.C:
for _, store := range bm.bstore {
useRatio, files := store.diskUsageRatio()
log.LogInfof("useRation(%v), files(%v)", useRatio, files)
bm.deleteTmpFile(store)
}
}
}
}
//lru cache
//func (bm *bcacheManager) freeSpace(index int, store *DiskStore, free float32, files int64) {
// var decreaseSpace int64
// var decreaseCnt int
// storeCnt := uint32(len(bm.bstore))
// bm.Lock()
// defer bm.Unlock()
// if free < store.freeLimit {
// decreaseSpace = int64((store.freeLimit - free) * (float32(store.capacity)))
// }
// if files > int64(store.limit) {
// decreaseCnt = int(files - int64(store.limit))
// }
// var lastKey string
// var lastItem cacheItem
// var cnt int
// for key, value := range bm.bcacheKeys {
// if int(hashKey(key)%storeCnt) == index {
// if cnt == 0 || lastItem.atime > value.atime {
// lastKey = key
// lastItem = value
// }
// cnt++
// if cnt > 1 {
// store.remove(lastKey)
// delete(bm.bcacheKeys, lastKey)
// decreaseSpace -= int64(value.size)
// decreaseCnt--
// cnt = 0
// log.LogDebugf("remove %s from cache, age: %d", lastKey, lastItem.atime)
// if decreaseCnt <= 0 && decreaseSpace <= 0 {
// break
// }
// }
// }
// }
//
//}
// lru
func (bm *bcacheManager) freeSpace(store *DiskStore, free float32, files int64) {
var decreaseSpace int64
var decreaseCnt int
if free < store.freeLimit {
decreaseSpace = int64((store.freeLimit - free) * (float32(store.capacity)))
}
if files > int64(store.limit) {
decreaseCnt = int(files - int64(store.limit))
}
cnt := 0
for {
if decreaseCnt <= 0 && decreaseSpace <= 0 {
break
}
// avoid dead loop
if cnt > 500000 {
break
}
bm.Lock()
element := bm.lrulist.Front()
if element == nil {
bm.Unlock()
return
}
item := element.Value.(*cacheItem)
if err := store.remove(item.key); err == nil {
bm.lrulist.Remove(element)
delete(bm.bcacheKeys, item.key)
decreaseSpace -= int64(item.size)
decreaseCnt--
cnt++
}
bm.Unlock()
log.LogDebugf("remove %v from cache", item.key)
}
}
func (bm *bcacheManager) reBuildCacheKeys(dir string, store *DiskStore) {
if _, err := os.Stat(dir); err != nil {
log.LogErrorf("cache dir %s is not exists", dir)
return
}
log.LogDebugf("reBuildCacheKeys(%s)", dir)
c := make(chan keyPair)
keyPrefix := filepath.Join(dir, Basedir)
go func() {
filepath.Walk(dir, bm.walker(c, keyPrefix, true))
close(c)
}()
for value := range c {
bm.Lock()
element := bm.lrulist.PushBack(value.it)
bm.bcacheKeys[value.key] = element
bm.Unlock()
log.LogDebugf("updateStat(%v)", value.it.size)
store.updateStat(value.it.size)
}
}
func (bm *bcacheManager) walker(c chan keyPair, prefix string, initial bool) filepath.WalkFunc {
return func(path string, info os.FileInfo, err error) error {
if err != nil {
log.LogWarnf("walk path %v failed %v", path, err)
return err
}
if info.IsDir() || !strings.HasPrefix(path, prefix) {
return nil
}
if strings.HasSuffix(path, ".tmp") && (initial || checkoutTempFileOuttime(path)) {
os.Remove(path)
log.LogDebugf("Remove tmp file %v", path)
return nil
}
_, key := filepath.Split(path)
size := uint32(info.Size())
pair := keyPair{
key: key,
it: &cacheItem{
key: key,
size: size,
},
}
c <- pair
return nil
}
}
func (bm *bcacheManager) flush() {
for {
pending := <-bm.pending
diskKv := bm.selectDiskKv(pending.Key)
log.LogDebugf("flush data,key(%v), dir(%v)", pending.Key, diskKv.dir)
if diskKv.flushKey(pending.Key, pending.Data) == nil {
bm.Lock()
item := &cacheItem{
key: pending.Key,
size: uint32(len(pending.Data)),
}
element := bm.lrulist.PushBack(item)
bm.bcacheKeys[pending.Key] = element
bm.Unlock()
}
}
}
func hashKey(key string) uint32 {
return crc32.ChecksumIEEE([]byte(key))
}
type DiskStore struct {
sync.Mutex
dir string
mode uint32
capacity int64
freeLimit float32
limit uint32
usedSize int64
usedCount int64
}
func NewDiskStore(dir string, cacheSize int64, config *bcacheConfig) *DiskStore {
if config.Mode == 0 {
config.Mode = FilePerm
}
if config.FreeRatio <= 0 {
config.FreeRatio = 0.15
}
if config.Limit <= 0 {
config.Limit = 50000000
}
if config.Limit > 50000000 {
config.Limit = 50000000
}
c := &DiskStore{
dir: dir,
mode: config.Mode,
capacity: cacheSize,
freeLimit: config.FreeRatio,
limit: config.Limit,
}
log.LogDebugf("ignored method DiskStore.scrub at %p", c.scrub) // TODO: ignored
c.checkBuildCacheDir(dir)
return c
}
func (d *DiskStore) checkBuildCacheDir(dir string) {
mode := os.FileMode(d.mode)
if st, err := os.Stat(dir); os.IsNotExist(err) {
if parent := filepath.Dir(dir); parent != dir {
d.checkBuildCacheDir(parent)
}
os.Mkdir(dir, mode)
} else if err != nil && st.Mode() != mode {
os.Chmod(dir, mode)
}
}
//func (d *DiskStore) flushKey(key string, data []byte) error {
// var err error
// bgTime := stat.BeginStat()
// defer func() {
// stat.EndStat("Cache:Write:FlushData", err, bgTime, 1)
// }()
// cachePath := d.buildCachePath(key, d.dir)
// log.LogDebugf("TRACE BCacheService flushKey Enter. key(%v) cachePath(%v)", key, cachePath)
// d.checkBuildCacheDir(filepath.Dir(cachePath))
// tmp := cachePath + ".tmp"
// f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(d.mode))
// defer os.Remove(tmp)
// if err != nil {
// log.LogErrorf("Create block tmp file:%s err:%s!", tmp, err)
// return err
// }
// //encrypt
// encryptXOR(data)
// _, err = f.Write(data)
// if err != nil {
// f.Close()
// log.LogErrorf("Write tmp failed: file %s err %s!", tmp, err)
// return err
// }
// err = f.Close()
// if err != nil {
// log.LogErrorf("Close tmp failed: file:%s err:%s!", tmp, err)
// return err
// }
// info, err := os.Stat(cachePath)
// //if already cached
// if !os.IsNotExist(err) {
// atomic.AddInt64(&d.usedSize, -(info.Size()))
// atomic.AddInt64(&d.usedCount, -1)
// os.Remove(cachePath)
// }
// err = os.Rename(tmp, cachePath)
// if err != nil {
// log.LogErrorf("Rename block tmp file:%s err:%s!", tmp, err)
// return err
// }
// atomic.AddInt64(&d.usedSize, int64(len(data)))
// atomic.AddInt64(&d.usedCount, 1)
// log.LogDebugf("TRACE BCacheService flushKey Exit. key(%v) cachePath(%v)", key, cachePath)
// return nil
//
//}
func (d *DiskStore) flushKey(key string, data []byte) error {
var err error
bgTime := stat.BeginStat()
defer func() {
stat.EndStat("Cache:Write:FlushData", err, bgTime, 1)
}()
cachePath := d.buildCachePath(key, d.dir)
info, err := os.Stat(cachePath)
// if already cached
if err == nil && info.Size() > 0 {
return nil
}
log.LogDebugf("TRACE BCacheService flushKey Enter. key(%v) cachePath(%v)", key, cachePath)
d.checkBuildCacheDir(filepath.Dir(cachePath))
tmp := cachePath + ".tmp"
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(d.mode))
defer os.Remove(tmp)
if err != nil {
log.LogWarnf("Create block tmp file:%s err:%s!", tmp, err)
return err
}
// encrypt
encryptXOR(data)
_, err = f.Write(data)
if err != nil {
f.Close()
log.LogErrorf("Write tmp failed: file %s err %s!", tmp, err)
return err
}
err = f.Close()
if err != nil {
log.LogErrorf("Close tmp failed: file:%s err:%s!", tmp, err)
return err
}
//info, err := os.Stat(cachePath)
////if already cached
//if !os.IsNotExist(err) {
// atomic.AddInt64(&d.usedSize, -(info.Size()))
// atomic.AddInt64(&d.usedCount, -1)
// os.Remove(cachePath)
//}
err = os.Rename(tmp, cachePath)
if err != nil {
log.LogErrorf("Rename block tmp file:%s err:%s!", tmp, err)
return err
}
atomic.AddInt64(&d.usedSize, int64(len(data)))
atomic.AddInt64(&d.usedCount, 1)
log.LogDebugf("TRACE BCacheService flushKey Exit. key(%v) cachePath(%v)", key, cachePath)
return nil
}
func (d *DiskStore) load(key string) (ReadCloser, error) {
cachePath := d.buildCachePath(key, d.dir)
log.LogDebugf("TRACE BCacheService load Enter. key(%v) cachePath(%v)", key, cachePath)
//if _, err := os.Stat(cachePath); err != nil {
// return nil, errors.NewError(os.ErrNotExist)
//}
f, err := os.OpenFile(cachePath, os.O_RDONLY, os.FileMode(d.mode))
log.LogDebugf("TRACE BCacheService load Exit. err(%v)", err)
return f, err
}
func (d *DiskStore) remove(key string) (err error) {
var size int64
cachePath := d.buildCachePath(key, d.dir)
log.LogDebugf("remove. cachePath(%v)", cachePath)
if info, err := os.Stat(cachePath); err == nil {
size = info.Size()
if err = os.Remove(cachePath); err == nil {
atomic.AddInt64(&d.usedSize, -size)
atomic.AddInt64(&d.usedCount, -1)
}
}
return err
}
func (d *DiskStore) buildCachePath(key string, dir string) string {
inodeId, err := strconv.ParseInt(strings.Split(key, "_")[1], 10, 64)
if err != nil {
return fmt.Sprintf("%s/blocks/%d/%d/%s", dir, hashKey(key)&0xFFF%512, hashKey(key)%512, key)
}
return fmt.Sprintf("%s/blocks/%d/%d/%s", dir, hashKey(key)&0xFFF%512, inodeId%512, key)
}
func (d *DiskStore) diskUsageRatio() (float32, int64) {
log.LogDebugf("usedSize(%v), usedCount(%v)", atomic.LoadInt64(&d.usedSize), atomic.LoadInt64(&d.usedCount))
if atomic.LoadInt64(&d.usedSize) < 0 || atomic.LoadInt64(&d.usedCount) < 0 {
return 0, 0
}
return float32(atomic.LoadInt64(&d.usedSize)) / float32(d.capacity), atomic.LoadInt64(&d.usedCount)
}
func (d *DiskStore) scrub(key string, md5Sum string) error {
defer func() {
if r := recover(); r != nil {
return
}
}()
cachePath := d.buildCachePath(key, d.dir)
f, err := os.Open(cachePath)
if err != nil {
return err
}
defer f.Close()
r := bufio.NewReader(f)
h := md5.New()
_, err = io.Copy(h, r)
if err != nil {
return err
}
if md5Sum != hex.EncodeToString(h.Sum(nil)) {
return errors.New("scrub error")
}
return nil
}
func (d *DiskStore) updateStat(size uint32) {
atomic.AddInt64(&d.usedSize, int64(size))
atomic.AddInt64(&d.usedCount, 1)
}
func (d *DiskStore) getPath(key string) string {
cachePath := d.buildCachePath(key, d.dir)
return cachePath
}
func (bm *bcacheManager) deleteTmpFile(store *DiskStore) {
if _, err := os.Stat(store.dir); err != nil {
log.LogErrorf("cache dir %s is not exists", store.dir)
return
}
log.LogDebugf("clear tmp files in %v", store.dir)
c := make(chan keyPair)
keyPrefix := filepath.Join(store.dir, Basedir)
log.LogDebugf("keyPrefix %v", keyPrefix)
go func() {
filepath.Walk(store.dir, bm.walker(c, keyPrefix, false))
close(c)
}()
// consume chan
for range c {
}
log.LogDebugf("clear tmp files end%v", store.dir)
}
func checkoutTempFileOuttime(file string) bool {
finfo, err := os.Stat(file)
if err != nil {
return false
}
stat_t := finfo.Sys().(*syscall.Stat_t)
now := time.Now()
return now.Sub(timespecToTime(stat_t.Ctim)).Seconds() > 60*60 // 1 hour
}
func timespecToTime(ts syscall.Timespec) time.Time {
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
}