压测工具

This commit is contained in:
meixiongfeng 2022-11-24 21:30:50 +08:00
parent 94482d6eb8
commit 3ead5b1abc
38 changed files with 3461 additions and 4 deletions

View File

@ -33,6 +33,7 @@ func init() {
RootCmd.AddCommand(runCmd) RootCmd.AddCommand(runCmd)
initLog() initLog()
robot.InitDb() robot.InitDb()
} }
var account = flag.String("account", "", "登录账号") var account = flag.String("account", "", "登录账号")
@ -47,7 +48,7 @@ func CloneNewHero(hero *pb.DBHero) (newHero *pb.DBHero) {
return return
} }
func main() { func main() {
//Execute() Execute()
} }
var runCmd = &cobra.Command{ var runCmd = &cobra.Command{

View File

@ -12,8 +12,8 @@ type Options struct {
func DefaultOpts() *Options { func DefaultOpts() *Options {
return &Options{ return &Options{
WsUrl: "ws://10.0.5.73:7891/gateway", WsUrl: "ws://10.0.0.9:7891/gateway",
RegUrl: "http://10.0.5.73:8000/register", RegUrl: "http://10.0.0.9:8000/register",
Create: false, Create: false,
ServerId: "1", ServerId: "1",
} }

5
go.mod
View File

@ -37,8 +37,10 @@ require (
go.mongodb.org/mongo-driver v1.5.1 go.mongodb.org/mongo-driver v1.5.1
go.uber.org/multierr v1.6.0 go.uber.org/multierr v1.6.0
golang.org/x/net v0.2.0 golang.org/x/net v0.2.0
google.golang.org/grpc v1.46.2
google.golang.org/protobuf v1.28.0 google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
) )
require ( require (
@ -48,6 +50,7 @@ require (
github.com/onsi/gomega v1.20.0 // indirect github.com/onsi/gomega v1.20.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect github.com/smartystreets/assertions v1.2.0 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
) )
require ( require (
@ -181,7 +184,7 @@ require (
golang.org/x/mod v0.7.0 // indirect golang.org/x/mod v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.2.0 // indirect golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/text v0.4.0
golang.org/x/tools v0.3.0 // indirect golang.org/x/tools v0.3.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect

15
go.sum
View File

@ -122,6 +122,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -146,6 +150,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
@ -828,6 +833,7 @@ go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoT
go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg=
go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc= go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@ -854,6 +860,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -1253,6 +1260,8 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@ -1276,6 +1285,9 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1288,6 +1300,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@ -1332,6 +1345,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab h1:05KeMI4s7jEdIfHb7QCjUr5X2BRA0gjLZLZEmmjGNc4=
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab/go.mod h1:pFWM9De99EY9TPVyHIyA56QmoRViVck/x41WFkUlc9A=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

969
stress/README.md Normal file
View File

@ -0,0 +1,969 @@
# go实现的压测工具【单台机器100w连接压测实战】
本文介绍压测是什么,解释压测的专属名词,教大家如何压测。介绍市面上的常见压测工具(ab、locust、Jmeter、go实现的压测工具、云压测),对比这些压测工具,教大家如何选择一款适合自己的压测工具,本文还有两个压测实战项目:
- 单台机器对 HTTP 短连接 QPS 1W+ 的压测实战
- 单台机器 100W 长连接的压测实战
- 对 grpc 接口进行压测
- 支持http1.1和2.0长连接
> 简单扩展即可支持 私有协议
## 目录
- [1、项目说明](#1项目说明)
- [1.1 go-stress-testing](#11-go-stress-testing)
- [1.2 项目体验](#12-项目体验)
- [2、压测](#2压测)
- [2.1 压测是什么](#21-压测是什么)
- [2.2 为什么要压测](#22-为什么要压测)
- [2.3 压测名词解释](#23-压测名词解释)
- [2.3.1 压测类型解释](#231-压测类型解释)
- [2.3.2 压测名词解释](#232-压测名词解释)
- [2.3.3 机器性能指标解释](#233-机器性能指标解释)
- [2.3.4 访问指标解释](#234-访问指标解释)
- [3.4 如何计算压测指标](#24-如何计算压测指标)
- [3、常见的压测工具](#3常见的压测工具)
- [3.1 ab](#31-ab)
- [3.2 locust](#32-locust)
- [3.3 JMeter](#33-JMeter)
- [3.4 云压测](#34-云压测)
- [3.4.1 云压测介绍](#341-云压测介绍)
- [3.4.2 阿里云 性能测试 PTS](#342-阿里云-性能测试-PTS)
- [3.4.3 腾讯云 压测大师 LM](#343-腾讯云-压测大师-LM)
- [4、go-stress-testing go语言实现的压测工具](#4go-stress-testing-go语言实现的压测工具)
- [4.1 介绍](#41-介绍)
- [4.2 用法](#42-用法)
- [4.3 实现](#43-实现)
- [4.4 go-stress-testing 对 Golang web 压测](#44-go-stress-testing-对-golang-web-压测)
- [4.5 grpc压测](#45-grpc压测)
- [5、压测工具的比较](#5压测工具的比较)
- [5.1 比较](#51-比较)
- [5.2 如何选择压测工具](#52-如何选择压测工具)
- [6、单台机器100w连接压测实战](#6单台机器100w连接压测实战)
- [6.1 说明](#61-说明)
- [6.2 内核优化](#62-内核优化)
- [6.3 客户端配置](#63-客户端配置)
- [6.4 准备](#64-准备)
- [6.5 压测数据](#65-压测数据)
- [7、常见问题](#7常见问题)
- [8、总结](#8总结)
- [9、参考文献](#9参考文献)
## 1、项目说明
### 1.1 go-stress-testing
go 实现的压测工具,每个用户用一个协程的方式模拟,最大限度的利用 CPU 资源
### 1.2 项目体验
- 可以在 mac/linux/windows 不同平台下执行的命令
- [go-stress-testing](https://github.com/link1st/go-stress-testing/releases) 压测工具下载地址
参数说明:
`-c` 表示并发数
`-n` 每个并发执行请求的次数,总请求的次数 = 并发数 `*` 每个并发执行请求的次数
`-u` 需要压测的地址
```shell
# 运行 以mac为示例
./go-stress-testing-mac -c 1 -n 100 -u https://www.baidu.com/
```
- 压测结果展示
执行以后,终端每秒钟都会输出一次结果,压测完成以后输出执行的压测结果
压测结果展示:
```
─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────
耗时│ 并发数 │ 成功数│ 失败数 │ qps │最长耗时 │最短耗时│平均耗时 │ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────
1s│ 1│ 8│ 0│ 8.09│ 133.16│ 110.98│ 123.56│200:8
2s│ 1│ 15│ 0│ 8.02│ 138.74│ 110.98│ 124.61│200:15
3s│ 1│ 23│ 0│ 7.80│ 220.43│ 110.98│ 128.18│200:23
4s│ 1│ 31│ 0│ 7.83│ 220.43│ 110.23│ 127.67│200:31
5s│ 1│ 39│ 0│ 7.81│ 220.43│ 110.23│ 128.03│200:39
6s│ 1│ 46│ 0│ 7.72│ 220.43│ 110.23│ 129.59│200:46
7s│ 1│ 54│ 0│ 7.79│ 220.43│ 110.23│ 128.42│200:54
8s│ 1│ 62│ 0│ 7.81│ 220.43│ 110.23│ 128.09│200:62
9s│ 1│ 70│ 0│ 7.79│ 220.43│ 110.23│ 128.33│200:70
10s│ 1│ 78│ 0│ 7.82│ 220.43│ 106.47│ 127.85│200:78
11s│ 1│ 84│ 0│ 7.64│ 371.02│ 106.47│ 130.96│200:84
12s│ 1│ 91│ 0│ 7.63│ 371.02│ 106.47│ 131.02│200:91
13s│ 1│ 99│ 0│ 7.66│ 371.02│ 106.47│ 130.54│200:99
13s│ 1│ 100│ 0│ 7.66│ 371.02│ 106.47│ 130.52│200:100
************************* 结果 stat ****************************
处理协程数量: 1
请求总数: 100 总请求时间: 13.055 秒 successNum: 100 failureNum: 0
************************* 结果 end ****************************
```
参数解释:
**耗时**: 程序运行耗时。程序每秒钟输出一次压测结果
**并发数**: 并发数,启动的协程数
**成功数**: 压测中,请求成功的数量
**失败数**: 压测中,请求失败的数量
**qps**: 当前压测的QPS(每秒钟处理请求数量)
**最长耗时**: 压测中,单个请求最长的响应时长
**最短耗时**: 压测中,单个请求最短的响应时长
**平均耗时**: 压测中,单个请求平均的响应时长
**错误码**: 压测中,接口返回的 code码:返回次数的集合
## 2、压测
### 2.1 压测是什么
压测,即压力测试,是确立系统稳定性的一种测试方法,通常在系统正常运作范围之外进行,以考察其功能极限和隐患。
主要检测服务器的承受能力,包括用户承受能力(多少用户同时玩基本不影响质量)、流量承受等。
### 2.2 为什么要压测
- 压测的目的就是通过压测(模拟真实用户的行为),测算出机器的性能(单台机器的QPS),从而推算出系统在承受指定用户数(100W)时,需要多少机器能支撑得住
- 压测是在上线前为了应对未来可能达到的用户数量的一次预估(提前演练),压测以后通过优化程序的性能或准备充足的机器,来保证用户的体验。
### 2.3 压测名词解释
#### 2.3.1 压测类型解释
| 压测类型 | 解释 |
| :---- | :---- |
| 压力测试(Stress Testing) | 也称之为强度测试,测试一个系统的最大抗压能力,在强负载(大数据、高并发)的情况下,测试系统所能承受的最大压力,预估系统的瓶颈 |
| 并发测试(Concurrency Testing) | 通过模拟很多用户同一时刻访问系统或对系统某一个功能进行操作,来测试系统的性能,从中发现问题(并发读写、线程控制、资源争抢) |
| 耐久性测试(Configuration Testing) | 通过对系统在大负荷的条件下长时间运行,测试系统、机器的长时间运行下的状况,从中发现问题(内存泄漏、数据库连接池不释放、资源不回收) |
#### 2.3.2 压测名词解释
| 压测名词 | 解释 |
| :---- | :---- |
| 并发(Concurrency) | 指一个处理器同时处理多个任务的能力(逻辑上处理的能力) |
| 并行(Parallel) | 多个处理器或者是多核的处理器同时处理多个不同的任务(物理上同时执行) |
| QPS(每秒钟查询数量 Query Per Second) | 服务器每秒钟处理请求数量 (req/sec 请求数/秒 一段时间内总请求数/请求时间) |
| 事务(Transactions) | 是用户一次或者是几次请求的集合 |
| TPS(每秒钟处理事务数量 Transaction Per Second) | 服务器每秒钟处理事务数量(一个事务可能包括多个请求) |
| 请求成功数(Request Success Number) | 在一次压测中,请求成功的数量 |
| 请求失败数(Request Failures Number) | 在一次压测中,请求失败的数量 |
| 错误率(Error Rate) | 在压测中,请求成功的数量与请求失败数量的比率 |
| 最大响应时间(Max Response Time) | 在一次压测中,从发出请求或指令系统做出的反映(响应)的最大时间 |
| 最少响应时间(Mininum Response Time) | 在一次压测中,从发出请求或指令系统做出的反映(响应)的最少时间 |
| 平均响应时间(Average Response Time) | 在一次压测中,从发出请求或指令系统做出的反映(响应)的平均时间 |
#### 2.3.3 机器性能指标解释
| 机器性能 | 解释 |
| :---- | :---- |
| CUP利用率(CPU Usage) | CUP 利用率分用户态、系统态和空闲态CPU利用率是指:CPU执行非系统空闲进程的时间与CPU总执行时间的比率 |
| 内存使用率(Memory usage) | 内存使用率指的是此进程所开销的内存。 |
| IO(Disk input/ output) | 磁盘的读写包速率 |
| 网卡负载(Network Load) | 网卡的进出带宽,包量 |
#### 2.3.4 访问指标解释
| 访问 | 解释 |
| :---- | :---- |
| PV(页面浏览量 Page View) | 用户每打开1个网站页面记录1个PV。用户多次打开同一页面PV值累计多次 |
| UV(网站独立访客 Unique Visitor) | 通过互联网访问、流量网站的自然人。1天内相同访客多次访问网站只计算为1个独立访客 |
### 2.4 如何计算压测指标
- 压测我们需要有目的性的压测,这次压测我们需要达到什么目标(如:单台机器的性能为 100QPS?网站能同时满足100W人同时在线)
- 可以通过以下计算方法来进行计算:
- 压测原则:每天80%的访问量集中在20%的时间里这20%的时间就叫做峰值
- 公式: ( 总PV数`*`80% ) / ( 每天的秒数`*`20% ) = 峰值时间每秒钟请求数(QPS)
- 机器: 峰值时间每秒钟请求数(QPS) / 单台机器的QPS = 需要的机器的数量
- 假设:网站每天的用户数(100W)每天的用户的访问量约为3000W PV这台机器的需要多少QPS?
> ( 30000000\*0.8 ) / (86400 * 0.2) ≈ 1389 (QPS)
- 假设:单台机器的的QPS是69需要需要多少台机器来支撑
> 1389 / 69 ≈ 20
## 3、常见的压测工具
### 3.1 ab
- 简介
ApacheBench 是 Apache 服务器自带的一个web压力测试工具简称 ab。ab 又是一个命令行工具,对发起负载的本机要求很低,根据 ab 命令可以创建很多的并发访问线程,模拟多个访问者同时对某一 URL 地址进行访问,因此可以用来测试目标服务器的负载压力。总的来说 ab 工具小巧简单,上手学习较快,可以提供需要的基本性能指标,但是没有图形化结果,不能监控。
ab 属于一个轻量级的压测工具,结果不会特别准确,可以用作参考。
- 安装
```shell
# 在linux环境安装
sudo yum -y install httpd
```
- 用法
```
Usage: ab [options] [http[s]://]hostname[:port]/path
用法ab [选项] 地址
选项:
Options are:
-n requests #执行的请求数,即一共发起多少请求。
-c concurrency #请求并发数
-s timeout #指定每个请求的超时时间默认是30秒。
-k #启用HTTP KeepAlive功能即在一个HTTP会话中执行多个请求。默认时不启用KeepAlive功能。
```
- 压测命令
```shell
# 使用ab压测工具对百度的链接 请求100次并发数1
ab -n 100 -c 1 https://www.baidu.com/
```
压测结果
```
~ >ab -n 100 -c 1 https://www.baidu.com/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking www.baidu.com (be patient).....done
Server Software: BWS/1.1
Server Hostname: www.baidu.com
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Document Path: /
Document Length: 227 bytes
Concurrency Level: 1
Time taken for tests: 9.430 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 89300 bytes
HTML transferred: 22700 bytes
Requests per second: 10.60 [#/sec] (mean)
Time per request: 94.301 [ms] (mean)
Time per request: 94.301 [ms] (mean, across all concurrent requests)
Transfer rate: 9.25 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 54 70 16.5 69 180
Processing: 18 24 12.0 23 140
Waiting: 18 24 12.0 23 139
Total: 72 94 20.5 93 203
Percentage of the requests served within a certain time (ms)
50% 93
66% 99
75% 101
80% 102
90% 108
95% 122
98% 196
99% 203
100% 203 (longest request)
```
- 主要关注的测试指标
- `Concurrency Level` 并发请求数
- `Time taken for tests` 整个测试时间
- `Complete requests` 完成请求个数
- `Failed requests` 失败个数
- `Requests per second` 吞吐量,指的是某个并发用户下单位时间内处理的请求数。等效于 QPS其实可以看作同一个统计方式只是叫法不同而已。
- `Time per request` 用户平均请求等待时间
- `Time per request` 服务器处理时间
### 3.2 Locust
- 简介
是非常简单易用、分布式、python 开发的压力测试工具。有图形化界面,支持将压测数据导出。
- 安装
```shell
# pip3 安装locust
pip3 install locust
# 查看是否安装成功
locust -h
# 运行 Locust 分布在多个进程/机器库
pip3 install pyzmq
# webSocket 压测库
pip3 install websocket-client
```
- 用法
编写压测脚本 **test.py**
```python
from locust import HttpUser, TaskSet, task
# 定义用户行为
class UserBehavior(TaskSet):
@task
def baidu_index(self):
self.client.get("/")
class WebsiteUser(HttpUser):
task = [UserBehavior] # 指向一个定义的用户行为类
min_wait = 3000 # 执行事务之间用户等待时间的下界(单位:毫秒)
max_wait = 6000 # 执行事务之间用户等待时间的上界(单位:毫秒)
```
- 启动压测
```shell
locust -f test.py --host=https://www.baidu.com
```
访问 http://localhost:8089 进入压测首页
Number of users to simulate 模拟用户数
Hatch rate (users spawned/second) 每秒钟增加用户数
点击 "Start swarming" 进入压测页面
![locust 首页](http://img.91vh.com/img/locust%20%E9%A6%96%E9%A1%B5.png)
压测界面右上角有:被压测的地址、当前状态、RPS、失败率、开始或重启按钮
性能测试参数
- `Type` 请求的类型例如GET/POST
- `Name` 请求的路径
- `Request` 当前请求的数量
- `Fails` 当前请求失败的数量
- `Median` 中间值,单位毫秒,请求响应时间的中间值
- `Average` 平均值,单位毫秒,请求的平均响应时间
- `Min` 请求的最小服务器响应时间,单位毫秒
- `Max` 请求的最大服务器响应时间,单位毫秒
- `Average size` 单个请求的大小,单位字节
- `Current RPS` 代表吞吐量(Requests Per Second的缩写)指的是某个并发用户数下单位时间内处理的请求数。等效于QPS其实可以看作同一个统计方式只是叫法不同而已。
![locust 压测页面](http://img.91vh.com/img/locust%20%E5%8E%8B%E6%B5%8B%E9%A1%B5%E9%9D%A2.png)
### 3.3 JMeter
- 简介
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试它最初被设计用于Web应用测试但后来扩展到其他测试领域。
JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。
- 安装
访问 https://jmeter-plugins.org/install/Install/ 下载解压以后即可使用
- 用法
JMeter的功能过于强大这里暂时不介绍用法可以查询相关文档使用(参考文献中有推荐的教程文档)
### 3.4 云压测
#### 3.4.1 云压测介绍
顾名思义就是将压测脚本部署在云端,通过云端对对我们的应用进行全方位压测,只需要配置压测的参数,无需准备实体机,云端自动给我们分配需要压测的云主机,对被压测目标进行压测。
云压测的优势:
1. 轻易的实现分布式部署
2. 能够模拟海量用户的访问
3. 流量可以从全国各地发起,更加真实的反映用户的体验
4. 全方位的监控压测指标
5. 文档比较完善
当然了云压测是一款商业产品,在使用的时候自然还是需要收费的,而且价格还是比较昂贵的~
#### 3.4.2 阿里云 性能测试 PTS
PTSPerformance Testing Service是面向所有技术背景人员的云化测试工具。有别于传统工具的繁复PTS以互联网化的交互提供性能测试、API调试和监测等多种能力。自研和适配开源的功能都可以轻松模拟任意体量的用户访问业务的场景任务随时发起免去繁琐的搭建和维护成本。更是紧密结合监控、流控等兄弟产品提供一站式高可用能力高效检验和管理业务性能。
阿里云同样还是支持渗透测试,通过模拟黑客对业务系统进行全面深入的安全测试。
#### 3.4.3 腾讯云 压测大师 LM
通过创建虚拟机器人模拟多用户的并发场景,提供一整套完整的服务器压测解决方案
## 4、go-stress-testing go语言实现的压测工具
### 4.1 介绍
- go-stress-testing 是go语言实现的简单压测工具源码开源、支持二次开发可以压测http、webSocket请求、私有rpc调用使用协程模拟单个用户可以更高效的利用CPU资源。
- 项目地址 [https://github.com/link1st/go-stress-testing](https://github.com/link1st/go-stress-testing)
### 4.2 用法
- [go-stress-testing](https://github.com/link1st/go-stress-testing/releases) 下载地址
- clone 项目源码运行的时候,需要将项目 clone 到 **$GOPATH** 目录下
- 支持参数:
```
Usage of ./go-stress-testing-mac:
-c uint
并发数 (default 1)
-n uint
请求数(单个并发/协程) (default 1)
-u string
压测地址
-d string
调试模式 (default "false")
-http2
是否开http2.0
-k 是否开启长连接
-m int
单个host最大连接数 (default 1)
-H value
自定义头信息传递给服务器 示例:-H 'Content-Type: application/json'
-data string
HTTP POST方式传送数据
-v string
验证方法 http 支持:statusCode、json webSocket支持:json
-p string
curl文件路径
```
- `-n` 是单个用户请求的次数,请求总次数 = `-c`* `-n` 这里考虑的是模拟用户行为,所以这个是每个用户请求的次数
- 下载以后执行下面命令即可压测
- 使用示例:
```
# 查看用法
./go-stress-testing-mac
# 使用请求百度页面
./go-stress-testing-mac -c 1 -n 100 -u https://www.baidu.com/
# 使用debug模式请求百度页面
./go-stress-testing-mac -c 1 -n 1 -d true -u https://www.baidu.com/
# 使用 curl文件(文件在curl目录下) 的方式请求
./go-stress-testing-mac -c 1 -n 1 -p curl/baidu.curl.txt
# 压测webSocket连接
./go-stress-testing-mac -c 10 -n 10 -u ws://127.0.0.1:8089/acc
```
- 完整压测命令示例
```shell script
# 更多参数 支持 header、post body
go run main.go -c 1 -n 1 -d true -u 'https://page.aliyun.com/delivery/plan/list' \
-H 'authority: page.aliyun.com' \
-H 'accept: application/json, text/plain, */*' \
-H 'content-type: application/x-www-form-urlencoded' \
-H 'origin: https://cn.aliyun.com' \
-H 'sec-fetch-site: same-site' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-dest: empty' \
-H 'referer: https://cn.aliyun.com/' \
-H 'accept-language: zh-CN,zh;q=0.9' \
-H 'cookie: aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn' \
-data 'adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D'
```
- 使用 curl文件进行压测
curl是Linux在命令行下的工作的文件传输工具是一款很强大的http命令行工具。
使用curl文件可以压测使用非GET的请求支持设置http请求的 method、cookies、header、body等参数
**I:** chrome 浏览器生成 curl文件打开开发者模式(快捷键F12),如图所示,生成 curl 在终端执行命令
![chrome cURL](http://img.91vh.com/img/copy%20cURL.png)
**II:** postman 生成 curl 命令
![postman cURL](http://img.91vh.com/img/postman%20cURL.png)
生成内容粘贴到项目目录下的**curl/baidu.curl.txt**文件中执行下面命令就可以从curl.txt文件中读取需要压测的内容进行压测了
```
# 使用 curl文件(文件在curl目录下) 的方式请求
go run main.go -c 1 -n 1 -p curl/baidu.curl.txt
```
### 4.3 实现
- 具体需求可以查看项目源码
- 项目目录结构
```
|____main.go // main函数获取命令行参数
|____server // 处理程序目录
| |____dispose.go // 压测启动,注册验证器、启动统计函数、启动协程进行压测
| |____statistics // 统计目录
| | |____statistics.go // 接收压测统计结果并处理
| |____golink // 建立连接目录
| | |____http_link.go // http建立连接
| | |____websocket_link.go // webSocket建立连接
| |____client // 请求数据客户端目录
| | |____http_client.go // http客户端
| | |____websocket_client.go // webSocket客户端
| |____verify // 对返回数据校验目录
| | |____http_verify.go // http返回数据校验
| | |____websokcet_verify.go // webSocket返回数据校验
|____heper // 通用函数目录
| |____heper.go // 通用函数
|____model // 模型目录
| |____request_model.go // 请求数据模型
| |____curl_model.go // curl文件解析
|____vendor // 项目依赖目录
```
### 4.4 go-stress-testing 对 Golang web 压测
这里使用go-stress-testing对go server进行压测(部署在同一台机器上),并统计压测结果
- 申请的服务器配置
CPU: 4核 (Intel Xeon(Cascade Lake) Platinum 8269 2.5 GHz/3.2 GHz)
内存: 16G
硬盘: 20G SSD
系统: CentOS 7.6
go version: go1.12.9 linux/amd64
![go-stress-testing01](http://img.91vh.com/img/go-stress-testing01.png)
- go server
```golangpackage main
import (
"log"
"net/http"
"runtime"
)
const (
httpPort = "8088"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU() - 1)
hello := func(w http.ResponseWriter, req *http.Request) {
data := "Hello, go-stress-testing! \n"
w.Header().Add("Server", "golang")
w.Write([]byte(data))
return
}
http.HandleFunc("/", hello)
err := http.ListenAndServe(":"+httpPort, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
```
- go_stress_testing 压测命令
```
./go-stress-testing-linux -c 100 -n 10000 -u http://127.0.0.1:8088/
```
- 压测结果
- [压测结果 示例](https://github.com/link1st/go-stress-testing/issues/32)
| 并发数 | go_stress_testing QPS |
| :----: | :----: |
| 1 | 6394.86 |
| 4 | 16909.36 |
| 10 | 18456.81 |
| 20 | 19490.50 |
| 30 | 19947.47 |
| 50 | 19922.56 |
| 80 | 19155.33 |
| 100 | 18336.46 |
从压测的结果上看效果还不错压测QPS有接近2W
### 4.5 grpc压测
- 介绍如何压测 grpc 接口
> [添加对 grpc 接口压测 commit](https://github.com/link1st/go-stress-testing/commit/2b4b14aaf026d08276531cf76f42de90efd3bc61)
- 1. 启动Server
```shell script
# 进入 grpc server 目录
cd tests/grpc
# 启动 grpc server
go run main.go
```
- 2. 对 grpc server 协议进行压测
```
# 回到项目根目录
go run main.go -c 300 -n 1000 -u grpc://127.0.0.1:8099 -data world
开始启动 并发数:300 请求数:1000 请求参数:
request:
form:grpc
url:grpc://127.0.0.1:8099
method:POST
headers:map[Content-Type:application/x-www-form-urlencoded; charset=utf-8]
data:world
verify:
timeout:30s
debug:false
─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────
耗时 │ 并发数 │ 成功数 │ 失败数 │ qps │最长耗时 │最短耗时 │平均耗时 │下载字节 │字节每秒 │ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────
1s│ 186│ 14086│ 0│34177.69│ 22.40│ 0.63│ 8.78│ │ │200:14086
2s│ 265│ 30408│ 0│26005.09│ 32.68│ 0.63│ 11.54│ │ │200:30408
3s│ 300│ 46747│ 0│21890.46│ 40.84│ 0.63│ 13.70│ │ │200:46747
4s│ 300│ 62837│ 0│20057.06│ 45.81│ 0.63│ 14.96│ │ │200:62837
5s│ 300│ 79119│ 0│19134.52│ 45.81│ 0.63│ 15.68│ │ │200:79119
```
- 如何扩展其它私有协议
> 由于私有协议、grpc 协议 都涉及到代码的书写,所以需要 编写go 的代码才能完成
> 参考 [添加对 grpc 接口压测 commit](https://github.com/link1st/go-stress-testing/commit/2b4b14aaf026d08276531cf76f42de90efd3bc61)
## 5、压测工具的比较
### 5.1 比较
| - | ab | locust | Jmeter | go-stress-testing | 云压测 |
| :---- | :---- | :---- | :---- | :---- | :---- |
| 实现语言 | C | Python | Java | Golang | - |
| UI界面 | 无 | 有 | 有 | 无 | 无 |
| 优势 | 使用简单,上手简单 | 支持分布式、压测数据支持导出 | 插件丰富支持生成HTML报告 | 项目开源使用简单没有依赖支持webSocket压测 | 更加真实的模拟用户,支持更高的压测力度 |
### 5.2 如何选择压测工具
这个世界上**没有最好的,只有最适合的**,工具千千万,选择一款适合你的才是最重要的
在实际使用中有各种场景,选择工具的时候就需要考虑这些:
* 明确你的目的,需要做什么压测、压测的目标是什么?
* 使用的工具你是否熟悉,你愿意花多大的成本了解它?
* 你是为了测试还是想了解其中的原理?
* 工具是否能支持你需要压测的场景
## 6、单台机器100w连接压测实战
### 6.1 说明
之前写了一篇文章,[基于websocket单台机器支持百万连接分布式聊天(IM)系统](https://github.com/link1st/gowebsocket)(不了解这个项目可以查看上一篇或搜索一下文章)这里我们要实现单台机器支持100W连接的压测
目标:
* 单台机器能保持100W个长连接
* 机器的CPU、内存、网络、I/O 状态都正常
说明:
gowebsocket 分布式聊天(IM)系统:
* 之前用户连接以后有个全员广播,这里需要将用户连接、退出等事件关闭
- 服务器准备:
> 由于自己手上没有自己的服务器,所以需要临时购买的云服务器
压测服务器:
16台(稍后解释为什么需要16台机器)
CPU: 2核
内存: 8G
硬盘: 20G
系统: CentOS 7.6
![webSocket压测服务器](http://img.91vh.com/img/webSocket%E5%8E%8B%E6%B5%8B%E6%9C%8D%E5%8A%A1%E5%99%A8.png)
被压测服务:
1台
CPU: 4核
内存: 32G
硬盘: 20G SSD
系统: CentOS 7.6
![webSocket被压测服务器](http://img.91vh.com/img/webSocket%E8%A2%AB%E5%8E%8B%E6%B5%8B%E6%9C%8D%E5%8A%A1%E5%99%A8.png)
### 6.2 内核优化
- 修改程序最大打开文件数
被压测服务器需要保持100W长连接客户和服务器端是通过socket通讯的每个连接需要建立一个socket程序需要保持100W长连接就需要单个程序能打开100W个文件句柄
```
# 查看系统默认的值
ulimit -n
# 设置最大打开文件数
ulimit -n 1040000
```
这里设置的要超过100W程序除了有100W连接还有其它资源连接(数据库、资源等连接),这里设置为 104W
centOS 7.6 上述设置不生效,需要手动修改配置文件
`vim /etc/security/limits.conf`
这里需要把硬限制和软限制、root用户和所有用户都设置为 1040000
core 是限制内核文件的大小,这里设置为 unlimited
```
# 添加以下参数
root soft nofile 1040000
root hard nofile 1040000
root soft nproc 1040000
root hard nproc 1040000
* soft nofile 1040000
* hard nofile 1040000
* soft nproc 1040000
* hard nproc 1040000
root soft core unlimited
root hard core unlimited
* soft core unlimited
* hard core unlimited
```
注意:
`/proc/sys/fs/file-max` 表示系统级别的能够打开的文件句柄的数量不能小于limits中设置的值
如果file-max的值小于limits设置的值会导致系统重启以后无法登录
```
# file-max 设置的值参考
cat /proc/sys/fs/file-max
12553500
```
修改以后重启服务器,`ulimit -n` 查看配置是否生效
### 6.3 客户端配置
由于linux端口的范围是 `0~65535(2^16-1)`这个和操作系统无关不管linux是32位的还是64位的
这个数字是由于tcp协议决定的tcp协议头部表示端口只有16位所以最大值只有65535(如果每台机器多几个虚拟ip就能突破这个限制)
1024以下是系统保留端口所以能使用的1024到65535
如果需要100W长连接每台机器有 65535-1024 个端口, 100W / (65535-1024) ≈ 15.5所以这里需要16台服务器
- `vim /etc/sysctl.conf` 在文件末尾添加
```
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_mem = 786432 2097152 3145728
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216
```
`sysctl -p` 修改配置以后使得配置生效命令
配置解释:
- `ip_local_port_range` 表示TCP/UDP协议允许使用的本地端口号 范围:1024~65000
- `tcp_mem` 确定TCP栈应该如何反映内存使用每个值的单位都是内存页通常是4KB。第一个值是内存使用的下限第二个值是内存压力模式开始对缓冲区使用应用压力的上限第三个值是内存使用的上限。在这个层次上可以将报文丢弃从而减少对内存的使用。对于较大的BDP可以增大这些值注意其单位是内存页而不是字节
- `tcp_rmem` 为自动调优定义socket使用的内存。第一个值是为socket接收缓冲区分配的最少字节数第二个值是默认值该值会被rmem_default覆盖缓冲区在系统负载不重的情况下可以增长到这个值第三个值是接收缓冲区空间的最大字节数该值会被rmem_max覆盖
- `tcp_wmem` 为自动调优定义socket使用的内存。第一个值是为socket发送缓冲区分配的最少字节数第二个值是默认值该值会被wmem_default覆盖缓冲区在系统负载不重的情况下可以增长到这个值第三个值是发送缓冲区空间的最大字节数该值会被wmem_max覆盖
### 6.4 准备
1. 在被压测服务器上启动Server服务(gowebsocket)
2. 查看被压测服务器的内网端口
3. 登录上16台压测服务器这里我提前把需要优化的系统做成了镜像申请机器的时候就可以直接使用这个镜像(参数已经调好)
![压测服务器16台准备](http://img.91vh.com/img/%E5%8E%8B%E6%B5%8B%E6%9C%8D%E5%8A%A1%E5%99%A816%E5%8F%B0%E5%87%86%E5%A4%87.png)
4. 启动压测
```
./go_stress_testing_linux -c 62500 -n 1 -u ws://192.168.0.74:443/acc
```
`62500*16 = 100W `正好可以达到我们的要求
建立连接以后,`-n 1`发送一个**ping**的消息给服务器,收到响应以后保持连接不中断
5. 通过 gowebsocket服务器的http接口实时查询连接数和项目启动的协程数
6. 压测过程中查看系统状态
```
# linux 命令
ps # 查看进程内存、cup使用情况
iostat # 查看系统IO情况
nload # 查看网络流量情况
/proc/pid/status # 查看进程状态
```
### 6.5 压测数据
- 压测以后查看连接数到100W然后保持10分钟观察系统是否正常
- 观察以后系统运行正常、CPU、内存、I/O 都正常,打开页面都正常
- 压测完成以后的数据
查看goWebSocket连接数统计可以看到 **clientsLen**连接数为100W**goroutine**数量2000008个每个连接两个goroutine加上项目启动默认的8个。这里可以看到连接数满足了100W
![查看goWebSocket连接数统计](http://img.91vh.com/img/%E6%9F%A5%E7%9C%8BgoWebSocket%E8%BF%9E%E6%8E%A5%E6%95%B0%E7%BB%9F%E8%AE%A1.png)
从压测服务上查看连接数是否达到了要求压测完成的统计数据并发数为62500是每个客户端连接的数量,总连接数: `62500*16=100W`
![压测服务16台 压测完成](http://img.91vh.com/img/%E5%8E%8B%E6%B5%8B%E6%9C%8D%E5%8A%A116%E5%8F%B0%20%E5%8E%8B%E6%B5%8B%E5%AE%8C%E6%88%90.png)
- 记录内存使用情况分别记录了1W到100W连接数内存使用情况
| 连接数 | 内存 |
| :----: | :----:|
| 10000 | 281M |
| 100000 | 2.7g |
| 200000 | 5.4g |
| 500000 | 13.1g |
| 1000000 | 25.8g |
100W连接时的查看内存详细数据:
```
cat /proc/pid/status
VmSize: 27133804 kB
```
`27133804/1000000≈27.1` 100W连接占用了25.8g的内存粗略计算了一下一个连接占用了27.1Kb的内存由于goWebSocket项目每个用户连接起了两个协程处理用户的读写事件所以内存占用稍微多一点
如果需要如何减少内存使用可以参考 **@Roy11568780** 大佬给的解决方案
> 传统的golang中是采用的一个goroutine循环read的方法对应每一个socket。实际百万链路场景中这是巨大的资源浪费优化的原理也不是什么新东西golang中一样也可以使用epoll的把fd拿到epoll中检测到事件然后在协程池里面去读就行了看情况读写分别10-20的协程goroutine池应该就足够了
至此压测已经全部完成单台机器支持100W连接已经满足~
## 7.常见问题
- **Q:** 压测过程中会出现大量 **TIME_WAIT**
A: 参考TCP四次挥手原理主动关闭连接的一方会出现 **TIME_WAIT** 状态,等待的时长为 2MSL(约1分钟左右)
原因是:主动断开的一方回复 ACK 消息可能丢失TCP 是可靠的传输协议,在没有收到 ACK 消息的另一端会重试重新发送FIN消息所以主动关闭的一方会等待 2MSL 时间,防止对方重试,这就出现了大量 **TIME_WAIT** 状态(参考: 四次挥手的最后两次)
TCP 握手:
<img border="0" src="http://img.91vh.com/img/TCP%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E3%80%81%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.png" width="830"/>
- **Q:** 没有go环境无法使用最新功能
A 可以使用Dockerfile构建一个容器镜像假设容器镜像名称为gostress:1111docker build -t gostress:1111 .
启动一个名称为go-stress的容器docker run -d --name=go-stress gostress:1111
开始压测 docker exec -it go-stress -c 10 -n 10 -u www.baidu.com
## 8、总结
到这里压测总算完成本次压测花费16元巨款。
单台机器支持100W连接是实测是满足的但是实际业务比较复杂还是需要持续优化~
本文通过介绍什么是压测在什么情况下需要压测通过单台机器100W长连接的压测实战了解Linux内核的参数的调优。如果觉得现有的压测工具不适用可以自己实现或者是改造成属于自己的自己的工具。
## 9、参考文献
[性能测试工具](https://testerhome.com/topics/17068)
[性能测试常见名词解释](https://blog.csdn.net/r455678/article/details/53063989)
[性能测试名词解释](https://codeigniter.org.cn/forums/blog-39678-2456.html)
[PV、TPS、QPS是怎么计算出来的](https://www.zhihu.com/question/21556347)
[超实用压力测试工具ab工具](https://www.jianshu.com/p/43d04d8baaf7)
[Locust 介绍](http://www.testclass.net/locust/introduce)
[Jmeter性能测试 入门](https://www.cnblogs.com/TankXiao/p/4045439.html)
[基于websocket单台机器支持百万连接分布式聊天(IM)系统](https://github.com/link1st/gowebsocket)
[https://github.com/link1st/go-stress-testing](https://github.com/link1st/go-stress-testing)
github 搜:link1st 查看项目 go-stress-testing
### 意见反馈
- 在项目中遇到问题可以直接在这里找找答案或者提问 [issues](https://github.com/link1st/go-stress-testing/issues)
- 也可以添加我的微信(申请信息填写:公司、姓名,我好备注下),直接反馈给我
<br/>
<p align="center">
<img border="0" src="http://img.91vh.com/img/%E5%BE%AE%E4%BF%A1%E4%BA%8C%E7%BB%B4%E7%A0%81.jpeg" alt="添加link1st的微信" width="200"/>
</p>
### 赞助商
- 感谢[JetBrains](https://www.jetbrains.com/?from=gowebsocket)对本项目的支持!
<br/>
<p align="center">
<a href="https://www.jetbrains.com/?from=gowebsocket">
<img border="0" src="http://img.91vh.com/img/jetbrains_logo.png" width="200"/>
</a>
</p>

8
stress/build.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# 编译mac下可以执行文件
go build -ldflags "-s -w" -o go-stress-testing-mac main.go
# 使用交叉编译 linux和windows版本可以执行的文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o go-stress-testing-linux main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o go-stress-testing-win.exe main.go

1
stress/curl/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
!.gitignore

View File

@ -0,0 +1,11 @@
curl 'https://www.baidu.com/sugrec?prod=pc_his&from=pc_web&json=1&sid=1464_21098_31424_31341_31464_31229_30823_31163_31475&hisdata=&req=2&csor=0' \
-H 'Connection: keep-alive' \
-H 'Accept: application/json, text/javascript, */*; q=0.01' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Referer: https://www.baidu.com/' \
-H 'Accept-Language: zh-CN,zh;q=0.9' \
-H 'Cookie: BIDUPSID=A2CDAA36D74F85E5007CAA415E35B9DF; PSTM=1588732560; BAIDUID=A2CDAA36D74F85E59E4B8060EC4A0230:FG=1; BD_HOME=1; BD_UPN=123253; H_PS_PSSID=1464_21098_31424_31341_31464_31229_30823_31163_31475' \
--compressed

11
stress/curl/test.curl.txt Normal file
View File

@ -0,0 +1,11 @@
curl 'https://www.baidu.com/sugrec?prod=pc_his&from=pc_web&json=1&sid=1464_21098_31424_31341_31464_31229_30823_31163_31475&hisdata=&req=2&csor=0' \
-H 'Connection: keep-alive' \
-H 'Accept: application/json, text/javascript, */*; q=0.01' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Referer: https://www.baidu.com/' \
-H 'Accept-Language: zh-CN,zh;q=0.9' \
-H 'Cookie: BIDUPSID=A2CDAA36D74F85E5007CAA415E35B9DF; PSTM=1588732560; BAIDUID=A2CDAA36D74F85E59E4B8060EC4A0230:FG=1; BD_HOME=1; BD_UPN=123253; H_PS_PSSID=1464_21098_31424_31341_31464_31229_30823_31163_31475' \
--compressed

View File

@ -0,0 +1,14 @@
curl 'https://page.aliyun.com/delivery/plan/list' \
-H 'authority: page.aliyun.com' \
-H 'accept: application/json, text/plain, */*' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36' \
-H 'content-type: application/x-www-form-urlencoded' \
-H 'origin: https://cn.aliyun.com' \
-H 'sec-fetch-site: same-site' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-dest: empty' \
-H 'referer: https://cn.aliyun.com/' \
-H 'accept-language: zh-CN,zh;q=0.9' \
-H 'cookie: aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn' \
--data 'adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D' \
--compressed

View File

@ -0,0 +1,4 @@
curl -X GET \
'https://www.baidu.com/sugrec?prod=pc_his&from=pc_web&json=1&sid=1464_21098_31424_31341_31464_31229_30823_31163_31475&hisdata=&req=2&csor=0' \
-H 'Postman-Token: c9b71950-61fd-43be-a38a-6596de238f0f' \
-H 'cache-control: no-cache'

23
stress/helper/helper.go Normal file
View File

@ -0,0 +1,23 @@
// Package helper 帮助函数,时间、数组的通用处理
package helper
import (
"time"
)
// DiffNano 时间差,纳秒
func DiffNano(startTime time.Time) (diff int64) {
diff = int64(time.Since(startTime))
return
}
// InArrayStr 判断字符串是否在数组内
func InArrayStr(str string, arr []string) (inArray bool) {
for _, s := range arr {
if s == str {
inArray = true
break
}
}
return
}

204
stress/model/curl_model.go Normal file
View File

@ -0,0 +1,204 @@
// Package model 数据模型
package model
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"strings"
"go_dreamfactory/stress/helper"
)
// CURL curl参数解析
type CURL struct {
Data map[string][]string
}
// getDataValue 获取数据
func (c *CURL) getDataValue(keys []string) []string {
var (
value = make([]string, 0)
)
for _, key := range keys {
var (
ok bool
)
value, ok = c.Data[key]
if ok {
break
}
}
return value
}
// ParseTheFile 从文件中解析curl
func ParseTheFile(path string) (curl *CURL, err error) {
if path == "" {
err = errors.New("路径不能为空")
return
}
curl = &CURL{
Data: make(map[string][]string),
}
file, err := os.Open(path)
if err != nil {
err = errors.New("打开文件失败:" + err.Error())
return
}
defer func() {
_ = file.Close()
}()
dataBytes, err := ioutil.ReadAll(file)
if err != nil {
err = errors.New("读取文件失败:" + err.Error())
return
}
data := string(dataBytes)
for len(data) > 0 {
if strings.HasPrefix(data, "curl") {
data = data[5:]
}
data = strings.TrimSpace(data)
var (
key string
value string
)
index := strings.Index(data, " ")
if index <= 0 {
break
}
key = strings.TrimSpace(data[:index])
data = data[index+1:]
data = strings.TrimSpace(data)
// url
if !strings.HasPrefix(key, "-") {
key = strings.Trim(key, "'")
curl.Data["curl"] = []string{key}
// 去除首尾空格
data = strings.TrimFunc(data, func(r rune) bool {
if r == ' ' || r == '\\' || r == '\n' {
return true
}
return false
})
continue
}
if strings.HasPrefix(data, "-") {
continue
}
var (
endSymbol = " "
)
if strings.HasPrefix(data, "'") {
endSymbol = "'"
data = data[1:]
}
index = strings.Index(data, endSymbol)
if index <= -1 {
index = len(data)
// break
}
value = data[:index]
if len(data) >= index+1 {
data = data[index+1:]
} else {
data = ""
}
// 去除首尾空格
data = strings.TrimFunc(data, func(r rune) bool {
if r == ' ' || r == '\\' || r == '\n' {
return true
}
return false
})
if key == "" {
continue
}
curl.Data[key] = append(curl.Data[key], value)
}
return
}
// String string
func (c *CURL) String() (url string) {
curlByte, _ := json.Marshal(c)
return string(curlByte)
}
// GetURL 获取url
func (c *CURL) GetURL() (url string) {
keys := []string{"curl", "--url"}
value := c.getDataValue(keys)
if len(value) <= 0 {
return
}
url = value[0]
return
}
// GetMethod 获取 请求方式
func (c *CURL) GetMethod() (method string) {
keys := []string{"-X", "--request"}
value := c.getDataValue(keys)
if len(value) <= 0 {
return c.defaultMethod()
}
method = strings.ToUpper(value[0])
if helper.InArrayStr(method, []string{"GET", "POST", "PUT", "DELETE"}) {
return method
}
return c.defaultMethod()
}
// defaultMethod 获取默认方法
func (c *CURL) defaultMethod() (method string) {
method = "GET"
body := c.GetBody()
if len(body) > 0 {
return "POST"
}
return
}
// GetHeaders 获取请求头
func (c *CURL) GetHeaders() (headers map[string]string) {
headers = make(map[string]string, 0)
keys := []string{"-H", "--header"}
value := c.getDataValue(keys)
for _, v := range value {
getHeaderValue(v, headers)
}
return
}
// GetHeadersStr 获取请求头string
func (c *CURL) GetHeadersStr() string {
headers := c.GetHeaders()
bytes, _ := json.Marshal(&headers)
return string(bytes)
}
// GetBody 获取body
func (c *CURL) GetBody() (body string) {
keys := []string{"--data", "-d", "--data-urlencode", "--data-raw", "--data-binary"}
value := c.getDataValue(keys)
if len(value) <= 0 {
body = c.getPostForm()
return
}
body = value[0]
return
}
// getPostForm get post form
func (c *CURL) getPostForm() (body string) {
keys := []string{"--form", "-F", "--form-string"}
value := c.getDataValue(keys)
if len(value) <= 0 {
return
}
body = strings.Join(value, "&")
return
}

View File

@ -0,0 +1,24 @@
// Package model 数据模型
package model
import (
"fmt"
"testing"
)
// TestCurl 测试函数
func TestCurl(t *testing.T) {
// ../curl.txt
c, err := ParseTheFile("../curl/post.curl.txt")
fmt.Println(c, err)
if err != nil {
return
}
fmt.Printf("curl:%s \n", c.String())
fmt.Printf("url:%s \n", c.GetURL())
fmt.Printf("method:%s \n", c.GetMethod())
fmt.Printf("body:%v \n", c.GetBody())
fmt.Printf("body string:%v \n", c.GetBody())
fmt.Printf("headers:%s \n", c.GetHeadersStr())
}

View File

@ -0,0 +1,284 @@
// Package model 请求数据模型package model
package model
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
// 返回 code 码
const (
// HTTPOk 请求成功
HTTPOk = 200
// RequestErr 请求错误
RequestErr = 509
// ParseError 解析错误
ParseError = 510 // 解析错误
)
// 支持协议
const (
// FormTypeHTTP http 协议
FormTypeHTTP = "http"
// FormTypeWebSocket webSocket 协议
FormTypeWebSocket = "webSocket"
// FormTypeGRPC grpc 协议
FormTypeGRPC = "grpc"
FormTypeRadius = "radius"
)
// 校验函数
var (
// verifyMapHTTP http 校验函数
verifyMapHTTP = make(map[string]VerifyHTTP)
// verifyMapHTTPMutex http 并发锁
verifyMapHTTPMutex sync.RWMutex
// verifyMapWebSocket webSocket 校验函数
verifyMapWebSocket = make(map[string]VerifyWebSocket)
// verifyMapWebSocketMutex webSocket 并发锁
verifyMapWebSocketMutex sync.RWMutex
)
// RegisterVerifyHTTP 注册 http 校验函数
func RegisterVerifyHTTP(verify string, verifyFunc VerifyHTTP) {
verifyMapHTTPMutex.Lock()
defer verifyMapHTTPMutex.Unlock()
key := fmt.Sprintf("%s.%s", FormTypeHTTP, verify)
verifyMapHTTP[key] = verifyFunc
}
// RegisterVerifyWebSocket 注册 webSocket 校验函数
func RegisterVerifyWebSocket(verify string, verifyFunc VerifyWebSocket) {
verifyMapWebSocketMutex.Lock()
defer verifyMapWebSocketMutex.Unlock()
key := fmt.Sprintf("%s.%s", FormTypeWebSocket, verify)
verifyMapWebSocket[key] = verifyFunc
}
// Verify 验证器
type Verify interface {
GetCode() int // 有一个方法返回code为200为成功
GetResult() bool // 返回是否成功
}
// VerifyHTTP http 验证
type VerifyHTTP func(request *Request, response *http.Response) (code int, isSucceed bool)
// VerifyWebSocket webSocket 验证
type VerifyWebSocket func(request *Request, seq string, msg []byte) (code int, isSucceed bool)
// Request 请求数据
type Request struct {
URL string // URL
Form string // http/webSocket/tcp
Method string // 方法 GET/POST/PUT
Headers map[string]string // Headers
Body string // body
Verify string // 验证的方法
Timeout time.Duration // 请求超时时间
Debug bool // 是否开启Debug模式
MaxCon int // 每个连接的请求数
HTTP2 bool // 是否使用http2.0
Keepalive bool // 是否开启长连接
Code int // 验证的状态码
}
// GetBody 获取请求数据
func (r *Request) GetBody() (body io.Reader) {
return strings.NewReader(r.Body)
}
// getVerifyKey 获取校验 key
func (r *Request) getVerifyKey() (key string) {
return fmt.Sprintf("%s.%s", r.Form, r.Verify)
}
// GetVerifyHTTP 获取数据校验方法
func (r *Request) GetVerifyHTTP() VerifyHTTP {
verify, ok := verifyMapHTTP[r.getVerifyKey()]
if !ok {
panic("GetVerifyHTTP 验证方法不存在:" + r.Verify)
}
return verify
}
// GetVerifyWebSocket 获取数据校验方法
func (r *Request) GetVerifyWebSocket() VerifyWebSocket {
verify, ok := verifyMapWebSocket[r.getVerifyKey()]
if !ok {
panic("GetVerifyWebSocket 验证方法不存在:" + r.Verify)
}
return verify
}
// NewRequest 生成请求结构体
// url 压测的url
// verify 验证方法 在server/verify中 http 支持:statusCode、json webSocket支持:json
// timeout 请求超时时间
// debug 是否开启debug
// path curl文件路径 http接口压测自定义参数设置
func NewRequest(url string, verify string, code int, timeout time.Duration, debug bool, path string,
reqHeaders []string,
reqBody string, maxCon int, http2 bool, keepalive bool) (request *Request, err error) {
var (
method = "GET"
headers = make(map[string]string)
body string
)
if path != "" {
var curl *CURL
curl, err = ParseTheFile(path)
if err != nil {
return nil, err
}
if url == "" {
url = curl.GetURL()
}
method = curl.GetMethod()
headers = curl.GetHeaders()
body = curl.GetBody()
} else {
if reqBody != "" {
method = "POST"
body = reqBody
}
for _, v := range reqHeaders {
getHeaderValue(v, headers)
}
if _, ok := headers["Content-Type"]; !ok {
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
}
}
form := ""
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
form = FormTypeHTTP
} else if strings.HasPrefix(url, "ws://") || strings.HasPrefix(url, "wss://") {
form = FormTypeWebSocket
} else if strings.HasPrefix(url, "grpc://") || strings.HasPrefix(url, "rpc://") {
form = FormTypeGRPC
} else if strings.HasPrefix(url, "radius://") {
form = FormTypeRadius
url = url[9:]
} else {
form = FormTypeHTTP
url = fmt.Sprintf("http://%s", url)
}
if form == "" {
err = fmt.Errorf("url:%s 不合法,必须是完整http、webSocket连接", url)
return
}
var ok bool
switch form {
case FormTypeHTTP:
// verify
if verify == "" {
verify = "statusCode"
}
key := fmt.Sprintf("%s.%s", form, verify)
_, ok = verifyMapHTTP[key]
if !ok {
err = errors.New("验证器不存在:" + key)
return
}
case FormTypeWebSocket:
// verify
if verify == "" {
verify = "json"
}
key := fmt.Sprintf("%s.%s", form, verify)
_, ok = verifyMapWebSocket[key]
if !ok {
err = errors.New("验证器不存在:" + key)
return
}
}
if timeout == 0 {
timeout = 30 * time.Second
}
request = &Request{
URL: url,
Form: form,
Method: strings.ToUpper(method),
Headers: headers,
Body: body,
Verify: verify,
Timeout: timeout,
Debug: debug,
MaxCon: maxCon,
HTTP2: http2,
Keepalive: keepalive,
Code: code,
}
return
}
// getHeaderValue 获取 header
func getHeaderValue(v string, headers map[string]string) {
index := strings.Index(v, ":")
if index < 0 {
return
}
vIndex := index + 1
if len(v) >= vIndex {
value := strings.TrimPrefix(v[vIndex:], " ")
if _, ok := headers[v[:index]]; ok {
headers[v[:index]] = fmt.Sprintf("%s; %s", headers[v[:index]], value)
} else {
headers[v[:index]] = value
}
}
}
// Print 格式化打印
func (r *Request) Print() {
if r == nil {
return
}
result := fmt.Sprintf("request:\n form:%s \n url:%s \n method:%s \n headers:%v \n", r.Form, r.URL, r.Method,
r.Headers)
result = fmt.Sprintf("%s data:%v \n", result, r.Body)
result = fmt.Sprintf("%s verify:%s \n timeout:%s \n debug:%v \n", result, r.Verify, r.Timeout, r.Debug)
result = fmt.Sprintf("%s http2.0%v \n keepalive%v \n maxCon:%v ", result, r.HTTP2, r.Keepalive, r.MaxCon)
fmt.Println(result)
return
}
// GetDebug 获取 debug 参数
func (r *Request) GetDebug() bool {
return r.Debug
}
// IsParameterLegal 参数是否合法
func (r *Request) IsParameterLegal() (err error) {
r.Form = "http"
// statusCode json
r.Verify = "json"
key := fmt.Sprintf("%s.%s", r.Form, r.Verify)
_, ok := verifyMapHTTP[key]
if !ok {
return errors.New("验证器不存在:" + key)
}
return
}
// RequestResults 请求结果
type RequestResults struct {
ID string // 消息ID
ChanID uint64 // 消息ID
Time uint64 // 请求时间 纳秒
IsSucceed bool // 是否请求成功
ErrCode int // 错误码
ReceivedBytes int64
}
// SetID 设置请求唯一ID
func (r *RequestResults) SetID(chanID uint64, number uint64) {
id := fmt.Sprintf("%d_%d", chanID, number)
r.ID = id
r.ChanID = chanID
}

228
stress/proto/pb.pb.go Normal file
View File

@ -0,0 +1,228 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: pb.proto
package protobuf
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// 请求
type Request struct {
// UserName 用户昵称
UserName string `protobuf:"bytes,1,opt,name=userName,proto3" json:"userName,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_f80abaa17e25ccc8, []int{0}
}
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (m *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(m, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
func (m *Request) GetUserName() string {
if m != nil {
return m.UserName
}
return ""
}
// 响应
type Response struct {
// 状态码
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
// 状态码说明
Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"`
// Data 返回数据
Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_f80abaa17e25ccc8, []int{1}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (m *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(m, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetCode() int32 {
if m != nil {
return m.Code
}
return 0
}
func (m *Response) GetMsg() string {
if m != nil {
return m.Msg
}
return ""
}
func (m *Response) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func init() {
proto.RegisterType((*Request)(nil), "protobuf.Request")
proto.RegisterType((*Response)(nil), "protobuf.Response")
}
func init() { proto.RegisterFile("pb.proto", fileDescriptor_f80abaa17e25ccc8) }
var fileDescriptor_f80abaa17e25ccc8 = []byte{
// 170 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x28, 0x48, 0xd2, 0x2b,
0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x00, 0x53, 0x49, 0xa5, 0x69, 0x4a, 0xaa, 0x5c, 0xec, 0x41,
0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x52, 0x5c, 0x1c, 0xa5, 0xc5, 0xa9, 0x45, 0x7e, 0x89,
0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x70, 0xbe, 0x92, 0x0b, 0x17, 0x47, 0x50,
0x6a, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x10, 0x17, 0x4b, 0x72, 0x7e, 0x0a, 0x44, 0x0d,
0x6b, 0x10, 0x98, 0x2d, 0x24, 0xc0, 0xc5, 0x9c, 0x5b, 0x9c, 0x2e, 0xc1, 0x04, 0xd6, 0x06, 0x62,
0x82, 0x54, 0xa5, 0x24, 0x96, 0x24, 0x4a, 0x30, 0x83, 0x85, 0xc0, 0x6c, 0x23, 0x27, 0x2e, 0x4e,
0xc7, 0x82, 0xcc, 0xe0, 0xd4, 0xa2, 0xb2, 0xd4, 0x22, 0x21, 0x53, 0x2e, 0x2e, 0x8f, 0xd4, 0x9c,
0x9c, 0xfc, 0xf0, 0xfc, 0xa2, 0x9c, 0x14, 0x21, 0x41, 0x3d, 0x98, 0x93, 0xf4, 0xa0, 0xee, 0x91,
0x12, 0x42, 0x16, 0x82, 0xd8, 0xad, 0xc4, 0x90, 0xc4, 0x06, 0x16, 0x34, 0x06, 0x04, 0x00, 0x00,
0xff, 0xff, 0x72, 0xb6, 0x62, 0x67, 0xcd, 0x00, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// ApiServerClient is the client API for ApiServer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ApiServerClient interface {
// 接口
HelloWorld(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}
type apiServerClient struct {
cc *grpc.ClientConn
}
func NewApiServerClient(cc *grpc.ClientConn) ApiServerClient {
return &apiServerClient{cc}
}
func (c *apiServerClient) HelloWorld(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
out := new(Response)
err := c.cc.Invoke(ctx, "/protobuf.ApiServer/HelloWorld", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ApiServerServer is the server API for ApiServer service.
type ApiServerServer interface {
// 接口
HelloWorld(context.Context, *Request) (*Response, error)
}
// UnimplementedApiServerServer can be embedded to have forward compatible implementations.
type UnimplementedApiServerServer struct {
}
func (*UnimplementedApiServerServer) HelloWorld(ctx context.Context, req *Request) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method HelloWorld not implemented")
}
func RegisterApiServerServer(s *grpc.Server, srv ApiServerServer) {
s.RegisterService(&_ApiServer_serviceDesc, srv)
}
func _ApiServer_HelloWorld_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServerServer).HelloWorld(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/protobuf.ApiServer/HelloWorld",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServerServer).HelloWorld(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
var _ApiServer_serviceDesc = grpc.ServiceDesc{
ServiceName: "protobuf.ApiServer",
HandlerType: (*ApiServerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "HelloWorld",
Handler: _ApiServer_HelloWorld_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "pb.proto",
}

25
stress/proto/pb.proto Normal file
View File

@ -0,0 +1,25 @@
syntax = "proto3";
package protobuf;
// ApiServer api
service ApiServer {
// HelloWorld
rpc HelloWorld (Request) returns (Response) {
}
}
//
message Request {
// UserName
string userName = 1;
}
//
message Response {
//
int32 code = 1;
//
string msg = 2;
// Data
string data = 3;
}

View File

@ -0,0 +1,9 @@
// Package client clientpackage client
package client
// Clienter 接口 注册、连接、发送 等
type Clienter interface {
GetConn() (err error)
Close() (err error)
Send()
}

View File

@ -0,0 +1,63 @@
// Package client grpc 客户端
package client
import (
"context"
"fmt"
"strings"
"time"
"google.golang.org/grpc"
)
// GrpcSocket grpc
type GrpcSocket struct {
conn *grpc.ClientConn
address string
}
// NewGrpcSocket new
func NewGrpcSocket(address string) (s *GrpcSocket) {
var newAddr string
arr := strings.Split(address, "//")
if len(arr) >= 2 {
newAddr = arr[1]
}
s = &GrpcSocket{
address: newAddr,
}
return
}
// getAddress 获取地址
func (g *GrpcSocket) getAddress() (address string) {
return g.address
}
// Close 关闭
func (g *GrpcSocket) Close() (err error) {
if g == nil {
return
}
if g.conn == nil {
return
}
return g.conn.Close()
}
// Link 建立连接
func (g *GrpcSocket) Link() (err error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, g.address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
return fmt.Errorf("getConn: 连接失败 address:%s %w", g.address, err)
}
g.conn = conn
return
}
// GetConn 获取连接
func (g *GrpcSocket) GetConn() (conn *grpc.ClientConn) {
return g.conn
}

View File

@ -0,0 +1,99 @@
// Package client http 客户端
package client
import (
"crypto/tls"
"log"
"net/http"
"os"
"time"
"go_dreamfactory/stress/model"
httplongclinet "go_dreamfactory/stress/server/client/http_longclinet"
"golang.org/x/net/http2"
"go_dreamfactory/stress/helper"
)
// logErr err
var logErr = log.New(os.Stderr, "", 0)
// HTTPRequest HTTP 请求
// method 方法 GET POST
// url 请求的url
// body 请求的body
// headers 请求头信息
// timeout 请求超时时间
func HTTPRequest(chanID uint64, request *model.Request) (resp *http.Response, requestTime uint64, err error) {
method := request.Method
url := request.URL
body := request.GetBody()
timeout := request.Timeout
headers := request.Headers
req, err := http.NewRequest(method, url, body)
if err != nil {
return
}
// 在req中设置Host解决在header中设置Host不生效问题
if _, ok := headers["Host"]; ok {
req.Host = headers["Host"]
}
// 设置默认为utf-8编码
if _, ok := headers["Content-Type"]; !ok {
if headers == nil {
headers = make(map[string]string)
}
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
}
for key, value := range headers {
req.Header.Set(key, value)
}
var client *http.Client
if request.Keepalive {
client = httplongclinet.NewClient(chanID, request)
startTime := time.Now()
resp, err = client.Do(req)
requestTime = uint64(helper.DiffNano(startTime))
if err != nil {
logErr.Println("请求失败:", err)
return
}
return
} else {
req.Close = true
tr := &http.Transport{}
if request.HTTP2 {
// 使用真实证书 验证证书 模拟真实请求
tr = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
if err = http2.ConfigureTransport(tr); err != nil {
return
}
} else {
// 跳过证书验证
tr = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
client = &http.Client{
Transport: tr,
Timeout: timeout,
}
}
startTime := time.Now()
resp, err = client.Do(req)
requestTime = uint64(helper.DiffNano(startTime))
if err != nil {
logErr.Println("请求失败:", err)
return
}
return
}

View File

@ -0,0 +1,75 @@
package httplongclinet
import (
"crypto/tls"
"net"
"net/http"
"sync"
"time"
"go_dreamfactory/stress/model"
"golang.org/x/net/http2"
)
var (
mutex sync.RWMutex
clients = make(map[uint64]*http.Client, 0)
)
// NewClient new
func NewClient(i uint64, request *model.Request) *http.Client {
client := getClient(i)
if client != nil {
return client
}
return setClient(i, request)
}
func getClient(i uint64) *http.Client {
mutex.RLock()
defer mutex.RUnlock()
return clients[i]
}
func setClient(i uint64, request *model.Request) *http.Client {
mutex.Lock()
defer mutex.Unlock()
client := createLangHttpClient(request)
clients[i] = client
return client
}
// createLangHttpClient 初始化长连接客户端参数
func createLangHttpClient(request *model.Request) *http.Client {
tr := &http.Transport{}
if request.HTTP2 {
// 使用真实证书 验证证书 模拟真实请求
tr = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 0, // 最大连接数,默认0无穷大
MaxIdleConnsPerHost: request.MaxCon, // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)
IdleConnTimeout: 90 * time.Second, // 多长时间未使用自动关闭连接
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
_ = http2.ConfigureTransport(tr)
} else {
// 跳过证书验证
tr = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 0, // 最大连接数,默认0无穷大
MaxIdleConnsPerHost: request.MaxCon, // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)
IdleConnTimeout: 90 * time.Second, // 多长时间未使用自动关闭连接
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
return &http.Client{
Transport: tr,
}
}

View File

@ -0,0 +1,118 @@
// Package client webSocket 客户端
package client
import (
"errors"
"fmt"
"net/url"
"strings"
"golang.org/x/net/websocket"
)
const (
connRetry = 3 // 建立连接重试次数
)
// WebSocket webSocket
type WebSocket struct {
conn *websocket.Conn
URLLink string
URL *url.URL
IsSsl bool
}
// NewWebSocket new
func NewWebSocket(urlLink string) (ws *WebSocket) {
var isSsl bool
if strings.HasPrefix(urlLink, "wss://") {
isSsl = true
}
u, err := url.Parse(urlLink)
// 解析失败
if err != nil {
panic(err)
}
ws = &WebSocket{
URLLink: urlLink,
URL: u,
IsSsl: isSsl,
}
return
}
// getLink 获取连接
func (w *WebSocket) getLink() (link string) {
return w.URLLink
}
// getOrigin 获取源连接
func (w *WebSocket) getOrigin() (origin string) {
origin = "http://"
if w.IsSsl {
origin = "https://"
}
origin = fmt.Sprintf("%s%s/", origin, w.URL.Host)
return
}
// Close 关闭
func (w *WebSocket) Close() (err error) {
if w == nil {
return
}
if w.conn == nil {
return
}
return w.conn.Close()
}
// GetConn 获取连接
func (w *WebSocket) GetConn() (err error) {
var (
conn *websocket.Conn
i int
)
for i = 0; i < connRetry; i++ {
conn, err = websocket.Dial(w.getLink(), "", w.getOrigin())
if err != nil {
fmt.Println("GetConn 建立连接失败 in...", i, err)
continue
}
w.conn = conn
return
}
if err != nil {
fmt.Println("GetConn 建立连接失败", i, err)
}
return
}
// Write 发送数据
func (w *WebSocket) Write(body []byte) (err error) {
if w.conn == nil {
err = errors.New("未建立连接")
return
}
_, err = w.conn.Write(body)
if err != nil {
fmt.Println("发送数据失败:", err)
return
}
return
}
// Read 接收数据
func (w *WebSocket) Read() (msg []byte, err error) {
if w.conn == nil {
err = errors.New("未建立连接")
return
}
msg = make([]byte, 512)
n, err := w.conn.Read(msg)
if err != nil {
fmt.Println("接收数据失败:", err)
return nil, err
}
return msg[:n], nil
}

104
stress/server/dispose.go Normal file
View File

@ -0,0 +1,104 @@
// Package server 压测启动
package server
import (
"context"
"fmt"
"go_dreamfactory/stress/model"
"sync"
"time"
"go_dreamfactory/stress/server/client"
"go_dreamfactory/stress/server/golink"
"go_dreamfactory/stress/server/statistics"
"go_dreamfactory/stress/server/verify"
)
const (
connectionMode = 1 // 1:顺序建立长链接 2:并发建立长链接
)
// init 注册验证器
func init() {
// http
model.RegisterVerifyHTTP("statusCode", verify.HTTPStatusCode)
model.RegisterVerifyHTTP("json", verify.HTTPJson)
// webSocket
model.RegisterVerifyWebSocket("json", verify.WebSocketJSON)
}
// Dispose 处理函数
func Dispose(ctx context.Context, concurrency, totalNumber uint64, request *model.Request) {
// 设置接收数据缓存
ch := make(chan *model.RequestResults, 1000)
var (
wg sync.WaitGroup // 发送数据完成
wgReceiving sync.WaitGroup // 数据处理完成
)
wgReceiving.Add(1)
go statistics.ReceivingResults(concurrency, ch, &wgReceiving)
for i := uint64(0); i < concurrency; i++ {
wg.Add(1)
switch request.Form {
case model.FormTypeHTTP:
go golink.HTTP(ctx, i, ch, totalNumber, &wg, request)
case model.FormTypeWebSocket:
switch connectionMode {
case 1:
// 连接以后再启动协程
ws := client.NewWebSocket(request.URL)
err := ws.GetConn()
if err != nil {
fmt.Println("连接失败:", i, err)
continue
}
go golink.WebSocket(ctx, i, ch, totalNumber, &wg, request, ws)
case 2:
// 并发建立长链接
go func(i uint64) {
// 连接以后再启动协程
ws := client.NewWebSocket(request.URL)
err := ws.GetConn()
if err != nil {
fmt.Println("连接失败:", i, err)
return
}
golink.WebSocket(ctx, i, ch, totalNumber, &wg, request, ws)
}(i)
// 注意:时间间隔太短会出现连接失败的报错 默认连接时长:20毫秒(公网连接)
time.Sleep(5 * time.Millisecond)
default:
data := fmt.Sprintf("不支持的类型:%d", connectionMode)
panic(data)
}
case model.FormTypeGRPC:
// 连接以后再启动协程
ws := client.NewGrpcSocket(request.URL)
err := ws.Link()
if err != nil {
fmt.Println("连接失败:", i, err)
continue
}
go golink.Grpc(ctx, i, ch, totalNumber, &wg, request, ws)
case model.FormTypeRadius:
// Radius use udp, does not a connection
go golink.Radius(ctx, i, ch, totalNumber, &wg, request)
default:
// 类型不支持
wg.Done()
}
}
// 等待所有的数据都发送完成
wg.Wait()
// 延时1毫秒 确保数据都处理完成了
time.Sleep(1 * time.Millisecond)
close(ch)
// 数据全部处理完成了
wgReceiving.Wait()
return
}

View File

@ -0,0 +1,73 @@
// Package golink 连接
package golink
import (
"context"
"sync"
"time"
"go_dreamfactory/stress/helper"
pb "go_dreamfactory/stress/proto"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/server/client"
)
// Grpc grpc 接口请求
func Grpc(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64, wg *sync.WaitGroup,
request *model.Request, ws *client.GrpcSocket) {
defer func() {
wg.Done()
}()
defer func() {
_ = ws.Close()
}()
for i := uint64(0); i < totalNumber; i++ {
grpcRequest(chanID, ch, i, request, ws)
}
return
}
// grpcRequest 请求
func grpcRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request,
ws *client.GrpcSocket) {
var (
startTime = time.Now()
isSucceed = false
errCode = model.HTTPOk
)
// 需要发送的数据
conn := ws.GetConn()
if conn == nil {
errCode = model.RequestErr
} else {
// TODO::请求接口示例
c := pb.NewApiServerClient(conn)
var (
ctx = context.Background()
req = &pb.Request{
UserName: request.Body,
}
)
rsp, err := c.HelloWorld(ctx, req)
// fmt.Printf("rsp:%+v", rsp)
if err != nil {
errCode = model.RequestErr
} else {
// 200 为成功
if rsp.Code != 200 {
errCode = model.RequestErr
} else {
isSucceed = true
}
}
}
requestTime := uint64(helper.DiffNano(startTime))
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}

View File

@ -0,0 +1,105 @@
// Package golink 连接
package golink
import (
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/server/client"
)
// HTTP 请求
func HTTP(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64, wg *sync.WaitGroup,
request *model.Request) {
defer func() {
wg.Done()
}()
// fmt.Printf("启动协程 编号:%05d \n", chanID)
for i := uint64(0); i < totalNumber; i++ {
if ctx.Err() != nil {
fmt.Printf("ctx.Err err: %v \n", ctx.Err())
break
}
list := getRequestList(request)
isSucceed, errCode, requestTime, contentLength := sendList(chanID, list)
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
ReceivedBytes: contentLength,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}
return
}
// sendList 多个接口分步压测
func sendList(chanID uint64, requestList []*model.Request) (isSucceed bool, errCode int, requestTime uint64,
contentLength int64) {
errCode = model.HTTPOk
for _, request := range requestList {
succeed, code, u, length := send(chanID, request)
isSucceed = succeed
errCode = code
requestTime = requestTime + u
contentLength = contentLength + length
if succeed == false {
break
}
}
return
}
// send 发送一次请求
func send(chanID uint64, request *model.Request) (bool, int, uint64, int64) {
var (
// startTime = time.Now()
isSucceed = false
errCode = model.HTTPOk
contentLength = int64(0)
err error
resp *http.Response
requestTime uint64
)
newRequest := getRequest(request)
resp, requestTime, err = client.HTTPRequest(chanID, newRequest)
if err != nil {
errCode = model.RequestErr // 请求错误
} else {
// 此处原方式获取的数据长度可能是 -1换成如下方式获取可获取到正确的长度
contentLength, err = getBodyLength(resp)
if err != nil {
contentLength = resp.ContentLength
}
// 验证请求是否成功
errCode, isSucceed = newRequest.GetVerifyHTTP()(newRequest, resp)
}
return isSucceed, errCode, requestTime, contentLength
}
// getBodyLength 获取响应数据长度
func getBodyLength(response *http.Response) (length int64, err error) {
var reader io.ReadCloser
switch response.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(response.Body)
defer func() {
_ = reader.Close()
}()
default:
reader = response.Body
}
body, err := ioutil.ReadAll(reader)
return int64(len(body)), err
}

View File

@ -0,0 +1,71 @@
// Package golink 连接
package golink
import (
"time"
"go_dreamfactory/stress/model"
)
// ReqListMany 接口分步压测
type ReqListMany struct {
list []*model.Request
}
// getCount 获取连接
func (r *ReqListMany) getCount() int {
return len(r.list)
}
var (
clientList *ReqListMany
)
// init 接口分步压测示例
func init() {
clientList = &ReqListMany{}
// TODO::接口分步压测示例
// 需要压测的接口参数
clients := make([]*model.Request, 0)
// 压测第一步
clients = append(clients, &model.Request{
URL: "https://page.aliyun.com/delivery/plan/list", // 请求url
Form: "http", // 请求方式 示例参数:http/webSocket/tcp
Method: "POST", // 请求方法 示例参数:GET/POST/PUT
Headers: map[string]string{
"referer": "https://cn.aliyun.com/",
"cookie": "aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn",
}, // headers 头信息
Body: "adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D", // 消息体
Verify: "statusCode", // 验证的方法 示例参数:statusCode、json
Timeout: 30 * time.Second, // 是否开启Debug模式
Debug: false, // 是否开启Debug模式
})
// 压测第二步
clients = append(clients, &model.Request{
URL: "https://page.aliyun.com/delivery/plan/list", // 请求url
Form: "http", // 请求方式 示例参数:http/webSocket/tcp
Method: "POST", // 请求方法 示例参数:GET/POST/PUT
Headers: map[string]string{
"referer": "https://cn.aliyun.com/",
"cookie": "aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn",
}, // headers 头信息
Body: "adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D", // 消息体
Verify: "statusCode", // 验证的方法 示例参数:statusCode、json
Timeout: 30 * time.Second, // 是否开启Debug模式
Debug: false, // 是否开启Debug模式
})
clientList.list = clients
// TODO::分步压测时,注释下面一行代码
clientList.list = nil
}
// getRequestList 获取请求列表
func getRequestList(request *model.Request) []*model.Request {
if len(clientList.list) <= 0 {
return []*model.Request{request}
}
return clientList.list
}

View File

@ -0,0 +1,97 @@
// Package golink 连接
package golink
import (
"math/rand"
"time"
"go_dreamfactory/stress/model"
)
// ReqListWeigh 接口加权压测
type ReqListWeigh struct {
list []Req
weighCount uint32 // 总权重
}
// Req req
type Req struct {
req *model.Request // 请求信息
weights uint32 // 权重,数字越大访问频率越高
}
// setWeighCount 设置权重
func (r *ReqListWeigh) setWeighCount() {
r.weighCount = 0
for _, value := range r.list {
r.weighCount = r.weighCount + value.weights
}
}
var (
clientWeigh *ReqListWeigh
r *rand.Rand
)
// 多接口压测示例
func init() {
// TODO::压测多个接口示例
// 需要压测的接口参数
clients := make([]Req, 0)
clients = append(clients, Req{req: &model.Request{
URL: "https://page.aliyun.com/delivery/plan/list", // 请求url
Form: "http", // 请求方式 示例参数:http/webSocket/tcp
Method: "POST", // 请求方法 示例参数:GET/POST/PUT
Headers: map[string]string{
"referer": "https://cn.aliyun.com/",
"cookie": "aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn",
}, // headers 头信息
Body: "adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D", // 消息体
Verify: "statusCode", // 验证的方法 示例参数:statusCode、json
Timeout: 30 * time.Second, // 是否开启Debug模式
Debug: false, // 是否开启Debug模式
}, weights: 2})
clients = append(clients, Req{req: &model.Request{
URL: "https://page.aliyun.com/delivery/plan/list", // 请求url
Form: "http", // 请求方式 示例参数:http/webSocket/tcp
Method: "POST", // 请求方法 示例参数:GET/POST/PUT
Headers: map[string]string{
"referer": "https://cn.aliyun.com/",
"cookie": "aliyun_choice=CN; JSESSIONID=J8866281-CKCFJ4BUZ7GDO9V89YBW1-KJ3J5V9K-GYUW7; maliyun_temporary_console0=1AbLByOMHeZe3G41KYd5WWZvrM%2BGErkaLcWfBbgveKA9ifboArprPASvFUUfhwHtt44qsDwVqMk8Wkdr1F5LccYk2mPCZJiXb0q%2Bllj5u3SQGQurtyPqnG489y%2FkoA%2FEvOwsXJTvXTFQPK%2BGJD4FJg%3D%3D; cna=L3Q5F8cHDGgCAXL3r8fEZtdU; isg=BFNThsmSCcgX-sUcc5Jo2s2T4tF9COfKYi8g9wVwr3KphHMmjdh3GrHFvPTqJD_C; l=eBaceXLnQGBjstRJBOfwPurza77OSIRAguPzaNbMiT5POw1B5WAlWZbqyNY6C3GVh6lwR37EODnaBeYBc3K-nxvOu9eFfGMmn",
}, // headers 头信息
Body: "adPlanQueryParam=%7B%22adZone%22%3A%7B%22positionList%22%3A%5B%7B%22positionId%22%3A83%7D%5D%7D%2C%22requestId%22%3A%2217958651-f205-44c7-ad5d-f8af92a6217a%22%7D", // 消息体
Verify: "statusCode", // 验证的方法 示例参数:statusCode、json
Timeout: 30 * time.Second, // 是否开启Debug模式
Debug: false, // 是否开启Debug模式
}, weights: 1})
r = rand.New(rand.NewSource(time.Now().Unix()))
clientWeigh = &ReqListWeigh{
list: clients,
}
// TODO::注释下面一行代码
clientWeigh.list = nil
clientWeigh.setWeighCount()
}
// getRequest 获取请求
func getRequest(request *model.Request) *model.Request {
if clientWeigh == nil || clientWeigh.weighCount <= 0 {
return request
}
n := uint32(r.Int31n(int32(clientWeigh.weighCount)))
var (
count uint32
)
for _, value := range clientWeigh.list {
if count >= n {
// value.req.Print()
return value.req
}
count = count + value.weights
}
panic("getRequest err")
}

View File

@ -0,0 +1,69 @@
package golink
import (
"context"
"strings"
"sync"
"time"
"go_dreamfactory/stress/helper"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
"go_dreamfactory/stress/model"
)
// Grpc grpc 接口请求
func Radius(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64, wg *sync.WaitGroup,
request *model.Request) {
defer func() {
wg.Done()
}()
for i := uint64(0); i < totalNumber; i++ {
authRequest(chanID, ch, i, request)
}
return
}
// grpcRequest 请求
func authRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request) {
var (
startTime = time.Now()
isSucceed = false
errCode = int(radius.CodeAccessAccept)
)
// 需要发送的数据
// fmt.Printf("rsp:%+v", rsp)
packet := radius.New(radius.CodeAccessRequest, []byte(`cisco`))
index := strings.Index(request.URL, "@")
username := "tim"
host := request.URL
if index != -1 {
username = username + "@" + request.URL[index+1:]
host = request.URL[:index]
}
rfc2865.UserName_SetString(packet, username)
rfc2865.UserPassword_SetString(packet, "12345678")
rfc2865.NASPortType_Set(packet, rfc2865.NASPortType_Value_Ethernet)
rfc2865.ServiceType_Set(packet, rfc2865.ServiceType_Value_FramedUser)
rfc2865.NASIdentifier_Set(packet, []byte(`benchmark`))
rsp, err := radius.Exchange(context.Background(), packet, host)
if err != nil {
errCode = model.RequestErr
} else {
if rsp.Code != radius.CodeAccessAccept {
errCode = int(rsp.Code)
} else {
isSucceed = true
}
}
requestTime := uint64(helper.DiffNano(startTime))
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}

View File

@ -0,0 +1,99 @@
// Package golink 连接
package golink
import (
"context"
"fmt"
"sync"
"time"
"go_dreamfactory/stress/helper"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/server/client"
)
const (
firstTime = 1 * time.Second // 连接以后首次请求数据的时间
intervalTime = 1 * time.Second // 发送数据的时间间隔
)
var (
// 请求完成以后是否保持连接
keepAlive bool
)
func init() {
keepAlive = true
}
// WebSocket webSocket go link
func WebSocket(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64,
wg *sync.WaitGroup, request *model.Request, ws *client.WebSocket) {
defer func() {
wg.Done()
}()
defer func() {
_ = ws.Close()
}()
var (
i uint64
)
// 暂停60秒
t := time.NewTimer(firstTime)
for {
select {
case <-t.C:
t.Reset(intervalTime)
// 请求
webSocketRequest(chanID, ch, i, request, ws)
// 结束条件
i = i + 1
if i >= totalNumber {
goto end
}
}
}
end:
t.Stop()
if keepAlive {
// 保持连接
chWaitFor := make(chan int, 0)
<-chWaitFor
}
return
}
// webSocketRequest 请求
func webSocketRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request,
ws *client.WebSocket) {
var (
startTime = time.Now()
isSucceed = false
errCode = model.HTTPOk
msg []byte
)
// 需要发送的数据
seq := fmt.Sprintf("%d_%d", chanID, i)
err := ws.Write([]byte(`{"seq":"` + seq + `","cmd":"ping","data":{}}`))
if err != nil {
errCode = model.RequestErr // 请求错误
} else {
msg, err = ws.Read()
if err != nil {
errCode = model.ParseError
fmt.Println("读取数据 失败~")
} else {
errCode, isSucceed = request.GetVerifyWebSocket()(request, seq, msg)
}
}
requestTime := uint64(helper.DiffNano(startTime))
requestResults := &model.RequestResults{
Time: requestTime,
IsSucceed: isSucceed,
ErrCode: errCode,
}
requestResults.SetID(chanID, i)
ch <- requestResults
}

View File

@ -0,0 +1,217 @@
// Package statistics 统计数据
package statistics
import (
"fmt"
"sort"
"strings"
"sync"
"time"
"go_dreamfactory/stress/tools"
"golang.org/x/text/language"
"golang.org/x/text/message"
"go_dreamfactory/stress/model"
)
var (
// 输出统计数据的时间
exportStatisticsTime = 1 * time.Second
p = message.NewPrinter(language.English)
requestTimeList []uint64 // 所有请求响应时间
)
// ReceivingResults 接收结果并处理
// 统计的时间都是纳秒,显示的时间 都是毫秒
// concurrent 并发数
func ReceivingResults(concurrent uint64, ch <-chan *model.RequestResults, wg *sync.WaitGroup) {
defer func() {
wg.Done()
}()
var stopChan = make(chan bool)
// 时间
var (
processingTime uint64 // 处理总时间
requestTime uint64 // 请求总时间
maxTime uint64 // 最大时长
minTime uint64 // 最小时长
successNum uint64 // 成功处理数code为0
failureNum uint64 // 处理失败数code不为0
chanIDLen int // 并发数
chanIDs = make(map[uint64]bool)
receivedBytes int64
mutex = sync.RWMutex{}
)
statTime := uint64(time.Now().UnixNano())
// 错误码/错误个数
var errCode = &sync.Map{}
// 定时输出一次计算结果
ticker := time.NewTicker(exportStatisticsTime)
go func() {
for {
select {
case <-ticker.C:
endTime := uint64(time.Now().UnixNano())
mutex.Lock()
go calculateData(concurrent, processingTime, endTime-statTime, maxTime, minTime, successNum, failureNum,
chanIDLen, errCode, receivedBytes)
mutex.Unlock()
case <-stopChan:
// 处理完成
return
}
}
}()
header()
for data := range ch {
mutex.Lock()
// fmt.Println("处理一条数据", data.ID, data.Time, data.IsSucceed, data.ErrCode)
processingTime = processingTime + data.Time
if maxTime <= data.Time {
maxTime = data.Time
}
if minTime == 0 {
minTime = data.Time
} else if minTime > data.Time {
minTime = data.Time
}
// 是否请求成功
if data.IsSucceed == true {
successNum = successNum + 1
} else {
failureNum = failureNum + 1
}
// 统计错误码
if value, ok := errCode.Load(data.ErrCode); ok {
valueInt, _ := value.(int)
errCode.Store(data.ErrCode, valueInt+1)
} else {
errCode.Store(data.ErrCode, 1)
}
receivedBytes += data.ReceivedBytes
if _, ok := chanIDs[data.ChanID]; !ok {
chanIDs[data.ChanID] = true
chanIDLen = len(chanIDs)
}
requestTimeList = append(requestTimeList, data.Time)
mutex.Unlock()
}
// 数据全部接受完成,停止定时输出统计数据
stopChan <- true
endTime := uint64(time.Now().UnixNano())
requestTime = endTime - statTime
calculateData(concurrent, processingTime, requestTime, maxTime, minTime, successNum, failureNum, chanIDLen, errCode,
receivedBytes)
fmt.Printf("\n\n")
fmt.Println("************************* 结果 stat ****************************")
fmt.Println("处理协程数量:", concurrent)
// fmt.Println("处理协程数量:", concurrent, "程序处理总时长:", fmt.Sprintf("%.3f", float64(processingTime/concurrent)/1e9), "秒")
fmt.Println("请求总数(并发数*请求数 -c * -n:", successNum+failureNum, "总请求时间:",
fmt.Sprintf("%.3f", float64(requestTime)/1e9),
"秒", "successNum:", successNum, "failureNum:", failureNum)
printTop(requestTimeList)
fmt.Println("************************* 结果 end ****************************")
fmt.Printf("\n\n")
}
// printTop 排序后计算 top 90 95 99
func printTop(requestTimeList []uint64) {
if requestTimeList == nil {
return
}
all := tools.MyUint64List{}
all = requestTimeList
sort.Sort(all)
fmt.Println("tp90:", fmt.Sprintf("%.3f", float64(all[int(float64(len(all))*0.90)]/1e6)))
fmt.Println("tp95:", fmt.Sprintf("%.3f", float64(all[int(float64(len(all))*0.95)]/1e6)))
fmt.Println("tp99:", fmt.Sprintf("%.3f", float64(all[int(float64(len(all))*0.99)]/1e6)))
}
// calculateData 计算数据
func calculateData(concurrent, processingTime, requestTime, maxTime, minTime, successNum, failureNum uint64,
chanIDLen int, errCode *sync.Map, receivedBytes int64) {
if processingTime == 0 {
processingTime = 1
}
var (
qps float64
averageTime float64
maxTimeFloat float64
minTimeFloat float64
requestTimeFloat float64
)
// 平均 QPS 成功数*总协程数/总耗时 (每秒)
if processingTime != 0 {
qps = float64(successNum*1e9*concurrent) / float64(processingTime)
}
// 平均时长 总耗时/总请求数/并发数 纳秒=>毫秒
if successNum != 0 && concurrent != 0 {
averageTime = float64(processingTime) / float64(successNum*1e6)
}
// 纳秒=>毫秒
maxTimeFloat = float64(maxTime) / 1e6
minTimeFloat = float64(minTime) / 1e6
requestTimeFloat = float64(requestTime) / 1e9
// 打印的时长都为毫秒
table(successNum, failureNum, errCode, qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat, chanIDLen,
receivedBytes)
}
// header 打印表头信息
func header() {
fmt.Printf("\n\n")
// 打印的时长都为毫秒 总请数
fmt.Println("─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────")
fmt.Println(" 耗时│ 并发数│ 成功数│ 失败数│ qps │最长耗时│最短耗时│平均耗时│下载字节│字节每秒│ 状态码")
fmt.Println("─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────")
return
}
// table 打印表格
func table(successNum, failureNum uint64, errCode *sync.Map,
qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat float64, chanIDLen int, receivedBytes int64) {
var (
speed int64
)
if requestTimeFloat > 0 {
speed = int64(float64(receivedBytes) / requestTimeFloat)
} else {
speed = 0
}
var (
receivedBytesStr string
speedStr string
)
// 判断获取下载字节长度是否是未知
if receivedBytes <= 0 {
receivedBytesStr = ""
speedStr = ""
} else {
receivedBytesStr = p.Sprintf("%d", receivedBytes)
speedStr = p.Sprintf("%d", speed)
}
// 打印的时长都为毫秒
result := fmt.Sprintf("%4.0fs│%7d│%7d│%7d│%8.2f│%8.2f│%8.2f│%8.2f│%8s│%8s│%v",
requestTimeFloat, chanIDLen, successNum, failureNum, qps, maxTimeFloat, minTimeFloat, averageTime,
receivedBytesStr, speedStr,
printMap(errCode))
fmt.Println(result)
return
}
// printMap 输出错误码、次数 节约字符(终端一行字符大小有限)
func printMap(errCode *sync.Map) (mapStr string) {
var (
mapArr []string
)
errCode.Range(func(key, value interface{}) bool {
mapArr = append(mapArr, fmt.Sprintf("%v:%v", key, value))
return true
})
sort.Strings(mapArr)
mapStr = strings.Join(mapArr, ";")
return
}

View File

@ -0,0 +1,65 @@
/**
* Package statistics
*
* User: link1st
* Date: 2020/9/28
* Time: 14:02
*/
package statistics
import (
"reflect"
"sync"
"testing"
)
// TestPrintMap
func TestPrintMap(t *testing.T) {
a := &sync.Map{}
a.Store(200, 50)
a.Store(100, 20)
a.Store(500, 10)
tt := map[string]struct {
a *sync.Map
result string
}{
"test1": {a: a, result: "100:20;200:50;500:10"},
}
for _, value := range tt {
str := printMap(value.a)
if !reflect.DeepEqual(value.result, str) {
t.Errorf("数据不一致 预期:%v 实际:%v", value.result, str)
}
}
}
func Test_printTop(t *testing.T) {
type args struct {
requestTimeList []uint64
}
tests := []struct {
name string
args args
}{
{
name: "nil",
args: args{
requestTimeList: nil,
},
},
{
name: "one data",
args: args{
requestTimeList: []uint64{1 * 1e6},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
printTop(tt.args.requestTimeList)
})
}
}

View File

@ -0,0 +1,94 @@
// Package verify 校验
package verify
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"go_dreamfactory/stress/model"
)
// getZipData 处理gzip压缩
func getZipData(response *http.Response) (body []byte, err error) {
var reader io.ReadCloser
switch response.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(response.Body)
defer func() {
_ = reader.Close()
}()
default:
reader = response.Body
}
body, err = ioutil.ReadAll(reader)
response.Body = ioutil.NopCloser(bytes.NewReader(body))
return
}
// HTTPStatusCode 通过 HTTP 状态码判断是否请求成功
func HTTPStatusCode(request *model.Request, response *http.Response) (code int, isSucceed bool) {
defer func() {
_ = response.Body.Close()
}()
code = response.StatusCode
if code == request.Code {
isSucceed = true
}
// 开启调试模式
if request.GetDebug() {
body, err := getZipData(response)
fmt.Printf("请求结果 httpCode:%d body:%s err:%v \n", response.StatusCode, string(body), err)
}
io.Copy(ioutil.Discard, response.Body)
return
}
/*************************** 返回值为json ********************************/
// ResponseJSON 返回数据结构体
type ResponseJSON struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// HTTPJson 通过返回的Body 判断
// 返回示例: {"code":200,"msg":"Success","data":{}}
// code 默认将http code作为返回码http code 为200时 取body中的返回code
func HTTPJson(request *model.Request, response *http.Response) (code int, isSucceed bool) {
defer func() {
_ = response.Body.Close()
}()
code = response.StatusCode
if code == http.StatusOK {
body, err := getZipData(response)
if err != nil {
code = model.ParseError
fmt.Printf("请求结果 ioutil.ReadAll err:%v", err)
} else {
responseJSON := &ResponseJSON{}
err = json.Unmarshal(body, responseJSON)
if err != nil {
code = model.ParseError
fmt.Printf("请求结果 json.Unmarshal err:%v", err)
} else {
code = responseJSON.Code
// body 中code返回200为返回数据成功
if responseJSON.Code == request.Code {
isSucceed = true
}
}
}
// 开启调试模式
if request.GetDebug() {
fmt.Printf("请求结果 httpCode:%d body:%s err:%v \n", response.StatusCode, string(body), err)
}
}
io.Copy(ioutil.Discard, response.Body)
return
}

View File

@ -0,0 +1,49 @@
// Package verify 校验
package verify
import (
"encoding/json"
"fmt"
"go_dreamfactory/stress/model"
)
// WebSocketResponseJSON 返回数据结构体返回值为json
type WebSocketResponseJSON struct {
Seq string `json:"seq"`
Cmd string `json:"cmd"`
Response struct {
Code int `json:"code"`
CodeMsg string `json:"codeMsg"`
Data interface{} `json:"data"`
} `json:"response"`
}
// WebSocketJSON 通过返回的Body 判断
// 返回示例: {"seq":"1566276523281-585638","cmd":"heartbeat","response":{"code":200,"codeMsg":"Success","data":null}}
// code 取body中的返回code
func WebSocketJSON(request *model.Request, seq string, msg []byte) (code int, isSucceed bool) {
responseJSON := &WebSocketResponseJSON{}
err := json.Unmarshal(msg, responseJSON)
if err != nil {
code = model.ParseError
fmt.Printf("请求结果 json.Unmarshal msg:%s err:%v", string(msg), err)
} else {
if seq != responseJSON.Seq {
code = model.ParseError
fmt.Println("请求和返回seq不一致 ~请求:", seq, responseJSON.Seq, string(msg))
} else {
code = responseJSON.Response.Code
// body 中code返回200为返回数据成功
if code == 200 {
isSucceed = true
}
}
}
// 开启调试模式
if request.GetDebug() {
fmt.Printf("请求结果 seq:%s body:%s \n", seq, string(msg))
}
return
}

106
stress/stress.go Normal file
View File

@ -0,0 +1,106 @@
// Package main go 实现的压测工具
package main
import (
"context"
"flag"
"fmt"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/server"
"runtime"
"strings"
"time"
)
// array 自定义数组参数
type array []string
// String string
func (a *array) String() string {
return fmt.Sprint(*a)
}
// Set set
func (a *array) Set(s string) error {
*a = append(*a, s)
return nil
}
var (
concurrency uint64 = 1 // 并发数
totalNumber uint64 = 1 // 请求数(单个并发/协程)
debugStr = "false" // 是否是debug
requestURL = "" // 压测的url 目前支持http/https ws/wss
path = "" // curl文件路径 http接口压测自定义参数设置
verify = "" // verify 验证方法 在server/verify中 http 支持:statusCode、json webSocket支持:json
headers array // 自定义头信息传递给服务器
body = "" // HTTP POST方式传送数据
maxCon = 1 // 单个连接最大请求数
code = 200 // 成功状态码
http2 = false // 是否开http2.0
keepalive = false // 是否开启长连接
cpuNumber = 1 // CUP 核数,默认为一核,一般场景下单核已经够用了
timeout int64 = 0 // 超时时间,默认不设置
)
func init() {
flag.Uint64Var(&concurrency, "c", concurrency, "并发数")
flag.Uint64Var(&totalNumber, "n", totalNumber, "请求数(单个并发/协程)")
flag.StringVar(&debugStr, "d", debugStr, "调试模式")
flag.StringVar(&requestURL, "u", requestURL, "压测地址")
flag.StringVar(&path, "p", path, "curl文件路径")
flag.StringVar(&verify, "v", verify, "验证方法 http 支持:statusCode、json webSocket支持:json")
flag.Var(&headers, "H", "自定义头信息传递给服务器 示例:-H 'Content-Type: application/json'")
flag.StringVar(&body, "data", body, "HTTP POST方式传送数据")
flag.IntVar(&maxCon, "m", maxCon, "单个host最大连接数")
flag.IntVar(&code, "code", code, "请求成功的状态码")
flag.BoolVar(&http2, "http2", http2, "是否开http2.0")
flag.BoolVar(&keepalive, "k", keepalive, "是否开启长连接")
flag.IntVar(&cpuNumber, "cpuNumber", cpuNumber, "CUP 核数,默认为一核")
flag.Int64Var(&timeout, "timeout", timeout, "超时时间 单位 秒,默认不设置")
// 解析参数
flag.Parse()
}
// main go 实现的压测工具
// 编译可执行文件
//
//go:generate go build main.go
func main() {
runtime.GOMAXPROCS(cpuNumber)
//go run .\main.go -c 10 -n 10 -u ws://10.0.5.101:7891/gateway
concurrency = 10
totalNumber = 10
debugStr = "true"
requestURL = "ws://10.0.5.101:7891/gateway"
if concurrency == 0 || totalNumber == 0 || (requestURL == "" && path == "") {
fmt.Printf("示例: go run main.go -c 1 -n 1 -u https://www.baidu.com/ \n")
fmt.Printf("压测地址或curl路径必填 \n")
fmt.Printf("当前请求参数: -c %d -n %d -d %v -u %s \n", concurrency, totalNumber, debugStr, requestURL)
flag.Usage()
return
}
debug := strings.ToLower(debugStr) == "true"
request, err := model.NewRequest(requestURL, verify, code, 0, debug, path, headers, body, maxCon, http2, keepalive)
if err != nil {
fmt.Printf("参数不合法 %v \n", err)
return
}
fmt.Printf("\n 开始启动 并发数:%d 请求数:%d 请求参数: \n", concurrency, totalNumber)
request.Print()
// 开始处理
ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
deadline, ok := ctx.Deadline()
if ok {
fmt.Printf(" deadline %s", deadline)
}
}
server.Dispose(ctx, concurrency, totalNumber, request)
return
}

47
stress/tests/grpc/main.go Normal file
View File

@ -0,0 +1,47 @@
// Package main grpc server
package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "go_dreamfactory/stress/proto"
)
const (
// port 监听端口
port = ":8099"
)
// server is used to implement helloWorld.GreeterServer.
type server struct {
pb.UnimplementedApiServerServer
}
// HelloWorld hello world 接口
func (s *server) HelloWorld(_ context.Context, req *pb.Request) (rsp *pb.Response, err error) {
rsp = &pb.Response{
Code: 200,
Msg: "success",
Data: fmt.Sprintf("hello %s !", req.UserName),
}
return
}
// main 主函数
func main() {
fmt.Println("trpc server 启动中...")
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterApiServerServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

28
stress/tests/servers.go Normal file
View File

@ -0,0 +1,28 @@
// Package main 测试用例package main
package main
import (
"log"
"net/http"
"runtime"
)
const (
httpPort = "8088"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU() - 1)
hello := func(w http.ResponseWriter, req *http.Request) {
data := "Hello, go-stress-testing! \n"
w.Header().Add("Server", "golang")
_, _ = w.Write([]byte(data))
return
}
http.HandleFunc("/", hello)
err := http.ListenAndServe(":"+httpPort, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

7
stress/tools/sort.go Normal file
View File

@ -0,0 +1,7 @@
package tools
type MyUint64List []uint64
func (my64 MyUint64List) Len() int { return len(my64) }
func (my64 MyUint64List) Swap(i, j int) { my64[i], my64[j] = my64[j], my64[i] }
func (my64 MyUint64List) Less(i, j int) bool { return my64[i] < my64[j] }

37
stress/目录.md Normal file
View File

@ -0,0 +1,37 @@
## 目录
- 1、项目说明
- 1.1 go-stress-testing
- 1.2 项目体验
- 2、压测
- 2.1 压测是什么
- 2.2 为什么要压测
- 2.3 压测名词解释
- 2.3.1 压测类型解释
- 2.3.2 压测名词解释
- 2.3.3 机器性能指标解释
- 2.3.4 访问指标解释
- 3.4 如何计算压测指标
- 3、常见的压测工具
- 3.1 ab
- 3.2 locust
- 3.3 Jmeter
- 3.4 云压测
- 3.4.1 云压测介绍
- 3.4.2 阿里云 性能测试 PTS
- 3.4.3 腾讯云 压测大师 LM
- 4、go-stress-testing go语言实现的压测工具
- 4.1 介绍
- 4.2 用法
- 4.3 实现
- 4.4 go-stress-testing 对 Golang web 压测
- 5、压测工具的比较
- 5.1 比较
- 5.2 如何选择压测工具
- 6、单台机器100w连接压测实战
- 6.1 说明
- 6.2 内核优化
- 6.3 客户端配置
- 6.4 准备
- 6.5 压测数据
- 7、总结
- 8、参考文献