Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Giới thiệu

Cách đây vài tháng, tôi gặp phải lỗi “Memory Leak - thất thoát bộ nhớ” trong Node.js. Tôi đã tìm thấy một vài tài liệu có liên quan đến vấn đề này. Nhưng sau khi đọc một cách tỉ mỉ xong, tôi vẫn cảm thấy bối rối, không biết thực sự nên làm gì để debug được lỗi này.

Trong bài này tôi muốn hướng dẫn các bạn phát hiện các vấn đề về “Memory Leak”  trong Node.js. Tôi sẽ phác thảo một cách đơn giản các tiếp cận, theo ý kiến của tôi thì đó sẽ là điểm khởi đầu  để debug của bất kỳ vấn đề “Memory Leak” trong Node.js. Đối với một số trường hợp thì cách tiếp cận này sẽ là không đủ nên tôi sẽ link đến một vài nguồn khác mà bạn có thể xem.

Học thuyết tối thiểu

JavaScript là một ngôn ngữ “garbage collected” (tự động dọn dẹp biến nếu biến không còn cần thiết nữa). Vì vậy, tất cả các bộ nhớ được sử dụng bởi một quá trình Node đang được tự động phân bổ và thu hồi 4 bởi engine  JavaScript V8.

Khi nào thì Javascript V8 sẽ thu hồi bộ nhớ đã cấp phát? Cơ chế này chứa một đồ thị các biến của chương trình, bắt đầu từ nút gốc. Có 4 kiểu dữ liệu trong JavaScript : Boolean, String, Number, Object.  Với 3 kiểu dữ liệu đầu tiên, hệ thống sẽ lưu trữ các giá trị được gán cho biến đó. Còn đối với kiểu Object, và các thức khác trong JS có kiểu là Object ( vd : Type of Array = object ), thì hệ thống sẽ lưu trữ con trỏ tới object đóBoolean, String, Number, Object.  Với 3 kiểu dữ liệu đầu tiên, hệ thống sẽ lưu trữ các giá trị được gán cho biến đó. Còn đối với kiểu Object, và các thức khác trong JS có kiểu là Object ( vd : Type of Array = object ), thì hệ thống sẽ lưu trữ con trỏ tới object đó

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Engine V8 Javascript sẽ duyệt trong Memory Graph (một đồ thị gồm nhiều nút, đường nối giữa các nút là mối liên hệ tham chiếu hoặc sử dụng nhau) , sẽ cố xác định những nhóm dữ liệu mà nút gốc không thể tìm đến. Nếu gặp trường hợp này thì, V8 sẽ giả định rằng dữ liệu này sẽ không còn được sử dụng nữa và sẽ thu hồi bộ nhớ. Quá trình này gọi là Garbage Collected (dọn rác)Garbage Collected (dọn rác)

Khi nào gặp phải trường hợp “Memory Leak”?

“Memory Leak” trong NodeJs gặp phải khi mà có một dữ liệu không cần thiết mà nút gốc vẫn có thể tìm tới  được . V8 sẽ cho rằng dữ liệu này vẫn đang được sử dụng và sẽ không giải phóng bộ nhớ. Để debug được lỗi này thì chúng ta cần phải xác định được dữ liệu  bị lưu giữ do nhầm lẫn, và chắn chắn rằng V8 có thể dọn sạch nó.

Cũng phải nói rằng Garbage Collection không phải lúc nào cũng chạy. Thông thường V8 chỉ cho GC chạy khi thích hợp. Ví dụ như, GC có thể chạy định kỳ hoặc nó có thể kích hoạt khi cảm nhận thấy bộ nhớ còn lại đang xuống thấp. Mỗi node sẽ có một không gian bộ nhớ nhất định trong mỗi tiến trình, vì vậy V8 sẽ sử dụng chúng một cách khôn ngoan nhất.

Các trường hợp sau này khi mà hết số lượt sử dụng GC , có thể là dấu hiệu cho thấy hệ thống đang giảm hiệu suất một cách đáng kể.

Ví dụ bạn có một ứng dụng bị tràn bộ nhớ nhiều lần. Tiến trình bị hết bộ nhớ, và số lần kích hoạt GC đã hết. Nhưng vẫn còn nhiều dữ liệu được tìm tới bởi nút gốc, chỉ một phần nhỏ dữ liệu được dọn về cùng một chỗ.

Và có thể ngay sau đó, các tiến trình có thể bị “out-of-memory”  lần nữa, và GC lại sẽ bị tiếp tục kích hoạt. Trước khi bạn phát hiện ra, thì app của bạn sẽ đi vào một vòng lặp như trên, chỉ là cố gắng giữ cho các tiến trình hoạt động, trong khi V8 phải dành phần lớn thời gian để xử lý GC, còn rất ít tài nguyên dành cho chương trình thực sự

Bước 1 : Ghi lại và xác nhận các vấn đề gặp phải

Như đã nói ở trên, V8 có một logic vô cùng phức tập mà nó sử dụng để quyết định khi nào thì GC sẽ được kích hoạt.  Vì vậy, mặc dù có thể chúng ta có thể thấy được rằng bộ nhớ của tiến trình chiếm ngày càng tăng, chúng ta không thể chắc chắn rằng hệ thống bị tràn bộ nhớ hay là chương trình đang chạy đúng, cho tơi khi GC được khởi chạy.

Thật là may, Node cho phép chúng ta có thể tự kích hoạt GC, và đó có thể là điều chúng ta có thể làm để cố gắng xác nhận là đang gặp phải trường hợp tràn bộ nhớ. Điều này có thể được thực hiện bằng cách thêm lựa chọn –expose-gc. Một khi tiến trình đã được khởi chạy với option –expose-gc thì chúng ta có thể gọi GC bằng cách gọi hàm global.gc().–expose-gc. Một khi tiến trình đã được khởi chạy với option –expose-gc thì chúng ta có thể gọi GC bằng cách gọi hàm global.gc().

Bạn cũng có thể kiểm tra lượng bộ nhớ mà tiến trình của bạn chiếm bằng cách gọi process.memoryUsage().heapUsed.

Ví dụ

"use strict";
require('heapdump');
 
var leakyData = [];
var nonLeakyData = [];
 
class SimpleClass {
  constructor(text){
    this.text = text;
  }
}
 
function cleanUpData(dataStore, randomObject){
  var objectIndex = dataStore.indexOf(randomObject);
  dataStore.splice(objectIndex, 1);
}
 
function getAndStoreRandomData(){
  var randomData = Math.random().toString();
  var randomObject = new SimpleClass(randomData);
 
  leakyData.push(randomObject);
  nonLeakyData.push(randomObject);
 
  // cleanUpData(leakyData, randomObject); //<-- Forgot to clean up
  cleanUpData(nonLeakyData, randomObject);
}
 
function generateHeapDumpAndStats(){
  //1. Force garbage collection every time this function is called
  try {
    global.gc();
  } catch (e) {
    console.log("You must run program with 'node --expose-gc index.js' or 'npm start'");
    process.exit();
  }
 
  //2. Output Heap stats
  var heapUsed = process.memoryUsage().heapUsed;
  console.log("Program is using " + heapUsed + " bytes of Heap.")
 
  //3. Get Heap dump
  process.kill(process.pid, 'SIGUSR2');
}
 
//Kick off the program
setInterval(getAndStoreRandomData, 5); //Add random data every 5 milliseconds
setInterval(generateHeapDumpAndStats, 2000); //Do garbage collection and heap dump every 2 seconds
 

setInterval(getAndStoreRandomData, 5); //Add random data every 5 millisecondsDo garbage collection and heap dump every 2 seconds

  • Cứ 5ms chương trình sẽ sinh ra một đối tượng và lưu nó vào 2 mảng, mảng leakyData và nonLeakyData. Giả sử cứ 5ms chúng ta sẽ thu dọn những Object đã cấp  tại mảng nonLeakyData nhưng lại “quên” thu dọn Object của mảng “leakyData”.
  • Cứ 2s chúng ta sẽ show ra lượng bộ nhớ được sử dụng

Nếu bạn chạy chương trình với option –expose-gc hoặc npm-start. Chương trình sẽ đưa ra  thống kê về bộ nhớ được sử dụng.–expose-gc hoặc npm-start. Chương trình sẽ đưa ra  thống kê về bộ nhớ được sử dụng.

Bạn sẽ thấy lượng bộ nhớ được sử dụng sẽ ngày càng kể cả khi đã kích hoạt GC 2 giây một lần ngay sau khi có được thống kê về bộ nhớ :

//1. Force garbage collection every time this function is called
try {
  global.gc();
} catch (e) {
  console.log("You must run program with 'node --expose-gc index.js' or 'npm start'");
  process.exit();
}
 
//2. Output Heap stats
var heapUsed = process.memoryUsage().heapUsed;
console.log("Program is using " + heapUsed + " bytes of Heap.")

Kết quả xuất ra có dạng như sau :

Program is using 3783656 bytes of Heap.
Program is using 3919520 bytes of Heap.
Program is using 3849976 bytes of Heap.
Program is using 3881480 bytes of Heap.
Program is using 3907608 bytes of Heap.
Program is using 3941752 bytes of Heap.
Program is using 3968136 bytes of Heap.
Program is using 3994504 bytes of Heap.
Program is using 4032400 bytes of Heap.
Program is using 4058464 bytes of Heap.
Program is using 4084656 bytes of Heap.
Program is using 4111128 bytes of Heap.
Program is using 4137336 bytes of Heap.
Program is using 4181240 bytes of Heap.
Program is using 4207304 bytes of Heap.

Nếu bạn đưa dữ liệu và một đồ thị, bạn sẽ thấy lượng bộ nhớ sẽ được sử dụng sẽ tăng một cách rõ rệt

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Tôi đã chuyển dữ liệu output thành kiểu JSON, rồi sau đó biểu thị nó với một vài dòng code Python. 

Đoạn code đó được biểu thị dưới đây

var fs = require('fs');
var stats = [];
 
//--- skip ---
 
var heapUsed = process.memoryUsage().heapUsed;
stats.push(heapUsed);
 
//--- skip ---
 
//On ctrl+c save the stats and exit
process.on('SIGINT', function(){
  var data = JSON.stringify(stats);
  fs.writeFile("stats.json", data, function(err) {
    if(err) {
      console.log(err);
    } else {
      console.log("\nSaved stats to stats.json");
    }
    process.exit();
  });
});
 

và 

#!/usr/bin/env python
 
import matplotlib.pyplot as plt
import json
 
statsFile = open('stats.json', 'r')
heapSizes = json.load(statsFile)
 
print('Plotting %s' % ', '.join(map(str, heapSizes)))
 
plt.plot(heapSizes)
plt.ylabel('Heap Size')
plt.show()

Bạn có thể biểu thị dữ liệu bằng python hoặc bằng excel ( Nếu sử dụng python thì bạn cần sử dụng thư viện Matplotlib.

Bước 2 : Lấy ít nhất 3 snapshot dữ liệu  

Sau khi đã tự tạo lại được bug đó, giờ là lúc tìm hiểu, xác định lỗi là ở đâu và fix chúng.

Tôi sử dụng module heapdump của Node.js để tìm ra lỗi.heapdump của Node.js để tìm ra lỗi.

Để sử dụng heapdump, bạn cần : 

  1. Cài đặt nó
  2. Require("heapdump") 
  3. Kill tiến trình theo platform  kill -USR2 {{pid}}
require('heapdump');
// ---skip---
 
//3. Get Heap dump
process.kill(process.pid, 'SIGUSR2');
 
// ---skip---
 

Module "heapdump"  được cấu hình để lấy những thống kê của tiến trình, bất cứ khi nào nó nhận đươc "user signal two" thông qua option -USR2."heapdump"  được cấu hình để lấy những thống kê của tiến trình, bất cứ khi nào nó nhận đươc "user signal two" thông qua option -USR2.

Trong chương trình này, tôi sẽ tự động kill process bằng cách chạy lệnh kill -USR2 {{pid}}  trong Linux bằng đoạn code trên, process.pid cho chúng ta biến được id của tiến trình đang chạy.

Trên Window bạn có thể sử dụng  

//1. Force garbage collection every time this function is called
try {
  global.gc();
} catch (e) {
  console.log("You must run program with 'node --expose-gc index.js' or 'npm start'");
  process.exit();
}
 
//2. Output Heap stats
var heapUsed = process.memoryUsage().heapUsed;
console.log("Program is using " + heapUsed + " bytes of Heap.")
1  thay vì sử dụng 
//1. Force garbage collection every time this function is called
try {
  global.gc();
} catch (e) {
  console.log("You must run program with 'node --expose-gc index.js' or 'npm start'");
  process.exit();
}
 
//2. Output Heap stats
var heapUsed = process.memoryUsage().heapUsed;
console.log("Program is using " + heapUsed + " bytes of Heap.")

Bước 3 : Tìm kiếm vấn đề

Ở bước 2, chúng ta đã sinh ra một loạt các heap dump (danh sách các phần tử trong vùng nhớ heap) tại các thời điểm nhất định, nhưng chúng ta cần ít nhất là 3 heap dump.

Đầu tiên vào Google Chrome, mở Chrome Developer tools, chọn Tab Profile, bấm vào Load rồi sau đó chọn heap dump đầu tiên bạn nhận được.

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Load cả 3 heap dump bạn nhận được theo thứ tự. Tab Profile của bạn sẽ trông như thế này

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Như tôi đã nói ở trên, các heap dump sẽ càng ngày càng chiếm bộ nhớ.

Phương pháp 3 Heap Dumps

Bạn click vào heap dump cuối cùng bạn load vào Tab Profile của Chrome Developer Tools. Nó sẽ tự động đưa bạn đến trang "Summary", sẽ có một drop-down có nhãn là "All" . Click vào "All" và chọn "Objects allocated between heapdump-YOUR-FIRST-HEAP-DUMP and heapdump-YOUR-SECOND-TO-LAST-HEAP-DUMP", nó sẽ trông như sau :

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

 

Nó cho thấy tất cả objects được cung cấp ở giữa heap dump đầu tiên và thứ 2, cho tới head dump cuối cùng. Thực tế là những objects kia vẫn còn đó trong lần heap dump cuối cùng. Chúng cần được để mắt đến và kiểm tra, bởi chúng đáng lẽ ra phải được dọn bởi Garbage Collection.

Ít nhất là bỏ qua các thành phần nằm trong dấu ngoặc ( string )

Ghi nhớ rằng cột "Shallow Size"  đại diện cho kích cỡ của object đó, còn "Retained Size"  đại diện cho kích cỡ của object đó và con của nóShallow Size"  đại diện cho kích cỡ của object đó, còn "Retained Size"  đại diện cho kích cỡ của object đó và con của nó

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Có 5 kiểu được lưu trữ trong lần snapshot mà đáng lẽ ra nó không nên ở đây : 

  (array), (compiled code), (string), (system), và SimpleClass.

  Và có lẽ SimpleClass trông có vẻ quen thuộc trong app.SimpleClass trông có vẻ quen thuộc trong app.

var randomObject = new SimpleClass(randomData);

Tất cả những objects trong Summary view đều được nhóm vào dưới tên khởi tạo. Trong trường hợp của array và string, chúng là những yếu tố khởi tạo bên trong đối với engine của Javascript. Trong khi chương trình của anh chắc chắn đang dựa vào 1 bộ nhớ (data) nào đó, được tạo ra dựa trên những thành phần khơi tạo kia (constructors), và bạn sẽ vướng phải nhiều rắc rối tại đây, khiến mọi thứ trở nên khó hơn khi muốn tìm ra căn nguyên của việc rò rỉ  bộ nhớ. Đấy là lí do ta nên bỏ qua việc soi vào array lẫn string trong bước đầu tiên, thay vào đó là có thể tìm ra những đối tượng "khả nghi" rõ ràng, như những constructor SimpleClass trong những app minh hoạ. Click vào biểu tượng mũi tên drop down trong thành phần khởi tạo SimpleClass (constructor), và chọn lấy bất kì object trong danh sách kết quả, sẽ khiến ''con đường" (path) lưu trữ dữ liệu ở phần phía dưới màn hình bị đầy (fill). Từ đó sẽ rất dễ để phát hiện ra những mảng dữ liệu bị rò gỉ đang cố bám vào dữ liệu. Nếu bạn không được may mắn như tôi khi sử dụng ứng dụng, bạn có thể nên nhìn thêm vào những thành phần khởi tạo bên trong ( như là chuỗi) và cố tìm hiểu xem điều gì đang khiến cho bộ nhớ bị rò gỉ. Trong trường hợp đó, có 1 mẹo có thể áp dụng, đó là nhận diện những nhóm giá trị (values) thỉnh thoảng hay xuất hiện trong những nhóm thành phần khởi tạo (constructor), và cố biến nó trở thành gợi ý trong con đường tìm ra sự rò gỉ.

Ví dụ, trong trường hợp ứng dụng mô phỏng, bạn có thể quan sát thấy rất nhiều chuỗi trông có vẻ như những con số bất kì được chuyển đổi thành chuỗi. Nếu bạn kiểu tra lại retained size của chúng, Chrome dev tools sẽ chỉ ra cho bạn ngay mảng dữ liệu rò gỉ

Bước 4 : Chắc chắn rằng bug đó đã được fix

Sau khi bạn đã tìm ra và giải quyết được vấn đề đó, bạn sẽ thấy một sự khác biệt rất lớn trong việc sử dụng heap.

Nếu như bạn rào lại lệnh này

cleanUpData(leakyData, randomObject); //<-- Forgot to clean up

Rồi sau đó re-run ứng dụng của bạn, thì đây sẽ là kết quả : 

Program is using 3756664 bytes of Heap.
Program is using 3862504 bytes of Heap.
Program is using 3763208 bytes of Heap.
Program is using 3763400 bytes of Heap.
Program is using 3763424 bytes of Heap.
Program is using 3763448 bytes of Heap.
Program is using 3763472 bytes of Heap.
Program is using 3763496 bytes of Heap.
Program is using 3763784 bytes of Heap.
Program is using 3763808 bytes of Heap.
Program is using 3763832 bytes of Heap.
Program is using 3758368 bytes of Heap.
Program is using 3758368 bytes of Heap.
Program is using 3758368 bytes of Heap.
Program is using 3758368 bytes of Heap.
 

Và nếu chúng ta biểu diễn dữ liệu đó dưới dạng đồ thị

Hướng dẫn wordpress memory leak - rò rỉ bộ nhớ wordpress

Vậy là vấn đề đã được giải quyết

Tổng kết:  

  1. Kích hoạt Garbage Collection khi tạo lại và xác định rò rỉ dữ liệu. Có thể chạy node cùng option --expose-gc và gọi hàm global.gc()
  2. Lấy ít nhất 3 heap dump, có thể sử dụng https://github.com/bnoordhuis/node-heapdump
  3. Sử dụng 3 head dump đó để phân tách lỗi rò rỉ dữ liệu
  4. Xử lý và xác định rằng lỗi đã không còn nữa

Bài dịch từ trang : http://www.alexkras.com/simple-guide-to-finding-a-javascript-memory-leak-in-node-js/