QCon全球软件开发大会9折优惠倒计时,了解详情 了解详情
写点什么

NPM 私库从搭建到数据迁移最后容灾备份的一些解决方案

2021 年 7 月 30 日

NPM 私库从搭建到数据迁移最后容灾备份的一些解决方案

前言


按照国际惯例,正文开始之前,我们先简单介绍下目前市面上的 NPM 私库开源框架


  • Verdaccio

Verdaccio 是 Sinopia 开源框架的一个分支。它提供了自己的小数据库,以及代理其他注册中心的能力(例如:npmjs.org 网站),配置以及部署相对简单,一步到"胃"。如果公司的私包比较少的话或者你想偷懒,可以考虑一下。


  • Cnpmjs.org

大名鼎鼎的 CNPM,想必各位早就感受到了它的速度之“快”,没错,它的 Register 服务就是淘宝镜像 (https://registry.npm.taobao.org/)。主要是基于 Koa、MySQL 和简单存储服务的企业专用 NPM 注册和 WEB 服务,其中最强大的功能就是它的同步模块机制(定时同步所有源 Registry 的模块、只同步已经存在于数据库的模块、只同步 Popular 模块)。


  • Nexus

后端开发的小伙伴应该比较熟悉。Nexus2 主要是用于 Maven/Gralde 仓库的统一管理,而 Nexus3 则添加了 NPM 插件,可以对 NPM 提供支持,其中 NPM 仓库有三种类型,分别是 Hosted(私有仓库)、Proxy(代理仓库)、Group(组合仓库)。


总体来讲,抛开 Nexus,虽然 Cnpmjs.org 在部署过程以及总体设计方案上相对于 Verdaccio 复杂的多,但是它提供更高的拓展性,定制性,可以支持多种业务使用场景。接下来,我们分别从 Cnpmjs.org 容器化部署数据迁移OSS 容灾备份等内容,层层展开。


Cnpmjs.org 容器化部署


目前,公司的应用部署都是容器化部署,内部搭建了 Ipaas 平台,应用流程化部署以及一键发布。而 Cnpmjs.org 也附带了 Dockerfile 以及 docker-compose.yml 文件,所以,这里大致讲解下怎么用 Docker 部署吧。


  • 首先让我们看看 Dockerfile 文件

FROM node:12MAINTAINER zian yuanzhian@cai-inc.com
# Working enviromentENV \    CNPM_DIR="/var/app/cnpmjs.org" \    CNPM_DATA_DIR="/var/data/cnpm_data" 
# Shell 格式# 在 Docker Build 时运行RUN mkdir -p ${CNPM_DIR}
# 指定工作目录:用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在WORKDIR ${CNPM_DIR}
# 复制指令:从上下文目录中复制目录或文件到容器里指定的路径COPY package.json ${CNPM_DIR}
RUN npm set registry https://registry.npm.taobao.org
RUN npm install --production
COPY .  ${CNPM_DIR}COPY docs/dockerize/config.js  ${CNPM_DIR}/config/
# 声明端口(7001 为 Register 服务、7002 为 web 服务)EXPOSE 7001/tcp 7002/tcp
# 匿名数据卷:在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。VOLUME ["/var/data/cnpm_data"]
RUN chmod +x ${CNPM_DIR}/docker-entrypoint_prod.sh
# Entrypoint # Exec 格式# 在 Docker Run 时运行# Dockerfile 存在多个 CMD 命令,仅最后一个生效# CMD ["node", "dispatch.js"]CMD ["npm", "run", "prod"]
复制代码


这里把 CMD 命令修改为 ["npm", "run", "prod"],因为增加了一层不同环境的 shell 脚本,目前全局变量全都存放在这里。


示例:docker-entrypoint_env.sh


export DB='db_cnpmjs'export DB_USRNAME='root'export DB_PASSWORD='123456'export DB_HOST='127.0.0.1'
export BINDING_HOST='0.0.0.0'
DEBUG=cnpm* node dispatch.js 
复制代码


  • 再修改下 docker-compose.yml 文件,这里把 mysql-db 这个服务删掉了,原因是可通过 /docs/dockerize/config.js 下的配置文件去连接公司测试环境的 MySQL 数据库,则不需要构建生成 mysql-db 镜像


version: '3' # docker版本services: # 配置的容器列表  web: # 自定义,服务名称    build: # 基于 Dockerfile 构建镜像(可增加 args )      context: .      dockerfile: Dockerfile ## 依赖的 Dockerfile 文件    image: cnpmjs.org # 镜像名称或 id    volumes:      - cnpm-files-volume:/var/data/cnpm_data    ports:      - "7001:7001"      - "7002:7002" 
复制代码


注意点:1、全局配置文件路径: /docs/dockerize/config.js;2、bindingHost 为 0.0.0.0。


  • 最后,在控制台敲下 docker-compose up -d,即以守护进程模式形式启动应用,然后打开浏览器入 http://127.0.0.1:7002,就会看到 WEB 页面。执行 npm config set registry http://127.0.0.1:7001 可设置为搭建的私库的镜像源地址,这里推荐使用 nrm,可自由切换 NPM 源。


展示站点如下图:



注意点:1、当你改变本地代码之后,先执行 docker-compose build 构建新的镜像,然后执行 docker-compose up -d 取代运行中的容器。


数据迁移


由于公司之前用的 Verdaccio 搭建的私库,要切换使用新的 NPM 私库,意味着要把之前发布过的私包全部迁移过来。大概统计了下,有 400 多个 Package,总共有  7000 多个版本,按照正常逻辑,做数据迁移首先会从数据库下手,但是 Verdaccio 并不依赖数据库。刚开始没有一点头绪,大概看了下 Cnpmjs.org 的源码,分析了当我们 publish 模块时,它是怎么把 NPM 模块 的元数据存储到数据库。


通过路由文件(/routes/registry.js)我们很容易找到 /controllers/registry/package/save.js,这个文件便是我们想要的。

核心代码:



var pkg = this.request.body; // 这里拿到 npm 模块元数据,即 package.json 文件经过 libnpmpublish模块处理过的 Json 数据var username = this.user.name; // 当前用户名var name = this.params.name || this.params[0]; // NPM 模块名var filename = Object.keys(pkg._attachments || {})[0]; // NPM 模块的压缩后的文件名var version = Object.keys(pkg.versions || {})[0]; // NPM 模块的最新版本

复制代码


// Upload Attachment
// Base64 解码,获取模块文件二进制数据。从 libnpmpublish 模块了解到 tardata.toString('base64'),即NPM 模块文件流转 Base64 字符串var tarballBuffer = Buffer.from(attachment.data, 'base64'); // 默认使用 fs-cnpm,将 NPM 模块文件保存到本地,默认保存路径:path.join(process.env.HOME, '.cnpmjs.org', 'nfs')var uploadResult = yield nfs.uploadBuffer(tarballBuffer, options);
var versionPackage = pkg.versions[version];var dist = {  shasum: shasum,  size: attachment.length};
// If nfs upload return a key, record itif (uploadResult.url) {  dist.tarball = uploadResult.url;} else if (uploadResult.key) {  dist.key = uploadResult.key;  dist.tarball = uploadResult.key;}var mod = {  name: name,  version: version,  author: username,  package: versionPackage};
mod.package.dist = dist;
// 模块数据保存到数据库var addResult = yield packageService.saveModule(mod);
复制代码


即只要我们能够拿到 NPM 模块的元数据(即 package.json 被处理过的 JSON 数据),就能把模块文件上传到文件系统或者 OSS 服务,同时数据落库。Verdaccio 有两个 API 可以拿到其私库 NPM 模块全量数据和当前 NPM 模块的 JSON 数据,路径分别是 /-/verdaccio/packages/-/verdaccio/sidebar/$PKG$,其中有 scope 的模块的请求路径是 /-/verdaccio/sidebar/$SCOPE$/$PKG$。思路已经很明确了,开始动起来吧!新增 save_zcy.js 文件,基于原来的 /controllers/registry/package/save.js 稍加改造下。

核心代码:


// 请求远程文件,并返回二进制流const handleFiles = function (url) {  return new Promise((resolve, reject) => {    try {      http.get(url, res => {        res.setEncoding('binary') // 二进制        let files = ''        res.on('data', chunk => { // 加载到内存          files += chunk        }).on('end', () => { // 加载完          resolve(files)        })      })     } catch (error) {      reject(error)    }  })};
// 获取远程模块文件的二进制数据yield handleFiles(dist.tarball).then(res => {  // 利用 Buffer 转为对象  const tardata = Buffer.from(res, 'binary')  pkg._attachments = {};  pkg._attachments[filename] = {    'content_type': 'application/octet-stream',    'data': tardata.toString('base64'), // 从缓冲区读取数据,使用 base64 编码并转换成字符串    'length': tardata.length,  };}, error => {  this.status = 400;  this.body = {    error,    reason: error,  };  return;});
复制代码


接下来我们把控制器 save_zcy.js 接入到 Registry 服务的 APP 路由上。


// 新增 fetchPackageZcy、savePackageZcy 控制器app.get('/:name/:version', syncByInstall, fetchPackageZcy, savePackageZcy, getOneVersion);app.get('/:name', syncByInstall, fetchPackageZcy, savePackageZcy, listAllVersions);
复制代码


控制器 fetchPackageZcy 作用是请求上面的 API(/-/verdaccio/sidebar// 或 /-/verdaccio/sidebar/)来拉取对应模块的 JSON 数据。



OK,接下来我们写一个定时任务,每隔一段时间执行 npm install [name],这样原来私库的 NPM 包都能够 install 并进入到上面的控制器逻辑,大功告成!


OSS 容灾备份


首先,简单说明下为什么要做 OSS 容灾备份,有以下几点。


  • 如果服务器上磁盘损坏,易丢失文件,有一定的风险

  • 若服务器磁盘爆满,可自动降级上传模块文件到 OSS


基于以上几点,我们整理了下容灾备份方案:


  • package publish



即发布模块文件时本地存储,同时上传到 OSS 作为备份,用到的插件分别是 fs-cnpmoss-cnpm


  • package install



即下载模块文件时,先判断是否是私包(即包名是否有带  scope ),如果不是私包代理到上游 Registry,若是私包先判断服务器本地是否有该私包文件,如果不存在先去 OSS 下载到本地 nfs 目录下,如果存在则直接从 nfs 目录找到模块文件,然后读取并写到 downloads 目录下,最后调用 fs.createReadStream 方法流读取该文件。


isEnsureFileExists 即判断模块文件本地是否存在,代码如下:


const mkdirp = require('mkdirp');const fs = require('fs');
function ensureFileExists(filepath) {  return function (callback) {    fs.access(filepath, fs.constants.F_OK, callback);  };}
复制代码


注意,在 OSS 下载模块文件到 nfs 之前,一定要先创建模块文件目录,方法如下:


const mkdirp = require('mkdirp');
function ensureDirExists(filepath) {  return function (callback) {    mkdirp(path.dirname(filepath), callback);  };}
复制代码


邮件通知


Cnpmjs.org 本来就带有邮件通知的功能,但只应用错误日志上报。由于我们的私包大部分都是业务组件、工具等,有时候发布正式版本的业务组件需要通知到业务组件的使用方。目前,我们采用 Maintainers 来维护,包含模块的维护者及使用者。


示例:

"maintainers": [  {    "name": "yuanzhian",    "email": "yuanzhian@cai-inc.com"  }]
复制代码


邮箱配置如下:


mail: {  enable: true,  appname: 'cnpmjs.org',  from: process.env.EMAIL_HOST,  host: 'smtp.mxhichina.com',  service: 'qiye.aliyun', // 使用了内置传输发送邮件,查看支持列表:https://nodemailer.com/smtp/well-known/  port: 465, // SMTP 端口  secureConnection: true, // 使用了 SSL  auth: {     user: process.env.EMAIL_HOST,     pass: process.env.EMAIL_PSD, //    } }
复制代码


写在文末


未来,我们还可以在 Cnpmjs.org 上做很多定制化开发,比如接入公司内部权限系统WEB 页面重构对接业务组件在线文档等等。如果你正好也需要搭建 NPM 私有库,希望这篇文章对你有所帮助。



头图:Unsplash

作者:梓安

原文:https://mp.weixin.qq.com/s/NUYooqqTklsSs77VDRLwKA

原文:NPM 私库从搭建到数据迁移最后容灾备份的一些解决方案

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 7 月 30 日 20:501118

评论

发布
暂无评论
发现更多内容

云原生在京东丨云原生时代下的监控:如何基于云原生进行指标采集?

京东科技开发者

云原生

万物互联的IoT时代,柔性电子会大行其道吗?

脑极体

一个草根的日常杂碎(10月20日)

刘新吾

随笔杂谈 生活记录 社会百态

利用区块链等技术,加强对交通运输信用信息的归集共享和分析应用

CECBC区块链专委会

区块链 交通运输

【线上排查实战】AOP切面执行顺序你真的了解吗

Zhendong

spring aop

膜拜!阿里技术总监纯手打的《MySQL笔记》内部资料限时分享

Java架构师迁哥

华为云瑶光:打通云边端界限,为企业云上业务带来最优解

华为云开发者社区

华为 云服务

透视HTTPS建造固若金汤的堡垒

码哥字节

https 加密解密 HTTP

算法分析关键

Geek_0b8195

算法和数据结构

一个草根的日常杂碎(10月18日)

刘新吾

随笔杂谈 生活记录 社会百态

Linux的上手命令

林昱榕

Linux 常用命令

第四周作业

dll

聊聊技术人员如何学习成长

架构精进之路

职业成长

一个草根的日常杂碎(10月19日)

刘新吾

随笔杂谈 生活记录 社会百态

分布式下,我想要一致性

架构师修行之路

分布式 微服务

央行数字货币离我们还有多远?

CECBC区块链专委会

数字货币

架构师必备的那些分布式事务解决方案!!

架构师修行之路

分布式 微服务 架构设计

解锁华为云AI如何助力无人车飞驰“新姿势”,大赛冠军有话说

华为云开发者社区

AI 无人驾驶

1分钟带你入门 React 公共逻辑抽离HOC...

Leo

React Hooks 前端进阶训练营 HOC Render Props

独家!阿里技术人限产的MySQL高级笔记及面试宝典,学完简直开挂

Java架构追梦

Java MySQL 数据库 架构 面试

Java程序员还在为没有项目经验感到苦恼?快来看看GitHub上最火的SpringCloud微服务商城系统开源项目,附全套教程!

Java架构之路

Java 程序员 架构 面试 编程语言

游戏数值策划之常用excel函数

吴优秀同学

Excel 游戏

深入java week1-01 字节码、内存、GC、调试工具

闷骚程序员

一套完整的后台管理系统(附源码),非常实用!

程序员生活志

管理系统

架构训练营学习笔记之五技术选型(一)

于成龙

架构训练营

java week1练习

闷骚程序员

美腻了!Java资深架构师带你深度学习字节跳动的亿级流量+高并发

Java架构追梦

Java 学习 架构 面试 微服务

基于注解的参数校验器Hibernate Validator

HelloLittleRain

Java springboot 参数校验 Hibernate-Validator

mongodb 源码实现、调优、最佳实践系列-百万级代码量mongodb内核源码阅读经验分享

杨亚洲(专注mongodb及高性能中间件)

MySQL mongodb 源码 中间件 分布式数据库mongodb

Nginx 在运维领域中的应用,看这一篇就够了

华章IT

nginx Linux 运维工程师

二十、深入Python迭代器和生成器

刘润森

Python

数据库运维技术发展与展望

数据库运维技术发展与展望

NPM 私库从搭建到数据迁移最后容灾备份的一些解决方案-InfoQ