1. Lời mở đầu
Chúc mừng năm mới tất cả anh chị em cô dì chú bác!!!
Trong thời gian gần đây, mình đã có cơ hội làm việc với K6 rất nhiều và thật sự, K6 là một công cụ kiểm tra hiệu suất vô cùng mạnh mẽ và tiện lợi. Tuy nhiên, khi các bài toán hiệu suất trở nên phức tạp hơn như: chạy với số lượng lớn VUs (Vitural Users) lớn, RPS (request per seconds), hay bài toán upload các file lớn,... Lúc này, việc tối ưu hóa tài nguyên khi sử dụng K6 là vô cùng quan trọng để đạt được hiệu suất tối ưu, tiết kiệm tài nguyên, mở rộng khả năng thực thi và đảm bảo sự ổn định trong quá trình thực hiện Performance Test. Đáng tiếc, nhiều người mới làm quen với công cụ này thường ít quan tâm đến vấn đề này. Vì vậy, trong bài viết này, mình sẽ chia sẻ những gợi ý và phương pháp để tối ưu hóa việc sử dụng tài nguyên khi thực hiện Performance Test bằng K6.
2. Giới thiệu về K6
Mình cũng đã có một bài viết giới thiệu tổng quan và có 1 chút so sánh về công cụ này, các bạn có thể tham khảo tại đây.
3. Các tùy chọn của K6 giúp giảm sử dụng tài nguyên
Dưới đây là một số tùy chọn cấu hình K6 giúp giảm chi phí hiệu suất khi chạy các bài kiểm tra lớn.
3.1. Tiết kiệm bộ nhớ với discardResponseBodies
Mặc định, K6 tải toàn bộ nội dung phản hồi của các yêu cầu vào bộ nhớ. Điều này có thể dẫn đến tiêu thụ bộ nhớ cao và điều này có thể sẽ không cần thiết trong một số trường hợp. Bạn có thể giải quyết vấn đề này bằng cách sử dụng tùy chọn discardResponseBodies
.
Ví dụ: Nếu bạn chỉ quan tâm đến thông tin header của phản hồi mà không cần lưu trữ phần body, bạn có thể thiết lập discardResponseBodies
trong đối tượng options
như sau:
export const options = { discardResponseBodies: true,
};
Trong trường hợp bạn đã bật tùy chọn discardResponseBodies
nhưng vẫn cần lưu trữ phần response body cho một số request cụ thể, lúc này bạn có thể ghi đè tùy chọn trên bằng Params.responseType
cho từng request. Ví dụ như sau:
http.get('https://example.com', { responseType: 'text', // accept response type is text or binary
})
Đối với các request có response body lớn thì tùy chọn này sẽ tối ưu được tài nguyên rất tốt do bộ nhớ sẽ không phải lưu trữ toàn bộ nội dung response body của các request nữa.
3.2. Khi truyền dữ liệu, hãy tắt --no-thresholds
và --no-summary
Nếu bạn đang chạy một bài kiểm tra và truyền kết quả lên một dịch vụ đám mây như K6 Cloud hay dịch vụ lưu trữ kết quả kiểm tra như Prometheus, bạn có thể tắt báo cáo tổng kết trên giao diện dòng lệnh và tính toán các ngưỡng. Điều này giúp tránh việc trùng lặp các hoạt động giữa máy cục bộ và máy chủ đám mây, từ đó tiết kiệm tài nguyên bộ nhớ và CPU.
Ví dụ: Khi chạy bài kiểm tra trực tiếp trên máy cục bộ và truyền kết quả lên đám mây, bạn có thể sử dụng các tùy chọn --no-thresholds
và --no-summary
. Dưới đây là một ví dụ về cách sử dụng cả hai tùy chọn này:
k6 run scripts/test.js \ -o cloud \ --vus=100000 \ --duration=10m \ --no-thresholds \ --no-summary
Đây cũng là một tùy chọn khá tối ưu khi bạn không quan tâm tới output của thresholds và báo cáo summary của bài test đó.
Bên cạnh 2 option trên, K6 cũng cung cấp 2 option --no-setup
và --no-teardown
, mặc định thì K6 sẽ chạy qua 2 hook setup()
và teardown()
nên bạn cũng có thể tắt chúng đi nếu không dùng tới.
4. Script optimizations
Để tận dụng tối đa hiệu suất phần cứng, hãy xem xét tối ưu mã của kịch bản kiểm thử. Một số tối ưu hóa này liên quan đến giới hạn cách bạn sử dụng một số tính năng của k6. Các tối ưu hóa khác là chung cho JavaScript.
4.1. Giới hạn các hoạt động tốn kém tài nguyên của K6
Một số tính năng của API K6 đòi hỏi nhiều tính toán để thực thi. Để tối đa hóa việc tạo tải, bạn có thể cần giới hạn cách tập lệnh của mình sử dụng những thông tin sau:
- Checks and groups: K6 sẽ ghi lại kết quả từng Check và Group riêng biệt. Nếu bạn đang sử dụng nhiều Check và Group, bạn có thể xem xét loại bỏ chúng để tăng hiệu suất.
- Custom metrics: Tương tự như Check, giá trị của các chỉ số tùy chỉnh (Trend, Counter, Gauge và Rate) được ghi lại riêng biệt. Hãy cân nhắc giảm thiểu việc sử dụng chúng nếu không cần thiết.
- Thresholds với
abortOnFail
: Nếu bạn đã cấu hình ngưỡngabortOnFail
, K6 sẽ cần liên tục đánh giá kết quả để xác minh rằng ngưỡng không bị vượt quá giới hạn của nó để thực hiện abort đúng lúc. Hãy xem xét loại bỏ cài đặt này nếu không cần thiết. - URL grouping: k6 phiên bản 0.41.0 giới thiệu một thay đổi để hỗ trợ time-series metrics. Tác dụng phụ của điều này là mỗi URL duy nhất tạo ra một đối tượng chuỗi thời gian mới, điều này có thể tiêu thụ nhiều RAM hơn dự tính. Để giải quyết vấn đề này, hãy sử dụng tính năng nhóm URL. Điều này sẽ giúp kết quả của các dynamic URLs sẽ gộp lại thành 1 kết quả duy nhất với tên mà bạn đặt cho URL đó.
Ví dụ URL grouping:
import http from 'k6/http'; export default function () { for (let id = 1; id <= 100; id++) { http.get(`http://example.com/posts/${id}`, { tags: { name: 'PostsItemURL' }, }); }
}
// tags.name=\"PostsItemURL\",
// tags.name=\"PostsItemURL\",
4.3. Tối ưu lifecycle
Trong vòng đời của một bài test trong k6, tập lệnh luôn chạy qua các giai đoạn trên với mục đích khác nhau. Bạn có thể thấy stage setup
được gọi khi bắt đầu thử nghiệm, sau giai đoạn init nhưng trước giai đoạn VU. Nó có nhiệm vụ dùng để khởi tạo data và share data đó cho các VUs trong bài test, và đặc biệt là K6 chỉ gọi tới setup
1 lần duy nhất xuyên suốt bài test. Như vậy thì sẽ tránh được việc k6 phải lặp lại các logic, request không cần thiết, giúp giảm lượng bộ nhớ tiêu thụ cho các logic, request đó.
Vậy nên với những request hay logic chỉ cần xử lý một lần duy nhất thì các bạn hãy xem xét đưa vào setup
xử lý, ví dụ như sau:
import http from 'k6/http'; export function setup() { const res = http.get('https://httpbin.test.k6.io/get'); return { data: res.json() };
} export function teardown(data) { console.log(JSON.stringify(data));
} export default function (data) { console.log(JSON.stringify(data));
}
Như ví dụ trên, k6 sẽ chỉ request tới https://httpbin.test.k6.io/get
một lần và trả về data cho default function sử dụng. Với các trường hợp cụ thể, bạn hãy thử ứng dụng cách này xem sao nhé!
4.2. Tối ưu hóa JavaScript
Cuối cùng, nếu các gợi ý trước đó không đủ, bạn có thể thực hiện một số tối ưu hóa JavaScript chung:
- Tránh sử dụng vòng lặp lồng nhau quá sâu.
- Tránh tham chiếu đến các đối tượng lớn trong bộ nhớ càng ít càng tốt.
- Giữ số lượng external JS dependencies ở mức tối thiểu.
- Loại bỏ code, module không sử dụng trong script.
Tham khảo bài viết về garbage collection trong V8 runtime. Mặc dù máy ảo JavaScript mà k6 sử dụng rất khác biệt và chạy trên Go, các nguyên tắc chung vẫn cần được áp dụng. Lưu ý rằng rò rỉ bộ nhớ vẫn có thể xảy ra trong các kịch bản k6 và nếu không được khắc phục, chúng có thể tiêu thụ RAM nhanh hơn rất nhiều.
5. Bài toán upload file
Thông thường đối với các trường hợp cần upload file trong K6 sẽ tiêu tốn rất nhiều tài nguyên, chưa kể khi chạy với lượng VUs lớn hay file được upload có dung lượng lớn thì rất chắc kèo là sẽ gặp tình trạng tràn memory. Tại sao lại gặp tình trạng như vậy? Đầu tiên cần nói tới cách mà K6 sẽ xử lý khi mở một file, ví dụ có đoạn K6 script sau sẽ thực hiện mở file và sử dụng file đó trong các request:
import { sleep } from 'k6'
import http from 'k6/http' export const options = { thresholds: { http_req_duration: ['p(95)<1000'], }, scenarios: { constant_vus: { executor: 'constant-vus', gracefulStop: '30s', duration: '5m', vus: 100, exec: 'constant_vus', }, },
} const binFile = open('/path/to/file.bin', 'b'); export function constant_vus() { const data = { field: 'this is a standard form field', file: http.file(binFile, 'test.bin'), }; const res = http.post('https://example.com/upload', data); sleep(3);
}
Ở ví dụ trên, mình cho 100 VUs sẽ thực hiện request upload file binary lên server. Có thể quan sát ngay document về hàm open
của K6:
open( filePath, [mode] )
Opens a file, reading all its contents into memory for use in the script.
Thằng open()
sẽ thường tiêu tốn một lượng lớn bộ nhớ vì mỗi VU trong kịch bản sẽ giữ một bản sao riêng của tệp trong bộ nhớ. Tức là khi chạy VU càng lớn thì bộ nhớ sẽ phải lưu trữ càng nhiều bản sao của tệp cho tất cả VUs sử dụng. Giả sử upload file 10MB thì 100 VU sẽ chiếm 1GB để lưu trữ file.
Vậy làm cách nào để giảm mức tiêu thụ bộ nhớ khi open file? Lúc này chúng ta sẽ cần sử dụng SharedArray
. Hàm SharedArray
sẽ chỉ thực hiện một lần và kết quả của nó được lưu vào bộ nhớ một lần thay vì bao nhiêu VUs sẽ open và tạo bản sao của file bấy nhiêu lần. Khi tập lệnh yêu cầu một phần tử, k6 sẽ cung cấp một bản sao của phần tử đó.
Ví dụ mình có 1 file users.json
như sau:
[ { "username": "user1", "password": "password1" }, { "username": "user2", "password": "password2" }, { "username": "user3", "password": "password3" }
]
Lúc này trong file script K6, mình sẽ open file json trên trong SharedArray thay vì ở bên ngoài:
import { SharedArray } from 'k6/data';
import { sleep } from 'k6'; const data = new SharedArray('users', function () { const f = JSON.parse(open('./users.json')); return f; // f must be an array[]
}); export default () => { const randomUser = data[Math.floor(Math.random() * data.length)]; console.log(`${randomUser.username}, ${randomUser.password}`); sleep(3);
};
Ở trên, mình sử dụng file json khá nhỏ, trong thực tế có thể là file json hoặc file csv nặng từ vài MB đến vài chục MB. Bạn hãy thử chạy 2 trường hợp sử dụng SharedArray và ngược lại để xem sự khác biệt về tài nguyên mà nó tiêu thụ nhé!
6. Tổng kết
Chúng ta đã khám phá các phương pháp tối ưu hóa cấu hình và script của K6 để giảm tài nguyên tiêu thụ trong quá trình chạy performance test với K6. Tuy nhiên, trong thực tế, có thể có yêu cầu về số lượng VUs hoặc dung lượng file lớn hơn rất nhiều, và mỗi hệ thống cung cấp lượng tài nguyên khác nhau cho quá trình test. Do đó, nếu các phương pháp trước vẫn chưa giải quyết hoàn toàn vấn đề tài nguyên, bạn có thể xem xét nâng cấp tài nguyên của hệ thống.
Chúc các bạn thành công 😉