追踪错误如何教会了我关于npm不了解的事情
npm成立仅3年,但已有5-6年的代码基础。 它的大部分已被重写,但CLI和注册表的核心仍然是原始代码。 此时,在npm上只工作了一年,所以还有很多事情需要我去学习整个系统的工作原理。
有时,用户提交了一个错误,该错误在调试过程中会教给您一些有关您自己的系统不了解的知识。 这是其中一个错误的故事。
错误
在过去一周左右的时间里,一些人在npm软件包页面中提出了一些奇怪的截断问题。 在一个问题中,用户报告了README似乎是损坏的链接:

另一位用户指出, README的整个结尾部分都丢失了!
作为npm的markdown解析器marky-markdown的维护者,我担心这些问题是由于解析规则出现问题导致的。 但是,另一个marky-markdown维护者@revin很快发现了一个奇怪的事情:描述被截取为正好255个字符, README被截取为正好64kb。 正如我的同事@aredridel指出的那样:这些数字是抽烟的枪支。
确实,一个内部内部npm服务(称为registry-relational-follower正在截断自述文件和发布到npm注册表的软件包的描述。 这让我和我的同事感到惊讶,因此我在我们的公共注册表回购中提出了一个问题。 几乎几乎没有时间,我们的CTO @ceejbot表示此截断是预期的行为(!),因此已解决该问题。
“提尔!”我想。 那就是我决定深入研究注册表如何处理README的原因……以及原因。
零一无限法则
在我深入探讨您的软件包的README在您编写和发布到npm网站上的渲染之间发生了什么之前,让我们解决一下房间中800磅的大猩猩:
当我发现注册表随意截断README ,我想到:“似乎很糟糕。”
也许你也这么想。
实际上,至少有一个人这样做了,对已解决的问题发表了评论:
npm可能希望这样做,但是我怀疑任何软件包作者都希望其描述被截断。 另外,请参见零一无限。
我应该指出,对已经结束的问题发表负面评论并不是世界上最好的举动。 但是,我对此评论表示赞赏,因为它给了我一些新词来解释我对这种截断情况的隐约负面感觉-那些名字很好的花哨词: “零一个无限”规则 。
零一无限规则是荷兰计算机科学家Willem Van der Poel广为流行的指导原则,其内容如下:
不允许foo,foo之一或任何数量的foo。 —术语文件
该原则旨在消除任何形式的任意限制。 从功能上讲,它建议,如果您要完全允许某件事,则允许一件事情或无限数量的事情。 这些似乎与似乎共生的规则一致:最小惊讶原则,其中指出:
如果必要的功能具有很高的惊讶系数,则可能需要重新设计该功能。
最终,这些原则是花哨的,听起来很重要的表达方式: 任意限制令人惊讶,我们也不应感到惊讶。
现在我们可以同意,具有奇怪且看似随意的限制的令人惊讶的用户不是没有理由…为什么npm注册表当前有此限制? 当然,npm的开发人员不想让开发人员感到惊讶,对吗?
注册中心架构的考古学
确实,他们没有! 当前对描述和README大小的限制是一项创可贴,由于npm Registry的原始体系结构,npm的注册表开发人员被迫应用:大README会使npm 变慢 。
哎呀…… ,您可能在想。 合理。 让我们来看看。
npm如何处理发布时的自述文件
当前,注册表是如何处理您的README的:
当您键入npm publish ,CLI工具将查看您的.npmignore (如果不存在.npmignore ,则为.gitignore )和package.json的files密钥。 根据在此找到的内容,CLI提取您要发布的文件并运行npm pack ,它将所有内容打包到一个tarball或.tar.gz文件中。 npm不允许您忽略README文件,因此无论如何都会收拾行囊!
当您输入npm publish ,您的README将打包成一个压缩包。 这是有人npm install您的软件包时下载的。 但这不是README 唯一发生的事情。
因此,尽管npm publish运行npm pack ,但它还会运行一个名为publish.js的脚本,该脚本将构建一个包含软件包元数据的对象。 在包的整个生命周期中(发布新版本时),此元数据会增长。 首先,运行read-package-json ,并根据package.json列出的内容获取README文件的内容。 然后publish.js将此README数据添加到您的包的元数据中。 您可以将此元数据视为package.json的更详细的版本-如果您想查看它的外观,可以访问http://registry.npmjs.com/ 。 例如,查看http://registry.npmjs.com/marky-markdown 。 正如您将看到的,其中有latest标签的软件包中都有README数据!
最后, publish.js发送此元数据(包括README )以validate-and-store …这是我们遇到截断情况的地方。
npm publish将整个README数据发送到注册表,但不会将整个README写入数据库。 相反,当数据库收到README ,它将在插入前以64kb的长度将其截断。
这意味着:当我们将npm注册表上的软件包作为单个实体讨论时,事实是单个软件包实际上是由npm注册表服务处理的多个组件组成的。 值得注意的是,有一个用于tarball的服务,另一个用于元数据的服务,并且您的README已添加到两者中 。
这意味着注册表具有自述文件的2个版本:
- 原始版本作为tarball包中的文件
- 包元数据中可能被截断的版本
您可能现在已经猜到了,因为npm网站使用包元数据中的README数据,所以用户在npm网站上看到了README的截断。 这有很多道理:如果我们想在软件包tarball中使用README ,就必须解开每个软件包tarpack来检索README ,但这并不是非常有效。 从JSON响应中读取README数据(这是npm注册表提供程序包元数据的方式),似乎至少比解压缩350,000个tarball更为合理。
历史课时间
因此,现在我们知道自述文件在哪里被截断,以及这些被截断的README是如何使用的-但仍未必清楚为什么。 了解这一点需要一些考古学。
像有关npm的许多事情一样,这种截断并不总是这样。 2014年1月20日,@ isaacs向npm-registry-couchapp couchapp提交了64kb README截断,他这样做的理由非常充分:
首先,允许超大型README使我们遭受潜在的DDoS攻击。 一个不好的演员可以自动发布带有大型README的多个软件包,并破坏npm的基础设施。
其次,程序包元数据中的README极大,导致该文档的文件大小爆炸式增长,这使得GET请求很慢地检索程序包数据。 请求软件包元数据会在npm安装中的每个软件包中发生,因此,通常必须进行一次npm install ,因为必须读取具有很长README的多个软件包-README甚至对最终用户都没有用,或者使用压缩包中的解压缩README ,或者甚至不需要README ,例如,如果程序包是在依赖树中很远的传递依赖。
有趣的是,文档大小爆炸的困境是npm以前处理过的问题。
还记得我们指出单个包实际上是由多个不同服务管理的一组数据吗? 像npm上的许多事情一样,情况并非总是如此。
最初,npm的注册表完全由单个服务CouchApp包含在CouchDB数据库之上。 CouchDB是一个数据库,它使用JSON作为文档,使用JavaScript作为MapReduce索引,并使用常规HTTP作为其API。
CouchDB带有一个现成的功能,称为CouchApp,它是直接从CouchDB提供服务的Web应用程序。 npm的注册表最初只是一个CouchApp:程序包是基于文档的单个实体,压缩包作为文档的附件。 该架构的简单性使其易于使用和维护,即完全合理的版本1。
不过,此后不久,npm开始迅速增长-程序包的发布和下载激增 -并且原始体系结构的伸缩性很差 。 随着程序包的大小和数量的增加,以及依赖树的长度和复杂性的增加,性能停滞不前,npm的注册表经常崩溃。 对于npm来说,这是一个痛苦的成长时期。
为了减轻这种情况,@ isaacs将注册表分为两部分:一个只有元数据的注册表(附件被移至名为Manta的对象存储中,并从CouchDB中删除了),他将其称为skim ,另一个注册表同时包含了元数据压缩包附件称为“ full-fat 。 这项拆分是进行多次(且正在进行中)重构工作的第一步,以减少软件包元数据文档的大小,并在多个服务之间分配我们如何处理软件包以提高性能。
如果您今天看一下npm注册表体系结构,您会看到我们现在的CTO @ceejbot继续分裂整体的努力的效果:将注册表功能缓慢分离为多个较小的服务,其中一些不再受原始服务的支持CouchDB,并由Postgres支持。
对未来的计划
事实证明,没有人认为任意限制README长度是一件好事。 计划中有一个注册表版本3的计划,并且更改README生命周期肯定是可行的。 就像@isaacs创建skim和full-fat注册服务机构时所做的最初转变一样,团队理想地希望看到README数据已从包元数据文档中删除,并转移到可以呈现它们并将其静态提供给他们的服务中。网站。 这将带来一些很棒的好处:
- 没有更多的
README截断了! 再见任意限制! - 通过将markdown解析移至其自身的服务来加速网站。
- 通过预编译
README并静态提供它们(而不是根据请求进行分析)来进一步提高网站的速度。 (是的,我们缓存了,但仍然……) - 为软件包的所有版本提供
README! 通过降低README的成本,我们不仅可以解析单个README更多内容 ,而且还可以解析更多的README! 🙂
npm非常关心向后兼容性,因此随着npm注册表从CouchApp和CouchDB的起源中发展出来,我们原始API的所有原始端点和功能将继续受到支持。 这意味着将始终有一项服务,您可以在其中请求程序包的元数据并获取latest版本的README 。 但是,npm本身不必使用该服务。 从多个角度出发,朝着我们对注册表版本3的愿景前进将是一项了不起的改进。
调试愉快!
最近有个朋友发了推文:
设计好的系统很棒,但是发现的系统很糟糕
这不是npm的镜头; 这个说法无处不在。 任何人都感兴趣的大多数系统都是长期的,可能是复杂的约束和动机历史的产物,而这种情况通常会产生奇怪的结果。 尽管您发现的系统令人不快,但仍然很高兴找出系统是如何工作的(当然,对于“工作”的某些值)。
最后,“错误”的“修复”是“我们已经为此制定了计划,但要花一些时间。”这还不是很令人满意。 但是,跟踪npm注册表系统中看似简单的元素并跨服务和时间对其进行探索的过程非常有益。
实际上,在撰写本文的过程中,我意识到,Rust编程语言的软件包管理器Cargo的网站Crates.io正在处理有关其软件包README的非常相似的情况。 他们没有考虑像我们一样从包元数据中删除它们,而是考虑将其放入! 如果我没有机会深入研究npm注册表的内部内容,那么我可能还没有准备好以5年的经验为他们提供建议。
因此,故事的寓意是这样的: 只要有可能,就花点时间在自己的软件中进行挖掘,并询问有关过去的决策和课程的问题。 然后,写下您所学。 有一天可能会有所帮助,而且可能比您想像的要早。
npm人 Ashley Williams的 帖子 最初 出现在npm博客上 。