# 对象存储 + CDN 加速路径:从上传到用户访问 > 💡 **学习指南**:本文会带你走完一条完整的链路——从文件上传到用户下载。你会看到对象存储如何像"智能仓库"一样管理海量文件,CDN 如何像"快递网点"一样把内容送到用户家门口,以及这中间有哪些"坑"等着你跳进去。建议先了解基础的 HTTP 请求和 DNS 解析原理。 在开始之前,建议你先补几块"基础砖": - **HTTP 请求流程**:可以先阅读 [浏览器输入 URL 后发生了什么](./web-basics/url-to-browser.md) 了解完整的请求链路。 - **DNS 解析原理**:如果你对域名解析还不太熟悉,可以先看 [DNS 查询流程](./deployment/dns-flow.md) 的图解部分。 --- ## 0. 引言:为什么文件上传下载这么"慢"? 想象一下这个场景:你在一个图片社区上传了一张 10MB 的高清照片,结果等了半分钟才传完;而你的朋友在北京,点击下载却只要 2 秒。为什么同一张文件,上传和下载的体验天差地别? 或者再想想:你的电商网站双十一搞活动,商品详情页突然涌入百万流量,服务器直接"躺平"。是带宽不够?还是架构设计有问题? 这些问题的答案,都藏在**对象存储**和 **CDN** 这对"黄金搭档"里。 --- ## 1. 对象存储:你的"智能云仓库" ### 1.1 什么是对象存储? 传统文件系统就像你家衣柜:衣服按"上衣/裤子/裙子"分层放,你要找一件衬衫,得先打开衣柜→上衣区→衬衫格。这种"层级嵌套"的模式,在文件数量爆炸时会变得极其笨重。 对象存储则像现代仓储物流:每个包裹都有一个唯一的"快递单号"(对象键),你只需报单号,仓库机器人就能从海量包裹中精准取出。 **核心区别一览**: | 维度 | 传统文件系统 | 对象存储 | | :----------- | :--------------------- | :---------------------- | | **组织方式** | 层级目录树 | 扁平键值对 | | **访问协议** | POSIX(本地文件操作) | HTTP/REST API | | **扩展性** | 单机容量有限 | 近乎无限水平扩展 | | **元数据** | 基础属性(大小、时间) | 丰富的自定义元数据 | | **典型场景** | 本地办公文档 | 图片/视频/备份/静态资源 | ### 1.2 对象存储的核心概念 #### 桶(Bucket):你的"仓库分区" 桶是对象存储的顶级容器,相当于一个独立的命名空间。所有对象都必须存放在某个桶中。 **命名规则**(以阿里云 OSS 为例): - 全局唯一:在整个云厂商的所有用户中不能重复 - 只能包含小写字母、数字和短横线 - 必须以小写字母或数字开头和结尾 - 长度在 3-63 个字符之间 **实战踩坑**:曾经有个团队按业务线创建了几十个桶,结果月底账单出来傻眼了——每个桶都有最低存储费用和请求费用。建议:按"环境+用途"组合规划桶,比如 `prod-static-assets`、`dev-backup-archive`。 #### 对象(Object):你的"数据包裹" 对象是存储的基本单元,由三部分组成: 1. **键(Key)**:对象的唯一标识,相当于"快递单号" - 示例:`images/avatar/2024/user123.jpg` - 虽然看起来像路径,但本质只是字符串 2. **数据(Data)**:对象的内容本身 - 可以是任意二进制数据 - 大小限制取决于云厂商(通常单个对象 5TB 以内) 3. **元数据(Metadata)**:描述对象的附加信息 - 系统元数据:Content-Type、ETag、Last-Modified 等 - 自定义元数据:如 `x-oss-meta-owner`、`x-oss-meta-project` #### 访问控制:谁能动我的"仓库"? 对象存储提供多层权限控制: | 层级 | 控制方式 | 典型场景 | | :----------- | :------------------------ | :------------------------------ | | **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP | | **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 | | **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 | **安全红线**:永远不要把 AccessKey ID 和 AccessKey Secret 写在前端代码里!正确做法是:前端向你的后端申请临时 STS 凭证,后端验证身份后返回带过期时间的临时凭证。 --- ## 2. CDN:你的"全球快递网络" ### 2.1 为什么需要 CDN? 想象你开了一家网店,服务器放在深圳。现在有个用户在北京访问你的图片: - **没有 CDN**:请求从北京→河北→河南→湖北→湖南→广东→深圳,跨越 2000 多公里,来回就是 4000 多公里。光网络传输就要几十毫秒,遇到网络拥堵更惨。 - **有了 CDN**:请求从北京直接到北京的 CDN 节点(可能就在北京联通机房),距离从 2000 公里变成 20 公里,延迟从 50ms 变成 5ms。 这就是 CDN 的核心价值:**让内容离用户更近**。 ### 2.2 CDN 的核心架构 #### 边缘节点:离用户最近的"快递站" 边缘节点是 CDN 网络中最接近用户的层级,通常部署在: - 运营商机房(联通/电信/移动) - 大城市互联网交换中心 - 重要交通枢纽 **中国主要 CDN 节点分布**: - 一线城市:北京、上海、广州、深圳 - 二线城市:杭州、南京、成都、武汉、西安 - 海外:香港、新加坡、东京、硅谷、法兰克福 #### 源站:内容的"总仓库" 源站是 CDN 回源获取内容的地方,可以是: - 对象存储(OSS/COS/S3) - 自建服务器(ECS/物理机) - 负载均衡(SLB/CLB) **关键配置**: - **回源 HOST**:CDN 节点访问源站时使用的域名/IP - **回源协议**:HTTP 还是 HTTPS - **回源端口**:80、443 还是自定义端口 #### 中间层节点:"区域分拨中心" 在边缘节点和源站之间,CDN 通常还有一层或多层中间节点: - **汇聚节点**:聚合多个边缘节点的回源请求,减少源站压力 - **区域中心**:负责一个大区的内容分发和调度 这种分层架构的好处: 1. **降低源站压力**:1000 个边缘节点的请求,可能只需要向源站发起 10 次 2. **提高命中率**:热门内容在中间层就被拦截,不需要回源 3. **故障隔离**:某条链路出问题,可以自动切换到其他路径 ### 2.3 CDN 加速的完整流程 让我们跟踪一次真实的用户请求: **Step 1:DNS 解析**(智能调度) ``` 用户输入:cdn.example.com/image.jpg ↓ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4) ``` 这里的关键是**智能 DNS**:根据用户的运营商、地理位置、节点负载,返回最优的 CDN 节点 IP。 **Step 2:边缘节点查找**(缓存命中?) ``` 请求到达北京联通 CDN 节点(1.2.3.4) ↓ 节点检查本地缓存: ├─ 命中?直接返回内容 ✓ └─ 未命中?继续下一步 ``` **Step 3:回源获取**(层层向上) ``` 边缘节点未命中 ↓ 向父节点(如:华北区域中心)请求 ├─ 父节点命中?返回内容 └─ 父节点未命中?继续向上 ↓ 向源站请求 ↓ 源站返回内容 ``` **Step 4:缓存并返回**(下次更快) ``` 内容沿链路返回 ↓ 每层节点都缓存一份 ↓ 最终到达用户 ``` 这样,下次有用户请求同一个文件时,就能直接从边缘节点返回,实现"秒开"。 --- ## 3. 从上传到访问:完整链路解析 ### 3.1 文件上传的三种方式 #### 方式一:客户端 → 服务端 → 对象存储(传统模式) ``` 浏览器 → 你的后端服务器 → 对象存储 ``` **流程**: 1. 用户选择文件,点击上传 2. 文件先上传到你的后端服务器 3. 后端接收完整文件后,再转上传到对象存储 4. 返回上传结果给用户 **优点**: - 实现简单,前后端都好控制 - 可以在后端做文件校验、格式转换 - 敏感操作可以记录日志、做权限校验 **缺点**: - **带宽双吃**:用户上传占用一次带宽,服务器转传又占用一次 - **服务器压力大**:大文件会占用大量内存和 CPU - **上传慢**:相当于多了一道中转,用户感知到的上传时间更长 **适用场景**:小文件(<10MB)、需要后端处理(如图片压缩、加水印)、内部管理系统。 #### 方式二:客户端直传对象存储(现代推荐) ``` 浏览器 ──────→ 对象存储 ↑ 后端只签发临时凭证 ``` **流程**: 1. 用户选择文件,前端先向后端申请"上传凭证" 2. 后端验证用户身份,向对象存储服务申请**临时 STS 凭证**(带过期时间) 3. 后端把临时凭证返回给前端 4. 前端拿着凭证,**直接上传文件到对象存储** 5. 对象存储返回上传结果,前端通知后端"上传完成" **优点**: - **上传快**:少了中转环节,用户感知速度最快 - **服务器压力小**:只处理凭证签发,不处理文件流 - **带宽省**:只走一次上传流量 - **安全性高**:临时凭证有过期时间,泄露也危害有限 **缺点**: - 实现稍复杂,需要理解 STS、签名机制 - 前端需要处理分片上传、断点续传等逻辑 - 跨域(CORS)需要配置 **适用场景**:大文件上传、用户生成内容(UGC)、需要高并发上传的业务。 #### 方式三:分片上传 + 断点续传(大文件必备) ``` 10GB 视频文件 ↓ 切分成 1000 个 10MB 的分片 ↓ 并行上传(同时传 5 个分片) ↓ 断网了!已传 600 个 ↓ 恢复网络,从第 601 个继续传 ↓ 所有分片传完,发起"合并"请求 ``` **为什么需要分片?** | 场景 | 不分片 | 分片 | | :----------- | :---------------------- | :------------------- | | **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 | | **上传速度** | 单线程,速度慢 | 多线程并行,速度快 | | **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 | | **进度显示** | 只有 0% 和 100% | 精确到每个分片的进度 | **主流云厂商的分片规格**: | 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 | | :------------- | :----------- | :--------- | :----------- | | **阿里云 OSS** | 100MB | 10000 | 100KB | | **腾讯云 COS** | 5GB | 10000 | 1MB | | **AWS S3** | 5GB | 10000 | 5MB(推荐) | | **七牛云** | 100MB | 10000 | 4MB | ### 3.2 CDN 回源策略详解 #### 什么是"回源"? CDN 边缘节点缓存了源站的内容,但当: - 用户请求的内容**第一次被访问** - 缓存的内容**已过期(TTL 到期)** - 缓存被**手动刷新/预热** CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"。 #### 回源的三种模式 | 模式 | 原理 | 适用场景 | 优缺点 | | :-------------------- | :----------------------- | :------------------------ | :----------------------- | | **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 | | **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 | | ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 | #### 回源配置实战 **场景 1:对象存储作为源站(推荐)** ``` 用户访问:cdn.example.com/images/photo.jpg ↓ CDN 边缘节点(北京) ↓ 未命中,回源到源站 ↓ 源站:bucket-name.oss-cn-beijing.aliyuncs.com ↓ 返回图片,CDN 缓存并响应用户 ``` 关键配置项: - **源站类型**:OSS/COS 域名 或 自定义源站 - **回源协议**:HTTP 还是 HTTPS(建议 HTTPS) - **回源 HOST**:访问源站时使用的 Host 头 - **回源 SNI**:HTTPS 回源时的服务器名称指示 **场景 2:多源站负载均衡** 当单个源站扛不住回源压力时,可以配置多个源站: ``` CDN 边缘节点 ├─ 源站 A (权重 50%) ├─ 源站 B (权重 30%) └─ 源站 C (权重 20%) ``` 主备模式: ``` CDN 边缘节点 ├─ 主源站 A (健康时全部流量) └─ 备源站 B (主源故障时切换) ``` #### 回源带宽 vs CDN 带宽 这里有个容易混淆的概念: | 指标 | 定义 | 计费关系 | | :--------------- | :---------------------- | :--------------------------- | | **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 | | **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 | **省钱技巧**: - 提高 CDN 命中率(让更多请求命中缓存,减少回源) - 设置合理的缓存时间(TTL) - 使用预热功能,在用户访问前就缓存热点内容 - 开启"跟随 301/302",避免不必要的回源跳转 ### 3.3 缓存策略配置 #### 缓存键(Cache Key):决定什么算"同一个文件" CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就是**缓存键**。 **默认缓存键通常包括**: - URL 路径(不含查询参数) - 例如:`/images/photo.jpg` **问题场景**: ``` 用户 A 请求:/images/photo.jpg?w=100&h=100 (100x100 缩略图) 用户 B 请求:/images/photo.jpg?w=800&h=600 (800x600 大图) ``` 如果缓存键只包含路径,两张不同尺寸的图片会被认为是同一个文件,导致混乱。 **解决方案:自定义缓存键规则** | 规则 | 示例 | 效果 | | :------------------- | :------------------------ | :------------------------ | | **保留指定查询参数** | 保留 `w`、`h` | 不同尺寸分别缓存 | | **保留所有查询参数** | 保留全部 | 完全精确匹配 | | **忽略特定查询参数** | 忽略 `token`、`timestamp` | 带时间戳的 URL 能命中缓存 | | **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 | **实战配置示例**(阿里云 CDN): ``` 缓存键规则: - URL 路径:/images/* - 保留查询参数:w, h, format - 忽略查询参数:token, timestamp, utm_source ``` #### 缓存时间(TTL):内容"新鲜度"的平衡 TTL(Time To Live)决定了内容在 CDN 节点上缓存多久。设置太短,回源多、成本高;设置太长,内容更新后用户看到旧内容。 **按文件类型设置 TTL 的建议**: | 文件类型 | 建议 TTL | 原因 | | :---------- | :---------------------- | :----------------------------- | | HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 | | JS/CSS 文件 | 1 年(配合文件名 hash) | 内容不变,文件名变化即缓存失效 | | 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 | | 字体文件 | 1 年 | 几乎不变 | | API 响应 | 0-5 分钟(视业务) | 数据实时性要求高 | **前端工程化配合 CDN 的最佳实践**: ```javascript // webpack/vite 配置 output: { filename: 'js/[name]-[contenthash:8].js', chunkFilename: 'js/[name]-[contenthash:8].chunk.js', } ``` 生成的文件名:`app-a3f2b1c9.js` - 文件内容变化 → hash 变化 → 新 URL → 自然缓存失效 - 文件内容不变 → hash 不变 → URL 不变 → 长期缓存命中 #### 缓存刷新与预热 **手动刷新(应急场景)**: 当你更新了源站内容,但 CDN 缓存还没过期,用户看到的还是旧内容: | 刷新类型 | 效果 | 耗时 | 适用场景 | | :----------- | :--------------------- | :---------- | :----------- | | **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 | | **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 | | **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 | **重要提醒**:刷新只是让缓存失效,下次请求会回源拉取新内容。不要在高峰期大批量刷新,否则可能导致源站被打爆。 **预热( proactive 优化)**: 刷新是被动的(内容已更新),预热是主动的(提前缓存)。 ``` 场景:明天上午 10 点要发一篇爆款文章 今晚就提交预热请求: - URL: https://cdn.example.com/articles/爆款文章.html - 预热范围:全国所有边缘节点 效果: 明天 10 点用户访问时,内容已经在边缘节点等着了 → 零回源延迟,秒开体验 ``` --- ## 4. 流量调度:让用户访问"最近"的节点 ### 4.1 智能 DNS 调度 传统 DNS 解析: ``` 用户问:cdn.example.com 的 IP 是什么? DNS 答:1.2.3.4(固定的) ``` 智能 DNS 解析: ``` 用户(北京联通)问:cdn.example.com 的 IP 是什么? 智能 DNS:让我查查... 北京联通的 CDN 节点是 1.2.3.4 用户(上海电信)问:cdn.example.com 的 IP 是什么? 智能 DNS:上海电信的 CDN 节点是 5.6.7.8 ``` **调度维度**: | 维度 | 说明 | 效果 | | :--- | :--- | :--- | | **地理位置** | 按省/市/国家分配 | 就近访问,降低延迟 | | **运营商** | 联通/电信/移动/BGP | 同运营商传输,避免跨网 | | **节点负载** | 实时 CPU/带宽/QPS | 避开过载节点 | | **节点健康** | 探测可用性 | 自动剔除故障节点 | | **成本因素** | 带宽单价差异 | 平衡性能与成本 | ### 4.2 HTTP DNS 与 IP 直连 传统 DNS 有个问题:**DNS 劫持和解析延迟**。 **HTTP DNS 方案**: ``` 客户端 → 绕过系统 DNS → 直接问 HTTP DNS 服务(如 223.5.5.5:80) ↓ 返回最优 IP 列表(带权重) ↓ 客户端根据网络质量探测,选择最优 IP ``` 优势: - 防劫持:不走运营商 DNS - 更精准:可以按客户端网络质量选择 IP - 实时性:故障切换更快 **实战建议**: - 移动端 APP 强烈建议接入 HTTP DNS - Web 端可以使用 CDN 提供的 CNAME 调度 - 关键业务可以做多 IP 容灾(一个域名返回多个 IP) --- ## 5. HTTPS 优化:安全与性能的平衡 ### 5.1 为什么 CDN 上 HTTPS 很重要? **场景对比**: ``` 无 HTTPS: 用户访问 http://cdn.example.com/image.jpg ↓ 浏览器地址栏显示"不安全" ↓ 某些浏览器/APP 直接拦截访问 ↓ SEO 排名降低 ``` ``` 有 HTTPS: 用户访问 https://cdn.example.com/image.jpg ↓ 浏览器显示绿色锁标志 ↓ HTTP/2 多路复用生效 ↓ 性能 + 安全双提升 ``` ### 5.2 CDN HTTPS 配置要点 #### 证书管理 | 方案 | 说明 | 成本 | 适用场景 | | :--------------------- | :-------------------- | :------------- | :--------------- | | **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 | | **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 | | **商业 DV/OV/EV 证书** | 赛门铁克、GeoTrust 等 | ¥几百-几万/年 | 企业级、需要绿条 | | **泛域名证书** | \*.example.com | ¥几千/年 | 多子域名 | **实战建议**: - 测试环境:Let's Encrypt 或云厂商免费证书 - 生产环境:泛域名证书(省事)或单域名 OV 证书(省钱) - 注意证书过期时间,设置自动续期提醒 #### HTTPS 优化配置 **TLS 版本选择**: ``` 推荐配置:仅 TLS 1.2 和 TLS 1.3 兼容配置:TLS 1.1 + TLS 1.2 + TLS 1.3(兼容老旧浏览器) ``` **密码套件**: ``` 推荐:ECDHE 密钥交换 + AES-GCM 加密 禁用:DES、RC4、MD5、SHA1 ``` **OCSP Stapling**: ``` 功能:CDN 节点预获取证书吊销状态 效果:减少客户端验证时间 200-500ms 建议:务必开启 ``` **TLS 会话复用**: ``` Session ID 复用:客户端带着上次 Session ID,服务端恢复会话 Session Ticket 复用:服务端把会话状态加密发给客户端,下次带来 效果:避免完整 TLS 握手,减少 1-RTT ``` ### 5.3 HTTP/2 与 HTTP/3 在 CDN 上的应用 **HTTP/2 多路复用**: ``` HTTP/1.1: 请求 1 (index.html) ────────────────→ 响应 1 ←────────────────────────────── 请求 2 (style.css) ─────────────────→ 响应 2 ←────────────────────────────── 请求 3 (script.js) ─────────────────→ 响应 3 ←────────────────────────────── (串行,一个完了下一个) HTTP/2: 请求 1 ──→ 请求 2 ──→ 合并在一个 TCP 连接上,帧交错传输 请求 3 ──→ 响应 1 ←── 按优先级流式返回 响应 2 ←── 响应 3 ←── (并行,一个连接多路复用) ``` **HTTP/2 服务端推送**: ``` 场景:用户请求 index.html,里面引用了 style.css 和 script.js 传统方式: 1. 用户下载 index.html 2. 解析发现需要 style.css 和 script.js 3. 再发两个请求获取 HTTP/2 推送: 1. 用户请求 index.html 2. CDN 节点返回 index.html 的同时,主动推送 style.css 和 script.js 3. 用户解析 html 时,资源已经在缓存里了 注意:推送要谨慎,推多了浪费带宽,推少了没效果 ``` **HTTP/3 (QUIC)**: ``` HTTP/2 的问题:基于 TCP,队头阻塞 → 一个 TCP 包丢失,整个连接等待重传 HTTP/3 的解决:基于 QUIC(UDP 之上实现可靠传输) → 每个流独立,一个流阻塞不影响其他流 → 连接迁移:WiFi 切 4G,连接不中断 → 0-RTT 握手:第一次访问也能快速建立连接 现状:2024 年主流 CDN 已支持 HTTP/3,建议开启 ``` --- ## 6. 访问分析:看懂你的 CDN 报表 ### 6.1 核心指标解读 #### 带宽(Bandwidth) ``` 定义:单位时间内传输的数据量 单位:bps(比特每秒)、Mbps、Gbps CDN 带宽 = 所有边缘节点的出流量总和 注意区分: - 计费带宽:通常按 95 峰值或日峰值计费 - 实际带宽:实时传输速率 ``` **带宽与流量的关系**: ``` 1 Mbps 带宽持续跑 1 小时 = 450 MB 流量 (计算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB) ``` #### QPS(Queries Per Second) ``` 定义:每秒查询/请求数 CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数 注意:QPS 高不代表带宽高 - 小文件场景:QPS 很高,带宽不高 - 大文件场景:QPS 不高,带宽很高 ``` #### 命中率(Hit Ratio) ``` 定义:在 CDN 边缘节点命中的请求占总请求的比例 计算公式: 命中率 = (命中数 / 总请求数) × 100% 或 命中率 = (1 - 回源流量 / 总出流量) × 100% 行业标准: - 图片/视频/JS/CSS:> 95% - HTML 页面:50-80%(视更新频率) - API 接口:通常不缓存或极低 ``` **命中率低的常见原因**: | 原因 | 现象 | 解决方案 | | :------------- | :----------------- | :----------------------- | | 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL | | 查询参数变化 | URL 带随机数 | 配置忽略特定参数 | | 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 | | 内容更新频繁 | 文件经常被覆盖 | 使用版本号或 hash 文件名 | | 首次访问多 | 新内容或新节点 | 提前预热 | ### 6.2 日志分析与问题排查 #### CDN 日志字段解析 典型 CDN 访问日志包含以下字段: ``` 时间 | 客户端 IP | 请求方法 | URL | HTTP 状态码 | 响应大小 | 缓存状态 | 响应时间 | Referer | User-Agent 示例: 2024-01-15 14:32:01 | 114.114.114.114 | GET | https://cdn.example.com/images/photo.jpg | 200 | 153600 | HIT | 23 | https://example.com/ | Mozilla/5.0... ``` 关键字段解释: | 字段 | 说明 | 分析价值 | | :-------------- | :------------- | :------------------------------------------- | | `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) | | `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 | | `http_status` | HTTP 状态码 | 404/500 错误排查 | | `bytes_sent` | 发送字节数 | 带宽统计 | #### 常见问题排查 **问题 1:用户反映访问慢** 排查步骤: ``` 1. 看日志 response_time - 如果很大(>500ms):检查是缓存 MISS 还是源站慢 2. 检查 cache_status - HIT:缓存命中,慢可能是文件太大或节点问题 - MISS:未命中,需优化缓存策略或命中率 3. 检查客户端 IP 分布 - 某些地区慢:可能是该节点负载高或覆盖不足 ``` **问题 2:缓存不生效,每次都回源** 排查清单: ``` □ 源站响应头是否有 Cache-Control: no-cache / private? □ URL 是否带随机参数(如 ?_=123456)? □ 缓存键配置是否正确? □ TTL 设置是否过短? □ 是否命中浏览器本地缓存而非 CDN? ``` **问题 3:费用暴涨** 排查方向: ``` 1. 看账单明细 - CDN 流量费高:检查是否有大文件被频繁访问,或被盗链 - 回源流量费高:检查命中率是否骤降 - 请求数费用高:检查是否有 CC 攻击或爬虫 2. 看访问日志 - 是否有大量 404 请求(可能是扫描或配置错误) - Referer 是否异常(判断是否被盗链) 3. 安全设置 - 开启防盗链(Referer 白名单) - 开启 IP 黑名单/白名单 - 配置 CC 防护 ``` --- ## 7. 实战案例:从 0 搭建图片加速方案 ### 7.1 业务场景 假设你是一个图片社区的技术负责人,面临以下挑战: - **用户上传**:用户每天上传 100 万张图片(平均 2MB/张) - **用户访问**:每天 5000 万次图片查看请求 - **访问分布**:用户遍布全国,海外也有少量访问 - **性能要求**:图片加载时间 < 500ms - **成本预算**:尽量控制在每月 5 万以内 ### 7.2 架构设计 ``` ┌──────────────────────────────────────┐ │ 用户上传流程 │ └──────────────────────────────────────┘ 用户浏览器 后端服务 对象存储 │ │ │ │ 1. 申请上传凭证 │ │ │───────────────────────────────────────────>│ │ │ │ │ │ │ 2. 申请 STS 临时凭证 │ │ │───────────────────────────>│ │ │ │ │ │ 3. 返回 STS 凭证 │ │ │<───────────────────────────│ │ │ │ │ 4. 返回上传凭证(含 STS) │ │<───────────────────────────────────────────│ │ │ │ │ │ 5. 直接上传文件(使用 STS 签名) │ │──────────────────────────────────────────────────────────────────────>│ │ │ │ │ 6. 返回上传结果(URL、ETag 等) │ │<──────────────────────────────────────────────────────────────────────│ │ │ │ │ 7. 通知后端上传完成(保存到数据库) │ │───────────────────────────────────────────>│ │ ┌──────────────────────────────────────┐ │ 用户访问流程 │ └──────────────────────────────────────┘ 用户浏览器 DNS 解析 CDN 节点 对象存储(源站) │ │ │ │ │ 1. 请求图片 URL │ │ │ │────────────────────────────────────────>│ │ │ │ │ │ │ │ 2. DNS 查询 │ │ │ │────────────────────>│ │ │ │ │ │ │ │ 3. 返回最优节点 IP │ │ │ │<────────────────────│ │ │ │ │ │ │ 4. 连接 CDN 节点 │ │ │ │────────────────────────────────────────>│ │ │ │ │ │ │ │ 5. 检查缓存 │ │ │ │ ├─ 命中?直接返回 │ │ │ └─ 未命中?继续 │ │ │ │ │ │ │ │ 6. 回源获取 │ │ │ │──────────────────>│ │ │ │ │ │ │ │ 7. 返回文件 │ │ │ │<──────────────────│ │ │ │ │ │ │ 8. 缓存并响应 │ │ │<────────────────────────────────────────│ │ ``` ### 7.3 关键配置详解 #### 对象存储配置 **存储桶规划**: ``` Bucket: myapp-images-prod ├─ 目录结构: │ ├─ uploads/ # 用户上传的原图 │ │ ├─ 2024/01/15/user123-abc.jpg │ │ └─ 2024/01/15/user456-def.png │ ├─ thumbnails/ # 缩略图 │ │ ├─ small/ # 100x100 │ │ ├─ medium/ # 400x300 │ │ └─ large/ # 800x600 │ └─ processed/ # 处理后的图片(加水印等) │ ├─ 访问权限: │ ├─ 原图目录:私有(需签名访问) │ ├─ 缩略图目录:公共读 │ └─ 跨域 CORS:允许 *.myapp.com 访问 │ └─ 生命周期策略: ├─ 上传 7 天后:低频存储(省 40% 费用) ├─ 上传 90 天后:归档存储(省 70% 费用) └─ 上传 3 年后:自动删除(或转存到更便宜的冷存储) ``` **CORS 跨域配置**: ```xml https://myapp.com https://www.myapp.com GET HEAD * ETag x-oss-request-id 3600 ``` #### CDN 加速配置 **缓存策略配置**: ``` 全局默认规则: ├─ 缓存键:URL 路径 + 保留 w、h、format 查询参数 ├─ 默认 TTL:7 天 └─ 回源 HOST:自动跟随 按文件类型细分: ├─ *.html: │ ├─ TTL:5 分钟 │ └─ 优先从内存缓存读取 │ ├─ *.js, *.css: │ ├─ TTL:1 年 │ └─ 忽略查询参数(因为文件名有 hash) │ ├─ *.jpg, *.png, *.gif, *.webp: │ ├─ TTL:30 天 │ ├─ 保留查询参数(w、h、format 用于动态裁剪) │ └─ 启用图片自动压缩优化 │ └─ /api/*: ├─ TTL:0(不缓存) └─ 直接回源 ``` **HTTPS 优化配置**: ``` 证书配置: ├─ 证书类型:泛域名证书 *.myapp.com ├─ 部署方式:CDN 控制台上传,自动续期 └─ 备用证书:EV 证书用于主域名(显示绿色地址栏) TLS 配置: ├─ 最低 TLS 版本:1.2(兼容性与安全平衡) ├─ 最高 TLS 版本:1.3 ├─ 密码套件:仅启用强加密套件 ├─ OCSP Stapling:开启 ├─ TLS 会话复用:开启 Session Ticket └─ HSTS:开启(max-age=31536000) HTTP/2 与 HTTP/3: ├─ HTTP/2:开启(多路复用、头部压缩) ├─ HTTP/2 Server Push:按需开启(推荐用 Preload 替代) └─ HTTP/3 (QUIC):开启(实验性功能,逐步放量) ``` ### 7.4 成本控制策略 #### 费用构成分析 ``` 月度 CDN + 对象存储费用构成: CDN 部分: ├─ 下行流量费(大头,约 60%) │ ├─ 中国大陆:0.15-0.30 元/GB │ ├─ 亚太地区:0.40-0.80 元/GB │ └─ 欧美:0.30-0.60 元/GB │ ├─ 请求数费用(小头,约 5%) │ ├─ HTTP:0.01-0.05 元/万次 │ └─ HTTPS:0.05-0.15 元/万次(因为 TLS 握手消耗资源) │ ├─ 带宽峰值费用(可选计费方式) │ └─ 95 峰值计费:适合流量波动大的场景 │ └─ 增值功能费(约 5%) ├─ HTTPS 证书管理 ├─ WAF 防护 ├─ 实时日志推送 └─ 边缘脚本/函数 对象存储部分: ├─ 存储容量费(约 15%) │ ├─ 标准存储:0.12-0.15 元/GB/月 │ ├─ 低频存储:0.08-0.10 元/GB/月 │ └─ 归档存储:0.03-0.05 元/GB/月 │ ├─ 请求费用(约 5%) │ ├─ PUT:0.01-0.05 元/万次 │ └─ GET:0.005-0.01 元/万次 │ ├─ 数据取回费用(低频/归档) │ └─ 提前删除或取回收额外费用 │ └─ 回源出流量费(约 10%) └─ CDN 回源到对象存储的流量费 ``` #### 省钱技巧实战 **技巧 1:存储分级,自动生命周期管理** ```yaml # 生命周期规则示例 rules: - id: image-lifecycle prefix: uploads/ transitions: # 7 天后转低频存储,省 30% 费用 - days: 7 storageClass: IA # 90 天后转归档存储,省 70% 费用 - days: 90 storageClass: Archive # 3 年后自动删除 expiration: days: 1095 ``` **技巧 2:提高 CDN 命中率,减少回源** ``` 命中率从 90% 提升到 95% 意味着什么? 假设: - 日流量:10 TB - 命中率 90%:回源 1 TB - 命中率 95%:回源 0.5 TB 节省回源流量:0.5 TB/天 × 0.15 元/GB × 30 天 = 2250 元/月 ``` **技巧 3:压缩与格式优化** ``` 图片优化方案: ├─ 原图存储在对象存储(不直接对外) ├─ CDN 开启图片处理功能: │ ├─ 格式自动转换:JPEG → WebP/AVIF(省 30-50%) │ ├─ 质量自动压缩:视觉无损压缩(省 20-40%) │ ├─ 尺寸自适应:根据设备返回合适尺寸 │ └─ 渐进式加载:先模糊后清晰 └─ 效果:带宽成本降低 50-70% ``` **技巧 4:带宽峰值封顶与告警** ```yaml # 带宽封顶配置 bandwidth_cap: daily_limit: 500 # Mbps,日峰值超过则自动停用 CDN monthly_limit: 10000 # GB,月流量超过则停用 # 告警阈值 alerts: - threshold: 70% # 达到 70% 发告警 channels: [sms, email] - threshold: 90% # 达到 90% 打电话 channels: [phone] ``` --- ## 8. 总结:对象存储 + CDN 的黄金法则 ### 8.1 架构设计原则 **原则 1:动静分离** ``` 动态内容(API、HTML)→ 走源站或边缘函数 静态内容(图片、JS、CSS、视频)→ 走 CDN + 对象存储 ``` **原则 2:就近服务** ``` 用户在哪里,内容就缓存到哪里 → 选择覆盖广的 CDN 服务商 → 启用智能 DNS 调度 → 重要内容提前预热 ``` **原则 3:分层缓存** ``` 浏览器本地缓存(最强) ↓ CDN 边缘节点缓存(次强) ↓ CDN 中间层/区域节点(兜底) ↓ 对象存储/源站(最后防线) ``` **原则 4:成本与体验的平衡** ``` 存储分级:热数据标准存储,冷数据归档存储 缓存策略:高频内容长 TTL,低频内容短 TTL 压缩优化:WebP/AVIF 格式,智能质量压缩 监控告警:设置带宽封顶,防止异常流量 ``` ### 8.2 避坑清单 **存储桶命名与权限** - [ ] 桶名全局唯一,避免被占用 - [ ] 私有文件不要设置为公共读 - [ ] AccessKey 不要写在前端代码里,用 STS 临时凭证 - [ ] 启用服务端加密(SSE)保护敏感数据 **CDN 缓存配置** - [ ] HTML 文件 TTL 不要太长(建议 < 5 分钟) - [ ] JS/CSS 建议用带 hash 的文件名,TTL 设为 1 年 - [ ] 缓存键要合理,不要把用户信息等变量放进去 - [ ] 重要更新后记得刷新缓存或预热 **HTTPS 安全** - [ ] 证书不要过期,设置自动续期 - [ ] 最低 TLS 版本建议 1.2 - [ ] 开启 HSTS 防止降级攻击 - [ ] 敏感 Cookie 设置 Secure 和 HttpOnly **成本控制** - [ ] 开启带宽封顶告警,防止异常流量 - [ ] 低频/归档存储有最小存储时间和提前删除费,注意规则 - [ ] 回源流量费也很贵,努力提高 CDN 命中率 - [ ] 定期分析访问日志,清理僵尸资源 --- ## 9. 实战代码模板 ### 9.1 前端直传对象存储(JavaScript) ```javascript /** * 对象存储直传工具类 * 支持:阿里云 OSS、腾讯云 COS、AWS S3 */ class DirectUploader { constructor(config) { this.provider = config.provider // 'oss' | 'cos' | 's3' this.region = config.region this.bucket = config.bucket this.getCredentials = config.getCredentials // 获取临时凭证的函数 } /** * 获取 STS 临时凭证 */ async fetchCredentials() { // 向后端申请临时凭证 const credentials = await this.getCredentials() return { accessKeyId: credentials.accessKeyId, accessKeySecret: credentials.accessKeySecret, sessionToken: credentials.securityToken || credentials.sessionToken, expiration: credentials.expiration } } /** * 生成上传签名(适用于前端计算签名) */ generateSignature(credentials, fileKey, fileType, options = {}) { const timestamp = new Date().toISOString() const date = timestamp.slice(0, 10).replace(/-/g, '') // 不同厂商的签名算法略有差异 switch (this.provider) { case 'oss': return this._ossSignature(credentials, fileKey, date, options) case 'cos': return this._cosSignature(credentials, fileKey, date, options) case 's3': return this._s3Signature(credentials, fileKey, date, options) default: throw new Error('Unknown provider') } } /** * 单文件上传(小文件 < 100MB) */ async upload(file, options = {}) { const credentials = await this.fetchCredentials() const fileKey = this._generateFileKey(file, options.directory) const formData = new FormData() // 构建表单字段(不同厂商字段名不同) const formFields = this._buildFormFields( credentials, fileKey, file.type, options ) Object.entries(formFields).forEach(([key, value]) => { formData.append(key, value) }) formData.append('file', file) // 发送上传请求 const uploadUrl = this._getUploadUrl() const response = await fetch(uploadUrl, { method: 'POST', body: formData, // 如果上传大文件,可能需要设置更长的超时 signal: options.signal // 支持 AbortController 取消上传 }) if (!response.ok) { const errorText = await response.text() throw new Error(`Upload failed: ${response.status} ${errorText}`) } return { url: this._getFileUrl(fileKey), key: fileKey, etag: response.headers.get('ETag'), size: file.size } } /** * 分片上传(大文件 > 100MB) */ async multipartUpload(file, options = {}) { const partSize = options.partSize || 10 * 1024 * 1024 // 默认 10MB/片 const parallel = options.parallel || 3 // 默认 3 个并发 const credentials = await this.fetchCredentials() const fileKey = this._generateFileKey(file, options.directory) // 1. 初始化分片上传 const uploadId = await this._initMultipartUpload( credentials, fileKey, file.type ) // 2. 计算分片 const parts = [] const totalParts = Math.ceil(file.size / partSize) for (let i = 0; i < totalParts; i++) { const start = i * partSize const end = Math.min(start + partSize, file.size) parts.push({ number: i + 1, start, end, blob: file.slice(start, end) }) } // 3. 上传分片(带并发控制和断点续传) const uploadedParts = [] const failedParts = [] // 支持断点续传:检查哪些分片已上传 if (options.resume) { const existingParts = await this._listParts( credentials, fileKey, uploadId ) for (const part of existingParts) { uploadedParts.push(part) } } // 过滤出未上传的分片 const pendingParts = parts.filter( (p) => !uploadedParts.some((up) => up.partNumber === p.number) ) // 并发上传 const uploadPart = async (part) => { try { const etag = await this._uploadPart( credentials, fileKey, uploadId, part ) return { partNumber: part.number, etag } } catch (error) { failedParts.push({ part, error }) throw error } } // 使用 Promise.all 控制并发 const chunks = [] for (let i = 0; i < pendingParts.length; i += parallel) { chunks.push(pendingParts.slice(i, i + parallel)) } for (const chunk of chunks) { const results = await Promise.allSettled(chunk.map(uploadPart)) for (const result of results) { if (result.status === 'fulfilled') { uploadedParts.push(result.value) } } } // 检查是否所有分片都上传成功 if (uploadedParts.length !== totalParts) { throw new Error( `Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded` ) } // 4. 完成分片上传(合并分片) await this._completeMultipartUpload( credentials, fileKey, uploadId, uploadedParts ) return { url: this._getFileUrl(fileKey), key: fileKey, size: file.size, parts: totalParts } } /** * 生成文件存储路径 */ _generateFileKey(file, directory = '') { const date = new Date() const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}` const random = Math.random().toString(36).substring(2, 10) const ext = file.name.split('.').pop() || 'bin' const key = directory ? `${directory}/${datePath}/${random}.${ext}` : `${datePath}/${random}.${ext}` return key } // ============ 各厂商特定方法 ============ _getUploadUrl() { switch (this.provider) { case 'oss': return `https://${this.bucket}.oss-${this.region}.aliyuncs.com` case 'cos': return `https://${this.bucket}.cos.${this.region}.myqcloud.com` case 's3': return `https://${this.bucket}.s3.${this.region}.amazonaws.com` default: throw new Error('Unknown provider') } } _getFileUrl(key) { return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${ this.provider === 'oss' ? 'aliyuncs.com' : this.provider === 'cos' ? 'myqcloud.com' : 'amazonaws.com' }/${key}` } // 各厂商的签名、分片上传等方法...(根据实际需求实现) _buildFormFields(credentials, fileKey, fileType, options) { // 各厂商表单字段构建逻辑 // 这里需要根据具体厂商的文档实现 return {} } async _initMultipartUpload(credentials, fileKey, fileType) { // 各厂商初始化分片上传逻辑 return 'upload-id' } async _uploadPart(credentials, fileKey, uploadId, part) { // 各厂商分片上传逻辑 return 'etag' } async _completeMultipartUpload(credentials, fileKey, uploadId, parts) { // 各厂商完成分片上传逻辑 } async _listParts(credentials, fileKey, uploadId) { // 各厂商列出已上传分片逻辑 return [] } } // 使用示例 const uploader = new DirectUploader({ provider: 'oss', region: 'cn-beijing', bucket: 'myapp-images-prod', getCredentials: async () => { // 向后端申请临时凭证 const res = await fetch('/api/upload/credentials') return res.json() } }) // 小文件上传 async function uploadAvatar(file) { try { const result = await uploader.upload(file, { directory: 'avatars', onProgress: (progress) => { console.log(`上传进度: ${progress.percent}%`) } }) console.log('上传成功:', result.url) return result } catch (error) { console.error('上传失败:', error) throw error } } // 大文件分片上传 async function uploadVideo(file) { try { const result = await uploader.multipartUpload(file, { directory: 'videos', partSize: 10 * 1024 * 1024, // 10MB 每片 parallel: 3, // 3 个并发 resume: true, // 支持断点续传 onProgress: (progress) => { console.log( `上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}` ) }, onPartComplete: (part) => { console.log(`分片 ${part.number} 上传完成`) } }) console.log('上传成功:', result.url) return result } catch (error) { console.error('上传失败:', error) // 可以在这里实现重试逻辑或保存断点信息 throw error } } ``` ### 9.2 后端临时凭证服务(Node.js/Express) ```javascript /** * 对象存储 STS 临时凭证服务 * 支持:阿里云 OSS、腾讯云 COS、AWS S3 */ const express = require('express') const STS = require('ali-oss').STS // 阿里云 // const COS = require('cos-nodejs-sdk-v5') // 腾讯云 const router = express.Router() // 配置 const config = { // 阿里云 OSS 配置 oss: { accessKeyId: process.env.OSS_ACCESS_KEY_ID, accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, region: 'oss-cn-beijing', bucket: 'myapp-images-prod', // STS 角色 ARN(需要在 RAM 控制台创建) roleArn: process.env.OSS_STS_ROLE_ARN } } /** * 获取 STS 临时凭证(阿里云 OSS) * POST /api/upload/credentials */ router.post('/credentials', async (req, res) => { try { // 1. 验证用户身份(根据实际情况实现) const userId = req.user?.id if (!userId) { return res.status(401).json({ error: 'Unauthorized' }) } // 2. 生成唯一的文件路径前缀(用于权限隔离) const date = new Date() const prefix = `uploads/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${userId}/` // 3. 创建 STS 客户端 const sts = new STS({ accessKeyId: config.oss.accessKeyId, accessKeySecret: config.oss.accessKeySecret }) // 4. 申请临时凭证 const result = await sts.assumeRole( config.oss.roleArn, { // Policy 限制权限范围(最小权限原则) Statement: [ { Effect: 'Allow', Action: [ 'oss:PutObject', 'oss:InitiateMultipartUpload', 'oss:UploadPart', 'oss:CompleteMultipartUpload', 'oss:AbortMultipartUpload', 'oss:ListParts' ], Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`] } ], Version: '1' }, 3600, // 凭证有效期 1 小时 'web-upload-session-' + Date.now() ) // 5. 返回凭证和配置 res.json({ success: true, data: { // STS 临时凭证 credentials: { accessKeyId: result.credentials.AccessKeyId, accessKeySecret: result.credentials.AccessKeySecret, sessionToken: result.credentials.SecurityToken, expiration: result.credentials.Expiration }, // 上传配置 config: { provider: 'oss', region: config.oss.region, bucket: config.oss.bucket, endpoint: `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com`, prefix: prefix, // 文件路径前缀 // 安全限制 maxSize: 100 * 1024 * 1024, // 最大 100MB allowedTypes: [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4' ] } } }) } catch (error) { console.error('Get credentials failed:', error) res.status(500).json({ success: false, error: 'Failed to get upload credentials', message: error.message }) } }) /** * 回调通知:前端上传完成后通知后端 * POST /api/upload/callback */ router.post('/callback', async (req, res) => { try { const { key, etag, size, mimeType, originalName } = req.body const userId = req.user?.id // 1. 验证文件是否存在 // 2. 保存文件信息到数据库 const fileRecord = await db.files.create({ userId, key, etag, size, mimeType, originalName, url: `https://cdn.example.com/${key}`, createdAt: new Date() }) // 3. 异步处理:生成缩略图、提取元数据、内容审核等 await processFileAsync(fileRecord) res.json({ success: true, data: { fileId: fileRecord.id, url: fileRecord.url, size: fileRecord.size } }) } catch (error) { console.error('Upload callback failed:', error) res.status(500).json({ success: false, error: 'Failed to process uploaded file' }) } }) module.exports = router ``` ### 9.3 防盗链与安全配置 ```javascript /** * CDN 防盗链与安全配置示例 */ // 1. Referer 防盗链(防止其他网站直接引用你的资源) const refererConfig = { // 白名单模式:只允许以下 Referer 访问 allowList: [ '*.myapp.com', // 主站 '*.myapp.cn', // 国内站 'localhost:*', // 本地开发 '127.0.0.1:*' ], // 黑名单模式(可选):禁止以下 Referer blockList: [ '*. competitor.com', // 竞争对手 'spam-site.com' ], // 空 Referer 处理:是否允许直接访问(浏览器地址栏输入 URL) allowEmptyReferer: false // 生产环境建议 false,测试环境可 true } // 2. URL 鉴权(更安全的防盗链,带时间戳和签名) class URLAuth { constructor(config) { this.key = config.key // 鉴权密钥,只在服务端保存 this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期 } /** * 生成带鉴权的 URL * @param {string} url - 原始 URL,如 https://cdn.example.com/images/photo.jpg * @param {number} expireIn - 有效期(秒) * @returns {string} 带鉴权参数的 URL */ sign(url, expireIn = this.expireTime) { const urlObj = new URL(url) const pathname = urlObj.pathname const timestamp = Math.floor(Date.now() / 1000) + expireIn // 构造签名字符串(不同厂商格式不同,这里是通用示例) const signStr = `${pathname}-${timestamp}-${this.key}` const signature = this._md5(signStr) // 添加鉴权参数 urlObj.searchParams.set('sign', signature) urlObj.searchParams.set('t', timestamp.toString()) return urlObj.toString() } /** * 验证 URL 签名(在 CDN 边缘或源站使用) */ verify(url) { const urlObj = new URL(url) const signature = urlObj.searchParams.get('sign') const timestamp = parseInt(urlObj.searchParams.get('t')) const pathname = urlObj.pathname // 检查是否过期 if (timestamp < Math.floor(Date.now() / 1000)) { return { valid: false, error: 'URL expired' } } // 验证签名 const signStr = `${pathname}-${timestamp}-${this.key}` const expectedSign = this._md5(signStr) if (signature !== expectedSign) { return { valid: false, error: 'Invalid signature' } } return { valid: true } } _md5(str) { // 实际项目中使用 crypto-js 或其他 MD5 库 // 这里仅作示例 return require('crypto').createHash('md5').update(str).digest('hex') } } // 使用示例 const auth = new URLAuth({ key: 'your-secret-key-only-known-by-server', expireTime: 3600 // 1 小时有效期 }) // 服务端生成带签名的 URL const signedUrl = auth.sign( 'https://cdn.example.com/private/document.pdf', 7200 ) // 结果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456 // CDN 边缘或源站验证 const result = auth.verify(signedUrl) if (!result.valid) { // 返回 403 Forbidden } // 3. IP 黑白名单 const ipConfig = { // 只允许特定 IP 访问(适合内部系统) whiteList: [ '192.168.1.0/24', // 内网网段 '10.0.0.0/8' ], // 禁止特定 IP 访问(封禁攻击者) blackList: ['1.2.3.4', '5.6.7.8'] } // 4. UA(User-Agent)黑白名单 const uaConfig = { // 禁止爬虫/下载工具 blackList: [ 'Wget', 'curl', 'python-requests', 'Scrapy', 'AhrefsBot', 'SemrushBot' ], // 只允许浏览器访问(严格模式) whiteList: [ 'Mozilla/*', // 现代浏览器 'AppleWebKit/*' ] } ``` --- ## 10. 名词对照表 | 英文术语 | 中文对照 | 解释 | | :------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------- | | **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 | | **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 | | **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 | | **CDN** | 内容分发网络 | Content Delivery Network,通过在全球部署边缘节点,将网站内容缓存到离用户最近的位置,加速访问速度。 | | **Edge Node** | 边缘节点 | CDN 网络中部署在各地的缓存服务器,直接为用户提供内容访问服务。 | | **Origin** | 源站 | CDN 回源获取内容的服务器,可以是对象存储、ECS 或自建服务器。 | | **Cache Hit** | 缓存命中 | 用户请求的内容在 CDN 边缘节点已存在,直接返回,无需回源。 | | **Cache Miss** | 缓存未命中 | 边缘节点没有请求的内容,需要回源获取。 | | **Hit Ratio** | 命中率 | 缓存命中次数占总请求次数的比例。命中率越高,回源越少,成本越低。 | | **TTL** | 生存时间/缓存时间 | Time To Live,内容在 CDN 节点上缓存的有效期。过期后需要重新回源。 | | **Back to Source** | 回源 | CDN 边缘节点向源站请求内容的过程。 | | **Purge/Refresh** | 刷新缓存 | 强制使 CDN 缓存失效,下次请求回源获取最新内容。 | | **Preheat** | 预热 | 在正式发布前,主动将内容推送到 CDN 节点,让用户第一次访问就能命中缓存。 | | **CORS** | 跨域资源共享 | Cross-Origin Resource Sharing,浏览器的安全机制,控制不同域之间的资源访问。 | | **Referer** | 来源页面 | HTTP 请求头字段,指示请求是从哪个页面链接过来的。用于防盗链。 | | **STS** | 安全令牌服务 | Security Token Service,颁发临时访问凭证的服务,用于前端直传等场景。 | | **Multipart Upload** | 分片上传 | 将大文件切分成多个小分片并行上传,支持断点续传,提高上传效率和可靠性。 | | **ETag** | 实体标签 | HTTP 响应头,用于标识资源的特定版本,常用于缓存验证。 | | **S3 API** | S3 兼容接口 | AWS S3 的对象存储 API 规范,多数云厂商的对象存储都兼容此接口。 | | **Canonical Query String** | 规范查询字符串 | 签名字符串的一部分,用于计算请求签名,确保请求不被篡改。 | --- ## 总结:对象存储 + CDN 的黄金法则 1. **上传走直传**:大文件用分片,安全用 STS 2. **缓存分层次**:浏览器 -> CDN -> 源站,层层缓存 3. **就近服务用户**:智能 DNS + 全球节点覆盖 4. **安全不松懈**:HTTPS + 防盗链 + 访问控制 5. **成本要监控**:命中率、带宽、存储分级,持续优化 这套架构撑起了互联网绝大部分的静态资源访问,理解它,你就理解了现代 Web 性能优化的基石。