gcsfuse的TypeCache

问题背景

在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

github.com/GoogleCloud...

github.com/GoogleCloud...

后来开发者逐步发现TypeCache和StatCache这种双缓存架构有一些问题:

  1. 维护负担重。typeCache和StatCache是两套独立的缓存,需要分别做插入、失效、TTL管理,任何写操作都要同步两边,极易出现不一致。
  2. 冗余: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
}
  1. stat_cache与type_cache两个缓存存在的一致性问题:
sequenceDiagram participant U as User/FUSE participant I as dirInode participant TC as TypeCache participant SB as SyncerBucket participant SC as StatCache participant G as GCS Note over I,G: TypeCache 的问题:自成一体的信息孤岛 U->>I: LookUpChild(&#34;foo&#34;) I->>TC: Get(&#34;foo&#34;) → RegularFileType I->>SB: StatObject(&#34;foo&#34;) SB->>SC: LookUp(&#34;foo&#34;) → hit, return MinObject Note over I: TypeCache 在此处更新<br/>但不知道 stat cache 已经变了 Note over I,G: 冲突场景 GCSC->>G: 外部进程删除 foo, 创建 foo/ I->>TC: TTL 内仍返回 RegularFileType(过时) I->>SB: StatObject(&#34;foo&#34;) → 走 GCS → NotFoundError Note over I: 用户收到 ENOENT,但实际 foo/ 存在

整合设计思路

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)中,新设计了三层优化:

  1. 若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
}
  1. 若TypeCache废弃开关没有设置,则按照原有的方式查询TypeCache。

  2. 进入通用流程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。其实也是一种屎山了。

相关推荐
心运软件1 小时前
Python实战:中国大学排行榜数据采集与可视化大屏
后端·python
爱勇宝2 小时前
我做了一个排版器,也踩了一遍富文本复制的坑
前端·后端·微信
布朗克1682 小时前
Go 入门到精通-33-unsafe 与 CGO
开发语言·后端·golang·unsafe·cgo
铁皮饭盒2 小时前
面试官:如何用 Bun + JS 实现安全的文件 MCP 工具集
前端·javascript·后端
小Ti客栈2 小时前
Spring Boot 整合 Swagger2 和 Knife4j实现接口文档与可视化调试
java·spring boot·后端
明月_清风2 小时前
💰 DeFi 入门完全指南:从 Uniswap 到 Aave,一文读懂去中心化金融
后端·web3
Conan在掘金2 小时前
鸿蒙报错速查:struct 里嵌套 @Component struct 就炸,Unexpected keyword 编译报错,根因 + 真解法
后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(三十六):多驱动上传接口
后端·go·ai编程
明月_清风3 小时前
🎨 NFT 全景解析:从 JPEG 到数字所有权革命
后端·web3