Introduction a hot deployment scheme that allows you to make new updated code effective without restarting nodejs online service
background
As you all know, if there is code change in the backend service of nodejs, you need to restart the process before the code can take effect.
When the nodejs process is restarted, a short 502 bad gateway will appear when the user accesses the service
If your server has a watch mechanism
When the code on the server changes frequently, or changes frequently in a short time, it will always 502 bad gateway
In recent years, when we need to compile online services, there will be high-frequency changes in online service codes and high-frequency updates of code function modules in a short period of time. In the case that the service cannot be restarted, the updated code will take effect.
This involves the concept of a hot deployment, which allows the newly deployed code to take effect without restarting the service.
Next, I will explain the principle and implementation scheme of hot deployment
Why the code can't take effect in real time
When we load a function module through require('xx/xx.js'), the node will cache the result of require('xx/xx.js') in require.cache('xx/xx.js')
When we call require('xx/xx.js') several times, the node will not reload, but directly read the cache from require.cache('xx/xx.js')
So when a small partner modifies a file under the path xx/xx.js on the server, the node will only read the cache and not load the latest code of the small partner
Source address and use
In order to realize this hot deployment mechanism, we searched the data everywhere on the Internet and stepped on many holes to finish it
The following code is extracted, complete and runnable basic principle code of hot deployment, which you can develop on your own: smart node reload
Note that the latest version of node 12 will report an error. The official has adjusted the require.cache and reported the problem to the official. It is recommended to use the nodejs version: v10.5.0
After git clone comes down, it doesn't need to be installed and runs directly
npm start
At this time, hot deployment change monitoring is enabled
How to see the effect
Look at the / hots/hot.js file
const hot = 1 module.exports = hot
Change the first line of code to const hot = 111
const hot = 111 module.exports = hot
At this time, you can see that the terminal listens for code changes, and then dynamically loads your latest code and gets the execution result. The output is:
Hot deployment file: hot.js, execution result: {'hot.js': 111}
The hot deployment service listens for code changes and reloads the code, so that small partners can get the execution results of the latest code in real time. The whole process runs on the online environment, and the node process is not restarted
Source code analysis
loadHandlers main function
const handlerMap = {};// cache const hotsPath = path.join(__dirname, "hots"); // Load the file code and listen for changes in the contents of the specified folder directory file const loadHandlers = async () => { // Traverse all files under the specified folder const files = await new Promise((resolve, reject) => { fs.readdir(hotsPath, (err, files) => { if (err) { reject(err); } else { resolve(files); } }); }); // Initialize loading all files and cache the results of each file into the handlerMap variable for (let f in files) { handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f])); } // Listen for file content changes in the specified folder await watchHandlers(); };
loadHandlers is the main function of the whole hot deployment service. We have specified that the hots folder in the root directory of the server is used to monitor changes and hot deployment
Use fs.readdir to scan all files in the hots folder, load and run each scanned file through the loadHandler method, and cache the results in the handlerMap
Then use the watchHandlers method to enable file change monitoring
watchHandlers listen for file changes
// Monitor file changes under specified folders const watchHandlers = async () => { // It is recommended to use chokidar's npm package instead of folder monitoring fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => { // Get the absolute path to each file // The reason for require.resolve in package 1. After the path is spliced, it will take the initiative to help you determine whether the file under this path exists const targetFile = require.resolve(path.join(hotsPath, filename)); // When you adapt to require to load a module, the data of the module will be cached in require.cache. Next time you load the same module, you will directly go to require.cache // Therefore, the first thing we do in hot load deployment is to clear the cache of the corresponding file in require.cache const cacheModule = require.cache[targetFile]; // Remove the reference of parent to the current module in the require.cache cache, otherwise memory leakage will occur. For details, see the following article //Record the "murder" caused by one line of code at a time https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63 //The murder case of memory leak caused by delete request.cache https://zhanglan.zhihu.com/p/34702356 if (cacheModule.parent) { cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1); } // Clear the require.cache cache of the module corresponding to the specified path require.cache[targetFile] = null; // Reload the changed module file to achieve the hot load deployment effect, and update the reloaded result to the handlerMap variable const code = await loadHandler(targetFile) handlerMap[filename] = code; console.log("Hot deployment file:", filename, ",Execution result:", handlerMap); }); };
The watchHandlers function is used to listen for file changes under the specified folder, and clean up the cache and update the cache.
Use the fs.watch native function to monitor the changes of the files under the hots folder. When the files change, the absolute path targetFile will be calculated
And require.cache[targetFile] is the cache of the original targetFile by require. To clear the cache, use require.cache[targetFile] = null;
Here comes the place of kengdao. Just setting the cache to null will cause memory leak. We also need to clear the reference require.cache[targetFile].parent of the cache parent, which is the following code
if (cacheModule.parent) { cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1); }
loadHandler load file
// Load code for specified file const loadHandler = filename => { return new Promise((resolve, reject) => { fs.readFile(filename, (err, data) => { if (err) { resolve(null); } else { try { // The Script method of vm module is used to precompile the changed file code, check the syntax errors, and find out in advance whether there are syntax errors and other errors new vm.Script(data); } catch (e) { // Syntax error, compilation failed reject(e); return; } // After the compilation passes, re require to load the latest code resolve(require(filename)); } }); }); };
The loadHandler function is used to load the specified file and verify the new file code syntax.
Read the contents of the file through fs.readFile
Use vm.Script, the node native VM module, to precompile the changed file code, check the syntax errors, and find out in advance whether there are syntax errors and other errors
After passing the verification, reload the file require through the resolve(require(filename)) method, and automatically add it to the require.cache cache
Ending:
That's all about hot deployment. The code address is: smart node reload
This code is my minimalist code, which is convenient for everyone to read and understand. Interested partners can further expand through this code
All of the above are my own thoughts. Please share them and ask for your attention