背景
先简单介绍下MinIO吧,官方给的介绍是它是一种高性能、S3 兼容的对象存储。它专为大规模 AI/ML、数据湖和数据库工作负载而构建,并且它是由软件定义的存储。不需要购买任何专有硬件,就可以在云上和普通硬件上拥有分布式对象存储。
MinIO拥有开源 GNU AGPL v3 和商业企业许可证的双重许可。
翻译一下,就是好使,能白嫖,能私有化部署。
这里提到的S3,是亚马逊提供的对象存储服务,S3兼容性是云原生应用程序的硬性要求,像国内流行的阿里云对象存储(OSS),腾讯云对象存储(COS),都是兼容S3的。
之所以要搭建私有化的对象存储服务,主要考虑是2个,一是原始的文件存储方案并不安全,也不能实现高可用,而且在使用场景上有很多局限,比如某个资源想要增加访问权限,或者想定期删除,又或者想使访问权限定期关闭,那就要在代码层面增加很多逻辑,非常不友好。而使用MinIO的话,这些都会变得非常简单。
在一个考虑是因为MinIO的可扩展性非常强大,且提供了丰富的API接口,只要条件允许,我甚至很方便的可以自建一个无限大的云存储服务出来,配合CDN等边缘加速技术,可以高效支撑团队内几乎所有涉及文件存储和访问的场景。
真正建立属于自己团队的存储基础设施。
安装
MinIO的安装过程,官方文档(MinIO | S3 Compatible Storage for AI)上有非常详细的说明,我这里还是简单的镜像一下。
官方文档里,分别提供了Kubernetes,Docker,Linux,macOS,Windows,5种环境的安装方案,我使用过docker,Linux,windows这三种,这里就贴一下Linux的安装方案,还是在WSL2环境下。
- 下载文件
wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio_20240913202602.0.0_amd64.deb -O minio.deb
sudo dpkg -i minio.deb
- 启动服务
mkdir ~/minio
minio server ~/minio --console-address :9001
启动完成后,我们会看到这样的界面⬇️
其中,webui给出的链接是可以直接访问的
输入控制台给出的账号密码,就可以登录进去了。
注意,这里介绍的这是基于开发或者测试环境的快速部署方案,如果要在生产环境中来部署MinIO集群,那不是本文讨论的范围,感兴趣的可以参照官方文档的详细说明。
传送门👉:
MinIO Object Storage for Linux — MinIO Object Storage for Linux
对接
安装SDK
找一个测试项目或者新建个项目,安装上MinIO的sdk。
我这里还是用的上次的测试项目,项目文件长这样⬇️
<ItemGroup><PackageReference Include="Dapper" Version="2.1.35" /><PackageReference Include="DotNetCore.CAP" Version="8.2.0" /><PackageReference Include="DotNetCore.CAP.Dashboard" Version="8.2.0" /><PackageReference Include="DotNetCore.CAP.Kafka" Version="8.2.0" /><PackageReference Include="DotNetCore.CAP.PostgreSql" Version="8.2.0" /><PackageReference Include="Minio" Version="6.0.3" /></ItemGroup>
注入服务
引入sdk后,在配置文件中增加一个类似这样的配置,如果是测试阶段在代码中直接配置也行。
"MinioSettings": {"Endpoint": "{宿主机分配给wsl的ip}:9000","AccessKey": "在minio面板中获取","SecretKey": "在minio面板中获取","UseSSL": false
}
这里面用到的accessKey和secretKey,涉及到访问权限,如果你了解过AWS的IAM相关知识点,就一目了然了。
这里我们不讨论这个话题,直接去获取相关的配置信息
在这个页面去创建这样一个access key即可。
或者我们也可以到Identity栏目,配置用户信息,用户组,组权限等信息,也可以生成一个更加具体的access key。
补充完配置信息后,就可以去项目中注入服务了。
关键代码如下
var minioSetting = builder.Configuration.GetSection("MinioSettings").Get<MinioSettings>();
builder.Services.AddMinio(x =>
{x.WithEndpoint(minioSetting.Endpoint).WithCredentials(minioSetting.AccessKey, minioSetting.SecretKey).WithSSL(minioSetting.UseSSL).Build();
});
这里,因为我们是在测试阶段,SSL协议可以先关掉,降低一些心智负担。
编写接口
服务注入完成后,就可以来编写一个测试接口了。
这里为了方便,我直接创建了一个支持分片上传的接口。
[HttpPost]
public async Task<IActionResult> Upload(FilePartModel model)
{try{ if (model.file == null || model.file.Length <= 0)return BadRequest("No file found.");await CreateBucketIfNotExists(model.bucket);using (var stream = model.file.OpenReadStream()){await _minioClient.PutObjectAsync(new PutObjectArgs().WithBucket(model.bucket).WithObject(model.savedFileName).WithContentType(model.contentType).WithObjectSize(stream.Length).WithStreamData(stream));Console.ForegroundColor = ConsoleColor.Yellow;Console.WriteLine($"{model.savedFileName} is uploaded successfully");Console.ForegroundColor = ConsoleColor.White;}if (model.merged){var listArgs = new ListObjectsArgs().WithBucket(model.bucket).WithPrefix(model.uploadId).WithRecursive(true).WithVersions(false);using (var mergedStream = new MemoryStream()){await foreach (var obj in _minioClient.ListObjectsEnumAsync(listArgs).ConfigureAwait(false)){var partStream = await _minioClient.GetObjectAsync(new GetObjectArgs().WithBucket(model.bucket).WithObject(obj.Key).WithCallbackStream((stream) =>{stream.CopyTo(mergedStream);}));await _minioClient.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(model.bucket).WithObject(obj.Key));}// 将合并后的流写入到MinIOmergedStream.Seek(0, SeekOrigin.Begin);await _minioClient.PutObjectAsync(new PutObjectArgs().WithBucket(model.bucket).WithObject(model.savedFileName).WithContentType(model.contentType).WithObjectSize(mergedStream.Length).WithStreamData(mergedStream));}}var respData = new FileResponseDto(){fileIndex = model.chunkIndex,Completed = model.completed,path = "未完成",};if (model.completed){// 上传完成后返回一个临时的访问链接,有效期24小时PresignedGetObjectArgs args = new PresignedGetObjectArgs().WithBucket(model.bucket).WithObject(model.savedFileName).WithExpiry(60 * 60 * 24);string temporaryUrl = await _minioClient.PresignedGetObjectAsync(args); respData.path = temporaryUrl;} return Json(_resp.success(respData, "上传成功"));}catch (Exception ex){Console.WriteLine($"Failed to upload part {model.chunkIndex} to MinIO: {ex.Message}");}return Json(_resp.error("上传失败"));
}
简单说明一下,我这里因为是测试,所以分段代码和合并代码的逻辑写到了一起,实际情况中,可以引入一些其他的流程或方法来拆解这个接口。比如,上传分段文件是一个单独的接口,合并文件又是一个单独的接口,还可以引入缓存或者队列结构,当文件上传完成后,发布一个事件,来完成一些数据库的写入业务等等。
编写前端代码
关于大文件上传的前端代码,市面上有很多方案,也可以使用js的原生技术来实现,这里由于逻辑点较多,不在赘述前端部分的实现。
我个人也曾在2021年的博客中聊到过大文件的上传,传送门👉:一个支持断点续传的大文件分片上传的小模块_dotnet_为自己带盐_InfoQ写作社区
效果
这是上传小文件的效果
小文件
这是上传大文件的效果
大文件
再看下minio面板上的传输记录
总结
至此,在开发环境下,就完成了一个简单的,支持各种文件上传的功能。
在系统内部搭建一个基于MinIO的存储集群可以带来多个方面的优势,包括但不限于以下几点
- MinIO支持多租户架构,可以配置为高可用(HA)模式,意味着即便某个节点出现问题,系统仍可以继续运行而不丢失数据,这是传统的文件存储方式无法比拟的。
- MinIO设计用于高性能存储,支持对象存储协议如S3 API,可以提供高速的数据访问速度。对于需要快速读写大量数据的应用场景来说,这是一个重要的优点。
- MinIO支持水平扩展,可以通过增加更多的节点来扩展存储容量和吞吐量。
- 相比于使用第三方云服务提供商的服务,自己搭建MinIO集群可能会更经济实惠,尤其是当数据量非常大或者有特定的安全性和合规性要求时
- 自建存储集群可以更好地控制数据的位置和安全性,这对于需要遵守严格数据保护法律或行业标准的企业来说是一个关键因素
- 自己管理存储集群允许对环境进行深度定制,以满足特定的工作负载需求或者集成现有的工具和技术栈。
好了,基本就这样了。