Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa hoàn thành một đợt load test cho hệ thống. Bạn bắn 5.000 người dùng ảo trong 30 phút, biểu đồ latency phẳng lì, throughput ổn định, error rate 0%. Bạn tự tin gửi báo cáo "hệ thống chịu tải tốt" cho sếp và team lên production với tâm thế thắng lợi. Ba ngày sau, lúc 2 giờ sáng, hệ thống sập. Không phải vì lượng truy cập tăng đột biến — mà vì nó cứ chạy... quá lâu.
Đây chính là loại lỗi mà Soak Testing (còn gọi là Endurance Testing — kiểm thử độ bền) sinh ra để bắt. Trong khi load test ngắn trả lời câu hỏi "hệ thống chịu được bao nhiêu tải?", thì soak test trả lời một câu hỏi hoàn toàn khác và thường bị bỏ quên: "hệ thống có sống sót được khi phải chạy liên tục trong nhiều ngày dưới tải bình thường không?".
Nhóm lỗi mà soak test phát hiện — memory leak, connection leak, disk đầy dần, cache phình to vô hạn — là nhóm nguy hiểm nhất trong nghề, bởi vì chúng vô hình trong mọi bài test ngắn. Chúng tích tụ chậm rãi, âm thầm, rồi giết hệ thống vào đúng thời điểm bạn không ngờ tới. Bài học này sẽ giúp bạn hiểu bản chất của soak test, biết cách thiết kế một bài soak test đúng đắn, và quan trọng nhất là biết cách "săn" memory leak — kỹ năng phân biệt một QA performance thường và một performance engineer thực thụ.
Khái niệm cốt lõi
Soak test là gì và khác gì các loại test khác
Nếu ở bài trước (Bài 5) bạn đã học về Load, Stress, Spike, thì soak test là một biến thể đặc biệt về mặt thời gian. Đặc trưng của nó là:
- Tải vừa phải — thường bằng khoảng 70–80% mức tải trung bình trong giờ cao điểm thật, KHÔNG phải tải cực đại. Đây là điểm nhiều người hiểu sai: mục tiêu không phải đẩy hệ thống tới giới hạn, mà mô phỏng đúng điều kiện production bình thường.
- Kéo dài rất lâu — từ vài giờ đến 24, 48, thậm chí 72 giờ liên tục. Có công ty chạy soak test cả một tuần trước khi release lớn.
Các vấn đề mà soak test "bắt" được
1. Memory leak (rò rỉ bộ nhớ) — đây là "con mồi" số một. Trong các ngôn ngữ có garbage collector như Java, C#, Node.js, Go, memory leak xảy ra khi các object đáng lẽ phải được thu hồi nhưng vẫn còn bị tham chiếu ở đâu đó (ví dụ một static List cứ được add vào mà không bao giờ xóa). Biểu hiện kinh điển: đồ thị heap memory tăng dần theo hình răng cưa, nhưng đáy của mỗi lần răng cưa (sau khi GC chạy) cứ cao dần lên. Đến một lúc, JVM ném OutOfMemoryError hoặc GC chạy liên tục khiến CPU 100% mà không làm được việc gì (hiện tượng "GC thrashing").
2. Connection leak — kết nối tới database, tới Redis, tới message queue được mở ra nhưng không đóng lại. Connection pool cạn dần, đến lúc mọi request đều phải chờ và timeout. Đây là lỗi cực phổ biến với code viết ẩu quên close() hoặc không dùng try-with-resources.
3. Resource cạn dần — disk đầy vì log ghi không giới hạn, số file descriptor chạm trần của OS, thread pool phình to không kiểm soát.
4. Suy giảm hiệu năng theo thời gian — latency ban đầu 100ms, sau 20 giờ tăng lên 400ms dù tải không đổi. Nguyên nhân có thể là bảng database phình to không có index phù hợp, cache eviction policy sai khiến cache miss ngày càng nhiều, hoặc fragmentation bộ nhớ.
Vì sao "đáy răng cưa dâng lên" là dấu hiệu vàng
Điểm mấu chốt khi phân tích memory trong soak test là đừng nhìn đỉnh, hãy nhìn đáy. Bộ nhớ heap luôn dao động lên xuống theo nhịp GC — điều đó bình thường. Một hệ thống khỏe mạnh: sau mỗi lần GC, bộ nhớ trở về gần cùng một mức đáy. Một hệ thống bị leak: cái đáy đó dâng lên đều đặn như thủy triều. Vẽ một đường thẳng nối các điểm đáy — nếu nó dốc lên, bạn có leak. Đây là kỹ thuật đọc đồ thị mà mọi performance engineer phải nằm lòng.
Tình huống thực tế
Ví dụ 1: Sàn thương mại điện tử "ShopViet" và cái chết lúc 3 giờ sáng
Một sàn TMĐT giả định tại TP.HCM, gọi là ShopViet, có traffic ổn định khoảng 3.000 request/giây trong giờ hành chính. Team QA đã chạy load test đầy đủ: 10.000 người dùng đồng thời trong 1 giờ, mọi chỉ số đều đẹp. Họ release tính năng "giỏ hàng thông minh" gợi ý sản phẩm.
Vấn đề: cứ khoảng 2–3 ngày một lần, vào rạng sáng, service giỏ hàng lại tự restart (do Kubernetes kill pod vì memory chạm limit 4GB). Ban ngày không ai để ý vì restart nhanh. Nhưng log lỗi tích lũy và một số khách mất giỏ hàng.
Team dựng một bài soak test: tải vừa phải 2.000 req/s nhưng chạy liên tục 36 giờ, đồng thời gắn monitoring heap của JVM qua Grafana. Kết quả lộ ra ngay: đường đáy heap dâng đều, cứ 6 tiếng tăng khoảng 600MB. Điều tra bằng heap dump cho thấy tính năng gợi ý sản phẩm cache kết quả vào một ConcurrentHashMap có key là sessionId nhưng không bao giờ xóa session cũ. Mỗi khách vào tạo một entry vĩnh viễn.
Bài học: Load test 1 giờ không thể bắt lỗi này vì 1 giờ chưa đủ để map phình tới ngưỡng nguy hiểm. Chỉ có việc chạy dài mới phơi bày. Và nguyên nhân gốc luôn là một cái gì đó "tích lũy mà không giải phóng".
Ví dụ 2: Fintech "PayNhanh" và connection pool cạn kiệt
Một startup fintech Đông Nam Á, PayNhanh, xử lý thanh toán QR. Họ dùng k6 chạy soak test 24 giờ với 500 giao dịch/giây trước khi ra mắt lớn. Trong 18 giờ đầu mọi thứ ổn. Sang giờ thứ 19, error rate bắt đầu nhích lên: 0.1%, rồi 2%, rồi 15%. Latency p95 nhảy từ 180ms lên 8 giây.
Điều tra cho thấy đây không phải memory leak mà là connection leak. Một luồng xử lý hoàn tiền (refund) hiếm khi được gọi trong test ngắn, nhưng trong 19 giờ đã chạy đủ số lần để lộ ra rằng nó mở kết nối tới database phụ mà quên đóng khi gặp một nhánh exception cụ thể. HikariCP connection pool (tối đa 50 kết nối) cạn dần. Đồ thị active_connections tăng bậc thang, còn idle_connections giảm về 0 — dấu hiệu leak kết nối kinh điển.
Bài học: Soak test không chỉ bắt memory leak. Bất kỳ tài nguyên nào có giới hạn (connection, file handle, thread) mà bị rò rỉ chậm đều chỉ lộ ra khi chạy dài. Và những nhánh code hiếm gặp (như refund, retry, error handling) thường là nơi ẩn náu của leak, vì chúng ít được test kỹ.
Ví dụ 3: Nền tảng học trực tuyến và suy giảm hiệu năng do database phình
Một nền tảng e-learning chạy soak test 48 giờ. Không có OutOfMemory, không có connection leak, error rate luôn 0%. Nhưng có một điều lạ: latency của API "lấy tiến độ học tập" tăng tuyến tính — giờ đầu là 90ms, giờ thứ 48 là 620ms, dù tải không đổi.
Nguyên nhân: mỗi lần học viên xem bài, hệ thống INSERT một bản ghi vào bảng activity_log mà không bao giờ dọn. Query lấy tiến độ có một câu SELECT ... WHERE user_id = ? ORDER BY created_at quét trên bảng ngày càng lớn mà thiếu index phù hợp. Sau 48 giờ bảng có hàng triệu dòng, mỗi query chậm dần.
Bài học: Suy giảm hiệu năng theo thời gian (performance degradation) là một dạng "leak" ở tầng dữ liệu, không phải bộ nhớ. Soak test bắt được vì nó tích lũy dữ liệu thật trong thời gian đủ dài — điều mà test ngắn với database sạch không bao giờ tái hiện.
Hướng dẫn từng bước
Dưới đây là quy trình thực hiện một bài soak test bài bản.
Bước 1 — Xác định mức tải và thời lượng. Chọn tải bằng khoảng 70–80% peak thật của production. Về thời lượng: tối thiểu 8 giờ để có ý nghĩa, lý tưởng 24–72 giờ. Nếu bị hạn chế thời gian, hãy chạy tải cao hơn một chút để tăng tốc độ tích lũy — nhưng nhớ điều này làm kết quả kém sát thực tế hơn.
Bước 2 — Thiết lập monitoring TRƯỚC khi chạy. Đây là bước quan trọng nhất và hay bị bỏ. Soak test mà không có monitoring server-side thì vô nghĩa, vì bản thân công cụ test (JMeter/k6) chỉ thấy latency và error rate, không thấy heap hay connection. Bạn cần theo dõi: heap/RSS memory, GC frequency và GC pause time, số connection database đang active, CPU, disk usage, số file descriptor. Dùng Grafana + Prometheus, hoặc APM như Datadog/New Relic.
Bước 3 — Viết kịch bản với dữ liệu đa dạng. Đây là một cạm bẫy tinh vi: nếu bạn dùng cùng một user và cùng dữ liệu lặp lại suốt 48 giờ, cache của hệ thống sẽ luôn "hit" và bạn che giấu chính vấn đề mình muốn tìm. Hãy parameter hóa để mỗi request dùng dữ liệu khác nhau, ép hệ thống phải xử lý thật. Ví dụ với k6:
import http from 'k6/http';
import { sleep, check } from 'k6';export const options = {
scenarios: {
soak: {
executor: 'constant-vus',
vus: 200, // tải vừa phải, không phải peak
duration: '24h', // đặc trưng của soak test
},
},
thresholds: {
http_req_duration: ['p(95)<500'], // p95 phải luôn dưới 500ms
http_req_failed: ['rate<0.01'], // lỗi dưới 1%
},
};
export default function () {
const userId = Math.floor(Math.random() * 100000); // dữ liệu đa dạng
const res = http.get(https://api.shopviet.vn/cart/${userId});
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
Bước 4 — Chạy và ghi baseline giờ đầu. Ghi lại latency, throughput, memory ở giờ đầu tiên làm mốc so sánh. Mọi phán đoán về "suy giảm" đều dựa trên việc so với baseline này.
Bước 5 — Quan sát xu hướng, không quan sát điểm. Trong soak test, một con số tại một thời điểm gần như vô nghĩa. Cái bạn cần là đường xu hướng. Vẽ đồ thị memory, latency, connection theo trục thời gian dài. Đường nằm ngang = khỏe. Đường dốc lên đều = có leak.
Bước 6 — Chụp heap dump khi nghi ngờ. Nếu thấy memory dâng, đừng đoán mò. Với JVM dùng jmap -dump:live,format=b,file=heap.bin <pid> rồi phân tích bằng Eclipse MAT hoặc VisualVM để tìm object nào chiếm nhiều nhất và bị cái gì giữ tham chiếu. Đây là bằng chứng để lập trình viên sửa gốc.
Bước 7 — Viết findings rõ ràng. Báo cáo không chỉ nói "có leak" mà phải nêu: tốc độ leak (MB/giờ), thời điểm dự kiến hệ thống chết nếu không sửa, và bằng chứng heap dump chỉ đúng dòng code thủ phạm.
Lỗi thường gặp & mẹo
Lỗi 1 — Chạy soak test với tải quá cao. Nhiều người tưởng "chạy lâu thì phải đẩy tải mạnh cho chắc". Sai. Soak test dùng tải vừa phải; nếu đẩy tải cực đại thì bạn đang làm stress test, và hệ thống có thể chết vì quá tải chứ không phải vì leak — che mất thứ bạn muốn tìm.
Lỗi 2 — Không monitor server-side. Chỉ nhìn output của k6/JMeter rồi kết luận "ổn". Nhưng memory leak không hiện ra ở latency cho tới phút cuối cùng trước khi sập. Không có đồ thị heap thì bạn mù.
Lỗi 3 — Dùng dữ liệu tĩnh lặp lại. Như đã nói, điều này làm cache che giấu vấn đề, khiến bài test vô dụng. Luôn dùng dữ liệu động, đa dạng.
Lỗi 4 — Nhầm nhịp răng cưa GC với leak. Người mới thấy memory tăng lên trong vài phút rồi hoảng. Nhớ: chỉ nhìn đáy sau GC. Nếu đáy ổn định thì không sao.
Lỗi 5 — Bản thân công cụ test bị leak. Trớ trêu là JMeter chạy 48 giờ ở chế độ GUI cũng có thể... hết RAM và crash trước cả hệ thống. Luôn chạy JMeter ở non-GUI mode cho soak test (đã học ở Bài 11), và cấu hình listener ghi ra file thay vì giữ trong bộ nhớ.
Mẹo: Nếu không có 48 giờ, hãy chạy một "mini soak" 4–8 giờ định kỳ trong CI/CD (Bài 27) như một tuyến phòng thủ đầu tiên. Nó không thay được soak test đầy đủ nhưng bắt được các leak nghiêm trọng sớm.
Mẹo: Chạy soak test trên môi trường càng giống production càng tốt về cấu hình RAM, connection pool, GC settings. Một leak có thể vô hại ở máy 32GB nhưng chí mạng ở pod 2GB.
Bài tập thực hành
- Thiết kế bài soak test. Giả sử hệ thống của bạn có peak 1.000 req/s trong giờ cao điểm. Hãy viết ra: mức tải bạn chọn cho soak test là bao nhiêu req/s, thời lượng bao lâu, và ba chỉ số server-side quan trọng nhất bạn sẽ monitor. Giải thích lý do từng lựa chọn.
- Viết script k6. Dựa trên ví dụ trong bài, viết một script k6 soak test chạy 12 giờ với 150 VUs, dùng userId ngẫu nhiên, và đặt threshold p95 < 400ms cùng error rate < 0.5%.
- Đọc đồ thị. Cho một đồ thị heap giả định: giờ 0 đáy ở 800MB, giờ 12 đáy ở 1.400MB, giờ 24 đáy ở 2.000MB, limit là 3GB. Tính tốc độ leak (MB/giờ) và dự đoán sau bao nhiêu giờ nữa hệ thống sẽ chạm limit và có nguy cơ OutOfMemory.
- Phân loại vấn đề. Với mỗi hiện tượng sau, cho biết đó là loại vấn đề gì (memory leak / connection leak / performance degradation do database): (a) idle_connections giảm dần về 0 sau 15 giờ; (b) latency tăng tuyến tính nhưng memory ổn định; (c) GC chạy ngày càng thường xuyên và CPU tăng dần.
Tóm tắt
Soak test (endurance test) là bài kiểm thử tải vừa phải nhưng kéo dài rất lâu (24–72 giờ), sinh ra để bắt nhóm lỗi vô hình trong mọi test ngắn: memory leak, connection leak, resource cạn dần và suy giảm hiệu năng theo thời gian. Bản chất của những lỗi này là chúng phụ thuộc vào tổng tích lũy theo thời gian, không phải cường độ tải — nên chỉ lộ ra khi hệ thống chạy đủ lâu.
Ba nguyên tắc cần nhớ: (1) dùng tải vừa phải, không phải peak; (2) monitoring server-side là bắt buộc, đặc biệt là heap và connection pool, vì công cụ test không nhìn thấy chúng; (3) khi phân tích memory, nhìn đáy của đồ thị răng cưa — nếu đáy dâng lên đều đặn, bạn có leak. Khi nghi ngờ, dùng heap dump để tìm đúng thủ phạm thay vì đoán mò. Ba ví dụ ShopViet, PayNhanh và nền tảng e-learning cho thấy leak có thể ẩn ở bộ nhớ, ở kết nối, hay ở tầng dữ liệu — và luôn núp trong những nhánh code ít được test kỹ. Nắm vững soak test, bạn sẽ ngăn được đúng loại sự cố khiến hệ thống chết lúc 3 giờ sáng — thời điểm không ai muốn nhận cuộc gọi khẩn.