本文系统梳理对象存储与 CDN 的核心知识,涵盖架构原理、主流产品实战、文件上传方案、CDN 配置、成本优化与监控运维。适合中后端工程师和运维人员快速上手。
一、对象存储基础
1.1 架构原理
对象存储(Object Storage)是一种扁平化、分布式的存储架构,与传统文件系统(层级目录)和块存储(裸磁盘)有本质区别:
传统文件系统: /data/images/2024/avatar.jpg → 逐级寻址
块存储: LUN → Block 0~N → 随机读写
对象存储: Bucket + Key + Data + Metadata → HTTP RESTful 访问核心特征:
1.2 主流产品对比
二、MinIO 实战
2.1 安装部署
单节点快速启动(Docker):
# 创建数据目录
mkdir -p /data/minio/{data,config}
# 启动 MinIO
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=YourStr0ngP@ssword!" \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
minio/minio server /data --console-address ":9001"
# 访问管理控制台: http://<IP>:9001systemd 服务化部署(生产推荐):
# /etc/systemd/system/minio.service
[Unit]
Description=MinIO Object Storage
After=network.target
[Service]
User=minio
Group=minio
Type=simple
Environment="MINIO_ROOT_USER=admin"
Environment="MINIO_ROOT_PASSWORD=YourStr0ngP@ssword!"
ExecStart=/usr/local/bin/minio server /data/minio --console-address ":9001"
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now minio2.2 mc 客户端
# 安装 mc
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && mv mc /usr/local/bin/
# 配置别名
mc alias set myminio http://localhost:9000 admin 'YourStr0ngP@ssword!'
# 基本操作
mc mb myminio/avatars # 创建桶
mc cp ./photo.jpg myminio/avatars/ # 上传文件
mc ls myminio/avatars/ # 列出文件
mc cat myminio/avatars/photo.jpg # 下载到 stdout
mc rm myminio/avatars/photo.jpg # 删除文件
mc mirror ./dist/ myminio/website/ # 同步目录
mc share download myminio/avatars/photo.jpg # 生成临时下载链接2.3 桶管理与策略
# 设置桶为公开读
mc anonymous set download myminio/avatars
# 查看桶策略
mc anonymous get myminio/avatars
# 设置生命周期(7天后过期)
cat > lifecycle.json << 'EOF'
{
"Rules": [
{
"ID": "expire-7d",
"Status": "Enabled",
"Expiration": { "Days": 7 },
"Filter": { "Prefix": "tmp/" }
}
]
}
EOF
mc ilm import myminio/avatars < lifecycle.json
# 版本控制
mc version enable myminio/avatars
mc version info myminio/avatars2.4 MinIO 集群(纠删码模式)
4 节点 4 磁盘部署(分布式模式):
# 在每个节点准备数据目录
mkdir -p /data/minio-disk{1,4}
# 启动命令(所有节点相同)
minio server \
http://node1/data/minio-disk{1...4} \
http://node2/data/minio-disk{1...4} \
http://node3/data/minio-disk{1...4} \
http://node4/data/minio-disk{1...4}纠删码原理: 4 节点 × 4 磁盘 = 16 块数据,每 4 块编码产生 4 块校验,最多允许同时损坏 4 块而不丢数据。生产环境建议至少 4 节点。
三、阿里云 OSS 实战
3.1 SDK 使用(Python)
import oss2
# 初始化客户端
auth = oss2.Auth('LTAI5txxxxxxxxxxxx', 'YourAccessKeySecret')
bucket = oss2.Bucket(auth, 'https://oss-cn-hangzhou.aliyuncs.com', 'my-bucket')
# 上传文件
bucket.put_object('images/photo.jpg', open('photo.jpg', 'rb'),
headers={'Content-Type': 'image/jpeg'})
# 分片上传(大文件)
oss2.resumable_upload(bucket, 'videos/movie.mp4', 'movie.mp4',
store=oss2.ObjectStore(),
part_size=10 * 1024 * 1024, # 10MB 分片
num_threads=4)
# 生成签名 URL(临时访问)
url = bucket.sign_url('GET', 'images/photo.jpg', 3600) # 1小时有效
print(url)
# 列举文件
for obj in oss2.ObjectIterator(bucket, prefix='images/'):
print(f'{obj.key} {obj.size} {obj.last_modified}')3.2 防盗链配置
通过控制台或 API 设置 Referer 白名单:
# 通过 ossutil 设置
ossutil cors put oss://my-bucket cors.xml<!-- cors.xml -->
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>https://www.example.com</AllowedOrigin>
<AllowedOrigin>https://cdn.example.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<MaxAgeSeconds>86400</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>Referer 白名单(控制台):
Bucket → 权限管理 → 防盗链 → 设置空 Referer 拒绝
白名单添加:
*.example.com
3.3 生命周期规则
{
"Rules": [
{
"ID": "auto-archive",
"Status": "Enabled",
"Prefix": "logs/",
"Transitions": [
{ "Days": 30, "StorageClass": "IA" },
{ "Days": 90, "StorageClass": "Archive" }
],
"Expiration": { "Days": 365 }
},
{
"ID": "abort-multipart",
"Status": "Enabled",
"Prefix": "",
"AbortMultipartUpload": { "Days": 7 }
}
]
}四、AWS S3 实战
4.1 CLI 操作
# 安装 AWS CLI
pip install awscli
aws configure # 输入 Access Key / Secret Key / Region
# 桶操作
aws s3 mb s3://my-bucket --region us-east-1
aws s3 ls
aws s3 ls s3://my-bucket --recursive --human-readable
# 文件操作
aws s3 cp ./file.txt s3://my-bucket/
aws s3 cp ./dist/ s3://my-bucket/static/ --recursive
aws s3 sync ./dist/ s3://my-bucket/static/ --delete
aws s3 rm s3://my-bucket/tmp/ --recursive
# 生成预签名 URL
aws s3 presign s3://my-bucket/file.txt --expires-in 36004.2 存储桶策略
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForStaticAssets",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/static/*"
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
]
}4.3 版本控制
# 开启版本控制
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled
# 查看对象的所有版本
aws s3api list-object-versions --bucket my-bucket
# 恢复到指定版本
aws s3api copy-object \
--bucket my-bucket \
--key config/app.yml \
--copy-source my-bucket/config/app.yml?versionId=abc1234.4 Glacier 归档
# 创建生命周期规则,自动迁移到 Glacier
aws s3api put-bucket-lifecycle-configuration \
--bucket my-bucket \
--lifecycle-configuration '{
"Rules": [
{
"ID": "archive-old-logs",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "GlacierStorageTier": true }
],
"Expiration": { "Days": 365 }
}
]
}'
# 从 Glacier 恢复(需要先发起请求,等待数小时)
aws s3api restore-object \
--bucket my-bucket \
--key logs/2024-01.log \
--restore-request '{"Days":5,"GlacierJobParameters":{"Tier":"Standard"}}'五、文件上传方案
5.1 前端直传(推荐方案)
架构: 前端 → 预签名 URL → 直传 OSS/S3,不经过应用服务器
前端请求签名 → 后端生成预签名 URL → 前端直传对象存储后端生成预签名 URL(Python Flask):
from flask import Flask, jsonify
import boto3
app = Flask(__name__)
s3 = boto3.client('s3')
@app.route('/api/upload/presign', methods=['POST'])
def presign():
key = f"uploads/{uuid4()}.jpg"
url = s3.generate_presigned_url(
'put_object',
Params={
'Bucket': 'my-bucket',
'Key': key,
'ContentType': 'image/jpeg',
},
ExpiresIn=300 # 5 分钟有效
)
return jsonify({
'upload_url': url,
'key': key,
'access_url': f'https://cdn.example.com/{key}'
})前端上传:
async function uploadFile(file) {
// 1. 获取预签名 URL
const { data } = await axios.post('/api/upload/presign', {
filename: file.name,
contentType: file.type
});
// 2. 直传到 OSS/S3
await axios.put(data.upload_url, file, {
headers: { 'Content-Type': file.type }
});
// 3. 使用 CDN 域名访问
return data.access_url;
}5.2 分片上传
大文件(>100MB)建议分片上传,支持断点续传:
import boto3
from boto3.s3.transfer import TransferConfig
s3 = boto3.client('s3')
# 配置分片参数
config = TransferConfig(
multipart_threshold=100 * 1024 * 1024, # 100MB
max_concurrency=10,
multipart_chunksize=50 * 1024 * 1024, # 50MB
)
# 使用 boto3 的高层 API
s3_resource = boto3.resource('s3')
s3_resource.Bucket('my-bucket').upload_file(
'large-video.mp4',
'videos/large-video.mp4',
Config=config,
ExtraArgs={'ContentType': 'video/mp4'}
)手动分片(精细控制):
# 1. 初始化分片上传
resp = s3.create_multipart_upload(Bucket='my-bucket', Key='videos/big.mp4')
upload_id = resp['UploadId']
# 2. 上传各分片
parts = []
with open('big.mp4', 'rb') as f:
part_num = 1
while chunk := f.read(50 * 1024 * 1024):
resp = s3.upload_part(
Bucket='my-bucket', Key='videos/big.mp4',
UploadId=upload_id, PartNumber=part_num, Body=chunk
)
parts.append({'PartNumber': part_num, 'ETag': resp['ETag']})
part_num += 1
# 3. 完成分片上传
s3.complete_multipart_upload(
Bucket='my-bucket', Key='videos/big.mp4',
UploadId=upload_id,
MultipartUpload={'Parts': parts}
)5.3 预签名 URL 最佳实践
# 限制上传大小(通过 Content-Length 范围)
url = s3.generate_presigned_url(
'put_object',
Params={
'Bucket': 'my-bucket',
'Key': 'uploads/user-avatar.jpg',
'ContentType': 'image/jpeg',
'ContentLengthRange': [1024, 10 * 1024 * 1024], # 1KB~10MB
},
ExpiresIn=300
)安全要点:
预签名 URL 有效期尽量短(5~15 分钟)
限制上传文件类型和大小
使用独立的 IAM 用户/角色,权限最小化
上传完成后回调后端校验
六、CDN 基础
6.1 工作原理
用户请求 → DNS 调度 → 最近边缘节点 → 有缓存? → 直接返回
↓ 无缓存
回源到源站 → 缓存到边缘 → 返回用户核心概念:
6.2 缓存策略
缓存优先级: Cache-Control 响应头 > CDN 控制台配置 > 默认规则
# 常见 Cache-Control 头
Cache-Control: max-age=31536000, immutable # 静态资源(1年,不可变)
Cache-Control: max-age=3600, must-revalidate # API 响应(1小时)
Cache-Control: no-cache # 每次验证
Cache-Control: no-store # 不缓存推荐缓存策略:
6.3 回源策略
# Nginx 作为源站时的典型配置
location ~* \.(js|css|png|jpg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
# CDN 回源时隐藏源站 IP
# 对象存储作为源站时天然实现七、CDN 配置实战
7.1 阿里云 CDN
域名配置流程:
# 1. 添加加速域名
# 控制台 → CDN → 域名管理 → 添加域名
# 加速域名:cdn.example.com
# 源站类型:OSS 域名
# 源站地址:my-bucket.oss-cn-hangzhou.aliyuncs.com
# 2. 配置 CNAME(DNS)
# cdn.example.com → cdn.example.com.w.kunlunsl.com
# 3. 验证 CNAME 生效
dig cdn.example.com +short
# 应返回 CDN 分配的域名
# 4. 缓存配置(通过控制台或 API)
# 静态资源:jpg|png|gif|css|js → 缓存30天
# HTML:html → 不缓存或缓存5分钟阿里云 CDN API 操作:
# 刷新缓存(内容更新后必须执行)
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://cdn.example.com/css/app.css" \
--ObjectType File
# 预热缓存(提前回源到边缘)
aliyun cdn PushObjectCache \
--ObjectPath "https://cdn.example.com/images/hero.jpg"
# 查询刷新状态
aliyun cdn DescribeRefreshTaskById --TaskId 123456787.2 Cloudflare 配置
免费计划即可使用的基础配置:
// Cloudflare Page Rules(免费计划限3条)
// 规则1:静态资源长缓存
// URL: cdn.example.com/assets/*
// 设置:Cache Level = Cache Everything, Edge Cache TTL = 1 month
// 规则2:HTML 短缓存
// URL: cdn.example.com/*
// 设置:Browser Cache TTL = 30 minutes, Always Online = OnCloudflare Workers(边缘计算):
// worker.js - 自定义缓存逻辑
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// 图片自动优化
if (url.pathname.match(/\.(jpg|png|gif)$/i)) {
const imageUrl = url.href
// 使用 Cloudflare Image Resizing
const resizeUrl = `/cdn-cgi/image/width=800,quality=80,format=webp${url.pathname}`
const response = await fetch(resizeUrl, request)
const headers = new Headers(response.headers)
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
return new Response(response.body, {
status: response.status,
headers
})
}
return fetch(request)
}7.3 腾讯云 CDN
# 刷新 URL
tccli cdn PurgePathCache --Paths '["https://cdn.example.com/css/"]' --FlushType flush
# 预热
tccli cdn PushUrlsCache --Urls '["https://cdn.example.com/index.html"]'腾讯云 CDN + COS 组合配置:
# 1. COS 开启默认 CDN 加速域名
# COS 控制台 → 基础配置 → CDN 加速
# 2. 绑定自定义域名
# COS 控制台 → 基础配置 → 自定义域名 → cdn.example.com
# 3. 配置回源 host 为 COS 域名
# my-bucket-1250000000.cos.ap-guangzhou.myqcloud.com八、静态资源加速
8.1 前端部署方案
方案一:对象存储 + CDN(经典方案)
# 构建前端项目
npm run build
# 上传到 OSS/ S3
aws s3 sync ./dist/ s3://my-bucket/static/v2/ \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
# HTML 不缓存
aws s3 sync ./dist/ s3://my-bucket/ \
--cache-control "no-cache" \
--include "*.html"方案二:使用 ossutil(阿里云)批量上传
# 设置缓存头
ossutil cp -r ./dist/ oss://my-bucket/static/ \
--include "*.js,*.css,*.png,*.jpg" \
--meta "Cache-Control:public,max-age=31536000,immutable" \
--jobs 8
# HTML 文件单独处理
ossutil cp -r ./dist/ oss://my-bucket/ \
--include "*.html" \
--meta "Cache-Control:no-cache"8.2 图片处理
阿里云 OSS 图片处理:
# 原图 URL
https://cdn.example.com/photo.jpg
# 缩放 + 格式转换 + 质量
https://cdn.example.com/photo.jpg?x-oss-process=image/resize,w_800/format,webp/quality,q_80
# 裁剪
https://cdn.example.com/photo.jpg?x-oss-process=image/crop,w_400,h_400,x_100,y_100
# 水印(Base64 编码文字)
https://cdn.example.com/photo.jpg?x-oss-process=image/watermark,text_SGVsbG8,size_30S3 + Lambda@Edge 图片处理:
# lambda-edge-resize.py
import boto3
import json
from PIL import Image
import io
def handler(event, context):
request = event['Records'][0]['cf']['request']
uri = request['uri']
# 解析参数: /800x600/image.jpg
parts = uri.strip('/').split('/')
if len(parts) >= 2 and 'x' in parts[0]:
width, height = parts[0].split('x')
key = '/'.join(parts[1:])
# 从 S3 获取原图
s3 = boto3.client('s3')
obj = s3.get_object(Bucket='images-bucket', Key=key)
img = Image.open(obj['Body'])
# 缩放
img.thumbnail((int(width), int(height)), Image.LANCZOS)
buffer = io.BytesIO()
img.save(buffer, format='WEBP', quality=80)
return {
'status': '200',
'headers': {
'content-type': [{'key': 'Content-Type', 'value': 'image/webp'}],
'cache-control': [{'key': 'Cache-Control', 'value': 'max-age=31536000'}]
},
'body': base64.b64encode(buffer.getvalue()).decode(),
'bodyEncoding': 'base64'
}
return request # 无参数则原样返回8.3 视频加速
视频点播 + CDN 方案:
# 视频上传并触发转码(阿里云 VOD)
import oss2
def upload_and_transcode(video_path, title):
# 1. 上传原始视频
bucket.put_object(f'videos/raw/{title}.mp4', open(video_path, 'rb'))
# 2. 通过 VOD API 触发转码
# 转码模板:HLS + 多码率(360p/720p/1080p)
transcode_config = {
'TemplateGroupId': 'your-template-group-id',
'Output': {
'Bucket': 'my-vod-bucket',
'Location': 'oss-cn-hangzhou'
}
}
# 调用 VOD API 创建转码任务...
# 3. 生成播放地址
# HLS: https://cdn.example.com/videos/title/360p.m3u8
return f'https://cdn.example.com/videos/{title}/1080p.m3u8'HLS 播放配置:
# 源站 Nginx 配置 HLS 流
location ~* \.m3u8$ {
add_header Cache-Control "no-cache";
add_header Access-Control-Allow-Origin "*";
}
location ~* \.ts$ {
expires 1d;
add_header Cache-Control "public";
}九、成本优化
9.1 存储成本
# 分析存储用量
aws s3api list-buckets --query 'Buckets[].Name' --output text | \
xargs -I {} aws s3 ls s3://{} --recursive --summarize | \
grep "Total Size"
# 查找大文件
aws s3 ls s3://my-bucket --recursive --summarize | \
sort -k3 -rn | head -20存储类型优化策略:
生命周期自动转换示例:
{
"Rules": [
{
"ID": "cost-optimize",
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER" }
],
"NoncurrentVersionTransitions": [
{ "NoncurrentDays": 7, "StorageClass": "STANDARD_IA" }
],
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
}
]
}9.2 流量成本
# CDN 流量监控(阿里云)
aliyun cdn DescribeDomainFlowData \
--DomainName cdn.example.com \
--StartTime "2024-01-01T00:00:00Z" \
--EndTime "2024-01-31T23:59:59Z"节省流量的实用技巧:
启用 Gzip/Brotli 压缩: JS/CSS 可减少 60~80% 体积
图片格式优化: WebP 比 JPEG 小 25~35%,AVIF 更优
合理设置缓存: 提高命中率,减少回源
使用 CDN 节省带宽: CDN 流量费通常低于源站直接出口
# Nginx 开启 Brotli 压缩
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;9.3 请求成本
# 减少不必要的 LIST 请求
# 差量同步替代全量扫描
aws s3 sync ./dist/ s3://my-bucket/static/ --size-only
# 使用 S3 Inventory 替代频繁 LIST
aws s3api put-bucket-inventory-configuration \
--bucket my-bucket \
--id daily-inventory \
--inventory-configuration '{
"Destination": {
"S3BucketDestination": {
"Bucket": "arn:aws:s3:::inventory-bucket",
"Format": "CSV"
}
},
"IsEnabled": true,
"Schedule": { "Frequency": "Daily" },
"Filter": { "Prefix": "" },
"IncludedObjectVersions": "Current"
}'十、监控与运维
10.1 存储监控
# MinIO 监控指标(Prometheus 格式)
curl http://localhost:9000/minio/v2/metrics/cluster
# 关键指标
# minio_cluster_capacity_free_bytes - 可用空间
# minio_cluster_capacity_total_bytes - 总容量
# minio_s3_requests_total - 请求总数
# minio_s3_requests_errors_total - 错误数
# minio_s3_ttfb_seconds_distribution - 首字节时间Prometheus + Grafana 监控配置:
# prometheus.yml
scrape_configs:
- job_name: 'minio'
metrics_path: /minio/v2/metrics/cluster
scheme: http
static_configs:
- targets: ['minio:9000']
labels:
cluster: 'prod'10.2 CDN 监控
# 关键监控指标
# - 带宽峰值
# - 流量消耗
# - 缓存命中率(目标 > 90%)
# - 回源率(越低越好)
# - 状态码分布(关注 4xx/5xx 比例)
# - P95 延迟
# 阿里云 CDN 查询(CLI)
aliyun cdn DescribeDomainBpsData \
--DomainName cdn.example.com \
--StartTime "2024-01-01T00:00:00Z" \
--EndTime "2024-01-01T23:59:59Z" \
--Interval 30010.3 告警配置
# 阿里云告警规则示例
# 当 CDN 缓存命中率低于 85% 时告警
MetricName: ByteHitRate
ComparisonOperator: LessThanThreshold
Threshold: 85
Period: 300
EvaluationCount: 3
# 当存储空间使用率超过 80% 时告警
MetricName: StorageUtilization
ComparisonOperator: GreaterThanThreshold
Threshold: 8010.4 运维 Checklist
□ 定期检查存储用量和增长趋势
□ 确认生命周期规则正常执行
□ 验证 CDN 缓存命中率(>90%)
□ 检查回源带宽和延迟
□ 审查 IAM 权限(最小权限原则)
□ 验证备份和恢复流程
□ 检查 CORS 配置安全性
□ 确认防盗链/Referer 配置
□ 监控异常流量(DDoS/CC 防护)
□ 更新和轮换 Access Key
□ 检查分片上传的未完成任务
□ 验证跨区域复制状态(如有)附录:常用工具速查
rclone 多云同步示例:
# 配置多云端点
rclone config
# → 阿里云 OSS: alias=oss
# → AWS S3: alias=aws
# → MinIO: alias=minio
# 跨云同步
rclone sync oss:my-bucket/images/ aws:my-bucket/images/ --progress
rclone sync minio:avatars/ oss:backup-bucket/avatars/ --transfers 8本文为《服务器运维笔记》系列第 26 篇。对象存储与 CDN 是现代 Web 架构的基石,合理配置可以显著降低成本、提升用户体验。