JuiceFS:写流程源码解析+刷盘+数据一致性分析

引言

juicefs是一款面向云原生设计的高性能分布式文件系统,其有如下特点:

  • 数据存储和元数据存储分离,可以适配多种数据和元数据存储引擎。

  • 后端存储可以直接对接各种对象存储,使用起来更方便,更加适配云服务趋势。

相关技术架构可直接参考:https://juicefs.com/docs/zh/community/architecture

写流程cache简要说明

cache分为两层,磁盘cache和内存cache

  • 磁盘cache:数据刷盘时先写入磁盘cache,即本地文件系统,然后将文件上传到对象存储,然后再将本地文件系统文件删除掉

  • 内存cache:内存cache只记录key和datasize

源码分析

写同步线程处理

func (fs *fileSystem) Write(cancel <-chan struct{}, in *fuse.WriteIn, data []byte) (written uint32, code fuse.Status)
    func (v *VFS) Write(ctx Context, ino Ino, buf []byte, off, fh uint64) (err syscall.Errno)
        // 获取filehandle
        func (v *VFS) findHandle(inode Ino, fh uint64) *handle
        // 加文件写锁,若有其他client进行并发写,等待
        func (h *handle) Wlock(ctx Context) bool
        // filewriter写数据
        func (f *fileWriter) Write(ctx meta.Context, off uint64, data []byte) syscall.Errno
            // 当前file使用超过1000个slice,或使用的buffersize大于预留,等待
            // 加filewriter mutex锁,使用cas操作
            f.Lock()
            // 等待正在进行的flush完成
            // 根据offset和length,拆分chunk,每个chunk单独写
            func (f *fileWriter) writeChunk(ctx meta.Context, indx uint32, off uint32, data []byte) syscall.Errno
                // 根据文件的chunk idx找到对应的chunkwriter
                func (f *fileWriter) findChunk(i uint32) *chunkWriter
                // 在chunkwriter中找到slicewriter,如果没有合适的slice,申请一个新的写
                func (c *chunkWriter) findWritableSlice(pos uint32, size uint32) *sliceWriter
                    // 遍历每个slice,发现未覆盖的slice直接用,如果没有可用的slice,再外面申请一个
                // 对于每个chunk,第一个slice生成的时候启动一个异步线程,等待数据写盘后刷slice到对象存储的映射
                func (c *chunkWriter) commitThread()    
                // slicewriter写数据
                func (s *sliceWriter) write(ctx meta.Context, off uint32, data []uint8) syscall.Errno
                    // 将数据写入buffer
                    func (s *wSlice) WriteAt(p []byte, off int64) (n int, err error)
                    // 如果刚好写满一个chunk,异步刷盘(异步)
                    func (s *sliceWriter) flushData() 

slice刷盘

func (s *sliceWriter) flushData(
    // 生成sliceid
    func (s *sliceWriter) prepareID(
    // 刷盘
    func (s *wSlice) Finish(
        func (s *wSlice) FlushTo(
            // 遍历每一个block刷盘
            func (s *wSlice) upload(
            // 根据sliceid,chunkidx,blocksize生成key,用于对象存储文件名
                func (s *rSlice) key(
                // 进行slice数据写盘(异步)
                go func() {
    // 标记slice刷盘完成
    func (s *sliceWriter) markDone(

slice数据写盘

func (s *wSlice) upload(
    go func()
        // 写磁盘cache,用加速盘进行存储
        func (cache *cacheStore) stage
            // 生成路径,与对象存储相关路径一致,写数据到本地文件系统目录
            func (cache *cacheStore) flushPage(
                // 将数据写入磁盘cache,即使用os本地文件系统写文件
                func (f *File) Write(
            // 插入keymap,将key、datasize、atime,插入内存cache
            func (cache *cacheStore) add(
            // 若内存cache空间不足,触发淘汰
            
        // 数据上传到对象存储
        func (store *cachedStore) upload(
            // 上传对象存储
            func (store *cachedStore) put(
        
        // 更新内存cache中的key,和对应的data
        func (cache *cacheStore) uploaded(
        // 删除本地文件系统cache的文件
        os.Remove(stagingPath)

关键总结

  • 多客户端如何保证数据一致性:不同客户端在读写时需要先调用SetLk、SetLkw、Flock接口,保证客户端之间的互斥。

  • 并发读写如何保证数据一致性:使用filehandle上的读写锁保证数据一致性。

  • 并发写如何保证内存原子性:使用filewriter上面的mutex锁保证原子性。

  • 并发写如何保证元数据和数据的一致性:写数据到盘的过程中使用的是追加写,元数据没有修改时不会更新数据,保证原子性。写流程不会更新inode信息(如size等),只会更新block到存储对象的映射,因此不会出现更新inode信息并发的情况。

  • 写文件如何保证inode数据和文件的一致性:不能保证,只有close文件后,才会更新inode信息,因此juicefs只能保证文件最终inode和文件信息的一致性。

  • slice中连续小io会在slice里面进行聚合,等待触发刷slice

参考文献

https://juicefs.com/docs/zh/community/internals

你可能感兴趣的:(JuiceFS,分布式,linux,云计算,大数据,数据库)