> 웹 프론트엔드 > JS 튜토리얼 > Node.js는 웹 애플리케이션 code_node.js의 핫 업데이트를 영리하게 구현합니다.

Node.js는 웹 애플리케이션 code_node.js의 핫 업데이트를 영리하게 구현합니다.

WBOY
풀어 주다: 2016-05-16 15:35:40
원래의
1613명이 탐색했습니다.

배경

Node.js를 사용하여 웹 애플리케이션을 개발한 학생들은 Node.js 프로세스를 업데이트하기 전에 새로 수정된 코드를 다시 시작해야 하는 문제로 인해 어려움을 겪었을 것이라고 생각합니다. 개발을 위해 PHP를 사용하는 데 익숙한 학생들은 예상대로 PHP가 세계 최고의 프로그래밍 언어라는 것을 알게 될 것입니다. 프로세스를 수동으로 다시 시작하는 것은 매우 짜증나는 작업 중복일 뿐만 아니라, 애플리케이션 규모가 커지면 시작 시간이 점차 무시할 수 없게 되기 시작합니다.

물론 프로그래머로서 어떤 언어를 사용하더라도 그러한 것들이 당신을 괴롭히도록 놔두지는 않을 것입니다. 이러한 종류의 문제를 해결하는 가장 직접적이고 보편적인 방법은 파일 수정 사항을 모니터링하고 프로세스를 다시 시작하는 것입니다. 이 방법은 폐기된 node-supervisor, 현재 인기 있는 PM2 또는 상대적으로 가벼운 node-dev 등과 같은 많은 성숙한 솔루션에서도 제공되었으며 모두 이 아이디어를 기반으로 합니다.

이 기사에서는 약간의 수정만으로 진정한 제로 재시작 핫 업데이트 코드를 달성하고 Node.js로 웹 애플리케이션을 개발할 때 성가신 코드 업데이트 문제를 해결할 수 있다는 또 다른 아이디어를 제공합니다.

일반 아이디어

코드 핫 업데이트에 관해 현재 가장 유명한 것은 Erlang 언어의 핫 업데이트 기능입니다. 이 언어는 높은 동시성 및 분산 프로그래밍이 특징이며 주요 응용 시나리오는 증권 거래 및 게임 서버와 같은 분야입니다. . 이러한 시나리오에서는 서비스가 작동하는 동안 작동 및 유지 관리 수단을 갖추어야 하며 코드 핫 업데이트는 매우 중요한 부분이므로 먼저 Erlang의 접근 방식을 간략하게 살펴보겠습니다.

나는 Erlang을 사용해 본 적이 없기 때문에 다음 내용은 모두 소문입니다. Erlang의 코드 핫 업데이트 구현에 대해 심층적이고 정확한 이해를 원한다면 공식 문서를 참조하는 것이 가장 좋습니다.

Erlang의 코드 로딩은 code_server라는 모듈에 의해 관리됩니다. 시작 시 필요한 일부 코드를 제외하고 대부분의 코드는 code_server에 의해 로딩됩니다.
code_server는 모듈 코드가 업데이트되었음을 ​​발견하면 모듈을 다시 로드합니다. 이후의 새 요청은 새 모듈을 사용하여 실행되고, 아직 실행 중인 요청은 이전 모듈을 사용하여 계속 실행됩니다.
새 모듈이 로드된 후 이전 모듈에는 old라는 레이블이 붙고, 새 모듈에는 current라는 레이블이 붙습니다. 다음 핫 업데이트 동안 Erlang은 아직 실행 중인 이전 모듈을 검색하고 종료한 다음 이 논리에 따라 모듈을 계속 업데이트합니다.
Erlang의 모든 코드가 핫 업데이트를 허용하는 것은 아닙니다. 커널, stdlib, 컴파일러 및 기타 기본 모듈은 기본적으로 업데이트가 허용되지 않습니다.
Node.js에도 code_server와 유사한 모듈, 즉 require 시스템이 있다는 것을 알 수 있으므로 Erlang의 접근 방식은 Node.js에서도 시도해 보아야 할 것입니다. Erlang의 접근 방식을 이해함으로써 Node.js의 코드 핫 업데이트를 해결하는 데 있어 주요 문제를 대략적으로 요약할 수 있습니다

모듈 코드 업데이트 방법
새 모듈을 사용하여 요청을 처리하는 방법
오래된 모듈의 리소스를 해제하는 방법

그럼 문제점을 하나씩 분석해보겠습니다.

모듈 코드 업데이트 방법

모듈 코드 업데이트 문제를 해결하려면 Node.js의 모듈 관리자 구현을 읽고 module.js에 직접 연결해야 합니다. 간단히 읽어보면 Module._load에 핵심 코드가 있다는 것을 알 수 있습니다. 코드를 단순화해서 올려보겠습니다.

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

로그인 후 복사

핵심은 Module._cache라는 것을 알 수 있습니다. 이 모듈 캐시가 지워지는 한 모듈 관리자는 다음에 필요할 때 최신 코드를 다시 로드합니다.

이를 검증하기 위한 작은 프로그램을 작성하세요

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

로그인 후 복사

main.js를 실행하고 동시에 code.js의 내용을 수정해 보겠습니다. 콘솔에서 코드가 최신 코드로 성공적으로 업데이트된 것을 확인할 수 있습니다.

Node.js는 웹 애플리케이션 code_node.js의 핫 업데이트를 영리하게 구현합니다.

이제 모듈 관리자가 코드를 업데이트하는 문제는 해결되었습니다. 다음으로는 웹 애플리케이션에서 새 모듈이 실제로 실행되도록 하는 방법을 살펴보겠습니다.

새 모듈을 사용하여 요청을 처리하는 방법

모든 사람의 사용 습관에 더 부합하기 위해 Express를 직접 예로 들어 이 문제를 확장할 것입니다. 실제로 비슷한 아이디어를 사용하면 대부분의 웹 애플리케이션을 적용할 수 있습니다.

먼저 우리 서비스가 Express의 DEMO와 같고 모든 코드가 동일한 모듈에 있으면 모듈을 핫로드할 수 없습니다

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

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

app.listen(3000);

로그인 후 복사

Erlang에서 허용되지 않는 기본 라이브러리와 마찬가지로 핫 로딩을 달성하려면 업데이트 프로세스를 제어하기 위해 핫 업데이트할 수 없는 기본 코드가 필요합니다. 그리고 app.listen과 ​​같은 작업을 다시 실행하면 Node.js 프로세스를 다시 시작하는 것과 크게 다르지 않습니다. 따라서 자주 업데이트되는 비즈니스 코드와 자주 업데이트되지 않는 기본 코드를 분리하려면 영리한 코드가 필요합니다.

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

로그인 후 복사

code.js 모듈이 업데이트되고 모든 참조가 제거되면 내부 이벤트 리스너를 포함하여 출시되지 않은 다른 모듈에서 참조되지 않는 한 moduleA도 자동으로 출시됩니다.

이 시스템에서 처리할 수 없는 잘못된 EventEmitter 애플리케이션 시나리오는 단 하나뿐입니다. 즉, code.js는 실행될 때마다 전역 개체에서 이벤트를 수신하므로 전역 개체에서 지속적인 마운팅 이벤트가 발생합니다. 동시에 Node.js는 너무 많은 이벤트 바인딩이 감지되었으며 이는 메모리 누수일 수 있다는 메시지를 신속하게 표시합니다.

이 시점에서 Node.js가 require 시스템에 자동으로 추가한 참조만 처리된다면 Erlang과 같은 다음 핫 업데이트를 달성할 수는 없지만 이전 모듈의 리소스 재활용은 큰 문제가 되지 않는다는 것을 알 수 있습니다. 나머지 오래된 모듈은 스캐닝 등 세밀한 제어를 받지만, 합리적인 회피 방법을 통해 오래된 모듈의 리소스 해제 문제를 해결할 수 있습니다.

웹 애플리케이션에서 또 다른 참조 문제는 출시되지 않은 모듈이나 핵심 모듈에 app.use와 같이 핫 업데이트가 필요한 모듈에 대한 참조가 있다는 것입니다. 요청을 올바르게 처리할 수 없습니다. 처리를 위해 새 모듈을 사용하십시오. 이 문제에 대한 해결책은 노출된 전역 변수 또는 참조 항목을 제어하고 핫 업데이트 실행 중에 항목을 수동으로 업데이트하는 것입니다. 예를 들어, 새 모듈을 사용하여 요청을 처리하는 방법의 라우터 캡슐화는 이 항목의 제어를 통해 router.js의 다른 모듈을 어떻게 참조하더라도 해당 항목이 릴리스되면 릴리스됩니다. .

리소스 해제를 유발할 수 있는 또 다른 문제는 객체의 수명 주기가 해제되지 않도록 하는 setInterval과 같은 작업입니다. 그러나 웹 애플리케이션에서는 이러한 유형의 기술을 거의 사용하지 않으므로 주의를 기울이지 않습니다. 계획.

에필로그

지금까지 우리는 웹 애플리케이션에서 Node.js 코드 핫 업데이트의 세 가지 주요 문제를 해결했습니다. 그러나 Node.js 자체에는 보유된 객체에 대한 효과적인 검색 메커니즘이 부족하기 때문에 setInterval로 인해 발생하는 문제를 100% 제거할 수는 없습니다. 이전 모듈의 리소스는 해제할 수 없습니다. 우리가 현재 제공하는 YOG2 프레임워크에서 이 기술은 핫 업데이트를 통해 빠른 개발을 달성하기 위해 개발 및 디버깅 기간에 주로 사용되는 것도 이러한 제한 때문입니다. 프로덕션 환경의 코드 업데이트는 온라인 서비스의 안정성을 보장하기 위해 계속해서 다시 시작 또는 PM2의 핫 리로드 기능을 사용합니다.

핫 업데이트는 실제로 프레임워크 및 비즈니스 아키텍처와 밀접한 관련이 있으므로 이 문서에서는 일반적인 해결책을 제시하지 않습니다. 참고로 YOG2 프레임워크에서 이 기술을 어떻게 사용하는지 간략하게 소개하겠습니다. YOG2 프레임워크 자체는 프런트엔드와 백엔드 하위 시스템 간의 앱 분할을 지원하므로 업데이트 전략은 앱 세분성에서 코드를 업데이트하는 것입니다. 동시에 fs.watch와 같은 작업에는 호환성 문제가 있고 fs.watchFile과 같은 일부 대안은 더 많은 성능을 소비하므로 YOG2의 테스트 머신 배포 기능을 결합하여 프레임워크에 업데이트해야 함을 알립니다. 새로운 코드를 업로드하고 배포합니다. 앱 세분성에서 모듈 캐시를 업데이트하는 동안 라우팅 캐시와 템플릿 캐시가 업데이트되어 모든 코드 업데이트가 완료됩니다.

Express 또는 Koa와 같은 프레임워크를 사용하는 경우 기사에 있는 방법을 따르고 자신의 비즈니스 요구 사항을 기본 경로에 대한 일부 수정과 결합하기만 하면 이 기술을 잘 적용할 수 있습니다.

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿