文章目录
前言
之前的博客介绍了Centos7.6部署MinIO分布式文件系统,这篇博客来介绍如何使用MinIO这款优秀的文件存储系统。
一、MinIO纠删码
Minio纠删码模式:MinIO使用纠删码erasure code和checksum来保护数据免受硬件故障和无声数据损坏。即使您丢失一半的硬盘,您仍然可以恢复数据。
纠删码是一种恢复丢失和损坏数据的数学算法,MinIO采用Reed-Solomon Code将对象拆分成N/2数据和N/2奇偶校验块。这就意味着如果是12块盘,一个对象会被分成6个数据块、6个奇偶校验块,你可以丢失任意6块盘(不关其是存放的数据块还是奇偶校验块),你仍然可以从剩下的盘中的数据进行恢复。
二、分布式集群部署
1.分布式存储可靠性常用的方法
分布式存储很关键的点在于数据的可靠性,即保证数据的完整、不丢失、不损坏。只有在可靠性的前提下,才有了追求一致性、高可用性、高性能的基础。而对于在存储领域,一般对于保证数据可靠性的方法主要有两类,一类是冗余法、一类是校验法。
- 冗余:冗余法最简单直接,对存储的数据进行副本备份,当数据出现丢失、损坏,即可使用备份内容进行恢复,而副本备份的多少决定了数据可靠性的高低。这其中会有成本的考量,副本数据越多,数据越可靠但需要设备的数量越多,成本越高。可靠性是允许丢失其中一份数据。当前已有很多分布式系统是采用这种方式实现,例如Hadoop的文件系统(3个副本),Redis的集群,MySQL的主备模式等。
- 校验:检验法即通过校验码的数学计算的方式,对出现丢失、损坏的数据进行校验、还原。注意,这里有两个作用,一个校验,通过对数据进行校验和(checksum)进行计算,可以检查数据是否完整,有无损坏或者更改,在数据传输和保存时常用,如Tcp协议;二是恢复还原,通过对数据结合校验码,通过数学计算,还原丢失或损坏的数据,可以在保证数据可靠的前提下,降低冗余,如单机硬盘存储中的RAID技术,交闪吗(Erasure Code)技术等。MinIO就是采用纠删码技术。
2.分布式MinIO
分布式MinIO具有如下特点:
- 数据保护
分布式MinIO采用纠删码来防范多个节点宕机和位衰减bit rot。
分布式MinIO至少需要4个硬盘,采用分布式MinIO自动引入了纠删码功能。 - 高可用性
单机MinIO服务存在单点故障,相反,如果是一个有N块硬盘的分布式MinIO,只要有N/2硬盘在线,你的数据就是安全的,不过你至少需要有N/2+1个硬盘来创建新的对象。 - 一致性
MinIO在分布式和单机模式下,所有读写操作都严格遵守read-after-write一致性模型。
3.分布式MinIO集群搭建
由于没那么多云服务器,采用虚拟机部署的方法进行验证。一共五台虚拟机,MinIO部署在四台虚拟机上,第五台虚拟机用来部署Nginx负载均衡。
3.1 下载MinIO
MinIO下载地址:http://dl.minio.org.cn/server/minio/release/linux-amd64/minio
3.2 为每一台虚拟机创建目录并上传MinIO文件:
(1)四台虚拟机开启时间同步:
# yum -y install ntp
# systemctl enable ntpd
# systemctl start ntpd
# timedatectl set-ntp yes
# ntpdate -u cn.pool.ntp.org
# ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# watch -n 1 'date'
(2)四台虚拟机都创建目录
mkdir /home/minio/{
app,config,data,logs} -p
登录xftp远程文件传输工具,将下载的MinIO上传到每个虚拟机的/home/minio/run目录下。
赋予可执行权限:
chmod +x minio
3.3 配置集群启动文件并使用Nginx代理
(1)配置脚本run.sh:
#!/bin/bash
export MINIO_ACCESS_KEY=minio123
export MINIO_SECRET_KEY=minio123456
/home/minio/run/minio server --config-dir /etc/minio --address ":9001" \
http://192.168.117.1/home/minio/data \
http://192.168.117.2/home/minio/data \
http://192.168.117.3/home/minio/data \
http://192.168.1117.4/home/minio/data >/home/minio/logs/start.txt 2>&1 &
(2)到四台虚拟机的run.sh目录下,启动应用:
sh run.sh
(3)上述完成了MinIO集群搭建,但是对每个节点进行访问,显然不合理,通过Nginx负载均衡,新建minio-cluster.conf文件,并写入以下内容:
upstream minio_console {
server 192.168.117.1:9001 max_fails=3 fail_timeout=5s;
server 192.168.117.2:9001 max_fails=3 fail_timeout=5s;
server 192.168.117.3:9001 max_fails=3 fail_timeout=5s;
server 192.168.117.4:9001 max_fails=3 fail_timeout=5s;
}
upstream minio_api {
server 192.168.117.1:9000 max_fails=3 fail_timeout=5s;
server 192.168.117.2:9000 max_fails=3 fail_timeout=5s;
server 192.168.117.3:9000 max_fails=3 fail_timeout=5s;
server 192.168.117.4:9000 max_fails=3 fail_timeout=5s;
}
server {
listen 9001; #或者用80端口也可以
server_name 192.168.117.6; #可以用域名
access_log /home/minio/logs/minio.com_access.log main;
error_log /home/minio/logs/minio.com_error.log warn;
location / {
proxy_next_upstream http_500 http_502 http_503 http_504 error timeout invalid_header;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://minio_console;
expires 0;
}
}
server {
listen 9000;
server_name 192.168.117.6; #可以用域名
access_log /home/minio/logs/minio.com_access.log main;
error_log /home/minio/logs/minio.com_error.log warn;
#root /home/minio/app/;
location / {
proxy_next_upstream http_500 http_502 http_503 http_504 error timeout invalid_header;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://minio_api;
expires 0;
}
}
部署完成之后,直接访问192.168.117.6:9001即可。
(4)额外知识(和上述部署无关):
Linux对某个.sh文件设置为开机自启动服务:
vim /usr/lib/systemd/system/minio.service
在minio.service中写入以下指令:
[Unit]
Description=Minio service
Documentation=https://docs.minio.io/
[Service]
WorkingDirectory=/home/data/minio/run
ExecStart=/home/data/minio/run/run.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
启动MinIO和开机自启动MinIO
systemctl start minio
systemctl enable minio
(5)systemctl和service
systemctl和service是Linux服务器管理服务的两种方式,systemctl兼容了service,systemctl的常用命令:
systemctl 提供了一组子命令来管理单个的 unit,其命令格式为:
systemctl [command] [unit]
command 主要有:
start:立刻启动后面接的 unit。
stop:立刻关闭后面接的 unit。
restart:立刻关闭后启动后面接的 unit,亦即执行 stop 再 start 的意思。
reload:不关闭 unit 的情况下,重新载入配置文件,让设置生效。
enable:设置下次开机时,后面接的 unit 会被启动。
disable:设置下次开机时,后面接的 unit 不会被启动。
status:目前后面接的这个 unit 的状态,会列出有没有正在执行、开机时是否启动等信息。
is-active:目前有没有正在运行中。
is-enable:开机时有没有默认要启用这个 unit。
kill :不要被 kill 这个名字吓着了,它其实是向运行 unit 的进程发送信号。
show:列出 unit 的配置。
mask:注销 unit,注销后你就无法启动这个 unit 了。
unmask:取消对 unit 的注销。
3.4 Springboot使用MinIO
application.yml文件:
# Tomcat
server:
port: 9201
servlet:
session:
timeout: 120m
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
mvc:
hiddenmethod:
filter:
enabled: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://150.158.10.136:2333/smgt?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: smgt
password: heuSSS321.002
mybatis:
typeAliasesPackage: com.wbz.system
mapperLocations: classpath:mapper/**/*.xml
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
minio:
url: http://150.158.102.211:9000
accessKey: minio
secretKey: minio123.
MinIO配置类:
package com.wbz.system.config;
import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig
{
/**
* 服务地址
*/
private String url;
/**
* 用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
public String getUrl()
{
return url;
}
public void setUrl(String url)
{
this.url = url;
}
public String getAccessKey()
{
return accessKey;
}
public void setAccessKey(String accessKey)
{
this.accessKey = accessKey;
}
public String getSecretKey()
{
return secretKey;
}
public void setSecretKey(String secretKey)
{
this.secretKey = secretKey;
}
public String getBucketName()
{
return bucketName;
}
public void setBucketName(String bucketName)
{
this.bucketName = bucketName;
}
@Bean
public MinioClient getMinioClient() {
return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
}
}
MinIO文件实体类
package com.wbz.system.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class SysFile {
private String name;
private String url;
public SysFile() {
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String toString() {
return (new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)).append("name", this.getName()).append("url", this.getUrl()).toString();
}
}
MinIO Controller层:
package com.wbz.system.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wbz.system.config.MinioConfig;
import com.wbz.system.domain.AjaxResult;
import com.wbz.system.domain.Data;
import com.wbz.system.domain.SysFile;
import com.wbz.system.service.ISysFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* 文件请求处理
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private ISysFileService sysFileService;
@GetMapping("/listBucket")
public AjaxResult listBucket() throws Exception{
return AjaxResult.success(sysFileService.listBucket());
}
@GetMapping("/listByBucket/{name}")
public List<Object> listByBucket(@PathVariable String name) throws Exception{
return sysFileService.list(name);
}
/**
* 桶操作
*/
@PostMapping("/add")
public AjaxResult add(@RequestBody MinioConfig minioConfig) throws Exception {
System.out.println(minioConfig.getBucketName());
return AjaxResult.success(sysFileService.makeBucket(minioConfig.getBucketName()));
}
@DeleteMapping("/delete/{bucketName}")
public AjaxResult remove(@PathVariable String bucketName) throws Exception {
System.out.println(bucketName);
return AjaxResult.success(sysFileService.removeBucket(bucketName));
}
/**
* 文件上传请求
* @return
*/
@PostMapping("/upload")
public AjaxResult upload(MultipartFile file, Data data, String bucket)
{
String bucketName = data.getBucket();
if(data.getBucket() == null){
bucketName = bucket;
}
try {
// 上传并返回访问地址
String newFile = sysFileService.uploadFile(file,bucketName);
SysFile sysFile = new SysFile();
sysFile.setName(newFile);
sysFile.setUrl(newFile);
return AjaxResult.success(newFile);
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
@RequestMapping("/download/{fileNames}")
public void download(HttpServletResponse response, @PathVariable String[] fileNames) throws Exception {
System.out.println(Arrays.toString(fileNames));
sysFileService.downloadFile(response,fileNames);
}
@DeleteMapping("/{fileNames}")
public void delete(@PathVariable String[] fileNames) throws Exception {
System.out.println(Arrays.toString(fileNames));
sysFileService.delete(fileNames);
}
@PostMapping("/getTag")
public AjaxResult getTag(@RequestBody Object object) throws Exception {
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(object));
return AjaxResult.success(sysFileService.getTagByFile(jsonObject.getString("bucketName"),jsonObject.getString("fileName")));
}
}
MinIO Service层:
package com.wbz.system.service.impl;
import com.alibaba.fastjson.JSON;
import com.wbz.system.config.MinioConfig;
import com.wbz.system.service.ISysFileService;
import com.wbz.system.utils.FileUploadUtils;
import io.minio.*;
import io.minio.messages.*;
//import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.time.format.DateTimeFormatter;
import java.util.*;
//import org.apache.tomcat.util.http.fileupload.IOUtils;
@Service
public class MinioSysFileServiceImpl implements ISysFileService
{
@Autowired
private MinioConfig minioConfig;
@Autowired
private MinioClient client;
@Override
public List<Object> listBucket() throws Exception {
List<Bucket> bucketList = client.listBuckets();
List<Object> items = new ArrayList<>();
String format = "{'id':%d,'label':'%s'}";
int id = 1;
for (Bucket bucket : bucketList) {
// System.out.println(bucket.creationDate() + ", " + bucket.name());
items.add(JSON.parse(String.format(format,id++,bucket.name())));
}
// System.out.println(items);
return items;
}
@Override
public List<Object> list(String name) throws Exception {
Iterable<Result<Item>> myObjects = client.listObjects(ListObjectsArgs.builder().bucket(name).build());
Iterator<Result<Item>> iterator = myObjects.iterator();
List<Object> items = new ArrayList<>();
String format = "{'fileName':'%s','fileSize':'%s','date':'%s','description':'%s'}";
while (iterator.hasNext()){
Item item = iterator.next().get();
Tags tags = client.getObjectTags(
GetObjectTagsArgs.builder().bucket(name).object(item.objectName()).build());
items.add(JSON.parse(String.format(format,item.objectName(),getSize(item.size()),item.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),tags.get().get("description"))));
}
System.out.println(items);
return items;
}
@Override
public String makeBucket(String bucketName) throws Exception {
boolean isExist = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if(isExist) {
return "0";
} else {
String s = String.format("{\"Version\":\"2012-10-17\"," +
"\"Statement\":[{\"Effect\":\"Allow\"," +
"\"Principal\":{\"AWS\":[\"*\"]}," +
"\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," +
"\"Resource\":[\"arn:aws:s3:::%s\"]}," +
"{\"Effect\":\"Allow\"," +
"\"Principal\":{\"AWS\":[\"*\"]}," +
"\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," +
"\"Resource\":[\"arn:aws:s3:::%s/*\"]}]}",bucketName,bucketName);
client.makeBucket(
MakeBucketArgs.builder()
.bucket(bucketName)
.build());
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(s).build());
}
return "1";
}
@Override
public String removeBucket(String bucketName) throws Exception {
boolean isExist = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if(isExist) {
client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
} else {
return "0";
}
return "1";
}
@Override
public Object getTagByFile(String bucketName, String fileName) throws Exception {
Tags tags = client.getObjectTags(
GetObjectTagsArgs.builder().bucket(bucketName).object(fileName).build());
System.out.println(Long.valueOf(tags.get().get("date")));
String format = "{'description':'%s','uid': %d}";
System.out.println(JSON.parse(String.format(format,tags.get().get("description"),Long.valueOf(tags.get().get("date")))));
return JSON.parse(String.format(format,tags.get().get("description"),Long.valueOf(tags.get().get("date"))));
}
/**
* 本地文件上传接口
*
* @param file 上传的文件
* @return 访问地址
* @throws Exception
*/
@Override
public String uploadFile(MultipartFile file, String bucketName) throws Exception
{
boolean isExist = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!isExist) {
makeBucket(bucketName);
}
String fileName = FileUploadUtils.extractFilename(file);
Map<String, String> map = new HashMap<>();
if(file.getOriginalFilename()!=null){
// String[] tags = file.getOriginalFilename().split("\\.");
map.put("description", file.getOriginalFilename());
map.put("date", String.valueOf(new Date().getTime()));
}
InputStream in = file.getInputStream();
PutObjectArgs args = PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(in, file.getSize(), -1)
.contentType(file.getContentType())
.build();
client.putObject(args);
client.setObjectTags(
SetObjectTagsArgs.builder().bucket(bucketName).object(fileName).tags(map).build());
in.close();
// String url = client.getPresignedObjectUrl(
// GetPresignedObjectUrlArgs.builder()
// .method(Method.PUT)
// .bucket(bucketName)
// .object(fileName)
// .expiry(1, TimeUnit.DAYS)
// .build());
// System.out.println(url);
return fileName;
}
/**
* 本地文件下载接口
*
* @param fileNames 上传的文件
* @return 访问地址
* @throws Exception
*/
@Override
public void downloadFile(HttpServletResponse response, String[] fileNames) throws Exception{
for (String fullFileName:fileNames){
String bucket = fullFileName.split("_")[0];
String fileName = fullFileName.split("_")[1];
InputStream in = null;
try{
StatObjectResponse statObjectResponse = client.statObject(
StatObjectArgs.builder().bucket(bucket).object(fileName).build()
);
byte[] buf = new byte[1024];
int length = 0;
response.reset();
System.out.println(statObjectResponse.contentType());
response.setContentType(statObjectResponse.contentType());
response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(fileName,"UTF-8"));
// response.setContentType("application/octet-stream");
// response.setHeader("Content-Disposition","attachment;filename="+ fileName);
response.setCharacterEncoding("UTF-8");
in = client.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.build()
);
// IOUtils.copy(in,response.getOutputStream());
OutputStream outputStream = response.getOutputStream();
while ((length = in.read(buf)) > 0) {
outputStream.write(buf, 0, length);
}
outputStream.close();
} catch (Exception e){
System.out.println(e);
} finally {
if(in!=null){
try{
in.close();
} catch (Exception e){
System.out.println(e);
}
}
}
}
}
@Override
public void delete(String[] fileNames) throws Exception {
List<DeleteObject> objects = new LinkedList<>();
String bucket = null;
for (String fullFileName:fileNames){
bucket = fullFileName.split("_")[0];
String fileName = fullFileName.split("_")[1];
objects.add(new DeleteObject(fileName));
}
Iterable<Result<DeleteError>> results =
client.removeObjects(
RemoveObjectsArgs.builder().bucket(bucket).objects(objects).build());
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
System.out.println(
"Error in deleting object " + error.objectName() + "; " + error.message());
}
}
private String getSize(long size){
DecimalFormat gb = new DecimalFormat("0.00");
DecimalFormat mb = new DecimalFormat("0.0");
if(size>=1024*1024*1024){
return gb.format((double)size / 1073741824L) +" GB";
}else if(size>=1024*1024){
return mb.format((double)size / 1048576L) +" MB";
}else if(size>=1024){
return (size / 1024) +" KB";
}else
return size+" B";
}
}
总结
这篇文章讲解了分布式存储保证可靠性常用的方法,MinIO纠错码,分布式MinIO的优点,以及虚拟机上分布式MinIO集群的搭建,并使用Nginx负载均衡来代理MinIO集群访问。最后给出了springboot使用MinIO的代码。