🙋🏻♀️ 编者按:本文是蚂蚁集团 Node.js 工程师零弌在 NodeParty 上的分享内容,介绍了 npmmirror.com 的现状以及背后全新实现的企业级包管理服务 cnpmcore,欢迎查阅~
开章
大家好,我是零弌,蚂蚁集团 nodejs 工程师,在蚂蚁负责 npm cli 和 registry 的工作,是 npmmirror 的维护者。和大家分享一下 npmmirror.com 的现状以及背后全新实现的 cnpmcore。
npmmirror 与 cnpmcore 介绍
npmmirror.com 是运行在阿里云上的 npm 镜像,为国内的大前端开发者提供了免费、高速的 npm registry 服务。只需要在使用 npm cli 的时候配置上 registry 即可享受到 npmmirror.com 的服务。
npmmirror.com 最早不是这个域名,是从 npm.taobao.org 换过来的,近年来一直为国内的大前端工作者提供服务。这一路走来经过了非常多的变化,从 js 到 ts,从 koa 到 tegg,从提供 web 服务 + registry 服务,到现在只提供 registry 服务,真的变化很大,见证了 nodejs 的技术发展。
首先带大家从数据上认识一下 npmmirror.com。目前的每月下载量已经到达了 58 亿次。从 2017 年有下载量的记录以来,可以看到下载量一直在飞速的增长,从 5 千万次上涨到 58 亿次,足足上涨了一百倍,见证了国内前端行业的蓬勃发展。
这里是 npmmirror.com 详细的系统用量,每秒从 npmmirror CDN 下载的峰值流量有 4G,峰值 QPS 有 7K。npmmirror.com 还维护了 npm 全量的包镜像,目前所有的 npm 加起来存储量已经占了 26TB。为了能支撑这样的用量,我们使用了这些云上基础设施。
首先是网络分发,要做好全国各地高性能的网络下载,高速、低延迟,是一件难度很高的工作,如果需要自己建设的话成本很高,在现有的云服务体系下,CDN 绝对是最佳之选,全国各地都有高性能的节点,我们只需要维护好回源的源站即可。
为了支持 npmmirror 的访问量和同步量,以及对于服务的稳定性考虑,单机服务显然是不够支撑的。因此我们由多台 ecs 配合 SLB 构成了 npmmirror 的分布式服务。同时做一个 registry 服务,存储是比较复杂的,registry 中有大量的 tgz 文件需要存储。每个包的版本、maintainer、disttag 都需要存储。因此我们需要有 oss 来做文件存储功能,db 来做包的元信息存储。
最后由于 registry 的设计,每次包的访问都需要返回所有版本的全量信息,有的包的版本会特别多,成千上万,映射到 db 中的概念就是一个包的数据可能有几万行,一次查询需要返回几万行的数据。对于 db 的存储压力是很大的,因此 redis 缓存在 npmmirror 中会是一个必要组成部分。
我们要做 npmmirror.com 的一大原因是访问海外的 npm 服务在网络上不通畅,我们需要一个镜像服务来保障国内使用 npm 的服务稳定以及快速。因此我们分别在香港和上海建立了两个镜像站,通过香港镜像站来保障海外服务同步的稳定性,通过上海镜像站来保障国内镜像服务的稳定性。并且我们使用了阿里云的 CDN 服务,在保障全国的访问性能的同时降低了使用成本。
cnpmcore 功能介绍
cnpmcore 是 npmmirror.com 背后的实现,在 github 上开源。除了基础的 npm 镜像服务,私有包发布功能之外,我们还增加了企业强烈需要的多 registry 同步功能,bug-versions 应急止血能力。最后 cnpmcore 还能很方便的进行二次研发,便于企业内部定制。下面来为大家详细介绍一下 cnpmcore。
应急止血
我们来看一下应急止血的问题,代码都是人写的,出 bug 很正常。前端研发重度依赖了开源社区的代码,如果依赖出了 bug 会是一个比较棘手的问题。开源社区提供了一下的解决方案,可以通过在 package.json 中声明 overrides 来强制指定依赖的版本,可以通过修改 lock 文件来强制指定需要安装的依赖版本。另外既然使用的是开源依赖,我们当然也可以通过开源的方式,提 PR 来修复 bug。
但是这些方式都比较低效,只能解决单个项目的问题,而企业内部有成百上千的项目,一个个项目去修改显然是很浪费人力的,我们需要一个更加高效的方式。
我们实现了 bug-versions 来解决这类问题,bug-versions 有以下能力:
- 在安装时自动回滚至无 bug 的版本
- 在安装时覆盖有安全问题的 install 脚本
- 在安装时自动升级至安全的 node 版本
通过这一系列的能力组合,就能实现安全的 npm 依赖安装。
bug-versions 分别可以在 npm 客户端和服务端运行,各有优劣。在客户端使用时,可以很好的覆盖所有的场景,但是也会带来较高的客户端维护成本。在服务端运行的场景下功能比较局限,只能进行 bug 版本的替换,额外的收益是使用任何 npm 客户端都行,不需要维护一个自己的客户端。目前 npmmirror.com 上也开启了 registry 版本的 bug-versions,我们在使用 npmmirror.com 作为 registry 的时候默认就是安全的,在现阶段 npm 包安全问题频发的时候为大家阻挡了很多的安全问题。
bug-versions 维护在 github 上,通过 PR 即可新增 bug 版本的记录,在 PR 合并之后会通过 github action 自动发布到 npm。现在已经维护了 151 个 bug 记录。在这里要特别感谢一下开源社区,是开源社区帮助我们积累了这么多的版本,如果只是一个内部维护的私有包,我们是没法做到这么多的。我们也期待越来越多的开发者来加 cnpmcore 的开源社区,可以就从简单的 bug-versions 做起,这样简单的工作也是可以让全国的开发者收益的。
稳定可靠
npm 镜像很重要的点是同步的成功率和性能,在 cnpmcore 中极大的增强了这个点。
我们先看看不稳定的同步会怎么样,monorepo 已经在前端研发中很流行了,对于大型项目来说,整体的组成一般都是很复杂的,会分为很多的模块,如果将模块放在一个 npm 包隔离性显得不够,而如果将模块都放到不同的仓库中又显得太为分散,维护和开发成本都会比较高,monorepo 对于大型项目来说就显得正正好,模块和模块之间有包的强隔离,又有单个仓库的优势,绝配。但是 monorepo 也不是没有成本的,每次 monorepo 仓库发布会有大量的 npm 包发布,而且这些 npm 包之间会有互相依赖,只要有一个包没有同步,项目就可能跑不起来。我们可以看到 umi 和 tegg 都有 20 个以上的 npm 包,这给同步成功率带来了很大的压力。
我们来看一下需要实现稳定的同步机制需要有哪些要素。首先是持久化,所有的同步任务需要持久化,不能因为意外的情况导致同步任务丢失,另外是任务需要有稳定的重试机制,失败的任务需要能一直重试,直到成功。如果出现任务丢失,任务失败,没有自动化的手段去解决这样的问题,就会导致包版本丢失、tag 不存在等等问题,影响用户体验,需要用户人工介入,手动补偿,这样的体验就很差了。另外怎么 sync 也不是人人都会的操作,我们经常遇到的一类咨询就是,为什么源上没有包的某个版本。
为了实现这两个要素,我们基于 DB 和 redis 实现了一个持久化的任务队列。说到持久化 ,DB 一定是一个不二之选,我们把 task 存入了 db 就不可能丢失,但是 db 中并没有队列这样的数据结构,靠 sql 搜索的话并不高效。一般需要队列的情况我们都会引入 mq,但是 cnpmcore 是一个开源项目,依赖的基础设施需要越少越好,基础设施越多的话,部署的成本也就越高。因此我们得合理的利用我们现有的基础设施,在 redis 中有一种数据结构是 sort set,可以很好的解决我们按序执行,插入幂等的需求。
让我们再来看下任务的状态机,任务的初始状态是创建,在创建完成之后会入队,出对后进入运行状态,在任务出现超时、失败的情况时,会重新入对,成功后会进入终态成功。通过这样的状态流转来保证了任务一定成功。
新旧 registry 迁移
再来看一下如何从 cnpmjs.org 向 cnpmcore 迁移,本身镜像的迁移是没有成本的,做一次同步即可。比较大的问题是私有包如何进行同步,在企业内部往往有大量的私有包,这些包的迁移会有较高的成本。蚂蚁内部也正在迁移中,下面就是我们的迁移方案(正在研发中)。
我们会期望内部私有的 package 和 maintainer 都能自动的同步,避免人工导入导出数据的操作。在 cnpmjs 和 cnpmcore 迁移的时候切换的时候我们期望是迁移是可控的,逐步切读切些,因此老的 cnpmjs 还会不断的产出数据,如果靠人工的话,时效性和数据的准确性都会是问题。既然我们可以从 npm 同步数据,那应该一样可以从 cnpmjs.org 把私有包同步过来。这样的话我们就需要一个能从多个 registry 同步的功能了。
我们简单来看下建模,首先是 registry,我们会在 registry 下指定默认哪些 scope 可以同步,scope 指定了新增 package 的默认 registry 值,在最老的 cnpmjs 的实现中,私有 package 是可以不带 scope 发布的,这些 package 我们就可以修改其 registry 信息。
基于这样的建模设计,我们即可实现以下几大需求,最基本的指定 scope 可以从哪同步实现迁移功能,提升同步安全性,避免了 npm 上的重名攻击,最后是老版本的 cnpmjs 兼容,无缝迁移。
cnpmcore 二次研发介绍
在介绍完基本功能之后,下面会分享一下如何基于 cnpmcore 去定制企业内部自己的 npm registry。
cnpmcore 完全采用 ts 进行研发,使用了 tegg 框架,引入了 DDD 研发模式。首先会介绍这些基本概念。
我们来看下 cnpmjs 和 cnpmcore 的实现区别,cnpmjs 采用 js + koa 的方式进行研发,函数中的 this 就是上下文,可以通过 this 来获取参数。cnpmcore 采用 ts + tegg 进行研发,我们可以看到所有的参数在方法签名中,以及引入了 http 上下文参数。所有的参数有 ts 的类型定义,我们可以很清楚的明白入参有什么,是什么。
接下来再简单介绍下 DDD 的基本概念,领域建模,核型的代码逻辑将会收敛到模型中。这样可以解决两大问题,防止业务逻辑分散在四处,包很重要的概念是 scope,name,通过领域模型 Package 中的 fullname getter 即可获取,而不是在每个地方单独写一下。另外还会有状态的问题,比如在任务模型中,会有 logPath 和 logStorePository 两个字端,两个字端是有强关联的,在 logPath 更新的时候 position 应该同步的清空,通过领域模型的方法就可以很好的对其进行保护。
再来看下我们的项目架构,最上面是我们的接入层,默认提供了标准的 HTTP registry 接口,可以对其进行扩展,比如进行消息、RPC 等等其他方式的接入。最下层是基础设施层,可对其进行插拔替换,目前 cnpmcore 是部署在阿里云上,提供基础设施的替换也可以部署到 azure、aws 上。
介绍完这些基本概念,让我们来进行几个小小的实战演练。
小明是我们打工人,这位黑黑的是我们的老板,会对小明提需求。首先是需要把 cnpmcore 运行起来。为什么我们已经有了 npmmirror.com 还需要在企业内部部署自己的 registry 服务?在研发上,出了对于依赖下载的需求,发布 npm 包也是非常重要的需求,工程师极具造轮子的欲望。而 npmmirror.com 仅提供了镜像代理的能力,并未开启私有包发布功能,因此在企业内部部署一套自己私有的 cnpmcore 是非常有必要的。
运行前有个前置条件,我们假设大家已经有了一个 tegg 的应用,没有的话可以赶紧用起来。首先安装 cnpmcore 依赖,在 config/module.json 中将 cnpmcore 所有的 module 声明出来。最后只要运行 npm run dev 即可开始激动人心的 cnpmcore 之旅。
另外小明公司使用的云是 aws,需要实现 aws 的适配。小明抄起键盘就实现了一个 s3 nfs client 替换了阿里云 oss,在 module.json 中替换了 cnpmcore 的 infra 模块。一顿操作就满足了 aws 的适配。
这时候黑黑的老板又给小明提了一个安全需求,所有的 npm 操作都需要交给安全进行分析。npm 的很多操作都是高危的,比如发布新的版本、修改 dist-tag,这些操作都将对我们的一线研发工作有很大的影响,如果没有很好的控制出了漏洞,后果是很严重的。因此将 npm 操作记录导出给安全审计是非常有必要的。
小明在深入了解 npm registry 之后,发现操作特别的多,有发布、修改 tag、修改 maintainer、删除等等。这些操作都需要打印审计日志,交给安全进行审计。如果需要去修改 cnpmcore 的代码成本有些过高了,而 cnpmcore 正好实现了领域事件,所有的变更都会通过应用内的 eventbus 进行广播,二次开发的时候对其进行监听即可,完全不需要对 cnpmcore 进行侵入性的改动。小明又快速的抄起键盘,定义好日志结构后,增加了事件监听的实现就把老板的需求给做了。
前端行业蓬勃发展,老板又招了五千个前端,对 xnpmcore 造成了很大的压力,前端依赖安装的性能变得很差。这里举这个例子,正好可以将我们的框架、系统、工具链都覆盖到,我们将会看到如何使用性能分析工具去定位问题,如果通过框架提供的能力去无倾入性的优化代码,最后也可以到我们系统的内部实践。
小明使用 ezm 快速定位到了热点问题,原因是某个仓储层的方法查询频率过高了,导致 db 压力很大。小明迅速想到 cnpmcore 已经有了 redis 依赖,可以使用 redis 来做方法执行的缓存。但是如何在不修改 cnpmcore 的代码下对其逻辑进行修改呢?小明深入了解了 tegg 发现其提供了 AOP 功能,可以对方法进行劫持,其中的 around 方法就能没有倾入性的实现缓存的逻辑。配合上一节的领域事件,可以在 npm 版本变更后对缓存进行失效操作,这样就能避免脏数据问题。
经历了这些需求,小明已经成长为了一名资深 cnpmcore 工程师,希望对大家的工作也有帮助。
👏🏾👏🏾 👏🏾 欢迎体验: