对象存储实战课程的要求是实现一个对象存储客户端,发现手头上只有Azure的服务可以用,所以就选择用Azure Blob存储服务来完成本次作业。
Blob存储
Azure Blob与课上提到的亚马逊S3、阿里云OSS、火山TOS类似,只不过把Bucket转换为了Container,而存储的对象改称为Blob,存储结构如下所示。
依据官方文档,Azure支持三种类型的Blob:
- 块 Blob 存储文本和二进制数据。 块 Blob 由可以分别管理的数据块构成。 块 blob 最多可存储约 190.7 TiB。
- 与块 Blob 一样,追加 Blob 也由块构成,但针对追加操作进行了优化。 追加 Blob 非常适用于诸如记录来自虚拟机的数据之类的场景。
- 页 Blob 用于存储最大 8 TiB 的随机访问文件。 页 blob 存储虚拟硬盘 (VHD) 文件并作为 Azure 虚拟机的磁盘。
Blob使用
Blob的申请过程就不在这里展开了,具体可以看Blob快速入门,下面采用Go SDK对Blob进行操作。官方示例参考
安装包
要使用Blob,需要安装azblob包以及Azure身份验证工具azidentity包。
go get github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
Azure身份验证
首先需要在Azure门户中为个人账户分配权限,在存储账户的访问控制选项卡中,为当前用户分配存储Blob数据参与者的角色。
接着需要进行身份验证,Azure的身份验证方法有很多,这里采用比较方便的Azure CLI进行验证登录,其他方法可以查看Azure for Go 身份验证
安装好Azure CLI后,在Power Shell中输入az login命令。
接着在Go中使用DefaultAzureCredential来验证权限,并创建一个Blob客户端
// TODO: replace <storage-account-name> with your actual storage account name
url := "https://<storage-account-name>.blob.core.windows.net/"
ctx := context.Background()
credential, _ := azidentity.NewDefaultAzureCredential(nil)
handleError(err)
client, err := azblob.NewClient(url, credential, nil)
handleError(err)
创建容器
containerName := "test-container"
client.CreateContainer(ctx, containerName, nil)
上传Blob至容器
SDK提供了三种上传方式字节数组、文件、输入流,对应着三个方法。
func (c *Client) UploadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *UploadBufferOptions) (UploadBufferResponse, error)
func (c *Client) UploadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *UploadFileOptions) (UploadFileResponse, error)
func (c *Client) UploadStream(ctx context.Context, containerName string, blobName string, body io.Reader, o *UploadStreamOptions) (UploadStreamResponse, error)
这里选择上传一个本地文件到容器中。
file, _ := os.Open("test.txt")
client.UploadFile(ctx, containerName, "test-blob.txt", file, &azblob.UploadFileOptions{})
运行结果
分块上传
块 Blob 由块组成,每个块可以是不同的大小,最大为 100MB,块 Blob 最多可以包含 50,000 块。因此,块 Blob 的最大大小约为 4.75 TB (100MB X 50,000 块)。在上传文件到Azure Blob存储时,支持两种方式,整体上传和分块上传。
要想使用分块上传,需要使用azblob中的blockblob包。大致思路是,使用想要使用的blob-name创建blockblob客户端,维护一个以Base64编码的块ID队列,将准备上传的文件分块读取(例如每次只读10MB)并分配一个Base64编码的块ID,接着使用StageBlock上传,每一块上传结束后将块ID加入队列,等待全部块上传结束后,使用CommitBlockList提交所有队列中的块。
// NOTE: The blockID must be <= 64 bytes and ALL blockIDs for the block must be the same length
blockIDBinaryToBase64 := func(blockID []byte) string { return base64.StdEncoding.EncodeToString(blockID) }
blockIDBase64ToBinary := func(blockID string) []byte { _binary, _ := base64.StdEncoding.DecodeString(blockID); return _binary }
// These helper functions convert an int block ID to a base-64 string and vice versa
blockIDIntToBase64 := func(blockID int) string {
binaryBlockID := &[4]byte{} // All block IDs are 4 bytes long
binary.LittleEndian.PutUint32(binaryBlockID[:], uint32(blockID))
return blockIDBinaryToBase64(binaryBlockID[:])
}
blockIDBase64ToInt := func(blockID string) int {
blockIDBase64ToBinary(blockID)
return int(binary.LittleEndian.Uint32(blockIDBase64ToBinary(blockID)))
}
bbClient, err := blockblob.NewClient(blobURL, cred, nil)
if err != nil {
return err
}
base64BlockIDs := make([]string, 0, 1024)
blockID := 0
// Block Size is 10MB
block := make([]byte, 1024*1024*10)
for n, _ := file.Read(block); n > 0; n, _ = file.Read(block) {
fmt.Printf("Put %v bytes\n", n)
base64BlockIDs = append(base64BlockIDs, blockIDIntToBase64(blockID))
_, err = bbClient.StageBlock(ctx, base64BlockIDs[blockID], streaming.NopCloser(bytes.NewReader(block[:n])), nil)
if err != nil {
return err
}
blockID++
}
_, err = bbClient.CommitBlockList(ctx, base64BlockIDs, nil)
if err != nil {
return err
}
// For the blob, show each block (ID and size) that is a committed part of it.
getBlock, err := bbClient.GetBlockList(ctx, blockblob.BlockListTypeAll, nil)
for _, block := range getBlock.BlockList.CommittedBlocks {
fmt.Printf("Block ID=%d, Size=%d\n", blockIDBase64ToInt(*block.Name), block.Size)
}
return err
下载Blob至本地
下载和上传一样提供了三种方式,这里同样选择用文件的形式下载。
file, _ := os.OpenFile("test-local.txt", os.O_WRONLY|os.O_CREATE, 0755)
client.DownloadFile(ctx, containerName, "test-blob.txt", file, &azblob.DownloadFileOptions{})
运行结果
删除Blob
client.DeleteBlob(ctx, containerName, "test-blob.txt", nil)
列出容器内所有Blob
pager := client.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
Include: azblob.ListBlobsInclude{Snapshots: true, Versions: true},
})
for pager.More() {
resp, err := pager.NextPage(ctx)
if err != nil {
return err
}
for _, blob := range resp.Segment.BlobItems {
fmt.Println(*blob.Name)
}
}
判断Blob是否存在
如果只想判断Blob是否存在,显然不需要获取整个Blob的数据内容,只需要采取HEAD的方式获取Blob的属性即可,为此需要使用azblob中的blob包,其专门针对指定容器中的blob进行操作。想要使用blob包同样需要创建对应的客户端,其url格式为https://<storage-account-name>.blob.core.windows.net/<container-name>/<blob-name>。
blobClient, _ := blob.NewClient(blobURL, cred, nil)
_, err = blobClient.GetProperties(ctx, &blob.GetPropertiesOptions{})
if err != nil {
// 不存在
}
// 存在