Latar belakang
Saya percaya bahawa pelajar yang telah membangunkan aplikasi web menggunakan Node.js pasti telah bermasalah dengan masalah bahawa kod yang baru diubah suai mesti dimulakan semula sebelum proses Node.js boleh dikemas kini. Pelajar yang biasa menggunakan PHP untuk pembangunan akan mendapati ia sangat tidak boleh digunakan Seperti yang dijangkakan, PHP adalah bahasa pengaturcaraan terbaik di dunia. Memulakan semula proses secara manual bukan sahaja merupakan pertindihan kerja yang sangat menjengkelkan, tetapi apabila skala aplikasi menjadi lebih besar, masa permulaan secara beransur-ansur mula menjadi tidak dapat diabaikan.
Sudah tentu, sebagai pengaturcara, tidak kira bahasa yang anda gunakan, anda tidak akan membiarkan perkara sebegitu menyeksa anda. Cara paling langsung dan universal untuk menyelesaikan masalah seperti ini ialah memantau pengubahsuaian fail dan memulakan semula proses. Kaedah ini juga telah disediakan oleh banyak penyelesaian matang, seperti penyelia nod yang terbengkalai, PM2 yang kini popular, atau nod-dev yang agak ringan, dsb., yang semuanya berdasarkan idea ini.
Artikel ini memberikan idea lain dengan hanya pengubahsuaian kecil, anda boleh mencapai kod kemas kini panas sifar semula dan menyelesaikan masalah kemas kini kod yang menjengkelkan semasa membangunkan aplikasi web dengan Node.js.
Idea umum
Bercakap tentang kemas kini panas kod, yang paling terkenal pada masa ini ialah fungsi kemas kini hangat bahasa Erlang Bahasa ini dicirikan oleh konkurensi tinggi dan pengaturcaraan teragih Senario aplikasi utamanya adalah bidang seperti perdagangan sekuriti dan pelayan permainan . Senario ini lebih kurang memerlukan perkhidmatan mempunyai cara pengendalian dan penyelenggaraan semasa operasi, dan pengemaskinian panas kod adalah bahagian yang sangat penting, jadi kita boleh melihat secara ringkas pendekatan Erlang.
Memandangkan saya tidak pernah menggunakan Erlang, kandungan berikut semuanya khabar angin Jika anda ingin mempunyai pemahaman yang mendalam dan tepat tentang pelaksanaan kemas kini panas kod Erlang, sebaiknya rujuk dokumentasi rasmi.
Pemuatan kod Erlang diuruskan oleh modul yang dipanggil code_server Kecuali beberapa kod yang diperlukan semasa permulaan, kebanyakan kod dimuatkan oleh code_server.
Apabila code_server mendapati bahawa kod modul telah dikemas kini, ia akan memuat semula modul Permintaan baharu selepas itu akan dilaksanakan menggunakan modul baharu, manakala permintaan yang masih dilaksanakan akan terus dilaksanakan menggunakan modul lama.
Modul lama akan dilabelkan lama selepas modul baharu dimuatkan dan modul baharu akan dilabel semasa. Semasa kemas kini panas seterusnya, Erlang akan mengimbas dan membunuh modul lama yang masih dilaksanakan, dan kemudian terus mengemas kini modul mengikut logik ini.
Tidak semua kod dalam Erlang membenarkan kemas kini hangat modul asas seperti kernel, stdlib, pengkompil dan modul asas lain tidak dibenarkan dikemas kini secara lalai
.
Kita boleh mendapati bahawa Node.js juga mempunyai modul yang serupa dengan code_server, iaitu sistem yang diperlukan, jadi pendekatan Erlang juga harus dicuba pada Node.js. Dengan memahami pendekatan Erlang, kami boleh meringkaskan secara kasar isu utama dalam menyelesaikan kemas kini hangat kod dalam Node.js
Cara mengemas kini kod modul
Cara mengendalikan permintaan menggunakan modul baharu
Bagaimana untuk melepaskan sumber modul lama
Kemudian mari kita analisa titik masalah ini satu persatu.
Cara mengemas kini kod modul
Untuk menyelesaikan masalah kemas kini kod modul, kita perlu membaca pelaksanaan pengurus modul Node.js dan memaut terus ke module.js. Melalui pembacaan mudah, kita dapati bahawa kod teras terletak pada Module._load Mari permudahkan kod dan siarkan.
// Check the cache for the requested file. // 1. If a module already exists in the cache: return its exports object. // 2. If the module is native: call `NativeModule.require()` with the // filename and return the result. // 3. Otherwise, create a new module for the file and save it to the cache. // Then have it load the file contents before returning its exports // object. Module._load = function(request, parent, isMain) { var filename = Module._resolveFilename(request, parent); var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; } var module = new Module(filename, parent); Module._cache[filename] = module; module.load(filename); return module.exports; }; require.cache = Module._cache;
Anda boleh mendapati bahawa terasnya ialah Module._cache Selagi cache modul ini dikosongkan, pengurus modul akan memuatkan semula kod terkini apabila ia diperlukan.
Tulis program kecil untuk mengesahkannya
// main.js function cleanCache (module) { var path = require.resolve(module); require.cache[path] = null; } setInterval(function () { cleanCache('./code.js'); var code = require('./code.js'); console.log(code); }, 5000); // code.js module.exports = 'hello world';
Mari laksanakan main.js dan ubah suai kandungan code.js pada masa yang sama Kita dapati bahawa dalam konsol, kod kami telah berjaya dikemas kini kepada kod terkini.
Jadi masalah pengurus modul yang mengemas kini kod telah diselesaikan Seterusnya, mari lihat bagaimana kita boleh membuat modul baharu benar-benar dilaksanakan dalam aplikasi web.
Cara menggunakan modul baharu untuk mengendalikan permintaan
Untuk lebih selaras dengan tabiat penggunaan semua orang, kami akan terus menggunakan Express sebagai contoh untuk mengembangkan masalah ini Malah, menggunakan idea yang sama, kebanyakan aplikasi web boleh digunakan.
Pertama sekali, jika perkhidmatan kami seperti DEMO Express dan semua kod berada dalam modul yang sama, kami tidak boleh memuatkan modul tersebut
var express = require('express'); var app = express(); app.get('/', function(req, res){ res.send('hello world'); }); app.listen(3000);
Untuk mencapai pemuatan panas, sama seperti perpustakaan asas yang tidak dibenarkan di Erlang, kami memerlukan beberapa kod asas yang tidak boleh dikemas kini secara panas untuk mengawal proses kemas kini. Dan jika operasi seperti app.listen dilaksanakan semula, ia tidak akan jauh berbeza daripada memulakan semula proses Node.js. Oleh itu, kami memerlukan beberapa kod pintar untuk mengasingkan kod perniagaan yang kerap dikemas kini daripada kod asas yang jarang dikemas kini.
// app.js 基础代码 var express = require('express'); var app = express(); var router = require('./router.js'); app.use(router); app.listen(3000); // router.js 业务代码 var express = require('express'); var router = express .Router(); // 此处加载的中间件也可以自动更新 router.use(express.static('public')); router.get('/', function(req, res){ res.send('hello world'); }); module.exports = router;
然而很遗憾,经过这样处理之后,虽然成功的分离了核心代码, router.js 依然无法进行热更新。首先,由于缺乏对更新的触发机制,服务无法知道应该何时去更新模块。其次, app.use 操作会一直保存老的 router.js 模块,因此即使模块被更新了,请求依然会使用老模块处理而非新模块。
那么继续改进一下,我们需要对 app.js 稍作调整,启动文件监听作为触发机制,并且通过闭包来解决 app.use 的缓存问题
// app.js var express = require('express'); var fs = require('fs'); var app = express(); var router = require('./router.js'); app.use(function (req, res, next) { // 利用闭包的特性获取最新的router对象,避免app.use缓存router对象 router(req, res, next); }); app.listen(3000); // 监听文件修改重新加载代码 fs.watch(require.resolve('./router.js'), function () { cleanCache(require.resolve('./router.js')); try { router = require('./router.js'); } catch (ex) { console.error('module update failed'); } }); function cleanCache(modulePath) { require.cache[modulePath] = null; }
再试着修改一下 router.js 就会发现我们的代码热更新已经初具雏形了,新的请求会使用最新的 router.js 代码。除了修改 router.js 的返回内容外,还可以试试看修改路由功能,也会如预期一样进行更新。
当然,要实现一个完善的热更新方案需要更多结合自身方案做一些改进。首先,在中间件的使用上,我们可以在 app.use 处声明一些不需要热更新或者说每次更新不希望重复执行的中间件,而在 router.use 处则可以声明一些希望可以灵活修改的中间件。其次,文件监听不能仅监听路由文件,而是要监听所有需要热更新的文件。除了文件监听这种手段外,还可以结合编辑器的扩展功能,在保存时向 Node.js 进程发送信号或者访问一个特定的 URL 等方式来触发更新。
如何释放老模块的资源
要解释清楚老模块的资源如何释放的问题,实际上需要先了解 Node.js 的内存回收机制,本文中并不准备详加描述,解释 Node.js 的内存回收机制的文章和书籍很多,感兴趣的同学可以自行扩展阅读。简单的总结一下就是当一个对象没有被任何对象引用的时候,这个对象就会被标记为可回收,并会在下一次GC处理的时候释放内存。
那么我们的课题就是,如何让老模块的代码更新后,确保没有对象保持了模块的引用。首先我们以 如何更新模块代码 一节中的代码为例,看看老模块资源不回收会出现什么问题。为了让结果更显著,我们修改一下 code.js
// code.js var array = []; for (var i = 0; i < 10000; i++) { array.push('mem_leak_when_require_cache_clean_test_item_' + i); } module.exports = array; // app.js function cleanCache (module) { var path = require.resolve(module); require.cache[path] = null; } setInterval(function () { var code = require('./code.js'); cleanCache('./code.js'); }, 10);
好~我们用了一个非常笨拙但是有效的方法,提高了 router.js 模块的内存占用,那么再次启动 main.js 后,就会发现内存出现显著的飙升,不到一会 Node.js 就提示 process out of memory。然而实际上从 app.js 与 router.js 的代码中观察的话,我们并没发现哪里保存了旧模块的引用。
我们借助一些 profile 工具如 node-heapdump 就可以很快的定位到问题所在,在 module.js 中我们发现 Node.js 会自动为所有模块添加一个引用
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; }
因此相应的,我们可以调整一下cleanCache函数,将这个引用在模块更新的时候一并去除。
// app.js function cleanCache(modulePath) { var module = require.cache[modulePath]; // remove reference in module.parent if (module.parent) { module.parent.children.splice(module.parent.children.indexOf(module), 1); } require.cache[modulePath] = null; } setInterval(function () { var code = require('./code.js'); cleanCache(require.resolve('./code.js')); }, 10);
再执行一下,这次好多了,内存只会有轻微的增长,说明老模块占用的资源已经正确的释放掉了。
使用了新的 cleanCache 函数后,常规的使用就没有问题,然而并非就可以高枕无忧了。在 Node.js 中,除了 require 系统会添加引用外,通过 EventEmitter 进行事件监听也是大家常用的功能,并且 EventEmitter 有非常大的嫌疑会出现模块间的互相引用。那么 EventEmitter 能否正确的释放资源呢?答案是肯定的。
// code.js var moduleA = require('events').EventEmitter(); moduleA.on('whatever', function () { });
Apabila modul code.js dikemas kini dan semua rujukan dialihkan keluar, modulA juga akan dikeluarkan secara automatik selagi ia tidak dirujuk oleh modul lain yang belum dikeluarkan, termasuk pendengar acara dalaman kami.
Hanya terdapat satu senario aplikasi EventEmitter yang cacat yang tidak boleh ditangani di bawah sistem ini, iaitu code.js akan mendengar peristiwa pada objek global setiap kali ia dilaksanakan, yang akan menyebabkan peristiwa pelekap berterusan pada objek global . Pada masa yang sama, Node.js akan segera menggesa bahawa terlalu banyak pengikatan peristiwa telah dikesan, yang mungkin merupakan kebocoran memori.
Pada ketika ini, anda dapat melihat bahawa selagi rujukan yang ditambahkan secara automatik oleh Node.js dalam sistem memerlukan diproses, kitar semula sumber modul lama bukanlah masalah besar, walaupun kami tidak dapat mencapai kemas kini panas seterusnya seperti Erlang . Modul lama yang tinggal tertakluk kepada kawalan terperinci seperti pengimbasan, tetapi kami boleh menyelesaikan masalah pelepasan sumber modul lama melalui kaedah pengelakan yang munasabah.
Dalam aplikasi web, masalah rujukan lain ialah modul yang belum dikeluarkan atau modul teras mempunyai rujukan kepada modul yang perlu dikemas kini secara hangat, seperti app.use Akibatnya, sumber modul lama tidak boleh dikeluarkan dan baharu permintaan tidak boleh diproses dengan betul. Gunakan modul baharu untuk pemprosesan. Penyelesaian kepada masalah ini adalah untuk mengawal entri terdedah pembolehubah global atau rujukan, dan mengemas kini secara manual entri semasa pelaksanaan kemas kini panas. Sebagai contoh, enkapsulasi penghala dalam Cara Menggunakan Modul Baharu untuk Memproses Permintaan adalah contoh Melalui kawalan entri ini, tidak kira bagaimana kita merujuk modul lain dalam router.js, ia akan dikeluarkan dengan keluaran entri. .
Masalah lain yang boleh menyebabkan keluaran sumber ialah operasi seperti setInterval, yang akan mengekalkan kitaran hayat objek daripada dikeluarkan Walau bagaimanapun, kami jarang menggunakan teknologi jenis ini dalam aplikasi web, jadi kami tidak memberi perhatian kepadanya rancangan itu.
Epilog
Setakat ini, kami telah menyelesaikan tiga masalah utama kemas kini panas kod Node.js dalam aplikasi web Walau bagaimanapun, kerana Node.js sendiri tidak mempunyai mekanisme pengimbasan yang berkesan untuk objek yang dikekalkan, ia tidak boleh 100% menghapuskan masalah yang disebabkan oleh setInterval. Sumber modul lama tidak boleh dikeluarkan. Ia juga disebabkan oleh pengehadan sedemikian yang dalam rangka kerja YOG2 yang kami sediakan pada masa ini, teknologi ini digunakan terutamanya dalam tempoh pembangunan dan penyahpepijatan untuk mencapai pembangunan pesat melalui kemas kini hangat. Kemas kini kod dalam persekitaran pengeluaran masih menggunakan fungsi restart atau hot reload PM2 untuk memastikan kestabilan perkhidmatan dalam talian.
Memandangkan kemas kini hangat sebenarnya berkait rapat dengan rangka kerja dan seni bina perniagaan, artikel ini tidak memberikan penyelesaian umum. Sebagai rujukan, mari kita perkenalkan secara ringkas cara kami menggunakan teknologi ini dalam rangka kerja YOG2. Memandangkan rangka kerja YOG2 itu sendiri menyokong pemisahan apl antara subsistem hadapan dan belakang, strategi kemas kini kami adalah untuk mengemas kini kod pada butiran apl. Pada masa yang sama, kerana operasi seperti fs.watch akan mempunyai masalah keserasian, dan beberapa alternatif seperti fs.watchFile akan menggunakan lebih banyak prestasi, jadi kami menggabungkan fungsi penggunaan mesin ujian YOG2 untuk memaklumkan rangka kerja bahawa ia perlu dikemas kini oleh memuat naik dan menggunakan kod apl baharu. Semasa mengemas kini cache modul pada butiran Apl, cache penghalaan dan cache templat akan dikemas kini untuk melengkapkan semua kemas kini kod.
Jika anda menggunakan rangka kerja seperti Express atau Koa, anda hanya perlu mengikut kaedah dalam artikel dan menggabungkan keperluan perniagaan anda sendiri dengan beberapa pengubahsuaian pada laluan utama, dan anda boleh menggunakan teknologi ini dengan baik.