提交 c115bb3d 编写于 作者: B Bomin Zhang

update stress test tool

上级 19a0b51c
stress stress
stress.exe stress.exe
script.json cases.json
\ No newline at end of file \ No newline at end of file
# 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>] ...@@ -29,4 +46,35 @@ $ ./stress [-server=<localhost>] [-db=<test>] [-concurrent=<1>] [-fetch=<false>]
] ]
}] }]
}] }]
``` ```
\ No newline at end of file
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 ...@@ -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 { ...@@ -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{} { ...@@ -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 { ...@@ -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() { ...@@ -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() { ...@@ -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) { ...@@ -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) { ...@@ -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()
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册