4

Verdaccio 性能优化:代理分流

 3 years ago
source link: http://claude-ray.com/2019/11/30/optimize-verdaccio-proxy/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

这里的 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 只需要这样改:

/**
* 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 要使用回调的方式取值,伪代码:

const names = await new Promise((resolve, reject) =>
storage.localStorage.storagePlugin.get((err, list) =>
err ? reject(err): resolve(list)))

客户端也能承担分流的任务,即像 cnpm 一样包装一层自己的 npm cli 工具,但分流的逻辑要简单许多,只需检查要安装的包是否属于私有,然后分为两批安装。

缺陷是推行难度和速度都不理想,于是这里只是顺便提一下。

到这一步,技术选型已经无所谓了,自然可以 nginx + lua,简单一点就继续使用 Node.js 实现。

由于其他原因,我用 express 做了实现,贴一点转发逻辑,大家就自由发挥吧。

const request = require('request');
const rp = require('request-promise-native');

const publicRegistry = 'http://registry.npm.taobao.org';
const privateRegistry = 'http://npm.private.com';

const sec = 1000;
const min = 60 * sec;

const privateListCache = [];

/**
* 检查并更新私服包名列表的缓存
* 缓存可以基于 redis 或内存,注意控制好更新节奏
*/
async function checkPrivateCache() {}

/**
* npm package 请求分流
* @route /:packages/:version? 版本检查
* @route /:packages/-/:filename 下载
*/
async function packages(req, res, next) {
console.log(req.url)
await checkPrivateCache();
// 请求默认转发至 taobao
let baseUrl = publicRegistry;
if (privateListCache.length && privateListCache.includes(req.params.package)) {
// 转发私服的请求
baseUrl = privateRegistry;
}

const options = {
uri: baseUrl + req.url,
timeout: 2 * min
};
try {
request(options).on('error', next).pipe(res)
} catch(err) {
next(err);
}
}

/**
* 其他请求原样转发私服
* @route /*
*/
function all(req, res, next) {
// 清除 headers 的 host
const headers = Object.assign({}, req.headers, { host: undefined })
const options = {
uri: privateRegistry + req.url,
method: req.method,
timeout: 2 * min,
headers
}
try {
req.pipe(request(options).on('error', next)).pipe(res);
} catch (err) {
next(err)
}
}

在同样的测试条件下,私服的 /:package/:version? 接口平均响应耗时从 4s 降至 400 ms,可以明显感觉到速度的提升,并且可以通过不断扩展代理层优化处理效率。作为轻量级的私服解决方案,已经可以续命很久了。

这个系列就此结束了吗?当然没有,cluster 的坑还没填呢!也确实可能会鸽掉…

因为支持 cluster 需要较深入的二次开发,也有新的中间件引入,相比目前的成本要高出不少。并且 Verdaccio 新旧版本的逻辑存在一定差异,我在老版本中已经解决了此问题,但新版可能又要另一套实现。

所以,等我读完 Verdaccio 最新的代码再说吧~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK