Cloudflare 的账单惊吓:Durable Object 踩坑记录
太长不看:Cloudflare 项目里用到 Durable Object(DO),一不留神账单就来了。文末提供了一个 skill 来帮你看着。
最近把一个东西迁到 Cloudflare,没几天就被一个账单击中了:
——这是 6 月 1 号的账单,5 月底做的代码变更,短短几天就 12.5 美元。6 月 4 号发现后马上修复了。右侧这个柱子是现在看不到、但已经预定了的下个月 1 号的账单,预估在 20 美元左右。
这次是发现及时,否则画风也可能是这样——
具体原因不一样,但都是因为DO——Durable Object(持久对象)。名字非常抽象。官方文档有多片文档,显然非常认真在解释,但因为过于详细很难看懂。。
简单来说,DO 是一种特别的 CF worker。
Worker
先说这个 CF 里无处不在的 Worker。 熟悉可以跳过。
用户访问我们的网站时,如果看到的内容是’定制化’的,比如说他的账单、积分等等,就得需要查数据库、然后把结果用漂亮的页面呈现出来,这一系列动作每一步都需要计算。如果是我们自己维护网站服务器,来一个请求就算一下,是不是很简单?一个月下来如果一个用户也没有,服务器也得一直开着。十年前大家建站一直是这样,很浪费,但很简单。
网站放在 CF Workers 就完全不同。没有用户请求的时候,可以认为这个世界上实际没有给我们的网站分配任何的计算资源。有用户请求时,CF 在几个毫秒内临时起一个 Worker 算一下。算完就清除,忙别人的事去了(实际会稍微等一会,但我们不抠这个)。
Worker 的厉害之处
因为有这个特点,所以 CF 可以用几万台服务器同时服务百万、千万个网站。绝大部分网站访问量很小,没必要开着服务器浪费电。
而且不止省电,全球不同区域的用户访问我们的网站的时候,会被距离他们最近的服务器所启动的 worker 来处理。因此,速度也更快!
CF worker 是 Cloudflare 的重大技术创新,充分发挥了它们的”边缘计算”优势。
CF 以 CDN 和反向代理起家,在全世界有很多服务器,提供静态资源是一把好手。现在又在这些服务器上搭起一整套”边缘计算”生态。Worker 是这个计算生态最早引入,也是最核心的技术。
Worker 的代价
但同一个网站的不同请求落在不同的 Worker / 服务器上,也不总是好事。
假设 iPhone 折叠屏刚刚上市了,正在被爆抢,很快库存还剩 1 台,并且有 100 个人在同一秒下单。
因为请求会被不同的 Worker 处理,每个 Worker 看到的库存都是 1,于是每个人都下单成功。一秒后网站显示库存 -99。。
Just DO it!
DO (Durable object)就是为了解决这个问题。如果启动 worker 的时候指定它是 DO,那么它在全世界范围内就有了一个固定的门牌号。所有发给这个ID/门牌号的请求都会被 CF 转发给这个worker。100 个同时下单也得一个个一个来,库存到了 0,后面的 99 个下单自然就会失败。完美。
一个全网唯一 + 单线程 的 worker,就是 DO。
官方有篇 DO 使用规则,Rules of Durable Objects,简单概括下里头提到的适用场景:要协调、强一致、每个实体一份独立存储、长连接。抢折叠屏iphone,属于这里的要协调(同一时刻只能一个人改数据库)、强一致(库存状态全网实时统一)。
当然,因为固定位置,它就跟自己维护服务器一样,失去了”给用户就近分配服务器”的好处。
而如果你让 DO 一直开着,也会跟自己维护服务器一样,会产生浪费——而且 DO 比自己开服务器还费钱!
我的账单
这次的账单是属于上面官方提到的几种场景里、‘长连接’这个场景。
简单说一下我的需求:想让多台服务器都能收到数据库的变动通知。本来是让这些服务器轮询数据库,因为成本高(要保证及时性就得频繁轮询,Neon 没法休眠)。后来灵机一动,让每台服务器跟CF worker 之间建一个自己的 websocket 长连接,数据库有变动时告诉 worker;worker 通过 websocket 通知到对应的服务器。
既然长连接,就必须要要用 DO。没问题。但一直开着 websocket 是个大问题,会导致 DO 不休眠!
——当我弄明白原因的时候,那感觉是从 Neon 家的一个坑里,跳到了 CF 家的、长得一模一样的坑里。。
具体一点。这个 websocket 一天下来也就几十条消息,所以 DO 实际大部分时候是闲着的。CF 为了充分利用 DO,提供了一个休眠(hibernatable websocket)机制,休眠期间 DO 不算钱。
用不用休眠能差多少?官方给了两个例子,都是每分钟一条消息:
- 100 个 DO、每个挂 50 条普通 websocket:一个月差不多138 美元。
- 100 个 DO、每个挂 100 条休眠 websocket:一个月 10 美元。 普通 websocket 连接数少了一半,账单是可休眠 websocket 的 14 倍。可谓事半费倍倍倍!我就用了这个费倍倍倍方案。
修复方法就是改成可休眠版本。(为什么 CF 明知这一点但还是允许普通 websocket?AI 的说法是还是会有使用普通 websocket 的场景,我表示怀疑。毕竟太不划算了。 anyway。)
鸡贼的 CF
巨坑的一点,是像这样的计费用量(billable usage)导致的 bill,在账单最终来之前,是看不到的!
这你能信?你打开面板看着目前每天的费用是空的,到了月初突然来了一个万元账单。再加上 CF 没有额度上限这一点,理论上损失无上限——这别说出海了,睡觉都不踏实了吧?
这个时间轴的其余部分很像个障眼法——因为无论如何都是 0。
社区一直有人反馈 CF 控制台不能看实时账单这个问题,但 CF 一直也没改。有人就吐槽说 CF 分明是故意的。我倾向于认为是确实没动力改+更高优先级的事情太多——最近CF上多了不少 AI 相关的东西。
这一次我是走了狗屎运,踩坑成本恰好很低——在月底两天修改, 1)有免费额度做缓冲,2)多出来的部分直接体现在了 1 号账单里所以能及时看到——尽管因为不常打开 CF 面板,看到时已经是4 号。但下次可能没有这么好运气。所以让 AI 写了一个 skill,专门监控 cf 的用量(对,CF 告诉你用量,但没有转成费用),参考了自己这次的坑和社区的 CF 意外账单案例,你也可以参考。放到了 https://github.com/jinzheio/jzskills