Skip to content
Zegging's Tech Blog
Go back

AWS 回源新架构:一次救火中完成的静态资源动态出海

项目要完蛋啦

2024年H2的时候国际化业务需要上线一个性能优化的项目,目的是为了解决海外用户在打开方案编辑器时的长时间等待问题。相比较于用户在国内访问海外用户的等待时间近乎翻倍了。虽说国内的网络基建很好,海外也确实存在不少弱网环境的用户,但是新加坡、美国、韩国、日本等国家用户的非空方案打开时间仍然偏高。于是在8月国际化业务组内的同事启动了这个项目,出于兴趣我一直在关注项目的进度。

非空方案打开的时候浏览器不仅需要加载js文件,还需要加载很多和方案相关的模型文件、贴图文件、图片等静态资源。虽说有并行优化,但是方案本身的构建可以理解为是一个串行的阻塞的流程。项目的主要工作就是通过将静态资源数据用AWS CDN做访问加速来提升客户端对静态资源的加载速度,进一步提升方案的打开速度。目标是清晰的,但是实现上出了挺大的问题……

线上404故障

项目最初的负责人是一位资深前端开发,在做了一些数据分析和方案调研后直接拉了国内的业务开发和运维同事对工具内业务使用的CDN进行直接替换,替换后经过简单验证就直接上线了。业务方将CDN域名替换为出海域名后,大量资源无法访问。10/9 发现4 域名回源有问题,10/16扩大到9个域名不可用。方案的核心架构是:海外用户访问 *.coohom.com (AWS CDN) → 如果 AWS S3 没有该资源,需要回源到国内*.kujiale.com (腾讯云 COS) 拉取。资源无法访问的根因有三类:

  1. 回源策略未配置 / 配置不完整:初期采用”一刀切”替换所有域名,但很多域名的 AWS S3 物理 Bucket 还没建好、回源策略也没配。10/9 发现 4 个域名回源有问题,就是 AWS CDN 找不到资源又无法回源到国内 COS。
  2. 资源本身特性导致回源不可行
    • dcs-model-task-oss.coohom.com:资源有效期仅 1 天,回源成功时资源可能已过期
    • qhyxpic.oss.coohom.com:多级子域名 CDN,配置回源策略极其复杂
  3. 中间件服务兼容性问题
    • spriteoss.coohom.com 域名在浏览器端访问正常,但后台中间件服务访问该域名不通,仅是 CDN 层面问题,还涉及服务端网络链路

造成线上故障的原因是最初的上线方案是一个黑名单模式:默认替换所有域名,发现哪个有问题就加入黑名单不替换。问题在于:先上线、后发现问题,每发现一个不可用域名就意味着线上用户已经受影响。9个域名是分批次才发现不可用的,整个项目组在那周始终处于”救火”状态。正常来说在做这种大影响面变更的时候我们一般会使用白名单模式:

  1. 先配置测试域名(如 test.coohom.com)
  2. 自动化门禁验证各项功能正常
  3. 验证通过后,才将正式域名加入白名单生效并执行灰度发布:当晚 20% → 次日中午 50% → 次日晚上 100%
  4. 出问题时从白名单移除即可秒级回退到原始 kujiale.com 域名

换方案:资源动态搬运方案 - 性能衰退!

上一个方案最重要的问题是做了AWS CDN和腾讯云CDN的“对接”,但是没有考虑到资源的物理位置,而物理位置才是访问速度的绝对上限。纯CDN域名替换只解决了”域名指向”问题——把 kujiale.com 换成 coohom.com,让请求走 AWS CDN。但它没有解决资源的物理位置问题:

  • AWS S3 桶是空的,海外用户每次请求都必须回源到国内腾讯云 COS 拉取
  • 跨海回源本身就慢,且每次请求都要走一遍,没有缓存积累

简言之:域名换了,但资源还在国内,用户访问依然慢。

那么是不是考虑把资源搬运出去放在离用户更近的地方就行解决这个问题呢?答案是:是的。但是没有想象中的简单:

全量同步的不可能 - 动态出海

海外用户相对于国内用户还是一个比较小的基数,但是核心业务组的数据是不区分数据归属的对象是海外还是国内,如果要改造整体链路上的各个环节都要进行适配,这是第一个不可能。还有第二个不可能,数据量太大了,要花的钱太多。

所以第二阶段的方案换成了动态出海:——不提前全量同步,而是用户首次访问时触发:

  1. 请求到达 AWS CDN → S3 无此资源
  2. 某个功能被触发,从 COS 拉取资源拷贝到 S3
  3. 同时返回 302 重定向,让客户端去 COS 拿
  4. 后续用户再访问时,资源已在 S3,直接命中

看似是完美解决了方案一的问题,只需要解决一点:某个功能到底是什么功能?

项目组最终采用的方案是:AWS 原生方案 Lambda@Edge,这是 AWS 官方推荐的 CDN 边缘处理方式,看似最”标准”

  1. 无需预同步:不需要提前梳理和迁移海量存量数据,资源”谁访问谁触发”,按需出海
  2. 开发成本低 相比自建同步服务,Lambda 是配置+少量代码,看起来能快速上线

但是这个技术选型忽略了很重要的因素:

冷启动问题

Lambda@Edge 在长时间无调用后,首次触发需要初始化运行环境(加载运行时、初始化代码),这个过程通常 100ms~2s。对于资源回源场景,每个域名的每个资源路径都可能触发不同的 Lambda 实例,冷启动频繁发生,延迟叠加到每次资源请求上。长期预留示例也无法解决动态流量峰值时新增实例的冷启动问题,并且还有另一笔成本。

函数冻结问题

AWS对 Lambda 的核心限制在于:Lambda@Edge 必须在请求-响应周期内同步完成。AWS 不提供在 Lambda@Edge 中”返回响应后继续后台异步执行”的机制——函数返回后就被冻结了。技术上完全可以这样做:

origin-request 触发器:

  1. 发起异步 COS→S3 拷贝(发个消息到后台服务)
  2. 立即返回 302 重定向到 COS,不等拷贝完成,不阻塞用户

但团队选择了同步等待拷贝完成,我并不清楚这个决定的具体原因,有可能是编码问题,也有可能是没有仔细阅读 AWS 文档并进行实验。

设计定位问题

Lambda 不适合大文件拷贝:模型文件(.pop)动辄几 MB,COS→S3 跨海拷贝远超 30 秒超时,Lambda 根本跑不完。根本矛盾在于 Lambda@Edge 的设计定位是轻量级请求/响应处理(修改 header、生成短响应、A/B 测试等),而跨海大文件拷贝是重量级 IO 密集型任务,本质上是用错了工具。

等到了这个时候项目组基本上已经急眼了,一个项目做了几个月,不出成果也就算了,还搞成了负向优化这谁能接受?

救火

好巧不巧,我在学习整理国际化业务的技术架构的时候发现和基建网络相关(其实就是AWS上的各种云服务)的信息非常的匮乏,所以我曾经和运维申请了只读权限去各个系统里学习梳理了一段时间。和项目组同学进行了一些沟通并旁听了一次项目周会后我发现这个问题不是没有解决方法,只要解决几个关键问题:

  1. 保留 AWS CDN -> 腾讯云 CDN 的方案来简化 CDN 的复杂配置问题,业务层只需要变更 CDN 域名即可。其他的复杂实现完全屏蔽。
  2. 将“第一次资源获取”的同步阻塞逻辑改为异步,优先保证切换CDN后非空方案打开的性能没有衰退。
  3. 如何保证同步到海外的数据一致、如何监控和处理同步延迟

新架构 - 降级回源+异步消费

AWS CDN 链路里 CloudFront Origin Group 承担了很重要的兜底职责:主源是海外 S3,备源是国内 CDN。当主源返回 403 或 404 时,CloudFront 会切到备源,继续从国内链路获取资源。从 AWS 的能力定义看,Origin Group 本来是高可用和灾备能力:主源不可用,或者返回指定错误码时,CloudFront 才回源到备源。AWS 官方文档也明确说明,CloudFront 会先请求主源,只有主源返回配置的 failover 状态码,才会切到备源;并且这种 failover 只对 GETHEADOPTIONS 生效。但在我们的业务里,403/404 不完全代表“主源故障”。更多时候,它代表“这个国内资源还没有同步到海外 S3”。这让 Origin Group 从灾备工具变成了日常数据路径:

  1. 海外 S3 没有资源,返回 403/404。
  2. CloudFront failover 到国内 CDN。
  3. 用户仍然拿到资源。
  4. 但如果没有后续同步动作,下一次请求仍然会重复同样的回源。

新架构的目标

我希望新架构解决三个问题:

  1. 用户请求不能被同步链路拖慢。 当前请求优先保证能返回资源,动态同步放到异步链路里做。
  2. 数据不能有缺失。 一些已经部署到海外的业务实际读写的本身就是 S3,回源的源站要是目标资源的超集。
  3. S3 miss 要变成可消费的事件。 403/404 不只用于 failover,也用于驱动后续资源同步。

因此,新架构被拆成两层:

  • 第一层是 CDN 回源层。CloudFront 使用 Origin Group “海外 S3 主源 + 国内 CDN 备源”的兜底能力。
  • 第二层是异步同步层。S3 server access log 记录主源访问结果,这个日志可以被投递到 SQS(AWS 的消息队列云服务)中。通过异步任务消费 403/404 日志,再把缺失资源从国内对象存储同步到海外 S3。

这两层之间没有强同步依赖。用户请求不等待同步完成,同步任务也不决定当前请求是否成功。它只负责让下一次访问变快。

CDN 配置怎么改

1. 保留 Origin Group,但收敛它的职责

Origin Group 仍然有必要存在,因为它解决的是“当前请求不能因为海外 S3 缺资源而失败”的问题。

新的定位是:

  • 主源:海外 S3,承载已经出海的资源。
  • 备源:国内 CDN/COS,作为兜底来源。
  • failover 状态码:保留 403/404,用来表达海外 S3 当前没有这个资源。

实际上这个设计不过是在实现一个定义:在按需出海架构里,主源缺资源是一种正常的中间状态。Origin Group 让用户当前请求继续成功;异步日志消费让这个中间状态逐渐消失。

2. 用 CloudFront Function 替代一部分回源规则

历史链路里,每个 CDN 都和三个独立的东西相关:

  1. S3 Bucket:一个业务使用的独立 OSS bucket
  2. 腾讯云 CDN:回源数据的访问 CDN,其本质是 COS 的一个 bucket
  3. 回源规则:普通文件直接回源,视频和图片需要经过一个特殊服务进行处理。

最后这个架构的流程大概可以梳理成这个样子,比较恶心:

旧链路:文件后缀分流 + 多层 Origin Group

CloudFront Function 的价值在这里不是“写代码显得高级”,而是将复杂度统一封装到一个函数中,将各个层级隐含的配置清晰暴露出来。这样做有两个好处:

  1. 不需要为了每一种文件后缀持续补 CDN Behavior。
  2. 原来的“主源失败后回国内”的兜底能力还能保留。

需要注意的是,Function 修改的是请求主源。如果图片处理服务不可用,请求会继续按照 Origin Group 的 failover 规则切到备源。这个设计适合“海外 S3 是国内资源子集”的场景;如果存在只上传到海外、国内没有的资源,就必须为这类资源设计独立链路,不能盲目复用同一套 failover。

S3 404 日志异步消费怎么做

CDN 层解决的是“当前请求怎么返回”,异步同步层解决的是“下一次请求怎么更快”。在这个模块中的输入是 S3 server access log。开启 S3 访问日志后,S3 会把每一次对象请求记录到指定日志 bucket。日志里包含 bucket、key、request URI、HTTP status 等字段。我们关心的是主源 S3 返回 403/404 的记录,因为它代表“这个资源被海外用户访问了,但海外 S3 当前没有”。

整体流程是:

  1. 给海外 S3 bucket 开启 server access logging。
  2. 日志写入统一日志 bucket 的固定前缀,比如 logs/
  3. 后台定时任务扫描日志文件,下载后按行解析。
  4. 找出 403/404 的对象请求。
  5. 根据海外 bucket 找到对应的国内对象存储 bucket。
  6. 投递同步消息,或者直接调用同步服务。
  7. 同步服务把国内资源复制到海外 S3。
  8. 日志处理成功后删除已消费日志。

这里也调研过用 S3 Event Notification 触发解析。它的好处是天然可以多实例并发消费,链路更实时;但代价是系统复杂度更高。考虑到这个同步链路本来就不要求实时,只要求最终一致,第一版选择了定时任务拉取日志:实现简单,发布风险低,也足够满足“按需动态出海”的目标。里还有一个当时很重要的取舍:如果追求“更优雅”的事件驱动,S3 Event Notification + 多实例消费当然更漂亮;但救火场景里最重要的是可控、可验证、能尽快落地。定时任务拉日志不酷,但它足够直观,失败后也容易恢复。

不过后续优化成了实时数据同步,也算是不忘初心。

日志解析里最容易踩的坑

S3 server access log 看起来是空格分隔,但不能直接用 split(" ")。原因是日志字段里有几类内容本身就可能包含空格:

  • 时间字段被包在 [] 里。
  • Request-URI 被包在引号里,里面包含 HTTP method、path、protocol。
  • User-Agent、Referer 等字段也可能包含空格。

如果简单按空格切分,字段下标很容易错位,最后把 status、key 或 request URI 解析错。更稳妥的方式是按状态机扫描单行日志:

  1. 普通字段按空格切。
  2. 遇到 [ 后,直到 ] 才结束时间字段。
  3. 遇到 " 后,直到下一个匹配引号才结束 Request-URI。
  4. 解析到 Request-URI 后,继续读取下一个字段作为 HTTP status。
  5. 拿到 bucket、key、Request-URI、status 后即可停止,不需要解析整行所有字段。

AWS 也在文档里说明,S3 access log 未来可能在行尾追加新字段。因此解析器应该只依赖自己需要的前序字段,并且允许尾部存在未知字段。

线上发布

这次变更涉及多个业务方、多个 CDN 域名和多种资源类型,不能用“一把梭”的方式发布。真实推进时,我们把验证拆成了几层。

测试验证

第一层是测试域名验证。正式域名发布前,先由运维配置对应的测试 CDN 域名,开发和测试在线上环境验证完整链路。每个域名都要分别验证:

  • 普通资源能否访问成功。
  • 海外 S3 命中时能否正常返回。
  • 海外 S3 miss 时能否回源到国内。
  • 图片裁剪、缩放等处理能力在国内回源和 S3 回源两种情况下是否都可用。
  • 模型文件这类非图片资源是否能正常返回。

业务灰度

第二层是业务灰度。CDN 配置准备上线前,先通过业务侧配置把普通用户流量切回旧域名,避免配置变更期间影响线上用户。配置上线后,再把测试账号加入白名单,用分桶方式验证正式域名。实际节奏不是一次全开,而是先小流量,再逐步放大:先 20%,再 50%,最后 100%。如果某个域名无法通过分桶精细控制,就选择在低峰时段短时间放开验证,验证结束后再决定是否继续推进。

最终计划出海的 8 个 CDN 域名全部完成了出海,用户方案打开性能大幅优化

整体用户方案打开性能P75数据

稳定性取舍

实际上新方案舍弃了资源一定被及时同步到海外的目标。如果把资源复制放在用户请求链路里,用户体验就会被最慢的 IO 操作拖住。跨云复制、国内外网络、对象存储读写,本来就不适合成为前台请求的关键路径。如果对异步消费任务做一致性验证会让整体方案变得复杂,但是实际上对加快用户非空方案打开这个最终目标没有显著的提升。

所以它的稳定性设计是最终一致:

  • 当前请求:由 CloudFront Origin Group 回源兜底。
  • 后续同步:由 S3 404 日志驱动。
  • 解析失败:日志文件不删除,下次任务继续处理。
  • 消费失败:依赖消息重试或任务重试。
  • 偶发日志丢失:下一次资源 miss 仍然会再次产生日志。

这不是一个强实时系统,但它符合业务目标。我们真正要优化的是海外用户持续访问时的体验,而不是让每个冷资源第一次访问都立刻变成热资源。

同时,系统还陆续建设了几个重要的基础监控:

  • S3 日志文件积压量。
  • 日志解析成功/失败数量。
  • 403/404 资源数量趋势。
  • 同步消息积压量。
  • 同步成功率和失败原因。
  • 海外 S3 命中率变化。

有了这些指标,动态出海的优化才成为了一个可以被观察、被验证、被持续优化的功能。

机会留给保持好奇又不断探究的人

对于 AWS 服务的了解和学习全都是在日常工作之余进行的,一是对业务本身依赖的底层技术的好奇,二是希望不断学习新的东西来开阔自己的认识范围。如果都不能认识到有这样那样的方案,又怎么能在问题出现的时候找到解法?我又不是什么天才。

或许这个blog应该换个标题,就叫:三天时间救火一个横跨三个月的关键项目,我做对了什么?。实际上这三天并不关键,只是花了一些时间做落地验证,方案和方案的可行性在我拿到这个问题瞬间就想到了。但这完全依赖于之前持续数个月我对“毫无直接价值的技术”的投入。如果没有这个项目这些知识是不是永远无法变现呢?或许。但对于一个希望从事很久很久的职业和工作来说,希望不断见到好的东西,并且希望自己不断做出好的东西,这才是更重要的。

在成熟公司里开发一般只负责一小部分功能、技术栈、业务,但实际上相关领域还有很多可以探索的空间。我们可以适时地跳出权限、组织架构的禁锢,去主动尝试解决一些不在自己职责范围内的问题,通常都会有些不同的收获,也会获得很多意想不到的认可。

参考资料


Share this post on:

Previous Post
AWS S3 回源配置踩坑:从 x-oss-process 到 CloudFront Function