一、理论介绍
1.对象存储:
在开发过程中由于数据有公共存储需求,存储需求量需要细化,同时存储量会随着用户增多变得巨大,就需要选择易用、海量、便宜的存储系统;由于数据量庞大,单机存储和单机数据库显然不能满足要求,而分布式数据库面向的是结构化数据,适用于存储管理具有多种数据的对象信息,这些信息都是KB级别,超过MB也会显得很吃力,因此需要选择专门针对海量数据设计的分布式存储;
分布式存储又具有分布式文件系统和对象存储之分,分布式文件系统能够存储PB到EB级别的海量数据,但是基于文件系统,文件数受NameNode限制,并且使用伪Posix文件接口,非云原生,开发难度大,搭建维护难,同时对于视频图片等大型文件相关生态差,接入复杂;对象存储能支持EB级别以上的海量数据,对象数量没有限制,使用Restful HTTP接口,云原生,开发使用简单,视频图片等的相关生态丰富,同时价格会更加低廉,因此对象存储方式是视频图片等海量存储的首选;
对象存储用于以高度可伸缩的方式存储和检索大量非结构化数据,例如文档、图像、音频和视频文件等,它不依赖于传统的文件系统层次结构或块级存储卷,它将数据存储为对象,每个对象包含数据、元数据(描述对象的信息)以及一个唯一标识符(通常称为对象键或对象ID),由于对象存储使用分布式存储,数据分布在多个物理位置,保证了数据的可靠性和持久化,同时对象存储通常使用 HTTP 或 HTTPS 协议来访问和检索数据,数据操作方便。
2.阿里云OSS:
链接(有新人3个月免费试用活动): 对象存储 OSS_云存储服务_企业数据管理_存储-阿里云 (aliyun.com)
阿里云对象存储服务(Alibaba Cloud Object Storage Service,简称OSS),是阿里云提供的高度可扩展的云端对象存储服务,提供了多语言SDK(OSS管理控制台 (aliyun.com)),提供了丰富的OSS操作接口,能够通过SDK自带函数直接实现上传、下载、列举、删除等操作,同时支持数据迁移、数据加密、访问控制、数据生命周期管理等操作,使用非常方便,可以支持大文件的分片上传和断点续传,性能高效;附带详细的文档和示例代码(Go SDK文档:OSSGoSDK示例代码_对象存储 OSS-阿里云帮助中心 (aliyun.com)),并且云原生集成,能够很好的与阿里云的其他云服务集成使用,也支持跨平台使用。
3.实战需求:
使用Go语言连接阿里云OSS实现一个简单的命令行存储对象客户端,要求能够实现以下功能:
- 创建对象:创建时超过1GB的对象需要使用
MultiUpload
即分片上传的方式,小于1GB的用普通的PUT
; - 下载对象;
- 删除对象;
- 查看对象是否存在;
- 列举对象;
- 列举CommonPrefix:
CommonPrefixes
表示与给定前缀匹配的对象键的一部分,是前缀下的子目录或对象键的集合,是一个用于组织对象的元数据字段,通常用于构建虚拟目录结构。
二、实战过程
1.申请对象存储Bucket:
购买或试用成功后,进入控制台选择对象存储OSS,选择Bucket列表,点击创建Bucket,填入Bucket名称,按自己的需求选择其他附加服务后即可成功创建:
进入Bucket后可以看到文件列表,可以直接通过该界面进行一些数据交互和属性设置:
申请完Bucket后还需要创建AccessKey即云服务访问密钥,以供自己的程序可以访问该OSS,创建后会显示
AccessKey ID
和AccessKey Secret
,需要自己记录AccessKey Secret
以防丢失;
2.Golang连接阿里云OSS:
首先下载并导入Go SDK:
源码地址:aliyun/aliyun-oss-go-sdk: Aliyun OSS SDK for Go (github.com)
下载指令:go get github.com/aliyun/aliyun-oss-go-sdk
导入SDK包:import "github.com/aliyun/aliyun-oss-go-sdk/oss"
代码中需要先设置四个字段:
accessKeyID
:密钥ID;accessKeySecret
:密钥内容;endpoint
: 阿里云OSS服务的访问终端点,指定了要连接的OSS服务的位置;如我创建Bucket时选择了华北2(北京),就需要填写为oss-cn-beijing.aliyuncs.com
;bucketName
:设置的Bucket容器名称;
接下来连接到阿里云OSS的客户端(使用oss.New
函数),然后创建存储桶对象的引用(使用Bucket
函数):
func main() {
accessKeyID := ""
accessKeySecret := ""
endpoint := "oss-cn-beijing.aliyuncs.com"
bucketName := "l-test-bucket"
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
fmt.Println("创建OSS客户端时发生错误:", err)
return
}
bucket, err := client.Bucket(bucketName)
if err != nil {
fmt.Println("创建存储桶对象时发生错误:", err)
return
}
}
3.上传对象:
阿里云OSS提供了多种上传文件的接口函数,文档也提供了很多示例代码:
文档地址:aliyun-oss-go-sdk/sample/put_object.go at master · aliyun/aliyun-oss-go-sdk (github.com)
常用接口函数:
bucket.PutObject(objectKey string, reader io.Reader, options ...oss.Option)
:用于将对象上传到OSS存储桶。可以使用不同的options
来设置对象的属性,如ACL(访问控制)、过期时间、元数据等;bucket.PutObjectFromFile(objectKey string, filePath string, options ...oss.Option)
:用于从本地文件上传对象到OSS存储桶。可以使用options
设置对象的属性;bucket.UploadFile(objectKey string, filePath string, partSize int64, options ...oss.Option)
:用于大文件的分片上传到 OSS 存储桶,支持并发上传和断点续传。
普通对象上传:
选择使用bucket.PutObjectFromFile
函数,上传单个文件到存储桶,并且设置文件的读写权限为公共读写;
func uploadToOSS(bucket *oss.Bucket, objectName, localFilePath string) {
//上传本地文件到OSS并设置公共读权限
err := bucket.PutObjectFromFile(objectName, localFilePath, oss.ObjectACL(oss.ACLPublicRead))
if err != nil {
fmt.Errorf("上传文件到OSS时发生错误: %v", err)
}
fmt.Println("文件已成功上传到OSS")
}
大文件 MultiUpload
分片上传:
按三个步骤依次进行分片上传:
InitiateMultipartUpload(objectName)
:初始化分片上传事件,返回一个分片上传事件的标识符,告诉OSS服务将要开启一个新的分片上传;UploadPart(imur, fd, chunk.Size, chunk.Number)
,将分片逐个上传到OSS桶;CompleteMultipartUpload(imur, parts, oss.GetResponseHeader(&retHeader))
:完成分片上传,告诉OSS已经完成所有分片上传,并将获取的分片组合成完整的对象,返回相关的元数据;
var retHeader http.Header
chunks, err := oss.SplitFileByPartNum(localFilePath, 3)
fd, err := os.Open(localFilePath)
defer fd.Close()
// 步骤1:初始化一个分片上传事件。
imur, err := bucket.InitiateMultipartUpload(objectName)
// 步骤2:上传分片。
var parts []oss.UploadPart
for _, chunk := range chunks {
fd.Seek(chunk.Offset, io.SeekStart)
// 对每个分片调用UploadPart方法上传。
part, err := bucket.UploadPart(imur, fd, chunk.Size, chunk.Number)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
parts = append(parts, part)
}
// 步骤3:完成分片上传。
cmur, err := bucket.CompleteMultipartUpload(imur, parts, oss.GetResponseHeader(&retHeader))
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
fmt.Println("cmur:", cmur)
// 打印x-oss-version-id。
fmt.Println("x-oss-version-id:", oss.GetVersionId(retHeader))
fmt.Println("文件已成功通过分片方式上传到OSS")
区分大文件和普通文件:
需求要求的是1GB以上的文件需要采用分片上传,就需要上传文件时判断文件的大小再选择合适的方法进行上传,Golang提供了os.Stat
的方法获取获取文件的文件信息,返回一个FileInfo
结构和error,FileInfo
结构包含了关于文件的各种信息:
Name
:返回文件的名称(不包括路径);Size
:返回文件的大小(字节数);Mode
:返回文件的权限和模式;ModTime
:返回文件的修改时间;IsDir
:检查文件是否为目录;
因此通过上述的Size
函数可以获取到文件的字节数,而1GB等于2^30字节,因此使用math.Pow
函数maxsize := int64(math.Pow(2, 30))
可以表示1GB的字节数,转换为int64类型是由于Size
函数是int64类型,为了进行比较需要转换为同类型,利用这些方法就可以实现文件根据不同大小选择不同方式上传;
最后完整的上传函数为:
func uploadToOSS(bucket *oss.Bucket, objectName, localFilePath string) {
maxsize := int64(math.Pow(2, 30))
file, err := os.Stat(localFilePath)
if err != nil {
fmt.Println("打开文件错误,请检查文件路径")
return
}
size := file.Size()
if size <= maxsize {
// 上传本地文件到OSS并设置公共读权限
err = bucket.PutObjectFromFile(objectName, localFilePath, oss.ObjectACL(oss.ACLPublicRead))
if err != nil {
fmt.Errorf("上传文件到OSS时发生错误: %v", err)
}
fmt.Println("文件已成功上传到OSS")
} else {
// 用oss.GetResponseHeader获取返回header。
var retHeader http.Header
chunks, err := oss.SplitFileByPartNum(localFilePath, 3)
fd, err := os.Open(localFilePath)
defer fd.Close()
// 步骤1:初始化一个分片上传事件。
imur, err := bucket.InitiateMultipartUpload(objectName)
// 步骤2:上传分片。
var parts []oss.UploadPart
for _, chunk := range chunks {
fd.Seek(chunk.Offset, io.SeekStart)
// 对每个分片调用UploadPart方法上传。
part, err := bucket.UploadPart(imur, fd, chunk.Size, chunk.Number)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
parts = append(parts, part)
}
// 步骤3:完成分片上传。
cmur, err := bucket.CompleteMultipartUpload(imur, parts, oss.GetResponseHeader(&retHeader))
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
fmt.Println("cmur:", cmur)
fmt.Println("文件已成功通过分片方式上传到OSS")
}
}
4.下载对象:
文档地址:aliyun-oss-go-sdk/sample/get_object.go at master · aliyun/aliyun-oss-go-sdk (github.com)
常用接口函数:
bucket.GetObject(objectKey)
:该函数用于以流式方式下载对象,返回一个ReadCloser
,需要在下载数据后进行读取和关闭;bucket.GetObject(objectKey, oss.Range(15, 19))
:该函数用于下载对象的特定范围,同样返回一个ReadCloser
;bucket.GetObject(objectKey)
和io.Copy(buf, body)
:这个组合用于将对象的内容下载到字节缓冲区中;bucket.GetObject(objectKey)
和io.Copy(fd, body)
:这个组合用于下载对象并保存到由文件描述符fd
指定的本地文件中;bucket.GetObjectToFile(objectKey, "mynewfile-2.jpg")
:该函数用于将对象直接下载到指定文件名的本地文件中;bucket.DownloadFile(objectKey, "mynewfile-3.jpg", ...)
:该函数用于对大文件进行分块下载,支持并行和可恢复下载,使用不同参数的函数变体来控制下载行为,包括分片大小、协程数量和检查点;bucket.GetObjectToFile(objectKey, "myhtml.gzip", oss.AcceptEncoding("gzip"))
:该函数用于下载使用GZIP编码的文件。
为了更好的实现命令行客户端交互,选择使用GetObjectToFile
函数,将对象下载到用户指定的文件中;
func downloadFromOSS(bucket *oss.Bucket, objectName, localFilePath string) {
//从OSS下载文件到本地
err := bucket.GetObjectToFile(objectName, localFilePath)
if err != nil {
fmt.Errorf("从OSS下载时发生错误:%v", err)
}
fmt.Println("文件已成功下载")
}
5.删除对象:
文档地址:aliyun-oss-go-sdk/sample/delete_object.go at master · aliyun/aliyun-oss-go-sdk (github.com)
常用接口函数:
bucket.DeleteObject(objectKey)
:该函数用于删除单个对象;bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"})
:该函数用于删除多个对象,可以传入对象的键的列表;bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"},oss.DeleteObjectsQuiet(false))
:该函数用于删除多个对象,并以详细模式返回已删除的对象信息;bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"},oss.DeleteObjectsQuiet(true))
:该函数用于删除多个对象,并以静默模式返回已删除的对象信息。
使用单个删除的方式,删除用户指定的对象:
func deleteFromOSS(bucket *oss.Bucket, objectName string) {
err := bucket.DeleteObject(objectName)
if err != nil {
fmt.Errorf("删除对象发生错误:%v", err)
}
fmt.Println("对象已删除")
}
6.查看对象是否存在:
SDK直接提供了判断对象是否存在的接口bucket.IsObjectExist(objectName)
,只需要提供对象名称,存在则返回true
,不存在返回false
;
func isExist(bucket *oss.Bucket, objectName string) {
exists, err := bucket.IsObjectExist(objectName)
if err != nil {
log.Fatal("查看对象是否存在时发生错误:", err)
}
if exists {
fmt.Println("对象存在")
} else {
fmt.Println("对象不存在")
}
}
7.列举对象:
文档地址:aliyun-oss-go-sdk/sample/delete_object.go at master · aliyun/aliyun-oss-go-sdk (github.com)
常用接口函数:
bucket.ListObjects()
: 使用默认参数列举对象。它返回oss.ListObjectsResult
结构,该结构包含以下信息:IsTruncated (bool)
: 表示是否还有更多的对象需要列举,如果为true
,则表示还有未列举的对象;NextMarker (string)
: 如果列表被截断,这个字段会包含一个标记,用于指示从哪个对象键开始继续列举;Objects ([]oss.ObjectProperties)
: 包含列举的对象的属性列表,每个对象的属性包括对象键Key
等信息;CommonPrefixes ([]string)
: 如果使用分隔符进行分组列举对象,这个字段包含常见的前缀列表,用于指示存储桶中存在的共同前缀,比如目录结构;Prefix (string)
: 如果使用前缀进行过滤列举对象,这个字段会返回所使用的前缀;Marker (string)
: 用于指示从哪个对象键开始列举的标记;Delimiter (string)
: 如果使用分隔符进行分组列举对象,这个字段会返回所使用的分隔符;
bucket.ListObjects(oss.MaxKeys(3))
: 指定最大返回的对象数,以列举对象;bucket.ListObjects(oss.Prefix("my-object-2"))
: 指定对象键的前缀,以列举以该前缀开头的对象;bucket.ListObjects(oss.Marker("my-object-22"))
: 指定一个标记,以从标记位置开始列举对象;bucket.ListObjects(oss.MaxKeys(3), marker)
: 用于分页列举对象,每页返回指定数量的对象,并使用进行分页;bucket.ListObjects(oss.MaxKeys(3), marker)
: 用于分页列举对象,同时指定一个标记,以从标记位置开始列举对象,并每页返回指定数量的对象;bucket.ListObjects(oss.MaxKeys(2), marker, pre)
: 用于分页列举对象,同时指定前缀和最大返回对象数,以分页列举对象;bucket.ListObjects(oss.Prefix("fun/"), oss.Delimiter("/"))
: 使用前缀和分隔符进行分组列举对象,返回匹配前缀的对象以及常见的前缀;
func listObject(bucket *oss.Bucket) {
// 列举对象
objectList, err := bucket.ListObjects()
if err != nil {
log.Fatal("列举对象时发生错误:", err)
}
fmt.Println("对象列表:")
for _, obj := range objectList.Objects {
fmt.Println(obj.Key)
}
}
8.列举CommonPrefixes:
使用上述列举对象中的函数bucket.ListObjects(oss.Prefix("fun/"), oss.Delimiter("/"))
列举CommonPrefixes
;
func GetCommonPrefixes(bucket *oss.Bucket, prefix string) {
// 获取CommonPrefixes
commonPrefixes, err := bucket.ListObjects(oss.Prefix(prefix))
if err != nil {
log.Fatal("获取CommonPrefixes时发生错误:", err)
}
fmt.Println("CommonPrefixes:")
for _, ob := range commonPrefixes.Objects {
fmt.Println(ob.Key)
}
}
9.命令行客户端实现:
在主函数中输出提示用户选择所需操作,利用switch
语句获取选择并提示用户输入所需参数,调用相应的函数进行响应:
func main() {
accessKeyID := ""
accessKeySecret := ""
endpoint := "oss-cn-beijing.aliyuncs.com"
bucketName := "l-test-bucket"
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
fmt.Println("创建OSS客户端时发生错误:", err)
return
}
bucket, err := client.Bucket(bucketName)
if err != nil {
fmt.Println("创建存储桶对象时发生错误:", err)
return
}
for {
fmt.Println("----------------------")
fmt.Println("请选择操作:")
fmt.Println("1. 上传文件到OSS")
fmt.Println("2. 从OSS下载文件")
fmt.Println("3. 从OSS删除文件")
fmt.Println("4. 检查对象是否存在")
fmt.Println("5. 列举OSS对象")
fmt.Println("6. 获取CommonPrefixes")
fmt.Println("0. 退出")
var choice int
fmt.Print("输入操作编号: ")
_, err := fmt.Scan(&choice)
if err != nil {
fmt.Println("无效的输入:", err)
continue
}
switch choice {
case 1:
fmt.Print("请输入本地文件路径: ")
var localFilePath string
fmt.Scan(&localFilePath)
fmt.Print("请输入要上传到OSS的对象名称: ")
var objectName string
fmt.Scan(&objectName)
uploadToOSS(bucket, objectName, localFilePath)
case 2:
fmt.Print("请输入要从OSS下载的对象名称: ")
var objectName string
fmt.Scan(&objectName)
fmt.Print("请输入保存到的本地文件路径: ")
var localFilePath string
fmt.Scan(&localFilePath)
downloadFromOSS(bucket, objectName, localFilePath)
case 3:
fmt.Print("请输入要从OSS删除的对象名称: ")
var objectName string
fmt.Scan(&objectName)
deleteFromOSS(bucket, objectName)
case 4:
fmt.Print("请输入要检查的对象名称: ")
var objectName string
fmt.Scan(&objectName)
isExist(bucket, objectName)
case 5:
listObject(bucket)
case 6:
fmt.Print("请输入要获取CommonPrefixes的前缀: ")
var prefix string
fmt.Scan(&prefix)
GetCommonPrefixes(bucket, prefix)
case 0:
fmt.Println("程序退出")
os.Exit(0)
default:
fmt.Println("无效的选择,请重新输入")
}
}
}
10.运行情况:
上传操作:
- 小于1GB上传:
- 由于没有比较合适的1GB大小的文件进行测试,将代码改成了大于20MB就分片上传进行测试:
- 在上传过程中,打开控制台的碎片管理可以看到分片情况:
下载操作:
删除操作:
查看对象是否存在:
列举对象:
不仅可以列举出对象还能列举出创建的目录:
列举CommonPrefixes:
可列举出相同前缀的目录和对象,目录内的相同前缀对象也会被列举出来,但是非相同前缀的目录,即使其中有相同前缀的对象,也无法被列举:
三、总结
由于阿里OSS的SDK的完备,整体实现方式并不复杂,很多操作都能直接通过SDK提供的函数直接实现;不过通过这样一个实践,让我更深入的了解了一个对象存储系统需要实现的操作,了解到其内部很类似于key-value存储的实现,每个数据对象都有一个便于访问的key字段也就是对象名称;也感受到对象存储对海量数据的支持,上传比较大的视频图片,通过分片等方式能够快速的将数据持久化,同时也自己按步骤实现了MultiUpload
的“闪电三连鞭”,了解分片上传的流程;
在实践过程中也可以看到,不论通过控制台还是程序上传的文件,上传成功后都会自动生成HTTP/HTTPS协议的文件URL,访问数据对象非常方便,让我想到可以在大项目中使用OSS存储视频图片,数据库保存URL,访问时直接通过OSS访问数据,这样可以避免使用静态资源访问,上传和访问的速度应该也会有很大的提升;
通过实践学习到了很多对象存储的使用方法,虽然只针对了阿里云OSS,但是可以肯定,无论是什么对象存储系统,需要的操作都是大同小异,都只会在用法细节和实现细节上有所区别,整体操作肯定是没有差别的,整理操作方法并实现这样的一个简单OSS客户端还是很有意义的。