概述
目前在后端开发中,对于文件存储的技术选型用Minio再合适不过,本文就来详细说明一下,在生产环境中是如何部署Minio存储服务,以及做到如下几点功能:
- 数据安全:数据防丢失,即便磁盘损坏,只要不严重都能恢复
- 数据迁移:快速方便的迁移磁盘数据
- 数据扩容:磁盘容量满了也可以快速灵活的扩容
- 数据上云:自建的存储机房成本受限,可以快速上云,比如阿里云OSS
Minio的部署有很多种方式,我们根据上面列出的几点功能来实际说明。
环境准备
部署的方式依赖于Docker/docker-compose环境,Docker可以方便我们演示多服务器的情况,不然准备多个Linux环境也太麻烦了。
所以学习本文内容前需要先掌握Docker和docker-compose的知识。
因为全部容器都在一台服务器上,所以我们先准备一个网络环境:
shell
docker network create mynetwork
数据安全
一个简单的生产环境:单服务器双磁盘,一块磁盘做业务存储,另一块做数据备份,这是我们能接受的数据安全的最低限度,如果没有备份磁盘,那么我们的数据就是不安全的。
单服务器单磁盘
假设我们没有备份磁盘,只有一个业务存储盘,那么:
yml
# minio0.yml
version: "3"
services:
minio0:
# 选择一个合适的版本即可
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
# 网络配置
network_mode: mynetwork
# 容器名
container_name: minio0
restart: always
# 端口映射
ports:
- 9000:9000
# 路径映射
volumes:
# 同步宿主机时间
- /etc/localtime:/etc/localtime
# 映射存储路径
- /application/containers/minio0/data:/data
# 环境变量
environment:
# 登录账密
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 启动命令
command: server /data
运行容器:
shell
docker-compose -f minio0.yml up
上面是单服务器、单磁盘的部署配置,我们把数据存到了宿主机的唯一路径下。
当我们的磁盘损坏了,那么数据就会发生丢失,所以这种方式是数据不安全的。
适用于开发环境,不担心数据丢失,而且数据没有冗余,可以节省空间。
单服务器多磁盘
我们不会在生产环境搭建不安全的存储服务,所以这里我们要开始配置多磁盘:
yml
# minio1.yml
version: "3"
services:
minio1:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio1
restart: always
ports:
- 9001:9000
volumes:
- /etc/localtime:/etc/localtime
# 这里模拟了4块磁盘
- /application/containers/minio1/data1:/data1
- /application/containers/minio1/data2:/data2
- /application/containers/minio1/data3:/data3
- /application/containers/minio1/data4:/data4
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 启动命令,/data{1...4}是便捷写法,意思是:/data1 /data2 /data3 /data4
command: server /data{1...4}
需要注意的是,Minio的数据安全是依靠纠删码 来保证的,Minion要求纠删码至少需要4块磁盘才能使用,否则服务是跑不起来的。
数据安全验证
纠删码后面会说。默认的纠删码机制,假设磁盘数量为N,则会分成N/2块数据,N/2块奇偶校验码,在上面的例子中,我们有四块磁盘,那么纠删码机制能保证:
- 丢失一块磁盘,数据可读、可写、可恢复
- 丢失两块磁盘,数据可读、不可写、可恢复
- 丢失三块磁盘,那么数据就真丢了。
数据扩容
Minio的纠删码机制,决定了Minio不允许动态扩容,所以我们加磁盘是没用的。
但是有了mc数据恢复命令,我们可以通过升级某一磁盘来曲线救国,变相实现扩容:
- 移除某一个磁盘,不担心,数据仍然可读可写
- 更换一个更大的磁盘,然后做数据恢复
- 重启Minio服务
这样我们也实现了扩容。
MC命令来恢复数据
我们需要用到Minio的mc工具,来模拟数据丢失后如何处理:
shell
# 启动一个mc程序
docker run -it --network mynetwork --entrypoint=/bin/sh minio/mc
# 添加minio服务进来,并且起一个别名
# 格式:mc config host add <别名> <minio链接> <账号> <密码>
mc config host add minio1 http://minio1:9000 minio minio123
# 查看host列表,能看到我们刚刚添加进来的:minio1
mc config host list
# 查看桶列表
# 格式:mc ls <别名>
mc ls minio1
# 创建桶
# mc ls <别名>/<桶名>
mc mb minio1/demo
# 删除桶
# mc rb <别名>/<桶名>
mc rb minio/test
# 监听变化:别名/桶名
# mc watch <别名>/<桶名>
mc watch minio/test
# 查找文件和对象
# mc find <别名>/<桶名> --name "<关键字>"
mc find minio/test --name "*.xml"
# 查看桶内文件列表
# 格式:mc ls <别名>/<桶名>
mc ls minio1/demo
# 创建几个测试文件
echo "test" > a.txt
echo "test" > b.txt
echo "test" > c.txt
# 拷贝到指定桶
mc cp a.txt minio1/demo
/a.txt: 5 B / 5 B ━━
# 加上--recursive表示递归
mc cp --recursive minio/test minio/demo
# 以上操作可以在Minio页面上进行,效果相同
# 模拟数据丢失:把/data{1...4}这四个路径,删除任意一个,剩下三个
# 数据仍然可读
mc ls minio1/demo
[2023-09-01 06:32:51 UTC] 0B STANDARD a.txt
# 数据仍然可写
mc cp b.txt minio1/demo
/b.txt: 5 B / 5 B ━━
# 现在再删除一个路径,还剩两个
# 数据还是可读
mc ls minio1/demo
[2023-09-01 06:32:51 UTC] 0B STANDARD a.txt
[2023-09-01 06:37:24 UTC] 0B STANDARD b.txt
# 数据不可写了
mc cp c.txt minio1/demo
# 恢复数据
mc admin heal -r minio1
# 恢复数据需要重启Minio容器
docker restart minio1
# 再次检查,刚刚删除的两个路径又恢复了
多服务器多磁盘
现在生活富裕了,有条件能上两台服务器了,我们可以跑两个Minio服务做集群,互相分担存储任务:
yml
# minio-multi.yml
version: "3"
services:
minio1:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio1
restart: always
ports:
- 9001:9000
volumes:
- /etc/localtime:/etc/localtime
# 至少是4块磁盘
- /application/containers/minio1/data1:/data1
- /application/containers/minio1/data2:/data2
- /application/containers/minio1/data3:/data3
- /application/containers/minio1/data4:/data4
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 启动命令
command: server http://minio1/data{1...4} http://minio2/data{1...6}
minio2:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio2
restart: always
ports:
- 9002:9000
volumes:
- /etc/localtime:/etc/localtime
# 至少4块磁盘,所以6块盘也是可以的
- /application/containers/minio2/data1:/data1
- /application/containers/minio2/data2:/data2
- /application/containers/minio2/data3:/data3
- /application/containers/minio2/data4:/data4
- /application/containers/minio2/data5:/data5
- /application/containers/minio2/data6:/data6
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
command: server http://minio1/data{1...4} http://minio2/data{1...6}
现在我们有两种情况:
- 一种是我们本来部署的是单服务器多磁盘,现在系统升级了,那么就得做数据迁移
- 第二种是我们一开始就搞这个多服务器多磁盘,那么直接跑脚本就是了,但是后面容量不够了,就要做数据扩容
数据迁移
还是用mc命令,通过mc mirror可以实现数据的迁移,很简单,假设本来跑了一个Minio服务是minio0,现在多服务器跑了两个服务:minio1和minio2,因为minio1和minio2已经做了集群,所以数据传给minio1还是minio2都是一样的。
我们来说明一下数据的迁移:
shell
# 启动一个mc程序
docker run -it --network mynetwork --entrypoint=/bin/sh minio/mc
# 旧Minio服务
mc config host add minio0 http://minio0:9000 minio minio123
# 注意:因为有两个服务,所以使用mc的时候也记得添加两个:
mc config host add minio1 http://minio1:9000 minio minio123
mc config host add minio2 http://minio2:9000 minio minio123
# 全量迁移
# 格式:mc mirror <旧数据源> <新数据源>
mc mirror minio0 minio1
# 指定桶迁移,需要目标桶已存在,不存在会报错
mc mirror minio0/demo minio1
这样我们就实现了数据的迁移,迁移完后删除旧服务即可。
数据扩容
一般来说,数据扩容分垂直扩容和水平扩容:
垂直扩容指给服务添加磁盘,增加单体服务的存储容量,Minio因为纠删码机制,并不支持垂直扩容。
所以我们只能用水平扩容,Minio支持两种扩容方式:对等扩容 和联邦扩容,这两者各有优缺点:
对等扩容
假设现有minio集群1,有两个服务minio1和minio2,各自的磁盘数量都是4块,那么扩容的新集群也必须要是4块磁盘,或者是4的倍数,比如8块、16块。
这种方式扩容的好处是扩容效果很理想,新增的集群可以自动添加进来分担数据的存储压力,原来的业务该怎么跑就继续怎么跑。
缺点很明显,就是扩容方式不够灵活,必须的原来集群的相同配置,不能说原来的集群磁盘数量是4,新加入的集群磁盘数量是5,这是因为纠删码机制导致的。并且需要重启服务才能生效,不能做到灰度发布。
其实数据迁移也是变相的扩容,并且数据迁移可以解决上面提到的扩容方式不灵活的问题,因为我们可以重构一个新的集群,用我们希望的磁盘配置,然后迁移过来的数据也会自动进行纠删码的分块存储,这样可以使扩容稍微灵活一些。
现在来实战对等扩容,一开始我们配置了一个minio集群:
yml
# minio-cluster-01.yml
version: "3"
services:
minio1:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio1
restart: always
ports:
- 9001:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/containers/minio1/data1:/data1
- /application/containers/minio1/data2:/data2
- /application/containers/minio1/data3:/data3
- /application/containers/minio1/data4:/data4
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
command: server http://minio{1...2}/data{1...4}
minio2:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio2
restart: always
ports:
- 9002:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/containers/minio2/data1:/data1
- /application/containers/minio2/data2:/data2
- /application/containers/minio2/data3:/data3
- /application/containers/minio2/data4:/data4
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
command: server http://minio{1...2}/data{1...4}
启动成功后访问任意一个服务:http://localhost:9001
创建一个桶:demo
上传一些文件...
现在可以在minio1和minio2上面看到上传的数据,假设现在磁盘满了,我们在新服务器上又搭建了一个集群:
yml
# minio-cluster-02.yml
version: "3"
services:
minio3:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio3
restart: always
ports:
- 9003:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/containers/minio3/data1:/data1
- /application/containers/minio3/data2:/data2
- /application/containers/minio3/data3:/data3
- /application/containers/minio3/data4:/data4
- /application/containers/minio3/data5:/data5
- /application/containers/minio3/data6:/data6
- /application/containers/minio3/data7:/data7
- /application/containers/minio3/data8:/data8
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 4块磁盘就这样写
# command: server http://minio{1...2}/data{1...4} http://minio{3...4}/data{1...4}
# 8块磁盘就这样写
command: server http://minio{1...2}/data{1...4} http://minio{3...4}/data{1...8}
minio4:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio4
restart: always
ports:
- 9004:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/containers/minio4/data1:/data1
- /application/containers/minio4/data2:/data2
- /application/containers/minio4/data3:/data3
- /application/containers/minio4/data4:/data4
- /application/containers/minio4/data5:/data5
- /application/containers/minio4/data6:/data6
- /application/containers/minio4/data7:/data7
- /application/containers/minio4/data8:/data8
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
command: server http://minio{1...2}/data{1...4} http://minio{3...4}/data{1...8}
对等扩容十分简单,只要一行命令:
shell
# # minio-cluster-02.yml
command: server http://minio{1...2}/data{1...4} http://minio{3...4}/data{1...8}
前面这段:
shell
http://minio{1...2}/data{1...4}
表示第一个集群的minio1到minio2服务,磁盘是1到4,
第二段:
shell
http://minio{3...4}/data{1...8}
表示第二个集群的minio3到minio4服务,磁盘是1到8,也就是集群一的倍数,记住一定要是倍数,比如4、8。
我们别忘了修改集群一的配置里的command命令,改成一样的:
bash
# minio-cluster-01.yml
command: server http://minio{1...2}/data{1...4} http://minio{3...4}/data{1...8}
重启集群一使修改的配置生效,现在访问minio3或者minio4:
- 可以看到刚刚上传的数据,说明读集群一数据成功
- 上传一些新的数据,可以在minio3的磁盘看到存储,表示加入集群扩容成功
联邦扩容
对等扩容有两个大问题:
- 不能灰度发布,扩容需要重启所有集群,意味着系统业务会因此暂停
- 扩容方式不灵活,假如我一开始部署的集群很庞大,有10个Minio服务,每个服务50个磁盘,那么扩容的时候也必须是50的倍数
联邦扩容能解决上面的两个问题,他引入了新的复杂度:etcd中间件,一个KV数据库,三个etcd可以做一个集群,每一个Minio集群只需要连接上这个etcd集群,就能通过etcd做桥梁彼此连接起来,就好像微服务中的注册中心。
每一个新连接进来的Minio集群,不需要旧集群重启服务,只要连接成功etcd集群,扩容就完成了,由此解决了第一个不能灰度发布的问题。
每一个Minio集群也不需要特定的磁盘配置,只要部署完成都可以添加进来,由此解决第二个扩容方式不灵活的问题。
同样来实战一下,既然引入了新的中间件,首先当然是先把etcd集群搭起来:
yml
# etcd.yml
x-variables:
flag_initial_cluster_token: &flag_initial_cluster_token '--initial-cluster-token=mys3cr3ttok3n'
common_settings: &common_settings
image: quay.io/coreos/etcd:v3.4.27
network_mode: mynetwork
entrypoint: /usr/local/bin/etcd
restart: always
ports:
- 2379
services:
etcd-1:
<<: *common_settings
container_name: etcd-1
command:
# 当前etcd节点名称
- '--name=etcd-1'
# 集群的其他节点通过该地址与当前节点通信
- '--initial-advertise-peer-urls=http://etcd-1:2380'
# 当前节点通过该地址监听集群其他节点发送的信息
- '--listen-peer-urls=http://0.0.0.0:2380'
# 当前节点通过该地址监听客户端发送的信息
- '--listen-client-urls=http://0.0.0.0:2379'
# 客户端通过该地址与当前节点通信
- '--advertise-client-urls=http://etcd-1:2379'
# 心跳
- '--heartbeat-interval=250'
- '--election-timeout=1250'
# 当前集群的所有节点信息,当前节点根据此信息与其他节点取得联系
- '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380'
# 本次是否为新建集群,有两个取值:new和existing
- '--initial-cluster-state=new'
- *flag_initial_cluster_token
volumes:
- etcd1:/etcd_data
etcd-2:
<<: *common_settings
container_name: etcd-2
command:
- '--name=etcd-2'
- '--initial-advertise-peer-urls=http://etcd-2:2380'
- '--listen-peer-urls=http://0.0.0.0:2380'
- '--listen-client-urls=http://0.0.0.0:2379'
- '--advertise-client-urls=http://etcd-2:2379'
- '--heartbeat-interval=250'
- '--election-timeout=1250'
- '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380'
- '--initial-cluster-state=new'
- *flag_initial_cluster_token
volumes:
- etcd2:/etcd_data
etcd-3:
<<: *common_settings
container_name: etcd-3
command:
- '--name=etcd-3'
- '--initial-advertise-peer-urls=http://etcd-3:2380'
- '--listen-peer-urls=http://0.0.0.0:2380'
- '--listen-client-urls=http://0.0.0.0:2379'
- '--advertise-client-urls=http://etcd-3:2379'
- '--heartbeat-interval=250'
- '--election-timeout=1250'
- '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380'
- '--initial-cluster-state=new'
- *flag_initial_cluster_token
volumes:
- etcd3:/etcd_data
volumes:
etcd1:
etcd2:
etcd3:s
跑起来后我们可以通过docker命令去看看成不成功:
shell
# 查看健康状态
docker exec -it etcd-1 etcdctl endpoint health
# 查看节点列表
docker exec -it etcd-1 etcdctl member list
# 查看版本
docker exec -it etcd-1 etcd --version
# 搜索指定前缀的key
docker exec -it etcd-1 etcdctl get --prefix ""
# 删除指定前缀的key
docker exec -it etcd-1 etcdctl del --prefix ""
然后我们创建两个Minio集群来测试:
yml
# minio-etcd1.yml
version: "3"
services:
minio1:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio1
restart: always
ports:
- 9001:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/ssd1/minio1/data1:/data1
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 连接etcd集群,这里写上所有可以连接的节点IP
- MINIO_ETCD_ENDPOINTS=http://etcd-1:2380,http://etcd-2:2380,http://etcd-3:2380
# 该Minio集群的所有节点IP,因为只有一个服务,所以就只有一个
- MINIO_PUBLIC_IPS=minio1
# 必须进行配置,即使你并不通过域名访问存储桶,否则联邦无法生效,只有MINIO_DOMAIN参数值相同的集群,才会组成联盟
- MINIO_DOMAIN=cluster1.minio.com
# 关闭Web访问
# - MINIO_BROWSER=off
command: server /data1
yml
# minio-etcd2.yml
version: "3"
services:
minio2:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio2
restart: always
ports:
- 9002:9000
volumes:
- /etc/localtime:/etc/localtime
- /application/ssd2/minio2/data1:/data1
environment:
- MINIO_ROOT_USER=minio
- MINIO_ROOT_PASSWORD=minio123
# 连接etcd集群,这里写上所有可以连接的节点IP
- MINIO_ETCD_ENDPOINTS=http://etcd-1:2380,http://etcd-2:2380,http://etcd-3:2380
# 该Minio集群的所有节点IP,因为只有一个服务,所以就只有一个
- MINIO_PUBLIC_IPS=minio2
# 必须进行配置,即使你并不通过域名访问存储桶,否则联邦无法生效,只有MINIO_DOMAIN参数值相同的集群,才会组成联盟
- MINIO_DOMAIN=cluster1.minio.com
# 关闭Web访问
# - MINIO_BROWSER=off
command: server /data1
现在测试一下:
在集群一上面新增桶,就可以在etcd里面看到有数据:
shell
[root@localhost ~]# docker exec -it etcd-1 etcdctl get --prefix ""
/skydns/com/minio/cluster1/aaaa/172.18.0.7
{"host":"172.18.0.7","port":9000,"ttl":30,"creationDate":"2023-09-01T14:51:31.010225219Z"}
/skydns/com/minio/cluster1/aaaa/minio1
{"host":"minio1","port":9000,"ttl":30,"creationDate":"2023-09-01T14:51:31.010225219Z"}
config/iam/format.json
{"version":1}
意思是etcd记录了minio1集群创建了桶aaaa,这样的作用是,所有往aaaa桶里面上传的数据都会存到minio1集群中。为了验证这点,我们可以去minio2集群的磁盘上面看,是没有aaaa桶的数据的,这和对等扩容不一样,对等扩容是大家都会存一点,而联邦扩容是由etcd来判断数据存到哪个节点上。
虽然数据存储的方式有点变化,但是数据共享的没有问题的,我们去minio2集群上面可以看到aaaa这个由minio1集群创建的桶,即可读,也能往里面上传数据,即可写。
联邦扩容也有自己的问题:新集群不能往旧集群创建的桶里面上传数据,因为旧集群创建的桶,数据不能保存到新集群上,所以需要自己额外做新建桶策略。
也许是可以的,但是我查了很多资料也没找到办法。
数据上云
当我们自建的存储服务到了瓶颈,比如维护成本太高了,这时候我们可以选择上云,因为都这时候了,花点钱上云存储反而是一件节省成本的事情。
数据上云还是一个兜底的方案,不管我们怎么折腾,折腾累了就不折腾了,直接上云,很高兴的是Minio为数据上云提供了天然的支持。
以阿里云OSS存储为例,这里需要你有OSS的使用经验,不然会不好理解。
我们在阿里云上一通乱点,开通了OSS存储服务,得到了一下信息:
shell
# OSS存储节点
https://oss-cn-heyuan.aliyuncs.com/
# 阿里云ACCESS_KEY,这里是模拟的,请自行申请
ACCESS_KEY=LTAI5tJRzEjkQrtL
# 阿里云SECRET_KEY
SECRET_KEY=Bi4IDskQj3JEhOf
那么我们就可以创建一个Minio服务,代理这个OSS:
yml
version: "3"
services:
minio-gateway:
image: minio/minio:RELEASE.2021-06-17T00-10-46Z
network_mode: mynetwork
container_name: minio-gateway
restart: always
ports:
- 9001:9000
volumes:
- /etc/localtime:/etc/localtime
environment:
- MINIO_ACCESS_KEY=LTAI5tJRzEjkQrtL
- MINIO_SECRET_KEY=Bi4IDskQj3JEhOfS
command: gateway s3 https://oss-cn-heyuan.aliyuncs.com/
服务跑起来后如何访问提示权限不足,请在阿里云控制台里面给该账号添加访问权限。
这里的关键在于:
shell
gateway s3 https://oss-cn-heyuan.aliyuncs.com/
这里表示Minio将成为这个OSS存储服务的代理,我们上传的数据会直接存储到被代理的OSS存储服务器上面。
这种方式有什么好处呢?首先我们不用考虑扩容了,上了OSS,就用OSS的扩容方案,主要加钱就是了;
其次呢是做了一层包装,我们可以保持原有的业务系统不变,原有的请求方式不变,数据迁移仍然可以用mc命令,还能兼容其他的OSS服务。
关于纠删码
纠删码机制需要冗余的存储空间,其实按照传统的数据备份来看,我们绝对能够忍受一倍的冗余以保证数据安全,即两块4T硬盘,一块做存储,另一款做备份,总共容量是8T,但是实际可用4T,这是我们能接受的。
这里给一个Minio官方容量计算器:
在这里可以通过修改实际的配置场景来计算实际可用的存储大小。
在这个计算器上调整就可以知道,当我们的服务集群越来越多,那么磁盘利用率就会越来越高,从原来的50%上到60%或者75%,这就又体现了一个好处。