Claude's Blog

Claude's Blog

马上订阅 Claude's Blog RSS 更新: https://claude-ray.github.io/atom.xml

Verdaccio 性能优化:代理分流

2019年11月30日 19:08

前言

这里的 Verdaccio 是指用于搭建轻量级 npm 私有仓库的开源解决方案,以下简称 npm 私服。

前段时间写了一点分流相关的优化思路,但那是以节省资源开销为主、不冲破原有结构的微调,从结果上看,甚至不是合格的优化。

随着用户(请求)数量的上升,服务响应速度和效率其实才是最要紧的问题,节省资源终究不能改善这一点。因此我决定实施上次浮现在脑中的想法,将内外网的 npm 包流量彻底分流。

关于 Cluster 模式的说明

再次解释,Verdaccio 官方文档明确表示不能支持(PM2)Cluster 模式。另外,其云存储方案是可以支持多进程多节点部署的,但只提供了 google cloud、aws s3 storage 的插件。

不过在此基础上,只要拥有自己的云存储服务,就能使用或设计一套新的存储插件,进而支持多进程架构。此方案一定可行,只是相比本篇的做法,需要的成本更高一些。

俗话说得好,没有一个中间层解决不了的问题,而在 Verdaccio 的场景下,这种做法又是相当地迅速和高效。

原理

npm 安装机制

如果不了解 npm 官方客户端的安装机制,稍后可以阅读阮一峰的博客[[http://www.ruanyifeng.com/blog/2016/01/npm-install.html][《npm 模块安装机制简介》]],少部分知识已经不适用于当前版本了,不过最重要的是能理解 npm 下载流程。

其中我们需要知道,npm 包下载前,客户端会向上游服务器查询包信息,以及获取压缩包的下载地址 url,并将此 url 存放在 package-lock.json 文件中。以后每次执行下载,都会优先使用 package-lock.json 中的地址。

npm 下载最长请求路径

为了方便理解 Verdaccio 所处的位置,我来绘制一下 npm 包下载时从客户端到 Verdaccio 再到上游的最长请求路径简图,并忽略中间的安全验证环节,如下所示。

npm 请求路径

接口转发

有了代理层,就可以忽略 Verdaccio 内部的各种逻辑,不受技术栈的约束,编写少量的代码,便能完成主要接口的分流。

首要的接口是 /:package/:version? ,释放私服最大的查询压力,原因可以看这里的解释

次要的接口是 /:package/-/:filename ,也就是实际的下载接口。并且其中还涉及另一个极为有利的优化。

尽管 Verdaccio 是转发上游的资源,它也会将下载 url 变更为自己的服务域名。因此不论依赖是否私有,记录到 package-lock.json 中的地址都是 Verdaccio 的地址。

但经过代理层的分流,此后经过更新的 package-lock.json 将保留原汁原味的下载地址,此后下载压缩包的请求再也不会发到私服。

综上所述,我们可以将私服超过 99.99% 的流量转移到代理或上游服务。

条件

接下来,我们来确定分流口径,自然是判断一个 package 是否是私服私有,因此需要 Verdaccio 提供接口,获取私有包的列表。

Verdaccio 有一个 /-/verdaccio/packages 接口用来获取所有私有包的信息,但这个包主要用于 Web 页面,包含大量我们不需要的信息,甚至简单一点,只要提供私有 npm 包的包名就能满足筛选条件。

因此,可以改良 /-/verdaccio/packages,例如新增一个专门获取包名列表的接口,并增加内存缓存。

Verdaccio 版本不同时,做法也有很大差异,相信这里的处理不是问题,只要认真阅读上述接口就能获取思路了。

PS:还是补充一点代码吧,早期版本 Verdaccio 只需要这样改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Get name list of all visible package
* @route /-/verdaccio/names
*/
route.get('/names', async function(req, res, next) {
// 此处 cache 作为缓存,在有新的私有 npm 包发布时刷新即可
let names = cache.get('packageNames');
if (!names) {
try {
names = await storage.localStorage.localList.get();
} catch(err) {
return next(err);
}
cache.set('packageNames', names);
}
next(names);
})

最新的 names 要使用回调的方式取值,伪代码:

1
2
3
const names = await new Promise((resolve, reject) =>...

剩余内容已隐藏

查看完整文章以阅读更多