什么是minio
一个高性能的Web应用系统,需要可以很好的实现横向扩展,就需要把应用和计算节点做成无状态的形式,即节点本身不存储应用级别的状态和数据。但随之带来的问题就是现实的业务需求,又需要在节点间保持数据的一致性,就是对于应用而言,无论有多少个节点,都是一致透明的系统。我们熟知的关系型数据库可以实现这一点。但是关系数据库是基于结构化关系型数据设计,其实不适合于保存缓存或者临时共享数据这一类的应用场景。于是就出现了redis,通过使用内存存储数据并通过网络提供数据服务,它可以提供高性能的共享数据缓存。
但是,另一类非常常见的数据形式-文件,无论是redis还是数据库,好像都不太适合。因为一般情况而言,文件显然不是结构化数据,也不是简单的对象结构数据,抽象而看它们都是简单的二进制数据,但是其规模大小的差异会比较大,还有需要永久化存储和管理的需求。
传统的方式是使用标准化的共享文件系统,如NFS、SMB、SAN等等。但这些技术其实并不是特别适应于在大规模的分布式应用和网络中使用,其架构、管理和维持的成本比较高昂。分布式的Web应用,需要一种轻量快速的,跨平台、分布式、一致化的文件存储和传输机制。本文的主角-Minio,就是为这种使用场景设计的。
在minio之前,其实业界已经有了一个非常优秀的产品,就是Amazon的Simple Store Service(简单存储服务S3),它实际上开创了"HTTP网络数据块存储服务"这一云计算产品和软件品类,并确立了相关的技术、概念和操作标准。很多后来的云服务厂商都提供了类似的产品和服务。但S3虽好,也必须要使用Amazon提供的云计算服务,并不适合于所有的应用场景,比如需要自己存储数据或者在封闭网络中运行。所以开源软件社区也开发了类似和兼容的产品,比如minio,让开发者可以将其应用和部署到自己的应用环境当中,来更好的支撑自己的应用软件和业务需求。
minio的官网(min.io)如此定位自己的产品(笔者加了一些自己的描述):
MinIO is a high-performance, S3 compatible object store. It is built for
large scale AI/ML, data lake and database workloads. It runs on-prem and
on any cloud (public or private) and from the data center to the edge. MinIO
is software-defined and open source under GNU AGPL v3.
MinIO是一个高性能,兼容S3的对象存储服务软件。它在新的时代为人工智能和机器学习(AL/ML)、数据湖和数据库等场景而架构。它可以在从单机、简单网络到云计算的任何环境中运行。MinIO是开源软件,遵循GNU AGPL v3软件协议。
这里简单提一下,无论是S3还是minio,它们所说的对象存储的对象,是一个抽象的数据块(二进制数据),和如Redis或者MongoDB对象存储所说的“对象”,其实是一种有结构的数据(KV、Array或者JSON),是有差异的。不能混淆。在本文中就是确指文件就是数据块,而非结构数据。
作为一个对象存储服务,minio具有以下特点:
- 开源: MinIO是基于Apache License 2.0开源协议的对象存储系统,方便系统集成、应用和扩展
- S3兼容: 它的API设计借鉴了Amazon S3的设计,可以作为私有云上兼容S3的对象存储服务使用
- 轻量级: minio无需任何特别的硬件和要求,可以运行在廉价的硬件上,大幅度降低了部署和运维成本
- 高性能: 支持群集和多磁盘,minio可以提供很高的文件数据存取性能,并优化了大文件、小文件和流媒体的读写性能
- 云优化:minio是基于go语言开发的单体应用,并且原生支持Kubernetes,这些都方便在云计算环境中部署和运行
- 安全型: 内置加密组件,支持传输中加密和静态加密,保证数据安全
- 存储复制: 提供了多种模式的对象存储复制,以实现更好的负载均衡、备份和恢复等特性
- 多租户: 支持基于硬件的租户隔离
核心概念
前面已经谈到,我们可以简单的把minio理解成Amazon S3的开源版本。因此,它的一些主要流程和核心概念也是和S3是类似的,在深入之前,我们需要先了解一下。需要注意这些概念初始都是来自于S3,有些方面和minio可能有所差异。
- 网络协议
S3使用两种主要网络协议: HTTP和Torrent。其操作语义主要使用REST API方式。 但看起来minio现在并不支持torrent协议,所以它应该是主要使用HTTP协议来进行网络传输。
- Bucket(存储桶)
这是S3引入的一个重要概念,S3以存储桶作为一个逻辑存储和管理单位。原来一般的文件系统并没有类似的概念(好像Windows的盘符也不是,因为桶实际上是一个逻辑概念,盘符更多是物理标识)。它可以理解成为为分布式云计算环境设计的一种对象的组织和管理方式,在一个存储桶中,可以进行业务的管理和组织,比如配置存储策略,访问控制管理等等。
- Object(对象)
对象就是上传并存储在S3系统中的数据,包括元数据和数据体。其数据体底层应该就是一个二进制数据块,就是数据的实际内容。元数据是该数据体相关的辅助信息,包括名词、版本、日期等,和普通文件系统是类似的。
- Key(键)
用于标识对象,S3中的对象由桶名+对象键唯一定位,类似文件路径,其实其形式也是路径-文件的形式。
- Region(地区)
S3是一个全球服务,所以它提供了物理区域的选择,可以帮助在就近区域快速存取数据。minio好像没有这一部分。
- ACL(访问控制列表)
可以定义对象访问的权限和方式。类似文件系统的访问控制。
- Storage Class(存储类别)
S3对象的存储类型,如标准(默认)、低频访问、归档等。存储类别决定对象的可用性、耐久性、费用。不确定minio
- Lifecycle(生命周期):
可以用于控制和自动转换对象的存储类别或过期删除对象的规则。
- Versioning(版本控制)
S3可以提供版本控制的功能。开启后可以保留对象的多个变更版本。特别适合于需要进行内容历史信息追溯的业务场合。
部署和安装
minio的一个很大的优点就是提供了非常灵活的部署方式,对于开发测试和简单应用,它可以支持单机和单机多磁盘的部署;对于大型应用,它可以支持多机多磁盘分布式部署,可以提供很大的数据存储规模和很高的数据存取性能,并同时了高性能和高可用性。
我们先以单机多磁盘部署为例。笔者有一台Debian Linux虚拟主机,为了做这个测试,增加了两块虚拟的40G虚拟磁盘。先进行磁盘的准备,将这两个磁盘制作为xfs文件系统,并将其挂载到指定位置:
$ mkfs.xfs /dev/sdb -L MIODISK1
$ mkfs.xfs /dev/sdc -L MIODISK2
$ nano /etc/fstab
#
LABEL=MIODISK1 /mnt/minio1 xfs defaults,noatime 0 2
LABEL=MIODISK2 /mnt/minio2 xfs defaults,noatime 0 2
// 检查挂载和文件系统
$ lsblk
准备好磁盘后,就可以开始部署服务程序了。最简单的方式,就是作为单体可执行文件,直接下载部署操作系统对应版本的二进程程序文件:
// 下载程序包
wget https://dl.min.io/server/minio/release/linux-amd64/minio
// 修改执行权限
chmod +x minio
// 启动程序
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password \n
./minio server /mnt/minio{1..2}/ \n
--address:"9000" --console-address ":9001" &
// 检查启动启动状态和端口
netstat -lpnt
ps aux | grep minio
要点如下:
- mioio建议使用4块或以上的独立磁盘来提供存储,这里简单起见,只使用两块
- 可以无需配置文件,使用命令行参数启动程序和服务
- 设置用户和密码环境变量,这个会在console web中使用
- minio server 为启动模式,作为服务器启动
- 可以指定侦听地址和控制台地址
- 检查防火墙是否开放服务端口
要将minio作为标准的服务程序启动,需要编写对应的服务程序脚本,这里主要讨论应用方面的问题,不详细涉及。
minio客户端
一般在minio服务启动时,会配置一个控制台的侦听端口,在这个端口上,就可以使用内置的Web管理界面(如图)。
在其中可以完成很多常规的系统管理和操作。
WebUI是给人员管理使用的,但我们通常不会把minio当作一个简单的文件服务和管理系统来使用,更看重其作为一个对象和文件存储服务,提供给应用程序来实现更高的价值。
对于远程管理或者程序化管理的需求,和S3一样,minio io提供了命令行客户端工具:mc,可以对minio服务的内容进行操作和管理。理论上而言,管理员可以完全不使用Web Console,就可以完成绝大部分的minio服务管理工作。 下载mc工具后,在正式使用前,一般进行一些简单的配置,如配置一个服务别名。
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
// 创建一个别名
./mc alias set ulmio http://MINIO-SERVER MYUSER MYPASSWORD
// 创建一个存储桶
./mc mb ulmio/uldata
// 复制文件到别名/桶/文件夹
./mc cp *.tar.gz ulmio/uldata/swbackup
// 列表文件夹
./mc ls ulmio/uldata/swbackup
// 下载文件
./mc cp ulmio/uldata/swbackup/mc.tar.gz .
// 复制文件同时改名
./mc cp mc.tar.gz ulmio/uldata/swbackup/mc2.tar.gz
// 删除文件
./mc rm ulmio/uldata/swbackup/mc2.tar.gz
要点和说明如下:
- 用户和密码可以在WebUI中创建
- 端点别名是保存在用户配置信息中的
- 复制之前需要创建存储桶
- 上传文件可以同时创建文件夹
mc的功能其实非常强大,但这不是本文讨论的主题,这里只是简要提及。
Nodejs集成
本文主要讨论的内容其实是,如何在一个nodejs应用程序中,使用minio进行文件管理相关的操作。比如用户可以通过nodejs程序的接口,上传一个文件,将其存储在minio系统当中,这部分的内容就涉及到minio和Nodejs程序的集成。这里这个nodejs程序,是作为minio客户端来进行文件存取的。
访问和安全密钥
作为一个最佳的安全实践,minio并不建议直接使用用户名和密码来访问minio,而是使用access key和secret key。用户可以使用WebUI创建一套密钥配置,然后配置到程序中。 这些配置应该相当于当前用户的“代理”,使用和用户相同的授权和访问控制设置。
nodejs minio程序包
在nodejs程序中,需要安装minio的npm包,包名就是minio。
npm i minio
连接配置信息
nodejs程序中,使用的minio客户端连接的配置信息内容如下:
const
fs = require("fs"),
Minio = require('minio');
mioConfig = {
endPoint: '192.168.9.1', // 端点和服务器地址和端口
port: 9000,
useSSL: false, // 内网连接可以不使用ssl,否则需要配置证书
accessKey: 'LJ5CbpmVP...', // 用户创建的访问和安全密钥
secretKey: 'lvXz9c154...'
};
// 然后可以使用配置信息创建客户端实例和连接服务器
const minioClient = new Minio.Client(mioConfig);
上传文件对象
如果有一个文件,在nodejs中上传文件到minio的参考代码如下:
const upload = async (fname, fid)=>{
let realFile = __dirname + "/" + fname;
let fData = fs.readFileSync(realFile);
const uresult = await minioClient
.putObject(BK_UPLOAD, fid, fData)
.catch((e) => {
console.log("Error while creating object from file data: ", e);
throw e;
});
console.log("File data submitted successfully: ", uresult);
};
这里的要点如下:
- 如果是小文件,可以直接使用putObject方法
文件下载
从minio下载文件的参考代码如下:
const download = async(fid, fname)=> {
// read object in chunks and store it as a file
const fileStream = fs.createWriteStream( __dirname + "/" + fname);
const fobject = await minioClient.getObject(BK_UPLOAD, fid);
fobject
.on("data", chunk => fileStream.write(chunk))
.on("end", () => console.log(`Reading ${fid} finished`));
};
这里的要点如下:
- 下载使用getObject方法,输入参数是fileKey,就是存储桶中的文件路径
- 对于大文件下载,可以考虑使用写入流来处理数据
- 可选保存时修改文件名词(在写入文件流设置)
上面就是nodejs中minio的基本操作,这里的操作都使用了文件,下面我们来研究以下,如何不使用文件,而直接使用二进制数据块来处理数据,这个方法可以在http服务中使用,并且不使用文件系统,并使用数据流来提高处理的效率。
和HTTP服务的集成
前面讨论的是使用minio npm 对文件进行的处理,在Web应用中,显然希望直接处理数据,而不使用中间文件。
基本构想
经过摸索和尝试,笔者提出的处理方案构想如下:
上传文件:
1 客户端在浏览器中,将图片文件编码为base64文本,并结合元数据作为JSON对象提交到HTTP POST文件上传接口
2 文件上传接口获取请求对象,将图片内容解码为buffer,并使用元数据生成文件key
3 服务端调用minio putObject方法,使用key作为路径,将buffer传输到minio服务中(使用配置好的存储桶)
下载文件:
1 客户端使用GET请求,地址中包含相关的元数据参数
2 服务端解析对象参数,计算其在minio服务中的路径
3 使用文件路径和getObject方法,获取其readStream的实例
4 使用此实例,配合HTTP响应对象,在读取数据时写入HTTP响应
5 完成响应内容
这个方案考虑到上传需要设置相关信息,并且通常读写信息是不对称的,就没有使用信息流实现上传;而使用数据流来完成数据的下载。
上传核心代码
在文件上传使用的核心代码如下,笔者使用fastify框架。
// put to mio
const mioPutImage = async (fData, fid)=>{
const uresult = await MIO_CLIENT
.putObject(BUNKECT, fid, fData)
.catch((e) => {
console.log("Error while creating object from file data: ", e);
return e;
});
console.log("File data submitted successfully: ", uresult);
return { R: 200 };
};
// 调用方式
// upload a file (base64) to minio
idhash = MIO_PATH_IMAGE + MIO_PREFIX + "imagehash";
rr = await mioPutImage( Buffer.from(qparam.imgContent,"base64"), idhash);
rep.send(rr);
// 客户端获取文件base64编码
const fileInput = document.getElementById('file');
// 监听文件选择
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
// 创建文件读取器
const reader = new FileReader();
// 文件读取成功完成时的回调
reader.onload = () => {
// reader.result 即为文件内容的base64编码
console.log(reader.result);
};
// 以base64格式读取文件
reader.readAsDataURL(file);
});
要点分析:
- 客户端使用FileReader,调用readAsDataURL,并重写onload方法就可以获取编码后的文件内容
- base64内容可以直接作为json对象内容提交给服务器
- 服务端的mimio客户端实例,可以直接传输base64字符串解码后的buffer
- 需要根据业务情况设置文件key和处理的结果
下载核心代码和解析
通过http接口下载文件内容的核心代码如下:
要点分析:
- 下载可以直接使用一个带参数的URL地址和get方法
// get object as readable stream
const mioGetObjStream = async( fid, cb)=>{
MIO_CLIENT
.statObject(BK_ZSB,fid)
.then( stat => stat.size)
.then( size => MIO_CLIENT.getObject(BK_ZSB,fid))
.then( stream => cb(stream))
.catch(err=>cb(null, err));
}
// 调用方法
let idhash = MIO_PATH_IMAGE + "I01-X01";
// let rstream = new Stream.Readable({
// read(size){},
// autoDestroy: true
// });
rep.type("image/png").hijack();
mioGetObjStream(idhash, (stream,err)=>{
if (err) return rep.send({R: 500});
stream
// .on("open", ()=>rep.type("image/png").hijack())
.on("data", chunk=> rep.raw.write(chunk))
.on("end", ()=>rep.raw.end())
});
要点分析:
- 先封装了一个标准getObject方法,这个方法会使用一个回调方法,向外部调用方暴露一个stream对象(minii getObject方法的结果是一个readStream对象),调用方通过重写stream对象的on方法完成业务需求
- rep是fastify的response对象,注意在重写onData方法前,需要先使用hijack方法,告知系统接管响应处理
- 获取stream对象后,可以结合rep来重写数据响应,但不能直接用rep,而是用rep.raw原生方法
- onData回调时,会得到一个data Chunk,使用这个数据作为参数调用rep.raw.write方法将数据块写入响应
- 最后结束时,关闭rep.raw
- 奇怪的是hijack方法只能在外部获取写数据流前执行,理论上在onOpen方法中执行最为合理
- 还有手动创建Readable对象的方式,但不如当前版本简洁
- 感谢claudi提高了此处理实现方案的思路
使用nodejs客户端
如果不在浏览器环境,直接使用nodejs客户端访问HTTP文件上传接口(比如测试或者数据维护的场景),可以参考以下代码:
const
minioTest = async ()=> {
const imgContent = fs.readFileSync( __dirname +'/1.png', { encoding: 'base64'});
let qr = await fetch("http://192.168.9.134:7101/mimage/upload",{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
fid: "I01-005",
imgContent
})
});
console.log("Upload Result:", qr);
}
nodejs环境直接了fs对象和读取方法,并直接得到base64内容。并可以作为body的组成调用fetch方法来完成上传。