Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Project 3: Endurance & Memory Leak Hunt

Mở đầu — vì sao bài này quan trọng

Có một loại lỗi mà bạn gần như không bao giờ bắt được bằng một bài test 5 phút, 10 phút hay thậm chí một tiếng: đó là memory leak (rò rỉ bộ nhớ) và connection leak (rò rỉ kết nối). Những lỗi này không làm hệ thống sập ngay. Chúng âm thầm tích tụ. Ứng dụng của bạn chạy mượt suốt buổi demo, mượt qua đợt load test buổi sáng, đội QA gật đầu ký duyệt — rồi ba ngày sau khi lên production, service tự nhiên "chết" lúc 2 giờ sáng, on-call bị dựng dậy, restart pod thì mọi thứ lại bình thường như chưa có gì xảy ra. Vài ngày sau lại lặp lại.

Đó chính xác là chân dung của một memory leak, và nó là lý do bài Endurance / Soak Testing tồn tại. Trong Project 3 này, bạn sẽ đóng vai một Performance Engineer được giao nhiệm vụ chạy soak test 24 giờ trên một order-service (dịch vụ đặt hàng viết bằng Node.js) và truy tìm điểm rò rỉ bộ nhớ/kết nối trước khi nó gây họa trên production.

Khác với load test hay stress test — vốn hỏi "hệ thống chịu được bao nhiêu tải?" — soak test hỏi một câu rất khác: "Hệ thống có chịu nổi thời gian không?" Cùng một mức tải vừa phải, nhưng kéo dài liên tục nhiều giờ. Đây là bài project tổng hợp: bạn sẽ dùng k6 để tạo tải bền bỉ, dùng APM để quan sát bộ nhớ theo thời gian, và quan trọng nhất — học cách đọc ra một đường xu hướng dốc lên chậm rãi giữa hàng nghìn điểm dữ liệu. Đó là kỹ năng phân biệt một QA làm cho có với một Performance Engineer thực thụ.

Khái niệm cốt lõi

Soak Test (Endurance Test) là gì

Soak test là kịch bản đưa hệ thống chịu một mức tải ổn định, thực tế (thường 60–80% công suất bình thường, không phải tải cực đại) trong một khoảng thời gian dài — 8 giờ, 12 giờ, 24 giờ, đôi khi cả tuần. Mục tiêu không phải tìm giới hạn, mà tìm những vấn đề chỉ lộ diện theo thời gian:

  • Memory leak: bộ nhớ dùng tăng dần và không bao giờ được thu hồi hết.
  • Connection/resource leak: kết nối database, socket, file handle mở ra mà không đóng lại, đến lúc cạn pool.
  • Degradation dần dần: latency (độ trễ) tăng từ từ do log file phình to, cache không có giới hạn, hoặc garbage collector (bộ dọn rác) làm việc ngày càng vất vả.
  • Disk fill: log, temp file, session file tích tụ đầy ổ đĩa.

Memory leak trong Node.js trông như thế nào

Node.js chạy trên V8 engine với cơ chế garbage collection tự động. Về lý thuyết, object nào không còn được tham chiếu sẽ bị dọn. Nhưng leak xảy ra khi code vô tình giữ tham chiếu đến object không còn cần nữa. Vài thủ phạm kinh điển:

  • Biến toàn cục hoặc closure tích lũy dữ liệu: let cache = {} rồi cứ cache[orderId] = data mà không bao giờ xóa.
  • Event listener đăng ký mà không gỡ (emitter.on(...) lặp lại mỗi request).
  • Timer (setInterval) tạo ra nhưng không clearInterval.
  • Connection pool cấu hình sai, mỗi request tạo connection mới thay vì tái sử dụng.
Trên biểu đồ bộ nhớ, một hệ thống khỏe mạnh sẽ có đường heap răng cưa nhưng đi ngang — tăng rồi tụt xuống khi GC chạy, dao động quanh một đường trung bình phẳng. Một hệ thống leak sẽ có đường trung bình dốc lên đều đặn: mỗi lần GC dọn ít hơn lượng cấp phát mới, đáy của mỗi chu kỳ răng cưa ngày một cao hơn. Đây là dấu hiệu vàng (smoking gun) bạn cần tìm.

Các chỉ số cần theo dõi trong 24 giờ

Không chỉ nhìn RAM. Một soak test tốt giám sát đồng thời:

NhómChỉ sốÝ nghĩa khi soak
MemoryHeap used, RSSLeak nếu đường trung bình dốc lên
GCTần suất & thời gian pause GCTăng dần = áp lực bộ nhớ
ConnectionDB pool active/idle, socket countCạn pool = connection leak
Latencyp95, p99 theo thời gianXấu dần = degradation
Error rate% lỗi 5xxTăng vọt cuối test = tài nguyên cạn
Hệ thốngCPU, disk usage, file descriptorDisk đầy, FD cạn
Quy tắc mentor muốn bạn khắc cốt ghi tâm: so sánh giờ đầu với giờ cuối. Nếu p99 latency giờ thứ 24 cao hơn giờ thứ nhất trong khi tải không đổi — hệ thống đang xuống cấp, dù chưa sập.

Tình huống thực tế

Ví dụ 1: Sàn TMĐT ở TP.HCM và cái pod chết lúc rạng sáng

Một công ty thương mại điện tử tại TP.HCM (giả định gọi là ShopViet) triển khai order-service bằng Node.js trên Kubernetes, 6 pod, mỗi pod giới hạn 512 MB RAM. Load test buổi chiều với 500 VU (virtual user) trong 30 phút: hoàn hảo, p95 = 180ms, không lỗi. Team tự tin release.

Ba ngày sau, cứ khoảng 3–4 giờ sáng, lần lượt vài pod bị Kubernetes OOMKilled (giết vì hết bộ nhớ) rồi tự restart. Không ai để ý vì auto-restart che giấu vấn đề, cho đến khi một đêm 4 pod chết gần đồng thời, service quá tải, đơn hàng lỗi hàng loạt.

Team dựng một soak test k6: 150 VU đều đặn suốt 12 giờ. Trên Grafana, đường heap used của mỗi pod tăng tuyến tính từ 120 MB lên chạm trần 512 MB sau đúng ~9 giờ — rồi OOMKilled. Truy vào code: một middleware log ghi mọi request body vào một mảng requestLog toàn cục "để debug", quên xóa. Mỗi ngày vài trăm nghìn request nhồi vào mảng đó. Bài học: load test 30 phút không bao giờ bắt được lỗi cần 9 giờ để lộ. Auto-restart của K8s là con dao hai lưỡi — nó giấu leak khỏi mắt bạn.

Ví dụ 2: Fintech Singapore và connection leak "vô hình"

Một fintech ở Singapore chạy service thanh toán Node.js kết nối PostgreSQL qua pool cấu hình max: 20. RAM của họ hoàn toàn ổn định qua soak test — đường heap phẳng lỳ, ai cũng yên tâm. Nhưng đến giờ thứ 6, error rate bắt đầu nhích lên, đến giờ thứ 8 thì 15% request lỗi với thông báo timeout acquiring connection from pool.

Vấn đề không phải RAM mà là connection leak. Trong một nhánh xử lý lỗi (catch block), code quên gọi client.release() để trả connection về pool. Mỗi request rơi vào nhánh lỗi đó (khoảng 0.5%) sẽ "ăn" mất một connection vĩnh viễn. Với pool 20 connection, chỉ cần ~20 lần đi vào nhánh lỗi là pool cạn sạch, mọi request sau đó phải xếp hàng chờ đến timeout.

Họ phát hiện nhờ giám sát chỉ số pool active connections trên APM: đường này leo dốc bậc thang, từ 2–3 lên chạm 20 rồi đứng yên ở đó. Bài học: leak không chỉ là bộ nhớ. Luôn giám sát connection pool. Và một leak rất chậm (0.5% request) là loại nguy hiểm nhất vì test ngắn hoàn toàn bỏ sót.

Ví dụ 3: Startup giao đồ ăn và cái log file 40GB

Một startup giao đồ ăn tại Hà Nội gặp sự cố lạ: sau khoảng 20 giờ chạy, service chậm dần rồi ngừng nhận request, nhưng RAM và connection đều bình thường. Soak test 24h hé lộ thủ phạm nằm ở tầng khác: disk. Ứng dụng bật log ở mức debug trên production, mỗi request ghi vài dòng, không có log rotation. Sau 20 giờ, log file phồng lên 40GB, lấp đầy ổ đĩa, và tiến trình Node.js không ghi được log nữa nên treo ở thao tác I/O.

Bài học: soak test không chỉ soi RAM. Giám sát cả disk usagefile descriptor. Nhiều "memory leak" bị đổ oan trong khi thủ phạm thật là disk, log hoặc temp file. Đây là lý do dashboard soak test phải phủ đủ các tầng tài nguyên.

Hướng dẫn từng bước

Dưới đây là quy trình đầy đủ cho Project 3: chạy soak test 24h trên order-service với k6 và truy tìm leak.

Bước 1 — Xác định mức tải "thực tế bền vững"

Đừng dùng tải cực đại. Soak test cần mô phỏng một ngày làm việc bình thường. Nếu production ở giờ cao điểm có ~200 request/giây, hãy chọn mức khoảng 60–70%, ví dụ 130 rps đều đặn. Mục tiêu là bền, không phải mạnh.

Bước 2 — Viết k6 script với constant-arrival-rate

Với soak test, bạn muốn giữ throughput (số request/giây) ổn định, không phải số VU cố định. Dùng executor constant-arrival-rate:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = { scenarios: { soak: { executor: 'constant-arrival-rate', rate: 130, // 130 request mỗi giây timeUnit: '1s', duration: '24h', // chạy liền 24 giờ preAllocatedVUs: 200, maxVUs: 400, }, }, thresholds: { http_req_duration: ['p(99)<800'], // p99 phải dưới 800ms http_req_failed: ['rate<0.01'], // lỗi dưới 1% }, };

export default function () { const payload = JSON.stringify({ userId: Math.floor(Math.random() * 100000), items: [{ sku: 'PHO-001', qty: 1 }], }); const res = http.post( 'https://staging.order-service.internal/api/orders', payload, { headers: { 'Content-Type': 'application/json' } } ); check(res, { 'status 201': (r) => r.status === 201 }); sleep(1); }

Điểm mấu chốt: duration: '24h' và tải giữ hằng số. Nếu leak tồn tại, chỉ có thời gian mới phơi bày nó.

Bước 3 — Bật giám sát APM song song

k6 cho bạn biết hệ thống phản ứng thế nào từ phía client. Nhưng để tìm leak, bạn cần nhìn bên trong service. Bật APM (Application Performance Monitoring) như Datadog, New Relic, hoặc Node.js exporter đẩy metric sang Prometheus/Grafana. Với Node.js, hãy chắc chắn thu thập:

  • process.memoryUsage() → heapUsed, rss, external.
  • Số lần và thời gian GC (qua perf_hooks hoặc APM agent).
  • DB pool: active, idle, waiting connections.
  • Số file descriptor đang mở.
Nếu chưa có APM đầy đủ, một cách "nghèo mà hiệu quả" là chạy node --inspect và định kỳ chụp heap snapshot, hoặc log process.memoryUsage() mỗi 60 giây ra file để vẽ biểu đồ sau.

Bước 4 — Chạy test và quan sát theo cửa sổ thời gian

Khởi động test ở chế độ non-GUI, để chạy nền:

k6 run --out influxdb=http://localhost:8086/k6 soak.js

Trong lúc chạy, đừng ngồi nhìn màn hình 24 giờ. Thay vào đó, đặt các mốc kiểm tra: giờ 1, giờ 6, giờ 12, giờ 18, giờ 24. Tại mỗi mốc, ghi lại: heap used trung bình, p99 latency, error rate, connection active. Bạn đang xây một bảng so sánh theo thời gian.

Bước 5 — Đọc biểu đồ và kết luận

Đây là phần quan trọng nhất. Nhìn đường heap:

  • Răng cưa quanh đường ngang → khỏe mạnh, không leak.
  • Đường trung bình dốc lên đều → nghi vấn memory leak cao. Xác nhận bằng heap snapshot đầu/cuối, so sánh object nào tăng bất thường (thường là Array, Object, hoặc closure của một hàm cụ thể).
  • Heap phẳng nhưng error rate/latency xấu dần → nghi connection leak hoặc disk. Kiểm tra pool và disk usage.

Bước 6 — Cô lập và xác nhận nguyên nhân

Khi đã nghi ngờ, dùng heap snapshot (2 snapshot cách nhau vài giờ) và tính năng "comparison" của Chrome DevTools để tìm object tăng nhiều nhất. Lần theo retainer path (chuỗi tham chiếu giữ object đó sống) để tìm đúng dòng code. Sau khi sửa, chạy lại soak test để xác nhận đường heap đã đi ngang — đây là bước nghiệm thu, không được bỏ.

Lỗi thường gặp & mẹo

  • Chạy soak với tải quá cao: Nếu bạn ép tải cực đại 24 giờ, bạn đang làm stress test kéo dài, không phải soak. Leak chậm sẽ bị lẫn vào tình trạng hệ thống vốn đã quá tải. Giữ tải ở mức thực tế, bền vững.
  • Test quá ngắn: 30 phút không phải soak test. Nhiều leak cần 6–9 giờ mới lộ. Nếu ngân sách hạn chế, tối thiểu hãy chạy 8 giờ (một ca làm việc).
  • Chỉ nhìn RAM, quên connection và disk: Như ví dụ 2 và 3, leak có thể ẩn ở pool hoặc ổ đĩa. Dashboard phải phủ đủ tầng.
  • Bị GC đánh lừa: Đừng hoảng khi thấy heap tăng — chờ GC chạy. Chỉ kết luận leak khi đáy của các chu kỳ răng cưa dốc lên, tức đường trung bình đi lên.
  • Không cô lập biến số: Nếu staging còn cron job, batch khác chạy chung, bạn không biết leak do đâu. Chạy soak trên môi trường sạch, chỉ có mình service cần test.
  • Quên trần bộ nhớ của container: Trên K8s, heap có thể vẫn "còn chỗ" theo V8 nhưng RSS đã chạm limit container → OOMKilled. Luôn giám sát RSS so với memory limit, không chỉ heap.
  • Mẹo vàng: Luôn log process.memoryUsage() mỗi phút ra file CSV ngay cả khi đã có APM. File CSV này là bằng chứng bất tử để vẽ đường xu hướng và viết báo cáo, không phụ thuộc vào việc APM còn lưu dữ liệu bao lâu.

Bài tập thực hành

  • Dựng target có leak cố ý: Viết một order-service Node.js đơn giản với endpoint POST /orders. Cố tình thêm một mảng toàn cục const history = [] và mỗi request history.push(req.body) mà không giới hạn. Đây sẽ là "bệnh nhân" của bạn.
  • Viết soak script k6: Dùng executor constant-arrival-rate, rate 50 rps, duration ban đầu 30 phút để kiểm thử nhanh, sau đó nâng lên 4 giờ. Thêm threshold p(99)<800http_req_failed rate<0.01.
  • Thu thập metric bộ nhớ: Trong service, log process.memoryUsage().heapUsed mỗi 30 giây ra file mem.csv. Sau khi test xong, vẽ biểu đồ đường heap theo thời gian (Excel/Google Sheets đủ dùng).
  • Đọc và kết luận: Xác định đường heap là "răng cưa ngang" hay "dốc lên". Ước lượng tốc độ leak (MB/giờ) và tính xem với giới hạn 512 MB thì sau bao lâu service sẽ OOM.
  • Sửa và nghiệm thu: Thêm giới hạn if (history.length > 1000) history.shift(), chạy lại soak, chứng minh đường heap đã đi ngang. Viết một đoạn "finding" ngắn 5 dòng: triệu chứng, nguyên nhân gốc, bằng chứng (biểu đồ), cách sửa, kết quả sau sửa.
  • Thử thách nâng cao: Thêm một connection leak (mở kết nối giả lập, quên đóng trong nhánh lỗi) và chứng minh rằng đường heap không cho thấy vấn đề này — buộc bạn phải giám sát một chỉ số khác. Đây là bài học đắt giá nhất của project.

Tóm tắt

Project 3 dạy bạn một sự thật mà nhiều đội QA phải trả giá bằng những đêm on-call mới hiểu: có những lỗi chỉ thời gian mới bắt được. Memory leak và connection leak không bao giờ xuất hiện trong bài test nhanh — chúng cần soak test kéo dài nhiều giờ, với tải thực tế ổn định, để lộ đường xu hướng dốc lên chậm rãi.

Những điểm cần nhớ: (1) Soak test dùng tải bền vững chứ không phải cực đại, chạy tối thiểu 8 giờ, lý tưởng 24 giờ; (2) Dùng k6 executor constant-arrival-rate để giữ throughput ổn định; (3) Giám sát nhiều tầng — heap, RSS, GC, connection pool, disk, file descriptor — vì leak có thể ẩn ở bất kỳ đâu; (4) Kỹ năng cốt lõi là đọc đường xu hướng: so sánh giờ đầu với giờ cuối, tìm đường trung bình dốc lên giữa những chu kỳ răng cưa; (5) Sau khi sửa, luôn chạy lại soak để nghiệm thu.

Khi bạn hoàn thành project này, bạn không còn chỉ là người "chạy test". Bạn là người có thể nhìn vào một biểu đồ 24 giờ và nói chính xác: "Service này rò rỉ 40 MB mỗi giờ, sẽ OOM sau 9 giờ, nguyên nhân ở middleware log." Đó là giá trị thật của một Performance Engineer.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi