冷启动——计算机世界里最大的不公平
目录
- 不公平藏在第一秒
- CPU 缓存——最短的记忆只有几微秒
- 数据库缓冲池——为什么重启后第一查总是慢
- Serverless 的冷启动税
- 个人验证:这台服务器的热身时间
- 尾声
不公平藏在第一秒
我有一个偏见:如果你没等过一个数据库的冷启动,你就没真正理解什么叫「慢」。
不是网络延迟那种慢(那是别人的问题),不是大查询那种慢(那是可以优化的)。是那种——服务刚启动、缓冲池空无一物、磁盘上没有缓存、CPU 刚从一个不知道什么状态醒过来——然后你对它发第一个请求。它花了 3 秒才回来。
3 秒。在互联网语境里意味着用户已经关掉了页面、打开了第二个标签页、开始刷抖音。3 秒等于永恒。
但诡异的是:同一条 SQL,第二次执行只用了 3 毫秒。差了整整一千倍。而这两次执行之间唯一的区别,是同一个二进制文件在上面多跑了 3 秒钟。
这就是冷启动。计算机世界里最大的不公平——你在为「没有历史」买单,而不是为计算付费。
CPU 缓存——最短的记忆只有几微秒
现代 CPU 的缓存层级看起来像一座金字塔:L1(几千字节,约 1ns 延迟)、L2(几兆,约 4ns)、L3(十几到几十兆,10-15ns)、主存(几十纳秒)。纳秒级别的差别听起来谁在乎?
实际上在乎的程度到了一个 L1 cache miss 带来的延迟惩罚,足够 CPU 执行几十条指令。如果一个循环的数据正好在 L1 里,它跑得像闪电;如果分散在不同的缓存行里,它慢得像在内存里逐字节查找。
所以为什么同一个程序第一次运行比第二次慢?因为第一次运行时 L1/L2/L3 都是空的。数据从内存进来,经过总线,填充到各级缓存——这个过程本身就是热身。第二次运行时数据可能还在缓存里(尤其是小数据集),于是快了一个数量级。
那第三次呢?如果中间有另一个大进程把缓存刷了一遍(比如编译内核),一切归零。不管你是谁,缓存没了就是没了。
数据库缓冲池——为什么重启后第一查总是慢
这可能是普通开发者最常体会冷启动的地方。PostgreSQL 的 shared_buffers、MySQL 的 InnoDB buffer pool、SQLite 的 page cache——它们都做同一件事:把热数据留在内存里。重启后呢?全没了。
buffer pool 冷的时候,每次读都是磁盘 I/O。在 HDD 上是毫秒级,在 SSD 上是微秒级,但无论如何都比已缓存的命中慢两个数量级。一个跑了 30 天的数据库实例,buffer pool 里住着所有索引的「热」版本。重启后的第一个查询不是真正的"慢查询"——它只是在一个没热身好的机器上做了一次被迫的不优雅访问。
数据库厂商也不是没想过办法。Oracle 有 buffer pool warmup,PostgreSQL 有 pg_prewarm,SQL Server 有间接检查点来减少重启后的 I/O 峰值。但它们都是在弥补同一个根本问题:内存断电即失,而磁盘慢得像上个世纪。
Serverless 的冷启动税
如果说数据库冷启动是「你等的 3 秒」,Serverless 冷启动就是「你根本不知道这 3 秒从哪扣的」。
AWS Lambda 的冷启动可以到几百毫秒甚至几秒。原因不是你的代码慢,而是运行时还没加载、容器还没创建、网络还没打通。你的 function 写得再快,头几秒都在等基础设施热起来。
而且这个税不收在同一个账单上——AWS 不因为你冷启动多计费,但用户会等。于是出现了奇怪的对抗方式:定时调用 keep-warm 函数、预留并发、用 provisioned concurrency 买热度。所有方案的本质都是同一件事——不要让你的函数变冷。
一个函数如果每 5 分钟被调用一次,它永远热着。如果被调一次后就闲置 30 分钟,下一次调用要付出的代价可能远超你的预期。
个人验证:这台服务器的热身时间
说这么多不如做一次。我在自己服务器上跑了两个简单测试:
# 第一次 cat 大文件(冷缓存)
$ time cat /var/log/syslog > /dev/null
→ real 0.82s
# 第二次 cat(热缓存)
$ time cat /var/log/syslog > /dev/null
→ real 0.01s
82 倍。同一个文件,同一个命令,隔了一秒。区别只是第一次读的时候硬盘在忙,第二次数据在 page cache 里。
那数据库呢?我没在生产机器上重启 PostgreSQL(不敢),但在 SQLite 上跑了一个小实验:
# 冷缓存读大表
$ sqlite3 test.db "SELECT COUNT(*) FROM large_table;"
→ 约 1.2s
# 再跑一次(page cache 还在)
$ sqlite3 test.db "SELECT COUNT(*) FROM large_table;"
→ 0.03s
40 倍。操作系统 block cache 记住了它刚读过的页。没有魔法,没有预读优化,只是简单的"第二次就不用在物理盘上找了"。
(严格来说测试有点作弊——SQLite 进程退出后内核的 page cache 还在,所以"第二次"不是纯冷返回。真正的冷启动需要 echo 3 > /proc/sys/vm/drop_caches 清除全部缓存。我没跑这个命令,因为不想让服务器上的其他服务跟着一起冷——这也是冷启动的悖论:你为了测量它,必须制造它;而制造本身就会让一切变慢。)
但 40-82 倍已经足够说明问题了。
尾声
冷启动不是 bug。它是物理世界对数字世界的约束——内存不能装下一切,缓存不会永远存在,磁盘不会用光速响应。
但理解冷启动意味着三件事:
- 设计时知道「首次一定慢」——所以不会为第一次的性能恐慌
- 编码时知道「热的数据应该留住」——所以会主动管理预热策略
- 排错时知道「这个慢不是写错了」——而是物理规律
不公平不一定是坏事。不公平意味着有优化的空间,而每一次优化都是让你离物理极限更近一步。物理极限是光的传播速度,那才是真正的不公平。
评论(0)
暂无评论,来写第一条吧~