├── __init__.py ├── hztest ├── __init__.py ├── rpc │ ├── __init__.py │ ├── protocol.py │ ├── script │ │ └── internal-test.py │ ├── events.py │ ├── master.py │ └── slave.py ├── uitest.py ├── wsgi.py ├── urls.py ├── settings.py ├── templates │ └── hello.html └── views.py ├── db.sqlite3 ├── docker ├── requirements.txt ├── dockerfile_locust ├── dockerfile_slave └── dockerfile_master ├── manage.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hztest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biyunfei/locustM/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /docker/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio 2 | Django==1.11.10 3 | psutil==5.4.3 -------------------------------------------------------------------------------- /hztest/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | from . import master 2 | from .protocol import Message 3 | -------------------------------------------------------------------------------- /hztest/uitest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import Tkinter 3 | 4 | top = Tkinter.Tk() 5 | hello = Tkinter.Label(top, text='Hello world!') 6 | hello.pack() 7 | quit = Tkinter.Button(top, text='QUIT', command=top.quit, bg='red', fg='white') 8 | quit.pack(fill=Tkinter.X, expand=1) 9 | Tkinter.mainloop() 10 | -------------------------------------------------------------------------------- /docker/dockerfile_locust: -------------------------------------------------------------------------------- 1 | # 使用Python官方镜像作为镜像的基础 2 | FROM python:2.7-slim 3 | MAINTAINER biyunfei 4 | # 添加vim和gcc依赖 5 | RUN apt-get update && apt-get install -y vim gcc \ 6 | # 用完包管理器后安排打扫卫生可以显著的减少镜像大小 7 | && apt-get clean \ 8 | && apt-get autoclean \ 9 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 10 | # 设置工作空间为/app 11 | WORKDIR /app 12 | # 安装requirements.txt中指定的依赖 13 | ADD requirements.txt /app 14 | RUN pip install -r requirements.txt -------------------------------------------------------------------------------- /docker/dockerfile_slave: -------------------------------------------------------------------------------- 1 | # 使用Python官方镜像作为镜像的基础 2 | FROM locust 3 | MAINTAINER biyunfei 4 | # 把当前目录下的文件拷贝到 容器里的/app里 5 | ADD ../hztest /app/hztest 6 | # 设置工作空间为/app 7 | WORKDIR /app/hztest/hztest/rpc 8 | # 设置时区 9 | RUN echo "Asia/Shanghai" > /etc/timezone 10 | RUN dpkg-reconfigure -f noninteractive tzdata 11 | # 开放8000端口 12 | EXPOSE 6666 6667 5557 5558 8089 8000 13 | # 设置 HOST 这个环境变量 14 | ENV HOST 0 15 | # 当容器启动时,运行app.py 16 | ENTRYPOINT python slave.py ${HOST} -------------------------------------------------------------------------------- /docker/dockerfile_master: -------------------------------------------------------------------------------- 1 | # 使用Python官方镜像作为镜像的基础 2 | FROM locust 3 | MAINTAINER biyunfei 4 | # 把当前目录下的文件拷贝到 容器里的/app里 5 | ADD ../hztest /app/hztest 6 | # 设置工作空间为/app 7 | WORKDIR /app/hztest 8 | # 设置时区 9 | RUN echo "Asia/Shanghai" > /etc/timezone 10 | RUN dpkg-reconfigure -f noninteractive tzdata 11 | # 开放8000端口 12 | EXPOSE 6666 6667 5557 5558 8089 8000 13 | # 设置 HOST 这个环境变量 14 | ENV HOST 0 15 | # 当容器启动时,运行app.py 16 | ENTRYPOINT python manage.py runserver ${HOST}:8000 -------------------------------------------------------------------------------- /hztest/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for hztest project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hztest.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /hztest/rpc/protocol.py: -------------------------------------------------------------------------------- 1 | import msgpack 2 | 3 | 4 | class Message(object): 5 | def __init__(self, message_type, data, node_id): 6 | self.type = message_type 7 | self.data = data 8 | self.node_id = node_id 9 | 10 | def serialize(self): 11 | return msgpack.dumps((self.type, self.data, self.node_id)) 12 | 13 | @classmethod 14 | def unserialize(cls, data): 15 | msg = cls(*msgpack.loads(data, encoding='utf-8')) 16 | return msg 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hztest.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /hztest/urls.py: -------------------------------------------------------------------------------- 1 | """hztest URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from . import views 19 | 20 | urlpatterns = [ 21 | url(r'^$', views.hello), 22 | # url(r'^admin/', admin.site.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # locustM 2 | It is a web tool for schedule locust test of distributed locust clients; 3 | 4 | To run it on you computer, you should be install locustio, Django==1.11.10, psutil==5.4.3 by PIP as first, now it support in python 2.x; or you can use the docker images which in the next; 5 | 6 | There are two parts of this tools: Master and Slave; 7 | 8 | Master: 9 | In CMD line, at . folder run: 10 | 11 | python manage.py runserver 0:8000 12 | 13 | Open http://localhost:8000 to get in the web tool for control you locust clients; http://localhost:8089 to locust web spawn mode; 14 | 15 | Detail information can see in the source code; 16 | 17 | Slave: 18 | For each locust client, in CMD line: at ./hztest/rpc folder run: 19 | 20 | python slave.py MASTER_IP 21 | 22 | # Make docker images: 23 | docker build -t locust -f dockerfile_locust . 24 | docker build -t locust:master -f dockerfile_master . 25 | docker build -t locust:slave -f dockerfile_slave . 26 | 27 | # Run docker images: 28 | **Master:** 29 | docker run -p 8000:8000 -p 6666:6666 -p 6667:6667 -p 5557:5557 -p 5558:5558 -p 8089:8089 -it --rm locust:master 30 | 31 | **Slave:** 32 | docker run -e HOST={MASTER_IP} -it --rm locust:slave 33 | -------------------------------------------------------------------------------- /hztest/rpc/script/internal-test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from locust import HttpLocust, TaskSet, task, events 4 | import logging 5 | import datetime 6 | import os 7 | from gevent._semaphore import Semaphore 8 | all_locusts_spawned = Semaphore() 9 | all_locusts_spawned.acquire() 10 | 11 | 12 | def on_hatch_complete(**kwargs): 13 | all_locusts_spawned.release() 14 | 15 | events.hatch_complete += on_hatch_complete 16 | 17 | 18 | class YJKTask(TaskSet): 19 | _headers = {"Content-Type": "application/json; charset=UTF-8"} 20 | 21 | # 并发用户初始化 22 | def on_start(self): 23 | """ on_start is called when a Locust start before any task is scheduled """ 24 | # self.login() 25 | all_locusts_spawned.wait() 26 | 27 | @task(1) 28 | def get(self): 29 | with self.client.get('/', catch_response=True) as resp: 30 | if resp.status_code != 200: 31 | resp.failure('Request failure!HTTP Response Status Code: %s, HTTP Response content: %s!' % (resp.status_code, resp.content)) 32 | self.locust.logger.error('index.json Request failure!HTTP Response Status Code: %s, HTTP Response content: %s!' % (resp.status_code, resp.content)) 33 | 34 | 35 | class YJKUser(HttpLocust): 36 | # 设置Locust压力测试主机地址,用户任务类 37 | host = "https://www.baidu.com" 38 | task_set = YJKTask 39 | 40 | # 设置日志文件及格式 41 | if not os.path.exists('./log'): 42 | os.mkdir('./log') 43 | logging.basicConfig(level=logging.INFO) 44 | fh = logging.FileHandler('./log/log_%s.txt' % datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), mode='w') 45 | formatter = logging.Formatter('[%(asctime)s] - %(name)s - %(levelname)s - %(message)s') 46 | fh.setFormatter(formatter) 47 | logger = logging.getLogger() #__name__) 48 | logger.addHandler(fh) 49 | 50 | 51 | # 用户测试场景的等待时间ms,随机时间在min和max之间 52 | min_wait = 0 53 | max_wait = 0 54 | # 压测时间s,到时自动停止 55 | # stop_timeout = 600 56 | -------------------------------------------------------------------------------- /hztest/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | Django settings for hztest project. 4 | 5 | Generated by 'django-admin startproject' using Django 1.11.10. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/1.11/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/1.11/ref/settings/ 12 | """ 13 | 14 | import os 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'koi9newyzk8*igr*0j+)$dd&nkc&yn)jkx8_!-br^qwzb3g3t8' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | # 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'hztest.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [BASE_DIR + "/hztest/templates", ], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'hztest.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'Asia/Shanghai' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /hztest/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Locust压测调度 6 | 7 | 8 |

{{hello}}

9 |
操作说明:
10 | 1.每个压测客户端单独设置运行步骤:
11 | a)发送压测脚本(可多选,单个可以编辑后再发送至客户端)
12 | b)选择压测脚本(多选则一个进程运行一个脚本,单选则所有进程用同一脚本)
13 | c)设置压测进程数(0代表CPU逻辑核心总数),点击ready启动客户机压测进程;
14 | 2.启动Locust Master服务;
15 | 3.打开locust控制WEB:http://{{host}}:8089
16 | 4.测试完成点击各客户端running按钮停止,更换压测脚本,重新启动客户端压测进程; 17 |
18 |
19 |   监听客户端资源
20 | 当前有{{clients.num}}个客户端!
21 | 22 | {% for client in clients.clients %} 23 | 24 | 27 | 40 | 52 | 58 | 59 | {% for psinfo in psinfos %} 60 | {% if psinfo.id == client.id %} 61 | 71 | {% endif %} 72 | {% endfor %} 73 | 74 | 75 | {% endfor %} 76 |
25 | Client ID:
{{client.id}} 26 |
28 | 发送压测脚本:
29 |
30 | {% if text == client.id %} 31 | 32 | 33 |
34 | {% endif %} 35 |
36 | 37 | 38 |
39 |
选择压测脚本:
41 |
42 | {% for f in filelist %} 43 | {% if f.id == client.id %} 44 | 49 | {% endif %} 50 | {% endfor %} 51 |
53 | 设置压测进程数:
54 | 55 |
56 | 当前压测进程数:{{client.slave_num}} 57 |
62 | 客户端IP[{{psinfo.psinfo.IP}}]
63 | CPU[核数:{{psinfo.psinfo.cpu.num}}, 线程数:{{psinfo.psinfo.cpu.logical_num}}, 使用率:{{psinfo.psinfo.cpu.percent}}%; 64 | {% if psinfo.psinfo.cpu.freq.current != 0 %} 65 | 频率(当前:{{psinfo.psinfo.cpu.freq.current}}MHz, 最小:{{psinfo.psinfo.cpu.freq.min}}MHz, 最大:{{psinfo.psinfo.cpu.freq.max}}MHz) 66 | {% endif %} 67 | ]
68 | 内存[总数:{{psinfo.psinfo.mem.total}}MB, 可用:{{psinfo.psinfo.mem.available}}MB, 使用率:{{psinfo.psinfo.mem.percent}}%]
69 | 网络[发送:{{psinfo.psinfo.net.sent}}KB, {{psinfo.psinfo.net.per_sec_sent}}KB/s; 接收:{{psinfo.psinfo.net.recv}}KB, {{psinfo.psinfo.net.per_sec_recv}}KB/s] 70 |
77 | {% if locust == 'stop' %} 78 | 79 | {% endif %} 80 | 81 | {% if locust == 'run' %} 82 | 83 | {% endif %} 84 |
85 | 86 | -------------------------------------------------------------------------------- /hztest/rpc/events.py: -------------------------------------------------------------------------------- 1 | class EventHook(object): 2 | """ 3 | Simple event class used to provide hooks for different types of events in Locust. 4 | 5 | Here's how to use the EventHook class:: 6 | 7 | my_event = EventHook() 8 | def on_my_event(a, b, **kw): 9 | print "Event was fired with arguments: %s, %s" % (a, b) 10 | my_event += on_my_event 11 | my_event.fire(a="foo", b="bar") 12 | """ 13 | 14 | def __init__(self): 15 | self._handlers = [] 16 | 17 | def __iadd__(self, handler): 18 | self._handlers.append(handler) 19 | return self 20 | 21 | def __isub__(self, handler): 22 | self._handlers.remove(handler) 23 | return self 24 | 25 | def fire(self, **kwargs): 26 | for handler in self._handlers: 27 | handler(**kwargs) 28 | 29 | request_success = EventHook() 30 | """ 31 | *request_success* is fired when a request is completed successfully. 32 | 33 | Listeners should take the following arguments: 34 | 35 | * *request_type*: Request type method used 36 | * *name*: Path to the URL that was called (or override name if it was used in the call to the client) 37 | * *response_time*: Response time in milliseconds 38 | * *response_length*: Content-length of the response 39 | """ 40 | 41 | request_failure = EventHook() 42 | """ 43 | *request_failure* is fired when a request fails 44 | 45 | Event is fired with the following arguments: 46 | 47 | * *request_type*: Request type method used 48 | * *name*: Path to the URL that was called (or override name if it was used in the call to the client) 49 | * *response_time*: Time in milliseconds until exception was thrown 50 | * *exception*: Exception instance that was thrown 51 | """ 52 | 53 | locust_error = EventHook() 54 | """ 55 | *locust_error* is fired when an exception occurs inside the execution of a Locust class. 56 | 57 | Event is fired with the following arguments: 58 | 59 | * *locust_instance*: Locust class instance where the exception occurred 60 | * *exception*: Exception that was thrown 61 | * *tb*: Traceback object (from sys.exc_info()[2]) 62 | """ 63 | 64 | report_to_master = EventHook() 65 | """ 66 | *report_to_master* is used when Locust is running in --slave mode. It can be used to attach 67 | data to the dicts that are regularly sent to the master. It's fired regularly when a report 68 | is to be sent to the master server. 69 | 70 | Note that the keys "stats" and "errors" are used by Locust and shouldn't be overridden. 71 | 72 | Event is fired with the following arguments: 73 | 74 | * *client_id*: The client id of the running locust process. 75 | * *data*: Data dict that can be modified in order to attach data that should be sent to the master. 76 | """ 77 | 78 | slave_report = EventHook() 79 | """ 80 | *slave_report* is used when Locust is running in --master mode and is fired when the master 81 | server receives a report from a Locust slave server. 82 | 83 | This event can be used to aggregate data from the locust slave servers. 84 | 85 | Event is fired with following arguments: 86 | 87 | * *client_id*: Client id of the reporting locust slave 88 | * *data*: Data dict with the data from the slave node 89 | """ 90 | 91 | hatch_complete = EventHook() 92 | """ 93 | *hatch_complete* is fired when all locust users has been spawned. 94 | 95 | Event is fire with the following arguments: 96 | 97 | * *user_count*: Number of users that was hatched 98 | """ 99 | 100 | quitting = EventHook() 101 | """ 102 | *quitting* is fired when the locust process in exiting 103 | """ 104 | 105 | master_start_hatching = EventHook() 106 | """ 107 | *master_start_hatching* is fired when we initiate the hatching process on the master. 108 | 109 | This event is especially usefull to detect when the 'start' button is clicked on the web ui. 110 | """ 111 | 112 | master_stop_hatching = EventHook() 113 | """ 114 | *master_stop_hatching* is fired when terminate the hatching process on the master. 115 | 116 | This event is especially usefull to detect when the 'stop' button is clicked on the web ui. 117 | """ 118 | 119 | locust_start_hatching = EventHook() 120 | """ 121 | *locust_start_hatching* is fired when we initiate the hatching process on any locust worker. 122 | """ 123 | 124 | locust_stop_hatching = EventHook() 125 | """ 126 | *locust_stop_hatching* is fired when terminate the hatching process on any locust worker. 127 | """ 128 | -------------------------------------------------------------------------------- /hztest/rpc/master.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | r""" 3 | master负责与slave端进行通信,发送来自web的请求给slave,接收slave的结果; 4 | 主要有1个全局master监听服务,2个全局队列用于消息和结果传递,两个gevent协程,分别为master_cmd:接收cmd_queue进行派发给slave,master_listener:接收slave的消息反馈并返回给web端; 5 | """ 6 | import zmq.green as zmq 7 | import multiprocessing 8 | from protocol import Message 9 | import six 10 | import gevent 11 | from gevent.pool import Group 12 | import socket 13 | import events 14 | import sys, time 15 | import logging 16 | import signal 17 | from multiprocessing import Queue, Lock 18 | 19 | # 定义全局变量:指令队列,结果队列,进程锁,日志 20 | cmd_queue = Queue() 21 | result_queue = Queue() 22 | lock = Lock() 23 | STATE_INIT, STATE_RUNNING, STATE_STOPPED = ["ready", "running", "stopped"] 24 | sh = logging.StreamHandler() 25 | sh.setFormatter(logging.Formatter('[%(asctime)s] - %(message)s')) 26 | logger = logging.getLogger() 27 | logger.addHandler(sh) 28 | logger.setLevel(logging.INFO) 29 | 30 | 31 | class BaseSocket(object): 32 | 33 | def send(self, msg): 34 | self.sender.send(msg.serialize()) 35 | 36 | def recv(self): 37 | data = self.receiver.recv() 38 | return Message.unserialize(data) 39 | 40 | def noop(self, *args, **kwargs): 41 | """ Used to link() greenlets to in order to be compatible with gevent 1.0 """ 42 | pass 43 | 44 | 45 | class Server(BaseSocket): 46 | def __init__(self, host, port): 47 | context = zmq.Context() 48 | self.receiver = context.socket(zmq.PULL) 49 | try: 50 | self.receiver.bind("tcp://%s:%i" % (host, port)) 51 | except: 52 | sys.exit(0) 53 | self.sender = context.socket(zmq.PUSH) 54 | try: 55 | self.sender.bind("tcp://%s:%i" % (host, port+1)) 56 | except: 57 | sys.exit(0) 58 | 59 | 60 | class Master(Server): 61 | def __init__(self, *args, **kwargs): 62 | super(Master, self).__init__(*args) 63 | 64 | class SlaveNodesDict(dict): 65 | def get_by_state(self, state): 66 | return [c for c in six.itervalues(self) if c.state == state] 67 | 68 | @property 69 | def ready(self): 70 | return self.get_by_state(STATE_INIT) 71 | 72 | @property 73 | def running(self): 74 | return self.get_by_state(STATE_RUNNING) 75 | 76 | self.cmd_queue = kwargs['cmd_queue'] 77 | self.result_queue = kwargs['result_queue'] 78 | self.clients = SlaveNodesDict() 79 | self.greenlet = Group() 80 | # 加载gevent协程,一个用于接收web端发来的指令消息,另一个用于接收slave反馈的消息 81 | self.greenlet.spawn(self.master_listener).link_exception(callback=self.noop) 82 | self.greenlet.spawn(self.master_cmd).link_exception(callback=self.noop) 83 | 84 | def on_quitting(): 85 | self.quit() 86 | events.quitting += on_quitting 87 | 88 | # 接收web端的cmd_queue队列指令消息并派发给相应的slave 89 | def master_cmd(self): 90 | while True: 91 | if not self.cmd_queue.empty(): 92 | while not self.cmd_queue.empty(): 93 | cmd = self.cmd_queue.get() 94 | logger.info('Master: Get new command - [%s]' % cmd['type']) 95 | if cmd['type'] == 'get_clients': 96 | del_clients = [] 97 | client_list = [] 98 | for client in six.itervalues(self.clients): 99 | if client.last_time + 61 > time.time(): 100 | client_list.append({"id": client.id, "state": client.state, "slave_num": client.slave_num}) 101 | else: 102 | del_clients.append(client.id) 103 | for i in del_clients: 104 | print(i) 105 | del self.clients[i] 106 | if client_list: 107 | self.result_queue.put(client_list) 108 | else: 109 | self.result_queue.put('0') 110 | elif cmd['type'] == 'sent_script': 111 | script_data = {"filename": cmd['filename'], "script": cmd['script']} 112 | for client in six.itervalues(self.clients): 113 | self.send(Message("send_script", script_data, cmd['client_id'])) 114 | self.result_queue.put("OK") 115 | elif cmd['type'] == 'run': 116 | data = {"filename": cmd['filename'], "num": cmd['num']} 117 | for client in six.itervalues(self.clients): 118 | self.send(Message("run", data, cmd['client_id'])) 119 | # if client.id == cmd['client_id']: 120 | # client.state = STATE_RUNNING 121 | elif cmd['type'] == 'stop': 122 | for client in six.itervalues(self.clients): 123 | self.send(Message("stop", None, cmd['client_id'])) 124 | # if client.id == cmd['client_id']: 125 | # client.state = STATE_INIT 126 | elif cmd['type'] == 'get_filelist': 127 | for client in six.itervalues(self.clients): 128 | self.send(Message("get_filelist", None, cmd['client_id'])) 129 | elif cmd['type'] == 'get_psinfo': 130 | for client in six.itervalues(self.clients): 131 | self.send(Message("get_psinfo", None, cmd['client_id'])) 132 | elif cmd['type'] == 'clear_folder': 133 | for client in six.itervalues(self.clients): 134 | self.send(Message("clear_folder", None, cmd['client_id'])) 135 | gevent.sleep(1) 136 | 137 | # 接收slave上报的消息并反馈到result_queue 138 | def master_listener(self): 139 | while True: 140 | msg = self.recv() 141 | logger.info('Master: Get new msg from slave - [%s]' % msg.type) 142 | if msg.type == "slave_ready": 143 | id = msg.node_id 144 | self.clients[id] = SlaveNode(id) 145 | self.clients[id].slave_num = msg.data 146 | self.clients[id].last_time = time.time() 147 | if msg.data == 0: 148 | self.clients[id].state = STATE_INIT 149 | else: 150 | self.clients[id].state = STATE_RUNNING 151 | logger.info( 152 | "Client %r reported as ready. Currently %i clients is running; %i clients ready to swarm." % (id, len(self.clients.running), len(self.clients.ready))) 153 | elif msg.type == "quit": 154 | if msg.node_id in self.clients: 155 | del self.clients[msg.node_id] 156 | logger.info("Client %r is exit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready))) 157 | elif msg.type == "file_list": 158 | self.result_queue.put({"client_id": msg.node_id, "file_list": msg.data}) 159 | elif msg.type == "slave_num": 160 | self.clients[msg.node_id].slave_num = msg.data 161 | if msg.data == 0: 162 | self.clients[msg.node_id].state = STATE_INIT 163 | self.result_queue.put("None") 164 | else: 165 | self.clients[msg.node_id].state = STATE_RUNNING 166 | self.result_queue.put("OK") 167 | elif msg.type == "psinfo": 168 | self.result_queue.put({"client_id": msg.node_id, "psinfo": msg.data}) 169 | elif msg.type == "clear_folder": 170 | self.result_queue.put({"client_id": msg.node_id, "clear_folder": "OK"}) 171 | 172 | def quit(self): 173 | for client in six.itervalues(self.clients): 174 | logger.info("Master: send quit message to client - %s" % client.id) 175 | self.send(Message("quit", None, client.id)) 176 | self.greenlet.kill(block=True) 177 | 178 | 179 | class SlaveNode(object): 180 | def __init__(self, id, state=STATE_INIT, slave_num=0): 181 | self.id = id 182 | self.state = state 183 | self.slave_num = slave_num 184 | self.last_time = time.time() 185 | 186 | 187 | def shutdown(code=0): 188 | logger.info("Shutting down (exit code %s), bye." % code) 189 | events.quitting.fire() 190 | sys.exit(code) 191 | 192 | 193 | # install SIGTERM handler 194 | def sig_term_handler(): 195 | logger.info("Got SIGTERM signal") 196 | shutdown(0) 197 | 198 | 199 | def start(cmd_queue, result_queue): 200 | master_host = "0" # socket.gethostbyname(socket.gethostname()) 201 | port = 6666 202 | server = Master(master_host, port, cmd_queue=cmd_queue, result_queue=result_queue) 203 | logger.info('Master is listening at %s:%i' % (master_host, port)) 204 | gevent.signal(signal.SIGTERM, sig_term_handler) 205 | 206 | try: 207 | server.greenlet.join() 208 | # gevent.joinall(server.greenlet.greenlets) 209 | code = 0 210 | shutdown(code=code) 211 | except KeyboardInterrupt as e: 212 | shutdown(1) 213 | 214 | 215 | def start_master(): 216 | p_master = multiprocessing.Process(target=start, args=(cmd_queue, result_queue, )) 217 | p_master.daemon = True 218 | p_master.start() 219 | return p_master 220 | 221 | 222 | master_server = start_master() 223 | 224 | 225 | if __name__ == '__main__': 226 | print "Please use python slave.py to load client!" 227 | -------------------------------------------------------------------------------- /hztest/rpc/slave.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | r""" 3 | slave接收来自master的消息指令,在gevent协程worker中进行处理,包括:状态,资源,脚本文件,locust客户端启停,完成后反馈结果给master; 4 | """ 5 | import zmq.green as zmq 6 | from protocol import Message 7 | import os 8 | import codecs 9 | from time import time, sleep 10 | import gevent 11 | from gevent.pool import Group 12 | import random 13 | import socket 14 | from hashlib import md5 15 | import events 16 | import logging 17 | import signal 18 | import sys 19 | from subprocess import Popen 20 | import psutil as ps 21 | import shutil 22 | import platform 23 | 24 | 25 | STATE_INIT, STATE_RUNNING, STATE_STOPPED = ["ready", "running", "stopped"] 26 | sh = logging.StreamHandler() 27 | sh.setFormatter(logging.Formatter('[%(asctime)s] - %(message)s')) 28 | logger = logging.getLogger() 29 | logger.addHandler(sh) 30 | logger.setLevel(logging.INFO) 31 | 32 | 33 | class BaseSocket(object): 34 | def send(self, msg): 35 | self.sender.send(msg.serialize()) 36 | 37 | def recv(self): 38 | data = self.receiver.recv() 39 | return Message.unserialize(data) 40 | 41 | def noop(self, *args, **kwargs): 42 | """ Used to link() greenlets to in order to be compatible with gevent 1.0 """ 43 | pass 44 | 45 | 46 | class Client(BaseSocket): 47 | def __init__(self, host, port): 48 | self.host = host 49 | context = zmq.Context() 50 | self.receiver = context.socket(zmq.PULL) 51 | try: 52 | self.receiver.connect("tcp://%s:%i" % (host, port + 1)) 53 | except: 54 | sys.exit(0) 55 | self.sender = context.socket(zmq.PUSH) 56 | try: 57 | self.sender.connect("tcp://%s:%i" % (host, port)) 58 | except: 59 | sys.exit(0) 60 | 61 | 62 | # 定义slave类 63 | class Slave(Client): 64 | def __init__(self, *args, **kwargs): 65 | super(Slave, self).__init__(*args, **kwargs) 66 | self.client_id = socket.gethostname() + "_" + md5( 67 | str(time() + random.randint(0, 10000)).encode('utf-8')).hexdigest() 68 | logger.info("Client id:%r" % self.client_id) 69 | self.state = STATE_INIT 70 | self.slave_num = 0 71 | self.file_name = '' 72 | self.cpu_num = ps.cpu_count() 73 | self.processes = [] 74 | self.greenlet = Group() 75 | # 加载gevent协程 76 | self.greenlet.spawn(self.worker).link_exception(callback=self.noop) 77 | self.greenlet.spawn(self.ready_loop).link_exception(callback=self.noop) 78 | def on_quitting(): 79 | self.send(Message("quit", None, self.client_id)) 80 | self.greenlet.kill(block=True) 81 | 82 | events.quitting += on_quitting 83 | 84 | # 消息收发循环,通过gevent协程加载 85 | def worker(self): 86 | while True: 87 | msg = self.recv() 88 | if msg.node_id == self.client_id: 89 | logger.info('Slave: Get new msg from master - [%s]' % msg.type) 90 | # 接收压测脚本保存到./script文件夹中 91 | if msg.type == "send_script": 92 | logger.info("Save script to file...") 93 | if not os.path.exists("./script/"): 94 | os.mkdir("./script/") 95 | self.file_name = os.path.join("./script/", msg.data["filename"]) 96 | with codecs.open(self.file_name, 'w', encoding='utf-8') as f: 97 | f.write(msg.data["script"]) 98 | logger.info("Script saved into file:%s" % self.file_name) 99 | # 运行locust压测进程,完成后返回成功启动的进程数给master 100 | elif msg.type == "run": 101 | if self.state != STATE_RUNNING: 102 | self.run_locusts(master_host=self.host, nums=msg.data["num"], file_name=msg.data["filename"]) 103 | if self.slave_num > 0: 104 | self.state = STATE_RUNNING 105 | logger.info("Client %s run OK!" % self.client_id) 106 | else: 107 | self.state = STATE_INIT 108 | self.send(Message("slave_num", self.slave_num, self.client_id)) 109 | # 停止locust压测进程并更新状态给master 110 | elif msg.type == "stop": 111 | logger.info("Client %s stopped!" % self.client_id) 112 | self.stop() 113 | #self.send(Message("client_ready", self.slave_num, self.client_id)) 114 | self.send(Message("slave_num", self.slave_num, self.client_id)) 115 | # 退出slave,当master退出时收到此消息 116 | elif msg.type == "quit": 117 | logger.info("Got quit message from master, shutting down...") 118 | self.stop() 119 | self.greenlet.kill(block=True) 120 | # 获取当前客户端的压测文件列表 121 | elif msg.type == "get_filelist": 122 | if os.path.exists("./script/"): 123 | file_list = [] 124 | for root, dirs, files in os.walk("./script/"): 125 | for f in files: 126 | if os.path.splitext(f)[1] == '.py': 127 | file_list.append(f) 128 | self.send(Message("file_list", file_list, self.client_id)) 129 | else: 130 | self.send(Message("file_list", None, self.client_id)) 131 | # 获取当前客户端的资源状态:包括IP, CPU,内存,网络 132 | elif msg.type == "get_psinfo": 133 | ip = socket.gethostbyname(socket.gethostname()) 134 | nets = ps.net_io_counters() 135 | sleep(1) 136 | nets1 = ps.net_io_counters() 137 | net = {'sent': nets1.bytes_sent / 1024, 'recv': nets1.bytes_recv / 1024, 138 | 'per_sec_sent': (nets1.bytes_sent - nets.bytes_sent) / 1024, 139 | 'per_sec_recv': (nets1.bytes_recv - nets.bytes_recv) / 1024} 140 | cpu_times = ps.cpu_percent(interval=0.1) 141 | cpu_logical_nums = ps.cpu_count() 142 | cpu_nums = ps.cpu_count(logical=False) 143 | cpu_freq = ps.cpu_freq() 144 | if cpu_freq is not None: 145 | cpu = {'num': cpu_nums, 'logical_num': cpu_logical_nums, 'percent': cpu_times, 146 | 'freq': {'current': cpu_freq.current, 'min': cpu_freq.min, 'max': cpu_freq.max}} 147 | else: 148 | cpu = {'num': cpu_nums, 'logical_num': cpu_logical_nums, 'percent': cpu_times, 149 | 'freq': {'current': 0, 'min': 0, 'max': 0}} 150 | mems = ps.virtual_memory() 151 | mem = {'total': mems.total / 1024 / 1024, 'available': mems.available / 1024 / 1024, 152 | 'percent': mems.percent} 153 | psinfo = {"cpu": cpu, "mem": mem, "net": net, "IP": ip} 154 | self.send(Message("psinfo", psinfo, self.client_id)) 155 | # 清除压测脚本文件夹 156 | elif msg.type == "clear_folder": 157 | if os.path.exists("./script/"): 158 | shutil.rmtree("./script") 159 | self.send(Message("clear_folder", None, self.client_id)) 160 | 161 | # 每分钟向master上报状态 162 | def ready_loop(self): 163 | while True: 164 | # 发送ready状态至master 165 | logger.info('Send ready to server!') 166 | self.send(Message("slave_ready", self.slave_num, self.client_id)) 167 | gevent.sleep(60) 168 | 169 | # 退出locust压测进程 170 | def stop(self): 171 | self.state = STATE_STOPPED 172 | self.slave_num = 0 173 | for p in self.processes: 174 | try: 175 | procs = p.children(recursive=True) 176 | for proc in procs: 177 | if platform.system() == "Windows": 178 | proc.send_signal(0) 179 | else: 180 | proc.terminate() 181 | proc.wait() 182 | if platform.system() == "Windows": 183 | p.send_signal(0) 184 | else: 185 | p.terminate() 186 | p.wait() 187 | except: 188 | pass 189 | logger.info("Quit a locust client process!") 190 | self.processes = [] 191 | 192 | # 运行locust压测进程 193 | def run_locusts(self, master_host, nums, file_name): 194 | # 设置压测进程数,不大于CPU逻辑核心数 195 | if int(nums) > self.cpu_num or int(nums) < 1: 196 | slave_num = self.cpu_num 197 | else: 198 | slave_num = int(nums) 199 | # 设置各压测进程的压测脚本,如果web端选择的小于进程数,则循环选择 200 | script_file = [] 201 | for i in range(slave_num): 202 | script_file.append(os.path.join('./script/', file_name[i % len(file_name)])) 203 | # 启动压测进程 204 | for i in range(slave_num): 205 | cmd = 'locust -f %s --slave --no-reset-stats --master-host=%s' % (script_file[i], master_host) 206 | print cmd 207 | p = ps.Popen(cmd, shell=True, stdout=None, stderr=None) 208 | self.processes.append(p) 209 | sleep(1) 210 | # 更新启动成功的压测进程列表 211 | proc = [] 212 | for p in self.processes: 213 | if p.poll() is None: 214 | proc.append(p) 215 | self.processes = proc 216 | self.slave_num = len(proc) 217 | 218 | 219 | def shutdown(code=0): 220 | logger.info("Shutting down (exit code %s), bye." % code) 221 | events.quitting.fire() 222 | sys.exit(code) 223 | 224 | 225 | # install SIGTERM handler 226 | def sig_term_handler(): 227 | logger.info("Got SIGTERM signal") 228 | shutdown(0) 229 | 230 | 231 | def slave(host=''): 232 | if host == '': 233 | host = socket.gethostbyname(socket.gethostname()) 234 | port = 6666 235 | client = Slave(host, port) 236 | logger.info('Slave is starting at %s:%i' % (host, port)) 237 | gevent.signal(signal.SIGTERM, sig_term_handler) 238 | try: 239 | client.greenlet.join() 240 | # gevent.joinall(client.greenlet.greenlets) 241 | code = 0 242 | shutdown(code=code) 243 | except KeyboardInterrupt as e: 244 | shutdown(1) 245 | 246 | 247 | if __name__ == '__main__': 248 | if len(sys.argv) > 1: 249 | slave(sys.argv[1]) 250 | else: 251 | slave(socket.gethostbyname(socket.gethostname())) 252 | 253 | -------------------------------------------------------------------------------- /hztest/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | r""" 3 | 本Locust压测调试系统分三块:web,master, slave; 4 | web(views.py): 用于对WEB交互的调试指令进行处理响应,把WEB操作通过命令队列(cmd_queue)发送给master去分发给相应的slave客户端执行,然后通过结果队列(result_queue)接收结果; 5 | master.py: 负责从web接收队列请求,并通过相应的slave处理; 6 | slave.py: 接收master的请求进行处理,包括:状态,资源,脚本文件,locust客户端启停, 并反馈结果; 7 | """ 8 | 9 | from django.shortcuts import render 10 | from .rpc.master import cmd_queue, result_queue, lock, logger 11 | import time 12 | import os.path 13 | import os 14 | from subprocess import Popen 15 | import psutil 16 | import platform 17 | 18 | locust_script = './hztest/rpc/script/internal-test.py' # Locust master script file 19 | script_filename = 'internal-test.py' # default script file name of client if don't select file from web 20 | locust_status = 'stop' 21 | p = None 22 | 23 | 24 | # 等待结果消息队列返回数据 25 | def wait_result(): 26 | retry = 0 27 | result = None 28 | while result_queue.empty(): 29 | time.sleep(1) 30 | retry = retry + 1 31 | if retry > 10: 32 | logger.info('Web server: retry max times, no clients response;') 33 | break 34 | logger.info('Web server: command response is none, get in next 1s...') 35 | while not result_queue.empty(): 36 | result = result_queue.get() 37 | return result 38 | 39 | 40 | # 刷新客户端列表 41 | def refresh_clients(): 42 | logger.info('Web server: send new command - [get_clients]') 43 | cmd_queue.put({'type': 'get_clients'}) 44 | clients = [] 45 | q = wait_result() 46 | if q: 47 | if q != '0': 48 | logger.info('Web server: command - [get_clients] - Got response!') 49 | print(q) 50 | clients = q 51 | return {'num': len(clients), 'clients': clients} 52 | 53 | 54 | # 发送压测脚本到客户端 55 | def send_script(client_id, filename, script): 56 | logger.info('Web server: send new command - [send_script(%s)]' % client_id) 57 | cmd_queue.put({'type': 'sent_script', 'client_id': client_id, 'filename': filename, 'script': script}) 58 | q = wait_result() 59 | if q: 60 | if q == 'OK': 61 | logger.info('Web server: command - [send_script(%s)] - Got response!' % client_id) 62 | return 'Script send to %s successful!' % client_id 63 | return 'Script send to %s failed!' % client_id 64 | 65 | 66 | # 启动客户端压测进程 67 | def run(client_id, num, filename): 68 | logger.info('Web server: send new command - [run locust slave(%s)]' % client_id) 69 | cmd_queue.put({'type': 'run', 'client_id': client_id, 'filename': filename, 'num': num}) 70 | q = wait_result() 71 | if q: 72 | if q == 'OK': 73 | logger.info('Web server: command - [run locust slave(%s)] - Got response!' % client_id) 74 | return 'Client %s run successful!' % client_id 75 | return 'Client %s run failed!' % client_id 76 | 77 | 78 | # 停止客户端压测进程 79 | def stop(client_id): 80 | logger.info('Web server: send new command - [stop locust slave(%s)]' % client_id) 81 | cmd_queue.put({'type': 'stop', 'client_id': client_id}) 82 | q = wait_result() 83 | if q: 84 | if q == 'None': 85 | logger.info('Web server: command - [stop locust slave(%s)] - Got response!' % client_id) 86 | return 'Client %s stop successful!' % client_id 87 | return 'Client %s stop failed!' % client_id 88 | 89 | 90 | # 获取客户端压测脚本文件列表 91 | def get_filelist(client_id): 92 | logger.info('Web server: send new command - [get file list(%s)]' % client_id) 93 | cmd_queue.put({'type': 'get_filelist', 'client_id': client_id}) 94 | q = wait_result() 95 | if q: 96 | if q['client_id'] == client_id: 97 | if q['file_list']: 98 | logger.info('Web server: command - [get file list(%s)] - Got response!' % client_id) 99 | return {'id': client_id, 'file_list': q['file_list']} 100 | return {'id': client_id, 'file_list': ''} 101 | 102 | 103 | # 获取客户端系统资源状态 104 | def get_psinfo(client_id): 105 | logger.info('Web server: send new command - [get ps info(%s)]' % client_id) 106 | cmd_queue.put({'type': 'get_psinfo', 'client_id': client_id}) 107 | q = wait_result() 108 | if q: 109 | if q['client_id'] == client_id: 110 | logger.info('Web server: command - [get ps info(%s)] - Got response!' % client_id) 111 | return {'id': client_id, 'psinfo': q['psinfo']} 112 | return {'id': client_id, 'psinfo': ''} 113 | 114 | 115 | # 清除客户端压测脚本文件夹 116 | def clear_folder(client_id): 117 | logger.info('Web server: send new command - [clear folder(%s)]' % client_id) 118 | cmd_queue.put({'type': 'clear_folder', 'client_id': client_id}) 119 | q = wait_result() 120 | if q: 121 | if q['client_id'] == client_id: 122 | if q['clear_folder'] == 'OK': 123 | logger.info('Web server: command - [clear folder(%s)] - Got response!' % client_id) 124 | return 'Client %s script folder cleared!' % client_id 125 | return 'Failed to clear client %s script folder!' % client_id 126 | 127 | 128 | # 参考停止子进程代码,暂时不用 129 | def reap_children(timeout=3): 130 | global p 131 | "Tries hard to terminate and ultimately kill all the children of this process." 132 | def on_terminate(proc): 133 | print("process {} terminated with exit code {}".format(proc, proc.returncode)) 134 | 135 | procs = p.children() 136 | # send SIGTERM 137 | for proc in procs: 138 | proc.terminate() 139 | gone, alive = psutil.wait_procs(procs, timeout=timeout, callback=on_terminate) 140 | if alive: 141 | # send SIGKILL 142 | for proc in alive: 143 | print("process {} survived SIGTERM; trying SIGKILL" % p) 144 | proc.kill() 145 | gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate) 146 | if alive: 147 | # give up 148 | for proc in alive: 149 | print("process {} survived SIGKILL; giving up" % proc) 150 | 151 | 152 | # Web交互处理 153 | def hello(request): 154 | # 使用进程锁每个WEB请求独占,规避多用户在WEB下操作时冲突 155 | with lock: 156 | global locust_status, p, locust_script, script_filename 157 | context = dict() 158 | context['filelist'] = [] 159 | context['hello'] = 'Clients list OK!' 160 | context['script'] = '' 161 | context['text'] = '' 162 | context['clients'] = refresh_clients() 163 | if request.method == 'POST': 164 | print(request.POST) 165 | if context['clients']['num'] > 0: 166 | for client in context['clients']['clients']: 167 | # 启动/停止压测Slave 168 | name = 'run%s' % client['id'] 169 | if name in request.POST: 170 | if request.POST.get(name, None) == 'ready': 171 | # 启动客户端压测进程 172 | file_select = 'fileselect%s' % client['id'] 173 | num = request.POST.get('num%s' % client['id'], None) 174 | if file_select in request.POST: 175 | file_name = request.POST.getlist(file_select) 176 | else: 177 | # 如果未选择压测脚本,则使用默认脚本名 178 | file_name = [script_filename] 179 | context['hello'] = run(client['id'], num, file_name) 180 | context['clients'] = refresh_clients() 181 | for c in context['clients']['clients']: 182 | if c['id'] == client['id']: 183 | if c['slave_num'] == 0: 184 | context['hello'] = 'Client %s run error, please check you script file %s is valid!' % (client['id'], file_name) 185 | else: 186 | context['hello'] = 'Client %s run OK, script file is %s!' % (client['id'], file_name) 187 | else: 188 | # 停止客户端压测进程 189 | context['hello'] = stop(client['id']) 190 | context['clients'] = refresh_clients() 191 | break 192 | # 获取客户端的脚本文件列表 193 | name = 'filelist%s' % client['id'] 194 | if name in request.POST: 195 | file_list = get_filelist(client['id']) 196 | if file_list['id'] == client['id']: 197 | context['filelist'].append(file_list) 198 | break 199 | # 清除客户端脚本文件夹 200 | name = 'clear%s' % client['id'] 201 | if name in request.POST: 202 | context['hello'] = clear_folder(client['id']) 203 | break 204 | # 发送压测脚本到客户端 205 | name = 'send%s' % client['id'] 206 | if name in request.POST: 207 | if request.FILES.get(client['id'], None): 208 | # 将所选脚本文件内容发给客户端,文件控件可以多选 209 | script_files = request.FILES.getlist(client['id'], None) 210 | for script_file in script_files: 211 | if os.path.splitext(script_file.name)[1] != '.py': 212 | context['hello'] = "File name should be end with .py!" 213 | else: 214 | script = script_file.read() 215 | context['hello'] = send_script(client['id'], script_file.name, script) 216 | elif 'text%s' % client['id'] in request.POST: 217 | # 将页面上编辑后的脚本内容发送给客户端,前提为未选择文件 218 | context['hello'] = send_script(client['id'], request.POST.get('filename', None), request.POST.get('text%s' % client['id'], None)) 219 | break 220 | # 编辑压测脚本(如果多选,则只编辑列表中最后一个文件) 221 | name = 'edit%s' % client['id'] 222 | if name in request.POST: 223 | if request.FILES.get(client['id'], None): 224 | script_file = request.FILES.get(client['id'], None) 225 | context['script'] = script_file.read() 226 | context['text'] = client['id'] 227 | context['filename'] = script_file.name 228 | break 229 | 230 | # 获取客户端系统资源利用率 231 | if request.POST.get('mon_clients', False): 232 | psinfo = [] 233 | for c in context['clients']['clients']: 234 | psinfo.append(get_psinfo(c['id'])) 235 | context['psinfos'] = psinfo 236 | context['mon_flag'] = "Checked" 237 | context['hello'] = "Clients's monitor data refresh OK!" 238 | else: 239 | context['mon_flag'] = "" 240 | 241 | # 启动/停止Locust Master 242 | if 'start_locust' in request.POST or 'stop_locust' in request.POST: 243 | if locust_status == 'run': 244 | # 停止Locust Master 245 | if p is not None: 246 | logger.info("Server: try to stop the locust master!") 247 | if p.poll() is None: 248 | try: 249 | # 先停止子进程再停止自身进程,Popen在容器中启动会有两个进程,一个是shell进程,另一个是应用进程 250 | procs = p.children(recursive=True) 251 | for proc in procs: 252 | if platform.system() == "Windows": 253 | proc.send_signal(0) 254 | else: 255 | proc.terminate() 256 | proc.wait() 257 | if platform.system() == "Windows": 258 | p.send_signal(0) 259 | else: 260 | p.terminate() 261 | p.wait() 262 | p = None 263 | logger.info("Server: locust master stopped!") 264 | except: 265 | pass 266 | if p is None: 267 | context['hello'] = 'Locust master has been stopped!' 268 | logger.info("Server: locust master is stopped!") 269 | locust_status = 'stop' 270 | # 通知各客户端停止压测 271 | for client in context['clients']['clients']: 272 | stop(client['id']) 273 | context['clients'] = refresh_clients() 274 | else: 275 | # 启动Locust的Master模式 276 | p = psutil.Popen('locust -f %s --master --no-reset-stats' % locust_script, shell=True, stdout=None, stderr=None) 277 | time.sleep(1) 278 | if p.poll() is not None: 279 | # 判断Locust进程是否启动失败并给出提示 280 | if p.poll() != 0: 281 | logger.info("Server: failed to start locus master...") 282 | context['hello'] = 'Failed to start locust master! Please check script file: %s' % locust_script 283 | p = None 284 | else: 285 | # 成功启动Locust的Master模式进程 286 | print("Locust Master process PID:{}".format(p.pid)) 287 | logger.info("Server: locust master is running...") 288 | context['hello'] = 'Locust master has been running!' 289 | locust_status = 'run' 290 | context['locust'] = locust_status 291 | host = request.get_host() 292 | context['host'] = host.split(':')[0] 293 | return render(request, 'hello.html', context) 294 | 295 | 296 | --------------------------------------------------------------------------------