惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

W
WeLiveSecurity
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
Cloudbric
Cloudbric
The Register - Security
The Register - Security
小众软件
小众软件
PCI Perspectives
PCI Perspectives
G
Google Developers Blog
AI
AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
量子位
TaoSecurity Blog
TaoSecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
F
Full Disclosure
N
Netflix TechBlog - Medium
博客园_首页
Last Week in AI
Last Week in AI
A
Arctic Wolf
B
Blog RSS Feed
J
Java Code Geeks
C
Cybersecurity and Infrastructure Security Agency CISA
I
InfoQ
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Jina AI
Jina AI
有赞技术团队
有赞技术团队
S
Schneier on Security
L
Lohrmann on Cybersecurity
P
Privacy & Cybersecurity Law Blog
T
Threat Research - Cisco Blogs
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Security Latest
Security Latest
Vercel News
Vercel News
博客园 - 司徒正美
Webroot Blog
Webroot Blog
Hacker News: Ask HN
Hacker News: Ask HN
A
About on SuperTechFans

Claude's Blog

浅谈 npm 依赖治理 记录 Got(Node.js) 代理 HTTP 请求的坑 如何管理桌面窗口 SSR 页面 CDN 缓存实践 从 is-promise 事件我们可以学到什么 分享一个 npm dist-tag 的冷知识 从 Vimium 到 qutebrowser 让 npm install 不使用缓存的方法 Verdaccio 性能优化:代理分流 Verdaccio 性能优化:上游路径转发 CSAPP DataLab 题解 不靠谱的 Egg.js 框架开发指南 如何解决 Debian 系 Elastic apm-server 7.x 启动失败 Hexo NexT 主题升级 7.4 From Journeyman to Master Mac 上移除 EasyConnect 常驻后台进程 Nginx SWRR 算法解读 记一次 Node.js 进程挂起的 BUG 追踪 警惕 Travis CI 的 npm 缓存
Verdaccio 性能优化:单机 Cluster
Post author: Claude Ray · 2019-12-31 · via Claude's Blog

本篇将讨论如何解决 Verdaccio 官方本地存储方案不支持 Cluster 的问题。

前言

标题为什么叫单机 Cluster 呢?

因为多机 Cluster 已经无法使用默认的本地存储,必须配合一套新的存储方案,而官方只提供了 AWS 和 Google Cloud 的支持。这在国内已经是一道门槛,因此大概率是要用上其他云存储服务的,这意味着必须做一个 Verdaccio 插件实现必备的 add、search、remove、get 功能。

糟糕的是,倘若自己的云存储不支持查询功能,还得基于数据库再造一套轮子,甚至再加一套解决读写冲突的轮子。

一句话来说,Verdaccio 是轻量级好手,不适合也不必要承载太重的装备。重度使用的场景下,与其从头定制的存储体系,不如直接上 cnpm、Nexus 等体积更大、相对成熟的系统。

话说回来,作为尝试,我还是基于 Redis 实现了它的单机 Cluster。虽然修改的 Verdaccio 版本较旧,但其新版 V4 的架构并没有太大变化,思路还是一致的。

思路

Verdaccio 默认无法使用 PM2 Cluster 启动,有两大阻碍。

其一,缓存同步。它使用进程级别的内存缓存,没有实现进程间通讯,多进程之间缓存信息不能同步。

其二,写锁。本地存储将内容持久化到本机磁盘,只有进程级别的“锁”,多进程容易出现写文件冲突。

这两个问题处理起来其实非常简单,特别是引入 Redis 之后。

针对第一点,内存缓存可以迁移到 Redis,但是其中有大体积的 JSON 信息,不适合存在 Redis,可以用 Redis 做消息中心,管理各进程的缓存状态。

针对第二点,私服本身属于简单的业务场景,Redis 锁完全可以胜任。

实现

本应该是 Show Code 环节,可念在笔者改的版本不存在普适性,索性改成修改要点的简单罗列吧。

  • 重写 local storage,本地存储依赖一个叫 .sinopia-db.json.verdaccio-db.json 的文件,其中保存所有私服的包。这个文件的内容适合使用 Redis 的 set 结构进行替换。

  • 查找并替换所有 fs.writeFile,加锁处理。在锁的实现上,新手需要多看官方文档,大部分博客的实现都是错误的,比如忽略了解锁步骤的原子化操作。

  • 向上回溯修改的链路。

额外的补充

想来这可能是专题的最后一期,于是把不太相关的几个小问题也堆到下面吧。

只关心 Cluster 改造的看官可跳过此节,直接看末尾总结。

异步风格

由于手上的 Verdaccio 版本较老,整体还是 callback 风格,让改造多了一点工作量。我使用的 Redis 客户端为 ioredis,注意把涉及到的调用链路都改造为 async/await。

发布订阅

另一个坑点是我拿到的 Redis 其实是 Codis 集群,这套方案的一个缺点是无法使用 Redis 弱弱的发布订阅功能,也就不能直接拿来订阅更新内存缓存的消息。只好另辟蹊径,将 Redis 作为“缓存中心”,进程取缓存前先查询标志位,如果标志位存在,代表内存缓存需要更新。以进程号等信息做 key 前缀表示区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const os = require('os');

class CacheCenter {
constructor(prefixKey = 'updated') {
this.data = new Map();

this.prefix = prefixKey;

this.ip = getIPAddress();
this.id = `${this.ip}:${process.env.NODE_APP_INSTANCE || 0}`;
}

async get(key) {
const isCached = this.data.has(key);
if (isCached) {
const isCacheLatest = await redis.hget(this._key(key), this.id);
if (isCacheLatest) {
return this.data.get(key);
}
}
return undefined;
}

async set(key, value) {
this.data.set(key, value);
await redis.hset(this._key(key), this.id, Date.now());
redis.expire(this._key(key), 7 * 24 * 60 * 60);
}

async del(key) {
redis.del(this._key(key));
}

has(key) {
return this.data.has(key);
}

_key(key) {
return `${this.prefix}:${key}`;
}
}

function getIPAddress() {
const interfaces = os.networkInterfaces();
for (const iface of Object.values(interfaces)) {
for (const alias of iface) {
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
return '127.0.0.1';
}

module.exports = new CacheCenter();

页面搜索优化

顺便一提,Verdaccio web 页面的 /search 接口性能极差,实现也存在诸多问题。此处值得加一层内存缓存,等到新包发布时刷新。

早期 Verdaccio 不支持使用 name 搜索名为 @scope/name 的包,可增加一条 name 专用的索引字段促成改进。根源是依赖的 lunr 引擎版本过低(0.7.0),但最新 lunr 的表现依然不太理想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Search {



constructor() {
this.index = lunr(function() {
this.field('name', {boost: 10});
this.field('unscoped', {boost: 8});
this.field('description', {boost: 4});
this.field('author', {boost: 6});
this.field('readme');
});
}





add(pkg) {
this.index.add({
id: pkg.name,
name: pkg.name,
unscoped: getUnscopedName(pkg.name),
description: pkg.description,
author: pkg._npmUser ? pkg._npmUser.name : '???',
});
}

}





function getUnscopedName(name) {
return name.split('/')[1];
}

总结

为 Verdaccio 开启 Cluster 能力并不是一个轻松的做法,但经过这个系列解读,却可以轻松地作出选择。

如果只是想一定程度上提高处理高并发的性能,可以采取上一篇代理分流的做法,代理可以帮你分担 99% 以上的压力。

如果想进一步提升性能,实现应用的平滑重启,本文单机 Cluster 并配合 pm2 reload 的做法不妨一试。

而一但想开启多节点集群的能力,几乎超出了轻量级私服的理念,试着迁移到 cnpm、Nexus 吧。