问题背景
在gcsfuse中,TypeCache的存在是为了解决一个文件系统模拟对象存储的根本gap: 对象存储的目录是由'/'来模拟的,它会有两种生成方式:
- 隐式目录:例如用户直接上传一个对象
<bucket_name>/prefix/to/file,此处的prefix,to都可以被当成是目录,但是本质上/并不存在。 - 显式目录:例如用户依次上传对象
<bucket_name>/prefix/,<bucket_name>/prefix/to/,<bucket_name>/prefix/to/file,那么实际上这些/都是存在的。这被称为"显式目录"。
即有以下几种定义:
go
type Type int
const (
UnknownType Type = 0
SymlinkType Type = 1
RegularFileType Type = 2
ExplicitDirType Type = 3
ImplicitDirType Type = 4
NonexistentType Type = 5 //独立的一个flag表示 负缓存
)
在LookUpChild时需要判断一个字路径是文件还是目录,或不存在。在早期设计中,是独立维护了一个typeCache,每次lookUp先查询typeCache拿到cachedType,再据此决定取GCS查什么。
type_cache的数据结构
TypeCache接口中则定义了基本增删改查方法:
go
type TypeCache interface {
// Insert inserts the given entry (name -> type)
// with the entry-expiration at now+ttl.
Insert(now time.Time, name string, it Type)
// Erase removes the entry with the given name.
Erase(name string)
// Get returns the entry with given name, and also
// records this entry as latest accessed in the cache.
// If now > expiration, then entry is removed from cache, and
// UnknownType is returned.
// If entry doesn't exist in the cache, then
// UnknownType is returned.
Get(now time.Time, name string) Type
}
具体的缓存entry很简单,针对key来描述其Type,然后指定ttl计算后的expiry过期时间。如下所示:
go
type cacheEntry struct {
expiry time.Time
inodeType Type
// Copy of key string (internally only
// points to the original
// string data, so size overhead is a fixed 16 bytes, i.e. O(1),
// irrespective of actual key value.
// This is needed to calculate the
// accurate size of the type-cache entry on heap.
key string
}
通过配置项TypeCacheMaxSizeMb来限制typeCache的缓存大小,通过dirTypeCacheTTL来限制过期时间。
type_cache的生命周期
缓存的添加
添加缓存定义如下:
go
func (tc *typeCache) Insert(now time.Time, name string, it Type) {
if tc.entries != nil { // only if caching is enabled
_, err := tc.entries.Insert(name, cacheEntry{
expiry: now.Add(tc.ttl),
inodeType: it,
key: name,
})
if err != nil {
panic(fmt.Errorf("failed to insert entry in typeCache: %v", err))
}
}
}
在创建文件/目录/symlink的时候添加。如MkDir() -> CreateChildDir()。调用完创建对象接口后,便会将type写入到缓存中,如此处的type为metadata.ExplicitDirType:
go
if !d.IsTypeCacheDeprecated() {
d.cache.Insert(d.cacheClock.Now(), name, metadata.ExplicitDirType)
}
还有许多其他的路径,例如在LookUpChild(),CreateChildFile(),CreateChildSymlink(),CreateChildDir()等处,可以获取到当前文件的type的场景,都可以设置type。
这个Cache.Insert()的本质上是一个lru。此处不再赘述。
缓存的消费
在LookUpChild(ctx, name)中,会先调用cachedType = d.cache.Get(d.cacheClock.Now(), name)来获取该name的type,然后根据不同的type(如ImplicitDirType, ExplicitDirType, RegularFileType/SymlinkType, NonexistentType)调用不同的接口来填充inode数据结构。
例如对于ImlicitDirType,这个目录本身在对象存储中不存在,因此直接构建一个Core数据结构,填充Bucket,FullName,并且令MinObject=nil。
对于ExplicitDirType,即在对象存储中存在这个对象,则调用findExplicitInode()进行处理。此时会实际调用StatObject接口,获取到MinObject,然后对Core数据结构进行填充。
Core数据结构,顾名思义,是一个"核心"数据结构",只保留最关键的部分,以降低内存占用。其数据结构如下所示:
go
// Core contains critical information about an inode before its creation.
type Core struct {
// The full name of the file or directory. Required for all inodes.
FullName Name
// The bucket that backs up the inode. Required for all inodes except the
// base directory that holds all the buckets mounted.
Bucket *gcsx.SyncerBucket
// The GCS object in the bucket above that backs up the inode. Can be empty
// if the inode is the base directory or an implicit directory.
MinObject *gcs.MinObject
// The GCS folder in the hierarchical bucket that backs up the inode.
Folder *gcs.Folder
// Specifies a local object which is not yet synced to GCS.
Local bool
}
演进:去除typeCache
后来开发者逐步发现TypeCache和StatCache这种双缓存架构有一些问题:
- 维护负担重。typeCache和StatCache是两套独立的缓存,需要分别做插入、失效、TTL管理,任何写操作都要同步两边,极易出现不一致。
- 冗余:StatCache本身已经存了完整的MinObject(其包含Generation, 大小等),类型信息可以从中进行推导,无需维护一套独立的TypeCache。
MinObject数据结构:
go
// MinObject is a record representing subset of properties of a particular
// generation object in GCS.
//
// See here for more information about its fields:
//
// https://cloud.google.com/storage/docs/json_api/v1/objects#resource
type MinObject struct {
Name string
Size uint64
Generation int64
MetaGeneration int64
Updated time.Time
Finalized time.Time
Metadata map[string]string
ContentEncoding string
CRC32C *uint32 // Missing for CMEK buckets
}
- stat_cache与type_cache两个缓存存在的一致性问题:
整合设计思路
在StatObjectRequest/ListObjectsRequest/GetFolderREquest结构体中,增加一个FetchOnlyFromCache,用于描述"请求是否应该显式地从StatCache中查询。如果是true,则先在StatCache中查询:cache miss的话,返回CacheMissError,但是不fall back到GCS中。如果该值为false,则会在cache miss的时候fallback到GCS中查询。
为什么要专门设计"不fall back到GCS中"呢? 这就要从该值的赋值处开始查看:
在findExplicitInode(ctx, bucket, name, fetchOnlyFromCache)/findExplicitFolder(...)中传入fetchOnlyFromCache变量,调用该方法的路径有如下众多,但大部分设置为false,只有少量设置为true:
在dirInode LookUpChild(ctx, name)中,新设计了三层优化:
- 若TypeCache废弃开关为true,且
d.metadataCacheTtlSecs!=0,即要求从StatCache中查询。此时强制要求只从StatCache中查询,若查不到,也不直接fall back到GCS:
- 先按照目录来查询。若查到了(
dirResult != nil),则直接返回,否则继续。
go
dirResult, dirErr = findExplicitInode(ctx, d.Bucket(), NewDirName(d.Name(), name), true)
// If we hit a real error (not a cache miss), exit early.
if dirErr != nil && !errors.As(dirErr, &cacheMissErr) {
return nil, dirErr
}
// If we found a directory, we're done. Return it now.
if dirResult != nil {
return dirResult, nil
}
- 若目录方式未查到,则按照文件来查。同样,若查到,则直接返回,否则继续。
先按照目录查询,再按照文件查询,这是因为设计上如果一个名字能同时被解释为目标和文件,则优先返回目录类型。POSIX 语义中目录优先(因为目录是命名空间的骨架结构),gcsfuse 遵循了这个惯例。
go
fileResult, fileErr := findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name), true)
if fileErr != nil && !errors.As(fileErr, &cacheMissErr) {
return nil, fileErr
}
if fileResult != nil {
return fileResult, nil
}
- 若这两次查询都返回cache hits(即没有cacheMiss报错),但没有
dirResult/fileResult,则 意味着是一个负缓存entry,则返回nil以表示该文件/文件夹显式地不存在:
go
if dirErr == nil && fileErr == nil {
return nil, nil
}
-
若TypeCache废弃开关没有设置,则按照原有的方式查询TypeCache。
-
进入通用流程
result, err := d.fetchCoreEntity(ctx, name, cachedType)。默认cachedType为
metadata.UnknownType,如果有在TypeCache缓存中查到type,则继续传递下去,否则保持UnknownType,将会在通用流程中进行二次查询。通用流程的本质实际上还是通过不同的Type进行构建或者查询。区别在于此处的
findExplicitInode()方法中的fetchOnlyFromCache被设置为false,即当查不到的时候,会fall back到GCS请求,去进行实际的查询操作。
go
// fetchCoreEntity contains all the existing logic for looking up children
// without worrying about the isTypeCacheDeprecated flag.
func (d *dirInode) fetchCoreEntity(ctx context.Context, name string, cachedType metadata.Type) (*Core, error) {
switch cachedType {
case metadata.ImplicitDirType:
return &Core{
Bucket: d.Bucket(),
FullName: NewDirName(d.Name(), name),
MinObject: nil,
}, nil
case metadata.NonexistentType:
return nil, nil
case metadata.ExplicitDirType:
if d.isBucketHierarchical() {
return findExplicitFolder(ctx, d.Bucket(), NewDirName(d.Name(), name), false)
}
return findExplicitInode(ctx, d.Bucket(), NewDirName(d.Name(), name), false)
case metadata.RegularFileType, metadata.SymlinkType:
return findExplicitInode(ctx, d.Bucket(), NewFileName(d.Name(), name), false)
case metadata.UnknownType:
return d.lookUpUnknownType(ctx, name)
}
return nil, nil
}
所以回到问题,前面专门设计"不fall back到GCS中",是因为后面有统一的流程去fall back,不需要前面就fall back。其实也是一种屎山了。