記一次排錯經歷——npm緩存淺析

緣起

一次在安裝項目依賴的時候,終端報了下面這個錯,致使依賴安裝失敗。javascript

經過報錯信息能夠看出是 @sentry/cli 這個包的緣由,由於項目中並無直接依賴這個包,爲了排除包之間的影響,就新建了一個文件夾,單獨安裝這個包,發現仍是報同樣的錯。而後就讓同事安裝下這個包試一下,發現一切正常,並無報錯。html

接下來就是一通操做:google搜、github issue搜、換成npm安裝、切換npm源、切換node版本、安裝別的版本 @sentry/cli 、清除yarn和npm的緩存、重啓電腦。。。然而發現並無什麼卵用。。。java

看來事情並無那麼簡單

再回過頭來看報錯信息,能夠發現是在執行 node scripts/install.js 時出現的錯誤,那就把代碼拉下來本地跑一下看看咯。說幹就幹,把 @sentry/cli clone到本地以後,先安裝下依賴,而後執行node scripts/install.js 發現以下報錯:node

發現其實是在執行 /Users/sliwey/githome/sentry-cli/sentry-cli --version 命令時發生的錯誤,根據上面的路徑發如今項目根目錄下多了一個叫 sentry-cli 的可執行文件。git

因此應該是這個文件有問題,那麼這個文件是哪裏來的呢,看一下 scripts/install.js 的代碼,會發現其實就作了一件事:github

downloadBinary()
      .then(() => checkVersion())
      .then(() => process.exit(0))
      .catch(e => {
        console.error(e.toString());
        process.exit(1);
      });
複製代碼

就是下載個可執行的文件,而後檢查下版本號。checkVersion先按下不表,不是重點,就只是判斷下版本號,來看 downloadBinary (我簡化了一下代碼,加了點註釋,具體代碼可查看github.com/getsentry/s…):npm

function downloadBinary() {
      const arch = os.arch();
      const platform = os.platform();
      const outputPath = helper.getPath();
    
      // 根據不一樣系統獲取對應的下載連接
      const downloadUrl = getDownloadUrl(platform, arch);
    
      // 根據下載連接生成緩存路徑
      const cachedPath = getCachedPath(downloadUrl);
    
      // 緩存命中,就把文件複製到當前路徑下
      if (fs.existsSync(cachedPath)) {
        copyFileSync(cachedPath, outputPath);
        return Promise.resolve();
      }
    
      // 緩存未命中,就下載,並把文件寫入緩存
      return fetch(downloadUrl, { redirect: 'follow', agent }).then(response => {
        const tempPath = getTempFile(cachedPath);
        mkdirp.sync(path.dirname(tempPath));
    
        return new Promise((resolve, reject) => {
          response.body
            .pipe(fs.createWriteStream(tempPath, { mode: '0755' }))
        }).then(() => {
          copyFileSync(tempPath, cachedPath);
          copyFileSync(tempPath, outputPath);
          fs.unlinkSync(tempPath);
        });
      });
    }
複製代碼

根據剛纔本地的執行狀況來看,並無進行下載,可知那個可執行文件是從緩存中拿的,那就打個斷點看一下緩存路徑:緩存

根據獲得的路徑,刪除對應文件,而後從新安裝,everything is ok~bash

下面的纔是重點

雖然問題解決了,可是回想了一下以前的一通操做,其中是有作過緩存清除的,包括yarn和npm,當時的作法是經過下面兩個命令作的:fetch

yarn cache clean
    npm cache clean --force
複製代碼

根據上面獲得的緩存路徑,能夠知道 sentry-cli 緩存在 ~/.npm 文件夾下,因此跟yarn應該不要緊,先排除掉。而後來看npm,發現經過 npm cache clean --force 來清除緩存,並無清掉 ~/.npm 文件夾下的文件,那麼這個命令清的是哪裏呢?先看下文檔怎麼說:npm-cache

爲了閱讀方便,我截了幾個圖:

乍一看貌似沒什麼毛病,檢查了一下本身的cache配置,也沒有發現什麼異常:

那麼究竟是哪裏的問題呢,看來只能看下源碼了,目標很直接,找到npm中跟cache相關的代碼,而後直接看clean方法的實現(具體代碼能夠看 lib/cache.js):

function clean (args) {
      if (!args) args = []
      if (args.length) {
        return BB.reject(new Error('npm cache clear does not accept arguments'))
      }
    
      // 重點在這
      // npm.cache就是 ~/.npm
      // 因此cachePath的值應該是 ~/.npm/_cacache
      const cachePath = path.join(npm.cache, '_cacache')
      if (!npm.config.get('force')) {
        return BB.reject(new Error("As of npm@5, the npm cache self-heals from corruption issues and data extracted from the cache is guaranteed to be valid. If you want to make sure everything is consistent, use 'npm cache verify' instead. On the other hand, if you're debugging an issue with the installer, you can use `npm install --cache /tmp/empty-cache` to use a temporary cache instead of nuking the actual one.\n\nIf you're sure you want to delete the entire cache, rerun this command with --force."))
      }
      // TODO - remove specific packages or package versions
      return rm(cachePath)
    }
複製代碼

看到這就很明白了, npm cache clean --force 清的是 ~/.npm/_cacache 文件夾中的數據。

轉念一想,這一點在文檔中不該該不提啊,再回去看一下文檔,發現漏看了一塊內容。。。

內容以下:

簡單來講在 npm@5 以後,npm把緩存數據放在配置文件中 cache 字段配置的路徑下面的 _cacache 文件夾中。結合上面兩段文檔的內容,可得出:

  • 配置文件中的 cache 字段配置的是根目錄
  • 緩存數據放在根目錄中的 _cacache 文件夾中
  • clean 命令清除的是 _cacache 文件夾

npm緩存到底存了什麼

打開 _cacache 文件夾,發現裏面並非像 node_modules 裏面同樣一個個的包,而是這樣的:

打開能夠發現 content-v2 裏面基本都是一些二進制文件,把二進制文件的擴展名改成 .tgz 再解壓以後,會發現就是在咱們熟知的npm包。 index-v5 裏面是一些描述性的文件,也是 content-v2 裏文件的索引,仔細看會發現有點像HTTP的響應頭,並且還有緩存相關的值:

那麼這些文件是怎麼生成的呢?從上面的文檔中,能夠得知,npm 主要是用 pacote 來安裝包的,咱們來看一下 npm 在代碼中是怎麼使用pacote的吧。npm主要有如下三個地方會用到 pacote:

對比上述三個 pacote 的方法能夠發現,其主要依賴的方法是 lib/withTarballStream.js,代碼比較多,簡化一下,主要看中文註釋就好:

function withTarballStream (spec, opts, streamHandler) {
      opts = optCheck(opts)
      spec = npa(spec, opts.where)
    
      // 讀本地文件
      const tryFile = (
        !opts.preferOnline &&
        opts.integrity &&
        opts.resolved &&
        opts.resolved.startsWith('file:')
      )
        ? BB.try(() => {
          const file = path.resolve(opts.where || '.', opts.resolved.substr(5))
          return statAsync(file)
            .then(() => {
              const verifier = ssri.integrityStream({ integrity: opts.integrity })
              const stream = fs.createReadStream(file)
                .on('error', err => verifier.emit('error', err))
                .pipe(verifier)
              return streamHandler(stream)
        })
        : BB.reject(Object.assign(new Error('no file!'), { code: 'ENOENT' }))
    
      // 上一步reject以後,從緩存中讀
      const tryDigest = tryFile
        .catch(err => {
          if (
            opts.preferOnline ||
          !opts.cache ||
          !opts.integrity ||
          !RETRIABLE_ERRORS.has(err.code)
          ) {
            throw err
          } else {
    	    // 經過cacache來讀緩存中的數據
            const stream = cacache.get.stream.byDigest(
              opts.cache, opts.integrity, opts
            )
            stream.once('error', err => stream.on('newListener', (ev, l) => {
              if (ev === 'error') { l(err) }
            }))
            return streamHandler(stream)
              .catch(err => {
                if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
                  opts.log.warn('tarball', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
                  // 當錯誤碼爲EINTEGRITY或Z_DATA_ERROR時,清除緩存
                  return cleanUpCached(opts.cache, opts.integrity, opts)
                    .then(() => { throw err })
                } else {
                  throw err
                }
              })
          }
        })
    
      // 上一步reject以後,再下載
      const trySpec = tryDigest
        .catch(err => {
          if (!RETRIABLE_ERRORS.has(err.code)) {
          // If it's not one of our retriable errors, bail out and give up.
            throw err
          } else {
            return BB.resolve(retry((tryAgain, attemptNum) => {
    
    	      // 下載包,這邊實際上是經過npm-registry-fetch來下載的
              const tardata = fetch.tarball(spec, opts)
              if (!opts.resolved) {
                tardata.on('manifest', m => {
                  opts = opts.concat({ resolved: m._resolved })
                })
                tardata.on('integrity', i => {
                  opts = opts.concat({ integrity: i })
                })
              }
              return BB.try(() => streamHandler(tardata))
            }, { retries: 1 }))
          }
        })
    
      return trySpec
        .catch(err => {
          if (err.code === 'EINTEGRITY') {
            err.message = `Verification failed while extracting ${spec}:\n${err.message}`
          }
          throw err
        })
    }
複製代碼

從上述代碼中,能夠知道 pacote 是依賴 npm-registry-fetch 來下載包的。查看 npm-registry-fetch 的文檔發現,在請求時有個 cache 屬性能夠設置:npm-registry-fetch#opts.cache

可知,若是設置了 cache 的值(npm中是 ~/.npm/_cacache),便會在給定的路徑下建立根據IETF RFC 7234生成的緩存數據。打開那個rfc的地址,發現就是描述 HTTP 緩存的文檔,因此本段開頭說的 index-v5 下面的文件也就好理解了。

簡單總結一下:

  • ~/.npm/_cacache 中存的是一些二進制文件,以及對應的索引。
  • npm install 時,有緩存的話,會經過 pacote 把對應的二進制文件解壓到相應的 node_modules 下面。
  • npm自己只提供清除緩存和驗證緩存完整性的方法,不提供直接操做緩存的方法,能夠經過 cacache 來操做這些緩存數據。

寫在最後

回顧了一下整件事情,發現文檔看仔細是多麼重要!謹記!謹記!可是也把平時不怎麼關注的點梳理了一遍,也算是有所收穫,以文字的形式記錄下來,便於回顧。

原文連接: github.com/sliwey/blog…

相關文章
相關標籤/搜索