1.基本組件介紹
JuiceFS Client:支持多種Client端的接口,比如兼容POSIX文件系統的接口,以此你可以將它掛載到系統上當文件系統使用,且可以為k8s提供存儲使用,用ks8s的csi driver進行接入。同時也支持S3協議,開發了對應的S3網關進行支持;
Data Storage:對象存儲服務,用以存儲具體數據的,可以類比文件系統里的block數據保存,支持多種后端存儲;
Metadata Engine:元數據服務,用以存儲文件元數據信息的,比如文件名、目錄信息、文件inode等信息,可以類比文件系統里的inode數據管理,支持多種元數據存儲;
2.快速部署
我們使用docker部署一個minio作為對象存儲服務,用docker部署一個redis作為元數據服務。
git下載juicefs代碼並進行編譯生成juicefs二進制可執行文件:
git clone https://github.com/juicedata/juicefs.git
cd juicefs && make
安裝docker和下載redis和minio/minio鏡像。
部署元數據服務和對象存儲服務:
sudo docker run -d --name redis -v /data/redis-data:/data -p 6379:6379 --restart unless-stopped redis redis-server --appendonly yes
sudo docker run -d --name minio -v /data/minio-data:/data -p 9000:9000 --restart unless-stopped minio/minio server /data
進行format和掛載:
mkdir /data/ussfs
./juicefs format --storage minio --bucket http://127.0.0.1:9000/test123 --access-key minioadmin --secret-key minioadmin redis://127.0.0.1:6379/10 test123
./juicefs mount -d redis://127.0.0.1:6379/10 /data/ussfs
3.掛載過程分析
format過程:
format的作用是將一些元數據信息先注冊好,在mount時進行獲取作為配置參數,比如對象存儲的相關信息,bucket的相關信息等。
流程說明:
(1)接收客戶端執行的命令,如果是mount的命令,則進入mount邏輯;
(2)通過format給定的url來判斷是哪種元數據服務,並初始化元數據服務對象;
(3)構建format結構體對象,結構體里包含對象存儲服務信息,block size等信息;
(4)根據format的信息初始化對象存儲服務並測試對象存儲服務是否可增刪改操作,確保對象存儲服務可用;
(5)持久化format信息,在redis為元數據服務時,即是將格式化后的format數據保存到redis的setting這個key中;
(6)創建inode號為1的第一個文件,文件類型為目錄,作為后續創建文件的父目錄,並持久化到元數據中。
mount過程:
mount就是將自定義的文件系統掛載到指定目錄下,可供符合POSIX的接口進行調用,由mount中開啟的server服務進行文件操作請求接收並處理。
流程說明:
(1)獲取mount中的命令行參數,獲取到元數據信息url;
(2)創建元數據服務連接實例,從元數據服務中獲取之前保存的format信息;
(3)根據format信息創建對象存儲服務連接實例storage,創建store對象,store對象是對對象存儲數據進行管理,store對象屬性里包含了storage對象和對cache的管理;
(4)初始化vfs對象,vfs是一層虛擬文件系統對象,它包含了對meta和storage的管理,創建了讀寫對象和文件句柄管理;
(5)如果命令行參數中是有用-d指定了mount進程后台運行,則調用makeDaemon函數將fork出一個進程作為daemon進程后台運行;
(6)通過讀取掛載目錄文件屬性來檢查掛載目錄是否可進行掛載;
(7)創建本次掛載session,生成session信息保存到元數據服務中;
(8)創建自定義的文件系統類型,通過用戶態fuse(用戶空間實現文件系統)庫來進行實現,啟動服務來接收fuse的請求信息並封裝成request,解析request找到對應的處理函數進行處理並返回。
啟動的server接收fuse請求大致流程圖:
4.元數據保存key含義解析
主要介紹在redis為元數據服務時,保存的各個key的含義,方便下面講解讀寫流程。
這里我們先列出redis中保存的所有key:
可以看到,大概分成好幾種key,有i開頭的,有d開頭的,有c開頭的,還有其它的一些固定字符串的key。
setting:保存的format信息的key,對應的結構體:
type Format struct { Name string UUID string Storage string Bucket string AccessKey string SecretKey string `json:",omitempty"` BlockSize int Compression string Shards int Partitions int Capacity uint64 Inodes uint64 EncryptKey string `json:",omitempty"` }
i1:表示記錄的inode號為1的文件的屬性信息,同理i2為inode號為2的文件屬性key,保存的值對應的結構體為:
// Attr represents attributes of a node. type Attr struct { Flags uint8 // reserved flags Typ uint8 // type of a node Mode uint16 // permission mode Uid uint32 // owner id Gid uint32 // group id of owner Atime int64 // last access time Mtime int64 // last modified time Ctime int64 // last change time for meta Atimensec uint32 // nanosecond part of atime Mtimensec uint32 // nanosecond part of mtime Ctimensec uint32 // nanosecond part of ctime Nlink uint32 // number of links (sub-directories or hardlinks) Length uint64 // length of regular file Rdev uint32 // device number Parent Ino // inode of parent, only for Directory Full bool // the attributes are completed or not KeepCache bool // whether to keep the cached page or not }
d1:當文件為目錄類型時,就會對應一個d開頭的key,1表示該文件夾是inode號為1的,它是一個hash類型的鍵,它的每個field key是文件名字,field key對應的值是文件類型和對應的inode號,field key的值保存着文件類型和文件inode號。這樣的話就可以方便的找到某個目錄下的某個文件名的inode號及該文件的類型,同理d3就是inode號為3的文件夾記錄着它文件夾下的文件元信息。
c14_0:c表示chunk的意思,我們先來看下這個數據結構圖
可以看到一個文件數據會被拆分成chunk來進行保存的,如果是大文件,那么會被按指定大小拆分成多個chunk,這個c14的14就表示這個chunk是歸屬於inode14這個文件的,c14_0的0則表示是第一個chunk,該key是列表類型的鍵,它的每個元素是一個slice,slice不是固定大小的,slice會根據block size大小拆分成多個block進行存儲,比如block size是4M,如果slice是6M,則拆分成2個block來進行存儲,計算出對應的兩個key把鍵值往對象存儲服務中存儲。比如生成35_0_16384和35_1_8192,35是表示這個slice的id(Slice結構體中的Chunkid字段),0和1表示兩個block,16384和8192分別表示block的大小,分別為4M和2M。
chunk的結構體:是slices列表的數據結構,[]meta.Slice
Slice結構體:
// Slice is a slice of a chunk. // Multiple slices could be combined together as a chunk. type Slice struct { Chunkid uint64 Size uint32 Off uint32 Len uint32 }
nextinode:它是一個自增的key,作用是分配新的inode;
nextchunk:它也是自增的,用來分配給slice的chunkid的;
nextsession:它也是自增的,用來給session結構體分配session id的。
totalInodes和usedSpace是記錄當前一共有多少個inode和當前空間已使用量的。
5.讀寫文件過程分析
5.1.創建文件夾
創建文件夾命令示例:mkdir /data/ussfs/testdir
流程圖:
流程說明:
(1)先獲取根文件屬性信息,然后通過文件路徑和文件名獲取指定文件屬性,獲取的方式就是一層層獲取,比如/a/b/c,先是在a目錄下找b,然后在b目錄下找c,這是通過調用doLookup函數來查詢獲取;
(2)如果要創建的文件夾不存在則進行創建,創建調用doMkdir函數進行創建,該函數又調用mknode函數來創建文件信息(文件夾也是一個文件);
(3)從元數據服務器中獲取一個新的inode號,如果是redis元數據服務,則是由一個自增的nextinode來保存當前分配的inode號;
(4)設置新文件的文件屬性信息,比如權限、創建時間等信息;
(5)向父目錄中添加該新文件,當redis為元數據服務時,則是向d開頭的key里比如d1里添加一條該新文件信息;
(6)更新父目錄文件屬性信息、新文件屬性信息和文件系統的一些總體使用信息。
創建文件跟創建文件夾類似,先調用的doCreate,然后也會調用mknode來生成inode和保存文件屬性信息。
5.2.寫入數據到文件
寫入文件命令示例:echo "123456789" > /data/ussfs/testdir/testfile
流程圖:
流程說明:
(1)跟先前類似,先通過文件路徑找到該文件,獲取該文件屬性信息,如果文件不存在先創建文件;
(2)調用doopen函數打開文件,主要是初始化文件handle,創建文件讀寫對象,返回文件描述符;
(3)調用dowrite,傳入偏移位置和寫入數據,通過偏移位置計算出要寫入到第幾個chunk中去,如果是跨chunk(一個chunk默認64M),則先寫入一部分數據到一個chunk,然后再寫剩下的數據到下一個chunk;
(4)在一個chunk中查找合適的slice進行寫入,比如改變的數據是在中間部分的,那其實只要更新那一個slice數據即可,其它slice可以不變更,我們目前這場景是找不到一個合適的slice,它會創建一個slice,然后通過該slice進行數據上傳;
(5)通過偏移量進行block的計算,每個block會生成對應的key,然后調用對象存儲的put方法進行key value的上傳來存儲數據;
(6)保存slice元信息到元數據服務中。
5.3.讀取文件數據
讀取文件命令示例:cat /data/ussfs/testdir/testfile
流程圖:
流程說明:
(1)跟先前類似,先通過文件路徑找到該文件,獲取該文件屬性信息,如果文件不存在先創建文件;
(2)調用doopen函數打開文件,主要是初始化文件handle,創建文件讀寫對象,返回文件描述符;
(3)分配存儲數據的page數據結構,從元數據服務獲取所有slice列表;
(4)遍歷每個slice,取出slice對應的所有block信息保存到page對象中;
(5)如果block有緩存則直接從緩沖中獲取,否則從對象存儲中重新獲取,並進行緩存。