导读:本期聚焦于郑钧天创作的《CDN Notification API 如何在不压垮源站的前提下实现用户触达?》,敬请观看详情。有没有办法既保证消息推送的即时性,又不让源站为每条通知承担全部带宽和计算压力?把通知分发逻辑前置到 CDN 边缘节点,就能在距离用户最近的位置完成鉴权、组装和投递。这篇文章会拆解 CDN Notification API 的工作链路,说明边缘缓存、Web Push 订阅管理和 SSE 长连接复用各自扮演的角色。文中对比了边缘推送与直连源站在延迟、可用性和成本上的差异,并给出一个在 Cloudflare Workers 上实现轻量级通知接口的完整代码示例。看完之后,你可以判断现有 CDN 架构是否适合叠加这样一层用户触达能力,以及如何避免边缘节点会话状态丢失的常见坑。

CDN 最初被设计来加速静态资源分发,但今天它的角色已经扩展到动态请求处理和实时消息推送。所谓 CDN Notification API,并不是某个统一的标准,而是一类把通知发送接口部署在边缘节点上的实现模式。核心思路很简单:用户的订阅信息、推送凭证和消息模板都缓存在离用户最近的边缘节点中,当需要触达用户时,请求首先命中边缘 API,由边缘节点完成鉴权和消息组装,再调用浏览器推送服务或直接通过 SSE 流下发给客户端。这种结构避免了每一次通知都穿透回源站,从而显著降低源站负载和端到端延迟。

CDN Notification API 如何在不压垮源站的前提下实现用户触达?

理解这个模式,需要先弄清楚两个基本组件:一个是 Web Push 协议中浏览器、推送服务和应用服务器之间的三角关系;另一个是 CDN 边缘节点上可以运行代码的运行时环境,例如 Cloudflare Workers、Fastly Compute 或者 AWS CloudFront Functions。传统 Web Push 中,应用服务器保存用户的订阅对象,其中包含 endpoint、p256dh 和 auth 三个关键字段。发送通知时,服务器需要用 VAPID 私钥对请求签名,然后向浏览器厂商的推送服务发送加密后的载荷。如果把这些操作搬到 CDN 边缘,最直接的好处是签名计算和请求投递发生在离用户更近的网络位置,同时源站只需要维护订阅数据库,不需要参与每一次下发。

边缘侧实现 Web Push 通知的关键步骤

在 CDN 边缘节点上处理 Web Push,第一步是管理订阅生命周期。用户在前端调用 registration.pushManager.subscribe() 之后,得到的订阅对象需要发送给边缘 API 进行存储。边缘节点通常没有持久化数据库,所以会用边缘键值存储(例如 Cloudflare KV)或者直接回源到中心数据库。为了减少回源次数,可以在边缘缓存订阅对象的哈希摘要,并设置较短的 TTL。这里有一个容易被忽略的细节:Web Push 的 endpoint 中包含用户代理和推送服务提供商的标识,如果把 endpoint 作为缓存键,需要保证它的长度和特殊字符不会破坏边缘缓存系统的键规则。常见的做法是先对 endpoint 做一次 SHA-256 摘要,再用十六进制字符串作为键。

第二步是消息的加密与签名。Web Push 使用 RFC 8291 定义的消息加密方案,需要根据用户的 p256dh 公钥和 auth 密钥生成加密内容。VAPID 签名则要求生成一个 JWT,其中包含 aud、exp 和 sub 等声明,并用应用服务器的私钥签名。这个计算过程可以在边缘节点完成,但前提是私钥不能被暴露在边缘代码中。推荐的做法是把私钥存放在环境变量或密钥管理服务中,边缘运行时通过 API 读取。对于安全性要求更高的场景,可以在边缘只做签名,而加密操作仍然回源执行,用一部分延迟换来密钥的隔离性。

第三步是发送请求到浏览器推送服务。边缘节点需要向 FCM、Mozilla autopush 或 Apple 的推送服务发起 HTTPS POST 请求。由于边缘节点分布在各个地区,请求的出口 IP 可能与源站不同,这可能会触发推送服务的风控策略。在实践中,建议在边缘节点配置固定的出口 IP 或使用推送服务提供的区域端点。如果边缘节点到推送服务的网络不稳定,可以在边缘侧实现简单的重试和退避逻辑,但不要无限重试,否则会造成消息重复投递。

SSE 流式推送与边缘会话保持的优化

除了 Web Push,很多实时触达场景使用 Server-Sent Events(SSE),因为它实现简单,客户端只需一个 EventSource 连接。传统 SSE 需要源站保持长连接,当连接数上升到几十万时,源站的内存和文件描述符压力会非常大。把 SSE 终点搬到 CDN 边缘后,边缘节点负责维持与客户端的长连接,源站只需要在消息产生时向边缘 API 发送一个 HTTP 请求,由边缘节点将消息广播给所有相关连接。这种模式也被称为“边缘扇出”。

边缘 SSE 的核心挑战是会话状态管理。边缘节点通常是按请求分配的,同一个用户的后续请求可能落在不同节点上,而 SSE 长连接必须保持在同一节点。解决这个问题有两种常见方案:一是使用粘性会话,在 DNS 或负载均衡层把相同用户的连接固定到特定边缘节点;二是在边缘节点之间使用发布订阅机制,把消息广播到所有节点,由每个节点自己过滤订阅者。后者更适合大规模部署,但会增加边缘节点之间的网络流量。Cloudflare 的 Durable Objects 提供了一种折中方案:每个用户连接绑定到一个 Durable Object 实例,该实例有稳定的存储和身份,可以跨请求保持状态。

还有一个容易忽略的性能点是 SSE 的缓冲策略。边缘节点到客户端的连接可能经过多层代理,如果代理设置了响应缓冲,消息会被延迟直到缓冲区填满。为了保证实时性,边缘代码需要在响应头中设置 Cache-Control: no-cache 和 X-Accel-Buffering: no,并且每隔一段时间发送一个心跳注释行,例如 : ping。这样可以防止空闲连接被中间设备关闭,也能让客户端及时检测到断线。

对比直连源站方案:延迟、成本与可用性

如果通知服务直接搭建在源站,每次消息下发都需要经过完整的网络路径:客户端到源站的 TLS 握手、源站到推送服务的请求、推送服务再到客户端的连接。这个过程中,源站的处理能力会成为瓶颈。假设一次营销活动要触达 100 万用户,每个通知的加密和签名计算需要 2 毫秒,源站单实例每秒最多处理 500 个请求,那么全部发送完需要 2000 秒,远超活动窗口。把同样的逻辑分散到全球 200 个边缘节点后,每个节点只承担 5000 个用户,处理时间可以压缩到几秒。

成本方面,直连源站需要为峰值流量预留大量计算资源,而通知发送往往是突发性的。边缘计算按请求计费,在流量低谷时几乎不产生费用。不过边缘计算也有额外的成本,比如边缘键值存储的读写费用、跨节点数据同步的带宽费用。如果通知频次很低,直连源站可能更经济;如果通知频次高且用户分布广,边缘方案的优势才会显现。一个折中的做法是让边缘节点只处理最近活跃的用户,不再活跃的订阅回源处理,这样可以在成本和速度之间取得平衡。

可用性上,边缘节点天然具备抗 DDoS 和区域故障隔离能力。即使某个边缘节点宕机,流量会被自动路由到附近的其他节点,源站不会受到直接影响。相比之下,源站单点故障会导致所有通知中断。不过边缘方案也引入了新的依赖:边缘运行时本身、键值存储服务以及边缘到推送服务的网络。在架构设计时需要为这些依赖准备好降级路径,例如当边缘键值存储不可用时,直接回源读取订阅数据。

从零搭建一个边缘推送接口的代码示例

下面用一个 Cloudflare Workers 的例子展示如何实现一个极简的 Web Push 发送接口。这个示例假设订阅数据已经存储在 Workers KV 中,键为订阅 ID,值为 JSON 序列化的订阅对象。代码使用 web-push 库的纯 JavaScript 实现,避免依赖 Node.js 内置模块。

// 边缘 Worker 入口
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const url = new URL(request.url)
  if (url.pathname === '/send' && request.method === 'POST') {
    const payload = await request.json()
    const subscription = await SUBSCRIPTIONS.get(payload.subscriptionId, 'json')
    if (!subscription) {
      return new Response('Subscription not found', { status: 404 })
    }
    const result = await sendWebPush(subscription, payload.message)
    return new Response(JSON.stringify(result), {
      headers: { 'Content-Type': 'application/json' }
    })
  }
  return new Response('Not found', { status: 404 })
}

async function sendWebPush(subscription, message) {
  // 使用 VAPID 签名,私钥从环境变量读取
  const vapidKeys = {
    publicKey: VAPID_PUBLIC_KEY,
    privateKey: VAPID_PRIVATE_KEY
  }
  // 生成 JWT 头部和载荷
  const jwtHeader = {
    typ: 'JWT',
    alg: 'ES256'
  }
  const jwtPayload = {
    aud: new URL(subscription.endpoint).origin,
    exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60,
    sub: 'mailto:admin@ipipp.com'
  }
  // 注意:以下签名和加密逻辑需要引入 web-push 库的完整实现
  // 这里只展示流程,生产环境请使用经过审计的库
  const headers = {
    'Content-Type': 'application/octet-stream',
    'TTL': '60',
    'Authorization': 'Bearer ' + generateVapidToken(jwtHeader, jwtPayload, vapidKeys.privateKey)
  }
  const encryptedPayload = await encryptMessage(subscription, message)
  const response = await fetch(subscription.endpoint, {
    method: 'POST',
    headers: headers,
    body: encryptedPayload
  })
  return { status: response.status }
}

这段代码省略了加密和 VAPID 签名的具体实现,因为这些算法比较复杂,容易出错。实际项目中可以直接使用 @remix-run/web-push 这类库,它支持在 Web Worker 环境中运行。需要特别注意的是,Workers 环境没有 Buffer 全局对象,所有二进制操作都要使用 ArrayBuffer 和 Uint8Array。另外,Web Push 的加密要求使用 AES-GCM 和椭圆曲线 Diffie-Hellman,这些操作在边缘运行时中的性能表现通常不错,但密钥的导入和导出需要小心处理。

对于 SSE 场景,边缘代码会更简单一些。下面是一个在 Cloudflare Workers 上实现 SSE 广播的片段,使用 Durable Object 来维护连接列表。消息到达时,向 Durable Object 发送一个内部请求,由它向所有活跃连接写入数据。

export class NotificationHub {
  constructor(state, env) {
    this.state = state
    this.env = env
    this.sessions = new Map()
  }

  async fetch(request) {
    const url = new URL(request.url)
    if (url.pathname === '/subscribe') {
      const sessionId = crypto.randomUUID()
      const stream = new TransformStream()
      this.sessions.set(sessionId, stream.writable)
      const readable = stream.readable
      return new Response(readable, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Connection': 'keep-alive'
        }
      })
    }
    if (url.pathname === '/broadcast') {
      const message = await request.text()
      for (const [id, writable] of this.sessions) {
        const writer = writable.getWriter()
        writer.write(`data: ${message}\n\n`)
        writer.releaseLock()
      }
      return new Response('OK')
    }
    return new Response('Not found', { status: 404 })
  }
}

这个实现只适用于单个 Durable Object 实例的场景,如果用户数量超过单个实例的承载能力,需要引入分片策略,例如按用户 ID 哈希分配到不同的 Durable Object。同时要注意 Durable Object 的存储和 CPU 限制,长时间空闲的连接可能被平台主动断开,客户端需要实现自动重连逻辑。

总结来说,CDN Notification API 并不是一个全新的协议,而是把已有的通知推送机制下沉到边缘网络的一种架构选择。它适合用户基数大、通知频次高、对延迟敏感的产品。在落地时,开发者需要重点处理订阅数据的边缘缓存一致性、VAPID 私钥的安全存储、推送服务的出口 IP 稳定性以及 SSE 会话的粘滞性问题。只要这些细节处理得当,边缘推送能带来数量级的性能提升和成本下降。

CDN Notification API用户触达边缘计算修改时间:2026-10-02 13:03:09

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1002/64674.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。