update stress test tool
This commit is contained in:
parent
19a0b51c1a
commit
c115bb3dd4
|
@ -1,3 +1,3 @@
|
||||||
stress
|
stress
|
||||||
stress.exe
|
stress.exe
|
||||||
script.json
|
cases.json
|
|
@ -1,26 +1,43 @@
|
||||||
# STRESS
|
# STRESS
|
||||||
|
|
||||||
|
Stress test tool for TDengine. It run a set of test cases randomly and show statistics.
|
||||||
|
|
||||||
|
## COMMAND LINE
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
$ ./stress [-server=<localhost>] [-db=<test>] [-concurrent=<1>] [-fetch=<false>] [scriptFile]
|
$ ./stress [-h=<localhost>] [-P=<0>] [-d=<test>] [-u=<root>] [-p=<taosdata>] [-c=<4>] [-f=<true>] [test case file]
|
||||||
```
|
```
|
||||||
|
|
||||||
## SCRIPT FILE
|
* **-h**: host name or IP address of TDengine server (default: localhost).
|
||||||
|
* **-P**: port number of TDengine server (default: 0).
|
||||||
|
* **-u**: user name (default: root).
|
||||||
|
* **-p**: password (default: taosdata).
|
||||||
|
* **-c**: concurrency, number of concurrent goroutines for query (default: 4).
|
||||||
|
* **-f**: fetch data or not (default: true).
|
||||||
|
* **test case file**: the path of a JSON file which contains the test cases (default: cases.json).
|
||||||
|
|
||||||
|
## TEST CASE FILE
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[{
|
[{
|
||||||
"sql": "select * from meters where id = %d and a >= %d and a <= %d and tbname='%s'",
|
"weight": 1,
|
||||||
|
"sql": "select * from meters where ts>=now+%dm and ts<=now-%dm and c1=%v and c2=%d and c3='%s' and tbname='%s'",
|
||||||
"args": [{
|
"args": [{
|
||||||
"type": "int",
|
|
||||||
"min": -10,
|
|
||||||
"max": 20
|
|
||||||
}, {
|
|
||||||
"type": "range",
|
"type": "range",
|
||||||
"min": 30,
|
"min": 30,
|
||||||
"max": 60
|
"max": 60
|
||||||
|
}, {
|
||||||
|
"type": "bool"
|
||||||
|
}, {
|
||||||
|
"type": "int",
|
||||||
|
"min": -10,
|
||||||
|
"max": 20
|
||||||
}, {
|
}, {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 10,
|
"max": 10,
|
||||||
|
}, {
|
||||||
|
"type": "list",
|
||||||
"list": [
|
"list": [
|
||||||
"table1",
|
"table1",
|
||||||
"table2",
|
"table2",
|
||||||
|
@ -29,4 +46,35 @@ $ ./stress [-server=<localhost>] [-db=<test>] [-concurrent=<1>] [-fetch=<false>]
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The test case file is a standard JSON file which contains an array of test cases. For test cases, field `sql` is mandatory, and it can optionally include a `weight` field and an `args` field which is an array of arguments.
|
||||||
|
|
||||||
|
`sql` is a SQL statement, it can include zero or more arguments (placeholders).
|
||||||
|
|
||||||
|
`weight` defines the possibility of the case being selected, the greater value the higher possibility. It must be an non-negative integer and the default value is zero, but, if all cases have a zero weight, all the weights are regarded as 1.
|
||||||
|
|
||||||
|
Placeholders of `sql` are replaced by arguments in `args` at runtime. There are 5 types of arguments currently:
|
||||||
|
|
||||||
|
* **bool**: generate a `boolean` value randomly.
|
||||||
|
* **int**: generate an `integer` between [`min`, `max`] randomly, the default value of `min` is 0 and `max` is 100.
|
||||||
|
* **range**: generate two `integer`s between [`min`, `max`] randomly, the first is less than the second, the default value of `min` is 0 and `max` is 100.
|
||||||
|
* **string**: generate a `string` with length between [`min`, `max`] randomly, the default value of `min` is 0 and `max` is 100.
|
||||||
|
* **list**: select an item from `list` randomly.
|
||||||
|
|
||||||
|
## OUTPUT
|
||||||
|
|
||||||
|
```
|
||||||
|
00:00:08 | TOTAL REQ | TOTAL TIME(us) | TOTAL AVG(us) | REQUEST | TIME(us) | AVERAGE(us) |
|
||||||
|
-----------------------------------------------------------------------------------------------
|
||||||
|
TOTAL | 3027 | 26183890 | 8650.11 | 287 | 3060935 | 10665.28 |
|
||||||
|
SUCCESS | 3027 | 26183890 | 8650.11 | 287 | 3060935 | 10665.28 |
|
||||||
|
FAIL | 0 | 0 | 0.00 | 0 | 0 | 0.00 |
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Col 2**: total number of request since test start.
|
||||||
|
* **Col 3**: total time of all request since test start.
|
||||||
|
* **Col 4**: average time of all request since test start.
|
||||||
|
* **Col 5**: number of request in last second.
|
||||||
|
* **Col 6**: time of all request in last second.
|
||||||
|
* **Col 7**: average time of all request in last second.
|
||||||
|
|
|
@ -2,4 +2,6 @@ module github.com/taosdata/stress
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require github.com/taosdata/driver-go v0.0.0-20200606095205-b786bac1857f
|
require (
|
||||||
|
github.com/taosdata/driver-go v0.0.0-20200606095205-b786bac1857f
|
||||||
|
)
|
||||||
|
|
|
@ -24,9 +24,10 @@ type argument struct {
|
||||||
List []interface{} `json:"list, omitempty"`
|
List []interface{} `json:"list, omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type script struct {
|
type testCase struct {
|
||||||
isQuery bool `json:"-"`
|
isQuery bool `json:"-"`
|
||||||
numArgs int `json:"-"`
|
numArgs int `json:"-"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
Sql string `json:"sql"`
|
Sql string `json:"sql"`
|
||||||
Args []argument `json:"args"`
|
Args []argument `json:"args"`
|
||||||
}
|
}
|
||||||
|
@ -97,6 +98,14 @@ func (arg *argument) generate(args []interface{}) []interface{} {
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *testCase) buildSql() string {
|
||||||
|
args := make([]interface{}, 0, tc.numArgs)
|
||||||
|
for i := 0; i < len(tc.Args); i++ {
|
||||||
|
args = tc.Args[i].generate(args)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(tc.Sql, args...)
|
||||||
|
}
|
||||||
|
|
||||||
type statitics struct {
|
type statitics struct {
|
||||||
succeeded int64
|
succeeded int64
|
||||||
failed int64
|
failed int64
|
||||||
|
@ -105,60 +114,79 @@ type statitics struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
server string
|
host string
|
||||||
database string
|
port uint
|
||||||
fetch bool
|
database string
|
||||||
concurrent uint
|
user string
|
||||||
startAt time.Time
|
password string
|
||||||
shouldStop int64
|
fetch bool
|
||||||
wg sync.WaitGroup
|
|
||||||
stat statitics
|
startAt time.Time
|
||||||
scripts []script
|
shouldStop int64
|
||||||
|
wg sync.WaitGroup
|
||||||
|
stat statitics
|
||||||
|
totalWeight int
|
||||||
|
cases []testCase
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadScript(path string) error {
|
func loadTestCase(path string) error {
|
||||||
f, e := os.Open(path)
|
f, e := os.Open(path)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
e = json.NewDecoder(f).Decode(&scripts)
|
e = json.NewDecoder(f).Decode(&cases)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(scripts); i++ {
|
for i := 0; i < len(cases); i++ {
|
||||||
s := &scripts[i]
|
c := &cases[i]
|
||||||
s.Sql = strings.TrimSpace(s.Sql)
|
c.Sql = strings.TrimSpace(c.Sql)
|
||||||
s.isQuery = strings.ToLower(s.Sql[:6]) == "select"
|
c.isQuery = strings.ToLower(c.Sql[:6]) == "select"
|
||||||
|
if c.Weight < 0 {
|
||||||
|
return fmt.Errorf("test %d: negative weight", i)
|
||||||
|
}
|
||||||
|
totalWeight += c.Weight
|
||||||
|
|
||||||
for j := 0; j < len(s.Args); j++ {
|
for j := 0; j < len(c.Args); j++ {
|
||||||
arg := &s.Args[j]
|
arg := &c.Args[j]
|
||||||
arg.Type = strings.ToLower(arg.Type)
|
arg.Type = strings.ToLower(arg.Type)
|
||||||
n, e := arg.check()
|
n, e := arg.check()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return fmt.Errorf("script %d argument %d: %s", i, j, e.Error())
|
return fmt.Errorf("test case %d argument %d: %s", i, j, e.Error())
|
||||||
}
|
}
|
||||||
s.numArgs += n
|
c.numArgs += n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalWeight == 0 {
|
||||||
|
for i := 0; i < len(cases); i++ {
|
||||||
|
cases[i].Weight = 1
|
||||||
|
}
|
||||||
|
totalWeight = len(cases)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSql() (string, bool) {
|
func selectTestCase() *testCase {
|
||||||
s := scripts[rand.Intn(len(scripts))]
|
sum, target := 0, rand.Intn(totalWeight)
|
||||||
args := make([]interface{}, 0, s.numArgs)
|
var c *testCase
|
||||||
for i := 0; i < len(s.Args); i++ {
|
for i := 0; i < len(cases); i++ {
|
||||||
args = s.Args[i].generate(args)
|
c = &cases[i]
|
||||||
|
sum += c.Weight
|
||||||
|
if sum > target {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(s.Sql, args...), s.isQuery
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTest() {
|
func runTest() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
db, e := sql.Open("taosSql", "root:taosdata@tcp("+server+":0)/"+database)
|
db, e := sql.Open("taosSql", fmt.Sprintf("%s:%s@tcp(%s:%v)/%s", user, password, host, port, database))
|
||||||
if e != nil {
|
if e != nil {
|
||||||
fmt.Printf("failed to connect to database: %s\n", e.Error())
|
fmt.Printf("failed to connect to database: %s\n", e.Error())
|
||||||
return
|
return
|
||||||
|
@ -166,10 +194,11 @@ func runTest() {
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
for atomic.LoadInt64(&shouldStop) == 0 {
|
for atomic.LoadInt64(&shouldStop) == 0 {
|
||||||
str, isQuery := buildSql()
|
c := selectTestCase()
|
||||||
start := time.Now()
|
str := c.buildSql()
|
||||||
|
|
||||||
if isQuery {
|
start := time.Now()
|
||||||
|
if c.isQuery {
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
if rows, e = db.Query(str); rows != nil {
|
if rows, e = db.Query(str); rows != nil {
|
||||||
if fetch {
|
if fetch {
|
||||||
|
@ -181,8 +210,8 @@ func runTest() {
|
||||||
} else {
|
} else {
|
||||||
_, e = db.Exec(str)
|
_, e = db.Exec(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Now().Sub(start).Microseconds()
|
duration := time.Now().Sub(start).Microseconds()
|
||||||
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
atomic.AddInt64(&stat.failed, 1)
|
atomic.AddInt64(&stat.failed, 1)
|
||||||
atomic.AddInt64(&stat.failedDuration, duration)
|
atomic.AddInt64(&stat.failedDuration, duration)
|
||||||
|
@ -208,7 +237,7 @@ func getStatPrinter() func(tm time.Time) {
|
||||||
seconds := int64(tm.Sub(startAt).Seconds())
|
seconds := int64(tm.Sub(startAt).Seconds())
|
||||||
format := "\033K %02v:%02v:%02v | TOTAL REQ | TOTAL TIME(us) | TOTAL AVG(us) | REQUEST | TIME(us) | AVERAGE(us) |\n"
|
format := "\033K %02v:%02v:%02v | TOTAL REQ | TOTAL TIME(us) | TOTAL AVG(us) | REQUEST | TIME(us) | AVERAGE(us) |\n"
|
||||||
fmt.Printf(format, seconds/3600, seconds%3600/60, seconds%60)
|
fmt.Printf(format, seconds/3600, seconds%3600/60, seconds%60)
|
||||||
fmt.Println("------------------------------------------------------------------------------------------------")
|
fmt.Println("-----------------------------------------------------------------------------------------------")
|
||||||
|
|
||||||
tr := current.succeeded + current.failed
|
tr := current.succeeded + current.failed
|
||||||
td := current.succeededDuration + current.failedDuration
|
td := current.succeededDuration + current.failedDuration
|
||||||
|
@ -258,35 +287,39 @@ func getStatPrinter() func(tm time.Time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.StringVar(&server, "server", "localhost", "host name or IP address of TDengine server")
|
var concurrency uint
|
||||||
flag.StringVar(&database, "db", "test", "database name")
|
flag.StringVar(&host, "h", "localhost", "host name or IP address of TDengine server")
|
||||||
flag.BoolVar(&fetch, "fetch", false, "fetch result or not")
|
flag.UintVar(&port, "P", 0, "port (default 0)")
|
||||||
flag.UintVar(&concurrent, "concurrent", 1, "number of concurrent queries")
|
flag.StringVar(&database, "d", "test", "database name")
|
||||||
|
flag.StringVar(&user, "u", "root", "user name")
|
||||||
|
flag.StringVar(&password, "p", "taosdata", "password")
|
||||||
|
flag.BoolVar(&fetch, "f", true, "fetch result or not")
|
||||||
|
flag.UintVar(&concurrency, "c", 4, "concurrency, number of goroutines for query")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
scriptFile := flag.Arg(0)
|
caseFile := flag.Arg(0)
|
||||||
if scriptFile == "" {
|
if caseFile == "" {
|
||||||
scriptFile = "script.json"
|
caseFile = "cases.json"
|
||||||
}
|
}
|
||||||
if e := loadScript(scriptFile); e != nil {
|
if e := loadTestCase(caseFile); e != nil {
|
||||||
fmt.Println("failed to load script file:", e.Error())
|
fmt.Println("failed to load test cases:", e.Error())
|
||||||
return
|
return
|
||||||
} else if len(scripts) == 0 {
|
} else if len(cases) == 0 {
|
||||||
fmt.Println("there's no script in the script file")
|
fmt.Println("there's no test case")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("SERVER: %s DATABASE: %s CONCURRENT QUERIES: %d FETCH DATA: %v\n", server, database, concurrent, fetch)
|
fmt.Printf("SERVER: %s DATABASE: %s CONCURRENCY: %d FETCH DATA: %v\n", host, database, concurrency, fetch)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
startAt = time.Now()
|
startAt = time.Now()
|
||||||
printStat := getStatPrinter()
|
printStat := getStatPrinter()
|
||||||
printStat(startAt)
|
printStat(startAt)
|
||||||
|
|
||||||
for i := uint(0); i < concurrent; i++ {
|
for i := uint(0); i < concurrency; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go runTest()
|
go runTest()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue