Home Featured k6 เครื่องมือสำหรับทำ Performance Testing

k6 เครื่องมือสำหรับทำ Performance Testing

by khomkrit

k6 คือเครื่องมือที่ช่วยให้เราสามารถทำ load testing ได้อย่างสนุกสนาน โดยสิ่งที่น่าสนใจของมันก็คือเราสามารถเขียน script ด้วย JavaScript ES2015/ES6 แถมยังใช้ local/remote modules ต่างๆ ได้อีกด้วย อีกทั้งยังมีเครื่องมือที่ช่วยให้เรา validate ค่าต่างๆ และ วัดผลตามเป้าที่เราตั้งไว้ได้อีกด้วย

หลังจากติดตั้ง และลองรันคำสั่ง k6 ที่ CLI แล้วพบว่าไม่มีอะไรผิดปกติแล้ว ก็มาดูกันว่ามันถูกใช้งานประมาณไหน

ถึงแม้จะเป็น JavaScript แต่ K6 ก็ไม่ได้รันใน browser และไม่ได้รันใน NodeJS

โครงสร้างไฟล์เมื่อใช้ k6

ประกอบไปด้วยส่วนหลักส่วนเดียว คือ default function ที่เราต้องการให้ทำงานตอนเราสั่งรัน โดยเนื้อหาหลักในฟังก์ชั่นนี้ที่เราจะลองใช้กันก็คือการส่ง request ไปยัง API นั่นเอง

export default () => {
  console.log('Hello World')
}

สมมติเราตั้งชื่อไฟล์นี้ว่า script.js เราสามารถสั่งรันได้ดังนี้

k6 run script.js

HTTP Request

ลองส่ง GET Request ไปยัง API

import http from 'k6/http'
export default () => {
    http.get('http://httpbin.org/get')
}

ส่ง request ที่ซับซ้อนขึ้นด้วยการกำหนด Request body และ header

import http from 'k6/http';
export default function() {
  var body = JSON.stringify({
    email: 'aaa',
    password: 'bbb',
  });
  var params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };
  http.post('http://httpbin.org/post', body, params);
}

นอกจากนี้เรายังสามารถอ้างถึง Response ที่ตอบกลับมาได้อีก เช่น

const res = http.post('http://httpbin.org/post', payload, params);
console.log(res.body)
console.log(res.status)

หลังจากที่เรารู้จักการส่ง HTTP Request แล้ว ต่อไปเราจะตั้งค่าการจำลองการส่ง HTTP Request โดยการกำหนดค่าต่างๆ ให้กับ options

options

k6 ใช้ Options ในการกำหนดพฤติกรรมว่าจะต้องรัน test แบบไหน ในเบื้องต้น options ที่น่าสนใจมีดังนี้

vus

ใช้จำลองว่ามี ผู้ใช้ หรือ Virtual User (k6 ใช้ตัวย่อว่า VUs) เรียก API เรากี่คน โดยแต่ละ VU ที่ถูกสร้างขึ้นมาจะรัน default function ให้เราตลอดการทดสอบ

duration

ใช้จำลองว่ามีคนเข้าเว็บเราเป็นเวลาเท่าใด ค่านี้คือเวลาที่ test run ทำงานรวมกันทั้งหมด

จากทั้ง 2 options ที่ยกมา หากเราต้องการจำลองว่ามีคน 10 คนใช้งานเว็บเราเป็นเวลา 30 นาที เราก็จะกำหนด options ได้ดังนี้

export let options = {
  vus: 10,
  duration: '30m',
}

หลังจากกำหนด option แล้ว ก่อนรัน script ใหม่อีกรอบ เราอาจต้องกำหนด delay ไว้ก่อนที่จะเริ่มส่ง request รอบใหม่ด้วย ไม่อย่างนั้นก็จะเสมือนว่า ผู้ใช้ส่ง requet รัวๆ (กด F5 รัวๆ) โดยไม่หยุดอ่าน content อะไรของเราเลย

ก่อนส่ง request ต่อไป เราสามารถกำหนด delay ด้วยคำสั่ง sleep มีหน่วยเป็นวินาที ดังนี้

import { sleep } from 'k6'
export default () => {
  // ... 
  http.post(url, body, params)
  sleep(1)
}

โดย default แล้วหากเราสั่งรัน script ดังที่ยกตัวอย่างมาตอนต้น ผลลัพธ์จะ print ออกมาที่ stdout เท่านั้น แต่หากเราต้องการเก็บรายละเอียดในการรันตลอดระยะเวลาที่ทดสอบ (ตลอด 30 นาทีนั้น) ให้ใช้ option --out และหากเราต้องการสรุปแบบเดียวกับที่แสดงออกมาที่ stdout เป็นไฟล์ด้วย ให้ใช้ option --summary-export ตอนสั่งรัน script ดังนี้

k6 run \
  --out json=output.json \
  --summary-export=summary-export.json \
  script.js

เมื่อรันเสร็จแล้วเราจะได้สรุปการรันออกมาดังนี้

k6 output
k6 output

นี่คือสรุปภาพรวมของผลการทดสอบเท่านั้น ซึ่งค่าที่แสดงออกมาทาง stdout นี้ก็คือค่าในไฟล์ summary-export.json ส่วนรายละเอียดการทดสอบตลอดช่วงเวลานั้นอยู่ในไฟล์ output.json ตามที่เราได้กำหนดค่าให้ไปตอนรัน

stages

ใช้จำลองจังหวะการใช้งานของ VU ประกอบไปด้วย 2 ส่วนคือ duration สำหรับระบุระยะเวลา และ target สำหรับระบุจำนวน VU ยกตัวอย่างเช่น เราต้องการจำลองว่ามีผู้ใช้ตั้งแต่ 1-10 คนใช้งานอยู่เป็นเวลา 3 นาที จากนั้นคงที่ไว้ที่ 10 คนเป็นเวลา 5 นาที และเพิ่มเป็น 35 คนในเวลา 10 นาที จากนั้นลดลงเป็น 0 คนในเวลา 3 นาที เราก็จะกำหนด options ได้ดังนี้

export let options = {
  stages: [
    { duration: '3m', target: 10 },
    { duration: '5m', target: 10 },
    { duration: '10m', target: 35 },
    { duration: '3m', target: 0 },
  ],
}

เนื้อหาในสรุปที่เห็นใน stdout นั้นประกอบไปด้วย 3 เรื่อง ได้แก่

  1. Metrics
  2. Checks, Thresholds
  3. Groups, tags

เริ่มจาก Metrics กันก่อน

Metrics

Metrics อยู่ใน k6/metrics module แบ่งเป็น 2 แบบ แบบแรกคือ built-in metrics ข้อมูลส่วนใหญ่ในสรุปผลการรันที่เราดูจากชื่อก็น่าจะพอเดาได้ว่าแต่ละ metric คืออะไร เช่น

  • iterations – บอกจำนวนรอบในการทดสอบทั้งหมด
  • iteration_duration – บอกเวลาที่แต่ละรอบของการรันใช้
  • http_req_waiting – บอกเวลาที่รอ server ตอบกลับ
  • data_received – บอกปริมาณข้อมูลทั้งหมดที่เราได้รับมาตลอดระยะเวลาการทดสอบ

แบบที่สองคือ custom metrics ซึ่งก็คือ metrics ที่เราสามารถสร้างขึ้นมาเองได้ แบ่งออกเป็น 4 ชนิด ได้แก่

  1. Counter – ใช้สำหรับเก็บรวบรวมค่าไปเรื่อยๆ ตลอดการทดสอบ
  2. Gauge – ใช้เก็บค่าสุดท้ายที่อ่านได้
  3. Trend – เก็บค่าต่างๆ และคำนวนออกมาเป็นสถิติแบบเดียวกับ built-in http_req_ metrics
  4. Rate – หาอัตราส่วนของค่าที่ไม่ใช่ 0 หรือไม่ใช่ false

ลองดูวิธีใช้งานและผลลัพธ์จากตัวอย่างต่อไปนี้ เทียบกับคำอธิบายดังกล่าวจะช่วยให้เราเห็นแนวทางการนำ metric แต่ละชนิดมาใช้งานได้ชัดเจนขึ้น

import { Counter, Gauge, Rate, Trend } from 'k6/metrics'
let counter = new Counter('my_counter')
let guage = new Gauge('my_guage')
let trend = new Trend('my_trend')
let rate = new Rate('my_rate')
export default () => {
  counter.add(1)
  counter.add(2)
  counter.add(3)
  guage.add(1)
  guage.add(30)
  guage.add(10)
  trend.add(1)
  trend.add(2)
  trend.add(3)
  rate.add(1)
  rate.add(1)
  rate.add(0)
}

ผลลัพธ์ของ metrics แต่ละแบบ

k6 metrics result
k6 metrics result

Checks

check ทำหน้าที่คล้าย assert คือใช้ตรวจค่าว่าเป็นไปตามที่เราต้องการหรือไม่ จากนั้น return ผลลัพธ์ออกมา ให้เราเอาไปใช้งานต่อได้ เช่นเอาไปใส่ใน custom metrics เป็นต้น

import { check } from 'k6'
import http from 'k6/http'
import { Rate } from 'k6/metrics'
const errorRate = new Rate('error_rate')
export default () => {
  const res = http.get('http://httpbin.org/json')
  const passed = check(res, {
    'status is 200': r => r.status === 200,
    'has slideshow.title': r => r.json()["slideshow"]["title"] !== undefined
  })
  errorRate.add(!passed)
}

จากโค้ดที่ยกมา เราจะเห็นการใช้งาน check ที่รับ parameter เป็น HTTP Response และโค้ดที่ใช้ในการตรวจสอบ response ซึ่ง check จะ return ผลลัพธ์ออกมาเป็น true ถ้าทุกการตรวจสอบที่กำหนดไว้ถูกต้องทั้งหมด

Thresholds

Threshold คือมาตรวัด ว่าสรุปผลการทดสอบผ่านหรือไม่ผ่านกันแน่ โดยเราจะดูว่าผ่านหรือไม่ผ่านโดยอิงกับ metrics ที่เราตั้งไว้เป็นเป้าในการวัด เช่น

  • error rate ต้องไม่เกิน 1%
  • 95% ของ response time ต้องใช้เวลาไม่เกิน 500ms

เราสามารถกำหนด thresholds ได้ที่ options ดังนี้

export const options = {
  thresholds: {
    'error_rate': ['rate<0.1'],
    'http_req_duration': ['p(95)<500']
  }
}

จากโค้ดที่ยกมา error_rate คือ custom metric ที่เราสร้างขึ้นมาเอง เป็น metric ชนิด Rate

เมื่อเราลองใช้ทั้ง check และ treshold ในผลสรุปการรันจะรวมผลการทดสอบของทั้ง 2 ไว้ด้วย ดังนี้

k6 check treshold

Test Types

การทำ Load testing แต่ละแบบ เราอาจทดสอบโดยใช้ script เดียวกันได้เลย ซึ่งแต่ละ script จะต่างกันแค่การกำหนดค่าใน options เท่านั้น

Smoke Testing

เราใช้ Smoke Test เพื่อทดสอบว่าโดยทั่วไประบบของเรานั้นยังปกติดีไหม และยังใช้ทดสอบอีกว่า test script ของเรานั้นรันได้ถูกต้องเท่านั้น ดังนั้นจึงไม่จำเป็นต้องกำหนด VU หรือ duration ให้มากมายนักก็ได้

// 1 user looping for 1 minute
export let options = {
  vus: 1,
  duration: '1m',
  thresholds: {
    'http_req_duration': ['p(99)<1500'],
  }
};

Load Testing

ใช้ทดสอบ performance ของระบบว่าสามารถรองรับการใช้งานตามที่เราตั้งเป้าหมายไว้หรือไม่ เช่น เราคาดว่าระบบที่เราออกแบบมาต้องรองรับการใช้งานพร้อมๆ ได้ประมาณ 100 คน หากไม่สามารถรองรับการใช้งานในภาวะปกติได้ตามที่คาดไว้ เราจะได้ tune ระบบได้ก่อนปล่อยไปยัง production

export let options = {
  stages: [
    { duration: "5m", target: 100 }, 
    { duration: "10m", target: 100 }, 
    { duration: "5m", target: 0 }, 
  ],
  thresholds: {
    'http_req_duration': ['p(99)<1500'], 
    'logged in successfully': ['p(99)<1500'], 
  }
};

ตอนทำ Load Tests เราควรกำหนด ramp-up stage เสมอ เพื่อเปิดโอกาสให้ระบบของเราวอร์มอัพ หรือ auto scale ได้ทัน อีกทั้งยังให้เราได้เห็นภาพตอนที่โหลดยังไม่เยอะไต่ขึ้นไปตอนที่โหลดเยอะแล้วอีกด้วย

ซึ่งการ ramp-up นั้น เราควรเริ่มจากความเข้มข้นน้อยๆ แล้วค่อยๆ เพิ่มปริมาณความเข้มข้นของการทดสอบไปทีละนิดๆ เพื่อป้องกันไม่ให้ระบบของเราล่มไปทันทีซึ่งนั้นจะเปลี่ยนจาก load test จะกลายเป็น stress testing ไปซะได้

Stress testing

ทดสอบเพื่อดูในเรื่อง availability และ stability ตอนที่ได้รับโหลดเยอะๆ เป็นหลัก หา limit ของระบบเรา และหาคำตอบว่าจะเกิดอะไรขึ้นถ้าเราดันขึ้นไปถึง limit ของระบบเราแล้ว อีกทั้งยังใช้ดูด้วยว่าระบบเราจะกลับมาปกติได้หรือไม่หลังจากจบการทดสอบไปแล้ว

นั่นก็แปลว่าเมื่อเราทำ stress test เราก็จะกำหนดให้เป็นค่าที่มันเกินกว่าค่าที่มันควรจะเป็นค่าปกติ แต่นั่นก็ไม่ได้หมายถึงว่าเราจะโผล่มาแล้วใช้ค่าสูงๆ เลยทีเดียวไม่อย่างนั้นเราจะเรียนว่า spike test แต่เรายังต้องค่อยๆ ramp-up ขึ้นไปเรื่อยๆ เช่นเดียวกับ load test อยู่ เพียงแต่เราจะดันให้มันเกิดนจุดปกติที่ระบบจะรับได้

ตัวอย่างคลาสสิคที่เอาไว้ทดสอบ stress test ก็พวก event ของวันสำคัญๆ ต่างๆ ที่คนจะเข้ามาใช้งานเว็บเราอย่างล้นหลาม เป็นต้น

ยกตัวอย่างเช่น ระบบเรารองรับโหลดได้ปกติที่ 200 VUs และจะถึง limit แถวๆ 400 VUs เราก็ config ได้ดังนี้

export let options = {
  stages: [
    { duration: '2m', target: 100 }, // below normal load
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 }, // normal load
    { duration: '5m', target: 200 },
    { duration: '2m', target: 300 }, // around the breaking point
    { duration: '5m', target: 300 },
    { duration: '2m', target: 400 }, // beyond the breaking point
    { duration: '5m', target: 400 },
    { duration: '10m', target: 0 }, // scale down. Recovery stage.
  ],
};

Spike Testing

ทดสอบในกรณีที่มีคนเข้าเว็บเราเยอะๆ ในระยะเวลาสั้นๆ เช่น มีโฆษณาสาธาณะแบบ realtime ที่เมื่อคนเห็นแล้วจะต้องรีบเปิดเว็บเราเข้ามาทันที คนก็จะแห่เข้าเว็บเราจำนวนมากๆ พร้อมๆ กัน หรือมีคนดังใน social network ที่มี follower หลักแสนคน เอา link โปรโมชั่นขายของของเว็บเราไปแชร์ ทำให้ follower กดเข้า link นี้พร้อมๆ กัน ซึ่งการ config ให้สอดคล้องกับสถานการณ์ที่ยกมา สามารถทำได้ดังนี้

export let options = {
  stages: [
    { duration: '10s', target: 100 }, // below normal load
    { duration: '1m', target: 100 },
    { duration: '10s', target: 1400 }, // spike to 1400 users
    { duration: '3m', target: 1400 }, // stay at 1400 for 3 minutes
    { duration: '10s', target: 100 }, // scale down. Recovery stage.
    { duration: '3m', target: 100 },
    { duration: '10s', target: 0 },
  ],
};

Soak Testing

หลังจากที่ระบบของเรารองรับคนได้ตามเป้าหมายที่คาดหวังแล้ว และทนต่อการ request จนเกิน limit ได้เป็นที่น่าพอใจ เราก็จะมาทำ soak testing กัน

soak testing เอาไว้ใช้หาปัญหาที่จะเกิดขึ้นจากการที่ระบบเราทำงานไปนานๆ พักหนึ่งแล้ว ปัญหาเล่านี้มักเกี่ยวกับ memory, storage, การจัดการและขนาด log ที่ไม่สมเหตุสมผล หรือ bug ของระบบเราที่เกิดจากการทำงานต่อเนื่องไปแล้วระยะเวลาหนึ่ง หรือหลายๆ ครั้งเราพบว่า database ตายเมื่อเวลาผ่านไประยะหนึ่ง เป็นต้น

เราควรตั้งค่าให้ได้ประมาณ 80% ของที่ระบบเรารองรับได้ เช่นถ้ารองรับได้ 500 VUs เราก็ตั้งค่าไว้ที่ 400 VUs

export let options = {
  stages: [
    { duration: "2m", target: 400 }, // ramp up to 400 users
    { duration: "3h56m", target: 400 }, // stay at 400 for ~4 hours
    { duration: "2m", target: 0 }, // scale down. (optional)
  ]
};

You may also like