diff --git a/cmd/cmd.go b/cmd/cmd.go index ce5e09422..ce9874c69 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -33,6 +33,7 @@ func init() { RootCmd.AddCommand(runCmd) initLog() robot.InitDb() + } var account = flag.String("account", "", "登录账号") @@ -47,7 +48,7 @@ func CloneNewHero(hero *pb.DBHero) (newHero *pb.DBHero) { return } func main() { - //Execute() + Execute() } var runCmd = &cobra.Command{ diff --git a/cmd/robot/options.go b/cmd/robot/options.go index b42161034..35fbfb84b 100644 --- a/cmd/robot/options.go +++ b/cmd/robot/options.go @@ -12,8 +12,8 @@ type Options struct { func DefaultOpts() *Options { return &Options{ - WsUrl: "ws://10.0.5.73:7891/gateway", - RegUrl: "http://10.0.5.73:8000/register", + WsUrl: "ws://10.0.0.9:7891/gateway", + RegUrl: "http://10.0.0.9:8000/register", Create: false, ServerId: "1", } diff --git a/go.mod b/go.mod index c25e5c670..2461bd9c8 100644 --- a/go.mod +++ b/go.mod @@ -37,8 +37,10 @@ require ( go.mongodb.org/mongo-driver v1.5.1 go.uber.org/multierr v1.6.0 golang.org/x/net v0.2.0 + google.golang.org/grpc v1.46.2 google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v2 v2.4.0 + layeh.com/radius v0.0.0-20210819152912-ad72663a72ab ) require ( @@ -48,6 +50,7 @@ require ( github.com/onsi/gomega v1.20.0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/smartystreets/assertions v1.2.0 // indirect + google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect ) require ( @@ -181,7 +184,7 @@ require ( golang.org/x/mod v0.7.0 // indirect golang.org/x/sync v0.1.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 gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 4dc60df26..77b371f61 100644 --- a/go.sum +++ b/go.sum @@ -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-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-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-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= @@ -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.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.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/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= 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 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/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.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 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-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-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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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-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-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.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 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.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 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.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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 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= @@ -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-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= +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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/stress/README.md b/stress/README.md new file mode 100644 index 000000000..1b4ca058c --- /dev/null +++ b/stress/README.md @@ -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 + +PTS(Performance 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 握手: + + +- **Q:** 没有go环境无法使用最新功能 + + A: 可以使用Dockerfile构建一个容器镜像,假设容器镜像名称为gostress:1111,docker 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) +- 也可以添加我的微信(申请信息填写:公司、姓名,我好备注下),直接反馈给我 +
+

+ 添加link1st的微信 +

+ +### 赞助商 + +- 感谢[JetBrains](https://www.jetbrains.com/?from=gowebsocket)对本项目的支持! +
+

+ + + +

diff --git a/stress/build.sh b/stress/build.sh new file mode 100644 index 000000000..3bc98ca74 --- /dev/null +++ b/stress/build.sh @@ -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 diff --git a/stress/curl/.gitignore b/stress/curl/.gitignore new file mode 100644 index 000000000..f935021a8 --- /dev/null +++ b/stress/curl/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/stress/curl/test.chrome.curl.txt b/stress/curl/test.chrome.curl.txt new file mode 100644 index 000000000..9f539729a --- /dev/null +++ b/stress/curl/test.chrome.curl.txt @@ -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 \ No newline at end of file diff --git a/stress/curl/test.curl.txt b/stress/curl/test.curl.txt new file mode 100644 index 000000000..9f539729a --- /dev/null +++ b/stress/curl/test.curl.txt @@ -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 \ No newline at end of file diff --git a/stress/curl/test.post.curl.txt b/stress/curl/test.post.curl.txt new file mode 100644 index 000000000..63f8c3b57 --- /dev/null +++ b/stress/curl/test.post.curl.txt @@ -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 \ No newline at end of file diff --git a/stress/curl/test.postman.curl.txt b/stress/curl/test.postman.curl.txt new file mode 100644 index 000000000..ad81434be --- /dev/null +++ b/stress/curl/test.postman.curl.txt @@ -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' \ No newline at end of file diff --git a/stress/helper/helper.go b/stress/helper/helper.go new file mode 100644 index 000000000..ea7c30636 --- /dev/null +++ b/stress/helper/helper.go @@ -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 +} diff --git a/stress/model/curl_model.go b/stress/model/curl_model.go new file mode 100644 index 000000000..2e1c9b297 --- /dev/null +++ b/stress/model/curl_model.go @@ -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 +} diff --git a/stress/model/curl_model_test.go b/stress/model/curl_model_test.go new file mode 100644 index 000000000..68c0b2140 --- /dev/null +++ b/stress/model/curl_model_test.go @@ -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()) +} diff --git a/stress/model/request_model.go b/stress/model/request_model.go new file mode 100644 index 000000000..523461590 --- /dev/null +++ b/stress/model/request_model.go @@ -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 +} diff --git a/stress/proto/pb.pb.go b/stress/proto/pb.pb.go new file mode 100644 index 000000000..bc5428532 --- /dev/null +++ b/stress/proto/pb.pb.go @@ -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", +} diff --git a/stress/proto/pb.proto b/stress/proto/pb.proto new file mode 100644 index 000000000..078979277 --- /dev/null +++ b/stress/proto/pb.proto @@ -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; +} \ No newline at end of file diff --git a/stress/server/client/clienter.go b/stress/server/client/clienter.go new file mode 100644 index 000000000..533fd002d --- /dev/null +++ b/stress/server/client/clienter.go @@ -0,0 +1,9 @@ +// Package client clientpackage client +package client + +// Clienter 接口 注册、连接、发送 等 +type Clienter interface { + GetConn() (err error) + Close() (err error) + Send() +} diff --git a/stress/server/client/grpc_client.go b/stress/server/client/grpc_client.go new file mode 100644 index 000000000..0bf7e0daf --- /dev/null +++ b/stress/server/client/grpc_client.go @@ -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 +} diff --git a/stress/server/client/http_client.go b/stress/server/client/http_client.go new file mode 100644 index 000000000..715d1f0a3 --- /dev/null +++ b/stress/server/client/http_client.go @@ -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 +} diff --git a/stress/server/client/http_longclinet/long_clinet.go b/stress/server/client/http_longclinet/long_clinet.go new file mode 100644 index 000000000..ac76d283f --- /dev/null +++ b/stress/server/client/http_longclinet/long_clinet.go @@ -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, + } +} diff --git a/stress/server/client/websocket_client.go b/stress/server/client/websocket_client.go new file mode 100644 index 000000000..7e6454b0d --- /dev/null +++ b/stress/server/client/websocket_client.go @@ -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 +} diff --git a/stress/server/dispose.go b/stress/server/dispose.go new file mode 100644 index 000000000..6f7d50d76 --- /dev/null +++ b/stress/server/dispose.go @@ -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 +} diff --git a/stress/server/golink/grpc_link.go b/stress/server/golink/grpc_link.go new file mode 100644 index 000000000..50db143b7 --- /dev/null +++ b/stress/server/golink/grpc_link.go @@ -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 +} diff --git a/stress/server/golink/http_link.go b/stress/server/golink/http_link.go new file mode 100644 index 000000000..98d923e90 --- /dev/null +++ b/stress/server/golink/http_link.go @@ -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 +} diff --git a/stress/server/golink/http_link_many.go b/stress/server/golink/http_link_many.go new file mode 100644 index 000000000..92ae0015d --- /dev/null +++ b/stress/server/golink/http_link_many.go @@ -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 +} diff --git a/stress/server/golink/http_link_weigh.go b/stress/server/golink/http_link_weigh.go new file mode 100644 index 000000000..840d95472 --- /dev/null +++ b/stress/server/golink/http_link_weigh.go @@ -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") +} diff --git a/stress/server/golink/radius_link.go b/stress/server/golink/radius_link.go new file mode 100644 index 000000000..2d5a4bb34 --- /dev/null +++ b/stress/server/golink/radius_link.go @@ -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 +} diff --git a/stress/server/golink/websocket_link.go b/stress/server/golink/websocket_link.go new file mode 100644 index 000000000..dc7557f3d --- /dev/null +++ b/stress/server/golink/websocket_link.go @@ -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 +} diff --git a/stress/server/statistics/statistics.go b/stress/server/statistics/statistics.go new file mode 100644 index 000000000..c38daccdd --- /dev/null +++ b/stress/server/statistics/statistics.go @@ -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 +} diff --git a/stress/server/statistics/statistics_test.go b/stress/server/statistics/statistics_test.go new file mode 100644 index 000000000..f74f32039 --- /dev/null +++ b/stress/server/statistics/statistics_test.go @@ -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) + }) + } +} diff --git a/stress/server/verify/http_verify.go b/stress/server/verify/http_verify.go new file mode 100644 index 000000000..5cd554542 --- /dev/null +++ b/stress/server/verify/http_verify.go @@ -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 +} diff --git a/stress/server/verify/websokcet_verify.go b/stress/server/verify/websokcet_verify.go new file mode 100644 index 000000000..0427b2751 --- /dev/null +++ b/stress/server/verify/websokcet_verify.go @@ -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 +} diff --git a/stress/stress.go b/stress/stress.go new file mode 100644 index 000000000..dcd64391d --- /dev/null +++ b/stress/stress.go @@ -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 +} diff --git a/stress/tests/grpc/main.go b/stress/tests/grpc/main.go new file mode 100644 index 000000000..323337026 --- /dev/null +++ b/stress/tests/grpc/main.go @@ -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) + } +} diff --git a/stress/tests/servers.go b/stress/tests/servers.go new file mode 100644 index 000000000..15b2f028f --- /dev/null +++ b/stress/tests/servers.go @@ -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) + } +} diff --git a/stress/tools/sort.go b/stress/tools/sort.go new file mode 100644 index 000000000..9e2bd2ff6 --- /dev/null +++ b/stress/tools/sort.go @@ -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] } diff --git a/stress/目录.md b/stress/目录.md new file mode 100644 index 000000000..b3d5bac96 --- /dev/null +++ b/stress/目录.md @@ -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、参考文献 \ No newline at end of file