Cloud Firestore / Firebase bandwidth 费用过高如何优化?
结论
你的判断基本正确。如果账单里的高带宽费用来自 Firebase Storage 的视频下载,把视频文件迁移到 Bunny CDN 或其他对象存储/CDN,并让客户端直接通过新服务上传和播放,通常能明显降低 Firebase 的带宽费用。
不过,有两点需要分清:
- Cloud Firestore 和 Firebase Storage 是两个不同的服务。视频文件应放在 Storage 中,Firestore 只保存标题、作者、播放地址等元数据。
- 只在 Firebase Storage 前加一层 CDN,不一定能完全消除源站流量。缓存未命中、缓存过期、Range 请求和回源仍会产生 Storage 流量。要彻底迁移,视频文件本身也应转移到新的存储服务。
所以,先不要急着迁移。第一步是确认账单费用到底来自 Firestore、Cloud Storage,还是其他网络出口。
为什么 700 名用户也可能产生很高的带宽
短视频应用的成本主要由播放次数决定,而不是注册用户数。
假设每个视频平均为 2 MB,每名用户每天观看 100 个视频:
2 MB × 100 × 700 × 30
= 4,200,000 MB
≈ 4.2 TB/月
这个估算还没有包括:
- 视频重复播放
- 用户上传流量
- 预加载下一个或多个视频
- 用户滑走但没有看完的视频
- 播放失败后的重试
- 同一视频的不同清晰度版本
- 缩略图、头像和其他媒体文件
即使只有几百名活跃用户,每月产生数 TB 流量也很常见。短视频应用本身就是带宽密集型业务。
先确认费用来自哪里
可以在 Google Cloud Billing 中按以下维度拆分费用:
- Service
- SKU
- Project
- Region
重点查看并区分这些项目:
- Cloud Firestore document reads、writes、deletes
- Cloud Firestore network egress
- Cloud Storage data storage
- Cloud Storage operations
- Cloud Storage network egress
- Cloud Functions 或 Cloud Run 网络费用
如果视频下载地址类似:
https://firebasestorage.googleapis.com/...
而且播放器直接请求这个地址,那么播放产生的主要流量通常会计入 Cloud Storage/Firebase Storage,而不是 Firestore。
如果 Firestore 文档只保存下面这类数据,文档本身产生的带宽一般远小于视频流量:
{
"authorId": "user_123",
"caption": "Example video",
"videoUrl": "https://cdn.example.com/videos/abc.mp4",
"thumbnailUrl": "https://cdn.example.com/thumbnails/abc.jpg",
"createdAt": "..."
}
具体 SKU 名称和价格可能随区域、计费方案和时间变化,请以当前项目的 Billing report 和官方价格页面为准。
推荐的迁移架构
Firebase Authentication 和 Cloud Firestore 可以继续使用,只迁移视频的存储与分发链路:
客户端
├─ Firebase Authentication:用户登录
├─ Cloud Firestore:视频元数据、评论、点赞
└─ CDN/Object Storage:视频上传与播放
可以按下面的流程实现:
- 客户端向自己的后端请求上传凭证。
- 后端验证 Firebase ID token。
- 后端生成短期上传 URL 或一次性上传凭证。
- 客户端把视频直接上传到对象存储。
- 上传完成后,由后端或可信客户端向 Firestore 写入视频元数据。
- 播放器直接请求 CDN 地址。
这样,视频文件不会经过 Firestore,也不需要先上传到 Firebase Storage,再复制到其他存储。
不要把 CDN 密钥放进客户端
对象存储的永久 API key、Storage password 或管理密钥不能写入 Android、iOS 或 Web 客户端。客户端安装包可以被反编译,Web 请求也能被检查。密钥一旦泄漏,攻击者可能上传、覆盖或删除文件。
正确的做法是由后端验证 Firebase 登录状态,再签发短期凭证。下面是一个不绑定具体 CDN 厂商的示例:
import express from "express";
import admin from "firebase-admin";
admin.initializeApp();
const app = express();
app.use(express.json());
app.post("/upload-session", async (req, res) => {
const authorization = req.headers.authorization ?? "";
const idToken = authorization.startsWith("Bearer ")
? authorization.slice(7)
: null;
if (!idToken) {
return res.status(401).json({ error: "Missing Firebase ID token" });
}
try {
const user = await admin.auth().verifyIdToken(idToken);
const objectKey = `videos/${user.uid}/${crypto.randomUUID()}.mp4`;
// 调用所选存储服务的服务端 API,
// 创建短期 signed upload URL 或一次性上传凭证。
const uploadUrl = await createSignedUploadUrl(objectKey);
return res.json({
objectKey,
uploadUrl,
expiresIn: 300
});
} catch {
return res.status(401).json({ error: "Invalid Firebase ID token" });
}
});
客户端获得短期 URL 后,直接上传文件:
async function uploadVideo(file, firebaseUser) {
const idToken = await firebaseUser.getIdToken();
const sessionResponse = await fetch("/upload-session", {
method: "POST",
headers: {
"Authorization": `Bearer ${idToken}`,
"Content-Type": "application/json"
}
});
if (!sessionResponse.ok) {
throw new Error("Unable to create upload session");
}
const { uploadUrl, objectKey } = await sessionResponse.json();
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type
},
body: file
});
if (!uploadResponse.ok) {
throw new Error("Video upload failed");
}
return objectKey;
}
createSignedUploadUrl() 的实现取决于所选服务商。需要先确认 Bunny CDN 或其他供应商当前支持的认证方式,再填写对应的 API 参数,不能直接套用其他服务商的签名规则。
Firestore 中只保存受控的对象标识
不要完全信任客户端提交的任意 URL,例如:
{
"videoUrl": "https://unknown.example/something.mp4"
}
更稳妥的做法是保存受控的 objectKey,再由后端或应用使用固定的 CDN 域名构造播放地址:
function getVideoUrl(objectKey) {
const allowedPattern = /^videos\/[A-Za-z0-9_-]+\/[A-Za-z0-9-]+\.mp4$/;
if (!allowedPattern.test(objectKey)) {
throw new Error("Invalid object key");
}
return `https://cdn.example.com/${objectKey}`;
}
这样可以减少恶意链接、域名替换和脏数据进入 Firestore 的风险。
仅添加 CDN 是否足够
如果继续把 Firebase Storage 作为源站,只在前面增加 CDN,能节省多少流量取决于缓存命中率。
适合缓存的公开视频通常需要满足这些条件:
- URL 稳定且不可变
- 设置合理的
Cache-Control - 文件更新时更换文件名或版本号
- CDN 支持 HTTP Range 请求
- 播放器始终使用 CDN 域名
例如,可以使用带版本标识的路径:
https://cdn.example.com/videos/user_123/550e8400-e29b-41d4-a716-446655440000.mp4
不可变文件可以使用类似的缓存策略:
Cache-Control: public, max-age=31536000, immutable
如果视频会在原路径上被覆盖,就不要设置长期不可变缓存,否则用户可能长时间看到旧文件。
即使缓存配置正确,首次访问和缓存未命中时仍会回源。因此:
- “Firebase Storage + 外部 CDN”可以减少源站下载流量;
- “外部对象存储 + CDN”才能真正把视频存储和分发流量迁出 Firebase。
还可以做哪些优化
避免过度预加载
短视频列表通常会提前加载后续视频。如果一次预加载多个完整 MP4,用户快速滑动时会产生大量无效流量。
可以只预加载下一个视频,并在卡片离开可视区域后取消请求。同时检查播放器是否会因组件重建而重复下载同一资源。
使用自适应码率
单个 MP4 使用简单,但不便于根据网络状况控制流量。可以考虑 HLS 或 MPEG-DASH,生成多种分辨率和码率,让客户端只下载当前网络环境和屏幕实际需要的片段。
转码、分片和多版本存储同样会产生费用,需要结合完播率和观看时长评估,不能只比较单个文件的大小。
检查 Range 请求和缓存行为
播放器通常会发送:
Range: bytes=...
迁移前需要确认 CDN:
- 能正确支持 Range 请求
- 不会因 Range 缓存策略频繁回源
- 能正确返回
206 Partial Content - 不会在每次拖动进度条时重新下载整个文件
限制上传
服务端需要限制:
- 文件大小
- MIME type
- 每名用户的上传频率
- 每日上传数量
- 对象路径
- 上传凭证有效期
只检查客户端文件扩展名并不够。上传完成后,还可以通过后台任务检查文件的真实类型和媒体信息。
设置预算告警
预算告警通常不会自动停止服务,但可以帮助你尽早发现费用异常增长。还应监控:
- 每日 CDN 流量
- 单用户播放流量
- 每个视频的请求次数
- 缓存命中率
- 上传和播放失败后的重试次数
- 热门视频的流量分布
迁移时不要忽略隐藏成本
比较服务商时,不能只看每 GB 的下载价格,还要计算:
总成本 =
存储费用
+ CDN 下行流量
+ 存储到 CDN 的回源流量
+ 请求次数费用
+ 上传与跨区域流量
+ 转码费用
+ 图片处理费用
+ 日志和监控费用
还需要确认服务区域、最低消费、缓存清除费用、访问控制、签名 URL、内容审核、删除响应时间和服务条款。
最稳妥的实施顺序
可以先选择少量视频做灰度迁移:
- 从 Billing report 确认高费用对应的具体服务和 SKU。
- 记录当前日均播放量、视频下载量和单次播放流量。
- 让新上传的视频使用新的对象存储和 CDN。
- 在 Firestore 中增加
storageProvider或objectKey,兼容新旧视频。 - 让少量用户通过 CDN 播放,检查 Range、缓存命中率和失败率。
- 对比迁移前后的 Firebase Storage 流量和整体成本。
- 确认运行稳定后,再迁移历史视频。
- 最后删除不再使用的 Firebase Storage 副本,避免重复支付存储费。
如果视频确实是主要流量来源,把视频存储和播放链路迁移到价格更合适的对象存储/CDN,通常能明显降低 Firebase 的带宽费用。Firebase 仍然可以负责认证、数据库和业务逻辑,不必因为视频带宽问题迁移整个后端。
备注:内容仅供参考。
