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

理解这个模式,需要先弄清楚两个基本组件:一个是 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