Maison  >  Article  >  interface Web  >  Node.js implémente intelligemment la mise à jour à chaud de l'application Web code_node.js

Node.js implémente intelligemment la mise à jour à chaud de l'application Web code_node.js

WBOY
WBOYoriginal
2016-05-16 15:35:401538parcourir

Contexte

Je pense que les étudiants qui ont développé des applications Web à l'aide de Node.js ont dû être troublés par le problème selon lequel le code nouvellement modifié doit être redémarré avant que le processus Node.js puisse être mis à jour. Les étudiants habitués à utiliser PHP pour le développement le trouveront très inapplicable. Comme prévu, PHP est le meilleur langage de programmation au monde. Le redémarrage manuel du processus n'est pas seulement une duplication de travail très ennuyeuse, mais lorsque l'échelle de l'application devient plus grande, le temps de démarrage commence progressivement à devenir incontournable.

Bien sûr, en tant que programmeur, quel que soit le langage que vous utilisez, vous ne laisserez pas de telles choses vous torturer. La manière la plus directe et la plus universelle de résoudre ce type de problème consiste à surveiller les modifications des fichiers et à redémarrer le processus. Cette méthode a également été fournie par de nombreuses solutions matures, telles que le node-supervisor abandonné, le désormais populaire PM2, ou le node-dev relativement léger, etc., qui sont toutes basées sur cette idée.

Cet article fournit une autre idée. Avec seulement une petite modification, vous pouvez obtenir un véritable code de mise à jour à chaud sans redémarrage et résoudre le problème ennuyeux de mise à jour du code lors du développement d'applications Web avec Node.js.

Idée générale

En parlant de mise à jour à chaud du code, la plus connue à l'heure actuelle est la fonction de mise à jour à chaud du langage Erlang. Ce langage se caractérise par une forte concurrence et une programmation distribuée. Ses principaux scénarios d'application sont des domaines tels que le trading de titres et les serveurs de jeux. . Ces scénarios nécessitent plus ou moins que les services disposent de moyens d'exploitation et de maintenance pendant le fonctionnement, et la mise à jour à chaud du code en est une partie très importante, nous pouvons donc d'abord jeter un bref coup d'œil à l'approche d'Erlang.

Comme je n'ai jamais utilisé Erlang, le contenu suivant n'est que du ouï-dire. Si vous souhaitez avoir une compréhension approfondie et précise de l'implémentation de la mise à jour à chaud du code d'Erlang, il est préférable de consulter la documentation officielle.

Le chargement du code d'Erlang est géré par un module appelé code_server À l'exception de certains codes nécessaires au démarrage, la majeure partie du code est chargée par code_server.
Lorsque code_server constate que le code du module a été mis à jour, il rechargera le module par la suite. Les nouvelles requêtes seront ensuite exécutées en utilisant le nouveau module, tandis que les requêtes en cours d'exécution continueront d'être exécutées en utilisant l'ancien module.
L'ancien module sera étiqueté ancien après le chargement du nouveau module, et le nouveau module sera étiqueté actuel. Lors de la prochaine mise à jour à chaud, Erlang analysera et tuera les anciens modules en cours d'exécution, puis continuera à mettre à jour les modules selon cette logique.
Tous les codes dans Erlang n'autorisent pas les mises à jour à chaud. Les modules de base tels que le noyau, stdlib, le compilateur et d'autres modules de base ne sont pas autorisés à être mis à jour par défaut
. Nous pouvons constater que Node.js possède également un module similaire à code_server, c'est-à-dire le système require, donc l'approche d'Erlang devrait également être essayée sur Node.js. En comprenant l'approche d'Erlang, nous pouvons résumer grossièrement les problèmes clés liés à la résolution des mises à jour à chaud du code dans Node.js

Comment mettre à jour le code du module
Comment gérer les demandes à l'aide du nouveau module
Comment libérer les ressources des anciens modules

Alors analysons ces points problématiques un par un.

Comment mettre à jour le code du module

Pour résoudre le problème de la mise à jour du code du module, nous devons lire l'implémentation du gestionnaire de modules de Node.js et créer un lien direct vers module.js. Grâce à une simple lecture, nous pouvons constater que le code principal se trouve dans Module._load. Simplifions le code et publions-le.

// 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;

Vous pouvez constater que le cœur de celui-ci est Module._cache. Tant que ce cache de module est vidé, le gestionnaire de modules rechargera le dernier code la prochaine fois qu'il sera requis.

Écrivez un petit programme pour le vérifier

// 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';

Exécutons main.js et modifions le contenu de code.js en même temps. Nous pouvons constater que dans la console, notre code a été mis à jour avec succès vers le dernier code.

Node.js implémente intelligemment la mise à jour à chaud de lapplication Web code_node.js

Le problème de la mise à jour du code par le gestionnaire de modules a donc été résolu. Voyons ensuite comment nous pouvons faire en sorte que le nouveau module soit réellement exécuté dans l'application Web.

Comment utiliser le nouveau module pour traiter les demandes

Afin d'être plus en phase avec les habitudes d'utilisation de chacun, nous utiliserons directement Express comme exemple pour développer ce problème. En fait, en utilisant une idée similaire, la plupart des applications Web peuvent être appliquées.

Tout d'abord, si notre service ressemble à la DEMO d'Express et que tout le code est dans le même module, nous ne pouvons pas charger le module à chaud

var express = require('express');
var app = express();

app.get('/', function(req, res){
 res.send('hello world');
});

app.listen(3000);

Pour réaliser un chargement à chaud, tout comme les bibliothèques de base non autorisées dans Erlang, nous avons besoin d'un code de base qui ne peut pas être mis à jour à chaud pour contrôler le processus de mise à jour. Et si une opération comme app.listen est exécutée à nouveau, ce ne sera pas très différent du redémarrage du processus Node.js. Par conséquent, nous avons besoin d’un code intelligent pour isoler le code métier fréquemment mis à jour du code de base rarement mis à jour.

// 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 () {
});

Lorsque le module code.js est mis à jour et que toutes les références sont supprimées, le moduleA sera également automatiquement publié tant qu'il n'est pas référencé par d'autres modules inédits, y compris nos écouteurs d'événements internes.

Il n'existe qu'un seul scénario d'application EventEmitter mal formé qui ne peut pas être traité avec ce système, c'est-à-dire que code.js écoutera les événements sur un objet global à chaque fois qu'il est exécuté, ce qui entraînera un montage constant d'événements sur l'objet global. . Dans le même temps, Node.js indiquera rapidement que trop de liaisons d'événements ont été détectées, ce qui peut être une fuite de mémoire.

À ce stade, vous pouvez voir que tant que les références automatiquement ajoutées par Node.js dans le système require sont traitées, le recyclage des ressources des anciens modules n'est pas un gros problème, même si nous ne pouvons pas réaliser la prochaine mise à jour à chaud comme Erlang Les anciens modules restants sont soumis à un contrôle précis tel que l'analyse, mais nous pouvons résoudre le problème de la libération des ressources des anciens modules grâce à des méthodes d'évitement raisonnables.

Dans les applications Web, un autre problème de référence est que les modules non publiés ou les modules de base ont des références à des modules qui doivent être mis à jour à chaud, comme app.use. Par conséquent, les ressources de l'ancien module ne peuvent pas être publiées, et les nouvelles. les demandes ne peuvent pas être traitées correctement. Utilisez de nouveaux modules pour le traitement. La solution à ce problème consiste à contrôler les entrées exposées des variables globales ou des références et à mettre à jour manuellement les entrées lors de l'exécution de mises à jour à chaud. Par exemple, l'encapsulation du routeur dans Comment utiliser un nouveau module pour traiter les demandes est un exemple Grâce au contrôle de cette entrée, quelle que soit la manière dont nous référençons les autres modules dans router.js, ils seront publiés avec la publication de l'entrée. .

Un autre problème qui peut provoquer la libération de ressources concerne les opérations telles que setInterval, qui empêcheront la libération du cycle de vie de l'objet. Cependant, nous utilisons rarement ce type de technologie dans les applications Web, nous n'y prêtons donc pas attention. le plan.

Épilogue

Jusqu'à présent, nous avons résolu les trois problèmes majeurs de la mise à jour à chaud du code Node.js dans les applications Web. Cependant, comme Node.js lui-même ne dispose pas d'un mécanisme d'analyse efficace pour les objets conservés, il ne peut pas éliminer à 100 % les problèmes causés par setInterval. La ressource de l'ancien module ne peut pas être libérée. C'est également à cause de ces limitations que dans le framework YOG2 que nous proposons actuellement, cette technologie est principalement utilisée dans la période de développement et de débogage pour obtenir un développement rapide grâce à des mises à jour à chaud. Les mises à jour de code dans l'environnement de production utilisent toujours la fonction de redémarrage ou de rechargement à chaud de PM2 pour garantir la stabilité des services en ligne.

Étant donné que la mise à jour à chaud est en fait étroitement liée au framework et à l'architecture métier, cet article ne donne pas de solution générale. Pour référence, présentons brièvement comment nous utilisons cette technologie dans le cadre des JOJ2. Étant donné que le framework YOG2 lui-même prend en charge la répartition des applications entre les sous-systèmes front-end et back-end, notre stratégie de mise à jour consiste à mettre à jour le code au niveau de la granularité de l'application. Dans le même temps, étant donné que des opérations telles que fs.watch auront des problèmes de compatibilité et que certaines alternatives telles que fs.watchFile consommeront plus de performances, nous avons donc combiné la fonction de déploiement de machine de test de YOG2 pour informer le framework qu'il doit être mis à jour par télécharger et déployer un nouveau code d'application. Lors de la mise à jour du cache de module au niveau de la granularité de l'application, le cache de routage et le cache de modèles seront mis à jour pour terminer toutes les mises à jour de code.

Si vous utilisez un framework comme Express ou Koa, il vous suffit de suivre les méthodes décrites dans l'article et de combiner vos propres besoins commerciaux avec quelques modifications de l'itinéraire principal, et vous pouvez bien appliquer cette technologie.

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn