进可攻退可守的存储解决方案

背景

掐指一算,已经离职创业一个月了。前些天在给创业公司寻找存储的解决方案,花了几天的时间把 MinIO 和 S3 相关文档深入了解了下,总结出一个进可攻退可守的存储解决方案。在此将一些重点列一下,算是给自己备忘,也算是抛砖引玉。

对象存储

说到存储,不得不提到最近非常火热的「对象存储」,英文全称为 Object Storage Service,简称 OSS。你可以把 OSS 简单理解为大型的文件键值对系统,区别于一般的键值对系统,OSS 存储的是文件。

最早做对象存储的应当是 Amazon Cloud,其对象存储服务通常被称为 Amazon S3,或简称 S3。后面做对象存储的基本都是在「山寨」S3,包括阿里、腾讯等云厂商。更普遍的证据在于,基本上所有的 OSS 服务商、以及相关的软件都在努力朝「兼容 S3」方向发展。此处应有一句「S3 NB」。

对象存储相比于块存储、文件存储,封装性更强,也非常满足我司存储图片、视频的需求,因此存储选型的大方向基本敲定了它。至于对象存储的更多解释,以及对象存储和其它存储的详细对比,此处不展开介绍,感兴趣可自行谷歌。

何谓进可攻退可守

看标题中的「进可攻退可守」,你可能会好奇什么是进可攻退可守?为何要进可攻退可守?

做软件开发的应该都知道扩展性的重要性,在选择技术方案的时候尤其要注意,千万不要因为仓促走了一条路,让自己无路可走。

初创企业因为资金有限、设备有限,没有自己的机房,可能也没有多好的服务器。这种情况下,我们多半是要使用第三方云厂商提供的对象存储服务,比如阿里云、腾讯云之类。

企业正常运转之后,考虑到数据的安全性以及长期成本等因素,多半会考虑自建分布式存储服务。说到这里,如果你想到迁移成本的问题,那么你已经在成为架构师的路上了。

为了降低迁移带来的开发成本,在使用第三方 OSS 时,应当尽量解耦,比如用一个「无公害」的第三方服务,对阿里云 OSS 进行代理和中转。那么谁能担当此大任呢?

MinIO

有这么一个团队,他们开发出了一款开源软件,可以用于快速部署私有的对象存储服务,具有高性能、高扩展性、安全可靠、兼容 S3、分布式友好等特点。它就是我们实现「进可攻退可守的关键」—— MinIO

Build high performance data infrastructure for machine learning, analytics and application data workloads with MinIO

MinIO 有 servergateway 两种运行模式,前者适用于有自己的硬盘等存储硬件设备的情况,后者可以理解为代理模式,目前可以代理 Azure、GCS、NAS、S3 以及其它兼容 S3 的 OSS 服务商。

代理模式下,MinIO 对外部请求进行解析和转发至被代理方(比如阿里云 OSS),间接提供存储服务。当然,MinIO 不只是做了请求转发,还提供了多种额外的功能,比如文件缓存、服务监控等。

使用 MinIO 提供对象存储服务,创业初期运行在 gateway 模式,后期迁移时,不需要改调用方的代码,只需要将数据从第三方云厂商迁移至自有设备即可,由此实现了「进可攻退可守」。关于 MinIO 的更多介绍,可以看 官方文档 以及 官方代码仓库,此处不啰嗦。

文件保护

说到文件存储,就会涉及到文件的上传与下载问题,尤其是其中的权限问题。对于一个非开放式的存储系统而言,不应该让所有人都可以随意访问或上传文件。MinIO 或者说 S3 提供两种方式,对文件进行保护。

一种是创建权限有限的用户,创建时设置其相关权限,创建后会生成相应的 KEYSECRET,用户使用合法的密钥信息读取和上传文件。一种是根据用户的访问意图,创建预先签名的临时的 URL,用户拿着该 URL 拥有对资源的临时上传下载权。

如果说访问策略简单,不需要创建过多的用户,同时不担心密钥泄露的问题,可以采用第一种方式,MinIO 也提供了完善的 JS SDK,可以方便地实现文件访问、小文件上传、大文件分片上传的功能。

需要注意的是,如果 MinIO 运行在 gateway 模式,需要配合 etcd 的使用,才可以创建多用户。详情可以查看 MinIO Multi-user Quickstart Guide

如果访问策略无法固化,而且站点用户数量庞大,或者希望尽可能轻量化 MinIO,则应该考虑第二种方式。根据实现不同,可以分为以下几个场景:

  • 访问文件:后端根据文件所在的存储桶 Bucket 和文件名 Key,按照 S3 签名过程生成临时访问链接给用户
  • 上传文件:
    • 小文件:后端根据存储桶和文件名,按照 S3 签名过程生成临时上传链接给用户,用户使用 PUT 方法,将文件上传至相应链接
    • 大文件:
      • 后端根据存储桶和文件名,向 MinIO 发起创建分片上传的请求,获取到 UploadId
      • 根据 UploadId 和分片数量生成分片上传的 URL
      • 用户对文件分片、计算每片的 Content-MD5 值,通过分片对应的 URL 使用 PUT 方式上传
      • 用户告诉后端所有分片上传完成后,后端向 MinIO 发合并分片请求,结束本次分片上传

此处有坑

如果你恰好是用 Laravel 开发,或者以下内容会对你有些帮助,否则可以跳过。

Laravel 使用 MinIO

How to use MinIO Server as Laravel Custom File Storage

创建预签名 URL

使用的 aws-dk-php 库,可以参考官方文档。话不多说,直接上示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/* @var S3Client $storageClient */
$storageClient = Storage::disk('minio')->getAdapter()->getClient();

// 生成临时访问 URL
$storageClient->createPresignedRequest(
$storageClient->getCommand('GetObject', [
'Bucket' => $resource->bucket,
'Key' => $resource->save_name
]),
$expires
)->getUri()->__toString();

// 生成小文件的临时上传 URL
$putUrl = $storageClient->createPresignedRequest(
$storageClient->getCommand('PutObject', [
'Bucket' => $resource->bucket,
'Key' => $resource->save_name
]),
$expires
)->getUri()->__toString();

// 创建分片上传
$multipartResult = $storageClient->createMultipartUpload([
'Bucket' => $resource->bucket,
'Key' => $resource->save_name
]);

// 创建单片上传的 URL
$partUrl = $storageClient->createPresignedRequest(
$storageClient->getCommand('UploadPart', [
'Bucket' => $resource->bucket,
'Key' => $resource->save_name,
'UploadId' => $resource->upload_id,
'PartNumber' => $i,
]),
$expires
)->getUri()->__toString();

// 获取已经上传的分片
$uploadedParts = $storageClient->listParts([
'Bucket' => $resource->bucket,
'Key' => $resource->save_name,
'UploadId' => $resource->upload_id
])->get('Parts');

// 完成分片上传
$storageClient->completeMultipartUpload([
'Bucket' => $resource->bucket,
'Key' => $resource->save_name,
'UploadId' => $resource->upload_id,
'MultipartUpload' => [
'Parts' => array_map(function ($item) {
return [
'ETag' => $item['ETag'],
'PartNumber' => $item['PartNumber']
];
}, $uploadedParts)
],
]);

// 取消分片上传
$abortResult = $storageClient->abortMultipartUpload([
'Bucket' => $resource->bucket,
'Key' => $resource->save_name,
'Requester' => 'requester',
'UploadId' => $resource->upload_id,
]);

Content-MD5 小坑

为保证上传的准确性,可以在上传时提供 Content-MD5 头部,MinIO 会对上传内容和提供的头部进行校验,只有校验通过才会保留并返回 200 的状态码。

但是,此处提到的 Content-MD5 与 MD5 值不同,是指将文件的 MD5 值的二进制表示通过 Base64 编码而来。如果不确定自己的实现方式是否有误,可以利用 这个网站 作对比。

小结

嗯,总的来说,如果你想使用对象存储,但又不希望过度依赖阿里云等云服务厂商,希望以后迁移成本较低,那么 MinIO 是一个不错的选择。

参考资料

  • 文件系统vs对象存储——选型和趋势
  • 什么是云对象存储?
  • 对象存储概述
  • Amazon S3
  • 块存储/文件存储/对象存储的本质差别
  • The Content-MD5 Header Field
  • 使用 Content-MD5 标头和 MD5 校验和
  • How to use MinIO in Laravel
  • AWS SDK PHP Document
  • Upload Part - Amazon Simple Storage