一)前言

  七牛云存储、阿里云OSS、腾讯云COS、华为云OBS等都是典型的对象存储服务,本篇文章会依次介绍一下我们在最近几年使用过程中遇到的问题,同时也介绍一下目前为止我们在项目中的一些实践经验(爬坑记录),或许对你有一些帮助。

二)数年经验积累

2.1 石器时代,一片混沌

  由于同一个程序需要多机部署,从14年开始我们的项目就不再把文件保存到服务器磁盘上了,而是统一上传到对象存储服务中,然后把拼接后的HTTP地址保存到数据中。因为当时连运维都没有,嫌麻烦所以直接使用的是七牛云给的域名,地址类似于这样的 “https://7xicvf.com1.z0.glb.clouddn.com/xxx.ext”。

  除了域名很丑之外,很长一段时间内都没出现问题,直到有一天早上我们的用户都在群里面反馈APP中的资源无法下载,我们一看原来是七牛云给的这个域名被停止解析了(后面了解到因为对应的一级域名涉及到“儿童涩情”,在全球范围内被停止解析),当时我们就懵了,数据库那么多URL太多了,涉及到各种表且分散在很多字段里面,看来快速在数据库中替换URL不现实了,还好我们用的是PHP框架,有一个统一的入口和出口,我们就在出口处进行了string replace替换,暂时解决了这个问题。

  经过这件事情后七牛云也强烈建议我们绑定自己的业务域名,而且在控制台也声明提供的域名只供测试,做了一些措施来防止误使用,例如会限制资源下载带宽、浏览器中访问资源地址是直接当成附件下载而不在浏览器中显示等手段。我们随后也就绑定了业务域名,并且在数据中进行了资源替换,但因为涉及到很多表、很多列,所以替换起来很费力气,这也在我心里埋下了一个种子,希望有一天能够给出一个比较完美的方案来解决遇到的这些问题。

2.2 青铜时代,暴露问题

  随着公司业务规模的扩大,为了折扣或者进行一些资源互换所以就得接入更多的服务商,虽然内心是抵触的,但还是得提供对应的支持才行,所以我在这个时候就接入了阿里云OSS存储。使用的方式还是一样,新建一个资源桶,然后绑定公司自己的业务域名,上传资源后就把拼接起来的URL(域名 + 路径)放到数据库中。

      随着团队的不断扩大,存储的资源场景就越来越多,有私有读的场景、也有公开读的场景,甚至很多时候干脆会为某个业务分配一个全新的桶,让大家爱怎么折腾就怎么折腾。如果大家的资源都放一个桶的话,当时又做不到在程序中对路径前缀强制隔离,有些操作就不方便,例如想备份某种非常重要的资源那就得把整个存储桶内容都备份才行,因为不容易区分哪些资源属于哪个业务的(让业务同学从数据库导出资源更麻烦,同样因为资源会放到很多表、不同列中)。另外由于涉及到很多桶,会绑定很多个业务域名,那前端对接的时候可能就要在后端设置很多跨域的域名配置(除非是设置为*,允许所有域名的跨域请求,但这会出现安全性的问题)。

铁器时代,初有想法

  针对上面的一些场景,总结此时遇到的问题:

存储桶绑定的业务如果发生更换。 因为数据库中写入的是拼接的HTTP URL(业务域名 + 存储桶中的路径),倘若业务域名由于某些原因被替换了,那就得替换数据库中所有的资源地址。
存储桶倘若要经常变换绑定域名。业务发展过程中会尝试很多种CDN服务商,每次更换CDN服务商就得在数据库中替换一次资源,虽然可以在网关处替换,但其实不推荐。
存储桶分配得太多难以管理。项目越来越多、人越来越复杂,为了避免互相干涉就只能不断分配新的桶,需要分配非常多的域名,管理起来非常累。

  针对上述的问题,我们进行尝试通过中间件的方式来解决,因此诞生了第一个版本的存储中间件,虽然后面也收到了很多吐槽,不过没关系,有了吐槽才会有后面不断完善的解决方案,存储中间件设计详情请看下文。

三)存储中间件

3.1 蒸汽时代

  其实中间件所有的内容都是围绕着我们自定义的存储协议展开的,存储协议的资源地址是这样的:oss://spaceKey/fileKey.ext,分为三部分内容:

oss,为固定的协议名称。
spaceKey,表示存储自定义存储空间,一个存储空间会绑定到某个服务商对象存储桶的某个路径下。
fileKey.ext 表示文件名(含扩展名),当然 fileKey 中也可以包含 “/”,因为在对象存储中并没有文件夹的概念,是模拟的。

   针对上面的协议,我们的中间件需要包含以下部分内容:

管控系统接口,供下面的存储组件SDK在内网调用。允许新增存储空间,并且关联到某个服务商某个存储桶的某个路径下,还可以为某个存储桶配置多个CDN域名。
存储组件SDK,供业务Web应用使用。服务端程序内部进行调用时都通过ossUrl(oss://spaceKey/fileKey.ext这种格式统称为OssUrl,下同)进行传递,只有在程序返回时才通过某种机制拦截ResponseBody并把其中的 ossUrl替换为CDN地址供用户访问;当然也要提供一个静态类把ossUrl转成最终的CDN地址,兼容一些奇怪的场景需求。

3.1.1 管控系统设计

  其中核心数据结构设计如案例如下,与我们实际业务代码不同,这里主要是表达含义即可:
项目中对象存储(OSS、COS、OBS、七牛云存储等)的实践经验-风君雪科技博客

  针对上面的数据结构相关的解释:

object_storage_space,表示存储业务定义的存储空间,目前有两种访问模式:1)公开读私有写2)私有读写,把 ossUrl 转换成CDN地址时会用到,如果是私有读地址则需要加上签名信息(需要指定失效时间)。
object_storage_vendor_bucket,表示云服务商的实际存储桶地址,会包含一些元数据信息,提供给存储组件SDK进行初始化吗,例如存储桶地域、Endpoint等。
object_storage_vendor_bucket_cdn,表示该服务商存储桶绑定的CDN的地址,例如提供一个腾讯云的CDN域名回源到阿里云的存储桶。priority表示优先级,倘若一个存储桶绑定了多个CDN域名可以进行优先级排序,在存储SDK解析CDN地址时可以用到。
object_storage_space_bucket,表示业务自定义存储空间关联到某个存储服务商存储桶的某个路径下,不同空间关联不同路径可以防止互相干涉。

3.1.2 存储组件设计

项目中对象存储(OSS、COS、OBS、七牛云存储等)的实践经验-风君雪科技博客

  针对上面的架构相关的解释:

存储SDK通过管控系统的元数据信息(配置的存储空间信息)来初始化对应云服务商的SDK执行文件上传。
存储SDK可以通过检测ResponseBody(我们项目是SpringBoot为基础框架的)中的OSS URL转换为CDN地址供用户访问。

  

四)经验总结

  当然我们的业务系统会更加复杂一些,例如通过processor参数来适配不同云存储的数据处理功能,无论使用哪家云存储都可以提供一致的图片处理方案(例如裁剪、水印、旋转等)。而且也遇到了一些比较尴尬的坑,有各端配合上的、有知识短板导致方面的,这些坑不方便在这里写下来,如有需要后面我会单独脱敏后记录下来。