Node.js中的code cache

更新日期: 2019-08-04阅读: 5k标签: 缓存

产生背景

V8使用JIT去解析JS,在JS代码执行之前,首先要消耗大量的时间在解析和编译中,这会拖慢JS的总执行时间,在2015年V8支持了code cache方案来解决这个问题。


code cache是什么

简单来说,使用了code caching后,当JS第一次被编译后,将生成的代码序列化后存储在磁盘上,后续再次加载时,会优先读取磁盘中的缓存,反序列化后执行,避免JS被从头编译造成重复开销。
实质是通过V8提供的几个方法实现的,在node中通过 vm.Script 调用。

v8::ScriptCompiler::kProduceCodeCache         生成code cache
v8::ScriptCompiler::Source::GetCachedData      获取code cache
v8::ScriptCompiler::kConsumeCodeCache       消费生成的code cache


使用场景

浏览器

通常浏览器进行资源缓存,存在两种方式,第一种是内存缓存,经过编译之后生成的代码存储在内存当中,
可以在同一个v8实例中被反复使用。第二种就是code cache,对编译之后生成的代码持久化到磁盘。这种方式缓存的结果不会被特定的个v8实例独占,可以在多个实例中使用。但是缺点也相对比较明显,就是会一直占用磁盘空间。所以并不是所有JS都会使用第二个缓存方案,一般初次运行的JS会被存储在内存中,在指定时间被重复多次访问的JS才会被存储到磁盘上。


node

这个功能首次被介绍是在Node v5.7.0中,可以通过vm.Script使用V8code cache功能,通过加快实例化时间来减少node的启动时间,经典实现可以参照v8-compile-cache。

    require('v8-compile-cache')

    它的适用方法非常简单,只需要引入即可。让我们来看下v8-compile-cache中做了什么。

    if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) {
      const cacheDir = getCacheDir();
      const prefix = getParentName();
      const blobStore = new FileSystemBlobStore(cacheDir, prefix);
    
      const nativeCompileCache = new NativeCompileCache();
      nativeCompileCache.setCacheStore(blobStore);
      nativeCompileCache.install();
    
      process.once('exit', code => {
        if (blobStore.isDirty()) {
          blobStore.save();
        }
        nativeCompileCache.uninstall();
      });
    }

    首先检测环境变量,看用户是否禁用此功能并且判断当前版本是否支持此功能

      function getCacheDir() {
        // Avoid cache ownership issues on POSIX systems.
        const dirname = typeof process.getuid === 'function'
          ? 'v8-compile-cache-' + process.getuid()
          : 'v8-compile-cache';
        const version = typeof process.versions.v8 === 'string'
          ? process.versions.v8
          : typeof process.versions.chakracore === 'string'
            ? 'chakracore-' + process.versions.chakracore
            : 'node-' + process.version;
        const cacheDir = path.join(os.tmpdir(), dirname, version);
        return cacheDir;
      }

      接着获取写入位置,一般是在/private/var/folders 或者/var/folders路径下,

      const blobStore = new FileSystemBlobStore(cacheDir, prefix);

      接着建立一个blobStore

      class NativeCompileCache {
        ...
        install() {
          const self = this;
          const hasRequireResolvePaths = typeof require.resolve.paths === 'function';
          // 保留原生模块的_compile 方法
          this._previousModuleCompile = Module.prototype._compile;
          Module.prototype._compile = function(content, filename) {
          const mod = this;
          
          function require(id) {
            return mod.require(id);
          }
      
          // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28
          function resolve(request, options) {
            return Module._resolveFilename(request, mod, false, options);
          }
          require.resolve = resolve;
      
          // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37
          // resolve.resolve.paths was added in v8.9.0
          if (hasRequireResolvePaths) {
            resolve.paths = function paths(request) {
            return Module._resolveLookupPaths(request, mod, true);
            };
          }
      
          require.main = process.mainModule;
          // Enable support to add extra extension types
          require.extensions = Module._extensions;
          require.cache = Module._cache;
      
          const dirname = path.dirname(filename);
      
          const compiledWrapper = self._moduleCompile(filename, content);
      
          // We skip the debugger setup because by the time we run, node has already
          // done that itself.
      
          const args = [mod.exports, require, mod, filename, dirname, process, global];
          return compiledWrapper.apply(mod.exports, args);
          };
        }
        ...
      }

      接着在NativeCompileCache类中,重写了compile方法,提供了新的require方法,其中调用内部moduleCompile方法

      _moduleCompile(filename, content) {
          ...
          // create wrapper function
          var wrapper = Module.wrap(content);
      
          var invalidationKey = crypto
            .createHash('sha1')
            .update(content, 'utf8')
            .digest('hex');
      
          var buffer = this._cacheStore.get(filename, invalidationKey);
      
          var script = new vm.Script(wrapper, {
            filename: filename,
            lineOffset: 0,
            displayErrors: true,
            cachedData: buffer,
            produceCachedData: true,
          });
      
          if (script.cachedDataProduced) {
         // 写入到内存中
            this._cacheStore.set(filename, invalidationKey, script.cachedData);
          } else if (script.cachedDataRejected) {
            this._cacheStore.delete(filename);
          }
      
          var compiledWrapper = script.runInThisContext({
            filename: filename,
            lineOffset: 0,
            columnOffset: 0,
            displayErrors: true,
          });
      
          return compiledWrapper;
        }

      moduleCompile中主要做了这几件事,创建 wrapper function,然后在_cacheStore中获取内存或磁盘中的存储内容,调用new vm.Script进行编译(并不执行),第一个参数是code string,是我们包装过的代码 wrapper,options为配置项,接着从内存中取已经生成cachedData,cachedData是编译好的code cache,如果没有传入的话,会通过配置produceCachedData:true来生成code cache,并保存到内存中。

        var compiledWrapper = script.runInThisContext({
            filename: filename,
            lineOffset: 0,
            columnOffset: 0,
            displayErrors: true,
          });
          
        const compiledWrapper = self._moduleCompile(filename, content);
        const args = [mod.exports, require, mod, filename, dirname, process, global];
        return compiledWrapper.apply(mod.exports, args);

        接着运行其中的代码,返回compiledWrapper,执行 compiledWrapper.apply(mod.exports, args)时, 对 mod 重新赋值,应用了新的 require 生成了新的 module.exports,通过Module.prototype._load将结果返回给用户。

        process.once('exit', code => {
           if (blobStore.isDirty()) {
             blobStore.save();
           }
           nativeCompileCache.uninstall();
        });

        最后在进程退出的时候,如果检测内存中有更新,则持久化到磁盘中。


        未开启code cache


        开启code cache
        以上是babel-core.js和rxjs-bundle.js在开启code cache和关闭code cache的加载数据,相比有较大幅度提升。


        总结

        code cache通过加快实例化时间来减少node的启动时间,在faas场景下也有重要使用场景,自动缩扩容是K8S的重要特性,面对忽然到来的大流量,需要在极短的时间对函数进行扩容并可以正常相应请求,这就对Node.js 函数的启动时间有了较高的要求,可以在构建阶段直接将代码转换为字节码并打包到镜像中,通过减少编译时间,来提升faas函数的启动时间。

        作者:Node地下铁
        来自:http://www.sohu.com/a/331337841_657169


        链接: https://fly63.com/article/detial/4522

        浏览器缓存_HTTP强缓存和协商缓存

        浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存),强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。协商缓存就是由服务器来确定缓存资源是否可用.

        angularjs 缓存详解

        一个缓存就是一个组件,它可以透明地存储数据,以便未来可以更快地服务于请求。缓存能够服务的请求越多,整体系统性能就提升得越多。

        浏览器缓存问题原理以及解决方案

        浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中,为什么使用缓存:减少网络带宽消耗,降低服务器压力,减少网络延迟,加快页面打开速度

        Web缓存相关知识整理

        一个H5页面在APP端,如果勾选已读状态,则下次打开该链接,会跳过此页面。用到了HTML5 的本地存储 API 中的 localStorage作为解决方案,回顾了下Web缓存的知识

        使用缓存加速之后的网站访问过程变化

        在描述CDN的实现原理之前,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别,用户访问未使用CDN缓存网站的过程为:用户向浏览器提供要访问的域名;

        html页面清除缓存

        页面打开时,由于缓存的存在,刚刚更新的数据有时无法在页面得到刷新,当这个页面作为模式窗口被打开时问题更为明显, 如何将缓存清掉?

        HTTP之缓存 Cache-Control

        通过在Response Header设置Cache-Control head 信息可以控制浏览器的缓存行为。我们先来看一下Cache-Control可以设置哪些值:缓存头Cache-Control只能在服务端设置,在客户端是由浏览器设置的,自己不能修改它的值。

        工程化_前端静态资源缓存策略

        增量更新是目前大部分团队采用的缓存更新方案,能让用户在无感知的情况获取最新内容。具体实现方式通常是(一般我们通过构建工具来实现,比如webpack):

        前端静态资源自动化处理版本号防缓存

        浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。

        vue后台管理系统解决keep-alive页面路由参数变化时缓存问题

        一个后台管理系统,一个列表页A路由配置需要缓存,另一个页面B里面有多个跳转到A路由的链接。问题描述:首先访问/A?id=1页面,然后到B页面再点击访问A?id=2的页面,发现由于页面A设置了缓存,数据还是id=1的数据,并没有更新。

        点击更多...

        内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!