Cloudflare R2 + 香港服务器混合存储方案:零出口流量费CDN分发与成本优化
存储需求分两类:高频访问的静态资源(产品图、JS/CSS),和低频访问的归档数据(备份、历史日志)。把所有数据都放同一地方,要么贵(AWS S3 出口流量费),要么慢(本地服务器全球访问延迟高)。本文构建一套混合存储架构:热数据放 Cloudflare R2(零出口流量费 + 全球 CDN),冷数据放香港服务器本地,按数据温度分层存储,将存储总成本降低 60% 以上。
一、存储方案成本横向对比
| 方案 | 存储费(1TB/月) | 出口流量费(1TB/月) | 全球访问速度 |
|---|---|---|---|
| AWS S3 标准 | $23 | $90 | 优秀 |
| 阿里云 OSS(香港) | ¥130 | ¥0.5/GB(¥500) | 良好 |
| Cloudflare R2 | $15(超出10GB免费后) | $0(永久免费!) | 优秀(CDN) |
| 香港 VPS 本地(MinIO) | 磁盘费用分摊(约¥50) | 含在 VPS 套餐 | 良好(亚太) |
| R2 + VPS 混合 | $15+¥50 | $0 | 优秀 |
二、混合存储架构设计
<code">
数据分层策略:
热数据(R2 + Cloudflare CDN)
├── 产品图片(高频全球访问)
├── 前端静态资源(JS/CSS/字体)
├── 用户上传的公开文件
└── 营销素材
冷/温数据(香港 VPS 本地)
├── 数据库备份(每日)
├── 应用日志(30天保留)
├── 开发资源(私有不公开)
└── 超大文件(视频原始素材)
访问路径:
用户 → Cloudflare CDN(全球边缘)→ R2 存储(热数据,近乎零延迟)
用户 → 香港服务器(冷数据,需要认证的私有文件)
三、创建 Cloudflare R2 Bucket
<code"> 在 Cloudflare Dashboard 操作: 1. 登录 dashboard.cloudflare.com 2. 左侧菜单 → R2 Object Storage 3. Create bucket - Bucket name: myapp-assets - Location: Automatic(Cloudflare 自动选择最近节点) 4. 创建 API Token(用于程序访问) R2 → Manage R2 API tokens → Create API token - Permissions: Object Read & Write - Specify bucket: myapp-assets 复制:Account ID / Access Key ID / Secret Access Key 5. 设置自定义域名(让图片通过 CDN 分发) Bucket → Settings → Custom Domains → 添加 assets.yourdomain.com (在 Cloudflare DNS 中会自动创建 CNAME 记录)
四、Python 代码:智能存储路由
<code">import boto3
import os
from botocore.config import Config
from enum import Enum
class StorageTier(Enum):
HOT = "r2" # 高频访问,R2 存储
COLD = "local" # 低频访问,本地 VPS
# R2 客户端(S3 兼容 API)
r2 = boto3.client(
's3',
endpoint_url=f'https://{os.getenv("CF_ACCOUNT_ID")}.r2.cloudflarestorage.com',
aws_access_key_id=os.getenv('R2_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('R2_SECRET_ACCESS_KEY'),
config=Config(signature_version='s3v4'),
region_name='auto'
)
# 本地 MinIO 客户端(香港服务器)
local = boto3.client(
's3',
endpoint_url='https://storage.yourdomain.com',
aws_access_key_id=os.getenv('MINIO_ACCESS_KEY'),
aws_secret_access_key=os.getenv('MINIO_SECRET_KEY'),
config=Config(signature_version='s3v4'),
region_name='us-east-1'
)
def upload_file(
file_path: str,
object_key: str,
tier: StorageTier = StorageTier.HOT,
content_type: str = 'application/octet-stream',
public: bool = False
) -> str:
"""上传文件到对应存储层,返回访问 URL"""
extra_args = {
'ContentType': content_type,
'CacheControl': 'public, max-age=31536000' if public else 'private'
}
if public:
extra_args['ACL'] = 'public-read'
if tier == StorageTier.HOT:
r2.upload_file(file_path, 'myapp-assets', object_key, ExtraArgs=extra_args)
return f"https://assets.yourdomain.com/{object_key}" # CDN URL
else:
local.upload_file(file_path, 'backups', object_key, ExtraArgs=extra_args)
return f"https://storage.yourdomain.com/backups/{object_key}"
def smart_upload(file_path: str, object_key: str, file_size_mb: float) -> str:
"""根据文件特征自动选择存储层"""
ext = os.path.splitext(file_path)[1].lower()
# 图片/静态资源:热存储(CDN 分发)
if ext in ['.jpg', '.jpeg', '.png', '.webp', '.svg', '.css', '.js', '.woff2']:
return upload_file(file_path, object_key, StorageTier.HOT,
public=True, content_type=f'image/{ext[1:]}')
# 视频:根据大小分层(大文件放本地,小视频放 R2)
if ext in ['.mp4', '.mov']:
tier = StorageTier.COLD if file_size_mb > 500 else StorageTier.HOT
return upload_file(file_path, object_key, tier, 'video/mp4')
# 备份文件:冷存储(本地)
if ext in ['.sql', '.tar', '.gz', '.zip']:
return upload_file(file_path, object_key, StorageTier.COLD)
# 默认:热存储
return upload_file(file_path, object_key, StorageTier.HOT)五、Django/Flask 集成示例(产品图上传)
<code">from django.core.files.storage import Storage
import boto3
class R2Storage(Storage):
"""Django 自定义存储后端(R2)"""
def __init__(self):
self.client = boto3.client(
's3',
endpoint_url=f'https://{CF_ACCOUNT_ID}.r2.cloudflarestorage.com',
aws_access_key_id=R2_ACCESS_KEY_ID,
aws_secret_access_key=R2_SECRET_ACCESS_KEY,
region_name='auto'
)
self.bucket = 'myapp-assets'
self.cdn_url = 'https://assets.yourdomain.com'
def _save(self, name, content):
self.client.upload_fileobj(
content,
self.bucket,
name,
ExtraArgs={
'ContentType': content.content_type,
'CacheControl': 'public, max-age=31536000',
}
)
return name
def url(self, name):
return f"{self.cdn_url}/{name}" # 返回 CDN URL
# settings.py
DEFAULT_FILE_STORAGE = 'myapp.storage.R2Storage'六、备份数据自动归档到本地
<code"># 定时备份脚本:数据库 → 本地 MinIO(冷存储)
cat > /opt/backup/r2-cold-archive.sh << 'EOF' #!/bin/bash DATE=$(date +%Y%m%d_%H%M) # 1. 数据库备份 mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases \ | gzip > /tmp/db-backup-$DATE.sql.gz
# 2. 上传到本地 MinIO(冷存储,香港服务器)
mc cp /tmp/db-backup-$DATE.sql.gz local/backups/mysql/
rm /tmp/db-backup-$DATE.sql.gz
# 3. 清理60天前的备份
mc find local/backups/mysql/ --older-than 60d --exec "mc rm {}"
echo "[$DATE] 备份完成并归档到冷存储"
EOF
chmod +x /opt/backup/r2-cold-archive.sh
echo "0 3 * * * /opt/backup/r2-cold-archive.sh >> /var/log/backup.log 2>&1" >> /etc/crontab七、R2 访问分析
<code"># R2 内置了详细的请求分析(在 Cloudflare Dashboard 查看)
# 可以看到:请求量、带宽、缓存命中率、热门对象
# 通过 Cloudflare Workers 实现访问控制(需要鉴权的资源)
# Workers 脚本示例:验证 Token 后才允许访问 R2 对象
export default {
async fetch(request, env) {
const url = new URL(request.url);
const token = request.headers.get('Authorization');
// 验证 Token
if (!token || token !== `Bearer ${env.ACCESS_TOKEN}`) {
return new Response('Unauthorized', { status: 401 });
}
// 从 R2 获取对象
const object = await env.MY_BUCKET.get(url.pathname.slice(1));
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: { 'Content-Type': object.httpMetadata.contentType }
});
}
};八、每月成本估算(1TB 热数据 + 500GB 冷数据)
| 费用项目 | 金额 | 说明 |
|---|---|---|
| Cloudflare R2 存储(1TB) | $15/月 | 超出 10GB 免费后 $0.015/GB |
| R2 出口流量 | $0 | 永久免费! |
| R2 API 操作(写入) | ~$1 | $4.50/百万次 A 类操作 |
| 香港 VPS(含 500GB 磁盘) | ¥150/月 | 本地冷存储 |
| 合计 | 约¥260/月 | 含全球 CDN 分发 |
| 对比 AWS S3(同等规模) | 约¥900/月 | 含出口流量费 |
九、总结
R2 + 香港 VPS 混合存储将存储成本降低约 70%,核心原因是 R2 的零出口流量费——传统对象存储的成本大头往往不是存储费而是流量费。热数据放 R2 享受 Cloudflare 全球 CDN 加速,冷数据放香港 VPS 本地低成本归档,两者通过 S3 兼容 API 统一管理,业务代码无缝切换。