GCS FUSE在Windows环境下的解决方案

武杰

2020 年 9 月 01 日

GCS FUSE在Windows环境下的解决方案

FUSE(Filesystem in Userspace)用户空间文件系统是一个面向类 Unix/Linux 操作系统的软件接口,它可以将用户空间程序创建的文件系统挂接到操作系统的内核中,从而使得其它程序可以无需重写 IO 代码,以近乎透明方式使用用户自定义的文件系统。基于这种易用性,FUSE 系统在业界有已经大量的系统实现,这些系统多用于跨网络、分布式、异构的环境。


现在各个云计算厂商的提供的都是对象存储(Object storage)系统,为了方便用户直接使用这些对象存储系统,业界产生了许多基于 FUSE 的系统,比如 GCP 的Cloud Storage FUSE


目前在云计算的各种操作系统的使用中,Windows 系统的操作系统的占比约在 25%-30%左右。但是大多数基于对象存储的 FUSE 系统都不能很好的支持 Windows 系统。比如 Cloud Storage FUSE 系统只支持 Linux 和 MacOS。本文主要讨论在 Windows 环境下如何构建自己的文件系统。


1.为什么要用 FUSE 系统?


为此,我们需要先了解一下 FUSE 系统的优点和缺点。


FUSE 系统的缺点:


● 性能开销比较大,由于程序运行在用户空间,并且受限于底层真正存储系统的性能,从而导致 FUSE 系统整体的性能开销比较大。尤其针对大量小文件的 IO 密集操作的场景,体现的尤为明显。


● 有一定的功能限制,和正常的 POSIX/NTFS 系统相比较,有些功能是难于实现的,比如文件内容的修改好追加操作,文件权限的控制等。需要特别指出的,基本对象存储的 FUSE 系统,文件一般都是只读模式。


FUSE 系统的优点:


● 用户几乎无需修改 IO 代码,可以直接操作对象存储。


● 可以实现分布式文件系统的功能,在不同操作系统的环境中共享文件。


● 成本较低,直接挂载对象存储的成本远低于使用 CIFS/NFS 的方式,因为 CIFS/NFS 的方式下,用户需要支付额外的费用。


我们在充分了解 FUSE 优缺点的基础上,就可以确定如何才能更好的使用 FUSE 系统。


2.FUSE on Windows


我比较了 Cygwin, Dokan, WinFsp 这三种 FUSE 的实现,最后选择了 WinFsp 做为开发的基础。主要原因是:WinFsp 项目经过几年的发展已经比较成熟,性能相对不错,项目也很活跃,而且我对 WinFsp 也已有发布的项目。WinFsp 是 NTFS 兼容的系统,可以将用户自定义的文件系统以 NTFS 的接口挂接到系统的内核中。


3. FUSE on GCS


在 Windows 系统中,FUSE 的兼容接口是 NTFS。NTFS 文件系统是比较复杂的系统,由于缺乏明晰的文档,WinFsp 各个 API 之间调用关系和传递的参数都需要在实际的开发过程不断调整。所幸的是 GCS 的一些技术特性简化了相关的开发工作。


项目使用 C#进行开发,目前,已经开发完成,项目地址:GcsFuse-Win,这个系统可以使得 Windows 的主机以本地文件系统的方式挂载 GCS 的资源。可以同时挂载整个 GCS 的多个 Bucket,也可以挂载单个 Bucket,也可以只挂载某个 Bucket 下特定 Prefix 的资源。




下面我们将探讨一下其中具体的关键技术细节:


3.1 文件的创建


系统创建文件时的 API 大致的调用顺序是 Create->Open->Read/Write … 。对应的在 GCS 上需要创首先建一个长度为零的 Object,随后系统的 Open 函数就会调用这个刚创建的 Object 进行后续的操作。在这个环节,由于 GCS 具有强一致性的特性,尤其是 Read-after-write 的一致性,可以确保 Open API 能够获取到之前的 Object。但是在在其它厂商的对象存储系统中,由于采用的是最终一致性,就无法保证 Open API 能够获取之前创建的 Object。


下面是创建一空 Object 的参考代码(C#):


public Google.Apis.Storage.v1.Data.Object CreateEmptyBlob(string bucketName, string blobName)         {            var content = Encoding.UTF8.GetBytes("");            string contentType = MimeMapping.GetMimeMapping(Path.GetFileName(blobName));            Google.Apis.Storage.v1.Data.Object blob = this.storage.UploadObject(bucketName, blobName, contentType, new MemoryStream(content));            return blob;        }

复制代码


3.2 子文件夹


GCS 和其他对象存储系统一样,都是基于 Key-Value 的扁平的命名方式。为了模拟文件系统中的层级关系,GCS 采用了非常巧妙的设计方式。在 GcsFuse-Win 中,通过创建一个名称为:“abc/”,大小为零的 Object,这样“abc/”就可以在系统中以文件夹“abc”的形式出现,注意这时“abc”文件夹下为空,没有任何文件。当在“abc”文件夹中添加一文件时,GCS 会将”abc/”这个 Object 自动删除。


下面是创建子文件夹的参考代码(C#):


 public Google.Apis.Storage.v1.Data.Object CreateSubDir(string bucketName, string blobName)        {            string subDir = blobName.EndsWith("/") ? blobName : blobName + "/";            Google.Apis.Storage.v1.Data.Object blob = CreateEmptyBlob(bucketName, subDir);            return blob;        }

复制代码


3.3 子文件夹和文件的遍历


GCS对如下一下操作也支持强一致性,比如:Read-after-deleteBucket listing,Object listing。这样可以确保系统能够遍历到最新的文件。另外对于子文件夹,如果子文件夹是空的,没有任何文件,那么遍历其父文件夹时是是可遍历到这个名如”abc/”的Object的。但是一旦该文件加下有了文件,那么在遍历其父文件夹时这个名如”abc/“将不再是Object Instance的类型,而是prefix类型。
复制代码


3.4 Read 操作


WinFsp会在多个场景下调用Read API的操作,对于一个文件的Read操作通常不是一次性完成的,而是需要调用多次,每次调用要通过计算offset和length来定位内容。但是由于Windows的内核设定, read-ahead的长度一般是8K,如果每次Read都调用一次GCS的分段下载的API的话,网络消耗会比较大。所以GcsFuse-Win在Read这个环节增加一个了Buffer,GcsFuse-Win会以根据用户设定的Buffer大小和文件的大小计算出一个最小的Buffer Size,然后根据这个Buffer Size来预先缓存数据。比如设定Buffer Size 是4MB, 文件的大小是1.2MB,则GcsFuse-Win在在系统调用Read操作是,将该文件全部下载并缓存,这样后续的Read将会直接读取缓存中的内容。通过Buffer机制可以极大的降低网络的请求数量。
复制代码


GCS 支持分段下载 Object 的数据,在请求的参数中加入”alt=media“表示下载 Object 的数据。


下面是分段下载 GCS Object 的参考代码(C#):


public byte[] DownloadByteRange(string bucketName, string blobName,            long firstByte, long lastByte)        {            byte[] content;            // Create an HTTP request for the media, for a limited byte range.            StorageService storage = this.storage.Service;            var uri = new Uri(                $"{storage.BaseUri}b/{bucketName}/o/{HttpUtility.UrlEncode(blobName, System.Text.Encoding.UTF8)}?alt=media");            var request = new HttpRequestMessage() { RequestUri = uri };            request.Headers.Range =                new System.Net.Http.Headers.RangeHeaderValue(firstByte,                lastByte);            var response = storage.HttpClient.SendAsync(request).Result;            if ((int)response.StatusCode == 200 | (int)response.StatusCode == 206)            {                content = response.Content.ReadAsByteArrayAsync().Result;                return content;            }            else            {                string errMsg = string.Format("Failed to download the blob:{0}" , bucketName + "/" + blobName);                throw new DownloadException(errMsg);            }                   }

复制代码


3.5 Write 操作


WinFsp 对文件内容的写入通常也是通过多次调用 Write 操作,和 Read 操作一样,在 Windows 内核中每次 Write 的长度一般是 128K,同样的如果每次 Write 操作都调用 GCS 的 Resumable upload 的 API 的话,网络消耗会比较大。在这个环节,GcsFuse-Win 同样实现了一个 Buffer 的功能,会等到 Buffer 充满之后,再以 Block 的方式统一上传至 GCS。这样可以提高性能,但同时也引入了一定的风险。


对应 Write 操作在 GCS 上的实现,可以有两种方式,一种是采用复合对象(composite-objects)的方式,可通过分别上传多个部分,最后通过 GCS 的 compose 函数整合成一个完整的对象,这个方式适用于 multipart upload 的场景。另一种是断点续传(resumable upload)的方式。GcsFuse-Win 采用的是断点续传的方式。


GCS 的断点续传操作需要分两步来进行:


第一步:开启断点续传的会话,在这个过程中,系统将会创建并返回一个 URI,后续的真实的数据上传将会根据这个 URI 进行。


参考代码如下(C#):


public Uri InitializeResumableUploader(string bucketName, string blobName)        {            StorageService storageService = this.storage.Service;            var uri = new Uri(                $"https://storage.googleapis.com/upload/storage/v1/b/{bucketName}/o?uploadType=resumable");            var initRequest = new HttpRequestMessage() { RequestUri = uri };            string body = "{\"name\":\"" + blobName + "\"}";            initRequest.Content = new StringContent(body, Encoding.UTF8, "application/json");            string contentType = MimeMapping.GetMimeMapping(Path.GetFileName(blobName));            initRequest.Headers.Add("X-Upload-Content-Type", contentType);            initRequest.Method = new HttpMethod("POST");            var initResponse = storageService.HttpClient.SendAsync(initRequest).Result;            Uri uploadUri = null;            if ((int)initResponse.StatusCode == 200)            {                uploadUri = initResponse.Headers.Location;            }            else            {                string errMsg = string.Format("Failed to initialize the upload session :{}" + bucketName + "/" + blobName);                throw new UploadException(errMsg);            }            return uploadUri;        }

复制代码


第二步:上传数据,需要注意的是,除去最后一个数据块之外上传数据块的大小必须是 256KB 的整数倍。每次上传成功后,系统会返回 308 的代码,当最后一个数据块上传成功后,系统会更加数据应有的大小和最后实际收到的数量进行对比,如果数量一致则会返回 200。


参考代码如下(C#):


public long UploadBlobChunk(Uri uploadUri, byte[] rawData, long byteSent, long length)         {            bool isEof = false;            StorageService storageService = this.storage.Service;            var uploadRequest = new HttpRequestMessage() { RequestUri = uploadUri };            uploadRequest.Method = new HttpMethod("PUT");            uploadRequest.Content = new ByteArrayContent(rawData);            if (length % (256 * 1024) != 0)            {                long totalLength = byteSent + length;                uploadRequest.Content.Headers.ContentRange = new System.Net.Http.Headers.ContentRangeHeaderValue(byteSent, byteSent + length - 1, totalLength);                isEof = true;            }            else {                uploadRequest.Content.Headers.ContentRange = new System.Net.Http.Headers.ContentRangeHeaderValue(byteSent, byteSent + length - 1);            }            var uploadResponse = storageService.HttpClient.SendAsync(uploadRequest).Result;            if (isEof)            {                if ((int)uploadResponse.StatusCode != 200)                {                    string errMsg = string.Format("Failed to the upload :{0}", uploadUri.ToString());                    throw new UploadException(errMsg);                }            }            else {                if ((int)uploadResponse.StatusCode != 308)                {                    string errMsg = string.Format("Failed to the upload :{0}", uploadUri.ToString());                    throw new UploadException(errMsg);                }            }            return length;        }

复制代码


最后


目前,业界在 Windows 系统使用 FUSE 功能还处于前期探索阶段,各种实现方法都有不同的优缺点。 GcsFuse-Win 的项目地址:https://github.com/weswu8/gcsfuse-win


目前代码存在许多不足和 Bug,要不断的优化和改进。用户需要根据自己是场景来确实最适合自己的方法。


参考资料:


https://zh.wikipedia.org/wiki/FUSE


https://github.com/libfuse/libfuse


https://cloud.google.com/storage/docs/gcs-fuse


https://github.com/billziss-gh/winfsp


http://www.secfs.net/winfsp/doc/Known-File-Systems/


https://cloud.google.com/storage/docs/composite-objects


https://cloud.google.com/storage/docs/resumable-uploads


2020 年 9 月 01 日 18:21314

评论

发布
暂无评论
发现更多内容

PHP实现一致性Hash算法

Arthur.Li

php 极客大学架构师训练营 一致性hash

架构师训练营 第5课作业

Glowry

极客大学架构师训练营

架构训练营第五周 - 作业

无心水

极客大学架构师训练营

区块链各行业应用案例

CECBC区块链专委会

产业落地 政策扶持 去中心化信任 防篡改不可逆 低廉高效

ARTS打卡 第6周

引花眠

ARTS 打卡计划

刚去面试现场聊了一个多小时的Redis ,悄悄分享给大家!

Java小咖秀

nosql redis Java 面试

架构师训练营第五章作业

饶军

为什么C++可以返回Vector局部变量

韩小非

c++ 内存泄露 函数调用 堆内存管理

【第五周】学习总结——缓存、消息队列、负载均衡

三尾鱼

极客大学架构师训练营

ARTS打卡-05

Geek_yansheng25

week5.课后作业

个人练习生niki

架构师训练营 第5课学习总结

Glowry

极客大学架构师训练营

它们为什么这么快:从多进程到多线程再到I/O复用

Ya

多线程 进程 并发

架构师训练营 - 第 4 周命题作业

红了哟

Week3:作业一

车小勺的男神

分布式缓存架构与负载均衡架构

负载均衡 极客大学架构师训练营 消息队列 分布式缓存 第五周

消息队列与异步架构||负载均衡架构

独孤魂

从Servlet到Spring Boot

双儿么么哒

Java Spring Boot

视读——沟通的艺术,看入人里,看出人外(开篇)

双儿么么哒

读书笔记 视觉笔记

第五周-作业2-学习总结

seng man

依赖倒置原则

John

极客大学架构师训练营

架构训练营第五周 - 总结

无心水

极客大学架构师训练营

iOS sonar实践

余志斐

ios sonar

读《看见》

YoungZY

操作系统概览

引花眠

计算机基础

架构师训练营第五周作业

CATTY

一致性Hash算法

架构师训练营 - 学习总结 第 5 周

水边

极客大学架构师训练营

Week3:作业二

车小勺的男神

谈谈Spring xml配置文件中的命名空间,以及一些例外情况

xiaoxi666

spring 命名空间

架构师训练营学习总结

John

极客大学架构师训练营

架构师训练营第五周总结

一剑

GCS FUSE在Windows环境下的解决方案-InfoQ