hey是一款go开发的压测工具,使用命令行模式,不支持图形界面展示,只支持单用户,不支持分布式,使用简单,学习成本低,只需要使用hey --help,会用几个参数就行。

安装也很简单

如果是mac 直接 brew install hey
如果是linux,从https://github.com/rakyll/hey?tab=readme-ov-file 这里下载可执行文件,不需安装。

使用举例:
hey -n 10000 -c 100 -m GET -H "Content-Type:application/json" "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEYXRhIjoiSHJic2wzeXk5Nzl6SnRBcFlLSUVRV3RqblF0WG9FVklnbEFjeWRBa0l3RkstbC15R0FsMk9HVnNISzJWV191SWpPa2Q5Q2VYSHNjTWFuRE53a2Y2SjZJR01uaUJ3SktkVTJBa1hidEN6VGhlNVc1anVkUDczdlRsbTl1VVFnPT0iLCJWZXJzaW9uIjoxLCJleHAiOjE3MTgyNzk2MTZ9.j-hUDHvokHvDB27llDElEUDIfBk0aHEuwkHalFGKI64" "https://segmentfault.com/api/instance/list?page=1&page_size=10"
使用100个协程并发,总共发送1万个请求,GET方式,设置两个header,请求url最好使用引号括起来,避免不识别参数,url必须放到最后。

主要的参数如下:

-n 总请求数
-c 并行worker数,hey使用go作为开发语言,这个选项指定了发出并行请求的工作协程数量。

**本文精华
-q 限制每个worker的qps数量。
比如 -n 100 -c 2 -q 5
启用2个协程,给服务发送请求,总请求数100个,每个协程限制qps不超过5,总的qps不超过2*5=10。
该参数并不能精确控制qps,他内部实现逻辑是通过一个定时器控制每个请求之后间隔时间。
比如设置qps=100,请求间隙是 1e6 / 100 = 1e4微妙,假如接口耗时100ms,那么每次请求相当于100ms+10000微妙=110ms。实际qps是 1000/110 = 9 **

-H 设置请求header,可以多次指定,hey在内部使用数组来接收。例如:
-H “Content-type:application/json” -H "Authorization:Bearer eyJhbGc"
-m 指定method类型,GET,POST等
-d '{"username":"subyun","password":"c3ViWGl"}' 指定post请求的参数,配合 -H "Content-type:application/json" 一起使用。

  • 返回结果包含
    平均rt,99分位,qps,成功数,失败数量会按不同类型区分,hey是根据http返回码区分是否成功的,如果业务上错误的时候也返回200,hey就认为是成功。不支持语义自定义。

整个项目大量使用了无缓冲channel来实现不同协程之间的同步。

自定义开发

hey的缺点很明显,每个并发的请求参数都完全一样,自己写了一个简单的http请求,每个请求都带自己的独特参数
shell版本。register.sh
register.sh -n 10
指定10个进程并发请求

#!/bin/bash
# 接收命令行参数 -n   
while getopts "n:" opt
do
    case $opt in
        n)
            num=$OPTARG
            ;;
        ?)
            echo "there is unrecognized parameter."
            exit 1
            ;;
    esac
done  
for((i=1;i<=${num};i++));  
do   
curl -X POST -d '{"username":"sifou${i}","password":"e***","phone":"${i}1234","verify_code":"615694"}' -H "Content-Type: application/json" "https://segmentfault.com/api/auth/register" & >> register.txt
done
# curl 命令加&后台并发执行,使用wait等待所有命令执行完,主进程才退出
wait

这种方式每个请求都开辟一个进程,比较费资源

使用下面python,使用线程并发。

./register.py 10
指定10个线程并发请求

      
#!/usr/bin/python
import requests
import time
import threading
import random
import redis
import sys
from redis.sentinel import Sentinel
import time

def generate_virtual_phone_number():
    # 定义手机号码的前缀范围
    prefixes = [13, 14, 15, 16, 17, 18, 19]
    # 随机选择一个前缀
    prefix = random.choice(prefixes)

    # 生成中间的四位数字,避免使用0000和1111等特殊组合
    middle_four = random.randint(1000, 9999)

    # 生成最后的五位数字,同样避免使用特殊组合
    last_six = random.randint(10000, 99999)

    # 组合成完整的手机号码
    phone_number = f"+86{prefix}{middle_four:04}{last_six:05}"

    return phone_number

class myThread (threading.Thread):
    def __init__(self, threadID, name):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name

    def run(self):
        global correct, err
        #print("Starting " + self.name)
        url = "https://segmentfault.com/api/auth/register"
        #url = "http://127.0.0.1:8888/api/auth/register"
        headers = {'content-type': 'application/json'}
        phone = generate_virtual_phone_number()
        verify_code = "123456"
        set_verify_code(phone, verify_code, self.threadID)
        requestData = {"username":"sifou"+ str(self.threadID),"password":"eGlZdW=","phone":phone,"verify_code":verify_code}
        print(requestData)
        ret = requests.post(url, json=requestData, headers=headers, timeout=(5, 10))
        threadLock.acquire()
        print(str(self.threadID) + " " + ret.text)
        if ret.status_code == 200:
            correct = correct+1
        else:
            err = err+1
        threadLock.release()

        #print("Exiting " + self.name)

def set_verify_code(phone, code, threadID):
    conf = {
        'sentinel': [('10.11.11.11', 26379), ('10.22.22.22', 26379)],
        'master_group_name': 'redis-ha',
        'connection_conf': {
            #'socket_timeout': 0.5,
            #'retry_on_timeout': True,
            #'socket_keepalive': True,
            #'max_connections': 10,
            'db': 0,
            'encoding': 'utf8',
            'decode_responses': True,
        }
    }
    sentinel = Sentinel(conf['sentinel'], **conf['connection_conf'])
    sentinel.discover_master(conf['master_group_name'])
    cli = sentinel.master_for(conf['master_group_name'], password='*****', port=6379)
    ret = cli.setex("register:" + phone, 60, code)
    print(str(threadID) + " " + str(ret))
    #print("redisget:" +  cli.get("register:" + phone))

threadLock = threading.Lock()
threads = []
correct = 0
err = 0

start = time.perf_counter()

for i in range(1, int(sys.argv[1])):
    id = random.randint(1, 9999999)
    thread = myThread(id, "Thread-" + str(i))
    thread.start()
    threads.append(thread)

for t in threads:
    t.join()

end = time.perf_counter()

print("correct: " + str(correct))
print('err: ' + str(err))
print('average: ' + str((end - start) / sys.argv[1]))

    

英雄之旅
8 声望1 粉丝

引用和评论

0 条评论