Load Testing with Artillery.io

Long time no see !! Chào mừng các bạn đến với phần tiếp theo trong Performance Tool Series: Artillery.io. Nhìn chung thì đây là một performance tool khá thú vị được viết bằng Node.js, và đồng thời nó được xem khá là nhẹ so với các tool đồng trang lứa. Trong bài này mình sẽ chia sẻ về những tính năng của tool mà mình đã xài trong project thực tế, kèm theo đó là các tips và các hạn chế của tool. Hi vọng bài viết này sẽ cung cấp thêm cho các bạn một option mới khi cân nhắc chọn performance tool.

Các bạn có thể tham khảo phần trước ở link sau.

Artillery.io?

Search google nhớ ghi là “artillery.io” nguyên văn nhé không là nó ra hình súng thần công như trên.

Về cơ bản, Artillery cũng là một load generator như các tool khác đồng trang lứa khác như Apache JMeter, Locust, etc, chuyên dụng cho việc load testing. Tuy nhiên có một số điểm tạo nên sự khác biệt so với các tool còn lại, đây đồng thời cũng là những điểm mà các bạn cần cân nhắc trước khi chọn tool này làm trong project thực tế:

  • Pros:
    • Không có giao diện windows 2000 như của JMeter. Mọi thứ đều thông qua command line interface và text editor cho nên rất là nhẹ.
    • Open source.
    • Test viết bằng yaml/jsonnode.js.
    • Có hỗ trợ load test cho socket.io.
    • Có cơ chế plugin để phục vụ cho các nhu cầu fck tạp. Một số plugin có sẵn như DataDog, Influx, StatsD dùng để gửi dữ liệu real-time vào các real-time database.
    • Có version PRO….. Mình chưa có điều kiện xài cái này nhưng theo document của tool thì ở version này mình có thể chạy test ở distributed mode, bạn nào làm performance nhiều chắc cũng nghe qua khái niệm này rồi.
  • Cons:
    • Cái socket.io nói support vậy chứ một vài trường hợp đòi hỏi phải workaround mới có thể chạy được.
    • Ít performance metrics (từ này các bạn có thể hiểu là những con số thống kê, dựa vào nó mà một người tester có thể phân tích được tình trạng của system mình – ví dụ như số lượng requests trên 1s, trung bình của response time) nếu so với các tool khác.
    • Trường hợp các bạn muốn load test kiểu phức tạp như chạy test với tỉ lệ 6:3:1 cho 1000 users, hay muốn giữ cho throughput của system ở một mức nhất định thì tool chưa support cho mấy chuyện này (nhưng muốn thì vẫn làm được).

Cài đặt

Với những bạn chưa cài node.js thì vào link sau để cài: https://nodejs.org/en/download/

Cài xong rồi thì mở command line và run lệnh sau:

npm install -g artillery

Run lệnh kế để check version, nếu hiện ra version là quá trình cài đặt thành thụ:

artillery -V


        ___         __  _ ____                  _
  _____/   |  _____/ /_(_) / /__  _______  __  (_)___  _____
 /____/ /| | / ___/ __/ / / / _ \/ ___/ / / / / / __ \/____/
/____/ ___ |/ /  / /_/ / / /  __/ /  / /_/ / / / /_/ /____/
    /_/  |_/_/   \__/_/_/_/\___/_/   \__, (_)_/\____/
                                    /____/

------------ Version Info ------------
Artillery: 1.6.0-29
Artillery Pro: not installed (https://artillery.io/pro)
Node.js: v10.15.3
OS: darwin/x64
--------------------------------------

Viết Test

Test trong artillery thông thường sẽ bao gồm 2 phần chính: các file test scenario (*.yml) và các file JS (.js). Mình sẽ giải thích detail từng phần thông qua các code mẫu (hàng thiệt phiên bản cắt xén) dưới đây.

Test Scenario Files

Một test scenario file bao gồm hai phần: configuration and scenarios

Configuration

Trong phần này, bạn sẽ chỉ định bao nhiêu virtual users sẽ được tạo ra trong load test của bạn và các tuỳ chỉnh như dùng plugin nào, định nghĩa biến, chỉ định đường dẫn tới JS file, bla bla….

  • config: đây là keyword bắt buộc để đánh dấu phần configuration của test scenario file.
  • target: URL của application. Mấy bạn đừng thêm dấu “/” vào cuối nhé, không là lúc run nó báo lỗi tè le.
  • processor: đường dẫn tương đối tới file JS.
  • plugins: ở phần trước mình có đề cập về plugin trong artillery, thì đây là cách mà mình chỉ định plugin nào sẽ xài, ở đây mình xài plugin gọi là faker. Một điểm cần lưu ý ở đây là trước khi bạn muốn xài plugin nào thì bạn phải install plugin đó thông qua npm. Ví dụ với plugin faker trong sample là : npm install -g artillery-plugin-faker
  • variables: định nghĩa biến. Xài bằng cách {{ tên biến }}, ví dụ {{ timeout }}.
  • payload: chỉ định csv file. Thường test data của các bạn nhiều thì sẽ bỏ nó trong csv file và test sẽ đọc csv file và loop trên đó. Trong sample dưới đây thì file csv của mình có 1 cột và mình lưu data trong cột đó vào biến email.
  • phases: define số lượng virtual users sẽ được tạo ra. Mình lấy ví dụ bên dưới là test của mình chia làm 2 phase (bạn có thể tạo nhiều phase): phase đầu tiên kéo dài trong 5s (duration) và cứ mỗi giây là 1 virtual user (arrivalRate) được tạo ra, tương tự ở phase sau test sẽ kéo dài 60s và cứ mỗi giây có 20 virtual user được tạo. Bạn có thể tham khảo thêm các option khác ở link này https://artillery.io/docs/script-reference/#load-phases.
config:
  target: http://127.0.0.1:7000
  processor: "../processors/console_proc.js"
  plugins:
    faker: {}
  variables:
    password: "123456789"
    prefix: "QA"
  payload:
  - path: "../data/QA_accounts.csv"
    fields:
      - "email"
  phases:
      - duration: 5
        arrivalRate: 1
        name: "Init phase"
      - duration: 60
        arrivalRate: 20
        name: "Intense phase"

Scenarios

Phần này chứa các action mà một virtual user sẽ làm, nói dễ hiểu ví dụ như hành động mua hàng, đặt sách, chuyển tiền, bla bla…. Ở mặt technical thì nó là chuỗi các request. Một file có thể có một hoặc nhiều scenario. Scenario được pick random dựa trên weight mà bạn input vào.

  • name: tên của scenario. Có thể có nhiều name trong một file, mỗi name đại diện cho 1 scenario.
  • weight: đây là trọng số của scenario, scenario nào có weight càng cao càng dễ xảy ra hơn. Giá trị mặc định của weight là 1, tức khả năng xảy ra của các scenario như nhau.
  • flow: chứa chuỗi các HTTP requests, hay dễ hiểu là chuỗi action của virtual user.
  • Đống còn lại chắc các bạn cũng biết nó là gì nếu bạn đã quen với HTTP request và response, mình chỉ giải thích về các syntax đặc biệt như
    • beforeRequest: hàm này sẽ chạy trước khi request được send. Đây là một trong các hooks được cung cấp bởi Artillery.
    • capture: dùng để store response trả về và lưu vào biến tên là response, biến này có thể dùng cho các request kế đó hoặc cho các hàm sau đó.
    • transform: operation cho cái biến được capture. Ở đây mình cộng chuỗi từ JWT với caí token mình capture được.
...
scenarios:
  - name: "Create a bot"
    weight: 1
    flow:
      - post: 
            url: /auth/sign_in
            json:
              email: "{{ email }}"
              password: "{{ password }}"
            beforeRequest: hashPassword
            capture:
              json: "$.jwtToken"
              transform: "`JWT ${this.token}`"
              as: "token"
      - think: 5
      - post:
            url: /bot/create
            headers:
              Authorization: "{{ token }}"
            json:
              botName: "{{ prefix }}_{{ botName }}"
              botWebsite: "https://artillery.io/"
              zoneName: "Asia/Bangkok"

JavaScript Files

Ở phần trên có một đoạn trong scenario file mình dùng để chỉ định đường dẫn tới JS file: processor: "./laura.js". File này chứa custom code viết bằng node.js. Do nhu cầu đa dạng nên hầu như lúc nào các bạn cũng sẽ dùng tới các file này. Trong file này bạn chú ý cho mình một vài tham số sau, còn tại sao nó nhận vào những tham số như vậy là do syntax của Artillery bắt buộc thế.

  • requestParams/response: dùng biến này để truy xuất vào header, body, v.v.. của request/response. Chi tiết hơn về thao tác trên request object, mấy bạn xem ở link sau: https://github.com/request/request
  • context: mỗi virtual user có một context riêng, context chứa tất cả các variables của từng virtual user. Mấy bạn chưa từng làm performance testing thì có thể hiểu context là khái niệm mà các performance tool dùng để phân biệt giữa các virtual users với nhau, tránh trường hợp VU này xoá nhầm hàng của VU khác chẳng hạn.
  • ee: Event Emitter. Ở một trường hợp nhất định, bạn có thể dùng biến này communicate với runner của artillery.
  • next: hàm callback của artillery, bắt buộc phải có để scenario file có thể chạy tiếp.
 module.exports = {
    saveUserToFile: saveUserToFile,
    hashPassword: hashPassword
}

const crypto = require('crypto');
var fs = require('fs');

function saveUserToFile(requestParams, response, context, ee, next) {
    if (response.statusCode === 200) {
        appendToFile(`./data/${context.vars.prefix}_accounts.csv`, response.body.email);
    }
    else {
        appendToFile(`./data/${context.vars.prefix}_accounts_err.csv`,
            `${requestParams.json['email']},${JSON.stringify(response.body)}`);
    }
    return next();
}

function appendToFile(filename, payload) {
    fs.appendFile(filename, payload + '\n', function (err) {
        if (err) throw err;
    });
}

function hashPassword(requestParams, context, ee, next) {
    requestParams.json['password'] = crypto.pbkdf2Sync(context.vars.password, new Buffer('', 'hex'), 100, 256, 'sha256').toString('hex');
    return next();
}

Cách dùng trong scenario files

  • Dùng dưới dạng hook.
  # ... a request in a scenario definition:
  - post:
      url: "/api/signin"
      beforeRequest: "setCredentials"
      afterResponse: "getJWTToken"
  • Dùng như function step
scenarios: 
  - name: "Chat" 
    engine: "socketio"
    flow:   
      - function: initConversation
      - think: 3
      - loop:
        - function: chat
        - think: 3
        count: 10
      - think: 10

Chạy test và phân tích kết quả

Khi code xong rồi thì còn đợi gì nữa mà không chạy thử xem thơm không :))). Có 2 cách để bạn chạy test

  • Chạy trực tiếp bằng command line
artillery run athenka.yml
  • Chạy thông qua node.js code.
const artillery = require('artillery');
const path = require('path');
const read = require('read-yaml');
var baseDir = path.basename(__dirname);
var scriptPath = `..\\${baseDir}\\Scripts\\${process.argv[2]}`;
var environment = process.argv[3]||'qa';
var fileName = path.basename(scriptPath, path.extname(scriptPath));
var scriptConfig = read.sync(scriptPath);
var phaseInfo;
var options;
if (scriptConfig.config.phases[0].arrivalRate) {
    phaseInfo = `${scriptConfig.config.phases[0].arrivalRate}-Rate_In-${scriptConfig.config.phases[0].duration}s`;
}
else {
    phaseInfo = `${scriptConfig.config.phases[0].arrivalCount}-Count_In-${scriptConfig.config.phases[0].duration}s`;
}

artillery.run(scriptPath, options);

Sau đó mở cmd lên và gõ:

node athenka.js

Bất kể bạn run bằng kiểu gì thì nó cũng sẽ hiển thị tương tự như hình dưới, toàn số là số do cái này không có giao diện, những số này cứ mỗi 10s sẽ update lại.

  • Scenarios launched: tổng số VU.
  • Scenarios completed: tổng số VU đã hoàn thành session.
  • Requests completed: tổng số request (HTTP, sockets) đã send.
  • RPS sent: số lượng request trung bình mỗi giây.
  • Request latency (milliseconds): nếu các bạn đã nghe về percentile (google đi bro) thì sẽ biết p95, p99 nghĩa là gì.
  • Codes: HTTP response code.
Complete report @ 2019-01-02T17:32:36.653Z
  Scenarios launched:  300
  Scenarios completed: 300
  Requests completed:  600
  RPS sent: 18.86
  Request latency:
    min: 52.1
    max: 11005.7
    median: 408.2
    p95: 1727.4
    p99: 3144
  Scenario counts:
    0: 300 (100%)
  Codes:
    200: 300
    302: 300

Bonus 1: Good Practices

Có một vài cái practices nho nhỏ về Artillery mình muốn share cho mấy bạn ở đây để khỏi phải đạp shit sau này.

  • Tin buồn là rồi sẽ tới lúc các bạn sẽ run test trên nhiều môi trường khác nhau, tin vui là Artillery có hỗ trợ 2 cách để quản lý cũng như chạy test trên nhiểu environment khác nhau.
    • Cách thứ nhất là xài magic syntax environment trong phần config của scenario file. Cách này bạn có thể dễ dàng tách biệt config của các môi trường với nhau. Config đơn giản thì ổn nhưng về sau config phức tạp file này no to đùng lên khó quản lý lắm. Điểm lợi của cách này là nó cho bạn quyền truy cập vào biến {{ $environment }}.
    • Cách thứ hai là xài một file config riêng, khi dùng cmd để run test nó có một option để bạn chỉ định đường dẫn tới file config nằm ngoài scenario file, về syntax thì y chang. Cách này các bạn dễ quản lý config của mình hơn, nhưng không truy cập được vào biến {{ $environment }} như cách trên.
# Reuse the same config to run two different scenarios:

artillery run --config common-config.yaml my-scenario.yaml
artillery run --config common-config.yaml another-scenario.yaml
  • Tuy Artillery cho phép mình bỏ nhiều scenario trong một scenario file nhưng bạn sẽ không control được số lượng virtual users cho từng scenario của file đó cho nên practice ở đây là nên tách ra mỗi scenario file một scenario thôi.
  • Một trường hợp mà mình cũng hay thường thấy của các bạn mới bắt đầu làm là ghi thẳng những thông tin nhạy cảm vào test luôn (e.g. secret key) rồi push lên github chơi <(“). Các bạn có thể ngăn trường hợp này xảy ra bằng cách dùng một magic syntax khác là processEnvironment, lúc này bạn không cần để mấy info nhạy cảm trong test nữa mà chỉ cần truyền vào lúc run test thôi.

Bonus 2: Artillery.io with Grafana

Làm performance testing chỉ xem bằng command line thôi thì nhiều vấn đề bị che khuất lắm. Do đó Artillery có support việc send những data (scenario launched, completed, RPS, v.v…) lên các real time database thông qua plugin, khi có data rồi mình có thể dùng một tool nào đó để vẽ nhưng con số thành những chart để dễ monitor hơn. Tool mình từng dùng để monitor là Grafana và plugin mình dùng là influx plugin.

Mình chỉ giới thiệu cho các bạn vậy thôi chứ không provide steps by steps :D, nói chứ dễ lắm, bạn chỉ cần set up được cái real time database và cái Grafana server là xong (dùng docker xí là xong ấy mà). Việc còn lại chỉ tìm và cài influxdb plugin cho Artillery và làm theo hướng dẫn.

Hết Artillery.io basic

Vậy là các bạn đã biết sương sương cách xài Artillery rồi. Theo cá nhân mình thấy tool này tương đối dễ xài cho một người mới bắt đầu (giống mình) làm performance testing mặc dù không có giao diện.

Một phần quan trọng mà mình không đề cập là cách chạy distributed trong Artillery, lý do là vì mình chưa có cơ hội xài nó và nó thuộc bản PRO. Ngoài ra còn một cách khác nữa nhưng phải dùng đến K8S hoặc Ansible, nếu có điều kiện thì mình sẽ thử và share với các bạn ở phần sau. See ya !!

P/S: code các bạn có thể vào repo này để lấy sample về nghiền ngẫm nhé.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s