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
เมื่อรันเสร็จแล้วเราจะได้สรุปการรันออกมาดังนี้
นี่คือสรุปภาพรวมของผลการทดสอบเท่านั้น ซึ่งค่าที่แสดงออกมาทาง 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 เรื่อง ได้แก่
- Metrics
- Checks, Thresholds
- 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 ชนิด ได้แก่
- Counter – ใช้สำหรับเก็บรวบรวมค่าไปเรื่อยๆ ตลอดการทดสอบ
- Gauge – ใช้เก็บค่าสุดท้ายที่อ่านได้
- Trend – เก็บค่าต่างๆ และคำนวนออกมาเป็นสถิติแบบเดียวกับ built-in
http_req_
metrics - 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 แต่ละแบบ
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 ไว้ด้วย ดังนี้
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)
]
};