Bộ nhớ đống javascript

Trong bài viết trước, chúng ta đã trải qua các trường hợp làm ảnh hưởng trực tiếp đến hiệu năng, các trường hợp khiến EventLoop trở thành viên ngọc thông chưa bao giờ được tỏa sáng. Nếu bạn chưa xem qua phần đầu tiên, có thể quay lại bài viết NodeJS có thực sự nhanh như bạn nghĩ để tránh bị chặn EventLoop sẽ nguy hiểm như thế nào nhé

Bộ nhớ đống javascript
Bộ nhớ đống javascript

Quay lại chủ đề hôm nay, đó là câu chuyện giữa Anh chàng NodeJS và Cô nàng V8. Wow, nghe có vẻ ông tác này như mới yêu đấy nhỉ 😇. Không đâu, hãy tin tôi đi với những phép tu từ trong ngữ pháp tiếng việt này sẽ giúp bạn hiểu sâu hơn và nhớ lâu hơn với những khái niệm mang tính chất hàn lâm như các bạn đang gặp phải

Ăn quả ghi nhớ cây trồng

NodeJS vốn được biết đến là Nền tảng và được điều hành bởi V8 Javascript (được viết bằng C/C++). Và công việc quản lý và cấp phát vùng nhớ cho các ứng dụng sẽ được đảm nhiệm bởi V8 (thực ra ông này cũng tận dụng mô hình tương đồng Java Virtual Machine-JVM)

Điều này đồng nghĩa với việc Công Phượng thi đấu dưới dạng cho mượn ở CLB TPHCM, nhưng mọi sự quản lý và phân bổ từ cầu thủ xứ Nghệ vẫn được đảm nhận bởi HAGL.

Bộ nhớ đống javascript
Bộ nhớ đống javascript

Nói lại bản ghi V8 Engine đảm bảo nhận vai trò quản lý và cấp phát Bộ nhớ cho các ứng dụng NodeJS (100% Javascript). Vì vậy, bất kỳ vấn đề gì liên quan đến Bộ nhớ, thì bản gốc vẫn xuất phát từ V8

😅 Bingo, vẫn dễ hiểu đúng không nào. Đây không phải là 1 bài viết đi sâu vào V8, chúng ta chỉ dừng lại ở mức cơ bản của V8, và phần còn lại cần tập trung vào NodeJS trong quá trình cấu hình các ảnh hưởng từ V8 lên NodeJS

tu luyện

Để kiểm tra lại thông tin Bộ nhớ đang sử dụng, chúng ta có thể kiểm tra qua đoạn mã sau

const v8 = require('v8'); let totalHeapSize = v8.getHeapStatistics().total_available_size totalHeapSize = totalHeapSize / 1024 / 1024 / 1024; console.log(`Total heap size: ${totalHeapSize} GB`); //Total heap size: 2.043386295437813 GB

Code language: JavaScript (javascript)

Vì ở mỗi phiên bản NodeJS khác nhau sẽ có bộ nhớ được cấp phát khác nhau, ở phiên bản NodeJS v. 12. x đang có cấu hình mặc định là ~2GB

‘Total heap size’ ở trên chính là heapTotal, là Bộ nhớ tối đa được cấp phát cho ứng dụng NodeJS, vì vậy câu hỏi đặt ra là. Chúng ta có nên thay đổi cấu hình mặc định?
Trước khi trả lời các câu hỏi trên, chúng ta cần điểm qua một vài khái niệm để giúp bạn có thể nhìn vào một góc độ tổng quát hơn (nếu bạn nào không quan tâm đến lý thuyết thì có thể tiếp theo sang phần thực hành )

Bộ nhớ đống javascript
Bộ nhớ đống javascript

Heap Memory V8 bao gồm 2 thành phần chính. NewSpace và OldSpace

  • Không gian mới. là nơi lưu trữ hầu hết các đối tượng và được quản lý bởi bộ Scavenger GC (có thể hiểu đơn giản như một bộ thu hồi rác, và diễn ra với tần suất cao). Trong NewSpace có 2 Semi-Space (giống như s0, s1 trong JVM)
  • Không gian cũ. là nơi chứa các con trỏ tham chiếu đến các đối tượng khác (bỏ qua nếu bạn không hiểu, chúng ta sẽ làm rõ phần này ở bài Trái tim rỉ máu khi bạn không còn quan tâm Bộ sưu tập rác trong Node? 💔) và được thu thập bởi

Tới đây mình đã rất phân vân, không biết có nên để những phần lý thuyết trên diễn ra trong bài viết mình hay không. Nhưng tóm tắt bạn chỉ cần quan tâm đến việc lưu ý từ bộ nhớ Heap Memory V8, như sau

NewSpaceOldSpaceScavenger GC (bộ thu hồi rác #Lao-Công-A)Mark-Sweep GC (bộ thu hồi rác #Lao-Công-B)#Lao-Công-A

– Lau nhiều lần trong ngày(1 khoảng thời gian nào để đo lường)
– Lau nhanh#Lao-Công-B

– Lau with tần suất ít hơn #A
– Lâu Chậm

Xuống núi kinh

Vậy Rác(Rác) ở đây là gì, Dự án chúng ta đang tồn tại ở nơi rác mà bài viết đang đề cập?

let users = []; app.post('/users', (req, res) => { let newUser = req.body; users.push(newUser); //...more... });

Code language: JavaScript (javascript)

Bingo, bạn không nhầm đâu, trên là 1 ví dụ mà mình nghĩ là đa số AE thấy nó rất bình thường, nhưng đó là vấn đề rất đáng để quan tâm khi mỗi yêu cầu Heap sẽ cấp một vùng nhớ mới để lưu trữ và đối

Bộ nhớ đống javascript
Bộ nhớ đống javascript

Giải pháp ở đây, chúng ta chỉ cần xóa đối tượng và trong javascript chúng ta có thể xóa đơn giản như sau

let users = []; app.post('/users', (req, res) => { let newUser = req.body; users.push(newUser); //...more... users = null; //<= here });

Code language: JavaScript (javascript)

Hoặc hạn chế bộ đệm hoặc sử dụng các biến toàn cầu mà không kiểm soát chặt chẽ

Quay lại câu chuyện #2-Anh-Lão-Công. NodeJS cấp phát 64 MB cho NewSpace(mặc định), nhưng với Scavenge GC, chỉ một nửa 64 MB được sử dụng (= 32 MB). Như vậy, trong một khoảng thời gian dài phát triển, các ứng dụng của chúng ta sẽ trở nên lớn hơn và số lượng các đối tượng trong chương trình tăng dần dẫn đến ngưỡng kích hoạt GC
→ Khi GC (GC Scavenger) được kích hoạt, nó sẽ thực thi với tần suất lớn, sẽ dẫn đến việc ứng dụng của chúng ta sẽ có thêm mức độ từ những lần thực hiện bởi GC

Vì vậy, giải pháp ở đây chính là tăng giới hạn bộ nhớ cho NewSpace và rất có thể NodeJS cũng đã hỗ trợ cờ. ‘–max-semi-space-size’ để giúp chúng ta linh hoạt cấu hình dễ dàng, bạn cũng có thể theo dõi thêm danh sách cờ V8 mà Node hỗ trợ tại đây

node --max-semi-space-size=128 app.js

Tại sao là 128 MB mà không phải là 1 số lượng lớn hơn, nếu nửa không gian càng lớn thì ứng dụng sẽ có hiệu suất tốt hơn?
Mình đã xem qua các bài phân tích và cũng đã thử trải nghiệm trên chính các ứng dụng NodeJS của mình và thấy rằng 128MB là con số phù hợp với đại đa số các ứng dụng mình khai thác. Ở đây có một luật bạn cần nắm, đó chính là

‘Số lượng diễn ra GC giảm khi ta tăng bộ nhớ lên (64MB mặc định -> 128MB), nhưng thời gian trễ của từng GC cũng tăng lên. ’
Vì vậy, việc thiết lập semi-space không tồn tại khái niệm TUYỆT ĐỐI, mà đó là việc đánh đổi lựa chọn ưu tiên cho từng ứng dụng của bạn

Đến đây, chúng ta có thể trả lời cho những câu hỏi mà ta đã đặt ra ở đầu bài

  1. Chúng ta có nên thay đổi cấu hình mặc định ?
    → Cấu hình mặc định, là điểm tương đồng cho hầu hết các ứng dụng NodeJS mà đội ngũ phát triển NodeJS họ đã quan sát đưa ra. Hiện tại là NodeJS ver12. x 2GB được cấp phát cho OldSpace và 64MB cho NewSpace(Young Generation)
  2. Nếu thay đổi, thì con số bao nhiêu là tốt nhất?
    Thay vì bạn mong muốn là 1 bản sao của người khác, hãy biến mình trở thành phiên bản tốt nhất của chính bản thân mình
    → Điểm hẹn chốt mình muốn đề cập ở đây là thay vì chúng ta đi tìm con số trong vô thức, thì hãy lên kế hoạch đo lường và quan sát (Stress Test + CPU Profile) để tìm ra con số phù hợp nhất cho

Tương tự như phần NewSpace, tại vùng OldSpace(Old Generation) cũng có thể điều chỉnh qua cờ. ‘–max_old_space_size’, Tiếp tục với các bước làm trên, bạn có thể điều chỉnh để ứng dụng được xử lý tốt trong phần Bộ nhớ cho ứng dụng

Như mục trên mình đã đề cập, OldSpace là vùng chứa các đối tượng và nơi không có khoảng thời gian lưu trữ lớn nhất trong Heap V8, nên khi cấu hình bạn nên hạn chế cấu hình OldSpace quá thấp, vì nó sẽ dẫn đến lỗi bên dưới

[md5:] 241613/241627 97.5% [md5:] 241614/241627 97.5% [md5:] 241625/241627 98.1% Creating missing list.. (79570 files missing) Creating new files list.. (241627 new files) <--- Last few GCs ---> 11629672 ms: Mark-sweep 1174.6 (1426.5) -> 1172.4 (1418.3) MB, 659.9 / 0 ms [allocation failure] [GC in old space requested]. 11630371 ms: Mark-sweep 1172.4 (1418.3) -> 1172.4 (1411.3) MB, 698.9 / 0 ms [allocation failure] [GC in old space requested]. 11631105 ms: Mark-sweep 1172.4 (1411.3) -> 1172.4 (1389.3) MB, 733.5 / 0 ms [last resort gc]. 11631778 ms: Mark-sweep 1172.4 (1389.3) -> 1172.4 (1368.3) MB, 673.6 / 0 ms [last resort gc]. <--- JS stacktrace ---> ==== JS stack trace ========================================= Security context: 0x3d1d329c9e59 <JS Object> 1: SparseJoinWithSeparatorJS(aka SparseJoinWithSeparatorJS) [native array.js:~84] [pc=0x3629ef689ad0] (this=0x3d1d32904189 <undefined>,w=0x2b690ce91071 <JS Array[241627]>,L=241627,M=0x3d1d329b4a11 <JS Function ConvertToString (SharedFunctionInfo 0x3d1d3294ef79)>,N=0x7c953bf4d49 <String[4]\: ,\n >) 2: Join(aka Join) [native array.js:143] [pc=0x3629ef616696] (this=0x3d1d32904189 <undefin... FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 1: node::Abort() [/usr/bin/node] 2: 0xe2c5fc [/usr/bin/node] 3: v8::Utils::ReportApiFailure(char const*, char const*) [/usr/bin/node] 4: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/bin/node] 5: v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/usr/bin/node] 6: v8::internal::Runtime_SparseJoinWithSeparator(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/bin/node] 7: 0x3629ef50961b

Code language: JavaScript (javascript)

Do đó giải pháp để xử lý lỗi là tăng bộ nhớ cho vùng OldSpace để đảm bảo việc lưu trữ các đối tượng vẫn đảm bảo

node --max-old-space-size=1024 index.js #increase to 1gb node --max-old-space-size=2048 index.js #increase to 2gb node --max-old-space-size=3072 index.js #increase to 3gb

Code language: PHP (php)

Với mình, nếu bạn sử dụng máy chủ với 2GB, bạn chỉ nên đặt OldSpace với 1. 5GB,
Bởi vì nếu bạn cấp phát tối đa tài nguyên của máy chủ, nó sẽ dẫn đến việc hệ điều hành sẽ giết đi các tiến trình khác (ngẫu nhiên), tráo đổi hoặc rò rỉ bộ nhớ,… và điều này là ác mộng có thật.

Đây chỉ là nói về NodeJS xung quanh cấu hình V8, ngoài máy chủ của bạn còn quan tâm đến các quy trình khác. Và việc nhiều quy trình hoặc dịch vụ giống nhau trên một máy chủ sẽ dẫn đến tình trạng mất kiểm soát, và thực sự rất khó để suy diễn được nguyên nhân. Đó cũng là lý do mình đã phân chia các Service. Ứng dụng NodeJS, MongoDB, Redis, ElasticSearch, … ra trên các máy chủ khác nhau

Câu chuyện giữa Anh Chàng NodeJS và Cô Nàng V8

Tin tôi đi, điều này sẽ rất hiệu quả trong quá trình kiểm tra giám sát tài nguyên của máy chủ. Tới đây vẫn mong AE còn đủ tỉnh táo để……………. Upvote cho mình đấy mà. Hi vọng ACE có thêm vài góc nhìn và lưu ý cho những dự án sắp tới của mình

Nguồn. https. //viblo. asia/p/noi-dep-nhat-chinh-la-noi-phu-hop-nhat-va-cau-chuyen-giua-anh-chang-nodejs-va-co-nang-v8-4dbZNNOLZYM