33.5 边缘计算

一句话

边缘计算省的不是“计算时间”,是用户到中心的那一次往返;收益只有两个来源——数据就近与少一次往返。把计算下沉到边缘、数据仍回源中心,等于白搬:省下的一跳被回源的一跳抵消。

三层部署位置

层 距用户 RTT(量级) 容量 数据 典型用途
边缘(CDN 节点 / 边缘函数) 10–30 ms 小(单节点几十到几百请求/秒) 只读、可下发、可容忍陈旧 静态资源、AB 配置、名单校验、个性化缓存命中
区域(Region) 40–80 ms 中 主副本或只读副本 主业务服务、区域级读模型
中心(中心机房) 100–200 ms 大 权威数据 写路径、强一致事务、跨区域汇总

(RTT 是量级估算,实际取决于物理距离与运营商互联,用你自己的拨测数据替换。)

延迟是“可加”的:一个矩阵看清真相

设延迟模型为:

有效延迟 = 用户到计算层 RTT + |计算层 RTT − 数据层 RTT| + 处理时间

(第二项是“计算层回源取数据”的那一跳,用 RTT 差近似。)

package main

import (
	"fmt"
	"math"
)

type stage struct {
	name string
	rtt  float64
}

// 从用户视角的三层 RTT(量级)
var stages = []stage{{"边缘", 20}, {"区域", 60}, {"中心", 150}}

type feature struct {
	name   string
	budget float64 // 延迟预算(ms)
	data   string  // 数据所在层
}

var features = []feature{
	{"静态内容 / AB 配置", 50, "边缘"},
	{"商品列表(区域缓存)", 80, "区域"},
	{"个性化推荐(区域画像)", 120, "区域"},
	{"风控名单校验", 100, "边缘"},
	{"下单(中心库存/订单)", 300, "中心"},
	{"支付回调(中心账务)", 500, "中心"},
}

func rttOf(name string) float64 {
	for _, s := range stages {
		if s.name == name {
			return s.rtt
		}
	}
	return 0
}

// 计算层 compute、数据层 data 时的有效延迟
func effective(compute, data string) float64 {
	c, d := rttOf(compute), rttOf(data)
	return c + math.Abs(c-d) + 2 // +2ms 处理时间
}

func main() {
	fmt.Println("=== 计算层 × 数据层:有效延迟(ms),✓=满足预算 ✗=超预算 ===")
	fmt.Printf("%-24s %-6s", "功能", "数据")
	for _, s := range stages {
		fmt.Printf(" %-14s", s.name)
	}
	fmt.Println()

	for _, f := range features {
		fmt.Printf("%-24s %-6s", f.name, f.data)
		for _, s := range stages {
			v := effective(s.name, f.data)
			mark := "✗"
			if v <= f.budget {
				mark = "✓"
			}
			fmt.Printf(" %8.0f%s      ", v, mark)
		}
		fmt.Println()
	}

	fmt.Println("\n=== 数据下沉到边缘,能省多少 ===")
	fmt.Printf("%-24s %-10s %-10s %-8s %s\n", "功能", "当前最优(ms)", "下沉后(ms)", "收益", "前提")
	for _, f := range features {
		cur := effective(f.data, f.data)
		edgeData := effective("边缘", "边缘")
		switch f.data {
		case "边缘":
			fmt.Printf("%-24s %-12.0f %-12.0f %-8s %s\n", f.name, cur, edgeData, "—", "已经下沉")
		case "中心":
			fmt.Printf("%-24s %-12.0f %-12s %-8s %s\n", f.name, cur, "—", "—", "不可行:写路径必须回中心")
		default:
			fmt.Printf("%-24s %-12.0f %-12.0f %-8.0f %s\n", f.name, cur, edgeData, cur-edgeData,
				"数据可下发且能容忍陈旧")
		}
	}
}

本机实测(macOS,Apple M1,Go 1.27):

=== 计算层 × 数据层:有效延迟(ms),✓=满足预算 ✗=超预算 ===
功能                       数据     边缘             区域             中心            
静态内容 / AB 配置             边缘           22✓            102✗            282✗      
商品列表(区域缓存)               区域           62✓             62✓            242✗      
个性化推荐(区域画像)              区域           62✓             62✓            242✗      
风控名单校验                   边缘           22✓            102✗            282✗      
下单(中心库存/订单)              中心          152✓            152✓            152✓      
支付回调(中心账务)               中心          152✓            152✓            152✓      

=== 数据下沉到边缘,能省多少 ===
功能                       当前最优(ms)   下沉后(ms)    收益       前提
静态内容 / AB 配置             22           22           —        已经下沉
商品列表(区域缓存)               62           22           40       数据可下发且能容忍陈旧
个性化推荐(区域画像)              62           22           40       数据可下发且能容忍陈旧
风控名单校验                   22           22           —        已经下沉
下单(中心库存/订单)              152          —            —        不可行:写路径必须回中心
支付回调(中心账务)               152          —            —        不可行:写路径必须回中心

读数:

现象 含义
数据在中心时,计算放边缘/区域/中心的有效延迟都是 152ms 计算下沉但数据回源,省下的一跳被回源抵消,白搬
数据在区域时,计算放边缘 62ms = 放区域 62ms 边缘只打平,不赢;因为还是要回区域取数据
数据在边缘时(静态内容、名单),计算放边缘 22ms,放中心 282ms 只有“数据也在边缘”时,边缘才真正赢
收益列:只有“数据可下沉”的功能有收益 下沉数据要付一致性代价(下一节)

🔑 结论:边缘计算的决策不是“这个函数要不要放边缘”,而是“这份数据能不能放到边缘”。数据能下沉(静态资源、配置、名单、可容忍陈旧的读模型)→ 放边缘有 2–3 倍延迟收益(62ms → 22ms);数据必须回中心(写路径、强一致读)→ 放边缘毫无收益,只多了运维复杂度。

数据下沉的收益与代价

数据类型 能下沉吗 收益 代价
静态资源(图片/JS/CSS) ✅ 完全 命中即 20ms 级 缓存失效传播(9.1 边缘缓存原理)
配置 / AB 实验 ✅ 决策在边缘完成 变更要下发到所有节点
名单(黑名单/白名单) ✅ 校验本地化 名单更新有延迟窗口,需要版本号
商品详情(读多写少) ⚠️ 可缓存 命中率高时收益大 价格/库存不能缓存,否则会卖超
用户画像(读多写少) ⚠️ 按需 推荐结果可预计算 隐私与合规(数据出境、地域限制)
库存 / 订单 / 账务 ❌ 无 强一致,写必须回中心

⚠️ 一个常见的坑:把“个性化推荐”的判断放到边缘,但推荐所需的行为数据在中心——每次都要回源,结果比放中心更慢。正确做法是“预计算推荐结果 → 下发到边缘 → 边缘只做选取”,把“计算”换成“数据下发”。

边缘部署的四个硬约束

约束 说明 应对
无状态、实例短命 边缘实例随时回收,本地磁盘不可靠 状态外置或纯函数
冷启动 边缘函数同样有冷启动,且节点更分散 见 33.3 Serverless / FaaS 的概率模型
发布与回滚难 几百个节点,灰度要按 POP 分批 版本化配置 + 按节点标签灰度
观测难 日志分散在边缘,采样与聚合成本高 边缘只上报聚合指标,必要时按 trace 抽样

反模式

反模式 后果
把业务函数搬到边缘,数据仍回中心 延迟没降,多了一套边缘部署与排障
在边缘做写操作(下单、扣库存) 多节点并发写、冲突只能靠中心仲裁,延迟反而更差
边缘缓存价格/库存且不设 TTL 卖超、价格错误,且用户看到的是过期数据
所有功能都上边缘 运维面按节点数放大,收益只集中在少数读多写少的功能
边缘只做静态缓存,不做“数据下发” 错过了真正的大头收益(配置/名单/预计算)
用边缘绕开区域的数据合规要求 数据落到不允许的地域,合规风险

自检

Q1 一个功能预算 100ms,数据在中心(RTT 150ms)。把它部署到边缘(RTT 20ms)后,有效延迟是多少?为什么“部署到边缘”没解决问题?

▸ 答案:20 + |20−150| + 2 = 152ms,和部署在中心(150 + 0 + 2 = 152)一样。因为计算层回源取数据要走“边缘→中心”的往返(约 130ms),把省下的 130ms 又花掉了。要真正解决,只有两条路:① 把数据下沉到边缘(缓存/下发),使 |计算−数据| 趋近 0;② 改业务,让这个功能不需要实时读中心数据(预计算、异步)。

Q2 为什么“个性化推荐”放在边缘常常不划算,而“推荐结果下发到边缘”却划算?

▸ 答案:因为前者每次请求仍要回中心取行为数据(一跳 130ms 左右),后者把“重计算”放在中心离线完成、只把“结果”(几十 KB 的候选列表)下发到边缘。边缘只做“按用户选取 + 排序截断”,数据在本地,|计算−数据| ≈ 0。这正是本节的核心判据:边缘适合“数据已在本地”的计算,不适合“需要回源取数”的计算。

Q3 库存查询能放边缘吗?如果业务要求“显示实时库存”,该怎么办?

▸ 答案:不能把可写的库存放边缘(多节点并发写、超卖),也尽量不要把“实时库存”原样缓存。可行做法:① 边缘只缓存“是否有货”这种粗粒度状态(TTL 几秒),下单时以中心为准,前端把“有货”当参考;② 用预占(reserve)代替“先查后扣”,把一致性压力放到中心的原子操作上(29.3 的库存聚合);③ 接受“边缘显示略有延迟 + 下单时校验”,并在 UI 上明确“以结算页为准”。

Q4 一个团队把 20 个边缘函数部署到 300 个 POP,结果发布一次要 40 分钟,故障排查靠人肉登录节点。问题出在哪?

▸ 答案:出在把边缘当成“小号中心”来运维。边缘的运维模型必须是“声明式 + 版本化 + 分批灰度”:配置与函数版本都进 git,按 POP 标签分批推送,每批观察错误率与延迟再继续;观测上只上报聚合指标(每 POP 的 p99、错误率、回源率),排障靠 trace 抽样而不是登录节点。否则节点数一多,发布与排障的成本会超过边缘带来的延迟收益。

关联