minio提供高性能、兼容S3的对象存储,适合存储非结构化数据,如视频、图片、日志文件、备份数据等,文中主要介绍minio中几个关键流程。
如图所示,假定擦除集中包含10个驱动(磁盘),则会组成一个6+4的纠删码集合,用户上传一个6M大小的对象,则会对其先拆分成6个1M大小的数据块,然后根据纠删码计算得到4个1M大小的校验块,一共10M数据,随机的分布在磁盘中
文中主要介绍上传、下载、删除、巡检等核心流程
数据上传主要分以下几个流程(如图所示):
3.1.1 选择pool
源码在
cmd/erasure-server-pool.go
中的PutObject
方法
3.1.2 选择set
源码在
cmd/erasure-sets.go
中的PutObject`方法
其实在选择pool的时候已经计算过一次对应object会落在那个set中,这里会有两种哈希算法:
3.1.3 上传
源码在
cmd/erasure-object.go
中的PutObject
方法
3.1.3.1 确定数据块、校验块个数及写入Quorum
x-amz-storage-class
值确定校验块个数 parityDrives
MINIO_STORAGE_CLASS_RRS
则返回对应的校验块数,否则为2MINIO_STORAGE_CLASS_STANDARD
则返回对应的校验块数,否则返回默认值 | Erasure Set Size | Default Parity (EC:N) | | --- | --- | | 5 or fewer | EC:2 | | 6-7 | EC:3 | | 8 or more | EC:4 |parityDrives+=统计set中掉线或者不存在的磁盘数
,如果parityDrives
大于set磁盘数的一半,则设置校验块个数为set的磁盘半数,也就是说校验块的个数是不定的dataDrives:=set中drives个数-partyDrives
3.1.3.2 数据写入
BlockSize
:表示纠删码计算的数据块大小,可以简单理解有1M的用户数据则会根据纠删码规则计算得到数据块+校验块ShardSize
:纠删码块的实际shard大小,比如blockSize=1M,数据块个数dataBlocks为5,那么单个shard大小为209716字节(blockSize/dataBlocks向上取整),是指ec的每个数据小块大小ShardFileSize
:最终纠删码数据shard大小,比如blockSize=1M,数据块个数dataBlocks为5,用户上传一个5M的对象,那么这里会将其分五次进行纠删码计算,最终得到的单个shard的实际文件大小为5*shardSizeshardFileSize计算
)。cmd/xl-storage.go
中的CreteFile
方法)3.1.3.3 元数据写入
元数据主要包含以下内容(详细定义见附录对象元数据信息
)
x-minio-internal-inline-data: true
元数据示例(源码xl-storage.go
中的getFileInfo
方法可以获取到元数据信息):
{
"volume":"lemon",
"name":"temp.2M",
"data_dir":"8366601f-8d64-40e8-90ac-121864c79a45",
"mod_time":"2021-08-12T01:46:45.320343158Z",
"size":2097152,
"metadata":{
"content-type":"application/octet-stream",
"etag":"b2d1236c286a3c0704224fe4105eca49"
},
"parts":[
{
"number":1,
"size":2097152,
"actualSize":2097152
}
],
"erasure":{
"algorithm":"reedsolomon",
"data":2,
"parity":2,
"blockSize":1048576,
"index":4,
"distribution":[
4,
1,
2,
3
],
"checksum":[
{
"PartNumber":1,
"Algorithm":3,
"Hash":""
}
]
},
...
}
3.1.4 数据在机器的组织结构
我们查看某个桶(目录)下的文件结构
.
├── GitKrakenSetup.exe #文件名
│ ├── 449e2259-fb0d-48db-97ed-0d71416c33a3 #datadir,存放数据,分片上传的话会有多个part
│ │ ├── part.1
│ │ ├── part.2
│ │ ├── part.3
│ │ ├── part.4
│ │ ├── part.5
│ │ ├── part.6
│ │ ├── part.7
│ │ └── part.8
│ └── xl.meta #存放对象的元数据信息
├── java_error_in_GOLAND_28748.log #可以看到这个文件没有datadir,因为其为小文件将数据存放到了xl.meta中
│ └── xl.meta
├── temp.1M
│ ├── bc58f35c-d62e-42e8-bd79-8e4a404f61d8
│ │ └── part.1
│ └── xl.meta
├── tmp.8M
│ ├── 1eca8474-2739-4316-9307-12fac3a3ccd9
│ │ └── part.1
│ └── xl.meta
└── worker.conf
└── xl.meta
3.1.5 思考
a. 对于不满足quorum写失败的数据如何清理?
minio在写入数据时会先将各个节点的数据写入到一个临时目录,如果写入不满足quorum则会将临时目录中的数据删除
b. 对于满足quorum写失败的节点数据如何恢复?
据的写入满足quorum机制且具备一定的数据可靠性,如果能够将写失败的数据通过某种手段恢复出来那么将极大提高数据的可靠性,所以针对上面问题可以从两个方面去思考:1. 如果发现写失败数据;2. 如何去恢复,这里如何去恢复比较简单,通过纠删码计算即可。所以这里主要介绍如何去发现缺失数据
minio在设计上是结合2跟3发现缺失数据并修复。
c. 数据写入时候会等待所有待写入节点返回(无论成功或失败),这里是否有优化点?
实际数据在写入时,如果需要等待所有节点返回响应,可能会存在长尾效应,导致写入时延不稳定,如果写入时满足quorum即向用户返回成功,由后台再等待其他节点响应在一定程度上能提升写入速度,不过也会在设计上增加复杂性。
3.2.1 选择pool
源码在
cmd/erasure-server-pool.go
中的GetObjectNInfo
方法
3.2.2 选择set
源码在
cmd/erasure-sets.go
中的GetObjectNInfo
方法
与上传对象类似,对对象名进行哈希得到具体存储的set
3.2.3 读元信息
源码在
cmd/erasure-object.go
中的GetObjectNInfo
方法
3.2.4 读数据
HealNormalScan
HealDeepScan
3.2.5 思考
a. 数据读取每次都会向所有online节点发起读请求,是否可以只发部分节点呢?考虑是什么?
正常来说只要能读到数据块个数的数据,即可将全部数据给计算出来,也可以提升数据的读取速度,不过这里向所有节点发起读请求可能考虑的是另外一个层面的事情,通过数据读取及时的发现缺失数据并修复,如果仅仅依赖巡检也有一定滞后性。
b. 数据修复的触发流程依赖读请求具有一定的局限性,对于一些冷数据可能一直得不到修复,是否还有其他流程修复数据?
除了根据读请求发现缺失数据,还有后台巡检流程能够修复缺失数据。
删除相对来说要简单一些,这里主要介绍DeleteObject
方法
3.3.1 普通删除
源码在
cmd/erasure-server-pool.go
中的DeleteObject
方法
前缀删除:会向所有pool发送删除请求 非前缀删除:主要流程如下
minioMetaTmpDeletedBucket
中异步清理minioMetaTmpDeletedBucket
中,异步清理如图所示,文件
temp.1M
为大文件,数据存储在磁盘中的数据结构为左边所示,在删除时候会进行以下关键几步:(小文件的删除少了第二步,因为数据存储在xl.meta中,所以在执行第三步的同时也把数据移动到了回收站)
3.3.2 思考
a. 删除数据如果不满足quorum机制,已经删除的数据是否会被修复回来?
这里其实分很多场景,删除数据失败,可能是删除元数据失败也可能是删除数据失败,这里简单介绍几种场景
b. 删除的数据会暂时保存在回收站,那么是否有办法将回收站中的数据恢复出来?
目前没有看到相关数据恢复代码,不过只要数据存在可以通过一定的手段将数据恢复出来
c. 删除满足quorum机制,对于删除失败的节点怎么处理?
删除失败节点上的数据其实为垃圾数据,这里会通过数据巡检流程去删除,巡检过程中如果发现存在数据不满足quorum,则会去执行数据清理操作
3.4.1 发现坏盘
源码在
cmd/erasure-sets.go
中的connectDisks
方法中
{
"version":"1",
"format":"xl",
"id":"8acad898-054b-4414-92b1-b01a49d61407",
"xl":{
"version":"3",
"this":"8585ed86-180f-4fd4-a95e-83d5ef2943ec",
"sets":[
[
"8585ed86-180f-4fd4-a95e-83d5ef2943ec",
"dba71e26-9bb0-49a4-9c4a-d4c1fb8dca6d",
"49fb2e14-3c71-4d59-99dd-f26029928f4a",
"5f755d25-bce7-40e7-b1cc-a360c7b8e4c7"
]
],
"distributionAlgo":"SIPMOD+PARITY"
}
}
errUnformattedDisk
errUnformattedDisk
错误的磁盘加入到待修复磁盘队列中3.4.2 修盘
源码在
cmd/background-newdisks-heal-ops.go
的monitorLocalDisksAndHeal
方法
定期检查是否存在待修磁盘,如果存在则会进行以下操作
HealFormat
,检查集群中所有磁盘是否缺失format.json
文件,如果缺失则会将其修补回来HealBucket
,修复桶元数据HealObject
,修复桶中的文件下面是磁盘修复示例日志
Found drives to heal 1, proceeding to heal content...
Healing disk '/data/minio/data1' on 1st pool
Healing disk '/data/minio/data1' on 1st pool complete
Summary:
{
"ID": "8585ed86-180f-4fd4-a95e-83d5ef2943ec",
"PoolIndex": 0,
"SetIndex": 0,
"DiskIndex": 0,
"Path": "/data/minio/data1",
"Endpoint": "/data/minio/data1",
"Started": "2021-10-15T10:07:27.12996706+08:00",
"LastUpdate": "2021-10-15T02:07:40.784965249Z",
"ObjectsTotalCount": 11,
"ObjectsTotalSize": 561956829,
"ItemsHealed": 20,
"ItemsFailed": 0,
"BytesDone": 561966273,
"BytesFailed": 0,
"QueuedBuckets": [],
"HealedBuckets": [
".minio.sys/config",
".minio.sys/buckets",
"lemon"
]
}
源码在
cmd/data-scanner.go
的runDataScanner
方法
数据巡检主要做以下事情:
巡检时候会在每块磁盘上对所有bucket中的数据进行巡检,这里主要介绍下巡检是如何发现待修复数据并执行修复?
每次巡检都会将巡检的结果缓存在本地,下次巡检与之对比
前面提到数据删除之后会先将其移动到回收站,然后交由后台协程定时扫描清理,接下来主要介绍清理过程
源码在
erasure-sets.go
的cleanupDeletedObjects
这里的清理策略其实很简单,定时的去清理回收站的所有文件,也就是说对于已经放入回收站的数据来说没有单独的时间保护窗口,均是定期被清理。
// hashOrder - hashes input key to return consistent
// hashed integer slice. Returned integer order is salted
// with an input key. This results in consistent order.
// NOTE: collisions are fine, we are not looking for uniqueness
// in the slices returned.
func hashOrder(key string, cardinality int) []int {
if cardinality <= 0 {
// Returns an empty int slice for cardinality < 0.
return nil
}
nums := make([]int, cardinality)
keyCrc := crc32.Checksum([]byte(key), crc32.IEEETable)
start := int(keyCrc % uint32(cardinality))
for i := 1; i <= cardinality; i++ {
nums[i-1] = 1 + ((start + i) % cardinality)
}
return nums
}
// ceilFrac takes a numerator and denominator representing a fraction
// and returns its ceiling. If denominator is 0, it returns 0 instead
// of crashing.
func ceilFrac(numerator, denominator int64) (ceil int64) {
if denominator == 0 {
// do nothing on invalid input
return
}
// Make denominator positive
if denominator < 0 {
numerator = -numerator
denominator = -denominator
}
ceil = numerator / denominator
if numerator > 0 && numerator%denominator != 0 {
ceil++
}
return
}
// ShardSize - returns actual shared size from erasure blockSize.
func (e *Erasure) ShardSize() int64 {
return ceilFrac(e.blockSize, int64(e.dataBlocks))
}
// ShardFileSize - returns final erasure size from original size.
func (e *Erasure) ShardFileSize(totalLength int64) int64 {
if totalLength == 0 {
return 0
}
if totalLength == -1 {
return -1
}
numShards := totalLength / e.blockSize
lastBlockSize := totalLength % e.blockSize
lastShardSize := ceilFrac(lastBlockSize, int64(e.dataBlocks))
return numShards*e.ShardSize() + lastShardSize
}
type FileInfo struct {
// Name of the volume.
Volume string
// Name of the file.
Name string
// Version of the file.
VersionID string
// Indicates if the version is the latest
IsLatest bool
// Deleted is set when this FileInfo represents
// a deleted marker for a versioned bucket.
Deleted bool
// TransitionStatus is set to Pending/Complete for transitioned
// entries based on state of transition
TransitionStatus string
// TransitionedObjName is the object name on the remote tier corresponding
// to object (version) on the source tier.
TransitionedObjName string
// TransitionTier is the storage class label assigned to remote tier.
TransitionTier string
// TransitionVersionID stores a version ID of the object associate
// with the remote tier.
TransitionVersionID string
// ExpireRestored indicates that the restored object is to be expired.
ExpireRestored bool
// DataDir of the file
DataDir string
// Indicates if this object is still in V1 format.
XLV1 bool
// Date and time when the file was last modified, if Deleted
// is 'true' this value represents when while was deleted.
ModTime time.Time
// Total file size.
Size int64
// File mode bits.
Mode uint32
// File metadata
Metadata map[string]string
// All the parts per object.
Parts []ObjectPartInfo
// Erasure info for all objects.
Erasure ErasureInfo
// DeleteMarkerReplicationStatus is set when this FileInfo represents
// replication on a DeleteMarker
MarkDeleted bool // mark this version as deleted
DeleteMarkerReplicationStatus string
VersionPurgeStatus VersionPurgeStatusType
Data []byte // optionally carries object data
NumVersions int
SuccessorModTime time.Time
}
// formatErasureV3 struct is same as formatErasureV2 struct except that formatErasureV3.Erasure.Version is "3" indicating
// the simplified multipart backend which is a flat hierarchy now.
// In .minio.sys/multipart we have:
// sha256(bucket/object)/uploadID/[xl.meta, part.1, part.2 ....]
type formatErasureV3 struct {
formatMetaV1
Erasure struct {
Version string `json:"version"` // Version of 'xl' format.
This string `json:"this"` // This field carries assigned disk uuid.
// Sets field carries the input disk order generated the first
// time when fresh disks were supplied, it is a two dimensional
// array second dimension represents list of disks used per set.
Sets [][]string `json:"sets"`
// Distribution algorithm represents the hashing algorithm
// to pick the right set index for an object.
DistributionAlgo string `json:"distributionAlgo"`
} `json:"xl"`
}
20盘2个set示例
{
"version":"1",
"format":"xl",
"id":"921e205e-15bc-480e-899d-8f220a0d908a",
"xl":{
"version":"3",
"this":"d3b71e3d-f71c-4140-a982-81071be76687",
"sets":[
[
"d3b71e3d-f71c-4140-a982-81071be76687",
"6e80d70a-7ce3-4446-a078-4ea97272deb4",
"fdfe30e1-97ba-48ad-8db9-b36f1adb40df",
"68e99791-8f3e-4b68-8e61-57b39ce8e105",
"8211a0d6-2be4-47a9-a08c-5bd42821fd47",
"b2bb82a4-cf92-406a-9a4b-235b4608009b",
"6bd02b7d-e40f-4d6d-ae28-cae9555a8148",
"da7fe426-232d-4510-bb28-1b4c6fff1695",
"1aa3fca1-20e5-48ac-8ac2-b49399300a42",
"b9088ba0-bf4c-45a7-87f7-3d10266577ab"
],
[
"500a6ae7-a46e-4ad9-a409-d6265d0d7d54",
"01b55e5f-3e15-4a0c-8a18-1ee9e1864753",
"7ce04256-c860-411f-92ea-bd5c5335d358",
"f334e1bd-498e-4d44-9ff7-2f7c41c40c7b",
"81c39b83-7215-49e1-86c5-e1af3d0283a4",
"e9d832fa-73e4-4963-90c3-8d11048a3dfc",
"e5c291ce-cc14-484a-b707-61088f91fd8c",
"6af2ed10-67ef-4c9f-b3fb-4a0bce6732b5",
"63fa146c-71bd-4466-a130-23c8d2b50cab",
"b80ca0e5-3e04-4b90-a1b6-6e83669c046d"
]
],
"distributionAlgo":"SIPMOD+PARITY"
}
}