一、Minio简介
[TOC]
博客园:字母哥:MinIO很强-让我放弃FastDFS拥抱MinIO的8个理由
简书:黄宝玲_1003:Minio 文件服务(1)—— Minio部署使用及存储机制分析
Minio 文件服务(2)—— Minio用Nginx做负载均衡
CSDN:Spring Boot + 对象存储服务MinIO API + Vue 实现图片上传以及展示
MinIO官网地址:docs.min.io/cn/
一、Minio简介
Minio 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
Minio是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。
二、Minio优势
2.1 UI界面
fastDFS默认是不带UI界面的,看看MinIO的界面吧。这个界面不需要你单独的部署,和服务端一并安装。
2.2 性能
MinIO号称是世界上速度最快的对象存储服务器。在标准硬件上,对象存储的读/写速度最高可以达到183 GB/s和171 GB/s。关于fastDFS我曾经单线程测试写了20万个文件,总共200G,大约用时10个小时。总体上是很难达到MinIO“号称的”以G为单位的每秒读写速度。
2.3 容器化支持
MinIO提供了与k8s、etcd、docker等容器化技术深度集成方案,可以说就是为了云环境而生的。这点是FastDFS不具备的。
2.4 丰富的SDK支持
fastDFS目前提供了 C 和 Java SDK ,以及 PHP 扩展 SDK。下图是MinIO提供的SDK支持,MinIO几乎提供了所有主流开发语言的SDK以及文档。
三、存储机制
Minio使用纠删码erasure code和校验和checksum来保护数据免受硬件故障和无声数据损坏。 即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。
3.1 纠删码
纠删码是一种恢复丢失和损坏数据的数学算法,目前,纠删码技术在分布式存储系统中的应用主要有三类,阵列纠删码(Array Code: RAID5、RAID6等)、RS(Reed-Solomon)里德-所罗门类纠删码和LDPC(LowDensity Parity Check Code)低密度奇偶校验纠删码。Erasure Code是一种编码技术,它可以将n份原始数据,增加m份数据,并能通过n+m份中的任意n份数据,还原为原始数据。即如果有任意小于等于m份的数据失效,仍然能通过剩下的数据还原出来。
Minio采用Reed-Solomon code将对象拆分成N/2数据和N/2 奇偶校验块。因此下面主要讲解RS类纠删码。
3.2 RS code编码数据恢复原理:
RS编码以word为编码和解码单位,大的数据块拆分到字长为w(取值一般为8或者16位)的word,然后对word进行编解码。 数据块的编码原理与word编码原理相同,后文中以word为例说明,变量Di, Ci将代表一个word。
把输入数据视为向量D=(D1,D2,…, Dn), 编码后数据视为向量(D1, D2,…, Dn, C1, C2,…, Cm),RS编码可视为如下(图1)所示矩阵运算。
图1最左边是编码矩阵(或称为生成矩阵、分布矩阵,Distribution Matrix),编码矩阵需要满足任意n*n子矩阵可逆。为方便数据存储,编码矩阵上部是单位阵(n行n列),下部是m行n列矩阵。下部矩阵可以选择范德蒙德矩阵或柯西矩阵。
RS最多能容忍m个数据块被删除。 数据恢复的过程如下:
(1)假设D1、D4、C2丢失,从编码矩阵中删掉丢失的数据块/编码块对应的行。(图2、3)
(2)由于B’ 是可逆的,记B’的逆矩阵为 (B’^-1),则B’ * (B’^-1) = I 单位矩阵。两边左乘B’ 逆矩阵。 (图4、5)
(3)得到如下原始数据D的计算公式 。
(4)对D重新编码,可得到丢失的编码码
3.3 实践
Minio采用Reed-Solomon code将对象拆分成N/2数据和N/2 奇偶校验块。 这就意味着如果是12块盘,一个对象会被分成6个数据块、6个奇偶校验块,可以丢失任意6块盘(不管其是存放的数据块还是奇偶校验块),仍可以从剩下的盘中的数据进行恢复。
四、部署
4.1 单节点
容器部署:
docker pull minio/minio
#在Docker中运行Minio单点模式
docker run -p 9000:9000 -e MINIO_ACCESS_KEY=sunseaiot -e MINIO_SECRET_KEY=sunseaiot minio/minio server /data
#要创建具有永久存储的Minio容器,您需要将本地持久目录从主机操作系统映射到虚拟配置~/.minio 并导出/data目录
#建立外挂文件夹 /Users/hbl/dockersp/volume/minio/
docker run -d -p 9000:9000 -e MINIO_ACCESS_KEY=sunseaiot -e MINIO_SECRET_KEY=sunseaiot -v /Users/hbl/dockersp/volume/minio/data:/data -v /Users/hbl/dockersp/volume/minio/config:/root/.minio minio/minio server /data
12345678
4.2 多节点
分布式搭建的流程和单节点基本一样,Minio服务基于命令行传入的参数自动切换成单机模式还是分布式模式。
分布式Minio单租户存在最少4个盘最多16个盘的限制(受限于纠删码)。这种限制确保了Minio的简洁,同时仍拥有伸缩性。如果你需要搭建一个多租户环境,你可以轻松的使用编排工具(Kubernetes)来管理多个Minio实例。
五、Java SDK访问Minio服务
package com.minio.client;
import io.minio.MinioClient;
import io.minio.errors.MinioException;
import lombok.extern.slf4j.Slf4j;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@Slf4j
public class FileUploader {
public static void main(String[] args) throws NoSuchAlgorithmException, IOException, InvalidKeyException, XmlPullParserException {
try {
MinioClient minioClient = new MinioClient("https://minio.sunseaiot.com", "sunseaiot", "sunseaiot",true);
// 检查存储桶是否已经存在
if(minioClient.bucketExists("ota")) {
log.info("Bucket already exists.");
} else {
// 创建一个名为ota的存储桶
minioClient.makeBucket("ota");
log.info("create a new bucket.");
}
//获取下载文件的url,直接点击该url即可在浏览器中下载文件
String url = minioClient.presignedGetObject("ota","hello.txt");
log.info(url);
//获取上传文件的url,这个url可以用Postman工具测试,在body里放入需要上传的文件即可
String url2 = minioClient.presignedPutObject("ota","ubuntu.tar");
log.info(url2);
// 下载文件到本地
minioClient.getObject("ota","hello.txt", "/Users/hbl/hello2.txt");
log.info("get");
// 使用putObject上传一个本地文件到存储桶中。
minioClient.putObject("ota","tenant2/hello.txt", "/Users/hbl/hello.txt");
log.info("/Users/hbl/hello.txt is successfully uploaded as hello.txt to `task1` bucket.");
} catch(MinioException e) {
log.error("Error occurred: " + e);
}
}
}
六、对象存储Minio 客户端工具类,实现文件上传、图像压缩、图像添加水印
对象存储Minio 客户端工具类,实现文件上传、图像压缩、图像添加水印
MioioUtil负责处理文件的上传及minio的相关初始化,ImageUtil负责对图像文件进行压缩及添加水印的操作。
需要在pom.xml中引入两个jar包:
MInio Client sdk包和谷歌的thumbnailator图像处理工具:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>3.0.10</version>
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
MinioUtil.class如下:
import io.minio.MinioClient;
import io.minio.errors.InvalidEndpointException;
import io.minio.errors.InvalidPortException;
import io.minio.policy.PolicyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.Random;
/**
* 对象存储工具类
* @author 10552
*/
public class MinioUtil {
private static final Logger log = LoggerFactory.getLogger(MinioUtil.class);
//Mino服务器的AccessKey
private final transient static String ACCESS_KEY="填写你的Mino服务器AccessKey";
//Mino服务器的SecretKey
private final transient static String SECRET_KEY="填写你的Mino服务器SecretKey";
//桶名称
private final transient static String BUCKET_NAME="delivery";
//读写分离-上传服务器
private final transient static String OSS_URL_WRITE="http://你的服务器上传地址";
//读写分离-下载服务器
private final transient static String OOS_URL_READ="http://你的服务器下载地址";
//minio服务端口,默认是9000
private final transient static int OSS_PORT=9000;
private transient static boolean BUCKET_EXISTS=false;
//单例模式-内部类实现
private static class MinioClientHolder {
private static MinioClient minioClient;
static {
try {
minioClient = new MinioClient(OSS_URL_WRITE, OSS_PORT,ACCESS_KEY, SECRET_KEY);
} catch (InvalidEndpointException e) {
e.printStackTrace();
} catch (InvalidPortException e) {
e.printStackTrace();
}
}
}
/**
* 获取minio客户端实例
* @return
*/
private static MinioClient getMinioClient(){
return MinioClientHolder.minioClient;
}
/**
* 上传文件
* 支持单文件,多文件
* 返回文件访问路径,多文件以分号‘;’分隔
* @param muFiles
* @return
*/
public static String uploadFiles(MultipartFile... muFiles) {
if (muFiles.length<1){
throw new RuntimeException("上传文件为空!");
}
StringBuilder str=new StringBuilder();
for (MultipartFile muFile : muFiles) {
str.append(uploadFile(muFile));
str.append(";");
}
return str.deleteCharAt(str.length()-1).toString();
}
/**
* 内部方法
* 上传文件
* @param muFile
* @return
*/
private static String uploadFile(MultipartFile muFile){
String fileName = getFilePathName(muFile,false);
try {
MinioClient minioClient = getMinioClient();
if(!BUCKET_EXISTS&&!minioClient.bucketExists(BUCKET_NAME)){
minioClient.makeBucket(BUCKET_NAME);
minioClient.setBucketPolicy(BUCKET_NAME, "", PolicyType.READ_ONLY);
BUCKET_EXISTS=true;
}
InputStream inputStream=muFile.getInputStream();
//如果是图片文件就进行压缩
if (ImageUtil.isImage(muFile.getOriginalFilename())){
inputStream=ImageUtil.getInputStream(
ImageUtil.setWatermark(
ImageUtil.compress(
ImageIO.read(inputStream))),
ImageUtil.getFileExtention(muFile.getOriginalFilename()));
}
minioClient.putObject(BUCKET_NAME, fileName , inputStream,muFile.getContentType());
} catch (Exception e) {
log.error("文件上传失败",e);
throw new RuntimeException("文件上传失败");
}
return OOS_URL_READ+BUCKET_NAME+fileName;
}
/**
* 获取文件名
* @param muFile 文件
* @param isRetain 是否保留源文件名
* @return 返回文件名,以当前年月日作为前缀路径
*/
private static String getFilePathName(MultipartFile muFile,boolean isRetain){
String fileName = muFile.getOriginalFilename();
String name=fileName;
String prefix="";
if(fileName.indexOf('.')!=-1) {
name=fileName.substring(0,fileName.indexOf('.'));
prefix=fileName.substring(fileName.lastIndexOf("."));
}
LocalDate date = LocalDate.now();
StringBuilder filePathName=new StringBuilder("/upload/");
filePathName.append(date.getYear());
filePathName.append("/");
filePathName.append(date.getMonthValue());
filePathName.append("/");
filePathName.append(date.getDayOfMonth());
filePathName.append("/");
//添加随机后缀
Random r = new Random();
int pix=r.ints(1, (100 + 1)).findFirst().getAsInt();
filePathName.append(System.currentTimeMillis());
filePathName.append(""+pix);
//文件名超过32字符则截取
if(isRetain){
filePathName.append("_");
if(name.length()>=32){
name=name.substring(0,32);
}
filePathName.append(name);
}
filePathName.append(prefix);
return filePathName.toString();
}
ImageUtil.class工具类如下:
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
/**
* 图像工具类
*/
public class ImageUtil {
private static final Logger log = LoggerFactory.getLogger(ImageUtil.class);
//压缩率
private static final transient float IMAGE_RATIO=0.1f;
//压缩最大宽度
private static final transient int IMAGE_WIDTH=800;
// 水印透明度
private static float alpha = 0.3f;
// 水印文字字体
private static Font font = new Font("PingFang SC Regular", Font.PLAIN, 36);
// 水印文字颜色
private static Color color = new Color(111, 111, 111);
//水印文字内容
private static final String text="这是一个水印文本";
// 水印之间的间隔
private static final int XMOVE = 80;
// 水印之间的间隔
private static final int YMOVE = 80;
/**
* 压缩图像
* @param image
* @return
* @throws IOException
*/
public static BufferedImage compress(BufferedImage image) throws IOException {
Thumbnails.Builder<BufferedImage> imageBuilder= Thumbnails.of(image).outputQuality(IMAGE_RATIO);
if(image.getWidth()>IMAGE_WIDTH){
return imageBuilder.width(IMAGE_WIDTH).asBufferedImage();
}
else {
return imageBuilder.scale(1).asBufferedImage();
}
}
/**
* 图像添加水印
* @param
* @return
*/
public static BufferedImage setWatermark(BufferedImage image)throws IOException {
return Thumbnails.of(image)
.outputQuality(IMAGE_RATIO)
.scale(1)
.watermark(Positions.BOTTOM_RIGHT
,createWatermark(text
,image.getWidth()
,image.getHeight()
)
,alpha)
.asBufferedImage();
}
/**
* 根据文件扩展名判断文件是否图片格式
* @return
*/
public static boolean isImage(String fileName) {
String[] imageExtension = new String[]{"jpeg", "jpg", "gif", "bmp", "png"};
for (String e : imageExtension) if (getFileExtention(fileName).toLowerCase().equals(e)) return true;
return false;
}
/**
* 获取文件后缀名称
* @param fileName
* @return
*/
public static String getFileExtention(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
return extension;
}
/**
* 根据图片对象获取对应InputStream
*
* @param image
* @param readImageFormat
* @return
* @throws IOException
*/
public static InputStream getInputStream(BufferedImage image, String readImageFormat) throws IOException
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, readImageFormat, os);
InputStream is = new ByteArrayInputStream(os.toByteArray());
os.close();
return is;
}
/**
* 创建水印图片
* @param text 水印文字
* @param width 图片宽
* @param height 图片高
* @return
*/
public static BufferedImage createWatermark(String text,int width,int height) {
BufferedImage image = new BufferedImage(width
, height, BufferedImage.TYPE_INT_RGB);
// 2.获取图片画笔
Graphics2D g = image.createGraphics();
// ---------- 增加下面的代码使得背景透明 -----------------
image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g.dispose();
g = image.createGraphics();
// ---------- 背景透明代码结束 -----------------
// 6、处理文字
AttributedString ats = new AttributedString(text);
ats.addAttribute(TextAttribute.FONT, font, 0, text.length());
AttributedCharacterIterator iter = ats.getIterator();
// 7、设置对线段的锯齿状边缘处理
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 8、设置水印旋转
g.rotate(Math.toRadians(-30));
// 9、设置水印文字颜色
g.setColor(color);
// 10、设置水印文字Font
g.setFont(font);
// 11、设置水印文字透明度
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
/**
* 水印铺满图片
* 计算水印位置
*/
int x = -width / 2;
int y = -height / 2;
int[] arr = getWidthAndHeight(text, font);
int markWidth = arr[0];// 字体长度
int markHeight = arr[1];// 字体高度
// 循环添加水印
while (x < width * 1.5) {
y = -height / 2;
while (y < height * 1.5) {
g.drawString (text, x, y);
y += markHeight + YMOVE;
}
x += markWidth + XMOVE;
}
// 13、释放资源
g.dispose();
return image;
}
/**
* 计算字体宽度及高度
* @param text
* @param font
* @return
*/
private static int[] getWidthAndHeight(String text, Font font) {
Rectangle2D r = font.getStringBounds(text, new FontRenderContext(
AffineTransform.getScaleInstance(1, 1), false, false));
int unitHeight = (int) Math.floor(r.getHeight());//
// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度
int width = (int) Math.round(r.getWidth()) + 1;
// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度
int height = unitHeight + 3;
return new int[]{width, height};
}
七、minIO如何设置直接通过访问链接在浏览器中打开文件
问题描述:
在创建文件bucket后,上传html文件,发现访问文件链接,浏览器并不会如预期那样像访问静态网页一样打开测试报告,而是跳转到minIO系统,并且定位到该文件路径
解决方案:
原因是因为minIO没有配置bucket策略,默认情况下,minIO没有配置匿名读写的权限


如上图所示,在bucket菜单栏中点击Edit policy,新增Read权限,(Read Only 或 Read and Write均可),即可通过链接的方式直接访问该文件
备注:
由于浏览器的限制,当上传文件时,设置header为application/octet-stream时,浏览器打开链接会默认进行下载而不是在浏览器中加载文件,所以如果想要文件时直接打开,上传时则不要设置application/octet-stream
application/octet-stream : 二进制流数据(如常见的文件下载)
常见的媒体格式类型如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
以application开头的媒体格式类型:
- application/xhtml+xml :XHTML格式
- application/xml: XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json: JSON数据格式
- application/pdf:pdf格式
- application/msword : Word文档格式
- application/octet-stream : 二进制流数据(如常见的文件下载)
- application/x-www-form-urlencoded : <form encType=“”>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)