登录协议测试

This commit is contained in:
meixiongfeng 2022-11-25 19:18:18 +08:00
parent d9a1a7584d
commit c7e6ffc8db
14 changed files with 529 additions and 1043 deletions

View File

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

View File

@ -431,7 +431,7 @@ func (this *ModelHero) StarHpAddition(star int32) (addValue float32) {
starCfg := this.moduleHero.configure.GetHeroStar(int32(i))
cfg := this.moduleHero.configure.GetHeroLv(starCfg.Level)
this.moduleHero.Debugf("cfg.Atk= %f,starCfg.StarupHp = %f,addValue= %f", cfg.Atk, starCfg.StarupHp, addValue)
//this.moduleHero.Debugf("cfg.Atk= %f,starCfg.StarupHp = %f,addValue= %f", cfg.Atk, starCfg.StarupHp, addValue)
addValue += cfg.Hp * starCfg.StarupHp / 1000.0
}
return addValue

View File

@ -57,7 +57,9 @@ func (this *apiComp) Sign(session comm.IUserSession, req *pb.UserSignReq) (code
if _data != nil { // 发奖
if bReward {
this.module.DispenseRes(session, _data.Loopgift, true)
this.module.DispenseRes(session, _data.Extra, true)
if len(_data.Extra) > 0 {
this.module.DispenseRes(session, _data.Extra, true)
}
}
} else {
if bReward {

View File

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

95
stress/robot/linkcase.go Normal file
View File

@ -0,0 +1,95 @@
package robot
import (
"fmt"
"log"
)
type CaseNode struct {
testCase *TestCase //用例名
Next *CaseNode //测试用例
}
type LinkCase struct {
Head *CaseNode
force bool
}
func NewLinkCase() *LinkCase {
return &LinkCase{}
}
func NewDefault(testCast *TestCase) *LinkCase {
return &LinkCase{
force: true,
Head: &CaseNode{testCase: testCast},
}
}
func (this *LinkCase) isEmpty() bool {
return this.Head == nil
}
func (this *LinkCase) lastNode() *CaseNode {
cur := this.Head
for cur.Next != nil {
cur = cur.Next
}
return cur
}
//尾部追加
func (this *LinkCase) Append(testCase *TestCase) {
newNode := &CaseNode{testCase: testCase}
if this.isEmpty() {
this.Head = newNode
} else {
this.lastNode().Next = newNode
}
}
//头部添加
func (this *LinkCase) Unshift(testCase *TestCase) {
if this.force {
log.Fatal("use `NewLinkCase` method create link if you want to use `Unshift` opt")
return
}
newNode := &CaseNode{testCase: testCase}
newNode.Next = this.Head
this.Head = newNode
}
func (this *LinkCase) length() int {
cur := this.Head
count := 0
for cur.Next != nil {
cur = cur.Next
count++
}
return count
}
func (this *LinkCase) Insert(index int, testCase *TestCase) {
if this.force {
log.Fatal("use `NewLinkCase` method create link if you want to use `Insert` opt")
return
}
if index < 0 {
this.Unshift(testCase)
} else if index > this.length() {
this.Append(testCase)
} else {
preNode := this.Head
count := 0
for count < index-1 {
preNode = preNode.Next
count++
}
newNode := &CaseNode{testCase: testCase, Next: preNode.Next}
preNode.Next = newNode
}
}
func (this *CaseNode) String() string {
return fmt.Sprintf("value=%v", this.testCase.desc)
}

29
stress/robot/login.go Normal file
View File

@ -0,0 +1,29 @@
package robot
import (
"fmt"
"go_dreamfactory/utils"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/nacos-group/nacos-sdk-go/util"
)
type LoginParam struct {
Account string `json:"account"`
ServerId string `json:"serverId"`
TimeStamp int64 `json:"timestamp"`
}
func (r *Robot) BuildSecStr() string {
jsonByte, _ := jsoniter.Marshal(&LoginParam{
Account: r.account,
ServerId: r.sid,
TimeStamp: time.Now().Unix(),
})
jsonBase64 := utils.Base64Encode(jsonByte)
// log.Printf("client base64:%s", jsonBase64)
clientMd5key := util.Md5(jsonBase64)
// log.Printf("client md5:%s", clientMd5key)
return fmt.Sprintf("CE:%s%s", clientMd5key, jsonBase64)
}

40
stress/robot/options.go Normal file
View File

@ -0,0 +1,40 @@
package robot
type Options struct {
WsUrl string //客户端访问网关的ws接口地址
RegUrl string //账号注册接口地址
Account string //玩家账号
Create bool
Secretkey string //秘钥串
ServerId string //区服ID
Role bool //是否创角
}
func DefaultOpts() *Options {
return &Options{
WsUrl: "ws://10.0.0.9:7891/gateway",
RegUrl: "http://10.0.0.9:8000/register",
Create: false,
ServerId: "1",
}
}
type Option func(*Options)
func WithWsUrl(addr string) Option {
return func(o *Options) {
o.WsUrl = addr
}
}
func WithAccount(account string) Option {
return func(o *Options) {
o.Account = account
}
}
func WithCreate(create bool) Option {
return func(o *Options) {
o.Create = create
}
}

23
stress/robot/pack.go Normal file
View File

@ -0,0 +1,23 @@
package robot
import (
"go_dreamfactory/comm"
"go_dreamfactory/pb"
)
//申明测试接口及请求和响应参数
var pack_builders = []*TestCase{
{
//create
mainType: string(comm.ModulePack),
subType: "queryuserpackreq",
// req: &pb.Pack_Getlist_Req{IType: 1},
rsp: &pb.UserCreateResp{},
// enabled: true,
},
}
//声明加入到构建器并发起请求
func (r *Robot) RunPack() {
//r.addBuilders(pack_builders)
}

234
stress/robot/robot.go Normal file
View File

@ -0,0 +1,234 @@
package robot
import (
"bytes"
"encoding/json"
"fmt"
"go_dreamfactory/comm"
"io"
"os"
// zlog "go_dreamfactory/lego/sys/log"
"go_dreamfactory/pb"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
)
type Robot struct {
ws *websocket.Conn
account string
sid string
user *pb.DBUser
}
var zlog *logrus.Logger
func initlog() {
zlog = logrus.New()
zlog.SetLevel(logrus.DebugLevel)
zlog.SetFormatter(&RobotFormatter{
DisableTimestamp: true,
DisableQuote: true,
})
//设置output,默认为stderr,可以为任何io.Writer比如文件*os.File
file, err := os.OpenFile("robot.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
writers := []io.Writer{
file,
os.Stdout}
//同时写文件和屏幕
fileAndStdoutWriter := io.MultiWriter(writers...)
if err == nil {
zlog.SetOutput(fileAndStdoutWriter)
} else {
zlog.Info("failed to log to file.")
}
}
func NewRobot(url string) *Robot {
initlog()
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
zlog.Fatalf("websocket conn err:%v", err)
}
r := &Robot{
ws: ws,
account: "user001",
sid: "dfmxf",
}
return r
}
type TestCase struct {
id string //用例ID 如果没有指定,会自动赋值uuid
desc string //用例描述
mainType string //协议类型 L1
subType string //协议类型 L2
req proto.Message //请求类型
rsp proto.Message //响应类型
}
//加入用例并执行请求
func (r *Robot) addTestCaseAndReq(tcs []*TestCase) {
}
//错误通知处理
func (r *Robot) handleNotify(uuid string, msg *pb.UserMessage) {
if msg.MainType == "notify" && msg.SubType == "errornotify" {
rsp := &pb.NotifyErrorNotifyPush{}
if !comm.ProtoUnmarshal(msg, rsp) {
return
}
}
}
//处理响应
func (r *Robot) handleRsp(id string) {
var msg *pb.UserMessage = &pb.UserMessage{}
_, data, err := r.ws.ReadMessage()
if err != nil {
zlog.Fatalf("readMessage err:%v", err)
}
if err = proto.Unmarshal(data, msg); err != nil {
zlog.Fatalf("unmarshal err:%v", err)
}
}
//登录回调
func (r *Robot) loginCallback(rsp proto.Message) {
//清除登录用例
lr := rsp.(*pb.UserLoginResp)
if lr.Data != nil {
r.user = lr.Data
r.onUserLoaded()
}
}
//发送消息
func (r *Robot) SendToClient(msg *pb.UserMessage, rsp proto.Message) error {
//模拟客户端 每次请求都会生成新的秘钥
msg.Sec = r.BuildSecStr()
if comm.ProtoMarshal(rsp, msg) {
data, _ := proto.Marshal(msg)
return r.ws.WriteMessage(websocket.BinaryMessage, data)
}
return nil
}
//格式化json
func formatJson(data string) (string, error) {
var out bytes.Buffer
err := json.Indent(&out, []byte(data), "", " ")
return out.String(), err
}
//方法参数跟踪
func traceFunc(module string, funcName string, uid string, funcArgs interface{}) {
zlog.Debugf("req [%s.%s] [%s] [%v]", module, funcName, uid, funcArgs)
}
//在这里添加玩家成功登录以后的测试方法
//次方法在用户登录成功后调用
func (r *Robot) onUserLoaded() {
//user
r.RunUser()
}
type RobotFormatter struct {
DisableTimestamp bool
DisableQuote bool
}
func (r *RobotFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
var timestamp string
var newLog string
if !r.DisableTimestamp {
timestamp = entry.Time.Format("2006-01-02 15:04:05")
newLog = fmt.Sprintf("[%s] %s\n", timestamp, entry.Message)
} else {
newLog = fmt.Sprintf("%s\n", entry.Message)
}
b.WriteString(newLog)
return b.Bytes(), nil
}
func (r *Robot) ExecSendMsg(mainType, subType string, msg proto.Message) {
if msg != nil {
head := &pb.UserMessage{MainType: mainType, SubType: subType}
err := r.SendToClient(head, msg)
if err != nil {
zlog.Errorf("send to client err:%v", err)
}
r.messageRsp(mainType, subType)
}
}
func (r *Robot) MessageRsp() {
var msg *pb.UserMessage = &pb.UserMessage{}
_, data, err := r.ws.ReadMessage()
if err != nil {
zlog.Fatalf("readMessage err:%v", err)
}
if err = proto.Unmarshal(data, msg); err != nil {
zlog.Fatalf("unmarshal err:%v", err)
}
if msg.MainType == "chat" && msg.SubType == "message" {
resp := &pb.ChatMessagePush{}
if !comm.ProtoUnmarshal(msg, resp) {
return
}
zlog.Fatalf("接收消息=====resp:%v", resp.Chat)
}
}
func (r *Robot) messageRsp(mainType, subType string) {
var msg *pb.UserMessage = &pb.UserMessage{}
_, data, err := r.ws.ReadMessage()
if err != nil {
zlog.Fatalf("readMessage err:%v", err)
}
if err = proto.Unmarshal(data, msg); err != nil {
zlog.Fatalf("unmarshal err:%v", err)
}
if msg.MainType == mainType &&
msg.SubType == subType {
rsp := &pb.UserLoginResp{}
if !comm.ProtoUnmarshal(msg, rsp) {
return
}
if msg.MainType == "user" && msg.SubType == "login" {
r.loginCallback(rsp)
}
}
}
func (r *Robot) CloseHandler() {
r.ws.Close()
}

60
stress/robot/user.go Normal file
View File

@ -0,0 +1,60 @@
package robot
import (
"go_dreamfactory/comm"
"go_dreamfactory/modules/user"
"go_dreamfactory/pb"
)
//申明测试接口及请求和响应参数
var user_builders = []*TestCase{
{
desc: "修改名称",
mainType: string(comm.ModuleUser),
subType: user.UserSubTypeModifyName,
req: &pb.UserModifynameReq{ //设置请求参数
Name: "uuuuuu",
},
rsp: &pb.UserModifynameResp{},
// enabled: true,
},
{
desc: "查看图鉴",
mainType: string(comm.ModuleUser),
subType: "gettujian",
req: &pb.UserGetTujianReq{},
rsp: &pb.UserGetTujianResp{},
// enabled: true,
}, {
desc: "获取配置",
mainType: string(comm.ModuleUser),
subType: user.UserSubTypeGetSetting,
req: &pb.UserGetSettingReq{},
rsp: &pb.UserGetSettingResp{},
// enabled: true,
}, {
desc: "更新配置",
mainType: string(comm.ModuleUser),
subType: user.UserSubTypeUpdatesetting,
req: &pb.UserUpdateSettingReq{
Setting: &pb.DBUserSetting{
Huazhi: 2,
Kangjuchi: 1,
Gaoguang: true,
},
},
rsp: &pb.UserUpdateSettingResp{},
// enabled: true,
}, {
desc: "验证码",
mainType: string(comm.ModuleUser),
subType: user.UserSubTypeVeriCode,
req: &pb.UserVeriCodeReq{},
rsp: &pb.UserVeriCodeResp{},
},
}
//声明加入到构建器并发起请求
func (r *Robot) RunUser() {
//r.addBuilders(user_builders)
}

View File

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/robot"
"sync"
"time"
@ -50,24 +51,27 @@ func Dispose(ctx context.Context, concurrency, totalNumber uint64, request *mode
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)
r := robot.NewRobot(request.URL)
// head := &pb.UserMessage{MainType: "user", SubType: "login"}
// req := &pb.UserLoginReq{
// Account: "user001",
// Sid: "dfmxf",
// }
// r.SendToClient(head, req)
// r.MessageRsp()
go golink.WebSocket(ctx, i, ch, totalNumber, &wg, request, r)
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)
// 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)

View File

@ -3,13 +3,13 @@ package golink
import (
"context"
"fmt"
"sync"
"time"
"go_dreamfactory/pb"
"go_dreamfactory/stress/helper"
"go_dreamfactory/stress/model"
"go_dreamfactory/stress/server/client"
"go_dreamfactory/stress/robot"
)
const (
@ -28,12 +28,12 @@ func init() {
// 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) {
wg *sync.WaitGroup, request *model.Request, r *robot.Robot) {
defer func() {
wg.Done()
}()
defer func() {
_ = ws.Close()
r.CloseHandler()
}()
var (
@ -46,7 +46,7 @@ func WebSocket(ctx context.Context, chanID uint64, ch chan<- *model.RequestResul
case <-t.C:
t.Reset(intervalTime)
// 请求
webSocketRequest(chanID, ch, i, request, ws)
webSocketRequest(chanID, ch, i, request, r)
// 结束条件
i = i + 1
if i >= totalNumber {
@ -67,27 +67,32 @@ end:
// webSocketRequest 请求
func webSocketRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request,
ws *client.WebSocket) {
r *robot.Robot) {
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)
}
head := &pb.UserMessage{MainType: "user", SubType: "login"}
req := &pb.UserLoginReq{
Account: "user001",
Sid: "dfmxf",
}
r.SendToClient(head, req)
r.MessageRsp()
// 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,

View File

@ -73,7 +73,7 @@ func main() {
concurrency = 10
totalNumber = 10
debugStr = "true"
requestURL = "ws://10.0.5.101:7891/gateway"
requestURL = "ws://10.0.5.194: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")

View File

@ -1,37 +0,0 @@
## 目录
- 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、参考文献