MinIO介绍部分
MinIO简介
MinIO 是一款高性能、分布式的对象存储系统,说白了就是一个文件服务器,替代以前的FastDFS。MinIO提供高性能、S3兼容的对象存储。Minio 是一个基于Go语言的对象存储服务。它实现了大部分亚马逊S3云存储服务接口,可以看做是是S3的开源版本,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
在中国:阿里巴巴、腾讯、百度、中国联通、华为、中国移动等等多家企业也都在使用MinIO产品。
官网:min.io
中文:www.minio.org.cn/,docs.minio.org.cn/docs/
MinIO各个版本对比
这个我从网上查询到的功能价格对比图,不保证一定准确。
开源版本是基于GNU AGPL v3协议开源的。这里需要了解下AGPL开源许可,在企业环境中使用 Minio,需要特别关注 AGPL 的要求,以避免因许可证合规性问题引发法律风险。为了避免自己开发的代码受到这个AGPL的“传染性”风险。需要注意:
- 不要修改其源码,最好就是直接使用其官方发行版本。
- 将 AGPL 软件部署为一个独立的服务,与私有代码保持隔离(通过网络通信交互,而不是在同一进程或项目中运行)。
- 避免提供基于 AGPL 软件的(SaaS)网络服务
MinIO特点
- 数据保护——分布式Minio采用 纠删码来防范多个节点宕机和位衰减bit rot。分布式Minio至少需要4个硬盘,使用分布式Minio自动引入了纠删码功能。
- 高可用——单机Minio服务存在单点故障,相反,如果是一个有N块硬盘的分布式Minio,只要有N/2硬盘在线,你的数据就是安全的。不过你需要至少有N/2+1个硬盘来创建新的对象。
例如,一个16节点的Minio集群,每个节点16块硬盘,就算8台服務器宕机,这个集群仍然是可读的,不过你需要9台服務器才能写数据。
MinIO 基础概念
-
S3——Simple Storage Service,简单存储服务,这个概念是Amazon在2006年推出的,对象存储就是从那个时候诞生的。S3提供了一个简单Web服务接口,可用于随时在Web上的任何位置存储和检索任何数量的数据。
-
Object——存储到 Minio 的基本对象,如文件、字节流,Anything...
-
Bucket——用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。
-
Drive——部署 Minio 时设置的磁盘,Minio 中所有的对象数据都会存储在 Drive 里。
-
Set——一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的 Drive 分布在不同位置。
- 一个对象存储在一个Set上
- 一个集群划分为多个Set
- 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出
- 一个SET中的Drive尽可能分布在不同的节点上
MinIO在Centos上的安装步骤
1. 创建单独的目录放置MinIO
# 单独放置MinIO的所有内容
mkdir minio
2. 下载MinIO
# 进入minio目录
cd minio
# 下载MinIO
wget https://dl.min.io/server/minio/release/linux-amd64/minio
3.创建脚本需要的目录和文件
| 目录名 | 作用说明 |
|---|---|
| data | 存实际文件的目录 |
| log | minio日志目录 |
| start.sh | 启动脚本 |
| stop.sh | 停止脚本 |
4.启动脚本
#!/bin/bash
MINIO_DIR='/root/minio'
DATA_DIR=$MINIO_DIR'/data'
LOG_DIR=$MINIO_DIR'/log/minio.log'
MINIO_ACCESS_KEY='admin'
MINIO_SECRET_KEY='admin123'
SERVER_PORT='9000'
CONSOLE_PORT='9001'
PID_FILE=$MINIO_DIR'/minio.pid'
# 检查Minio目录是否存在
if [ ! -d "$MINIO_DIR" ]; then
echo "Minio directory does not exist: $MINIO_DIR"
exit 1
fi
# 检查日志目录是否存在
LOG_DIR_PATH=$(dirname "$LOG_DIR")
if [ ! -d "$LOG_DIR_PATH" ]; then
mkdir -p "$LOG_DIR_PATH"
fi
# 检查Minio是否已经在运行
if [ -f "$PID_FILE" ]; then
echo "Minio is already running with PID $(cat $PID_FILE)"
exit 1
fi
export MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY
export MINIO_SECRET_KEY=$MINIO_SECRET_KEY
# 启动Minio
nohup $MINIO_DIR/minio server --address 0.0.0.0:$SERVER_PORT --console-address 0.0.0.0:$CONSOLE_PORT $DATA_DIR > $LOG_DIR 2>&1 &
echo $! > "$PID_FILE"
echo "Start Success!"
5.停止脚本
#!/bin/bash
MINIO_DIR='/root/minio'
PID_FILE=$MINIO_DIR'/minio.pid'
# 检查PID文件是否存在
if [ ! -f "$PID_FILE" ]; then
echo "Minio is not running or PID file not found."
exit 1
fi
# 获取PID并终止进程
PID=$(cat "$PID_FILE")
if [ -z "$PID" ]; then
echo "PID file is empty, unable to stop Minio."
exit 1
fi
# 确保进程存在
if ps -p $PID > /dev/null; then
kill $PID
if [ $? -eq 0 ]; then
echo "Minio stopped successfully."
rm -f "$PID_FILE" # 删除PID文件
else
echo "Failed to stop Minio."
fi
else
echo "No Minio process found with PID $PID."
rm -f "$PID_FILE" # 删除无效的PID文件
exit 1
fi
Springboot集成部分
Minio SDK方式
MinIO 官方提供了一个 Java SDK,可以方便地在 Spring Boot 应用中集成 MinIO。
添加依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.14</version>
</dependency>
该版本是截止发文时的最新版本。
minio相关配置
# Minio配置
minio:
url: http://localhost:9000 # Minio 服务的 URL
accessKey: admin # Minio 的 Access Key
secretKey: admin123 # Minio 的 Secret Key
bucketName: my-bucket # Minio 的存储桶名称
在application.yml文件中加入的内容
bucket的名称,其实就是区分项目的标识。针对一个项目来说就是对应一个bucket,可以理解成该项目对应的所有资源文件的存储目录。所以在项目的全局文件中进行配置。
属性类MinioProperties
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String url;
private String accessKey;
private String secretKey;
private String bucketName;
}
作用就是承载minio的相关配置。这里需要注意的是需要在springboot的启动类上加上
@EnableConfigurationProperties注解,启用对配置属性的支持
配置类MinioConfig
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MinioConfig {
private final MinioProperties minioProperties;
@Bean
public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder()
.endpoint(minioProperties.getUrl())
.credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
.build();
// 检查存储桶是否存在,如果不存在则创建
try {
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(minioProperties.getBucketName()).build());
if (!isExist) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(minioProperties.getBucketName()).build());
}
} catch (Exception e) {
log.error("初始化 minio 失败", e);
}
return minioClient;
}
}
该配置类的主要作用就是在springboot启动时,容器中就有MinioClient这个我们可以操作minio的客户端工具类了。
测试Controller类
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/minio")
public class MinioController {
private final MinioClient minioClient;
private final MinioProperties minioProperties;
/**
* 上传文件
*/
@RequestMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
try {
PutObjectArgs objectArgs = PutObjectArgs.builder()
.stream(file.getInputStream(), file.getSize(), -1)
.bucket(minioProperties.getBucketName())
.object("ccc/" + file.getOriginalFilename())
.contentType(file.getContentType())
.build();
minioClient.putObject(objectArgs);
return "上传成功";
}catch (Exception e) {
log.error("上传文件失败", e);
return "上传失败";
}
}
/**
* 下载文件
*/
@RequestMapping("/download")
public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
try (GetObjectResponse objectResponse = minioClient.getObject(
GetObjectArgs.builder()
.bucket(minioProperties.getBucketName())
.object(fileName)
.build())) {
// 设置响应头
response.setContentType(objectResponse.headers().get("Content-Type"));
response.setHeader("Content-Disposition",
"attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
// 写入响应流
IoUtil.copy(objectResponse, response.getOutputStream());
response.getOutputStream().flush();
} catch (Exception e) {
log.error("下载文件失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 获取文件访问的完整URL
*/
@RequestMapping("/fileUrl")
public String getFileUrl(@RequestParam("fileName") String fileName) {
try {
String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(minioProperties.getBucketName())
.object(fileName)
.method(Method.GET)
.build());
log.info("文件访问URL: {}", url);
return url;
} catch (Exception e) {
log.error("获取文件URL失败", e);
return "获取文件URL失败";
}
}
/**
* 删除文件
*/
@RequestMapping("/delete")
public String deleteFile(@RequestParam("fileName") String fileName) {
try {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioProperties.getBucketName())
.object(fileName)
.build());
log.info("文件 {} 删除成功", fileName);
return "删除成功";
} catch (Exception e) {
log.error("删除文件失败", e);
return "删除失败";
}
}
}
说明:
-
上传方法中的文件名尽量用全局唯一的名字。比如:uuid,雪花算法等,我例子中是用原文件名,仅用于演示。这么名字相同,后面的文件会覆盖之前上传的。
-
上传方法中的
.object可以设置在bucket中的路径。其实就是子目录的作用,层级可以非常深。 -
上传方法中的
.contentType尽量设置,否则默认都是'application/octet-stream'。无法进行预览。 -
上传的文件就会保存在minio启动脚本中指定的
DATA_DIR路径中。 -
获取文件访问url方法,会返回一个有时效的链接。比如:http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20241224%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241224T060814Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=1662ac35e91d65ddacc4c01cf751c9d99aaa5257d36606ac1183e425c2537725。解释下这个url:
-
http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg:
http://localhost:9000: 这是MinIO服务器的地址和端口。/my-bucket/: 这是存储对象的桶(bucket)的名称。%E9%AD%94%E6%96%B9%E5%9B%BE.jpg: 这是存储在桶中的对象名称,它已经进行了URL编码,解码后为“魔方图.jpg”【我最近没事就对对7阶的魔方】。
-
X-Amz-Algorithm=AWS4-HMAC-SHA256:
- 这是用于生成签名的算法,这里使用的是AWS版本4的HMAC-SHA256算法。
-
X-Amz-Credential=admin%2F20241224%2Fus-east-1%2Fs3%2Faws4_request:
- 这是用于签名的凭证信息,包括用户名、日期、区域和请求类型。这里的
admin是用户名,20241224是日期,us-east-1是假设的区域(MinIO服务不验证区域),s3/aws4_request指示这是一个S3请求。 %2F是URL编码中的斜杠字符/。
- 这是用于签名的凭证信息,包括用户名、日期、区域和请求类型。这里的
-
X-Amz-Date=20241224T060814Z:
- 这是请求的时间戳,采用ISO 8601格式,并且是UTC时间。
-
X-Amz-Expires=604800:
- 这是链接的有效期限,单位为秒。在这个例子中,
604800秒等于7天。
- 这是链接的有效期限,单位为秒。在这个例子中,
-
X-Amz-SignedHeaders=host:
- 这是请求中已经签名的HTTP头部。在这个例子中,只有
host头部被签名。
- 这是请求中已经签名的HTTP头部。在这个例子中,只有
-
X-Amz-Signature=1662ac35e91d65ddacc4c01cf751c9d99aaa5257d36606ac1183e425c2537725:
- 这是根据上述参数生成的签名值,用于验证请求的合法性。
-
-
如果bucket是公有属性的话,其实可以通过**http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg**进行直接访问。
AWS SDK 方式
Amazon S3 的 API 规范是由 亚马逊公司(Amazon.com)制定的。具体来说,它是由亚马逊的云计算部门 Amazon Web Services(AWS)设计和发布的,AWS 提供了一整套云计算服务,其中包括存储服务 S3(Simple Storage Service)。之所以用该方式实现,主要是出于对未来对象存储服务可能不用MinIO的考虑。由于S3是一套API规范,目前好多云服务商的OSS服务都是支持S3规范的,比如:阿里云、七牛云、腾讯云、百度云等等。
添加依赖
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.29.39</version>
</dependency>
该版本是截止发文时的最新版本。
minio相关配置
# Minio配置
minio:
url: http://localhost:9000 # Minio 服务的 URL
accessKey: admin # Minio 的 Access Key
secretKey: admin123 # Minio 的 Secret Key
bucketName: my-bucket # Minio 的存储桶名称
同上面的Minio SDK方式中的配置内容
属性配置类S3Properties
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class S3Properties {
private String url;
private String accessKey;
private String secretKey;
private String bucketName;
}
同上面的Minio SDK方式中的类似
配置类S3Config
@Configuration
@RequiredArgsConstructor
public class S3Config {
private final S3Properties s3Properties;
@Bean
public S3Client s3Client() {
// 使用AWS SDK来创建S3客户端,用于连接到 Minio
return S3Client.builder()
// 设置 Minio 的端点
.endpointOverride(URI.create(s3Properties.getUrl()))
// Minio 本身不关心区域,可以选择任意区域
.region(Region.US_EAST_1)
.credentialsProvider(() ->
AwsBasicCredentials.create(s3Properties.getAccessKey()
, s3Properties.getSecretKey()))
.build();
}
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.endpointOverride(URI.create(s3Properties.getUrl()))
.region(Region.US_EAST_1) // 区域
.credentialsProvider(() ->
AwsBasicCredentials.create(s3Properties.getAccessKey()
, s3Properties.getSecretKey()))
.build();
}
}
这个配置类不但可以使我们拿到S3的客户端。还可以获得S3的Presigner,S3Presigner的作用就是生成预签名的 URL。这点比上面的Minio SDK稍微繁琐一点。
测试Controller类
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/minio")
public class S3Controller {
private final S3Client s3Client;
private final S3Presigner s3Presigner;
private final S3Properties s3Properties;
/**
* 上传文件
*/
@RequestMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
// 创建上传请求
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(s3Properties.getBucketName())
.key("test/" + file.getOriginalFilename())
.contentType(file.getContentType())
.build();
// 上传文件到 Minio
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
return "上传成功";
} catch (Exception e) {
log.error("上传文件失败", e);
return "上传失败";
}
}
/**
* 下载文件
*/
@RequestMapping("/download")
public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
try {
// 创建 GetObjectRequest 用于获取对象
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(s3Properties.getBucketName())
.key(fileName)
.build();
ResponseInputStream<GetObjectResponse> object = s3Client.getObject(getObjectRequest);
// 设置响应头
response.setContentType(object.response().contentType());
response.setHeader("Content-Disposition",
"attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
// 写入响应流
IoUtil.copy(object, response.getOutputStream());
response.getOutputStream().flush();
} catch (Exception e) {
log.error("下载文件失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 获取文件访问的完整URL (AS3接口方式)
*/
@RequestMapping("/fileUrl")
public String getFileUrl(@RequestParam("fileName") String fileName) {
try {
// // 直接用 s3Client 构造 URL 。适用于public bucket
// URL url = s3Client.utilities().getUrl(builder -> builder.bucket(s3Properties.getBucketName()).key(fileName));
// return url.toString();
// 生成预签名 URL
PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(builder ->
builder.getObjectRequest(getBuilder ->
getBuilder.bucket(s3Properties.getBucketName()).key(fileName))
// 签名有效期为1个小时
.signatureDuration(Duration.ofHours(1))
);
return presignedRequest.url().toString();
} catch (Exception e) {
return "获取文件URL失败:" + e.getMessage();
}
}
/**
* 删除文件
*/
@RequestMapping("/delete")
public String deleteFile(@RequestParam("fileName") String fileName) {
try {
// 调用 S3Client 的 deleteObject 方法删除文件
s3Client.deleteObject(builder ->
builder.bucket(s3Properties.getBucketName()).key(fileName)
);
log.info("文件 {} 删除成功", fileName);
return "删除成功";
} catch (Exception e) {
log.error("删除文件失败", e);
return "删除失败:" + e.getMessage();
}
}
}
功能和上面的Minio SDK方式一样,只不过通过用S3再实现一遍。这里
getFileUrl注释掉的获得url方式是针对bucket是public的。如果真有应用场景,可以试试。你会发现生成的url短很多。
总结
-
MinIO可以作为我们项目的文件服务器。但是受到开源协议影响,别动它代码;别深度集成。避免“传染性”。
-
如果你明确OSS服务就用MinIO的话,那么用MinIO 提供的SDK方式继承到我们的springboot项目比较方便;
但是如果有可能换其他的云存储服务,那么用S3方式不失为一个好的选择。
-
MinIO上传的文件就会保存在启动脚本指定的目录下。
-
在某些场景下可以把bucket设置为public的,里面的图片url就固定为:http://localhost:9000/my-bucket/ccc/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg。