MinIO源码深度解读 - 第4章第2节:纠删码对象操作——erasure-object.go PUT/GET/DELETE 源码深度剖析
erasure-object.go 实现了 MinIO 对象读写的所有核心操作。本文带你逐方法深度分析 PutObject、GetObject、DeleteObject 的完整实现。
本文导航
- 文件总览
- PutObject 深度分析
- GetObject 深度分析
- DeleteObject 深度分析
- HeadObject 深度分析
文件总览
cmd/erasure-object.go是 MinIO 对象存储的核心实现文件,约 2500+ 行代码。
方法分类
| 类别 | 方法数量 | 核心方法 |
|---|---|---|
| 对象写入 | 6 | PutObject, composeObject, uploadDirect |
| 对象读取 | 8 | GetObject, GetObjectNInfo, GetObjectInfo |
| 对象删除 | 4 | DeleteObject, DeleteObjects |
| 元数据操作 | 10 | HeadObject, StatObject, ListObjectParts |
| 多部分上传 | 8 | NewMultipartUpload, UploadPart, CompleteMultipartUpload |
| 对象复制 | 4 | CopyObject, RenameObject |
| 标签操作 | 6 | GetObjectTagging, SetObjectTagging, DeleteObjectTagging |
| 锁操作 | 4 | LockObject, UnlockObject |
| 版本操作 | 6 | GetObjectVersion, DeleteObjectVersion |
PutObject 深度分析
完整流程
// cmd/erasure-object.go - PutObject 完整实现func(e*erasureObjects)PutObject(ctx context.Context,bucket,objectstring,reader io.Reader,opts ObjectOptions)(ObjectInfo,error){// ========== 第1步:前置验证 ==========// 1.1 验证桶名称iferr:=checkBucketName(bucket);err!=nil{returnObjectInfo{},toObjectErr(err,bucket)}// 1.2 验证对象名称iferr:=checkObjectName(object);err!=nil{returnObjectInfo{},toObjectErr(err,object)}// 1.3 验证内容长度size:=opts.SourceModifiedifsize<0||size>maxObjectSize{returnObjectInfo{},toObjectErr(errInvalidSize,size)}// ========== 第2步:定位 Erasure Set ==========// 2.1 通过 SipHash 确定对象所在的 Setset,setIndex:=e.sets.getByIndex(bucket,object)ifset==nil{returnObjectInfo{},toObjectErr(errErasureSetNotFound,bucket,object)}// 2.2 获取锁lk:=set.lock(ctx,bucket,object)deferlk.Unlock()// ========== 第3步:处理加密 ==========// 3.1 创建加密 Readerdata:=readerifopts.ServerSideEncryption!=nil{data=opts.ServerSideEncryption.CreateReader(reader)}// 3.2 计算加密 checksumhashReader:=newHashReader(data)data=hashReader// ========== 第4步:分片写入 ==========// 4.1 创建临时目录tmp:=mustGetUUID()deferset.rmObject(ctx,bucket,tmp)// 4.2 确定分片策略parts:=make([]ObjectPartInfo,0)ifsize>0{// 已知大小,预计算分片parts=e.calculateParts(size,opts)}// 4.3 流式分片写入for{// 读取一个数据块block,err:=io.ReadAll(io.LimitReader(data,e.blockSize))iferr==io.EOF{break}iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// Reed-Solomon 编码encoded,err:=set.encode(ctx,block)iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// 写入每个分片fori,shard:=rangeencoded{shardPath:=pathJoin(tmp,fmt.Sprintf("shard-%d",i))iferr:=set.writeShard(ctx,shardPath,shard);err!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}}// 记录分片信息parts=append(parts,ObjectPartInfo{PartNumber:len(parts),Size:int64(len(block)),Checksum:hashReader.Sum(),})}// ========== 第5步:写入元数据 ==========// 5.1 创建 xl.metameta,err:=e.createPartMetadata(ctx,bucket,object,parts,opts)iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// 5.2 写入 xl.metaiferr:=set.writeMeta(ctx,bucket,object,meta);err!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第6步:原子重命名 ==========// 6.1 将临时目录移动到最终位置iferr:=set.renameObject(ctx,bucket,tmp,object);err!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第7步:返回结果 ==========returnObjectInfo{Bucket:bucket,Object:object,Size:meta.Stat.Size,ModTime:meta.Stat.ModTime,ETag:meta.ETag,ContentType:meta.Stat.ContentType,VersionID:meta.Stat.VersionID,IsLatest:meta.Stat.IsLatest,// ...},nil}分片计算
// cmd/erasure-object.go - 分片计算// 计算对象分片信息func(e*erasureObjects)calculateParts(sizeint64,opts ObjectOptions)[]ObjectPartInfo{blockSize:=e.blockSize numParts:=int((size+blockSize-1)/blockSize)parts:=make([]ObjectPartInfo,numParts)fori:=0;i<numParts;i++{parts[i]=ObjectPartInfo{PartNumber:i+1,Size:blockSize,}}// 最后一个分片可能不满lastPartSize:=size-int64(numParts-1)*blockSizeiflastPartSize>0&&lastPartSize<blockSize{parts[numParts-1].Size=lastPartSize}returnparts}GetObject 深度分析
完整流程
// cmd/erasure-object.go - GetObject 完整实现func(e*erasureObjects)GetObject(ctx context.Context,bucket,objectstring,offset,lengthint64,opts ObjectOptions)(io.ReadCloser,ObjectInfo,error){// ========== 第1步:前置验证 ==========// 1.1 验证桶存在if!e.bucketsExist(bucket){returnnil,ObjectInfo{},toObjectErr(errBucketNotFound,bucket)}// ========== 第2步:定位 Erasure Set ==========// 2.1 通过 SipHash 确定对象所在的 Setset,setIndex:=e.sets.getByIndex(bucket,object)ifset==nil{returnnil,ObjectInfo{},toObjectErr(errErasureSetNotFound,bucket,object)}// 2.2 获取锁lk:=set.rlock(ctx,bucket,object)deferlk.RUnlock()// ========== 第3步:读取元数据 ==========// 3.1 读取 xl.metapartsMetadata,err:=set.readAllMetadata(ctx,bucket,object)iferr!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}// 3.2 验证元数据iferr:=set.checkObject(ctx,bucket,object,partsMetadata);err!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第4步:获取对象信息 ==========objInfo,err:=partsMetadataGetObjectInfo(partsMetadata,opts)iferr!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第5步:计算读取范围 ==========// 5.1 处理 Range 请求ifoffset<0{offset=0}iflength<=0{length=objInfo.Size-offset}ifoffset+length>objInfo.Size{length=objInfo.Size-offset}// 5.2 计算需要的分片neededShards,err:=e.getNeededShards(objInfo,offset,length,partsMetadata)iferr!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第6步:读取分片数据 ==========// 6.1 从磁盘读取分片varreaders[]io.Readerfor_,shard:=rangeneededShards{reader,err:=set.readShard(ctx,shard.Disk,shard.Offset,shard.Size)iferr!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}readers=append(readers,reader)}// 6.2 Reed-Solomon 解码decoded,err:=set.decode(ctx,readers)iferr!=nil{returnnil,ObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第7步:处理加密 ==========dataReader:=bytes.NewReader(decoded)ifopts.ServerSideEncryption!=nil{dataReader=opts.ServerSideEncryption.DecryptReader(dataReader)}// ========== 第8步:应用 Range 裁剪 ==========ifoffset>0{// 跳过 offset 字节dataReader=io.LimitReader(dataReader,length)}// ========== 第9步:返回结果 ==========return&readCloser{reader:dataReader},objInfo,nil}Range 请求处理
// cmd/erasure-object.go - Range 请求实现// Range 读取器typerangeReaderstruct{reader io.Reader offsetint64lengthint64donebool}func(r*rangeReader)Read(p[]byte)(nint,errerror){ifr.done{return0,io.EOF}// 跳过 offsetifr.offset>0{skip:=make([]byte,r.offset)n,err:=r.reader.Read(skip)iferr!=nil{r.done=truereturn0,err}r.offset-=int64(n)return0,nil}// 读取剩余长度ifr.length>0&&int64(len(p))>r.length{p=p[:r.length]}n,err=r.reader.Read(p)ifn>0{r.length-=int64(n)}ifr.length<=0||err==io.EOF{r.done=true}returnn,err}DeleteObject 深度分析
// cmd/erasure-object.go - DeleteObject 完整实现func(e*erasureObjects)DeleteObject(ctx context.Context,bucket,objectstring,opts ObjectOptions)(ObjectInfo,error){// ========== 第1步:前置验证 ==========if!e.bucketsExist(bucket){returnObjectInfo{},toObjectErr(errBucketNotFound,bucket)}// ========== 第2步:定位 Erasure Set ==========set,setIndex:=e.sets.getByIndex(bucket,object)ifset==nil{returnObjectInfo{},toObjectErr(errErasureSetNotFound,bucket,object)}// ========== 第3步:获取对象信息 ==========objInfo,err:=set.getObjectInfo(ctx,bucket,object)iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第4步:删除元数据 ==========iferr:=set.deleteMeta(ctx,bucket,object);err!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// ========== 第5步:删除分片 ==========// 5.1 从所有 Set 中删除对象allSets:=e.sets.getAllSets()for_,s:=rangeallSets{s.deleteShards(ctx,bucket,object)}// ========== 第6步:通知事件系统 ==========ifglobalEventNotifier!=nil{globalEventNotifier.Send(Event{Name:ObjectRemovedDelete,Bucket:bucket,Object:object,Version:objInfo.VersionID,})}returnobjInfo,nil}HeadObject 深度分析
// cmd/erasure-object.go - HeadObject 完整实现func(e*erasureObjects)HeadObject(ctx context.Context,bucket,objectstring,opts ObjectOptions)(ObjectInfo,error){// 1. 定位 Erasure Setset,setIndex:=e.sets.getByIndex(bucket,object)ifset==nil{returnObjectInfo{},toObjectErr(errErasureSetNotFound,bucket,object)}// 2. 读取元数据partsMetadata,err:=set.readAllMetadata(ctx,bucket,object)iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// 3. 验证对象健康iferr:=set.checkObject(ctx,bucket,object,partsMetadata);err!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}// 4. 返回对象信息objInfo,err:=partsMetadataGetObjectInfo(partsMetadata,opts)iferr!=nil{returnObjectInfo{},toObjectErr(err,bucket,object)}returnobjInfo,nil}小结
本文深入分析了 erasure-object.go 的核心操作:
- PutObject:验证 → Set定位 → 分片写入 → 元数据写入 → 原子重命名
- GetObject:Set定位 → 元数据读取 → 分片读取 → RS解码 → Range裁剪
- DeleteObject:验证 → 元数据删除 → 分片删除 → 事件通知
- HeadObject:Set定位 → 元数据读取 → 对象信息返回
每个操作都遵循相同的核心模式:Set 定位 → 元数据操作 → 分片操作。
下节预告
下节预告:分片上传——erasure-multipart.go 中 NewMultipartUpload 到 CompleteMultipartUpload 的完整实现。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~