企業(yè)級包管理服務(wù) cnpmcore
?????♀? 編者按:本文是螞蟻集團(tuán) Node.js 工程師零弌在 NodeParty 上的分享內(nèi)容,介紹了 npmmirror.com 的現(xiàn)狀以及背后全新實現(xiàn)的企業(yè)級包管理服務(wù) cnpmcore,歡迎查閱~

開章

大家好,我是零弌,螞蟻集團(tuán) nodejs 工程師,在螞蟻負(fù)責(zé) npm cli 和 registry 的工作,是 npmmirror 的維護(hù)者。和大家分享一下 npmmirror.com 的現(xiàn)狀以及背后全新實現(xiàn)的 cnpmcore。
?npmmirror 與 cnpmcore 介紹

npmmirror.com 是運(yùn)行在阿里云上的 npm 鏡像,為國內(nèi)的大前端開發(fā)者提供了免費(fèi)、高速的 npm registry 服務(wù)。只需要在使用 npm cli 的時候配置上 registry 即可享受到 npmmirror.com 的服務(wù)。
npmmirror.com 最早不是這個域名,是從 npm.taobao.org 換過來的,近年來一直為國內(nèi)的大前端工作者提供服務(wù)。這一路走來經(jīng)過了非常多的變化,從 js 到 ts,從 koa 到 tegg,從提供 web 服務(wù) + registry 服務(wù),到現(xiàn)在只提供 registry 服務(wù),真的變化很大,見證了 nodejs 的技術(shù)發(fā)展。

首先帶大家從數(shù)據(jù)上認(rèn)識一下 npmmirror.com。目前的每月下載量已經(jīng)到達(dá)了 58 億次。從 2017 年有下載量的記錄以來,可以看到下載量一直在飛速的增長,從 5 千萬次上漲到 58 億次,足足上漲了一百倍,見證了國內(nèi)前端行業(yè)的蓬勃發(fā)展。

這里是 npmmirror.com 詳細(xì)的系統(tǒng)用量,每秒從 npmmirror CDN 下載的峰值流量有 4G,峰值 QPS 有 7K。npmmirror.com 還維護(hù)了 npm 全量的包鏡像,目前所有的 npm 加起來存儲量已經(jīng)占了 26TB。為了能支撐這樣的用量,我們使用了這些云上基礎(chǔ)設(shè)施。

首先是網(wǎng)絡(luò)分發(fā),要做好全國各地高性能的網(wǎng)絡(luò)下載,高速、低延遲,是一件難度很高的工作,如果需要自己建設(shè)的話成本很高,在現(xiàn)有的云服務(wù)體系下,CDN 絕對是最佳之選,全國各地都有高性能的節(jié)點,我們只需要維護(hù)好回源的源站即可。
為了支持 npmmirror 的訪問量和同步量,以及對于服務(wù)的穩(wěn)定性考慮,單機(jī)服務(wù)顯然是不夠支撐的。因此我們由多臺 ecs 配合 SLB 構(gòu)成了 npmmirror 的分布式服務(wù)。同時做一個 registry 服務(wù),存儲是比較復(fù)雜的,registry 中有大量的 tgz 文件需要存儲。每個包的版本、maintainer、disttag 都需要存儲。因此我們需要有 oss 來做文件存儲功能,db 來做包的元信息存儲。
最后由于 registry 的設(shè)計,每次包的訪問都需要返回所有版本的全量信息,有的包的版本會特別多,成千上萬,映射到 db 中的概念就是一個包的數(shù)據(jù)可能有幾萬行,一次查詢需要返回幾萬行的數(shù)據(jù)。對于 db 的存儲壓力是很大的,因此 redis 緩存在 npmmirror 中會是一個必要組成部分。
我們要做 npmmirror.com 的一大原因是訪問海外的 npm 服務(wù)在網(wǎng)絡(luò)上不通暢,我們需要一個鏡像服務(wù)來保障國內(nèi)使用 npm 的服務(wù)穩(wěn)定以及快速。因此我們分別在香港和上海建立了兩個鏡像站,通過香港鏡像站來保障海外服務(wù)同步的穩(wěn)定性,通過上海鏡像站來保障國內(nèi)鏡像服務(wù)的穩(wěn)定性。并且我們使用了阿里云的 CDN 服務(wù),在保障全國的訪問性能的同時降低了使用成本。
?cnpmcore 功能介紹

cnpmcore 是 npmmirror.com 背后的實現(xiàn),在 github 上開源。除了基礎(chǔ)的 npm 鏡像服務(wù),私有包發(fā)布功能之外,我們還增加了企業(yè)強(qiáng)烈需要的多 registry 同步功能,bug-versions 應(yīng)急止血能力。最后 cnpmcore 還能很方便的進(jìn)行二次研發(fā),便于企業(yè)內(nèi)部定制。下面來為大家詳細(xì)介紹一下 cnpmcore。

應(yīng)急止血

我們來看一下應(yīng)急止血的問題,代碼都是人寫的,出 bug 很正常。前端研發(fā)重度依賴了開源社區(qū)的代碼,如果依賴出了 bug 會是一個比較棘手的問題。開源社區(qū)提供了一下的解決方案,可以通過在 package.json 中聲明 overrides 來強(qiáng)制指定依賴的版本,可以通過修改 lock 文件來強(qiáng)制指定需要安裝的依賴版本。另外既然使用的是開源依賴,我們當(dāng)然也可以通過開源的方式,提 PR 來修復(fù) bug。
但是這些方式都比較低效,只能解決單個項目的問題,而企業(yè)內(nèi)部有成百上千的項目,一個個項目去修改顯然是很浪費(fèi)人力的,我們需要一個更加高效的方式。

我們實現(xiàn)了 bug-versions 來解決這類問題,bug-versions 有以下能力:
在安裝時自動回滾至無 bug 的版本
在安裝時覆蓋有安全問題的 install 腳本
在安裝時自動升級至安全的 node 版本
通過這一系列的能力組合,就能實現(xiàn)安全的 npm 依賴安裝。

bug-versions 分別可以在 npm 客戶端和服務(wù)端運(yùn)行,各有優(yōu)劣。在客戶端使用時,可以很好的覆蓋所有的場景,但是也會帶來較高的客戶端維護(hù)成本。在服務(wù)端運(yùn)行的場景下功能比較局限,只能進(jìn)行 bug 版本的替換,額外的收益是使用任何 npm 客戶端都行,不需要維護(hù)一個自己的客戶端。目前 npmmirror.com 上也開啟了 registry 版本的 bug-versions,我們在使用 npmmirror.com 作為 registry 的時候默認(rèn)就是安全的,在現(xiàn)階段 npm 包安全問題頻發(fā)的時候為大家阻擋了很多的安全問題。

bug-versions 維護(hù)在 github 上,通過 PR 即可新增 bug 版本的記錄,在 PR 合并之后會通過 github action 自動發(fā)布到 npm。現(xiàn)在已經(jīng)維護(hù)了 151 個 bug 記錄。在這里要特別感謝一下開源社區(qū),是開源社區(qū)幫助我們積累了這么多的版本,如果只是一個內(nèi)部維護(hù)的私有包,我們是沒法做到這么多的。我們也期待越來越多的開發(fā)者來加 cnpmcore 的開源社區(qū),可以就從簡單的 bug-versions 做起,這樣簡單的工作也是可以讓全國的開發(fā)者收益的。
穩(wěn)定可靠
npm 鏡像很重要的點是同步的成功率和性能,在 cnpmcore 中極大的增強(qiáng)了這個點。

我們先看看不穩(wěn)定的同步會怎么樣,monorepo 已經(jīng)在前端研發(fā)中很流行了,對于大型項目來說,整體的組成一般都是很復(fù)雜的,會分為很多的模塊,如果將模塊放在一個 npm 包隔離性顯得不夠,而如果將模塊都放到不同的倉庫中又顯得太為分散,維護(hù)和開發(fā)成本都會比較高,monorepo 對于大型項目來說就顯得正正好,模塊和模塊之間有包的強(qiáng)隔離,又有單個倉庫的優(yōu)勢,絕配。但是 monorepo 也不是沒有成本的,每次 monorepo 倉庫發(fā)布會有大量的 npm 包發(fā)布,而且這些 npm 包之間會有互相依賴,只要有一個包沒有同步,項目就可能跑不起來。我們可以看到 umi 和 tegg 都有 20 個以上的 npm 包,這給同步成功率帶來了很大的壓力。

我們來看一下需要實現(xiàn)穩(wěn)定的同步機(jī)制需要有哪些要素。首先是持久化,所有的同步任務(wù)需要持久化,不能因為意外的情況導(dǎo)致同步任務(wù)丟失,另外是任務(wù)需要有穩(wěn)定的重試機(jī)制,失敗的任務(wù)需要能一直重試,直到成功。如果出現(xiàn)任務(wù)丟失,任務(wù)失敗,沒有自動化的手段去解決這樣的問題,就會導(dǎo)致包版本丟失、tag 不存在等等問題,影響用戶體驗,需要用戶人工介入,手動補(bǔ)償,這樣的體驗就很差了。另外怎么 sync 也不是人人都會的操作,我們經(jīng)常遇到的一類咨詢就是,為什么源上沒有包的某個版本。

為了實現(xiàn)這兩個要素,我們基于 DB 和 redis 實現(xiàn)了一個持久化的任務(wù)隊列。說到持久化 ,DB 一定是一個不二之選,我們把 task 存入了 db 就不可能丟失,但是 db 中并沒有隊列這樣的數(shù)據(jù)結(jié)構(gòu),靠 sql 搜索的話并不高效。一般需要隊列的情況我們都會引入 mq,但是 cnpmcore 是一個開源項目,依賴的基礎(chǔ)設(shè)施需要越少越好,基礎(chǔ)設(shè)施越多的話,部署的成本也就越高。因此我們得合理的利用我們現(xiàn)有的基礎(chǔ)設(shè)施,在 redis 中有一種數(shù)據(jù)結(jié)構(gòu)是 sort set,可以很好的解決我們按序執(zhí)行,插入冪等的需求。

讓我們再來看下任務(wù)的狀態(tài)機(jī),任務(wù)的初始狀態(tài)是創(chuàng)建,在創(chuàng)建完成之后會入隊,出對后進(jìn)入運(yùn)行狀態(tài),在任務(wù)出現(xiàn)超時、失敗的情況時,會重新入對,成功后會進(jìn)入終態(tài)成功。通過這樣的狀態(tài)流轉(zhuǎn)來保證了任務(wù)一定成功。
新舊 registry 遷移?

再來看一下如何從 cnpmjs.org 向 cnpmcore 遷移,本身鏡像的遷移是沒有成本的,做一次同步即可。比較大的問題是私有包如何進(jìn)行同步,在企業(yè)內(nèi)部往往有大量的私有包,這些包的遷移會有較高的成本。螞蟻內(nèi)部也正在遷移中,下面就是我們的遷移方案(正在研發(fā)中)。

我們會期望內(nèi)部私有的 package 和 maintainer 都能自動的同步,避免人工導(dǎo)入導(dǎo)出數(shù)據(jù)的操作。在 cnpmjs 和 cnpmcore 遷移的時候切換的時候我們期望是遷移是可控的,逐步切讀切些,因此老的 cnpmjs 還會不斷的產(chǎn)出數(shù)據(jù),如果靠人工的話,時效性和數(shù)據(jù)的準(zhǔn)確性都會是問題。既然我們可以從 npm 同步數(shù)據(jù),那應(yīng)該一樣可以從 cnpmjs.org 把私有包同步過來。這樣的話我們就需要一個能從多個 registry 同步的功能了。

我們簡單來看下建模,首先是 registry,我們會在 registry 下指定默認(rèn)哪些 scope 可以同步,scope 指定了新增 package 的默認(rèn) registry 值,在最老的 cnpmjs 的實現(xiàn)中,私有 package 是可以不帶 scope 發(fā)布的,這些 package 我們就可以修改其 registry 信息。

基于這樣的建模設(shè)計,我們即可實現(xiàn)以下幾大需求,最基本的指定 scope 可以從哪同步實現(xiàn)遷移功能,提升同步安全性,避免了 npm 上的重名攻擊,最后是老版本的 cnpmjs 兼容,無縫遷移。
cnpmcore 二次研發(fā)介紹

在介紹完基本功能之后,下面會分享一下如何基于 cnpmcore 去定制企業(yè)內(nèi)部自己的 npm registry。
cnpmcore 完全采用 ts 進(jìn)行研發(fā),使用了 tegg 框架,引入了 DDD 研發(fā)模式。首先會介紹這些基本概念。
我們來看下 cnpmjs 和 cnpmcore 的實現(xiàn)區(qū)別,cnpmjs 采用 js + koa 的方式進(jìn)行研發(fā),函數(shù)中的 this 就是上下文,可以通過 this 來獲取參數(shù)。cnpmcore 采用 ts + tegg 進(jìn)行研發(fā),我們可以看到所有的參數(shù)在方法簽名中,以及引入了 http 上下文參數(shù)。所有的參數(shù)有 ts 的類型定義,我們可以很清楚的明白入?yún)⒂惺裁?,是什么?/p>
接下來再簡單介紹下 DDD 的基本概念,領(lǐng)域建模,核型的代碼邏輯將會收斂到模型中。這樣可以解決兩大問題,防止業(yè)務(wù)邏輯分散在四處,包很重要的概念是 scope,name,通過領(lǐng)域模型 Package 中的 fullname getter 即可獲取,而不是在每個地方單獨(dú)寫一下。另外還會有狀態(tài)的問題,比如在任務(wù)模型中,會有 logPath 和 logStorePository 兩個字端,兩個字端是有強(qiáng)關(guān)聯(lián)的,在 logPath 更新的時候 position 應(yīng)該同步的清空,通過領(lǐng)域模型的方法就可以很好的對其進(jìn)行保護(hù)。
再來看下我們的項目架構(gòu),最上面是我們的接入層,默認(rèn)提供了標(biāo)準(zhǔn)的 HTTP registry 接口,可以對其進(jìn)行擴(kuò)展,比如進(jìn)行消息、RPC 等等其他方式的接入。最下層是基礎(chǔ)設(shè)施層,可對其進(jìn)行插拔替換,目前 cnpmcore 是部署在阿里云上,提供基礎(chǔ)設(shè)施的替換也可以部署到 azure、aws 上。
介紹完這些基本概念,讓我們來進(jìn)行幾個小小的實戰(zhàn)演練。
小明是我們打工人,這位黑黑的是我們的老板,會對小明提需求。首先是需要把 cnpmcore 運(yùn)行起來。為什么我們已經(jīng)有了 npmmirror.com 還需要在企業(yè)內(nèi)部部署自己的 registry 服務(wù)?在研發(fā)上,出了對于依賴下載的需求,發(fā)布 npm 包也是非常重要的需求,工程師極具造輪子的欲望。而 npmmirror.com 僅提供了鏡像代理的能力,并未開啟私有包發(fā)布功能,因此在企業(yè)內(nèi)部部署一套自己私有的 cnpmcore 是非常有必要的。
運(yùn)行前有個前置條件,我們假設(shè)大家已經(jīng)有了一個 tegg 的應(yīng)用,沒有的話可以趕緊用起來。首先安裝 cnpmcore 依賴,在 config/module.json 中將 cnpmcore 所有的 module 聲明出來。最后只要運(yùn)行 npm run dev 即可開始激動人心的 cnpmcore 之旅。
另外小明公司使用的云是 aws,需要實現(xiàn) aws 的適配。小明抄起鍵盤就實現(xiàn)了一個 s3 nfs client 替換了阿里云 oss,在 module.json 中替換了 cnpmcore 的 infra 模塊。一頓操作就滿足了 aws 的適配。
這時候黑黑的老板又給小明提了一個安全需求,所有的 npm 操作都需要交給安全進(jìn)行分析。npm 的很多操作都是高危的,比如發(fā)布新的版本、修改 dist-tag,這些操作都將對我們的一線研發(fā)工作有很大的影響,如果沒有很好的控制出了漏洞,后果是很嚴(yán)重的。因此將 npm 操作記錄導(dǎo)出給安全審計是非常有必要的。
小明在深入了解 npm registry 之后,發(fā)現(xiàn)操作特別的多,有發(fā)布、修改 tag、修改 maintainer、刪除等等。這些操作都需要打印審計日志,交給安全進(jìn)行審計。如果需要去修改 cnpmcore 的代碼成本有些過高了,而 cnpmcore 正好實現(xiàn)了領(lǐng)域事件,所有的變更都會通過應(yīng)用內(nèi)的 eventbus 進(jìn)行廣播,二次開發(fā)的時候?qū)ζ溥M(jìn)行監(jiān)聽即可,完全不需要對 cnpmcore 進(jìn)行侵入性的改動。小明又快速的抄起鍵盤,定義好日志結(jié)構(gòu)后,增加了事件監(jiān)聽的實現(xiàn)就把老板的需求給做了。
前端行業(yè)蓬勃發(fā)展,老板又招了五千個前端,對 xnpmcore 造成了很大的壓力,前端依賴安裝的性能變得很差。這里舉這個例子,正好可以將我們的框架、系統(tǒng)、工具鏈都覆蓋到,我們將會看到如何使用性能分析工具去定位問題,如果通過框架提供的能力去無傾入性的優(yōu)化代碼,最后也可以到我們系統(tǒng)的內(nèi)部實踐。
小明使用 ezm 快速定位到了熱點問題,原因是某個倉儲層的方法查詢頻率過高了,導(dǎo)致 db 壓力很大。小明迅速想到 cnpmcore 已經(jīng)有了 redis 依賴,可以使用 redis 來做方法執(zhí)行的緩存。但是如何在不修改 cnpmcore 的代碼下對其邏輯進(jìn)行修改呢?小明深入了解了 tegg 發(fā)現(xiàn)其提供了 AOP 功能,可以對方法進(jìn)行劫持,其中的 around 方法就能沒有傾入性的實現(xiàn)緩存的邏輯。配合上一節(jié)的領(lǐng)域事件,可以在 npm 版本變更后對緩存進(jìn)行失效操作,這樣就能避免臟數(shù)據(jù)問題。
經(jīng)歷了這些需求,小明已經(jīng)成長為了一名資深 cnpmcore 工程師,希望對大家的工作也有幫助。
???????????????歡迎體驗:
https://github.com/cnpm/cnpmcore