├── scenario_noop.go ├── scenario_defer.go ├── scenario_sleep.go ├── scenario_delay.go ├── scenario.go ├── http_json.go ├── scenario_http.go ├── README.md ├── scenario_group.go ├── http.go └── implementation.slide /scenario_noop.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | type NoopScenario struct { 4 | } 5 | 6 | func (scenario *NoopScenario) run(_ *ScenarioContext) chan struct{} { 7 | ch := make(chan struct{}, 1) 8 | ch <- struct{}{} 9 | return ch 10 | } 11 | -------------------------------------------------------------------------------- /scenario_defer.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | type DeferScenario struct { 4 | Defer func(ScenarioState) Scenario 5 | } 6 | 7 | func (scenario *DeferScenario) run(c *ScenarioContext) chan struct{} { 8 | next := scenario.Defer(c.State) 9 | return next.run(c) 10 | } 11 | -------------------------------------------------------------------------------- /scenario_sleep.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type SleepScenario struct { 8 | Duration time.Duration 9 | OnComplete func(ScenarioState) 10 | } 11 | 12 | func (scenario *SleepScenario) run(c *ScenarioContext) chan struct{} { 13 | ch := make(chan struct{}, 1) 14 | go func() { 15 | time.Sleep(scenario.Duration) 16 | if cb := scenario.OnComplete; cb != nil { 17 | cb(c.State) 18 | } 19 | ch <- struct{}{} 20 | }() 21 | return ch 22 | } 23 | -------------------------------------------------------------------------------- /scenario_delay.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type DelayScenario struct { 8 | Duration time.Duration 9 | Scenario Scenario 10 | OnComplete func(ScenarioState) 11 | } 12 | 13 | func (scenario *DelayScenario) run(c *ScenarioContext) chan struct{} { 14 | ch := make(chan struct{}, 1) 15 | ch <- struct{}{} 16 | 17 | c.wg.Add(1) 18 | go func() { 19 | time.Sleep(scenario.Duration) 20 | ch := scenario.Scenario.run(c) 21 | <-ch 22 | close(ch) 23 | if cb := scenario.OnComplete; cb != nil { 24 | cb(c.State) 25 | } 26 | c.wg.Done() 27 | }() 28 | return ch 29 | } 30 | -------------------------------------------------------------------------------- /scenario.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import "sync" 4 | 5 | type ScenarioState interface{} 6 | 7 | type Scenario interface { 8 | run(*ScenarioContext) chan struct{} 9 | } 10 | 11 | type ScenarioContext struct { 12 | client *HttpClient 13 | wg *sync.WaitGroup 14 | State ScenarioState 15 | } 16 | 17 | func NewScenarioContext(client *HttpClient, state ScenarioState) *ScenarioContext { 18 | return &ScenarioContext{ 19 | client: client, 20 | wg: &sync.WaitGroup{}, 21 | State: state, 22 | } 23 | } 24 | 25 | func (c *ScenarioContext) Run(scenario Scenario) { 26 | done := scenario.run(c) 27 | <-done 28 | close(done) 29 | c.wg.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /http_json.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | type JsonRequestEncoder struct{} 11 | type JsonResponseDecoder struct{} 12 | 13 | func (c *JsonRequestEncoder) GetContentType() string { 14 | return "application/json" 15 | } 16 | 17 | func (c *JsonRequestEncoder) Encode(data interface{}) (io.Reader, error) { 18 | json, err := json.Marshal(data) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return bytes.NewReader(json), nil 23 | } 24 | 25 | func (c *JsonResponseDecoder) SupportedContentType(contentType string) bool { 26 | return strings.HasPrefix(contentType, "application/json") 27 | } 28 | 29 | func (c *JsonResponseDecoder) Decode(reader io.Reader) (interface{}, error) { 30 | decoder := json.NewDecoder(reader) 31 | 32 | var data interface{} 33 | if err := decoder.Decode(&data); err != nil { 34 | return nil, err 35 | } 36 | return data, nil 37 | } 38 | -------------------------------------------------------------------------------- /scenario_http.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type HttpScenario struct { 8 | Method string 9 | Path string 10 | Headers map[string]string 11 | Content interface{} 12 | BeforeRun func(ScenarioState, *HttpScenario) 13 | OnComplete func(ScenarioState, *HttpResponse, time.Duration) 14 | OnError func(ScenarioState, error) 15 | } 16 | 17 | func (scenario *HttpScenario) run(c *ScenarioContext) chan struct{} { 18 | ch := make(chan struct{}, 1) 19 | go func() { 20 | if cb := scenario.BeforeRun; cb != nil { 21 | cb(c.State, scenario) 22 | } 23 | startAt := time.Now() 24 | res, err := c.client.Request(scenario.Method, scenario.Path, scenario.Headers, scenario.Content) 25 | endAt := time.Now() 26 | if err == nil { 27 | if cb := scenario.OnComplete; cb != nil { 28 | duration := endAt.Sub(startAt) 29 | cb(c.State, res, duration) 30 | } 31 | } else { 32 | if cb := scenario.OnError; cb != nil { 33 | cb(c.State, err) 34 | } else { 35 | panic(err) 36 | } 37 | } 38 | ch <- struct{}{} 39 | }() 40 | return ch 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gostress 2 | 3 | HTTP/HTTPS stress test framework. 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "github.com/karupanerura/gostress" 10 | "log" 11 | "math/rand" 12 | "runtime" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | runtime.GOMAXPROCS(runtime.NumCPU()) 18 | rand.Seed(time.Now().UnixNano()) 19 | 20 | client := gostress.NewHttpClient( 21 | gostress.HttpClientConfig{ 22 | Server: gostress.ServerConfig{ 23 | Hostname: "myhost.com", 24 | Secure: false, 25 | }, 26 | Headers: map[string]string{}, 27 | UserAgent: "Gostress/alpha", 28 | MaxIdleConnsPerHost: 1024, 29 | RequestEncoder: &gostress.JsonRequestEncoder{}, 30 | ResponseDecoder: &gostress.JsonResponseDecoder{}, 31 | }, 32 | ) 33 | state := map[string]string{} 34 | context := gostress.NewScenarioContext(client, state) 35 | scenario := makeScenario() 36 | context.Run(scenario) 37 | } 38 | 39 | func makeScenario() gostress.Scenario { 40 | scenarios := gostress.NewSeriesScenarioGroup(256) 41 | scenarios.MinInterval = 500 * time.Millisecond 42 | scenarios.MaxInterval = 10000 * time.Millisecond 43 | scenarios.Next(makeHTTPScenario("GET", "/", nil)) 44 | scenarios.Next( 45 | &gostress.SleepScenario{Duration: 1 * time.Millisecond}, 46 | ) 47 | scenarios.Next( 48 | gostress.NewConcurrentScenarioGroup(3).Add( 49 | makeHTTPScenario("GET", "/api/foo", nil), 50 | ).Add( 51 | makeHTTPScenario("GET", "/api/bar", nil), 52 | ).Add( 53 | makeHTTPScenario("GET", "/api/baz", nil), 54 | ), 55 | ) 56 | scenarios.Next( 57 | &gostress.DelayScenario{ 58 | Duration: 1 * time.Millisecond, 59 | Scenario: makeHTTPScenario("GET", "/api/hoge", nil), 60 | }, 61 | ) 62 | scenarios.Next( 63 | &gostress.DeferScenario{ 64 | Defer: func (state gostress.ScenarioState) gostress.Scenario { 65 | return makeHTTPScenario("GET", "/api/fuga", nil) 66 | }, 67 | }, 68 | ) 69 | scenarios.Next( 70 | &gostress.DeferScenario{ 71 | Defer: func (state gostress.ScenarioState) gostress.Scenario { 72 | return &gostress.NoopScenario{} 73 | }, 74 | }, 75 | ) 76 | return scenarios 77 | } 78 | 79 | func makeHTTPScenario(method, path string, content interface{}) gostress.Scenario { 80 | return &gostress.HttpScenario{ 81 | Method: method, 82 | Path: path, 83 | Content: content, 84 | OnComplete: func(state gostress.ScenarioState, res *gostress.HttpResponse, duration time.Duration) { 85 | log.Printf("method:%s\tpath:%s\tstatus:%d\ttime:%f", method, path, res.StatusCode, duration.Seconds()) 86 | }, 87 | OnError: func(state gostress.ScenarioState, err error) { 88 | log.Printf("Error: %s", err) 89 | }, 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /scenario_group.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | "math/rand" 7 | ) 8 | 9 | type ScenarioGroup struct { 10 | scenarios []Scenario 11 | OnComplete func(ScenarioState, time.Duration) 12 | } 13 | 14 | type ConcurrentScenarioGroup struct { 15 | ScenarioGroup 16 | } 17 | 18 | type SeriesScenarioGroup struct { 19 | ScenarioGroup 20 | MaxInterval time.Duration 21 | MinInterval time.Duration 22 | } 23 | 24 | const ZERO_SEC = 0 * time.Second 25 | 26 | func NewConcurrentScenarioGroup(size int) *ConcurrentScenarioGroup { 27 | return &ConcurrentScenarioGroup{ 28 | ScenarioGroup{ 29 | scenarios: make([]Scenario, 0, size), 30 | OnComplete: nil, 31 | }, 32 | } 33 | } 34 | 35 | func NewSeriesScenarioGroup(size int) *SeriesScenarioGroup { 36 | return &SeriesScenarioGroup{ 37 | ScenarioGroup: ScenarioGroup{ 38 | scenarios: make([]Scenario, 0, size), 39 | OnComplete: nil, 40 | }, 41 | MaxInterval: ZERO_SEC, 42 | MinInterval: ZERO_SEC, 43 | } 44 | } 45 | 46 | func (c *ConcurrentScenarioGroup) Add(scenario Scenario) *ConcurrentScenarioGroup { 47 | c.scenarios = append(c.scenarios, scenario) 48 | return c 49 | } 50 | 51 | func (c *ConcurrentScenarioGroup) AddNth(count uint, scenario Scenario) *ConcurrentScenarioGroup { 52 | for i := (uint)(0); i < count; i++ { 53 | c.Add(scenario) 54 | } 55 | return c 56 | } 57 | 58 | func (c *SeriesScenarioGroup) Next(scenario Scenario) *SeriesScenarioGroup { 59 | c.scenarios = append(c.scenarios, scenario) 60 | return c 61 | } 62 | 63 | func (group *ConcurrentScenarioGroup) run(c *ScenarioContext) chan struct{} { 64 | ch := make(chan struct{}, 1) 65 | wg := &sync.WaitGroup{} 66 | 67 | for _, scenario := range group.scenarios { 68 | scenario := scenario // redeclare c for the closure 69 | wg.Add(1) 70 | go func() { 71 | done := scenario.run(c) 72 | <-done 73 | wg.Done() 74 | }() 75 | } 76 | 77 | go func() { 78 | startAt := time.Now() 79 | wg.Wait() 80 | endAt := time.Now() 81 | if cb := group.OnComplete; cb != nil { 82 | duration := endAt.Sub(startAt) 83 | cb(c.State, duration) 84 | } 85 | ch <- struct{}{} 86 | }() 87 | 88 | return ch 89 | } 90 | 91 | func (group *SeriesScenarioGroup) run(c *ScenarioContext) chan struct{} { 92 | ch := make(chan struct{}, 1) 93 | 94 | go func() { 95 | startAt := time.Now() 96 | for _, scenario := range group.scenarios { 97 | if group.MaxInterval > ZERO_SEC { 98 | baseInterval := group.MinInterval 99 | rangedInterval := group.MaxInterval - group.MinInterval 100 | interval := baseInterval + time.Duration(rand.Int63n(rangedInterval.Nanoseconds())) 101 | time.Sleep(interval) 102 | } 103 | done := scenario.run(c) 104 | <-done 105 | } 106 | endAt := time.Now() 107 | if cb := group.OnComplete; cb != nil { 108 | duration := endAt.Sub(startAt) 109 | cb(c.State, duration) 110 | } 111 | ch <- struct{}{} 112 | }() 113 | 114 | return ch 115 | } 116 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package gostress 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type RequestEncoder interface { 12 | GetContentType() string 13 | Encode(interface{}) (io.Reader, error) 14 | } 15 | 16 | type ResponseDecoder interface { 17 | SupportedContentType(string) bool 18 | Decode(io.Reader) (interface{}, error) 19 | } 20 | 21 | type ServerConfig struct { 22 | Hostname string 23 | Secure bool 24 | } 25 | 26 | type HttpClientConfig struct { 27 | Server ServerConfig 28 | Headers map[string]string 29 | UserAgent string 30 | MaxIdleConnsPerHost int 31 | RequestEncoder RequestEncoder 32 | ResponseDecoder ResponseDecoder 33 | } 34 | 35 | type HttpResponse struct { 36 | StatusCode int 37 | Header http.Header 38 | Content interface{} 39 | } 40 | 41 | type HttpClient struct { 42 | Client http.Client 43 | Config HttpClientConfig 44 | } 45 | 46 | func NewHttpClient(config HttpClientConfig) *HttpClient { 47 | return &HttpClient{ 48 | Client: http.Client{ 49 | Timeout: 0, 50 | Transport: &http.Transport{ 51 | MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, 52 | }, 53 | }, 54 | Config: config, 55 | } 56 | } 57 | 58 | func (c *ServerConfig) MakeUrl(path string) string { 59 | if c.Secure { 60 | return fmt.Sprintf("https://%s%s", c.Hostname, path) 61 | } 62 | return fmt.Sprintf("http://%s%s", c.Hostname, path) 63 | } 64 | 65 | func (c *HttpClient) Request(method, path string, headers map[string]string, content interface{}) (*HttpResponse, error) { 66 | req, err := c.makeRequest(method, path, headers, content) 67 | res, err := c.Client.Do(req) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return c.parseResponse(res) 72 | } 73 | 74 | func (c *HttpClient) makeRequest(method, path string, headers map[string]string, content interface{}) (*http.Request, error) { 75 | uri, body, err := c.preformContent(method, path, content) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | req, err := http.NewRequest(method, uri, body) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if content != nil { 86 | contentType := c.Config.RequestEncoder.GetContentType() 87 | req.Header.Set("Content-Type", contentType) 88 | } 89 | 90 | req.Header.Set("User-Agent", c.Config.UserAgent) 91 | for k, v := range c.Config.Headers { 92 | req.Header.Set(k, v) 93 | } 94 | if headers != nil { 95 | for k, v := range headers { 96 | req.Header.Set(k, v) 97 | } 98 | } 99 | return req, nil 100 | } 101 | 102 | func (c *HttpClient) preformContent(method, path string, content interface{}) (uri string, body io.Reader, err error) { 103 | uri = c.Config.Server.MakeUrl(path) 104 | 105 | if content != nil { 106 | if isContentMethod(method) { 107 | body, err = c.Config.RequestEncoder.Encode(content) 108 | if err != nil { 109 | return 110 | } 111 | } else { 112 | query := url.Values{} 113 | switch content := content.(type) { 114 | case map[string]string: 115 | for k, v := range content { 116 | query.Set(k, v) 117 | } 118 | case map[fmt.Stringer]string: 119 | for k, v := range content { 120 | query.Set(k.String(), v) 121 | } 122 | case map[string]fmt.Stringer: 123 | for k, v := range content { 124 | query.Set(k, v.String()) 125 | } 126 | case map[fmt.Stringer]fmt.Stringer: 127 | for k, v := range content { 128 | query.Set(k.String(), v.String()) 129 | } 130 | } 131 | uri = uri + "?" + query.Encode() 132 | } 133 | } 134 | 135 | return 136 | } 137 | 138 | func (c *HttpClient) parseResponse(res *http.Response) (*HttpResponse, error) { 139 | defer res.Body.Close() 140 | if decoder := c.Config.ResponseDecoder; res.ContentLength > 0 && decoder != nil { 141 | if contentType := res.Header.Get("Content-Type"); decoder.SupportedContentType(contentType) { 142 | content, err := decoder.Decode(res.Body) 143 | if err != nil { 144 | return nil, err 145 | } 146 | res := &HttpResponse{ 147 | StatusCode: res.StatusCode, 148 | Header: res.Header, 149 | Content: content, 150 | } 151 | return res, err 152 | } else { 153 | buf := make([]byte, res.ContentLength) 154 | io.ReadFull(res.Body, buf) 155 | log.Print("Content-Type: " + contentType) 156 | log.Print(string(buf)) 157 | } 158 | } 159 | 160 | return &HttpResponse{ 161 | StatusCode: res.StatusCode, 162 | Header: res.Header, 163 | Content: map[string]string{}, 164 | }, nil 165 | } 166 | 167 | func isContentMethod(method string) bool { 168 | return method == "POST" || method == "PUT" || method == "PATCH" 169 | } 170 | -------------------------------------------------------------------------------- /implementation.slide: -------------------------------------------------------------------------------- 1 | Implementation of gostress 2 | Shibuya.go #1 3 | 4 | 20:00 16 Feb 2016 5 | Tags: #shibuyago 6 | 7 | id:karupanerura 8 | https://karupas.org/ 9 | 10 | * About me 11 | 12 | - id:karupanerura 13 | - Perl/Go/Swift/Kotlin/Java/Crystal/etc... 14 | - Senior Engineer at Mobile Factory, Inc. 15 | 16 | .image http://karupas.org/img/karupanerura.png 17 | 18 | * gostress 19 | 20 | * gostress is ... 21 | 22 | - HTTP/HTTPS stress test tool 23 | - (will) supports HTTP/2 by net/http in Go 1.6 24 | - write scenarios as a code in Go language 25 | - my first go product 26 | 27 | * Motivation 28 | 29 | - Want to write somthing in go language 30 | 31 | * SPEC(client) 32 | 33 | - Network: 1Gbps 34 | - CPU: Core i7 2.5GHz (4core/HT) 35 | - Memory: 16GB DDR3 36 | - Users/MaxIdleConnsPerHost: over 2000 37 | - Scenario: real play scenario 38 | 39 | * SPEC(server) 40 | 41 | - Network: 1Gbps 42 | - Social Game 43 | 44 | * Result 45 | 46 | 200Mbps available! 47 | 48 | * Usage 49 | 50 | * Make a http client 51 | 52 | client := gostress.NewHttpClient( 53 | gostress.HttpClientConfig{ 54 | Server: gostress.ServerConfig{ 55 | Hostname: "myhost.com", 56 | Secure: false, 57 | }, 58 | Headers: map[string]string{}, 59 | UserAgent: "Gostress/alpha", 60 | MaxIdleConnsPerHost: 1024, 61 | RequestEncoder: &gostress.JsonRequestEncoder{}, 62 | ResponseDecoder: &gostress.JsonResponseDecoder{}, 63 | }, 64 | ) 65 | 66 | 67 | * Make a scenario context 68 | 69 | context := gostress.NewScenarioContext(client, nil) 70 | 71 | * Make a scenario and run it 72 | 73 | scenario := makeScenario() 74 | context.Run(scenario) 75 | 76 | * Implementation 77 | 78 | * Scenario 79 | 80 | type Scenario interface { 81 | run(*ScenarioContext) chan struct{} 82 | } 83 | 84 | * ScenarioContext 85 | 86 | func (c *ScenarioContext) Run(scenario Scenario) { 87 | done := scenario.run(c) 88 | <-done 89 | close(done) 90 | c.wg.Wait() 91 | } 92 | 93 | * NoopScenario 94 | 95 | no operation 96 | 97 | type NoopScenario struct { 98 | } 99 | 100 | func (scenario *NoopScenario) run(_ *ScenarioContext) <-chan struct{} { 101 | ch := make(chan struct{}, 1) 102 | ch <- struct{}{} 103 | return ch 104 | } 105 | 106 | * DeferScenario 107 | 108 | defer to decide scenario 109 | 110 | type DeferScenario struct { 111 | Defer func(ScenarioState) Scenario 112 | } 113 | 114 | func (scenario *DeferScenario) run(c *ScenarioContext) <-chan struct{} { 115 | next := scenario.Defer(c.State) 116 | return next.run(c) 117 | } 118 | 119 | * DelayScenario 120 | 121 | type DelayScenario struct { 122 | Duration time.Duration 123 | Scenario Scenario 124 | OnComplete func(ScenarioState) 125 | } 126 | 127 | func (scenario *DelayScenario) run(c *ScenarioContext) <-chan struct{} { 128 | ch := make(chan struct{}, 1) 129 | ch <- struct{}{} 130 | close(ch) 131 | 132 | c.wg.Add(1) 133 | go func() { 134 | time.Sleep(scenario.Duration) 135 | ch := scenario.Scenario.run(c) 136 | <-ch 137 | if cb := scenario.OnComplete; cb != nil { 138 | cb(c.State) 139 | } 140 | c.wg.Done() 141 | }() 142 | return ch 143 | } 144 | 145 | 146 | * HttpScenario 147 | 148 | send http request 149 | 150 | type HttpScenario struct { 151 | Method string 152 | Path string 153 | Headers map[string]string 154 | Content interface{} 155 | BeforeRun func(ScenarioState, *HttpScenario) 156 | OnComplete func(ScenarioState, *HttpResponse, time.Duration) 157 | OnError func(ScenarioState, error) 158 | } 159 | 160 | .link https://github.com/karupanerura/gostress/blob/master/scenario_group.go 161 | 162 | * ScenarioGroup 163 | 164 | type ScenarioGroup struct { 165 | scenarios []Scenario 166 | OnComplete func(ScenarioState, time.Duration) 167 | } 168 | 169 | type ConcurrentScenarioGroup struct { 170 | ScenarioGroup 171 | } 172 | 173 | type SeriesScenarioGroup struct { 174 | ScenarioGroup 175 | MaxInterval time.Duration 176 | MinInterval time.Duration 177 | } 178 | 179 | .link https://github.com/karupanerura/gostress/blob/master/scenario_group.go 180 | 181 | * Impressions of Go language 182 | 183 | - Easy to write concurrent processing by channel/goroutine 184 | - Very good! 185 | 186 | * that's all 187 | 188 | thank you for listening 189 | --------------------------------------------------------------------------------