01-minio

  • minio 版本:RELEASE.2021-07-* 及以上
  • go语言写的,天然跨平台,有性能优势

1. 分布式文件系统应用场景

互联网海量非结构化数据的存储需求

  • 电商网站:海量商品图片
  • 视频网站:海量视频
  • 文件网盘 : 海量文件
  • 社交网站:海量图片

1. 介绍

MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务。它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据(eg:图片、视频、日志文件、备份数据和容器/虚拟机镜像等),而一个对象文件可以是任意大小,从几kb到最大5T不等

MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS、Redis、MySQL

REpuYnZWTGJMQlZ2NzF6K094VUh1d29OUGJZcFZtWG5xbkM3NHVJPQ==

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本

对于中小型企业,如果不选择存储上云,那么 Minio 是个不错的选择,麻雀虽小,五脏俱全。当然 minio 除了直接作为对象存储使用,还可以作为云上对象存储服务的网关层,无缝对接到 Amazon S3、MicroSoft Azure。在中国:阿里巴巴、腾讯、百度、中国联通、华为、中国移动等 9000 多家企业也都在使用 MinIO 产品

1. 优点

  • 部署简单:一个 single 二进制文件即是一切,还可支持各种平台
  • minio 支持海量存储,可按 zone 扩展(原 zone 不受任何影响),支持单个对象最大5TB
  • 兼容 Amazon S3 接口,充分考虑开发人员的需求和体验
  • 低冗余且磁盘损坏高容忍,标准且最高的数据冗余系数为 2(即存储一个1M的数据对象,实际占用磁盘空间为 2M)。但在任意 n/2 块 disk 损坏的情况下依然可以读出数据(n 为一个纠删码集合(ErasureCoding Set)中的 disk 数量)。并且这种损坏恢复是基于单个对象的,而不是基于整个存储卷的
  • 读写性能优异
S21oWXQxTGwzQW95NFhCNzNic0tGUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

2. 基础概念

  • Object:存储到 minio 的基本对象(eg:文件、字节流... )
  • Bucket:用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。对于客户端而言,就相当于一个存放文件的顶层文件夹
  • Drive:即存储数据的磁盘,在 MinIO 启动时,以参数的方式传入。minio 中所有的对象数据都会存储在 Drive 里
  • Set:即一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的 Drive 分布在不同位置。一个对象存储在一个 Set 上。(eg: {1...64} is divided into 4 sets each of size 16.
    • 一个对象存储在一个 Set 上
    • 一个集群划分为多个 Set
    • 一个 Set 包含的 Drive 数量是固定的,默认由系统根据集群规模自动计算得出
    • 一个 Set 中的 Drive 尽可能分布在不同的节点上

3. 纠删码EC

MinIO 使用纠删码机制(Erasure Code)来保证高可靠性,使用 highway hash 来处理数据损坏(Bit Rot Protection)。关于纠删码,简单来说就是可以通过数学计算,把丢失的数据进行还原,它可以将 n 份原始数据,增加 m 份数据,并能通过 n + m 份中的任意 n 份数据,还原为原始数据。即如果有任意小于等于 m 份的数据失效,仍然能通过剩下的数据还原出来

4. 存储形式

文件对象上传到 MinIO,会在对应的数据存储磁盘中,以 Bucket 名称为目录,文件名称为下一级目录,文件名下是 part.1xl.meta(老版本,最新版本如下图),前者是编码数据块及检验块,后者是元数据文件

ZUMzb29ZOGozaFI3Wm5BNkpFQ2x1d29OUGJZcFZtWG5xbkM3NHVJPQ==

5. 存储方案

QStxbWVWcFJSYUZMbzQxZEFtNEMvZ29OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

2. 环境搭建

MinIO 支持多种 server 启动模式:

ZGJ4OTFlb05QclRGcWNOQUkwUmtFQW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

1. 单机部署

MinIO server 的 standalone 模式,即要管理的磁盘都在 host 本地。该启动模式一般仅用于实验环境、测试环境的验证和学习使用。在 standalone 模式下,还可以分为 non-erasure code modeerasurecode mode

1. non-erasure code mode

在此启动模式下,对于每一份对象数据,minio 直接在 data 下面存储这份数据,不会建立副本,也不会启用纠删码机制。因此,这种模式无论是服务实例还是磁盘都是“单点”,无任何高可用保障,磁盘损坏就表示数据丢失

2. erasure code mode

此模式为 minio server 实例传入多个本地磁盘参数。一旦遇到多于一个磁盘参数,minio server 会自动启用 erasure code modeerasure code 对磁盘的个数是有要求的,如不满足要求,实例启动将失败。erasure code 启用后,要求传给 minio server 的 endpoint(standalone 模式下,即本地磁盘上的目录)至少为 4 个

3. 基于centos7

操作系统CPU架构地址
GNU/Linux64-bit Intelhttp://dl.minio.org.cn/serer/minio/release/linux-amd64/mini
wget -o  http://dl.minio.org.cn/server/minio/release/linux-amd64/minio

chmod +x minio

# 启动 minio server 服务,指定数据存储目录 /mnt/data
./minioserver/mnt/data
ZTdUUVpWNWtNMmVsYkc1R1FENHZSZ29OUGJZcFZtWG5xbkM3NHVJPQ==

默认用户名密码 minioadmin:minioadmin,修改默认用户名密码可以使用

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678

默认的配置目录是 ${HOME}/.minio,可以通过 --config-dir 命令自定义配置目录

./minio server --config-dir /mnt/config /mnt/data

控制台监听端口是动态生成的,可以通过 --console-address ":port" 指定静态端口

./minio server --console-address ":50000" /mnt/data
YTczYVhSaWNYcFl3S05IMGhQZzFBUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

访问 minio 控制台:http://192.168.3.14:50000/dashboard

elEvbWxnUXZwVUF5bDRtMlJjMWJVQW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

4. 基于docker

MinIO_dockeropen in new window

docker run -p 9000:9000 --name minio \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server /data
  • 存在问题:浏览器无法访问 minio 控制台,因为没有对外暴露控制台端口
  • 对外暴露 minio 控制台的端口,通过 --console-address ":50000" 指定控制台端口为静态端口
docker run -p 9000:9000 -p 50000:50000 --name minio \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server --console-address ":50000" /data



 
UjZjbHpCbWxMT3pBUjdibWVKRHA4QW9OUGJZcFZtWG5xbkM3NHVJPQ==

MinIO 自定义用户名密码

docker run -d -p 9000:9000 -p 50000:50000 --name minio \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=12345678" \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server --console-address ":50000" /data

5. 纠删码模式

minio 使用《纠删码 erasure code》和《校验和 checksum》来保护数据免受硬件故障和无声数据损坏。 即便您丢失一半数量(N/2)的硬盘,仍然可以恢复数据

纠删码是一种恢复丢失和损坏数据的数学算法,MinIO 采用 Reed-Solomon code 将对象拆分成 N/2 数据和 N/2 奇偶校验块。这就意味着如果是 12 块盘,一个对象会被分成 6 个数据块、6 个奇偶校验块,可以丢失任意 6 块盘(不管其是存放的数据块还是奇偶校验块),仍可以从剩下的盘中的数据进行恢复

VFJidVg3UHR2cU0yQWlqdEh4eDhHUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

使用 Minio Docker 镜像,在 8 块盘中启动 Minio 服务:

docker run -d -p 9000:9000 -p 50000:50000 --name minio \
-v /mnt/data1:/data1 \
-v /mnt/data2:/data2 \
-v /mnt/data3:/data3 \
-v /mnt/data4:/data4 \
-v /mnt/data5:/data5 \
-v /mnt/data6:/data6 \
-v /mnt/data7:/data7 \
-v /mnt/data8:/data8 \
minio/minio server /data{1...8} --console-address ":50000"

2. 分布式集群部署

分布式 minio 可以将多块硬盘(甚至在不同的机器上)组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式 minio 避免了单点故障

aDZSQ3o0VUc4OUx0YXU5amdIRkJyZ29OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

1. 可靠性常用方法

分布式存储,很关键的点在于数据的可靠性,即保证数据的完整,不丢失,不损坏。只有在可靠性实现的前提下,才有了追求一致性、高可用、高性能的基础。而对于在存储领域,一般对于保证数据可靠性的方法主要有两类:

1. 冗余

冗余法最简单直接,即对存储的数据进行副本备份,当数据出现丢失,损坏,即可使用备份内容进行恢复,而副本备份的多少,决定了数据可靠性的高低。这其中会有成本的考量,副本数据越多,数据越可靠,但需要的设备就越多,成本就越高。可靠性是允许丢失其中一份数据。当前已有很多分布式系统是采用此种方式实现。eg:Hadoop 的文件系统(3个副本),Redis 的集群,MySQL 的主备模式等

2. 校验

校验法即通过校验码的数学计算的方式,对出现丢失、损坏的数据进行校验、还原。注意,这里有两个作用

  • 校验。通过对数据进行《校验和(checksum)》进行计算,可以检查数据是否完整,有无损坏或更改,在数据传输和保存时经常用到。如 TCP 协议
  • 恢复还原。通过对数据结合校验码,通过数学计算,还原丢失或损坏的数据,可以在保证数据可靠的前提下,降低冗余
    • eg:单机硬盘存储中的 RAID 技术,纠删码(Erasure Code)技术等。MinIO 采用的就是纠删码技术

2. 分布式优势

1. 数据保护

分布式 Minio 采用纠删码来防范多个节点宕机和位衰减 bit rot。至少需要 4 个硬盘,使用分布式 Minio 自动引入了纠删码功能

2. 高可用
  • 单机 minio 服务存在单点故障。有 N 块硬盘的分布式 Minio,只要有 N/2 硬盘在线,数据就是安全的。不过需要至少有 N/2+1 个硬盘来创建新的对象
    • eg:一个 16 节点的 minio 集群,每个节点 16 块硬盘,就算 8 台服务器宕机,这个集群仍然是可读的,不过需要 9 台服务器才能写数据
3. 一致性

minio 在分布式和单机模式下,所有读写操作都严格遵守 read-after-write 一致性模型

3. 运行

启动一个分布式 minio 实例,只需要把硬盘位置做为参数传给 minio server 命令即可,然后,需要在所有其它节点运行同样的命令

  • 所有的节点需要有同样的《access 秘钥》和《secret 秘钥》,这些节点才能建立联接。为了实现这个,需要在执行 minio server 命令之前,先将《access 秘钥》和《secret 秘钥》export 成环境变量。新版本使用 MINIO_ROOT_USERMINIO_ROOT_PASSWORD
  • 使用的磁盘里必须是干净的,里面没有数据
  • 节点时间差不能超过3秒,可以使用 NTP 来保证时间一致
  • 在 Windows 下运行分布式 minio 处于实验阶段
1. 8个节点,每节点1块盘

需要在 8 个节点上都运行下面的命令

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678
minio server http://192.168.1.11/export1 http://192.168.1.12/export2 \
				http://192.168.1.13/export3 http://192.168.1.14/export4 \
				http://192.168.1.15/export5 http://192.168.1.16/export6 \
				http://192.168.1.17/export7 http://192.168.1.18/export8
ZjRXYVhROCs5eXZsNHVlVTJJV3NPZ29OUGJZcFZtWG5xbkM3NHVJPQ==
2. 4节点,每节点4块盘

需要在 4 个节点上都运行下面的命令

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678
minio server http://192.168.1.11/export1 http://192.168.1.11/export2 \
				http://192.168.1.11/export3 http://192.168.1.11/export4 \
				http://192.168.1.12/export1 http://192.168.1.12/export2 \
				http://192.168.1.12/export3 http://192.168.1.12/export4 \
				http://192.168.1.13/export1 http://192.168.1.13/export2 \
				http://192.168.1.13/export3 http://192.168.1.13/export4 \
				http://192.168.1.14/export1 http://192.168.1.14/export2 \
				http://192.168.1.14/export3 http://192.168.1.14/export4
c2x0YjR1dmlKcFhFM1AzY0ltcHBoUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

通过脚本方式启动

Zi9kWFNzMUxQWmJLR1RZZ3BZQ2NLQW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

执行脚本后查看日志和进程

tail -f minio-9002.log
NlFjZUJMWnA1TFIzWDBQVUwzZ0NtUW9OUGJZcFZtWG5xbkM3NHVJPQ==
ps -ef|grep minio
Z3krM0twSy9TdnYremN5K0F5MXZHd29OUGJZcFZtWG5xbkM3NHVJPQ==

测试上传:

TlpHaTVobmRqYTFTdXp1VVpNelU5QW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

存储结构

WHlUM0M1L2p3bjdTVUFKRllRWVhwUW9OUGJZcFZtWG5xbkM3NHVJPQ==

4. DockerCompose部署

请下载 docker-compose.yamlopen in new windownginx.confopen in new window 到当前的工作目录

docker-compose pull
docker-compose up
version: '3.7'

# Settings and configurations that are common for all containers
x-minio-common: &minio-common
  image: quay.io/minio/minio:RELEASE.2024-11-07T00-52-20Z
  command: server --console-address ":9001" http://minio{1...4}/data{1...2}
  expose:
    - "9000"
    - "9001"
  # environment:
    # MINIO_ROOT_USER: minioadmin
    # MINIO_ROOT_PASSWORD: minioadmin
  healthcheck:
    test: ["CMD", "mc", "ready", "local"]
    interval: 5s
    timeout: 5s
    retries: 5

# starts 4 docker containers running minio server instances.
# using nginx reverse proxy, load balancing, you can access
# it through port 9000.
services:
  minio1:
    <<: *minio-common
    hostname: minio1
    volumes:
      - data1-1:/data1
      - data1-2:/data2

  minio2:
    <<: *minio-common
    hostname: minio2
    volumes:
      - data2-1:/data1
      - data2-2:/data2

  minio3:
    <<: *minio-common
    hostname: minio3
    volumes:
      - data3-1:/data1
      - data3-2:/data2

  minio4:
    <<: *minio-common
    hostname: minio4
    volumes:
      - data4-1:/data1
      - data4-2:/data2

  nginx:
    image: nginx:1.19.2-alpine
    hostname: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "9000:9000"
      - "9001:9001"
    depends_on:
      - minio1
      - minio2
      - minio3
      - minio4

## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:
  data3-1:
  data3-2:
  data4-1:
  data4-2:
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  4096;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    # include /etc/nginx/conf.d/*.conf;

    upstream minio {
        server minio1:9000;
        server minio2:9000;
        server minio3:9000;
        server minio4:9000;
    }

    upstream console {
        ip_hash;
        server minio1:9001;
        server minio2:9001;
        server minio3:9001;
        server minio4:9001;
    }

    server {
        listen       9000;
        listen  [::]:9000;
        server_name  localhost;

        # To allow special characters in headers
        ignore_invalid_headers off;
        # Allow any size file to be uploaded.
        # Set to a value such as 1000m; to restrict file size to a specific value
        client_max_body_size 0;
        # To disable buffering
        proxy_buffering off;
        proxy_request_buffering off;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 300;
            # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            chunked_transfer_encoding off;

            proxy_pass http://minio;
        }
    }

    server {
        listen       9001;
        listen  [::]:9001;
        server_name  localhost;

        # To allow special characters in headers
        ignore_invalid_headers off;
        # Allow any size file to be uploaded.
        # Set to a value such as 1000m; to restrict file size to a specific value
        client_max_body_size 0;
        # To disable buffering
        proxy_buffering off;
        proxy_request_buffering off;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-NginX-Proxy true;

            # This is necessary to pass the correct IP to be hashed
            real_ip_header X-Real-IP;

            proxy_connect_timeout 300;

            # To support websocket
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

            chunked_transfer_encoding off;

            proxy_pass http://console;
        }
    }
}

5. 扩展现有的分布式集群

通过区的方式启动 MinIO 集群

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678

minio server http://host{1...32}/export{1...32}

MinIO 支持通过命令,指定新的集群来扩展现有集群(纠删码模式)

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678

minio server http://host{1...32}/export{1...32}
http://host{33...64}/export{1...32}

现在整个集群就扩展了 1024 个磁盘,总磁盘变为 2048 个,新的对象上传请求会自动分配到最少使用的集群上。通过以上扩展策略,您就可以按需扩展您的集群。重新配置后重启集群,会立即在集群中生效,并对现有集群无影响。如上命令中,可以把原来的集群看做一个区,新增集群看做另一个区,新对象按每个区域中的可用空间比例放置在区域中。在每个区域内,基于确定性哈希算法确定位置

说明:您添加的每个区域必须具有与原始区域相同的磁盘数量(纠删码集)大小,以便维持相同的数据冗余 SLA。eg:第一个区有 8 个磁盘,您可以将集群扩展为 16 个、32 个或 1024 个磁盘的区域,您只需确保部署的 SLA 是原始区域的倍数即可

6. nginx实现负载均衡

upstream minio {
    server 192.168.3.14:9001;
    server 192.168.3.14:9002;
    server 192.168.3.14:9003;
    server 192.168.3.14:9004;
}

upstream console {
    ip_hash;
    server 192.168.3.14:50001;
    server 192.168.3.14:50002;
    server 192.168.3.14:50003;
    server 192.168.3.14:50004;
}

server {
    listen 9000;
    listen [::]:9000;
    server_name localhost;
    # To allow special characters in headers
    ignore_invalid_headers off;
    # Allow any size file to be uploaded.
    # Set to a value such as 1000m; to restrict file size to a specific
    value
    client_max_body_size 0;
    # To disable buffering
    proxy_buffering off;
    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 300;
        # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;
        proxy_pass http://minio;
    }
}

server {
    listen 50000;
    listen [::]:50000;
    server_name localhost;
    # To allow special characters in headers
    ignore_invalid_headers off;
    # Allow any size file to be uploaded.
    # Set to a value such as 1000m; to restrict file size to a specific
    value
    client_max_body_size 0;
    # To disable buffering
    proxy_buffering off;

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-NginX-Proxy true;
        proxy_connect_timeout 300;
        # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;
        proxy_pass http://console;
    }
}
VTU0cmI0R0ZjbU8yT2dkZTJIMHBnd29OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

3. 客户端使用

MinIO Client(mc)为 ls, cat, cp, mirror, diff, find 等 UNIX 命令提供了一种替代方案。它支持文件系统和兼容 Amazon S3 的云存储服务(AWS Signature v2 和 v4)

命令释义
ls列出文件和文件夹
mb创建一个存储桶或一个文件夹
cat显示文件和对象内容
pipe将一个STDIN重定向到一个对象或者文件或者STDOUT
share生成用于共享的URL
cp拷贝文件和对象
mirror给存储桶和文件夹做镜像
find基于参数查找文件
diff对两个文件夹或者存储桶比较差异
rm删除文件和对象
events管理对象通知
watch监视文件和对象的事件
policy管理访问策略
config管理mc配置文件
update检查软件更新
version输出版本信息

1. 部署客户端mc

平台CPU架构地址
GNU/Linux64-bit Intelhttp://dl.minio.org.cn/client/mc/release/linux-amd64/mc
Microsoft Windows64-bit Intelhttp://dl.minio.org.cn/client/mc/release/windows-amd64/mc.exe
wget http://dl.minio.org.cn/client/mc/release/linux-amd64/mc
chmod +x mc
./mc --help
mv mc /usr/local/sbin/

2. 配置mc

mc 将所有的配置信息都存储在 ~/.mc/config.json 文件中

# 查询 mc host 配置
mc config host ls
# 添加 minio 服务
mc config host add minio-server http://192.168.3.14:9000 admin 12345678
# 删除 host
mc config host remove minio-server

3. mc命令使用

MinIO客户端open in new window

mc命令使用
Is-列出存储桶和对象mb-创建存储桶cat-台并对象
cp-拷贝对象rm-删除对象pipe-Pipe到一个对象
share-共享mirror-存储桶镜像find-查找文件和对象
diff-比较存储桶差异poligy-给存储桶或前缀设置访问策略events-管理存储桶事件
config-管理配置文件watch-事件监听
update-管理软件更新version-显示版本信息
1. 上传下载
# 查询 minio 服务上的所有 buckets(文件和文件夹)
mc ls minio-server
# 下载文件
mc cp minio-server/tulingmall/fox/fox.jpg /tmp/
# 删除文件
mc rm minio-server/tulingmall/fox/fox.jpg
# 上传文件
mc cp zookeeper.out minio-server/tulingmall/
SUJrbTNlcTVST2NLa2tNSTdtRlgwd29OUGJZcFZtWG5xbkM3NHVJPQ==
2. Bucket管理
# 创建 bucket
mc mb minio-server/bucket01
# 删除 bucket
mc rb minio-server/bucket02
# bucket 不为空,可以强制删除 慎用
mc rb --force minio-server/bucket01
NSt1R3ZJRlRjaTdqYkJVdkxZanh4UW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=
cUtBdlRmNmRKUlp0eHdpcjFHN3BWUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=
# 查询 bucket03 磁盘使用情况
mc du minio-server/bucket03
TE1ZbzZlbFR4Wmo3T1l5MXNKcjZmQW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=
# 查询 bucket03 磁盘使用情况
mcduminio-server/bucket03

4. mc admin使用

MinIO Client(mc)提供了 admin 子命令来对 MinIO 部署执行管理任务

命令释义
service服务重启并停止所有MinIO服务器
update更新更新所有MinIO服务器
info信息显示MinIO服务器信息
user用户管理用户
group小组管理小组
policyMinIO服务器中定义的策略管理策略
config配置管理MinIO服务器配置
heal修复MinIO服务器上的磁盘,存储桶和对象
profile概要文件生成概要文件数据以进行调试
top顶部提供MinIO的顶部统计信息
trace跟踪显示MinIO服务器的http跟踪
console控制台显示MinIO服务器的控制台日志
prometheusPrometheus管理Prometheus配置
kmskms执行KMS管理操作
1. 用户管理
mc admin user --help
# 新建用户
mc admin user add minio-server fox
mc admin user add minio-server fox02 12345678
# 查看用户
mc admin user list minio-server
# 禁用用户
mc admin user disable minio-server fox02
# 启用用户
mc admin user disable minio-server fox02
# 查看用户信息
mc admin user info minio-server fox
# 删除用户
mc admin user remove minio-server fox02
aHVncVRoQm1MTnplU0pWL205K0JEd29OUGJZcFZtWG5xbkM3NHVJPQ==
2. 策略管理

policy 命令,用于添加、删除、列出策略,获取有关策略的信息并为 MinIO 服务器上的用户设置策略

mc admin policy --help
# 列出 MinIO 上的所有固定策略
mc admin policy list minio-server
# 查看 plicy 信息
mc admin policy info minio-server readwrite
RnlydG1jWkZ3SGVjK25odkZMTkZod29OUGJZcFZtWG5xbkM3NHVKYmJnPT0=
d2Qra0hneTI0Ums5R1FmZnBVcWNvZ29OUGJZcFZtWG5xbkM3NHVJPQ==

添加新的策略,编写策略文件:/root/tulingmall.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::tulingmall"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::tulingmall/*"
            ]
        }
    ]
}
"Action": [
    "s3:GetBucketLocation",
    "s3:ListBucket",
    "s3:GetObject",
    "s3:PutObject",
    "s3:DeleteObject"
]
# 添加新的策略
mc admin policy add minio-server tulingmall-admin /root/tulingmall.json
mc admin policy list minio-server
b20wWVdZc3BuazM1OW95NnYyeG50d29OUGJZcFZtWG5xbkM3NHVKYmJnPT0=
mc admin user add minio-server fox03 12345678
# 设置用户的访问策略
mc admin policy set minio-server tulingmall-admin user=fox03
c2RUTGRUYjBDTGRXRUNZTk5lVGZFUW9OUGJZcFZtWG5xbkM3NHVKYmJnPT0=

测试:fox03/12345678。登录 minio 控制台 http://192.168.3.14:50000/ ,只能操作 tulingmall 的 bucket

3. Java_Client使用

MinIO Java Client SDK 提供简单的 API 来访问任何与 Amazon S3 兼容的对象存储服务

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.3.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.8.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>

1. 文件上传

import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class FileUploader {
    public static void main(String[] args)
            throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        try {
            // Create a minioClient with the MinIO server playground, its access key and secret key.
            MinioClient minioClient = MinioClient.builder()
                    .endpoint("http://listao.cn:9000")
                    .credentials("root", "******")
                    .build();
            // 创建bucket
            String bucketName = "ooxx";
            boolean exists =
                    minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!exists) {
                // 不存在,创建bucket
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
            // 上传文件
            minioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucketName)
                            .object("Squeezer_2.5.2_osx.cx.dmg")
                            .filename("/Users/listao/Desktop/Squeezer_2.5.2_osx.cx.dmg")
                            .build()
            );
            System.out.println("上传文件成功");
        } catch (MinioException e) {
            System.out.println("Error occurred: " + e);
            System.out.println("HTTP trace: " + e.httpTrace());
        }
    }
}

2. 文件下载

import io.minio.DownloadObjectArgs;
import io.minio.MinioClient;

public class FileDownLoad {
    public static void main(String[] args) {
        // Create a minioClient with the MinIO server playground, its access key and secret key.
        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint("http://listao.cn:9000")
                        .credentials("root", "******")
                        .build();
        // Download object given the bucket, object name and output file name
        try {
            minioClient.downloadObject(
                    DownloadObjectArgs.builder()
                            .bucket("ooxx")
                            .object("Squeezer_2.5.2_osx.cx.dmg")
                            // 输出目录为项目根目录 listao_boot
                            .filename("ooxx.dmg")
                            .build());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. Boot整合

构建 MinioClient 对象,并交给 Spring 管理

@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
	private String endpoint;
	private String accessKey;
	private String secretKey;
}
minio:
  endpoint: http://listao.cn:9000
  accesskey: root
  secretKey: ******
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {

    @Autowired
    private MinioProperties minioProperties;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(),
                        minioProperties.getSecretKey())
                .build();
    }
}

实现文件查询、上传、下载、删除操作

import com.alibaba.fastjson.JSON;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Component
public class MinioUtils {
    @Autowired
    private MinioClient minioClient;

    /**
     * 判断bucket是否存在,不存在则创建
     */
    public void existBucket(String bucketName) {
        try {
            boolean exists = minioClient.bucketExists(
                    BucketExistsArgs.builder().bucket(bucketName).build()
            );
            if (!exists) {
                minioClient.makeBucket(
                        MakeBucketArgs.builder().bucket(bucketName).build()
                );
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建bucket
     */
    public Boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(
                    MakeBucketArgs.builder()
                            .bucket(bucketName)
                            .build()
            );
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 删除存储bucket
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(
                    RemoveBucketArgs.builder()
                            .bucket(bucketName)
                            .build()
            );
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 文件状态查询
     */
    public Boolean statObject(String bucketName, String originalFilename) {
        try {
            // 对象不存在 statObject() 抛异常
            StatObjectResponse stat = minioClient.statObject(
                    StatObjectArgs.builder().bucket(bucketName).object(originalFilename).build()
            );
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 查看文件列表
     */
    public List<Object> listObjects(String bucketName) throws Exception {
        // 获取 bucket 列表
        Iterable<Result<Item>> myObjects = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).build()
        );
        Iterator<Result<Item>> iterator = myObjects.iterator();
        List<Object> items = new ArrayList<>();
        String format = "{'fileName':'%s','fileSize':'%s'}";
        while (iterator.hasNext()) {
            Item item = iterator.next().get();
            items.add(JSON.parse(String.format(format, item.objectName(), formatFileSize(item.size()))));
        }
        return items;
    }

    /**
     * 上传
     */
    public List<String> upload(String bucketName, MultipartFile[] multipartFiles) {
        List<String> names = new ArrayList<>(multipartFiles.length);
        for (MultipartFile file : multipartFiles) {
            String fileName = file.getOriginalFilename();
            // String[] split = fileName.split("\\.");
            // if (split.length > 1) {
            //     fileName = split[0] + "_" + System.currentTimeMillis() + "." + split[1];
            // } else {
            //     fileName = fileName + System.currentTimeMillis();
            // }
            if (statObject(bucketName, fileName)) {
                names.add(fileName + " - [已经存在]");
                continue;
            }
            InputStream in = null;
            try {
                in = file.getInputStream();
                minioClient.putObject(
                        PutObjectArgs.builder()
                                .bucket(bucketName)
                                .object(fileName)
                                .stream(in, in.available(), -1)
                                .contentType(file.getContentType())
                                .build()
                );
                names.add(fileName + " - [上传成功]");
            } catch (Exception e) {
                e.printStackTrace();
                names.add(fileName + " - [上传失败]");
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return names;
    }

    /**
     * 下载文件
     */
    public InputStream download(String bucketName, String fileName) {
        InputStream in = null;
        if (statObject(bucketName, fileName)) {
            // 文件下载
            try {
                in = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(bucketName)
                                .object(fileName)
                                .build()
                );
                // minioClient.downloadObject(
                //         DownloadObjectArgs.builder()
                //                 .bucket("ooxx")
                //                 .object("Squeezer_2.5.2_osx.cx.dmg")
                //                 // 输出目录为项目根目录 listao_boot
                //                 .filename("ooxx.dmg")
                //                 .build()
                // );
            } catch (Exception e) {
                e.printStackTrace();
                log.error(e.getMessage());
            }
        }
        // 文件不存在
        return in;
    }

    private static String formatFileSize(long fileS) {
        DecimalFormat df = new DecimalFormat("#.00");
        String fileSizeString = "";
        String wrongSize = "0B";
        if (fileS == 0) {
            return wrongSize;
        }
        if (fileS < 1024) {
            fileSizeString = df.format((double) fileS) + " B";
        } else if (fileS < 1048576) {
            fileSizeString = df.format((double) fileS / 1024) + " KB";
        } else if (fileS < 1073741824) {
            fileSizeString = df.format((double) fileS / 1048576) + " MB";
        } else {
            fileSizeString = df.format((double) fileS / 1073741824) + " GB";
        }
        return fileSizeString;
    }

    /**
     * 删除文件对象
     */
    public Boolean removeObject(String bucketName, String fileName) {
        try {
            Boolean b = statObject(bucketName, fileName);
            if (b) {
                // void 文件不存在,直接通过
                minioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build()
                );
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        return false;
    }

    /**
     * 批量删除文件对象
     */
    public List<Map<String, String>> removeObjects(String bucketName, List<String> objects) throws Exception {
        List<DeleteObject> dos = objects.stream()
                .map(DeleteObject::new)
                .collect(Collectors.toList());
        // 无声删除,删除成功 results 不会有任何内容
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(
                RemoveObjectsArgs.builder()
                        .bucket(bucketName)
                        .objects(dos).build()
        );
        List<Map<String, String>> list = new ArrayList<>();
        results.forEach(result -> {
                    try {
                        DeleteError error = result.get();
                        log.error("Error in deleting object " + error.objectName() + "; " + error.message());
                        list.add(Collections.singletonMap(error.objectName(), error.message()));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
        );
        return list;
    }

}
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

@Slf4j
@Controller
@RequestMapping("/minio")
public class MinioCtl {
    @Autowired
    MinioUtils minioUtils;
    private final String bucketName = "ooxx";

    @GetMapping("/makeBucket")
    @ResponseBody
    public Rst<Boolean> makeBucket() {
        return Rst.ok(minioUtils.makeBucket(bucketName));
    }

    @GetMapping("/list")
    @ResponseBody
    public List<Object> list() throws Exception {
        return minioUtils.listObjects(bucketName);
    }

    @PostMapping("/upload")
    @ResponseBody
    public Rst<Object> upload(@RequestParam(name = "file", required = false) MultipartFile[] files) {
        if (files == null || files.length == 0) {
            return Rst.error("上传文件不能为空");
        }
        List<String> upload = minioUtils.upload(bucketName, files);
        return Rst.ok("上传成功", upload);
    }

    @GetMapping("/download/{fileName}")
    public void download(HttpServletResponse response, @PathVariable("fileName") String fileName) {
        InputStream in = null;
        OutputStream out = null;
        try {
            // 对象不存在 statObject() 抛异常
            in = minioUtils.download(bucketName, fileName);
            if (in != null) {
                response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
                response.setHeader("Content-Disposition", "attachment;filename=" +
                        URLEncoder.encode(fileName, "UTF-8"));
                // 文件下载

                out = response.getOutputStream();
                IOUtils.copy(in, out);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
        }
    }

    @DeleteMapping("/delete/{fileName}")
    @ResponseBody
    public Rst<Boolean> delete(@PathVariable("fileName") String fileName) {
        return Rst.ok("删除成功", minioUtils.removeObject(bucketName, fileName));
    }

    @PostMapping("/deleteBatch")
    @ResponseBody
    public Rst<List<Map<String, String>>> deleteBatch(@RequestParam List<String> fileNames) throws Exception {
        List<Map<String, String>> maps = minioUtils.removeObjects(bucketName, fileNames);
        return Rst.ok("删除成功", maps);
    }

}

1. 文件大小控制

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB

2. 下载请求设置

  • application/octet-stream
@GetMapping("/download/{fileName}")
public void download(HttpServletResponse response, @PathVariable("fileName") String fileName) {
    InputStream in = null;
    OutputStream out = null;
    try {
        // 对象不存在 statObject() 抛异常
        in = minioUtils.download(bucketName, fileName);
        if (in != null) {
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader("Content-Disposition", "attachment;filename=" +
                    URLEncoder.encode(fileName, "UTF-8"));
            // 文件下载

            out = response.getOutputStream();
            IOUtils.copy(in, out);
        }
    } catch (Exception e) {
        log.error(e.getMessage());
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    }
}








 


























3. statObject

  • 查询不存在的文件
error occurred
ErrorResponse(code = NoSuchKey, message = Object does not exist, bucketName = ooxx, objectName = Navicat自动备份数据库.pdf, resource = /ooxx/Navicat%E8%87%AA%E5%8A%A8%E5%A4%87%E4%BB%BD%E6%95%B0%E6%8D%AE%E5%BA%93.pdf, requestId = 180ED0FD616609DD, hostId = dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8)
request={method=HEAD, url=http://listao.cn:9000/ooxx/Navicat%E8%87%AA%E5%8A%A8%E5%A4%87%E4%BB%BD%E6%95%B0%E6%8D%AE%E5%BA%93.pdf, headers=Host: listao.site:9000
Accept-Encoding: identity
User-Agent: MinIO (Mac OS X; aarch64) minio-java/8.3.0
Content-MD5: 1B2M2Y8AsgTpgAmY7PhCfg==
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date: 20241207T062452Z
Authorization: AWS4-HMAC-SHA256 Credential=*REDACTED*/20241207/us-east-1/s3/aws4_request, SignedHeaders=content-md5;host;x-amz-content-sha256;x-amz-date, Signature=*REDACTED*
}
response={code=404, headers=Accept-Ranges: bytes
Content-Length: 0
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
X-Amz-Request-Id: 180ED0FD616609DD
X-Content-Type-Options: nosniff
X-Minio-Error-Code: NoSuchKey
X-Minio-Error-Desc: "The specified key does not exist."
X-Ratelimit-Limit: 388
X-Ratelimit-Remaining: 388
X-Xss-Protection: 1; mode=block
Date: Sat, 07 Dec 2024 06:24:53 GMT
}

	at io.minio.S3Base.execute(S3Base.java:670)
	at io.minio.S3Base.execute(S3Base.java:466)
	at io.minio.S3Base.executeHead(S3Base.java:728)
	at io.minio.S3Base.statObject(S3Base.java:1447)
	at io.minio.MinioClient.statObject(MinioClient.java:217)
	at com.listao.spring_boot.minio.MinioUtils.statObject(MinioUtils.java:92)
	at com.listao.spring_boot.minio.MinioUtils.removeObject(MinioUtils.java:257)
	at com.listao.spring_boot.minio.MinioCtl.delete(MinioCtl.java:88)

4. 公有读,私有写

验证时,一定要重开无痕模式。GET 请求下的图片,浏览器不会缓存;下载过的文件,浏览器会缓存

最小权限原则,先设置为 Private

NVNtMjJJeHpjWGhyVUsyZHFidnJjUVpOSHZHaGtuVEN5aUlHOFRBPQ==

桶中 generator_picture 文件夹设置为《公有读》。/ 在文件夹后,/ 在前会自动删除

V1F6Q3AzZmxVR0Q4aDJZWDVnTzMwd1pOSHZHaGtuVEN5aUlHOFRBPQ==
NHNIOXE3akVRVjN2VmVCbE1HZ3hTZ1pOSHZHaGtuVEN5aUlHOFREVldnPT0=
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::generator"
            ]
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::generator"
            ],
            "Condition": {
                "StringEquals": {
                    "s3:prefix": [
                        "generator_picture/"
                    ]
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::generator/generator_picture/*"
            ]
        }
    ]
}
















































 
 
 



5. 防盗链

UTZnTjVqVE0xT1VxT0FKV1lHR21jd1pOSHZHaGtuVEN5aUlHOFREVldnPT0=
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "http://*listao.cn:*",
                        "http://localhost:8000/"
                    ]
                }
            }
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::generator"
            ]
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::generator/*"
            ],
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "http://*listao.cn:*",
                        "http://localhost:8000/"
                    ]
                }
            }
        }
    ]
}