早年写过一篇关于常用图床的介绍,时过境迁,连当时最稳定的微博图床都已经不能简单地正常使用了。后来改用了一段时间 Github 图床,虽然一共没几张图片,但总感觉这是在滥用 Github 提供的免费服务,正好最近在折腾其他项目,干脆就自建了一个图床服务。这里就借机讲讲自建图床的思路。

图床的主要功能是图片的持久化存储和外部通过链接访问,由于图片的大小通常在几十 KB 到几 MB 不等,因此我们必须着重考虑以下几个因素:

  1. 大容量的数据存储能力:至少要支持几十 GB 的容量以避免未来频繁迁移或者需要分片访问的麻烦。
  2. 安全稳定的存储环境:数据安全对于任何在线服务都是至关重要的,当然我们也可以通过定期备份的方式来曲线救国,但除非做镜像存储,否则备份总是不能保证全量数据的完整性的。
  3. 充足的带宽和流量:由于图片更容易被爬虫抓取或被非主观引用,访问量较大时需要消耗很可观的带宽,这种场景下 1M 的小水管类型的机器显然是无法满足需求的。同样,图片这种中型文件也需要足够的流量额度来支撑,这里要注意的是按量付费的流量计费方式是很危险的,起床发现欠了服务提供商一栋楼也不是没可能,提供了固定配额的流量包模式更适合图床的场景。另一方面,流量的费用也至关重要,这也是图床服务前期成本的主要组成部分。
  4. 较低的访问耗时:图片大多内嵌于文章,如果访问耗时较大(通常由于网络线路导致)是很影响阅读体验的。
  5. 攻击防御问题:由于图床的特殊性,DDOS 和 CC 的爱好者可能会比较喜欢发起攻击,所以攻击防御问题也是要考虑的点。
  6. 支持一定程度的自定义:自定义域名、URI ,便捷的上传方式和 HTTPS 的支持都是图床的标配。

在这几个因素限定的框架下,我们首先排除了直接使用云服务厂商的 VPS 或容器搭建图床的想法,因为国内友好线路的厂商的高配机器价格较高、流量和带宽较小,而未自定义的情况下硬盘大小在 20~50 GB 不等,容量较小。虽然加钱世界可得,但初期的公共图床或自用图床的建立成本应该有所控制才更理性。而且 VPS 没有完备的数据安全保障机制,宿主机宕机、忘记续费等等问题都可能会导致数据的丢失。

于是我将目光放到了近几年很火的对象存储上,对象存储服务数据可靠性非常高,大部分都是 S3 兼容,即使是国内的阿里云等等的 OSS 这类自定义 API 也比较友好,同时大部分厂商还支持 CDN 加速,是文件存储的理想选择。可惜的是一线厂商如阿里云、腾讯云、 AWS 和 GCP 的对象存储服务大多按量计费而且价格不低。还好我们熟悉的三家二线厂商Linode 、 Vultr 和 DigitalOcean 也有提供对象存储能力而且是大容量、高流量的套餐包形式的服务。考虑到 Vultr 目前只在新泽西机房提供服务,Linode 的注册验证很不友好,所以最终选择了 DigitalOcean Spaces 作为存储方案。

DigitalOcean Spaces 的最低套餐为 $5 每月,提供 250GB 的对象存储容量和 1TB 的流量,S3 API 兼容且提供了 CDN ,令人惊喜的是其还支持配置式地使用 LetsEncrypt 签发 SSL 证书以用于自定义的 CDN 域名。然而实际试验后发现其部分 CDN 节点由于众所周知的原因在国内无法访问,虽然刷新几次总能碰到可以用的节点,但使用于生产环境会十分影响用户体验。因此我们还需要增加一层支持缓存的反向代理,来自建一个简单的 CDN 节点,用户直接访问的是反向代理的服务器, DigitalOcean Spaces 只负责数据的存储和分发。在反向代理的选择上,由于目前只是单节点服务,所以我直接使用了 Nginx 配合其缓存指令进行简单处理。

另一方面,虽然我们可以使用 uPic 、PicGO 这类工具直接调用 S3 API 进行图片上传,但这只适合单个用户使用,多人使用的场景下 AccessKey 的安全管理成本、非技术人员的上手成本都很高,对外部开放使用更是无从谈起,故并不是个通用方案。 因此在图片的管理上我选择了 Chevereto 这个成熟组件。 Chevereto 当前的 3.X 版本有免费开源版本和付费版本之分,付费版本提供了外部存储(依托 S3 API )等功能的支持,可以直接操作 DigitalOcean Spaces 中的文件而避免使用本地磁盘,价格也很便宜只要差不多 20$ ,非常良心。

而在攻击防御层面,我们可以借助 CloudFlare 的免费防御,将域名 DNS 托管于其上以便能随时开启攻击保护。免费版 CloudFlare 在国内的访问速度较慢,虽然有魔改方式来指定使用 CF 的优良线路 IP ,但这其实是完全违反 TOS 的。

这里我们还可以借鉴 CQS 的思想进行优化,首先我们将图片的访问(读流量)和管理(写流量)分离至不同服务器。由于读请求直接访问的是很薄的一层 CDN 节点,故我们大可以在攻击前正常暴露真实 IP ,攻击发生后再通过更改 A 记录至备用机器并开启 CF 来保护新源站。而对于写流量,由于写逻辑是由依赖 DB 的 PHP 程序搭建的,重新部署的成本很高故不适合直接复用读流量的处理方案。好在 B 端用户的数量有限且更愿意付出时间来等待网页的加载,我们可以考虑牺牲一部分用户体验,对写流量全程开启 CF 防护来保护源站。

最终的架构图如下: image-server-structure