MinIO纠错码、分布式MinIO集群搭建及启动


前言

之前的博客介绍了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具有如下特点:

  1. 数据保护
    分布式MinIO采用纠删码来防范多个节点宕机和位衰减bit rot。
    分布式MinIO至少需要4个硬盘,采用分布式MinIO自动引入了纠删码功能。
  2. 高可用性
    单机MinIO服务存在单点故障,相反,如果是一个有N块硬盘的分布式MinIO,只要有N/2硬盘在线,你的数据就是安全的,不过你至少需要有N/2+1个硬盘来创建新的对象。
  3. 一致性
    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的代码。

猜你喜欢

转载自blog.csdn.net/qq_43403676/article/details/124910350