├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── burst ├── histogram.go ├── histogram_test.go └── mysqlburst.go ├── go.mod ├── go.sum └── mysqlburst.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .idea 27 | *.iml 28 | mysqlburst 29 | 30 | vendor 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pwd=$(shell pwd) 2 | 3 | .PHONY : all clean 4 | 5 | all : mysqlburst 6 | 7 | mysqlburst: 8 | go build 9 | 10 | clean: 11 | rm -rf mysqlburst 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysqlburst 2 | A mysql pressure test tool that makes large quantity of short connections 3 | 4 | # usage 5 | 6 | e.g. `./mysqlburst -c 100 -r 1000 -a 127.0.0.1:3306 -d mysql -u user -p pswd -q 'select * from user limit 1' -i 1` 7 | 8 | params: 9 | -a string 10 | mysql server address (default "127.0.0.1:3306") 11 | -c int 12 | concurrency (default 100) 13 | -cto duration 14 | connect timeout (default 1s) 15 | -d string 16 | database (default "mysql") 17 | -i int 18 | summery interval (sec) 19 | -l enable driver log, will be written to stderr 20 | -n int 21 | repeat queries in a connection (default 1) 22 | -p string 23 | password 24 | -q value 25 | queries 26 | -qps int 27 | max qps. <= 0 means no limit 28 | -r int 29 | rounds (default 1000) 30 | -rto duration 31 | read timeout (default 5s) 32 | -t use short connection, reconnect before each test 33 | -u string 34 | user (default "root") 35 | -wto duration 36 | write timeout (default 5s) 37 | 38 | 39 | output may likes this: 40 | 41 | time: 999.917 ms 42 | connect count: 27124 failed: 0 avg: 7.298 ms min: 4.418 ms max: 159.979 ms stddev: 12.091 ms err: - 43 | query count: 27124 failed: 0 avg: 0.481 ms min: 0.174 ms max: 147.570 ms stddev: 3.358 ms err: - 44 | read count: 27124 failed: 0 avg: 0.000 ms min: 0.000 ms max: 0.037 ms stddev: 0.000 ms err: - 45 | total count: 27124 failed: 0 avg: 7.781 ms min: 4.729 ms max: 160.729 ms stddev: 12.547 ms err: - 46 | 47 | time: 999.794 ms 48 | connect count: 29090 failed: 0 avg: 6.490 ms min: 4.762 ms max: 17.331 ms stddev: 0.691 ms err: - 49 | query count: 29090 failed: 0 avg: 0.356 ms min: 0.173 ms max: 10.547 ms stddev: 0.239 ms err: - 50 | read count: 29090 failed: 0 avg: 0.000 ms min: 0.000 ms max: 0.024 ms stddev: 0.000 ms err: - 51 | total count: 29090 failed: 0 avg: 6.847 ms min: 5.022 ms max: 17.861 ms stddev: 0.731 ms err: - 52 | 53 | time: 1000.037 ms 54 | connect count: 29450 failed: 0 avg: 6.390 ms min: 4.798 ms max: 18.382 ms stddev: 0.689 ms err: - 55 | query count: 29450 failed: 0 avg: 0.366 ms min: 0.173 ms max: 11.494 ms stddev: 0.318 ms err: - 56 | read count: 29450 failed: 0 avg: 0.000 ms min: 0.000 ms max: 0.391 ms stddev: 0.002 ms err: - 57 | total count: 29450 failed: 0 avg: 6.757 ms min: 5.078 ms max: 18.752 ms stddev: 0.769 ms err: - 58 | 59 | 60 | param `-q` can be applied more than one times to send multi queries in a connection. 61 | 62 | -------------------------------------------------------------------------------- /burst/histogram.go: -------------------------------------------------------------------------------- 1 | package burst 2 | 3 | import "math" 4 | 5 | type Bucket struct { 6 | Count int64 7 | Sum int64 8 | } 9 | 10 | type Histogram struct { 11 | b [][]Bucket 12 | max int64 13 | min int64 14 | r int 15 | size int 16 | n int 17 | } 18 | 19 | func newHistogram(rb int) (ret Histogram) { 20 | // us, 10us, 100us, 1ms, 10ms, 100ms, 1s, 10s, 100s, 100s+ 21 | initSize := 10 22 | ret.r = int(math.Pow10(rb)) 23 | ret.size = initSize 24 | ret.b = make([][]Bucket, initSize) 25 | ret.min = math.MaxInt64 26 | ret.max = math.MinInt64 27 | for i := 0; i < initSize; i++ { 28 | ret.b[i] = make([]Bucket, ret.rowSize()) 29 | } 30 | return 31 | } 32 | 33 | func (h *Histogram) rowSize() int { 34 | return h.r - h.r/10 35 | } 36 | 37 | func NewHistogram() Histogram { 38 | return newHistogram(2) 39 | } 40 | 41 | func (h *Histogram) Clean() { 42 | for i := 0; i < h.size; i++ { 43 | for j := 0; j < h.rowSize(); j++ { 44 | h.b[i][j].Count++ 45 | } 46 | } 47 | } 48 | 49 | func (h *Histogram) Add(n int64) { 50 | h.n++ 51 | if n > h.max { 52 | h.max = n 53 | } 54 | if n < h.min { 55 | h.min = n 56 | } 57 | bi, oi := h.pos(n) 58 | for i := h.size; i <= bi; i++ { 59 | h.b = append(h.b, make([]Bucket, h.rowSize())) 60 | } 61 | h.b[bi][oi].Count++ 62 | h.b[bi][oi].Sum += n 63 | } 64 | 65 | func (h *Histogram) pos(n int64) (int, int) { 66 | if n <= 0 { 67 | return 0, 0 68 | } 69 | bi := int(math.Log10(float64(n))) 70 | oi := int(n*int64(h.r/10)/int64(math.Pow10(bi)) - int64(h.r/10)) 71 | return bi, oi 72 | } 73 | 74 | func (h *Histogram) Count() int { 75 | return h.n 76 | } 77 | 78 | func (h *Histogram) Max() int64 { 79 | return h.max 80 | } 81 | 82 | func (h *Histogram) Min() int64 { 83 | return h.min 84 | } 85 | 86 | func (h *Histogram) iterate(f func(int, int) bool) { 87 | i0, j0 := h.pos(h.min) 88 | i1, j1 := h.pos(h.max) 89 | i, j := i0, j0 90 | for { 91 | if f(i, j) { 92 | break 93 | } 94 | j++ 95 | if j >= h.rowSize() { 96 | i++ 97 | j = 0 98 | } 99 | if i > i1 || i == i1 && j > j1 { 100 | break 101 | } 102 | } 103 | } 104 | 105 | func (h *Histogram) Summery() []Bucket { 106 | ret := make([]Bucket, h.size) 107 | if h.n == 0 { 108 | return ret 109 | } 110 | h.iterate(func(i, j int) bool { 111 | ret[i].Count += h.b[i][j].Count 112 | ret[i].Sum += h.b[i][j].Sum 113 | return false 114 | }) 115 | return ret 116 | } 117 | 118 | func (h *Histogram) Histogram(n int) []Bucket { 119 | ret := make([]Bucket, n) 120 | rest := int64(h.n) 121 | ns := int64(n) 122 | c := rest / ns 123 | rest -= c 124 | ri := 0 125 | h.iterate(func(i, j int) bool { 126 | if c == 0 { 127 | ns-- 128 | if n == 0 || rest == 0 { 129 | return true 130 | } 131 | c = rest / ns 132 | rest -= c 133 | ri++ 134 | } 135 | t := h.b[i][j] 136 | if ret[ri].Count+t.Count <= c { 137 | ret[ri].Count += t.Count 138 | ret[ri].Sum += t.Sum 139 | c -= t.Count 140 | } else { 141 | for t.Count > 0 { 142 | part := t.Sum * c / t.Count 143 | ret[ri].Sum += part 144 | t.Sum -= part 145 | t.Count -= c 146 | ret[ri].Count += c 147 | 148 | ns-- 149 | if n == 0 || rest == 0 { 150 | return true 151 | } 152 | c = rest / ns 153 | rest -= c 154 | ri++ 155 | } 156 | } 157 | return false 158 | }) 159 | return ret 160 | } 161 | -------------------------------------------------------------------------------- /burst/histogram_test.go: -------------------------------------------------------------------------------- 1 | package burst 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHistogram_pos(t *testing.T) { 8 | hist := NewHistogram() 9 | i, j := hist.pos(0) 10 | if i != 0 || j != 0 { 11 | t.Errorf("%d %d %d", 0, i, j) 12 | } 13 | i, j = hist.pos(1) 14 | if i != 0 || j != 0 { 15 | t.Errorf("%d %d %d", 1, i, j) 16 | } 17 | i, j = hist.pos(9) 18 | if i != 0 || j != 80 { 19 | t.Errorf("%d %d %d", 9, i, j) 20 | } 21 | i, j = hist.pos(10) 22 | if i != 1 || j != 0 { 23 | t.Errorf("%d %d %d", 10, i, j) 24 | } 25 | i, j = hist.pos(100) 26 | if i != 2 || j != 0 { 27 | t.Errorf("%d %d %d", 100, i, j) 28 | } 29 | i, j = hist.pos(109) 30 | if i != 2 || j != 0 { 31 | t.Errorf("%d %d %d", 100, i, j) 32 | } 33 | i, j = hist.pos(110) 34 | if i != 2 || j != 1 { 35 | t.Errorf("%d %d %d", 100, i, j) 36 | } 37 | i, j = hist.pos(119) 38 | if i != 2 || j != 1 { 39 | t.Errorf("%d %d %d", 100, i, j) 40 | } 41 | i, j = hist.pos(199) 42 | if i != 2 || j != 9 { 43 | t.Errorf("%d %d %d", 100, i, j) 44 | } 45 | i, j = hist.pos(999) 46 | if i != 2 || j != 89 { 47 | t.Errorf("%d %d %d", 100, i, j) 48 | } 49 | 50 | hist = newHistogram(1) 51 | i, j = hist.pos(1) 52 | if i != 0 || j != 0 { 53 | t.Errorf("%d %d %d", 1, i, j) 54 | } 55 | } 56 | 57 | func TestHistogram_Summery(t *testing.T) { 58 | hist := NewHistogram() 59 | hist.Add(9999) 60 | hist.Add(1) 61 | hist.Add(123) 62 | hist.Add(321) 63 | hist.Add(555) 64 | if hist.Count() != 5 { 65 | t.Error("Count != 5") 66 | } 67 | if hist.Min() != 1 { 68 | t.Error("Min != 1") 69 | } 70 | if hist.Max() != 9999 { 71 | t.Error("Max != 9999") 72 | } 73 | s := hist.Summery() 74 | if s[0].Count != 1 || s[0].Sum != 1 { 75 | t.Errorf("p0[count, sum]!=%d, %d", 1, 1) 76 | } 77 | if s[1].Count != 0 || s[1].Sum != 0 { 78 | t.Errorf("p1[count, sum]!=%d, %d", 0, 0) 79 | } 80 | if s[2].Count != 3 || s[2].Sum != 999 { 81 | t.Errorf("p2[count, sum] %v !=%d, %d", s[2], 3, 999) 82 | } 83 | if s[3].Count != 1 || s[3].Sum != 9999 { 84 | t.Errorf("p3[count, sum]!=%d, %d", 1, 9999) 85 | } 86 | } 87 | 88 | func TestHistogram_Histogram(t *testing.T) { 89 | hist := NewHistogram() 90 | hist.Add(1) 91 | hist.Add(10) 92 | hist.Add(100) 93 | hist.Add(1000) 94 | h := hist.Histogram(4) 95 | if h[0].Count != 1 || h[0].Sum != 1 { 96 | t.Errorf("p0 %v != 1, 1\n", h[0]) 97 | } 98 | if h[1].Count != 1 || h[1].Sum != 10 { 99 | t.Errorf("p1 %v != 1, 1\n", h[1]) 100 | } 101 | if h[3].Count != 1 || h[3].Sum != 1000 { 102 | t.Errorf("p1 %v != 1, 1\n", h[3]) 103 | } 104 | hist = NewHistogram() 105 | hist.Add(1) 106 | hist.Add(2) 107 | hist.Add(3) 108 | hist.Add(4) 109 | hist.Add(5) 110 | h = hist.Histogram(2) 111 | t.Logf("%v\n", h) 112 | h = hist.Histogram(10) 113 | t.Logf("%v\n", h) 114 | } 115 | -------------------------------------------------------------------------------- /burst/mysqlburst.go: -------------------------------------------------------------------------------- 1 | package burst 2 | 3 | import ( 4 | "database/sql/driver" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "math" 9 | "math/big" 10 | "math/rand" 11 | "os" 12 | "runtime" 13 | "sync" 14 | "time" 15 | 16 | "github.com/go-sql-driver/mysql" 17 | ) 18 | 19 | const ( 20 | StageConn byte = 0 21 | StageQuery byte = 1 22 | StageRead byte = 2 23 | StageTotal byte = 3 24 | // 25 | StageMax byte = 4 26 | ) 27 | 28 | type TestResult struct { 29 | //stage byte 30 | ok bool 31 | err error 32 | time time.Duration 33 | } 34 | 35 | type SummeryResult struct { 36 | count int64 37 | failCount int64 38 | totalTime time.Duration 39 | totalSquareTime big.Int 40 | maxTime time.Duration 41 | minTime time.Duration 42 | avgTime time.Duration 43 | stddevTime time.Duration 44 | lastError error 45 | } 46 | 47 | type SummerySet struct { 48 | startTime time.Time 49 | endTime time.Time 50 | summery [StageMax]SummeryResult 51 | } 52 | 53 | func (s *SummerySet) Init() { 54 | for i := byte(0); i < StageMax; i++ { 55 | s.summery[i].minTime = math.MaxInt64 56 | } 57 | s.startTime = time.Now() 58 | } 59 | 60 | func (s *SummerySet) Summery() { 61 | s.endTime = time.Now() 62 | for i := byte(0); i < StageMax; i++ { 63 | s.summery[i].Summery() 64 | } 65 | } 66 | 67 | type Config struct { 68 | procs int 69 | rounds int 70 | repeat int 71 | qps int 72 | dsn string 73 | queries []string 74 | short bool 75 | } 76 | 77 | type Routine struct { 78 | db driver.Conn 79 | config Config 80 | //end time.Time 81 | } 82 | 83 | type Test struct { 84 | routine *Routine 85 | result [StageMax]TestResult 86 | 87 | beforeConn time.Time 88 | afterConn time.Time 89 | beforeQuery time.Time 90 | afterQuery time.Time 91 | afterRead time.Time 92 | } 93 | 94 | func (t *Test) testOnce() { 95 | //for i := byte(0); i < StageMax; i++ { 96 | // t.result[i].ok = false 97 | //} 98 | config := t.routine.config 99 | beforeConn := time.Now() 100 | var afterConn time.Time 101 | if config.short { 102 | db, err := (mysql.MySQLDriver{}).Open(config.dsn) 103 | if err != nil { 104 | t.result[StageConn].err = err 105 | return 106 | } 107 | afterConn = time.Now() 108 | t.routine.db = db 109 | 110 | defer db.Close() 111 | } else { 112 | afterConn = beforeConn 113 | } 114 | t.result[StageConn].ok = true 115 | t.result[StageConn].time = afterConn.Sub(beforeConn) 116 | 117 | t.result[StageQuery].time = 0 118 | t.result[StageRead].time = 0 119 | for i := 0; i < config.repeat; i++ { 120 | for _, query := range config.queries { 121 | beforeQuery := time.Now() 122 | rows, err := t.routine.db.(driver.Queryer).Query(query, []driver.Value{}) 123 | if err != nil { 124 | t.result[StageQuery].err = err 125 | t.result[StageQuery].ok = false 126 | return 127 | } 128 | 129 | afterQuery := time.Now() 130 | t.result[StageQuery].ok = true 131 | t.result[StageQuery].time += afterQuery.Sub(beforeQuery) 132 | 133 | err = rows.Close() // Close() will read all rows 134 | if err != nil && err != io.EOF { 135 | t.result[StageRead].err = err 136 | t.result[StageRead].ok = false 137 | return 138 | } 139 | afterRead := time.Now() 140 | t.result[StageRead].ok = true 141 | t.result[StageRead].time += afterRead.Sub(afterQuery) 142 | } 143 | } 144 | 145 | t.result[StageTotal].ok = true 146 | t.result[StageTotal].time = time.Now().Sub(beforeConn) 147 | } 148 | 149 | func (r *Routine) run(outChan chan<- [StageMax]TestResult) { 150 | rate := float64(r.config.qps) / float64(r.config.procs) 151 | var test, prevTest Test 152 | if !r.config.short { 153 | db, err := (mysql.MySQLDriver{}).Open(r.config.dsn) 154 | if err != nil { 155 | test.result[StageConn].err = err 156 | return 157 | } 158 | r.db = db 159 | } 160 | for i := 0; i < r.config.rounds; i++ { 161 | prevTest, test = test, Test{routine: r} 162 | if rate > 0 { 163 | d := time.Duration(rand.ExpFloat64() * float64(time.Second) / rate) 164 | if prevTest.result[StageTotal].time < d { 165 | time.Sleep(d - prevTest.result[StageTotal].time) 166 | } 167 | } 168 | test.testOnce() 169 | outChan <- test.result 170 | } 171 | if !r.config.short { 172 | r.db.Close() 173 | } 174 | } 175 | 176 | func collectInto(result *[StageMax]TestResult, ret *SummerySet) { 177 | var bigA big.Int 178 | for i := byte(0); i < StageMax; i++ { 179 | summery := &(ret.summery[i]) 180 | summery.count++ 181 | if result[i].ok { 182 | if result[i].time > summery.maxTime { 183 | summery.maxTime = result[i].time 184 | } 185 | if result[i].time < summery.minTime { 186 | summery.minTime = result[i].time 187 | } 188 | summery.totalTime += result[i].time 189 | bigA.SetInt64((int64)(result[i].time)).Mul(&bigA, &bigA) 190 | summery.totalSquareTime.Add(&(summery.totalSquareTime), &bigA) 191 | } else { 192 | summery.failCount++ 193 | if result[i].err != nil { 194 | summery.lastError = result[i].err 195 | } 196 | } 197 | } 198 | } 199 | 200 | func summeryRoutine(inChan <-chan [StageMax]TestResult, outChan chan<- SummerySet, summeryIntervalSecond int) { 201 | var ret SummerySet 202 | var ticker *time.Ticker 203 | ret.Init() 204 | 205 | if summeryIntervalSecond > 0 { 206 | summeryInterval := time.Second * time.Duration(summeryIntervalSecond) 207 | ticker = time.NewTicker(summeryInterval) 208 | loop: 209 | for { 210 | select { 211 | case result, ok := <-inChan: 212 | if !ok { 213 | break loop 214 | } 215 | collectInto(&result, &ret) 216 | case <-ticker.C: 217 | ret.Summery() 218 | outChan <- ret 219 | ret = SummerySet{} 220 | ret.Init() 221 | } 222 | } 223 | } else { 224 | for result := range inChan { 225 | collectInto(&result, &ret) 226 | } 227 | } 228 | ret.Summery() 229 | outChan <- ret 230 | close(outChan) 231 | return 232 | } 233 | 234 | func (r *SummeryResult) Summery() { 235 | var bigA, big2 big.Int 236 | var bigR1, bigR2, big1N, big1N1 big.Rat 237 | big2.SetInt64(2) 238 | // ∑(i-miu)2 = ∑(i2)-(∑i)2/n 239 | n := r.count - r.failCount 240 | if n > 1 { 241 | r.avgTime = (time.Duration)((int64)(r.totalTime) / n) 242 | 243 | big1N.SetInt64(n).Inv(&big1N) // 1/n 244 | big1N1.SetInt64(n - 1).Inv(&big1N1) // 1/(n-1) 245 | bigA.SetInt64((int64)(r.totalTime)).Mul(&bigA, &bigA) // (∑i)2 246 | bigR1.SetInt(&bigA).Mul(&bigR1, &big1N) // (∑i)2/n 247 | bigR2.SetInt(&r.totalSquareTime).Sub(&bigR2, &bigR1) 248 | s2, _ := bigR2.Mul(&bigR2, &big1N1).Float64() 249 | r.stddevTime = (time.Duration)((int64)(math.Sqrt(s2))) 250 | } 251 | if r.minTime == math.MaxInt64 { 252 | r.minTime = 0 253 | } 254 | } 255 | 256 | func msStr(t time.Duration) string { 257 | return fmt.Sprintf("%0.3f ms", float64(int64(t)/1000)/1000.0) 258 | } 259 | 260 | type NullLogger struct{} 261 | 262 | func (*NullLogger) Print(v ...interface{}) { 263 | } 264 | 265 | type arrayFlags []string 266 | 267 | func (a *arrayFlags) String() string { 268 | return fmt.Sprintf("%v", *a) 269 | } 270 | func (a *arrayFlags) Set(value string) error { 271 | *a = append(*a, value) 272 | return nil 273 | } 274 | 275 | func badArg() { 276 | flag.Usage() 277 | os.Exit(1) 278 | } 279 | 280 | // mysqlburst -c 2000 -r 30 -d 'mha:M616VoUJBnYFi0L02Y24@tcp(10.200.180.54:3342)/x?timeout=5s&readTimeout=3s&writeTimeout=3s' 281 | func Main() { 282 | runtime.GOMAXPROCS(runtime.NumCPU()) 283 | //go func() { 284 | // http.ListenAndServe("localhost:6060", nil) 285 | //}() 286 | //driver.ErrBadConn = errors.New(driver.ErrBadConn) 287 | config := Config{ 288 | qps: math.MaxInt32, 289 | repeat: 1, 290 | } 291 | //procs := 0 292 | //rounds := 0 293 | //repeat := 1 294 | //dsn := "" 295 | driverLog := false 296 | var queries arrayFlags 297 | summeryIntervalSec := 0 298 | myCfg := mysql.Config{ 299 | Net: "tcp", 300 | InterpolateParams: true, 301 | MaxAllowedPacket: 16777216, 302 | AllowNativePasswords: true, 303 | } 304 | flag.Usage = func() { 305 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 306 | fmt.Fprintln(os.Stderr, " e.g. ./mysqlburst -c 100 -r 1000 -a 127.0.0.1:3306 -d mysql -u user -p pswd -q 'select * from user limit 1' -i 1") 307 | fmt.Fprintln(os.Stderr, "params:") 308 | flag.PrintDefaults() 309 | } 310 | 311 | flag.IntVar(&config.procs, "c", 100, "concurrency") 312 | flag.IntVar(&config.rounds, "r", 1000, "rounds") 313 | flag.IntVar(&config.repeat, "n", 1, "repeat queries in a connection") 314 | flag.IntVar(&config.qps, "qps", 0, "max qps. <= 0 means no limit") 315 | flag.StringVar(&myCfg.Addr, "a", "127.0.0.1:3306", "mysql server address") 316 | flag.StringVar(&myCfg.User, "u", "root", "user") 317 | flag.StringVar(&myCfg.Passwd, "p", "", "password") 318 | flag.StringVar(&myCfg.DBName, "d", "mysql", "database") 319 | flag.DurationVar(&myCfg.Timeout, "cto", 1*time.Second, "connect timeout") 320 | flag.DurationVar(&myCfg.ReadTimeout, "rto", 5*time.Second, "read timeout") 321 | flag.DurationVar(&myCfg.WriteTimeout, "wto", 5*time.Second, "write timeout") 322 | flag.Var(&queries, "q", "queries") 323 | flag.BoolVar(&driverLog, "l", false, "enable driver log, will be written to stderr") 324 | flag.BoolVar(&config.short, "t", false, "use short connection, reconnect before each test") 325 | flag.IntVar(&summeryIntervalSec, "i", 0, "summery interval (sec)") 326 | flag.Parse() 327 | 328 | if !driverLog { 329 | mysql.SetLogger(&NullLogger{}) 330 | } 331 | if len(queries) == 0 { 332 | badArg() 333 | return 334 | } 335 | config.dsn = myCfg.FormatDSN() 336 | config.queries = queries 337 | wg := sync.WaitGroup{} 338 | wg.Add(config.procs) 339 | resultChan := make(chan [StageMax]TestResult, config.procs*8) 340 | summeryChan := make(chan SummerySet, 16) 341 | go func() { 342 | for i := 0; i < config.procs; i++ { 343 | go func() { 344 | (&Routine{config: config}).run(resultChan) 345 | wg.Done() 346 | }() 347 | } 348 | wg.Wait() 349 | close(resultChan) 350 | }() 351 | go summeryRoutine(resultChan, summeryChan, summeryIntervalSec) 352 | 353 | titles := [StageMax]string{ 354 | "connect", "query", "read", "total", 355 | } 356 | for set := range summeryChan { 357 | fmt.Printf("[ %s ] time: %s\n", set.endTime, msStr(set.endTime.Sub(set.startTime))) 358 | var i byte 359 | if config.short { 360 | i = 0 361 | } else { 362 | i = 1 363 | } 364 | for ; i < StageMax; i++ { 365 | title := titles[i] 366 | summery := set.summery[i] 367 | errStr := "-" 368 | lastErr := summery.lastError 369 | if lastErr != nil { 370 | errStr = lastErr.Error() 371 | } 372 | fmt.Printf("%-8s count: %-10d failed: %-8d avg: %-14s min: %-14s max: %-14s stddev: %-14s err: %s\n", 373 | title, summery.count, summery.failCount, 374 | msStr(summery.avgTime), msStr(summery.minTime), msStr(summery.maxTime), msStr(summery.stddevTime), 375 | errStr) 376 | } 377 | fmt.Println() 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiezhenye/mysqlburst 2 | 3 | go 1.16 4 | 5 | require github.com/go-sql-driver/mysql v1.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 2 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | -------------------------------------------------------------------------------- /mysqlburst.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xiezhenye/mysqlburst/burst" 5 | ) 6 | 7 | func main() { 8 | burst.Main() 9 | } 10 | --------------------------------------------------------------------------------